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