diff --git a/.gitignore b/.gitignore index fd89525..23c18ee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ config.json +extset/ # ---> Node # Logs diff --git a/bot.js b/bot.js index 66847c5..028c2c4 100644 --- a/bot.js +++ b/bot.js @@ -14,6 +14,11 @@ if (!fs.existsSync('./whitelist.json')) { } const whitelist = require('./whitelist.json'); + +if (!fs.existsSync('./extset')) { + fs.mkdirSync('./extset'); +} + const dir = dirname(import.meta.url); const bot = new Eris(cfg.token); @@ -25,6 +30,45 @@ global.ctx = { global.ctx[key] = value; }, }; + +let extension_settings = {}; + +const loadSettings = (guild) => { + extension_settings[guild] = {}; + if (!fs.existsSync(`./extset/${guild}.json`)) { + fs.writeFileSync(`./extset/${guild}.json`, '{}'); + } + let settings = require(`./extset/${guild}.json`); + if (settings != undefined) { + extension_settings[guild] = settings; + } +}; +const saveSettings = (guild) => { + let data = JSON.stringify(extension_settings[guild]); + fs.writeFileSync(`./extset/${guild}.json`, data); +}; + +global.ctx.settings = {}; +global.ctx.settings.getSetting = (guild, extensionId, key) => { + if (!extension_settings[guild]) { + loadSettings(guild); + } + if (!extension_settings[guild][extensionId]) { + extension_settings[guild][extensionId] = {}; + } + return extension_settings[guild][extensionId][key]; +}; +global.ctx.settings.setSetting = (guild, extensionId, key, value) => { + if (!extension_settings[guild]) { + loadSettings(guild); + } + if (!extension_settings[guild][extensionId]) { + extension_settings[guild][extensionId] = {}; + } + extension_settings[guild][extensionId][key] = value; + saveSettings(guild); +}; + const log = new Logger(filename(import.meta.url), global.ctx.log_level); const parse = new CommandParser(global.ctx, cfg.prefix || undefined); @@ -55,7 +99,7 @@ global.ctx.whitelist = { wl: whitelist, }; -const listeners = {} +const listeners = {}; bot.on('ready', () => { log.info('ready recieved.'); @@ -64,7 +108,7 @@ bot.on('ready', () => { bot.on('messageCreate', (msg) => parse.parseMsg(msg, global.ctx)); -bot.on('error', (err) => log.error(err.toString())); +bot.on('error', (err) => log.error(err.stack)); bot.connect(); @@ -83,6 +127,7 @@ async function load_commands() { if (obj.default.constructor.name == 'CommandInitializer') { obj.default.initialize(global.ctx); let cmds = obj.default.getCommands(); + let aliases = obj.default.getAliases(); if (parse.isCmd(cmds)) { parse.addCommand(cmds); } else if (cmds.constructor.name == 'Array') { @@ -90,15 +135,27 @@ async function load_commands() { 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]) - } - } + if (aliases != undefined) { + parse.addAliases(aliases); + } + let events = obj.default.getEvents(); + if (events != undefined) { + for (let event in events) { + if (!listeners[event]) { + listeners[event] = []; + } + log.trace(events[event]); + listeners[event] = listeners[event].concat(events[event]); + log.debug(`added ${events[event].length} events for ${event} from ${file}`); + bot.on(event, (...args) => { + for (let handler of listeners[event]) { + log.trace(event); + log.trace(handler); + handler.func(global.ctx, args); + } + }); + } + } } } else { log.warn('module ' + file + ' returned an undefined module.'); diff --git a/cmd/pin.js b/cmd/pin.js new file mode 100644 index 0000000..e9b304d --- /dev/null +++ b/cmd/pin.js @@ -0,0 +1,124 @@ +import { CommandInitializer, Command, Event } 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); + +let activePins = {}; + +let defaultSettings = { threshold: 3, emoji: '📌' }; + +class PinEvent extends Event { + event = 'messageReactionAdd'; + async func(ctx, args) { + const msg = args[0]; + const fullMsg = await ctx.bot.getMessage(msg.channel.id, msg.id); + const reaction = args[1]; + const reactor = args[2]; + log.trace(msg); + log.trace(reaction); + log.trace(reactor); + let threshold = ctx.settings.getSetting(msg.channel.guild.id, 'pin', 'threshold'); + if (threshold == undefined) { + threshold = defaultSettings.threshold; + ctx.settings.setSetting(msg.channel.guild.id, 'pin', 'threshold', threshold); + } + let emoji = ctx.settings.getSetting(msg.channel.guild.id, 'pin', 'emoji'); + if (emoji == undefined) { + emoji = defaultSettings.emoji; + ctx.settings.setSetting(msg.channel.guild.id, 'pin', 'emoji', emoji); + } + if (reaction.name === emoji) { + const fullReaction = await ctx.bot.getMessageReaction(msg.channel.id, msg.id, emoji); + log.debug('reaction matched'); + log.trace(fullReaction); + if (fullReaction.length >= threshold) { + if (fullMsg.author.bot || fullReaction.filter((reaction) => reaction.id == fullMsg.author.id).length > 0) { + if (activePins[fullMsg.id]) { + activePins[fullMsg.id].delete(); + } + fullMsg.pin().catch(async (err) => { + let sn = await fullMsg.channel.createMessage('could not pin the message: ' + err.toString()); + if (err.toString().includes('Missing Permissions')) { + sn.edit( + 'could not pin the message: ' + + err.toString() + + '\n(requires the manage messages or pin messages permission)' + ); + } + }); + } else { + let text = ''; + text += fullMsg.author.mention + ', '; + if (fullReaction.length == 1) { + text += fullReaction[0].username + ' wants '; + } else { + for (let i = 0; i < fullReaction.length; i++) { + text += (i == fullReaction.length - 1) ? 'and ' + fullReaction[i].username : fullReaction[i].username + ', '; + } + text += ' want '; + } + text += 'to pin your message. react with ' + emoji + ' to pin it.'; + let message = await fullMsg.channel.createMessage(text); + activePins[fullMsg.id] = message; + } + } + } else if (emoji.contains(reaction.name) && reaction.emoji(reaction.id)) { + // good luck + } + } +} + +initializer.addEvent(new PinEvent()); + +function settingsSubcommand(msg, args, ctx) { + if (args[1] == undefined) { + msg.channel.createMessage('not enough arguments given'); + return; + } + if (defaultSettings[args[1].trim()] == undefined) { + msg.channel.createMessage( + args[1] + + ' is not a valid setting.\navailable settings are:\n```\n' + + Object.keys(defaultSettings).join('\n') + + '\n```' + ); + return; + } + if (args[2] == undefined) { + let setting = ctx.settings.getSetting(msg.channel.guild.id, 'pin', args[1]); + if (setting == undefined) { + setting = defaultSettings[args[1]]; + ctx.settings.setSetting(msg.channel.guild.id, 'pin', args[1], defaultSettings[args[1]]); + } + msg.channel.createMessage(args[1] + ' is currently set to ' + setting); + } else { + ctx.settings.setSetting(msg.channel.guild.id, 'pin', args[1], args[2]); + msg.channel.createMessage('set the value of ' + args[1] + ' to ' + args[2]); + } +} + +class PinCommand extends Command { + name = 'pin'; + perms = 32; + description = 'manage the pin module'; + helptext = ''; + func(msg, args, ctx) { + switch (args[0]) { + case 'settings': + case 'setting': + settingsSubcommand(msg, args, ctx); + break; + case undefined: + msg.channel.createMessage('no arguments given'); + break; + default: + msg.channel.createMessage('unknown argument ' + args[0]); + } + } +} + +initializer.addCommand(new PinCommand()); + +export default initializer; diff --git a/cmd/reg.js b/cmd/reg.js index ada4883..e2cfb07 100644 --- a/cmd/reg.js +++ b/cmd/reg.js @@ -7,12 +7,13 @@ const log = new Logger(filename(import.meta.url), global.ctx.log_level); class PingCommand extends Command { name = 'ping'; + aliases = ['p']; func(msg, args, ctx) { msg.channel.createMessage('p').then((m) => { m.edit( - `rtt: ${Math.floor(m.timestamp - msg.timestamp)}, gateway: ${ + `rtt: ${Math.floor(m.timestamp - msg.timestamp)}ms, gateway: ${ ctx.bot.shards.get(ctx.bot.guildShardMap[ctx.bot.channelGuildMap[msg.channel.id]] || 0).latency - }` + }ms` ); }); } @@ -35,18 +36,31 @@ 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"); - } + log.debug('evaluating ' + args[0]); + let result; + try { + result = eval(args[0]); + } catch (e) { + result = e.stack; + } + log.debug('result is: \n' + result); + if (String.valueOf(result).length <= 1999) { + msg.channel.createMessage('```\n' + result + '```\n'); + } else { + msg.channel.createMessage('result too long, see logs'); + } } } initializer.addCommand(new EvalCommand()); +class HelpCommand extends Command { + name = 'help'; + description = 'get help for commands'; + helptext = '```md\nhelp\nshows this text\n\nhelp \nshows the helptext for '; + func(msg, args, ctx) {} +} + +initializer.addCommand(new HelpCommand()); + export default initializer; diff --git a/parser.js b/parser.js index 5edb2d9..5610894 100644 --- a/parser.js +++ b/parser.js @@ -1,5 +1,5 @@ import Logger, { levels } from './logger.js'; -import { filename } from './utils.js'; +import { filename, getPerms, hasPerms } from './utils.js'; export class Command { init(ctx, log) { @@ -7,17 +7,31 @@ export class Command { } log; name = 'DEFAULT'; + description = 'No description'; + helptext = 'No helptext'; + aliases = ''; whitelist = false; + perms = 0; func() {} } +export class Event { + event = 'NONE'; + func(...args) {} +} + export class CommandInitializer { + ctx; + log; commands = []; + aliases = {}; uninitialized = []; events = {}; initialize(ctx) { - for (let index in this.uninitialized) { + this.ctx = ctx; + this.log = new Logger('parser', ctx.log_level); + for (const index in this.uninitialized) { this.initCommand(this.uninitialized[index], ctx); delete this.uninitialized[index]; } @@ -26,24 +40,34 @@ export class CommandInitializer { initCommand(cmd, ctx) { cmd.init(ctx, new Logger(`cmd.${cmd.name}`, ctx.log_level)); this.commands.push(cmd); + for (const alias of cmd.aliases) { + this.aliases[alias] = cmd.name; + } } addCommand(cmd) { this.uninitialized.push(cmd); } - addEvent(event, func) { - if (!events[event]) { - events[event] = []; + addEvent(handler) { + console.log(handler); + const event = handler.event; + if (!this.events[event]) { + this.events[event] = []; } - events[event].push(func); + this.events[event].push(handler); } getCommands() { return this.commands; } + getAliases() { + return this.aliases; + } + getEvents() { + console.log(this.events); return this.events; } } @@ -56,6 +80,7 @@ export default class CommandParser { log; prefix; commands = []; + aliases = {}; addCommand(cmd) { this.log.trace(`cmd to add: ${JSON.stringify(cmd)}`); @@ -64,8 +89,20 @@ export default class CommandParser { } } + addAliases(aliases) { + this.log.debug('added ' + Object.keys(aliases).length + ' aliases'); + this.aliases = { + ...this.aliases, + ...aliases, + }; + } + hasCmd(str) { - let results = this.commands.filter((c) => c.name == str); + if (this.aliases && this.aliases[str] != undefined) { + str = this.aliases[str]; + } + + const results = this.commands.filter((c) => c.name == str); return results.length ? results[0] : undefined; } @@ -75,7 +112,7 @@ export default class CommandParser { } getArgsList(split) { - let parsed_args = []; + const parsed_args = []; let join_index = -1; let add = true; for (let index in split) { @@ -112,18 +149,29 @@ export default class CommandParser { 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)); + const snip = msg.content.slice(this.prefix.length); + const unsep = snip.split(' '); + const res = this.hasCmd(unsep[0]); + const 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}`); + const perms = msg.channel.guild.members.get(msg.author.id).permissions; + const resolvedPerms = res.perms.toString(); + this.log.trace(resolvedPerms); + this.log.trace(perms); if (res.whitelist && !ctx.whitelist.user(msg.author)) { msg.channel.createMessage('not whitelisted'); + } else if (resolvedPerms && !hasPerms(perms, resolvedPerms)) { + const required = getPerms(perms.allow.toString() & res.perms).join(', '); + msg.channel.createMessage('not enough permissions.').then((sent) => { + if (required.length <= 1000) { + sent.edit('not enough permissions. needs: `' + required + '`'); + } + }); } else { res.func(msg, args, ctx); } diff --git a/utils.js b/utils.js index 24fad97..f9ced32 100644 --- a/utils.js +++ b/utils.js @@ -1,15 +1,34 @@ -import path from "path"; -import {platform} from "process"; +import path from 'path'; +import { platform } from 'process'; +import { Constants } from 'eris'; export function filename(url) { - let __filename = new URL(url).pathname; - return path.basename(__filename, ".js"); + const __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 + const __filename = new URL(url).pathname; + const __dirname = path.dirname(__filename); + return platform == 'win32' ? __dirname.slice(1) : __dirname; +} + +export function getPerms(perms) { + const result = []; + for (const key in Constants.Permissions) { + if (Constants.Permissions[key].toString() & perms) { + result.push(key); + } + } + return result; +} + +export function hasPerms(perms, test) { + let testPerms = getPerms(test); + for (const tp of testPerms) { + if (!perms.has(tp)) { + return false; + } + } + return true; +}