+
+### 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 @@