Switch to knub-command-manager instead of Eris's command client. Update sqlite3 for Node.js 12 support.

master
Dragory 2019-06-16 22:27:30 +03:00
parent 68ec9748a1
commit 38c7bd83ff
18 changed files with 385 additions and 350 deletions

134
package-lock.json generated
View File

@ -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": {

View File

@ -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"

View File

@ -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;

130
src/commands.js Normal file
View File

@ -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,
};
}
};

View File

@ -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();

View File

@ -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 {

View File

@ -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', '<userId:userId> [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', '<userId:userId> [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);

View File

@ -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) {

View File

@ -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);
});
};

View File

@ -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-<max page>
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', '<userId:userId> [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;

View File

@ -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', '<category:string$>', async (msg, args, thread) => {
const searchStr = args.category;
const normalizedSearchStr = transliterate.slugify(searchStr);
const categories = msg.channel.guild.channels.filter(c => {

View File

@ -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', '<userId:userId>', 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}>`);
});
};

View File

@ -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', '<text$>', 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', '<text$>', 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');
};

View File

@ -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', '<trigger> [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', '<trigger>', 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', '<trigger> [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();

View File

@ -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) {

View File

@ -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;

View File

@ -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
};

View File

@ -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,
};