a cache for slack profile pictures and emojis
1import { createHmac, timingSafeEqual } from "node:crypto"; 2import type { 3 SlackEmojiListResponse, 4 SlackUser, 5 SlackUserInfoResponse, 6} from "./slack"; 7 8/** 9 * Interface for mapping emoji names to their URLs 10 */ 11interface SlackEmoji { 12 [key: string]: string; 13} 14 15/** 16 * Configuration options for initializing the SlackWrapper 17 */ 18interface SlackConfig { 19 /** Slack signing secret for request verification */ 20 signingSecret?: string; 21 /** Slack bot user OAuth token */ 22 botToken?: string; 23} 24 25/** 26 * Wrapper class for Slack API interactions 27 */ 28class SlackWrapper { 29 private signingSecret: string; 30 private botToken: string; 31 32 /** 33 * Creates a new SlackWrapper instance 34 * @param config Optional configuration object containing signing secret and bot token 35 * @throws Error if required credentials are missing 36 */ 37 constructor(config?: SlackConfig) { 38 this.signingSecret = 39 config?.signingSecret || process.env.SLACK_SIGNING_SECRET || ""; 40 this.botToken = config?.botToken || process.env.SLACK_BOT_TOKEN || ""; 41 42 const missingFields = []; 43 if (!this.signingSecret) missingFields.push("signing secret"); 44 if (!this.botToken) missingFields.push("bot token"); 45 46 if (missingFields.length > 0) { 47 throw new Error( 48 `Missing required Slack credentials: ${missingFields.join(" and ")} either pass them to the class or set them as environment variables`, 49 ); 50 } 51 } 52 53 /** 54 * Tests authentication with current credentials 55 * @returns Promise resolving to true if auth is valid 56 * @throws Error if authentication fails 57 */ 58 async testAuth(): Promise<boolean> { 59 const response = await fetch("https://slack.com/api/auth.test", { 60 headers: { 61 Authorization: `Bearer ${this.botToken}`, 62 "Content-Type": "application/json", 63 }, 64 }); 65 66 const data = await response.json(); 67 if (!data.ok) { 68 throw new Error(`Authentication failed: ${data.error}`); 69 } 70 71 return true; 72 } 73 74 /** 75 * Retrieves information about a Slack user 76 * @param userId The ID of the user to look up 77 * @returns Promise resolving to the user's information 78 * @throws Error if the API request fails 79 */ 80 async getUserInfo(userId: string): Promise<SlackUser> { 81 const response = await fetch( 82 `https://slack.com/api/users.info?user=${userId}`, 83 { 84 method: "POST", 85 headers: { 86 Authorization: `Bearer ${this.botToken}`, 87 "Content-Type": "application/json", 88 }, 89 body: JSON.stringify({ user: userId }), 90 }, 91 ); 92 93 const data: SlackUserInfoResponse = await response.json(); 94 if ((!data.ok && data.error !== "user_not_found") || !data.user) { 95 throw new Error(data.error); 96 } 97 98 return data.user; 99 } 100 101 /** 102 * Retrieves the list of custom emojis from the Slack workspace 103 * @returns Promise resolving to the emoji list 104 * @throws Error if the API request fails 105 */ 106 async getEmojiList(): Promise<Record<string, string>> { 107 const response = await fetch("https://slack.com/api/emoji.list", { 108 headers: { 109 Authorization: `Bearer ${this.botToken}`, 110 "Content-Type": "application/json", 111 }, 112 }); 113 114 const data: SlackEmojiListResponse = await response.json(); 115 if (!data.ok || !data.emoji) { 116 throw new Error(`Failed to get emoji list: ${data.error}`); 117 } 118 119 return data.emoji; 120 } 121 122 /** 123 * Verifies a Slack request signature 124 * @param signature The signature from the request header 125 * @param timestamp The timestamp from the request header 126 * @param body The raw request body 127 * @returns boolean indicating if the signature is valid 128 */ 129 verifySignature(signature: string, timestamp: string, body: string): boolean { 130 const baseString = `v0:${timestamp}:${body}`; 131 const hmac = createHmac("sha256", this.signingSecret); 132 const computedSignature = `v0=${hmac.update(baseString).digest("hex")}`; 133 return timingSafeEqual( 134 Buffer.from(signature).valueOf() as Uint8Array, 135 Buffer.from(computedSignature).valueOf() as Uint8Array, 136 ); 137 } 138} 139 140export { SlackWrapper };