this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: cache user lookups

+102 -22
+4 -1
biome.json
··· 15 15 "linter": { 16 16 "enabled": true, 17 17 "rules": { 18 - "recommended": true 18 + "recommended": true, 19 + "suspicious": { 20 + "noControlCharactersInRegex": "off" 21 + } 19 22 } 20 23 }, 21 24 "javascript": {
+4 -4
src/commands.ts
··· 1 1 import type { AnyMessageBlock } from "slack-edge"; 2 - import { channelMappings, userMappings } from "./lib/db"; 3 2 import { ircClient, slackApp } from "./index"; 3 + import { channelMappings, userMappings } from "./lib/db"; 4 4 import { canManageChannel } from "./lib/permissions"; 5 5 6 6 export function registerCommands() { ··· 137 137 138 138 context.respond({ 139 139 response_type: "ephemeral", 140 - text: "Are you sure you want to remove the bridge to *${mapping.irc_channel}*?", 140 + text: `Are you sure you want to remove the bridge to *${mapping.irc_channel}*?`, 141 141 blocks: [ 142 142 { 143 143 type: "section", ··· 313 313 314 314 context.respond({ 315 315 response_type: "ephemeral", 316 - text: "Are you sure you want to remove your link to IRC nick *${mapping.irc_nick}*?", 316 + text: `Are you sure you want to remove your link to IRC nick *${mapping.irc_nick}*?`, 317 317 blocks: [ 318 318 { 319 319 type: "section", ··· 400 400 }); 401 401 402 402 // List channel mappings 403 - slackApp.command("/irc-bridge-list", async ({ payload, context }) => { 403 + slackApp.command("/irc-bridge-list", async ({ context }) => { 404 404 const channelMaps = channelMappings.getAll(); 405 405 const userMaps = userMappings.getAll(); 406 406
+19 -14
src/index.ts
··· 2 2 import { SlackApp } from "slack-edge"; 3 3 import { version } from "../package.json"; 4 4 import { registerCommands } from "./commands"; 5 - import { channelMappings, userMappings } from "./lib/db"; 6 5 import { getAvatarForNick } from "./lib/avatars"; 7 6 import { uploadToCDN } from "./lib/cdn"; 7 + import { channelMappings, userMappings } from "./lib/db"; 8 8 import { 9 9 convertIrcMentionsToSlack, 10 10 convertSlackMentionsToIrc, ··· 16 16 isFirstThreadMessage, 17 17 updateThreadTimestamp, 18 18 } from "./lib/threads"; 19 + import { cleanupUserCache, getUserInfo } from "./lib/user-cache"; 19 20 20 21 const missingEnvVars = []; 21 22 if (!process.env.SLACK_BOT_TOKEN) missingEnvVars.push("SLACK_BOT_TOKEN"); ··· 77 78 registerCommands(); 78 79 79 80 // Periodic cleanup of old thread timestamps (every hour) 80 - setInterval(() => { 81 - cleanupOldThreads(); 82 - }, 60 * 60 * 1000); 81 + setInterval( 82 + () => { 83 + cleanupOldThreads(); 84 + cleanupUserCache(); 85 + }, 86 + 60 * 60 * 1000, 87 + ); 83 88 84 89 // Track NickServ authentication state 85 90 let nickServAuthAttempted = false; 86 - let isAuthenticated = false; 91 + let _isAuthenticated = false; 87 92 88 93 // Join all mapped IRC channels on connect 89 94 ircClient.addListener("registered", async () => { ··· 113 118 // Handle NickServ notices 114 119 ircClient.addListener( 115 120 "notice", 116 - async (nick: string, to: string, text: string) => { 121 + async (nick: string, _to: string, text: string) => { 117 122 if (nick !== "NickServ") return; 118 123 119 124 console.log(`NickServ: ${text}`); ··· 124 129 text.includes("Password accepted") 125 130 ) { 126 131 console.log("✓ Successfully authenticated with NickServ"); 127 - isAuthenticated = true; 132 + _isAuthenticated = true; 128 133 129 134 // Join channels after successful auth 130 135 const mappings = channelMappings.getAll(); ··· 193 198 if (threadMatch) { 194 199 const threadId = threadMatch[1]; 195 200 const threadInfo = getThreadByThreadId(threadId); 196 - if (threadInfo && threadInfo.slack_channel_id === mapping.slack_channel_id) { 201 + if ( 202 + threadInfo && 203 + threadInfo.slack_channel_id === mapping.slack_channel_id 204 + ) { 197 205 threadTs = threadInfo.thread_ts; 198 206 // Remove the @xxxxx from the message 199 207 messageText = messageText.replace(threadMentionPattern, "").trim(); ··· 325 333 } 326 334 327 335 try { 328 - const userInfo = await slackClient.users.info({ 329 - token: process.env.SLACK_BOT_TOKEN, 330 - user: payload.user, 331 - }); 336 + const userInfo = await getUserInfo(payload.user, slackClient); 332 337 333 338 // Check for user mapping, otherwise use Slack name 334 339 const userMapping = userMappings.getBySlackUser(payload.user); 335 340 const username = 336 341 userMapping?.irc_nick || 337 - userInfo.user?.real_name || 338 - userInfo.user?.name || 342 + userInfo?.realName || 343 + userInfo?.name || 339 344 "Unknown"; 340 345 341 346 // Parse Slack mentions and replace with IRC nicks or display names
+6 -1
src/lib/db.ts
··· 127 127 .get(threadId) as ThreadInfo | null; 128 128 }, 129 129 130 - update(threadTs: string, threadId: string, slackChannelId: string, timestamp: number): void { 130 + update( 131 + threadTs: string, 132 + threadId: string, 133 + slackChannelId: string, 134 + timestamp: number, 135 + ): void { 131 136 db.run( 132 137 "INSERT OR REPLACE INTO thread_timestamps (thread_ts, thread_id, slack_channel_id, last_message_time) VALUES (?, ?, ?, ?)", 133 138 [threadTs, threadId, slackChannelId, timestamp],
+1 -1
src/lib/mentions.ts
··· 1 - import { userMappings } from "./db"; 2 1 import { getCachetUser } from "./cachet"; 2 + import { userMappings } from "./db"; 3 3 4 4 /** 5 5 * Converts IRC @mentions and nick: mentions to Slack user mentions
+1 -1
src/lib/parser.ts
··· 29 29 parsed = parsed.replace(/<!date\^[0-9]+\^[^|]+\|([^>]+)>/g, "$1"); 30 30 31 31 // Replace Slack bold *text* with IRC bold \x02text\x02 32 - parsed = parsed.replace(/\*((?:[^\*]|\\\*)+)\*/g, "\x02$1\x02"); 32 + parsed = parsed.replace(/\*((?:[^*]|\\\*)+)\*/g, "\x02$1\x02"); 33 33 34 34 // Replace Slack italic _text_ with IRC italic \x1Dtext\x1D 35 35 parsed = parsed.replace(/_((?:[^_]|\\_)+)_/g, "\x1D$1\x1D");
+67
src/lib/user-cache.ts
··· 1 + interface CachedUserInfo { 2 + name: string; 3 + realName: string; 4 + timestamp: number; 5 + } 6 + 7 + interface SlackClient { 8 + users: { 9 + info: (params: { token: string; user: string }) => Promise<{ 10 + user?: { 11 + name?: string; 12 + real_name?: string; 13 + }; 14 + }>; 15 + }; 16 + } 17 + 18 + const userCache = new Map<string, CachedUserInfo>(); 19 + const CACHE_TTL = 60 * 60 * 1000; // 1 hour 20 + 21 + /** 22 + * Get user info from cache or fetch from Slack 23 + */ 24 + export async function getUserInfo( 25 + userId: string, 26 + slackClient: SlackClient, 27 + ): Promise<{ name: string; realName: string } | null> { 28 + const cached = userCache.get(userId); 29 + const now = Date.now(); 30 + 31 + if (cached && now - cached.timestamp < CACHE_TTL) { 32 + return { name: cached.name, realName: cached.realName }; 33 + } 34 + 35 + try { 36 + const userInfo = await slackClient.users.info({ 37 + token: process.env.SLACK_BOT_TOKEN, 38 + user: userId, 39 + }); 40 + 41 + const name = userInfo.user?.name || "Unknown"; 42 + const realName = userInfo.user?.real_name || name; 43 + 44 + userCache.set(userId, { 45 + name, 46 + realName, 47 + timestamp: now, 48 + }); 49 + 50 + return { name, realName }; 51 + } catch (error) { 52 + console.error(`Error fetching user info for ${userId}:`, error); 53 + return null; 54 + } 55 + } 56 + 57 + /** 58 + * Clear expired entries from cache 59 + */ 60 + export function cleanupUserCache(): void { 61 + const now = Date.now(); 62 + for (const [userId, info] of userCache.entries()) { 63 + if (now - info.timestamp > CACHE_TTL) { 64 + userCache.delete(userId); 65 + } 66 + } 67 + }