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