diff options
67 files changed, 9096 insertions, 0 deletions
diff --git a/.DS_Store b/.DS_Store Binary files differnew file mode 100644 index 0000000..f0c1918 --- /dev/null +++ b/.DS_Store diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5d12634 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +# editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..d33e1cf --- /dev/null +++ b/.eslintrc @@ -0,0 +1,24 @@ +{ + "root": true, + + "parserOptions": { + "ecmaVersion": 10, + "sourceType": "module" + }, + + "env": { + "node": true + }, + + "rules": { + "space-infix-ops": "error", + "space-unary-ops": ["error", { + "words": true, + "nonwords": false, + "overrides": { + "!": true, + "!!": true + } + }] + } +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6757ecf --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/.vscode +/.idea +/node_modules +/config.* +!/config.example.ini +/welcome.png +/update.sh @@ -0,0 +1 @@ +loglevel=silent @@ -0,0 +1 @@ +10 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f4f990b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,284 @@ +# Changelog + +## v2.30.1 +* Fix crash with `responseMessage` and `closeMessage` introduced in v2.30.0 + ([#369](https://github.com/Dragory/modmailbot/pull/369)) + +## v2.30.0 +* The following config options now also support multi-line values: + * `responseMessage` + * `closeMessage` + * `botMentionResponse` + * `greetingMessage` + * `accountAgeDeniedMessage` + * `timeOnServerDeniedMessage` +* When the bot is mentioned on the main server, the log message about this now + also includes a link to the message ([#319](https://github.com/Dragory/modmailbot/pull/319)) +* Fix error when supplying all config values from env variables without a config file +* Fix crash in update checker if the repository value in package.json is set to + a GitHub repository without releases (this only applies to forks) + +## v2.29.1 +* Fix boolean values in `config.ini` not being handled properly + +## v2.29.0 +* **Default configuration format is now .ini** + * Existing `config.json` files will continue to work and will not be deprecated + * This makes the default configuration format for the bot much more approachable than JSON +* Config values can now also be loaded from environment variables + (see [Configuration](docs/configuration.md#environment-variables) for more details) +* New rewritten instructions for setting up and using the bot +* New easy-to-use `start.bat` file for Windows +* Update several package dependencies +* Fixed incompatibility with Node.js 10 versions prior to 10.9.0 + +## v2.28.0 +* Fix error when saving attachments locally with `attachmentStorage` set to `"local"` (default) when the bot's folder is + on a different storage device than the system's temp folder +* Add `attachments` object to the plugin API + * This allows plugins to add new storage types via `attachments.addStorageType()` + * See the [Plugins section in the README](README.md#plugins) for more details + +## v2.27.0 +* The `syncPermissionsOnMove` option now defaults to `true`, which should be more intuitive +* **Plugins:** Plugin functions are no longer called with 4 arguments. Instead, the function is called with 1 argument, +which is an object that contains the previous 4 values as properties: `bot`, `knex`, `config`, `commands`. +This will make it easier to scale the plugin system in the future with new features. +You can see an [updated example in the README](https://github.com/Dragory/modmailbot/blob/master/README.md#example-plugin-file). + +## v2.26.0 +* The bot now waits for the main server(s) and inbox server to become available before initializing. +This is a potential fix to [#335](https://github.com/Dragory/modmailbot/issues/335). +This should have little to no effect on smaller servers. +* The bot status ("Playing") is now reapplied hourly since the status can sometimes disappear + +## v2.25.0 +* Fix regression introduced in v2.24.0 where line breaks would get turned to spaces in replies and snippets ([#304](https://github.com/Dragory/modmailbot/issues/304)) +* Replace the internal command handler with a new one. This should be fairly thoroughly tested, but please report any issues you encounter! +* Plugins are now called with a fourth parameter that allows you to easily add specific types of commands + * Due to the command handler change, any calls to `bot.registerCommand` should be replaced with the new system + +## v2.24.0 +* Switch to the new stable version of Eris (0.10.0) instead of the dev version + +## v2.23.2 +* Update Node.js version check at startup to require Node.js 10 + +## v2.23.1 +* Updated required Node.js version in .nvmrc and README (v10 is now the minimum) + +## v2.23.0 +* Add update notifications. The bot will check for new versions every 12 hours and notify moderators at the top of new +modmail threads when there are new versions available. Can be disabled by setting the `updateNotifications` option to `false`. +New available versions are also shown in `!version`. + * If you have forked the repository and want to check for updates in your own repository instead, + change the `repository` value in `package.json` +* Add basic support for plugins. See the **Plugins** section in README for more information. +* Add support for snippet arguments. To use these, put {1}, {2}, etc. in the snippet text and they will be replaced by the given arguments when using the snippet. +* Add support for multiple `mentionRole` config option values in an array +* Add `commandAliases` config option to set custom command aliases +* Add support for timed blocks. Simply specify the duration as the last argument in `!block` or `!unblock`. +* Add pagination to `!logs` + +## v2.22.0 +* Add `guildGreetings` option to allow configuring greeting messages on a per-server basis +* Add `rolesInThreadHeader` option to show the user's roles in the modmail thread's header + +## v2.21.3 +* Fix crash caused by Nitro Boosting notifications + +## v2.21.2 +* Update Eris to fix crashes with news channels and nitro boosting + +## v2.21.1 +* "Account age" and "time on server" requirements are now ignored when using `!newthread` + +## v2.21.0 +* Add `requiredTimeOnServer` and `timeOnServerDeniedMessage` config options to restrict modmail from users who have just joined the server. Thanks [@reboxer](https://github.com/reboxer) ([#270](https://github.com/Dragory/modmailbot/pull/270))! + +## v2.20.0 +* Add `categoryAutomation` option to automate thread categories. Currently supported sub-options: + * `newThread` - same as `newThreadCategoryId`, the default category for new threads + * `newThreadFromGuild` - default category on a per-guild basis, value is an object with guild IDs as keys and category IDs as values +* Threads should now include member information (nickname, joined at, etc.) more reliably +* Thread header now also includes the member's current voice channel, if any + +## v2.19.0 +* Add `attachmentStorage` option to control where attachments are saved. Currently supported: + * `"local"` (default) - Same as before: attachments are saved locally on the machine running the bot and served through the bot's web server + * `"discord"` - Attachments are saved on a special Discord channel specified by the `attachmentStorageChannelId` option +* Add `syncPermissionsOnMove` option. When enabled, thread channel permissions are synced with the category when the thread is moved with `!move`. +* Add support for scheduling `!suspend`. Works the same way as with `!close`, just specify the time after the command. Can be cancelled with `!suspend cancel`. +* Scheduled `!close` can now be silent - just add `silent` as an argument to the command before or after the schedule time +* The schedule time format for `!close` is now stricter and times with whitespace (e.g. `2 h 30 m`) no longer work. Use e.g. `2h30m` instead. +* `!loglink` can now be used in suspended threads +* User can now be mentioned in `botMentionResponse` by adding `{userMention}` to the response text. Thanks @reboxer (#225)! +* Fixed a small mistake in README, thanks @GabrielLewis2 (#226)! + +## v2.18.0 +* Add `silent` option to `!close` (e.g. `!close silent`) to close threads without sending the specified `closeMessage` +* Update some package versions (may help with sqlite3 install issues) + +## v2.17.0 +* Add `mentionUserInThreadHeader` option. When set to `true`, mentions the thread's user in the thread header. Fixes #152. +* Add `botMentionResponse` option. If set, the bot auto-responds to bot mentions with this message. Fixes #143. +* Fix member info sometimes missing in thread header. Thanks @Akhawais (#136)! +* Add support for role and user IDs in inboxServerPermission instead of just permission names +* Allow specifying multiple values (an array) for inboxServerPermission. Members will be considered "staff" if they pass any of the values. +* Update Eris to 0.9.0, Knex to 0.15.2 +* Add support for sending anonymous snippets. By default, you can do this by using `!!!` instead of `!!`. Fixes #82. +* Add `snippetPrefixAnon` option +* Add `allowUserClose` option. When set to `true`, users can use the close command to close threads by themselves. +* Fix `allowMove` missing from README. Thanks @AndreasGassmann (#126)! + +## v2.16.0 +* Add support for a .js config file (export config with `module.exports`) + +## v2.15.2 +* Update several other packages as well + +## v2.15.1 +* Update `node-sqlite3` dependency to hopefully fix installation issues on some setups + +## v2.15.0 +* Add `smallAttachmentLimit` config option to control the max size of attachments forwarded by `relaySmallAttachmentsAsAttachments` +* Fix crash when `closeMessage` failed to send +* Handle webserver errors gracefully + +## v2.14.1 +* Don't alert for main server pings if the pinger is a bot + +## v2.14.0 +* Changed `requiredAccountAge` to be in hours instead of days + +## v2.13.0 +* Added `requiredAccountAge` and `accountAgeDeniedMessage` options for restricting how new accounts can message modmail + +## v2.12.0 +* Added `closeMessage` option. This option can be used to send a message to the user when their modmail thread is closed. +* Documented `pingOnBotMention` option + +## v2.11.1 +* Fixed greetings not being sent since multi-server support was added in 2.9.0 + +## v2.11.0 +* Config files are now parsed using [JSON5](https://json5.org/), allowing you to use comments, trailing commas, and other neat things in your config.json +* When using multiple main guilds, the originating guild name is now always included at the top of the thread (if possible). +Previously, if the user that messaged modmail was on only one of the guilds, the guild name would not be shown at the top. +* Fixed crash when a user edited a message in their DMs with modmail without an open thread +* Small fixes to category name matching when using `!move` +* Fixed crash when the bot was unable to send an auto-response to a user +* Added option `pingOnBotMention` (defaults to `true`) that allows you to control whether staff are pinged when the bot is mentioned +* Long messages are now chunked so they don't fail to send due to added length from e.g. user name + +## v2.10.1 +* Changed timed close default unit from seconds to minutes. +This means that doing e.g. `!close 30` now closes the thread in 30 *minutes*, not seconds. + +## v2.10.0 +* Added `!alert` +Using `!alert` in a modmail thread will ping you the next time the thread gets a new reply. +Use `!alert cancel` to cancel. + +## v2.9.1 +* If using multiple main guilds, the originating server is now specified in bot mention notifications + +## v2.9.0 +* Added multi-server support. +Multi-server support allows you to set an array of ids in mainGuildId. +Nickname and join date will be displayed for each main guild the user is in. +* Information posted at the top of modmail threads now also includes time since the user joined the guild(s) +* Added `!id` +`!id` posts the user ID of the current thread. Useful on mobile when you need to get the user ID. +* Added `!newthread` +`!newthread <userid>` opens a new thread with the specified user +* Fixed a crash when the bot was unable to send a greeting message due to the user's privacy options + +## v2.8.0 +* Added a `!version` command for checking the version of the bot you're running + +## v2.7.0 +* Split more code from main.js to individual module files + +## v2.6.0 +* Warn the user if new dependencies haven't been installed +* `!close` now supports `d` for days in the delay +* `!close` is now stricter about the time format + +## v2.5.0 +* Commands used in inbox threads are now saved in logs again +* Moved more of the code to individual plugin files + +## v2.4.1-v2.4.4 +* Fix errors on first run after upgrading to v2.2.0 +* Various other fixes + +## v2.4.0 +* Add thread suspending. A modmail thread can now be suspended with `!suspend`. Suspended threads will function as closed until unsuspended with `!unsuspend`. + +## v2.3.2 +* Auto-close threads if their modmail channel is deleted + +## v2.3.1 +* Fixed incorrect default value for `mentionRole` (was `null`, should've been `"here"`) + +## v2.3.0 +* Added `mentionRole` configuration option ([#59](https://github.com/Dragory/modmailbot/pull/59)). This option can be used to set the role that is pinged when new threads are created or the bot is mentioned. See README for more details. + +## v2.2.0 +* Added the ability to schedule a thread to close by specifying a time after `!close`, e.g. `!close 1h`. The scheduling is cancelled if a new message is sent to or received from the user. + +## v2.1.0 +* Added typing proxy ([#48](https://github.com/Dragory/modmailbot/pull/48)): + * If the `typingProxy` config option is enabled, any time a user is typing to modmail in their DMs, the modmail thread will show the bot as "typing" + * If the `typingProxyReverse` config option is enabled, any time a moderator is typing in a modmail thread, the user will see the bot "typing" in their DMs + +## v2.0.1 +* The link to the current thread's log is no longer posted to the top of the thread. Use `!loglink` instead. + +## v2.0.0 +* Rewrote large parts of the code to be more modular and maintainable. There may be some new bugs because of this - please report them through GitHub issues if you encounter any! +* Threads, logs, and snippets are now stored in an SQLite database. The bot will migrate old data on the first run. +* Small attachments (<2MB) from users can now be relayed as Discord attachments in the modmail thread with the `relaySmallAttachmentsAsAttachments` config option. Logs will have the link as usual. +* Fixed system messages like pins in DMs being relayed to the thread +* Fixed channels sometimes being created without a category even when `newThreadCategoryId` was set +* Removed timestamps from threads by default. Logs will still have accurate timestamps. Can be re-enabled with the `threadTimestamps` config option. +* Added `!move` command to move threads between categories. Can be enabled with the `allowMove` config option, disabled by default. + +## Sep 22, 2017 +* Added `newThreadCategoryId` option. This option can be set to a category ID to place all new threads in that category. + +## Sep 20, 2017 +* Fixed crash when the bot was unable to find or create a modmail thread +* Reduced error log spam in case of network errors from Eris +* Fixed unintended error when a message was ignored due to an "accidental thread" word + +## Sep 19, 2017 +* Added `logChannelId` option +* Some code clean-up. Please open an issue if you encounter any bugs! +* The bot now throws an error for unknown options in `config.json` (assuming they're typos) and tells you if you haven't configured the token or mail guild id. + +## Aug 3, 2017 +* Fixed user nicknames not showing in new threads +* The "manageRoles" permission is no longer required to use commands on the inbox server. +This can be configured with the `inboxServerPermission` config option. + +## July 24, 2017 +* Commands are now case-insensitive (so !close, !Close, and !CLOSE all work) +* The before/after prefixes in edit notifications are now the same length, making it easier to spot the edited part +* Non-ASCII names should now result in better channel names (not just "unknown") + +## May 18, 2017 +* Identical edits are now ignored +* New thread notification (with @ here ping) is now posted in the thread instead of the inbox server default channel +* Thread close notifications no longer ping the user who closed the thread +* Received attachments are now only linked once the bot has saved them (should fix embeds) +* Replies now use your nickname, if any +* Several messages are now ignored for thread creation ("ok", "thanks", and similar) +* Logs from !logs are now sorted in descending order (newest first) + +## Feb 15, 2017 +More info is now available at the beginning of modmail threads. + +## Feb 10, 2017 +Major rewrite. Includes anonymous replies (!ar), stability improvements, and server greeting feature. diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..f682d0f --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Miikka Virtanen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4863c7f --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +# 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 +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. + +Inspired by Reddit's modmail system. + +## Getting started +* **[🛠️ Setting up the bot](docs/setup.md)** +* [📝 Configuration](docs/configuration.md) +* [🤖 Commands](docs/commands.md) +* [🧩 Plugins](docs/plugins.md) +* [🙋 Frequently Asked Questions](docs/faq.md) +* [Release notes](CHANGELOG.md) + +## 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: + +👉 **[Join support server](https://discord.gg/vRuhG9R)** diff --git a/attachments/.gitignore b/attachments/.gitignore new file mode 100644 index 0000000..120f485 --- /dev/null +++ b/attachments/.gitignore @@ -0,0 +1,2 @@ +* +!/.gitignore diff --git a/db/.gitignore.example b/db/.gitignore.example new file mode 100644 index 0000000..08f41a1 --- /dev/null +++ b/db/.gitignore.example @@ -0,0 +1,3 @@ +/* +!/.gitignore +!/migrations/ diff --git a/db/data.sqlite b/db/data.sqlite Binary files differnew file mode 100644 index 0000000..967e6c5 --- /dev/null +++ b/db/data.sqlite diff --git a/db/migrations/20171223203915_create_tables.js b/db/migrations/20171223203915_create_tables.js new file mode 100644 index 0000000..b5880a3 --- /dev/null +++ b/db/migrations/20171223203915_create_tables.js @@ -0,0 +1,45 @@ +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(); + }); + + 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(); + }); + + 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(); + }); + + 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(); + }); +}; + +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'); +}; diff --git a/db/migrations/20180224235946_add_close_at_to_threads.js b/db/migrations/20180224235946_add_close_at_to_threads.js new file mode 100644 index 0000000..dbbb04d --- /dev/null +++ b/db/migrations/20180224235946_add_close_at_to_threads.js @@ -0,0 +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'); + }); +}; + +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'); + }); +}; diff --git a/db/migrations/20180421161550_add_alert_id_to_threads.js b/db/migrations/20180421161550_add_alert_id_to_threads.js new file mode 100644 index 0000000..5defc38 --- /dev/null +++ b/db/migrations/20180421161550_add_alert_id_to_threads.js @@ -0,0 +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'); + }); +}; + +exports.down = async function(knex, Promise) { + 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 new file mode 100644 index 0000000..ac33267 --- /dev/null +++ b/db/migrations/20180920224224_remove_is_anonymous_from_snippets.js @@ -0,0 +1,11 @@ +exports.up = async function (knex, Promise) { + 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(); + }); +}; diff --git a/db/migrations/20190306204728_add_scheduled_close_silent_to_threads.js b/db/migrations/20190306204728_add_scheduled_close_silent_to_threads.js new file mode 100644 index 0000000..e61490d --- /dev/null +++ b/db/migrations/20190306204728_add_scheduled_close_silent_to_threads.js @@ -0,0 +1,11 @@ +exports.up = async function(knex, Promise) { + 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'); + }); +}; diff --git a/db/migrations/20190306211534_add_scheduled_suspend_to_threads.js b/db/migrations/20190306211534_add_scheduled_suspend_to_threads.js new file mode 100644 index 0000000..a22b5f3 --- /dev/null +++ b/db/migrations/20190306211534_add_scheduled_suspend_to_threads.js @@ -0,0 +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'); + }); +}; + +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'); + }); +}; diff --git a/db/migrations/20190609161116_create_updates_table.js b/db/migrations/20190609161116_create_updates_table.js new file mode 100644 index 0000000..c90e9bd --- /dev/null +++ b/db/migrations/20190609161116_create_updates_table.js @@ -0,0 +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(); + }); + } +}; + +exports.down = async function(knex, Promise) { + 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 new file mode 100644 index 0000000..ea456a6 --- /dev/null +++ b/db/migrations/20190609193213_add_expires_at_to_blocked_users.js @@ -0,0 +1,11 @@ +exports.up = async function(knex, Promise) { + 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'); + }); +}; diff --git a/docs/commands.md b/docs/commands.md new file mode 100644 index 0000000..397cf16 --- /dev/null +++ b/docs/commands.md @@ -0,0 +1,31 @@ +# 🤖 Commands + +## Anywhere on the inbox server +`!logs <user> <page>` Lists previous modmail logs with the specified user. If there are a lot of logs, they will be paginated. In this case, you can specify the page number to view as the second argument. +`!block <user> <time>` Blocks the specified user from using modmail. If a time is specified, the block is temporary. +`!unblock <user> <time>` Unblocks the specified user from using modmail. If a time is specified, the user will be scheduled to be unblocked after that time. +`!is_blocked <user>` Checks whether the user is blocked and for how long +`!s <shortcut> <text>` Adds a snippet (a canned response). Supports {1}, {2}, etc. for arguments. See below for how to use it. +`!edit_snippet <shortcut> <text>` Edits an existing snippet (alias `!es`) +`!delete_snippet <shortcut>` Deletes the specified snippet (alias `!ds`) +`!snippets` Lists all available snippets +`!version` Print the version of the bot you're running +`!newthread <user>` Opens a new thread with the specified user + +## Inside a modmail thread +`!reply <text>` Sends a reply to the user in the format "(Role) User: text" (alias `!r`) +`!anonreply <text>` Sends an anonymous reply to the user in the format "Role: text" (alias `!ar`) +`!close <time>` Closes the modmail thread. If a time is specified, the thread is scheduled to be closed later. Scheduled closing is cancelled if a message is sent to or received from the user. +`!logs <page>` Lists previous modmail logs with this user. If there are a lot of logs, they will be paginated. In this case, you can specify the page number to view as an argument. +`!block <time>` Blocks the user from using modmail. If a time is specified, the block is temporary. +`!unblock <time>` Unblocks the user from using modmail. If a time is specified, the user will be scheduled to be unblocked after that time. +`!!shortcut` Reply with a snippet. Replace `shortcut` with the snippet's actual shortcut. +`!!!shortcut` Reply with a snippet anonymously. Replace `shortcut` with the snippet's actual shortcut. +`!move <category>` If `allowMove` is enabled, moves the thread channel to the specified category +`!loglink` Shows the link to the current thread's log +`!suspend` Suspend a thread. The thread will act as closed and not receive any messages until unsuspended. +`!unsuspend` Unsuspend a thread +`!id` Prints the user's ID +`!alert` Pings you when the thread gets a new reply. Use `!alert cancel` to cancel. + +To automatically reply without using !reply or !r, [enable `alwaysReply` in the config](configuration.md). diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..cb4f2bb --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,342 @@ +# 📝 Configuration +Haven't set up the bot yet? Check out [Setting up the bot](setup.md) first! + +## Table of contents +- [Configuration file](#configuration-file) (start here) +- [Adding new options](#adding-new-options) +- [Required options](#required-options) +- [Other options](#other-options) +- [config.ini vs config.json](#configini-vs-configjson) +- [Other formats](#other-formats) +- [Environment variables](#environment-variables) + +## Configuration file +All bot options are saved in a configuration file in the bot's folder. +This is created during the [setup](setup.md) and is generally either `config.ini` or, if you've been using the bot for +longer, `config.json`. + +The instructions on this page are for `config.ini` but can be adapted to `config.json` as well. +See [config.ini vs config.json](#configini-vs-configjson) for more details. +Note that the format of `.ini` and `.json` are different -- you can't simply rename a `.json` to `.ini` or +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`. + +**You need to restart the bot for configuration changes to take effect!** + +You can add comments in the config file by prefixing the line with `#`. Example: +```ini +# This is a comment +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` +* E.g. `allowMove = on` or `allowMove = off` + +### "Accepts multiple values" +Some options are marked as "**Accepts multiple values**". To give these options multiple values, +write the option as `option[] = value` and repeat for every value. For example: + +```ini +inboxServerPermission[] = kickMembers +inboxServerPermission[] = manageMessages +``` + +You can also give these options a single value in the usual way, i.e. `inboxServerPermission = kickMembers` + +### Multiple lines of text +For some options, such as `greetingMessage`, you might want to add text that spans multiple lines. +To do that, use the same format as with "Accepts multiple values" above: + +```ini +greetingMessage[] = Welcome to the server! +greetingMessage[] = This is the second line of the greeting. +greetingMessage[] = +greetingMessage[] = Fourth line! With an empty line in the middle. +``` + +## Required options + +#### token +The bot user's token from [Discord Developer Portal](https://discordapp.com/developers/). + +#### mainGuildId +Your server's ID, wrapped in quotes. + +#### mailGuildId +For a two-server setup, the inbox server's ID. +For a single-server setup, same as [mainGuildId](#mainguildid). + +#### logChannelId +ID of a channel on the inbox server where logs are posted after a modmail thread is closed + +## Other options + +#### accountAgeDeniedMessage +**Default:** `Your Discord account is not old enough to contact modmail.` +See `requiredAccountAge` below + +#### allowMove +**Default:** `off` +If enabled, allows you to move threads between categories using `!move <category>` + +#### allowUserClose +**Default:** `off` +If enabled, users can use the close command to close threads by themselves from their DMs with the bot + +#### alwaysReply +**Default:** `off` +If enabled, all messages in modmail threads will be sent to the user without having to use `!r`. +To send internal messages in the thread when this option is enabled, prefix them with `!note` (using your `prefix`), +e.g. `!note This is an internal message`. + +#### alwaysReplyAnon +**Default:** `off` +If `alwaysReply` is enabled, this option controls whether the auto-reply is anonymous + +#### attachmentStorage +**Default:** `local` +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. + +#### attachmentStorageChannelId +**Default:** *None* +When using attachmentStorage is set to "discord", the id of the channel on the inbox server where attachments are saved + +#### 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. + +#### categoryAutomation.newThread +**Default:** *None* +ID of the category where new threads are opened. Also functions as a fallback for `categoryAutomation.newThreadFromGuild`. + +#### categoryAutomation.newThreadFromGuild.GUILDID +**Default:** *None* +When running the bot on multiple main servers, this allows you to specify new thread categories for users from each guild. 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 +# When the user is from the server ID 541484311354933258, their modmail thread will be placed in the category ID 542780020972716042 +categoryAutomation.newThreadFromGuild.541484311354933258 = 542780020972716042 +``` + +#### closeMessage +**Default:** *None* +If set, the bot sends this message to the user when the modmail thread is closed. + +#### commandAliases +**Default:** *None* +Custom aliases/shortcuts for commands. Example: +```ini +# !mv is an alias/shortcut for !move +commandAliases.mv = move +# !x is an alias/shortcut for !close +commandAliases.x = close +``` + +#### enableGreeting +**Default:** `off` +When enabled, the bot will send a greeting DM to users that join the main server. + +#### greetingAttachment +**Default:** *None* +Path to an image or other attachment to send as a greeting. Requires `enableGreeting` to be enabled. + +#### greetingMessage +**Default:** *None* +Message to send as a greeting. Requires `enableGreeting` to be enabled. Example: +```ini +greetingMessage[] = Welcome to the server! +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. +``` + +#### ignoreAccidentalThreads +**Default:** `off` +If enabled, the bot attempts to ignore common "accidental" messages that would start a new thread, such as "ok", "thanks", etc. + +#### inboxServerPermission +**Default:** *None* +**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`). + +#### 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. + +#### 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. +Requires `pingOnBotMention` to be enabled. +Set to an empty value (`mentionRole=`) to disable these pings entirely. + +#### mentionUserInThreadHeader +**Default:** `off` +If enabled, mentions the user messaging modmail in the modmail thread's header. + +#### newThreadCategoryId +**Default:** *None* +**Deprecated.** Same as `categoryAutomation.newThread`. + +#### 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. + +#### plugins +**Default:** *None* +**Accepts multiple values.** External plugins to load on startup. See [Plugins](plugins.md) for more information. + +#### port +**Default:** `8890` +Port to use for attachments (when `attachmentStorage` is set to `local`) and logs. +Make sure to do the necessary [port forwarding](https://portforward.com/) and add any needed firewall exceptions so the port is accessible from the internet. + +#### prefix +**Default:** `!` +Prefix for bot commands + +#### relaySmallAttachmentsAsAttachments +**Default:** `off` +If enabled, small attachments from users are sent as real attachments rather than links in modmail threads. +The limit for "small" is 2MB by default; you can change this with the `smallAttachmentLimit` option. + +#### requiredAccountAge +**Default:** *None* +Required account age for contacting modmail (in hours). If the account is not old enough, a new thread will not be created and the bot will reply with `accountAgeDeniedMessage` (if set) instead. + +#### requiredTimeOnServer +**Default:** *None* +Required amount of time (in minutes) the user must be a member of the server before being able to contact modmail. If the user hasn't been a member of the server for the specified time, a new thread will not be created and the bot will reply with `timeOnServerDeniedMessage` (if set) instead. + +#### 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 + +#### rolesInThreadHeader +**Default:** `off` +If enabled, the user's roles will be shown in the modmail thread header + +#### smallAttachmentLimit +**Default:** `2097152` +Size limit of `relaySmallAttachmentsAsAttachments` in bytes (default is 2MB) + +#### snippetPrefix +**Default:** `!!` +Prefix for snippets + +#### snippetPrefixAnon +**Default:** `!!!` +Prefix to use snippets anonymously + +#### status +**Default:** `Message me for help` +The bot's "Playing" text + +#### syncPermissionsOnMove +**Default:** `on` +If enabled, channel permissions for the thread are synchronized with the category when using `!move`. Requires `allowMove` to be enabled. + +#### threadTimestamps +**Default:** `off` +If enabled, modmail threads will show accurate UTC timestamps for each message, in addition to Discord's own timestamps. +Logs show these always, regardless of this setting. + +#### typingProxy +**Default:** `off` +If enabled, any time a user is typing to modmail in their DMs, the modmail thread will show the bot as "typing" + +#### typingProxyReverse +**Default:** `off` +If enabled, any time a moderator is typing in a modmail thread, the user will see the bot "typing" in their DMs + +#### updateNotifications +**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 + +#### url +**Default:** *None* +URL to use for attachment and log links. Defaults to `http://IP:PORT`. + +#### useNicknames +**Default:** `off` +If enabled, mod replies will use their nicknames (on the inbox server) instead of their usernames + +## config.ini vs config.json +Earlier versions of the bot instructed you to create a `config.json` instead of a `config.ini`. +**This is still fully supported, and will be in the future as well.** +However, there are some differences between `config.ini` and `config.json`. + +### Formatting +*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 numbers (other than IDs) are written without quotes, e.g. `"port": 3000` + +### Toggle options +In `config.json`, valid values for toggle options are `true` and `false` (not quoted), +which correspond to `on` and `off` in `config.ini`. + +### "Accepts multiple values" +Multiple values are specified in `config.json` using arrays: +```json +{ + "inboxPermission": [ + "kickMembers", + "manageMessages" + ] +} +``` + +### Multiple lines of text +Since `config.json` is parsed using [JSON5](https://json5.org/), multiple lines of text are supported +by escaping the newline with a backslash (`\ `): +```json5 +{ + "greetingMessage": "Welcome to the server!\ +This is the second line of the greeting." +} +``` + +## Other formats +Loading config values programmatically is also supported. +Create a `config.js` in the bot's folder and export the config object with `module.exports`. +All other configuration files take precedence, so make sure you don't have both. + +## Environment variables +Config options can be passed via environment variables. + +To get the name of the corresponding environment variable for an option, convert the option to SNAKE_CASE with periods +being replaced by two underscores and add `MM_` as a prefix. If adding multiple values for the same option, separate the +values with two pipe characters: `||`. + +Examples: +* `mainGuildId` -> `MM_MAIN_GUILD_ID` +* `commandAliases.mv` -> `MM_COMMAND_ALIASES__MV` +* From: + ```ini + inboxServerPermission[] = kickMembers + inboxServerPermission[] = manageMessages + ``` + To: + `MM_INBOX_SERVER_PERMISSION=kickMembers||manageMessages` + +The `port` option also accepts the environment variable `PORT` without a prefix, but `MM_PORT` takes precedence. diff --git a/docs/faq.md b/docs/faq.md new file mode 100644 index 0000000..29b0370 --- /dev/null +++ b/docs/faq.md @@ -0,0 +1,21 @@ +# 🙋 Frequently Asked Questions + +## 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 +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` diff --git a/docs/plugins.md b/docs/plugins.md new file mode 100644 index 0000000..2a08558 --- /dev/null +++ b/docs/plugins.md @@ -0,0 +1,47 @@ +# 🧩 Plugins +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. + +## 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 + +See [src/plugins.js#L4](../src/plugins.js#L4) for more details + +### Example plugin file +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 }) { + commands.addInboxThreadCommand('mycommand', [], (msg, args, thread) => { + thread.replyToUser(msg.author, 'Reply from my custom plugin!'); + }); +} +``` + +(Note the use of [object destructuring](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Unpacking_fields_from_objects_passed_as_function_parameter) in the function parameters) + +### Example of a custom attachment storage type +This example adds a custom type for the `attachmentStorage` option called `"original"` that simply returns the original attachment URL without rehosting it in any way. +```js +module.exports = function({ attachments }) { + attachments.addStorageType('original', attachment => { + return { url: attachment.url }; + }); +}; +``` +To use this custom attachment storage type, you would set the `attachmentStorage` config option to `"original"`. + +## 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. + +Please send any feature suggestions to the [issue tracker](https://github.com/Dragory/modmailbot/issues)! diff --git a/docs/setup.md b/docs/setup.md new file mode 100644 index 0000000..ab724f0 --- /dev/null +++ b/docs/setup.md @@ -0,0 +1,50 @@ +# 🛠️ Setting up the bot +**Note:** This bot is run on your own machine or a server. +To keep it online, you need to keep the bot process running. + +## Terminology +* **Main server** (or main guild) is the server where users will be contacting modmail from +* **Inbox server** (or inbox guild, or mail guild) is the server where modmail threads will be created. + In a "single-server setup" this is the same server as the main server. +* A **modmail thread** is a channel on the **inbox server** that contains the current exchange with the **user**. + These threads can be closed to archive them. One **user** can only have 1 modmail thread open at once. +* A **moderator**, in modmail's context, is a server staff member who responds to and manages modmail threads +* 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. 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` + +## Single-server setup +In this setup, modmail threads are opened on the main server in a special category. +This is the recommended setup for small and medium sized servers. + +1. 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. 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 +3. Make sure the bot has `Manage Channels`, `Manage Messages`, and `Attach Files` permissions in the category +4. **[🏃 Start the bot!](starting-the-bot.md)** +5. Want to change other bot options? See **[📝 Configuration](configuration.md)** +6. Have any other questions? Check out the **[🙋 Frequently Asked Questions](faq.md)** or + **[join the support server!](../README.md#support-server)** + +## Two-server setup +In this setup, modmail threads are opened on a separate inbox server. +This is the recommended setup for large servers that get a lot of modmails, where a single-server setup could get messy. +You might also want this setup for privacy concerns*. + +1. Create an inbox server on Discord +2. Invite the bot to the inbox server. +3. Open `config.ini` in a text editor and fill in the values +4. Make sure the bot has the `Manage Channels`, `Manage Messages`, and `Attach Files` permissions on the **inbox** server +5. **[🏃 Start the bot!](starting-the-bot.md)** +5. Want to change other bot options? See **[📝 Configuration](configuration.md)** +6. 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 +modified client could see the names of all users contacting modmail through the modmail channel names.* diff --git a/docs/starting-the-bot.md b/docs/starting-the-bot.md new file mode 100644 index 0000000..624c8f2 --- /dev/null +++ b/docs/starting-the-bot.md @@ -0,0 +1,16 @@ +# 🏃 Starting the bot +Haven't set up the bot yet? Check out [Setting up the bot](setup.md) first! + +## Windows +* To start the bot, double-click on `start.bat` in the bot's folder +* To shut down the bot, close the console window +* To restart the bot, close the console window and then double-click on `start.bat` again + +## Linux / macOS / Advanced on Windows +The following assumes basic knowledge about using command line tools. +1. Before first start-up and after every update, run `npm ci` in the bot's folder +2. Run `npm start` in the bot's folder to start the bot + +## Process managers +If you're using a process manager like PM2, the command to run is `npm start`. +A PM2 process file, `modmailbot-pm2.json`, is included in the repository. diff --git a/knexfile.js b/knexfile.js new file mode 100644 index 0000000..cf568f4 --- /dev/null +++ b/knexfile.js @@ -0,0 +1,2 @@ +const config = require('./src/config'); +module.exports = config.knex; diff --git a/logs/.gitignore b/logs/.gitignore new file mode 100644 index 0000000..120f485 --- /dev/null +++ b/logs/.gitignore @@ -0,0 +1,2 @@ +* +!/.gitignore diff --git a/modmailbot-pm2.json b/modmailbot-pm2.json new file mode 100644 index 0000000..6322645 --- /dev/null +++ b/modmailbot-pm2.json @@ -0,0 +1,8 @@ +{ + "apps": [{ + "name": "ModMailBot", + "cwd": "./", + "script": "npm", + "args": "start" + }] +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..baca952 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4180 @@ +{ + "name": "modmailbot", + "version": "2.29.1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/code-frame": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", + "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==", + "dev": true, + "requires": { + "@babel/highlight": "^7.0.0" + } + }, + "@babel/highlight": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.5.0.tgz", + "integrity": "sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@sindresorhus/is": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", + "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==" + }, + "@szmarczak/http-timer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", + "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", + "requires": { + "defer-to-connect": "^1.0.1" + } + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "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==", + "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==", + "dev": true + }, + "ajv": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", + "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", + "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-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-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", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "ansi-styles": { + "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==", + "requires": { + "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", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + }, + "are-we-there-yet": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" + }, + "array-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", + "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=" + }, + "array-slice": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", + "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==" + }, + "array-unique": { + "version": "0.3.2", + "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==", + "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=" + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" + }, + "astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "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", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "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=" + }, + "aws4": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.0.tgz", + "integrity": "sha512-Uvq6hVe90D0B2WEnUqtdgY1bATGz3mw33nH9Y+dmA+w5DHvUmBgkr5rM/KCHpCsiFNRUfokW/szpPPgMK2hm4A==" + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "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=" + } + } + }, + "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 + }, + "bluebird": { + "version": "3.7.2", + "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", + "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + } + }, + "cacheable-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", + "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^3.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^4.1.0", + "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", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" + } + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "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", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "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 + }, + "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.3", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.3.tgz", + "integrity": "sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw==" + }, + "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", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "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", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "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", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.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==" + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + }, + "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=" + }, + "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==", + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.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==", + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "requires": { + "mimic-response": "^1.0.0" + } + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "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==" + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.0.1.tgz", + "integrity": "sha512-IPF4ouhCP+qdlcmCedhxX4xiGBPyigb8v5NeUp+0LyhwLgxMqyp3S0vl7TAPfS/hiP7FC3caI/PB9lTmP8r1NA==" + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + }, + "concat-map": { + "version": "0.0.1", + "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", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "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", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" + }, + "decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", + "requires": { + "mimic-response": "^1.0.0" + } + }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "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==" + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + }, + "detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=" + }, + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" + }, + "dns-packet": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.2.1.tgz", + "integrity": "sha512-JHj2yJeKOqlxzeuYpN1d56GfhzivAxavNwHj9co3qptECel27B1rLY5PifJAvubsInX5pGLDjAHuCfCUc2Zv/w==", + "requires": { + "ip": "^1.1.5" + } + }, + "dns-socket": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dns-socket/-/dns-socket-4.2.0.tgz", + "integrity": "sha512-4XuD3z28jht3jvHbiom6fAipgG5LkjYeDLrX5OH8cbl0AtzTyUUAxGckcW8T7z0pLfBBV5qOiuC4wUEohk6FrQ==", + "requires": { + "dns-packet": "^5.1.2" + } + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "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", + "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=", + "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", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "requires": { + "once": "^1.4.0" + } + }, + "eris": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/eris/-/eris-0.11.1.tgz", + "integrity": "sha512-Ct32iXjESOnmklxZCEA281BQsTlAsS9xzQkbGlnvzXshCjBptWJ5h8Oxbu67ui1DirsYs0WipB8EBC9ITQ5ZQA==", + "requires": { + "opusscript": "^0.0.4", + "tweetnacl": "^1.0.0", + "ws": "^7.1.2" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "eslint": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.7.2.tgz", + "integrity": "sha512-qMlSWJaCSxDFr8fBPvJM9kJwbazrhNcBU3+DszDW1OlEwKBBRWsJc7NJFelvwQpanHCR14cOLD41x8Eqvo3Nng==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.10.0", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "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", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "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", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.3", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^6.1.2", + "strip-ansi": "^5.2.0", + "strip-json-comments": "^3.0.1", + "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==", + "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==", + "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 + } + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "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==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.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==", + "dev": true + } + } + }, + "eslint-scope": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", + "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "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==", + "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==", + "dev": true + }, + "espree": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.1.2.tgz", + "integrity": "sha512-2iUPuuPP+yW1PZaMSDM9eyVf8D5P0Hi8h83YtZ5bPc/zHYjII5khoixIUTMO794NOY8F/ThF1Bo8ncZILarUTA==", + "dev": true, + "requires": { + "acorn": "^7.1.0", + "acorn-jsx": "^5.1.0", + "eslint-visitor-keys": "^1.1.0" + } + }, + "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 + }, + "esquery": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", + "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", + "dev": true, + "requires": { + "estraverse": "^4.0.0" + } + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "requires": { + "estraverse": "^4.1.0" + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "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", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "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" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", + "requires": { + "homedir-polyfill": "^1.0.1" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "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", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "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=" + }, + "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=" + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "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", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "dev": true, + "requires": { + "flat-cache": "^2.0.1" + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "requires": { + "locate-path": "^3.0.0" + } + }, + "findup-sync": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", + "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==", + "requires": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.0", + "micromatch": "^3.0.4", + "resolve-dir": "^1.0.1" + } + }, + "fined": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz", + "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==", + "requires": { + "expand-tilde": "^2.0.2", + "is-plain-object": "^2.0.3", + "object.defaults": "^1.1.0", + "object.pick": "^1.2.0", + "parse-filepath": "^1.0.1" + } + }, + "flagged-respawn": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", + "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==" + }, + "flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, + "requires": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + } + }, + "flatted": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.1.tgz", + "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==", + "dev": true + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" + }, + "for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", + "requires": { + "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=" + }, + "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==", + "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", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "requires": { + "map-cache": "^0.2.2" + } + }, + "fs-minipass": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", + "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", + "requires": { + "minipass": "^2.6.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "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 + }, + "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", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + }, + "dependencies": { + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + } + } + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "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", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" + }, + "getopts": { + "version": "2.2.5", + "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=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz", + "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==", + "dev": true, + "requires": { + "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", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "requires": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + } + }, + "global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", + "requires": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + } + }, + "globals": { + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.3.0.tgz", + "integrity": "sha512-wAfjdLgFsPZsklLJvOBUBmzYE8/CwhEqSBEMRXA3qxIiNtyqvjYurAtIfDh6chlEPUfmTY3MnZh5Hfh4q0UlIw==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, + "got": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", + "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", + "requires": { + "@sindresorhus/is": "^0.14.0", + "@szmarczak/http-timer": "^1.1.2", + "cacheable-request": "^6.0.0", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^4.1.0", + "lowercase-keys": "^1.0.1", + "mimic-response": "^1.0.1", + "p-cancelable": "^1.0.0", + "to-readable-stream": "^1.0.0", + "url-parse-lax": "^3.0.0" + }, + "dependencies": { + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "requires": { + "pump": "^3.0.0" + } + } + } + }, + "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 + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "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==", + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "requires": { + "parse-passwd": "^1.0.0" + } + }, + "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==" + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "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", + "integrity": "sha512-Eu68Xnq5C38391em1zfVy8tiapQrOvTNTlWpax9smHMlEEUcudXrdMfXMoMRyZx4uODowYgi1AYiMzUXEbG+sA==" + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "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", + "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", + "requires": { + "minimatch": "^3.0.4" + } + }, + "import-fresh": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", + "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "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", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "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==" + }, + "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", + "integrity": "sha512-e0/LknJ8wpMMhTiWcjivB+ESwIuvHnBSlBbmP/pSb8CQJldoj1p2qv7xGZ/+BtbTziYRFSz8OsvdbiX45LtYQA==" + }, + "ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" + }, + "ip-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.1.0.tgz", + "integrity": "sha512-pKnZpbgCTfH/1NLIlOduP/V+WRXzC2MOz3Qo8xmxk8C5GudJLgK5QyLVXOSWy3ParAH7Eemurl3xjv/WXYFvMA==" + }, + "is-absolute": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", + "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", + "requires": { + "is-relative": "^1.0.0", + "is-windows": "^1.0.1" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "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", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + } + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "requires": { + "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", + "integrity": "sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==", + "requires": { + "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", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "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-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", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "requires": { + "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-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-relative": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", + "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "requires": { + "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", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "is-unc-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", + "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", + "requires": { + "unc-path-regex": "^0.1.2" + } + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "isobject": { + "version": "3.0.1", + "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=" + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, + "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=" + }, + "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==" + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "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=" + }, + "json5": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.1.tgz", + "integrity": "sha512-l+3HXD0GEI3huGq1njuqtzYK8OYJyXMkOLtQ53pjWh89tvWS2h6l+1zMkYWqlb57+SiQodKZyvMEFb2X+KrFhQ==", + "requires": { + "minimist": "^1.2.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + } + } + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "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", + "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", + "requires": { + "json-buffer": "3.0.0" + } + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + }, + "knex": { + "version": "0.20.3", + "resolved": "https://registry.npmjs.org/knex/-/knex-0.20.3.tgz", + "integrity": "sha512-zzYO34pSCCYVqRTbCp8xL+Z7fvHQl5anif3Oacu6JaHFDubB7mFGWRRJBNSO3N8Ql4g4CxUgBctaPiliwoOsNA==", + "requires": { + "bluebird": "^3.7.1", + "colorette": "1.1.0", + "commander": "^4.0.1", + "debug": "4.1.1", + "getopts": "2.2.5", + "inherits": "~2.0.4", + "interpret": "^2.0.0", + "liftoff": "3.1.0", + "lodash": "^4.17.15", + "mkdirp": "^0.5.1", + "pg-connection-string": "2.1.0", + "tarn": "^2.0.0", + "tildify": "2.0.0", + "uuid": "^3.3.3", + "v8flags": "^3.1.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.4", + "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==" + }, + "uuid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", + "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==" + } + } + }, + "knub-command-manager": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/knub-command-manager/-/knub-command-manager-6.1.0.tgz", + "integrity": "sha512-Bn//fk3ZKUNoJ+p0fNdUfbcyzTUdWHGaP12irSy8U1lfxy3pBrOZZsc0tpIkBFLpwLWw/VxHInX1x2b6MFhn0Q==", + "requires": { + "escape-string-regexp": "^2.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==" + } + } + }, + "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", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "liftoff": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz", + "integrity": "sha512-DlIPlJUkCV0Ips2zf2pJP0unEoT1kwYhiiPUGF3s/jtxTCjziNLoiVVh+jqWOWeFi6mmwQ5fNxvAUyPad4Dfog==", + "requires": { + "extend": "^3.0.0", + "findup-sync": "^3.0.0", + "fined": "^1.0.1", + "flagged-respawn": "^1.0.0", + "is-plain-object": "^2.0.4", + "object.map": "^1.0.0", + "rechoir": "^0.6.2", + "resolve": "^1.1.7" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, + "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": "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", + "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", + "requires": { + "kind-of": "^6.0.2" + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=" + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "requires": { + "object-visit": "^1.0.0" + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "mime": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", + "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==" + }, + "mime-db": { + "version": "1.42.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.42.0.tgz", + "integrity": "sha512-UbfJCR4UAVRNgMpfImz05smAXK7+c+ZntjaA26ANtkXLlOe947Aag5zdIcKQULAiF9Cq4WxBi9jUs5zkA84bYQ==" + }, + "mime-types": { + "version": "2.1.25", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.25.tgz", + "integrity": "sha512-5KhStqB5xpTAeGqKBAMgwaYMnQik7teQN4IAzC7npDv6kzeU6prfkR67bc87J1kWMPGkoaZSq1npmexMgkmEVg==", + "requires": { + "mime-db": "1.42.0" + } + }, + "mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + }, + "minipass": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", + "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "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": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", + "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", + "requires": { + "minipass": "^2.9.0" + } + }, + "mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + } + }, + "moment": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" + }, + "ms": { + "version": "2.1.2", + "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", + "integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=", + "requires": { + "mkdirp": "~0.5.1", + "ncp": "~2.0.0", + "rimraf": "~2.4.0" + }, + "dependencies": { + "glob": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "rimraf": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", + "integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=", + "requires": { + "glob": "^6.0.1" + } + } + } + }, + "nan": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" + }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + } + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=" + }, + "needle": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.4.0.tgz", + "integrity": "sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg==", + "requires": { + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "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-pre-gyp": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz", + "integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==", + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + } + }, + "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": "4.0.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "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", + "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==" + }, + "npm-bundled": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.6.tgz", + "integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==" + }, + "npm-packlist": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.6.tgz", + "integrity": "sha512-u65uQdb+qwtGvEJh/DgQgW1Xg7sqeNbmxYyrvlNznaVTjV3E5P6F/EFjM+BVHXl7JJlsdG8A64M0XI8FI/IOlg==", + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^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", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "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==" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "requires": { + "isobject": "^3.0.0" + } + }, + "object.defaults": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", + "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=", + "requires": { + "array-each": "^1.0.1", + "array-slice": "^1.0.0", + "for-own": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "object.map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", + "integrity": "sha1-z4Plncj8wK1fQlDh94s7gb2AHTc=", + "requires": { + "for-own": "^1.0.0", + "make-iterator": "^1.0.0" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "requires": { + "isobject": "^3.0.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "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" + }, + "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 + } + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "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" + } + }, + "opusscript": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/opusscript/-/opusscript-0.0.4.tgz", + "integrity": "sha512-bEPZFE2lhUJYQD5yfTFO4RhbRZ937x6hRwBC1YoGacT35bwDVwKFP1+amU8NYfZL/v4EU7ZTU3INTqzYAnuP7Q==", + "optional": true + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" + }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "p-cancelable": { + "version": "1.1.0", + "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", + "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==", + "requires": { + "p-try": "^2.0.0" + } + }, + "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==", + "requires": { + "p-limit": "^2.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==" + }, + "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", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-filepath": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", + "integrity": "sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=", + "requires": { + "is-absolute": "^1.0.0", + "map-cache": "^0.2.0", + "path-root": "^0.1.1" + } + }, + "parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=" + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "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=" + }, + "path-is-absolute": { + "version": "1.0.1", + "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", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + }, + "path-root": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", + "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=", + "requires": { + "path-root-regex": "^0.1.0" + } + }, + "path-root-regex": { + "version": "0.1.2", + "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=" + }, + "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==" + }, + "picomatch": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.1.1.tgz", + "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 + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "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=", + "dev": true + }, + "prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=" + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "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.5.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.5.0.tgz", + "integrity": "sha512-4vqUjKi2huMu1OJiLhi3jN6jeeKvMZdI1tYgi/njW5zV52jNLgSAZSdN16m9bJFe61/cT8ulmw4qFitV9QRsEA==" + }, + "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", + "integrity": "sha512-Q5dcQ5qLPpMSyj0iEqucTfeINHVeEhuSjjaPVEU24+6RGlvCrEM6AXwOlWW4iIn10yyWROASRuS3rgbUZIE/5g==", + "requires": { + "dns-socket": "^4.2.0", + "got": "^9.6.0", + "is-ip": "^3.1.0" + } + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "requires": { + "deep-extend": "^0.6.0", + "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": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "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", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "requires": { + "resolve": "^1.1.6" + } + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "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", + "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==" + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "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.0", + "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.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "dependencies": { + "uuid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", + "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==" + } + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "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==", + "requires": { + "path-parse": "^1.0.6" + } + }, + "resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", + "requires": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" + }, + "responselike": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", + "requires": { + "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", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "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" + } + } + } + }, + "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", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "requires": { + "ret": "~0.1.10" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "semver": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", + "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==" + }, + "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" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.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=", + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, + "slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + }, + "dependencies": { + "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=", + "dev": true + } + } + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.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" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "requires": { + "kind-of": "^3.2.0" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "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==", + "requires": { + "atob": "^2.1.1", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "sqlite3": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.1.0.tgz", + "integrity": "sha512-RvqoKxq+8pDHsJo7aXxsFR18i+dU2Wp5o12qAJOV5LNcDt+fgJsc2QKKg3sIRfXrN9ZjzY1T7SNe/DFVqAXjaw==", + "requires": { + "nan": "^2.12.1", + "node-pre-gyp": "^0.11.0", + "request": "^2.87.0" + } + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "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=" + } + } + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "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=" + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "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-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "table": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", + "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "dev": true, + "requires": { + "ajv": "^6.10.2", + "lodash": "^4.17.14", + "slice-ansi": "^2.1.0", + "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", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "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=", + "dev": true + }, + "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==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.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" + } + } + } + }, + "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" + }, + "dependencies": { + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + } + } + }, + "tarn": { + "version": "2.0.0", + "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", + "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", + "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", + "integrity": "sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw==", + "requires": { + "rimraf": "^2.6.3" + } + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-readable-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", + "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==" + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "requires": { + "is-number": "^3.0.0", + "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.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + } + }, + "transliteration": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/transliteration/-/transliteration-2.1.7.tgz", + "integrity": "sha512-o3678GPmKKGqOBB+trAKzhBUjHddU18He2V8AKB1XuegaGJekO0xmfkkvbc9LCBat62nb7IH8z5/OJY+mNugkg==", + "requires": { + "yargs": "^14.0.0" + } + }, + "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", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.1.tgz", + "integrity": "sha512-kcoMoKTPYnoeS50tzoqjPY3Uv9axeuuFAZY9M/9zFnhoVvRfxz9K29IMPD7jGmt2c8SW7i3gT9WqDl2+nV7p4A==", + "optional": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": 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=" + }, + "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", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "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", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" + } + } + }, + "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", + "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": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" + }, + "url-parse-lax": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", + "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", + "requires": { + "prepend-http": "^2.0.0" + } + }, + "use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "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==" + }, + "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==", + "dev": true + }, + "v8flags": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.1.3.tgz", + "integrity": "sha512-amh9CCg3ZxkzQ48Mhcb8iX7xpAfYJgePHxWMQCBWECpOSqJUXgY26ncA61UTV0BkPqfhcy6mzwCIoP4ygxpW8w==", + "requires": { + "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=", + "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", + "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "requires": { + "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", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "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==", + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.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==" + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + }, + "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=" + }, + "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==", + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.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==", + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "write": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", + "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", + "dev": true, + "requires": { + "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", + "integrity": "sha512-+SqNqFbwTm/0DC18KYzIsMTnEWpLwJsiasW/O17la4iDRRIO9uaHbvKiAS3AHgTiuuWerK/brj4O6MYZkei9xg==", + "requires": { + "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 + }, + "yargs": { + "version": "14.2.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-14.2.2.tgz", + "integrity": "sha512-/4ld+4VV5RnrynMhPZJ/ZpOCGSCeghMykZ3BhdFBDa9Wy/RH6uEGNWDJog+aUlq+9OM1CFTgtYRW5Is1Po9NOA==", + "requires": { + "cliui": "^5.0.0", + "decamelize": "^1.2.0", + "find-up": "^3.0.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", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^15.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==" + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + }, + "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=" + }, + "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==", + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.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==", + "requires": { + "ansi-regex": "^4.1.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==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..f3052ff --- /dev/null +++ b/package.json @@ -0,0 +1,37 @@ +{ + "name": "modmailbot", + "version": "2.30.1", + "description": "", + "license": "MIT", + "main": "src/index.js", + "scripts": { + "start": "node src/index.js", + "watch": "nodemon -w src src/index.js", + "test": "echo \"Error: no test specified\" && exit 1", + "lint": "eslint ./src" + }, + "repository": { + "type": "git", + "url": "https://github.com/Dragory/modmailbot" + }, + "dependencies": { + "eris": "^0.11.1", + "humanize-duration": "^3.12.1", + "ini": "^1.3.5", + "json5": "^2.1.1", + "knex": "^0.20.3", + "knub-command-manager": "^6.1.0", + "mime": "^2.4.4", + "moment": "^2.24.0", + "mv": "^2.1.1", + "public-ip": "^4.0.0", + "sqlite3": "^4.1.0", + "tmp": "^0.1.0", + "transliteration": "^2.1.7", + "uuid": "^3.3.3" + }, + "devDependencies": { + "eslint": "^6.7.2", + "nodemon": "^2.0.1" + } +} diff --git a/src/bot.js b/src/bot.js new file mode 100644 index 0000000..2a1591a --- /dev/null +++ b/src/bot.js @@ -0,0 +1,9 @@ +const Eris = require('eris'); +const config = require('./config'); + +const bot = new Eris.Client(config.token, { + getAllUsers: true, + restMode: true, +}); + +module.exports = bot; diff --git a/src/commands.js b/src/commands.js new file mode 100644 index 0000000..59c6506 --- /dev/null +++ b/src/commands.js @@ -0,0 +1,130 @@ +const { CommandManager, defaultParameterTypes, TypeConversionError } = require('knub-command-manager'); +const config = require('./config'); +const utils = require('./utils'); +const threads = require('./data/threads'); + +module.exports = { + createCommandManager(bot) { + const manager = new CommandManager({ + prefix: config.prefix, + types: Object.assign({}, defaultParameterTypes, { + userId(value) { + const userId = utils.getUserMention(value); + if (! userId) throw new TypeConversionError(); + return userId; + }, + + delay(value) { + const ms = utils.convertDelayStringToMS(value); + if (ms === null) throw new TypeConversionError(); + return ms; + } + }) + }); + + const handlers = {}; + const aliasMap = new Map(); + + bot.on('messageCreate', async msg => { + if (msg.author.bot) return; + if (msg.author.id === bot.user.id) return; + if (! msg.content) return; + + const matchedCommand = await manager.findMatchingCommand(msg.content, { msg }); + if (matchedCommand === null) return; + if (matchedCommand.error !== undefined) { + utils.postError(msg.channel, matchedCommand.error); + return; + } + + const allArgs = {}; + for (const [name, arg] of Object.entries(matchedCommand.args)) { + allArgs[name] = arg.value; + } + for (const [name, opt] of Object.entries(matchedCommand.opts)) { + allArgs[name] = opt.value; + } + + handlers[matchedCommand.id](msg, allArgs); + }); + + /** + * Add a command that can be invoked anywhere + */ + const addGlobalCommand = (trigger, parameters, handler, commandConfig = {}) => { + let aliases = aliasMap.has(trigger) ? [...aliasMap.get(trigger)] : []; + if (commandConfig.aliases) aliases.push(...commandConfig.aliases); + + const cmd = manager.add(trigger, parameters, { ...commandConfig, aliases }); + handlers[cmd.id] = handler; + }; + + /** + * Add a command that can only be invoked on the inbox server + */ + const addInboxServerCommand = (trigger, parameters, handler, commandConfig = {}) => { + const aliases = aliasMap.has(trigger) ? [...aliasMap.get(trigger)] : []; + if (commandConfig.aliases) aliases.push(...commandConfig.aliases); + + const cmd = manager.add(trigger, parameters, { + ...commandConfig, + aliases, + preFilters: [ + (_, context) => { + if (! utils.messageIsOnInboxServer(context.msg)) return false; + if (! utils.isStaff(context.msg.member)) return false; + return true; + } + ] + }); + + handlers[cmd.id] = async (msg, args) => { + const thread = await threads.findOpenThreadByChannelId(msg.channel.id); + handler(msg, args, thread); + }; + }; + + /** + * Add a command that can only be invoked in a thread on the inbox server + */ + const addInboxThreadCommand = (trigger, parameters, handler, commandConfig = {}) => { + const aliases = aliasMap.has(trigger) ? [...aliasMap.get(trigger)] : []; + if (commandConfig.aliases) aliases.push(...commandConfig.aliases); + + let thread; + const cmd = manager.add(trigger, parameters, { + ...commandConfig, + aliases, + preFilters: [ + async (_, context) => { + if (! utils.messageIsOnInboxServer(context.msg)) return false; + if (! utils.isStaff(context.msg.member)) return false; + thread = await threads.findOpenThreadByChannelId(context.msg.channel.id); + if (! thread) return false; + return true; + } + ] + }); + + handlers[cmd.id] = async (msg, args) => { + handler(msg, args, thread); + }; + }; + + const addAlias = (originalCmd, alias) => { + if (! aliasMap.has(originalCmd)) { + aliasMap.set(originalCmd, new Set()); + } + + aliasMap.get(originalCmd).add(alias); + }; + + return { + manager, + addGlobalCommand, + addInboxServerCommand, + addInboxThreadCommand, + addAlias, + }; + } +}; diff --git a/src/config.js b/src/config.js new file mode 100644 index 0000000..a2e3beb --- /dev/null +++ b/src/config.js @@ -0,0 +1,289 @@ +/** + * !!! 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'); + +let userConfig = {}; + +// Config files to search for, in priority order +const configFiles = [ + 'config.ini', + 'config.ini.ini', + 'config.ini.txt', + 'config.json', + 'config.json5', + 'config.json.json', + 'config.json.txt', + 'config.js' +]; + +let foundConfigFile; +for (const configFile of configFiles) { + try { + fs.accessSync(__dirname + '/../' + configFile); + foundConfigFile = configFile; + break; + } catch (e) {} +} + +// Load config file +if (foundConfigFile) { + console.log(`Loading configuration from ${foundConfigFile}...`); + try { + if (foundConfigFile.endsWith('.js')) { + userConfig = require(`../${foundConfigFile}`); + } else { + const raw = fs.readFileSync(__dirname + '/../' + foundConfigFile, {encoding: "utf8"}); + if (foundConfigFile.endsWith('.ini') || foundConfigFile.endsWith('.ini.txt')) { + userConfig = require('ini').decode(raw); + } else { + userConfig = require('json5').parse(raw); + } + } + } catch (e) { + throw new Error(`Error reading config file! The error given was: ${e.message}`); + } +} + +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, + + "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'), +}; + +// Load config values from environment variables +const envKeyPrefix = 'MM_'; +let loadedEnvValues = 0; + +for (const [key, value] of Object.entries(process.env)) { + if (! key.startsWith(envKeyPrefix)) continue; + + // MM_CLOSE_MESSAGE -> closeMessage + // MM_COMMAND_ALIASES__MV => commandAliases.mv + const configKey = key.slice(envKeyPrefix.length) + .toLowerCase() + .replace(/([a-z])_([a-z])/g, (m, m1, m2) => `${m1}${m2.toUpperCase()}`) + .replace('__', '.'); + + userConfig[configKey] = value.includes('||') + ? value.split('||') + : value; + + loadedEnvValues++; +} + +if (process.env.PORT && !process.env.MM_PORT) { + // Special case: allow common "PORT" environment variable without prefix + userConfig.port = process.env.PORT; + loadedEnvValues++; +} + +if (loadedEnvValues > 0) { + 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(userConfig)) { + if (! key.includes('.')) continue; + + const keys = key.split('.'); + let cursor = userConfig; + for (let i = 0; i < keys.length; i++) { + if (i === keys.length - 1) { + cursor[keys[i]] = value; + } else { + cursor[keys[i]] = cursor[keys[i]] || {}; + cursor = cursor[keys[i]]; + } + } + + delete userConfig[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}`); + } + + finalConfig[prop] = value; +} + +// Default knex config +if (! finalConfig['knex']) { + finalConfig['knex'] = { + client: 'sqlite', + connection: { + filename: path.join(finalConfig.dbDir, 'data.sqlite') + }, + useNullAsDefault: true + }; +} + +// Make sure migration settings are always present in knex config +Object.assign(finalConfig['knex'], { + migrations: { + directory: path.join(finalConfig.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']]; +} + +// Make sure inboxServerPermission is always an array +if (! Array.isArray(finalConfig['inboxServerPermission'])) { + if (finalConfig['inboxServerPermission'] == null) { + finalConfig['inboxServerPermission'] = []; + } else { + finalConfig['inboxServerPermission'] = [finalConfig['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 +// 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 + }; + } +} + +// newThreadCategoryId is syntactic sugar for categoryAutomation.newThread +if (finalConfig.newThreadCategoryId) { + finalConfig.categoryAutomation.newThread = finalConfig.newThreadCategoryId; + delete finalConfig.newThreadCategoryId; +} + +// Turn empty string options to null (i.e. "option=" without a value in config.ini) +for (const [key, value] of Object.entries(finalConfig)) { + if (value === '') { + finalConfig[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; + } +} + +// 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 config.json value: ${opt}`); + process.exit(1); + } +} + +console.log("Configuration ok!"); + +module.exports = finalConfig; diff --git a/src/data/Snippet.js b/src/data/Snippet.js new file mode 100644 index 0000000..4d5b684 --- /dev/null +++ b/src/data/Snippet.js @@ -0,0 +1,15 @@ +const utils = require("../utils"); + +/** + * @property {String} trigger + * @property {String} body + * @property {String} created_by + * @property {String} created_at + */ +class Snippet { + constructor(props) { + utils.setDataModelProps(this, props); + } +} + +module.exports = Snippet; diff --git a/src/data/Thread.js b/src/data/Thread.js new file mode 100644 index 0000000..23e9e4a --- /dev/null +++ b/src/data/Thread.js @@ -0,0 +1,468 @@ +const moment = require('moment'); + +const bot = require('../bot'); +const knex = require('../knex'); +const utils = require('../utils'); +const config = require('../config'); +const attachments = require('./attachments'); + +const ThreadMessage = require('./ThreadMessage'); + +const {THREAD_MESSAGE_TYPE, THREAD_STATUS} = require('./constants'); + +/** + * @property {String} id + * @property {Number} status + * @property {String} user_id + * @property {String} user_name + * @property {String} channel_id + * @property {String} scheduled_close_at + * @property {String} scheduled_close_id + * @property {String} scheduled_close_name + * @property {Number} scheduled_close_silent + * @property {String} alert_id + * @property {String} created_at + */ +class Thread { + constructor(props) { + utils.setDataModelProps(this, props); + } + + /** + * @param {Eris~Member} moderator + * @param {String} text + * @param {Eris~MessageFile[]} replyAttachments + * @param {Boolean} isAnonymous + * @returns {Promise<boolean>} Whether we were able to send the reply + */ + async replyToUser(moderator, text, replyAttachments = [], isAnonymous = false) { + // Username to reply with + let modUsername, logModUsername; + const mainRole = utils.getMainRole(moderator); + + if (isAnonymous) { + modUsername = (mainRole ? mainRole.name : 'Moderator'); + logModUsername = `(Anonymous) (${moderator.user.username}) ${mainRole ? mainRole.name : 'Moderator'}`; + } else { + const name = (config.useNicknames ? moderator.nick || moderator.user.username : moderator.user.username); + modUsername = (mainRole ? `(${mainRole.name}) ${name}` : name); + logModUsername = modUsername; + } + + // Build the reply message + let dmContent = `**${modUsername}:** ${text}`; + let threadContent = `**${logModUsername}:** ${text}`; + let logContent = text; + + if (config.threadTimestamps) { + const timestamp = utils.getTimestamp(); + threadContent = `[${timestamp}] » ${threadContent}`; + } + + // Prepare attachments, if any + let files = []; + + if (replyAttachments.length > 0) { + for (const attachment of replyAttachments) { + let savedAttachment; + + await Promise.all([ + attachments.attachmentToDiscordFileObject(attachment).then(file => { + files.push(file); + }), + attachments.saveAttachment(attachment).then(result => { + savedAttachment = result; + }) + ]); + + logContent += `\n\n**Attachment:** ${savedAttachment.url}`; + } + } + + // Send the reply DM + let dmMessage; + try { + dmMessage = await this.postToUser(dmContent, files); + } catch (e) { + await this.addThreadMessageToDB({ + message_type: THREAD_MESSAGE_TYPE.COMMAND, + user_id: moderator.id, + user_name: logModUsername, + body: logContent + }); + + await this.postSystemMessage(`Error while replying to user: ${e.message}`); + + return false; + } + + // Send the reply to the modmail thread + await this.postToThreadChannel(threadContent, files); + + // Add the message to the database + await this.addThreadMessageToDB({ + message_type: THREAD_MESSAGE_TYPE.TO_USER, + user_id: moderator.id, + user_name: logModUsername, + body: logContent, + is_anonymous: (isAnonymous ? 1 : 0), + dm_message_id: dmMessage.id + }); + + if (this.scheduled_close_at) { + await this.cancelScheduledClose(); + await this.postSystemMessage(`Cancelling scheduled closing of this thread due to new reply`); + } + + return true; + } + + /** + * @param {Eris~Message} msg + * @returns {Promise<void>} + */ + async receiveUserReply(msg) { + let content = msg.content; + if (msg.content.trim() === '' && msg.embeds.length) { + content = '<message contains embeds>'; + } + + let threadContent = `**${msg.author.username}#${msg.author.discriminator}:** ${content}`; + let logContent = msg.content; + + if (config.threadTimestamps) { + const timestamp = utils.getTimestamp(msg.timestamp, 'x'); + threadContent = `[${timestamp}] « ${threadContent}`; + } + + // Prepare attachments, if any + let attachmentFiles = []; + + for (const attachment of msg.attachments) { + const savedAttachment = await attachments.saveAttachment(attachment); + + // Forward small attachments (<2MB) as attachments, just link to larger ones + const formatted = '\n\n' + await utils.formatAttachment(attachment, savedAttachment.url); + logContent += formatted; // Logs always contain the link + + if (config.relaySmallAttachmentsAsAttachments && attachment.size <= 1024 * 1024 * 2) { + const file = await attachments.attachmentToDiscordFileObject(attachment); + attachmentFiles.push(file); + } else { + threadContent += formatted; + } + } + + await this.postToThreadChannel(threadContent, attachmentFiles); + await this.addThreadMessageToDB({ + message_type: THREAD_MESSAGE_TYPE.FROM_USER, + user_id: this.user_id, + user_name: `${msg.author.username}#${msg.author.discriminator}`, + body: logContent, + is_anonymous: 0, + dm_message_id: msg.id + }); + + 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.`); + } + + if (this.alert_id) { + await this.setAlert(null); + await this.postSystemMessage(`<@!${this.alert_id}> New message from ${this.user_name}`); + } + } + + /** + * @returns {Promise<PrivateChannel>} + */ + getDMChannel() { + return bot.getDMChannel(this.user_id); + } + + /** + * @param {String} text + * @param {Eris~MessageFile|Eris~MessageFile[]} file + * @returns {Promise<Eris~Message>} + * @throws Error + */ + async postToUser(text, 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]; + } + + /** + * @returns {Promise<Eris~Message>} + */ + async postToThreadChannel(...args) { + 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]; + } else { + return bot.createMessage(this.channel_id, ...args); + } + } catch (e) { + // Channel not found + if (e.code === 10003) { + console.log(`[INFO] Failed to send message to thread channel for ${this.user_name} because the channel no longer exists. Auto-closing the thread.`); + this.close(true); + } else { + throw e; + } + } + } + + /** + * @param {String} text + * @param {*} args + * @returns {Promise<void>} + */ + async postSystemMessage(text, ...args) { + const msg = await this.postToThreadChannel(text, ...args); + await this.addThreadMessageToDB({ + message_type: THREAD_MESSAGE_TYPE.SYSTEM, + user_id: null, + user_name: '', + body: typeof text === 'string' ? text : text.content, + is_anonymous: 0, + dm_message_id: msg.id + }); + } + + /** + * @param {*} args + * @returns {Promise<void>} + */ + async postNonLogMessage(...args) { + await this.postToThreadChannel(...args); + } + + /** + * @param {Eris.Message} msg + * @returns {Promise<void>} + */ + async saveChatMessage(msg) { + return this.addThreadMessageToDB({ + message_type: THREAD_MESSAGE_TYPE.CHAT, + user_id: msg.author.id, + user_name: `${msg.author.username}#${msg.author.discriminator}`, + body: msg.content, + is_anonymous: 0, + dm_message_id: msg.id + }); + } + + async saveCommandMessage(msg) { + return this.addThreadMessageToDB({ + message_type: THREAD_MESSAGE_TYPE.COMMAND, + user_id: msg.author.id, + user_name: `${msg.author.username}#${msg.author.discriminator}`, + body: msg.content, + is_anonymous: 0, + dm_message_id: msg.id + }); + } + + /** + * @param {Eris.Message} msg + * @returns {Promise<void>} + */ + async updateChatMessage(msg) { + await knex('thread_messages') + .where('thread_id', this.id) + .where('dm_message_id', msg.id) + .update({ + body: msg.content + }); + } + + /** + * @param {String} messageId + * @returns {Promise<void>} + */ + async deleteChatMessage(messageId) { + await knex('thread_messages') + .where('thread_id', this.id) + .where('dm_message_id', messageId) + .delete(); + } + + /** + * @param {Object} data + * @returns {Promise<void>} + */ + async addThreadMessageToDB(data) { + await knex('thread_messages').insert({ + thread_id: this.id, + created_at: moment.utc().format('YYYY-MM-DD HH:mm:ss'), + is_anonymous: 0, + ...data + }); + } + + /** + * @returns {Promise<ThreadMessage[]>} + */ + async getThreadMessages() { + 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)); + } + + /** + * @returns {Promise<void>} + */ + async close(suppressSystemMessage = false, silent = false) { + if (! suppressSystemMessage) { + console.log(`Closing thread ${this.id}`); + + if (silent) { + await this.postSystemMessage('Closing thread silently...'); + } else { + await this.postSystemMessage('Closing thread...'); + } + } + + // Update DB status + await knex('threads') + .where('id', this.id) + .update({ + status: THREAD_STATUS.CLOSED + }); + + // Delete channel + const channel = bot.getChannel(this.channel_id); + if (channel) { + console.log(`Deleting channel ${this.channel_id}`); + await channel.delete('Thread closed'); + } + } + + /** + * @param {String} time + * @param {Eris~User} user + * @param {Number} silent + * @returns {Promise<void>} + */ + async scheduleClose(time, user, silent) { + await knex('threads') + .where('id', this.id) + .update({ + scheduled_close_at: time, + scheduled_close_id: user.id, + scheduled_close_name: user.username, + scheduled_close_silent: silent + }); + } + + /** + * @returns {Promise<void>} + */ + async cancelScheduledClose() { + await knex('threads') + .where('id', this.id) + .update({ + scheduled_close_at: null, + scheduled_close_id: null, + scheduled_close_name: null, + scheduled_close_silent: null + }); + } + + /** + * @returns {Promise<void>} + */ + async suspend() { + await knex('threads') + .where('id', this.id) + .update({ + status: THREAD_STATUS.SUSPENDED, + scheduled_suspend_at: null, + scheduled_suspend_id: null, + scheduled_suspend_name: null + }); + } + + /** + * @returns {Promise<void>} + */ + async unsuspend() { + await knex('threads') + .where('id', this.id) + .update({ + status: THREAD_STATUS.OPEN + }); + } + + /** + * @param {String} time + * @param {Eris~User} user + * @returns {Promise<void>} + */ + async scheduleSuspend(time, user) { + await knex('threads') + .where('id', this.id) + .update({ + scheduled_suspend_at: time, + scheduled_suspend_id: user.id, + scheduled_suspend_name: user.username + }); + } + + /** + * @returns {Promise<void>} + */ + async cancelScheduledSuspend() { + await knex('threads') + .where('id', this.id) + .update({ + scheduled_suspend_at: null, + scheduled_suspend_id: null, + scheduled_suspend_name: null + }); + } + + /** + * @param {String} userId + * @returns {Promise<void>} + */ + async setAlert(userId) { + await knex('threads') + .where('id', this.id) + .update({ + alert_id: userId + }); + } + + /** + * @returns {Promise<String>} + */ + getLogUrl() { + return utils.getSelfUrl(`logs/${this.id}`); + } +} + +module.exports = Thread; diff --git a/src/data/ThreadMessage.js b/src/data/ThreadMessage.js new file mode 100644 index 0000000..2704287 --- /dev/null +++ b/src/data/ThreadMessage.js @@ -0,0 +1,20 @@ +const utils = require("../utils"); + +/** + * @property {Number} id + * @property {String} thread_id + * @property {Number} message_type + * @property {String} user_id + * @property {String} user_name + * @property {String} body + * @property {Number} is_anonymous + * @property {Number} dm_message_id + * @property {String} created_at + */ +class ThreadMessage { + constructor(props) { + utils.setDataModelProps(this, props); + } +} + +module.exports = ThreadMessage; diff --git a/src/data/attachments.js b/src/data/attachments.js new file mode 100644 index 0000000..40b13b0 --- /dev/null +++ b/src/data/attachments.js @@ -0,0 +1,202 @@ +const Eris = require('eris'); +const fs = require('fs'); +const https = require('https'); +const {promisify} = require('util'); +const tmp = require('tmp'); +const config = require('../config'); +const utils = require('../utils'); +const mv = promisify(require('mv')); + +const getUtils = () => require('../utils'); + +const access = promisify(fs.access); +const readFile = promisify(fs.readFile); + +const localAttachmentDir = config.attachmentDir || `${__dirname}/../../attachments`; + +const attachmentSavePromises = {}; + +const attachmentStorageTypes = {}; + +function getErrorResult(msg = null) { + return { + url: `Attachment could not be saved${msg ? ': ' + msg : ''}`, + failed: true + }; +} + +/** + * Attempts to download and save the given attachement + * @param {Object} attachment + * @param {Number=0} tries + * @returns {Promise<{ url: string }>} + */ +async function saveLocalAttachment(attachment) { + const targetPath = getLocalAttachmentPath(attachment.id); + + try { + // If the file already exists, resolve immediately + await access(targetPath); + const url = await getLocalAttachmentUrl(attachment.id, attachment.filename); + return { url }; + } catch (e) {} + + // Download the attachment + const downloadResult = await downloadAttachment(attachment); + + // Move the temp file to the attachment folder + await mv(downloadResult.path, targetPath); + + // Resolve the attachment URL + const url = await getLocalAttachmentUrl(attachment.id, attachment.filename); + + return { url }; +} + +/** + * @param {Object} attachment + * @param {Number} tries + * @returns {Promise<{ path: string, cleanup: function }>} + */ +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'); + return; + } + + tmp.file((err, filepath, fd, cleanupCallback) => { + const writeStream = fs.createWriteStream(filepath); + + https.get(attachment.url, (res) => { + res.pipe(writeStream); + writeStream.on('finish', () => { + writeStream.end(); + resolve({ + path: filepath, + cleanup: cleanupCallback + }); + }); + }).on('error', (err) => { + fs.unlink(filepath); + console.error('Error downloading attachment, retrying'); + resolve(downloadAttachment(attachment, tries++)); + }); + }); + }); +} + +/** + * Returns the filesystem path for the given attachment id + * @param {String} attachmentId + * @returns {String} + */ +function getLocalAttachmentPath(attachmentId) { + return `${localAttachmentDir}/${attachmentId}`; +} + +/** + * Returns the self-hosted URL to the given attachment ID + * @param {String} attachmentId + * @param {String=null} desiredName Custom name for the attachment as a hint for the browser + * @returns {Promise<String>} + */ +function getLocalAttachmentUrl(attachmentId, desiredName = null) { + if (desiredName == null) desiredName = 'file.bin'; + return getUtils().getSelfUrl(`attachments/${attachmentId}/${desiredName}`); +} + +/** + * @param {Object} attachment + * @returns {Promise<{ url: string }>} + */ +async function saveDiscordAttachment(attachment) { + if (attachment.size > 1024 * 1024 * 8) { + 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!'); + } + + const attachmentChannel = inboxGuild.channels.get(attachmentChannelId); + if (! (attachmentChannel instanceof Eris.TextChannel)) { + throw new Error('Attachment storage channel must be a text channel!'); + } + + const file = await attachmentToDiscordFileObject(attachment); + const savedAttachment = await createDiscordAttachmentMessage(attachmentChannel, file); + if (! savedAttachment) return getErrorResult(); + + return { url: savedAttachment.url }; +} + +async function createDiscordAttachmentMessage(channel, file, tries = 0) { + tries++; + + try { + const attachmentMessage = await channel.createMessage(undefined, file); + return attachmentMessage.attachments[0]; + } catch (e) { + if (tries > 3) { + console.error(`Attachment storage message could not be created after 3 tries: ${e.message}`); + return; + } + + return createDiscordAttachmentMessage(channel, file, tries); + } +} + +/** + * 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}>} + */ +async function attachmentToDiscordFileObject(attachment) { + const downloadResult = await downloadAttachment(attachment); + const data = await readFile(downloadResult.path); + downloadResult.cleanup(); + return {file: data, name: attachment.filename}; +} + +/** + * Saves the given attachment based on the configured storage system + * @param {Object} attachment + * @returns {Promise<{ url: string }>} + */ +function saveAttachment(attachment) { + if (attachmentSavePromises[attachment.id]) { + return attachmentSavePromises[attachment.id]; + } + + if (attachmentStorageTypes[config.attachmentStorage]) { + attachmentSavePromises[attachment.id] = Promise.resolve(attachmentStorageTypes[config.attachmentStorage](attachment)); + } else { + throw new Error(`Unknown attachment storage option: ${config.attachmentStorage}`); + } + + attachmentSavePromises[attachment.id].then(() => { + delete attachmentSavePromises[attachment.id]; + }); + + return attachmentSavePromises[attachment.id]; +} + +function addStorageType(name, handler) { + attachmentStorageTypes[name] = handler; +} + +attachmentStorageTypes.local = saveLocalAttachment; +attachmentStorageTypes.discord = saveDiscordAttachment; + +module.exports = { + getLocalAttachmentPath, + attachmentToDiscordFileObject, + saveAttachment, + addStorageType, + downloadAttachment +}; diff --git a/src/data/blocked.js b/src/data/blocked.js new file mode 100644 index 0000000..81c1214 --- /dev/null +++ b/src/data/blocked.js @@ -0,0 +1,94 @@ +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) + .first(); + + return { + isBlocked: !! row, + expiresAt: row && row.expires_at + }; +} + +/** + * Checks whether userId is blocked + * @param {String} userId + * @returns {Promise<Boolean>} + */ +async function isBlocked(userId) { + return (await getBlockStatus(userId)).isBlocked; +} + +/** + * Blocks the given userId + * @param {String} userId + * @param {String} userName + * @param {String} blockedBy + * @returns {Promise} + */ +async function block(userId, userName = '', blockedBy = null, expiresAt = null) { + if (await isBlocked(userId)) return; + + 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'), + expires_at: expiresAt + }); +} + +/** + * Unblocks the given userId + * @param {String} userId + * @returns {Promise} + */ +async function unblock(userId) { + return knex('blocked_users') + .where('user_id', userId) + .delete(); +} + +/** + * Updates the expiry time of the block for the given userId + * @param {String} userId + * @param {String} expiresAt + * @returns {Promise<void>} + */ +async function updateExpiryTime(userId, expiresAt) { + return knex('blocked_users') + .where('user_id', userId) + .update({ + expires_at: expiresAt + }); +} + +/** + * @returns {String[]} + */ +async function getExpiredBlocks() { + const now = moment.utc().format('YYYY-MM-DD HH:mm:ss'); + + const blocks = await knex('blocked_users') + .whereNotNull('expires_at') + .where('expires_at', '<=', now) + .select(); + + return blocks.map(block => block.user_id); +} + +module.exports = { + getBlockStatus, + isBlocked, + block, + unblock, + updateExpiryTime, + getExpiredBlocks, +}; diff --git a/src/data/constants.js b/src/data/constants.js new file mode 100644 index 0000000..13a30e8 --- /dev/null +++ b/src/data/constants.js @@ -0,0 +1,55 @@ +module.exports = { + THREAD_STATUS: { + OPEN: 1, + CLOSED: 2, + SUSPENDED: 3 + }, + + THREAD_MESSAGE_TYPE: { + SYSTEM: 1, + CHAT: 2, + FROM_USER: 3, + TO_USER: 4, + LEGACY: 5, + COMMAND: 6 + }, + + 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' + ], +}; diff --git a/src/data/snippets.js b/src/data/snippets.js new file mode 100644 index 0000000..a95b2b4 --- /dev/null +++ b/src/data/snippets.js @@ -0,0 +1,58 @@ +const moment = require('moment'); +const knex = require('../knex'); +const Snippet = require('./Snippet'); + +/** + * @param {String} trigger + * @returns {Promise<Snippet>} + */ +async function getSnippet(trigger) { + const snippet = await knex('snippets') + .where('trigger', trigger) + .first(); + + return (snippet ? new Snippet(snippet) : null); +} + +/** + * @param {String} trigger + * @param {String} body + * @returns {Promise<void>} + */ +async function addSnippet(trigger, body, createdBy = 0) { + if (await getSnippet(trigger)) return; + + return knex('snippets').insert({ + trigger, + body, + created_by: createdBy, + created_at: moment.utc().format('YYYY-MM-DD HH:mm:ss') + }); +} + +/** + * @param {String} trigger + * @returns {Promise<void>} + */ +async function deleteSnippet(trigger) { + return knex('snippets') + .where('trigger', trigger) + .delete(); +} + +/** + * @returns {Promise<Snippet[]>} + */ +async function getAllSnippets() { + const snippets = await knex('snippets') + .select(); + + return snippets.map(s => new Snippet(s)); +} + +module.exports = { + get: getSnippet, + add: addSnippet, + del: deleteSnippet, + all: getAllSnippets, +}; diff --git a/src/data/threads.js b/src/data/threads.js new file mode 100644 index 0000000..6e700b0 --- /dev/null +++ b/src/data/threads.js @@ -0,0 +1,381 @@ +const {User, Member} = require('eris'); + +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('../config'); +const utils = require('../utils'); +const updates = require('./updates'); + +const Thread = require('./Thread'); +const {THREAD_STATUS} = require('./constants'); + +const MINUTES = 60 * 1000; +const HOURS = 60 * MINUTES; + +/** + * @param {String} id + * @returns {Promise<Thread>} + */ +async function findById(id) { + const thread = await knex('threads') + .where('id', id) + .first(); + + return (thread ? new Thread(thread) : null); +} + +/** + * @param {String} userId + * @returns {Promise<Thread>} + */ +async function findOpenThreadByUserId(userId) { + const thread = await knex('threads') + .where('user_id', userId) + .where('status', THREAD_STATUS.OPEN) + .first(); + + return (thread ? new Thread(thread) : null); +} + +function getHeaderGuildInfo(member) { + return { + nickname: member.nick || member.user.username, + joinDate: humanizeDuration(Date.now() - member.joinedAt, {largest: 2, round: true}) + }; +} + +/** + * 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 + * @returns {Promise<Thread|undefined>} + * @throws {Error} + */ +async function createNewThreadForUser(user, quiet = false, 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!'); + } + + // 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; + } + } + + // 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 (member) { + userGuildData.set(guild.id, { guild, member }); + } + } + + // 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; + }); + + if (! isAllowed) { + if (config.timeOnServerDeniedMessage) { + const timeOnServerDeniedMessage = utils.readMultilineConfigValue(config.timeOnServerDeniedMessage); + const privateChannel = await user.getDMChannel(); + await privateChannel.createMessage(timeOnServerDeniedMessage); + } + 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 + + const channelName = `${cleanName}-${user.discriminator}`; + + console.log(`[NOTE] Creating new thread channel ${channelName}`); + + // Figure out which category we should place the thread channel in + let newThreadCategoryId; + + if (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)) { + 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, null, 'New Modmail thread', 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 + if (config.mentionRole) { + await newThread.postNonLogMessage({ + content: `${utils.getInboxMention()}New modmail thread (${newThread.user_name})`, + disableEveryone: false + }); + } + + // Send auto-reply to the user + if (config.responseMessage) { + const responseMessage = utils.readMultilineConfigValue(config.responseMessage); + + try { + await newThread.postToUser(responseMessage); + } catch (err) { + responseMessageError = err; + } + } + } + + // 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 (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); + + if (config.updateNotifications) { + const availableUpdate = await updates.getAvailableUpdate(); + if (availableUpdate) { + await newThread.postNonLogMessage(`📣 New bot version available (${availableUpdate})`); + } + } + + // 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; +} + +/** + * Creates a new thread row in the database + * @param {Object} data + * @returns {Promise<String>} The ID of the created thread + */ +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}); + + await knex('threads').insert(finalData); + + return threadId; +} + +/** + * @param {String} channelId + * @returns {Promise<Thread>} + */ +async function findByChannelId(channelId) { + const thread = await knex('threads') + .where('channel_id', channelId) + .first(); + + return (thread ? new Thread(thread) : null); +} + +/** + * @param {String} channelId + * @returns {Promise<Thread>} + */ +async function findOpenThreadByChannelId(channelId) { + const thread = await knex('threads') + .where('channel_id', channelId) + .where('status', THREAD_STATUS.OPEN) + .first(); + + return (thread ? new Thread(thread) : null); +} + +/** + * @param {String} channelId + * @returns {Promise<Thread>} + */ +async function findSuspendedThreadByChannelId(channelId) { + const thread = await knex('threads') + .where('channel_id', channelId) + .where('status', THREAD_STATUS.SUSPENDED) + .first(); + + return (thread ? new Thread(thread) : null); +} + +/** + * @param {String} userId + * @returns {Promise<Thread[]>} + */ +async function getClosedThreadsByUserId(userId) { + const threads = await knex('threads') + .where('status', THREAD_STATUS.CLOSED) + .where('user_id', userId) + .select(); + + return threads.map(thread => new Thread(thread)); +} + +/** + * @param {String} userId + * @returns {Promise<number>} + */ +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')); + + return parseInt(row.thread_count, 10); +} + +async function findOrCreateThreadForUser(user) { + const existingThread = await findOpenThreadByUserId(user.id); + if (existingThread) return existingThread; + + return createNewThreadForUser(user); +} + +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') + .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') + .select(); + + return threads.map(thread => new Thread(thread)); +} + +module.exports = { + findById, + findOpenThreadByUserId, + findByChannelId, + findOpenThreadByChannelId, + findSuspendedThreadByChannelId, + createNewThreadForUser, + getClosedThreadsByUserId, + findOrCreateThreadForUser, + getThreadsThatShouldBeClosed, + getThreadsThatShouldBeSuspended, + createThreadInDB +}; diff --git a/src/data/updates.js b/src/data/updates.js new file mode 100644 index 0000000..a57bc62 --- /dev/null +++ b/src/data/updates.js @@ -0,0 +1,115 @@ +const url = require('url'); +const https = require('https'); +const moment = require('moment'); +const knex = require('../knex'); +const config = require('../config'); + +const UPDATE_CHECK_FREQUENCY = 12; // In hours +let updateCheckPromise = null; + +async function initUpdatesTable() { + const row = await knex('updates').first(); + if (! row) { + await knex('updates').insert({ + available_version: null, + last_checked: null, + }); + } +} + +/** + * Update current and available versions in the database. + * Only works when `repository` in package.json is set to a GitHub repository + * @returns {Promise<void>} + */ +async function refreshVersions() { + await initUpdatesTable(); + 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; + + 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; + + const [, owner, repo] = parsedUrl.pathname.split('/'); + if (! owner || ! repo) return; + + https.get( + { + hostname: 'api.github.com', + path: `/repos/${owner}/${repo}/tags`, + headers: { + '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') + }); + 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 () => { + const parsed = JSON.parse(data); + if (! Array.isArray(parsed) || parsed.length === 0) return; + + const latestVersion = parsed[0].name; + await knex('updates').update({ + available_version: latestVersion, + last_checked: moment.utc().format('YYYY-MM-DD HH:mm:ss') + }); + }); + } + ); +} + +/** + * @param {String} a Version string, e.g. "2.20.0" + * @param {String} b Version string, e.g. "2.20.0" + * @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('.'); + 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); + if (aPart > bPart) return 1; + if (aPart < bPart) return -1; + } + return 0; +} + +async function getAvailableUpdate() { + await initUpdatesTable(); + + const packageJson = require('../../package.json'); + const currentVersion = packageJson.version; + const { available_version: availableVersion } = await knex('updates').first(); + if (availableVersion == null) return null; + if (currentVersion == null) return availableVersion; + + const versionDiff = compareVersions(currentVersion, availableVersion); + if (versionDiff === -1) return availableVersion; + + return null; +} + +async function refreshVersionsLoop() { + await refreshVersions(); + setTimeout(refreshVersionsLoop, UPDATE_CHECK_FREQUENCY * 60 * 60 * 1000); +} + +module.exports = { + getAvailableUpdate, + startVersionRefreshLoop: refreshVersionsLoop +}; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..dca80ac --- /dev/null +++ b/src/index.js @@ -0,0 +1,93 @@ +// 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.'); + process.exit(1); +} + +// Verify node modules have been installed +const fs = require('fs'); +const path = require('path'); + +try { + fs.accessSync(path.join(__dirname, '..', 'node_modules')); +} catch (e) { + console.error('Please run "npm install" before starting the bot'); + process.exit(1); +} + +// Error handling +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')) { + return; + } + + // For everything else, crash with the error + console.error(err); + process.exit(1); +}); + +let testedPackage = ''; +try { + const packageJson = require('../package.json'); + const modules = Object.keys(packageJson.dependencies); + modules.forEach(mod => { + testedPackage = mod; + fs.accessSync(path.join(__dirname, '..', 'node_modules', mod)) + }); +} catch (e) { + console.error(`Please run "npm install" again! Package "${testedPackage}" is missing.`); + process.exit(1); +} + +const config = require('./config'); +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 => { + 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 + await knex.migrate.latest(); + + // 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/knex.js b/src/knex.js new file mode 100644 index 0000000..b6b6346 --- /dev/null +++ b/src/knex.js @@ -0,0 +1,2 @@ +const config = require('./config'); +module.exports = require('knex')(config.knex); diff --git a/src/legacy/jsonDb.js b/src/legacy/jsonDb.js new file mode 100644 index 0000000..d2e8ca1 --- /dev/null +++ b/src/legacy/jsonDb.js @@ -0,0 +1,71 @@ +const fs = require('fs'); +const path = require('path'); +const config = require('../config'); + +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 new file mode 100644 index 0000000..2c491e3 --- /dev/null +++ b/src/legacy/legacyMigrator.js @@ -0,0 +1,222 @@ +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('../config'); +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, +}; diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..b8ef515 --- /dev/null +++ b/src/main.js @@ -0,0 +1,281 @@ +const Eris = require('eris'); +const path = require('path'); + +const config = require('./config'); +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 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 {ACCIDENTAL_THREAD_MESSAGES} = require('./data/constants'); + +module.exports = { + async start() { + console.log('Connecting to Discord...'); + + 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...'); + initStatus(); + initBaseMessageHandlers(); + initPlugins(); + + console.log(''); + console.log('Done! Now listening to DMs.'); + console.log(''); + }); + + bot.connect(); + } +}; + +function waitForGuild(guildId) { + if (bot.guilds.has(guildId)) { + return Promise.resolve(); + } + + return new Promise(resolve => { + bot.on('guildAvailable', guild => { + if (guild.id === guildId) { + resolve(); + } + }); + }); +} + +function initStatus() { + function applyStatus() { + bot.editStatus(null, {name: config.status}); + } + + // Set the bot status initially, then reapply it every hour since in some cases it gets unset + applyStatus(); + setInterval(applyStatus, 60 * 60 * 1000); +} + +function initBaseMessageHandlers() { + /** + * When a moderator posts in a modmail thread... + * 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 => { + if (! utils.messageIsOnInboxServer(msg)) return; + if (msg.author.bot) return; + + const thread = await threads.findByChannelId(msg.channel.id); + if (! thread) return; + + if (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) { + // 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 + + const replied = await thread.replyToUser(msg.member, msg.content.trim(), msg.attachments, config.alwaysReplyAnon || false); + if (replied) msg.delete(); + } else { + // Otherwise just save the messages as "chat" in the logs + thread.saveChatMessage(msg); + } + }); + + /** + * When we get a private message... + * 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 => { + if (! (msg.channel instanceof Eris.PrivateChannel)) return; + if (msg.author.bot) return; + if (msg.type !== 0) return; // Ignore pins etc. + + if (await blocked.isBlocked(msg.author.id)) return; + + // 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); + + + // 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); + } + + if (thread) await thread.receiveUserReply(msg); + }); + }); + + /** + * When a message is edited... + * 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) => { + 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 newContent = msg.content; + + // Ignore bogus edit events with no changes + if (newContent.trim() === oldContent.trim()) return; + + // 1) Edit in DMs + if (msg.channel instanceof Eris.PrivateChannel) { + const thread = await threads.findOpenThreadByUserId(msg.author.id); + if (! thread) return; + + const editMessage = utils.disableLinkPreviews(`**The user edited their message:**\n\`B:\` ${oldContent}\n\`A:\` ${newContent}`); + thread.postSystemMessage(editMessage); + } + + // 2) Edit in the thread + else if (utils.messageIsOnInboxServer(msg) && utils.isStaff(msg.member)) { + const thread = await threads.findOpenThreadByChannelId(msg.channel.id); + if (! thread) return; + + thread.updateChatMessage(msg); + } + }); + + /** + * When a staff message is deleted in a modmail thread, delete it from the database as well + */ + bot.on('messageDelete', async msg => { + if (! msg.author) return; + if (msg.author.bot) return; + if (! utils.messageIsOnInboxServer(msg)) return; + if (! utils.isStaff(msg.member)) return; + + const thread = await threads.findOpenThreadByChannelId(msg.channel.id); + if (! thread) return; + + thread.deleteChatMessage(msg.id); + }); + + /** + * When the bot is mentioned on the main server, ping staff in the log channel about it + */ + 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; + + if (utils.messageIsOnInboxServer(msg)) { + // For same server setups, check if the person who pinged modmail is staff. If so, ignore the ping. + if (utils.isStaff(msg.member)) return; + } else { + // For separate server setups, check if the member is staff on the modmail server + const inboxMember = utils.getInboxGuild().members.get(msg.author.id); + if (inboxMember && utils.isStaff(inboxMember)) return; + } + + // If the person who mentioned the bot is blocked, ignore them + if (await blocked.isBlocked(msg.author.id)) return; + + let content; + const mainGuilds = utils.getMainGuilds(); + 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<https:\/\/discordapp.com\/channels\/${msg.channel.guild.id}\/${msg.channel.id}\/${msg.id}>`; + } 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<https:\/\/discordapp.com\/channels\/${msg.channel.guild.id}\/${msg.channel.id}\/${msg.id}>`; + } + + + bot.createMessage(utils.getLogChannel().id, { + content, + disableEveryone: false, + }); + + // Send an auto-response to the mention, if enabled + if (config.botMentionResponse) { + const botMentionResponse = utils.readMultilineConfigValue(config.botMentionResponse); + bot.createMessage(msg.channel.id, botMentionResponse.replace(/{userMention}/g, `<@${msg.author.id}>`)); + } + }); +} + +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); + } + } + + // Load plugins + console.log('Loading plugins'); + const builtInPlugins = [ + reply, + close, + logs, + block, + move, + snippets, + suspend, + greeting, + webserver, + typingProxy, + version, + newthread, + idModule, + alert + ]; + + const plugins = [...builtInPlugins]; + + if (config.plugins && config.plugins.length) { + for (const plugin of config.plugins) { + const pluginFn = require(`../${plugin}`); + plugins.push(pluginFn); + } + } + + 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)`); + + if (config.updateNotifications) { + updates.startVersionRefreshLoop(); + } +} diff --git a/src/modules/alert.js b/src/modules/alert.js new file mode 100644 index 0000000..a844230 --- /dev/null +++ b/src/modules/alert.js @@ -0,0 +1,11 @@ +module.exports = ({ bot, knex, config, commands }) => { + commands.addInboxThreadCommand('alert', '[opt:string]', async (msg, args, thread) => { + if (args.opt && args.opt.startsWith('c')) { + await thread.setAlert(null); + await thread.postSystemMessage(`Cancelled new message alert`); + } else { + 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 new file mode 100644 index 0000000..94913c4 --- /dev/null +++ b/src/modules/block.js @@ -0,0 +1,99 @@ +const humanizeDuration = require('humanize-duration'); +const moment = require('moment'); +const blocked = require("../data/blocked"); +const utils = require("../utils"); + +module.exports = ({ bot, knex, config, commands }) => { + async function removeExpiredBlocks() { + const expiredBlocks = await blocked.getExpiredBlocks(); + const logChannel = utils.getLogChannel(); + for (const userId of expiredBlocks) { + await blocked.unblock(userId); + logChannel.createMessage(`Block of <@!${userId}> (id \`${userId}\`) expired`); + } + } + + async function expiredBlockLoop() { + try { + removeExpiredBlocks(); + } catch (e) { + console.error(e); + } + + setTimeout(expiredBlockLoop, 2000); + } + + bot.on('ready', expiredBlockLoop); + + const blockCmd = async (msg, args, thread) => { + const userIdToBlock = args.userId || (thread && thread.user_id); + if (! userIdToBlock) return; + + const isBlocked = await blocked.isBlocked(userIdToBlock); + if (isBlocked) { + 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') + : null; + + const user = bot.users.get(userIdToBlock); + await blocked.block(userIdToBlock, (user ? `${user.username}#${user.discriminator}` : ''), msg.author.id, expiresAt); + + if (expiresAt) { + const humanized = humanizeDuration(args.blockTime, { largest: 2, round: true }); + msg.channel.createMessage(`Blocked <@${userIdToBlock}> (id \`${userIdToBlock}\`) from modmail for ${humanized}`); + } else { + msg.channel.createMessage(`Blocked <@${userIdToBlock}> (id \`${userIdToBlock}\`) from modmail indefinitely`); + } + }; + + commands.addInboxServerCommand('block', '<userId:userId> [blockTime:delay]', blockCmd); + commands.addInboxServerCommand('block', '[blockTime:delay]', blockCmd); + + const unblockCmd = async (msg, args, thread) => { + const userIdToUnblock = args.userId || (thread && thread.user_id); + if (! userIdToUnblock) return; + + const isBlocked = await blocked.isBlocked(userIdToUnblock); + if (! isBlocked) { + 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') + : null; + + const user = bot.users.get(userIdToUnblock); + if (unblockAt) { + const humanized = humanizeDuration(args.unblockDelay, { largest: 2, round: true }); + await blocked.updateExpiryTime(userIdToUnblock, unblockAt); + msg.channel.createMessage(`Scheduled <@${userIdToUnblock}> (id \`${userIdToUnblock}\`) to be unblocked in ${humanized}`); + } else { + await blocked.unblock(userIdToUnblock); + msg.channel.createMessage(`Unblocked <@${userIdToUnblock}> (id ${userIdToUnblock}) from modmail`); + } + }; + + commands.addInboxServerCommand('unblock', '<userId:userId> [unblockDelay:delay]', unblockCmd); + commands.addInboxServerCommand('unblock', '[unblockDelay:delay]', unblockCmd); + + commands.addInboxServerCommand('is_blocked', '[userId:userId]',async (msg, args, thread) => { + const userIdToCheck = args.userId || (thread && thread.user_id); + if (! userIdToCheck) return; + + const blockStatus = await blocked.getBlockStatus(userIdToCheck); + if (blockStatus.isBlocked) { + if (blockStatus.expiresAt) { + msg.channel.createMessage(`<@!${userIdToCheck}> (id \`${userIdToCheck}\`) is blocked until ${blockStatus.expiresAt} (UTC)`); + } else { + msg.channel.createMessage(`<@!${userIdToCheck}> (id \`${userIdToCheck}\`) is blocked indefinitely`); + } + } else { + msg.channel.createMessage(`<@!${userIdToCheck}> (id \`${userIdToCheck}\`) is NOT blocked`); + } + }); +}; diff --git a/src/modules/close.js b/src/modules/close.js new file mode 100644 index 0000000..b3e7421 --- /dev/null +++ b/src/modules/close.js @@ -0,0 +1,153 @@ +const moment = require('moment'); +const Eris = require('eris'); +const config = require('../config'); +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 + async function applyScheduledCloses() { + const threadsToBeClosed = await threads.getThreadsThatShouldBeClosed(); + for (const thread of threadsToBeClosed) { + if (config.closeMessage && ! thread.scheduled_close_silent) { + const closeMessage = utils.readMultilineConfigValue(config.closeMessage); + await thread.postToUser(closeMessage).catch(() => {}); + } + + 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} + `)); + } + } + + async function scheduledCloseLoop() { + try { + await applyScheduledCloses(); + } catch (e) { + console.error(e); + } + + setTimeout(scheduledCloseLoop, 2000); + } + + 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) => { + let thread, closedBy; + + let hasCloseMessage = !! config.closeMessage; + let silentClose = false; + + if (msg.channel instanceof Eris.PrivateChannel) { + // User is closing the thread by themselves (if enabled) + if (! config.allowUserClose) return; + if (await blocked.isBlocked(msg.author.id)) return; + + thread = await threads.findOpenThreadByUserId(msg.author.id); + if (! thread) return; + + // 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...'); + await thread.close(true); + }); + + closedBy = 'the user'; + } else { + // A staff member is closing the thread + if (! utils.messageIsOnInboxServer(msg)) return; + if (! utils.isStaff(msg.member)) return; + + thread = await threads.findOpenThreadByChannelId(msg.channel.id); + if (! thread) return; + + if (args.opts && args.opts.length) { + if (args.opts.includes('cancel') || args.opts.includes('c')) { + // Cancel timed close + if (thread.scheduled_close_at) { + await thread.cancelScheduledClose(); + thread.postSystemMessage(`Cancelled scheduled closing`); + } + + return; + } + + // Silent close (= no close message) + if (args.opts.includes('silent') || args.opts.includes('s')) { + silentClose = true; + } + + // Timed close + const delayStringArg = args.opts.find(arg => utils.delayStringRegex.test(arg)); + if (delayStringArg) { + const delay = utils.convertDelayStringToMS(delayStringArg); + if (delay === 0 || delay === null) { + thread.postSystemMessage(`Invalid delay specified. Format: "1h30m"`); + return; + } + + const 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) { + response = `Thread is now scheduled to be closed silently in ${utils.humanizeDelay(delay)}. Use \`${config.prefix}close cancel\` to cancel.`; + } else { + response = `Thread is now scheduled to be closed in ${utils.humanizeDelay(delay)}. Use \`${config.prefix}close cancel\` to cancel.`; + } + + thread.postSystemMessage(response); + + return; + } + } + + // Regular close + await thread.close(false, silentClose); + closedBy = msg.author.username; + } + + // Send close message (unless suppressed with a silent close) + if (hasCloseMessage && ! silentClose) { + const closeMessage = utils.readMultilineConfigValue(config.closeMessage); + await thread.postToUser(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} + `)); + }); + + // Auto-close threads if their channel is deleted + bot.on('channelDelete', async (channel) => { + if (! (channel instanceof Eris.TextChannel)) return; + if (channel.guild.id !== utils.getInboxGuild().id) return; + + const thread = await threads.findOpenThreadByChannelId(channel.id); + if (! thread) return; + + console.log(`[INFO] Auto-closing thread with ${thread.user_name} because the channel was deleted`); + if (config.closeMessage) { + const closeMessage = utils.readMultilineConfigValue(config.closeMessage); + await thread.postToUser(closeMessage).catch(() => {}); + } + + 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} + `)); + }); +}; diff --git a/src/modules/greeting.js b/src/modules/greeting.js new file mode 100644 index 0000000..f85bde9 --- /dev/null +++ b/src/modules/greeting.js @@ -0,0 +1,37 @@ +const path = require('path'); +const fs = require('fs'); +const config = require('../config'); +const utils = require('../utils'); + +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; + + function sendGreeting(message, file) { + bot.getDMChannel(member.id).then(channel => { + if (! channel) return; + + channel.createMessage(message || '', file) + .catch(e => { + if (e.code === 50007) return; + throw e; + }); + }); + } + + const greetingMessage = utils.readMultilineConfigValue(guildGreeting.message); + + if (guildGreeting.attachment) { + const filename = path.basename(guildGreeting.attachment); + fs.readFile(guildGreeting.attachment, (err, data) => { + const file = {file: data, name: filename}; + sendGreeting(greetingMessage, file); + }); + } else { + sendGreeting(greetingMessage); + } + }); +}; diff --git a/src/modules/id.js b/src/modules/id.js new file mode 100644 index 0000000..e3cf2e2 --- /dev/null +++ b/src/modules/id.js @@ -0,0 +1,5 @@ +module.exports = ({ bot, knex, config, commands }) => { + commands.addInboxThreadCommand('id', [], async (msg, args, thread) => { + thread.postSystemMessage(thread.user_id); + }); +}; diff --git a/src/modules/logs.js b/src/modules/logs.js new file mode 100644 index 0000000..0664c07 --- /dev/null +++ b/src/modules/logs.js @@ -0,0 +1,69 @@ +const threads = require("../data/threads"); +const moment = require('moment'); +const utils = require("../utils"); + +const LOG_LINES_PER_PAGE = 10; + +module.exports = ({ bot, knex, config, commands }) => { + const logsCmd = async (msg, args, thread) => { + let userId = args.userId || (thread && thread.user_id); + if (! userId) return; + + let userThreads = await threads.getClosedThreadsByUserId(userId); + + // Descending by date + userThreads.sort((a, b) => { + if (a.created_at > b.created_at) return -1; + if (a.created_at < b.created_at) return 1; + return 0; + }); + + // Pagination + const totalUserThreads = userThreads.length; + const maxPage = Math.ceil(totalUserThreads / LOG_LINES_PER_PAGE); + const inputPage = args.page; + const page = Math.max(Math.min(inputPage ? parseInt(inputPage, 10) : 1, maxPage), 1); // Clamp page to 1-<max page> + const isPaginated = totalUserThreads > LOG_LINES_PER_PAGE; + const start = (page - 1) * LOG_LINES_PER_PAGE; + const end = page * LOG_LINES_PER_PAGE; + 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 formattedDate = moment.utc(thread.created_at).format('MMM Do [at] HH:mm [UTC]'); + return `\`${formattedDate}\`: <${logUrl}>`; + })); + + let message = isPaginated + ? `**Log files for <@${userId}>** (page **${page}/${maxPage}**, showing logs **${start + 1}-${end}/${totalUserThreads}**):` + : `**Log files for <@${userId}>:**`; + + message += `\n${threadLines.join('\n')}`; + + if (isPaginated) { + 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 chunks = utils.chunk(lines, 15); + + let root = Promise.resolve(); + chunks.forEach(lines => { + root = root.then(() => msg.channel.createMessage(lines.join('\n'))); + }); + }; + + commands.addInboxServerCommand('logs', '<userId:userId> [page:number]', logsCmd); + commands.addInboxServerCommand('logs', '[page:number]', logsCmd); + + commands.addInboxServerCommand('loglink', [], async (msg, args, thread) => { + if (! thread) { + thread = await threads.findSuspendedThreadByChannelId(msg.channel.id); + if (! thread) return; + } + + const logUrl = await thread.getLogUrl(); + thread.postSystemMessage(`Log URL: ${logUrl}`); + }); +}; diff --git a/src/modules/move.js b/src/modules/move.js new file mode 100644 index 0000000..973143d --- /dev/null +++ b/src/modules/move.js @@ -0,0 +1,82 @@ +const config = require('../config'); +const Eris = require('eris'); +const transliterate = require("transliteration"); +const erisEndpoints = require('eris/lib/rest/Endpoints'); + +module.exports = ({ bot, knex, config, commands }) => { + if (! config.allowMove) return; + + commands.addInboxThreadCommand('move', '<category:string$>', async (msg, args, thread) => { + const searchStr = args.category; + const normalizedSearchStr = transliterate.slugify(searchStr); + + const categories = msg.channel.guild.channels.filter(c => { + // Filter to categories that are not the thread's current parent category + return (c instanceof Eris.CategoryChannel) && (c.id !== msg.channel.parentID); + }); + + if (categories.length === 0) return; + + // See if any category name contains a part of the search string + const containsRankings = categories.map(cat => { + const normalizedCatName = transliterate.slugify(cat.name); + + let i = 0; + do { + if (! normalizedCatName.includes(normalizedSearchStr.slice(0, i + 1))) break; + i++; + } while (i < normalizedSearchStr.length); + + if (i > 0 && normalizedCatName.startsWith(normalizedSearchStr.slice(0, i))) { + // Slightly prioritize categories that *start* with the search string + i += 0.5; + } + + return [cat, i]; + }); + + // Sort by best match + containsRankings.sort((a, b) => { + return a[1] > b[1] ? -1 : 1; + }); + + if (containsRankings[0][1] === 0) { + thread.postSystemMessage('No matching category'); + return; + } + + const targetCategory = containsRankings[0][0]; + + try { + await bot.editChannel(thread.channel_id, { + parentID: targetCategory.id + }); + } catch (e) { + thread.postSystemMessage(`Failed to move thread: ${e.message}`); + return; + } + + // If enabled, sync thread channel permissions with the category it's moved to + if (config.syncPermissionsOnMove) { + const newPerms = Array.from(targetCategory.permissionOverwrites.map(ow => { + return { + id: ow.id, + type: ow.type, + allow: ow.allow, + deny: ow.deny + }; + })); + + try { + await bot.requestHandler.request("PATCH", erisEndpoints.CHANNEL(thread.channel_id), true, { + permission_overwrites: newPerms + }); + } catch (e) { + thread.postSystemMessage(`Thread moved to ${targetCategory.name.toUpperCase()}, but failed to sync permissions: ${e.message}`); + return; + } + } + + thread.postSystemMessage(`Thread moved to ${targetCategory.name.toUpperCase()}`); + }); +}; diff --git a/src/modules/newthread.js b/src/modules/newthread.js new file mode 100644 index 0000000..aca6f54 --- /dev/null +++ b/src/modules/newthread.js @@ -0,0 +1,23 @@ +const utils = require("../utils"); +const threads = require("../data/threads"); + +module.exports = ({ bot, knex, config, commands }) => { + commands.addInboxServerCommand('newthread', '<userId:userId>', async (msg, args, thread) => { + const user = bot.users.get(args.userId); + if (! user) { + utils.postSystemMessageWithFallback(msg.channel, thread, 'User not found!'); + return; + } + + 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}>`); + return; + } + + const createdThread = await threads.createNewThreadForUser(user, true, true); + createdThread.postSystemMessage(`Thread was opened by ${msg.author.username}#${msg.author.discriminator}`); + + msg.channel.createMessage(`Thread opened: <#${createdThread.channel_id}>`); + }); +}; diff --git a/src/modules/reply.js b/src/modules/reply.js new file mode 100644 index 0000000..bc2afe3 --- /dev/null +++ b/src/modules/reply.js @@ -0,0 +1,32 @@ +const attachments = require("../data/attachments"); +const utils = require('../utils'); + +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) => { + if (! args.text && msg.attachments.length === 0) { + utils.postError(msg.channel, 'Text or attachment required'); + return; + } + + const replied = await thread.replyToUser(msg.member, args.text || '', msg.attachments, false); + if (replied) msg.delete(); + }, { + 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) { + utils.postError(msg.channel, 'Text or attachment required'); + return; + } + + const replied = await thread.replyToUser(msg.member, args.text || '', msg.attachments, true); + if (replied) msg.delete(); + }, { + aliases: ['ar'] + }); +}; diff --git a/src/modules/snippets.js b/src/modules/snippets.js new file mode 100644 index 0000000..e9c1271 --- /dev/null +++ b/src/modules/snippets.js @@ -0,0 +1,138 @@ +const threads = require('../data/threads'); +const snippets = require('../data/snippets'); +const config = require('../config'); +const utils = require('../utils'); +const { parseArguments } = require('knub-command-manager'); + +const whitespaceRegex = /\s/; +const quoteChars = ["'", '"']; + +module.exports = ({ bot, knex, config, commands }) => { + /** + * "Renders" a snippet by replacing all argument placeholders e.g. {1} {2} with their corresponding arguments. + * The number in the placeholder is the argument's order in the argument list, i.e. {1} is the first argument (= index 0) + * @param {String} body + * @param {String[]} args + * @returns {String} + */ + function renderSnippet(body, args) { + return body + .replace(/(?<!\\){\d+}/g, match => { + const index = parseInt(match.slice(1, -1), 10) - 1; + return (args[index] != null ? args[index] : match); + }) + .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 => { + if (! utils.messageIsOnInboxServer(msg)) return; + if (! utils.isStaff(msg.member)) return; + + if (msg.author.bot) return; + if (! msg.content) return; + if (! msg.content.startsWith(config.snippetPrefix) && ! msg.content.startsWith(config.snippetPrefixAnon)) return; + + let snippetPrefix, isAnonymous; + + if (config.snippetPrefixAnon.length > config.snippetPrefix.length) { + // Anonymous prefix is longer -> check it first + if (msg.content.startsWith(config.snippetPrefixAnon)) { + snippetPrefix = config.snippetPrefixAnon; + isAnonymous = true; + } else { + snippetPrefix = config.snippetPrefix; + isAnonymous = false; + } + } else { + // Regular prefix is longer -> check it first + if (msg.content.startsWith(config.snippetPrefix)) { + snippetPrefix = config.snippetPrefix; + isAnonymous = false; + } else { + snippetPrefix = config.snippetPrefixAnon; + isAnonymous = true; + } + } + + const thread = await threads.findByChannelId(msg.channel.id); + if (! thread) return; + + let [, trigger, rawArgs] = msg.content.slice(snippetPrefix.length).match(/(\S+)(?:\s+(.*))?/s); + trigger = trigger.toLowerCase(); + + const snippet = await snippets.get(trigger); + if (! snippet) return; + + let args = rawArgs ? parseArguments(rawArgs) : []; + args = args.map(arg => arg.value); + const rendered = renderSnippet(snippet.body, args); + + const replied = await thread.replyToUser(msg.member, rendered, [], isAnonymous); + if (replied) msg.delete(); + }); + + // Show or add a snippet + commands.addInboxServerCommand('snippet', '<trigger> [text$]', async (msg, args, thread) => { + const snippet = await snippets.get(args.trigger); + + if (snippet) { + if (args.text) { + // If the snippet exists and we're trying to create a new one, inform the user the snippet already exists + utils.postSystemMessageWithFallback(msg.channel, thread, `Snippet "${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)}\`\`\``); + } + } else { + if (args.text) { + // If the snippet doesn't exist and the user wants to create it, create it + await snippets.add(args.trigger, args.text, msg.author.id); + utils.postSystemMessageWithFallback(msg.channel, thread, `Snippet "${args.trigger}" created!`); + } else { + // If the snippet doesn't exist and the user isn't trying to create it, inform them how to create it + utils.postSystemMessageWithFallback(msg.channel, thread, `Snippet "${args.trigger}" doesn't exist! You can create it with \`${config.prefix}snippet ${args.trigger} text\``); + } + } + }, { + aliases: ['s'] + }); + + commands.addInboxServerCommand('delete_snippet', '<trigger>', async (msg, args, thread) => { + const snippet = await snippets.get(args.trigger); + if (! snippet) { + utils.postSystemMessageWithFallback(msg.channel, thread, `Snippet "${args.trigger}" doesn't exist!`); + return; + } + + await snippets.del(args.trigger); + utils.postSystemMessageWithFallback(msg.channel, thread, `Snippet "${args.trigger}" deleted!`); + }, { + aliases: ['ds'] + }); + + commands.addInboxServerCommand('edit_snippet', '<trigger> [text$]', async (msg, args, thread) => { + const snippet = await snippets.get(args.trigger); + if (! snippet) { + utils.postSystemMessageWithFallback(msg.channel, thread, `Snippet "${args.trigger}" doesn't exist!`); + return; + } + + await snippets.del(args.trigger); + await snippets.add(args.trigger, args.text, msg.author.id); + + utils.postSystemMessageWithFallback(msg.channel, thread, `Snippet "${args.trigger}" edited!`); + }, { + aliases: ['es'] + }); + + 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(', ')}`); + }); +}; diff --git a/src/modules/suspend.js b/src/modules/suspend.js new file mode 100644 index 0000000..7edd096 --- /dev/null +++ b/src/modules/suspend.js @@ -0,0 +1,77 @@ +const moment = require('moment'); +const threads = require("../data/threads"); +const utils = require('../utils'); +const config = require('../config'); + +const {THREAD_STATUS} = require('../data/constants'); + +module.exports = ({ bot, knex, config, commands }) => { + // Check for threads that are scheduled to be suspended and suspend them + async function applyScheduledSuspensions() { + const threadsToBeSuspended = await threads.getThreadsThatShouldBeSuspended(); + for (const thread of threadsToBeSuspended) { + if (thread.status === THREAD_STATUS.OPEN) { + await thread.suspend(); + await thread.postSystemMessage(`**Thread suspended** as scheduled by ${thread.scheduled_suspend_name}. This thread will act as closed until unsuspended with \`!unsuspend\``); + } + } + } + + async function scheduledSuspendLoop() { + try { + await applyScheduledSuspensions(); + } catch (e) { + console.error(e); + } + + setTimeout(scheduledSuspendLoop, 2000); + } + + scheduledSuspendLoop(); + + commands.addInboxThreadCommand('suspend cancel', [], async (msg, args, thread) => { + // Cancel timed suspend + if (thread.scheduled_suspend_at) { + await thread.cancelScheduledSuspend(); + thread.postSystemMessage(`Cancelled scheduled suspension`); + } else { + thread.postSystemMessage(`Thread is not scheduled to be suspended`); + } + }); + + 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); + + thread.postSystemMessage(`Thread will be suspended in ${utils.humanizeDelay(args.delay)}. Use \`${config.prefix}suspend cancel\` to cancel.`); + + return; + } + + await thread.suspend(); + thread.postSystemMessage(`**Thread suspended!** This thread will act as closed until unsuspended with \`!unsuspend\``); + }); + + commands.addInboxServerCommand('unsuspend', [], async (msg, args, thread) => { + if (thread) { + thread.postSystemMessage(`Thread is not suspended`); + return; + } + + thread = await threads.findSuspendedThreadByChannelId(msg.channel.id); + if (! thread) { + thread.postSystemMessage(`Not in a thread`); + return; + } + + const otherOpenThread = await threads.findOpenThreadByUserId(thread.user_id); + if (otherOpenThread) { + thread.postSystemMessage(`Cannot unsuspend; there is another open thread with this user: <#${otherOpenThread.channel_id}>`); + return; + } + + await thread.unsuspend(); + thread.postSystemMessage(`**Thread unsuspended!**`); + }); +}; diff --git a/src/modules/typingProxy.js b/src/modules/typingProxy.js new file mode 100644 index 0000000..5881808 --- /dev/null +++ b/src/modules/typingProxy.js @@ -0,0 +1,33 @@ +const config = require('../config'); +const threads = require("../data/threads"); +const Eris = require("eris"); + +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) => { + // 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); + if (! thread) return; + + try { + await bot.sendChannelTyping(thread.channel_id); + } catch (e) {} + } + + // config.typingProxyReverse: forward moderator typing in a thread to the DM + else if (config.typingProxyReverse && (channel instanceof Eris.GuildChannel) && ! user.bot) { + const thread = await threads.findByChannelId(channel.id); + if (! thread) return; + + const dmChannel = await thread.getDMChannel(); + if (! dmChannel) return; + + try { + await bot.sendChannelTyping(dmChannel.id); + } catch(e) {} + } + }); + } +}; diff --git a/src/modules/version.js b/src/modules/version.js new file mode 100644 index 0000000..033fbf0 --- /dev/null +++ b/src/modules/version.js @@ -0,0 +1,53 @@ +const path = require('path'); +const fs = require('fs'); +const {promisify} = require('util'); +const utils = require("../utils"); +const updates = require('../data/updates'); +const config = require('../config'); + +const access = promisify(fs.access); +const readFile = promisify(fs.readFile); + +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)})`; + } + + if (config.updateNotifications) { + const availableUpdate = await updates.getAvailableUpdate(); + if (availableUpdate) { + response += ` (version ${availableUpdate} available)`; + } + } + + utils.postSystemMessageWithFallback(msg.channel, thread, response); + }); +}; diff --git a/src/modules/webserver.js b/src/modules/webserver.js new file mode 100644 index 0000000..5d8b2c9 --- /dev/null +++ b/src/modules/webserver.js @@ -0,0 +1,92 @@ +const http = require('http'); +const mime = require('mime'); +const url = require('url'); +const fs = require('fs'); +const moment = require('moment'); +const config = require('../config'); +const threads = require('../data/threads'); +const attachments = require('../data/attachments'); + +const {THREAD_MESSAGE_TYPE} = require('../data/constants'); + +function notfound(res) { + res.statusCode = 404; + res.end('Page Not Found'); +} + +async function serveLogs(res, pathParts) { + const threadId = pathParts[pathParts.length - 1]; + if (threadId.match(/^[0-9a-f\-]+$/) === null) return notfound(res); + + const thread = await threads.findById(threadId); + if (! thread) return notfound(res); + + const threadMessages = await thread.getThreadMessages(); + 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 (message.message_type === THREAD_MESSAGE_TYPE.SYSTEM) { + // System messages don't need the username + line += message.body; + } else 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 { + line += `${message.user_name}: ${message.body}`; + } + + return line; + }); + + res.setHeader('Content-Type', 'text/plain; charset=UTF-8'); + res.end(lines.join('\n')); +} + +function serveAttachments(res, pathParts) { + const desiredFilename = pathParts[pathParts.length - 1]; + const id = pathParts[pathParts.length - 2]; + + 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); + fs.access(attachmentPath, (err) => { + if (err) return notfound(res); + + const filenameParts = desiredFilename.split('.'); + const ext = (filenameParts.length > 1 ? filenameParts[filenameParts.length - 1] : 'bin'); + const fileMime = mime.getType(ext); + + res.setHeader('Content-Type', fileMime); + + const read = fs.createReadStream(attachmentPath); + read.pipe(res); + }) +} + +module.exports = () => { + const server = http.createServer((req, res) => { + const parsedUrl = url.parse(`http://${req.url}`); + const pathParts = parsedUrl.path.split('/').filter(v => v !== ''); + + if (parsedUrl.path.startsWith('/logs/')) { + serveLogs(res, pathParts); + } else if (parsedUrl.path.startsWith('/attachments/')) { + serveAttachments(res, pathParts); + } else { + notfound(res); + } + }); + + 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 new file mode 100644 index 0000000..64c9df6 --- /dev/null +++ b/src/plugins.js @@ -0,0 +1,26 @@ +const attachments = require('./data/attachments'); + +module.exports = { + getPluginAPI({ bot, knex, config, commands }) { + return { + bot, + knex, + config, + commands: { + manager: commands.manager, + addGlobalCommand: commands.addGlobalCommand, + addInboxServerCommand: commands.addInboxServerCommand, + addInboxThreadCommand: commands.addInboxThreadCommand, + addAlias: commands.addAlias + }, + attachments: { + addStorageType: attachments.addStorageType, + downloadAttachment: attachments.downloadAttachment + }, + }; + }, + + loadPlugin(plugin, api) { + plugin(api); + } +}; diff --git a/src/queue.js b/src/queue.js new file mode 100644 index 0000000..589425e --- /dev/null +++ b/src/queue.js @@ -0,0 +1,40 @@ +class Queue { + constructor() { + this.running = false; + this.queue = []; + } + + add(fn) { + const promise = new Promise(resolve => { + this.queue.push(async () => { + await Promise.resolve(fn()); + resolve(); + }); + + if (! this.running) this.next(); + }); + + return promise; + } + + next() { + this.running = true; + + if (this.queue.length === 0) { + this.running = false; + return; + } + + const fn = this.queue.shift(); + new Promise(resolve => { + // Either fn() completes or the timeout of 10sec is reached + fn().then(resolve); + setTimeout(resolve, 10000); + }).then(() => this.next()); + } +} + +module.exports = { + Queue, + messageQueue: new Queue() +}; diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..3e475ab --- /dev/null +++ b/src/utils.js @@ -0,0 +1,357 @@ +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('./config'); + +class BotError extends Error {} + +const userMentionRegex = /^<@!?([0-9]+?)>$/; + +let inboxGuild = null; +let mainGuilds = []; +let logChannel = null; + +/** + * @returns {Eris~Guild} + */ +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!'); + return inboxGuild; +} + +/** + * @returns {Eris~Guild[]} + */ +function getMainGuilds() { + if (mainGuilds.length === 0) { + mainGuilds = bot.guilds.filter(g => config.mainGuildId.includes(g.id)); + } + + if (mainGuilds.length !== config.mainGuildId.length) { + if (config.mainGuildId.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!`); + } + } + + return mainGuilds; +} + +/** + * Returns the designated log channel, or the default channel if none is set + * @returns {Eris~TextChannel} + */ +function getLogChannel() { + const inboxGuild = getInboxGuild(); + const logChannel = inboxGuild.channels.get(config.logChannelId); + + if (! logChannel) { + 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!'); + } + + return logChannel; +} + +function postLog(...args) { + getLogChannel().createMessage(...args); +} + +function postError(channel, str, opts = {}) { + return channel.createMessage({ + ...opts, + content: `⚠ ${str}` + }); +} + +/** + * Returns whether the given member has permission to use modmail commands + * @param member + * @returns {boolean} + */ +function isStaff(member) { + if (! member) return false; + if (config.inboxServerPermission.length === 0) return true; + + return config.inboxServerPermission.some(perm => { + if (isSnowflake(perm)) { + // If perm is a snowflake, check it against the member's user id and roles + if (member.id === perm) return true; + if (member.roles.includes(perm)) return true; + } else { + // Otherwise assume perm is the name of a permission + return member.permission.has(perm); + } + + return false; + }); +} + +/** + * Returns whether the given message is on the inbox server + * @param msg + * @returns {boolean} + */ +function messageIsOnInboxServer(msg) { + if (! msg.channel.guild) return false; + if (msg.channel.guild.id !== getInboxGuild().id) return false; + return true; +} + +/** + * Returns whether the given message is on the main server + * @param msg + * @returns {boolean} + */ +function messageIsOnMainServer(msg) { + if (! msg.channel.guild) return false; + + return getMainGuilds() + .some(g => msg.channel.guild.id === g.id); +} + +/** + * @param attachment + * @returns {Promise<string>} + */ +async function formatAttachment(attachment, attachmentUrl) { + let filesize = attachment.size || 0; + filesize /= 1024; + + return `**Attachment:** ${attachment.filename} (${filesize.toFixed(1)}KB)\n${attachmentUrl}`; +} + +/** + * Returns the user ID of the user mentioned in str, if any + * @param {String} str + * @returns {String|null} + */ +function getUserMention(str) { + if (! str) return null; + + str = str.trim(); + + if (isSnowflake(str)) { + // User ID + return str; + } else { + let mentionMatch = str.match(userMentionRegex); + if (mentionMatch) return mentionMatch[1]; + } + + return null; +} + +/** + * Returns the current timestamp in an easily readable form + * @returns {String} + */ +function getTimestamp(...momentArgs) { + return moment.utc(...momentArgs).format('HH:mm'); +} + +/** + * Disables link previews in the given string by wrapping links in < > + * @param {String} str + * @returns {String} + */ +function disableLinkPreviews(str) { + return str.replace(/(^|[^<])(https?:\/\/\S+)/ig, '$1<$2>'); +} + +/** + * Returns a URL to the bot's web server + * @param {String} path + * @returns {Promise<String>} + */ +async function getSelfUrl(path = '') { + if (config.url) { + return `${config.url}/${path}`; + } else { + const port = config.port || 8890; + const ip = await publicIp.v4(); + return `http://${ip}:${port}/${path}`; + } +} + +/** + * Returns the highest hoisted role of the given member + * @param {Eris~Member} member + * @returns {Eris~Role} + */ +function getMainRole(member) { + const roles = member.roles.map(id => member.guild.roles.get(id)); + roles.sort((a, b) => a.position > b.position ? -1 : 1); + return roles.find(r => r.hoist); +} + +/** + * Splits array items into chunks of the specified size + * @param {Array|String} items + * @param {Number} chunkSize + * @returns {Array} + */ +function chunk(items, chunkSize) { + const result = []; + + for (let i = 0; i < items.length; i += chunkSize) { + result.push(items.slice(i, i + chunkSize)); + } + + return result; +} + +/** + * Trims every line in the string + * @param {String} str + * @returns {String} + */ +function trimAll(str) { + return str + .split('\n') + .map(str => str.trim()) + .join('\n'); +} + +const delayStringRegex = /^([0-9]+)(?:([dhms])[a-z]*)?/i; + +/** + * Turns a "delay string" such as "1h30m" to milliseconds + * @param {String} str + * @returns {Number|null} + */ +function convertDelayStringToMS(str) { + let match; + let ms = 0; + + 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; + + str = str.slice(match[0].length); + } + + // Invalid delay string + if (str !== '') { + return null; + } + + return ms; +} + +function getInboxMention() { + const mentionRoles = Array.isArray(config.mentionRole) ? config.mentionRole : [config.mentionRole]; + 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 mentions.push(`<@&${role}>`); + } + return mentions.join(' ') + ' '; +} + +function postSystemMessageWithFallback(channel, thread, text) { + if (thread) { + thread.postSystemMessage(text); + } else { + channel.createMessage(text); + } +} + +/** + * A normalized way to set props in data models, fixing some inconsistencies between different DB drivers in knex + * @param {Object} target + * @param {Object} props + */ +function setDataModelProps(target, props) { + for (const prop in props) { + if (! props.hasOwnProperty(prop)) continue; + // DATETIME fields are always returned as Date objects in MySQL/MariaDB + if (props[prop] instanceof Date) { + // ...even when NULL, in which case the date's set to unix epoch + if (props[prop].getUTCFullYear() === 1970) { + 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'); + } + } else { + target[prop] = props[prop]; + } + } +} + +const snowflakeRegex = /^[0-9]{17,}$/; +function isSnowflake(str) { + return str && snowflakeRegex.test(str); +} + +const humanizeDelay = (delay, opts = {}) => humanizeDuration(delay, Object.assign({conjunction: ' and '}, opts)); + +const markdownCharsRegex = /([\\_*|`~])/g; +function escapeMarkdown(str) { + return str.replace(markdownCharsRegex, '\\$1'); +} + +function disableCodeBlocks(str) { + return str.replace(/`/g, "`\u200b"); +} + +/** + * + */ +function readMultilineConfigValue(str) { + return Array.isArray(str) ? str.join('\n') : str; +} + +module.exports = { + BotError, + + getInboxGuild, + getMainGuilds, + getLogChannel, + postError, + postLog, + + isStaff, + messageIsOnInboxServer, + messageIsOnMainServer, + + formatAttachment, + + getUserMention, + getTimestamp, + disableLinkPreviews, + getSelfUrl, + getMainRole, + delayStringRegex, + convertDelayStringToMS, + getInboxMention, + postSystemMessageWithFallback, + + chunk, + trimAll, + + setDataModelProps, + + isSnowflake, + + humanizeDelay, + + escapeMarkdown, + disableCodeBlocks, + + readMultilineConfigValue, +}; diff --git a/start.bat b/start.bat new file mode 100644 index 0000000..40accb6 --- /dev/null +++ b/start.bat @@ -0,0 +1,17 @@ +@echo off + +echo Installing/updating bot dependencies +call npm ci --only=production --loglevel=warn >NUL + +if NOT ["%errorlevel%"]==["0"] ( + pause + exit /b %errorlevel% +) + +echo Starting the bot +call npm run start + +if NOT ["%errorlevel%"]==["0"] ( + pause + exit /b %errorlevel% +) |