a cache for slack profile pictures and emojis
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 };