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

+135 -14
+3
.env.example
··· 9 9 SLACK_USER_COOKIE=your-slack-cookie-here 10 10 SLACK_USER_TOKEN=your-user-token-here 11 11 12 + # Optional: Enable Cachet API for user lookups (recommended for better performance) 13 + CACHET_ENABLED=true 14 + 12 15 # IRC Configuration 13 16 IRC_NICK=slackbridge 14 17 NICKSERV_PASSWORD=your-nickserv-password-here
+46 -3
README.md
··· 13 13 bun dev 14 14 ``` 15 15 16 + To run tests: 17 + ```bash 18 + bun test 19 + ``` 20 + 16 21 ### Slack App Setup 17 22 18 23 1. Go to [api.slack.com/apps](https://api.slack.com/apps) and create a new app ··· 38 43 SLACK_USER_COOKIE=your-slack-cookie-here 39 44 SLACK_USER_TOKEN=your-user-token-here 40 45 46 + # Optional: Enable Cachet API for user lookups (recommended for better performance) 47 + CACHET_ENABLED=true 48 + 41 49 # IRC Configuration 42 50 IRC_NICK=slackbridge 43 51 NICKSERV_PASSWORD=your-nickserv-password-here ··· 75 83 **Using Bun REPL:** 76 84 ```bash 77 85 bun repl 78 - > import { channelMappings, userMappings } from "./src/db" 86 + > import { channelMappings, userMappings } from "./src/lib/db" 79 87 > channelMappings.create("C1234567890", "#general") 80 88 > userMappings.create("U1234567890", "myircnick") 81 89 > channelMappings.getAll() ··· 103 111 - IRC `/me` actions are displayed in a context block with the user's avatar 104 112 - Thread replies: Use `@xxxxx` (5-char thread ID) to reply to a Slack thread from IRC 105 113 - **Slack → IRC**: Messages from mapped Slack channels are sent to their corresponding IRC channels 106 - - Slack mentions are converted to mapped IRC nicks, or the display name from `<@U123|name>` format 114 + - User display names: Uses name from Slack event if available, otherwise Cachet API (if `CACHET_ENABLED=true`), then Slack API fallback 115 + - All lookups are cached locally (1 hour TTL) to reduce API calls 116 + - Slack mentions are converted to mapped IRC nicks, or looked up via the above priority 107 117 - Slack markdown is converted to IRC formatting codes 108 118 - File attachments are uploaded to Hack Club CDN and URLs are shared 109 119 - Thread messages are prefixed with `@xxxxx` (5-char thread ID) to show they're part of a thread 110 120 - First reply in a thread includes a quote of the parent message 111 121 - **User mappings** allow custom IRC nicknames for specific Slack users and enable proper mentions both ways 122 + - **Permissions**: Only channel creators, channel managers, or global admins can bridge/unbridge channels 112 123 113 124 #### Thread Support 114 125 ··· 120 131 - **IRC → Slack**: Reply to a thread by including the thread ID in your message 121 132 - Example: `@abc12 this is my reply` 122 133 - The bridge removes the `@xxxxx` prefix and sends your message to the correct thread 123 - - Thread IDs are unique per thread and persist across restarts 134 + - Thread IDs are unique per thread and persist across restarts (stored in SQLite) 124 135 125 136 The bridge ignores its own messages and bot messages to prevent loops. 137 + 138 + ### Architecture 139 + 140 + The bridge consists of several modules: 141 + 142 + - **`src/index.ts`** - Main application entry point with IRC/Slack event handlers 143 + - **`src/commands.ts`** - Slash command handlers for managing bridges 144 + - **`src/lib/db.ts`** - SQLite database layer for channel/user/thread mappings 145 + - **`src/lib/parser.ts`** - Bidirectional message formatting conversion (IRC ↔ Slack) 146 + - **`src/lib/mentions.ts`** - User mention conversion with Cachet integration 147 + - **`src/lib/threads.ts`** - Thread tracking and ID generation 148 + - **`src/lib/user-cache.ts`** - Cached Slack user info lookups (1-hour TTL) 149 + - **`src/lib/permissions.ts`** - Channel management permission checks 150 + - **`src/lib/avatars.ts`** - Stable avatar URL generation for IRC users 151 + - **`src/lib/cdn.ts`** - File upload integration with Hack Club CDN 152 + - **`src/lib/cachet.ts`** - User profile lookups from Cachet API 153 + 154 + ### Testing 155 + 156 + The project includes comprehensive unit tests covering all core functionality: 157 + 158 + ```bash 159 + bun test 160 + ``` 161 + 162 + Tests cover: 163 + - Message format parsing (IRC ↔ Slack) 164 + - User mention conversion 165 + - Thread ID generation and tracking 166 + - User info caching 167 + - Database operations 168 + - Avatar generation 126 169 127 170 If you want to report an issue the main repo is [the tangled repo](https://tangled.org/dunkirk.sh/irc-slack-bridge) and the github is just a mirror. 128 171
+11 -1
src/index.ts
··· 333 333 } 334 334 335 335 try { 336 - const userInfo = await getUserInfo(payload.user, slackClient); 336 + // Get display name from payload if available, otherwise fetch from API 337 + const displayNameFromEvent = 338 + (payload as any).user_profile?.display_name || 339 + (payload as any).user_profile?.real_name || 340 + (payload as any).username; 341 + 342 + const userInfo = await getUserInfo( 343 + payload.user, 344 + slackClient, 345 + displayNameFromEvent, 346 + ); 337 347 338 348 // Check for user mapping, otherwise use Slack name 339 349 const userMapping = userMappings.getBySlackUser(payload.user);
+14 -8
src/lib/mentions.ts
··· 40 40 } 41 41 42 42 /** 43 - * Converts Slack user mentions to IRC @mentions, with Cachet fallback 43 + * Converts Slack user mentions to IRC @mentions 44 + * Priority: user mappings > display name from mention > Cachet lookup 44 45 */ 45 46 export async function convertSlackMentionsToIrc( 46 47 messageText: string, ··· 57 58 const mentionedUserMapping = userMappings.getBySlackUser(userId); 58 59 if (mentionedUserMapping) { 59 60 result = result.replace(match[0], `@${mentionedUserMapping.irc_nick}`); 60 - } else if (displayName) { 61 - // Use the display name from the mention format <@U123|name> 62 - result = result.replace(match[0], `@${displayName}`); 63 61 } else { 64 - // Fallback to Cachet lookup 65 - const data = await getCachetUser(userId); 66 - if (data) { 67 - result = result.replace(match[0], `@${data.displayName}`); 62 + // Try Cachet lookup if enabled 63 + if (process.env.CACHET_ENABLED === "true") { 64 + const data = await getCachetUser(userId); 65 + if (data) { 66 + result = result.replace(match[0], `@${data.displayName}`); 67 + continue; 68 + } 69 + } 70 + 71 + // Fallback to display name from the mention format <@U123|name> 72 + if (displayName) { 73 + result = result.replace(match[0], `@${displayName}`); 68 74 } 69 75 } 70 76 }
+23 -1
src/lib/user-cache.test.ts
··· 20 20 }); 21 21 22 22 describe("getUserInfo", () => { 23 + test("uses display name from event if provided", async () => { 24 + const client = { 25 + users: { 26 + info: mock(async () => ({ 27 + user: { 28 + name: "testuser", 29 + real_name: "Test User", 30 + }, 31 + })), 32 + }, 33 + }; 34 + 35 + const result = await getUserInfo("U123", client, "Event Display Name"); 36 + 37 + expect(result).toEqual({ 38 + name: "Event Display Name", 39 + realName: "Event Display Name", 40 + }); 41 + // Should not call API when display name provided 42 + expect(client.users.info).toHaveBeenCalledTimes(0); 43 + }); 44 + 23 45 test("fetches user info from Slack on cache miss", async () => { 24 46 const client = { 25 47 users: { ··· 32 54 }, 33 55 }; 34 56 35 - const result = await getUserInfo("U123", client); 57 + const result = await getUserInfo("U125", client); 36 58 37 59 expect(result).toEqual({ 38 60 name: "testuser",
+38 -1
src/lib/user-cache.ts
··· 1 + import { getCachetUser } from "./cachet"; 2 + 1 3 interface CachedUserInfo { 2 4 name: string; 3 5 realName: string; ··· 19 21 const CACHE_TTL = 60 * 60 * 1000; // 1 hour 20 22 21 23 /** 22 - * Get user info from cache or fetch from Slack 24 + * Get user info from cache or fetch from Cachet (if enabled) or Slack API 25 + * If displayName is provided (from Slack event), use that directly and cache it 23 26 */ 24 27 export async function getUserInfo( 25 28 userId: string, 26 29 slackClient: SlackClient, 30 + displayName?: string, 27 31 ): Promise<{ name: string; realName: string } | null> { 28 32 const cached = userCache.get(userId); 29 33 const now = Date.now(); ··· 32 36 return { name: cached.name, realName: cached.realName }; 33 37 } 34 38 39 + // If we have a display name from the event, use it directly 40 + if (displayName) { 41 + userCache.set(userId, { 42 + name: displayName, 43 + realName: displayName, 44 + timestamp: now, 45 + }); 46 + 47 + return { name: displayName, realName: displayName }; 48 + } 49 + 50 + // Try Cachet first if enabled (it has its own caching) 51 + if (process.env.CACHET_ENABLED === "true") { 52 + try { 53 + const cachetUser = await getCachetUser(userId); 54 + if (cachetUser) { 55 + const name = cachetUser.displayName || "Unknown"; 56 + const realName = cachetUser.displayName || "Unknown"; 57 + 58 + userCache.set(userId, { 59 + name, 60 + realName, 61 + timestamp: now, 62 + }); 63 + 64 + return { name, realName }; 65 + } 66 + } catch (error) { 67 + console.error(`Error fetching user from Cachet for ${userId}:`, error); 68 + } 69 + } 70 + 71 + // Fallback to Slack API 35 72 try { 36 73 const userInfo = await slackClient.users.info({ 37 74 token: process.env.SLACK_BOT_TOKEN,