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