···99SLACK_USER_COOKIE=your-slack-cookie-here
1010SLACK_USER_TOKEN=your-user-token-here
11111212+# Optional: Enable Cachet API for user lookups (recommended for better performance)
1313+CACHET_ENABLED=true
1414+1215# IRC Configuration
1316IRC_NICK=slackbridge
1417NICKSERV_PASSWORD=your-nickserv-password-here
+46-3
README.md
···1313bun dev
1414```
15151616+To run tests:
1717+```bash
1818+bun test
1919+```
2020+1621### Slack App Setup
172218231. Go to [api.slack.com/apps](https://api.slack.com/apps) and create a new app
···3843SLACK_USER_COOKIE=your-slack-cookie-here
3944SLACK_USER_TOKEN=your-user-token-here
40454646+# Optional: Enable Cachet API for user lookups (recommended for better performance)
4747+CACHET_ENABLED=true
4848+4149# IRC Configuration
4250IRC_NICK=slackbridge
4351NICKSERV_PASSWORD=your-nickserv-password-here
···7583**Using Bun REPL:**
7684```bash
7785bun repl
7878-> import { channelMappings, userMappings } from "./src/db"
8686+> import { channelMappings, userMappings } from "./src/lib/db"
7987> channelMappings.create("C1234567890", "#general")
8088> userMappings.create("U1234567890", "myircnick")
8189> channelMappings.getAll()
···103111 - IRC `/me` actions are displayed in a context block with the user's avatar
104112 - Thread replies: Use `@xxxxx` (5-char thread ID) to reply to a Slack thread from IRC
105113- **Slack → IRC**: Messages from mapped Slack channels are sent to their corresponding IRC channels
106106- - Slack mentions are converted to mapped IRC nicks, or the display name from `<@U123|name>` format
114114+ - User display names: Uses name from Slack event if available, otherwise Cachet API (if `CACHET_ENABLED=true`), then Slack API fallback
115115+ - All lookups are cached locally (1 hour TTL) to reduce API calls
116116+ - Slack mentions are converted to mapped IRC nicks, or looked up via the above priority
107117 - Slack markdown is converted to IRC formatting codes
108118 - File attachments are uploaded to Hack Club CDN and URLs are shared
109119 - Thread messages are prefixed with `@xxxxx` (5-char thread ID) to show they're part of a thread
110120 - First reply in a thread includes a quote of the parent message
111121- **User mappings** allow custom IRC nicknames for specific Slack users and enable proper mentions both ways
122122+- **Permissions**: Only channel creators, channel managers, or global admins can bridge/unbridge channels
112123113124#### Thread Support
114125···120131- **IRC → Slack**: Reply to a thread by including the thread ID in your message
121132 - Example: `@abc12 this is my reply`
122133 - The bridge removes the `@xxxxx` prefix and sends your message to the correct thread
123123- - Thread IDs are unique per thread and persist across restarts
134134+ - Thread IDs are unique per thread and persist across restarts (stored in SQLite)
124135125136The bridge ignores its own messages and bot messages to prevent loops.
137137+138138+### Architecture
139139+140140+The bridge consists of several modules:
141141+142142+- **`src/index.ts`** - Main application entry point with IRC/Slack event handlers
143143+- **`src/commands.ts`** - Slash command handlers for managing bridges
144144+- **`src/lib/db.ts`** - SQLite database layer for channel/user/thread mappings
145145+- **`src/lib/parser.ts`** - Bidirectional message formatting conversion (IRC ↔ Slack)
146146+- **`src/lib/mentions.ts`** - User mention conversion with Cachet integration
147147+- **`src/lib/threads.ts`** - Thread tracking and ID generation
148148+- **`src/lib/user-cache.ts`** - Cached Slack user info lookups (1-hour TTL)
149149+- **`src/lib/permissions.ts`** - Channel management permission checks
150150+- **`src/lib/avatars.ts`** - Stable avatar URL generation for IRC users
151151+- **`src/lib/cdn.ts`** - File upload integration with Hack Club CDN
152152+- **`src/lib/cachet.ts`** - User profile lookups from Cachet API
153153+154154+### Testing
155155+156156+The project includes comprehensive unit tests covering all core functionality:
157157+158158+```bash
159159+bun test
160160+```
161161+162162+Tests cover:
163163+- Message format parsing (IRC ↔ Slack)
164164+- User mention conversion
165165+- Thread ID generation and tracking
166166+- User info caching
167167+- Database operations
168168+- Avatar generation
126169127170If 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.
128171
+11-1
src/index.ts
···333333 }
334334335335 try {
336336- const userInfo = await getUserInfo(payload.user, slackClient);
336336+ // Get display name from payload if available, otherwise fetch from API
337337+ const displayNameFromEvent =
338338+ (payload as any).user_profile?.display_name ||
339339+ (payload as any).user_profile?.real_name ||
340340+ (payload as any).username;
341341+342342+ const userInfo = await getUserInfo(
343343+ payload.user,
344344+ slackClient,
345345+ displayNameFromEvent,
346346+ );
337347338348 // Check for user mapping, otherwise use Slack name
339349 const userMapping = userMappings.getBySlackUser(payload.user);
+14-8
src/lib/mentions.ts
···4040}
41414242/**
4343- * Converts Slack user mentions to IRC @mentions, with Cachet fallback
4343+ * Converts Slack user mentions to IRC @mentions
4444+ * Priority: user mappings > display name from mention > Cachet lookup
4445 */
4546export async function convertSlackMentionsToIrc(
4647 messageText: string,
···5758 const mentionedUserMapping = userMappings.getBySlackUser(userId);
5859 if (mentionedUserMapping) {
5960 result = result.replace(match[0], `@${mentionedUserMapping.irc_nick}`);
6060- } else if (displayName) {
6161- // Use the display name from the mention format <@U123|name>
6262- result = result.replace(match[0], `@${displayName}`);
6361 } else {
6464- // Fallback to Cachet lookup
6565- const data = await getCachetUser(userId);
6666- if (data) {
6767- result = result.replace(match[0], `@${data.displayName}`);
6262+ // Try Cachet lookup if enabled
6363+ if (process.env.CACHET_ENABLED === "true") {
6464+ const data = await getCachetUser(userId);
6565+ if (data) {
6666+ result = result.replace(match[0], `@${data.displayName}`);
6767+ continue;
6868+ }
6969+ }
7070+7171+ // Fallback to display name from the mention format <@U123|name>
7272+ if (displayName) {
7373+ result = result.replace(match[0], `@${displayName}`);
6874 }
6975 }
7076 }
+23-1
src/lib/user-cache.test.ts
···2020 });
21212222 describe("getUserInfo", () => {
2323+ test("uses display name from event if provided", async () => {
2424+ const client = {
2525+ users: {
2626+ info: mock(async () => ({
2727+ user: {
2828+ name: "testuser",
2929+ real_name: "Test User",
3030+ },
3131+ })),
3232+ },
3333+ };
3434+3535+ const result = await getUserInfo("U123", client, "Event Display Name");
3636+3737+ expect(result).toEqual({
3838+ name: "Event Display Name",
3939+ realName: "Event Display Name",
4040+ });
4141+ // Should not call API when display name provided
4242+ expect(client.users.info).toHaveBeenCalledTimes(0);
4343+ });
4444+2345 test("fetches user info from Slack on cache miss", async () => {
2446 const client = {
2547 users: {
···3254 },
3355 };
34563535- const result = await getUserInfo("U123", client);
5757+ const result = await getUserInfo("U125", client);
36583759 expect(result).toEqual({
3860 name: "testuser",
+38-1
src/lib/user-cache.ts
···11+import { getCachetUser } from "./cachet";
22+13interface CachedUserInfo {
24 name: string;
35 realName: string;
···1921const CACHE_TTL = 60 * 60 * 1000; // 1 hour
20222123/**
2222- * Get user info from cache or fetch from Slack
2424+ * Get user info from cache or fetch from Cachet (if enabled) or Slack API
2525+ * If displayName is provided (from Slack event), use that directly and cache it
2326 */
2427export async function getUserInfo(
2528 userId: string,
2629 slackClient: SlackClient,
3030+ displayName?: string,
2731): Promise<{ name: string; realName: string } | null> {
2832 const cached = userCache.get(userId);
2933 const now = Date.now();
···3236 return { name: cached.name, realName: cached.realName };
3337 }
34383939+ // If we have a display name from the event, use it directly
4040+ if (displayName) {
4141+ userCache.set(userId, {
4242+ name: displayName,
4343+ realName: displayName,
4444+ timestamp: now,
4545+ });
4646+4747+ return { name: displayName, realName: displayName };
4848+ }
4949+5050+ // Try Cachet first if enabled (it has its own caching)
5151+ if (process.env.CACHET_ENABLED === "true") {
5252+ try {
5353+ const cachetUser = await getCachetUser(userId);
5454+ if (cachetUser) {
5555+ const name = cachetUser.displayName || "Unknown";
5656+ const realName = cachetUser.displayName || "Unknown";
5757+5858+ userCache.set(userId, {
5959+ name,
6060+ realName,
6161+ timestamp: now,
6262+ });
6363+6464+ return { name, realName };
6565+ }
6666+ } catch (error) {
6767+ console.error(`Error fetching user from Cachet for ${userId}:`, error);
6868+ }
6969+ }
7070+7171+ // Fallback to Slack API
3572 try {
3673 const userInfo = await slackClient.users.info({
3774 token: process.env.SLACK_BOT_TOKEN,