From 311b35ac9c5f6f423239f18f4f1d56310ae8a04c Mon Sep 17 00:00:00 2001 From: pyraxo Date: Sat, 24 Dec 2016 01:24:48 +0800 Subject: [PATCH 01/21] Prepare for rewrite --- .env.example | 33 ---- .gitignore | 13 +- README.md | 41 ---- gulpfile.js | 28 --- index.js | 62 ------ package.json | 6 +- resources/i18n/en/common.json | 60 ------ resources/i18n/en/credits.json | 20 -- resources/i18n/en/help.json | 19 -- src/commands/admin/eval.js | 89 --------- src/commands/admin/kill.js | 22 --- src/commands/admin/reload.js | 31 --- src/commands/core/help.js | 90 --------- src/commands/core/ping.js | 21 --- src/core/Automaton.js | 75 -------- src/core/base/Base.js | 158 ---------------- src/core/base/Command.js | 148 --------------- src/core/base/Module.js | 37 ---- src/core/base/index.js | 1 - src/core/index.js | 7 - src/core/managers/ModelManager.js | 69 ------- src/core/managers/Resolver.js | 127 ------------- src/core/managers/Transmitter.js | 73 -------- src/core/managers/resolvers/channel.js | 25 --- src/core/managers/resolvers/command.js | 10 - src/core/managers/resolvers/commandGroup.js | 7 - src/core/managers/resolvers/integer.js | 16 -- src/core/managers/resolvers/list.js | 26 --- src/core/managers/resolvers/member.js | 30 --- src/core/managers/resolvers/role.js | 23 --- src/core/managers/resolvers/string.js | 32 ---- src/core/system/Bridge.js | 92 --------- src/core/system/Commander.js | 47 ----- src/core/system/Engine.js | 158 ---------------- src/core/system/Router.js | 67 ------- src/core/util/Cache.js | 66 ------- src/core/util/Collection.js | 56 ------ src/core/util/Emojis.js | 21 --- src/core/util/LocalCache.js | 114 ----------- src/core/util/Locales.js | 60 ------ src/core/util/MessageCollector.js | 71 ------- src/core/util/Parser.js | 56 ------ src/core/util/Permitter.js | 95 ---------- src/core/util/Responder.js | 198 -------------------- src/core/util/Util.js | 32 ---- src/core/util/index.js | 11 -- src/index.js | 41 ---- src/ipc/eval.js | 16 -- src/ipc/kill.js | 4 - src/ipc/reload.js | 13 -- src/middleware/checkPrivate.js | 8 - src/middleware/cleverbot.js | 28 --- src/middleware/commandCheck.js | 31 --- src/middleware/parseMessage.js | 15 -- src/middleware/verifySettings.js | 22 --- src/models/Guild.js | 20 -- src/models/User.js | 25 --- 57 files changed, 3 insertions(+), 2763 deletions(-) delete mode 100644 .env.example delete mode 100644 README.md delete mode 100644 gulpfile.js delete mode 100644 index.js delete mode 100644 resources/i18n/en/common.json delete mode 100644 resources/i18n/en/credits.json delete mode 100644 resources/i18n/en/help.json delete mode 100644 src/commands/admin/eval.js delete mode 100644 src/commands/admin/kill.js delete mode 100644 src/commands/admin/reload.js delete mode 100644 src/commands/core/help.js delete mode 100644 src/commands/core/ping.js delete mode 100644 src/core/Automaton.js delete mode 100644 src/core/base/Base.js delete mode 100644 src/core/base/Command.js delete mode 100644 src/core/base/Module.js delete mode 100644 src/core/base/index.js delete mode 100644 src/core/index.js delete mode 100644 src/core/managers/ModelManager.js delete mode 100644 src/core/managers/Resolver.js delete mode 100644 src/core/managers/Transmitter.js delete mode 100644 src/core/managers/resolvers/channel.js delete mode 100644 src/core/managers/resolvers/command.js delete mode 100644 src/core/managers/resolvers/commandGroup.js delete mode 100644 src/core/managers/resolvers/integer.js delete mode 100644 src/core/managers/resolvers/list.js delete mode 100644 src/core/managers/resolvers/member.js delete mode 100644 src/core/managers/resolvers/role.js delete mode 100644 src/core/managers/resolvers/string.js delete mode 100644 src/core/system/Bridge.js delete mode 100644 src/core/system/Commander.js delete mode 100644 src/core/system/Engine.js delete mode 100644 src/core/system/Router.js delete mode 100644 src/core/util/Cache.js delete mode 100644 src/core/util/Collection.js delete mode 100644 src/core/util/Emojis.js delete mode 100644 src/core/util/LocalCache.js delete mode 100644 src/core/util/Locales.js delete mode 100644 src/core/util/MessageCollector.js delete mode 100644 src/core/util/Parser.js delete mode 100644 src/core/util/Permitter.js delete mode 100644 src/core/util/Responder.js delete mode 100644 src/core/util/Util.js delete mode 100644 src/core/util/index.js delete mode 100644 src/index.js delete mode 100644 src/ipc/eval.js delete mode 100644 src/ipc/kill.js delete mode 100644 src/ipc/reload.js delete mode 100644 src/middleware/checkPrivate.js delete mode 100644 src/middleware/cleverbot.js delete mode 100644 src/middleware/commandCheck.js delete mode 100644 src/middleware/parseMessage.js delete mode 100644 src/middleware/verifySettings.js delete mode 100644 src/models/Guild.js delete mode 100644 src/models/User.js diff --git a/.env.example b/.env.example deleted file mode 100644 index feeab0d..0000000 --- a/.env.example +++ /dev/null @@ -1,33 +0,0 @@ -# Bot configuration - -# String of admin IDs, separated by commas -ADMIN_IDS= - -# Token of the bot client -CLIENT_TOKEN= - -# Standard prefix -CLIENT_PREFIX= - -# Debug mode toggling (true/false) -CLIENT_DEBUG= - -# Number of processes (default: number of CPU cores) -CLIENT_PROCESSES= - -# Number of shards per process (default: 1) -CLIENT_SHARDS_PER_PROCESS= - -# Redis databases -REDIS_HOST= -REDIS_PORT= - -# API keys -API_CARBONITEX= -API_DBOTS= - -# RethinkDB options -DB_HOST= -DB_PORT= -DB_DBNAME= -DB_AUTHKEY= diff --git a/.gitignore b/.gitignore index 5e5c27c..575ea31 100644 --- a/.gitignore +++ b/.gitignore @@ -33,19 +33,10 @@ node_modules # Optional REPL history .node_repl_history -# Configuration -resources/config +# Assets +resources .env - -# Databases / Storage -resources/i18n -resources/scripts -resources/images .cache -src/modules -src/commands -src/models - # Documentation docs diff --git a/README.md b/README.md deleted file mode 100644 index b105621..0000000 --- a/README.md +++ /dev/null @@ -1,41 +0,0 @@ -
-

-
- iris -
-

-

The better Discord bot base

-

- -
- Built with ❤ -
- Tatsumaki's Server -

-
- -### Used by -* [Tatsumaki](https://tatsumaki.xyz), a multi-purpose social Discord bot -* [haru](https://pyraxo.com/haru), everyone's favourite idol and part-time bot - -**iris** is an advanced, efficient and highly customisable base for Discord command bots written in Node.js - -### Requirements -* Redis -* Rethinkdb -* **Node.js 7+** - -A firm grasp of **ES6 + async/await** syntax is recommended. - -### Installation -```bash -$ git clone https://github.com/pyraxo/iris -$ cd iris -$ npm i -$ npm start -``` - -### Configuration -Create a new `.env` file from `.env.example` and edit the values as desired. Follow `.env` instructions [here](https://www.npmjs.com/package/dotenv-safe) - -As the bot base uses the [Eris](https://github.com/abalabahaha/Eris) library, please refer to the docs [here](https://abal.moe/ErisDev/docs.html). diff --git a/gulpfile.js b/gulpfile.js deleted file mode 100644 index 5850bb7..0000000 --- a/gulpfile.js +++ /dev/null @@ -1,28 +0,0 @@ -const gulp = require('gulp') -const babel = require('gulp-babel') -const standard = require('gulp-standard') - -const paths = ['src/**/*.js'] - -gulp.task('default', ['build']) - -gulp.task('watch', ['build'], () => { - gulp.watch(paths, ['build']) -}) - -gulp.task('build', ['babel']) - -gulp.task('lint', () => { - gulp.src(paths) - .pipe(standard()) - .pipe(standard.reporter('default', { - breakOnError: true, - quiet: true - })) -}) - -gulp.task('babel', () => { - gulp.src(paths) - .pipe(babel()) - .pipe(gulp.dest('build')) -}) diff --git a/index.js b/index.js deleted file mode 100644 index 816fa1a..0000000 --- a/index.js +++ /dev/null @@ -1,62 +0,0 @@ -global.Promise = require('bluebird') - -const util = require('util') -const chalk = require('chalk') -const path = require('path') -const moment = require('moment') -const winston = require('winston') - -require('longjohn') -require('winston-daily-rotate-file') -require('dotenv-safe').config({ - path: path.join(__dirname, '.env'), - allowEmptyValues: true -}) -require('moment-duration-format') - -const processShards = parseInt(process.env.CLIENT_SHARDS_PER_PROCESS, 10) -const firstShardID = parseInt(process.env.BASE_SHARD_ID, 10) * processShards -const lastShardID = firstShardID + processShards - 1 -const debugMode = process.env.CLIENT_DEBUG === 'true' - -winston.remove(winston.transports.Console) -winston.add(winston.transports.Console, { - level: debugMode ? 'silly' : 'verbose', - colorize: true, - label: process.env.BASE_SHARD_ID - ? processShards > 1 - ? `S ${firstShardID}-${lastShardID}` - : `S ${process.env.SHARD_ID}` - : 'MASTER', - timestamp: function () { - return moment().format('YYYY-MM-DD hh:mm:ss a') - } -}) -winston.add(winston.transports.DailyRotateFile, { - level: debugMode ? 'silly' : 'verbose', - colorize: false, - datePattern: '.yyyy-MM-dd', - filename: path.join(__dirname, 'logs/application.log'), - prepend: true, - json: false, - formatter: function ({ level, message = '', meta = {}, formatter, depth, colorize }) { - const timestamp = moment().format('YYYY-MM-DD hh:mm:ss a') - const obj = Object.keys(meta).length - ? `\n\t${meta.stack ? meta.stack : util.inspect(meta, false, depth || null, colorize)}` - : '' - return `${timestamp} ${level.toUpperCase()} ${chalk.stripColor(message)} ${obj}` - } -}) -winston.cli() - -process.on('unhandledRejection', (reason, promise) => { - if (typeof reason === 'undefined') return - winston.error(`Unhandled rejection: ${reason} - ${util.inspect(promise)}`) -}) - -if (debugMode) { - winston.debug('Running in debug mode') - require('./src') -} else { - require('./build') -} diff --git a/package.json b/package.json index 70c3486..5492187 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,10 @@ { "name": "iris", - "version": "2.2.0", - "private": true, + "version": "3.0.0", "description": "The better Discord bot base", "main": "index.js", "scripts": { "start": "node --harmony_async_await index.js", - "lint": "standard --fix", "gendocs": "node_modules/.bin/jsdoc --configure .jsdoc.json --verbose" }, "engines": { @@ -24,7 +22,6 @@ "dependencies": { "bluebird": "^3.4.6", "chalk": "^1.1.3", - "dotenv-safe": "^2.3.2", "eris": "github:abalabahaha/eris#dev", "eventemitter3": "^2.0.1", "longjohn": "^0.2.11", @@ -34,7 +31,6 @@ "redis": "^2.6.1", "require-all": "^2.0.0", "superagent": "^2.2.0", - "superagent-xml2jsparser": "^0.1.1", "thinky": "^2.3.7", "winston": "^2.2.0", "winston-cluster": "0.0.4", diff --git a/resources/i18n/en/common.json b/resources/i18n/en/common.json deleted file mode 100644 index 87d808a..0000000 --- a/resources/i18n/en/common.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "CHECK_PMS": "check your PMs!", - "ERROR": "an error has occurred! Please try again later, or submit a bug report.", - "ERROR_FULL": "An error has occurred! Please try again later, or submit a bug report.", - "COOLDOWN": "please cool down! ({{time}} seconds left)", - "NO_PERMS": "you need the permission {{perms}} to run this command!", - "NO_PERMS_BOT": "I need the permission {{perms}} to run this command!", - "menus": { - "EXIT": "Enter {{cancel}} to exit the menu.", - "ERRORED": "the menu has closed:", - "EXITED": "you have exited the menu.", - "SELECTION": "Selection Menu", - "ERROR": "Unknown Error", - "INPUT": "Type the number of your choice into chat or '{{cancel}}' to cancel.", - "MORE_RESULTS": "And {{num}} more..." - }, - "collector": { - "timeout": "Timeout after {{time}}s", - "max": "Exceeded {{max}} tries", - "maxMatches": "Exceeded {{maxMatches}} matches" - }, - "resolver": { - "INSUFFICIENT_ARGS": "Insufficient arguments - Expected at least {{requiredArgs}}, saw {{argsCount}}", - "CORRECT_USAGE": "Correct usage", - "NO_END_QUOTE": "you have a missing end quote", - "member": { - "NOT_FOUND": "the member could not be found" - }, - "channel": { - "NOT_FOUND": "the channel could not be found" - }, - "command": { - "NOT_FOUND": "the command could not be found" - }, - "group": { - "NOT_FOUND": "the command group could not be found" - }, - "role": { - "NOT_FOUND": "the role could not be found" - }, - "string": { - "NOT_STRING": "{{arg}} must be a string", - "MAX": "{{arg}} length cannot be more than {{max}}", - "MIN": "{{arg}} length cannot be less than {{min}}", - "ONE_OF": "{{arg}} must be one of the following" - }, - "list": { - "MAX": "{{arg}} length cannot contain more than {{max}} items", - "MIN": "{{arg}} length cannot contain less than {{min}} items", - "MAX_LENGTH": "{{arg}} cannot contain items longer than {{maxLength}}", - "MIN_LENGTH": "{{arg}} cannot contain items shorter than {{minLength}}", - "DUPES": "{{arg}} cannot contain duplicate values" - }, - "int": { - "NOT_INT": "{{arg}} is not an integer", - "MAX": "{{arg}} must be less than or equals to {{max}}", - "MIN": "{{arg}} must be more than or equals to {{min}}" - } - } -} diff --git a/resources/i18n/en/credits.json b/resources/i18n/en/credits.json deleted file mode 100644 index 6223e44..0000000 --- a/resources/i18n/en/credits.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "balance": "{{user}}'s account balance is {{balance}} credits.", - "topup": "{{amount}} credits have been added to your account.", - "cooldown": "your claimable credit refreshes in {{time}}.", - "dialog": [ - "**Credits Transfer**\n", - "{{author}}, you are transferring {{amount}} credits to {{user}}\n", - "__Current balance__: {{balance}}", - "__Balance after transfer__: {{afterAmount}}\n", - "To confirm, enter {{code}} to proceed or {{exit}} to quit the menu." - ], - "confirmation": [ - "you have transferred {{amount}} credits to {{user}}'s account.\n", - "__Updated balance__: {{afterAmount}}" - ], - "invalidCode": "you have entered an invalid code! Your credits haven't been transferred.", - "selfSenderError": "you can't send credits to yourself!", - "insufficientCredits": "you have insufficient ({{balance}}) credits!", - "botUserError": "you can't send credits to bots!" -} diff --git a/resources/i18n/en/help.json b/resources/i18n/en/help.json deleted file mode 100644 index fbf045d..0000000 --- a/resources/i18n/en/help.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "header_1": "To run a command, invoke it with {{prefix}} in {{server}}", - "header_1_alt": "or the default {{defaultPrefix}}", - "header_2": "To get more help on a command, call {{helpCommand}}", - "header_3": "For example: {{exampleCommand}}", - "definitions": { - "usage": "Usage", - "aliases": "Aliases", - "pms": "PMs" - }, - "footer": "For support, suggestions and feedback, visit {{link}}", - "footer_group": [ - "`[]` refers to __**optional**__ arguments", - "`<>` refers to __**required**__ arguments", - "Omit `<>` or `[]` when invoking a command" - ], - "noDesc": "No description", - "checkPMs": "check your PMs!" -} diff --git a/src/commands/admin/eval.js b/src/commands/admin/eval.js deleted file mode 100644 index 78d64ed..0000000 --- a/src/commands/admin/eval.js +++ /dev/null @@ -1,89 +0,0 @@ -const { Command } = require('../../core') - -class Eval extends Command { - constructor (...args) { - super(...args, { - name: 'eval', - description: 'Evaluates an expression', - options: { - adminOnly: true - }, - cooldown: 0 - }) - } - - createEmbed (success = true, isPromise = false, result) { - let embed = { - description: String(result ? result.content || result.message || result : 'null') - } - let title - let color - if (success) { - title = isPromise ? 'Promise resolved' : 'Success' - color = this.colours.green - } else { - if (success === null && isPromise) { - title = 'Promise resolving' - color = this.colours.blue - } else { - title = isPromise ? 'Promise rejected' : 'Error' - color = this.colours.red - } - } - embed.title = title - embed.color = color - return embed - } - - async handle (container, responder) { - const { rawArgs } = container - let resp - try { - resp = eval(rawArgs.join(' ')) - } catch (err) { - resp = err - } - - const success = !(resp instanceof Error) - const isPromise = typeof resp === 'function' && (resp.then ? resp.then : false) - - const message = await responder.embed( - this.createEmbed(isPromise ? null : success, isPromise, (resp && resp.message) ? resp.message : resp) - ).send() - - if (!isPromise) return - - resp - .then(result => message.edit({ content: '', embed: this.createEmbed(true, true, result) })) - .catch(err => message.edit({ content: '', embed: this.createEmbed(false, true, err) })) - } -} - -class FullEval extends Command { - constructor (...args) { - super(...args, { - name: 'fulleval', - description: 'Evaluates an expression across processes', - options: { - adminOnly: true - }, - cooldown: 0 - }) - } - - async handle (container, responder) { - const { msg } = container - const content = msg.content.split(' ').slice(1).join(' ') - this.bot.engine.ipc.awaitResponse('evaluate', { content }) - .then(data => responder.format('code:js').send(data.map(d => { - const r = d.result || null - return [ - `PROCESS ${d.id}:`, - (r && r.length > 200 ? r.substr(0, 200) + '...' : r) + '\n' - ].join('\n') - }).join('\n'))) - .catch(err => responder.format('code:js').send(err)) - } -} - -module.exports = [ Eval, FullEval ] diff --git a/src/commands/admin/kill.js b/src/commands/admin/kill.js deleted file mode 100644 index c636407..0000000 --- a/src/commands/admin/kill.js +++ /dev/null @@ -1,22 +0,0 @@ -const util = require('util') -const { Command } = require('../../core') - -class Kill extends Command { - constructor (...args) { - super(...args, { - name: 'kill', - description: 'Kills all processes', - options: { - adminOnly: true - } - }) - } - - handle (container, responder) { - return this.bot.engine.ipc.awaitResponse('kill') - .then(data => responder.format('code:js').send(data.map(d => util.inspect(d)).join('\n'))) - .catch(err => responder.format('code:js').send(err)) - } -} - -module.exports = Kill diff --git a/src/commands/admin/reload.js b/src/commands/admin/reload.js deleted file mode 100644 index b85a98d..0000000 --- a/src/commands/admin/reload.js +++ /dev/null @@ -1,31 +0,0 @@ -const util = require('util') -const { Command } = require('../../core') - -class Reload extends Command { - constructor (...args) { - super(...args, { - name: 'reload', - description: 'Reloads commands, middleware and modules', - options: { - adminOnly: true - }, - cooldown: 0, - usage: [ - { name: 'type', type: 'string', optional: true }, - { name: 'group', type: 'string', optional: true }, - { name: 'file', type: 'string', optional: true } - ] - }) - } - - async handle ({ args }, responder) { - try { - const data = await this.bot.engine.ipc.awaitResponse('reload', { type: args.type, group: args.group, file: args.file }) - return responder.format('code:js').send(data.map(d => util.inspect(d)).join('\n')) - } catch (err) { - return responder.format('code:js').send(err) - } - } -} - -module.exports = Reload diff --git a/src/commands/core/help.js b/src/commands/core/help.js deleted file mode 100644 index e0a91b8..0000000 --- a/src/commands/core/help.js +++ /dev/null @@ -1,90 +0,0 @@ -const { padEnd } = require('../../core/util') -const { Command } = require('../../core') - -class HelpMenu extends Command { - constructor (...args) { - super(...args, { - name: 'help', - description: 'Displays info on commands', - cooldown: 1, - usage: [ - { name: 'command', type: 'command', optional: true } - ] - }) - } - - async handle ({ msg, commander, settings, args }, responder) { - const prefix = settings.prefix - if (args.command) { - const command = args.command.cmd - const name = command.labels[0] - let desc = this.i18n.get(`descriptions.${name}`, settings.lang) || this.i18n.get(`${command.localeKey}.description`, settings.lang) - if (typeof desc !== 'string') desc = command.description || '{{noDesc}}' - let reply = [ - `**\`${prefix}${name}\`** __\`${desc}\`__\n`, - `**{{definitions.usage}}**: ${prefix}${command.labels[0]} ${Object.keys(command.resolver.usage).map(usage => { - usage = command.resolver.usage[usage] - return usage.optional ? `[${usage.displayName}]` : `<${usage.displayName}>` - }).join(' ')}` - ] - if (command.labels.length > 1) { - reply.push(`\n**{{definitions.aliases}}**: \`${command.labels.slice(1).join(' ')}\``) - } - reply.push('\n{{footer_group}}') - responder.send(reply.join('\n')) - return - } - - let maxPad = 10 - const commands = commander.unique().reduce((obj, c) => { - if (c.cmd.labels[0] !== c.label || c.cmd.options.hidden || c.cmd.options.adminOnly) return obj - const module = c.group - const name = c.cmd.labels[0] - - let desc = this.i18n.get(`descriptions.${name}`, settings.lang) || - this.i18n.get(`${c.cmd.localeKey}.description`, settings.lang) - if (typeof desc !== 'string') desc = '{{noDesc}}' - - if (name.length > maxPad) maxPad = name.length - if (!Array.isArray(obj[module])) obj[module] = [] - obj[module].push([name, desc]) - return obj - }, {}) - - let toSend = [] - for (const mod in commands) { - if (!commands[mod].length) continue - toSend.push([ - `# ${mod}:`, - commands[mod].map(c => ` ${padEnd(c[0], maxPad)} // ${c[1]}`).join('\n') - ].join('\n')) - if (toSend.length >= 10) { - await responder.send(['**```glsl'].concat(toSend, '```**'), { DM: true }) - toSend = [] - } - } - if (toSend.length) await responder.send(['**```glsl'].concat(toSend, '```**'), { DM: true }) - - return responder.send([ - `{{header_1}} ${prefix === process.env.CLIENT_PREFIX ? '' : '{{header_1_alt}}'}`, - '{{header_2}}', - '{{header_3}}', - '{{footer}}' - ], { - DM: true, - prefix: `\`${prefix}\``, - defaultPrefix: `\`${process.env.CLIENT_PREFIX}\``, - server: `**${msg.guild ? msg.guild.name : responder.t('{{pms}}')}**`, - helpCommand: `\`${prefix}help \``, - exampleCommand: `\`${prefix}help credits\``, - link: '****' - }) - .then(m => { - if (msg.guild) { - responder.format('emoji:inbox').reply('{{checkPMs}}') - } - }) - } -} - -module.exports = HelpMenu diff --git a/src/commands/core/ping.js b/src/commands/core/ping.js deleted file mode 100644 index 48ecac0..0000000 --- a/src/commands/core/ping.js +++ /dev/null @@ -1,21 +0,0 @@ -const logger = require('winston') -const { Command } = require('../../core') - -class Ping extends Command { - constructor (...args) { - super(...args, { - name: 'ping', - description: 'Pong!', - options: { hidden: true } - }) - } - - handle ({ msg }, responder) { - return responder.format('emoji:info').send('Pong!').then(m => { - m.edit(`${m.content} - Time taken: **${m.timestamp - msg.timestamp}ms**`) - .catch(logger.error) - }) - } -} - -module.exports = Ping diff --git a/src/core/Automaton.js b/src/core/Automaton.js deleted file mode 100644 index a50814a..0000000 --- a/src/core/Automaton.js +++ /dev/null @@ -1,75 +0,0 @@ -const { Client } = require('eris') -const path = require('path') -const logger = require('winston') - -const Engine = require('./system/Engine') - -class Automaton extends Client { - constructor (options) { - super(process.env.CLIENT_TOKEN, { - messageLimit: 0, - getAllUsers: true, - disableEveryone: true, - firstShardID: options.firstShardID, - lastShardID: options.lastShardID, - maxShards: options.maxShards, - disableEvents: { - TYPING_START: true, - MESSAGE_UPDATE: true, - MESSAGE_DELETE: true, - MESSAGE_DELETE_BULK: true - } - }) - - this.shardIDs = [] - for (let i = this.firstShardID; i <= this.lastShardID; i++) { - this.shardIDs.push(i) - } - - this.paths = { - commands: options.commands || path.join(__dirname, '../commands'), - middleware: options.middleware || path.join(__dirname, '../middleware'), - modules: options.middleware || path.join(__dirname, '../modules'), - ipc: options.middleware || path.join(__dirname, '../ipc'), - models: options.models || path.join(__dirname, '../models'), - resources: options.resources || path.join(process.cwd(), 'resources') - } - - this.dbOptions = { - host: process.env.DB_HOST, - port: process.env.DB_PORT, - db: process.env.DB_DBNAME, - authKey: process.env.DB_AUTHKEY - } - - this.init() - } - - init () { - let engine = this.engine = new Engine(this) - - engine.on('loaded:commands', count => logger.info(`Loaded ${count} commands`)) - engine.on('loaded:middleware', count => logger.info(`Loaded ${count} middleware`)) - engine.on('loaded:modules', count => logger.info(`Loaded ${count} modules`)) - - engine.on('register:ipc', command => logger.debug(`Registering IPC command '${command}'`)) - engine.on('register:db', id => logger.debug(`Registering DB model '${id}'`)) - - engine.run() - } - - run () { - this.emit('connecting') - this.connect() - } - - fetchCounts () { - return { - guilds: this.guilds.size, - channels: Object.keys(this.channelGuildMap).length, - users: this.users.size - } - } -} - -module.exports = Automaton diff --git a/src/core/base/Base.js b/src/core/base/Base.js deleted file mode 100644 index 44cff68..0000000 --- a/src/core/base/Base.js +++ /dev/null @@ -1,158 +0,0 @@ -const emoji = require('node-emoji') -const logger = require('winston') -const { Emojis } = require('../util') - -const colours = { - blue: '#117ea6', - green: '#1f8b4c', - red: '#be2626', - pink: '#E33C96', - gold: '#d5a500', - silver: '#b7b7b7', - bronze: '#a17419', - orange: '#c96941' -} - -class Base { - constructor (bot) { - if (this.constructor === Base) { - throw new Error('Must extend abstract Base') - } - - this.bot = bot - this.i18n = bot.engine.i18n - - this.colours = {} - for (const colour in colours) { - this.colours[colour] = this.hexToInt(colours[colour]) - } - } - - getColour (colour) { - return this.colours[colour] || this.colours.blue - } - - parseNumber (number) { - if (typeof number === 'number') number = number.toString() - return number.replace(/\B(?=(\d{3})+(?!\d))/g, ',') - } - - hasRoleHierarchy (guild, user, role) { - if (!guild) return false - - const member = guild.members.get(user.id) - if (!member) return false - for (let r of member.roles) { - r = guild.roles.get(r) - if (r.id === role.id) continue - if (r.position > role.position) return true - } - - return false - } - - hasPermissions (channel, user, ...perms) { - const member = channel.guild.members.get(user.id) - - if (!perms.every(p => member.permission.has(p))) return false - return perms.every(perm => ( - !channel.permissionOverwrites.find(p => (member.roles.includes(p.id) || p.id === user.id) && p.json[perm] === false) - )) - } - - t (content = '', lang = 'en', tags = {}) { - const file = this.name ? this.name.split(':')[0] : (this.labels ? this.labels[0] : 'common') - return this.i18n.parse(content, this.localeKey || file || null, lang, tags) - } - - async send (channel, content, options = {}) { - if (typeof channel === 'string') { - channel = this.bot.getChannel(channel) - } - if (!channel) return null - - let { file = null, lang, delay = 0, deleteDelay = 0, embed } = options - if (channel.guild) { - const guild = channel.guild - if (!this.hasPermissions(channel, this.bot.user, 'sendMessages')) { - logger.error(`Channel ${channel.name} (${channel.id}) in ${guild.name} (${guild.id}) denies message sending`) - return Promise.reject('403_SEND_MSG') - } - } - - if (delay) { - await Promise.delay(delay) - } - - lang = !lang && channel.guild ? (await this.bot.engine.db.data.Guild.fetch(channel.guild.id)).lang : 'en' - - if (Array.isArray(content)) content = content.join('\n') - content = this.t(content, lang, options) - content = content.replace(/:(\S+):/gi, (matched, name) => ( - this.i18n.locate(name, Emojis) || emoji.get(name) || matched - )) - content = content.match(/(.|[\r\n]){1,2000}/g) - - try { - if (!content || !content.length) { - let msg = await channel.createMessage({ embed, content: '' }, file) - if (deleteDelay) { - setTimeout(() => { - msg.delete().catch(err => logger.error(`Could not delete message ${msg.id} - ${err}`)) - }, deleteDelay) - } - return msg - } - let replies = await Promise.mapSeries(content, (c, idx) => { - return channel.createMessage(!idx ? { embed, content: c } : c, !idx ? file : null).then(msg => { - if (deleteDelay) setTimeout(() => msg.delete(), deleteDelay) - return msg - }) - }) - return replies[0] - } catch (err) { - logger.error(`Error sending message to ${channel.name} (${channel.id})`) - if (err.response) { - logger.error(JSON.parse(err.response).message) - return - } - throw err - } - } - - async edit (msg, content, options) { - let { lang, delay = 0 } = options - if (delay) { - await Promise.delay(delay) - } - - if (!lang && msg.channel.guild) { - lang = (await this.bot.engine.db.data.Guild.fetch(msg.channel.guild.id)).lang - } else { - lang = 'en' - } - - if (Array.isArray(content)) content = content.join('\n') - content = this.t(content, lang, options) - content = content.replace(/:(\S+):/gi, (matched, name) => { - return this.i18n.locate(name, Emojis) || emoji.get(name) || matched - }) - - return msg.edit(content) - } - - deleteMessages (...msgs) { - const id = this.bot.user.id - for (let msg of msgs.filter(m => m)) { - if (msg.author.id === id || msg.channel.permissionsOf(id).has('manageMessages')) { - msg.delete() - } - } - } - - hexToInt (colour) { - return parseInt(colour.replace('#', ''), 16) - } -} - -module.exports = Base diff --git a/src/core/base/Command.js b/src/core/base/Command.js deleted file mode 100644 index 17a84f1..0000000 --- a/src/core/base/Command.js +++ /dev/null @@ -1,148 +0,0 @@ -const logger = require('winston') -const moment = require('moment') - -const { Responder, Collection } = require('../util') -const Resolver = require('../managers/Resolver') -const Base = require('./Base') - -class Command extends Base { - constructor (bot, group, ...args) { - super(bot) - if (this.constructor === Command) { - throw new Error('Cannot instantiate abstract Command') - } - - this.group = group - this.resolver = new Resolver(bot) - this.responder = new Responder(this) - this.subcommands = new Collection() - - const options = args.reduce((p, c) => Object.assign(c, p), {}) - this._verify(options, ...args) - - this.timers = new Map() - } - - _verify ({ - name, - aliases = [], - cooldown = 5, - usage = [], - options = {}, - subcommands = {}, - subcommand - } = {}) { - this.labels = typeof name === 'string' - ? [name].concat(aliases) - : (Array.isArray(aliases) && aliases.length > 0 ? aliases : []) - - if (this.labels.length === 0) { - throw new Error(`${this.constructor.name} command is not named`) - } - this.cooldown = cooldown - this.options = options - if (this.options.modOnly) { - this.options.permissions = (this.options.permissions || []).concat('manageGuild') - } - - this.usage = usage - this.localeKey = options.localeKey - this.resolver.load(usage) - - for (const command in subcommands) { - const name = subcommands[command].name || command - for (const alias of [name].concat(subcommands[command].aliases || [])) { - this.subcommands.set(alias, { - usage: subcommands[command].usage || [], name - }) - } - } - this.subcommand = subcommand - } - - _execute (container) { - const responder = this.responder.create(container) - - let usage = this.usage - let process = 'handle' - - const subcmd = this.subcommand ? this.subcommand : container.rawArgs[0] - const cmd = this.subcommands.get(subcmd) - if (cmd) { - usage = cmd.usage - process = cmd.name - container.rawArgs = container.rawArgs.slice(this.subcommand ? 0 : 1) - container.trigger += ' ' + subcmd - } - - if (!this._execCheck(container, responder)) return - - this.resolver.resolve(container.msg, container.rawArgs, { - prefix: container.settings.prefix, - command: container.trigger - }, usage).then((args = {}) => { - container.args = args - this[process](container, responder).catch(err => { - logger.error(`Rejection from ${this.labels[0]}`) - logger.error(err) - }) - }).catch(err => { - return responder.error(err.message || err.err || err, err) - }) - } - - _execCheck ({ msg, isPrivate, admins, client }, responder) { - const isAdmin = admins.includes(msg.author.id) - const { adminOnly, guildOnly, permissions = [], botPerms = [] } = this.options - if (adminOnly === true && !isAdmin) return false - if (guildOnly === true && isPrivate) return false - - if (permissions.length && !this.hasPermissions(msg.channel, msg.author, ...permissions)) { - responder.error('{{%NO_PERMS}}', { - perms: permissions.map(p => `\`${p}\``).join(', ') - }) - return false - } - - if (botPerms.length && !this.hasPermissions(msg.channel, client.user, ...botPerms)) { - responder.error('{{%NO_PERMS_BOT}}', { - perms: botPerms.map(p => `\`${p}\``).join(', ') - }) - return false - } - - if (isAdmin) return true - const awaitID = msg.channel.id + msg.author.id - if (this.cooldown > 0) { - if (!this.timers.has(awaitID)) { - this.timers.set(awaitID, +moment()) - } else { - const diff = moment().diff(moment(this.timers.get(awaitID)), 'seconds') - if (diff < this.cooldown) { - responder.error('{{%COOLDOWN}}', { - delay: 0, - deleteDelay: 5000, - time: `**${this.cooldown - diff}**` - }) - return false - } else { - this.timers.delete(awaitID) - this.timers.set(awaitID, +moment()) - } - } - } - return true - } - - async handle () { return true } - - logError (err) { - logger.error(`Error running ${this.labels[0]} command: ${err}`) - } - - get permissionNode () { - return `${this.group}.${this.labels[0]}` - } -} - -module.exports = Command diff --git a/src/core/base/Module.js b/src/core/base/Module.js deleted file mode 100644 index d918874..0000000 --- a/src/core/base/Module.js +++ /dev/null @@ -1,37 +0,0 @@ -const Base = require('./Base') - -class Module extends Base { - constructor (bot, options) { - super(bot) - if (this.constructor === Module) { - throw new Error('Must extend abstract Module') - } - - this._verify(options) - } - - init () {} - - unload () {} - - _verify ({ name, events = {}, localeKey } = {}) { - if (typeof name === 'undefined') throw new Error(`${this.constructor.name} is not named`) - if (typeof events !== 'object') throw new Error('Module event must be an object') - - for (let event in events) { - if (typeof event !== 'string') { - throw new TypeError(`Module ${name} has an invalid event`) - } - - if (typeof this[events[event]] !== 'function') { - throw new TypeError(`Module ${name} has an invalid handler`) - } - } - - this.name = name - this.events = events - this.localeKey = localeKey - } -} - -module.exports = Module diff --git a/src/core/base/index.js b/src/core/base/index.js deleted file mode 100644 index fcb42e9..0000000 --- a/src/core/base/index.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('require-all')(__dirname) diff --git a/src/core/index.js b/src/core/index.js deleted file mode 100644 index 5ef74dc..0000000 --- a/src/core/index.js +++ /dev/null @@ -1,7 +0,0 @@ -const extensions = require('./base') -const util = require('./util') - -module.exports = Object.assign({ - ShardManager: require('./managers/ShardManager'), - Automaton: require('./Automaton') -}, extensions, util) diff --git a/src/core/managers/ModelManager.js b/src/core/managers/ModelManager.js deleted file mode 100644 index 9613123..0000000 --- a/src/core/managers/ModelManager.js +++ /dev/null @@ -1,69 +0,0 @@ -const Thinky = require('thinky') -const requireAll = require('require-all') -const EventEmitter = require('eventemitter3') - -const { LocalCache } = require('../util') - -class ModelManager extends EventEmitter { - constructor (options) { - super() - - const thinky = this.thinky = new Thinky(options) - thinky.dbReady().then(() => this.emit('ready')) - - this.models = { r: thinky.r } - this.data = {} - } - - loadFolder (folder) { - const models = requireAll(folder) - for (let model in models) { - this.load(models[model].call(this)) - } - this.relate(models) - } - - load (model) { - const { tableName, schema, options = {}, cache = false, expiry } = model - if (typeof tableName !== 'string') { - this.emit('error', TypeError('Model found with invalid name')) - return - } - if (typeof schema !== 'object') { - this.emit('error', TypeError(`Model ${tableName} has an invalid schema`)) - return - } - if (typeof options !== 'object') { - this.emit('error', TypeError(`Model ${tableName} has invalid options`)) - return - } - if (typeof this.models[tableName] === 'object') { - this.emit('error', Error(`Duplicate model found: ${tableName}`)) - return - } - - const modelID = tableName - const Model = this.thinky.createModel(modelID, schema, options) - - this.models[modelID] = Model - if (cache) { - this.data[modelID] = new LocalCache(Model, model, expiry) - } - this.emit('loaded', modelID) - } - - relate (models) { - for (let modelID in models) { - let { relations } = models[modelID] - if (typeof relations === 'undefined') continue - for (let relation in relations) { - const Model = this.models[modelID] - const Sub = this.models[relations[relation][0]] - if (!Model || !Sub) continue - Model[relation](...relations[relation].slice(1)) - } - } - } -} - -module.exports = ModelManager diff --git a/src/core/managers/Resolver.js b/src/core/managers/Resolver.js deleted file mode 100644 index b0d348c..0000000 --- a/src/core/managers/Resolver.js +++ /dev/null @@ -1,127 +0,0 @@ -const path = require('path') -const requireAll = require('require-all') - -class Resolver { - constructor (bot) { - this.bot = bot - this._resolvers = {} - this.loadResolvers() - } - - loadResolvers () { - const resolvers = requireAll(path.join(__dirname, 'resolvers')) - for (let resolver in resolvers) { - resolver = resolvers[resolver] - if (!resolver.resolve || !resolver.type) continue - this._resolvers[resolver.type] = resolver - } - } - - load (data) { - this.usage = this.verify(data) - } - - verify (usage) { - return (Array.isArray(usage) ? usage : [usage]).map(args => { - if (!args.name) throw new Error('Argument specified in usage has no name') - if (!args.types) args.types = [ args.type || 'string' ] - if (!args.displayName) args.displayName = args.name - return args - }) - } - - resolve (message, rawArgs, data = {}, usage = this.usage) { - usage = this.verify(usage) - if (!usage.length) return Promise.resolve() - - const argsCount = rawArgs.length - const requiredArgs = usage.filter(arg => !arg.optional).length - const optionalArgs = argsCount - requiredArgs - - if (argsCount < requiredArgs) { - let msg = '{{%resolver.INSUFFICIENT_ARGS}}' - if (data.prefix && data.command) { - msg += `\n\n**{{%resolver.CORRECT_USAGE}}**: \`${data.prefix}${data.command} ` + (usage.length - ? usage.map(arg => arg.optional ? `[${arg.displayName}]` : `<${arg.displayName}>`).join(' ') - : '') + '`' - } - return Promise.reject({ - message: msg, - requiredArgs: `**${requiredArgs}**`, - argsCount: `**${argsCount}**.` - }) - } - - let args = {} - let idx = 0 - let optArgs = 0 - let resolves = [] - let skip = false - for (const arg of usage) { - let rawArg - if (arg.last) { - rawArg = rawArgs.slice(idx).join(' ') - skip = true - } else { - if (arg.optional) { - if (optionalArgs > optArgs) { - optArgs++ - } else { - if (arg.default) args[arg.name] = arg.default - continue - } - } - rawArg = rawArgs[idx] - if (typeof rawArg !== 'undefined') { - if (rawArg.startsWith('"')) { - const endQuote = rawArgs.findIndex((str, i) => str.endsWith('"') && i >= idx) - if (endQuote > -1) { - rawArg = rawArgs.slice(idx, endQuote + 1).join(' ').replace(/"/g, '') - idx = endQuote - } else { - return Promise.reject('{{%resolver.NO_END_QUOTE}}') - } - } - } - idx++ - } - resolves.push( - Promise.all(arg.types.map(type => { - const resolver = this._resolvers[type] - if (typeof resolver === 'undefined') { - return Promise.reject({ err: 'Invalid resolver type' }) - } - return resolver.resolve(rawArg, arg, message, this.bot) - .catch(err => Object.assign(arg, { - arg: `**\`${arg.name || 'argument'}\`**`, - err: err.message ? err.message : `{{%resolver.${err}}}` + - (data.prefix && data.command - ? `\n\n**{{%resolver.CORRECT_USAGE}}**: \`${data.prefix}${data.command} ` + - (usage.length ? usage.map(arg => - skip ? arg.displayName - : (arg.optional ? `[${arg.displayName}]` : `<${arg.displayName}>`) - ).join(' ') : '') + '`' - : '') - })) - })) - .then(results => { - const resolved = results.filter(v => !v.err) - - if (resolved.length) { - const res = resolved.length === 1 - ? resolved[0] - : resolved.reduce((p, c) => p.concat(c), []) - args[arg.name] = res - return res - } - - return Promise.reject(results[0]) - }) - ) - if (skip) break - } - return Promise.all(resolves).then(() => args) - } -} - -module.exports = Resolver diff --git a/src/core/managers/Transmitter.js b/src/core/managers/Transmitter.js deleted file mode 100644 index 68579ff..0000000 --- a/src/core/managers/Transmitter.js +++ /dev/null @@ -1,73 +0,0 @@ -const EventEmitter = require('eventemitter3') -const logger = require('winston') -const crypto = require('crypto') - -class Transmitter extends EventEmitter { - constructor (shardID = 0, bot) { - super() - - this.ids = Array.isArray(shardID) ? shardID : [shardID] - this.pid = process.pid - this.commands = new Map() - this._bot = bot - - process.on('message', this.onMessage.bind(this)) - } - - send (event, data) { - process.send({ - op: event, - d: data - }) - } - - onMessage (message) { - if (!message.op) { - return logger.warn('Received IPC message with no op') - } - - if (['resp', 'broadcast'].includes(message.op)) return - - if (this[message.op]) { - return this[message.op](message) - } - - const command = this.commands.get(message.op) - if (command) { - return command(message, this._bot) - } - - this.emit(message.op, message.d) - } - - async awaitResponse (op, d) { - const code = crypto.randomBytes(64).toString('hex') - return new Promise((resolve, reject) => { - const awaitListener = (msg) => { - if (!['resp', 'error'].includes(msg.op)) return - process.removeListener('message', awaitListener) - if (msg.op === 'resp' && msg.code === code) return resolve(msg.d) - if (msg.op === 'error') return reject(msg.d) - } - - const payload = { op, code } - if (d) payload.d = d - - process.on('message', awaitListener) - process.send(payload) - - setTimeout(() => { - process.removeListener('message', awaitListener) - return reject('IPC timed out after 2000ms') - }, 2000) - }) - } - - register (command) { - if (!command || !command.name) return logger.error('Invalid command') - this.commands.set(command.name, command) - this.emit('registered', command.name) - } -} - -module.exports = Transmitter diff --git a/src/core/managers/resolvers/channel.js b/src/core/managers/resolvers/channel.js deleted file mode 100644 index 94fe84b..0000000 --- a/src/core/managers/resolvers/channel.js +++ /dev/null @@ -1,25 +0,0 @@ -module.exports = { - type: 'channel', - resolve: (content, { text = true, voice = true } = {}, msg) => { - const guild = msg.guild - content = String(content).toLowerCase() - let channel = content.match(/^<#?(\d{17,18})>$/) - if (!channel) { - let channels = guild.channels.filter(c => { - if (text && c.type !== 0) return - if (voice && c.type !== 2) return - const name = c.name.toLowerCase() - return name === content || name.includes(content) - }) - if (channels.length) { - return Promise.resolve(channels) - } else { - return Promise.reject('channel.NOT_FOUND') - } - } else { - let chan = guild.channels.get(channel[1]) - if (!chan) return Promise.reject('channel.NOT_FOUND') - return Promise.resolve([chan]) - } - } -} diff --git a/src/core/managers/resolvers/command.js b/src/core/managers/resolvers/command.js deleted file mode 100644 index 1ba626c..0000000 --- a/src/core/managers/resolvers/command.js +++ /dev/null @@ -1,10 +0,0 @@ -module.exports = { - type: 'command', - resolve: (content, args, msg, { engine }) => { - const command = engine.commands.get(content.toLowerCase()) - return !command || - ((command.options || {}).adminOnly && !process.env.ADMIN_IDS.split(', ').includes(msg.author.id)) - ? Promise.reject('command.NOT_FOUND') - : Promise.resolve(command) - } -} diff --git a/src/core/managers/resolvers/commandGroup.js b/src/core/managers/resolvers/commandGroup.js deleted file mode 100644 index d411662..0000000 --- a/src/core/managers/resolvers/commandGroup.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - type: 'commandGroup', - resolve: (content, args, msg, { engine }) => { - const group = engine.commands.find(c => c.group === content.toLowerCase()) - return group ? Promise.resolve(group) : Promise.reject('group.NOT_FOUND') - } -} diff --git a/src/core/managers/resolvers/integer.js b/src/core/managers/resolvers/integer.js deleted file mode 100644 index 170d0d5..0000000 --- a/src/core/managers/resolvers/integer.js +++ /dev/null @@ -1,16 +0,0 @@ -module.exports = { - type: 'int', - resolve: (content, { min = 0, max = Infinity }) => { - const num = parseInt(content, 10) - if (isNaN(num)) { - return Promise.reject('int.NOT_INT') - } - if (num > max) { - return Promise.reject('int.MAX') - } - if (num < min) { - return Promise.reject('int.MIN') - } - return Promise.resolve(num) - } -} diff --git a/src/core/managers/resolvers/list.js b/src/core/managers/resolvers/list.js deleted file mode 100644 index eb50390..0000000 --- a/src/core/managers/resolvers/list.js +++ /dev/null @@ -1,26 +0,0 @@ -module.exports = { - type: 'list', - resolve: (content, { separator = ', ', maxLength = Infinity, minLength = 0, max = Infinity, min = 0, unique = false }) => { - const list = content.split(separator) - const num = list.length - if (num > max) { - return Promise.reject('list.MAX') - } - if (num < min) { - return Promise.reject('list.MIN') - } - - const itemLength = list.map(item => item.length) - if (Math.max(...itemLength) > maxLength) { - return Promise.reject('list.MAX_LENGTH') - } - if (Math.min(...itemLength) < minLength) { - return Promise.reject('list.MAX_LENGTH') - } - - if (unique && new Set(list).size < list.length) { - return Promise.reject('list.DUPES') - } - return Promise.resolve(list) - } -} diff --git a/src/core/managers/resolvers/member.js b/src/core/managers/resolvers/member.js deleted file mode 100644 index a04c198..0000000 --- a/src/core/managers/resolvers/member.js +++ /dev/null @@ -1,30 +0,0 @@ -module.exports = { - type: 'member', - resolve: (content, { bot = false }, msg) => { - const guild = msg.guild - content = String(content).toLowerCase() - let user = content.match(/^<@!?(\d{17,18})>$/) || content.match(/^(\d{17,18})$/) - if (!user) { - let members = guild.members.filter(m => { - if (!bot && m.user.bot) return - const name = m.user.username.toLowerCase() - const nick = m.nick ? m.nick.toLowerCase() : name - const discrim = m.user.discriminator - return name === content || nick === content || - `${name}#${discrim}` === content || - `${nick}#${discrim}` === content || - name.includes(content) || - nick.includes(content) - }) - if (members.length) { - return Promise.resolve(members) - } else { - return Promise.reject('member.NOT_FOUND') - } - } else { - let member = guild.members.get(user[1]) - if (!member) return Promise.reject('member.NOT_FOUND') - return Promise.resolve([member]) - } - } -} diff --git a/src/core/managers/resolvers/role.js b/src/core/managers/resolvers/role.js deleted file mode 100644 index e14c368..0000000 --- a/src/core/managers/resolvers/role.js +++ /dev/null @@ -1,23 +0,0 @@ -module.exports = { - type: 'role', - resolve: (content, arg, msg) => { - const guild = msg.guild - content = String(content).toLowerCase() - let role = content.match(/^<@&(\d{17,18})>$/) - if (!role) { - let roles = guild.roles.filter(r => { - const name = r.name.toLowerCase() - return name === content || name.includes(content) - }) - if (roles.length) { - return Promise.resolve(roles) - } else { - return Promise.reject('role.NOT_FOUND') - } - } else { - let r = guild.roles.get(role[1]) - if (!r) return Promise.reject('role.NOT_FOUND') - return Promise.resolve([r]) - } - } -} diff --git a/src/core/managers/resolvers/string.js b/src/core/managers/resolvers/string.js deleted file mode 100644 index b0d5459..0000000 --- a/src/core/managers/resolvers/string.js +++ /dev/null @@ -1,32 +0,0 @@ -module.exports = { - type: 'string', - resolve: (content, { choices = [], categories = [], max = Infinity, min = 0, optional = false }) => { - if (typeof content === 'undefined') { - if (!optional) { - return Promise.reject('string.NOT_STRING') - } - return Promise.resolve(content) - } - const num = content.length - if (num > max) { - return Promise.reject('string.MAX') - } - if (num < min) { - return Promise.reject('string.MIN') - } - if (choices.length && !choices.includes(content)) { - return Promise.reject({ - message: `{{%resolver.string.ONE_OF}}: ${choices.map(c => '`' + c + '`').join(', ')}` - }) - } - if (categories.length) { - for (const [ cat, choice ] of Object.entries(categories)) { - if (choice.includes(content)) return Promise.resolve(cat) - } - return Promise.reject({ - message: `{{%resolver.string.ONE_OF}}: ${Object.keys(categories).map(c => '`' + c + '`').join(', ')}` - }) - } - return Promise.resolve(content) - } -} diff --git a/src/core/system/Bridge.js b/src/core/system/Bridge.js deleted file mode 100644 index ae5ec7b..0000000 --- a/src/core/system/Bridge.js +++ /dev/null @@ -1,92 +0,0 @@ -class Bridge { - constructor (manager) { - this.manager = manager - this.tasks = [] - this.collectors = [] - } - - push (middleware) { - if (!middleware.hasOwnProperty('process')) { - throw new Error('Middleware must contain the process method') - } - if (typeof middleware.process !== 'function') { - throw new Error('Middleware process must be a function') - } - this.tasks.push(middleware.process) - } - - collect (options) { - const { tries = 10, time = 60, matches = 10, channel, author, filter } = options - let collector = { - collected: [], - _tries: 0, - _matches: 0, - _listening: false, - _ended: false - } - if (time) { - setTimeout(() => { - collector._ended = { reason: 'timeout', arg: time, collected: collector.collected } - }, time * 1000) - } - collector.stop = () => { - collector._listening = false - this.collectors.splice(this.collectors.indexOf(collector), 1) - } - collector.next = () => { - return new Promise((resolve, reject) => { - collector._resolve = resolve - if (collector._ended) { - collector.stop() - reject(collector._ended) - } - collector._listening = true - }) - } - collector.passMessage = msg => { - if (!collector._listening) return false - if (author && author !== msg.author.id) return false - if (channel && channel !== msg.channel.id) return false - if (typeof filter === 'function' && !filter(msg)) return false - - collector.collected.push(msg) - if (collector.collected.size >= (matches || Infinity)) { - collector._ended = { reason: 'maxMatches', arg: matches } - } else if (tries && collector.collected.size === (tries || Infinity)) { - collector._ended = { reason: 'max', arg: tries } - } - collector._resolve(msg) - return true - } - this.collectors.push(collector) - return collector - } - - destroy () { - this.tasks = [] - } - - handle (container, idx = 0) { - const { msg } = container - return new Promise((resolve, reject) => { - for (let collector of this.collectors) { - let res = collector.passMessage(msg) - if (res) return - } - if (idx === this.tasks.length) { - try { - this.manager.execute(container.trigger, container) - } catch (err) { - return reject(err) - } - return resolve(container) - } - this.tasks[idx++](container).then(c => { - if (!c) return reject() - return this.handle(c, idx).then(resolve).catch(reject) - }).catch(reject) - }) - } -} - -module.exports = Bridge diff --git a/src/core/system/Commander.js b/src/core/system/Commander.js deleted file mode 100644 index a280d37..0000000 --- a/src/core/system/Commander.js +++ /dev/null @@ -1,47 +0,0 @@ -const logger = require('winston') -const { Collection } = require('../util') - -class Commander extends Collection { - constructor (bot) { - super() - this.bot = bot - } - - attach (group, Command) { - const command = new Command(this.bot, group) - for (let label of command.labels) { - if (this.has(label)) throw new Error(`Duplicate command: ${label}`) - this.set(label.toLowerCase(), { cmd: command, group, label }) - } - } - - eject (group) { - this.filter(e => e.group === group || typeof group === 'undefined') - .forEach(e => this.delete(e.label)) - } - - execute (label, ...args) { - const e = this.get(label) - if (e) { - try { - e.cmd._execute(...args) - } catch (err) { - logger.error(`Error running command ${label}`) - logger.error(err) - } - } - } - - executeGroup (group, ...args) { - this.filter(e => e.group === group) - .forEach(e => { - try { - e.cmd._execute(...args) - } catch (err) { - logger.error(`Error running command group ${group}: ${err}`) - } - }) - } -} - -module.exports = Commander diff --git a/src/core/system/Engine.js b/src/core/system/Engine.js deleted file mode 100644 index c9dce21..0000000 --- a/src/core/system/Engine.js +++ /dev/null @@ -1,158 +0,0 @@ -const path = require('path') -const logger = require('winston') -const EventEmitter = require('eventemitter3') -const requireAll = require('require-all') - -const Bridge = require('./Bridge') -const Commander = require('./Commander') -const Router = require('./Router') - -const IPC = require('../managers/Transmitter') -const ModelManager = require('../managers/ModelManager') -const { Parser, Cache, readdirRecursive } = require('../util') - -class Engine extends EventEmitter { - constructor (bot) { - super() - - this.bot = bot - this.paths = bot.paths - - let ipc = this.ipc = new IPC(bot.shardIDs, bot) - let db = this.db = new ModelManager(bot.dbOptions) - let cache = this.cache = new Cache() - - this.commands = new Commander(bot) - this.modules = new Router(bot) - this.bridge = new Bridge(this.commands) - this.i18n = new Parser(path.join(bot.paths.resources, 'i18n')) - - ipc.on('registered', command => this.emit('register:ipc', command)) - db.on('loaded', id => this.emit('register:db', id)) - cache.on('error', err => logger.error(err)) - } - - run () { - this.loadAll() - - const admins = process.env.ADMIN_IDS.split(', ') - this.bot.on('messageCreate', msg => { - if (msg.author.id === this.bot.user.id || msg.author.bot) return - this.bridge.handle({ - msg, - admins, - commander: this.commands, - modules: this.modules, - client: this.bot, - cache: this.cache, - db: this.db.models, - data: this.db.data - }).catch(err => { - if (err) logger.error(`Failed to run command: ${err.stack}`) - }) - }) - - this.emit('ready') - } - - loadAll () { - this.loadModels() - this.loadCommands() - this.loadMiddleware() - this.loadModules() - this.loadIpc() - } - - loadModels () { - this.db.loadFolder(this.paths.models) - this.emit('loaded:db') - } - - loadCommands (mod, cmd) { - let count = 0 - this.commands.eject(mod) - - const commands = requireAll(this.paths.commands) - for (let group in commands) { - if (typeof mod === 'string' && group !== mod) continue - for (let command in commands[group]) { - if (typeof cmd === 'string' && cmd !== command) continue - let cmds = commands[group][command] - cmds = Array.isArray(cmds) ? cmds : [cmds] - - cmds.forEach(c => { - this.commands.attach(group, c) - count++ - }) - } - } - this.emit('loaded:commands', count) - } - - loadMiddleware (group = '.', file) { - this.bridge.destroy() - - let count = 0 - readdirRecursive(this.paths.middleware, group).then(mw => { - if (typeof file === 'string') mw = mw.filter(m => path.basename(m).startsWith(file)) - mw = mw.map(mw => require(mw)) - mw = Object.keys(mw).sort((a, b) => mw[a].priority - mw[b].priority).map(m => mw[m]) - mw.forEach(m => { - this.bridge.push(m) - count++ - }) - this.emit('loaded:middleware', count) - }) - } - - loadModules (group = '.', file) { - this.modules.destroy() - - let count = 0 - readdirRecursive(this.paths.modules, group).then(mod => { - if (typeof file === 'string') mod = mod.filter(m => path.basename(m).startsWith(file)) - mod = mod.map(m => require(m)) - for (let module in mod) { - this.modules.attach(mod, mod[module]) - count++ - } - this.modules.initAll() - this.emit('loaded:modules', count) - }) - } - - loadIpc (group = '.', file) { - let count = 0 - readdirRecursive(this.paths.ipc, group).then(prcs => { - if (typeof file === 'string') prcs = prcs.filter(m => path.basename(m).startsWith(file)) - prcs = prcs.map(i => require(i)) - for (let proc in prcs) { - this.ipc.register(prcs[proc]) - count++ - } - this.emit('loaded:ipc', count) - }) - } - - async reload (type = 'commands', cat = '.', file = '.') { - this.i18n.reload() - - const dir = this.paths[type] - if (!dir) { - const err = new TypeError(`"${type}" not found`) - this.emit('error', err) - throw err - } - - let count = 0 - Object.keys(require.cache).forEach(filepath => { - if (!filepath.startsWith(path.join(dir, cat, file))) return - delete require.cache[require.resolve(filepath)] - count++ - }) - this.emit(`reload:${type}`, count) - return count - } -} - -module.exports = Engine diff --git a/src/core/system/Router.js b/src/core/system/Router.js deleted file mode 100644 index 2a778af..0000000 --- a/src/core/system/Router.js +++ /dev/null @@ -1,67 +0,0 @@ -const logger = require('winston') -const { Collection } = require('../util') - -class Router extends Collection { - constructor (bot) { - super() - this.bot = bot - this.events = {} - } - - attach (group, Module) { - const module = new Module(this.bot) - this.set(module.name, module) - for (const event in module.events) { - if (typeof this.events[event] === 'undefined') { - this.register(event) - } - - const listener = module.events[event] - if (typeof module[listener] !== 'function') { - throw new TypeError(`${listener} is an invalid handler`) - } - - let events = this.events[event] || {} - events[module.name] = listener - this.events[event] = events - } - } - - register (event) { - this.bot.on(event, (...args) => { - const events = this.events[event] || {} - for (const name in events) { - const module = this.get(name) - if (!module) continue - try { - module[events[name]](...args) - } catch (err) { - logger.error(`Error executing ${event} in ${name}`) - logger.error(err) - } - } - }) - } - - initAll () { - this.forEach(module => { - if (typeof module.init === 'function') { - module.init() - } - }) - } - - destroy () { - for (const event in this.events) { - this.events[event] = {} - } - this.forEach(module => { - if (typeof module.unload === 'function') { - module.unload() - } - }) - this.clear() - } -} - -module.exports = Router diff --git a/src/core/util/Cache.js b/src/core/util/Cache.js deleted file mode 100644 index 995363c..0000000 --- a/src/core/util/Cache.js +++ /dev/null @@ -1,66 +0,0 @@ -const winston = require('winston') -const redis = require('redis') -const EventEmitter = require('eventemitter3') - -const bluebird = require('bluebird') -bluebird.promisifyAll(redis.RedisClient.prototype) -bluebird.promisifyAll(redis.Multi.prototype) - -class Cache extends EventEmitter { - constructor (opts) { - super() - this.init(opts) - } - - init (opts = {}) { - let options = { - host: opts.host || process.env.REDIS_HOST, - port: opts.port || process.env.REDIS_PORT || 6379, - db: opts.db || 0, - retryStrategy: function (opt) { - if (opt.error.code === 'ECONNREFUSED') { - return new Error('The server refused the connection') - } - if (opt.total_retry_time > 1000 * 60 * 60) { - return new Error('Retry time exhausted') - } - if (opt.times_connected > 10) { - return undefined - } - return Math.max(opts.attempt * 100, 3000) - } - } - - let pass = opts.password || process.env.REDIS_PASS - if (pass) options.password = pass - - let client = this.client = redis.createClient(options) - - client.on('ready', () => this.emit('ready')) - client.on('error', err => winston.error(`Cache met with an error: ${err}`)) - client.on('end', () => this.emit('end')) - } - - store (key, value, time) { - return this.client.setexAsync(key, time, value) - } - - unwrapValues (key, hash) { - let multi = this.client.multi() - for (let field in hash) { - if (!hash.hasOwnProperty(field)) continue - multi.hset(key, field, hash[field]) - } - return multi.execAsync() - } - - unwrapScores (key, hash) { - let multi = this.client.multi() - for (let field in hash) { - if (typeof hash[field] !== 'number') continue - multi.zadd(key, hash[field], field) - } - return multi.execAsync() - } -} -module.exports = Cache diff --git a/src/core/util/Collection.js b/src/core/util/Collection.js deleted file mode 100644 index f03fd90..0000000 --- a/src/core/util/Collection.js +++ /dev/null @@ -1,56 +0,0 @@ -class Collection extends Map { - load (obj, overwrite = true) { - for (let k of Object.keys(obj)) { - if (this.has(k) && !overwrite) continue - this.set(k, obj[k]) - } - return this - } - - toArray () { - return [...this.values()] - } - - forEach (...args) { - return this.toArray().forEach(...args) - } - - filter (...args) { - return this.toArray().filter(...args) - } - - find (...args) { - return this.toArray().find(...args) - } - - map (...args) { - return this.toArray().map(...args) - } - - reduce (...args) { - return this.toArray().reduce(...args) - } - - pluck (key) { - return this.toArray().reduce((i, o) => { - if (!o[key]) return i - i.push(o[key]) - return i - }, []) - } - - groupBy (key) { - return this.toArray().reduce((i, o) => { - let val = o[key] - i[val] = i[val] || [] - i[val].push(o) - return i - }, {}) - } - - unique () { - return [...new Set(this.toArray())] - } -} - -module.exports = Collection diff --git a/src/core/util/Emojis.js b/src/core/util/Emojis.js deleted file mode 100644 index 2bbc758..0000000 --- a/src/core/util/Emojis.js +++ /dev/null @@ -1,21 +0,0 @@ -module.exports = { - // Common - success: '✅', - error: '❎', - info: 'ℹ', - search: '🔍', - inbox: '📥', - - // Currency - credits: '💳', - - // Music - headphones: '🎧', - skip: '⏩', - play: '▶', - stop: '⏹', - - // Companions - atk: '⚔', - def: '🛡' -} diff --git a/src/core/util/LocalCache.js b/src/core/util/LocalCache.js deleted file mode 100644 index ad3e00b..0000000 --- a/src/core/util/LocalCache.js +++ /dev/null @@ -1,114 +0,0 @@ -const logger = require('winston') -const moment = require('moment') -const Collection = require('./Collection') - -class LocalCache extends Collection { - constructor (model, ttl = 3600) { - super() - - this.ttl = ttl * 1000 - this.timers = new Collection() - - if (model) { - model.changes().then(feed => { - feed.each((err, doc) => { - if (err) return logger.error(err) - if (doc.isSaved() === false) { - this.delete(doc.id) - } else if (this.has(doc.id)) { - this.store(doc.id, doc) - } - }) - }).error(logger.error) - this.model = model - } - } - - async fetch (key, pure = false) { - let value = this.get(key) - if (this.model && (typeof value === 'undefined' && this.model) || pure) { - try { - value = await this.model.get(key).run() - } catch (err) { - if (err.name === 'DocumentNotFoundError') { - const Model = this.model - value = new Model({ id: key }) - await value.save() - } else { - logger.error(`Could not fetch ${key} from ${this.model.getTableName()}`) - logger.error(err) - } - } - this.store(key, value) - } - return value - } - - async fetchJoin (key, options) { - let value = await this.fetch(key) - for (let type in options) { - if (this.model && (options[type] === true && typeof value[type] === 'undefined')) { - try { - value = await this.model.get(key).getJoin(options).run() - } catch (err) { - if (err.name === 'DocumentNotFoundError') { - const Model = this.model - value = new Model({ id: key }) - await value.save() - } else { - logger.error(`Could not fetch joined ${key} from ${this.model.getTableName()}`) - logger.error(err) - } - } - this.store(key, value) - } - } - return value - } - - async update (key, newValue) { - let oldValue = await this.fetch(key) - this.store(key, newValue) - return oldValue - } - - clearAll () { - this.clear() - this.timers.clear() - } - - clear (key) { - this.delete(key) - this.clearTimer(key) - } - - store (key, value) { - this.set(key, value) - this.refresh(key) - } - - ttl (key) { - return this.timers.get(key).expiry - +moment() - } - - clearTimer (key) { - const timer = this.timers.get(key) - if (!timer) return - clearTimeout(timer.timer) - } - - refresh (key) { - this.clearTimer(key) - const timer = setTimeout(() => this.clear(key), this.ttl) - this.timers.set(key, { timer, expiry: +moment() + this.ttl }) - } - - persist (key) { - this.clearTimer(key) - let ttl = this.ttl(key) - this.timers.delete(key) - return ttl - } -} - -module.exports = LocalCache diff --git a/src/core/util/Locales.js b/src/core/util/Locales.js deleted file mode 100644 index 9d74a58..0000000 --- a/src/core/util/Locales.js +++ /dev/null @@ -1,60 +0,0 @@ -const requireAll = require('require-all') -const Collection = require('./Collection') - -class Locales { - constructor (folderpath) { - if (typeof folderpath !== 'string') throw new TypeError('Invalid locale filepath') - this._folder = folderpath - - this.reload() - } - - reload () { - Object.keys(require.cache).forEach(filepath => { - if (!filepath.startsWith(this._folder)) return - delete require.cache[require.resolve(filepath)] - }) - const data = requireAll(this._folder) - this.strings = new Collection().load(data) - } - - locate (fullkey, obj) { - let keys = fullkey.split('.') - let val = obj[keys.shift()] - if (!val) return null - for (let key of keys) { - if (!val[key]) return val - val = val[key] - if (Array.isArray(val)) return val.join('\n') - } - return val || null - } - - get (key = 'common', locale = 'en') { - if (!this.strings.has(locale)) locale = 'en' - return this.locate(key, this.strings.get(locale)) - } - - shift (string, options) { - if (!string) return string - return string.split(' ').map(str => ( - str.replace(/\{\{(.+)\}\}/gi, (matched, key) => ( - this.locate(key, options) || matched - )) - )).join(' ') - } - - parse (string, group = 'common', locale, options = {}) { - if (!string) return string - return string.split(' ').map(str => ( - str.replace(/\{\{(.+)\}\}/gi, (matched, key) => { - const g = key.startsWith('%') ? 'common.' : group + '.' - key = key.startsWith('%') ? key.substr(1) : key - let val = this.get(`${g}${key}`, locale) - return typeof val === 'string' ? this.shift(val, options) : matched - }) - )).join(' ') - } -} - -module.exports = Locales diff --git a/src/core/util/MessageCollector.js b/src/core/util/MessageCollector.js deleted file mode 100644 index 32c46d3..0000000 --- a/src/core/util/MessageCollector.js +++ /dev/null @@ -1,71 +0,0 @@ -const EventEmitter = require('eventemitter3') -const Collection = require('./Collection') - -class MessageCollector extends EventEmitter { - constructor (channel, filter, options = {}) { - super() - this.channel = channel - this.filter = filter - this.options = options - this.ended = false - this.collected = new Collection() - - this.listener = message => this.verify(message) - if (options.time) setTimeout(() => this.stop({ reason: 'timeout', arg: options.time }), options.time * 1000) - } - - passMessage (message) { - if (this.ended) return false - return this.listener(message) - } - - verify (message) { - if (this.channel && this.channel !== message.channel.id) return false - if (this.filter(message, this)) { - this.collected.set(message.id, message) - this.emit('message', message, this) - if (this.collected.size >= (this.options.maxMatches || Infinity)) { - this.stop({ reason: 'maxMatches', arg: this.options.maxMatches }) - } else if (this.options.max && this.collected.size === (this.options.max || Infinity)) { - this.stop({ reason: 'max', arg: this.options.max }) - } - return true - } - return false - } - - get next () { - return new Promise((resolve, reject) => { - if (this.ended) { - reject({ collected: this.collected, reason: this.ended.reason, arg: this.ended.arg }) - return - } - - const cleanup = () => { - this.removeListener('message', onMessage) - this.removeListener('end', onEnd) - } - - const onMessage = (...args) => { - cleanup() - resolve(...args) - } - - const onEnd = (...args) => { - cleanup() - reject(...args) - } - - this.once('message', onMessage) - this.once('end', onEnd) - }) - } - - stop ({ reason = 'user', arg } = {}) { - if (this.ended) return - this.ended = { reason, arg } - this.emit('end', { collected: this.collected, reason, arg }) - } -} - -module.exports = MessageCollector diff --git a/src/core/util/Parser.js b/src/core/util/Parser.js deleted file mode 100644 index 7edd017..0000000 --- a/src/core/util/Parser.js +++ /dev/null @@ -1,56 +0,0 @@ -const requireAll = require('require-all') -const Collection = require('./Collection') - -class Parser { - constructor (folderpath) { - this._folder = folderpath - - if (typeof this._folder === 'string') { - this.reload() - } - } - - reload () { - if (typeof this._folder !== 'string') { - throw new TypeError('Invalid locale filepath') - } - Object.keys(require.cache).forEach(filepath => { - if (!filepath.startsWith(this._folder)) return - delete require.cache[require.resolve(filepath)] - }) - const data = requireAll(this._folder) - this.strings = new Collection().load(data) - } - - locate (fullkey, obj) { - const val = fullkey.split('.').reduce((o, i) => o === null ? o : o[i] || null, obj) - return Array.isArray(val) ? val.join('\n') : val - } - - get (key = 'common', locale = 'en') { - if (!this.strings.has(locale)) locale = 'en' - return this.locate(key, this.strings.get(locale)) - } - - shift (string, options) { - if (!string) return string - return string.split(' ').map(str => ( - str.replace(/\{\{(.+)\}\}/gi, (matched, key) => ( - this.locate(key, options) || matched - )) - )).join(' ') - } - - parse (string, group = 'common', locale, options = {}) { - if (!string) return string - return string.split(' ').map(str => ( - str.replace(/\{\{(.+)\}\}/gi, (matched, key) => { - const fullKey = key.startsWith('%') ? `common.${key.substr(1)}` : `${group}.${key}` - let val = this.get(fullKey, locale) || this.get(`common.${key}`, locale) - return typeof val === 'string' ? this.shift(val, options) : matched - }) - )).join(' ') - } -} - -module.exports = Parser diff --git a/src/core/util/Permitter.js b/src/core/util/Permitter.js deleted file mode 100644 index 93365cc..0000000 --- a/src/core/util/Permitter.js +++ /dev/null @@ -1,95 +0,0 @@ -const Permitter = { - contexts: ['members', 'roles', 'channels'], - - verify (query, rawPerms, node, defVal = true) { - for (const context of Permitter.contexts) { - const list = query[context] - if (typeof list === 'undefined') continue - for (const id of Array.isArray(list) ? list : [list]) { - if (Permitter.check(node, context, id, rawPerms, defVal) === !defVal) { - return !defVal - } - } - } - return defVal - }, - - isBoolean (val) { - return val === true || val === false - }, - - hasWildcard (obj) { - return obj !== null && '*' in obj ? Permitter.isBoolean(obj['*']) : false - }, - - verifyMessage (node, msg, perms = {}, defVal = true) { - if (!msg.guild) return true - let res = Permitter.check(`${msg.channel.id}.${msg.author.id}.${node}`, perms) - if (Permitter.isBoolean(res)) return res - - for (const perm of msg.member.roles.map(r => `${msg.channel.id}.${r}.${node}`)) { - res = Permitter.check(perm, perms) - if (Permitter.isBoolean(res)) return res - } - - res = Permitter.check(`*.${msg.author.id}.${node}`, perms) - if (Permitter.isBoolean(res)) return res - - for (const perm of msg.member.roles.map(r => `*.${r}.${node}`)) { - res = Permitter.check(perm, perms) - if (Permitter.isBoolean(res)) return res - } - - res = Permitter.check(`${msg.channel.id}.${node}`, perms) - if (Permitter.isBoolean(res)) return res - - res = Permitter.check(`*.*.${node}`, perms) - if (Permitter.isBoolean(res)) return res - - return defVal - }, - - check (node, perms = {}) { - const res = node.split('.').reduce((obj, idx) => { - if (obj === null || Permitter.isBoolean(obj)) return obj - if (idx in obj) return obj[idx] - else if ('*' in obj) return obj['*'] - return null - }, perms) - if (res === true || res === false) return res - return null - }, - - allow (node, perms) { - return Permitter.grant(node, true, perms) - }, - - deny (node, perms) { - return Permitter.grant(node, false, perms) - }, - - grant (node, val = true, rawPerms = {}) { - const nodes = node.split('.') - const last = nodes.length - 1 - - nodes.reduce((o, c, i) => { - if (i >= last) { - if (typeof o['*'] === 'undefined') o['*'] = null - if (o[c] === true || o[c] === false && o[c] !== val) { - o[c] = null - } else { - o[c] = val - } - } else if (typeof o[c] === 'undefined') { - o[c] = {} - } else if (o[c] === true || o[c] === false) { - o[c] = { '*': o[c] } - } - return o[c] - }, rawPerms) - - return rawPerms - } -} - -module.exports = Permitter diff --git a/src/core/util/Responder.js b/src/core/util/Responder.js deleted file mode 100644 index 98f1db5..0000000 --- a/src/core/util/Responder.js +++ /dev/null @@ -1,198 +0,0 @@ -const emojis = require('node-emoji') -const { padEnd } = require('./Util') -const emoji = require('./Emojis') - -class Responder { - constructor (command) { - this.command = command - this.i18n = command.i18n - - this.responseMethods = { - send: (msg, res) => res, - reply: (msg, res) => `**${msg.author.username}**, ${res}`, - success: (msg, res) => `${emoji.success} | **${msg.author.username}**, ${res}`, - error: (msg, res) => `${emoji.error} | **${msg.author.username}**, ` + (res || '{{%ERROR}}') - } - - this.formatMethods = { - bold: (res) => `**${res}**`, - italic: (res) => `*${res}*`, - underline: (res) => `__${res}__`, - strikethrough: (res) => `~~${res}~~`, - inlineCode: (res) => `\`${res}\``, - code: (res, type = '') => `\`\`\`${type}\n${res}\n\`\`\``, - emoji: (res, type) => `${this.i18n.locate(type, emoji) || emojis.get(type) || emoji.success} | ${res}` - } - } - - create ({ msg: message, settings }, data) { - let responder = (...args) => responder.send(...args) - responder.command = this.command - responder.message = message - responder.settings = settings - responder.data = data - responder._options = {} - - responder.responseMethods = this.responseMethods - responder.formatMethods = this.formatMethods - - const copy = ['_send', 't', 'clean', 'typing', 'format', 'file', 'embed', 'dialog', 'selection'] - copy.forEach(prop => { responder[prop] = this[prop].bind(responder) }) - - for (let method in this.responseMethods) { - responder[method] = responder._send.bind(responder, method) - } - - return responder - } - - t (content = '', tags = {}) { - const cmd = this.command - const file = cmd.name ? cmd.name.split(':')[0] : (cmd.labels ? cmd.labels[0] : 'common') - return cmd.i18n.parse(content, cmd.localeKey || file || null, this.settings.lang, tags) - } - - _send (method, response = '', options = {}) { - const message = this.message - const formats = this._formats || [] - - Object.assign(options, this._options || {}) - - if (response instanceof Array) response = response.join('\n') - response = this.command.t(response, this.settings.lang, options) - response = this.responseMethods[method || 'send'](message, response) - - for (let format of formats) { - format = format.split(':') - response = this.formatMethods[format[0]](response, format[1]) - } - - const promise = (options.DM ? this.command.bot.getDMChannel(message.author.id) : Promise.resolve(message.channel)) - .then(channel => this.command.send(channel, response, options)) - - delete this._formats - this._options = {} - - return promise - } - - clean () { - delete this._formats - return this - } - - typing () { - return this.message.channel.sendTyping() - } - - format (formats) { - this._formats = (formats instanceof Array) ? formats : [formats] - return this - } - - file (name, file) { - this._options.file = { name, file } - return this - } - - embed (embed) { - this._options.embed = embed - return this - } - - async dialog (dialogs = [], options = {}) { - const { message, data } = this - const { tries = 10, time = 60, matches = 10, filter, cancel = 'cancel' } = options - - const args = {} - let cancelled = false - for (const dialog of dialogs) { - let prompt = dialog.prompt - const input = this.command.resolver - if (Array.isArray(prompt) && prompt.length) { - prompt[0] = `**${prompt[0]}**` - } - - let p1 = await this.send(prompt, options) - const collector = this.command.bot.engine.bridge.collect({ - channel: message.channel.id, - author: message.author.id, - tries, - time, - matches, - filter - }) - - const awaitMessage = async (msg) => { - try { - var ans = await collector.next() - if (ans.content.toLowerCase() === cancel) return Promise.reject() - try { - return await input.resolve(ans, [ans.cleanContent], data, dialog.input) - } catch (err) { - let p2 = await this.format('emoji:error').send( - `${err.err || err.message || err || '{{%menus.ERROR}}'}\n\n{{%menus.EXIT}}`, - Object.assign(err, { cancel: `\`${cancel}\`` }) - ) - return awaitMessage(p2) - } - } catch (o) { - return Promise.reject(o) - } finally { - this.command.deleteMessages(msg, ans) - } - } - - try { - Object.assign(args, await awaitMessage()) - collector.stop() - } catch (err) { - if (typeof err !== 'undefined') { - this.error(`{{%menus.ERRORED}} **{{%collector.${err.reason}}}**`, { - [err.reason]: err.arg, err: `**${err.reason}**` - }) - } else { - this.success('{{%menus.EXITED}}') - } - collector.stop() - cancelled = true - break - } finally { - p1.delete() - } - } - - if (cancelled) return Promise.reject() - return Promise.resolve(args) - } - - async selection (selections = [], options = {}) { - if (!Array.isArray(selections)) return [selections, 0] - if (!selections.length) return [] - if (selections.length === 1) return [selections[0], 0] - - const { title = '{{%menus.SELECTION}}', footer = '{{%menus.INPUT}}', mapFunc } = options - const choices = (mapFunc ? selections.map(mapFunc) : selections).splice(0, 10) - - try { - const { reply } = await this.dialog([{ - prompt: [ - '```markdown', - `### ${title} ###\n`, - choices.map((c, i) => `${padEnd(`[${i + 1}]:`, 4)} ${c}`).join('\n'), - selections.length > 10 ? '{{%menus.MORE_RESULTS}}\n' : '', - Array.isArray(footer) ? footer.join('\n') : '> ' + footer, - '```' - ].join('\n'), - input: { type: 'int', name: 'reply', min: 1, max: choices.length } - }], Object.assign(options, { - num: selections.length - 10, cancel: options.cancel || 'cancel' - })) - return [selections[reply - 1], reply - 1] - } catch (err) { - return [] - } - } -} - -module.exports = Responder diff --git a/src/core/util/Util.js b/src/core/util/Util.js deleted file mode 100644 index 7639183..0000000 --- a/src/core/util/Util.js +++ /dev/null @@ -1,32 +0,0 @@ -const fs = require('fs') -const path = require('path') - -const padEnd = (v, n = 0, c = ' ') => String(v).length >= n ? '' + v : String(v) + String(c).repeat(n - String(v).length) -const padStart = (v, n = 0, c = ' ') => String(v).length >= n ? '' + v : (String(c).repeat(n) + v).slice(-n) - -async function readdirRecursive (...paths) { - const dir = path.join(...paths) - let list = [] - if (!fs.existsSync(dir)) return list - let files = fs.readdirSync(dir) - let dirs - - function isDir (fname) { - return fs.existsSync(path.join(dir, fname)) ? fs.statSync(path.join(dir, fname)).isDirectory() : false - } - - dirs = files.filter(isDir) - files = files.filter(file => !isDir(file)).map(file => path.join(dir, file)).filter(file => !path.basename(file).startsWith('.')) - list = list.concat(files) - - while (dirs.length) { - let d = path.join(dir, dirs.shift()) - list = list.concat(await readdirRecursive(d)) - } - - return list -} - -module.exports = { - padEnd, padStart, readdirRecursive -} diff --git a/src/core/util/index.js b/src/core/util/index.js deleted file mode 100644 index 13013c9..0000000 --- a/src/core/util/index.js +++ /dev/null @@ -1,11 +0,0 @@ -const util = require('./Util') - -module.exports = Object.assign({ - Collection: require('./Collection'), - Responder: require('./Responder'), - Parser: require('./Parser'), - Permitter: require('./Permitter'), - Emojis: require('./Emojis'), - LocalCache: require('./LocalCache'), - Cache: require('./Cache') -}, util) diff --git a/src/index.js b/src/index.js deleted file mode 100644 index ee47c2a..0000000 --- a/src/index.js +++ /dev/null @@ -1,41 +0,0 @@ -const chalk = require('chalk') -const cluster = require('cluster') -const winston = require('winston') -const winstonCluster = require('winston-cluster') - -const { Automaton, ShardManager } = require('./core') - -if (cluster.isMaster) { - winstonCluster.bindListeners() - const shardManager = new ShardManager( - parseInt(process.env.CLIENT_PROCESSES, 10), - parseInt(process.env.CLIENT_SHARDS_PER_PROCESS, 10) - ) - shardManager.createShards() -} else { - winstonCluster.bindTransport() - const processShards = parseInt(process.env.CLIENT_SHARDS_PER_PROCESS, 10) - const firstShardID = parseInt(process.env.BASE_SHARD_ID, 10) * processShards - const lastShardID = firstShardID + processShards - 1 - const maxShards = parseInt(process.env.CLIENT_PROCESSES, 10) * processShards - - const automaton = new Automaton({ firstShardID, lastShardID, maxShards }) - - automaton.once('ready', () => { - const { guilds, channels, users } = automaton.fetchCounts() - - winston.info(`${chalk.red.bold('iris')} - ${ - firstShardID === lastShardID - ? `Shard ${firstShardID} is ready` - : `Shards ${firstShardID} to ${lastShardID} are ready` - }`) - winston.info( - `G: ${chalk.green.bold(guilds)} | ` + - `C: ${chalk.green.bold(channels)} | ` + - `U: ${chalk.green.bold(users)}` - ) - winston.info(`Prefix: ${chalk.cyan.bold(process.env.CLIENT_PREFIX)}`) - }) - automaton.on('error', err => winston.error(err)) - automaton.run() -} diff --git a/src/ipc/eval.js b/src/ipc/eval.js deleted file mode 100644 index 68b5749..0000000 --- a/src/ipc/eval.js +++ /dev/null @@ -1,16 +0,0 @@ -const util = require('util') - -module.exports = async function evaluate (msg, bot) { - const content = msg.d ? msg.d.content || true : true - let resp - try { - resp = eval(String(content)) - } catch (err) { - resp = `${err.message}\n\n${err.stack}` - } - if (Array.isArray(resp) || typeof resp === 'object') { - resp = util.inspect(resp) - } - - process.send({ op: 'resp', d: resp }) -} diff --git a/src/ipc/kill.js b/src/ipc/kill.js deleted file mode 100644 index d60c3b2..0000000 --- a/src/ipc/kill.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = async function kill (msg, bot) { - process.send({ op: 'resp', d: 'success' }) - setTimeout(() => process.exit(1), 3000) -} diff --git a/src/ipc/reload.js b/src/ipc/reload.js deleted file mode 100644 index fae7986..0000000 --- a/src/ipc/reload.js +++ /dev/null @@ -1,13 +0,0 @@ -module.exports = async function reload (msg, bot) { - const type = msg.d.type || 'commands' - const group = msg.d.group - const file = msg.d.file - - try { - await bot.engine.reload(type, group, file) - bot.engine[`load${type.charAt(0).toUpperCase()}${type.substr(1)}`](group, file) - process.send({ op: 'resp', d: 'success' }) - } catch (err) { - process.send({ op: 'resp', d: err.toString() }) - } -} diff --git a/src/middleware/checkPrivate.js b/src/middleware/checkPrivate.js deleted file mode 100644 index 29497ff..0000000 --- a/src/middleware/checkPrivate.js +++ /dev/null @@ -1,8 +0,0 @@ -module.exports = { - priority: 2, - process: (container) => { - const { msg } = container - container.isPrivate = !msg.guild - return Promise.resolve(container) - } -} diff --git a/src/middleware/cleverbot.js b/src/middleware/cleverbot.js deleted file mode 100644 index e711b6e..0000000 --- a/src/middleware/cleverbot.js +++ /dev/null @@ -1,28 +0,0 @@ -const logger = require('winston') -const chalk = require('chalk') - -module.exports = { - priority: 6, - process: async container => { - const { settings, msg, modules, client, isPrivate } = container - const { prefix } = settings - const defPrefix = process.env.CLIENT_PREFIX - - if (msg.content.startsWith(prefix) || msg.content.startsWith(defPrefix)) return container - - const mention = msg.content.split(' ')[0] - if (!mention.match(new RegExp('<@!*' + client.user.id + '>'))) return - - const cleverbot = modules.get('cleverbot') - if (!cleverbot) return - await cleverbot.respond(msg.cleanContent.split(' ').slice(1).join(' '), msg.channel) - - logger.info(`${chalk.bold.magenta( - !isPrivate - ? msg.guild.name - : '(in PMs)' - )} > ${chalk.bold.green(msg.author.username)}: ` + - `${chalk.bold.yellow(msg.cleanContent.replace(/\n/g, ' '))}`) - return - } -} diff --git a/src/middleware/commandCheck.js b/src/middleware/commandCheck.js deleted file mode 100644 index 288b268..0000000 --- a/src/middleware/commandCheck.js +++ /dev/null @@ -1,31 +0,0 @@ -const chalk = require('chalk') -const logger = require('winston') - -const { Permitter } = require('../core') - -module.exports = { - priority: 100, - process: container => { - const { msg, isPrivate, isCommand, cache, commander, trigger, settings, admins } = container - if (!isCommand) return Promise.resolve() - const cmd = commander.get(trigger).cmd - if (!admins.includes(msg.author.id) || !(cmd.options.modOnly || cmd.options.adminOnly)) { - const isAllowed = Permitter.verifyMessage(cmd.permissionNode, msg, settings.permissions) - if (!isAllowed) return Promise.resolve() - } - - logger.info(`${chalk.bold.magenta( - !isPrivate - ? msg.guild.name - : '(in PMs)' - )} > ${chalk.bold.green(msg.author.username)}: ` + - `${chalk.bold.blue(msg.cleanContent.replace(/\n/g, ' '))}`) - - cache.client.multi() - .hincrby('usage', commander.get(trigger).cmd.labels[0], 1) - .hincrby('usage', 'ALL', 1) - .exec() - - return Promise.resolve(container) - } -} diff --git a/src/middleware/parseMessage.js b/src/middleware/parseMessage.js deleted file mode 100644 index ee938eb..0000000 --- a/src/middleware/parseMessage.js +++ /dev/null @@ -1,15 +0,0 @@ -module.exports = { - priority: 10, - process: container => { - const { settings, msg, commander } = container - const { prefix } = settings - const defPrefix = process.env.CLIENT_PREFIX - - const chk = msg.content.startsWith(prefix) - const rawArgs = msg.content.substring((chk ? prefix : defPrefix).length).split(' ') - container.trigger = rawArgs[0].toLowerCase() - container.isCommand = commander.has(container.trigger) - container.rawArgs = rawArgs.slice(1).filter(v => !!v) - return Promise.resolve(container) - } -} diff --git a/src/middleware/verifySettings.js b/src/middleware/verifySettings.js deleted file mode 100644 index 04f1c0c..0000000 --- a/src/middleware/verifySettings.js +++ /dev/null @@ -1,22 +0,0 @@ -module.exports = { - priority: 5, - process: async container => { - const { client, msg, isPrivate, data, db } = container - try { - if (isPrivate) { - let channel = await client.getDMChannel(msg.author.id) - container.settings = new db.Guild({ id: channel.id }) - return container - } - let settings = await data.Guild.fetch(msg.guild.id) - if (!settings) { - settings = new db.Guild({ id: msg.guild.id }) - await settings.save() - } - container.settings = settings - return container - } catch (err) { - throw err - } - } -} diff --git a/src/models/Guild.js b/src/models/Guild.js deleted file mode 100644 index e433b19..0000000 --- a/src/models/Guild.js +++ /dev/null @@ -1,20 +0,0 @@ -module.exports = function () { - const type = this.thinky.type - const object = type.object - const string = type.string - const bool = type.boolean - - return { - tableName: 'Guild', - schema: { - id: string(), - permissions: object().default({}), - deleted: bool().default(false), - prefix: string().default(process.env.CLIENT_PREFIX), - lang: string().default('en'), - tz: string().default('utc') - }, - cache: true, - expiry: 10 - } -} diff --git a/src/models/User.js b/src/models/User.js deleted file mode 100644 index 823e8c1..0000000 --- a/src/models/User.js +++ /dev/null @@ -1,25 +0,0 @@ -module.exports = function () { - const type = this.thinky.type - const string = type.string - const number = type.number - const bool = type.boolean - const array = type.array - - return { - tableName: 'User', - schema: { - id: string(), - credits: number().default(0), - exp: number().default(0), - deleted: bool().default(false), - title: string().default('Commoner'), - description: string().default('A simple wandering soul'), - inventory: array().default([]) - }, - cache: true, - expiry: 300 * 1000, - relations: { - hasOne: ['Companion', 'companion', 'id', 'id'] - } - } -} From bee0eb85f9fa1651359195e1b4092242d0b3af31 Mon Sep 17 00:00:00 2001 From: pyraxo Date: Wed, 28 Dec 2016 12:15:38 +0800 Subject: [PATCH 02/21] Initial rewrite commit --- .jsdoc.json | 5 +- README.md | 45 +++++++ package.json | 24 ++-- src/core/Client.js | 127 ++++++++++++++++++++ src/core/engine/Bridge.js | 220 +++++++++++++++++++++++++++++++++++ src/core/engine/Commander.js | 198 +++++++++++++++++++++++++++++++ src/core/engine/Router.js | 170 +++++++++++++++++++++++++++ src/core/engine/index.js | 5 + src/managers/Resolver.js | 202 ++++++++++++++++++++++++++++++++ src/managers/Transmitter.js | 117 +++++++++++++++++++ src/util/Collection.js | 94 +++++++++++++++ src/util/Utils.js | 73 ++++++++++++ src/util/index.js | 3 + 13 files changed, 1269 insertions(+), 14 deletions(-) create mode 100644 README.md create mode 100644 src/core/Client.js create mode 100644 src/core/engine/Bridge.js create mode 100644 src/core/engine/Commander.js create mode 100644 src/core/engine/Router.js create mode 100644 src/core/engine/index.js create mode 100644 src/managers/Resolver.js create mode 100644 src/managers/Transmitter.js create mode 100644 src/util/Collection.js create mode 100644 src/util/Utils.js create mode 100644 src/util/index.js diff --git a/.jsdoc.json b/.jsdoc.json index ee700ac..1c89a21 100644 --- a/.jsdoc.json +++ b/.jsdoc.json @@ -9,7 +9,8 @@ "excludePattern": "(node_modules/|docs)" }, "plugins": [ - "plugins/markdown" + "plugins/markdown", + "node_modules/jsdoc-strip-async-await" ], "templates": { "cleverLinks": false, @@ -20,6 +21,6 @@ "encoding": "utf8", "verbose": true, "recurse": true, - "template": "node_modules/minami" + "template": "node_modules/docdash" } } diff --git a/README.md b/README.md new file mode 100644 index 0000000..0dbbd69 --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +
+

+
+ iris +
+

+

The better Discord bot framework

+

+ +
+ Built with ❤ +
+ Studio 777 +

+
+ +### Used by +* [Tatsumaki](https://tatsumaki.xyz), a multi-purpose social Discord bot +* [haru](https://pyraxo.moe/haru), everyone's favourite idol and part-time bot + +**iris** is an advanced, efficient and highly customisable base for Discord command bots written in Node.js + +### Requirements +* **Node.js 7+** + +A firm grasp of **ES6 + async/await** syntax is recommended. + +### Usage +```bash +$ npm install --save pyraxo/iris +``` + +#### Quick Example +```js +const Bot = require('iris') + +const client = new Bot({ + token: 'your token here' +}) + +client.run() +``` + +### Configuration +As the bot framework extends the [Eris](https://github.com/abalabahaha/Eris) client, please refer to the docs [here](https://abal.moe/Eris/docs). diff --git a/package.json b/package.json index 5492187..f2a240f 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,7 @@ "description": "The better Discord bot base", "main": "index.js", "scripts": { - "start": "node --harmony_async_await index.js", - "gendocs": "node_modules/.bin/jsdoc --configure .jsdoc.json --verbose" + "docs": "node_modules/.bin/jsdoc --configure .jsdoc.json --verbose" }, "engines": { "node": ">=7.0.0" @@ -20,25 +19,26 @@ "author": "pyraxo (https://pyraxo.moe)", "license": "AGPL-3.0", "dependencies": { - "bluebird": "^3.4.6", + "bluebird": "^3.4.7", + "eventemitter3": "^2.0.2", "chalk": "^1.1.3", "eris": "github:abalabahaha/eris#dev", - "eventemitter3": "^2.0.1", - "longjohn": "^0.2.11", "moment": "^2.13.0", "moment-timezone": "^0.5.5", + "require-all": "^2.0.0", + "superagent": "^2.2.0" + }, + "devDependencies": { + "docdash": "^0.4.0", + "jsdoc": "^3.4.3", + "jsdoc-strip-async-await": "^0.1.0" + }, + "optionalDependencies": { "node-opus": "^0.2.4", "redis": "^2.6.1", - "require-all": "^2.0.0", - "superagent": "^2.2.0", "thinky": "^2.3.7", "winston": "^2.2.0", "winston-cluster": "0.0.4", "winston-daily-rotate-file": "^1.3.1" - }, - "devDependencies": { - "jsdoc": "^3.4.0", - "minami": "^1.1.1", - "standard": "^8.1.0" } } diff --git a/src/core/Client.js b/src/core/Client.js new file mode 100644 index 0000000..b3ac0b1 --- /dev/null +++ b/src/core/Client.js @@ -0,0 +1,127 @@ +const Eris = require('eris').client + +const { Commander, Router, Bridge } = require('./core/engine') +const { Collection } = require('./util') + +/** + * The Eris client + * @external "Eris.Client" + * @see {@link https://abal.moe/Eris/docs/Client|Eris.Client} + */ + +/** + * The Eris message object + * @external "Eris.Message" + * @see {@link https://abal.moe/Eris/docs/Message|Eris.Message} + */ + +/** + * Interface between the Discord client and plugins + * @version 3.0.0 + * @extends Eris.Client + * @prop {Collection} plugins A Collection of plugins + * @see {@link https://abal.moe/Eris/docs/Client|Eris.Client} + */ +class Client extends Eris { + /** + * Creates a new Client instance + * @arg {Object} options An object containing iris's and/or Eris client options + * @arg {String} options.token Discord bot token + * @arg {String} [options.commands] Relative path to commands folder + * @arg {String} [options.modules] Relative path to modules folder + * @arg {String} [options.middleware] Relative path to middleware folder + * @arg {String} [options.suppressWarnings] Option to suppress console warnings + */ + constructor (options = {}) { + super(options.token, options) + this.suppressWarnings = options.suppressWarnings + + this.plugins = new Collection() + + this + .createPlugin('commands', Commander) + .createPlugin('modules', Router) + .createPlugin('middleware', Bridge) + + if (options.commands) this.register('commands', options.commands) + if (options.modules) this.register('modules', options.modules) + if (options.middleware) this.register('middleware', options.middleware) + } + + /** + * Creates a plugin + * @arg {String} type The type of plugin + * @arg {Plugin} Plugin Plugin class + * @returns {Client} + */ + createPlugin (type, Plugin) { + const plugin = new Plugin(this) + this.plugins.set(type, plugin) + return this + } + + /** + * Registers plugins + * @arg {String} type The type of plugin
+ * Defaults: `commands`, `modules`, `middleware`, `resolvers`, `ipc` + * @arg {...*} args Arguments supplied to the plugin + * @returns {Client} + */ + register (type, ...args) { + if (typeof type !== 'string') { + throw new Error('Invalid type supplied to register') + } + const plugin = this.plugins.get(type) + if (!plugin) { + throw new Error(`Plugin type ${type} not found`) + } + plugin.register(...args) + return this + } + + /** + * Unregisters plugins + * @arg {String} type The type of plugin
+ * Defaults: `commands`, `modules`, `middleware`, `resolvers`, `ipc` + * @arg {...*} args Arguments supplied to the plugin + * @returns {Client} + */ + unregister (type, ...args) { + if (typeof type !== 'string') { + throw new Error('Invalid type supplied to register') + } + const plugin = this.plugins.get(type) + if (!plugin) { + throw new Error(`Plugin type ${type} not found`) + } + plugin.unregister(...args) + return this + } + + /** + * Runs the bot + * @returns {Client} + */ + run () { + if (typeof this.token !== 'string') { + throw new TypeError('No bot token supplied') + } + this.connect() + return this + } + + /** + * Emits an error or throws when there are no listeners + * @arg {String} event Event name + * @arg {Error} error Thrown or emitted error + * @private + */ + throwOrEmit (event, error) { + if (!this.listeners(event, true)) { + throw error + } + this.emit(event, error) + } +} + +module.exports = Client diff --git a/src/core/engine/Bridge.js b/src/core/engine/Bridge.js new file mode 100644 index 0000000..2f5e4ed --- /dev/null +++ b/src/core/engine/Bridge.js @@ -0,0 +1,220 @@ +const path = require('path') +const fs = require('fs') +const requireAll = require('require-all') + +/** + * Middleware manager for commands + * @prop {Array} tasks An array of middleware + * @prop {Array} collectors An array of message collectors + */ +class Bridge { + /** + * Creates a new Bridge instance + * @arg {Commander} commander Commander instance + */ + constructor (commander) { + this.tasks = [] + this.collectors = [] + this._commander = commander + } + + /** + * Registers middleware + * @param {String|Object|Array} middleware An object, array or relative path to a folder or file to load middleware from + * @returns {Client} + */ + register (middleware) { + switch (typeof middleware) { + case 'string': { + const filepath = path.join(__dirname, middleware) + if (!fs.existsSync(filepath)) { + throw new Error(`Folder path ${filepath} does not exist`) + } + const middleware = fs.statSync(filepath).isDirectory() ? requireAll(filepath) : require(filepath) + return this.register(middleware) + } + case 'object': { + if (Array.isArray(middleware)) { + for (const mw of middleware) { + this.push(mw) + } + return this + } + for (const key in middleware) { + this.push(middleware[key]) + } + return this + } + default: { + throw new Error('Path supplied is not an object or string') + } + } + } + + /** + * Methods that parses messages and adds properties to a context container + * @typedef {Object} Middleware + * @prop {String} name Name of middleware + * @prop {Number} priority Priority level of the middleware + * @prop {Promise(Container)} Middleware process + */ + + /** + * Inserts new middleware to the task queue according to ascending priority (lower numbers are earlier in queue) + * @arg {Middleware} middleware Middleware object + */ + push (middleware) { + const priority = middleware.priority || this.tasks.length + if (!middleware.process || !middleware.process.then) { + throw new Error('Middleware must be a promise') + } + this.tasks.splice(priority, 0, middleware) + } + + /** + * Creates a message collector + * @arg {Object} options Collector options + * @arg {String} options.filter The filter function to pass all messages through + * @arg {String} [options.channel] The channel ID to filter messages from + * @arg {String} [options.author] The author ID to filter messages from + * @arg {Number} [options.tries=10] Max number of attempts to filter a message + * @arg {Number} [options.time=60] Max length of time to wait for messages, in seconds + * @arg {Number} [options.matches=10] Max number of successful filtered messages + * @returns {Collector} Message collector object + */ + collect (options = {}) { + const { tries = 10, time = 60, matches = 10, channel, author, filter } = options + + /** + * Message collector object, intended for menus + * @namespace Collector + * @type {Object} + */ + const collector = { + /** + * An array of collected messages + * @memberof Collector + * @type {Array} + */ + collected: [], + _tries: 0, + _matches: 0, + _listening: false, + _ended: false, + _timer: time ? setTimeout(() => { + collector._ended = { + reason: 'timeout', + arg: time, + collected: collector.collected + } + }, time * 1000) : null + } + /** + * Stop collecting messages + * @memberof Collector + * @method + */ + collector.stop = () => { + collector._listening = false + this.collectors.splice(this.collectors.indexOf(collector), 1) + } + /** + * Resolves when message is collected, and rejects when collector has ended + * @memberof Collector + * @returns {Promise} + */ + collector.next = () => { + return new Promise((resolve, reject) => { + collector._resolve = resolve + if (collector._ended) { + collector.stop() + reject(collector._ended) + } + collector._listening = true + }) + } + /** + * Pass a message object to be filtered + * @memberof Collector + * @method + * @returns {Boolean} + */ + collector.passMessage = msg => { + if (!collector._listening) return false + if (author && author !== msg.author.id) return false + if (channel && channel !== msg.channel.id) return false + if (typeof filter === 'function' && !filter(msg)) return false + + collector.collected.push(msg) + if (collector.collected.size >= matches) { + collector._ended = { reason: 'maxMatches', arg: matches } + } else if (tries && collector.collected.size === tries) { + collector._ended = { reason: 'max', arg: tries } + } + collector._resolve(msg) + return true + } + this.collectors.push(collector) + return collector + } + + /** + * Destroy all tasks and collectors + */ + destroy () { + this.tasks = [] + this.collectors = [] + } + + /** + * Remove middleware by name and returns it if found + * @arg {String} name Middleware name + * @returns {?Middleware} + */ + unregister (name) { + const middleware = this.tasks.find(mw => mw.name === name) + if (!middleware) return null + this.tasks.splice(this.tasks.indexOf(middleware, 1)) + return middleware + } + + /** + * Context container holding a message object along with added properties and objects + * @typedef {Object} Container + * @prop {external:"Eris.Message"} msg The message object + * @prop {Client} client The client object + * @prop {String} trigger The command trigger
+ * At least one middleware should add this into the container; default middleware does it for you + */ + + /** + * Collects and executes messages after running them through middleware + * @arg {Container} container The message container + * @returns {Promise} + */ + async handle (container) { + const { msg } = container + for (let collector of this.collectors) { + const collected = collector.passMessage(msg) + if (collected) return Promise.reject() + } + for (const task of this.tasks) { + try { + const result = await task(container) + if (!result) return Promise.reject() + container = result + } catch (err) { + throw err + } + } + try { + if (!container.trigger) return Promise.reject() + this._commander.execute(container.trigger, container) + } catch (err) { + throw err + } + return container + } +} + +module.exports = Bridge diff --git a/src/core/engine/Commander.js b/src/core/engine/Commander.js new file mode 100644 index 0000000..5598ea5 --- /dev/null +++ b/src/core/engine/Commander.js @@ -0,0 +1,198 @@ +const path = require('path') +const fs = require('fs') +const requireAll = require('require-all') + +const Collection = require('../../util/Collection') + +/** + * Commander class for command processing + */ +class Commander extends Collection { + /** + * Creates a new Commander instance + * @arg {Client} client Client instance + */ + constructor (client) { + super() + this._client = client + } + + /** + * Registers commands + * @param {String|Object|Array} commands An object, array or relative path to a folder or file to load commands from + * @returns {Client} + */ + register (commands) { + switch (typeof commands) { + case 'string': { + const filepath = path.join(__dirname, commands) + if (!fs.existsSync(filepath)) { + throw new Error(`Folder path ${filepath} does not exist`) + } + const cmds = fs.statSync(filepath).isDirectory() ? requireAll(filepath) : require(filepath) + return this.registerCommands(cmds) + } + case 'object': { + if (Array.isArray(commands)) { + for (const command of commands) { + this.commands.attach(command) + } + return this + } + for (const group in commands) { + this.commands.attach(commands[group], group) + } + return this + } + default: { + throw new Error('Path supplied is not an object or string') + } + } + } + + /** + * Class, object or function that can be utilised as a command + * @typedef {(Object|function)} AbstractCommand + * @prop {Array} triggers An array of command triggers, the first is the main trigger while the rest are aliases + * @prop {String} [group] Command group + * @prop {function(Container)} execute The command's execution function
+ * It should accept a {@link Container} as the first argument + */ + + /** + * Attaches a command + * @arg {AbstractCommand} Command Command class, object or function + * @arg {String} [group] Default command group, will be overwritten by group setting in the command + * @returns {Commander} + */ + attach (Command, group) { + let command = typeof Command === 'function' ? new Command(this._client) : command + if (!command.triggers) { + this._client.throwOrEmit('commander:error', new Error(`Invalid command - ${command}`)) + return this + } + for (const trigger of command.triggers) { + if (this.has(trigger)) { + this._client.throwOrEmit('commander:error', new Error(`Duplicate command - ${trigger}`)) + return this + } + command.group = command.group || group + this.set(trigger.toLowerCase(), command) + } + + /** + * Fires when a command is registered + * + * @event Client#commander:registered + * @type {Object} + * @prop {String} trigger Command trigger + * @prop {String} group Command group + * @prop {Number} aliases Number of trigger aliases + */ + this._client.emit('commander:registered', { + trigger: command.triggers[0], + group: command.group, + aliases: command.triggers.length - 1 + }) + return this + } + + /** + * Unregisters a command group or trigger + * @arg {?String} group The command group + * @arg {String} [trigger] The command trigger + * @returns {Commander} + */ + unregister (group, trigger) { + if (this.group) { + return this.ejectGroup(group, trigger) + } + return this.eject(trigger) + } + + /** + * Ejects a command + * @arg {String} trigger The command trigger + * @returns {Commander} + */ + eject (trigger) { + const command = this.get(trigger) + if (command) { + for (const trigger of command.triggers) { + this.delete(trigger) + } + + /** + * Fires when a command is ejected + * + * @event Client#commander:ejected + * @type {Object} + * @prop {String} group Command group + * @prop {Number} aliases Number of trigger aliases + */ + this._client.emit('commander:ejectedGroup', { + trigger: command.triggers[0], + aliases: command.triggers.length - 1 + }) + } + return this + } + + /** + * Ejects a command group + * @arg {String} [group='*] The command group to be ejected + * @arg {String} [trigger] The command trigger in the group + * @returns {Commander} + */ + ejectGroup (group = '*', trig) { + let count = 0 + for (const [trigger, command] of this.entries()) { + if (command.group === group || group === '*' && trig === trigger) { + this.delete(trigger) + count++ + } + } + + /** + * Fires when a command group is ejected + * + * @event Client#commander:ejectedGroup + * @type {Object} + * @prop {String} group Command group + * @prop {Number} count Number of ejected commands + */ + this._client.emit('commander:ejectedGroup', { group, count }) + return this + } + + /** + * Executes a command + * @arg {String} trigger The trigger of the command to be executed + * @arg {...*} args Arguments to be supplied to the command + */ + execute (trigger, ...args) { + const command = this.get(trigger) + if (!command) return + try { + command.execute(...args) + } catch (err) { + this._client.throwOrEmit('commander:commandError', err) + } + } + + /** + * Fires when an error occurs in Commander + * + * @event Client#commander:error + * @type {Error} + */ + + /** + * Fires when an error occurs in a command + * + * @event Client#commander:commandError + * @type {Error} + */ +} + +module.exports = Commander diff --git a/src/core/engine/Router.js b/src/core/engine/Router.js new file mode 100644 index 0000000..afcff9d --- /dev/null +++ b/src/core/engine/Router.js @@ -0,0 +1,170 @@ +const path = require('path') +const fs = require('fs') +const requireAll = require('require-all') + +const Collection = require('../../util/Collection') + +/** + * Router class for event routing + * @prop {Object} events Event map + */ +class Router extends Collection { + /** + * Creates a new Router instance + * @arg {Client} client Client instance + */ + constructor (client) { + super() + this._client = client + this.events = {} + } + + /** + * Class, object or function that can be utilised as a module + * @typedef {(Object|function)} AbstractModule + * @prop {String} name Module name + * @prop {Object} events Object mapping Eris event name to a function name + * @example + * // in constructor + * events: { + * messageCreate: 'onMessage' + * } + * + * // in class or object + * onMessage (msg) { + * // handle message + * } + */ + + /** + * Registers modules + * @param {String|Object|Array} modules An object, array or relative path to a folder or file to load modules from + * @returns {Client} + */ + register (modules) { + switch (typeof modules) { + case 'string': { + const filepath = path.join(__dirname, modules) + if (!fs.existsSync(filepath)) { + throw new Error(`Folder path ${filepath} does not exist`) + } + const mods = fs.statSync(filepath).isDirectory() ? requireAll(filepath) : require(filepath) + return this.registerModules(mods) + } + case 'object': { + if (Array.isArray(modules)) { + for (const module of modules) { + this.modules.attach(module) + } + return this + } + for (const key in modules) { + this.modules.attach(modules[key]) + } + return this + } + default: { + throw new Error('Path supplied is not an object or string') + } + } + } + + /** + * Attaches a module + * @arg {AbstractModule} Module Module class, object or function + * @returns {Router} + */ + attach (Module) { + const module = typeof Module === 'function' ? new Module(this._client) : Module + this.set(module.name, module) + for (const event in module.events) { + if (typeof this.events[event] === 'undefined') { + this.record(event) + } + + const listener = module.events[event] + if (typeof module[listener] !== 'function') { + this._client.throwOrEmit('router:error', new TypeError(`${listener} in ${module.name} is not a function`)) + return this + } + + this.events[event] = Object.assign(this.events[event] || {}, { [module.name]: listener }) + } + return this + } + + /** + * Registers an event + * @arg {String} event Event name + * @returns {Router} + */ + record (event) { + this.bot.on(event, (...args) => { + const events = this.events[event] || {} + for (const name in events) { + const module = this.get(name) + if (!module) continue + try { + module[events[name]](...args) + } catch (err) { + this._client.throwOrEmit('router:runError', err) + } + } + }) + return this + } + + /** + * Initialises all modules + * @returns {Router} + */ + initAll () { + this.forEach(module => { + if (typeof module.init === 'function') { + module.init() + } + }) + return this + } + + /** + * Unregisters all modules + * @returns {Router} + */ + unregister () { + return this.destroy() + } + + /** + * Destroys all modules and unloads them + * @returns {Router} + */ + destroy () { + for (const event in this.events) { + this.events[event] = {} + } + this.forEach(module => { + if (typeof module.unload === 'function') { + module.unload() + } + }) + this.clear() + return this + } + + /** + * Fires when an error occurs in Router + * + * @event Client#router:error + * @type {Error} + */ + + /** + * Fires when an error occurs in Router's event handling + * + * @event Client#router:runError + * @type {Error} + */ +} + +module.exports = Router diff --git a/src/core/engine/index.js b/src/core/engine/index.js new file mode 100644 index 0000000..6ba9739 --- /dev/null +++ b/src/core/engine/index.js @@ -0,0 +1,5 @@ +module.exports = { + Bridge: require('./Bridge'), + Commander: require('./Commander'), + Router: require('./Router') +} diff --git a/src/managers/Resolver.js b/src/managers/Resolver.js new file mode 100644 index 0000000..d1fac34 --- /dev/null +++ b/src/managers/Resolver.js @@ -0,0 +1,202 @@ +const { readdirRecursive } = require('../util') + +/** + * Resolver manager for resolving usages + * @prop {Object} resolvers Object with resolver functions + */ +class Resolver { + /** + * Creates a new Resolver instance + * @arg {Client} client Client instance + */ + constructor (client) { + this._client = client + this.resolvers = {} + } + + /** + * Resolver function + * @typedef {Object} ResolverObject + * @prop {String} type Resolver type + * @prop {Promise(Container)} resolve Promise that takes in {@link Container} as an argument and resolves a result + */ + + /** + * Loads {@link ResolverObject|ResolverObjects} from a file path + * @arg {String} path File path to load resolvers from + * @returns {Promise} + */ + loadResolvers (path) { + return readdirRecursive(path).then(resolvers => { + resolvers = resolvers.map(r => require(r)) + for (const name in resolvers) { + const resolver = resolvers[name] + if (!resolver.resolve || !resolver.type) continue + this._resolvers[resolver.type] = resolver + } + return resolvers + }) + } + + /** + * Loads a command usage locally + * @arg {CommandUsage} usage CommandUsage object to be loaded + */ + load (data) { + this.usage = this.verify(data) + } + + /** + * Verifies a {@link CommandUsage} + * @arg {CommandUsage} usage CommandUsage object to be verified + * @returns CommandUsage + */ + verify (usage) { + /** + * An object documenting the requirements of a command argument + * @namespace CommandUsage + * @type {Object} + */ + return (Array.isArray(usage) ? usage : [usage]).map(entry => { + /** + * The name of the argument + * @type {String} + * @memberof CommandUsage + * @name name + */ + if (!entry.name) { + throw new Error('Argument specified in usage has no name') + } + /** + * Single allowed argument type to be resolved + * @type {String} + * @memberof CommandUsage + * @name type + * @see {@link ResolverObject} + */ + + /** + * Multiple allowed argument types to be resolved + * @type {Array} + * @memberof CommandUsage + * @name types + * @see {@link ResolverObject} + */ + if (!entry.types) entry.types = [ entry.type || 'string' ] + + /** + * The display name of the argument, to be used when displaying argument info + * @type {?String} + * @memberof CommandUsage + * @name displayName + */ + if (!entry.displayName) entry.displayName = entry.name + return entry + }) + } + + /** + * Resolves a message + * @arg {external:"Eris.Message"} message Eris message + * @arg {String[]} args Array of strings, by default a message split by spaces, without command trigger and prefix + * @arg {Object} [data] Additional data + * @arg {String} [data.prefix] The client's prefix + * @arg {String} [data.command] The command trigger + * @arg {CommandUsage} [usage=this.usage] CommandUsage object + * @returns {Promise<*, ResolverError>} + */ + resolve (message, rawArgs, data = {}, rawUsage = this.usage) { + const usage = this.verify(usage) + if (!usage.length) return Promise.resolve() + + const argsCount = rawArgs.length + const requiredArgs = usage.filter(arg => !arg.optional).length + const optionalArgs = argsCount - requiredArgs + + // REDO + if (argsCount < requiredArgs) { + let msg = '{{%resolver.INSUFFICIENT_ARGS}}' + if (data.prefix && data.command) { + msg += `\n\n**{{%resolver.CORRECT_USAGE}}**: \`${data.prefix}${data.command} ` + (usage.length + ? usage.map(arg => arg.optional ? `[${arg.displayName}]` : `<${arg.displayName}>`).join(' ') + : '') + '`' + } + return Promise.reject({ + message: msg, + requiredArgs: `**${requiredArgs}**`, + argsCount: `**${argsCount}**.` + }) + } + + let args = {} + let idx = 0 + let optArgs = 0 + let resolves = [] + let skip = false + for (const arg of usage) { + let rawArg + if (arg.last) { + rawArg = rawArgs.slice(idx).join(' ') + skip = true + } else { + if (arg.optional) { + if (optionalArgs > optArgs) { + optArgs++ + } else { + if (arg.default) args[arg.name] = arg.default + continue + } + } + rawArg = rawArgs[idx] + if (typeof rawArg !== 'undefined') { + if (rawArg.startsWith('"')) { + const endQuote = rawArgs.findIndex((str, i) => str.endsWith('"') && i >= idx) + if (endQuote > -1) { + rawArg = rawArgs.slice(idx, endQuote + 1).join(' ').replace(/"/g, '') + idx = endQuote + } else { + return Promise.reject('{{%resolver.NO_END_QUOTE}}') + } + } + } + idx++ + } + resolves.push( + Promise.all(arg.types.map(type => { + const resolver = this._resolvers[type] + if (typeof resolver === 'undefined') { + return Promise.reject({ err: 'Invalid resolver type' }) + } + return resolver.resolve(rawArg, arg, message, this.bot) + .catch(err => Object.assign(arg, { + arg: `**\`${arg.name || 'argument'}\`**`, + err: err.message ? err.message : `{{%resolver.${err}}}` + + (data.prefix && data.command + ? `\n\n**{{%resolver.CORRECT_USAGE}}**: \`${data.prefix}${data.command} ` + + (usage.length ? usage.map(arg => + skip ? arg.displayName + : (arg.optional ? `[${arg.displayName}]` : `<${arg.displayName}>`) + ).join(' ') : '') + '`' + : '') + })) + })).then(results => { + const resolved = results.filter(v => !v.err) + + if (resolved.length) { + const res = resolved.length === 1 + ? resolved[0] + : resolved.reduce((p, c) => p.concat(c), []) + args[arg.name] = res + return res + } + + return Promise.reject(results[0]) + }) + ) + if (skip) break + } + return Promise.all(resolves).then(() => args) + } +} + +module.exports = Resolver diff --git a/src/managers/Transmitter.js b/src/managers/Transmitter.js new file mode 100644 index 0000000..0537afc --- /dev/null +++ b/src/managers/Transmitter.js @@ -0,0 +1,117 @@ +const crypto = require('crypto') + +const Collection = require('../util/Collection') + +/** + * Manager class for inter-process communication + * @prop {String} pid Process ID + * @prop {Map} commands IPC commands + */ +class Transmitter extends Collection { + /** + * Creates a new Transmitter instance + * @arg {Client} client Client instance + */ + constructor (client) { + super() + + this.pid = process.pid + this._client = client + + process.on('message', this.onMessage.bind(this)) + } + + /** + * Sends a message to the master IPC process + * @arg {String} event Event name + * @arg {*} data Attached data + */ + send (event, data) { + process.send({ + op: event, + d: data + }) + } + + onMessage (message) { + if (!message.op) { + if (!this._client.suppressWarnings) { + this._logger.warn('Received IPC message with no op') + } + return + } + + if (['resp', 'broadcast'].includes(message.op)) return + + if (this[message.op]) { + return this[message.op](message) + } + + const command = this.get(message.op) + if (command) { + return command(message, this._bot) + } + } + + /** + * Awaits for a certain response + * @arg {String} op op code + * @arg {*} d Attached data + * @returns {Promise<*>} + */ + async awaitResponse (op, d) { + const code = crypto.randomBytes(64).toString('hex') + return new Promise((resolve, reject) => { + const awaitListener = (msg) => { + if (!['resp', 'error'].includes(msg.op)) return + process.removeListener('message', awaitListener) + if (msg.op === 'resp' && msg.code === code) return resolve(msg.d) + if (msg.op === 'error') return reject(msg.d) + } + + const payload = { op, code } + if (d) payload.d = d + + process.on('message', awaitListener) + process.send(payload) + + setTimeout(() => { + process.removeListener('message', awaitListener) + return reject('IPC timed out after 2000ms') + }, 2000) + }) + } + + /** + * Registers an IPC command + * @arg {function} command Command function + * @arg {String} command.name Name of the IPC command + * @returns {Transmitter} + */ + register (command) { + if (!command || !command.name) { + this._client.throwOrEmit('ipc:error', new TypeError(`Invalid command - ${command}`)) + return + } + this.set(command.name, command) + return this + } + + /** + * Unregisters an IPC command by name + * @arg {String} name Name of the IPC command + * @returns {Transmitter} + */ + unregister (name) { + this.delete(name) + return this + } + + /** + * Fires when an error occurs in Transmitter + * @event Client#ipc:error + * @type {Error} + */ +} + +module.exports = Transmitter diff --git a/src/util/Collection.js b/src/util/Collection.js new file mode 100644 index 0000000..47a326a --- /dev/null +++ b/src/util/Collection.js @@ -0,0 +1,94 @@ +/** + * An extended map with utility functions + * @extends Map + */ +class Collection extends Map { + /** + * Returns all items in the collection as an array + * @returns {Array} Array of values + */ + toArray () { + return [...this.values()] + } + + /** + * Executes a function on all values + * @param {function} func forEach function + */ + forEach (...args) { + return this.toArray().forEach(...args) + } + + /** + * Filter values by function + * @param {function} func filter function + * @returns {Array} Array of filtered values + */ + filter (...args) { + return this.toArray().filter(...args) + } + + /** + * Find values by function + * @param {function} func find function + * @returns {*} Value that was found + */ + find (...args) { + return this.toArray().find(...args) + } + + /** + * Map values by function + * @param {function} func map function + * @returns {Array} Array of mapped values + */ + map (...args) { + return this.toArray().map(...args) + } + + /** + * Reduce values by function + * @param {function} func reduce function + * @returns {Array} Array of reduced values + */ + reduce (...args) { + return this.toArray().reduce(...args) + } + + /** + * Pluck values with key by function + * @param {String} key The matching key + * @returns {Array} Array of keyed values + */ + pluck (key) { + return this.toArray().reduce((i, o) => { + if (!o[key]) return i + i.push(o[key]) + return i + }, []) + } + + /** + * Group values by key + * @param {String} key The matching key + * @returns {Object} Object containing grouped values + */ + groupBy (key) { + return this.toArray().reduce((i, o) => { + let val = o[key] + i[val] = i[val] || [] + i[val].push(o) + return i + }, {}) + } + + /** + * Get unique values + * @returns {Array} unique Array of unique values + */ + unique () { + return [...new Set(this.toArray())] + } +} + +module.exports = Collection diff --git a/src/util/Utils.js b/src/util/Utils.js new file mode 100644 index 0000000..0b22ec7 --- /dev/null +++ b/src/util/Utils.js @@ -0,0 +1,73 @@ +const path = require('path') +const fs = require('fs') + +/** + * Utility class + */ +class Utils { + /** + * Creates a new Utils instance + * @arg {Client} client Client instance + */ + constructor (client) { + this._client = client + } + + /** + * Reads a directory recursively and returns an array of paths + * @arg {...String} paths Folder path(s) + */ + static async readdirRecursive (...paths) { + const dir = path.join(...paths) + let list = [] + if (!fs.existsSync(dir)) return list + let files = fs.readdirSync(dir) + let dirs + + dirs = files.filter(this.constructor.isDir) + files = files.filter(file => !Utils.isDir(this.constructor)) + .map(file => path.join(dir, file)).filter(file => !path.basename(file).startsWith('.')) + list = list.concat(files) + + while (dirs.length) { + let d = path.join(dir, dirs.shift()) + list = list.concat(await this.constructor.readdirRecursive(d)) + } + + return list + } + + /** + * Checks if a path is a directory + * @arg {String} filename The path to check + */ + static isDir (fname) { + return fs.existsSync(fname) ? fs.statSync(fname).isDirectory() : false + } + + /** + * Pads a string on the right if it's shorter than the padding length + * @arg {String} [String=''] The string to pad + * @arg {Number} [length=0] The padding length + * @arg {String} [chars=' '] The string used as padding + */ + static padEnd (string = '', len = 0, chars = ' ') { + const str = String(string) + const pad = String(chars) + return str.length >= len ? '' + str : str + pad.repeat(len - str.length) + } + + /** + * Pads a string on the left if it's shorter than the padding length + * @arg {String} [String=''] The string to pad + * @arg {Number} [length=0] The padding length + * @arg {String} [chars=' '] The string used as padding + */ + static padStart (string = '', len = 0, chars = ' ') { + const str = String(string) + const pad = String(chars) + return str.length >= len ? '' + str : (pad.repeat(len) + str).slice(-len) + } +} + +module.exports = Utils diff --git a/src/util/index.js b/src/util/index.js new file mode 100644 index 0000000..a18d12a --- /dev/null +++ b/src/util/index.js @@ -0,0 +1,3 @@ +module.exports = Object.assign(require('./Utils'), { + Collection: require('./Collection') +}) From 40bcfe22da27d28b41eacf99457c8f86f49d3f2e Mon Sep 17 00:00:00 2001 From: pyraxo Date: Thu, 29 Dec 2016 21:06:34 +0800 Subject: [PATCH 03/21] Add yarn file, more utils, license in readme, externals in docs --- README.md | 13 + src/core/Client.js | 42 +++- src/util/Utils.js | 44 ++++ yarn.lock | 573 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 660 insertions(+), 12 deletions(-) create mode 100644 yarn.lock diff --git a/README.md b/README.md index 0dbbd69..587318d 100644 --- a/README.md +++ b/README.md @@ -43,3 +43,16 @@ client.run() ### Configuration As the bot framework extends the [Eris](https://github.com/abalabahaha/Eris) client, please refer to the docs [here](https://abal.moe/Eris/docs). + +### License +Copyright (C) 2017 Pyraxo + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. diff --git a/src/core/Client.js b/src/core/Client.js index b3ac0b1..ae1f816 100644 --- a/src/core/Client.js +++ b/src/core/Client.js @@ -3,18 +3,6 @@ const Eris = require('eris').client const { Commander, Router, Bridge } = require('./core/engine') const { Collection } = require('./util') -/** - * The Eris client - * @external "Eris.Client" - * @see {@link https://abal.moe/Eris/docs/Client|Eris.Client} - */ - -/** - * The Eris message object - * @external "Eris.Message" - * @see {@link https://abal.moe/Eris/docs/Message|Eris.Message} - */ - /** * Interface between the Discord client and plugins * @version 3.0.0 @@ -125,3 +113,33 @@ class Client extends Eris { } module.exports = Client + +/** + * The Eris client + * @external "Eris.Client" + * @see {@link https://abal.moe/Eris/docs/Client|Eris.Client} + */ + +/** + * The Eris message object + * @external "Eris.Message" + * @see {@link https://abal.moe/Eris/docs/Message|Eris.Message} + */ + +/** + * The Eris guild object + * @external "Eris.Guild" + * @see {@link https://abal.moe/Eris/docs/Guild|Eris.Guild} + */ + +/** + * The Eris role object + * @external "Eris.Role" + * @see {@link https://abal.moe/Eris/docs/Role|Eris.Role} + */ + +/** + * The Eris member object + * @external "Eris.Member" + * @see {@link https://abal.moe/Eris/docs/Member|Eris.Member} + */ diff --git a/src/util/Utils.js b/src/util/Utils.js index 0b22ec7..82c896c 100644 --- a/src/util/Utils.js +++ b/src/util/Utils.js @@ -1,6 +1,17 @@ const path = require('path') const fs = require('fs') +const colours = { + blue: '#117ea6', + green: '#1f8b4c', + red: '#be2626', + pink: '#E33C96', + gold: '#d5a500', + silver: '#b7b7b7', + bronze: '#a17419', + orange: '#c96941' +} + /** * Utility class */ @@ -68,6 +79,39 @@ class Utils { const pad = String(chars) return str.length >= len ? '' + str : (pad.repeat(len) + str).slice(-len) } + + /** + * Gets the integer of a colour + * @arg {String} colour Hex colour code or name of colour + * @returns {?Number} + */ + static getColour (colour) { + if (!colours[colour]) return + return this.constructor.parseInt(String(colours[colour] || colour).replace('#', ''), 16) || null + } + + /** + * Format a number with grouped thousands + * @arg {Number} num Number to Format + * @returns {String} + */ + static parseNumber (num) { + return String(num).replace(/\B(?=(\d{3})+(?!\d))/g, ',') + } + + /** + * Determines if a member has a role that is higher than the given role + * @arg {external:"Eris.Member"} member Member to check + * @arg {external:"Eris.Role"} role Role to check + * @returns {Boolean} + */ + static hasRoleHierarchy (member, role) { + const guild = member.guild + return guild && member.roles.some(id => { + const r = guild.roles.get(id) + return r.position > role.position + }) + } } module.exports = Utils diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..cc8ea45 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,573 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 +acorn-jsx@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b" + dependencies: + acorn "^3.0.4" + +acorn@^3.0.4, acorn@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" + +ansi-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.0.0.tgz#c5061b6e0ef8a81775e50f5d66151bf6bf371107" + +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + +asap@~2.0.3: + version "2.0.5" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.5.tgz#522765b50c3510490e52d7dcfe085ef9ba96958f" + +async@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" + +async@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/async/-/async-1.0.0.tgz#f8fc04ca3a13784ade9e1641af98578cfbd647a9" + +bindings@~1.2.1, bindings@1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.2.1.tgz#14ad6113812d2d37d72e67b4cacb4bb726505f11" + +bluebird@^3.4.7, "bluebird@>= 3.0.1", bluebird@~3.4.6: + version "3.4.7" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3" + +bluebird@~2.10.2: + version "2.10.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.10.2.tgz#024a5517295308857f14f91f1106fc3b555f446b" + +buffer-shims@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-shims/-/buffer-shims-1.0.0.tgz#9978ce317388c649ad8793028c3477ef044a8b51" + +catharsis@~0.8.8: + version "0.8.8" + resolved "https://registry.yarnpkg.com/catharsis/-/catharsis-0.8.8.tgz#693479f43aac549d806bd73e924cd0d944951a06" + dependencies: + underscore-contrib "~0.3.0" + +chalk@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + +cluster@^0.7.7: + version "0.7.7" + resolved "https://registry.yarnpkg.com/cluster/-/cluster-0.7.7.tgz#e497e267cc956bd0b0513adb4aa393357d0085ef" + dependencies: + log ">= 1.2.0" + mkdirp ">= 0.0.1" + +colors@1.0.x: + version "1.0.3" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" + +combined-stream@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" + dependencies: + delayed-stream "~1.0.0" + +commander@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-0.6.1.tgz#fa68a14f6a945d54dbbe50d8cdb3320e9e3b1a06" + +commander@2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.3.0.tgz#fd430e889832ec353b9acd1de217c11cb3eef873" + +component-emitter@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" + +cookiejar@^2.0.6: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.0.tgz#86549689539b6d0e269b6637a304be508194d898" + +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" + +cycle@1.0.x: + version "1.0.3" + resolved "https://registry.yarnpkg.com/cycle/-/cycle-1.0.3.tgz#21e80b2be8580f98b468f379430662b046c34ad2" + +debug@^2.2.0, debug@2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.5.2.tgz#50c295a53dbf1657146e0c1b21307275e90d49cb" + dependencies: + ms "0.7.2" + +debug@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" + dependencies: + ms "0.7.1" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + +diff@1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-1.4.0.tgz#7f28d2eb9ee7b15a97efd89ce63dcfdaa3ccbabf" + +docdash@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/docdash/-/docdash-0.4.0.tgz#05c3a50d83189981699ee0c076d3a3950db7ec00" + +double-ended-queue@^2.1.0-0: + version "2.1.0-0" + resolved "https://registry.yarnpkg.com/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz#103d3527fd31528f40188130c841efdd78264e5c" + +"eris@github:abalabahaha/eris#dev": + version "0.5.1" + resolved "https://codeload.github.com/abalabahaha/eris/tar.gz/ef1e706d2f7ce9f9bd12b7f36786e0825ee04f07" + dependencies: + ws "^1.1.1" + optionalDependencies: + opusscript "0.0.1" + tweetnacl "^0.14.3" + +escape-string-regexp@^1.0.2, 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" + +escape-string-regexp@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.2.tgz#4dbc2fe674e71949caf3fb2695ce7f2dc1d9a8d1" + +espree@~3.1.7: + version "3.1.7" + resolved "https://registry.yarnpkg.com/espree/-/espree-3.1.7.tgz#fd5deec76a97a5120a9cd3a7cb1177a0923b11d2" + dependencies: + acorn "^3.3.0" + acorn-jsx "^3.0.0" + +eventemitter3@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-2.0.2.tgz#20ce4891909ce9f35b088c94fab40e2c96f473ac" + +extend@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.0.tgz#5a474353b9f3353ddd8176dfd37b91c83a46f1d4" + +eyes@0.1.x: + version "0.1.8" + resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0" + +form-data@1.0.0-rc4: + version "1.0.0-rc4" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-1.0.0-rc4.tgz#05ac6bc22227b43e4461f488161554699d4f8b5e" + dependencies: + async "^1.5.2" + combined-stream "^1.0.5" + mime-types "^2.1.10" + +formidable@^1.0.17: + version "1.0.17" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.0.17.tgz#ef5491490f9433b705faa77249c99029ae348559" + +glob@3.2.11: + version "3.2.11" + resolved "https://registry.yarnpkg.com/glob/-/glob-3.2.11.tgz#4a973f635b9190f715d10987d5c00fd2815ebe3d" + dependencies: + inherits "2" + minimatch "0.3" + +graceful-fs@^4.1.9: + version "4.1.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" + +growl@1.9.2: + version "1.9.2" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.9.2.tgz#0ea7743715db8d8de2c5ede1775e1b45ac85c02f" + +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + dependencies: + ansi-regex "^2.0.0" + +inherits@~2.0.1, inherits@2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + +isstream@0.1.x: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + +jade@0.26.3: + version "0.26.3" + resolved "https://registry.yarnpkg.com/jade/-/jade-0.26.3.tgz#8f10d7977d8d79f2f6ff862a81b0513ccb25686c" + dependencies: + commander "0.6.1" + mkdirp "0.3.0" + +js2xmlparser@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/js2xmlparser/-/js2xmlparser-1.0.0.tgz#5a170f2e8d6476ce45405e04823242513782fe30" + +jsdoc-strip-async-await@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/jsdoc-strip-async-await/-/jsdoc-strip-async-await-0.1.0.tgz#7a755a2b7527da2c76e2163a455636183456152a" + +jsdoc@^3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/jsdoc/-/jsdoc-3.4.3.tgz#e5740d6145c681f6679e6c17783a88dbdd97ccd3" + dependencies: + bluebird "~3.4.6" + catharsis "~0.8.8" + escape-string-regexp "~1.0.5" + espree "~3.1.7" + js2xmlparser "~1.0.0" + klaw "~1.3.0" + marked "~0.3.6" + mkdirp "~0.5.1" + requizzle "~0.2.1" + strip-json-comments "~2.0.1" + taffydb "2.6.2" + underscore "~1.8.3" + +klaw@~1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439" + optionalDependencies: + graceful-fs "^4.1.9" + +"log@>= 1.2.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/log/-/log-1.4.0.tgz#4ba1d890fde249b031dca03bc37eaaf325656f1c" + +lru-cache@2: + version "2.7.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952" + +marked@~0.3.6: + version "0.3.6" + resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.6.tgz#b2c6c618fccece4ef86c4fc6cb8a7cbf5aeda8d7" + +methods@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + +mime-db@~1.25.0: + version "1.25.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.25.0.tgz#c18dbd7c73a5dbf6f44a024dc0d165a1e7b1c392" + +mime-types@^2.1.10: + version "2.1.13" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.13.tgz#e07aaa9c6c6b9a7ca3012c69003ad25a39e92a88" + dependencies: + mime-db "~1.25.0" + +mime@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" + +minimatch@0.3: + version "0.3.0" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-0.3.0.tgz#275d8edaac4f1bb3326472089e7949c8394699dd" + dependencies: + lru-cache "2" + sigmund "~1.0.0" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + +"mkdirp@>= 0.0.1", mkdirp@~0.5.1, mkdirp@0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + dependencies: + minimist "0.0.8" + +mkdirp@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.0.tgz#1bbf5ab1ba827af23575143490426455f481fe1e" + +mocha@^2.2.5: + version "2.5.3" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-2.5.3.tgz#161be5bdeb496771eb9b35745050b622b5aefc58" + dependencies: + commander "2.3.0" + debug "2.2.0" + diff "1.4.0" + escape-string-regexp "1.0.2" + glob "3.2.11" + growl "1.9.2" + jade "0.26.3" + mkdirp "0.5.1" + supports-color "1.2.0" + to-iso-string "0.0.2" + +moment-timezone@^0.5.5: + version "0.5.11" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.11.tgz#9b76c03d8ef514c7e4249a7bbce649eed39ef29f" + dependencies: + moment ">= 2.6.0" + +moment@^2.13.0, "moment@>= 2.6.0": + version "2.17.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.17.1.tgz#fed9506063f36b10f066c8b59a144d7faebe1d82" + +ms@0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" + +ms@0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765" + +nan@^2.3.2, nan@2: + version "2.5.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.5.0.tgz#aa8f1e34531d807e9e27755b234b4a6ec0c152a8" + +node-opus@^0.2.4: + version "0.2.4" + resolved "https://registry.yarnpkg.com/node-opus/-/node-opus-0.2.4.tgz#79fb35cf0e7ad04cfb9398eb362aba0774e30a61" + dependencies: + bindings "~1.2.1" + nan "^2.3.2" + ogg-packet "^1.0.0" + +ogg-packet@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/ogg-packet/-/ogg-packet-1.0.0.tgz#45b885721ac8f7dd5cf22391d42106ae533ac678" + dependencies: + ref-struct "*" + +options@>=0.0.5: + version "0.0.6" + resolved "https://registry.yarnpkg.com/options/-/options-0.0.6.tgz#ec22d312806bb53e731773e7cdaefcf1c643128f" + +opusscript@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/opusscript/-/opusscript-0.0.1.tgz#c8f61d4301b2942ee9ddf68b075b3e373b7943dd" + +pkginfo@0.3.x: + version "0.3.1" + resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.3.1.tgz#5b29f6a81f70717142e09e765bbeab97b4f81e21" + +process-nextick-args@~1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" + +promise@^7.0.3: + version "7.1.1" + resolved "https://registry.yarnpkg.com/promise/-/promise-7.1.1.tgz#489654c692616b8aa55b0724fa809bb7db49c5bf" + dependencies: + asap "~2.0.3" + +qs@^6.1.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.0.tgz#f403b264f23bc01228c74131b407f18d5ea5d442" + +readable-stream@^2.0.5: + version "2.2.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.2.tgz#a9e6fec3c7dda85f8bb1b3ba7028604556fc825e" + dependencies: + buffer-shims "^1.0.0" + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "~1.0.0" + process-nextick-args "~1.0.6" + string_decoder "~0.10.x" + util-deprecate "~1.0.1" + +redis-commands@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.3.0.tgz#4307d8094aee1315829ab6729b37b99f62365d63" + +redis-parser@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-2.3.0.tgz#313a47965e49ee35ab3a86c93388b403d76237f6" + +redis@^2.6.1: + version "2.6.3" + resolved "https://registry.yarnpkg.com/redis/-/redis-2.6.3.tgz#84305b92553c6a1f09c7c47c30b11ace7dbb7ad4" + dependencies: + double-ended-queue "^2.1.0-0" + redis-commands "^1.2.0" + redis-parser "^2.0.0" + +ref-struct@*: + version "1.1.0" + resolved "https://registry.yarnpkg.com/ref-struct/-/ref-struct-1.1.0.tgz#5d5ee65ad41cefc3a5c5feb40587261e479edc13" + dependencies: + debug "2" + ref "1" + +ref@1: + version "1.3.3" + resolved "https://registry.yarnpkg.com/ref/-/ref-1.3.3.tgz#116d1ef64f2bc56d9e54a648cea7332a36b81932" + dependencies: + bindings "1" + debug "2" + nan "2" + +require-all@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/require-all/-/require-all-2.1.0.tgz#109e1c3dab8a5acab2312f552d5e8d27d8de9f77" + +requizzle@~0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/requizzle/-/requizzle-0.2.1.tgz#6943c3530c4d9a7e46f1cddd51c158fc670cdbde" + dependencies: + underscore "~1.6.0" + +rethinkdbdash@~2.3.0: + version "2.3.27" + resolved "https://registry.yarnpkg.com/rethinkdbdash/-/rethinkdbdash-2.3.27.tgz#66de1c6cf13ed89db0c81ee0f060656caddfe1a5" + dependencies: + bluebird ">= 3.0.1" + +sigmund@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590" + +stack-trace@0.0.x: + version "0.0.9" + resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.9.tgz#a8f6eaeca90674c333e7c43953f275b451510695" + +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + +strip-ansi@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + dependencies: + ansi-regex "^2.0.0" + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + +superagent@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-2.3.0.tgz#703529a0714e57e123959ddefbce193b2e50d115" + dependencies: + component-emitter "^1.2.0" + cookiejar "^2.0.6" + debug "^2.2.0" + extend "^3.0.0" + form-data "1.0.0-rc4" + formidable "^1.0.17" + methods "^1.1.1" + mime "^1.3.4" + qs "^6.1.0" + readable-stream "^2.0.5" + +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + +supports-color@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-1.2.0.tgz#ff1ed1e61169d06b3cf2d588e188b18d8847e17e" + +taffydb@2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/taffydb/-/taffydb-2.6.2.tgz#7cbcb64b5a141b6a2efc2c5d2c67b4e150b2a268" + +thinky@^2.3.7: + version "2.3.8" + resolved "https://registry.yarnpkg.com/thinky/-/thinky-2.3.8.tgz#4d3d01fe0aaaa8cd97276965f40dbb4f017300f3" + dependencies: + bluebird "~2.10.2" + rethinkdbdash "~2.3.0" + validator "~3.22.1" + +to-iso-string@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/to-iso-string/-/to-iso-string-0.0.2.tgz#4dc19e664dfccbe25bd8db508b00c6da158255d1" + +tweetnacl@^0.14.3: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + +ultron@1.0.x: + version "1.0.2" + resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.0.2.tgz#ace116ab557cd197386a4e88f4685378c8b2e4fa" + +underscore-contrib@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/underscore-contrib/-/underscore-contrib-0.3.0.tgz#665b66c24783f8fa2b18c9f8cbb0e2c7d48c26c7" + dependencies: + underscore "1.6.0" + +underscore@~1.6.0, underscore@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.6.0.tgz#8b38b10cacdef63337b8b24e4ff86d45aea529a8" + +underscore@~1.8.3: + version "1.8.3" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022" + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + +validator@~3.22.1: + version "3.22.2" + resolved "https://registry.yarnpkg.com/validator/-/validator-3.22.2.tgz#6f297ae67f7f82acc76d0afdb49f18d9a09c18c0" + +winston-cluster@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/winston-cluster/-/winston-cluster-0.0.4.tgz#640647d0d1e24cde19c215ff9f03d87cb969c40e" + dependencies: + cluster "^0.7.7" + mocha "^2.2.5" + promise "^7.0.3" + winston "^1.0.0" + +winston-daily-rotate-file@^1.3.1: + version "1.4.0" + resolved "https://registry.yarnpkg.com/winston-daily-rotate-file/-/winston-daily-rotate-file-1.4.0.tgz#71052f4c372ba7c5ae163834c5b043edd0c06be0" + +winston@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/winston/-/winston-1.1.2.tgz#68edd769ff79d4f9528cf0e5d80021aade67480c" + dependencies: + async "~1.0.0" + colors "1.0.x" + cycle "1.0.x" + eyes "0.1.x" + isstream "0.1.x" + pkginfo "0.3.x" + stack-trace "0.0.x" + +winston@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/winston/-/winston-2.3.0.tgz#207faaab6fccf3fe493743dd2b03dbafc7ceb78c" + dependencies: + async "~1.0.0" + colors "1.0.x" + cycle "1.0.x" + eyes "0.1.x" + isstream "0.1.x" + stack-trace "0.0.x" + +ws@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-1.1.1.tgz#082ddb6c641e85d4bb451f03d52f06eabdb1f018" + dependencies: + options ">=0.0.5" + ultron "1.0.x" + From 2a72ab10c2d291b9e3a924eeb2eb14aa3edd4a4b Mon Sep 17 00:00:00 2001 From: pyraxo Date: Fri, 30 Dec 2016 00:20:15 +0800 Subject: [PATCH 04/21] Write docs, gulpfile, readme, package.json --- .gitignore | 35 +-------- README.md | 195 +++++++++++++++++++++++++++++++++++++++++++++++++-- gulpfile.js | 14 ++++ package.json | 25 ++++--- 4 files changed, 218 insertions(+), 51 deletions(-) create mode 100644 gulpfile.js diff --git a/.gitignore b/.gitignore index 575ea31..11d6bb2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,42 +1,9 @@ -# Logs logs *.log npm-debug.log* -# Runtime data -pids -*.pid -*.seed - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (http://nodejs.org/api/addons.html) -build/Release - -# Dependency directory -# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git node_modules - -# Optional npm cache directory .npm -# Optional REPL history -.node_repl_history - -# Assets -resources -.env -.cache - -# Documentation +assets docs diff --git a/README.md b/README.md index 587318d..c601a77 100644 --- a/README.md +++ b/README.md @@ -18,19 +18,23 @@ * [Tatsumaki](https://tatsumaki.xyz), a multi-purpose social Discord bot * [haru](https://pyraxo.moe/haru), everyone's favourite idol and part-time bot -**iris** is an advanced, efficient and highly customisable base for Discord command bots written in Node.js +**iris** is an advanced, efficient and highly customisable framework for Discord command bots written in Node.js ### Requirements -* **Node.js 7+** +* **Node.js 6+** -A firm grasp of **ES6 + async/await** syntax is recommended. +A firm grasp of **ES6 + async/await** syntax is optional but recommended. + +As the bot framework extends the [Eris](https://github.com/abalabahaha/Eris) client, please refer to the docs [here](https://abal.moe/Eris/docs). ### Usage ```bash $ npm install --save pyraxo/iris ``` -#### Quick Example +If you don't want to use any built-in plugins, you may run the above command with the `--no-optional` flag. + +#### Example ```js const Bot = require('iris') @@ -38,11 +42,190 @@ const client = new Bot({ token: 'your token here' }) +client.register('commands', 'path/to/commands') + client.run() ``` -### Configuration -As the bot framework extends the [Eris](https://github.com/abalabahaha/Eris) client, please refer to the docs [here](https://abal.moe/Eris/docs). +### Plugins +The framework consists of plugins that extend the functionality of the Discord client. + +Plugins can be added via external modules; several have already been included in the framework. + +#### Commander +The **Commander** plugin allows **Commands** to be executed. When a successful command is called, the Commander will execute the corresponding command functions. + +Example usage: +```js +const filepath = 'path/to/commands' +class PingCommand extends Command { + constructor (...args) { + super(...args, { + name: 'ping', + options: { guildOnly: true } + }) + } + + handle ({ msg }) { + return msg.reply('Pong!') + } +} +const commands = [ + PingCommand, + { + name: 'powerup', + execute: async ({ msg, client }) => { + const channel = await client.getChannel('247727924889911297') + return msg.reply(channel ? 'Powered up!' : 'Unpowered...') + } + } +] + +client +.register('commands', commands) +.register('commands', filepath) +``` + +#### Router +The **Router** plugin takes care of **Modules** by routing event arguments to the corresponding modules' methods. + +Example usage: +```js +class BanModule extends Module { + constructor (...args) { + name: 'guilds:bans', + events: { + guildBanAdd: 'onBan' + } + } + + onBan (guild, user) { + console.log(`User ${user.username} has been banned from ${guild.name}`) + } +} +const modules = [ + BanModule, + { + name: 'guilds:logger', + events: { + guildCreate: 'newGuild' + }, + newGuild: (guild) => console.log(`New guild: ${guild.name}`) + } +] + +client.register('modules', modules) +``` + +#### Bridge +The **Bridge** plugin maintains a chain of **Middleware**, passing messages through each middleware function and resolving a **Container** object. + +Example usage: +```js +client.register('middleware', [{ + name: 'checkPrivate', + priority: 1, + process: (container) => { + container.isPrivate = !!msg.guild + return Promise.resolve(container) + } +}]) +``` + +#### Registering Components into Built-in Plugins +As shown in the examples above, to fully utilise the plugins, the components have to be registered into the appropriate plugin with the `register()` method. + +The 1st argument should take in the plugin type, while the 2nd argument should **not** receive the raw component, but instead should be an array or object containing the components. + +```js +// Correct: +client +.register('commands', [ SomeCommand ]) +.register('commands', { + core: { + 'ping': PingCommand, + 'help': HelpCommand + } +}) + +// Incorrect: +client.register('commands', SomeCommand) +``` + +#### Custom Plugins +Custom plugin support is available as long as the added plugins follow certain criteria: +* Must be a class +* Must contain the `register()` and `unregister()` methods, to which any number of arguments can be passed + +To add a custom plugin: +```js +client.createPlugin('pluginType', PluginClass) +``` + +To access a plugin: +```js +const plugin = client.plugins.get('pluginType') +``` + +The plugin's `register` method will be called when `client.register()` is called with the first argument matching the corresponding plugin type. + +### Components +Plugins handle certain components that players can choose to add. For example, the **Commander** plugin makes use of **Commands**, which could be a class, function or object depending on the user's preference. + +**All built-in components have a built-in utility class containing various special methods. Do read the docs.** + +#### Commands +Text commands are specially formatted messages that Discord users can send. Bots will then carry out tasks according to the issued command. Most commands follow this format: + +``` + +e.g. !help ping +``` + +Each **Command** component corresponds to a text command. Commands can be a class, function or object. + +Commands must include: +* `triggers` - An array of triggers, where the first is the main trigger while the rest are aliases +* `execute` - A function that should accept a **Container** object as the first argument +* `group` - An *optional* string referencing the group of the command + +#### Modules +**Modules** are classes or objects containing methods that listen to specific events the client emits. + +Modules must include: +* `name` - A string containing the name of the middleware +* `events` - An object mapping an event name to a function name + +Example module object: +```js +{ + name: 'guilds:logger', + events: { + guildCreate: 'newGuild', + guildDelete: 'delGuild' + }, + newGuild: (guild) => console.log(`New guild: ${guild.name}`), + delGuild: (guild) => console.log(`Deleted guild: ${guild.name}`) +} +``` + +#### Middleware +**Middleware** are objects that receive a **Container** object, insert or modify its elements, and resolve the container. Middleware are chained together and executed according to their priority number. + +Middleware must include: +* `name` - A string containing the name of the middleware +* `priority` - A number referencing the priority to run the middleware. A lower number means it will be run earlier. +* `process` - A function that should a **Container** object as the first argument. + +#### Container +**Containers** are special objects containing properties that are passed around as a context. Among the default plugins, the **Commander** and **Bridge** plugins use them, and the **Commands** and **Middleware** components will be passed a **Container** as an argument, where they can modify and add properties to the container. + +A default unmodified container contains: +* `msg` - A message object +* `client` - The client instance +* `commands` - A **Commander** instance +* `modules` - A **Router** instance +* `middleware` - A **Bridge** instance ### License Copyright (C) 2017 Pyraxo diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..53606e2 --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,14 @@ +const gulp = require('gulp') +const babel = require('gulp-babel') + +const paths = ['src/**/*.js'] + +gulp.task('default', ['babel']) + +gulp.task('babel', () => { + gulp.src(paths) + .pipe(babel({ + plugins: ['transform-async-to-generator'] + })) + .pipe(gulp.dest('build')) +}) diff --git a/package.json b/package.json index f2a240f..18e3990 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,11 @@ { "name": "iris", "version": "3.0.0", - "description": "The better Discord bot base", - "main": "index.js", + "description": "The better Discord bot framework", + "main": "build/index.js", "scripts": { - "docs": "node_modules/.bin/jsdoc --configure .jsdoc.json --verbose" + "docs": "node_modules/.bin/jsdoc --configure .jsdoc.json --verbose", + "prepublish": "gulp" }, "engines": { "node": ">=7.0.0" @@ -19,23 +20,25 @@ "author": "pyraxo (https://pyraxo.moe)", "license": "AGPL-3.0", "dependencies": { - "bluebird": "^3.4.7", - "eventemitter3": "^2.0.2", - "chalk": "^1.1.3", - "eris": "github:abalabahaha/eris#dev", - "moment": "^2.13.0", - "moment-timezone": "^0.5.5", - "require-all": "^2.0.0", - "superagent": "^2.2.0" + "eris": "github:abalabahaha/eris#dev" }, "devDependencies": { + "babel-plugin-transform-async-to-generator": "^6.16.0", "docdash": "^0.4.0", + "gulp": "^3.9.1", + "gulp-babel": "^6.1.2", "jsdoc": "^3.4.3", "jsdoc-strip-async-await": "^0.1.0" }, "optionalDependencies": { + "bluebird": "^3.4.7", + "chalk": "^1.1.3", + "eventemitter3": "^2.0.2", + "moment": "^2.13.0", + "moment-timezone": "^0.5.5", "node-opus": "^0.2.4", "redis": "^2.6.1", + "superagent": "^2.2.0", "thinky": "^2.3.7", "winston": "^2.2.0", "winston-cluster": "0.0.4", From a7e74f96b1d798e028a72afad51274249e11fd8b Mon Sep 17 00:00:00 2001 From: pyraxo Date: Fri, 30 Dec 2016 00:20:59 +0800 Subject: [PATCH 05/21] Remove node-require-all dependency --- src/core/engine/Bridge.js | 7 ++--- src/core/engine/Commander.js | 4 +-- src/core/engine/Router.js | 4 +-- src/util/Utils.js | 52 +++++++++++++++++++----------------- 4 files changed, 35 insertions(+), 32 deletions(-) diff --git a/src/core/engine/Bridge.js b/src/core/engine/Bridge.js index 2f5e4ed..14c090b 100644 --- a/src/core/engine/Bridge.js +++ b/src/core/engine/Bridge.js @@ -1,6 +1,7 @@ const path = require('path') const fs = require('fs') -const requireAll = require('require-all') + +const { readdirRecursive, isDir } = require('../../util') /** * Middleware manager for commands @@ -30,7 +31,7 @@ class Bridge { if (!fs.existsSync(filepath)) { throw new Error(`Folder path ${filepath} does not exist`) } - const middleware = fs.statSync(filepath).isDirectory() ? requireAll(filepath) : require(filepath) + const middleware = isDir(filepath) ? readdirRecursive(filepath) : require(filepath) return this.register(middleware) } case 'object': { @@ -56,7 +57,7 @@ class Bridge { * @typedef {Object} Middleware * @prop {String} name Name of middleware * @prop {Number} priority Priority level of the middleware - * @prop {Promise(Container)} Middleware process + * @prop {Promise(Container)} process Middleware process */ /** diff --git a/src/core/engine/Commander.js b/src/core/engine/Commander.js index 5598ea5..a5887f5 100644 --- a/src/core/engine/Commander.js +++ b/src/core/engine/Commander.js @@ -1,8 +1,8 @@ const path = require('path') const fs = require('fs') -const requireAll = require('require-all') const Collection = require('../../util/Collection') +const { requireAll, isDir } = require('../../util') /** * Commander class for command processing @@ -29,7 +29,7 @@ class Commander extends Collection { if (!fs.existsSync(filepath)) { throw new Error(`Folder path ${filepath} does not exist`) } - const cmds = fs.statSync(filepath).isDirectory() ? requireAll(filepath) : require(filepath) + const cmds = isDir(filepath) ? requireAll(filepath) : require(filepath) return this.registerCommands(cmds) } case 'object': { diff --git a/src/core/engine/Router.js b/src/core/engine/Router.js index afcff9d..756f3b4 100644 --- a/src/core/engine/Router.js +++ b/src/core/engine/Router.js @@ -1,8 +1,8 @@ const path = require('path') const fs = require('fs') -const requireAll = require('require-all') const Collection = require('../../util/Collection') +const { readdirRecursive, isDir } = require('../../util') /** * Router class for event routing @@ -48,7 +48,7 @@ class Router extends Collection { if (!fs.existsSync(filepath)) { throw new Error(`Folder path ${filepath} does not exist`) } - const mods = fs.statSync(filepath).isDirectory() ? requireAll(filepath) : require(filepath) + const mods = isDir(filepath) ? readdirRecursive(filepath) : require(filepath) return this.registerModules(mods) } case 'object': { diff --git a/src/util/Utils.js b/src/util/Utils.js index 82c896c..6eb28a1 100644 --- a/src/util/Utils.js +++ b/src/util/Utils.js @@ -24,30 +24,6 @@ class Utils { this._client = client } - /** - * Reads a directory recursively and returns an array of paths - * @arg {...String} paths Folder path(s) - */ - static async readdirRecursive (...paths) { - const dir = path.join(...paths) - let list = [] - if (!fs.existsSync(dir)) return list - let files = fs.readdirSync(dir) - let dirs - - dirs = files.filter(this.constructor.isDir) - files = files.filter(file => !Utils.isDir(this.constructor)) - .map(file => path.join(dir, file)).filter(file => !path.basename(file).startsWith('.')) - list = list.concat(files) - - while (dirs.length) { - let d = path.join(dir, dirs.shift()) - list = list.concat(await this.constructor.readdirRecursive(d)) - } - - return list - } - /** * Checks if a path is a directory * @arg {String} filename The path to check @@ -87,7 +63,7 @@ class Utils { */ static getColour (colour) { if (!colours[colour]) return - return this.constructor.parseInt(String(colours[colour] || colour).replace('#', ''), 16) || null + return parseInt(String(colours[colour] || colour).replace('#', ''), 16) || null } /** @@ -112,6 +88,32 @@ class Utils { return r.position > role.position }) } + + /** + * Reads a directory recursively and returns an array of paths + * @arg {String} dir Directory path + * @returns {Array} + */ + static readdirRecursive (dir) { + return fs.readdirSync(dir).reduce((arr, file) => { + const filepath = path.join(dir, file) + arr.push(this.isDir(filepath) ? this.readdirRecursive(filepath) : require(filepath)) + return arr + }, []) + } + + /** + * Reads a directory recursively and returns an object mapping the required files to the folder + * @arg {String} dir Directory path + * @returns {Object} + */ + static requireAll (dir) { + return fs.readdirSync(dir).reduce((obj, file) => { + const filepath = path.join(dir, file) + obj[file] = this.isDir(filepath) ? this.requireAll(filepath) : require(filepath) + return obj + }, {}) + } } module.exports = Utils From 0fea30e0d7f3a9fd6f1d137ec70015916dfe0974 Mon Sep 17 00:00:00 2001 From: pyraxo Date: Fri, 30 Dec 2016 00:22:29 +0800 Subject: [PATCH 06/21] Improve readme readability --- README.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index c601a77..fab43b7 100644 --- a/README.md +++ b/README.md @@ -14,27 +14,27 @@

-### Used by +## Used by * [Tatsumaki](https://tatsumaki.xyz), a multi-purpose social Discord bot * [haru](https://pyraxo.moe/haru), everyone's favourite idol and part-time bot **iris** is an advanced, efficient and highly customisable framework for Discord command bots written in Node.js -### Requirements +# Requirements * **Node.js 6+** A firm grasp of **ES6 + async/await** syntax is optional but recommended. As the bot framework extends the [Eris](https://github.com/abalabahaha/Eris) client, please refer to the docs [here](https://abal.moe/Eris/docs). -### Usage +# Usage ```bash $ npm install --save pyraxo/iris ``` If you don't want to use any built-in plugins, you may run the above command with the `--no-optional` flag. -#### Example +## Example ```js const Bot = require('iris') @@ -47,12 +47,12 @@ client.register('commands', 'path/to/commands') client.run() ``` -### Plugins +# Plugins The framework consists of plugins that extend the functionality of the Discord client. Plugins can be added via external modules; several have already been included in the framework. -#### Commander +## Commander The **Commander** plugin allows **Commands** to be executed. When a successful command is called, the Commander will execute the corresponding command functions. Example usage: @@ -86,7 +86,7 @@ client .register('commands', filepath) ``` -#### Router +## Router The **Router** plugin takes care of **Modules** by routing event arguments to the corresponding modules' methods. Example usage: @@ -117,7 +117,7 @@ const modules = [ client.register('modules', modules) ``` -#### Bridge +## Bridge The **Bridge** plugin maintains a chain of **Middleware**, passing messages through each middleware function and resolving a **Container** object. Example usage: @@ -132,7 +132,7 @@ client.register('middleware', [{ }]) ``` -#### Registering Components into Built-in Plugins +## Registering Components into Built-in Plugins As shown in the examples above, to fully utilise the plugins, the components have to be registered into the appropriate plugin with the `register()` method. The 1st argument should take in the plugin type, while the 2nd argument should **not** receive the raw component, but instead should be an array or object containing the components. @@ -152,7 +152,7 @@ client client.register('commands', SomeCommand) ``` -#### Custom Plugins +## Custom Plugins Custom plugin support is available as long as the added plugins follow certain criteria: * Must be a class * Must contain the `register()` and `unregister()` methods, to which any number of arguments can be passed @@ -169,12 +169,12 @@ const plugin = client.plugins.get('pluginType') The plugin's `register` method will be called when `client.register()` is called with the first argument matching the corresponding plugin type. -### Components +# Components Plugins handle certain components that players can choose to add. For example, the **Commander** plugin makes use of **Commands**, which could be a class, function or object depending on the user's preference. **All built-in components have a built-in utility class containing various special methods. Do read the docs.** -#### Commands +## Commands Text commands are specially formatted messages that Discord users can send. Bots will then carry out tasks according to the issued command. Most commands follow this format: ``` @@ -189,7 +189,7 @@ Commands must include: * `execute` - A function that should accept a **Container** object as the first argument * `group` - An *optional* string referencing the group of the command -#### Modules +## Modules **Modules** are classes or objects containing methods that listen to specific events the client emits. Modules must include: @@ -209,7 +209,7 @@ Example module object: } ``` -#### Middleware +## Middleware **Middleware** are objects that receive a **Container** object, insert or modify its elements, and resolve the container. Middleware are chained together and executed according to their priority number. Middleware must include: @@ -217,7 +217,7 @@ Middleware must include: * `priority` - A number referencing the priority to run the middleware. A lower number means it will be run earlier. * `process` - A function that should a **Container** object as the first argument. -#### Container +## Container **Containers** are special objects containing properties that are passed around as a context. Among the default plugins, the **Commander** and **Bridge** plugins use them, and the **Commands** and **Middleware** components will be passed a **Container** as an argument, where they can modify and add properties to the container. A default unmodified container contains: @@ -227,7 +227,7 @@ A default unmodified container contains: * `modules` - A **Router** instance * `middleware` - A **Bridge** instance -### License +# License Copyright (C) 2017 Pyraxo This program is free software: you can redistribute it and/or modify From 05f7ce007eea1610ae7e36eb475cf4f116673a90 Mon Sep 17 00:00:00 2001 From: pyraxo Date: Fri, 30 Dec 2016 00:23:44 +0800 Subject: [PATCH 07/21] Modify readme --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index fab43b7..b5c81e4 100644 --- a/README.md +++ b/README.md @@ -14,27 +14,28 @@

-## Used by +### Used by * [Tatsumaki](https://tatsumaki.xyz), a multi-purpose social Discord bot * [haru](https://pyraxo.moe/haru), everyone's favourite idol and part-time bot **iris** is an advanced, efficient and highly customisable framework for Discord command bots written in Node.js -# Requirements +# Getting Started +## Requirements * **Node.js 6+** A firm grasp of **ES6 + async/await** syntax is optional but recommended. As the bot framework extends the [Eris](https://github.com/abalabahaha/Eris) client, please refer to the docs [here](https://abal.moe/Eris/docs). -# Usage +## Usage ```bash $ npm install --save pyraxo/iris ``` If you don't want to use any built-in plugins, you may run the above command with the `--no-optional` flag. -## Example +### Example ```js const Bot = require('iris') From 98238489aef1f845a17d621bcd37293d47dcee87 Mon Sep 17 00:00:00 2001 From: pyraxo Date: Fri, 30 Dec 2016 00:36:22 +0800 Subject: [PATCH 08/21] Export classes --- src/core/Client.js | 4 +++- src/core/index.js | 3 +++ src/index.js | 6 ++++++ 3 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 src/core/index.js create mode 100644 src/index.js diff --git a/src/core/Client.js b/src/core/Client.js index ae1f816..9dcd2c9 100644 --- a/src/core/Client.js +++ b/src/core/Client.js @@ -18,11 +18,13 @@ class Client extends Eris { * @arg {String} [options.commands] Relative path to commands folder * @arg {String} [options.modules] Relative path to modules folder * @arg {String} [options.middleware] Relative path to middleware folder - * @arg {String} [options.suppressWarnings] Option to suppress console warnings + * @arg {Boolean} [options.suppressWarnings=false] Option to suppress console warnings + * @arg {Boolean} [options.noDefaults=false] Option to not use built-in plugins */ constructor (options = {}) { super(options.token, options) this.suppressWarnings = options.suppressWarnings + this.noDefaults = options.noDefaults this.plugins = new Collection() diff --git a/src/core/index.js b/src/core/index.js new file mode 100644 index 0000000..83b60eb --- /dev/null +++ b/src/core/index.js @@ -0,0 +1,3 @@ +module.exports = Object.assign(require('./engine'), { + Client: require('./Client') +}) diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..be7f595 --- /dev/null +++ b/src/index.js @@ -0,0 +1,6 @@ +const Core = require('./core') +const Client = function Client (opts) { + return new Core.Client(opts) +} + +module.exports = Object.assign(Client, Core, require('./util')) From 0ce9440c4b775e57916393228fe6a1972f960f88 Mon Sep 17 00:00:00 2001 From: pyraxo Date: Fri, 30 Dec 2016 00:46:30 +0800 Subject: [PATCH 09/21] Add npmignore --- .gitignore | 1 + .npmignore | 9 +++++++++ 2 files changed, 10 insertions(+) create mode 100644 .npmignore diff --git a/.gitignore b/.gitignore index 11d6bb2..2a08766 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ node_modules assets docs +build diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..11d6bb2 --- /dev/null +++ b/.npmignore @@ -0,0 +1,9 @@ +logs +*.log +npm-debug.log* + +node_modules +.npm + +assets +docs From b0e982385c22f911c42d0efde29d57b6250e1f4a Mon Sep 17 00:00:00 2001 From: pyraxo Date: Fri, 30 Dec 2016 01:08:12 +0800 Subject: [PATCH 10/21] Prepare for publish --- .npmignore | 9 --------- package.json | 6 +++++- src/core/Client.js | 2 +- 3 files changed, 6 insertions(+), 11 deletions(-) delete mode 100644 .npmignore diff --git a/.npmignore b/.npmignore deleted file mode 100644 index 11d6bb2..0000000 --- a/.npmignore +++ /dev/null @@ -1,9 +0,0 @@ -logs -*.log -npm-debug.log* - -node_modules -.npm - -assets -docs diff --git a/package.json b/package.json index 18e3990..3c7cbd7 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "iris", + "name": "sylphy", "version": "3.0.0", "description": "The better Discord bot framework", "main": "build/index.js", @@ -10,6 +10,10 @@ "engines": { "node": ">=7.0.0" }, + "repository": { + "type": "git", + "url": "git+https://github.com/pyraxo/sylphy.git" + }, "keywords": [ "Discord", "Chat", diff --git a/src/core/Client.js b/src/core/Client.js index 9dcd2c9..6ca350b 100644 --- a/src/core/Client.js +++ b/src/core/Client.js @@ -13,7 +13,7 @@ const { Collection } = require('./util') class Client extends Eris { /** * Creates a new Client instance - * @arg {Object} options An object containing iris's and/or Eris client options + * @arg {Object} options An object containing sylphy's and/or Eris client options * @arg {String} options.token Discord bot token * @arg {String} [options.commands] Relative path to commands folder * @arg {String} [options.modules] Relative path to modules folder From f6c4ea17a4ff733ea91be2e038502e219a1453ec Mon Sep 17 00:00:00 2001 From: pyraxo Date: Fri, 30 Dec 2016 01:11:39 +0800 Subject: [PATCH 11/21] Rename iris title to sylphy --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b5c81e4..97a2e22 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@


- iris + sylphy

The better Discord bot framework

From db0f15e3e8127962fc84d695b7a0a03f0fade6a0 Mon Sep 17 00:00:00 2001 From: pyraxo Date: Fri, 30 Dec 2016 01:14:59 +0800 Subject: [PATCH 12/21] Update readme --- README.md | 29 ++++++----------------------- 1 file changed, 6 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 97a2e22..98669e6 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,7 @@ -
-

-
- sylphy -
-

-

The better Discord bot framework

-

- -
- Built with ❤ -
- Studio 777 -

-
- -### Used by -* [Tatsumaki](https://tatsumaki.xyz), a multi-purpose social Discord bot -* [haru](https://pyraxo.moe/haru), everyone's favourite idol and part-time bot - -**iris** is an advanced, efficient and highly customisable framework for Discord command bots written in Node.js +# Sylphy +Studio 777 [![npm](https://img.shields.io/npm/v/sylphy.svg)](https://www.npmjs.com/package/sylphy) + +**Sylphy** is an advanced, efficient and highly customisable framework for Discord command bots written in Node.js # Getting Started ## Requirements @@ -30,14 +13,14 @@ As the bot framework extends the [Eris](https://github.com/abalabahaha/Eris) cli ## Usage ```bash -$ npm install --save pyraxo/iris +$ npm install --save sylphy ``` If you don't want to use any built-in plugins, you may run the above command with the `--no-optional` flag. ### Example ```js -const Bot = require('iris') +const Bot = require('sylphy') const client = new Bot({ token: 'your token here' From 28e3b05182aa8a03f07d2d8b59239e7d76c75a92 Mon Sep 17 00:00:00 2001 From: pyraxo Date: Fri, 30 Dec 2016 01:25:58 +0800 Subject: [PATCH 13/21] Update tags and npmignore --- .npmignore | 9 +++++++++ package.json | 19 +++++++++++++------ 2 files changed, 22 insertions(+), 6 deletions(-) create mode 100644 .npmignore diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..11d6bb2 --- /dev/null +++ b/.npmignore @@ -0,0 +1,9 @@ +logs +*.log +npm-debug.log* + +node_modules +.npm + +assets +docs diff --git a/package.json b/package.json index 3c7cbd7..0278534 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sylphy", - "version": "3.0.0", + "version": "3.0.1", "description": "The better Discord bot framework", "main": "build/index.js", "scripts": { @@ -15,11 +15,18 @@ "url": "git+https://github.com/pyraxo/sylphy.git" }, "keywords": [ - "Discord", - "Chat", - "Bot", - "Commands", - "Base" + "discord", + "bot", + "framework", + "eris", + "core", + "base", + "es6", + "plugins", + "commands", + "modules", + "middleware", + "chat" ], "author": "pyraxo (https://pyraxo.moe)", "license": "AGPL-3.0", From 3652ec7d8b007522c11ffd1bbba981278ca61160 Mon Sep 17 00:00:00 2001 From: pyraxo Date: Fri, 30 Dec 2016 01:51:42 +0800 Subject: [PATCH 14/21] Move down to 0.0.1, fix stuff, add tests --- .gitignore | 2 ++ .npmignore | 2 ++ package.json | 7 ++++--- src/core/Client.js | 6 +++--- test/bot.js | 11 +++++++++++ 5 files changed, 22 insertions(+), 6 deletions(-) create mode 100644 test/bot.js diff --git a/.gitignore b/.gitignore index 2a08766..9976cb8 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ npm-debug.log* node_modules .npm +test/auth.json + assets docs build diff --git a/.npmignore b/.npmignore index 11d6bb2..c40498f 100644 --- a/.npmignore +++ b/.npmignore @@ -5,5 +5,7 @@ npm-debug.log* node_modules .npm +test/auth.json + assets docs diff --git a/package.json b/package.json index 0278534..ed55a2a 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,15 @@ { "name": "sylphy", - "version": "3.0.1", + "version": "0.0.1", "description": "The better Discord bot framework", "main": "build/index.js", "scripts": { "docs": "node_modules/.bin/jsdoc --configure .jsdoc.json --verbose", - "prepublish": "gulp" + "prepublish": "gulp", + "test": "gulp && node test/bot.js" }, "engines": { - "node": ">=7.0.0" + "node": ">=6.0.0" }, "repository": { "type": "git", diff --git a/src/core/Client.js b/src/core/Client.js index 6ca350b..a9ec565 100644 --- a/src/core/Client.js +++ b/src/core/Client.js @@ -1,7 +1,7 @@ -const Eris = require('eris').client +const Eris = require('eris').Client -const { Commander, Router, Bridge } = require('./core/engine') -const { Collection } = require('./util') +const { Commander, Router, Bridge } = require('./engine') +const { Collection } = require('../util') /** * Interface between the Discord client and plugins diff --git a/test/bot.js b/test/bot.js new file mode 100644 index 0000000..c545839 --- /dev/null +++ b/test/bot.js @@ -0,0 +1,11 @@ +const Client = require('../') + +const { token } = require('./auth.json') + +const bot = new Client({ + token: token +}) + +bot.on('ready', () => console.log('running!')) + +bot.run() From cdf8dc4508c0af0f0f6ab1989c4dc184e7e9e751 Mon Sep 17 00:00:00 2001 From: pyraxo Date: Fri, 30 Dec 2016 15:33:25 +0800 Subject: [PATCH 15/21] Modify doc files and readme --- README.md | 196 +++------------------------------------------------ package.json | 2 +- 2 files changed, 12 insertions(+), 186 deletions(-) diff --git a/README.md b/README.md index 98669e6..b013ad9 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,32 @@ # Sylphy Studio 777 [![npm](https://img.shields.io/npm/v/sylphy.svg)](https://www.npmjs.com/package/sylphy) +NPM info + **Sylphy** is an advanced, efficient and highly customisable framework for Discord command bots written in Node.js -# Getting Started -## Requirements +## Getting Started +### Requirements * **Node.js 6+** A firm grasp of **ES6 + async/await** syntax is optional but recommended. As the bot framework extends the [Eris](https://github.com/abalabahaha/Eris) client, please refer to the docs [here](https://abal.moe/Eris/docs). -## Usage +### Usage ```bash $ npm install --save sylphy ``` If you don't want to use any built-in plugins, you may run the above command with the `--no-optional` flag. -### Example +#### Example ```js const Bot = require('sylphy') const client = new Bot({ - token: 'your token here' + token: 'your token here', + modules: 'path/to/modules' }) client.register('commands', 'path/to/commands') @@ -31,187 +34,10 @@ client.register('commands', 'path/to/commands') client.run() ``` -# Plugins -The framework consists of plugins that extend the functionality of the Discord client. - -Plugins can be added via external modules; several have already been included in the framework. - -## Commander -The **Commander** plugin allows **Commands** to be executed. When a successful command is called, the Commander will execute the corresponding command functions. - -Example usage: -```js -const filepath = 'path/to/commands' -class PingCommand extends Command { - constructor (...args) { - super(...args, { - name: 'ping', - options: { guildOnly: true } - }) - } - - handle ({ msg }) { - return msg.reply('Pong!') - } -} -const commands = [ - PingCommand, - { - name: 'powerup', - execute: async ({ msg, client }) => { - const channel = await client.getChannel('247727924889911297') - return msg.reply(channel ? 'Powered up!' : 'Unpowered...') - } - } -] - -client -.register('commands', commands) -.register('commands', filepath) -``` - -## Router -The **Router** plugin takes care of **Modules** by routing event arguments to the corresponding modules' methods. - -Example usage: -```js -class BanModule extends Module { - constructor (...args) { - name: 'guilds:bans', - events: { - guildBanAdd: 'onBan' - } - } - - onBan (guild, user) { - console.log(`User ${user.username} has been banned from ${guild.name}`) - } -} -const modules = [ - BanModule, - { - name: 'guilds:logger', - events: { - guildCreate: 'newGuild' - }, - newGuild: (guild) => console.log(`New guild: ${guild.name}`) - } -] - -client.register('modules', modules) -``` - -## Bridge -The **Bridge** plugin maintains a chain of **Middleware**, passing messages through each middleware function and resolving a **Container** object. - -Example usage: -```js -client.register('middleware', [{ - name: 'checkPrivate', - priority: 1, - process: (container) => { - container.isPrivate = !!msg.guild - return Promise.resolve(container) - } -}]) -``` - -## Registering Components into Built-in Plugins -As shown in the examples above, to fully utilise the plugins, the components have to be registered into the appropriate plugin with the `register()` method. - -The 1st argument should take in the plugin type, while the 2nd argument should **not** receive the raw component, but instead should be an array or object containing the components. - -```js -// Correct: -client -.register('commands', [ SomeCommand ]) -.register('commands', { - core: { - 'ping': PingCommand, - 'help': HelpCommand - } -}) - -// Incorrect: -client.register('commands', SomeCommand) -``` - -## Custom Plugins -Custom plugin support is available as long as the added plugins follow certain criteria: -* Must be a class -* Must contain the `register()` and `unregister()` methods, to which any number of arguments can be passed - -To add a custom plugin: -```js -client.createPlugin('pluginType', PluginClass) -``` - -To access a plugin: -```js -const plugin = client.plugins.get('pluginType') -``` - -The plugin's `register` method will be called when `client.register()` is called with the first argument matching the corresponding plugin type. - -# Components -Plugins handle certain components that players can choose to add. For example, the **Commander** plugin makes use of **Commands**, which could be a class, function or object depending on the user's preference. - -**All built-in components have a built-in utility class containing various special methods. Do read the docs.** - -## Commands -Text commands are specially formatted messages that Discord users can send. Bots will then carry out tasks according to the issued command. Most commands follow this format: - -``` - -e.g. !help ping -``` - -Each **Command** component corresponds to a text command. Commands can be a class, function or object. - -Commands must include: -* `triggers` - An array of triggers, where the first is the main trigger while the rest are aliases -* `execute` - A function that should accept a **Container** object as the first argument -* `group` - An *optional* string referencing the group of the command - -## Modules -**Modules** are classes or objects containing methods that listen to specific events the client emits. - -Modules must include: -* `name` - A string containing the name of the middleware -* `events` - An object mapping an event name to a function name - -Example module object: -```js -{ - name: 'guilds:logger', - events: { - guildCreate: 'newGuild', - guildDelete: 'delGuild' - }, - newGuild: (guild) => console.log(`New guild: ${guild.name}`), - delGuild: (guild) => console.log(`Deleted guild: ${guild.name}`) -} -``` - -## Middleware -**Middleware** are objects that receive a **Container** object, insert or modify its elements, and resolve the container. Middleware are chained together and executed according to their priority number. - -Middleware must include: -* `name` - A string containing the name of the middleware -* `priority` - A number referencing the priority to run the middleware. A lower number means it will be run earlier. -* `process` - A function that should a **Container** object as the first argument. - -## Container -**Containers** are special objects containing properties that are passed around as a context. Among the default plugins, the **Commander** and **Bridge** plugins use them, and the **Commands** and **Middleware** components will be passed a **Container** as an argument, where they can modify and add properties to the container. - -A default unmodified container contains: -* `msg` - A message object -* `client` - The client instance -* `commands` - A **Commander** instance -* `modules` - A **Router** instance -* `middleware` - A **Bridge** instance +### Documentation +To view the API, please visit the [wiki](https://github.com/pyraxo/sylphy/wiki). -# License +## License Copyright (C) 2017 Pyraxo This program is free software: you can redistribute it and/or modify diff --git a/package.json b/package.json index ed55a2a..ff2401d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sylphy", - "version": "0.0.1", + "version": "0.0.2", "description": "The better Discord bot framework", "main": "build/index.js", "scripts": { From 9efd7dc708376cf8d152f408087983ce9460aaa1 Mon Sep 17 00:00:00 2001 From: pyraxo Date: Fri, 30 Dec 2016 15:34:51 +0800 Subject: [PATCH 16/21] Add Permitter class --- src/util/Permitter.js | 123 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 src/util/Permitter.js diff --git a/src/util/Permitter.js b/src/util/Permitter.js new file mode 100644 index 0000000..f28b0fd --- /dev/null +++ b/src/util/Permitter.js @@ -0,0 +1,123 @@ +/** + * Permission checker + */ +class Permitter { + /** + * Creates a new Permitter instance + * @arg {Client} Client instance + */ + constructor (client) { + this._client = client + } + + static get contexts () { + return ['members', 'roles', 'channels'] + } + + static isBoolean (val) { + return val === true || val === false + } + + static hasWildcard (obj) { + return obj !== null && '*' in obj ? this.isBoolean(obj['*']) : false + } + + /** + * Verifies if a command has permission to be executed + * @arg {String} node The permission node + * @arg {external:"Eris.Message"} message The command message + * @arg {Object} perms The permissions map + * @arg {Boolean} [defaultValue=true] The default result + */ + static verifyMessage (node, msg, perms = {}, defVal = true) { + if (!msg.guild) return true + let res = this.check(`${msg.channel.id}.${msg.author.id}.${node}`, perms) + if (this.isBoolean(res)) return res + + for (const perm of msg.member.roles.map(r => `${msg.channel.id}.${r}.${node}`)) { + res = this.check(perm, perms) + if (this.isBoolean(res)) return res + } + + res = this.check(`*.${msg.author.id}.${node}`, perms) + if (this.isBoolean(res)) return res + + for (const perm of msg.member.roles.map(r => `*.${r}.${node}`)) { + res = this.check(perm, perms) + if (this.isBoolean(res)) return res + } + + res = this.check(`${msg.channel.id}.${node}`, perms) + if (this.isBoolean(res)) return res + + res = this.check(`*.*.${node}`, perms) + if (this.isBoolean(res)) return res + + return defVal + } + + /** + * Checks if a node is allowed + * @arg {String} node The permission node + * @arg {Object} perms THe permissions map + */ + check (node, perms = {}) { + const res = node.split('.').reduce((obj, idx) => { + if (obj === null || this.isBoolean(obj)) return obj + if (idx in obj) return obj[idx] + else if ('*' in obj) return obj['*'] + return null + }, perms) + if (res === true || res === false) return res + return null + } + + /** + * Allows a permission node + * @arg {String} node The permission node + * @arg {Object} perms THe permissions map + */ + static allow (node, perms) { + return this.grant(node, true, perms) + } + + /** + * Denies a permission node + * @arg {String} node The permission node + * @arg {Object} perms THe permissions map + */ + static deny (node, perms) { + return this.grant(node, false, perms) + } + + /** + * Grants a permission node allow or deny + * @arg {String} node The permission node + * @arg {Boolean} value The value of the node + * @arg {Object} perms THe permissions map + */ + static grant (node, val = true, rawPerms = {}) { + const nodes = node.split('.') + const last = nodes.length - 1 + + nodes.reduce((o, c, i) => { + if (i >= last) { + if (typeof o['*'] === 'undefined') o['*'] = null + if (o[c] === true || o[c] === false && o[c] !== val) { + o[c] = null + } else { + o[c] = val + } + } else if (typeof o[c] === 'undefined') { + o[c] = {} + } else if (o[c] === true || o[c] === false) { + o[c] = { '*': o[c] } + } + return o[c] + }, rawPerms) + + return rawPerms + } +} + +module.exports = Permitter From be097091cfbddf7ad410b25b2ed8abfa4f04fd2a Mon Sep 17 00:00:00 2001 From: pyraxo Date: Fri, 30 Dec 2016 15:35:05 +0800 Subject: [PATCH 17/21] Various changes to Client and Resolver --- src/core/Client.js | 18 +++--- src/managers/Resolver.js | 117 ++++++++++++++++++++------------------- 2 files changed, 71 insertions(+), 64 deletions(-) diff --git a/src/core/Client.js b/src/core/Client.js index a9ec565..6753db6 100644 --- a/src/core/Client.js +++ b/src/core/Client.js @@ -28,14 +28,16 @@ class Client extends Eris { this.plugins = new Collection() - this - .createPlugin('commands', Commander) - .createPlugin('modules', Router) - .createPlugin('middleware', Bridge) - - if (options.commands) this.register('commands', options.commands) - if (options.modules) this.register('modules', options.modules) - if (options.middleware) this.register('middleware', options.middleware) + if (!this.noDefaults) { + this + .createPlugin('commands', Commander) + .createPlugin('modules', Router) + .createPlugin('middleware', Bridge) + + if (options.commands) this.register('commands', options.commands) + if (options.modules) this.register('modules', options.modules) + if (options.middleware) this.register('middleware', options.middleware) + } } /** diff --git a/src/managers/Resolver.js b/src/managers/Resolver.js index d1fac34..30d405e 100644 --- a/src/managers/Resolver.js +++ b/src/managers/Resolver.js @@ -1,17 +1,16 @@ -const { readdirRecursive } = require('../util') +const { readdirRecursive, Collection } = require('../util') /** * Resolver manager for resolving usages - * @prop {Object} resolvers Object with resolver functions */ -class Resolver { +class Resolver extends Collection { /** * Creates a new Resolver instance * @arg {Client} client Client instance */ constructor (client) { + super() this._client = client - this.resolvers = {} } /** @@ -32,7 +31,7 @@ class Resolver { for (const name in resolvers) { const resolver = resolvers[name] if (!resolver.resolve || !resolver.type) continue - this._resolvers[resolver.type] = resolver + this.set(resolver.type, resolver) } return resolvers }) @@ -99,42 +98,36 @@ class Resolver { * Resolves a message * @arg {external:"Eris.Message"} message Eris message * @arg {String[]} args Array of strings, by default a message split by spaces, without command trigger and prefix - * @arg {Object} [data] Additional data - * @arg {String} [data.prefix] The client's prefix - * @arg {String} [data.command] The command trigger - * @arg {CommandUsage} [usage=this.usage] CommandUsage object - * @returns {Promise<*, ResolverError>} + * @arg {Object} data Additional data + * @arg {String} data.prefix The client's prefix + * @arg {String} data.command The command trigger + * @arg {CommandUsage[]} [usage=this.usage] Array of CommandUsage objects + * @returns {Promise} */ - resolve (message, rawArgs, data = {}, rawUsage = this.usage) { + resolve (message, rawArgs, data, rawUsage = this.usage) { + let args = {} + const usage = this.verify(usage) - if (!usage.length) return Promise.resolve() + if (!usage.length) return Promise.resolve(args) const argsCount = rawArgs.length const requiredArgs = usage.filter(arg => !arg.optional).length const optionalArgs = argsCount - requiredArgs - // REDO if (argsCount < requiredArgs) { - let msg = '{{%resolver.INSUFFICIENT_ARGS}}' - if (data.prefix && data.command) { - msg += `\n\n**{{%resolver.CORRECT_USAGE}}**: \`${data.prefix}${data.command} ` + (usage.length - ? usage.map(arg => arg.optional ? `[${arg.displayName}]` : `<${arg.displayName}>`).join(' ') - : '') + '`' - } return Promise.reject({ - message: msg, + message: 'INSUFFICIENT_ARGS', requiredArgs: `**${requiredArgs}**`, argsCount: `**${argsCount}**.` }) } - let args = {} let idx = 0 let optArgs = 0 let resolves = [] let skip = false for (const arg of usage) { - let rawArg + let rawArg = rawArgs[idx] if (arg.last) { rawArg = rawArgs.slice(idx).join(' ') skip = true @@ -147,7 +140,6 @@ class Resolver { continue } } - rawArg = rawArgs[idx] if (typeof rawArg !== 'undefined') { if (rawArg.startsWith('"')) { const endQuote = rawArgs.findIndex((str, i) => str.endsWith('"') && i >= idx) @@ -155,48 +147,61 @@ class Resolver { rawArg = rawArgs.slice(idx, endQuote + 1).join(' ').replace(/"/g, '') idx = endQuote } else { - return Promise.reject('{{%resolver.NO_END_QUOTE}}') + return Promise.reject({ message: 'NO_END_QUOTE' }) } } } idx++ } - resolves.push( - Promise.all(arg.types.map(type => { - const resolver = this._resolvers[type] - if (typeof resolver === 'undefined') { - return Promise.reject({ err: 'Invalid resolver type' }) - } - return resolver.resolve(rawArg, arg, message, this.bot) - .catch(err => Object.assign(arg, { - arg: `**\`${arg.name || 'argument'}\`**`, - err: err.message ? err.message : `{{%resolver.${err}}}` + - (data.prefix && data.command - ? `\n\n**{{%resolver.CORRECT_USAGE}}**: \`${data.prefix}${data.command} ` + - (usage.length ? usage.map(arg => - skip ? arg.displayName - : (arg.optional ? `[${arg.displayName}]` : `<${arg.displayName}>`) - ).join(' ') : '') + '`' - : '') - })) - })).then(results => { - const resolved = results.filter(v => !v.err) - - if (resolved.length) { - const res = resolved.length === 1 - ? resolved[0] - : resolved.reduce((p, c) => p.concat(c), []) - args[arg.name] = res - return res - } - - return Promise.reject(results[0]) - }) - ) + resolves.push(this._resolveArg(arg, rawArg, message, data).then(res => { + args[arg.name] = res + return res + })) if (skip) break } return Promise.all(resolves).then(() => args) } + + _resolveArg (arg, rawArg, message, data) { + const resolves = arg.types.map(type => { + const resolver = this.get(type) + if (!resolver) { + return Promise.resolve({ err: 'Invalid resolver type' }) + } + return resolver.resolve(rawArg, arg, message, this.bot) + .catch(err => Object.assign(arg, { + arg: `**\`${arg.name || 'argument'}\`**`, + err: err + })) + }) + return Promise.all(resolves).then(results => { + const resolved = results.filter(v => !v.err) + if (resolved.length) { + const res = resolved.length === 1 ? resolved[0] : resolved.reduce((arr, c) => arr.concat(c), []) + return res + } + let err = results[0].err + if (err instanceof Error) { + return Promise.reject({ message: 'PARSING_ERROR', err: err }) + } + return Promise.reject({ message: err }) + }) + } + + /** + * Gets a command usage + * @arg {CommandUsage[]} [usage=this.usage] Array of CommandUsage objects + * @arg {Object} data Additional data + * @arg {String} data.prefix The client's prefix + * @arg {String} data.command The command trigger + * @returns {String} + */ + getUsage (usage = this.usage, { prefix, command } = {}) { + const argsUsage = usage.map(arg => + arg.last ? arg.displayName : arg.optional ? `[${arg.displayName}]` : `<${arg.displayName}>` + ).join(' ') + return `${prefix}${command}` + (usage.length ? ' ' + argsUsage : '') + } } module.exports = Resolver From 11e7d4dc49317fd16995018721d11208c9cc8218 Mon Sep 17 00:00:00 2001 From: pyraxo Date: Fri, 30 Dec 2016 23:31:58 +0800 Subject: [PATCH 18/21] Remove unnecessary optional dependencies --- package.json | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/package.json b/package.json index ff2401d..4884e98 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sylphy", - "version": "0.0.2", + "version": "0.1.0", "description": "The better Discord bot framework", "main": "build/index.js", "scripts": { @@ -41,19 +41,5 @@ "gulp-babel": "^6.1.2", "jsdoc": "^3.4.3", "jsdoc-strip-async-await": "^0.1.0" - }, - "optionalDependencies": { - "bluebird": "^3.4.7", - "chalk": "^1.1.3", - "eventemitter3": "^2.0.2", - "moment": "^2.13.0", - "moment-timezone": "^0.5.5", - "node-opus": "^0.2.4", - "redis": "^2.6.1", - "superagent": "^2.2.0", - "thinky": "^2.3.7", - "winston": "^2.2.0", - "winston-cluster": "0.0.4", - "winston-daily-rotate-file": "^1.3.1" } } From 561110673f0b6b7efa51a13213a48faf7ce27199 Mon Sep 17 00:00:00 2001 From: pyraxo Date: Sat, 31 Dec 2016 00:54:46 +0800 Subject: [PATCH 19/21] Add reloading --- src/core/Client.js | 28 ++++++++++++++++++++++++++++ src/core/engine/Bridge.js | 19 +++++++++++++++++-- src/core/engine/Commander.js | 24 +++++++++++++++++++----- src/core/engine/Router.js | 24 +++++++++++++++++++----- src/util/Collection.js | 14 +++++++------- 5 files changed, 90 insertions(+), 19 deletions(-) diff --git a/src/core/Client.js b/src/core/Client.js index 6753db6..3b751fd 100644 --- a/src/core/Client.js +++ b/src/core/Client.js @@ -1,3 +1,4 @@ +const path = require('path') const Eris = require('eris').Client const { Commander, Router, Bridge } = require('./engine') @@ -90,6 +91,21 @@ class Client extends Eris { return this } + /** + * Unloads files from the require cache + * @arg {String} filepath A relative or absolute directory path, file path or file name + * @returns {Client} + */ + unload (filepath) { + Object.keys(require.cache).forEach(file => { + const str = path.isAbsolute(filepath) ? filepath : path.join(process.cwd(), filepath) + if (str === file || file.startsWith(str)) { + delete require.cache[require.resolve(file)] + } + }) + return this + } + /** * Runs the bot * @returns {Client} @@ -147,3 +163,15 @@ module.exports = Client * @external "Eris.Member" * @see {@link https://abal.moe/Eris/docs/Member|Eris.Member} */ + +/** + * The Eris user object + * @external "Eris.User" + * @see {@link https://abal.moe/Eris/docs/User|Eris.User} + */ + +/** + * The Eris channel object + * @external "Eris.GuildChannel" + * @see {@link https://abal.moe/Eris/docs/GuildChannel|Eris.GuildChannel} + */ diff --git a/src/core/engine/Bridge.js b/src/core/engine/Bridge.js index 14c090b..adab00d 100644 --- a/src/core/engine/Bridge.js +++ b/src/core/engine/Bridge.js @@ -16,21 +16,23 @@ class Bridge { constructor (commander) { this.tasks = [] this.collectors = [] + this._cached = [] this._commander = commander } /** * Registers middleware - * @param {String|Object|Array} middleware An object, array or relative path to a folder or file to load middleware from + * @arg {String|Object|Array} middleware An object, array or relative path to a folder or file to load middleware from * @returns {Client} */ register (middleware) { switch (typeof middleware) { case 'string': { - const filepath = path.join(__dirname, middleware) + const filepath = path.isAbsolute(middleware) ? middleware : path.join(process.cwd(), middleware) if (!fs.existsSync(filepath)) { throw new Error(`Folder path ${filepath} does not exist`) } + this._cached.push(filepath) const middleware = isDir(filepath) ? readdirRecursive(filepath) : require(filepath) return this.register(middleware) } @@ -179,6 +181,19 @@ class Bridge { return middleware } + /** + * Reloads middleware files (only those that have been added from by file path) + * @returns {Client} + */ + reload () { + for (const filepath of this._cached) { + this._client.unload(filepath) + this._cached.shift() + this.register(filepath) + } + return this + } + /** * Context container holding a message object along with added properties and objects * @typedef {Object} Container diff --git a/src/core/engine/Commander.js b/src/core/engine/Commander.js index a5887f5..6f0421a 100644 --- a/src/core/engine/Commander.js +++ b/src/core/engine/Commander.js @@ -1,8 +1,7 @@ const path = require('path') const fs = require('fs') -const Collection = require('../../util/Collection') -const { requireAll, isDir } = require('../../util') +const { requireAll, isDir, Collection } = require('../../util') /** * Commander class for command processing @@ -15,22 +14,24 @@ class Commander extends Collection { constructor (client) { super() this._client = client + this._cached = [] } /** * Registers commands - * @param {String|Object|Array} commands An object, array or relative path to a folder or file to load commands from + * @arg {String|Object|Array} commands An object, array or relative path to a folder or file to load commands from * @returns {Client} */ register (commands) { switch (typeof commands) { case 'string': { - const filepath = path.join(__dirname, commands) + const filepath = path.join(process.cwd(), commands) if (!fs.existsSync(filepath)) { throw new Error(`Folder path ${filepath} does not exist`) } const cmds = isDir(filepath) ? requireAll(filepath) : require(filepath) - return this.registerCommands(cmds) + this._cached.push(filepath) + return this.register(cmds) } case 'object': { if (Array.isArray(commands)) { @@ -165,6 +166,19 @@ class Commander extends Collection { return this } + /** + * Reloads command files (only those that have been added from by file path) + * @returns {Client} + */ + reload () { + for (const filepath of this._cached) { + this._client.unload(filepath) + this._cached.shift() + this.register(filepath) + } + return this + } + /** * Executes a command * @arg {String} trigger The trigger of the command to be executed diff --git a/src/core/engine/Router.js b/src/core/engine/Router.js index 756f3b4..a9a1c76 100644 --- a/src/core/engine/Router.js +++ b/src/core/engine/Router.js @@ -1,8 +1,7 @@ const path = require('path') const fs = require('fs') -const Collection = require('../../util/Collection') -const { readdirRecursive, isDir } = require('../../util') +const { readdirRecursive, isDir, Collection } = require('../../util') /** * Router class for event routing @@ -16,6 +15,7 @@ class Router extends Collection { constructor (client) { super() this._client = client + this._cached = [] this.events = {} } @@ -38,18 +38,19 @@ class Router extends Collection { /** * Registers modules - * @param {String|Object|Array} modules An object, array or relative path to a folder or file to load modules from + * @arg {String|Object|Array} modules An object, array or relative path to a folder or file to load modules from * @returns {Client} */ register (modules) { switch (typeof modules) { case 'string': { - const filepath = path.join(__dirname, modules) + const filepath = path.join(process.cwd(), modules) if (!fs.existsSync(filepath)) { throw new Error(`Folder path ${filepath} does not exist`) } const mods = isDir(filepath) ? readdirRecursive(filepath) : require(filepath) - return this.registerModules(mods) + this._cached.push(filepath) + return this.register(mods) } case 'object': { if (Array.isArray(modules)) { @@ -152,6 +153,19 @@ class Router extends Collection { return this } + /** + * Reloads module files (only those that have been added from by file path) + * @returns {Client} + */ + reload () { + for (const filepath of this._cached) { + this._client.unload(filepath) + this._cached.shift() + this.register(filepath) + } + return this + } + /** * Fires when an error occurs in Router * diff --git a/src/util/Collection.js b/src/util/Collection.js index 47a326a..e10e107 100644 --- a/src/util/Collection.js +++ b/src/util/Collection.js @@ -13,7 +13,7 @@ class Collection extends Map { /** * Executes a function on all values - * @param {function} func forEach function + * @arg {function} func forEach function */ forEach (...args) { return this.toArray().forEach(...args) @@ -21,7 +21,7 @@ class Collection extends Map { /** * Filter values by function - * @param {function} func filter function + * @arg {function} func filter function * @returns {Array} Array of filtered values */ filter (...args) { @@ -30,7 +30,7 @@ class Collection extends Map { /** * Find values by function - * @param {function} func find function + * @arg {function} func find function * @returns {*} Value that was found */ find (...args) { @@ -39,7 +39,7 @@ class Collection extends Map { /** * Map values by function - * @param {function} func map function + * @arg {function} func map function * @returns {Array} Array of mapped values */ map (...args) { @@ -48,7 +48,7 @@ class Collection extends Map { /** * Reduce values by function - * @param {function} func reduce function + * @arg {function} func reduce function * @returns {Array} Array of reduced values */ reduce (...args) { @@ -57,7 +57,7 @@ class Collection extends Map { /** * Pluck values with key by function - * @param {String} key The matching key + * @arg {String} key The matching key * @returns {Array} Array of keyed values */ pluck (key) { @@ -70,7 +70,7 @@ class Collection extends Map { /** * Group values by key - * @param {String} key The matching key + * @arg {String} key The matching key * @returns {Object} Object containing grouped values */ groupBy (key) { From 4148aadb72081c2f4e1c21fcbea9092fd64f7f0b Mon Sep 17 00:00:00 2001 From: pyraxo Date: Sat, 31 Dec 2016 00:54:56 +0800 Subject: [PATCH 20/21] Add Interpreter plugin --- src/core/engine/Interpreter.js | 137 +++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 src/core/engine/Interpreter.js diff --git a/src/core/engine/Interpreter.js b/src/core/engine/Interpreter.js new file mode 100644 index 0000000..52fd4cb --- /dev/null +++ b/src/core/engine/Interpreter.js @@ -0,0 +1,137 @@ +const path = require('path') +const fs = require('fs') + +const { requireAll, isDir, Collection } = require('../../util') + +/** + * Locale manager and string parser + */ +class Interpreter extends Collection { + /** + * Creates a new Localisations instance + * @arg {Client} Client instance + */ + constructor (client) { + super() + this._client = client + this._cached = [] + } + + /** + * Registers a string table for a locale + * @arg {String|Object|Array} strings An object, array or relative path to a folder or file to load strings from + * @arg {String} [locale] The name of the locale. If none is supplied, `strings` will be treated as an object mapping locale names to string tables + */ + register (strings, loc) { + switch (typeof strings) { + case 'string': { + const filepath = path.isAbsolute(strings) ? strings : path.join(process.cwd(), strings) + if (!fs.existsSync(filepath)) { + throw new Error(`Folder path ${filepath} does not exist`) + } + this._cached.push(filepath) + const stringMap = isDir(filepath) ? requireAll(filepath) : require(filepath) + return this.register(stringMap) + } + case 'object': { + if (Array.isArray(strings)) { + for (const pair of strings) { + if (typeof pair[0] !== 'string') continue + this.set(pair[0], pair[1]) + } + return this + } + if (!loc) { + for (const lang in strings) { + this.set(lang, strings[lang]) + } + } else { + this.set(loc, strings) + } + return this + } + default: { + throw new Error('Path supplied is not an object or string') + } + } + } + + /** + * Reloads locale files (only those that have been added from by file path) + * @returns {Client} + */ + reload () { + for (const filepath of this._cached) { + this._client.unload(filepath) + this._cached.shift() + this.register(filepath) + } + return this + } + + /** + * Locate a nested key within an object + * @arg {String} key The key to find + * @arg {Object} obj The object to search from + * @returns {?String} + */ + locate (fullkey, obj) { + let keys = fullkey.split('.') + let val = obj[keys.shift()] + if (!val) return null + for (let key of keys) { + if (!val[key]) return val + val = val[key] + if (Array.isArray(val)) return val.join('\n') + } + return val || null + } + + /** + * Gets strings under a group key from a locale + * @arg {String} [key='common'] The string group to find + * @arg {String} [locale='en'] The locale to find + * @returns {?String} + */ + get (key = 'common', locale = 'en') { + if (!this.strings.has(locale)) locale = 'en' + return this.locate(key, this.strings.get(locale)) + } + + /** + * Parses a string and interpolating tags from a supplied object + * @arg {String} string String to do interpolation + * @arg {Object} options Object containing tags to interpolate into the string + * @returns {String} + */ + shift (string, options) { + if (!string) return string + return string.split(' ').map(str => ( + str.replace(/\{\{(.+)\}\}/gi, (matched, key) => ( + this.locate(key, options) || matched + )) + )).join(' ') + } + + /** + * Parses a string, converting keys to the matching locale string, with interpolation + * @arg {String} string The string to parse + * @arg {String} [group='common'] The string group to use + * @arg {String} [locale='en'] The locale to use + * @arg {Object} [options] Object containing tags to interpolate into the string + * @returns {String} + */ + parse (string, group = 'common', locale = 'en', options = {}) { + if (!string) return string + return string.split(' ').map(str => ( + str.replace(/\{\{(.+)\}\}/gi, (matched, key) => { + const g = key.startsWith('%') ? 'common.' : group + '.' + key = key.startsWith('%') ? key.substr(1) : key + let val = this.get(`${g}${key}`, locale) + return typeof val === 'string' ? this.shift(val, options) : matched + }) + )).join(' ') + } +} + +module.exports = Interpreter From d3b659ed3f5e4f24c352b7e158c1d2588b753884 Mon Sep 17 00:00:00 2001 From: pyraxo Date: Mon, 2 Jan 2017 02:35:44 +0800 Subject: [PATCH 21/21] 0.1.0 - Add default locales - Add logger class without chalk dependency - Add responder helper - Add default middleware - Add built-in command and base classes - Add emoji JSON to prevent node-emoji dependency - Add more tests - Various fixes --- .gitignore | 1 - package.json | 7 +- res/emoji.json | 1 + res/i18n/en/default.json | 24 + src/{core => }/Client.js | 26 +- src/core/{engine => }/Bridge.js | 50 +- src/core/{engine => }/Commander.js | 13 +- src/core/{engine => }/Interpreter.js | 26 +- src/core/Logger.js | 121 +++ src/core/{engine => }/Router.js | 8 +- src/core/engine/index.js | 5 - src/core/index.js | 10 +- src/index.js | 12 +- src/managers/Resolver.js | 8 +- src/managers/Responder.js | 200 ++++ src/managers/index.js | 5 + src/middleware/checkPrivate.js | 9 + src/middleware/commandLogger.js | 17 + src/middleware/parseMessage.js | 15 + src/structures/Base.js | 138 +++ src/structures/Command.js | 182 ++++ src/structures/index.js | 4 + src/util/Utils.js | 21 +- test/bot.js | 28 +- test/commands/EndCommand.js | 16 + test/commands/PingCommand.js | 4 + test/i18n/en/test.json | 3 + yarn.lock | 1368 +++++++++++++++++++++----- 28 files changed, 1987 insertions(+), 335 deletions(-) create mode 100644 res/emoji.json create mode 100644 res/i18n/en/default.json rename src/{core => }/Client.js (81%) rename src/core/{engine => }/Bridge.js (83%) rename src/core/{engine => }/Commander.js (95%) rename src/core/{engine => }/Interpreter.js (83%) create mode 100644 src/core/Logger.js rename src/core/{engine => }/Router.js (95%) delete mode 100644 src/core/engine/index.js create mode 100644 src/managers/Responder.js create mode 100644 src/managers/index.js create mode 100644 src/middleware/checkPrivate.js create mode 100644 src/middleware/commandLogger.js create mode 100644 src/middleware/parseMessage.js create mode 100644 src/structures/Base.js create mode 100644 src/structures/Command.js create mode 100644 src/structures/index.js create mode 100644 test/commands/EndCommand.js create mode 100644 test/commands/PingCommand.js create mode 100644 test/i18n/en/test.json diff --git a/.gitignore b/.gitignore index 9976cb8..755eca0 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,5 @@ node_modules test/auth.json -assets docs build diff --git a/package.json b/package.json index 4884e98..14cc438 100644 --- a/package.json +++ b/package.json @@ -5,15 +5,16 @@ "main": "build/index.js", "scripts": { "docs": "node_modules/.bin/jsdoc --configure .jsdoc.json --verbose", - "prepublish": "gulp", + "pub": "gulp && npm publish", + "install": "gulp", "test": "gulp && node test/bot.js" }, "engines": { "node": ">=6.0.0" }, "repository": { - "type": "git", - "url": "git+https://github.com/pyraxo/sylphy.git" + "url": "git+https://github.com/pyraxo/sylphy.git", + "type": "git" }, "keywords": [ "discord", diff --git a/res/emoji.json b/res/emoji.json new file mode 100644 index 0000000..4398941 --- /dev/null +++ b/res/emoji.json @@ -0,0 +1 @@ +{"100":"💯","1234":"🔢","interrobang":"⁉️","tm":"™️","information_source":"ℹ️","left_right_arrow":"↔️","arrow_up_down":"↕️","arrow_upper_left":"↖️","arrow_upper_right":"↗️","arrow_lower_right":"↘️","arrow_lower_left":"↙️","keyboard":"⌨","sunny":"☀️","cloud":"☁️","umbrella":"☔️","showman":"☃","comet":"☄","ballot_box_with_check":"☑️","coffee":"☕️","shamrock":"☘","skull_and_crossbones":"☠","radioactive_sign":"☢","biohazard_sign":"☣","orthodox_cross":"☦","wheel_of_dharma":"☸","white_frowning_face":"☹","aries":"♈️","taurus":"♉️","sagittarius":"♐️","capricorn":"♑️","aquarius":"♒️","pisces":"♓️","spades":"♠️","clubs":"♣️","hearts":"♥️","diamonds":"♦️","hotsprings":"♨️","hammer_and_pick":"⚒","anchor":"⚓️","crossed_swords":"⚔","scales":"⚖","alembic":"⚗","gear":"⚙","scissors":"✂️","white_check_mark":"✅","airplane":"✈️","email":"✉️","envelope":"✉️","black_nib":"✒️","heavy_check_mark":"✔️","heavy_multiplication_x":"✖️","star_of_david":"✡","sparkles":"✨","eight_spoked_asterisk":"✳️","eight_pointed_black_star":"✴️","snowflake":"❄️","sparkle":"❇️","question":"❓","grey_question":"❔","grey_exclamation":"❕","exclamation":"❗️","heavy_exclamation_mark":"❗️","heavy_heart_exclamation_mark_ornament":"❣","heart":"❤️","heavy_plus_sign":"➕","heavy_minus_sign":"➖","heavy_division_sign":"➗","arrow_heading_up":"⤴️","arrow_heading_down":"⤵️","wavy_dash":"〰️","congratulations":"㊗️","secret":"㊙️","copyright":"©️","registered":"®️","bangbang":"‼️","leftwards_arrow_with_hook":"↩️","arrow_right_hook":"↪️","watch":"⌚️","hourglass":"⌛️","fast_forward":"⏩","rewind":"⏪","arrow_double_up":"⏫","arrow_double_down":"⏬","black_right_pointing_double_triangle_with_vertical_bar":"⏭","black_left_pointing_double_triangle_with_vertical_bar":"⏮","black_right_pointing_triangle_with_double_vertical_bar":"⏯","alarm_clock":"⏰","stopwatch":"⏱","timer_clock":"⏲","hourglass_flowing_sand":"⏳","double_vertical_bar":"⏸","black_square_for_stop":"⏹","black_circle_for_record":"⏺","m":"Ⓜ️","black_small_square":"▪️","white_small_square":"▫️","arrow_forward":"▶️","arrow_backward":"◀️","white_medium_square":"◻️","black_medium_square":"◼️","white_medium_small_square":"◽️","black_medium_small_square":"◾️","phone":"☎️","telephone":"☎️","point_up":"☝️","star_and_crescent":"☪","peace_symbol":"☮","yin_yang":"☯","relaxed":"☺️","gemini":"♊️","cancer":"♋️","leo":"♌️","virgo":"♍️","libra":"♎️","scorpius":"♏️","recycle":"♻️","wheelchair":"♿️","atom_symbol":"⚛","fleur_de_lis":"⚜","warning":"⚠️","zap":"⚡️","white_circle":"⚪️","black_circle":"⚫️","coffin":"⚰","funeral_urn":"⚱","soccer":"⚽️","baseball":"⚾️","snowman":"⛄️","partly_sunny":"⛅️","thunder_cloud_and_rain":"⛈","ophiuchus":"⛎","pick":"⛏","helmet_with_white_cross":"⛑","chains":"⛓","no_entry":"⛔️","shinto_shrine":"⛩","church":"⛪️","mountain":"⛰","umbrella_on_ground":"⛱","fountain":"⛲️","golf":"⛳️","ferry":"⛴","boat":"⛵️","sailboat":"⛵️","skier":"⛷","ice_skate":"⛸","person_with_ball":"⛹","tent":"⛺️","fuelpump":"⛽️","fist":"✊","hand":"✋","raised_hand":"✋","v":"✌️","writing_hand":"✍","pencil2":"✏️","latin_cross":"✝","x":"❌","negative_squared_cross_mark":"❎","arrow_right":"➡️","curly_loop":"➰","loop":"➿","arrow_left":"⬅️","arrow_up":"⬆️","arrow_down":"⬇️","black_large_square":"⬛️","white_large_square":"⬜️","star":"⭐️","o":"⭕️","part_alternation_mark":"〽️","mahjong":"🀄️","black_joker":"🃏","a":"🅰️","b":"🅱️","o2":"🅾️","parking":"🅿️","ab":"🆎","cl":"🆑","cool":"🆒","free":"🆓","id":"🆔","new":"🆕","ng":"🆖","ok":"🆗","sos":"🆘","up":"🆙","vs":"🆚","koko":"🈁","sa":"🈂️","u7121":"🈚️","u6307":"🈯️","u7981":"🈲","u7a7a":"🈳","u5408":"🈴","u6e80":"🈵","u6709":"🈶","u6708":"🈷️","u7533":"🈸","u5272":"🈹","u55b6":"🈺","ideograph_advantage":"🉐","accept":"🉑","cyclone":"🌀","foggy":"🌁","closed_umbrella":"🌂","night_with_stars":"🌃","sunrise_over_mountains":"🌄","sunrise":"🌅","city_sunset":"🌆","city_sunrise":"🌇","rainbow":"🌈","bridge_at_night":"🌉","ocean":"🌊","volcano":"🌋","milky_way":"🌌","earth_africa":"🌍","earth_americas":"🌎","earth_asia":"🌏","globe_with_meridians":"🌐","new_moon":"🌑","waxing_crescent_moon":"🌒","first_quarter_moon":"🌓","moon":"🌔","waxing_gibbous_moon":"🌔","full_moon":"🌕","waning_gibbous_moon":"🌖","last_quarter_moon":"🌗","waning_crescent_moon":"🌘","crescent_moon":"🌙","new_moon_with_face":"🌚","first_quarter_moon_with_face":"🌛","last_quarter_moon_with_face":"🌜","full_moon_with_face":"🌝","sun_with_face":"🌞","star2":"🌟","stars":"🌠","thermometer":"🌡","mostly_sunny":"🌤","sun_small_cloud":"🌤","barely_sunny":"🌥","sun_behind_cloud":"🌥","partly_sunny_rain":"🌦","sun_behind_rain_cloud":"🌦","rain_cloud":"🌧","snow_cloud":"🌨","lightning":"🌩","lightning_cloud":"🌩","tornado":"🌪","tornado_cloud":"🌪","fog":"🌫","wind_blowing_face":"🌬","hotdog":"🌭","taco":"🌮","burrito":"🌯","chestnut":"🌰","seedling":"🌱","evergreen_tree":"🌲","deciduous_tree":"🌳","palm_tree":"🌴","cactus":"🌵","hot_pepper":"🌶","tulip":"🌷","cherry_blossom":"🌸","rose":"🌹","hibiscus":"🌺","sunflower":"🌻","blossom":"🌼","corn":"🌽","ear_of_rice":"🌾","herb":"🌿","four_leaf_clover":"🍀","maple_leaf":"🍁","fallen_leaf":"🍂","leaves":"🍃","mushroom":"🍄","tomato":"🍅","eggplant":"🍆","grapes":"🍇","melon":"🍈","watermelon":"🍉","tangerine":"🍊","lemon":"🍋","banana":"🍌","pineapple":"🍍","apple":"🍎","green_apple":"🍏","pear":"🍐","peach":"🍑","cherries":"🍒","strawberry":"🍓","hamburger":"🍔","pizza":"🍕","meat_on_bone":"🍖","poultry_leg":"🍗","rice_cracker":"🍘","rice_ball":"🍙","rice":"🍚","curry":"🍛","ramen":"🍜","spaghetti":"🍝","bread":"🍞","fries":"🍟","sweet_potato":"🍠","dango":"🍡","oden":"🍢","sushi":"🍣","fried_shrimp":"🍤","fish_cake":"🍥","icecream":"🍦","shaved_ice":"🍧","ice_cream":"🍨","doughnut":"🍩","cookie":"🍪","chocolate_bar":"🍫","candy":"🍬","lollipop":"🍭","custard":"🍮","honey_pot":"🍯","cake":"🍰","bento":"🍱","stew":"🍲","egg":"🍳","fork_and_knife":"🍴","tea":"🍵","sake":"🍶","wine_glass":"🍷","cocktail":"🍸","tropical_drink":"🍹","beer":"🍺","beers":"🍻","baby_bottle":"🍼","knife_fork_plate":"🍽","champagne":"🍾","popcorn":"🍿","ribbon":"🎀","gift":"🎁","birthday":"🎂","jack_o_lantern":"🎃","christmas_tree":"🎄","santa":"🎅","fireworks":"🎆","sparkler":"🎇","balloon":"🎈","tada":"🎉","confetti_ball":"🎊","tanabata_tree":"🎋","crossed_flags":"🎌","bamboo":"🎍","dolls":"🎎","flags":"🎏","wind_chime":"🎐","rice_scene":"🎑","school_satchel":"🎒","mortar_board":"🎓","medal":"🎖","reminder_ribbon":"🎗","studio_microphone":"🎙","level_slider":"🎚","control_knobs":"🎛","film_frames":"🎞","admission_tickets":"🎟","carousel_horse":"🎠","ferris_wheel":"🎡","roller_coaster":"🎢","fishing_pole_and_fish":"🎣","microphone":"🎤","movie_camera":"🎥","cinema":"🎦","headphones":"🎧","art":"🎨","tophat":"🎩","circus_tent":"🎪","ticket":"🎫","clapper":"🎬","performing_arts":"🎭","video_game":"🎮","dart":"🎯","slot_machine":"🎰","8ball":"🎱","game_die":"🎲","bowling":"🎳","flower_playing_cards":"🎴","musical_note":"🎵","notes":"🎶","saxophone":"🎷","guitar":"🎸","musical_keyboard":"🎹","trumpet":"🎺","violin":"🎻","musical_score":"🎼","running_shirt_with_sash":"🎽","tennis":"🎾","ski":"🎿","basketball":"🏀","checkered_flag":"🏁","snowboarder":"🏂","runner":"🏃","running":"🏃","surfer":"🏄","sports_medal":"🏅","trophy":"🏆","horse_racing":"🏇","football":"🏈","rugby_football":"🏉","swimmer":"🏊","weight_lifter":"🏋","golfer":"🏌","racing_motorcycle":"🏍","racing_car":"🏎","cricket_bat_and_ball":"🏏","volleyball":"🏐","field_hockey_stick_and_ball":"🏑","ice_hockey_stick_and_puck":"🏒","table_tennis_paddle_and_ball":"🏓","snow_capped_mountain":"🏔","camping":"🏕","beach_with_umbrella":"🏖","building_construction":"🏗","house_buildings":"🏘","cityscape":"🏙","derelict_house_building":"🏚","classical_building":"🏛","desert":"🏜","desert_island":"🏝","national_park":"🏞","stadium":"🏟","house":"🏠","house_with_garden":"🏡","office":"🏢","post_office":"🏣","european_post_office":"🏤","hospital":"🏥","bank":"🏦","atm":"🏧","hotel":"🏨","love_hotel":"🏩","convenience_store":"🏪","school":"🏫","department_store":"🏬","factory":"🏭","izakaya_lantern":"🏮","lantern":"🏮","japanese_castle":"🏯","european_castle":"🏰","waving_white_flag":"🏳","waving_black_flag":"🏴","rosette":"🏵","label":"🏷","badminton_racquet_and_shuttlecock":"🏸","bow_and_arrow":"🏹","amphora":"🏺","skin-tone-2":"🏻","skin-tone-3":"🏼","skin-tone-4":"🏽","skin-tone-5":"🏾","skin-tone-6":"🏿","rat":"🐀","mouse2":"🐁","ox":"🐂","water_buffalo":"🐃","cow2":"🐄","tiger2":"🐅","leopard":"🐆","rabbit2":"🐇","cat2":"🐈","dragon":"🐉","crocodile":"🐊","whale2":"🐋","snail":"🐌","snake":"🐍","racehorse":"🐎","ram":"🐏","goat":"🐐","sheep":"🐑","monkey":"🐒","rooster":"🐓","chicken":"🐔","dog2":"🐕","pig2":"🐖","boar":"🐗","elephant":"🐘","octopus":"🐙","shell":"🐚","bug":"🐛","ant":"🐜","bee":"🐝","honeybee":"🐝","beetle":"🐞","fish":"🐟","tropical_fish":"🐠","blowfish":"🐡","turtle":"🐢","hatching_chick":"🐣","baby_chick":"🐤","hatched_chick":"🐥","bird":"🐦","penguin":"🐧","koala":"🐨","poodle":"🐩","dromedary_camel":"🐪","camel":"🐫","dolphin":"🐬","flipper":"🐬","mouse":"🐭","cow":"🐮","tiger":"🐯","rabbit":"🐰","cat":"🐱","dragon_face":"🐲","whale":"🐳","horse":"🐴","monkey_face":"🐵","dog":"🐶","pig":"🐷","frog":"🐸","hamster":"🐹","wolf":"🐺","bear":"🐻","panda_face":"🐼","pig_nose":"🐽","feet":"🐾","paw_prints":"🐾","chipmunk":"🐿","eyes":"👀","eye":"👁","ear":"👂","nose":"👃","lips":"👄","tongue":"👅","point_up_2":"👆","point_down":"👇","point_left":"👈","point_right":"👉","facepunch":"👊","punch":"👊","wave":"👋","ok_hand":"👌","+1":"👍","thumbsup":"👍","-1":"👎","thumbsdown":"👎","clap":"👏","open_hands":"👐","crown":"👑","womans_hat":"👒","eyeglasses":"👓","necktie":"👔","shirt":"👕","tshirt":"👕","jeans":"👖","dress":"👗","kimono":"👘","bikini":"👙","womans_clothes":"👚","purse":"👛","handbag":"👜","pouch":"👝","mans_shoe":"👞","shoe":"👞","athletic_shoe":"👟","high_heel":"👠","sandal":"👡","boot":"👢","footprints":"👣","bust_in_silhouette":"👤","busts_in_silhouette":"👥","boy":"👦","girl":"👧","man":"👨","woman":"👩","family":"👨‍👩‍👦","man-woman-boy":"👨‍👩‍👦","couple":"👫","man_and_woman_holding_hands":"👫","two_men_holding_hands":"👬","two_women_holding_hands":"👭","cop":"👮","dancers":"👯","bride_with_veil":"👰","person_with_blond_hair":"👱","man_with_gua_pi_mao":"👲","man_with_turban":"👳","older_man":"👴","older_woman":"👵","baby":"👶","construction_worker":"👷","princess":"👸","japanese_ogre":"👹","japanese_goblin":"👺","ghost":"👻","angel":"👼","alien":"👽","space_invader":"👾","imp":"👿","skull":"💀","information_desk_person":"💁","guardsman":"💂","dancer":"💃","lipstick":"💄","nail_care":"💅","massage":"💆","haircut":"💇","barber":"💈","syringe":"💉","pill":"💊","kiss":"💋","love_letter":"💌","ring":"💍","gem":"💎","couplekiss":"💏","bouquet":"💐","couple_with_heart":"💑","wedding":"💒","heartbeat":"💓","broken_heart":"💔","two_hearts":"💕","sparkling_heart":"💖","heartpulse":"💗","cupid":"💘","blue_heart":"💙","green_heart":"💚","yellow_heart":"💛","purple_heart":"💜","gift_heart":"💝","revolving_hearts":"💞","heart_decoration":"💟","diamond_shape_with_a_dot_inside":"💠","bulb":"💡","anger":"💢","bomb":"💣","zzz":"💤","boom":"💥","collision":"💥","sweat_drops":"💦","droplet":"💧","dash":"💨","hankey":"💩","poop":"💩","shit":"💩","muscle":"💪","dizzy":"💫","speech_balloon":"💬","thought_balloon":"💭","white_flower":"💮","moneybag":"💰","currency_exchange":"💱","heavy_dollar_sign":"💲","credit_card":"💳","yen":"💴","dollar":"💵","euro":"💶","pound":"💷","money_with_wings":"💸","chart":"💹","seat":"💺","computer":"💻","briefcase":"💼","minidisc":"💽","floppy_disk":"💾","cd":"💿","dvd":"📀","file_folder":"📁","open_file_folder":"📂","page_with_curl":"📃","page_facing_up":"📄","date":"📅","calendar":"📆","card_index":"📇","chart_with_upwards_trend":"📈","chart_with_downwards_trend":"📉","bar_chart":"📊","clipboard":"📋","pushpin":"📌","round_pushpin":"📍","paperclip":"📎","straight_ruler":"📏","triangular_ruler":"📐","bookmark_tabs":"📑","ledger":"📒","notebook":"📓","notebook_with_decorative_cover":"📔","closed_book":"📕","book":"📖","open_book":"📖","green_book":"📗","blue_book":"📘","orange_book":"📙","books":"📚","name_badge":"📛","scroll":"📜","memo":"📝","pencil":"📝","telephone_receiver":"📞","pager":"📟","fax":"📠","satellite":"🛰","loudspeaker":"📢","mega":"📣","outbox_tray":"📤","inbox_tray":"📥","package":"📦","e-mail":"📧","incoming_envelope":"📨","envelope_with_arrow":"📩","mailbox_closed":"📪","mailbox":"📫","mailbox_with_mail":"📬","mailbox_with_no_mail":"📭","postbox":"📮","postal_horn":"📯","newspaper":"📰","iphone":"📱","calling":"📲","vibration_mode":"📳","mobile_phone_off":"📴","no_mobile_phones":"📵","signal_strength":"📶","camera":"📷","camera_with_flash":"📸","video_camera":"📹","tv":"📺","radio":"📻","vhs":"📼","film_projector":"📽","prayer_beads":"📿","twisted_rightwards_arrows":"🔀","repeat":"🔁","repeat_one":"🔂","arrows_clockwise":"🔃","arrows_counterclockwise":"🔄","low_brightness":"🔅","high_brightness":"🔆","mute":"🔇","speaker":"🔈","sound":"🔉","loud_sound":"🔊","battery":"🔋","electric_plug":"🔌","mag":"🔍","mag_right":"🔎","lock_with_ink_pen":"🔏","closed_lock_with_key":"🔐","key":"🔑","lock":"🔒","unlock":"🔓","bell":"🔔","no_bell":"🔕","bookmark":"🔖","link":"🔗","radio_button":"🔘","back":"🔙","end":"🔚","on":"🔛","soon":"🔜","top":"🔝","underage":"🔞","keycap_ten":"🔟","capital_abcd":"🔠","abcd":"🔡","symbols":"🔣","abc":"🔤","fire":"🔥","flashlight":"🔦","wrench":"🔧","hammer":"🔨","nut_and_bolt":"🔩","hocho":"🔪","knife":"🔪","gun":"🔫","microscope":"🔬","telescope":"🔭","crystal_ball":"🔮","six_pointed_star":"🔯","beginner":"🔰","trident":"🔱","black_square_button":"🔲","white_square_button":"🔳","red_circle":"🔴","large_blue_circle":"🔵","large_orange_diamond":"🔶","large_blue_diamond":"🔷","small_orange_diamond":"🔸","small_blue_diamond":"🔹","small_red_triangle":"🔺","small_red_triangle_down":"🔻","arrow_up_small":"🔼","arrow_down_small":"🔽","om_symbol":"🕉","dove_of_peace":"🕊","kaaba":"🕋","mosque":"🕌","synagogue":"🕍","menorah_with_nine_branches":"🕎","clock1":"🕐","clock2":"🕑","clock3":"🕒","clock4":"🕓","clock5":"🕔","clock6":"🕕","clock7":"🕖","clock8":"🕗","clock9":"🕘","clock10":"🕙","clock11":"🕚","clock12":"🕛","clock130":"🕜","clock230":"🕝","clock330":"🕞","clock430":"🕟","clock530":"🕠","clock630":"🕡","clock730":"🕢","clock830":"🕣","clock930":"🕤","clock1030":"🕥","clock1130":"🕦","clock1230":"🕧","candle":"🕯","mantelpiece_clock":"🕰","hole":"🕳","man_in_business_suit_levitating":"🕴","sleuth_or_spy":"🕵","dark_sunglasses":"🕶","spider":"🕷","spider_web":"🕸","joystick":"🕹","linked_paperclips":"🖇","lower_left_ballpoint_pen":"🖊","lower_left_fountain_pen":"🖋","lower_left_paintbrush":"🖌","lower_left_crayon":"🖍","raised_hand_with_fingers_splayed":"🖐","middle_finger":"🖕","reversed_hand_with_middle_finger_extended":"🖕","spock-hand":"🖖","desktop_computer":"🖥","printer":"🖨","three_button_mouse":"🖱","trackball":"🖲","frame_with_picture":"🖼","card_index_dividers":"🗂","card_file_box":"🗃","file_cabinet":"🗄","wastebasket":"🗑","spiral_note_pad":"🗒","spiral_calendar_pad":"🗓","compression":"🗜","old_key":"🗝","rolled_up_newspaper":"🗞","dagger_knife":"🗡","speaking_head_in_silhouette":"🗣","left_speech_bubble":"🗨","right_anger_bubble":"🗯","ballot_box_with_ballot":"🗳","world_map":"🗺","mount_fuji":"🗻","tokyo_tower":"🗼","statue_of_liberty":"🗽","japan":"🗾","moyai":"🗿","grinning":"😀","grin":"😁","joy":"😂","smiley":"😃","smile":"😄","sweat_smile":"😅","laughing":"😆","satisfied":"😆","innocent":"😇","smiling_imp":"😈","wink":"😉","blush":"😊","yum":"😋","relieved":"😌","heart_eyes":"😍","sunglasses":"😎","smirk":"😏","neutral_face":"😐","expressionless":"😑","unamused":"😒","sweat":"😓","pensive":"😔","confused":"😕","confounded":"😖","kissing":"😗","kissing_heart":"😘","kissing_smiling_eyes":"😙","kissing_closed_eyes":"😚","stuck_out_tongue":"😛","stuck_out_tongue_winking_eye":"😜","stuck_out_tongue_closed_eyes":"😝","disappointed":"😞","worried":"😟","angry":"😠","rage":"😡","cry":"😢","persevere":"😣","triumph":"😤","disappointed_relieved":"😥","frowning":"😦","anguished":"😧","fearful":"😨","weary":"😩","sleepy":"😪","tired_face":"😫","grimacing":"😬","sob":"😭","open_mouth":"😮","hushed":"😯","cold_sweat":"😰","scream":"😱","astonished":"😲","flushed":"😳","sleeping":"😴","dizzy_face":"😵","no_mouth":"😶","mask":"😷","smile_cat":"😸","joy_cat":"😹","smiley_cat":"😺","heart_eyes_cat":"😻","smirk_cat":"😼","kissing_cat":"😽","pouting_cat":"😾","crying_cat_face":"😿","scream_cat":"🙀","slightly_frowning_face":"🙁","slightly_smiling_face":"🙂","upside_down_face":"🙃","face_with_rolling_eyes":"🙄","no_good":"🙅","ok_woman":"🙆","bow":"🙇","see_no_evil":"🙈","hear_no_evil":"🙉","speak_no_evil":"🙊","raising_hand":"🙋","raised_hands":"🙌","person_frowning":"🙍","person_with_pouting_face":"🙎","pray":"🙏","rocket":"🚀","helicopter":"🚁","steam_locomotive":"🚂","railway_car":"🚃","bullettrain_side":"🚄","bullettrain_front":"🚅","train2":"🚆","metro":"🚇","light_rail":"🚈","station":"🚉","tram":"🚊","train":"🚋","bus":"🚌","oncoming_bus":"🚍","trolleybus":"🚎","busstop":"🚏","minibus":"🚐","ambulance":"🚑","fire_engine":"🚒","police_car":"🚓","oncoming_police_car":"🚔","taxi":"🚕","oncoming_taxi":"🚖","car":"🚗","red_car":"🚗","oncoming_automobile":"🚘","blue_car":"🚙","truck":"🚚","articulated_lorry":"🚛","tractor":"🚜","monorail":"🚝","mountain_railway":"🚞","suspension_railway":"🚟","mountain_cableway":"🚠","aerial_tramway":"🚡","ship":"🚢","rowboat":"🚣","speedboat":"🚤","traffic_light":"🚥","vertical_traffic_light":"🚦","construction":"🚧","rotating_light":"🚨","triangular_flag_on_post":"🚩","door":"🚪","no_entry_sign":"🚫","smoking":"🚬","no_smoking":"🚭","put_litter_in_its_place":"🚮","do_not_litter":"🚯","potable_water":"🚰","non-potable_water":"🚱","bike":"🚲","no_bicycles":"🚳","bicyclist":"🚴","mountain_bicyclist":"🚵","walking":"🚶","no_pedestrians":"🚷","children_crossing":"🚸","mens":"🚹","womens":"🚺","restroom":"🚻","baby_symbol":"🚼","toilet":"🚽","wc":"🚾","shower":"🚿","bath":"🛀","bathtub":"🛁","passport_control":"🛂","customs":"🛃","baggage_claim":"🛄","left_luggage":"🛅","couch_and_lamp":"🛋","sleeping_accommodation":"🛌","shopping_bags":"🛍","bellhop_bell":"🛎","bed":"🛏","place_of_worship":"🛐","hammer_and_wrench":"🛠","shield":"🛡","oil_drum":"🛢","motorway":"🛣","railway_track":"🛤","motor_boat":"🛥","small_airplane":"🛩","airplane_departure":"🛫","airplane_arriving":"🛬","passenger_ship":"🛳","zipper_mouth_face":"🤐","money_mouth_face":"🤑","face_with_thermometer":"🤒","nerd_face":"🤓","thinking_face":"🤔","face_with_head_bandage":"🤕","robot_face":"🤖","hugging_face":"🤗","the_horns":"🤘","sign_of_the_horns":"🤘","crab":"🦀","lion_face":"🦁","scorpion":"🦂","turkey":"🦃","unicorn_face":"🦄","cheese_wedge":"🧀","hash":"#️⃣","keycap_star":"*⃣","zero":"0️⃣","one":"1️⃣","two":"2️⃣","three":"3️⃣","four":"4️⃣","five":"5️⃣","six":"6️⃣","seven":"7️⃣","eight":"8️⃣","nine":"9️⃣","flag-ac":"🇦🇨","flag-ad":"🇦🇩","flag-ae":"🇦🇪","flag-af":"🇦🇫","flag-ag":"🇦🇬","flag-ai":"🇦🇮","flag-al":"🇦🇱","flag-am":"🇦🇲","flag-ao":"🇦🇴","flag-aq":"🇦🇶","flag-ar":"🇦🇷","flag-as":"🇦🇸","flag-at":"🇦🇹","flag-au":"🇦🇺","flag-aw":"🇦🇼","flag-ax":"🇦🇽","flag-az":"🇦🇿","flag-ba":"🇧🇦","flag-bb":"🇧🇧","flag-bd":"🇧🇩","flag-be":"🇧🇪","flag-bf":"🇧🇫","flag-bg":"🇧🇬","flag-bh":"🇧🇭","flag-bi":"🇧🇮","flag-bj":"🇧🇯","flag-bl":"🇧🇱","flag-bm":"🇧🇲","flag-bn":"🇧🇳","flag-bo":"🇧🇴","flag-bq":"🇧🇶","flag-br":"🇧🇷","flag-bs":"🇧🇸","flag-bt":"🇧🇹","flag-bv":"🇧🇻","flag-bw":"🇧🇼","flag-by":"🇧🇾","flag-bz":"🇧🇿","flag-ca":"🇨🇦","flag-cc":"🇨🇨","flag-cd":"🇨🇩","flag-cf":"🇨🇫","flag-cg":"🇨🇬","flag-ch":"🇨🇭","flag-ci":"🇨🇮","flag-ck":"🇨🇰","flag-cl":"🇨🇱","flag-cm":"🇨🇲","flag-cn":"🇨🇳","cn":"🇨🇳","flag-co":"🇨🇴","flag-cp":"🇨🇵","flag-cr":"🇨🇷","flag-cu":"🇨🇺","flag-cv":"🇨🇻","flag-cw":"🇨🇼","flag-cx":"🇨🇽","flag-cy":"🇨🇾","flag-cz":"🇨🇿","flag-de":"🇩🇪","de":"🇩🇪","flag-dg":"🇩🇬","flag-dj":"🇩🇯","flag-dk":"🇩🇰","flag-dm":"🇩🇲","flag-do":"🇩🇴","flag-dz":"🇩🇿","flag-ea":"🇪🇦","flag-ec":"🇪🇨","flag-ee":"🇪🇪","flag-eg":"🇪🇬","flag-eh":"🇪🇭","flag-er":"🇪🇷","flag-es":"🇪🇸","es":"🇪🇸","flag-et":"🇪🇹","flag-eu":"🇪🇺","flag-fi":"🇫🇮","flag-fj":"🇫🇯","flag-fk":"🇫🇰","flag-fm":"🇫🇲","flag-fo":"🇫🇴","flag-fr":"🇫🇷","fr":"🇫🇷","flag-ga":"🇬🇦","flag-gb":"🇬🇧","gb":"🇬🇧","uk":"🇬🇧","flag-gd":"🇬🇩","flag-ge":"🇬🇪","flag-gf":"🇬🇫","flag-gg":"🇬🇬","flag-gh":"🇬🇭","flag-gi":"🇬🇮","flag-gl":"🇬🇱","flag-gm":"🇬🇲","flag-gn":"🇬🇳","flag-gp":"🇬🇵","flag-gq":"🇬🇶","flag-gr":"🇬🇷","flag-gs":"🇬🇸","flag-gt":"🇬🇹","flag-gu":"🇬🇺","flag-gw":"🇬🇼","flag-gy":"🇬🇾","flag-hk":"🇭🇰","flag-hm":"🇭🇲","flag-hn":"🇭🇳","flag-hr":"🇭🇷","flag-ht":"🇭🇹","flag-hu":"🇭🇺","flag-ic":"🇮🇨","flag-id":"🇮🇩","flag-ie":"🇮🇪","flag-il":"🇮🇱","flag-im":"🇮🇲","flag-in":"🇮🇳","flag-io":"🇮🇴","flag-iq":"🇮🇶","flag-ir":"🇮🇷","flag-is":"🇮🇸","flag-it":"🇮🇹","it":"🇮🇹","flag-je":"🇯🇪","flag-jm":"🇯🇲","flag-jo":"🇯🇴","flag-jp":"🇯🇵","jp":"🇯🇵","flag-ke":"🇰🇪","flag-kg":"🇰🇬","flag-kh":"🇰🇭","flag-ki":"🇰🇮","flag-km":"🇰🇲","flag-kn":"🇰🇳","flag-kp":"🇰🇵","flag-kr":"🇰🇷","kr":"🇰🇷","flag-kw":"🇰🇼","flag-ky":"🇰🇾","flag-kz":"🇰🇿","flag-la":"🇱🇦","flag-lb":"🇱🇧","flag-lc":"🇱🇨","flag-li":"🇱🇮","flag-lk":"🇱🇰","flag-lr":"🇱🇷","flag-ls":"🇱🇸","flag-lt":"🇱🇹","flag-lu":"🇱🇺","flag-lv":"🇱🇻","flag-ly":"🇱🇾","flag-ma":"🇲🇦","flag-mc":"🇲🇨","flag-md":"🇲🇩","flag-me":"🇲🇪","flag-mf":"🇲🇫","flag-mg":"🇲🇬","flag-mh":"🇲🇭","flag-mk":"🇲🇰","flag-ml":"🇲🇱","flag-mm":"🇲🇲","flag-mn":"🇲🇳","flag-mo":"🇲🇴","flag-mp":"🇲🇵","flag-mq":"🇲🇶","flag-mr":"🇲🇷","flag-ms":"🇲🇸","flag-mt":"🇲🇹","flag-mu":"🇲🇺","flag-mv":"🇲🇻","flag-mw":"🇲🇼","flag-mx":"🇲🇽","flag-my":"🇲🇾","flag-mz":"🇲🇿","flag-na":"🇳🇦","flag-nc":"🇳🇨","flag-ne":"🇳🇪","flag-nf":"🇳🇫","flag-ng":"🇳🇬","flag-ni":"🇳🇮","flag-nl":"🇳🇱","flag-no":"🇳🇴","flag-np":"🇳🇵","flag-nr":"🇳🇷","flag-nu":"🇳🇺","flag-nz":"🇳🇿","flag-om":"🇴🇲","flag-pa":"🇵🇦","flag-pe":"🇵🇪","flag-pf":"🇵🇫","flag-pg":"🇵🇬","flag-ph":"🇵🇭","flag-pk":"🇵🇰","flag-pl":"🇵🇱","flag-pm":"🇵🇲","flag-pn":"🇵🇳","flag-pr":"🇵🇷","flag-ps":"🇵🇸","flag-pt":"🇵🇹","flag-pw":"🇵🇼","flag-py":"🇵🇾","flag-qa":"🇶🇦","flag-re":"🇷🇪","flag-ro":"🇷🇴","flag-rs":"🇷🇸","flag-ru":"🇷🇺","ru":"🇷🇺","flag-rw":"🇷🇼","flag-sa":"🇸🇦","flag-sb":"🇸🇧","flag-sc":"🇸🇨","flag-sd":"🇸🇩","flag-se":"🇸🇪","flag-sg":"🇸🇬","flag-sh":"🇸🇭","flag-si":"🇸🇮","flag-sj":"🇸🇯","flag-sk":"🇸🇰","flag-sl":"🇸🇱","flag-sm":"🇸🇲","flag-sn":"🇸🇳","flag-so":"🇸🇴","flag-sr":"🇸🇷","flag-ss":"🇸🇸","flag-st":"🇸🇹","flag-sv":"🇸🇻","flag-sx":"🇸🇽","flag-sy":"🇸🇾","flag-sz":"🇸🇿","flag-ta":"🇹🇦","flag-tc":"🇹🇨","flag-td":"🇹🇩","flag-tf":"🇹🇫","flag-tg":"🇹🇬","flag-th":"🇹🇭","flag-tj":"🇹🇯","flag-tk":"🇹🇰","flag-tl":"🇹🇱","flag-tm":"🇹🇲","flag-tn":"🇹🇳","flag-to":"🇹🇴","flag-tr":"🇹🇷","flag-tt":"🇹🇹","flag-tv":"🇹🇻","flag-tw":"🇹🇼","flag-tz":"🇹🇿","flag-ua":"🇺🇦","flag-ug":"🇺🇬","flag-um":"🇺🇲","flag-us":"🇺🇸","us":"🇺🇸","flag-uy":"🇺🇾","flag-uz":"🇺🇿","flag-va":"🇻🇦","flag-vc":"🇻🇨","flag-ve":"🇻🇪","flag-vg":"🇻🇬","flag-vi":"🇻🇮","flag-vn":"🇻🇳","flag-vu":"🇻🇺","flag-wf":"🇼🇫","flag-ws":"🇼🇸","flag-xk":"🇽🇰","flag-ye":"🇾🇪","flag-yt":"🇾🇹","flag-za":"🇿🇦","flag-zm":"🇿🇲","flag-zw":"🇿🇼","man-man-boy":"👨‍👨‍👦","man-man-boy-boy":"👨‍👨‍👦‍👦","man-man-girl":"👨‍👨‍👧","man-man-girl-boy":"👨‍👨‍👧‍👦","man-man-girl-girl":"👨‍👨‍👧‍👧","man-woman-boy-boy":"👨‍👩‍👦‍👦","man-woman-girl":"👨‍👩‍👧","man-woman-girl-boy":"👨‍👩‍👧‍👦","man-woman-girl-girl":"👨‍👩‍👧‍👧","man-heart-man":"👨‍❤️‍👨","man-kiss-man":"👨‍❤️‍💋‍👨","woman-woman-boy":"👩‍👩‍👦","woman-woman-boy-boy":"👩‍👩‍👦‍👦","woman-woman-girl":"👩‍👩‍👧","woman-woman-girl-boy":"👩‍👩‍👧‍👦","woman-woman-girl-girl":"👩‍👩‍👧‍👧","woman-heart-woman":"👩‍❤️‍👩","woman-kiss-woman":"👩‍❤️‍💋‍👩"} diff --git a/res/i18n/en/default.json b/res/i18n/en/default.json new file mode 100644 index 0000000..066019d --- /dev/null +++ b/res/i18n/en/default.json @@ -0,0 +1,24 @@ +{ + "errors": { + "INSUFFICIENT_ARGS": "insufficient arguments supplied - {{requiredArgs}} expected, but only {{argsCount}} found.", + "NO_END_QUOTE": "you have a missing end quote.", + "ON_COOLDOWN": "please cool down! ({{time}} seconds left)", + "NO_PERMS": "you need the permissions {{perms}} to run this command!", + "NO_PERMS_BOT": "I need the permissions {{perms}} to run this command!", + "NO_PMS": "this command can't be run in PMs." + }, + "menus": { + "EXIT": "Enter {{cancel}} to exit the menu.", + "ERRORED": "the menu has closed:", + "EXITED": "you have exited the menu.", + "SELECTION": "Selection Menu", + "ERROR": "Unknown Error", + "INPUT": "Type the number of your choice into chat or '{{cancel}}' to cancel.", + "MORE_RESULTS": "And {{num}} more..." + }, + "collector": { + "timeout": "Timeout after {{time}}s", + "max": "Exceeded {{max}} tries", + "maxMatches": "Exceeded {{maxMatches}} matches" + } +} diff --git a/src/core/Client.js b/src/Client.js similarity index 81% rename from src/core/Client.js rename to src/Client.js index 3b751fd..61c6340 100644 --- a/src/core/Client.js +++ b/src/Client.js @@ -1,8 +1,8 @@ const path = require('path') const Eris = require('eris').Client -const { Commander, Router, Bridge } = require('./engine') -const { Collection } = require('../util') +const { Commander, Router, Bridge, Interpreter, Logger } = require('./core') +const { Collection } = require('./util') /** * Interface between the Discord client and plugins @@ -16,16 +16,21 @@ class Client extends Eris { * Creates a new Client instance * @arg {Object} options An object containing sylphy's and/or Eris client options * @arg {String} options.token Discord bot token + * @arg {String} [options.prefix='!'] Default prefix for commands + * @arg {String} [options.admins=[]] Array of admin IDs * @arg {String} [options.commands] Relative path to commands folder * @arg {String} [options.modules] Relative path to modules folder * @arg {String} [options.middleware] Relative path to middleware folder + * @arg {String} [options.locales] Relative path to locales folder * @arg {Boolean} [options.suppressWarnings=false] Option to suppress console warnings * @arg {Boolean} [options.noDefaults=false] Option to not use built-in plugins */ constructor (options = {}) { super(options.token, options) + this.prefix = options.prefix || '!' this.suppressWarnings = options.suppressWarnings this.noDefaults = options.noDefaults + this.admins = Array.isArray(options.admins) ? options.admins : [] this.plugins = new Collection() @@ -34,10 +39,18 @@ class Client extends Eris { .createPlugin('commands', Commander) .createPlugin('modules', Router) .createPlugin('middleware', Bridge) + .createPlugin('i18n', Interpreter) + .createPlugin('logger', Logger) + + this.logger = this.plugins.get('logger') + + this.register('i18n', path.join(__dirname, '..', 'res/i18n')) + this.register('middleware', path.join(__dirname, 'middleware')) if (options.commands) this.register('commands', options.commands) if (options.modules) this.register('modules', options.modules) if (options.middleware) this.register('middleware', options.middleware) + if (options.locales) this.register('i18n', options.locales) } } @@ -68,7 +81,7 @@ class Client extends Eris { if (!plugin) { throw new Error(`Plugin type ${type} not found`) } - plugin.register(...args) + if (typeof plugin.register === 'function') plugin.register(...args) return this } @@ -87,7 +100,7 @@ class Client extends Eris { if (!plugin) { throw new Error(`Plugin type ${type} not found`) } - plugin.unregister(...args) + if (typeof plugin.unregister === 'function') plugin.unregister(...args) return this } @@ -114,6 +127,9 @@ class Client extends Eris { if (typeof this.token !== 'string') { throw new TypeError('No bot token supplied') } + this.plugins.forEach(plugin => { + if (typeof plugin.run === 'function') plugin.run() + }) this.connect() return this } @@ -125,7 +141,7 @@ class Client extends Eris { * @private */ throwOrEmit (event, error) { - if (!this.listeners(event, true)) { + if (!this.listeners(event).length) { throw error } this.emit(event, error) diff --git a/src/core/engine/Bridge.js b/src/core/Bridge.js similarity index 83% rename from src/core/engine/Bridge.js rename to src/core/Bridge.js index adab00d..7ac5d43 100644 --- a/src/core/engine/Bridge.js +++ b/src/core/Bridge.js @@ -1,7 +1,7 @@ const path = require('path') const fs = require('fs') -const { readdirRecursive, isDir } = require('../../util') +const { readdirRecursive, isDir } = require('../util') /** * Middleware manager for commands @@ -11,13 +11,14 @@ const { readdirRecursive, isDir } = require('../../util') class Bridge { /** * Creates a new Bridge instance - * @arg {Commander} commander Commander instance + * @arg {Client} client Client instance */ - constructor (commander) { + constructor (client) { this.tasks = [] this.collectors = [] this._cached = [] - this._commander = commander + this._client = client + this._commander = client.plugins.get('commands') } /** @@ -33,8 +34,8 @@ class Bridge { throw new Error(`Folder path ${filepath} does not exist`) } this._cached.push(filepath) - const middleware = isDir(filepath) ? readdirRecursive(filepath) : require(filepath) - return this.register(middleware) + const mw = isDir(filepath) ? readdirRecursive(filepath) : require(filepath) + return this.register(mw) } case 'object': { if (Array.isArray(middleware)) { @@ -66,12 +67,10 @@ class Bridge { * Inserts new middleware to the task queue according to ascending priority (lower numbers are earlier in queue) * @arg {Middleware} middleware Middleware object */ - push (middleware) { - const priority = middleware.priority || this.tasks.length - if (!middleware.process || !middleware.process.then) { - throw new Error('Middleware must be a promise') - } - this.tasks.splice(priority, 0, middleware) + push (Middleware) { + const middleware = typeof Middleware === 'function' ? new Middleware(this) : Middleware + this.tasks.push(middleware) + this.tasks.sort((a, b) => a.priority - b.priority) } /** @@ -194,6 +193,31 @@ class Bridge { return this } + /** Starts running the bridge */ + run () { + this._client.on('messageCreate', msg => { + if (msg.author.id === this._client.user.id || msg.author.bot) return + this.handle({ + msg: msg, + client: this._client, + logger: this._client.logger, + admins: this._client.admins, + commands: this._commander, + modules: this._client.plugins.get('modules'), + middleware: this + }).catch(err => { + if (err && this._client.logger) { + this._client.logger.error('Failed to handle message in Bridge -', err) + } + }) + }) + } + + /** Stops running the bridge */ + stop () { + this._client.removeAllListeners('messageCreate') + } + /** * Context container holding a message object along with added properties and objects * @typedef {Object} Container @@ -216,7 +240,7 @@ class Bridge { } for (const task of this.tasks) { try { - const result = await task(container) + const result = await task.process(container) if (!result) return Promise.reject() container = result } catch (err) { diff --git a/src/core/engine/Commander.js b/src/core/Commander.js similarity index 95% rename from src/core/engine/Commander.js rename to src/core/Commander.js index 6f0421a..e293d75 100644 --- a/src/core/engine/Commander.js +++ b/src/core/Commander.js @@ -1,7 +1,8 @@ const path = require('path') const fs = require('fs') +const util = require('util') -const { requireAll, isDir, Collection } = require('../../util') +const { requireAll, isDir, Collection } = require('../util') /** * Commander class for command processing @@ -36,12 +37,12 @@ class Commander extends Collection { case 'object': { if (Array.isArray(commands)) { for (const command of commands) { - this.commands.attach(command) + this.attach(command) } return this } for (const group in commands) { - this.commands.attach(commands[group], group) + this.attach(commands[group], group) } return this } @@ -67,9 +68,9 @@ class Commander extends Collection { * @returns {Commander} */ attach (Command, group) { - let command = typeof Command === 'function' ? new Command(this._client) : command - if (!command.triggers) { - this._client.throwOrEmit('commander:error', new Error(`Invalid command - ${command}`)) + let command = typeof Command === 'function' ? new Command(this._client) : Command + if (!command.triggers || !command.triggers.length) { + this._client.throwOrEmit('commander:error', new Error(`Invalid command - ${util.inspect(command)}`)) return this } for (const trigger of command.triggers) { diff --git a/src/core/engine/Interpreter.js b/src/core/Interpreter.js similarity index 83% rename from src/core/engine/Interpreter.js rename to src/core/Interpreter.js index 52fd4cb..13b2cc3 100644 --- a/src/core/engine/Interpreter.js +++ b/src/core/Interpreter.js @@ -1,11 +1,9 @@ const path = require('path') const fs = require('fs') -const { requireAll, isDir, Collection } = require('../../util') +const { requireAll, isDir, Collection } = require('../util') -/** - * Locale manager and string parser - */ +/** Locale manager and string parser */ class Interpreter extends Collection { /** * Creates a new Localisations instance @@ -37,16 +35,16 @@ class Interpreter extends Collection { if (Array.isArray(strings)) { for (const pair of strings) { if (typeof pair[0] !== 'string') continue - this.set(pair[0], pair[1]) + this.set(pair[0], Object.assign(this.get(pair[0]) || {}, pair[1])) } return this } if (!loc) { for (const lang in strings) { - this.set(lang, strings[lang]) + this.set(lang, Object.assign(this.get(lang) || {}, strings[lang])) } } else { - this.set(loc, strings) + this.set(loc, Object.assign(this.get(loc) || {}, strings)) } return this } @@ -93,9 +91,9 @@ class Interpreter extends Collection { * @arg {String} [locale='en'] The locale to find * @returns {?String} */ - get (key = 'common', locale = 'en') { - if (!this.strings.has(locale)) locale = 'en' - return this.locate(key, this.strings.get(locale)) + getStrings (key = 'common', locale = 'en') { + if (!this.has(locale)) locale = 'en' + return this.locate(key, this.get(locale)) } /** @@ -116,18 +114,18 @@ class Interpreter extends Collection { /** * Parses a string, converting keys to the matching locale string, with interpolation * @arg {String} string The string to parse - * @arg {String} [group='common'] The string group to use + * @arg {String} [group='default'] The string group to use * @arg {String} [locale='en'] The locale to use * @arg {Object} [options] Object containing tags to interpolate into the string * @returns {String} */ - parse (string, group = 'common', locale = 'en', options = {}) { + parse (string, group = 'default', locale = 'en', options = {}) { if (!string) return string return string.split(' ').map(str => ( str.replace(/\{\{(.+)\}\}/gi, (matched, key) => { - const g = key.startsWith('%') ? 'common.' : group + '.' + const g = key.startsWith('%') ? 'default.' : group + '.' key = key.startsWith('%') ? key.substr(1) : key - let val = this.get(`${g}${key}`, locale) + let val = this.getStrings(`${g}${key}`, locale) return typeof val === 'string' ? this.shift(val, options) : matched }) )).join(' ') diff --git a/src/core/Logger.js b/src/core/Logger.js new file mode 100644 index 0000000..cd3dc88 --- /dev/null +++ b/src/core/Logger.js @@ -0,0 +1,121 @@ +/** Logs information to the console */ +class Logger { + /** + * Creates a new Logger instance + * @arg {Object} [options] Logger options + * @arg {String} [options.name] Option for a prefix before logging + * @arg {Boolean} [options.suppressWarnings=false] Option to suppress warnings + * @arg {Boolean} [options.timestamps=true] Option to show timestamps + */ + constructor ({ name, suppressWarnings, timestamps = true } = {}) { + this.timestamps = timestamps + this.name = name ? `- [${name}] ` : '' + + this.styles = {} + for (const code in this.codes) { + const val = this.codes[code] + this.styles[code] = this.supportsColours ? (str) => `\u001b[${val[0]}m${str}\u001b[${val[1]}m` : (str) => str + } + } + + /** + * Logs to console + * @arg {...String} args Strings to log to console + */ + log (...args) { + console.log(this.timestamp + this.name + args.join(' ')) + } + + /** + * Logs to console at `info` level + * @arg {...String} args Strings to log to console + */ + info (...args) { + console.log(`${this.timestamp}${this.name}${this.styles.green('info')} - ${args.join(' ')}`) + } + + /** + * Logs to console at `warn` level + * @arg {...String} args Strings to log to console + */ + warn (...args) { + console.log(`${this.timestamp}${this.name}${this.styles.yellow('warn')} - ${args.join(' ')}`) + } + + /** + * Logs to console at `error` level + * @arg {...(String|Error)} args Strings or errors to log to console + */ + error (...args) { + console.log(`${this.timestamp}${this.name}${this.styles.red('error')} - ${args.map(e => e instanceof Error ? e.stack : e).join(' ')}`) + } + + /** + * Logs to console at `debug` level + * @arg {...String} args Strings to log to console + */ + debug (...args) { + console.log(`${this.timestamp}${this.name}${this.styles.grey('debug')} - ${args.join(' ')}`) + } + + get codes () { + return { + reset: [0, 0], + + bold: [1, 22], + dim: [2, 22], + italic: [3, 23], + underline: [4, 24], + inverse: [7, 27], + hidden: [8, 28], + strikethrough: [9, 29], + + black: [30, 39], + red: [31, 39], + green: [32, 39], + yellow: [33, 39], + blue: [34, 39], + magenta: [35, 39], + cyan: [36, 39], + white: [37, 39], + grey: [90, 39], + + bgBlack: [40, 49], + bgRed: [41, 49], + bgGreen: [42, 49], + bgYellow: [43, 49], + bgBlue: [44, 49], + bgMagenta: [45, 49], + bgCyan: [46, 49], + bgWhite: [47, 49] + } + } + + /** + * Get current formatted timestamp + * @returns {String} + */ + get timestamp () { + const time = new Date().toISOString() + return this.timestamps ? `[${this.styles.grey(time.slice(time.indexOf('T') + 1).replace(/\..+/, ''))}] ` : '' + } + + /** + * Checks if the console supports colours + * @returns {Boolean} + */ + supportsColours () { + if (process.stdout && !process.stdout.isTTY) return false + if (process.platfrom === 'win32') return true + if ('COLORTERM' in process.env) return true + if (process.env.TERM === 'dumb') return false + if (['screen', 'xterm', 'vt100', 'color', 'ansi', 'cygwin', 'linux'].includes(process.env.TERM)) return true + return false + } + + stripColour (str) { + return String(str).replace(/\x1B\[\d+m/g, '') + } +} + +module.exports = Logger diff --git a/src/core/engine/Router.js b/src/core/Router.js similarity index 95% rename from src/core/engine/Router.js rename to src/core/Router.js index a9a1c76..95e5b36 100644 --- a/src/core/engine/Router.js +++ b/src/core/Router.js @@ -1,7 +1,7 @@ const path = require('path') const fs = require('fs') -const { readdirRecursive, isDir, Collection } = require('../../util') +const { readdirRecursive, isDir, Collection } = require('../util') /** * Router class for event routing @@ -55,12 +55,12 @@ class Router extends Collection { case 'object': { if (Array.isArray(modules)) { for (const module of modules) { - this.modules.attach(module) + this.attach(module) } return this } for (const key in modules) { - this.modules.attach(modules[key]) + this.attach(modules[key]) } return this } @@ -100,7 +100,7 @@ class Router extends Collection { * @returns {Router} */ record (event) { - this.bot.on(event, (...args) => { + this._client.on(event, (...args) => { const events = this.events[event] || {} for (const name in events) { const module = this.get(name) diff --git a/src/core/engine/index.js b/src/core/engine/index.js deleted file mode 100644 index 6ba9739..0000000 --- a/src/core/engine/index.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - Bridge: require('./Bridge'), - Commander: require('./Commander'), - Router: require('./Router') -} diff --git a/src/core/index.js b/src/core/index.js index 83b60eb..298f102 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -1,3 +1,7 @@ -module.exports = Object.assign(require('./engine'), { - Client: require('./Client') -}) +module.exports = { + Bridge: require('./Bridge'), + Commander: require('./Commander'), + Interpreter: require('./Interpreter'), + Logger: require('./Logger'), + Router: require('./Router') +} diff --git a/src/index.js b/src/index.js index be7f595..7c5dbf9 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,12 @@ +const Client = require('./Client') const Core = require('./core') -const Client = function Client (opts) { - return new Core.Client(opts) + +function Sylphy (opts) { + return new Client(opts) } -module.exports = Object.assign(Client, Core, require('./util')) +module.exports = Object.assign(Sylphy, Core, + require('./util'), + require('./structures'), + require('./managers') +) diff --git a/src/managers/Resolver.js b/src/managers/Resolver.js index 30d405e..c1af13e 100644 --- a/src/managers/Resolver.js +++ b/src/managers/Resolver.js @@ -1,8 +1,6 @@ const { readdirRecursive, Collection } = require('../util') -/** - * Resolver manager for resolving usages - */ +/** Resolver manager for resolving usages */ class Resolver extends Collection { /** * Creates a new Resolver instance @@ -107,7 +105,7 @@ class Resolver extends Collection { resolve (message, rawArgs, data, rawUsage = this.usage) { let args = {} - const usage = this.verify(usage) + const usage = this.verify(rawUsage) if (!usage.length) return Promise.resolve(args) const argsCount = rawArgs.length @@ -168,7 +166,7 @@ class Resolver extends Collection { if (!resolver) { return Promise.resolve({ err: 'Invalid resolver type' }) } - return resolver.resolve(rawArg, arg, message, this.bot) + return resolver.resolve(rawArg, arg, message, this._client) .catch(err => Object.assign(arg, { arg: `**\`${arg.name || 'argument'}\`**`, err: err diff --git a/src/managers/Responder.js b/src/managers/Responder.js new file mode 100644 index 0000000..a96a20c --- /dev/null +++ b/src/managers/Responder.js @@ -0,0 +1,200 @@ +const { padEnd, emojis } = require('../util') + +class Responder { + constructor (command) { + this.command = command + this.i18n = command.i18n + + /** Response methods */ + this.responseMethods = { + send: (msg, res) => res, + reply: (msg, res) => `**${msg.author.username}**, ${res}`, + success: (msg, res) => `${emojis['white_check_mark']} | **${msg.author.username}**, ${res}`, + error: (msg, res) => `${emojis['negative_squared_cross_mark']} | **${msg.author.username}**, ` + (res || '{{%ERROR}}') + } + + /** Formatter methods */ + this.formatMethods = { + bold: (res) => `**${res}**`, + italic: (res) => `*${res}*`, + underline: (res) => `__${res}__`, + strikethrough: (res) => `~~${res}~~`, + inlineCode: (res) => `\`${res}\``, + code: (res, type = '') => `\`\`\`${type}\n${res}\n\`\`\``, + emoji: (res, type) => `${emojis[type] || emojis['information_source']} | ${res}` + } + } + + create ({ msg: message, settings = {} }, data) { + let responder = Object.assign((...args) => responder.send(...args), { + command: this.command, + message: message, + settings: settings, + data: data, + _options: {}, + responseMethods: this.responseMethods, + formatMethods: this.formatMethods + }) + + const copy = ['_send', 't', 'clean', 'typing', 'format', 'file', 'embed', 'dialog', 'selection'] + copy.forEach(prop => { responder[prop] = this[prop].bind(responder) }) + + for (let method in this.responseMethods) { + responder[method] = responder._send.bind(responder, method) + } + + return responder + } + + t (content = '', tags = {}) { + const cmd = this.command + const file = cmd.name ? cmd.name.split(':')[0] : (cmd.labels ? cmd.labels[0] : 'common') + const res = cmd.i18n.parse(content, cmd.localeKey || file, this.settings.lang || 'en', tags) + return res.replace(/:(\S+):/gi, (matched, name) => emojis[name] || emojis['information_source']) + } + + _send (method, response = '', options = {}) { + const message = this.message + const formats = this._formats || [] + + Object.assign(options, this._options || {}) + + if (response instanceof Array) response = response.join('\n') + response = this.command.t(response, this.settings.lang || 'en', options) + response = this.responseMethods[method || 'send'](message, response) + + for (let format of formats) { + format = format.split(':') + response = this.formatMethods[format[0]](response, format[1]) + } + + const promise = (options.DM ? this.command.bot.getDMChannel(message.author.id) : Promise.resolve(message.channel)) + .then(channel => this.command.send(channel, response, options)) + + delete this._formats + this._options = {} + + return promise + } + + clean () { + delete this._formats + return this + } + + typing () { + return this.message.channel.sendTyping() + } + + format (formats) { + this._formats = (formats instanceof Array) ? formats : [formats] + return this + } + + file (name, file) { + this._options.file = { name, file } + return this + } + + embed (embed) { + this._options.embed = embed + return this + } + + async dialog (dialogs = [], options = {}) { + const { message, data } = this + const { tries = 10, time = 60, matches = 10, filter, cancel = 'cancel' } = options + + const args = {} + let cancelled = false + for (const dialog of dialogs) { + let prompt = dialog.prompt + const input = this.command.resolver + if (Array.isArray(prompt) && prompt.length) { + prompt[0] = `**${prompt[0]}**` + } + + let p1 = await this.send(prompt, options) + const collector = this.command.bot.engine.bridge.collect({ + channel: message.channel.id, + author: message.author.id, + tries, + time, + matches, + filter + }) + + const awaitMessage = async (msg) => { + let ans + try { + ans = await collector.next() + if (ans.content.toLowerCase() === cancel) return Promise.reject() + try { + return await input.resolve(ans, [ans.cleanContent], data, dialog.input) + } catch (err) { + const p2 = await this.format('emoji:error').send( + `${err.err || err.message || err || '{{%menus.ERROR}}'}\n\n{{%menus.EXIT}}`, + Object.assign(err, { cancel: `\`${cancel}\`` }) + ) + return awaitMessage(p2) + } + } catch (o) { + return Promise.reject(o) + } finally { + this.command.deleteMessages(msg, ans) + } + } + + try { + Object.assign(args, await awaitMessage()) + collector.stop() + } catch (err) { + if (typeof err !== 'undefined') { + this.error(`{{%menus.ERRORED}} **{{%collector.${err.reason}}}**`, { + [err.reason]: err.arg, err: `**${err.reason}**` + }) + } else { + this.success('{{%menus.EXITED}}') + } + collector.stop() + cancelled = true + break + } finally { + p1.delete() + } + } + + if (cancelled) return Promise.reject() + return Promise.resolve(args) + } + + async selection (selections = [], options = {}) { + if (!Array.isArray(selections)) return [selections, 0] + if (!selections.length) return [] + if (selections.length === 1) return [selections[0], 0] + + const { title = '{{%menus.SELECTION}}', footer = '{{%menus.INPUT}}', mapFunc } = options + const choices = (mapFunc ? selections.map(mapFunc) : selections).splice(0, 10) + + try { + const { reply } = await this.dialog([{ + prompt: [ + '```markdown', + `### ${title} ###\n`, + choices.map((c, i) => `${padEnd(`[${i + 1}]:`, 4)} ${c}`).join('\n'), + selections.length > 10 ? '{{%menus.MORE_RESULTS}}\n' : '', + Array.isArray(footer) ? footer.join('\n') : '> ' + footer, + '```' + ].join('\n'), + input: { type: 'int', name: 'reply', min: 1, max: choices.length } + }], Object.assign(options, { + num: selections.length - 10, cancel: options.cancel || 'cancel' + })) + return [selections[reply - 1], reply - 1] + } catch (err) { + return [] + } + } +} + +module.exports = Responder diff --git a/src/managers/index.js b/src/managers/index.js new file mode 100644 index 0000000..238e1a8 --- /dev/null +++ b/src/managers/index.js @@ -0,0 +1,5 @@ +module.exports = { + Resolver: require('./Resolver'), + Transmitter: require('./Transmitter'), + Responder: require('./Responder') +} diff --git a/src/middleware/checkPrivate.js b/src/middleware/checkPrivate.js new file mode 100644 index 0000000..90ab0e4 --- /dev/null +++ b/src/middleware/checkPrivate.js @@ -0,0 +1,9 @@ +module.exports = { + name: 'checkPrivate', + priority: 2, + process: container => { + const { msg } = container + container.isPrivate = !msg.guild + return Promise.resolve(container) + } +} diff --git a/src/middleware/commandLogger.js b/src/middleware/commandLogger.js new file mode 100644 index 0000000..83e9430 --- /dev/null +++ b/src/middleware/commandLogger.js @@ -0,0 +1,17 @@ +module.exports = { + name: 'commandLogger', + priority: 100, + process: container => { + const { msg, isPrivate, isCommand, logger } = container + if (!isCommand || !logger) return Promise.resolve() + + const curry = (color) => (str) => logger.styles[color](logger.styles.bold(str)) + + logger.info( + curry('magenta')(!isPrivate ? msg.guild.name : '(in PMs)') + ' > ' + + curry('green')(msg.author.username) + ': ' + + curry('blue')(msg.cleanContent.replace(/\n/g, ' ')) + ) + return Promise.resolve(container) + } +} diff --git a/src/middleware/parseMessage.js b/src/middleware/parseMessage.js new file mode 100644 index 0000000..05fa19c --- /dev/null +++ b/src/middleware/parseMessage.js @@ -0,0 +1,15 @@ +module.exports = { + name: 'parseMessage', + priority: 10, + process: container => { + const { msg, client, commands } = container + + if (!msg.content.startsWith(client.prefix)) return Promise.resolve() + + const rawArgs = msg.content.substring(client.prefix.length).split(' ') + container.trigger = rawArgs[0].toLowerCase() + container.isCommand = commands.has(container.trigger) + container.rawArgs = rawArgs.slice(1).filter(v => v) + return Promise.resolve(container) + } +} diff --git a/src/structures/Base.js b/src/structures/Base.js new file mode 100644 index 0000000..cba761b --- /dev/null +++ b/src/structures/Base.js @@ -0,0 +1,138 @@ +const { delay: promDelay } = require('../util') + +/** + * Built-in base helper class + * @abstract + * @prop {?Logger} logger Logger instance + * @prop {?Interpreter} i18n Interpreter instance + */ +class Base { + /** + * Creates a new Base instance + * @arg {Client} client Client instance + */ + constructor (client) { + this._client = client + this.logger = client.logger + this.i18n = client.plugins.get('i18n') + } + + /** + * Checks if a user has certain permissions in a channel + * @arg {external:"Eris.GuildChannel"} channel The channel to check in + * @arg {external:"Eris.User"} user The user to check + * @arg {...String} perms The permission(s) to check + * @returns {Boolean} + */ + hasPermissions (channel, user, ...perms) { + if (!channel.guild) return true + const member = channel.guild.members.get(user.id) + + if (!perms.every(p => member.permission.has(p))) return false + return perms.every(perm => + !channel.permissionOverwrites.find(p => (member.roles.includes(p.id) || p.id === user.id) && p.json[perm] === false) + ) + } + + /** + * Parses a string for localisation + * @arg {String} content The string to parse + * @arg {String} [lang='en'] The locale name + * @arg {Object} [tags] Additional interpolation tags + */ + t (content = '', lang = 'en', tags = {}) { + if (!this.i18n) return content + const file = this.name ? this.name.split(':')[0] : (this.labels ? this.labels[0] : 'common') + return this.i18n.parse(content, this.localeKey || file || null, lang, tags) + } + + /** + * Sends a message to a channel + * @arg {external:"Eris.GuildChannel"} channel The channel to send the message in + * @arg {String} content The string to send + * @arg {Object} [options] Message options + * @arg {String} [options.lang] Message locale + * @arg {Number} [options.delay=0] Delay to send the message, set to 0 for no delay + * @arg {Number} [options.deleteDelay=0] Delay to delete the message after it's sent, set to 0 for no deletion + * @arg {Object} [options.file] A file object + * @arg {String} [options.file.name] Name of file + * @arg {Buffer} [options.file.file] Buffer containing file data + * @arg {Object} [options.embed] Embed object, see {@link https://discordapp.com/developers/docs/resources/channel#embed-object|official Discord API documentation} + * @returns {Promise.} + */ + async send (chan, content, options = {}) { + const channel = typeof chan === 'string' ? this._client.getChannel(chan) : chan + if (!channel) { + const err = new Error(`Could not send message: Invalid channel - ${chan}`) + if (this.logger) this.logger.error(err) + throw err + } + + let { file = null, lang, delay = 0, deleteDelay = 0, embed } = options + if (channel.guild && !this.hasPermissions(channel, this._client.user, 'sendMessages')) { + const err = new Error('Could not send message: Insufficient permissions') + if (this.logger) this.logger.error(err) + } + + if (delay) await promDelay(delay) + + if (Array.isArray(content)) content = content.join('\n') + if (this.i18n) content = this.t(content, lang, options) + content = content.match(/(.|[\r\n]){1,2000}/g) + + try { + if (!content || !content.length) { + const msg = await channel.createMessage({ embed, content: '' }, file) + return deleteDelay ? promDelay(deleteDelay).then(() => msg.delete()) : msg + } + + let msg + for (const c of content) { + const firstMsg = await channel.createMessage(!msg ? { embed, content: c } : c, !msg ? file : null) + .then(msg => deleteDelay ? promDelay(deleteDelay).then(() => msg.delete()) : msg) + msg = firstMsg + } + + return msg + } catch (err) { + if (this.logger) this.logger.error('Could not send message -', err) + throw err + } + } + + /** + * Edits a message with updated content + * @arg {external:"Eris.Message"} message The message to edit + * @arg {String} content The content to edit the message with + * @arg {Object} [options] Options object + * @arg {String} [options.lang] Locale of content + * @arg {Number} [options.delay] Delay to edit the message + * @returns {Promise} + */ + edit (msg, content, options) { + const { lang, delay = 0 } = options + + if (Array.isArray(content)) content = content.join('\n') + if (this.i18n) content = this.t(content, lang, options) + + return (delay ? promDelay(delay) : Promise.resolve()).then(() => msg.edit(content)) + } + + /** + * Deletes multiple messages if there are sufficient permissions + * @arg {...external:"Eris.Message"} messages The messages to delete + * @returns {Promise} + */ + deleteMessages (...msgs) { + const id = this._client.user.id + return Promise.all(msgs.reduce((arr, msg) => { + if (!msg || !msg.guild) return arr + if (msg.author.id === id || msg.channel.permissionsOf(id).has('manageMessages')) { + arr.push(msg.delete) + } + return arr + }), []) + } +} + +module.exports = Base diff --git a/src/structures/Command.js b/src/structures/Command.js new file mode 100644 index 0000000..691e964 --- /dev/null +++ b/src/structures/Command.js @@ -0,0 +1,182 @@ +const { Collection } = require('../util') +const { Responder, Resolver } = require('../managers') +const Base = require('./Base') + +/** + * Built-in command class + * @extends {Base} + * @abstract + * @prop {Resolver} resolver Command resolver + * @prop {Responder} responder Command responder + * @prop {Collection} subcommands Collection of subcommands + * @prop {Map} timers Map of timer cooldowns + */ +class Command extends Base { + /** + * Creates a new Command instance + * @arg {Client} client Client instance + * @arg {...Object} args Command options + */ + constructor (client, ...args) { + super(client) + if (this.constructor === Command) { + throw new Error('Cannot instantiate abstract Command') + } + + this.resolver = new Resolver(client) + this.responder = new Responder(this) + this.subcommands = new Collection() + + const options = args.reduce((p, c) => Object.assign(c, p), {}) + this._verify(options, ...args) + + this.timers = new Map() + } + + /** + * Verifies the options passed to the constructor + * @arg {Object} args Options passed to the Command constructor + */ + _verify (args = {}) { + const { name, group = 'none', aliases = [], cooldown = 5, usage = [], options = {}, subcommands = {}, subcommand } = args + + this.triggers = typeof name === 'string' + ? [name].concat(aliases) + : (Array.isArray(aliases) && aliases.length > 0 ? aliases : []) + + if (!this.triggers.length) { + throw new Error(`${this.constructor.name} command is not named`) + } + + this.cooldown = cooldown + this.options = options + if (this.options.modOnly) { + this.options.permissions = (this.options.permissions || []).concat('manageGuild') + } + + this.group = group + this.usage = usage + this.localeKey = options.localeKey + this.resolver.load(usage) + + for (const command in subcommands) { + const name = subcommands[command].name || command + for (const alias of [name].concat(subcommands[command].aliases || [])) { + this.subcommands.set(alias, { + name, + usage: subcommands[command].usage || [], + options: subcommands[command].options || {} + }) + } + } + this.subcommand = subcommand + } + + /** + * Checks the validatiy of a command and executes it + * @arg {Container} container Container context + * @returns {Promise} + */ + execute (container) { + const responder = this.responder.create(container) + + let usage = this.usage + let process = 'handle' + + const subcmd = this.subcommand ? this.subcommand : container.rawArgs[0] + const cmd = this.subcommands.get(subcmd) + if (cmd) { + usage = cmd.usage + process = cmd.name + container.rawArgs = container.rawArgs.slice(this.subcommand ? 0 : 1) + container.trigger += ' ' + subcmd + } + + if (!this.check(container, responder, cmd)) return + + return this.resolver.resolve(container.msg, container.rawArgs, { + prefix: (container.settings || {}).prefix || this._client.prefix, + command: container.trigger + }, usage).then((args = {}) => { + container.args = args + return this[process](container, responder) + }, err => responder.error(`{{%errors.${err.message}`)).catch(this.logError.bind(this)) + } + + /** + * Checks if a command is valid, run in `execute()` + * @arg {Container} container Container context + * @arg {Responder} responder Responder instance + * @arg {Command} [subcmd] Subcommand + */ + check ({ msg, isPrivate, admins, client }, responder, subcmd) { + const isAdmin = admins.includes(msg.author.id) + const { guildOnly, permissions = [], botPerms = [] } = subcmd ? subcmd.options : this.options + const adminOnly = (subcmd && subcmd.options.adminOnly) || this.options.adminOnly + + if (adminOnly === true && !isAdmin) { + return false + } + + if (guildOnly === true && isPrivate) { + responder.format('emoji:error').send('{{%errors.NO_PMS}}') + return false + } + + if (permissions.length && !this.hasPermissions(msg.channel, msg.author, ...permissions)) { + responder.error('{{%errors.NO_PERMS}}', { + perms: permissions.map(p => `\`${p}\``).join(', ') + }) + return false + } + + if (botPerms.length && !this.hasPermissions(msg.channel, client.user, ...botPerms)) { + responder.error('{{%errors.NO_PERMS_BOT}}', { + perms: botPerms.map(p => `\`${p}\``).join(', ') + }) + return false + } + + if (isAdmin) return true + const awaitID = msg.author.id + if (this.cooldown > 0) { + const now = Date.now() / 1000 | 0 + if (!this.timers.has(awaitID)) { + this.timers.set(awaitID, now) + } else { + const diff = now - this.timers.get(awaitID) + if (diff < this.cooldown) { + responder.error('{{%errors.ON_COOLDOWN}}', { + delay: 0, + deleteDelay: 5000, + time: `**${this.cooldown - diff}**` + }) + return false + } else { + this.timers.delete(awaitID) + this.timers.set(awaitID, now) + } + } + } + return true + } + + /** + * Command handler + * @arg {Container} container Container object + * @arg {Responder} responder Responder instance + */ + async handle (container, responder) { return true } + + logError (err) { + if (err && this.logger) { + this.logger.error(`Error running ${this.triggers[0]} command`, err) + } + } + + get permissionNode () { + return `${this.group}.${this.triggers[0]}` + } +} + +module.exports = Command diff --git a/src/structures/index.js b/src/structures/index.js new file mode 100644 index 0000000..ea673ba --- /dev/null +++ b/src/structures/index.js @@ -0,0 +1,4 @@ +module.exports = { + Base: require('./Base'), + Command: require('./Command') +} diff --git a/src/util/Utils.js b/src/util/Utils.js index 6eb28a1..91b48a2 100644 --- a/src/util/Utils.js +++ b/src/util/Utils.js @@ -1,5 +1,6 @@ const path = require('path') const fs = require('fs') +const emojis = require('../../res/emoji') const colours = { blue: '#117ea6', @@ -21,7 +22,7 @@ class Utils { * @arg {Client} client Client instance */ constructor (client) { - this._client = client + Utils._client = client } /** @@ -32,6 +33,10 @@ class Utils { return fs.existsSync(fname) ? fs.statSync(fname).isDirectory() : false } + static get emojis () { + return emojis + } + /** * Pads a string on the right if it's shorter than the padding length * @arg {String} [String=''] The string to pad @@ -97,7 +102,7 @@ class Utils { static readdirRecursive (dir) { return fs.readdirSync(dir).reduce((arr, file) => { const filepath = path.join(dir, file) - arr.push(this.isDir(filepath) ? this.readdirRecursive(filepath) : require(filepath)) + arr.push(Utils.isDir(filepath) ? Utils.readdirRecursive(filepath) : require(filepath)) return arr }, []) } @@ -110,10 +115,20 @@ class Utils { static requireAll (dir) { return fs.readdirSync(dir).reduce((obj, file) => { const filepath = path.join(dir, file) - obj[file] = this.isDir(filepath) ? this.requireAll(filepath) : require(filepath) + obj[file.substring(0, path.basename(filepath, path.extname(filepath)).length)] = Utils.isDir(filepath) + ? Utils.requireAll(filepath) : require(filepath) return obj }, {}) } + + /** + * Delay Promise + * @arg {Number} time Time to delay + * @returns {Promise} + */ + static delay (time) { + return new Promise((resolve) => setTimeout(() => resolve(), time)) + } } module.exports = Utils diff --git a/test/bot.js b/test/bot.js index c545839..224507e 100644 --- a/test/bot.js +++ b/test/bot.js @@ -1,11 +1,33 @@ +const util = require('util') const Client = require('../') -const { token } = require('./auth.json') +const { token, admins } = require('./auth.json') const bot = new Client({ - token: token + token: token, + commands: 'test/commands', + locales: 'test/i18n', + prefix: '+', + admins: admins }) -bot.on('ready', () => console.log('running!')) +const logger = bot.logger + +logger.debug([ + 'Running bot\n', + 'With flashy logging' +].join('\n')) +logger.error(new Error('Testing error')) + +bot.on('commander:registered', logger.log) + +bot.on('ready', () => { + logger.info('Logged in as ' + bot.user.username) +}) bot.run() + +process.on('unhandledRejection', (reason, promise) => { + if (typeof reason === 'undefined') return + logger.error(`Unhandled rejection: ${reason} - ${util.inspect(promise)}`) +}) diff --git a/test/commands/EndCommand.js b/test/commands/EndCommand.js new file mode 100644 index 0000000..3d56699 --- /dev/null +++ b/test/commands/EndCommand.js @@ -0,0 +1,16 @@ +const { Command } = require('../../') + +module.exports = class Stop extends Command { + constructor (...args) { + super(...args, { + name: 'stop', + aliases: ['end'], + options: { localeKey: 'test' } + }) + } + + handle ({ msg }, responder) { + this.logger.debug('Ending test') + return responder.reply('{{end}}', { x: 'now' }).then(process.exit) + } +} diff --git a/test/commands/PingCommand.js b/test/commands/PingCommand.js new file mode 100644 index 0000000..de4c561 --- /dev/null +++ b/test/commands/PingCommand.js @@ -0,0 +1,4 @@ +module.exports = { + triggers: ['ping'], + execute: ({ msg }) => msg.channel.createMessage('Pong!') +} diff --git a/test/i18n/en/test.json b/test/i18n/en/test.json new file mode 100644 index 0000000..c8c3b98 --- /dev/null +++ b/test/i18n/en/test.json @@ -0,0 +1,3 @@ +{ + "end": "Ending test {{x}}" +} diff --git a/yarn.lock b/yarn.lock index cc8ea45..834fb2d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18,29 +18,210 @@ ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" -asap@~2.0.3: - version "2.0.5" - resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.5.tgz#522765b50c3510490e52d7dcfe085ef9ba96958f" +archy@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" + +arr-diff@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf" + dependencies: + arr-flatten "^1.0.1" -async@^1.5.2: - version "1.5.2" - resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" +arr-flatten@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.0.1.tgz#e5ffe54d45e19f32f216e91eb99c8ce892bb604b" -async@~1.0.0: +array-differ@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/async/-/async-1.0.0.tgz#f8fc04ca3a13784ade9e1641af98578cfbd647a9" + resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-1.0.0.tgz#eff52e3758249d33be402b8bb8e564bb2b5d4031" + +array-uniq@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" + +array-unique@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" + +babel-code-frame@^6.20.0: + version "6.20.0" + resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.20.0.tgz#b968f839090f9a8bc6d41938fb96cb84f7387b26" + dependencies: + chalk "^1.1.0" + esutils "^2.0.2" + js-tokens "^2.0.0" + +babel-core@^6.0.2, babel-core@^6.18.0: + version "6.21.0" + resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.21.0.tgz#75525480c21c803f826ef3867d22c19f080a3724" + dependencies: + babel-code-frame "^6.20.0" + babel-generator "^6.21.0" + babel-helpers "^6.16.0" + babel-messages "^6.8.0" + babel-register "^6.18.0" + babel-runtime "^6.20.0" + babel-template "^6.16.0" + babel-traverse "^6.21.0" + babel-types "^6.21.0" + babylon "^6.11.0" + convert-source-map "^1.1.0" + debug "^2.1.1" + json5 "^0.5.0" + lodash "^4.2.0" + minimatch "^3.0.2" + path-is-absolute "^1.0.0" + private "^0.1.6" + slash "^1.0.0" + source-map "^0.5.0" + +babel-generator@^6.21.0: + version "6.21.0" + resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.21.0.tgz#605f1269c489a1c75deeca7ea16d43d4656c8494" + dependencies: + babel-messages "^6.8.0" + babel-runtime "^6.20.0" + babel-types "^6.21.0" + detect-indent "^4.0.0" + jsesc "^1.3.0" + lodash "^4.2.0" + source-map "^0.5.0" + +babel-helper-function-name@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.18.0.tgz#68ec71aeba1f3e28b2a6f0730190b754a9bf30e6" + dependencies: + babel-helper-get-function-arity "^6.18.0" + babel-runtime "^6.0.0" + babel-template "^6.8.0" + babel-traverse "^6.18.0" + babel-types "^6.18.0" + +babel-helper-get-function-arity@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.18.0.tgz#a5b19695fd3f9cdfc328398b47dafcd7094f9f24" + dependencies: + babel-runtime "^6.0.0" + babel-types "^6.18.0" + +babel-helper-remap-async-to-generator@^6.16.0: + version "6.20.3" + resolved "https://registry.yarnpkg.com/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.20.3.tgz#9dd3b396f13e35ef63e538098500adc24c63c4e7" + dependencies: + babel-helper-function-name "^6.18.0" + babel-runtime "^6.20.0" + babel-template "^6.16.0" + babel-traverse "^6.20.0" + babel-types "^6.20.0" + +babel-helpers@^6.16.0: + version "6.16.0" + resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.16.0.tgz#1095ec10d99279460553e67eb3eee9973d3867e3" + dependencies: + babel-runtime "^6.0.0" + babel-template "^6.16.0" + +babel-messages@^6.8.0: + version "6.8.0" + resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.8.0.tgz#bf504736ca967e6d65ef0adb5a2a5f947c8e0eb9" + dependencies: + babel-runtime "^6.0.0" + +babel-plugin-syntax-async-functions@^6.8.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95" -bindings@~1.2.1, bindings@1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.2.1.tgz#14ad6113812d2d37d72e67b4cacb4bb726505f11" +babel-plugin-transform-async-to-generator@^6.16.0: + version "6.16.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.16.0.tgz#19ec36cb1486b59f9f468adfa42ce13908ca2999" + dependencies: + babel-helper-remap-async-to-generator "^6.16.0" + babel-plugin-syntax-async-functions "^6.8.0" + babel-runtime "^6.0.0" + +babel-register@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.18.0.tgz#892e2e03865078dd90ad2c715111ec4449b32a68" + dependencies: + babel-core "^6.18.0" + babel-runtime "^6.11.6" + core-js "^2.4.0" + home-or-tmp "^2.0.0" + lodash "^4.2.0" + mkdirp "^0.5.1" + source-map-support "^0.4.2" + +babel-runtime@^6.0.0, babel-runtime@^6.11.6, babel-runtime@^6.20.0, babel-runtime@^6.9.0: + version "6.20.0" + resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.20.0.tgz#87300bdcf4cd770f09bf0048c64204e17806d16f" + dependencies: + core-js "^2.4.0" + regenerator-runtime "^0.10.0" + +babel-template@^6.16.0, babel-template@^6.8.0: + version "6.16.0" + resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.16.0.tgz#e149dd1a9f03a35f817ddbc4d0481988e7ebc8ca" + dependencies: + babel-runtime "^6.9.0" + babel-traverse "^6.16.0" + babel-types "^6.16.0" + babylon "^6.11.0" + lodash "^4.2.0" + +babel-traverse@^6.16.0, babel-traverse@^6.18.0, babel-traverse@^6.20.0, babel-traverse@^6.21.0: + version "6.21.0" + resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.21.0.tgz#69c6365804f1a4f69eb1213f85b00a818b8c21ad" + dependencies: + babel-code-frame "^6.20.0" + babel-messages "^6.8.0" + babel-runtime "^6.20.0" + babel-types "^6.21.0" + babylon "^6.11.0" + debug "^2.2.0" + globals "^9.0.0" + invariant "^2.2.0" + lodash "^4.2.0" + +babel-types@^6.16.0, babel-types@^6.18.0, babel-types@^6.20.0, babel-types@^6.21.0: + version "6.21.0" + resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.21.0.tgz#314b92168891ef6d3806b7f7a917fdf87c11a4b2" + dependencies: + babel-runtime "^6.20.0" + esutils "^2.0.2" + lodash "^4.2.0" + to-fast-properties "^1.0.1" + +babylon@^6.11.0: + version "6.14.1" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.14.1.tgz#956275fab72753ad9b3435d7afe58f8bf0a29815" -bluebird@^3.4.7, "bluebird@>= 3.0.1", bluebird@~3.4.6: +balanced-match@^0.4.1: + version "0.4.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" + +beeper@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/beeper/-/beeper-1.1.1.tgz#e6d5ea8c5dad001304a70b22638447f69cb2f809" + +bluebird@~3.4.6: version "3.4.7" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3" -bluebird@~2.10.2: - version "2.10.2" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.10.2.tgz#024a5517295308857f14f91f1106fc3b555f446b" +brace-expansion@^1.0.0: + version "1.1.6" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.6.tgz#7197d7eaa9b87e648390ea61fc66c84427420df9" + dependencies: + balanced-match "^0.4.1" + concat-map "0.0.1" + +braces@^1.8.2: + version "1.8.5" + resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7" + dependencies: + expand-range "^1.8.1" + preserve "^0.2.0" + repeat-element "^1.1.2" buffer-shims@^1.0.0: version "1.0.0" @@ -52,7 +233,7 @@ catharsis@~0.8.8: dependencies: underscore-contrib "~0.3.0" -chalk@^1.1.3: +chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" dependencies: @@ -62,78 +243,85 @@ chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" -cluster@^0.7.7: - version "0.7.7" - resolved "https://registry.yarnpkg.com/cluster/-/cluster-0.7.7.tgz#e497e267cc956bd0b0513adb4aa393357d0085ef" - dependencies: - log ">= 1.2.0" - mkdirp ">= 0.0.1" - -colors@1.0.x: - version "1.0.3" - resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" +clone-stats@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-0.0.1.tgz#b88f94a82cf38b8791d58046ea4029ad88ca99d1" -combined-stream@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" - dependencies: - delayed-stream "~1.0.0" +clone@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/clone/-/clone-0.2.0.tgz#c6126a90ad4f72dbf5acdb243cc37724fe93fc1f" -commander@0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-0.6.1.tgz#fa68a14f6a945d54dbbe50d8cdb3320e9e3b1a06" +clone@^1.0.0, clone@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.2.tgz#260b7a99ebb1edfe247538175f783243cb19d149" -commander@2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.3.0.tgz#fd430e889832ec353b9acd1de217c11cb3eef873" +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" -component-emitter@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" +convert-source-map@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.3.0.tgz#e9f3e9c6e2728efc2676696a70eb382f73106a67" -cookiejar@^2.0.6: - version "2.1.0" - resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.0.tgz#86549689539b6d0e269b6637a304be508194d898" +core-js@^2.4.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e" 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" -cycle@1.0.x: - version "1.0.3" - resolved "https://registry.yarnpkg.com/cycle/-/cycle-1.0.3.tgz#21e80b2be8580f98b468f379430662b046c34ad2" +dateformat@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-2.0.0.tgz#2743e3abb5c3fc2462e527dca445e04e9f4dee17" -debug@^2.2.0, debug@2: - version "2.5.2" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.5.2.tgz#50c295a53dbf1657146e0c1b21307275e90d49cb" +debug@^2.1.1, debug@^2.2.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.0.tgz#bc596bcabe7617f11d9fa15361eded5608b8499b" dependencies: ms "0.7.2" -debug@2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" +defaults@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d" dependencies: - ms "0.7.1" + clone "^1.0.2" -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" +deprecated@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/deprecated/-/deprecated-0.0.1.tgz#f9c9af5464afa1e7a971458a8bdef2aa94d5bb19" -diff@1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-1.4.0.tgz#7f28d2eb9ee7b15a97efd89ce63dcfdaa3ccbabf" +detect-file@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-0.1.0.tgz#4935dedfd9488648e006b0129566e9386711ea63" + dependencies: + fs-exists-sync "^0.1.0" + +detect-indent@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" + dependencies: + repeating "^2.0.0" docdash@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/docdash/-/docdash-0.4.0.tgz#05c3a50d83189981699ee0c076d3a3950db7ec00" -double-ended-queue@^2.1.0-0: - version "2.1.0-0" - resolved "https://registry.yarnpkg.com/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz#103d3527fd31528f40188130c841efdd78264e5c" +duplexer2@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.0.2.tgz#c614dcf67e2fb14995a91711e5a617e8a60a31db" + dependencies: + readable-stream "~1.1.9" + +end-of-stream@~0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-0.1.5.tgz#8e177206c3c80837d85632e8b9359dfe8b2f6eaf" + dependencies: + once "~1.3.0" "eris@github:abalabahaha/eris#dev": version "0.5.1" - resolved "https://codeload.github.com/abalabahaha/eris/tar.gz/ef1e706d2f7ce9f9bd12b7f36786e0825ee04f07" + resolved "https://codeload.github.com/abalabahaha/eris/tar.gz/074364ad4cf41aba57a9d73fee10bc316d35afbb" dependencies: ws "^1.1.1" optionalDependencies: @@ -144,10 +332,6 @@ escape-string-regexp@^1.0.2, 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" -escape-string-regexp@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.2.tgz#4dbc2fe674e71949caf3fb2695ce7f2dc1d9a8d1" - espree@~3.1.7: version "3.1.7" resolved "https://registry.yarnpkg.com/espree/-/espree-3.1.7.tgz#fd5deec76a97a5120a9cd3a7cb1177a0923b11d2" @@ -155,44 +339,270 @@ espree@~3.1.7: acorn "^3.3.0" acorn-jsx "^3.0.0" -eventemitter3@^2.0.2: +esutils@^2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-2.0.2.tgz#20ce4891909ce9f35b088c94fab40e2c96f473ac" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" + +expand-brackets@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b" + dependencies: + is-posix-bracket "^0.1.0" + +expand-range@^1.8.1: + version "1.8.2" + resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337" + dependencies: + fill-range "^2.1.0" + +expand-tilde@^1.2.1, expand-tilde@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-1.2.2.tgz#0b81eba897e5a3d31d1c3d102f8f01441e559449" + dependencies: + os-homedir "^1.0.1" extend@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.0.tgz#5a474353b9f3353ddd8176dfd37b91c83a46f1d4" -eyes@0.1.x: - version "0.1.8" - resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0" +extglob@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" + dependencies: + is-extglob "^1.0.0" + +fancy-log@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/fancy-log/-/fancy-log-1.3.0.tgz#45be17d02bb9917d60ccffd4995c999e6c8c9948" + dependencies: + chalk "^1.1.1" + time-stamp "^1.0.0" + +filename-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.0.tgz#996e3e80479b98b9897f15a8a58b3d084e926775" + +fill-range@^2.1.0: + version "2.2.3" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.3.tgz#50b77dfd7e469bc7492470963699fe7a8485a723" + dependencies: + is-number "^2.1.0" + isobject "^2.0.0" + randomatic "^1.1.3" + repeat-element "^1.1.2" + repeat-string "^1.5.2" + +find-index@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/find-index/-/find-index-0.1.1.tgz#675d358b2ca3892d795a1ab47232f8b6e2e0dde4" + +findup-sync@^0.4.2: + version "0.4.3" + resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-0.4.3.tgz#40043929e7bc60adf0b7f4827c4c6e75a0deca12" + dependencies: + detect-file "^0.1.0" + is-glob "^2.0.1" + micromatch "^2.3.7" + resolve-dir "^0.1.0" -form-data@1.0.0-rc4: - version "1.0.0-rc4" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-1.0.0-rc4.tgz#05ac6bc22227b43e4461f488161554699d4f8b5e" +fined@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/fined/-/fined-1.0.2.tgz#5b28424b760d7598960b7ef8480dff8ad3660e97" dependencies: - async "^1.5.2" - combined-stream "^1.0.5" - mime-types "^2.1.10" + expand-tilde "^1.2.1" + lodash.assignwith "^4.0.7" + lodash.isempty "^4.2.1" + lodash.isplainobject "^4.0.4" + lodash.isstring "^4.0.1" + lodash.pick "^4.2.1" + parse-filepath "^1.0.1" + +first-chunk-stream@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/first-chunk-stream/-/first-chunk-stream-1.0.0.tgz#59bfb50cd905f60d7c394cd3d9acaab4e6ad934e" -formidable@^1.0.17: - version "1.0.17" - resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.0.17.tgz#ef5491490f9433b705faa77249c99029ae348559" +flagged-respawn@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/flagged-respawn/-/flagged-respawn-0.3.2.tgz#ff191eddcd7088a675b2610fffc976be9b8074b5" + +for-in@^0.1.5: + version "0.1.6" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.6.tgz#c9f96e89bfad18a545af5ec3ed352a1d9e5b4dc8" + +for-own@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.4.tgz#0149b41a39088c7515f51ebe1c1386d45f935072" + dependencies: + for-in "^0.1.5" + +fs-exists-sync@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz#982d6893af918e72d08dec9e8673ff2b5a8d6add" -glob@3.2.11: - version "3.2.11" - resolved "https://registry.yarnpkg.com/glob/-/glob-3.2.11.tgz#4a973f635b9190f715d10987d5c00fd2815ebe3d" +gaze@^0.5.1: + version "0.5.2" + resolved "https://registry.yarnpkg.com/gaze/-/gaze-0.5.2.tgz#40b709537d24d1d45767db5a908689dfe69ac44f" dependencies: + globule "~0.1.0" + +glob-base@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" + dependencies: + glob-parent "^2.0.0" + is-glob "^2.0.0" + +glob-parent@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28" + dependencies: + is-glob "^2.0.0" + +glob-stream@^3.1.5: + version "3.1.18" + resolved "https://registry.yarnpkg.com/glob-stream/-/glob-stream-3.1.18.tgz#9170a5f12b790306fdfe598f313f8f7954fd143b" + dependencies: + glob "^4.3.1" + glob2base "^0.0.12" + minimatch "^2.0.1" + ordered-read-streams "^0.1.0" + through2 "^0.6.1" + unique-stream "^1.0.0" + +glob-watcher@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/glob-watcher/-/glob-watcher-0.0.6.tgz#b95b4a8df74b39c83298b0c05c978b4d9a3b710b" + dependencies: + gaze "^0.5.1" + +glob@^4.3.1: + version "4.5.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-4.5.3.tgz#c6cb73d3226c1efef04de3c56d012f03377ee15f" + dependencies: + inflight "^1.0.4" inherits "2" - minimatch "0.3" + minimatch "^2.0.1" + once "^1.3.0" + +glob@~3.1.21: + version "3.1.21" + resolved "https://registry.yarnpkg.com/glob/-/glob-3.1.21.tgz#d29e0a055dea5138f4d07ed40e8982e83c2066cd" + dependencies: + graceful-fs "~1.2.0" + inherits "1" + minimatch "~0.2.11" + +glob2base@^0.0.12: + version "0.0.12" + resolved "https://registry.yarnpkg.com/glob2base/-/glob2base-0.0.12.tgz#9d419b3e28f12e83a362164a277055922c9c0d56" + dependencies: + find-index "^0.1.1" + +global-modules@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-0.2.3.tgz#ea5a3bed42c6d6ce995a4f8a1269b5dae223828d" + dependencies: + global-prefix "^0.1.4" + is-windows "^0.2.0" + +global-prefix@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-0.1.5.tgz#8d3bc6b8da3ca8112a160d8d496ff0462bfef78f" + dependencies: + homedir-polyfill "^1.0.0" + ini "^1.3.4" + is-windows "^0.2.0" + which "^1.2.12" + +globals@^9.0.0: + version "9.14.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-9.14.0.tgz#8859936af0038741263053b39d0e76ca241e4034" + +globule@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/globule/-/globule-0.1.0.tgz#d9c8edde1da79d125a151b79533b978676346ae5" + dependencies: + glob "~3.1.21" + lodash "~1.0.1" + minimatch "~0.2.11" + +glogg@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/glogg/-/glogg-1.0.0.tgz#7fe0f199f57ac906cf512feead8f90ee4a284fc5" + dependencies: + sparkles "^1.0.0" + +graceful-fs@^3.0.0: + version "3.0.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-3.0.11.tgz#7613c778a1afea62f25c630a086d7f3acbbdd818" + dependencies: + natives "^1.1.0" graceful-fs@^4.1.9: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" -growl@1.9.2: - version "1.9.2" - resolved "https://registry.yarnpkg.com/growl/-/growl-1.9.2.tgz#0ea7743715db8d8de2c5ede1775e1b45ac85c02f" +graceful-fs@~1.2.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-1.2.3.tgz#15a4806a57547cb2d2dbf27f42e89a8c3451b364" + +gulp-babel@^6.1.2: + version "6.1.2" + resolved "https://registry.yarnpkg.com/gulp-babel/-/gulp-babel-6.1.2.tgz#7c0176e4ba3f244c60588a0c4b320a45d1adefce" + dependencies: + babel-core "^6.0.2" + gulp-util "^3.0.0" + object-assign "^4.0.1" + replace-ext "0.0.1" + through2 "^2.0.0" + vinyl-sourcemaps-apply "^0.2.0" + +gulp-util@^3.0.0: + version "3.0.8" + resolved "https://registry.yarnpkg.com/gulp-util/-/gulp-util-3.0.8.tgz#0054e1e744502e27c04c187c3ecc505dd54bbb4f" + dependencies: + array-differ "^1.0.0" + array-uniq "^1.0.2" + beeper "^1.0.0" + chalk "^1.0.0" + dateformat "^2.0.0" + fancy-log "^1.1.0" + gulplog "^1.0.0" + has-gulplog "^0.1.0" + lodash._reescape "^3.0.0" + lodash._reevaluate "^3.0.0" + lodash._reinterpolate "^3.0.0" + lodash.template "^3.0.0" + minimist "^1.1.0" + multipipe "^0.1.2" + object-assign "^3.0.0" + replace-ext "0.0.1" + through2 "^2.0.0" + vinyl "^0.5.0" + +gulp@^3.9.1: + version "3.9.1" + resolved "https://registry.yarnpkg.com/gulp/-/gulp-3.9.1.tgz#571ce45928dd40af6514fc4011866016c13845b4" + dependencies: + archy "^1.0.0" + chalk "^1.0.0" + deprecated "^0.0.1" + gulp-util "^3.0.0" + interpret "^1.0.0" + liftoff "^2.1.0" + minimist "^1.1.0" + orchestrator "^0.3.0" + pretty-hrtime "^1.0.0" + semver "^4.1.0" + tildify "^1.0.0" + v8flags "^2.0.2" + vinyl-fs "^0.3.0" + +gulplog@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/gulplog/-/gulplog-1.0.0.tgz#e28c4d45d05ecbbed818363ce8f9c5926229ffe5" + dependencies: + glogg "^1.0.0" has-ansi@^2.0.0: version "2.0.0" @@ -200,24 +610,150 @@ has-ansi@^2.0.0: dependencies: ansi-regex "^2.0.0" +has-gulplog@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/has-gulplog/-/has-gulplog-0.1.0.tgz#6414c82913697da51590397dafb12f22967811ce" + dependencies: + sparkles "^1.0.0" + +home-or-tmp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.1" + +homedir-polyfill@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz#4c2bbc8a758998feebf5ed68580f76d46768b4bc" + dependencies: + parse-passwd "^1.0.0" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + dependencies: + once "^1.3.0" + wrappy "1" + inherits@~2.0.1, inherits@2: version "2.0.3" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" -isarray@~1.0.0: +inherits@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-1.0.2.tgz#ca4309dadee6b54cc0b8d247e8d7c7a0975bdc9b" + +ini@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e" + +interpret@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.1.tgz#d579fb7f693b858004947af39fa0db49f795602c" + +invariant@^2.2.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360" + dependencies: + loose-envify "^1.0.0" + +is-absolute@^0.2.3: + version "0.2.6" + resolved "https://registry.yarnpkg.com/is-absolute/-/is-absolute-0.2.6.tgz#20de69f3db942ef2d87b9c2da36f172235b1b5eb" + dependencies: + is-relative "^0.2.1" + is-windows "^0.2.0" + +is-buffer@^1.0.2: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.4.tgz#cfc86ccd5dc5a52fa80489111c6920c457e2d98b" + +is-dotfile@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.2.tgz#2c132383f39199f8edc268ca01b9b007d205cc4d" + +is-equal-shallow@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534" + dependencies: + is-primitive "^2.0.0" + +is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + +is-extglob@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" -isstream@0.1.x: +is-finite@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" + dependencies: + number-is-nan "^1.0.0" + +is-glob@^2.0.0, is-glob@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863" + dependencies: + is-extglob "^1.0.0" + +is-number@^2.0.2, is-number@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" + dependencies: + kind-of "^3.0.2" + +is-posix-bracket@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4" + +is-primitive@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575" + +is-relative@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-0.2.1.tgz#d27f4c7d516d175fb610db84bbeef23c3bc97aa5" + dependencies: + is-unc-path "^0.1.1" + +is-unc-path@^0.1.1: version "0.1.2" - resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + resolved "https://registry.yarnpkg.com/is-unc-path/-/is-unc-path-0.1.2.tgz#6ab053a72573c10250ff416a3814c35178af39b9" + dependencies: + unc-path-regex "^0.1.0" + +is-utf8@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" -jade@0.26.3: - version "0.26.3" - resolved "https://registry.yarnpkg.com/jade/-/jade-0.26.3.tgz#8f10d7977d8d79f2f6ff862a81b0513ccb25686c" +is-windows@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-0.2.0.tgz#de1aa6d63ea29dd248737b69f1ff8b8002d2108c" + +isarray@~1.0.0, isarray@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + +isexe@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-1.1.2.tgz#36f3e22e60750920f5e7241a476a8c6a42275ad0" + +isobject@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" dependencies: - commander "0.6.1" - mkdirp "0.3.0" + isarray "1.0.0" + +js-tokens@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-2.0.0.tgz#79903f5563ee778cc1162e6dcf1a0027c97f9cb5" js2xmlparser@~1.0.0: version "1.0.0" @@ -244,113 +780,272 @@ jsdoc@^3.4.3: taffydb "2.6.2" underscore "~1.8.3" +jsesc@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" + +json5@^0.5.0: + version "0.5.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" + +kind-of@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.1.0.tgz#475d698a5e49ff5e53d14e3e732429dc8bf4cf47" + dependencies: + is-buffer "^1.0.2" + klaw@~1.3.0: version "1.3.1" resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439" optionalDependencies: graceful-fs "^4.1.9" -"log@>= 1.2.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/log/-/log-1.4.0.tgz#4ba1d890fde249b031dca03bc37eaaf325656f1c" +liftoff@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/liftoff/-/liftoff-2.3.0.tgz#a98f2ff67183d8ba7cfaca10548bd7ff0550b385" + dependencies: + extend "^3.0.0" + findup-sync "^0.4.2" + fined "^1.0.1" + flagged-respawn "^0.3.2" + lodash.isplainobject "^4.0.4" + lodash.isstring "^4.0.1" + lodash.mapvalues "^4.4.0" + rechoir "^0.6.2" + resolve "^1.1.7" + +lodash._basecopy@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36" + +lodash._basetostring@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash._basetostring/-/lodash._basetostring-3.0.1.tgz#d1861d877f824a52f669832dcaf3ee15566a07d5" + +lodash._basevalues@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._basevalues/-/lodash._basevalues-3.0.0.tgz#5b775762802bde3d3297503e26300820fdf661b7" + +lodash._getnative@^3.0.0: + version "3.9.1" + resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" + +lodash._isiterateecall@^3.0.0: + version "3.0.9" + resolved "https://registry.yarnpkg.com/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz#5203ad7ba425fae842460e696db9cf3e6aac057c" + +lodash._reescape@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._reescape/-/lodash._reescape-3.0.0.tgz#2b1d6f5dfe07c8a355753e5f27fac7f1cde1616a" + +lodash._reevaluate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._reevaluate/-/lodash._reevaluate-3.0.0.tgz#58bc74c40664953ae0b124d806996daca431e2ed" + +lodash._reinterpolate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" + +lodash._root@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash._root/-/lodash._root-3.0.1.tgz#fba1c4524c19ee9a5f8136b4609f017cf4ded692" + +lodash.assignwith@^4.0.7: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.assignwith/-/lodash.assignwith-4.2.0.tgz#127a97f02adc41751a954d24b0de17e100e038eb" + +lodash.escape@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-3.2.0.tgz#995ee0dc18c1b48cc92effae71a10aab5b487698" + dependencies: + lodash._root "^3.0.0" + +lodash.isarguments@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" + +lodash.isarray@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" + +lodash.isempty@^4.2.1: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.isempty/-/lodash.isempty-4.4.0.tgz#6f86cbedd8be4ec987be9aaf33c9684db1b31e7e" + +lodash.isplainobject@^4.0.4: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + +lodash.keys@^3.0.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a" + dependencies: + lodash._getnative "^3.0.0" + lodash.isarguments "^3.0.0" + lodash.isarray "^3.0.0" + +lodash.mapvalues@^4.4.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz#1bafa5005de9dd6f4f26668c30ca37230cc9689c" + +lodash.pick@^4.2.1: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" + +lodash.restparam@^3.0.0: + version "3.6.1" + resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" + +lodash.template@^3.0.0: + version "3.6.2" + resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-3.6.2.tgz#f8cdecc6169a255be9098ae8b0c53d378931d14f" + dependencies: + lodash._basecopy "^3.0.0" + lodash._basetostring "^3.0.0" + lodash._basevalues "^3.0.0" + lodash._isiterateecall "^3.0.0" + lodash._reinterpolate "^3.0.0" + lodash.escape "^3.0.0" + lodash.keys "^3.0.0" + lodash.restparam "^3.0.0" + lodash.templatesettings "^3.0.0" + +lodash.templatesettings@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz#fb307844753b66b9f1afa54e262c745307dba8e5" + dependencies: + lodash._reinterpolate "^3.0.0" + lodash.escape "^3.0.0" + +lodash@^4.2.0: + version "4.17.4" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" + +lodash@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-1.0.2.tgz#8f57560c83b59fc270bd3d561b690043430e2551" + +loose-envify@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.0.tgz#6b26248c42f6d4fa4b0d8542f78edfcde35642a8" + dependencies: + js-tokens "^2.0.0" lru-cache@2: version "2.7.3" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952" +map-cache@^0.2.0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" + marked@~0.3.6: version "0.3.6" resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.6.tgz#b2c6c618fccece4ef86c4fc6cb8a7cbf5aeda8d7" -methods@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" - -mime-db@~1.25.0: - version "1.25.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.25.0.tgz#c18dbd7c73a5dbf6f44a024dc0d165a1e7b1c392" - -mime-types@^2.1.10: - version "2.1.13" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.13.tgz#e07aaa9c6c6b9a7ca3012c69003ad25a39e92a88" +micromatch@^2.3.7: + version "2.3.11" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" + dependencies: + arr-diff "^2.0.0" + array-unique "^0.2.1" + braces "^1.8.2" + expand-brackets "^0.1.4" + extglob "^0.3.1" + filename-regex "^2.0.0" + is-extglob "^1.0.0" + is-glob "^2.0.1" + kind-of "^3.0.2" + normalize-path "^2.0.1" + object.omit "^2.0.0" + parse-glob "^3.0.4" + regex-cache "^0.4.2" + +minimatch@^2.0.1: + version "2.0.10" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-2.0.10.tgz#8d087c39c6b38c001b97fca7ce6d0e1e80afbac7" dependencies: - mime-db "~1.25.0" + brace-expansion "^1.0.0" -mime@^1.3.4: - version "1.3.4" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" +minimatch@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.3.tgz#2a4e4090b96b2db06a9d7df01055a62a77c9b774" + dependencies: + brace-expansion "^1.0.0" -minimatch@0.3: - version "0.3.0" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-0.3.0.tgz#275d8edaac4f1bb3326472089e7949c8394699dd" +minimatch@~0.2.11: + version "0.2.14" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-0.2.14.tgz#c74e780574f63c6f9a090e90efbe6ef53a6a756a" dependencies: lru-cache "2" sigmund "~1.0.0" +minimist@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + minimist@0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" -"mkdirp@>= 0.0.1", mkdirp@~0.5.1, mkdirp@0.5.1: +mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" dependencies: minimist "0.0.8" -mkdirp@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.0.tgz#1bbf5ab1ba827af23575143490426455f481fe1e" +ms@0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765" -mocha@^2.2.5: - version "2.5.3" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-2.5.3.tgz#161be5bdeb496771eb9b35745050b622b5aefc58" +multipipe@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/multipipe/-/multipipe-0.1.2.tgz#2a8f2ddf70eed564dff2d57f1e1a137d9f05078b" dependencies: - commander "2.3.0" - debug "2.2.0" - diff "1.4.0" - escape-string-regexp "1.0.2" - glob "3.2.11" - growl "1.9.2" - jade "0.26.3" - mkdirp "0.5.1" - supports-color "1.2.0" - to-iso-string "0.0.2" + duplexer2 "0.0.2" -moment-timezone@^0.5.5: - version "0.5.11" - resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.11.tgz#9b76c03d8ef514c7e4249a7bbce649eed39ef29f" - dependencies: - moment ">= 2.6.0" +natives@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/natives/-/natives-1.1.0.tgz#e9ff841418a6b2ec7a495e939984f78f163e6e31" -moment@^2.13.0, "moment@>= 2.6.0": - version "2.17.1" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.17.1.tgz#fed9506063f36b10f066c8b59a144d7faebe1d82" +normalize-path@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.0.1.tgz#47886ac1662760d4261b7d979d241709d3ce3f7a" -ms@0.7.1: - version "0.7.1" - resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" -ms@0.7.2: - version "0.7.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765" +object-assign@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-3.0.0.tgz#9bedd5ca0897949bca47e7ff408062d549f587f2" -nan@^2.3.2, nan@2: - version "2.5.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.5.0.tgz#aa8f1e34531d807e9e27755b234b4a6ec0c152a8" +object-assign@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.0.tgz#7a3b3d0e98063d43f4c03f2e8ae6cd51a86883a0" -node-opus@^0.2.4: - version "0.2.4" - resolved "https://registry.yarnpkg.com/node-opus/-/node-opus-0.2.4.tgz#79fb35cf0e7ad04cfb9398eb362aba0774e30a61" +object.omit@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" dependencies: - bindings "~1.2.1" - nan "^2.3.2" - ogg-packet "^1.0.0" + for-own "^0.1.4" + is-extendable "^0.1.1" -ogg-packet@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/ogg-packet/-/ogg-packet-1.0.0.tgz#45b885721ac8f7dd5cf22391d42106ae533ac678" +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + dependencies: + wrappy "1" + +once@~1.3.0: + version "1.3.3" + resolved "https://registry.yarnpkg.com/once/-/once-1.3.3.tgz#b2e261557ce4c314ec8304f3fa82663e4297ca20" dependencies: - ref-struct "*" + wrappy "1" options@>=0.0.5: version "0.0.6" @@ -360,25 +1055,85 @@ opusscript@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/opusscript/-/opusscript-0.0.1.tgz#c8f61d4301b2942ee9ddf68b075b3e373b7943dd" -pkginfo@0.3.x: - version "0.3.1" - resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.3.1.tgz#5b29f6a81f70717142e09e765bbeab97b4f81e21" +orchestrator@^0.3.0: + version "0.3.8" + resolved "https://registry.yarnpkg.com/orchestrator/-/orchestrator-0.3.8.tgz#14e7e9e2764f7315fbac184e506c7aa6df94ad7e" + dependencies: + end-of-stream "~0.1.5" + sequencify "~0.0.7" + stream-consume "~0.1.0" + +ordered-read-streams@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/ordered-read-streams/-/ordered-read-streams-0.1.0.tgz#fd565a9af8eb4473ba69b6ed8a34352cb552f126" + +os-homedir@^1.0.0, os-homedir@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + +os-tmpdir@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + +parse-filepath@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parse-filepath/-/parse-filepath-1.0.1.tgz#159d6155d43904d16c10ef698911da1e91969b73" + dependencies: + is-absolute "^0.2.3" + map-cache "^0.2.0" + path-root "^0.1.1" + +parse-glob@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" + dependencies: + glob-base "^0.3.0" + is-dotfile "^1.0.0" + is-extglob "^1.0.0" + is-glob "^2.0.0" + +parse-passwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" + +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" + +path-root-regex@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/path-root-regex/-/path-root-regex-0.1.2.tgz#bfccdc8df5b12dc52c8b43ec38d18d72c04ba96d" + +path-root@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/path-root/-/path-root-0.1.1.tgz#9a4a6814cac1c0cd73360a95f32083c8ea4745b7" + dependencies: + path-root-regex "^0.1.0" + +preserve@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" + +pretty-hrtime@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" + +private@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/private/-/private-0.1.6.tgz#55c6a976d0f9bafb9924851350fe47b9b5fbb7c1" process-nextick-args@~1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" -promise@^7.0.3: - version "7.1.1" - resolved "https://registry.yarnpkg.com/promise/-/promise-7.1.1.tgz#489654c692616b8aa55b0724fa809bb7db49c5bf" +randomatic@^1.1.3: + version "1.1.6" + resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.6.tgz#110dcabff397e9dcff7c0789ccc0a49adf1ec5bb" dependencies: - asap "~2.0.3" - -qs@^6.1.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.0.tgz#f403b264f23bc01228c74131b407f18d5ea5d442" + is-number "^2.0.2" + kind-of "^3.0.2" -readable-stream@^2.0.5: +readable-stream@^2.1.5: version "2.2.2" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.2.tgz#a9e6fec3c7dda85f8bb1b3ba7028604556fc825e" dependencies: @@ -390,40 +1145,58 @@ readable-stream@^2.0.5: string_decoder "~0.10.x" util-deprecate "~1.0.1" -redis-commands@^1.2.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.3.0.tgz#4307d8094aee1315829ab6729b37b99f62365d63" +"readable-stream@>=1.0.33-1 <1.1.0-0": + version "1.0.34" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" -redis-parser@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-2.3.0.tgz#313a47965e49ee35ab3a86c93388b403d76237f6" +readable-stream@~1.1.9: + version "1.1.14" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" -redis@^2.6.1: - version "2.6.3" - resolved "https://registry.yarnpkg.com/redis/-/redis-2.6.3.tgz#84305b92553c6a1f09c7c47c30b11ace7dbb7ad4" +rechoir@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" dependencies: - double-ended-queue "^2.1.0-0" - redis-commands "^1.2.0" - redis-parser "^2.0.0" + resolve "^1.1.6" -ref-struct@*: - version "1.1.0" - resolved "https://registry.yarnpkg.com/ref-struct/-/ref-struct-1.1.0.tgz#5d5ee65ad41cefc3a5c5feb40587261e479edc13" +regenerator-runtime@^0.10.0: + version "0.10.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.1.tgz#257f41961ce44558b18f7814af48c17559f9faeb" + +regex-cache@^0.4.2: + version "0.4.3" + resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.3.tgz#9b1a6c35d4d0dfcef5711ae651e8e9d3d7114145" dependencies: - debug "2" - ref "1" + is-equal-shallow "^0.1.3" + is-primitive "^2.0.0" -ref@1: - version "1.3.3" - resolved "https://registry.yarnpkg.com/ref/-/ref-1.3.3.tgz#116d1ef64f2bc56d9e54a648cea7332a36b81932" +repeat-element@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a" + +repeat-string@^1.5.2: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + +repeating@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" dependencies: - bindings "1" - debug "2" - nan "2" + is-finite "^1.0.0" -require-all@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/require-all/-/require-all-2.1.0.tgz#109e1c3dab8a5acab2312f552d5e8d27d8de9f77" +replace-ext@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-0.0.1.tgz#29bbd92078a739f0bcce2b4ee41e837953522924" requizzle@~0.2.1: version "0.2.1" @@ -431,19 +1204,50 @@ requizzle@~0.2.1: dependencies: underscore "~1.6.0" -rethinkdbdash@~2.3.0: - version "2.3.27" - resolved "https://registry.yarnpkg.com/rethinkdbdash/-/rethinkdbdash-2.3.27.tgz#66de1c6cf13ed89db0c81ee0f060656caddfe1a5" +resolve-dir@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-0.1.1.tgz#b219259a5602fac5c5c496ad894a6e8cc430261e" dependencies: - bluebird ">= 3.0.1" + expand-tilde "^1.2.2" + global-modules "^0.2.3" + +resolve@^1.1.6, resolve@^1.1.7: + version "1.2.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.2.0.tgz#9589c3f2f6149d1417a40becc1663db6ec6bc26c" + +semver@^4.1.0: + version "4.3.6" + resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.6.tgz#300bc6e0e86374f7ba61068b5b1ecd57fc6532da" + +sequencify@~0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/sequencify/-/sequencify-0.0.7.tgz#90cff19d02e07027fd767f5ead3e7b95d1e7380c" sigmund@~1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590" -stack-trace@0.0.x: - version "0.0.9" - resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.9.tgz#a8f6eaeca90674c333e7c43953f275b451510695" +slash@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" + +source-map-support@^0.4.2: + version "0.4.8" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.8.tgz#4871918d8a3af07289182e974e32844327b2e98b" + dependencies: + source-map "^0.5.3" + +source-map@^0.5.0, source-map@^0.5.1, source-map@^0.5.3: + version "0.5.6" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" + +sparkles@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/sparkles/-/sparkles-1.0.0.tgz#1acbbfb592436d10bbe8f785b7cc6f82815012c3" + +stream-consume@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/stream-consume/-/stream-consume-0.1.0.tgz#a41ead1a6d6081ceb79f65b061901b6d8f3d1d0f" string_decoder@~0.10.x: version "0.10.31" @@ -455,48 +1259,52 @@ strip-ansi@^3.0.0: dependencies: ansi-regex "^2.0.0" +strip-bom@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-1.0.0.tgz#85b8862f3844b5a6d5ec8467a93598173a36f794" + dependencies: + first-chunk-stream "^1.0.0" + is-utf8 "^0.2.0" + strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" -superagent@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/superagent/-/superagent-2.3.0.tgz#703529a0714e57e123959ddefbce193b2e50d115" - dependencies: - component-emitter "^1.2.0" - cookiejar "^2.0.6" - debug "^2.2.0" - extend "^3.0.0" - form-data "1.0.0-rc4" - formidable "^1.0.17" - methods "^1.1.1" - mime "^1.3.4" - qs "^6.1.0" - readable-stream "^2.0.5" - supports-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" -supports-color@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-1.2.0.tgz#ff1ed1e61169d06b3cf2d588e188b18d8847e17e" - taffydb@2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/taffydb/-/taffydb-2.6.2.tgz#7cbcb64b5a141b6a2efc2c5d2c67b4e150b2a268" -thinky@^2.3.7: - version "2.3.8" - resolved "https://registry.yarnpkg.com/thinky/-/thinky-2.3.8.tgz#4d3d01fe0aaaa8cd97276965f40dbb4f017300f3" +through2@^0.6.1: + version "0.6.5" + resolved "https://registry.yarnpkg.com/through2/-/through2-0.6.5.tgz#41ab9c67b29d57209071410e1d7a7a968cd3ad48" dependencies: - bluebird "~2.10.2" - rethinkdbdash "~2.3.0" - validator "~3.22.1" + readable-stream ">=1.0.33-1 <1.1.0-0" + xtend ">=4.0.0 <4.1.0-0" -to-iso-string@0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/to-iso-string/-/to-iso-string-0.0.2.tgz#4dc19e664dfccbe25bd8db508b00c6da158255d1" +through2@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be" + dependencies: + readable-stream "^2.1.5" + xtend "~4.0.1" + +tildify@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/tildify/-/tildify-1.2.0.tgz#dcec03f55dca9b7aa3e5b04f21817eb56e63588a" + dependencies: + os-homedir "^1.0.0" + +time-stamp@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-1.0.1.tgz#9f4bd23559c9365966f3302dbba2b07c6b99b151" + +to-fast-properties@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.2.tgz#f3f5c0c3ba7299a7ef99427e44633257ade43320" tweetnacl@^0.14.3: version "0.14.5" @@ -506,6 +1314,10 @@ ultron@1.0.x: version "1.0.2" resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.0.2.tgz#ace116ab557cd197386a4e88f4685378c8b2e4fa" +unc-path-regex@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" + underscore-contrib@~0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/underscore-contrib/-/underscore-contrib-0.3.0.tgz#665b66c24783f8fa2b18c9f8cbb0e2c7d48c26c7" @@ -520,49 +1332,67 @@ underscore@~1.8.3: version "1.8.3" resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022" +unique-stream@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unique-stream/-/unique-stream-1.0.0.tgz#d59a4a75427447d9aa6c91e70263f8d26a4b104b" + +user-home@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/user-home/-/user-home-1.1.1.tgz#2b5be23a32b63a7c9deb8d0f28d485724a3df190" + util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" -validator@~3.22.1: - version "3.22.2" - resolved "https://registry.yarnpkg.com/validator/-/validator-3.22.2.tgz#6f297ae67f7f82acc76d0afdb49f18d9a09c18c0" +v8flags@^2.0.2: + version "2.0.11" + resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-2.0.11.tgz#bca8f30f0d6d60612cc2c00641e6962d42ae6881" + dependencies: + user-home "^1.1.1" -winston-cluster@0.0.4: - version "0.0.4" - resolved "https://registry.yarnpkg.com/winston-cluster/-/winston-cluster-0.0.4.tgz#640647d0d1e24cde19c215ff9f03d87cb969c40e" +vinyl-fs@^0.3.0: + version "0.3.14" + resolved "https://registry.yarnpkg.com/vinyl-fs/-/vinyl-fs-0.3.14.tgz#9a6851ce1cac1c1cea5fe86c0931d620c2cfa9e6" + dependencies: + defaults "^1.0.0" + glob-stream "^3.1.5" + glob-watcher "^0.0.6" + graceful-fs "^3.0.0" + mkdirp "^0.5.0" + strip-bom "^1.0.0" + through2 "^0.6.1" + vinyl "^0.4.0" + +vinyl-sourcemaps-apply@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz#ab6549d61d172c2b1b87be5c508d239c8ef87705" dependencies: - cluster "^0.7.7" - mocha "^2.2.5" - promise "^7.0.3" - winston "^1.0.0" + source-map "^0.5.1" -winston-daily-rotate-file@^1.3.1: - version "1.4.0" - resolved "https://registry.yarnpkg.com/winston-daily-rotate-file/-/winston-daily-rotate-file-1.4.0.tgz#71052f4c372ba7c5ae163834c5b043edd0c06be0" +vinyl@^0.4.0: + version "0.4.6" + resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-0.4.6.tgz#2f356c87a550a255461f36bbeb2a5ba8bf784847" + dependencies: + clone "^0.2.0" + clone-stats "^0.0.1" -winston@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/winston/-/winston-1.1.2.tgz#68edd769ff79d4f9528cf0e5d80021aade67480c" +vinyl@^0.5.0: + version "0.5.3" + resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-0.5.3.tgz#b0455b38fc5e0cf30d4325132e461970c2091cde" dependencies: - async "~1.0.0" - colors "1.0.x" - cycle "1.0.x" - eyes "0.1.x" - isstream "0.1.x" - pkginfo "0.3.x" - stack-trace "0.0.x" + clone "^1.0.0" + clone-stats "^0.0.1" + replace-ext "0.0.1" -winston@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/winston/-/winston-2.3.0.tgz#207faaab6fccf3fe493743dd2b03dbafc7ceb78c" +which@^1.2.12: + version "1.2.12" + resolved "https://registry.yarnpkg.com/which/-/which-1.2.12.tgz#de67b5e450269f194909ef23ece4ebe416fa1192" dependencies: - async "~1.0.0" - colors "1.0.x" - cycle "1.0.x" - eyes "0.1.x" - isstream "0.1.x" - stack-trace "0.0.x" + isexe "^1.1.1" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" ws@^1.1.1: version "1.1.1" @@ -571,3 +1401,7 @@ ws@^1.1.1: options ">=0.0.5" ultron "1.0.x" +"xtend@>=4.0.0 <4.1.0-0", xtend@~4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" +