a cache for slack profile pictures and emojis

feat: roll my own slack client

Changed files
+245
src
+105
src/slack.d.ts
···
···
+
/**
+
* Response from Slack's users.info API endpoint
+
*/
+
export interface SlackUserInfoResponse {
+
/** Whether the request was successful */
+
ok: boolean;
+
/** The user information if found */
+
user?: SlackUser;
+
/** Error message if request failed */
+
error?: string;
+
}
+
+
/**
+
* Response from Slack's emoji.list API endpoint
+
*/
+
export interface SlackEmojiListResponse {
+
/** Whether the request was successful */
+
ok: boolean;
+
/** Map of emoji names to their image URLs */
+
emoji?: Record<string, string>;
+
/** Error message if request failed */
+
error?: string;
+
}
+
+
/**
+
* A Slack user's information
+
*/
+
export interface SlackUser {
+
/** Unique identifier for the user */
+
id: string;
+
/** ID of the team the user belongs to */
+
team_id: string;
+
/** Username */
+
name: string;
+
/** Whether the user has been deactivated */
+
deleted: boolean;
+
/** User's color preference */
+
color: string;
+
/** User's full name */
+
real_name: string;
+
/** User's timezone identifier */
+
tz: string;
+
/** Display label for the timezone */
+
tz_label: string;
+
/** Timezone offset in seconds */
+
tz_offset: number;
+
/** Extended profile information */
+
profile: SlackUserProfile;
+
/** Whether user is a workspace admin */
+
is_admin: boolean;
+
/** Whether user is a workspace owner */
+
is_owner: boolean;
+
/** Whether user is the primary workspace owner */
+
is_primary_owner: boolean;
+
/** Whether user has restricted access */
+
is_restricted: boolean;
+
/** Whether user has ultra restricted access */
+
is_ultra_restricted: boolean;
+
/** Whether user is a bot */
+
is_bot: boolean;
+
/** Timestamp of last update to user info */
+
updated: number;
+
/** Whether user is an app user */
+
is_app_user: boolean;
+
/** Whether user has two-factor auth enabled */
+
has_2fa: boolean;
+
}
+
+
/**
+
* Extended profile information for a Slack user
+
*/
+
export interface SlackUserProfile {
+
/** Hash of user's profile picture */
+
avatar_hash: string;
+
/** User's status text */
+
status_text: string;
+
/** Emoji shown in user's status */
+
status_emoji: string;
+
/** User's full name */
+
real_name: string;
+
/** User's display name */
+
display_name: string;
+
/** Normalized version of real name */
+
real_name_normalized: string;
+
/** Normalized version of display name */
+
display_name_normalized: string;
+
/** User's email address */
+
email: string;
+
/** Original size profile image URL */
+
image_original: string;
+
/** 24x24 profile image URL */
+
image_24: string;
+
/** 32x32 profile image URL */
+
image_32: string;
+
/** 48x48 profile image URL */
+
image_48: string;
+
/** 72x72 profile image URL */
+
image_72: string;
+
/** 192x192 profile image URL */
+
image_192: string;
+
/** 512x512 profile image URL */
+
image_512: string;
+
/** Team ID the profile belongs to */
+
team: string;
+
}
+140
src/slackWrapper.ts
···
···
+
import { createHmac, timingSafeEqual } from "node:crypto";
+
import type {
+
SlackEmojiListResponse,
+
SlackUser,
+
SlackUserInfoResponse,
+
} from "./slack";
+
+
/**
+
* Interface for mapping emoji names to their URLs
+
*/
+
interface SlackEmoji {
+
[key: string]: string;
+
}
+
+
/**
+
* Configuration options for initializing the SlackWrapper
+
*/
+
interface SlackConfig {
+
/** Slack signing secret for request verification */
+
signingSecret?: string;
+
/** Slack bot user OAuth token */
+
botToken?: string;
+
}
+
+
/**
+
* Wrapper class for Slack API interactions
+
*/
+
class SlackWrapper {
+
private signingSecret: string;
+
private botToken: string;
+
+
/**
+
* Creates a new SlackWrapper instance
+
* @param config Optional configuration object containing signing secret and bot token
+
* @throws Error if required credentials are missing
+
*/
+
constructor(config?: SlackConfig) {
+
this.signingSecret =
+
config?.signingSecret || process.env.SLACK_SIGNING_SECRET || "";
+
this.botToken = config?.botToken || process.env.SLACK_BOT_TOKEN || "";
+
+
const missingFields = [];
+
if (!this.signingSecret) missingFields.push("signing secret");
+
if (!this.botToken) missingFields.push("bot token");
+
+
if (missingFields.length > 0) {
+
throw new Error(
+
`Missing required Slack credentials: ${missingFields.join(" and ")} either pass them to the class or set them as environment variables`,
+
);
+
}
+
}
+
+
/**
+
* Tests authentication with current credentials
+
* @returns Promise resolving to true if auth is valid
+
* @throws Error if authentication fails
+
*/
+
async testAuth(): Promise<boolean> {
+
const response = await fetch("https://slack.com/api/auth.test", {
+
headers: {
+
Authorization: `Bearer ${this.botToken}`,
+
"Content-Type": "application/json",
+
},
+
});
+
+
const data = await response.json();
+
if (!data.ok) {
+
throw new Error(`Authentication failed: ${data.error}`);
+
}
+
+
return true;
+
}
+
+
/**
+
* Retrieves information about a Slack user
+
* @param userId The ID of the user to look up
+
* @returns Promise resolving to the user's information
+
* @throws Error if the API request fails
+
*/
+
async getUserInfo(userId: string): Promise<SlackUser> {
+
const response = await fetch(
+
`https://slack.com/api/users.info?user=${userId}`,
+
{
+
method: "POST",
+
headers: {
+
Authorization: `Bearer ${this.botToken}`,
+
"Content-Type": "application/json",
+
},
+
body: JSON.stringify({ user: userId }),
+
},
+
);
+
+
const data: SlackUserInfoResponse = await response.json();
+
if ((!data.ok && data.error !== "user_not_found") || !data.user) {
+
throw new Error(data.error);
+
}
+
+
return data.user;
+
}
+
+
/**
+
* Retrieves the list of custom emojis from the Slack workspace
+
* @returns Promise resolving to the emoji list
+
* @throws Error if the API request fails
+
*/
+
async getEmojiList(): Promise<Record<string, string>> {
+
const response = await fetch("https://slack.com/api/emoji.list", {
+
headers: {
+
Authorization: `Bearer ${this.botToken}`,
+
"Content-Type": "application/json",
+
},
+
});
+
+
const data: SlackEmojiListResponse = await response.json();
+
if (!data.ok || !data.emoji) {
+
throw new Error(`Failed to get emoji list: ${data.error}`);
+
}
+
+
return data.emoji;
+
}
+
+
/**
+
* Verifies a Slack request signature
+
* @param signature The signature from the request header
+
* @param timestamp The timestamp from the request header
+
* @param body The raw request body
+
* @returns boolean indicating if the signature is valid
+
*/
+
verifySignature(signature: string, timestamp: string, body: string): boolean {
+
const baseString = `v0:${timestamp}:${body}`;
+
const hmac = createHmac("sha256", this.signingSecret);
+
const computedSignature = `v0=${hmac.update(baseString).digest("hex")}`;
+
return timingSafeEqual(
+
Buffer.from(signature).valueOf() as Uint8Array,
+
Buffer.from(computedSignature).valueOf() as Uint8Array,
+
);
+
}
+
}
+
+
export { SlackWrapper };