From 60b564a154095f230c802349fcbda8cddd20ba83 Mon Sep 17 00:00:00 2001 From: hiimgoodpack Date: Mon, 25 Jan 2021 00:23:34 -0500 Subject: [PATCH] Create bots --- bots/ChanServ.js | 154 +++++++++++++++++++++++++++++++++++++++++++++++ bots/DinoServ.js | 47 +++++++++++++++ bots/MsgServ.js | 31 ++++++++++ bots/NickServ.js | 100 ++++++++++++++++++++++++++++++ config.js | 43 +++++++------ index.js | 68 +++++++-------------- 6 files changed, 379 insertions(+), 64 deletions(-) create mode 100644 bots/ChanServ.js create mode 100644 bots/DinoServ.js create mode 100644 bots/MsgServ.js create mode 100644 bots/NickServ.js diff --git a/bots/ChanServ.js b/bots/ChanServ.js new file mode 100644 index 0000000..80e0bc9 --- /dev/null +++ b/bots/ChanServ.js @@ -0,0 +1,154 @@ +/* + An IRC bot to sync Brainlet categories + Copyright (C) 2020 hiimgoodpack + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +const ircClient = require("irc"); +// Node's replaceAll is a new feature. We'll use a package instead +const replaceAll = require("replaceall"); +const brainlet = require("../brainlet-lib/index.js"); + +module.exports = { + start: (config, {tokens, sockets, availableBots, channelNameToCategoryId, categoryIdToChannelName}) => { + let bot = new ircClient.Client("localhost", "ChanServ", { + realName: "A bot to sync channels from a Brainlet instance to the IRC server" + }); + + const getRandomToken = () => { + const names = Object.keys(tokens); + let nameIndex = Math.floor(Math.random() * names.length); + // Floating point rounding might make nameIndex >= tokens.length, which we don't want + if (nameIndex >= tokens.length) + nameIndex = tokens.length-1; + + return tokens[names[nameIndex]]; + } + + const commands = { + help: (from) => { + bot.notice(from, config.responses.ChanServ.help); + }, + invite: (from) => { + if (!tokens[from]) { + bot.notice(from, config.responses.ChanServ.accessDenied); + return; + } + Object.keys(channelNameToCategoryId).forEach((channelName) => { + bot.send("INVITE", from, channelName); + }) + }, + create: (from, args) => { + if (!tokens[from]) { + bot.notice(from, config.responses.ChanServ.accessDenied); + return; + } + const categoryTitle = args.splice(1).join(" "); + + bot.notice(from, `Creating category ${categoryTitle}...`); + brainlet.categories.create(config.server, tokens[from], categoryTitle).then(() => { + bot.notice(from, "Successfully created category on Brainlet\n" + + "You may need to wait a few seconds for a category sync to occur to use the channel"); + }).catch((error) => { + bot.notice(from, `Failed to create category: ${error}`); + }); + } + }; + + bot.addListener("error", console.error); + bot.addListener("message", (from, channel, text, message) => { + // Make sure it's a private message channel the bot + if (channel !== bot.opt.nick) + return; + + const args = text.split(" "); + if (args.length == 0) { + bot.notice(from, config.responses.ChanServ.invalid); + return; + } + + const command = commands[args[0].toLowerCase()]; + if (!command) { + bot.notice(from, config.responses.ChanServ.invalid); + return; + } + + command(from, args); + }); + bot.addListener("join", (channel, nick) => { + bot.send("MODE", channel, "+v", nick); + + // Is it a bot? + if (availableBots[nick]) { + // Give it channel op + bot.send("MODE", channel, "+o", nick); + } else { + const socket = sockets[nick]; + const categoryId = channelNameToCategoryId[channel]; + console.log(socket); + socket.connect(categoryId).then(() => { + console.log("Successfully joined channel"); + }); + } + }); + + // We need 2 hexadecimal digits for a buffer to work + let nextId = 0xF+1; + const categoryToChannelName = (name) => { + return `#${replaceAll(" ", "_", name)}_${Buffer.from((nextId++).toString(16), "hex").toString("base64")}`; + }; + // Update category list + setInterval(() => { + const token = getRandomToken(); + if (!token) + return; + // TODO: Handle having more than 100 categories + // Alternatively, send a notice if there is more than 100 categories + const maxCategories = 100; + brainlet.categories.list(config.server, token, maxCategories).then((categories) => { + for (const {_id: categoryId, title: categoryTitle, creator: {username: creatorUsername}} of categories) { + // Make sure this is a new channel + if (categoryIdToChannelName[categoryId]) + continue; + + const channelName = categoryToChannelName(categoryTitle); + + bot.join(channelName, () => { + channelNameToCategoryId[channelName] = categoryId; + categoryIdToChannelName[categoryId] = channelName; + bot.send("TOPIC", channelName, `${ircClient.colors.wrap("light_blue", categoryTitle)} by ${ircClient.colors.wrap("light_red", creatorUsername)}`); + + // Invite only + bot.send("MODE", channelName, "+i"); + // Allow mutes + bot.send("MODE", channelName, "+m"); + + // Invite everyone who is logged in + Object.keys(tokens).forEach((nick) => { + bot.send("INVITE", nick, channelName); + }); + Object.keys(availableBots).forEach((nick) => { + if (nick === "ChanServ") + return; + bot.send("INVITE", nick, channelName); + }); + }); + } + }).catch((error) => { + console.error(`An error occured when updating category list: ${error}`); + }); + }, 5000); + } +} diff --git a/bots/DinoServ.js b/bots/DinoServ.js new file mode 100644 index 0000000..ccdd177 --- /dev/null +++ b/bots/DinoServ.js @@ -0,0 +1,47 @@ +/* + An IRC bot to annoy people + Copyright (C) 2020 hiimgoodpack + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +const ircClient = require("irc"); +const brainlet = require("../brainlet-lib/index.js"); + +module.exports = { + start: (config, {tokens}) => { + let bot = new ircClient.Client("localhost", "DinoServ", { + realName: "A bot to annoy users" + }); + + bot.addListener("error", console.error); + bot.addListener("message", (from, channel, text, message) => { + if (text.match(/apt-get/)) + bot.say(channel, `Hello, annoying bot reporting in, just notifying @${from} that the apt-get command on debian is deprecated. Please use apt instead. + +Thank you for attending my TED-talk.`); + if (text.match(/reverse engineer/i)) { + bot.say(channel, `@${from} Watch your language.`); + bot.send("MODE", channel, "-v", from); + bot.notice(from, "You have been muted for 10 seconds."); + setTimeout(() => { + bot.send("MODE", channel, "+v", from); + }, 10000); + } + }); + bot.addListener("invite", (channel) => { + bot.join(channel); + }); + } +} diff --git a/bots/MsgServ.js b/bots/MsgServ.js new file mode 100644 index 0000000..8bea17e --- /dev/null +++ b/bots/MsgServ.js @@ -0,0 +1,31 @@ +/* + An IRC bot manager to sync Brainlet messages + Copyright (C) 2020 hiimgoodpack + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +const ircClient = require("irc"); +// Node's replaceAll is a new feature. We'll use a package instead +const replaceAll = require("replaceall"); +const brainlet = require("../brainlet-lib/index.js"); + +module.exports = { + start: (config, {tokens, sockets, channelNameToCategoryId, categoryIdToChannelName}) => { + let bot = new ircClient.Client("localhost", "MsgServ", { + realName: "A bot to sync messages from a Brainlet instance to the IRC server" + }); + // TODO: Actually do stuff + } +}; diff --git a/bots/NickServ.js b/bots/NickServ.js new file mode 100644 index 0000000..c6cc88a --- /dev/null +++ b/bots/NickServ.js @@ -0,0 +1,100 @@ +/* + An IRC bot to manage Brainlet accounts + Copyright (C) 2020 hiimgoodpack + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +const ircClient = require("irc"); +const brainlet = require("../brainlet-lib/index.js"); + +module.exports = { + start: (config, {tokens, sockets}) => { + let bot = new ircClient.Client("localhost", "NickServ", { + realName: "A bot to let users manage Brainlet accounts from an IRC server" + }); + + const commands = { + help: (from) => { + bot.notice(from, config.responses.NickServ.help); + }, + register: (from, [command, password, email]) => { + if (!password || !email) { + bot.notice(from, config.responses.NickServ.invalid); + return; + } + + bot.notice(from, `Registering account ${from}...`); + brainlet.users.create(config.server, { + name: from, + email: email, + password: password + }).then(() => { + bot.notice(from, "Successfully registered account"); + commands.identify(from, ["identify", from, password]); + }).catch((error) => { + bot.notice(from, `Failed to register account: ${error}`); + }); + }, + identify: (from, [command, name, password]) => { + if (!name || !password) { + bot.notice(from, config.responses.NickServ.invalid); + return; + } + + bot.notice(from, `Logging into account ${name}...`); + brainlet.users.login(config.server, { + name: name, + password: password + }).then(({token = ""}) => { + bot.notice(from, "Successfully logged into account"); + tokens[from] = token; + + bot.notice(from, "Creating websocket connection..."); + brainlet.categories.client(config.server, token).then((client) => { + sockets[from] = client; + bot.notice(from, "Successfully connected to server\n" + + "You will likely want to use \"/msg ChanServ INVITE\" now\n" + + "Use \"/msg ChanServ HELP\" for more information"); + }).catch((error) => { + bot.notice(from, `Failed to create websocket connection: ${error}`); + }); + }).catch((error) => { + bot.notice(from, `Failed to log into account: ${error}`); + }); + } + }; + + bot.addListener("error", console.error); + bot.addListener("message", (from, channel, text, message) => { + // Make sure it's a private message channel the bot + if (channel !== bot.opt.nick) + return; + + const args = text.split(" "); + if (args.length == 0) { + bot.notice(from, config.responses.NickServ.invalid); + return; + } + + const command = commands[args[0].toLowerCase()]; + if (!command) { + bot.notice(from, config.responses.NickServ.invalid); + return; + } + + command(from, args); + }); + } +}; diff --git a/config.js b/config.js index 0c2244d..65b1d0f 100644 --- a/config.js +++ b/config.js @@ -1,33 +1,42 @@ module.exports = { server: "127.0.0.1:3005", - spectator: { - name: "BrainletToIrcSpectator", - password: "Put a secure password here. See the README for more info" - }, - motd: "A Brainlet to IRC translation layer.\n" + - "SECURITY WARNING: The traffic to the Brainlet server is not encrypted.\n" + - "Use \"/msg NickServ help\" to see how to use an account", + motd: "A Brainlet to IRC translation layer\n" + + "SECURITY WARNING: The traffic to the Brainlet server is not encrypted\n" + + "Use \"/msg NickServ help\" to see how to use an account\n" + + "Use \"/msg ChanServ help\" to see how to join and manage channels", responses: { NickServ: { - help: `NickServ allows you to use and create a Brainlet account from IRC. + help: `NickServ allows you to use and create a Brainlet account from the IRC server You can use a command with "/msg NickServ " Commands: HELP - Shows this message -REGISTER - Creates an account, using your current nick as the username. - Note: The email is not directly used by IRC, but may be used by the Brainlet host. -IDENTIFY - Logs into an account.`, +REGISTER - Creates an account, using your current nick as the username + Note: The email is not directly used by the IRC server, but could be used by the Brainlet host +IDENTIFY - Logs into an account`, // Sent on invalid command invalid: "Invalid command. Use \"/msg NickServ help\" for help", + }, + ChanServ: { + help: `ChanServ syncs channel information between a Brainlet instance and the IRC server +You may get channel invites from ChanServ, as each channel is invite-only to require you to log in +You can use a command with "/msg ChanServ - // Sent on an invalid entry. - // %s is replaced with how it's invalid. - invalidName: "Invalid username specified. %s", - invalidEmail: "Invalid email specified. %s", - invalidPassword: "Invalid password specified. %s" +Commands: +HELP - Shows this message +INVITE - Get invited to every channel, as each channel is invite only +CREATE - Creates a category`, + + // Sent on invalid command + invalid: "Invalid command. Use \"/msg ChanServ help\" for help", + + // Sent when using a command requiring authentication without being logged in + accessDenied: "You must log in before using this command. Use \"/msg NickServ help\" to see how to log in" } - } + }, + + bots: ["NickServ", "ChanServ", "MsgServ"] } diff --git a/index.js b/index.js index b1062b5..05df6d5 100644 --- a/index.js +++ b/index.js @@ -19,6 +19,7 @@ const fs = require("fs"); const ircServer = require("./ircd.js/lib/server.js"); const ircClient = require("irc"); const crypto = require("crypto"); +const brainlet = require("./brainlet-lib/index.js"); // Set up IRC server const config = require(`${__dirname}/config.js`) @@ -28,11 +29,19 @@ const genPass = () => { return crypto.randomBytes(66).toString("base64"); } +// NOTE: You don't actually need the password to log into the account let systemUsers = { NickServ: { + password: "12345" + }, + ChanServ: { + password: genPass() + }, + DinoServ: { password: genPass() } } + const ircConfig = { network: "BrainletToIRC", hostname: "localhost", @@ -61,51 +70,16 @@ fs.writeFileSync(`${__dirname}/ircd.js/config/config.json`, JSON.stringify(ircCo ircServer.Server.boot(); // Set up system users -const ircServerAddress = ircConfig.links.server.host; -const ircServerPort = ircConfig.links.server.port; +let tokens = {}; +let sockets = {}; +let availableBots = {}; +// Maps channel names to category ids +let channelNameToCategoryId = {}; +// Maps category ids to channel names +let categoryIdToChannelName = {}; -let NickServ = new ircClient.Client("localhost", "NickServ"); -console.log(NickServ) -NickServ.addListener("error", console.error); -NickServ.addListener("message", (from, to, text, message) => { - // Make sure it's a private message to NickServ - if (to !== NickServ.opt.nick) - return; - - const args = text.split(" "); - if (args.length == 0) { - NickServ.notice(from, config.responses.NickServ.invalid); - return; - } - - switch (args[0].toLowerCase()) { - case "help": { - NickServ.notice(from, config.responses.NickServ.help); - break; - } - case "register": { - const password = args[1]; - const email = args[2]; - if (!password || !email) { - NickServ.notice(from, config.responses.NickServ.invalid); - return; - } - NickServ.notice(from, "Account registration is currently not implemented"); - break; - } - case "identify": { - const name = args[1]; - const password = args[2]; - if (!name || !password) { - NickServ.notice(from, config.responses.NickServ.invalid); - return; - } - NickServ.notice(from, "Account login is currently not implemented"); - break; - } - default: { - NickServ.notice(from, config.responses.NickServ.invalid); - break; - } - } -}); +for (const botIndex in config.bots) { + const bot = config.bots[botIndex]; + require(`./bots/${bot}.js`).start(config, {tokens, sockets, availableBots, channelNameToCategoryId, categoryIdToChannelName}); + availableBots[bot] = true; +}