diff --git a/.config.yaml.kate-swp b/.config.yaml.kate-swp new file mode 100644 index 0000000..32aede4 Binary files /dev/null and b/.config.yaml.kate-swp differ diff --git a/.gitignore b/.gitignore index 8c08375..d4d4f0f 100644 --- a/.gitignore +++ b/.gitignore @@ -130,4 +130,5 @@ dist .yarn/install-state.gz .pnp.* -config.xml \ No newline at end of file +# Ignore the normal config. (Used by me for testing.) +config.yaml diff --git a/README.md b/README.md index d137998..05bf85e 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,69 @@ # Discord-userbot-to-irc A crappy group chat to a IRC room bridge written in node { - if (err) throw err; - config = result.BridgeConfig; - }); + const file = fs.readFileSync("config.yaml", "utf8"); + config = YAML.parse(file); } catch (e) { - console.error('Failed to load or parse config.xml:', e); + console.error("Failed to load or parse config.yaml:", e); process.exit(1); } -const DISCORD_TOKEN = config.Discord.Token; -const bridges = Array.isArray(config.Bridges.Bridge) - ? config.Bridges.Bridge - : [config.Bridges.Bridge]; +const DISCORD_TOKEN = config.discord.token; +const DEBUG = config.debug === true; +const LOG_FORWARD = config.logForward === true; +const bridges = config.bridges; -// Create Discord client +// Discord client const discordClient = new Client({ checkUpdate: false, - intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES] + intents: [ + Intents.FLAGS.GUILDS, + Intents.FLAGS.GUILD_MESSAGES, + Intents.FLAGS.GUILD_MESSAGE_REACTIONS, + ], }); -// Map to hold IRC clients and their config +// Map IRC clients const ircClients = new Map(); -// Helper function to create and connect an IRC client for each bridge +// Logging helpers +function logDebug(...args) { + if (DEBUG) console.log(...args); +} +function logForward(...args) { + if (LOG_FORWARD) console.log(...args); +} + +// Create IRC client for a bridge function createIRCClient(bridge) { - const ircConfig = bridge.IRC; - const discordChannelId = bridge.Discord.ChannelId; + const ircConfig = bridge.irc; + const discordChannelId = bridge.discordChannelId; - const server = ircConfig.Server; - const port = parseInt(ircConfig.Port, 10); - const nick = ircConfig.Nick; - const channel = ircConfig.Channel; + const client = new IRC.Client(); - const client = new irc.Client(server, nick, { - channels: [channel], - port: port, - autoConnect: false + 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.on('error', (message) => { - console.error(`IRC error on ${server} (${channel}):`, message); + 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("join", (event) => + logDebug(`🎉 Joined ${event.channel} as ${event.nick}`) + ); + 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) + ); - client.on('message', (from, to, message) => { - // Avoid echoing own messages - if (from === nick) return; - - const ircMessage = `<${from}> ${message}`; + client.on("message", (event) => { + if (event.nick === ircConfig.nick) return; // Skip self + const ircMessage = `<${event.nick}> ${event.message}`; const discordChannel = discordClient.channels.cache.get(discordChannelId); - if (discordChannel) { - discordChannel.send(ircMessage).catch(console.error); - } else { - console.warn(`Discord channel ${discordChannelId} not found.`); - } + if (discordChannel) discordChannel.send(ircMessage).catch(console.error); }); - return { client, server, channel, nick, discordChannelId }; -} - -// Initialize all IRC clients -for (const bridge of bridges) { - const { client, server, channel, nick } = createIRCClient(bridge); - ircClients.set(client, { server, channel, nick, discordChannelId: bridge.Discord.ChannelId }); -} - -// Connect all IRC clients once Discord is ready -discordClient.once('ready', () => { - console.log(`Logged in as ${discordClient.user.tag}`); - - for (const [ircClient, info] of ircClients.entries()) { - console.log(`Connecting to IRC server ${info.server} on channel ${info.channel}...`); - ircClient.connect(5, () => { - console.log(`Connected to IRC server ${info.server} channel ${info.channel}`); + client.on("relaymsg", (event) => { + const discordChannel = discordClient.channels.cache.get(discordChannelId); + if (discordChannel) + discordChannel.send(`[relay ${event.nick}] ${event.message}`).catch(console.error); }); - } -}); -// Forward Discord messages to corresponding IRC channel, handling replies as quotes -discordClient.on('messageCreate', async (message) => { + if (DEBUG) { + client.on("raw", (event) => console.log("RAW:", event.line)); + } + + return { client, channel: ircConfig.channel, nick: ircConfig.nick, discordChannelId }; +} + +// Initialize IRC clients +for (const bridge of bridges) { + const { client, channel, nick, discordChannelId } = createIRCClient(bridge); + ircClients.set(client, { channel, nick, discordChannelId }); +} + +// 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) { - if (message.author.id === discordClient.user.id) return; + if (message.channel.id !== info.discordChannelId) continue; + if (message.author.id === discordClient.user.id) return; - let quote = ''; - if (message.reference && message.reference.messageId) { + // 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 + let quote = ""; + if (message.reference && message.reference.messageId) { + try { + const referencedMessage = await message.channel.messages.fetch( + message.reference.messageId + ); + if (referencedMessage) { + const refMember = + referencedMessage.guild && + (await referencedMessage.guild.members.fetch( + referencedMessage.author.id + ).catch(() => null)); + const refAuthor = refMember + ? refMember.displayName + : referencedMessage.author.username; + const refContent = referencedMessage.content || "[Embed/Attachment]"; + const quotedLines = refContent + .split("\n") + .map((line) => `> ${line}`) + .join("\n"); + quote = `> ${refAuthor} said:\n${quotedLines}\n`; + } + } catch (err) { + logDebug("Failed to fetch referenced message:", err); + } + } + + // Build message text + let discordMessage = `${quote}<${nickname}> ${message.content}`; + + // Append attachment URLs (images/files) + if (message.attachments.size > 0) { + const urls = message.attachments.map((att) => att.url).join(" "); + discordMessage += ` [Attachments: ${urls}]`; + } + + logForward(`Forwarding Discord → IRC [${info.channel}]: ${discordMessage}`); + ircClient.say(info.channel, discordMessage); + } +}); + +// 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 referencedMessage = await message.channel.messages.fetch(message.reference.messageId); - if (referencedMessage) { - const refAuthor = referencedMessage.author.username; - const refContent = referencedMessage.content || '[Embed/Attachment]'; - const quotedLines = refContent.split('\n').map(line => `> ${line}`).join('\n'); - quote = `> ${refAuthor} said:\n${quotedLines}\n`; - } + const member = await message.guild.members.fetch(user.id); + nickname = member ? member.displayName : user.username; } catch (err) { - console.warn('Failed to fetch referenced message:', err); + nickname = user.username; } } - const discordMessage = `${quote}<${message.author.username}> ${message.content}`; - console.log(`Forwarding Discord message to IRC [${info.server} ${info.channel}]: ${discordMessage}`); - ircClient.say(info.channel, discordMessage); + 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}`); + 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); -}); +discordClient.login(DISCORD_TOKEN).catch((err) => +console.error("Failed to login to Discord:", err) +); diff --git a/example.config.xml b/example.config.xml deleted file mode 100644 index 00b732e..0000000 --- a/example.config.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - YOUR_DISCORD_TOKEN - - - - - irc.example1.net - 6667 - MyIRCBot1 - #channel1 - - - DISCORD_CHANNEL_ID_1 - - - - - irc.example2.org - 6667 - MyIRCBot2 - #channel2 - - - DISCORD_CHANNEL_ID_2 - - - - - diff --git a/example.config.yaml b/example.config.yaml new file mode 100644 index 0000000..59ef9b1 --- /dev/null +++ b/example.config.yaml @@ -0,0 +1,27 @@ +discord: + token: "YOUR_DISCORD_TOKEN" # Insert your bot or user token here. + +# Debugging options +debug: true # raw IRC + connection info +logForward: true # Discord -> IRC message forwarding logs + +# Bridges +bridges: + - discordChannelId: "123456789012345678" + irc: + server: "irc.example.org" + port: 6697 + tls: true + nick: "MyBot" + password: null # or "yourpassword" if required + channel: "#test" + + # Adding more then one bridge + - discordChannelId: "987654321098765432" + irc: + server: "irc.other.net" + port: 6667 + tls: false + nick: "OtherBot" + password: null + channel: "#another" diff --git a/package-lock.json b/package-lock.json index b903f5c..7ee57b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,8 @@ "discord.js-selfbot-v13": "^3.4.5", "irc": "^0.5.2", "irc-framework": "^4.14.0", - "xml2js": "^0.6.2" + "xml2js": "^0.6.2", + "yaml": "^2.8.1" } }, "node_modules/@discordjs/builders": { @@ -1513,6 +1514,18 @@ "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", "license": "ISC" }, + "node_modules/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, "node_modules/yargs": { "version": "15.4.1", "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", diff --git a/package.json b/package.json index 4d3f100..38b4f90 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,16 @@ { - "dependencies": { - "discord.js-selfbot-v13": "^3.4.5", - "irc": "^0.5.2", - "irc-framework": "^4.14.0", - "xml2js": "^0.6.2" - }, - "name": "bridge", - "version": "1.0.0", + "name": "discord-irc-bridge", + "version": "2.0.0", + "description": "A Discord <-> IRC bridge using selfbot and irc-framework", "main": "bot.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "start": "node bot.js" }, - "keywords": [], - "author": "", - "license": "ISC", - "description": "" + "author": "Purplebored", + "license": "MIT", + "dependencies": { + "discord.js-selfbot-v13": "^2.10.0", + "irc-framework": "^3.0.0", + "yaml": "^2.3.1" + } }