From dab194f8f51a3e6fb34b5908938b98ca42bcd44f Mon Sep 17 00:00:00 2001 From: Snazzah Date: Mon, 10 Aug 2020 23:08:26 -0500 Subject: [PATCH] Initial command base --- .eslintignore | 2 + .eslintrc.json | 79 ++ .gitattributes | 2 + .github/dependabot.yml | 12 + .github/workflows/eslint.yml | 32 + .gitignore | 5 + LICENSE | 21 + README.md | 27 +- config/_default.js | 40 + package.json | 33 + pm2.json | 9 + src/bot.js | 148 ++++ src/commandloader.js | 100 +++ src/commands/help.js | 101 +++ src/commands/owner/asynceval.js | 38 + src/commands/owner/eval.js | 35 + src/commands/owner/exec.js | 34 + src/commands/owner/reload.js | 23 + src/commands/owner/reloadone.js | 46 ++ src/commands/owner/restart.js | 22 + src/commands/ping.js | 27 + src/database.js | 145 ++++ src/events.js | 50 ++ src/messageawaiter.js | 154 ++++ src/structures/ArgumentInterpreter.js | 182 +++++ src/structures/Command.js | 121 +++ src/structures/GenericPager.js | 102 +++ src/structures/GenericPrompt.js | 141 ++++ src/structures/Halt.js | 61 ++ src/structures/MultiSelect.js | 145 ++++ src/structures/Paginator.js | 184 +++++ src/structures/ReactionCollector.js | 60 ++ src/structures/SubMenu.js | 44 ++ src/util.js | 258 +++++++ yarn.lock | 1032 +++++++++++++++++++++++++ 35 files changed, 3513 insertions(+), 2 deletions(-) create mode 100644 .eslintignore create mode 100644 .eslintrc.json create mode 100644 .gitattributes create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/eslint.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 config/_default.js create mode 100644 package.json create mode 100644 pm2.json create mode 100644 src/bot.js create mode 100644 src/commandloader.js create mode 100644 src/commands/help.js create mode 100644 src/commands/owner/asynceval.js create mode 100644 src/commands/owner/eval.js create mode 100644 src/commands/owner/exec.js create mode 100644 src/commands/owner/reload.js create mode 100644 src/commands/owner/reloadone.js create mode 100644 src/commands/owner/restart.js create mode 100644 src/commands/ping.js create mode 100644 src/database.js create mode 100644 src/events.js create mode 100644 src/messageawaiter.js create mode 100644 src/structures/ArgumentInterpreter.js create mode 100644 src/structures/Command.js create mode 100644 src/structures/GenericPager.js create mode 100644 src/structures/GenericPrompt.js create mode 100644 src/structures/Halt.js create mode 100644 src/structures/MultiSelect.js create mode 100644 src/structures/Paginator.js create mode 100644 src/structures/ReactionCollector.js create mode 100644 src/structures/SubMenu.js create mode 100644 src/util.js create mode 100644 yarn.lock diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..0d81a1a --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +.Config +Config/ \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..39b4666 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,79 @@ +{ + "env": { + "commonjs": true, + "es6": true, + "node": true + }, + "extends": "eslint:recommended", + "globals": {}, + "parserOptions": { + "ecmaVersion": 2020, + "sourceType": "module" + }, + "rules": { + "array-bracket-spacing": [ + "warn", + "never" + ], + "computed-property-spacing": "warn", + "indent": [ + "warn", + 2 + ], + "keyword-spacing": [ + "warn", + { + "before": true, + "after": true + } + ], + "max-len": [ + "warn", + { + "code": 110, + "ignoreComments": true, + "ignoreUrls": true + } + ], + "no-cond-assign": [ + 2, + "except-parens" + ], + "no-use-before-define": [ + 2, + { + "functions": false, + "classes": false, + "variables": false + } + ], + "new-cap": 0, + "no-caller": 2, + "no-undef": 2, + "no-unused-vars": 1, + "no-empty": [ + "error", + { + "allowEmptyCatch": true + } + ], + "no-console": "off", + "no-multi-spaces": "warn", + "prefer-const": [ + "warn", + { + "destructuring": "all" + } + ], + "quotes": [ + "warn", + "single" + ], + "semi": [ + "warn", + "always" + ], + "spaced-comment": "warn", + "space-infix-ops": "warn" + } +} \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..06fdef4 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ + +* text=auto eol=lf \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..1659a00 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: +# Update packages +- package-ecosystem: npm + directory: "/" + schedule: + interval: weekly + open-pull-requests-limit: 10 + target-branch: dev + commit-message: + prefix: chore + include: scope diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml new file mode 100644 index 0000000..550e3a8 --- /dev/null +++ b/.github/workflows/eslint.yml @@ -0,0 +1,32 @@ +name: ESLint +on: + push: + paths: + - "src/**" + - ".eslintrc.*" + - ".github/workflows/eslint.yml" + +jobs: + update: + name: ESLint + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v2 + - name: Install Node v12 + uses: actions/setup-node@v1 + with: + node-version: 12.x + - name: Install Yarn + run: npm install -g yarn + - name: Install dependencies + run: yarn install + - name: Run ESLint + run: yarn run eslint:fix + - name: Commit changes + uses: EndBug/add-and-commit@v4 + with: + add: src + message: "chore(lint): Auto-fix linting errors" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..25f07b4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules +config/default.js +config/production.js +package-lock.json +.idea/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a0b3185 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 LBRY Foundation + +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 index 0532578..af9c838 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,25 @@ -# curate -LBRY Curation bot for discord interaction +# LBRY Curation Bot + +## Introduction + +This bot allows the community of LBRY to support eachother through the [LBRY Foundation Discord](https://discord.gg/UgBhwZ8) + + +## Installation +* Pull the repo +* Install [Node.JS LTS](https://nodejs.org/) (Currently Node v12.x) +* Install [Yarn](https://yarnpkg.com/) (`npm install yarn -g`) +* Install [Redis](https://redis.io/) ([quickstart](https://redis.io/topics/quickstart)) +* Install LBRY-SDK +* Set your NODE_ENV (Node environment) Environment Variable to Production (`EXPORT NODE_ENV=production`) +* In the `config/` folder, copy `_default.js` to `production.js` and edit the config as needed + +## Contributions + +This bot would not be possible without the following people/software: + +* LBRY Inc. and the LBRY SDK +* Eris - NodeJS Library for Discord +* LBRY Foundation +* Snazzah - Creator of the Faux command base and developer of the bot +* Coolguy3289 - Developer of the bot and command flow diff --git a/config/_default.js b/config/_default.js new file mode 100644 index 0000000..a0a921c --- /dev/null +++ b/config/_default.js @@ -0,0 +1,40 @@ +module.exports = { + // [string] The token for the bot + token: "", + // [string] The prefix for the bot + prefix: "L!", + // [Array] An array of elevated IDs, giving them access to developer commands + elevated: [], + // [string] The path where the commands will be found + commandsPath: "./src/commands", + // [boolean] Whether debug logs will be shown + debug: false, + // [number] The main embed color (#ffffff -> 0xffffff) + embedColor: 0x429bce, + // [string] curator_role_id + curatorRoleID: "", + // [string] admin_role_id + adminRoleID: "", + // [string] sdk_url + sdkURL: "", + // [Object] Eris client options (https://abal.moe/Eris/docs/Client) + discordConfig: { + autoreconnect: true, + allowedMentions: { + everyone: false, + roles: false, + users: true + }, + maxShards: "auto", + messageLimit: 0, + intents: [ + "guilds", + "guildEmojis", + "guildWebhooks", + "guildMessages", + "guildMessageReactions", + "directMessages", + "directMessageReactions" + ] // 13865 - Intent Raw. + } +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..bdf2cb4 --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "name": "lbry-curate", + "version": "0.0.1", + "description": "Support the LBRY Community through Discord!", + "main": "src/bot.js", + "scripts": { + "start": "node src/bot.js", + "eslint": "eslint ./src", + "eslint:fix": "eslint ./src --fix" + }, + "dependencies": { + "abort-controller": "^3.0.0", + "cat-loggr": "^1.1.0", + "config": "^3.3.1", + "eris": "^0.13.3", + "eventemitter3": "^4.0.4", + "fuzzy": "^0.1.3", + "moment": "^2.27.0", + "node-fetch": "^2.3.0", + "redis": "^3.0.2", + "require-reload": "^0.2.2", + "winston": "^3.3.3" + }, + "devDependencies": { + "eslint": "^7.6.0" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/trello-talk/TrelloBot.git" + }, + "author": "LBRY Curate Dev Team ", + "license": "MIT" +} diff --git a/pm2.json b/pm2.json new file mode 100644 index 0000000..cbe1de0 --- /dev/null +++ b/pm2.json @@ -0,0 +1,9 @@ +{ + "apps": [ + { + "name": "LBRYCurate", + "script": "node", + "args": "src/bot.js" + } + ] +} diff --git a/src/bot.js b/src/bot.js new file mode 100644 index 0000000..d8e48b2 --- /dev/null +++ b/src/bot.js @@ -0,0 +1,148 @@ +const Eris = require('eris'); +const Database = require('./database'); +const EventHandler = require('./events'); +const CommandLoader = require('./commandloader'); +const MessageAwaiter = require('./messageawaiter'); +const path = require('path'); +const CatLoggr = require('cat-loggr'); +const config = require('config'); + +class CurateBot extends Eris.Client { + constructor({ packagePath, mainDir } = {}) { + // Initialization + const pkg = require(packagePath || `${mainDir}/package.json`); + super(config.token, JSON.parse(JSON.stringify(config.discordConfig))); + this.dir = mainDir; + this.pkg = pkg; + this.logger = new CatLoggr({ + level: config.debug ? 'debug' : 'info', + levels: [ + { name: 'fatal', color: CatLoggr._chalk.red.bgBlack, err: true }, + { name: 'error', color: CatLoggr._chalk.black.bgRed, err: true }, + { name: 'warn', color: CatLoggr._chalk.black.bgYellow, err: true }, + { name: 'init', color: CatLoggr._chalk.black.bgGreen }, + { name: 'webserv', color: CatLoggr._chalk.black.bgBlue }, + { name: 'info', color: CatLoggr._chalk.black.bgCyan }, + { name: 'assert', color: CatLoggr._chalk.cyan.bgBlack }, + { name: 'poster', color: CatLoggr._chalk.yellow.bgBlack }, + { name: 'debug', color: CatLoggr._chalk.magenta.bgBlack, aliases: ['log', 'dir'] }, + { name: 'limiter', color: CatLoggr._chalk.gray.bgBlack }, + { name: 'fileload', color: CatLoggr._chalk.white.bgBlack } + ] + }); + this.logger.setGlobal(); + this.config = config; + this.typingIntervals = new Map(); + + // Events + this.on('ready', () => console.info('All shards ready.')); + this.on('disconnect', () => console.warn('All shards Disconnected.')); + this.on('reconnecting', () => console.warn('Reconnecting client.')); + this.on('debug', message => console.debug(message)); + + // Shard Events + this.on('connect', id => console.info(`Shard ${id} connected.`)); + this.on('error', (error, id) => console.error(`Error in shard ${id}`, error)); + this.on('hello', (_, id) => console.debug(`Shard ${id} recieved hello.`)); + this.on('warn', (message, id) => console.warn(`Warning in Shard ${id}`, message)); + this.on('shardReady', id => console.info(`Shard ${id} ready.`)); + this.on('shardResume', id => console.warn(`Shard ${id} resumed.`)); + this.on('shardDisconnect', (error, id) => console.warn(`Shard ${id} disconnected`, error)); + + // SIGINT & uncaught exceptions + process.once('uncaughtException', async err => { + console.error('Uncaught Exception', err.stack); + await this.dieGracefully(); + process.exit(0); + }); + + process.once('SIGINT', async () => { + console.info('Caught SIGINT'); + await this.dieGracefully(); + process.exit(0); + }); + + console.init('Client initialized'); + } + + /** + * Creates a promise that resolves on the next event + * @param {string} event The event to wait for + */ + waitTill(event) { + return new Promise(resolve => this.once(event, resolve)); + } + + /** + * Starts the processes and log-in to Discord. + */ + async start() { + // Redis + this.db = new Database(this); + await this.db.connect(this.config.redis); + + // Discord + await this.connect(); + await this.waitTill('ready'); + this.editStatus('online', { + name: `${this.config.prefix}help`, + type: 3, + }); + + // Commands + this.cmds = new CommandLoader(this, path.join(this.dir, this.config.commandsPath)); + this.cmds.reload(); + this.cmds.preloadAll(); + + // Events + this.messageAwaiter = new MessageAwaiter(this); + this.eventHandler = new EventHandler(this); + } + + /** + * KIlls the bot + */ + dieGracefully() { + return super.disconnect(); + } + + // Typing + + /** + * Start typing in a channel + * @param {Channel} channel The channel to start typing in + */ + async startTyping(channel) { + if (this.isTyping(channel)) return; + await channel.sendTyping(); + this.typingIntervals.set(channel.id, setInterval(() => { + channel.sendTyping().catch(() => this.stopTyping(channel)); + }, 5000)); + } + + /** + * Whether the bot is currently typing in a channel + * @param {Channel} channel + */ + isTyping(channel) { + return this.typingIntervals.has(channel.id); + } + + /** + * Stops typing in a channel + * @param {Channel} channel + */ + stopTyping(channel) { + if (!this.isTyping(channel)) return; + const interval = this.typingIntervals.get(channel.id); + clearInterval(interval); + this.typingIntervals.delete(channel.id); + } +} + +const Bot = new CurateBot({ mainDir: path.join(__dirname, '..') }); +Bot.start().catch(e => { + Bot.logger.error('Failed to start bot! Exiting in 10 seconds...'); + console.error(e); + setTimeout(() => process.exit(0), 10000); +}); diff --git a/src/commandloader.js b/src/commandloader.js new file mode 100644 index 0000000..8192abe --- /dev/null +++ b/src/commandloader.js @@ -0,0 +1,100 @@ +const fs = require('fs'); +const path = require('path'); +const reload = require('require-reload')(require); + +module.exports = class CommandLoader { + constructor(client, cPath) { + this.commands = []; + this.path = path.resolve(cPath); + this.client = client; + } + + /** + * Loads commands from a folder + * @param {String} folderPath + */ + iterateFolder(folderPath) { + const files = fs.readdirSync(folderPath); + files.map(file => { + const filePath = path.join(folderPath, file); + const stat = fs.lstatSync(filePath); + if (stat.isSymbolicLink()) { + const realPath = fs.readlinkSync(filePath); + if (stat.isFile() && file.endsWith('.js')) { + this.load(realPath); + } else if (stat.isDirectory()) { + this.iterateFolder(realPath); + } + } else if (stat.isFile() && file.endsWith('.js')) + this.load(filePath); + else if (stat.isDirectory()) + this.iterateFolder(filePath); + }); + } + + /** + * Loads a command + * @param {string} commandPath + */ + load(commandPath) { + console.fileload('Loading command', commandPath); + const cls = reload(commandPath); + const cmd = new cls(this.client); + cmd.path = commandPath; + this.commands.push(cmd); + return cmd; + } + + /** + * Reloads all commands + */ + reload() { + this.commands = []; + this.iterateFolder(this.path); + } + + /** + * Gets a command based on it's name or alias + * @param {string} name The command's name or alias + */ + get(name) { + let cmd = this.commands.find(c => c.name === name); + if (cmd) return cmd; + this.commands.forEach(c => { + if (c.options.aliases.includes(name)) cmd = c; + }); + return cmd; + } + + /** + * Preloads a command + * @param {string} name The command's name or alias + */ + preload(name) { + if (!this.get(name)) return; + this.get(name)._preload(); + } + + /** + * Preloads all commands + */ + preloadAll() { + this.commands.forEach(c => c._preload()); + } + + /** + * Processes the cooldown of a command + * @param {Message} message + * @param {Command} command + */ + async processCooldown(message, command) { + if (this.client.config.elevated.includes(message.author.id)) return true; + const now = Date.now() - 1; + const cooldown = command.cooldownAbs; + let userCD = await this.client.db.hget(`cooldowns:${message.author.id}`, command.name) || 0; + if (userCD) userCD = parseInt(userCD); + if (userCD + cooldown > now) return false; + await this.client.db.hset(`cooldowns:${message.author.id}`, command.name, now); + return true; + } +}; diff --git a/src/commands/help.js b/src/commands/help.js new file mode 100644 index 0000000..73a4dfd --- /dev/null +++ b/src/commands/help.js @@ -0,0 +1,101 @@ +const Command = require('../structures/Command'); +const Util = require('../util'); +const config = require('config'); + +module.exports = class Help extends Command { + get name() { return 'help'; } + + get _options() { return { + aliases: [ + '?', 'h', 'commands', 'cmds', // English + 'yardim', 'yardım', 'komutlar', // Turkish + 'ayuda', // Spanish + 'ajuda' // Catalan & Portuguese + ], + permissions: ['embed'], + cooldown: 0, + }; } + + exec(message, { args, }) { + if (args[0]) { + // Display help on a command + const command = this.client.cmds.get(args[0]); + if (!command) + return message.channel.createMessage(`The command \`${args[0]}\` could not be found.`); + else { + const embed = { + title: `${config.prefix}${command.name}`, + color: config.embedColor, + fields: [ + { name: '*Usage*', + value: `${config.prefix}${command.name}${ + command.metadata.usage ? + ` \`${command.metadata.usage}\`` : ''}` } + ], + description: command.metadata.description + }; + + // Cooldown + if (command.options.cooldown) + embed.fields.push({ + name: '*Cooldown*', + value: `${command.options.cooldown.toLocaleString()} second(s)`, + inline: false + }); + + // Aliases + if (command.options.aliases.length !== 0) embed.fields.push({ + name: '*Alias(es)*', + value: command.options.aliases.map(a => `\`${a}\``).join(', ') + }); + + // Image + if (command.metadata.image) + embed.image = { url: command.metadata.image }; + + // Note + if (command.metadata.note) + embed.fields.push({ + name: '*Note*', + value: command.metadata.note + }); + + return message.channel.createMessage({ embed }); + } + } else { + // Display general help command + const embed = { + color: config.embedColor, + description: 'LBRY Curate', + footer: { text: `\`${config.prefix}help [command]\` for more info.` }, + fields: [] + }; + + // Populate categories + const categories = {}; + this.client.cmds.commands.forEach(v => { + if (!v.options.listed && !config.elevated.includes(message.author.id)) return; + const string = v.name; + if (categories[v.metadata.category]) + categories[v.metadata.category].push(string); + else categories[v.metadata.category] = [string]; + }); + + // List categories + Util.keyValueForEach(categories, (k, v) => { + embed.fields.push({ + name: `*${k}*`, + value: '```' + v.join(', ') + '```', + inline: true + }); + }); + return message.channel.createMessage({ embed }); + } + } + + get metadata() { return { + category: 'General', + description: 'Shows the help message and gives information on commands.', + usage: '[command]' + }; } +}; diff --git a/src/commands/owner/asynceval.js b/src/commands/owner/asynceval.js new file mode 100644 index 0000000..a068b20 --- /dev/null +++ b/src/commands/owner/asynceval.js @@ -0,0 +1,38 @@ +/* jshint evil: true */ + +const Command = require('../../structures/Command'); +const Util = require('../../util'); + +module.exports = class AsyncEval extends Command { + get name() { return 'asynceval'; } + + get _options() { return { + aliases: ['ae', 'aeval', 'aevaluate', 'asyncevaluate'], + permissions: ['elevated'], + listed: false, + }; } + + // eslint-disable-next-line no-unused-vars + async exec(message, opts) { + try { + const start = Date.now(); + const code = Util.Prefix.strip(message, this.client).split(' ').slice(1).join(' '); + const result = await eval(`(async () => {${code}})()`); + const time = Date.now() - start; + return Util.Hastebin.autosend( + `Took ${time.toLocaleString()} ms\n\`\`\`js\n${result}\`\`\`\n`, + message); + } catch (e) { + return Util.Hastebin.autosend('```js\n' + e.stack + '\n```', message); + } + } + + get metadata() { return { + category: 'Developer', + description: 'Evaluate code asynchronously.', + usage: '', + note: 'Due to the added async IIFE wrapper in this command, ' + + 'it is necessary to use the return statement to return a result.\n' + + 'e.g. `return 1`' + }; } +}; diff --git a/src/commands/owner/eval.js b/src/commands/owner/eval.js new file mode 100644 index 0000000..f1fb708 --- /dev/null +++ b/src/commands/owner/eval.js @@ -0,0 +1,35 @@ +/* jshint evil: true */ + +const Command = require('../../structures/Command'); +const Util = require('../../util'); + +module.exports = class Eval extends Command { + get name() { return 'eval'; } + + get _options() { return { + aliases: ['e'], + permissions: ['elevated'], + listed: false, + minimumArgs: 1 + }; } + + // eslint-disable-next-line no-unused-vars + async exec(message, opts) { + try { + const start = Date.now(); + const result = eval(Util.Prefix.strip(message, this.client).split(' ').slice(1).join(' ')); + const time = Date.now() - start; + return Util.Hastebin.autosend( + `Took ${time.toLocaleString()} ms\n\`\`\`js\n${result}\`\`\`\n`, + message); + } catch (e) { + return Util.Hastebin.autosend('```js\n' + e.stack + '\n```', message); + } + } + + get metadata() { return { + category: 'Developer', + description: 'Evaluate code.', + usage: '' + }; } +}; diff --git a/src/commands/owner/exec.js b/src/commands/owner/exec.js new file mode 100644 index 0000000..e28dce0 --- /dev/null +++ b/src/commands/owner/exec.js @@ -0,0 +1,34 @@ +const Command = require('../../structures/Command'); +const Util = require('../../util'); +const { exec } = require('child_process'); + +module.exports = class Exec extends Command { + get name() { return 'exec'; } + + get _options() { return { + aliases: ['ex', 'sys'], + permissions: ['elevated'], + listed: false, + minimumArgs: 1 + }; } + + codeBlock(content, lang = null) { + return `\`\`\`${lang ? `${lang}\n` : ''}${content}\`\`\``; + } + + async exec(message) { + await this.client.startTyping(message.channel); + exec(Util.Prefix.strip(message, this.client).split(' ').slice(1).join(' '), (err, stdout, stderr) => { + this.client.stopTyping(message.channel); + if (err) return message.channel.createMessage(this.codeBlock(err, 'js')); + const stdErrBlock = (stderr ? this.codeBlock(stderr, 'js') + '\n' : ''); + return Util.Hastebin.autosend(stdErrBlock + this.codeBlock(stdout), message); + }); + } + + get metadata() { return { + category: 'Developer', + description: 'Do some terminal commands.', + usage: ' …' + }; } +}; diff --git a/src/commands/owner/reload.js b/src/commands/owner/reload.js new file mode 100644 index 0000000..4378d3a --- /dev/null +++ b/src/commands/owner/reload.js @@ -0,0 +1,23 @@ +const Command = require('../../structures/Command'); + +module.exports = class Reload extends Command { + get name() { return 'reload'; } + + get _options() { return { + aliases: ['r'], + permissions: ['elevated'], + listed: false, + }; } + + async exec(message) { + const sentMessage = await message.channel.createMessage('♻️ Reloading commands…'); + this.client.cmds.reload(); + this.client.cmds.preloadAll(); + return sentMessage.edit('✅ Reloaded commands.'); + } + + get metadata() { return { + category: 'Developer', + description: 'Reloads all commands.' + }; } +}; diff --git a/src/commands/owner/reloadone.js b/src/commands/owner/reloadone.js new file mode 100644 index 0000000..a552cc0 --- /dev/null +++ b/src/commands/owner/reloadone.js @@ -0,0 +1,46 @@ +const Command = require('../../structures/Command'); +const fs = require('fs'); + +module.exports = class ReloadOne extends Command { + get name() { return 'reloadone'; } + + get _options() { return { + aliases: ['r1', 'reloadsingle', 'rs'], + permissions: ['elevated'], + minimumArgs: 1, + listed: false, + }; } + + async exec(message, { args }) { + const commands = args.map(name => this.client.cmds.get(name)); + if (commands.includes(undefined)) + return message.channel.createMessage('Invalid command!'); + + const fileExist = commands.map(command => { + const path = command.path; + const stat = fs.lstatSync(path); + return stat.isFile(); + }); + + if (fileExist.includes(false)) + return message.channel.createMessage('A file that had a specified command no longer exists!'); + + const sentMessage = await message.channel.createMessage('♻️ Reloading commands…'); + + const reloadedCommands = commands.map(command => { + const path = command.path; + const index = this.client.cmds.commands.indexOf(command); + this.client.cmds.commands.splice(index, 1); + const newCommand = this.client.cmds.load(path); + newCommand.preload(); + return newCommand; + }); + return sentMessage.edit(`✅ Reloaded ${reloadedCommands.map(c => `\`${c.name}\``).join(', ')}.`); + } + + get metadata() { return { + category: 'Developer', + description: 'Reloads specific commands.', + usage: ' [commandName] …' + }; } +}; diff --git a/src/commands/owner/restart.js b/src/commands/owner/restart.js new file mode 100644 index 0000000..517e223 --- /dev/null +++ b/src/commands/owner/restart.js @@ -0,0 +1,22 @@ +const Command = require('../../structures/Command'); + +module.exports = class Restart extends Command { + get name() { return 'restart'; } + + get _options() { return { + aliases: ['re'], + permissions: ['elevated'], + listed: false, + }; } + + async exec(message) { + await message.channel.createMessage('Restarting...'); + await this.client.dieGracefully(); + process.exit(0); + } + + get metadata() { return { + category: 'Developer', + description: 'Restarts the bot.' + }; } +}; diff --git a/src/commands/ping.js b/src/commands/ping.js new file mode 100644 index 0000000..41dda22 --- /dev/null +++ b/src/commands/ping.js @@ -0,0 +1,27 @@ +const Command = require('../structures/Command'); + +module.exports = class Ping extends Command { + get name() { return 'ping'; } + + get _options() { return { + aliases: ['p', 'pong'], + cooldown: 0, + }; } + + async exec(message) { + const currentPing = Array.from(this.client.shards.values()) + .map(shard => shard.latency).reduce((prev, val) => prev + val, 0); + const timeBeforeMessage = Date.now(); + const sentMessage = await message.channel.createMessage('> :ping_pong: ***Ping...***\n' + + `> WS: ${currentPing.toLocaleString()} ms`); + await sentMessage.edit( + '> :ping_pong: ***Pong!***\n' + + `> WS: ${currentPing.toLocaleString()} ms\n` + + `> REST: ${(Date.now() - timeBeforeMessage).toLocaleString()} ms`); + } + + get metadata() { return { + category: 'General', + description: 'Pong!' + }; } +}; diff --git a/src/database.js b/src/database.js new file mode 100644 index 0000000..b78140e --- /dev/null +++ b/src/database.js @@ -0,0 +1,145 @@ +const redis = require('redis'); +const { EventEmitter } = require('eventemitter3'); + +/** + * The Redis database handler + */ +module.exports = class Database extends EventEmitter { + constructor(client) { + super(); + this.client = client; + this.reconnectAfterClose = true; + console.init('Redis initialized'); + } + + /** + * Creates a client and connects to the database + * @param {Object} options + */ + connect({ host = 'localhost', port, password }) { + console.info('Connecting to redis...'); + return new Promise((resolve, reject) => { + this.redis = redis.createClient({ host, port, password }); + this.redis.on('error', this.onError.bind(this)); + this.redis.on('warning', w => console.warn('Redis Warning', w)); + this.redis.on('end', () => this.onClose.bind(this)); + this.redis.on('reconnecting', () => console.warn('Reconnecting to redis...')); + this.redis.on('ready', () => console.info('Redis client ready.')); + this.redis.on('connect', () => console.info('Redis connection has started.')); + this.host = host; + this.port = port; + this.password = password; + + this.redis.once('ready', resolve.bind(this)); + this.redis.once('error', reject.bind(this)); + }); + } + + /** + * @private + * @param {string} k Key + */ + _p(k) { return (this.client.config.prefix || '') + k; } + + // #region Redis functions + hget(key, hashkey) { + return new Promise((resolve, reject) => { + this.redis.HGET(this._p(key), hashkey, (err, value) => { + if (err) reject(err); + resolve(value); + }); + }); + } + + hset(key, hashkey, value) { + return new Promise((resolve, reject) => { + this.redis.HSET(this._p(key), hashkey, value, (err, res) => { + if (err) reject(err); + resolve(res); + }); + }); + } + + incr(key) { + return new Promise((resolve, reject) => { + this.redis.incr(this._p(key), (err, res) => { + if (err) reject(err); + resolve(res); + }); + }); + } + + get(key) { + return new Promise((resolve, reject) => { + this.redis.get(this._p(key), function(err, reply) { + if (err) reject(err); + resolve(reply); + }); + }); + } + + expire(key, ttl) { + return new Promise((resolve, reject) => { + this.redis.expire(this._p(key), ttl, (err, value) => { + if (err) reject(err); + resolve(value); + }); + }); + } + + + exists(key) { + return new Promise((resolve, reject) => { + this.redis.exists(this._p(key), (err, value) => { + if (err) reject(err); + resolve(value === 1); + }); + }); + } + + set(key, value) { + return new Promise((resolve, reject) => { + this.redis.set(this._p(key), value, (err, res) => { + if (err) reject(err); + resolve(res); + }); + }); + } + // #endregion + + /** + * Reconnects the client + */ + async reconnect() { + console.warn('Attempting redis reconnection'); + this.conn = await this.connect(this); + } + + /** + * Disconnects the client + */ + disconnect() { + this.reconnectAfterClose = false; + return new Promise(resolve => { + this.redis.once('end', resolve); + this.redis.quit(); + }); + } + + /** + * @private + */ + onError(err) { + console.error('Redis Error', err); + this.emit('error', err); + } + + /** + * @private + */ + async onClose() { + console.error('Redis closed'); + this.emit('close'); + if (this.reconnectAfterClose) await this.reconnect(); + } +}; diff --git a/src/events.js b/src/events.js new file mode 100644 index 0000000..bd9c9f6 --- /dev/null +++ b/src/events.js @@ -0,0 +1,50 @@ +const ArgumentInterpreter = require('./structures/ArgumentInterpreter'); +const Util = require('./util'); +const config = require('config'); + +module.exports = class Events { + constructor(client) { + this.client = client; + client.on('messageCreate', this.onMessage.bind(this)); + client.on('messageReactionAdd', this.onReaction.bind(this)); + } + + async onMessage(message) { + if (message.author.bot || message.author.system) return; + + // Check to see if bot can send messages + if (message.channel.type !== 1 && + !message.channel.permissionsOf(this.client.user.id).has('sendMessages')) return; + + // Message awaiter + if (this.client.messageAwaiter.processHalt(message)) return; + + // Command parsing + const argInterpretor = new ArgumentInterpreter(Util.Prefix.strip(message, this.client, [config.prefix])); + const args = argInterpretor.parseAsStrings(); + const commandName = args.splice(0, 1)[0]; + const command = this.client.cmds.get(commandName, message); + if (!message.content.match(Util.Prefix.regex(this.client, [config.prefix])) || !command) return; + + try { + await command._exec(message, { + args + }); + } catch (e) { + if (this.client.config.debug) { + console.error(`The '${command.name}' command failed.`); + console.log(e); + } + message.channel.createMessage(':fire: An error occurred while processing that command!'); + this.client.stopTyping(message.channel); + } + } + + onReaction(message, emoji, userID) { + const id = `${message.id}:${userID}`; + if (this.client.messageAwaiter.reactionCollectors.has(id)) { + const collector = this.client.messageAwaiter.reactionCollectors.get(id); + collector._onReaction(emoji, userID); + } + } +}; diff --git a/src/messageawaiter.js b/src/messageawaiter.js new file mode 100644 index 0000000..bb54aad --- /dev/null +++ b/src/messageawaiter.js @@ -0,0 +1,154 @@ +const Halt = require('./structures/Halt'); +const ReactionCollector = require('./structures/ReactionCollector'); + +/** + * Handles async message functions + */ +class MessageAwaiter { + constructor(client) { + this.client = client; + this.halts = new Map(); + this.reactionCollectors = new Map(); + } + + /** + * Creates a halt. This pauses any events in the event handler for a specific channel and user. + * This allows any async functions to handle any follow-up messages. + * @param {string} channelID The channel's ID + * @param {string} userID The user's ID + * @param {number} [timeout=30000] The time until the halt is auto-cleared + */ + createHalt(channelID, userID, timeout = 30000) { + const id = `${channelID}:${userID}`; + if (this.halts.has(id)) this.halts.get(id).end(); + const halt = new Halt(this, timeout); + halt.once('end', () => this.halts.delete(id)); + this.halts.set(id, halt); + return halt; + } + + /** + * Creates a reaction collector. Any reactions from the user will be emitted from the collector. + * @param {string} message The message to collect from + * @param {string} userID The user's ID + * @param {number} [timeout=30000] The time until the halt is auto-cleared + */ + createReactionCollector(message, userID, timeout = 30000) { + const id = `${message.id}:${userID}`; + if (this.reactionCollectors.has(id)) this.reactionCollectors.get(id).end(); + const collector = new ReactionCollector(this, timeout); + collector.once('end', () => this.reactionCollectors.delete(id)); + this.reactionCollectors.set(id, collector); + return collector; + } + + /** + * Gets an ongoing halt based on a message + * @param {Message} message + */ + getHalt(message) { + const id = `${message.channel.id}:${message.author.id}`; + return this.halts.get(id); + } + + /** + * Processes a halt based on a message + * @param {Message} message + */ + processHalt(message) { + const id = `${message.channel.id}:${message.author.id}`; + if (this.halts.has(id)) { + const halt = this.halts.get(id); + halt._onMessage(message); + return true; + } + return false; + } + + /** + * Awaits the next message from a user + * @param {Message} message The message to wait for + * @param {Object} [options] The options for the await + * @param {number} [options.filter] The message filter + * @param {number} [options.timeout=30000] The timeout for the halt + * @returns {?Message} + */ + awaitMessage(message, { filter = () => true, timeout = 30000 } = {}) { + return new Promise(resolve => { + const halt = this.createHalt(message.channel.id, message.author.id, timeout); + let foundMessage = null; + halt.on('message', nextMessage => { + if (filter(nextMessage)) { + foundMessage = nextMessage; + halt.end(); + } + }); + halt.on('end', () => resolve(foundMessage)); + }); + } + + /** + * Same as {@see #awaitMessage}, but is used for getting user input via next message + * @param {Message} message The message to wait for + * @param {Object} [options] The options for the await + * @param {number} [options.filter] The message filter + * @param {number} [options.timeout=30000] The timeout for the halt + * @param {string} [options.header] The content to put in the bot message + * @returns {?Message} + */ + async getInput(message, { filter = () => true, timeout = 30000, header = null } = {}) { + await message.channel.createMessage(`<@${message.author.id}>, ` + + (header || 'Type the message you want to input.') + '\n\n' + + `Typing "<@!${this.client.user.id}> cancel" will cancel the input.`); + return new Promise(resolve => { + const halt = this.createHalt(message.channel.id, message.author.id, timeout); + let handled = false, input = null; + halt.on('message', nextMessage => { + if (filter(nextMessage)) { + const cancelRegex = new RegExp(`^(?:<@!?${this.client.user.id}>\\s?)(cancel|stop|end)$`); + + if (!nextMessage.content || cancelRegex.test(nextMessage.content.toLowerCase())) { + handled = true; + message.channel.createMessage(`<@${message.author.id}>, Your last input was canceled.`); + } else input = nextMessage.content; + halt.end(); + } + }); + halt.on('end', async () => { + if (!input && !handled) + await message.channel.createMessage(`<@${message.author.id}>, Your last input was canceled.`); + resolve(input); + }); + }); + } + + /** + * Same as {@see #awaitMessage}, but is used for confirmation + * @param {Message} message The message to wait for + * @param {Object} [options] The options for the await + * @param {number} [options.timeout=30000] The timeout for the halt + * @param {string} [options.header] The content to put in the bot message + * @returns {?Message} + */ + async confirm(message, { timeout = 30000, header = null } = {}) { + await message.channel.createMessage(`<@${message.author.id}>, ` + + (header || 'Are you sure you want to do this?') + '\n\n' + + 'Type `yes` to confirm. Any other message will cancel the confirmation.'); + return new Promise(resolve => { + const halt = this.createHalt(message.channel.id, message.author.id, timeout); + let input = false; + halt.on('message', nextMessage => { + input = nextMessage.content === 'yes'; + halt.end(); + }); + halt.on('end', async () => { + if (!input) + await message.channel.createMessage(`<@${message.author.id}>, Confirmation canceled.`); + resolve(input); + }); + }); + } +} + +MessageAwaiter.Halt = Halt; +module.exports = MessageAwaiter; \ No newline at end of file diff --git a/src/structures/ArgumentInterpreter.js b/src/structures/ArgumentInterpreter.js new file mode 100644 index 0000000..c06066d --- /dev/null +++ b/src/structures/ArgumentInterpreter.js @@ -0,0 +1,182 @@ +/** + * A class that iterates a string's index + * @see ArgumentInterpreter + */ +class StringIterator { + /** + * @param {string} string The string to iterate through + */ + constructor(string) { + this.string = string; + this.index = 0; + this.previous = 0; + this.end = string.length; + } + + /** + * Get the character on an index and moves the index forward. + * @returns {?string} + */ + get() { + const nextChar = this.string[this.index]; + if (!nextChar) + return nextChar; + else { + this.previous += this.index; + this.index += 1; + return nextChar; + } + } + + /** + * Reverts to the previous index. + */ + undo() { + this.index = this.previous; + } + + /** + * The previous character that was used + * @type {string} + */ + get prevChar() { + return this.string[this.previous]; + } + + /** + * Whether or not the index is out of range + * @type {boolean} + */ + get inEOF() { + return this.index >= this.end; + } +} + +/** + * Parses arguments from a message. + */ +class ArgumentInterpreter { + /** + * @param {string} string The string that will be parsed for arguments + * @param {?Object} options The options for the interpreter + * @param {?boolean} [options.allowWhitespace=false] Whether to allow whitespace characters in the arguments + */ + constructor(string, { allowWhitespace = false } = {}) { + this.string = string; + this.allowWhitespace = allowWhitespace; + } + + /** + * Parses the arguements as strings. + * @returns {Array} + */ + parseAsStrings() { + const args = []; + let currentWord = ''; + let quotedWord = ''; + const string = this.allowWhitespace ? this.string : this.string.trim(); + const iterator = new StringIterator(string); + while (!iterator.inEOF) { + const char = iterator.get(); + if (char === undefined) break; + + if (this.isOpeningQuote(char) && iterator.prevChar !== '\\') { + currentWord += char; + const closingQuote = ArgumentInterpreter.QUOTES[char]; + + // Quote iteration + while (!iterator.inEOF) { + const quotedChar = iterator.get(); + + // Unexpected EOF + if (quotedChar === undefined) { + args.push(...currentWord.split(' ')); + break; + } + + if (quotedChar == '\\') { + currentWord += quotedChar; + const nextChar = iterator.get(); + + if (nextChar === undefined) { + args.push(...currentWord.split(' ')); + break; + } + + currentWord += nextChar; + // Escaped quote + if (ArgumentInterpreter.ALL_QUOTES.includes(nextChar)) { + quotedWord += nextChar; + } else { + // Ignore escape + quotedWord += quotedChar + nextChar; + } + continue; + } + + // Closing quote + if (quotedChar == closingQuote) { + currentWord = ''; + args.push(quotedWord); + quotedWord = ''; + break; + } + + currentWord += quotedChar; + quotedWord += quotedChar; + } + continue; + } + + if (/^\s$/.test(char)) { + if (currentWord) + args.push(currentWord); + currentWord = ''; + continue; + } + + currentWord += char; + } + + if (currentWord.length) + args.push(...currentWord.split(' ')); + return args; + } + + /** + * Checks whether or not a character is an opening quote + * @param {string} char The character to check + */ + isOpeningQuote(char) { + return Object.keys(ArgumentInterpreter.QUOTES).includes(char); + } +} + +// Opening / Closing +ArgumentInterpreter.QUOTES = { + '"': '"', + '‘': '’', + '‚': '‛', + '“': '”', + '„': '‟', + '⹂': '⹂', + '「': '」', + '『': '』', + '〝': '〞', + '﹁': '﹂', + '﹃': '﹄', + '"': '"', + '「': '」', + '«': '»', + '‹': '›', + '《': '》', + '〈': '〉', +}; + +ArgumentInterpreter.ALL_QUOTES = Object.keys(ArgumentInterpreter.QUOTES) + .map(i => ArgumentInterpreter.QUOTES[i]) + .concat(Object.keys(ArgumentInterpreter.QUOTES)); + +ArgumentInterpreter.StringIterator = StringIterator; + +module.exports = ArgumentInterpreter; \ No newline at end of file diff --git a/src/structures/Command.js b/src/structures/Command.js new file mode 100644 index 0000000..c6ac5a8 --- /dev/null +++ b/src/structures/Command.js @@ -0,0 +1,121 @@ +const Util = require('../util'); +const config = require('config'); + +/** + * A command in the bot. + */ +class Command { + /** + * @param {TrelloBot} client + */ + constructor(client) { + this.client = client; + this.subCommands = {}; + } + + /** + * @private + */ + _preload() { + if (!this.preload() && this.client.config.debug) + this.client.cmds.logger.info('Preloading command', this.name); + } + + /** + * The function executed while loading the command into the command handler. + */ + preload() { + return true; + } + + /** + * @private + * @param {Message} message + * @param {Object} opts + */ + async _exec(message, opts) { + // Check minimum arguments + if (this.options.minimumArgs > 0 && opts.args.length < this.options.minimumArgs) + return message.channel.createMessage( + `${this.options.minimumArgsMessage}\nUsage: ${config.prefix}${this.name}${ + this.metadata.usage ? + ` \`${this.metadata.usage}\`` : ''}`); + + // Check commmand permissions + if (this.options.permissions.length) + for (const i in this.options.permissions) { + const perm = this.options.permissions[i]; + if (!Util.CommandPermissions[perm]) + throw new Error(`Invalid command permission "${perm}"`); + if (!Util.CommandPermissions[perm](this.client, message, opts)) + return message.channel.createMessage({ + attach: 'I need the permission `Attach Files` to use this command!', + embed: 'I need the permission `Embed Links` to use this command!', + emoji: 'I need the permission `Use External Emojis` to use this command!', + elevated: 'Only the elevated users of the bot can use this command!', + curator: `This command requires you to have the "${ + message.guild.roles.get(config.curatorRoleID).name}" role!`, + admin: `This command requires you to have the "${ + message.guild.roles.get(config.adminRoleID).name}" role!`, + curatorOrAdmin: `This command requires you to have the "${ + message.guild.roles.get(config.curatorRoleID).name}" or "${ + message.guild.roles.get(config.adminRoleID).name}" roles!`, + guild: 'This command must be ran in a guild!', + }[perm]); + } + + // Process cooldown + if (!this.cooldownAbs || await this.client.cmds.processCooldown(message, this)) { + await this.exec(message, opts); + } else { + const cd = await this.client.db.hget(`cooldowns:${message.author.id}`, this.name); + return message.channel.createMessage( + `:watch: This command is on cooldown! Wait ${ + Math.ceil(this.cooldownAbs - (Date.now() - cd))} second(s) before doing this again!`); + } + } + + // eslint-disable-next-line no-empty-function, no-unused-vars + exec(Message, opts) { } + + /** + * The options for the command + * @type {Object} + */ + get options() { + const options = { + aliases: [], + cooldown: 2, + listed: true, + minimumArgs: 0, + permissions: [], + + minimumArgsMessage: 'Not enough arguments!', + }; + Object.assign(options, this._options); + return options; + } + + /** + * @private + */ + _options() { return {}; } + + /** + * The cooldown in milliseconnds + * @returns {number} + */ + get cooldownAbs() { return this.options.cooldown * 1000; } + + /** + * The metadata for the command + * @return {Object} + */ + get metadata() { + return { + category: 'Misc.', + }; + } +} + +module.exports = Command; \ No newline at end of file diff --git a/src/structures/GenericPager.js b/src/structures/GenericPager.js new file mode 100644 index 0000000..f486d88 --- /dev/null +++ b/src/structures/GenericPager.js @@ -0,0 +1,102 @@ +const Paginator = require('./Paginator'); +const lodash = require('lodash'); + +/** + * A generic pager that shows a list of items + */ +class GenericPager extends Paginator { + /** + * @param {TrelloBot} client The client to use + * @param {Message} message The user's message to read permissions from + * @param {Object} options The options for the pager + * @param {Array} options.items The items the paginator will display + * @param {number} [options.itemsPerPage=15] How many items a page will have + * @param {Function} [options.display] The function that will be used to display items on the prompt + * @param {Object} [options.embedExtra] The embed object to add any extra embed elements to the prompt + * @param {string} [options.itemTitle='words.item.many'] The title to use for the items + * @param {string} [options.header] The text to show above the prompt + * @param {string} [options.footer] The text to show below the prompt + */ + constructor(client, message, { + items = [], itemsPerPage = 15, + display = item => item.toString(), + embedExtra = {}, itemTitle = 'words.item.many', + header = null, footer = null + } = {}) { + super(client, message, { items, itemsPerPage }); + this.displayFunc = display; + this.embedExtra = embedExtra; + this.itemTitle = itemTitle; + this.header = header; + this.footer = footer; + } + + /** + * Whether or not this instance can use embed + * @returns {boolean} + */ + canEmbed() { + return this.message.channel.type === 1 || + this.message.channel.permissionsOf(this.client.user.id).has('embedLinks'); + } + + /** + * Updates the current message + * @returns {Promise} + */ + updateMessage() { + return this.message.edit(this.currentMessage).catch(() => {}); + } + + /** + * The message for the current page + * @type {Object|string} + */ + get currentMessage() { + const displayPage = this.page.map((item, index) => + this.displayFunc(item, index, ((this.pageNumber - 1) * this.itemsPerPage) + index)); + if (this.canEmbed()) { + const embed = lodash.defaultsDeep({ + title: `${this.itemTitle} ` + + `(${this.items.length}, Page ${this.pageNumber}/${this.maxPages})`, + description: this.header || undefined, + footer: this.footer ? { text: this.footer } : undefined, + fields: [] + }, this.embedExtra, { color: this.client.config.embedColor }); + + embed.fields.push({ + name: '*List Prompt*', + value: displayPage.join('\n') + }); + + return { embed }; + } else { + const top = `${this.itemTitle} ` + + `(${this.items.length}, Page ${this.pageNumber}/${this.maxPages})`; + const lines = '─'.repeat(top.length); + return (this.header || '') + '```prolog\n' + `${top}\n` + `${lines}\n` + + displayPage.join('\n') + `${lines}\`\`\`` + (this.footer || ''); + } + } + + /** + * Starts the reaction collector and pagination + * @param {string} channelID The channel to post the new message to + * @param {string} userID The user's ID that started the process + * @param {number} timeout + */ + async start(channelID, userID, timeout) { + this.message = await this.client.createMessage(channelID, this.currentMessage); + return super.start(userID, timeout); + } + + /** + * @private + */ + _change() { + this.updateMessage().catch(() => this.collector.end()); + this.emit('change', this.pageNumber); + } +} + +module.exports = GenericPager; \ No newline at end of file diff --git a/src/structures/GenericPrompt.js b/src/structures/GenericPrompt.js new file mode 100644 index 0000000..4de7427 --- /dev/null +++ b/src/structures/GenericPrompt.js @@ -0,0 +1,141 @@ +const GenericPager = require('./GenericPager'); +const Paginator = require('./Paginator'); +const lodash = require('lodash'); +const fuzzy = require('fuzzy'); + +/** + * A generic pager that shows a list of items + */ +class GenericPrompt { + /** + * @param {TrelloBot} client The client to use + * @param {Message} message The user's message to read permissions from + * @param {Object} pagerOptions The options for the pager + */ + constructor(client, message, pagerOptions = {}) { + this.client = client; + this.message = message; + this.pagerOptions = pagerOptions; + this.displayFunc = pagerOptions.display || ((item) => item.toString()); + + // Override some pager options + this.pagerOptions.display = (item, i, ai) => `${ai + 1}. ${this.displayFunc(item, i, ai)}`; + this.pagerOptions.header = pagerOptions.header || 'Type the number of the item you want to use.'; + this.pagerOptions.footer = (pagerOptions.footer ? pagerOptions.footer + '\n\n' : '') + + 'Typing "cancel" will close this prompt.'; + this.pagerOptions.embedExtra = this.pagerOptions.embedExtra || {}; + this.pagerOptions.embedExtra.author = { + name: `${message.author.username}#${message.author.discriminator}`, + icon_url: message.author.avatarURL || message.author.defaultAvatarURL + }; + + this.pager = new GenericPager(client, message, this.pagerOptions); + this.halt = null; + } + + /** + * Starts the prompt + * @param {string} channelID The channel to post the new message to + * @param {string} userID The user's ID that started the process + * @param {number} timeout + */ + async choose(channelID, userID, timeout) { + if (this.pager.items.length === 0) + return null; + else if (this.pager.items.length === 1) + return this.pager.items[0]; + + await this.pager.start(channelID, userID, timeout); + this.halt = this.client.messageAwaiter.createHalt(channelID, userID, timeout); + + // Sync timeouts + if (this.pager.collector) + this.pager.collector.restart(); + this.halt.restart(); + + return new Promise(resolve => { + let foundItem = null; + + this.halt.on('message', nextMessage => { + if (this.pager.canManage()) + nextMessage.delete().catch(() => {}); + + if (GenericPrompt.CANCEL_TRIGGERS.includes(nextMessage.content.toLowerCase())) { + foundItem = { _canceled: true }; + this.halt.end(); + } + const chosenIndex = parseInt(nextMessage.content); + if (chosenIndex <= 0) return; + const chosenItem = this.pager.items[chosenIndex - 1]; + if (chosenItem !== undefined) { + foundItem = chosenItem; + this.halt.end(); + } + }); + + this.halt.on('end', () => { + // In case the halt ends before reactions are finished coming up + this.pager.reactionsCleared = true; + if (this.pager.collector) + this.pager.collector.end(); + this.pager.message.delete().catch(() => {}); + + if (foundItem && foundItem._canceled) + foundItem = null; + else if (foundItem === null) + this.pager.message.channel.createMessage( + `<@${userID}>, Your last prompt was timed out.`).catch(() => {}); + + resolve(foundItem); + }); + + if (this.pager.collector) + this.pager.collector.on('reaction', emoji => { + if (Paginator.STOP === emoji.name) { + foundItem = { _canceled: true }; + this.halt.end(); + } + }); + }); + } + + /** + * Filters the items into a search and prompts results. + * @param {string} query The term to search for + * @param {Object} options The options passed on to {@see #choose} . + * @param {string} options.channelID The channel to post the new message to + * @param {string} options.userID The user's ID that started the process + * @param {number} options.timeout + * @param {string|Function} [key='name'] The path to use for searches + */ + async search(query, { channelID, userID, timeout }, key = 'name') { + if (!query) + return this.choose(channelID, userID, timeout); + + const results = fuzzy.filter(query, this.pager.items, { + extract: item => { + if (typeof key === 'string') + return lodash.get(item, key); + else if (typeof key === 'function') + return key(item); + else if (key === null) + return item; + } + }).map(el => el.original); + + if (!results.length) + return { _noresults: true }; + + const tempItems = this.pager.items; + this.pager.items = results; + const result = await this.choose(channelID, userID, timeout); + this.pager.items = tempItems; + return result; + } +} + +GenericPrompt.CANCEL_TRIGGERS = [ + 'c', 'cancel', 's', 'stop' +]; + +module.exports = GenericPrompt; \ No newline at end of file diff --git a/src/structures/Halt.js b/src/structures/Halt.js new file mode 100644 index 0000000..38559cd --- /dev/null +++ b/src/structures/Halt.js @@ -0,0 +1,61 @@ +const EventEmitter = require('eventemitter3'); + +/** + * A class that represents a message halt + */ +class Halt extends EventEmitter { + constructor(messageAwaiter, timeout) { + super(); + this.messageAwaiter = messageAwaiter; + this.timeout = timeout; + this.interval = null; + this.ended = false; + this.messages = new Map(); + this._endBind = this._end.bind(this); + this._start(); + } + + /** + * Restarts the halt. + * @param {number} [timeout] The new timeout to halt by + */ + restart(timeout) { + if (this.ended) return; + clearTimeout(this.interval); + this.interval = setTimeout(this._endBind, timeout || this.timeout); + } + + /** + * Ends the halt. + */ + end() { + if (this.ended) return; + clearTimeout(this.interval); + this._end(); + } + + /** + * @private + */ + _onMessage(message) { + this.messages.set(message.id, message); + this.emit('message', message); + } + + /** + * @private + */ + _start() { + this.interval = setTimeout(this._endBind, this.timeout); + } + + /** + * @private + */ + _end() { + this.ended = true; + this.emit('end'); + } +} + +module.exports = Halt; \ No newline at end of file diff --git a/src/structures/MultiSelect.js b/src/structures/MultiSelect.js new file mode 100644 index 0000000..2fb60f0 --- /dev/null +++ b/src/structures/MultiSelect.js @@ -0,0 +1,145 @@ +const EventEmitter = require('eventemitter3'); +const GenericPager = require('./GenericPager'); +const Paginator = require('./Paginator'); +const lodash = require('lodash'); + +/** + * A prompt that allows users to toggle multiple values + */ +class MultiSelect extends EventEmitter { + /** + * @param {TrelloBot} client The client to use + * @param {Message} message The user's message to read permissions from + * @param {Object} options The options for the multi-select + * @param {string|Array} options.path The path of the boolean + * @param {string} [options.checkEmoji] The emoji that resembles true + * @param {string} [options.uncheckEmoji] The emoji that resembles false + * @param {Object} pagerOptions The options for the pager + */ + constructor(client, message, { path, checkEmoji = '☑️', uncheckEmoji = '⬜' }, pagerOptions = {}) { + super(); + this.client = client; + this.message = message; + this.pagerOptions = pagerOptions; + this.displayFunc = pagerOptions.display || ((item) => item.toString()); + this.boolPath = path; + + // Override some pager options + this.pagerOptions.display = (item, i, ai) => { + const value = lodash.get(item, this.boolPath); + return `\`[${ai + 1}]\` ${value ? checkEmoji : uncheckEmoji} ${this.displayFunc(item, i, ai)}`; + }; + this.pagerOptions.header = pagerOptions.header || 'Type the number of the item to toggle its value.'; + this.pagerOptions.footer = (pagerOptions.footer ? pagerOptions.footer + '\n\n' : '') + + 'Type "save" to save the selection, otherwise type "cancel" to exit.'; + this.pagerOptions.embedExtra = this.pagerOptions.embedExtra || {}; + this.pagerOptions.embedExtra.author = { + name: `${message.author.username}#${message.author.discriminator}`, + icon_url: message.author.avatarURL || message.author.defaultAvatarURL + }; + + this.pager = new GenericPager(client, message, this.pagerOptions); + this.halt = null; + } + + /** + * Starts the prompt + * @param {string} channelID The channel to post the new message to + * @param {string} userID The user's ID that started the process + * @param {number} timeout + */ + async start(channelID, userID, timeout) { + if (this.pager.items.length === 0) + return null; + + await this.pager.start(channelID, userID, timeout); + // React with done + if (this.pager.collector) + await this.pager.message.addReaction(MultiSelect.DONE); + this.halt = this.client.messageAwaiter.createHalt(channelID, userID, timeout); + + // Sync timeouts + if (this.pager.collector) + this.pager.collector.restart(); + this.halt.restart(); + + return new Promise(resolve => { + let result = null; + + this.halt.on('message', nextMessage => { + if (this.pager.canManage()) + nextMessage.delete(); + + if (MultiSelect.CANCEL_TRIGGERS.includes(nextMessage.content.toLowerCase())) { + result = { _canceled: true }; + this.halt.end(); + } else if (MultiSelect.DONE_TRIGGERS.includes(nextMessage.content.toLowerCase())) { + result = this.pager.items; + this.halt.end(); + } + + // Find and update item + const chosenIndex = parseInt(nextMessage.content); + if (chosenIndex <= 0) return; + let chosenItem = this.pager.items[chosenIndex - 1]; + if (chosenItem !== undefined) { + const oldItem = chosenItem; + chosenItem = lodash.set(chosenItem, this.boolPath, !lodash.get(chosenItem, this.boolPath)); + this.emit('update', oldItem, chosenItem, chosenIndex - 1); + this.pager.items.splice(chosenIndex - 1, 1, chosenItem); + this.pager.updateMessage(); + } + + this.halt.restart(); + if (this.pager.collector) + this.pager.collector.restart(); + }); + + this.halt.on('end', () => { + // In case the halt ends before reactions are finished coming up + this.pager.reactionsCleared = true; + if (this.pager.collector) + this.pager.collector.end(); + this.pager.message.delete().catch(() => {}); + + if (result && result._canceled) { + this.pager.message.channel.createMessage( + `<@${userID}>, Your last prompt was canceled. All changes have been lost.`); + result = null; + } else if (result === null) + this.pager.message.channel.createMessage( + `<@${userID}>, Your last prompt was timed out. All changes have been lost.`); + + resolve(result); + }); + + if (this.pager.collector) { + this.pager.collector.on('clearReactions', () => { + if (!this.pager.canManage()) + this.pager.message.removeReaction(MultiSelect.DONE).catch(() => {}); + }); + this.pager.collector.on('reaction', emoji => { + if (Paginator.STOP === emoji.name) { + result = { _canceled: true }; + this.halt.end(); + } else if (MultiSelect.DONE === emoji.name) { + result = this.pager.items; + this.halt.end(); + } + + this.halt.restart(); + }); + } + }); + } +} + +MultiSelect.DONE = '✅'; +MultiSelect.CANCEL_TRIGGERS = [ + 'cancel', 'stop', 'quit' +]; +MultiSelect.DONE_TRIGGERS = [ + 'save', 'finish', 'done' +]; + +module.exports = MultiSelect; \ No newline at end of file diff --git a/src/structures/Paginator.js b/src/structures/Paginator.js new file mode 100644 index 0000000..af7a0d3 --- /dev/null +++ b/src/structures/Paginator.js @@ -0,0 +1,184 @@ +const EventEmitter = require('eventemitter3'); + +/** + * A class that creates a paging process for messages + */ +class Paginator extends EventEmitter { + /** + * @param {TrelloBot} client The client to use + * @param {Message} message The user's message to read permissions from + * @param {Object} options The options for the paginator + * @param {Array} options.items The items the paginator will display + * @param {number} [options.itemsPerPage=15] How many items a page will have + */ + constructor(client, message, { items = [], itemsPerPage = 15 } = {}) { + super(); + this.messageAwaiter = client.messageAwaiter; + this.client = client; + this.collector = null; + this.items = items; + this.message = message; + this.itemsPerPage = itemsPerPage; + this.pageNumber = 1; + this.reactionsCleared = false; + this._reactBind = this._react.bind(this); + } + + /** + * All pages in the paginator + * @type {Array} + */ + get pages() { + const pages = []; + let i, j, page; + for (i = 0, j = this.items.length; i < j; i += this.itemsPerPage) { + page = this.items.slice(i, i + this.itemsPerPage); + pages.push(page); + } + return pages; + } + + /** + * The current page + * @type {Array} + */ + get page() { + return this.pages[this.pageNumber - 1]; + } + + /** + * The current page number + * @type {number} + */ + get maxPages() { + return Math.ceil(this.items.length / this.itemsPerPage); + } + + /** + * Changes the page number + * @param {number} newPage The page to change to + */ + toPage(newPage) { + if (Number(newPage)){ + this.pageNumber = Number(newPage); + if (this.pageNumber < 1) this.pageNumber = 1; + if (this.pageNumber > this.maxPages) this.pageNumber = this.maxPages; + } + return this; + } + + /** + * Moves to the next page + */ + nextPage() { + return this.toPage(this.pageNumber + 1); + } + + /** + * Moves to the previous page + */ + previousPage() { + return this.toPage(this.pageNumber - 1); + } + + /** + * Whether or not this instance can paginate + * @returns {boolean} + */ + canPaginate() { + return this.message.channel.type === 1 || + this.message.channel.permissionsOf(this.client.user.id).has('addReactions'); + } + + /** + * Whether or not this instance can manage messages + * @returns {boolean} + */ + canManage() { + return this.message.channel.type !== 1 && + this.message.channel.permissionsOf(this.client.user.id).has('manageMessages'); + } + + /** + * Starts the reaction collector and pagination + * @param {string} userID The user's ID that started the process + * @param {number} timeout + */ + async start(userID, timeout) { + this.reactionsCleared = false; + if (this.maxPages > 1 && this.canPaginate()) { + try { + await Promise.all([ + this.message.addReaction(Paginator.PREV), + this.message.addReaction(Paginator.STOP), + this.message.addReaction(Paginator.NEXT), + ]); + this.collector = this.messageAwaiter.createReactionCollector(this.message, userID, timeout); + this._hookEvents(); + } catch (e) { + return this.clearReactions(); + } + } + } + + /** + * Clears reaction from the message + */ + async clearReactions() { + if (!this.reactionsCleared) { + this.reactionsCleared = true; + this.emit('clearReactions'); + try { + if (!this.canManage()) + await Promise.all([ + this.message.removeReaction(Paginator.NEXT).catch(() => {}), + this.message.removeReaction(Paginator.STOP).catch(() => {}), + this.message.removeReaction(Paginator.PREV).catch(() => {}) + ]); + else + await this.message.removeReactions().catch(() => {}); + } catch (e) { + // Do nothing + } + } + } + + /** + * @private + */ + _hookEvents() { + this.collector.on('reaction', this._react.bind(this)); + this.collector.once('end', this.clearReactions.bind(this)); + } + + /** + * @private + */ + _change() { + this.emit('change', this.pageNumber); + } + + /** + * @private + */ + _react(emoji, userID) { + const oldPage = this.pageNumber; + if (Paginator.PREV == emoji.name) + this.previousPage(); + else if (Paginator.NEXT == emoji.name) + this.nextPage(); + else if (Paginator.STOP == emoji.name) + this.collector.end(); + if (this.pageNumber !== oldPage) + this._change(); + if ([Paginator.PREV, Paginator.STOP, Paginator.NEXT].includes(emoji.name) && this.canManage()) + this.message.removeReaction(emoji.name, userID).catch(() => {}); + this.collector.restart(); + } +} + +Paginator.PREV = '⬅️'; +Paginator.STOP = '🛑'; +Paginator.NEXT = '➡️'; + +module.exports = Paginator; \ No newline at end of file diff --git a/src/structures/ReactionCollector.js b/src/structures/ReactionCollector.js new file mode 100644 index 0000000..f1db617 --- /dev/null +++ b/src/structures/ReactionCollector.js @@ -0,0 +1,60 @@ +const EventEmitter = require('eventemitter3'); + +/** + * A class that collects reactions from a message + */ +class ReactionCollector extends EventEmitter { + constructor(messageAwaiter, timeout) { + super(); + this.messageAwaiter = messageAwaiter; + this.timeout = timeout; + this.interval = null; + this.ended = false; + this._endBind = this._end.bind(this); + this._start(); + } + + /** + * Restarts the timeout. + * @param {number} [timeout] The new timeout to halt by + */ + restart(timeout) { + if (this.ended) return; + clearTimeout(this.interval); + this.interval = setTimeout(this._endBind, timeout || this.timeout); + } + + /** + * Ends the collection. + */ + end() { + if (this.ended) return; + clearTimeout(this.interval); + this._end(); + } + + /** + * @private + */ + _onReaction(emoji, userID) { + this.emit('reaction', emoji, userID); + this.restart(); + } + + /** + * @private + */ + _start() { + this.interval = setTimeout(this._endBind, this.timeout); + } + + /** + * @private + */ + _end() { + this.ended = true; + this.emit('end'); + } +} + +module.exports = ReactionCollector; \ No newline at end of file diff --git a/src/structures/SubMenu.js b/src/structures/SubMenu.js new file mode 100644 index 0000000..744719f --- /dev/null +++ b/src/structures/SubMenu.js @@ -0,0 +1,44 @@ +const GenericPrompt = require('./GenericPrompt'); + +class SubMenu { + /** + * @param {TrelloBot} client The client to use + * @param {Message} message The user's message to read permissions from + * @param {Object} pagerOptions The options for the pager + */ + constructor(client, message, pagerOptions = {}) { + this.client = client; + this.message = message; + this.pagerOptions = pagerOptions; + this.prompt = new GenericPrompt(client, message, this.pagerOptions); + } + + /** + * Starts the menu + * @param {string} channelID The channel to post the new message to + * @param {string} userID The user's ID that started the process + * @param {Array} menu + * @param {number} timeout + */ + async start(channelID, userID, name, menu = [], timeout = 30000) { + /* + menu = [ + { + names: ['a', 'b'], + title: 'Title', + exec: (client) => ... + } + ] + */ + const command = menu.find(command => command.names.includes(name ? name.toLowerCase() : null)); + if (!command) { + this.prompt.pager.items = menu; + this.prompt.pager.displayFunc = (item, _, ai) => `\`[${ai + 1}]\` ${item.title}`; + const chosenCommand = await this.prompt.choose(channelID, userID, timeout); + if (!chosenCommand) return; + return chosenCommand.exec(this.client); + } else return command.exec(this.client); + } +} + +module.exports = SubMenu; \ No newline at end of file diff --git a/src/util.js b/src/util.js new file mode 100644 index 0000000..b596b3b --- /dev/null +++ b/src/util.js @@ -0,0 +1,258 @@ +const fetch = require('node-fetch'); +const config = require('config'); + +/** + * Represents the utilities for the bot + * @typedef {Object} Util + */ +const Util = module.exports = {}; + +/** + * Iterates through each key of an object + * @memberof Util. + */ +Util.keyValueForEach = (obj, func) => Object.keys(obj).map(key => func(key, obj[key])); + +/** + * @memberof Util. + * @deprecated + */ +Util.sliceKeys = (obj, f) => { + const newObject = {}; + Util.keyValueForEach(obj, (k, v) => { + if (f(k, v)) newObject[k] = v; + }); + return newObject; +}; + +/** + * Converts a number into a 00:00:00 format + * @memberof Util. + */ +Util.toHHMMSS = string => { + const sec_num = parseInt(string, 10); + let hours = Math.floor(sec_num / 3600); + let minutes = Math.floor((sec_num - (hours * 3600)) / 60); + let seconds = sec_num - (hours * 3600) - (minutes * 60); + + if (hours < 10) {hours = '0' + hours;} + if (minutes < 10) {minutes = '0' + minutes;} + if (seconds < 10) {seconds = '0' + seconds;} + const time = hours + ':' + minutes + ':' + seconds; + return time; +}; + +/** + * @memberof Util. + * @deprecated + */ +Util.formatNumber = num => num.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,'); + +/** + * Flattens a JSON object + * @memberof Util. + * @see https://stackoverflow.com/a/19101235/6467130 + */ +Util.flattenObject = (data) => { + const result = {}; + function recurse (cur, prop) { + if (Object(cur) !== cur) { + result[prop] = cur; + } else if (Array.isArray(cur)) { + const l = cur.length; + for (let i = 0; i < l; i++) + recurse(cur[i], prop + '[' + i + ']'); + if (l == 0) + result[prop] = []; + } else { + let isEmpty = true; + for (const p in cur) { + isEmpty = false; + recurse(cur[p], prop ? prop + '.' + p : p); + } + if (isEmpty && prop) + result[prop] = {}; + } + } + recurse(data, ''); + return result; +}; + +/** + * Randomness generator + * @memberof Util. + */ +Util.Random = { + int(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; + }, + bool() { + return Util.Random.int(0, 1) === 1; + }, + array(array) { + return array[Util.Random.int(0, array.length - 1)]; + }, + shuffle(array) { + return array.sort(() => Math.random() - 0.5); + }, +}; + +/** + * Prefix-related functions + * @memberof Util. + */ +Util.Prefix = { + regex(client, prefixes = null) { + if (!prefixes) + prefixes = [client.config.prefix]; + return new RegExp(`^((?:<@!?${client.user.id}>|${ + prefixes.map(prefix => Util.Escape.regex(prefix)).join('|')})\\s?)(\\n|.)`, 'i'); + }, + strip(message, client, prefixes) { + return message.content.replace( + Util.Prefix.regex(client, prefixes), '$2').replace(/\s\s+/g, ' ').trim(); + }, +}; + +/** + * Commonly used regex patterns + * @memberof Util. + * @deprecated + */ +Util.Regex = { + escape: /[-/\\^$*+?.()|[\]{}]/g, + url: /https?:\/\/(-\.)?([^\s/?.#-]+\.?)+(\/[^\s]*)?/gi, + userMention: /<@!?(\d+)>/gi, + webhookURL: + /(?:https?:\/\/)(?:canary\.|ptb\.|)discord(?:app)?\.com\/api\/webhooks\/(\d{17,18})\/([\w-]{68})/ +}; + +/** + * Discord.JS's method of escaping characters + * @memberof Util. + */ +Util.Escape = { + regex(s) { + return s.replace(Util.Regex.escape, '\\$&'); + } +}; + +/** + * Command permission parsers + * @memberof Util. + */ +Util.CommandPermissions = { + attach: (client, message) => message.channel.type === 1 || + message.channel.permissionsOf(client.user.id).has('attachFiles'), + embed: (client, message) => message.channel.type === 1 || + message.channel.permissionsOf(client.user.id).has('embedLinks'), + emoji: (client, message) => message.channel.type === 1 || + message.channel.permissionsOf(client.user.id).has('externalEmojis'), + guild: (_, message) => !!message.guildID, + elevated: (client, message) => client.config.elevated.includes(message.author.id), + curator: (client, message) => { + if (!message.guildID) return false; + // Server owner or elevated users + if (message.channel.guild.ownerID == message.author.id || + Util.CommandPermissions.elevated(client, message)) return true; + return message.member.roles.includes(config.curatorRoleID); + }, + admin: (client, message) => { + if (!message.guildID) return false; + // Server owner or elevated users + if (message.channel.guild.ownerID == message.author.id || + Util.CommandPermissions.elevated(client, message)) return true; + return message.member.roles.includes(config.adminRoleID); + }, + curatorOrAdmin: (client, message) => { + if (!message.guildID) return false; + // Server owner or elevated users + if (message.channel.guild.ownerID == message.author.id || + Util.CommandPermissions.elevated(client, message)) return true; + return message.member.roles.includes(config.curatorRoleID) || + message.member.roles.includes(config.adminRoleID); + }, +}; + +/** + * Creates a module that makes emoji fallbacks + * @memberof Util. + */ +Util.emojiFallback = ({ message, client }) => { + return (id, fallback, animated = false) => { + if (Util.CommandPermissions.emoji(client, message)) + return `<${animated ? 'a' : ''}:_:${id}>`; + else return fallback; + }; +}; + +/** + * Cuts off text to a limit + * @memberof Util. + * @param {string} text + * @param {number} limit + */ +Util.cutoffText = (text, limit = 2000) => { + return text.length > limit ? text.slice(0, limit - 1) + '…' : text; +}; + +/** + * Cuts off an array of text to a limit + * @memberof Util. + * @param {Array} texts + * @param {number} limit + * @param {number} rollbackAmount Amount of items to roll back when the limit has been hit + */ +Util.cutoffArray = (texts, limit = 2000, rollbackAmount = 1, paddingAmount = 1) => { + let currLength = 0; + for (let i = 0; i < texts.length; i++) { + const text = texts[i]; + currLength += text.length + paddingAmount; + if (currLength > limit) { + const clampedRollback = rollbackAmount > i ? i : rollbackAmount; + return texts.slice(0, (i + 1) - clampedRollback); + } + } + return texts; +}; + +/** + * Hastebin-related functions + * @memberof Util. + */ +Util.Hastebin = { + async autosend(content, message) { + if (content.length > 2000) { + const haste = await Util.Hastebin.post(content); + if (haste.ok) + return message.channel.createMessage(``); + else + return message.channel.createMessage({}, { + name: 'output.txt', + file: new Buffer(content) + }); + } else return message.channel.createMessage(content); + }, + /** + * Post text to hastebin + * @param {string} content - The content to upload + */ + async post(content) { + const haste = await fetch('https://hastebin.com/documents', { + method: 'POST', + body: content + }); + if (haste.status >= 400) + return { + ok: false, + status: haste.status + }; + else { + const hasteInfo = await haste.json(); + return { + ok: true, + key: hasteInfo.key + }; + } + } +}; \ No newline at end of file diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..ef19439 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,1032 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/code-frame@^7.0.0": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e" + integrity sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g== + dependencies: + "@babel/highlight" "^7.8.3" + +"@babel/helper-validator-identifier@^7.9.0": + version "7.9.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz#90977a8e6fbf6b431a7dc31752eee233bf052d80" + integrity sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g== + +"@babel/highlight@^7.8.3": + version "7.9.0" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.9.0.tgz#4e9b45ccb82b79607271b2979ad82c7b68163079" + integrity sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ== + dependencies: + "@babel/helper-validator-identifier" "^7.9.0" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@dabh/diagnostics@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.2.tgz#290d08f7b381b8f94607dc8f471a12c675f9db31" + integrity sha512-+A1YivoVDNNVCdfozHSR8v/jyuuLTMXwjWuxPFlFlUapXoGc+Gj9mDlTDDfrwl7rXCl2tNZ0kE8sIBO6YOn96Q== + dependencies: + colorspace "1.1.x" + enabled "2.0.x" + kuler "^2.0.0" + +"@types/color-name@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" + integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== + +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + +acorn-jsx@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.2.0.tgz#4c66069173d6fdd68ed85239fc256226182b2ebe" + integrity sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ== + +acorn@^7.3.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.3.1.tgz#85010754db53c3fbaf3b9ea3e083aa5c5d147ffd" + integrity sha512-tLc0wSnatxAQHVHUapaHdz72pi9KUyHjq5KyHjGg9Y8Ifdc79pTh2XvI6I1/chZbnM7QtNKzh66ooDogPZSleA== + +ajv@^6.10.0, ajv@^6.10.2: + version "6.12.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.2.tgz#c629c5eced17baf314437918d2da88c99d5958cd" + integrity sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-colors@^3.2.1: + version "3.2.4" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" + integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA== + +ansi-regex@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" + integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== + +ansi-regex@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" + integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== + +ansi-styles@^3.2.0, ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" + integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA== + dependencies: + "@types/color-name" "^1.1.1" + color-convert "^2.0.1" + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +astral-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" + integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== + +async@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720" + integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw== + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +cat-loggr@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/cat-loggr/-/cat-loggr-1.1.0.tgz#ed47d95213a20c619d9c36ba4ae1272f4fa8ef7c" + integrity sha512-91HA5xug2mYbfBqKd4ldAUAo7rTQpA39Cy3rzM7rfPynueiYSBe60tskwP8SriG+UwZKCsKycTk7FyR1N557zg== + dependencies: + chalk "^2.4.1" + moment "^2.22.2" + +chalk@^2.0.0, chalk@^2.4.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.0.0.tgz#6e98081ed2d17faab615eb52ac66ec1fe6209e72" + integrity sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +color-convert@^1.9.0, color-convert@^1.9.1: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +color-name@^1.0.0, color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +color-string@^1.5.2: + version "1.5.3" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.3.tgz#c9bbc5f01b58b5492f3d6857459cb6590ce204cc" + integrity sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw== + dependencies: + color-name "^1.0.0" + simple-swizzle "^0.2.2" + +color@3.0.x: + version "3.0.0" + resolved "https://registry.yarnpkg.com/color/-/color-3.0.0.tgz#d920b4328d534a3ac8295d68f7bd4ba6c427be9a" + integrity sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w== + dependencies: + color-convert "^1.9.1" + color-string "^1.5.2" + +colors@^1.2.1: + version "1.4.0" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" + integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== + +colorspace@1.1.x: + version "1.1.2" + resolved "https://registry.yarnpkg.com/colorspace/-/colorspace-1.1.2.tgz#e0128950d082b86a2168580796a0aa5d6c68d8c5" + integrity sha512-vt+OoIP2d76xLhjwbBaucYlNSpPsrJWPlBTtwCpQKIu6/CSMutyzX93O/Do0qzpH3YoHEes8YEFXyZ797rEhzQ== + dependencies: + color "3.0.x" + text-hex "1.0.x" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +config@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/config/-/config-3.3.1.tgz#b6a70e2908a43b98ed20be7e367edf0cc8ed5a19" + integrity sha512-+2/KaaaAzdwUBE3jgZON11L1ggLLhpf2FsGrfqYFHZW22ySGv/HqYIXrBwKKvn+XZh1UBUjHwAcrfsSkSygT+Q== + dependencies: + json5 "^2.1.1" + +core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +cross-spawn@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.2.tgz#d0d7dcfa74e89115c7619f4f721a94e1fdb716d6" + integrity sha512-PD6G8QG3S4FK/XCGFbEQrDqO2AnMMsy0meR7lerlIOHAAbkuavGU/pOqprrlvfTNjvowivTeBsjebAL0NSoMxw== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +debug@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" + integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== + dependencies: + ms "^2.1.1" + +deep-is@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" + integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= + +denque@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/denque/-/denque-1.4.1.tgz#6744ff7641c148c3f8a69c307e51235c1f4a37cf" + integrity sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ== + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +emoji-regex@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" + integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== + +enabled@2.0.x: + version "2.0.0" + resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2" + integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ== + +enquirer@^2.3.5: + version "2.3.5" + resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.5.tgz#3ab2b838df0a9d8ab9e7dff235b0e8712ef92381" + integrity sha512-BNT1C08P9XD0vNg3J475yIUG+mVdp9T6towYFHUv897X0KoHBjB1shyrNmhmtHWKP17iSWgo7Gqh7BBuzLZMSA== + dependencies: + ansi-colors "^3.2.1" + +eris@^0.13.3: + version "0.13.3" + resolved "https://registry.yarnpkg.com/eris/-/eris-0.13.3.tgz#22abb71f9ce0d453200b537cad457dc39c7b302b" + integrity sha512-WBtLyknOWZpYZL9yPhez0oKUWvYpunSg43hGxawwjwSf3gFXmbEPYrT8KlmZXtpJnX16eQ7mzIq+MgSh3LarEg== + dependencies: + ws "^7.2.1" + optionalDependencies: + opusscript "^0.0.7" + tweetnacl "^1.0.1" + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +eslint-scope@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.0.tgz#d0f971dfe59c69e0cada684b23d49dbf82600ce5" + integrity sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w== + dependencies: + esrecurse "^4.1.0" + estraverse "^4.1.1" + +eslint-utils@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" + integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== + dependencies: + eslint-visitor-keys "^1.1.0" + +eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" + integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== + +eslint@^7.6.0: + version "7.6.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.6.0.tgz#522d67cfaea09724d96949c70e7a0550614d64d6" + integrity sha512-QlAManNtqr7sozWm5TF4wIH9gmUm2hE3vNRUvyoYAa4y1l5/jxD/PQStEjBMQtCqZmSep8UxrcecI60hOpe61w== + dependencies: + "@babel/code-frame" "^7.0.0" + ajv "^6.10.0" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.0.1" + doctrine "^3.0.0" + enquirer "^2.3.5" + eslint-scope "^5.1.0" + eslint-utils "^2.1.0" + eslint-visitor-keys "^1.3.0" + espree "^7.2.0" + esquery "^1.2.0" + esutils "^2.0.2" + file-entry-cache "^5.0.1" + functional-red-black-tree "^1.0.1" + glob-parent "^5.0.0" + globals "^12.1.0" + ignore "^4.0.6" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + js-yaml "^3.13.1" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash "^4.17.19" + minimatch "^3.0.4" + natural-compare "^1.4.0" + optionator "^0.9.1" + progress "^2.0.0" + regexpp "^3.1.0" + semver "^7.2.1" + strip-ansi "^6.0.0" + strip-json-comments "^3.1.0" + table "^5.2.3" + text-table "^0.2.0" + v8-compile-cache "^2.0.3" + +espree@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-7.2.0.tgz#1c263d5b513dbad0ac30c4991b93ac354e948d69" + integrity sha512-H+cQ3+3JYRMEIOl87e7QdHX70ocly5iW4+dttuR8iYSPr/hXKFb+7dBsZ7+u1adC4VrnPlTkv0+OwuPnDop19g== + dependencies: + acorn "^7.3.1" + acorn-jsx "^5.2.0" + eslint-visitor-keys "^1.3.0" + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esquery@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.3.1.tgz#b78b5828aa8e214e29fb74c4d5b752e1c033da57" + integrity sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf" + integrity sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ== + dependencies: + estraverse "^4.1.0" + +estraverse@^4.1.0, estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.1.0.tgz#374309d39fd935ae500e7b92e8a6b4c720e59642" + integrity sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + +eventemitter3@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.4.tgz#b5463ace635a083d018bdc7c917b4c5f10a85384" + integrity sha512-rlaVLnVxtxvoyLsQQFBx53YmXHDxRIzzTLbdfxqi4yocpSjAxXwkU0cScM5JgSKMqEhrZpnvQ2D9gjylR0AimQ== + +fast-deep-equal@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4" + integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= + +fast-safe-stringify@^2.0.4: + version "2.0.7" + resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743" + integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA== + +fecha@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.0.tgz#3ffb6395453e3f3efff850404f0a59b6747f5f41" + integrity sha512-aN3pcx/DSmtyoovUudctc8+6Hl4T+hI9GBBHLjA76jdZl7+b1sgh5g4k+u/GL3dTy1/pnYzKp69FpJ0OicE3Wg== + +file-entry-cache@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c" + integrity sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g== + dependencies: + flat-cache "^2.0.1" + +flat-cache@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0" + integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA== + dependencies: + flatted "^2.0.0" + rimraf "2.6.3" + write "1.0.3" + +flatted@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138" + integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA== + +fn.name@1.x.x: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc" + integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +functional-red-black-tree@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" + integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= + +fuzzy@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/fuzzy/-/fuzzy-0.1.3.tgz#4c76ec2ff0ac1a36a9dccf9a00df8623078d4ed8" + integrity sha1-THbsL/CsGjap3M+aAN+GIweNTtg= + +glob-parent@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" + integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== + dependencies: + is-glob "^4.0.1" + +glob@^7.1.3: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + 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" + +globals@^12.1.0: + version "12.4.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-12.4.0.tgz#a18813576a41b00a24a97e7f815918c2e19925f8" + integrity sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg== + dependencies: + type-fest "^0.8.1" + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +ignore@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" + integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== + +import-fresh@^3.0.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66" + integrity sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@^2.0.3, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +is-arrayish@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" + integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= + +is-glob@^4.0.0, is-glob@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" + integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== + dependencies: + is-extglob "^2.1.1" + +is-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" + integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^3.13.1: + version "3.13.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" + integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= + +json5@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43" + integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA== + dependencies: + minimist "^1.2.5" + +kuler@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3" + integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A== + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +lodash@^4.17.14, lodash@^4.17.19: + version "4.17.19" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" + integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== + +logform@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/logform/-/logform-2.2.0.tgz#40f036d19161fc76b68ab50fdc7fe495544492f2" + integrity sha512-N0qPlqfypFx7UHNn4B3lzS/b0uLqt2hmuoa+PpuXNYgozdJYAyauF5Ky0BWVjrxDlMWiT3qN4zPq3vVAfZy7Yg== + dependencies: + colors "^1.2.1" + fast-safe-stringify "^2.0.4" + fecha "^4.2.0" + ms "^2.1.1" + triple-beam "^1.3.0" + +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimist@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + +mkdirp@^0.5.1: + version "0.5.5" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== + dependencies: + minimist "^1.2.5" + +moment@^2.22.2, moment@^2.27.0: + version "2.27.0" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d" + integrity sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ== + +ms@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= + +node-fetch@^2.3.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" + integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +one-time@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/one-time/-/one-time-1.0.0.tgz#e06bc174aed214ed58edede573b433bbf827cb45" + integrity sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g== + dependencies: + fn.name "1.x.x" + +optionator@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" + integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.3" + +opusscript@^0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/opusscript/-/opusscript-0.0.7.tgz#7dd7ec55b302d26bf588e6fc3feb090b8c7da856" + integrity sha512-DcBadTdYTUuH9zQtepsLjQn4Ll6rs3dmeFvN+SD0ThPnxRBRm/WC1zXWPg+wgAJimB784gdZvUMA57gDP7FdVg== + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +progress@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + +punycode@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +readable-stream@^2.3.7: + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + dependencies: + 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" + +readable-stream@^3.4.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +redis-commands@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.5.0.tgz#80d2e20698fe688f227127ff9e5164a7dd17e785" + integrity sha512-6KxamqpZ468MeQC3bkWmCB1fp56XL64D4Kf0zJSwDZbVLLm7KFkoIcHrgRvQ+sk8dnhySs7+yBg94yIkAK7aJg== + +redis-errors@^1.0.0, redis-errors@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" + integrity sha1-62LSrbFeTq9GEMBK/hUpOEJQq60= + +redis-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4" + integrity sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ= + dependencies: + redis-errors "^1.0.0" + +redis@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/redis/-/redis-3.0.2.tgz#bd47067b8a4a3e6a2e556e57f71cc82c7360150a" + integrity sha512-PNhLCrjU6vKVuMOyFu7oSP296mwBkcE6lrAjruBYG5LgdSqtRBoVQIylrMyVZD/lkF24RSNNatzvYag6HRBHjQ== + dependencies: + denque "^1.4.1" + redis-commands "^1.5.0" + redis-errors "^1.2.0" + redis-parser "^3.0.0" + +regexpp@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2" + integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q== + +require-reload@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/require-reload/-/require-reload-0.2.2.tgz#29a7591846caf91b6e8a3cda991683f95f8d7d42" + integrity sha1-KadZGEbK+RtuijzamRaD+V+NfUI= + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +rimraf@2.6.3: + version "2.6.3" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" + integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== + dependencies: + glob "^7.1.3" + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +semver@^7.2.1: + version "7.3.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" + integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +simple-swizzle@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" + integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo= + dependencies: + is-arrayish "^0.3.1" + +slice-ansi@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636" + integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ== + dependencies: + ansi-styles "^3.2.0" + astral-regex "^1.0.0" + is-fullwidth-code-point "^2.0.0" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + +stack-trace@0.0.x: + version "0.0.10" + resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" + integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA= + +string-width@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" + integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== + dependencies: + emoji-regex "^7.0.1" + is-fullwidth-code-point "^2.0.0" + strip-ansi "^5.1.0" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== + dependencies: + ansi-regex "^4.1.0" + +strip-ansi@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" + integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== + dependencies: + ansi-regex "^5.0.0" + +strip-json-comments@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.0.tgz#7638d31422129ecf4457440009fba03f9f9ac180" + integrity sha512-e6/d0eBu7gHtdCqFt0xJr642LdToM5/cN4Qb9DbHjVx1CP5RyeM+zH7pbecEmDv/lBqb0QH+6Uqq75rxFPkM0w== + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" + integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== + dependencies: + has-flag "^4.0.0" + +table@^5.2.3: + version "5.4.6" + resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" + integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug== + dependencies: + ajv "^6.10.2" + lodash "^4.17.14" + slice-ansi "^2.1.0" + string-width "^3.0.0" + +text-hex@1.0.x: + version "1.0.0" + resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5" + integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg== + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= + +triple-beam@^1.2.0, triple-beam@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9" + integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw== + +tweetnacl@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596" + integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw== + +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +type-fest@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" + integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== + +uri-js@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" + integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== + dependencies: + punycode "^2.1.0" + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +v8-compile-cache@^2.0.3: + version "2.1.0" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz#e14de37b31a6d194f5690d67efc4e7f6fc6ab30e" + integrity sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g== + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +winston-transport@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.4.0.tgz#17af518daa690d5b2ecccaa7acf7b20ca7925e59" + integrity sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw== + dependencies: + readable-stream "^2.3.7" + triple-beam "^1.2.0" + +winston@^3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/winston/-/winston-3.3.3.tgz#ae6172042cafb29786afa3d09c8ff833ab7c9170" + integrity sha512-oEXTISQnC8VlSAKf1KYSSd7J6IWuRPQqDdo8eoRNaYKLvwSb5+79Z3Yi1lrl6KDpU6/VWaxpakDAtb1oQ4n9aw== + dependencies: + "@dabh/diagnostics" "^2.0.2" + async "^3.1.0" + is-stream "^2.0.0" + logform "^2.2.0" + one-time "^1.0.0" + readable-stream "^3.4.0" + stack-trace "0.0.x" + triple-beam "^1.3.0" + winston-transport "^4.4.0" + +word-wrap@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" + integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +write@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3" + integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig== + dependencies: + mkdirp "^0.5.1" + +ws@^7.2.1: + version "7.3.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.0.tgz#4b2f7f219b3d3737bc1a2fbf145d825b94d38ffd" + integrity sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w==