From 022318005386018002ab8962ada8ae12fd2530f6 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Thu, 28 Feb 2019 21:01:06 +0100 Subject: [PATCH] Started restructuring command handling --- commands/globalcommands.json | 9 + lib/CommandLib.js | 181 ++++++++++++++++++ lib/MessageLib.js | 122 ++++++++++++ lib/{anilistApiLib.js => api/AnilistApi.js} | 14 +- .../graphql/AnilistApi/AnimeById.gql | 0 .../graphql/AnilistApi/MangaById.gql | 0 .../graphql/AnilistApi/MediaSearchByName.gql | 0 lib/{ => api}/graphql/schema.gql | 0 lib/cmd.js | 125 ++++++++---- .../AniListCommandsLogic.js | 56 ++++++ .../AniListCommandsTemplate.yaml | 0 lib/logging.js | 10 +- package.json | 6 +- 13 files changed, 469 insertions(+), 54 deletions(-) create mode 100644 lib/CommandLib.js create mode 100644 lib/MessageLib.js rename lib/{anilistApiLib.js => api/AnilistApi.js} (91%) rename lib/{ => api}/graphql/AnilistApi/AnimeById.gql (100%) rename lib/{ => api}/graphql/AnilistApi/MangaById.gql (100%) rename lib/{ => api}/graphql/AnilistApi/MediaSearchByName.gql (100%) rename lib/{ => api}/graphql/schema.gql (100%) create mode 100644 lib/commands/AnilistApiCommands/AniListCommandsLogic.js create mode 100644 lib/commands/AnilistApiCommands/AniListCommandsTemplate.yaml diff --git a/commands/globalcommands.json b/commands/globalcommands.json index ce90dbb..ca49c38 100644 --- a/commands/globalcommands.json +++ b/commands/globalcommands.json @@ -94,6 +94,15 @@ "response": { "not_found": "The Anime was not found :(" } + }, + "mangaSearch": { + "name": "manga", + "permission": "all", + "description": "Answers the manga found for that name on AniList.", + "category": "AniList", + "response": { + "not_found": "The Manga was not found :(" + } } } } diff --git a/lib/CommandLib.js b/lib/CommandLib.js new file mode 100644 index 0000000..eb86255 --- /dev/null +++ b/lib/CommandLib.js @@ -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} + */ + 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 +}); diff --git a/lib/MessageLib.js b/lib/MessageLib.js new file mode 100644 index 0000000..9f7b9df --- /dev/null +++ b/lib/MessageLib.js @@ -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>} + * @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(/(? x.replace(/^ +/, '')); + for (let indepCommand in independentCommands) + commandSequence.push(indepCommand + .split(/(? 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); + } +} diff --git a/lib/anilistApiLib.js b/lib/api/AnilistApi.js similarity index 91% rename from lib/anilistApiLib.js rename to lib/api/AnilistApi.js index d1fa6a8..33a8916 100644 --- a/lib/anilistApiLib.js +++ b/lib/api/AnilistApi.js @@ -6,7 +6,7 @@ const fetch = require('node-fetch'), /** * Return a graphql query read from a file from a configured path. * @param name - * @returns {Promise<*>} + * @returns {Promise} */ async function getGraphqlQuery(name) { return await fsx.readFile(`${queryPath}/${name}.gql`, {encoding: 'utf-8'}); @@ -16,7 +16,7 @@ async function getGraphqlQuery(name) { * Post a query read from a file to the configured graphql endpoint and return the data. * @param queryName * @param queryVariables - * @returns {Promise} + * @returns {Promise} */ function postGraphqlQuery(queryName, queryVariables) { return new Promise(async (resolve, reject) => { @@ -40,7 +40,7 @@ function postGraphqlQuery(queryName, queryVariables) { /** * Get an anime by id. * @param id - * @returns {Promise} + * @returns {Promise} */ exports.getAnimeById = async function(id) { let data = await postGraphqlQuery('AnimeById', {id: id}); @@ -53,7 +53,7 @@ exports.getAnimeById = async function(id) { /** * Get a manga by id. * @param id - * @returns {Promise} + * @returns {Promise} */ exports.getMangaById = async function(id) { let data = await postGraphqlQuery('MangaById', {id: id}); @@ -66,7 +66,7 @@ exports.getMangaById = async function(id) { /** * Search for a media entry by name and return it. * @param name - * @returns {Promise} + * @returns {Promise} */ exports.searchMediaByName = async function(name) { let data = await postGraphqlQuery('MediaSearchByName', {name: name}); @@ -95,9 +95,9 @@ exports.searchAnimeByName = async function(name) { * @returns {Promise<*>} */ exports.searchMangaByName = async function(name) { - let data = await postGraphqlQuery('MediaSearchByName', {name: name, type: 'MANGA'}).data; + let data = await postGraphqlQuery('MediaSearchByName', {name: name, type: 'MANGA'}); if (data && data.Media && data.Media.id) - return await postGraphqlQuery('MangaById', {id: data.Media.id}); + return await exports.getMangaById(data.Media.id); else return null; }; diff --git a/lib/graphql/AnilistApi/AnimeById.gql b/lib/api/graphql/AnilistApi/AnimeById.gql similarity index 100% rename from lib/graphql/AnilistApi/AnimeById.gql rename to lib/api/graphql/AnilistApi/AnimeById.gql diff --git a/lib/graphql/AnilistApi/MangaById.gql b/lib/api/graphql/AnilistApi/MangaById.gql similarity index 100% rename from lib/graphql/AnilistApi/MangaById.gql rename to lib/api/graphql/AnilistApi/MangaById.gql diff --git a/lib/graphql/AnilistApi/MediaSearchByName.gql b/lib/api/graphql/AnilistApi/MediaSearchByName.gql similarity index 100% rename from lib/graphql/AnilistApi/MediaSearchByName.gql rename to lib/api/graphql/AnilistApi/MediaSearchByName.gql diff --git a/lib/graphql/schema.gql b/lib/api/graphql/schema.gql similarity index 100% rename from lib/graphql/schema.gql rename to lib/api/graphql/schema.gql diff --git a/lib/cmd.js b/lib/cmd.js index 2f36c66..c142d62 100644 --- a/lib/cmd.js +++ b/lib/cmd.js @@ -14,7 +14,7 @@ let logger = require('winston'), /** * @type {Servant} */ -exports.Servant = class { +class Servant { constructor(prefix) { this.commands = {}; this.prefix = prefix; @@ -31,7 +31,7 @@ exports.Servant = class { .addField('Usage', `\`${cmd} [${allCommands[cmd].args.join('] [')}]\``.replace('[]', '')) .addField('Description', allCommands[cmd].description) .addField('Permission Role', allCommands[cmd].role || 'all'); - else + else return 'Command not found :('; } else { @@ -110,7 +110,7 @@ exports.Servant = class { let argv = argvars.slice(nLength); logger.debug(`Executing callback for command: ${command}, kwargs: ${kwargs}, argv: ${argv}`); try { - let locResult = returnFunction? () => cmd.callback(msg, kwargs, argv) : cmd.callback(msg, kwargs, argv); + let locResult = returnFunction ? () => cmd.callback(msg, kwargs, argv) : cmd.callback(msg, kwargs, argv); return locResult || globResult; } catch (err) { logger.error(err.message); @@ -130,14 +130,14 @@ exports.Servant = class { let commands = content.split(/(? x.replace(/^ +/, '')); if (commands.length === 1) { return this.processCommand(msg, globResult, content); - } else if (commands.length < (config.maxCmdSequenceLength || 5)) { + } else if (commands.length < (config.maxCmdSequenceLength || 5)) { let answers = []; let previousCommand = (commands[0].match(/^.\w+/) || [])[0]; for (let i = 0; i < commands.length; i++) { answers.push(this.processCommand(msg, globResult[i], commands[i], true, previousCommand)); // return function to avoid "race conditions" let commandMatch = (commands[i].match(/^.\w+/) || [])[0]; - previousCommand = this.commands[commandMatch]? commandMatch : previousCommand; + previousCommand = this.commands[commandMatch] ? commandMatch : previousCommand; } return answers; @@ -146,15 +146,15 @@ exports.Servant = class { } } -}; +} /** * Getting the logger * @param {Object} newLogger */ -exports.setLogger = function (newLogger) { +function setModuleLogger(newLogger) { logger = newLogger; -}; +} /** * Creates a global command that can be executed in every channel. @@ -162,7 +162,7 @@ exports.setLogger = function (newLogger) { * @param template * @param call */ -exports.createGlobalCommand = function (prefix, template, call) { +function createGlobalCommand(prefix, template, call) { if (!template.name) { logger.debug(`Name of command template is null or undef. Failed to create command.`); return; @@ -176,7 +176,7 @@ exports.createGlobalCommand = function (prefix, template, call) { 'category': template.category || 'Other' }; logger.debug(`Created global command: ${prefix + template.name}, args: ${template.args}`); -}; +} /** @@ -191,9 +191,9 @@ exports.parseMessage = function (msg) { /** * Initializes the module by creating a help command */ -exports.init = function (prefix) { +function initModule(prefix) { logger.verbose("Creating help command..."); - this.createGlobalCommand((prefix || config.prefix), gcmdTempl.utils.help, (msg, kwargs) => { + createGlobalCommand((prefix || config.prefix), gcmdTempl.utils.help, (msg, kwargs) => { if (kwargs.command) { let cmd = kwargs.command; if (cmd.charAt(0) !== prefix) @@ -209,7 +209,7 @@ exports.init = function (prefix) { return createHelpEmbed(globCommands, msg, prefix); } }); -}; +} /** * Processes commands for command series. @@ -227,7 +227,7 @@ function processCommand(cmd, msg, content, returnFunction) { kwargs[cmd.args[i]] = argvars[i]; let argv = argvars.slice(nLength); logger.debug(`Executing callback for command: ${cmd.name}, kwargs: ${JSON.stringify(kwargs)}, argv: ${argv}`); - return returnFunction? () => cmd.callback(msg, kwargs, argv) : cmd.callback(msg, kwargs, argv); + return returnFunction ? () => cmd.callback(msg, kwargs, argv) : cmd.callback(msg, kwargs, argv); } /** @@ -246,12 +246,12 @@ function parseGlobalCommand(msg) { return false; logger.debug(`Permission <${cmd.role}> granted for command ${command} for user <${msg.author.tag}>`); return processCommand(cmd, msg, content); - } else if (commands.length < (config.maxCmdSequenceLength || 5)) { + } else if (commands.length < (config.maxCmdSequenceLength || 5)) { let answers = []; let previousCommand = ''; for (let commandPart of commands) { let command = (commandPart.match(/^.\w+/) || [])[0] || previousCommand; - previousCommand = globCommands[command]? command : previousCommand; + previousCommand = globCommands[command] ? command : previousCommand; if (!commandPart || !globCommands[command]) { commandPart = `${previousCommand} ${commandPart}`; command = previousCommand; @@ -316,10 +316,9 @@ function checkPermission(msg, rolePerm) { return true; if (msg.author.tag === args.owner || 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; + 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; } @@ -329,14 +328,14 @@ function checkPermission(msg, rolePerm) { * @param prefix * @param bot - the instance of the bot that called */ -exports.registerUtilityCommands = function(prefix, bot) { +function registerUtilityCommands(prefix, bot) { // responde with the commands args - exports.createGlobalCommand(prefix, gcmdTempl.utils.say, (msg, argv, args) => { + createGlobalCommand(prefix, gcmdTempl.utils.say, (msg, argv, args) => { return args.join(' '); }); // adds a presence that will be saved in the presence file and added to the rotation - exports.createGlobalCommand(prefix, gcmdTempl.utils.addpresence, async (msg, argv, args) => { + createGlobalCommand(prefix, gcmdTempl.utils.addpresence, async (msg, argv, args) => { let p = args.join(' '); this.presences.push(p); await bot.maindb.run('INSERT INTO presences (text) VALUES (?)', [p]); @@ -344,7 +343,7 @@ exports.registerUtilityCommands = function(prefix, bot) { }); // shuts down the bot after destroying the client - exports.createGlobalCommand(prefix, gcmdTempl.utils.shutdown, async (msg) => { + createGlobalCommand(prefix, gcmdTempl.utils.shutdown, async (msg) => { try { await msg.reply('Shutting down...'); logger.debug('Destroying client...'); @@ -370,7 +369,7 @@ exports.registerUtilityCommands = function(prefix, bot) { }); // forces a presence rotation - exports.createGlobalCommand(prefix, gcmdTempl.utils.rotate, () => { + createGlobalCommand(prefix, gcmdTempl.utils.rotate, () => { try { bot.client.clearInterval(this.rotator); bot.rotatePresence(); @@ -380,7 +379,7 @@ exports.registerUtilityCommands = function(prefix, bot) { } }); - exports.createGlobalCommand(prefix, gcmdTempl.utils.createUser, (msg, argv) => { + createGlobalCommand(prefix, gcmdTempl.utils.createUser, (msg, argv) => { return new Promise((resolve, reject) => { if (msg.guild) { resolve("It's not save here! Try again via PM."); @@ -398,26 +397,26 @@ exports.registerUtilityCommands = function(prefix, bot) { }); }); - exports.createGlobalCommand(prefix, gcmdTempl.utils.bugreport, () => { + createGlobalCommand(prefix, gcmdTempl.utils.bugreport, () => { return new Discord.RichEmbed() .setTitle('Where to report a bug?') .setDescription(gcmdTempl.utils.bugreport.response.bug_report); }); -}; +} /** * Registers the bot's info commands * @param prefix {String} * @param bot {Object} */ -exports.registerInfoCommands = function(prefix, bot) { +function registerInfoCommands(prefix, bot) { // ping command that returns the ping attribute of the client - exports.createGlobalCommand(prefix, gcmdTempl.info.ping, () => { + createGlobalCommand(prefix, gcmdTempl.info.ping, () => { return `Current average ping: \`${bot.client.ping} ms\``; }); // returns the time the bot is running - exports.createGlobalCommand(prefix, gcmdTempl.info.uptime, () => { + createGlobalCommand(prefix, gcmdTempl.info.uptime, () => { let uptime = utils.getSplitDuration(bot.client.uptime); return new Discord.RichEmbed().setDescription(` **${uptime.days}** days @@ -429,41 +428,42 @@ exports.registerInfoCommands = function(prefix, bot) { }); // returns the number of guilds, the bot has joined - exports.createGlobalCommand(prefix, gcmdTempl.info.guilds, () => { + createGlobalCommand(prefix, gcmdTempl.info.guilds, () => { return `Number of guilds: \`${bot.client.guilds.size}\``; }); // returns information about the bot - exports.createGlobalCommand(prefix, gcmdTempl.info.about, () => { + createGlobalCommand(prefix, gcmdTempl.info.about, () => { return new Discord.RichEmbed() .setTitle('About') .setDescription(gcmdTempl.info.about.response.about_creator) .addField('Icon', gcmdTempl.info.about.response.about_icon); }); -}; +} /** * Registers all commands that use the anilist api. * @param prefix {String} */ -exports.registerAnilistApiCommands = function(prefix) { - const anilistApi = require('./anilistApiLib'); +function registerAnilistApiCommands(prefix) { + const anilistApi = require('./api/AnilistApi'); // returns the anime found for the name - exports.createGlobalCommand(prefix, gcmdTempl.api.AniList.animeSearch, async (msg, kwargs, argv) => { + createGlobalCommand(prefix, gcmdTempl.api.AniList.animeSearch, async (msg, kwargs, args) => { try { - let animeData = await anilistApi.searchAnimeByName(argv.join(' ')); + let animeData = await anilistApi.searchAnimeByName(args.join(' ')); if (animeData) { - let response = new Discord.RichEmbed() + let response = new Discord.RichEmbed() .setTitle(animeData.title.romaji) .setDescription(animeData.description.replace(/<\/?.*?>/g, '')) .setThumbnail(animeData.coverImage.large) .setURL(animeData.siteUrl) .setColor(animeData.coverImage.color) .addField('Genres', animeData.genres.join(', ')) + .setFooter('Provided by anilist.co') .setTimestamp(); if (animeData.studios.studioList.length > 0) - response.addField(animeData.studios.studioList.length === 1? 'Studio' : 'Studios', animeData.studios.studioList.map(x => `[${x.name}](${x.siteUrl})`)); + response.addField(animeData.studios.studioList.length === 1 ? 'Studio' : 'Studios', animeData.studios.studioList.map(x => `[${x.name}](${x.siteUrl})`)); response.addField('Scoring', `**Average Score:** ${animeData.averageScore} **Favourites:** ${animeData.favourites}`); @@ -477,7 +477,7 @@ exports.registerAnilistApiCommands = function(prefix) { if (animeData.nextAiringEpisode) response.addField('Next Episode', `**Episode** ${animeData.nextAiringEpisode.episode} - **Airing at:** ${new Date(animeData.nextAiringEpisode.airingAt*1000).toUTCString()}`); + **Airing at:** ${new Date(animeData.nextAiringEpisode.airingAt * 1000).toUTCString()}`); if (animeData.endDate.day) response.addField('End Date', ` @@ -496,4 +496,45 @@ exports.registerAnilistApiCommands = function(prefix) { return gcmdTempl.api.AniList.animeSearch.response.not_found; } }); -}; + + createGlobalCommand(prefix, gcmdTempl.api.AniList.mangaSearch, async (msg, kwargs, args) => { + try { + let mangaData = await anilistApi.searchMangaByName(args.join(' ')); + if (mangaData) { + let response = new Discord.RichEmbed() + .setTitle(mangaData.title.romaji) + .setThumbnail(mangaData.coverImage.large) + .setDescription(mangaData.description.replace(/<\/?.*?>/g, '')) + .setURL(mangaData.siteUrl) + .setFooter('Provided by anilist.co') + .setTimestamp(); + if (mangaData.endDate.day) + response.addField('End Date', ` + ${mangaData.endDate.day}.${mangaData.endDate.month}.${mangaData.endDate.year}`); + return response; + } else { + return gcmdTempl.api.AniList.mangaSearch.response.not_found; + } + } catch (err) { + if (err.message) { + logger.warn(err.message); + logger.debug(err.stack); + } else { + logger.debug(JSON.stringify(err)); + } + return gcmdTempl.api.AniList.mangaSearch.response.not_found; + } + }); +} + +// -- exports -- // + +Object.assign(exports, { + init: initModule, + Servant: Servant, + registerAnilistApiCommands: registerAnilistApiCommands, + registerInfoCommands: registerInfoCommands, + registerUtilityCommands: registerUtilityCommands, + setLogger: setModuleLogger, + createGlobalCommand: createGlobalCommand +}); diff --git a/lib/commands/AnilistApiCommands/AniListCommandsLogic.js b/lib/commands/AnilistApiCommands/AniListCommandsLogic.js new file mode 100644 index 0000000..173514f --- /dev/null +++ b/lib/commands/AnilistApiCommands/AniListCommandsLogic.js @@ -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 +}); diff --git a/lib/commands/AnilistApiCommands/AniListCommandsTemplate.yaml b/lib/commands/AnilistApiCommands/AniListCommandsTemplate.yaml new file mode 100644 index 0000000..e69de29 diff --git a/lib/logging.js b/lib/logging.js index 2ff994a..8d8caa4 100644 --- a/lib/logging.js +++ b/lib/logging.js @@ -7,16 +7,17 @@ const winston = require('winston'), return `${info.timestamp} ${info.level.toUpperCase()}: ${JSON.stringify(info.message)}`; // the logging format for files }), consoleLoggingFormat = winston.format.printf(info => { - return `${info.timestamp} [${info.level}] ${JSON.stringify(info.message)}`; //the logging format for the console + return `${info.timestamp} {${info.label}} [${info.level}] ${JSON.stringify(info.message)}`; //the logging format for the console }), loggingFullFormat = winston.format.combine( winston.format.splat(), winston.format.timestamp({ format: 'YY-MM-DD HH:mm:ss.SSS' }), + winston.format.label({label: ''}), winston.format.json() - ), - logger = winston.createLogger({ + ); +let logger = winston.createLogger({ level: winston.config.npm.levels, // logs with npm levels format: loggingFullFormat, transports: [ @@ -27,6 +28,7 @@ const winston = require('winston'), winston.format.timestamp({ format: 'YY-MM-DD HH:mm:ss.SSS' }), + winston.format.label({label: ''}), consoleLoggingFormat ), level: args.loglevel || 'info' @@ -48,6 +50,8 @@ const winston = require('winston'), ] }); +//class SpecialLogger extends winston. + /** * A function to return the logger that has been created after appending an exception handler * @returns {Object} diff --git a/package.json b/package.json index a0c7e26..21236a7 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,8 @@ "winston": "3.2.1", "winston-daily-rotate-file": "3.7.0", "youtube-playlist-info": "1.1.2", - "ytdl-core": "0.29.1" + "ytdl-core": "0.29.1", + "js-yaml": "latest" }, "devDependencies": { "assert": "1.4.1", @@ -47,7 +48,8 @@ }, "eslintConfig": { "parserOptions": { - "ecmaVersion": 2018 + "ecmaVersion": 2018, + "sourceType": "module" }, "env": { "node": true,