this repo has no description

feat: cache user lookups

dunkirk.sh 7af20c9e bf2dbec6

verified
+4 -1
biome.json
···
"linter": {
"enabled": true,
"rules": {
-
"recommended": true
+
"recommended": true,
+
"suspicious": {
+
"noControlCharactersInRegex": "off"
+
}
}
},
"javascript": {
+4 -4
src/commands.ts
···
import type { AnyMessageBlock } from "slack-edge";
-
import { channelMappings, userMappings } from "./lib/db";
import { ircClient, slackApp } from "./index";
+
import { channelMappings, userMappings } from "./lib/db";
import { canManageChannel } from "./lib/permissions";
export function registerCommands() {
···
context.respond({
response_type: "ephemeral",
-
text: "Are you sure you want to remove the bridge to *${mapping.irc_channel}*?",
+
text: `Are you sure you want to remove the bridge to *${mapping.irc_channel}*?`,
blocks: [
{
type: "section",
···
context.respond({
response_type: "ephemeral",
-
text: "Are you sure you want to remove your link to IRC nick *${mapping.irc_nick}*?",
+
text: `Are you sure you want to remove your link to IRC nick *${mapping.irc_nick}*?`,
blocks: [
{
type: "section",
···
});
// List channel mappings
-
slackApp.command("/irc-bridge-list", async ({ payload, context }) => {
+
slackApp.command("/irc-bridge-list", async ({ context }) => {
const channelMaps = channelMappings.getAll();
const userMaps = userMappings.getAll();
+19 -14
src/index.ts
···
import { SlackApp } from "slack-edge";
import { version } from "../package.json";
import { registerCommands } from "./commands";
-
import { channelMappings, userMappings } from "./lib/db";
import { getAvatarForNick } from "./lib/avatars";
import { uploadToCDN } from "./lib/cdn";
+
import { channelMappings, userMappings } from "./lib/db";
import {
convertIrcMentionsToSlack,
convertSlackMentionsToIrc,
···
isFirstThreadMessage,
updateThreadTimestamp,
} from "./lib/threads";
+
import { cleanupUserCache, getUserInfo } from "./lib/user-cache";
const missingEnvVars = [];
if (!process.env.SLACK_BOT_TOKEN) missingEnvVars.push("SLACK_BOT_TOKEN");
···
registerCommands();
// Periodic cleanup of old thread timestamps (every hour)
-
setInterval(() => {
-
cleanupOldThreads();
-
}, 60 * 60 * 1000);
+
setInterval(
+
() => {
+
cleanupOldThreads();
+
cleanupUserCache();
+
},
+
60 * 60 * 1000,
+
);
// Track NickServ authentication state
let nickServAuthAttempted = false;
-
let isAuthenticated = false;
+
let _isAuthenticated = false;
// Join all mapped IRC channels on connect
ircClient.addListener("registered", async () => {
···
// Handle NickServ notices
ircClient.addListener(
"notice",
-
async (nick: string, to: string, text: string) => {
+
async (nick: string, _to: string, text: string) => {
if (nick !== "NickServ") return;
console.log(`NickServ: ${text}`);
···
text.includes("Password accepted")
) {
console.log("✓ Successfully authenticated with NickServ");
-
isAuthenticated = true;
+
_isAuthenticated = true;
// Join channels after successful auth
const mappings = channelMappings.getAll();
···
if (threadMatch) {
const threadId = threadMatch[1];
const threadInfo = getThreadByThreadId(threadId);
-
if (threadInfo && threadInfo.slack_channel_id === mapping.slack_channel_id) {
+
if (
+
threadInfo &&
+
threadInfo.slack_channel_id === mapping.slack_channel_id
+
) {
threadTs = threadInfo.thread_ts;
// Remove the @xxxxx from the message
messageText = messageText.replace(threadMentionPattern, "").trim();
···
}
try {
-
const userInfo = await slackClient.users.info({
-
token: process.env.SLACK_BOT_TOKEN,
-
user: payload.user,
-
});
+
const userInfo = await getUserInfo(payload.user, slackClient);
// Check for user mapping, otherwise use Slack name
const userMapping = userMappings.getBySlackUser(payload.user);
const username =
userMapping?.irc_nick ||
-
userInfo.user?.real_name ||
-
userInfo.user?.name ||
+
userInfo?.realName ||
+
userInfo?.name ||
"Unknown";
// Parse Slack mentions and replace with IRC nicks or display names
+6 -1
src/lib/db.ts
···
.get(threadId) as ThreadInfo | null;
},
-
update(threadTs: string, threadId: string, slackChannelId: string, timestamp: number): void {
+
update(
+
threadTs: string,
+
threadId: string,
+
slackChannelId: string,
+
timestamp: number,
+
): void {
db.run(
"INSERT OR REPLACE INTO thread_timestamps (thread_ts, thread_id, slack_channel_id, last_message_time) VALUES (?, ?, ?, ?)",
[threadTs, threadId, slackChannelId, timestamp],
+1 -1
src/lib/mentions.ts
···
-
import { userMappings } from "./db";
import { getCachetUser } from "./cachet";
+
import { userMappings } from "./db";
/**
* Converts IRC @mentions and nick: mentions to Slack user mentions
+1 -1
src/lib/parser.ts
···
parsed = parsed.replace(/<!date\^[0-9]+\^[^|]+\|([^>]+)>/g, "$1");
// Replace Slack bold *text* with IRC bold \x02text\x02
-
parsed = parsed.replace(/\*((?:[^\*]|\\\*)+)\*/g, "\x02$1\x02");
+
parsed = parsed.replace(/\*((?:[^*]|\\\*)+)\*/g, "\x02$1\x02");
// Replace Slack italic _text_ with IRC italic \x1Dtext\x1D
parsed = parsed.replace(/_((?:[^_]|\\_)+)_/g, "\x1D$1\x1D");
+67
src/lib/user-cache.ts
···
+
interface CachedUserInfo {
+
name: string;
+
realName: string;
+
timestamp: number;
+
}
+
+
interface SlackClient {
+
users: {
+
info: (params: { token: string; user: string }) => Promise<{
+
user?: {
+
name?: string;
+
real_name?: string;
+
};
+
}>;
+
};
+
}
+
+
const userCache = new Map<string, CachedUserInfo>();
+
const CACHE_TTL = 60 * 60 * 1000; // 1 hour
+
+
/**
+
* Get user info from cache or fetch from Slack
+
*/
+
export async function getUserInfo(
+
userId: string,
+
slackClient: SlackClient,
+
): Promise<{ name: string; realName: string } | null> {
+
const cached = userCache.get(userId);
+
const now = Date.now();
+
+
if (cached && now - cached.timestamp < CACHE_TTL) {
+
return { name: cached.name, realName: cached.realName };
+
}
+
+
try {
+
const userInfo = await slackClient.users.info({
+
token: process.env.SLACK_BOT_TOKEN,
+
user: userId,
+
});
+
+
const name = userInfo.user?.name || "Unknown";
+
const realName = userInfo.user?.real_name || name;
+
+
userCache.set(userId, {
+
name,
+
realName,
+
timestamp: now,
+
});
+
+
return { name, realName };
+
} catch (error) {
+
console.error(`Error fetching user info for ${userId}:`, error);
+
return null;
+
}
+
}
+
+
/**
+
* Clear expired entries from cache
+
*/
+
export function cleanupUserCache(): void {
+
const now = Date.now();
+
for (const [userId, info] of userCache.entries()) {
+
if (now - info.timestamp > CACHE_TTL) {
+
userCache.delete(userId);
+
}
+
}
+
}