const path = require('path')
const fs = require('fs')
const util = require('util')
const { requireAll, isDir, Collection } = require('../util')
/**
* Commander class for command processing
* @prop {Set} prefixes Set of added prefixes
*/
class Commander extends Collection {
/**
* Creates a new Commander instance
* @arg {Client} client Client instance
*/
constructor (client) {
super()
this._client = client
this._cached = []
this.prefixes = new Set()
this.prefixes.add(client.prefix)
}
/**
* Registers commands
* @arg {String|Object|Array} commands An object, array or relative path to a folder or file to load commands from
* @arg {Object} [options] Additional command options
* @arg {String} [options.prefix] Command prefix, will be overwritten by prefix setting in the command
* @arg {Boolean} [options.groupedCommands] Option for object/path supplied to be an object of objects with command groups as keys
* @returns {Client}
*/
register (commands, options = {}) {
switch (typeof commands) {
case 'string': {
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)
this._cached.push(filepath)
return this.register(cmds, options)
}
case 'object': {
if (options.prefix) {
this.prefixes.add(options.prefix)
}
if (Array.isArray(commands)) {
for (const command of commands) {
this.attach(command, null, options.prefix)
}
return this
}
for (const group in commands) {
const command = commands[group]
if (options.groupedCommands && typeof command === 'object') {
for (const name in command) {
this.attach(command[name], group, options.prefix)
}
return this
}
this.attach(command, group, options.prefix)
}
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<br />
* 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
* @arg {String} [prefix] Default command prefix, will be overwritten by prefix setting in the command
* @returns {Commander}
*/
attach (Command, group = 'misc', prefix) {
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) {
if (this.has(trigger)) {
this._client.throwOrEmit('commander:error', new Error(`Duplicate command - ${trigger}`))
return this
}
command.group = command.group || group
command.prefix = command.prefix || prefix
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
* @prop {Number} count Number of loaded command triggers
*/
this._client.emit('commander:registered', {
trigger: command.triggers[0],
group: command.group,
aliases: command.triggers.length - 1,
count: this.size
})
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
}
/**
* 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
* @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