#!/usr/bin/env node const fs = require("fs"); const path = require("path"); const irc = require("irc"); // Spells const Summon = require("./spells/summon"); const BotList = require("./spells/botlist"); const Banish = require("./spells/banish"); const Pardon = require("./spells/pardon"); const Hmm = require("./spells/hmm"); const Whois = require("./spells/whois"); // Tasks const RSS = require("./tasks/rss"); const BBJ = require("./tasks/bbj"); const debug = false; const botName = `BabiliBot${debug ? "Demo" : ""}`; const client = new irc.Client("localhost", botName, { channels: ["#bots"], localAddress: "127.0.0.1", port: 6667, userName: "babilibot", floodProtection: true, floodProtectionDelay: 1000, autoConnect: false, stripColors: true, encoding: "utf-8", debug: true }); if (!debug) { client.opt.channels = client.opt.channels.concat([ "#meta", "#team", "#chaos", "#tildeverse" ]); } client.config = JSON.parse(fs.readFileSync("config.json", "utf8")); client.config.botName = botName; client.config.dirname = __dirname; client.spells = [ Summon, BotList, Banish, Pardon, Hmm, Whois ]; // NOTE - For my personal copy, jan6 will remain here until #chaos is mine client.blacklist = JSON.parse(fs.readFileSync( path.resolve(__dirname, "blacklist.json"), "utf8" )); // Memory log format: // { // timestamp: Date.now(), // spell: spell.name // } client.memory = { log: [], users: {}, tasks: {} }; client.tasks = debug ? [] : [ new RSS( client, "links", "https://tilde.news/newest.rss", "title", ["#meta", "#tildeverse"], 5000 ), new RSS( client, "links-comments", "https://tilde.news/comments.rss", "summary", ["#tildeverse"], 5000 ), new BBJ( client, "bbj", "http://localhost:7099/api", ["#team"], 5000 ) ]; client.triggers = { "!cast": { vars: " ", description: "Used to issue an Esperanto spell." }, "!summon": { vars: " ", description: "Used to send a summoning email to @tilde.team." }, "!hmmscore": { vars: " ", description: "Reports your's or another user's hmmscore." }, "!hmmscoreboard": { vars: "", description: "Displays the leaderboard for hmmscores." }, "!whois": { vars: "", description: "Displays the status of a domain name." }, "!help BabiliBot": { vars: "", description: "PMs user this message." }, // "!spells": { // vars: "", // description: "PMs spell list to user" // } }; // Brain of the bot client.brain = {}; // Persistent memories client.brain.memories = JSON.parse(fs.readFileSync( path.resolve(__dirname, "memories.json"), "utf8" )); // For backup of last state of memories client.brain.backup = JSON.parse(fs.readFileSync( path.resolve(__dirname, "memories.json"), "utf8" )); // How you commit memories to file client.brain.remember = () => { const memories = JSON.stringify(client.brain.memories); const file = path.resolve(__dirname, "memories.json"); const dirtyClone = (x) => JSON.parse(JSON.stringify(x)); fs.writeFile(file, memories, (err) => { if (err) { client.brain.memories = dirtyClone(client.brain.backup); console.log(`${Date.now()}: Reverting back to backup memories`); throw err; } client.brain.backup = dirtyClone(client.brain.memories); console.log(`${Date.now()}: Memories have been saved`); }); } const contains = (text, test) => { return text.indexOf(test) > -1; } const logBlacklisters = (from, message) => { const blacklistLog = `${from} tried to issue ${message}\n`; fs.appendFile("blacklist.log", blacklistLog, (err) => { if (err) throw err; console.log("Blacklist", blacklistLog); }); } const addBlacklister = (from, message) => { // Log reason for adding to the blacklist const blacklistReason = `${from} was added for spamming ${message}\n`; const log = path.resolve(__dirname, "blacklist.log"); fs.appendFile(log, blacklistReason, (err) => { if (err) throw err; console.log("Blacklist", blacklistReason); }); // Apply to current session client.blacklist[from] = { reason: `Your actions triggered auto-banishment`, when: Date.now() }; // Apply to future sessions const blacklist = JSON.stringify(client.blacklist); const file = path.resolve(__dirname, "blacklist.json"); fs.writeFile(file, blacklist, (err) => { if (err) throw err; console.log("Added to blacklist:", from); }); } const castSpell = (client, from, to, incantation) => { let casted = false; client.spells.forEach((_spell) => { const spell = new _spell(client, from, to, incantation); const vars = spell.varsUsed; let response = null; // Note: Handle variables in spells const spellName = spell.spell; const splitIncantation = incantation.split(spellName); if (splitIncantation[1]) { const variable = splitIncantation[1].trim().split(" "); spell.incantation = incantation.split(splitIncantation[1])[0]; response = spell.test.apply(spell, variable); } else { response = spell.test(); } if (spell.casted) { casted = true; client.memory.log.push({ timestamp: Date.now(), spell: spell.name }); client.memory.users[from] = client.memory.users[from] || {}; client.memory.users[from].timestamp = Date.now(); if (response.say) { client.say(to, response.content); } } }); if (!casted) { client.say(to, `${from}, malsagxulo! That spell does not exist.`); } } // For those who cannot handle Esperanto, plebeyoj! const useCheatCode = (client, from, to, cheatCode) => { let cheatCodes = {}; let variables = ""; // Handle variables in cheat code if (cheatCode.indexOf(" ") > -1) { splitCheatCode = cheatCode.split(" "); variables = splitCheatCode.slice(1).join(" "); cheatCode = splitCheatCode[0]; } // Generate cheat code client.spells.forEach((_spell) => { const spell = new _spell(client, from, ""); cheatCodes[spell.cheatCode] = spell.spell; if (variables.length > 0) { cheatCodes[spell.cheatCode] += ` ${variables}`; } }); if (contains(Object.keys(cheatCodes), cheatCode)) { castSpell(client, from, to, cheatCodes[cheatCode]); } } const checkBlacklist = (client, from, to, message) => { const now = Date.now(); const lastUsed = client.memory.users[from]; const lastTimestamp = lastUsed ? lastUsed.timestamp : 0; const blacklisters = Object.keys(client.blacklist); if (contains(blacklisters, from)) { const blacklistInfo = client.blacklist[from]; const reason = blacklistInfo.reason; // Log the blacklisters for later scrutiny logBlacklisters(from, message); client.action(to, `is ignoring ${from} because: ${reason}. Malsaĝa!`); return false; } // Add to blacklist if abusing bot if (now < lastTimestamp + client.config.timeout) { // Add exception for author of bot if (from !== client.config.author) { addBlacklister(from, message); let banished = `Malpermesante ${from}!`; banished = `${banished} Your actions triggered auto-banishment.`; banished = `${banished} I will no longer heed your words.` client.say(to, banished); return false; } } return true; } const runLogic = (client, from, to, message) => { const triggerWord = client.config.triggerWord; const address = botName.toLowerCase(); const triggerNames = Object.keys(client.triggers); const tildebridge = new RegExp(/^<~?([^>]+)>\s/); if (!message || typeof message == "undefined") return false; // Handle bots if (from === "tildebridge") { const parsedMessage = message.split(tildebridge); from = parsedMessage[1]; message = parsedMessage[2]; } else if (from === "TildeBot" || from === "sedbot") { // This bot is a monster return false; } const hmm = new RegExp(/hm+/g); const hmms = message ? message.match(hmm) : null; if (hmms && !message.startsWith("!hmmscore") && to !== "#bots") { const blacklisters = Object.keys(client.blacklist); if (contains(blacklisters, from)) return false; client.brain.memories.hmmscores = client.brain.memories.hmmscores || {}; let hmmscore = client.brain.memories.hmmscores[from]; if (!hmmscore) { client.brain.memories.hmmscores[from] = 0; hmmscore = 0; } client.brain.memories.hmmscores[from] = hmmscore + hmms.length; client.brain.remember(); } if (message.startsWith(triggerWord)) { // NOTE: entry for !cast const incantation = message.split(triggerWord)[1].trim(); const check = checkBlacklist(client, from, to, incantation); if (check) { castSpell(client, from, to, incantation); } } else if (message.startsWith(address)) { // NOTE: Use the English spells of !cast let cheatCode = message.split(address)[1]; // Why punish formality? if (cheatCode.startsWith(": ")) { cheatCode = cheatCode.split(": ")[1]; } else if (cheatCode.startsWith(", ")) { cheatCode = cheatCode.split(", ")[1]; } cheatCode = cheatCode.trim(); const check = checkBlacklist(client, from, to, cheatCode); if (check) { useCheatCode(client, from, to, cheatCode); } } else if (message.startsWith("!hmmscoreboard")) { const check = checkBlacklist(client, from, to, ""); const size = 3; const compare = (a, b) => { if (a.score < b.score) { return -1; } else if (a.score > b.score) { return 1; } return 0; } if (check) { const hmmscores = client.brain.memories.hmmscores || {}; const scores = []; Object.keys(hmmscores).forEach((user) => { const hmmscore = hmmscores[user]; scores.push({ user: user, score: hmmscore }); }); const leaderBoard = scores.sort(compare).reverse().splice(0, size); const header = "Hmm Score Leaderboard:"; const entries = []; leaderBoard.forEach((entry) => { entries.push(`${entry.user} ${entry.score}`); }); client.say(to, `${header} ${entries.join(" | ")}`); } } else if ( message.startsWith("!summon ") || message.startsWith("!hmmscore") || message.startsWith("!whois ") || message.startsWith("!botlist") || message.startsWith("!rollcall") ) { // Alias botlist to rollcall because I don't know, khuxkm said so? let cheatCode = message.slice(1).trim(); if (message.startsWith("!rollcall")) { cheatCode = "botlist"; } const check = checkBlacklist(client, from, to, cheatCode); if (check) { useCheatCode(client, from, to, cheatCode); } } else if (message.startsWith(`!help ${address}`)) { // NOTE: Help information PM'd to requester client.say(to, `Komprenita, sending help info to ${from}`); client.say(from, `I answer have ${triggerNames.length} commands:`); triggerNames.forEach((name) => { const trigger = client.triggers[name]; let helpText = `${name} ${trigger.vars}`; helpText = `${helpText} | ${trigger.description}`; client.say(from, helpText); }); client.say(from, "I also respond to: `BabiliBot, `"); } } client.addListener("message", (from, to, message) => { console.log(`${from} => ${to}: ${message}`); from = from.split("|")[0]; message = message.toLowerCase(); runLogic(client, from, to, message); }); client.addListener("pm", (from, message) => { console.log(`${from} => ME: ${message}`); runLogic(client, from, botName, message.toLowerCase()); }); client.addListener("invite", (channel, from, message) => { console.log(`${from} invites to ${channel}: ${message}`); client.join(channel); // client.say("ChanServ", `INFO ${channel}`); }); client.addListener("-mode", (channel, by, mode, argument, message) => { console.log(`${channel}: *${by}* <${mode}> [${argument}] | ${message}`); if (by === "ChanServ" && mode === "r") { const author = client.config.author; client.say("ChanServ", `REGISTER ${channel}`); client.say("ChanServ", `SET Founder ${channel} ${author}`); client.say("ChanServ", `SET Successor ${channel} ${botName}`); } }); client.addListener("error", (message) => { console.error(`error: ${message}`, message); }); client.addListener("join", (channel) => { const author = client.config.author; if (channel === "#chaos") { client.say("ChanServ", `SET Founder ${channel} ${author}`); client.say("ChanServ", `SET Successor ${channel} ${botName}`); } }); client.addListener("registered", () => { // client.say("NickServ", `RECOVER ${botName} ${client.config.password}`); client.say("NickServ", `IDENTIFY ${client.config.password}`); client.send("MODE", botName, "+B"); }); client.connect(); client.join("#bots"); if (!debug) { client.opt.channels.forEach((channel) => { client.join(channel); }); } client.tasks.forEach((task) => { task.start(); });