From c4f3635dd63bceaca56a36dbdf5ae58da9211a06 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Fri, 1 Mar 2019 21:01:50 +0100 Subject: [PATCH] Started Reimplementing Commands - reimplemented utils - reimplemented info --- bot.js | 12 ++- lib/CommandLib.js | 83 ++++++++++++------ lib/MessageLib.js | 50 ++++++++--- lib/api/AnilistApi.js | 2 +- .../{AniListCommandsLogic.js => index.js} | 85 +++++++++---------- .../InfoCommands/InfoCommandsTemplate.yaml | 34 ++++++++ lib/commands/InfoCommands/index.js | 0 .../UtilityCommandsTemplate.yaml | 44 ++++++++++ lib/commands/UtilityCommands/index.js | 0 lib/utils.js | 12 +++ lib/weblib.js | 2 +- 11 files changed, 236 insertions(+), 88 deletions(-) rename lib/commands/AnilistApiCommands/{AniListCommandsLogic.js => index.js} (51%) create mode 100644 lib/commands/InfoCommands/InfoCommandsTemplate.yaml create mode 100644 lib/commands/InfoCommands/index.js create mode 100644 lib/commands/UtilityCommands/UtilityCommandsTemplate.yaml create mode 100644 lib/commands/UtilityCommands/index.js diff --git a/bot.js b/bot.js index fb6ef09..1fa71ad 100644 --- a/bot.js +++ b/bot.js @@ -1,6 +1,7 @@ const Discord = require("discord.js"), fs = require('fs-extra'), logger = require('./lib/logging').getLogger(), + msgLib = require('./lib/MessageLib'), cmd = require("./lib/cmd"), guilding = require('./lib/guilding'), utils = require('./lib/utils'), @@ -21,6 +22,7 @@ class Bot { this.rotator = null; this.maindb = null; this.presences = []; + this.messageHandler = new msgLib.MessageHandler(this.client, logger); this.guildHandlers = []; this.userRates = {}; @@ -65,7 +67,11 @@ class Bot { if (config.webservice && config.webservice.enabled) await this.initializeWebserver(); logger.verbose('Registering commands'); - this.registerCommands(); + await this.messageHandler + .registerCommandModule(require('./lib/commands/AnilistApiCommands').module, {}); + await this.messageHandler + .registerCommandModule(require('./lib/commands/UtilityCommands').module, {bot: this, logger: logger, config: config}); + //this.registerCommands(); this.registerCallbacks(); cmd.init(prefix); } @@ -166,7 +172,6 @@ class Bot { registerCommands() { cmd.registerUtilityCommands(prefix, this); cmd.registerInfoCommands(prefix, this); - cmd.registerAnilistApiCommands(prefix); } /** @@ -206,6 +211,7 @@ class Bot { }); }); + /* this.client.on('message', async (msg) => { try { if (msg.author === this.client.user) { @@ -235,7 +241,7 @@ class Bot { logger.error(err.message); logger.debug(err.stack); } - }); + });*/ this.client.on('voiceStateUpdate', async (oldMember, newMember) => { let gh = await this.getGuildHandler(newMember.guild, prefix); diff --git a/lib/CommandLib.js b/lib/CommandLib.js index eb86255..5381a3a 100644 --- a/lib/CommandLib.js +++ b/lib/CommandLib.js @@ -1,4 +1,7 @@ -const Discord = require('discord.js'); +const Discord = require('discord.js'), + yaml = require('js-yaml'), + fsx = require('fs-extra'), + utils = require('./utils'); const scopes = { 'Global': 0, @@ -9,28 +12,27 @@ const scopes = { class Answer { /** - * Creates an new Answer object with func as answer logic. + * Creates an new Answer object with _func as answer logic. * @param func */ constructor(func) { - this.func = func; + this._func = func; } /** * 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); - switch (result.constructor.name) { - case 'Promise': - return await this.evaluate(await result); - default: - return result; - } + let result = this._func(message, kwargs, argsString); + if (result instanceof Promise) + return await utils.resolveNestedPromise(result); + else + return result; } } @@ -69,7 +71,7 @@ class Command { /** * Returns rich help embed for this command. - * @returns {Discord.RichEmbed} + * @returns {*|Discord.RichEmbed} */ get help() { return new ExtendedRichEmbed(`Help for ${this.name}`) @@ -102,17 +104,22 @@ class CommandHandler { */ handleCommand(commandMessage, message) { let commandName = commandMessage.match(/^\S+/); - if (commandName.indexOf(this.prefix) > 0) { - commandName = commandName.replace(this.prefix); + if (commandName.length > 0) + commandName = commandName[0]; + if (commandName.indexOf(this.prefix) >= 0) { + commandName = commandName.replace(this.prefix, ''); let argsString = commandMessage.replace(/^\S+/, ''); - let args = argsString(/\S+/g); + let args = argsString.match(/\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); + if (command) { + let kwargs = {}; + if (args) + 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; + } } else { return false; } @@ -126,16 +133,39 @@ class CommandHandler { registerCommand(name, command) { this.commands[name] = command; } +} + +/** + * @abstract + */ +class CommandModule { + + /** + * Initializes a CommandModule instance. + * @param scope + */ + constructor(scope) { + this.scope = scope; + } /** - * Registers a map of commands containing of the name and the command. - * @param commandMap {Map} + * Loads a template for the object property templateFile or the given argument file. + * @returns {Promise} + * @private */ - registerCommands(commandMap) { - for (let [name, cmd] in commandMap) - this.commands[name] = cmd; + async _loadTemplate(file) { + let templateString = await fsx.readFile(this.templateFile || file, {encoding: 'utf-8'}); + 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 { @@ -165,7 +195,7 @@ class ExtendedRichEmbed extends Discord.RichEmbed { * @param fields {JSON} */ addFields(fields) { - for (let [name, value] in Object.entries(fields)) + for (let [name, value] of Object.entries(fields)) this.addNonemptyField(name, value); } } @@ -176,6 +206,7 @@ Object.assign(exports, { Answer: Answer, Command: Command, CommandHandler: CommandHandler, + CommandModule: CommandModule, ExtendedRichEmbed: ExtendedRichEmbed, CommandScopes: scopes }); diff --git a/lib/MessageLib.js b/lib/MessageLib.js index 9f7b9df..588e32e 100644 --- a/lib/MessageLib.js +++ b/lib/MessageLib.js @@ -1,4 +1,4 @@ -const cmdLib = require('CommandLib'), +const cmdLib = require('./CommandLib'), config = require('../config.json'), Discord = require('discord.js'), promiseWaterfall = require('promise-waterfall'); @@ -39,14 +39,32 @@ class MessageHandler { } } + /** + * Registers a command module to a command handler. + * @param CommandModule {cmdLib.CommandModule} + * @param options {Object} Options passed to the module constructor. + * @returns {Promise} + */ + async registerCommandModule(CommandModule, options) { + this.logger.info(`Registering command module ${CommandModule.name}...`); + let cmdModule = new CommandModule(options); + await cmdModule.register(this.getHandler(cmdModule.scope)); + } + /** * Registering event handlers. * @private */ _registerEvents() { + this.logger.debug('Registering message event...'); this.discordClient.on('message', async (msg) => { - let sequence = this._parseSyntax(msg); - await this._executeCommandSequence(sequence); + this.logger.debug(`<${msg.channel.name || 'PRIVATE'}> ${msg.author.name}: ${msg.content}`); + if (msg.author !== this.discordClient.user) { + let sequence = this._parseSyntax(msg); + this.logger.debug(`Syntax parsing returned: ${JSON.stringify(sequence)}`); + await this._executeCommandSequence(sequence, msg); + this.logger.debug('Executed command sequence'); + } }); } @@ -57,6 +75,7 @@ class MessageHandler { * @private */ _parseSyntax(message) { + this.logger.debug('Parsing command sequence...'); let commandSequence = []; let content = message.content; let strings = content.match(/".+?"/g); @@ -68,7 +87,7 @@ class MessageHandler { let independentCommands = content // independent command sequende with ; .split(/(? x.replace(/^ +/, '')); - for (let indepCommand in independentCommands) + for (let indepCommand of independentCommands) commandSequence.push(indepCommand .split(/(? x.replace(/^ +/, '')) @@ -80,18 +99,24 @@ class MessageHandler { * 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) => { + this.logger.debug(`Executing command sequence: ${JSON.stringify(cmdSequence)} ...`); + let scopeCmdHandler = this._getScopeHandler(message); + await Promise.all(cmdSequence.map((sq) => promiseWaterfall(sq.map((cmd) => async () => { + try { + this.logger.debug(`Executing command ${cmd}`); let globalResult = await this.globalCmdHandler.handleCommand(cmd, message); let scopeResult = await scopeCmdHandler.handleCommand(cmd, message); + this.logger.debug(`globalResult: ${globalResult}, scopeResult: ${scopeResult}`); if (scopeResult) this._answerMessage(message, scopeResult); else if (globalResult) this._answerMessage(message, globalResult); - })); - })); + } catch (err) { + this.logger.verbose(err.message); + this.logger.silly(err.stack); + } + })))); } /** @@ -100,7 +125,7 @@ class MessageHandler { * @private */ _getScopeHandler(message) { - if (message.guild) + if (message && message.guild) return this.guildCmdHandler; else return this.userCmdHandler; @@ -113,6 +138,7 @@ class MessageHandler { * @private */ _answerMessage(message, answer) { + this.logger.debug(`Sending answer ${answer}`); if (answer) if (answer instanceof Discord.RichEmbed) message.channel.send('', answer); @@ -120,3 +146,7 @@ class MessageHandler { message.channel.send(answer); } } + +Object.assign(exports, { + MessageHandler: MessageHandler +}); diff --git a/lib/api/AnilistApi.js b/lib/api/AnilistApi.js index 33a8916..ed728d5 100644 --- a/lib/api/AnilistApi.js +++ b/lib/api/AnilistApi.js @@ -1,6 +1,6 @@ const fetch = require('node-fetch'), fsx = require('fs-extra'), - queryPath = './lib/graphql/AnilistApi', + queryPath = './lib/api/graphql/AnilistApi', alApiEndpoint = 'https://graphql.anilist.co'; /** diff --git a/lib/commands/AnilistApiCommands/AniListCommandsLogic.js b/lib/commands/AnilistApiCommands/index.js similarity index 51% rename from lib/commands/AnilistApiCommands/AniListCommandsLogic.js rename to lib/commands/AnilistApiCommands/index.js index 493dfa0..de844ef 100644 --- a/lib/commands/AnilistApiCommands/AniListCommandsLogic.js +++ b/lib/commands/AnilistApiCommands/index.js @@ -1,8 +1,6 @@ const cmdLib = require('../../CommandLib'), anilistApi = require('../../api/AnilistApi'), - yaml = require('js-yaml'), - fsx = require('fs-extra'), - templateFile = 'AniListCommandsTemplate.yaml'; + location = './lib/commands/AnilistApiCommands'; class RichMediaInfo extends cmdLib.ExtendedRichEmbed { @@ -19,7 +17,7 @@ class RichMediaInfo extends cmdLib.ExtendedRichEmbed { .setFooter('Provided by AniList.co'); let fields = { 'Genres': mediaInfo.genres.join(' '), - 'Studios': mediaInfo.studios.studioList.map(x => `[${x.name}](${x.siteUrl})`), + 'Studios': mediaInfo.studios? mediaInfo.studios.studioList.map(x => `[${x.name}](${x.siteUrl})`) : null, 'Scoring': `**AverageScore**: ${mediaInfo.averageScore}\n**Favourites**${mediaInfo.favourites}`, 'Episodes': mediaInfo.episodes, 'Duration': null, @@ -43,55 +41,48 @@ class RichMediaInfo extends cmdLib.ExtendedRichEmbed { // -- initialize -- // -let template = null; - /** - * Initializes the module. - * @returns {Promise} + * Implementing the AniList commands module. */ -async function init() { - let templateString = await fsx.readFile(templateFile, {encoding: 'utf-8'}); - template = yaml.safeLoad(templateString); -} +class AniListCommandModule extends cmdLib.CommandModule { -/** - * Registers the commands to the CommandHandler. - * @param commandHandler {cmdLib.CommandHandler} - * @returns {Promise} - */ -async function register(commandHandler) { - // creating commands - let animeSearch = new cmdLib.Command( - template.anime_search, - new cmdLib.Answer(async (m, k, s) => { - try { - let animeData = await anilistApi.searchAnimeByName(s); - return new RichMediaInfo(animeData); - } catch (err) { - return template.anime_search.not_found; - } - })); + constructor() { + super(cmdLib.CommandScopes.Global); + this.templateFile = location + '/AniListCommandsTemplate.yaml'; + this.template = null; + } - let mangaSearch = new cmdLib.Command( - template.manga_search, - new cmdLib.Answer(async (m, k, s) => { - try { - let mangaData = await anilistApi.searchMangaByName(s); - return new RichMediaInfo(mangaData); - } catch (err) { - return template.manga_search.not_found; - } - }) - ); + async register(commandHandler) { + await this._loadTemplate(); + let animeSearch = new cmdLib.Command( + this.template.anime_search, + new cmdLib.Answer(async (m, k, s) => { + try { + let animeData = await anilistApi.searchAnimeByName(s); + return new RichMediaInfo(animeData); + } catch (err) { + return this.template.anime_search.not_found; + } + })); - // registering commands - commandHandler.registerCommand(template.anime_search.name, animeSearch); - commandHandler.registerCommand(template.manga_search.name, mangaSearch); -} + let mangaSearch = new cmdLib.Command( + this.template.manga_search, + new cmdLib.Answer(async (m, k, s) => { + try { + let mangaData = await anilistApi.searchMangaByName(s); + return new RichMediaInfo(mangaData); + } catch (err) { + return this.template.manga_search.not_found; + } + }) + ); -// -- exports -- // + // registering commands + commandHandler.registerCommand(this.template.anime_search.name, animeSearch); + commandHandler.registerCommand(this.template.manga_search.name, mangaSearch); + } +} Object.assign(exports, { - init: init, - register: register + 'module': AniListCommandModule }); diff --git a/lib/commands/InfoCommands/InfoCommandsTemplate.yaml b/lib/commands/InfoCommands/InfoCommandsTemplate.yaml new file mode 100644 index 0000000..247603a --- /dev/null +++ b/lib/commands/InfoCommands/InfoCommandsTemplate.yaml @@ -0,0 +1,34 @@ +about: + name: about + description: > + Shows information about this Discord Bot. + permission: all + category: Info + response: + about_icon: | + This icon war created by [blackrose14344](https://www.deviantart.com/blackrose14344). + [Original](https://www.deviantart.com/blackrose14344/art/2B-Chi-B-685771489) + about_creator: | + This bot was created by Trivernis. + More about this bot [here](https://github.com/Trivernis/discordbot.js). + +ping: + name: ping + description: > + Answers with the current average ping of the bot. + permission: all + category: Info + +uptime: + name: uptime + description: > + Answers with the uptime of the bot. + permission: all + category: Info + +guilds: + name: guilds + description: > + Answers with the number of guilds the bot has joined + permission: owner + category: Info diff --git a/lib/commands/InfoCommands/index.js b/lib/commands/InfoCommands/index.js new file mode 100644 index 0000000..e69de29 diff --git a/lib/commands/UtilityCommands/UtilityCommandsTemplate.yaml b/lib/commands/UtilityCommands/UtilityCommandsTemplate.yaml new file mode 100644 index 0000000..09cab2c --- /dev/null +++ b/lib/commands/UtilityCommands/UtilityCommandsTemplate.yaml @@ -0,0 +1,44 @@ +shutdown: + name: shutdown + description: > + Shuts down the bot. + permission: owner + category: Utility + +add_presence: + name: addpresence + description: > + Adds a Rich Presence to the bot. + permission: owner + category: Utility + usage: addpresence [presence] + +rotate_presence: + name: rotate_presence + description: > + Forces a presence rotation + permission: owner + category: Utility + +create_user: + name: create_user + description: > + Creates a user for the webinterface. + permission: owner + category: Utility + args: + - username + - password + - scope + +bugreport: + name: bug + description: > + Get information about where to report bugs. + permission: all + category: Utility + response: + title: > + You want to report a bug? + bug_report: > + Please report your bugs [here](https://github.com/Trivernis/discordbot.js/issues) diff --git a/lib/commands/UtilityCommands/index.js b/lib/commands/UtilityCommands/index.js new file mode 100644 index 0000000..e69de29 diff --git a/lib/utils.js b/lib/utils.js index 3975f2d..747af42 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -115,6 +115,18 @@ exports.getSplitDuration = function (duration) { return retObj; }; +/** + * Resolves a nested promise by resolving it iterative. + * @param promise + * @returns {Promise<*>} + */ +exports.resolveNestedPromise = async function(promise) { + let result = await promise; + while (result instanceof Promise) + result = await result; // eslint-disable-line no-await-in-loop + return result; +}; + /* Classes */ exports.YouTube = class { diff --git a/lib/weblib.js b/lib/weblib.js index bd5cef2..b28458e 100644 --- a/lib/weblib.js +++ b/lib/weblib.js @@ -23,7 +23,7 @@ exports.WebServer = class { this.app = express(); this.server = null; this.port = port; - this.schema = buildSchema(fs.readFileSync('./lib/graphql/schema.gql', 'utf-8')); + this.schema = buildSchema(fs.readFileSync('./lib/api/graphql/schema.gql', 'utf-8')); this.root = {}; }