278 lines
8.2 KiB
JavaScript
278 lines
8.2 KiB
JavaScript
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)
|
|
);
|