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 };