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
136 // Find IRC channel mapping for this Slack channel
137 const mapping = channelMappings.getBySlackChannel(payload.channel);
138 if (!mapping) {
139 console.log(
140 `No IRC channel mapping found for Slack channel ${payload.channel}`,
141 );
142 slackClient.conversations.leave({
143 channel: payload.channel,
144 });
145 return;
146 }
147
148 try {
149 const userInfo = await slackClient.users.info({
150 token: process.env.SLACK_BOT_TOKEN,
151 user: payload.user,
152 });
153
154 // Check for user mapping, otherwise use Slack name
155 const userMapping = userMappings.getBySlackUser(payload.user);
156 const username =
157 userMapping?.irc_nick ||
158 userInfo.user?.real_name ||
159 userInfo.user?.name ||
160 "Unknown";
161
162 // Parse Slack mentions and replace with display names
163 let messageText = payload.text;
164 const mentionRegex = /<@(U[A-Z0-9]+)>/g;
165 const mentions = Array.from(messageText.matchAll(mentionRegex));
166
167 for (const match of mentions) {
168 const userId = match[1];
169 try {
170 const response = await fetch(
171 `https://cachet.dunkirk.sh/users/${userId}`,
172 );
173 if (response.ok) {
174 const data = (await response.json()) as CachetUser;
175 messageText = messageText.replace(match[0], `@${data.displayName}`);
176 }
177 } catch (error) {
178 console.error(`Error fetching user ${userId} from cachet:`, error);
179 }
180 }
181
182 // Parse Slack markdown formatting
183 messageText = parseSlackMarkdown(messageText);
184
185 const message = `<${username}> ${messageText}`;
186
187 ircClient.say(mapping.irc_channel, message);
188 console.log(`Slack → IRC: ${message}`);
189 } catch (error) {
190 console.error("Error handling Slack message:", error);
191 }
192});
193
194export default {
195 port: process.env.PORT || 3000,
196 async fetch(request: Request) {
197 const url = new URL(request.url);
198 const path = url.pathname;
199
200 switch (path) {
201 case "/":
202 return new Response(`Hello World from irc-slack-bridge@${version}`);
203 case "/health":
204 return new Response("OK");
205 case "/slack":
206 return slackApp.run(request);
207 default:
208 return new Response("404 Not Found", { status: 404 });
209 }
210 },
211};
212
213console.log(
214 `🚀 Server Started in ${Bun.nanoseconds() / 1000000} milliseconds on version: ${version}!\n\n----------------------------------\n`,
215);
216console.log(
217 `Connecting to IRC: irc.hackclub.com:6667 as ${process.env.IRC_NICK}`,
218);
219console.log(`Channel mappings: ${channelMappings.getAll().length}`);
220console.log(`User mappings: ${userMappings.getAll().length}`);
221
222export { slackApp, slackClient, ircClient };