const Discord = require('discord.js'), yaml = require('js-yaml'), fsx = require('fs-extra'), logging = require('../utils/logging'), config = require('../../config.json'), xevents = require('../utils/extended-events'), utils = require('../utils'); const scopes = new utils.Enum([ 'Global', 'User', 'Guild' ]); /** * The answer message object that is used for easyer access to events. */ class Response extends xevents.ExtendedEventEmitter { /** * Constructor. * @param content */ constructor(content) { super(); this.content = content; this.message = null; } } class Answer { /** * Creates an new Answer object with _func as answer logic. * @param func {function} - the function to evaluate the answer * @param [onSent] {function} - executed when the response was sent */ constructor(func, onSent) { this._func = func; this.listeners = onSent? {sent: onSent} : {}; this.lastResponse = null; } /** * Evaluates the answer string for the answer object. * If the logic function returns a promise all nested promises get resolved. * @param message * @param kwargs * @param argsString * @returns {Promise} */ async evaluate(message, kwargs, argsString) { let result = this._func(message, kwargs, argsString); if (result instanceof Promise) return this._getResponseInstance(await utils.resolveNestedPromise(result)); else return this._getResponseInstance(result); } /** * Returns a response instance with listeners attached if defined. * @param responseContent * @returns {Response} * @private */ _getResponseInstance(responseContent) { this.lastResponse = new Response(responseContent); if (this.listeners) this.lastResponse.addListeners(this.listeners); return this.lastResponse; } } class Command { /** * Creates a new command object where the answer function needs * to be implemented for it to work. * @param template {JSON:{}} * @param answer {Answer} */ constructor(template, answer) { this.name = template.name; this.prefix = ''; this.description = template.description; this.args = template.args || []; this.permission = template.permission; this.category = template.category || 'Other'; this.usage = template.usage || `${this.name} [${this.args.join('][')}]`.replace('[]', ''); this.answObj = answer; if (!template.name) throw new Error("Template doesn't define a name."); } /** * This method is meant to be replaced by logic. * @abstract * @param message {Discord.Message} * @param kwargs {JSON} * @param argsString {String} The raw argument string. * @returns {Response} */ async answer(message, kwargs, argsString) { return await this.answObj.evaluate(message, kwargs, argsString); } /** * Returns rich help embed for this command. * @returns {*|Discord.RichEmbed} */ get help() { return new ExtendedRichEmbed(`Help for ${this.name}`) .addFields({ 'Usage': `\`${this.prefix}${this.usage}\``, 'Description': this.description, 'Permission Role': this.permission }); } } class CommandHandler { /** * Initializes the CommandHandler * @param prefix {String} The prefix of all commands. * @param scope {Number} A scope from the CommandScopes (scopes) */ constructor(prefix, scope) { this.prefix = prefix; this.scope = scope; this.commands = {}; this._logger = new logging.Logger(`${this.constructor.name}@${Object.keys(scopes)[this.scope]}`); } /** * Handles the command and responds to the message. * @param commandMessage {String} * @param message {Discord.Message} * @returns {Response | Promise} */ handleCommand(commandMessage, message) { this._logger.debug(`Handling command ${commandMessage}`); let commandName = commandMessage.match(/^\S+/); if (commandName.length > 0) commandName = commandName[0]; this._logger.silly(`Command name is ${commandName}`); if (commandName.indexOf(this.prefix) >= 0) { commandName = commandName.replace(this.prefix, ''); let argsString = commandMessage.replace(/^\S+/, ''); argsString = argsString .replace(/^\s+/, '') // leading whitespace .replace(/\s+$/, ''); // trailing whitespace let args = argsString.match(/\S+/g); let command = this.commands[commandName]; if (command && this._checkPermission(message, command.permission)) { this._logger.silly(`Permission ${command.permission} granted for command ${commandName}`); let kwargs = {}; if (args) for (let i = 0; i < Math.min(command.args.length, args.length); i++) kwargs[command.args[i]] = args[i]; return command.answer(message, kwargs, argsString); } else if (command) { this._logger.silly(`Permission ${command.permission} denied for command ${commandName}`); return new Response("You don't have permission for this command"); } else { this._logger.silly(`Command ${commandName} not found.`); return null; } } else { this._logger.silly(`No prefix found in command ${commandName}`); return null; } } /** * Registers the command so that the handler can use it. * @param command {Command} */ registerCommand(command) { command.prefix = this.prefix; this.commands[command.name] = command; this._logger.debug(`Registered ${command.name} on handler`); return this; } /** * Checks if the author of the message has the given permission * @param msg {Discord.Message} * @param rolePerm {String} Permission String * @returns {boolean} * @private */ _checkPermission(msg, rolePerm) { if (!rolePerm || ['all', 'any', 'everyone'].includes(rolePerm)) return true; if (config.owners.includes(msg.author.tag)) return true; else if (msg.member && rolePerm && rolePerm !== 'owner' && msg.member.roles .some(role => (role.name.toLowerCase() === rolePerm.toLowerCase() || role.name.toLowerCase() === 'botcommander'))) return true; return false; } } /** * @abstract */ class CommandModule { /** * Initializes a CommandModule instance. * @param scope */ constructor(scope) { this.scope = scope; this._logger = new logging.Logger(this); } /** * Loads a template for the module from the this._templateDir directory. * Loads the template from this.templateFile if the attribute exists. * @param dir {String} Overides the this._templateDir with this directory. * @returns {Promise} * @private */ async _loadTemplate(dir) { if (!this.templateFile) this.templateFile = (dir || this._templateDir) + '/template.yaml'; let templateString = await fsx.readFile(this.templateFile, {encoding: 'utf-8'}); this._logger.silly(`Loaded Template file ${this.templateFile}`); this.template = yaml.safeLoad(templateString); } /** * Registering commands after loading a template * @param commandHandler {CommandHandler} * @returns {Promise} */ async register(commandHandler) { // eslint-disable-line no-unused-vars } } class ExtendedRichEmbed extends Discord.RichEmbed { /** * Constructor that automatically set's the Title and Timestamp. * @param title {String} */ constructor(title) { super(); this.setTitle(title); this.setTimestamp(); } /** * Adds the fields defined in the fields JSON * @param fields {JSON} * @returns {ExtendedRichEmbed} */ addFields(fields) { for (let [name, value] of Object.entries(fields)) this.addField(name, value); return this; } /** * Sets the description by shortening the value string to a fitting length for discord. * @param value */ setDescription(value) { if (value) { let croppedValue = value; if (value.substring) croppedValue = value.substring(0, 1024); if (croppedValue.length < value.length && croppedValue.replace) croppedValue = croppedValue.replace(/\n.*$/g, ''); if (croppedValue && croppedValue.replace && croppedValue.replace(/\n/g, '').length > 0) super.setDescription(croppedValue); } return this; } /** * Sets the field by shortening the value stirn to a fitting length for discord. * @param name * @param value */ addField(name, value) { if (name && value) { let croppedValue = value; if (value.substring) croppedValue = value.substring(0, 1024); if (croppedValue && croppedValue.length < value.length && croppedValue.replace) croppedValue = croppedValue.replace(/\n.*$/g, ''); if (croppedValue && croppedValue.replace && croppedValue.replace(/\n/g, '').length > 0 && name.replace(/\n/g, '').length > 0) super.addField(name, croppedValue); } return this; } } // -- exports -- // Object.assign(exports, { Answer: Answer, Command: Command, CommandHandler: CommandHandler, CommandModule: CommandModule, ExtendedRichEmbed: ExtendedRichEmbed, CommandScopes: scopes });