diff --git a/package-lock.json b/package-lock.json index a8b3352..e7791e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,7 +45,6 @@ "version": "6.10.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", - "dev": true, "requires": { "fast-deep-equal": "^2.0.1", "fast-json-stable-stringify": "^2.0.0", @@ -445,9 +444,9 @@ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "combined-stream": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", - "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "requires": { "delayed-stream": "~1.0.0" } @@ -1155,9 +1154,9 @@ } }, "fs-minipass": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", - "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.6.tgz", + "integrity": "sha512-crhvyXcMejjv3Z5d2Fa9sf5xLYVCF5O1c71QxbVnbLsmYMBEvDAftewesN/HhY03YRoA7zOMxjNGrF5svGaaeQ==", "requires": { "minipass": "^2.2.1" } @@ -1227,6 +1226,7 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -1299,19 +1299,6 @@ "requires": { "ajv": "^6.5.5", "har-schema": "^2.0.0" - }, - "dependencies": { - "ajv": { - "version": "6.9.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.9.2.tgz", - "integrity": "sha512-4UFy0/LgDo7Oa/+wOAlj44tp9K78u38E5/359eSrqEp1Z5PdVfimCcs7SluXMP755RUQu6d2b4AvF0R1C9RZjg==", - "requires": { - "fast-deep-equal": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - } } }, "has-flag": { @@ -1850,6 +1837,21 @@ } } }, + "knub-command-manager": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/knub-command-manager/-/knub-command-manager-3.1.2.tgz", + "integrity": "sha512-ylSRl84FcRjanvE1cMXhgrHIMOEgYmUEFHDutbiUr5aAg/C1coHE/HrPIwiovhDfRLU2++Y79yJoV99Jmwy4mA==", + "requires": { + "escape-string-regexp": "^2.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==" + } + } + }, "lcid": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", @@ -1978,16 +1980,16 @@ "integrity": "sha512-OEUllcVoydBHGN1z84yfQDimn58pZNNNXgZlHXSboxMlFvgI6MXSWpWKpFRra7H1HxpVhHTkrghfRW49k6yjeg==" }, "mime-db": { - "version": "1.38.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz", - "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==" + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" }, "mime-types": { - "version": "2.1.22", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz", - "integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==", + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", "requires": { - "mime-db": "~1.38.0" + "mime-db": "1.40.0" } }, "mimic-fn": { @@ -2081,9 +2083,9 @@ "dev": true }, "nan": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", - "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==" + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" }, "nanomatch": { "version": "1.2.13", @@ -2110,13 +2112,28 @@ "dev": true }, "needle": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/needle/-/needle-2.2.4.tgz", - "integrity": "sha512-HyoqEb4wr/rsoaIDfTH2aVL9nWtQqba2/HvMv+++m8u0dz808MaagKILxtfeSN7QU7nvbQ79zk3vYOJp9zsNEA==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.4.0.tgz", + "integrity": "sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg==", "requires": { - "debug": "^2.1.2", + "debug": "^3.2.6", "iconv-lite": "^0.4.4", "sax": "^1.2.4" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } } }, "nice-try": { @@ -2490,9 +2507,9 @@ "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" }, "psl": { - "version": "1.1.31", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz", - "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==" + "version": "1.1.32", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.32.tgz", + "integrity": "sha512-MHACAkHpihU/REGGPLj4sEfc/XKW2bheigvHO1dUqjaKigMp1C8+WLQYRGgeKFMsw5PMfegZcaN8IDXK/cD0+g==" }, "public-ip": { "version": "2.3.5", @@ -2709,11 +2726,26 @@ "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" }, "rimraf": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.1.tgz", - "integrity": "sha1-wjOOxkPfeht/5cVPqG9XQopV8z0=", + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", "requires": { - "glob": "^7.0.5" + "glob": "^7.1.3" + }, + "dependencies": { + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } } }, "run-async": { @@ -2977,11 +3009,11 @@ "dev": true }, "sqlite3": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.0.6.tgz", - "integrity": "sha512-EqBXxHdKiwvNMRCgml86VTL5TK1i0IKiumnfxykX0gh6H6jaKijAXvE9O1N7+omfNSawR2fOmIyJZcfe8HYWpw==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.0.9.tgz", + "integrity": "sha512-IkvzjmsWQl9BuBiM4xKpl5X8WCR4w0AeJHRdobCdXZ8dT/lNc1XS6WqvY35N6+YzIIgzSBeY5prdFObID9F9tA==", "requires": { - "nan": "~2.10.0", + "nan": "^2.12.1", "node-pre-gyp": "^0.11.0", "request": "^2.87.0" } @@ -3151,17 +3183,17 @@ } }, "tar": { - "version": "4.4.8", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.8.tgz", - "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==", + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.10.tgz", + "integrity": "sha512-g2SVs5QIxvo6OLp0GudTqEf05maawKUxXru104iaayWA09551tFCTI8f1Asb4lPfkBr91k07iL4c11XO3/b0tA==", "requires": { "chownr": "^1.1.1", "fs-minipass": "^1.2.5", - "minipass": "^2.3.4", - "minizlib": "^1.1.1", + "minipass": "^2.3.5", + "minizlib": "^1.2.1", "mkdirp": "^0.5.0", "safe-buffer": "^5.1.2", - "yallist": "^3.0.2" + "yallist": "^3.0.3" }, "dependencies": { "safe-buffer": { diff --git a/package.json b/package.json index 9347a7a..7d4c475 100644 --- a/package.json +++ b/package.json @@ -19,10 +19,11 @@ "humanize-duration": "^3.12.1", "json5": "^1.0.1", "knex": "^0.15.2", + "knub-command-manager": "^3.1.2", "mime": "^2.3.1", "moment": "^2.21.0", "public-ip": "^2.0.1", - "sqlite3": "^4.0.6", + "sqlite3": "^4.0.9", "tmp": "0.0.33", "transliteration": "^1.6.2", "uuid": "^3.1.0" diff --git a/src/bot.js b/src/bot.js index 05ffec7..2a1591a 100644 --- a/src/bot.js +++ b/src/bot.js @@ -1,17 +1,9 @@ const Eris = require('eris'); const config = require('./config'); -const bot = new Eris.CommandClient(config.token, { +const bot = new Eris.Client(config.token, { getAllUsers: true, restMode: true, -}, { - prefix: config.prefix, - ignoreSelf: true, - ignoreBots: true, - defaultHelpCommand: false, - defaultCommandOptions: { - caseInsensitive: true, - }, }); module.exports = bot; diff --git a/src/commands.js b/src/commands.js new file mode 100644 index 0000000..59c6506 --- /dev/null +++ b/src/commands.js @@ -0,0 +1,130 @@ +const { CommandManager, defaultParameterTypes, TypeConversionError } = require('knub-command-manager'); +const config = require('./config'); +const utils = require('./utils'); +const threads = require('./data/threads'); + +module.exports = { + createCommandManager(bot) { + const manager = new CommandManager({ + prefix: config.prefix, + types: Object.assign({}, defaultParameterTypes, { + userId(value) { + const userId = utils.getUserMention(value); + if (! userId) throw new TypeConversionError(); + return userId; + }, + + delay(value) { + const ms = utils.convertDelayStringToMS(value); + if (ms === null) throw new TypeConversionError(); + return ms; + } + }) + }); + + const handlers = {}; + const aliasMap = new Map(); + + bot.on('messageCreate', async msg => { + if (msg.author.bot) return; + if (msg.author.id === bot.user.id) return; + if (! msg.content) return; + + const matchedCommand = await manager.findMatchingCommand(msg.content, { msg }); + if (matchedCommand === null) return; + if (matchedCommand.error !== undefined) { + utils.postError(msg.channel, matchedCommand.error); + return; + } + + const allArgs = {}; + for (const [name, arg] of Object.entries(matchedCommand.args)) { + allArgs[name] = arg.value; + } + for (const [name, opt] of Object.entries(matchedCommand.opts)) { + allArgs[name] = opt.value; + } + + handlers[matchedCommand.id](msg, allArgs); + }); + + /** + * Add a command that can be invoked anywhere + */ + const addGlobalCommand = (trigger, parameters, handler, commandConfig = {}) => { + let aliases = aliasMap.has(trigger) ? [...aliasMap.get(trigger)] : []; + if (commandConfig.aliases) aliases.push(...commandConfig.aliases); + + const cmd = manager.add(trigger, parameters, { ...commandConfig, aliases }); + handlers[cmd.id] = handler; + }; + + /** + * Add a command that can only be invoked on the inbox server + */ + const addInboxServerCommand = (trigger, parameters, handler, commandConfig = {}) => { + const aliases = aliasMap.has(trigger) ? [...aliasMap.get(trigger)] : []; + if (commandConfig.aliases) aliases.push(...commandConfig.aliases); + + const cmd = manager.add(trigger, parameters, { + ...commandConfig, + aliases, + preFilters: [ + (_, context) => { + if (! utils.messageIsOnInboxServer(context.msg)) return false; + if (! utils.isStaff(context.msg.member)) return false; + return true; + } + ] + }); + + handlers[cmd.id] = async (msg, args) => { + const thread = await threads.findOpenThreadByChannelId(msg.channel.id); + handler(msg, args, thread); + }; + }; + + /** + * Add a command that can only be invoked in a thread on the inbox server + */ + const addInboxThreadCommand = (trigger, parameters, handler, commandConfig = {}) => { + const aliases = aliasMap.has(trigger) ? [...aliasMap.get(trigger)] : []; + if (commandConfig.aliases) aliases.push(...commandConfig.aliases); + + let thread; + const cmd = manager.add(trigger, parameters, { + ...commandConfig, + aliases, + preFilters: [ + async (_, context) => { + if (! utils.messageIsOnInboxServer(context.msg)) return false; + if (! utils.isStaff(context.msg.member)) return false; + thread = await threads.findOpenThreadByChannelId(context.msg.channel.id); + if (! thread) return false; + return true; + } + ] + }); + + handlers[cmd.id] = async (msg, args) => { + handler(msg, args, thread); + }; + }; + + const addAlias = (originalCmd, alias) => { + if (! aliasMap.has(originalCmd)) { + aliasMap.set(originalCmd, new Set()); + } + + aliasMap.get(originalCmd).add(alias); + }; + + return { + manager, + addGlobalCommand, + addInboxServerCommand, + addInboxThreadCommand, + addAlias, + }; + } +}; diff --git a/src/main.js b/src/main.js index efdd0ca..40ad69a 100644 --- a/src/main.js +++ b/src/main.js @@ -6,6 +6,8 @@ const bot = require('./bot'); const knex = require('./knex'); const {messageQueue} = require('./queue'); const utils = require('./utils'); +const { createCommandManager } = require('./commands'); + const blocked = require('./data/blocked'); const threads = require('./data/threads'); const updates = require('./data/updates'); @@ -182,44 +184,54 @@ bot.on('messageCreate', async msg => { module.exports = { async start() { - // Load modules - console.log('Loading modules...'); - await reply(bot); - await close(bot); - await logs(bot); - await block(bot); - await move(bot); - await snippets(bot); - await suspend(bot); - await greeting(bot); - await webserver(bot); - await typingProxy(bot); - await version(bot); - await newthread(bot); - await idModule(bot); - await alert(bot); - - // Load plugins - if (config.plugins && config.plugins.length) { - console.log('Loading plugins...'); - for (const plugin of config.plugins) { - const pluginFn = require(`../${plugin}`); - pluginFn(bot, knex, config); - } - console.log(`Loaded ${config.plugins.length} plugin(s)`); - } - - if (config.updateNotifications) { - updates.startVersionRefreshLoop(); - } + // Initialize command manager + const commands = createCommandManager(bot); // Register command aliases if (config.commandAliases) { for (const alias in config.commandAliases) { - bot.registerCommandAlias(alias, config.commandAliases[alias]); + commands.addAlias(config.commandAliases[alias], alias); } } + // Load modules + console.log('Loading plugins...'); + const builtInPlugins = [ + reply, + close, + logs, + block, + move, + snippets, + suspend, + greeting, + webserver, + typingProxy, + version, + newthread, + idModule, + alert + ]; + + const plugins = [...builtInPlugins]; + + if (config.plugins && config.plugins.length) { + for (const plugin of config.plugins) { + const pluginFn = require(`../${plugin}`); + plugins.push(pluginFn); + } + } + + plugins.forEach(pluginFn => { + pluginFn(bot, knex, config, commands); + }); + + console.log(`Loaded ${plugins.length} plugins (${builtInPlugins.length} built-in plugins, ${plugins.length - builtInPlugins.length} external plugins)`); + + if (config.updateNotifications) { + updates.startVersionRefreshLoop(); + } + // Connect to Discord console.log('Connecting to Discord...'); await bot.connect(); diff --git a/src/modules/alert.js b/src/modules/alert.js index 1db7f03..f2c49c0 100644 --- a/src/modules/alert.js +++ b/src/modules/alert.js @@ -1,12 +1,6 @@ -const threadUtils = require('../threadUtils'); - -module.exports = bot => { - const addInboxServerCommand = (...args) => threadUtils.addInboxServerCommand(bot, ...args); - - addInboxServerCommand('alert', async (msg, args, thread) => { - if (! thread) return; - - if (args[0] && args[0].startsWith('c')) { +module.exports = (bot, knex, config, commands) => { + commands.addInboxThreadCommand('alert', '[opt:string]', async (msg, args, thread) => { + if (args.opt && args.opt.startsWith('c')) { await thread.setAlert(null); await thread.postSystemMessage(`Cancelled new message alert`); } else { diff --git a/src/modules/block.js b/src/modules/block.js index fd4428b..2658136 100644 --- a/src/modules/block.js +++ b/src/modules/block.js @@ -1,12 +1,9 @@ const humanizeDuration = require('humanize-duration'); const moment = require('moment'); -const threadUtils = require('../threadUtils'); const blocked = require("../data/blocked"); const utils = require("../utils"); -module.exports = bot => { - const addInboxServerCommand = (...args) => threadUtils.addInboxServerCommand(bot, ...args); - +module.exports = (bot, knex, config, commands) => { async function removeExpiredBlocks() { const expiredBlocks = await blocked.getExpiredBlocks(); const logChannel = utils.getLogChannel(); @@ -28,12 +25,8 @@ module.exports = bot => { bot.on('ready', expiredBlockLoop); - addInboxServerCommand('block', async (msg, args, thread) => { - const firstArgUserId = utils.getUserMention(args[0]); - const userIdToBlock = firstArgUserId - ? firstArgUserId - : thread && thread.user_id; - + const blockCmd = async (msg, args, thread) => { + const userIdToBlock = args.userId || (thread && thread.user_id); if (! userIdToBlock) return; const isBlocked = await blocked.isBlocked(userIdToBlock); @@ -42,29 +35,26 @@ module.exports = bot => { return; } - const inputExpiryTime = firstArgUserId ? args[1] : args[0]; - const expiryTime = inputExpiryTime ? utils.convertDelayStringToMS(inputExpiryTime) : null; - const expiresAt = expiryTime - ? moment.utc().add(expiryTime, 'ms').format('YYYY-MM-DD HH:mm:ss') + const expiresAt = args.blockTime + ? moment.utc().add(args.blockTime, 'ms').format('YYYY-MM-DD HH:mm:ss') : null; const user = bot.users.get(userIdToBlock); await blocked.block(userIdToBlock, (user ? `${user.username}#${user.discriminator}` : ''), msg.author.id, expiresAt); if (expiresAt) { - const humanized = humanizeDuration(expiryTime, { largest: 2, round: true }); + const humanized = humanizeDuration(args.blockTime, { largest: 2, round: true }); msg.channel.createMessage(`Blocked <@${userIdToBlock}> (id \`${userIdToBlock}\`) from modmail for ${humanized}`); } else { msg.channel.createMessage(`Blocked <@${userIdToBlock}> (id \`${userIdToBlock}\`) from modmail indefinitely`); } - }); + }; - addInboxServerCommand('unblock', async (msg, args, thread) => { - const firstArgUserId = utils.getUserMention(args[0]); - const userIdToUnblock = firstArgUserId - ? firstArgUserId - : thread && thread.user_id; + commands.addInboxServerCommand('block', ' [blockTime:delay]', blockCmd); + commands.addInboxServerCommand('block', '[blockTime:delay]', blockCmd); + const unblockCmd = async (msg, args, thread) => { + const userIdToUnblock = args.userId || (thread && thread.user_id); if (! userIdToUnblock) return; const isBlocked = await blocked.isBlocked(userIdToUnblock); @@ -73,28 +63,26 @@ module.exports = bot => { return; } - const inputUnblockDelay = firstArgUserId ? args[1] : args[0]; - const unblockDelay = inputUnblockDelay ? utils.convertDelayStringToMS(inputUnblockDelay) : null; - const unblockAt = unblockDelay - ? moment.utc().add(unblockDelay, 'ms').format('YYYY-MM-DD HH:mm:ss') + const unblockAt = args.unblockDelay + ? moment.utc().add(args.unblockDelay, 'ms').format('YYYY-MM-DD HH:mm:ss') : null; const user = bot.users.get(userIdToUnblock); if (unblockAt) { - const humanized = humanizeDuration(unblockDelay, { largest: 2, round: true }); + const humanized = humanizeDuration(args.unblockDelay, { largest: 2, round: true }); await blocked.updateExpiryTime(userIdToUnblock, unblockAt); msg.channel.createMessage(`Scheduled <@${userIdToUnblock}> (id \`${userIdToUnblock}\`) to be unblocked in ${humanized}`); } else { await blocked.unblock(userIdToUnblock); msg.channel.createMessage(`Unblocked <@${userIdToUnblock}> (id ${userIdToUnblock}) from modmail`); } - }); + }; - addInboxServerCommand('is_blocked', async (msg, args, thread) => { - const userIdToCheck = args[0] - ? utils.getUserMention(args[0]) - : thread && thread.user_id; + commands.addInboxServerCommand('unblock', ' [unblockDelay:delay]', unblockCmd); + commands.addInboxServerCommand('unblock', '[unblockDelay:delay]', unblockCmd); + commands.addInboxServerCommand('is_blocked', '[userId:userId]',async (msg, args, thread) => { + const userIdToCheck = args.userId || (thread && thread.user_id); if (! userIdToCheck) return; const blockStatus = await blocked.getBlockStatus(userIdToCheck); diff --git a/src/modules/close.js b/src/modules/close.js index acedc62..fad5a91 100644 --- a/src/modules/close.js +++ b/src/modules/close.js @@ -6,7 +6,7 @@ const threads = require('../data/threads'); const blocked = require('../data/blocked'); const {messageQueue} = require('../queue'); -module.exports = bot => { +module.exports = (bot, knex, config, commands) => { // Check for threads that are scheduled to be closed and close them async function applyScheduledCloses() { const threadsToBeClosed = await threads.getThreadsThatShouldBeClosed(); @@ -38,7 +38,7 @@ module.exports = bot => { scheduledCloseLoop(); // Close a thread. Closing a thread saves a log of the channel's contents and then deletes the channel. - bot.registerCommand('close', async (msg, args) => { + commands.addGlobalCommand('close', '[opts...]', async (msg, args) => { let thread, closedBy; let hasCloseMessage = !! config.closeMessage; @@ -68,8 +68,8 @@ module.exports = bot => { thread = await threads.findOpenThreadByChannelId(msg.channel.id); if (! thread) return; - if (args.length) { - if (args.includes('cancel') || args.includes('c')) { + if (args.opts && args.opts.length) { + if (args.opts.includes('cancel') || args.opts.includes('c')) { // Cancel timed close if (thread.scheduled_close_at) { await thread.cancelScheduledClose(); @@ -80,12 +80,12 @@ module.exports = bot => { } // Silent close (= no close message) - if (args.includes('silent') || args.includes('s')) { + if (args.opts.includes('silent') || args.opts.includes('s')) { silentClose = true; } // Timed close - const delayStringArg = args.find(arg => utils.delayStringRegex.test(arg)); + const delayStringArg = args.opts.find(arg => utils.delayStringRegex.test(arg)); if (delayStringArg) { const delay = utils.convertDelayStringToMS(delayStringArg); if (delay === 0 || delay === null) { diff --git a/src/modules/id.js b/src/modules/id.js index 7e8c6b1..9024fd1 100644 --- a/src/modules/id.js +++ b/src/modules/id.js @@ -1,10 +1,5 @@ -const threadUtils = require("../threadUtils"); - -module.exports = bot => { - const addInboxServerCommand = (...args) => threadUtils.addInboxServerCommand(bot, ...args); - - addInboxServerCommand('id', async (msg, args, thread) => { - if (! thread) return; +module.exports = (bot, knex, config, commands) => { + commands.addInboxThreadCommand('id', [], async (msg, args, thread) => { thread.postSystemMessage(thread.user_id); }); }; diff --git a/src/modules/logs.js b/src/modules/logs.js index f24c77f..76009e8 100644 --- a/src/modules/logs.js +++ b/src/modules/logs.js @@ -1,19 +1,12 @@ -const threadUtils = require('../threadUtils'); const threads = require("../data/threads"); const moment = require('moment'); const utils = require("../utils"); const LOG_LINES_PER_PAGE = 10; -module.exports = bot => { - const addInboxServerCommand = (...args) => threadUtils.addInboxServerCommand(bot, ...args); - - addInboxServerCommand('logs', async (msg, args, thread) => { - const firstArgUserId = utils.getUserMention(args[0]); - let userId = firstArgUserId - ? firstArgUserId - : thread && thread.user_id; - +module.exports = (bot, knex, config, commands) => { + const logsCmd = async (msg, args, thread) => { + let userId = args.userId || (thread && thread.user_id); if (! userId) return; let userThreads = await threads.getClosedThreadsByUserId(userId); @@ -28,7 +21,7 @@ module.exports = bot => { // Pagination const totalUserThreads = userThreads.length; const maxPage = Math.ceil(totalUserThreads / LOG_LINES_PER_PAGE); - const inputPage = firstArgUserId ? args[1] : args[0]; + const inputPage = args.page; const page = Math.max(Math.min(inputPage ? parseInt(inputPage, 10) : 1, maxPage), 1); // Clamp page to 1- const isPaginated = totalUserThreads > LOG_LINES_PER_PAGE; const start = (page - 1) * LOG_LINES_PER_PAGE; @@ -59,9 +52,12 @@ module.exports = bot => { chunks.forEach(lines => { root = root.then(() => msg.channel.createMessage(lines.join('\n'))); }); - }); + }; - addInboxServerCommand('loglink', async (msg, args, thread) => { + commands.addInboxServerCommand('logs', ' [page:number]', logsCmd); + commands.addInboxServerCommand('logs', '[page:number]', logsCmd); + + commands.addInboxServerCommand('loglink', [], async (msg, args, thread) => { if (! thread) { thread = await threads.findSuspendedThreadByChannelId(msg.channel.id); if (! thread) return; diff --git a/src/modules/move.js b/src/modules/move.js index 8d5a9bc..dd63166 100644 --- a/src/modules/move.js +++ b/src/modules/move.js @@ -1,20 +1,13 @@ const config = require('../config'); const Eris = require('eris'); -const threadUtils = require('../threadUtils'); const transliterate = require("transliteration"); const erisEndpoints = require('eris/lib/rest/Endpoints'); -module.exports = bot => { - const addInboxServerCommand = (...args) => threadUtils.addInboxServerCommand(bot, ...args); - - addInboxServerCommand('move', async (msg, args, thread) => { - if (! config.allowMove) return; - - if (! thread) return; - - const searchStr = args[0]; - if (! searchStr || searchStr.trim() === '') return; +module.exports = (bot, knex, config, commands) => { + if (! config.allowMove) return; + commands.addInboxThreadCommand('move', '', async (msg, args, thread) => { + const searchStr = args.category; const normalizedSearchStr = transliterate.slugify(searchStr); const categories = msg.channel.guild.channels.filter(c => { @@ -33,7 +26,7 @@ module.exports = bot => { if (! normalizedCatName.includes(normalizedSearchStr.slice(0, i + 1))) break; i++; } while (i < normalizedSearchStr.length); - + if (i > 0 && normalizedCatName.startsWith(normalizedSearchStr.slice(0, i))) { // Slightly prioritize categories that *start* with the search string i += 0.5; diff --git a/src/modules/newthread.js b/src/modules/newthread.js index f3dfaca..ccd63db 100644 --- a/src/modules/newthread.js +++ b/src/modules/newthread.js @@ -1,17 +1,9 @@ const utils = require("../utils"); -const threadUtils = require("../threadUtils"); const threads = require("../data/threads"); -module.exports = bot => { - const addInboxServerCommand = (...args) => threadUtils.addInboxServerCommand(bot, ...args); - - addInboxServerCommand('newthread', async (msg, args, thread) => { - if (args.length === 0) return; - - const userId = utils.getUserMention(args[0]); - if (! userId) return; - - const user = bot.users.get(userId); +module.exports = (bot, knex, config, commands) => { + commands.addInboxServerCommand('newthread', '', async (msg, args, thread) => { + const user = bot.users.get(args.userId); if (! user) { utils.postSystemMessageWithFallback(msg.channel, thread, 'User not found!'); return; @@ -26,8 +18,6 @@ module.exports = bot => { const createdThread = await threads.createNewThreadForUser(user, true, true); createdThread.postSystemMessage(`Thread was opened by ${msg.author.username}#${msg.author.discriminator}`); - if (thread) { - msg.delete(); - } + msg.channel.createMessage(`Thread opened: <#${createdThread.channel_id}>`); }); }; diff --git a/src/modules/reply.js b/src/modules/reply.js index f041cc9..545fec3 100644 --- a/src/modules/reply.js +++ b/src/modules/reply.js @@ -1,29 +1,21 @@ const attachments = require("../data/attachments"); -const threadUtils = require("../threadUtils"); - -module.exports = bot => { - const addInboxServerCommand = (...args) => threadUtils.addInboxServerCommand(bot, ...args); +module.exports = (bot, knex, config, commands) => { // Mods can reply to modmail threads using !r or !reply // These messages get relayed back to the DM thread between the bot and the user - addInboxServerCommand('reply', async (msg, args, thread) => { - if (! thread) return; - - const text = args.join(' ').trim(); - const replied = await thread.replyToUser(msg.member, text, msg.attachments, false); + commands.addInboxThreadCommand('reply', '', async (msg, args, thread) => { + const replied = await thread.replyToUser(msg.member, args.text, msg.attachments, false); if (replied) msg.delete(); + }, { + aliases: ['r'] }); - bot.registerCommandAlias('r', 'reply'); // Anonymous replies only show the role, not the username - addInboxServerCommand('anonreply', async (msg, args, thread) => { - if (! thread) return; - - const text = args.join(' ').trim(); - const replied = await thread.replyToUser(msg.member, text, msg.attachments, true); + commands.addInboxThreadCommand('anonreply', '', async (msg, args, thread) => { + const replied = await thread.replyToUser(msg.member, args.text, msg.attachments, true); if (replied) msg.delete(); + }, { + aliases: ['ar'] }); - - bot.registerCommandAlias('ar', 'anonreply'); }; diff --git a/src/modules/snippets.js b/src/modules/snippets.js index 0fd6352..e7cc3ba 100644 --- a/src/modules/snippets.js +++ b/src/modules/snippets.js @@ -2,50 +2,12 @@ const threads = require('../data/threads'); const snippets = require('../data/snippets'); const config = require('../config'); const utils = require('../utils'); -const threadUtils = require('../threadUtils'); +const { parseArguments } = require('knub-command-manager'); const whitespaceRegex = /\s/; const quoteChars = ["'", '"']; -module.exports = bot => { - const addInboxServerCommand = (...args) => threadUtils.addInboxServerCommand(bot, ...args); - - /** - * Parse a string of arguments, e.g. - * arg "quoted arg" with\ some\ escapes - * ...to an array of said arguments - * @param {String} str - * @returns {String[]} - */ - function parseArgs(str) { - const args = []; - let current = ''; - let inQuote = false; - let escapeNext = false; - - for (const char of [...str]) { - if (escapeNext) { - current += char; - escapeNext = false; - } else if (char === '\\') { - escapeNext = true; - } else if (! inQuote && whitespaceRegex.test(char)) { - args.push(current); - current = ''; - } else if (! inQuote && quoteChars.includes(char)) { - inQuote = char; - } else if (inQuote && inQuote === char) { - inQuote = false; - } else { - current += char; - } - } - - if (current !== '') args.push(current); - - return args; - } - +module.exports = (bot, knex, config, commands) => { /** * "Renders" a snippet by replacing all argument placeholders e.g. {1} {2} with their corresponding arguments. * The number in the placeholder is the argument's order in the argument list, i.e. {1} is the first argument (= index 0) @@ -104,7 +66,8 @@ module.exports = bot => { const snippet = await snippets.get(trigger); if (! snippet) return; - const args = rawArgs ? parseArgs(rawArgs) : []; + let args = rawArgs ? parseArguments(rawArgs) : []; + args = args.map(arg => arg.value); const rendered = renderSnippet(snippet.body, args); const replied = await thread.replyToUser(msg.member, rendered, [], isAnonymous); @@ -112,73 +75,60 @@ module.exports = bot => { }); // Show or add a snippet - addInboxServerCommand('snippet', async (msg, args, thread) => { - const trigger = args[0]; - if (! trigger) return - - const text = args.slice(1).join(' ').trim(); - const snippet = await snippets.get(trigger); + commands.addInboxServerCommand('snippet', ' [text$]', async (msg, args, thread) => { + const snippet = await snippets.get(args.trigger); if (snippet) { - if (text) { + if (args.text) { // If the snippet exists and we're trying to create a new one, inform the user the snippet already exists - utils.postSystemMessageWithFallback(msg.channel, thread, `Snippet "${trigger}" already exists! You can edit or delete it with ${config.prefix}edit_snippet and ${config.prefix}delete_snippet respectively.`); + utils.postSystemMessageWithFallback(msg.channel, thread, `Snippet "${args.trigger}" already exists! You can edit or delete it with ${config.prefix}edit_snippet and ${config.prefix}delete_snippet respectively.`); } else { // If the snippet exists and we're NOT trying to create a new one, show info about the existing snippet - utils.postSystemMessageWithFallback(msg.channel, thread, `\`${config.snippetPrefix}${trigger}\` replies with:\n${snippet.body}`); + utils.postSystemMessageWithFallback(msg.channel, thread, `\`${config.snippetPrefix}${args.trigger}\` replies with: \`\`\`${utils.disableCodeBlocks(snippet.body)}\`\`\``); } } else { - if (text) { + if (args.text) { // If the snippet doesn't exist and the user wants to create it, create it - await snippets.add(trigger, text, msg.author.id); - utils.postSystemMessageWithFallback(msg.channel, thread, `Snippet "${trigger}" created!`); + await snippets.add(args.trigger, args.text, msg.author.id); + utils.postSystemMessageWithFallback(msg.channel, thread, `Snippet "${args.trigger}" created!`); } else { // If the snippet doesn't exist and the user isn't trying to create it, inform them how to create it - utils.postSystemMessageWithFallback(msg.channel, thread, `Snippet "${trigger}" doesn't exist! You can create it with \`${config.prefix}snippet ${trigger} text\``); + utils.postSystemMessageWithFallback(msg.channel, thread, `Snippet "${args.trigger}" doesn't exist! You can create it with \`${config.prefix}snippet ${args.trigger} text\``); } } + }, { + aliases: ['s'] }); - bot.registerCommandAlias('s', 'snippet'); - - addInboxServerCommand('delete_snippet', async (msg, args, thread) => { - const trigger = args[0]; - if (! trigger) return; - - const snippet = await snippets.get(trigger); + commands.addInboxServerCommand('delete_snippet', '', async (msg, args, thread) => { + const snippet = await snippets.get(args.trigger); if (! snippet) { - utils.postSystemMessageWithFallback(msg.channel, thread, `Snippet "${trigger}" doesn't exist!`); + utils.postSystemMessageWithFallback(msg.channel, thread, `Snippet "${args.trigger}" doesn't exist!`); return; } - await snippets.del(trigger); - utils.postSystemMessageWithFallback(msg.channel, thread, `Snippet "${trigger}" deleted!`); + await snippets.del(args.trigger); + utils.postSystemMessageWithFallback(msg.channel, thread, `Snippet "${args.trigger}" deleted!`); + }, { + aliases: ['ds'] }); - bot.registerCommandAlias('ds', 'delete_snippet'); - - addInboxServerCommand('edit_snippet', async (msg, args, thread) => { - const trigger = args[0]; - if (! trigger) return; - - const text = args.slice(1).join(' ').trim(); - if (! text) return; - - const snippet = await snippets.get(trigger); + commands.addInboxServerCommand('edit_snippet', ' [text$]', async (msg, args, thread) => { + const snippet = await snippets.get(args.trigger); if (! snippet) { - utils.postSystemMessageWithFallback(msg.channel, thread, `Snippet "${trigger}" doesn't exist!`); + utils.postSystemMessageWithFallback(msg.channel, thread, `Snippet "${args.trigger}" doesn't exist!`); return; } - await snippets.del(trigger); - await snippets.add(trigger, text, msg.author.id); + await snippets.del(args.trigger); + await snippets.add(args.trigger, args.text, msg.author.id); - utils.postSystemMessageWithFallback(msg.channel, thread, `Snippet "${trigger}" edited!`); + utils.postSystemMessageWithFallback(msg.channel, thread, `Snippet "${args.trigger}" edited!`); + }, { + aliases: ['es'] }); - bot.registerCommandAlias('es', 'edit_snippet'); - - addInboxServerCommand('snippets', async (msg, args, thread) => { + commands.addInboxServerCommand('snippets', [], async (msg, args, thread) => { const allSnippets = await snippets.all(); const triggers = allSnippets.map(s => s.trigger); triggers.sort(); diff --git a/src/modules/suspend.js b/src/modules/suspend.js index ed05630..fed3859 100644 --- a/src/modules/suspend.js +++ b/src/modules/suspend.js @@ -1,14 +1,11 @@ const moment = require('moment'); -const threadUtils = require('../threadUtils'); const threads = require("../data/threads"); const utils = require('../utils'); const config = require('../config'); const {THREAD_STATUS} = require('../data/constants'); -module.exports = bot => { - const addInboxServerCommand = (...args) => threadUtils.addInboxServerCommand(bot, ...args); - +module.exports = (bot, knex, config, commands) => { // Check for threads that are scheduled to be suspended and suspend them async function applyScheduledSuspensions() { const threadsToBeSuspended = await threads.getThreadsThatShouldBeSuspended(); @@ -32,46 +29,41 @@ module.exports = bot => { scheduledSuspendLoop(); - addInboxServerCommand('suspend', async (msg, args, thread) => { - if (! thread) return; + commands.addInboxThreadCommand('suspend cancel', [], async (msg, args, thread) => { + // Cancel timed suspend + if (thread.scheduled_suspend_at) { + await thread.cancelScheduledSuspend(); + thread.postSystemMessage(`Cancelled scheduled suspension`); + } else { + thread.postSystemMessage(`Thread is not scheduled to be suspended`); + } + }); - if (args.length) { - // Cancel timed suspend - if (args.includes('cancel') || args.includes('c')) { - // Cancel timed suspend - if (thread.scheduled_suspend_at) { - await thread.cancelScheduledSuspend(); - thread.postSystemMessage(`Cancelled scheduled suspension`); - } + commands.addInboxThreadCommand('suspend', '[delay:delay]', async (msg, args, thread) => { + if (args.delay) { + const suspendAt = moment.utc().add(args.delay, 'ms'); + await thread.scheduleSuspend(suspendAt.format('YYYY-MM-DD HH:mm:ss'), msg.author); - return; - } + thread.postSystemMessage(`Thread will be suspended in ${utils.humanizeDelay(args.delay)}. Use \`${config.prefix}suspend cancel\` to cancel.`); - // Timed suspend - const delayStringArg = args.find(arg => utils.delayStringRegex.test(arg)); - if (delayStringArg) { - const delay = utils.convertDelayStringToMS(delayStringArg); - if (delay === 0 || delay === null) { - thread.postSystemMessage(`Invalid delay specified. Format: "1h30m"`); - return; - } - - const suspendAt = moment.utc().add(delay, 'ms'); - await thread.scheduleSuspend(suspendAt.format('YYYY-MM-DD HH:mm:ss'), msg.author); - - thread.postSystemMessage(`Thread will be suspended in ${utils.humanizeDelay(delay)}. Use \`${config.prefix}suspend cancel\` to cancel.`); - - return; - } + return; } await thread.suspend(); thread.postSystemMessage(`**Thread suspended!** This thread will act as closed until unsuspended with \`!unsuspend\``); }); - addInboxServerCommand('unsuspend', async msg => { - const thread = await threads.findSuspendedThreadByChannelId(msg.channel.id); - if (! thread) return; + commands.addInboxServerCommand('unsuspend', [], async (msg, args, thread) => { + if (thread) { + thread.postSystemMessage(`Thread is not suspended`); + return; + } + + thread = await threads.findSuspendedThreadByChannelId(msg.channel.id); + if (! thread) { + thread.postSystemMessage(`Not in a thread`); + return; + } const otherOpenThread = await threads.findOpenThreadByUserId(thread.user_id); if (otherOpenThread) { diff --git a/src/modules/version.js b/src/modules/version.js index a194b76..10c4cfd 100644 --- a/src/modules/version.js +++ b/src/modules/version.js @@ -2,7 +2,6 @@ const path = require('path'); const fs = require('fs'); const {promisify} = require('util'); const utils = require("../utils"); -const threadUtils = require("../threadUtils"); const updates = require('../data/updates'); const config = require('../config'); @@ -11,10 +10,8 @@ const readFile = promisify(fs.readFile); const GIT_DIR = path.join(__dirname, '..', '..', '.git'); -module.exports = bot => { - const addInboxServerCommand = (...args) => threadUtils.addInboxServerCommand(bot, ...args); - - addInboxServerCommand('version', async (msg, args, thread) => { +module.exports = (bot, knex, config, commands) => { + commands.addInboxServerCommand('version', [], async (msg, args, thread) => { const packageJson = require('../../package.json'); const packageVersion = packageJson.version; diff --git a/src/threadUtils.js b/src/threadUtils.js deleted file mode 100644 index 16b98e0..0000000 --- a/src/threadUtils.js +++ /dev/null @@ -1,24 +0,0 @@ -const threads = require('./data/threads'); -const utils = require("./utils"); - -/** - * Adds a command that can only be triggered on the inbox server. - * Command handlers added with this function also get the thread the message was posted in as a third argument, if any. - * @param {Eris~CommandClient} bot - * @param {String} cmd - * @param {Function} commandHandler - * @param {Eris~CommandOptions} opts - */ -function addInboxServerCommand(bot, cmd, commandHandler, opts) { - bot.registerCommand(cmd, async (msg, args) => { - if (! utils.messageIsOnInboxServer(msg)) return; - if (! utils.isStaff(msg.member)) return; - - const thread = await threads.findOpenThreadByChannelId(msg.channel.id); - commandHandler(msg, args, thread); - }, opts); -} - -module.exports = { - addInboxServerCommand -}; diff --git a/src/utils.js b/src/utils.js index b0f8362..7478193 100644 --- a/src/utils.js +++ b/src/utils.js @@ -65,10 +65,10 @@ function postLog(...args) { getLogChannel().createMessage(...args); } -function postError(str) { - getLogChannel().createMessage({ - content: `${getInboxMention()}**Error:** ${str.trim()}`, - disableEveryone: false +function postError(channel, str, opts = {}) { + return channel.createMessage({ + ...opts, + content: `⚠ ${str}` }); } @@ -306,6 +306,10 @@ function escapeMarkdown(str) { return str.replace(markdownCharsRegex, '\\$1'); } +function disableCodeBlocks(str) { + return str.replace(/`/g, "`\u200b"); +} + module.exports = { BotError, @@ -341,4 +345,5 @@ module.exports = { humanizeDelay, escapeMarkdown, + disableCodeBlocks, };