Started restructuring command handling
parent
42ee8cc4c5
commit
0223180053
@ -0,0 +1,181 @@
|
|||||||
|
const Discord = require('discord.js');
|
||||||
|
|
||||||
|
const scopes = {
|
||||||
|
'Global': 0,
|
||||||
|
'User': 1,
|
||||||
|
'Guild': 2
|
||||||
|
};
|
||||||
|
|
||||||
|
class Answer {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an new Answer object with func as answer logic.
|
||||||
|
* @param func
|
||||||
|
*/
|
||||||
|
constructor(func) {
|
||||||
|
this.func = func;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluates the answer string for the answer object.
|
||||||
|
* @param message
|
||||||
|
* @param kwargs
|
||||||
|
* @param argsString
|
||||||
|
* @returns {Promise<*>}
|
||||||
|
*/
|
||||||
|
async evaluate(message, kwargs, argsString) {
|
||||||
|
let result = this.func(message, kwargs, argsString);
|
||||||
|
switch (result.constructor.name) {
|
||||||
|
case 'Promise':
|
||||||
|
return await this.evaluate(await result);
|
||||||
|
default:
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.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 {String}
|
||||||
|
*/
|
||||||
|
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.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 = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the command and responds to the message.
|
||||||
|
* @param commandMessage {String}
|
||||||
|
* @param message {Discord.Message}
|
||||||
|
* @returns {Boolean | Promise<String|Discord.RichEmbed>}
|
||||||
|
*/
|
||||||
|
handleCommand(commandMessage, message) {
|
||||||
|
let commandName = commandMessage.match(/^\S+/);
|
||||||
|
if (commandName.indexOf(this.prefix) > 0) {
|
||||||
|
commandName = commandName.replace(this.prefix);
|
||||||
|
let argsString = commandMessage.replace(/^\S+/, '');
|
||||||
|
let args = argsString(/\S+/g);
|
||||||
|
let command = this.commands[commandName];
|
||||||
|
let kwargs = {};
|
||||||
|
|
||||||
|
for (let i = 0; i < Math.min(command.kwargs, args.length); i++)
|
||||||
|
kwargs[command.kwargs[i]] = args[i];
|
||||||
|
|
||||||
|
return command.answer(message, kwargs, argsString);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers the command so that the handler can use it.
|
||||||
|
* @param name {String}
|
||||||
|
* @param command {Command}
|
||||||
|
*/
|
||||||
|
registerCommand(name, command) {
|
||||||
|
this.commands[name] = command;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a map of commands containing of the name and the command.
|
||||||
|
* @param commandMap {Map}
|
||||||
|
*/
|
||||||
|
registerCommands(commandMap) {
|
||||||
|
for (let [name, cmd] in commandMap)
|
||||||
|
this.commands[name] = cmd;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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 a Field when a name is given or adds a blank Field otherwise
|
||||||
|
* @param name {String}
|
||||||
|
* @param content {String}
|
||||||
|
*/
|
||||||
|
addNonemptyField(name, content) {
|
||||||
|
if (name && name.length > 0 && content)
|
||||||
|
this.addField(name, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the fields defined in the fields JSON
|
||||||
|
* @param fields {JSON}
|
||||||
|
*/
|
||||||
|
addFields(fields) {
|
||||||
|
for (let [name, value] in Object.entries(fields))
|
||||||
|
this.addNonemptyField(name, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- exports -- //
|
||||||
|
|
||||||
|
Object.assign(exports, {
|
||||||
|
Answer: Answer,
|
||||||
|
Command: Command,
|
||||||
|
CommandHandler: CommandHandler,
|
||||||
|
ExtendedRichEmbed: ExtendedRichEmbed,
|
||||||
|
CommandScopes: scopes
|
||||||
|
});
|
@ -0,0 +1,122 @@
|
|||||||
|
const cmdLib = require('CommandLib'),
|
||||||
|
config = require('../config.json'),
|
||||||
|
Discord = require('discord.js'),
|
||||||
|
promiseWaterfall = require('promise-waterfall');
|
||||||
|
|
||||||
|
class MessageHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Message Handler to handle messages. Listens on the
|
||||||
|
* client message event.
|
||||||
|
* @param client {Discord.Client}
|
||||||
|
* @param logger {winston.logger}
|
||||||
|
*/
|
||||||
|
constructor (client, logger) {
|
||||||
|
this.logger = logger;
|
||||||
|
this.discordClient = client;
|
||||||
|
this.globalCmdHandler = new cmdLib.CommandHandler(config.prefix,
|
||||||
|
cmdLib.CommandScopes.Global);
|
||||||
|
this.userCmdHandler = new cmdLib.CommandHandler(config.prefix,
|
||||||
|
cmdLib.CommandScopes.User);
|
||||||
|
this.guildCmdHandler = new cmdLib.CommandHandler(config.prefix,
|
||||||
|
cmdLib.CommandScopes.Guild);
|
||||||
|
this._registerEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the handler fitting the scope
|
||||||
|
* @param scope {Number}
|
||||||
|
* @returns {cmdLib.CommandHandler}
|
||||||
|
*/
|
||||||
|
getHandler(scope) {
|
||||||
|
switch (scope) {
|
||||||
|
case cmdLib.CommandScopes.Global:
|
||||||
|
return this.globalCmdHandler;
|
||||||
|
case cmdLib.CommandScopes.Guild:
|
||||||
|
return this.guildCmdHandler;
|
||||||
|
case cmdLib.CommandScopes.User:
|
||||||
|
return this.userCmdHandler;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registering event handlers.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_registerEvents() {
|
||||||
|
this.discordClient.on('message', async (msg) => {
|
||||||
|
let sequence = this._parseSyntax(msg);
|
||||||
|
await this._executeCommandSequence(sequence);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the syntax of a message into a command array.
|
||||||
|
* @param message
|
||||||
|
* @returns {Array<Array<String>>}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_parseSyntax(message) {
|
||||||
|
let commandSequence = [];
|
||||||
|
let content = message.content;
|
||||||
|
let strings = content.match(/".+?"/g);
|
||||||
|
|
||||||
|
for (let string in strings)
|
||||||
|
content.replace(string, string // escape all special chars
|
||||||
|
.replace(';', '\\;'))
|
||||||
|
.replace('&', '\\&');
|
||||||
|
let independentCommands = content // independent command sequende with ;
|
||||||
|
.split(/(?<!\\);/g)
|
||||||
|
.map(x => x.replace(/^ +/, ''));
|
||||||
|
for (let indepCommand in independentCommands)
|
||||||
|
commandSequence.push(indepCommand
|
||||||
|
.split(/(?<!\\)&&/g) // dependend sequence with && (like unix)
|
||||||
|
.map(x => x.replace(/^ +/, ''))
|
||||||
|
);
|
||||||
|
return commandSequence;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a sequence of commands
|
||||||
|
*/
|
||||||
|
async _executeCommandSequence(cmdSequence, message) {
|
||||||
|
let scopeCmdHandler = this._getScopeHandlers(message);
|
||||||
|
await Promise.all(cmdSequence.map(async (sq) => {
|
||||||
|
return await promiseWaterfall(sq.map(async (cmd) => {
|
||||||
|
let globalResult = await this.globalCmdHandler.handleCommand(cmd, message);
|
||||||
|
let scopeResult = await scopeCmdHandler.handleCommand(cmd, message);
|
||||||
|
|
||||||
|
if (scopeResult)
|
||||||
|
this._answerMessage(message, scopeResult);
|
||||||
|
else if (globalResult)
|
||||||
|
this._answerMessage(message, globalResult);
|
||||||
|
}));
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns two commandHandlers for the messages scope.
|
||||||
|
* @param message
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_getScopeHandler(message) {
|
||||||
|
if (message.guild)
|
||||||
|
return this.guildCmdHandler;
|
||||||
|
else
|
||||||
|
return this.userCmdHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Answers
|
||||||
|
* @param message {Discord.Message}
|
||||||
|
* @param answer {String | Discord.RichEmbed}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_answerMessage(message, answer) {
|
||||||
|
if (answer)
|
||||||
|
if (answer instanceof Discord.RichEmbed)
|
||||||
|
message.channel.send('', answer);
|
||||||
|
else
|
||||||
|
message.channel.send(answer);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,56 @@
|
|||||||
|
const cmdLib = require('../../../CommandLib'),
|
||||||
|
yaml = require('js-yaml'),
|
||||||
|
fsx = require('fs-extra'),
|
||||||
|
templateFile = 'AniListCommandsTemplate.yaml';
|
||||||
|
|
||||||
|
class RichMediaInfo extends cmdLib.ExtendedRichEmbed {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a rich embed with info for AniListApi Media.
|
||||||
|
* @param mediaInfo
|
||||||
|
*/
|
||||||
|
constructor(mediaInfo) {
|
||||||
|
super(mediaInfo.title.romaji);
|
||||||
|
this.setDescription(mediaInfo.description.replace(/<\/?.*?>/g, ''))
|
||||||
|
.setThumbnail(mediaInfo.coverImage.large)
|
||||||
|
.setURL(mediaInfo.siteUrl)
|
||||||
|
.setColor(mediaInfo.coverImage.color)
|
||||||
|
.setFooter('Provided by AniList.co');
|
||||||
|
let fields = {
|
||||||
|
'Genres': mediaInfo.genres.join(' '),
|
||||||
|
'Studios': mediaInfo.studios.studioList.map(x => `[${x.name}](${x.siteUrl})`),
|
||||||
|
'Scoring': `**AverageScore**: ${mediaInfo.averageScore}\n**Favourites**${mediaInfo.favourites}`,
|
||||||
|
'Episodes': mediaInfo.episodes,
|
||||||
|
'Duration': null,
|
||||||
|
'Season': mediaInfo.season,
|
||||||
|
'Status': mediaInfo.status,
|
||||||
|
'Format': mediaInfo.format
|
||||||
|
};
|
||||||
|
if (mediaInfo.duration)
|
||||||
|
fields['Episode Duration'] = `${mediaInfo.duration} min`;
|
||||||
|
if (mediaInfo.startDate.day)
|
||||||
|
fields['Start Date'] = `${mediaInfo.startDate.day}.${mediaInfo.startDate.month}.${mediaInfo.startDate.year}`;
|
||||||
|
if (mediaInfo.nextAiringEpisode) {
|
||||||
|
let epInfo = mediaInfo.nextAiringEpisode;
|
||||||
|
fields['Next Episode'] = `**Episode** ${epInfo.episode}\n**Airing at:** ${new Date(epInfo.airingAt * 1000).toUTCString()}`;
|
||||||
|
}
|
||||||
|
if (mediaInfo.endDate.day)
|
||||||
|
fields['End Date'] = `${mediaInfo.endDate.day}.${mediaInfo.endDate.month}.${mediaInfo.endDate.year}`;
|
||||||
|
this.addFields(fields);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- initialize -- //
|
||||||
|
|
||||||
|
let template = null;
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
let templateString = fsx.readFile(templateFile, {encoding: 'utf-8'});
|
||||||
|
template = yaml.safeLoad(templateString);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- exports -- //
|
||||||
|
|
||||||
|
Object.assign(exports, {
|
||||||
|
init: init
|
||||||
|
});
|
Loading…
Reference in New Issue