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