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