You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
discordbot.js/lib/command/index.js

319 lines
9.9 KiB
JavaScript

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<Response>}
*/
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<Response>}
*/
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<void>}
* @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<void>}
*/
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
});