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 };