const fs = require("fs"); const YAML = require("yaml"); const { Client, Intents } = require("discord.js-selfbot-v13"); const IRC = require("irc-framework"); // Load config.yaml let config; try { const file = fs.readFileSync("config.yaml", "utf8"); config = YAML.parse(file); } catch (e) { console.error("Failed to load or parse config.yaml:", e); process.exit(1); } const DISCORD_TOKEN = config.discord.token; const DEBUG = config.debug === true; const LOG_FORWARD = config.logForward === true; const bridges = config.bridges; // Discord client const discordClient = new Client({ checkUpdate: false, intents: [ Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES, Intents.FLAGS.GUILD_MESSAGE_REACTIONS, ], }); // Map IRC clients const ircClients = new Map(); // Logging helpers function logDebug(...args) { if (DEBUG) console.log(...args); } function logForward(...args) { if (LOG_FORWARD) console.log(...args); } // Utility to check ASCII function isASCII(str) { return /^[\x00-\x7F]*$/.test(str); } // Create IRC client for a bridge function createIRCClient(bridge) { const ircConfig = bridge.irc; const discordChannelId = bridge.discordChannelId; const useRelayMsg = bridge.useRelayMsg === true; const client = new IRC.Client(); client.connect({ host: ircConfig.server, port: parseInt(ircConfig.port, 10), nick: ircConfig.nick, password: ircConfig.password || undefined, tls: ircConfig.tls || false, tlsOptions: { servername: ircConfig.server, rejectUnauthorized: false, }, }); client.state = { hasOp: false, relayMsgEnabled: useRelayMsg, warnedRelayMsg: false, }; client.on("connecting", () => logDebug(`Connecting to ${ircConfig.server}:${ircConfig.port} ...`) ); client.on("connected", () => logDebug(`TCP connection established to ${ircConfig.server}`) ); client.on("registered", () => { console.log( `Connected to IRC ${ircConfig.server} as ${ircConfig.nick}, joining ${ircConfig.channel}` ); client.join(ircConfig.channel, () => logDebug(`Sent JOIN for ${ircConfig.channel}`) ); }); client.on("names", (event) => { const myNick = ircConfig.nick; const modes = event.users[myNick]; if (Array.isArray(modes) && modes.includes("@")) { client.state.hasOp = true; logDebug(`${myNick} detected as +o from NAMES list`); } }); client.on("mode", (event) => { if (event.target === ircConfig.channel) { for (const m of event.modes) { if (m.mode === "+o" && m.param === ircConfig.nick) { client.state.hasOp = true; logDebug(`${ircConfig.nick} gained +o (channel operator)`); } else if (m.mode === "-o" && m.param === ircConfig.nick) { client.state.hasOp = false; logDebug(`${ircConfig.nick} lost +o (channel operator)`); } } } }); client.on("error", (event) => console.error(`IRC error on ${ircConfig.server}:`, event) ); client.on("socket close", () => logDebug(`Socket closed for ${ircConfig.server}`) ); client.on("socket error", (err) => console.error(`Socket error on ${ircConfig.server}:`, err) ); // Handle IRC → Discord client.on("message", (event) => { if (event.nick === ircConfig.nick || event.nick.endsWith("/dc")) return; const ircMessage = `<${event.nick}> ${event.message}`; const discordChannel = discordClient.channels.cache.get(discordChannelId); if (discordChannel) discordChannel.send(ircMessage).catch(console.error); }); client.on("relaymsg", (event) => { const discordChannel = discordClient.channels.cache.get(discordChannelId); if (discordChannel) discordChannel.send(`<${event.nick}> ${event.message}`).catch(console.error); }); if (DEBUG) { client.on("raw", (event) => console.log("RAW:", event.line)); } return { client, channel: ircConfig.channel, nick: ircConfig.nick, discordChannelId, useRelayMsg, }; } // Initialize IRC clients for (const bridge of bridges) { const { client, channel, nick, discordChannelId, useRelayMsg } = createIRCClient(bridge); ircClients.set(client, { channel, nick, discordChannelId, useRelayMsg }); } // Discord ready discordClient.once("ready", () => console.log(`Logged in as ${discordClient.user.tag}`) ); // Forward Discord messages → IRC discordClient.on("messageCreate", async (message) => { if (message.author.bot) return; for (const [ircClient, info] of ircClients.entries()) { if (message.channel.id !== info.discordChannelId) continue; if (message.author.id === discordClient.user.id) return; // Fetch nickname let nickname = message.author.username; if (message.guild) { try { const member = await message.guild.members.fetch(message.author.id); nickname = member ? member.displayName : message.author.username; } catch (err) { nickname = message.author.username; } } // Handle quoted replies (text only) in one line let quote = ""; if (message.reference && message.reference.messageId) { try { const referencedMessage = await message.channel.messages.fetch( message.reference.messageId ); if (referencedMessage.content) { let refAuthor = referencedMessage.author.username; if (referencedMessage.guild) { const member = await referencedMessage.guild.members .fetch(referencedMessage.author.id) .catch(() => null); if (member) refAuthor = member.displayName; } const originalText = referencedMessage.content.replace(/\n/g, " "); quote = `<${refAuthor}> said: ${originalText} | `; } } catch (err) { logDebug("Failed to fetch referenced message:", err); } } // Build message let baseMessage = message.content; if (quote) baseMessage = `${quote}${baseMessage}`; if (message.attachments.size > 0) { const urls = message.attachments.map((att) => att.url).join(" "); baseMessage += ` [Attachments: ${urls}]`; } const privmsgMessage = `<${nickname}> ${baseMessage}`; logForward(`Forwarding Discord → IRC [${info.channel}]: ${privmsgMessage}`); // RELAYMSG logic with proper fallback for non-ASCII if (info.useRelayMsg && ircClient.state.hasOp && isASCII(nickname)) { const relayNick = `${nickname}/dc`; ircClient.raw(`RELAYMSG ${info.channel} ${relayNick} :${baseMessage}`); } else { if (info.useRelayMsg && !ircClient.state.hasOp && !ircClient.state.warnedRelayMsg) { ircClient.say( info.channel, `[Bridge] Missing +o, falling back to PRIVMSG (RELAYMSG disabled)` ); ircClient.state.warnedRelayMsg = true; } ircClient.say(info.channel, privmsgMessage); } } }); // Forward reactions → IRC discordClient.on("messageReactionAdd", async (reaction, user) => { if (user.bot) return; try { if (reaction.partial) await reaction.fetch(); const message = reaction.message; const channelId = message.channel.id; for (const [ircClient, info] of ircClients.entries()) { if (channelId !== info.discordChannelId) continue; // Get nickname let nickname = user.username; if (message.guild) { try { const member = await message.guild.members.fetch(user.id); nickname = member ? member.displayName : user.username; } catch (err) { nickname = user.username; } } const emoji = reaction.emoji.name; const originalContent = message.content || "[Embed/Attachment]"; const reactionMessage = `<${nickname}> reacted with ${emoji} to "${originalContent}"`; logForward(`Reaction → IRC [${info.channel}]: ${reactionMessage}`); if (info.useRelayMsg && ircClient.state.hasOp && isASCII(nickname)) { const relayNick = `${nickname}/dc`; ircClient.raw( `RELAYMSG ${info.channel} ${relayNick} :${reactionMessage}` ); } else { ircClient.say(info.channel, reactionMessage); } } } catch (err) { logDebug("Failed to handle reaction:", err); } }); // Login to Discord discordClient.login(DISCORD_TOKEN).catch((err) => console.error("Failed to login to Discord:", err) );