473 lines
14 KiB
JavaScript
473 lines
14 KiB
JavaScript
#!/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: " <spell>",
|
|
description: "Used to issue an Esperanto spell."
|
|
},
|
|
"!summon": {
|
|
vars: " <user> <reason:optional>",
|
|
description: "Used to send a summoning email to <user>@tilde.team."
|
|
},
|
|
"!hmmscore": {
|
|
vars: " <user:optional>",
|
|
description: "Reports your's or another user's hmmscore."
|
|
},
|
|
"!hmmscoreboard": {
|
|
vars: "",
|
|
description: "Displays the leaderboard for hmmscores."
|
|
},
|
|
"!whois": {
|
|
vars: "<domain>",
|
|
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 <spell>
|
|
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, <command>`");
|
|
}
|
|
}
|
|
|
|
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();
|
|
});
|