···
import * as irc from "irc";
2
-
import { SlackAPIClient, SlackApp } from "slack-edge";
2
+
import { SlackApp } from "slack-edge";
import { version } from "../package.json";
import { channelMappings, userMappings } from "./db";
5
+
import { parseSlackMarkdown, parseIRCFormatting } from "./parser";
6
+
import type { CachetUser } from "./types";
const missingEnvVars = [];
if (!process.env.SLACK_BOT_TOKEN) missingEnvVars.push("SLACK_BOT_TOKEN");
if (!process.env.SLACK_SIGNING_SECRET)
9
-
missingEnvVars.push("SLACK_SIGNING_SECRET");
11
+
missingEnvVars.push("SLACK_SIGNING_SECRET");
if (!process.env.ADMINS) missingEnvVars.push("ADMINS");
if (!process.env.IRC_NICK) missingEnvVars.push("IRC_NICK");
if (missingEnvVars.length > 0) {
15
-
`Missing required environment variables: ${missingEnvVars.join(", ")}`,
17
+
`Missing required environment variables: ${missingEnvVars.join(", ")}`,
const slackApp = new SlackApp({
21
-
SLACK_BOT_TOKEN: process.env.SLACK_BOT_TOKEN as string,
22
-
SLACK_SIGNING_SECRET: process.env.SLACK_SIGNING_SECRET as string,
23
-
SLACK_LOGGING_LEVEL: "INFO",
25
-
startLazyListenerAfterAck: true,
23
+
SLACK_BOT_TOKEN: process.env.SLACK_BOT_TOKEN as string,
24
+
SLACK_SIGNING_SECRET: process.env.SLACK_SIGNING_SECRET as string,
25
+
SLACK_LOGGING_LEVEL: "INFO",
27
+
startLazyListenerAfterAck: true,
const slackClient = slackApp.client;
let botUserId: string | undefined;
31
-
slackClient.auth.test({
32
-
token: process.env.SLACK_BOT_TOKEN,
33
-
}).then((result) => {
34
-
botUserId = result.user_id;
35
-
console.log(`Bot user ID: ${botUserId}`);
35
+
token: process.env.SLACK_BOT_TOKEN,
38
+
botUserId = result.user_id;
39
+
console.log(`Bot user ID: ${botUserId}`);
const ircClient = new irc.Client(
41
-
process.env.IRC_NICK || "slackbridge",
48
-
userName: process.env.IRC_NICK,
49
-
realName: "Slack IRC Bridge",
45
+
process.env.IRC_NICK || "slackbridge",
52
+
userName: process.env.IRC_NICK,
53
+
realName: "Slack IRC Bridge",
57
+
// Clean up IRC connection on hot reload or exit
58
+
process.on("beforeExit", () => {
59
+
ircClient.disconnect("Reloading", () => {
60
+
console.log("IRC client disconnected");
// Join all mapped IRC channels on connect
ircClient.addListener("registered", async () => {
55
-
console.log("Connected to IRC server");
56
-
const mappings = channelMappings.getAll();
57
-
for (const mapping of mappings) {
58
-
ircClient.join(mapping.irc_channel);
66
+
console.log("Connected to IRC server");
67
+
const mappings = channelMappings.getAll();
68
+
for (const mapping of mappings) {
69
+
ircClient.join(mapping.irc_channel);
ircClient.addListener("join", (channel: string, nick: string) => {
63
-
if (nick === process.env.IRC_NICK) {
64
-
console.log(`Joined IRC channel: ${channel}`);
74
+
if (nick === process.env.IRC_NICK) {
75
+
console.log(`Joined IRC channel: ${channel}`);
70
-
async (nick: string, to: string, text: string) => {
71
-
if (nick === process.env.IRC_NICK) return;
72
-
if (nick === "****") return;
81
+
async (nick: string, to: string, text: string) => {
82
+
// Ignore messages from our own bot (with or without numbers suffix)
83
+
const botNickPattern = new RegExp(`^${process.env.IRC_NICK}\\d*$`);
84
+
if (botNickPattern.test(nick)) return;
85
+
if (nick === "****") return;
74
-
// Find Slack channel mapping for this IRC channel
75
-
const mapping = channelMappings.getByIrcChannel(to);
76
-
if (!mapping) return;
87
+
// Find Slack channel mapping for this IRC channel
88
+
const mapping = channelMappings.getByIrcChannel(to);
89
+
if (!mapping) return;
78
-
// Check if this IRC nick is mapped to a Slack user
79
-
const userMapping = userMappings.getByIrcNick(nick);
91
+
// Check if this IRC nick is mapped to a Slack user
92
+
const userMapping = userMappings.getByIrcNick(nick);
81
-
const displayName = `${nick} <irc>`;
82
-
let iconUrl: string | undefined;
94
+
const displayName = `${nick} <irc>`;
95
+
let iconUrl: string | undefined;
86
-
iconUrl = `https://cachet.dunkirk.sh/users/${userMapping.slack_user_id}/r`;
88
-
console.error("Error fetching user info:", error);
99
+
iconUrl = `https://cachet.dunkirk.sh/users/${userMapping.slack_user_id}/r`;
101
+
console.error("Error fetching user info:", error);
93
-
await slackClient.chat.postMessage({
94
-
token: process.env.SLACK_BOT_TOKEN,
95
-
channel: mapping.slack_channel_id,
97
-
username: displayName,
99
-
unfurl_links: false,
100
-
unfurl_media: false,
103
-
console.error("Error posting to Slack:", error);
106
+
await slackClient.chat.postMessage({
107
+
token: process.env.SLACK_BOT_TOKEN,
108
+
channel: mapping.slack_channel_id,
109
+
text: parseIRCFormatting(text),
110
+
username: displayName,
112
+
unfurl_links: false,
113
+
unfurl_media: false,
115
+
console.log(`IRC → Slack: <${nick}> ${text}`);
117
+
console.error("Error posting to Slack:", error);
ircClient.addListener("error", (error: string) => {
109
-
console.error("IRC error:", error);
123
+
console.error("IRC error:", error);
slackApp.event("message", async ({ payload }) => {
114
-
if (payload.subtype) return;
115
-
if (payload.bot_id) return;
116
-
if (payload.user === botUserId) return;
128
+
if (payload.subtype) return;
129
+
if (payload.bot_id) return;
130
+
if (payload.user === botUserId) return;
118
-
// Find IRC channel mapping for this Slack channel
119
-
const mapping = channelMappings.getBySlackChannel(payload.channel);
122
-
`No IRC channel mapping found for Slack channel ${payload.channel}`,
124
-
slackClient.conversations.leave({
125
-
channel: payload.channel,
132
+
// Find IRC channel mapping for this Slack channel
133
+
const mapping = channelMappings.getBySlackChannel(payload.channel);
136
+
`No IRC channel mapping found for Slack channel ${payload.channel}`,
138
+
slackClient.conversations.leave({
139
+
channel: payload.channel,
131
-
const userInfo = await slackClient.users.info({
132
-
token: process.env.SLACK_BOT_TOKEN,
133
-
user: payload.user,
145
+
const userInfo = await slackClient.users.info({
146
+
token: process.env.SLACK_BOT_TOKEN,
147
+
user: payload.user,
136
-
// Check for user mapping, otherwise use Slack name
137
-
const userMapping = userMappings.getBySlackUser(payload.user);
139
-
userMapping?.irc_nick ||
140
-
userInfo.user?.real_name ||
141
-
userInfo.user?.name ||
144
-
// Parse Slack mentions and replace with display names
145
-
let messageText = payload.text;
146
-
const mentionRegex = /<@(U[A-Z0-9]+)>/g;
147
-
const mentions = Array.from(messageText.matchAll(mentionRegex));
149
-
for (const match of mentions) {
150
-
const userId = match[1];
152
-
const response = await fetch(`https://cachet.dunkirk.sh/users/${userId}`);
154
-
const data = await response.json();
155
-
messageText = messageText.replace(match[0], `@${data.displayName}`);
158
-
console.error(`Error fetching user ${userId} from cachet:`, error);
162
-
const message = `<${username}> ${messageText}`;
150
+
// Check for user mapping, otherwise use Slack name
151
+
const userMapping = userMappings.getBySlackUser(payload.user);
153
+
userMapping?.irc_nick ||
154
+
userInfo.user?.real_name ||
155
+
userInfo.user?.name ||
164
-
console.log(`Sending to IRC ${mapping.irc_channel}: ${message}`);
165
-
ircClient.say(mapping.irc_channel, message);
167
-
console.error("Error handling Slack message:", error);
158
+
// Parse Slack mentions and replace with display names
159
+
let messageText = payload.text;
160
+
const mentionRegex = /<@(U[A-Z0-9]+)>/g;
161
+
const mentions = Array.from(messageText.matchAll(mentionRegex));
163
+
for (const match of mentions) {
164
+
const userId = match[1];
166
+
const response = await fetch(
167
+
`https://cachet.dunkirk.sh/users/${userId}`,
170
+
const data = await response.json() as CachetUser;
171
+
messageText = messageText.replace(match[0], `@${data.displayName}`);
174
+
console.error(`Error fetching user ${userId} from cachet:`, error);
178
+
// Parse Slack markdown formatting
179
+
messageText = parseSlackMarkdown(messageText);
181
+
const message = `<${username}> ${messageText}`;
183
+
ircClient.say(mapping.irc_channel, message);
184
+
console.log(`Slack → IRC: ${message}`);
186
+
console.error("Error handling Slack message:", error);
172
-
port: process.env.PORT || 3000,
173
-
async fetch(request: Request) {
174
-
const url = new URL(request.url);
175
-
const path = url.pathname;
191
+
port: process.env.PORT || 3000,
192
+
async fetch(request: Request) {
193
+
const url = new URL(request.url);
194
+
const path = url.pathname;
179
-
return new Response(`Hello World from irc-slack-bridge@${version}`);
181
-
return new Response("OK");
183
-
return slackApp.run(request);
185
-
return new Response("404 Not Found", { status: 404 });
198
+
return new Response(`Hello World from irc-slack-bridge@${version}`);
200
+
return new Response("OK");
202
+
return slackApp.run(request);
204
+
return new Response("404 Not Found", { status: 404 });
191
-
`🚀 Server Started in ${Bun.nanoseconds() / 1000000} milliseconds on version: ${version}!\n\n----------------------------------\n`,
210
+
`🚀 Server Started in ${Bun.nanoseconds() / 1000000} milliseconds on version: ${version}!\n\n----------------------------------\n`,
194
-
`Connecting to IRC: irc.hackclub.com:6667 as ${process.env.IRC_NICK}`,
213
+
`Connecting to IRC: irc.hackclub.com:6667 as ${process.env.IRC_NICK}`,
console.log(`Channel mappings: ${channelMappings.getAll().length}`);
console.log(`User mappings: ${userMappings.getAll().length}`);