this repo has no description
1import * as irc from "irc";
2import { SlackApp } from "slack-edge";
3import { version } from "../package.json";
4import { registerCommands } from "./commands";
5import { channelMappings, userMappings } from "./db";
6import { parseIRCFormatting, parseSlackMarkdown } from "./parser";
7import type { CachetUser } from "./types";
8
9const missingEnvVars = [];
10if (!process.env.SLACK_BOT_TOKEN) missingEnvVars.push("SLACK_BOT_TOKEN");
11if (!process.env.SLACK_SIGNING_SECRET)
12 missingEnvVars.push("SLACK_SIGNING_SECRET");
13if (!process.env.ADMINS) missingEnvVars.push("ADMINS");
14if (!process.env.IRC_NICK) missingEnvVars.push("IRC_NICK");
15
16if (missingEnvVars.length > 0) {
17 throw new Error(
18 `Missing required environment variables: ${missingEnvVars.join(", ")}`,
19 );
20}
21
22const slackApp = new SlackApp({
23 env: {
24 SLACK_BOT_TOKEN: process.env.SLACK_BOT_TOKEN as string,
25 SLACK_SIGNING_SECRET: process.env.SLACK_SIGNING_SECRET as string,
26 SLACK_LOGGING_LEVEL: "INFO",
27 },
28 startLazyListenerAfterAck: true,
29});
30const slackClient = slackApp.client;
31
32// Get bot user ID
33let botUserId: string | undefined;
34slackClient.auth
35 .test({
36 token: process.env.SLACK_BOT_TOKEN,
37 })
38 .then((result) => {
39 botUserId = result.user_id;
40 console.log(`Bot user ID: ${botUserId}`);
41 });
42
43// IRC client setup
44const ircClient = new irc.Client(
45 "irc.hackclub.com",
46 process.env.IRC_NICK || "slackbridge",
47 {
48 port: 6667,
49 autoRejoin: true,
50 autoConnect: true,
51 channels: [],
52 secure: false,
53 userName: process.env.IRC_NICK,
54 realName: "Slack IRC Bridge",
55 },
56);
57
58// Clean up IRC connection on hot reload or exit
59process.on("beforeExit", () => {
60 ircClient.disconnect("Reloading", () => {
61 console.log("IRC client disconnected");
62 });
63});
64
65// Register slash commands
66registerCommands();
67
68// Join all mapped IRC channels on connect
69ircClient.addListener("registered", async () => {
70 console.log("Connected to IRC server");
71 const mappings = channelMappings.getAll();
72 for (const mapping of mappings) {
73 ircClient.join(mapping.irc_channel);
74 }
75});
76
77ircClient.addListener("join", (channel: string, nick: string) => {
78 if (nick === process.env.IRC_NICK) {
79 console.log(`Joined IRC channel: ${channel}`);
80 }
81});
82
83ircClient.addListener(
84 "message",
85 async (nick: string, to: string, text: string) => {
86 // Ignore messages from our own bot (with or without numbers suffix)
87 const botNickPattern = new RegExp(`^${process.env.IRC_NICK}\\d*$`);
88 if (botNickPattern.test(nick)) return;
89 if (nick === "****") return;
90
91 // Find Slack channel mapping for this IRC channel
92 const mapping = channelMappings.getByIrcChannel(to);
93 if (!mapping) return;
94
95 // Check if this IRC nick is mapped to a Slack user
96 const userMapping = userMappings.getByIrcNick(nick);
97
98 const displayName = `${nick} <irc>`;
99 let iconUrl: string | undefined;
100
101 if (userMapping) {
102 try {
103 iconUrl = `https://cachet.dunkirk.sh/users/${userMapping.slack_user_id}/r`;
104 } catch (error) {
105 console.error("Error fetching user info:", error);
106 }
107 }
108
109 try {
110 await slackClient.chat.postMessage({
111 token: process.env.SLACK_BOT_TOKEN,
112 channel: mapping.slack_channel_id,
113 text: parseIRCFormatting(text),
114 username: displayName,
115 icon_url: iconUrl,
116 unfurl_links: false,
117 unfurl_media: false,
118 });
119 console.log(`IRC → Slack: <${nick}> ${text}`);
120 } catch (error) {
121 console.error("Error posting to Slack:", error);
122 }
123 },
124);
125
126ircClient.addListener("error", (error: string) => {
127 console.error("IRC error:", error);
128});
129
130// Slack event handlers
131slackApp.event("message", async ({ payload }) => {
132 if (payload.subtype) return;
133 if (payload.bot_id) return;
134 if (payload.user === botUserId) return;
135 if (payload.thread_ts) return;
136
137 // Find IRC channel mapping for this Slack channel
138 const mapping = channelMappings.getBySlackChannel(payload.channel);
139 if (!mapping) {
140 console.log(
141 `No IRC channel mapping found for Slack channel ${payload.channel}`,
142 );
143 slackClient.conversations.leave({
144 channel: payload.channel,
145 });
146 return;
147 }
148
149 try {
150 const userInfo = await slackClient.users.info({
151 token: process.env.SLACK_BOT_TOKEN,
152 user: payload.user,
153 });
154
155 // Check for user mapping, otherwise use Slack name
156 const userMapping = userMappings.getBySlackUser(payload.user);
157 const username =
158 userMapping?.irc_nick ||
159 userInfo.user?.real_name ||
160 userInfo.user?.name ||
161 "Unknown";
162
163 // Parse Slack mentions and replace with display names
164 let messageText = payload.text;
165 const mentionRegex = /<@(U[A-Z0-9]+)>/g;
166 const mentions = Array.from(messageText.matchAll(mentionRegex));
167
168 for (const match of mentions) {
169 const userId = match[1];
170 try {
171 const response = await fetch(
172 `https://cachet.dunkirk.sh/users/${userId}`,
173 );
174 if (response.ok) {
175 const data = (await response.json()) as CachetUser;
176 messageText = messageText.replace(match[0], `@${data.displayName}`);
177 }
178 } catch (error) {
179 console.error(`Error fetching user ${userId} from cachet:`, error);
180 }
181 }
182
183 // Parse Slack markdown formatting
184 messageText = parseSlackMarkdown(messageText);
185
186 const message = `<${username}> ${messageText}`;
187
188 ircClient.say(mapping.irc_channel, message);
189 console.log(`Slack → IRC: ${message}`);
190 } catch (error) {
191 console.error("Error handling Slack message:", error);
192 }
193});
194
195export default {
196 port: process.env.PORT || 3000,
197 async fetch(request: Request) {
198 const url = new URL(request.url);
199 const path = url.pathname;
200
201 switch (path) {
202 case "/":
203 return new Response(`Hello World from irc-slack-bridge@${version}`);
204 case "/health":
205 return new Response("OK");
206 case "/slack":
207 return slackApp.run(request);
208 default:
209 return new Response("404 Not Found", { status: 404 });
210 }
211 },
212};
213
214console.log(
215 `🚀 Server Started in ${Bun.nanoseconds() / 1000000} milliseconds on version: ${version}!\n\n----------------------------------\n`,
216);
217console.log(
218 `Connecting to IRC: irc.hackclub.com:6667 as ${process.env.IRC_NICK}`,
219);
220console.log(`Channel mappings: ${channelMappings.getAll().length}`);
221console.log(`User mappings: ${userMappings.getAll().length}`);
222
223export { slackApp, slackClient, ircClient };