From a2f34113d419856d0ce1c1a1f9af73d1b593d5e3 Mon Sep 17 00:00:00 2001 From: SnowyLuma Date: Sun, 29 Sep 2019 03:59:09 +0200 Subject: [PATCH 001/300] Don't ignore other bots chatting in threads --- src/main.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main.js b/src/main.js index 16d7058..bd60e4d 100644 --- a/src/main.js +++ b/src/main.js @@ -85,16 +85,16 @@ function initBaseMessageHandlers() { */ bot.on('messageCreate', async msg => { if (! utils.messageIsOnInboxServer(msg)) return; - if (msg.author.bot) return; + if (msg.author.id === bot.user.id) return; const thread = await threads.findByChannelId(msg.channel.id); if (! thread) return; - if (msg.content.startsWith(config.prefix) || msg.content.startsWith(config.snippetPrefix)) { + if (! msg.author.bot && (msg.content.startsWith(config.prefix) || msg.content.startsWith(config.snippetPrefix))) { // Save commands as "command messages" if (msg.content.startsWith(config.snippetPrefix)) return; // Ignore snippets thread.saveCommandMessage(msg); - } else if (config.alwaysReply) { + } else if (! msg.author.bot && config.alwaysReply) { // AUTO-REPLY: If config.alwaysReply is enabled, send all chat messages in thread channels as replies if (! utils.isStaff(msg.member)) return; // Only staff are allowed to reply @@ -141,7 +141,7 @@ function initBaseMessageHandlers() { */ bot.on('messageUpdate', async (msg, oldMessage) => { if (! msg || ! msg.author) return; - if (msg.author.bot) return; + if (msg.author.id === bot.user.id) return; if (await blocked.isBlocked(msg.author.id)) return; // Old message content doesn't persist between bot restarts @@ -152,7 +152,7 @@ function initBaseMessageHandlers() { if (newContent.trim() === oldContent.trim()) return; // 1) Edit in DMs - if (msg.channel instanceof Eris.PrivateChannel) { + if (! msg.author.bot && msg.channel instanceof Eris.PrivateChannel) { const thread = await threads.findOpenThreadByUserId(msg.author.id); if (! thread) return; @@ -161,7 +161,7 @@ function initBaseMessageHandlers() { } // 2) Edit in the thread - else if (utils.messageIsOnInboxServer(msg) && utils.isStaff(msg.member)) { + else if (utils.messageIsOnInboxServer(msg) && (msg.author.bot || utils.isStaff(msg.member))) { const thread = await threads.findOpenThreadByChannelId(msg.channel.id); if (! thread) return; @@ -174,9 +174,9 @@ function initBaseMessageHandlers() { */ bot.on('messageDelete', async msg => { if (! msg.author) return; - if (msg.author.bot) return; + if (msg.author.id === bot.user.id) return; if (! utils.messageIsOnInboxServer(msg)) return; - if (! utils.isStaff(msg.member)) return; + if (! msg.author.bot && ! utils.isStaff(msg.member)) return; const thread = await threads.findOpenThreadByChannelId(msg.channel.id); if (! thread) return; From 949a5efdb25026bd75bcdda6a6d23ec62ed92f69 Mon Sep 17 00:00:00 2001 From: Dark <7890309+DarkView@users.noreply.github.com> Date: Mon, 17 Feb 2020 20:04:43 +0100 Subject: [PATCH 002/300] Allow multiple users to do !alert without overriding each other --- src/data/Thread.js | 61 ++++++++++++++++++++++++++++++++++++++++---- src/modules/alert.js | 6 ++--- 2 files changed, 59 insertions(+), 8 deletions(-) diff --git a/src/data/Thread.js b/src/data/Thread.js index 23e9e4a..fad9c36 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -169,8 +169,8 @@ class Thread { } if (this.alert_id) { - await this.setAlert(null); - await this.postSystemMessage(`<@!${this.alert_id}> New message from ${this.user_name}`); + await this.deleteAlerts(); + await this.postSystemMessage(`${this.alert_id} New message from ${this.user_name}`); } } @@ -449,14 +449,65 @@ class Thread { * @param {String} userId * @returns {Promise} */ - async setAlert(userId) { + async addAlert(userId) { + let alerts = await knex('threads') + .where('id', this.id) + .select('alert_id') + .first(); + alerts = alerts.alert_id; + + if (alerts == null) { + alerts = `<@!${userId}>`; + } else { + if (! alerts.includes(`<@!${userId}>`)) { + alerts += ` <@!${userId}>`; + } + } + await knex('threads') .where('id', this.id) .update({ - alert_id: userId + alert_id: alerts }); } + /** + * @param {String} userId + * @returns {Promise} + */ + async removeAlert(userId) { + let alerts = await knex('threads') + .where('id', this.id) + .select('alert_id') + .first(); + alerts = alerts.alert_id; + + if (! (alerts == null)) { + if (alerts.startsWith(`<@!${userId}>`)) { // we do this to properly handle the spacing between @s + alerts = alerts.replace(`<@!${userId}> `, ""); + } else { + alerts = alerts.replace(` <@!${userId}>`, ""); + } + } + + await knex('threads') + .where('id', this.id) + .update({ + alert_id: alerts + }); + } + + /** + * @returns {Promise} + */ + async deleteAlerts() { + await knex('threads') + .where('id', this.id) + .update({ + alert_id: null + }) + } + /** * @returns {Promise} */ @@ -465,4 +516,4 @@ class Thread { } } -module.exports = Thread; +module.exports = Thread; \ No newline at end of file diff --git a/src/modules/alert.js b/src/modules/alert.js index a844230..65d6576 100644 --- a/src/modules/alert.js +++ b/src/modules/alert.js @@ -1,10 +1,10 @@ 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`); + await thread.removeAlert(msg.author.id) + await thread.postSystemMessage(`Cancelled your new message alert`); } else { - await thread.setAlert(msg.author.id); + await thread.addAlert(msg.author.id); await thread.postSystemMessage(`Pinging ${msg.author.username}#${msg.author.discriminator} when this thread gets a new reply`); } }); From c41f7a09787374ec193a5fb24f0ca84d9066accf Mon Sep 17 00:00:00 2001 From: Dark <7890309+DarkView@users.noreply.github.com> Date: Tue, 18 Feb 2020 01:22:00 +0100 Subject: [PATCH 003/300] Change to comma separated list --- src/data/Thread.js | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/src/data/Thread.js b/src/data/Thread.js index fad9c36..6858d5c 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -169,8 +169,15 @@ class Thread { } if (this.alert_id) { + const ids = this.alert_id.split(","); + let mentions = ""; + + ids.forEach(id => { + mentions += `<@!${id}> `; + }); + await this.deleteAlerts(); - await this.postSystemMessage(`${this.alert_id} New message from ${this.user_name}`); + await this.postSystemMessage(`${mentions}New message from ${this.user_name}`); } } @@ -457,13 +464,15 @@ class Thread { alerts = alerts.alert_id; if (alerts == null) { - alerts = `<@!${userId}>`; + alerts = [userId] } else { - if (! alerts.includes(`<@!${userId}>`)) { - alerts += ` <@!${userId}>`; + alerts = alerts.split(","); + if (!alerts.includes(userId)) { + alerts.push(userId); } } + alerts = alerts.join(","); await knex('threads') .where('id', this.id) .update({ @@ -482,12 +491,20 @@ class Thread { .first(); alerts = alerts.alert_id; - if (! (alerts == null)) { - if (alerts.startsWith(`<@!${userId}>`)) { // we do this to properly handle the spacing between @s - alerts = alerts.replace(`<@!${userId}> `, ""); - } else { - alerts = alerts.replace(` <@!${userId}>`, ""); + if (alerts != null) { + alerts = alerts.split(","); + + for (let i = 0; i < alerts.length; i++) { + if (alerts[i] == userId) { + alerts.splice(i, 1); + } } + } else return; + + if (alerts.length == 0) { + alerts = null; + } else { + alerts = alerts.join(","); } await knex('threads') From ae3bf11d0b79457bf8bbc344e495a61d787ce82f Mon Sep 17 00:00:00 2001 From: Eegras Date: Thu, 30 Apr 2020 18:33:57 -0500 Subject: [PATCH 004/300] Update pm2 config file to run on Windows 10 https://github.com/Unitech/pm2/issues/3657#issuecomment-482010714 This is a known issue with pm2 on Windows. pm2 tries to run node.cmd as javascript which is wrong. --- modmailbot-pm2.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modmailbot-pm2.json b/modmailbot-pm2.json index 6322645..4635ec8 100644 --- a/modmailbot-pm2.json +++ b/modmailbot-pm2.json @@ -3,6 +3,6 @@ "name": "ModMailBot", "cwd": "./", "script": "npm", - "args": "start" + "args": "./src/index.js" }] } From edd2ceb1abd92f024d3fd59abf4258d00f62ffd5 Mon Sep 17 00:00:00 2001 From: DopeGhoti Date: Thu, 4 Jun 2020 20:27:01 -0700 Subject: [PATCH 005/300] Add optional automatic thread creation on mention Add an option to have the bot automatically open a new thread when a user @s the bot in a monitored channel. Modify configuration parser to handle the new settings; add a stanza to the configuration documentiaion for it. --- docs/configuration.md | 4 ++++ src/config.js | 1 + src/main.js | 11 +++++++++++ 3 files changed, 16 insertions(+) diff --git a/docs/configuration.md b/docs/configuration.md index cb4f2bb..a6a3fe3 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -254,6 +254,10 @@ The bot's "Playing" text **Default:** `on` If enabled, channel permissions for the thread are synchronized with the category when using `!move`. Requires `allowMove` to be enabled. +#### threadOnMention +**Default:** `off` +If enabled, the bot will automatically create a new thread for a user who pings it. + #### threadTimestamps **Default:** `off` If enabled, modmail threads will show accurate UTC timestamps for each message, in addition to Discord's own timestamps. diff --git a/src/config.js b/src/config.js index 5d649f8..7d23c86 100644 --- a/src/config.js +++ b/src/config.js @@ -76,6 +76,7 @@ const defaultConfig = { "mentionRole": "here", "pingOnBotMention": true, "botMentionResponse": null, + "threadOnMention": false, "inboxServerPermission": null, "alwaysReply": false, diff --git a/src/main.js b/src/main.js index e8f7258..c2bd4a5 100644 --- a/src/main.js +++ b/src/main.js @@ -229,6 +229,17 @@ function initBaseMessageHandlers() { const botMentionResponse = utils.readMultilineConfigValue(config.botMentionResponse); bot.createMessage(msg.channel.id, botMentionResponse.replace(/{userMention}/g, `<@${msg.author.id}>`)); } + + // If configured, automatically open a new thread with a user who has pinged it + if (config.threadOnMention) { + if (await blocked.isBlocked(msg.author.id)) return; // This may not be needed as it is checked above. + if (utils.isStaff(msg.member)) return; // Same. + const existingThread = await threads.findOpenThreadByUserId(msg.author.id); + if (existingThread) { // Already a thread open; nothing to do + return; + } + const createdThread = await threads.createNewThreadForUser(msg.author, true, true); + } }); } From cd17fdbaed6a7e415d494a53e8ecb6dee14a7910 Mon Sep 17 00:00:00 2001 From: DopeGhoti Date: Thu, 4 Jun 2020 20:43:01 -0700 Subject: [PATCH 006/300] Slight refactor of existing thread check --- src/main.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main.js b/src/main.js index c2bd4a5..8eefd26 100644 --- a/src/main.js +++ b/src/main.js @@ -235,10 +235,9 @@ function initBaseMessageHandlers() { if (await blocked.isBlocked(msg.author.id)) return; // This may not be needed as it is checked above. if (utils.isStaff(msg.member)) return; // Same. const existingThread = await threads.findOpenThreadByUserId(msg.author.id); - if (existingThread) { // Already a thread open; nothing to do - return; + if (! existingThread) { // Only open a thread if we don't already have one. + const createdThread = await threads.createNewThreadForUser(msg.author, true, true); } - const createdThread = await threads.createNewThreadForUser(msg.author, true, true); } }); } From b566be85fef3b98aa0559dfd6dce6eaaa9d5fd7e Mon Sep 17 00:00:00 2001 From: "eegras@eegrasstudios.com" <1c2X$n%WFVkiL*ap#f*7biqj1HBNb1> Date: Fri, 5 Jun 2020 13:11:56 -0500 Subject: [PATCH 007/300] Implement reactOnSeen --- docs/configuration.md | 9 +++++++++ src/config.js | 3 +++ src/data/Thread.js | 5 +++++ 3 files changed, 17 insertions(+) diff --git a/docs/configuration.md b/docs/configuration.md index cb4f2bb..6918354 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -213,6 +213,15 @@ Make sure to do the necessary [port forwarding](https://portforward.com/) and ad **Default:** `!` Prefix for bot commands +#### reactOnSeen +**Default:** `off` +If enabled, the bot will react to messages sent to it with the emoji defined in `reactOnSeenEmoji` + +#### reactOnSeenEmoji +**Default:** `📨` +The emoji that the bot will react with when it sees a message. Requires `reactOnSeen` to be enabled. +Must be pasted in the config file as the Emoji representation and not as a unicode codepoint. + #### relaySmallAttachmentsAsAttachments **Default:** `off` If enabled, small attachments from users are sent as real attachments rather than links in modmail threads. diff --git a/src/config.js b/src/config.js index a2e3beb..769366e 100644 --- a/src/config.js +++ b/src/config.js @@ -121,6 +121,9 @@ const defaultConfig = { "knex": null, "logDir": path.join(__dirname, '..', 'logs'), + + "reactOnSeen": false, + "reactOnSeenEmoji": "\uD83D\uDCE8", }; // Load config values from environment variables diff --git a/src/data/Thread.js b/src/data/Thread.js index 23e9e4a..113eacb 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -163,6 +163,11 @@ class Thread { dm_message_id: msg.id }); + if (config.reactOnSeen) + { + await msg.addReaction(config.reactOnSeenEmoji) + } + if (this.scheduled_close_at) { await this.cancelScheduledClose(); await this.postSystemMessage(`<@!${this.scheduled_close_id}> Thread that was scheduled to be closed got a new reply. Cancelling.`); From 9994e075c1e02cc88100cad27d9e7fa60c2152b8 Mon Sep 17 00:00:00 2001 From: "eegras@eegrasstudios.com" <1c2X$n%WFVkiL*ap#f*7biqj1HBNb1> Date: Fri, 5 Jun 2020 13:14:14 -0500 Subject: [PATCH 008/300] Revert changes --- modmailbot-pm2.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modmailbot-pm2.json b/modmailbot-pm2.json index 4635ec8..6322645 100644 --- a/modmailbot-pm2.json +++ b/modmailbot-pm2.json @@ -3,6 +3,6 @@ "name": "ModMailBot", "cwd": "./", "script": "npm", - "args": "./src/index.js" + "args": "start" }] } From 3fc3905628bfeda7affd8b51366f73ffc4b1b9ab Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 9 Jul 2020 04:05:28 +0300 Subject: [PATCH 009/300] Update node-sqlite3 to v5. Update Node.js version requirements accordingly. --- .nvmrc | 2 +- docs/setup.md | 3 +- package-lock.json | 484 +++++++++++++++++++++++++++++++++++++++++----- package.json | 2 +- src/index.js | 4 +- 5 files changed, 445 insertions(+), 50 deletions(-) diff --git a/.nvmrc b/.nvmrc index f599e28..8351c19 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -10 +14 diff --git a/docs/setup.md b/docs/setup.md index d76728f..4b68dbd 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -14,8 +14,7 @@ To keep it online, you need to keep the bot process running. ## Prerequisites 1. Create a bot account through the [Discord Developer Portal](https://discordapp.com/developers/) 2. Invite the created bot to your server -3. Install Node.js 10, 11, or 12 - - Node.js 13 is currently not supported +3. Install Node.js 11, 12, 13, or 14 4. Download the latest bot version from [the releases page](https://github.com/Dragory/modmailbot/releases) and extract it to a folder 5. In the bot's folder, make a copy of the file `config.example.ini` and rename the copy to `config.ini` diff --git a/package-lock.json b/package-lock.json index 711d7f5..2ec9b68 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "modmailbot", - "version": "2.30.1", + "version": "2.31.0-beta.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -58,7 +58,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", @@ -160,6 +159,21 @@ "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "optional": true, + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "optional": true + }, "assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", @@ -176,11 +190,29 @@ "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "optional": true + }, "atob": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "optional": true + }, + "aws4": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.0.tgz", + "integrity": "sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA==", + "optional": true + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -236,12 +268,38 @@ } } }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "optional": true, + "requires": { + "tweetnacl": "^0.14.3" + }, + "dependencies": { + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "optional": true + } + } + }, "binary-extensions": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==", "dev": true }, + "block-stream": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", + "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", + "optional": true, + "requires": { + "inherits": "~2.0.0" + } + }, "bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", @@ -368,6 +426,12 @@ "integrity": "sha512-mYQLZnx5Qt1JgB1WEiMCf2647plpGeQ2NMR/5L0HNZzGQo4fuSPnK+wjfPnKZV0aiJDgzmWqqkV/g7JD+DW0qw==", "dev": true }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "optional": true + }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -574,6 +638,15 @@ "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.1.0.tgz", "integrity": "sha512-6S062WDQUXi6hOfkO/sBPVwE5ASXY4G2+b4atvhJfSsuUUhIaUKlkjLe9692Ipyt5/a+IPF5aVTu3V5gvXq5cg==" }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "optional": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, "commander": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.0.1.tgz", @@ -644,6 +717,15 @@ "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=", "dev": true }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "optional": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, "debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", @@ -723,6 +805,12 @@ } } }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "optional": true + }, "delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -777,6 +865,16 @@ "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=" }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "optional": true, + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -1147,17 +1245,21 @@ } } }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "optional": true + }, "fast-deep-equal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", - "dev": true + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" }, "fast-json-stable-stringify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", - "dev": true + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" }, "fast-levenshtein": { "version": "2.0.6", @@ -1270,6 +1372,23 @@ "for-in": "^1.0.1" } }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "optional": true + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "optional": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, "fragment-cache": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", @@ -1298,6 +1417,18 @@ "dev": true, "optional": true }, + "fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "optional": true, + "requires": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + } + }, "functional-red-black-tree": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", @@ -1352,6 +1483,29 @@ "resolved": "https://registry.npmjs.org/getopts/-/getopts-2.2.5.tgz", "integrity": "sha512-9jb7AW5p3in+IiJWhQiZmmwkpLaR/ccTWdWQCtZM66HJcHHLegowh4q4tSD7gouUyeNvFWRavfK9GXosQHDpFA==" }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "optional": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "optional": true, + "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" + } + }, "glob-parent": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz", @@ -1432,8 +1586,23 @@ "graceful-fs": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", - "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", - "dev": true + "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==" + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "optional": true + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "optional": true, + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } }, "has-flag": { "version": "3.0.0", @@ -1488,6 +1657,17 @@ "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.0.3.tgz", "integrity": "sha512-TcIMG3qeVLgDr1TEd2XvHaTnMPwYQUQMIBLy+5pLSDKYFc7UIqj39w8EGzZkaxoLv/l2K8HaI0t5AVA+YYgUew==" }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "optional": true, + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, "humanize-duration": { "version": "3.12.1", "resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.12.1.tgz", @@ -1859,6 +2039,12 @@ "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", "dev": true }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "optional": true + }, "is-unc-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", @@ -1887,6 +2073,12 @@ "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "optional": true + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -1903,16 +2095,27 @@ "esprima": "^4.0.0" } }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "optional": true + }, "json-buffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=" }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "optional": true + }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -1920,6 +2123,12 @@ "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", "dev": true }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "optional": true + }, "json5": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.1.tgz", @@ -1935,6 +2144,18 @@ } } }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "optional": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, "keyv": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", @@ -2121,6 +2342,21 @@ "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==" }, + "mime-db": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==", + "optional": true + }, + "mime-types": { + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "optional": true, + "requires": { + "mime-db": "1.44.0" + } + }, "mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", @@ -2238,11 +2474,6 @@ } } }, - "nan": { - "version": "2.14.1", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz", - "integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==" - }, "nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", @@ -2298,6 +2529,39 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "node-addon-api": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.0.tgz", + "integrity": "sha512-ASCL5U13as7HhOExbT6OlWJJUV/lLzL2voOSP1UVehpRD8FbSrSDjfScK/KwAvVTI5AS6r4VwbOMlIqtvRidnA==" + }, + "node-gyp": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", + "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==", + "optional": true, + "requires": { + "fstream": "^1.0.0", + "glob": "^7.0.3", + "graceful-fs": "^4.1.2", + "mkdirp": "^0.5.0", + "nopt": "2 || 3", + "npmlog": "0 || 1 || 2 || 3 || 4", + "osenv": "0", + "request": "^2.87.0", + "rimraf": "2", + "semver": "~5.3.0", + "tar": "^2.0.0", + "which": "1" + }, + "dependencies": { + "semver": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", + "optional": true + } + } + }, "node-pre-gyp": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz", @@ -2313,6 +2577,36 @@ "rimraf": "^2.6.1", "semver": "^5.3.0", "tar": "^4" + }, + "dependencies": { + "nopt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", + "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "tar": { + "version": "4.4.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", + "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.8.6", + "minizlib": "^1.2.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.3" + } + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + } } }, "nodemon": { @@ -2351,12 +2645,12 @@ } }, "nopt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", - "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "optional": true, "requires": { - "abbrev": "1", - "osenv": "^0.1.4" + "abbrev": "1" } }, "normalize-path": { @@ -2418,6 +2712,12 @@ "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "optional": true + }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -2700,6 +3000,12 @@ "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=" }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "optional": true + }, "pg-connection-string": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.1.0.tgz", @@ -2750,6 +3056,12 @@ "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", "dev": true }, + "psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", + "optional": true + }, "pstree.remy": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.7.tgz", @@ -2775,6 +3087,18 @@ "once": "^1.3.1" } }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "optional": true + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "optional": true + }, "rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -2868,6 +3192,34 @@ "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" }, + "request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "optional": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + } + }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -3219,14 +3571,40 @@ "dev": true }, "sqlite3": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.2.0.tgz", - "integrity": "sha512-roEOz41hxui2Q7uYnWsjMOTry6TcNUNmp8audCx18gF10P2NknwdpF+E+HKvz/F2NvPKGGBF4NGc+ZPQ+AABwg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.0.0.tgz", + "integrity": "sha512-rjvqHFUaSGnzxDy2AHCwhHy6Zp6MNJzCPGYju4kD8yi6bze4d1/zMTg6C7JI49b7/EM7jKMTvyfN/4ylBKdwfw==", "requires": { - "nan": "^2.12.1", + "node-addon-api": "2.0.0", + "node-gyp": "3.x", "node-pre-gyp": "^0.11.0" } }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "optional": true, + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "dependencies": { + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "optional": true + } + } + }, "static-extend": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", @@ -3376,24 +3754,14 @@ } }, "tar": { - "version": "4.4.13", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", - "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz", + "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==", + "optional": true, "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.8.6", - "minizlib": "^1.2.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.3" - }, - "dependencies": { - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" - } + "block-stream": "*", + "fstream": "^1.0.12", + "inherits": "2" } }, "tarn": { @@ -3504,6 +3872,16 @@ } } }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "optional": true, + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, "transliteration": { "version": "2.1.7", "resolved": "https://registry.npmjs.org/transliteration/-/transliteration-2.1.7.tgz", @@ -3518,6 +3896,15 @@ "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", "dev": true }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "optional": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, "tweetnacl": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.1.tgz", @@ -3654,7 +4041,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "dev": true, "requires": { "punycode": "^2.1.0" }, @@ -3662,8 +4048,7 @@ "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" } } }, @@ -3709,6 +4094,17 @@ "homedir-polyfill": "^1.0.1" } }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "optional": true, + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, "which": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", diff --git a/package.json b/package.json index d888756..c183c23 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "moment": "^2.24.0", "mv": "^2.1.1", "public-ip": "^4.0.0", - "sqlite3": "^4.2.0", + "sqlite3": "^5.0.0", "tmp": "^0.1.0", "transliteration": "^2.1.7", "uuid": "^3.3.3" diff --git a/src/index.js b/src/index.js index 3e1615e..1e23ae3 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,7 @@ // Verify NodeJS version const nodeMajorVersion = parseInt(process.versions.node.split('.')[0], 10); -if (nodeMajorVersion < 10) { - console.error('Unsupported NodeJS version! Please install NodeJS 10 or newer.'); +if (nodeMajorVersion < 11) { + console.error('Unsupported NodeJS version! Please install Node.js 11, 12, 13, or 14.'); process.exit(1); } From 3c0352ff099ab2a2918eb2f953f392acc0c6dac8 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Tue, 14 Jul 2020 00:13:32 +0300 Subject: [PATCH 010/300] Add support for hooks. Add beforeNewThread hook. Allow overriding new thread category id in createNewThreadForUser(). --- src/data/threads.js | 15 ++++++++++----- src/hooks.js | 35 +++++++++++++++++++++++++++++++++++ src/main.js | 25 +++++++++++++++++++++++-- src/modules/newthread.js | 2 +- 4 files changed, 69 insertions(+), 8 deletions(-) create mode 100644 src/hooks.js diff --git a/src/data/threads.js b/src/data/threads.js index 75685b0..aac6bf2 100644 --- a/src/data/threads.js +++ b/src/data/threads.js @@ -52,12 +52,17 @@ function getHeaderGuildInfo(member) { /** * Creates a new modmail thread for the specified user * @param {User} user - * @param {Member} member - * @param {Boolean} quiet If true, doesn't ping mentionRole or reply with responseMessage + * @param {Object} opts + * @param {Boolean} opts.quiet If true, doesn't ping mentionRole or reply with responseMessage + * @param {Boolean} opts.ignoreRequirements If true, creates a new thread even if the account doesn't meet requiredAccountAge + * @param {String} opts.categoryId Override the category ID for the new thread * @returns {Promise} * @throws {Error} */ -async function createNewThreadForUser(user, quiet = false, ignoreRequirements = false) { +async function createNewThreadForUser(user, opts = {}) { + const quiet = opts.quiet != null ? opts.quiet : false; + const ignoreRequirements = opts.ignoreRequirements != null ? opts.ignoreRequirements : false; + const existingThread = await findOpenThreadByUserId(user.id); if (existingThread) { throw new Error('Attempted to create a new thread for a user with an existing open thread!'); @@ -126,9 +131,9 @@ async function createNewThreadForUser(user, quiet = false, ignoreRequirements = console.log(`[NOTE] Creating new thread channel ${channelName}`); // Figure out which category we should place the thread channel in - let newThreadCategoryId; + let newThreadCategoryId = opts.categoryId || null; - if (config.categoryAutomation.newThreadFromGuild) { + if (! newThreadCategoryId && config.categoryAutomation.newThreadFromGuild) { // Categories for specific source guilds (in case of multiple main guilds) for (const [guildId, categoryId] of Object.entries(config.categoryAutomation.newThreadFromGuild)) { if (userGuildData.has(guildId)) { diff --git a/src/hooks.js b/src/hooks.js new file mode 100644 index 0000000..1896177 --- /dev/null +++ b/src/hooks.js @@ -0,0 +1,35 @@ +/** + * @callback BeforeNewThreadHook_SetCategoryId + * @param {String} categoryId + * @return void + */ + +/** + * @typedef BeforeNewThreadHookEvent + * @property {Function} cancel + * @property {BeforeNewThreadHook_SetCategoryId} setCategoryId + * + */ + +/** + * @callback BeforeNewThreadHook + * @param {BeforeNewThreadHookEvent} ev + * @return {void|Promise} + */ + +/** + * @type BeforeNewThreadHook[] + */ +const beforeNewThreadHooks = []; + +/** + * @param {BeforeNewThreadHook} fn + */ +function beforeNewThread(fn) { + beforeNewThreadHooks.push(fn); +} + +module.exports = { + beforeNewThreadHooks, + beforeNewThread, +}; diff --git a/src/main.js b/src/main.js index e8f7258..c1f9c2c 100644 --- a/src/main.js +++ b/src/main.js @@ -8,6 +8,7 @@ const {messageQueue} = require('./queue'); const utils = require('./utils'); const { createCommandManager } = require('./commands'); const { getPluginAPI, loadPlugin } = require('./plugins'); +const { beforeNewThreadHooks } = require('./hooks'); const blocked = require('./data/blocked'); const threads = require('./data/threads'); @@ -123,13 +124,33 @@ function initBaseMessageHandlers() { messageQueue.add(async () => { let thread = await threads.findOpenThreadByUserId(msg.author.id); - // New thread if (! thread) { // Ignore messages that shouldn't usually open new threads, such as "ok", "thanks", etc. if (config.ignoreAccidentalThreads && msg.content && ACCIDENTAL_THREAD_MESSAGES.includes(msg.content.trim().toLowerCase())) return; - thread = await threads.createNewThreadForUser(msg.author); + let cancelled = false; + let categoryId = null; + + /** + * @type {BeforeNewThreadHookEvent} + */ + const ev = { + cancel() { + cancelled = true; + }, + + setCategoryId(_categoryId) { + categoryId = _categoryId; + }, + }; + + for (const hook of beforeNewThreadHooks) { + await hook(ev, msg); + if (cancelled) return; + } + + thread = await threads.createNewThreadForUser(msg.author, { categoryId }); } if (thread) { diff --git a/src/modules/newthread.js b/src/modules/newthread.js index aca6f54..35f70d8 100644 --- a/src/modules/newthread.js +++ b/src/modules/newthread.js @@ -15,7 +15,7 @@ module.exports = ({ bot, knex, config, commands }) => { return; } - const createdThread = await threads.createNewThreadForUser(user, true, true); + const createdThread = await threads.createNewThreadForUser(user, { quiet: true, ignoreRequirements: true }); createdThread.postSystemMessage(`Thread was opened by ${msg.author.username}#${msg.author.discriminator}`); msg.channel.createMessage(`Thread opened: <#${createdThread.channel_id}>`); From 815825de94a42470d6fc4c755375f18a5fdf7a28 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Tue, 14 Jul 2020 00:14:31 +0300 Subject: [PATCH 011/300] Add hooks to plugin API --- src/plugins.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/plugins.js b/src/plugins.js index 64c9df6..87bb687 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -1,4 +1,5 @@ const attachments = require('./data/attachments'); +const { beforeNewThread } = require('./hooks'); module.exports = { getPluginAPI({ bot, knex, config, commands }) { @@ -17,6 +18,9 @@ module.exports = { addStorageType: attachments.addStorageType, downloadAttachment: attachments.downloadAttachment }, + hooks: { + beforeNewThread, + }, }; }, From 0c25afaec22a470cc374a5fbb1362a5bfe90c37d Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Tue, 14 Jul 2020 00:17:31 +0300 Subject: [PATCH 012/300] Add support for async plugin load functions --- src/main.js | 20 ++++++++++++++------ src/plugins.js | 4 ++-- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/main.js b/src/main.js index c1f9c2c..494805e 100644 --- a/src/main.js +++ b/src/main.js @@ -45,7 +45,10 @@ module.exports = { console.log('Initializing...'); initStatus(); initBaseMessageHandlers(); - initPlugins(); + + console.log('Loading plugins...'); + const pluginResult = await initPlugins(); + console.log(`Loaded ${pluginResult.loadedCount} plugins (${pluginResult.builtInCount} built-in plugins, ${pluginResult.externalCount} external plugins)`); console.log(''); console.log('Done! Now listening to DMs.'); @@ -253,7 +256,7 @@ function initBaseMessageHandlers() { }); } -function initPlugins() { +async function initPlugins() { // Initialize command manager const commands = createCommandManager(bot); @@ -265,7 +268,6 @@ function initPlugins() { } // Load plugins - console.log('Loading plugins'); const builtInPlugins = [ reply, close, @@ -293,11 +295,17 @@ function initPlugins() { } const pluginApi = getPluginAPI({ bot, knex, config, commands }); - plugins.forEach(pluginFn => loadPlugin(pluginFn, pluginApi)); - - console.log(`Loaded ${plugins.length} plugins (${builtInPlugins.length} built-in plugins, ${plugins.length - builtInPlugins.length} external plugins)`); + for (const plugin of plugins) { + await loadPlugin(plugin, pluginApi); + } if (config.updateNotifications) { updates.startVersionRefreshLoop(); } + + return { + loadedCount: plugins.length, + builtInCount: builtInPlugins.length, + externalCount: plugins.length - builtInPlugins.length, + }; } diff --git a/src/plugins.js b/src/plugins.js index 87bb687..311cecd 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -24,7 +24,7 @@ module.exports = { }; }, - loadPlugin(plugin, api) { - plugin(api); + async loadPlugin(plugin, api) { + await plugin(api); } }; From 8a975d7da477c6bd43276592522528914f9aa71e Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Tue, 14 Jul 2020 01:11:48 +0300 Subject: [PATCH 013/300] Add message formatters. Expose message formatters to plugins. --- src/data/Thread.js | 138 ++---------------------------- src/formatters.js | 208 +++++++++++++++++++++++++++++++++++++++++++++ src/plugins.js | 2 + 3 files changed, 216 insertions(+), 132 deletions(-) create mode 100644 src/formatters.js diff --git a/src/data/Thread.js b/src/data/Thread.js index 3d686fa..3e5faf9 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -5,6 +5,7 @@ const knex = require('../knex'); const utils = require('../utils'); const config = require('../config'); const attachments = require('./attachments'); +const { formatters } = require('../formatters'); const ThreadMessage = require('./ThreadMessage'); @@ -28,133 +29,6 @@ class Thread { utils.setDataModelProps(this, props); } - /** - * @param {Eris.Member} moderator - * @param {string} text - * @param {boolean} isAnonymous - * @returns {string} - * @private - */ - _formatStaffReplyDM(moderator, text, isAnonymous) { - const mainRole = utils.getMainRole(moderator); - const modName = (config.useNicknames ? moderator.nick || moderator.user.username : moderator.user.username); - const modInfo = isAnonymous - ? (mainRole ? mainRole.name : 'Moderator') - : (mainRole ? `(${mainRole.name}) ${modName}` : modName); - - return `**${modInfo}:** ${text}`; - } - - /** - * @param {Eris.Member} moderator - * @param {string} text - * @param {boolean} isAnonymous - * @param {number} messageNumber - * @param {number} timestamp - * @returns {string} - * @private - */ - _formatStaffReplyThreadMessage(moderator, text, isAnonymous, messageNumber, timestamp) { - const mainRole = utils.getMainRole(moderator); - const modName = (config.useNicknames ? moderator.nick || moderator.user.username : moderator.user.username); - const modInfo = isAnonymous - ? `(Anonymous) (${modName}) ${mainRole ? mainRole.name : 'Moderator'}` - : (mainRole ? `(${mainRole.name}) ${modName}` : modName); - - // TODO: Add \`[${messageNumber}]\` here once !edit and !delete exist - let result = `**${modInfo}:** ${text}`; - - if (config.threadTimestamps) { - const formattedTimestamp = timestamp ? utils.getTimestamp(timestamp, 'x') : utils.getTimestamp(); - result = `[${formattedTimestamp}] ${result}`; - } - - return result; - } - - /** - * @param {Eris.Member} moderator - * @param {string} text - * @param {boolean} isAnonymous - * @param {string[]} attachmentLinks - * @returns {string} - * @private - */ - _formatStaffReplyLogMessage(moderator, text, isAnonymous, attachmentLinks = []) { - const mainRole = utils.getMainRole(moderator); - const modName = moderator.user.username; - - // Mirroring the DM formatting here... - const modInfo = isAnonymous - ? (mainRole ? mainRole.name : 'Moderator') - : (mainRole ? `(${mainRole.name}) ${modName}` : modName); - - let result = `**${modInfo}:** ${text}`; - - if (attachmentLinks.length) { - result += '\n'; - for (const link of attachmentLinks) { - result += `\n**Attachment:** ${link}`; - } - } - - return result; - } - - /** - * @param {Eris.User} user - * @param {string} body - * @param {Eris.EmbedBase[]} embeds - * @param {string[]} formattedAttachments - * @param {number} timestamp - * @return string - * @private - */ - _formatUserReplyThreadMessage(user, body, embeds, formattedAttachments = [], timestamp) { - const content = (body.trim() === '' && embeds.length) - ? '' - : body; - - let result = `**${user.username}#${user.discriminator}:** ${content}`; - - if (formattedAttachments.length) { - for (const formatted of formattedAttachments) { - result += `\n\n${formatted}`; - } - } - - if (config.threadTimestamps) { - const formattedTimestamp = timestamp ? utils.getTimestamp(timestamp, 'x') : utils.getTimestamp(); - result = `[${formattedTimestamp}] ${result}`; - } - - return result; - } - - /** - * @param {Eris.User} user - * @param {string} body - * @param {Eris.EmbedBase[]} embeds - * @param {string[]} formattedAttachments - * @return string - * @private - */ - _formatUserReplyLogMessage(user, body, embeds, formattedAttachments = []) { - const content = (body.trim() === '' && embeds.length) - ? '' - : body; - - let result = content; - - if (formattedAttachments.length) { - for (const formatted of formattedAttachments) { - result += `\n\n${formatted}`; - } - } - - return result; - } - /** * @param {string} text * @param {Eris.MessageFile|Eris.MessageFile[]} file @@ -285,7 +159,7 @@ class Thread { } // Send the reply DM - const dmContent = this._formatStaffReplyDM(moderator, text, isAnonymous); + const dmContent = formatters.formatStaffReplyDM(moderator, text, { isAnonymous }); let dmMessage; try { dmMessage = await this._sendDMToUser(dmContent, files); @@ -295,7 +169,7 @@ class Thread { } // Save the log entry - const logContent = this._formatStaffReplyLogMessage(moderator, text, isAnonymous, attachmentLinks); + const logContent = formatters.formatStaffReplyLogMessage(moderator, text, { isAnonymous, attachmentLinks }); const threadMessage = await this._addThreadMessageToDB({ message_type: THREAD_MESSAGE_TYPE.TO_USER, user_id: moderator.id, @@ -306,7 +180,7 @@ class Thread { }); // Show the reply in the inbox thread - const inboxContent = this._formatStaffReplyThreadMessage(moderator, text, isAnonymous, threadMessage.message_number, null); + const inboxContent = formatters.formatStaffReplyThreadMessage(moderator, text, threadMessage.message_number, { isAnonymous }); const inboxMessage = await this._postToThreadChannel(inboxContent, files); await this._updateThreadMessage(threadMessage.id, { inbox_message_id: inboxMessage.id }); @@ -347,7 +221,7 @@ class Thread { } // Save log entry - const logContent = this._formatUserReplyLogMessage(msg.author, msg.content, msg.embeds, logFormattedAttachments); + const logContent = formatters.formatUserReplyLogMessage(msg.author, msg, { attachmentLinks: logFormattedAttachments }); const threadMessage = await this._addThreadMessageToDB({ message_type: THREAD_MESSAGE_TYPE.FROM_USER, user_id: this.user_id, @@ -358,7 +232,7 @@ class Thread { }); // Show user reply in the inbox thread - const inboxContent = this._formatUserReplyThreadMessage(msg.author, msg.content, msg.embeds, threadFormattedAttachments, null); + const inboxContent = formatters.formatUserReplyThreadMessage(msg.author, msg, { attachmentLinks: threadFormattedAttachments }); const inboxMessage = await this._postToThreadChannel(inboxContent, attachmentFiles); if (inboxMessage) await this._updateThreadMessage(threadMessage.id, { inbox_message_id: inboxMessage.id }); diff --git a/src/formatters.js b/src/formatters.js new file mode 100644 index 0000000..15b5660 --- /dev/null +++ b/src/formatters.js @@ -0,0 +1,208 @@ +const Eris = require('eris'); +const utils = require('./utils'); +const config = require('./config'); + +/** + * Function to format the DM that is sent to the user when a staff member replies to them via !reply + * @callback FormatStaffReplyDM + * @param {Eris.Member} moderator Staff member that is replying + * @param {string} text Reply text + * @param {{ + * isAnonymous: boolean, + * }} opts={} + * @return {string} Message content to send as a DM + */ + +/** + * Function to format a staff reply in a thread channel + * @callback FormatStaffReplyThreadMessage + * @param {Eris.Member} moderator + * @param {string} text + * @param {number} messageNumber + * @param {{ + * isAnonymous: boolean, + * }} opts={} + * @return {string} Message content to post in the thread channel + */ + +/** + * Function to format a staff reply in a log + * @callback FormatStaffReplyLogMessage + * @param {Eris.Member} moderator + * @param {string} text + * @param {{ + * isAnonymous: boolean, + * attachmentLinks: string[], + * }} opts={} + * @returns {string} Text to show in the log + */ + +/** + * Function to format a user reply in a thread channel + * @callback FormatUserReplyThreadMessage + * @param {Eris.User} user Use that sent the reply + * @param {Eris.Message} msg The message object that the user sent + * @param {{ + * attachmentLinks: string[], + * }} opts + * @return {string} Message content to post in the thread channel + */ + +/** + * @callback FormatUserReplyLogMessage + * @param {Eris.User} user + * @param {Eris.Message} msg + * @param {{ + * attachmentLinks: string[], + * }} opts={} + * @return {string} Text to show in the log + */ + +/** + * @typedef MessageFormatters + * @property {FormatStaffReplyDM} formatStaffReplyDM + * @property {FormatStaffReplyThreadMessage} formatStaffReplyThreadMessage + * @property {FormatStaffReplyLogMessage} formatStaffReplyLogMessage + * @property {FormatUserReplyThreadMessage} formatUserReplyThreadMessage + * @property {FormatUserReplyLogMessage} formatUserReplyLogMessage + */ + +/** + * @type {MessageFormatters} + */ +const defaultFormatters = { + formatStaffReplyDM(moderator, text, opts = {}) { + const mainRole = utils.getMainRole(moderator); + const modName = (config.useNicknames ? moderator.nick || moderator.user.username : moderator.user.username); + const modInfo = opts.isAnonymous + ? (mainRole ? mainRole.name : 'Moderator') + : (mainRole ? `(${mainRole.name}) ${modName}` : modName); + + return `**${modInfo}:** ${text}`; + }, + + formatStaffReplyThreadMessage(moderator, text, messageNumber, opts = {}) { + const mainRole = utils.getMainRole(moderator); + const modName = (config.useNicknames ? moderator.nick || moderator.user.username : moderator.user.username); + const modInfo = opts.isAnonymous + ? `(Anonymous) (${modName}) ${mainRole ? mainRole.name : 'Moderator'}` + : (mainRole ? `(${mainRole.name}) ${modName}` : modName); + + // TODO: Add \`[${messageNumber}]\` here once !edit and !delete exist + let result = `**${modInfo}:** ${text}`; + + if (config.threadTimestamps) { + const formattedTimestamp = utils.getTimestamp(); + result = `[${formattedTimestamp}] ${result}`; + } + + return result; + }, + + formatStaffReplyLogMessage(moderator, text, opts = {}) { + const mainRole = utils.getMainRole(moderator); + const modName = moderator.user.username; + + // Mirroring the DM formatting here... + const modInfo = opts.isAnonymous + ? (mainRole ? mainRole.name : 'Moderator') + : (mainRole ? `(${mainRole.name}) ${modName}` : modName); + + let result = `**${modInfo}:** ${text}`; + + if (opts.attachmentLinks && opts.attachmentLinks.length) { + result += '\n'; + for (const link of opts.attachmentLinks) { + result += `\n**Attachment:** ${link}`; + } + } + + return result; + }, + + formatUserReplyThreadMessage(user, msg, opts = {}) { + const content = (msg.content.trim() === '' && msg.embeds.length) + ? '' + : msg.content; + + let result = `**${user.username}#${user.discriminator}:** ${content}`; + + if (opts.attachmentLinks && opts.attachmentLinks.length) { + for (const link of opts.attachmentLinks) { + result += `\n\n${link}`; + } + } + + if (config.threadTimestamps) { + const formattedTimestamp = utils.getTimestamp(msg.timestamp, 'x'); + result = `[${formattedTimestamp}] ${result}`; + } + + return result; + }, + + formatUserReplyLogMessage(user, msg, opts = {}) { + const content = (msg.content.trim() === '' && msg.embeds.length) + ? '' + : msg.content; + + let result = content; + + if (opts.attachmentLinks && opts.attachmentLinks.length) { + for (const link of opts.attachmentLinks) { + result += `\n\n${link}`; + } + } + + return result; + }, +}; + +/** + * @type {MessageFormatters} + */ +const formatters = { ...defaultFormatters }; + +module.exports = { + formatters, + + /** + * @param {FormatStaffReplyDM} fn + * @return {void} + */ + setStaffReplyDMFormatter(fn) { + formatters.formatStaffReplyDM = fn; + }, + + /** + * @param {FormatStaffReplyThreadMessage} fn + * @return {void} + */ + setStaffReplyThreadMessageFormatter(fn) { + formatters.formatStaffReplyThreadMessage = fn; + }, + + /** + * @param {FormatStaffReplyLogMessage} fn + * @return {void} + */ + setStaffReplyLogMessageFormatter(fn) { + formatters.formatStaffReplyLogMessage = fn; + }, + + /** + * @param {FormatUserReplyThreadMessage} fn + * @return {void} + */ + setUserReplyThreadMessageFormatter(fn) { + formatters.formatUserReplyThreadMessage = fn; + }, + + /** + * @param {FormatUserReplyLogMessage} fn + * @return {void} + */ + setUserReplyLogMessageFormatter(fn) { + formatters.formatUserReplyLogMessage = fn; + }, +}; diff --git a/src/plugins.js b/src/plugins.js index 311cecd..7cf3a7f 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -1,5 +1,6 @@ const attachments = require('./data/attachments'); const { beforeNewThread } = require('./hooks'); +const formats = require('./formatters'); module.exports = { getPluginAPI({ bot, knex, config, commands }) { @@ -21,6 +22,7 @@ module.exports = { hooks: { beforeNewThread, }, + formats, }; }, From 662c6b0c2167704f30a126ef2d07bbde5637c05e Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Tue, 14 Jul 2020 01:31:50 +0300 Subject: [PATCH 014/300] Allow message formatters to return full message content objects as well as strings --- src/data/Thread.js | 70 ++++++++++++++++++++++++++++++++-------------- src/formatters.js | 6 ++-- 2 files changed, 52 insertions(+), 24 deletions(-) diff --git a/src/data/Thread.js b/src/data/Thread.js index 3e5faf9..13501fe 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -1,4 +1,5 @@ const moment = require('moment'); +const Eris = require('eris'); const bot = require('../bot'); const knex = require('../knex'); @@ -30,46 +31,73 @@ class Thread { } /** - * @param {string} text + * @param {Eris.MessageContent} text * @param {Eris.MessageFile|Eris.MessageFile[]} file * @returns {Promise} * @throws Error * @private */ - async _sendDMToUser(text, file = null) { + async _sendDMToUser(content, file = null) { // Try to open a DM channel with the user const dmChannel = await this.getDMChannel(); if (! dmChannel) { throw new Error('Could not open DMs with the user. They may have blocked the bot or set their privacy settings higher.'); } - // Send the DM - const chunks = utils.chunk(text, 2000); - const messages = await Promise.all(chunks.map((chunk, i) => { - return dmChannel.createMessage( - chunk, - (i === chunks.length - 1 ? file : undefined) // Only send the file with the last message - ); - })); - return messages[0]; + let firstMessage; + + if (typeof content === 'string') { + // Content is a string, chunk it and send it as individual messages. + // Files (attachments) are only sent with the last message. + const chunks = utils.chunk(content, 2000); + for (const [i, chunk] of chunks.entries()) { + let msg; + if (i === chunks.length - 1) { + // Only send embeds, files, etc. with the last message + msg = await dmChannel.createMessage(chunk, file); + } else { + msg = await dmChannel.createMessage(chunk); + } + + firstMessage = firstMessage || msg; + } + } else { + // Content is a full message content object, send it as-is with the files (if any) + firstMessage = await dmChannel.createMessage(content, file); + } + + return firstMessage; } /** - * @returns {Promise} + * @param {Eris.MessageContent} content + * @param {Eris.MessageFile} file + * @return {Promise} * @private */ - async _postToThreadChannel(...args) { + async _postToThreadChannel(content, file = null) { try { - if (typeof args[0] === 'string') { - const chunks = utils.chunk(args[0], 2000); - const messages = await Promise.all(chunks.map((chunk, i) => { - const rest = (i === chunks.length - 1 ? args.slice(1) : []); // Only send the rest of the args (files, embeds) with the last message - return bot.createMessage(this.channel_id, chunk, ...rest); - })); - return messages[0]; + let firstMessage; + + if (typeof content === 'string') { + // Content is a string, chunk it and send it as individual messages. + // Files (attachments) are only sent with the last message. + const chunks = utils.chunk(content, 2000); + for (const [i, chunk] of chunks.entries()) { + let msg; + if (i === chunks.length - 1) { + // Only send embeds, files, etc. with the last message + msg = await bot.createMessage(this.channel_id, chunk, file); + } + + firstMessage = firstMessage || msg; + } } else { - return bot.createMessage(this.channel_id, ...args); + // Content is a full message content object, send it as-is with the files (if any) + firstMessage = await bot.createMessage(this.channel_id, content, file); } + + return firstMessage; } catch (e) { // Channel not found if (e.code === 10003) { diff --git a/src/formatters.js b/src/formatters.js index 15b5660..4230e57 100644 --- a/src/formatters.js +++ b/src/formatters.js @@ -10,7 +10,7 @@ const config = require('./config'); * @param {{ * isAnonymous: boolean, * }} opts={} - * @return {string} Message content to send as a DM + * @return {Eris.MessageContent} Message content to send as a DM */ /** @@ -22,7 +22,7 @@ const config = require('./config'); * @param {{ * isAnonymous: boolean, * }} opts={} - * @return {string} Message content to post in the thread channel + * @return {Eris.MessageContent} Message content to post in the thread channel */ /** @@ -45,7 +45,7 @@ const config = require('./config'); * @param {{ * attachmentLinks: string[], * }} opts - * @return {string} Message content to post in the thread channel + * @return {Eris.MessageContent} Message content to post in the thread channel */ /** From ec3a2455e0623064a8b6ff558137397e6a6a94fe Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Tue, 14 Jul 2020 01:35:26 +0300 Subject: [PATCH 015/300] Add more safeguards when using Thread#_postToThreadChannel() --- src/data/Thread.js | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/data/Thread.js b/src/data/Thread.js index 13501fe..1db3bc1 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -210,7 +210,7 @@ class Thread { // Show the reply in the inbox thread const inboxContent = formatters.formatStaffReplyThreadMessage(moderator, text, threadMessage.message_number, { isAnonymous }); const inboxMessage = await this._postToThreadChannel(inboxContent, files); - await this._updateThreadMessage(threadMessage.id, { inbox_message_id: inboxMessage.id }); + if (inboxMessage) await this._updateThreadMessage(threadMessage.id, { inbox_message_id: inboxMessage.id }); // Interrupt scheduled closing, if in progress if (this.scheduled_close_at) { @@ -288,16 +288,18 @@ class Thread { * @param {*} args * @returns {Promise} */ - async postSystemMessage(content, ...args) { - const msg = await this._postToThreadChannel(content, ...args); - await this._addThreadMessageToDB({ - message_type: THREAD_MESSAGE_TYPE.SYSTEM, - user_id: null, - user_name: '', - body: typeof content === 'string' ? content : content.content, - is_anonymous: 0, - dm_message_id: msg.id - }); + async postSystemMessage(content, file = null) { + const msg = await this._postToThreadChannel(content, file); + if (msg) { + await this._addThreadMessageToDB({ + message_type: THREAD_MESSAGE_TYPE.SYSTEM, + user_id: null, + user_name: '', + body: typeof content === 'string' ? content : content.content, + is_anonymous: 0, + dm_message_id: msg.id + }); + } } /** From 75b292077737e218e75b3a2d83518a68221dbc77 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Tue, 14 Jul 2020 01:38:35 +0300 Subject: [PATCH 016/300] Use explicit parameters for Thread#postSystemMessage, Thread#sendSystemMessageToUser, Thread#postNonLogMessage --- src/data/Thread.js | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/data/Thread.js b/src/data/Thread.js index 1db3bc1..054b48d 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -284,8 +284,8 @@ class Thread { } /** - * @param {string|Eris.MessageContent} content - * @param {*} args + * @param {Eris.MessageContent} content + * @param {Eris.MessageFile} file * @returns {Promise} */ async postSystemMessage(content, file = null) { @@ -303,12 +303,12 @@ class Thread { } /** - * @param {string|Eris.MessageContent} content - * @param {*} args + * @param {Eris.MessageContent} content + * @param {Eris.MessageFile} file * @returns {Promise} */ - async sendSystemMessageToUser(content, ...args) { - const msg = await this._sendDMToUser(content, ...args); + async sendSystemMessageToUser(content, file = null) { + const msg = await this._sendDMToUser(content, file); await this._addThreadMessageToDB({ message_type: THREAD_MESSAGE_TYPE.SYSTEM_TO_USER, user_id: null, @@ -320,11 +320,12 @@ class Thread { } /** - * @param {*} args - * @returns {Promise} + * @param {Eris.MessageContent} content + * @param {Eris.MessageFile} file + * @return {Promise} */ - async postNonLogMessage(...args) { - await this._postToThreadChannel(...args); + async postNonLogMessage(content, file = null) { + return this._postToThreadChannel(content, file); } /** From 3723bf788b5aa85da075e9e93934c657fa8cdb4e Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 15 Jul 2020 23:50:30 +0300 Subject: [PATCH 017/300] Add 'source' to beforeNewThread hooks, call hooks in threads.createNewThreadForUser() --- src/data/threads.js | 27 +++++++++--- src/hooks.js | 35 --------------- src/hooks/beforeNewThread.js | 82 ++++++++++++++++++++++++++++++++++++ src/main.js | 27 ++---------- src/modules/newthread.js | 7 ++- src/plugins.js | 2 +- 6 files changed, 113 insertions(+), 67 deletions(-) delete mode 100644 src/hooks.js create mode 100644 src/hooks/beforeNewThread.js diff --git a/src/data/threads.js b/src/data/threads.js index aac6bf2..2fefa7a 100644 --- a/src/data/threads.js +++ b/src/data/threads.js @@ -12,6 +12,7 @@ const utils = require('../utils'); const updates = require('./updates'); const Thread = require('./Thread'); +const {callBeforeNewThreadHooks} = require("../hooks/beforeNewThread"); const {THREAD_STATUS} = require('./constants'); const MINUTES = 60 * 1000; @@ -49,13 +50,17 @@ function getHeaderGuildInfo(member) { }; } +/** + * @typedef CreateNewThreadForUserOpts + * @property {boolean} quiet If true, doesn't ping mentionRole or reply with responseMessage + * @property {boolean} ignoreRequirements If true, creates a new thread even if the account doesn't meet requiredAccountAge + * @property {string} source A string identifying the source of the new thread + */ + /** * Creates a new modmail thread for the specified user * @param {User} user - * @param {Object} opts - * @param {Boolean} opts.quiet If true, doesn't ping mentionRole or reply with responseMessage - * @param {Boolean} opts.ignoreRequirements If true, creates a new thread even if the account doesn't meet requiredAccountAge - * @param {String} opts.categoryId Override the category ID for the new thread + * @param {CreateNewThreadForUserOpts} opts * @returns {Promise} * @throws {Error} */ @@ -68,6 +73,9 @@ async function createNewThreadForUser(user, opts = {}) { throw new Error('Attempted to create a new thread for a user with an existing open thread!'); } + const hookResult = await callBeforeNewThreadHooks({ user, opts }); + if (hookResult.cancelled) return; + // If set in config, check that the user's account is old enough (time since they registered on Discord) // If the account is too new, don't start a new thread and optionally reply to them with a message if (config.requiredAccountAge && ! ignoreRequirements) { @@ -131,7 +139,7 @@ async function createNewThreadForUser(user, opts = {}) { console.log(`[NOTE] Creating new thread channel ${channelName}`); // Figure out which category we should place the thread channel in - let newThreadCategoryId = opts.categoryId || null; + let newThreadCategoryId = hookResult.categoryId || null; if (! newThreadCategoryId && config.categoryAutomation.newThreadFromGuild) { // Categories for specific source guilds (in case of multiple main guilds) @@ -340,11 +348,16 @@ async function getClosedThreadCountByUserId(userId) { return parseInt(row.thread_count, 10); } -async function findOrCreateThreadForUser(user) { +/** + * @param {User} user + * @param {CreateNewThreadForUserOpts} opts + * @returns {Promise} + */ +async function findOrCreateThreadForUser(user, opts = {}) { const existingThread = await findOpenThreadByUserId(user.id); if (existingThread) return existingThread; - return createNewThreadForUser(user); + return createNewThreadForUser(user, opts); } async function getThreadsThatShouldBeClosed() { diff --git a/src/hooks.js b/src/hooks.js deleted file mode 100644 index 1896177..0000000 --- a/src/hooks.js +++ /dev/null @@ -1,35 +0,0 @@ -/** - * @callback BeforeNewThreadHook_SetCategoryId - * @param {String} categoryId - * @return void - */ - -/** - * @typedef BeforeNewThreadHookEvent - * @property {Function} cancel - * @property {BeforeNewThreadHook_SetCategoryId} setCategoryId - * - */ - -/** - * @callback BeforeNewThreadHook - * @param {BeforeNewThreadHookEvent} ev - * @return {void|Promise} - */ - -/** - * @type BeforeNewThreadHook[] - */ -const beforeNewThreadHooks = []; - -/** - * @param {BeforeNewThreadHook} fn - */ -function beforeNewThread(fn) { - beforeNewThreadHooks.push(fn); -} - -module.exports = { - beforeNewThreadHooks, - beforeNewThread, -}; diff --git a/src/hooks/beforeNewThread.js b/src/hooks/beforeNewThread.js new file mode 100644 index 0000000..801d84d --- /dev/null +++ b/src/hooks/beforeNewThread.js @@ -0,0 +1,82 @@ +const Eris = require('eris'); + +/** + * @callback BeforeNewThreadHook_SetCategoryId + * @param {String} categoryId + * @return void + */ + +/** + * @typedef BeforeNewThreadHookData + * @property {Eris.User} user + * @property {CreateNewThreadForUserOpts} opts + * @property {Function} cancel + * @property {BeforeNewThreadHook_SetCategoryId} setCategoryId + */ + +/** + * @typedef BeforeNewThreadHookResult + * @property {boolean} cancelled + * @property {string|null} categoryId + */ + +/** + * @callback BeforeNewThreadHookData + * @param {BeforeNewThreadHookData} data + * @return {void|Promise} + */ + +/** + * @type BeforeNewThreadHookData[] + */ +const beforeNewThreadHooks = []; + +/** + * @param {BeforeNewThreadHookData} fn + */ +function beforeNewThread(fn) { + beforeNewThreadHooks.push(fn); +} + +/** + * @param {{ + * user: Eris.User, + * opts: CreateNewThreadForUserOpts, + * }} input + * @return {Promise} + */ +async function callBeforeNewThreadHooks(input) { + /** + * @type {BeforeNewThreadHookResult} + */ + const result = { + cancelled: false, + categoryId: null, + }; + + /** + * @type {BeforeNewThreadHookData} + */ + const data = { + ...input, + + cancel() { + result.cancelled = true; + }, + + setCategoryId(value) { + result.categoryId = value; + }, + }; + + for (const hook of beforeNewThreadHooks) { + await hook(data); + } + + return result; +} + +module.exports = { + beforeNewThread, + callBeforeNewThreadHooks, +}; diff --git a/src/main.js b/src/main.js index 494805e..381efd8 100644 --- a/src/main.js +++ b/src/main.js @@ -8,7 +8,7 @@ const {messageQueue} = require('./queue'); const utils = require('./utils'); const { createCommandManager } = require('./commands'); const { getPluginAPI, loadPlugin } = require('./plugins'); -const { beforeNewThreadHooks } = require('./hooks'); +const { callBeforeNewThreadHooks } = require('./hooks/beforeNewThread'); const blocked = require('./data/blocked'); const threads = require('./data/threads'); @@ -132,28 +132,9 @@ function initBaseMessageHandlers() { // Ignore messages that shouldn't usually open new threads, such as "ok", "thanks", etc. if (config.ignoreAccidentalThreads && msg.content && ACCIDENTAL_THREAD_MESSAGES.includes(msg.content.trim().toLowerCase())) return; - let cancelled = false; - let categoryId = null; - - /** - * @type {BeforeNewThreadHookEvent} - */ - const ev = { - cancel() { - cancelled = true; - }, - - setCategoryId(_categoryId) { - categoryId = _categoryId; - }, - }; - - for (const hook of beforeNewThreadHooks) { - await hook(ev, msg); - if (cancelled) return; - } - - thread = await threads.createNewThreadForUser(msg.author, { categoryId }); + thread = await threads.createNewThreadForUser(msg.author, { + source: 'dm', + }); } if (thread) { diff --git a/src/modules/newthread.js b/src/modules/newthread.js index 35f70d8..834ba55 100644 --- a/src/modules/newthread.js +++ b/src/modules/newthread.js @@ -15,7 +15,12 @@ module.exports = ({ bot, knex, config, commands }) => { return; } - const createdThread = await threads.createNewThreadForUser(user, { quiet: true, ignoreRequirements: true }); + const createdThread = await threads.createNewThreadForUser(user, { + quiet: true, + ignoreRequirements: true, + source: 'command', + }); + createdThread.postSystemMessage(`Thread was opened by ${msg.author.username}#${msg.author.discriminator}`); msg.channel.createMessage(`Thread opened: <#${createdThread.channel_id}>`); diff --git a/src/plugins.js b/src/plugins.js index 7cf3a7f..b078dbb 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -1,5 +1,5 @@ const attachments = require('./data/attachments'); -const { beforeNewThread } = require('./hooks'); +const { beforeNewThread } = require('./hooks/beforeNewThread'); const formats = require('./formatters'); module.exports = { From 6b8c7e1bdfa57fb56376fc9136819b97fd52b75a Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 19 Jul 2020 13:35:54 +0300 Subject: [PATCH 018/300] Rename config.js to cfg.js So people don't accidentally edit the config source file rather than their own config file. --- knexfile.js | 2 +- src/bot.js | 2 +- src/{config.js => cfg.js} | 0 src/commands.js | 2 +- src/data/Thread.js | 2 +- src/data/attachments.js | 2 +- src/data/threads.js | 2 +- src/data/updates.js | 2 +- src/formatters.js | 2 +- src/index.js | 2 +- src/knex.js | 2 +- src/legacy/jsonDb.js | 2 +- src/legacy/legacyMigrator.js | 2 +- src/main.js | 2 +- src/modules/close.js | 2 +- src/modules/greeting.js | 2 +- src/modules/move.js | 2 +- src/modules/snippets.js | 2 +- src/modules/suspend.js | 2 +- src/modules/typingProxy.js | 2 +- src/modules/version.js | 2 +- src/modules/webserver.js | 2 +- src/utils.js | 2 +- 23 files changed, 22 insertions(+), 22 deletions(-) rename src/{config.js => cfg.js} (100%) diff --git a/knexfile.js b/knexfile.js index cf568f4..12606e3 100644 --- a/knexfile.js +++ b/knexfile.js @@ -1,2 +1,2 @@ -const config = require('./src/config'); +const config = require('./src/cfg'); module.exports = config.knex; diff --git a/src/bot.js b/src/bot.js index 2a1591a..41a37d9 100644 --- a/src/bot.js +++ b/src/bot.js @@ -1,5 +1,5 @@ const Eris = require('eris'); -const config = require('./config'); +const config = require('./cfg'); const bot = new Eris.Client(config.token, { getAllUsers: true, diff --git a/src/config.js b/src/cfg.js similarity index 100% rename from src/config.js rename to src/cfg.js diff --git a/src/commands.js b/src/commands.js index 59c6506..1973638 100644 --- a/src/commands.js +++ b/src/commands.js @@ -1,5 +1,5 @@ const { CommandManager, defaultParameterTypes, TypeConversionError } = require('knub-command-manager'); -const config = require('./config'); +const config = require('./cfg'); const utils = require('./utils'); const threads = require('./data/threads'); diff --git a/src/data/Thread.js b/src/data/Thread.js index 054b48d..f4357eb 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -4,7 +4,7 @@ const Eris = require('eris'); const bot = require('../bot'); const knex = require('../knex'); const utils = require('../utils'); -const config = require('../config'); +const config = require('../cfg'); const attachments = require('./attachments'); const { formatters } = require('../formatters'); diff --git a/src/data/attachments.js b/src/data/attachments.js index 40b13b0..cf8cc6d 100644 --- a/src/data/attachments.js +++ b/src/data/attachments.js @@ -3,7 +3,7 @@ const fs = require('fs'); const https = require('https'); const {promisify} = require('util'); const tmp = require('tmp'); -const config = require('../config'); +const config = require('../cfg'); const utils = require('../utils'); const mv = promisify(require('mv')); diff --git a/src/data/threads.js b/src/data/threads.js index 2fefa7a..6288cb1 100644 --- a/src/data/threads.js +++ b/src/data/threads.js @@ -7,7 +7,7 @@ const humanizeDuration = require('humanize-duration'); const bot = require('../bot'); const knex = require('../knex'); -const config = require('../config'); +const config = require('../cfg'); const utils = require('../utils'); const updates = require('./updates'); diff --git a/src/data/updates.js b/src/data/updates.js index a57bc62..b0e7285 100644 --- a/src/data/updates.js +++ b/src/data/updates.js @@ -2,7 +2,7 @@ const url = require('url'); const https = require('https'); const moment = require('moment'); const knex = require('../knex'); -const config = require('../config'); +const config = require('../cfg'); const UPDATE_CHECK_FREQUENCY = 12; // In hours let updateCheckPromise = null; diff --git a/src/formatters.js b/src/formatters.js index 4230e57..04ed053 100644 --- a/src/formatters.js +++ b/src/formatters.js @@ -1,6 +1,6 @@ const Eris = require('eris'); const utils = require('./utils'); -const config = require('./config'); +const config = require('./cfg'); /** * Function to format the DM that is sent to the user when a staff member replies to them via !reply diff --git a/src/index.js b/src/index.js index 1e23ae3..7c5152e 100644 --- a/src/index.js +++ b/src/index.js @@ -41,7 +41,7 @@ try { process.exit(1); } -const config = require('./config'); +const config = require('./cfg'); const utils = require('./utils'); const main = require('./main'); const knex = require('./knex'); diff --git a/src/knex.js b/src/knex.js index b6b6346..ce33d98 100644 --- a/src/knex.js +++ b/src/knex.js @@ -1,2 +1,2 @@ -const config = require('./config'); +const config = require('./cfg'); module.exports = require('knex')(config.knex); diff --git a/src/legacy/jsonDb.js b/src/legacy/jsonDb.js index d2e8ca1..9b97c05 100644 --- a/src/legacy/jsonDb.js +++ b/src/legacy/jsonDb.js @@ -1,6 +1,6 @@ const fs = require('fs'); const path = require('path'); -const config = require('../config'); +const config = require('../cfg'); const dbDir = config.dbDir; diff --git a/src/legacy/legacyMigrator.js b/src/legacy/legacyMigrator.js index 2c491e3..bf3f947 100644 --- a/src/legacy/legacyMigrator.js +++ b/src/legacy/legacyMigrator.js @@ -5,7 +5,7 @@ const moment = require('moment'); const Eris = require('eris'); const knex = require('../knex'); -const config = require('../config'); +const config = require('../cfg'); const jsonDb = require('./jsonDb'); const threads = require('../data/threads'); diff --git a/src/main.js b/src/main.js index 381efd8..b9a319c 100644 --- a/src/main.js +++ b/src/main.js @@ -1,7 +1,7 @@ const Eris = require('eris'); const path = require('path'); -const config = require('./config'); +const config = require('./cfg'); const bot = require('./bot'); const knex = require('./knex'); const {messageQueue} = require('./queue'); diff --git a/src/modules/close.js b/src/modules/close.js index 774348e..b3173ac 100644 --- a/src/modules/close.js +++ b/src/modules/close.js @@ -1,6 +1,6 @@ const moment = require('moment'); const Eris = require('eris'); -const config = require('../config'); +const config = require('../cfg'); const utils = require('../utils'); const threads = require('../data/threads'); const blocked = require('../data/blocked'); diff --git a/src/modules/greeting.js b/src/modules/greeting.js index f85bde9..1d1d7cb 100644 --- a/src/modules/greeting.js +++ b/src/modules/greeting.js @@ -1,6 +1,6 @@ const path = require('path'); const fs = require('fs'); -const config = require('../config'); +const config = require('../cfg'); const utils = require('../utils'); module.exports = ({ bot }) => { diff --git a/src/modules/move.js b/src/modules/move.js index 973143d..44eefd3 100644 --- a/src/modules/move.js +++ b/src/modules/move.js @@ -1,4 +1,4 @@ -const config = require('../config'); +const config = require('../cfg'); const Eris = require('eris'); const transliterate = require("transliteration"); const erisEndpoints = require('eris/lib/rest/Endpoints'); diff --git a/src/modules/snippets.js b/src/modules/snippets.js index e9c1271..2948849 100644 --- a/src/modules/snippets.js +++ b/src/modules/snippets.js @@ -1,6 +1,6 @@ const threads = require('../data/threads'); const snippets = require('../data/snippets'); -const config = require('../config'); +const config = require('../cfg'); const utils = require('../utils'); const { parseArguments } = require('knub-command-manager'); diff --git a/src/modules/suspend.js b/src/modules/suspend.js index b44b323..61d3708 100644 --- a/src/modules/suspend.js +++ b/src/modules/suspend.js @@ -1,7 +1,7 @@ const moment = require('moment'); const threads = require("../data/threads"); const utils = require('../utils'); -const config = require('../config'); +const config = require('../cfg'); const {THREAD_STATUS} = require('../data/constants'); diff --git a/src/modules/typingProxy.js b/src/modules/typingProxy.js index 5881808..d2e585e 100644 --- a/src/modules/typingProxy.js +++ b/src/modules/typingProxy.js @@ -1,4 +1,4 @@ -const config = require('../config'); +const config = require('../cfg'); const threads = require("../data/threads"); const Eris = require("eris"); diff --git a/src/modules/version.js b/src/modules/version.js index 033fbf0..0411cf0 100644 --- a/src/modules/version.js +++ b/src/modules/version.js @@ -3,7 +3,7 @@ const fs = require('fs'); const {promisify} = require('util'); const utils = require("../utils"); const updates = require('../data/updates'); -const config = require('../config'); +const config = require('../cfg'); const access = promisify(fs.access); const readFile = promisify(fs.readFile); diff --git a/src/modules/webserver.js b/src/modules/webserver.js index 6c8a536..22dff34 100644 --- a/src/modules/webserver.js +++ b/src/modules/webserver.js @@ -4,7 +4,7 @@ const url = require('url'); const fs = require('fs'); const qs = require('querystring'); const moment = require('moment'); -const config = require('../config'); +const config = require('../cfg'); const threads = require('../data/threads'); const attachments = require('../data/attachments'); diff --git a/src/utils.js b/src/utils.js index acb1439..4c5d08d 100644 --- a/src/utils.js +++ b/src/utils.js @@ -3,7 +3,7 @@ const bot = require('./bot'); const moment = require('moment'); const humanizeDuration = require('humanize-duration'); const publicIp = require('public-ip'); -const config = require('./config'); +const config = require('./cfg'); class BotError extends Error {} From 4e9e347b04a001a287dbd6988b54a9b46da73cf0 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 19 Jul 2020 14:11:38 +0300 Subject: [PATCH 019/300] Add internal support for editing/deleting staff replies --- src/data/Thread.js | 86 ++++++++++++++++++++++++++++++++++------ src/formatters.js | 98 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 171 insertions(+), 13 deletions(-) diff --git a/src/data/Thread.js b/src/data/Thread.js index f4357eb..0304dcc 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -286,18 +286,22 @@ class Thread { /** * @param {Eris.MessageContent} content * @param {Eris.MessageFile} file + * @param {object} opts + * @param {boolean} opts.saveToLog + * @param {string} opts.logBody * @returns {Promise} */ - async postSystemMessage(content, file = null) { + async postSystemMessage(content, file = null, opts = {}) { const msg = await this._postToThreadChannel(content, file); - if (msg) { + if (msg && opts.saveToLog !== false) { + const finalLogBody = opts.logBody || msg.content || ''; await this._addThreadMessageToDB({ message_type: THREAD_MESSAGE_TYPE.SYSTEM, user_id: null, user_name: '', - body: typeof content === 'string' ? content : content.content, + body: finalLogBody, is_anonymous: 0, - dm_message_id: msg.id + inbox_message_id: msg.id, }); } } @@ -305,18 +309,24 @@ class Thread { /** * @param {Eris.MessageContent} content * @param {Eris.MessageFile} file + * @param {object} opts + * @param {boolean} opts.saveToLog + * @param {string} opts.logBody * @returns {Promise} */ - async sendSystemMessageToUser(content, file = null) { + async sendSystemMessageToUser(content, file = null, opts = {}) { const msg = await this._sendDMToUser(content, file); - await this._addThreadMessageToDB({ - message_type: THREAD_MESSAGE_TYPE.SYSTEM_TO_USER, - user_id: null, - user_name: '', - body: typeof content === 'string' ? content : content.content, - is_anonymous: 0, - dm_message_id: msg.id - }); + if (opts.saveToLog !== false) { + const finalLogBody = opts.logBody || msg.content || ''; + await this._addThreadMessageToDB({ + message_type: THREAD_MESSAGE_TYPE.SYSTEM_TO_USER, + user_id: null, + user_name: '', + body: finalLogBody, + is_anonymous: 0, + dm_message_id: msg.id, + }); + } } /** @@ -529,6 +539,56 @@ class Thread { }); } + /** + * @param {Eris.Member} moderator + * @param {ThreadMessage} threadMessage + * @param {string} newText + * @param {object} opts + * @param {boolean} opts.quiet Whether to suppress edit notifications in the thread channel + * @returns {Promise} + */ + async editStaffReply(moderator, threadMessage, newText, opts = {}) { + const formattedThreadMessage = formatters.formatStaffReplyThreadMessage( + moderator, + newText, + threadMessage.message_number, + { isAnonymous: threadMessage.is_anonymous } + ); + + const formattedDM = formatters.formatStaffReplyDM( + moderator, + newText, + { isAnonymous: threadMessage.is_anonymous } + ); + + await bot.editMessage(threadMessage.dm_channel_id, threadMessage.dm_message_id, formattedDM); + await bot.editMessage(this.channel_id, threadMessage.inbox_message_id, formattedThreadMessage); + + if (! opts.quiet) { + const threadNotification = formatters.formatStaffReplyEditNotificationThreadMessage(moderator, threadMessage, newText); + const logNotification = formatters.formatStaffReplyEditNotificationLogMessage(moderator, threadMessage, newText); + await this.postSystemMessage(threadNotification, null, { logBody: logNotification }); + } + } + + /** + * @param {Eris.Member} moderator + * @param {ThreadMessage} threadMessage + * @param {object} opts + * @param {boolean} opts.quiet Whether to suppress edit notifications in the thread channel + * @returns {Promise} + */ + async deleteStaffReply(moderator, threadMessage, opts = {}) { + await bot.deleteMessage(threadMessage.dm_channel_id, threadMessage.dm_message_id); + await bot.deleteMessage(this.channel_id, threadMessage.inbox_message_id); + + if (! opts.quiet) { + const threadNotification = formatters.formatStaffReplyDeletionNotificationThreadMessage(moderator, threadMessage); + const logNotification = formatters.formatStaffReplyDeletionNotificationLogMessage(moderator, threadMessage); + await this.postSystemMessage(threadNotification, null, { logBody: logNotification }); + } + } + /** * @returns {Promise} */ diff --git a/src/formatters.js b/src/formatters.js index 04ed053..c3af718 100644 --- a/src/formatters.js +++ b/src/formatters.js @@ -1,6 +1,7 @@ const Eris = require('eris'); const utils = require('./utils'); const config = require('./cfg'); +const ThreadMessage = require('./data/ThreadMessage'); /** * Function to format the DM that is sent to the user when a staff member replies to them via !reply @@ -49,6 +50,7 @@ const config = require('./cfg'); */ /** + * Function to format a user reply in a log * @callback FormatUserReplyLogMessage * @param {Eris.User} user * @param {Eris.Message} msg @@ -58,6 +60,40 @@ const config = require('./cfg'); * @return {string} Text to show in the log */ +/** + * Function to format the inbox channel notification for a staff reply edit + * @callback FormatStaffReplyEditNotificationThreadMessage + * @param {Eris.Member} moderator + * @param {ThreadMessage} threadMessage + * @param {string} newText + * @return {Eris.MessageContent} Message content to post in the thread channel + */ + +/** + * Function to format the log notification for a staff reply edit + * @callback FormatStaffReplyEditNotificationLogMessage + * @param {Eris.Member} moderator + * @param {ThreadMessage} threadMessage + * @param {string} newText + * @return {string} Text to show in the log + */ + +/** + * Function to format the inbox channel notification for a staff reply deletion + * @callback FormatStaffReplyDeletionNotificationThreadMessage + * @param {Eris.Member} moderator + * @param {ThreadMessage} threadMessage + * @return {Eris.MessageContent} Message content to post in the thread channel + */ + +/** + * Function to format the log notification for a staff reply deletion + * @callback FormatStaffReplyDeletionNotificationLogMessage + * @param {Eris.Member} moderator + * @param {ThreadMessage} threadMessage + * @return {string} Text to show in the log + */ + /** * @typedef MessageFormatters * @property {FormatStaffReplyDM} formatStaffReplyDM @@ -65,6 +101,10 @@ const config = require('./cfg'); * @property {FormatStaffReplyLogMessage} formatStaffReplyLogMessage * @property {FormatUserReplyThreadMessage} formatUserReplyThreadMessage * @property {FormatUserReplyLogMessage} formatUserReplyLogMessage + * @property {FormatStaffReplyEditNotificationThreadMessage} formatStaffReplyEditNotificationThreadMessage + * @property {FormatStaffReplyEditNotificationLogMessage} formatStaffReplyEditNotificationLogMessage + * @property {FormatStaffReplyDeletionNotificationThreadMessage} formatStaffReplyDeletionNotificationThreadMessage + * @property {FormatStaffReplyDeletionNotificationLogMessage} formatStaffReplyDeletionNotificationLogMessage */ /** @@ -156,6 +196,32 @@ const defaultFormatters = { return result; }, + + formatStaffReplyEditNotificationThreadMessage(moderator, threadMessage, newText) { + let content = `**${moderator.user.username}#${moderator.user.discriminator} (\`${moderator.id}\`) edited reply \`[${threadMessage.message_number}]\`:**`; + content += `\n\`B:\` ${threadMessage.body}`; + content += `\n\`A:\` ${newText}`; + return utils.disableLinkPreviews(content); + }, + + formatStaffReplyEditNotificationLogMessage(moderator, threadMessage, newText) { + let content = `${moderator.user.username}#${moderator.user.discriminator} (${moderator.id}) edited reply [${threadMessage.message_number}]:`; + content += `\nB: ${threadMessage.body}`; + content += `\nA: ${newText}`; + return content; + }, + + formatStaffReplyDeletionNotificationThreadMessage(moderator, threadMessage) { + let content = `**${moderator.user.username}#${moderator.user.discriminator} (\`${moderator.id}\`) deleted reply \`[${threadMessage.message_number}]\`:**`; + content += `\n\`B:\` ${threadMessage.body}`; + return utils.disableLinkPreviews(content); + }, + + formatStaffReplyDeletionNotificationLogMessage(moderator, threadMessage) { + let content = `${moderator.user.username}#${moderator.user.discriminator} (${moderator.id}) deleted reply [${threadMessage.message_number}]:`; + content += `\nB: ${threadMessage.body}`; + return content; + }, }; /** @@ -205,4 +271,36 @@ module.exports = { setUserReplyLogMessageFormatter(fn) { formatters.formatUserReplyLogMessage = fn; }, + + /** + * @param {FormatStaffReplyEditNotificationThreadMessage} fn + * @return {void} + */ + setStaffReplyEditNotificationThreadMessageFormatter(fn) { + formatters.formatStaffReplyEditNotificationThreadMessage = fn; + }, + + /** + * @param {FormatStaffReplyEditNotificationLogMessage} fn + * @return {void} + */ + setStaffReplyEditNotificationLogMessageFormatter(fn) { + formatters.formatStaffReplyEditNotificationLogMessage = fn; + }, + + /** + * @param {FormatStaffReplyDeletionNotificationThreadMessage} fn + * @return {void} + */ + setStaffReplyDeletionNotificationThreadMessageFormatter(fn) { + formatters.formatStaffReplyDeletionNotificationThreadMessage = fn; + }, + + /** + * @param {FormatStaffReplyDeletionNotificationLogMessage} fn + * @return {void} + */ + setStaffReplyDeletionNotificationLogMessageFormatter(fn) { + formatters.formatStaffReplyDeletionNotificationLogMessage = fn; + }, }; From 3002473905092fbfbc03908cb799b7581810d0f4 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 19 Jul 2020 14:20:45 +0300 Subject: [PATCH 020/300] Add !edit and !delete with options (disabled by default) --- src/cfg.js | 2 ++ src/commands.js | 15 ++++++++++++++- src/modules/reply.js | 41 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/src/cfg.js b/src/cfg.js index 5d649f8..dc3b47b 100644 --- a/src/cfg.js +++ b/src/cfg.js @@ -89,6 +89,8 @@ const defaultConfig = { "typingProxyReverse": false, "mentionUserInThreadHeader": false, "rolesInThreadHeader": false, + "allowStaffEdit": false, + "allowStaffDelete": false, "enableGreeting": false, "greetingMessage": null, diff --git a/src/commands.js b/src/commands.js index 1973638..3135ee9 100644 --- a/src/commands.js +++ b/src/commands.js @@ -1,7 +1,9 @@ -const { CommandManager, defaultParameterTypes, TypeConversionError } = require('knub-command-manager'); +const { CommandManager, defaultParameterTypes, TypeConversionError, IParameter, ICommandConfig } = require('knub-command-manager'); +const Eris = require('eris'); const config = require('./cfg'); const utils = require('./utils'); const threads = require('./data/threads'); +const Thread = require('./data/Thread'); module.exports = { createCommandManager(bot) { @@ -84,8 +86,19 @@ module.exports = { }; }; + /** + * @callback InboxThreadCommandHandler + * @param {Eris.Message} msg + * @param {object} args + * @param {Thread} thread + */ + /** * Add a command that can only be invoked in a thread on the inbox server + * @param {string|RegExp} trigger + * @param {string|IParameter[]} parameters + * @param {InboxThreadCommandHandler} handler + * @param {ICommandConfig} commandConfig */ const addInboxThreadCommand = (trigger, parameters, handler, commandConfig = {}) => { const aliases = aliasMap.has(trigger) ? [...aliasMap.get(trigger)] : []; diff --git a/src/modules/reply.js b/src/modules/reply.js index bc2afe3..cd0bef2 100644 --- a/src/modules/reply.js +++ b/src/modules/reply.js @@ -1,5 +1,7 @@ const attachments = require("../data/attachments"); const utils = require('../utils'); +const config = require('../cfg'); +const Thread = require('../data/Thread'); module.exports = ({ bot, knex, config, commands }) => { // Mods can reply to modmail threads using !r or !reply @@ -16,7 +18,6 @@ module.exports = ({ bot, knex, config, commands }) => { aliases: ['r'] }); - // Anonymous replies only show the role, not the username commands.addInboxThreadCommand('anonreply', '[text$]', async (msg, args, thread) => { if (! args.text && msg.attachments.length === 0) { @@ -29,4 +30,42 @@ module.exports = ({ bot, knex, config, commands }) => { }, { aliases: ['ar'] }); + + if (config.allowStaffEdit) { + commands.addInboxThreadCommand('edit', ' ', async (msg, args, thread) => { + const threadMessage = await thread.findThreadMessageByMessageNumber(args.messageNumber); + if (! threadMessage) { + utils.postError(msg.channel, 'Unknown message number'); + return; + } + + if (threadMessage.user_id !== msg.author.id) { + utils.postError(msg.channel, 'You can only edit your own replies'); + return; + } + + await thread.editStaffReply(msg.member, threadMessage, args.text) + }, { + aliases: ['e'] + }); + } + + if (config.allowStaffDelete) { + commands.addInboxThreadCommand('delete', '', async (msg, args, thread) => { + const threadMessage = await thread.findThreadMessageByMessageNumber(args.messageNumber); + if (! threadMessage) { + utils.postError(msg.channel, 'Unknown message number'); + return; + } + + if (threadMessage.user_id !== msg.author.id) { + utils.postError(msg.channel, 'You can only delete your own replies'); + return; + } + + await thread.deleteStaffReply(msg.member, threadMessage); + }, { + aliases: ['d'] + }); + } }; From e74363a55ccdfc2cf4f160b77ac8ccfc19eccb8a Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 19 Jul 2020 14:24:17 +0300 Subject: [PATCH 021/300] Show staff reply numbers in threads/logs --- src/data/Thread.js | 2 +- src/formatters.js | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/data/Thread.js b/src/data/Thread.js index 0304dcc..84582c3 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -197,7 +197,7 @@ class Thread { } // Save the log entry - const logContent = formatters.formatStaffReplyLogMessage(moderator, text, { isAnonymous, attachmentLinks }); + const logContent = formatters.formatStaffReplyLogMessage(moderator, text, threadMessage.message_number, { isAnonymous, attachmentLinks }); const threadMessage = await this._addThreadMessageToDB({ message_type: THREAD_MESSAGE_TYPE.TO_USER, user_id: moderator.id, diff --git a/src/formatters.js b/src/formatters.js index c3af718..8733019 100644 --- a/src/formatters.js +++ b/src/formatters.js @@ -31,6 +31,7 @@ const ThreadMessage = require('./data/ThreadMessage'); * @callback FormatStaffReplyLogMessage * @param {Eris.Member} moderator * @param {string} text + * @param {number} messageNumber * @param {{ * isAnonymous: boolean, * attachmentLinks: string[], @@ -128,7 +129,6 @@ const defaultFormatters = { ? `(Anonymous) (${modName}) ${mainRole ? mainRole.name : 'Moderator'}` : (mainRole ? `(${mainRole.name}) ${modName}` : modName); - // TODO: Add \`[${messageNumber}]\` here once !edit and !delete exist let result = `**${modInfo}:** ${text}`; if (config.threadTimestamps) { @@ -136,10 +136,12 @@ const defaultFormatters = { result = `[${formattedTimestamp}] ${result}`; } + result = `\`[${messageNumber}]\` ${result}`; + return result; }, - formatStaffReplyLogMessage(moderator, text, opts = {}) { + formatStaffReplyLogMessage(moderator, text, messageNumber, opts = {}) { const mainRole = utils.getMainRole(moderator); const modName = moderator.user.username; @@ -157,6 +159,8 @@ const defaultFormatters = { } } + result = `[${messageNumber}] ${result}`; + return result; }, From ce8ebbfc2f632f9673cf23545a0ec02d7618853c Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 19 Jul 2020 14:28:32 +0300 Subject: [PATCH 022/300] Propagate staff reply edits/deletions to the DB --- src/data/Thread.js | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/data/Thread.js b/src/data/Thread.js index 84582c3..7046366 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -136,8 +136,8 @@ class Thread { } /** - * @param {string} id - * @param {Object} data + * @param {number} id + * @param {object} data * @returns {Promise} * @private */ @@ -147,6 +147,17 @@ class Thread { .update(data); } + /** + * @param {number} id + * @returns {Promise} + * @private + */ + async _deleteThreadMessage(id) { + await knex('thread_messages') + .where('id', id) + .delete(); + } + /** * @returns {string} * @private @@ -561,6 +572,14 @@ class Thread { { isAnonymous: threadMessage.is_anonymous } ); + // FIXME: Fix attachment links disappearing by moving them off the main message content in the DB + const formattedLog = formatters.formatStaffReplyLogMessage( + moderator, + newText, + threadMessage.message_number, + { isAnonymous: threadMessage.is_anonymous } + ); + await bot.editMessage(threadMessage.dm_channel_id, threadMessage.dm_message_id, formattedDM); await bot.editMessage(this.channel_id, threadMessage.inbox_message_id, formattedThreadMessage); @@ -569,6 +588,8 @@ class Thread { const logNotification = formatters.formatStaffReplyEditNotificationLogMessage(moderator, threadMessage, newText); await this.postSystemMessage(threadNotification, null, { logBody: logNotification }); } + + await this._updateThreadMessage(threadMessage.id, { body: formattedLog }); } /** @@ -587,6 +608,8 @@ class Thread { const logNotification = formatters.formatStaffReplyDeletionNotificationLogMessage(moderator, threadMessage); await this.postSystemMessage(threadNotification, null, { logBody: logNotification }); } + + await this._deleteThreadMessage(threadMessage.id); } /** From d6348ea8968de9fa971701da1bc1718c35b30dd7 Mon Sep 17 00:00:00 2001 From: Miikka <2606411+Dragory@users.noreply.github.com> Date: Sun, 2 Aug 2020 22:16:41 +0300 Subject: [PATCH 023/300] Add link to community resources in the README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index af4e399..1089ed6 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Inspired by Reddit's modmail system. * [🧩 Plugins](docs/plugins.md) * [🙋 Frequently Asked Questions](docs/faq.md) * [Release notes](CHANGELOG.md) +* [**Community Guides & Resources**](https://github.com/Dragory/modmailbot-community-resources) ## Support server If you need help with setting up the bot or would like to discuss other things related to it, join the support server on Discord here: From 468d1fc0377ff7237a9688ebbb8cdc141508f827 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 12 Aug 2020 23:18:42 +0300 Subject: [PATCH 024/300] Use JSON Schema via AJV for config schema + validation --- package-lock.json | 39 ++++- package.json | 5 +- src/cfg.js | 229 +++++++++--------------------- src/data/cfg.jsdoc.js | 53 +++++++ src/data/cfg.schema.json | 268 +++++++++++++++++++++++++++++++++++ src/data/generateCfgJsdoc.js | 8 ++ 6 files changed, 431 insertions(+), 171 deletions(-) create mode 100644 src/data/cfg.jsdoc.js create mode 100644 src/data/cfg.schema.json create mode 100644 src/data/generateCfgJsdoc.js diff --git a/package-lock.json b/package-lock.json index 2ec9b68..6e9477c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,14 +55,21 @@ "dev": true }, "ajv": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", - "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", + "version": "6.12.3", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.3.tgz", + "integrity": "sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA==", "requires": { - "fast-deep-equal": "^2.0.1", + "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" + }, + "dependencies": { + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + } } }, "ansi-align": { @@ -1254,7 +1261,8 @@ "fast-deep-equal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true }, "fast-json-stable-stringify": { "version": "2.0.0", @@ -1372,6 +1380,11 @@ "for-in": "^1.0.1" } }, + "foreach": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", + "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=" + }, "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -2106,12 +2119,28 @@ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=" }, + "json-pointer": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/json-pointer/-/json-pointer-0.6.0.tgz", + "integrity": "sha1-jlAFUKaqxUZKRzN32leqbMIoKNc=", + "requires": { + "foreach": "^2.0.4" + } + }, "json-schema": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", "optional": true }, + "json-schema-to-jsdoc": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-to-jsdoc/-/json-schema-to-jsdoc-1.0.0.tgz", + "integrity": "sha512-xuP+10g5VOOTrA5ELnOVO1puiCYPQfx0GqmtDQh/OGGh+CbXyNLtJeEpKl6HPXQbiPPYm7NmMypkRlznZmfZbg==", + "requires": { + "json-pointer": "^0.6.0" + } + }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", diff --git a/package.json b/package.json index c183c23..07a81ba 100644 --- a/package.json +++ b/package.json @@ -8,16 +8,19 @@ "start": "node src/index.js", "watch": "nodemon -w src src/index.js", "test": "echo \"Error: no test specified\" && exit 1", - "lint": "eslint ./src" + "lint": "eslint ./src", + "generate-config-jsdoc": "node src/data/generateCfgJsdoc.js" }, "repository": { "type": "git", "url": "https://github.com/Dragory/modmailbot" }, "dependencies": { + "ajv": "^6.12.3", "eris": "^0.11.1", "humanize-duration": "^3.12.1", "ini": "^1.3.5", + "json-schema-to-jsdoc": "^1.0.0", "json5": "^2.1.1", "knex": "^0.20.3", "knub-command-manager": "^6.1.0", diff --git a/src/cfg.js b/src/cfg.js index dc3b47b..a31f36e 100644 --- a/src/cfg.js +++ b/src/cfg.js @@ -1,29 +1,23 @@ -/** - * !!! NOTE !!! - * - * If you're setting up the bot, DO NOT EDIT THIS FILE DIRECTLY! - * - * Create a configuration file in the same directory as the example file. - * You never need to edit anything under src/ to use the bot. - * - * !!! NOTE !!! - */ - const fs = require('fs'); const path = require('path'); +const Ajv = require('ajv'); +const schema = require('./data/cfg.schema.json'); -let userConfig = {}; +/** @type {ModmailConfig} */ +let config = {}; // Config files to search for, in priority order const configFiles = [ 'config.ini', - 'config.ini.ini', - 'config.ini.txt', 'config.json', 'config.json5', + 'config.js', + + // Possible config files when file extensions are hidden + 'config.ini.ini', + 'config.ini.txt', 'config.json.json', 'config.json.txt', - 'config.js' ]; let foundConfigFile; @@ -35,18 +29,18 @@ for (const configFile of configFiles) { } catch (e) {} } -// Load config file +// Load config values from a config file (if any) if (foundConfigFile) { console.log(`Loading configuration from ${foundConfigFile}...`); try { if (foundConfigFile.endsWith('.js')) { - userConfig = require(`../${foundConfigFile}`); + config = require(`../${foundConfigFile}`); } else { const raw = fs.readFileSync(__dirname + '/../' + foundConfigFile, {encoding: "utf8"}); if (foundConfigFile.endsWith('.ini') || foundConfigFile.endsWith('.ini.txt')) { - userConfig = require('ini').decode(raw); + config = require('ini').decode(raw); } else { - userConfig = require('json5').parse(raw); + config = require('json5').parse(raw); } } } catch (e) { @@ -54,76 +48,9 @@ if (foundConfigFile) { } } -const required = ['token', 'mailGuildId', 'mainGuildId', 'logChannelId']; -const numericOptions = ['requiredAccountAge', 'requiredTimeOnServer', 'smallAttachmentLimit', 'port']; - -const defaultConfig = { - "token": null, - "mailGuildId": null, - "mainGuildId": null, - "logChannelId": null, - - "prefix": "!", - "snippetPrefix": "!!", - "snippetPrefixAnon": "!!!", - - "status": "Message me for help!", - "responseMessage": "Thank you for your message! Our mod team will reply to you here as soon as possible.", - "closeMessage": null, - "allowUserClose": false, - - "newThreadCategoryId": null, - "mentionRole": "here", - "pingOnBotMention": true, - "botMentionResponse": null, - - "inboxServerPermission": null, - "alwaysReply": false, - "alwaysReplyAnon": false, - "useNicknames": false, - "ignoreAccidentalThreads": false, - "threadTimestamps": false, - "allowMove": false, - "syncPermissionsOnMove": true, - "typingProxy": false, - "typingProxyReverse": false, - "mentionUserInThreadHeader": false, - "rolesInThreadHeader": false, - "allowStaffEdit": false, - "allowStaffDelete": false, - - "enableGreeting": false, - "greetingMessage": null, - "greetingAttachment": null, - - "guildGreetings": {}, - - "requiredAccountAge": null, // In hours - "accountAgeDeniedMessage": "Your Discord account is not old enough to contact modmail.", - - "requiredTimeOnServer": null, // In minutes - "timeOnServerDeniedMessage": "You haven't been a member of the server for long enough to contact modmail.", - - "relaySmallAttachmentsAsAttachments": false, - "smallAttachmentLimit": 1024 * 1024 * 2, - "attachmentStorage": "local", - "attachmentStorageChannelId": null, - - "categoryAutomation": {}, - - "updateNotifications": true, - "plugins": [], - - "commandAliases": {}, - - "port": 8890, - "url": null, - - "dbDir": path.join(__dirname, '..', 'db'), - "knex": null, - - "logDir": path.join(__dirname, '..', 'logs'), -}; +// Set dynamic default values which can't be set in the schema directly +config.dbDir = path.join(__dirname, '..', 'db'); +config.logDir = path.join(__dirname, '..', 'logs'); // Only used for migrating data from older Modmail versions // Load config values from environment variables const envKeyPrefix = 'MM_'; @@ -139,16 +66,16 @@ for (const [key, value] of Object.entries(process.env)) { .replace(/([a-z])_([a-z])/g, (m, m1, m2) => `${m1}${m2.toUpperCase()}`) .replace('__', '.'); - userConfig[configKey] = value.includes('||') + config[configKey] = value.includes('||') ? value.split('||') : value; loadedEnvValues++; } -if (process.env.PORT && !process.env.MM_PORT) { +if (process.env.PORT && ! process.env.MM_PORT) { // Special case: allow common "PORT" environment variable without prefix - userConfig.port = process.env.PORT; + config.port = process.env.PORT; loadedEnvValues++; } @@ -158,11 +85,11 @@ if (loadedEnvValues > 0) { // Convert config keys with periods to objects // E.g. commandAliases.mv -> commandAliases: { mv: ... } -for (const [key, value] of Object.entries(userConfig)) { +for (const [key, value] of Object.entries(config)) { if (! key.includes('.')) continue; const keys = key.split('.'); - let cursor = userConfig; + let cursor = config; for (let i = 0; i < keys.length; i++) { if (i === keys.length - 1) { cursor[keys[i]] = value; @@ -172,60 +99,47 @@ for (const [key, value] of Object.entries(userConfig)) { } } - delete userConfig[key]; + delete config[key]; } -// Combine user config with default config to form final config -const finalConfig = Object.assign({}, defaultConfig); - -for (const [prop, value] of Object.entries(userConfig)) { - if (! defaultConfig.hasOwnProperty(prop)) { - throw new Error(`Unknown option: ${prop}`); +// Cast boolean options (on, true, 1) (off, false, 0) +for (const [key, value] of Object.entries(config)) { + if (typeof value !== "string") continue; + if (["on", "true", "1"].includes(value)) { + config[key] = true; + } else if (["off", "false", "0"].includes(value)) { + config[key] = false; } - - finalConfig[prop] = value; } -// Default knex config -if (! finalConfig['knex']) { - finalConfig['knex'] = { +if (! config['knex']) { + config.knex = { client: 'sqlite', - connection: { - filename: path.join(finalConfig.dbDir, 'data.sqlite') + connection: { + filename: path.join(config.dbDir, 'data.sqlite') }, useNullAsDefault: true }; } // Make sure migration settings are always present in knex config -Object.assign(finalConfig['knex'], { +Object.assign(config['knex'], { migrations: { - directory: path.join(finalConfig.dbDir, 'migrations') + directory: path.join(config.dbDir, 'migrations') } }); -if (finalConfig.smallAttachmentLimit > 1024 * 1024 * 8) { - finalConfig.smallAttachmentLimit = 1024 * 1024 * 8; - console.warn('[WARN] smallAttachmentLimit capped at 8MB'); -} - -// Specific checks -if (finalConfig.attachmentStorage === 'discord' && ! finalConfig.attachmentStorageChannelId) { - console.error('Config option \'attachmentStorageChannelId\' is required with attachment storage \'discord\''); - process.exit(1); -} - // Make sure mainGuildId is internally always an array -if (! Array.isArray(finalConfig['mainGuildId'])) { - finalConfig['mainGuildId'] = [finalConfig['mainGuildId']]; +if (! Array.isArray(config['mainGuildId'])) { + config['mainGuildId'] = [config['mainGuildId']]; } // Make sure inboxServerPermission is always an array -if (! Array.isArray(finalConfig['inboxServerPermission'])) { - if (finalConfig['inboxServerPermission'] == null) { - finalConfig['inboxServerPermission'] = []; +if (! Array.isArray(config['inboxServerPermission'])) { + if (config['inboxServerPermission'] == null) { + config['inboxServerPermission'] = []; } else { - finalConfig['inboxServerPermission'] = [finalConfig['inboxServerPermission']]; + config['inboxServerPermission'] = [config['inboxServerPermission']]; } } @@ -233,59 +147,44 @@ if (! Array.isArray(finalConfig['inboxServerPermission'])) { // Or, in other words, if greetingMessage and/or greetingAttachment is set, it is applied for all servers that don't // already have something set up in guildGreetings. This retains backwards compatibility while allowing you to override // greetings for specific servers in guildGreetings. -if (finalConfig.greetingMessage || finalConfig.greetingAttachment) { - for (const guildId of finalConfig.mainGuildId) { - if (finalConfig.guildGreetings[guildId]) continue; - finalConfig.guildGreetings[guildId] = { - message: finalConfig.greetingMessage, - attachment: finalConfig.greetingAttachment +if (config.greetingMessage || config.greetingAttachment) { + for (const guildId of config.mainGuildId) { + if (config.guildGreetings[guildId]) continue; + config.guildGreetings[guildId] = { + message: config.greetingMessage, + attachment: config.greetingAttachment }; } } // newThreadCategoryId is syntactic sugar for categoryAutomation.newThread -if (finalConfig.newThreadCategoryId) { - finalConfig.categoryAutomation.newThread = finalConfig.newThreadCategoryId; - delete finalConfig.newThreadCategoryId; +if (config.newThreadCategoryId) { + config.categoryAutomation.newThread = config.newThreadCategoryId; + delete config.newThreadCategoryId; } // Turn empty string options to null (i.e. "option=" without a value in config.ini) -for (const [key, value] of Object.entries(finalConfig)) { +for (const [key, value] of Object.entries(config)) { if (value === '') { - finalConfig[key] = null; + config[key] = null; } } -// Cast numeric options to numbers -for (const numericOpt of numericOptions) { - if (finalConfig[numericOpt] != null) { - const number = parseFloat(finalConfig[numericOpt]); - if (Number.isNaN(number)) { - console.error(`Invalid numeric value for ${numericOpt}: ${finalConfig[numericOpt]}`); - process.exit(1); - } - finalConfig[numericOpt] = number; - } -} +// Validate config and assign defaults (if missing) +const ajv = new Ajv({ useDefaults: true }); +const validate = ajv.compile(schema); +const configIsValid = validate(config); -// Cast boolean options (on, true, 1) (off, false, 0) -for (const [key, value] of Object.entries(finalConfig)) { - if (typeof value !== "string") continue; - if (["on", "true", "1"].includes(value)) { - finalConfig[key] = true; - } else if (["off", "false", "0"].includes(value)) { - finalConfig[key] = false; - } -} - -// Make sure all of the required config options are present -for (const opt of required) { - if (! finalConfig[opt]) { - console.error(`Missing required configuration value: ${opt}`); - process.exit(1); +if (! configIsValid) { + console.error('Issues with configuration options:'); + for (const error of validate.errors) { + console.error(`The "${error.dataPath.slice(1)}" option ${error.message}`); } + console.error(''); + console.error('Please restart the bot after fixing the issues mentioned above.'); + process.exit(1); } console.log("Configuration ok!"); -module.exports = finalConfig; +module.exports = config; diff --git a/src/data/cfg.jsdoc.js b/src/data/cfg.jsdoc.js new file mode 100644 index 0000000..a581fe1 --- /dev/null +++ b/src/data/cfg.jsdoc.js @@ -0,0 +1,53 @@ +/** + * @typedef {object} ModmailConfig + * @property {string} [token] + * @property {*} [mainGuildId] + * @property {string} [mailGuildId] + * @property {string} [logChannelId] + * @property {string} [prefix="!"] + * @property {string} [snippetPrefix="!!"] + * @property {string} [snippetPrefixAnon="!!!"] + * @property {string} [status="Message me for help!"] + * @property {*} [responseMessage="Thank you for your message! Our mod team will reply to you here as soon as possible."] + * @property {*} [closeMessage] + * @property {boolean} [allowUserClose=false] + * @property {string} [newThreadCategoryId] + * @property {string} [mentionRole="here"] + * @property {boolean} [pingOnBotMention=true] + * @property {*} [botMentionResponse] + * @property {*} [inboxServerPermission] + * @property {boolean} [alwaysReply=false] + * @property {boolean} [alwaysReplyAnon=false] + * @property {boolean} [useNicknames=false] + * @property {boolean} [ignoreAccidentalThreads=false] + * @property {boolean} [threadTimestamps=false] + * @property {boolean} [allowMove=false] + * @property {boolean} [syncPermissionsOnMove=true] + * @property {boolean} [typingProxy=false] + * @property {boolean} [typingProxyReverse=false] + * @property {boolean} [mentionUserInThreadHeader=false] + * @property {boolean} [rolesInThreadHeader=false] + * @property {boolean} [allowStaffEdit=false] + * @property {boolean} [allowStaffDelete=false] + * @property {boolean} [enableGreeting=false] + * @property {*} [greetingMessage] + * @property {string} [greetingAttachment] + * @property {*} [guildGreetings={}] + * @property {number} [requiredAccountAge] Required account age to message Modmail, in hours + * @property {*} [accountAgeDeniedMessage="Your Discord account is not old enough to contact modmail."] + * @property {number} [requiredTimeOnServer] Required time on server to message Modmail, in minutes + * @property {*} [timeOnServerDeniedMessage="You haven't been a member of the server for long enough to contact modmail."] + * @property {boolean} [relaySmallAttachmentsAsAttachments=false] + * @property {number} [smallAttachmentLimit=2097152] Max size of attachment to relay directly. Default is 2MB. + * @property {string} [attachmentStorage="local"] + * @property {string} [attachmentStorageChannelId] + * @property {*} [categoryAutomation] + * @property {boolean} [updateNotifications=true] + * @property {array} [plugins=[]] + * @property {*} [commandAliases] + * @property {number} [port=8890] + * @property {string} [url] + * @property {string} [dbDir] + * @property {object} [knex] + * @property {string} [logDir] + */ diff --git a/src/data/cfg.schema.json b/src/data/cfg.schema.json new file mode 100644 index 0000000..de1a675 --- /dev/null +++ b/src/data/cfg.schema.json @@ -0,0 +1,268 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ModmailConfig", + "type": "object", + "definitions": { + "stringOrArrayOfStrings": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "stringOrMultilineString": { + "$ref": "#/definitions/stringOrArrayOfStrings" + } + }, + "properties": { + "token": { + "type": "string" + }, + "mainGuildId": { + "$ref": "#/definitions/stringOrArrayOfStrings" + }, + "mailGuildId": { + "type": "string" + }, + "logChannelId": { + "type": "string" + }, + + "prefix": { + "type": "string", + "default": "!" + }, + "snippetPrefix": { + "type": "string", + "default": "!!" + }, + "snippetPrefixAnon": { + "type": "string", + "default": "!!!" + }, + "status": { + "type": "string", + "default": "Message me for help!" + }, + "responseMessage": { + "$ref": "#/definitions/stringOrMultilineString", + "default": "Thank you for your message! Our mod team will reply to you here as soon as possible." + }, + "closeMessage": { + "$ref": "#/definitions/stringOrMultilineString" + }, + "allowUserClose": { + "type": "boolean", + "default": false + }, + + "newThreadCategoryId": { + "type": "string" + }, + "mentionRole": { + "type": "string", + "default": "here" + }, + "pingOnBotMention": { + "type": "boolean", + "default": true + }, + "botMentionResponse": { + "$ref": "#/definitions/stringOrMultilineString" + }, + + "inboxServerPermission": { + "$ref": "#/definitions/stringOrArrayOfStrings" + }, + "alwaysReply": { + "type": "boolean", + "default": false + }, + "alwaysReplyAnon": { + "type": "boolean", + "default": false + }, + "useNicknames": { + "type": "boolean", + "default": false + }, + "ignoreAccidentalThreads": { + "type": "boolean", + "default": false + }, + "threadTimestamps": { + "type": "boolean", + "default": false + }, + "allowMove": { + "type": "boolean", + "default": false + }, + "syncPermissionsOnMove": { + "type": "boolean", + "default": true + }, + "typingProxy": { + "type": "boolean", + "default": false + }, + "typingProxyReverse": { + "type": "boolean", + "default": false + }, + "mentionUserInThreadHeader": { + "type": "boolean", + "default": false + }, + "rolesInThreadHeader": { + "type": "boolean", + "default": false + }, + "allowStaffEdit": { + "type": "boolean", + "default": false + }, + "allowStaffDelete": { + "type": "boolean", + "default": false + }, + + "enableGreeting": { + "type": "boolean", + "default": false + }, + "greetingMessage": { + "$ref": "#/definitions/stringOrMultilineString" + }, + "greetingAttachment": { + "type": "string" + }, + "guildGreetings": { + "patternProperties": { + "^\\d+$": { + "$ref": "#/definitions/stringOrMultilineString" + } + }, + "default": {} + }, + + "requiredAccountAge": { + "description": "Required account age to message Modmail, in hours", + "type": "number" + }, + "accountAgeDeniedMessage": { + "$ref": "#/definitions/stringOrMultilineString", + "default": "Your Discord account is not old enough to contact modmail." + }, + + "requiredTimeOnServer": { + "description": "Required time on server to message Modmail, in minutes", + "type": "number" + }, + "timeOnServerDeniedMessage": { + "$ref": "#/definitions/stringOrMultilineString", + "default": "You haven't been a member of the server for long enough to contact modmail." + }, + + "relaySmallAttachmentsAsAttachments": { + "type": "boolean", + "default": false + }, + "smallAttachmentLimit": { + "description": "Max size of attachment to relay directly. Default is 2MB.", + "type": "number", + "default": 2097152, + "maximum": 8388608, + "minimum": 0 + }, + + "attachmentStorage": { + "type": "string", + "default": "local" + }, + "attachmentStorageChannelId": { + "type": "string" + }, + + "categoryAutomation": { + "patternProperties": { + "^.+$": { + "type": "string", + "pattern": "^\\d+$" + } + }, + "default": {} + }, + + "updateNotifications": { + "type": "boolean", + "default": true + }, + "plugins": { + "type": "array", + "items": { + "type": "string" + }, + "default": [] + }, + + "commandAliases": { + "patternProperties": { + "^.+$": { + "type": "string" + } + } + }, + + "port": { + "type": "number", + "maximum": 65535, + "minimum": 1, + "default": 8890 + }, + "url": { + "type": "string" + }, + + "dbDir": { + "type": "string" + }, + "knex": { + "type": "object" + }, + + "logDir": { + "type": "string", + "deprecationMessage": "This option is no longer used" + } + }, + "allOf": [ + { + "$comment": "Base required values", + "required": ["token", "mainGuildId", "mailGuildId", "logChannelId"] + }, + { + "$comment": "Make attachmentStorageChannelId required if attachmentStorage is set to 'discord'", + "if": { + "properties": { + "attachmentStorage": { + "const": "discord" + } + }, + "required": ["attachmentStorage"] + }, + "then": { + "required": ["attachmentStorageChannelId"] + } + } + ] +} diff --git a/src/data/generateCfgJsdoc.js b/src/data/generateCfgJsdoc.js new file mode 100644 index 0000000..219615c --- /dev/null +++ b/src/data/generateCfgJsdoc.js @@ -0,0 +1,8 @@ +const path = require('path'); +const fs = require('fs'); +const toJsdoc = require('json-schema-to-jsdoc'); +const schema = require('./cfg.schema.json'); +const target = path.join(__dirname, 'cfg.jsdoc.js'); + +const result = toJsdoc(schema); +fs.writeFileSync(target, result, { encoding: 'utf8' }); From d03903ce807478454c06ebf953151d633ee925ab Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 12 Aug 2020 23:19:11 +0300 Subject: [PATCH 025/300] Move beforeNewThread hook after validations, fix a couple bugs --- src/data/Thread.js | 5 +++-- src/data/constants.js | 11 +++++++++++ src/data/threads.js | 14 +++++++++----- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/data/Thread.js b/src/data/Thread.js index 7046366..465d25f 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -208,15 +208,16 @@ class Thread { } // Save the log entry - const logContent = formatters.formatStaffReplyLogMessage(moderator, text, threadMessage.message_number, { isAnonymous, attachmentLinks }); const threadMessage = await this._addThreadMessageToDB({ message_type: THREAD_MESSAGE_TYPE.TO_USER, user_id: moderator.id, user_name: fullModeratorName, - body: logContent, + body: '', is_anonymous: (isAnonymous ? 1 : 0), dm_message_id: dmMessage.id }); + const logContent = formatters.formatStaffReplyLogMessage(moderator, text, threadMessage.message_number, { isAnonymous, attachmentLinks }); + await this._updateThreadMessage(threadMessage.id, { body: logContent }); // Show the reply in the inbox thread const inboxContent = formatters.formatStaffReplyThreadMessage(moderator, text, threadMessage.message_number, { isAnonymous }); diff --git a/src/data/constants.js b/src/data/constants.js index 01bae0c..40388db 100644 --- a/src/data/constants.js +++ b/src/data/constants.js @@ -15,6 +15,17 @@ module.exports = { SYSTEM_TO_USER: 7 }, + // https://discord.com/developers/docs/resources/channel#channel-object-channel-types + DISOCRD_CHANNEL_TYPES: { + GUILD_TEXT: 0, + DM: 1, + GUILD_VOICE: 2, + GROUP_DM: 3, + GUILD_CATEGORY: 4, + GUILD_NEWS: 5, + GUILD_STORE: 6, + }, + ACCIDENTAL_THREAD_MESSAGES: [ 'ok', 'okay', diff --git a/src/data/threads.js b/src/data/threads.js index 6288cb1..4120489 100644 --- a/src/data/threads.js +++ b/src/data/threads.js @@ -13,7 +13,7 @@ const updates = require('./updates'); const Thread = require('./Thread'); const {callBeforeNewThreadHooks} = require("../hooks/beforeNewThread"); -const {THREAD_STATUS} = require('./constants'); +const {THREAD_STATUS, DISOCRD_CHANNEL_TYPES} = require('./constants'); const MINUTES = 60 * 1000; const HOURS = 60 * MINUTES; @@ -73,9 +73,6 @@ async function createNewThreadForUser(user, opts = {}) { throw new Error('Attempted to create a new thread for a user with an existing open thread!'); } - const hookResult = await callBeforeNewThreadHooks({ user, opts }); - if (hookResult.cancelled) return; - // If set in config, check that the user's account is old enough (time since they registered on Discord) // If the account is too new, don't start a new thread and optionally reply to them with a message if (config.requiredAccountAge && ! ignoreRequirements) { @@ -128,6 +125,10 @@ async function createNewThreadForUser(user, opts = {}) { } } + // Call any registered beforeNewThreadHooks + const hookResult = await callBeforeNewThreadHooks({ user, opts }); + if (hookResult.cancelled) return; + // Use the user's name+discrim for the thread channel's name // Channel names are particularly picky about what characters they allow, so we gotta do some clean-up let cleanName = transliterate.slugify(user.username); @@ -159,7 +160,10 @@ async function createNewThreadForUser(user, opts = {}) { // Attempt to create the inbox channel for this thread let createdChannel; try { - createdChannel = await utils.getInboxGuild().createChannel(channelName, null, 'New Modmail thread', newThreadCategoryId); + createdChannel = await utils.getInboxGuild().createChannel(channelName, DISOCRD_CHANNEL_TYPES.GUILD_TEXT, { + reason: 'New Modmail thread', + parentID: newThreadCategoryId, + }); } catch (err) { console.error(`Error creating modmail channel for ${user.username}#${user.discriminator}!`); throw err; From f7b8a312f9c82842307fe43712442fc526bbba6b Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 12 Aug 2020 23:24:17 +0300 Subject: [PATCH 026/300] Coerce arrays of strings to arrays automatically --- src/cfg.js | 2 +- src/data/cfg.schema.json | 42 +++++++++++++++------------------------- 2 files changed, 17 insertions(+), 27 deletions(-) diff --git a/src/cfg.js b/src/cfg.js index a31f36e..dc38d7f 100644 --- a/src/cfg.js +++ b/src/cfg.js @@ -171,7 +171,7 @@ for (const [key, value] of Object.entries(config)) { } // Validate config and assign defaults (if missing) -const ajv = new Ajv({ useDefaults: true }); +const ajv = new Ajv({ useDefaults: true, coerceTypes: "array" }); const validate = ajv.compile(schema); const configIsValid = validate(config); diff --git a/src/data/cfg.schema.json b/src/data/cfg.schema.json index de1a675..da29db6 100644 --- a/src/data/cfg.schema.json +++ b/src/data/cfg.schema.json @@ -3,24 +3,14 @@ "title": "ModmailConfig", "type": "object", "definitions": { - "stringOrArrayOfStrings": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "items": { - "type": "string" - } - }, - { - "type": "null" - } - ] + "stringArray": { + "type": "array", + "items": { + "type": "string" + } }, - "stringOrMultilineString": { - "$ref": "#/definitions/stringOrArrayOfStrings" + "multilineString": { + "$ref": "#/definitions/stringArray" } }, "properties": { @@ -28,7 +18,7 @@ "type": "string" }, "mainGuildId": { - "$ref": "#/definitions/stringOrArrayOfStrings" + "$ref": "#/definitions/stringArray" }, "mailGuildId": { "type": "string" @@ -54,11 +44,11 @@ "default": "Message me for help!" }, "responseMessage": { - "$ref": "#/definitions/stringOrMultilineString", + "$ref": "#/definitions/multilineString", "default": "Thank you for your message! Our mod team will reply to you here as soon as possible." }, "closeMessage": { - "$ref": "#/definitions/stringOrMultilineString" + "$ref": "#/definitions/multilineString" }, "allowUserClose": { "type": "boolean", @@ -77,11 +67,11 @@ "default": true }, "botMentionResponse": { - "$ref": "#/definitions/stringOrMultilineString" + "$ref": "#/definitions/multilineString" }, "inboxServerPermission": { - "$ref": "#/definitions/stringOrArrayOfStrings" + "$ref": "#/definitions/stringArray" }, "alwaysReply": { "type": "boolean", @@ -141,7 +131,7 @@ "default": false }, "greetingMessage": { - "$ref": "#/definitions/stringOrMultilineString" + "$ref": "#/definitions/multilineString" }, "greetingAttachment": { "type": "string" @@ -149,7 +139,7 @@ "guildGreetings": { "patternProperties": { "^\\d+$": { - "$ref": "#/definitions/stringOrMultilineString" + "$ref": "#/definitions/multilineString" } }, "default": {} @@ -160,7 +150,7 @@ "type": "number" }, "accountAgeDeniedMessage": { - "$ref": "#/definitions/stringOrMultilineString", + "$ref": "#/definitions/multilineString", "default": "Your Discord account is not old enough to contact modmail." }, @@ -169,7 +159,7 @@ "type": "number" }, "timeOnServerDeniedMessage": { - "$ref": "#/definitions/stringOrMultilineString", + "$ref": "#/definitions/multilineString", "default": "You haven't been a member of the server for long enough to contact modmail." }, From bd8dcc61294ace52eeea489c7382cef7b79ba10b Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 13 Aug 2020 00:03:01 +0300 Subject: [PATCH 027/300] Fixes and tweaks to new config validation --- src/cfg.js | 85 +++++++++++++++++++++++++----------- src/data/cfg.jsdoc.js | 18 ++++---- src/data/cfg.schema.json | 67 ++++++++++++++++++++-------- src/data/generateCfgJsdoc.js | 17 +++++++- 4 files changed, 132 insertions(+), 55 deletions(-) diff --git a/src/cfg.js b/src/cfg.js index dc38d7f..0d86065 100644 --- a/src/cfg.js +++ b/src/cfg.js @@ -102,16 +102,6 @@ for (const [key, value] of Object.entries(config)) { delete config[key]; } -// Cast boolean options (on, true, 1) (off, false, 0) -for (const [key, value] of Object.entries(config)) { - if (typeof value !== "string") continue; - if (["on", "true", "1"].includes(value)) { - config[key] = true; - } else if (["off", "false", "0"].includes(value)) { - config[key] = false; - } -} - if (! config['knex']) { config.knex = { client: 'sqlite', @@ -129,20 +119,6 @@ Object.assign(config['knex'], { } }); -// Make sure mainGuildId is internally always an array -if (! Array.isArray(config['mainGuildId'])) { - config['mainGuildId'] = [config['mainGuildId']]; -} - -// Make sure inboxServerPermission is always an array -if (! Array.isArray(config['inboxServerPermission'])) { - if (config['inboxServerPermission'] == null) { - config['inboxServerPermission'] = []; - } else { - config['inboxServerPermission'] = [config['inboxServerPermission']]; - } -} - // Move greetingMessage/greetingAttachment to the guildGreetings object internally // Or, in other words, if greetingMessage and/or greetingAttachment is set, it is applied for all servers that don't // already have something set up in guildGreetings. This retains backwards compatibility while allowing you to override @@ -163,15 +139,71 @@ if (config.newThreadCategoryId) { delete config.newThreadCategoryId; } -// Turn empty string options to null (i.e. "option=" without a value in config.ini) +// Delete empty string options (i.e. "option=" without a value in config.ini) for (const [key, value] of Object.entries(config)) { if (value === '') { - config[key] = null; + delete config[key]; } } // Validate config and assign defaults (if missing) const ajv = new Ajv({ useDefaults: true, coerceTypes: "array" }); + +// https://github.com/ajv-validator/ajv/issues/141#issuecomment-270692820 +const truthyValues = ["1", "true", "on"]; +const falsyValues = ["0", "false", "off"]; +ajv.addKeyword('coerceBoolean', { + compile(value) { + return (data, dataPath, parentData, parentKey) => { + if (! value) { + // Disabled -> no coercion + return true; + } + + // https://github.com/ajv-validator/ajv/issues/141#issuecomment-270777250 + // The "data" argument doesn't update within the same set of schemas inside "allOf", + // so we're referring to the original property instead. + // This also means we can't use { "type": "boolean" }, as it would test the un-updated data value. + const realData = parentData[parentKey]; + + if (typeof realData === "boolean") { + return true; + } + + if (truthyValues.includes(realData)) { + parentData[parentKey] = true; + } else if (falsyValues.includes(realData)) { + parentData[parentKey] = false; + } else { + return false; + } + + return true; + }; + }, +}); + +ajv.addKeyword('multilineString', { + compile(value) { + return (data, dataPath, parentData, parentKey) => { + if (! value) { + // Disabled -> no coercion + return true; + } + + const realData = parentData[parentKey]; + + if (typeof realData === "string") { + return true; + } + + parentData[parentKey] = realData.join("\n"); + + return true; + }; + }, +}); + const validate = ajv.compile(schema); const configIsValid = validate(config); @@ -186,5 +218,6 @@ if (! configIsValid) { } console.log("Configuration ok!"); +process.exit(0); module.exports = config; diff --git a/src/data/cfg.jsdoc.js b/src/data/cfg.jsdoc.js index a581fe1..e3de128 100644 --- a/src/data/cfg.jsdoc.js +++ b/src/data/cfg.jsdoc.js @@ -1,21 +1,21 @@ /** * @typedef {object} ModmailConfig * @property {string} [token] - * @property {*} [mainGuildId] + * @property {array} [mainGuildId] * @property {string} [mailGuildId] * @property {string} [logChannelId] * @property {string} [prefix="!"] * @property {string} [snippetPrefix="!!"] * @property {string} [snippetPrefixAnon="!!!"] * @property {string} [status="Message me for help!"] - * @property {*} [responseMessage="Thank you for your message! Our mod team will reply to you here as soon as possible."] - * @property {*} [closeMessage] + * @property {string} [responseMessage="Thank you for your message! Our mod team will reply to you here as soon as possible."] + * @property {string} [closeMessage] * @property {boolean} [allowUserClose=false] * @property {string} [newThreadCategoryId] * @property {string} [mentionRole="here"] * @property {boolean} [pingOnBotMention=true] - * @property {*} [botMentionResponse] - * @property {*} [inboxServerPermission] + * @property {string} [botMentionResponse] + * @property {array} [inboxServerPermission] * @property {boolean} [alwaysReply=false] * @property {boolean} [alwaysReplyAnon=false] * @property {boolean} [useNicknames=false] @@ -30,18 +30,18 @@ * @property {boolean} [allowStaffEdit=false] * @property {boolean} [allowStaffDelete=false] * @property {boolean} [enableGreeting=false] - * @property {*} [greetingMessage] + * @property {string} [greetingMessage] * @property {string} [greetingAttachment] * @property {*} [guildGreetings={}] * @property {number} [requiredAccountAge] Required account age to message Modmail, in hours - * @property {*} [accountAgeDeniedMessage="Your Discord account is not old enough to contact modmail."] + * @property {string} [accountAgeDeniedMessage="Your Discord account is not old enough to contact modmail."] * @property {number} [requiredTimeOnServer] Required time on server to message Modmail, in minutes - * @property {*} [timeOnServerDeniedMessage="You haven't been a member of the server for long enough to contact modmail."] + * @property {string} [timeOnServerDeniedMessage="You haven't been a member of the server for long enough to contact modmail."] * @property {boolean} [relaySmallAttachmentsAsAttachments=false] * @property {number} [smallAttachmentLimit=2097152] Max size of attachment to relay directly. Default is 2MB. * @property {string} [attachmentStorage="local"] * @property {string} [attachmentStorageChannelId] - * @property {*} [categoryAutomation] + * @property {*} [categoryAutomation={}] * @property {boolean} [updateNotifications=true] * @property {array} [plugins=[]] * @property {*} [commandAliases] diff --git a/src/data/cfg.schema.json b/src/data/cfg.schema.json index da29db6..70c1a70 100644 --- a/src/data/cfg.schema.json +++ b/src/data/cfg.schema.json @@ -10,7 +10,36 @@ } }, "multilineString": { - "$ref": "#/definitions/stringArray" + "allOf": [ + { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + { + "$comment": "See definition of multilineString in cfg.js", + "multilineString": true + } + ] + }, + "customBoolean": { + "allOf": [ + { + "type": ["boolean", "string"] + }, + { + "$comment": "See definition of coerceBoolean in cfg.js", + "coerceBoolean": true + } + ] } }, "properties": { @@ -51,7 +80,7 @@ "$ref": "#/definitions/multilineString" }, "allowUserClose": { - "type": "boolean", + "$ref": "#/definitions/customBoolean", "default": false }, @@ -63,7 +92,7 @@ "default": "here" }, "pingOnBotMention": { - "type": "boolean", + "$ref": "#/definitions/customBoolean", "default": true }, "botMentionResponse": { @@ -74,60 +103,60 @@ "$ref": "#/definitions/stringArray" }, "alwaysReply": { - "type": "boolean", + "$ref": "#/definitions/customBoolean", "default": false }, "alwaysReplyAnon": { - "type": "boolean", + "$ref": "#/definitions/customBoolean", "default": false }, "useNicknames": { - "type": "boolean", + "$ref": "#/definitions/customBoolean", "default": false }, "ignoreAccidentalThreads": { - "type": "boolean", + "$ref": "#/definitions/customBoolean", "default": false }, "threadTimestamps": { - "type": "boolean", + "$ref": "#/definitions/customBoolean", "default": false }, "allowMove": { - "type": "boolean", + "$ref": "#/definitions/customBoolean", "default": false }, "syncPermissionsOnMove": { - "type": "boolean", + "$ref": "#/definitions/customBoolean", "default": true }, "typingProxy": { - "type": "boolean", + "$ref": "#/definitions/customBoolean", "default": false }, "typingProxyReverse": { - "type": "boolean", + "$ref": "#/definitions/customBoolean", "default": false }, "mentionUserInThreadHeader": { - "type": "boolean", + "$ref": "#/definitions/customBoolean", "default": false }, "rolesInThreadHeader": { - "type": "boolean", + "$ref": "#/definitions/customBoolean", "default": false }, "allowStaffEdit": { - "type": "boolean", + "$ref": "#/definitions/customBoolean", "default": false }, "allowStaffDelete": { - "type": "boolean", + "$ref": "#/definitions/customBoolean", "default": false }, "enableGreeting": { - "type": "boolean", + "$ref": "#/definitions/customBoolean", "default": false }, "greetingMessage": { @@ -164,7 +193,7 @@ }, "relaySmallAttachmentsAsAttachments": { - "type": "boolean", + "$ref": "#/definitions/customBoolean", "default": false }, "smallAttachmentLimit": { @@ -194,7 +223,7 @@ }, "updateNotifications": { - "type": "boolean", + "$ref": "#/definitions/customBoolean", "default": true }, "plugins": { diff --git a/src/data/generateCfgJsdoc.js b/src/data/generateCfgJsdoc.js index 219615c..096ddb8 100644 --- a/src/data/generateCfgJsdoc.js +++ b/src/data/generateCfgJsdoc.js @@ -4,5 +4,20 @@ const toJsdoc = require('json-schema-to-jsdoc'); const schema = require('./cfg.schema.json'); const target = path.join(__dirname, 'cfg.jsdoc.js'); -const result = toJsdoc(schema); +// Fix up some custom types for the JSDoc conversion +const schemaCopy = JSON.parse(JSON.stringify(schema)); +for (const propertyDef of Object.values(schemaCopy.properties)) { + if (propertyDef.$ref === "#/definitions/stringArray") { + propertyDef.type = "array"; + delete propertyDef.$ref; + } else if (propertyDef.$ref === "#/definitions/customBoolean") { + propertyDef.type = "boolean"; + delete propertyDef.$ref; + } else if (propertyDef.$ref === "#/definitions/multilineString") { + propertyDef.type = "string"; + delete propertyDef.$ref; + } +} + +const result = toJsdoc(schemaCopy); fs.writeFileSync(target, result, { encoding: 'utf8' }); From 782efd217f68f44508383c61c2e8543d1f83ea1c Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 13 Aug 2020 00:04:48 +0300 Subject: [PATCH 028/300] Enforce double quotes in .eslintrc --- .eslintrc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.eslintrc b/.eslintrc index d33e1cf..6698909 100644 --- a/.eslintrc +++ b/.eslintrc @@ -19,6 +19,7 @@ "!": true, "!!": true } - }] + }], + "quotes": ["error", "double"] } } From b628ac1bfabbbd39473d4c407c92d813dcbfcca9 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 13 Aug 2020 00:08:04 +0300 Subject: [PATCH 029/300] Run eslint --fix in a pre-commit hook --- package-lock.json | 888 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 11 + 2 files changed, 899 insertions(+) diff --git a/package-lock.json b/package-lock.json index 6e9477c..cf953bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,6 +37,18 @@ "defer-to-connect": "^1.0.1" } }, + "@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true + }, + "@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", + "dev": true + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -54,6 +66,16 @@ "integrity": "sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw==", "dev": true }, + "aggregate-error": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz", + "integrity": "sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA==", + "dev": true, + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + } + }, "ajv": { "version": "6.12.3", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.3.tgz", @@ -81,6 +103,12 @@ "string-width": "^2.0.0" } }, + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + }, "ansi-escapes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.0.tgz", @@ -539,6 +567,12 @@ } } }, + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true + }, "cli-boxes": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", @@ -554,6 +588,92 @@ "restore-cursor": "^3.1.0" } }, + "cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "requires": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + } + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + } + } + }, "cli-width": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", @@ -659,6 +779,12 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-4.0.1.tgz", "integrity": "sha512-IPF4ouhCP+qdlcmCedhxX4xiGBPyigb8v5NeUp+0LyhwLgxMqyp3S0vl7TAPfS/hiP7FC3caI/PB9lTmP8r1NA==" }, + "compare-versions": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz", + "integrity": "sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==", + "dev": true + }, "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", @@ -698,6 +824,19 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, + "cosmiconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", + "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "dev": true, + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.7.2" + } + }, "create-error-class": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", @@ -759,6 +898,12 @@ "mimic-response": "^1.0.0" } }, + "dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=", + "dev": true + }, "deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -896,6 +1041,15 @@ "once": "^1.4.0" } }, + "enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "requires": { + "ansi-colors": "^4.1.1" + } + }, "eris": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/eris/-/eris-0.11.1.tgz", @@ -906,6 +1060,15 @@ "ws": "^7.1.2" } }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -1322,6 +1485,15 @@ "locate-path": "^3.0.0" } }, + "find-versions": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-3.2.0.tgz", + "integrity": "sha512-P8WRou2S+oe222TOCHitLy8zj+SIsVJh52VP4lvXkaFVnOFFdoWv1H1Jjvel1aI6NCFOAaeAVm8qrI0odiLcww==", + "dev": true, + "requires": { + "semver-regex": "^2.0.0" + } + }, "findup-sync": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", @@ -1480,6 +1652,12 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, + "get-own-enumerable-property-symbols": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", + "dev": true + }, "get-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", @@ -1681,11 +1859,93 @@ "sshpk": "^1.7.0" } }, + "human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true + }, "humanize-duration": { "version": "3.12.1", "resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.12.1.tgz", "integrity": "sha512-Eu68Xnq5C38391em1zfVy8tiapQrOvTNTlWpax9smHMlEEUcudXrdMfXMoMRyZx4uODowYgi1AYiMzUXEbG+sA==" }, + "husky": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/husky/-/husky-4.2.5.tgz", + "integrity": "sha512-SYZ95AjKcX7goYVZtVZF2i6XiZcHknw50iXvY7b0MiGoj5RwdgRQNEHdb+gPDPCXKlzwrybjFjkL6FOj8uRhZQ==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "ci-info": "^2.0.0", + "compare-versions": "^3.6.0", + "cosmiconfig": "^6.0.0", + "find-versions": "^3.2.0", + "opencollective-postinstall": "^2.0.2", + "pkg-dir": "^4.2.0", + "please-upgrade-node": "^3.2.0", + "slash": "^3.0.0", + "which-pm-runs": "^1.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -1736,6 +1996,12 @@ "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", "dev": true }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -1871,6 +2137,12 @@ } } }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, "is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -2032,6 +2304,12 @@ "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=", "dev": true }, + "is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk=", + "dev": true + }, "is-relative": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", @@ -2119,6 +2397,12 @@ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=" }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, "json-pointer": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/json-pointer/-/json-pointer-0.6.0.tgz", @@ -2286,6 +2570,304 @@ "resolve": "^1.1.7" } }, + "lines-and-columns": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", + "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", + "dev": true + }, + "lint-staged": { + "version": "10.2.11", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-10.2.11.tgz", + "integrity": "sha512-LRRrSogzbixYaZItE2APaS4l2eJMjjf5MbclRZpLJtcQJShcvUzKXsNeZgsLIZ0H0+fg2tL4B59fU9wHIHtFIA==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "cli-truncate": "2.1.0", + "commander": "^5.1.0", + "cosmiconfig": "^6.0.0", + "debug": "^4.1.1", + "dedent": "^0.7.0", + "enquirer": "^2.3.5", + "execa": "^4.0.1", + "listr2": "^2.1.0", + "log-symbols": "^4.0.0", + "micromatch": "^4.0.2", + "normalize-path": "^3.0.0", + "please-upgrade-node": "^3.2.0", + "string-argv": "0.3.1", + "stringify-object": "^3.3.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "execa": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.0.3.tgz", + "integrity": "sha512-WFDXGHckXPWZX19t1kCsXzOpqX9LWYNqn4C+HqZlk/V0imTkzJZqf87ZBhvpHaftERYknpk0fjSylnXVlVgI0A==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "get-stream": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", + "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "dev": true + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "listr2": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-2.5.1.tgz", + "integrity": "sha512-qkNRW70SwfwWLD/eiaTf2tfgWT/ZvjmMsnEFJOCzac0cjcc8rYHDBr1eQhRxopj6lZO7Oa5sS/pZzS6q+BsX+w==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "cli-truncate": "^2.1.0", + "figures": "^3.2.0", + "indent-string": "^4.0.0", + "log-update": "^4.0.0", + "p-map": "^4.0.0", + "rxjs": "^6.6.2", + "through": "^2.3.8" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "rxjs": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.2.tgz", + "integrity": "sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "locate-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", @@ -2301,6 +2883,166 @@ "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", "dev": true }, + "log-symbols": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz", + "integrity": "sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==", + "dev": true, + "requires": { + "chalk": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "log-update": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", + "dev": true, + "requires": { + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + } + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + } + } + }, "lowercase-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", @@ -2346,6 +3088,12 @@ "object-visit": "^1.0.0" } }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, "micromatch": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", @@ -2841,6 +3589,12 @@ } } }, + "opencollective-postinstall": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz", + "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==", + "dev": true + }, "optionator": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", @@ -2907,6 +3661,15 @@ "p-limit": "^2.0.0" } }, + "p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } + }, "p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -2979,6 +3742,18 @@ "path-root": "^0.1.1" } }, + "parse-json": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.1.tgz", + "integrity": "sha512-ztoZ4/DYeXQq4E21v169sC8qWINGpcosGv9XhTDvg9/hWvx/zrFkc9BiWxR58OJLHGk28j5BL0SDLeV2WmFZlQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1", + "lines-and-columns": "^1.1.6" + } + }, "parse-passwd": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", @@ -3029,6 +3804,12 @@ "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=" }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -3052,6 +3833,60 @@ "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", "dev": true }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + } + } + }, + "please-upgrade-node": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", + "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==", + "dev": true, + "requires": { + "semver-compare": "^1.0.0" + } + }, "posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", @@ -3379,6 +4214,12 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==" }, + "semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=", + "dev": true + }, "semver-diff": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", @@ -3388,6 +4229,12 @@ "semver": "^5.0.3" } }, + "semver-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-2.0.0.tgz", + "integrity": "sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw==", + "dev": true + }, "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -3434,6 +4281,12 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, "slice-ansi": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", @@ -3653,6 +4506,12 @@ } } }, + "string-argv": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", + "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==", + "dev": true + }, "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", @@ -3690,6 +4549,17 @@ "safe-buffer": "~5.1.0" } }, + "stringify-object": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", + "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", + "dev": true, + "requires": { + "get-own-enumerable-property-symbols": "^3.0.0", + "is-obj": "^1.0.1", + "is-regexp": "^1.0.0" + } + }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", @@ -3704,6 +4574,12 @@ "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", "dev": true }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true + }, "strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", @@ -4147,6 +5023,12 @@ "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" }, + "which-pm-runs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz", + "integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=", + "dev": true + }, "wide-align": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", @@ -4265,6 +5147,12 @@ "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", "dev": true }, + "yaml": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz", + "integrity": "sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==", + "dev": true + }, "yargs": { "version": "14.2.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-14.2.2.tgz", diff --git a/package.json b/package.json index 07a81ba..441d6da 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "watch": "nodemon -w src src/index.js", "test": "echo \"Error: no test specified\" && exit 1", "lint": "eslint ./src", + "lint-fix": "eslint --fix ./src", "generate-config-jsdoc": "node src/data/generateCfgJsdoc.js" }, "repository": { @@ -35,9 +36,19 @@ }, "devDependencies": { "eslint": "^6.7.2", + "husky": "^4.2.5", + "lint-staged": "^10.2.11", "nodemon": "^2.0.1" }, "engines": { "node": ">=10.0.0 <14.0.0" + }, + "husky": { + "hooks": { + "pre-commit": "lint-staged" + } + }, + "lint-staged": { + "*.js": "eslint --fix" } } From 86a060410f95f0087c132cb1f5ff2cf549c56bc5 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 13 Aug 2020 00:08:37 +0300 Subject: [PATCH 030/300] Apply code style from .eslintrc --- src/bot.js | 4 +- src/cfg.js | 76 +++++++++++----------- src/commands.js | 14 ++-- src/data/Thread.js | 122 +++++++++++++++++------------------ src/data/attachments.js | 38 +++++------ src/data/blocked.js | 30 ++++----- src/data/constants.js | 72 ++++++++++----------- src/data/generateCfgJsdoc.js | 12 ++-- src/data/snippets.js | 20 +++--- src/data/threads.js | 108 +++++++++++++++---------------- src/data/updates.js | 54 ++++++++-------- src/formatters.js | 26 ++++---- src/hooks/beforeNewThread.js | 2 +- src/index.js | 68 +++++++++---------- src/knex.js | 4 +- src/legacy/jsonDb.js | 10 +-- src/legacy/legacyMigrator.js | 98 ++++++++++++++-------------- src/main.js | 90 +++++++++++++------------- src/modules/alert.js | 6 +- src/modules/block.js | 26 ++++---- src/modules/close.js | 34 +++++----- src/modules/greeting.js | 12 ++-- src/modules/id.js | 4 +- src/modules/logs.js | 32 ++++----- src/modules/move.js | 10 +-- src/modules/newthread.js | 6 +- src/modules/reply.js | 38 +++++------ src/modules/snippets.js | 32 ++++----- src/modules/suspend.js | 30 ++++----- src/modules/typingProxy.js | 2 +- src/modules/version.js | 22 +++---- src/modules/webserver.js | 46 ++++++------- src/plugins.js | 6 +- src/utils.js | 58 ++++++++--------- 34 files changed, 606 insertions(+), 606 deletions(-) diff --git a/src/bot.js b/src/bot.js index 41a37d9..1f52c16 100644 --- a/src/bot.js +++ b/src/bot.js @@ -1,5 +1,5 @@ -const Eris = require('eris'); -const config = require('./cfg'); +const Eris = require("eris"); +const config = require("./cfg"); const bot = new Eris.Client(config.token, { getAllUsers: true, diff --git a/src/cfg.js b/src/cfg.js index 0d86065..dc18bd0 100644 --- a/src/cfg.js +++ b/src/cfg.js @@ -1,29 +1,29 @@ -const fs = require('fs'); -const path = require('path'); -const Ajv = require('ajv'); -const schema = require('./data/cfg.schema.json'); +const fs = require("fs"); +const path = require("path"); +const Ajv = require("ajv"); +const schema = require("./data/cfg.schema.json"); /** @type {ModmailConfig} */ let config = {}; // Config files to search for, in priority order const configFiles = [ - 'config.ini', - 'config.json', - 'config.json5', - 'config.js', + "config.ini", + "config.json", + "config.json5", + "config.js", // Possible config files when file extensions are hidden - 'config.ini.ini', - 'config.ini.txt', - 'config.json.json', - 'config.json.txt', + "config.ini.ini", + "config.ini.txt", + "config.json.json", + "config.json.txt", ]; let foundConfigFile; for (const configFile of configFiles) { try { - fs.accessSync(__dirname + '/../' + configFile); + fs.accessSync(__dirname + "/../" + configFile); foundConfigFile = configFile; break; } catch (e) {} @@ -33,14 +33,14 @@ for (const configFile of configFiles) { if (foundConfigFile) { console.log(`Loading configuration from ${foundConfigFile}...`); try { - if (foundConfigFile.endsWith('.js')) { + if (foundConfigFile.endsWith(".js")) { config = require(`../${foundConfigFile}`); } else { - const raw = fs.readFileSync(__dirname + '/../' + foundConfigFile, {encoding: "utf8"}); - if (foundConfigFile.endsWith('.ini') || foundConfigFile.endsWith('.ini.txt')) { - config = require('ini').decode(raw); + const raw = fs.readFileSync(__dirname + "/../" + foundConfigFile, {encoding: "utf8"}); + if (foundConfigFile.endsWith(".ini") || foundConfigFile.endsWith(".ini.txt")) { + config = require("ini").decode(raw); } else { - config = require('json5').parse(raw); + config = require("json5").parse(raw); } } } catch (e) { @@ -49,11 +49,11 @@ if (foundConfigFile) { } // Set dynamic default values which can't be set in the schema directly -config.dbDir = path.join(__dirname, '..', 'db'); -config.logDir = path.join(__dirname, '..', 'logs'); // Only used for migrating data from older Modmail versions +config.dbDir = path.join(__dirname, "..", "db"); +config.logDir = path.join(__dirname, "..", "logs"); // Only used for migrating data from older Modmail versions // Load config values from environment variables -const envKeyPrefix = 'MM_'; +const envKeyPrefix = "MM_"; let loadedEnvValues = 0; for (const [key, value] of Object.entries(process.env)) { @@ -64,10 +64,10 @@ for (const [key, value] of Object.entries(process.env)) { const configKey = key.slice(envKeyPrefix.length) .toLowerCase() .replace(/([a-z])_([a-z])/g, (m, m1, m2) => `${m1}${m2.toUpperCase()}`) - .replace('__', '.'); + .replace("__", "."); - config[configKey] = value.includes('||') - ? value.split('||') + config[configKey] = value.includes("||") + ? value.split("||") : value; loadedEnvValues++; @@ -80,15 +80,15 @@ if (process.env.PORT && ! process.env.MM_PORT) { } if (loadedEnvValues > 0) { - console.log(`Loaded ${loadedEnvValues} ${loadedEnvValues === 1 ? 'value' : 'values'} from environment variables`); + console.log(`Loaded ${loadedEnvValues} ${loadedEnvValues === 1 ? "value" : "values"} from environment variables`); } // Convert config keys with periods to objects // E.g. commandAliases.mv -> commandAliases: { mv: ... } for (const [key, value] of Object.entries(config)) { - if (! key.includes('.')) continue; + if (! key.includes(".")) continue; - const keys = key.split('.'); + const keys = key.split("."); let cursor = config; for (let i = 0; i < keys.length; i++) { if (i === keys.length - 1) { @@ -102,20 +102,20 @@ for (const [key, value] of Object.entries(config)) { delete config[key]; } -if (! config['knex']) { +if (! config["knex"]) { config.knex = { - client: 'sqlite', + client: "sqlite", connection: { - filename: path.join(config.dbDir, 'data.sqlite') + filename: path.join(config.dbDir, "data.sqlite") }, useNullAsDefault: true }; } // Make sure migration settings are always present in knex config -Object.assign(config['knex'], { +Object.assign(config["knex"], { migrations: { - directory: path.join(config.dbDir, 'migrations') + directory: path.join(config.dbDir, "migrations") } }); @@ -141,7 +141,7 @@ if (config.newThreadCategoryId) { // Delete empty string options (i.e. "option=" without a value in config.ini) for (const [key, value] of Object.entries(config)) { - if (value === '') { + if (value === "") { delete config[key]; } } @@ -152,7 +152,7 @@ const ajv = new Ajv({ useDefaults: true, coerceTypes: "array" }); // https://github.com/ajv-validator/ajv/issues/141#issuecomment-270692820 const truthyValues = ["1", "true", "on"]; const falsyValues = ["0", "false", "off"]; -ajv.addKeyword('coerceBoolean', { +ajv.addKeyword("coerceBoolean", { compile(value) { return (data, dataPath, parentData, parentKey) => { if (! value) { @@ -183,7 +183,7 @@ ajv.addKeyword('coerceBoolean', { }, }); -ajv.addKeyword('multilineString', { +ajv.addKeyword("multilineString", { compile(value) { return (data, dataPath, parentData, parentKey) => { if (! value) { @@ -208,12 +208,12 @@ const validate = ajv.compile(schema); const configIsValid = validate(config); if (! configIsValid) { - console.error('Issues with configuration options:'); + console.error("Issues with configuration options:"); for (const error of validate.errors) { console.error(`The "${error.dataPath.slice(1)}" option ${error.message}`); } - console.error(''); - console.error('Please restart the bot after fixing the issues mentioned above.'); + console.error(""); + console.error("Please restart the bot after fixing the issues mentioned above."); process.exit(1); } diff --git a/src/commands.js b/src/commands.js index 3135ee9..cdc95de 100644 --- a/src/commands.js +++ b/src/commands.js @@ -1,9 +1,9 @@ -const { CommandManager, defaultParameterTypes, TypeConversionError, IParameter, ICommandConfig } = require('knub-command-manager'); -const Eris = require('eris'); -const config = require('./cfg'); -const utils = require('./utils'); -const threads = require('./data/threads'); -const Thread = require('./data/Thread'); +const { CommandManager, defaultParameterTypes, TypeConversionError, IParameter, ICommandConfig } = require("knub-command-manager"); +const Eris = require("eris"); +const config = require("./cfg"); +const utils = require("./utils"); +const threads = require("./data/threads"); +const Thread = require("./data/Thread"); module.exports = { createCommandManager(bot) { @@ -27,7 +27,7 @@ module.exports = { const handlers = {}; const aliasMap = new Map(); - bot.on('messageCreate', async msg => { + bot.on("messageCreate", async msg => { if (msg.author.bot) return; if (msg.author.id === bot.user.id) return; if (! msg.content) return; diff --git a/src/data/Thread.js b/src/data/Thread.js index 465d25f..747c449 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -1,16 +1,16 @@ -const moment = require('moment'); -const Eris = require('eris'); +const moment = require("moment"); +const Eris = require("eris"); -const bot = require('../bot'); -const knex = require('../knex'); -const utils = require('../utils'); -const config = require('../cfg'); -const attachments = require('./attachments'); -const { formatters } = require('../formatters'); +const bot = require("../bot"); +const knex = require("../knex"); +const utils = require("../utils"); +const config = require("../cfg"); +const attachments = require("./attachments"); +const { formatters } = require("../formatters"); -const ThreadMessage = require('./ThreadMessage'); +const ThreadMessage = require("./ThreadMessage"); -const {THREAD_MESSAGE_TYPE, THREAD_STATUS} = require('./constants'); +const {THREAD_MESSAGE_TYPE, THREAD_STATUS} = require("./constants"); /** * @property {String} id @@ -41,12 +41,12 @@ class Thread { // Try to open a DM channel with the user const dmChannel = await this.getDMChannel(); if (! dmChannel) { - throw new Error('Could not open DMs with the user. They may have blocked the bot or set their privacy settings higher.'); + throw new Error("Could not open DMs with the user. They may have blocked the bot or set their privacy settings higher."); } let firstMessage; - if (typeof content === 'string') { + if (typeof content === "string") { // Content is a string, chunk it and send it as individual messages. // Files (attachments) are only sent with the last message. const chunks = utils.chunk(content, 2000); @@ -79,7 +79,7 @@ class Thread { try { let firstMessage; - if (typeof content === 'string') { + if (typeof content === "string") { // Content is a string, chunk it and send it as individual messages. // Files (attachments) are only sent with the last message. const chunks = utils.chunk(content, 2000); @@ -120,16 +120,16 @@ class Thread { } const dmChannel = await this.getDMChannel(); - const insertedIds = await knex('thread_messages').insert({ + const insertedIds = await knex("thread_messages").insert({ thread_id: this.id, - created_at: moment.utc().format('YYYY-MM-DD HH:mm:ss'), + created_at: moment.utc().format("YYYY-MM-DD HH:mm:ss"), is_anonymous: 0, dm_channel_id: dmChannel.id, ...data }); - const threadMessage = await knex('thread_messages') - .where('id', insertedIds[0]) + const threadMessage = await knex("thread_messages") + .where("id", insertedIds[0]) .select(); return new ThreadMessage(threadMessage[0]); @@ -142,8 +142,8 @@ class Thread { * @private */ async _updateThreadMessage(id, data) { - await knex('thread_messages') - .where('id', id) + await knex("thread_messages") + .where("id", id) .update(data); } @@ -153,8 +153,8 @@ class Thread { * @private */ async _deleteThreadMessage(id) { - await knex('thread_messages') - .where('id', id) + await knex("thread_messages") + .where("id", id) .delete(); } @@ -163,8 +163,8 @@ class Thread { * @private */ _lastMessageNumberInThreadSQL() { - return knex('thread_messages AS tm_msg_num_ref') - .select(knex.raw('MAX(tm_msg_num_ref.message_number)')) + return knex("thread_messages AS tm_msg_num_ref") + .select(knex.raw("MAX(tm_msg_num_ref.message_number)")) .whereRaw(`tm_msg_num_ref.thread_id = '${this.id}'`) .toSQL() .sql; @@ -212,7 +212,7 @@ class Thread { message_type: THREAD_MESSAGE_TYPE.TO_USER, user_id: moderator.id, user_name: fullModeratorName, - body: '', + body: "", is_anonymous: (isAnonymous ? 1 : 0), dm_message_id: dmMessage.id }); @@ -227,7 +227,7 @@ class Thread { // Interrupt scheduled closing, if in progress if (this.scheduled_close_at) { await this.cancelScheduledClose(); - await this.postSystemMessage(`Cancelling scheduled closing of this thread due to new reply`); + await this.postSystemMessage("Cancelling scheduled closing of this thread due to new reply"); } return true; @@ -306,11 +306,11 @@ class Thread { async postSystemMessage(content, file = null, opts = {}) { const msg = await this._postToThreadChannel(content, file); if (msg && opts.saveToLog !== false) { - const finalLogBody = opts.logBody || msg.content || ''; + const finalLogBody = opts.logBody || msg.content || ""; await this._addThreadMessageToDB({ message_type: THREAD_MESSAGE_TYPE.SYSTEM, user_id: null, - user_name: '', + user_name: "", body: finalLogBody, is_anonymous: 0, inbox_message_id: msg.id, @@ -329,11 +329,11 @@ class Thread { async sendSystemMessageToUser(content, file = null, opts = {}) { const msg = await this._sendDMToUser(content, file); if (opts.saveToLog !== false) { - const finalLogBody = opts.logBody || msg.content || ''; + const finalLogBody = opts.logBody || msg.content || ""; await this._addThreadMessageToDB({ message_type: THREAD_MESSAGE_TYPE.SYSTEM_TO_USER, user_id: null, - user_name: '', + user_name: "", body: finalLogBody, is_anonymous: 0, dm_message_id: msg.id, @@ -381,9 +381,9 @@ class Thread { * @returns {Promise} */ async updateChatMessageInLogs(msg) { - await knex('thread_messages') - .where('thread_id', this.id) - .where('dm_message_id', msg.id) + await knex("thread_messages") + .where("thread_id", this.id) + .where("dm_message_id", msg.id) .update({ body: msg.content }); @@ -394,9 +394,9 @@ class Thread { * @returns {Promise} */ async deleteChatMessageFromLogs(messageId) { - await knex('thread_messages') - .where('thread_id', this.id) - .where('dm_message_id', messageId) + await knex("thread_messages") + .where("thread_id", this.id) + .where("dm_message_id", messageId) .delete(); } @@ -404,10 +404,10 @@ class Thread { * @returns {Promise} */ async getThreadMessages() { - const threadMessages = await knex('thread_messages') - .where('thread_id', this.id) - .orderBy('created_at', 'ASC') - .orderBy('id', 'ASC') + const threadMessages = await knex("thread_messages") + .where("thread_id", this.id) + .orderBy("created_at", "ASC") + .orderBy("id", "ASC") .select(); return threadMessages.map(row => new ThreadMessage(row)); @@ -418,9 +418,9 @@ class Thread { * @returns {Promise} */ async findThreadMessageByMessageNumber(messageNumber) { - const data = await knex('thread_messages') - .where('thread_id', this.id) - .where('message_number', messageNumber) + const data = await knex("thread_messages") + .where("thread_id", this.id) + .where("message_number", messageNumber) .select(); return data ? new ThreadMessage(data) : null; @@ -434,15 +434,15 @@ class Thread { console.log(`Closing thread ${this.id}`); if (silent) { - await this.postSystemMessage('Closing thread silently...'); + await this.postSystemMessage("Closing thread silently..."); } else { - await this.postSystemMessage('Closing thread...'); + await this.postSystemMessage("Closing thread..."); } } // Update DB status - await knex('threads') - .where('id', this.id) + await knex("threads") + .where("id", this.id) .update({ status: THREAD_STATUS.CLOSED }); @@ -451,7 +451,7 @@ class Thread { const channel = bot.getChannel(this.channel_id); if (channel) { console.log(`Deleting channel ${this.channel_id}`); - await channel.delete('Thread closed'); + await channel.delete("Thread closed"); } } @@ -462,8 +462,8 @@ class Thread { * @returns {Promise} */ async scheduleClose(time, user, silent) { - await knex('threads') - .where('id', this.id) + await knex("threads") + .where("id", this.id) .update({ scheduled_close_at: time, scheduled_close_id: user.id, @@ -476,8 +476,8 @@ class Thread { * @returns {Promise} */ async cancelScheduledClose() { - await knex('threads') - .where('id', this.id) + await knex("threads") + .where("id", this.id) .update({ scheduled_close_at: null, scheduled_close_id: null, @@ -490,8 +490,8 @@ class Thread { * @returns {Promise} */ async suspend() { - await knex('threads') - .where('id', this.id) + await knex("threads") + .where("id", this.id) .update({ status: THREAD_STATUS.SUSPENDED, scheduled_suspend_at: null, @@ -504,8 +504,8 @@ class Thread { * @returns {Promise} */ async unsuspend() { - await knex('threads') - .where('id', this.id) + await knex("threads") + .where("id", this.id) .update({ status: THREAD_STATUS.OPEN }); @@ -517,8 +517,8 @@ class Thread { * @returns {Promise} */ async scheduleSuspend(time, user) { - await knex('threads') - .where('id', this.id) + await knex("threads") + .where("id", this.id) .update({ scheduled_suspend_at: time, scheduled_suspend_id: user.id, @@ -530,8 +530,8 @@ class Thread { * @returns {Promise} */ async cancelScheduledSuspend() { - await knex('threads') - .where('id', this.id) + await knex("threads") + .where("id", this.id) .update({ scheduled_suspend_at: null, scheduled_suspend_id: null, @@ -544,8 +544,8 @@ class Thread { * @returns {Promise} */ async setAlert(userId) { - await knex('threads') - .where('id', this.id) + await knex("threads") + .where("id", this.id) .update({ alert_id: userId }); diff --git a/src/data/attachments.js b/src/data/attachments.js index cf8cc6d..3d3ae16 100644 --- a/src/data/attachments.js +++ b/src/data/attachments.js @@ -1,13 +1,13 @@ -const Eris = require('eris'); -const fs = require('fs'); -const https = require('https'); -const {promisify} = require('util'); -const tmp = require('tmp'); -const config = require('../cfg'); -const utils = require('../utils'); -const mv = promisify(require('mv')); +const Eris = require("eris"); +const fs = require("fs"); +const https = require("https"); +const {promisify} = require("util"); +const tmp = require("tmp"); +const config = require("../cfg"); +const utils = require("../utils"); +const mv = promisify(require("mv")); -const getUtils = () => require('../utils'); +const getUtils = () => require("../utils"); const access = promisify(fs.access); const readFile = promisify(fs.readFile); @@ -20,7 +20,7 @@ const attachmentStorageTypes = {}; function getErrorResult(msg = null) { return { - url: `Attachment could not be saved${msg ? ': ' + msg : ''}`, + url: `Attachment could not be saved${msg ? ": " + msg : ""}`, failed: true }; } @@ -61,8 +61,8 @@ async function saveLocalAttachment(attachment) { function downloadAttachment(attachment, tries = 0) { return new Promise((resolve, reject) => { if (tries > 3) { - console.error('Attachment download failed after 3 tries:', attachment); - reject('Attachment download failed after 3 tries'); + console.error("Attachment download failed after 3 tries:", attachment); + reject("Attachment download failed after 3 tries"); return; } @@ -71,16 +71,16 @@ function downloadAttachment(attachment, tries = 0) { https.get(attachment.url, (res) => { res.pipe(writeStream); - writeStream.on('finish', () => { + writeStream.on("finish", () => { writeStream.end(); resolve({ path: filepath, cleanup: cleanupCallback }); }); - }).on('error', (err) => { + }).on("error", (err) => { fs.unlink(filepath); - console.error('Error downloading attachment, retrying'); + console.error("Error downloading attachment, retrying"); resolve(downloadAttachment(attachment, tries++)); }); }); @@ -103,7 +103,7 @@ function getLocalAttachmentPath(attachmentId) { * @returns {Promise} */ function getLocalAttachmentUrl(attachmentId, desiredName = null) { - if (desiredName == null) desiredName = 'file.bin'; + if (desiredName == null) desiredName = "file.bin"; return getUtils().getSelfUrl(`attachments/${attachmentId}/${desiredName}`); } @@ -113,19 +113,19 @@ function getLocalAttachmentUrl(attachmentId, desiredName = null) { */ async function saveDiscordAttachment(attachment) { if (attachment.size > 1024 * 1024 * 8) { - return getErrorResult('attachment too large (max 8MB)'); + return getErrorResult("attachment too large (max 8MB)"); } const attachmentChannelId = config.attachmentStorageChannelId; const inboxGuild = utils.getInboxGuild(); if (! inboxGuild.channels.has(attachmentChannelId)) { - throw new Error('Attachment storage channel not found!'); + throw new Error("Attachment storage channel not found!"); } const attachmentChannel = inboxGuild.channels.get(attachmentChannelId); if (! (attachmentChannel instanceof Eris.TextChannel)) { - throw new Error('Attachment storage channel must be a text channel!'); + throw new Error("Attachment storage channel must be a text channel!"); } const file = await attachmentToDiscordFileObject(attachment); diff --git a/src/data/blocked.js b/src/data/blocked.js index 81c1214..533baf1 100644 --- a/src/data/blocked.js +++ b/src/data/blocked.js @@ -1,13 +1,13 @@ -const moment = require('moment'); -const knex = require('../knex'); +const moment = require("moment"); +const knex = require("../knex"); /** * @param {String} userId * @returns {Promise<{ isBlocked: boolean, expiresAt: string }>} */ async function getBlockStatus(userId) { - const row = await knex('blocked_users') - .where('user_id', userId) + const row = await knex("blocked_users") + .where("user_id", userId) .first(); return { @@ -32,15 +32,15 @@ async function isBlocked(userId) { * @param {String} blockedBy * @returns {Promise} */ -async function block(userId, userName = '', blockedBy = null, expiresAt = null) { +async function block(userId, userName = "", blockedBy = null, expiresAt = null) { if (await isBlocked(userId)) return; - return knex('blocked_users') + return knex("blocked_users") .insert({ user_id: userId, user_name: userName, blocked_by: blockedBy, - blocked_at: moment.utc().format('YYYY-MM-DD HH:mm:ss'), + blocked_at: moment.utc().format("YYYY-MM-DD HH:mm:ss"), expires_at: expiresAt }); } @@ -51,8 +51,8 @@ async function block(userId, userName = '', blockedBy = null, expiresAt = null) * @returns {Promise} */ async function unblock(userId) { - return knex('blocked_users') - .where('user_id', userId) + return knex("blocked_users") + .where("user_id", userId) .delete(); } @@ -63,8 +63,8 @@ async function unblock(userId) { * @returns {Promise} */ async function updateExpiryTime(userId, expiresAt) { - return knex('blocked_users') - .where('user_id', userId) + return knex("blocked_users") + .where("user_id", userId) .update({ expires_at: expiresAt }); @@ -74,11 +74,11 @@ async function updateExpiryTime(userId, expiresAt) { * @returns {String[]} */ async function getExpiredBlocks() { - const now = moment.utc().format('YYYY-MM-DD HH:mm:ss'); + const now = moment.utc().format("YYYY-MM-DD HH:mm:ss"); - const blocks = await knex('blocked_users') - .whereNotNull('expires_at') - .where('expires_at', '<=', now) + const blocks = await knex("blocked_users") + .whereNotNull("expires_at") + .where("expires_at", "<=", now) .select(); return blocks.map(block => block.user_id); diff --git a/src/data/constants.js b/src/data/constants.js index 40388db..067cb06 100644 --- a/src/data/constants.js +++ b/src/data/constants.js @@ -27,41 +27,41 @@ module.exports = { }, ACCIDENTAL_THREAD_MESSAGES: [ - 'ok', - 'okay', - 'thanks', - 'ty', - 'k', - 'kk', - 'thank you', - 'thanx', - 'thnx', - 'thx', - 'tnx', - 'ok thank you', - 'ok thanks', - 'ok ty', - 'ok thanx', - 'ok thnx', - 'ok thx', - 'ok no problem', - 'ok np', - 'okay thank you', - 'okay thanks', - 'okay ty', - 'okay thanx', - 'okay thnx', - 'okay thx', - 'okay no problem', - 'okay np', - 'okey thank you', - 'okey thanks', - 'okey ty', - 'okey thanx', - 'okey thnx', - 'okey thx', - 'okey no problem', - 'okey np', - 'cheers' + "ok", + "okay", + "thanks", + "ty", + "k", + "kk", + "thank you", + "thanx", + "thnx", + "thx", + "tnx", + "ok thank you", + "ok thanks", + "ok ty", + "ok thanx", + "ok thnx", + "ok thx", + "ok no problem", + "ok np", + "okay thank you", + "okay thanks", + "okay ty", + "okay thanx", + "okay thnx", + "okay thx", + "okay no problem", + "okay np", + "okey thank you", + "okey thanks", + "okey ty", + "okey thanx", + "okey thnx", + "okey thx", + "okey no problem", + "okey np", + "cheers" ], }; diff --git a/src/data/generateCfgJsdoc.js b/src/data/generateCfgJsdoc.js index 096ddb8..4470dd4 100644 --- a/src/data/generateCfgJsdoc.js +++ b/src/data/generateCfgJsdoc.js @@ -1,8 +1,8 @@ -const path = require('path'); -const fs = require('fs'); -const toJsdoc = require('json-schema-to-jsdoc'); -const schema = require('./cfg.schema.json'); -const target = path.join(__dirname, 'cfg.jsdoc.js'); +const path = require("path"); +const fs = require("fs"); +const toJsdoc = require("json-schema-to-jsdoc"); +const schema = require("./cfg.schema.json"); +const target = path.join(__dirname, "cfg.jsdoc.js"); // Fix up some custom types for the JSDoc conversion const schemaCopy = JSON.parse(JSON.stringify(schema)); @@ -20,4 +20,4 @@ for (const propertyDef of Object.values(schemaCopy.properties)) { } const result = toJsdoc(schemaCopy); -fs.writeFileSync(target, result, { encoding: 'utf8' }); +fs.writeFileSync(target, result, { encoding: "utf8" }); diff --git a/src/data/snippets.js b/src/data/snippets.js index a95b2b4..0e4aebc 100644 --- a/src/data/snippets.js +++ b/src/data/snippets.js @@ -1,14 +1,14 @@ -const moment = require('moment'); -const knex = require('../knex'); -const Snippet = require('./Snippet'); +const moment = require("moment"); +const knex = require("../knex"); +const Snippet = require("./Snippet"); /** * @param {String} trigger * @returns {Promise} */ async function getSnippet(trigger) { - const snippet = await knex('snippets') - .where('trigger', trigger) + const snippet = await knex("snippets") + .where("trigger", trigger) .first(); return (snippet ? new Snippet(snippet) : null); @@ -22,11 +22,11 @@ async function getSnippet(trigger) { async function addSnippet(trigger, body, createdBy = 0) { if (await getSnippet(trigger)) return; - return knex('snippets').insert({ + return knex("snippets").insert({ trigger, body, created_by: createdBy, - created_at: moment.utc().format('YYYY-MM-DD HH:mm:ss') + created_at: moment.utc().format("YYYY-MM-DD HH:mm:ss") }); } @@ -35,8 +35,8 @@ async function addSnippet(trigger, body, createdBy = 0) { * @returns {Promise} */ async function deleteSnippet(trigger) { - return knex('snippets') - .where('trigger', trigger) + return knex("snippets") + .where("trigger", trigger) .delete(); } @@ -44,7 +44,7 @@ async function deleteSnippet(trigger) { * @returns {Promise} */ async function getAllSnippets() { - const snippets = await knex('snippets') + const snippets = await knex("snippets") .select(); return snippets.map(s => new Snippet(s)); diff --git a/src/data/threads.js b/src/data/threads.js index 4120489..58692f0 100644 --- a/src/data/threads.js +++ b/src/data/threads.js @@ -1,19 +1,19 @@ -const {User, Member} = require('eris'); +const {User, Member} = require("eris"); -const transliterate = require('transliteration'); -const moment = require('moment'); -const uuid = require('uuid'); -const humanizeDuration = require('humanize-duration'); +const transliterate = require("transliteration"); +const moment = require("moment"); +const uuid = require("uuid"); +const humanizeDuration = require("humanize-duration"); -const bot = require('../bot'); -const knex = require('../knex'); -const config = require('../cfg'); -const utils = require('../utils'); -const updates = require('./updates'); +const bot = require("../bot"); +const knex = require("../knex"); +const config = require("../cfg"); +const utils = require("../utils"); +const updates = require("./updates"); -const Thread = require('./Thread'); +const Thread = require("./Thread"); const {callBeforeNewThreadHooks} = require("../hooks/beforeNewThread"); -const {THREAD_STATUS, DISOCRD_CHANNEL_TYPES} = require('./constants'); +const {THREAD_STATUS, DISOCRD_CHANNEL_TYPES} = require("./constants"); const MINUTES = 60 * 1000; const HOURS = 60 * MINUTES; @@ -23,8 +23,8 @@ const HOURS = 60 * MINUTES; * @returns {Promise} */ async function findById(id) { - const thread = await knex('threads') - .where('id', id) + const thread = await knex("threads") + .where("id", id) .first(); return (thread ? new Thread(thread) : null); @@ -35,9 +35,9 @@ async function findById(id) { * @returns {Promise} */ async function findOpenThreadByUserId(userId) { - const thread = await knex('threads') - .where('user_id', userId) - .where('status', THREAD_STATUS.OPEN) + const thread = await knex("threads") + .where("user_id", userId) + .where("status", THREAD_STATUS.OPEN) .first(); return (thread ? new Thread(thread) : null); @@ -70,7 +70,7 @@ async function createNewThreadForUser(user, opts = {}) { const existingThread = await findOpenThreadByUserId(user.id); if (existingThread) { - throw new Error('Attempted to create a new thread for a user with an existing open thread!'); + throw new Error("Attempted to create a new thread for a user with an existing open thread!"); } // If set in config, check that the user's account is old enough (time since they registered on Discord) @@ -132,7 +132,7 @@ async function createNewThreadForUser(user, opts = {}) { // Use the user's name+discrim for the thread channel's name // Channel names are particularly picky about what characters they allow, so we gotta do some clean-up let cleanName = transliterate.slugify(user.username); - if (cleanName === '') cleanName = 'unknown'; + if (cleanName === "") cleanName = "unknown"; cleanName = cleanName.slice(0, 95); // Make sure the discrim fits const channelName = `${cleanName}-${user.discriminator}`; @@ -161,7 +161,7 @@ async function createNewThreadForUser(user, opts = {}) { let createdChannel; try { createdChannel = await utils.getInboxGuild().createChannel(channelName, DISOCRD_CHANNEL_TYPES.GUILD_TEXT, { - reason: 'New Modmail thread', + reason: "New Modmail thread", parentID: newThreadCategoryId, }); } catch (err) { @@ -175,7 +175,7 @@ async function createNewThreadForUser(user, opts = {}) { user_id: user.id, user_name: `${user.username}#${user.discriminator}`, channel_id: createdChannel.id, - created_at: moment.utc().format('YYYY-MM-DD HH:mm:ss') + created_at: moment.utc().format("YYYY-MM-DD HH:mm:ss") }); const newThread = await findById(newThreadId); @@ -216,7 +216,7 @@ async function createNewThreadForUser(user, opts = {}) { infoHeaderItems.push(`ID **${user.id}**`); } - let infoHeader = infoHeaderItems.join(', '); + let infoHeader = infoHeaderItems.join(", "); // Guild member info for (const [guildId, guildData] of userGuildData.entries()) { @@ -235,10 +235,10 @@ async function createNewThreadForUser(user, opts = {}) { if (config.rolesInThreadHeader && guildData.member.roles.length) { const roles = guildData.member.roles.map(roleId => guildData.guild.roles.get(roleId)).filter(Boolean); - headerItems.push(`ROLES **${roles.map(r => r.name).join(', ')}**`); + headerItems.push(`ROLES **${roles.map(r => r.name).join(", ")}**`); } - const headerStr = headerItems.join(', '); + const headerStr = headerItems.join(", "); if (mainGuilds.length === 1) { infoHeader += `\n${headerStr}`; @@ -253,7 +253,7 @@ async function createNewThreadForUser(user, opts = {}) { infoHeader += `\n\nThis user has **${userLogCount}** previous modmail threads. Use \`${config.prefix}logs\` to see them.`; } - infoHeader += '\n────────────────'; + infoHeader += "\n────────────────"; await newThread.postSystemMessage(infoHeader); @@ -280,10 +280,10 @@ async function createNewThreadForUser(user, opts = {}) { */ async function createThreadInDB(data) { const threadId = uuid.v4(); - const now = moment.utc().format('YYYY-MM-DD HH:mm:ss'); + const now = moment.utc().format("YYYY-MM-DD HH:mm:ss"); const finalData = Object.assign({created_at: now, is_legacy: 0}, data, {id: threadId}); - await knex('threads').insert(finalData); + await knex("threads").insert(finalData); return threadId; } @@ -293,8 +293,8 @@ async function createThreadInDB(data) { * @returns {Promise} */ async function findByChannelId(channelId) { - const thread = await knex('threads') - .where('channel_id', channelId) + const thread = await knex("threads") + .where("channel_id", channelId) .first(); return (thread ? new Thread(thread) : null); @@ -305,9 +305,9 @@ async function findByChannelId(channelId) { * @returns {Promise} */ async function findOpenThreadByChannelId(channelId) { - const thread = await knex('threads') - .where('channel_id', channelId) - .where('status', THREAD_STATUS.OPEN) + const thread = await knex("threads") + .where("channel_id", channelId) + .where("status", THREAD_STATUS.OPEN) .first(); return (thread ? new Thread(thread) : null); @@ -318,9 +318,9 @@ async function findOpenThreadByChannelId(channelId) { * @returns {Promise} */ async function findSuspendedThreadByChannelId(channelId) { - const thread = await knex('threads') - .where('channel_id', channelId) - .where('status', THREAD_STATUS.SUSPENDED) + const thread = await knex("threads") + .where("channel_id", channelId) + .where("status", THREAD_STATUS.SUSPENDED) .first(); return (thread ? new Thread(thread) : null); @@ -331,9 +331,9 @@ async function findSuspendedThreadByChannelId(channelId) { * @returns {Promise} */ async function getClosedThreadsByUserId(userId) { - const threads = await knex('threads') - .where('status', THREAD_STATUS.CLOSED) - .where('user_id', userId) + const threads = await knex("threads") + .where("status", THREAD_STATUS.CLOSED) + .where("user_id", userId) .select(); return threads.map(thread => new Thread(thread)); @@ -344,10 +344,10 @@ async function getClosedThreadsByUserId(userId) { * @returns {Promise} */ async function getClosedThreadCountByUserId(userId) { - const row = await knex('threads') - .where('status', THREAD_STATUS.CLOSED) - .where('user_id', userId) - .first(knex.raw('COUNT(id) AS thread_count')); + const row = await knex("threads") + .where("status", THREAD_STATUS.CLOSED) + .where("user_id", userId) + .first(knex.raw("COUNT(id) AS thread_count")); return parseInt(row.thread_count, 10); } @@ -365,24 +365,24 @@ async function findOrCreateThreadForUser(user, opts = {}) { } async function getThreadsThatShouldBeClosed() { - const now = moment.utc().format('YYYY-MM-DD HH:mm:ss'); - const threads = await knex('threads') - .where('status', THREAD_STATUS.OPEN) - .whereNotNull('scheduled_close_at') - .where('scheduled_close_at', '<=', now) - .whereNotNull('scheduled_close_at') + const now = moment.utc().format("YYYY-MM-DD HH:mm:ss"); + const threads = await knex("threads") + .where("status", THREAD_STATUS.OPEN) + .whereNotNull("scheduled_close_at") + .where("scheduled_close_at", "<=", now) + .whereNotNull("scheduled_close_at") .select(); return threads.map(thread => new Thread(thread)); } async function getThreadsThatShouldBeSuspended() { - const now = moment.utc().format('YYYY-MM-DD HH:mm:ss'); - const threads = await knex('threads') - .where('status', THREAD_STATUS.OPEN) - .whereNotNull('scheduled_suspend_at') - .where('scheduled_suspend_at', '<=', now) - .whereNotNull('scheduled_suspend_at') + const now = moment.utc().format("YYYY-MM-DD HH:mm:ss"); + const threads = await knex("threads") + .where("status", THREAD_STATUS.OPEN) + .whereNotNull("scheduled_suspend_at") + .where("scheduled_suspend_at", "<=", now) + .whereNotNull("scheduled_suspend_at") .select(); return threads.map(thread => new Thread(thread)); diff --git a/src/data/updates.js b/src/data/updates.js index b0e7285..ac83ad8 100644 --- a/src/data/updates.js +++ b/src/data/updates.js @@ -1,16 +1,16 @@ -const url = require('url'); -const https = require('https'); -const moment = require('moment'); -const knex = require('../knex'); -const config = require('../cfg'); +const url = require("url"); +const https = require("https"); +const moment = require("moment"); +const knex = require("../knex"); +const config = require("../cfg"); const UPDATE_CHECK_FREQUENCY = 12; // In hours let updateCheckPromise = null; async function initUpdatesTable() { - const row = await knex('updates').first(); + const row = await knex("updates").first(); if (! row) { - await knex('updates').insert({ + await knex("updates").insert({ available_version: null, last_checked: null, }); @@ -24,48 +24,48 @@ async function initUpdatesTable() { */ async function refreshVersions() { await initUpdatesTable(); - const { last_checked } = await knex('updates').first(); + const { last_checked } = await knex("updates").first(); // Only refresh available version if it's been more than UPDATE_CHECK_FREQUENCY since our last check - if (last_checked != null && last_checked > moment.utc().subtract(UPDATE_CHECK_FREQUENCY, 'hours').format('YYYY-MM-DD HH:mm:ss')) return; + if (last_checked != null && last_checked > moment.utc().subtract(UPDATE_CHECK_FREQUENCY, "hours").format("YYYY-MM-DD HH:mm:ss")) return; - const packageJson = require('../../package.json'); + const packageJson = require("../../package.json"); const repositoryUrl = packageJson.repository && packageJson.repository.url; if (! repositoryUrl) return; const parsedUrl = url.parse(repositoryUrl); - if (parsedUrl.hostname !== 'github.com') return; + if (parsedUrl.hostname !== "github.com") return; - const [, owner, repo] = parsedUrl.pathname.split('/'); + const [, owner, repo] = parsedUrl.pathname.split("/"); if (! owner || ! repo) return; https.get( { - hostname: 'api.github.com', + hostname: "api.github.com", path: `/repos/${owner}/${repo}/tags`, headers: { - 'User-Agent': `Modmail Bot (https://github.com/${owner}/${repo}) (${packageJson.version})` + "User-Agent": `Modmail Bot (https://github.com/${owner}/${repo}) (${packageJson.version})` } }, async res => { if (res.statusCode !== 200) { - await knex('updates').update({ - last_checked: moment.utc().format('YYYY-MM-DD HH:mm:ss') + await knex("updates").update({ + last_checked: moment.utc().format("YYYY-MM-DD HH:mm:ss") }); console.warn(`[WARN] Got status code ${res.statusCode} when checking for available updates`); return; } - let data = ''; - res.on('data', chunk => data += chunk); - res.on('end', async () => { + let data = ""; + res.on("data", chunk => data += chunk); + res.on("end", async () => { const parsed = JSON.parse(data); if (! Array.isArray(parsed) || parsed.length === 0) return; const latestVersion = parsed[0].name; - await knex('updates').update({ + await knex("updates").update({ available_version: latestVersion, - last_checked: moment.utc().format('YYYY-MM-DD HH:mm:ss') + last_checked: moment.utc().format("YYYY-MM-DD HH:mm:ss") }); }); } @@ -78,11 +78,11 @@ async function refreshVersions() { * @returns {Number} 1 if version a is larger than b, -1 is version a is smaller than b, 0 if they are equal */ function compareVersions(a, b) { - const aParts = a.split('.'); - const bParts = b.split('.'); + const aParts = a.split("."); + const bParts = b.split("."); for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) { - let aPart = parseInt((aParts[i] || '0').match(/\d+/)[0] || '0', 10); - let bPart = parseInt((bParts[i] || '0').match(/\d+/)[0] || '0', 10); + let aPart = parseInt((aParts[i] || "0").match(/\d+/)[0] || "0", 10); + let bPart = parseInt((bParts[i] || "0").match(/\d+/)[0] || "0", 10); if (aPart > bPart) return 1; if (aPart < bPart) return -1; } @@ -92,9 +92,9 @@ function compareVersions(a, b) { async function getAvailableUpdate() { await initUpdatesTable(); - const packageJson = require('../../package.json'); + const packageJson = require("../../package.json"); const currentVersion = packageJson.version; - const { available_version: availableVersion } = await knex('updates').first(); + const { available_version: availableVersion } = await knex("updates").first(); if (availableVersion == null) return null; if (currentVersion == null) return availableVersion; diff --git a/src/formatters.js b/src/formatters.js index 8733019..ea18eae 100644 --- a/src/formatters.js +++ b/src/formatters.js @@ -1,7 +1,7 @@ -const Eris = require('eris'); -const utils = require('./utils'); -const config = require('./cfg'); -const ThreadMessage = require('./data/ThreadMessage'); +const Eris = require("eris"); +const utils = require("./utils"); +const config = require("./cfg"); +const ThreadMessage = require("./data/ThreadMessage"); /** * Function to format the DM that is sent to the user when a staff member replies to them via !reply @@ -116,7 +116,7 @@ const defaultFormatters = { const mainRole = utils.getMainRole(moderator); const modName = (config.useNicknames ? moderator.nick || moderator.user.username : moderator.user.username); const modInfo = opts.isAnonymous - ? (mainRole ? mainRole.name : 'Moderator') + ? (mainRole ? mainRole.name : "Moderator") : (mainRole ? `(${mainRole.name}) ${modName}` : modName); return `**${modInfo}:** ${text}`; @@ -126,7 +126,7 @@ const defaultFormatters = { const mainRole = utils.getMainRole(moderator); const modName = (config.useNicknames ? moderator.nick || moderator.user.username : moderator.user.username); const modInfo = opts.isAnonymous - ? `(Anonymous) (${modName}) ${mainRole ? mainRole.name : 'Moderator'}` + ? `(Anonymous) (${modName}) ${mainRole ? mainRole.name : "Moderator"}` : (mainRole ? `(${mainRole.name}) ${modName}` : modName); let result = `**${modInfo}:** ${text}`; @@ -147,13 +147,13 @@ const defaultFormatters = { // Mirroring the DM formatting here... const modInfo = opts.isAnonymous - ? (mainRole ? mainRole.name : 'Moderator') + ? (mainRole ? mainRole.name : "Moderator") : (mainRole ? `(${mainRole.name}) ${modName}` : modName); let result = `**${modInfo}:** ${text}`; if (opts.attachmentLinks && opts.attachmentLinks.length) { - result += '\n'; + result += "\n"; for (const link of opts.attachmentLinks) { result += `\n**Attachment:** ${link}`; } @@ -165,8 +165,8 @@ const defaultFormatters = { }, formatUserReplyThreadMessage(user, msg, opts = {}) { - const content = (msg.content.trim() === '' && msg.embeds.length) - ? '' + const content = (msg.content.trim() === "" && msg.embeds.length) + ? "" : msg.content; let result = `**${user.username}#${user.discriminator}:** ${content}`; @@ -178,7 +178,7 @@ const defaultFormatters = { } if (config.threadTimestamps) { - const formattedTimestamp = utils.getTimestamp(msg.timestamp, 'x'); + const formattedTimestamp = utils.getTimestamp(msg.timestamp, "x"); result = `[${formattedTimestamp}] ${result}`; } @@ -186,8 +186,8 @@ const defaultFormatters = { }, formatUserReplyLogMessage(user, msg, opts = {}) { - const content = (msg.content.trim() === '' && msg.embeds.length) - ? '' + const content = (msg.content.trim() === "" && msg.embeds.length) + ? "" : msg.content; let result = content; diff --git a/src/hooks/beforeNewThread.js b/src/hooks/beforeNewThread.js index 801d84d..9805843 100644 --- a/src/hooks/beforeNewThread.js +++ b/src/hooks/beforeNewThread.js @@ -1,4 +1,4 @@ -const Eris = require('eris'); +const Eris = require("eris"); /** * @callback BeforeNewThreadHook_SetCategoryId diff --git a/src/index.js b/src/index.js index 7c5152e..588111a 100644 --- a/src/index.js +++ b/src/index.js @@ -1,25 +1,25 @@ // Verify NodeJS version -const nodeMajorVersion = parseInt(process.versions.node.split('.')[0], 10); +const nodeMajorVersion = parseInt(process.versions.node.split(".")[0], 10); if (nodeMajorVersion < 11) { - console.error('Unsupported NodeJS version! Please install Node.js 11, 12, 13, or 14.'); + console.error("Unsupported NodeJS version! Please install Node.js 11, 12, 13, or 14."); process.exit(1); } // Verify node modules have been installed -const fs = require('fs'); -const path = require('path'); +const fs = require("fs"); +const path = require("path"); try { - fs.accessSync(path.join(__dirname, '..', 'node_modules')); + fs.accessSync(path.join(__dirname, "..", "node_modules")); } catch (e) { - console.error('Please run "npm ci" before starting the bot'); + console.error("Please run \"npm ci\" before starting the bot"); process.exit(1); } // Error handling -process.on('uncaughtException', err => { +process.on("uncaughtException", err => { // Unknown message types (nitro boosting messages at the time) should be safe to ignore - if (err && err.message && err.message.startsWith('Unhandled MESSAGE_CREATE type')) { + if (err && err.message && err.message.startsWith("Unhandled MESSAGE_CREATE type")) { return; } @@ -28,27 +28,27 @@ process.on('uncaughtException', err => { process.exit(1); }); -let testedPackage = ''; +let testedPackage = ""; try { - const packageJson = require('../package.json'); + const packageJson = require("../package.json"); const modules = Object.keys(packageJson.dependencies); modules.forEach(mod => { testedPackage = mod; - fs.accessSync(path.join(__dirname, '..', 'node_modules', mod)) + fs.accessSync(path.join(__dirname, "..", "node_modules", mod)) }); } catch (e) { console.error(`Please run "npm ci" again! Package "${testedPackage}" is missing.`); process.exit(1); } -const config = require('./cfg'); -const utils = require('./utils'); -const main = require('./main'); -const knex = require('./knex'); -const legacyMigrator = require('./legacy/legacyMigrator'); +const config = require("./cfg"); +const utils = require("./utils"); +const main = require("./main"); +const knex = require("./knex"); +const legacyMigrator = require("./legacy/legacyMigrator"); // Force crash on unhandled rejections (use something like forever/pm2 to restart) -process.on('unhandledRejection', err => { +process.on("unhandledRejection", err => { if (err instanceof utils.BotError || (err && err.code)) { // We ignore stack traces for BotErrors (the message has enough info) and network errors from Eris (their stack traces are unreadably long) console.error(`Error: ${err.message}`); @@ -63,34 +63,34 @@ process.on('unhandledRejection', err => { // Make sure the database is up to date const [completed, newMigrations] = await knex.migrate.list(); if (newMigrations.length > 0) { - console.log('Updating database. This can take a while. Don\'t close the bot!'); + console.log("Updating database. This can take a while. Don't close the bot!"); await knex.migrate.latest(); - console.log('Done!'); + console.log("Done!"); } // Migrate legacy data if we need to if (await legacyMigrator.shouldMigrate()) { - console.log('=== MIGRATING LEGACY DATA ==='); - console.log('Do not close the bot!'); - console.log(''); + console.log("=== MIGRATING LEGACY DATA ==="); + console.log("Do not close the bot!"); + console.log(""); await legacyMigrator.migrate(); const relativeDbDir = (path.isAbsolute(config.dbDir) ? config.dbDir : path.resolve(process.cwd(), config.dbDir)); const relativeLogDir = (path.isAbsolute(config.logDir) ? config.logDir : path.resolve(process.cwd(), config.logDir)); - console.log(''); - console.log('=== LEGACY DATA MIGRATION FINISHED ==='); - console.log(''); - console.log('IMPORTANT: After the bot starts, please verify that all logs, threads, blocked users, and snippets are still working correctly.'); - console.log('Once you\'ve done that, the following files/directories are no longer needed. I would recommend keeping a backup of them, however.'); - console.log(''); - console.log('FILE: ' + path.resolve(relativeDbDir, 'threads.json')); - console.log('FILE: ' + path.resolve(relativeDbDir, 'blocked.json')); - console.log('FILE: ' + path.resolve(relativeDbDir, 'snippets.json')); - console.log('DIRECTORY: ' + relativeLogDir); - console.log(''); - console.log('Starting the bot...'); + console.log(""); + console.log("=== LEGACY DATA MIGRATION FINISHED ==="); + console.log(""); + console.log("IMPORTANT: After the bot starts, please verify that all logs, threads, blocked users, and snippets are still working correctly."); + console.log("Once you've done that, the following files/directories are no longer needed. I would recommend keeping a backup of them, however."); + console.log(""); + console.log("FILE: " + path.resolve(relativeDbDir, "threads.json")); + console.log("FILE: " + path.resolve(relativeDbDir, "blocked.json")); + console.log("FILE: " + path.resolve(relativeDbDir, "snippets.json")); + console.log("DIRECTORY: " + relativeLogDir); + console.log(""); + console.log("Starting the bot..."); } // Start the bot diff --git a/src/knex.js b/src/knex.js index ce33d98..35c8cb5 100644 --- a/src/knex.js +++ b/src/knex.js @@ -1,2 +1,2 @@ -const config = require('./cfg'); -module.exports = require('knex')(config.knex); +const config = require("./cfg"); +module.exports = require("knex")(config.knex); diff --git a/src/legacy/jsonDb.js b/src/legacy/jsonDb.js index 9b97c05..1c534e9 100644 --- a/src/legacy/jsonDb.js +++ b/src/legacy/jsonDb.js @@ -1,6 +1,6 @@ -const fs = require('fs'); -const path = require('path'); -const config = require('../cfg'); +const fs = require("fs"); +const path = require("path"); +const config = require("../cfg"); const dbDir = config.dbDir; @@ -15,7 +15,7 @@ class JSONDB { this.useCloneByDefault = useCloneByDefault; this.load = new Promise(resolve => { - fs.readFile(path, {encoding: 'utf8'}, (err, data) => { + fs.readFile(path, {encoding: "utf8"}, (err, data) => { if (err) return resolve(def); let unserialized; @@ -39,7 +39,7 @@ class JSONDB { save(newData) { const serialized = JSON.stringify(newData); this.load = new Promise((resolve, reject) => { - fs.writeFile(this.path, serialized, {encoding: 'utf8'}, () => { + fs.writeFile(this.path, serialized, {encoding: "utf8"}, () => { resolve(newData); }); }); diff --git a/src/legacy/legacyMigrator.js b/src/legacy/legacyMigrator.js index bf3f947..18720ec 100644 --- a/src/legacy/legacyMigrator.js +++ b/src/legacy/legacyMigrator.js @@ -1,15 +1,15 @@ -const fs = require('fs'); -const path = require('path'); -const promisify = require('util').promisify; -const moment = require('moment'); -const Eris = require('eris'); +const fs = require("fs"); +const path = require("path"); +const promisify = require("util").promisify; +const moment = require("moment"); +const Eris = require("eris"); -const knex = require('../knex'); -const config = require('../cfg'); -const jsonDb = require('./jsonDb'); -const threads = require('../data/threads'); +const knex = require("../knex"); +const config = require("../cfg"); +const jsonDb = require("./jsonDb"); +const threads = require("../data/threads"); -const {THREAD_STATUS, THREAD_MESSAGE_TYPE} = require('../data/constants'); +const {THREAD_STATUS, THREAD_MESSAGE_TYPE} = require("../data/constants"); const readDir = promisify(fs.readdir); const readFile = promisify(fs.readFile); @@ -17,43 +17,43 @@ const access = promisify(fs.access); const writeFile = promisify(fs.writeFile); async function migrate() { - console.log('Migrating open threads...'); + console.log("Migrating open threads..."); await migrateOpenThreads(); - console.log('Migrating logs...'); + console.log("Migrating logs..."); await migrateLogs(); - console.log('Migrating blocked users...'); + console.log("Migrating blocked users..."); await migrateBlockedUsers(); - console.log('Migrating snippets...'); + console.log("Migrating snippets..."); await migrateSnippets(); - await writeFile(path.join(config.dbDir, '.migrated_legacy'), ''); + await writeFile(path.join(config.dbDir, ".migrated_legacy"), ""); } async function shouldMigrate() { // If there is a file marking a finished migration, assume we don't need to migrate - const migrationFile = path.join(config.dbDir, '.migrated_legacy'); + const migrationFile = path.join(config.dbDir, ".migrated_legacy"); try { await access(migrationFile); return false; } catch (e) {} // If there are any old threads, we need to migrate - const oldThreads = await jsonDb.get('threads', []); + const oldThreads = await jsonDb.get("threads", []); if (oldThreads.length) { return true; } // If there are any old blocked users, we need to migrate - const blockedUsers = await jsonDb.get('blocked', []); + const blockedUsers = await jsonDb.get("blocked", []); if (blockedUsers.length) { return true; } // If there are any old snippets, we need to migrate - const snippets = await jsonDb.get('snippets', {}); + const snippets = await jsonDb.get("snippets", {}); if (Object.keys(snippets).length) { return true; } @@ -71,12 +71,12 @@ async function migrateOpenThreads() { const bot = new Eris.Client(config.token); const toReturn = new Promise(resolve => { - bot.on('ready', async () => { - const oldThreads = await jsonDb.get('threads', []); + bot.on("ready", async () => { + const oldThreads = await jsonDb.get("threads", []); const promises = oldThreads.map(async oldThread => { - const existingOpenThread = await knex('threads') - .where('channel_id', oldThread.channelId) + const existingOpenThread = await knex("threads") + .where("channel_id", oldThread.channelId) .first(); if (existingOpenThread) return; @@ -86,9 +86,9 @@ async function migrateOpenThreads() { const threadMessages = await oldChannel.getMessages(1000); const log = threadMessages.reverse().map(msg => { - const date = moment.utc(msg.timestamp, 'x').format('YYYY-MM-DD HH:mm:ss'); + const date = moment.utc(msg.timestamp, "x").format("YYYY-MM-DD HH:mm:ss"); return `[${date}] ${msg.author.username}#${msg.author.discriminator}: ${msg.content}`; - }).join('\n') + '\n'; + }).join("\n") + "\n"; const newThread = { status: THREAD_STATUS.OPEN, @@ -100,14 +100,14 @@ async function migrateOpenThreads() { const threadId = await threads.createThreadInDB(newThread); - await knex('thread_messages').insert({ + await knex("thread_messages").insert({ thread_id: threadId, message_type: THREAD_MESSAGE_TYPE.LEGACY, user_id: oldThread.userId, - user_name: '', + user_name: "", body: log, is_anonymous: 0, - created_at: moment.utc().format('YYYY-MM-DD HH:mm:ss') + created_at: moment.utc().format("YYYY-MM-DD HH:mm:ss") }); }); @@ -128,38 +128,38 @@ async function migrateLogs() { for (let i = 0; i < logFiles.length; i++) { const logFile = logFiles[i]; - if (! logFile.endsWith('.txt')) continue; + if (! logFile.endsWith(".txt")) continue; - const [rawDate, userId, threadId] = logFile.slice(0, -4).split('__'); - const date = `${rawDate.slice(0, 10)} ${rawDate.slice(11).replace('-', ':')}`; + const [rawDate, userId, threadId] = logFile.slice(0, -4).split("__"); + const date = `${rawDate.slice(0, 10)} ${rawDate.slice(11).replace("-", ":")}`; const fullPath = path.join(logDir, logFile); - const contents = await readFile(fullPath, {encoding: 'utf8'}); + const contents = await readFile(fullPath, {encoding: "utf8"}); const newThread = { id: threadId, status: THREAD_STATUS.CLOSED, user_id: userId, - user_name: '', + user_name: "", channel_id: null, is_legacy: 1, created_at: date }; await knex.transaction(async trx => { - const existingThread = await trx('threads') - .where('id', newThread.id) + const existingThread = await trx("threads") + .where("id", newThread.id) .first(); if (existingThread) return; - await trx('threads').insert(newThread); + await trx("threads").insert(newThread); - await trx('thread_messages').insert({ + await trx("thread_messages").insert({ thread_id: newThread.id, message_type: THREAD_MESSAGE_TYPE.LEGACY, user_id: userId, - user_name: '', + user_name: "", body: contents, is_anonymous: 0, created_at: date @@ -174,19 +174,19 @@ async function migrateLogs() { } async function migrateBlockedUsers() { - const now = moment.utc().format('YYYY-MM-DD HH:mm:ss'); - const blockedUsers = await jsonDb.get('blocked', []); + const now = moment.utc().format("YYYY-MM-DD HH:mm:ss"); + const blockedUsers = await jsonDb.get("blocked", []); for (const userId of blockedUsers) { - const existingBlockedUser = await knex('blocked_users') - .where('user_id', userId) + const existingBlockedUser = await knex("blocked_users") + .where("user_id", userId) .first(); if (existingBlockedUser) return; - await knex('blocked_users').insert({ + await knex("blocked_users").insert({ user_id: userId, - user_name: '', + user_name: "", blocked_by: null, blocked_at: now }); @@ -194,17 +194,17 @@ async function migrateBlockedUsers() { } async function migrateSnippets() { - const now = moment.utc().format('YYYY-MM-DD HH:mm:ss'); - const snippets = await jsonDb.get('snippets', {}); + const now = moment.utc().format("YYYY-MM-DD HH:mm:ss"); + const snippets = await jsonDb.get("snippets", {}); const promises = Object.entries(snippets).map(async ([trigger, data]) => { - const existingSnippet = await knex('snippets') - .where('trigger', trigger) + const existingSnippet = await knex("snippets") + .where("trigger", trigger) .first(); if (existingSnippet) return; - return knex('snippets').insert({ + return knex("snippets").insert({ trigger, body: data.text, is_anonymous: data.isAnonymous ? 1 : 0, diff --git a/src/main.js b/src/main.js index b9a319c..9f0ff4c 100644 --- a/src/main.js +++ b/src/main.js @@ -1,58 +1,58 @@ -const Eris = require('eris'); -const path = require('path'); +const Eris = require("eris"); +const path = require("path"); -const config = require('./cfg'); -const bot = require('./bot'); -const knex = require('./knex'); -const {messageQueue} = require('./queue'); -const utils = require('./utils'); -const { createCommandManager } = require('./commands'); -const { getPluginAPI, loadPlugin } = require('./plugins'); -const { callBeforeNewThreadHooks } = require('./hooks/beforeNewThread'); +const config = require("./cfg"); +const bot = require("./bot"); +const knex = require("./knex"); +const {messageQueue} = require("./queue"); +const utils = require("./utils"); +const { createCommandManager } = require("./commands"); +const { getPluginAPI, loadPlugin } = require("./plugins"); +const { callBeforeNewThreadHooks } = require("./hooks/beforeNewThread"); -const blocked = require('./data/blocked'); -const threads = require('./data/threads'); -const updates = require('./data/updates'); +const blocked = require("./data/blocked"); +const threads = require("./data/threads"); +const updates = require("./data/updates"); -const reply = require('./modules/reply'); -const close = require('./modules/close'); -const snippets = require('./modules/snippets'); -const logs = require('./modules/logs'); -const move = require('./modules/move'); -const block = require('./modules/block'); -const suspend = require('./modules/suspend'); -const webserver = require('./modules/webserver'); -const greeting = require('./modules/greeting'); -const typingProxy = require('./modules/typingProxy'); -const version = require('./modules/version'); -const newthread = require('./modules/newthread'); -const idModule = require('./modules/id'); -const alert = require('./modules/alert'); +const reply = require("./modules/reply"); +const close = require("./modules/close"); +const snippets = require("./modules/snippets"); +const logs = require("./modules/logs"); +const move = require("./modules/move"); +const block = require("./modules/block"); +const suspend = require("./modules/suspend"); +const webserver = require("./modules/webserver"); +const greeting = require("./modules/greeting"); +const typingProxy = require("./modules/typingProxy"); +const version = require("./modules/version"); +const newthread = require("./modules/newthread"); +const idModule = require("./modules/id"); +const alert = require("./modules/alert"); -const {ACCIDENTAL_THREAD_MESSAGES} = require('./data/constants'); +const {ACCIDENTAL_THREAD_MESSAGES} = require("./data/constants"); module.exports = { async start() { - console.log('Connecting to Discord...'); + console.log("Connecting to Discord..."); - bot.once('ready', async () => { - console.log('Connected! Waiting for guilds to become available...'); + bot.once("ready", async () => { + console.log("Connected! Waiting for guilds to become available..."); await Promise.all([ ...config.mainGuildId.map(id => waitForGuild(id)), waitForGuild(config.mailGuildId) ]); - console.log('Initializing...'); + console.log("Initializing..."); initStatus(); initBaseMessageHandlers(); - console.log('Loading plugins...'); + console.log("Loading plugins..."); const pluginResult = await initPlugins(); console.log(`Loaded ${pluginResult.loadedCount} plugins (${pluginResult.builtInCount} built-in plugins, ${pluginResult.externalCount} external plugins)`); - console.log(''); - console.log('Done! Now listening to DMs.'); - console.log(''); + console.log(""); + console.log("Done! Now listening to DMs."); + console.log(""); }); bot.connect(); @@ -65,7 +65,7 @@ function waitForGuild(guildId) { } return new Promise(resolve => { - bot.on('guildAvailable', guild => { + bot.on("guildAvailable", guild => { if (guild.id === guildId) { resolve(); } @@ -89,7 +89,7 @@ function initBaseMessageHandlers() { * 1) If alwaysReply is enabled, reply to the user * 2) If alwaysReply is disabled, save that message as a chat message in the thread */ - bot.on('messageCreate', async msg => { + bot.on("messageCreate", async msg => { if (! utils.messageIsOnInboxServer(msg)) return; if (msg.author.bot) return; @@ -116,7 +116,7 @@ function initBaseMessageHandlers() { * 1) Find the open modmail thread for this user, or create a new one * 2) Post the message as a user reply in the thread */ - bot.on('messageCreate', async msg => { + bot.on("messageCreate", async msg => { if (! (msg.channel instanceof Eris.PrivateChannel)) return; if (msg.author.bot) return; if (msg.type !== 0) return; // Ignore pins etc. @@ -133,7 +133,7 @@ function initBaseMessageHandlers() { if (config.ignoreAccidentalThreads && msg.content && ACCIDENTAL_THREAD_MESSAGES.includes(msg.content.trim().toLowerCase())) return; thread = await threads.createNewThreadForUser(msg.author, { - source: 'dm', + source: "dm", }); } @@ -148,13 +148,13 @@ function initBaseMessageHandlers() { * 1) If that message was in DMs, and we have a thread open with that user, post the edit as a system message in the thread * 2) If that message was moderator chatter in the thread, update the corresponding chat message in the DB */ - bot.on('messageUpdate', async (msg, oldMessage) => { + bot.on("messageUpdate", async (msg, oldMessage) => { if (! msg || ! msg.author) return; if (msg.author.bot) return; if (await blocked.isBlocked(msg.author.id)) return; // Old message content doesn't persist between bot restarts - const oldContent = oldMessage && oldMessage.content || '*Unavailable due to bot restart*'; + const oldContent = oldMessage && oldMessage.content || "*Unavailable due to bot restart*"; const newContent = msg.content; // Ignore edit events with changes only in embeds etc. @@ -181,7 +181,7 @@ function initBaseMessageHandlers() { /** * When a staff message is deleted in a modmail thread, delete it from the database as well */ - bot.on('messageDelete', async msg => { + bot.on("messageDelete", async msg => { if (! msg.author) return; if (msg.author.bot) return; if (! utils.messageIsOnInboxServer(msg)) return; @@ -196,7 +196,7 @@ function initBaseMessageHandlers() { /** * When the bot is mentioned on the main server, ping staff in the log channel about it */ - bot.on('messageCreate', async msg => { + bot.on("messageCreate", async msg => { if (! utils.messageIsOnMainServer(msg)) return; if (! msg.mentions.some(user => user.id === bot.user.id)) return; if (msg.author.bot) return; @@ -215,7 +215,7 @@ function initBaseMessageHandlers() { let content; const mainGuilds = utils.getMainGuilds(); - const staffMention = (config.pingOnBotMention ? utils.getInboxMention() : ''); + const staffMention = (config.pingOnBotMention ? utils.getInboxMention() : ""); if (mainGuilds.length === 1) { content = `${staffMention}Bot mentioned in ${msg.channel.mention} by **${msg.author.username}#${msg.author.discriminator}(${msg.author.id})**: "${msg.cleanContent}"\n\n`; diff --git a/src/modules/alert.js b/src/modules/alert.js index a844230..6f8f1ba 100644 --- a/src/modules/alert.js +++ b/src/modules/alert.js @@ -1,8 +1,8 @@ module.exports = ({ bot, knex, config, commands }) => { - commands.addInboxThreadCommand('alert', '[opt:string]', async (msg, args, thread) => { - if (args.opt && args.opt.startsWith('c')) { + 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`); + await thread.postSystemMessage("Cancelled new message alert"); } else { await thread.setAlert(msg.author.id); await thread.postSystemMessage(`Pinging ${msg.author.username}#${msg.author.discriminator} when this thread gets a new reply`); diff --git a/src/modules/block.js b/src/modules/block.js index 94913c4..5eea8fd 100644 --- a/src/modules/block.js +++ b/src/modules/block.js @@ -1,5 +1,5 @@ -const humanizeDuration = require('humanize-duration'); -const moment = require('moment'); +const humanizeDuration = require("humanize-duration"); +const moment = require("moment"); const blocked = require("../data/blocked"); const utils = require("../utils"); @@ -23,7 +23,7 @@ module.exports = ({ bot, knex, config, commands }) => { setTimeout(expiredBlockLoop, 2000); } - bot.on('ready', expiredBlockLoop); + bot.on("ready", expiredBlockLoop); const blockCmd = async (msg, args, thread) => { const userIdToBlock = args.userId || (thread && thread.user_id); @@ -31,16 +31,16 @@ module.exports = ({ bot, knex, config, commands }) => { const isBlocked = await blocked.isBlocked(userIdToBlock); if (isBlocked) { - msg.channel.createMessage('User is already blocked'); + msg.channel.createMessage("User is already blocked"); return; } const expiresAt = args.blockTime - ? moment.utc().add(args.blockTime, 'ms').format('YYYY-MM-DD HH:mm:ss') + ? 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); + await blocked.block(userIdToBlock, (user ? `${user.username}#${user.discriminator}` : ""), msg.author.id, expiresAt); if (expiresAt) { const humanized = humanizeDuration(args.blockTime, { largest: 2, round: true }); @@ -50,8 +50,8 @@ module.exports = ({ bot, knex, config, commands }) => { } }; - commands.addInboxServerCommand('block', ' [blockTime:delay]', blockCmd); - commands.addInboxServerCommand('block', '[blockTime:delay]', blockCmd); + 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); @@ -59,12 +59,12 @@ module.exports = ({ bot, knex, config, commands }) => { const isBlocked = await blocked.isBlocked(userIdToUnblock); if (! isBlocked) { - msg.channel.createMessage('User is not blocked'); + msg.channel.createMessage("User is not blocked"); return; } const unblockAt = args.unblockDelay - ? moment.utc().add(args.unblockDelay, 'ms').format('YYYY-MM-DD HH:mm:ss') + ? moment.utc().add(args.unblockDelay, "ms").format("YYYY-MM-DD HH:mm:ss") : null; const user = bot.users.get(userIdToUnblock); @@ -78,10 +78,10 @@ module.exports = ({ bot, knex, config, commands }) => { } }; - commands.addInboxServerCommand('unblock', ' [unblockDelay:delay]', unblockCmd); - commands.addInboxServerCommand('unblock', '[unblockDelay:delay]', unblockCmd); + commands.addInboxServerCommand("unblock", " [unblockDelay:delay]", unblockCmd); + commands.addInboxServerCommand("unblock", "[unblockDelay:delay]", unblockCmd); - commands.addInboxServerCommand('is_blocked', '[userId:userId]',async (msg, args, thread) => { + commands.addInboxServerCommand("is_blocked", "[userId:userId]",async (msg, args, thread) => { const userIdToCheck = args.userId || (thread && thread.user_id); if (! userIdToCheck) return; diff --git a/src/modules/close.js b/src/modules/close.js index b3173ac..19679aa 100644 --- a/src/modules/close.js +++ b/src/modules/close.js @@ -1,10 +1,10 @@ -const moment = require('moment'); -const Eris = require('eris'); -const config = require('../cfg'); -const utils = require('../utils'); -const threads = require('../data/threads'); -const blocked = require('../data/blocked'); -const {messageQueue} = require('../queue'); +const moment = require("moment"); +const Eris = require("eris"); +const config = require("../cfg"); +const utils = require("../utils"); +const threads = require("../data/threads"); +const blocked = require("../data/blocked"); +const {messageQueue} = require("../queue"); module.exports = ({ bot, knex, config, commands }) => { // Check for threads that are scheduled to be closed and close them @@ -39,7 +39,7 @@ module.exports = ({ bot, knex, config, commands }) => { scheduledCloseLoop(); // Close a thread. Closing a thread saves a log of the channel's contents and then deletes the channel. - commands.addGlobalCommand('close', '[opts...]', async (msg, args) => { + commands.addGlobalCommand("close", "[opts...]", async (msg, args) => { let thread, closedBy; let hasCloseMessage = !! config.closeMessage; @@ -56,11 +56,11 @@ module.exports = ({ bot, knex, config, commands }) => { // We need to add this operation to the message queue so we don't get a race condition // between showing the close command in the thread and closing the thread await messageQueue.add(async () => { - thread.postSystemMessage('Thread closed by user, closing...'); + thread.postSystemMessage("Thread closed by user, closing..."); await thread.close(true); }); - closedBy = 'the user'; + closedBy = "the user"; } else { // A staff member is closing the thread if (! utils.messageIsOnInboxServer(msg)) return; @@ -70,18 +70,18 @@ module.exports = ({ bot, knex, config, commands }) => { if (! thread) return; if (args.opts && args.opts.length) { - if (args.opts.includes('cancel') || args.opts.includes('c')) { + if (args.opts.includes("cancel") || args.opts.includes("c")) { // Cancel timed close if (thread.scheduled_close_at) { await thread.cancelScheduledClose(); - thread.postSystemMessage(`Cancelled scheduled closing`); + thread.postSystemMessage("Cancelled scheduled closing"); } return; } // Silent close (= no close message) - if (args.opts.includes('silent') || args.opts.includes('s')) { + if (args.opts.includes("silent") || args.opts.includes("s")) { silentClose = true; } @@ -90,12 +90,12 @@ module.exports = ({ bot, knex, config, commands }) => { if (delayStringArg) { const delay = utils.convertDelayStringToMS(delayStringArg); if (delay === 0 || delay === null) { - thread.postSystemMessage(`Invalid delay specified. Format: "1h30m"`); + thread.postSystemMessage("Invalid delay specified. Format: \"1h30m\""); return; } - const closeAt = moment.utc().add(delay, 'ms'); - await thread.scheduleClose(closeAt.format('YYYY-MM-DD HH:mm:ss'), msg.author, silentClose ? 1 : 0); + const closeAt = moment.utc().add(delay, "ms"); + await thread.scheduleClose(closeAt.format("YYYY-MM-DD HH:mm:ss"), msg.author, silentClose ? 1 : 0); let response; if (silentClose) { @@ -129,7 +129,7 @@ module.exports = ({ bot, knex, config, commands }) => { }); // Auto-close threads if their channel is deleted - bot.on('channelDelete', async (channel) => { + bot.on("channelDelete", async (channel) => { if (! (channel instanceof Eris.TextChannel)) return; if (channel.guild.id !== utils.getInboxGuild().id) return; diff --git a/src/modules/greeting.js b/src/modules/greeting.js index 1d1d7cb..37a2620 100644 --- a/src/modules/greeting.js +++ b/src/modules/greeting.js @@ -1,12 +1,12 @@ -const path = require('path'); -const fs = require('fs'); -const config = require('../cfg'); -const utils = require('../utils'); +const path = require("path"); +const fs = require("fs"); +const config = require("../cfg"); +const utils = require("../utils"); module.exports = ({ bot }) => { if (! config.enableGreeting) return; - bot.on('guildMemberAdd', (guild, member) => { + bot.on("guildMemberAdd", (guild, member) => { const guildGreeting = config.guildGreetings[guild.id]; if (! guildGreeting || (! guildGreeting.message && ! guildGreeting.attachment)) return; @@ -14,7 +14,7 @@ module.exports = ({ bot }) => { bot.getDMChannel(member.id).then(channel => { if (! channel) return; - channel.createMessage(message || '', file) + channel.createMessage(message || "", file) .catch(e => { if (e.code === 50007) return; throw e; diff --git a/src/modules/id.js b/src/modules/id.js index 18fecdf..ceced1b 100644 --- a/src/modules/id.js +++ b/src/modules/id.js @@ -1,9 +1,9 @@ module.exports = ({ bot, knex, config, commands }) => { - commands.addInboxThreadCommand('id', [], async (msg, args, thread) => { + commands.addInboxThreadCommand("id", [], async (msg, args, thread) => { thread.postSystemMessage(thread.user_id); }); - commands.addInboxThreadCommand('dm_channel_id', [], async (msg, args, thread) => { + commands.addInboxThreadCommand("dm_channel_id", [], async (msg, args, thread) => { const dmChannel = await thread.getDMChannel(); thread.postSystemMessage(dmChannel.id); }); diff --git a/src/modules/logs.js b/src/modules/logs.js index 1559d49..61b7729 100644 --- a/src/modules/logs.js +++ b/src/modules/logs.js @@ -1,5 +1,5 @@ const threads = require("../data/threads"); -const moment = require('moment'); +const moment = require("moment"); const utils = require("../utils"); const LOG_LINES_PER_PAGE = 10; @@ -30,7 +30,7 @@ module.exports = ({ bot, knex, config, commands }) => { const threadLines = await Promise.all(userThreads.map(async thread => { const logUrl = await thread.getLogUrl(); - const formattedDate = moment.utc(thread.created_at).format('MMM Do [at] HH:mm [UTC]'); + const formattedDate = moment.utc(thread.created_at).format("MMM Do [at] HH:mm [UTC]"); return `\`${formattedDate}\`: <${logUrl}>`; })); @@ -38,26 +38,26 @@ module.exports = ({ bot, knex, config, commands }) => { ? `**Log files for <@${userId}>** (page **${page}/${maxPage}**, showing logs **${start + 1}-${end}/${totalUserThreads}**):` : `**Log files for <@${userId}>:**`; - message += `\n${threadLines.join('\n')}`; + message += `\n${threadLines.join("\n")}`; if (isPaginated) { - message += `\nTo view more, add a page number to the end of the command`; + message += "\nTo view more, add a page number to the end of the command"; } // Send the list of logs in chunks of 15 lines per message - const lines = message.split('\n'); + const lines = message.split("\n"); const chunks = utils.chunk(lines, 15); let root = Promise.resolve(); chunks.forEach(lines => { - root = root.then(() => msg.channel.createMessage(lines.join('\n'))); + root = root.then(() => msg.channel.createMessage(lines.join("\n"))); }); }; - commands.addInboxServerCommand('logs', ' [page:number]', logsCmd); - commands.addInboxServerCommand('logs', '[page:number]', logsCmd); + commands.addInboxServerCommand("logs", " [page:number]", logsCmd); + commands.addInboxServerCommand("logs", "[page:number]", logsCmd); - commands.addInboxServerCommand('loglink', [], async (msg, args, thread) => { + commands.addInboxServerCommand("loglink", [], async (msg, args, thread) => { if (! thread) { thread = await threads.findSuspendedThreadByChannelId(msg.channel.id); if (! thread) return; @@ -65,21 +65,21 @@ module.exports = ({ bot, knex, config, commands }) => { const logUrl = await thread.getLogUrl(); const query = []; - if (args.verbose) query.push('verbose=1'); - if (args.simple) query.push('simple=1'); - let qs = query.length ? `?${query.join('&')}` : ''; + if (args.verbose) query.push("verbose=1"); + if (args.simple) query.push("simple=1"); + let qs = query.length ? `?${query.join("&")}` : ""; thread.postSystemMessage(`Log URL: ${logUrl}${qs}`); }, { options: [ { - name: 'verbose', - shortcut: 'v', + name: "verbose", + shortcut: "v", isSwitch: true, }, { - name: 'simple', - shortcut: 's', + name: "simple", + shortcut: "s", isSwitch: true, }, ], diff --git a/src/modules/move.js b/src/modules/move.js index 44eefd3..0d4adcc 100644 --- a/src/modules/move.js +++ b/src/modules/move.js @@ -1,12 +1,12 @@ -const config = require('../cfg'); -const Eris = require('eris'); +const config = require("../cfg"); +const Eris = require("eris"); const transliterate = require("transliteration"); -const erisEndpoints = require('eris/lib/rest/Endpoints'); +const erisEndpoints = require("eris/lib/rest/Endpoints"); module.exports = ({ bot, knex, config, commands }) => { if (! config.allowMove) return; - commands.addInboxThreadCommand('move', '', async (msg, args, thread) => { + commands.addInboxThreadCommand("move", "", async (msg, args, thread) => { const searchStr = args.category; const normalizedSearchStr = transliterate.slugify(searchStr); @@ -41,7 +41,7 @@ module.exports = ({ bot, knex, config, commands }) => { }); if (containsRankings[0][1] === 0) { - thread.postSystemMessage('No matching category'); + thread.postSystemMessage("No matching category"); return; } diff --git a/src/modules/newthread.js b/src/modules/newthread.js index 834ba55..22bae22 100644 --- a/src/modules/newthread.js +++ b/src/modules/newthread.js @@ -2,10 +2,10 @@ const utils = require("../utils"); const threads = require("../data/threads"); module.exports = ({ bot, knex, config, commands }) => { - commands.addInboxServerCommand('newthread', '', async (msg, args, thread) => { + commands.addInboxServerCommand("newthread", "", async (msg, args, thread) => { const user = bot.users.get(args.userId); if (! user) { - utils.postSystemMessageWithFallback(msg.channel, thread, 'User not found!'); + utils.postSystemMessageWithFallback(msg.channel, thread, "User not found!"); return; } @@ -18,7 +18,7 @@ module.exports = ({ bot, knex, config, commands }) => { const createdThread = await threads.createNewThreadForUser(user, { quiet: true, ignoreRequirements: true, - source: 'command', + source: "command", }); createdThread.postSystemMessage(`Thread was opened by ${msg.author.username}#${msg.author.discriminator}`); diff --git a/src/modules/reply.js b/src/modules/reply.js index cd0bef2..146469e 100644 --- a/src/modules/reply.js +++ b/src/modules/reply.js @@ -1,71 +1,71 @@ const attachments = require("../data/attachments"); -const utils = require('../utils'); -const config = require('../cfg'); -const Thread = require('../data/Thread'); +const utils = require("../utils"); +const config = require("../cfg"); +const Thread = require("../data/Thread"); 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 - commands.addInboxThreadCommand('reply', '[text$]', async (msg, args, thread) => { + commands.addInboxThreadCommand("reply", "[text$]", async (msg, args, thread) => { if (! args.text && msg.attachments.length === 0) { - utils.postError(msg.channel, 'Text or attachment required'); + utils.postError(msg.channel, "Text or attachment required"); return; } - const replied = await thread.replyToUser(msg.member, args.text || '', msg.attachments, false); + const replied = await thread.replyToUser(msg.member, args.text || "", msg.attachments, false); if (replied) msg.delete(); }, { - aliases: ['r'] + aliases: ["r"] }); // Anonymous replies only show the role, not the username - commands.addInboxThreadCommand('anonreply', '[text$]', async (msg, args, thread) => { + commands.addInboxThreadCommand("anonreply", "[text$]", async (msg, args, thread) => { if (! args.text && msg.attachments.length === 0) { - utils.postError(msg.channel, 'Text or attachment required'); + utils.postError(msg.channel, "Text or attachment required"); return; } - const replied = await thread.replyToUser(msg.member, args.text || '', msg.attachments, true); + const replied = await thread.replyToUser(msg.member, args.text || "", msg.attachments, true); if (replied) msg.delete(); }, { - aliases: ['ar'] + aliases: ["ar"] }); if (config.allowStaffEdit) { - commands.addInboxThreadCommand('edit', ' ', async (msg, args, thread) => { + commands.addInboxThreadCommand("edit", " ", async (msg, args, thread) => { const threadMessage = await thread.findThreadMessageByMessageNumber(args.messageNumber); if (! threadMessage) { - utils.postError(msg.channel, 'Unknown message number'); + utils.postError(msg.channel, "Unknown message number"); return; } if (threadMessage.user_id !== msg.author.id) { - utils.postError(msg.channel, 'You can only edit your own replies'); + utils.postError(msg.channel, "You can only edit your own replies"); return; } await thread.editStaffReply(msg.member, threadMessage, args.text) }, { - aliases: ['e'] + aliases: ["e"] }); } if (config.allowStaffDelete) { - commands.addInboxThreadCommand('delete', '', async (msg, args, thread) => { + commands.addInboxThreadCommand("delete", "", async (msg, args, thread) => { const threadMessage = await thread.findThreadMessageByMessageNumber(args.messageNumber); if (! threadMessage) { - utils.postError(msg.channel, 'Unknown message number'); + utils.postError(msg.channel, "Unknown message number"); return; } if (threadMessage.user_id !== msg.author.id) { - utils.postError(msg.channel, 'You can only delete your own replies'); + utils.postError(msg.channel, "You can only delete your own replies"); return; } await thread.deleteStaffReply(msg.member, threadMessage); }, { - aliases: ['d'] + aliases: ["d"] }); } }; diff --git a/src/modules/snippets.js b/src/modules/snippets.js index 2948849..2a5ae73 100644 --- a/src/modules/snippets.js +++ b/src/modules/snippets.js @@ -1,11 +1,11 @@ -const threads = require('../data/threads'); -const snippets = require('../data/snippets'); -const config = require('../cfg'); -const utils = require('../utils'); -const { parseArguments } = require('knub-command-manager'); +const threads = require("../data/threads"); +const snippets = require("../data/snippets"); +const config = require("../cfg"); +const utils = require("../utils"); +const { parseArguments } = require("knub-command-manager"); const whitespaceRegex = /\s/; -const quoteChars = ["'", '"']; +const quoteChars = ["'", "\""]; module.exports = ({ bot, knex, config, commands }) => { /** @@ -21,13 +21,13 @@ module.exports = ({ bot, knex, config, commands }) => { const index = parseInt(match.slice(1, -1), 10) - 1; return (args[index] != null ? args[index] : match); }) - .replace(/\\{/g, '{'); + .replace(/\\{/g, "{"); } /** * When a staff member uses a snippet (snippet prefix + trigger word), find the snippet and post it as a reply in the thread */ - bot.on('messageCreate', async msg => { + bot.on("messageCreate", async msg => { if (! utils.messageIsOnInboxServer(msg)) return; if (! utils.isStaff(msg.member)) return; @@ -75,7 +75,7 @@ module.exports = ({ bot, knex, config, commands }) => { }); // Show or add a snippet - commands.addInboxServerCommand('snippet', ' [text$]', async (msg, args, thread) => { + commands.addInboxServerCommand("snippet", " [text$]", async (msg, args, thread) => { const snippet = await snippets.get(args.trigger); if (snippet) { @@ -97,10 +97,10 @@ module.exports = ({ bot, knex, config, commands }) => { } } }, { - aliases: ['s'] + aliases: ["s"] }); - commands.addInboxServerCommand('delete_snippet', '', async (msg, args, thread) => { + commands.addInboxServerCommand("delete_snippet", "", async (msg, args, thread) => { const snippet = await snippets.get(args.trigger); if (! snippet) { utils.postSystemMessageWithFallback(msg.channel, thread, `Snippet "${args.trigger}" doesn't exist!`); @@ -110,10 +110,10 @@ module.exports = ({ bot, knex, config, commands }) => { await snippets.del(args.trigger); utils.postSystemMessageWithFallback(msg.channel, thread, `Snippet "${args.trigger}" deleted!`); }, { - aliases: ['ds'] + aliases: ["ds"] }); - commands.addInboxServerCommand('edit_snippet', ' [text$]', async (msg, args, thread) => { + commands.addInboxServerCommand("edit_snippet", " [text$]", async (msg, args, thread) => { const snippet = await snippets.get(args.trigger); if (! snippet) { utils.postSystemMessageWithFallback(msg.channel, thread, `Snippet "${args.trigger}" doesn't exist!`); @@ -125,14 +125,14 @@ module.exports = ({ bot, knex, config, commands }) => { utils.postSystemMessageWithFallback(msg.channel, thread, `Snippet "${args.trigger}" edited!`); }, { - aliases: ['es'] + aliases: ["es"] }); - commands.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(); - utils.postSystemMessageWithFallback(msg.channel, thread, `Available snippets (prefix ${config.snippetPrefix}):\n${triggers.join(', ')}`); + utils.postSystemMessageWithFallback(msg.channel, thread, `Available snippets (prefix ${config.snippetPrefix}):\n${triggers.join(", ")}`); }); }; diff --git a/src/modules/suspend.js b/src/modules/suspend.js index 61d3708..bddb756 100644 --- a/src/modules/suspend.js +++ b/src/modules/suspend.js @@ -1,9 +1,9 @@ -const moment = require('moment'); +const moment = require("moment"); const threads = require("../data/threads"); -const utils = require('../utils'); -const config = require('../cfg'); +const utils = require("../utils"); +const config = require("../cfg"); -const {THREAD_STATUS} = require('../data/constants'); +const {THREAD_STATUS} = require("../data/constants"); module.exports = ({ bot, knex, config, commands }) => { // Check for threads that are scheduled to be suspended and suspend them @@ -29,20 +29,20 @@ module.exports = ({ bot, knex, config, commands }) => { scheduledSuspendLoop(); - commands.addInboxThreadCommand('suspend cancel', [], async (msg, args, thread) => { + commands.addInboxThreadCommand("suspend cancel", [], async (msg, args, thread) => { // Cancel timed suspend if (thread.scheduled_suspend_at) { await thread.cancelScheduledSuspend(); - thread.postSystemMessage(`Cancelled scheduled suspension`); + thread.postSystemMessage("Cancelled scheduled suspension"); } else { - thread.postSystemMessage(`Thread is not scheduled to be suspended`); + thread.postSystemMessage("Thread is not scheduled to be suspended"); } }); - commands.addInboxThreadCommand('suspend', '[delay:delay]', async (msg, args, thread) => { + 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); + const suspendAt = moment.utc().add(args.delay, "ms"); + await thread.scheduleSuspend(suspendAt.format("YYYY-MM-DD HH:mm:ss"), msg.author); thread.postSystemMessage(`Thread will be suspended in ${utils.humanizeDelay(args.delay)}. Use \`${config.prefix}suspend cancel\` to cancel.`); @@ -50,18 +50,18 @@ module.exports = ({ bot, knex, config, commands }) => { } await thread.suspend(); - thread.postSystemMessage(`**Thread suspended!** This thread will act as closed until unsuspended with \`!unsuspend\``); + thread.postSystemMessage("**Thread suspended!** This thread will act as closed until unsuspended with `!unsuspend`"); }); - commands.addInboxServerCommand('unsuspend', [], async (msg, args, thread) => { + commands.addInboxServerCommand("unsuspend", [], async (msg, args, thread) => { if (thread) { - thread.postSystemMessage(`Thread is not suspended`); + thread.postSystemMessage("Thread is not suspended"); return; } thread = await threads.findSuspendedThreadByChannelId(msg.channel.id); if (! thread) { - msg.channel.createMessage(`Not in a thread`); + msg.channel.createMessage("Not in a thread"); return; } @@ -72,6 +72,6 @@ module.exports = ({ bot, knex, config, commands }) => { } await thread.unsuspend(); - thread.postSystemMessage(`**Thread unsuspended!**`); + thread.postSystemMessage("**Thread unsuspended!**"); }); }; diff --git a/src/modules/typingProxy.js b/src/modules/typingProxy.js index d2e585e..cdcc97a 100644 --- a/src/modules/typingProxy.js +++ b/src/modules/typingProxy.js @@ -1,4 +1,4 @@ -const config = require('../cfg'); +const config = require("../cfg"); const threads = require("../data/threads"); const Eris = require("eris"); diff --git a/src/modules/version.js b/src/modules/version.js index 0411cf0..b198297 100644 --- a/src/modules/version.js +++ b/src/modules/version.js @@ -1,18 +1,18 @@ -const path = require('path'); -const fs = require('fs'); -const {promisify} = require('util'); +const path = require("path"); +const fs = require("fs"); +const {promisify} = require("util"); const utils = require("../utils"); -const updates = require('../data/updates'); -const config = require('../cfg'); +const updates = require("../data/updates"); +const config = require("../cfg"); const access = promisify(fs.access); const readFile = promisify(fs.readFile); -const GIT_DIR = path.join(__dirname, '..', '..', '.git'); +const GIT_DIR = path.join(__dirname, "..", "..", ".git"); module.exports = ({ bot, knex, config, commands }) => { - commands.addInboxServerCommand('version', [], async (msg, args, thread) => { - const packageJson = require('../../package.json'); + commands.addInboxServerCommand("version", [], async (msg, args, thread) => { + const packageJson = require("../../package.json"); const packageVersion = packageJson.version; let response = `Modmail v${packageVersion}`; @@ -27,12 +27,12 @@ module.exports = ({ bot, knex, config, commands }) => { if (isGit) { let commitHash; - const HEAD = await readFile(path.join(GIT_DIR, 'HEAD'), {encoding: 'utf8'}); + const HEAD = await readFile(path.join(GIT_DIR, "HEAD"), {encoding: "utf8"}); - if (HEAD.startsWith('ref:')) { + if (HEAD.startsWith("ref:")) { // Branch const ref = HEAD.match(/^ref: (.*)$/m)[1]; - commitHash = (await readFile(path.join(GIT_DIR, ref), {encoding: 'utf8'})).trim(); + commitHash = (await readFile(path.join(GIT_DIR, ref), {encoding: "utf8"})).trim(); } else { // Detached head commitHash = HEAD.trim(); diff --git a/src/modules/webserver.js b/src/modules/webserver.js index 22dff34..9f22f47 100644 --- a/src/modules/webserver.js +++ b/src/modules/webserver.js @@ -1,18 +1,18 @@ -const http = require('http'); -const mime = require('mime'); -const url = require('url'); -const fs = require('fs'); -const qs = require('querystring'); -const moment = require('moment'); -const config = require('../cfg'); -const threads = require('../data/threads'); -const attachments = require('../data/attachments'); +const http = require("http"); +const mime = require("mime"); +const url = require("url"); +const fs = require("fs"); +const qs = require("querystring"); +const moment = require("moment"); +const config = require("../cfg"); +const threads = require("../data/threads"); +const attachments = require("../data/attachments"); -const {THREAD_MESSAGE_TYPE} = require('../data/constants'); +const {THREAD_MESSAGE_TYPE} = require("../data/constants"); function notfound(res) { res.statusCode = 404; - res.end('Page Not Found'); + res.end("Page Not Found"); } async function serveLogs(req, res, pathParts, query) { @@ -41,7 +41,7 @@ async function serveLogs(req, res, pathParts, query) { return message.body; } - let line = `[${moment.utc(message.created_at).format('YYYY-MM-DD HH:mm:ss')}]`; + let line = `[${moment.utc(message.created_at).format("YYYY-MM-DD HH:mm:ss")}]`; if (query.verbose) { if (message.dm_channel_id) { @@ -72,12 +72,12 @@ async function serveLogs(req, res, pathParts, query) { return line; }); - const openedAt = moment(thread.created_at).format('YYYY-MM-DD HH:mm:ss'); + const openedAt = moment(thread.created_at).format("YYYY-MM-DD HH:mm:ss"); const header = `# Modmail thread with ${thread.user_name} (${thread.user_id}) started at ${openedAt}. All times are in UTC+0.`; - const fullResponse = header + '\n\n' + lines.join('\n'); + const fullResponse = header + "\n\n" + lines.join("\n"); - res.setHeader('Content-Type', 'text/plain; charset=UTF-8'); + res.setHeader("Content-Type", "text/plain; charset=UTF-8"); res.end(fullResponse); } @@ -92,11 +92,11 @@ function serveAttachments(req, res, pathParts) { fs.access(attachmentPath, (err) => { if (err) return notfound(res); - const filenameParts = desiredFilename.split('.'); - const ext = (filenameParts.length > 1 ? filenameParts[filenameParts.length - 1] : 'bin'); + const filenameParts = desiredFilename.split("."); + const ext = (filenameParts.length > 1 ? filenameParts[filenameParts.length - 1] : "bin"); const fileMime = mime.getType(ext); - res.setHeader('Content-Type', fileMime); + res.setHeader("Content-Type", fileMime); const read = fs.createReadStream(attachmentPath); read.pipe(res); @@ -106,20 +106,20 @@ function serveAttachments(req, res, pathParts) { module.exports = () => { const server = http.createServer((req, res) => { const parsedUrl = url.parse(`http://${req.url}`); - const pathParts = parsedUrl.pathname.split('/').filter(v => v !== ''); + const pathParts = parsedUrl.pathname.split("/").filter(v => v !== ""); const query = qs.parse(parsedUrl.query); - if (parsedUrl.pathname.startsWith('/logs/')) { + if (parsedUrl.pathname.startsWith("/logs/")) { serveLogs(req, res, pathParts, query); - } else if (parsedUrl.pathname.startsWith('/attachments/')) { + } else if (parsedUrl.pathname.startsWith("/attachments/")) { serveAttachments(req, res, pathParts, query); } else { notfound(res); } }); - server.on('error', err => { - console.log('[WARN] Web server error:', err.message); + server.on("error", err => { + console.log("[WARN] Web server error:", err.message); }); server.listen(config.port); diff --git a/src/plugins.js b/src/plugins.js index b078dbb..d03c2ae 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -1,6 +1,6 @@ -const attachments = require('./data/attachments'); -const { beforeNewThread } = require('./hooks/beforeNewThread'); -const formats = require('./formatters'); +const attachments = require("./data/attachments"); +const { beforeNewThread } = require("./hooks/beforeNewThread"); +const formats = require("./formatters"); module.exports = { getPluginAPI({ bot, knex, config, commands }) { diff --git a/src/utils.js b/src/utils.js index 4c5d08d..791ca67 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,9 +1,9 @@ -const Eris = require('eris'); -const bot = require('./bot'); -const moment = require('moment'); -const humanizeDuration = require('humanize-duration'); -const publicIp = require('public-ip'); -const config = require('./cfg'); +const Eris = require("eris"); +const bot = require("./bot"); +const moment = require("moment"); +const humanizeDuration = require("humanize-duration"); +const publicIp = require("public-ip"); +const config = require("./cfg"); class BotError extends Error {} @@ -18,7 +18,7 @@ let logChannel = null; */ function getInboxGuild() { if (! inboxGuild) inboxGuild = bot.guilds.find(g => g.id === config.mailGuildId); - if (! inboxGuild) throw new BotError('The bot is not on the modmail (inbox) server!'); + if (! inboxGuild) throw new BotError("The bot is not on the modmail (inbox) server!"); return inboxGuild; } @@ -32,9 +32,9 @@ function getMainGuilds() { if (mainGuilds.length !== config.mainGuildId.length) { if (config.mainGuildId.length === 1) { - console.warn(`[WARN] The bot hasn't joined the main guild!`); + console.warn("[WARN] The bot hasn't joined the main guild!"); } else { - console.warn(`[WARN] The bot hasn't joined one or more main guilds!`); + console.warn("[WARN] The bot hasn't joined one or more main guilds!"); } } @@ -50,11 +50,11 @@ function getLogChannel() { const logChannel = inboxGuild.channels.get(config.logChannelId); if (! logChannel) { - throw new BotError('Log channel (logChannelId) not found!'); + throw new BotError("Log channel (logChannelId) not found!"); } if (! (logChannel instanceof Eris.TextChannel)) { - throw new BotError('Make sure the logChannelId option is set to a text channel!'); + throw new BotError("Make sure the logChannelId option is set to a text channel!"); } return logChannel; @@ -155,7 +155,7 @@ function getUserMention(str) { * @returns {String} */ function getTimestamp(...momentArgs) { - return moment.utc(...momentArgs).format('HH:mm'); + return moment.utc(...momentArgs).format("HH:mm"); } /** @@ -164,7 +164,7 @@ function getTimestamp(...momentArgs) { * @returns {String} */ function disableLinkPreviews(str) { - return str.replace(/(^|[^<])(https?:\/\/\S+)/ig, '$1<$2>'); + return str.replace(/(^|[^<])(https?:\/\/\S+)/ig, "$1<$2>"); } /** @@ -172,7 +172,7 @@ function disableLinkPreviews(str) { * @param {String} path * @returns {Promise} */ -async function getSelfUrl(path = '') { +async function getSelfUrl(path = "") { if (config.url) { return `${config.url}/${path}`; } else { @@ -216,9 +216,9 @@ function chunk(items, chunkSize) { */ function trimAll(str) { return str - .split('\n') + .split("\n") .map(str => str.trim()) - .join('\n'); + .join("\n"); } const delayStringRegex = /^([0-9]+)(?:([dhms])[a-z]*)?/i; @@ -234,17 +234,17 @@ function convertDelayStringToMS(str) { str = str.trim(); - while (str !== '' && (match = str.match(delayStringRegex)) !== null) { - if (match[2] === 'd') ms += match[1] * 1000 * 60 * 60 * 24; - else if (match[2] === 'h') ms += match[1] * 1000 * 60 * 60; - else if (match[2] === 's') ms += match[1] * 1000; - else if (match[2] === 'm' || ! match[2]) ms += match[1] * 1000 * 60; + while (str !== "" && (match = str.match(delayStringRegex)) !== null) { + if (match[2] === "d") ms += match[1] * 1000 * 60 * 60 * 24; + else if (match[2] === "h") ms += match[1] * 1000 * 60 * 60; + else if (match[2] === "s") ms += match[1] * 1000; + else if (match[2] === "m" || ! match[2]) ms += match[1] * 1000 * 60; str = str.slice(match[0].length); } // Invalid delay string - if (str !== '') { + if (str !== "") { return null; } @@ -256,11 +256,11 @@ function getInboxMention() { const mentions = []; for (const role of mentionRoles) { if (role == null) continue; - else if (role === 'here') mentions.push('@here'); - else if (role === 'everyone') mentions.push('@everyone'); + else if (role === "here") mentions.push("@here"); + else if (role === "everyone") mentions.push("@everyone"); else mentions.push(`<@&${role}>`); } - return mentions.join(' ') + ' '; + return mentions.join(" ") + " "; } function postSystemMessageWithFallback(channel, thread, text) { @@ -286,7 +286,7 @@ function setDataModelProps(target, props) { target[prop] = null; } else { // Set the value as a string in the same format it's returned in SQLite - target[prop] = moment.utc(props[prop]).format('YYYY-MM-DD HH:mm:ss'); + target[prop] = moment.utc(props[prop]).format("YYYY-MM-DD HH:mm:ss"); } } else { target[prop] = props[prop]; @@ -299,11 +299,11 @@ function isSnowflake(str) { return str && snowflakeRegex.test(str); } -const humanizeDelay = (delay, opts = {}) => humanizeDuration(delay, Object.assign({conjunction: ' and '}, opts)); +const humanizeDelay = (delay, opts = {}) => humanizeDuration(delay, Object.assign({conjunction: " and "}, opts)); const markdownCharsRegex = /([\\_*|`~])/g; function escapeMarkdown(str) { - return str.replace(markdownCharsRegex, '\\$1'); + return str.replace(markdownCharsRegex, "\\$1"); } function disableCodeBlocks(str) { @@ -314,7 +314,7 @@ function disableCodeBlocks(str) { * */ function readMultilineConfigValue(str) { - return Array.isArray(str) ? str.join('\n') : str; + return Array.isArray(str) ? str.join("\n") : str; } module.exports = { From 4fb5152d277b303e7475d613b695320013baee9f Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 13 Aug 2020 00:10:32 +0300 Subject: [PATCH 031/300] Switch from nodemon to node-supervisor node-supervisor is smaller with fewer dependencies and does everything we used nodemon for --- package-lock.json | 604 +--------------------------------------------- package.json | 4 +- 2 files changed, 10 insertions(+), 598 deletions(-) diff --git a/package-lock.json b/package-lock.json index cf953bf..f58bb4a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -94,15 +94,6 @@ } } }, - "ansi-align": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz", - "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=", - "dev": true, - "requires": { - "string-width": "^2.0.0" - } - }, "ansi-colors": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", @@ -131,16 +122,6 @@ "color-convert": "^1.9.0" } }, - "anymatch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", - "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, "aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", @@ -320,12 +301,6 @@ } } }, - "binary-extensions": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", - "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==", - "dev": true - }, "block-stream": { "version": "0.0.9", "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", @@ -340,29 +315,6 @@ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" }, - "boxen": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", - "integrity": "sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==", - "dev": true, - "requires": { - "ansi-align": "^2.0.0", - "camelcase": "^4.0.0", - "chalk": "^2.0.1", - "cli-boxes": "^1.0.0", - "string-width": "^2.0.0", - "term-size": "^1.2.0", - "widest-line": "^2.0.0" - }, - "dependencies": { - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", - "dev": true - } - } - }, "brace-expansion": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", @@ -455,12 +407,6 @@ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" }, - "capture-stack-trace": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz", - "integrity": "sha512-mYQLZnx5Qt1JgB1WEiMCf2647plpGeQ2NMR/5L0HNZzGQo4fuSPnK+wjfPnKZV0aiJDgzmWqqkV/g7JD+DW0qw==", - "dev": true - }, "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", @@ -484,68 +430,11 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, - "chokidar": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.0.tgz", - "integrity": "sha512-dGmKLDdT3Gdl7fBUe8XK+gAtGmzy5Fn0XkkWQuYxGIgWVPPse2CxFA5mtrlD0TOHaHjEUqkWNyP1XdHoJES/4A==", - "dev": true, - "requires": { - "anymatch": "~3.1.1", - "braces": "~3.0.2", - "fsevents": "~2.1.1", - "glob-parent": "~5.1.0", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.2.0" - }, - "dependencies": { - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - } - } - }, "chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, - "ci-info": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", - "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==", - "dev": true - }, "class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", @@ -573,12 +462,6 @@ "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", "dev": true }, - "cli-boxes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", - "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=", - "dev": true - }, "cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -795,20 +678,6 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, - "configstore": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.2.tgz", - "integrity": "sha512-vtv5HtGjcYUgFrXc6Kx747B83MRRVS5R1VTEQoXvuP+kMI+if6uywV0nDGoiydJRy4yk7h9od5Og0kxx4zUXmw==", - "dev": true, - "requires": { - "dot-prop": "^4.1.0", - "graceful-fs": "^4.1.2", - "make-dir": "^1.0.0", - "unique-string": "^1.0.0", - "write-file-atomic": "^2.0.0", - "xdg-basedir": "^3.0.0" - } - }, "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", @@ -837,32 +706,6 @@ "yaml": "^1.7.2" } }, - "create-error-class": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", - "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", - "dev": true, - "requires": { - "capture-stack-trace": "^1.0.0" - } - }, - "cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", - "dev": true, - "requires": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "crypto-random-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", - "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=", - "dev": true - }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -1003,15 +846,6 @@ "esutils": "^2.0.2" } }, - "dot-prop": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", - "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", - "dev": true, - "requires": { - "is-obj": "^1.0.0" - } - }, "duplexer3": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", @@ -1242,21 +1076,6 @@ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true }, - "execa": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", - "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", - "dev": true, - "requires": { - "cross-spawn": "^5.0.1", - "get-stream": "^3.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, "expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", @@ -1595,13 +1414,6 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, - "fsevents": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.2.tgz", - "integrity": "sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==", - "dev": true, - "optional": true - }, "fstream": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", @@ -1658,12 +1470,6 @@ "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", "dev": true }, - "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", - "dev": true - }, "get-value": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", @@ -1706,15 +1512,6 @@ "is-glob": "^4.0.1" } }, - "global-dirs": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", - "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", - "dev": true, - "requires": { - "ini": "^1.3.4" - } - }, "global-modules": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", @@ -1777,7 +1574,8 @@ "graceful-fs": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", - "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==" + "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", + "optional": true }, "har-schema": { "version": "2.0.0", @@ -1960,12 +1758,6 @@ "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", "dev": true }, - "ignore-by-default": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=", - "dev": true - }, "ignore-walk": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", @@ -1984,12 +1776,6 @@ "resolve-from": "^4.0.0" } }, - "import-lazy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", - "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", - "dev": true - }, "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -2143,29 +1929,11 @@ "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", "dev": true }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "requires": { - "binary-extensions": "^2.0.0" - } - }, "is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" }, - "is-ci": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz", - "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", - "dev": true, - "requires": { - "ci-info": "^1.5.0" - } - }, "is-data-descriptor": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", @@ -2227,16 +1995,6 @@ "is-extglob": "^2.1.1" } }, - "is-installed-globally": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.1.0.tgz", - "integrity": "sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=", - "dev": true, - "requires": { - "global-dirs": "^0.1.0", - "is-path-inside": "^1.0.0" - } - }, "is-ip": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-3.1.0.tgz", @@ -2245,12 +2003,6 @@ "ip-regex": "^4.0.0" } }, - "is-npm": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", - "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=", - "dev": true - }, "is-number": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", @@ -2275,15 +2027,6 @@ "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", "dev": true }, - "is-path-inside": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", - "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", - "dev": true, - "requires": { - "path-is-inside": "^1.0.1" - } - }, "is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -2298,12 +2041,6 @@ "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", "dev": true }, - "is-redirect": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", - "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=", - "dev": true - }, "is-regexp": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", @@ -2318,18 +2055,6 @@ "is-unc-path": "^1.0.0" } }, - "is-retry-allowed": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", - "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==", - "dev": true - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true - }, "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", @@ -2536,15 +2261,6 @@ } } }, - "latest-version": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", - "integrity": "sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=", - "dev": true, - "requires": { - "package-json": "^4.0.0" - } - }, "levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", @@ -3048,25 +2764,6 @@ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==" }, - "lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "dev": true, - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", - "dev": true, - "requires": { - "pify": "^3.0.0" - } - }, "make-iterator": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", @@ -3386,41 +3083,6 @@ } } }, - "nodemon": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.1.tgz", - "integrity": "sha512-UC6FVhNLXjbbV4UzaXA3wUdbEkUZzLGgMGzmxvWAex5nzib/jhcSHVFlQODdbuUHq8SnnZ4/EABBAbC3RplvPg==", - "dev": true, - "requires": { - "chokidar": "^3.2.2", - "debug": "^3.2.6", - "ignore-by-default": "^1.0.1", - "minimatch": "^3.0.4", - "pstree.remy": "^1.1.7", - "semver": "^5.7.1", - "supports-color": "^5.5.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.2", - "update-notifier": "^2.5.0" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, "nopt": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", @@ -3464,15 +3126,6 @@ "npm-normalize-package-bin": "^1.0.1" } }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true, - "requires": { - "path-key": "^2.0.0" - } - }, "npmlog": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", @@ -3639,12 +3292,6 @@ "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==" }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true - }, "p-limit": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz", @@ -3675,54 +3322,6 @@ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" }, - "package-json": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz", - "integrity": "sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=", - "dev": true, - "requires": { - "got": "^6.7.1", - "registry-auth-token": "^3.0.1", - "registry-url": "^3.0.3", - "semver": "^5.1.0" - }, - "dependencies": { - "got": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", - "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", - "dev": true, - "requires": { - "create-error-class": "^3.0.0", - "duplexer3": "^0.1.4", - "get-stream": "^3.0.0", - "is-redirect": "^1.0.0", - "is-retry-allowed": "^1.0.0", - "is-stream": "^1.0.0", - "lowercase-keys": "^1.0.0", - "safe-buffer": "^5.0.1", - "timed-out": "^4.0.0", - "unzip-response": "^2.0.1", - "url-parse-lax": "^1.0.0" - } - }, - "prepend-http": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", - "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", - "dev": true - }, - "url-parse-lax": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", - "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", - "dev": true, - "requires": { - "prepend-http": "^1.0.1" - } - } - } - }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -3774,12 +3373,6 @@ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, - "path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", - "dev": true - }, "path-key": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", @@ -3827,12 +3420,6 @@ "integrity": "sha512-OYMyqkKzK7blWO/+XZYP6w8hH0LDvkBvdvKukti+7kqYFCiEAk+gI3DWnryapc0Dau05ugGTy0foQ6mqn4AHYA==", "dev": true }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - }, "pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -3914,24 +3501,12 @@ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true }, - "pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", - "dev": true - }, "psl": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", "optional": true }, - "pstree.remy": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.7.tgz", - "integrity": "sha512-xsMgrUwRpuGskEzBFkH8NmTimbZ5PcPup0LA8JJkHIm2IMUbQcpo3yeLNWVrufEYjh8YwtSVh0xz6UeWc5Oh5A==", - "dev": true - }, "public-ip": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/public-ip/-/public-ip-4.0.0.tgz", @@ -3995,15 +3570,6 @@ "util-deprecate": "~1.0.1" } }, - "readdirp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.2.0.tgz", - "integrity": "sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ==", - "dev": true, - "requires": { - "picomatch": "^2.0.4" - } - }, "rechoir": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", @@ -4027,25 +3593,6 @@ "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", "dev": true }, - "registry-auth-token": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.4.0.tgz", - "integrity": "sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A==", - "dev": true, - "requires": { - "rc": "^1.1.6", - "safe-buffer": "^5.0.1" - } - }, - "registry-url": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", - "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", - "dev": true, - "requires": { - "rc": "^1.0.1" - } - }, "repeat-element": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", @@ -4220,15 +3767,6 @@ "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=", "dev": true }, - "semver-diff": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", - "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=", - "dev": true, - "requires": { - "semver": "^5.0.3" - } - }, "semver-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-2.0.0.tgz", @@ -4568,12 +4106,6 @@ "ansi-regex": "^2.0.0" } }, - "strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true - }, "strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", @@ -4585,6 +4117,12 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" }, + "supervisor": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/supervisor/-/supervisor-0.12.0.tgz", + "integrity": "sha1-3n5jNwFbKRhRwQ81OMSn8EkX7ME=", + "dev": true + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -4674,15 +4212,6 @@ "resolved": "https://registry.npmjs.org/tarn/-/tarn-2.0.0.tgz", "integrity": "sha512-7rNMCZd3s9bhQh47ksAQd92ADFcJUjjbyOvyFjNLwTPpGieFHMC84S+LOzw0fx1uh6hnDz/19r8CPMnIjJlMMA==" }, - "term-size": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", - "integrity": "sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=", - "dev": true, - "requires": { - "execa": "^0.7.0" - } - }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -4700,12 +4229,6 @@ "resolved": "https://registry.npmjs.org/tildify/-/tildify-2.0.0.tgz", "integrity": "sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==" }, - "timed-out": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", - "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", - "dev": true - }, "tmp": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.1.0.tgz", @@ -4757,26 +4280,6 @@ "repeat-string": "^1.6.1" } }, - "touch": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", - "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", - "dev": true, - "requires": { - "nopt": "~1.0.10" - }, - "dependencies": { - "nopt": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", - "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", - "dev": true, - "requires": { - "abbrev": "1" - } - } - } - }, "tough-cookie": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", @@ -4836,32 +4339,6 @@ "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=" }, - "undefsafe": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.2.tgz", - "integrity": "sha1-Il9rngM3Zj4Njnz9aG/Cg2zKznY=", - "dev": true, - "requires": { - "debug": "^2.2.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, "union-value": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", @@ -4873,15 +4350,6 @@ "set-value": "^2.0.1" } }, - "unique-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", - "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", - "dev": true, - "requires": { - "crypto-random-string": "^1.0.0" - } - }, "unset-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", @@ -4918,30 +4386,6 @@ } } }, - "unzip-response": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", - "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=", - "dev": true - }, - "update-notifier": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-2.5.0.tgz", - "integrity": "sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==", - "dev": true, - "requires": { - "boxen": "^1.2.1", - "chalk": "^2.0.1", - "configstore": "^3.0.0", - "import-lazy": "^2.1.0", - "is-ci": "^1.0.10", - "is-installed-globally": "^0.1.0", - "is-npm": "^1.0.0", - "latest-version": "^3.0.0", - "semver-diff": "^2.0.0", - "xdg-basedir": "^3.0.0" - } - }, "uri-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", @@ -5037,15 +4481,6 @@ "string-width": "^1.0.2 || 2" } }, - "widest-line": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz", - "integrity": "sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA==", - "dev": true, - "requires": { - "string-width": "^2.1.1" - } - }, "word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", @@ -5111,17 +4546,6 @@ "mkdirp": "^0.5.1" } }, - "write-file-atomic": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", - "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.2" - } - }, "ws": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/ws/-/ws-7.2.0.tgz", @@ -5130,23 +4554,11 @@ "async-limiter": "^1.0.0" } }, - "xdg-basedir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", - "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=", - "dev": true - }, "y18n": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", - "dev": true - }, "yaml": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz", diff --git a/package.json b/package.json index 441d6da..78a31f1 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "main": "src/index.js", "scripts": { "start": "node src/index.js", - "watch": "nodemon -w src src/index.js", + "watch": "supervisor -n exit -w src src/index.js", "test": "echo \"Error: no test specified\" && exit 1", "lint": "eslint ./src", "lint-fix": "eslint --fix ./src", @@ -38,7 +38,7 @@ "eslint": "^6.7.2", "husky": "^4.2.5", "lint-staged": "^10.2.11", - "nodemon": "^2.0.1" + "supervisor": "^0.12.0" }, "engines": { "node": ">=10.0.0 <14.0.0" From 38d5d193aa2951a0d724a9c357bfed42a7fb21c3 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 13 Aug 2020 00:12:07 +0300 Subject: [PATCH 032/300] Update eslint to v7.6.0 --- package-lock.json | 481 ++++++++++++++++++---------------------------- package.json | 2 +- 2 files changed, 185 insertions(+), 298 deletions(-) diff --git a/package-lock.json b/package-lock.json index f58bb4a..6683e70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,15 +55,15 @@ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, "acorn": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.0.tgz", - "integrity": "sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.0.tgz", + "integrity": "sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==", "dev": true }, "acorn-jsx": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.1.0.tgz", - "integrity": "sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz", + "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", "dev": true }, "aggregate-error": { @@ -424,12 +424,6 @@ "supports-color": "^5.3.0" } }, - "chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", - "dev": true - }, "chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", @@ -557,12 +551,6 @@ } } }, - "cli-width": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", - "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", - "dev": true - }, "cliui": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", @@ -706,6 +694,28 @@ "yaml": "^1.7.2" } }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "dependencies": { + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -910,22 +920,23 @@ "dev": true }, "eslint": { - "version": "6.7.2", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.7.2.tgz", - "integrity": "sha512-qMlSWJaCSxDFr8fBPvJM9kJwbazrhNcBU3+DszDW1OlEwKBBRWsJc7NJFelvwQpanHCR14cOLD41x8Eqvo3Nng==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.6.0.tgz", + "integrity": "sha512-QlAManNtqr7sozWm5TF4wIH9gmUm2hE3vNRUvyoYAa4y1l5/jxD/PQStEjBMQtCqZmSep8UxrcecI60hOpe61w==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", "ajv": "^6.10.0", - "chalk": "^2.1.0", - "cross-spawn": "^6.0.5", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", "debug": "^4.0.1", "doctrine": "^3.0.0", - "eslint-scope": "^5.0.0", - "eslint-utils": "^1.4.3", - "eslint-visitor-keys": "^1.1.0", - "espree": "^6.1.2", - "esquery": "^1.0.1", + "enquirer": "^2.3.5", + "eslint-scope": "^5.1.0", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^1.3.0", + "espree": "^7.2.0", + "esquery": "^1.2.0", "esutils": "^2.0.2", "file-entry-cache": "^5.0.1", "functional-red-black-tree": "^1.0.1", @@ -934,80 +945,107 @@ "ignore": "^4.0.6", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", - "inquirer": "^7.0.0", "is-glob": "^4.0.0", "js-yaml": "^3.13.1", "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.3.0", - "lodash": "^4.17.14", + "levn": "^0.4.1", + "lodash": "^4.17.19", "minimatch": "^3.0.4", - "mkdirp": "^0.5.1", "natural-compare": "^1.4.0", - "optionator": "^0.8.3", + "optionator": "^0.9.1", "progress": "^2.0.0", - "regexpp": "^2.0.1", - "semver": "^6.1.2", - "strip-ansi": "^5.2.0", - "strip-json-comments": "^3.0.1", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", "table": "^5.2.3", "text-table": "^0.2.0", "v8-compile-cache": "^2.0.3" }, "dependencies": { "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", "dev": true }, - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", "dev": true, "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" } }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", "dev": true }, "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", "dev": true, "requires": { - "ansi-regex": "^4.1.0" + "ansi-regex": "^5.0.0" } }, "strip-json-comments": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", - "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } } } }, "eslint-scope": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", - "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.0.tgz", + "integrity": "sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w==", "dev": true, "requires": { "esrecurse": "^4.1.0", @@ -1015,29 +1053,29 @@ } }, "eslint-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", - "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", "dev": true, "requires": { "eslint-visitor-keys": "^1.1.0" } }, "eslint-visitor-keys": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz", - "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", "dev": true }, "espree": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/espree/-/espree-6.1.2.tgz", - "integrity": "sha512-2iUPuuPP+yW1PZaMSDM9eyVf8D5P0Hi8h83YtZ5bPc/zHYjII5khoixIUTMO794NOY8F/ThF1Bo8ncZILarUTA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.2.0.tgz", + "integrity": "sha512-H+cQ3+3JYRMEIOl87e7QdHX70ocly5iW4+dttuR8iYSPr/hXKFb+7dBsZ7+u1adC4VrnPlTkv0+OwuPnDop19g==", "dev": true, "requires": { - "acorn": "^7.1.0", - "acorn-jsx": "^5.1.0", - "eslint-visitor-keys": "^1.1.0" + "acorn": "^7.3.1", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.3.0" } }, "esprima": { @@ -1047,12 +1085,20 @@ "dev": true }, "esquery": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", - "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", + "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", "dev": true, "requires": { - "estraverse": "^4.0.0" + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } } }, "esrecurse": { @@ -1153,28 +1199,6 @@ } } }, - "external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "dev": true, - "requires": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - }, - "dependencies": { - "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "requires": { - "os-tmpdir": "~1.0.2" - } - } - } - }, "extglob": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", @@ -1240,12 +1264,6 @@ "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", "optional": true }, - "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", - "dev": true - }, "fast-json-stable-stringify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", @@ -1257,15 +1275,6 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, - "figures": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.1.0.tgz", - "integrity": "sha512-ravh8VRXqHuMvZt/d8GblBeqDMkdJMBdv/2KntFH+ra5MXkO7nxNKpzQ3n6QD/2da1kH0aWmNISdvhM7gl2gVg==", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, "file-entry-cache": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", @@ -1353,9 +1362,9 @@ } }, "flatted": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.1.tgz", - "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", "dev": true }, "for-in": { @@ -1504,9 +1513,9 @@ } }, "glob-parent": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz", - "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", "dev": true, "requires": { "is-glob": "^4.0.1" @@ -1535,9 +1544,9 @@ } }, "globals": { - "version": "12.3.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-12.3.0.tgz", - "integrity": "sha512-wAfjdLgFsPZsklLJvOBUBmzYE8/CwhEqSBEMRXA3qxIiNtyqvjYurAtIfDh6chlEPUfmTY3MnZh5Hfh4q0UlIw==", + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", "dev": true, "requires": { "type-fest": "^0.8.1" @@ -1807,80 +1816,6 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" }, - "inquirer": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.0.tgz", - "integrity": "sha512-rSdC7zelHdRQFkWnhsMu2+2SO41mpv2oF2zy4tMhmiLWkcKbOAs87fWAJhVXttKVwhdZvymvnuM95EyEXg2/tQ==", - "dev": true, - "requires": { - "ansi-escapes": "^4.2.1", - "chalk": "^2.4.2", - "cli-cursor": "^3.1.0", - "cli-width": "^2.0.0", - "external-editor": "^3.0.3", - "figures": "^3.0.0", - "lodash": "^4.17.15", - "mute-stream": "0.0.8", - "run-async": "^2.2.0", - "rxjs": "^6.4.0", - "string-width": "^4.1.0", - "strip-ansi": "^5.1.0", - "through": "^2.3.6" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - } - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - } - } - } - } - }, "interpret": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.0.0.tgz", @@ -2035,12 +1970,6 @@ "isobject": "^3.0.1" } }, - "is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", - "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", - "dev": true - }, "is-regexp": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", @@ -2102,9 +2031,9 @@ "dev": true }, "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", + "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", "dev": true, "requires": { "argparse": "^1.0.7", @@ -2262,13 +2191,13 @@ } }, "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" } }, "liftoff": { @@ -2594,9 +2523,9 @@ } }, "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", "dev": true }, "log-symbols": { @@ -2910,12 +2839,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, - "mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", - "dev": true - }, "mv": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", @@ -2997,12 +2920,6 @@ } } }, - "nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, "node-addon-api": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.0.tgz", @@ -3249,17 +3166,17 @@ "dev": true }, "optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", "dev": true, "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" } }, "opusscript": { @@ -3374,9 +3291,9 @@ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true }, "path-parse": { @@ -3480,9 +3397,9 @@ "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" }, "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true }, "prepend-http": { @@ -3588,9 +3505,9 @@ } }, "regexpp": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", - "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", + "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", "dev": true }, "repeat-element": { @@ -3715,24 +3632,6 @@ } } }, - "run-async": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", - "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", - "dev": true, - "requires": { - "is-promise": "^2.1.0" - } - }, - "rxjs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.3.tgz", - "integrity": "sha512-wuYsAYYFdWTAnAaPoKGNhfpWwKZbJW+HgAJ+mImp+Epl7BG8oNWBCTyRM8gba9k4lk8BgWdoYm21Mo/RYhhbgA==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - } - }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -3800,18 +3699,18 @@ } }, "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "requires": { - "shebang-regex": "^1.0.0" + "shebang-regex": "^3.0.0" } }, "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true }, "signal-exit": { @@ -4144,18 +4043,6 @@ "string-width": "^3.0.0" }, "dependencies": { - "ajv": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", - "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", - "dev": true, - "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" - } - }, "ansi-regex": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", @@ -4320,12 +4207,12 @@ "optional": true }, "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "requires": { - "prelude-ls": "~1.1.2" + "prelude-ls": "^1.2.1" } }, "type-fest": { @@ -4430,9 +4317,9 @@ "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==" }, "v8-compile-cache": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz", - "integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz", + "integrity": "sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==", "dev": true }, "v8flags": { diff --git a/package.json b/package.json index 78a31f1..8f3f3c5 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "uuid": "^3.3.3" }, "devDependencies": { - "eslint": "^6.7.2", + "eslint": "^7.6.0", "husky": "^4.2.5", "lint-staged": "^10.2.11", "supervisor": "^0.12.0" From 3eef0251436bed281909f87c2cf63115c68c4a6a Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 13 Aug 2020 00:12:58 +0300 Subject: [PATCH 033/300] Update knex to v0.21.4 --- package-lock.json | 109 +++++++++++++++++++++++----------------------- package.json | 2 +- 2 files changed, 55 insertions(+), 56 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6683e70..06f8c0e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -310,11 +310,6 @@ "inherits": "~2.0.0" } }, - "bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" - }, "brace-expansion": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", @@ -632,9 +627,9 @@ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "colorette": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.1.0.tgz", - "integrity": "sha512-6S062WDQUXi6hOfkO/sBPVwE5ASXY4G2+b4atvhJfSsuUUhIaUKlkjLe9692Ipyt5/a+IPF5aVTu3V5gvXq5cg==" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz", + "integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==" }, "combined-stream": { "version": "1.0.8", @@ -646,9 +641,9 @@ } }, "commander": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.0.1.tgz", - "integrity": "sha512-IPF4ouhCP+qdlcmCedhxX4xiGBPyigb8v5NeUp+0LyhwLgxMqyp3S0vl7TAPfS/hiP7FC3caI/PB9lTmP8r1NA==" + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==" }, "compare-versions": { "version": "3.6.0", @@ -1067,6 +1062,11 @@ "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", "dev": true }, + "esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==" + }, "espree": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/espree/-/espree-7.2.0.tgz", @@ -1817,9 +1817,9 @@ "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" }, "interpret": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.0.0.tgz", - "integrity": "sha512-e0/LknJ8wpMMhTiWcjivB+ESwIuvHnBSlBbmP/pSb8CQJldoj1p2qv7xGZ/+BtbTziYRFSz8OsvdbiX45LtYQA==" + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", + "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==" }, "ip": { "version": "1.1.5", @@ -2132,30 +2132,30 @@ } }, "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" }, "knex": { - "version": "0.20.3", - "resolved": "https://registry.npmjs.org/knex/-/knex-0.20.3.tgz", - "integrity": "sha512-zzYO34pSCCYVqRTbCp8xL+Z7fvHQl5anif3Oacu6JaHFDubB7mFGWRRJBNSO3N8Ql4g4CxUgBctaPiliwoOsNA==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/knex/-/knex-0.21.4.tgz", + "integrity": "sha512-vUrR4mJBKWJPouV9C7kqvle9cTpiuuzBWqrQXP7bAv+Ua9oeKkEhhorJwArzcjVrVBojZYPMMtNVliW9B00sTA==", "requires": { - "bluebird": "^3.7.1", - "colorette": "1.1.0", - "commander": "^4.0.1", + "colorette": "1.2.1", + "commander": "^5.1.0", "debug": "4.1.1", + "esm": "^3.2.25", "getopts": "2.2.5", "inherits": "~2.0.4", - "interpret": "^2.0.0", + "interpret": "^2.2.0", "liftoff": "3.1.0", - "lodash": "^4.17.15", - "mkdirp": "^0.5.1", - "pg-connection-string": "2.1.0", - "tarn": "^2.0.0", + "lodash": "^4.17.19", + "mkdirp": "^1.0.4", + "pg-connection-string": "2.3.0", + "tarn": "^3.0.0", "tildify": "2.0.0", - "uuid": "^3.3.3", - "v8flags": "^3.1.3" + "uuid": "^7.0.3", + "v8flags": "^3.2.0" }, "dependencies": { "inherits": { @@ -2163,15 +2163,15 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" }, "uuid": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", - "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==" + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", + "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==" } } }, @@ -2525,8 +2525,7 @@ "lodash": { "version": "4.17.19", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", - "dev": true + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" }, "log-symbols": { "version": "4.0.0", @@ -3327,9 +3326,9 @@ "optional": true }, "pg-connection-string": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.1.0.tgz", - "integrity": "sha512-bhlV7Eq09JrRIvo1eKngpwuqKtJnNhZdpdOlvrPrA4dxqXPjxSrbNrfnIDmTpwMyRszrcV4kU5ZA4mMsQUrjdg==" + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.3.0.tgz", + "integrity": "sha512-ukMTJXLI7/hZIwTW7hGMZJ0Lj0S2XQBCJ4Shv4y1zgQ/vqVea+FLhzywvPj0ujSuofu+yA4MYHGZPTsgjBgJ+w==" }, "picomatch": { "version": "2.1.1", @@ -3559,9 +3558,9 @@ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" }, "resolve": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.13.1.tgz", - "integrity": "sha512-CxqObCX8K8YtAhOBRg+lrcdn+LK+WYOS8tSjqSFbjtrI5PnS63QPhZl4+yKfrU9tdsbMu9Anr/amegT87M9Z6w==", + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", "requires": { "path-parse": "^1.0.6" } @@ -3859,11 +3858,11 @@ "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" }, "source-map-resolve": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", - "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", "requires": { - "atob": "^2.1.1", + "atob": "^2.1.2", "decode-uri-component": "^0.2.0", "resolve-url": "^0.2.1", "source-map-url": "^0.4.0", @@ -4095,9 +4094,9 @@ } }, "tarn": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tarn/-/tarn-2.0.0.tgz", - "integrity": "sha512-7rNMCZd3s9bhQh47ksAQd92ADFcJUjjbyOvyFjNLwTPpGieFHMC84S+LOzw0fx1uh6hnDz/19r8CPMnIjJlMMA==" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.0.tgz", + "integrity": "sha512-PKUnlDFODZueoA8owLehl8vLcgtA8u4dRuVbZc92tspDYZixjJL6TqYOmryf/PfP/EBX+2rgNcrj96NO+RPkdQ==" }, "text-table": { "version": "0.2.0", @@ -4323,9 +4322,9 @@ "dev": true }, "v8flags": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.1.3.tgz", - "integrity": "sha512-amh9CCg3ZxkzQ48Mhcb8iX7xpAfYJgePHxWMQCBWECpOSqJUXgY26ncA61UTV0BkPqfhcy6mzwCIoP4ygxpW8w==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", + "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==", "requires": { "homedir-polyfill": "^1.0.1" } diff --git a/package.json b/package.json index 8f3f3c5..1bdca86 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "ini": "^1.3.5", "json-schema-to-jsdoc": "^1.0.0", "json5": "^2.1.1", - "knex": "^0.20.3", + "knex": "^0.21.4", "knub-command-manager": "^6.1.0", "mime": "^2.4.4", "moment": "^2.24.0", From ba364ae03b465ff66a86e06196eaf324bbca7ca4 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 13 Aug 2020 00:13:58 +0300 Subject: [PATCH 034/300] Update transliteration to v2.1.11 --- package-lock.json | 226 ++++++++++++++++++++++++---------------------- package.json | 2 +- 2 files changed, 118 insertions(+), 110 deletions(-) diff --git a/package-lock.json b/package-lock.json index 06f8c0e..a0e5b3c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,8 +40,7 @@ "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", - "dev": true + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" }, "@types/parse-json": { "version": "4.0.0", @@ -118,6 +117,7 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, "requires": { "color-convert": "^1.9.0" } @@ -547,46 +547,41 @@ } }, "cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" }, "dependencies": { "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" }, "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" } }, "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", "requires": { - "ansi-regex": "^4.1.0" + "ansi-regex": "^5.0.0" } } } @@ -617,6 +612,7 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, "requires": { "color-name": "1.1.3" } @@ -624,7 +620,8 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true }, "colorette": { "version": "1.2.1", @@ -869,8 +866,7 @@ "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "end-of-stream": { "version": "1.4.4", @@ -1306,11 +1302,12 @@ } }, "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "requires": { - "locate-path": "^3.0.0" + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" } }, "find-versions": { @@ -2514,12 +2511,11 @@ } }, "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" + "p-locate": "^4.1.0" } }, "lodash": { @@ -3217,11 +3213,11 @@ } }, "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "requires": { - "p-limit": "^2.0.0" + "p-limit": "^2.2.0" } }, "p-map": { @@ -3280,9 +3276,9 @@ "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" }, "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" }, "path-is-absolute": { "version": "1.0.1", @@ -4177,11 +4173,11 @@ } }, "transliteration": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/transliteration/-/transliteration-2.1.7.tgz", - "integrity": "sha512-o3678GPmKKGqOBB+trAKzhBUjHddU18He2V8AKB1XuegaGJekO0xmfkkvbc9LCBat62nb7IH8z5/OJY+mNugkg==", + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/transliteration/-/transliteration-2.1.11.tgz", + "integrity": "sha512-CMCKB2VHgc9JabQ3NiC2aXG5hEd3FKoU+F+zRQJoDRtZFdQwLYKfRSK8zH/B/4HML4WnOx8U0xmob1ehlt/xvw==", "requires": { - "yargs": "^14.0.0" + "yargs": "^15.3.1" } }, "tslib": { @@ -4374,46 +4370,63 @@ "dev": true }, "wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "dependencies": { "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" } }, "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", "requires": { - "ansi-regex": "^4.1.0" + "ansi-regex": "^5.0.0" } } } @@ -4452,62 +4465,57 @@ "dev": true }, "yargs": { - "version": "14.2.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-14.2.2.tgz", - "integrity": "sha512-/4ld+4VV5RnrynMhPZJ/ZpOCGSCeghMykZ3BhdFBDa9Wy/RH6uEGNWDJog+aUlq+9OM1CFTgtYRW5Is1Po9NOA==", + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", "requires": { - "cliui": "^5.0.0", + "cliui": "^6.0.0", "decamelize": "^1.2.0", - "find-up": "^3.0.0", + "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", - "string-width": "^3.0.0", + "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", - "yargs-parser": "^15.0.0" + "yargs-parser": "^18.1.2" }, "dependencies": { "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" }, "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" } }, "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", "requires": { - "ansi-regex": "^4.1.0" + "ansi-regex": "^5.0.0" } } } }, "yargs-parser": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-15.0.0.tgz", - "integrity": "sha512-xLTUnCMc4JhxrPEPUYD5IBR1mWCK/aT6+RJ/K29JY2y1vD+FhtgKK0AXRWvI262q3QSffAQuTouFIKUuHX89wQ==", + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", "requires": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" diff --git a/package.json b/package.json index 1bdca86..509df3a 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "public-ip": "^4.0.0", "sqlite3": "^5.0.0", "tmp": "^0.1.0", - "transliteration": "^2.1.7", + "transliteration": "^2.1.11", "uuid": "^3.3.3" }, "devDependencies": { From b74463f6c496be1c21d4c6928f4b3035a6e096a9 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 13 Aug 2020 00:14:56 +0300 Subject: [PATCH 035/300] Update json5 to v2.1.3 --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index a0e5b3c..cdfc440 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2094,17 +2094,17 @@ "optional": true }, "json5": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.1.tgz", - "integrity": "sha512-l+3HXD0GEI3huGq1njuqtzYK8OYJyXMkOLtQ53pjWh89tvWS2h6l+1zMkYWqlb57+SiQodKZyvMEFb2X+KrFhQ==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", + "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", "requires": { - "minimist": "^1.2.0" + "minimist": "^1.2.5" }, "dependencies": { "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" } } }, diff --git a/package.json b/package.json index 509df3a..7054a9b 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "humanize-duration": "^3.12.1", "ini": "^1.3.5", "json-schema-to-jsdoc": "^1.0.0", - "json5": "^2.1.1", + "json5": "^2.1.3", "knex": "^0.21.4", "knub-command-manager": "^6.1.0", "mime": "^2.4.4", From 6630d46f48aaeed149fbc64ee3873092ab91eb3b Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 13 Aug 2020 00:15:44 +0300 Subject: [PATCH 036/300] Resolve vulnerabilities from npm audit + dedupe package-lock.json --- package-lock.json | 328 +++++++++++++--------------------------------- 1 file changed, 90 insertions(+), 238 deletions(-) diff --git a/package-lock.json b/package-lock.json index cdfc440..b93ce84 100644 --- a/package-lock.json +++ b/package-lock.json @@ -84,13 +84,6 @@ "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" - }, - "dependencies": { - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - } } }, "ansi-colors": { @@ -376,14 +369,6 @@ "responselike": "^1.0.2" }, "dependencies": { - "get-stream": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", - "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", - "requires": { - "pump": "^3.0.0" - } - }, "lowercase-keys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", @@ -424,6 +409,12 @@ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, + "ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true + }, "class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", @@ -1118,6 +1109,23 @@ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true }, + "execa": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.0.3.tgz", + "integrity": "sha512-WFDXGHckXPWZX19t1kCsXzOpqX9LWYNqn4C+HqZlk/V0imTkzJZqf87ZBhvpHaftERYknpk0fjSylnXVlVgI0A==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + } + }, "expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", @@ -1260,6 +1268,11 @@ "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", "optional": true }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, "fast-json-stable-stringify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", @@ -1271,6 +1284,15 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, + "figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, "file-entry-cache": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", @@ -1476,6 +1498,14 @@ "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", "dev": true }, + "get-stream": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", + "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", + "requires": { + "pump": "^3.0.0" + } + }, "get-value": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", @@ -1499,7 +1529,6 @@ "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "optional": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -1712,12 +1741,6 @@ "supports-color": "^7.1.0" } }, - "ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", - "dev": true - }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1981,6 +2004,12 @@ "is-unc-path": "^1.0.0" } }, + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "dev": true + }, "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", @@ -2099,13 +2128,6 @@ "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", "requires": { "minimist": "^1.2.5" - }, - "dependencies": { - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" - } } }, "jsprim": { @@ -2285,40 +2307,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "commander": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", - "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", - "dev": true - }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "execa": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/execa/-/execa-4.0.3.tgz", - "integrity": "sha512-WFDXGHckXPWZX19t1kCsXzOpqX9LWYNqn4C+HqZlk/V0imTkzJZqf87ZBhvpHaftERYknpk0fjSylnXVlVgI0A==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.0", - "get-stream": "^5.0.0", - "human-signals": "^1.1.1", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.0", - "onetime": "^5.1.0", - "signal-exit": "^3.0.2", - "strip-final-newline": "^2.0.0" - } - }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -2328,15 +2316,6 @@ "to-regex-range": "^5.0.1" } }, - "get-stream": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", - "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2349,12 +2328,6 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, - "is-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", - "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", - "dev": true - }, "micromatch": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", @@ -2365,36 +2338,6 @@ "picomatch": "^2.0.5" } }, - "npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "requires": { - "path-key": "^3.0.0" - } - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true - }, "supports-color": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", @@ -2417,7 +2360,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "requires": { "isexe": "^2.0.0" } @@ -2475,30 +2417,12 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "rxjs": { - "version": "6.6.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.2.tgz", - "integrity": "sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - } - }, "supports-color": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", @@ -2599,8 +2523,7 @@ "ansi-regex": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" }, "ansi-styles": { "version": "4.2.1", @@ -2636,8 +2559,7 @@ "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, "slice-ansi": { "version": "4.0.0", @@ -2654,7 +2576,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", - "dev": true, "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -2665,21 +2586,9 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, "requires": { "ansi-regex": "^5.0.0" } - }, - "wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } } } }, @@ -2755,6 +2664,12 @@ "mime-db": "1.44.0" } }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, "mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", @@ -2769,9 +2684,9 @@ } }, "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, "minipass": { "version": "2.9.0", @@ -2780,13 +2695,6 @@ "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" - }, - "dependencies": { - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" - } } }, "minizlib": { @@ -2817,11 +2725,11 @@ } }, "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", "requires": { - "minimist": "0.0.8" + "minimist": "^1.2.5" } }, "moment": { @@ -2987,11 +2895,6 @@ "safe-buffer": "^5.1.2", "yallist": "^3.0.3" } - }, - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" } } }, @@ -3038,6 +2941,15 @@ "npm-normalize-package-bin": "^1.0.1" } }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, "npmlog": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", @@ -3144,14 +3056,6 @@ "dev": true, "requires": { "mimic-fn": "^2.1.0" - }, - "dependencies": { - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true - } } }, "opencollective-postinstall": { @@ -3339,42 +3243,6 @@ "dev": true, "requires": { "find-up": "^4.0.0" - }, - "dependencies": { - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - } } }, "please-upgrade-node": { @@ -3441,8 +3309,7 @@ "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "optional": true + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, "qs": { "version": "6.5.2", @@ -3459,13 +3326,6 @@ "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - } } }, "readable-stream": { @@ -3610,21 +3470,15 @@ "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", "requires": { "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" - } - } + } + }, + "rxjs": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.2.tgz", + "integrity": "sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg==", + "dev": true, + "requires": { + "tslib": "^1.9.0" } }, "safe-buffer": { @@ -4274,13 +4128,6 @@ "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", "requires": { "punycode": "^2.1.0" - }, - "dependencies": { - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" - } } }, "urix": { @@ -4458,6 +4305,11 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + }, "yaml": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz", From 565032da1e41891307da8ad9eefa7c19b2f2b2ad Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 13 Aug 2020 00:16:28 +0300 Subject: [PATCH 037/300] Update to eris v0.13.3 --- package-lock.json | 38 +++++++++++++++----------------------- package.json | 2 +- 2 files changed, 16 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index b93ce84..3dd0ee9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -194,11 +194,6 @@ "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", "dev": true }, - "async-limiter": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", - "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" - }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -877,13 +872,13 @@ } }, "eris": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/eris/-/eris-0.11.1.tgz", - "integrity": "sha512-Ct32iXjESOnmklxZCEA281BQsTlAsS9xzQkbGlnvzXshCjBptWJ5h8Oxbu67ui1DirsYs0WipB8EBC9ITQ5ZQA==", + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/eris/-/eris-0.13.3.tgz", + "integrity": "sha512-WBtLyknOWZpYZL9yPhez0oKUWvYpunSg43hGxawwjwSf3gFXmbEPYrT8KlmZXtpJnX16eQ7mzIq+MgSh3LarEg==", "requires": { - "opusscript": "^0.0.4", - "tweetnacl": "^1.0.0", - "ws": "^7.1.2" + "opusscript": "^0.0.7", + "tweetnacl": "^1.0.1", + "ws": "^7.2.1" } }, "error-ex": { @@ -3079,9 +3074,9 @@ } }, "opusscript": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/opusscript/-/opusscript-0.0.4.tgz", - "integrity": "sha512-bEPZFE2lhUJYQD5yfTFO4RhbRZ937x6hRwBC1YoGacT35bwDVwKFP1+amU8NYfZL/v4EU7ZTU3INTqzYAnuP7Q==", + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/opusscript/-/opusscript-0.0.7.tgz", + "integrity": "sha512-DcBadTdYTUuH9zQtepsLjQn4Ll6rs3dmeFvN+SD0ThPnxRBRm/WC1zXWPg+wgAJimB784gdZvUMA57gDP7FdVg==", "optional": true }, "os-homedir": { @@ -4050,9 +4045,9 @@ } }, "tweetnacl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.1.tgz", - "integrity": "sha512-kcoMoKTPYnoeS50tzoqjPY3Uv9axeuuFAZY9M/9zFnhoVvRfxz9K29IMPD7jGmt2c8SW7i3gT9WqDl2+nV7p4A==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", "optional": true }, "type-check": { @@ -4293,12 +4288,9 @@ } }, "ws": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.2.0.tgz", - "integrity": "sha512-+SqNqFbwTm/0DC18KYzIsMTnEWpLwJsiasW/O17la4iDRRIO9uaHbvKiAS3AHgTiuuWerK/brj4O6MYZkei9xg==", - "requires": { - "async-limiter": "^1.0.0" - } + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz", + "integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==" }, "y18n": { "version": "4.0.0", diff --git a/package.json b/package.json index 7054a9b..f0f9f0f 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ }, "dependencies": { "ajv": "^6.12.3", - "eris": "^0.11.1", + "eris": "^0.13.3", "humanize-duration": "^3.12.1", "ini": "^1.3.5", "json-schema-to-jsdoc": "^1.0.0", From 2beadbe92492133a4b643ec257ab40910e520031 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 13 Aug 2020 00:16:57 +0300 Subject: [PATCH 038/300] Disable getAllUsers from the client We can lazy-load members instead. --- src/bot.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/bot.js b/src/bot.js index 1f52c16..0ab965b 100644 --- a/src/bot.js +++ b/src/bot.js @@ -2,7 +2,6 @@ const Eris = require("eris"); const config = require("./cfg"); const bot = new Eris.Client(config.token, { - getAllUsers: true, restMode: true, }); From b30e61520061a17759c7d00e1419200b64b7c1c0 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 13 Aug 2020 00:23:59 +0300 Subject: [PATCH 039/300] Use gateway intents, add extraIntents config option --- src/bot.js | 15 +++++++++++++++ src/cfg.js | 3 +++ src/data/cfg.jsdoc.js | 1 + src/data/cfg.schema.json | 5 +++++ 4 files changed, 24 insertions(+) diff --git a/src/bot.js b/src/bot.js index 0ab965b..82ef37e 100644 --- a/src/bot.js +++ b/src/bot.js @@ -1,8 +1,23 @@ const Eris = require("eris"); const config = require("./cfg"); +const intents = [ + "directMessages", // For core functionality + "guildMessages", // For bot commands and mentions + "guilds", // For core functionality + "guildVoiceStates", // For member information in the thread header + "guildMessageTyping", // For typing indicators + "directMessageTyping", // For typing indicators + + ...config.extraIntents, // Any extra intents added to the config +]; + const bot = new Eris.Client(config.token, { restMode: true, + intents: Array.from(new Set(intents)), }); +/** + * @type {Eris.Client} + */ module.exports = bot; diff --git a/src/cfg.js b/src/cfg.js index dc18bd0..18f7b91 100644 --- a/src/cfg.js +++ b/src/cfg.js @@ -220,4 +220,7 @@ if (! configIsValid) { console.log("Configuration ok!"); process.exit(0); +/** + * @type {ModmailConfig} + */ module.exports = config; diff --git a/src/data/cfg.jsdoc.js b/src/data/cfg.jsdoc.js index e3de128..339b8f8 100644 --- a/src/data/cfg.jsdoc.js +++ b/src/data/cfg.jsdoc.js @@ -50,4 +50,5 @@ * @property {string} [dbDir] * @property {object} [knex] * @property {string} [logDir] + * @property {array} [extraIntents=[]] */ diff --git a/src/data/cfg.schema.json b/src/data/cfg.schema.json index 70c1a70..acf5c33 100644 --- a/src/data/cfg.schema.json +++ b/src/data/cfg.schema.json @@ -262,6 +262,11 @@ "logDir": { "type": "string", "deprecationMessage": "This option is no longer used" + }, + + "extraIntents": { + "$ref": "#/definitions/stringArray", + "default": [] } }, "allOf": [ From ff89ab557c38e13956f2678d81f1f1c6c6266b4e Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 13 Aug 2020 00:26:54 +0300 Subject: [PATCH 040/300] Remove debug process.exit() --- src/cfg.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/cfg.js b/src/cfg.js index 18f7b91..8364346 100644 --- a/src/cfg.js +++ b/src/cfg.js @@ -218,7 +218,6 @@ if (! configIsValid) { } console.log("Configuration ok!"); -process.exit(0); /** * @type {ModmailConfig} From 6d16daea0dc653343ee8e3566ce8c1a755a14771 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 13 Aug 2020 02:36:15 +0300 Subject: [PATCH 041/300] Add new attachment storage option: "original" --- src/data/attachments.js | 32 ++++++++++++++++++++++---------- src/data/cfg.schema.json | 2 +- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/data/attachments.js b/src/data/attachments.js index 3d3ae16..c26d056 100644 --- a/src/data/attachments.js +++ b/src/data/attachments.js @@ -26,8 +26,17 @@ function getErrorResult(msg = null) { } /** - * Attempts to download and save the given attachement - * @param {Object} attachment + * An attachment storage option that simply forwards the original attachment URL + * @param {Eris.Attachment} attachment + * @returns {{url: string}} + */ +function passthroughOriginalAttachment(attachment) { + return { url: attachment.url }; +} + +/** + * An attachment storage option that downloads each attachment and serves them from a local web server + * @param {Eris.Attachment} attachment * @param {Number=0} tries * @returns {Promise<{ url: string }>} */ @@ -54,7 +63,7 @@ async function saveLocalAttachment(attachment) { } /** - * @param {Object} attachment + * @param {Eris.Attachment} attachment * @param {Number} tries * @returns {Promise<{ path: string, cleanup: function }>} */ @@ -108,7 +117,9 @@ function getLocalAttachmentUrl(attachmentId, desiredName = null) { } /** - * @param {Object} attachment + * An attachment storage option that downloads each attachment and re-posts them to a specified Discord channel. + * The re-posted attachment is then linked in the actual thread. + * @param {Eris.Attachment} attachment * @returns {Promise<{ url: string }>} */ async function saveDiscordAttachment(attachment) { @@ -153,19 +164,19 @@ async function createDiscordAttachmentMessage(channel, file, tries = 0) { /** * Turns the given attachment into a file object that can be sent forward as a new attachment - * @param {Object} attachment - * @returns {Promise<{file, name: string}>} + * @param {Eris.Attachment} attachment + * @returns {Promise} */ async function attachmentToDiscordFileObject(attachment) { const downloadResult = await downloadAttachment(attachment); const data = await readFile(downloadResult.path); downloadResult.cleanup(); - return {file: data, name: attachment.filename}; + return { file: data, name: attachment.filename }; } /** * Saves the given attachment based on the configured storage system - * @param {Object} attachment + * @param {Eris.Attachment} attachment * @returns {Promise<{ url: string }>} */ function saveAttachment(attachment) { @@ -190,8 +201,9 @@ function addStorageType(name, handler) { attachmentStorageTypes[name] = handler; } -attachmentStorageTypes.local = saveLocalAttachment; -attachmentStorageTypes.discord = saveDiscordAttachment; +addStorageType("original", passthroughOriginalAttachment); +addStorageType("local", saveLocalAttachment); +addStorageType("discord", saveDiscordAttachment); module.exports = { getLocalAttachmentPath, diff --git a/src/data/cfg.schema.json b/src/data/cfg.schema.json index acf5c33..e03c913 100644 --- a/src/data/cfg.schema.json +++ b/src/data/cfg.schema.json @@ -206,7 +206,7 @@ "attachmentStorage": { "type": "string", - "default": "local" + "default": "original" }, "attachmentStorageChannelId": { "type": "string" From 37c523cd041167bfebd1e84f926ee43d27244d18 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 13 Aug 2020 02:36:52 +0300 Subject: [PATCH 042/300] Band-aid fix for import in move.js --- src/modules/move.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/move.js b/src/modules/move.js index 0d4adcc..71fb9cc 100644 --- a/src/modules/move.js +++ b/src/modules/move.js @@ -1,7 +1,7 @@ const config = require("../cfg"); const Eris = require("eris"); const transliterate = require("transliteration"); -const erisEndpoints = require("eris/lib/rest/Endpoints"); +const erisEndpoints = require("../../node_modules/eris/lib/rest/Endpoints"); module.exports = ({ bot, knex, config, commands }) => { if (! config.allowMove) return; From 1f5faaab5b61b03a16d960e5705e176db94c1789 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 13 Aug 2020 02:44:42 +0300 Subject: [PATCH 043/300] Fix unnecessary warning about migrations from knex --- src/knex.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/knex.js b/src/knex.js index 35c8cb5..ff1a9cd 100644 --- a/src/knex.js +++ b/src/knex.js @@ -1,2 +1,13 @@ const config = require("./cfg"); -module.exports = require("knex")(config.knex); +module.exports = require("knex")({ + ...config.knex, + log: { + warn(message) { + if (message.startsWith("FS-related option specified for migration configuration")) { + return; + } + + console.warn(message); + }, + }, +}); From 1c5b08b4c6a4f81be77303339580169b38d804a2 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 13 Aug 2020 02:46:30 +0300 Subject: [PATCH 044/300] Update docs on the attachmentStorage option --- docs/configuration.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index cb4f2bb..998dce3 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -100,10 +100,11 @@ e.g. `!note This is an internal message`. If `alwaysReply` is enabled, this option controls whether the auto-reply is anonymous #### attachmentStorage -**Default:** `local` +**Default:** `original` Controls how attachments in modmail threads are stored. Possible values: -* **local** - Files are saved locally on the machine running the bot -* **discord** - Files are saved as attachments on a special channel on the inbox server. Requires `attachmentStorageChannelId` to be set. +* `original` - The original attachment is linked directly +* `local` - Files are saved locally on the machine running the bot and served via a local web server +* `discord` - Files are saved as attachments on a special channel on the inbox server. Requires `attachmentStorageChannelId` to be set. #### attachmentStorageChannelId **Default:** *None* From ab6b84e6ded4a313267bd91c5c6f24da081efbfd Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 13 Aug 2020 03:31:48 +0300 Subject: [PATCH 045/300] Add official MySQL support. Simplify database options. --- package-lock.json | 121 ++++++++++++++++++++++++++++++++++++++- package.json | 1 + src/cfg.js | 35 ++++++----- src/data/cfg.jsdoc.js | 7 ++- src/data/cfg.schema.json | 83 +++++++++++++++++++++++++-- src/knex.js | 34 ++++++++++- 6 files changed, 253 insertions(+), 28 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3dd0ee9..876df54 100644 --- a/package-lock.json +++ b/package-lock.json @@ -115,6 +115,11 @@ "color-convert": "^1.9.0" } }, + "ansicolors": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz", + "integrity": "sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk=" + }, "aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", @@ -382,6 +387,15 @@ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" }, + "cardinal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/cardinal/-/cardinal-2.1.1.tgz", + "integrity": "sha1-fMEFXYItISlU0HsIXeolHMe8VQU=", + "requires": { + "ansicolors": "~0.3.2", + "redeyed": "~2.1.0" + } + }, "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", @@ -799,6 +813,11 @@ "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" }, + "denque": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz", + "integrity": "sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ==" + }, "detect-file": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", @@ -1063,8 +1082,7 @@ "esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" }, "esquery": { "version": "1.3.1", @@ -1482,6 +1500,14 @@ } } }, + "generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "requires": { + "is-property": "^1.0.2" + } + }, "get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -1985,6 +2011,11 @@ "isobject": "^3.0.1" } }, + "is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=" + }, "is-regexp": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", @@ -2587,11 +2618,24 @@ } } }, + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, "lowercase-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==" }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "requires": { + "yallist": "^3.0.2" + } + }, "make-iterator": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", @@ -2769,6 +2813,56 @@ } } }, + "mysql2": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-2.1.0.tgz", + "integrity": "sha512-9kGVyi930rG2KaHrz3sHwtc6K+GY9d8wWk1XRSYxQiunvGcn4DwuZxOwmK11ftuhhwrYDwGx9Ta4VBwznJn36A==", + "requires": { + "cardinal": "^2.1.1", + "denque": "^1.4.1", + "generate-function": "^2.3.1", + "iconv-lite": "^0.5.0", + "long": "^4.0.0", + "lru-cache": "^5.1.1", + "named-placeholders": "^1.1.2", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.1" + }, + "dependencies": { + "iconv-lite": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.5.2.tgz", + "integrity": "sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + } + } + }, + "named-placeholders": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.2.tgz", + "integrity": "sha512-wiFWqxoLL3PGVReSZpjLVxyJ1bRqe+KKJVbr4hGs1KWfTZTQyezHFBbuKj9hsizHyGV2ne7EMjHdxEGAybD5SA==", + "requires": { + "lru-cache": "^4.1.3" + }, + "dependencies": { + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + } + } + }, "nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", @@ -3276,6 +3370,11 @@ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" + }, "psl": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", @@ -3345,6 +3444,14 @@ "resolve": "^1.1.6" } }, + "redeyed": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/redeyed/-/redeyed-2.1.1.tgz", + "integrity": "sha1-iYS1gV2ZyyIEacme7v/jiRPmzAs=", + "requires": { + "esprima": "~4.0.0" + } + }, "regex-not": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", @@ -3516,6 +3623,11 @@ "integrity": "sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw==", "dev": true }, + "seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha1-1WgS4cAXpuTnw+Ojeh2m143TyT4=" + }, "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -3743,6 +3855,11 @@ "node-pre-gyp": "^0.11.0" } }, + "sqlstring": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.2.tgz", + "integrity": "sha512-vF4ZbYdKS8OnoJAWBmMxCQDkiEBkGQYU7UZPtL8flbDRSNkhaXvRJ279ZtI6M+zDaQovVU4tuRgzK5fVhvFAhg==" + }, "sshpk": { "version": "1.16.1", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", diff --git a/package.json b/package.json index f0f9f0f..23ca0b2 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "mime": "^2.4.4", "moment": "^2.24.0", "mv": "^2.1.1", + "mysql2": "^2.1.0", "public-ip": "^4.0.0", "sqlite3": "^5.0.0", "tmp": "^0.1.0", diff --git a/src/cfg.js b/src/cfg.js index 8364346..35c2371 100644 --- a/src/cfg.js +++ b/src/cfg.js @@ -102,22 +102,15 @@ for (const [key, value] of Object.entries(config)) { delete config[key]; } -if (! config["knex"]) { - config.knex = { - client: "sqlite", - connection: { - filename: path.join(config.dbDir, "data.sqlite") - }, - useNullAsDefault: true - }; +if (! config.dbType) { + config.dbType = "sqlite"; } -// Make sure migration settings are always present in knex config -Object.assign(config["knex"], { - migrations: { - directory: path.join(config.dbDir, "migrations") - } -}); +if (! config.sqliteOptions) { + config.sqliteOptions = { + filename: path.resolve(__dirname, "..", "db", "data.sqlite"), + }; +} // Move greetingMessage/greetingAttachment to the guildGreetings object internally // Or, in other words, if greetingMessage and/or greetingAttachment is set, it is applied for all servers that don't @@ -150,8 +143,8 @@ for (const [key, value] of Object.entries(config)) { const ajv = new Ajv({ useDefaults: true, coerceTypes: "array" }); // https://github.com/ajv-validator/ajv/issues/141#issuecomment-270692820 -const truthyValues = ["1", "true", "on"]; -const falsyValues = ["0", "false", "off"]; +const truthyValues = ["1", "true", "on", "yes"]; +const falsyValues = ["0", "false", "off", "no"]; ajv.addKeyword("coerceBoolean", { compile(value) { return (data, dataPath, parentData, parentKey) => { @@ -208,12 +201,18 @@ const validate = ajv.compile(schema); const configIsValid = validate(config); if (! configIsValid) { - console.error("Issues with configuration options:"); + console.error(""); + console.error("NOTE! Issues with configuration:"); for (const error of validate.errors) { - console.error(`The "${error.dataPath.slice(1)}" option ${error.message}`); + if (error.params.missingProperty) { + console.error(`- Missing required option: "${error.params.missingProperty.slice(1)}"`); + } else { + console.error(`- The "${error.dataPath.slice(1)}" option ${error.message}`); + } } console.error(""); console.error("Please restart the bot after fixing the issues mentioned above."); + console.error(""); process.exit(1); } diff --git a/src/data/cfg.jsdoc.js b/src/data/cfg.jsdoc.js index 339b8f8..26eb805 100644 --- a/src/data/cfg.jsdoc.js +++ b/src/data/cfg.jsdoc.js @@ -15,7 +15,7 @@ * @property {string} [mentionRole="here"] * @property {boolean} [pingOnBotMention=true] * @property {string} [botMentionResponse] - * @property {array} [inboxServerPermission] + * @property {array} [inboxServerPermission=[]] * @property {boolean} [alwaysReply=false] * @property {boolean} [alwaysReplyAnon=false] * @property {boolean} [useNicknames=false] @@ -39,7 +39,7 @@ * @property {string} [timeOnServerDeniedMessage="You haven't been a member of the server for long enough to contact modmail."] * @property {boolean} [relaySmallAttachmentsAsAttachments=false] * @property {number} [smallAttachmentLimit=2097152] Max size of attachment to relay directly. Default is 2MB. - * @property {string} [attachmentStorage="local"] + * @property {string} [attachmentStorage="original"] * @property {string} [attachmentStorageChannelId] * @property {*} [categoryAutomation={}] * @property {boolean} [updateNotifications=true] @@ -51,4 +51,7 @@ * @property {object} [knex] * @property {string} [logDir] * @property {array} [extraIntents=[]] + * @property {*} [dbType="sqlite"] + * @property {*} [sqliteOptions] + * @property {*} [mysqlOptions] */ diff --git a/src/data/cfg.schema.json b/src/data/cfg.schema.json index e03c913..186b8c7 100644 --- a/src/data/cfg.schema.json +++ b/src/data/cfg.schema.json @@ -100,7 +100,8 @@ }, "inboxServerPermission": { - "$ref": "#/definitions/stringArray" + "$ref": "#/definitions/stringArray", + "default": [] }, "alwaysReply": { "$ref": "#/definitions/customBoolean", @@ -253,26 +254,72 @@ }, "dbDir": { - "type": "string" + "type": "string", + "deprecationMessage": "This option is deprecated. Please use sqliteOptions instead." }, + "knex": { - "type": "object" + "type": "object", + "deprecationMessage": "This option is deprecated. Please use dbType and sqliteOptions/mysqlOptions instead." }, "logDir": { "type": "string", - "deprecationMessage": "This option is no longer used" + "deprecationMessage": "This option is deprecated. Logs are no longer stored in individual files." }, "extraIntents": { "$ref": "#/definitions/stringArray", "default": [] + }, + + "dbType": { + "anyOf": [ + { "const": "sqlite" }, + { "const": "mysql" } + ], + "default": "sqlite" + }, + + "sqliteOptions": { + "type": "object", + "properties": { + "filename": { + "type": "string" + } + }, + "required": ["filename"] + }, + + "mysqlOptions": { + "type": "object", + "properties": { + "host": { + "type": "string" + }, + "port": { + "type": "number" + }, + "user": { + "type": "string" + }, + "password": { + "type": "string" + }, + "database": { + "type": "string" + }, + "timezone": { + "type": "string" + } + }, + "required": ["user", "password", "database"] } }, "allOf": [ { "$comment": "Base required values", - "required": ["token", "mainGuildId", "mailGuildId", "logChannelId"] + "required": ["token", "mainGuildId", "mailGuildId", "logChannelId", "dbType"] }, { "$comment": "Make attachmentStorageChannelId required if attachmentStorage is set to 'discord'", @@ -287,6 +334,32 @@ "then": { "required": ["attachmentStorageChannelId"] } + }, + { + "$comment": "Make sqliteOptions required if dbType is set to 'sqlite'", + "if": { + "properties": { + "dbType": { + "const": "sqlite" + } + } + }, + "then": { + "required": ["sqliteOptions"] + } + }, + { + "$comment": "Make mysqlOptions required if dbType is set to 'mysql'", + "if": { + "properties": { + "dbType": { + "const": "mysql" + } + } + }, + "then": { + "required": ["mysqlOptions"] + } } ] } diff --git a/src/knex.js b/src/knex.js index ff1a9cd..45aa9c5 100644 --- a/src/knex.js +++ b/src/knex.js @@ -1,6 +1,38 @@ +const path = require("path"); const config = require("./cfg"); + +let knexOptions; +if (config.dbType === "sqlite") { + const resolvedPath = path.resolve(process.cwd(), config.sqliteOptions.filename); + console.log(`Using an SQLite database:\n ${resolvedPath}`); + + knexOptions = { + client: "sqlite", + connection: { + ...config.sqliteOptions, + }, + }; +} else if (config.dbType === "mysql") { + const host = config.mysqlOptions.host || "localhost"; + const port = config.mysqlOptions.port || 3306; + const mysqlStr = `${config.mysqlOptions.user}@${host}:${port}/${config.mysqlOptions.database}`; + console.log(`Using a MySQL database:\n ${mysqlStr}`); + + knexOptions = { + client: "mysql2", + connection: { + ...config.mysqlOptions, + }, + }; +} + module.exports = require("knex")({ - ...config.knex, + ...knexOptions, + + useNullAsDefault: true, + migrations: { + directory: path.resolve(__dirname, "..", "db", "migrations"), + }, log: { warn(message) { if (message.startsWith("FS-related option specified for migration configuration")) { From 98b8a05d5c915a0edfcc961f4873dbf8cb96f393 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 13 Aug 2020 03:32:04 +0300 Subject: [PATCH 046/300] Remove several deprecated config options --- src/data/cfg.schema.json | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/src/data/cfg.schema.json b/src/data/cfg.schema.json index 186b8c7..4fa58a1 100644 --- a/src/data/cfg.schema.json +++ b/src/data/cfg.schema.json @@ -227,6 +227,7 @@ "$ref": "#/definitions/customBoolean", "default": true }, + "plugins": { "type": "array", "items": { @@ -249,25 +250,11 @@ "minimum": 1, "default": 8890 }, + "url": { "type": "string" }, - "dbDir": { - "type": "string", - "deprecationMessage": "This option is deprecated. Please use sqliteOptions instead." - }, - - "knex": { - "type": "object", - "deprecationMessage": "This option is deprecated. Please use dbType and sqliteOptions/mysqlOptions instead." - }, - - "logDir": { - "type": "string", - "deprecationMessage": "This option is deprecated. Logs are no longer stored in individual files." - }, - "extraIntents": { "$ref": "#/definitions/stringArray", "default": [] From 9973f7594031538aa394ae3ff338b3ba16de2fdd Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 13 Aug 2020 03:32:31 +0300 Subject: [PATCH 047/300] Initial CHANGELOG pass for v2.31.0-beta.1 --- CHANGELOG.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c556d73..665e844 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,32 @@ # Changelog +## v2.31.0-beta.1 +This is a beta release. It is not available on the Releases page and bugs are expected. + +General changes: +* Added support for Node.js 14, dropped support for Node.js 10 and 11 +* Added support for editing and deleting staff replies + * This can be disabled with the `allowStaffEdit` and `allowStaffDelete` options + * Only the staff member who sent the reply can edit/delete it +* New **default** attachment storage option: `original` + * This option simply links the original attachment and does not rehost it in any way +* Added official support for MySQL databases. Refer to the documentation on `dbType` for more details. + * This change also means the following options are **no longer supported:** + * `dbDir` (use `sqliteOptions.filename` to specify the database file instead) + * `knex` (see `dbType` documentation for more details) +* Removed the long-deprecated `logDir` option + +Plugins: +* Added support for replacing default message formatting in threads, DMs, and logs +* Added support for *hooks*. Hooks can be used to run custom plugin code before/after specific moments. + * Initially only the `beforeNewThread` hook is available. See plugin documentation for more details. +* If your plugin requires special gateway intents, use the new `extraIntents` config option + +Internal/technical updates: +* Updated to Eris 0.13.3 +* The client now requests specific gateway intents on connection +* New config parser that validates each option and their types more strictly to prevent undefined behavior + ## v2.31.0-beta.0 This is a beta release. It is not available on the Releases page and bugs are expected. From 555a75929b013fe353e1ea6bb489fb7c584f5d9c Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 13 Aug 2020 03:37:01 +0300 Subject: [PATCH 048/300] Fix migrations so they don't show Knex warnings --- db/migrations/20171223203915_create_tables.js | 93 +++++++++++-------- 1 file changed, 56 insertions(+), 37 deletions(-) diff --git a/db/migrations/20171223203915_create_tables.js b/db/migrations/20171223203915_create_tables.js index b5880a3..ac0c778 100644 --- a/db/migrations/20171223203915_create_tables.js +++ b/db/migrations/20171223203915_create_tables.js @@ -1,45 +1,64 @@ exports.up = async function(knex, Promise) { - await knex.schema.createTableIfNotExists('threads', table => { - table.string('id', 36).notNullable().primary(); - table.integer('status').unsigned().notNullable().index(); - table.integer('is_legacy').unsigned().notNullable(); - table.string('user_id', 20).notNullable().index(); - table.string('user_name', 128).notNullable(); - table.string('channel_id', 20).nullable().unique(); - table.dateTime('created_at').notNullable().index(); - }); + if (! await knex.schema.hasTable("threads")) { + await knex.schema.createTable("threads", table => { + table.string("id", 36).notNullable().primary(); + table.integer("status").unsigned().notNullable().index(); + table.integer("is_legacy").unsigned().notNullable(); + table.string("user_id", 20).notNullable().index(); + table.string("user_name", 128).notNullable(); + table.string("channel_id", 20).nullable().unique(); + table.dateTime("created_at").notNullable().index(); + }); + } - await knex.schema.createTableIfNotExists('thread_messages', table => { - table.increments('id'); - table.string('thread_id', 36).notNullable().index().references('id').inTable('threads').onDelete('CASCADE'); - table.integer('message_type').unsigned().notNullable(); - table.string('user_id', 20).nullable(); - table.string('user_name', 128).notNullable(); - table.mediumtext('body').notNullable(); - table.integer('is_anonymous').unsigned().notNullable(); - table.string('dm_message_id', 20).nullable().unique(); - table.dateTime('created_at').notNullable().index(); - }); + if (! await knex.schema.hasTable("thread_messages")) { + await knex.schema.createTable("thread_messages", table => { + table.increments("id"); + table.string("thread_id", 36).notNullable().index().references("id").inTable("threads").onDelete("CASCADE"); + table.integer("message_type").unsigned().notNullable(); + table.string("user_id", 20).nullable(); + table.string("user_name", 128).notNullable(); + table.mediumtext("body").notNullable(); + table.integer("is_anonymous").unsigned().notNullable(); + table.string("dm_message_id", 20).nullable().unique(); + table.dateTime("created_at").notNullable().index(); + }); + } - await knex.schema.createTableIfNotExists('blocked_users', table => { - table.string('user_id', 20).primary().notNullable(); - table.string('user_name', 128).notNullable(); - table.string('blocked_by', 20).nullable(); - table.dateTime('blocked_at').notNullable(); - }); + if (! await knex.schema.hasTable("blocked_users")) { + await knex.schema.createTable("blocked_users", table => { + table.string("user_id", 20).primary().notNullable(); + table.string("user_name", 128).notNullable(); + table.string("blocked_by", 20).nullable(); + table.dateTime("blocked_at").notNullable(); + }); + } - await knex.schema.createTableIfNotExists('snippets', table => { - table.string('trigger', 32).primary().notNullable(); - table.text('body').notNullable(); - table.integer('is_anonymous').unsigned().notNullable(); - table.string('created_by', 20).nullable(); - table.dateTime('created_at').notNullable(); - }); + if (! await knex.schema.hasTable("snippets")) { + await knex.schema.createTable("snippets", table => { + table.string("trigger", 32).primary().notNullable(); + table.text("body").notNullable(); + table.integer("is_anonymous").unsigned().notNullable(); + table.string("created_by", 20).nullable(); + table.dateTime("created_at").notNullable(); + }); + } }; exports.down = async function(knex, Promise) { - await knex.schema.dropTableIfExists('thread_messages'); - await knex.schema.dropTableIfExists('threads'); - await knex.schema.dropTableIfExists('blocked_users'); - await knex.schema.dropTableIfExists('snippets'); + if (await knex.schema.hasTable("thread_messages")) { + await knex.schema.dropTable("thread_messages"); + } + + if (await knex.schema.hasTable("threads")) { + await knex.schema.dropTable("threads"); + } + + if (await knex.schema.hasTable("blocked_users")) { + await knex.schema.dropTable("blocked_users"); + } + + if (await knex.schema.hasTable("snippets")) { + await knex.schema.dropTable("snippets"); + } }; From d89b27d9b15b261f7b1f41aeeb3cc4e19faed61c Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 13 Aug 2020 03:37:57 +0300 Subject: [PATCH 049/300] Also lint ./db/migrations in lint scripts --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 23ca0b2..5fe3846 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,8 @@ "start": "node src/index.js", "watch": "supervisor -n exit -w src src/index.js", "test": "echo \"Error: no test specified\" && exit 1", - "lint": "eslint ./src", - "lint-fix": "eslint --fix ./src", + "lint": "eslint ./src ./db/migrations", + "lint-fix": "eslint --fix ./src ./db/migrations", "generate-config-jsdoc": "node src/data/generateCfgJsdoc.js" }, "repository": { From 9df221aa02837b67d5b4d6743e92478194932524 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 13 Aug 2020 03:38:24 +0300 Subject: [PATCH 050/300] Apply code style from .eslintrc to migrations --- .../20180224235946_add_close_at_to_threads.js | 16 ++++++++-------- .../20180421161550_add_alert_id_to_threads.js | 8 ++++---- ...20224224_remove_is_anonymous_from_snippets.js | 8 ++++---- ...4728_add_scheduled_close_silent_to_threads.js | 8 ++++---- ...306211534_add_scheduled_suspend_to_threads.js | 16 ++++++++-------- .../20190609161116_create_updates_table.js | 12 ++++++------ ...0609193213_add_expires_at_to_blocked_users.js | 8 ++++---- ...191206002418_add_number_to_thread_messages.js | 10 +++++----- ...8_add_thread_message_id_to_thread_messages.js | 10 +++++----- ...80113_add_dm_channel_id_to_thread_messages.js | 10 +++++----- 10 files changed, 53 insertions(+), 53 deletions(-) diff --git a/db/migrations/20180224235946_add_close_at_to_threads.js b/db/migrations/20180224235946_add_close_at_to_threads.js index dbbb04d..9d77d5d 100644 --- a/db/migrations/20180224235946_add_close_at_to_threads.js +++ b/db/migrations/20180224235946_add_close_at_to_threads.js @@ -1,15 +1,15 @@ exports.up = async function (knex, Promise) { - await knex.schema.table('threads', table => { - table.dateTime('scheduled_close_at').index().nullable().defaultTo(null).after('channel_id'); - table.string('scheduled_close_id', 20).nullable().defaultTo(null).after('channel_id'); - table.string('scheduled_close_name', 128).nullable().defaultTo(null).after('channel_id'); + await knex.schema.table("threads", table => { + table.dateTime("scheduled_close_at").index().nullable().defaultTo(null).after("channel_id"); + table.string("scheduled_close_id", 20).nullable().defaultTo(null).after("channel_id"); + table.string("scheduled_close_name", 128).nullable().defaultTo(null).after("channel_id"); }); }; exports.down = async function(knex, Promise) { - await knex.schema.table('threads', table => { - table.dropColumn('scheduled_close_at'); - table.dropColumn('scheduled_close_id'); - table.dropColumn('scheduled_close_name'); + await knex.schema.table("threads", table => { + table.dropColumn("scheduled_close_at"); + table.dropColumn("scheduled_close_id"); + table.dropColumn("scheduled_close_name"); }); }; diff --git a/db/migrations/20180421161550_add_alert_id_to_threads.js b/db/migrations/20180421161550_add_alert_id_to_threads.js index 5defc38..7a9f6f0 100644 --- a/db/migrations/20180421161550_add_alert_id_to_threads.js +++ b/db/migrations/20180421161550_add_alert_id_to_threads.js @@ -1,11 +1,11 @@ exports.up = async function (knex, Promise) { - await knex.schema.table('threads', table => { - table.string('alert_id', 20).nullable().defaultTo(null).after('scheduled_close_name'); + await knex.schema.table("threads", table => { + table.string("alert_id", 20).nullable().defaultTo(null).after("scheduled_close_name"); }); }; exports.down = async function(knex, Promise) { - await knex.schema.table('threads', table => { - table.dropColumn('alert_id'); + await knex.schema.table("threads", table => { + table.dropColumn("alert_id"); }); }; diff --git a/db/migrations/20180920224224_remove_is_anonymous_from_snippets.js b/db/migrations/20180920224224_remove_is_anonymous_from_snippets.js index ac33267..48a0293 100644 --- a/db/migrations/20180920224224_remove_is_anonymous_from_snippets.js +++ b/db/migrations/20180920224224_remove_is_anonymous_from_snippets.js @@ -1,11 +1,11 @@ exports.up = async function (knex, Promise) { - await knex.schema.table('snippets', table => { - table.dropColumn('is_anonymous'); + await knex.schema.table("snippets", table => { + table.dropColumn("is_anonymous"); }); }; exports.down = async function(knex, Promise) { - await knex.schema.table('snippets', table => { - table.integer('is_anonymous').unsigned().notNullable(); + await knex.schema.table("snippets", table => { + table.integer("is_anonymous").unsigned().notNullable(); }); }; diff --git a/db/migrations/20190306204728_add_scheduled_close_silent_to_threads.js b/db/migrations/20190306204728_add_scheduled_close_silent_to_threads.js index e61490d..0c0b254 100644 --- a/db/migrations/20190306204728_add_scheduled_close_silent_to_threads.js +++ b/db/migrations/20190306204728_add_scheduled_close_silent_to_threads.js @@ -1,11 +1,11 @@ exports.up = async function(knex, Promise) { - await knex.schema.table('threads', table => { - table.integer('scheduled_close_silent').nullable().after('scheduled_close_name'); + await knex.schema.table("threads", table => { + table.integer("scheduled_close_silent").nullable().after("scheduled_close_name"); }); }; exports.down = async function(knex, Promise) { - await knex.schema.table('threads', table => { - table.dropColumn('scheduled_close_silent'); + await knex.schema.table("threads", table => { + table.dropColumn("scheduled_close_silent"); }); }; diff --git a/db/migrations/20190306211534_add_scheduled_suspend_to_threads.js b/db/migrations/20190306211534_add_scheduled_suspend_to_threads.js index a22b5f3..0b6dfd0 100644 --- a/db/migrations/20190306211534_add_scheduled_suspend_to_threads.js +++ b/db/migrations/20190306211534_add_scheduled_suspend_to_threads.js @@ -1,15 +1,15 @@ exports.up = async function(knex, Promise) { - await knex.schema.table('threads', table => { - table.dateTime('scheduled_suspend_at').index().nullable().defaultTo(null).after('channel_id'); - table.string('scheduled_suspend_id', 20).nullable().defaultTo(null).after('channel_id'); - table.string('scheduled_suspend_name', 128).nullable().defaultTo(null).after('channel_id'); + await knex.schema.table("threads", table => { + table.dateTime("scheduled_suspend_at").index().nullable().defaultTo(null).after("channel_id"); + table.string("scheduled_suspend_id", 20).nullable().defaultTo(null).after("channel_id"); + table.string("scheduled_suspend_name", 128).nullable().defaultTo(null).after("channel_id"); }); }; exports.down = async function(knex, Promise) { - await knex.schema.table('threads', table => { - table.dropColumn('scheduled_suspend_at'); - table.dropColumn('scheduled_suspend_id'); - table.dropColumn('scheduled_suspend_name'); + await knex.schema.table("threads", table => { + table.dropColumn("scheduled_suspend_at"); + table.dropColumn("scheduled_suspend_id"); + table.dropColumn("scheduled_suspend_name"); }); }; diff --git a/db/migrations/20190609161116_create_updates_table.js b/db/migrations/20190609161116_create_updates_table.js index c90e9bd..f4e0f92 100644 --- a/db/migrations/20190609161116_create_updates_table.js +++ b/db/migrations/20190609161116_create_updates_table.js @@ -1,14 +1,14 @@ exports.up = async function(knex, Promise) { - if (! await knex.schema.hasTable('updates')) { - await knex.schema.createTable('updates', table => { - table.string('available_version', 16).nullable(); - table.dateTime('last_checked').nullable(); + if (! await knex.schema.hasTable("updates")) { + await knex.schema.createTable("updates", table => { + table.string("available_version", 16).nullable(); + table.dateTime("last_checked").nullable(); }); } }; exports.down = async function(knex, Promise) { - if (await knex.schema.hasTable('updates')) { - await knex.schema.dropTable('updates'); + if (await knex.schema.hasTable("updates")) { + await knex.schema.dropTable("updates"); } }; diff --git a/db/migrations/20190609193213_add_expires_at_to_blocked_users.js b/db/migrations/20190609193213_add_expires_at_to_blocked_users.js index ea456a6..c2afda4 100644 --- a/db/migrations/20190609193213_add_expires_at_to_blocked_users.js +++ b/db/migrations/20190609193213_add_expires_at_to_blocked_users.js @@ -1,11 +1,11 @@ exports.up = async function(knex, Promise) { - await knex.schema.table('blocked_users', table => { - table.dateTime('expires_at').nullable(); + await knex.schema.table("blocked_users", table => { + table.dateTime("expires_at").nullable(); }); }; exports.down = async function(knex, Promise) { - await knex.schema.table('blocked_users', table => { - table.dropColumn('expires_at'); + await knex.schema.table("blocked_users", table => { + table.dropColumn("expires_at"); }); }; diff --git a/db/migrations/20191206002418_add_number_to_thread_messages.js b/db/migrations/20191206002418_add_number_to_thread_messages.js index 3564b90..0b44113 100644 --- a/db/migrations/20191206002418_add_number_to_thread_messages.js +++ b/db/migrations/20191206002418_add_number_to_thread_messages.js @@ -1,11 +1,11 @@ -const Knex = require('knex'); +const Knex = require("knex"); /** * @param {Knex} knex */ exports.up = async function(knex) { - await knex.schema.table('thread_messages', table => { - table.integer('message_number').unsigned().nullable(); + await knex.schema.table("thread_messages", table => { + table.integer("message_number").unsigned().nullable(); }); }; @@ -13,7 +13,7 @@ exports.up = async function(knex) { * @param {Knex} knex */ exports.down = async function(knex) { - await knex.schema.table('thread_messages', table => { - table.dropColumn('message_number'); + await knex.schema.table("thread_messages", table => { + table.dropColumn("message_number"); }); }; diff --git a/db/migrations/20191206011638_add_thread_message_id_to_thread_messages.js b/db/migrations/20191206011638_add_thread_message_id_to_thread_messages.js index 475c066..f884a60 100644 --- a/db/migrations/20191206011638_add_thread_message_id_to_thread_messages.js +++ b/db/migrations/20191206011638_add_thread_message_id_to_thread_messages.js @@ -1,11 +1,11 @@ -const Knex = require('knex'); +const Knex = require("knex"); /** * @param {Knex} knex */ exports.up = async function(knex) { - await knex.schema.table('thread_messages', table => { - table.string('inbox_message_id', 20).nullable().unique(); + await knex.schema.table("thread_messages", table => { + table.string("inbox_message_id", 20).nullable().unique(); }); }; @@ -13,7 +13,7 @@ exports.up = async function(knex) { * @param {Knex} knex */ exports.down = async function(knex) { - await knex.schema.table('thread_messages', table => { - table.dropColumn('inbox_message_id'); + await knex.schema.table("thread_messages", table => { + table.dropColumn("inbox_message_id"); }); }; diff --git a/db/migrations/20191206180113_add_dm_channel_id_to_thread_messages.js b/db/migrations/20191206180113_add_dm_channel_id_to_thread_messages.js index 4b0aa6a..768dff8 100644 --- a/db/migrations/20191206180113_add_dm_channel_id_to_thread_messages.js +++ b/db/migrations/20191206180113_add_dm_channel_id_to_thread_messages.js @@ -1,11 +1,11 @@ -const Knex = require('knex'); +const Knex = require("knex"); /** * @param {Knex} knex */ exports.up = async function(knex) { - await knex.schema.table('thread_messages', table => { - table.string('dm_channel_id', 20).nullable(); + await knex.schema.table("thread_messages", table => { + table.string("dm_channel_id", 20).nullable(); }); }; @@ -13,7 +13,7 @@ exports.up = async function(knex) { * @param {Knex} knex */ exports.down = async function(knex) { - await knex.schema.table('thread_messages', table => { - table.dropColumn('dm_channel_id'); + await knex.schema.table("thread_messages", table => { + table.dropColumn("dm_channel_id"); }); }; From 8ba25d95048d134873e785509bf145e714cd613f Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 13 Aug 2020 03:40:34 +0300 Subject: [PATCH 051/300] Update Node.js version ranges in package.json and index.js --- package.json | 2 +- src/index.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 5fe3846..3d716da 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "supervisor": "^0.12.0" }, "engines": { - "node": ">=10.0.0 <14.0.0" + "node": ">=12.0.0 <14.0.0" }, "husky": { "hooks": { diff --git a/src/index.js b/src/index.js index 588111a..cde9e0e 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,7 @@ // Verify NodeJS version const nodeMajorVersion = parseInt(process.versions.node.split(".")[0], 10); -if (nodeMajorVersion < 11) { - console.error("Unsupported NodeJS version! Please install Node.js 11, 12, 13, or 14."); +if (nodeMajorVersion < 12) { + console.error("Unsupported NodeJS version! Please install Node.js 12, 13, or 14."); process.exit(1); } From 77cb19e70c3ab9168f246074e87efc81a476d482 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 13 Aug 2020 23:55:29 +0300 Subject: [PATCH 052/300] Fix inconsistency between knexfile and runtime knex config --- knexfile.js | 4 ++-- src/knex.js | 47 ++--------------------------------------------- src/knexConfig.js | 45 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 47 deletions(-) create mode 100644 src/knexConfig.js diff --git a/knexfile.js b/knexfile.js index 12606e3..623871c 100644 --- a/knexfile.js +++ b/knexfile.js @@ -1,2 +1,2 @@ -const config = require('./src/cfg'); -module.exports = config.knex; +const knexConfig = require("./src/knexConfig"); +module.exports = knexConfig; diff --git a/src/knex.js b/src/knex.js index 45aa9c5..b5f3f5c 100644 --- a/src/knex.js +++ b/src/knex.js @@ -1,45 +1,2 @@ -const path = require("path"); -const config = require("./cfg"); - -let knexOptions; -if (config.dbType === "sqlite") { - const resolvedPath = path.resolve(process.cwd(), config.sqliteOptions.filename); - console.log(`Using an SQLite database:\n ${resolvedPath}`); - - knexOptions = { - client: "sqlite", - connection: { - ...config.sqliteOptions, - }, - }; -} else if (config.dbType === "mysql") { - const host = config.mysqlOptions.host || "localhost"; - const port = config.mysqlOptions.port || 3306; - const mysqlStr = `${config.mysqlOptions.user}@${host}:${port}/${config.mysqlOptions.database}`; - console.log(`Using a MySQL database:\n ${mysqlStr}`); - - knexOptions = { - client: "mysql2", - connection: { - ...config.mysqlOptions, - }, - }; -} - -module.exports = require("knex")({ - ...knexOptions, - - useNullAsDefault: true, - migrations: { - directory: path.resolve(__dirname, "..", "db", "migrations"), - }, - log: { - warn(message) { - if (message.startsWith("FS-related option specified for migration configuration")) { - return; - } - - console.warn(message); - }, - }, -}); +const knexConfig = require("./knexConfig"); +module.exports = require("knex")(knexConfig); diff --git a/src/knexConfig.js b/src/knexConfig.js new file mode 100644 index 0000000..e72a52d --- /dev/null +++ b/src/knexConfig.js @@ -0,0 +1,45 @@ +const path = require("path"); +const config = require("./cfg"); + +let knexOptions; +if (config.dbType === "sqlite") { + const resolvedPath = path.resolve(process.cwd(), config.sqliteOptions.filename); + console.log(`Using an SQLite database:\n ${resolvedPath}`); + + knexOptions = { + client: "sqlite", + connection: { + ...config.sqliteOptions, + }, + }; +} else if (config.dbType === "mysql") { + const host = config.mysqlOptions.host || "localhost"; + const port = config.mysqlOptions.port || 3306; + const mysqlStr = `${config.mysqlOptions.user}@${host}:${port}/${config.mysqlOptions.database}`; + console.log(`Using a MySQL database:\n ${mysqlStr}`); + + knexOptions = { + client: "mysql2", + connection: { + ...config.mysqlOptions, + }, + }; +} + +module.exports = { + ...knexOptions, + + useNullAsDefault: true, + migrations: { + directory: path.resolve(__dirname, "..", "db", "migrations"), + }, + log: { + warn(message) { + if (message.startsWith("FS-related option specified for migration configuration")) { + return; + } + + console.warn(message); + }, + }, +}; From 98532be55a7bab460992fe781d93f88e8faf393f Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 13 Aug 2020 23:55:45 +0300 Subject: [PATCH 053/300] Add create-migration script Usage: npm run create-migration -- my_migration_name_here --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 3d716da..88d33ae 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "test": "echo \"Error: no test specified\" && exit 1", "lint": "eslint ./src ./db/migrations", "lint-fix": "eslint --fix ./src ./db/migrations", - "generate-config-jsdoc": "node src/data/generateCfgJsdoc.js" + "generate-config-jsdoc": "node src/data/generateCfgJsdoc.js", + "create-migration": "knex migrate:make" }, "repository": { "type": "git", From 296d1304a7ffeec1a4649ea7d61fd7d179097909 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Fri, 14 Aug 2020 00:42:32 +0300 Subject: [PATCH 054/300] Reproducible formatters, add full log formatter Format-specific parts of replies, including the role name and attachments, are now stored in separate columns. This allows us to store only one version of the actual message body and, by keeping format-specific data separate, reproduce formatter results regardless of when they are called. This cleans up code around message formats significantly and was required to support !edit/!delete properly. --- ...00813230319_separate_message_components.js | 21 ++ src/data/Thread.js | 121 ++++--- src/data/ThreadMessage.js | 32 ++ src/formatters.js | 295 ++++++++---------- src/modules/reply.js | 1 + src/modules/webserver.js | 61 +--- 6 files changed, 246 insertions(+), 285 deletions(-) create mode 100644 db/migrations/20200813230319_separate_message_components.js diff --git a/db/migrations/20200813230319_separate_message_components.js b/db/migrations/20200813230319_separate_message_components.js new file mode 100644 index 0000000..0b0e025 --- /dev/null +++ b/db/migrations/20200813230319_separate_message_components.js @@ -0,0 +1,21 @@ +exports.up = async function(knex) { + await knex.schema.table("thread_messages", table => { + table.string("role_name", 255).nullable(); + table.text("attachments").nullable(); + table.text("small_attachments").nullable(); + table.boolean("use_legacy_format").nullable(); + }); + + await knex("thread_messages").update({ + use_legacy_format: 1, + }); +}; + +exports.down = async function(knex) { + await knex.schema.table("thread_messages", table => { + table.dropColumn("role_name"); + table.dropColumn("attachments"); + table.dropColumn("small_attachments"); + table.dropColumn("use_legacy_format"); + }); +}; diff --git a/src/data/Thread.js b/src/data/Thread.js index 747c449..83fc5eb 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -178,7 +178,9 @@ class Thread { * @returns {Promise} Whether we were able to send the reply */ async replyToUser(moderator, text, replyAttachments = [], isAnonymous = false) { - const fullModeratorName = `${moderator.user.username}#${moderator.user.discriminator}`; + const moderatorName = config.useNicknames && moderator.nick ? moderator.nick : moderator.user.username; + const mainRole = utils.getMainRole(moderator); + const roleName = mainRole ? mainRole.name : null; // Prepare attachments, if any const files = []; @@ -197,8 +199,18 @@ class Thread { } } + let threadMessage = new ThreadMessage({ + message_type: THREAD_MESSAGE_TYPE.TO_USER, + user_id: moderator.id, + user_name: moderatorName, + body: text, + is_anonymous: (isAnonymous ? 1 : 0), + role_name: roleName, + attachments: attachmentLinks, + }); + // Send the reply DM - const dmContent = formatters.formatStaffReplyDM(moderator, text, { isAnonymous }); + const dmContent = formatters.formatStaffReplyDM(threadMessage); let dmMessage; try { dmMessage = await this._sendDMToUser(dmContent, files); @@ -208,21 +220,17 @@ class Thread { } // Save the log entry - const threadMessage = await this._addThreadMessageToDB({ - message_type: THREAD_MESSAGE_TYPE.TO_USER, - user_id: moderator.id, - user_name: fullModeratorName, - body: "", - is_anonymous: (isAnonymous ? 1 : 0), - dm_message_id: dmMessage.id + threadMessage = await this._addThreadMessageToDB({ + ...threadMessage.getSQLProps(), + dm_message_id: dmMessage.id, }); - const logContent = formatters.formatStaffReplyLogMessage(moderator, text, threadMessage.message_number, { isAnonymous, attachmentLinks }); - await this._updateThreadMessage(threadMessage.id, { body: logContent }); // Show the reply in the inbox thread - const inboxContent = formatters.formatStaffReplyThreadMessage(moderator, text, threadMessage.message_number, { isAnonymous }); + const inboxContent = formatters.formatStaffReplyThreadMessage(threadMessage); const inboxMessage = await this._postToThreadChannel(inboxContent, files); - if (inboxMessage) await this._updateThreadMessage(threadMessage.id, { inbox_message_id: inboxMessage.id }); + if (inboxMessage) { + await this._updateThreadMessage(threadMessage.id, { inbox_message_id: inboxMessage.id }); + } // Interrupt scheduled closing, if in progress if (this.scheduled_close_at) { @@ -238,43 +246,46 @@ class Thread { * @returns {Promise} */ async receiveUserReply(msg) { - // Prepare attachments - const attachmentFiles = []; - const threadFormattedAttachments = []; - const logFormattedAttachments = []; + const fullUserName = `${msg.author.username}#${msg.author.discriminator}`; + + // Prepare attachments + const attachments = []; + const smallAttachments = []; + const attachmentFiles = []; - // TODO: Save attachment info with the message, use that to re-create attachment formatting in - // TODO: this._formatUserReplyLogMessage and this._formatUserReplyThreadMessage for (const attachment of msg.attachments) { const savedAttachment = await attachments.saveAttachment(attachment); - const formatted = await utils.formatAttachment(attachment, savedAttachment.url); - logFormattedAttachments.push(formatted); - // Forward small attachments (<2MB) as attachments, link to larger ones if (config.relaySmallAttachmentsAsAttachments && attachment.size <= config.smallAttachmentLimit) { const file = await attachments.attachmentToDiscordFileObject(attachment); attachmentFiles.push(file); - } else { - threadFormattedAttachments.push(formatted); + smallAttachments.push(savedAttachment.url); } + + attachments.push(savedAttachment.url); } - // Save log entry - const logContent = formatters.formatUserReplyLogMessage(msg.author, msg, { attachmentLinks: logFormattedAttachments }); - const threadMessage = await this._addThreadMessageToDB({ + // Save DB entry + let threadMessage = new ThreadMessage({ message_type: THREAD_MESSAGE_TYPE.FROM_USER, user_id: this.user_id, - user_name: `${msg.author.username}#${msg.author.discriminator}`, - body: logContent, + user_name: fullUserName, + body: msg.content || "", is_anonymous: 0, - dm_message_id: msg.id + dm_message_id: msg.id, + attachments, + small_attachments: smallAttachments, }); + threadMessage = await this._addThreadMessageToDB(threadMessage.getSQLProps()); + // Show user reply in the inbox thread - const inboxContent = formatters.formatUserReplyThreadMessage(msg.author, msg, { attachmentLinks: threadFormattedAttachments }); + const inboxContent = formatters.formatUserReplyThreadMessage(threadMessage); const inboxMessage = await this._postToThreadChannel(inboxContent, attachmentFiles); - if (inboxMessage) await this._updateThreadMessage(threadMessage.id, { inbox_message_id: inboxMessage.id }); + if (inboxMessage) { + await this._updateThreadMessage(threadMessage.id, { inbox_message_id: inboxMessage.id }); + } // Interrupt scheduled closing, if in progress if (this.scheduled_close_at) { @@ -306,12 +317,11 @@ class Thread { async postSystemMessage(content, file = null, opts = {}) { const msg = await this._postToThreadChannel(content, file); if (msg && opts.saveToLog !== false) { - const finalLogBody = opts.logBody || msg.content || ""; await this._addThreadMessageToDB({ message_type: THREAD_MESSAGE_TYPE.SYSTEM, user_id: null, user_name: "", - body: finalLogBody, + body: msg.content || "", is_anonymous: 0, inbox_message_id: msg.id, }); @@ -329,12 +339,11 @@ class Thread { async sendSystemMessageToUser(content, file = null, opts = {}) { const msg = await this._sendDMToUser(content, file); if (opts.saveToLog !== false) { - const finalLogBody = opts.logBody || msg.content || ""; await this._addThreadMessageToDB({ message_type: THREAD_MESSAGE_TYPE.SYSTEM_TO_USER, user_id: null, user_name: "", - body: finalLogBody, + body: msg.content || "", is_anonymous: 0, dm_message_id: msg.id, }); @@ -355,6 +364,7 @@ class Thread { * @returns {Promise} */ async saveChatMessageToLogs(msg) { + // TODO: Save attachments? return this._addThreadMessageToDB({ message_type: THREAD_MESSAGE_TYPE.CHAT, user_id: msg.author.id, @@ -421,7 +431,7 @@ class Thread { const data = await knex("thread_messages") .where("thread_id", this.id) .where("message_number", messageNumber) - .select(); + .first(); return data ? new ThreadMessage(data) : null; } @@ -560,37 +570,23 @@ class Thread { * @returns {Promise} */ async editStaffReply(moderator, threadMessage, newText, opts = {}) { - const formattedThreadMessage = formatters.formatStaffReplyThreadMessage( - moderator, - newText, - threadMessage.message_number, - { isAnonymous: threadMessage.is_anonymous } - ); + const newThreadMessage = new ThreadMessage({ + ...threadMessage.getSQLProps(), + body: newText, + }); - const formattedDM = formatters.formatStaffReplyDM( - moderator, - newText, - { isAnonymous: threadMessage.is_anonymous } - ); - - // FIXME: Fix attachment links disappearing by moving them off the main message content in the DB - const formattedLog = formatters.formatStaffReplyLogMessage( - moderator, - newText, - threadMessage.message_number, - { isAnonymous: threadMessage.is_anonymous } - ); + const formattedThreadMessage = formatters.formatStaffReplyThreadMessage(newThreadMessage); + const formattedDM = formatters.formatStaffReplyDM(newThreadMessage); await bot.editMessage(threadMessage.dm_channel_id, threadMessage.dm_message_id, formattedDM); await bot.editMessage(this.channel_id, threadMessage.inbox_message_id, formattedThreadMessage); if (! opts.quiet) { - const threadNotification = formatters.formatStaffReplyEditNotificationThreadMessage(moderator, threadMessage, newText); - const logNotification = formatters.formatStaffReplyEditNotificationLogMessage(moderator, threadMessage, newText); - await this.postSystemMessage(threadNotification, null, { logBody: logNotification }); + const threadNotification = formatters.formatStaffReplyEditNotificationThreadMessage(threadMessage, newText, moderator); + await this.postSystemMessage(threadNotification); } - await this._updateThreadMessage(threadMessage.id, { body: formattedLog }); + await this._updateThreadMessage(threadMessage.id, { body: newText }); } /** @@ -605,9 +601,8 @@ class Thread { await bot.deleteMessage(this.channel_id, threadMessage.inbox_message_id); if (! opts.quiet) { - const threadNotification = formatters.formatStaffReplyDeletionNotificationThreadMessage(moderator, threadMessage); - const logNotification = formatters.formatStaffReplyDeletionNotificationLogMessage(moderator, threadMessage); - await this.postSystemMessage(threadNotification, null, { logBody: logNotification }); + const threadNotification = formatters.formatStaffReplyDeletionNotificationThreadMessage(threadMessage, moderator); + await this.postSystemMessage(threadNotification); } await this._deleteThreadMessage(threadMessage.id); diff --git a/src/data/ThreadMessage.js b/src/data/ThreadMessage.js index e132aaa..70e1d21 100644 --- a/src/data/ThreadMessage.js +++ b/src/data/ThreadMessage.js @@ -7,16 +7,48 @@ const utils = require("../utils"); * @property {Number} message_number * @property {String} user_id * @property {String} user_name + * @property {String} role_name * @property {String} body * @property {Number} is_anonymous + * @property {String[]} attachments + * @property {String[]} small_attachments The subset of attachments that were relayed when relaySmallAttachmentsAsAttachments is enabled * @property {String} dm_channel_id * @property {String} dm_message_id * @property {String} inbox_message_id * @property {String} created_at + * @property {Number} use_legacy_format */ class ThreadMessage { constructor(props) { utils.setDataModelProps(this, props); + + if (props.attachments) { + if (typeof props.attachments === "string") { + this.attachments = JSON.parse(props.attachments); + } + } else { + this.attachments = []; + } + + if (props.small_attachments) { + if (typeof props.small_attachments === "string") { + this.small_attachments = JSON.parse(props.small_attachments); + } + } else { + this.small_attachments = []; + } + } + + getSQLProps() { + return Object.entries(this).reduce((obj, [key, value]) => { + if (typeof value === "function") return obj; + if (typeof value === "object") { + obj[key] = JSON.stringify(value); + } else { + obj[key] = value; + } + return obj; + }, {}); } } diff --git a/src/formatters.js b/src/formatters.js index ea18eae..3e36e48 100644 --- a/src/formatters.js +++ b/src/formatters.js @@ -2,229 +2,208 @@ const Eris = require("eris"); const utils = require("./utils"); const config = require("./cfg"); const ThreadMessage = require("./data/ThreadMessage"); +const {THREAD_MESSAGE_TYPE} = require("./data/constants"); +const moment = require("moment"); /** * Function to format the DM that is sent to the user when a staff member replies to them via !reply * @callback FormatStaffReplyDM - * @param {Eris.Member} moderator Staff member that is replying - * @param {string} text Reply text - * @param {{ - * isAnonymous: boolean, - * }} opts={} + * @param {ThreadMessage} threadMessage * @return {Eris.MessageContent} Message content to send as a DM */ /** * Function to format a staff reply in a thread channel * @callback FormatStaffReplyThreadMessage - * @param {Eris.Member} moderator - * @param {string} text - * @param {number} messageNumber - * @param {{ - * isAnonymous: boolean, - * }} opts={} + * @param {ThreadMessage} threadMessage * @return {Eris.MessageContent} Message content to post in the thread channel */ -/** - * Function to format a staff reply in a log - * @callback FormatStaffReplyLogMessage - * @param {Eris.Member} moderator - * @param {string} text - * @param {number} messageNumber - * @param {{ - * isAnonymous: boolean, - * attachmentLinks: string[], - * }} opts={} - * @returns {string} Text to show in the log - */ - /** * Function to format a user reply in a thread channel * @callback FormatUserReplyThreadMessage - * @param {Eris.User} user Use that sent the reply - * @param {Eris.Message} msg The message object that the user sent - * @param {{ - * attachmentLinks: string[], - * }} opts + * @param {ThreadMessage} threadMessage * @return {Eris.MessageContent} Message content to post in the thread channel */ -/** - * Function to format a user reply in a log - * @callback FormatUserReplyLogMessage - * @param {Eris.User} user - * @param {Eris.Message} msg - * @param {{ - * attachmentLinks: string[], - * }} opts={} - * @return {string} Text to show in the log - */ - /** * Function to format the inbox channel notification for a staff reply edit * @callback FormatStaffReplyEditNotificationThreadMessage - * @param {Eris.Member} moderator * @param {ThreadMessage} threadMessage * @param {string} newText + * @param {Eris.Member} moderator Moderator that edited the message * @return {Eris.MessageContent} Message content to post in the thread channel */ -/** - * Function to format the log notification for a staff reply edit - * @callback FormatStaffReplyEditNotificationLogMessage - * @param {Eris.Member} moderator - * @param {ThreadMessage} threadMessage - * @param {string} newText - * @return {string} Text to show in the log - */ - /** * Function to format the inbox channel notification for a staff reply deletion * @callback FormatStaffReplyDeletionNotificationThreadMessage - * @param {Eris.Member} moderator * @param {ThreadMessage} threadMessage + * @param {Eris.Member} moderator Moderator that deleted the message * @return {Eris.MessageContent} Message content to post in the thread channel */ /** - * Function to format the log notification for a staff reply deletion - * @callback FormatStaffReplyDeletionNotificationLogMessage - * @param {Eris.Member} moderator - * @param {ThreadMessage} threadMessage - * @return {string} Text to show in the log + * @typedef {Object} FormatLogOptions + * @property {Boolean?} simple + * @property {Boolean?} verbose + */ + +/** + * @typedef {Object} FormatLogResult + * @property {String} content Contents of the entire log + * @property {*?} extra + */ + +/** + * Function to format the inbox channel notification for a staff reply deletion + * @callback FormatLog + * @param {Thread} thread + * @param {ThreadMessage[]} threadMessages + * @param {FormatLogOptions={}} opts + * @return {FormatLogResult} */ /** * @typedef MessageFormatters * @property {FormatStaffReplyDM} formatStaffReplyDM * @property {FormatStaffReplyThreadMessage} formatStaffReplyThreadMessage - * @property {FormatStaffReplyLogMessage} formatStaffReplyLogMessage * @property {FormatUserReplyThreadMessage} formatUserReplyThreadMessage - * @property {FormatUserReplyLogMessage} formatUserReplyLogMessage * @property {FormatStaffReplyEditNotificationThreadMessage} formatStaffReplyEditNotificationThreadMessage - * @property {FormatStaffReplyEditNotificationLogMessage} formatStaffReplyEditNotificationLogMessage * @property {FormatStaffReplyDeletionNotificationThreadMessage} formatStaffReplyDeletionNotificationThreadMessage - * @property {FormatStaffReplyDeletionNotificationLogMessage} formatStaffReplyDeletionNotificationLogMessage + * @property {FormatLog} formatLog */ /** * @type {MessageFormatters} */ const defaultFormatters = { - formatStaffReplyDM(moderator, text, opts = {}) { - const mainRole = utils.getMainRole(moderator); - const modName = (config.useNicknames ? moderator.nick || moderator.user.username : moderator.user.username); - const modInfo = opts.isAnonymous - ? (mainRole ? mainRole.name : "Moderator") - : (mainRole ? `(${mainRole.name}) ${modName}` : modName); + formatStaffReplyDM(threadMessage) { + const modInfo = threadMessage.is_anonymous + ? (threadMessage.role_name ? threadMessage.role_name : "Moderator") + : (threadMessage.role_name ? `(${threadMessage.role_name}) ${threadMessage.user_name}` : threadMessage.user_name); - return `**${modInfo}:** ${text}`; + return `**${modInfo}:** ${threadMessage.body}`; }, - formatStaffReplyThreadMessage(moderator, text, messageNumber, opts = {}) { - const mainRole = utils.getMainRole(moderator); - const modName = (config.useNicknames ? moderator.nick || moderator.user.username : moderator.user.username); - const modInfo = opts.isAnonymous - ? `(Anonymous) (${modName}) ${mainRole ? mainRole.name : "Moderator"}` - : (mainRole ? `(${mainRole.name}) ${modName}` : modName); + formatStaffReplyThreadMessage(threadMessage) { + const modInfo = threadMessage.is_anonymous + ? `(Anonymous) (${threadMessage.user_name}) ${threadMessage.role_name || "Moderator"}` + : (threadMessage.role_name ? `(${threadMessage.role_name}) ${threadMessage.user_name}` : threadMessage.user_name); - let result = `**${modInfo}:** ${text}`; + let result = `**${modInfo}:** ${threadMessage.body}`; if (config.threadTimestamps) { - const formattedTimestamp = utils.getTimestamp(); + const formattedTimestamp = utils.getTimestamp(threadMessage.created_at); result = `[${formattedTimestamp}] ${result}`; } - result = `\`[${messageNumber}]\` ${result}`; + result = `\`[${threadMessage.message_number}]\` ${result}`; return result; }, - formatStaffReplyLogMessage(moderator, text, messageNumber, opts = {}) { - const mainRole = utils.getMainRole(moderator); - const modName = moderator.user.username; + formatUserReplyThreadMessage(threadMessage) { + let result = `**${threadMessage.user_name}:** ${threadMessage.body}`; - // Mirroring the DM formatting here... - const modInfo = opts.isAnonymous - ? (mainRole ? mainRole.name : "Moderator") - : (mainRole ? `(${mainRole.name}) ${modName}` : modName); - - let result = `**${modInfo}:** ${text}`; - - if (opts.attachmentLinks && opts.attachmentLinks.length) { - result += "\n"; - for (const link of opts.attachmentLinks) { - result += `\n**Attachment:** ${link}`; - } - } - - result = `[${messageNumber}] ${result}`; - - return result; - }, - - formatUserReplyThreadMessage(user, msg, opts = {}) { - const content = (msg.content.trim() === "" && msg.embeds.length) - ? "" - : msg.content; - - let result = `**${user.username}#${user.discriminator}:** ${content}`; - - if (opts.attachmentLinks && opts.attachmentLinks.length) { - for (const link of opts.attachmentLinks) { - result += `\n\n${link}`; - } + for (const link of threadMessage.attachments) { + result += `\n\n${link}`; } if (config.threadTimestamps) { - const formattedTimestamp = utils.getTimestamp(msg.timestamp, "x"); + const formattedTimestamp = utils.getTimestamp(threadMessage.created_at); result = `[${formattedTimestamp}] ${result}`; } return result; }, - formatUserReplyLogMessage(user, msg, opts = {}) { - const content = (msg.content.trim() === "" && msg.embeds.length) - ? "" - : msg.content; - - let result = content; - - if (opts.attachmentLinks && opts.attachmentLinks.length) { - for (const link of opts.attachmentLinks) { - result += `\n\n${link}`; - } - } - - return result; - }, - - formatStaffReplyEditNotificationThreadMessage(moderator, threadMessage, newText) { - let content = `**${moderator.user.username}#${moderator.user.discriminator} (\`${moderator.id}\`) edited reply \`[${threadMessage.message_number}]\`:**`; + formatStaffReplyEditNotificationThreadMessage(threadMessage, newText, moderator) { + let content = `**${moderator.user.username}#${moderator.user.discriminator}** (\`${moderator.id}\`) edited reply \`[${threadMessage.message_number}]\`:`; content += `\n\`B:\` ${threadMessage.body}`; content += `\n\`A:\` ${newText}`; return utils.disableLinkPreviews(content); }, - formatStaffReplyEditNotificationLogMessage(moderator, threadMessage, newText) { - let content = `${moderator.user.username}#${moderator.user.discriminator} (${moderator.id}) edited reply [${threadMessage.message_number}]:`; - content += `\nB: ${threadMessage.body}`; - content += `\nA: ${newText}`; - return content; - }, - - formatStaffReplyDeletionNotificationThreadMessage(moderator, threadMessage) { + formatStaffReplyDeletionNotificationThreadMessage(threadMessage, moderator) { let content = `**${moderator.user.username}#${moderator.user.discriminator} (\`${moderator.id}\`) deleted reply \`[${threadMessage.message_number}]\`:**`; content += `\n\`B:\` ${threadMessage.body}`; return utils.disableLinkPreviews(content); }, - formatStaffReplyDeletionNotificationLogMessage(moderator, threadMessage) { - let content = `${moderator.user.username}#${moderator.user.discriminator} (${moderator.id}) deleted reply [${threadMessage.message_number}]:`; - content += `\nB: ${threadMessage.body}`; - return content; + formatLog(thread, threadMessages, opts = {}) { + if (opts.simple) { + threadMessages = threadMessages.filter(message => { + return ( + message.message_type !== THREAD_MESSAGE_TYPE.SYSTEM + && message.message_type !== THREAD_MESSAGE_TYPE.SYSTEM_TO_USER + && message.message_type !== THREAD_MESSAGE_TYPE.CHAT + && message.message_type !== THREAD_MESSAGE_TYPE.COMMAND + ); + }); + } + + const lines = threadMessages.map(message => { + // Legacy messages (from 2018) are the entire log in one message, so just serve them as they are + if (message.message_type === THREAD_MESSAGE_TYPE.LEGACY) { + return message.body; + } + + let line = `[${moment.utc(message.created_at).format("YYYY-MM-DD HH:mm:ss")}]`; + + if (opts.verbose) { + if (message.dm_channel_id) { + line += ` [DM CHA ${message.dm_channel_id}]`; + } + + if (message.dm_message_id) { + line += ` [DM MSG ${message.dm_message_id}]`; + } + } + + if (message.message_type === THREAD_MESSAGE_TYPE.FROM_USER) { + line += ` [FROM USER] [${message.user_name}] ${message.body}`; + } else if (message.message_type === THREAD_MESSAGE_TYPE.TO_USER) { + line += ` [TO USER] [${message.message_number || "0"}] [${message.user_name}]`; + if (message.use_legacy_format) { + // Legacy format (from pre-2.31.0) includes the role and username in the message body, so serve that as is + line += ` ${message.body}`; + } else if (message.is_anonymous) { + if (message.role_name) { + line += ` (Anonymous) ${message.role_name}: ${message.body}`; + } else { + line += ` (Anonymous) Moderator: ${message.body}`; + } + } else { + if (message.role_name) { + line += ` (${message.role_name}) ${message.user_name}: ${message.body}`; + } else { + line += ` ${message.user_name}: ${message.body}`; + } + } + } else if (message.message_type === THREAD_MESSAGE_TYPE.SYSTEM) { + line += ` [SYSTEM] ${message.body}`; + } else if (message.message_type === THREAD_MESSAGE_TYPE.SYSTEM_TO_USER) { + line += ` [SYSTEM TO USER] ${message.body}`; + } else if (message.message_type === THREAD_MESSAGE_TYPE.CHAT) { + line += ` [CHAT] [${message.user_name}] ${message.body}`; + } else if (message.message_type === THREAD_MESSAGE_TYPE.COMMAND) { + line += ` [COMMAND] [${message.user_name}] ${message.body}`; + } else { + line += ` [${message.user_name}] ${message.body}`; + } + + return line; + }); + + const openedAt = moment(thread.created_at).format("YYYY-MM-DD HH:mm:ss"); + const header = `# Modmail thread with ${thread.user_name} (${thread.user_id}) started at ${openedAt}. All times are in UTC+0.`; + + const fullResult = header + "\n\n" + lines.join("\n"); + + return { + content: fullResult, + }; }, }; @@ -234,7 +213,11 @@ const defaultFormatters = { const formatters = { ...defaultFormatters }; module.exports = { - formatters, + formatters: new Proxy(formatters, { + set() { + throw new Error("Please use the formatter setter functions instead of modifying the formatters directly"); + }, + }), /** * @param {FormatStaffReplyDM} fn @@ -252,14 +235,6 @@ module.exports = { formatters.formatStaffReplyThreadMessage = fn; }, - /** - * @param {FormatStaffReplyLogMessage} fn - * @return {void} - */ - setStaffReplyLogMessageFormatter(fn) { - formatters.formatStaffReplyLogMessage = fn; - }, - /** * @param {FormatUserReplyThreadMessage} fn * @return {void} @@ -268,14 +243,6 @@ module.exports = { formatters.formatUserReplyThreadMessage = fn; }, - /** - * @param {FormatUserReplyLogMessage} fn - * @return {void} - */ - setUserReplyLogMessageFormatter(fn) { - formatters.formatUserReplyLogMessage = fn; - }, - /** * @param {FormatStaffReplyEditNotificationThreadMessage} fn * @return {void} @@ -284,14 +251,6 @@ module.exports = { formatters.formatStaffReplyEditNotificationThreadMessage = fn; }, - /** - * @param {FormatStaffReplyEditNotificationLogMessage} fn - * @return {void} - */ - setStaffReplyEditNotificationLogMessageFormatter(fn) { - formatters.formatStaffReplyEditNotificationLogMessage = fn; - }, - /** * @param {FormatStaffReplyDeletionNotificationThreadMessage} fn * @return {void} @@ -301,10 +260,10 @@ module.exports = { }, /** - * @param {FormatStaffReplyDeletionNotificationLogMessage} fn + * @param {FormatLog} fn * @return {void} */ - setStaffReplyDeletionNotificationLogMessageFormatter(fn) { - formatters.formatStaffReplyDeletionNotificationLogMessage = fn; + setLogFormatter(fn) { + formatters.formatLog = fn; }, }; diff --git a/src/modules/reply.js b/src/modules/reply.js index 146469e..693850d 100644 --- a/src/modules/reply.js +++ b/src/modules/reply.js @@ -39,6 +39,7 @@ module.exports = ({ bot, knex, config, commands }) => { return; } + console.log(threadMessage.user_id, msg.author.id); if (threadMessage.user_id !== msg.author.id) { utils.postError(msg.channel, "You can only edit your own replies"); return; diff --git a/src/modules/webserver.js b/src/modules/webserver.js index 9f22f47..0a04401 100644 --- a/src/modules/webserver.js +++ b/src/modules/webserver.js @@ -7,8 +7,7 @@ const moment = require("moment"); const config = require("../cfg"); const threads = require("../data/threads"); const attachments = require("../data/attachments"); - -const {THREAD_MESSAGE_TYPE} = require("../data/constants"); +const { formatters } = require("../formatters"); function notfound(res) { res.statusCode = 404; @@ -24,61 +23,15 @@ async function serveLogs(req, res, pathParts, query) { let threadMessages = await thread.getThreadMessages(); - if (query.simple) { - threadMessages = threadMessages.filter(message => { - return ( - message.message_type !== THREAD_MESSAGE_TYPE.SYSTEM - && message.message_type !== THREAD_MESSAGE_TYPE.SYSTEM_TO_USER - && message.message_type !== THREAD_MESSAGE_TYPE.CHAT - && message.message_type !== THREAD_MESSAGE_TYPE.COMMAND - ); - }); - } - - const lines = threadMessages.map(message => { - // Legacy messages are the entire log in one message, so just serve them as they are - if (message.message_type === THREAD_MESSAGE_TYPE.LEGACY) { - return message.body; - } - - let line = `[${moment.utc(message.created_at).format("YYYY-MM-DD HH:mm:ss")}]`; - - if (query.verbose) { - if (message.dm_channel_id) { - line += ` [DM CHA ${message.dm_channel_id}]`; - } - - if (message.dm_message_id) { - line += ` [DM MSG ${message.dm_message_id}]`; - } - } - - if (message.message_type === THREAD_MESSAGE_TYPE.FROM_USER) { - line += ` [FROM USER] [${message.user_name}] ${message.body}`; - } else if (message.message_type === THREAD_MESSAGE_TYPE.TO_USER) { - line += ` [TO USER] [${message.user_name}] ${message.body}`; - } else if (message.message_type === THREAD_MESSAGE_TYPE.SYSTEM) { - line += ` [SYSTEM] ${message.body}`; - } else if (message.message_type === THREAD_MESSAGE_TYPE.SYSTEM_TO_USER) { - line += ` [SYSTEM TO USER] ${message.body}`; - } else if (message.message_type === THREAD_MESSAGE_TYPE.CHAT) { - line += ` [CHAT] [${message.user_name}] ${message.body}`; - } else if (message.message_type === THREAD_MESSAGE_TYPE.COMMAND) { - line += ` [COMMAND] [${message.user_name}] ${message.body}`; - } else { - line += ` [${message.user_name}] ${message.body}`; - } - - return line; + const formatLogResult = await formatters.formatLog(thread, threadMessages, { + simple: Boolean(query.simple), + verbose: Boolean(query.verbose), }); - const openedAt = moment(thread.created_at).format("YYYY-MM-DD HH:mm:ss"); - const header = `# Modmail thread with ${thread.user_name} (${thread.user_id}) started at ${openedAt}. All times are in UTC+0.`; + const contentType = formatLogResult.extra && formatLogResult.extra.contentType || "text/plain; charset=UTF-8"; - const fullResponse = header + "\n\n" + lines.join("\n"); - - res.setHeader("Content-Type", "text/plain; charset=UTF-8"); - res.end(fullResponse); + res.setHeader("Content-Type", contentType); + res.end(formatLogResult.content); } function serveAttachments(req, res, pathParts) { From 97a37eba61e7420df570bf503137099f8753f7d5 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Fri, 14 Aug 2020 00:50:09 +0300 Subject: [PATCH 055/300] Small clarification in CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 665e844..a6ede84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ This is a beta release. It is not available on the Releases page and bugs are ex General changes: * Added support for Node.js 14, dropped support for Node.js 10 and 11 + * The supported versions are now 12, 13, and 14 * Added support for editing and deleting staff replies * This can be disabled with the `allowStaffEdit` and `allowStaffDelete` options * Only the staff member who sent the reply can edit/delete it From 25f2814e11045a8556678cd5fe6d97d4d83ec435 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Fri, 14 Aug 2020 01:00:43 +0300 Subject: [PATCH 056/300] Update documentation with new options --- docs/configuration.md | 57 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/docs/configuration.md b/docs/configuration.md index 998dce3..144094f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -89,6 +89,14 @@ If enabled, allows you to move threads between categories using `!move Date: Fri, 14 Aug 2020 01:01:40 +0300 Subject: [PATCH 057/300] Update mysqlOptions schema --- src/data/cfg.jsdoc.js | 14 +++++++++----- src/data/cfg.schema.json | 8 +++++--- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/data/cfg.jsdoc.js b/src/data/cfg.jsdoc.js index 26eb805..49a21f3 100644 --- a/src/data/cfg.jsdoc.js +++ b/src/data/cfg.jsdoc.js @@ -47,11 +47,15 @@ * @property {*} [commandAliases] * @property {number} [port=8890] * @property {string} [url] - * @property {string} [dbDir] - * @property {object} [knex] - * @property {string} [logDir] * @property {array} [extraIntents=[]] * @property {*} [dbType="sqlite"] - * @property {*} [sqliteOptions] - * @property {*} [mysqlOptions] + * @property {object} [sqliteOptions] + * @property {string} sqliteOptions.filename + * @property {object} [mysqlOptions] + * @property {string} mysqlOptions.host + * @property {number} mysqlOptions.port + * @property {string} mysqlOptions.user + * @property {string} mysqlOptions.password + * @property {string} mysqlOptions.database + * @property {string} [mysqlOptions.timezone] */ diff --git a/src/data/cfg.schema.json b/src/data/cfg.schema.json index 4fa58a1..198c87a 100644 --- a/src/data/cfg.schema.json +++ b/src/data/cfg.schema.json @@ -282,10 +282,12 @@ "type": "object", "properties": { "host": { - "type": "string" + "type": "string", + "default": "localhost" }, "port": { - "type": "number" + "type": "number", + "default": "3306" }, "user": { "type": "string" @@ -300,7 +302,7 @@ "type": "string" } }, - "required": ["user", "password", "database"] + "required": ["host", "port", "user", "password", "database"] } }, "allOf": [ From 205262660bf7d012d7e463eb0ea79eb7f20b9f94 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Fri, 14 Aug 2020 01:04:07 +0300 Subject: [PATCH 058/300] Fix line break in documentation --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 144094f..eb6edde 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -313,7 +313,7 @@ Other databases are *not* currently supported. Object with SQLite-specific options ##### sqliteOptions.filename -**Default:** `db/data.sqlite` +**Default:** `db/data.sqlite` Can be used to specify the path to the database file #### mysqlOptions From a4c7b84616ab1e3f50d0d1f7875e294e8abe4693 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Fri, 14 Aug 2020 01:18:45 +0300 Subject: [PATCH 059/300] Add next_message_number to threads, use it for reply numbers --- ...1007_add_next_message_number_to_threads.js | 11 ++++++++ src/data/Thread.js | 26 ++++++++++++------- 2 files changed, 28 insertions(+), 9 deletions(-) create mode 100644 db/migrations/20200814011007_add_next_message_number_to_threads.js diff --git a/db/migrations/20200814011007_add_next_message_number_to_threads.js b/db/migrations/20200814011007_add_next_message_number_to_threads.js new file mode 100644 index 0000000..a5649d3 --- /dev/null +++ b/db/migrations/20200814011007_add_next_message_number_to_threads.js @@ -0,0 +1,11 @@ +exports.up = async function(knex) { + await knex.schema.table("threads", table => { + table.integer("next_message_number").defaultTo(1); + }); +}; + +exports.down = async function(knex) { + await knex.schema.table("threads", table => { + table.dropColumn("next_message_number"); + }); +}; diff --git a/src/data/Thread.js b/src/data/Thread.js index 83fc5eb..42f0b41 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -18,6 +18,7 @@ const {THREAD_MESSAGE_TYPE, THREAD_STATUS} = require("./constants"); * @property {String} user_id * @property {String} user_name * @property {String} channel_id + * @property {Number} next_message_number * @property {String} scheduled_close_at * @property {String} scheduled_close_id * @property {String} scheduled_close_name @@ -116,7 +117,7 @@ class Thread { */ async _addThreadMessageToDB(data) { if (data.message_type === THREAD_MESSAGE_TYPE.TO_USER) { - data.message_number = knex.raw(`IFNULL((${this._lastMessageNumberInThreadSQL()}), 0) + 1`); + data.message_number = await this._getAndIncrementNextMessageNumber(); } const dmChannel = await this.getDMChannel(); @@ -159,15 +160,22 @@ class Thread { } /** - * @returns {string} - * @private + * @returns {Promise} */ - _lastMessageNumberInThreadSQL() { - return knex("thread_messages AS tm_msg_num_ref") - .select(knex.raw("MAX(tm_msg_num_ref.message_number)")) - .whereRaw(`tm_msg_num_ref.thread_id = '${this.id}'`) - .toSQL() - .sql; + async _getAndIncrementNextMessageNumber() { + return knex.transaction(async trx => { + const nextNumberRow = await trx("threads") + .where("id", this.id) + .select("next_message_number") + .first(); + const nextNumber = nextNumberRow.next_message_number; + + await trx("threads") + .where("id", this.id) + .update({ next_message_number: nextNumber + 1 }); + + return nextNumber; + }); } /** From 7c96d71efee91fa8186c4d25f6328458cb0bf9f7 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Fri, 14 Aug 2020 01:19:01 +0300 Subject: [PATCH 060/300] Tweak edit/delete formatting --- src/formatters.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/formatters.js b/src/formatters.js index 3e36e48..e329685 100644 --- a/src/formatters.js +++ b/src/formatters.js @@ -120,15 +120,15 @@ const defaultFormatters = { formatStaffReplyEditNotificationThreadMessage(threadMessage, newText, moderator) { let content = `**${moderator.user.username}#${moderator.user.discriminator}** (\`${moderator.id}\`) edited reply \`[${threadMessage.message_number}]\`:`; - content += `\n\`B:\` ${threadMessage.body}`; - content += `\n\`A:\` ${newText}`; - return utils.disableLinkPreviews(content); + content += `\n\nBefore:\n\`\`\`${utils.disableCodeBlocks(threadMessage.body)}\`\`\``; + content += `\nAfter:\n\`\`\`${utils.disableCodeBlocks(newText)}\`\`\``; + return content; }, formatStaffReplyDeletionNotificationThreadMessage(threadMessage, moderator) { - let content = `**${moderator.user.username}#${moderator.user.discriminator} (\`${moderator.id}\`) deleted reply \`[${threadMessage.message_number}]\`:**`; - content += `\n\`B:\` ${threadMessage.body}`; - return utils.disableLinkPreviews(content); + let content = `**${moderator.user.username}#${moderator.user.discriminator}** (\`${moderator.id}\`) deleted reply \`[${threadMessage.message_number}]\`:`; + content += "```" + utils.disableCodeBlocks(threadMessage.body) + "```"; + return content; }, formatLog(thread, threadMessages, opts = {}) { From 31a5fb55b83e277706959d43300beb1d33aead9e Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Fri, 14 Aug 2020 01:23:49 +0300 Subject: [PATCH 061/300] Remove debug console.log --- src/modules/reply.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/modules/reply.js b/src/modules/reply.js index 693850d..146469e 100644 --- a/src/modules/reply.js +++ b/src/modules/reply.js @@ -39,7 +39,6 @@ module.exports = ({ bot, knex, config, commands }) => { return; } - console.log(threadMessage.user_id, msg.author.id); if (threadMessage.user_id !== msg.author.id) { utils.postError(msg.channel, "You can only edit your own replies"); return; From 25998fa8a297645fc2e7c8a83a45d2c26439b7de Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 16 Aug 2020 18:18:32 +0300 Subject: [PATCH 062/300] Show a small note if a user messages the bot with e.g. a Spotify invite --- src/data/Thread.js | 32 ++++++++++++++++++++++++++++++-- src/data/constants.js | 8 ++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/data/Thread.js b/src/data/Thread.js index 42f0b41..2912b11 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -10,7 +10,7 @@ const { formatters } = require("../formatters"); const ThreadMessage = require("./ThreadMessage"); -const {THREAD_MESSAGE_TYPE, THREAD_STATUS} = require("./constants"); +const {THREAD_MESSAGE_TYPE, THREAD_STATUS, DISCORD_MESSAGE_ACTIVITY_TYPES} = require("./constants"); /** * @property {String} id @@ -255,6 +255,7 @@ class Thread { */ async receiveUserReply(msg) { const fullUserName = `${msg.author.username}#${msg.author.discriminator}`; + let messageContent = msg.content || ""; // Prepare attachments const attachments = []; @@ -274,12 +275,39 @@ class Thread { attachments.push(savedAttachment.url); } + // Handle special embeds (listening party invites etc.) + if (msg.activity) { + let applicationName = msg.application && msg.application.name; + + if (! applicationName && msg.activity.party_id.startsWith("spotify:")) { + applicationName = "Spotify"; + } + + if (! applicationName) { + applicationName = "Unknown Application"; + } + + let activityText; + if (msg.activity.type === DISCORD_MESSAGE_ACTIVITY_TYPES.JOIN || msg.activity.type === DISCORD_MESSAGE_ACTIVITY_TYPES.JOIN_REQUEST) { + activityText = "join a game"; + } else if (msg.activity.type === DISCORD_MESSAGE_ACTIVITY_TYPES.SPECTATE) { + activityText = "spectate"; + } else if (msg.activity.type === DISCORD_MESSAGE_ACTIVITY_TYPES.LISTEN) { + activityText = "listen along"; + } else { + activityText = "do something"; + } + + messageContent += `\n\n**`; + messageContent = messageContent.trim(); + } + // Save DB entry let threadMessage = new ThreadMessage({ message_type: THREAD_MESSAGE_TYPE.FROM_USER, user_id: this.user_id, user_name: fullUserName, - body: msg.content || "", + body: messageContent, is_anonymous: 0, dm_message_id: msg.id, attachments, diff --git a/src/data/constants.js b/src/data/constants.js index 067cb06..221becd 100644 --- a/src/data/constants.js +++ b/src/data/constants.js @@ -26,6 +26,14 @@ module.exports = { GUILD_STORE: 6, }, + // https://discord.com/developers/docs/resources/channel#message-object-message-activity-types + DISCORD_MESSAGE_ACTIVITY_TYPES: { + JOIN: 1, + SPECTATE: 2, + LISTEN: 3, + JOIN_REQUEST: 5, + }, + ACCIDENTAL_THREAD_MESSAGES: [ "ok", "okay", From 78a1cc34befa13e51f06df1a085182f8bb60005e Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 16 Aug 2020 18:36:51 +0300 Subject: [PATCH 063/300] threadOnMention -> createThreadOnMention --- docs/configuration.md | 2 +- src/data/cfg.schema.json | 2 +- src/main.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 8c77988..a770a61 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -272,7 +272,7 @@ The bot's "Playing" text **Default:** `on` If enabled, channel permissions for the thread are synchronized with the category when using `!move`. Requires `allowMove` to be enabled. -#### threadOnMention +#### createThreadOnMention **Default:** `off` If enabled, the bot will automatically create a new thread for a user who pings it. diff --git a/src/data/cfg.schema.json b/src/data/cfg.schema.json index 8955f84..519802d 100644 --- a/src/data/cfg.schema.json +++ b/src/data/cfg.schema.json @@ -254,7 +254,7 @@ "default": "\uD83D\uDCE8" }, - "threadOnMention": { + "createThreadOnMention": { "$ref": "#/definitions/customBoolean", "default": false }, diff --git a/src/main.js b/src/main.js index 7d65fa6..1928886 100644 --- a/src/main.js +++ b/src/main.js @@ -235,7 +235,7 @@ function initBaseMessageHandlers() { } // If configured, automatically open a new thread with a user who has pinged it - if (config.threadOnMention) { + if (config.createThreadOnMention) { const existingThread = await threads.findOpenThreadByUserId(msg.author.id); if (! existingThread) { // Only open a thread if we don't already have one From c7bf0592207fb4139448874c4cc642f03852f5ad Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 16 Aug 2020 18:41:58 +0300 Subject: [PATCH 064/300] When creating a new thread from a mention, include a note and the initial message in the thread --- src/main.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main.js b/src/main.js index 1928886..6e348c2 100644 --- a/src/main.js +++ b/src/main.js @@ -239,7 +239,9 @@ function initBaseMessageHandlers() { const existingThread = await threads.findOpenThreadByUserId(msg.author.id); if (! existingThread) { // Only open a thread if we don't already have one - await threads.createNewThreadForUser(msg.author, { quiet: true }); + const createdThread = await threads.createNewThreadForUser(msg.author, { quiet: true }); + await createdThread.postSystemMessage(`This thread was opened from a bot mention in <#${msg.channel.id}>`); + await createdThread.receiveUserReply(msg); } } }); From 51df75e641877b8d87ec7fd34924425ac4a6a4de Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 16 Aug 2020 18:42:19 +0300 Subject: [PATCH 065/300] Set dm_channel_id directly from the message object Allows us to set the channel id correctly for non-DM messages, such as mentions that create a thread with createThreadOnMention where the initial message is forwarded to the thread. --- src/data/Thread.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/data/Thread.js b/src/data/Thread.js index ffedd55..4d91564 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -310,6 +310,7 @@ class Thread { body: messageContent, is_anonymous: 0, dm_message_id: msg.id, + dm_channel_id: msg.channel.id, attachments, small_attachments: smallAttachments, }); From 7a671eab1f0eabb746212d0da5e42ff9398b1b0b Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 16 Aug 2020 18:45:11 +0300 Subject: [PATCH 066/300] Ignore errors from adding a reaction with reactOnSeen --- src/data/Thread.js | 2 +- src/utils.js | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/data/Thread.js b/src/data/Thread.js index 4d91564..5f4263b 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -325,7 +325,7 @@ class Thread { } if (config.reactOnSeen) { - await msg.addReaction(config.reactOnSeenEmoji); + await msg.addReaction(config.reactOnSeenEmoji).catch(utils.noop); } // Interrupt scheduled closing, if in progress diff --git a/src/utils.js b/src/utils.js index 791ca67..8c993b2 100644 --- a/src/utils.js +++ b/src/utils.js @@ -310,13 +310,12 @@ function disableCodeBlocks(str) { return str.replace(/`/g, "`\u200b"); } -/** - * - */ function readMultilineConfigValue(str) { return Array.isArray(str) ? str.join("\n") : str; } +function noop() {} + module.exports = { BotError, @@ -355,4 +354,6 @@ module.exports = { disableCodeBlocks, readMultilineConfigValue, + + noop, }; From fbcdec5a4b6d163e0f4074baeb04e4e5cbbb3d52 Mon Sep 17 00:00:00 2001 From: David <49169805+mesub7@users.noreply.github.com> Date: Sun, 16 Aug 2020 17:01:52 +0100 Subject: [PATCH 067/300] Update configuration.md (#411) Removes the "wrapped in quotes section" and adds "accepts multiple values" for mainGuildID --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index a770a61..fb50674 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -66,7 +66,7 @@ greetingMessage[] = Fourth line! With an empty line in the middle. The bot user's token from [Discord Developer Portal](https://discordapp.com/developers/). #### mainGuildId -Your server's ID, wrapped in quotes. +**Accepts multiple values** Your server's ID. #### mailGuildId For a two-server setup, the inbox server's ID. From 70a1ae2c12dc415cb2a6cb89ccc7f73bd4ac4907 Mon Sep 17 00:00:00 2001 From: Miikka <2606411+Dragory@users.noreply.github.com> Date: Sun, 16 Aug 2020 19:12:22 +0300 Subject: [PATCH 068/300] Create dependabot.yml --- .github/dependabot.yml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..04d2265 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,9 @@ +version: 2 +updates: + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "monthly" + target-branch: "dev" + allow: + - dependency-type: "direct" From 23844ae4ac699bd58f8c41a6a40bf73335154f9f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 16 Aug 2020 16:13:07 +0000 Subject: [PATCH 069/300] Bump ajv from 6.12.3 to 6.12.4 Bumps [ajv](https://github.com/ajv-validator/ajv) from 6.12.3 to 6.12.4. - [Release notes](https://github.com/ajv-validator/ajv/releases) - [Commits](https://github.com/ajv-validator/ajv/compare/v6.12.3...v6.12.4) Signed-off-by: dependabot[bot] --- package-lock.json | 12 ++++++------ package.json | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 876df54..e52fb52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -76,9 +76,9 @@ } }, "ajv": { - "version": "6.12.3", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.3.tgz", - "integrity": "sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA==", + "version": "6.12.4", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz", + "integrity": "sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ==", "requires": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -1287,9 +1287,9 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, "fast-levenshtein": { "version": "2.0.6", diff --git a/package.json b/package.json index 88d33ae..f9740db 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "url": "https://github.com/Dragory/modmailbot" }, "dependencies": { - "ajv": "^6.12.3", + "ajv": "^6.12.4", "eris": "^0.13.3", "humanize-duration": "^3.12.1", "ini": "^1.3.5", From 2ce5eee23a8e8802c91247b31aa49d7b3c0e8663 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 16 Aug 2020 16:13:16 +0000 Subject: [PATCH 070/300] Bump moment from 2.24.0 to 2.27.0 Bumps [moment](https://github.com/moment/moment) from 2.24.0 to 2.27.0. - [Release notes](https://github.com/moment/moment/releases) - [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md) - [Commits](https://github.com/moment/moment/compare/2.24.0...2.27.0) Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index e52fb52..fac453d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2772,9 +2772,9 @@ } }, "moment": { - "version": "2.24.0", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", - "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.27.0.tgz", + "integrity": "sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ==" }, "ms": { "version": "2.1.2", diff --git a/package.json b/package.json index f9740db..6f0fad0 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "knex": "^0.21.4", "knub-command-manager": "^6.1.0", "mime": "^2.4.4", - "moment": "^2.24.0", + "moment": "^2.27.0", "mv": "^2.1.1", "mysql2": "^2.1.0", "public-ip": "^4.0.0", From 57fe492186c019d58e751095b9abfc3ee159c82b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 16 Aug 2020 16:13:23 +0000 Subject: [PATCH 071/300] Bump uuid from 3.3.3 to 8.3.0 Bumps [uuid](https://github.com/uuidjs/uuid) from 3.3.3 to 8.3.0. - [Release notes](https://github.com/uuidjs/uuid/releases) - [Changelog](https://github.com/uuidjs/uuid/blob/master/CHANGELOG.md) - [Commits](https://github.com/uuidjs/uuid/compare/v3.3.3...v8.3.0) Signed-off-by: dependabot[bot] --- package-lock.json | 14 +++++++++++--- package.json | 2 +- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index fac453d..e5b4fa6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3503,6 +3503,14 @@ "tough-cookie": "~2.5.0", "tunnel-agent": "^0.6.0", "uuid": "^3.3.2" + }, + "dependencies": { + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "optional": true + } } }, "require-directory": { @@ -4266,9 +4274,9 @@ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "uuid": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", - "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==" + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz", + "integrity": "sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ==" }, "v8-compile-cache": { "version": "2.1.1", diff --git a/package.json b/package.json index 6f0fad0..8ff2f32 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "sqlite3": "^5.0.0", "tmp": "^0.1.0", "transliteration": "^2.1.11", - "uuid": "^3.3.3" + "uuid": "^8.3.0" }, "devDependencies": { "eslint": "^7.6.0", From 797dd0f8292eeca1630e6dc7f690cfa44c9714fa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 16 Aug 2020 16:15:23 +0000 Subject: [PATCH 072/300] Bump eslint from 7.6.0 to 7.7.0 Bumps [eslint](https://github.com/eslint/eslint) from 7.6.0 to 7.7.0. - [Release notes](https://github.com/eslint/eslint/releases) - [Changelog](https://github.com/eslint/eslint/blob/master/CHANGELOG.md) - [Commits](https://github.com/eslint/eslint/compare/v7.6.0...v7.7.0) Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index e5b4fa6..c252301 100644 --- a/package-lock.json +++ b/package-lock.json @@ -916,9 +916,9 @@ "dev": true }, "eslint": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.6.0.tgz", - "integrity": "sha512-QlAManNtqr7sozWm5TF4wIH9gmUm2hE3vNRUvyoYAa4y1l5/jxD/PQStEjBMQtCqZmSep8UxrcecI60hOpe61w==", + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.7.0.tgz", + "integrity": "sha512-1KUxLzos0ZVsyL81PnRN335nDtQ8/vZUD6uMtWbF+5zDtjKcsklIi78XoE0MVL93QvWTu+E5y44VyyCsOMBrIg==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", diff --git a/package.json b/package.json index 8ff2f32..fba376d 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "uuid": "^8.3.0" }, "devDependencies": { - "eslint": "^7.6.0", + "eslint": "^7.7.0", "husky": "^4.2.5", "lint-staged": "^10.2.11", "supervisor": "^0.12.0" From 582944cb43f3808b2ef8e09c186b0cc0866e4d19 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 16 Aug 2020 16:15:18 +0000 Subject: [PATCH 073/300] Bump public-ip from 4.0.0 to 4.0.2 Bumps [public-ip](https://github.com/sindresorhus/public-ip) from 4.0.0 to 4.0.2. - [Release notes](https://github.com/sindresorhus/public-ip/releases) - [Commits](https://github.com/sindresorhus/public-ip/compare/v4.0.0...v4.0.2) Signed-off-by: dependabot[bot] --- package-lock.json | 26 +++++++++++++------------- package.json | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index c252301..da39d28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -761,9 +761,9 @@ "dev": true }, "defer-to-connect": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.0.tgz", - "integrity": "sha512-WE2sZoctWm/v4smfCAdjYbrfS55JiMRdlY9ZubFhsYbteCK9+BvAx4YV7nPjYM6ZnX5BcoVKwfmyx9sIFTgQMQ==" + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", + "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==" }, "define-property": { "version": "2.0.2", @@ -837,9 +837,9 @@ } }, "dns-socket": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/dns-socket/-/dns-socket-4.2.0.tgz", - "integrity": "sha512-4XuD3z28jht3jvHbiom6fAipgG5LkjYeDLrX5OH8cbl0AtzTyUUAxGckcW8T7z0pLfBBV5qOiuC4wUEohk6FrQ==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/dns-socket/-/dns-socket-4.2.1.tgz", + "integrity": "sha512-fNvDq86lS522+zMbh31X8cQzYQd6xumCNlxsuZF5TKxQThF/e+rJbVM6K8mmlsdcSm6yNjKJQq3Sf38viAJj8g==", "requires": { "dns-packet": "^5.1.2" } @@ -1698,9 +1698,9 @@ } }, "http-cache-semantics": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.0.3.tgz", - "integrity": "sha512-TcIMG3qeVLgDr1TEd2XvHaTnMPwYQUQMIBLy+5pLSDKYFc7UIqj39w8EGzZkaxoLv/l2K8HaI0t5AVA+YYgUew==" + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" }, "http-signature": { "version": "1.2.0", @@ -3382,11 +3382,11 @@ "optional": true }, "public-ip": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/public-ip/-/public-ip-4.0.0.tgz", - "integrity": "sha512-Q5dcQ5qLPpMSyj0iEqucTfeINHVeEhuSjjaPVEU24+6RGlvCrEM6AXwOlWW4iIn10yyWROASRuS3rgbUZIE/5g==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/public-ip/-/public-ip-4.0.2.tgz", + "integrity": "sha512-ZHqUjaYT/+FuSiy5/o2gBxvj0PF7M3MXGnaLJBsJNMCyXI4jzuXXHJKrk0gDxx1apiF/jYsBwjTQOM9V8G6oCQ==", "requires": { - "dns-socket": "^4.2.0", + "dns-socket": "^4.2.1", "got": "^9.6.0", "is-ip": "^3.1.0" } diff --git a/package.json b/package.json index fba376d..492b126 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "moment": "^2.27.0", "mv": "^2.1.1", "mysql2": "^2.1.0", - "public-ip": "^4.0.0", + "public-ip": "^4.0.2", "sqlite3": "^5.0.0", "tmp": "^0.1.0", "transliteration": "^2.1.11", From 034a9172c48b61534a7cecb5a84ab4cd081366ed Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 16 Aug 2020 19:22:50 +0300 Subject: [PATCH 074/300] Update config JSDoc --- src/data/cfg.jsdoc.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/data/cfg.jsdoc.js b/src/data/cfg.jsdoc.js index 49a21f3..8d614c7 100644 --- a/src/data/cfg.jsdoc.js +++ b/src/data/cfg.jsdoc.js @@ -45,6 +45,9 @@ * @property {boolean} [updateNotifications=true] * @property {array} [plugins=[]] * @property {*} [commandAliases] + * @property {boolean} [reactOnSeen=false] + * @property {string} [reactOnSeenEmoji="📨"] + * @property {boolean} [createThreadOnMention=false] * @property {number} [port=8890] * @property {string} [url] * @property {array} [extraIntents=[]] From 2d2ae2a118a5db5ccea3a982d1f2012e284747ad Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 16 Aug 2020 19:55:59 +0300 Subject: [PATCH 075/300] Update CHANGELOG --- CHANGELOG.md | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6ede84..8b50872 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,32 +1,42 @@ # Changelog ## v2.31.0-beta.1 -This is a beta release. It is not available on the Releases page and bugs are expected. +This is a beta release. Please report any bugs you encounter! -General changes: +**General changes:** * Added support for Node.js 14, dropped support for Node.js 10 and 11 * The supported versions are now 12, 13, and 14 * Added support for editing and deleting staff replies * This can be disabled with the `allowStaffEdit` and `allowStaffDelete` options * Only the staff member who sent the reply can edit/delete it +* New option `reactOnSeen` ([#398](https://github.com/Dragory/modmailbot/pull/398) by @Eegras) + * When enabled, the bot will react to user DMs with a checkmark when they have been received + * The reaction emoji can be customized with the `reactOnSeenEmoji` option +* New option `createThreadOnMention` ([#397](https://github.com/Dragory/modmailbot/pull/397) by @dopeghoti) + * When enabled, a new modmail thread will be created whenever a user mentions/pings the bot on the main server + * As with `pingOnBotMention`, staff members are automatically ignored * New **default** attachment storage option: `original` * This option simply links the original attachment and does not rehost it in any way +* Multiple people can now sign up for reply alerts (`!alert`) simultaneously ([#373](https://github.com/Dragory/modmailbot/pull/373) by @DarkView) +* The bot now displays a note if the user sent an application invite, e.g. an invite to listen along on Spotify +* Messages in modmail threads by other bots are no longer ignored, and are displayed in logs * Added official support for MySQL databases. Refer to the documentation on `dbType` for more details. * This change also means the following options are **no longer supported:** * `dbDir` (use `sqliteOptions.filename` to specify the database file instead) * `knex` (see `dbType` documentation for more details) * Removed the long-deprecated `logDir` option -Plugins: +**Plugins:** * Added support for replacing default message formatting in threads, DMs, and logs * Added support for *hooks*. Hooks can be used to run custom plugin code before/after specific moments. * Initially only the `beforeNewThread` hook is available. See plugin documentation for more details. * If your plugin requires special gateway intents, use the new `extraIntents` config option -Internal/technical updates: +**Internal/technical updates:** * Updated to Eris 0.13.3 +* Updated several other dependencies * The client now requests specific gateway intents on connection -* New config parser that validates each option and their types more strictly to prevent undefined behavior +* New JSON Schema based config parser that validates each option and their types more strictly to prevent undefined behavior ## v2.31.0-beta.0 This is a beta release. It is not available on the Releases page and bugs are expected. From 778a30941d0097032f11bec7fbbfa80609a35da9 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 16 Aug 2020 19:58:10 +0300 Subject: [PATCH 076/300] Update CHANGELOG --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b50872..de50cbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ # Changelog ## v2.31.0-beta.1 -This is a beta release. Please report any bugs you encounter! +**This is a beta release, bugs are expected.** +Please report any bugs you encounter by [creating a GitHub issue](https://github.com/Dragory/modmailbot/issues/new)! **General changes:** * Added support for Node.js 14, dropped support for Node.js 10 and 11 @@ -33,7 +34,7 @@ This is a beta release. Please report any bugs you encounter! * If your plugin requires special gateway intents, use the new `extraIntents` config option **Internal/technical updates:** -* Updated to Eris 0.13.3 +* Updated Eris to v0.13.3 * Updated several other dependencies * The client now requests specific gateway intents on connection * New JSON Schema based config parser that validates each option and their types more strictly to prevent undefined behavior From f61b1cc397a49d745e5f47c0226796c02fcc031e Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 16 Aug 2020 22:14:49 +0300 Subject: [PATCH 077/300] Require new text arg in !edit_snippet. Fixes #413 --- src/modules/snippets.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/snippets.js b/src/modules/snippets.js index 2a5ae73..e68d6a3 100644 --- a/src/modules/snippets.js +++ b/src/modules/snippets.js @@ -113,7 +113,7 @@ module.exports = ({ bot, knex, config, commands }) => { aliases: ["ds"] }); - commands.addInboxServerCommand("edit_snippet", " [text$]", async (msg, args, thread) => { + commands.addInboxServerCommand("edit_snippet", " ", async (msg, args, thread) => { const snippet = await snippets.get(args.trigger); if (! snippet) { utils.postSystemMessageWithFallback(msg.channel, thread, `Snippet "${args.trigger}" doesn't exist!`); From 2ceefbe1ec8c24317b17873da12bc81c6a52b83e Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 16 Aug 2020 22:16:36 +0300 Subject: [PATCH 078/300] Update CHANGELOG --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de50cbe..88ef9c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Changelog ## v2.31.0-beta.1 -**This is a beta release, bugs are expected.** +**This is a beta release, bugs are expected.** Please report any bugs you encounter by [creating a GitHub issue](https://github.com/Dragory/modmailbot/issues/new)! **General changes:** @@ -26,6 +26,7 @@ Please report any bugs you encounter by [creating a GitHub issue](https://github * `dbDir` (use `sqliteOptions.filename` to specify the database file instead) * `knex` (see `dbType` documentation for more details) * Removed the long-deprecated `logDir` option +* Fixed `!edit_snippet` crashing the bot when leaving the new snippet text empty **Plugins:** * Added support for replacing default message formatting in threads, DMs, and logs From 60ae79d4e44103098fccf4d83e739dee43062d5f Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 16 Aug 2020 22:21:49 +0300 Subject: [PATCH 079/300] Update setup instructions --- docs/setup.md | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/docs/setup.md b/docs/setup.md index 4b68dbd..fc6ef10 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -13,10 +13,9 @@ To keep it online, you need to keep the bot process running. ## Prerequisites 1. Create a bot account through the [Discord Developer Portal](https://discordapp.com/developers/) -2. Invite the created bot to your server -3. Install Node.js 11, 12, 13, or 14 -4. Download the latest bot version from [the releases page](https://github.com/Dragory/modmailbot/releases) and extract it to a folder -5. In the bot's folder, make a copy of the file `config.example.ini` and rename the copy to `config.ini` +2. Install Node.js 12, 13, or 14 +3. Download the latest bot version from [the releases page](https://github.com/Dragory/modmailbot/releases) and extract it to a folder +4. In the bot's folder, make a copy of the file `config.example.ini` and rename the copy to `config.ini` ## Single-server setup In this setup, modmail threads are opened on the main server in a special category. @@ -24,12 +23,14 @@ This is the recommended setup for small and medium sized servers. 1. **Go through the [prerequisites](#prerequisites) above first!** 2. Open `config.ini` in a text editor and fill in the required values. `mainGuildId` and `mailGuildId` should both be set to your server's id. -3. On a new line at the end of `config.ini`, add `categoryAutomation.newThread = CATEGORY_ID_HERE` - - Replace `CATEGORY_ID_HERE` with the ID of the category where new modmail threads should go -4. Make sure the bot has `Manage Channels`, `Manage Messages`, and `Attach Files` permissions in the category -5. **[🏃 Start the bot!](starting-the-bot.md)** -6. Want to change other bot options? See **[📝 Configuration](configuration.md)** -7. Have any other questions? Check out the **[🙋 Frequently Asked Questions](faq.md)** or +3. Invite the bot to the server +4. On a new line at the end of `config.ini`, add `categoryAutomation.newThread = CATEGORY_ID_HERE` + * Replace `CATEGORY_ID_HERE` with the ID of the category where new modmail threads should go +5. Make sure the bot has `Manage Channels`, `Manage Messages`, and `Attach Files` permissions in the category + * It is not recommended to give the bot Administrator permissions under *any* circumstances +6. **[🏃 Start the bot!](starting-the-bot.md)** +7. Want to change other bot options? See **[📝 Configuration](configuration.md)** +8. Have any other questions? Check out the **[🙋 Frequently Asked Questions](faq.md)** or **[join the support server!](../README.md#support-server)** ## Two-server setup @@ -39,12 +40,16 @@ You might also want this setup for privacy concerns*. 1. **Go through the [prerequisites](#prerequisites) above first!** 2. Create an inbox server on Discord -3. Invite the bot to the inbox server. -4. Open `config.ini` in a text editor and fill in the values -5. Make sure the bot has the `Manage Channels`, `Manage Messages`, and `Attach Files` permissions on the **inbox** server -6. **[🏃 Start the bot!](starting-the-bot.md)** -7. Want to change other bot options? See **[📝 Configuration](configuration.md)** -8. Have any other questions? Check out the **[🙋 Frequently Asked Questions](faq.md)** or +3. Open `config.ini` in a text editor and fill in the required values + * Set `mainGuildId` to the ID of the *main* server where users will message the bot from + * Set `mailGuildId` to the ID of the *inbox* server created in step 2 +4. Invite the bot to both the main server and the newly-created inbox server +5. Open `config.ini` in a text editor and fill in the values +6. Make sure the bot has the `Manage Channels`, `Manage Messages`, and `Attach Files` permissions on the **inbox** server + * The bot does not need any permissions on the main server +7. **[🏃 Start the bot!](starting-the-bot.md)** +8. Want to change other bot options? See **[📝 Configuration](configuration.md)** +9. Have any other questions? Check out the **[🙋 Frequently Asked Questions](faq.md)** or **[join the support server!](../README.md#support-server)** *\* Since all channel names, even for channels you can't see, are public information through the API, a user with a From d5219556a7e848aea6914278c498cd2152d4e611 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 16 Aug 2020 23:26:04 +0300 Subject: [PATCH 080/300] Add full JSDocs for the plugin API --- docs/plugins.md | 42 +++++++++++++++----- src/commands.js | 62 +++++++++++++++++++++++------ src/data/attachments.js | 76 ++++++++++++++++++++++++++---------- src/formatters.js | 38 +++++++----------- src/hooks/beforeNewThread.js | 13 ++++-- src/pluginApi.js | 34 ++++++++++++++++ src/plugins.js | 7 ++++ 7 files changed, 204 insertions(+), 68 deletions(-) create mode 100644 src/pluginApi.js diff --git a/docs/plugins.md b/docs/plugins.md index 70fb617..52af73d 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -7,17 +7,11 @@ The path is relative to the bot's folder. Plugins are automatically loaded on bot startup. ## Creating a plugin -Create a `.js` file that exports a function. -This function will be called when the plugin is loaded, with 1 argument: an object that has the following properties: -* `bot` - the [Eris Client object](https://abal.moe/Eris/docs/Client) -* `knex` - the [Knex database object](https://knexjs.org/#Builder) -* `config` - the loaded config -* `commands` - an object with functions to add and manage commands -* `attachments` - an object with functions to save attachments and manage attachment storage types +Plugins are simply `.js` files that export a function that gets called when the plugin is loaded. -See [src/plugins.js#L4](../src/plugins.js#L4) for more details +For details about the function arguments, see [Plugin API](#plugin-api) below. -### Example plugin file +### Example plugin This example adds a command `!mycommand` that replies with `"Reply from my custom plugin!"` when the command is used inside a modmail inbox thread channel. ```js module.exports = function({ bot, knex, config, commands }) { @@ -40,6 +34,36 @@ module.exports = function({ attachments }) { ``` To use this custom attachment storage type, you would set the `attachmentStorage` config option to `"original"`. +### Plugin API +The first and only argument to the plugin function is an object with the following properties: + +| Property | Description | +| -------- | ----------- | +| `bot` | [Eris Client instance](https://abal.moe/Eris/docs/Client) | +| `knex` | [Knex database object](https://knexjs.org/#Builder) | +| `config` | The loaded config | +| `commands` | An object with functions to add and manage commands | +| `attachments` | An object with functions to save attachments and manage attachment storage types | +| — `addStorageType(name, handler)` | Function to add a new attachment storage type | +| — `downloadAttachment(attachment)` | Function to add a new attachment storage type | + +* `bot` - the [Eris Client object](https://abal.moe/Eris/docs/Client) +* `knex` - the [Knex database object](https://knexjs.org/#Builder) +* `config` - the loaded config +* `commands` - an object with functions to add and manage commands +* `attachments` - an object with functions to save attachments and manage attachment storage types + * `attachments.addStorageType(name, handler)` + +Create a `.js` file that exports a function. +This function will be called when the plugin is loaded, with 1 argument: an object that has the following properties: +* `bot` - the [Eris Client object](https://abal.moe/Eris/docs/Client) +* `knex` - the [Knex database object](https://knexjs.org/#Builder) +* `config` - the loaded config +* `commands` - an object with functions to add and manage commands +* `attachments` - an object with functions to save attachments and manage attachment storage types + +See [src/plugins.js#L4](../src/plugins.js#L4) for more details + ## Work in progress The current plugin API is fairly rudimentary and will be expanded on in the future. The API can change in non-major releases during this early stage. Keep an eye on [CHANGELOG.md](../CHANGELOG.md) for any changes. diff --git a/src/commands.js b/src/commands.js index cdc95de..a0c34e2 100644 --- a/src/commands.js +++ b/src/commands.js @@ -5,6 +5,51 @@ const utils = require("./utils"); const threads = require("./data/threads"); const Thread = require("./data/Thread"); +/** + * @callback CommandFn + * @param {Eris.Message} msg + * @param {object} args + */ + +/** + * @callback InboxThreadCommandHandler + * @param {Eris.Message} msg + * @param {object} args + * @param {Thread} thread + */ + +/** + * @callback AddGlobalCommandFn + * @param {string} trigger + * @param {string} parameters + * @param {CommandFn} handler + * @param {ICommandConfig} commandConfig + */ + +/** + * @callback AddInboxServerCommandFn + * @param {string} trigger + * @param {string} parameters + * @param {CommandFn} handler + * @param {ICommandConfig} commandConfig + */ + +/** + * @callback AddInboxThreadCommandFn + * Add a command that can only be invoked in a thread on the inbox server + * + * @param {string} trigger + * @param {string} parameters + * @param {InboxThreadCommandHandler} handler + * @param {ICommandConfig} commandConfig + */ + +/** + * @callback AddAliasFn + * @param {string} originalCmd + * @param {string} alias + */ + module.exports = { createCommandManager(bot) { const manager = new CommandManager({ @@ -52,6 +97,7 @@ module.exports = { /** * Add a command that can be invoked anywhere + * @type {AddGlobalCommandFn} */ const addGlobalCommand = (trigger, parameters, handler, commandConfig = {}) => { let aliases = aliasMap.has(trigger) ? [...aliasMap.get(trigger)] : []; @@ -63,6 +109,7 @@ module.exports = { /** * Add a command that can only be invoked on the inbox server + * @type {AddInboxServerCommandFn} */ const addInboxServerCommand = (trigger, parameters, handler, commandConfig = {}) => { const aliases = aliasMap.has(trigger) ? [...aliasMap.get(trigger)] : []; @@ -86,19 +133,9 @@ module.exports = { }; }; - /** - * @callback InboxThreadCommandHandler - * @param {Eris.Message} msg - * @param {object} args - * @param {Thread} thread - */ - /** * Add a command that can only be invoked in a thread on the inbox server - * @param {string|RegExp} trigger - * @param {string|IParameter[]} parameters - * @param {InboxThreadCommandHandler} handler - * @param {ICommandConfig} commandConfig + * @type {AddInboxThreadCommandFn} */ const addInboxThreadCommand = (trigger, parameters, handler, commandConfig = {}) => { const aliases = aliasMap.has(trigger) ? [...aliasMap.get(trigger)] : []; @@ -124,6 +161,9 @@ module.exports = { }; }; + /** + * @type {AddAliasFn} + */ const addAlias = (originalCmd, alias) => { if (! aliasMap.has(originalCmd)) { aliasMap.set(originalCmd, new Set()); diff --git a/src/data/attachments.js b/src/data/attachments.js index c26d056..a5392c0 100644 --- a/src/data/attachments.js +++ b/src/data/attachments.js @@ -26,21 +26,54 @@ function getErrorResult(msg = null) { } /** - * An attachment storage option that simply forwards the original attachment URL - * @param {Eris.Attachment} attachment - * @returns {{url: string}} + * @callback AddAttachmentStorageTypeFn + * @param {string} name + * @param {AttachmentStorageTypeHandler} handler */ -function passthroughOriginalAttachment(attachment) { + +/** + * @callback AttachmentStorageTypeHandler + * @param {Eris.Attachment} attachment + * @return {AttachmentStorageTypeResult|Promise} + */ + +/** + * @typedef {object} AttachmentStorageTypeResult + * @property {string} url + */ + +/** + * @callback DownloadAttachmentFn + * @param {Eris.Attachment} attachment + * @param {number?} tries Used internally, don't pass + * @return {Promise} + */ + +/** + * @typedef {object} DownloadAttachmentResult + * @property {string} path + * @property {DownloadAttachmentCleanupFn} cleanup + */ + +/** + * @callback DownloadAttachmentCleanupFn + * @return {void} + */ + +/** + * @type {AttachmentStorageTypeHandler} + */ +let passthroughOriginalAttachment; // Workaround to inconsistent IDE bug with @type and anonymous functions +passthroughOriginalAttachment = (attachment) => { return { url: attachment.url }; -} +}; /** * An attachment storage option that downloads each attachment and serves them from a local web server - * @param {Eris.Attachment} attachment - * @param {Number=0} tries - * @returns {Promise<{ url: string }>} + * @type {AttachmentStorageTypeHandler} */ -async function saveLocalAttachment(attachment) { +let saveLocalAttachment; // Workaround to inconsistent IDE bug with @type and anonymous functions +saveLocalAttachment = async (attachment) => { const targetPath = getLocalAttachmentPath(attachment.id); try { @@ -60,14 +93,12 @@ async function saveLocalAttachment(attachment) { const url = await getLocalAttachmentUrl(attachment.id, attachment.filename); return { url }; -} +}; /** - * @param {Eris.Attachment} attachment - * @param {Number} tries - * @returns {Promise<{ path: string, cleanup: function }>} + * @type {DownloadAttachmentFn} */ -function downloadAttachment(attachment, tries = 0) { +const downloadAttachment = (attachment, tries = 0) => { return new Promise((resolve, reject) => { if (tries > 3) { console.error("Attachment download failed after 3 tries:", attachment); @@ -94,7 +125,7 @@ function downloadAttachment(attachment, tries = 0) { }); }); }); -} +}; /** * Returns the filesystem path for the given attachment id @@ -119,10 +150,10 @@ function getLocalAttachmentUrl(attachmentId, desiredName = null) { /** * An attachment storage option that downloads each attachment and re-posts them to a specified Discord channel. * The re-posted attachment is then linked in the actual thread. - * @param {Eris.Attachment} attachment - * @returns {Promise<{ url: string }>} + * @type {AttachmentStorageTypeHandler} */ -async function saveDiscordAttachment(attachment) { +let saveDiscordAttachment; // Workaround to inconsistent IDE bug with @type and anonymous functions +saveDiscordAttachment = async (attachment) => { if (attachment.size > 1024 * 1024 * 8) { return getErrorResult("attachment too large (max 8MB)"); } @@ -144,7 +175,7 @@ async function saveDiscordAttachment(attachment) { if (! savedAttachment) return getErrorResult(); return { url: savedAttachment.url }; -} +}; async function createDiscordAttachmentMessage(channel, file, tries = 0) { tries++; @@ -197,9 +228,12 @@ function saveAttachment(attachment) { return attachmentSavePromises[attachment.id]; } -function addStorageType(name, handler) { +/** + * @type AddAttachmentStorageTypeFn + */ +const addStorageType = (name, handler) => { attachmentStorageTypes[name] = handler; -} +}; addStorageType("original", passthroughOriginalAttachment); addStorageType("local", saveLocalAttachment); diff --git a/src/formatters.js b/src/formatters.js index e329685..6693276 100644 --- a/src/formatters.js +++ b/src/formatters.js @@ -212,6 +212,20 @@ const defaultFormatters = { */ const formatters = { ...defaultFormatters }; +/** + * @typedef {object} FormattersExport + * @property {MessageFormatters} formatters Read only + * @property {function(FormatStaffReplyDM): void} setStaffReplyDMFormatter + * @property {function(FormatStaffReplyThreadMessage): void} setStaffReplyThreadMessageFormatter + * @property {function(FormatUserReplyThreadMessage): void} setUserReplyThreadMessageFormatter + * @property {function(FormatStaffReplyEditNotificationThreadMessage): void} setStaffReplyEditNotificationThreadMessageFormatter + * @property {function(FormatStaffReplyDeletionNotificationThreadMessage): void} setStaffReplyDeletionNotificationThreadMessageFormatter + * @property {function(FormatLog): void} setLogFormatter + */ + +/** + * @type {FormattersExport} + */ module.exports = { formatters: new Proxy(formatters, { set() { @@ -219,50 +233,26 @@ module.exports = { }, }), - /** - * @param {FormatStaffReplyDM} fn - * @return {void} - */ setStaffReplyDMFormatter(fn) { formatters.formatStaffReplyDM = fn; }, - /** - * @param {FormatStaffReplyThreadMessage} fn - * @return {void} - */ setStaffReplyThreadMessageFormatter(fn) { formatters.formatStaffReplyThreadMessage = fn; }, - /** - * @param {FormatUserReplyThreadMessage} fn - * @return {void} - */ setUserReplyThreadMessageFormatter(fn) { formatters.formatUserReplyThreadMessage = fn; }, - /** - * @param {FormatStaffReplyEditNotificationThreadMessage} fn - * @return {void} - */ setStaffReplyEditNotificationThreadMessageFormatter(fn) { formatters.formatStaffReplyEditNotificationThreadMessage = fn; }, - /** - * @param {FormatStaffReplyDeletionNotificationThreadMessage} fn - * @return {void} - */ setStaffReplyDeletionNotificationThreadMessageFormatter(fn) { formatters.formatStaffReplyDeletionNotificationThreadMessage = fn; }, - /** - * @param {FormatLog} fn - * @return {void} - */ setLogFormatter(fn) { formatters.formatLog = fn; }, diff --git a/src/hooks/beforeNewThread.js b/src/hooks/beforeNewThread.js index 9805843..e0173aa 100644 --- a/src/hooks/beforeNewThread.js +++ b/src/hooks/beforeNewThread.js @@ -26,17 +26,24 @@ const Eris = require("eris"); * @return {void|Promise} */ +/** + * @callback AddBeforeNewThreadHookFn + * @param {BeforeNewThreadHookData} fn + * @return {void} + */ + /** * @type BeforeNewThreadHookData[] */ const beforeNewThreadHooks = []; /** - * @param {BeforeNewThreadHookData} fn + * @type {AddBeforeNewThreadHookFn} */ -function beforeNewThread(fn) { +let beforeNewThread; // Workaround to inconsistent IDE bug with @type and anonymous functions +beforeNewThread = (fn) => { beforeNewThreadHooks.push(fn); -} +}; /** * @param {{ diff --git a/src/pluginApi.js b/src/pluginApi.js new file mode 100644 index 0000000..65d8227 --- /dev/null +++ b/src/pluginApi.js @@ -0,0 +1,34 @@ +const { CommandManager } = require("knub-command-manager"); +const { Client } = require("eris"); +const Knex = require("knex"); + +/** + * @typedef {object} PluginAPI + * @property {Client} bot + * @property {Knex} knex + * @property {ModmailConfig} config + * @property {PluginCommandsAPI} commands + * @property {PluginAttachmentsAPI} attachments + * @property {PluginHooksAPI} hooks + * @property {FormattersExport} formats + */ + +/** + * @typedef {object} PluginCommandsAPI + * @property {CommandManager} manager + * @property {AddGlobalCommandFn} addGlobalCommand + * @property {AddInboxServerCommandFn} addInboxServerCommand + * @property {AddInboxThreadCommandFn} addInboxThreadCommand + * @property {AddAliasFn} addAlias + */ + +/** + * @typedef {object} PluginAttachmentsAPI + * @property {AddAttachmentStorageTypeFn} addStorageType + * @property {DownloadAttachmentFn} downloadAttachment + */ + +/** + * @typedef {object} PluginHooksAPI + * @property {AddBeforeNewThreadHookFn} beforeNewThread + */ diff --git a/src/plugins.js b/src/plugins.js index d03c2ae..f27cf50 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -3,6 +3,13 @@ const { beforeNewThread } = require("./hooks/beforeNewThread"); const formats = require("./formatters"); module.exports = { + /** + * @param bot + * @param knex + * @param config + * @param commands + * @returns {PluginAPI} + */ getPluginAPI({ bot, knex, config, commands }) { return { bot, From a863c1b382b6be5c651e5d0d053d1174835fc49f Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 16 Aug 2020 23:40:44 +0300 Subject: [PATCH 081/300] Generate plugin API documentation. Update plugin documentation. --- docs/plugin-api-template.hbs | 6 + docs/plugin-api.md | 69 ++++ docs/plugins.md | 26 +- package-lock.json | 687 ++++++++++++++++++++++++++++++++++- package.json | 2 + 5 files changed, 767 insertions(+), 23 deletions(-) create mode 100644 docs/plugin-api-template.hbs create mode 100644 docs/plugin-api.md diff --git a/docs/plugin-api-template.hbs b/docs/plugin-api-template.hbs new file mode 100644 index 0000000..be49cad --- /dev/null +++ b/docs/plugin-api-template.hbs @@ -0,0 +1,6 @@ +# Plugin API +**NOTE:** This file is generated automatically. + +Scroll down to [PluginAPI](#PluginAPI) for a list of properties available to plugins. + +{{>main}} diff --git a/docs/plugin-api.md b/docs/plugin-api.md new file mode 100644 index 0000000..fa0fdee --- /dev/null +++ b/docs/plugin-api.md @@ -0,0 +1,69 @@ +# Plugin API +**NOTE:** This file is generated automatically. + +Scroll down to [PluginAPI](#PluginAPI) for a list of properties available to plugins. + +## Typedefs + +
+
PluginAPI : object
+
+
PluginCommandsAPI : object
+
+
PluginAttachmentsAPI : object
+
+
PluginHooksAPI : object
+
+
+ + + +## PluginAPI : object +**Kind**: global typedef +**Properties** + +| Name | Type | +| --- | --- | +| bot | Client | +| knex | Knex | +| config | ModmailConfig | +| commands | [PluginCommandsAPI](#PluginCommandsAPI) | +| attachments | [PluginAttachmentsAPI](#PluginAttachmentsAPI) | +| hooks | [PluginHooksAPI](#PluginHooksAPI) | +| formats | FormattersExport | + + + +## PluginCommandsAPI : object +**Kind**: global typedef +**Properties** + +| Name | Type | +| --- | --- | +| manager | CommandManager | +| addGlobalCommand | AddGlobalCommandFn | +| addInboxServerCommand | AddInboxServerCommandFn | +| addInboxThreadCommand | AddInboxThreadCommandFn | +| addAlias | AddAliasFn | + + + +## PluginAttachmentsAPI : object +**Kind**: global typedef +**Properties** + +| Name | Type | +| --- | --- | +| addStorageType | AddAttachmentStorageTypeFn | +| downloadAttachment | DownloadAttachmentFn | + + + +## PluginHooksAPI : object +**Kind**: global typedef +**Properties** + +| Name | Type | +| --- | --- | +| beforeNewThread | AddBeforeNewThreadHookFn | + diff --git a/docs/plugins.md b/docs/plugins.md index 52af73d..bf60e09 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -44,28 +44,12 @@ The first and only argument to the plugin function is an object with the followi | `config` | The loaded config | | `commands` | An object with functions to add and manage commands | | `attachments` | An object with functions to save attachments and manage attachment storage types | -| — `addStorageType(name, handler)` | Function to add a new attachment storage type | -| — `downloadAttachment(attachment)` | Function to add a new attachment storage type | +| `hooks` | An object with functions to add *hooks* that are called at specific times, e.g. before a new thread is created | +| `formats` | An object with functions that allow you to replace the default functions used for formatting messages and logs | -* `bot` - the [Eris Client object](https://abal.moe/Eris/docs/Client) -* `knex` - the [Knex database object](https://knexjs.org/#Builder) -* `config` - the loaded config -* `commands` - an object with functions to add and manage commands -* `attachments` - an object with functions to save attachments and manage attachment storage types - * `attachments.addStorageType(name, handler)` +See the auto-generated [Plugin API](plugin-api.md) page for details. -Create a `.js` file that exports a function. -This function will be called when the plugin is loaded, with 1 argument: an object that has the following properties: -* `bot` - the [Eris Client object](https://abal.moe/Eris/docs/Client) -* `knex` - the [Knex database object](https://knexjs.org/#Builder) -* `config` - the loaded config -* `commands` - an object with functions to add and manage commands -* `attachments` - an object with functions to save attachments and manage attachment storage types - -See [src/plugins.js#L4](../src/plugins.js#L4) for more details - -## Work in progress -The current plugin API is fairly rudimentary and will be expanded on in the future. -The API can change in non-major releases during this early stage. Keep an eye on [CHANGELOG.md](../CHANGELOG.md) for any changes. +## Plugin API stability +Bot releases may contain changes to the plugin API. Make sure to check the [CHANGELOG](../CHANGELOG.md) before upgrading! Please send any feature suggestions to the [issue tracker](https://github.com/Dragory/modmailbot/issues)! diff --git a/package-lock.json b/package-lock.json index da39d28..cfdbf8d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,12 @@ "js-tokens": "^4.0.0" } }, + "@babel/parser": { + "version": "7.11.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.3.tgz", + "integrity": "sha512-REo8xv7+sDxkKvoxEywIdsNFiZLybwdI7hcT5uEPyQrSMB4YQ973BfC9OOrD/81MaIjh6UxdulIQXkjmiH3PcA==", + "dev": true + }, "@sindresorhus/is": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", @@ -92,6 +98,23 @@ "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", "dev": true }, + "ansi-escape-sequences": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-escape-sequences/-/ansi-escape-sequences-4.1.0.tgz", + "integrity": "sha512-dzW9kHxH011uBsidTXd14JXgzye/YLb2LzeKZ4bsgl/Knwx8AtbSFkkGxagdNOoh0DlqHCmfiEjWKBaqjOanVw==", + "dev": true, + "requires": { + "array-back": "^3.0.1" + }, + "dependencies": { + "array-back": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", + "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==", + "dev": true + } + } + }, "ansi-escapes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.0.tgz", @@ -158,6 +181,12 @@ "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" }, + "array-back": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.1.tgz", + "integrity": "sha512-Z/JnaVEXv+A9xabHzN43FiiiWEE7gPCRXMrVmRm00tWbjZRul1iHm7ECzlyNq1p4a4ATXz+G9FJ3GqGOkOV3fg==", + "dev": true + }, "array-each": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", @@ -303,6 +332,12 @@ "inherits": "~2.0.0" } }, + "bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, "brace-expansion": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", @@ -355,6 +390,17 @@ "unset-value": "^1.0.0" } }, + "cache-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cache-point/-/cache-point-2.0.0.tgz", + "integrity": "sha512-4gkeHlFpSKgm3vm2gJN5sPqfmijYRFYCQ6tv5cLw0xVmT6r1z1vd4FNnpuOREco3cBs1G709sZ72LdgddKvL5w==", + "dev": true, + "requires": { + "array-back": "^4.0.1", + "fs-then-native": "^2.0.0", + "mkdirp2": "^1.0.4" + } + }, "cacheable-request": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", @@ -402,6 +448,15 @@ "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", "optional": true }, + "catharsis": { + "version": "0.8.11", + "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.8.11.tgz", + "integrity": "sha512-a+xUyMV7hD1BrDQA/3iPV7oc+6W26BgVJO05PGEoatMyIuPScQKsde6i3YorWX1qs+AZjnJ18NqdKoCtKiNh1g==", + "dev": true, + "requires": { + "lodash": "^4.17.14" + } + }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -599,6 +654,16 @@ "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, + "collect-all": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-all/-/collect-all-1.0.3.tgz", + "integrity": "sha512-0y0rBgoX8IzIjBAUnO73SEtSb4Mhk3IoceWJq5zZSxb9mWORhWH8xLYo4EDSOE1jRBk1LhmfjqWFFt10h/+MEA==", + "dev": true, + "requires": { + "stream-connect": "^1.0.2", + "stream-via": "^1.0.4" + } + }, "collection-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", @@ -637,11 +702,90 @@ "delayed-stream": "~1.0.0" } }, + "command-line-args": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.1.1.tgz", + "integrity": "sha512-hL/eG8lrll1Qy1ezvkant+trihbGnaKaeEjj6Scyr3DN+RC7iQ5Rz84IeLERfAWDGo0HBSNAakczwgCilDXnWg==", + "dev": true, + "requires": { + "array-back": "^3.0.1", + "find-replace": "^3.0.0", + "lodash.camelcase": "^4.3.0", + "typical": "^4.0.0" + }, + "dependencies": { + "array-back": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", + "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==", + "dev": true + }, + "typical": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", + "integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==", + "dev": true + } + } + }, + "command-line-tool": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/command-line-tool/-/command-line-tool-0.8.0.tgz", + "integrity": "sha512-Xw18HVx/QzQV3Sc5k1vy3kgtOeGmsKIqwtFFoyjI4bbcpSgnw2CWVULvtakyw4s6fhyAdI6soQQhXc2OzJy62g==", + "dev": true, + "requires": { + "ansi-escape-sequences": "^4.0.0", + "array-back": "^2.0.0", + "command-line-args": "^5.0.0", + "command-line-usage": "^4.1.0", + "typical": "^2.6.1" + }, + "dependencies": { + "array-back": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-2.0.0.tgz", + "integrity": "sha512-eJv4pLLufP3g5kcZry0j6WXpIbzYw9GUB4mVJZno9wfwiBxbizTnHCw3VJb07cBihbFX48Y7oSrW9y+gt4glyw==", + "dev": true, + "requires": { + "typical": "^2.6.1" + } + } + } + }, + "command-line-usage": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-4.1.0.tgz", + "integrity": "sha512-MxS8Ad995KpdAC0Jopo/ovGIroV/m0KHwzKfXxKag6FHOkGsH8/lv5yjgablcRxCJJC0oJeUMuO/gmaq+Wq46g==", + "dev": true, + "requires": { + "ansi-escape-sequences": "^4.0.0", + "array-back": "^2.0.0", + "table-layout": "^0.4.2", + "typical": "^2.6.1" + }, + "dependencies": { + "array-back": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-2.0.0.tgz", + "integrity": "sha512-eJv4pLLufP3g5kcZry0j6WXpIbzYw9GUB4mVJZno9wfwiBxbizTnHCw3VJb07cBihbFX48Y7oSrW9y+gt4glyw==", + "dev": true, + "requires": { + "typical": "^2.6.1" + } + } + } + }, "commander": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==" }, + "common-sequence": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/common-sequence/-/common-sequence-2.0.0.tgz", + "integrity": "sha512-f0QqPLpRTgMQn/pQIynf+SdE73Lw5Q1jn4hjirHLgH/NJ71TiHjXusV16BmOyuK5rRQ1W2f++II+TFZbQOh4hA==", + "dev": true + }, "compare-versions": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz", @@ -658,6 +802,23 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, + "config-master": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/config-master/-/config-master-3.1.0.tgz", + "integrity": "sha1-ZnZjWQUFooO/JqSE1oSJ10xUhdo=", + "dev": true, + "requires": { + "walk-back": "^2.0.1" + }, + "dependencies": { + "walk-back": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/walk-back/-/walk-back-2.0.1.tgz", + "integrity": "sha1-VU4qnYdPrEeoywBr9EwvDEmYoKQ=", + "dev": true + } + } + }, "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", @@ -828,6 +989,34 @@ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" }, + "dmd": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/dmd/-/dmd-5.0.2.tgz", + "integrity": "sha512-npXsE2+/onRPk/LCrUmx7PcUSqcSVnbrDDMi2nBSawNZ8QXlHE/8xaEZ6pNqPD1lQZv8LGr1xEIpyxP336xyfw==", + "dev": true, + "requires": { + "array-back": "^4.0.1", + "cache-point": "^2.0.0", + "common-sequence": "^2.0.0", + "file-set": "^4.0.1", + "handlebars": "^4.7.6", + "marked": "^1.1.0", + "object-get": "^2.1.1", + "reduce-flatten": "^3.0.0", + "reduce-unique": "^2.0.1", + "reduce-without": "^1.0.1", + "test-value": "^3.0.0", + "walk-back": "^4.0.0" + }, + "dependencies": { + "reduce-flatten": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-3.0.0.tgz", + "integrity": "sha512-eczl8wAYBxJ6Egl6I1ECIF+8z6sHu+KE7BzaEDZTpPXKXfy9SUDQlVYwkRcNTjJLC3Iakxbhss50KuT/R6SYfg==", + "dev": true + } + } + }, "dns-packet": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.2.1.tgz", @@ -890,6 +1079,12 @@ "ansi-colors": "^4.1.1" } }, + "entities": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz", + "integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==", + "dev": true + }, "eris": { "version": "0.13.3", "resolved": "https://registry.npmjs.org/eris/-/eris-0.13.3.tgz", @@ -1315,6 +1510,16 @@ "flat-cache": "^2.0.1" } }, + "file-set": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/file-set/-/file-set-4.0.1.tgz", + "integrity": "sha512-tRzX4kGPmxS2HDK2q2L4qcPopTl/gcyahve2/O8l8hHNJgJ7m+r/ZncCJ1MmFWEMp1yHxJGIU9gAcsWu5jPMpg==", + "dev": true, + "requires": { + "array-back": "^4.0.1", + "glob": "^7.1.6" + } + }, "fill-range": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", @@ -1336,6 +1541,23 @@ } } }, + "find-replace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz", + "integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==", + "dev": true, + "requires": { + "array-back": "^3.0.1" + }, + "dependencies": { + "array-back": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", + "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==", + "dev": true + } + } + }, "find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -1450,6 +1672,12 @@ "minipass": "^2.6.0" } }, + "fs-then-native": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fs-then-native/-/fs-then-native-2.0.0.tgz", + "integrity": "sha1-GaEk2U2QwiyOBF8ujdbr6jbUjGc=", + "dev": true + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -1630,8 +1858,28 @@ "graceful-fs": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", - "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", - "optional": true + "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==" + }, + "handlebars": { + "version": "4.7.6", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.6.tgz", + "integrity": "sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA==", + "dev": true, + "requires": { + "minimist": "^1.2.5", + "neo-async": "^2.6.0", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4", + "wordwrap": "^1.0.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } }, "har-schema": { "version": "2.0.0", @@ -2092,12 +2340,115 @@ "esprima": "^4.0.0" } }, + "js2xmlparser": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.1.tgz", + "integrity": "sha512-KrPTolcw6RocpYjdC7pL7v62e55q7qOMHvLX1UCLc5AAS8qeJ6nukarEJAF2KL2PZxlbGueEbINqZR2bDe/gUw==", + "dev": true, + "requires": { + "xmlcreate": "^2.0.3" + } + }, "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", "optional": true }, + "jsdoc": { + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-3.6.5.tgz", + "integrity": "sha512-SbY+i9ONuxSK35cgVHaI8O9senTE4CDYAmGSDJ5l3+sfe62Ff4gy96osy6OW84t4K4A8iGnMrlRrsSItSNp3RQ==", + "dev": true, + "requires": { + "@babel/parser": "^7.9.4", + "bluebird": "^3.7.2", + "catharsis": "^0.8.11", + "escape-string-regexp": "^2.0.0", + "js2xmlparser": "^4.0.1", + "klaw": "^3.0.0", + "markdown-it": "^10.0.0", + "markdown-it-anchor": "^5.2.7", + "marked": "^0.8.2", + "mkdirp": "^1.0.4", + "requizzle": "^0.2.3", + "strip-json-comments": "^3.1.0", + "taffydb": "2.6.2", + "underscore": "~1.10.2" + }, + "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==", + "dev": true + }, + "marked": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-0.8.2.tgz", + "integrity": "sha512-EGwzEeCcLniFX51DhTpmTom+dSA/MG/OBUDjnWtHbEnjAH180VzUeAw+oE4+Zv+CoYBWyRlYOTR0N8SO9R1PVw==", + "dev": true + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + } + } + }, + "jsdoc-api": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/jsdoc-api/-/jsdoc-api-6.0.0.tgz", + "integrity": "sha512-zvfB63nAc9e+Rv2kKmJfE6tmo4x8KFho5vKr6VfYTlCCgqtrfPv0McCdqT4betUT9rWtw0zGkNUVkVqeQipY6Q==", + "dev": true, + "requires": { + "array-back": "^4.0.1", + "cache-point": "^2.0.0", + "collect-all": "^1.0.3", + "file-set": "^4.0.1", + "fs-then-native": "^2.0.0", + "jsdoc": "^3.6.4", + "object-to-spawn-args": "^2.0.0", + "temp-path": "^1.0.0", + "walk-back": "^4.0.0" + } + }, + "jsdoc-parse": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/jsdoc-parse/-/jsdoc-parse-5.0.0.tgz", + "integrity": "sha512-Khw8c3glrTeA3/PfUJUBvhrMhWpSClORBUvL4pvq2wFcqvUVmA96wxnMkCno2GfZY4pnd8BStK5WGKGyn4Vckg==", + "dev": true, + "requires": { + "array-back": "^4.0.1", + "lodash.omit": "^4.5.0", + "lodash.pick": "^4.4.0", + "reduce-extract": "^1.0.0", + "sort-array": "^4.1.1", + "test-value": "^3.0.0" + } + }, + "jsdoc-to-markdown": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/jsdoc-to-markdown/-/jsdoc-to-markdown-6.0.1.tgz", + "integrity": "sha512-hUI2PAR5n/KlmQU3mAWO9i3D7jVbhyvUHfQ6oYVBt+wnnsyxpsAuhCODY1ryLOb2U9OPJd4GIK9mL2hqy7fHDg==", + "dev": true, + "requires": { + "array-back": "^4.0.1", + "command-line-tool": "^0.8.0", + "config-master": "^3.1.0", + "dmd": "^5.0.1", + "jsdoc-api": "^6.0.0", + "jsdoc-parse": "^5.0.0", + "walk-back": "^4.0.0" + } + }, "json-buffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", @@ -2181,6 +2532,15 @@ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" }, + "klaw": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", + "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.9" + } + }, "knex": { "version": "0.21.4", "resolved": "https://registry.npmjs.org/knex/-/knex-0.21.4.tgz", @@ -2266,6 +2626,15 @@ "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", "dev": true }, + "linkify-it": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz", + "integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==", + "dev": true, + "requires": { + "uc.micro": "^1.0.1" + } + }, "lint-staged": { "version": "10.2.11", "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-10.2.11.tgz", @@ -2473,6 +2842,30 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", + "dev": true + }, + "lodash.omit": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz", + "integrity": "sha1-brGa5aHuHdnfC5aeZs4Lf6MLXmA=", + "dev": true + }, + "lodash.padend": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/lodash.padend/-/lodash.padend-4.6.1.tgz", + "integrity": "sha1-U8y6BH0G4VjTEfRdpiX05J5vFm4=", + "dev": true + }, + "lodash.pick": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", + "integrity": "sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=", + "dev": true + }, "log-symbols": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz", @@ -2657,6 +3050,37 @@ "object-visit": "^1.0.0" } }, + "markdown-it": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-10.0.0.tgz", + "integrity": "sha512-YWOP1j7UbDNz+TumYP1kpwnP0aEa711cJjrAQrzd0UXlbJfc5aAq0F/PZHjiioqDC1NKgvIMX+o+9Bk7yuM2dg==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "entities": "~2.0.0", + "linkify-it": "^2.0.0", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + } + }, + "markdown-it-anchor": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-5.3.0.tgz", + "integrity": "sha512-/V1MnLL/rgJ3jkMWo84UR+K+jF1cxNG1a+KwqeXqTIJ+jtA8aWSHuigx8lTzauiIjBDbwF3NcWQMotd0Dm39jA==", + "dev": true + }, + "marked": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/marked/-/marked-1.1.1.tgz", + "integrity": "sha512-mJzT8D2yPxoPh7h0UXkB+dBj4FykPJ2OIfxAWeIHrvoHDkFxukV/29QxoFQoPM6RLEwhIFdJpmKBlqVM3s2ZIw==", + "dev": true + }, + "mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=", + "dev": true + }, "merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -2771,6 +3195,12 @@ "minimist": "^1.2.5" } }, + "mkdirp2": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp2/-/mkdirp2-1.0.4.tgz", + "integrity": "sha512-Q2PKB4ZR4UPtjLl76JfzlgSCUZhSV1AXQgAZa1qt5RiaALFjP/CDrGvFBrOz7Ck6McPcwMAxTsJvWOUjOU8XMw==", + "dev": true + }, "moment": { "version": "2.27.0", "resolved": "https://registry.npmjs.org/moment/-/moment-2.27.0.tgz", @@ -2912,6 +3342,12 @@ } } }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, "node-addon-api": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.0.tgz", @@ -3094,6 +3530,18 @@ } } }, + "object-get": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/object-get/-/object-get-2.1.1.tgz", + "integrity": "sha512-7n4IpLMzGGcLEMiQKsNR7vCe+N5E9LORFrtNUVy4sO3dj9a3HedZCxEL2T7QuLhcHN1NBuBsMOKaOsAYI9IIvg==", + "dev": true + }, + "object-to-spawn-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/object-to-spawn-args/-/object-to-spawn-args-2.0.0.tgz", + "integrity": "sha512-ZMT4owlXg3JGegecLlAgAA/6BsdKHn63R3ayXcAa3zFkF7oUBHcSb0oxszeutYe0FO2c1lT5pwCuidLkC4Gx3g==", + "dev": true + }, "object-visit": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", @@ -3452,6 +3900,78 @@ "esprima": "~4.0.0" } }, + "reduce-extract": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/reduce-extract/-/reduce-extract-1.0.0.tgz", + "integrity": "sha1-Z/I4W+2mUGG19fQxJmLosIDKFSU=", + "dev": true, + "requires": { + "test-value": "^1.0.1" + }, + "dependencies": { + "array-back": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-1.0.4.tgz", + "integrity": "sha1-ZEun8JX3/898Q7Xw3DnTwfA8Bjs=", + "dev": true, + "requires": { + "typical": "^2.6.0" + } + }, + "test-value": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/test-value/-/test-value-1.1.0.tgz", + "integrity": "sha1-oJE29y7AQ9J8iTcHwrFZv6196T8=", + "dev": true, + "requires": { + "array-back": "^1.0.2", + "typical": "^2.4.2" + } + } + } + }, + "reduce-flatten": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-1.0.1.tgz", + "integrity": "sha1-JYx479FT3fk8tWEjf2EYTzaW4yc=", + "dev": true + }, + "reduce-unique": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/reduce-unique/-/reduce-unique-2.0.1.tgz", + "integrity": "sha512-x4jH/8L1eyZGR785WY+ePtyMNhycl1N2XOLxhCbzZFaqF4AXjLzqSxa2UHgJ2ZVR/HHyPOvl1L7xRnW8ye5MdA==", + "dev": true + }, + "reduce-without": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/reduce-without/-/reduce-without-1.0.1.tgz", + "integrity": "sha1-aK0OrRGFXJo31OglbBW7+Hly/Iw=", + "dev": true, + "requires": { + "test-value": "^2.0.0" + }, + "dependencies": { + "array-back": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-1.0.4.tgz", + "integrity": "sha1-ZEun8JX3/898Q7Xw3DnTwfA8Bjs=", + "dev": true, + "requires": { + "typical": "^2.6.0" + } + }, + "test-value": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/test-value/-/test-value-2.1.0.tgz", + "integrity": "sha1-Edpv9nDzRxpztiXKTz/c97t0gpE=", + "dev": true, + "requires": { + "array-back": "^1.0.3", + "typical": "^2.6.0" + } + } + } + }, "regex-not": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", @@ -3523,6 +4043,15 @@ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" }, + "requizzle": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.3.tgz", + "integrity": "sha512-YanoyJjykPxGHii0fZP0uUPEXpvqfBDxWV7s6GKAiiOsiqhX6vHNyW3Qzdmqp/iq/ExbhaGbVrjB4ruEVSM4GQ==", + "dev": true, + "requires": { + "lodash": "^4.17.14" + } + }, "resolve": { "version": "1.17.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", @@ -3817,6 +4346,24 @@ } } }, + "sort-array": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/sort-array/-/sort-array-4.1.2.tgz", + "integrity": "sha512-G5IUpM+OcVnyaWHMv84Y/RQYiFQoSu6eUtJZu840iM6nR7zeY/eOGny2epkr5VKqCGDkOj3UBzOluDZ7hFpljA==", + "dev": true, + "requires": { + "array-back": "^4.0.1", + "typical": "^6.0.1" + }, + "dependencies": { + "typical": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/typical/-/typical-6.0.1.tgz", + "integrity": "sha512-+g3NEp7fJLe9DPa1TArHm9QAA7YciZmWnfAqEaFrBihQ7epOv9i99rjtgb6Iz0wh3WuQDjsCTDfgRoGnmHN81A==", + "dev": true + } + } + }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -3912,6 +4459,32 @@ } } }, + "stream-connect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stream-connect/-/stream-connect-1.0.2.tgz", + "integrity": "sha1-GLyB8u2zW4tdmoAJIAqYUxRCipc=", + "dev": true, + "requires": { + "array-back": "^1.0.2" + }, + "dependencies": { + "array-back": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-1.0.4.tgz", + "integrity": "sha1-ZEun8JX3/898Q7Xw3DnTwfA8Bjs=", + "dev": true, + "requires": { + "typical": "^2.6.0" + } + } + } + }, + "stream-via": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/stream-via/-/stream-via-1.0.4.tgz", + "integrity": "sha512-DBp0lSvX5G9KGRDTkR/R+a29H+Wk2xItOF+MpZLLNDWbEV9tGPnqLPxHEYjmiz8xGtJHRIqmI+hCjmNzqoA4nQ==", + "dev": true + }, "string-argv": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", @@ -4052,6 +4625,36 @@ } } }, + "table-layout": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-0.4.5.tgz", + "integrity": "sha512-zTvf0mcggrGeTe/2jJ6ECkJHAQPIYEwDoqsiqBjI24mvRmQbInK5jq33fyypaCBxX08hMkfmdOqj6haT33EqWw==", + "dev": true, + "requires": { + "array-back": "^2.0.0", + "deep-extend": "~0.6.0", + "lodash.padend": "^4.6.1", + "typical": "^2.6.1", + "wordwrapjs": "^3.0.0" + }, + "dependencies": { + "array-back": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-2.0.0.tgz", + "integrity": "sha512-eJv4pLLufP3g5kcZry0j6WXpIbzYw9GUB4mVJZno9wfwiBxbizTnHCw3VJb07cBihbFX48Y7oSrW9y+gt4glyw==", + "dev": true, + "requires": { + "typical": "^2.6.1" + } + } + } + }, + "taffydb": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/taffydb/-/taffydb-2.6.2.tgz", + "integrity": "sha1-fLy2S1oUG2ou/CxdLGe04VCyomg=", + "dev": true + }, "tar": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz", @@ -4068,6 +4671,33 @@ "resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.0.tgz", "integrity": "sha512-PKUnlDFODZueoA8owLehl8vLcgtA8u4dRuVbZc92tspDYZixjJL6TqYOmryf/PfP/EBX+2rgNcrj96NO+RPkdQ==" }, + "temp-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/temp-path/-/temp-path-1.0.0.tgz", + "integrity": "sha1-JLFUOXOrRCiW2a02fdnL2/r+kYs=", + "dev": true + }, + "test-value": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/test-value/-/test-value-3.0.0.tgz", + "integrity": "sha512-sVACdAWcZkSU9x7AOmJo5TqE+GyNJknHaHsMrR6ZnhjVlVN9Yx6FjHrsKZ3BjIpPCT68zYesPWkakrNupwfOTQ==", + "dev": true, + "requires": { + "array-back": "^2.0.0", + "typical": "^2.6.1" + }, + "dependencies": { + "array-back": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-2.0.0.tgz", + "integrity": "sha512-eJv4pLLufP3g5kcZry0j6WXpIbzYw9GUB4mVJZno9wfwiBxbizTnHCw3VJb07cBihbFX48Y7oSrW9y+gt4glyw==", + "dev": true, + "requires": { + "typical": "^2.6.1" + } + } + } + }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -4190,11 +4820,36 @@ "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", "dev": true }, + "typical": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/typical/-/typical-2.6.1.tgz", + "integrity": "sha1-XAgOXWYcu+OCWdLnCjxyU+hziB0=", + "dev": true + }, + "uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "dev": true + }, + "uglify-js": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.10.1.tgz", + "integrity": "sha512-RjxApKkrPJB6kjJxQS3iZlf///REXWYxYJxO/MpmlQzVkDWVI3PSnCBWezMecmTU/TRkNxrl8bmsfFQCp+LO+Q==", + "dev": true, + "optional": true + }, "unc-path-regex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=" }, + "underscore": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.10.2.tgz", + "integrity": "sha512-N4P+Q/BuyuEKFJ43B9gYuOj4TQUHXX+j2FqguVOpjkssLUUrnJofCcBccJSCoeturDoZU6GorDTHSvUDlSQbTg==", + "dev": true + }, "union-value": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", @@ -4303,6 +4958,12 @@ "extsprintf": "^1.2.0" } }, + "walk-back": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/walk-back/-/walk-back-4.0.0.tgz", + "integrity": "sha512-kudCA8PXVQfrqv2mFTG72vDBRi8BKWxGgFLwPpzHcpZnSwZk93WMwUDVcLHWNsnm+Y0AC4Vb6MUNRgaHfyV2DQ==", + "dev": true + }, "which": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", @@ -4336,6 +4997,22 @@ "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", "dev": true }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + }, + "wordwrapjs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-3.0.0.tgz", + "integrity": "sha512-mO8XtqyPvykVCsrwj5MlOVWvSnCdT+C+QVbm6blradR7JExAhbkZ7hZ9A+9NUtwzSqrlUo9a67ws0EiILrvRpw==", + "dev": true, + "requires": { + "reduce-flatten": "^1.0.1", + "typical": "^2.6.1" + } + }, "wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", @@ -4417,6 +5094,12 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz", "integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==" }, + "xmlcreate": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.3.tgz", + "integrity": "sha512-HgS+X6zAztGa9zIK3Y3LXuJes33Lz9x+YyTxgrkIdabu2vqcGOWwdfCpf1hWLRrd553wd4QCDf6BBO6FfdsRiQ==", + "dev": true + }, "y18n": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", diff --git a/package.json b/package.json index 492b126..56e9964 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "lint": "eslint ./src ./db/migrations", "lint-fix": "eslint --fix ./src ./db/migrations", "generate-config-jsdoc": "node src/data/generateCfgJsdoc.js", + "generate-plugin-api-docs": "jsdoc2md -t docs/plugin-api-template.hbs src/pluginApi.js > docs/plugin-api.md", "create-migration": "knex migrate:make" }, "repository": { @@ -39,6 +40,7 @@ "devDependencies": { "eslint": "^7.7.0", "husky": "^4.2.5", + "jsdoc-to-markdown": "^6.0.1", "lint-staged": "^10.2.11", "supervisor": "^0.12.0" }, From 3f2948bbc131ad998502c2b33269a8e0d52bd76f Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 16 Aug 2020 23:44:48 +0300 Subject: [PATCH 082/300] Update prerequisite docs --- docs/setup.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/setup.md b/docs/setup.md index fc6ef10..fcb6e0d 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -14,8 +14,11 @@ To keep it online, you need to keep the bot process running. ## Prerequisites 1. Create a bot account through the [Discord Developer Portal](https://discordapp.com/developers/) 2. Install Node.js 12, 13, or 14 -3. Download the latest bot version from [the releases page](https://github.com/Dragory/modmailbot/releases) and extract it to a folder -4. In the bot's folder, make a copy of the file `config.example.ini` and rename the copy to `config.ini` +3. Download the latest bot version from [the releases page](https://github.com/Dragory/modmailbot/releases) + * Make sure the release doesn't say "Pre-release" next to it unless you want to run an unstable beta version! +4. Extract the zip file that you just downloaded +5. In the bot's folder (that you extracted from the zip file), make a copy of the file `config.example.ini` and rename the copy to `config.ini` + * If you're on Windows, the file may be named `config.example` (without `.ini` at the end) ## Single-server setup In this setup, modmail threads are opened on the main server in a special category. From be7f172b62c2a466bb6f0364cdd85f3e1eb3fb9a Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 16 Aug 2020 23:53:35 +0300 Subject: [PATCH 083/300] Rename mainGuildId to mainServerId, mailGuildId to inboxServerId The original names (mainGuildId, mailGuildId) are now aliases for the new option names, so old configs still work after this change. --- config.example.ini | 4 ++-- docs/configuration.md | 20 +++++++++++++------- docs/setup.md | 6 +++--- src/cfg.js | 11 ++++++++++- src/data/cfg.jsdoc.js | 4 +++- src/data/cfg.schema.json | 15 ++++++++++++--- src/main.js | 4 ++-- src/utils.js | 8 ++++---- 8 files changed, 49 insertions(+), 23 deletions(-) diff --git a/config.example.ini b/config.example.ini index 8b403a9..ff3c145 100644 --- a/config.example.ini +++ b/config.example.ini @@ -1,8 +1,8 @@ # Required settings # ----------------- token = REPLACE_WITH_TOKEN -mainGuildId = REPLACE_WITH_MAIN_SERVER_ID -mailGuildId = REPLACE_WITH_INBOX_SERVER_ID +mainServerId = REPLACE_WITH_MAIN_SERVER_ID +inboxServerId = REPLACE_WITH_INBOX_SERVER_ID logChannelId = REPLACE_WITH_LOG_CHANNEL_ID # Add new options below this line: diff --git a/docs/configuration.md b/docs/configuration.md index fb50674..fe06522 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -22,7 +22,7 @@ vice versa. ## Adding new options To add a new option to your `config.ini`, open the file in a text editor such as notepad. -Each option is put on a new line, and follows the format `option = value`. For example, `mainGuildId = 1234`. +Each option is put on a new line, and follows the format `option = value`. For example, `mainServerId = 1234`. **You need to restart the bot for configuration changes to take effect!** @@ -65,12 +65,12 @@ greetingMessage[] = Fourth line! With an empty line in the middle. #### token The bot user's token from [Discord Developer Portal](https://discordapp.com/developers/). -#### mainGuildId +#### mainServerId **Accepts multiple values** Your server's ID. -#### mailGuildId -For a two-server setup, the inbox server's ID. -For a single-server setup, same as [mainGuildId](#mainguildid). +#### inboxServerId +For a single-server setup, same as [mainServerId](#mainServerId). +For a two-server setup, the inbox server's ID. #### logChannelId ID of a channel on the inbox server where logs are posted after a modmail thread is closed @@ -190,6 +190,12 @@ See ["Permissions" on this page](https://abal.moe/Eris/docs/reference) for suppo **Default:** `You haven't been a member of the server for long enough to contact modmail.` If `requiredTimeOnServer` is set, users that are too new will be sent this message if they try to message modmail. +#### mainGuildId +Alias for [mainServerId](#mainServerId) + +#### mailGuildId +Alias for [inboxServerId](#inboxServerId) + #### mentionRole **Default:** `here` **Accepts multiple values.** Role that is mentioned when new threads are created or the bot is mentioned. @@ -359,7 +365,7 @@ However, there are some differences between `config.ini` and `config.json`. *See [the example on the Wikipedia page for JSON](https://en.wikipedia.org/wiki/JSON#Example) for a general overview of the JSON format.* -* In `config.json`, all text values and IDs need to be wrapped in quotes, e.g. `"mainGuildId": "94882524378968064"` +* In `config.json`, all text values and IDs need to be wrapped in quotes, e.g. `"mainServerId": "94882524378968064"` * In `config.json`, all numbers (other than IDs) are written without quotes, e.g. `"port": 3000` ### Toggle options @@ -400,7 +406,7 @@ being replaced by two underscores and add `MM_` as a prefix. If adding multiple values with two pipe characters: `||`. Examples: -* `mainGuildId` -> `MM_MAIN_GUILD_ID` +* `mainServerId` -> `MM_MAIN_SERVER_ID` * `commandAliases.mv` -> `MM_COMMAND_ALIASES__MV` * From: ```ini diff --git a/docs/setup.md b/docs/setup.md index fcb6e0d..4d3a447 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -25,7 +25,7 @@ In this setup, modmail threads are opened on the main server in a special catego This is the recommended setup for small and medium sized servers. 1. **Go through the [prerequisites](#prerequisites) above first!** -2. Open `config.ini` in a text editor and fill in the required values. `mainGuildId` and `mailGuildId` should both be set to your server's id. +2. Open `config.ini` in a text editor and fill in the required values. `mainServerId` and `inboxServerId` should both be set to your server's id. 3. Invite the bot to the server 4. On a new line at the end of `config.ini`, add `categoryAutomation.newThread = CATEGORY_ID_HERE` * Replace `CATEGORY_ID_HERE` with the ID of the category where new modmail threads should go @@ -44,8 +44,8 @@ You might also want this setup for privacy concerns*. 1. **Go through the [prerequisites](#prerequisites) above first!** 2. Create an inbox server on Discord 3. Open `config.ini` in a text editor and fill in the required values - * Set `mainGuildId` to the ID of the *main* server where users will message the bot from - * Set `mailGuildId` to the ID of the *inbox* server created in step 2 + * Set `mainServerId` to the ID of the *main* server where users will message the bot from + * Set `inboxServerId` to the ID of the *inbox* server created in step 2 4. Invite the bot to both the main server and the newly-created inbox server 5. Open `config.ini` in a text editor and fill in the values 6. Make sure the bot has the `Manage Channels`, `Manage Messages`, and `Attach Files` permissions on the **inbox** server diff --git a/src/cfg.js b/src/cfg.js index 35c2371..2d91e8c 100644 --- a/src/cfg.js +++ b/src/cfg.js @@ -102,6 +102,15 @@ for (const [key, value] of Object.entries(config)) { delete config[key]; } +// mainGuildId => mainServerId +// mailGuildId => inboxServerId +if (config.mainGuildId && ! config.mainServerId) { + config.mainServerId = config.mainGuildId; +} +if (config.mailGuildId && ! config.inboxServerId) { + config.inboxServerId = config.mailGuildId; +} + if (! config.dbType) { config.dbType = "sqlite"; } @@ -117,7 +126,7 @@ if (! config.sqliteOptions) { // already have something set up in guildGreetings. This retains backwards compatibility while allowing you to override // greetings for specific servers in guildGreetings. if (config.greetingMessage || config.greetingAttachment) { - for (const guildId of config.mainGuildId) { + for (const guildId of config.mainServerId) { if (config.guildGreetings[guildId]) continue; config.guildGreetings[guildId] = { message: config.greetingMessage, diff --git a/src/data/cfg.jsdoc.js b/src/data/cfg.jsdoc.js index 8d614c7..45dc6ff 100644 --- a/src/data/cfg.jsdoc.js +++ b/src/data/cfg.jsdoc.js @@ -1,9 +1,11 @@ /** * @typedef {object} ModmailConfig * @property {string} [token] + * @property {array} [mainServerId] + * @property {string} [inboxServerId] + * @property {string} [logChannelId] * @property {array} [mainGuildId] * @property {string} [mailGuildId] - * @property {string} [logChannelId] * @property {string} [prefix="!"] * @property {string} [snippetPrefix="!!"] * @property {string} [snippetPrefixAnon="!!!"] diff --git a/src/data/cfg.schema.json b/src/data/cfg.schema.json index 519802d..64550e6 100644 --- a/src/data/cfg.schema.json +++ b/src/data/cfg.schema.json @@ -46,16 +46,25 @@ "token": { "type": "string" }, - "mainGuildId": { + "mainServerId": { "$ref": "#/definitions/stringArray" }, - "mailGuildId": { + "inboxServerId": { "type": "string" }, "logChannelId": { "type": "string" }, + "mainGuildId": { + "$comment": "Alias for mainServerId", + "$ref": "#/definitions/stringArray" + }, + "mailGuildId": { + "$comment": "Alias for inboxServerId", + "type": "string" + }, + "prefix": { "type": "string", "default": "!" @@ -323,7 +332,7 @@ "allOf": [ { "$comment": "Base required values", - "required": ["token", "mainGuildId", "mailGuildId", "logChannelId", "dbType"] + "required": ["token", "mainServerId", "inboxServerId", "logChannelId", "dbType"] }, { "$comment": "Make attachmentStorageChannelId required if attachmentStorage is set to 'discord'", diff --git a/src/main.js b/src/main.js index 7d1a9eb..8af38be 100644 --- a/src/main.js +++ b/src/main.js @@ -38,8 +38,8 @@ module.exports = { bot.once("ready", async () => { console.log("Connected! Waiting for guilds to become available..."); await Promise.all([ - ...config.mainGuildId.map(id => waitForGuild(id)), - waitForGuild(config.mailGuildId) + ...config.mainServerId.map(id => waitForGuild(id)), + waitForGuild(config.inboxServerId), ]); console.log("Initializing..."); diff --git a/src/utils.js b/src/utils.js index 8c993b2..2a8bddb 100644 --- a/src/utils.js +++ b/src/utils.js @@ -17,7 +17,7 @@ let logChannel = null; * @returns {Eris~Guild} */ function getInboxGuild() { - if (! inboxGuild) inboxGuild = bot.guilds.find(g => g.id === config.mailGuildId); + if (! inboxGuild) inboxGuild = bot.guilds.find(g => g.id === config.inboxServerId); if (! inboxGuild) throw new BotError("The bot is not on the modmail (inbox) server!"); return inboxGuild; } @@ -27,11 +27,11 @@ function getInboxGuild() { */ function getMainGuilds() { if (mainGuilds.length === 0) { - mainGuilds = bot.guilds.filter(g => config.mainGuildId.includes(g.id)); + mainGuilds = bot.guilds.filter(g => config.mainServerId.includes(g.id)); } - if (mainGuilds.length !== config.mainGuildId.length) { - if (config.mainGuildId.length === 1) { + if (mainGuilds.length !== config.mainServerId.length) { + if (config.mainServerId.length === 1) { console.warn("[WARN] The bot hasn't joined the main guild!"); } else { console.warn("[WARN] The bot hasn't joined one or more main guilds!"); From a327160ed9d9844e8d555a6c490d885eed8883c8 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 17 Aug 2020 00:19:41 +0300 Subject: [PATCH 084/300] Improve CLI feedback for 'Waiting for servers', always continue after a delay --- src/main.js | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/src/main.js b/src/main.js index 8af38be..460e02c 100644 --- a/src/main.js +++ b/src/main.js @@ -36,11 +36,38 @@ module.exports = { console.log("Connecting to Discord..."); bot.once("ready", async () => { - console.log("Connected! Waiting for guilds to become available..."); - await Promise.all([ - ...config.mainServerId.map(id => waitForGuild(id)), - waitForGuild(config.inboxServerId), - ]); + console.log("Connected! Waiting for servers to become available..."); + + await (new Promise(resolve => { + const waitNoteTimeout = setTimeout(() => { + console.log("Servers did not become available after 15 seconds, continuing start-up anyway"); + console.log(""); + + const isSingleServer = config.mainServerId.includes(config.inboxServerId); + if (isSingleServer) { + console.log("WARNING: The bot will not work before it's invited to the server."); + } else { + const hasMultipleMainServers = config.mainServerId.length > 1; + if (hasMultipleMainServers) { + console.log("WARNING: The bot will not function correctly until it's invited to *all* main servers and the inbox server."); + } else { + console.log("WARNING: The bot will not function correctly until it's invited to *both* the main server and the inbox server."); + } + } + + console.log(""); + + resolve(); + }, 15 * 1000); + + Promise.all([ + ...config.mainServerId.map(id => waitForGuild(id)), + waitForGuild(config.inboxServerId), + ]).then(() => { + clearTimeout(waitNoteTimeout); + resolve(); + }); + })); console.log("Initializing..."); initStatus(); From 16a3af07efba3a500ca4795048df213df3b856c4 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 17 Aug 2020 00:20:04 +0300 Subject: [PATCH 085/300] Hide ajv warning about ignored keywords This was showing up when using $ref with $comment in the JSON Schema --- src/cfg.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/cfg.js b/src/cfg.js index 2d91e8c..69e12a6 100644 --- a/src/cfg.js +++ b/src/cfg.js @@ -149,7 +149,11 @@ for (const [key, value] of Object.entries(config)) { } // Validate config and assign defaults (if missing) -const ajv = new Ajv({ useDefaults: true, coerceTypes: "array" }); +const ajv = new Ajv({ + useDefaults: true, + coerceTypes: "array", + extendRefs: true, // Hides an error about ignored keywords when using $ref with $comment +}); // https://github.com/ajv-validator/ajv/issues/141#issuecomment-270692820 const truthyValues = ["1", "true", "on", "yes"]; From d85a2dad5ddb3003f875184449680f4e44525721 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 17 Aug 2020 00:39:39 +0300 Subject: [PATCH 086/300] Add !message --- docs/commands.md | 7 +++++++ src/modules/id.js | 28 ++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/docs/commands.md b/docs/commands.md index ae2037e..fc838cd 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -75,6 +75,13 @@ This is mainly useful when reporting messages to Discord's Trust & Safety team. ### `!id` Prints the user's ID. +### `!dm_channel_id` +Prints the ID of the current DM channel with the user + +### `!message ` +Shows the DM channel ID, DM message ID, and message link of the specified user reply. +`` is the message number shown in brackets before the user reply in the thread. + ## Anywhere on the inbox server These commands can be used anywhere on the inbox server, even outside Modmail threads. diff --git a/src/modules/id.js b/src/modules/id.js index ceced1b..abc09a8 100644 --- a/src/modules/id.js +++ b/src/modules/id.js @@ -1,3 +1,6 @@ +const ThreadMessage = require("../data/ThreadMessage"); +const utils = require("../utils"); + module.exports = ({ bot, knex, config, commands }) => { commands.addInboxThreadCommand("id", [], async (msg, args, thread) => { thread.postSystemMessage(thread.user_id); @@ -7,4 +10,29 @@ module.exports = ({ bot, knex, config, commands }) => { const dmChannel = await thread.getDMChannel(); thread.postSystemMessage(dmChannel.id); }); + + commands.addInboxThreadCommand("message", "", async (msg, args, thread) => { + /** @type {ThreadMessage} */ + const threadMessage = await thread.findThreadMessageByMessageNumber(args.messageNumber); + if (! threadMessage) { + thread.postSystemMessage("No message in this thread with that number"); + return; + } + + const channelId = threadMessage.dm_channel_id; + // In specific rare cases, such as createThreadOnMention, a thread message may originate from a main server + const channelIdServer = utils.getMainGuilds().find(g => g.channels.has(channelId)); + const messageLink = channelIdServer + ? `https://discord.com/channels/${channelIdServer.id}/${channelId}/${threadMessage.dm_message_id}` + : `https://discord.com/channels/@me/${channelId}/${threadMessage.dm_message_id}`; + + const parts = [ + `Details for message \`[${threadMessage.message_number}]\`:`, + `Channel ID: \`${channelId}\``, + `Message ID: \`${threadMessage.dm_message_id}\``, + `Link: <${messageLink}>`, + ]; + + thread.postSystemMessage(parts.join("\n")); + }); }; From c437634a180463a3be97b37ca9f15c836a322835 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 17 Aug 2020 00:39:50 +0300 Subject: [PATCH 087/300] Add !edit and !delete to the command docs --- docs/commands.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/commands.md b/docs/commands.md index fc838cd..d273f7d 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -62,6 +62,14 @@ Pings you when the thread gets a new reply. ### `!alert cancel` Cancel the ping set by `!alert`. +### `!edit ` +Edit your own previous reply sent with `!reply`. +`` is the message number shown in brackets before the user reply in the thread. + +### `!delete ` +Delete your own previous reply sent with `!reply`. +`` is the message number shown in brackets before the user reply in the thread. + ### `!loglink` Get a link to the open Modmail thread's log. From 099b5f96160d7ed8937135992a8b4a324ece9e1d Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 17 Aug 2020 00:52:24 +0300 Subject: [PATCH 088/300] Enable !edit and !delete by default --- CHANGELOG.md | 1 + src/data/cfg.jsdoc.js | 4 ++-- src/data/cfg.schema.json | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88ef9c3..90a56ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Please report any bugs you encounter by [creating a GitHub issue](https://github * Added support for Node.js 14, dropped support for Node.js 10 and 11 * The supported versions are now 12, 13, and 14 * Added support for editing and deleting staff replies + * This is **enabled by default** * This can be disabled with the `allowStaffEdit` and `allowStaffDelete` options * Only the staff member who sent the reply can edit/delete it * New option `reactOnSeen` ([#398](https://github.com/Dragory/modmailbot/pull/398) by @Eegras) diff --git a/src/data/cfg.jsdoc.js b/src/data/cfg.jsdoc.js index 45dc6ff..2d1a6b2 100644 --- a/src/data/cfg.jsdoc.js +++ b/src/data/cfg.jsdoc.js @@ -29,8 +29,8 @@ * @property {boolean} [typingProxyReverse=false] * @property {boolean} [mentionUserInThreadHeader=false] * @property {boolean} [rolesInThreadHeader=false] - * @property {boolean} [allowStaffEdit=false] - * @property {boolean} [allowStaffDelete=false] + * @property {boolean} [allowStaffEdit=true] + * @property {boolean} [allowStaffDelete=true] * @property {boolean} [enableGreeting=false] * @property {string} [greetingMessage] * @property {string} [greetingAttachment] diff --git a/src/data/cfg.schema.json b/src/data/cfg.schema.json index 64550e6..9431823 100644 --- a/src/data/cfg.schema.json +++ b/src/data/cfg.schema.json @@ -158,11 +158,11 @@ }, "allowStaffEdit": { "$ref": "#/definitions/customBoolean", - "default": false + "default": true }, "allowStaffDelete": { "$ref": "#/definitions/customBoolean", - "default": false + "default": true }, "enableGreeting": { From c177be89207b175dcae6efe35e3d223df8bee520 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 17 Aug 2020 00:55:18 +0300 Subject: [PATCH 089/300] Fix categoryAutomation type --- src/data/cfg.schema.json | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/data/cfg.schema.json b/src/data/cfg.schema.json index 9431823..ffb416d 100644 --- a/src/data/cfg.schema.json +++ b/src/data/cfg.schema.json @@ -223,10 +223,18 @@ }, "categoryAutomation": { - "patternProperties": { - "^.+$": { - "type": "string", - "pattern": "^\\d+$" + "properties": { + "newThread": { + "type": "string" + }, + "newThreadFromGuild": { + "type": "object", + "patternProperties": { + "^.+$": { + "type": "string", + "pattern": "^\\d+$" + } + } } }, "default": {} From 900a14d8a1fd867e114bdbee288d5a0b17f38995 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 17 Aug 2020 01:04:05 +0300 Subject: [PATCH 090/300] Rename categoryAutomation.newThreadFromGuild to categoryAutomation.newThreadFromServer The original name (categoryAutomation.newThreadFromGuild) is now an alias for the new option name, so old configs still work after this change. --- docs/configuration.md | 13 ++++++++----- src/cfg.js | 5 +++++ src/data/cfg.schema.json | 6 +++++- src/data/threads.js | 4 ++-- 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index fe06522..cbb107f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -124,16 +124,19 @@ If set, the bot auto-replies to bot mentions (pings) with this message. Use `{us #### categoryAutomation.newThread **Default:** *None* -ID of the category where new threads are opened. Also functions as a fallback for `categoryAutomation.newThreadFromGuild`. +ID of the category where new threads are opened. Also functions as a fallback for `categoryAutomation.newThreadFromServer`. -#### categoryAutomation.newThreadFromGuild.GUILDID +#### categoryAutomation.newThreadFromGuild.SERVER_ID +Alias for [`categoryAutomation.newThreadFromServer`](#categoryAutomationNewThreadFromServerServer_id) + +#### categoryAutomation.newThreadFromServer.SERVER_ID **Default:** *None* -When running the bot on multiple main servers, this allows you to specify new thread categories for users from each guild. Example: +When running the bot on multiple main servers, this allows you to specify which category to use for modmail threads from each server. Example: ```ini # When the user is from the server ID 94882524378968064, their modmail thread will be placed in the category ID 360863035130249235 -categoryAutomation.newThreadFromGuild.94882524378968064 = 360863035130249235 +categoryAutomation.newThreadFromServer.94882524378968064 = 360863035130249235 # When the user is from the server ID 541484311354933258, their modmail thread will be placed in the category ID 542780020972716042 -categoryAutomation.newThreadFromGuild.541484311354933258 = 542780020972716042 +categoryAutomation.newThreadFromServer.541484311354933258 = 542780020972716042 ``` #### closeMessage diff --git a/src/cfg.js b/src/cfg.js index 69e12a6..abff870 100644 --- a/src/cfg.js +++ b/src/cfg.js @@ -121,6 +121,11 @@ if (! config.sqliteOptions) { }; } +// categoryAutomation.newThreadFromGuild => categoryAutomation.newThreadFromServer +if (config.categoryAutomation && config.categoryAutomation.newThreadFromGuild && ! config.categoryAutomation.newThreadFromServer) { + config.categoryAutomation.newThreadFromServer = config.categoryAutomation.newThreadFromGuild; +} + // Move greetingMessage/greetingAttachment to the guildGreetings object internally // Or, in other words, if greetingMessage and/or greetingAttachment is set, it is applied for all servers that don't // already have something set up in guildGreetings. This retains backwards compatibility while allowing you to override diff --git a/src/data/cfg.schema.json b/src/data/cfg.schema.json index ffb416d..6e0b676 100644 --- a/src/data/cfg.schema.json +++ b/src/data/cfg.schema.json @@ -227,7 +227,7 @@ "newThread": { "type": "string" }, - "newThreadFromGuild": { + "newThreadFromServer": { "type": "object", "patternProperties": { "^.+$": { @@ -235,6 +235,10 @@ "pattern": "^\\d+$" } } + }, + "newThreadFromGuild": { + "$comment": "Alias for categoryAutomation.newThreadFromServer", + "$ref": "#/properties/categoryAutomation/properties/newThreadFromServer" } }, "default": {} diff --git a/src/data/threads.js b/src/data/threads.js index 58692f0..2d986d0 100644 --- a/src/data/threads.js +++ b/src/data/threads.js @@ -142,9 +142,9 @@ async function createNewThreadForUser(user, opts = {}) { // Figure out which category we should place the thread channel in let newThreadCategoryId = hookResult.categoryId || null; - if (! newThreadCategoryId && config.categoryAutomation.newThreadFromGuild) { + if (! newThreadCategoryId && config.categoryAutomation.newThreadFromServer) { // Categories for specific source guilds (in case of multiple main guilds) - for (const [guildId, categoryId] of Object.entries(config.categoryAutomation.newThreadFromGuild)) { + for (const [guildId, categoryId] of Object.entries(config.categoryAutomation.newThreadFromServer)) { if (userGuildData.has(guildId)) { newThreadCategoryId = categoryId; break; From 4fbf2a176911b35889f033af17d6a26c4c9316a3 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 17 Aug 2020 01:20:59 +0300 Subject: [PATCH 091/300] Rename guildGreetings to serverGreetings The original name (guildGreetings) is now an alias for the new option name, so old configs will still work after this change. --- docs/configuration.md | 21 ++++++++++++--------- src/cfg.js | 15 ++++++++++----- src/data/cfg.jsdoc.js | 3 ++- src/data/cfg.schema.json | 16 ++++++++++++++-- src/modules/greeting.js | 12 ++++++------ 5 files changed, 44 insertions(+), 23 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index cbb107f..b32912e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -170,15 +170,7 @@ greetingMessage[] = Remember to read the rules. ``` #### guildGreetings -**Default:** *None* -When running the bot on multiple main servers, this allows you to set different greetings for each server. Example: -```ini -guildGreetings.94882524378968064.message = Welcome to server ID 94882524378968064! -guildGreetings.94882524378968064.attachment = greeting.png - -guildGreetings.541484311354933258.message[] = Welcome to server ID 541484311354933258! -guildGreetings.541484311354933258.message[] = Second line of the greeting. -``` +Alias for [`serverGreetings`](#serverGreetings) #### ignoreAccidentalThreads **Default:** `off` @@ -261,6 +253,17 @@ The bot's response to the user when they message the bot and open a new modmail **Default:** `off` If enabled, the user's roles will be shown in the modmail thread header +#### serverGreetings +**Default:** *None* +When running the bot on multiple main servers, this allows you to set different greetings for each server. Example: +```ini +serverGreetings.94882524378968064.message = Welcome to server ID 94882524378968064! +serverGreetings.94882524378968064.attachment = greeting.png + +serverGreetings.541484311354933258.message[] = Welcome to server ID 541484311354933258! +serverGreetings.541484311354933258.message[] = Second line of the greeting. +``` + #### smallAttachmentLimit **Default:** `2097152` Size limit of `relaySmallAttachmentsAsAttachments` in bytes (default is 2MB) diff --git a/src/cfg.js b/src/cfg.js index abff870..1d7c304 100644 --- a/src/cfg.js +++ b/src/cfg.js @@ -126,14 +126,19 @@ if (config.categoryAutomation && config.categoryAutomation.newThreadFromGuild && config.categoryAutomation.newThreadFromServer = config.categoryAutomation.newThreadFromGuild; } -// Move greetingMessage/greetingAttachment to the guildGreetings object internally +// guildGreetings => serverGreetings +if (config.guildGreetings && ! config.serverGreetings) { + config.serverGreetings = config.guildGreetings; +} + +// Move greetingMessage/greetingAttachment to the serverGreetings object internally // Or, in other words, if greetingMessage and/or greetingAttachment is set, it is applied for all servers that don't -// already have something set up in guildGreetings. This retains backwards compatibility while allowing you to override -// greetings for specific servers in guildGreetings. +// already have something set up in serverGreetings. This retains backwards compatibility while allowing you to override +// greetings for specific servers in serverGreetings. if (config.greetingMessage || config.greetingAttachment) { for (const guildId of config.mainServerId) { - if (config.guildGreetings[guildId]) continue; - config.guildGreetings[guildId] = { + if (config.serverGreetings[guildId]) continue; + config.serverGreetings[guildId] = { message: config.greetingMessage, attachment: config.greetingAttachment }; diff --git a/src/data/cfg.jsdoc.js b/src/data/cfg.jsdoc.js index 2d1a6b2..1e1658e 100644 --- a/src/data/cfg.jsdoc.js +++ b/src/data/cfg.jsdoc.js @@ -34,7 +34,8 @@ * @property {boolean} [enableGreeting=false] * @property {string} [greetingMessage] * @property {string} [greetingAttachment] - * @property {*} [guildGreetings={}] + * @property {*} [serverGreetings={}] + * @property {*} [guildGreetings] * @property {number} [requiredAccountAge] Required account age to message Modmail, in hours * @property {string} [accountAgeDeniedMessage="Your Discord account is not old enough to contact modmail."] * @property {number} [requiredTimeOnServer] Required time on server to message Modmail, in minutes diff --git a/src/data/cfg.schema.json b/src/data/cfg.schema.json index 6e0b676..5b1469b 100644 --- a/src/data/cfg.schema.json +++ b/src/data/cfg.schema.json @@ -175,14 +175,26 @@ "greetingAttachment": { "type": "string" }, - "guildGreetings": { + "serverGreetings": { "patternProperties": { "^\\d+$": { - "$ref": "#/definitions/multilineString" + "type": "object", + "properties": { + "message": { + "$ref": "#/definitions/multilineString" + }, + "attachment": { + "type": "string" + } + } } }, "default": {} }, + "guildGreetings": { + "$comment": "Alias for serverGreetings", + "$ref": "#/properties/serverGreetings" + }, "requiredAccountAge": { "description": "Required account age to message Modmail, in hours", diff --git a/src/modules/greeting.js b/src/modules/greeting.js index 37a2620..81d7d50 100644 --- a/src/modules/greeting.js +++ b/src/modules/greeting.js @@ -7,8 +7,8 @@ module.exports = ({ bot }) => { if (! config.enableGreeting) return; bot.on("guildMemberAdd", (guild, member) => { - const guildGreeting = config.guildGreetings[guild.id]; - if (! guildGreeting || (! guildGreeting.message && ! guildGreeting.attachment)) return; + const serverGreeting = config.serverGreetings[guild.id]; + if (! serverGreeting || (! serverGreeting.message && ! serverGreeting.attachment)) return; function sendGreeting(message, file) { bot.getDMChannel(member.id).then(channel => { @@ -22,11 +22,11 @@ module.exports = ({ bot }) => { }); } - const greetingMessage = utils.readMultilineConfigValue(guildGreeting.message); + const greetingMessage = utils.readMultilineConfigValue(serverGreeting.message); - if (guildGreeting.attachment) { - const filename = path.basename(guildGreeting.attachment); - fs.readFile(guildGreeting.attachment, (err, data) => { + if (serverGreeting.attachment) { + const filename = path.basename(serverGreeting.attachment); + fs.readFile(serverGreeting.attachment, (err, data) => { const file = {file: data, name: filename}; sendGreeting(greetingMessage, file); }); From ac2548b6cc1b9ef41a16e3497b13ae0dae032859 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 17 Aug 2020 01:24:30 +0300 Subject: [PATCH 092/300] Request GUILD_MEMBERS intent for server greetings. Emphasize new intent requirement in the changelog. --- CHANGELOG.md | 4 +++- src/bot.js | 7 ++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90a56ab..8393046 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ Please report any bugs you encounter by [creating a GitHub issue](https://github **General changes:** * Added support for Node.js 14, dropped support for Node.js 10 and 11 * The supported versions are now 12, 13, and 14 +* The bot now requests the necessary [Gateway Intents](https://discord.com/developers/docs/topics/gateway#gateway-intents) + * **This includes the privileged "Server Members Intent"**, which is used for server greetings/welcome messages. + This means that **you need to enable "Server Members Intent"** on the bot's page on the Discord Developer Portal. * Added support for editing and deleting staff replies * This is **enabled by default** * This can be disabled with the `allowStaffEdit` and `allowStaffDelete` options @@ -38,7 +41,6 @@ Please report any bugs you encounter by [creating a GitHub issue](https://github **Internal/technical updates:** * Updated Eris to v0.13.3 * Updated several other dependencies -* The client now requests specific gateway intents on connection * New JSON Schema based config parser that validates each option and their types more strictly to prevent undefined behavior ## v2.31.0-beta.0 diff --git a/src/bot.js b/src/bot.js index 82ef37e..666869a 100644 --- a/src/bot.js +++ b/src/bot.js @@ -2,6 +2,10 @@ const Eris = require("eris"); const config = require("./cfg"); const intents = [ + // PRIVILEGED INTENTS + "guildMembers", // For server greetings + + // REGULAR INTENTS "directMessages", // For core functionality "guildMessages", // For bot commands and mentions "guilds", // For core functionality @@ -9,7 +13,8 @@ const intents = [ "guildMessageTyping", // For typing indicators "directMessageTyping", // For typing indicators - ...config.extraIntents, // Any extra intents added to the config + // EXTRA INTENTS (from the config) + ...config.extraIntents, ]; const bot = new Eris.Client(config.token, { From ab40214b640428884609ac51efd4731b506e5dfd Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 17 Aug 2020 01:25:05 +0300 Subject: [PATCH 093/300] Add renamed config options to the changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8393046..30bb2a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,11 @@ Please report any bugs you encounter by [creating a GitHub issue](https://github * The bot now requests the necessary [Gateway Intents](https://discord.com/developers/docs/topics/gateway#gateway-intents) * **This includes the privileged "Server Members Intent"**, which is used for server greetings/welcome messages. This means that **you need to enable "Server Members Intent"** on the bot's page on the Discord Developer Portal. +* Renamed the following options. Old names are still supported as aliases, so old config files won't break. + * `mainGuildId` => `mainServerId` + * `mailGuildId` => `inboxServerId` + * `categoryAutomation.newThreadFromGuild` => `categoryAutomation.newThreadFromServer` + * `guildGreetings` => `serverGreetings` * Added support for editing and deleting staff replies * This is **enabled by default** * This can be disabled with the `allowStaffEdit` and `allowStaffDelete` options From bdfdeaf7e181032c89a672b46e5ef7303325930f Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 17 Aug 2020 01:25:29 +0300 Subject: [PATCH 094/300] Emphasized breaking changes in the changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30bb2a1..0a3b219 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,9 @@ Please report any bugs you encounter by [creating a GitHub issue](https://github.com/Dragory/modmailbot/issues/new)! **General changes:** -* Added support for Node.js 14, dropped support for Node.js 10 and 11 +* **BREAKING CHANGE:** Added support for Node.js 14, dropped support for Node.js 10 and 11 * The supported versions are now 12, 13, and 14 -* The bot now requests the necessary [Gateway Intents](https://discord.com/developers/docs/topics/gateway#gateway-intents) +* **BREAKING CHANGE:** The bot now requests the necessary [Gateway Intents](https://discord.com/developers/docs/topics/gateway#gateway-intents) * **This includes the privileged "Server Members Intent"**, which is used for server greetings/welcome messages. This means that **you need to enable "Server Members Intent"** on the bot's page on the Discord Developer Portal. * Renamed the following options. Old names are still supported as aliases, so old config files won't break. From ca7dde0850fdba8143671ccb4404788f88f57791 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 17 Aug 2020 01:32:30 +0300 Subject: [PATCH 095/300] Add instructions for enabling Server Members Intent in setup docs --- docs/server-members-intent.png | Bin 0 -> 97345 bytes docs/setup.md | 11 ++++++----- 2 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 docs/server-members-intent.png diff --git a/docs/server-members-intent.png b/docs/server-members-intent.png new file mode 100644 index 0000000000000000000000000000000000000000..463d22a12724318b1180cd9cf26de3a7607167bb GIT binary patch literal 97345 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 E0Dk7pVgLXD literal 0 HcmV?d00001 diff --git a/docs/setup.md b/docs/setup.md index 4d3a447..576e1ce 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -12,12 +12,13 @@ To keep it online, you need to keep the bot process running. * A **user**, in modmail's context, is a Discord user who is contacting modmail by DMing the bot ## Prerequisites -1. Create a bot account through the [Discord Developer Portal](https://discordapp.com/developers/) -2. Install Node.js 12, 13, or 14 -3. Download the latest bot version from [the releases page](https://github.com/Dragory/modmailbot/releases) +1. Create a bot on the [Discord Developer Portal](https://discordapp.com/developers/) +2. Enable **Server Members Intent** in the bot's settings page on the developer portal ([Image]((server-members-intent.png))) +3. Install Node.js 12, 13, or 14 +4. Download the latest bot version from [the releases page](https://github.com/Dragory/modmailbot/releases) * Make sure the release doesn't say "Pre-release" next to it unless you want to run an unstable beta version! -4. Extract the zip file that you just downloaded -5. In the bot's folder (that you extracted from the zip file), make a copy of the file `config.example.ini` and rename the copy to `config.ini` +5. Extract the zip file that you just downloaded +6. In the bot's folder (that you extracted from the zip file), make a copy of the file `config.example.ini` and rename the copy to `config.ini` * If you're on Windows, the file may be named `config.example` (without `.ini` at the end) ## Single-server setup From d9683efec6c70787858e275ca40f7e29621c47b1 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 17 Aug 2020 01:34:10 +0300 Subject: [PATCH 096/300] Add very important line break --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a3b219..14c836f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ Please report any bugs you encounter by [creating a GitHub issue](https://github * **BREAKING CHANGE:** Added support for Node.js 14, dropped support for Node.js 10 and 11 * The supported versions are now 12, 13, and 14 * **BREAKING CHANGE:** The bot now requests the necessary [Gateway Intents](https://discord.com/developers/docs/topics/gateway#gateway-intents) - * **This includes the privileged "Server Members Intent"**, which is used for server greetings/welcome messages. + * **This includes the privileged "Server Members Intent"**, which is used for server greetings/welcome messages. This means that **you need to enable "Server Members Intent"** on the bot's page on the Discord Developer Portal. * Renamed the following options. Old names are still supported as aliases, so old config files won't break. * `mainGuildId` => `mainServerId` From 153237109199a6ea35db620f647fcd27f315a639 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 17 Aug 2020 01:35:09 +0300 Subject: [PATCH 097/300] Add link to intent instructions image in changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14c836f..1260963 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ Please report any bugs you encounter by [creating a GitHub issue](https://github * The supported versions are now 12, 13, and 14 * **BREAKING CHANGE:** The bot now requests the necessary [Gateway Intents](https://discord.com/developers/docs/topics/gateway#gateway-intents) * **This includes the privileged "Server Members Intent"**, which is used for server greetings/welcome messages. - This means that **you need to enable "Server Members Intent"** on the bot's page on the Discord Developer Portal. + This means that [**you need to enable "Server Members Intent"**](docs/server-members-intent.png) on the bot's page on the Discord Developer Portal. * Renamed the following options. Old names are still supported as aliases, so old config files won't break. * `mainGuildId` => `mainServerId` * `mailGuildId` => `inboxServerId` From c7b49b548425fa24aa04e30532f521293d14fe6a Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 17 Aug 2020 01:43:36 +0300 Subject: [PATCH 098/300] Tweaks to default message number formatting --- docs/commands.md | 6 +++--- src/formatters.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/commands.md b/docs/commands.md index d273f7d..2def0c0 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -64,11 +64,11 @@ Cancel the ping set by `!alert`. ### `!edit ` Edit your own previous reply sent with `!reply`. -`` is the message number shown in brackets before the user reply in the thread. +`` is the message number shown in front of staff replies in the thread channel. ### `!delete ` Delete your own previous reply sent with `!reply`. -`` is the message number shown in brackets before the user reply in the thread. +`` is the message number shown in front of staff replies in the thread channel. ### `!loglink` Get a link to the open Modmail thread's log. @@ -88,7 +88,7 @@ Prints the ID of the current DM channel with the user ### `!message ` Shows the DM channel ID, DM message ID, and message link of the specified user reply. -`` is the message number shown in brackets before the user reply in the thread. +`` is the message number shown in front of staff replies in the thread channel. ## Anywhere on the inbox server These commands can be used anywhere on the inbox server, even outside Modmail threads. diff --git a/src/formatters.js b/src/formatters.js index 6693276..e749ed4 100644 --- a/src/formatters.js +++ b/src/formatters.js @@ -98,7 +98,7 @@ const defaultFormatters = { result = `[${formattedTimestamp}] ${result}`; } - result = `\`[${threadMessage.message_number}]\` ${result}`; + result = `\`${threadMessage.message_number}\` ${result}`; return result; }, From 3aa74f2cbfbdf2d85326677fe899f4892d2f5db7 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 17 Aug 2020 01:45:45 +0300 Subject: [PATCH 099/300] Hide message numbers from non-verbose logs --- src/formatters.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/formatters.js b/src/formatters.js index e749ed4..56d0946 100644 --- a/src/formatters.js +++ b/src/formatters.js @@ -164,7 +164,12 @@ const defaultFormatters = { if (message.message_type === THREAD_MESSAGE_TYPE.FROM_USER) { line += ` [FROM USER] [${message.user_name}] ${message.body}`; } else if (message.message_type === THREAD_MESSAGE_TYPE.TO_USER) { - line += ` [TO USER] [${message.message_number || "0"}] [${message.user_name}]`; + if (opts.verbose) { + line += ` [TO USER] [${message.message_number || "0"}] [${message.user_name}]`; + } else { + line += ` [TO USER] [${message.user_name}]`; + } + if (message.use_legacy_format) { // Legacy format (from pre-2.31.0) includes the role and username in the message body, so serve that as is line += ` ${message.body}`; From 6a8ecfed8a3b91e58ce1d4b20a285e7eb51bb81d Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 17 Aug 2020 01:46:51 +0300 Subject: [PATCH 100/300] Small extra tweak to thread message numbers --- src/formatters.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/formatters.js b/src/formatters.js index 56d0946..6a4009b 100644 --- a/src/formatters.js +++ b/src/formatters.js @@ -98,7 +98,7 @@ const defaultFormatters = { result = `[${formattedTimestamp}] ${result}`; } - result = `\`${threadMessage.message_number}\` ${result}`; + result = `\`${threadMessage.message_number}\` ${result}`; return result; }, From adc54909faca8b4a0a4137ce0b134421377b8a08 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 17 Aug 2020 01:53:21 +0300 Subject: [PATCH 101/300] Tidy up edit/deletion styles --- src/formatters.js | 27 ++++++++++++++++++++++----- src/utils.js | 5 +++++ 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/formatters.js b/src/formatters.js index 6a4009b..f484216 100644 --- a/src/formatters.js +++ b/src/formatters.js @@ -119,15 +119,32 @@ const defaultFormatters = { }, formatStaffReplyEditNotificationThreadMessage(threadMessage, newText, moderator) { - let content = `**${moderator.user.username}#${moderator.user.discriminator}** (\`${moderator.id}\`) edited reply \`[${threadMessage.message_number}]\`:`; - content += `\n\nBefore:\n\`\`\`${utils.disableCodeBlocks(threadMessage.body)}\`\`\``; - content += `\nAfter:\n\`\`\`${utils.disableCodeBlocks(newText)}\`\`\``; + let content = `**${moderator.user.username}#${moderator.user.discriminator}** (\`${moderator.id}\`) edited reply \`${threadMessage.message_number}\``; + + if (threadMessage.body.length < 200 && newText.length < 200) { + // Show edits of small messages inline + content += ` from \`${utils.disableInlineCode(threadMessage.body)}\` to \`${newText}\``; + } else { + // Show edits of long messages in two code blocks + content += ":"; + content += `\n\nBefore:\n\`\`\`${utils.disableCodeBlocks(threadMessage.body)}\`\`\``; + content += `\nAfter:\n\`\`\`${utils.disableCodeBlocks(newText)}\`\`\``; + } + return content; }, formatStaffReplyDeletionNotificationThreadMessage(threadMessage, moderator) { - let content = `**${moderator.user.username}#${moderator.user.discriminator}** (\`${moderator.id}\`) deleted reply \`[${threadMessage.message_number}]\`:`; - content += "```" + utils.disableCodeBlocks(threadMessage.body) + "```"; + let content = `**${moderator.user.username}#${moderator.user.discriminator}** (\`${moderator.id}\`) deleted reply \`[${threadMessage.message_number}]\``; + + if (threadMessage.body.length < 200) { + // Show the original content of deleted small messages inline + content += ` (message content: \`${utils.disableInlineCode(threadMessage.body)}\`)`; + } else { + // Show the original content of deleted large messages in a code block + content += ":\n```" + utils.disableCodeBlocks(threadMessage.body) + "```"; + } + return content; }, diff --git a/src/utils.js b/src/utils.js index 2a8bddb..6c537cf 100644 --- a/src/utils.js +++ b/src/utils.js @@ -306,6 +306,10 @@ function escapeMarkdown(str) { return str.replace(markdownCharsRegex, "\\$1"); } +function disableInlineCode(str) { + return str.replace(/`/g, "'"); +} + function disableCodeBlocks(str) { return str.replace(/`/g, "`\u200b"); } @@ -351,6 +355,7 @@ module.exports = { humanizeDelay, escapeMarkdown, + disableInlineCode, disableCodeBlocks, readMultilineConfigValue, From aff119549edf51fcf774d22c5a90af803dac78b8 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 17 Aug 2020 01:58:20 +0300 Subject: [PATCH 102/300] Removed some leftover brackets around message numbers --- src/formatters.js | 2 +- src/modules/id.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/formatters.js b/src/formatters.js index f484216..d469d44 100644 --- a/src/formatters.js +++ b/src/formatters.js @@ -135,7 +135,7 @@ const defaultFormatters = { }, formatStaffReplyDeletionNotificationThreadMessage(threadMessage, moderator) { - let content = `**${moderator.user.username}#${moderator.user.discriminator}** (\`${moderator.id}\`) deleted reply \`[${threadMessage.message_number}]\``; + let content = `**${moderator.user.username}#${moderator.user.discriminator}** (\`${moderator.id}\`) deleted reply \`${threadMessage.message_number}\``; if (threadMessage.body.length < 200) { // Show the original content of deleted small messages inline diff --git a/src/modules/id.js b/src/modules/id.js index abc09a8..b8ffdf8 100644 --- a/src/modules/id.js +++ b/src/modules/id.js @@ -27,7 +27,7 @@ module.exports = ({ bot, knex, config, commands }) => { : `https://discord.com/channels/@me/${channelId}/${threadMessage.dm_message_id}`; const parts = [ - `Details for message \`[${threadMessage.message_number}]\`:`, + `Details for message \`${threadMessage.message_number}\`:`, `Channel ID: \`${channelId}\``, `Message ID: \`${threadMessage.dm_message_id}\``, `Link: <${messageLink}>`, From 4d42656c24c8dbc5b7aeea462f3c62d1b8b6a5d0 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 17 Aug 2020 02:02:19 +0300 Subject: [PATCH 103/300] Delete command message after !edit/!delete Consistency with !reply --- src/modules/reply.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/modules/reply.js b/src/modules/reply.js index 146469e..a26f531 100644 --- a/src/modules/reply.js +++ b/src/modules/reply.js @@ -44,7 +44,8 @@ module.exports = ({ bot, knex, config, commands }) => { return; } - await thread.editStaffReply(msg.member, threadMessage, args.text) + await thread.editStaffReply(msg.member, threadMessage, args.text); + msg.delete().catch(utils.noop); }, { aliases: ["e"] }); @@ -64,6 +65,7 @@ module.exports = ({ bot, knex, config, commands }) => { } await thread.deleteStaffReply(msg.member, threadMessage); + msg.delete().catch(utils.noop); }, { aliases: ["d"] }); From f8d53741e44628cca348c983552cb6c6eb07bf17 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 17 Aug 2020 02:03:19 +0300 Subject: [PATCH 104/300] Update version in package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 56e9964..790a4e0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "modmailbot", - "version": "2.31.0-beta.0", + "version": "2.31.0-beta.1", "description": "", "license": "MIT", "main": "src/index.js", From 958db5135dce7713d09e562221e58532abc50d51 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 17 Aug 2020 02:07:03 +0300 Subject: [PATCH 105/300] Use releases instead of tags in update check, ignore prereleases/drafts --- src/data/updates.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/data/updates.js b/src/data/updates.js index ac83ad8..02b8cf1 100644 --- a/src/data/updates.js +++ b/src/data/updates.js @@ -42,7 +42,7 @@ async function refreshVersions() { https.get( { hostname: "api.github.com", - path: `/repos/${owner}/${repo}/tags`, + path: `/repos/${owner}/${repo}/releases`, headers: { "User-Agent": `Modmail Bot (https://github.com/${owner}/${repo}) (${packageJson.version})` } @@ -62,7 +62,10 @@ async function refreshVersions() { const parsed = JSON.parse(data); if (! Array.isArray(parsed) || parsed.length === 0) return; - const latestVersion = parsed[0].name; + const latestStableRelease = parsed.find(r => ! r.prerelease && ! r.draft); + if (! latestStableRelease) return; + + const latestVersion = latestStableRelease.name; await knex("updates").update({ available_version: latestVersion, last_checked: moment.utc().format("YYYY-MM-DD HH:mm:ss") From cb9f04a5e326e9a235f34601721ffba5779b41c6 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 17 Aug 2020 02:12:02 +0300 Subject: [PATCH 106/300] faq: add entry about message numbers, remove notes on attachments Note on attachments is no longer necessary as the new default is to link to the original attachment instead of rehosting it. --- docs/faq.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index 29b0370..889938e 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -1,21 +1,21 @@ # 🙋 Frequently Asked Questions +## What are these numbers in front of staff replies in modmail threads? +Each staff reply gets an internal number. This number can be used with +`!edit`, `!delete`, `!message` and potentially other commands in the future. + ## In a [single-server setup](setup.md#single-server-setup), how do I hide modmails from regular users? 1. Create a private category for modmail threads that only your server staff and the bot can see and set the option `categoryAutomation.newThread = 1234` (replace `1234` with the ID of the category) 2. Set the `inboxServerPermission` option to limit who can use bot commands. [Click here for more information.](configuration.md#inboxserverpermission) -## My logs and/or attachments aren't loading! -Since logs and attachments are both stored and sent directly from the machine running the bot, you'll need to make sure +## My logs aren't loading! +Since logs are stored and sent directly from the machine running the bot, you'll need to make sure that the machine doesn't have a firewall blocking the bot and has the appropriate port forwardings set up. [You can find more information and instructions for port forwarding here.](https://portforward.com/) By default, the bot uses the port **8890**. -## I don't want attachments saved on my computer -As an alternative to storing modmail attachments on the machine running the bot, they can be stored in a special Discord -channel instead. Create a new text channel and then set the options `attachmentStorage = discord` and -`attachmentStorageChannelId = 1234` (replace `1234` with the ID of the new channel). ## I want to categorize my modmail threads in multiple categories Set `allowMove = on` to allow your staff to move threads to other categories with `!move` From 47c3878e071a1de8041ab5fa25cf3c582a3dd9ff Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 17 Aug 2020 02:14:12 +0300 Subject: [PATCH 107/300] Show FAQ more prominently in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1089ed6..11b09df 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,11 @@ Inspired by Reddit's modmail system. ## Getting started * **[🛠️ Setting up the bot](docs/setup.md)** +* **[🙋 Frequently Asked Questions](docs/faq.md)** * [📝 Configuration](docs/configuration.md) * [🤖 Commands](docs/commands.md) * [📋 Snippets](docs/snippets.md) * [🧩 Plugins](docs/plugins.md) -* [🙋 Frequently Asked Questions](docs/faq.md) * [Release notes](CHANGELOG.md) * [**Community Guides & Resources**](https://github.com/Dragory/modmailbot-community-resources) From ceff84a9ad7b961927d36b7f34cd771e3dd13849 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 17 Aug 2020 02:15:05 +0300 Subject: [PATCH 108/300] Fix server intents image link in setup.md --- docs/setup.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/setup.md b/docs/setup.md index 576e1ce..487f868 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -13,7 +13,7 @@ To keep it online, you need to keep the bot process running. ## Prerequisites 1. Create a bot on the [Discord Developer Portal](https://discordapp.com/developers/) -2. Enable **Server Members Intent** in the bot's settings page on the developer portal ([Image]((server-members-intent.png))) +2. Enable **Server Members Intent** in the bot's settings page on the developer portal ([Image](server-members-intent.png)) 3. Install Node.js 12, 13, or 14 4. Download the latest bot version from [the releases page](https://github.com/Dragory/modmailbot/releases) * Make sure the release doesn't say "Pre-release" next to it unless you want to run an unstable beta version! From 15afd0995d0db49508b724c24f04218e24d3c648 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 17 Aug 2020 02:17:32 +0300 Subject: [PATCH 109/300] Some wording tweaks/clarifications in docs --- CHANGELOG.md | 2 +- docs/commands.md | 2 +- docs/configuration.md | 8 ++++---- docs/setup.md | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1260963..f876876 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ Please report any bugs you encounter by [creating a GitHub issue](https://github * The supported versions are now 12, 13, and 14 * **BREAKING CHANGE:** The bot now requests the necessary [Gateway Intents](https://discord.com/developers/docs/topics/gateway#gateway-intents) * **This includes the privileged "Server Members Intent"**, which is used for server greetings/welcome messages. - This means that [**you need to enable "Server Members Intent"**](docs/server-members-intent.png) on the bot's page on the Discord Developer Portal. + This means that [**you need to turn on "Server Members Intent"**](docs/server-members-intent.png) on the bot's page on the Discord Developer Portal. * Renamed the following options. Old names are still supported as aliases, so old config files won't break. * `mainGuildId` => `mainServerId` * `mailGuildId` => `inboxServerId` diff --git a/docs/commands.md b/docs/commands.md index 2def0c0..dcd6bb3 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -13,7 +13,7 @@ Send a reply to the user. **Example:** `!r How can I help you?` -To reply automatically without using `!reply`, [enable `alwaysReply` in bot settings](configuration.md). +To reply automatically without using `!reply`, [turn on `alwaysReply` in bot settings](configuration.md). ### `!anonreply ` / `!ar ` Send an anonymous reply to the user. Anonymous replies only show the moderator's role in the reply. diff --git a/docs/configuration.md b/docs/configuration.md index b32912e..7267f3f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -32,10 +32,10 @@ You can add comments in the config file by prefixing the line with `#`. Example: option = value ``` -### Toggle options -Some options like `allowMove` are "**Toggle options**": they control whether certain features are enabled (on) or not (off). -* To enable a toggle option, set its value to `on`, `true`, or `1` -* To disable a toggle option, set its value to `off`, `false`, or `0` +### Toggled options +Some options like `allowMove` can only be turned on or off. +* To turn on a toggled option, set its value to `on`, `true`, or `1` +* To turn off a toggled option, set its value to `off`, `false`, or `0` * E.g. `allowMove = on` or `allowMove = off` ### "Accepts multiple values" diff --git a/docs/setup.md b/docs/setup.md index 487f868..fc0fbf3 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -13,7 +13,7 @@ To keep it online, you need to keep the bot process running. ## Prerequisites 1. Create a bot on the [Discord Developer Portal](https://discordapp.com/developers/) -2. Enable **Server Members Intent** in the bot's settings page on the developer portal ([Image](server-members-intent.png)) +2. Turn on **Server Members Intent** in the bot's settings page on the developer portal ([Image](server-members-intent.png)) 3. Install Node.js 12, 13, or 14 4. Download the latest bot version from [the releases page](https://github.com/Dragory/modmailbot/releases) * Make sure the release doesn't say "Pre-release" next to it unless you want to run an unstable beta version! From 5886da6da67ac4ac3d142b38de6fe75f6a197cbc Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 17 Aug 2020 02:20:11 +0300 Subject: [PATCH 110/300] Update server members intent instruction image --- docs/server-members-intent.png | Bin 97345 -> 71993 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/server-members-intent.png b/docs/server-members-intent.png index 463d22a12724318b1180cd9cf26de3a7607167bb..375b251590461169ec6936017da3edd1c602f0e6 100644 GIT binary patch literal 71993 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 literal 97345 zcmeFZc~p|?_cv^I%uZ(3X|OW2PUkdeYB}btdCbZ&$54^VoW&VuBrEHbsg*h8gtMZe zCIZe@rj$4fsGyh{p&*(As37v-`F4I!zu)^l|3B|q@3of8#jV`e#dUx7-k)pl&*rI( zm6_z8<9kFzL?o|Y`^#2DWEVq3M08^JF5#8#_ZpXkzjlP$nq3j8?mICjeDRByv8Azy z$lDZ&%{yYk*S`i`1B8l*?CaS6*?|izzb7IhD7pTZv3;c5JU{BSEb$FJrX4npU7UzB5U~S1ZU!HLx(i1-gFwr`Tl9SmVq`?Kfxb-XInMOw@8tYsCEM z+tYWw@*+dbHem*wCF|oN`7DDdB1E{Xk*tXNHQyK8i{Bn6rQ&r&yk8JbO8n=w zh>@HqL}cJ3!uZIK3-|SYVTj}#+djPTk1TZ2GTrC(Vk}*D*zfZYE_EZR^dx~NA3Kr^zns<+XwBou*SGU{^5o3m z)YJ{u=QXeH2MH;j9y-zg&VP;h-6&TyOd_KO)tKn`$4X_R&(JU?j@ed+meC>;jI}6d zfAe6D^mkQ{d7+*ZFaKr`l|2y`bNZp1)aYsw&wrxmAS?Cr=B}f4A4n=EhBr5k$YA`- zl#Y$m)LHnIaAy`eF?ady;foH&#|M&1c8&EdNPuFMBt~c1n=joztFpP&HQ4QT0e{h@ zI3g_Q+I!X3xaCy%nH9OXdSGxK36VkGPe)^YRTg7}c=~4;(FY3Rb)<%n(Y{Y6 zg6!b;-uNCWchUU3)7)w7>|&NzNGt&m{*)A~ zR-QkL`y9evKIxD{9ejTZJn1E1()~Lt&c50Jm#_vD50?+?pcgoKC7FdzH-laYQTfjp z&`wg|D>$F_Gh}r>c?zU%uQ; z;8e-X{=U5mP7fy+Q2T9sBZKvUCx36z2?>B-a7pR=t=un=C<@7VVp3(P|7zx0d zWf=Ini?5;O0B$GB{j!2Jp+mcQl|dV?{C26-zDWV2*i6mQo{bcW$(kFekM|MM;ZMjx zb7mhWX~Oy}nwd?<^R!3&N|mS$;|ru7RP&LsZb-_g(#>PYm4cYSdcTD?0OvK*5Jum# zXyMmDp7wxTrrKbD0(kPvTrU%DK#hi~mY>~II>8;7;TU!LfAm9+e0hT6I#s*+*_};l zYiJ1oYj57kZt{Mp6iCJBR{M10St(4ju}3~d^I0K$F@7jcJzJ*`gy?~4wzNpj%s;+; z)h&fO11`kg*}5VV7xU2b7g5O<$M5N?N%hSE0_5t4kr(Ye3wzLVIWeL!x?J^)h&9fg>17*o@rk==?#f27Tm8Uat_c2+b ztltwnz`Jhe=?RfF+_=ISqZFySQ7NydJ?`~&q})@Ru6IK(u_CBw{TR3+eUXfsq{b{> z!RTkbET?s_XIqB|yR1`ptV#BY7DdNRq`)>)05Qac+8)MPQbvYD6xzgtnC|Q;&LEPF zfH&bP3M?&J;8;`Z-9~zfTtE55qI?OTO*a6&+zvCs@ue_aiV*d}s{geMCK}ZXY*2N& zx!*dziKg~=3_UXGJHj~@a9Y~`?O}s=BvDk14%?H|I5BGbl|y_1kTm zlo7CdQ|$avz;~f5`Z49XTcZ^Mfx9sl=aUpX5@T;44D7yPfhR9 z$Ywg}+dKQ3r<$-1&1{WLYE_b!6l`3M(lT~+a;PqO05YRV&T5oWhsk*#9APP-8|$89 zpuJ=;CU)brX1+CowNnn0Ad~GmrIB{b9fb`*;qpfBqOMB4wFbdzB;9IzN!8Gg+;z>r z;&pnjR_(U%q_q4|DIil#{J+;jG`+%B{5#_%z*F;p@W9FD8O91~*Q+^Q#Ip&=>fZe+@X1WqWV%`x@+cwTF-Qb_(Q$Rs{x_+&6z`}`BiQi zj7xtO&Gy%tNVYq4j>oQnrKa%)Q(&%x1~s7@GnL(t^OjLmIl%>a=xXH!CHAsvG$*G$ zj$H}zU^?lpa(fivz1`;`kFFr|1FrG;?XF2j(a|~S;@t_Uo^fCyK!-Sr2 zO`IcU83C1`3pmTsvT2<0(RVJ*TZjuvD(gR@aDLHFh+iH?=-j_n{Aq*r?9K8gk$OsG zz_hMhOqd((dF8D|gbN<^z53WnG15in{OV4yzm}Oe}@-0OzJ*WL z6#YFS2s1`ESlg%?&)DMNY4x>z@U2yon6R;DA#Msb(lJXtjaARNr!7V-RC_&F`uEf! zEcdKNIitC)z&j^$#YgQWl{Y5FlTnTh~u8l8up{FQAoN{GqzO4bu zUTx%LJgC|Nu(?d;WMpg30xnZDJnx4A=G)?^+~v4ENi%2)g^6C+X>7xEZmwR8 zl+pEa2#>+1WpmOqZr=9Y`jm)8R1pr94_CDl&CBd1w>+GGDXrfO&kl34xf+n|h1a@J zXK=^)qyz>WNB(wxeKy@6qv1jteNm~Lju(XlpRD@*R;kg%u%>2KtMo2~%3X}t*sxd7 zI&Br&K_@Dh-=*Zg=T*R{j~{anfjD4TaXKfS?&B7iVyqd=3UEPrA}vp7jR!Q@9-S@WmQm|V~$NuBOjLc+ z=$#NTAtWor7{xet_;4%Af7(_|ErRmu*ERb$$lYO8gp-tG8!NNw5=#5Nt(#*n@6GVJ zC;Y@FQFDQng%G4i|1#z#FSa_ylU7Ba{>)#1VIo$yff+Sk_LpHM2~bWTo^fIF31)wS`&fSqH% zUxXwkMYhnMw_3mMM#jW80S^|Z0W}7{1y<*1(-i{!-U#Q%WZ;WMAtRK55)C1SsUDa! z5#J_(n$eJ0mtNt4VSr!h>!&D?6Ob`+b= zD0qfKLp~mN!Ft9|F#2!tzKY0Tqp`j6Q8S%1(dZ+MtA%YPGaFDm{- zzf>UPT-1B4eYD$H%Wa#Sg?`iF3-z@*v*FL85xHDfrFX^Zt@dXu);tbb>=~ZfTtAY# z6iy?m1UDbrPcT;-nXZeYe0IlKd%{&r^Q-d=q+xkeO9#v9;9Hxzy;XKmy~T+sr~YGY zB1(|Re5*rMsrGIffLdQ>n`aETH&q7w?R%T_L#HvMN(6<@ z{dOt6rDaD@7?LHbG7)208u)qYk@%+4x@~TNu9U}65k>wZnHU;aGQe^w4NE_6{Lzev!B!& zoY;FplUnM(TNHxW`dbu|z_$~kBzWygj6KhSl7V*5?mZS|rZe!XtEdpas6n+)t2gu$ zdw`LGhgHu5Ue5W*74L!XEHg@f5FH)md`r_J_44J{8P$MW(XA^BF^6e-7mpawm1?Y( zmKA1v_B@_*P#P`a2)HBd)z^kumkvh`;fHP6&GyapEg!Mu)vaYplAAEr=|h#b^i06k z4a!nF3c&sQr`ie%ET?v^!;zz|LS|8c*O5|1Jcp5%4Fwx2b>W>YxpL)%s<${NgO_4m z^|`?|wlubFlPFv_eBnI3giMRa!1?oZj9>qje{&nJbu7bD|e@gIW#6W?y)x%h{AQO0QdCeH=vN-wr94Ca%%XaI8-uMqRO3*uWre} z+Vz)9U>ztSU%spWjBeBf$-<*(D4Gb>OrBqnlQ*JFoOy5Ay4ps%?;0b{@MVS4`3J<8 z>I2xP@kEU%8Oo*fx`s8zBo681**dsf-%8R^I5N&%>!vsy>XKDhq|0B6ic9W;4Hf>y3TfJj2fr7&_(* zNn81QMK-syXKbY+`IJtL!?(V+ov${40J9YM@fBfG=5>!^h<5TZQ{1=wQ&M4EgDCHw zV&#|xd#0ee1t?q4ngV3E3c!&oU?}VDVZSQ6WUI?-;gd4D^y4XE)CM!?D=O_-e)I)C zEyN{nZ0znv^;KyIWXJCo!<`R#Pm(IG#YDdUEoR}%%3+_0&ku{b{v2j%1Pxr&jr|&_ zY8|#0vAy=yHnwt1_?m%k=z@oC+;Zo0s)`#ovaZFY;U0jrRUjqAiu83^Cv=XP^S5^e zMYPcG15@sr-m`z-8@uWa)^%|wWIhd_|0*FVVek-|-PPytHOaXAQfO(gQTqzXuu=OBsJMAE$tI0}=6d3FfQNK8J|uEJHkBzT@1F?AO@yzG zA}V;1f_OT3a?50_4zc+=Vx}T}XUAsj@?q5Cm zBO9rUYEfAP^M|N_V&(K=(ZdtDoKS^UHyuZv(EI@QL75PhuWPL_`MEK3Y4>>Qi3_7o zrR!A7`f~z_ohU-E13v1tv+z7)P2PDLhq)H`h=f6@c@u(Twat^BCJmHFS0>>_Jh8?2Ik@W*)~NoVd0$`It+&3Uf-5HEjHSn>7&+ z{79kZ0;m}gf}yxYs0nyD-K*}MaV*n_Z}y#mw)VsxAy`BAt$6hTZpW<2cLy+~>C#Fv zL;mOZ^tauNbgzSDJ>{Q9ypU!eTErwWJh2B}_gV+`W9+cz6qMk0IQ~@0hS;M!QqTLk zd3B2LTkwfj-plpcv?jCUJexjgc2dCGa+clXs(d6YT;`Hgi#=s)wDy3M2Lu6kaHPyf zHJQE{dR@jEiSF>IL_1)g4Tt+0GRp$Yio?K*^>0G)5GD!k2G0W=3UE4l%~t-17(-q6 zCh9|d3u6%V{DUfy0rMTkRP}BF;?>aohKZc19_)x#z8im!+s7}QCOy+a^ox#sZXMG_Mugv0tHK^ zV^c;{*v2#e)>DRy_4Aoac6I88BmB|Ny2&%#KToKvs_A8X^t$v_w#anGvYgyj9-d4~ z0=y-W3i#z1KN$iju5y@=S-$Gb$t@2g>|2yca=Fo8>GTZa0P#LH7@WDLdtUhDwDO!5 zgO4>;k0=443I|L5vOmy+9c8ct&87ZAT8BD2JFWt14W>FF~!+)}D`2wmEM6i*0$~xG2ueakCp%=8vi~ocO;jbtCgOJ@z_pru^;22WA~ACH2)T7RImiT z6o5P?p~v@u`Y~26EqSjI3z@E zP~*ReexYee$a6(&D2sDf9_C4C+#CHgj}pHaW^4gxoANo$Tra~83cDB7px%s5f8bQe zjny~BcYUa9TAz`g>x4g?5*`P7kJ#kNJ+DcWDwMbwJ=&UgtWZL!skdNg^Vsi87LvfA zft8rB1LkaBepXTmMiFFuXrj(W+9K?|33h$ZR0Vb$hGwPsOjMZjtH7=;tCw7w>|qvJs+BFVXv{NmF~15PHw9o4I$hB>-sPfN|yV3i{ zO?l=Av~tcIDmfT=`oihp35J9NNB}$$WYo-$*h=NYRs<2MFkin4t zww*M0=S8$Y4}=r$I?0pgB&|=}T8|vmMzD3#*ed{DAcd1VrdYTMX7pQh>-f`MbBNfbuS$9 z8;jWUtUOS|AZl{KXa8Y$fOTO^Vbtc?80IzL5ChihK+U4x2i}Ftrw~WH(?A< z$UsgY^zZC9;#s-ZQ?>5){Ee{UE877TYC`SX&C|5*EI*$=@qIE?C$J;fnb$~OoX;ee z&h%0o7vCaQj48~Szc~nUs&u)8;Mm6m9B}Z`5kb=LzV4Zh_)Dd6LziY)9R4idKI}Oy zms#NJ-nfib@_mAFiq}|#p+7UvF$9?myXF-RY%rV{vx%#huLnkh%;kMreNiyd+g5iv zwF<1WYgJpr@w(ZGW}U4MobnbyfL(LFRoYFyb4t1-78hmMwO#D{WcH(I748`&ZLn$z zTWi~SSb9&`j%#z#HHp$oViHiqT4Gq!r`oV6u4u$yU4aXC6)oN0n$vQ-p#J;aigl&E zd=!}p4o6711!pN=)~hRDo^BKkraCoUJ4&j=Jd!?L0ft#nUTDSKfTsESMH&~EBYy*( zyD58PMZ$cVe@tGgViJE|1#+niVb#0md;gtwf-Nlet%Y<-9=RSJd*Fujg#8o7vZ@uk zi*hWHXB9R+x4zO(?fc&Fs$n4#TA@pIGNs?_QOQr>m0WWL#njtD3k-f$iRQI^=Of~X zoz1oTXf#=sJ)cTN3kwRj;Wcw(9LqfsP{a77d$lr23%#3X!LN$$fFh!2+!f_eSCEK* zNXyBFck*_;X@d%n$vHhj10C)j+-k}qEEP%6rmD^javt;a4#_to^_AU^gFZxS?Av%d zM>&AK&gRBlXCoLA!}Y+WC~3Plkw;!|@hUGyLSid{QT(&Mp$(wiz_PUx?q57TJu?l> znA_KxU}ollAu*_!8;8V*H^pCzU;xRJ6Umpjm1&t?5RfCt=!@@hD3jAOR#ALOG}80`oI1+E^<}=r84pNrcbl*N-*piyTnuLGWvYZoii1R<6VWA zXWdUW5Qz#=;K5F(NyoIO84@MQXF(B4Y(EMikgU1H`kr{jH{!%sh(VEZO^vDa0X5#k##3y>|sxQgrGsh?b%7}R?+GDz|Sco->PQ& z>EGE1;yJ5n9%2ccG3Lw~_IirXSPMbW6Ej|{{&HXLi(x>0ui4r_*KMYRI z^$d>z36tOA;Xq$_lXJ4U5HPO)?|_+HRwD4yb@4QQVV3eyrsyN(XM`qy3u)!B9VYB` zB#ICKJdwhD`TcGob}T{q(ehB(`%^8m(>_$EJr^S{nqf7E5|;incd~!oPuY6Gz&AIX z^0gb^HyCGk`^I7-SwTVmTV-quwFM(&QU*07Dq-m>v$`a12}}}Gn~Z2ODo*t1#`99b zZ{`;}Uo^%)!964wd(=yrB+5fuI$^JI&~lFsb+oqLPi)NVDj zWc>PYw&Jw%!IA9afp<-LSD*(1n@KL}o(aw_PvRKzzZ@DE!0-%S)h1an&ygb#ZIYer zBWSh7%o{9uX_Ztbv%%mu)9mb?j`>VkL~nC*U=SmtYz=?HFit*|&zp18u+)@w`T zq;}yhUOuqAMYvVDbj@eaq4Tw+E#KjP5K0>sJL1YcCLb+#>(cyU2NvyRO>Ri^QwVX@ zBx!O)zr6O#+_PNDX;+w=%LjgHg)Z@I!Ntqc$~AEAiS)xnI__45rMf` z51L*D`mefpY={wG*Y+wyPMx+I7JIUO<*(Ra6KV7G42I+FGVgDe4g6EExF^bcJEIEQ zmIdVzz7LfjY|x#_6_7{&@gO(b*J0L`eUXhejL2okh;4g0xankRm{b!&>UFimEg}c2 zy!Tsgw@q`{4uh7%@NQnhaHc0Yfq#6hKfH4Uo6=1Yb?xZ6^SC~loRZBv0#4W&21 zCm6~awBoNp8PjdaVuJonLU5*zTYX+_s}5|d0m)OXQF_D*iz zk+|@w$b>{;I5{J@zvHO z=0!pg2ET47i4oVxHF<9MOGhyMoykPmbVXV{pm}B?WUF;e68$7wrEm6u^ZMoiVEq#JS-Rw$XnMfX3>*@C zMf!Y8@1E{ij?JC8qmegbI${UK64p6SrV{oIZX@LJ(1*4m|KmXaVc0tJ4sVL%NZHC9 z>d*r}1nnOZ(+7^OwAto$Kb8*i^{M29#@;y#xoIkG?}!9|wqga}Zvukw0+b7+G(8kC zbKPgFtvv3gE+qC5b1>m_iG$FG4AwR6E=lygBVeu%!OEXVXV^PJWw0Zi(wX)ol|z*Q zVj~~G=CVRE^70&rC5it!DE9H<k0dk@XXYQ+S z1>U2w;0xTdYQKGgJA7}@YjTZ?m^syAq3~tGN(8bxTXbSu?6B>BCFEiNAW*r6gwe z!QGf&Xdu-O_odLLRhKY?kXS`SjTqhHaG%NIWkNgm$=??*Bl(8}sgD@mLPBCk>7b#Z zZ7S-z!#EA4nvgi^4PQe=B_f>xzc^YjS#$g>H0HHin7=Q)a1W#vZigM&yhR`O>NYzWsRI^j5<-M5*0o{l zAAVvm5Ng-KJzOr)FE%1X(hww%5li&C2}61dYtu13DaX*p>`tD2yjPk8FJy@D26KCA z=vjymUgbZB~Pkg2lb^^cH*ceGmu0G#8G5r=KFb3r1{y;m>Bl6|7XUg1jFG@9;B$KV4 z6G)0m+hHMtkv0nB>8aGfQq?mklQswZ#gP0?*vr^vUOS;vmgR zAjicHcb#pFC1T9+6<{musI6OMwJ)!JA-6d@@8DNF4j0UbB!nmwMX#!j@46|ooIF^# zbjH43r6SjzDD+N#jH4Q!cN;C+B&RxO37NO>Z3o3_pa7&DeN0k|nnj}}p@|p=V^PCA z`>Slf$L1}t9P&~5GNiWhiZ_TU3dyNvm1_q0RDB2UPZ}hr2f5hcfENAPd-`|qlYbQy znA(?dFA?E|Z*IDot;hUc90t5)opgzzNR%m^lmBBH_G%!SqT~6o9;quFx2}2E@c=$i&;|FWi~$|C9y%!IS+XU2N>~ucpb+eF4~{^88(x%6>R*~~EOfU} zreIYdEPN1Vb_IaqvqjI#m)BD?$L2q(O&%yE}F$Ee1%Fl zB355sp)LQ|4Vr@@C`phJG@c%_Ap5H1iH|8oen@j&&B)f5RjF6ePNF4pQAl!g+#sf+ zpInz0DPP%F$R)=#|HZVCFI_IyrvUmd0c#3ZTz$2Z{SakY(djB=>8Rtu`|K+_QUvyS z_2Cm#ttj3}2jTf!p$5(C)|hU1{?PBOGp1;E)c;Z8j2-x#&Y8o%44AGH3~#7AxALxf z*zsM%EJiBR!w<`5``YPKN@1FaBjx9Gy6US7p_JXGxJ-LLFD3j)VYz@wEDtAEILnum z$13r#MadJs%$pNYl(H|NNV>St4G^mNq7WzRS{5jFM7c#^JVw`)>RV3FYs@5lt>ny? z52@n&JmDZ$M}SknhOHaiESc;?Mf3=|wh&J}vRZ>jOJifJ^m%EQ7ps&ktCTEHGT>9P3lOH#bh$vF zEGJA2Ygm`Tbb>L@p+(uf>!GcnPSF zGtsS#n_J0eS!#>W z+lz5?wY`nJ>F6U?&HTG>zTFT)qm65RnD2ji`?(?nwqAKU6wNlmGUnBHP!1LE_2K38g09H zbiW>yA3(j%C(GORUpfDHQRWSxPaO;Zp_rs-5-sbt$fZ^wj=XTT2$kqpzOQNJH zON+6$Ck+}$t5;hhCx-V?n7tGs!YQn$PD8961CXDEx%D$@jK}1Pl|XXyBJ8#_HLAk^ zwQ~1e_4#X%K_`xdw{vNi3n+S{vrOf+!CBF`0-@|DzYnFD<@xo`hYz$_ug;54`nS-Gn1D4pagZrvo-4*koozx3y!%Dg{ z+#O-EKdXpYN6S<5!e>lU#;E<8V)InJR=sAlyXlJ?o_oOxAPt+5y3Gz8Rk@`7UW%z2 zM?skIZC!c=23LnpWtLJrNQcXto1%_Pc){_EThLYWk`U?n5{+Ig)!!|`?&5b~3^S1q z#?LpST1wgKQER$26qtS;`LR7^pGdCTPjY8#U2%r1bv8FQWMbZ>&epp;t*l00xp|FWkv?yT3Ap$j zoqJH(AVxW~8OiNQ_(pMav*-^<0653tzYKYaLf(1&`IEl7Y(f+gp`;T4S|>FFzF1P@ zW*jJ&&&2L?E%84B49#CCtf5q2a_G)rG%YW9gs(+= z$f%-e2(UH~#t)qTUh!m}yj<725IJP9;x^qFlDdzqMTL#Hja|$%k_~$Ev;KQ7-vtzY zpUDLXcJQ5d5k&I;n_N5$E|l52-u)RJ{~ywr6J*PI{qoE^0*YV>MsDXi2~d1%!LUGD zgi*R=GV^y=D&g|5gV56n&HJCREPTgsCtE;+@h4gRf@wnvXp@E$$qzVgsGjVtcRfiB z=K*p7VK z?R!iqyW*hH$Q5D6>pvq%yZMj!Cc_OW!KGt=S3Y4}BTD8ow+n3l84WoQ?|dt@sW3&n z{THN$y)=q>zdG?>Rkg^M5YiptyLl_^J$c$0dm7LQ4EqTLM09ri=cNdxWW!Zh&h8;? zVsDR5K6AUs@L#vTmS6o6+uTzGDJDtgul)YgpU^S+3)Wj5FpFIz@-{>Sq1uiS((R__|M zp6;m*m1~<(EWyCvcDi|fe&g?QMc6O5AN}H_Dxd z4?PTn=_e>*)1e&N$YgC`>{1mtD+&w!{gWWEy@m9+DzdL{lRa`{qlWy{4vrn^Fq&9T z3cuU`TQjrb0B3!wG}=Hg?U)^n$IW*aRL;LhkKt>?1g}^yS&;PZKI*uu7l!Ht?qBAq zR69ZP#=TNj{kX27`PE+!4#QQ@H6fmNFi9$H=}6`ZJgQGlJ{oN2*00T3{p`g0MBbv+ z!Im;kgF9DLM&ohvyNB*G{u}C!mh&P$pL8}GXtD!8lZ^Wo$oddd#(F6yF+UnnNFQK3 z`>C`GFtXVZC6=)Z+2K=)&HfGaiMk%QNaoHM9=^;!H9$UUT_a|NFTv^birt`cJ|P0r z7;(c}-@ZG#RDU7UC+j0V809|joZNT}Wg#P2c;O;3i;-h(9I7}6Sp(~!#?mV}%WXgm ze^cuQz6IRh@8jnAHGntGgrn4WHy{R>_u+z(Ass5LBRoZdu~#&h{R?AjS|wxj^ALAZ z8(lmXx}w>(0a~LRBzM`BU|NKT(Qv_Fky}0DX{i0EV-Jm829+kp0`FVG`PUp2Qk^vu zelp5%fHmEC?@x-E3vGph01S~tEsPG}`clV@z-x12<3 zdqX@oSiKv0!t%0kP0770s!rq~l>BSPfs$9ZBlS7i$on4nQ|E) zN_)uC5%`#9+fs|AQlPut7B;yLZvbjv_K2bLoxqTq(#?jH0jPrniFmMBSgQX4$@};* z4=584Fq>?Mc7P1~cZYCvAbezH#ykaWZeyb@~lN7zmlY#bIR(D3`ft!VQqI#7aI9Y$|Hq{q!i&1o z?o&=uRuiP=U2xK)XS-O~nkW5Lcu`k4_AJV)-JwrU5ic5Si7oIya8i+uGe&boA*~rX z7%gh^?ZDWrD2lpQ&IDv_m+nZu5s;?F$igm3JOOXGD=PNF$~VF@Ec?IX17O;>E1?E0 zVrP{l?$lOm80-A(o45U3ihvJ4^Uzz?Sm+-Sw)i)}W2_UCF=JM3>lonrxMwpSudD}N z{AEYAnHBJFQQs1ekrrRM9=~Y$U%2;#u)C2h3b90uQ_rK<4&IE*-U-3>)bnf7Fmp2E z;JYgq-ucV4*Alr;Xg3T7XSx#?&Dn@{%))wXC)gx-!{&Z9`v1A0e;n_RU9v~`kVc)t z^}{Y9H^P&pyVdYn!>F;K$$EZjmI3T;UvLb{{##;mDH)<~;OVq);x>Jf`>u^WT~|8h z*~@O(7}_NRS!)d+$&?J|WJ}**cNwD{g>Dq}K43&=5pbIVf(}y=x;AT;x~NXCPvBbC zSp%=h0VSnLu3t>Zz5S2ym?bce36z*$sJy{!-q?{{zx5ir@z=%FTAZjZCs%@F<>L|x zFA}#m{wd!2VT~r>f-=F~ZPy4?eL>2rw?gU?OK;eL1v0_+=Wb-jY;KeDgPK_K3hf4M zSG6|f&=-Xg#oD#5N*8dCgLKB6{$dobj^4iVIM)qD2#0Cxs}M?;S6o1DRV%dKO8+&d zX`Mt-NH5+?c@#^H?R6V}OL+VXI54C{zhNouwv1sd76j|W^tb3{4vvh25P)bS$^U`I zRP$gSo*WP}c5Z}h88PW6EFFeu)7J2w6D2Dy1-K*ge%r<9N`~jOs$`8hg7qabF5s653qfy^vaufTb;Ne+C;jWe?^ovw z+t-%8U|Sii->U3#D|FFTBDpf#AIi8EOo{O5j~MPz7i)-SFWBwF92R4^U4~*}@z?eJ zl$GiD8%N8BS&D3fD#H?TbXc}kjmlT%ugo+5Qo4M_*;=DqZ<5RDz=w#Y%EiFl<4w%KUuS;>xFLm&1QJJF1o3NL{pofuY8EGU`t2cX_+Qp9Q@(Jp5+gT3wT%+$ zjF#BrYPYXx|F?DTg#2KOR7Qc0EoA35BZOR#dT_q>Fx@gsRS_@o5Zq*0G89I+0V1t* z^rL@aJT|zf6C>DGzIA`BKza#vN@G0XlW@3zikXb}=KwRW{%f~JPC?g2A%$;(V4I5q z!Z!JCzIRgD)C0X2HG6*{-Ux`RI3oP;DjK1z*|&C@+j5!k)M(8d?_Jp{m2U#Dlf-sm zSB^;EPDpfKP5|P3h00#S=QjS6y8cS|=nLYd(7sqoJ?+i2aC?^&@)Y>EFq@LzK7~*M zw0X`au>Xs@4jgu^Gs@+y2v$M!+m5hKBqzWF;njEkWoS$c2Hj=%M7XrKwv8EK^T$3GO{!Qb7XP1wr>tjlAo&0P1*nXCA@Ntv;az^A9MKPgxHop~PMA}+!n`ak~T4w0ds-zs!%|2OnEhdJoi zEP^9AGTtVSlj_9fvu+pREHF>Htiij9B{~jRQURodJTUQZ-ROt44-n6JYX*JVEu$8f zm6WaC(Pf?IbFj1r^E7~w(^Kpd>SBi19JzF-yUJhIIfE3{C^r8;KJM&|fYuqg81MN* zGFm>K+DEmbxE(ZjK=<>gHwZPlS81=ajTXp}UB0!H>gB<7l(7XflmF)}6v8CBgAaP$ zL(yY7lytA~u@R7<6uLguKaXwa0k1BmmFkJH^lY@CoYTls+TmKeARDlai!FFQ;D6p} zk9s0_{^Q&jQt5EM%iHLO(6t!D+QN+P@~Zx7KfiZY5DR4_2;0$e!jtsxMobJABh-*i ziw2+d8c~ZPF+r>A_uN{ak%$wn3>L91`(jkIYX5l<%{B^Mj!C9ssSXWsu3>C6+^Te> zLRbQk)OFH*$T5gJJ^@1K1SS<@u(WYgo=<=1Qo*Iv(Z%h3_AR7;&{q%e-Pj0q!LWKf z`-ogyfYNaJ7)03h63UFG28Gu3PKf>Jt*49$BS%s$Y=c-g?A7PxgQ$zDAAe`u@hVCc za&=C6LroVCk6=Oj(k}pS-Yj)AWmcF9tp#OlB6sZ@L^ElWiWzA8M|k|dSM5wnIV0>9 z!OWZmS$t!SHjZ%QBMshQ0|f=kTf>d=d6ht5jJ#peLQS#(l^8y`)-XFIRN*#0oLl*` zgu47TPiqzPkIK1{HmVc8ES9bA;iy`Zg-ITJM8yG78j$B3RlUv+ zb^@>e*W!OAlt>=eDXgA6z)nx`k^hLhsf0`K7M?m=e91bV<`un{&^1c@?XMO1GBo7h z6rAemX?$s8VXbZQM0BN~#KYl9z{4>T08$uz$Y{O~wSE5Dw(YULGmul@uBxbTVeEWX z@5_)Rpv8n?s|4zF=(0D{bkdhu?!j-kMhBd@=OpiYb)u`6;=eS6bNT9XH5W#T8Y~Y* zt9dWZlnwHUgScjLHKcTTAh=#AQ7PB&@BHukI53pU-$j1(&BZN%+Ek?dmBh_p45%#} z506{TGsovl_xbOjEX4EBm9$Rq(89}!B}u4W9|aTkUh3izhLqt z#Y5Auwe`d=5*cF{Xoh8uYEg@Y1=CIjZWuk3H_@u%*G%`hTDTnRGX3svQo%8+l_M8E zFQZ%XOb@01N~~*jj@uR^bG#>rd3NEGU&QNl>I2X>jmIudQcBximT7|h4qR?nYbzKF zKQ~D^?6yh_egA>O2yKW}Vg^+>MLCyoZ?N*Ty^OVCLd`oSXlePG$6svfP+4GN7<)pf z14cpTab}}!Y1UQ!o*5fU=T4hhpQG1MY7w1P>bX+F#VEt%^uui%kz~Qt)6iE&@yl5wlp+tu>&qG{)NFCw6$~C%vL>CeDi=WN zZG@1%N6g_iSG`=|xLl{l&2A==R5S~)33)ZT)YNwh6e#eM2h9(Tf(HSWM; z+wqqP-;m+J8kaiEN2Z?QS3?_+Y!zZbQ!YVt8eYyVFMIAhJH zDprb_#T3TeG>H|#?%y&wlk-w~Xv537I8>_@Zw+!u`i(QJ175QO|Fd>_{*R-4>I%W+|Rn5qvM`ooG2wM86p2^Y`c8)Ai0h; zW(!P2)iDCWSkOH-cvy!J4$kH{9KAdcH>}zug4D6kAk%qv(q$cOLNz@nF&$lXx;VPO zb@iD(?CW2Sj$ni6dHX1VH_VUk)_(3}+L|6lSh5-A4z<|<5nHypVHX%5(Bv|XA3x6T z6QN2Si4UYzy!)G67GAMMXv~L{Kj#jl959^x3-7(l)mQ$oP$o5_=YBY{233&$=NtCueJa9WqAXKZ=bDR~jD zxapa?^c9^8HQ-9k{>*XUC;wD#+@W=y$?yK_y7*3#m)!cZsNuu$7f7P_Z8kF-+o+LE)n++$n_ zI@Hk?!ic!h@Z8`DeY+8Rz#-|lZs!@RH{h8wy|q$?n}C1ec}AwYZJ3s>ix#UKES>w{pyy&FJ;a}ObbUfqv|NO|w2;2I zvbAF9Sg#DUcjSH8Y-km#x(4^Y5bAYZhZ8h_zl=A=z*JRCQAehQN*Pf>6-1%e5a&Ba zy&P1$Ss-QKcdMsJBAF`PByu7!o0F>S&-Ehm-+g*}*yHsgPYrShBz=)T9iqhidqq9< z_i-(PTpIqAv)>>H~*U$2V=vi}!*ZywF&+WviOZM9o19hA0; z?)L70qNeON{~pr zm%Z=3?|rZ5d7nR?wcbB|YrX!q+Us(i=W!nAaeThtBX-z9!fCFB-p|{ZiW=6(>rPhu zzI5!Sh{2#Q{ra}szZN9;`!f&ljCpE{k+Nb<>$0Uz2~jU4nG*%fWK>)(5=si|@pH(3 z6e-ZO8}?Tiq9qY%qMZYTWCL<@jk96i}9*5;@__;(h$?r zjt{dg-8fVhF}{gOe5lYO!5~L;(2_WTxwG{_Ivl$ra4bDDpc86FJt}`Dv}tNpYr%2G zUdH6k@`{aVPMeNmiOl zZ%rjP;-ad=h6K*uEzQ)Lz>o+T)nW!G-P7X+1fd<`d71bWn;AE`Hqwf>aoNzc>*CNk zZu{!BASP%~K*Y1)vn67kxC`@8|8s^Xzx4~2lk2k41uLHTC(_R7%51(Q#^m8`i6Y6$ zGZ~rE9t#Ili|D%puc`m5m~4)H=QCY85$^VcFCT20CAIkY(!sK1yx#Sj-mTML=cjNX z2?4QhXFH#dBd)RU7;QrUNr-cZ9J@O8PzzbaPom(3&#xQ%e`c;<`>nWsDT;kX-#CdA z(#L7Hi4K)X>N#Gis`|ioP>xh?V9=mjz=iHd1)-O3LhSikvs2X2fRO-R?ReNv8I;9) z6-tzdzS!bTU)e60j*{P@J-T;DJ=&yqbdByIPR&h-DU*E7e5I%I)$U&>A&{fD*|?-s zhSRWRG$WPB_yB{r4p+l7D4-XanJ$!*Lntr9y6;!2<|CsO8)j^n12uDu0pdpFbNcG) z&j?#cYCE&CSk)_9o~(tg_!YFej;F!Y(iSY$=|+{}vy1U={fA52}Jj zpZGiHFTP*4m60m7bw7ga!6>NDUj42KJ#y{{(0Gx&aiBKOJnkoDhc1y%gHOw2c0-M% zc|RL5Zn^I#`MTG$iP6*wu^?P;iC8f1xg5=Om?No-q+W^XRUgOO@zSMW;y}_Ok@D{C z*nOp<+jb2v=UR(`J4=qgp*W=P<()=DxE0X37Rn^+`lJzIMBA1&12CtK6pe920B z&t5yd1y~TML=p_* zrHbtt(gbE{kLlQMYXHPfOH;7yqT)aBqZvcBlhLw6)hnKa7?%L*zer$oy=K+G_Flt} zVSG6;->7Fc%w%C`DBf>-P9C(TN1AmS9e3egKFe-$hT3aK+Mi=Z*&K9crakVvrF0@; zmA*zBP%JGSL8Hgd0-sH2?kga^44OW#AwoX^WtftRX@gf4t&qoKP?$)do%DxGSdi}# z@QvhRl@3%~RtD}~@wUx6ZR8X5iS(|c7cOKg98z@2d?c}KCREwwd+25#h;bT0d7E8a zc*=P^LBQBulChfnH7)fJ=3PUk$t{M2I`6V)7(&8jQlyVl3o*m;aTD+oIW z?P)6DY*Z>wdr^+2SdKfmzjf@r1&#IS!&j`kJwKWPHKMA-$YY%2p1t0EOlzpE*=amF zlN=I|wz3r;KF6pLEh5@_}CdtYy$HKnlF41C48tueRo#arXcMI-qxz8jX(HsL+DJo0|hC8)Ak zKLB4RU+K8kotMCsUtO#!p2?`zNTj-K+sLNu2b6P}65-QLE`Qmy{jsxVrQP^UfSX_=XV?Ht%TW}Q=?&^F>s!74qPw#Rx6EYBir2lcfq+Nd5{B6juNlcDp2 z@}ao?IE3^cr!3khZuv?=HMh;{)>gDNy58jD%X>OjuC2-IE_yn%ZOaM^s$*Omb^q4s zatgj;#?7fADmp^jWc#Gz2}Zz3M2n+~lQFwYPGZB}mf1XjA?AiHZzOlBkXRqD7AstP zOCei84p6JSb*`1V20VMHRq5DCAlrun`4_BgrdhnJ+;R$L?=p@-RbYNHD=+T$%+p%j zayrO1X>LxYN{6Iz${*c_!ON@d9j`5xP)^#qwuPA{aWC7gH8swSc5B1!EEslHTKkQa zTKu-mNvMfNmmz&6D;m%L-%#j%Ibg~3rjL$<WT#0H1KnhE>6Y+a=ES=L5Nxa1=CYAli-hXo zvWj476ma(*Do%EV%L9AfFV-%Rd^7~HJHR8+=be8^)@}zpjK5!%(=PeLbLzK;N4gi5 zpn*BzuXRT`1^L%2GMM(RZA_ASl8v-3^e1Z|JZ}66atiwP3xg5Clf@8{kvmahJ-s5~ zei-P;nNh?($tb{4WOB7BY3R@i&Tn%Sm6II*yh$T#q*w)slT(`KnLu|MiY2D0-j3Z_ zDPOHlodPt5Fh@dJFiB=l@GsC#YFYi#F>CN9b099KO{|mj=(xo$v>or4c%;@eQ&LvU z(^W@V$G8l*OJY+doouvp55iG!hZbdnulXELPvR_+KPTmL94oLg}&YZPS3zX$|iXJpZkwGyC_4% z8G~WgEg`>j+73^=-(fn}Glx*M`?8*T*Xy_9hbqsvg=zp{f2+yC-zfgLB5;EYUv!XB zV#g}w9%3f-jM(Mj+)LKeGi-APK{LgiGdqjK-Yqsh1gHfXN5pMQ70?r%zZK0gYA92hi*3+~M~O?7|%$6?@D>}ypFj~%+*QV3@bfE?Md7 z@(k~v6?5oEAc^{Q<%NR-5s*l9JW4Bx^BAL=+w!`A^F1BqEz9mVA~z@3{)7gyl^N14 z_2@G9IQwl3l&8`jYL^zk!+KD9FsZ6A@p_E85zh=#;f*Ni`53^v%yP4Ob^7Ry=bI78 zA?k@T6ZP)_mDO|2N)%2E(TIs0yqvO-cx`F+M5u@3z;j$VJ2Fr*&lWf=?MvYJixe!g z2JAAB7Vwzz>z5q}1XrL`wcx)#rr+9OksDmtVfJN_yF#7dS81*}@OElqr1!r~eD@El zGjLbja8MH2eD=%sz2ULH!mZE=Q|XX+kcA@QHE{W;AFABN=Oh}$_NuuvDlo=Nr=AC& zEu~s@Y}Taeu(W=~FkTyVWr^$*;J(!&4aTnOhSaM~BYm(HC)EPtBFfie9zhk$z3VIV z5dddH(aN!?TfwacY5CI(#!^7Hx%UAwuhCqlk5~npXQyw9|*^Dfrej5h&h1LjZ^*^veuV5bkDo z-8=>0e^%lbaT+7zi$(uuUbTj^;5fDBQ?274=z4KrA*Bt}&e-}QCxe2jYUj+fxVluO zVQJG!3+*IS!zof|-B6Bk;UCKY4io zDjP?4X=-6T2r|wM2BUtXzVa*#VscYBr0V!K7?c%M z5Kt5F@CPD9ZpiK2kAv>IUE<9iUlJGr=>TtV77%@A~-)ISz(e=3x(zd|Xp40;BUdJ^2<{pFQ z1U|)oJbP)7?W?N!j7&&0C=U)H)2eoT21S?vkZn$6Kb|iTiDiC^V?;u|Jvx7&kJpp& zYe2hsJQETtb!z`^s54dX1F@o|r64s~iqZ$5seb~uO)C*uLa)yMO(YsPPJYA@aGZe% zR{kU?X7p`P$>{UGBMT2-Pyh8d^!YX)k(qsyW{4a5S~74Ms71|lh(*ch|2Kx!DsqQm zEp@{HzF+{USm!vL9HQ_IQ}-68W~C9-nVGXvCp4}M;=LP)pxeFCO#_X=SMgU>sWL27 zJd46Wj9mXs+~ECxtZ&_4Ozf%aL{@!wF$<$du37oD<5n)A1l^K&Oos8<2ToXa*SdZt-nR;uLXZd5zb}^KL9*-{DxpRdQ3bwqYU^*E-U7g6`EHyP)mZe z0+7T!?2+@9)a74oX&yBLi5SW!>fssf!q3G5s+MkE=DT1^Li5z~eI)qM8 zu*==Yhc`2U2Auh}Xhj0?5;G#Fg;xP+FTk(%xc%391dvzTZ_K%6+t0tbC4B*boXF(a z#rP2mX4EIH$I&8iU)SQhntq?rBSlaj=Hb#NVuqZiXP0Qnik299IZL^6*wWE;TT=!y zKb-UU%D4Xi+TH60W6IbuXUA7P{yR8tod@~rD1??;Q7dpPT5Wm>Y{YiM4?QJH^@sig zt(ICg?V&HqgeP47(NTi*x~wSzOWlgErlgJ_EWXdoT^;nP9`_>V#f?W)z&`Ea>|5ER zS_`dPtdrBwKlt{YcYMLA+iBY8sr}}n3rnwsIfE;ok8Wz1J9(`py1SQoz0|FRr76ph zb0bN4pzw}cuIpz0nxrNe>VO25NG_uyc_iq)HZ0^6#BJ;?yQ)=tpI9^gE12^ zi3hTps1|wSL1%P(OKy+6Zo{k(>$z&8>`Er0T;u1CYU2Vn+O-y=zl!H--zk^JVeL9w zK=c=9B77$9G*xO6CRV&6qS_Q=xq~-jeKvz+zJDwVWdsp%#h~D*m`1KJlpFVz!_xQH zb~LZs)eVJf&~2QsM{Z;+6#Uq?XD(c4YN^Nu=xN~tY=O&MueZPWOBwChR` zN5Gsfm2hmqEab5dJ`W?DXfZNro;KVITc`AztW)OIO_+@|%d1$=Qu8Q!Rvxu!>5kP0 z)?sQhq`3Qd9(}bmWx!-axx34Jlv}2&+t?`H$oHgr1Q0upceB3^`NsIf;*J%~W3j7N=gld;IIOWQN(O!)3bwFpm-m@9^Ws@%?jhBF-N6#(+{i-Ef>LEZ%hHad=8b@O-4=J+b}ch&z> zQ9|@SSZdl`%J3(T#h8E?W$SHC3vmY@*euaD1WZim0Hzj_*h5`DFBjwj%CU~DiPNk+ zX<%zLe^$GGb!w%S_oa6oAw`d9u8LNlT8v-9eCmAo6Yi6XWOoF71GueOXA$ zfzNv6et84NKh+2T5#%2DrQYJBI=$exoJaapxnS|Br@?;kr}jWf`lK@EgSV^(Cn(!K zL%OsHwgS=)oo&!X`{f>JKx5+-#ZNPHp9gpV z_rkCvR~QeI9jSuACTX5gK9U!&Im)N;jptfcb~xB z(D?^JX*P&FDz;%WItsle+8guF#g}Q6zvv~;3RH*d1|m6C^@lVPAAW!jMUG4+{pOnX zp23`(XM)aNSVzIqhP}dg8u2Ro0k_I3L<_m+q>Hz37qO;^5`408NGdgun=5{QzA?x! ze=$i9bZ0Xxf_@l4KtWNJvbw7k=Bs)a(B5B$>LS)&h;|Vd1Ksi$19Dc(Hr$%}_1@KV zr`9*HXZE`4_3CT9@D+IP8aZR>wy<9QY-HuSDm8V)Bl3U!Qym8Kpn|j%_xRsZ^J3dA z5L%E*8V+=XGpfCG90Ps3)EMdcACL46GLNU6bE5LgUirQi+le2nOJc*EO z@AyZ@U!>$(XO3}KM!Oq?r3M8!22>-5}o{l)eVFVK$<^*~k|)k;zg* zV^I^9^AXuyCRFy!mJ3(dujQX-bk}(+$Dx6$K$y=cbmn-7g858YroufV)uv?!-7w|X znY6O;V$A$z_={$7@&mn6d*yh0x=cb3*uHW;GJn@UlV6oKpe|LP_RoJ@eDC0{-)2q$ zEXp}YFwcL-RnQv%AtHm}YxtR-q#2gX)`RX`UDW|b2~}~$&e6@oA1|Knm=`mV5i!~q zVt^5dnxxbLXp9Q7v$+zeYXtQFUV+mCLMRM(q zv)W4J!l*0o2GLDzSJFwbGxPC5JI;rkz}U8#wt6%}!{Ork=ttU{he)<%aMgwL*X57p z%>dX-Pm{t!3c1Q=j@c_V)NgXfG%T_j2g0a^Duln zw1-aj@BG+iQa-M7ndBf+GNt89bbnR6iMz_^TBT@PNw2Uw0nT?c6%ki+x*MY*SS4tHYnYBxNR7TOrZl8-DH)h(3Yzzh$ zcQP{yF#n#i;(qim0PS7WL4PX8eMG$tu7AHkr|rw?&)L>c;)9yGyEhIKZ5yv$D7r#AR8suN6UAw2aHmv3v?F}$ zy5nT>FzSnzG^`K_*ShvW7lnDz-|PV?)YC>hott9o;NqS_U@@`A#IjW_NvlEFD8Q{& z#9h<2P~XkZ$lTYj;6$U+A$A8Xs!bDV6JVKR6Q)z3tEr+7C2ievJ9oWpCt(^ z?AyJmKA33kGnhqw^#RUA<*X>a`l-Y|-wReR@ypMz0848lK<2h`R0M z!JDoRFw|P6(b;%*l~goL`}0ynUIA@g!F^Yk9Tl*~%qWi5mlPqy>5?$*3@dHY_!6cN zkIy3BT?3U7*G72m{+><6rImj^tbwD^Ur67%(1SM(uZS48x>`I_|9H*}4o_dbogM5I z#dGIFe*f@##`S2-B~wF{YFBTN4*D!_!SJVIMwlz*z0l|m_QY+=f4h=P+-=)yG6Mkm zDuIDfu{T9lb)3HI97J9gY-v?{0YqIbaR*tiF;hGy6_Nv(%T()GX6c+M2gi)}HWf7M z%P#_a^a!YwNCx~)O&#&;BVRSeeub=IP>8`q7wnfrp4r3dd&YTB)Fs>4r7#xpo@=X6 z_Ie8|j9oLq>7PGlbDYVzW`&IK*H`!(|5lOy`-2=7eJy#>`k*l}cD32+*s;F-7`4T4mXZ1k;Sgf2Leub%1D&5+`%OO66awDUE%5- z?~HN48PvyL??~_FDl6_8ToltRj_wwAt3p52s#bqX$pFG!Mu>t!%yw_qO=tM-*Z^Cn zMD7H24ie_#z&o@5$1-r+o(uM znQV|3_2BPWHUD{=$bwU^eCnBF2^YjlE*z`_Fgq#lE31lTO?HFgx&tcdj!L9MqH|i= zy9+$6t`r%#nAG>J2fFf({QJwF{fj|tviZ$t{Lxuu_@Tc5?NZr)hmyYwIRA6zME(C9 zHPE{Mzu>6dKa$8WO2rEy2dTy7915-PFh0+yfup)zL_C<49a4s^S%&Y=li=G403FhTs z!2TIiArU{Z)KnW5SUsID_4t|2y^(EY=W=5vGN$v|pvCmb&M-1Ln_zMR0<7>g_zZD|z)<&Kt<0%HN=`5B+7o>WFWILlJW% z@;88!9(T~I!Jp>eFN9fdZ_vEXgFgWK(*CW#visbiNW~=xd7Ksmw9^3N=Z|e-8N*?s z2+LXM(H(tIn@Or;k?yqsw|1Jf*Eyfg8@k|TdnDar#$CmR_K15{XT6v0pU^?GDm{}b zoLA`E(-M?}7!PifJC=9xh#0YN@Y+G!{6B1c*(dck?-;Xypw<|0FnD3(bA({V5dsKG zK5w~v^0Ki;m=Xk!7e>Z-3!|phcNyekAlOv>UFZ2Q$L*k43}6ZE?q}z+)l}QkPJKZS z&=e2o%Z4QcwP#BuczGy_NvmQ3{gAsL_e9towqT5$K{z5)gK$yrcppJAnOq+Ioj_$1 zW!EKS>|YO!C+J7E6No@^Y>*@DmU80AS@cA}`w46&Z(>^rm%&?1xD>0G`zSoE+64@h zkiag+T`?b2jy?di-&V1M7`vP%vA@Db-3vl^5?<%Z3W3{HwlP(xa%~6QE-(v3sq7t? z?E$;7cl$dQ>m_5+lLkVq+)3v#?e`LU!W&VkB^?i6`LrqMAFXhtpay*30Lja)eW?n=3BYw> z^px}KC^JQR*iE$k!)qc}r^2oCmM8WzH=8a$C0NsKyF#s!ht`^UutRR2f+gsQOIsn` zQN8dJH3%&#cvuC}JsEiV2-yWzSW4CEpgD#t=jSgNiQwgGpK9c9Xq?2zb2fEb&7 z<;nn;T_-jKep0@KZl>c_7kR%zSmJ>4OymAq!$PL5*}AP|y6A(doryJ|N$LG#{kO1P zeNhz%E;6a1xntg6o#s&F-w2@ON(p#oYE#`iBiu|Tj2`CX0Zl~2w+NiEql4|&`M!kU z&qp($+3zof^=vJ0T=8=5-`C>fI&MblSa!|69F?STV0qApvuMW7_-X907&1)k9w5MDf{Jzs@sw|!fb)MN9OU&GOblqD1N$aW^?c*l}>2>6OnPsaXf|iP%V?&Ep z1R$hQmRYp4Zt>`#77#s;k6k9FY0Dm&6xlAoG&Y9;u4ZAXqlo}bU+RRRa2c8!RSc*p zX7OIMHMP|@Yh#&C3v1Tf@vfe}k{K|uWg^$T5=Ut7G)1db4?*tf3HRVp$C)5?^mzN<@~}sX9fhedna%7y=-U}%A9NH3u*%9I1J9W_-s|c z?p#m4OzJ3YR=XLGnvAt}e;=$npsB@{l#+MQY^Y=L~kZ|KP-# z0%~9xzswSnr55N>>oyKM!a_8Ac})9XTby&+o4|75QA*ue=mI}1Snh8cy}UV=fyCvfD(fZK&Qcrwf4v=QMJTDiG5zkTGeorY8Lm`Dqq3e zI|Y|Cm0;*>11hFrhE%o`hIb_g}csBM3k(++m?!QEkwOs`2~EhP3)*A;TqG@ zGFdgKh*O=aTj>ZcB8l?{0T01dX_amaiT5)zjXZBh_(B(}`l5%6Gvc&+@F;1bkjGqg zw-O3y4*6v+J`Nsr>R%Vn1QcSd8P)`#oPcf@>?OMniq?FnP_cL_ZAqe0h%nfkliMF& z_Nv}N>o)l#Dt1;oxISMw9T4}ZSzI_Q&U>Ef`tyy;j8dH=_zup&rgRlwOLK?i&a}GA))U+$o6#Z>{TifSH`4e!A#05Z97xoG<0xV zNkz*f%qGF#-J^=aQX$E#KcvhhTz}3zEzrKcf~m_;Q!a{WRl5bKjXE{-^;bJ2^4?5_ zA}72~oN0!ki4(~j>#UM^XBO)>Cp1Cr4n;!?G);(X4Q-?x%OtbZWVHk8w&A4V>Qyxz zM3SHiVD-kEnfrJb1Y-p-(u7j|rSD|b3O;WI)(c3rTtA$^ou)es0f6E&z= z8FuZf7(r=}8e-9zgY+L6oTz>uyJx=wgx9msa0Bh!g_0!+&JAdl^O=S5l5)i5VI*|! zm>r*@Pa$)8_R%C*Vy9QRzP6eNxNqWgrRHoaw=Ld-8KS{X+*4sT!~H z4lZ56AaCREMhnf|+8GISB;n?fl(tD)7XDQtxRfo@PWWx^P7XdR3O*OsklFwv9W9v` zdyco;l%94e4RTAwH$3rf#rQX|y;a>3RX}sesYq0}B5kv50P8AkOpnfZC6G~H5JsO_ zMBa;sO`h56FJAYY2;3awiQ5G|#!GgxwTgiu1T)e7gGJet@ zwX!TY@VLbSvLtqadxrq{pg%Iah-Q)CO9XQgi2XD{g|dfl_wJfLpbS{%AIa!zFaKZ$ zxIgRtI8PV^UyOet!Mjuxe}ZTg>SEcT{|oOzkmD%0V0ARj5^c+Vve{|T{7U3_5=F{Z zzrC}9vhsVum*gI9^&M7u0D~OM;<*qHv7J^ zRwgc<>kN@;RXbx8z1;8~Q`YU7-Gja~r|7Be){JjhEa0CU5DX5VWF=Yw#S`awz-pGI z0zsr%a;b(qYiYHXW|dnVysMHSzRF%MH)hIH&=f(ybfV8e*$eD9wDu`ZJI|e2wjO!= zbpVf)_V`}Br-9S_q#!f^$#Q&wRVumt0ML<3!7YtCpJA`h=;-U{C&93#Eu@a*EQ$lu zDRZg0eCC>1S1f@XBOu`rD_oN)NEx~tqASO4I;Z1MUm<#s!PG!bcMsH?FHcmXe_SWf zFK~0r7!@3B8tfUhU>%MW!c&g*94t5H%ff9#$3nAN+YN|8!H0OZTo~MuI1zk;&;~ow z{fQIt9HSi4qOvGVat;^o?7S7brF*!Xv+gyVSe4=BL9Lv2o`F*auT1yOZ&}NFx@=(0 zmH9sirCiyOby#sN$`Azp80+sf?oZEsHY%`zzC^smzcG*ORe7kzm&QK2k7~C2um}vJ zI^VV1z;UMro--)D|Bp4U(y@`?OBb_=dTjufn-Y46e?FI7n2^dKODA74hdu#wR@Q*~ zW54vf&RS}2sh~isRkeIs`kXYZ&wAv8c)G-D+1Qj>t1Ghk9yiEHzf;=wUv3Ck2l(RI z=Cc5$$pGras&7y$F2onM=GevixZ;;A)%5jb<;lyQx;w{*n5Aj@Iy3ysj_NEa!VmNq zyf&a#ysbjQ4<*bxG`)INrXQ3BoD#sKl)qQ`O?D)`;7N61sl*-a2xhm>(*RxEfoxXjV=iE+NN=Ynp!^eyFTA|A0;moeck(0J1GQ}w`hgjG)4?O!gATPV9;DS2v# zr#Q*T3&UqSQ|UuNdzA}Ut!ds)pkTyKq-U-Q?i)dFp@8)41|um=csW^MbEV^Ev%8u| z?3D9)Pq37%OhDpp5l{IW4f5su;G697{?@R6NiGiPOs-H;3#Kn#M9?FP(cbz;*_O@b zXpH8fhO#4Z`Pd#~JHl?3M?wpK_ASewQql|)Q!prYewb}v>gD2fAFb6aE6{X9ifwaV ztRu!_C+21Ei$PMEG z;U|2~72#Je3uZi|lOI_*Oh8!@Gh{b9wDbsC22ntHIj}n+e=KJd-m>!i_lL^NGiYc1bT}DN)`e99>e~e;ftfL;xv?Z83hYR>Fvkro0YS#JqPO7 zW6Llr7_gpISx)z|?GJ-<#oB=ZCUcR6J+WgTrJ}VF6JV- zS&s)Rb0BOl?&I}^C?R13?LIU9drTXGE{^xyz8lTCYH2N5>05?MZ@;AAxxsqtABb+X20>lTFjiB;fcyTJkg=m_*^{%s(Kt z{_u&H^EfNRj!;7RJ{6yRSfx6m11M|cKAo}U!^xs>>CDV~f3Itm!Wp`z92zFk(@u2? zAv^_qt>jFi*EHU9Z*BjOPu*2!?%9IUvOA|!)ds}@Dku`AbSr?+X4{qMAK*yeu%d41 zSZy|#p(Bb{0{B$(Xq-J32A*#TZa+4Qk4qT;ewx-9s}T#GE%U2p9&=dS^7qSRT}?GwT;g|r-Y6bClo zOIPqu5VqkznQv=WK5&mxQ-i*@Vf+~KQHorxD&^@y7rS8w4w|A4NRr+sKbXvfr0-pY zJ~Wgb-FjrQWu&xqxQP1pJKwhk|71QgiF~H)`~qz-6MrwBPTe5b8$p(~oyaT8cvF(h zZ2OG8@tdiPWbE0$vKvoA5?8%?OaX`X9#2p~(j};8P+nEQ?Z9``a&qNxCJ^o8 zn)2_pusMb|I)2hB~rNJ!<5b7NqW?D9=_sag^lmL%48abB70W;DY#W)#TbKFB9^=zQJ@^83@STdsos%v#En z3s*>(zxKnASj~$^N}d90yZ~xGjLiCUZy{0(HbomPjnfJ1@!x(5$V?`uR@J3}BL~VJSc&9Ek7QZlt zq&gYxzV=tP68`Dfj?;;h8;?Q;hx~baK5@Kspi))-#+C|Qg#r{GMYrjhuWcVxxxBKl zVPCNT=QOQ*Yon(RF=Fjs!AO^4@3Ne=(NLT_*ji8^=Y_xw5M}FIRGH{^m2iGZ{%Azb z@af7^RvOVcdBZLJ#$!Wu^Q-p4L7Y^ZSj0(R-Kkn3mBzoq|9)|OT-1HfB@3|KR&w?V`}X?1Je2AwHNImF`Ms(Jx8MkQQbIG6mzefNpgs6FsSFI= zkUqn=)1T`~S1lxZYPYHya#3RECM#4buY-&D4hRpdTue0(o1K}Y1RJjJ3o=p+KMC?P zz8!VQ3Hpg=;Sb0ifkWdDlNqExqQWmYM+GxCRjVI%om$`ud&-_Wwv>FP%rd?-elq^3 zycddJF1E`hjlN#Lqw12-AeYC>3OZ3k(jm#FBzcU4BmmD2G``KNG84FTQU z6<>MFt>#|61F8$a3@~*W`IVu}0EAxWOdIUNiB-38&=Wh~sEQaMiDEm-NGGkJ7o$e4 za;Biq>UC4}#P7J640pmjNoj#hN3`~+#w2&Gjskj~L5h;^p$~8v`fJnia(cWAC9C%F z3y9>73on35H2)%900{OzwK)RO3$QDeS0a=?#-^5)PUo;_pijM>?fBw8S0adGl2*pdAuGVI4HXh6BUMR-YDLxwX{GQb zmZt1Fcl^jA`4vTmg>^~Y{E+ZgPEcpoHvux@U#X}Uq+7b_SP11;E-q;Fc31}Q8-xru z6l;#|IHXv)r{LJdyP@%{<8On2BPWBVM&5QTkwOUS;?zw-o{|7H4#hwX<%R7M|!7Mi$S^lCHLV7?(W03czkY|xNeRS zxMFR5)jpAK2k&Ql_Y+QXR|2jNTa3}$3IBS*JsV`QarQ>InqJW3;LoM1xGw_<_hQCy zy6NzDhd6ckyWCoVSz=T?pDXbt!=4JzM6;W8a@2%^GOO!uS#zlcQ?#;;cY3R}JlS9z zCDXnj81YfghS3aDv?XGC#Z!OI_uBX;FL)RA*(>_KG*ps3)gKGIg*%<>7e)54bQ_Ek-(uipVtu$b)X`>GOIY!B&8vMH$u_=pFC_4Pt zuBz~cRw2KOT$-BxWleL`QVUeZUpx%PZudyasvBWj-bQH~@!wz37oSp7c7Rlv=OHF* z)C0bL3OcUOVe9R`hT(+dx^yNeaAnREC1V20Gs%{+g`V4zDWSj8*I&u=U~(GhKtrRZ zq|^iG>qO&KfH@jx84+A<_$T0mn!&6i$(gEJJ*_JN$$GJHBpRwe!%lnBi2~G_!0kJ) zqv9T-p#UVA60&nI37Zty$-4LenAK6}1^pnCQO6T+-ohi}I)J0B4R3qfxa$eew|}t2 zced1RSPxP-&a0(h@g6BZ;msQG1*>*544?+p93zOZ)ehI0+v*99-(2HVG*f1I_VLFN zu}1N)w4~a?ZiVN#Rkx9g;|EI#CU!`HiWSceftl}{QF1w{q=KF(n#)S#necuZwIm5J z3iR_cGZ1;9`VKUC9RoNe-Nj~}wC1*~evBkcoV@?1TM|9{3e-aCZeU=3@XA_ZCGCL5 zdFr@qJzzr*-saVUjnVz;^kC)y<&@KFDnx*Hgmf<0ovQi_Nft+jK%#jz)W}(XnM$FF zUUtORhD(#Gih9&*NUnbKX=cgZK$ViQ%k6R#Yq_$qn{S!p_1z#9S#mRc)?n;6J&@m` z7BzIKUKbmdXWm%%%{l4^`U49kpj#x(tvw{j^j8G04-mT?PyMuIIW}Y^>tdp)%07E; z@OM?U$uQxcxvc(XT6BX8H$Nlh#Cu9iE58_@OjzRy#0OkV*-y5OP6QC~e%&BH0u7DM z5HpPic0=i$jxDbYVmltQhINX2c($0rO{Pc%c1KyUw36(^s$OvGT(r|2yI_IJBzH){ ztv=9T`CM*!Mm67t1#6M)d`Ll@Xrs1L?dl0-FR-1A{R4MM9Yv`Q;S2Rf;}K;E^btW! zS7KRS+LyrYnu}-*N|%fEds#lSVFR;nN}_mF%0hG}yhs?0<_+0^uF!!Wjd%*hXh@Z> zrU(TnR9aP5xpZJplTuS$1n0`xA&Apj7+at(>fn2L9lz+x8)Z4O&B{&3%m|E+< zrvF(=#UuESI?eSEG@<@whP~{*Eps?XLu|14Q3%@RRmhD?GI;HPOoKcf&IUc{w%cot z*pPWmit5p?ZZa(+7Uv&<_kGKQ^&@IDMjhnzLu8B{Gd736Gb3P(wWNAjSDL#SLvy&p zECH~SjvF2_LqKdh4~XxE(wLuKyupN#uE^YD{7Jv79NmAhiSkTmc{djSgBA)%h)A26 zB7AIoRL<``B>?W5nPZnPu8clw6^WW>eK2;bpet_eP8FQN&0%8m>?4Qw2+i%k0;f;m zeCpgR2x6|L{F%CMHk)~dCVS{OZQZncW6#Ww?kcT0QsR@4@QK(fEEFEUZ_s^U)fjNg z3yA3Ya;UJwmmo^tJA|sp=&KdP0Y>bBj3BMhR7nwqGJEAfWzhGXe?d_5@-U_Pq3)U@^*E7+cd}R$64g5%&gy{2{+UO zRouvYF=?ELiSqVOQK-2gvuL8i;V~Z*toqHF_gd_+XGplk?BwaO-N8BuZ?7NEc>1*q2Kx=@JT{GAhu$i-PpGQTaC9~|m z+0vd5RLW|v3PgqKQyxJz)*GoEW?4Rk9Tu2LiBvQ&B$y&P!S@Kept4HR^k+Jx`M`0IV)AkvJ8wT=w$Fc^nR$rWP1Si#cU4x?~`9NyK1(aEtOVi;lM2X|$f3QeU--=yvv`WtU| zd;L30vmFH23zRVyHT(b%gYX*tX5H$7O2%HN4-p3d86Vl}+Z`&D{=5?QZ8rbpgC3wq z{%?fy|6Vrf-fxf>ZuVi(2YB;}JWA8xv$f`9uNa}L$6TYo0e2VRvLgSMTsNoaJ1kO_ z0gaPF}2(S5|RzZ>OjS(t(>msS2@nFqTXOUFB)p)D**;A ze^O(BiNFTrQfwSOd!ey7ezCc8&jHzY-MKQGy`L^dod*ngyL|dm5IW^2iw2Zcb*&ul z-a>6XQK(~nw8`Ty%HAIlnHxPTF?cia2S(MLmlt{dc$g{`^#;iVq?Qr0qYBiTxNxIa z0K6|Btds^gS$ED(_^jaVRfp$Oj^qeOuaC3}t5O@)RVmB1zRcmpbWj}KN89~SY17C# zJ$^x+^}Cj?Y~SglqV#jY=Ifs~r403j@(ll}_d@;%{O7uLi%A|4)0L4>A@>d5^!N}A z*u{Nb5$bjR=S-GxoI&2FkGM`baNb2xp}d>_^)UMfUp#^WE!aM(i*DIpMIZG1Qy_;N z*aMiXl>cYf{Lj_`O8kF<6#Coe2me3Ry=PRD>H0ou8D+)-Dk?e%SVmD15RndvZd7a# z5EZEr5s)q=Kp-J1Dk2Ihy+$l_5-ACUlBg&xK#)!XL`o7N1SBDW^z$+^`}f~#_V289 zKAiL6WbuXdLh|N$pSwKwecjjf-O|~g+Y>ajm^#1a)ac2WJ9{oX*L8LL&y@y5He6jG zu8|7&qF^;QUH@^(t7r9{?awufg9}RkbCeF?_D-)GQe0J@`nz{s$^Y>fr&hp_I>rCg z{QxF6?_PM{oVfz;`P-gXy@&%?aW5VHr)&p+M|HXK&yNGR)xW>G_)mF|IE9(KAtAE< zrxcp)CMu>Bhpf_3`scq+|2#GNNaN(XX$hZMPr8`4_AdPV|MEsxuvbl1rN{k0y73i7 znQTQ#u5^(_0%rF6mwG~+K-YBd%^m#bm*D{9^gmTdL5lUbx%W3+OWanCUiv3JFCeKg z_{5=PY{2;E9O>lX(4r`vE*%?-=FD@xof-C3AC-ETg)39vesG6AnAww<-guHeZBBcV&RpE zzK!5zSb;Y7YGe7g6CVXi0vt30DaU;nQNZ)DC?Ay=ZnT)Mi}F(;@|9_I!dtQmBb0~y z=QsHWI%<*5i3Uq>NZHrBj{m(qR)(DkLbh(T$<2syF$u339s_nnX#<1dKVngM??g#= zo{3ypOa)64q#_Ma_4Cu2-Xxi`xD$)=K!p;oGF!n^dP=*5Y^5>0nu6kc3-}uHPcghn zD3wJN>6^i3feMn7?teW8^&;hUY^o@VHwrz-#ykS3_4UVNJm})Kd5#fD94nmw*f&N& z9)UavCzdWxKvvyK8%g41lTL{1Nz&Ij&E>lHn=k#>w)tgdstGC~ngB$mzyoxu-t~H3dZ=((+3wmn1hbv+D{9`O%1~pd*&h?Py>+=Xv^nJxZ^0t_iDX!_Z+X zk{p-YAH8lS2DV_BZbO!iYs5G$WS&y%zVTn1JHxvVNqSBx5`V&0MP1Z8Tmw5b`A=(H+0+{_QYl!r#I$v0yA?+{ zzXhKC{c;OgexsZ%cL$?ry)ie@a_=fAQJ#c<1JB%{D{YUF#nGuM=+ZLQ+a+0rWcMlB z|Mtdbb=Ps9uDqq?YWz>Wm@XS5ig6mLu_2i(*)UO<3?7r}8!`lnTgMZVUF8OQ;#8b61usw@2GKw->=yv8^N|m}#IEHJ; zCYIVNSJkEeFMl4`d!Iyp5HPN#YpX(+T_O7juR8wkrmnCG_fPk;;{DUF{?k+e)Nbv& zf7+(yYsHTT|6g~xFd!$;m7e3?GF#Pm>3`)9-;B6(iTmLH_Hw)qM!!n&l%J2}N8oAG z|BT8&3{qUxedRxb%Kta9Zbf`!Woqny=8}Px`jd_MKcmf`>`g0lGApmW|G&NwDj*Yw zl_SHyciXMd1eCw=yk=Drxax=he@+Ghe-!caE;}~N=2P9((N*2U|CxMM`@PoL5$X5* zzbw9cph zHY_kVuY;@~?8gYtg^$CdeP>1o4%qGdaZHAWAlePhh1CHg)%}3HNy+WqCxK0s+bQsl zpy`4|MW2&Uk|e>fTx6npm)<>f;)qzFQB4wyHK-ZE$;m?a_8(F5*m)`-NYL)%RO42n ze~gxiV42O|4sfYlT(}+MPZ?uX#%zX?Wc;AnQmD8NP&v>ibTIrseYUFdSnz|2&;9%B z{J`jBpBl|)ETw``Gk71tRJ?=#`~f7{$6|d~4UDXL{g?R?pMckM(`_=LZ#`4i;y<5R zg?B%xpV|!{9TFRw2|o|dx{Q2S@Oc2cw!*f3?$hzc7%`2WwiYJ4vbT8PBMlP6?e))$ ziuEJLXwUnM0>?iqdNfA?yj)*a=B5#|oZl!MsHuECI**czk8#klX>>Aq=fcf2fRkyy zy$>6O3#XaZ1re8|8n?yz{}vMHo2;zmd+sSpR<}pvcya1gW)G^Psx52>-^nL6XGePN zkM`W_Vh(_ioT-i8mAsdkFyRWQYV!VxbuH%BpWf?AIq6l48|P8Bb|A{rYmnQpJHuCF z)KdpToW^?Vfbr<^8e(p$_*)VxCPGd$~>O5xy}@=s-5W#zPiZ^+5}Ggtqf>9kc^ z1Ish~tN8V%qXqN(P35#8oQ0JQE!512+H+y;tbHe3y-0kOm!KLaoFD)*?E&~(@sWZk6K(m#5>PEr4i_r6Y zLB5&ohq>i8m8#HJH(j0n?6}@HwOeJ41xv=2c1)|~*Z(pDXh5S=zjb_xTAP8}Ynz?~ z%sKw~sw*J_eH4v)-wbW=V#wEWU9cfMnz4zXwVu{v$qJB*d z3M9vDBSdG-LV&?#^%yLlIKPz^Bica&@p@I*PpW_IOQlv!od58Q`e8Eu3Q*)?4QS(D zN}~+l(|H+c#t~nxgReSK%9p4`nu$Z1$OI0a85Nkdl#^>xJ$8;?-xRf^>eHKst=heG z5<5AG#s@>tID0eoHvHS-+;NWKeARnk>F=-Ad~R-7JATg>Z(ZC^ZV zC+TpmG{@M}ADn-djAknon78)#nGzdsaMdmL7a~0$~jRNnH>8c*sr4|5e?~O*^ zSh^}OQM`QwI<4C%x%kx^>%d(Di}K3jhSAPjD9uk5_qMIdpb7+L^J(bd0!q`lXLx{( zom#h9mCX2)K&?NITo{riEC_$vjs)cTTB$d*ZCO>@D7h$;Otcjna6AQN9N+Z7^l93- zXENx9dnzZU3VFFB!mG@!#Lc@=4fI=-NDpFt7c zU3rulRU>!b1oI2)sFE9w(l4T?NKMMzh`r9>6XMICktn-3zZL^^X0mhF1$D-w|^i2>?UE^s&~vq7LAMZk{Ct)VKnpre5dL z40IzNyX-8YRSf~>=EP~kw1tkw7i(ZGg21Zh*%!k>L509jOx)l)%2#_;H;^9$qUc@a z8t~RAuLha$;PpgH9Ibs)cc}f7H98J!=0~xJ3KqCNqNP)b`ViWW*SRayCEnp3IKN8D zhQ3C?^(2nsjjEUV|l=S9Jjl`c{2iwWyib>6?uyyf^COcl(^^fzuazws8(Fe!yzGf#)i zqXboGZ-&GqX1vtt_1rvaxvA`#=tL@-rH@4zI@@r*iTV!7XW2Z^2Ix4k%<&rA$oV$6cHQRxpA^p*^V>-Vtc-)7IC4%I|gP2iqWytd% zs^3m*=xC(vS>$;x_GnvQYo=`{H7JES+c#F1iQ3jiDJX)VZhwmqZK?AQM8YFGhy3w_ z4X)UsV$GIyr;EMpUfZ0;sa5g3TRiXA{n!n=sI{Sw<7{RoPLc#S&Er&~MP+y|-zK13 zrF)sY#z%KJKeux=(W@Tf;tx%7XV110c;RNZ*U1#rk^RRH^yl>TPx(t#p-^->o@Od@46DwQ#V# z-;eB*weEd7<;KRr>D4L#M&zK4VyF zgd2c8KDw*W6NyT|)=)1u_2J=JZX5E1vsUXG7rEaZ$GDg@;#N9p!+O!>)9Zv`$9%FI zTy7X%R!;4mB_HQ}>&YykV@`IkHW=o&*QVjGO+fsg}+iAfL6$DeYQm=kRM*24N{ zI+XfyKU`B9dpR0@eEWJ))Z(7o2UNOBiL&m3JIUtNCu?heY(l@Es$fi*lD z{Mu&qfyU2_GpqktXMIxN2Hl7`UUW!Q<}hTaUFaPBnDJ%pr6{amCN)c#N*%l5_Om&C zGnC4#b-B~aEO#V)Kwm*>;sg#yR}~_Z2H-Ek$pf4nXx=+&(Z_VREca<2qSXDT4!|i5 zNmvW*Z^z?~=)kA-bC_B8WK>4BWZ~zX#|{)D-Do-BVOL zroyjLa#cEAM_tK8ngN(v_y919e*Zg6(2ADH-ikvk%#X5HHCZ6SBDI2@=`-CP#P6w zI;K+MHm(g?Dyz2C1xo55{qiaM*utS!=d7^^fOaG~Tsqqr&g?#_8kt(?8eo*@;)q{Y@X-rQ{5aPNQc;+BDtc3d{6XFR z&Y2M98m_bLU8&BWJE$mO?b##nPruroI-|Spym_bh5#h6wslToDdbOeG%l*2m$MtsV zmO39e>RR)oPiq7OWq~D4Nltx}ea*heWMJ2r>uVpxDJtFZy*_C~N09M>G`qoPbx|TR zd8Uy1TKYb$acNbaAX?H<=R`Zapw`9!`;>!x7u3rFCckg?J}jL5Nl54Pwe^Ne9`V9O-Mxl$ zE2J%gh3tUkl30LO!_mymmmIIp;tX}A8k%Qsjm+H)l{VxaeeYAy49FaD`r_3Ld0h#o zaq1CWRfUigDyrm&d@=K_ps%k!du@clFEblNZ=%HA{iBhg=pLd)kT(U@yna&y*m1q} zUw$p&>hYu3gI?R1-!PEBJm`4uP1@cB>YH?h&@?ddsJN`I;j+Pot|N!JDT70B_DJQ;U);L*~1 zCAyO|ndIB#Fy*IV5x?3Fp}OOpeYef0j}5ZBeE*JEJ&_04(VuHNu5>W;?QX6K+PKg{{5RK(0Tg?sr=Oqot*C7lo8p|caK>^D>WbD;n zUfvp0Ul%yhP);$=mM-p1TchQE^Focz)hxFKK{LW)gYEkV_CUBoGot0uD1=Z8dMT~+ z;I4x>L3-rR%h;=EwgKQVL!vAKwwK%UC8KothdtXj@$9eA(rOy{DLd$xH$)$R*a4P( z33ue$&cD9ii}1~TxqztI?eFF43Q8K*1DjXnTCb$K0PBWk7W{qsg9E`Ij_$C%ux{q$ z+e^9@O4e16U*%SbH*xvZ-{9aG8~|e4DXfTk z5LCQ)ZqXX>4Wra6*qSj>_;vKay!4&<`;}mcSAQ?$-29u5Rl8uh3L`J3!jXHLh5wjGG?L?VElY ze&jCmv~HtJh?Wnl=aE`Lq8|?053Zu&j4DQDyAtoIhzz&AwEF$=Mrwr}MD^?^lg91U z&v{p_w46O!?iA%LF33BT>*XbywQM|@d{tVq%Bw-0f6cex@59Glpeecb9vSGJoTUL; zMok5s8TMO8wvB$ayPvt7_L#epm2G_WV<01V>_>%im8fL&s6G6wr|)<`y8D51n&c<= z@}g&2N3#c^0oNd^7Pf`oeU6eE!@}rApbPH@sSbZ_o!W+fON!kA%NHT?oOLrL7Aj?F zyYJpPw(ipye5ui&{*Y4lcV^tZ?eBg@f`Hc{$aOq^K1{XAh%UMgu!55g=I2?bIVp0i zCav;dUe@iz;D?TVcB z3K#eP&#O7h@~S1xj}b0Q?kqFGm*AYpO5;3$KEOEKeBqDaV=ak`6Rq3S?6w`m>_7N4 zq4tUDuUq!6()qq#XU%Vc`@g!noP8=LKB+&v(AIVv(r_p-_qL=x_(Tlc-hOv9_vrk! zuh)V^&&-n4`g+UIXAx!rg|4sft5?sjX&&!62BA!)<6N&5(FBdib2c<5M&#qJs1u4%XV;!_P`*2CBfMT`vb@iKXoNTOy1U&llN(um6`AtLrZ9NdxV*j*)?={4gAM= z3-z}gr;MI;D$oJ*pE#>hn&9yXX-giWfDq2=)cSm^=$H!HXBcw!LR+coyD{@8`4Ouf7~gCm;AYI zGd%KkPNbcEI$Jk42F|mfNCJ6KKs(k5^&?Z>Ey-ueYMAwIGAWZpnwj=p)&a-NaOt|9 z^8Qjep^iwV?1{R{a{!a_>jDe}HBmH`odoaP&)cUl0(~$`WR8v&7>K?%!jr0=P=j+P z#{8jfDg!-lSf}Aw6TVcm%TH)>Gt=bnjYEENKl%{6PN%YFEZm_UnHOJ|3^5DE{LEPT z96dCS_L-|=$(T(V(nwhJX88Uf9I*O`E@xB%;)NGFP95F=JOrZbqgw$=m}-{woCU4j z-|@!R)A^mYZdId&orAi@47b2J5Kzb(&<+V)8qauzz2P<`nXz<<=H(RiE43KJnRH(c z_PA~~_a&Rw{K@xe=z}Pisi;@tgnjl?q2+pZ5qlM9!!jpE_V_9Q*t@|Ga4;lwaPA1z zDINDLx;4c|KIG_7lN=(S+)Z;RlYg`;Jc*jCw7OoVdSd>|Aki==M&6O07u1h0ZjT!*Lg*ZU|h>@Vs71sw4D|Oj7DbP65C@>QOoTo z%CJ`_Upi^l`!BCIkG%+OoX_`a>B<%ibv;@FwCVd5-(dN1)v^4~&Q-0i`}S+p2?jv+ zPUOWU*s$R7$`X{|IA^J1bK`<3t+Ya$q*?*2KKM=G4Nk3cP@t+p>5;DZvm#2q?9Iuu zph2oR!(_|l;6R;_(eC@$hH%KYfx0r!5zB(h;^;itC{s5FS2(U}zN*Hk)_@niCuL&HXMJWo_tiWxR>mT?0j@bu&K zka5q>WC)mU8a-Mt96sUxub<;q=sToLWj37gvAl*pYA%+LIUp4%S7vl3^Y$NOH<;I) zRSg4uK&T#{&PF9fIedXJD9AwkVJ==Uf;Y@QEzLfVDD%%=&-gBvl<#%hyoJ^}xS*Lw z^(W0x4Re20emwK;4Cpt@8-jR;Ew(`v($9+t`~~-i^q&)!n*3A9sY*M6W?l>iDP1L= zlDv#7QW7L_Q^jBWeifPt*6~vZ`N&$;l33$&0O?vDZmp~<*p@;V6QUVIZUqqJ*)hv% z{h9a^Eiws-Q>o?6w{b$KJ_~S#-N1XkU}3?szFFfbL;V8R)K(SY)UK_%!lVFXi`s&< zA@v|_n1mf&*eCAO&Fgg}{qWT{cxecG96g7;^L6tb5Xh5O5YRu6$}$W=hDdC{ypeDu zXZh|io+))B=P`-A=&K<(V!5dNolkm~+w@|6Ks>4VK+g|$^c`JU^L*N=$fiA5_=&YNU%a-KU!1(tM z;`=d5L*c{Ms+NUjLFBqbvw&3mH(0=DbCxCDpfo(^0P&95tb>*n)Px7qwr##))JJU2 zQO?)Wa2-s(QoZl<>seiqs2+n*6O6eS(fR18YLVAM5lEC2?-U6Bec`RtKd zExN5)tBC4NuQ(cHLD{|>CrvjEHKsZcE_`d?7jFD75ZCt3#| z69X)FUeFh95WPJd1fp0%Nr@Q2l{~N|E07o-Yq+fi)j`PN^E;6oW44*P(CPdzk5x;o zeo0<8aS)M%^++;<^e&W;sra5a5I5d+9P1!HvXbc1q->HPa}=8{{kT;V$IiSv zA(zk1czcPZ{3Z}h^1aJ}hFk`=i=Z%2N~#ezb0O+TCtmCz$t?F;N_WmlS?7J!lC516Rh?{myTMR4%h zm?K6s`N(^yA8~1c?rYaf5)t@SNYvs8n`VBWU z)d4yfm;j1HTrsME45SBkX~|iBmhp&kCJ-*7h+}67twzFmttN_wXxea|VellzKtAV6 zH%M6ALcS>vaV;8MTKWo}x7aSi9B~Z*ZE@h#CYspTyQn$$v$M0%6>Wb|Ld^xa+&!TOy^ML0vl4j5t#PBjU*NmTk$dLRdCU zXLQW13|lWFbLbWHwMxiF=|{-#g*h?d+2;_i4}*V%*rDbgrPDs`8Ov822Ws0=rL)2b z8+FGRVd@}F)tCEK)-aRINnyNqaLYy(Q9n#SFr+xliH%O1qrukx(jc9 zQBc7zG_@ekuYqj^zoRH*s+k4(^LE1-udMAzT)=vP1%wxC2w)IEtbnx^FE$A1kv1Qkq+O}!4 zx!1(JD5lU*cGy0k$6`yKYLW}dM2-w7*bdiJb5@xqXC(>W(yxMUuW<{hQ(Hkufjy3c*V zveH1+S&(jN1Wp~cD(46QFV%jn9&|UbJN=AAV(r`^evFy4vACk^I#>YjQdo4YnlQHa z-s6q0lFagSaY*%+^*LzCoaGmjt2PsR(G{B#9m-UFp&nhoEi&G+qh39q{vVg+$l zGU;OINyWA(`#_J^4-A{>dN)ruAc!Tnz zxTcKo7cjM@e2;5SL-&e~Cn_)!?;mb&ySFE}J4x?uvnW4Ign|4h-3ZFw;!y3%-OdW2 zz`!*W;qn1ftR;o5rsmY%9z=uSciaZ3d-RXb7FPbb7o7|n8nMcKgQ6F6BSI& zM6-H+W0G`WmY?%bcpyo_9*()WKj|d5?ii#`+S;6*J&&BI$)1Oc=rzf{Tfxb<{pAb2 zIsuxRr+1^cMfNm)-@b8Q_?qAo!vf79u^CXj{9f-SganL+^{cvvUxjJJ}v+@hs~}deoEXlzhLP z)BDm+L!B`jJxLbZ@1jOq`hwA& z&LZ`fU)3A~cSTn|yMKDfI|BLh@Sp-Whv%Bids^9LS08`tRFdXlVLcu~k2C8N zOeeGVCfbd4Dezq=@2-KqeU57@S~`K0B?cg#Fq-@~HEhl=-;}D2O~}sYX1tQ;kci>e zCYm&r_uv8yJYA;1sASAB`{L`Q>iAl{-M;#}jn;XN9YKNegv+l7@MKtDP~v1>w1$7s zu3IML7R@aVZbpZcb_bkDPf*>#-D0$1)q9 z`Q-8P57opPcM)>W(BeJs9=mnQ@3INGZgtC{f#*I@TwPo9RBTus^;q+w%6m93o){Vz zqf?j;W}V5_X21G5l}z&U`~&PdUGd!SiY#E{ynfBvna7}gX;pdwFJ$rfbFy|*3|}bB zGTjb_b7stJ@uYmNc8_BFB#3z&(Wo)t)Y#bViSxm zC!ewrnX&4q=1gQsLSfrys7sC5=Xn-bGBsb8ajIPzT4bGqlp{$)5+&ydrOFcoLCc(+ zF>H8bbM(JGo#kxlyF!hX>*3zhwr&1?Fi2HD&kQk+xtwgENy0@68gyTY~!TRpQ` zE3qF_<3wnWo_r=d#CrZgFMk^{Y$fXN$KGFVhwU32&RAv<;+`%8VgHLOUU-F5d#;fw zZ70^8oQJu0@JaKbp;LPJ*pNlsG!=xaNNLDj(b?Y|JU)#zcRoGPkP1QKRz zc8;NNTBgH3hPtcM%K{=XKL69UnJm*nw^q9CFxQlO4gcE<((-GYWx()mA!ST=YkDxY z-$BQ|cxRsK^CEh_Ve+?#FV7tglOb-7Dr6kq6>|V(qA1Jr{+?9Jtt-Duo1szRsdrYgjOY04OZR(GcGV~HL%z^KvYclPRZW8KLpTfC}Fr?-P~(=7HR zq2q~hoap0~J+U_ql~W0BAnTO=M6-o$4g{T^iTMha<&QeA{zQGC>D&=A=3SKIIl5N^ z>r_`po*tMvq?(*`qN^uEwgxG6bpVM4M;Q0P>-#1`75N4QDBa0JfRXj%x3-jG=VM0w zzF)v#Lc=brP9fg7Ad(4^Hy7b^`=R#YiTRS`GJljMLzSjj$*oN?xqfg$Gq*uUX;nY> z_#rg`U}60Jb>iUie3|eNs6|h}p$&K&_=Ky)Epyk1M0|{B!xw0t5)v@3D#)>9qn;-W zOeo=!1O3^Fxhv4dmlUGkn&tv)7bFlC`|^s!nU+bu=$N(27cbxZYIvw3a`Qn*s$TKe zWOt;p%@-+BN$w?fYWY}tK)$%SUZFqphX|97LJ7ilCp&#nNsf5uYO^TqpERn55EfE? zhp-ruDp1}%5AHEYMt_Pj>>awM3KR4uu_0#Z5~=15Vy|1f<*tbi*eZF$Aya;8 z3n}6cIh2EaGl#?8UozpD5?y-x)BCxf>RJ_OqC^FjsSguLUM^xGM)XBai5CzcR?Hue zH3QsStfL7w|9aA&_SP0XrIeB?^OK(?^ixsO3>+}+U$i`qO<+r zWoij73OHoD;0j)?{l#Ga%tjyZ%?(Vy6)63@QCt!rbtMvM3g*oYDF9yU@1y_S z3W~=1&%4v6jR4x{sE zhXWq~QX`O8QbQVfuQYTpC=d#M@%43N0QxL=1W1Q{Tb>*@iCM8ZrK4%nJR`O*?V`UX z3i0cE^&3W4@ePTH)PVI<&nKJc4>f!p9J^4R-T$^YVonj;Pv22{-cv|E{!ZBkR<6a) zB$3VIDnX}Q;G-GlDL|$fbKscK?<2;Q&x3T&4V`Gbp(3u(b}N4sY(DHZ;8>ROLZuVGZ$M+n~%TSFQM`@=Vq#rO^d5Ftv7ScQ^ zJ_}1jy!%`rMGcYuf=^}KG?~}G+Ep3&BFRB}^u$;WB}-5EV}tV0xr_!(2hA4cKxTD= z-kWD+uoLCvs5R3JS>v;8&YvcypseYc{Vzq8*4_-m)_Yr&VS2SY+Cx=@SyUC2L+)*Y zqTW@|OziQkAi?ydxQ25vdO&3R6x$rL*eJj~HL z%e7@6q>s;MyYhED0i%_GGxG5D_PROK^h=gVeUU)Pm4Dr@*L|!yzH}atJkF;Vk&`c@ z7(CVN`IrSTUfI1N@nzfF{NFSW5oD6DN*#w}pCPD;+8yraV+&_*ZF^>}_w6e_l5H9qw5YEIFL zAzi`a;IxhgAY8@V`LX9+C(k!nKt_gYTMOpv2ZFo)!+@Nb*`AMfY8!9WdbK8WqnxN<_PT{=wH=EMKmNK5hkVr@X!Cwg{x@ppNu%~t0 z!?lZSEAMk)DV+Ep+xf{Ul(^pu`Wn1?YVFmALz;Jk6PG+fw`xQ8rRe715{|NOnG7&C z1`s-u{TOehQwH2iHw108b|p0v^LBHl)WbC~mVve4Mw;2R#MSP>pS-Gx1PY3L)wTYf z#K~#ma`#gwH!_!3_uBvq<`{sc#(ikO;32z8VySu)*$zVE7uOxVH=R0X(!MA(!!P`v zwmfisGq&q6bhY7%y$H}r_BW%=_nlhK4^QPc8hUj6+g|bpUw=)0V;OL0GLjYkv{Eu< z(BgpSP))h%lY1eq^y}Du?O+DXgKN9r-01VHDqV5PxFRoTndxz*K!v^kx%pNn%N$;H zW@SuwL`OX}pa^%m=lf&rG;(+o-7|}@XW@6M3k2B?`f#SfM2ZQ`z3Z1|D3yX2;)Wbek6=JV7n9_D=`5 zF2FVAX{|#mh9U_A4$0?`sV#3Q8HVR&=i@$U%$NjeQCz3|e4!Fcq(ZyE=qgcf@c9+^ zNXwqmZ42p)4)lFL_^xCdI^So^qmxxgHToHc_bk0obzLFFLd4CFK^8S`LFFiUbh*m9 zTAnp9@B=WN^v5EvlwhSc{=kw+;8=AWNOzO_EjFc)9#7`}p`3hM67EE34Eh{Dctg=T zZ7UekskcusS?KRKxbQ`d-R68bazPb-`8%VK+#Y}O3tMN{i1ez`3#SazFrIiPDE>^; z9NU>&L@rvAJni=X@mDQ$4&f-irxK5`b zjmK}G2Ol|sZm0-%nX2cor}WbN7E+g*4>tR>YACxeg#F&^QWfIH6JX3)rmE2#Pqg03 znP@w(_h0aK`SP>Kpsz6ZA0kM$?x3kWJ&1l8)6L>9D}+VW?Z18T^UoPxp8wH=*ly zN#1Vot`52<{L^L!gslg-XSe9C%Hx@PAq&>?LpG8rT~w`RxjiYe%Re*xW6~d1!{kL_ zHmssUud^lRa zyk6nn#JR&<$mmwXK|5_Fp`>IVXrfK{gv-pnwFL@Cgl5*>zXhJr#P_0I7sd#c=d+JA zg}S^|^nIe$Vm0(*a`(;gh9pVF^AGUv75+vNSA13Fo=wcDs6%7ROat)}P&1k6Wc_-l zW>K%g1rLbV|JgjK&}lcN2#<$#mZ-6uXznJ0Pqrq6)rm3buua<-lLiII{+WW4DJuz} zElRI|q{TDwLjNvW;8K;@V$qVG)cU#^arRQ99jYQx01LH}D&bDAzc`ti{>6G|q`+t6 zlz!TUbaSiSk0&(cgiW*QV2uz7mtgXj$(17&)c})Y2~@`?JGbI^zj$DT1oJ2CUR&3n z@IMC4SiyZTx*@lN{Rt$9Z}O%!0jI8G2%D$)PjdgakoccTf<^XT#1|FX+r}x-tqua2 zPS6fTaACi?vO>(O2|sNgbsE-WbV%WW4CfZ z85!F+6<=v?XO#m~Zc!C2XVf4}&E=Rq~2B&vKQ=IDaD*ws{ z`+7Vp6Oj8|WttjOnsFO(hz~kky)g5lwLJ*|vqoV-E|y@3F{T(U?KVgNus2`$Pi3WL zEvIrB+(d#Pq@g(0r3({Xl<#+uzOV^5X&RPOP^@BF#cX}x<>M$ zsqWXL9A2PPB2aD3xsGzmClA&NXSdXi4=8hU~Zl`VhlpjQNyE0Py~wN!nw{F|zV z{{aNFzGU1I*`>ls%yz+VR*RS^_4i;Xu%9h8ru#6mFD&9Rw@J@_v5L+=MvyQT#v{{P z8eAOmXsy#{@@~C&74rbLXJg{#7K1^%%s8s>T2l=C-YZ^SSi7#?i=`a5GP37{B+>MF zR8qJ_P;!i3z1Wff33)C~^6I*Ez<6R7Em@vne&X@(kLV zy64iN`8S(9sDl}yw+?G~<``H?zuKZ|T5%PRKm9UOCb_iYBG z@ia%J&tEI1Y3HDAH;O6SGPdOC%ghe|)?0xyvbE!w-GS zsJ`{;D8zW7PYv55d8;H9`H!MWKgYsdXRS<89Q}Dn>%9wy^N0zahsufKNAlVUT>6vc z_ymWWcv8B<<(mubmk({p{6J>=F@XRJ`}vj~T;se`-=NoNC;L5jb6cXcdxmI%*PgW-Q|*@E*SX zGjSmi{j^=UDAl8W-!qa-B7B?inQ%%y@h6h?y&Lv^AZ`;!|50P=T*jmxbF8)?C}i`Z z5fXWlcDM$x`0*Tj!-LdOkdi}5o zrnYeF(EuQsBtJCT(Kmn{Fjt0&sh=WsG%nw|l{je2EaX+2n=2IR&7V=@cm1+)a_hp3 zmzw#`+@XG1XT0f!tn=l`{jyl4&xl!*$%)Xc zsWuC1O6c9+J?5TQ`Gt>k513(T>2| z;|B;9ePY%8EQkb*)0D^$G*-&Qlt}adb`&%o`&R+>vS{MuJe5ostO*}azX2JnD{R53AB)1e$>G#18IcNZO+$%}T^J~RUwDHG4sWEJK`Vc@I^4j_P6 zJrLIwG#4XI^oWBwiG#Mde1MS6y|Y-Smu zqVa(al6lZb4*K)6ijX&RO`QwrNZc^NeYW&G#5bIH^+;*hmZJXvlsQ(#n*yvky;_Ip z?ett48%s2w;xfl0)Pr%n^QeHZUatz?ghB(IMwT2kM^p2lz2o&_6lHO#6z4-e8_GYMLt`5Sj+O=>$IR7# zJXjef7SOQYgF!vP4>ur9B>(eI_~ZV*D2&8i|h4QHc6mBJM!4MMbMgQT(y zabkZ=+K>9j5IbmTC;E>IexBBvGhJ>GV^7;^tTdv1fk-vy3+b=G@3tDQ@d2bBmnp&^QBhI$@zv`01U}T z4wy-L=SE%RTf+AVatW;+vrbTQegdMzKojXg2M{|VnS@ViE=>N3IB#bkEU zR4f+ysycfq#36(bZ$@MM8>AQgFuP~$hptfop;mh%iVmYlI}OYNGXf?Xy3k4VNM3g5 z*K~h$!2AhF{W12-6X;0pwASZYYfscUl#HNk<7fx*xI1$AQUGncFLGgjduT*Cud(K? z_FFaP{qD*#+;Q^lOmTlD_*g&2Ao?x%Ejp`Eej`9_zBEK9*7FWp;gTyBCoQLIgwjvL zuk#$=U~LMDu>DNPt9Q7{0%cgO{C=FnrV}G~C$&2%G0QA(TIP$%X%jctZ!))p3w3DG zv`2O+T6jHlux=@hbT^vEr7anFs{r19JeAq$;O6HR$rX6@Ua&H1$q_S{6-Jr1(O`(j z=h=Kjs;8JrZplu#5eR)QbO~h?(rJQTmO(@+w*B^-IZjQMgB!d=$Yu6sH6_x9MnsZ` z>o$hln4c2D<;Yf*EWXOWHK};HMkhS)A{uZY(x+lD+ zzq2VQCe)#^VaYkN9w>@yrBea|={scOAs9T}m;Uu6n{6}X{r{;_=q;ui+=(rUI(Lb5xacfSs z#z?bsfRk5SAKfHYzIt=poHN8FH-^QL{!$hT7<0!VVLDJY(HA>1g ziheLxZI&IB60G;z61A~26UCOoG#b9M=OGt>yui*!QUv0Me=kUKR{k^D|Tl#V-c5p~a{w1nP=vSi5n!XYO2Q zl$)O=gHA8MCQNhx7zmV!&WhhWi+)SG{rYeh^w4xjRGDtC_lbD`f&!T8MRC;6l37Zt zl3WA8(XwV(MKwba+FRt?rGdkkK~uW8M>YV_4vmT9UaudReJ*>3FtMwNIA$p}zcCY4 z&Iyvnz;iMA=0I-q>LkekU9>hi1n1zw4I6c2cR9b+{82fo><$FVGrVhVYlV4ndGhN# zo2=LWGW)IOR;6FMl53B7#-WDhhsDCFeu_cR7;+3lYNHs@1_2zhM09CF-W=fFiN|F; zEVUl%G_xDDS+*D)NuL=nO==iv5K?nHkIX-sM!lIEVoqRzAS>?o&W;YOtv2C2vGf`b z4}D{!fL%dw*6NM-`Bn05B<^`604x^NzNE$U<2tJ-q!EO4p->PRa05{ViW`sE5|W{_>8?GV z7?fde)ARke%`yt(oHp|F8A6e6dsoO97JKPaLDj&Awgh&`}=Gg%`#5H2Kh7!upNYBzHmeC=i;m)*;x_&zK z6EopvB$8Fkf$|(mr9`-1Bq>*^5#IyQW2Cd8qkL$dcC5qy#on98L%IKd?-RZA&E%aWRD4nG1*NFr<5g(%Dzq{Dl*owjhP68VU*nrCd-&I1~bDjhH+my z%lCVK?)&%q>;B{S_&x6bJzUpZ@7MdaKA+E*FgxX%vw652zjYESSaXs~J1q+DhRy?t zClt9kAE(59Gg0eO18Hmc=uMc?@msiRjr1x*-%E~95iM*MGaG(QLgZLu zRSXVPwu2b-+wSz?eYA6dD{bEx(^X{w+PjW6uD?*{HhhX$u6ja~NM+&{a{$EU1+b9` zZ^TGYmU=FR$1^b+_f;Zx29s|nC#h%?VyF9poKEJJc^iWxn*BF&KecP}PAv>_(<_YJ zPf=~v+$Rc*x^IIUhd!6xb3qUS-X&i+WlNnt3cGn}s9Q?lxk zlBXK@->#(gXWu4f+3Z^%Z9unbqaIaNBojnjFvsbIn;Nm@I!7qsfee{;YW040mu zs7jR5otllDJ4P*hVd!VNK6X;`z+PWj$?(ZTK_VYp4Ncs;qS+rss{#Ucmp$^c`WgSJ z<&{3(@)LK>DNQF@1Sl6y5Bt-H+_(%dV`5nk1J>A&%8N(g|}4zxR8n1 z6xlN03;}N8Pmy$7+MYdg$JzQe^__d+05gH>w6}r1cc_i}N(3(Bej6Iuxx5ftRiUZJMED6! z7rBTDA!dx`OL0u{VZ^VLI<2edc^mryhj8Sl0+bh>=QEHVlOL>CWO$|}AlxPV`=DK9 z8;%$73A9Q5ynQV-xC#*wO34U|7$&dQ?{wq<^Rq5tYbHkX8hGX%Hrf+1=iTke=#cwf zWkfq3=?l~MCDzgUFGrU2HvS=K%9!W(9hf^06n0ffg!qJAgla8!Ex-?wp zGFl&2)iaCu*~6$99Bs?MIv{bTi8d2SDm<10X?(ruHM_oAB zd(}`LnCwsE+^XNcbj9JA4>Z zOm^`thv+jaxmOlGzOo-Ht%{VDh;4|cUBi8$k32Ym!XGp=<6WAbQ)@D+`!mNgWo077 zD&~2ywA}ap*#LfKvD2UI6e>vGc`gA@Xa%Rec$l~e2?JIfZ@B9pbr}fR7*8i0(`~X@ zTpwE>(qAllTV;0jV&2%413BC~=aB{y>Qk(uar>q~*2y5s_ zFWiOQET{lgZ+#AeC{M*xd3$G+=%><^9$?$kYD_oW4M&YoREa-qM$6)A(Q=uq4WG`> z&3>S~r$m$X%$1@SzcV~{Co|;S?TG3$jjsU~%DoQ0b4`m4x6cfqhlUOsqF;kbzN?KU z`Ok*`8Oo`@MRd5RORU^4)0@wI?$=ao)P+hqkL$t;ciox|dn>J&WQn;pV!$un7U|o8xR9x+pSXlM~FoZmpGMKwpdecEs72QKB}) zS^ezE$icHmQof_P&L%GlE4Xuj!CtM8Rhw_%q1&_*s0CqLwUd{bN(%k3;hN+JNsd}! z4fDlV&z zd1Eo8`@Wm)+SzBknz4u8V!ROUgsKOk(Raz_vt)-nK zLF{XO4P?pLVnw(An)w;I7SXp(Ddcm zQcb8`dOE?a1!??ehZs#&SVH2)f+Y$dZEzAP8Jg}8_W9hEam6HE!wS_Y$u0%B+sDMX z9XNw*Sa@7|aS9t3E>!Ei^jObxhguqZt}y(iB0l1KqLX&LE&x~2-DMRzv)t?#7r0xF zdQINN*4%b@APi^c`Sm2;{~3rrohN*z?)$>}uCuZF+#cwO6oU5)_CWZ`7T-7MP(CC{)O6&Y))1t=*t7C)_gLjjq9jTp(>C!=t}SZ>NUCz<^Ch- z9Absr0kOfb04|`ZhO;UmA7m=_SAYUl z6gdtK=4;rpTb>*M8&{GVXS|FPl;21@WI{;B^T{Ud$?`RwO6*idIXWp}2`*_uIOR~7 zV?sC~e{A}Q6-rX<*Hebnndz2RFKV5gh z-MK5^9Xh;b^E;lXB|W)Ef7#CM+Yv(#hXL{Sa14p3#MdQX+;5Xfuiy68w)`Gb8M&qV ztbOY(%pM|)N-ZTO-&6{(;IXxFgJVsutESHSdsa!c!A{@s^$e5F>>dUTXumxC)UbS? z;ODBxni;Q+A9jxBuB6K(Kihj{0P$XTOHi9t8$w%@vT-(|tVR)hU<;8!V1Y|y09b&p zFOgQ$UUgg=9i4FiEPAtL#GPYS(#h%0@DEPC`?LutDDV5RuC_!FeEO!&6O|;(yqpr5 zDrHzN^n{hI;nc~z5rrXI_#i2{3}S!WckW4fA+h0a+vh1We$k!K05Y1ps2ntVBQc1j z{hg(M!u`lVe+DQmdTyV{Gfm?QoBdo4Mx#qJ?Ve>l;um%<<8i>F^6JyLpYnP;H50e} zdHxlRwaYRGkQ@X2`u%%bny7DoTt`)k+|iZmx3{nF5uyF?=-yuCj{C7#y}g-2TK)UJf)9J4@dh%42K+E7pAX^GA+_mCDgfVzf{?+kJZ{F~3Mg z`K(yX0Lm?8|BW9FrDWTQ1ofi_Z44?t1as1czsM1Ku6*r`)ttb}QJ*a~O#b@ALDP-b zHOkwx@kE&AOX^1A5&)K0?e|0}68^H$yceJDLXbA4@u9Ol3h0Z8FugNhM$=KhX|INR zr~OpIrgV+XvpA1jf7LR4dbRq{X+5KaP-1cyr(u+B9j0hBhB` zQ#)`vcNOr+f0?Ae6N&Q~{xZocEAJ$L9OL@1BUpvialmNYIu)WIxT(LCG}>}i&Yst)s%Qn?oT=-p8xsCdt3R8 z_S-qb?s~13c5d~hVgL~l=3)+{6@+Eho64RP9x~{~5OxzDr*KyC_RJzK;OD~YzUVT4z9lvPp_sbCc;JtJpp55<+%##?0u4(eq&=heXrFS z$8T=)PnEb311gfPeZ)=9xx}BNQrinasVY?k+vHZQV*Kjmmfy6k;eXD=zBZy1$=QUc zk6akFNOl(};Cb>`zySuVcWB2zx>6E5O>nkl$2I}xb9h?=d0zn1QxPR{*evg354@Go z`t!;qM0+rjd8ln)_$yp}1#R$hw%Q2B%}Ac*PjQpNqf$85dGI!{a+K5*7P-mUp*n&N zHxm~+G-H5&7TrLM5@kIbs2tbGY_8}OdVNm1ccfP9!N-Qdv?Lvlt`x&R&#TxL{Y}5< z6fQ_wD7w{HHlPvx#t7E<^|oo)PTsM^*bu95TKy%sKW@ZXmVgz7MnSR|PpQ?EYT6Wc zF&~t+K9^{G+?jXCb<~=o#?dZRhQyuqCe--IRUkF{z_TmvN8r@_>6W91Z0@n1smL>? zvR4lV|7DJjwIga}kV128%BhyCT%A>mq(}8*9<$K?0lQK0r0>&~GslJ{S+n7UhPPrZ zq=51KMEY9h+xTF^KgSqY z++~uQF22gCqPrMafPsid~lkZo9^ZN8?%T?e3IsYHFS}&r=KyscA^AYo6F}*{- zkg@^%4;;R(MiK^G<%leUtqb4){JZG}eMFWm%yzgnlONa$gZ@SbYCJwB^@jx$20ZIk zVu}-4H-Wn$%Hq&|h4+>#3XS`-bC2J0xpatjR=M7|6l(ohA49gIH9zT&Aek^dPr7{-AhqaIh~>D>t?Z+_3%^+{UI_) zHxC!i<{x{ZZ~(bfp{89}kvlxbYIub;_`K_$S|RbIOmdLO&M$|jX0u^E7abcZ9jQue z(pT&1{EWNqE_vi*(4pZu733kYlNCrqF$)I&o9JwAvF?TUC$*7cexAS)W2nUB{ z!SjCDavMt`0LW_hB7>?=uv;}}6!NL3iu0&C)@bolL-_ulu*8H7+cGk}Nm|ZzH_?+YlE)9k z*w$+7866g1Ua{naYh3`#Mw7p1taoRO78FNtRvOUV=MZ#Fp3{}Bj{etQ_8*U! z`vYi3>uLw?>unzpcb8cQqQAM*>{=)k&V$D-<@B1Y9;umDv*^41H>hD=@Qk`o(FLNl zi~7EFbK!D3H(x#e$YoS^*@o0F*E5T)uFo99t>~h0;rmk`f#@)Ln#AvlumL}Syjv^w zt^$;@|32-tfvWw2kJaXxwV`vbG@sTe%JI_)o>op#n{SiJm}4f)<0sTiTMX zE7?z1n-aae`SI+!z@{T<&FAuR6o*%3z(Yx1#U^j*xBx|@ITT2ChOemriWp(cF&W4_ zZy$R!=n6_3{|3&kxMx=d?_-=P8mrl;#NKl;u%R4h4mygJcL+Eb;l6LbTH3{xU{q3q zK(O`O@zc%pOi;PYR6Gb|aebB(fHHdHgc=@9c$a;wej!6b36-I0YcqeHqIH<=49-n$ z&nfX))Qk9XxS@{`ANd9PrVOOqXI)%=X+pen+CCM%82WOkfbO3T=YPR%SiG#({s-UF z*ZkO61?QJ7{3vu&j{2~NYVYJB^4o(b!`UEkUvt7Ri1mT9@yv#V$om#c1*H~lUa>m7 zGf%vqje^jr4}o*v>DB1Q1kUCt-No0%$!@Qb1wU#V;KB7|h22Gj22o8! zjr15oyp2!vn0KfPM<6bRr-A?P^^d5?|Ds0oFZSXe5tz$=|H(kx-yoILzp)_i;{SQa zzabIV#Q%mZ{udnN9|X<6@R7f{W&eeu_~$$R-xvA&3;aI|1^wTn`P+>Cl}`FU@kN3& z!`EKR!I10StTQo)^^8%BiDB@_H>eH(hv>$jf=KWljAboQ|Ax-_$newo&m&sfwhkVy z2_hgCX0)75BO0c9_y=dV^HG68in9;;*n}BT<9FDE`CkXx*G&BT;yB4G&FB}hUZcArYFiDMzjIkg14TYRM_(Izu2D+ZR9*h#kalbuVIl39k_(DMC(e{=? zq$0%@AvT6pZY~=e`KW~iZGu^bxbyl&{_53-HI+|+QGEFW-Du!_F8%dvz0iN|Iq{_Y zT*h4pZ#zxP)_E3pO5j=WW3rfT6w-292}4`cf;AO7MRFUiFxh~-lzR5r_&sIPvhbgk z65Yj}u&e%!&Kc|2VpTT-5eAYyrl}pfISr6{v|dtL;hI&l_=7!f3=1 zcFOr<6%dmNqp+?v*hJe-@Dn3I#OsjXi}bZ@3tfRHucKF*DsamF)8v18&^VUX;kW&O zA7AfZm)dCMR#3*Rwztw^MQ&V_Mz<^1rt;lI0a6k_P%@IR6D%n|bzDvPOavleq=c{qs$DG&GUMTiB#Wo}8K|GqHI|5_k`kIy&?Y1zjcOy|YvL&eOuNEzU{@%qMOTPS)W-k8!DA4o?=APpbw*$KU6tn=z6zhPY`s}PA zM(tU;FZRxFt!6o`SB@0kBz}5C`ky0o;pV<%4~0B`kE)32r#~$RY!u|87-D&Sq}4-pjNye@f4Ii12o5kUi?R5z~Ad^44>eM*V^>9^}|HNnH96( z2&T(f{Aw7Ux{+?Q7IiqX3JJ9$x0#Ncr8_sNb3I#-oyP3*&iCP<&;MM%zx_+K(bv;OZbd`rqDYLDX>LOOiGK zK!lR)YbzQMlG~DbmbrL%Zev71KzD`0jasiDEogm5gTY5=V_;aoH&^ppwS&_4He(8- zgon?RdDlRJH|QH!bE*sIZM?(f2Z(}}@9}5GMk1fMqu%9}#dqpO)Gjc0EnpT<3Kkqvt=9A zXK=U%YM%>dZ~+OslCoj;`nK|34uG8-gw_OvJ`j>`kiW}oi&fhsaJ2J&*D z$YCMJ`=zR&LX_Wr*9rQwuWYMvM2CY)ukf$`(ohEE=OmVA-!));N3AqIJ1d2GS!JBo zv8@fB{op{3gf^vt0iVf~4leMuPj{(*JsK)_ItV@IJ&5)%3#o}xm+!9BlMrgzlTYk9se}QhyYB z)ad3vCkaf6{`@Xbx1BW2ZO^Lp&3#MR0$wE2t|TI;yO3~P>_nfvU7>-$pxg!OkO-Ut z0GJ*CSYjhS4;NF@R}I!W!mj(qZ=Kj5G41;de5caLPlhv^{2p#};2%39%8O>mZc7yM z7b5FqmS?xN0Y~Pc5qsxapy}w_goxEK{%0TFcEBIoMmuUdhm~xiXwrFg9REToBGx}y zVSNZX@&j~q>&ZTi&(;|Vs0dt}t>dL5J9dPhvM@EiL3OW#raPUZkf;?AoL?we+v;d= z0H#T<2K;0tfZ9|h~d#2nG!(vGT#{>Hw6>~pR#jer`%a=NTn1ElUsuIP+aGrQ4nIxV0<>f`>w2H-FErHZa;p@|ZkzK* zw6*3B<_W#&$2qZc#aZ(P%|`vMsSFWgH%;6_dBIuu7i-oH8CFj4?E3u)(VhR~FO{8Q zK^xY;3SAd@$l0=EXe}Jf8_s6&fp{$ha%qCC9z7PPa9@RTAK4$qWi~=7rAN>2bnxSFomHU!iq?z`wq^0x_3K_k=e986|*_ zML;mQAlvkTJ(Q4nY{fQb07JQ#+)6ll3P79D%GhG!l+2yuG7;wb2aW{s0lcsmV5(n& z-QFjleYUoZX0Wj0OQ-#*=P-T#dj#k}{A)DzGJOp2=WJRqhh-UjG zPGP68uH#n);Oo?RDLUx;1HmlBI6KaJNweB%26WZ@NfOkAztDrBq>K0E=guwx9_4GP z(U=r8EnG~P`C{XsDD=_vVb^>30pkxM;Ydiq?}dIqh!R;i0l3Sj0a`v#5*3Kz6SMKz zRDKgR64n4^MYb9{I!uJUFVo^_iB9Q+Os}C_N2veJsqNUctCQ^?XWRl(B%Qw{a^TGE zj;}X-gowI{nh{f742eJ8KEL(8v#*u6cD=XkHsLE+7Yrq<;#jd|3GRh1L#dsW6W z0Qabtk*%xK)j)VVvYRYws+&W^C1e$QfZD!eJPSpL8&(Lg52OQSVvyAI>{z-nkMH zI`z2t=e1ped=$tHqsD4lUtbtz|B{DOObXFGAW4W@(e;T7=iD_zqQaMEE=Z;+e0t0A zN7yM~TM~d7RrDqdWwbmTBg!XXCWzq(RJf@7k1*!ZRT4TRV0V5_%s&h8ogGo7EmX~!TXV_%oZ zJkb<^OE%m}xldydx9&35!N~j8hAUJ(3LN=^A1^{PGZd za_@bHrc`qJ#!i{!{Q9l$`m;W$f*$rsCtjfgGh+q}!vn<}xX__Ua-I7EC?Ot96z!$N zH3Voos{B(^*vp z=o3i)ujQs$?l^$lL4GC1n6fA+Q@2yGQi1Loggc!=SKZZ2G+uGkezK?}=8mH&g<#N} zE_{vH)aqS{ohPk^VG9FuzTK~s#Nx3HCBG*}Z}c*V5-BscXZ7wreg$l?=+!t)F_l&r zO6PTUgkY`D>d*HR<_}_&${ZD}9&5bqF%?7e3mkoVc}f&|24a0C?t?$x9;8g*UBD5l z@mKY6HA~9o6zS{#?Hrs-l49{nHW0@1AX^0=Dayk6TvMsN!bG_EWB`xfcafR#90f?j zOq}S+cqOgpA#p!j@zVhN=b<2jAbTYj7XjFT>MomUT43aBw_)XK$Jm&m;(~7iol7(+H0zwV|vlRVwiZV;PzPs=R z{;6|&@6nsigdM;0XVl-2cSul!>F>@8{8{5Z`w|v?~Ekl^Fb}M37 zY+tNq6t(<&R;&_3n!aOzz7_$Xm|)l036HdG zn7#iBfnAr%iTkO-FuWwkq9WsJ2Sf7w)Xj&=9HUA0-JL=LPc-E$g#;_7o%I%qIkhX- zALspDy$-{yT*YsauK!#b-+a37>xxH3Q_N!3j^R$qA98gJ>D2Tq*uuT^p*PQBfm*1D?5!-pAFZL(gJkSgCjnr2Ff<|87 z(M8^%`;5{qZR6Z)f)P?hoo?Hp_P1Z>G=~-Z+80$c%86g^MRYxHXe!9K{cAPBQQPYL zi(3OK1WlXEw&0Xg^rWSFTnzVEl63Nj^QTvOgzE68qn~-dCGUPAhg4Xx#r(J}+%A6r zrlMyImb`32XheSiLJO=*j*#pwf9Sr~t`?+jjGcIp=3lX8Dh~2EzuKE{a=l4M{5-YR zi`d4bnh-<={1zbI_g$mQ4!yDGMYHi)>qWS($||IwEJw#B8j$`=9R6b#x(u=PMDX2U znDKnYaZ>VLDOAL(X|x5@iE8tiUC` z**dZ8M2BFTf$^0=;fZWthYdNu1H(7aIDm{a(wpnp!+-SgpEi(I%}*aeM~9xmQqSDW zaS=Ks@w=NQ^3hR2Z`8FPx*s?n`c0%2`JgK1%nmag+wcuE-4_;l{g)Tk8%Qf1fYwI= z>$BcpTGD2?=?l43B!XxNe#_OoN|MCV)L{AyAc*rbr$Nfs*_PYmP+KGPY9{@`hOafW zVE;Vfjx>w?tnsbQ)a+`A+vhS<(38Z6$HJ|i6?g-yy<@e9J!dv^R_MK5hu}$ZC$AQ> zE|W&IZH|iUYtZblK$uDLwi+0C5XIWQLS;@xj*#$^Ir7Zp1N!)CO;m|(t;p3`$azW0 zw>`Y+J-hnC0M5om<|k8I-ExO1P=3#F-p|Kyj@-Aa=NsJ+)kel;qZb9I-F4GC8Creb`{0fgy@Hgxj9Nup$>u z@VRXhK8+^upOVuG<&qzbPTZ@Kp5FaQx7RZ++-fSTEtsr|*UswiL4a&dC5;xBBwD^a z`TZ;V%XSY4NckwkmU;CI!^zq$CO!<-1r2FRC=H%OHn^p2u|KdA@7iBn+Nj}o zCNy2Rn{Kw@D)+oV zbyq=X;&1q0uKNz-YRJamo*2#}@P#4FBc@1;X6Eop7?`+lUh$JX2iq^`!ff|4-|Yni zf^=uZde#%gtI?$(PEI1zCbIjCo%wC9D|HQ_m-obh*&hf>4LNFbMfmTQ3HSWRl9!=P z{UfmV-iwn#i7dVjS>KJ_5DjVOT?;c97fk)fLqwBWb1?xhj%|h^yGgu?NsyuHsG?h5m`ED zeh*X34~u&lS9{&ssCc`+(!wtj@#GCT{3@TB9Pkx3RFSC8JUL#52t@#ut>chSQz-wv z!gr*;VDBJS_TGQ=QOR=ZcB{>J-j~_y=CYJh2mBj^9ujPq7d%_fygSxeoI98kJ{K0e z*`tR!7+v0|@I4sM_CJTeLV!_Zc$s4F;h+Qmj3oZ)=iy^J8&LdjgzR3_Mw`bt+<~G% zBRST-)198@kQL}3sleRB<|}|~$}0pIn`U`M5L7?OG4kVC+kzZ+1FguHj<)@<{;M}xm}hqf8s`>I5cI_p(X zSv7#(HphJ@9lYjA4&09IgG|J&Ds*+)kH)4QJ!g1+1P%$9DHFUsJX<$y<4aMa+e7&& zBqx0RD6rlHlhk^;7&Tsm*zQm<{ctW1Sngr-K&W*$@XNW+D1ON>7-FriCpu6xb8CF3 zof(AtaJ%4{RL(vAO?|!I*A?wK$=7A}8b)3nPKV(=Fi3Y~*nVwvlb=X&ho_g-xSCre z4^)#7u`Wh9XN2W(XU(;MwkfVu(0&YcKVoCRGRr8e4bmfw)OmO0B4C!@|4!_*s1H}g z?P0jh55ZrRht9-GQ`moMM%@aTK)C!ET2YYVE)Rp+{-g-Ggax*7v$n~we|2e(B4g1u zPa-GZz`BQ=;5{T@5_D_AGxIr&t%Jdy6Cr7QC46;+>dV5HJGEqxLC`SN;Jg&ZG1QI& zNNHhxrzzBNBlPN z%j^;lFW5+(d(zSyM34x*$Fs)|MKP$zTa~-M%CTSO+)#<+ce!kit2x&fkSrd7*=t zs6DqZMgVX9he4`EmDT4y8L`ejtq@`28zwT-?x&(MT^Lg&zIoYj0ftJbGjW*B2b z8|lp(vA+BzPcnzJ=>V*s$$RS%|IqDc)w!?j+4xE~iovR(4h55C@w9Tnt`?xmSJlA) zLYoJ$oafeH(%``QxV|zTQrmwB*l8kg|KT zzFD&2=ZY5SHQ(s(>;>G^vbcsNx@uTq1NC>n3jx!Hf`3zRF(JLlDN9T(gL6j|7?!}& z2zt8+Q>Tl9q_k>$7IvmNu;f#2b%!IUztz z?WclcFNOz2#)fxEdB5e5*EJ50rw`wKftFZP=1AJGeoRln3#=z5A|jg2;^@&XA;A}G zSObka+HkK}??KzEa6@qWek*hlvo|W@g;qDUJ~E!G+s4DC6_ph`3fCuHwECLP5P!jt zaY9U`&={9rcLNqASB&G_OE4pK{`e5J$L0(TQ6i)1ga>)yL#8g3Lt;TSf$g^g@iosC z<^n{>&jDv;xQ*#eatqt$$kIseA&yGutdHkXJ^V zKr%bi*xHvD>KGGtYwVg!B7G@-PeD4v=Q21W$-UL+%QIVK?ugN@x!qD?7escZqwQL! z0ipjg`iesZ60BH>1S?f`7k(L*i@bkW9PKfqopU^VhLI(sT}=c!jbp0Q0SY;q4)W4| ztcRaB^!1MIs*;UZj?-%DSRG|(^|UP0o-R{Ig2rx(_F;qRdHARP!v?Fp>`$5mU4n(Q zE%#bIOgVW!K}CMDA+L3^CSByxMZ4zpvT2k{oH$gTn>Z~pmC!6GixZW92~U-G?ND$} zXtqU)(RM@?r6d)@x^x=4!Zs6Y&u*7WTnnM8NWoL^E$ju18WYED)o9scY@6B{1fIVf?@ksX4p^#c16v(J zMv>mkpmWg2!EHQobXcL)02JGxMt(40oCzLEUHP5pLj^{;qv9G5XoJH&B!j+Zot&&< z6k^L_J_Gbs?;s;qzlEDxRpOND{%$1>XaL>8UrMU;B7DL8nBIpacwcBW8NaC82;8Wq zmtZou&=~cczuGZ8B1ma?jVuuCK{331!Uy!-3#7l;97Eb>tb!y0tbt~$0c|*it-X1r zu-E(fiT&*J_7xv;-kwmM`i1Z~=t*yo*|=>x9O;^~Hzg5bf%pv^an565(IV-ABVRFO znV;Y7X`Kuarfp`pom6}L>vCPoh2TFweS`*$hzWI9bi+1NSTjpBScCed!<6G? z&xI=?>cUp`W*Ow2R}S+D+}otCM}$z?5g|j3KQA_vLhj_mU6NM`kUnVWeEksbneit_ z;llLv={*(XlUK|53Dq5htIqzL(HlqlukOD$OWj>C&x)*<>q0i-FYh^b{o6EzL5uQJ|I?8YXO*fw~1FsO;R{kvhePCP|FPH zY3T76&9{(pQ3TuQ{JIAA(jZ^<{#KEXU&LoXkdtt_MNGkuqwV#=1Kc!AjW~|x<(wcH z)^MIQ(;TC(Rp*XqlSv~O6w%n;>RPM+fM58dN18KBGbN7l;hE{BjL-xxu@_0#a>Tzx zpBUGG$J09YcI*{v-nf#t(6}`@s(U`$6aSmog^EaNv93dm0vnk>Ji(2B5U(-BPG27| z9hiFkLN4ajWI z5w4d>pIW0sZ1?s)IXzh*_eQz8M#q`u{)C+|Th|e?aafukfFwLQnw-m#{-h(`rRSb$ z_!1&X+?a5xc6#n%SL|*WkmY_PMPo4@YoMO54Fnk7NRD9g@m{suO&QuBNhUtM9WGDe zHB*Cf(>c1y4p2QOd zAC)vIOtme6kCb2rjO+K^%1n}Lazg-MAY!ts#`mEbD`$S+(G45fCVp)Jel?EjJIpG) zzm07Ez4iRf ziCgW-!ZB{@iZjjSu}U+>nF&Aj?f&$qh>*_)YIR?QjmQBO*=iT>wm7q0=YgH zj*%#L`>e1lty9=0bm_{n#LNi&ASf1OQ^k?R-auErZ0%I8~J`Iv!n z#7pBjr?zFX%YtvFy(zIt(2HGzwCn{TOm#@GDG2|&XP&;dT)hawD+S(ZBg|P=VYNLm zNikrb!x+)R<2b?^70$8@+}f1H#Vyl`<5np~*>up+#F2?0z#})W@Bk4hPyUiN|A6ZH z7q*^dgF6VWuV`L zD#4Gaie4Gm%=C8giI}RZ@=T72TzeNeth)8_3aq!KT-2tBr9By1iQGfTsN-L+#ZjW; z_Ec((0b49SrsHee++GImG1s+2#&wl;Q-qPA`VZP^r-|EZt(w=0I%0stuDSc)UYv$R z%lUm`=QT#s+U{5@5AO`W&!Ot6c1R`1xV&t&ZC5@+;%AJ zTD|Y~D#Mu60m06prL8WAv}$y8`Db1%6Y5G$@1-{ISM1Imw6Rv=o>w0`5wC%`b<6LSda*VKa_JCii*mtKcy^E?Q(((5^50*LR z6*K7pD%^NY_4-d+HNnQXF83C|%nn1v{6JS1!~n;8Da2~REz<%iA>GT@aJ}rD7rI^_ zoC!KuBwyPPf~TU2r2p`=)~_wGxAS*OW2FcEws|kkB0F@`-M4nzV&foUUXL}akUDm% zoSRBEao|7td^uu4?)42*C2go-R!E`Ptgfhklc}Q*Bohl@3LReM4wyX0(xnumI4Rz^aqiTe= z+SD4(TyRrAzrDdxI3=EKyFV>cC%*22%fzKHtN)#wjeC?LalYH_v*Ip$$s8q@bH7u+ z3EPy=pc)@vPX7wODla;`EGNX%=xnQa_5i;5da+4X zDPG4vZ^}V1z#Iu*P1Qz1pG-!kLBhafUIvYOkTui!tep24TJ_+Og=$3DAM$5E>ShU7 z0BwdYA-6ku>xt9tJNHsLjEL~hXB~rjND~j;e|xbf8pL7&5UaA?{clPk16>Dri4SOO z5Y$L?`AoeHqSe$)kZ`fLLgsBIh@Pe4lB=@kE-Y zixW*GXVK@i?6%wvzxCQgq$19K11=M3Ztt4MIXZ0FYUX@f9;UoX(0EAwtGqwc1 z2z}Kpq{7tdy8qgxGxSq)#odva`A>viUahY7hES-Bl}?4~uHt6cc)4nAlaf+Y5bvEu z+NfGfI=YWR3~_-^k6e@xc)}H#W!x7E9R-FTy~$mB{AZ>+r~pr2blX48bY3&%Lmct~ zJDG`wk7#8N-5qj!NcyeSk>?i6fQ=T^>fF$^gT<{IEjKM!xOg z%&Qb}>5ICs4=8 z_L4T4Tt5N|#4l~xvYHQkx~Lajs>E?oIuXPMlr%7~op;xw``lCDH#W( zhy;;dGoTcuN$+7qAdpa$7D#|d2~i+m5(tElK2xns0rOPZg}|D`*U%f3w5&9pOPpitPW}H=^b^Unn-pg ziwN6LeN5$PG!C^DyRFjDd7tA?3DUjL3p~ziuI33>6s&IX3M=Y9xtu~Yh`g+On3YzI z8t|qO?h6eI`a3a?x<%!CiRF|@2osMopPe+*%lh6Z#>(><(SA4=Ee$)bDwxsztNB4z z_fJ-H^S*d>9he{2dM5d8=CzO0eK;EanTHx}UY^XV@HTrVFdX}qR5sgf7$382G9T|KSN zRt9gMBZGb;ctjmhz#T)&3a-W(Jfh+y);m$Eu25|)Vk9Za{qP+?sZ?0shBq~uT}9U< zD7^Q9os@)L)&bg(!HU2QJSNM}#6stfR)qUo4Y^q$u=EB?8?B798VY`i9#vW8HT)W| zHr4e`8NR}tc!jVZtIaFZPv9LS6Y;dk#$?mcOi?lQAz`Ui3IA&4&`N1c``w1x+n;o5 z4H-)5P^Z!KrmFU(g})#Qp!&C0%FWjTg{;)$3WGu#%JC05$xtCg&>KnSz-N!VHs;nD zZw2a@7_fA7IxQ6&*adM#s#z-Z@tZ@3VN3f}mq8isTn#D&(bIZlPOj2#74~e!h$l2R zb^48-wQzAGwMFLCaD75pP^E}fwia6m_CksjDJSEOI5CIW&HTI=sWaq^bQ_89zsTs{ zVI-f$)~3%}{_uzxq_S|zIeQB>Oq>K&p_$t5=DG++PV7#hqv7pOIeNOOMWuIgjMLbg7%Tm5U-pky_KR;F_z-k$_Gxp0XSM$H3IDn`X^9k= zE;@fEm>6Bx-}KiHH#ig9)~?TxI*AvCDkL}BwjNbi>LAiix$@uo4cHIc_;6Pt&d+f4 zyH~O^@ii}!I<`m3>+XMa+Gwe|+CHz2&VQFY_&dPno(OFe!>{}JYX0j)Y!S9i?fr5L4e`yi1j<|DkQf!_n|wvF4B^3TviXwpIK zn;ZHruoa9FZqM=?zff&r5bMeP;%Q=SpvCIlNYbhg0EBE&m6HU5b(Vkfc$gNgP;}3C zqpm`(dczbl5RCT#TE_>hBuT-K&(3@6q%oI&^Xf~Dms|Sn=h4<_+#C1a&TYhjxaaa1RcGe=6OIFSW=e8R-6kxPl4 z!&fA9G8KF6y)F#0|aH{wU_lt1n%xJJr9cau-sRo7-_!xs{{6nn2E%vNy zr(0N}&(Pla<9ZqH8h36uB_nq>_E!e6zb2lKB6R|iH>a}HZGTO2ltym@QJGdb=t~Q^ zdNM$tpa00W7Va(V36S^?1=eMc0Zc%5paRLx&j(^`wQwbeFEtnlO|c?VP2}gtQN~sb zU<7Vv2?gOVxFXHdy%@|;Ez)&s7&|RABDLC~0YI_cXLjj11l>wP&rChOn zD<^fyVa0M>dRrJEDXNOi1e#tiPyE}w)Xe37W`5sL=fLCn`t?6gaRDIyx0mhJJB6rKwR2$B3dm5xiM=Vvb%yRj!H{p7;L;85G;~iwV*ebR zRDBN44&3!Sm-p>ky_=Ank3^p+T6M&2t;Q#g&5_?-+WFT}&+bEBJ?6<-d0&+qg-91> z-NgoV){}3`sD80W$Fpxv(UDrG3F^)7qqVEphbO*$<4cdGiiPs;Pa-X2^WyirQ> zk^mdF4$89&FO>~Tc6P;qcBo6wlBF8mq~_d^atMxJ@J=>)C}Z#3WG@FsYzGnO54$p` zztS!l(Rg!NlePjb(1*%yQaNr~ftKCe+f_kqZm0~zhnF{g&4@DK zU@+Gr>Kf2Q3_%+zO@KBx=#(9aK+e@RXRMc>)7e9qUcE+@>!3I(PIjX+j1ZgTXq%O)OZc>4Krt2|m;a6hXCKa%)DljJ-udtor3n~<%#iS79 zrCQ#HlH9HJoU(8rvc8d8qiR+5I4__=;`REa@7CW#KLHUvE=vEX{#)TE=Dbzq?}XJV z%lmd^B`9X7q3(vaNv1mRBqEId`hCOQ!#qSj$1tSmkQ!bU{?;YHC@f9mp8c|KT!Jf!>Noy9Zi)TVoqX(>Cfm0V%~ zlHpE}-F&BXVd3VBmmQSaa(rYhz&iDY_}LHJKBHe7^JqbdCVlgM>m9bxK(&heV@b4x zo|rrKG}(}8p?^Zy%C5)csIZp~;EE23w3u!O6!!!!GLl2$y*FBkEA?k48dhMWQ^H^- zAS<2!R2V0d+ap29`c+n0*gmF(H!y+lb=4*rL~Nqs@ChAZ9;ZGVZS*bFC6x3j2z#bX z6hZQMR!+=kucqpbr)Eu;R`|X0gI($K=76UNCgnLhan8mxC&kF)_?bej8^pxkZ$@RQ zeI>`i`8oesOJ1p+w79Nu=N(Z)=w@ucur_tWx!p)ped=BLDOmbRYWGf-&U}BRr1a|E zsLrJQZKg8thso+Spz779xR4{D&u{WO7edLc@>wDY(UCwCn;P0J5!C9kDIA7Tb1fE^ zg{Q1%Stt6B9qhFzyX5S$*>SJ^+q?bf_A>Bpe|PMBeYbH^v%GhA)D6H7_~@GnS4Tcg zC<{As;H%69_*3_~xv3RsCWgUs5bBeF>9TH91CoaPg0CkP0UU7jNt@jaC+0H1)X}eV zu>w2*qoEo?Pyi>|h~1T94p@y*#|W~JFF+uT@r-(QkF9s&*f2n<3m842x|~;UmRx^v zOo&yNr!aOHddBmjf7yThQ&)sUKr8>{mj#;bm*jYs?hx5Te+{8@4F}t6)7`e-O|wy3 z%ClkVCxvZl4gtSkPxoOi*-TcxXA;@$=JHZ1)lcJ<2-f0{ngJ8Xm_fx+pZPFmIxe>k z=kN0lSYq_6+Pt+i;>qB!;>bUq-9NFc$WT)lJ3sR@+d8qi*7-mFA%T><)?5E5zyJSl zN&jc|R;jE*%x$k+_iM8s@@hZ7TB#RO?Ixc6^j{R<_-IwEC=Z#i7M+R?|4=&|*G78{Ev(UD#&N!VAR{BJV zFT7=HyBOZ6lmxF;s)WB$8sx|Mb^SNTjFCqRUly9qkpsB;gS}Fnlh^9~JLbSzqSM>C zgFkDt%a<-r?^dH>YP&ycQ}A28#CwfycFs-#+nb))O>WUt*GX3vxAWepE*B5>A7YJw zX3`mI^{_IM|GEF}!-$3be9EecIwq0oV^Qz&fZjPp!E&)->y*)JV>8>il*Q@2!|ZEx zrA{U?hsd&mzvo%oI~{iiTlovv`6ZFp*IU89T9j>)3v3(h4|v<1$NuYs(o43CRSh)< ztAgXw8J}BP6v#gcKhWl`XdRMg1w?&G!O7F?vK~k0>7!$1yCC01V$)u zyg?0PF1G@bryDa`$AhPV78lp$QB>Qk+Dak|itmo_=z*Mdg z+94Cq9t8Zxp4Aa0g;gB^SNThgjUW+qoY$S8&nI12M`!A|jgdhCtDr8UF9U(v|KEey9scX0t(z65jiJ%?&_{KsZn()`RB5P}YVl~%SY6g+H%?M#X0ssV1EC!5$s zo)XU#MTYjF5BXj*Walk60ygubu3?`gC^cpNqq9_ivr?!_T^!|oC|0fCOLJD0f9@~j zp(1a|oqjp|qXNLpK7@7zJ^1FApML`7f6`&Y^=Qj0HQJhhUZ8(oAv5CWySE#wt2+}n zN_o2plIN{PSe?c*LOs zerSGmZXPbT$+cG2B9mRS{(J+KIsuwStpzotK8QDK&6VE81zV2vfBD?A*&{eaTqVTHwTwxZ2%p&q`|?v(=0} z3SUxpVW3gwBLN1MO%SwNWA!WNu;+&iEVpI>W7dWQy%k#pUk&FpbdJnJ^K1b`XV3Ps z<99en>DivgFSR~4`)S`fXFGj3S=>)b*z=b|Vr$|2^GV^8nI3X84blc5ryb57+&Quc z#fBKIRYu)&SWVgiTecZYBG8Nl=u74S6qa`;ssGnH&yCA04}ep-0Jb#H0pWjY{$qnl%XFTw4RH~vnTal6cXmE` zZ5io)O}{A0*PD@EzT!N932~V4T6JnHtXlX`TrP8}ayMKPGF>K!$$ytB?^+}i7~kC0!~*rcBkdMk*MJ?Ai;{= z*+J0`YjdrjY?9a)ha_Jy2;B!gnen05Ob-1HveI^|NNt8TY04F&q_AmZjS9vNVVs+C z2%Ua5p>Ab+d}p+l_@vP$>K2OA>*UEgxM?1V!&HUj)NlrQ>0rB`gef&x!p_dKBd`T7 zt*-k$R7J)PuSdQ;J3Fpgc4j%Pvvk`pTV;fJ=M#Op9dZPn#ds<_ z3#(NIBouI(Ro;FIsH(GZ2#~i4_<7S~0RI$8Gc>ky`lKiFoFQ214?{)XBptOH6&mL4 z^vrbPHi-DQPt6ZcHE)||6)%kq4&R^~C7K_f7e;(%B*&yZl@ zy|F@|&h#TBR4~pc*bA@57{k$^G=2QfpKYuJRG)j+N1Lk=4bPu%uYUnVmgu7M%q3+o zra2pTbdyl@Utc7FVIB{_PAur{Gb)_*ChibH5*0*xI|Jc3vr*JwnS_c!Ay1Uf+5uVYuBRKynMR5zNJw06@tcKcHeiEF3I?E8(6BSKJjVvQh zUXzd`+Hj3Yuk#ayLKO)f4oF?jbZjfaRHkbwDlOtVKjhj6h5OU@BwpGv4Ab`12+$mX zH_{8foJHUJ(zNp}H15GY-}=wZe35H0wjufa$&DVBs@3*ty0t<*3 z4q}B?$SfOQZD45KKnpYnqwZJL7>k6s-AR z?WwL8YyQuJr~ z?_9C`k)xtVjQldSb*BGXBpuP_W6D33*lcdO{R}C%RC7?7d=|5vpj5qJ43gRK)!z3= zN5*Pz+wC`U0o%=|Paz?}{G1_Jb$Z8*1IGAifrA~X$lUz00|7B&tG~Vb31i{7`pz;+ zRUez%G=a?PGm2cS8FX52cdFE$uwfXKJ;aNz+?vh`62R0hnb}(-3{IQp=^7&TXcyEt zC)Sx{?Ium#6J$Cyjve&>qZ6IsQJ`b9ws(HOzfJOoJNn-A&`Ov4d)a-Z0Ek^rnC#+hkCIU0aYFD)R zV$HkX{$=lKr=NL`yld%bOrCFt6g3Jm5n4JoR1st#y}1$Vj5PKy3o+Pvwk}x^tXy@| zChkMyk1#nYPE~`=yb|tbfR<|7b>?jrF6FYci7H8pXcv2gx#l}+O!54bCq6oy?gPET zWDU__Lr4@Vs?4Bllj5;McZTcob&LC#nNytay5JguELtU5?H%QYOM4bIWwOXS*cxRP zNdBjgS!?d|%K%bgN5)6u*I^hdE9%v{qxc<6h@)(vu1q%?01+6?wE|J!Y6OC{6kYfC z{YmYH0u2Xfm!sSWKKPJV<&m44p~fCACx`Gxi*4;{pMMiYKLNtJ2Go!8vx5o@NV^H7 zdkWn`1Yg_=jKDS^J0uv8wqr-U+$#N|$HJ#F&de(h9~>F5(qD>r`l{3~#VF9iDNCF8 z({dkdhg-CimDkFsq<99rXo-r$U!>Sy!GrvEM?l^RQ?G^ZyH|u!fR4FD6~}ZC&Wz@+ z{B3dPhfrf?W6QZ0&aKn4{>w9VY5RU9$cvviOMBtW6G%HmkEWP1Y3W1;cANR$b+(L@ z`+E*PNu21D6gJtZrNMYFmpoh`@|st_qoKBaO=7U;(GunX@%yk3yU3+!S0TXw%0%KM z>h$2BCyDB(IPH}iyp#{(Ungf?B!_belVcithpDf#&PVp#m4|0)Ta%jd=r5I_^yQT{ zF!k8B)_qt-TmS~6?h*cjH_#xAoUIk;NeGfrS3ib+_NM(8`BF0%G^DcOz;aSx3GLuR z)Z?g63d#00hH@MY^0jQhB>e*a5jGpBC`Gdff1!FIjFCi7c!=HW*AP^vx||dlY?ypn zp>^mK>anD-7JwdiAz!c@^8HnHUKvNyTlGC4PTj~?@R)UXsTvJxX{^fdY6JlxUP>b+ zDj^3OC+0utX95Tn=RECeV~l>`OU9)6u#C-{vV+d+9+8ZOLe15~%d$hKGj!o%X$yfg zo5o1>-;r3`Mw<~e++#Ygbl2sRCS5i%r?gn>6iiQpg)sIO~mweEH*7#e;+Us(t6u zbu85H%jK-Zeu?xb8fc0j4KW%nHsIb$gr2J;HgWL6J=$MAEbh}&e zQSIwvE)>2e`;XIHuVHEt5?IGPp%SH`3IW?j{%0c4qXe=F);h0@)F7#75XxRM9C^BD z%CYSS*nDidgxyf|?$HaEcRhBLGs)M)|5e+8z$KNC*{v~@DXjvEuMXJf&&;p-YR5?FdHHOD8_O$h z#kK6hT)HnDr{{qdJ0^g`0>29kys?92(8R_B@)P?YD$m!l81_nB101+9HDp%C&+Gc{ z`_JH&DeVG#|NHu@z}`7;;DSRrMhc^?r7~GDg&{WB1s@|P&$QsO$VVFsS%=c0DLTHa zw5?O+hF<8ggQ<0kk2&$U)4eAJ(6>`o&aKcOY6Qb2-Qnh;!x;}8G8ZJSb`Biy*(?}m zylUX~@vp=!pDvebDrB5_ASs$uH_Z<#3P@Q1%MMDGR1kqzY1@{=G7xzJqM5Oz2-vnyPPII zZNFvNORI%`|(H&Fs+=eHEks;ye1y4r)Ak10p0 zpS%PWE7utD1!0XIZ7-O`J4$$j+ewXKzz2{k+4R>DGl~Q{WcfGa1fAfS-qyZR>o##c zuilN|Vn?YZ8W}2tZYNu1uy)3sl|(x@6Nn3z*{bk5)wuruSZX7yghTjH194Y3VjkKt zq5=z3Uhz(eN`S(UK`kW%=q#g`H_F!w^*qC2ltadpKk6}*a;9JQ_;Na_RyyGfM*GIZ zrTGeq?QG^oI&c-h7?uQt<)657MZZ(^!3UtYcJPs})(u{$V(g}ucsnvTD7( zlomScA;t&oU&gMu1ac(Av4SyO#6)TC5$`4SRoe*&F$$o*l;) zhfG2g5KY1^3_eX0BJ*4QSqp!u@jyB)Uc2ekVY{(AuA&fuhR07gSVO6`Qh>a_R5_Dv zEnbr0C7)4NzMH1H3A?0mLf8i)kZ%s0AY~yLtoL>!{5={nzu8q4M$tRA|adAmv|H#HHwM zv$zd=>E&Qw^z-3tT?0znfaz^wd!>lXN|1Da@SHA>QJH;QK6_G@LW%bVe%@7pr-?^O{S8J~N0zHSf*uq&;2F*4bDLV9gDs{*?s z+TvPQRXsBtTtGFko8}7fLo-;pg&Qp**Gp53$fEwWN``OXFw__xK7tUZ#W+X6Sdm=pz7Wr(NWwmJL4;+-T#Pd}PyX zdYA92{}&SAT^_l1&m{i~9JR+?hA{}upxd=U4q6PUXI-xXqgZ-|heh775tk;7RuF7{wDZ*cEw_Ch5W!cv@H*dGRb?_9#D(1B^LEIBz~@JcGXDH~?U$JI%A+Ftoeu4eVFp{{Qy&Byq8U{iKsO=nG~yT~Iap~# zOi%Etz;P32>c(Z55m4T7k^Y5ap#^Cabztp1HLFwdK`{M}4~&nZLNjxDUlk0>8lM-_ z1JYc2F3m>(otR<7evgv1d$<~@NRlkO_It%^M0&#v!~|A+y>9DA2a_JozsNUz*ewyA z{qZ4IuQ3`22|Bd+aL9+`)gpjQwlosF68%PCnY0j7rFpkIdqr!4WN@${x6l`x6{YI5 zVIULE5#ZJN=M{oz3vWRleBTnUSp^%F8kM#SXIDyLodP?q zrPc}Dgi>T~Wo{GL&7~^NQD2367ZNk~u)h^3^s({Ton~QH(p@4p^Th!@;%q;1G+MXR0eNB4Y}pnkrt{<8W(a!(kfi8VU#x7A&@ct+lgI z4wBP2YVmWLy+;GRc!C4K@NQT+P@0pxLG;GQg*sM1S5(4I@&Kn40K3{RZht+1qP@yg zo*Ifg&lo;|)Im)Jy{oQ%ScK?D4eG-ydDA4Hq4q_>U?8`ei`*)iUgDxr-8FNs;gfk= z#c+JY#)1+^Th;Af3m)uxW&8KRCNcWK^DTy*Tc~dBHOz&qhOYJK2ka7o-bHsm*ZrcM zkPnX^ivJQ_yVl#CRM@J3c(oE$hQMJQ(>(gD*%R+)r%D?u>#yvd8#4z>+7{Q96>h}g z^;w%zEEu-wihhoE(s+SFx%bNA8Tcs7hc)lq?U>>FX?uxA>v^(!9YbbtY)nI=a>pBq#qDE~*YV@+XZ?+Xau+rtJznV)d z{xk$vm2A85&KN35Xv1iGL{^h(-pNwDf3Z#TJqM|1%pb$ECwJe-KwW|G5XgWnql)g) zca%K(Y+Mq3sS?_i^Q?Y7zm{5alY337dU!gB4GJ7;TS97)ojIX7z5(37FQmk9 z+rR}bz+FCqjxFcd1dr@gpW{J}UNZ~OC`mA7;U9`9u=AC9&kQ@S#ZrI386gI78R-1^ zhMRu0TLdBU9^gNf+UEgSCZ3-K6}%?2)9r3;-H;N7YVYo>5nr{@WX^g8uxq#Y%$D?KGyiicJ9wy#XhA3?P~`-1t!Rme6f_xkgf(KbY3aUNf(BcOL( zmy&!aMw2HMa~w!faGo$a*7W}F%o7|K#l0~js`jwFWqG0#x69z}Ac?t8Ryd|&UA4oy zne2Q|_DyaXbIePv;O_H7ET0Hy{6D(7|J0u8sY{0950>Xv5E|AeBsdGkFlIHi1Vn0{#vmz$CO)i9=qZlc$Nh(?rg|V(J6?|5|(Z;I>=W zTJvb2-%#uJxaTMWj`!=BLpBLRIs$g=ctdW_I^LbJ(V44;?{7`gkE)4m4FI0_8;2(L zI)w((_5w2kf|q&%wG@zB_Q8w$?X+Y96h0YgeqK?n`G z19}@kvj&|kO@0+Sg+myv#ds2m2~Kg+A$QLWXzK8Whp_h5C*|dY(xAyXr1vNj7_8lF z+Hx2~jX3Hpy2qX4&8F*s5c9#5ww)J_SqlhIi+^XHZv(|v54f9q0*rCI(Gi~QJ(uy> ztHQSl&5ZgQJ^~<+Gd8;+uPc#PtG4XXL>?d?`(UCqDZEmqoa8Z;VmI`HVn_L8klcGb z_}Vq7^7ifp7cc;uozlzDUDKUzXY^|1$s&t2pq=q-5`9c_=d+b0ZuiR)Ay;N9EEvkF z-I0ikc+I5Rs(Eu(k0qaz(V2hgBL;}@_xAkj68Gdlf72}c@dS_LEv1vVDIkk(H28Di z&++_FBCefISs!Rz0FFW~8Xp;d;iO$^I+mW+X`kgKsz4>;<>ps}=fJ@1zYYpyz6W1& zewn`IAitPvfKUlM7#%FIt)XK_K=6X-E#ur>2(w=2Sg|Uit`i2VUF=tYdwSD{kVz5r z3NOT6D{!V!RzQ)=Q^~;q`nDIdWs}jiRlWzJg17+VdlHOHWBs<1DrwYCTb7_ON(-V; zt&zW#=GO#dWcpThL|x*FzzXIgz)HS0R#t0Il!ok<6#@x7b=FSHW@Fnezg$y?^@p38{EVaQ8YVH6N*{w${r5iE-2+j(nW_lD_e(eaJF5zZ!8nJ zzJ$zU!ihFkV_=o1fC2r{zwO*l4wy+tz&@p*y#T_Hm4Ny}Hrds=Mljlss+}6DfDG}J z2pBMtBK|%c9bG(bF=n{cwPuF2a}9qXrKqTDIO!HI7^SmbN+6I_RX9-4jWW+Byr!RO(+x zRg`>xe9&{~AKi!gY4*xFQ5*Xq?9!oE;fOuxA^|Yo4t;-?#~XS+T&@8HRbK*GjnkT{ zA6Sk(28B+^jk8O52Y0g(TJFC$g^&CXdkK*2nmx}T{`=79x!C*t78zfD88Dc9M~ops z?|y3V=WFBLkYe*laUNw|=s39w_xt$e}K;k0bV&4gVrL7Ig=FLlTLV*I0zv~!7J zyRWP~9L(wJJosFC;i-QmtJm?w69jN%!bk^NHoqg7Kq$x_L{ z%lduQ2T4%)qEh1)U^R}zGvAI{RkV>&`~;8X6+qdV-+-K!XDz3jP28(q#YVXoIG#AP z{EWSgZ1x7VRO11`#kl)hR18_C)}WC>KPtld&^r%}0Ip)!+t!}A05GEP+u0BMNhJZn zEL8%evZJcR@_m&KcOIIYme87Hw{=MZp{Gd4wAL25WmEJ+X3w%@`KsD?T7e)7M-7-O zeWPw2lpvML@2ub^1u>mO@yw#N$_+*-P#gYL(a%EK6dn8o5HF$Sn+&mx4=W=mdf9b4 zmD|y-vYwp%Bpk$*W*#RXuZND=jDhBY4U>|RAC_9A$?cr(*7${9zz@8D5z1wyyE)f6 zC7lTyu!BKB0_qi|#<5_oeoa_HPhhTB^$QF*?H~QP2Ak|Me4eXED0LYWpcPXOm4?Pr`$4D;!2h8yTcr{{3xdK%zF5?b}mwP zqfBe951pLf;oeBJl8SO`Q;|Q;?Za^`a|8TJox9C<;fyEf+;!Mas23sX({p|=rb3`g zHD^E=3aY2Pb(lqMQHhQ=v#4*@z*+VG)M9;mXmS5hJ`|0&5Ti9aH*I@U-(_}w?)ufi z51=|o_aWX&s(T;1<0E|5VO56UuB*f^y^!uJu-*N`@z1r0M-X}0?D_$S$#$6KO@NMk zFj(rDt1CjG+b8vW%i5U{Ss7zk|E2`Z*TPu$&uq9{d@Y*G`Mj>j2XMxAQu63!Qz>5BO2B7+4DWreW~=L z^9d$=PP$P7(x&3E0IcDi-FkQ#JGZund4YA?MY43CRbKZX@#Uo)m(3hFE1`WU7@@-& z8xrQr3>Lo%q&g7q>AMr6RQbI%%!IHuXzI^=m}l=&tAnz?c(~S9EohWjniyS$xPIr@ z$f^CRX7LkU8`iqciSr`=;9tPLYO$|}1F=xFt4K70mgTunUMrhdTD`Qe;kQxH?NF9i zDH=QjNgJl4qMB}1w{U8+4nK_;-w~acvk11RGRZ|mN6H5%iunAxmWBzbp`6?Z_j{Q)C1t=!-cFr@>)Aeb8yPyh^~ zbH}|UWbjTn5~$)-OCq~#Eun~4ba*JJmD3xNz5fje!XoBd*0Q(kC_{Uz`}~?ZcUg7* z+o@pYwKlewQPjqhO~B@=Y8Bh_(g~*tqnJm30l_xbh)+1P3Q=t?=+r*|)qx^EybeT4 zJgXQwBpNh()h5oEYMCMkoXI-w{OffdI1~uPtN;G)hnvdi+rPN|`CFjULVvWehS*Ws zZTs$Hv_CC+fAEgEb3S&D7H60hJE_1IALcI~9Z`{p^QKv@wp$gb_eC2E*lTe<*Oc*Th0#`VuTy>M+=qk2tS}MJ}A+0C?KA7C%;|Cre#XFHO`{rdR2by9~y> zSu;QASCx^Dxe_O@R1F|ujU9N`i-fAND9{45J97yKGHjIG0tH_7{uL#{ z0*6tL)qOEYc}p~gKuIU~^5vWj*xjgZAuEGkO$fTCnA5s^Ze$_CXs3CmGuVVBMb9;@ z_m=-Mh2*{pW(<0KyjfPE5S6iDZ!(J^Y50TY-P{uBDV-Ob`ZoI)FR5xXf6Gj$9x%_q z{!!yEE2D;&zf%NCjx0K8+Uiad!T#B9@H4&tU9vZKh;>OuCiVR(`9@jkp#u{=%;#I; ze>25K#uYA0ne1xJ{aj!qD@oR-*049u4Cgl_JIA8VV%ir=qhUK4+iqPaFke^aAXGOq zI?G-y843p2VlUvbfkN^aAOa-eOJ!ZsQX$5QWtU}LWU2rHg_^hDO5?)t*lmC+{B9GY2MHIB#~2m58l$pduA z1~n}ZWO}Nu{~c&|tkX9W{;{OV>&|4H!svGHeFFz{jJw4&`;`;X|6TV_4iW)&F!)C- z(NROgtTpCE9`F-AZY@#vnifn?NMO@Q#xl(A+kXRcyaWG}7~n|s34t;7Jx8Pc9R!xT zgZ$MM1P`T0%WLe5`HCasr4aI9xY@~(^IwkLY3t^|)Rp+|yWOvr=X)Z)_|%}coP&9L zvirPbbcJ%J_oCjP&M+pRw*(_^3R4&B-9YowshN4VVoqN&u(6`vIH9v<@r$~;%UWCz zF{nk-56kkjx%DQGp5Hl$L{7%P;t1U26e*(`i1H?2`jb)LN_|SoHV~ z220cx;MM_6tC1m(VT#=Ro%30xQiu1JHvp#`P*_*2T{MudX6@_(J0J5Rzk7XeudqG@ zby$8r$*w{ORqmR-+7~HcwO7baH)R$8=qze>*8t>zISdJgk21o!bt>*n)!zmCit^BS z7TKA8AGCzfdr3wqP4<6N#j+RU-%{IUgi97lK-<$dvtRg*Pmum#svXlCQy8>VAKdmj z{S?ka9PXg-Avq(fJ^=?_3L#`(&d$`)N|9GQvl5>QFmxJdX<@~(6fKaAT}HXmH-3)Z zH-1i!Rz|-BHWD`Q8LfvGqnwamc?&IiT516Z&Z6yXpA+v5>qflHrXSg~UFx(@Ks~sq zGmq#Ws0uEc?#%FXXuQ67x++C+VQ?;9t@I3IS7o#xVsgBpeFC6{6-i@*$fO7a8fGH> zBCrpaw-0y9YAhXqe+7Haxk*(QEt*wxm&&<*jca>N^TuMCDF?Ni2y|P24IA`Nw_Jx- zV&#|TLqEC&mFFq{gl*X7Y#a=%Uzu-W$RgIm7r_m}J{4H#`4&YP((UYox_G4xQc^v- z`)>#%BlbVd!8Cq7viG#c?^!*6+6dHZ>e!)t6j2`p3vOWvZoi*G^A=v|fo#)N&{=@8 zWA*ydRG6g*0VuUEl#RS=*_Ue<>jjF zUoSObnd84n<~sV*4A5S^G7t`!&!+kBN(xXAuIr8{*87vg%&nBz;No95NrX%I0cH;US!S z(0g_-S9d=d4)i=RjR2V7_91ZW&^(i6p zpQFOCQvS}$=x%3=qMjhvOofTA8^TZnYH#Zm?fE|7$amKv=0KvwSo;5N%U*nRRNlbN z(Cy91sFj}$U?Ns;7cZd0Djh`jD);SG>xfK)k;=X#Y0Hk=XgY;EkLtflpTPFA=r!nXcNu~}hwA*Z#w-@h$)gJx8naogd2?~E< zb-Sh9bxMSM4nke---mAHgNtYcps)2NO~gf?iJ=qI|{s1A=Z`n?Kn+#M;| zy|@>h`o^Qhchh?5)QX<8F*?O;Z8Dh?t%rC}GkV)?H7` z>#TuqB4fX5l1in}g5(w} zyIRiObYsFTqsLibvN*8b_5N?xuP-V5W#SusdxKzaPVzbUOe)A*YtAFobyWe3=XHVY zF}~kxH4WU5_`kw?&qDw-_5Hr;Pws#G2zaH$cIS`Xt_e&~Z`f0p)2HNSeWb#RS8!s# zCb6wi(nm;t%5a56t0g_YXpSf*+xQcMbY;+Z&Z93vsuRGVZ-2&an(dS-JyoZrN=F2C z8??CpDQ-55y9uSfZu@&Bi{`Cl-)}A;gr&iwTUfcdvNK%}<>0)phrjN-a&BEhV^GNh z2yto@n_7zR)b(tM3M!B@EC{%RRql4s-FT6!3PMg*JCt-zh99BNEIfGG(Ip6;_|GDv!z&W+ zr$v+cysoJ&Ob>P<&GZvW0p=(G71vZFwLE7vh0O$JS62na_MNf1Z^NyTWF;pg2Lo`p zeKOu2?!(c}TSB6+JG%Jm*=R}8BIE#D5msW^o$A{F5DY&j_eoF`*Cm)@!2fYUQVZ{vLQrT&kw3$Us&$6)%iHYc_0IK7FRaCt8a8@pR)f%a zcHCsfN;(p+>=Lzg5co^uV~%4_$RD}DIV~fcz~wU5mTuO*_~i$Hl-1vx)NIyWedmaq zCRp#9d{*D#yC(Zvg=U(m5*8*$KRk|)za4WjUjFiQi?!*CZJFl#Dm$Xh*PRu&73%Z& z$0y43me+uLziZO22xxYrqiMaeb&K(8F0YDE$?k}J+#1#q2tP*o;Rl@q$C%@77YYyk zc}3)|(oKPrDknxnO)erOB~nk-_@6Yz?;PG=uPlDdjpYX}w8A`f>?%D4-ucNoaN4MT z>nZ#AW}Ot?ot=|)=YxWm9Ap|k3fdVt^0HE*-PWZ zz@`IKl^u?A(!5X{8^xjs`fpWn8hr_k37wS4Hbln?C*GF;UcXPqbrLuXXW=@z9h1oT zg&y+Ovq(ytDZy{(y(ji8M8yr{5Bzh)S|UCBiH3P#gn@H=>9K7mH|-}sPz*bCLfM-# z@RjP#!Wl1$C3gc84c*H%hV!&ZHB(Spu5w&2^%8(a5x{jq3naCd7nx1b%$I>!=Ema1mW z-;4MCkSreO{H}(*0Ga1Joq7P&z;<4P`?&>Hox3Nw3Da`Z{_M^-*L2 zAKJM^B!2QG+@%mi<7(;eSC0p~u_uRJZ|H9VJZIJ z@tFzotBU++Y?|Efhb3;)I|F8Em0)Rho0@t9+C7%#%A_+ia&JCv{qj+vV5WUe6i!fu zv#U8ZEni**`r(FuxvRla-5L2M^2E;0(t7(tzqb2V3Im9o-|%~WxG(Y>tvodPqSW~m z<-S{b>yKU<7S$TwT6P^d(oKPYQxq}Jd*`B?kh4Zrfk)S~E88SO8j2JAYe{FQVO7nD zsn)uQwO3OO_llBhW~xjRldiQ!7LQ7|x#UKYmQ5ltV^pH`}Lpm@4C3&Axu+4Ui7BIxPWQU7bj(E(ETgEjj~Fa5ZE z#L_Z%|6@6xo4ew!?fd_CJ>K(qwJ}#IgX<=R=*+8)PvS0|=ixAPR=mw;A^mpy z>G9lhI7|f{S0ljYEX)tuy)HUha&wS zXP)oh@|Uf~W}d-qHj8M(=w~nc=jD|#96!4FrP98=KMqg7^wnW*g-)`KpOIbhM~~w_ zr1y7!zo~CCIgg)#VNLSYKdYee6jiFg$N&TpcTuch0*P(d75aHK!p$rov4qmq&%#At zF?6LkZo9OKnG+;-;8ob!^;%aLMNT&5EDdEB0*N(TT{|m2bOp1nbL6hNY1X~a#bp4_s@eHaZlPmN3Qbv(X;*f zv3B2VQb7tC%61n&ef8{=wO-D;*zeM%4F9JC)8AX$_tpQOJ#y~PdsqMZw)uDa4`yJ+ zd<7359X>7kvI&wlz-iz>&nRa!41ft2IUP8>J9)OAAwh)m&-GSa2~gVcboFyt=akR{ E0Dk7pVgLXD From a69e9dd0a2c1dc571d09dedbdeb689c71b0e8cc9 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 17 Aug 2020 02:22:22 +0300 Subject: [PATCH 111/300] Rename server intents image to clear GitHub image cache --- CHANGELOG.md | 2 +- ...mbers-intent.png => server-members-intent-2.png} | Bin docs/setup.md | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename docs/{server-members-intent.png => server-members-intent-2.png} (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index f876876..70e441d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ Please report any bugs you encounter by [creating a GitHub issue](https://github * The supported versions are now 12, 13, and 14 * **BREAKING CHANGE:** The bot now requests the necessary [Gateway Intents](https://discord.com/developers/docs/topics/gateway#gateway-intents) * **This includes the privileged "Server Members Intent"**, which is used for server greetings/welcome messages. - This means that [**you need to turn on "Server Members Intent"**](docs/server-members-intent.png) on the bot's page on the Discord Developer Portal. + This means that [**you need to turn on "Server Members Intent"**](docs/server-members-intent-2.png) on the bot's page on the Discord Developer Portal. * Renamed the following options. Old names are still supported as aliases, so old config files won't break. * `mainGuildId` => `mainServerId` * `mailGuildId` => `inboxServerId` diff --git a/docs/server-members-intent.png b/docs/server-members-intent-2.png similarity index 100% rename from docs/server-members-intent.png rename to docs/server-members-intent-2.png diff --git a/docs/setup.md b/docs/setup.md index fc0fbf3..77f178a 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -13,7 +13,7 @@ To keep it online, you need to keep the bot process running. ## Prerequisites 1. Create a bot on the [Discord Developer Portal](https://discordapp.com/developers/) -2. Turn on **Server Members Intent** in the bot's settings page on the developer portal ([Image](server-members-intent.png)) +2. Turn on **Server Members Intent** in the bot's settings page on the developer portal ([Image](server-members-intent-2.png)) 3. Install Node.js 12, 13, or 14 4. Download the latest bot version from [the releases page](https://github.com/Dragory/modmailbot/releases) * Make sure the release doesn't say "Pre-release" next to it unless you want to run an unstable beta version! From 883d8adf933cf155d6bb6a5412acd81810fddcf7 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 17 Aug 2020 11:26:06 +0300 Subject: [PATCH 112/300] Fix crash when a user sends an attachment Also added an eslint rule to catch similar errors caused by shadowed variables in the future. --- .eslintrc | 3 ++- src/data/Thread.js | 12 ++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.eslintrc b/.eslintrc index 6698909..437180d 100644 --- a/.eslintrc +++ b/.eslintrc @@ -20,6 +20,7 @@ "!!": true } }], - "quotes": ["error", "double"] + "quotes": ["error", "double"], + "no-shadow": "error" } } diff --git a/src/data/Thread.js b/src/data/Thread.js index f27359f..87e407a 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -258,8 +258,8 @@ class Thread { let messageContent = msg.content || ""; // Prepare attachments - const attachments = []; - const smallAttachments = []; + const attachmentLinks = []; + const smallAttachmentLinks = []; const attachmentFiles = []; for (const attachment of msg.attachments) { @@ -269,10 +269,10 @@ class Thread { if (config.relaySmallAttachmentsAsAttachments && attachment.size <= config.smallAttachmentLimit) { const file = await attachments.attachmentToDiscordFileObject(attachment); attachmentFiles.push(file); - smallAttachments.push(savedAttachment.url); + smallAttachmentLinks.push(savedAttachment.url); } - attachments.push(savedAttachment.url); + attachmentLinks.push(savedAttachment.url); } // Handle special embeds (listening party invites etc.) @@ -311,8 +311,8 @@ class Thread { is_anonymous: 0, dm_message_id: msg.id, dm_channel_id: msg.channel.id, - attachments, - small_attachments: smallAttachments, + attachments: attachmentLinks, + small_attachments: smallAttachmentLinks, }); threadMessage = await this._addThreadMessageToDB(threadMessage.getSQLProps()); From 47125fd7fd055e3eb37b31815073b40609e6709d Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Tue, 18 Aug 2020 21:43:30 +0300 Subject: [PATCH 113/300] Fix rare crash in typingProxy --- src/modules/typingProxy.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/modules/typingProxy.js b/src/modules/typingProxy.js index cdcc97a..e84d085 100644 --- a/src/modules/typingProxy.js +++ b/src/modules/typingProxy.js @@ -6,6 +6,11 @@ module.exports = ({ bot }) => { // Typing proxy: forwarding typing events between the DM and the modmail thread if(config.typingProxy || config.typingProxyReverse) { bot.on("typingStart", async (channel, user) => { + if (! user) { + // If the user doesn't exist in the bot's cache, it will be undefined here + return; + } + // config.typingProxy: forward user typing in a DM to the modmail thread if (config.typingProxy && (channel instanceof Eris.PrivateChannel)) { const thread = await threads.findOpenThreadByUserId(user.id); From fdabf6588264cbdd8d2aa71b1b77f90579c635e9 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Tue, 18 Aug 2020 22:43:08 +0300 Subject: [PATCH 114/300] Fix thread alert_id being limited to 20 chars on MySQL --- ...0818214801_change_alert_id_to_alert_ids.js | 21 +++++++++++++++++++ src/data/Thread.js | 20 +++++++++--------- 2 files changed, 31 insertions(+), 10 deletions(-) create mode 100644 db/migrations/20200818214801_change_alert_id_to_alert_ids.js diff --git a/db/migrations/20200818214801_change_alert_id_to_alert_ids.js b/db/migrations/20200818214801_change_alert_id_to_alert_ids.js new file mode 100644 index 0000000..9ba6f5d --- /dev/null +++ b/db/migrations/20200818214801_change_alert_id_to_alert_ids.js @@ -0,0 +1,21 @@ +exports.up = async function(knex) { + await knex.schema.table("threads", table => { + table.text("alert_ids").nullable(); + }); + + await knex("threads") + .update({ + alert_ids: knex.raw("alert_id"), + }); + + await knex.schema.table("threads", table => { + table.dropColumn("alert_id"); + }); +}; + +exports.down = async function(knex) { + await knex.schema.table("threads", table => { + table.dropColumn("alert_ids"); + table.text("alert_id").nullable(); + }); +}; diff --git a/src/data/Thread.js b/src/data/Thread.js index 87e407a..37526a7 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -23,7 +23,7 @@ const {THREAD_MESSAGE_TYPE, THREAD_STATUS, DISCORD_MESSAGE_ACTIVITY_TYPES} = req * @property {String} scheduled_close_id * @property {String} scheduled_close_name * @property {Number} scheduled_close_silent - * @property {String} alert_id + * @property {String} alert_ids * @property {String} created_at */ class Thread { @@ -334,8 +334,8 @@ class Thread { await this.postSystemMessage(`<@!${this.scheduled_close_id}> Thread that was scheduled to be closed got a new reply. Cancelling.`); } - if (this.alert_id) { - const ids = this.alert_id.split(","); + if (this.alert_ids) { + const ids = this.alert_ids.split(","); let mentions = ""; ids.forEach(id => { @@ -604,9 +604,9 @@ class Thread { async addAlert(userId) { let alerts = await knex("threads") .where("id", this.id) - .select("alert_id") + .select("alert_ids") .first(); - alerts = alerts.alert_id; + alerts = alerts.alert_ids; if (alerts == null) { alerts = [userId] @@ -621,7 +621,7 @@ class Thread { await knex("threads") .where("id", this.id) .update({ - alert_id: alerts + alert_ids: alerts }); } @@ -632,9 +632,9 @@ class Thread { async removeAlert(userId) { let alerts = await knex("threads") .where("id", this.id) - .select("alert_id") + .select("alert_ids") .first(); - alerts = alerts.alert_id; + alerts = alerts.alert_ids; if (alerts != null) { alerts = alerts.split(","); @@ -657,7 +657,7 @@ class Thread { await knex("threads") .where("id", this.id) .update({ - alert_id: alerts + alert_ids: alerts }); } @@ -668,7 +668,7 @@ class Thread { await knex("threads") .where("id", this.id) .update({ - alert_id: null + alert_ids: null }) } From c467c7d0f6938028b23cb2166c3291cba0d9a21d Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Tue, 18 Aug 2020 22:43:26 +0300 Subject: [PATCH 115/300] Add npm script to run migrations manually --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 790a4e0..292222b 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "lint-fix": "eslint --fix ./src ./db/migrations", "generate-config-jsdoc": "node src/data/generateCfgJsdoc.js", "generate-plugin-api-docs": "jsdoc2md -t docs/plugin-api-template.hbs src/pluginApi.js > docs/plugin-api.md", - "create-migration": "knex migrate:make" + "create-migration": "knex migrate:make", + "run-migrations": "knex migrate:latest" }, "repository": { "type": "git", From 581b09a8ae655846bc3972e8160978e5ededb0fd Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Fri, 21 Aug 2020 05:13:24 +0300 Subject: [PATCH 116/300] Fix thread header ping not working, utilize allowed_mentions --- src/bot.js | 5 +++++ src/data/Thread.js | 20 +++++++++++++------- src/data/threads.js | 7 +++++-- src/main.js | 3 ++- src/modules/block.js | 22 ++++++++++++++++++---- src/utils.js | 29 +++++++++++++++++++++++------ 6 files changed, 66 insertions(+), 20 deletions(-) diff --git a/src/bot.js b/src/bot.js index 666869a..1338d03 100644 --- a/src/bot.js +++ b/src/bot.js @@ -20,6 +20,11 @@ const intents = [ const bot = new Eris.Client(config.token, { restMode: true, intents: Array.from(new Set(intents)), + allowedMentions: { + everyone: false, + roles: false, + users: false, + }, }); /** diff --git a/src/data/Thread.js b/src/data/Thread.js index 37526a7..4ed6f8c 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -331,19 +331,25 @@ class Thread { // Interrupt scheduled closing, if in progress if (this.scheduled_close_at) { await this.cancelScheduledClose(); - await this.postSystemMessage(`<@!${this.scheduled_close_id}> Thread that was scheduled to be closed got a new reply. Cancelling.`); + await this.postSystemMessage({ + content: `<@!${this.scheduled_close_id}> Thread that was scheduled to be closed got a new reply. Cancelling.`, + allowedMentions: { + users: [this.scheduled_close_id], + }, + }); } if (this.alert_ids) { const ids = this.alert_ids.split(","); - let mentions = ""; - - ids.forEach(id => { - mentions += `<@!${id}> `; - }); + const mentionsStr = ids.map(id => `<@!${id}> `).join(""); await this.deleteAlerts(); - await this.postSystemMessage(`${mentions}New message from ${this.user_name}`); + await this.postSystemMessage({ + content: `${mentionsStr}New message from ${this.user_name}`, + allowedMentions: { + users: ids, + }, + }); } } diff --git a/src/data/threads.js b/src/data/threads.js index 2d986d0..305f9ec 100644 --- a/src/data/threads.js +++ b/src/data/threads.js @@ -186,7 +186,7 @@ async function createNewThreadForUser(user, opts = {}) { if (config.mentionRole) { await newThread.postNonLogMessage({ content: `${utils.getInboxMention()}New modmail thread (${newThread.user_name})`, - disableEveryone: false + allowedMentions: utils.getInboxMentionAllowedMentions(), }); } @@ -255,7 +255,10 @@ async function createNewThreadForUser(user, opts = {}) { infoHeader += "\n────────────────"; - await newThread.postSystemMessage(infoHeader); + await newThread.postSystemMessage({ + content: infoHeader, + allowedMentions: config.mentionUserInThreadHeader ? { users: [user.id] } : undefined, + }); if (config.updateNotifications) { const availableUpdate = await updates.getAvailableUpdate(); diff --git a/src/main.js b/src/main.js index 460e02c..600b3c6 100644 --- a/src/main.js +++ b/src/main.js @@ -243,6 +243,7 @@ function initBaseMessageHandlers() { let content; const mainGuilds = utils.getMainGuilds(); const staffMention = (config.pingOnBotMention ? utils.getInboxMention() : ""); + const allowedMentions = (config.pingOnBotMention ? utils.getInboxMentionAllowedMentions() : undefined); if (mainGuilds.length === 1) { content = `${staffMention}Bot mentioned in ${msg.channel.mention} by **${msg.author.username}#${msg.author.discriminator}(${msg.author.id})**: "${msg.cleanContent}"\n\n`; @@ -252,7 +253,7 @@ function initBaseMessageHandlers() { bot.createMessage(utils.getLogChannel().id, { content, - disableEveryone: false, + allowedMentions, }); // Send an auto-response to the mention, if enabled diff --git a/src/modules/block.js b/src/modules/block.js index 5eea8fd..6a73822 100644 --- a/src/modules/block.js +++ b/src/modules/block.js @@ -9,7 +9,12 @@ module.exports = ({ bot, knex, config, commands }) => { const logChannel = utils.getLogChannel(); for (const userId of expiredBlocks) { await blocked.unblock(userId); - logChannel.createMessage(`Block of <@!${userId}> (id \`${userId}\`) expired`); + logChannel.createMessage({ + content: `Block of <@!${userId}> (id \`${userId}\`) expired`, + allowedMentions: { + users: [userId], + }, + }); } } @@ -88,12 +93,21 @@ module.exports = ({ bot, knex, config, commands }) => { const blockStatus = await blocked.getBlockStatus(userIdToCheck); if (blockStatus.isBlocked) { if (blockStatus.expiresAt) { - msg.channel.createMessage(`<@!${userIdToCheck}> (id \`${userIdToCheck}\`) is blocked until ${blockStatus.expiresAt} (UTC)`); + msg.channel.createMessage({ + content: `<@!${userIdToCheck}> (id \`${userIdToCheck}\`) is blocked until ${blockStatus.expiresAt} (UTC)`, + allowedMentions: { users: [userIdToCheck] }, + }); } else { - msg.channel.createMessage(`<@!${userIdToCheck}> (id \`${userIdToCheck}\`) is blocked indefinitely`); + msg.channel.createMessage({ + content: `<@!${userIdToCheck}> (id \`${userIdToCheck}\`) is blocked indefinitely`, + allowedMentions: { users: [userIdToCheck] }, + }); } } else { - msg.channel.createMessage(`<@!${userIdToCheck}> (id \`${userIdToCheck}\`) is NOT blocked`); + msg.channel.createMessage({ + content: `<@!${userIdToCheck}> (id \`${userIdToCheck}\`) is NOT blocked`, + allowedMentions: { users: [userIdToCheck] }, + }); } }); }; diff --git a/src/utils.js b/src/utils.js index 6c537cf..1d5f238 100644 --- a/src/utils.js +++ b/src/utils.js @@ -46,18 +46,18 @@ function getMainGuilds() { * @returns {Eris~TextChannel} */ function getLogChannel() { - const inboxGuild = getInboxGuild(); - const logChannel = inboxGuild.channels.get(config.logChannelId); + const _inboxGuild = getInboxGuild(); + const _logChannel = _inboxGuild.channels.get(config.logChannelId); - if (! logChannel) { + if (! _logChannel) { throw new BotError("Log channel (logChannelId) not found!"); } - if (! (logChannel instanceof Eris.TextChannel)) { + if (! (_logChannel instanceof Eris.TextChannel)) { throw new BotError("Make sure the logChannelId option is set to a text channel!"); } - return logChannel; + return _logChannel; } function postLog(...args) { @@ -217,7 +217,7 @@ function chunk(items, chunkSize) { function trimAll(str) { return str .split("\n") - .map(str => str.trim()) + .map(_str => _str.trim()) .join("\n"); } @@ -263,6 +263,22 @@ function getInboxMention() { return mentions.join(" ") + " "; } +function getInboxMentionAllowedMentions() { + const mentionRoles = Array.isArray(config.mentionRole) ? config.mentionRole : [config.mentionRole]; + const allowedMentions = { + everyone: false, + roles: [], + }; + + for (const role of mentionRoles) { + if (role == null) continue; + else if (role === "here" || role === "everyone") allowedMentions.everyone = true; + else allowedMentions.roles.push(role); + } + + return allowedMentions; +} + function postSystemMessageWithFallback(channel, thread, text) { if (thread) { thread.postSystemMessage(text); @@ -343,6 +359,7 @@ module.exports = { delayStringRegex, convertDelayStringToMS, getInboxMention, + getInboxMentionAllowedMentions, postSystemMessageWithFallback, chunk, From b992b49c5c55cfcfe7630c6fe9ad526c22d62f1d Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Fri, 21 Aug 2020 05:16:23 +0300 Subject: [PATCH 117/300] Clean up bot mention notification styles --- src/main.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main.js b/src/main.js index 600b3c6..b1316a9 100644 --- a/src/main.js +++ b/src/main.js @@ -245,10 +245,13 @@ function initBaseMessageHandlers() { const staffMention = (config.pingOnBotMention ? utils.getInboxMention() : ""); const allowedMentions = (config.pingOnBotMention ? utils.getInboxMentionAllowedMentions() : undefined); + const userMentionStr = `**${msg.author.username}#${msg.author.discriminator}** (\`${msg.author.id}\`)`; + const messageLink = `https:\/\/discordapp.com\/channels\/${msg.channel.guild.id}\/${msg.channel.id}\/${msg.id}`; + if (mainGuilds.length === 1) { - content = `${staffMention}Bot mentioned in ${msg.channel.mention} by **${msg.author.username}#${msg.author.discriminator}(${msg.author.id})**: "${msg.cleanContent}"\n\n`; + content = `${staffMention}Bot mentioned in ${msg.channel.mention} by ${userMentionStr}: "${msg.cleanContent}"\n\n<${messageLink}>`; } else { - content = `${staffMention}Bot mentioned in ${msg.channel.mention} (${msg.channel.guild.name}) by **${msg.author.username}#${msg.author.discriminator}(${msg.author.id})**: "${msg.cleanContent}"\n\n`; + content = `${staffMention}Bot mentioned in ${msg.channel.mention} (${msg.channel.guild.name}) by ${userMentionStr}: "${msg.cleanContent}"\n\n<${messageLink}>`; } bot.createMessage(utils.getLogChannel().id, { From 8930e5dde866a3ac3102aa5a173b3bd2009e058c Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 24 Aug 2020 19:31:53 +0300 Subject: [PATCH 118/300] Fix crash when greetingMessage is used instead of serverGreetings --- src/cfg.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cfg.js b/src/cfg.js index 1d7c304..09a4633 100644 --- a/src/cfg.js +++ b/src/cfg.js @@ -136,6 +136,7 @@ if (config.guildGreetings && ! config.serverGreetings) { // already have something set up in serverGreetings. This retains backwards compatibility while allowing you to override // greetings for specific servers in serverGreetings. if (config.greetingMessage || config.greetingAttachment) { + config.serverGreetings = config.serverGreetings || {}; for (const guildId of config.mainServerId) { if (config.serverGreetings[guildId]) continue; config.serverGreetings[guildId] = { From aea216f289d273263a354462f7ac5c54ee28313d Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 24 Aug 2020 19:35:01 +0300 Subject: [PATCH 119/300] Fix crash when using newThreadCategoryId without categoryAutomation --- src/cfg.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cfg.js b/src/cfg.js index 09a4633..3fcceab 100644 --- a/src/cfg.js +++ b/src/cfg.js @@ -148,6 +148,7 @@ if (config.greetingMessage || config.greetingAttachment) { // newThreadCategoryId is syntactic sugar for categoryAutomation.newThread if (config.newThreadCategoryId) { + config.categoryAutomation = config.categoryAutomation || {}; config.categoryAutomation.newThread = config.newThreadCategoryId; delete config.newThreadCategoryId; } From 96f97c78c0d42b25f1e6aa0608cb7353dded6a84 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 13 Sep 2020 15:24:51 +0300 Subject: [PATCH 120/300] Remove husky/lint-staged Broke "npm ci" when downloading the zip instead of cloning. --- package-lock.json | 843 +--------------------------------------------- package.json | 10 - 2 files changed, 1 insertion(+), 852 deletions(-) diff --git a/package-lock.json b/package-lock.json index cfdbf8d..662250e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "modmailbot", - "version": "2.31.0-beta.0", + "version": "2.31.0-beta.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -48,12 +48,6 @@ "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" }, - "@types/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", - "dev": true - }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -71,16 +65,6 @@ "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", "dev": true }, - "aggregate-error": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz", - "integrity": "sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA==", - "dev": true, - "requires": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - } - }, "ajv": { "version": "6.12.4", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz", @@ -115,15 +99,6 @@ } } }, - "ansi-escapes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.0.tgz", - "integrity": "sha512-EiYhwo0v255HUL6eDyuLrXEkTi7WwVCLAw+SeOQ7M7qdun1z1pum4DEm/nuqIVbPvi9RPPc9k9LbyBv6H0DwVg==", - "dev": true, - "requires": { - "type-fest": "^0.8.1" - } - }, "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", @@ -473,12 +448,6 @@ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, - "ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", - "dev": true - }, "class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", @@ -500,107 +469,6 @@ } } }, - "clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true - }, - "cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, - "requires": { - "restore-cursor": "^3.1.0" - } - }, - "cli-truncate": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", - "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", - "dev": true, - "requires": { - "slice-ansi": "^3.0.0", - "string-width": "^4.2.0" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "slice-ansi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", - "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - } - }, - "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - } - } - }, "cliui": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", @@ -786,12 +654,6 @@ "integrity": "sha512-f0QqPLpRTgMQn/pQIynf+SdE73Lw5Q1jn4hjirHLgH/NJ71TiHjXusV16BmOyuK5rRQ1W2f++II+TFZbQOh4hA==", "dev": true }, - "compare-versions": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz", - "integrity": "sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==", - "dev": true - }, "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", @@ -834,19 +696,6 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, - "cosmiconfig": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", - "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", - "dev": true, - "requires": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.1.0", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.7.2" - } - }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -904,12 +753,6 @@ "mimic-response": "^1.0.0" } }, - "dedent": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", - "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=", - "dev": true - }, "deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -1095,15 +938,6 @@ "ws": "^7.2.1" } }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "requires": { - "is-arrayish": "^0.2.1" - } - }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -1317,23 +1151,6 @@ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true }, - "execa": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/execa/-/execa-4.0.3.tgz", - "integrity": "sha512-WFDXGHckXPWZX19t1kCsXzOpqX9LWYNqn4C+HqZlk/V0imTkzJZqf87ZBhvpHaftERYknpk0fjSylnXVlVgI0A==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.0", - "get-stream": "^5.0.0", - "human-signals": "^1.1.1", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.0", - "onetime": "^5.1.0", - "signal-exit": "^3.0.2", - "strip-final-newline": "^2.0.0" - } - }, "expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", @@ -1492,15 +1309,6 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, - "figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, "file-entry-cache": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", @@ -1567,15 +1375,6 @@ "path-exists": "^4.0.0" } }, - "find-versions": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-3.2.0.tgz", - "integrity": "sha512-P8WRou2S+oe222TOCHitLy8zj+SIsVJh52VP4lvXkaFVnOFFdoWv1H1Jjvel1aI6NCFOAaeAVm8qrI0odiLcww==", - "dev": true, - "requires": { - "semver-regex": "^2.0.0" - } - }, "findup-sync": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", @@ -1741,12 +1540,6 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, - "get-own-enumerable-property-symbols": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", - "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", - "dev": true - }, "get-stream": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", @@ -1961,87 +1754,11 @@ "sshpk": "^1.7.0" } }, - "human-signals": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", - "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", - "dev": true - }, "humanize-duration": { "version": "3.12.1", "resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.12.1.tgz", "integrity": "sha512-Eu68Xnq5C38391em1zfVy8tiapQrOvTNTlWpax9smHMlEEUcudXrdMfXMoMRyZx4uODowYgi1AYiMzUXEbG+sA==" }, - "husky": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/husky/-/husky-4.2.5.tgz", - "integrity": "sha512-SYZ95AjKcX7goYVZtVZF2i6XiZcHknw50iXvY7b0MiGoj5RwdgRQNEHdb+gPDPCXKlzwrybjFjkL6FOj8uRhZQ==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "ci-info": "^2.0.0", - "compare-versions": "^3.6.0", - "cosmiconfig": "^6.0.0", - "find-versions": "^3.2.0", - "opencollective-postinstall": "^2.0.2", - "pkg-dir": "^4.2.0", - "please-upgrade-node": "^3.2.0", - "slash": "^3.0.0", - "which-pm-runs": "^1.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -2080,12 +1797,6 @@ "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", "dev": true }, - "indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true - }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -2147,12 +1858,6 @@ } } }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - }, "is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", @@ -2245,12 +1950,6 @@ } } }, - "is-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", - "dev": true - }, "is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -2264,12 +1963,6 @@ "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=" }, - "is-regexp": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", - "integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk=", - "dev": true - }, "is-relative": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", @@ -2278,12 +1971,6 @@ "is-unc-path": "^1.0.0" } }, - "is-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", - "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", - "dev": true - }, "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", @@ -2454,12 +2141,6 @@ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=" }, - "json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true - }, "json-pointer": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/json-pointer/-/json-pointer-0.6.0.tgz", @@ -2620,12 +2301,6 @@ "resolve": "^1.1.7" } }, - "lines-and-columns": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", - "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", - "dev": true - }, "linkify-it": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz", @@ -2635,200 +2310,6 @@ "uc.micro": "^1.0.1" } }, - "lint-staged": { - "version": "10.2.11", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-10.2.11.tgz", - "integrity": "sha512-LRRrSogzbixYaZItE2APaS4l2eJMjjf5MbclRZpLJtcQJShcvUzKXsNeZgsLIZ0H0+fg2tL4B59fU9wHIHtFIA==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "cli-truncate": "2.1.0", - "commander": "^5.1.0", - "cosmiconfig": "^6.0.0", - "debug": "^4.1.1", - "dedent": "^0.7.0", - "enquirer": "^2.3.5", - "execa": "^4.0.1", - "listr2": "^2.1.0", - "log-symbols": "^4.0.0", - "micromatch": "^4.0.2", - "normalize-path": "^3.0.0", - "please-upgrade-node": "^3.2.0", - "string-argv": "0.3.1", - "stringify-object": "^3.3.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" - } - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "listr2": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-2.5.1.tgz", - "integrity": "sha512-qkNRW70SwfwWLD/eiaTf2tfgWT/ZvjmMsnEFJOCzac0cjcc8rYHDBr1eQhRxopj6lZO7Oa5sS/pZzS6q+BsX+w==", - "dev": true, - "requires": { - "chalk": "^4.1.0", - "cli-truncate": "^2.1.0", - "figures": "^3.2.0", - "indent-string": "^4.0.0", - "log-update": "^4.0.0", - "p-map": "^4.0.0", - "rxjs": "^6.6.2", - "through": "^2.3.8" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, "locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -2866,151 +2347,6 @@ "integrity": "sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=", "dev": true }, - "log-symbols": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz", - "integrity": "sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==", - "dev": true, - "requires": { - "chalk": "^4.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "log-update": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", - "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", - "dev": true, - "requires": { - "ansi-escapes": "^4.3.0", - "cli-cursor": "^3.1.0", - "slice-ansi": "^4.0.0", - "wrap-ansi": "^6.2.0" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" - }, - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" - }, - "slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - } - }, - "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "requires": { - "ansi-regex": "^5.0.0" - } - } - } - }, "long": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", @@ -3081,12 +2417,6 @@ "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=", "dev": true }, - "merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, "micromatch": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", @@ -3127,12 +2457,6 @@ "mime-db": "1.44.0" } }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true - }, "mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", @@ -3432,12 +2756,6 @@ "abbrev": "1" } }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, "normalize-url": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", @@ -3466,15 +2784,6 @@ "npm-normalize-package-bin": "^1.0.1" } }, - "npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "requires": { - "path-key": "^3.0.0" - } - }, "npmlog": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", @@ -3586,21 +2895,6 @@ "wrappy": "1" } }, - "onetime": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", - "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", - "dev": true, - "requires": { - "mimic-fn": "^2.1.0" - } - }, - "opencollective-postinstall": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz", - "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==", - "dev": true - }, "optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -3661,15 +2955,6 @@ "p-limit": "^2.2.0" } }, - "p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "requires": { - "aggregate-error": "^3.0.0" - } - }, "p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -3694,18 +2979,6 @@ "path-root": "^0.1.1" } }, - "parse-json": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.1.tgz", - "integrity": "sha512-ztoZ4/DYeXQq4E21v169sC8qWINGpcosGv9XhTDvg9/hWvx/zrFkc9BiWxR58OJLHGk28j5BL0SDLeV2WmFZlQ==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1", - "lines-and-columns": "^1.1.6" - } - }, "parse-passwd": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", @@ -3750,12 +3023,6 @@ "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=" }, - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true - }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -3767,30 +3034,6 @@ "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.3.0.tgz", "integrity": "sha512-ukMTJXLI7/hZIwTW7hGMZJ0Lj0S2XQBCJ4Shv4y1zgQ/vqVea+FLhzywvPj0ujSuofu+yA4MYHGZPTsgjBgJ+w==" }, - "picomatch": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.1.1.tgz", - "integrity": "sha512-OYMyqkKzK7blWO/+XZYP6w8hH0LDvkBvdvKukti+7kqYFCiEAk+gI3DWnryapc0Dau05ugGTy0foQ6mqn4AHYA==", - "dev": true - }, - "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "requires": { - "find-up": "^4.0.0" - } - }, - "please-upgrade-node": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", - "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==", - "dev": true, - "requires": { - "semver-compare": "^1.0.0" - } - }, "posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", @@ -4088,16 +3331,6 @@ "lowercase-keys": "^1.0.0" } }, - "restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, - "requires": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - } - }, "ret": { "version": "0.1.15", "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", @@ -4111,15 +3344,6 @@ "glob": "^7.1.3" } }, - "rxjs": { - "version": "6.6.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.2.tgz", - "integrity": "sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - } - }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -4148,18 +3372,6 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==" }, - "semver-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=", - "dev": true - }, - "semver-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-2.0.0.tgz", - "integrity": "sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw==", - "dev": true - }, "seq-queue": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", @@ -4211,12 +3423,6 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, "slice-ansi": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", @@ -4485,12 +3691,6 @@ "integrity": "sha512-DBp0lSvX5G9KGRDTkR/R+a29H+Wk2xItOF+MpZLLNDWbEV9tGPnqLPxHEYjmiz8xGtJHRIqmI+hCjmNzqoA4nQ==", "dev": true }, - "string-argv": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", - "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==", - "dev": true - }, "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", @@ -4528,17 +3728,6 @@ "safe-buffer": "~5.1.0" } }, - "stringify-object": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", - "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", - "dev": true, - "requires": { - "get-own-enumerable-property-symbols": "^3.0.0", - "is-obj": "^1.0.1", - "is-regexp": "^1.0.0" - } - }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", @@ -4547,12 +3736,6 @@ "ansi-regex": "^2.0.0" } }, - "strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true - }, "strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", @@ -4704,12 +3887,6 @@ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", "dev": true }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true - }, "tildify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/tildify/-/tildify-2.0.0.tgz", @@ -4784,12 +3961,6 @@ "yargs": "^15.3.1" } }, - "tslib": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", - "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", - "dev": true - }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -4977,12 +4148,6 @@ "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" }, - "which-pm-runs": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz", - "integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=", - "dev": true - }, "wide-align": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", @@ -5110,12 +4275,6 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, - "yaml": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz", - "integrity": "sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==", - "dev": true - }, "yargs": { "version": "15.4.1", "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", diff --git a/package.json b/package.json index 292222b..b6d9788 100644 --- a/package.json +++ b/package.json @@ -40,20 +40,10 @@ }, "devDependencies": { "eslint": "^7.7.0", - "husky": "^4.2.5", "jsdoc-to-markdown": "^6.0.1", - "lint-staged": "^10.2.11", "supervisor": "^0.12.0" }, "engines": { "node": ">=12.0.0 <14.0.0" - }, - "husky": { - "hooks": { - "pre-commit": "lint-staged" - } - }, - "lint-staged": { - "*.js": "eslint --fix" } } From 2fa7ec68664e0b4fdd023988854581875185f401 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 13 Sep 2020 15:27:40 +0300 Subject: [PATCH 121/300] Combine changelogs for v2.31.0-beta.0 and beta.1 --- CHANGELOG.md | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70e441d..d1b3063 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ Please report any bugs you encounter by [creating a GitHub issue](https://github.com/Dragory/modmailbot/issues/new)! **General changes:** -* **BREAKING CHANGE:** Added support for Node.js 14, dropped support for Node.js 10 and 11 +* **BREAKING CHANGE:** Added support for Node.js 13 and 14, dropped support for Node.js 10 and 11 * The supported versions are now 12, 13, and 14 * **BREAKING CHANGE:** The bot now requests the necessary [Gateway Intents](https://discord.com/developers/docs/topics/gateway#gateway-intents) * **This includes the privileged "Server Members Intent"**, which is used for server greetings/welcome messages. @@ -27,8 +27,13 @@ Please report any bugs you encounter by [creating a GitHub issue](https://github * As with `pingOnBotMention`, staff members are automatically ignored * New **default** attachment storage option: `original` * This option simply links the original attachment and does not rehost it in any way +* DM channel and message IDs are now stored + * Use `!loglink -v` to view these in logs + * Use `!dm_channel_id` in an inbox thread to view the DM channel ID + * *DM channel and message IDs are primarily useful for Discord T&S reports* * Multiple people can now sign up for reply alerts (`!alert`) simultaneously ([#373](https://github.com/Dragory/modmailbot/pull/373) by @DarkView) * The bot now displays a note if the user sent an application invite, e.g. an invite to listen along on Spotify +* Log formatting is now more consistent and easier to parse with automated tools * Messages in modmail threads by other bots are no longer ignored, and are displayed in logs * Added official support for MySQL databases. Refer to the documentation on `dbType` for more details. * This change also means the following options are **no longer supported:** @@ -42,27 +47,15 @@ Please report any bugs you encounter by [creating a GitHub issue](https://github * Added support for *hooks*. Hooks can be used to run custom plugin code before/after specific moments. * Initially only the `beforeNewThread` hook is available. See plugin documentation for more details. * If your plugin requires special gateway intents, use the new `extraIntents` config option +* Some code reorganisation related to threads and thread messages. + If you have a plugin that interacts with Thread or ThreadMessage objects, + test them before running this update in production! **Internal/technical updates:** * Updated Eris to v0.13.3 * Updated several other dependencies * New JSON Schema based config parser that validates each option and their types more strictly to prevent undefined behavior -## v2.31.0-beta.0 -This is a beta release. It is not available on the Releases page and bugs are expected. - -* Add support for Node.js 13 - * Support for Node.js 14 is coming in a future update that will also drop Node.js 10 support. - This mirrors the `sqlite3` library's version support. -* Log formatting is now more consistent and easier to parse with automated tools -* DM channel and message IDs are now stored - * Use `!loglink -v` to view these in logs - * Use `!dm_channel_id` in an inbox thread to view the DM channel ID - * *DM channel and message IDs are primarily useful for Discord T&S reports* -* Some code reorganisation related to threads and thread messages. - If you have a plugin that interacts with Thread or ThreadMessage objects, - test them before running this update in production! - ## v2.30.1 * Fix crash with `responseMessage` and `closeMessage` introduced in v2.30.0 ([#369](https://github.com/Dragory/modmailbot/pull/369)) From 909625a48e473eeaa76e56dfc20d37e6f61ab32a Mon Sep 17 00:00:00 2001 From: Miikka <2606411+Dragory@users.noreply.github.com> Date: Sun, 13 Sep 2020 15:58:54 +0300 Subject: [PATCH 122/300] Update setup.md --- docs/setup.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/setup.md b/docs/setup.md index d76728f..16c7b8b 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -16,8 +16,9 @@ To keep it online, you need to keep the bot process running. 2. Invite the created bot to your server 3. Install Node.js 10, 11, or 12 - Node.js 13 is currently not supported -4. Download the latest bot version from [the releases page](https://github.com/Dragory/modmailbot/releases) and extract it to a folder -5. In the bot's folder, make a copy of the file `config.example.ini` and rename the copy to `config.ini` +4. [Download the latest bot release here](https://github.com/Dragory/modmailbot/releases/latest) (click on "Source code (zip)") +5. Extract the downloaded Zip file to a new folder +6. In the bot's folder, make a copy of the file `config.example.ini` and rename the copy to `config.ini` ## Single-server setup In this setup, modmail threads are opened on the main server in a special category. From 8d2e76c7b17b6274cf967b1edc25b81135246963 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Tue, 22 Sep 2020 22:26:08 +0300 Subject: [PATCH 123/300] docs: fix misnamed option, fixes #454 --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 7267f3f..ee4ebb3 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -350,7 +350,7 @@ Object with MySQL-specific options ##### mysqlOptions.port **Default:** `3306` -##### mysqlOptions.username +##### mysqlOptions.user **Default:** *None* Required if using `mysql` for `dbType`. MySQL user to connect with. From 307b9fdee4c08c92c6aad691f9c574f5a2e74582 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Tue, 22 Sep 2020 22:26:21 +0300 Subject: [PATCH 124/300] docs: fix formatting --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index ee4ebb3..ffe09a9 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -327,7 +327,7 @@ extraIntents[] = guildMembers ``` #### dbType -**Default:** `sqlite` +**Default:** `sqlite` Specifies the type of database to use. Valid options: * `sqlite` (see also [sqliteOptions](#sqliteOptions) below) * `mysql` (see also [mysqlOptions](#mysqlOptions) below) From 8d151c1800fdb0857d5bed750cd0874f127df4fb Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Tue, 22 Sep 2020 23:46:56 +0300 Subject: [PATCH 125/300] Fix null values in entity getSQLProps() Fixes 'null' role name when staff member has no hoisted roles --- src/data/ThreadMessage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/ThreadMessage.js b/src/data/ThreadMessage.js index 70e1d21..b05f3d6 100644 --- a/src/data/ThreadMessage.js +++ b/src/data/ThreadMessage.js @@ -42,7 +42,7 @@ class ThreadMessage { getSQLProps() { return Object.entries(this).reduce((obj, [key, value]) => { if (typeof value === "function") return obj; - if (typeof value === "object") { + if (typeof value === "object" && value != null) { obj[key] = JSON.stringify(value); } else { obj[key] = value; From 7227775c273a51e05893d84877337dc9241b2382 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 23 Sep 2020 00:02:14 +0300 Subject: [PATCH 126/300] Use latest node-supervisor version from git Includes improved --inspect support --- package-lock.json | 5 ++--- package.json | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 662250e..7fa3f7a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3742,9 +3742,8 @@ "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" }, "supervisor": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/supervisor/-/supervisor-0.12.0.tgz", - "integrity": "sha1-3n5jNwFbKRhRwQ81OMSn8EkX7ME=", + "version": "github:petruisfan/node-supervisor#fb89a695779770d3cd63b624ef4b1ab2908c105d", + "from": "github:petruisfan/node-supervisor#fb89a695779770d3cd63b624ef4b1ab2908c105d", "dev": true }, "supports-color": { diff --git a/package.json b/package.json index b6d9788..4c9aa29 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "devDependencies": { "eslint": "^7.7.0", "jsdoc-to-markdown": "^6.0.1", - "supervisor": "^0.12.0" + "supervisor": "github:petruisfan/node-supervisor#fb89a695779770d3cd63b624ef4b1ab2908c105d" }, "engines": { "node": ">=12.0.0 <14.0.0" From 0d25e48cd55b223b01f21256bcb0000f02e474d3 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 23 Sep 2020 00:02:55 +0300 Subject: [PATCH 127/300] Add debugger support to watch task 0.0.0.0 used as IP to allow debugging from Windows to WSL2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4c9aa29..f961ef7 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "main": "src/index.js", "scripts": { "start": "node src/index.js", - "watch": "supervisor -n exit -w src src/index.js", + "watch": "supervisor -n exit -w src --inspect=0.0.0.0:9229 src/index.js", "test": "echo \"Error: no test specified\" && exit 1", "lint": "eslint ./src ./db/migrations", "lint-fix": "eslint --fix ./src ./db/migrations", From bf476532f050a8bb452b8d517e9cd8a0a55a8197 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 13 Sep 2020 12:26:57 +0000 Subject: [PATCH 128/300] Bump humanize-duration from 3.12.1 to 3.23.1 Bumps [humanize-duration](https://github.com/EvanHahn/HumanizeDuration.js) from 3.12.1 to 3.23.1. - [Release notes](https://github.com/EvanHahn/HumanizeDuration.js/releases) - [Changelog](https://github.com/EvanHahn/HumanizeDuration.js/blob/master/HISTORY.md) - [Commits](https://github.com/EvanHahn/HumanizeDuration.js/compare/v3.12.1...v3.23.1) Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7fa3f7a..231e580 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1755,9 +1755,9 @@ } }, "humanize-duration": { - "version": "3.12.1", - "resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.12.1.tgz", - "integrity": "sha512-Eu68Xnq5C38391em1zfVy8tiapQrOvTNTlWpax9smHMlEEUcudXrdMfXMoMRyZx4uODowYgi1AYiMzUXEbG+sA==" + "version": "3.23.1", + "resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.23.1.tgz", + "integrity": "sha512-aoOEkomAETmVuQyBx4E7/LfPlC9s8pAA/USl7vFRQpDjepo3aiyvFfOhtXSDqPowdBVPFUZ7onG/KyuolX0qPg==" }, "iconv-lite": { "version": "0.4.24", diff --git a/package.json b/package.json index f961ef7..d629c08 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "dependencies": { "ajv": "^6.12.4", "eris": "^0.13.3", - "humanize-duration": "^3.12.1", + "humanize-duration": "^3.23.1", "ini": "^1.3.5", "json-schema-to-jsdoc": "^1.0.0", "json5": "^2.1.3", From d45bb8d09c1829e5189b4a2e479d15470b2fb895 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 13 Sep 2020 12:26:58 +0000 Subject: [PATCH 129/300] Bump mime from 2.4.4 to 2.4.6 Bumps [mime](https://github.com/broofa/mime) from 2.4.4 to 2.4.6. - [Release notes](https://github.com/broofa/mime/releases) - [Changelog](https://github.com/broofa/mime/blob/master/CHANGELOG.md) - [Commits](https://github.com/broofa/mime/commits) Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 231e580..2328ae4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2438,9 +2438,9 @@ } }, "mime": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", - "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==" + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", + "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==" }, "mime-db": { "version": "1.44.0", diff --git a/package.json b/package.json index d629c08..57d9832 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "json5": "^2.1.3", "knex": "^0.21.4", "knub-command-manager": "^6.1.0", - "mime": "^2.4.4", + "mime": "^2.4.6", "moment": "^2.27.0", "mv": "^2.1.1", "mysql2": "^2.1.0", From 88c2fc2e83db795787bbcd427e2c2c2704f178fa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Sep 2020 06:23:42 +0000 Subject: [PATCH 130/300] Bump knex from 0.21.4 to 0.21.5 Bumps [knex](https://github.com/knex/knex) from 0.21.4 to 0.21.5. - [Release notes](https://github.com/knex/knex/releases) - [Changelog](https://github.com/knex/knex/blob/master/CHANGELOG.md) - [Commits](https://github.com/knex/knex/commits/0.21.5) Signed-off-by: dependabot[bot] --- package-lock.json | 16 +++++++++++----- package.json | 2 +- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2328ae4..5d6008d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2223,9 +2223,9 @@ } }, "knex": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/knex/-/knex-0.21.4.tgz", - "integrity": "sha512-vUrR4mJBKWJPouV9C7kqvle9cTpiuuzBWqrQXP7bAv+Ua9oeKkEhhorJwArzcjVrVBojZYPMMtNVliW9B00sTA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/knex/-/knex-0.21.5.tgz", + "integrity": "sha512-cQj7F2D/fu03eTr6ZzYCYKdB9w7fPYlvTiU/f2OeXay52Pq5PwD+NAkcf40WDnppt/4/4KukROwlMOaE7WArcA==", "requires": { "colorette": "1.2.1", "commander": "^5.1.0", @@ -2235,7 +2235,7 @@ "inherits": "~2.0.4", "interpret": "^2.2.0", "liftoff": "3.1.0", - "lodash": "^4.17.19", + "lodash": "^4.17.20", "mkdirp": "^1.0.4", "pg-connection-string": "2.3.0", "tarn": "^3.0.0", @@ -2249,6 +2249,11 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" + }, "mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -2321,7 +2326,8 @@ "lodash": { "version": "4.17.19", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", + "dev": true }, "lodash.camelcase": { "version": "4.3.0", diff --git a/package.json b/package.json index 57d9832..b2548dd 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "ini": "^1.3.5", "json-schema-to-jsdoc": "^1.0.0", "json5": "^2.1.3", - "knex": "^0.21.4", + "knex": "^0.21.5", "knub-command-manager": "^6.1.0", "mime": "^2.4.6", "moment": "^2.27.0", From 3af5a67c1bba970596b3d643d6812cd3762586e3 Mon Sep 17 00:00:00 2001 From: funkyhippo Date: Mon, 21 Sep 2020 13:44:07 -0700 Subject: [PATCH 131/300] Added anonymizeChannelName configuration option. --- docs/configuration.md | 4 ++++ src/data/cfg.schema.json | 4 ++++ src/data/threads.js | 9 ++++++++- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index ffe09a9..c766605 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -313,6 +313,10 @@ URL to use for attachment and log links. Defaults to `http://IP:PORT`. **Default:** `off` If enabled, mod replies will use their nicknames (on the inbox server) instead of their usernames +#### anonymizeChannelName +**Default:** `off` +If enabled, channel names will be the user's name and discriminator salted with the current time, then hashed to protect the user's privacy + ## Advanced options #### extraIntents diff --git a/src/data/cfg.schema.json b/src/data/cfg.schema.json index 5b1469b..8aa5ba7 100644 --- a/src/data/cfg.schema.json +++ b/src/data/cfg.schema.json @@ -124,6 +124,10 @@ "$ref": "#/definitions/customBoolean", "default": false }, + "anonymizeChannelName": { + "$ref": "#/definitions/customBoolean", + "default": false + }, "ignoreAccidentalThreads": { "$ref": "#/definitions/customBoolean", "default": false diff --git a/src/data/threads.js b/src/data/threads.js index 305f9ec..dbc6142 100644 --- a/src/data/threads.js +++ b/src/data/threads.js @@ -4,6 +4,7 @@ const transliterate = require("transliteration"); const moment = require("moment"); const uuid = require("uuid"); const humanizeDuration = require("humanize-duration"); +const crypto = require("crypto"); const bot = require("../bot"); const knex = require("../knex"); @@ -135,7 +136,13 @@ async function createNewThreadForUser(user, opts = {}) { if (cleanName === "") cleanName = "unknown"; cleanName = cleanName.slice(0, 95); // Make sure the discrim fits - const channelName = `${cleanName}-${user.discriminator}`; + let temporalName = `${cleanName}-${user.discriminator}`; + + if (config.anonymizeChannelName) { + temporalName = crypto.createHash("md5").update(temporalName + new Date()).digest("hex").slice(0, 12); + } + + const channelName = temporalName; console.log(`[NOTE] Creating new thread channel ${channelName}`); From 96e8eae188d34edba595dfc76dfdfd1b5d0ee007 Mon Sep 17 00:00:00 2001 From: Nils <7890309+DarkView@users.noreply.github.com> Date: Tue, 22 Sep 2020 23:19:34 +0200 Subject: [PATCH 132/300] Fully functioning built-in plugin to send system messages on join/leave (#437) Co-authored-by: Miikka <2606411+Dragory@users.noreply.github.com> --- docs/configuration.md | 8 +++++++ src/data/cfg.jsdoc.js | 2 ++ src/data/cfg.schema.json | 10 +++++++++ src/main.js | 4 +++- src/modules/joinLeaveNotification.js | 31 ++++++++++++++++++++++++++++ 5 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 src/modules/joinLeaveNotification.js diff --git a/docs/configuration.md b/docs/configuration.md index c766605..84cec7c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -313,6 +313,14 @@ URL to use for attachment and log links. Defaults to `http://IP:PORT`. **Default:** `off` If enabled, mod replies will use their nicknames (on the inbox server) instead of their usernames +#### notifyOnMainServerLeave +**Default:** `on` +If enabled, a system message will be posted into any open threads if the user leaves a main server + +#### notifyOnMainServerJoin +**Default:** `on` +If enabled, a system message will be posted into any open threads if the user joins a main server + #### anonymizeChannelName **Default:** `off` If enabled, channel names will be the user's name and discriminator salted with the current time, then hashed to protect the user's privacy diff --git a/src/data/cfg.jsdoc.js b/src/data/cfg.jsdoc.js index 1e1658e..200971a 100644 --- a/src/data/cfg.jsdoc.js +++ b/src/data/cfg.jsdoc.js @@ -51,6 +51,8 @@ * @property {boolean} [reactOnSeen=false] * @property {string} [reactOnSeenEmoji="📨"] * @property {boolean} [createThreadOnMention=false] + * @property {boolean} [notifyOnMainServerLeave=true] + * @property {boolean} [notifyOnMainServerJoin=true] * @property {number} [port=8890] * @property {string} [url] * @property {array} [extraIntents=[]] diff --git a/src/data/cfg.schema.json b/src/data/cfg.schema.json index 8aa5ba7..1678530 100644 --- a/src/data/cfg.schema.json +++ b/src/data/cfg.schema.json @@ -296,6 +296,16 @@ "default": false }, + "notifyOnMainServerLeave": { + "$ref": "#/definitions/customBoolean", + "default": true + }, + + "notifyOnMainServerJoin": { + "$ref": "#/definitions/customBoolean", + "default": true + }, + "port": { "type": "number", "maximum": 65535, diff --git a/src/main.js b/src/main.js index b1316a9..70d11e8 100644 --- a/src/main.js +++ b/src/main.js @@ -28,6 +28,7 @@ const version = require("./modules/version"); const newthread = require("./modules/newthread"); const idModule = require("./modules/id"); const alert = require("./modules/alert"); +const joinLeaveNotification = require("./modules/joinLeaveNotification"); const {ACCIDENTAL_THREAD_MESSAGES} = require("./data/constants"); @@ -304,7 +305,8 @@ async function initPlugins() { version, newthread, idModule, - alert + alert, + joinLeaveNotification ]; const plugins = [...builtInPlugins]; diff --git a/src/modules/joinLeaveNotification.js b/src/modules/joinLeaveNotification.js new file mode 100644 index 0000000..970a2f9 --- /dev/null +++ b/src/modules/joinLeaveNotification.js @@ -0,0 +1,31 @@ +const config = require("../cfg"); +const threads = require("../data/threads"); +const utils = require("../utils"); + +module.exports = ({ bot }) => { + // Join Notification: Post a message in the thread if the user joins a main server + if (config.notifyOnMainServerJoin) { + bot.on("guildMemberAdd", async (guild, member) => { + const mainGuilds = utils.getMainGuilds(); + if (! mainGuilds.find(gld => gld.id === guild.id)) return; + + const thread = await threads.findOpenThreadByUserId(member.id); + if (thread != null) { + await thread.postSystemMessage(`***The user joined the guild ${guild.name}.***`); + } + }); + } + + // Leave Notification: Post a message in the thread if the user leaves a main server + if (config.notifyOnMainServerLeave) { + bot.on("guildMemberRemove", async (guild, member) => { + const mainGuilds = utils.getMainGuilds(); + if (! mainGuilds.find(gld => gld.id === guild.id)) return; + + const thread = await threads.findOpenThreadByUserId(member.id); + if (thread != null) { + await thread.postSystemMessage(`***The user left the guild ${guild.name}.***`); + } + }); + } +}; From e2de5b97bd907055ecf8e964e042a179952d717d Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 23 Sep 2020 00:20:55 +0300 Subject: [PATCH 133/300] Documentation clean-up --- docs/configuration.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 84cec7c..c362e2e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -107,6 +107,10 @@ e.g. `!note This is an internal message`. **Default:** `off` If `alwaysReply` is enabled, this option controls whether the auto-reply is anonymous +#### anonymizeChannelName +**Default:** `off` +If enabled, channel names will be the user's name and discriminator salted with the current time, then hashed to protect the user's privacy + #### attachmentStorage **Default:** `original` Controls how attachments in modmail threads are stored. Possible values: @@ -206,6 +210,14 @@ If enabled, mentions the user messaging modmail in the modmail thread's header. **Default:** *None* **Deprecated.** Same as `categoryAutomation.newThread`. +#### notifyOnMainServerJoin +**Default:** `on` +If enabled, a system message will be posted into any open threads if the user joins a main server + +#### notifyOnMainServerLeave +**Default:** `on` +If enabled, a system message will be posted into any open threads if the user leaves a main server + #### pingOnBotMention **Default:** `on` If enabled, the bot will mention staff (see `mentionRole` option) on the inbox server when the bot is mentioned on the main server. @@ -313,18 +325,6 @@ URL to use for attachment and log links. Defaults to `http://IP:PORT`. **Default:** `off` If enabled, mod replies will use their nicknames (on the inbox server) instead of their usernames -#### notifyOnMainServerLeave -**Default:** `on` -If enabled, a system message will be posted into any open threads if the user leaves a main server - -#### notifyOnMainServerJoin -**Default:** `on` -If enabled, a system message will be posted into any open threads if the user joins a main server - -#### anonymizeChannelName -**Default:** `off` -If enabled, channel names will be the user's name and discriminator salted with the current time, then hashed to protect the user's privacy - ## Advanced options #### extraIntents From f46d719f4cd8f0d095104fecd30895760890fa27 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 23 Sep 2020 00:21:44 +0300 Subject: [PATCH 134/300] Code clean-up --- src/data/threads.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/data/threads.js b/src/data/threads.js index dbc6142..9ed03a8 100644 --- a/src/data/threads.js +++ b/src/data/threads.js @@ -136,14 +136,12 @@ async function createNewThreadForUser(user, opts = {}) { if (cleanName === "") cleanName = "unknown"; cleanName = cleanName.slice(0, 95); // Make sure the discrim fits - let temporalName = `${cleanName}-${user.discriminator}`; + let channelName = `${cleanName}-${user.discriminator}`; if (config.anonymizeChannelName) { - temporalName = crypto.createHash("md5").update(temporalName + new Date()).digest("hex").slice(0, 12); + channelName = crypto.createHash("md5").update(channelName + Date.now()).digest("hex").slice(0, 12); } - const channelName = temporalName; - console.log(`[NOTE] Creating new thread channel ${channelName}`); // Figure out which category we should place the thread channel in From 171ad403d998c3ef5f9528e0be034f02064b5f9a Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 23 Sep 2020 00:25:50 +0300 Subject: [PATCH 135/300] Add updateNotificationsForBetaVersions option --- docs/configuration.md | 4 ++++ src/data/cfg.schema.json | 5 +++++ src/data/updates.js | 6 +++--- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index c362e2e..75495c0 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -317,6 +317,10 @@ If enabled, any time a moderator is typing in a modmail thread, the user will se **Default:** `on` If enabled, the bot will automatically check for new bot updates periodically and notify about them at the top of new modmail threads +#### updateNotificationsForBetaVersions +**Default:** `off` +If enabled, update notifications will also be given for new beta versions + #### url **Default:** *None* URL to use for attachment and log links. Defaults to `http://IP:PORT`. diff --git a/src/data/cfg.schema.json b/src/data/cfg.schema.json index 1678530..57a36fd 100644 --- a/src/data/cfg.schema.json +++ b/src/data/cfg.schema.json @@ -265,6 +265,11 @@ "default": true }, + "updateNotificationsForBetaVersions": { + "$ref": "#/definitions/customBoolean", + "default": false + }, + "plugins": { "type": "array", "items": { diff --git a/src/data/updates.js b/src/data/updates.js index 02b8cf1..46ef8c2 100644 --- a/src/data/updates.js +++ b/src/data/updates.js @@ -62,10 +62,10 @@ async function refreshVersions() { const parsed = JSON.parse(data); if (! Array.isArray(parsed) || parsed.length === 0) return; - const latestStableRelease = parsed.find(r => ! r.prerelease && ! r.draft); - if (! latestStableRelease) return; + const latestMatchingRelease = parsed.find(r => ! r.draft && (config.updateNotificationsForBetaVersions || ! r.prerelease)); + if (! latestMatchingRelease) return; - const latestVersion = latestStableRelease.name; + const latestVersion = latestMatchingRelease.name; await knex("updates").update({ available_version: latestVersion, last_checked: moment.utc().format("YYYY-MM-DD HH:mm:ss") From 180f936bc4d99abb90738f830b333bbbfddb0db2 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 23 Sep 2020 00:26:36 +0300 Subject: [PATCH 136/300] Update config jsdoc --- src/data/cfg.jsdoc.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/data/cfg.jsdoc.js b/src/data/cfg.jsdoc.js index 200971a..d04ea58 100644 --- a/src/data/cfg.jsdoc.js +++ b/src/data/cfg.jsdoc.js @@ -21,6 +21,7 @@ * @property {boolean} [alwaysReply=false] * @property {boolean} [alwaysReplyAnon=false] * @property {boolean} [useNicknames=false] + * @property {boolean} [anonymizeChannelName=false] * @property {boolean} [ignoreAccidentalThreads=false] * @property {boolean} [threadTimestamps=false] * @property {boolean} [allowMove=false] @@ -46,6 +47,7 @@ * @property {string} [attachmentStorageChannelId] * @property {*} [categoryAutomation={}] * @property {boolean} [updateNotifications=true] + * @property {boolean} [updateNotificationsForBetaVersions=false] * @property {array} [plugins=[]] * @property {*} [commandAliases] * @property {boolean} [reactOnSeen=false] From bf47fb7406bdaa6eb6608c0fea8a403379e37a15 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 23 Sep 2020 00:38:46 +0300 Subject: [PATCH 137/300] Add 'none' option for mentionRole --- docs/configuration.md | 2 +- src/utils.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 75495c0..8ac6cd1 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -198,7 +198,7 @@ Alias for [inboxServerId](#inboxServerId) #### mentionRole **Default:** `here` **Accepts multiple values.** Role that is mentioned when new threads are created or the bot is mentioned. -Accepted values are "here", "everyone", or a role id. +Accepted values are `none`, `here`, `everyone`, or a role id. Requires `pingOnBotMention` to be enabled. Set to an empty value (`mentionRole=`) to disable these pings entirely. diff --git a/src/utils.js b/src/utils.js index 1d5f238..37be29a 100644 --- a/src/utils.js +++ b/src/utils.js @@ -271,7 +271,7 @@ function getInboxMentionAllowedMentions() { }; for (const role of mentionRoles) { - if (role == null) continue; + if (role == null || role === "none") continue; else if (role === "here" || role === "everyone") allowedMentions.everyone = true; else allowedMentions.roles.push(role); } From 4ea5650289dd99ab881cd5e5df2754f52bc8c926 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 23 Sep 2020 00:39:56 +0300 Subject: [PATCH 138/300] Consider an empty value for mentionRole as 'none' --- src/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.js b/src/utils.js index 37be29a..47b92d3 100644 --- a/src/utils.js +++ b/src/utils.js @@ -271,7 +271,7 @@ function getInboxMentionAllowedMentions() { }; for (const role of mentionRoles) { - if (role == null || role === "none") continue; + if (role == null || role === "none" || role === "") continue; else if (role === "here" || role === "everyone") allowedMentions.everyone = true; else allowedMentions.roles.push(role); } From 0c9302b41b29b8e206c8d1d141d55f51627fd90a Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 23 Sep 2020 00:58:18 +0300 Subject: [PATCH 139/300] Add statusType option --- docs/configuration.md | 6 +++++- src/data/cfg.jsdoc.js | 1 + src/data/cfg.schema.json | 5 +++++ src/main.js | 11 ++++++++++- 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 8ac6cd1..b1ed910 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -290,7 +290,11 @@ Prefix to use snippets anonymously #### status **Default:** `Message me for help` -The bot's "Playing" text +The bot's status text. Set to an empty value - `status = ""` - to disable. + +#### statusType +**Default:** `playing` +The bot's status type. One of `playing`, `watching`, `listening`. #### syncPermissionsOnMove **Default:** `on` diff --git a/src/data/cfg.jsdoc.js b/src/data/cfg.jsdoc.js index d04ea58..7b86312 100644 --- a/src/data/cfg.jsdoc.js +++ b/src/data/cfg.jsdoc.js @@ -10,6 +10,7 @@ * @property {string} [snippetPrefix="!!"] * @property {string} [snippetPrefixAnon="!!!"] * @property {string} [status="Message me for help!"] + * @property {"playing"|"watching"|"listening"} [statusType="playing"] * @property {string} [responseMessage="Thank you for your message! Our mod team will reply to you here as soon as possible."] * @property {string} [closeMessage] * @property {boolean} [allowUserClose=false] diff --git a/src/data/cfg.schema.json b/src/data/cfg.schema.json index 57a36fd..697d2f4 100644 --- a/src/data/cfg.schema.json +++ b/src/data/cfg.schema.json @@ -81,6 +81,11 @@ "type": "string", "default": "Message me for help!" }, + "statusType": { + "type": "string", + "enum": ["playing", "watching", "listening"], + "default": "playing" + }, "responseMessage": { "$ref": "#/definitions/multilineString", "default": "Thank you for your message! Our mod team will reply to you here as soon as possible." diff --git a/src/main.js b/src/main.js index 70d11e8..02e8cd2 100644 --- a/src/main.js +++ b/src/main.js @@ -103,7 +103,16 @@ function waitForGuild(guildId) { function initStatus() { function applyStatus() { - bot.editStatus(null, {name: config.status}); + const type = { + "playing": 0, + "watching": 3, + "listening": 2, + }[config.statusType] || 0; + bot.editStatus(null, {name: config.status, type}); + } + + if (config.status == null || config.status === "") { + return; } // Set the bot status initially, then reapply it every hour since in some cases it gets unset From 9be6b2aa1f2571fdbfd5c738811179b7108d1987 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 23 Sep 2020 01:04:40 +0300 Subject: [PATCH 140/300] Don't allow opening threads with bots with !newthread, fixes #452 --- src/modules/newthread.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/modules/newthread.js b/src/modules/newthread.js index 22bae22..79f5e97 100644 --- a/src/modules/newthread.js +++ b/src/modules/newthread.js @@ -9,6 +9,11 @@ module.exports = ({ bot, knex, config, commands }) => { return; } + if (user.bot) { + utils.postSystemMessageWithFallback(msg.channel, thread, "Can't create a thread for a bot"); + return; + } + const existingThread = await threads.findOpenThreadByUserId(user.id); if (existingThread) { utils.postSystemMessageWithFallback(msg.channel, thread, `Cannot create a new thread; there is another open thread with this user: <#${existingThread.channel_id}>`); From f5b6e4604024ef377351e7025385d83e5806dd19 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 23 Sep 2020 02:28:41 +0300 Subject: [PATCH 141/300] Add afterThreadClose plugin hook --- src/data/Thread.js | 3 +++ src/hooks/afterThreadClose.js | 46 +++++++++++++++++++++++++++++++++++ src/pluginApi.js | 1 + src/plugins.js | 2 ++ 4 files changed, 52 insertions(+) create mode 100644 src/hooks/afterThreadClose.js diff --git a/src/data/Thread.js b/src/data/Thread.js index 4ed6f8c..087c997 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -7,6 +7,7 @@ const utils = require("../utils"); const config = require("../cfg"); const attachments = require("./attachments"); const { formatters } = require("../formatters"); +const { callAfterThreadCloseHooks } = require("../hooks/afterThreadClose"); const ThreadMessage = require("./ThreadMessage"); @@ -517,6 +518,8 @@ class Thread { console.log(`Deleting channel ${this.channel_id}`); await channel.delete("Thread closed"); } + + await callAfterThreadCloseHooks({ threadId: this.id }); } /** diff --git a/src/hooks/afterThreadClose.js b/src/hooks/afterThreadClose.js new file mode 100644 index 0000000..9dd4e1d --- /dev/null +++ b/src/hooks/afterThreadClose.js @@ -0,0 +1,46 @@ +const Eris = require("eris"); + +/** + * @typedef AfterThreadCloseHookData + * @property {string} threadId + */ + +/** + * @callback AfterThreadCloseHookFn + * @param {AfterThreadCloseHookData} data + * @return {void|Promise} + */ + +/** + * @callback AddAfterThreadCloseHookFn + * @param {AfterThreadCloseHookFn} fn + * @return {void} + */ + +/** + * @type AfterThreadCloseHookFn[] + */ +const afterThreadCloseHooks = []; + +/** + * @type {AddAfterThreadCloseHookFn} + */ +let afterThreadClose; // Workaround to inconsistent IDE bug with @type and anonymous functions +afterThreadClose = (fn) => { + afterThreadCloseHooks.push(fn); +}; + +/** + * @param {AfterThreadCloseHookData} input + * @return {Promise} + */ +async function callAfterThreadCloseHooks(input) { + for (const hook of afterThreadCloseHooks) { + await hook(input); + } +} + +module.exports = { + afterThreadClose, + callAfterThreadCloseHooks, +}; diff --git a/src/pluginApi.js b/src/pluginApi.js index 65d8227..2e4b381 100644 --- a/src/pluginApi.js +++ b/src/pluginApi.js @@ -31,4 +31,5 @@ const Knex = require("knex"); /** * @typedef {object} PluginHooksAPI * @property {AddBeforeNewThreadHookFn} beforeNewThread + * @property {AddAfterThreadCloseHookFn} afterThreadClose */ diff --git a/src/plugins.js b/src/plugins.js index f27cf50..71bcf28 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -1,5 +1,6 @@ const attachments = require("./data/attachments"); const { beforeNewThread } = require("./hooks/beforeNewThread"); +const { afterThreadClose } = require("./hooks/afterThreadClose"); const formats = require("./formatters"); module.exports = { @@ -28,6 +29,7 @@ module.exports = { }, hooks: { beforeNewThread, + afterThreadClose, }, formats, }; From 5c6df913bf31b482de8d95e86620d3ea92e678f2 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 23 Sep 2020 02:29:09 +0300 Subject: [PATCH 142/300] beforeNewThread hook type fixes --- src/hooks/beforeNewThread.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/hooks/beforeNewThread.js b/src/hooks/beforeNewThread.js index e0173aa..bfde8bd 100644 --- a/src/hooks/beforeNewThread.js +++ b/src/hooks/beforeNewThread.js @@ -21,19 +21,19 @@ const Eris = require("eris"); */ /** - * @callback BeforeNewThreadHookData + * @callback BeforeNewThreadHookFn * @param {BeforeNewThreadHookData} data * @return {void|Promise} */ /** * @callback AddBeforeNewThreadHookFn - * @param {BeforeNewThreadHookData} fn + * @param {BeforeNewThreadHookFn} fn * @return {void} */ /** - * @type BeforeNewThreadHookData[] + * @type BeforeNewThreadHookFn[] */ const beforeNewThreadHooks = []; From 0d29859dd86075c18039e7b85f97934b7973ab21 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 23 Sep 2020 02:30:14 +0300 Subject: [PATCH 143/300] Remove Feb 2018 legacy migrator This is to allow other features to use the /logs folder. Going forward, updating from a pre-Feb 2018 version of the bot will require first updating the bot to version v2.30.1 and running it once. After that, updating to newer versions is possible. --- src/index.js | 26 ---- src/legacy/jsonDb.js | 71 ----------- src/legacy/legacyMigrator.js | 222 ----------------------------------- 3 files changed, 319 deletions(-) delete mode 100644 src/legacy/jsonDb.js delete mode 100644 src/legacy/legacyMigrator.js diff --git a/src/index.js b/src/index.js index cde9e0e..73a4b29 100644 --- a/src/index.js +++ b/src/index.js @@ -45,7 +45,6 @@ const config = require("./cfg"); const utils = require("./utils"); const main = require("./main"); const knex = require("./knex"); -const legacyMigrator = require("./legacy/legacyMigrator"); // Force crash on unhandled rejections (use something like forever/pm2 to restart) process.on("unhandledRejection", err => { @@ -68,31 +67,6 @@ process.on("unhandledRejection", err => { console.log("Done!"); } - // Migrate legacy data if we need to - if (await legacyMigrator.shouldMigrate()) { - console.log("=== MIGRATING LEGACY DATA ==="); - console.log("Do not close the bot!"); - console.log(""); - - await legacyMigrator.migrate(); - - const relativeDbDir = (path.isAbsolute(config.dbDir) ? config.dbDir : path.resolve(process.cwd(), config.dbDir)); - const relativeLogDir = (path.isAbsolute(config.logDir) ? config.logDir : path.resolve(process.cwd(), config.logDir)); - - console.log(""); - console.log("=== LEGACY DATA MIGRATION FINISHED ==="); - console.log(""); - console.log("IMPORTANT: After the bot starts, please verify that all logs, threads, blocked users, and snippets are still working correctly."); - console.log("Once you've done that, the following files/directories are no longer needed. I would recommend keeping a backup of them, however."); - console.log(""); - console.log("FILE: " + path.resolve(relativeDbDir, "threads.json")); - console.log("FILE: " + path.resolve(relativeDbDir, "blocked.json")); - console.log("FILE: " + path.resolve(relativeDbDir, "snippets.json")); - console.log("DIRECTORY: " + relativeLogDir); - console.log(""); - console.log("Starting the bot..."); - } - // Start the bot main.start(); })(); diff --git a/src/legacy/jsonDb.js b/src/legacy/jsonDb.js deleted file mode 100644 index 1c534e9..0000000 --- a/src/legacy/jsonDb.js +++ /dev/null @@ -1,71 +0,0 @@ -const fs = require("fs"); -const path = require("path"); -const config = require("../cfg"); - -const dbDir = config.dbDir; - -const databases = {}; - -/** - * @deprecated Only used for migrating legacy data - */ -class JSONDB { - constructor(path, def = {}, useCloneByDefault = false) { - this.path = path; - this.useCloneByDefault = useCloneByDefault; - - this.load = new Promise(resolve => { - fs.readFile(path, {encoding: "utf8"}, (err, data) => { - if (err) return resolve(def); - - let unserialized; - try { unserialized = JSON.parse(data); } - catch (e) { unserialized = def; } - - resolve(unserialized); - }); - }); - } - - get(clone) { - if (clone == null) clone = this.useCloneByDefault; - - return this.load.then(data => { - if (clone) return JSON.parse(JSON.stringify(data)); - else return data; - }); - } - - save(newData) { - const serialized = JSON.stringify(newData); - this.load = new Promise((resolve, reject) => { - fs.writeFile(this.path, serialized, {encoding: "utf8"}, () => { - resolve(newData); - }); - }); - - return this.get(); - } -} - -function getDb(dbName, def) { - if (! databases[dbName]) { - const dbPath = path.resolve(dbDir, `${dbName}.json`); - databases[dbName] = new JSONDB(dbPath, def); - } - - return databases[dbName]; -} - -function get(dbName, def) { - return getDb(dbName, def).get(); -} - -function save(dbName, data) { - return getDb(dbName, data).save(data); -} - -module.exports = { - get, - save, -}; diff --git a/src/legacy/legacyMigrator.js b/src/legacy/legacyMigrator.js deleted file mode 100644 index 18720ec..0000000 --- a/src/legacy/legacyMigrator.js +++ /dev/null @@ -1,222 +0,0 @@ -const fs = require("fs"); -const path = require("path"); -const promisify = require("util").promisify; -const moment = require("moment"); -const Eris = require("eris"); - -const knex = require("../knex"); -const config = require("../cfg"); -const jsonDb = require("./jsonDb"); -const threads = require("../data/threads"); - -const {THREAD_STATUS, THREAD_MESSAGE_TYPE} = require("../data/constants"); - -const readDir = promisify(fs.readdir); -const readFile = promisify(fs.readFile); -const access = promisify(fs.access); -const writeFile = promisify(fs.writeFile); - -async function migrate() { - console.log("Migrating open threads..."); - await migrateOpenThreads(); - - console.log("Migrating logs..."); - await migrateLogs(); - - console.log("Migrating blocked users..."); - await migrateBlockedUsers(); - - console.log("Migrating snippets..."); - await migrateSnippets(); - - await writeFile(path.join(config.dbDir, ".migrated_legacy"), ""); -} - -async function shouldMigrate() { - // If there is a file marking a finished migration, assume we don't need to migrate - const migrationFile = path.join(config.dbDir, ".migrated_legacy"); - try { - await access(migrationFile); - return false; - } catch (e) {} - - // If there are any old threads, we need to migrate - const oldThreads = await jsonDb.get("threads", []); - if (oldThreads.length) { - return true; - } - - // If there are any old blocked users, we need to migrate - const blockedUsers = await jsonDb.get("blocked", []); - if (blockedUsers.length) { - return true; - } - - // If there are any old snippets, we need to migrate - const snippets = await jsonDb.get("snippets", {}); - if (Object.keys(snippets).length) { - return true; - } - - // If the log file dir exists and has logs in it, we need to migrate - try { - const files = await readDir(config.logDir); - if (files.length > 1) return true; // > 1, since .gitignore is one of them - } catch(e) {} - - return false; -} - -async function migrateOpenThreads() { - const bot = new Eris.Client(config.token); - - const toReturn = new Promise(resolve => { - bot.on("ready", async () => { - const oldThreads = await jsonDb.get("threads", []); - - const promises = oldThreads.map(async oldThread => { - const existingOpenThread = await knex("threads") - .where("channel_id", oldThread.channelId) - .first(); - - if (existingOpenThread) return; - - const oldChannel = bot.getChannel(oldThread.channelId); - if (! oldChannel) return; - - const threadMessages = await oldChannel.getMessages(1000); - const log = threadMessages.reverse().map(msg => { - const date = moment.utc(msg.timestamp, "x").format("YYYY-MM-DD HH:mm:ss"); - return `[${date}] ${msg.author.username}#${msg.author.discriminator}: ${msg.content}`; - }).join("\n") + "\n"; - - const newThread = { - status: THREAD_STATUS.OPEN, - user_id: oldThread.userId, - user_name: oldThread.username, - channel_id: oldThread.channelId, - is_legacy: 1 - }; - - const threadId = await threads.createThreadInDB(newThread); - - await knex("thread_messages").insert({ - thread_id: threadId, - message_type: THREAD_MESSAGE_TYPE.LEGACY, - user_id: oldThread.userId, - user_name: "", - body: log, - is_anonymous: 0, - created_at: moment.utc().format("YYYY-MM-DD HH:mm:ss") - }); - }); - - resolve(Promise.all(promises)); - }); - - bot.connect(); - }); - - await toReturn; - - bot.disconnect(); -} - -async function migrateLogs() { - const logDir = config.logDir || `${__dirname}/../../logs`; - const logFiles = await readDir(logDir); - - for (let i = 0; i < logFiles.length; i++) { - const logFile = logFiles[i]; - if (! logFile.endsWith(".txt")) continue; - - const [rawDate, userId, threadId] = logFile.slice(0, -4).split("__"); - const date = `${rawDate.slice(0, 10)} ${rawDate.slice(11).replace("-", ":")}`; - - const fullPath = path.join(logDir, logFile); - const contents = await readFile(fullPath, {encoding: "utf8"}); - - const newThread = { - id: threadId, - status: THREAD_STATUS.CLOSED, - user_id: userId, - user_name: "", - channel_id: null, - is_legacy: 1, - created_at: date - }; - - await knex.transaction(async trx => { - const existingThread = await trx("threads") - .where("id", newThread.id) - .first(); - - if (existingThread) return; - - await trx("threads").insert(newThread); - - await trx("thread_messages").insert({ - thread_id: newThread.id, - message_type: THREAD_MESSAGE_TYPE.LEGACY, - user_id: userId, - user_name: "", - body: contents, - is_anonymous: 0, - created_at: date - }); - }); - - // Progress indicator for servers with tons of logs - if ((i + 1) % 500 === 0) { - console.log(` ${i + 1}...`); - } - } -} - -async function migrateBlockedUsers() { - const now = moment.utc().format("YYYY-MM-DD HH:mm:ss"); - const blockedUsers = await jsonDb.get("blocked", []); - - for (const userId of blockedUsers) { - const existingBlockedUser = await knex("blocked_users") - .where("user_id", userId) - .first(); - - if (existingBlockedUser) return; - - await knex("blocked_users").insert({ - user_id: userId, - user_name: "", - blocked_by: null, - blocked_at: now - }); - } -} - -async function migrateSnippets() { - const now = moment.utc().format("YYYY-MM-DD HH:mm:ss"); - const snippets = await jsonDb.get("snippets", {}); - - const promises = Object.entries(snippets).map(async ([trigger, data]) => { - const existingSnippet = await knex("snippets") - .where("trigger", trigger) - .first(); - - if (existingSnippet) return; - - return knex("snippets").insert({ - trigger, - body: data.text, - is_anonymous: data.isAnonymous ? 1 : 0, - created_by: null, - created_at: now - }); - }); - - return Promise.all(promises); -} - -module.exports = { - migrate, - shouldMigrate, -}; From a7e863da6aef2c4d8cc576cdb00cda24f8482a4c Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 23 Sep 2020 02:34:35 +0300 Subject: [PATCH 144/300] Move migration files within src This means that the db folder no longer contains any code required for the bot to run. --- {db => src/data}/migrations/20171223203915_create_tables.js | 0 .../data}/migrations/20180224235946_add_close_at_to_threads.js | 0 .../data}/migrations/20180421161550_add_alert_id_to_threads.js | 0 .../20180920224224_remove_is_anonymous_from_snippets.js | 0 .../20190306204728_add_scheduled_close_silent_to_threads.js | 0 .../20190306211534_add_scheduled_suspend_to_threads.js | 0 .../data}/migrations/20190609161116_create_updates_table.js | 0 .../20190609193213_add_expires_at_to_blocked_users.js | 0 .../migrations/20191206002418_add_number_to_thread_messages.js | 0 .../20191206011638_add_thread_message_id_to_thread_messages.js | 0 .../20191206180113_add_dm_channel_id_to_thread_messages.js | 0 .../migrations/20200813230319_separate_message_components.js | 0 .../20200814011007_add_next_message_number_to_threads.js | 0 .../migrations/20200818214801_change_alert_id_to_alert_ids.js | 0 src/knexConfig.js | 2 +- 15 files changed, 1 insertion(+), 1 deletion(-) rename {db => src/data}/migrations/20171223203915_create_tables.js (100%) rename {db => src/data}/migrations/20180224235946_add_close_at_to_threads.js (100%) rename {db => src/data}/migrations/20180421161550_add_alert_id_to_threads.js (100%) rename {db => src/data}/migrations/20180920224224_remove_is_anonymous_from_snippets.js (100%) rename {db => src/data}/migrations/20190306204728_add_scheduled_close_silent_to_threads.js (100%) rename {db => src/data}/migrations/20190306211534_add_scheduled_suspend_to_threads.js (100%) rename {db => src/data}/migrations/20190609161116_create_updates_table.js (100%) rename {db => src/data}/migrations/20190609193213_add_expires_at_to_blocked_users.js (100%) rename {db => src/data}/migrations/20191206002418_add_number_to_thread_messages.js (100%) rename {db => src/data}/migrations/20191206011638_add_thread_message_id_to_thread_messages.js (100%) rename {db => src/data}/migrations/20191206180113_add_dm_channel_id_to_thread_messages.js (100%) rename {db => src/data}/migrations/20200813230319_separate_message_components.js (100%) rename {db => src/data}/migrations/20200814011007_add_next_message_number_to_threads.js (100%) rename {db => src/data}/migrations/20200818214801_change_alert_id_to_alert_ids.js (100%) diff --git a/db/migrations/20171223203915_create_tables.js b/src/data/migrations/20171223203915_create_tables.js similarity index 100% rename from db/migrations/20171223203915_create_tables.js rename to src/data/migrations/20171223203915_create_tables.js diff --git a/db/migrations/20180224235946_add_close_at_to_threads.js b/src/data/migrations/20180224235946_add_close_at_to_threads.js similarity index 100% rename from db/migrations/20180224235946_add_close_at_to_threads.js rename to src/data/migrations/20180224235946_add_close_at_to_threads.js diff --git a/db/migrations/20180421161550_add_alert_id_to_threads.js b/src/data/migrations/20180421161550_add_alert_id_to_threads.js similarity index 100% rename from db/migrations/20180421161550_add_alert_id_to_threads.js rename to src/data/migrations/20180421161550_add_alert_id_to_threads.js diff --git a/db/migrations/20180920224224_remove_is_anonymous_from_snippets.js b/src/data/migrations/20180920224224_remove_is_anonymous_from_snippets.js similarity index 100% rename from db/migrations/20180920224224_remove_is_anonymous_from_snippets.js rename to src/data/migrations/20180920224224_remove_is_anonymous_from_snippets.js diff --git a/db/migrations/20190306204728_add_scheduled_close_silent_to_threads.js b/src/data/migrations/20190306204728_add_scheduled_close_silent_to_threads.js similarity index 100% rename from db/migrations/20190306204728_add_scheduled_close_silent_to_threads.js rename to src/data/migrations/20190306204728_add_scheduled_close_silent_to_threads.js diff --git a/db/migrations/20190306211534_add_scheduled_suspend_to_threads.js b/src/data/migrations/20190306211534_add_scheduled_suspend_to_threads.js similarity index 100% rename from db/migrations/20190306211534_add_scheduled_suspend_to_threads.js rename to src/data/migrations/20190306211534_add_scheduled_suspend_to_threads.js diff --git a/db/migrations/20190609161116_create_updates_table.js b/src/data/migrations/20190609161116_create_updates_table.js similarity index 100% rename from db/migrations/20190609161116_create_updates_table.js rename to src/data/migrations/20190609161116_create_updates_table.js diff --git a/db/migrations/20190609193213_add_expires_at_to_blocked_users.js b/src/data/migrations/20190609193213_add_expires_at_to_blocked_users.js similarity index 100% rename from db/migrations/20190609193213_add_expires_at_to_blocked_users.js rename to src/data/migrations/20190609193213_add_expires_at_to_blocked_users.js diff --git a/db/migrations/20191206002418_add_number_to_thread_messages.js b/src/data/migrations/20191206002418_add_number_to_thread_messages.js similarity index 100% rename from db/migrations/20191206002418_add_number_to_thread_messages.js rename to src/data/migrations/20191206002418_add_number_to_thread_messages.js diff --git a/db/migrations/20191206011638_add_thread_message_id_to_thread_messages.js b/src/data/migrations/20191206011638_add_thread_message_id_to_thread_messages.js similarity index 100% rename from db/migrations/20191206011638_add_thread_message_id_to_thread_messages.js rename to src/data/migrations/20191206011638_add_thread_message_id_to_thread_messages.js diff --git a/db/migrations/20191206180113_add_dm_channel_id_to_thread_messages.js b/src/data/migrations/20191206180113_add_dm_channel_id_to_thread_messages.js similarity index 100% rename from db/migrations/20191206180113_add_dm_channel_id_to_thread_messages.js rename to src/data/migrations/20191206180113_add_dm_channel_id_to_thread_messages.js diff --git a/db/migrations/20200813230319_separate_message_components.js b/src/data/migrations/20200813230319_separate_message_components.js similarity index 100% rename from db/migrations/20200813230319_separate_message_components.js rename to src/data/migrations/20200813230319_separate_message_components.js diff --git a/db/migrations/20200814011007_add_next_message_number_to_threads.js b/src/data/migrations/20200814011007_add_next_message_number_to_threads.js similarity index 100% rename from db/migrations/20200814011007_add_next_message_number_to_threads.js rename to src/data/migrations/20200814011007_add_next_message_number_to_threads.js diff --git a/db/migrations/20200818214801_change_alert_id_to_alert_ids.js b/src/data/migrations/20200818214801_change_alert_id_to_alert_ids.js similarity index 100% rename from db/migrations/20200818214801_change_alert_id_to_alert_ids.js rename to src/data/migrations/20200818214801_change_alert_id_to_alert_ids.js diff --git a/src/knexConfig.js b/src/knexConfig.js index e72a52d..3510afe 100644 --- a/src/knexConfig.js +++ b/src/knexConfig.js @@ -31,7 +31,7 @@ module.exports = { useNullAsDefault: true, migrations: { - directory: path.resolve(__dirname, "..", "db", "migrations"), + directory: path.resolve(__dirname, "data", "migrations"), }, log: { warn(message) { From 3a7f7ffc9001c24637080790630a044af4a780e5 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 23 Sep 2020 03:16:26 +0300 Subject: [PATCH 145/300] Add support for alternative log storage types --- docs/configuration.md | 19 ++++ docs/plugin-api.md | 18 ++++ docs/plugins.md | 20 +++++ src/cfg.js | 4 + src/data/Thread.js | 7 -- src/data/cfg.jsdoc.js | 4 + src/data/cfg.schema.json | 20 +++++ src/data/logs.js | 184 +++++++++++++++++++++++++++++++++++++++ src/modules/close.js | 48 ++++++---- src/modules/logs.js | 65 ++++++++------ src/pluginApi.js | 10 +++ src/plugins.js | 8 ++ src/utils.js | 2 +- 13 files changed, 356 insertions(+), 53 deletions(-) create mode 100644 src/data/logs.js diff --git a/docs/configuration.md b/docs/configuration.md index b1ed910..52c1d77 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -189,6 +189,25 @@ See ["Permissions" on this page](https://abal.moe/Eris/docs/reference) for suppo **Default:** `You haven't been a member of the server for long enough to contact modmail.` If `requiredTimeOnServer` is set, users that are too new will be sent this message if they try to message modmail. +#### logStorage +**Default:** `local` +Controls how logs are stored. Possible values: +* `local` - Logs are served from a local web server via links +* `attachment` - Logs are sent as attachments +* `none` - Logs are not available through the bot + +#### logOptions +Options for logs + +##### logOptions.attachmentDirectory +**Default:** `logs` +When using `logStorage = "attachment"`, the directory where the log files are stored + +##### logOptions.allowAttachmentUrlFallback +**Default:** `off` +When using `logStorage = "attachment"`, if enabled, threads that don't have a log file will send a log link instead. +Useful if transitioning from `logStorage = "local"` (the default). + #### mainGuildId Alias for [mainServerId](#mainServerId) diff --git a/docs/plugin-api.md b/docs/plugin-api.md index fa0fdee..fa62912 100644 --- a/docs/plugin-api.md +++ b/docs/plugin-api.md @@ -12,6 +12,8 @@ Scroll down to [PluginAPI](#PluginAPI) for a list of properties available to plu

PluginAttachmentsAPI : object
+
PluginLogsAPI : object
+
PluginHooksAPI : object
@@ -29,6 +31,7 @@ Scroll down to [PluginAPI](#PluginAPI) for a list of properties available to plu | config | ModmailConfig | | commands | [PluginCommandsAPI](#PluginCommandsAPI) | | attachments | [PluginAttachmentsAPI](#PluginAttachmentsAPI) | +| logs | [PluginLogsAPI](#PluginLogsAPI) | | hooks | [PluginHooksAPI](#PluginHooksAPI) | | formats | FormattersExport | @@ -57,6 +60,20 @@ Scroll down to [PluginAPI](#PluginAPI) for a list of properties available to plu | addStorageType | AddAttachmentStorageTypeFn | | downloadAttachment | DownloadAttachmentFn | + + +## PluginLogsAPI : object +**Kind**: global typedef +**Properties** + +| Name | Type | +| --- | --- | +| addStorageType | AddLogStorageTypeFn | +| saveLogToStorage | SaveLogToStorageFn | +| getLogUrl | GetLogUrlFn | +| getLogFile | GetLogFileFn | +| getLogCustomResponse | GetLogCustomResponseFn | + ## PluginHooksAPI : object @@ -66,4 +83,5 @@ Scroll down to [PluginAPI](#PluginAPI) for a list of properties available to plu | Name | Type | | --- | --- | | beforeNewThread | AddBeforeNewThreadHookFn | +| afterThreadClose | AddAfterThreadCloseHookFn | diff --git a/docs/plugins.md b/docs/plugins.md index bf60e09..c037e9e 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -34,6 +34,25 @@ module.exports = function({ attachments }) { ``` To use this custom attachment storage type, you would set the `attachmentStorage` config option to `"original"`. +### Example of a custom log storage type +This example adds a custom type for the `logStorage` option called `"pastebin"` that uploads logs to Pastebin. +The code that handles API calls to Pastebin is left out, as it's not relevant to the example. +```js +module.exports = function({ logs, formatters }) { + logs.addStorageType('pastebin', { + async save(thread, threadMessages) { + const formatLogResult = await formatters.formatLog(thread, threadMessages); + // formatLogResult.content contains the log text + // Some code here that uploads the full text to Pastebin, and stores the Pastebin link in the database + }, + + getUrl(threadId) { + // Find the previously-saved Pastebin link from the database based on the thread id, and return it + } + }); +}; +``` + ### Plugin API The first and only argument to the plugin function is an object with the following properties: @@ -44,6 +63,7 @@ The first and only argument to the plugin function is an object with the followi | `config` | The loaded config | | `commands` | An object with functions to add and manage commands | | `attachments` | An object with functions to save attachments and manage attachment storage types | +| `logs` | An object with functions to get attachment URLs/files and manage log storage types | | `hooks` | An object with functions to add *hooks* that are called at specific times, e.g. before a new thread is created | | `formats` | An object with functions that allow you to replace the default functions used for formatting messages and logs | diff --git a/src/cfg.js b/src/cfg.js index 3fcceab..e559d13 100644 --- a/src/cfg.js +++ b/src/cfg.js @@ -121,6 +121,10 @@ if (! config.sqliteOptions) { }; } +if (! config.logOptions) { + config.logOptions = {}; +} + // categoryAutomation.newThreadFromGuild => categoryAutomation.newThreadFromServer if (config.categoryAutomation && config.categoryAutomation.newThreadFromGuild && ! config.categoryAutomation.newThreadFromServer) { config.categoryAutomation.newThreadFromServer = config.categoryAutomation.newThreadFromGuild; diff --git a/src/data/Thread.js b/src/data/Thread.js index 087c997..e3b7b1f 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -727,13 +727,6 @@ class Thread { await this._deleteThreadMessage(threadMessage.id); } - - /** - * @returns {Promise} - */ - getLogUrl() { - return utils.getSelfUrl(`logs/${this.id}`); - } } module.exports = Thread; diff --git a/src/data/cfg.jsdoc.js b/src/data/cfg.jsdoc.js index 7b86312..1d46881 100644 --- a/src/data/cfg.jsdoc.js +++ b/src/data/cfg.jsdoc.js @@ -56,6 +56,10 @@ * @property {boolean} [createThreadOnMention=false] * @property {boolean} [notifyOnMainServerLeave=true] * @property {boolean} [notifyOnMainServerJoin=true] + * @property {string} [logStorage="local"] + * @property {object} [logOptions] + * @property {string} logOptions.attachmentDirectory + * @property {*} [logOptions.allowAttachmentUrlFallback=false] * @property {number} [port=8890] * @property {string} [url] * @property {array} [extraIntents=[]] diff --git a/src/data/cfg.schema.json b/src/data/cfg.schema.json index 697d2f4..96c42fc 100644 --- a/src/data/cfg.schema.json +++ b/src/data/cfg.schema.json @@ -316,6 +316,26 @@ "default": true }, + "logStorage": { + "type": "string", + "default": "local" + }, + + "logOptions": { + "type": "object", + "properties": { + "attachmentDirectory": { + "type": "string", + "default": "logs" + }, + "allowAttachmentUrlFallback": { + "$ref": "#/definitions/customBoolean", + "default": false + } + }, + "required": ["attachmentDirectory"] + }, + "port": { "type": "number", "maximum": 65535, diff --git a/src/data/logs.js b/src/data/logs.js new file mode 100644 index 0000000..a47521e --- /dev/null +++ b/src/data/logs.js @@ -0,0 +1,184 @@ +const Thread = require("./Thread"); +const ThreadMessage = require("./ThreadMessage"); +const utils = require("../utils"); +const config = require("../cfg"); +const { THREAD_STATUS } = require("./constants"); +const path = require("path"); +const fs = require("fs"); +const { formatters } = require("../formatters"); + +/** + * @typedef {object} LogStorageTypeHandler + * @property {LogStorageTypeHandlerSaveFn?} save + * @property {LogStorageTypeHandlerGetUrlFn?} getUrl + * @property {LogStorageTypeHandlerGetFileFn?} getFile + * @property {LogStorageTypeHandlerGetCustomResponseFn?} getCustomResponse + */ + +/** + * @callback LogStorageTypeHandlerSaveFn + * @param {Thread} thread + * @param {ThreadMessage[]} threadMessages + * @return {void|Promise} + */ + +/** + * @callback LogStorageTypeHandlerGetUrlFn + * @param {string} threadId + * @return {string|Promise|null|Promise} + */ + +/** + * @callback LogStorageTypeHandlerGetFileFn + * @param {string} threadId + * @return {Eris.MessageFile|Promise|null|Promise>} + */ + +/** + * @typedef {object} LogStorageTypeHandlerGetCustomResult + * @property {Eris.MessageContent?} content + * @property {Eris.MessageFile?} file + */ + +/** + * @callback LogStorageTypeHandlerGetCustomResponseFn + * @param {string} threadId + * @return {LogStorageTypeHandlerGetCustomResponseResult|Promise|null|Promise>} + */ + +/** + * @callback AddLogStorageTypeFn + * @param {string} name + * @param {LogStorageTypeHandler} handler + */ + +const logStorageTypes = {}; + +/** + * @type AddLogStorageTypeFn + */ +const addStorageType = (name, handler) => { + logStorageTypes[name] = handler; +}; + +/** + * @callback SaveLogToStorageFn + * @param {Thread} thread + * @param {ThreadMessage[]} threadMessages + * @returns {Promise} + */ +/** + * @type {SaveLogToStorageFn} + */ +const saveLogToStorage = async (thread, threadMessages) => { + const { save } = logStorageTypes[config.logStorage]; + if (save) { + await save(thread, threadMessages); + } +}; + +/** + * @callback GetLogUrlFn + * @param {string} threadId + * @returns {Promise} + */ +/** + * @type {GetLogUrlFn} + */ +const getLogUrl = async (threadId) => { + const { getUrl } = logStorageTypes[config.logStorage]; + return getUrl + ? getUrl(threadId) + : null; +}; + +/** + * @callback GetLogFileFn + * @param {string} threadId + * @returns {Promise} + */ +/** + * @type {GetLogFileFn} + */ +const getLogFile = async (threadId) => { + const { getFile } = logStorageTypes[config.logStorage]; + return getFile + ? getFile(threadId) + : null; +}; + +/** + * @callback GetLogCustomResponseFn + * @param {string} threadId + * @returns {Promise} + */ +/** + * @type {GetLogCustomResponseFn} + */ +const getLogCustomResponse = async (threadId) => { + const { getCustomResponse } = logStorageTypes[config.logStorage]; + return getCustomResponse + ? getCustomResponse(threadId) + : null; +}; + +addStorageType("local", { + getUrl(threadId) { + return utils.getSelfUrl(`logs/${threadId}`); + }, +}); + +const getLogAttachmentFilename = threadId => { + const filename = `${threadId}.txt`; + const fullPath = path.resolve(config.logOptions.attachmentDirectory, filename); + + return { filename, fullPath }; +}; + +addStorageType("attachment", { + async save(thread, threadMessages) { + const { fullPath } = getLogAttachmentFilename(thread.id); + + const formatLogResult = await formatters.formatLog(thread, threadMessages); + fs.writeFileSync(fullPath, formatLogResult.content, { encoding: "utf8" }); + }, + + async getUrl(threadId) { + if (! config.logOptions.allowAttachmentUrlFallback) { + return null; + } + + const { fullPath } = getLogAttachmentFilename(threadId); + try { + fs.accessSync(fullPath); + return null; + } catch (e) { + return utils.getSelfUrl(`logs/${threadId}`); + } + }, + + async getFile(threadId) { + const { filename, fullPath } = getLogAttachmentFilename(threadId); + + try { + fs.accessSync(fullPath); + } catch (e) { + return null; + } + + return { + file: fs.readFileSync(fullPath, { encoding: "utf8" }), + name: filename, + }; + } +}); + +addStorageType("none", {}); + +module.exports = { + addStorageType, + saveLogToStorage, + getLogUrl, + getLogFile, + getLogCustomResponse, +}; diff --git a/src/modules/close.js b/src/modules/close.js index 19679aa..eadece2 100644 --- a/src/modules/close.js +++ b/src/modules/close.js @@ -1,12 +1,38 @@ const moment = require("moment"); const Eris = require("eris"); -const config = require("../cfg"); const utils = require("../utils"); const threads = require("../data/threads"); const blocked = require("../data/blocked"); -const {messageQueue} = require("../queue"); +const { messageQueue } = require("../queue"); +const { getLogUrl, getLogFile, getLogCustomResponse } = require("../data/logs"); module.exports = ({ bot, knex, config, commands }) => { + async function sendCloseNotification(threadId, body) { + const logCustomResponse = await getLogCustomResponse(threadId); + if (logCustomResponse) { + await utils.postLog(body); + await utils.postLog(logCustomResponse.content, logCustomResponse.file); + return; + } + + const logUrl = await getLogUrl(threadId); + if (logUrl) { + utils.postLog(utils.trimAll(` + ${body} + Logs: ${logUrl} + `)); + return; + } + + const logFile = await getLogFile(threadId); + if (logFile) { + utils.postLog(body, logFile); + return; + } + + utils.postLog(body); + } + // Check for threads that are scheduled to be closed and close them async function applyScheduledCloses() { const threadsToBeClosed = await threads.getThreadsThatShouldBeClosed(); @@ -18,11 +44,7 @@ module.exports = ({ bot, knex, config, commands }) => { await thread.close(false, thread.scheduled_close_silent); - const logUrl = await thread.getLogUrl(); - utils.postLog(utils.trimAll(` - Modmail thread with ${thread.user_name} (${thread.user_id}) was closed as scheduled by ${thread.scheduled_close_name} - Logs: ${logUrl} - `)); + await sendCloseNotification(thread.id, `Modmail thread with ${thread.user_name} (${thread.user_id}) was closed as scheduled by ${thread.scheduled_close_name}`); } } @@ -121,11 +143,7 @@ module.exports = ({ bot, knex, config, commands }) => { await thread.sendSystemMessageToUser(closeMessage).catch(() => {}); } - const logUrl = await thread.getLogUrl(); - utils.postLog(utils.trimAll(` - Modmail thread with ${thread.user_name} (${thread.user_id}) was closed by ${closedBy} - Logs: ${logUrl} - `)); + await sendCloseNotification(thread.id, `Modmail thread with ${thread.user_name} (${thread.user_id}) was closed by ${closedBy}`); }); // Auto-close threads if their channel is deleted @@ -144,10 +162,6 @@ module.exports = ({ bot, knex, config, commands }) => { await thread.close(true); - const logUrl = await thread.getLogUrl(); - utils.postLog(utils.trimAll(` - Modmail thread with ${thread.user_name} (${thread.user_id}) was closed automatically because the channel was deleted - Logs: ${logUrl} - `)); + await sendCloseNotification(thread.id, `Modmail thread with ${thread.user_name} (${thread.user_id}) was closed automatically because the channel was deleted`); }); }; diff --git a/src/modules/logs.js b/src/modules/logs.js index 61b7729..916a80f 100644 --- a/src/modules/logs.js +++ b/src/modules/logs.js @@ -1,10 +1,11 @@ const threads = require("../data/threads"); const moment = require("moment"); const utils = require("../utils"); +const { getLogUrl, getLogFile, getLogCustomResponse, saveLogToStorage } = require("../data/logs"); const LOG_LINES_PER_PAGE = 10; -module.exports = ({ bot, knex, config, commands }) => { +module.exports = ({ bot, knex, config, commands, hooks }) => { const logsCmd = async (msg, args, thread) => { let userId = args.userId || (thread && thread.user_id); if (! userId) return; @@ -29,9 +30,12 @@ module.exports = ({ bot, knex, config, commands }) => { userThreads = userThreads.slice((page - 1) * LOG_LINES_PER_PAGE, page * LOG_LINES_PER_PAGE); const threadLines = await Promise.all(userThreads.map(async thread => { - const logUrl = await thread.getLogUrl(); + const logUrl = await getLogUrl(thread.id); + const formattedLogUrl = logUrl + ? `<${logUrl}>` + : `View log with \`${config.prefix}log ${thread.id}\`` const formattedDate = moment.utc(thread.created_at).format("MMM Do [at] HH:mm [UTC]"); - return `\`${formattedDate}\`: <${logUrl}>`; + return `\`${formattedDate}\`: ${formattedLogUrl}`; })); let message = isPaginated @@ -54,34 +58,39 @@ module.exports = ({ bot, knex, config, commands }) => { }); }; + const logCmd = async (msg, args, thread) => { + const threadId = args.threadId || (thread && thread.id); + if (! threadId) return; + + const customResponse = await getLogCustomResponse(threadId); + if (customResponse && (customResponse.content || customResponse.file)) { + msg.channel.createMessage(customResponse.content, customResponse.file); + } + + const logUrl = await getLogUrl(threadId); + if (logUrl) { + msg.channel.createMessage(`Open the following link to view the log:\n<${logUrl}>`); + return; + } + + const logFile = await getLogFile(threadId); + if (logFile) { + msg.channel.createMessage("Download the following file to view the log:", logFile); + return; + } + + msg.channel.createMessage("This thread's logs are not currently available"); + }; + 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; - } + commands.addInboxServerCommand("log", "[threadId:string]", logCmd); + commands.addInboxServerCommand("loglink", "[threadId:string]", logCmd); - const logUrl = await thread.getLogUrl(); - const query = []; - if (args.verbose) query.push("verbose=1"); - if (args.simple) query.push("simple=1"); - let qs = query.length ? `?${query.join("&")}` : ""; - - thread.postSystemMessage(`Log URL: ${logUrl}${qs}`); - }, { - options: [ - { - name: "verbose", - shortcut: "v", - isSwitch: true, - }, - { - name: "simple", - shortcut: "s", - isSwitch: true, - }, - ], + hooks.afterThreadClose(async ({ threadId }) => { + const thread = await threads.findById(threadId); + const threadMessages = await thread.getThreadMessages(); + await saveLogToStorage(thread, threadMessages); }); }; diff --git a/src/pluginApi.js b/src/pluginApi.js index 2e4b381..662c832 100644 --- a/src/pluginApi.js +++ b/src/pluginApi.js @@ -9,6 +9,7 @@ const Knex = require("knex"); * @property {ModmailConfig} config * @property {PluginCommandsAPI} commands * @property {PluginAttachmentsAPI} attachments + * @property {PluginLogsAPI} logs * @property {PluginHooksAPI} hooks * @property {FormattersExport} formats */ @@ -28,6 +29,15 @@ const Knex = require("knex"); * @property {DownloadAttachmentFn} downloadAttachment */ +/** + * @typedef {object} PluginLogsAPI + * @property {AddLogStorageTypeFn} addStorageType + * @property {SaveLogToStorageFn} saveLogToStorage + * @property {GetLogUrlFn} getLogUrl + * @property {GetLogFileFn} getLogFile + * @property {GetLogCustomResponseFn} getLogCustomResponse + */ + /** * @typedef {object} PluginHooksAPI * @property {AddBeforeNewThreadHookFn} beforeNewThread diff --git a/src/plugins.js b/src/plugins.js index 71bcf28..4de156d 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -1,4 +1,5 @@ const attachments = require("./data/attachments"); +const logs = require("./data/logs"); const { beforeNewThread } = require("./hooks/beforeNewThread"); const { afterThreadClose } = require("./hooks/afterThreadClose"); const formats = require("./formatters"); @@ -27,6 +28,13 @@ module.exports = { addStorageType: attachments.addStorageType, downloadAttachment: attachments.downloadAttachment }, + logs: { + addStorageType: logs.addStorageType, + saveLogToStorage: logs.saveLogToStorage, + getLogUrl: logs.getLogUrl, + getLogFile: logs.getLogFile, + getLogCustomResponse: logs.getLogCustomResponse, + }, hooks: { beforeNewThread, afterThreadClose, diff --git a/src/utils.js b/src/utils.js index 47b92d3..d97447a 100644 --- a/src/utils.js +++ b/src/utils.js @@ -61,7 +61,7 @@ function getLogChannel() { } function postLog(...args) { - getLogChannel().createMessage(...args); + return getLogChannel().createMessage(...args); } function postError(channel, str, opts = {}) { From 4bdcf1e4278ab9adfb5090d18052f3f8673abb75 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 23 Sep 2020 03:43:29 +0300 Subject: [PATCH 146/300] Update CHANGELOG for v2.31.0-beta.2. Update package version. --- CHANGELOG.md | 31 +++++++++++++++++++++++++++++++ package.json | 2 +- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1b3063..b99c70a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,36 @@ # Changelog +## v2.31.0-beta.2 +**This is a beta release, bugs are expected.** +Please report any bugs you encounter by [creating a GitHub issue](https://github.com/Dragory/modmailbot/issues/new)! + +**General changes:** +* **BREAKING CHANGE:** Logs from Modmail versions prior to Feb 2018 are no longer converted automatically + * To update from a Modmail version from before Feb 2018, update to `v2.30.1` and run the bot once first. Then you can update to later versions. +* New option `logStorage` + * Allows changing how logs are stored + * Possible values are `local` (default), `attachment`, and `none` +* New option `statusType` + * Allows changing the bot's status type between "Playing", "Watching", "Listening" + * Possible values are `playing` (default), `watching`, `listening` +* New option `anonymizeChannelName` ([#457](https://github.com/Dragory/modmailbot/pull/457) by @funkyhippo) + * Off by default. When enabled, instead of using the user's name as the channel name, uses a random channel name instead. + * Useful on single-server setups where people with modified clients can see the names of even hidden channels +* New option `updateNotificationsForBetaVersions` + * Off by default. When enabled, also shows update notifications for beta versions. + * By default, update notifications are only shown for stable releases +* `mentionRole` can now be set to `none` +* The bot now notifies if the user leaves/joins the server ([#437](https://github.com/Dragory/modmailbot/pull/437) by @DarkView) +* Fix crash when using `!newthread` with the bot's own ID (fixes [#452](https://github.com/Dragory/modmailbot/issues/452)) + +**Plugins:** +* New hook: `afterThreadClose` + * Called right after a thread is closed with the thread's id +* You can now add custom log storage types + +**Internal/technical updates:** +* Database migrations are now stored under `src/` + ## v2.31.0-beta.1 **This is a beta release, bugs are expected.** Please report any bugs you encounter by [creating a GitHub issue](https://github.com/Dragory/modmailbot/issues/new)! diff --git a/package.json b/package.json index b2548dd..c3c4878 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "modmailbot", - "version": "2.31.0-beta.1", + "version": "2.31.0-beta.2", "description": "", "license": "MIT", "main": "src/index.js", From 454ab75fec7d36d09105c35ddabaf5f88f6e17ee Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Fri, 25 Sep 2020 01:42:37 +0300 Subject: [PATCH 147/300] Fix !newthread failing with uncached users --- src/modules/newthread.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/newthread.js b/src/modules/newthread.js index 79f5e97..3b094f1 100644 --- a/src/modules/newthread.js +++ b/src/modules/newthread.js @@ -3,7 +3,7 @@ const threads = require("../data/threads"); module.exports = ({ bot, knex, config, commands }) => { commands.addInboxServerCommand("newthread", "", async (msg, args, thread) => { - const user = bot.users.get(args.userId); + const user = bot.users.get(args.userId) || await bot.getRESTUser(args.userId).catch(() => null); if (! user) { utils.postSystemMessageWithFallback(msg.channel, thread, "User not found!"); return; From 19b9d4db6161a50e1243d59121f69474b2024302 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sat, 3 Oct 2020 15:17:30 +0300 Subject: [PATCH 148/300] Fix missing await when removing blocks --- src/modules/block.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/block.js b/src/modules/block.js index 6a73822..25d51b4 100644 --- a/src/modules/block.js +++ b/src/modules/block.js @@ -20,7 +20,7 @@ module.exports = ({ bot, knex, config, commands }) => { async function expiredBlockLoop() { try { - removeExpiredBlocks(); + await removeExpiredBlocks(); } catch (e) { console.error(e); } From 3937c0a83892964cba53d2fb2e3a852477d1caba Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sat, 3 Oct 2020 15:18:27 +0300 Subject: [PATCH 149/300] Start expiredBlockLoop() directly, not on "ready" event This is because the client is already ready by this point, as plugins are only loaded after the ready event. --- src/modules/block.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/block.js b/src/modules/block.js index 25d51b4..252be93 100644 --- a/src/modules/block.js +++ b/src/modules/block.js @@ -28,7 +28,7 @@ module.exports = ({ bot, knex, config, commands }) => { setTimeout(expiredBlockLoop, 2000); } - bot.on("ready", expiredBlockLoop); + expiredBlockLoop(); const blockCmd = async (msg, args, thread) => { const userIdToBlock = args.userId || (thread && thread.user_id); From 0d2202d38ccfff5ff00aa4dd1ec192faec2fae81 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sat, 3 Oct 2020 16:10:27 +0300 Subject: [PATCH 150/300] Allow log storage handlers to store data. Add shouldSave() function to log storage handlers. --- src/data/Thread.js | 40 +++++++ src/data/logs.js | 102 +++++++++++------- ...53122_add_log_storage_fields_to_threads.js | 13 +++ src/modules/close.js | 14 +-- src/modules/logs.js | 18 ++-- 5 files changed, 132 insertions(+), 55 deletions(-) create mode 100644 src/data/migrations/20201003153122_add_log_storage_fields_to_threads.js diff --git a/src/data/Thread.js b/src/data/Thread.js index e3b7b1f..ac32c9d 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -25,11 +25,31 @@ const {THREAD_MESSAGE_TYPE, THREAD_STATUS, DISCORD_MESSAGE_ACTIVITY_TYPES} = req * @property {String} scheduled_close_name * @property {Number} scheduled_close_silent * @property {String} alert_ids + * @property {String} log_storage_type + * @property {Object} log_storage_data * @property {String} created_at */ class Thread { constructor(props) { utils.setDataModelProps(this, props); + + if (props.log_storage_data) { + if (typeof props.log_storage_data === "string") { + this.log_storage_data = JSON.parse(props.log_storage_data); + } + } + } + + getSQLProps() { + return Object.entries(this).reduce((obj, [key, value]) => { + if (typeof value === "function") return obj; + if (typeof value === "object" && value != null) { + obj[key] = JSON.stringify(value); + } else { + obj[key] = value; + } + return obj; + }, {}); } /** @@ -506,6 +526,7 @@ class Thread { } // Update DB status + this.status = THREAD_STATUS.CLOSED; await knex("threads") .where("id", this.id) .update({ @@ -727,6 +748,25 @@ class Thread { await this._deleteThreadMessage(threadMessage.id); } + + /** + * @param {String} storageType + * @param {Object|null} storageData + * @returns {Promise} + */ + async updateLogStorageValues(storageType, storageData) { + this.log_storage_type = storageType; + this.log_storage_data = storageData; + + const { log_storage_type, log_storage_data } = this.getSQLProps(); + + await knex("threads") + .where("id", this.id) + .update({ + log_storage_type, + log_storage_data, + }); + } } module.exports = Thread; diff --git a/src/data/logs.js b/src/data/logs.js index a47521e..89e78fe 100644 --- a/src/data/logs.js +++ b/src/data/logs.js @@ -9,7 +9,8 @@ const { formatters } = require("../formatters"); /** * @typedef {object} LogStorageTypeHandler - * @property {LogStorageTypeHandlerSaveFn?} save + * @property {LogStorageTypeHandlerSaveFn} save + * @property {LogStorageTypeHandlerShouldSaveFn?} shouldSave * @property {LogStorageTypeHandlerGetUrlFn?} getUrl * @property {LogStorageTypeHandlerGetFileFn?} getFile * @property {LogStorageTypeHandlerGetCustomResponseFn?} getCustomResponse @@ -19,18 +20,24 @@ const { formatters } = require("../formatters"); * @callback LogStorageTypeHandlerSaveFn * @param {Thread} thread * @param {ThreadMessage[]} threadMessages - * @return {void|Promise} + * @return {Object|Promise|null|Promise} Information about the saved log that can be used to retrieve the log later + */ + +/** + * @callback LogStorageTypeHandlerShouldSaveFn + * @param {Thread} thread + * @return {boolean|Promise} Whether the log should be saved at this time */ /** * @callback LogStorageTypeHandlerGetUrlFn - * @param {string} threadId + * @param {Thread} thread * @return {string|Promise|null|Promise} */ /** * @callback LogStorageTypeHandlerGetFileFn - * @param {string} threadId + * @param {Thread} thread * @return {Eris.MessageFile|Promise|null|Promise>} */ @@ -42,7 +49,7 @@ const { formatters } = require("../formatters"); /** * @callback LogStorageTypeHandlerGetCustomResponseFn - * @param {string} threadId + * @param {Thread} thread * @return {LogStorageTypeHandlerGetCustomResponseResult|Promise|null|Promise>} */ @@ -70,95 +77,110 @@ const addStorageType = (name, handler) => { /** * @type {SaveLogToStorageFn} */ -const saveLogToStorage = async (thread, threadMessages) => { - const { save } = logStorageTypes[config.logStorage]; +const saveLogToStorage = async (thread, overrideType = null) => { + const storageType = overrideType || config.logStorage; + + const { save, shouldSave } = logStorageTypes[storageType] || {}; + if (shouldSave && ! await shouldSave(thread)) return; + if (save) { - await save(thread, threadMessages); + const threadMessages = await thread.getThreadMessages(); + const storageData = await save(thread, threadMessages); + await thread.updateLogStorageValues(storageType, storageData); } }; /** * @callback GetLogUrlFn - * @param {string} threadId + * @param {Thread} thread * @returns {Promise} */ /** * @type {GetLogUrlFn} */ -const getLogUrl = async (threadId) => { - const { getUrl } = logStorageTypes[config.logStorage]; +const getLogUrl = async (thread) => { + if (! thread.log_storage_type) { + await saveLogToStorage(thread); + } + + const { getUrl } = logStorageTypes[thread.log_storage_type] || {}; return getUrl - ? getUrl(threadId) + ? getUrl(thread) : null; }; /** * @callback GetLogFileFn - * @param {string} threadId + * @param {Thread} thread * @returns {Promise} */ /** * @type {GetLogFileFn} */ -const getLogFile = async (threadId) => { - const { getFile } = logStorageTypes[config.logStorage]; +const getLogFile = async (thread) => { + if (! thread.log_storage_type) { + await saveLogToStorage(thread); + } + + const { getFile } = logStorageTypes[thread.log_storage_type] || {}; return getFile - ? getFile(threadId) + ? getFile(thread) : null; }; /** * @callback GetLogCustomResponseFn - * @param {string} threadId + * @param {Thread} threadId * @returns {Promise} */ /** * @type {GetLogCustomResponseFn} */ -const getLogCustomResponse = async (threadId) => { - const { getCustomResponse } = logStorageTypes[config.logStorage]; +const getLogCustomResponse = async (thread) => { + if (! thread.log_storage_type) { + await saveLogToStorage(thread); + } + + const { getCustomResponse } = logStorageTypes[thread.log_storage_type] || {}; return getCustomResponse - ? getCustomResponse(threadId) + ? getCustomResponse(thread) : null; }; addStorageType("local", { - getUrl(threadId) { - return utils.getSelfUrl(`logs/${threadId}`); + save() { + return null; + }, + + getUrl(thread) { + return utils.getSelfUrl(`logs/${thread.id}`); }, }); const getLogAttachmentFilename = threadId => { const filename = `${threadId}.txt`; - const fullPath = path.resolve(config.logOptions.attachmentDirectory, filename); + const fullPath = path.join(config.logOptions.attachmentDirectory, filename); return { filename, fullPath }; }; addStorageType("attachment", { + shouldSave(thread) { + return thread.status === THREAD_STATUS.CLOSED; + }, + async save(thread, threadMessages) { - const { fullPath } = getLogAttachmentFilename(thread.id); + const { fullPath, filename } = getLogAttachmentFilename(thread.id); const formatLogResult = await formatters.formatLog(thread, threadMessages); fs.writeFileSync(fullPath, formatLogResult.content, { encoding: "utf8" }); + + return { fullPath, filename }; }, - async getUrl(threadId) { - if (! config.logOptions.allowAttachmentUrlFallback) { - return null; - } - - const { fullPath } = getLogAttachmentFilename(threadId); - try { - fs.accessSync(fullPath); - return null; - } catch (e) { - return utils.getSelfUrl(`logs/${threadId}`); - } - }, - - async getFile(threadId) { - const { filename, fullPath } = getLogAttachmentFilename(threadId); + async getFile(thread) { + const { fullPath, filename } = thread.log_storage_data || {}; + if (! fullPath) return; try { fs.accessSync(fullPath); diff --git a/src/data/migrations/20201003153122_add_log_storage_fields_to_threads.js b/src/data/migrations/20201003153122_add_log_storage_fields_to_threads.js new file mode 100644 index 0000000..5757186 --- /dev/null +++ b/src/data/migrations/20201003153122_add_log_storage_fields_to_threads.js @@ -0,0 +1,13 @@ +exports.up = async function(knex) { + await knex.schema.table("threads", table => { + table.string("log_storage_type", 255).nullable().defaultTo(null); + table.text("log_storage_data").nullable().defaultTo(null); + }); +}; + +exports.down = async function(knex) { + await knex.schema.table("threads", table => { + table.dropColumn("log_storage_type"); + table.dropColumn("log_storage_data"); + }); +}; diff --git a/src/modules/close.js b/src/modules/close.js index eadece2..196ba7e 100644 --- a/src/modules/close.js +++ b/src/modules/close.js @@ -7,15 +7,15 @@ const { messageQueue } = require("../queue"); const { getLogUrl, getLogFile, getLogCustomResponse } = require("../data/logs"); module.exports = ({ bot, knex, config, commands }) => { - async function sendCloseNotification(threadId, body) { - const logCustomResponse = await getLogCustomResponse(threadId); + async function sendCloseNotification(thread, body) { + const logCustomResponse = await getLogCustomResponse(thread); if (logCustomResponse) { await utils.postLog(body); await utils.postLog(logCustomResponse.content, logCustomResponse.file); return; } - const logUrl = await getLogUrl(threadId); + const logUrl = await getLogUrl(thread); if (logUrl) { utils.postLog(utils.trimAll(` ${body} @@ -24,7 +24,7 @@ module.exports = ({ bot, knex, config, commands }) => { return; } - const logFile = await getLogFile(threadId); + const logFile = await getLogFile(thread); if (logFile) { utils.postLog(body, logFile); return; @@ -44,7 +44,7 @@ module.exports = ({ bot, knex, config, commands }) => { await thread.close(false, thread.scheduled_close_silent); - await sendCloseNotification(thread.id, `Modmail thread with ${thread.user_name} (${thread.user_id}) was closed as scheduled by ${thread.scheduled_close_name}`); + await sendCloseNotification(thread, `Modmail thread with ${thread.user_name} (${thread.user_id}) was closed as scheduled by ${thread.scheduled_close_name}`); } } @@ -143,7 +143,7 @@ module.exports = ({ bot, knex, config, commands }) => { await thread.sendSystemMessageToUser(closeMessage).catch(() => {}); } - await sendCloseNotification(thread.id, `Modmail thread with ${thread.user_name} (${thread.user_id}) was closed by ${closedBy}`); + await sendCloseNotification(thread, `Modmail thread with ${thread.user_name} (${thread.user_id}) was closed by ${closedBy}`); }); // Auto-close threads if their channel is deleted @@ -162,6 +162,6 @@ module.exports = ({ bot, knex, config, commands }) => { await thread.close(true); - await sendCloseNotification(thread.id, `Modmail thread with ${thread.user_name} (${thread.user_id}) was closed automatically because the channel was deleted`); + await sendCloseNotification(thread, `Modmail thread with ${thread.user_name} (${thread.user_id}) was closed automatically because the channel was deleted`); }); }; diff --git a/src/modules/logs.js b/src/modules/logs.js index 916a80f..5e567c3 100644 --- a/src/modules/logs.js +++ b/src/modules/logs.js @@ -30,7 +30,7 @@ module.exports = ({ bot, knex, config, commands, hooks }) => { userThreads = userThreads.slice((page - 1) * LOG_LINES_PER_PAGE, page * LOG_LINES_PER_PAGE); const threadLines = await Promise.all(userThreads.map(async thread => { - const logUrl = await getLogUrl(thread.id); + const logUrl = await getLogUrl(thread); const formattedLogUrl = logUrl ? `<${logUrl}>` : `View log with \`${config.prefix}log ${thread.id}\`` @@ -58,22 +58,25 @@ module.exports = ({ bot, knex, config, commands, hooks }) => { }); }; - const logCmd = async (msg, args, thread) => { - const threadId = args.threadId || (thread && thread.id); + const logCmd = async (msg, args, _thread) => { + const threadId = args.threadId || (_thread && _thread.id); if (! threadId) return; - const customResponse = await getLogCustomResponse(threadId); + const thread = await threads.findById(threadId); + if (! thread) return; + + const customResponse = await getLogCustomResponse(thread); if (customResponse && (customResponse.content || customResponse.file)) { msg.channel.createMessage(customResponse.content, customResponse.file); } - const logUrl = await getLogUrl(threadId); + const logUrl = await getLogUrl(thread); if (logUrl) { msg.channel.createMessage(`Open the following link to view the log:\n<${logUrl}>`); return; } - const logFile = await getLogFile(threadId); + const logFile = await getLogFile(thread); if (logFile) { msg.channel.createMessage("Download the following file to view the log:", logFile); return; @@ -90,7 +93,6 @@ module.exports = ({ bot, knex, config, commands, hooks }) => { hooks.afterThreadClose(async ({ threadId }) => { const thread = await threads.findById(threadId); - const threadMessages = await thread.getThreadMessages(); - await saveLogToStorage(thread, threadMessages); + await saveLogToStorage(thread); }); }; From b80cb0ed15d87c1a0798359ff5b6d9ec1844a806 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sat, 3 Oct 2020 16:12:57 +0300 Subject: [PATCH 151/300] Add CHANGELOG entries for beta.3 --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b99c70a..7b757d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## v2.31.0-beta.3 (UNRELEASED) +**This is a beta release, bugs are expected.** +Please report any bugs you encounter by [creating a GitHub issue](https://github.com/Dragory/modmailbot/issues/new)! + +**General changes:** +* Fix occasional bug with expiring blocks where the bot would send the expiry message multiple times + +**Plugins:** +* Log storage functions `getLogUrl()`, `getLogFile()`, `getLogCustomResponse()` now take the entire thread object as an argument rather than the thread ID + ## v2.31.0-beta.2 **This is a beta release, bugs are expected.** Please report any bugs you encounter by [creating a GitHub issue](https://github.com/Dragory/modmailbot/issues/new)! From bf4920f4b0471ddf482bca3588b681b3cfd53c04 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sat, 3 Oct 2020 16:15:02 +0300 Subject: [PATCH 152/300] Update log storage plugin example for beta.3 --- docs/plugins.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/plugins.md b/docs/plugins.md index c037e9e..53929e9 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -36,18 +36,18 @@ To use this custom attachment storage type, you would set the `attachmentStorage ### Example of a custom log storage type This example adds a custom type for the `logStorage` option called `"pastebin"` that uploads logs to Pastebin. -The code that handles API calls to Pastebin is left out, as it's not relevant to the example. + ```js module.exports = function({ logs, formatters }) { logs.addStorageType('pastebin', { async save(thread, threadMessages) { const formatLogResult = await formatters.formatLog(thread, threadMessages); - // formatLogResult.content contains the log text - // Some code here that uploads the full text to Pastebin, and stores the Pastebin link in the database + const pastebinUrl = await saveToPastebin(formatLogResult); // saveToPastebin is an example function that returns the pastebin URL for the saved log + return { url: pastebinUrl }; }, - getUrl(threadId) { - // Find the previously-saved Pastebin link from the database based on the thread id, and return it + getUrl(thread) { + return thread.log_storage_data.url; } }); }; From e99352e2acb60ed69c7b2defb1c16302b97596c0 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sat, 3 Oct 2020 16:40:05 +0300 Subject: [PATCH 153/300] Use Express as the web server for logs/attachments --- package-lock.json | 393 ++++++++++++++++++++++++++++++++++++++- package.json | 2 + src/modules/webserver.js | 56 +++--- 3 files changed, 414 insertions(+), 37 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5d6008d..4fdda3f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "modmailbot", - "version": "2.31.0-beta.1", + "version": "2.31.0-beta.2", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -53,6 +53,15 @@ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, "acorn": { "version": "7.4.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.0.tgz", @@ -167,6 +176,11 @@ "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=" }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, "array-slice": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", @@ -313,6 +327,43 @@ "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", "dev": true }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + } + } + }, "brace-expansion": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", @@ -349,6 +400,11 @@ } } }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + }, "cache-base": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", @@ -686,6 +742,29 @@ "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, "copy-descriptor": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", @@ -822,6 +901,16 @@ "resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz", "integrity": "sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ==" }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, "detect-file": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", @@ -900,11 +989,21 @@ "safer-buffer": "^2.1.0" } }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, "end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -938,6 +1037,11 @@ "ws": "^7.2.1" } }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -1151,6 +1255,11 @@ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, "expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", @@ -1204,6 +1313,63 @@ "homedir-polyfill": "^1.0.1" } }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + } + } + }, "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -1349,6 +1515,35 @@ } } }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, "find-replace": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz", @@ -1455,6 +1650,11 @@ "mime-types": "^2.1.12" } }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, "fragment-cache": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", @@ -1463,6 +1663,11 @@ "map-cache": "^0.2.2" } }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, "fs-minipass": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", @@ -1730,6 +1935,11 @@ } } }, + "helmet": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-4.1.1.tgz", + "integrity": "sha512-Avg4XxSBrehD94mkRwEljnO+6RZx7AGfk8Wa6K1nxaU+hbXlFOhlOIMgPfFqOYQB/dBCsTpootTGuiOG+CHiQA==" + }, "homedir-polyfill": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", @@ -1743,6 +1953,18 @@ "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -1831,6 +2053,11 @@ "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.1.0.tgz", "integrity": "sha512-pKnZpbgCTfH/1NLIlOduP/V+WRXzC2MOz3Qo8xmxk8C5GudJLgK5QyLVXOSWy3ParAH7Eemurl3xjv/WXYFvMA==" }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, "is-absolute": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", @@ -2423,6 +2650,21 @@ "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=", "dev": true }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, "micromatch": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", @@ -2451,14 +2693,12 @@ "mime-db": { "version": "1.44.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", - "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==", - "optional": true + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" }, "mime-types": { "version": "2.1.27", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", - "optional": true, "requires": { "mime-db": "1.44.0" } @@ -2672,6 +2912,11 @@ } } }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + }, "neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", @@ -2893,6 +3138,14 @@ "isobject": "^3.0.1" } }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -2990,6 +3243,11 @@ "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=" }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, "pascalcase": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", @@ -3029,6 +3287,11 @@ "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=" }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -3067,6 +3330,15 @@ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true }, + "proxy-addr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", + "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.1" + } + }, "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", @@ -3108,6 +3380,22 @@ "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", "optional": true }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, "rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -3378,11 +3666,69 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==" }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } + }, "seq-queue": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", "integrity": "sha1-1WgS4cAXpuTnw+Ojeh2m143TyT4=" }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -3409,6 +3755,11 @@ } } }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -3671,6 +4022,11 @@ } } }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, "stream-connect": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/stream-connect/-/stream-connect-1.0.2.tgz", @@ -3948,6 +4304,11 @@ "repeat-string": "^1.6.1" } }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, "tough-cookie": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", @@ -3996,6 +4357,15 @@ "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", "dev": true }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, "typical": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/typical/-/typical-2.6.1.tgz", @@ -4037,6 +4407,11 @@ "set-value": "^2.0.1" } }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, "unset-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", @@ -4104,6 +4479,11 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, "uuid": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz", @@ -4123,6 +4503,11 @@ "homedir-polyfill": "^1.0.1" } }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, "verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", diff --git a/package.json b/package.json index c3c4878..5afa350 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,8 @@ "dependencies": { "ajv": "^6.12.4", "eris": "^0.13.3", + "express": "^4.17.1", + "helmet": "^4.1.1", "humanize-duration": "^3.23.1", "ini": "^1.3.5", "json-schema-to-jsdoc": "^1.0.0", diff --git a/src/modules/webserver.js b/src/modules/webserver.js index 0a04401..c607573 100644 --- a/src/modules/webserver.js +++ b/src/modules/webserver.js @@ -1,4 +1,5 @@ -const http = require("http"); +const express = require("express"); +const helmet = require("helmet"); const mime = require("mime"); const url = require("url"); const fs = require("fs"); @@ -10,46 +11,43 @@ const attachments = require("../data/attachments"); const { formatters } = require("../formatters"); function notfound(res) { - res.statusCode = 404; - res.end("Page Not Found"); + res.status(404).send("Page Not Found"); } -async function serveLogs(req, res, pathParts, query) { - const threadId = pathParts[pathParts.length - 1]; - if (threadId.match(/^[0-9a-f\-]+$/) === null) return notfound(res); - - const thread = await threads.findById(threadId); +/** + * @param {express.Request} req + * @param {express.Response} res + */ +async function serveLogs(req, res) { + const thread = await threads.findById(req.params.threadId); if (! thread) return notfound(res); let threadMessages = await thread.getThreadMessages(); const formatLogResult = await formatters.formatLog(thread, threadMessages, { - simple: Boolean(query.simple), - verbose: Boolean(query.verbose), + simple: Boolean(req.query.simple), + verbose: Boolean(req.query.verbose), }); const contentType = formatLogResult.extra && formatLogResult.extra.contentType || "text/plain; charset=UTF-8"; - res.setHeader("Content-Type", contentType); - res.end(formatLogResult.content); + res.set("Content-Type", contentType); + res.send(formatLogResult.content); } -function serveAttachments(req, res, pathParts) { - const desiredFilename = pathParts[pathParts.length - 1]; - const id = pathParts[pathParts.length - 2]; +function serveAttachments(req, res) { + if (req.params.attachmentId.match(/^[0-9]+$/) === null) return notfound(res); + if (req.params.filename.match(/^[0-9a-z._-]+$/i) === null) return notfound(res); - if (id.match(/^[0-9]+$/) === null) return notfound(res); - if (desiredFilename.match(/^[0-9a-z._-]+$/i) === null) return notfound(res); - - const attachmentPath = attachments.getLocalAttachmentPath(id); + const attachmentPath = attachments.getLocalAttachmentPath(req.params.attachmentId); fs.access(attachmentPath, (err) => { if (err) return notfound(res); - const filenameParts = desiredFilename.split("."); + const filenameParts = req.params.filename.split("."); const ext = (filenameParts.length > 1 ? filenameParts[filenameParts.length - 1] : "bin"); const fileMime = mime.getType(ext); - res.setHeader("Content-Type", fileMime); + res.set("Content-Type", fileMime); const read = fs.createReadStream(attachmentPath); read.pipe(res); @@ -57,19 +55,11 @@ function serveAttachments(req, res, pathParts) { } module.exports = () => { - const server = http.createServer((req, res) => { - const parsedUrl = url.parse(`http://${req.url}`); - const pathParts = parsedUrl.pathname.split("/").filter(v => v !== ""); - const query = qs.parse(parsedUrl.query); + const server = express(); + server.use(helmet()); - if (parsedUrl.pathname.startsWith("/logs/")) { - serveLogs(req, res, pathParts, query); - } else if (parsedUrl.pathname.startsWith("/attachments/")) { - serveAttachments(req, res, pathParts, query); - } else { - notfound(res); - } - }); + server.get("/logs/:threadId", serveLogs); + server.get("/logs/:attachmentId/:filename", serveAttachments); server.on("error", err => { console.log("[WARN] Web server error:", err.message); From 9048942ce9f15653165819dc80ceddc6e0633c6b Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 4 Oct 2020 02:10:13 +0300 Subject: [PATCH 154/300] Expose web server express application to plugins --- docs/plugins.md | 1 + src/main.js | 2 +- src/modules/webserver.js | 21 ++++++++++++--------- src/pluginApi.js | 2 ++ src/plugins.js | 2 ++ 5 files changed, 18 insertions(+), 10 deletions(-) diff --git a/docs/plugins.md b/docs/plugins.md index 53929e9..b323f38 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -66,6 +66,7 @@ The first and only argument to the plugin function is an object with the followi | `logs` | An object with functions to get attachment URLs/files and manage log storage types | | `hooks` | An object with functions to add *hooks* that are called at specific times, e.g. before a new thread is created | | `formats` | An object with functions that allow you to replace the default functions used for formatting messages and logs | +| `webserver` | An [Express Application object](https://expressjs.com/en/api.html#app) that functions as the bot's web server | See the auto-generated [Plugin API](plugin-api.md) page for details. diff --git a/src/main.js b/src/main.js index 02e8cd2..e6de612 100644 --- a/src/main.js +++ b/src/main.js @@ -21,7 +21,7 @@ const logs = require("./modules/logs"); const move = require("./modules/move"); const block = require("./modules/block"); const suspend = require("./modules/suspend"); -const webserver = require("./modules/webserver"); +const { plugin: webserver } = require("./modules/webserver"); const greeting = require("./modules/greeting"); const typingProxy = require("./modules/typingProxy"); const version = require("./modules/version"); diff --git a/src/modules/webserver.js b/src/modules/webserver.js index c607573..ae60e9e 100644 --- a/src/modules/webserver.js +++ b/src/modules/webserver.js @@ -54,16 +54,19 @@ function serveAttachments(req, res) { }) } -module.exports = () => { - const server = express(); - server.use(helmet()); +const server = express(); +server.use(helmet()); - server.get("/logs/:threadId", serveLogs); - server.get("/logs/:attachmentId/:filename", serveAttachments); +server.get("/logs/:threadId", serveLogs); +server.get("/logs/:attachmentId/:filename", serveAttachments); - server.on("error", err => { - console.log("[WARN] Web server error:", err.message); - }); +server.on("error", err => { + console.log("[WARN] Web server error:", err.message); +}); - server.listen(config.port); +module.exports = { + server, + plugin() { + server.listen(config.port); + }, }; diff --git a/src/pluginApi.js b/src/pluginApi.js index 662c832..4ae092d 100644 --- a/src/pluginApi.js +++ b/src/pluginApi.js @@ -1,3 +1,4 @@ +const express = require("express"); const { CommandManager } = require("knub-command-manager"); const { Client } = require("eris"); const Knex = require("knex"); @@ -12,6 +13,7 @@ const Knex = require("knex"); * @property {PluginLogsAPI} logs * @property {PluginHooksAPI} hooks * @property {FormattersExport} formats + * @property {express.Application} webserver */ /** diff --git a/src/plugins.js b/src/plugins.js index 4de156d..f50fa64 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -3,6 +3,7 @@ const logs = require("./data/logs"); const { beforeNewThread } = require("./hooks/beforeNewThread"); const { afterThreadClose } = require("./hooks/afterThreadClose"); const formats = require("./formatters"); +const { server: webserver } = require("./modules/webserver"); module.exports = { /** @@ -40,6 +41,7 @@ module.exports = { afterThreadClose, }, formats, + webserver, }; }, From c7fc8a6bc03520827012fc79ed24227f69a4c242 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 4 Oct 2020 02:12:44 +0300 Subject: [PATCH 155/300] Update CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b757d7..e2772ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ Please report any bugs you encounter by [creating a GitHub issue](https://github **Plugins:** * Log storage functions `getLogUrl()`, `getLogFile()`, `getLogCustomResponse()` now take the entire thread object as an argument rather than the thread ID +* Plugins can now access the bot's web server via a new `webserver` property in plugin arguments + +**Internal/technical updates:** +* Modmail now uses [Express](https://expressjs.com/) as its web server for logs/attachments ## v2.31.0-beta.2 **This is a beta release, bugs are expected.** From e6bdc4cd8c64bd23bf8965d48ebd6d163fba0c4d Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 4 Oct 2020 02:14:07 +0300 Subject: [PATCH 156/300] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2772ba..6d6fa76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Please report any bugs you encounter by [creating a GitHub issue](https://github **Plugins:** * Log storage functions `getLogUrl()`, `getLogFile()`, `getLogCustomResponse()` now take the entire thread object as an argument rather than the thread ID +* Log storage function `save()` can now return information about the saved log to be stored with the thread. This can then be accessed in e.g. `getLogUrl()` via `thread.log_storage_data`. * Plugins can now access the bot's web server via a new `webserver` property in plugin arguments **Internal/technical updates:** From d8c531cb4d48722e9bc9dc5a810d3f82ff24c523 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 4 Oct 2020 16:46:43 +0300 Subject: [PATCH 157/300] Add support for different plugin sources; add support for installing plugins from npm --- package-lock.json | 1051 ++++++++++++++++++++++++++++++-- package.json | 1 + src/main.js | 61 +- src/modules/webserver.js | 7 +- src/modules/webserverPlugin.js | 5 + src/plugins.js | 90 ++- 6 files changed, 1104 insertions(+), 111 deletions(-) create mode 100644 src/modules/webserverPlugin.js diff --git a/package-lock.json b/package-lock.json index 4fdda3f..df4330a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,205 @@ "integrity": "sha512-REo8xv7+sDxkKvoxEywIdsNFiZLybwdI7hcT5uEPyQrSMB4YQ973BfC9OOrD/81MaIjh6UxdulIQXkjmiH3PcA==", "dev": true }, + "@npmcli/ci-detect": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@npmcli/ci-detect/-/ci-detect-1.3.0.tgz", + "integrity": "sha512-oN3y7FAROHhrAt7Rr7PnTSwrHrZVRTS2ZbyxeQwSSYD0ifwM3YNgQqbaRmjcWoPyq77MjchusjJDspbzMmip1Q==" + }, + "@npmcli/git": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-2.0.4.tgz", + "integrity": "sha512-OJZCmJ9DNn1cz9HPXXsPmUBnqaArot3CGYo63CyajHQk+g87rPXVOJByGsskQJhPsUUEXJcsZ2Q6bWd2jSwnBA==", + "requires": { + "@npmcli/promise-spawn": "^1.1.0", + "lru-cache": "^6.0.0", + "mkdirp": "^1.0.3", + "npm-pick-manifest": "^6.0.0", + "promise-inflight": "^1.0.1", + "promise-retry": "^1.1.1", + "semver": "^7.3.2", + "unique-filename": "^1.1.1", + "which": "^2.0.2" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + }, + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==" + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, + "@npmcli/installed-package-contents": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-1.0.5.tgz", + "integrity": "sha512-aKIwguaaqb6ViwSOFytniGvLPb9SMCUm39TgM3SfUo7n0TxUMbwoXfpwyvQ4blm10lzbAwTsvjr7QZ85LvTi4A==", + "requires": { + "npm-bundled": "^1.1.1", + "npm-normalize-package-bin": "^1.0.1", + "read-package-json-fast": "^1.1.1", + "readdir-scoped-modules": "^1.1.0" + } + }, + "@npmcli/move-file": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.0.1.tgz", + "integrity": "sha512-Uv6h1sT+0DrblvIrolFtbvM1FgWm+/sy4B3pvLp67Zys+thcukzS5ekn7HsZFGpWP4Q3fYJCljbWQE/XivMRLw==", + "requires": { + "mkdirp": "^1.0.4" + }, + "dependencies": { + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + } + } + }, + "@npmcli/node-gyp": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-1.0.1.tgz", + "integrity": "sha512-pBqoKPWmuk9iaEcXlLBVRIA6I1kG9JiICU+sG0NuD6NAR461F+02elHJS4WkQxHW2W5rnsfvP/ClKwmsZ9RaaA==" + }, + "@npmcli/promise-spawn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-1.2.0.tgz", + "integrity": "sha512-nFtqjVETliApiRdjbYwKwhlSHx2ZMagyj5b9YbNt0BWeeOVxJd47ZVE2u16vxDHyTOZvk+YLV7INwfAE9a2uow==", + "requires": { + "infer-owner": "^1.0.4" + } + }, + "@npmcli/run-script": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-1.7.2.tgz", + "integrity": "sha512-EZO9uXrZrfzdIJsNi/WwrP2jt1P0lbFSxOq15ljgYn1/rr4UyQXUKBZRURioFVbUb7Z1BJDEKswnWrtRybZPzw==", + "requires": { + "@npmcli/node-gyp": "^1.0.0", + "@npmcli/promise-spawn": "^1.2.0", + "infer-owner": "^1.0.4", + "node-gyp": "^7.1.0", + "read-package-json-fast": "^1.1.3" + }, + "dependencies": { + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" + }, + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", + "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "requires": { + "yallist": "^4.0.0" + } + }, + "minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + }, + "node-gyp": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-7.1.0.tgz", + "integrity": "sha512-rjlHQlnl1dqiDZxZYiKqQdrjias7V+81OVR5PTzZioCBtWkNdrKy06M05HLKxy/pcKikKRCabeDRoZaEc6nIjw==", + "requires": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.3", + "nopt": "^4.0.3", + "npmlog": "^4.1.2", + "request": "^2.88.2", + "rimraf": "^2.6.3", + "semver": "^7.3.2", + "tar": "^6.0.1", + "which": "^2.0.2" + } + }, + "nopt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", + "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==" + }, + "tar": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.0.5.tgz", + "integrity": "sha512-0b4HOimQHj9nXNEAA7zWwMM91Zhhba3pspja6sQbgTpynOJf+bkjBnfybNYzbpLbnwXnbyB4LOREvlyXLkCHSg==", + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, "@sindresorhus/is": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", @@ -43,6 +242,11 @@ "defer-to-connect": "^1.0.1" } }, + "@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==" + }, "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", @@ -74,6 +278,33 @@ "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", "dev": true }, + "agent-base": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.1.tgz", + "integrity": "sha512-01q25QQDwLSsyfhrKbn8yuur+JNw0H+0Y4JiGIKd3z9aYk/w/2kxD/Upc+t2ZBBSUNff50VjPsSW2YxM8QYKVg==", + "requires": { + "debug": "4" + } + }, + "agentkeepalive": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.1.3.tgz", + "integrity": "sha512-wn8fw19xKZwdGPO47jivonaHRTd+nGOMP1z11sgGeQzDy2xd5FG0R67dIMcKHDE2cJ5y+YXV30XVGUBPRSY7Hg==", + "requires": { + "debug": "^4.1.0", + "depd": "^1.1.2", + "humanize-ms": "^1.2.1" + } + }, + "aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + } + }, "ajv": { "version": "6.12.4", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz", @@ -191,11 +422,15 @@ "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" + }, "asn1": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "optional": true, "requires": { "safer-buffer": "~2.1.0" } @@ -203,8 +438,7 @@ "assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "optional": true + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" }, "assign-symbols": { "version": "1.0.0", @@ -220,8 +454,7 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "optional": true + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "atob": { "version": "2.1.2", @@ -231,14 +464,12 @@ "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "optional": true + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" }, "aws4": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.0.tgz", - "integrity": "sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA==", - "optional": true + "integrity": "sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA==" }, "balanced-match": { "version": "1.0.0", @@ -299,7 +530,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "optional": true, "requires": { "tweetnacl": "^0.14.3" }, @@ -307,8 +537,7 @@ "tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "optional": true + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" } } }, @@ -400,11 +629,111 @@ } } }, + "builtins": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz", + "integrity": "sha1-y5T662HIaWRR2zZTThQi+U8K7og=" + }, "bytes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" }, + "cacache": { + "version": "15.0.5", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.0.5.tgz", + "integrity": "sha512-lloiL22n7sOjEEXdL8NAjTgv9a1u43xICE9/203qonkZUCj5X1UEWIdf2/Y0d6QcCtMzbKQyhrcDbdvlZTs/+A==", + "requires": { + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.0", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "dependencies": { + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" + }, + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "requires": { + "minipass": "^3.0.0" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "minipass": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", + "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "requires": { + "yallist": "^4.0.0" + } + }, + "minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "requires": { + "glob": "^7.1.3" + } + }, + "tar": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.0.5.tgz", + "integrity": "sha512-0b4HOimQHj9nXNEAA7zWwMM91Zhhba3pspja6sQbgTpynOJf+bkjBnfybNYzbpLbnwXnbyB4LOREvlyXLkCHSg==", + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, "cache-base": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", @@ -476,8 +805,7 @@ "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", - "optional": true + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, "catharsis": { "version": "0.8.11", @@ -525,6 +853,11 @@ } } }, + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==" + }, "cliui": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", @@ -621,7 +954,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "optional": true, "requires": { "delayed-stream": "~1.0.0" } @@ -801,7 +1133,6 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "optional": true, "requires": { "assert-plus": "^1.0.0" } @@ -814,6 +1145,11 @@ "ms": "^2.1.1" } }, + "debuglog": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz", + "integrity": "sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=" + }, "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", @@ -888,8 +1224,7 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "optional": true + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, "delegates": { "version": "1.0.0", @@ -921,6 +1256,15 @@ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" }, + "dezalgo": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz", + "integrity": "sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY=", + "requires": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, "dmd": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/dmd/-/dmd-5.0.2.tgz", @@ -983,7 +1327,6 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "optional": true, "requires": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" @@ -1004,6 +1347,26 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" }, + "encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "optional": true, + "requires": { + "iconv-lite": "^0.6.2" + }, + "dependencies": { + "iconv-lite": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz", + "integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==", + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } + } + }, "end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -1027,6 +1390,11 @@ "integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==", "dev": true }, + "env-paths": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.0.tgz", + "integrity": "sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==" + }, "eris": { "version": "0.13.3", "resolved": "https://registry.npmjs.org/eris/-/eris-0.13.3.tgz", @@ -1037,6 +1405,11 @@ "ws": "^7.2.1" } }, + "err-code": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-1.1.2.tgz", + "integrity": "sha1-BuARbTAo9q70gGhJ6w6mp0iuaWA=" + }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -1456,8 +1829,7 @@ "extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "optional": true + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" }, "fast-deep-equal": { "version": "3.1.3", @@ -1636,14 +2008,12 @@ "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "optional": true + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" }, "form-data": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "optional": true, "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.6", @@ -1767,7 +2137,6 @@ "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "optional": true, "requires": { "assert-plus": "^1.0.0" } @@ -1882,14 +2251,12 @@ "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "optional": true + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" }, "har-validator": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", - "optional": true, "requires": { "ajv": "^6.5.5", "har-schema": "^2.0.0" @@ -1948,6 +2315,29 @@ "parse-passwd": "^1.0.0" } }, + "hosted-git-info": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.5.tgz", + "integrity": "sha512-i4dpK6xj9BIpVOTboXIlKG9+8HMKggcrMX7WA24xZtKwX0TPelq/rbaS5rCKeNX8sJXZJGdSxpnEGtta+wismQ==", + "requires": { + "lru-cache": "^6.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, "http-cache-semantics": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", @@ -1965,22 +2355,48 @@ "toidentifier": "1.0.0" } }, + "http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "requires": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + } + }, "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "optional": true, "requires": { "assert-plus": "^1.0.0", "jsprim": "^1.2.2", "sshpk": "^1.7.0" } }, + "https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "requires": { + "agent-base": "6", + "debug": "4" + } + }, "humanize-duration": { "version": "3.23.1", "resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.23.1.tgz", "integrity": "sha512-aoOEkomAETmVuQyBx4E7/LfPlC9s8pAA/USl7vFRQpDjepo3aiyvFfOhtXSDqPowdBVPFUZ7onG/KyuolX0qPg==" }, + "humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=", + "requires": { + "ms": "^2.0.0" + } + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -2016,8 +2432,17 @@ "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" + }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==" + }, + "infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==" }, "inflight": { "version": "1.0.6", @@ -2159,6 +2584,11 @@ "ip-regex": "^4.0.0" } }, + "is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha1-PZh3iZ5qU+/AFgUEzeFfgubwYdU=" + }, "is-number": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", @@ -2201,8 +2631,7 @@ "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "optional": true + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" }, "is-unc-path": { "version": "1.0.0", @@ -2235,8 +2664,7 @@ "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", - "optional": true + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, "js-tokens": { "version": "4.0.0", @@ -2266,8 +2694,7 @@ "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "optional": true + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" }, "jsdoc": { "version": "3.6.5", @@ -2368,6 +2795,11 @@ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=" }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, "json-pointer": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/json-pointer/-/json-pointer-0.6.0.tgz", @@ -2379,8 +2811,7 @@ "json-schema": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "optional": true + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" }, "json-schema-to-jsdoc": { "version": "1.0.0", @@ -2404,8 +2835,7 @@ "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "optional": true + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, "json5": { "version": "2.1.3", @@ -2415,11 +2845,15 @@ "minimist": "^1.2.5" } }, + "jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=" + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "optional": true, "requires": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", @@ -2598,6 +3032,51 @@ "yallist": "^3.0.2" } }, + "make-fetch-happen": { + "version": "8.0.9", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-8.0.9.tgz", + "integrity": "sha512-uHa4gv/NIdm9cUvfOhYb57nxrCY08iyMRXru0jbpaH57Q3NCge/ypY7fOvgCr8tPyucKrGbVndKhjXE0IX0VfQ==", + "requires": { + "agentkeepalive": "^4.1.0", + "cacache": "^15.0.0", + "http-cache-semantics": "^4.0.4", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "promise-retry": "^1.1.1", + "socks-proxy-agent": "^5.0.0", + "ssri": "^8.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "minipass": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", + "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "requires": { + "yallist": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, "make-iterator": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", @@ -2730,6 +3209,157 @@ "yallist": "^3.0.0" } }, + "minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", + "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "requires": { + "yallist": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, + "minipass-fetch": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.3.2.tgz", + "integrity": "sha512-/i4fX1ss+Dtwyk++OsAI6SEV+eE1dvI6W+0hORdjfruQ7VD5uYTetJIHcEMjWiEiszWjn2aAtP1CB/Q4KfeoYA==", + "requires": { + "encoding": "^0.1.12", + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", + "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "requires": { + "yallist": "^4.0.0" + } + }, + "minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, + "minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", + "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "requires": { + "yallist": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, + "minipass-json-stream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz", + "integrity": "sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg==", + "requires": { + "jsonparse": "^1.3.1", + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", + "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "requires": { + "yallist": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, + "minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", + "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "requires": { + "yallist": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, + "minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", + "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "requires": { + "yallist": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, "minizlib": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", @@ -3020,11 +3650,43 @@ "npm-normalize-package-bin": "^1.0.1" } }, + "npm-install-checks": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-4.0.0.tgz", + "integrity": "sha512-09OmyDkNLYwqKPOnbI8exiOZU2GVVmQp7tgez2BPi5OZC8M82elDAps7sxC4l//uSUtotWqoEIDwjRvWH4qz8w==", + "requires": { + "semver": "^7.1.1" + }, + "dependencies": { + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==" + } + } + }, "npm-normalize-package-bin": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==" }, + "npm-package-arg": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-8.0.1.tgz", + "integrity": "sha512-/h5Fm6a/exByzFSTm7jAyHbgOqErl9qSNJDQF32Si/ZzgwT2TERVxRxn3Jurw1wflgyVVAxnFR4fRHPM7y1ClQ==", + "requires": { + "hosted-git-info": "^3.0.2", + "semver": "^7.0.0", + "validate-npm-package-name": "^3.0.0" + }, + "dependencies": { + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==" + } + } + }, "npm-packlist": { "version": "1.4.8", "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz", @@ -3035,6 +3697,70 @@ "npm-normalize-package-bin": "^1.0.1" } }, + "npm-pick-manifest": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-6.1.0.tgz", + "integrity": "sha512-ygs4k6f54ZxJXrzT0x34NybRlLeZ4+6nECAIbr2i0foTnijtS1TJiyzpqtuUAJOps/hO0tNDr8fRV5g+BtRlTw==", + "requires": { + "npm-install-checks": "^4.0.0", + "npm-package-arg": "^8.0.0", + "semver": "^7.0.0" + }, + "dependencies": { + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==" + } + } + }, + "npm-registry-fetch": { + "version": "8.1.4", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-8.1.4.tgz", + "integrity": "sha512-UaLGFQP7VCuyBsb7S5P5od3av/Zy9JW6K5gbMigjZCYnEpIkWWRiLQTKVpxM4QocfPcsjm+xtyrDNm4jdqwNEg==", + "requires": { + "@npmcli/ci-detect": "^1.0.0", + "lru-cache": "^6.0.0", + "make-fetch-happen": "^8.0.9", + "minipass": "^3.1.3", + "minipass-fetch": "^1.3.0", + "minipass-json-stream": "^1.0.1", + "minizlib": "^2.0.0", + "npm-package-arg": "^8.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "minipass": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", + "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "requires": { + "yallist": "^4.0.0" + } + }, + "minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, "npmlog": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", @@ -3054,8 +3780,7 @@ "oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "optional": true + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" }, "object-assign": { "version": "4.1.1", @@ -3214,11 +3939,119 @@ "p-limit": "^2.2.0" } }, + "p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "requires": { + "aggregate-error": "^3.0.0" + } + }, "p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" }, + "pacote": { + "version": "11.1.11", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-11.1.11.tgz", + "integrity": "sha512-r6PHtCEhkaGv+QPx1JdE/xRdkSkZUG7dE2oloNk/CGTPGNOtaJyYqZPFeN6d6UcUrTPRvZXFo3IBzJIBopPuSA==", + "requires": { + "@npmcli/git": "^2.0.1", + "@npmcli/installed-package-contents": "^1.0.5", + "@npmcli/promise-spawn": "^1.2.0", + "@npmcli/run-script": "^1.3.0", + "cacache": "^15.0.5", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "infer-owner": "^1.0.4", + "minipass": "^3.1.3", + "mkdirp": "^1.0.3", + "npm-package-arg": "^8.0.1", + "npm-packlist": "^2.1.0", + "npm-pick-manifest": "^6.0.0", + "npm-registry-fetch": "^8.1.3", + "promise-retry": "^1.1.1", + "read-package-json-fast": "^1.1.3", + "rimraf": "^3.0.2", + "ssri": "^8.0.0", + "tar": "^6.0.1" + }, + "dependencies": { + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" + }, + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", + "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "requires": { + "yallist": "^4.0.0" + } + }, + "minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + }, + "npm-packlist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-2.1.2.tgz", + "integrity": "sha512-eByPaP+wsKai0BJX5pmb58d3mfR0zUATcnyuvSxIudTEn+swCPFLxh7srCmqB4hr7i9V24/DPjjq5b2qUtbgXQ==", + "requires": { + "glob": "^7.1.6", + "ignore-walk": "^3.0.3", + "npm-bundled": "^1.1.1", + "npm-normalize-package-bin": "^1.0.1" + } + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "requires": { + "glob": "^7.1.3" + } + }, + "tar": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.0.5.tgz", + "integrity": "sha512-0b4HOimQHj9nXNEAA7zWwMM91Zhhba3pspja6sQbgTpynOJf+bkjBnfybNYzbpLbnwXnbyB4LOREvlyXLkCHSg==", + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -3295,8 +4128,7 @@ "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", - "optional": true + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, "pg-connection-string": { "version": "2.3.0", @@ -3330,6 +4162,20 @@ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true }, + "promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=" + }, + "promise-retry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-1.1.1.tgz", + "integrity": "sha1-ZznpaOMFHaIM5kl/srUPaRHfPW0=", + "requires": { + "err-code": "^1.0.0", + "retry": "^0.10.0" + } + }, "proxy-addr": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", @@ -3347,8 +4193,7 @@ "psl": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", - "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", - "optional": true + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" }, "public-ip": { "version": "4.0.2", @@ -3377,8 +4222,7 @@ "qs": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", - "optional": true + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" }, "range-parser": { "version": "1.2.1", @@ -3407,6 +4251,15 @@ "strip-json-comments": "~2.0.1" } }, + "read-package-json-fast": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-1.2.1.tgz", + "integrity": "sha512-OFbpwnHcv74Oa5YN5WvbOBfLw6yPmPcwvyJJw/tj9cWFBF7juQUDLDSZiOjEcgzfweWeeROOmbPpNN1qm4hcRg==", + "requires": { + "json-parse-even-better-errors": "^2.3.0", + "npm-normalize-package-bin": "^1.0.1" + } + }, "readable-stream": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", @@ -3421,6 +4274,17 @@ "util-deprecate": "~1.0.1" } }, + "readdir-scoped-modules": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz", + "integrity": "sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw==", + "requires": { + "debuglog": "^1.0.1", + "dezalgo": "^1.0.0", + "graceful-fs": "^4.1.2", + "once": "^1.3.0" + } + }, "rechoir": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", @@ -3538,7 +4402,6 @@ "version": "2.88.2", "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "optional": true, "requires": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", @@ -3565,8 +4428,7 @@ "uuid": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "optional": true + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" } } }, @@ -3630,6 +4492,11 @@ "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" }, + "retry": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.10.1.tgz", + "integrity": "sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q=" + }, "rimraf": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", @@ -3799,6 +4666,11 @@ } } }, + "smart-buffer": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.1.0.tgz", + "integrity": "sha512-iVICrxOzCynf/SNaBQCw34eM9jROU/s5rzIhpOvzhzuYHfJR/DhZfDkXiZSgKXfgv26HT3Yni3AV/DGw0cGnnw==" + }, "snapdragon": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", @@ -3909,6 +4781,25 @@ } } }, + "socks": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.4.4.tgz", + "integrity": "sha512-7LmHN4IHj1Vpd/k8D872VGCHJ6yIVyeFkfIBExRmGPYQ/kdUkpdg9eKh9oOzYYYKQhuxavayJHTnmBG+EzluUA==", + "requires": { + "ip": "^1.1.5", + "smart-buffer": "^4.1.0" + } + }, + "socks-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-5.0.0.tgz", + "integrity": "sha512-lEpa1zsWCChxiynk+lCycKuC502RxDWLKJZoIhnxrWNjLSDGYRFflHA1/228VkRcnv9TIb8w98derGbpKxJRgA==", + "requires": { + "agent-base": "6", + "debug": "4", + "socks": "^2.3.3" + } + }, "sort-array": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/sort-array/-/sort-array-4.1.2.tgz", @@ -3982,7 +4873,6 @@ "version": "1.16.1", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "optional": true, "requires": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", @@ -3998,8 +4888,30 @@ "tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "optional": true + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + } + } + }, + "ssri": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.0.tgz", + "integrity": "sha512-aq/pz989nxVYwn16Tsbj1TqFpD5LLrQxHf5zaHuieFV+R0Bbr4y8qUsOA45hXT/N4/9UNXTarBjnjVmjSOVaAA==", + "requires": { + "minipass": "^3.1.1" + }, + "dependencies": { + "minipass": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", + "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "requires": { + "yallist": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } } }, @@ -4313,7 +5225,6 @@ "version": "2.5.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "optional": true, "requires": { "psl": "^1.1.28", "punycode": "^2.1.1" @@ -4331,7 +5242,6 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "optional": true, "requires": { "safe-buffer": "^5.0.1" } @@ -4407,6 +5317,22 @@ "set-value": "^2.0.1" } }, + "unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "requires": { + "unique-slug": "^2.0.0" + } + }, + "unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "requires": { + "imurmurhash": "^0.1.4" + } + }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -4503,6 +5429,14 @@ "homedir-polyfill": "^1.0.1" } }, + "validate-npm-package-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz", + "integrity": "sha1-X6kS2B630MdK/BQN5zF/DKffQ34=", + "requires": { + "builtins": "^1.0.3" + } + }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -4512,7 +5446,6 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "optional": true, "requires": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", diff --git a/package.json b/package.json index 5afa350..ed6d3a7 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "moment": "^2.27.0", "mv": "^2.1.1", "mysql2": "^2.1.0", + "pacote": "^11.1.11", "public-ip": "^4.0.2", "sqlite3": "^5.0.0", "tmp": "^0.1.0", diff --git a/src/main.js b/src/main.js index e6de612..0a5b533 100644 --- a/src/main.js +++ b/src/main.js @@ -7,29 +7,13 @@ const knex = require("./knex"); const {messageQueue} = require("./queue"); const utils = require("./utils"); const { createCommandManager } = require("./commands"); -const { getPluginAPI, loadPlugin } = require("./plugins"); +const { getPluginAPI, installPlugins, loadPlugins } = require("./plugins"); const { callBeforeNewThreadHooks } = require("./hooks/beforeNewThread"); const blocked = require("./data/blocked"); const threads = require("./data/threads"); const updates = require("./data/updates"); -const reply = require("./modules/reply"); -const close = require("./modules/close"); -const snippets = require("./modules/snippets"); -const logs = require("./modules/logs"); -const move = require("./modules/move"); -const block = require("./modules/block"); -const suspend = require("./modules/suspend"); -const { plugin: webserver } = require("./modules/webserver"); -const greeting = require("./modules/greeting"); -const typingProxy = require("./modules/typingProxy"); -const version = require("./modules/version"); -const newthread = require("./modules/newthread"); -const idModule = require("./modules/id"); -const alert = require("./modules/alert"); -const joinLeaveNotification = require("./modules/joinLeaveNotification"); - const {ACCIDENTAL_THREAD_MESSAGES} = require("./data/constants"); module.exports = { @@ -301,36 +285,29 @@ async function initPlugins() { // Load plugins const builtInPlugins = [ - reply, - close, - logs, - block, - move, - snippets, - suspend, - greeting, - webserver, - typingProxy, - version, - newthread, - idModule, - alert, - joinLeaveNotification + "file:./src/modules/reply", + "file:./src/modules/close", + "file:./src/modules/logs", + "file:./src/modules/block", + "file:./src/modules/move", + "file:./src/modules/snippets", + "file:./src/modules/suspend", + "file:./src/modules/greeting", + "file:./src/modules/webserverPlugin", + "file:./src/modules/typingProxy", + "file:./src/modules/version", + "file:./src/modules/newthread", + "file:./src/modules/id", + "file:./src/modules/alert", + "file:./src/modules/joinLeaveNotification", ]; - const plugins = [...builtInPlugins]; + const plugins = [...builtInPlugins, ...config.plugins]; - if (config.plugins && config.plugins.length) { - for (const plugin of config.plugins) { - const pluginFn = require(`../${plugin}`); - plugins.push(pluginFn); - } - } + await installPlugins(plugins); const pluginApi = getPluginAPI({ bot, knex, config, commands }); - for (const plugin of plugins) { - await loadPlugin(plugin, pluginApi); - } + await loadPlugins(plugins, pluginApi); if (config.updateNotifications) { updates.startVersionRefreshLoop(); diff --git a/src/modules/webserver.js b/src/modules/webserver.js index ae60e9e..0462e7b 100644 --- a/src/modules/webserver.js +++ b/src/modules/webserver.js @@ -64,9 +64,4 @@ server.on("error", err => { console.log("[WARN] Web server error:", err.message); }); -module.exports = { - server, - plugin() { - server.listen(config.port); - }, -}; +module.exports = server; diff --git a/src/modules/webserverPlugin.js b/src/modules/webserverPlugin.js new file mode 100644 index 0000000..4f607b0 --- /dev/null +++ b/src/modules/webserverPlugin.js @@ -0,0 +1,5 @@ +const server = require("./webserver"); + +module.exports = ({ config }) => { + server.listen(config.port); +}; diff --git a/src/plugins.js b/src/plugins.js index f50fa64..1d9fdd5 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -4,8 +4,94 @@ const { beforeNewThread } = require("./hooks/beforeNewThread"); const { afterThreadClose } = require("./hooks/afterThreadClose"); const formats = require("./formatters"); const { server: webserver } = require("./modules/webserver"); +const childProcess = require("child_process"); +const pacote = require("pacote"); +const path = require("path"); + +const pluginSources = { + npm: { + install(plugins) { + return new Promise((resolve, reject) => { + console.log(`Installing ${plugins.length} plugins from NPM...`); + + let stderr = ""; + const npmProcess = childProcess.spawn("npm", ["install", "--no-save", ...plugins], { cwd: process.cwd() }); + npmProcess.stderr.on("data", data => { stderr += String(data) }); + npmProcess.on("close", code => { + if (code !== 0) { + return reject(new Error(stderr)); + } + + return resolve(); + }); + }); + }, + + async load(plugin, pluginApi) { + const manifest = await pacote.manifest(plugin); + const packageName = manifest.name; + const pluginFn = require(packageName); + if (typeof pluginFn !== "function") { + throw new Error(`Plugin '${plugin}' is not a valid plugin`); + } + + return pluginFn(pluginApi); + }, + }, + + file: { + install(plugins) {}, + load(plugin, pluginApi) { + const requirePath = path.join(__dirname, "..", plugin); + const pluginFn = require(requirePath); + if (typeof pluginFn !== "function") { + throw new Error(`Plugin '${plugin}' is not a valid plugin`); + } + return pluginFn(pluginApi); + }, + } +}; + +const defaultPluginSource = "file"; + +function splitPluginSource(pluginName) { + for (const pluginSource of Object.keys(pluginSources)) { + if (pluginName.startsWith(`${pluginSource}:`)) { + return { + source: pluginSource, + plugin: pluginName.slice(pluginSource.length + 1), + }; + } + } + + return { + source: defaultPluginSource, + plugin: pluginName, + }; +} module.exports = { + async installPlugins(plugins) { + const pluginsBySource = {}; + + for (const pluginName of plugins) { + const { source, plugin } = splitPluginSource(pluginName); + pluginsBySource[source] = pluginsBySource[source] || []; + pluginsBySource[source].push(plugin); + } + + for (const [source, sourcePlugins] of Object.entries(pluginsBySource)) { + await pluginSources[source].install(sourcePlugins); + } + }, + + async loadPlugins(plugins, pluginApi) { + for (const pluginName of plugins) { + const { source, plugin } = splitPluginSource(pluginName); + await pluginSources[source].load(plugin, pluginApi); + } + }, + /** * @param bot * @param knex @@ -44,8 +130,4 @@ module.exports = { webserver, }; }, - - async loadPlugin(plugin, api) { - await plugin(api); - } }; From c58ebf56985db1b95a1296f645add21817a70590 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 4 Oct 2020 17:19:49 +0300 Subject: [PATCH 158/300] Update CHANGELOG and documentation on plugin loading --- CHANGELOG.md | 2 ++ docs/plugins.md | 12 +++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d6fa76..ed335f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ Please report any bugs you encounter by [creating a GitHub issue](https://github **General changes:** * Fix occasional bug with expiring blocks where the bot would send the expiry message multiple times +* Plugins can now also be installed from NPM modules + * Example: `plugins[] = npm:some-plugin-package` **Plugins:** * Log storage functions `getLogUrl()`, `getLogFile()`, `getLogCustomResponse()` now take the entire thread object as an argument rather than the thread ID diff --git a/docs/plugins.md b/docs/plugins.md index b323f38..b264533 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -2,9 +2,15 @@ The bot supports loading external plugins. ## Specifying plugins to load -For each plugin file you'd like to load, add the file path to the [`plugins` option](configuration.md#plugins). -The path is relative to the bot's folder. -Plugins are automatically loaded on bot startup. +Plugins can be loaded either from local files or NPM. Examples: +```ini +# Local file +plugins[] = ./path/to/plugin.js +# NPM package +plugins[] = npm:some-plugin-package +``` +Paths to local files are always relative to the bot's folder. +NPM plugins are automatically installed on bot start-up. ## Creating a plugin Plugins are simply `.js` files that export a function that gets called when the plugin is loaded. From aadda310693b0789216945f2ef1b1510826c907d Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 12 Oct 2020 19:57:35 +0300 Subject: [PATCH 159/300] Add metadata field for threads Useful for e.g. storing plugin-specific data about a thread. --- src/data/Thread.js | 31 +++++++++++++++++++ ...012183935_add_metadata_field_to_threads.js | 11 +++++++ 2 files changed, 42 insertions(+) create mode 100644 src/data/migrations/20201012183935_add_metadata_field_to_threads.js diff --git a/src/data/Thread.js b/src/data/Thread.js index ac32c9d..ef1373d 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -28,6 +28,7 @@ const {THREAD_MESSAGE_TYPE, THREAD_STATUS, DISCORD_MESSAGE_ACTIVITY_TYPES} = req * @property {String} log_storage_type * @property {Object} log_storage_data * @property {String} created_at + * @property {String} metadata */ class Thread { constructor(props) { @@ -38,6 +39,12 @@ class Thread { this.log_storage_data = JSON.parse(props.log_storage_data); } } + + if (props.metadata) { + if (typeof props.metadata === "string") { + this.metadata = JSON.parse(props.metadata); + } + } } getSQLProps() { @@ -767,6 +774,30 @@ class Thread { log_storage_data, }); } + + /** + * @param {string} key + * @param {*} value + * @return {Promise} + */ + async setMetadataValue(key, value) { + this.metadata = this.metadata || {}; + this.metadata[key] = value; + + await knex("threads") + .where("id", this.id) + .update({ + metadata: this.getSQLProps().metadata, + }); + } + + /** + * @param {string} key + * @returns {*} + */ + getMetadataValue(key) { + return this.metadata ? this.metadata[key] : null; + } } module.exports = Thread; diff --git a/src/data/migrations/20201012183935_add_metadata_field_to_threads.js b/src/data/migrations/20201012183935_add_metadata_field_to_threads.js new file mode 100644 index 0000000..d567f20 --- /dev/null +++ b/src/data/migrations/20201012183935_add_metadata_field_to_threads.js @@ -0,0 +1,11 @@ +exports.up = async function(knex) { + await knex.schema.table("threads", table => { + table.text("metadata").nullable().defaultTo(null); + }); +}; + +exports.down = async function(knex) { + await knex.schema.table("threads", table => { + table.dropColumn("metadata"); + }); +}; From a3c1ed0a280631562dc05e9c2d66267447b1c75e Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 12 Oct 2020 19:59:47 +0300 Subject: [PATCH 160/300] Include DM message in beforeNewThread hook data. Allow specifying categoryId in createNewThreadForUser(). --- src/data/threads.js | 16 ++++++++++------ src/hooks/beforeNewThread.js | 2 ++ src/main.js | 1 + 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/data/threads.js b/src/data/threads.js index 9ed03a8..c4429d6 100644 --- a/src/data/threads.js +++ b/src/data/threads.js @@ -1,4 +1,4 @@ -const {User, Member} = require("eris"); +const {User, Member, Message} = require("eris"); const transliterate = require("transliteration"); const moment = require("moment"); @@ -53,9 +53,12 @@ function getHeaderGuildInfo(member) { /** * @typedef CreateNewThreadForUserOpts - * @property {boolean} quiet If true, doesn't ping mentionRole or reply with responseMessage - * @property {boolean} ignoreRequirements If true, creates a new thread even if the account doesn't meet requiredAccountAge - * @property {string} source A string identifying the source of the new thread + * @property {boolean} [quiet] If true, doesn't ping mentionRole or reply with responseMessage + * @property {boolean} [ignoreRequirements] If true, creates a new thread even if the account doesn't meet requiredAccountAge + * @property {boolean} [ignoreHooks] If true, doesn't call beforeNewThread hooks + * @property {Message} [message] Original DM message that is trying to start the thread, if there is one + * @property {string} [categoryId] Category where to open the thread + * @property {string} [source] A string identifying the source of the new thread */ /** @@ -68,6 +71,7 @@ function getHeaderGuildInfo(member) { async function createNewThreadForUser(user, opts = {}) { const quiet = opts.quiet != null ? opts.quiet : false; const ignoreRequirements = opts.ignoreRequirements != null ? opts.ignoreRequirements : false; + const ignoreHooks = opts.ignoreHooks != null ? opts.ignoreHooks : false; const existingThread = await findOpenThreadByUserId(user.id); if (existingThread) { @@ -127,7 +131,7 @@ async function createNewThreadForUser(user, opts = {}) { } // Call any registered beforeNewThreadHooks - const hookResult = await callBeforeNewThreadHooks({ user, opts }); + const hookResult = await callBeforeNewThreadHooks({ user, opts, message: opts.message }); if (hookResult.cancelled) return; // Use the user's name+discrim for the thread channel's name @@ -145,7 +149,7 @@ async function createNewThreadForUser(user, opts = {}) { console.log(`[NOTE] Creating new thread channel ${channelName}`); // Figure out which category we should place the thread channel in - let newThreadCategoryId = hookResult.categoryId || null; + let newThreadCategoryId = hookResult.categoryId || opts.categoryId || null; if (! newThreadCategoryId && config.categoryAutomation.newThreadFromServer) { // Categories for specific source guilds (in case of multiple main guilds) diff --git a/src/hooks/beforeNewThread.js b/src/hooks/beforeNewThread.js index bfde8bd..e581163 100644 --- a/src/hooks/beforeNewThread.js +++ b/src/hooks/beforeNewThread.js @@ -9,6 +9,7 @@ const Eris = require("eris"); /** * @typedef BeforeNewThreadHookData * @property {Eris.User} user + * @property {Eris.Message} [message] * @property {CreateNewThreadForUserOpts} opts * @property {Function} cancel * @property {BeforeNewThreadHook_SetCategoryId} setCategoryId @@ -48,6 +49,7 @@ beforeNewThread = (fn) => { /** * @param {{ * user: Eris.User, + * message?: Eris.Message, * opts: CreateNewThreadForUserOpts, * }} input * @return {Promise} diff --git a/src/main.js b/src/main.js index 0a5b533..45ce593 100644 --- a/src/main.js +++ b/src/main.js @@ -155,6 +155,7 @@ function initBaseMessageHandlers() { thread = await threads.createNewThreadForUser(msg.author, { source: "dm", + message: msg, }); } From f5caa80c4c15f9c906870d8ee2e8590f1bd69e96 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 12 Oct 2020 20:00:25 +0300 Subject: [PATCH 161/300] Allow plugins to access the 'threads' module for creating new threads --- src/pluginApi.js | 2 ++ src/plugins.js | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/pluginApi.js b/src/pluginApi.js index 4ae092d..4fdcac9 100644 --- a/src/pluginApi.js +++ b/src/pluginApi.js @@ -2,6 +2,7 @@ const express = require("express"); const { CommandManager } = require("knub-command-manager"); const { Client } = require("eris"); const Knex = require("knex"); +const threads = require("./data/threads"); /** * @typedef {object} PluginAPI @@ -14,6 +15,7 @@ const Knex = require("knex"); * @property {PluginHooksAPI} hooks * @property {FormattersExport} formats * @property {express.Application} webserver + * @property {threads} threads */ /** diff --git a/src/plugins.js b/src/plugins.js index 1d9fdd5..c0741fe 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -7,6 +7,7 @@ const { server: webserver } = require("./modules/webserver"); const childProcess = require("child_process"); const pacote = require("pacote"); const path = require("path"); +const threads = require("./data/threads"); const pluginSources = { npm: { @@ -128,6 +129,7 @@ module.exports = { }, formats, webserver, + threads, }; }, }; From c9e0dbf0407484a30c1b796765c9a0514f3a3cd8 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 12 Oct 2020 20:00:55 +0300 Subject: [PATCH 162/300] Handle unhandled rejections the same way as uncaught exceptions --- src/index.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/index.js b/src/index.js index 73a4b29..cc2d662 100644 --- a/src/index.js +++ b/src/index.js @@ -17,7 +17,7 @@ try { } // Error handling -process.on("uncaughtException", err => { +function errorHandler(err) { // Unknown message types (nitro boosting messages at the time) should be safe to ignore if (err && err.message && err.message.startsWith("Unhandled MESSAGE_CREATE type")) { return; @@ -26,7 +26,9 @@ process.on("uncaughtException", err => { // For everything else, crash with the error console.error(err); process.exit(1); -}); +} +process.on("uncaughtException", errorHandler); +process.on("unhandledRejection", errorHandler); let testedPackage = ""; try { From f9671c385d606f7a4585fc6701b01eb99754aa85 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 12 Oct 2020 20:33:43 +0300 Subject: [PATCH 163/300] Add formatters for system messages This has backwards-compatibility-breaking changes to the signature of thread.postSystemMessage() and thread.sendSystemMessageToUser(). --- src/data/Thread.js | 87 +++++++++++++++++++++++++++------------------- src/formatters.js | 69 ++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 36 deletions(-) diff --git a/src/data/Thread.js b/src/data/Thread.js index ef1373d..a6f0de5 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -359,8 +359,7 @@ class Thread { // Interrupt scheduled closing, if in progress if (this.scheduled_close_at) { await this.cancelScheduledClose(); - await this.postSystemMessage({ - content: `<@!${this.scheduled_close_id}> Thread that was scheduled to be closed got a new reply. Cancelling.`, + await this.postSystemMessage(`<@!${this.scheduled_close_id}> Thread that was scheduled to be closed got a new reply. Cancelling.`, { allowedMentions: { users: [this.scheduled_close_id], }, @@ -372,8 +371,7 @@ class Thread { const mentionsStr = ids.map(id => `<@!${id}> `).join(""); await this.deleteAlerts(); - await this.postSystemMessage({ - content: `${mentionsStr}New message from ${this.user_name}`, + await this.postSystemMessage(`${mentionsStr}New message from ${this.user_name}`, { allowedMentions: { users: ids, }, @@ -389,47 +387,64 @@ class Thread { } /** - * @param {Eris.MessageContent} content - * @param {Eris.MessageFile} file + * @param {string} text * @param {object} opts - * @param {boolean} opts.saveToLog - * @param {string} opts.logBody + * @param {object} [allowedMentions] Allowed mentions for the thread channel message + * @param {boolean} [allowedMentions.everyone] + * @param {boolean|string[]} [allowedMentions.roles] + * @param {boolean|string[]} [allowedMentions.users] * @returns {Promise} */ - async postSystemMessage(content, file = null, opts = {}) { - const msg = await this._postToThreadChannel(content, file); - if (msg && opts.saveToLog !== false) { - await this._addThreadMessageToDB({ - message_type: THREAD_MESSAGE_TYPE.SYSTEM, - user_id: null, - user_name: "", - body: msg.content || "", - is_anonymous: 0, - inbox_message_id: msg.id, - }); - } + async postSystemMessage(text, opts = {}) { + const threadMessage = new ThreadMessage({ + message_type: THREAD_MESSAGE_TYPE.SYSTEM, + user_id: null, + user_name: "", + body: text, + is_anonymous: 0, + }); + + const content = await formatters.formatSystemThreadMessage(threadMessage); + + const finalContent = typeof content === "string" ? { content } : content; + finalContent.allowedMentions = opts.allowedMentions; + const msg = await this._postToThreadChannel(finalContent); + + threadMessage.inbox_message_id = msg.id; + await this._addThreadMessageToDB(threadMessage.getSQLProps()); } /** - * @param {Eris.MessageContent} content - * @param {Eris.MessageFile} file + * @param {string} text * @param {object} opts - * @param {boolean} opts.saveToLog - * @param {string} opts.logBody + * @param {object} [allowedMentions] Allowed mentions for the thread channel message + * @param {boolean} [allowedMentions.everyone] + * @param {boolean|string[]} [allowedMentions.roles] + * @param {boolean|string[]} [allowedMentions.users] * @returns {Promise} */ - async sendSystemMessageToUser(content, file = null, opts = {}) { - const msg = await this._sendDMToUser(content, file); - if (opts.saveToLog !== false) { - await this._addThreadMessageToDB({ - message_type: THREAD_MESSAGE_TYPE.SYSTEM_TO_USER, - user_id: null, - user_name: "", - body: msg.content || "", - is_anonymous: 0, - dm_message_id: msg.id, - }); - } + async sendSystemMessageToUser(text, opts = {}) { + const threadMessage = new ThreadMessage({ + message_type: THREAD_MESSAGE_TYPE.SYSTEM_TO_USER, + user_id: null, + user_name: "", + body: text, + is_anonymous: 0, + }); + + const dmContent = await formatters.formatSystemToUserDM(threadMessage); + const dmMsg = await this._sendDMToUser(dmContent); + + const inboxContent = await formatters.formatSystemToUserThreadMessage(threadMessage); + const finalInboxContent = typeof inboxContent === "string" ? { content: inboxContent } : inboxContent; + finalInboxContent.allowedMentions = opts.allowedMentions; + const inboxMsg = await this._postToThreadChannel(inboxContent); + + threadMessage.inbox_message_id = inboxMsg.id; + threadMessage.dm_channel_id = dmMsg.channel.id; + threadMessage.dm_message_id = dmMsg.id; + + await this._addThreadMessageToDB(threadMessage.getSQLProps()); } /** diff --git a/src/formatters.js b/src/formatters.js index d469d44..4216866 100644 --- a/src/formatters.js +++ b/src/formatters.js @@ -43,6 +43,27 @@ const moment = require("moment"); * @return {Eris.MessageContent} Message content to post in the thread channel */ +/** + * Function to format a system message in a thread channel + * @callback FormatSystemThreadMessage + * @param {ThreadMessage} threadMessage + * @return {Eris.MessageContent} Message content to post in the thread channel + */ + +/** + * Function to format a system message sent to the user in a thread channel + * @callback FormatSystemToUserThreadMessage + * @param {ThreadMessage} threadMessage + * @return {Eris.MessageContent} Message content to post in the thread channel + */ + +/** + * Function to format the DM that is sent to the user when the bot sends a system message to the user + * @callback FormatSystemToUserDM + * @param {ThreadMessage} threadMessage + * @return {Eris.MessageContent} Message content to send as a DM + */ + /** * @typedef {Object} FormatLogOptions * @property {Boolean?} simple @@ -71,6 +92,9 @@ const moment = require("moment"); * @property {FormatUserReplyThreadMessage} formatUserReplyThreadMessage * @property {FormatStaffReplyEditNotificationThreadMessage} formatStaffReplyEditNotificationThreadMessage * @property {FormatStaffReplyDeletionNotificationThreadMessage} formatStaffReplyDeletionNotificationThreadMessage + * @property {FormatSystemThreadMessage} formatSystemThreadMessage + * @property {FormatSystemToUserThreadMessage} formatSystemToUserThreadMessage + * @property {FormatSystemToUserDM} formatSystemToUserDM * @property {FormatLog} formatLog */ @@ -148,6 +172,36 @@ const defaultFormatters = { return content; }, + formatSystemThreadMessage(threadMessage) { + let result = threadMessage.body; + + for (const link of threadMessage.attachments) { + result += `\n\n${link}`; + } + + return result; + }, + + formatSystemToUserThreadMessage(threadMessage) { + let result = `**[BOT TO USER]** ${threadMessage.body}`; + + for (const link of threadMessage.attachments) { + result += `\n\n${link}`; + } + + return result; + }, + + formatSystemToUserDM(threadMessage) { + let result = threadMessage.body; + + for (const link of threadMessage.attachments) { + result += `\n\n${link}`; + } + + return result; + }, + formatLog(thread, threadMessages, opts = {}) { if (opts.simple) { threadMessages = threadMessages.filter(message => { @@ -242,6 +296,9 @@ const formatters = { ...defaultFormatters }; * @property {function(FormatUserReplyThreadMessage): void} setUserReplyThreadMessageFormatter * @property {function(FormatStaffReplyEditNotificationThreadMessage): void} setStaffReplyEditNotificationThreadMessageFormatter * @property {function(FormatStaffReplyDeletionNotificationThreadMessage): void} setStaffReplyDeletionNotificationThreadMessageFormatter + * @property {function(FormatSystemThreadMessage): void} setSystemThreadMessageFormatter + * @property {function(FormatSystemToUserThreadMessage): void} setSystemToUserThreadMessageFormatter + * @property {function(FormatSystemToUserDM): void} setSystemToUserDMFormatter * @property {function(FormatLog): void} setLogFormatter */ @@ -275,6 +332,18 @@ module.exports = { formatters.formatStaffReplyDeletionNotificationThreadMessage = fn; }, + setSystemThreadMessageFormatter(fn) { + formatters.formatSystemThreadMessage = fn; + }, + + setSystemToUserThreadMessageFormatter(fn) { + formatters.formatSystemToUserThreadMessage = fn; + }, + + setSystemToUserDMFormatter(fn) { + formatters.formatSystemToUserDM = fn; + }, + setLogFormatter(fn) { formatters.formatLog = fn; }, From 5de750bc3e7e1f1e293073a2157ae4879026beed Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 12 Oct 2020 20:53:53 +0300 Subject: [PATCH 164/300] Add metadata field for thread messages --- src/data/ThreadMessage.js | 32 +++++++++++++++++++ ...2_add_metadata_field_to_thread_messages.js | 11 +++++++ 2 files changed, 43 insertions(+) create mode 100644 src/data/migrations/20201012203622_add_metadata_field_to_thread_messages.js diff --git a/src/data/ThreadMessage.js b/src/data/ThreadMessage.js index b05f3d6..eb6b4f7 100644 --- a/src/data/ThreadMessage.js +++ b/src/data/ThreadMessage.js @@ -37,6 +37,12 @@ class ThreadMessage { } else { this.small_attachments = []; } + + if (props.metadata) { + if (typeof props.metadata === "string") { + this.metadata = JSON.parse(props.metadata); + } + } } getSQLProps() { @@ -50,6 +56,32 @@ class ThreadMessage { return obj; }, {}); } + + /** + * @param {string} key + * @param {*} value + * @return {Promise} + */ + async setMetadataValue(key, value) { + this.metadata = this.metadata || {}; + this.metadata[key] = value; + + if (this.id) { + await knex("thread_messages") + .where("id", this.id) + .update({ + metadata: this.getSQLProps().metadata, + }); + } + } + + /** + * @param {string} key + * @returns {*} + */ + getMetadataValue(key) { + return this.metadata ? this.metadata[key] : null; + } } module.exports = ThreadMessage; diff --git a/src/data/migrations/20201012203622_add_metadata_field_to_thread_messages.js b/src/data/migrations/20201012203622_add_metadata_field_to_thread_messages.js new file mode 100644 index 0000000..8822f1b --- /dev/null +++ b/src/data/migrations/20201012203622_add_metadata_field_to_thread_messages.js @@ -0,0 +1,11 @@ +exports.up = async function(knex) { + await knex.schema.table("thread_messages", table => { + table.text("metadata").nullable().defaultTo(null); + }); +}; + +exports.down = async function(knex) { + await knex.schema.table("thread_messages", table => { + table.dropColumn("metadata"); + }); +}; From 5b0f9d31b7ad1d4d78cb0120dedde8d248185e88 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 12 Oct 2020 20:54:42 +0300 Subject: [PATCH 165/300] Create new message types for staff reply edits/deletions This has backwards-compatibility-breaking changes to the formatters of staff reply edits/deletions, which now only receive the thread message for the edit/deletion with the original data in the thread message's metadata. --- src/data/Thread.js | 31 +++++++++++++++++++++++++++---- src/data/constants.js | 4 +++- src/formatters.js | 40 +++++++++++++++++++++++++--------------- 3 files changed, 55 insertions(+), 20 deletions(-) diff --git a/src/data/Thread.js b/src/data/Thread.js index a6f0de5..a84734e 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -745,8 +745,20 @@ class Thread { await bot.editMessage(this.channel_id, threadMessage.inbox_message_id, formattedThreadMessage); if (! opts.quiet) { - const threadNotification = formatters.formatStaffReplyEditNotificationThreadMessage(threadMessage, newText, moderator); - await this.postSystemMessage(threadNotification); + const editThreadMessage = new ThreadMessage({ + message_type: THREAD_MESSAGE_TYPE.REPLY_EDITED, + user_id: null, + user_name: "", + body: "", + is_anonymous: 0, + }); + editThreadMessage.setMetadataValue("originalThreadMessage", threadMessage); + editThreadMessage.setMetadataValue("newBody", newText); + + const threadNotification = formatters.formatStaffReplyEditNotificationThreadMessage(editThreadMessage); + const inboxMessage = await this._postToThreadChannel(threadNotification); + editThreadMessage.inbox_message_id = inboxMessage.id; + await this._addThreadMessageToDB(editThreadMessage.getSQLProps()); } await this._updateThreadMessage(threadMessage.id, { body: newText }); @@ -764,8 +776,19 @@ class Thread { await bot.deleteMessage(this.channel_id, threadMessage.inbox_message_id); if (! opts.quiet) { - const threadNotification = formatters.formatStaffReplyDeletionNotificationThreadMessage(threadMessage, moderator); - await this.postSystemMessage(threadNotification); + const deletionThreadMessage = new ThreadMessage({ + message_type: THREAD_MESSAGE_TYPE.REPLY_DELETED, + user_id: null, + user_name: "", + body: "", + is_anonymous: 0, + }); + deletionThreadMessage.setMetadataValue("originalThreadMessage", threadMessage); + + const threadNotification = formatters.formatStaffReplyDeletionNotificationThreadMessage(deletionThreadMessage); + const inboxMessage = await this._postToThreadChannel(threadNotification); + deletionThreadMessage.inbox_message_id = inboxMessage.id; + await this._addThreadMessageToDB(deletionThreadMessage.getSQLProps()); } await this._deleteThreadMessage(threadMessage.id); diff --git a/src/data/constants.js b/src/data/constants.js index 221becd..992d0a1 100644 --- a/src/data/constants.js +++ b/src/data/constants.js @@ -12,7 +12,9 @@ module.exports = { TO_USER: 4, LEGACY: 5, COMMAND: 6, - SYSTEM_TO_USER: 7 + SYSTEM_TO_USER: 7, + REPLY_EDITED: 8, + REPLY_DELETED: 9, }, // https://discord.com/developers/docs/resources/channel#channel-object-channel-types diff --git a/src/formatters.js b/src/formatters.js index 4216866..a173ac8 100644 --- a/src/formatters.js +++ b/src/formatters.js @@ -30,8 +30,6 @@ const moment = require("moment"); * Function to format the inbox channel notification for a staff reply edit * @callback FormatStaffReplyEditNotificationThreadMessage * @param {ThreadMessage} threadMessage - * @param {string} newText - * @param {Eris.Member} moderator Moderator that edited the message * @return {Eris.MessageContent} Message content to post in the thread channel */ @@ -39,7 +37,6 @@ const moment = require("moment"); * Function to format the inbox channel notification for a staff reply deletion * @callback FormatStaffReplyDeletionNotificationThreadMessage * @param {ThreadMessage} threadMessage - * @param {Eris.Member} moderator Moderator that deleted the message * @return {Eris.MessageContent} Message content to post in the thread channel */ @@ -142,31 +139,35 @@ const defaultFormatters = { return result; }, - formatStaffReplyEditNotificationThreadMessage(threadMessage, newText, moderator) { - let content = `**${moderator.user.username}#${moderator.user.discriminator}** (\`${moderator.id}\`) edited reply \`${threadMessage.message_number}\``; + formatStaffReplyEditNotificationThreadMessage(threadMessage) { + const originalThreadMessage = threadMessage.getMetadataValue("originalThreadMessage"); + const newBody = threadMessage.getMetadataValue("newBody"); - if (threadMessage.body.length < 200 && newText.length < 200) { + let content = `**${originalThreadMessage.user_name}** (\`${originalThreadMessage.user_id}\`) edited reply \`${originalThreadMessage.message_number}\``; + + if (originalThreadMessage.body.length < 200 && newBody.length < 200) { // Show edits of small messages inline - content += ` from \`${utils.disableInlineCode(threadMessage.body)}\` to \`${newText}\``; + content += ` from \`${utils.disableInlineCode(originalThreadMessage.body)}\` to \`${newBody}\``; } else { // Show edits of long messages in two code blocks content += ":"; - content += `\n\nBefore:\n\`\`\`${utils.disableCodeBlocks(threadMessage.body)}\`\`\``; - content += `\nAfter:\n\`\`\`${utils.disableCodeBlocks(newText)}\`\`\``; + content += `\n\nBefore:\n\`\`\`${utils.disableCodeBlocks(originalThreadMessage.body)}\`\`\``; + content += `\nAfter:\n\`\`\`${utils.disableCodeBlocks(newBody)}\`\`\``; } return content; }, - formatStaffReplyDeletionNotificationThreadMessage(threadMessage, moderator) { - let content = `**${moderator.user.username}#${moderator.user.discriminator}** (\`${moderator.id}\`) deleted reply \`${threadMessage.message_number}\``; + formatStaffReplyDeletionNotificationThreadMessage(threadMessage) { + const originalThreadMessage = threadMessage.getMetadataValue("originalThreadMessage"); + let content = `**${originalThreadMessage.user_name}** (\`${originalThreadMessage.user_id}\`) deleted reply \`${originalThreadMessage.message_number}\``; - if (threadMessage.body.length < 200) { + if (originalThreadMessage.body.length < 200) { // Show the original content of deleted small messages inline - content += ` (message content: \`${utils.disableInlineCode(threadMessage.body)}\`)`; + content += ` (message content: \`${utils.disableInlineCode(originalThreadMessage.body)}\`)`; } else { // Show the original content of deleted large messages in a code block - content += ":\n```" + utils.disableCodeBlocks(threadMessage.body) + "```"; + content += ":\n```" + utils.disableCodeBlocks(originalThreadMessage.body) + "```"; } return content; @@ -183,7 +184,7 @@ const defaultFormatters = { }, formatSystemToUserThreadMessage(threadMessage) { - let result = `**[BOT TO USER]** ${threadMessage.body}`; + let result = `**[SYSTEM TO USER]** ${threadMessage.body}`; for (const link of threadMessage.attachments) { result += `\n\n${link}`; @@ -265,6 +266,15 @@ const defaultFormatters = { line += ` [CHAT] [${message.user_name}] ${message.body}`; } else if (message.message_type === THREAD_MESSAGE_TYPE.COMMAND) { line += ` [COMMAND] [${message.user_name}] ${message.body}`; + } else if (message.message_type === THREAD_MESSAGE_TYPE.REPLY_EDITED) { + const originalThreadMessage = message.getMetadataValue("originalThreadMessage"); + line += ` [REPLY EDITED] ${originalThreadMessage.user_name} edited reply ${originalThreadMessage.message_number}:`; + line += `\n\nBefore:\n${originalThreadMessage.body}`; + line += `\n\nAfter:\n${message.getMetadataValue("newBody")}`; + } else if (message.message_type === THREAD_MESSAGE_TYPE.REPLY_DELETED) { + const originalThreadMessage = message.getMetadataValue("originalThreadMessage"); + line += ` [REPLY DELETED] ${originalThreadMessage.user_name} deleted reply ${originalThreadMessage.message_number}:`; + line += `\n\n${originalThreadMessage.body}`; } else { line += ` [${message.user_name}] ${message.body}`; } From 192fec6952bfc404705c13c050720c207db6b4e0 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 19 Oct 2020 01:50:12 +0300 Subject: [PATCH 166/300] Fix rounding in thread messages that only contain an ID Internally, the thread_messages table's body column's type was undefined in sqlite. This is because it was set to mediumtext, but sqlite doesn't have that type. Because of that, sqlite treated the column as a numeric column. Sqlite also allows storing text data in numeric columns (because reasons), so in most cases everything worked fine. It was only when storing an actual number, like an ID, that it was also *treated* as a number. And since snowflake IDs are larger than the maximum safe integer in JavaScript, the stored ID could get rounded when retrieving data from the database. --- ...31_fix_thread_messages_body_column_type.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/data/migrations/20201019013831_fix_thread_messages_body_column_type.js diff --git a/src/data/migrations/20201019013831_fix_thread_messages_body_column_type.js b/src/data/migrations/20201019013831_fix_thread_messages_body_column_type.js new file mode 100644 index 0000000..ecfbdd5 --- /dev/null +++ b/src/data/migrations/20201019013831_fix_thread_messages_body_column_type.js @@ -0,0 +1,19 @@ +exports.up = async function(knex) { + await knex.schema.table("thread_messages", table => { + table.text("temp_body"); + }); + + await knex.raw("UPDATE thread_messages SET temp_body = body"); + + await knex.schema.table("thread_messages", table => { + table.dropColumn("body"); + }); + + await knex.schema.table("thread_messages", table => { + table.renameColumn("temp_body", "body"); + }); +}; + +exports.down = async function(knex) { + +}; From 42f6e79df80716092c15b779eb3791f37498b682 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 21 Oct 2020 21:00:45 +0300 Subject: [PATCH 167/300] Remove faulty message chunking logic in user DMs The logic could cause things like code blocks get cut in the middle without being handled gracefully. With this change, messages sent to the user can take, at most, 1 message. This does not affect messages in the thread channel. --- src/data/Thread.js | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/src/data/Thread.js b/src/data/Thread.js index a84734e..fdb789a 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -73,29 +73,7 @@ class Thread { throw new Error("Could not open DMs with the user. They may have blocked the bot or set their privacy settings higher."); } - let firstMessage; - - if (typeof content === "string") { - // Content is a string, chunk it and send it as individual messages. - // Files (attachments) are only sent with the last message. - const chunks = utils.chunk(content, 2000); - for (const [i, chunk] of chunks.entries()) { - let msg; - if (i === chunks.length - 1) { - // Only send embeds, files, etc. with the last message - msg = await dmChannel.createMessage(chunk, file); - } else { - msg = await dmChannel.createMessage(chunk); - } - - firstMessage = firstMessage || msg; - } - } else { - // Content is a full message content object, send it as-is with the files (if any) - firstMessage = await dmChannel.createMessage(content, file); - } - - return firstMessage; + return dmChannel.createMessage(content, file); } /** From d5ea95d9e902185077f8c579908fc67449076988 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 21 Oct 2020 21:03:33 +0300 Subject: [PATCH 168/300] Limit replies to fit within one message This applies to both the DM to be sent to the user, and the message created in the thread channel. This is because edits (via !edit) could change the amount of messages a reply takes (based on message formatting), leading to either being unable to post the full edit if it goes over the message limits, or having to edit a previous message to be 'empty' if the result of the edit would take fewer messages to post than the original reply. This also fixes an issue where !edit/!delete would not apply to more than the first message created by a reply - whether in user DMs or in the thread channel. --- src/data/Thread.js | 18 ++++++++++++++-- src/utils.js | 52 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/src/data/Thread.js b/src/data/Thread.js index fdb789a..8cd79e8 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -223,8 +223,16 @@ class Thread { attachments: attachmentLinks, }); - // Send the reply DM const dmContent = formatters.formatStaffReplyDM(threadMessage); + const inboxContent = formatters.formatStaffReplyThreadMessage(threadMessage); + + // Because moderator replies have to be editable, we enforce them to fit within 1 message + if (! utils.messageContentIsWithinMaxLength(dmContent) || ! utils.messageContentIsWithinMaxLength(inboxContent)) { + await this.postSystemMessage("Reply is too long! Make sure your reply is under 2000 characters total, moderator name in the reply included."); + return false; + } + + // Send the reply DM let dmMessage; try { dmMessage = await this._sendDMToUser(dmContent, files); @@ -240,7 +248,6 @@ class Thread { }); // Show the reply in the inbox thread - const inboxContent = formatters.formatStaffReplyThreadMessage(threadMessage); const inboxMessage = await this._postToThreadChannel(inboxContent, files); if (inboxMessage) { await this._updateThreadMessage(threadMessage.id, { inbox_message_id: inboxMessage.id }); @@ -719,6 +726,13 @@ class Thread { const formattedThreadMessage = formatters.formatStaffReplyThreadMessage(newThreadMessage); const formattedDM = formatters.formatStaffReplyDM(newThreadMessage); + // Same restriction as in replies. Because edits could theoretically change the number of messages a reply takes, we enforce replies + // to fit within 1 message to avoid the headache and issues caused by that. + if (! utils.messageContentIsWithinMaxLength(formattedDM) || ! utils.messageContentIsWithinMaxLength(formattedThreadMessage)) { + await this.postSystemMessage("Edited reply is too long! Make sure the edit is under 2000 characters total, moderator name in the reply included."); + return false; + } + await bot.editMessage(threadMessage.dm_channel_id, threadMessage.dm_message_id, formattedDM); await bot.editMessage(this.channel_id, threadMessage.inbox_message_id, formattedThreadMessage); diff --git a/src/utils.js b/src/utils.js index d97447a..83118fe 100644 --- a/src/utils.js +++ b/src/utils.js @@ -336,6 +336,56 @@ function readMultilineConfigValue(str) { function noop() {} +// https://discord.com/developers/docs/resources/channel#create-message-params +const MAX_MESSAGE_CONTENT_LENGTH = 2000; + +// https://discord.com/developers/docs/resources/channel#embed-limits +const MAX_EMBED_CONTENT_LENGTH = 6000; + +/** + * Checks if the given message content is within Discord's message length limits. + * + * Based on testing, Discord appears to enforce length limits (at least in the client) + * the same way JavaScript does, using the UTF-16 byte count as the number of characters. + * + * @param {string|Eris.MessageContent} content + */ +function messageContentIsWithinMaxLength(content) { + if (typeof content === "string") { + content = { content }; + } + + if (content.content && content.content.length > MAX_MESSAGE_CONTENT_LENGTH) { + return false; + } + + if (content.embed) { + let embedContentLength = 0; + + if (content.embed.title) embedContentLength += content.embed.title.length; + if (content.embed.description) embedContentLength += content.embed.description.length; + if (content.embed.footer && content.embed.footer.text) { + embedContentLength += content.embed.footer.text.length; + } + if (content.embed.author && content.embed.author.name) { + embedContentLength += content.embed.author.name.length; + } + + if (content.embed.fields) { + for (const field of content.embed.fields) { + if (field.title) embedContentLength += field.name.length; + if (field.description) embedContentLength += field.value.length; + } + } + + if (embedContentLength > MAX_EMBED_CONTENT_LENGTH) { + return false; + } + } + + return true; +} + module.exports = { BotError, @@ -377,5 +427,7 @@ module.exports = { readMultilineConfigValue, + messageContentIsWithinMaxLength, + noop, }; From 4ecea31fd8e2fc4aef4beff701bec689a1f94ebb Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 21 Oct 2020 21:28:06 +0300 Subject: [PATCH 169/300] Fix 'undefined' message numbers --- src/data/Thread.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/data/Thread.js b/src/data/Thread.js index 8cd79e8..f952340 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -213,7 +213,7 @@ class Thread { } } - let threadMessage = new ThreadMessage({ + const rawThreadMessage = new ThreadMessage({ message_type: THREAD_MESSAGE_TYPE.TO_USER, user_id: moderator.id, user_name: moderatorName, @@ -222,12 +222,14 @@ class Thread { role_name: roleName, attachments: attachmentLinks, }); + const threadMessage = await this._addThreadMessageToDB(rawThreadMessage.getSQLProps()); const dmContent = formatters.formatStaffReplyDM(threadMessage); const inboxContent = formatters.formatStaffReplyThreadMessage(threadMessage); // Because moderator replies have to be editable, we enforce them to fit within 1 message if (! utils.messageContentIsWithinMaxLength(dmContent) || ! utils.messageContentIsWithinMaxLength(inboxContent)) { + await this._deleteThreadMessage(rawThreadMessage); await this.postSystemMessage("Reply is too long! Make sure your reply is under 2000 characters total, moderator name in the reply included."); return false; } @@ -237,19 +239,18 @@ class Thread { try { dmMessage = await this._sendDMToUser(dmContent, files); } catch (e) { + await this._deleteThreadMessage(rawThreadMessage); await this.postSystemMessage(`Error while replying to user: ${e.message}`); return false; } - // Save the log entry - threadMessage = await this._addThreadMessageToDB({ - ...threadMessage.getSQLProps(), - dm_message_id: dmMessage.id, - }); + threadMessage.dm_message_id = dmMessage.id; + await this._updateThreadMessage(threadMessage.id, { dm_message_id: dmMessage.id }); // Show the reply in the inbox thread const inboxMessage = await this._postToThreadChannel(inboxContent, files); if (inboxMessage) { + threadMessage.inbox_message_id = inboxMessage.id; await this._updateThreadMessage(threadMessage.id, { inbox_message_id: inboxMessage.id }); } From 0a98b5cb49d223dd3f5111dad67874bd632d535a Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 21 Oct 2020 21:29:20 +0300 Subject: [PATCH 170/300] Don't remove !edit command if it fails --- src/data/Thread.js | 1 + src/modules/reply.js | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/data/Thread.js b/src/data/Thread.js index f952340..386a33f 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -755,6 +755,7 @@ class Thread { } await this._updateThreadMessage(threadMessage.id, { body: newText }); + return true; } /** diff --git a/src/modules/reply.js b/src/modules/reply.js index a26f531..17b5678 100644 --- a/src/modules/reply.js +++ b/src/modules/reply.js @@ -44,8 +44,8 @@ module.exports = ({ bot, knex, config, commands }) => { return; } - await thread.editStaffReply(msg.member, threadMessage, args.text); - msg.delete().catch(utils.noop); + const edited = await thread.editStaffReply(msg.member, threadMessage, args.text); + if (edited) msg.delete().catch(utils.noop); }, { aliases: ["e"] }); From 012bd5b9b25b193143f7ca63473f03ac59f5751b Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 21 Oct 2020 21:36:50 +0300 Subject: [PATCH 171/300] Fix faulty chunking for received user messages The chunks now properly handle code blocks and prefer to split at a newline rather than arbitrarily within the text. --- src/data/Thread.js | 11 ++++--- src/utils.js | 72 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 6 deletions(-) diff --git a/src/data/Thread.js b/src/data/Thread.js index 386a33f..1aeee44 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -89,13 +89,12 @@ class Thread { if (typeof content === "string") { // Content is a string, chunk it and send it as individual messages. // Files (attachments) are only sent with the last message. - const chunks = utils.chunk(content, 2000); + const chunks = utils.chunkMessageLines(content); for (const [i, chunk] of chunks.entries()) { - let msg; - if (i === chunks.length - 1) { - // Only send embeds, files, etc. with the last message - msg = await bot.createMessage(this.channel_id, chunk, file); - } + // Only send embeds, files, etc. with the last message + const msg = (i === chunks.length - 1) + ? await bot.createMessage(this.channel_id, chunk, file) + : await bot.createMessage(this.channel_id, chunk); firstMessage = firstMessage || msg; } diff --git a/src/utils.js b/src/utils.js index 83118fe..54a0d0c 100644 --- a/src/utils.js +++ b/src/utils.js @@ -386,6 +386,77 @@ function messageContentIsWithinMaxLength(content) { return true; } +/** + * Splits a string into chunks, preferring to split at a newline + * @param {string} str + * @param {number} [maxChunkLength=2000] + * @returns {string[]} + */ +function chunkByLines(str, maxChunkLength = 2000) { + if (str.length < maxChunkLength) { + return [str]; + } + + const chunks = []; + + while (str.length) { + if (str.length <= maxChunkLength) { + chunks.push(str); + break; + } + + const slice = str.slice(0, maxChunkLength); + + const lastLineBreakIndex = slice.lastIndexOf("\n"); + if (lastLineBreakIndex === -1) { + chunks.push(str.slice(0, maxChunkLength)); + str = str.slice(maxChunkLength); + } else { + chunks.push(str.slice(0, lastLineBreakIndex)); + str = str.slice(lastLineBreakIndex + 1); + } + } + + return chunks; +} + +/** + * Chunks a long message to multiple smaller messages, retaining leading and trailing line breaks, open code blocks, etc. + * + * Default maxChunkLength is 1990, a bit under the message length limit of 2000, so we have space to add code block + * shenanigans to the start/end when needed. Take this into account when choosing a custom maxChunkLength as well. + */ +function chunkMessageLines(str, maxChunkLength = 1990) { + const chunks = chunkByLines(str, maxChunkLength); + let openCodeBlock = false; + + return chunks.map(_chunk => { + // If the chunk starts with a newline, add an invisible unicode char so Discord doesn't strip it away + if (_chunk[0] === "\n") _chunk = "\u200b" + _chunk; + // If the chunk ends with a newline, add an invisible unicode char so Discord doesn't strip it away + if (_chunk[_chunk.length - 1] === "\n") _chunk = _chunk + "\u200b"; + // If the previous chunk had an open code block, open it here again + if (openCodeBlock) { + openCodeBlock = false; + if (_chunk.startsWith("```")) { + // Edge case: chunk starts with a code block delimiter, e.g. the previous chunk and this one were split right before the end of a code block + // Fix: just strip the code block delimiter away from here, we don't need it anymore + _chunk = _chunk.slice(3); + } else { + _chunk = "```" + _chunk; + } + } + // If the chunk has an open code block, close it and open it again in the next chunk + const codeBlockDelimiters = _chunk.match(/```/g); + if (codeBlockDelimiters && codeBlockDelimiters.length % 2 !== 0) { + _chunk += "```"; + openCodeBlock = true; + } + + return _chunk; + }); +} + module.exports = { BotError, @@ -428,6 +499,7 @@ module.exports = { readMultilineConfigValue, messageContentIsWithinMaxLength, + chunkMessageLines, noop, }; From ccd56a1d8886d0991c0219413f059e616471ea39 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 21 Oct 2020 21:41:38 +0300 Subject: [PATCH 172/300] Handle error 1006 (Connection reset by peer) gracefully --- src/bot.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/bot.js b/src/bot.js index 1338d03..4293f89 100644 --- a/src/bot.js +++ b/src/bot.js @@ -27,6 +27,16 @@ const bot = new Eris.Client(config.token, { }, }); +bot.on("error", err => { + if (err.code === 1006) { + // 1006 = "Connection reset by peer" + // Eris allegedly handles this internally, so we can ignore it + return; + } + + throw err; +}); + /** * @type {Eris.Client} */ From cc79ac88174c894f400f86338b2d34d3efb259bd Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 21 Oct 2020 21:50:58 +0300 Subject: [PATCH 173/300] Update CHANGELOG for beta.3 --- CHANGELOG.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed335f2..a7884e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,17 +5,28 @@ Please report any bugs you encounter by [creating a GitHub issue](https://github.com/Dragory/modmailbot/issues/new)! **General changes:** -* Fix occasional bug with expiring blocks where the bot would send the expiry message multiple times +* Replies are now limited in length to the Discord message limit (including the moderator name and role in the DM sent to the user) + * This was to fix issues with `!edit` and `!delete` when a reply spanned multiple messages * Plugins can now also be installed from NPM modules * Example: `plugins[] = npm:some-plugin-package` +* Fix occasional bug with expiring blocks where the bot would send the expiry message multiple times +* Fix bug with long messages being cut off and only the last part being shown in the thread (most evident in long DMs and e.g. !edit notifications of long messages) +* "Connection reset by peer" error (code 1006) is now handled gracefully in the background and no longer crashes the bot +* Fix messages containing *only* a large number (e.g. an ID) rounding the number **Plugins:** * Log storage functions `getLogUrl()`, `getLogFile()`, `getLogCustomResponse()` now take the entire thread object as an argument rather than the thread ID * Log storage function `save()` can now return information about the saved log to be stored with the thread. This can then be accessed in e.g. `getLogUrl()` via `thread.log_storage_data`. * Plugins can now access the bot's web server via a new `webserver` property in plugin arguments +* Plugins can now store *metadata* in threads and thread messages via new `setMetadataValue` and `getMetadataValue` functions on `Thread` and `ThreadMessage` objects +* Edit/delete notifications now have their own message type and formatter. The original message content is now included in the thread message's metadata (see above). +* System messages now have a formatter +* The `beforeNewThread` hook's parameters now also include the original DM message object +* Plugins can now access the `threads` module (via `pluginApi.threads`) to create and fetch threads **Internal/technical updates:** * Modmail now uses [Express](https://expressjs.com/) as its web server for logs/attachments +* Unhandled rejections are now handled the same as uncaught exceptions, and *will* crash the bot ## v2.31.0-beta.2 **This is a beta release, bugs are expected.** From 90dd35194ccab495581e886a66e2d5937e2e693b Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 21 Oct 2020 22:07:35 +0300 Subject: [PATCH 174/300] Add support for inline snippets. Fixes #464 --- CHANGELOG.md | 4 ++++ docs/configuration.md | 15 +++++++++++++++ src/data/Thread.js | 20 ++++++++++++++++++++ src/data/cfg.jsdoc.js | 3 +++ src/data/cfg.schema.json | 15 +++++++++++++++ 5 files changed, 57 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7884e8..2d9eb77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ Please report any bugs you encounter by [creating a GitHub issue](https://github **General changes:** * Replies are now limited in length to the Discord message limit (including the moderator name and role in the DM sent to the user) * This was to fix issues with `!edit` and `!delete` when a reply spanned multiple messages +* Snippets can now be included *within* messages by wrapping the snippet name in curly braces + * E.g. `!r Hello! {{rules}}` + * The symbols used can be changed with the `inlineSnippetStart` and `inlineSnippetEnd` options + * This feature can be disabled by setting `allowInlineSnippets = off` in your config * Plugins can now also be installed from NPM modules * Example: `plugins[] = npm:some-plugin-package` * Fix occasional bug with expiring blocks where the bot would send the expiry message multiple times diff --git a/docs/configuration.md b/docs/configuration.md index 52c1d77..800771c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -97,6 +97,13 @@ If enabled, staff members can delete their own replies in modmail threads with ` **Default:** `on` If enabled, staff members can edit their own replies in modmail threads with `!edit` +#### allowInlineSnippets +**Default:** `on` +If enabled, snippets can be included *within* replies by wrapping the snippet's name in {{ and }}. +E.g. `!r Hello! {{rules}}` + +See [inlineSnippetStart](#inlineSnippetStart) and [inlineSnippetEnd](#inlineSnippetEnd) to customize the symbols used. + #### alwaysReply **Default:** `off` If enabled, all messages in modmail threads will be sent to the user without having to use `!r`. @@ -185,6 +192,14 @@ If enabled, the bot attempts to ignore common "accidental" messages that would s **Accepts multiple values.** Permission name, user id, or role id required to use bot commands on the inbox server. See ["Permissions" on this page](https://abal.moe/Eris/docs/reference) for supported permission names (e.g. `kickMembers`). +#### inlineSnippetStart +**Default:** `{{` +Symbol(s) to use at the beginning of an inline snippet. See [allowInlineSnippets](#allowInlineSnippets) for more details. + +#### inlineSnippetEnd +**Default:** `}}` +Symbol(s) to use at the end of an inline snippet. See [allowInlineSnippets](#allowInlineSnippets) for more details. + #### timeOnServerDeniedMessage **Default:** `You haven't been a member of the server for long enough to contact modmail.` If `requiredTimeOnServer` is set, users that are too new will be sent this message if they try to message modmail. diff --git a/src/data/Thread.js b/src/data/Thread.js index 1aeee44..2db3d0c 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -8,6 +8,7 @@ const config = require("../cfg"); const attachments = require("./attachments"); const { formatters } = require("../formatters"); const { callAfterThreadCloseHooks } = require("../hooks/afterThreadClose"); +const snippets = require("./snippets"); const ThreadMessage = require("./ThreadMessage"); @@ -195,6 +196,25 @@ class Thread { const mainRole = utils.getMainRole(moderator); const roleName = mainRole ? mainRole.name : null; + if (config.allowInlineSnippets) { + // Replace {{snippet}} with the corresponding snippet + // The beginning and end of the variable - {{ and }} - can be changed with the config options + // config.inlineSnippetStart and config.inlineSnippetEnd + const allSnippets = await snippets.all(); + const snippetMap = allSnippets.reduce((_map, snippet) => { + _map[snippet.trigger.toLowerCase()] = snippet; + return _map; + }, {}); + + text = text.replace( + new RegExp(`${config.inlineSnippetStart}(.+)${config.inlineSnippetEnd}`, "i"), + (orig, trigger) => { + const snippet = snippetMap[trigger.toLowerCase()]; + return snippet != null ? snippet.body : orig; + } + ); + } + // Prepare attachments, if any const files = []; const attachmentLinks = []; diff --git a/src/data/cfg.jsdoc.js b/src/data/cfg.jsdoc.js index 1d46881..1acae72 100644 --- a/src/data/cfg.jsdoc.js +++ b/src/data/cfg.jsdoc.js @@ -56,6 +56,9 @@ * @property {boolean} [createThreadOnMention=false] * @property {boolean} [notifyOnMainServerLeave=true] * @property {boolean} [notifyOnMainServerJoin=true] + * @property {boolean} [allowInlineSnippets=true] + * @property {string} [inlineSnippetStart="{{"] + * @property {string} [inlineSnippetEnd="}}"] * @property {string} [logStorage="local"] * @property {object} [logOptions] * @property {string} logOptions.attachmentDirectory diff --git a/src/data/cfg.schema.json b/src/data/cfg.schema.json index 96c42fc..d042dab 100644 --- a/src/data/cfg.schema.json +++ b/src/data/cfg.schema.json @@ -316,6 +316,21 @@ "default": true }, + "allowInlineSnippets": { + "$ref": "#/definitions/customBoolean", + "default": true + }, + + "inlineSnippetStart": { + "type": "string", + "default": "{{" + }, + + "inlineSnippetEnd": { + "type": "string", + "default": "}}" + }, + "logStorage": { "type": "string", "default": "local" From db40c39dfae918d20e35c7880ecd2fb0f1664f1d Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 21 Oct 2020 22:21:04 +0300 Subject: [PATCH 175/300] Snippets are now case-insensitive everywhere --- src/data/snippets.js | 4 ++-- src/modules/snippets.js | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/data/snippets.js b/src/data/snippets.js index 0e4aebc..70cad4e 100644 --- a/src/data/snippets.js +++ b/src/data/snippets.js @@ -8,7 +8,7 @@ const Snippet = require("./Snippet"); */ async function getSnippet(trigger) { const snippet = await knex("snippets") - .where("trigger", trigger) + .where(knex.raw("LOWER(trigger)"), trigger.toLowerCase()) .first(); return (snippet ? new Snippet(snippet) : null); @@ -36,7 +36,7 @@ async function addSnippet(trigger, body, createdBy = 0) { */ async function deleteSnippet(trigger) { return knex("snippets") - .where("trigger", trigger) + .where(knex.raw("LOWER(trigger)"), trigger.toLowerCase()) .delete(); } diff --git a/src/modules/snippets.js b/src/modules/snippets.js index e68d6a3..fa68d9e 100644 --- a/src/modules/snippets.js +++ b/src/modules/snippets.js @@ -1,6 +1,5 @@ const threads = require("../data/threads"); const snippets = require("../data/snippets"); -const config = require("../cfg"); const utils = require("../utils"); const { parseArguments } = require("knub-command-manager"); From ddb759f6ae04a6464e7bd24d3242af6c4d6701ff Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 21 Oct 2020 22:21:46 +0300 Subject: [PATCH 176/300] Add !s as an alias for !snippets --- src/modules/snippets.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/modules/snippets.js b/src/modules/snippets.js index fa68d9e..90d1b5a 100644 --- a/src/modules/snippets.js +++ b/src/modules/snippets.js @@ -133,5 +133,7 @@ module.exports = ({ bot, knex, config, commands }) => { triggers.sort(); utils.postSystemMessageWithFallback(msg.channel, thread, `Available snippets (prefix ${config.snippetPrefix}):\n${triggers.join(", ")}`); + }, { + aliases: ["s"] }); }; From 9a88a6ef8951086c17146c57e098704dc26bcd23 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 21 Oct 2020 22:26:43 +0300 Subject: [PATCH 177/300] Fix multiple inline snippets breaking --- src/data/Thread.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/Thread.js b/src/data/Thread.js index 2db3d0c..5b34a8b 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -207,7 +207,7 @@ class Thread { }, {}); text = text.replace( - new RegExp(`${config.inlineSnippetStart}(.+)${config.inlineSnippetEnd}`, "i"), + new RegExp(`${config.inlineSnippetStart}(.+?)${config.inlineSnippetEnd}`, "i"), (orig, trigger) => { const snippet = snippetMap[trigger.toLowerCase()]; return snippet != null ? snippet.body : orig; From 3e44416077267affa95a179c92303625d5ef0b9c Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 21 Oct 2020 22:30:38 +0300 Subject: [PATCH 178/300] Only consider single words as inline snippets Optionally surrounded by whitespace. This mirrors the restrictions on !!snippets. --- src/data/Thread.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/data/Thread.js b/src/data/Thread.js index 5b34a8b..75ebfc9 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -207,8 +207,9 @@ class Thread { }, {}); text = text.replace( - new RegExp(`${config.inlineSnippetStart}(.+?)${config.inlineSnippetEnd}`, "i"), + new RegExp(`${config.inlineSnippetStart}(\\s*\\S+?\\s*)${config.inlineSnippetEnd}`, "i"), (orig, trigger) => { + trigger = trigger.trim(); const snippet = snippetMap[trigger.toLowerCase()]; return snippet != null ? snippet.body : orig; } From 5815157190a27e7ef499f3436b1a3687abf0b485 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 21 Oct 2020 22:33:15 +0300 Subject: [PATCH 179/300] Refuse to send replies with an unknown inline snippet This behavior can be disabled by setting the following option: errorOnUnknownInlineSnippet = off --- CHANGELOG.md | 1 + docs/configuration.md | 5 +++++ src/data/Thread.js | 10 ++++++++++ src/data/cfg.jsdoc.js | 1 + src/data/cfg.schema.json | 5 +++++ 5 files changed, 22 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d9eb77..ee90434 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Please report any bugs you encounter by [creating a GitHub issue](https://github * E.g. `!r Hello! {{rules}}` * The symbols used can be changed with the `inlineSnippetStart` and `inlineSnippetEnd` options * This feature can be disabled by setting `allowInlineSnippets = off` in your config + * By default, the bot will refuse to send a reply with an unknown inline snippet. To disable this behavior, set `errorOnUnknownInlineSnippet = off`. * Plugins can now also be installed from NPM modules * Example: `plugins[] = npm:some-plugin-package` * Fix occasional bug with expiring blocks where the bot would send the expiry message multiple times diff --git a/docs/configuration.md b/docs/configuration.md index 800771c..5d46811 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -168,6 +168,11 @@ commandAliases.x = close **Default:** `off` When enabled, the bot will send a greeting DM to users that join the main server. +#### errorOnUnknownInlineSnippet +**Default:** `on` +When enabled, the bot will refuse to send any reply with an unknown inline snippet. +See [allowInlineSnippets](#allowInlineSnippets) for more details. + #### greetingAttachment **Default:** *None* Path to an image or other attachment to send as a greeting. Requires `enableGreeting` to be enabled. diff --git a/src/data/Thread.js b/src/data/Thread.js index 75ebfc9..fd09a63 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -206,14 +206,24 @@ class Thread { return _map; }, {}); + let unknownSnippets = new Set(); text = text.replace( new RegExp(`${config.inlineSnippetStart}(\\s*\\S+?\\s*)${config.inlineSnippetEnd}`, "i"), (orig, trigger) => { trigger = trigger.trim(); const snippet = snippetMap[trigger.toLowerCase()]; + if (snippet == null) { + unknownSnippets.add(trigger); + } + return snippet != null ? snippet.body : orig; } ); + + if (config.errorOnUnknownInlineSnippet && unknownSnippets.size > 0) { + this.postSystemMessage(`The following snippets used in the reply do not exist:\n${Array.from(unknownSnippets).join(", ")}`); + return false; + } } // Prepare attachments, if any diff --git a/src/data/cfg.jsdoc.js b/src/data/cfg.jsdoc.js index 1acae72..c23e3c1 100644 --- a/src/data/cfg.jsdoc.js +++ b/src/data/cfg.jsdoc.js @@ -59,6 +59,7 @@ * @property {boolean} [allowInlineSnippets=true] * @property {string} [inlineSnippetStart="{{"] * @property {string} [inlineSnippetEnd="}}"] + * @property {boolean} [errorOnUnknownInlineSnippet=true] * @property {string} [logStorage="local"] * @property {object} [logOptions] * @property {string} logOptions.attachmentDirectory diff --git a/src/data/cfg.schema.json b/src/data/cfg.schema.json index d042dab..8c42ab0 100644 --- a/src/data/cfg.schema.json +++ b/src/data/cfg.schema.json @@ -331,6 +331,11 @@ "default": "}}" }, + "errorOnUnknownInlineSnippet": { + "$ref": "#/definitions/customBoolean", + "default": true + }, + "logStorage": { "type": "string", "default": "local" From 4b8d01ebea17f818e78707b77829959c8196bd79 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 21 Oct 2020 22:36:46 +0300 Subject: [PATCH 180/300] Clarify CHANGELOG on inline snippets --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee90434..8cd9947 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ Please report any bugs you encounter by [creating a GitHub issue](https://github * Replies are now limited in length to the Discord message limit (including the moderator name and role in the DM sent to the user) * This was to fix issues with `!edit` and `!delete` when a reply spanned multiple messages * Snippets can now be included *within* messages by wrapping the snippet name in curly braces - * E.g. `!r Hello! {{rules}}` + * E.g. `!r Hello! {{rules}}` to include the snippet `rules` in the place of `{{rules}}` * The symbols used can be changed with the `inlineSnippetStart` and `inlineSnippetEnd` options * This feature can be disabled by setting `allowInlineSnippets = off` in your config * By default, the bot will refuse to send a reply with an unknown inline snippet. To disable this behavior, set `errorOnUnknownInlineSnippet = off`. From 0e2135943fcb445bec16fc6c6ff36ebfcba79f0c Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 21 Oct 2020 23:15:41 +0300 Subject: [PATCH 181/300] Add !role command to change the role displayed in your replies --- docs/commands.md | 6 ++++ docs/configuration.md | 4 +++ src/data/Thread.js | 42 +++++++++++++++++++++++++-- src/data/cfg.jsdoc.js | 1 + src/data/cfg.schema.json | 5 ++++ src/main.js | 1 + src/modules/roles.js | 63 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 src/modules/roles.js diff --git a/docs/commands.md b/docs/commands.md index dcd6bb3..a6d39f2 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -70,6 +70,12 @@ Edit your own previous reply sent with `!reply`. Delete your own previous reply sent with `!reply`. `` is the message number shown in front of staff replies in the thread channel. +### `!role` +View the role that is sent with your replies + +### `!role ` +Change the role that is sent with your replies to any role you currently have + ### `!loglink` Get a link to the open Modmail thread's log. diff --git a/docs/configuration.md b/docs/configuration.md index 5d46811..9e9a4a0 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -102,6 +102,10 @@ If enabled, staff members can edit their own replies in modmail threads with `!e If enabled, snippets can be included *within* replies by wrapping the snippet's name in {{ and }}. E.g. `!r Hello! {{rules}}` +#### allowChangingDisplayedRole +**Default:** `on` +If enabled, moderators can change the role that's shown with their replies to any role they currently have using the `!role` command. + See [inlineSnippetStart](#inlineSnippetStart) and [inlineSnippetEnd](#inlineSnippetEnd) to customize the symbols used. #### alwaysReply diff --git a/src/data/Thread.js b/src/data/Thread.js index fd09a63..9f151f4 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -14,6 +14,8 @@ const ThreadMessage = require("./ThreadMessage"); const {THREAD_MESSAGE_TYPE, THREAD_STATUS, DISCORD_MESSAGE_ACTIVITY_TYPES} = require("./constants"); +const ROLE_OVERRIDES_METADATA_KEY = "moderatorRoleOverrides"; + /** * @property {String} id * @property {Number} status @@ -193,8 +195,7 @@ class Thread { */ async replyToUser(moderator, text, replyAttachments = [], isAnonymous = false) { const moderatorName = config.useNicknames && moderator.nick ? moderator.nick : moderator.user.username; - const mainRole = utils.getMainRole(moderator); - const roleName = mainRole ? mainRole.name : null; + const roleName = this.getModeratorDisplayRoleName(moderator); if (config.allowInlineSnippets) { // Replace {{snippet}} with the corresponding snippet @@ -837,6 +838,43 @@ class Thread { }); } + setModeratorRoleOverride(moderatorId, roleId) { + const moderatorRoleOverrides = this.getMetadataValue(ROLE_OVERRIDES_METADATA_KEY) || {}; + moderatorRoleOverrides[moderatorId] = roleId; + return this.setMetadataValue(ROLE_OVERRIDES_METADATA_KEY, moderatorRoleOverrides); + } + + resetModeratorRoleOverride(moderatorId) { + const moderatorRoleOverrides = this.getMetadataValue(ROLE_OVERRIDES_METADATA_KEY) || {}; + delete moderatorRoleOverrides[moderatorId]; + return this.setMetadataValue(ROLE_OVERRIDES_METADATA_KEY, moderatorRoleOverrides) + } + + /** + * Get the role that is shown in the replies of the specified moderator, + * taking role overrides into account. + * @param {Eris.Member} moderator + * @return {Eris.Role|undefined} + */ + getModeratorDisplayRole(moderator) { + const moderatorRoleOverrides = this.getMetadataValue(ROLE_OVERRIDES_METADATA_KEY) || {}; + const overrideRoleId = moderatorRoleOverrides[moderator.id]; + const overrideRole = overrideRoleId && moderator.roles.includes(overrideRoleId) && utils.getInboxGuild().roles.get(overrideRoleId); + const finalRole = overrideRole || utils.getMainRole(moderator); + return finalRole; + } + + /** + * Get the role NAME that is shown in the replies of the specified moderator, + * taking role overrides into account. + * @param {Eris.Member} moderator + * @return {Eris.Role|undefined} + */ + getModeratorDisplayRoleName(moderator) { + const displayRole = this.getModeratorDisplayRole(moderator); + return displayRole ? displayRole.name : null; + } + /** * @param {string} key * @param {*} value diff --git a/src/data/cfg.jsdoc.js b/src/data/cfg.jsdoc.js index c23e3c1..438d61f 100644 --- a/src/data/cfg.jsdoc.js +++ b/src/data/cfg.jsdoc.js @@ -60,6 +60,7 @@ * @property {string} [inlineSnippetStart="{{"] * @property {string} [inlineSnippetEnd="}}"] * @property {boolean} [errorOnUnknownInlineSnippet=true] + * @property {boolean} [allowChangingDisplayedRole=true] * @property {string} [logStorage="local"] * @property {object} [logOptions] * @property {string} logOptions.attachmentDirectory diff --git a/src/data/cfg.schema.json b/src/data/cfg.schema.json index 8c42ab0..b410e09 100644 --- a/src/data/cfg.schema.json +++ b/src/data/cfg.schema.json @@ -336,6 +336,11 @@ "default": true }, + "allowChangingDisplayedRole": { + "$ref": "#/definitions/customBoolean", + "default": true + }, + "logStorage": { "type": "string", "default": "local" diff --git a/src/main.js b/src/main.js index 45ce593..384b02a 100644 --- a/src/main.js +++ b/src/main.js @@ -301,6 +301,7 @@ async function initPlugins() { "file:./src/modules/id", "file:./src/modules/alert", "file:./src/modules/joinLeaveNotification", + "file:./src/modules/roles", ]; const plugins = [...builtInPlugins, ...config.plugins]; diff --git a/src/modules/roles.js b/src/modules/roles.js new file mode 100644 index 0000000..0f0921a --- /dev/null +++ b/src/modules/roles.js @@ -0,0 +1,63 @@ +const utils = require("../utils"); + +const ROLE_OVERRIDES_METADATA_KEY = "moderatorRoleOverrides"; + +module.exports = ({ bot, knex, config, commands }) => { + if (! config.allowChangingDisplayedRole) { + return; + } + + commands.addInboxThreadCommand("role", "[role:string$]", async (msg, args, thread) => { + const moderatorRoleOverrides = thread.getMetadataValue(ROLE_OVERRIDES_METADATA_KEY); + + // Set display role + if (args.role) { + if (args.role === "reset") { + await thread.resetModeratorRoleOverride(msg.member.id); + + const displayRole = thread.getModeratorDisplayRoleName(msg.member); + if (displayRole) { + thread.postSystemMessage(`Your display role has been reset. Your replies will now display the role **${displayRole}**.`); + } else { + thread.postSystemMessage("Your display role has been reset. Your replies will no longer display a role."); + } + + return; + } + + let role; + if (utils.isSnowflake(args.role)) { + if (! msg.member.roles.includes(args.role)) { + thread.postSystemMessage("No matching role found. Make sure you have the role before trying to set it as your role."); + return; + } + + role = utils.getInboxGuild().roles.get(args.role); + } else { + const matchingMemberRole = utils.getInboxGuild().roles.find(r => { + if (! msg.member.roles.includes(r.id)) return false; + return r.name.toLowerCase() === args.role.toLowerCase(); + }); + + if (! matchingMemberRole) { + thread.postSystemMessage("No matching role found. Make sure you have the role before trying to set it as your role."); + return; + } + + role = matchingMemberRole; + } + + await thread.setModeratorRoleOverride(msg.member.id, role.id); + thread.postSystemMessage(`Your display role has been set to **${role.name}**. You can reset it with \`${config.prefix}role reset\`.`); + return; + } + + // Get display role + const displayRole = thread.getModeratorDisplayRoleName(msg.member); + if (displayRole) { + thread.postSystemMessage(`Your displayed role is currently: **${displayRole}**`); + } else { + thread.postSystemMessage("Your replies do not currently display a role"); + } + }); +}; From 2d13f88ccceaa7e9d35a58f5c74099cbe5cfd583 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 21 Oct 2020 23:24:45 +0300 Subject: [PATCH 182/300] Add `fallbackRoleName` option. Don't include "Moderator" in role-less anonymous replies unless `fallbackRoleName` is set. --- CHANGELOG.md | 3 +++ docs/configuration.md | 4 ++++ src/data/cfg.jsdoc.js | 1 + src/data/cfg.schema.json | 5 +++++ src/formatters.js | 18 ++++++++++++------ 5 files changed, 25 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cd9947..6f82b84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ Please report any bugs you encounter by [creating a GitHub issue](https://github * The symbols used can be changed with the `inlineSnippetStart` and `inlineSnippetEnd` options * This feature can be disabled by setting `allowInlineSnippets = off` in your config * By default, the bot will refuse to send a reply with an unknown inline snippet. To disable this behavior, set `errorOnUnknownInlineSnippet = off`. +* New option: `fallbackRoleName` + * Sets the role name to display in moderator replies if the moderator doesn't have a hoisted role +* Unless `fallbackRoleName` is set, anonymous replies without a role will no longer display "Moderator:" at the beginning of the message * Plugins can now also be installed from NPM modules * Example: `plugins[] = npm:some-plugin-package` * Fix occasional bug with expiring blocks where the bot would send the expiry message multiple times diff --git a/docs/configuration.md b/docs/configuration.md index 9e9a4a0..bd01c60 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -177,6 +177,10 @@ When enabled, the bot will send a greeting DM to users that join the main server When enabled, the bot will refuse to send any reply with an unknown inline snippet. See [allowInlineSnippets](#allowInlineSnippets) for more details. +#### fallbackRoleName +**Default:** *None* +Role name to display in moderator replies if the moderator doesn't have a hoisted role + #### greetingAttachment **Default:** *None* Path to an image or other attachment to send as a greeting. Requires `enableGreeting` to be enabled. diff --git a/src/data/cfg.jsdoc.js b/src/data/cfg.jsdoc.js index 438d61f..1dbb5eb 100644 --- a/src/data/cfg.jsdoc.js +++ b/src/data/cfg.jsdoc.js @@ -61,6 +61,7 @@ * @property {string} [inlineSnippetEnd="}}"] * @property {boolean} [errorOnUnknownInlineSnippet=true] * @property {boolean} [allowChangingDisplayedRole=true] + * @property {string} [fallbackRoleName=null] * @property {string} [logStorage="local"] * @property {object} [logOptions] * @property {string} logOptions.attachmentDirectory diff --git a/src/data/cfg.schema.json b/src/data/cfg.schema.json index b410e09..6955e83 100644 --- a/src/data/cfg.schema.json +++ b/src/data/cfg.schema.json @@ -341,6 +341,11 @@ "default": true }, + "fallbackRoleName": { + "type": "string", + "default": null + }, + "logStorage": { "type": "string", "default": "local" diff --git a/src/formatters.js b/src/formatters.js index a173ac8..8c086c8 100644 --- a/src/formatters.js +++ b/src/formatters.js @@ -100,19 +100,25 @@ const moment = require("moment"); */ const defaultFormatters = { formatStaffReplyDM(threadMessage) { + const roleName = threadMessage.role_name || config.fallbackRoleName; const modInfo = threadMessage.is_anonymous - ? (threadMessage.role_name ? threadMessage.role_name : "Moderator") - : (threadMessage.role_name ? `(${threadMessage.role_name}) ${threadMessage.user_name}` : threadMessage.user_name); + ? roleName + : (roleName ? `(${roleName}) ${threadMessage.user_name}` : threadMessage.user_name); - return `**${modInfo}:** ${threadMessage.body}`; + return modInfo + ? `**${modInfo}:** ${threadMessage.body}` + : threadMessage.body; }, formatStaffReplyThreadMessage(threadMessage) { + const roleName = threadMessage.role_name || config.fallbackRoleName; const modInfo = threadMessage.is_anonymous - ? `(Anonymous) (${threadMessage.user_name}) ${threadMessage.role_name || "Moderator"}` - : (threadMessage.role_name ? `(${threadMessage.role_name}) ${threadMessage.user_name}` : threadMessage.user_name); + ? (roleName ? `(Anonymous) (${threadMessage.user_name}) ${roleName}` : `(Anonymous) (${threadMessage.user_name})`) + : (roleName ? `(${roleName}) ${threadMessage.user_name}` : threadMessage.user_name); - let result = `**${modInfo}:** ${threadMessage.body}`; + let result = modInfo + ? `**${modInfo}:** ${threadMessage.body}` + : threadMessage.body; if (config.threadTimestamps) { const formattedTimestamp = utils.getTimestamp(threadMessage.created_at); From 405a19b9180c587ac94fc1b54c6815143bed7ae5 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 21 Oct 2020 23:28:26 +0300 Subject: [PATCH 183/300] Add !role to CHANGELOG --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f82b84..2cc4cfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,11 @@ Please report any bugs you encounter by [creating a GitHub issue](https://github * The symbols used can be changed with the `inlineSnippetStart` and `inlineSnippetEnd` options * This feature can be disabled by setting `allowInlineSnippets = off` in your config * By default, the bot will refuse to send a reply with an unknown inline snippet. To disable this behavior, set `errorOnUnknownInlineSnippet = off`. +* Moderators can now set the role they'd like to be displayed with their replies on a per-thread basis by using `!role` + * Moderators can only choose roles they currently have + * You can view your currently displayed role by using `!role` + * You can set the displayed role by using `!role `, e.g. `!role Interviewer` + * This feature can be disabled by setting `allowChangingDisplayedRole = off` * New option: `fallbackRoleName` * Sets the role name to display in moderator replies if the moderator doesn't have a hoisted role * Unless `fallbackRoleName` is set, anonymous replies without a role will no longer display "Moderator:" at the beginning of the message From bbca6a873fc78efab6b1f57fe3899e3dc708964c Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 22 Oct 2020 00:17:14 +0300 Subject: [PATCH 184/300] Allow setting a default display role Same command as for threads, !role, but used outside a thread. --- CHANGELOG.md | 15 +- docs/commands.md | 16 +- docs/configuration.md | 2 +- src/commands.js | 9 +- src/data/Thread.js | 42 +---- src/data/cfg.jsdoc.js | 2 +- src/data/cfg.schema.json | 2 +- ...0_create_moderator_role_overrides_table.js | 17 ++ src/data/moderatorRoleOverrides.js | 165 ++++++++++++++++++ src/modules/roles.js | 120 ++++++++----- 10 files changed, 298 insertions(+), 92 deletions(-) create mode 100644 src/data/migrations/20201021232940_create_moderator_role_overrides_table.js create mode 100644 src/data/moderatorRoleOverrides.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cc4cfb..b83d333 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,11 +12,18 @@ Please report any bugs you encounter by [creating a GitHub issue](https://github * The symbols used can be changed with the `inlineSnippetStart` and `inlineSnippetEnd` options * This feature can be disabled by setting `allowInlineSnippets = off` in your config * By default, the bot will refuse to send a reply with an unknown inline snippet. To disable this behavior, set `errorOnUnknownInlineSnippet = off`. -* Moderators can now set the role they'd like to be displayed with their replies on a per-thread basis by using `!role` +* Moderators can now set the role they'd like to be displayed with their replies ("display role") by default and on a per-thread basis by using `!role` * Moderators can only choose roles they currently have - * You can view your currently displayed role by using `!role` - * You can set the displayed role by using `!role `, e.g. `!role Interviewer` - * This feature can be disabled by setting `allowChangingDisplayedRole = off` + * You can view your current display role by using `!role` + * If you're in a modmail thread, this will show your display role for that thread + * If you're *not* in a modmail thread, this will show your *default* display role + * You can set the display role by using `!role `, e.g. `!role Interviewer` + * If you're in a modmail thread, this will set your display role for that thread + * If you're *not* in a modmail thread, this will set your *default* display role + * You can reset the display role by using `!role reset` + * If you're in a modmail thread, this will reset your display role for that thread to the default + * If you're *not* in a modmail thread, this will reset your *default* display role + * This feature can be disabled by setting `allowChangingDisplayRole = off` * New option: `fallbackRoleName` * Sets the role name to display in moderator replies if the moderator doesn't have a hoisted role * Unless `fallbackRoleName` is set, anonymous replies without a role will no longer display "Moderator:" at the beginning of the message diff --git a/docs/commands.md b/docs/commands.md index a6d39f2..e980c37 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -71,10 +71,13 @@ Delete your own previous reply sent with `!reply`. `` is the message number shown in front of staff replies in the thread channel. ### `!role` -View the role that is sent with your replies +View your display role for the thread - the role that is shown in front of your name in your replies + +### `!role` +Reset your display role for the thread to the default ### `!role ` -Change the role that is sent with your replies to any role you currently have +Change your display role for the thread to any role you currently have ### `!loglink` Get a link to the open Modmail thread's log. @@ -129,6 +132,15 @@ Check if the specified user is blocked. **Example:** `!is_blocked 106391128718245888` +### `!role` +(Outside a modmail thread) View your default display role - the role that is shown in front of your name in your replies + +### `!role` +(Outside a modmail thread) Reset your default display role + +### `!role ` +(Outside a modmail thread) Change your default display role to any role you currently have + ### `!version` Show the Modmail bot's version. diff --git a/docs/configuration.md b/docs/configuration.md index bd01c60..9bd8eb5 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -102,7 +102,7 @@ If enabled, staff members can edit their own replies in modmail threads with `!e If enabled, snippets can be included *within* replies by wrapping the snippet's name in {{ and }}. E.g. `!r Hello! {{rules}}` -#### allowChangingDisplayedRole +#### allowChangingDisplayRole **Default:** `on` If enabled, moderators can change the role that's shown with their replies to any role they currently have using the `!role` command. diff --git a/src/commands.js b/src/commands.js index a0c34e2..c7facd6 100644 --- a/src/commands.js +++ b/src/commands.js @@ -11,6 +11,13 @@ const Thread = require("./data/Thread"); * @param {object} args */ +/** + * @callback InboxServerCommandHandler + * @param {Eris.Message} msg + * @param {object} args + * @param {Thread} [thread] + */ + /** * @callback InboxThreadCommandHandler * @param {Eris.Message} msg @@ -30,7 +37,7 @@ const Thread = require("./data/Thread"); * @callback AddInboxServerCommandFn * @param {string} trigger * @param {string} parameters - * @param {CommandFn} handler + * @param {InboxServerCommandHandler} handler * @param {ICommandConfig} commandConfig */ diff --git a/src/data/Thread.js b/src/data/Thread.js index 9f151f4..27f9a0e 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -9,13 +9,12 @@ const attachments = require("./attachments"); const { formatters } = require("../formatters"); const { callAfterThreadCloseHooks } = require("../hooks/afterThreadClose"); const snippets = require("./snippets"); +const { getModeratorThreadDisplayRoleName } = require("./moderatorRoleOverrides"); const ThreadMessage = require("./ThreadMessage"); const {THREAD_MESSAGE_TYPE, THREAD_STATUS, DISCORD_MESSAGE_ACTIVITY_TYPES} = require("./constants"); -const ROLE_OVERRIDES_METADATA_KEY = "moderatorRoleOverrides"; - /** * @property {String} id * @property {Number} status @@ -195,7 +194,7 @@ class Thread { */ async replyToUser(moderator, text, replyAttachments = [], isAnonymous = false) { const moderatorName = config.useNicknames && moderator.nick ? moderator.nick : moderator.user.username; - const roleName = this.getModeratorDisplayRoleName(moderator); + const roleName = await getModeratorThreadDisplayRoleName(moderator, this.id); if (config.allowInlineSnippets) { // Replace {{snippet}} with the corresponding snippet @@ -838,43 +837,6 @@ class Thread { }); } - setModeratorRoleOverride(moderatorId, roleId) { - const moderatorRoleOverrides = this.getMetadataValue(ROLE_OVERRIDES_METADATA_KEY) || {}; - moderatorRoleOverrides[moderatorId] = roleId; - return this.setMetadataValue(ROLE_OVERRIDES_METADATA_KEY, moderatorRoleOverrides); - } - - resetModeratorRoleOverride(moderatorId) { - const moderatorRoleOverrides = this.getMetadataValue(ROLE_OVERRIDES_METADATA_KEY) || {}; - delete moderatorRoleOverrides[moderatorId]; - return this.setMetadataValue(ROLE_OVERRIDES_METADATA_KEY, moderatorRoleOverrides) - } - - /** - * Get the role that is shown in the replies of the specified moderator, - * taking role overrides into account. - * @param {Eris.Member} moderator - * @return {Eris.Role|undefined} - */ - getModeratorDisplayRole(moderator) { - const moderatorRoleOverrides = this.getMetadataValue(ROLE_OVERRIDES_METADATA_KEY) || {}; - const overrideRoleId = moderatorRoleOverrides[moderator.id]; - const overrideRole = overrideRoleId && moderator.roles.includes(overrideRoleId) && utils.getInboxGuild().roles.get(overrideRoleId); - const finalRole = overrideRole || utils.getMainRole(moderator); - return finalRole; - } - - /** - * Get the role NAME that is shown in the replies of the specified moderator, - * taking role overrides into account. - * @param {Eris.Member} moderator - * @return {Eris.Role|undefined} - */ - getModeratorDisplayRoleName(moderator) { - const displayRole = this.getModeratorDisplayRole(moderator); - return displayRole ? displayRole.name : null; - } - /** * @param {string} key * @param {*} value diff --git a/src/data/cfg.jsdoc.js b/src/data/cfg.jsdoc.js index 1dbb5eb..4431a30 100644 --- a/src/data/cfg.jsdoc.js +++ b/src/data/cfg.jsdoc.js @@ -60,7 +60,7 @@ * @property {string} [inlineSnippetStart="{{"] * @property {string} [inlineSnippetEnd="}}"] * @property {boolean} [errorOnUnknownInlineSnippet=true] - * @property {boolean} [allowChangingDisplayedRole=true] + * @property {boolean} [allowChangingDisplayRole=true] * @property {string} [fallbackRoleName=null] * @property {string} [logStorage="local"] * @property {object} [logOptions] diff --git a/src/data/cfg.schema.json b/src/data/cfg.schema.json index 6955e83..e490195 100644 --- a/src/data/cfg.schema.json +++ b/src/data/cfg.schema.json @@ -336,7 +336,7 @@ "default": true }, - "allowChangingDisplayedRole": { + "allowChangingDisplayRole": { "$ref": "#/definitions/customBoolean", "default": true }, diff --git a/src/data/migrations/20201021232940_create_moderator_role_overrides_table.js b/src/data/migrations/20201021232940_create_moderator_role_overrides_table.js new file mode 100644 index 0000000..cc82053 --- /dev/null +++ b/src/data/migrations/20201021232940_create_moderator_role_overrides_table.js @@ -0,0 +1,17 @@ +exports.up = async function(knex, Promise) { + if (! await knex.schema.hasTable("moderator_role_overrides")) { + await knex.schema.createTable("moderator_role_overrides", table => { + table.string("moderator_id", 20); + table.string("thread_id", 36).nullable().defaultTo(null); + table.string("role_id", 20); + + table.primary(["moderator_id", "thread_id"]); + }); + } +}; + +exports.down = async function(knex, Promise) { + if (await knex.schema.hasTable("moderator_role_overrides")) { + await knex.schema.dropTable("moderator_role_overrides"); + } +}; diff --git a/src/data/moderatorRoleOverrides.js b/src/data/moderatorRoleOverrides.js new file mode 100644 index 0000000..d65a1fb --- /dev/null +++ b/src/data/moderatorRoleOverrides.js @@ -0,0 +1,165 @@ +const knex = require("../knex"); +const Eris = require("eris"); +const utils = require("../utils"); +const config = require("../cfg"); + +/** + * @param {string} moderatorId + * @returns {Promise} + */ +async function getModeratorDefaultRoleOverride(moderatorId) { + const roleOverride = await knex("moderator_role_overrides") + .where("moderator_id", moderatorId) + .whereNull("thread_id") + .first(); + + return roleOverride ? roleOverride.role_id : null; +} + +/** + * @param {string} moderatorId + * @param {string} roleId + * @returns {Promise} + */ +async function setModeratorDefaultRoleOverride(moderatorId, roleId) { + const existingGlobalOverride = await getModeratorDefaultRoleOverride(moderatorId); + if (existingGlobalOverride) { + await knex("moderator_role_overrides") + .where("moderator_id", moderatorId) + .whereNull("thread_id") + .update({ role_id: roleId }); + } else { + await knex("moderator_role_overrides") + .insert({ + moderator_id: moderatorId, + thread_id: null, + role_id: roleId, + }); + } +} + +/** + * @param {string} moderatorId + * @returns {Promise} + */ +async function resetModeratorDefaultRoleOverride(moderatorId) { + await knex("moderator_role_overrides") + .where("moderator_id", moderatorId) + .whereNull("thread_id") + .delete(); +} + +/** + * @param {string} moderatorId + * @param {string} threadId + * @returns {Promise} + */ +async function getModeratorThreadRoleOverride(moderatorId, threadId) { + const roleOverride = await knex("moderator_role_overrides") + .where("moderator_id", moderatorId) + .where("thread_id", threadId) + .first(); + + return roleOverride ? roleOverride.role_id : null; +} + +/** + * @param {string} moderatorId + * @param {string} threadId + * @param {string} roleId + * @returns {Promise} + */ +async function setModeratorThreadRoleOverride(moderatorId, threadId, roleId) { + const existingGlobalOverride = await getModeratorThreadRoleOverride(moderatorId, threadId); + if (existingGlobalOverride) { + await knex("moderator_role_overrides") + .where("moderator_id", moderatorId) + .where("thread_id", threadId) + .update({ role_id: roleId }); + } else { + await knex("moderator_role_overrides") + .insert({ + moderator_id: moderatorId, + thread_id: threadId, + role_id: roleId, + }); + } +} + +/** + * @param {string} moderatorId + * @param {string} threadId + * @returns {Promise} + */ +async function resetModeratorThreadRoleOverride(moderatorId, threadId) { + await knex("moderator_role_overrides") + .where("moderator_id", moderatorId) + .where("thread_id", threadId) + .delete(); +} + +/** + * @param {Eris.Member} moderator + * @returns {Promise} + */ +async function getModeratorDefaultDisplayRole(moderator) { + const globalOverrideRoleId = await getModeratorDefaultRoleOverride(moderator.id); + if (globalOverrideRoleId && moderator.roles.includes(globalOverrideRoleId)) { + return moderator.guild.roles.get(globalOverrideRoleId); + } + + return utils.getMainRole(moderator); +} + +/** + * @param {Eris.Member} moderator + * @returns {Promise} + */ +async function getModeratorDefaultDisplayRoleName(moderator) { + const defaultDisplayRole = await getModeratorDefaultDisplayRole(moderator); + return defaultDisplayRole + ? defaultDisplayRole.name + : (config.fallbackRoleName || null); +} + +/** + * @param {Eris.Member} moderator + * @param {string} threadId + * @returns {Promise} + */ +async function getModeratorThreadDisplayRole(moderator, threadId) { + const threadOverrideRoleId = await getModeratorThreadRoleOverride(moderator.id, threadId); + if (threadOverrideRoleId && moderator.roles.includes(threadOverrideRoleId)) { + return moderator.guild.roles.get(threadOverrideRoleId); + } + + return getModeratorDefaultDisplayRole(moderator); +} + +/** + * @param {Eris.Member} moderator + * @param {string} threadId + * @returns {Promise} + */ +async function getModeratorThreadDisplayRoleName(moderator, threadId) { + const threadDisplayRole = await getModeratorThreadDisplayRole(moderator, threadId); + return threadDisplayRole + ? threadDisplayRole.name + : (config.fallbackRoleName || null); +} + +module.exports = { + getModeratorDefaultRoleOverride, + setModeratorDefaultRoleOverride, + resetModeratorDefaultRoleOverride, + + getModeratorThreadRoleOverride, + setModeratorThreadRoleOverride, + resetModeratorThreadRoleOverride, + + getModeratorDefaultDisplayRole, + getModeratorDefaultDisplayRoleName, + + getModeratorThreadDisplayRole, + getModeratorThreadDisplayRoleName, +}; diff --git a/src/modules/roles.js b/src/modules/roles.js index 0f0921a..5ba59a8 100644 --- a/src/modules/roles.js +++ b/src/modules/roles.js @@ -1,63 +1,99 @@ const utils = require("../utils"); +const { + setModeratorDefaultRoleOverride, + resetModeratorDefaultRoleOverride, + + setModeratorThreadRoleOverride, + resetModeratorThreadRoleOverride, + + getModeratorThreadDisplayRoleName, + getModeratorDefaultDisplayRoleName, +} = require("../data/moderatorRoleOverrides"); const ROLE_OVERRIDES_METADATA_KEY = "moderatorRoleOverrides"; module.exports = ({ bot, knex, config, commands }) => { - if (! config.allowChangingDisplayedRole) { + if (! config.allowChangingDisplayRole) { return; } - commands.addInboxThreadCommand("role", "[role:string$]", async (msg, args, thread) => { - const moderatorRoleOverrides = thread.getMetadataValue(ROLE_OVERRIDES_METADATA_KEY); + function resolveRoleInput(input) { + if (utils.isSnowflake(input)) { + return utils.getInboxGuild().roles.get(input); + } - // Set display role - if (args.role) { - if (args.role === "reset") { - await thread.resetModeratorRoleOverride(msg.member.id); + return utils.getInboxGuild().roles.find(r => r.name.toLowerCase() === input.toLowerCase()); + } - const displayRole = thread.getModeratorDisplayRoleName(msg.member); - if (displayRole) { - thread.postSystemMessage(`Your display role has been reset. Your replies will now display the role **${displayRole}**.`); - } else { - thread.postSystemMessage("Your display role has been reset. Your replies will no longer display a role."); - } + // Get display role for a thread + commands.addInboxThreadCommand("role", [], async (msg, args, thread) => { + const displayRole = await getModeratorThreadDisplayRoleName(msg.member, thread.id); + if (displayRole) { + thread.postSystemMessage(`Your display role in this thread is currently **${displayRole}**`); + } else { + thread.postSystemMessage("Your replies in this thread do not currently display a role"); + } + }); - return; - } + // Reset display role for a thread + commands.addInboxThreadCommand("role reset", [], async (msg, args, thread) => { + await resetModeratorThreadRoleOverride(msg.member.id, thread.id); - let role; - if (utils.isSnowflake(args.role)) { - if (! msg.member.roles.includes(args.role)) { - thread.postSystemMessage("No matching role found. Make sure you have the role before trying to set it as your role."); - return; - } + const displayRole = await getModeratorThreadDisplayRoleName(msg.member, thread.id); + if (displayRole) { + thread.postSystemMessage(`Your display role for this thread has been reset. Your replies will now display the default role **${displayRole}**.`); + } else { + thread.postSystemMessage("Your display role for this thread has been reset. Your replies will no longer display a role."); + } + }, { + aliases: ["role_reset", "reset_role"], + }); - role = utils.getInboxGuild().roles.get(args.role); - } else { - const matchingMemberRole = utils.getInboxGuild().roles.find(r => { - if (! msg.member.roles.includes(r.id)) return false; - return r.name.toLowerCase() === args.role.toLowerCase(); - }); - - if (! matchingMemberRole) { - thread.postSystemMessage("No matching role found. Make sure you have the role before trying to set it as your role."); - return; - } - - role = matchingMemberRole; - } - - await thread.setModeratorRoleOverride(msg.member.id, role.id); - thread.postSystemMessage(`Your display role has been set to **${role.name}**. You can reset it with \`${config.prefix}role reset\`.`); + // Set display role for a thread + commands.addInboxThreadCommand("role", "", async (msg, args, thread) => { + const role = resolveRoleInput(args.role); + if (! role || ! msg.member.roles.includes(role.id)) { + thread.postSystemMessage("No matching role found. Make sure you have the role before trying to set it as your display role in this thread."); return; } - // Get display role - const displayRole = thread.getModeratorDisplayRoleName(msg.member); + await setModeratorThreadRoleOverride(msg.member.id, thread.id, role.id); + thread.postSystemMessage(`Your display role for this thread has been set to **${role.name}**. You can reset it with \`${config.prefix}role reset\`.`); + }); + + // Get default display role + commands.addInboxServerCommand("role", [], async (msg, args, thread) => { + const displayRole = await getModeratorDefaultDisplayRoleName(msg.member); if (displayRole) { - thread.postSystemMessage(`Your displayed role is currently: **${displayRole}**`); + msg.channel.createMessage(`Your default display role is currently **${displayRole}**`); } else { - thread.postSystemMessage("Your replies do not currently display a role"); + msg.channel.createMessage("Your replies do not currently display a role by default"); } }); + + // Reset default display role + commands.addInboxServerCommand("role reset", [], async (msg, args, thread) => { + await resetModeratorDefaultRoleOverride(msg.member.id); + + const displayRole = await getModeratorDefaultDisplayRoleName(msg.member); + if (displayRole) { + msg.channel.createMessage(`Your default display role has been reset. Your replies will now display the role **${displayRole}** by default.`); + } else { + msg.channel.createMessage("Your default display role has been reset. Your replies will no longer display a role by default."); + } + }, { + aliases: ["role_reset", "reset_role"], + }); + + // Set default display role + commands.addInboxServerCommand("role", "", async (msg, args, thread) => { + const role = resolveRoleInput(args.role); + if (! role || ! msg.member.roles.includes(role.id)) { + msg.channel.createMessage("No matching role found. Make sure you have the role before trying to set it as your default display role."); + return; + } + + await setModeratorDefaultRoleOverride(msg.member.id, role.id); + msg.channel.createMessage(`Your default display role has been set to **${role.name}**. You can reset it with \`${config.prefix}role reset\`.`); + }); }; From 371c49981c333167b103197bb61fdd61709ffd5d Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 22 Oct 2020 00:40:01 +0300 Subject: [PATCH 185/300] Fix missing -v/-s options for !loglink, add same options for !logs --- src/modules/logs.js | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/modules/logs.js b/src/modules/logs.js index 5e567c3..51326f9 100644 --- a/src/modules/logs.js +++ b/src/modules/logs.js @@ -6,6 +6,19 @@ const { getLogUrl, getLogFile, getLogCustomResponse, saveLogToStorage } = requir const LOG_LINES_PER_PAGE = 10; module.exports = ({ bot, knex, config, commands, hooks }) => { + const addOptQueryStringToUrl = (url, args) => { + const params = []; + if (args.verbose) params.push("verbose=1"); + if (args.simple) params.push("simple=1"); + + if (params.length === 0) { + return url; + } + + const hasQueryString = url.indexOf("?") > -1; + return url + (hasQueryString ? "&" : "?") + params.join("&"); + }; + const logsCmd = async (msg, args, thread) => { let userId = args.userId || (thread && thread.user_id); if (! userId) return; @@ -32,7 +45,7 @@ module.exports = ({ bot, knex, config, commands, hooks }) => { const threadLines = await Promise.all(userThreads.map(async thread => { const logUrl = await getLogUrl(thread); const formattedLogUrl = logUrl - ? `<${logUrl}>` + ? `<${addOptQueryStringToUrl(logUrl, args)}>` : `View log with \`${config.prefix}log ${thread.id}\`` const formattedDate = moment.utc(thread.created_at).format("MMM Do [at] HH:mm [UTC]"); return `\`${formattedDate}\`: ${formattedLogUrl}`; @@ -72,7 +85,7 @@ module.exports = ({ bot, knex, config, commands, hooks }) => { const logUrl = await getLogUrl(thread); if (logUrl) { - msg.channel.createMessage(`Open the following link to view the log:\n<${logUrl}>`); + msg.channel.createMessage(`Open the following link to view the log:\n<${addOptQueryStringToUrl(logUrl, args)}>`); return; } @@ -85,11 +98,16 @@ module.exports = ({ bot, knex, config, commands, hooks }) => { msg.channel.createMessage("This thread's logs are not currently available"); }; - commands.addInboxServerCommand("logs", " [page:number]", logsCmd); - commands.addInboxServerCommand("logs", "[page:number]", logsCmd); + const logCmdOptions = [ + { name: "verbose", shortcut: "v", isSwitch: true }, + { name: "simple", shortcut: "s", isSwitch: true }, + ]; - commands.addInboxServerCommand("log", "[threadId:string]", logCmd); - commands.addInboxServerCommand("loglink", "[threadId:string]", logCmd); + commands.addInboxServerCommand("logs", " [page:number]", logsCmd, { options: logCmdOptions }); + commands.addInboxServerCommand("logs", "[page:number]", logsCmd, { options: logCmdOptions }); + + commands.addInboxServerCommand("log", "[threadId:string]", logCmd, { options: logCmdOptions }); + commands.addInboxServerCommand("loglink", "[threadId:string]", logCmd, { options: logCmdOptions }); hooks.afterThreadClose(async ({ threadId }) => { const thread = await threads.findById(threadId); From f4ced372ba53a53282bd3d2aad6147d0dcd0d363 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 22 Oct 2020 00:47:59 +0300 Subject: [PATCH 186/300] Fix SQL syntax error when using snippets --- src/data/snippets.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/data/snippets.js b/src/data/snippets.js index 70cad4e..717c80f 100644 --- a/src/data/snippets.js +++ b/src/data/snippets.js @@ -8,7 +8,7 @@ const Snippet = require("./Snippet"); */ async function getSnippet(trigger) { const snippet = await knex("snippets") - .where(knex.raw("LOWER(trigger)"), trigger.toLowerCase()) + .where(knex.raw("LOWER(`trigger`)"), trigger.toLowerCase()) .first(); return (snippet ? new Snippet(snippet) : null); @@ -36,7 +36,7 @@ async function addSnippet(trigger, body, createdBy = 0) { */ async function deleteSnippet(trigger) { return knex("snippets") - .where(knex.raw("LOWER(trigger)"), trigger.toLowerCase()) + .where(knex.raw("LOWER(`trigger`)"), trigger.toLowerCase()) .delete(); } From d05672a2dcef986e2bff550c7f704610b31d529d Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 22 Oct 2020 00:53:13 +0300 Subject: [PATCH 187/300] Expose the display roles module to plugins --- CHANGELOG.md | 2 ++ src/data/Thread.js | 2 +- src/data/{moderatorRoleOverrides.js => displayRoles.js} | 0 src/modules/roles.js | 2 +- src/pluginApi.js | 2 ++ src/plugins.js | 2 ++ 6 files changed, 8 insertions(+), 2 deletions(-) rename src/data/{moderatorRoleOverrides.js => displayRoles.js} (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index b83d333..e2d95cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,8 @@ Please report any bugs you encounter by [creating a GitHub issue](https://github * System messages now have a formatter * The `beforeNewThread` hook's parameters now also include the original DM message object * Plugins can now access the `threads` module (via `pluginApi.threads`) to create and fetch threads +* Plugins can now access the `displayRoles` module (via `pluginApi.displayRoles`) to get, set, and reset display role overrides for moderators, + and to get the final role that will be displayed in moderator replies (by default or per-thread) **Internal/technical updates:** * Modmail now uses [Express](https://expressjs.com/) as its web server for logs/attachments diff --git a/src/data/Thread.js b/src/data/Thread.js index 27f9a0e..09c75dc 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -9,7 +9,7 @@ const attachments = require("./attachments"); const { formatters } = require("../formatters"); const { callAfterThreadCloseHooks } = require("../hooks/afterThreadClose"); const snippets = require("./snippets"); -const { getModeratorThreadDisplayRoleName } = require("./moderatorRoleOverrides"); +const { getModeratorThreadDisplayRoleName } = require("./displayRoles"); const ThreadMessage = require("./ThreadMessage"); diff --git a/src/data/moderatorRoleOverrides.js b/src/data/displayRoles.js similarity index 100% rename from src/data/moderatorRoleOverrides.js rename to src/data/displayRoles.js diff --git a/src/modules/roles.js b/src/modules/roles.js index 5ba59a8..9b77744 100644 --- a/src/modules/roles.js +++ b/src/modules/roles.js @@ -8,7 +8,7 @@ const { getModeratorThreadDisplayRoleName, getModeratorDefaultDisplayRoleName, -} = require("../data/moderatorRoleOverrides"); +} = require("../data/displayRoles"); const ROLE_OVERRIDES_METADATA_KEY = "moderatorRoleOverrides"; diff --git a/src/pluginApi.js b/src/pluginApi.js index 4fdcac9..8ea5fc7 100644 --- a/src/pluginApi.js +++ b/src/pluginApi.js @@ -3,6 +3,7 @@ const { CommandManager } = require("knub-command-manager"); const { Client } = require("eris"); const Knex = require("knex"); const threads = require("./data/threads"); +const displayRoles = require("./data/displayRoles"); /** * @typedef {object} PluginAPI @@ -16,6 +17,7 @@ const threads = require("./data/threads"); * @property {FormattersExport} formats * @property {express.Application} webserver * @property {threads} threads + * @property {displayRoles} displayRoles */ /** diff --git a/src/plugins.js b/src/plugins.js index c0741fe..894c529 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -8,6 +8,7 @@ const childProcess = require("child_process"); const pacote = require("pacote"); const path = require("path"); const threads = require("./data/threads"); +const displayRoles = require("./data/displayRoles"); const pluginSources = { npm: { @@ -130,6 +131,7 @@ module.exports = { formats, webserver, threads, + displayRoles, }; }, }; From 48777b77338500bdf64edcc5f118d69a6a64432c Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 22 Oct 2020 00:53:54 +0300 Subject: [PATCH 188/300] Remove unused constant --- src/modules/roles.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/modules/roles.js b/src/modules/roles.js index 9b77744..d700909 100644 --- a/src/modules/roles.js +++ b/src/modules/roles.js @@ -10,8 +10,6 @@ const { getModeratorDefaultDisplayRoleName, } = require("../data/displayRoles"); -const ROLE_OVERRIDES_METADATA_KEY = "moderatorRoleOverrides"; - module.exports = ({ bot, knex, config, commands }) => { if (! config.allowChangingDisplayRole) { return; From f8f9204dac71114287523f37d220eb4f7fbf3f2b Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 22 Oct 2020 01:30:11 +0300 Subject: [PATCH 189/300] Use custom Eris fork for sticker support --- package-lock.json | 5 ++--- package.json | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index df4330a..ccb681b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1396,9 +1396,8 @@ "integrity": "sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==" }, "eris": { - "version": "0.13.3", - "resolved": "https://registry.npmjs.org/eris/-/eris-0.13.3.tgz", - "integrity": "sha512-WBtLyknOWZpYZL9yPhez0oKUWvYpunSg43hGxawwjwSf3gFXmbEPYrT8KlmZXtpJnX16eQ7mzIq+MgSh3LarEg==", + "version": "github:dragory/eris#9c1935b31a1c2972861464b54bb382a165674b93", + "from": "github:dragory/eris#stickers", "requires": { "opusscript": "^0.0.7", "tweetnacl": "^1.0.1", diff --git a/package.json b/package.json index ed6d3a7..ca49651 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ }, "dependencies": { "ajv": "^6.12.4", - "eris": "^0.13.3", + "eris": "github:dragory/eris#stickers", "express": "^4.17.1", "helmet": "^4.1.1", "humanize-duration": "^3.23.1", From 620ab79f71ea2525ab5fd86115af8451a742c3f8 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 22 Oct 2020 01:37:30 +0300 Subject: [PATCH 190/300] Add basic sticker receiving support No support for moderator replies with stickers. If a user sends the bot a sticker, the thread will show that sticker's name. Not all stickers can be linked to directly (as they're not all regular images), so this feels like the best compromise. --- src/data/Thread.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/data/Thread.js b/src/data/Thread.js index 09c75dc..4d60593 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -346,6 +346,16 @@ class Thread { messageContent = messageContent.trim(); } + if (msg.stickers && msg.stickers.length) { + const stickerLines = msg.stickers.map(sticker => { + return `**`; + }); + + messageContent += "\n\n" + stickerLines.join("\n"); + } + + messageContent = messageContent.trim(); + // Save DB entry let threadMessage = new ThreadMessage({ message_type: THREAD_MESSAGE_TYPE.FROM_USER, From fec963715c6b9da95a1b75a943b875711cba3de6 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 25 Oct 2020 03:40:43 +0200 Subject: [PATCH 191/300] Fix error when sending out moderator reply errors Yo dawg. --- src/data/Thread.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/data/Thread.js b/src/data/Thread.js index 4d60593..c0f331a 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -259,7 +259,7 @@ class Thread { // Because moderator replies have to be editable, we enforce them to fit within 1 message if (! utils.messageContentIsWithinMaxLength(dmContent) || ! utils.messageContentIsWithinMaxLength(inboxContent)) { - await this._deleteThreadMessage(rawThreadMessage); + await this._deleteThreadMessage(threadMessage.id); await this.postSystemMessage("Reply is too long! Make sure your reply is under 2000 characters total, moderator name in the reply included."); return false; } @@ -269,7 +269,7 @@ class Thread { try { dmMessage = await this._sendDMToUser(dmContent, files); } catch (e) { - await this._deleteThreadMessage(rawThreadMessage); + await this._deleteThreadMessage(threadMessage.id); await this.postSystemMessage(`Error while replying to user: ${e.message}`); return false; } From 4decd4229420b8a2979ef80f20e5593b070f4a55 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 25 Oct 2020 03:49:40 +0200 Subject: [PATCH 192/300] Handle ECONNRESET errors gracefully as well --- src/bot.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/bot.js b/src/bot.js index 4293f89..628c8d2 100644 --- a/src/bot.js +++ b/src/bot.js @@ -28,9 +28,10 @@ const bot = new Eris.Client(config.token, { }); bot.on("error", err => { - if (err.code === 1006) { + if (err.code === 1006 || err.code === "ECONNRESET") { // 1006 = "Connection reset by peer" - // Eris allegedly handles this internally, so we can ignore it + // ECONNRESET is similar + // Eris allegedly handles these internally, so we can ignore them return; } From 4663886629875318f52206ad75a1c29db1b4bfd5 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 25 Oct 2020 03:52:27 +0200 Subject: [PATCH 193/300] Also handle error 1001 gracefully --- src/bot.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/bot.js b/src/bot.js index 628c8d2..d65134a 100644 --- a/src/bot.js +++ b/src/bot.js @@ -27,11 +27,15 @@ const bot = new Eris.Client(config.token, { }, }); +// Eris allegedly handles these internally, so we can ignore them +const SAFE_TO_IGNORE_ERROR_CODES = [ + 1001, // "CloudFlare WebSocket proxy restarting" + 1006, // "Connection reset by peer" + "ECONNRESET", // Pretty much the same as above +]; + bot.on("error", err => { - if (err.code === 1006 || err.code === "ECONNRESET") { - // 1006 = "Connection reset by peer" - // ECONNRESET is similar - // Eris allegedly handles these internally, so we can ignore them + if (SAFE_TO_IGNORE_ERROR_CODES.includes(err.code)) { return; } From 012a819242b26ae2215ab58a7c312f2dbf5340e0 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 25 Oct 2020 04:27:12 +0200 Subject: [PATCH 194/300] Send response message after creating thread This way the response message is shown in the right order in the created thread. --- src/data/threads.js | 16 ---------------- src/main.js | 16 +++++++++++++++- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/data/threads.js b/src/data/threads.js index c4429d6..240fd12 100644 --- a/src/data/threads.js +++ b/src/data/threads.js @@ -198,17 +198,6 @@ async function createNewThreadForUser(user, opts = {}) { allowedMentions: utils.getInboxMentionAllowedMentions(), }); } - - // Send auto-reply to the user - if (config.responseMessage) { - const responseMessage = utils.readMultilineConfigValue(config.responseMessage); - - try { - await newThread.sendSystemMessageToUser(responseMessage); - } catch (err) { - responseMessageError = err; - } - } } // Post some info to the beginning of the new thread @@ -276,11 +265,6 @@ async function createNewThreadForUser(user, opts = {}) { } } - // If there were errors sending a response to the user, note that - if (responseMessageError) { - await newThread.postSystemMessage(`**NOTE:** Could not send auto-response to the user. The error given was: \`${responseMessageError.message}\``); - } - // Return the thread return newThread; } diff --git a/src/main.js b/src/main.js index 384b02a..788d0ef 100644 --- a/src/main.js +++ b/src/main.js @@ -147,9 +147,10 @@ function initBaseMessageHandlers() { // Private message handling is queued so e.g. multiple message in quick succession don't result in multiple channels being created messageQueue.add(async () => { let thread = await threads.findOpenThreadByUserId(msg.author.id); + const createNewThread = (thread == null); // New thread - if (! thread) { + if (createNewThread) { // Ignore messages that shouldn't usually open new threads, such as "ok", "thanks", etc. if (config.ignoreAccidentalThreads && msg.content && ACCIDENTAL_THREAD_MESSAGES.includes(msg.content.trim().toLowerCase())) return; @@ -161,6 +162,19 @@ function initBaseMessageHandlers() { if (thread) { await thread.receiveUserReply(msg); + + if (createNewThread) { + // Send auto-reply to the user + if (config.responseMessage) { + const responseMessage = utils.readMultilineConfigValue(config.responseMessage); + + try { + await thread.sendSystemMessageToUser(responseMessage); + } catch (err) { + await thread.postSystemMessage(`**NOTE:** Could not send auto-response to the user. The error given was: \`${err.message}\``); + } + } + } } }); }); From 11629bb6cb728904436a1c2f1240f1594881aad6 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 25 Oct 2020 04:32:44 +0200 Subject: [PATCH 195/300] Fix thread info header rendering in logs --- src/data/threads.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/data/threads.js b/src/data/threads.js index 240fd12..e30a76f 100644 --- a/src/data/threads.js +++ b/src/data/threads.js @@ -253,8 +253,7 @@ async function createNewThreadForUser(user, opts = {}) { infoHeader += "\n────────────────"; - await newThread.postSystemMessage({ - content: infoHeader, + await newThread.postSystemMessage(infoHeader, { allowedMentions: config.mentionUserInThreadHeader ? { users: [user.id] } : undefined, }); From b3f7d094a8b0e88a17631bc3d0930931fc8fcb3a Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 25 Oct 2020 04:54:07 +0200 Subject: [PATCH 196/300] Rephrase [SYSTEM TO USER] to the bot's name instead in the thread channel --- src/formatters.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/formatters.js b/src/formatters.js index 8c086c8..e3b8645 100644 --- a/src/formatters.js +++ b/src/formatters.js @@ -4,6 +4,7 @@ const config = require("./cfg"); const ThreadMessage = require("./data/ThreadMessage"); const {THREAD_MESSAGE_TYPE} = require("./data/constants"); const moment = require("moment"); +const bot = require("./bot"); /** * Function to format the DM that is sent to the user when a staff member replies to them via !reply @@ -190,7 +191,7 @@ const defaultFormatters = { }, formatSystemToUserThreadMessage(threadMessage) { - let result = `**[SYSTEM TO USER]** ${threadMessage.body}`; + let result = `**(Bot) ${bot.user.username}:** ${threadMessage.body}`; for (const link of threadMessage.attachments) { result += `\n\n${link}`; From b2e473de5a36910a48cddbd0e6822fc691ddc1a5 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 25 Oct 2020 04:54:59 +0200 Subject: [PATCH 197/300] SYSTEM -> BOT --- src/formatters.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/formatters.js b/src/formatters.js index e3b8645..0c9a52b 100644 --- a/src/formatters.js +++ b/src/formatters.js @@ -266,9 +266,9 @@ const defaultFormatters = { } } } else if (message.message_type === THREAD_MESSAGE_TYPE.SYSTEM) { - line += ` [SYSTEM] ${message.body}`; + line += ` [BOT] ${message.body}`; } else if (message.message_type === THREAD_MESSAGE_TYPE.SYSTEM_TO_USER) { - line += ` [SYSTEM TO USER] ${message.body}`; + line += ` [BOT TO USER] ${message.body}`; } else if (message.message_type === THREAD_MESSAGE_TYPE.CHAT) { line += ` [CHAT] [${message.user_name}] ${message.body}`; } else if (message.message_type === THREAD_MESSAGE_TYPE.COMMAND) { From c761802ddd492bde1a45425b7bcf0bde981cd590 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Tue, 27 Oct 2020 18:11:31 +0200 Subject: [PATCH 198/300] Fix attachments missing from logs --- src/formatters.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/formatters.js b/src/formatters.js index 0c9a52b..9ed9d82 100644 --- a/src/formatters.js +++ b/src/formatters.js @@ -286,6 +286,11 @@ const defaultFormatters = { line += ` [${message.user_name}] ${message.body}`; } + if (message.attachments.length) { + line += "\n\n"; + line += message.attachments.join("\n"); + } + return line; }); From 7196e690a22f77fa39f89222e2fd9a8a59fefcf2 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Tue, 27 Oct 2020 18:11:59 +0200 Subject: [PATCH 199/300] For 'original' attachments, always use the attachment link from the DMs, even in staff replies --- src/data/Thread.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/data/Thread.js b/src/data/Thread.js index c0f331a..371a034 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -274,8 +274,13 @@ class Thread { return false; } + // Special case: "original" attachments + if (config.attachmentStorage === "original") { + threadMessage.attachments = dmMessage.attachments.map(att => att.url); + } + threadMessage.dm_message_id = dmMessage.id; - await this._updateThreadMessage(threadMessage.id, { dm_message_id: dmMessage.id }); + await this._updateThreadMessage(threadMessage.id, threadMessage.getSQLProps()); // Show the reply in the inbox thread const inboxMessage = await this._postToThreadChannel(inboxContent, files); From 1df7ba3e64f515717d0d76b6b5c8c515fd499dc3 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Tue, 27 Oct 2020 18:26:39 +0200 Subject: [PATCH 200/300] Tweak bot/system user message visibility in threads --- src/formatters.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/formatters.js b/src/formatters.js index 9ed9d82..5a92935 100644 --- a/src/formatters.js +++ b/src/formatters.js @@ -191,7 +191,7 @@ const defaultFormatters = { }, formatSystemToUserThreadMessage(threadMessage) { - let result = `**(Bot) ${bot.user.username}:** ${threadMessage.body}`; + let result = `**⚙️ ${bot.user.username}:** ${threadMessage.body}`; for (const link of threadMessage.attachments) { result += `\n\n${link}`; From fa84fa603407a0145e3454e023e3df24db1fc111 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Tue, 27 Oct 2020 18:27:30 +0200 Subject: [PATCH 201/300] Fix thread channel being deleted before the close message is sent --- src/modules/close.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/modules/close.js b/src/modules/close.js index 196ba7e..5e20e8e 100644 --- a/src/modules/close.js +++ b/src/modules/close.js @@ -66,6 +66,7 @@ module.exports = ({ bot, knex, config, commands }) => { let hasCloseMessage = !! config.closeMessage; let silentClose = false; + let suppressSystemMessages = false; if (msg.channel instanceof Eris.PrivateChannel) { // User is closing the thread by themselves (if enabled) @@ -79,7 +80,7 @@ module.exports = ({ bot, knex, config, commands }) => { // between showing the close command in the thread and closing the thread await messageQueue.add(async () => { thread.postSystemMessage("Thread closed by user, closing..."); - await thread.close(true); + suppressSystemMessages = true; }); closedBy = "the user"; @@ -133,7 +134,6 @@ module.exports = ({ bot, knex, config, commands }) => { } // Regular close - await thread.close(false, silentClose); closedBy = msg.author.username; } @@ -143,6 +143,8 @@ module.exports = ({ bot, knex, config, commands }) => { await thread.sendSystemMessageToUser(closeMessage).catch(() => {}); } + await thread.close(suppressSystemMessages, silentClose); + await sendCloseNotification(thread, `Modmail thread with ${thread.user_name} (${thread.user_id}) was closed by ${closedBy}`); }); From 4184ad17e9cbd2f02b54fb325dab6af091f17d5a Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Tue, 27 Oct 2020 19:15:26 +0200 Subject: [PATCH 202/300] Update CHANGELOG for v3.0.0 --- CHANGELOG.md | 90 ++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 76 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2d95cb..864311d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,17 +1,25 @@ # Changelog -## v2.31.0-beta.3 (UNRELEASED) -**This is a beta release, bugs are expected.** -Please report any bugs you encounter by [creating a GitHub issue](https://github.com/Dragory/modmailbot/issues/new)! +## v3.0.0 +*This changelog also includes changes from v2.31.0-beta.1 and v2.31.0-beta.2* **General changes:** -* Replies are now limited in length to the Discord message limit (including the moderator name and role in the DM sent to the user) - * This was to fix issues with `!edit` and `!delete` when a reply spanned multiple messages -* Snippets can now be included *within* messages by wrapping the snippet name in curly braces - * E.g. `!r Hello! {{rules}}` to include the snippet `rules` in the place of `{{rules}}` - * The symbols used can be changed with the `inlineSnippetStart` and `inlineSnippetEnd` options - * This feature can be disabled by setting `allowInlineSnippets = off` in your config - * By default, the bot will refuse to send a reply with an unknown inline snippet. To disable this behavior, set `errorOnUnknownInlineSnippet = off`. +* **BREAKING CHANGE:** Logs from Modmail versions prior to Feb 2018 are no longer converted automatically + * To update from a Modmail version from before Feb 2018, update to `v2.30.1` and run the bot once first. Then you can update to version v3.0.0 and later. +* **BREAKING CHANGE:** Added support for Node.js 13 and 14, **dropped support for Node.js 10 and 11** + * The supported versions are now 12, 13, and 14 +* **BREAKING CHANGE:** The bot now requests the necessary [Gateway Intents](https://discord.com/developers/docs/topics/gateway#gateway-intents) + * **This includes the privileged "Server Members Intent"**, which is used for server greetings/welcome messages. + This means that [**you need to turn on "Server Members Intent"**](docs/server-members-intent-2.png) on the bot's page on the Discord Developer Portal. +* Added support for editing and deleting staff replies via new `!edit` and `!delete` commands + * This is **enabled by default** + * This can be disabled with the `allowStaffEdit` and `allowStaffDelete` options + * Only the staff member who sent the reply can edit/delete it +* Renamed the following options. Old names are still supported as aliases, so old config files won't break. + * `mainGuildId` => `mainServerId` + * `mailGuildId` => `inboxServerId` + * `categoryAutomation.newThreadFromGuild` => `categoryAutomation.newThreadFromServer` + * `guildGreetings` => `serverGreetings` * Moderators can now set the role they'd like to be displayed with their replies ("display role") by default and on a per-thread basis by using `!role` * Moderators can only choose roles they currently have * You can view your current display role by using `!role` @@ -26,20 +34,70 @@ Please report any bugs you encounter by [creating a GitHub issue](https://github * This feature can be disabled by setting `allowChangingDisplayRole = off` * New option: `fallbackRoleName` * Sets the role name to display in moderator replies if the moderator doesn't have a hoisted role +* New option `logStorage` + * Allows changing how logs are stored + * Possible values are `local` (default), `attachment`, and `none` +* New **default** attachment storage option: `original` + * This option simply links the original attachment and does not rehost it in any way +* New option `reactOnSeen` ([#398](https://github.com/Dragory/modmailbot/pull/398) by @Eegras) + * When enabled, the bot will react to user DMs with a checkmark when they have been received + * The reaction emoji can be customized with the `reactOnSeenEmoji` option +* New option `createThreadOnMention` ([#397](https://github.com/Dragory/modmailbot/pull/397) by @dopeghoti) + * When enabled, a new modmail thread will be created whenever a user mentions/pings the bot on the main server + * As with `pingOnBotMention`, staff members are automatically ignored +* New option `statusType` + * Allows changing the bot's status type between "Playing", "Watching", "Listening" + * Possible values are `playing` (default), `watching`, `listening` +* New option `anonymizeChannelName` ([#457](https://github.com/Dragory/modmailbot/pull/457) by @funkyhippo) + * Off by default. When enabled, instead of using the user's name as the channel name, uses a random channel name instead. + * Useful on single-server setups where people with modified clients can see the names of even hidden channels +* New option `updateNotificationsForBetaVersions` + * Off by default. When enabled, also shows update notifications for beta versions. + * By default, update notifications are only shown for stable releases +* Snippets can now be included *within* messages by wrapping the snippet name in curly braces + * E.g. `!r Hello! {{rules}}` to include the snippet `rules` in the place of `{{rules}}` + * The symbols used can be changed with the `inlineSnippetStart` and `inlineSnippetEnd` options + * This feature can be disabled by setting `allowInlineSnippets = off` in your config + * By default, the bot will refuse to send a reply with an unknown inline snippet. To disable this behavior, set `errorOnUnknownInlineSnippet = off`. +* `mentionRole` can now be set to `none` +* Removed the long-deprecated `logDir` option +* The bot now notifies if the user leaves/joins the server ([#437](https://github.com/Dragory/modmailbot/pull/437) by @DarkView) +* Replies are now limited in length to the Discord message limit (including the moderator name and role in the DM sent to the user) + * This is to fix issues with `!edit` and `!delete` when a reply spanned multiple messages +* DM channel and message IDs are now stored + * Use `!loglink -v` to view these in logs + * Use `!dm_channel_id` in an inbox thread to view the DM channel ID + * *DM channel and message IDs are primarily useful for Discord T&S reports* * Unless `fallbackRoleName` is set, anonymous replies without a role will no longer display "Moderator:" at the beginning of the message * Plugins can now also be installed from NPM modules * Example: `plugins[] = npm:some-plugin-package` +* "Connection reset by peer" error (code 1006) is now handled gracefully in the background and no longer crashes the bot +* Multiple people can now sign up for reply alerts (`!alert`) simultaneously ([#373](https://github.com/Dragory/modmailbot/pull/373) by @DarkView) +* The bot now displays a note if the user sent an application invite, e.g. an invite to listen along on Spotify +* The bot now displays a note if the user sent a sticker, including the sticker's name +* Log formatting is now more consistent and easier to parse with automated tools +* Messages in modmail threads by other bots are no longer ignored, and are displayed in logs +* Added official support for MySQL databases. Refer to the documentation on `dbType` for more details. + * This change also means the following options are **no longer supported:** + * `dbDir` (use `sqliteOptions.filename` to specify the database file instead) + * `knex` (see `dbType` documentation for more details) +* System messages sent to the user, such as `responseMessage` and `closeMessage`, are now shown in the thread channel +* Fixed `!edit_snippet` crashing the bot when leaving the new snippet text empty +* Fix crash when using `!newthread` with the bot's own ID (fixes [#452](https://github.com/Dragory/modmailbot/issues/452)) * Fix occasional bug with expiring blocks where the bot would send the expiry message multiple times * Fix bug with long messages being cut off and only the last part being shown in the thread (most evident in long DMs and e.g. !edit notifications of long messages) -* "Connection reset by peer" error (code 1006) is now handled gracefully in the background and no longer crashes the bot * Fix messages containing *only* a large number (e.g. an ID) rounding the number +* Several common errors are now handled silently in the background, such as "Connection reset by peer" **Plugins:** -* Log storage functions `getLogUrl()`, `getLogFile()`, `getLogCustomResponse()` now take the entire thread object as an argument rather than the thread ID -* Log storage function `save()` can now return information about the saved log to be stored with the thread. This can then be accessed in e.g. `getLogUrl()` via `thread.log_storage_data`. +* Added support for replacing default message formatting in threads, DMs, and logs +* Added support for *hooks*. Hooks can be used to run custom plugin code before/after specific moments. + * Two hooks are initially available: `beforeNewThread` and `afterThreadClose` + * See plugin documentation for mode details +* If your plugin requires special gateway intents, use the new `extraIntents` config option * Plugins can now access the bot's web server via a new `webserver` property in plugin arguments * Plugins can now store *metadata* in threads and thread messages via new `setMetadataValue` and `getMetadataValue` functions on `Thread` and `ThreadMessage` objects -* Edit/delete notifications now have their own message type and formatter. The original message content is now included in the thread message's metadata (see above). +* Plugins can access the API for setting/getting moderator display roles via a new `displayRoles` property in plugin arguments * System messages now have a formatter * The `beforeNewThread` hook's parameters now also include the original DM message object * Plugins can now access the `threads` module (via `pluginApi.threads`) to create and fetch threads @@ -47,6 +105,10 @@ Please report any bugs you encounter by [creating a GitHub issue](https://github and to get the final role that will be displayed in moderator replies (by default or per-thread) **Internal/technical updates:** +* Updated Eris to v0.13.3 +* Updated several other dependencies +* New JSON Schema based config parser that validates each option and their types more strictly to prevent undefined behavior +* Database migrations are now stored under `src/` * Modmail now uses [Express](https://expressjs.com/) as its web server for logs/attachments * Unhandled rejections are now handled the same as uncaught exceptions, and *will* crash the bot From 5ca09ae9b48f46d19405a2acbd92e8d306dd3087 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Tue, 27 Oct 2020 19:17:39 +0200 Subject: [PATCH 203/300] Add note on upgrading to v3.0.0 --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 11b09df..67d1504 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,10 @@ To the user, the entire process happens in DMs with the bot. Inspired by Reddit's modmail system. +**⚠ Note on updating to v3.0.0:** If you're currently using a *very* old version of the bot, from before February 2018, you'll first need to update to v2.30.1 and run the bot once before updating to v3.0.0. + +Always take a backup of your `db/data.sqlite` file before updating the bot. + ## Getting started * **[🛠️ Setting up the bot](docs/setup.md)** * **[🙋 Frequently Asked Questions](docs/faq.md)** From b0eb402b860794de39f4734b9c176d3e755a0975 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Tue, 27 Oct 2020 19:18:41 +0200 Subject: [PATCH 204/300] Recommend installing Node.js 14 in setup docs --- docs/setup.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/setup.md b/docs/setup.md index 0278644..f746198 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -14,7 +14,8 @@ To keep it online, you need to keep the bot process running. ## Prerequisites 1. Create a bot on the [Discord Developer Portal](https://discordapp.com/developers/) 2. Turn on **Server Members Intent** in the bot's settings page on the developer portal ([Image](server-members-intent-2.png)) -3. Install Node.js 12, 13, or 14 +3. Install Node.js 14 (LTS) + * Node.js 15 is not currently officially supported 4. [Download the latest bot release here](https://github.com/Dragory/modmailbot/releases/latest) (click on "Source code (zip)") 5. Extract the downloaded Zip file to a new folder 6. In the bot's folder (that you extracted from the zip file), make a copy of the file `config.example.ini` and rename the copy to `config.ini` From 8a27e25192a3ad11f70dc2e9ab021ac4620ef947 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Tue, 27 Oct 2020 19:26:11 +0200 Subject: [PATCH 205/300] Add docs on how to update the bot --- README.md | 1 + docs/updating.md | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 docs/updating.md diff --git a/README.md b/README.md index 67d1504..6de8952 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ Always take a backup of your `db/data.sqlite` file before updating the bot. ## Getting started * **[🛠️ Setting up the bot](docs/setup.md)** +* **[✨ Updating the bot](docs/updating.md)** * **[🙋 Frequently Asked Questions](docs/faq.md)** * [📝 Configuration](docs/configuration.md) * [🤖 Commands](docs/commands.md) diff --git a/docs/updating.md b/docs/updating.md new file mode 100644 index 0000000..d3fbcd8 --- /dev/null +++ b/docs/updating.md @@ -0,0 +1,17 @@ +# ✨ Updating the bot + +**Before updating the bot, always take a backup of your `db/data.sqlite` file.** + +To update the bot, follow these steps: +1. Shut down the bot +2. Take a backup of your `db/data.sqlite` file + * If you're using a different supported database, take database backups from there +3. Download the latest version of the bot from https://github.com/Dragory/modmailbot/releases/latest +4. Extract the new version's files over the old files +5. Read the [CHANGELOG](https://github.com/Dragory/modmailbot/blob/master/CHANGELOG.md) to see if there are any config changes you have to make + * Especially note changes to supported Node.js versions! +6. Start the bot: + * If you're using `start.bat` to run the bot, just run it again + * If you're running the bot via command line, first run `npm ci` and then start the bot again + +👉 If you run into any issues, **[join the support server for help!](https://discord.gg/vRuhG9R)** From 35c433bd3c19534e9f126c6867fceee6e72d3dae Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Tue, 27 Oct 2020 19:26:56 +0200 Subject: [PATCH 206/300] Add v3.0.0 update notes on the new update docs as well --- docs/updating.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/updating.md b/docs/updating.md index d3fbcd8..95c81c7 100644 --- a/docs/updating.md +++ b/docs/updating.md @@ -2,7 +2,10 @@ **Before updating the bot, always take a backup of your `db/data.sqlite` file.** -To update the bot, follow these steps: +**⚠ Note on updating to v3.0.0:** If you're currently using a *very* old version of the bot, from before February 2018, you'll first need to update to v2.30.1 and run the bot once before updating to v3.0.0. + +## To update the bot, follow these steps: + 1. Shut down the bot 2. Take a backup of your `db/data.sqlite` file * If you're using a different supported database, take database backups from there From f1528a3f05819e99856ad5364757775a3b8040b0 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Tue, 27 Oct 2020 19:28:08 +0200 Subject: [PATCH 207/300] 3.0.0 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index ccb681b..ea207e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "modmailbot", - "version": "2.31.0-beta.2", + "version": "3.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index ca49651..fb54abd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "modmailbot", - "version": "2.31.0-beta.2", + "version": "3.0.0", "description": "", "license": "MIT", "main": "src/index.js", From 8801143861402cc5d078b37d847b6f8ccf75e3e2 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Tue, 27 Oct 2020 19:55:00 +0200 Subject: [PATCH 208/300] Fix local attachments --- src/modules/webserver.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/webserver.js b/src/modules/webserver.js index 0462e7b..ab43b0e 100644 --- a/src/modules/webserver.js +++ b/src/modules/webserver.js @@ -58,7 +58,7 @@ const server = express(); server.use(helmet()); server.get("/logs/:threadId", serveLogs); -server.get("/logs/:attachmentId/:filename", serveAttachments); +server.get("/attachments/:attachmentId/:filename", serveAttachments); server.on("error", err => { console.log("[WARN] Web server error:", err.message); From 3d2a9c517f5628e98ceaa77e710a285f28d0609e Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Tue, 27 Oct 2020 19:55:26 +0200 Subject: [PATCH 209/300] Update CHANGELOG for v3.0.1 --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 864311d..fa3cddd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## v3.0.1 +* Fix local attachments not being accessible through the bot's links + ## v3.0.0 *This changelog also includes changes from v2.31.0-beta.1 and v2.31.0-beta.2* From 73775d3012d4012b96739c3602568145cdb42223 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Tue, 27 Oct 2020 19:55:30 +0200 Subject: [PATCH 210/300] 3.0.1 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index ea207e2..6c3ce6e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "modmailbot", - "version": "3.0.0", + "version": "3.0.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index fb54abd..1e0e2d8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "modmailbot", - "version": "3.0.0", + "version": "3.0.1", "description": "", "license": "MIT", "main": "src/index.js", From 919a1e54f7163bcc99a36c2d4ecda627563a43c0 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Tue, 27 Oct 2020 20:51:27 +0200 Subject: [PATCH 211/300] Fix npm ci failing when git is not installed --- package-lock.json | 8 ++++---- package.json | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6c3ce6e..3ca76e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1396,8 +1396,8 @@ "integrity": "sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==" }, "eris": { - "version": "github:dragory/eris#9c1935b31a1c2972861464b54bb382a165674b93", - "from": "github:dragory/eris#stickers", + "version": "https://github.com/Dragory/eris/archive/9c1935b31a1c2972861464b54bb382a165674b93.tar.gz", + "integrity": "sha512-YskzQHkf6WbW5ukOrBmM7KaCMk+r7PVtvJp+ecpixBmuJCP2dkmOxwa8fvYhprLw97O8Zkcs0JHs2VpoUEmhSA==", "requires": { "opusscript": "^0.0.7", "tweetnacl": "^1.0.1", @@ -5015,8 +5015,8 @@ "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" }, "supervisor": { - "version": "github:petruisfan/node-supervisor#fb89a695779770d3cd63b624ef4b1ab2908c105d", - "from": "github:petruisfan/node-supervisor#fb89a695779770d3cd63b624ef4b1ab2908c105d", + "version": "https://github.com/petruisfan/node-supervisor/archive/fb89a695779770d3cd63b624ef4b1ab2908c105d.tar.gz", + "integrity": "sha512-+87+CaauTtnY3TozWU8dWZ7lR0pFYrdJzJXnRFGUiCMdrs8i7z61PmXI/QH/XA3a1zsu4KYJFYEKAIoBD8r6LA==", "dev": true }, "supports-color": { diff --git a/package.json b/package.json index 1e0e2d8..62f294e 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ }, "dependencies": { "ajv": "^6.12.4", - "eris": "github:dragory/eris#stickers", + "eris": "https://github.com/Dragory/eris/archive/9c1935b31a1c2972861464b54bb382a165674b93.tar.gz", "express": "^4.17.1", "helmet": "^4.1.1", "humanize-duration": "^3.23.1", @@ -44,7 +44,7 @@ "devDependencies": { "eslint": "^7.7.0", "jsdoc-to-markdown": "^6.0.1", - "supervisor": "github:petruisfan/node-supervisor#fb89a695779770d3cd63b624ef4b1ab2908c105d" + "supervisor": "https://github.com/petruisfan/node-supervisor/archive/fb89a695779770d3cd63b624ef4b1ab2908c105d.tar.gz" }, "engines": { "node": ">=12.0.0 <14.0.0" From 23e5c1affe264a34c01ac5ab207309e88c2763f0 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Tue, 27 Oct 2020 20:52:02 +0200 Subject: [PATCH 212/300] Update CHANGELOG for v3.0.2 --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa3cddd..23ce477 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## v3.0.2 +* Fix `npm ci` and `start.bat` failing when Git is not installed + ## v3.0.1 * Fix local attachments not being accessible through the bot's links From a2140b71f8a92d95702056408251baef3715097c Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Tue, 27 Oct 2020 20:52:06 +0200 Subject: [PATCH 213/300] 3.0.2 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3ca76e4..11bf1d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "modmailbot", - "version": "3.0.1", + "version": "3.0.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 62f294e..386ea78 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "modmailbot", - "version": "3.0.1", + "version": "3.0.2", "description": "", "license": "MIT", "main": "src/index.js", From ec95b526155e16dc157ddc59563f0eaaf924aa41 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Tue, 27 Oct 2020 21:15:54 +0200 Subject: [PATCH 214/300] Fix inline snippets only working once per reply --- src/data/Thread.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/Thread.js b/src/data/Thread.js index 371a034..4b48683 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -208,7 +208,7 @@ class Thread { let unknownSnippets = new Set(); text = text.replace( - new RegExp(`${config.inlineSnippetStart}(\\s*\\S+?\\s*)${config.inlineSnippetEnd}`, "i"), + new RegExp(`${config.inlineSnippetStart}(\\s*\\S+?\\s*)${config.inlineSnippetEnd}`, "ig"), (orig, trigger) => { trigger = trigger.trim(); const snippet = snippetMap[trigger.toLowerCase()]; From 15285eda763f959f2f788b3e07109a6a75dc2ce8 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Tue, 27 Oct 2020 21:16:39 +0200 Subject: [PATCH 215/300] Update CHANGELOG for v3.0.3 --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23ce477..0695609 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## v3.0.3 +* Fix inline snippets only working once per reply + ## v3.0.2 * Fix `npm ci` and `start.bat` failing when Git is not installed From 20fff8db1c8a516a89b3bcf4fd7402f8084afec4 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Tue, 27 Oct 2020 21:16:44 +0200 Subject: [PATCH 216/300] 3.0.3 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 11bf1d8..3d29818 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "modmailbot", - "version": "3.0.2", + "version": "3.0.3", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 386ea78..3a6e2ac 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "modmailbot", - "version": "3.0.2", + "version": "3.0.3", "description": "", "license": "MIT", "main": "src/index.js", From 9f8c94939d1deea3b44723c3bae8c00dcdc16dda Mon Sep 17 00:00:00 2001 From: Miikka <2606411+Dragory@users.noreply.github.com> Date: Tue, 27 Oct 2020 23:44:01 +0200 Subject: [PATCH 217/300] Add a note about gateway intents --- docs/updating.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/updating.md b/docs/updating.md index 95c81c7..52353ff 100644 --- a/docs/updating.md +++ b/docs/updating.md @@ -13,6 +13,7 @@ 4. Extract the new version's files over the old files 5. Read the [CHANGELOG](https://github.com/Dragory/modmailbot/blob/master/CHANGELOG.md) to see if there are any config changes you have to make * Especially note changes to supported Node.js versions! + * If you're updating from a version prior to v3.0.0, make sure to enable the **Server Members** intent on the bot's Discord Developer Portal page ([Image](https://raw.githubusercontent.com/Dragory/modmailbot/master/docs/server-members-intent-2.png)) 6. Start the bot: * If you're using `start.bat` to run the bot, just run it again * If you're running the bot via command line, first run `npm ci` and then start the bot again From 8e69385389d56bf0dcc82db6461610c6fdf7997f Mon Sep 17 00:00:00 2001 From: Gugu72 <60748483+Gugu7264@users.noreply.github.com> Date: Sun, 1 Nov 2020 19:38:58 +0100 Subject: [PATCH 218/300] Fix snippet took as language identifier (#491) Added a new line so it doesn't take snippet as language identifier. Without this new line, if the first line of the snippet contains only one word (if it doesn't have at least 1 space character), the first line is taken as a language identifier, unknown in most cases. Just added `\n` to avoid using snippet as a language identifier. --- src/modules/snippets.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/snippets.js b/src/modules/snippets.js index 90d1b5a..1c7a45e 100644 --- a/src/modules/snippets.js +++ b/src/modules/snippets.js @@ -83,7 +83,7 @@ module.exports = ({ bot, knex, config, commands }) => { 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}${args.trigger}\` replies with: \`\`\`${utils.disableCodeBlocks(snippet.body)}\`\`\``); + utils.postSystemMessageWithFallback(msg.channel, thread, `\`${config.snippetPrefix}${args.trigger}\` replies with: \`\`\`\n${utils.disableCodeBlocks(snippet.body)}\`\`\``); } } else { if (args.text) { From 07e763c2be166eb8adb0d774cc4f28aa9920ba9f Mon Sep 17 00:00:00 2001 From: Eight8 <66164881+Eight8-7020@users.noreply.github.com> Date: Sun, 1 Nov 2020 18:39:39 +0000 Subject: [PATCH 219/300] Update modmailbot-pm2.json (#478) Make the pm2 file work on windows, as well as linux. --- modmailbot-pm2.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/modmailbot-pm2.json b/modmailbot-pm2.json index 6322645..d5b8b5d 100644 --- a/modmailbot-pm2.json +++ b/modmailbot-pm2.json @@ -2,7 +2,6 @@ "apps": [{ "name": "ModMailBot", "cwd": "./", - "script": "npm", - "args": "start" + "script": "src/index.js" }] } From f517cccd7b8b33b73b5fc9ebbf2d01d5052115f3 Mon Sep 17 00:00:00 2001 From: jay_lac2000 Date: Sun, 1 Nov 2020 13:41:38 -0500 Subject: [PATCH 220/300] update discord url (#473) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6de8952..959d2ad 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Modmail for Discord -Modmail Bot is a bot for [Discord](https://discordapp.com/) that allows users to DM the bot to contact the server's moderators/staff +Modmail Bot is a bot for [Discord](https://discord.com/) that allows users to DM the bot to contact the server's moderators/staff without messaging them individually or pinging them publically on the server. These DMs get relayed to modmail *threads*, channels where staff members can reply to and talk with the user. To the user, the entire process happens in DMs with the bot. From 91d07dda8aac656944489b8e18513c63415c84ad Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 1 Nov 2020 20:56:35 +0200 Subject: [PATCH 221/300] Allow 'off' to disable mentionRole --- src/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.js b/src/utils.js index 54a0d0c..deafbbc 100644 --- a/src/utils.js +++ b/src/utils.js @@ -255,7 +255,7 @@ function getInboxMention() { const mentionRoles = Array.isArray(config.mentionRole) ? config.mentionRole : [config.mentionRole]; const mentions = []; for (const role of mentionRoles) { - if (role == null) continue; + if (role == null || role === "none" || role === "off" || role === "") continue; else if (role === "here") mentions.push("@here"); else if (role === "everyone") mentions.push("@everyone"); else mentions.push(`<@&${role}>`); From a8580e1ef8a556fdcf7e91ce8b119559967ce448 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 1 Nov 2020 20:58:06 +0200 Subject: [PATCH 222/300] Fix 'new thread' message being shown in thread header when mentionRole is disabled --- src/data/threads.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/data/threads.js b/src/data/threads.js index e30a76f..6ec50a0 100644 --- a/src/data/threads.js +++ b/src/data/threads.js @@ -192,9 +192,10 @@ async function createNewThreadForUser(user, opts = {}) { if (! quiet) { // Ping moderators of the new thread - if (config.mentionRole) { + const staffMention = utils.getInboxMention(); + if (staffMention.trim() !== "") { await newThread.postNonLogMessage({ - content: `${utils.getInboxMention()}New modmail thread (${newThread.user_name})`, + content: `${staffMention}New modmail thread (${newThread.user_name})`, allowedMentions: utils.getInboxMentionAllowedMentions(), }); } From f6825376c0721471652997f1b8e393d525896b9e Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 1 Nov 2020 21:01:42 +0200 Subject: [PATCH 223/300] Unify mentionRole parsing --- src/utils.js | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/utils.js b/src/utils.js index deafbbc..cba844b 100644 --- a/src/utils.js +++ b/src/utils.js @@ -251,12 +251,18 @@ function convertDelayStringToMS(str) { return ms; } -function getInboxMention() { +function getValidMentionRoles() { const mentionRoles = Array.isArray(config.mentionRole) ? config.mentionRole : [config.mentionRole]; + return mentionRoles.filter(roleStr => { + return (roleStr !== null && roleStr !== "none" && roleStr !== "off" && roleStr !== ""); + }); +} + +function getInboxMention() { + const mentionRoles = getValidMentionRoles(); const mentions = []; for (const role of mentionRoles) { - if (role == null || role === "none" || role === "off" || role === "") continue; - else if (role === "here") mentions.push("@here"); + if (role === "here") mentions.push("@here"); else if (role === "everyone") mentions.push("@everyone"); else mentions.push(`<@&${role}>`); } @@ -264,15 +270,14 @@ function getInboxMention() { } function getInboxMentionAllowedMentions() { - const mentionRoles = Array.isArray(config.mentionRole) ? config.mentionRole : [config.mentionRole]; + const mentionRoles = getValidMentionRoles(); const allowedMentions = { everyone: false, roles: [], }; for (const role of mentionRoles) { - if (role == null || role === "none" || role === "") continue; - else if (role === "here" || role === "everyone") allowedMentions.everyone = true; + if (role === "here" || role === "everyone") allowedMentions.everyone = true; else allowedMentions.roles.push(role); } From 280fad36f738cc53f37d826e326710acc6253eac Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 1 Nov 2020 21:41:03 +0200 Subject: [PATCH 224/300] Add thread numbers --- .../20201101210234_add_thread_numbers.js | 12 + ...213139_set_default_thread_number_values.js | 16 + src/data/threads.js | 366 ++++++++++-------- src/modules/close.js | 6 +- src/modules/logs.js | 2 +- 5 files changed, 226 insertions(+), 176 deletions(-) create mode 100644 src/data/migrations/20201101210234_add_thread_numbers.js create mode 100644 src/data/migrations/20201101213139_set_default_thread_number_values.js diff --git a/src/data/migrations/20201101210234_add_thread_numbers.js b/src/data/migrations/20201101210234_add_thread_numbers.js new file mode 100644 index 0000000..7001f83 --- /dev/null +++ b/src/data/migrations/20201101210234_add_thread_numbers.js @@ -0,0 +1,12 @@ +exports.up = async function(knex) { + await knex.schema.table("threads", table => { + table.integer("thread_number"); + table.unique("thread_number"); + }); +}; + +exports.down = async function(knex) { + await knex.schema.table("threads", table => { + table.dropColumn("thread_number"); + }); +}; diff --git a/src/data/migrations/20201101213139_set_default_thread_number_values.js b/src/data/migrations/20201101213139_set_default_thread_number_values.js new file mode 100644 index 0000000..7a26f7a --- /dev/null +++ b/src/data/migrations/20201101213139_set_default_thread_number_values.js @@ -0,0 +1,16 @@ +exports.up = async function(knex) { + const threads = await knex.table("threads") + .orderBy("created_at", "ASC") + .select(["id"]); + + let threadNumber = 0; + for (const { id } of threads) { + await knex.table("threads") + .where("id", id) + .update({ thread_number: ++threadNumber }); + } +}; + +exports.down = async function(knex) { + // Nothing +}; diff --git a/src/data/threads.js b/src/data/threads.js index 6ec50a0..f53a9a1 100644 --- a/src/data/threads.js +++ b/src/data/threads.js @@ -19,6 +19,18 @@ const {THREAD_STATUS, DISOCRD_CHANNEL_TYPES} = require("./constants"); const MINUTES = 60 * 1000; const HOURS = 60 * MINUTES; +let threadCreationQueue = Promise.resolve(); + +function _addToThreadCreationQueue(fn) { + threadCreationQueue = threadCreationQueue + .then(fn) + .catch(err => { + console.error(`Error while creating thread: ${err.message}`); + }); + + return threadCreationQueue; +} + /** * @param {String} id * @returns {Promise} @@ -69,204 +81,206 @@ function getHeaderGuildInfo(member) { * @throws {Error} */ async function createNewThreadForUser(user, opts = {}) { - const quiet = opts.quiet != null ? opts.quiet : false; - const ignoreRequirements = opts.ignoreRequirements != null ? opts.ignoreRequirements : false; - const ignoreHooks = opts.ignoreHooks != null ? opts.ignoreHooks : false; + return _addToThreadCreationQueue(async () => { + const quiet = opts.quiet != null ? opts.quiet : false; + const ignoreRequirements = opts.ignoreRequirements != null ? opts.ignoreRequirements : false; + const ignoreHooks = opts.ignoreHooks != null ? opts.ignoreHooks : false; - const existingThread = await findOpenThreadByUserId(user.id); - if (existingThread) { - throw new Error("Attempted to create a new thread for a user with an existing open thread!"); - } - - // If set in config, check that the user's account is old enough (time since they registered on Discord) - // If the account is too new, don't start a new thread and optionally reply to them with a message - if (config.requiredAccountAge && ! ignoreRequirements) { - if (user.createdAt > moment() - config.requiredAccountAge * HOURS){ - if (config.accountAgeDeniedMessage) { - const accountAgeDeniedMessage = utils.readMultilineConfigValue(config.accountAgeDeniedMessage); - const privateChannel = await user.getDMChannel(); - await privateChannel.createMessage(accountAgeDeniedMessage); - } - return; + const existingThread = await findOpenThreadByUserId(user.id); + if (existingThread) { + throw new Error("Attempted to create a new thread for a user with an existing open thread!"); } - } - // Find which main guilds this user is part of - const mainGuilds = utils.getMainGuilds(); - const userGuildData = new Map(); - - for (const guild of mainGuilds) { - let member = guild.members.get(user.id); - - if (! member) { - try { - member = await bot.getRESTGuildMember(guild.id, user.id); - } catch (e) { - continue; + // If set in config, check that the user's account is old enough (time since they registered on Discord) + // If the account is too new, don't start a new thread and optionally reply to them with a message + if (config.requiredAccountAge && ! ignoreRequirements) { + if (user.createdAt > moment() - config.requiredAccountAge * HOURS){ + if (config.accountAgeDeniedMessage) { + const accountAgeDeniedMessage = utils.readMultilineConfigValue(config.accountAgeDeniedMessage); + const privateChannel = await user.getDMChannel(); + await privateChannel.createMessage(accountAgeDeniedMessage); + } + return; } } - if (member) { - userGuildData.set(guild.id, { guild, member }); - } - } + // Find which main guilds this user is part of + const mainGuilds = utils.getMainGuilds(); + const userGuildData = new Map(); - // If set in config, check that the user has been a member of one of the main guilds long enough - // If they haven't, don't start a new thread and optionally reply to them with a message - if (config.requiredTimeOnServer && ! ignoreRequirements) { - // Check if the user joined any of the main servers a long enough time ago - // If we don't see this user on any of the main guilds (the size check below), assume we're just missing some data and give the user the benefit of the doubt - const isAllowed = userGuildData.size === 0 || Array.from(userGuildData.values()).some(({guild, member}) => { - return member.joinedAt < moment() - config.requiredTimeOnServer * MINUTES; - }); + for (const guild of mainGuilds) { + let member = guild.members.get(user.id); - if (! isAllowed) { - if (config.timeOnServerDeniedMessage) { - const timeOnServerDeniedMessage = utils.readMultilineConfigValue(config.timeOnServerDeniedMessage); - const privateChannel = await user.getDMChannel(); - await privateChannel.createMessage(timeOnServerDeniedMessage); + if (! member) { + try { + member = await bot.getRESTGuildMember(guild.id, user.id); + } catch (e) { + continue; + } } - return; - } - } - // Call any registered beforeNewThreadHooks - const hookResult = await callBeforeNewThreadHooks({ user, opts, message: opts.message }); - if (hookResult.cancelled) return; - - // Use the user's name+discrim for the thread channel's name - // Channel names are particularly picky about what characters they allow, so we gotta do some clean-up - let cleanName = transliterate.slugify(user.username); - if (cleanName === "") cleanName = "unknown"; - cleanName = cleanName.slice(0, 95); // Make sure the discrim fits - - let channelName = `${cleanName}-${user.discriminator}`; - - if (config.anonymizeChannelName) { - channelName = crypto.createHash("md5").update(channelName + Date.now()).digest("hex").slice(0, 12); - } - - console.log(`[NOTE] Creating new thread channel ${channelName}`); - - // Figure out which category we should place the thread channel in - let newThreadCategoryId = hookResult.categoryId || opts.categoryId || null; - - if (! newThreadCategoryId && config.categoryAutomation.newThreadFromServer) { - // Categories for specific source guilds (in case of multiple main guilds) - for (const [guildId, categoryId] of Object.entries(config.categoryAutomation.newThreadFromServer)) { - if (userGuildData.has(guildId)) { - newThreadCategoryId = categoryId; - break; + if (member) { + userGuildData.set(guild.id, { guild, member }); } } - } - if (! newThreadCategoryId && config.categoryAutomation.newThread) { - // Blanket category id for all new threads (also functions as a fallback for the above) - newThreadCategoryId = config.categoryAutomation.newThread; - } - - // Attempt to create the inbox channel for this thread - let createdChannel; - try { - createdChannel = await utils.getInboxGuild().createChannel(channelName, DISOCRD_CHANNEL_TYPES.GUILD_TEXT, { - reason: "New Modmail thread", - parentID: newThreadCategoryId, - }); - } catch (err) { - console.error(`Error creating modmail channel for ${user.username}#${user.discriminator}!`); - throw err; - } - - // Save the new thread in the database - const newThreadId = await createThreadInDB({ - status: THREAD_STATUS.OPEN, - user_id: user.id, - user_name: `${user.username}#${user.discriminator}`, - channel_id: createdChannel.id, - created_at: moment.utc().format("YYYY-MM-DD HH:mm:ss") - }); - - const newThread = await findById(newThreadId); - let responseMessageError = null; - - if (! quiet) { - // Ping moderators of the new thread - const staffMention = utils.getInboxMention(); - if (staffMention.trim() !== "") { - await newThread.postNonLogMessage({ - content: `${staffMention}New modmail thread (${newThread.user_name})`, - allowedMentions: utils.getInboxMentionAllowedMentions(), + // If set in config, check that the user has been a member of one of the main guilds long enough + // If they haven't, don't start a new thread and optionally reply to them with a message + if (config.requiredTimeOnServer && ! ignoreRequirements) { + // Check if the user joined any of the main servers a long enough time ago + // If we don't see this user on any of the main guilds (the size check below), assume we're just missing some data and give the user the benefit of the doubt + const isAllowed = userGuildData.size === 0 || Array.from(userGuildData.values()).some(({guild, member}) => { + return member.joinedAt < moment() - config.requiredTimeOnServer * MINUTES; }); - } - } - // Post some info to the beginning of the new thread - const infoHeaderItems = []; - - // Account age - const accountAge = humanizeDuration(Date.now() - user.createdAt, {largest: 2, round: true}); - infoHeaderItems.push(`ACCOUNT AGE **${accountAge}**`); - - // User id (and mention, if enabled) - if (config.mentionUserInThreadHeader) { - infoHeaderItems.push(`ID **${user.id}** (<@!${user.id}>)`); - } else { - infoHeaderItems.push(`ID **${user.id}**`); - } - - let infoHeader = infoHeaderItems.join(", "); - - // Guild member info - for (const [guildId, guildData] of userGuildData.entries()) { - const {nickname, joinDate} = getHeaderGuildInfo(guildData.member); - const headerItems = [ - `NICKNAME **${utils.escapeMarkdown(nickname)}**`, - `JOINED **${joinDate}** ago` - ]; - - if (guildData.member.voiceState.channelID) { - const voiceChannel = guildData.guild.channels.get(guildData.member.voiceState.channelID); - if (voiceChannel) { - headerItems.push(`VOICE CHANNEL **${utils.escapeMarkdown(voiceChannel.name)}**`); + if (! isAllowed) { + if (config.timeOnServerDeniedMessage) { + const timeOnServerDeniedMessage = utils.readMultilineConfigValue(config.timeOnServerDeniedMessage); + const privateChannel = await user.getDMChannel(); + await privateChannel.createMessage(timeOnServerDeniedMessage); + } + return; } } - if (config.rolesInThreadHeader && guildData.member.roles.length) { - const roles = guildData.member.roles.map(roleId => guildData.guild.roles.get(roleId)).filter(Boolean); - headerItems.push(`ROLES **${roles.map(r => r.name).join(", ")}**`); + // Call any registered beforeNewThreadHooks + const hookResult = await callBeforeNewThreadHooks({ user, opts, message: opts.message }); + if (hookResult.cancelled) return; + + // Use the user's name+discrim for the thread channel's name + // Channel names are particularly picky about what characters they allow, so we gotta do some clean-up + let cleanName = transliterate.slugify(user.username); + if (cleanName === "") cleanName = "unknown"; + cleanName = cleanName.slice(0, 95); // Make sure the discrim fits + + let channelName = `${cleanName}-${user.discriminator}`; + + if (config.anonymizeChannelName) { + channelName = crypto.createHash("md5").update(channelName + Date.now()).digest("hex").slice(0, 12); } - const headerStr = headerItems.join(", "); + console.log(`[NOTE] Creating new thread channel ${channelName}`); - if (mainGuilds.length === 1) { - infoHeader += `\n${headerStr}`; + // Figure out which category we should place the thread channel in + let newThreadCategoryId = hookResult.categoryId || opts.categoryId || null; + + if (! newThreadCategoryId && config.categoryAutomation.newThreadFromServer) { + // Categories for specific source guilds (in case of multiple main guilds) + for (const [guildId, categoryId] of Object.entries(config.categoryAutomation.newThreadFromServer)) { + if (userGuildData.has(guildId)) { + newThreadCategoryId = categoryId; + break; + } + } + } + + if (! newThreadCategoryId && config.categoryAutomation.newThread) { + // Blanket category id for all new threads (also functions as a fallback for the above) + newThreadCategoryId = config.categoryAutomation.newThread; + } + + // Attempt to create the inbox channel for this thread + let createdChannel; + try { + createdChannel = await utils.getInboxGuild().createChannel(channelName, DISOCRD_CHANNEL_TYPES.GUILD_TEXT, { + reason: "New Modmail thread", + parentID: newThreadCategoryId, + }); + } catch (err) { + console.error(`Error creating modmail channel for ${user.username}#${user.discriminator}!`); + throw err; + } + + // Save the new thread in the database + const newThreadId = await createThreadInDB({ + status: THREAD_STATUS.OPEN, + user_id: user.id, + user_name: `${user.username}#${user.discriminator}`, + channel_id: createdChannel.id, + created_at: moment.utc().format("YYYY-MM-DD HH:mm:ss") + }); + + const newThread = await findById(newThreadId); + let responseMessageError = null; + + if (! quiet) { + // Ping moderators of the new thread + const staffMention = utils.getInboxMention(); + if (staffMention.trim() !== "") { + await newThread.postNonLogMessage({ + content: `${staffMention}New modmail thread (${newThread.user_name})`, + allowedMentions: utils.getInboxMentionAllowedMentions(), + }); + } + } + + // Post some info to the beginning of the new thread + const infoHeaderItems = []; + + // Account age + const accountAge = humanizeDuration(Date.now() - user.createdAt, {largest: 2, round: true}); + infoHeaderItems.push(`ACCOUNT AGE **${accountAge}**`); + + // User id (and mention, if enabled) + if (config.mentionUserInThreadHeader) { + infoHeaderItems.push(`ID **${user.id}** (<@!${user.id}>)`); } else { - infoHeader += `\n**[${utils.escapeMarkdown(guildData.guild.name)}]** ${headerStr}`; + infoHeaderItems.push(`ID **${user.id}**`); } - } - // Modmail history / previous logs - const userLogCount = await getClosedThreadCountByUserId(user.id); - if (userLogCount > 0) { - infoHeader += `\n\nThis user has **${userLogCount}** previous modmail threads. Use \`${config.prefix}logs\` to see them.`; - } + let infoHeader = infoHeaderItems.join(", "); - infoHeader += "\n────────────────"; + // Guild member info + for (const [guildId, guildData] of userGuildData.entries()) { + const {nickname, joinDate} = getHeaderGuildInfo(guildData.member); + const headerItems = [ + `NICKNAME **${utils.escapeMarkdown(nickname)}**`, + `JOINED **${joinDate}** ago` + ]; - await newThread.postSystemMessage(infoHeader, { - allowedMentions: config.mentionUserInThreadHeader ? { users: [user.id] } : undefined, + if (guildData.member.voiceState.channelID) { + const voiceChannel = guildData.guild.channels.get(guildData.member.voiceState.channelID); + if (voiceChannel) { + headerItems.push(`VOICE CHANNEL **${utils.escapeMarkdown(voiceChannel.name)}**`); + } + } + + if (config.rolesInThreadHeader && guildData.member.roles.length) { + const roles = guildData.member.roles.map(roleId => guildData.guild.roles.get(roleId)).filter(Boolean); + headerItems.push(`ROLES **${roles.map(r => r.name).join(", ")}**`); + } + + const headerStr = headerItems.join(", "); + + if (mainGuilds.length === 1) { + infoHeader += `\n${headerStr}`; + } else { + infoHeader += `\n**[${utils.escapeMarkdown(guildData.guild.name)}]** ${headerStr}`; + } + } + + // Modmail history / previous logs + const userLogCount = await getClosedThreadCountByUserId(user.id); + if (userLogCount > 0) { + infoHeader += `\n\nThis user has **${userLogCount}** previous modmail threads. Use \`${config.prefix}logs\` to see them.`; + } + + infoHeader += "\n────────────────"; + + await newThread.postSystemMessage(infoHeader, { + allowedMentions: config.mentionUserInThreadHeader ? { users: [user.id] } : undefined, + }); + + if (config.updateNotifications) { + const availableUpdate = await updates.getAvailableUpdate(); + if (availableUpdate) { + await newThread.postNonLogMessage(`📣 New bot version available (${availableUpdate})`); + } + } + + // Return the thread + return newThread; }); - - if (config.updateNotifications) { - const availableUpdate = await updates.getAvailableUpdate(); - if (availableUpdate) { - await newThread.postNonLogMessage(`📣 New bot version available (${availableUpdate})`); - } - } - - // Return the thread - return newThread; } /** @@ -277,7 +291,15 @@ async function createNewThreadForUser(user, opts = {}) { async function createThreadInDB(data) { const threadId = uuid.v4(); const now = moment.utc().format("YYYY-MM-DD HH:mm:ss"); - const finalData = Object.assign({created_at: now, is_legacy: 0}, data, {id: threadId}); + const latestThreadNumberRow = await knex("threads") + .orderBy("thread_number", "DESC") + .first(); + const latestThreadNumber = latestThreadNumberRow ? latestThreadNumberRow.thread_number : 0; + const finalData = Object.assign( + {created_at: now, is_legacy: 0}, + data, + {id: threadId, thread_number: latestThreadNumber + 1} + ); await knex("threads").insert(finalData); diff --git a/src/modules/close.js b/src/modules/close.js index 5e20e8e..35032a6 100644 --- a/src/modules/close.js +++ b/src/modules/close.js @@ -44,7 +44,7 @@ module.exports = ({ bot, knex, config, commands }) => { await thread.close(false, thread.scheduled_close_silent); - await sendCloseNotification(thread, `Modmail thread with ${thread.user_name} (${thread.user_id}) was closed as scheduled by ${thread.scheduled_close_name}`); + await sendCloseNotification(thread, `Modmail thread #${thread.thread_number} with ${thread.user_name} (${thread.user_id}) was closed as scheduled by ${thread.scheduled_close_name}`); } } @@ -145,7 +145,7 @@ module.exports = ({ bot, knex, config, commands }) => { await thread.close(suppressSystemMessages, silentClose); - await sendCloseNotification(thread, `Modmail thread with ${thread.user_name} (${thread.user_id}) was closed by ${closedBy}`); + await sendCloseNotification(thread, `Modmail thread #${thread.thread_number} with ${thread.user_name} (${thread.user_id}) was closed by ${closedBy}`); }); // Auto-close threads if their channel is deleted @@ -164,6 +164,6 @@ module.exports = ({ bot, knex, config, commands }) => { await thread.close(true); - await sendCloseNotification(thread, `Modmail thread with ${thread.user_name} (${thread.user_id}) was closed automatically because the channel was deleted`); + await sendCloseNotification(thread, `Modmail thread #${thread.thread_number} with ${thread.user_name} (${thread.user_id}) was closed automatically because the channel was deleted`); }); }; diff --git a/src/modules/logs.js b/src/modules/logs.js index 51326f9..9fa7757 100644 --- a/src/modules/logs.js +++ b/src/modules/logs.js @@ -48,7 +48,7 @@ module.exports = ({ bot, knex, config, commands, hooks }) => { ? `<${addOptQueryStringToUrl(logUrl, args)}>` : `View log with \`${config.prefix}log ${thread.id}\`` const formattedDate = moment.utc(thread.created_at).format("MMM Do [at] HH:mm [UTC]"); - return `\`${formattedDate}\`: ${formattedLogUrl}`; + return `\`#${thread.thread_number}\` \`${formattedDate}\`: ${formattedLogUrl}`; })); let message = isPaginated From 02daa367f8147e632f661d70facc39fc413979a3 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 1 Nov 2020 21:45:06 +0200 Subject: [PATCH 225/300] Allow using thread number in !log --- src/data/threads.js | 13 +++++++++++++ src/modules/logs.js | 8 ++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/data/threads.js b/src/data/threads.js index f53a9a1..fbefdb2 100644 --- a/src/data/threads.js +++ b/src/data/threads.js @@ -43,6 +43,18 @@ async function findById(id) { return (thread ? new Thread(thread) : null); } +/** + * @param {number} threadNumber + * @returns {Promise} + */ +async function findByThreadNumber(threadNumber) { + const thread = await knex("threads") + .where("thread_number", threadNumber) + .first(); + + return (thread ? new Thread(thread) : null); +} + /** * @param {String} userId * @returns {Promise} @@ -408,6 +420,7 @@ async function getThreadsThatShouldBeSuspended() { module.exports = { findById, + findByThreadNumber, findOpenThreadByUserId, findByChannelId, findOpenThreadByChannelId, diff --git a/src/modules/logs.js b/src/modules/logs.js index 9fa7757..928d3f2 100644 --- a/src/modules/logs.js +++ b/src/modules/logs.js @@ -46,7 +46,7 @@ module.exports = ({ bot, knex, config, commands, hooks }) => { const logUrl = await getLogUrl(thread); const formattedLogUrl = logUrl ? `<${addOptQueryStringToUrl(logUrl, args)}>` - : `View log with \`${config.prefix}log ${thread.id}\`` + : `View log with \`${config.prefix}log ${thread.thread_number}\`` const formattedDate = moment.utc(thread.created_at).format("MMM Do [at] HH:mm [UTC]"); return `\`#${thread.thread_number}\` \`${formattedDate}\`: ${formattedLogUrl}`; })); @@ -75,7 +75,7 @@ module.exports = ({ bot, knex, config, commands, hooks }) => { const threadId = args.threadId || (_thread && _thread.id); if (! threadId) return; - const thread = await threads.findById(threadId); + const thread = (await threads.findById(threadId)) || (await threads.findByThreadNumber(threadId)); if (! thread) return; const customResponse = await getLogCustomResponse(thread); @@ -85,13 +85,13 @@ module.exports = ({ bot, knex, config, commands, hooks }) => { const logUrl = await getLogUrl(thread); if (logUrl) { - msg.channel.createMessage(`Open the following link to view the log:\n<${addOptQueryStringToUrl(logUrl, args)}>`); + msg.channel.createMessage(`Open the following link to view the log for thread #${thread.thread_number}:\n<${addOptQueryStringToUrl(logUrl, args)}>`); return; } const logFile = await getLogFile(thread); if (logFile) { - msg.channel.createMessage("Download the following file to view the log:", logFile); + msg.channel.createMessage(`Download the following file to view the log for thread #${thread.thread_number}:`, logFile); return; } From 6e19575ca4b5a4ecc49f1bb1b7037c8c02efeccc Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 1 Nov 2020 21:46:01 +0200 Subject: [PATCH 226/300] New alias for !log: !thread --- src/modules/logs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/logs.js b/src/modules/logs.js index 928d3f2..5599bd8 100644 --- a/src/modules/logs.js +++ b/src/modules/logs.js @@ -106,7 +106,7 @@ module.exports = ({ bot, knex, config, commands, hooks }) => { commands.addInboxServerCommand("logs", " [page:number]", logsCmd, { options: logCmdOptions }); commands.addInboxServerCommand("logs", "[page:number]", logsCmd, { options: logCmdOptions }); - commands.addInboxServerCommand("log", "[threadId:string]", logCmd, { options: logCmdOptions }); + commands.addInboxServerCommand("log", "[threadId:string]", logCmd, { options: logCmdOptions, aliases: ["thread"] }); commands.addInboxServerCommand("loglink", "[threadId:string]", logCmd, { options: logCmdOptions }); hooks.afterThreadClose(async ({ threadId }) => { From b15e0e955c6799712467e81adcabbfb03dce5a3f Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 1 Nov 2020 21:47:08 +0200 Subject: [PATCH 227/300] Add Thread#thread_number to jsdoc --- src/data/Thread.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/data/Thread.js b/src/data/Thread.js index 4b48683..00e775a 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -17,6 +17,7 @@ const {THREAD_MESSAGE_TYPE, THREAD_STATUS, DISCORD_MESSAGE_ACTIVITY_TYPES} = req /** * @property {String} id + * @property {Number} thread_number * @property {Number} status * @property {String} user_id * @property {String} user_name From 4d5aaaf99da89088d96327382e68fa4f79f4234e Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 1 Nov 2020 21:50:29 +0200 Subject: [PATCH 228/300] Link thread channel if logs are not available but the thread is open when using !log --- src/modules/logs.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/modules/logs.js b/src/modules/logs.js index 5599bd8..fca9b0c 100644 --- a/src/modules/logs.js +++ b/src/modules/logs.js @@ -2,6 +2,7 @@ const threads = require("../data/threads"); const moment = require("moment"); const utils = require("../utils"); const { getLogUrl, getLogFile, getLogCustomResponse, saveLogToStorage } = require("../data/logs"); +const { THREAD_STATUS } = require("../data/constants"); const LOG_LINES_PER_PAGE = 10; @@ -95,6 +96,11 @@ module.exports = ({ bot, knex, config, commands, hooks }) => { return; } + if (thread.status === THREAD_STATUS.OPEN) { + msg.channel.createMessage(`This thread's logs are not currently available, but it's open at <#${thread.channel_id}>`); + return; + } + msg.channel.createMessage("This thread's logs are not currently available"); }; From b2d9c6ecb1ef130d575a5ca7faa2312ac5fdd9e2 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 1 Nov 2020 21:51:37 +0200 Subject: [PATCH 229/300] Clarify mentionRole docs --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 9bd8eb5..a715be3 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -247,7 +247,7 @@ Alias for [inboxServerId](#inboxServerId) **Accepts multiple values.** Role that is mentioned when new threads are created or the bot is mentioned. Accepted values are `none`, `here`, `everyone`, or a role id. Requires `pingOnBotMention` to be enabled. -Set to an empty value (`mentionRole=`) to disable these pings entirely. +Set to `none` to disable these pings entirely. #### mentionUserInThreadHeader **Default:** `off` From 53dc6edb6a7a16fd33c9751fe3d5d57e85d7a967 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 1 Nov 2020 22:16:15 +0200 Subject: [PATCH 230/300] Update plugin API docs --- docs/plugin-api.md | 33 ++++++++++++++++++++++++++++++++- docs/plugins.md | 2 ++ src/pluginApi.js | 28 ++++++++++++++++++++++++---- 3 files changed, 58 insertions(+), 5 deletions(-) diff --git a/docs/plugin-api.md b/docs/plugin-api.md index fa62912..87f013e 100644 --- a/docs/plugin-api.md +++ b/docs/plugin-api.md @@ -16,6 +16,14 @@ Scroll down to [PluginAPI](#PluginAPI) for a list of properties available to plu
PluginHooksAPI : object
+
PluginDisplayRolesAPI : displayRoles
+
+
PluginThreadsAPI : threads
+
+
PluginWebServerAPI : express.Application
+
+
PluginFormattersAPI : FormattersExport
+
@@ -33,7 +41,10 @@ Scroll down to [PluginAPI](#PluginAPI) for a list of properties available to plu | attachments | [PluginAttachmentsAPI](#PluginAttachmentsAPI) | | logs | [PluginLogsAPI](#PluginLogsAPI) | | hooks | [PluginHooksAPI](#PluginHooksAPI) | -| formats | FormattersExport | +| formats | [PluginFormattersAPI](#PluginFormattersAPI) | +| webserver | [PluginWebServerAPI](#PluginWebServerAPI) | +| threads | [PluginThreadsAPI](#PluginThreadsAPI) | +| displayRoles | [PluginDisplayRolesAPI](#PluginDisplayRolesAPI) | @@ -85,3 +96,23 @@ Scroll down to [PluginAPI](#PluginAPI) for a list of properties available to plu | beforeNewThread | AddBeforeNewThreadHookFn | | afterThreadClose | AddAfterThreadCloseHookFn | + + +## PluginDisplayRolesAPI : displayRoles +**Kind**: global typedef +**See**: https://github.com/Dragory/modmailbot/blob/master/src/data/displayRoles.js + + +## PluginThreadsAPI : threads +**Kind**: global typedef +**See**: https://github.com/Dragory/modmailbot/blob/master/src/data/threads.js + + +## PluginWebServerAPI : express.Application +**Kind**: global typedef +**See**: https://expressjs.com/en/api.html#app + + +## PluginFormattersAPI : FormattersExport +**Kind**: global typedef +**See**: https://github.com/Dragory/modmailbot/blob/master/src/formatters.js diff --git a/docs/plugins.md b/docs/plugins.md index b264533..3b6300a 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -73,6 +73,8 @@ The first and only argument to the plugin function is an object with the followi | `hooks` | An object with functions to add *hooks* that are called at specific times, e.g. before a new thread is created | | `formats` | An object with functions that allow you to replace the default functions used for formatting messages and logs | | `webserver` | An [Express Application object](https://expressjs.com/en/api.html#app) that functions as the bot's web server | +| `threads` | An object with functions to find and create threads | +| `displayRoles` | An object with functions to set and get moderators' display roles | See the auto-generated [Plugin API](plugin-api.md) page for details. diff --git a/src/pluginApi.js b/src/pluginApi.js index 8ea5fc7..0a101dc 100644 --- a/src/pluginApi.js +++ b/src/pluginApi.js @@ -14,10 +14,10 @@ const displayRoles = require("./data/displayRoles"); * @property {PluginAttachmentsAPI} attachments * @property {PluginLogsAPI} logs * @property {PluginHooksAPI} hooks - * @property {FormattersExport} formats - * @property {express.Application} webserver - * @property {threads} threads - * @property {displayRoles} displayRoles + * @property {PluginFormattersAPI} formats + * @property {PluginWebServerAPI} webserver + * @property {PluginThreadsAPI} threads + * @property {PluginDisplayRolesAPI} displayRoles */ /** @@ -49,3 +49,23 @@ const displayRoles = require("./data/displayRoles"); * @property {AddBeforeNewThreadHookFn} beforeNewThread * @property {AddAfterThreadCloseHookFn} afterThreadClose */ + +/** + * @typedef {displayRoles} PluginDisplayRolesAPI + * @see https://github.com/Dragory/modmailbot/blob/master/src/data/displayRoles.js + */ + +/** + * @typedef {threads} PluginThreadsAPI + * @see https://github.com/Dragory/modmailbot/blob/master/src/data/threads.js + */ + +/** + * @typedef {express.Application} PluginWebServerAPI + * @see https://expressjs.com/en/api.html#app + */ + +/** + * @typedef {FormattersExport} PluginFormattersAPI + * @see https://github.com/Dragory/modmailbot/blob/master/src/formatters.js + */ From dd4640bfff8c323fda6afa836c69445c5a91e5b5 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 1 Nov 2020 22:51:05 +0200 Subject: [PATCH 231/300] Add autoAlert/autoAlertDelay options --- docs/configuration.md | 9 +++++++++ src/data/Thread.js | 21 +++++++++++++++++++++ src/data/cfg.jsdoc.js | 2 ++ src/data/cfg.schema.json | 11 +++++++++++ src/modules/reply.js | 1 - 5 files changed, 43 insertions(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index a715be3..39d4299 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -133,6 +133,15 @@ Controls how attachments in modmail threads are stored. Possible values: **Default:** *None* When using attachmentStorage is set to "discord", the id of the channel on the inbox server where attachments are saved +#### autoAlert +**Default:** `off` +When enabled, the last moderator to reply to a modmail thread will be automatically alerted when the thread gets a new reply. +This alert kicks in after a delay, set by the `autoAlertDelay` option below. + +#### autoAlertDelay +**Default:** `2m` +The delay after which `autoAlert` kicks in. Uses the same format as timed close; for example `1m30s` for 1 minute and 30 seconds. + #### botMentionResponse **Default:** *None* If set, the bot auto-replies to bot mentions (pings) with this message. Use `{userMention}` in the text to ping the user back. diff --git a/src/data/Thread.js b/src/data/Thread.js index 00e775a..ef0ecc8 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -169,6 +169,7 @@ class Thread { /** * @returns {Promise} + * @private */ async _getAndIncrementNextMessageNumber() { return knex.transaction(async trx => { @@ -186,6 +187,21 @@ class Thread { }); } + /** + * Adds the specified moderator to the thread's alert list after config.autoAlertDelay + * @param {string} modId + * @returns {Promise} + * @private + */ + async _startAutoAlertTimer(modId) { + clearTimeout(this._autoAlertTimeout); + const autoAlertDelay = utils.convertDelayStringToMS(config.autoAlertDelay); + this._autoAlertTimeout = setTimeout(() => { + if (this.status !== THREAD_STATUS.OPEN) return; + this.addAlert(modId); + }, autoAlertDelay); + } + /** * @param {Eris.Member} moderator * @param {string} text @@ -296,6 +312,11 @@ class Thread { await this.postSystemMessage("Cancelling scheduled closing of this thread due to new reply"); } + // If enabled, set up a reply alert for the moderator after a slight delay + if (config.autoAlert) { + this._startAutoAlertTimer(moderator.id); + } + return true; } diff --git a/src/data/cfg.jsdoc.js b/src/data/cfg.jsdoc.js index 4431a30..f743562 100644 --- a/src/data/cfg.jsdoc.js +++ b/src/data/cfg.jsdoc.js @@ -62,6 +62,8 @@ * @property {boolean} [errorOnUnknownInlineSnippet=true] * @property {boolean} [allowChangingDisplayRole=true] * @property {string} [fallbackRoleName=null] + * @property {boolean} [autoAlert=false] + * @property {string} [autoAlertDelay="2m"] Delay before auto-alert kicks in. Uses the same format as timed close; for example 1m30s for 1 minute and 30 seconds. * @property {string} [logStorage="local"] * @property {object} [logOptions] * @property {string} logOptions.attachmentDirectory diff --git a/src/data/cfg.schema.json b/src/data/cfg.schema.json index e490195..6d2bc30 100644 --- a/src/data/cfg.schema.json +++ b/src/data/cfg.schema.json @@ -346,6 +346,17 @@ "default": null }, + "autoAlert": { + "$ref": "#/definitions/customBoolean", + "default": false + }, + + "autoAlertDelay": { + "type": "string", + "default": "2m", + "description": "Delay before auto-alert kicks in. Uses the same format as timed close; for example 1m30s for 1 minute and 30 seconds." + }, + "logStorage": { "type": "string", "default": "local" diff --git a/src/modules/reply.js b/src/modules/reply.js index 17b5678..31ca967 100644 --- a/src/modules/reply.js +++ b/src/modules/reply.js @@ -1,6 +1,5 @@ const attachments = require("../data/attachments"); const utils = require("../utils"); -const config = require("../cfg"); const Thread = require("../data/Thread"); module.exports = ({ bot, knex, config, commands }) => { From a5279feb1864616bf51570205e7345e99881892e Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 1 Nov 2020 22:59:54 +0200 Subject: [PATCH 232/300] Allow overriding mentionRole in threads.createNewThreadForUser() opts --- src/data/threads.js | 12 ++++++++++-- src/utils.js | 46 +++++++++++++++++++++++++++++++++++++++------ 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/src/data/threads.js b/src/data/threads.js index fbefdb2..946d65d 100644 --- a/src/data/threads.js +++ b/src/data/threads.js @@ -83,6 +83,7 @@ function getHeaderGuildInfo(member) { * @property {Message} [message] Original DM message that is trying to start the thread, if there is one * @property {string} [categoryId] Category where to open the thread * @property {string} [source] A string identifying the source of the new thread + * @property {string} [mentionRole] Override the mentionRole option for this thread */ /** @@ -217,11 +218,18 @@ async function createNewThreadForUser(user, opts = {}) { if (! quiet) { // Ping moderators of the new thread - const staffMention = utils.getInboxMention(); + const staffMention = opts.mentionRole + ? utils.mentionRolesToMention(utils.getValidMentionRoles(opts.mentionRole)) + : utils.getInboxMention(); + if (staffMention.trim() !== "") { + const allowedMentions = opts.mentionRole + ? utils.mentionRolesToAllowedMentions(utils.getValidMentionRoles(opts.mentionRole)) + : utils.getInboxMentionAllowedMentions(); + await newThread.postNonLogMessage({ content: `${staffMention}New modmail thread (${newThread.user_name})`, - allowedMentions: utils.getInboxMentionAllowedMentions(), + allowedMentions, }); } } diff --git a/src/utils.js b/src/utils.js index cba844b..8fe1770 100644 --- a/src/utils.js +++ b/src/utils.js @@ -251,15 +251,25 @@ function convertDelayStringToMS(str) { return ms; } -function getValidMentionRoles() { - const mentionRoles = Array.isArray(config.mentionRole) ? config.mentionRole : [config.mentionRole]; +/** + * @param {string|string[]} mentionRoles + * @returns {string[]} + */ +function getValidMentionRoles(mentionRoles) { + if (! Array.isArray(mentionRoles)) { + mentionRoles = [mentionRoles]; + } + return mentionRoles.filter(roleStr => { return (roleStr !== null && roleStr !== "none" && roleStr !== "off" && roleStr !== ""); }); } -function getInboxMention() { - const mentionRoles = getValidMentionRoles(); +/** + * @param {string[]} mentionRoles + * @returns {string} + */ +function mentionRolesToMention(mentionRoles) { const mentions = []; for (const role of mentionRoles) { if (role === "here") mentions.push("@here"); @@ -269,8 +279,19 @@ function getInboxMention() { return mentions.join(" ") + " "; } -function getInboxMentionAllowedMentions() { - const mentionRoles = getValidMentionRoles(); +/** + * @returns {string} + */ +function getInboxMention() { + const mentionRoles = getValidMentionRoles(config.mentionRole); + return mentionRolesToMention(mentionRoles); +} + +/** + * @param {string[]} mentionRoles + * @returns {object} + */ +function mentionRolesToAllowedMentions(mentionRoles) { const allowedMentions = { everyone: false, roles: [], @@ -284,6 +305,14 @@ function getInboxMentionAllowedMentions() { return allowedMentions; } +/** + * @returns {object} + */ +function getInboxMentionAllowedMentions() { + const mentionRoles = getValidMentionRoles(config.mentionRole); + return mentionRolesToAllowedMentions(mentionRoles); +} + function postSystemMessageWithFallback(channel, thread, text) { if (thread) { thread.postSystemMessage(text); @@ -484,8 +513,13 @@ module.exports = { getMainRole, delayStringRegex, convertDelayStringToMS, + + getValidMentionRoles, + mentionRolesToMention, getInboxMention, + mentionRolesToAllowedMentions, getInboxMentionAllowedMentions, + postSystemMessageWithFallback, chunk, From 5f5ad92aa4824ecc24ce993102331509b3ef7604 Mon Sep 17 00:00:00 2001 From: Gugu72 <60748483+Gugu7264@users.noreply.github.com> Date: Sun, 1 Nov 2020 22:16:33 +0100 Subject: [PATCH 233/300] Fixing docs for the !role command (#488) * Fixing docs for the !role command `!role` was twice instead of `!role` and `!role default` * Update commands.md --- docs/commands.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/commands.md b/docs/commands.md index e980c37..80266ff 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -73,7 +73,7 @@ Delete your own previous reply sent with `!reply`. ### `!role` View your display role for the thread - the role that is shown in front of your name in your replies -### `!role` +### `!role reset` Reset your display role for the thread to the default ### `!role ` @@ -135,7 +135,7 @@ Check if the specified user is blocked. ### `!role` (Outside a modmail thread) View your default display role - the role that is shown in front of your name in your replies -### `!role` +### `!role reset` (Outside a modmail thread) Reset your default display role ### `!role ` From 7a9bcc5b9507131b540b92bb48d852246eb550d4 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 1 Nov 2020 23:26:38 +0200 Subject: [PATCH 234/300] Add pinThreadHeader option --- docs/configuration.md | 4 ++++ src/data/Thread.js | 7 ++++++- src/data/cfg.jsdoc.js | 1 + src/data/cfg.schema.json | 5 +++++ src/data/threads.js | 6 +++++- 5 files changed, 21 insertions(+), 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 39d4299..5c531f8 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -278,6 +278,10 @@ If enabled, a system message will be posted into any open threads if the user le **Default:** `on` If enabled, the bot will mention staff (see `mentionRole` option) on the inbox server when the bot is mentioned on the main server. +#### pinThreadHeader +**Default:** `off` +If enabled, the bot will pin the "thread header" message in each thread that contains the user's details + #### plugins **Default:** *None* **Accepts multiple values.** External plugins to load on startup. See [Plugins](plugins.md) for more information. diff --git a/src/data/Thread.js b/src/data/Thread.js index ef0ecc8..863937d 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -464,7 +464,12 @@ class Thread { const msg = await this._postToThreadChannel(finalContent); threadMessage.inbox_message_id = msg.id; - await this._addThreadMessageToDB(threadMessage.getSQLProps()); + const finalThreadMessage = await this._addThreadMessageToDB(threadMessage.getSQLProps()); + + return { + message: msg, + threadMessage: finalThreadMessage, + }; } /** diff --git a/src/data/cfg.jsdoc.js b/src/data/cfg.jsdoc.js index f743562..e3fd3bf 100644 --- a/src/data/cfg.jsdoc.js +++ b/src/data/cfg.jsdoc.js @@ -64,6 +64,7 @@ * @property {string} [fallbackRoleName=null] * @property {boolean} [autoAlert=false] * @property {string} [autoAlertDelay="2m"] Delay before auto-alert kicks in. Uses the same format as timed close; for example 1m30s for 1 minute and 30 seconds. + * @property {boolean} [pinThreadHeader=false] * @property {string} [logStorage="local"] * @property {object} [logOptions] * @property {string} logOptions.attachmentDirectory diff --git a/src/data/cfg.schema.json b/src/data/cfg.schema.json index 6d2bc30..766671e 100644 --- a/src/data/cfg.schema.json +++ b/src/data/cfg.schema.json @@ -357,6 +357,11 @@ "description": "Delay before auto-alert kicks in. Uses the same format as timed close; for example 1m30s for 1 minute and 30 seconds." }, + "pinThreadHeader": { + "$ref": "#/definitions/customBoolean", + "default": false + }, + "logStorage": { "type": "string", "default": "local" diff --git a/src/data/threads.js b/src/data/threads.js index 946d65d..f0c9cf1 100644 --- a/src/data/threads.js +++ b/src/data/threads.js @@ -287,10 +287,14 @@ async function createNewThreadForUser(user, opts = {}) { infoHeader += "\n────────────────"; - await newThread.postSystemMessage(infoHeader, { + const { message: threadHeaderMessage } = await newThread.postSystemMessage(infoHeader, { allowedMentions: config.mentionUserInThreadHeader ? { users: [user.id] } : undefined, }); + if (config.pinThreadHeader) { + await threadHeaderMessage.pin(); + } + if (config.updateNotifications) { const availableUpdate = await updates.getAvailableUpdate(); if (availableUpdate) { From ab501871ec569cc679c47bc1c82128c16864dfcf Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 1 Nov 2020 23:28:05 +0200 Subject: [PATCH 235/300] Add thread number to logs --- src/formatters.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/formatters.js b/src/formatters.js index 5a92935..6302de6 100644 --- a/src/formatters.js +++ b/src/formatters.js @@ -295,7 +295,7 @@ const defaultFormatters = { }); const openedAt = moment(thread.created_at).format("YYYY-MM-DD HH:mm:ss"); - const header = `# Modmail thread with ${thread.user_name} (${thread.user_id}) started at ${openedAt}. All times are in UTC+0.`; + const header = `# Modmail thread #${thread.thread_number} with ${thread.user_name} (${thread.user_id}) started at ${openedAt}. All times are in UTC+0.`; const fullResult = header + "\n\n" + lines.join("\n"); From 2f44b636907674f56226388b759663b4dbff21c3 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 1 Nov 2020 23:33:50 +0200 Subject: [PATCH 236/300] More consistent wording in docs --- docs/configuration.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 5c531f8..9a774ca 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -135,7 +135,7 @@ When using attachmentStorage is set to "discord", the id of the channel on the i #### autoAlert **Default:** `off` -When enabled, the last moderator to reply to a modmail thread will be automatically alerted when the thread gets a new reply. +If enabled, the last moderator to reply to a modmail thread will be automatically alerted when the thread gets a new reply. This alert kicks in after a delay, set by the `autoAlertDelay` option below. #### autoAlertDelay @@ -179,11 +179,11 @@ commandAliases.x = close #### enableGreeting **Default:** `off` -When enabled, the bot will send a greeting DM to users that join the main server. +If enabled, the bot will send a greeting DM to users that join the main server #### errorOnUnknownInlineSnippet **Default:** `on` -When enabled, the bot will refuse to send any reply with an unknown inline snippet. +If enabled, the bot will refuse to send any reply with an unknown inline snippet. See [allowInlineSnippets](#allowInlineSnippets) for more details. #### fallbackRoleName @@ -280,7 +280,7 @@ If enabled, the bot will mention staff (see `mentionRole` option) on the inbox s #### pinThreadHeader **Default:** `off` -If enabled, the bot will pin the "thread header" message in each thread that contains the user's details +If enabled, the bot will automatically pin the "thread header" message that contains the user's details #### plugins **Default:** *None* From 5aa111e469e928002f6f4d0f524b5aa0491cdd92 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 1 Nov 2020 23:36:02 +0200 Subject: [PATCH 237/300] Update CHANGELOG for v3.1.0 --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0695609..1314056 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## v3.1.0 +* Each thread is now assigned a number, increasing by 1 each thread. This can be used in commands like `!log` in place of the full thread ID. +* New option: `autoAlert` + * When enabled, the last moderator to reply to a modmail thread will be automatically alerted when the thread gets a new reply + * Auto-alert kicks in after a small delay after the reply to prevent alerts in the middle of an ongoing conversation. This delay is set by the option `autoAlertDelay`. +* New option: `pinThreadHeader` + * When enabled, the bot will automatically pin the "thread header" message that contains the user's details +* `!thread` is now an alias for `!log`/`!loglink` +* Fix some bugs with the `mentionRole` option +* `mentionRole = off` now behaves the same as `mentionRole = none` +* Fixed snippet previews (via `!snippet snippet_name_here`) sometimes cutting off the first word ([#491](https://github.com/Dragory/modmailbot/pull/491) by @Gugu7264) +* When calling `threads.createNewThreadForUser()`, plugins can now specify `mentionRole` as one of the options to override the default mentionRole config option value for the new thread + ## v3.0.3 * Fix inline snippets only working once per reply From 963eb5a47b590987b85d665a8aaf03bd8bb7ee3d Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 1 Nov 2020 23:36:23 +0200 Subject: [PATCH 238/300] 3.1.0 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3d29818..d10d48a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "modmailbot", - "version": "3.0.3", + "version": "3.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 3a6e2ac..e27d855 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "modmailbot", - "version": "3.0.3", + "version": "3.1.0", "description": "", "license": "MIT", "main": "src/index.js", From 910d410d6c2b5a1ec0effd5cbdfa5ba7b9b59e6a Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 2 Nov 2020 02:07:15 +0200 Subject: [PATCH 239/300] Fix some message updates not being handled properly --- src/main.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.js b/src/main.js index 788d0ef..076855b 100644 --- a/src/main.js +++ b/src/main.js @@ -188,6 +188,7 @@ function initBaseMessageHandlers() { if (! msg || ! msg.author) return; if (msg.author.id === bot.user.id) return; if (await blocked.isBlocked(msg.author.id)) return; + if (! msg.content) return; // Old message content doesn't persist between bot restarts const oldContent = oldMessage && oldMessage.content || "*Unavailable due to bot restart*"; From 2ae12ee0498869892e91b58c22994914868e2895 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 2 Nov 2020 17:20:18 +0200 Subject: [PATCH 240/300] Ignore Knex ECONNRESET errors Knex handles them internally and reconnects. --- src/knexConfig.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/knexConfig.js b/src/knexConfig.js index 3510afe..9741502 100644 --- a/src/knexConfig.js +++ b/src/knexConfig.js @@ -39,6 +39,11 @@ module.exports = { return; } + if (message === "Connection Error: Error: read ECONNRESET") { + // Knex automatically handles the reconnection + return; + } + console.warn(message); }, }, From d8e6222baefd5fa06087a2a377d5d15b810675d1 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 2 Nov 2020 17:21:43 +0200 Subject: [PATCH 241/300] Label database warnings from Knex clearly --- src/knexConfig.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/knexConfig.js b/src/knexConfig.js index 9741502..3e62000 100644 --- a/src/knexConfig.js +++ b/src/knexConfig.js @@ -44,7 +44,7 @@ module.exports = { return; } - console.warn(message); + console.warn(`[DATABASE WARNING] ${message}`); }, }, }; From 1210b2acaa7210127a2f4a47ba66369483a601e9 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 2 Nov 2020 17:25:56 +0200 Subject: [PATCH 242/300] Fix postSystemMessage() text not being chunked This would cause errors if the system message was over 2000 characters in length. --- src/data/Thread.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/data/Thread.js b/src/data/Thread.js index 863937d..45f1585 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -89,10 +89,11 @@ class Thread { try { let firstMessage; - if (typeof content === "string") { - // Content is a string, chunk it and send it as individual messages. + const textContent = typeof content === "string" ? content : content.content; + if (textContent) { + // Text content is included, chunk it and send it as individual messages. // Files (attachments) are only sent with the last message. - const chunks = utils.chunkMessageLines(content); + const chunks = utils.chunkMessageLines(textContent); for (const [i, chunk] of chunks.entries()) { // Only send embeds, files, etc. with the last message const msg = (i === chunks.length - 1) @@ -102,7 +103,7 @@ class Thread { firstMessage = firstMessage || msg; } } else { - // Content is a full message content object, send it as-is with the files (if any) + // No text content, send as one message firstMessage = await bot.createMessage(this.channel_id, content, file); } From 32c8d025313ef850d130375b62cfed4ccccc9066 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 2 Nov 2020 17:37:07 +0200 Subject: [PATCH 243/300] Add saveAttachment() to the attachments plugin API --- docs/plugin-api.md | 1 + src/data/attachments.js | 15 ++++++++++----- src/pluginApi.js | 1 + src/plugins.js | 3 ++- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/docs/plugin-api.md b/docs/plugin-api.md index 87f013e..dbde608 100644 --- a/docs/plugin-api.md +++ b/docs/plugin-api.md @@ -70,6 +70,7 @@ Scroll down to [PluginAPI](#PluginAPI) for a list of properties available to plu | --- | --- | | addStorageType | AddAttachmentStorageTypeFn | | downloadAttachment | DownloadAttachmentFn | +| saveAttachment | SaveAttachmentFn | diff --git a/src/data/attachments.js b/src/data/attachments.js index a5392c0..8b9be56 100644 --- a/src/data/attachments.js +++ b/src/data/attachments.js @@ -60,6 +60,13 @@ function getErrorResult(msg = null) { * @return {void} */ +/** + * Saves the given attachment based on the configured storage system + * @callback SaveAttachmentFn + * @param {Eris.Attachment} attachment + * @returns {Promise<{ url: string }>} + */ + /** * @type {AttachmentStorageTypeHandler} */ @@ -206,11 +213,9 @@ async function attachmentToDiscordFileObject(attachment) { } /** - * Saves the given attachment based on the configured storage system - * @param {Eris.Attachment} attachment - * @returns {Promise<{ url: string }>} + * @type {SaveAttachmentFn} */ -function saveAttachment(attachment) { +const saveAttachment = (attachment) => { if (attachmentSavePromises[attachment.id]) { return attachmentSavePromises[attachment.id]; } @@ -226,7 +231,7 @@ function saveAttachment(attachment) { }); return attachmentSavePromises[attachment.id]; -} +}; /** * @type AddAttachmentStorageTypeFn diff --git a/src/pluginApi.js b/src/pluginApi.js index 0a101dc..dd776c8 100644 --- a/src/pluginApi.js +++ b/src/pluginApi.js @@ -33,6 +33,7 @@ const displayRoles = require("./data/displayRoles"); * @typedef {object} PluginAttachmentsAPI * @property {AddAttachmentStorageTypeFn} addStorageType * @property {DownloadAttachmentFn} downloadAttachment + * @property {SaveAttachmentFn} saveAttachment */ /** diff --git a/src/plugins.js b/src/plugins.js index 894c529..7ee9fce 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -115,7 +115,8 @@ module.exports = { }, attachments: { addStorageType: attachments.addStorageType, - downloadAttachment: attachments.downloadAttachment + downloadAttachment: attachments.downloadAttachment, + saveAttachment: attachments.saveAttachment, }, logs: { addStorageType: logs.addStorageType, From 26293134453cfce5d5e97275bedfd4b253779eb2 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 2 Nov 2020 18:05:13 +0200 Subject: [PATCH 244/300] Clarify jsdoc on threads.createNewThreadForUser() opts.quiet --- src/data/threads.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/threads.js b/src/data/threads.js index f0c9cf1..7293796 100644 --- a/src/data/threads.js +++ b/src/data/threads.js @@ -77,7 +77,7 @@ function getHeaderGuildInfo(member) { /** * @typedef CreateNewThreadForUserOpts - * @property {boolean} [quiet] If true, doesn't ping mentionRole or reply with responseMessage + * @property {boolean} [quiet] If true, doesn't ping mentionRole * @property {boolean} [ignoreRequirements] If true, creates a new thread even if the account doesn't meet requiredAccountAge * @property {boolean} [ignoreHooks] If true, doesn't call beforeNewThread hooks * @property {Message} [message] Original DM message that is trying to start the thread, if there is one From 66429c629d6cf2da592f2e4a4254080053ff0f5f Mon Sep 17 00:00:00 2001 From: Nils <7890309+DarkView@users.noreply.github.com> Date: Wed, 4 Nov 2020 00:59:55 +0100 Subject: [PATCH 245/300] Fix message chunking not properly handling allowedMentions (#496) --- src/data/Thread.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/data/Thread.js b/src/data/Thread.js index 45f1585..dd95b95 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -90,6 +90,7 @@ class Thread { let firstMessage; const textContent = typeof content === "string" ? content : content.content; + const contentObj = typeof content === "string" ? {} : content; if (textContent) { // Text content is included, chunk it and send it as individual messages. // Files (attachments) are only sent with the last message. @@ -97,8 +98,8 @@ class Thread { for (const [i, chunk] of chunks.entries()) { // Only send embeds, files, etc. with the last message const msg = (i === chunks.length - 1) - ? await bot.createMessage(this.channel_id, chunk, file) - : await bot.createMessage(this.channel_id, chunk); + ? await bot.createMessage(this.channel_id, { ...contentObj, content: chunk }, file) + : await bot.createMessage(this.channel_id, { ...contentObj, content: chunk, embed: null }); firstMessage = firstMessage || msg; } From fcad5df6bf3d23e9048e1cdd3e5100ae808ec415 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 4 Nov 2020 22:58:40 +0200 Subject: [PATCH 246/300] Install npm plugins with --verbose This should allow us to catch several errors that NPM simply swallows when not using --verbose. Yeah, I don't know either. --- src/plugins.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins.js b/src/plugins.js index 7ee9fce..0537de5 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -17,7 +17,7 @@ const pluginSources = { console.log(`Installing ${plugins.length} plugins from NPM...`); let stderr = ""; - const npmProcess = childProcess.spawn("npm", ["install", "--no-save", ...plugins], { cwd: process.cwd() }); + const npmProcess = childProcess.spawn("npm", ["install", "--verbose", "--no-save", ...plugins], { cwd: process.cwd() }); npmProcess.stderr.on("data", data => { stderr += String(data) }); npmProcess.on("close", code => { if (code !== 0) { From e5172612e9ba6879660a99294ada78a179e4d8f7 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 4 Nov 2020 23:00:11 +0200 Subject: [PATCH 247/300] Fix npm plugin installation on Windows --- src/plugins.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/plugins.js b/src/plugins.js index 0537de5..b09392e 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -17,7 +17,12 @@ const pluginSources = { console.log(`Installing ${plugins.length} plugins from NPM...`); let stderr = ""; - const npmProcess = childProcess.spawn("npm", ["install", "--verbose", "--no-save", ...plugins], { cwd: process.cwd() }); + const npmProcessName = /^win/.test(process.platform) ? "npm.cmd" : "npm"; + const npmProcess = childProcess.spawn( + npmProcessName, + ["install", "--verbose", "--no-save", ...plugins], + { cwd: process.cwd() } + ); npmProcess.stderr.on("data", data => { stderr += String(data) }); npmProcess.on("close", code => { if (code !== 0) { From 717072a415bbd8f12f8e0c7491656267f0f06904 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 4 Nov 2020 23:13:45 +0200 Subject: [PATCH 248/300] Improve error handling --- src/index.js | 45 +++++++++++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/src/index.js b/src/index.js index cc2d662..df5893c 100644 --- a/src/index.js +++ b/src/index.js @@ -17,16 +17,45 @@ try { } // Error handling +// Force crash on unhandled rejections and uncaught exceptions. +// Use something like forever/pm2 to restart. +const MAX_STACK_TRACE_LINES = 8; + function errorHandler(err) { // Unknown message types (nitro boosting messages at the time) should be safe to ignore if (err && err.message && err.message.startsWith("Unhandled MESSAGE_CREATE type")) { return; } - // For everything else, crash with the error - console.error(err); + if (err) { + if (typeof err === "string") { + console.error(`Error: ${err}`); + } else if (err instanceof utils.BotError) { + // Leave out stack traces for BotErrors (the message has enough info) + console.error(`Error: ${err.message}`); + } else { + // Truncate long stack traces for other errors + const stack = err.stack || ""; + let stackLines = stack.split("\n"); + if (stackLines.length > (MAX_STACK_TRACE_LINES + 2)) { + stackLines = stackLines.slice(0, MAX_STACK_TRACE_LINES); + stackLines.push(` ...stack trace truncated to ${MAX_STACK_TRACE_LINES} lines`); + } + const finalStack = stackLines.join("\n"); + + if (err.code) { + console.error(`Error ${err.code}: ${finalStack}`); + } else { + console.error(`Error: ${finalStack}`); + } + } + } else { + console.error("Unknown error occurred"); + } + process.exit(1); } + process.on("uncaughtException", errorHandler); process.on("unhandledRejection", errorHandler); @@ -48,18 +77,6 @@ const utils = require("./utils"); const main = require("./main"); const knex = require("./knex"); -// Force crash on unhandled rejections (use something like forever/pm2 to restart) -process.on("unhandledRejection", err => { - if (err instanceof utils.BotError || (err && err.code)) { - // We ignore stack traces for BotErrors (the message has enough info) and network errors from Eris (their stack traces are unreadably long) - console.error(`Error: ${err.message}`); - } else { - console.error(err); - } - - process.exit(1); -}); - (async function() { // Make sure the database is up to date const [completed, newMigrations] = await knex.migrate.list(); From 2a5b766c2b728c75c70e527403f3ebe38b565420 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 4 Nov 2020 23:17:13 +0200 Subject: [PATCH 249/300] Add better error message for 'Disallowed intents specified' --- src/index.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/index.js b/src/index.js index df5893c..f656345 100644 --- a/src/index.js +++ b/src/index.js @@ -33,6 +33,17 @@ function errorHandler(err) { } else if (err instanceof utils.BotError) { // Leave out stack traces for BotErrors (the message has enough info) console.error(`Error: ${err.message}`); + } else if (err.message === "Disallowed intents specified") { + let fullMessage = "Error: Disallowed intents specified"; + fullMessage += "\n\n"; + fullMessage += "To run the bot, you must enable 'Server Members Intent' on your bot's page in the Discord Developer Portal:"; + fullMessage += "\n\n"; + fullMessage += "1. Go to https://discord.com/developers/applications" + fullMessage += "2. Click on your bot" + fullMessage += "3. Click 'Bot' on the sidebar" + fullMessage += "4. Turn on 'Server Members Intent'" + + console.error(fullMessage); } else { // Truncate long stack traces for other errors const stack = err.stack || ""; From 4e8c35cae79fa32c815480b1d54ee0c8f16f626b Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 4 Nov 2020 23:18:45 +0200 Subject: [PATCH 250/300] Clarify 'the bot is not on the modmail server' error slightly --- src/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.js b/src/utils.js index 8fe1770..ee8e390 100644 --- a/src/utils.js +++ b/src/utils.js @@ -18,7 +18,7 @@ let logChannel = null; */ function getInboxGuild() { if (! inboxGuild) inboxGuild = bot.guilds.find(g => g.id === config.inboxServerId); - if (! inboxGuild) throw new BotError("The bot is not on the modmail (inbox) server!"); + if (! inboxGuild) throw new BotError("The bot is not on the inbox server!"); return inboxGuild; } From 4337d74aba90ff2903e4e5f24308538d00b8b44d Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 4 Nov 2020 23:53:03 +0200 Subject: [PATCH 251/300] Fix ignoreHooks opt in createNewThreadForUser() not working --- src/data/threads.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/data/threads.js b/src/data/threads.js index 7293796..6488cf3 100644 --- a/src/data/threads.js +++ b/src/data/threads.js @@ -156,9 +156,16 @@ async function createNewThreadForUser(user, opts = {}) { } } - // Call any registered beforeNewThreadHooks - const hookResult = await callBeforeNewThreadHooks({ user, opts, message: opts.message }); - if (hookResult.cancelled) return; + let hookResult; + if (! ignoreHooks) { + // Call any registered beforeNewThreadHooks + hookResult = await callBeforeNewThreadHooks({ + user, + opts, + message: opts.message + }); + if (hookResult.cancelled) return; + } // Use the user's name+discrim for the thread channel's name // Channel names are particularly picky about what characters they allow, so we gotta do some clean-up @@ -175,7 +182,7 @@ async function createNewThreadForUser(user, opts = {}) { console.log(`[NOTE] Creating new thread channel ${channelName}`); // Figure out which category we should place the thread channel in - let newThreadCategoryId = hookResult.categoryId || opts.categoryId || null; + let newThreadCategoryId = (hookResult && hookResult.categoryId) || opts.categoryId || null; if (! newThreadCategoryId && config.categoryAutomation.newThreadFromServer) { // Categories for specific source guilds (in case of multiple main guilds) From 968d780e285084602131ed8131fbb66361bdaf1d Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 5 Nov 2020 01:29:54 +0200 Subject: [PATCH 252/300] Fix utils being required too late in index.js --- src/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index f656345..a989886 100644 --- a/src/index.js +++ b/src/index.js @@ -16,6 +16,8 @@ try { process.exit(1); } +const utils = require("./utils"); + // Error handling // Force crash on unhandled rejections and uncaught exceptions. // Use something like forever/pm2 to restart. @@ -84,7 +86,6 @@ try { } const config = require("./cfg"); -const utils = require("./utils"); const main = require("./main"); const knex = require("./knex"); From eea6a1c2b788126bde6839828406c11d2e92c851 Mon Sep 17 00:00:00 2001 From: Liam Date: Thu, 5 Nov 2020 16:32:43 +0000 Subject: [PATCH 253/300] Add allowBlock, allowSuspend, and allowSnippets as configuration options (#498) Co-authored-by: Miikka <2606411+Dragory@users.noreply.github.com> --- docs/configuration.md | 18 +++++++++++++++--- src/data/Thread.js | 2 +- src/data/cfg.jsdoc.js | 3 +++ src/data/cfg.schema.json | 13 ++++++++++++- src/modules/block.js | 1 + src/modules/snippets.js | 1 + src/modules/suspend.js | 1 + 7 files changed, 34 insertions(+), 5 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 9a774ca..86d73cc 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -97,17 +97,29 @@ If enabled, staff members can delete their own replies in modmail threads with ` **Default:** `on` If enabled, staff members can edit their own replies in modmail threads with `!edit` +#### allowBlock +**Default:** `on` +If enabled, staff members can block a user from using modmail with `!block` + +#### allowSuspend +**Default:** `on` +If enabled, staff members can suspend a user from using modmail with `!suspend` + +#### allowSnippets +**Default:** `on` +If enabled, staff members can use [Snippets](snippets.md) + #### allowInlineSnippets **Default:** `on` -If enabled, snippets can be included *within* replies by wrapping the snippet's name in {{ and }}. +If `allowSnippets` is enabled, this option controls whether the snippets can be included *within* replies by wrapping the snippet's name in {{ and }}. E.g. `!r Hello! {{rules}}` +See [inlineSnippetStart](#inlineSnippetStart) and [inlineSnippetEnd](#inlineSnippetEnd) to customize the symbols used. + #### allowChangingDisplayRole **Default:** `on` If enabled, moderators can change the role that's shown with their replies to any role they currently have using the `!role` command. -See [inlineSnippetStart](#inlineSnippetStart) and [inlineSnippetEnd](#inlineSnippetEnd) to customize the symbols used. - #### alwaysReply **Default:** `off` If enabled, all messages in modmail threads will be sent to the user without having to use `!r`. diff --git a/src/data/Thread.js b/src/data/Thread.js index dd95b95..54bbce9 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -215,7 +215,7 @@ class Thread { const moderatorName = config.useNicknames && moderator.nick ? moderator.nick : moderator.user.username; const roleName = await getModeratorThreadDisplayRoleName(moderator, this.id); - if (config.allowInlineSnippets) { + if (config.allowSnippets && config.allowInlineSnippets) { // Replace {{snippet}} with the corresponding snippet // The beginning and end of the variable - {{ and }} - can be changed with the config options // config.inlineSnippetStart and config.inlineSnippetEnd diff --git a/src/data/cfg.jsdoc.js b/src/data/cfg.jsdoc.js index e3fd3bf..a2f1fc9 100644 --- a/src/data/cfg.jsdoc.js +++ b/src/data/cfg.jsdoc.js @@ -33,6 +33,9 @@ * @property {boolean} [rolesInThreadHeader=false] * @property {boolean} [allowStaffEdit=true] * @property {boolean} [allowStaffDelete=true] + * @property {boolean} [allowBlock=true] + * @property {boolean} [allowSuspend=true] + * @property {boolean} [allowSnippets=true] * @property {boolean} [enableGreeting=false] * @property {string} [greetingMessage] * @property {string} [greetingAttachment] diff --git a/src/data/cfg.schema.json b/src/data/cfg.schema.json index 766671e..92b5b93 100644 --- a/src/data/cfg.schema.json +++ b/src/data/cfg.schema.json @@ -173,7 +173,18 @@ "$ref": "#/definitions/customBoolean", "default": true }, - + "allowBlock": { + "$ref": "#/definitions/customBoolean", + "default": true + }, + "allowSuspend": { + "$ref": "#/definitions/customBoolean", + "default": true + }, + "allowSnippets": { + "$ref": "#/definitions/customBoolean", + "default": true + }, "enableGreeting": { "$ref": "#/definitions/customBoolean", "default": false diff --git a/src/modules/block.js b/src/modules/block.js index 252be93..348dd5a 100644 --- a/src/modules/block.js +++ b/src/modules/block.js @@ -4,6 +4,7 @@ const blocked = require("../data/blocked"); const utils = require("../utils"); module.exports = ({ bot, knex, config, commands }) => { + if (! config.allowBlock) return; async function removeExpiredBlocks() { const expiredBlocks = await blocked.getExpiredBlocks(); const logChannel = utils.getLogChannel(); diff --git a/src/modules/snippets.js b/src/modules/snippets.js index 1c7a45e..94e8c8e 100644 --- a/src/modules/snippets.js +++ b/src/modules/snippets.js @@ -7,6 +7,7 @@ const whitespaceRegex = /\s/; const quoteChars = ["'", "\""]; module.exports = ({ bot, knex, config, commands }) => { + if (! config.allowSnippets) return; /** * "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) diff --git a/src/modules/suspend.js b/src/modules/suspend.js index bddb756..6ccc305 100644 --- a/src/modules/suspend.js +++ b/src/modules/suspend.js @@ -6,6 +6,7 @@ const config = require("../cfg"); const {THREAD_STATUS} = require("../data/constants"); module.exports = ({ bot, knex, config, commands }) => { + if (! config.allowSuspend) return; // Check for threads that are scheduled to be suspended and suspend them async function applyScheduledSuspensions() { const threadsToBeSuspended = await threads.getThreadsThatShouldBeSuspended(); From 82f418a2997448986fe6b3a5f22c84efebeb81d8 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Fri, 6 Nov 2020 00:34:37 +0200 Subject: [PATCH 254/300] Fix !newthread throwing an error if a hook cancels thread creation !newthread ignores beforeNewThread hooks entirely now. --- src/modules/newthread.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/newthread.js b/src/modules/newthread.js index 3b094f1..b9e57b9 100644 --- a/src/modules/newthread.js +++ b/src/modules/newthread.js @@ -23,6 +23,7 @@ module.exports = ({ bot, knex, config, commands }) => { const createdThread = await threads.createNewThreadForUser(user, { quiet: true, ignoreRequirements: true, + ignoreHooks: true, source: "command", }); From 17acee00391283683f597818061691f3e0ddd3ef Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Fri, 6 Nov 2020 02:30:22 +0200 Subject: [PATCH 255/300] Update to Eris 0.14.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index d10d48a..9fb5f89 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1396,8 +1396,8 @@ "integrity": "sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==" }, "eris": { - "version": "https://github.com/Dragory/eris/archive/9c1935b31a1c2972861464b54bb382a165674b93.tar.gz", - "integrity": "sha512-YskzQHkf6WbW5ukOrBmM7KaCMk+r7PVtvJp+ecpixBmuJCP2dkmOxwa8fvYhprLw97O8Zkcs0JHs2VpoUEmhSA==", + "version": "https://github.com/Dragory/eris/archive/stickers-0.14.0.tar.gz", + "integrity": "sha512-4L04+OUPdKaADpFyatM0FYpNRYRCJSdYEIFRpVbPGjQUeHO4XkbqNZ4knL5yedMJqzzib3u2qFIJUkHu9HgAbw==", "requires": { "opusscript": "^0.0.7", "tweetnacl": "^1.0.1", diff --git a/package.json b/package.json index e27d855..3a831a8 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ }, "dependencies": { "ajv": "^6.12.4", - "eris": "https://github.com/Dragory/eris/archive/9c1935b31a1c2972861464b54bb382a165674b93.tar.gz", + "eris": "https://github.com/Dragory/eris/archive/stickers-0.14.0.tar.gz", "express": "^4.17.1", "helmet": "^4.1.1", "humanize-duration": "^3.23.1", From 91f2beadf72807768a16b0f9d75c02e98f9acad5 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Fri, 6 Nov 2020 02:41:52 +0200 Subject: [PATCH 256/300] Update CHANGELOG for v3.2.0 --- CHANGELOG.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1314056..cf640ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## v3.2.0 + +**General changes:** +* Updated to Eris 0.14.0, which changes the API url used to `discord.com` instead of `discordapp.com` + * API calls to `discordapp.com` are being phased out on Nov 7, 2020 + * This means that bot versions prior to v3.2.0 *might stop working* on Nov 7, 2020 +* New options: `allowBlock`, `allowSuspend`, `allowSnippets` ([#498](https://github.com/Dragory/modmailbot/pull/498) by [@lirolake](https://github.com/lirolake)) + * These all default to `on` +* Improved error messages and error handling + * Removes at least one instance of ECONNRESET errors +* Fixed issue where NPM plugins would not install on Windows +* Fixed issue where mentions by the bot were not working in certain situations ([#496] by [@DarkView](https://github.com/DarkView)) +* Fixed issue where long system messages (primarily from plugins) would not get chunked properly and would throw an error instead + +**Plugins:** +* Make sure to check the [Eris 0.14.0 changelog](https://github.com/abalabahaha/eris/releases/tag/0.14.0) for any changes that might affect your plugins +* The `attachments` object now includes a new `saveAttachment()` function to save arbitrary attachments using the bot's `attachmentStorage` +* Fixed the `ignoreHooks` option for `threads.createNewThreadForUser()` not working +* Fixed `!newthread` throwing an error if a plugin cancels thread creation in the `beforeNewThread` hook + ## v3.1.0 * Each thread is now assigned a number, increasing by 1 each thread. This can be used in commands like `!log` in place of the full thread ID. * New option: `autoAlert` From 17c485306eeb41d4c12dbe080944e49013907132 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Fri, 6 Nov 2020 02:43:17 +0200 Subject: [PATCH 257/300] 3.2.0 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9fb5f89..c9419aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "modmailbot", - "version": "3.1.0", + "version": "3.2.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 3a831a8..65cf6f6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "modmailbot", - "version": "3.1.0", + "version": "3.2.0", "description": "", "license": "MIT", "main": "src/index.js", From 623ec15d13de9c9776c1d1568f5655d377de495c Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 8 Nov 2020 16:46:09 +0200 Subject: [PATCH 258/300] Add option: showResponseMessageInThreadChannel --- docs/configuration.md | 8 +++++++- src/data/Thread.js | 13 ++++++++----- src/data/cfg.jsdoc.js | 1 + src/data/cfg.schema.json | 5 +++++ src/data/threads.js | 1 - src/main.js | 3 ++- 6 files changed, 23 insertions(+), 8 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 86d73cc..f4abcc9 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -331,7 +331,8 @@ Required amount of time (in minutes) the user must be a member of the server bef #### responseMessage **Default:** `Thank you for your message! Our mod team will reply to you here as soon as possible.` -The bot's response to the user when they message the bot and open a new modmail thread +The bot's response to the user when they message the bot and open a new modmail thread. +If you have a multi-line or otherwise long `responseMessage`, you might want to turn off [showResponseMessageInThreadChannel](#showResponseMessageInThreadChannel) to reduce clutter in the thread channel on the inbox server. #### rolesInThreadHeader **Default:** `off` @@ -348,6 +349,11 @@ serverGreetings.541484311354933258.message[] = Welcome to server ID 541484311354 serverGreetings.541484311354933258.message[] = Second line of the greeting. ``` +#### showResponseMessageInThreadChannel +**Default:** `on` +Whether to show the [responseMessage](#responseMessage) sent to the user in the thread channel on the inbox server as well. +If you have a multi-line or otherwise long `responseMessage`, it might be a good idea to turn this off to reduce clutter. + #### smallAttachmentLimit **Default:** `2097152` Size limit of `relaySmallAttachmentsAsAttachments` in bytes (default is 2MB) diff --git a/src/data/Thread.js b/src/data/Thread.js index 54bbce9..6e1ee16 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -481,6 +481,7 @@ class Thread { * @param {boolean} [allowedMentions.everyone] * @param {boolean|string[]} [allowedMentions.roles] * @param {boolean|string[]} [allowedMentions.users] + * @param {boolean} [allowedMentions.postToThreadChannel] * @returns {Promise} */ async sendSystemMessageToUser(text, opts = {}) { @@ -495,12 +496,14 @@ class Thread { const dmContent = await formatters.formatSystemToUserDM(threadMessage); const dmMsg = await this._sendDMToUser(dmContent); - const inboxContent = await formatters.formatSystemToUserThreadMessage(threadMessage); - const finalInboxContent = typeof inboxContent === "string" ? { content: inboxContent } : inboxContent; - finalInboxContent.allowedMentions = opts.allowedMentions; - const inboxMsg = await this._postToThreadChannel(inboxContent); + if (opts.postToThreadChannel !== false) { + const inboxContent = await formatters.formatSystemToUserThreadMessage(threadMessage); + const finalInboxContent = typeof inboxContent === "string" ? {content: inboxContent} : inboxContent; + finalInboxContent.allowedMentions = opts.allowedMentions; + const inboxMsg = await this._postToThreadChannel(inboxContent); + threadMessage.inbox_message_id = inboxMsg.id; + } - threadMessage.inbox_message_id = inboxMsg.id; threadMessage.dm_channel_id = dmMsg.channel.id; threadMessage.dm_message_id = dmMsg.id; diff --git a/src/data/cfg.jsdoc.js b/src/data/cfg.jsdoc.js index a2f1fc9..b6e46db 100644 --- a/src/data/cfg.jsdoc.js +++ b/src/data/cfg.jsdoc.js @@ -68,6 +68,7 @@ * @property {boolean} [autoAlert=false] * @property {string} [autoAlertDelay="2m"] Delay before auto-alert kicks in. Uses the same format as timed close; for example 1m30s for 1 minute and 30 seconds. * @property {boolean} [pinThreadHeader=false] + * @property {boolean} [showResponseMessageInThreadChannel=true] * @property {string} [logStorage="local"] * @property {object} [logOptions] * @property {string} logOptions.attachmentDirectory diff --git a/src/data/cfg.schema.json b/src/data/cfg.schema.json index 92b5b93..bc5d443 100644 --- a/src/data/cfg.schema.json +++ b/src/data/cfg.schema.json @@ -373,6 +373,11 @@ "default": false }, + "showResponseMessageInThreadChannel": { + "$ref": "#/definitions/customBoolean", + "default": true + }, + "logStorage": { "type": "string", "default": "local" diff --git a/src/data/threads.js b/src/data/threads.js index 6488cf3..a5f50e9 100644 --- a/src/data/threads.js +++ b/src/data/threads.js @@ -221,7 +221,6 @@ async function createNewThreadForUser(user, opts = {}) { }); const newThread = await findById(newThreadId); - let responseMessageError = null; if (! quiet) { // Ping moderators of the new thread diff --git a/src/main.js b/src/main.js index 076855b..a220cb1 100644 --- a/src/main.js +++ b/src/main.js @@ -169,7 +169,8 @@ function initBaseMessageHandlers() { const responseMessage = utils.readMultilineConfigValue(config.responseMessage); try { - await thread.sendSystemMessageToUser(responseMessage); + const postToThreadChannel = config.showResponseMessageInThreadChannel; + await thread.sendSystemMessageToUser(responseMessage, { postToThreadChannel }); } catch (err) { await thread.postSystemMessage(`**NOTE:** Could not send auto-response to the user. The error given was: \`${err.message}\``); } From 37cba80ed9d469e767ba4f2f0923190fc56b5ff0 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 22 Nov 2020 12:32:53 +0200 Subject: [PATCH 259/300] Rewrite GitHub NPM plugin names to full GitHub tarball links This allows those plugins to be installed from GitHub even without having Git installed. --- src/plugins.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/plugins.js b/src/plugins.js index b09392e..aca0a98 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -16,11 +16,25 @@ const pluginSources = { return new Promise((resolve, reject) => { console.log(`Installing ${plugins.length} plugins from NPM...`); + // Rewrite GitHub npm package names to full GitHub tarball links to avoid + // needing to have Git installed to install these plugins. + + // $1 package author, $2 package name, $3 branch (optional) + const npmGitHubPattern = /^([a-z0-9_.-]+)\/([a-z0-9_.-]+)(?:#([a-z0-9_.-]+))?$/i; + const rewrittenPluginNames = plugins.map(pluginName => { + const gitHubPackageParts = pluginName.match(npmGitHubPattern); + if (! gitHubPackageParts) { + return pluginName; + } + + return `https://api.github.com/repos/${gitHubPackageParts[1]}/${gitHubPackageParts[2]}/tarball${gitHubPackageParts[3] ? "/" + gitHubPackageParts[3] : ""}`; + }); + let stderr = ""; const npmProcessName = /^win/.test(process.platform) ? "npm.cmd" : "npm"; const npmProcess = childProcess.spawn( npmProcessName, - ["install", "--verbose", "--no-save", ...plugins], + ["install", "--verbose", "--no-save", ...rewrittenPluginNames], { cwd: process.cwd() } ); npmProcess.stderr.on("data", data => { stderr += String(data) }); From 3f3de280910b8e6d55f9dfe552ae52acbb6236e0 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 22 Nov 2020 12:54:44 +0200 Subject: [PATCH 260/300] Show bot version in console on start-up --- src/BotError.js | 5 +++++ src/botVersion.js | 40 ++++++++++++++++++++++++++++++++++++++++ src/index.js | 15 +++++++++------ src/modules/version.js | 31 ++----------------------------- src/utils.js | 5 +---- 5 files changed, 57 insertions(+), 39 deletions(-) create mode 100644 src/BotError.js create mode 100644 src/botVersion.js diff --git a/src/BotError.js b/src/BotError.js new file mode 100644 index 0000000..2629e16 --- /dev/null +++ b/src/BotError.js @@ -0,0 +1,5 @@ +class BotError extends Error {} + +module.exports = { + BotError, +}; diff --git a/src/botVersion.js b/src/botVersion.js new file mode 100644 index 0000000..cdfa315 --- /dev/null +++ b/src/botVersion.js @@ -0,0 +1,40 @@ +const fs = require("fs"); +const path = require("path"); + +const gitDir = path.resolve(__dirname, "..", ".git"); + +function getPackageVersion() { + const packageJson = require("../package.json"); + return packageJson.version; +} + +function getHeadCommitHash() { + try { + fs.accessSync(gitDir); + } catch (e) { + return null; + } + + // Find HEAD ref and read the commit hash from that ref + const headRefInfo = fs.readFileSync(path.resolve(gitDir, "HEAD"), { encoding: "utf8" }); + if (headRefInfo.startsWith("ref:")) { + const refPath = headRefInfo.slice(5).trim(); // ref: refs/heads/... to refs/heads/... + return fs.readFileSync(path.resolve(gitDir, refPath), { encoding: "utf8" }).trim(); + } else { + // Detached head, just the commit hash + return headRefInfo.trim(); + } +} + +function getPrettyVersion() { + const packageVersion = getPackageVersion(); + const headCommitHash = getHeadCommitHash(); + + return headCommitHash + ? `v${packageVersion} (${headCommitHash.slice(0, 8)})` + : packageVersion; +} + +module.exports = { + getPrettyVersion, +}; diff --git a/src/index.js b/src/index.js index a989886..bc67f95 100644 --- a/src/index.js +++ b/src/index.js @@ -16,7 +16,7 @@ try { process.exit(1); } -const utils = require("./utils"); +const { BotError } = require("./BotError"); // Error handling // Force crash on unhandled rejections and uncaught exceptions. @@ -32,7 +32,7 @@ function errorHandler(err) { if (err) { if (typeof err === "string") { console.error(`Error: ${err}`); - } else if (err instanceof utils.BotError) { + } else if (err instanceof BotError) { // Leave out stack traces for BotErrors (the message has enough info) console.error(`Error: ${err.message}`); } else if (err.message === "Disallowed intents specified") { @@ -72,6 +72,9 @@ function errorHandler(err) { process.on("uncaughtException", errorHandler); process.on("unhandledRejection", errorHandler); +const { getPrettyVersion } = require("./botVersion"); +console.log(`Starting Modmail ${getPrettyVersion()}`); + let testedPackage = ""; try { const packageJson = require("../package.json"); @@ -85,11 +88,11 @@ try { process.exit(1); } -const config = require("./cfg"); -const main = require("./main"); -const knex = require("./knex"); - (async function() { + require("./cfg"); + const main = require("./main"); + const knex = require("./knex"); + // Make sure the database is up to date const [completed, newMigrations] = await knex.migrate.list(); if (newMigrations.length > 0) { diff --git a/src/modules/version.js b/src/modules/version.js index b198297..42f6b4e 100644 --- a/src/modules/version.js +++ b/src/modules/version.js @@ -3,7 +3,7 @@ const fs = require("fs"); const {promisify} = require("util"); const utils = require("../utils"); const updates = require("../data/updates"); -const config = require("../cfg"); +const { getPrettyVersion } = require("../botVersion"); const access = promisify(fs.access); const readFile = promisify(fs.readFile); @@ -12,34 +12,7 @@ const GIT_DIR = path.join(__dirname, "..", "..", ".git"); module.exports = ({ bot, knex, config, commands }) => { commands.addInboxServerCommand("version", [], async (msg, args, thread) => { - const packageJson = require("../../package.json"); - const packageVersion = packageJson.version; - - let response = `Modmail v${packageVersion}`; - - let isGit; - try { - await access(GIT_DIR); - isGit = true; - } catch (e) { - isGit = false; - } - - if (isGit) { - let commitHash; - const HEAD = await readFile(path.join(GIT_DIR, "HEAD"), {encoding: "utf8"}); - - if (HEAD.startsWith("ref:")) { - // Branch - const ref = HEAD.match(/^ref: (.*)$/m)[1]; - commitHash = (await readFile(path.join(GIT_DIR, ref), {encoding: "utf8"})).trim(); - } else { - // Detached head - commitHash = HEAD.trim(); - } - - response += ` (${commitHash.slice(0, 7)})`; - } + let response = `Modmail ${getPrettyVersion()}`; if (config.updateNotifications) { const availableUpdate = await updates.getAvailableUpdate(); diff --git a/src/utils.js b/src/utils.js index ee8e390..1cfa685 100644 --- a/src/utils.js +++ b/src/utils.js @@ -4,8 +4,7 @@ const moment = require("moment"); const humanizeDuration = require("humanize-duration"); const publicIp = require("public-ip"); const config = require("./cfg"); - -class BotError extends Error {} +const { BotError } = require("./BotError"); const userMentionRegex = /^<@!?([0-9]+?)>$/; @@ -492,8 +491,6 @@ function chunkMessageLines(str, maxChunkLength = 1990) { } module.exports = { - BotError, - getInboxGuild, getMainGuilds, getLogChannel, From 4a548dc2612a71c356db272f260adc2d204ce076 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 22 Nov 2020 12:56:41 +0200 Subject: [PATCH 261/300] Don't truncate plugin installation errors --- src/PluginInstallationError.js | 5 +++++ src/index.js | 4 ++++ src/plugins.js | 7 ++++--- 3 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 src/PluginInstallationError.js diff --git a/src/PluginInstallationError.js b/src/PluginInstallationError.js new file mode 100644 index 0000000..05bad2c --- /dev/null +++ b/src/PluginInstallationError.js @@ -0,0 +1,5 @@ +class PluginInstallationError extends Error {} + +module.exports = { + PluginInstallationError, +}; diff --git a/src/index.js b/src/index.js index bc67f95..98eadb4 100644 --- a/src/index.js +++ b/src/index.js @@ -17,6 +17,7 @@ try { } const { BotError } = require("./BotError"); +const { PluginInstallationError } = require("./PluginInstallationError"); // Error handling // Force crash on unhandled rejections and uncaught exceptions. @@ -46,6 +47,9 @@ function errorHandler(err) { fullMessage += "4. Turn on 'Server Members Intent'" console.error(fullMessage); + } else if (err instanceof PluginInstallationError) { + // Don't truncate PluginInstallationErrors as they can get lengthy + console.error(err); } else { // Truncate long stack traces for other errors const stack = err.stack || ""; diff --git a/src/plugins.js b/src/plugins.js index aca0a98..15d525a 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -9,6 +9,7 @@ const pacote = require("pacote"); const path = require("path"); const threads = require("./data/threads"); const displayRoles = require("./data/displayRoles"); +const { PluginInstallationError } = require("./PluginInstallationError"); const pluginSources = { npm: { @@ -40,7 +41,7 @@ const pluginSources = { npmProcess.stderr.on("data", data => { stderr += String(data) }); npmProcess.on("close", code => { if (code !== 0) { - return reject(new Error(stderr)); + return reject(new PluginInstallationError(stderr)); } return resolve(); @@ -53,7 +54,7 @@ const pluginSources = { const packageName = manifest.name; const pluginFn = require(packageName); if (typeof pluginFn !== "function") { - throw new Error(`Plugin '${plugin}' is not a valid plugin`); + throw new PluginInstallationError(`Plugin '${plugin}' is not a valid plugin`); } return pluginFn(pluginApi); @@ -66,7 +67,7 @@ const pluginSources = { const requirePath = path.join(__dirname, "..", plugin); const pluginFn = require(requirePath); if (typeof pluginFn !== "function") { - throw new Error(`Plugin '${plugin}' is not a valid plugin`); + throw new PluginInstallationError(`Plugin '${plugin}' is not a valid plugin`); } return pluginFn(pluginApi); }, From c45cd2bc70919ff86a821a0c54f2ba60d8c0896e Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 22 Nov 2020 12:56:55 +0200 Subject: [PATCH 262/300] Include Node.js version in start-up console message --- src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index 98eadb4..9c6f6cb 100644 --- a/src/index.js +++ b/src/index.js @@ -77,7 +77,7 @@ process.on("uncaughtException", errorHandler); process.on("unhandledRejection", errorHandler); const { getPrettyVersion } = require("./botVersion"); -console.log(`Starting Modmail ${getPrettyVersion()}`); +console.log(`Starting Modmail ${getPrettyVersion()} on Node.js ${process.versions.node}`); let testedPackage = ""; try { From 994a07843ad01ba7b6cb54629e479c9543744e53 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 22 Nov 2020 13:00:10 +0200 Subject: [PATCH 263/300] Move start-up version string to the very beginning --- src/index.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/index.js b/src/index.js index 9c6f6cb..3ce1a16 100644 --- a/src/index.js +++ b/src/index.js @@ -5,6 +5,10 @@ if (nodeMajorVersion < 12) { process.exit(1); } +// Print out bot and Node.js version +const { getPrettyVersion } = require("./botVersion"); +console.log(`Starting Modmail ${getPrettyVersion()} on Node.js ${process.versions.node}`); + // Verify node modules have been installed const fs = require("fs"); const path = require("path"); @@ -76,9 +80,6 @@ function errorHandler(err) { process.on("uncaughtException", errorHandler); process.on("unhandledRejection", errorHandler); -const { getPrettyVersion } = require("./botVersion"); -console.log(`Starting Modmail ${getPrettyVersion()} on Node.js ${process.versions.node}`); - let testedPackage = ""; try { const packageJson = require("../package.json"); From daf7cb5deb739251dc02d3b36cd6d9e2da4c8f4d Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 22 Nov 2020 13:14:50 +0200 Subject: [PATCH 264/300] Install plugins before connecting to Discord This avoids unnecessarily connecting to the gateway if plugin installation fails. --- src/main.js | 63 +++++++++++++++++++++++++++++++++++------------------ 1 file changed, 42 insertions(+), 21 deletions(-) diff --git a/src/main.js b/src/main.js index a220cb1..b7fcbfd 100644 --- a/src/main.js +++ b/src/main.js @@ -18,6 +18,9 @@ const {ACCIDENTAL_THREAD_MESSAGES} = require("./data/constants"); module.exports = { async start() { + console.log("Preparing plugins..."); + await installAllPlugins(); + console.log("Connecting to Discord..."); bot.once("ready", async () => { @@ -57,10 +60,11 @@ module.exports = { console.log("Initializing..."); initStatus(); initBaseMessageHandlers(); + initUpdateNotifications(); console.log("Loading plugins..."); - const pluginResult = await initPlugins(); - console.log(`Loaded ${pluginResult.loadedCount} plugins (${pluginResult.builtInCount} built-in plugins, ${pluginResult.externalCount} external plugins)`); + const pluginResult = await loadAllPlugins(); + console.log(`Loaded ${pluginResult.loadedCount} plugins (${pluginResult.baseCount} built-in plugins, ${pluginResult.externalCount} external plugins)`); console.log(""); console.log("Done! Now listening to DMs."); @@ -289,19 +293,14 @@ function initBaseMessageHandlers() { }); } -async function initPlugins() { - // Initialize command manager - const commands = createCommandManager(bot); - - // Register command aliases - if (config.commandAliases) { - for (const alias in config.commandAliases) { - commands.addAlias(config.commandAliases[alias], alias); - } +function initUpdateNotifications() { + if (config.updateNotifications) { + updates.startVersionRefreshLoop(); } +} - // Load plugins - const builtInPlugins = [ +function getBasePlugins() { + return [ "file:./src/modules/reply", "file:./src/modules/close", "file:./src/modules/logs", @@ -319,21 +318,43 @@ async function initPlugins() { "file:./src/modules/joinLeaveNotification", "file:./src/modules/roles", ]; +} - const plugins = [...builtInPlugins, ...config.plugins]; +function getExternalPlugins() { + return config.plugins; +} +function getAllPlugins() { + return [...getBasePlugins(), ...getExternalPlugins()]; +} + +async function installAllPlugins() { + const plugins = getAllPlugins(); await installPlugins(plugins); +} + +async function loadAllPlugins() { + // Initialize command manager + const commands = createCommandManager(bot); + + // Register command aliases + if (config.commandAliases) { + for (const alias in config.commandAliases) { + commands.addAlias(config.commandAliases[alias], alias); + } + } + + // Load plugins + const basePlugins = getBasePlugins(); + const externalPlugins = getExternalPlugins(); + const plugins = getAllPlugins(); const pluginApi = getPluginAPI({ bot, knex, config, commands }); - await loadPlugins(plugins, pluginApi); - - if (config.updateNotifications) { - updates.startVersionRefreshLoop(); - } + await loadPlugins([...basePlugins, ...externalPlugins], pluginApi); return { loadedCount: plugins.length, - builtInCount: builtInPlugins.length, - externalCount: plugins.length - builtInPlugins.length, + baseCount: basePlugins.length, + externalCount: externalPlugins.length, }; } From 18da38367378bfe38a71c468be0fee3dd44cdf23 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 22 Nov 2020 13:15:34 +0200 Subject: [PATCH 265/300] Use 7 chars for git commit hash, not 8 Consistent with GitHub --- src/botVersion.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/botVersion.js b/src/botVersion.js index cdfa315..ee33ac6 100644 --- a/src/botVersion.js +++ b/src/botVersion.js @@ -31,7 +31,7 @@ function getPrettyVersion() { const headCommitHash = getHeadCommitHash(); return headCommitHash - ? `v${packageVersion} (${headCommitHash.slice(0, 8)})` + ? `v${packageVersion} (${headCommitHash.slice(0, 7)})` : packageVersion; } From 179361d76158e173d398fe44f8326103d3b79b47 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 22 Nov 2020 13:18:14 +0200 Subject: [PATCH 266/300] Update json-schema-to-jsdoc and move it to devDependencies --- package-lock.json | 17 ++++++++++------- package.json | 2 +- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index c9419aa..05bad2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2002,7 +2002,8 @@ "foreach": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", - "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=" + "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=", + "dev": true }, "forever-agent": { "version": "0.6.1", @@ -2800,9 +2801,10 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" }, "json-pointer": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/json-pointer/-/json-pointer-0.6.0.tgz", - "integrity": "sha1-jlAFUKaqxUZKRzN32leqbMIoKNc=", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/json-pointer/-/json-pointer-0.6.1.tgz", + "integrity": "sha512-3OvjqKdCBvH41DLpV4iSt6v2XhZXV1bPB4OROuknvUXI7ZQNofieCPkmE26stEJ9zdQuvIxDHCuYhfgxFAAs+Q==", + "dev": true, "requires": { "foreach": "^2.0.4" } @@ -2813,9 +2815,10 @@ "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" }, "json-schema-to-jsdoc": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-to-jsdoc/-/json-schema-to-jsdoc-1.0.0.tgz", - "integrity": "sha512-xuP+10g5VOOTrA5ELnOVO1puiCYPQfx0GqmtDQh/OGGh+CbXyNLtJeEpKl6HPXQbiPPYm7NmMypkRlznZmfZbg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/json-schema-to-jsdoc/-/json-schema-to-jsdoc-1.1.0.tgz", + "integrity": "sha512-HmJ9vqExXMGKeSyRLFi2Q0TiHJYhIx0dY7dHhxQKsHIjfShzDeWHBYfduCeskS7eWxq6UYIdSyri7+sQUv3uZA==", + "dev": true, "requires": { "json-pointer": "^0.6.0" } diff --git a/package.json b/package.json index 65cf6f6..9b1ce7a 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "helmet": "^4.1.1", "humanize-duration": "^3.23.1", "ini": "^1.3.5", - "json-schema-to-jsdoc": "^1.0.0", "json5": "^2.1.3", "knex": "^0.21.5", "knub-command-manager": "^6.1.0", @@ -44,6 +43,7 @@ "devDependencies": { "eslint": "^7.7.0", "jsdoc-to-markdown": "^6.0.1", + "json-schema-to-jsdoc": "^1.1.0", "supervisor": "https://github.com/petruisfan/node-supervisor/archive/fb89a695779770d3cd63b624ef4b1ab2908c105d.tar.gz" }, "engines": { From 8662917beacb72bfa22b1dca3e9e27a5ca0f3342 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 22 Nov 2020 13:22:31 +0200 Subject: [PATCH 267/300] Set inboxServerPermission to manageMessages by default With single server setups being extremely common, this is a safer default than not requiring any permissions at all. --- src/data/cfg.jsdoc.js | 2 +- src/data/cfg.schema.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/data/cfg.jsdoc.js b/src/data/cfg.jsdoc.js index b6e46db..1989a48 100644 --- a/src/data/cfg.jsdoc.js +++ b/src/data/cfg.jsdoc.js @@ -18,7 +18,7 @@ * @property {string} [mentionRole="here"] * @property {boolean} [pingOnBotMention=true] * @property {string} [botMentionResponse] - * @property {array} [inboxServerPermission=[]] + * @property {array} [inboxServerPermission=["manageMessages"]] * @property {boolean} [alwaysReply=false] * @property {boolean} [alwaysReplyAnon=false] * @property {boolean} [useNicknames=false] diff --git a/src/data/cfg.schema.json b/src/data/cfg.schema.json index bc5d443..b0f3989 100644 --- a/src/data/cfg.schema.json +++ b/src/data/cfg.schema.json @@ -115,7 +115,7 @@ "inboxServerPermission": { "$ref": "#/definitions/stringArray", - "default": [] + "default": ["manageMessages"] }, "alwaysReply": { "$ref": "#/definitions/customBoolean", From b6ac6ec791dc75dd155a3a0d9cf49ad13dade8c2 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 22 Nov 2020 13:25:05 +0200 Subject: [PATCH 268/300] Add some common settings to config.example.ini --- config.example.ini | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/config.example.ini b/config.example.ini index ff3c145..c8e7da7 100644 --- a/config.example.ini +++ b/config.example.ini @@ -5,5 +5,12 @@ mainServerId = REPLACE_WITH_MAIN_SERVER_ID inboxServerId = REPLACE_WITH_INBOX_SERVER_ID logChannelId = REPLACE_WITH_LOG_CHANNEL_ID +# Common settings +# ---------------------------------- +prefix = ! +inboxServerPermission = manageMessages +status = Message me for help! +responseMessage = Thank you for your message! Our mod team will reply to you here as soon as possible. + # Add new options below this line: # ---------------------------------- From 402afbf70338c18f2b0d55a980502921797a3399 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 22 Nov 2020 13:27:39 +0200 Subject: [PATCH 269/300] Add CPU arch to start-up message --- src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index 3ce1a16..a62d2ed 100644 --- a/src/index.js +++ b/src/index.js @@ -7,7 +7,7 @@ if (nodeMajorVersion < 12) { // Print out bot and Node.js version const { getPrettyVersion } = require("./botVersion"); -console.log(`Starting Modmail ${getPrettyVersion()} on Node.js ${process.versions.node}`); +console.log(`Starting Modmail ${getPrettyVersion()} on Node.js ${process.versions.node} (${process.arch})`); // Verify node modules have been installed const fs = require("fs"); From aee9cde40acf1291bdb9e7e44fa76a0b38eba15f Mon Sep 17 00:00:00 2001 From: SamiKamal <40594075+SamiKamal@users.noreply.github.com> Date: Sun, 13 Dec 2020 05:40:06 +0300 Subject: [PATCH 270/300] Update README.md (added more emojis to 'Getting Started') (#513) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 959d2ad..bdf7252 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,8 @@ Always take a backup of your `db/data.sqlite` file before updating the bot. * [🤖 Commands](docs/commands.md) * [📋 Snippets](docs/snippets.md) * [🧩 Plugins](docs/plugins.md) -* [Release notes](CHANGELOG.md) -* [**Community Guides & Resources**](https://github.com/Dragory/modmailbot-community-resources) +* [📌 Release notes](CHANGELOG.md) +* [📚 **Community Guides & Resources**](https://github.com/Dragory/modmailbot-community-resources) ## Support server If you need help with setting up the bot or would like to discuss other things related to it, join the support server on Discord here: From e9893fe56ea68559050cd7bf3ae41636c559d096 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 7 Jan 2021 20:00:04 +0200 Subject: [PATCH 271/300] Add CLI option --config/-c to specify config file --- package-lock.json | 19 +++++++++++------- package.json | 3 ++- src/cfg.js | 51 ++++++++++++++++++++++++++++++++++------------- src/cliOpts.js | 1 + 4 files changed, 52 insertions(+), 22 deletions(-) create mode 100644 src/cliOpts.js diff --git a/package-lock.json b/package-lock.json index 05bad2f..191270e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5645,17 +5645,22 @@ "requires": { "ansi-regex": "^5.0.0" } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } } } }, "yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==" } } } diff --git a/package.json b/package.json index 9b1ce7a..ca15f7f 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,8 @@ "sqlite3": "^5.0.0", "tmp": "^0.1.0", "transliteration": "^2.1.11", - "uuid": "^8.3.0" + "uuid": "^8.3.0", + "yargs-parser": "^20.2.4" }, "devDependencies": { "eslint": "^7.7.0", diff --git a/src/cfg.js b/src/cfg.js index e559d13..be33128 100644 --- a/src/cfg.js +++ b/src/cfg.js @@ -2,12 +2,13 @@ const fs = require("fs"); const path = require("path"); const Ajv = require("ajv"); const schema = require("./data/cfg.schema.json"); +const cliOpts = require("./cliOpts"); /** @type {ModmailConfig} */ let config = {}; -// Config files to search for, in priority order -const configFiles = [ +// Auto-detected config files, in priority order +const configFilesToSearch = [ "config.ini", "config.json", "config.json5", @@ -20,24 +21,46 @@ const configFiles = [ "config.json.txt", ]; -let foundConfigFile; -for (const configFile of configFiles) { +let configFileToLoad; + +const requestedConfigFile = cliOpts.config || cliOpts.c; // CLI option --config/-c +if (requestedConfigFile) { try { - fs.accessSync(__dirname + "/../" + configFile); - foundConfigFile = configFile; - break; - } catch (e) {} + // Config files specified with --config/-c are loaded from cwd + fs.accessSync(requestedConfigFile); + configFileToLoad = requestedConfigFile; + } catch (e) { + if (e.code === "ENOENT") { + console.error(`Specified config file was not found: ${requestedConfigFile}`); + } else { + console.error(`Error reading specified config file ${requestedConfigFile}: ${e.message}`); + } + + process.exit(1); + } +} else { + for (const configFile of configFilesToSearch) { + try { + // Auto-detected config files are always loaded from the bot's folder, even if the cwd differs + const relativePath = path.relative(process.cwd(), path.resolve(__dirname, "..", configFile)); + fs.accessSync(relativePath); + configFileToLoad = relativePath; + break; + } catch (e) {} + } } // Load config values from a config file (if any) -if (foundConfigFile) { - console.log(`Loading configuration from ${foundConfigFile}...`); +if (configFileToLoad) { + const srcRelativePath = path.resolve(__dirname, process.cwd(), configFileToLoad); + console.log(`Loading configuration from ${configFileToLoad}...`); + try { - if (foundConfigFile.endsWith(".js")) { - config = require(`../${foundConfigFile}`); + if (configFileToLoad.endsWith(".js")) { + config = require(srcRelativePath); } else { - const raw = fs.readFileSync(__dirname + "/../" + foundConfigFile, {encoding: "utf8"}); - if (foundConfigFile.endsWith(".ini") || foundConfigFile.endsWith(".ini.txt")) { + const raw = fs.readFileSync(configFileToLoad, {encoding: "utf8"}); + if (configFileToLoad.endsWith(".ini") || configFileToLoad.endsWith(".ini.txt")) { config = require("ini").decode(raw); } else { config = require("json5").parse(raw); diff --git a/src/cliOpts.js b/src/cliOpts.js new file mode 100644 index 0000000..f920e40 --- /dev/null +++ b/src/cliOpts.js @@ -0,0 +1 @@ +module.exports = require("yargs-parser")(process.argv.slice(2)); From d702122e5691853486610e1526a37b436682308e Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 7 Jan 2021 20:07:18 +0200 Subject: [PATCH 272/300] Add dev version changes to CHANGELOG.md --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf640ba..72519d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## DEVELOPMENT VERSION +*This version is not released yet.* + +**General changes:** +* **BREAKING CHANGE:** The default value for [`inboxServerPermission`](docs/configuration.md#inboxserverpermission) is now `manageMessages` + * This means that after updating, if you haven't set `inboxServerPermission` in your `config.ini`, + only those members with the "Manage Messages" permission on the inbox server will be able to use the bot's commands +* New option: `showResponseMessageInThreadChannel` + * Controls whether the bot's response message is shown in the thread channel +* Bot and Node.js version is now shown on bot start-up +* `config.example.ini` now contains several common options by default +* When starting the bot via command line, you can now specify which config file to load with the `--config`/`-c` CLI option + +**Plugins:** +* Plugins are now installed before connecting to the Discord Gateway +* Fix GitHub-based NPM plugins requiring Git to be installed to work +* Plugin installation errors are no longer truncated + ## v3.2.0 **General changes:** From b95c134a025b834fb93b68cdb941a8638e32d3fe Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 7 Jan 2021 20:10:06 +0200 Subject: [PATCH 273/300] Update docs with new default value for inboxServerPermission --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index f4abcc9..df09512 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -222,7 +222,7 @@ Alias for [`serverGreetings`](#serverGreetings) If enabled, the bot attempts to ignore common "accidental" messages that would start a new thread, such as "ok", "thanks", etc. #### inboxServerPermission -**Default:** *None* +**Default:** `manageMessages` **Accepts multiple values.** Permission name, user id, or role id required to use bot commands on the inbox server. See ["Permissions" on this page](https://abal.moe/Eris/docs/reference) for supported permission names (e.g. `kickMembers`). From 4a2edc6267687a66afa8a3607345d1d34d0adb3f Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 7 Jan 2021 20:11:57 +0200 Subject: [PATCH 274/300] Change default mentionRole value to none --- docs/configuration.md | 3 +-- src/data/cfg.jsdoc.js | 2 +- src/data/cfg.schema.json | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index df09512..e3e7756 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -264,10 +264,9 @@ Alias for [mainServerId](#mainServerId) Alias for [inboxServerId](#inboxServerId) #### mentionRole -**Default:** `here` +**Default:** `none` **Accepts multiple values.** Role that is mentioned when new threads are created or the bot is mentioned. Accepted values are `none`, `here`, `everyone`, or a role id. -Requires `pingOnBotMention` to be enabled. Set to `none` to disable these pings entirely. #### mentionUserInThreadHeader diff --git a/src/data/cfg.jsdoc.js b/src/data/cfg.jsdoc.js index 1989a48..bd6976a 100644 --- a/src/data/cfg.jsdoc.js +++ b/src/data/cfg.jsdoc.js @@ -15,7 +15,7 @@ * @property {string} [closeMessage] * @property {boolean} [allowUserClose=false] * @property {string} [newThreadCategoryId] - * @property {string} [mentionRole="here"] + * @property {string} [mentionRole="none"] * @property {boolean} [pingOnBotMention=true] * @property {string} [botMentionResponse] * @property {array} [inboxServerPermission=["manageMessages"]] diff --git a/src/data/cfg.schema.json b/src/data/cfg.schema.json index b0f3989..f24de07 100644 --- a/src/data/cfg.schema.json +++ b/src/data/cfg.schema.json @@ -103,7 +103,7 @@ }, "mentionRole": { "type": "string", - "default": "here" + "default": "none" }, "pingOnBotMention": { "$ref": "#/definitions/customBoolean", From 7758408fab3b4cfcffc7e1d8c8a5c114dbd28d1b Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 7 Jan 2021 20:13:42 +0200 Subject: [PATCH 275/300] Update dev version changelog --- CHANGELOG.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72519d9..3228c10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,10 +3,14 @@ ## DEVELOPMENT VERSION *This version is not released yet.* -**General changes:** -* **BREAKING CHANGE:** The default value for [`inboxServerPermission`](docs/configuration.md#inboxserverpermission) is now `manageMessages` +**Breaking changes:** +* The default value for [`inboxServerPermission`](docs/configuration.md#inboxServerPermission) is now `manageMessages` * This means that after updating, if you haven't set `inboxServerPermission` in your `config.ini`, only those members with the "Manage Messages" permission on the inbox server will be able to use the bot's commands +* The default value for [`mentionRole`](docs/configuration.md#mentionRole) is now `none` + * To turn `@here` pings back on for new threads, set `mentionRole = here` in your `config.ini` + +**General changes:** * New option: `showResponseMessageInThreadChannel` * Controls whether the bot's response message is shown in the thread channel * Bot and Node.js version is now shown on bot start-up From dc3a1a05d3093ae37bb9b36877eccdddc98ce957 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 7 Jan 2021 20:15:25 +0200 Subject: [PATCH 276/300] Fix PluginApi.webserver being undefined --- src/plugins.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins.js b/src/plugins.js index 15d525a..2c32f16 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -3,7 +3,7 @@ const logs = require("./data/logs"); const { beforeNewThread } = require("./hooks/beforeNewThread"); const { afterThreadClose } = require("./hooks/afterThreadClose"); const formats = require("./formatters"); -const { server: webserver } = require("./modules/webserver"); +const webserver = require("./modules/webserver"); const childProcess = require("child_process"); const pacote = require("pacote"); const path = require("path"); From 4b1c092a7b6ae32931295d267e64db5b6992e3fb Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 7 Jan 2021 20:18:19 +0200 Subject: [PATCH 277/300] Allow setting status to 'none' to disable setting status automatically --- docs/configuration.md | 2 +- src/main.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index e3e7756..69ce082 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -367,7 +367,7 @@ Prefix to use snippets anonymously #### status **Default:** `Message me for help` -The bot's status text. Set to an empty value - `status = ""` - to disable. +The bot's status text. Set to `none` to disable. #### statusType **Default:** `playing` diff --git a/src/main.js b/src/main.js index b7fcbfd..d11c90d 100644 --- a/src/main.js +++ b/src/main.js @@ -99,7 +99,7 @@ function initStatus() { bot.editStatus(null, {name: config.status, type}); } - if (config.status == null || config.status === "") { + if (config.status == null || config.status === "" || config.status === "none" || config.status === "off") { return; } From 151b3e7fd8d0d74c8fbee3f7f6f87f671c4dcd90 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 7 Jan 2021 20:21:43 +0200 Subject: [PATCH 278/300] Add config.json.ini to auto-detected config files --- src/cfg.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cfg.js b/src/cfg.js index be33128..61ecb38 100644 --- a/src/cfg.js +++ b/src/cfg.js @@ -19,6 +19,7 @@ const configFilesToSearch = [ "config.ini.txt", "config.json.json", "config.json.txt", + "config.json.ini", ]; let configFileToLoad; From e2970394d04e0c49e52b0da2ac59ee2f14f2818d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 Jan 2021 20:23:01 +0200 Subject: [PATCH 279/300] Bump ini from 1.3.5 to 1.3.6 (#512) Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.6. - [Release notes](https://github.com/isaacs/ini/releases) - [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.6) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 191270e..fb80ec7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2459,9 +2459,9 @@ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.6.tgz", + "integrity": "sha512-IZUoxEjNjubzrmvzZU4lKP7OnYmX72XRl3sqkfJhBKweKi5rnGi5+IUdlj/H1M+Ip5JQ1WzaDMOBRY90Ajc5jg==" }, "interpret": { "version": "2.2.0", diff --git a/package.json b/package.json index ca15f7f..a200e69 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "express": "^4.17.1", "helmet": "^4.1.1", "humanize-duration": "^3.23.1", - "ini": "^1.3.5", + "ini": "^1.3.6", "json5": "^2.1.3", "knex": "^0.21.5", "knub-command-manager": "^6.1.0", From ff5707c40fbd6bc27a5689c0408e950fb2d64716 Mon Sep 17 00:00:00 2001 From: Lilly Rose Berner Date: Thu, 7 Jan 2021 19:24:26 +0100 Subject: [PATCH 280/300] Document silent close, allow silent to be passed like other options (#528) !loglink accepts options prefixed with a dash, which often leads to users using close the same way and it not working as expected. --- docs/commands.md | 3 +++ src/modules/close.js | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/commands.md b/docs/commands.md index 80266ff..6b7a2d2 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -28,6 +28,9 @@ Close the Modmail thread after a timer. Sending a message to the user or receivi **Example:** `!close 15m` +### `!close -s` / `!close -s