···
import * as irc from "irc";
import { SlackApp } from "slack-edge";
import { version } from "../package.json";
4
+
import { registerCommands } from "./commands";
import { channelMappings, userMappings } from "./db";
5
-
import { parseSlackMarkdown, parseIRCFormatting } from "./parser";
6
+
import { parseIRCFormatting, parseSlackMarkdown } from "./parser";
import type { CachetUser } from "./types";
const missingEnvVars = [];
if (!process.env.SLACK_BOT_TOKEN) missingEnvVars.push("SLACK_BOT_TOKEN");
if (!process.env.SLACK_SIGNING_SECRET)
11
-
missingEnvVars.push("SLACK_SIGNING_SECRET");
12
+
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) {
17
-
`Missing required environment variables: ${missingEnvVars.join(", ")}`,
18
+
`Missing required environment variables: ${missingEnvVars.join(", ")}`,
const slackApp = new SlackApp({
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,
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",
28
+
startLazyListenerAfterAck: true,
const slackClient = slackApp.client;
let botUserId: string | undefined;
35
-
token: process.env.SLACK_BOT_TOKEN,
38
-
botUserId = result.user_id;
39
-
console.log(`Bot user ID: ${botUserId}`);
36
+
token: process.env.SLACK_BOT_TOKEN,
39
+
botUserId = result.user_id;
40
+
console.log(`Bot user ID: ${botUserId}`);
const ircClient = new irc.Client(
45
-
process.env.IRC_NICK || "slackbridge",
52
-
userName: process.env.IRC_NICK,
53
-
realName: "Slack IRC Bridge",
46
+
process.env.IRC_NICK || "slackbridge",
53
+
userName: process.env.IRC_NICK,
54
+
realName: "Slack IRC Bridge",
// Clean up IRC connection on hot reload or exit
process.on("beforeExit", () => {
59
-
ircClient.disconnect("Reloading", () => {
60
-
console.log("IRC client disconnected");
60
+
ircClient.disconnect("Reloading", () => {
61
+
console.log("IRC client disconnected");
65
+
// Register slash commands
// Join all mapped IRC channels on connect
ircClient.addListener("registered", async () => {
66
-
console.log("Connected to IRC server");
67
-
const mappings = channelMappings.getAll();
68
-
for (const mapping of mappings) {
69
-
ircClient.join(mapping.irc_channel);
70
+
console.log("Connected to IRC server");
71
+
const mappings = channelMappings.getAll();
72
+
for (const mapping of mappings) {
73
+
ircClient.join(mapping.irc_channel);
ircClient.addListener("join", (channel: string, nick: string) => {
74
-
if (nick === process.env.IRC_NICK) {
75
-
console.log(`Joined IRC channel: ${channel}`);
78
+
if (nick === process.env.IRC_NICK) {
79
+
console.log(`Joined IRC channel: ${channel}`);
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;
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;
87
-
// Find Slack channel mapping for this IRC channel
88
-
const mapping = channelMappings.getByIrcChannel(to);
89
-
if (!mapping) return;
91
+
// Find Slack channel mapping for this IRC channel
92
+
const mapping = channelMappings.getByIrcChannel(to);
93
+
if (!mapping) return;
91
-
// Check if this IRC nick is mapped to a Slack user
92
-
const userMapping = userMappings.getByIrcNick(nick);
95
+
// Check if this IRC nick is mapped to a Slack user
96
+
const userMapping = userMappings.getByIrcNick(nick);
94
-
const displayName = `${nick} <irc>`;
95
-
let iconUrl: string | undefined;
98
+
const displayName = `${nick} <irc>`;
99
+
let iconUrl: string | undefined;
99
-
iconUrl = `https://cachet.dunkirk.sh/users/${userMapping.slack_user_id}/r`;
101
-
console.error("Error fetching user info:", error);
103
+
iconUrl = `https://cachet.dunkirk.sh/users/${userMapping.slack_user_id}/r`;
105
+
console.error("Error fetching user info:", 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);
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,
116
+
unfurl_links: false,
117
+
unfurl_media: false,
119
+
console.log(`IRC → Slack: <${nick}> ${text}`);
121
+
console.error("Error posting to Slack:", error);
ircClient.addListener("error", (error: string) => {
123
-
console.error("IRC error:", error);
127
+
console.error("IRC error:", error);
slackApp.event("message", async ({ payload }) => {
128
-
if (payload.subtype) return;
129
-
if (payload.bot_id) return;
130
-
if (payload.user === botUserId) return;
132
+
if (payload.subtype) return;
133
+
if (payload.bot_id) return;
134
+
if (payload.user === botUserId) return;
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,
136
+
// Find IRC channel mapping for this Slack channel
137
+
const mapping = channelMappings.getBySlackChannel(payload.channel);
140
+
`No IRC channel mapping found for Slack channel ${payload.channel}`,
142
+
slackClient.conversations.leave({
143
+
channel: payload.channel,
145
-
const userInfo = await slackClient.users.info({
146
-
token: process.env.SLACK_BOT_TOKEN,
147
-
user: payload.user,
149
+
const userInfo = await slackClient.users.info({
150
+
token: process.env.SLACK_BOT_TOKEN,
151
+
user: payload.user,
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 ||
154
+
// Check for user mapping, otherwise use Slack name
155
+
const userMapping = userMappings.getBySlackUser(payload.user);
157
+
userMapping?.irc_nick ||
158
+
userInfo.user?.real_name ||
159
+
userInfo.user?.name ||
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));
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));
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);
167
+
for (const match of mentions) {
168
+
const userId = match[1];
170
+
const response = await fetch(
171
+
`https://cachet.dunkirk.sh/users/${userId}`,
174
+
const data = (await response.json()) as CachetUser;
175
+
messageText = messageText.replace(match[0], `@${data.displayName}`);
178
+
console.error(`Error fetching user ${userId} from cachet:`, error);
178
-
// Parse Slack markdown formatting
179
-
messageText = parseSlackMarkdown(messageText);
182
+
// Parse Slack markdown formatting
183
+
messageText = parseSlackMarkdown(messageText);
181
-
const message = `<${username}> ${messageText}`;
185
+
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);
187
+
ircClient.say(mapping.irc_channel, message);
188
+
console.log(`Slack → IRC: ${message}`);
190
+
console.error("Error handling Slack message:", error);
191
-
port: process.env.PORT || 3000,
192
-
async fetch(request: Request) {
193
-
const url = new URL(request.url);
194
-
const path = url.pathname;
195
+
port: process.env.PORT || 3000,
196
+
async fetch(request: Request) {
197
+
const url = new URL(request.url);
198
+
const path = url.pathname;
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 });
202
+
return new Response(`Hello World from irc-slack-bridge@${version}`);
204
+
return new Response("OK");
206
+
return slackApp.run(request);
208
+
return new Response("404 Not Found", { status: 404 });
210
-
`🚀 Server Started in ${Bun.nanoseconds() / 1000000} milliseconds on version: ${version}!\n\n----------------------------------\n`,
214
+
`🚀 Server Started in ${Bun.nanoseconds() / 1000000} milliseconds on version: ${version}!\n\n----------------------------------\n`,
213
-
`Connecting to IRC: irc.hackclub.com:6667 as ${process.env.IRC_NICK}`,
217
+
`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}`);
222
+
export { slackApp, slackClient, ircClient };