diff --git a/bot.js b/bot.js new file mode 100644 index 0000000..66847c5 --- /dev/null +++ b/bot.js @@ -0,0 +1,112 @@ +import { createRequire } from 'module'; +const require = createRequire(import.meta.url); + +import Eris from 'eris'; +import Logger, { levels } from './logger.js'; +import CommandParser, { Command } from './parser.js'; +import { filename, dirname } from './utils.js'; +import path from 'path'; +import fs from 'fs'; + +const cfg = require('./config.json'); +if (!fs.existsSync('./whitelist.json')) { + fs.writeFileSync('./whitelist.json', '{}'); +} + +const whitelist = require('./whitelist.json'); +const dir = dirname(import.meta.url); + +const bot = new Eris(cfg.token); + +global.ctx = { + bot: bot, + log_level: levels[cfg.log_level.toUpperCase()] || levels.WARN, + set_ctx: (key, value) => { + global.ctx[key] = value; + }, +}; +const log = new Logger(filename(import.meta.url), global.ctx.log_level); +const parse = new CommandParser(global.ctx, cfg.prefix || undefined); + +const checkUser = (user) => { + if (cfg.user_whitelist.includes(user.id) || (whitelist.user && whitelist.user.includes(user.id))) { + return true; + } + log.info(`user not on whitelist: ${user.username}`); + return false; +}; +const checkGuild = (guild) => { + if (cfg.whitelist.includes(guild.id) || (whitelist.guild && whitelist.guild.includes(guild.id))) { + return true; + } + log.info(`guild not on whitelist: ${guild.name}`); + return false; +}; + +const saveWhitelist = () => { + let data = JSON.stringify(whitelist); + fs.writeFileSync('./whitelist.json', data); +}; + +global.ctx.whitelist = { + guild: checkGuild, + user: checkUser, + save: saveWhitelist, + wl: whitelist, +}; + +const listeners = {} + +bot.on('ready', () => { + log.info('ready recieved.'); + bot.guilds.forEach((guild) => checkGuild(guild)); +}); + +bot.on('messageCreate', (msg) => parse.parseMsg(msg, global.ctx)); + +bot.on('error', (err) => log.error(err.toString())); + +bot.connect(); + +async function load_commands() { + for (let file of files) { + let p = path.join(cmd_dir, file); + log.debug('loading ' + p); + let obj; + try { + obj = await import('file:////' + p); + } catch (e) { + log.warn(`loading file ${file}, ran into issue: ${e.stack}`); + continue; + } + if (obj.default != undefined) { + if (obj.default.constructor.name == 'CommandInitializer') { + obj.default.initialize(global.ctx); + let cmds = obj.default.getCommands(); + if (parse.isCmd(cmds)) { + parse.addCommand(cmds); + } else if (cmds.constructor.name == 'Array') { + for (let cmd of cmds) { + parse.addCommand(cmd); + } + } + let events = obj.default.getEvents(); + if (events != undefined) { + for (let event in events) { + if (!listeners[event]) { + listeners[event] = [] + } + listeners[event].push(events[event]) + } + } + } + } else { + log.warn('module ' + file + ' returned an undefined module.'); + } + } +} + +let cmd_dir = path.join(dir, 'cmd'); +log.trace(dir); +let files = fs.readdirSync(cmd_dir); +load_commands(); diff --git a/cmd/perm.js b/cmd/perm.js new file mode 100644 index 0000000..45abebc --- /dev/null +++ b/cmd/perm.js @@ -0,0 +1,94 @@ +import { CommandInitializer, Command } from '../parser.js'; + +const initializer = new CommandInitializer(); + +class WhitelistUser extends Command { + name = 'wu'; + whitelist = true; + func = async function (msg, args, ctx) { + let user = await ctx.bot.users.get(args[0].trim()); + this.log.debug(msg.channel.guild.members); + this.log.debug(user); + if (!user) { + user = msg.channel.guild.members.get(args[0].trim()); + this.log.debug(user); + if (!user) { + user = (await msg.channel.guild.fetchMembers({ userIDs: [args[0]] }))[0]; + this.log.debug(user); + } + } + if (user.username) { + if (!ctx.whitelist.wl) { + ctx.whitelist.wl = { + user: [], + guild: [], + }; + } + let list = ctx.whitelist.wl.user || []; + if (!list.includes(args[0])) { + list.push(args[0]); + ctx.whitelist.wl.user = list; + ctx.whitelist.save(); + msg.channel.createMessage(`added user "${user.username}#${user.discriminator}" (${args[0]}) to whitelist`); + } else { + msg.channel.createMessage('user already whitelisted'); + } + } else { + msg.channel.createMessage(`user with id ${args[0]} could not be found`); + } + }; +} +initializer.addCommand(new WhitelistUser()); + +class WhitelistGuild extends Command { + name = 'wg'; + whitelist = true; + func = async function (msg, args, ctx) { + let guild = await ctx.bot.guilds.get(args[0]); + if (guild && guild.name) { + if (!ctx.whitelist.wl) { + ctx.whitelist.wl = { + user: [], + guild: [], + }; + } + let list = ctx.whitelist.wl.guild || []; + if (!list.includes(args[0])) { + list.push(args[0]); + ctx.whitelist.wl.guild = list; + ctx.whitelist.save(); + msg.channel.createMessage(`added guild "${guild.name}" (${args[0]}) to whitelist`); + } else { + msg.channel.createMessage('guild already whitelisted'); + } + } else { + msg.channel.createMessage(`guild with id ${args[0]} could not be found`); + } + }; +} + +initializer.addCommand(new WhitelistGuild()); + +class Purge extends Command { + name = 'purge'; + whitelist = true; + func = async function (msg, args, ctx) { + let guilds = await ctx.bot.guilds; + let whitelist = ctx.whitelist.guild; + let purged = 0; + guilds.forEach((guild) => { + if (!whitelist(guild)) { + this.log.info(`purging guild ${guild.name} (${guild.id})`); + guild.leave(); + purged++; + } else { + this.log.debug(`keeping guild ${guild.name} (${guild.id})`); + } + }); + msg.channel.createMessage(`purged ${purged} guilds.`); + }; +} + +initializer.addCommand(new Purge()); + +export default initializer; diff --git a/cmd/reg.js b/cmd/reg.js new file mode 100644 index 0000000..ada4883 --- /dev/null +++ b/cmd/reg.js @@ -0,0 +1,52 @@ +import { CommandInitializer, Command } from '../parser.js'; +import Logger, { levels } from '../logger.js'; +import { filename } from '../utils.js'; + +const initializer = new CommandInitializer(); +const log = new Logger(filename(import.meta.url), global.ctx.log_level); + +class PingCommand extends Command { + name = 'ping'; + func(msg, args, ctx) { + msg.channel.createMessage('p').then((m) => { + m.edit( + `rtt: ${Math.floor(m.timestamp - msg.timestamp)}, gateway: ${ + ctx.bot.shards.get(ctx.bot.guildShardMap[ctx.bot.channelGuildMap[msg.channel.id]] || 0).latency + }` + ); + }); + } +} + +initializer.addCommand(new PingCommand()); + +class RestartCommand extends Command { + name = 'restart'; + func(msg, args, ctx) { + msg.channel.createMessage('restarting.').then(() => { + process.exit(); + }); + } +} + +initializer.addCommand(new RestartCommand()); + +class EvalCommand extends Command { + name = 'eval'; + whitelist = true; + func(msg, args, ctx) { + log.debug('evaluating ' + args[0]); + let result = eval(args[0]); + log.debug('result is: \n' + result); + if (result.length <= 1999) { + msg.channel.createMessage(result); + } + else { + msg.channel.createMessage("result too long, see logs"); + } + } +} + +initializer.addCommand(new EvalCommand()); + +export default initializer; diff --git a/config.json b/config.json new file mode 100644 index 0000000..1910cc9 --- /dev/null +++ b/config.json @@ -0,0 +1,7 @@ +{ + "token": "MjY3Mzg2MTUyMzIxOTQxNTA1.WHFMwg.KgEUojOa43UL43yqUy9-TKz05tY", + "log_level": "DEBUG", + "prefix": "p.", + "whitelist": ["656579275008245830"], + "user_whitelist": ["123601647258697730"] +} diff --git a/logger.js b/logger.js new file mode 100644 index 0000000..133eea4 --- /dev/null +++ b/logger.js @@ -0,0 +1,56 @@ +export const levels = { + TRACE: 5, + DEBUG: 4, + INFO: 3, + WARN: 2, + ERROR: 1, + PANIC: 0, +}; + +export default class Logger { + constructor(name, level) { + console.log(`created new logger for ${name} with level ${level}`); + this.sn(name); + this.s(level); + } + n = 'DEFAULT'; + l = 0; + sn(n) { + this.n = n; + } + s(l) { + if (l && l.constructor === Number) { + this.l = l; + } else { + this.l = levels[l]; + } + } + + lo(l, m) { + if (l <= this.l) { + let level = Object.keys(levels).find((key) => levels[key] === l); + let ms = typeof m == 'object' ? JSON.stringify(m) : m; + console.log(`${level} [${this.n}]: ${ms}`); + } + } + + trace(msg) { + this.lo(levels.TRACE, msg); + } + + debug(msg) { + this.lo(levels.DEBUG, msg); + } + info(msg) { + this.lo(levels.INFO, msg); + } + warn(msg) { + this.lo(levels.WARN, msg); + } + error(msg) { + this.lo(levels.ERROR, msg); + } + panic(msg) { + this.lo(levels.PANIC, msg); + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..3f585af --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "type": "module", + "name": "pinbot", + "version": "1.0.0", + "description": "a bot to manage pins (and also other stuff!)", + "main": "bot.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git@ssh.gitdab.com:jane/pinbot.git" + }, + "author": "jane petrovna (jane@j4.pm)", + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "eris": "^0.15.0" + } +} diff --git a/parser.js b/parser.js new file mode 100644 index 0000000..5edb2d9 --- /dev/null +++ b/parser.js @@ -0,0 +1,133 @@ +import Logger, { levels } from './logger.js'; +import { filename } from './utils.js'; + +export class Command { + init(ctx, log) { + this.log = log; + } + log; + name = 'DEFAULT'; + whitelist = false; + func() {} +} + +export class CommandInitializer { + commands = []; + uninitialized = []; + events = {}; + + initialize(ctx) { + for (let index in this.uninitialized) { + this.initCommand(this.uninitialized[index], ctx); + delete this.uninitialized[index]; + } + } + + initCommand(cmd, ctx) { + cmd.init(ctx, new Logger(`cmd.${cmd.name}`, ctx.log_level)); + this.commands.push(cmd); + } + + addCommand(cmd) { + this.uninitialized.push(cmd); + } + + addEvent(event, func) { + if (!events[event]) { + events[event] = []; + } + events[event].push(func); + } + + getCommands() { + return this.commands; + } + + getEvents() { + return this.events; + } +} + +export default class CommandParser { + constructor(ctx, prefix = ';') { + this.log = new Logger(filename(import.meta.url), ctx.log_level); + this.prefix = prefix ? prefix : this.prefix || ';'; + } + log; + prefix; + commands = []; + + addCommand(cmd) { + this.log.trace(`cmd to add: ${JSON.stringify(cmd)}`); + if (this.isCmd(cmd)) { + this.commands.push(cmd); + } + } + + hasCmd(str) { + let results = this.commands.filter((c) => c.name == str); + + return results.length ? results[0] : undefined; + } + + isCmd(cmd) { + return typeof cmd == 'object' && cmd instanceof Command; + } + + getArgsList(split) { + let parsed_args = []; + let join_index = -1; + let add = true; + for (let index in split) { + if (split[index].startsWith('```')) { + join_index = index; + add = false; + } + if (add) { + parsed_args.push(split[index]); + } + if (split[index].endsWith('```') && join_index != -1) { + let joined = split + .slice(join_index, index + 1) + .join(' ') + .replace(/```/g, ''); + parsed_args.push(joined); + add = true; + join_index = -1; + } + } + return parsed_args; + } + + parseMsg(msg, ctx) { + if (msg.author.bot) { + return; + } + if (msg.channel.guild && !ctx.whitelist.guild(msg.channel.guild)) { + msg.channel.createMessage('guild not whitelisted'); + return; + } + this.log.trace(msg.content); + this.log.trace(msg.content.startsWith(this.prefix)); + this.log.trace(msg.content[0]); + this.log.trace(this.prefix); + if (msg.content.startsWith(this.prefix)) { + let snip = msg.content.slice(this.prefix.length); + let unsep = snip.split(' '); + let res = this.hasCmd(unsep[0]); + let args = this.getArgsList(unsep.slice(1)); + this.log.trace(snip); + this.log.trace(res); + this.log.trace(args); + + if (res != undefined) { + this.log.debug(`execute function ${res.name}`); + if (res.whitelist && !ctx.whitelist.user(msg.author)) { + msg.channel.createMessage('not whitelisted'); + } else { + res.func(msg, args, ctx); + } + } + } + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..8bf4c2d --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,41 @@ +dependencies: + eris: 0.15.0 +lockfileVersion: 5.2 +packages: + /eris/0.15.0: + dependencies: + ws: 7.4.4 + dev: false + engines: + node: '>=10.4.0' + optionalDependencies: + opusscript: 0.0.8 + tweetnacl: 1.0.3 + resolution: + integrity: sha512-muBdi5XyMXdxFQ8xUG6yofq40/Z02CHlqJP7zIdHhpdDiHvFM/mybGiFAHuoSYcsVTTvEfbUaAJ+SDEmMjARYw== + /opusscript/0.0.8: + dev: false + optional: true + resolution: + integrity: sha512-VSTi1aWFuCkRCVq+tx/BQ5q9fMnQ9pVZ3JU4UHKqTkf0ED3fKEPdr+gKAAl3IA2hj9rrP6iyq3hlcJq3HELtNQ== + /tweetnacl/1.0.3: + dev: false + optional: true + resolution: + integrity: sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw== + /ws/7.4.4: + dev: false + engines: + node: '>=8.3.0' + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + resolution: + integrity: sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw== +specifiers: + eris: ^0.15.0 diff --git a/start.sh b/start.sh new file mode 100644 index 0000000..9fbb508 --- /dev/null +++ b/start.sh @@ -0,0 +1,2 @@ +git pull +pnpm start \ No newline at end of file diff --git a/utils.js b/utils.js new file mode 100644 index 0000000..24fad97 --- /dev/null +++ b/utils.js @@ -0,0 +1,15 @@ +import path from "path"; +import {platform} from "process"; + +export function filename(url) { + let __filename = new URL(url).pathname; + return path.basename(__filename, ".js"); +} + +export function dirname(url) { + let __filename = new URL(url).pathname; + let __dirname = path.dirname(__filename); + return platform == "win32" + ? __dirname.slice(1) + : __dirname; +} \ No newline at end of file diff --git a/whitelist.json b/whitelist.json new file mode 100644 index 0000000..4ce71c4 --- /dev/null +++ b/whitelist.json @@ -0,0 +1 @@ +{"guild":["656579275008245830","823796249832194088"],"user":["123601647258697730"]} \ No newline at end of file