From cb4df8862d16ddf8d3bf043308494edd178d2ef0 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sun, 10 Feb 2019 14:16:44 +0000 Subject: [PATCH 01/26] Pin dependency promise-waterfall to 0.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2767f8d..7b5d6d7 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "js-sha512": "0.8.0", "node-sass": "4.11.0", "opusscript": "0.0.6", - "promise-waterfall": "^0.1.0", + "promise-waterfall": "0.1.0", "pug": "2.0.3", "sqlite3": "4.0.6", "winston": "3.2.1", From 5db721c91ea52645f34ba1eab9903031711d36e0 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 13 Feb 2019 16:03:57 +0000 Subject: [PATCH 02/26] Update dependency winston-daily-rotate-file to v3.7.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2767f8d..fcb7caa 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "pug": "2.0.3", "sqlite3": "4.0.6", "winston": "3.2.1", - "winston-daily-rotate-file": "3.6.0", + "winston-daily-rotate-file": "3.7.0", "youtube-playlist-info": "1.1.2", "ytdl-core": "0.29.1" }, From 6b8b0a6c158b5a3ee8bcee9c603305e44678e545 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 14 Feb 2019 23:22:49 +0000 Subject: [PATCH 03/26] Update dependency nyc to v13.3.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2767f8d..910fcd3 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "assert": "1.4.1", "chai": "4.2.0", "mocha": "5.2.0", - "nyc": "13.2.0", + "nyc": "13.3.0", "rewire": "4.0.1", "sinon": "7.2.3", "eslint-plugin-graphql": "3.0.3", From 6a924e2c365255381bc34aa1d9cbcf45d073fafa Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 18 Feb 2019 15:25:24 +0000 Subject: [PATCH 04/26] Update dependency sinon to v7.2.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2767f8d..7224618 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "mocha": "5.2.0", "nyc": "13.2.0", "rewire": "4.0.1", - "sinon": "7.2.3", + "sinon": "7.2.4", "eslint-plugin-graphql": "3.0.3", "eslint": "5.13.0", "eslint-plugin-promise": "4.0.1" From b96564c1b8be894c53e25eaf09ecf14ab86b1feb Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 18 Feb 2019 17:45:23 +0000 Subject: [PATCH 05/26] Update dependency eslint to v5.14.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2767f8d..0945170 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "rewire": "4.0.1", "sinon": "7.2.3", "eslint-plugin-graphql": "3.0.3", - "eslint": "5.13.0", + "eslint": "5.14.1", "eslint-plugin-promise": "4.0.1" }, "eslintConfig": { From 7d8b6a0aedacf33e7ab1742373748345aef9f9ae Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 18 Feb 2019 21:29:24 +0000 Subject: [PATCH 06/26] Update dependency mocha to v6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2767f8d..aeef9b4 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "devDependencies": { "assert": "1.4.1", "chai": "4.2.0", - "mocha": "5.2.0", + "mocha": "6.0.0", "nyc": "13.2.0", "rewire": "4.0.1", "sinon": "7.2.3", From 531672d13cf342a44f00da92bce654c682ecec0d Mon Sep 17 00:00:00 2001 From: Trivernis Date: Thu, 21 Feb 2019 17:41:56 +0100 Subject: [PATCH 07/26] Fixed Syntax error in package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e57a3a1..81582f1 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "assert": "1.4.1", "chai": "4.2.0", "mocha": "6.0.0", - "nyc": "13.3.0",, + "nyc": "13.3.0", "rewire": "4.0.1", "sinon": "7.2.4", "eslint-plugin-graphql": "3.0.3", From 65c1271bfe9ff05b45dc346231cc1e16e03a3ffd Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sun, 24 Feb 2019 01:36:28 +0000 Subject: [PATCH 08/26] Update dependency winston-daily-rotate-file to v3.8.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 81582f1..e4ac135 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "pug": "2.0.3", "sqlite3": "4.0.6", "winston": "3.2.1", - "winston-daily-rotate-file": "3.7.0", + "winston-daily-rotate-file": "3.8.0", "youtube-playlist-info": "1.1.2", "ytdl-core": "0.29.1" }, From 66fb213c7fd4d53a3ab7caf40a85c85e4da4b423 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 25 Feb 2019 18:33:03 +0000 Subject: [PATCH 09/26] Update dependency mocha to v6.0.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 81582f1..7b13a35 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "devDependencies": { "assert": "1.4.1", "chai": "4.2.0", - "mocha": "6.0.0", + "mocha": "6.0.2", "nyc": "13.3.0", "rewire": "4.0.1", "sinon": "7.2.4", From 42ee8cc4c5339855c6b815eda24e8dcf096131ac Mon Sep 17 00:00:00 2001 From: Trivernis Date: Tue, 26 Feb 2019 20:57:26 +0100 Subject: [PATCH 10/26] Added Anilist Api - moved graphql schemas and queries to lib/graphql - added anilistApiLib that contains functions to access the anilist graphql api - added graphql queries to lib/graphql to access data on anilist - added global ~anime command that returns information to an anime - modified help command so that it shows command categories for global and server commands - moved global command registration to lib/cmd --- bot.js | 122 +------- commands/globalcommands.json | 15 +- lib/anilistApiLib.js | 103 +++++++ lib/cmd.js | 264 +++++++++++++++--- lib/graphql/AnilistApi/AnimeById.gql | 47 ++++ lib/graphql/AnilistApi/MangaById.gql | 53 ++++ lib/graphql/AnilistApi/MediaSearchByName.gql | 11 + .../schema.graphql => lib/graphql/schema.gql | 0 lib/weblib.js | 5 +- package.json | 3 +- 10 files changed, 466 insertions(+), 157 deletions(-) create mode 100644 lib/anilistApiLib.js create mode 100644 lib/graphql/AnilistApi/AnimeById.gql create mode 100644 lib/graphql/AnilistApi/MangaById.gql create mode 100644 lib/graphql/AnilistApi/MediaSearchByName.gql rename web/graphql/schema.graphql => lib/graphql/schema.gql (100%) diff --git a/bot.js b/bot.js index 02e1c63..fb6ef09 100644 --- a/bot.js +++ b/bot.js @@ -8,7 +8,6 @@ const Discord = require("discord.js"), args = require('args-parser')(process.argv), waterfall = require('promise-waterfall'), sqliteAsync = require('./lib/sqliteAsync'), - globcommands = require('./commands/globalcommands.json'), authToken = args.token || config.api.botToken, prefix = args.prefix || config.prefix || '~', gamepresence = args.game || config.presence; @@ -47,6 +46,7 @@ class Bot { */ async initServices() { logger.verbose('Registering cleanup function'); + utils.Cleanup(() => { for (let gh in Object.values(this.guildHandlers)) if (gh instanceof guilding.GuildHandler) @@ -61,6 +61,7 @@ class Bot { this.maindb.close(); }); await this.initializeDatabase(); + if (config.webservice && config.webservice.enabled) await this.initializeWebserver(); logger.verbose('Registering commands'); @@ -76,6 +77,7 @@ class Bot { async start() { await this.client.login(authToken); logger.debug("Logged in"); + if (this.webServer) { this.webServer.start(); logger.info(`WebServer runing on port ${this.webServer.port}`); @@ -92,6 +94,7 @@ class Bot { logger.verbose('Connecting to main database'); this.maindb = new sqliteAsync.Database('./data/main.db'); await this.maindb.init(); + await this.maindb.run(`${utils.sql.tableExistCreate} presences ( ${utils.sql.pkIdSerial}, text VARCHAR(255) UNIQUE NOT NULL @@ -161,109 +164,9 @@ class Bot { * registeres global commands */ registerCommands() { - // useless test command - cmd.createGlobalCommand(prefix, globcommands.utils.say, (msg, argv, args) => { - return args.join(' '); - }); - - // adds a presence that will be saved in the presence file and added to the rotation - cmd.createGlobalCommand(prefix, globcommands.utils.addpresence, async (msg, argv, args) => { - let p = args.join(' '); - this.presences.push(p); - - await this.maindb.run('INSERT INTO presences (text) VALUES (?)', [p]); - return `Added Presence \`${p}\``; - }); - - // shuts down the bot after destroying the client - cmd.createGlobalCommand(prefix, globcommands.utils.shutdown, async (msg) => { - try { - await msg.reply('Shutting down...'); - logger.debug('Destroying client...'); - } catch (err) { - logger.error(err.message); - logger.debug(err.stack); - } - try { - await this.client.destroy(); - logger.debug('Exiting server...'); - } catch (err) { - logger.error(err.message); - logger.debug(err.stack); - } - try { - await this.webServer.stop(); - logger.debug(`Exiting Process...`); - process.exit(0); - } catch (err) { - logger.error(err.message); - logger.debug(err.stack); - } - }); - - // forces a presence rotation - cmd.createGlobalCommand(prefix, globcommands.utils.rotate, () => { - try { - this.client.clearInterval(this.rotator); - this.rotatePresence(); - this.rotator = this.client.setInterval(() => this.rotatePresence(), config.presence_duration); - } catch (error) { - logger.warn(error.message); - } - }); - - // ping command that returns the ping attribute of the client - cmd.createGlobalCommand(prefix, globcommands.info.ping, () => { - return `Current average ping: \`${this.client.ping} ms\``; - }); - - // returns the time the bot is running - cmd.createGlobalCommand(prefix, globcommands.info.uptime, () => { - let uptime = utils.getSplitDuration(this.client.uptime); - return new Discord.RichEmbed().setDescription(` - **${uptime.days}** days - **${uptime.hours}** hours - **${uptime.minutes}** minutes - **${uptime.seconds}** seconds - **${uptime.milliseconds}** milliseconds - `).setTitle('Uptime'); - }); - - // returns the numbe of guilds, the bot has joined - cmd.createGlobalCommand(prefix, globcommands.info.guilds, () => { - return `Number of guilds: \`${this.client.guilds.size}\``; - }); - - cmd.createGlobalCommand(prefix, globcommands.utils.createUser, (msg, argv) => { - return new Promise((resolve, reject) => { - if (msg.guild) { - resolve("It's not save here! Try again via PM."); - } else if (argv.username && argv.scope) { - logger.debug(`Creating user entry ${argv.username}, scope: ${argv.scope}`); - - this.webServer.createUser(argv.username, argv.password, argv.scope, false).then((token) => { - resolve(`Created entry - username: ${argv.username}, - scope: ${argv.scope}, - token: ${token} - `); - }).catch((err) => reject(err.message)); - } - }); - }); - - cmd.createGlobalCommand(prefix, globcommands.info.about, () => { - return new Discord.RichEmbed() - .setTitle('About') - .setDescription(globcommands.info.about.response.about_creator) - .addField('Icon', globcommands.info.about.response.about_icon); - }); - - cmd.createGlobalCommand(prefix, globcommands.utils.bugreport, () => { - return new Discord.RichEmbed() - .setTitle('Where to report a bug?') - .setDescription(globcommands.utils.bugreport.response.bug_report); - }); + cmd.registerUtilityCommands(prefix, this); + cmd.registerInfoCommands(prefix, this); + cmd.registerAnilistApiCommands(prefix); } /** @@ -292,15 +195,15 @@ class Bot { this.client.on('ready', () => { logger.info(`logged in as ${this.client.user.tag}!`); + this.client.user.setPresence({ game: { name: gamepresence, type: "PLAYING" }, status: 'online' - }) - .catch((err) => { - if (err) - logger.warn(err.message); - }); + }).catch((err) => { + if (err) + logger.warn(err.message); + }); }); this.client.on('message', async (msg) => { @@ -336,6 +239,7 @@ class Bot { this.client.on('voiceStateUpdate', async (oldMember, newMember) => { let gh = await this.getGuildHandler(newMember.guild, prefix); + if (newMember.user === this.client.user) { if (newMember.voiceChannel) gh.dj.updateChannel(newMember.voiceChannel); diff --git a/commands/globalcommands.json b/commands/globalcommands.json index 7ee7886..ce90dbb 100644 --- a/commands/globalcommands.json +++ b/commands/globalcommands.json @@ -80,8 +80,21 @@ "guilds": { "name": "guilds", "permission": "owner", - "description": "Answers with the number of guilds the bot has joined", + "description": "Answers with the number of guilds the bot has joined.", "category": "Info" } + }, + "api": { + "AniList": { + "animeSearch": { + "name": "anime", + "permission": "all", + "description": "Answers the anime found for that name on AniList.", + "category": "AniList", + "response": { + "not_found": "The Anime was not found :(" + } + } + } } } diff --git a/lib/anilistApiLib.js b/lib/anilistApiLib.js new file mode 100644 index 0000000..d1fa6a8 --- /dev/null +++ b/lib/anilistApiLib.js @@ -0,0 +1,103 @@ +const fetch = require('node-fetch'), + fsx = require('fs-extra'), + queryPath = './lib/graphql/AnilistApi', + alApiEndpoint = 'https://graphql.anilist.co'; + +/** + * Return a graphql query read from a file from a configured path. + * @param name + * @returns {Promise<*>} + */ +async function getGraphqlQuery(name) { + return await fsx.readFile(`${queryPath}/${name}.gql`, {encoding: 'utf-8'}); +} + +/** + * Post a query read from a file to the configured graphql endpoint and return the data. + * @param queryName + * @param queryVariables + * @returns {Promise} + */ +function postGraphqlQuery(queryName, queryVariables) { + return new Promise(async (resolve, reject) => { + fetch(alApiEndpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify({ + query: await getGraphqlQuery(queryName), + variables: queryVariables + }) + }).then(async (response) => { + let json = await response.json(); + return response.ok ? json: Promise.reject(json); + }).then((data) => resolve(data.data)).catch((err) => reject(err)); + }); +} + +/** + * Get an anime by id. + * @param id + * @returns {Promise} + */ +exports.getAnimeById = async function(id) { + let data = await postGraphqlQuery('AnimeById', {id: id}); + if (data.Media) + return data.Media; + else + return null; +}; + +/** + * Get a manga by id. + * @param id + * @returns {Promise} + */ +exports.getMangaById = async function(id) { + let data = await postGraphqlQuery('MangaById', {id: id}); + if (data.Media) + return data.Media; + else + return null; +}; + +/** + * Search for a media entry by name and return it. + * @param name + * @returns {Promise} + */ +exports.searchMediaByName = async function(name) { + let data = await postGraphqlQuery('MediaSearchByName', {name: name}); + if (data.Media) + return data.Media; + else + return null; +}; + +/** + * Search for an anime by name and get it by id. + * @param name + * @returns {Promise<*>} + */ +exports.searchAnimeByName = async function(name) { + let data = await postGraphqlQuery('MediaSearchByName', {name: name, type: 'ANIME'}); + if (data && data.Media && data.Media.id) + return await exports.getAnimeById(data.Media.id); + else + return null; +}; + +/** + * Search for a manga by name and get it by id. + * @param name + * @returns {Promise<*>} + */ +exports.searchMangaByName = async function(name) { + let data = await postGraphqlQuery('MediaSearchByName', {name: name, type: 'MANGA'}).data; + if (data && data.Media && data.Media.id) + return await postGraphqlQuery('MangaById', {id: data.Media.id}); + else + return null; +}; diff --git a/lib/cmd.js b/lib/cmd.js index a6dd546..2f36c66 100644 --- a/lib/cmd.js +++ b/lib/cmd.js @@ -5,7 +5,8 @@ const Discord = require('discord.js'), args = require('args-parser')(process.argv), config = require('../config.json'), gcmdTempl = require('../commands/globalcommands'), - scmdTempl = require('../commands/servercommands'); + scmdTempl = require('../commands/servercommands'), + utils = require('./utils'); let logger = require('winston'), globCommands = {}; @@ -34,34 +35,8 @@ exports.Servant = class { return 'Command not found :('; } else { - let helpEmbed = new Discord.RichEmbed() - .setTitle('Commands') - .setDescription('Create a sequence of commands with `;` (semicolon).') - .setTimestamp(); - let globHelp = ''; - Object.entries(globCommands).sort().forEach(([key, value]) => { - if (value.role !== 'owner' || checkPermission(msg, 'owner')) - globHelp += `\`${key}\` \t`; - - }); - helpEmbed.addField('Global Commands', globHelp); - let categories = []; - let catCommands = {}; - Object.entries(this.commands).sort().forEach(([key, value]) => { - if (value.role !== 'owner' || checkPermission(msg, 'owner')) - if (!categories.includes(value.category)) { - categories.push(value.category); - catCommands[value.category] = `\`${key}\` \t`; - } else { - catCommands[value.category] += `\`${key}\` \t`; - } - - }); - for (let cat of categories) - helpEmbed.addField(cat, catCommands[cat]); - - helpEmbed.setFooter(prefix + 'help [command] for more info to each command'); - return helpEmbed; + let allCommands = {...globCommands, ...this.commands}; + return createHelpEmbed(allCommands, msg, prefix); } }); @@ -217,7 +192,7 @@ exports.parseMessage = function (msg) { * Initializes the module by creating a help command */ exports.init = function (prefix) { - logger.verbose("Created help command"); + logger.verbose("Creating help command..."); this.createGlobalCommand((prefix || config.prefix), gcmdTempl.utils.help, (msg, kwargs) => { if (kwargs.command) { let cmd = kwargs.command; @@ -231,25 +206,19 @@ exports.init = function (prefix) { .addField('Permission Role', globCommands[cmd].role || 'all'); } else { - let helpEmbed = new Discord.RichEmbed() - .setTitle('Global Commands') - .setDescription('Create a sequence of commands with `;` (semicolon).') - .setTimestamp(); - let description = ''; - Object.entries(globCommands).sort().forEach(([key, value]) => { - if (value.role === 'owner' && checkPermission(msg, 'owner')) - description += `\`${key}\` \t`; - else if (value.role !== 'owner') - description += `\`${key}\` \t`; - - }); - helpEmbed.setFooter(prefix + 'help [command] for more info to each command'); - helpEmbed.setDescription(description); - return helpEmbed; + return createHelpEmbed(globCommands, msg, prefix); } }); }; +/** + * Processes commands for command series. + * @param cmd + * @param msg + * @param content + * @param returnFunction + * @returns {function(): *} + */ function processCommand(cmd, msg, content, returnFunction) { let argvars = content.match(/(?<= )\S+/g) || []; let kwargs = {}; @@ -306,6 +275,37 @@ function parseGlobalCommand(msg) { } } +/** + * Creates a rich embed that contains help for all commands in the commands object + * @param commands {Object} + * @param msg {module:discord.js.Message} + * @param prefix {String} + * @returns {module:discord.js.RichEmbed} + */ +function createHelpEmbed(commands, msg, prefix) { + let helpEmbed = new Discord.RichEmbed() + .setTitle('Commands') + .setDescription('Create a sequence of commands with `;` (semicolon).') + .setTimestamp(); + let categories = []; + let catCommands = {}; + Object.entries(commands).sort().forEach(([key, value]) => { + if (value.role !== 'owner' || checkPermission(msg, 'owner')) + if (!categories.includes(value.category)) { + categories.push(value.category); + catCommands[value.category] = `\`${key}\` \t`; + } else { + catCommands[value.category] += `\`${key}\` \t`; + } + + }); + for (let cat of categories) + helpEmbed.addField(cat, catCommands[cat]); + + helpEmbed.setFooter(prefix + 'help [command] for more info to each command'); + return helpEmbed; +} + /** * @param msg * @param rolePerm {String} @@ -323,3 +323,177 @@ function checkPermission(msg, rolePerm) { return false; } + +/** + * Registers the bot's utility commands + * @param prefix + * @param bot - the instance of the bot that called + */ +exports.registerUtilityCommands = function(prefix, bot) { + // responde with the commands args + exports.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) => { + let p = args.join(' '); + this.presences.push(p); + await bot.maindb.run('INSERT INTO presences (text) VALUES (?)', [p]); + return `Added Presence \`${p}\``; + }); + + // shuts down the bot after destroying the client + exports.createGlobalCommand(prefix, gcmdTempl.utils.shutdown, async (msg) => { + try { + await msg.reply('Shutting down...'); + logger.debug('Destroying client...'); + } catch (err) { + logger.error(err.message); + logger.debug(err.stack); + } + try { + await bot.client.destroy(); + logger.debug('Exiting server...'); + } catch (err) { + logger.error(err.message); + logger.debug(err.stack); + } + try { + await bot.webServer.stop(); + logger.debug(`Exiting Process...`); + process.exit(0); + } catch (err) { + logger.error(err.message); + logger.debug(err.stack); + } + }); + + // forces a presence rotation + exports.createGlobalCommand(prefix, gcmdTempl.utils.rotate, () => { + try { + bot.client.clearInterval(this.rotator); + bot.rotatePresence(); + bot.rotator = this.client.setInterval(() => this.rotatePresence(), config.presence_duration); + } catch (error) { + logger.warn(error.message); + } + }); + + exports.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."); + } else if (argv.username && argv.scope) { + logger.debug(`Creating user entry ${argv.username}, scope: ${argv.scope}`); + + bot.webServer.createUser(argv.username, argv.password, argv.scope, false).then((token) => { + resolve(`Created entry + username: ${argv.username}, + scope: ${argv.scope}, + token: ${token} + `); + }).catch((err) => reject(err.message)); + } + }); + }); + + exports.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) { + // ping command that returns the ping attribute of the client + exports.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, () => { + let uptime = utils.getSplitDuration(bot.client.uptime); + return new Discord.RichEmbed().setDescription(` + **${uptime.days}** days + **${uptime.hours}** hours + **${uptime.minutes}** minutes + **${uptime.seconds}** seconds + **${uptime.milliseconds}** milliseconds + `).setTitle('Uptime'); + }); + + // returns the number of guilds, the bot has joined + exports.createGlobalCommand(prefix, gcmdTempl.info.guilds, () => { + return `Number of guilds: \`${bot.client.guilds.size}\``; + }); + + // returns information about the bot + exports.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'); + + // returns the anime found for the name + exports.createGlobalCommand(prefix, gcmdTempl.api.AniList.animeSearch, async (msg, kwargs, argv) => { + try { + let animeData = await anilistApi.searchAnimeByName(argv.join(' ')); + if (animeData) { + 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(', ')) + .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('Scoring', `**Average Score:** ${animeData.averageScore} + **Favourites:** ${animeData.favourites}`); + + if (animeData.episodes) + response.addField('Episodes', animeData.episodes); + response.addField('Season', animeData.season); + + if (animeData.startDate.day) + response.addField('Start Date', ` + ${animeData.startDate.day}.${animeData.startDate.month}.${animeData.startDate.year}`); + + if (animeData.nextAiringEpisode) + response.addField('Next Episode', `**Episode** ${animeData.nextAiringEpisode.episode} + **Airing at:** ${new Date(animeData.nextAiringEpisode.airingAt*1000).toUTCString()}`); + + if (animeData.endDate.day) + response.addField('End Date', ` + ${animeData.endDate.day}.${animeData.endDate.month}.${animeData.endDate.year}`); + return response; + } else { + return gcmdTempl.api.AniList.animeSearch.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.animeSearch.response.not_found; + } + }); +}; diff --git a/lib/graphql/AnilistApi/AnimeById.gql b/lib/graphql/AnilistApi/AnimeById.gql new file mode 100644 index 0000000..b9c5589 --- /dev/null +++ b/lib/graphql/AnilistApi/AnimeById.gql @@ -0,0 +1,47 @@ +query ($id: Int) { + Media (id: $id, type: ANIME) { + id + title { + romaji + english + native + } + status + startDate { + year + month + day + } + endDate { + year + month + day + } + format + season + episodes + duration + genres + siteUrl + coverImage { + large + medium + color + } + description(asHtml: false) + averageScore + favourites + studios(isMain: true) { + studioList: nodes { + id + name + siteUrl + } + } + nextAiringEpisode { + id + airingAt + episode + } + } +} diff --git a/lib/graphql/AnilistApi/MangaById.gql b/lib/graphql/AnilistApi/MangaById.gql new file mode 100644 index 0000000..146bf9b --- /dev/null +++ b/lib/graphql/AnilistApi/MangaById.gql @@ -0,0 +1,53 @@ +query ($id: Int) { + Media (id: $id, type: MANGA) { + id + title { + romaji + english + native + } + status + startDate { + year + month + day + } + endDate { + year + month + day + } + format + chapters + volumes + genres + siteUrl + coverImage { + large + medium + color + } + staff { + edges { + node { + id + name { + first + last + native + } + image { + large + medium + } + language + siteUrl + } + role + } + } + description(asHtml: false) + averageScore + favourites + } +} diff --git a/lib/graphql/AnilistApi/MediaSearchByName.gql b/lib/graphql/AnilistApi/MediaSearchByName.gql new file mode 100644 index 0000000..90a6afb --- /dev/null +++ b/lib/graphql/AnilistApi/MediaSearchByName.gql @@ -0,0 +1,11 @@ +query ($name: String, $type: MediaType) { + Media (search: $name, type: $type) { + id + title { + romaji + english + native + } + type + } +} diff --git a/web/graphql/schema.graphql b/lib/graphql/schema.gql similarity index 100% rename from web/graphql/schema.graphql rename to lib/graphql/schema.gql diff --git a/lib/weblib.js b/lib/weblib.js index 6704709..bd5cef2 100644 --- a/lib/weblib.js +++ b/lib/weblib.js @@ -23,10 +23,13 @@ exports.WebServer = class { this.app = express(); this.server = null; this.port = port; - this.schema = buildSchema(fs.readFileSync('./web/graphql/schema.graphql', 'utf-8')); + this.schema = buildSchema(fs.readFileSync('./lib/graphql/schema.gql', 'utf-8')); this.root = {}; } + /** + * Configures express by setting properties and middleware. + */ configureExpress() { this.app.set('view engine', 'pug'); this.app.set('trust proxy', 1); diff --git a/package.json b/package.json index 81582f1..a0c7e26 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "discordbot", - "version": "1.0.0", + "version": "0.9.1", "scripts": { "start": "node bot.js", "test": "mocha --exit", @@ -23,6 +23,7 @@ "graphql": "14.1.1", "js-md5": "0.7.3", "js-sha512": "0.8.0", + "node-fetch": "^2.3.0", "node-sass": "4.11.0", "opusscript": "0.0.6", "promise-waterfall": "0.1.0", From 022318005386018002ab8962ada8ae12fd2530f6 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Thu, 28 Feb 2019 21:01:06 +0100 Subject: [PATCH 11/26] 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, From 08dcf2f084c233e56bd9a33689e709838a948483 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Thu, 28 Feb 2019 21:01:24 +0100 Subject: [PATCH 12/26] Added 2 Anilist Commands --- .../AniListCommandsLogic.js | 47 +++++++++++++++++-- .../AniListCommandsTemplate.yaml | 23 +++++++++ 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/lib/commands/AnilistApiCommands/AniListCommandsLogic.js b/lib/commands/AnilistApiCommands/AniListCommandsLogic.js index 173514f..493dfa0 100644 --- a/lib/commands/AnilistApiCommands/AniListCommandsLogic.js +++ b/lib/commands/AnilistApiCommands/AniListCommandsLogic.js @@ -1,4 +1,5 @@ -const cmdLib = require('../../../CommandLib'), +const cmdLib = require('../../CommandLib'), + anilistApi = require('../../api/AnilistApi'), yaml = require('js-yaml'), fsx = require('fs-extra'), templateFile = 'AniListCommandsTemplate.yaml'; @@ -44,13 +45,53 @@ class RichMediaInfo extends cmdLib.ExtendedRichEmbed { let template = null; +/** + * Initializes the module. + * @returns {Promise} + */ async function init() { - let templateString = fsx.readFile(templateFile, {encoding: 'utf-8'}); + let templateString = await fsx.readFile(templateFile, {encoding: 'utf-8'}); template = yaml.safeLoad(templateString); } +/** + * 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; + } + })); + + 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; + } + }) + ); + + // registering commands + commandHandler.registerCommand(template.anime_search.name, animeSearch); + commandHandler.registerCommand(template.manga_search.name, mangaSearch); +} + // -- exports -- // Object.assign(exports, { - init: init + init: init, + register: register }); diff --git a/lib/commands/AnilistApiCommands/AniListCommandsTemplate.yaml b/lib/commands/AnilistApiCommands/AniListCommandsTemplate.yaml index e69de29..400d567 100644 --- a/lib/commands/AnilistApiCommands/AniListCommandsTemplate.yaml +++ b/lib/commands/AnilistApiCommands/AniListCommandsTemplate.yaml @@ -0,0 +1,23 @@ +anime_search: + name: anime + permission: all + usage: anime [search query] + description: > + Searches AniList.co for the anime Title and returns information about + it if there is an result. + category: AniList + response: + not_found: > + I couldn't find the anime you were searching for :( + +manga_search: + name: manga + permission: all + usage: manga [search query] + description: > + Searches AniList.co for the manga Title and returns information about + it if there is an result. + category: AniList + response: + not_found: > + I couldn't find the manga you were searching for :( From c4f3635dd63bceaca56a36dbdf5ae58da9211a06 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Fri, 1 Mar 2019 21:01:50 +0100 Subject: [PATCH 13/26] 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 = {}; } From fdcd56c9ea469259bba2ab12685b6cc216603e1c Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Fri, 1 Mar 2019 23:19:23 +0000 Subject: [PATCH 14/26] Update dependency sinon to v7.2.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1247bbd..f2d1f88 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "mocha": "6.0.2", "nyc": "13.3.0", "rewire": "4.0.1", - "sinon": "7.2.4", + "sinon": "7.2.6", "eslint-plugin-graphql": "3.0.3", "eslint": "5.14.1", "eslint-plugin-promise": "4.0.1" From 063f3d9ee442fac9461491ba5e77bf9d729e943e Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sat, 2 Mar 2019 03:17:20 +0000 Subject: [PATCH 15/26] Update dependency eslint to v5.15.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1247bbd..19823c3 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "rewire": "4.0.1", "sinon": "7.2.4", "eslint-plugin-graphql": "3.0.3", - "eslint": "5.14.1", + "eslint": "5.15.0", "eslint-plugin-promise": "4.0.1" }, "eslintConfig": { From a425607a6bedf62c306a38a0fc1501fd2415a809 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sat, 2 Mar 2019 10:43:57 +0100 Subject: [PATCH 16/26] Changed CommandHandler - registering commands no longer needs a name - added info commands implementation --- bot.js | 2 + lib/CommandLib.js | 4 +- lib/MessageLib.js | 12 +-- lib/commands/AnilistApiCommands/index.js | 4 +- lib/commands/InfoCommands/index.js | 72 +++++++++++++++ lib/commands/UtilityCommands/index.js | 113 +++++++++++++++++++++++ 6 files changed, 197 insertions(+), 10 deletions(-) diff --git a/bot.js b/bot.js index 1fa71ad..4bcf2f2 100644 --- a/bot.js +++ b/bot.js @@ -71,6 +71,8 @@ class Bot { .registerCommandModule(require('./lib/commands/AnilistApiCommands').module, {}); await this.messageHandler .registerCommandModule(require('./lib/commands/UtilityCommands').module, {bot: this, logger: logger, config: config}); + await this.messageHandler + .registerCommandModule(require('./lib/commands/InfoCommands').module, {client: this.client}); //this.registerCommands(); this.registerCallbacks(); cmd.init(prefix); diff --git a/lib/CommandLib.js b/lib/CommandLib.js index 5381a3a..07ad8e1 100644 --- a/lib/CommandLib.js +++ b/lib/CommandLib.js @@ -130,8 +130,8 @@ class CommandHandler { * @param name {String} * @param command {Command} */ - registerCommand(name, command) { - this.commands[name] = command; + registerCommand(command) { + this.commands[command.name] = command; } } diff --git a/lib/MessageLib.js b/lib/MessageLib.js index 588e32e..8545021 100644 --- a/lib/MessageLib.js +++ b/lib/MessageLib.js @@ -58,7 +58,7 @@ class MessageHandler { _registerEvents() { this.logger.debug('Registering message event...'); this.discordClient.on('message', async (msg) => { - this.logger.debug(`<${msg.channel.name || 'PRIVATE'}> ${msg.author.name}: ${msg.content}`); + this.logger.debug(`<${msg.guild? msg.channel.name+'@'+msg.guild.name : 'PRIVATE'}> ${msg.author.tag}: ${msg.content}`); if (msg.author !== this.discordClient.user) { let sequence = this._parseSyntax(msg); this.logger.debug(`Syntax parsing returned: ${JSON.stringify(sequence)}`); @@ -78,12 +78,12 @@ class MessageHandler { this.logger.debug('Parsing command sequence...'); let commandSequence = []; let content = message.content; - let strings = content.match(/".+?"/g); + let strings = content.match(/".+?"/g) || []; - for (let string in strings) - content.replace(string, string // escape all special chars - .replace(';', '\\;')) - .replace('&', '\\&'); + for (let string of strings) + content = content.replace(string, string // escape all special chars + .replace(';', '\\;') + .replace('&', '\\&')); let independentCommands = content // independent command sequende with ; .split(/(? x.replace(/^ +/, '')); diff --git a/lib/commands/AnilistApiCommands/index.js b/lib/commands/AnilistApiCommands/index.js index de844ef..160b645 100644 --- a/lib/commands/AnilistApiCommands/index.js +++ b/lib/commands/AnilistApiCommands/index.js @@ -78,8 +78,8 @@ class AniListCommandModule extends cmdLib.CommandModule { ); // registering commands - commandHandler.registerCommand(this.template.anime_search.name, animeSearch); - commandHandler.registerCommand(this.template.manga_search.name, mangaSearch); + commandHandler.registerCommand(animeSearch); + commandHandler.registerCommand(mangaSearch); } } diff --git a/lib/commands/InfoCommands/index.js b/lib/commands/InfoCommands/index.js index e69de29..3063fe5 100644 --- a/lib/commands/InfoCommands/index.js +++ b/lib/commands/InfoCommands/index.js @@ -0,0 +1,72 @@ +const cmdLib = require('../../CommandLib'), + utils = require('../../utils'), + location = './lib/commands/InfoCommands'; + +/** + * Info commands provide information about the bot. These informations are + * not process specific but access the discord client instance of the bot. + */ + +class InfoCommandModule extends cmdLib.CommandModule { + + /** + * @param opts {Object} properties: + * client - the instance of the discord client. + */ + constructor(opts) { + super(cmdLib.CommandScopes.Global); + this.templateFile = location + '/InfoCommandsTemplate.yaml'; + this.client = opts.client; + } + + async register(commandHandler) { + await this._loadTemplate(); + + let about = new cmdLib.Command( + this.template.about, + new cmdLib.Answer(() => { + return new cmdLib.ExtendedRichEmbed('About') + .setDescription(this.template.about.response.about_creator) + .addField('Icon', this.template.about.response.about_icon); + }) + ); + + let ping = new cmdLib.Command( + this.template.ping, + new cmdLib.Answer(() => { + return `Current average ping: \`${this.client.ping} ms\``; + }) + ); + + let uptime = new cmdLib.Command( + this.template.uptime, + new cmdLib.Answer(() => { + let uptime = utils.getSplitDuration(this.client.uptime); + return new cmdLib.ExtendedRichEmbed('Uptime').setDescription(` + **${uptime.days}** days + **${uptime.hours}** hours + **${uptime.minutes}** minutes + **${uptime.seconds}** seconds + **${uptime.milliseconds}** milliseconds + `).setTitle('Uptime'); + }) + ); + + let guilds = new cmdLib.Command( + this.template.guilds, + new cmdLib.Answer(() => { + return `Number of guilds: \`${this.client.guilds.size}\``; + }) + ); + + // register commands + commandHandler.registerCommand(about); + commandHandler.registerCommand(ping); + commandHandler.registerCommand(uptime); + commandHandler.registerCommand(guilds); + } +} + +Object.assign(exports, { + 'module': InfoCommandModule +}); diff --git a/lib/commands/UtilityCommands/index.js b/lib/commands/UtilityCommands/index.js index e69de29..e2c198c 100644 --- a/lib/commands/UtilityCommands/index.js +++ b/lib/commands/UtilityCommands/index.js @@ -0,0 +1,113 @@ +const cmdLib = require('../../CommandLib'), + location = './lib/commands/UtilityCommands'; + +/** + * Utility commands are all commands that allow the user to control the behaviour of the + * bot. Utility commands for example are allowed to: + * - manipulate the main database + * - manipulate the bot's presences + * - manipulate the process (e.g. shutdown) + */ + +class UtilityCommandModule extends cmdLib.CommandModule { + + /** + * @param opts {Object} properties: + * bot - the instance of the bot. + * logger - the instance of the logger. + * config - the config object + */ + constructor(opts) { + super(cmdLib.CommandScopes.User); + this.templateFile = location + '/UtilityCommandsTemplate.yaml'; + this.bot = opts.bot; + this.logger = opts.logger; + this.config = opts.config; + } + + async register(commandHandler) { + await this._loadTemplate(); + + let addPresence = new cmdLib.Command( + this.template.add_presence, + new cmdLib.Answer(async (m, k, s) => { + this.bot.presences.push(s); + await this.bot.maindb.run('INSERT INTO presences (text) VALUES (?)', [s]); + return `Added Presence \`${s}\``; + }) + ); + + let rotatePresence = new cmdLib.Command( + this.template.rotate_presence, + new cmdLib.Answer(() => { + try { + this.bot.client.clearInterval(this.rotator); + this.bot.rotatePresence(); + this.bot.rotator = this.bot.client.setInterval(() => this.bot.rotatePresence(), + this.config.presence_duration); + } catch (error) { + this.logger.warn(error.message); + } + }) + ); + + let shutdown = new cmdLib.Command( + this.template.shutdown, + new cmdLib.Answer(async (m) => { + try { + await m.reply('Shutting down...'); + this.logger.debug('Destroying client...'); + } catch (err) { + this.logger.error(err.message); + this.logger.debug(err.stack); + } + try { + await this.bot.client.destroy(); + this.logger.debug('Exiting server...'); + } catch (err) { + this.logger.error(err.message); + this.logger.debug(err.stack); + } + try { + await this.bot.webServer.stop(); + this.logger.debug(`Exiting Process...`); + process.exit(0); + } catch (err) { + this.logger.error(err.message); + this.logger.debug(err.stack); + } + }) + ); + + let createUser = new cmdLib.Command( + this.template.create_user, + new cmdLib.Answer(async (m, k) => { + if (k.username &&k.password && k.scope) { + this.logger.debug(`Creating user entry for ${k.username}`); + let token = await this.bot.webServer.createUser( + k.username, k.password, k.scope, false); + return `${k.username}'s token is ${token}`; + } + }) + ); + + let bugReport = new cmdLib.Command( + this.template.bugreport, + new cmdLib.Answer(() => { + return new cmdLib.ExtendedRichEmbed(this.template.bugreport.response.title) + .setDescription(this.template.bugreport.response.bug_report); + }) + ); + + // register commands + commandHandler.registerCommand(addPresence); + commandHandler.registerCommand(rotatePresence); + commandHandler.registerCommand(shutdown); + commandHandler.registerCommand(createUser); + commandHandler.registerCommand(bugReport); + } +} + +Object.assign(exports, { + 'module': UtilityCommandModule +}); From 556c3a5e7fc058bc676b1aea9ddcd77d9cb613f3 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sat, 2 Mar 2019 13:15:39 +0100 Subject: [PATCH 17/26] Added Servercommands - added commands for music functionalities --- bot.js | 6 +- lib/CommandLib.js | 18 +- lib/MessageLib.js | 8 +- .../AniListCommandsTemplate.yaml | 8 +- .../InfoCommands/InfoCommandsTemplate.yaml | 9 + lib/commands/InfoCommands/index.js | 58 +++- .../MusicCommands/MusicCommandsTemplate.yaml | 178 +++++++++++ lib/commands/MusicCommands/index.js | 285 ++++++++++++++++++ .../UtilityCommandsTemplate.yaml | 4 +- 9 files changed, 550 insertions(+), 24 deletions(-) create mode 100644 lib/commands/MusicCommands/MusicCommandsTemplate.yaml create mode 100644 lib/commands/MusicCommands/index.js diff --git a/bot.js b/bot.js index 4bcf2f2..d6b9b4c 100644 --- a/bot.js +++ b/bot.js @@ -72,7 +72,11 @@ class Bot { await this.messageHandler .registerCommandModule(require('./lib/commands/UtilityCommands').module, {bot: this, logger: logger, config: config}); await this.messageHandler - .registerCommandModule(require('./lib/commands/InfoCommands').module, {client: this.client}); + .registerCommandModule(require('./lib/commands/InfoCommands').module, {client: this.client, messageHandler: this.messageHandler}); + await this.messageHandler + .registerCommandModule(require('./lib/commands/MusicCommands').module, {getGuildHandler: (g) => { + return this.getGuildHandler(g, prefix); + }, logger: logger}) //this.registerCommands(); this.registerCallbacks(); cmd.init(prefix); diff --git a/lib/CommandLib.js b/lib/CommandLib.js index 07ad8e1..280d980 100644 --- a/lib/CommandLib.js +++ b/lib/CommandLib.js @@ -46,12 +46,13 @@ class Command { */ 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.name} [${this.args.join('][')}]`.replace('[]', ''); this.answObj = answer; if (!template.name) throw new Error("Template doesn't define a name."); @@ -76,7 +77,7 @@ class Command { get help() { return new ExtendedRichEmbed(`Help for ${this.name}`) .addFields({ - 'Usage': this.usage, + 'Usage': `\`${this.prefix}${this.usage}\``, 'Description': this.description, 'Permission Role': this.permission }); @@ -109,13 +110,16 @@ class CommandHandler { 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) { let kwargs = {}; if (args) - for (let i = 0; i < Math.min(command.kwargs, args.length); i++) - kwargs[command.kwargs[i]] = args[i]; + 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 { return false; @@ -131,7 +135,9 @@ class CommandHandler { * @param command {Command} */ registerCommand(command) { + command.prefix = this.prefix; this.commands[command.name] = command; + return this; } } @@ -184,19 +190,23 @@ class ExtendedRichEmbed extends Discord.RichEmbed { * Adds a Field when a name is given or adds a blank Field otherwise * @param name {String} * @param content {String} + * @returns {ExtendedRichEmbed} */ addNonemptyField(name, content) { if (name && name.length > 0 && content) this.addField(name, content); + return this; } /** * Adds the fields defined in the fields JSON * @param fields {JSON} + * @returns {ExtendedRichEmbed} */ addFields(fields) { for (let [name, value] of Object.entries(fields)) this.addNonemptyField(name, value); + return this; } } diff --git a/lib/MessageLib.js b/lib/MessageLib.js index 8545021..5b16d9e 100644 --- a/lib/MessageLib.js +++ b/lib/MessageLib.js @@ -7,9 +7,9 @@ class MessageHandler { /** * Message Handler to handle messages. Listens on the - * client message event. + * _client message event. * @param client {Discord.Client} - * @param logger {winston.logger} + * @param logger {winston._logger} */ constructor (client, logger) { this.logger = logger; @@ -100,7 +100,7 @@ class MessageHandler { */ async _executeCommandSequence(cmdSequence, message) { this.logger.debug(`Executing command sequence: ${JSON.stringify(cmdSequence)} ...`); - let scopeCmdHandler = this._getScopeHandler(message); + let scopeCmdHandler = this.getScopeHandler(message); await Promise.all(cmdSequence.map((sq) => promiseWaterfall(sq.map((cmd) => async () => { try { this.logger.debug(`Executing command ${cmd}`); @@ -124,7 +124,7 @@ class MessageHandler { * @param message * @private */ - _getScopeHandler(message) { + getScopeHandler(message) { if (message && message.guild) return this.guildCmdHandler; else diff --git a/lib/commands/AnilistApiCommands/AniListCommandsTemplate.yaml b/lib/commands/AnilistApiCommands/AniListCommandsTemplate.yaml index 400d567..df61a40 100644 --- a/lib/commands/AnilistApiCommands/AniListCommandsTemplate.yaml +++ b/lib/commands/AnilistApiCommands/AniListCommandsTemplate.yaml @@ -3,8 +3,8 @@ anime_search: permission: all usage: anime [search query] description: > - Searches AniList.co for the anime Title and returns information about - it if there is an result. + Searches AniList.co for the anime title and returns information about + it if there is a result. category: AniList response: not_found: > @@ -15,8 +15,8 @@ manga_search: permission: all usage: manga [search query] description: > - Searches AniList.co for the manga Title and returns information about - it if there is an result. + Searches AniList.co for the manga title and returns information about + it if there is a result. category: AniList response: not_found: > diff --git a/lib/commands/InfoCommands/InfoCommandsTemplate.yaml b/lib/commands/InfoCommands/InfoCommandsTemplate.yaml index 247603a..893754c 100644 --- a/lib/commands/InfoCommands/InfoCommandsTemplate.yaml +++ b/lib/commands/InfoCommands/InfoCommandsTemplate.yaml @@ -32,3 +32,12 @@ guilds: Answers with the number of guilds the bot has joined permission: owner category: Info + +help: + name: help + description: > + Shows help for bot ocmmands. + permission: all + category: Info + args: + - command diff --git a/lib/commands/InfoCommands/index.js b/lib/commands/InfoCommands/index.js index 3063fe5..5caee38 100644 --- a/lib/commands/InfoCommands/index.js +++ b/lib/commands/InfoCommands/index.js @@ -4,7 +4,7 @@ const cmdLib = require('../../CommandLib'), /** * Info commands provide information about the bot. These informations are - * not process specific but access the discord client instance of the bot. + * not process specific but access the discord _client instance of the bot. */ class InfoCommandModule extends cmdLib.CommandModule { @@ -12,11 +12,33 @@ class InfoCommandModule extends cmdLib.CommandModule { /** * @param opts {Object} properties: * client - the instance of the discord client. + * messageHandler - the instance of the Message Handler */ constructor(opts) { super(cmdLib.CommandScopes.Global); this.templateFile = location + '/InfoCommandsTemplate.yaml'; - this.client = opts.client; + this._client = opts.client; + this._messageHandler = opts.messageHandler; + } + + _createHelpEmbed(commands, msg, prefix) { + let helpEmbed = new cmdLib.ExtendedRichEmbed('Commands') + .setDescription('Create a sequence of commands with `;` and `&&`.'); + let categories = []; + let catCommands = {}; + Object.entries(commands).sort().forEach(([key, value]) => { + if (!categories.includes(value.category)) { + categories.push(value.category); + catCommands[value.category] = `\`${prefix}${key}\` \t`; + } else { + catCommands[value.category] += `\`${prefix}${key}\` \t`; + } + }); + for (let cat of categories) + helpEmbed.addField(cat, catCommands[cat]); + + helpEmbed.setFooter(prefix + 'help [command] for more info to each command'); + return helpEmbed; } async register(commandHandler) { @@ -34,14 +56,14 @@ class InfoCommandModule extends cmdLib.CommandModule { let ping = new cmdLib.Command( this.template.ping, new cmdLib.Answer(() => { - return `Current average ping: \`${this.client.ping} ms\``; + return `Current average ping: \`${this._client.ping} ms\``; }) ); let uptime = new cmdLib.Command( this.template.uptime, new cmdLib.Answer(() => { - let uptime = utils.getSplitDuration(this.client.uptime); + let uptime = utils.getSplitDuration(this._client.uptime); return new cmdLib.ExtendedRichEmbed('Uptime').setDescription(` **${uptime.days}** days **${uptime.hours}** hours @@ -55,15 +77,33 @@ class InfoCommandModule extends cmdLib.CommandModule { let guilds = new cmdLib.Command( this.template.guilds, new cmdLib.Answer(() => { - return `Number of guilds: \`${this.client.guilds.size}\``; + return `Number of guilds: \`${this._client.guilds.size}\``; + }) + ); + + let help = new cmdLib.Command( + this.template.help, + new cmdLib.Answer((m, k) => { + let globH = this._messageHandler.globalCmdHandler; + let scopeH = this._messageHandler.getScopeHandler(m); + if (k.command) { + k.command = k.command.replace(globH.prefix, ''); + let commandInstance = globH.commands[k.command] || scopeH.commands[k.command]; + return commandInstance.help; + } else { + let commandObj = {...globH.commands, ...scopeH.commands}; + return this._createHelpEmbed(commandObj, m, globH.prefix); + } }) ); // register commands - commandHandler.registerCommand(about); - commandHandler.registerCommand(ping); - commandHandler.registerCommand(uptime); - commandHandler.registerCommand(guilds); + commandHandler + .registerCommand(about) + .registerCommand(ping) + .registerCommand(uptime) + .registerCommand(guilds) + .registerCommand(help); } } diff --git a/lib/commands/MusicCommands/MusicCommandsTemplate.yaml b/lib/commands/MusicCommands/MusicCommandsTemplate.yaml new file mode 100644 index 0000000..cdfb709 --- /dev/null +++ b/lib/commands/MusicCommands/MusicCommandsTemplate.yaml @@ -0,0 +1,178 @@ +play: + name: play + description: > + Adds the url to the YouTube video or YouTube playlist into the queue. + permission: all + category: Music + args: + - url + response: + success: > + Added URL to the media queue. + failure: > + Failed adding the URL to the media queue. + url_invalid: > + The URL you provided is not a valid YouTube video or Playlist URL. + no_url: > + You need to provide an URL to a YouTube viceo or Playlist. + no_voicechannel: > + You need to join a VoiceChannel to request media playback. + +play_next: + name: playnext + description: > + Adds the url to the YouTube video or YouTube playlist into the queue as + next playing song. + permission: all + category: Music + args: + - url + response: + success: > + Added URL as next media to the media queue. + failure: > + Failed adding the URL to the media queue. + url_invalid: > + The URL you provided is not a valid YouTube video or Playlist URL. + no_url: > + You need to provide an URL to a YouTube viceo or Playlist. + no_voicechannel: > + You need to join a VoiceChannel to request media playback. + +join: + name: join + description: > + Joins the VoiceChannel you are in. + permission: all + category: Music + response: + no_voicechannel: > + You need to join a VoiceChannel for me to join. + +stop: + name: stop + description: > + Stops the media playback and leaves the VoiceChannel. + permission: dj + category: Music + response: + success: > + Stopped music playback. + not_playing: > + I'm not playing music at the moment. What do you want me to stop? + +pause: + name: pause + description: > + Pauses the media playback. + permission: all + category: Music + response: + success: > + Paused playback. + not_playing: > + I'm not playing music at the moment. + +resume: + name: resume + description: > + Resumes the media playback. + permission: all + category: Music + response: + success: > + Resumed playback. + not_playing: > + I'm not playing music at the moment. + +skip: + name: skip + description: > + Skips the currently playing song. + permission: dj + category: Music + response: + success: > + Skipped to the next song. + not_playing: > + I'm not playing music at the moment. + +clear: + name: clear + description: > + Clears the media queue. + permission: dj + category: Music + response: + success: > + The media queue has been cleared. + +media_queue: + name: queue + descriptions: > + Shows the next ten songs in the media queue. + permission: all + category: Music + +media_current: + name: np + description: > + Shows the currently playing song. + permission: all + category: Music + response: + not_playing: > + I'm not playing music at the moment. + +shuffle: + name: shuffle + description: > + Shuffles the media queue + permission: all + category: Music + response: + success: > + The queue has been shuffled. + +toggle_repeat: + name: repeat + description: > + Toggles listening o repeat. + permission: all + category: Music + response: + repeat_true: > + Listening on repeat now! + repeat_false: > + Not listening on repeat anymore. + +save_media: + name: savemedia + description: > + Saves the YouTube URL with a specific name. + permission: dj + category: Music + args: + - url + usage: savemedia [url] [name...] + +delete_media: + name: deletemedia + description: > + Deletes a saved YouTube URL from saved media. + permission: dj + category: Music + usage: deletemedia [name] + response: + no_name: > + You must provide a name for the media to delete. + +saved_media: + name: savedmedia + description: > + Shows all saved YouTube URLs. + permission: all + category: Music + response: + no_saved: > + There are no saved YouTube URLs :( diff --git a/lib/commands/MusicCommands/index.js b/lib/commands/MusicCommands/index.js new file mode 100644 index 0000000..68e8c64 --- /dev/null +++ b/lib/commands/MusicCommands/index.js @@ -0,0 +1,285 @@ +const cmdLib = require('../../CommandLib'), + utils = require('../../utils'), + location = './lib/commands/MusicCommands'; + +/** + * Music commands provide commands to control the bots music functions. + * These commands are for server music functionalities. + */ +class MusicCommandModule extends cmdLib.CommandModule { + + /** + * @param opts {Object} properties: + * getGuildHandler - a function to get the guild handler for a guild. + * logger - the logger instance + */ + constructor(opts) { + super(cmdLib.CommandScopes.Guild); + this.templateFile = location + '/MusicCommandsTemplate.yaml'; + this._getGuildHandler = opts.getGuildHandler; + this._logger = opts.logger; + } + + /** + * Connects to a voice-channel if not connected and plays the url + * @param gh {guilding.GuildHandler} + * @param vc {Discord.VoiceChannel} + * @param url {String} The url to the YouTube media + * @param next {Boolean} Should the song be played next + * @returns {Promise} + * @private + */ + async _connectAndPlay(gh, vc, url, next) { + if (!gh.dj.connected) { + await gh.dj.connect(vc); + await gh.dj.playYouTube(url, next); + } else { + await gh.dj.playYouTube(url, next); + } + } + + /** + * The play function for the music commands play and playnext + * @param m {Discord.Message} + * @param k {Object} kwargs + * @param s {String} argsString + * @param t {Object} template + * @param n {Boolean} play next + * @returns {Promise<*>} + * @private + */ + async _playFunction(m, k, s, t, n) { + let gh = await this._getGuildHandler(m.guild); + let vc = gh.dj.voiceChannel || m.member.voiceChannel; + let url = k['url']; + if (!vc) + return t.response.no_voicechannel; + if (!url) + return t.response.no_url; + if (!utils.YouTube.isValidEntityUrl(url)) { + url = s; + let row = await gh.db.get('SELECT url FROM playlists WHERE name = ?', [url]); + if (!row) { + this._logger.debug('Got invalid url for play command.'); + return t.response.url_invalid; + } else { + await this._connectAndPlay(gh, vc, row.url, n); + return t.response.success; + } + } else { + await this._connectAndPlay(gh, vc, url, n); + return t.response.success; + } + } + + async register(commandHandler) { + await this._loadTemplate(); + + let play = new cmdLib.Command( + this.template.play, + new cmdLib.Answer(async (m, k, s) => { + return await this._playFunction(m, k, s, this.template.play, false); + }) + ); + + let playNext = new cmdLib.Command( + this.template.play_next, + new cmdLib.Answer(async (m, k, s) => { + return await this._playFunction(m, k, s, this.template.play_next, true); + }) + ); + + let join = new cmdLib.Command( + this.template.join, + new cmdLib.Answer(async (m) => { + let gh = await this._getGuildHandler(m.guild); + if (m.member.voiceChannel) + await gh.dj.connect(m.member.voiceChannel); + else + return this.template.join.response.no_voicechannel; + }) + ); + + let stop = new cmdLib.Command( + this.template.stop, + new cmdLib.Answer(async (m) => { + let gh = await this._getGuildHandler(m.guild); + if (gh.dj.connected) { + gh.dj.stop(); + return this.template.stop.success; + } else { + return this.template.stop.not_playing; + } + }) + ); + + let pause = new cmdLib.Command( + this.template.pause, + new cmdLib.Answer(async (m) => { + let gh = await this._getGuildHandler(m.guild); + if (gh.dj.playing) { + gh.dj.pause(); + return this.template.pause.response.success; + } else { + return this.template.pause.response.not_playing; + } + }) + ); + + let resume = new cmdLib.Command( + this.template.resume, + new cmdLib.Answer(async (m) => { + let gh = await this._getGuildHandler(m.guild); + if (gh.dj.playing) { + gh.dj.resume(); + return this.template.resume.response.success; + } else { + return this.template.resume.response.not_playing; + } + }) + ); + + let skip = new cmdLib.Command( + this.template.skip, + new cmdLib.Answer(async (m) => { + let gh = await this._getGuildHandler(m.guild); + if (gh.dj.playing) { + gh.dj.skip(); + return this.template.skip.response.success; + } else { + return this.template.skip.response.not_playing; + } + }) + ); + + let clear = new cmdLib.Command( + this.template.clear, + new cmdLib.Answer(async (m) => { + let gh = await this._getGuildHandler(m.guild); + gh.dj.clear(); + return this.template.clear.response.success; + }) + ); + + let mediaQueue = new cmdLib.Command( + this.template.media_queue, + new cmdLib.Answer(async (m) => { + let gh = await this._getGuildHandler(m.guild); + this._logger.debug(`Found ${gh.dj.queue.length} songs.`); + let description = ''; + + for (let i = 0; i < Math.min(gh.dj.queue.length, 9); i++) { + let entry = gh.dj.queue[i]; + description += `[${entry.title}](${entry.url})\n`; + } + return new cmdLib.ExtendedRichEmbed(`${gh.dj.queue.length} songs in queue`) + .setDescription(description); + }) + ); + + let mediaCurrent = new cmdLib.Command( + this.template.media_current, + new cmdLib.Answer(async (m) => { + let gh = await this._getGuildHandler(m.guild); + let song = gh.dj.song; + if (song) + return new cmdLib.ExtendedRichEmbed('Now playing:') + .setDescription(`[${song.title}](${song.url})`) + .setImage(utils.YouTube.getVideoThumbnailUrlFromUrl(song.url)) + .setColor(0x00aaff); + else + return this.template.media_current.response.not_playing; + }) + ); + + let shuffle = new cmdLib.Command( + this.template.shuffle, + new cmdLib.Answer(async (m) => { + let gh = await this._getGuildHandler(m.guild); + gh.dj.shuffle(); + return this.template.shuffle.response.success; + }) + ); + + let toggleRepeat = new cmdLib.Command( + this.template.toggle_repeat, + new cmdLib.Answer(async (m) => { + let gh = await this._getGuildHandler(m.guild); + gh.dj.repeat = !gh.dj.repeat; + return gh.dj.repeat? + this.template.toggle_repeat.response.repeat_true : + this.template.toggle_repeat.response.repeat_false; + }) + ); + + let saveMedia = new cmdLib.Command( + this.template.save_media, + new cmdLib.Answer(async (m, k, s) => { + let gh = await this._getGuildHandler(m.guild); + let saveName = s.replace(k.url + ' ', ''); + let row = await gh.db + .get('SELECT COUNT(*) count FROM playlists WHERE name = ?', [saveName]); + if (!row || row.count === 0) + await gh.db.run('INSERT INTO playlists (name, url) VALUES (?, ?)', + [saveName, k.url]); + else + await gh.db.run('UPDATE playlists SET url = ? WHERE name = ?', + [k.url, saveName]); + return `Saved song/playlist as ${saveName}`; + }) + ); + + let deleteMedia = new cmdLib.Command( + this.template.delete_media, + new cmdLib.Answer(async (m, k, s) => { + let gh = await this._getGuildHandler(m.guild); + if (!s) { + return this.template.delete_media.response.no_name; + } else { + await gh.db.run('DELETE FROM playlists WHERE name = ?', [s]); + return `Deleted ${s} from saved media`; + } + }) + ); + + let savedMedia = new cmdLib.Command( + this.template.saved_media, + new cmdLib.Answer(async (m) => { + let gh = await this._getGuildHandler(m.guild); + let response = ''; + let rows = await gh.db.all('SELECT name, url FROM playlists'); + for (let row of rows) + response += `[${row.name}](${row.url})\n`; + + if (rows.length === 0) + return this.template.saved_media.response.no_saved; + else + return new cmdLib.ExtendedRichEmbed('Saved Songs and Playlists') + .setDescription(response) + .setFooter(`Play a saved entry with play [Entryname]`); + }) + ); + + // register commands + commandHandler + .registerCommand(play) + .registerCommand(playNext) + .registerCommand(join) + .registerCommand(stop) + .registerCommand(pause) + .registerCommand(resume) + .registerCommand(skip) + .registerCommand(clear) + .registerCommand(mediaQueue) + .registerCommand(mediaCurrent) + .registerCommand(shuffle) + .registerCommand(toggleRepeat) + .registerCommand(saveMedia) + .registerCommand(deleteMedia) + .registerCommand(savedMedia); + } +} + +Object.assign(exports, { + 'module': MusicCommandModule +}); diff --git a/lib/commands/UtilityCommands/UtilityCommandsTemplate.yaml b/lib/commands/UtilityCommands/UtilityCommandsTemplate.yaml index 09cab2c..488a730 100644 --- a/lib/commands/UtilityCommands/UtilityCommandsTemplate.yaml +++ b/lib/commands/UtilityCommands/UtilityCommandsTemplate.yaml @@ -14,14 +14,14 @@ add_presence: usage: addpresence [presence] rotate_presence: - name: rotate_presence + name: rotatepresence description: > Forces a presence rotation permission: owner category: Utility create_user: - name: create_user + name: createuser description: > Creates a user for the webinterface. permission: owner From 4d7018d51059d3b64c26a98e9a526293b59d9998 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sat, 2 Mar 2019 18:06:42 +0100 Subject: [PATCH 18/26] Restructuring and new Commands - Added Server Utility Commands - Removed lib/cmd - Renamed lib/music - renamed lib/weblib - removed command parsing function from GuildHandler - renamed DJ to MusicPlayer - updated graphql schema and interface to new names --- bot.js | 134 ++--- lib/CommandLib.js | 28 +- lib/MessageLib.js | 59 +- lib/{music.js => MusicLib.js} | 34 +- lib/{weblib.js => WebLib.js} | 48 +- lib/api/graphql/schema.gql | 4 +- lib/cmd.js | 540 ------------------ lib/commands/AnilistApiCommands/index.js | 4 +- .../InfoCommands/InfoCommandsTemplate.yaml | 1 + lib/commands/InfoCommands/index.js | 9 +- .../MusicCommands/MusicCommandsTemplate.yaml | 10 +- lib/commands/MusicCommands/index.js | 46 +- .../ServerUtilityCommandsTemplate.yaml | 0 lib/commands/ServerUtilityCommands/index.js | 0 lib/commands/UtilityCommands/index.js | 56 +- lib/guilding.js | 311 +--------- test/test.js | 4 +- web/http/index.pug | 26 +- web/http/sass/style.sass | 10 +- web/http/scripts/query.js | 52 +- 20 files changed, 285 insertions(+), 1091 deletions(-) rename lib/{music.js => MusicLib.js} (95%) rename lib/{weblib.js => WebLib.js} (91%) delete mode 100644 lib/cmd.js create mode 100644 lib/commands/ServerUtilityCommands/ServerUtilityCommandsTemplate.yaml create mode 100644 lib/commands/ServerUtilityCommands/index.js diff --git a/bot.js b/bot.js index d6b9b4c..5d995fc 100644 --- a/bot.js +++ b/bot.js @@ -2,12 +2,10 @@ 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'), config = require('./config.json'), args = require('args-parser')(process.argv), - waterfall = require('promise-waterfall'), sqliteAsync = require('./lib/sqliteAsync'), authToken = args.token || config.api.botToken, prefix = args.prefix || config.prefix || '~', @@ -15,21 +13,26 @@ const Discord = require("discord.js"), let weblib = null; +/** + * The Bot class handles the initialization and Mangagement of the Discord bot and + * is the main class. + */ class Bot { + constructor() { this.client = new Discord.Client({autoReconnect: true}); - this.mention = false; this.rotator = null; this.maindb = null; this.presences = []; this.messageHandler = new msgLib.MessageHandler(this.client, logger); - this.guildHandlers = []; - this.userRates = {}; + this.guildHandlers = {}; logger.verbose('Verifying config'); let configVerifyer = new utils.ConfigVerifyer(config, [ - "api.botToken", "api.youTubeApiKey" + "api.botToken", "api.youTubeApiKey", + "commandSettings.maxSequenceParallel", + "commandSettings.maxSequenceSerial" ]); if (!configVerifyer.verifyConfig(logger)) if (!args.i) { @@ -38,7 +41,6 @@ class Bot { process.exit(1); }); } - cmd.setLogger(logger); guilding.setLogger(logger); } @@ -64,7 +66,7 @@ class Bot { }); await this.initializeDatabase(); - if (config.webservice && config.webservice.enabled) + if (config.webinterface && config.webinterface.enabled) await this.initializeWebserver(); logger.verbose('Registering commands'); await this.messageHandler @@ -74,12 +76,18 @@ class Bot { await this.messageHandler .registerCommandModule(require('./lib/commands/InfoCommands').module, {client: this.client, messageHandler: this.messageHandler}); await this.messageHandler - .registerCommandModule(require('./lib/commands/MusicCommands').module, {getGuildHandler: (g) => { - return this.getGuildHandler(g, prefix); - }, logger: logger}) - //this.registerCommands(); - this.registerCallbacks(); - cmd.init(prefix); + .registerCommandModule(require('./lib/commands/MusicCommands').module, { + getGuildHandler: async (g) => await this.getGuildHandler(g), + logger: logger + }); + await this.messageHandler + .registerCommandModule(require('./lib/commands/ServerUtilityCommands').module, { + getGuildHandler: async (g) => await this.getGuildHandler(g), + logger: logger, + messageHandler: this.messageHandler, + config: config + }); + this.registerEvents(); } /** @@ -120,10 +128,10 @@ class Bot { */ async initializeWebserver() { logger.verbose('Importing weblib'); - weblib = require('./lib/weblib'); + weblib = require('./lib/WebLib'); weblib.setLogger(logger); logger.verbose('Creating WebServer'); - this.webServer = new weblib.WebServer(config.webservice.port || 8080); + this.webServer = new weblib.WebServer(config.webinterface.port || 8080); logger.debug('Setting Reference Objects to webserver'); await this.webServer.setReferenceObjects({ @@ -131,7 +139,7 @@ class Bot { presences: this.presences, maindb: this.maindb, prefix: prefix, - getGuildHandler: (guild) => this.getGuildHandler(guild, prefix), + getGuildHandler: async (g) => await this.getGuildHandler(g), guildHandlers: this.guildHandlers }); } @@ -172,14 +180,6 @@ class Bot { } } - /** - * registeres global commands - */ - registerCommands() { - cmd.registerUtilityCommands(prefix, this); - cmd.registerInfoCommands(prefix, this); - } - /** * changes the presence of the bot by using one stored in the presences array */ @@ -198,7 +198,7 @@ class Bot { /** * Registeres callbacks for client events message and ready */ - registerCallbacks() { + registerEvents() { this.client.on('error', (err) => { logger.error(err.message); logger.debug(err.stack); @@ -217,93 +217,27 @@ class Bot { }); }); - /* - this.client.on('message', async (msg) => { - try { - if (msg.author === this.client.user) { - logger.verbose(`ME: ${msg.content}`); - return; - } - if (this.checkRate(msg.author.tag)) { - logger.verbose(`<${msg.author.tag}>: ${msg.content}`); - if (!msg.guild) { - let reply = cmd.parseMessage(msg); - await this.answerMessage(msg, reply); - } else { - let gh = await this.getGuildHandler(msg.guild, prefix); - await gh.handleMessage(msg); - } - if (((Date.now() - this.userRates[msg.author.tag].last)/1000) > (config.rateLimitTime || 10)) - this.userRates[msg.author.tag].count = 0; - else - this.userRates[msg.author.tag].count++; - this.userRates[msg.author.tag].last = Date.now(); - this.userRates[msg.author.tag].reached = false; - } else if (!this.userRates[msg.author.tag].reached) { - logger.verbose(`${msg.author.tag} reached it's rate limit.`); - this.userRates[msg.author.tag].reached = true; - } - } catch (err) { - logger.error(err.message); - logger.debug(err.stack); - } - });*/ - this.client.on('voiceStateUpdate', async (oldMember, newMember) => { - let gh = await this.getGuildHandler(newMember.guild, prefix); + let gh = await this.getGuildHandler(newMember.guild); if (newMember.user === this.client.user) { if (newMember.voiceChannel) - gh.dj.updateChannel(newMember.voiceChannel); + gh.musicPlayer.updateChannel(newMember.voiceChannel); } else { - if (oldMember.voiceChannel === gh.dj.voiceChannel || newMember.voiceChannel === gh.dj.voiceChannel) - gh.dj.checkListeners(); + if (oldMember.voiceChannel === gh.musicPlayer.voiceChannel || newMember.voiceChannel === gh.musicPlayer.voiceChannel) + gh.musicPlayer.checkListeners(); } }); } - /** - * Returns true if the user has not reached it's rate limit. - * @param usertag - * @returns {boolean} - */ - checkRate(usertag) { - if (!this.userRates[usertag]) - this.userRates[usertag] = {last: Date.now(), count: 0}; - return ((Date.now() - this.userRates[usertag].last)/1000) > (config.rateLimitTime || 10) || - this.userRates[usertag].count < (config.rateLimitCount || 5); - } - - /** - * Sends the answer recieved from the commands callback. - * Handles the sending differently depending on the type of the callback return - * @param msg - * @param answer - */ - async answerMessage(msg, answer) { - if (answer instanceof Discord.RichEmbed) { - (this.mention) ? msg.reply('', answer) : msg.channel.send('', answer); - } else if (answer instanceof Promise) { - let resolvedAnswer = await answer; - await this.answerMessage(msg, resolvedAnswer); - } else if (answer instanceof Array) { - await waterfall(answer.map((x) => async () => await this.answerMessage(msg, x))); // execute each after another - } else if ({}.toString.call(answer) === '[object Function]') { - await this.answerMessage(msg, answer()); - } else if (answer) { - (this.mention) ? msg.reply(answer) : msg.channel.send(answer); - } - } - /** * Returns the guild handler by id, creates one if it doesn't exist and returns it then - * @param guild - * @param prefix + * @param guild {Guild} * @returns {*} */ - async getGuildHandler(guild, prefix) { + async getGuildHandler(guild) { if (!this.guildHandlers[guild.id]) { - let newGuildHandler = new guilding.GuildHandler(guild, prefix); + let newGuildHandler = new guilding.GuildHandler(guild); await newGuildHandler.initDatabase(); this.guildHandlers[guild.id] = newGuildHandler; } @@ -314,7 +248,7 @@ class Bot { // Executing the main function if (typeof require !== 'undefined' && require.main === module) { - logger.info("Starting up... "); // log the current date so that the logfile is better to read. + logger.info("Starting up... "); logger.debug('Calling constructor...'); let discordBot = new Bot(); logger.debug('Initializing services...'); diff --git a/lib/CommandLib.js b/lib/CommandLib.js index 280d980..5e8526b 100644 --- a/lib/CommandLib.js +++ b/lib/CommandLib.js @@ -1,6 +1,7 @@ const Discord = require('discord.js'), yaml = require('js-yaml'), fsx = require('fs-extra'), + config = require('../config.json'), utils = require('./utils'); const scopes = { @@ -101,7 +102,7 @@ class CommandHandler { * Handles the command and responds to the message. * @param commandMessage {String} * @param message {Discord.Message} - * @returns {Boolean | Promise} + * @returns {Boolean | String | Promise} */ handleCommand(commandMessage, message) { let commandName = commandMessage.match(/^\S+/); @@ -115,12 +116,14 @@ class CommandHandler { .replace(/\s+$/, ''); // trailing whitespace let args = argsString.match(/\S+/g); let command = this.commands[commandName]; - if (command) { + if (command && this._checkPermission(message, command.permission)) { 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) { + return "You don't have permission for this command"; } else { return false; } @@ -131,7 +134,6 @@ class CommandHandler { /** * Registers the command so that the handler can use it. - * @param name {String} * @param command {Command} */ registerCommand(command) { @@ -139,6 +141,26 @@ class CommandHandler { this.commands[command.name] = command; 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; + } } /** diff --git a/lib/MessageLib.js b/lib/MessageLib.js index 5b16d9e..dd69d54 100644 --- a/lib/MessageLib.js +++ b/lib/MessageLib.js @@ -3,6 +3,8 @@ const cmdLib = require('./CommandLib'), Discord = require('discord.js'), promiseWaterfall = require('promise-waterfall'); +/* eslint no-useless-escape: 0 */ + class MessageHandler { /** @@ -20,6 +22,7 @@ class MessageHandler { cmdLib.CommandScopes.User); this.guildCmdHandler = new cmdLib.CommandHandler(config.prefix, cmdLib.CommandScopes.Guild); + this.userRates = {}; this._registerEvents(); } @@ -51,6 +54,15 @@ class MessageHandler { await cmdModule.register(this.getHandler(cmdModule.scope)); } + /** + * Parses a string to a command sequence Array. + * Workaround to not reveal the private parseSyntax function. + * @param synStr {String} + */ + parseSyntaxString(synStr) { + return this._parseSyntax({content: synStr}); + } + /** * Registering event handlers. * @private @@ -59,10 +71,13 @@ class MessageHandler { this.logger.debug('Registering message event...'); this.discordClient.on('message', async (msg) => { this.logger.debug(`<${msg.guild? msg.channel.name+'@'+msg.guild.name : 'PRIVATE'}> ${msg.author.tag}: ${msg.content}`); - if (msg.author !== this.discordClient.user) { + if (msg.author !== this.discordClient.user + && this._checkPrefixStart(msg.content) + && !this._checkRateReached(msg.author)) { + let sequence = this._parseSyntax(msg); this.logger.debug(`Syntax parsing returned: ${JSON.stringify(sequence)}`); - await this._executeCommandSequence(sequence, msg); + await this.executeCommandSequence(sequence, msg); this.logger.debug('Executed command sequence'); } }); @@ -82,8 +97,8 @@ class MessageHandler { for (let string of strings) content = content.replace(string, string // escape all special chars - .replace(';', '\\;') - .replace('&', '\\&')); + .replace(/;/g, '\\;') + .replace(/&/g, '\\&')); let independentCommands = content // independent command sequende with ; .split(/(? x.replace(/^ +/, '')); @@ -98,7 +113,7 @@ class MessageHandler { /** * Executes a sequence of commands */ - async _executeCommandSequence(cmdSequence, message) { + async executeCommandSequence(cmdSequence, message) { 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 () => { @@ -145,6 +160,40 @@ class MessageHandler { else message.channel.send(answer); } + + /** + * Checks if the messageString beginns with a command prefix. + * @param msgString {String} + * @private + */ + _checkPrefixStart(msgString) { + let p1 = this.globalCmdHandler.prefix; + let p2 = this.guildCmdHandler.prefix; + let p3 = this.userCmdHandler.prefix; + return ( + new RegExp(`^\\s*?${p1}`).test(msgString) || + new RegExp(`^\\s*?${p2}`).test(msgString) || + new RegExp(`^\\s*?${p3}`).test(msgString)); + } + + /** + * Checks if the user has reached the command rate limit and updates it. + * @param user {Discord.User} + * @returns {boolean} + * @private + */ + _checkRateReached(user) { + if (!this.userRates[user.tag]) + this.userRates[user.tag] = {last: 0, count: 0}; + let userEntry = this.userRates[user.tag]; + let reached = ((Date.now() - userEntry.last)/1000) < (config.rateLimitTime || 10) + && userEntry.count > (config.rateLimitCount || 5); + if (((Date.now() - userEntry.last)/1000) > (config.rateLimitTime || 10)) + this.userRates[user.tag].count = 0; + this.userRates[user.tag].last = Date.now(); + this.userRates[user.tag].count++; + return reached; + } } Object.assign(exports, { diff --git a/lib/music.js b/lib/MusicLib.js similarity index 95% rename from lib/music.js rename to lib/MusicLib.js index 0f77880..7467e9c 100644 --- a/lib/music.js +++ b/lib/MusicLib.js @@ -1,20 +1,22 @@ const ytdl = require("ytdl-core"), ypi = require('youtube-playlist-info'), yttl = require('get-youtube-title'), - args = require('args-parser')(process.argv), config = require('../config.json'), utils = require('./utils.js'), - ytapiKey = args.ytapi || config.api.youTubeApiKey; -/* Variable Definition */ -let logger = require('winston'); + ytapiKey = config.api.youTubeApiKey; + -/* Function Definition */ +let logger = require('winston'); exports.setLogger = function (newLogger) { logger = newLogger; }; -exports.DJ = class { +/** + * The Music Player class is used to handle music playing tasks on Discord Servers (Guilds). + * @type {MusicPlayer} + */ +class MusicPlayer { constructor(voiceChannel) { this.conn = null; this.disp = null; @@ -51,7 +53,7 @@ exports.DJ = class { /** * Defining setter for listenOnRepeat to include the current song into the repeating loop. - * @param value + * @param value {Boolean} */ set listenOnRepeat(value) { this.repeat = value; @@ -74,7 +76,7 @@ exports.DJ = class { /** * Updates the channel e.g. when the bot is moved to another channel. - * @param voiceChannel + * @param voiceChannel {Discord.VoiceChannel} */ updateChannel(voiceChannel) { if (voiceChannel) { @@ -86,7 +88,7 @@ exports.DJ = class { /** * Plays a file for the given filename. * TODO: Implement queue - * @param filename + * @param filename {String} * @todo */ playFile(filename) { @@ -125,8 +127,8 @@ exports.DJ = class { * Plays the url of the current song if there is no song playing or puts it in the queue. * If the url is a playlist, the videos of the playlist are fetched and put * in the queue. For each song the title is saved in the queue too. - * @param url - * @param playnext + * @param url {String} + * @param playnext {Boolean} */ async playYouTube(url, playnext) { let plist = utils.YouTube.getPlaylistIdFromUrl(url); @@ -195,7 +197,7 @@ exports.DJ = class { /** * Gets the name of the YouTube Video at url - * @param url + * @param url {String} * @returns {Promise<>} */ getVideoName(url) { @@ -213,7 +215,7 @@ exports.DJ = class { /** * Sets the volume of the dispatcher to the given value - * @param percentage + * @param percentage {Number} */ setVolume(percentage) { logger.verbose(`Setting volume to ${percentage}`); @@ -323,4 +325,8 @@ exports.DJ = class { clear() { this.queue = []; } -}; +} + +Object.assign(exports, { + MusicPlayer: MusicPlayer +}); diff --git a/lib/weblib.js b/lib/WebLib.js similarity index 91% rename from lib/weblib.js rename to lib/WebLib.js index b28458e..2949816 100644 --- a/lib/weblib.js +++ b/lib/WebLib.js @@ -41,7 +41,7 @@ exports.WebServer = class { this.app.use(require('cors')()); this.app.use(session({ store: new SQLiteStore({dir: './data', db: 'sessions.db'}), - secret: config.webservice.sessionSecret, + secret: config.webinterface.sessionSecret, resave: false, saveUninitialized: true, cookie: {secure: 'auto'}, @@ -89,7 +89,7 @@ exports.WebServer = class { this.app.use('/graphql', graphqlHTTP({ schema: this.schema, rootValue: this.root, - graphiql: config.webservice.graphiql || false + graphiql: config.webinterface.graphiql || false })); } @@ -98,14 +98,14 @@ exports.WebServer = class { */ start() { this.configureExpress(); - if (config.webservice.https && config.webservice.https.enabled) { + if (config.webinterface.https && config.webinterface.https.enabled) { let sslKey = null; let sslCert = null; - if (config.webservice.https.keyFile) - sslKey = fs.readFileSync(config.webservice.https.keyFile, 'utf-8'); - if (config.webservice.https.certFile) - sslCert = fs.readFileSync(config.webservice.https.certFile, 'utf-8'); + if (config.webinterface.https.keyFile) + sslKey = fs.readFileSync(config.webinterface.https.keyFile, 'utf-8'); + if (config.webinterface.https.certFile) + sslCert = fs.readFileSync(config.webinterface.https.certFile, 'utf-8'); if (sslKey && sslCert) { logger.verbose('Creating https server.'); this.server = require('https').createServer({key: sslKey, cert: sslCert}, this.app); @@ -200,8 +200,8 @@ exports.WebServer = class { return dcGuilds.filter((x) => { let gh = objects.guildHandlers[x.id]; if (gh) - if (gh.dj) - return gh.dj.playing; + if (gh.musicPlayer) + return gh.musicPlayer.playing; else return false; else @@ -272,12 +272,12 @@ function generateUUID(input) { } /** - * Used for graphql attribute access to the lib/music/DJ + * Used for graphql attribute access to the lib/music/MusicPlayer */ -class DJ { - constructor(musicDj) { - this.dj = musicDj; - this.quality = musicDj.quality; +class MusicPlayer { + constructor(musicPlayer) { + this.musicPlayer = musicPlayer; + this.quality = musicPlayer.quality; } queue(args) { @@ -297,35 +297,35 @@ class DJ { } get playing() { - return this.dj.playing; + return this.musicPlayer.playing; } get connected() { - return this.dj.connected; + return this.musicPlayer.connected; } get paused() { - return this.dj.disp? this.dj.disp.paused : false; + return this.musicPlayer.disp? this.dj.disp.paused : false; } get queueCount() { - return this.dj.queue.length; + return this.musicPlayer.queue.length; } get songStartTime() { - return this.dj.disp.player.streamingData.startTime; + return this.musicPlayer.disp.player.streamingData.startTime; } get volume() { - return this.dj.volume; + return this.musicPlayer.volume; } get repeat() { - return this.dj.repeat; + return this.musicPlayer.repeat; } get currentSong() { - let x = this.dj.current; + let x = this.musicPlayer.current; return { id: generateID(['Media', x.url]), name: x.title, @@ -335,7 +335,7 @@ class DJ { } get voiceChannel() { - return this.dj.voiceChannel.name; + return this.musicPlayer.voiceChannel.name; } } @@ -358,7 +358,7 @@ class Guild { this.ready = guildHandler.ready; this.prSaved = null; this.guildHandler = guildHandler; - this.dj = this.guildHandler.dj ? new DJ(this.guildHandler.dj) : null; + this.musicPlayer = this.guildHandler.musicPlayer ? new MusicPlayer(this.guildHandler.musicPlayer) : null; } async querySaved() { diff --git a/lib/api/graphql/schema.gql b/lib/api/graphql/schema.gql index bed89c0..1ba4323 100644 --- a/lib/api/graphql/schema.gql +++ b/lib/api/graphql/schema.gql @@ -26,7 +26,7 @@ type GuildMember { roles(first: Int = 10, offset: Int = 0, id: String): [Role] highestRole: Role } -type DJ { +type MusicPlayer { queue(first: Int = 10, offset: Int = 0, id: String): [MediaEntry] queueCount: Int! songStartTime: String @@ -44,7 +44,7 @@ type Guild { discordId: String name: String owner: GuildMember - dj: DJ + musicPlayer: MusicPlayer members(first: Int = 10, offset: Int = 0, id: String): [GuildMember] memberCount: Int! roles(first: Int = 10, offset: Int = 0, id: String): [Role] diff --git a/lib/cmd.js b/lib/cmd.js deleted file mode 100644 index c142d62..0000000 --- a/lib/cmd.js +++ /dev/null @@ -1,540 +0,0 @@ -/* Module definition */ - -/* Variable Definition */ -const Discord = require('discord.js'), - args = require('args-parser')(process.argv), - config = require('../config.json'), - gcmdTempl = require('../commands/globalcommands'), - scmdTempl = require('../commands/servercommands'), - utils = require('./utils'); - -let logger = require('winston'), - globCommands = {}; - -/** - * @type {Servant} - */ -class Servant { - constructor(prefix) { - this.commands = {}; - this.prefix = prefix; - // show all commands (except the owner commands if the user is not an owner) - this.createCommand(gcmdTempl.utils.help, (msg, kwargs) => { - if (kwargs.command) { - let cmd = kwargs.command; - let allCommands = {...globCommands, ...this.commands}; - if (cmd.charAt(0) !== prefix) - cmd = this.prefix + cmd; - if (allCommands[cmd]) - return new Discord.RichEmbed() - .setTitle(`Help for ${cmd}`) - .addField('Usage', `\`${cmd} [${allCommands[cmd].args.join('] [')}]\``.replace('[]', '')) - .addField('Description', allCommands[cmd].description) - .addField('Permission Role', allCommands[cmd].role || 'all'); - else - return 'Command not found :('; - - } else { - let allCommands = {...globCommands, ...this.commands}; - return createHelpEmbed(allCommands, msg, prefix); - } - }); - - // show all roles that are used by commands - this.createCommand(scmdTempl.utils.roles, () => { - let roles = []; - Object.values(globCommands).concat(Object.values(this.commands)).sort().forEach((value) => { - roles.push(value.role || 'all'); - }); - return `**Roles**\n${[...new Set(roles)].join('\n')}`; - }); - } - - /** - * Creates a command entry in the private commands dict - * @param template - * @param call - */ - createCommand(template, call) { - if (!template.name) { - logger.debug(`Name of command template is null or undef. Failed creating command.`); - return; - } - this.commands[this.prefix + template.name] = { - 'args': template.args || [], - 'description': template.description, - 'callback': call, - 'role': template.permission, - 'category': template.category || 'Other' - }; - logger.debug(`Created server command: ${this.prefix + template.name}, args: ${template.args}`); - } - - /** - * Removes a command - * @param command - * @deprecated Why would you want to remove a command? - */ - removeCommand(command) { - delete this.commands[command]; - } - - /** - * Processes the command - * @param msg - * @param globResult - * @param content - * @param returnFunction Boolean if the return value should be a function. - * @param fallback - * @returns {*} - */ - processCommand(msg, globResult, content, returnFunction, fallback) { - let command = (content.match(/^.\w+/) || [])[0]; - if (!command || !this.commands[command]) - if (fallback && !globResult) { - command = fallback; - content = `${fallback} ${content}`; - } else { - return globResult; - } - let cmd = this.commands[command]; - if (!checkPermission(msg, cmd.role)) - return 'No Permission'; - logger.debug(`Permission <${cmd.role || 'all'}> granted for command ${command} for user <${msg.author.tag}>`); - let argvars = content.match(/(?<= )\S+/g) || []; - let kwargs = {}; - let nLength = Math.min(cmd.args.length, argvars.length); - for (let i = 0; i < nLength; i++) - kwargs[cmd.args[i]] = argvars[i]; - - 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); - return locResult || globResult; - } catch (err) { - logger.error(err.message); - return `The command \`${command}\` has thrown an error.`; - } - } - - /** - * Parses the message and executes the command callback for the found command entry in the commands dict - * @param msg - * @returns {*} - */ - parseCommand(msg) { - let globResult = parseGlobalCommand(msg); - logger.debug(`Global command result is ${globResult}`); - let content = msg.content; - let commands = content.split(/(? x.replace(/^ +/, '')); - if (commands.length === 1) { - return this.processCommand(msg, globResult, content); - } 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; - } - - return answers; - } else { - return 'This command sequence is too long!'; - } - } - -} - -/** - * Getting the logger - * @param {Object} newLogger - */ -function setModuleLogger(newLogger) { - logger = newLogger; -} - -/** - * Creates a global command that can be executed in every channel. - * @param prefix - * @param template - * @param call - */ -function createGlobalCommand(prefix, template, call) { - if (!template.name) { - logger.debug(`Name of command template is null or undef. Failed to create command.`); - return; - } - globCommands[prefix + template.name] = { - 'args': template.args || [], - 'description': template.description, - 'callback': call, - 'role': template.permission, - 'name': template.name, - 'category': template.category || 'Other' - }; - logger.debug(`Created global command: ${prefix + template.name}, args: ${template.args}`); -} - - -/** - * Parses a message for a global command - * @param msg - * @returns {boolean|*} - */ -exports.parseMessage = function (msg) { - return parseGlobalCommand(msg); -}; - -/** - * Initializes the module by creating a help command - */ -function initModule(prefix) { - logger.verbose("Creating help command..."); - createGlobalCommand((prefix || config.prefix), gcmdTempl.utils.help, (msg, kwargs) => { - if (kwargs.command) { - let cmd = kwargs.command; - if (cmd.charAt(0) !== prefix) - cmd = prefix + cmd; - if (globCommands[cmd]) - return new Discord.RichEmbed() - .setTitle(`Help for ${cmd}`) - .addField('Usage', `\`${cmd} [${globCommands[cmd].args.join('] [')}]\``.replace('[]', '')) - .addField('Description', globCommands[cmd].description) - .addField('Permission Role', globCommands[cmd].role || 'all'); - - } else { - return createHelpEmbed(globCommands, msg, prefix); - } - }); -} - -/** - * Processes commands for command series. - * @param cmd - * @param msg - * @param content - * @param returnFunction - * @returns {function(): *} - */ -function processCommand(cmd, msg, content, returnFunction) { - let argvars = content.match(/(?<= )\S+/g) || []; - let kwargs = {}; - let nLength = Math.min(cmd.args.length, argvars.length); - for (let i = 0; i < nLength; i++) - 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); -} - -/** - * Parses the message by calling the assigned function for the command with arguments - * @param msg - */ -function parseGlobalCommand(msg) { - let content = msg.content; - let commands = content.split(/(? x.replace(/^ +/, '')); - if (commands.length === 1) { - let command = (content.match(/^.\w+/) || [])[0]; - if (!command || !globCommands[command]) - return false; - let cmd = globCommands[command]; - if (!checkPermission(msg, cmd.role)) - 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)) { - let answers = []; - let previousCommand = ''; - for (let commandPart of commands) { - let command = (commandPart.match(/^.\w+/) || [])[0] || previousCommand; - previousCommand = globCommands[command] ? command : previousCommand; - if (!commandPart || !globCommands[command]) { - commandPart = `${previousCommand} ${commandPart}`; - command = previousCommand; - } - if (command && globCommands[command]) { - let cmd = globCommands[command]; - if (checkPermission(msg, cmd.role)) { - logger.debug(`Permission <${cmd.role}> granted for command ${command} for user <${msg.author.tag}>`); - answers.push(processCommand(cmd, msg, commandPart, - true)); // return an function to avoid "race conditions" - } else { - answers.push(false); - } - } else { - answers.push(false); - } - } - return answers; - } else { - return 'This command sequence is too long!'; - } -} - -/** - * Creates a rich embed that contains help for all commands in the commands object - * @param commands {Object} - * @param msg {module:discord.js.Message} - * @param prefix {String} - * @returns {module:discord.js.RichEmbed} - */ -function createHelpEmbed(commands, msg, prefix) { - let helpEmbed = new Discord.RichEmbed() - .setTitle('Commands') - .setDescription('Create a sequence of commands with `;` (semicolon).') - .setTimestamp(); - let categories = []; - let catCommands = {}; - Object.entries(commands).sort().forEach(([key, value]) => { - if (value.role !== 'owner' || checkPermission(msg, 'owner')) - if (!categories.includes(value.category)) { - categories.push(value.category); - catCommands[value.category] = `\`${key}\` \t`; - } else { - catCommands[value.category] += `\`${key}\` \t`; - } - - }); - for (let cat of categories) - helpEmbed.addField(cat, catCommands[cat]); - - helpEmbed.setFooter(prefix + 'help [command] for more info to each command'); - return helpEmbed; -} - -/** - * @param msg - * @param rolePerm {String} - * @returns {boolean} - */ -function checkPermission(msg, rolePerm) { - if (!rolePerm || ['all', 'any', 'everyone'].includes(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; - - return false; -} - -/** - * Registers the bot's utility commands - * @param prefix - * @param bot - the instance of the bot that called - */ -function registerUtilityCommands(prefix, bot) { - // responde with the commands 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 - 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]); - return `Added Presence \`${p}\``; - }); - - // shuts down the bot after destroying the client - createGlobalCommand(prefix, gcmdTempl.utils.shutdown, async (msg) => { - try { - await msg.reply('Shutting down...'); - logger.debug('Destroying client...'); - } catch (err) { - logger.error(err.message); - logger.debug(err.stack); - } - try { - await bot.client.destroy(); - logger.debug('Exiting server...'); - } catch (err) { - logger.error(err.message); - logger.debug(err.stack); - } - try { - await bot.webServer.stop(); - logger.debug(`Exiting Process...`); - process.exit(0); - } catch (err) { - logger.error(err.message); - logger.debug(err.stack); - } - }); - - // forces a presence rotation - createGlobalCommand(prefix, gcmdTempl.utils.rotate, () => { - try { - bot.client.clearInterval(this.rotator); - bot.rotatePresence(); - bot.rotator = this.client.setInterval(() => this.rotatePresence(), config.presence_duration); - } catch (error) { - logger.warn(error.message); - } - }); - - 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."); - } else if (argv.username && argv.scope) { - logger.debug(`Creating user entry ${argv.username}, scope: ${argv.scope}`); - - bot.webServer.createUser(argv.username, argv.password, argv.scope, false).then((token) => { - resolve(`Created entry - username: ${argv.username}, - scope: ${argv.scope}, - token: ${token} - `); - }).catch((err) => reject(err.message)); - } - }); - }); - - 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} - */ -function registerInfoCommands(prefix, bot) { - // ping command that returns the ping attribute of the client - createGlobalCommand(prefix, gcmdTempl.info.ping, () => { - return `Current average ping: \`${bot.client.ping} ms\``; - }); - - // returns the time the bot is running - createGlobalCommand(prefix, gcmdTempl.info.uptime, () => { - let uptime = utils.getSplitDuration(bot.client.uptime); - return new Discord.RichEmbed().setDescription(` - **${uptime.days}** days - **${uptime.hours}** hours - **${uptime.minutes}** minutes - **${uptime.seconds}** seconds - **${uptime.milliseconds}** milliseconds - `).setTitle('Uptime'); - }); - - // returns the number of guilds, the bot has joined - createGlobalCommand(prefix, gcmdTempl.info.guilds, () => { - return `Number of guilds: \`${bot.client.guilds.size}\``; - }); - - // returns information about the bot - 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} - */ -function registerAnilistApiCommands(prefix) { - const anilistApi = require('./api/AnilistApi'); - - // returns the anime found for the name - createGlobalCommand(prefix, gcmdTempl.api.AniList.animeSearch, async (msg, kwargs, args) => { - try { - let animeData = await anilistApi.searchAnimeByName(args.join(' ')); - if (animeData) { - 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('Scoring', `**Average Score:** ${animeData.averageScore} - **Favourites:** ${animeData.favourites}`); - - if (animeData.episodes) - response.addField('Episodes', animeData.episodes); - response.addField('Season', animeData.season); - - if (animeData.startDate.day) - response.addField('Start Date', ` - ${animeData.startDate.day}.${animeData.startDate.month}.${animeData.startDate.year}`); - - if (animeData.nextAiringEpisode) - response.addField('Next Episode', `**Episode** ${animeData.nextAiringEpisode.episode} - **Airing at:** ${new Date(animeData.nextAiringEpisode.airingAt * 1000).toUTCString()}`); - - if (animeData.endDate.day) - response.addField('End Date', ` - ${animeData.endDate.day}.${animeData.endDate.month}.${animeData.endDate.year}`); - return response; - } else { - return gcmdTempl.api.AniList.animeSearch.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.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/index.js b/lib/commands/AnilistApiCommands/index.js index 160b645..b6dc426 100644 --- a/lib/commands/AnilistApiCommands/index.js +++ b/lib/commands/AnilistApiCommands/index.js @@ -78,8 +78,8 @@ class AniListCommandModule extends cmdLib.CommandModule { ); // registering commands - commandHandler.registerCommand(animeSearch); - commandHandler.registerCommand(mangaSearch); + commandHandler.registerCommand(animeSearch) + .registerCommand(mangaSearch); } } diff --git a/lib/commands/InfoCommands/InfoCommandsTemplate.yaml b/lib/commands/InfoCommands/InfoCommandsTemplate.yaml index 893754c..e58e4c5 100644 --- a/lib/commands/InfoCommands/InfoCommandsTemplate.yaml +++ b/lib/commands/InfoCommands/InfoCommandsTemplate.yaml @@ -39,5 +39,6 @@ help: Shows help for bot ocmmands. permission: all category: Info + embed_color: 0xffffff args: - command diff --git a/lib/commands/InfoCommands/index.js b/lib/commands/InfoCommands/index.js index 5caee38..d243897 100644 --- a/lib/commands/InfoCommands/index.js +++ b/lib/commands/InfoCommands/index.js @@ -21,9 +21,10 @@ class InfoCommandModule extends cmdLib.CommandModule { this._messageHandler = opts.messageHandler; } - _createHelpEmbed(commands, msg, prefix) { + _createHelpEmbed(commands, msg, prefix, embedColor = 0xfff) { let helpEmbed = new cmdLib.ExtendedRichEmbed('Commands') - .setDescription('Create a sequence of commands with `;` and `&&`.'); + .setDescription('Create a sequence of commands with `;` and `&&`.') + .setColor(embedColor); let categories = []; let catCommands = {}; Object.entries(commands).sort().forEach(([key, value]) => { @@ -89,10 +90,10 @@ class InfoCommandModule extends cmdLib.CommandModule { if (k.command) { k.command = k.command.replace(globH.prefix, ''); let commandInstance = globH.commands[k.command] || scopeH.commands[k.command]; - return commandInstance.help; + return commandInstance.help.setColor(this.template.help.embed_color); } else { let commandObj = {...globH.commands, ...scopeH.commands}; - return this._createHelpEmbed(commandObj, m, globH.prefix); + return this._createHelpEmbed(commandObj, m, globH.prefix, this.template.help.embed_color); } }) ); diff --git a/lib/commands/MusicCommands/MusicCommandsTemplate.yaml b/lib/commands/MusicCommands/MusicCommandsTemplate.yaml index cdfb709..e2d7587 100644 --- a/lib/commands/MusicCommands/MusicCommandsTemplate.yaml +++ b/lib/commands/MusicCommands/MusicCommandsTemplate.yaml @@ -53,7 +53,7 @@ stop: name: stop description: > Stops the media playback and leaves the VoiceChannel. - permission: dj + permission: musicPlayer category: Music response: success: > @@ -89,7 +89,7 @@ skip: name: skip description: > Skips the currently playing song. - permission: dj + permission: musicPlayer category: Music response: success: > @@ -101,7 +101,7 @@ clear: name: clear description: > Clears the media queue. - permission: dj + permission: musicPlayer category: Music response: success: > @@ -150,7 +150,7 @@ save_media: name: savemedia description: > Saves the YouTube URL with a specific name. - permission: dj + permission: musicPlayer category: Music args: - url @@ -160,7 +160,7 @@ delete_media: name: deletemedia description: > Deletes a saved YouTube URL from saved media. - permission: dj + permission: musicPlayer category: Music usage: deletemedia [name] response: diff --git a/lib/commands/MusicCommands/index.js b/lib/commands/MusicCommands/index.js index 68e8c64..c667706 100644 --- a/lib/commands/MusicCommands/index.js +++ b/lib/commands/MusicCommands/index.js @@ -30,11 +30,11 @@ class MusicCommandModule extends cmdLib.CommandModule { * @private */ async _connectAndPlay(gh, vc, url, next) { - if (!gh.dj.connected) { - await gh.dj.connect(vc); - await gh.dj.playYouTube(url, next); + if (!gh.musicPlayer.connected) { + await gh.musicPlayer.connect(vc); + await gh.musicPlayer.playYouTube(url, next); } else { - await gh.dj.playYouTube(url, next); + await gh.musicPlayer.playYouTube(url, next); } } @@ -50,7 +50,7 @@ class MusicCommandModule extends cmdLib.CommandModule { */ async _playFunction(m, k, s, t, n) { let gh = await this._getGuildHandler(m.guild); - let vc = gh.dj.voiceChannel || m.member.voiceChannel; + let vc = gh.musicPlayer.voiceChannel || m.member.voiceChannel; let url = k['url']; if (!vc) return t.response.no_voicechannel; @@ -94,7 +94,7 @@ class MusicCommandModule extends cmdLib.CommandModule { new cmdLib.Answer(async (m) => { let gh = await this._getGuildHandler(m.guild); if (m.member.voiceChannel) - await gh.dj.connect(m.member.voiceChannel); + await gh.musicPlayer.connect(m.member.voiceChannel); else return this.template.join.response.no_voicechannel; }) @@ -104,8 +104,8 @@ class MusicCommandModule extends cmdLib.CommandModule { this.template.stop, new cmdLib.Answer(async (m) => { let gh = await this._getGuildHandler(m.guild); - if (gh.dj.connected) { - gh.dj.stop(); + if (gh.musicPlayer.connected) { + gh.musicPlayer.stop(); return this.template.stop.success; } else { return this.template.stop.not_playing; @@ -117,8 +117,8 @@ class MusicCommandModule extends cmdLib.CommandModule { this.template.pause, new cmdLib.Answer(async (m) => { let gh = await this._getGuildHandler(m.guild); - if (gh.dj.playing) { - gh.dj.pause(); + if (gh.musicPlayer.playing) { + gh.musicPlayer.pause(); return this.template.pause.response.success; } else { return this.template.pause.response.not_playing; @@ -130,8 +130,8 @@ class MusicCommandModule extends cmdLib.CommandModule { this.template.resume, new cmdLib.Answer(async (m) => { let gh = await this._getGuildHandler(m.guild); - if (gh.dj.playing) { - gh.dj.resume(); + if (gh.musicPlayer.playing) { + gh.musicPlayer.resume(); return this.template.resume.response.success; } else { return this.template.resume.response.not_playing; @@ -143,8 +143,8 @@ class MusicCommandModule extends cmdLib.CommandModule { this.template.skip, new cmdLib.Answer(async (m) => { let gh = await this._getGuildHandler(m.guild); - if (gh.dj.playing) { - gh.dj.skip(); + if (gh.musicPlayer.playing) { + gh.musicPlayer.skip(); return this.template.skip.response.success; } else { return this.template.skip.response.not_playing; @@ -156,7 +156,7 @@ class MusicCommandModule extends cmdLib.CommandModule { this.template.clear, new cmdLib.Answer(async (m) => { let gh = await this._getGuildHandler(m.guild); - gh.dj.clear(); + gh.musicPlayer.clear(); return this.template.clear.response.success; }) ); @@ -165,14 +165,14 @@ class MusicCommandModule extends cmdLib.CommandModule { this.template.media_queue, new cmdLib.Answer(async (m) => { let gh = await this._getGuildHandler(m.guild); - this._logger.debug(`Found ${gh.dj.queue.length} songs.`); + this._logger.debug(`Found ${gh.musicPlayer.queue.length} songs.`); let description = ''; - for (let i = 0; i < Math.min(gh.dj.queue.length, 9); i++) { - let entry = gh.dj.queue[i]; + for (let i = 0; i < Math.min(gh.musicPlayer.queue.length, 9); i++) { + let entry = gh.musicPlayer.queue[i]; description += `[${entry.title}](${entry.url})\n`; } - return new cmdLib.ExtendedRichEmbed(`${gh.dj.queue.length} songs in queue`) + return new cmdLib.ExtendedRichEmbed(`${gh.musicPlayer.queue.length} songs in queue`) .setDescription(description); }) ); @@ -181,7 +181,7 @@ class MusicCommandModule extends cmdLib.CommandModule { this.template.media_current, new cmdLib.Answer(async (m) => { let gh = await this._getGuildHandler(m.guild); - let song = gh.dj.song; + let song = gh.musicPlayer.song; if (song) return new cmdLib.ExtendedRichEmbed('Now playing:') .setDescription(`[${song.title}](${song.url})`) @@ -196,7 +196,7 @@ class MusicCommandModule extends cmdLib.CommandModule { this.template.shuffle, new cmdLib.Answer(async (m) => { let gh = await this._getGuildHandler(m.guild); - gh.dj.shuffle(); + gh.musicPlayer.shuffle(); return this.template.shuffle.response.success; }) ); @@ -205,8 +205,8 @@ class MusicCommandModule extends cmdLib.CommandModule { this.template.toggle_repeat, new cmdLib.Answer(async (m) => { let gh = await this._getGuildHandler(m.guild); - gh.dj.repeat = !gh.dj.repeat; - return gh.dj.repeat? + gh.musicPlayer.repeat = !gh.musicPlayer.repeat; + return gh.musicPlayer.repeat? this.template.toggle_repeat.response.repeat_true : this.template.toggle_repeat.response.repeat_false; }) diff --git a/lib/commands/ServerUtilityCommands/ServerUtilityCommandsTemplate.yaml b/lib/commands/ServerUtilityCommands/ServerUtilityCommandsTemplate.yaml new file mode 100644 index 0000000..e69de29 diff --git a/lib/commands/ServerUtilityCommands/index.js b/lib/commands/ServerUtilityCommands/index.js new file mode 100644 index 0000000..e69de29 diff --git a/lib/commands/UtilityCommands/index.js b/lib/commands/UtilityCommands/index.js index e2c198c..d7e1a6a 100644 --- a/lib/commands/UtilityCommands/index.js +++ b/lib/commands/UtilityCommands/index.js @@ -20,9 +20,9 @@ class UtilityCommandModule extends cmdLib.CommandModule { constructor(opts) { super(cmdLib.CommandScopes.User); this.templateFile = location + '/UtilityCommandsTemplate.yaml'; - this.bot = opts.bot; - this.logger = opts.logger; - this.config = opts.config; + this._bot = opts.bot; + this._logger = opts.logger; + this._config = opts.config; } async register(commandHandler) { @@ -31,8 +31,8 @@ class UtilityCommandModule extends cmdLib.CommandModule { let addPresence = new cmdLib.Command( this.template.add_presence, new cmdLib.Answer(async (m, k, s) => { - this.bot.presences.push(s); - await this.bot.maindb.run('INSERT INTO presences (text) VALUES (?)', [s]); + this._bot.presences.push(s); + await this._bot.maindb.run('INSERT INTO presences (text) VALUES (?)', [s]); return `Added Presence \`${s}\``; }) ); @@ -41,12 +41,12 @@ class UtilityCommandModule extends cmdLib.CommandModule { this.template.rotate_presence, new cmdLib.Answer(() => { try { - this.bot.client.clearInterval(this.rotator); - this.bot.rotatePresence(); - this.bot.rotator = this.bot.client.setInterval(() => this.bot.rotatePresence(), - this.config.presence_duration); + this._bot.client.clearInterval(this._bot.rotator); + this._bot.rotatePresence(); + this._bot.rotator = this._bot.client.setInterval(() => this._bot.rotatePresence(), + this._config.presence_duration); } catch (error) { - this.logger.warn(error.message); + this._logger.warn(error.message); } }) ); @@ -56,25 +56,25 @@ class UtilityCommandModule extends cmdLib.CommandModule { new cmdLib.Answer(async (m) => { try { await m.reply('Shutting down...'); - this.logger.debug('Destroying client...'); + this._logger.debug('Destroying client...'); + await this._bot.client.destroy(); } catch (err) { - this.logger.error(err.message); - this.logger.debug(err.stack); + this._logger.error(err.message); + this._logger.debug(err.stack); } try { - await this.bot.client.destroy(); - this.logger.debug('Exiting server...'); + this._logger.debug('Exiting server...'); + await this._bot.webServer.stop(); } catch (err) { - this.logger.error(err.message); - this.logger.debug(err.stack); + this._logger.error(err.message); + this._logger.debug(err.stack); } try { - await this.bot.webServer.stop(); - this.logger.debug(`Exiting Process...`); + this._logger.debug(`Exiting Process...`); process.exit(0); } catch (err) { - this.logger.error(err.message); - this.logger.debug(err.stack); + this._logger.error(err.message); + this._logger.debug(err.stack); } }) ); @@ -83,8 +83,8 @@ class UtilityCommandModule extends cmdLib.CommandModule { this.template.create_user, new cmdLib.Answer(async (m, k) => { if (k.username &&k.password && k.scope) { - this.logger.debug(`Creating user entry for ${k.username}`); - let token = await this.bot.webServer.createUser( + this._logger.debug(`Creating user entry for ${k.username}`); + let token = await this._bot.webServer.createUser( k.username, k.password, k.scope, false); return `${k.username}'s token is ${token}`; } @@ -100,11 +100,11 @@ class UtilityCommandModule extends cmdLib.CommandModule { ); // register commands - commandHandler.registerCommand(addPresence); - commandHandler.registerCommand(rotatePresence); - commandHandler.registerCommand(shutdown); - commandHandler.registerCommand(createUser); - commandHandler.registerCommand(bugReport); + commandHandler.registerCommand(addPresence) + .registerCommand(rotatePresence) + .registerCommand(shutdown) + .registerCommand(createUser) + .registerCommand(bugReport); } } diff --git a/lib/guilding.js b/lib/guilding.js index 0af2aa2..ed5bb75 100644 --- a/lib/guilding.js +++ b/lib/guilding.js @@ -1,49 +1,45 @@ -const cmd = require('./cmd'), - music = require('./music'), +const music = require('./MusicLib'), utils = require('./utils'), config = require('../config.json'), sqliteAsync = require('./sqliteAsync'), fs = require('fs-extra'), - servercmd = require('../commands/servercommands'), - Discord = require('discord.js'), - waterfall = require('promise-waterfall'), dataDir = config.dataPath || './data'; + let logger = require('winston'); exports.setLogger = function (newLogger) { logger = newLogger; music.setLogger(logger); - cmd.setLogger(logger); }; /** - * Server-Specific commands, music and more + * The Guild Handler handles guild settings and data. * @type {GuildHandler} */ -exports.GuildHandler = class { - constructor(guild, prefix) { +class GuildHandler { + + constructor(guild) { this.guild = guild; - this.dj = null; - this.mention = false; - this.prefix = prefix || config.prefix; - this.servant = new cmd.Servant(this.prefix); + this.musicPlayer = new music.MusicPlayer(null); } + /** + * Initializes the database + * @returns {Promise} + */ async initDatabase() { await fs.ensureDir(dataDir + '/gdb'); this.db = new sqliteAsync.Database(`${dataDir}/gdb/${this.guild}.db`); await this.db.init(); logger.debug(`Connected to the database for ${this.guild}`); await this.createTables(); - // register commands - this.registerCommands(); } /** * Destroys the guild handler */ destroy() { - this.dj.stop(); + this.musicPlayer.stop(); this.db.close(); } @@ -72,283 +68,8 @@ exports.GuildHandler = class { command VARCHAR(255) NOT NULL )`); } +} - /** - * Answers a message via mention if mentioning is active or with just sending it to the same channel. - * @param msg - * @param answer - */ - async answerMessage(msg, answer) { - if (answer instanceof Promise || answer) - if (answer instanceof Discord.RichEmbed) { - (this.mention) ? msg.reply('', answer) : msg.channel.send('', answer); - } else if (answer instanceof Promise) { - let resolvedAnswer = await answer; - await this.answerMessage(msg, resolvedAnswer); - } else if (answer instanceof Array) { - await waterfall(answer.map((x) => async () => await this.answerMessage(msg, x))); - } else if ({}.toString.call(answer) === '[object Function]') { // check if the answer is of type function - await this.answerMessage(msg, answer()); - } else { - (this.mention) ? msg.reply(answer) : msg.channel.send(answer); - } - } - - /** - * handles the message by letting the servant parse the command. Depending on the message setting it - * replies or just sends the answer. - * @param msg - */ - async handleMessage(msg) { - if (this.db) - await this.db.run( - 'INSERT INTO messages (author, creation_timestamp, author_name, content) values (?, ?, ?, ?)', - [msg.author.id, msg.createdTimestamp, msg.author.username, msg.content] - ); - await this.answerMessage(msg, this.servant.parseCommand(msg)); - } - - /** - * Connect to a voice-channel if not connected and play the url - * @param vc - * @param url - * @param next - */ - async connectAndPlay(vc, url, next) { - if (!this.dj.connected) { - await this.dj.connect(vc); - await this.dj.playYouTube(url, next); - } else { - await this.dj.playYouTube(url, next); - } - } - - /** - * registers all music commands and initializes a dj - */ - registerCommands() { - this.dj = new music.DJ(); - - let playCb = async (msg, kwargs, argv, template, next) => { - let vc = this.dj.voiceChannel || msg.member.voiceChannel; - let url = kwargs['url']; - if (!vc) - return template.response.no_voicechannel; - if (!url) - return template.response.no_url; - if (!utils.YouTube.isValidEntityUrl(url)) { - if (argv && argv.length > 0) - url += ' ' + argv.join(' '); // join to get the whole expression behind the command - let row = await this.db.get('SELECT url FROM playlists WHERE name = ?', [url]); - if (!row) { - logger.debug('Got invalid url for play command.'); - return template.response.url_invalid; - } else { - await this.connectAndPlay(vc, row.url, next); - return template.response.success; - } - } else { - await this.connectAndPlay(vc, url, next); - return template.response.success; - } - }; - - // play command - this.servant.createCommand(servercmd.music.play, async (msg, kwargs, argv) => { - return await playCb(msg, kwargs, argv, servercmd.music.play, false); - }); - - // playnext command - this.servant.createCommand(servercmd.music.playnext, async (msg, kwargs, argv) => { - return await playCb(msg, kwargs, argv, servercmd.music.playnext, true); - }); - - // join command - this.servant.createCommand(servercmd.music.join, (msg) => { - if (msg.member.voiceChannel) - this.dj.connect(msg.member.voiceChannel); - else - return servercmd.music.join.response.not_connected; - - }); - - // stop command - this.servant.createCommand(servercmd.music.stop, () => { - if (this.dj.connected) { - this.dj.stop(); - return servercmd.music.stop.response.success; - } else { - return servercmd.music.stop.response.not_playing; - } - }); - - // pause command - this.servant.createCommand(servercmd.music.pause, () => { - if (this.dj.playing) { - this.dj.pause(); - return servercmd.music.pause.response.success; - } else { - return servercmd.music.pause.response.not_playing; - } - }); - - // resume command - this.servant.createCommand(servercmd.music.resume, () => { - if (this.dj.playing) { - this.dj.resume(); - return servercmd.music.resume.response.success; - } else { - return servercmd.music.resume.response.not_playing; - } - }); - - // skip command - this.servant.createCommand(servercmd.music.skip, () => { - if (this.dj.playing) { - this.dj.skip(); - return servercmd.music.skip.response.success; - } else { - return servercmd.music.skip.response.not_playing; - } - - }); - - // clear command - this.servant.createCommand(servercmd.music.clear, () => { - this.dj.clear(); - return servercmd.music.clear.response.success; - }); - - // playlist command - this.servant.createCommand(servercmd.music.playlist, () => { - logger.debug(`found ${this.dj.queue.length} songs`); - let describtion = ''; - for (let i = 0; i < Math.min(this.dj.queue.length, 9); i++) { - let entry = this.dj.queue[i]; - describtion += `[${entry.title}](${entry.url})\n`; - } - return new Discord.RichEmbed() - .setTitle(`${this.dj.queue.length} songs in queue`) - .setDescription(describtion); - }); - - // np command - this.servant.createCommand(servercmd.music.current, () => { - let song = this.dj.song; - if (song) - return new Discord.RichEmbed() - .setTitle('Now playing:') - .setDescription(`[${song.title}](${song.url})`) - .setImage(utils.YouTube.getVideoThumbnailUrlFromUrl(song.url)) - .setColor(0x00aaff); - else - return servercmd.music.current.response.not_playing; - - }); - - // shuffle command - this.servant.createCommand(servercmd.music.shuffle, () => { - this.dj.shuffle(); - return servercmd.music.shuffle.response.success; - }); - - // repeat command - this.servant.createCommand(servercmd.music.repeat, () => { - if (this.dj) { - this.dj.repeat = !this.dj.repeat; - if (this.dj.repeat) - return servercmd.music.repeat.response.repeat_true; - else - return servercmd.music.repeat.response.repeat_false; - } - }); - - // saves playlists and videos - this.servant.createCommand(servercmd.music.savemedia, async (msg, kwargs, argv) => { - let saveName = argv.join(' '); - let row = await this.db.get('SELECT COUNT(*) count FROM playlists WHERE name = ?', [saveName]); - if (!row || row.count === 0) - await this.db.run('INSERT INTO playlists (name, url) VALUES (?, ?)', [saveName, kwargs.url]); - else - await this.db.run('UPDATE playlists SET url = ? WHERE name = ?', [kwargs.url, saveName]); - return `Saved song/playlist as ${saveName}`; - }); - - // savedmedia command - prints out saved playlists and videos - this.servant.createCommand(servercmd.music.savedmedia, async () => { - let response = ''; - let rows = await this.db.all('SELECT name, url FROM playlists'); - for (let row of rows) - response += `[${row.name}](${row.url})\n`; - - if (rows.length === 0) - return servercmd.music.savedmedia.response.no_saved; - else - return new Discord.RichEmbed() - .setTitle('Saved Songs and Playlists') - .setDescription(response) - .setFooter(`Play a saved entry with ${this.prefix}play [Entryname]`) - .setTimestamp(); - }); - - this.servant.createCommand(servercmd.music.deletemedia, async (msg, kwargs, argv) => { - let saveName = argv.join(' '); - if (!saveName) { - return servercmd.music.deletemedia.response.no_name; - } else { - await this.db.run('DELETE FROM playlists WHERE name = ?', [saveName]); - return `Deleted ${saveName} from saved media`; - } - }); - - // savecmd - saves a command sequence with a name - this.servant.createCommand(servercmd.utils.savecmd, async (msg, kwargs, argv) => { - let saveName = argv.pop(); - let cmdsequence = argv.join(' ').replace(/\\/g, ''); - if (argv.includes(this.prefix + servercmd.utils.execute.name)) { - return servercmd.utils.savecmd.response.no_recursion; - } else if (cmdsequence.split(';').length < (config.maxCmdSequenceLength || 5)){ - let row = await this.db.get('SELECT COUNT(*) count FROM commands WHERE name = ?', [saveName]); - if (!row || row.count === 0) - await this.db.run('INSERT INTO commands (name, command) VALUES (?, ?)', [saveName, cmdsequence]); - else - await this.db.run('UPDATE commands SET sequence = ? WHERE name = ?', [cmdsequence, saveName]); - return `saved command sequence as ${saveName}`; - } else { - return servercmd.utils.savecmd.response.sequence_too_long; - } - }); - - // savedcmd - prints saved commands - this.servant.createCommand(servercmd.utils.savedcmd, async () => { - let response = new Discord.RichEmbed() - .setTitle('Saved Commands') - .setFooter(`Execute a saved entry with ${this.prefix}execute [Entryname]`) - .setTimestamp(); - let rows = await this.db.all('SELECT name, command FROM commands'); - if (rows.length === 0) - return servercmd.utils.savedcmd.response.no_commands; - else - for (let row of rows) - response.addField(row.name, '`' + row.command + '`'); - return response; - }); - - // deletecmd - deletes a command from saved commands - this.servant.createCommand(servercmd.utils.deletecmd, async (msg, kwargs) => { - await this.db.run('DELETE FROM commands WHERE name = ?', [kwargs.cmdname]); - return `Deleted command ${kwargs.cmdname}`; - }); - - // execute - executes a saved command - this.servant.createCommand(servercmd.utils.execute, async (msg, kwargs) => { - let row = await this.db.get('SELECT command FROM commands WHERE name = ?', [kwargs.cmdname]); - if (row) { - msg.content = row.command; - await this.handleMessage(msg); - } else { - return servercmd.utils.execute.response.not_found; - } - }); - } -}; +Object.assign(exports, { + GuildHandler: GuildHandler +}); diff --git a/test/test.js b/test/test.js index 83d3379..0b18266 100644 --- a/test/test.js +++ b/test/test.js @@ -201,7 +201,7 @@ describe('lib/music', function() { "api": {} }); - describe('#DJ', function () { + describe('#MusicPlayer', function () { it('connects to a VoiceChannel', function (done) { let dj = new music.DJ(mockobjects.mockVoicechannel); @@ -456,7 +456,7 @@ describe('lib/guilding', function*() { // deactivated because of problems with s let gh = new guilding.GuildHandler('test', '~'); gh.db = new mockobjects.MockDatabase('', ()=>{}); gh.ready = true; - gh.dj = new music.DJ(mockobjects.mockVoicechannel); + gh.musicPlayer = new music.DJ(mockobjects.mockVoicechannel); gh.connectAndPlay(mockobjects.mockVoicechannel, 'test', false).then(() => { done(); }); diff --git a/web/http/index.pug b/web/http/index.pug index 145e2c6..95650ea 100644 --- a/web/http/index.pug +++ b/web/http/index.pug @@ -50,30 +50,30 @@ head span.label.text-right Member Count: span#guild-memberCount.text-left .space - h3.cell DJ + h3.cell MusicPlayer .cell span.label.text-right State: - span#guild-djStatus.text-left + span#guild-mpStatus.text-left .cell span.label.text-right Repeat: - span#dj-repeat.text-left + span#mp-repeat.text-left .cell span.label.text-right Voice Channel: - span#dj-voiceChannel.text-left - #dj-songinfo.listContainer(style='display: none') + span#mp-voiceChannel.text-left + #mp-songinfo.listContainer(style='display: none') a#songinfo-container - span#dj-songname - img#dj-songImg(src='' alt='') - #dj-songProgress(style='display:none') - span#dj-songCurrentTS - #dj-queue-container + span#mp-songname + img#mp-songImg(src='' alt='') + #mp-songProgress(style='display:none') + span#mp-songCurrentTS + #mp-queue-container span.cell.label(id='Queue Song count') - span#dj-queueCount + span#mp-queueCount | Songs in Queue span.cell | Next - span#dj-queueDisplayCount 0 + span#mp-queueDisplayCount 0 | Songs: - #dj-songQueue + #mp-songQueue script. startUpdating(); diff --git a/web/http/sass/style.sass b/web/http/sass/style.sass index b8142b6..40bee43 100644 --- a/web/http/sass/style.sass +++ b/web/http/sass/style.sass @@ -247,7 +247,7 @@ div.cell > * #guild-nameAndIcon width: 50% -#dj-songinfo +#mp-songinfo background-color: $cBackgroundVariant border-radius: 20px overflow-x: hidden @@ -259,15 +259,15 @@ div.cell > * padding: 10px width: calc(100% - 20px) -#dj-queue-container +#mp-queue-container display: grid padding: 0 5px 5px -#dj-songname +#mp-songname font-weight: bold font-size: 120% -#dj-songImg +#mp-songImg align-self: center width: 80% height: auto @@ -281,6 +281,6 @@ div.cell > * #guildinfo:hover overflow-y: auto -#dj-songQueue +#mp-songQueue display: grid max-height: 100% diff --git a/web/http/scripts/query.js b/web/http/scripts/query.js index 424abf2..04690c4 100644 --- a/web/http/scripts/query.js +++ b/web/http/scripts/query.js @@ -60,7 +60,7 @@ function queryGuilds() { guilds { id name - dj { + musicPlayer { playing } } @@ -71,8 +71,8 @@ function queryGuilds() { if ($(`option[value=${guild.id}]`).length === 0) { let option = document.createElement('option'); option.setAttribute('value', guild.id); - if (guild.dj) - option.innerText = guild.dj.playing? guild.name + ' 🎶' : guild.name; + if (guild.musicPlayer) + option.innerText = guild.musicPlayer.playing? guild.name + ' 🎶' : guild.name; let guildSelect = document.querySelector('#guild-select'); guildSelect.appendChild(option); } @@ -118,7 +118,7 @@ function queryGuildStatus(guildId) { let query = `{ client { guilds(id: "${guildId}") { - dj { + musicPlayer { playing connected repeat @@ -144,29 +144,29 @@ function queryGuildStatus(guildId) { }`; postQuery(query).then((res) => { let guild = res.data.client.guilds[0]; - document.querySelector('#dj-repeat').innerText = guild.dj.repeat? 'on': 'off'; - document.querySelector('#guild-djStatus').innerText = guild.dj.connected? 'connected' : 'disconnected'; - if (guild.dj.connected) { - let songinfoContainer = $('#dj-songinfo'); + document.querySelector('#mp-repeat').innerText = guild.musicPlayer.repeat? 'on': 'off'; + document.querySelector('#guild-mpStatus').innerText = guild.musicPlayer.connected? 'connected' : 'disconnected'; + if (guild.musicPlayer.connected) { + let songinfoContainer = $('#mp-songinfo'); songinfoContainer.show(); - document.querySelector('#guild-djStatus').innerText = guild.dj.playing? 'playing' : 'connected'; - document.querySelector('#dj-voiceChannel').innerText = guild.dj.voiceChannel; + document.querySelector('#guild-mpStatus').innerText = guild.musicPlayer.playing? 'playing' : 'connected'; + document.querySelector('#mp-voiceChannel').innerText = guild.musicPlayer.voiceChannel; - if (guild.dj.playing) { + if (guild.musicPlayer.playing) { if (songinfoContainer.is(':hidden')) songinfoContainer.show(); - document.querySelector('#guild-djStatus').innerText = guild.dj.paused? 'paused' : 'playing'; - document.querySelector('#songinfo-container').setAttribute('href', guild.dj.currentSong.url); - document.querySelector('#dj-songname').innerText = guild.dj.currentSong.name; - document.querySelector('#dj-songImg').setAttribute('src', guild.dj.currentSong.thumbnail.replace('maxresdefault', 'mqdefault')); - let songSd = getSplitDuration(Date.now() - guild.dj.songStartTime); - document.querySelector('#dj-songCurrentTS').innerText = `${songSd.minutes}:${songSd.seconds.toString().padStart(2, '0')}`; - document.querySelector('#dj-songCurrentTS').setAttribute('start-ts', guild.dj.songStartTime); - document.querySelector('#dj-queueCount').innerText = guild.dj.queueCount; - let songContainer = document.querySelector('#dj-songQueue'); + document.querySelector('#guild-mpStatus').innerText = guild.musicPlayer.paused? 'paused' : 'playing'; + document.querySelector('#songinfo-container').setAttribute('href', guild.musicPlayer.currentSong.url); + document.querySelector('#mp-songname').innerText = guild.musicPlayer.currentSong.name; + document.querySelector('#mp-songImg').setAttribute('src', guild.musicPlayer.currentSong.thumbnail.replace('maxresdefault', 'mqdefault')); + let songSd = getSplitDuration(Date.now() - guild.musicPlayer.songStartTime); + document.querySelector('#mp-songCurrentTS').innerText = `${songSd.minutes}:${songSd.seconds.toString().padStart(2, '0')}`; + document.querySelector('#mp-songCurrentTS').setAttribute('start-ts', guild.musicPlayer.songStartTime); + document.querySelector('#mp-queueCount').innerText = guild.musicPlayer.queueCount; + let songContainer = document.querySelector('#mp-songQueue'); $('.songEntry').remove(); - for (let song of guild.dj.queue) { + for (let song of guild.musicPlayer.queue) { let songEntry = document.createElement('a'); songEntry.setAttribute('href', song.url); songEntry.setAttribute('class', 'songEntry'); @@ -179,14 +179,14 @@ function queryGuildStatus(guildId) { songEntry.appendChild(nameEntry); songContainer.appendChild(songEntry); } - document.querySelector('#dj-queueDisplayCount').innerText = document.querySelectorAll('.songEntry').length; + document.querySelector('#mp-queueDisplayCount').innerText = document.querySelectorAll('.songEntry').length; } else { if (songinfoContainer.is(':not(:hidden)')) songinfoContainer.hide(); } } else { - $('#dj-songinfo').hide(); - document.querySelector('#dj-voiceChannel').innerText = 'None'; + $('#mp-songinfo').hide(); + document.querySelector('#mp-voiceChannel').innerText = 'None'; } }); } @@ -302,7 +302,7 @@ function startUpdating() { queryGuild(guildId); }); setInterval(() => { - let songSd = getSplitDuration(Date.now() - $('#dj-songCurrentTS').attr('start-ts')); - document.querySelector('#dj-songCurrentTS').innerText = `${songSd.minutes}:${songSd.seconds.toString().padStart(2, '0')}`; + let songSd = getSplitDuration(Date.now() - $('#mp-songCurrentTS').attr('start-ts')); + document.querySelector('#mp-songCurrentTS').innerText = `${songSd.minutes}:${songSd.seconds.toString().padStart(2, '0')}`; }, 500); } From 6f890dacee5cae39606263ac97f72355dba4dc35 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sat, 2 Mar 2019 18:07:16 +0100 Subject: [PATCH 19/26] Added ServerUtilityCommands module --- .../ServerUtilityCommandsTemplate.yaml | 48 +++++++ lib/commands/ServerUtilityCommands/index.js | 135 ++++++++++++++++++ 2 files changed, 183 insertions(+) diff --git a/lib/commands/ServerUtilityCommands/ServerUtilityCommandsTemplate.yaml b/lib/commands/ServerUtilityCommands/ServerUtilityCommandsTemplate.yaml index e69de29..92db20e 100644 --- a/lib/commands/ServerUtilityCommands/ServerUtilityCommandsTemplate.yaml +++ b/lib/commands/ServerUtilityCommands/ServerUtilityCommandsTemplate.yaml @@ -0,0 +1,48 @@ +save_cmd: + name: savecmd + description: > + Saves a sequence of commands under a new name. + permission: moderator + category: Server Utility + usage: savecmd [cmdname] [cmdsequence] + args: + - name + response: + no_recursion: > + You are **not** allowed to execute another saved command in this sequence. + This is to prevent recursion that could break the bot. + sequence_too_many_parallel: > + This sequence executes too many commands in parallel. + sequence_too_many_serial: > + This sequence executes too long serial chains. + +delete_cmd: + name: deletecmd + description: > + Deletes a saved command. + permission: moderator + category: Server Utility + args: + - name + +saved_cmd: + name: savedcmd + description: > + Lists all saved commands. + category: Server Utility + permission: all + response: + no_commands: > + There are no saved commands. + +execute: + name: execute + description: > + Executes a saved command. + permission: all + category: Server Utility + args: + - name + response: + not_found: > + The command to be executed couldn't be found. diff --git a/lib/commands/ServerUtilityCommands/index.js b/lib/commands/ServerUtilityCommands/index.js index e69de29..2198c5c 100644 --- a/lib/commands/ServerUtilityCommands/index.js +++ b/lib/commands/ServerUtilityCommands/index.js @@ -0,0 +1,135 @@ +const cmdLib = require('../../CommandLib'), + location = './lib/commands/ServerUtilityCommands'; + +/** + * This command module includes utility commands for the server. + */ +class ServerUtilityCommandModule extends cmdLib.CommandModule { + + /** + * @param opts {Object} properties: + * getGuildHandler - a function to get the guild handler for the guild + * messagehandler - the MessageHandler instance + * logger - the instance of the logger. + * config - the config object + */ + constructor(opts) { + super(cmdLib.CommandScopes.Guild); + this.templateFile = location + '/ServerUtilityCommandsTemplate.yaml'; + this._messageHandler = opts.messageHandler; + this._getGuildHandler = opts.getGuildHandler; + this._logger = opts.logger; + this._config = opts.config; + } + + /** + * Serializes a command sequence to string. + * @param sqArray + * @returns {*} + * @private + */ + _serializeCmdSequence(sqArray) { + this._logger.debug(sqArray); + return sqArray.map((x) => x.join(' && ')).join('; '); + } + + /** + * Registers the utility commands. + * @param commandHandler + */ + async register(commandHandler) { + await this._loadTemplate(); + + let saveCmd = new cmdLib.Command( + this.template.save_cmd, + new cmdLib.Answer(async (m, k, s) => { + let gh = await this._getGuildHandler(m.guild); + let sequenceString = s + .replace(new RegExp(`^${k.name}\\s`), '') + .replace(/\\&/g, '&') + .replace(/\\;/g, ';'); + let innerStrings = sequenceString.match(/'.+?'/g) || []; + + for (let innerString of innerStrings) + sequenceString.replace(innerString, innerString + .replace(/&/g, '\\&')) + .replace(/;/g, '\\;'); + sequenceString = sequenceString + .replace(/"/g, '') + .replace("'", '"'); + let sequence = this._messageHandler.parseSyntaxString(sequenceString); + let execCommand = this._config.prefix + this.template.execute.name; + let maxSqPar = this._config.commandSettings.maxSequenceParallel; + let maxSqSer = this._config.commandSettings.maxSequenceSerial; + + if (sequenceString.includes(execCommand)) { + return this.template.save_cmd.response.no_recursion; + } else if (sequence.length > maxSqPar) { + return this.template.save_cmd.response.sequence_too_many_parallel; + } else if (sequence.find(x => x.length > maxSqSer)) { + return this.template.save_cmd.response.sequence_too_many_serial; + } else { + let row = await gh.db + .get('SELECT COUNT(*) count FROM commands WHERE name = ?', [k.name]); + if (!row || row.count === 0) + await gh.db + .run('INSERT INTO commands (name, command) VALUES (?, ?)', [k.name, JSON.stringify(sequence)]); + else + await await gh.db + .run('UPDATE commands SET command = ? WHERE name = ?', [JSON.stringify(sequence), k.name]); + } + }) + ); + + let deleteCmd = new cmdLib.Command( + this.template.delete_cmd, + new cmdLib.Answer(async (m, k) => { + let gh = await this._getGuildHandler(m.guild); + await gh.db.run('DELETE FROM commands WHERE name = ?', [k.name]); + return `Deleted command ${k.name}`; + }) + ); + + let savedCmd = new cmdLib.Command( + this.template.saved_cmd, + new cmdLib.Answer(async (m) => { + let gh = await this._getGuildHandler(m.guild); + let response = new cmdLib.ExtendedRichEmbed('Saved Commands') + .setFooter(`Execute a saved entry with ${this._config.prefix}execute [Entryname]`); + let rows = await gh.db.all('SELECT name, command FROM commands'); + if (rows.length === 0) + return this.template.saved_cmd.response.no_commands; + else + for (let row of rows) + response.addField(row.name, '`' + this._serializeCmdSequence(JSON.parse(row.command)) + '`'); + return response; + }) + ); + + let execute = new cmdLib.Command( + this.template.execute, + new cmdLib.Answer(async (m, k) => { + let gh = await this._getGuildHandler(m.guild); + let row = await gh.db + .get('SELECT command FROM commands WHERE name = ?', [k.name]); + if (row) + await this._messageHandler + .executeCommandSequence(JSON.parse(row.command), m); + else + return this.template.execute.response.not_found; + + }) + ); + + // register commands + commandHandler + .registerCommand(saveCmd) + .registerCommand(deleteCmd) + .registerCommand(savedCmd) + .registerCommand(execute); + } +} + +Object.assign(exports, { + 'module': ServerUtilityCommandModule +}); From e74fa83ed33288b7458e3cf9420ecbd09b703eb9 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sat, 2 Mar 2019 20:10:13 +0100 Subject: [PATCH 20/26] Added Unit Tests & Logging - Added own logger class that includes the module name - added unit tests for MessageHandler, Command and Answer - removed unit tests for GuildHandler and lib/cmd - removed old command templates --- bot.js | 107 +++++----- commands/globalcommands.json | 109 ---------- commands/servercommands.json | 203 ------------------ lib/CommandLib.js | 11 + lib/MessageLib.js | 16 +- lib/MusicLib.js | 77 ++++--- lib/WebLib.js | 16 +- lib/commands/AnilistApiCommands/index.js | 6 + lib/commands/InfoCommands/index.js | 1 + lib/commands/MusicCommands/index.js | 2 - lib/commands/ServerUtilityCommands/index.js | 2 - lib/commands/UtilityCommands/index.js | 2 - lib/guilding.js | 19 +- lib/logging.js | 170 ++++++++++----- lib/utils.js | 39 ++-- test/test.js | 225 ++++++-------------- 16 files changed, 332 insertions(+), 673 deletions(-) delete mode 100644 commands/globalcommands.json delete mode 100644 commands/servercommands.json diff --git a/bot.js b/bot.js index 5d995fc..0edd8db 100644 --- a/bot.js +++ b/bot.js @@ -1,6 +1,6 @@ const Discord = require("discord.js"), fs = require('fs-extra'), - logger = require('./lib/logging').getLogger(), + logging = require('./lib/logging'), msgLib = require('./lib/MessageLib'), guilding = require('./lib/guilding'), utils = require('./lib/utils'), @@ -21,27 +21,27 @@ class Bot { constructor() { this.client = new Discord.Client({autoReconnect: true}); + this.logger = new logging.Logger(this); this.rotator = null; this.maindb = null; this.presences = []; - this.messageHandler = new msgLib.MessageHandler(this.client, logger); + this.messageHandler = new msgLib.MessageHandler(this.client); this.guildHandlers = {}; - logger.verbose('Verifying config'); + this.logger.verbose('Verifying config'); let configVerifyer = new utils.ConfigVerifyer(config, [ "api.botToken", "api.youTubeApiKey", "commandSettings.maxSequenceParallel", "commandSettings.maxSequenceSerial" ]); - if (!configVerifyer.verifyConfig(logger)) + if (!configVerifyer.verifyConfig(this.logger)) if (!args.i) { - logger.info('Invalid config. Exiting'); - logger.flush().then(() => { + this.logger.info('Invalid config. Exiting'); + this.logger.flush().then(() => { process.exit(1); }); } - guilding.setLogger(logger); } /** @@ -49,7 +49,7 @@ class Bot { * @returns {Promise} */ async initServices() { - logger.verbose('Registering cleanup function'); + this.logger.verbose('Registering cleanup function'); utils.Cleanup(() => { for (let gh in Object.values(this.guildHandlers)) @@ -57,10 +57,10 @@ class Bot { gh.destroy(); this.client.destroy().then(() => { - logger.debug('destroyed client'); + this.logger.debug('destroyed client'); }).catch((err) => { - logger.error(err.message); - logger.debug(err.stack); + this.logger.error(err.message); + this.logger.debug(err.stack); }); this.maindb.close(); }); @@ -68,25 +68,24 @@ class Bot { if (config.webinterface && config.webinterface.enabled) await this.initializeWebserver(); - logger.verbose('Registering commands'); - await this.messageHandler - .registerCommandModule(require('./lib/commands/AnilistApiCommands').module, {}); - await this.messageHandler - .registerCommandModule(require('./lib/commands/UtilityCommands').module, {bot: this, logger: logger, config: config}); - await this.messageHandler - .registerCommandModule(require('./lib/commands/InfoCommands').module, {client: this.client, messageHandler: this.messageHandler}); - await this.messageHandler - .registerCommandModule(require('./lib/commands/MusicCommands').module, { - getGuildHandler: async (g) => await this.getGuildHandler(g), - logger: logger - }); - await this.messageHandler - .registerCommandModule(require('./lib/commands/ServerUtilityCommands').module, { - getGuildHandler: async (g) => await this.getGuildHandler(g), - logger: logger, - messageHandler: this.messageHandler, - config: config - }); + this.logger.verbose('Registering commands'); + await this.messageHandler.registerCommandModule(require('./lib/commands/AnilistApiCommands').module, {}); + await this.messageHandler.registerCommandModule(require('./lib/commands/UtilityCommands').module, { + bot: this, + config: config + }); + await this.messageHandler.registerCommandModule(require('./lib/commands/InfoCommands').module, { + client: this.client, + messageHandler: this.messageHandler + }); + await this.messageHandler.registerCommandModule(require('./lib/commands/MusicCommands').module, { + getGuildHandler: async (g) => await this.getGuildHandler(g) + }); + await this.messageHandler.registerCommandModule(require('./lib/commands/ServerUtilityCommands').module, { + getGuildHandler: async (g) => await this.getGuildHandler(g), + messageHandler: this.messageHandler, + config: config + }); this.registerEvents(); } @@ -96,11 +95,11 @@ class Bot { */ async start() { await this.client.login(authToken); - logger.debug("Logged in"); + this.logger.debug("Logged in"); if (this.webServer) { this.webServer.start(); - logger.info(`WebServer runing on port ${this.webServer.port}`); + this.logger.info(`WebServer runing on port ${this.webServer.port}`); } } @@ -109,9 +108,9 @@ class Bot { * @returns {Promise} */ async initializeDatabase() { - logger.debug('Checking for ./data/ existence'); + this.logger.debug('Checking for ./data/ existence'); await fs.ensureDir('./data'); - logger.verbose('Connecting to main database'); + this.logger.verbose('Connecting to main database'); this.maindb = new sqliteAsync.Database('./data/main.db'); await this.maindb.init(); @@ -119,7 +118,7 @@ class Bot { ${utils.sql.pkIdSerial}, text VARCHAR(255) UNIQUE NOT NULL )`); - logger.debug('Loading Presences...'); + this.logger.debug('Loading Presences...'); await this.loadPresences(); } @@ -127,12 +126,11 @@ class Bot { * initializes the api webserver */ async initializeWebserver() { - logger.verbose('Importing weblib'); + this.logger.verbose('Importing weblib'); weblib = require('./lib/WebLib'); - weblib.setLogger(logger); - logger.verbose('Creating WebServer'); + this.logger.verbose('Creating WebServer'); this.webServer = new weblib.WebServer(config.webinterface.port || 8080); - logger.debug('Setting Reference Objects to webserver'); + this.logger.debug('Setting Reference Objects to webserver'); await this.webServer.setReferenceObjects({ client: this.client, @@ -159,7 +157,7 @@ class Bot { lineReader.on('line', (line) => { this.maindb.run('INSERT INTO presences (text) VALUES (?)', [line], (err) => { if (err) - logger.warn(err.message); + this.logger.warn(err.message); }); this.presences.push(line); @@ -190,8 +188,8 @@ class Bot { this.client.user.setPresence({ game: {name: `${gamepresence} | ${pr}`, type: "PLAYING"}, status: 'online' - }).then(() => logger.debug(`Presence rotation to ${pr}`)) - .catch((err) => logger.warn(err.message)); + }).then(() => this.logger.debug(`Presence rotation to ${pr}`)) + .catch((err) => this.logger.warn(err.message)); } @@ -200,12 +198,12 @@ class Bot { */ registerEvents() { this.client.on('error', (err) => { - logger.error(err.message); - logger.debug(err.stack); + this.logger.error(err.message); + this.logger.debug(err.stack); }); this.client.on('ready', () => { - logger.info(`logged in as ${this.client.user.tag}!`); + this.logger.info(`logged in as ${this.client.user.tag}!`); this.client.user.setPresence({ game: { @@ -213,7 +211,7 @@ class Bot { }, status: 'online' }).catch((err) => { if (err) - logger.warn(err.message); + this.logger.warn(err.message); }); }); @@ -248,18 +246,19 @@ class Bot { // Executing the main function if (typeof require !== 'undefined' && require.main === module) { - logger.info("Starting up... "); - logger.debug('Calling constructor...'); + let logger = logging.logger; + logger.info("Starting up... ", {m: 'bot.init'}); + logger.debug('Calling constructor...', {m: 'bot.init'}); let discordBot = new Bot(); - logger.debug('Initializing services...'); + logger.debug('Initializing services...', {m: 'bot.init'}); discordBot.initServices().then(() => { - logger.debug('Starting Bot...'); + logger.debug('Starting Bot...', {m: 'bot.init'}); discordBot.start().catch((err) => { //eslint-disable-line promise/no-nesting - logger.error(err.message); - logger.debug(err.stack); + logger.error(err.message, {m: 'bot.init'}); + logger.debug(err.stack, {m: 'bot.init'}); }); }).catch((err) => { - logger.error(err.message); - logger.debug(err.stack); + logger.error(err.message, {m: 'bot.init'}); + logger.debug(err.stack, {m: 'bot.init'}); }); } diff --git a/commands/globalcommands.json b/commands/globalcommands.json deleted file mode 100644 index ca49c38..0000000 --- a/commands/globalcommands.json +++ /dev/null @@ -1,109 +0,0 @@ -{ - "utils": { - "help": { - "name": "help", - "permission": "all", - "description": "Shows this help command.", - "category": "Utility", - "args": [ - "command" - ] - }, - "say": { - "name": "say", - "permission": "all", - "description": "Says something. ~say [String].", - "category": "Utility" - }, - "addpresence": { - "name": "addpresence", - "permission": "owner", - "description": "Adds a presence to presences.", - "category": "Utility" - }, - "shutdown": { - "name": "shutdown", - "description": "Shuts the bot down.", - "permission": "owner", - "category": "Utility" - }, - "rotate": { - "name": "rotate", - "description": "Forces a presence rotation", - "permission": "owner", - "category": "Utility" - }, - "createUser": { - "name": "createUser", - "permission": "owner", - "description": "Creates a new user for the webinterface.", - "category": "Utility", - "args": [ - "username", - "password", - "scope" - ] - }, - "bugreport": { - "name": "bug", - "permission": "all", - "description": "Get info about how to report a bug", - "category": "Utility", - "response": { - "bug_report": "Please report your bugs [here](https://github.com/Trivernis/discordbot.js/issues)" - } - } - }, - "info": { - "about": { - "name": "about", - "permission": "all", - "description": "Shows information about this bot.", - "category": "Info", - "response": { - "about_icon": "This icon war created by [blackrose14344](https://www.deviantart.com/blackrose14344). \n [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", - "permission": "owner", - "description": "Answers with the current average ping of the bot.", - "category": "Info" - }, - "uptime": { - "name": "uptime", - "permission": "owner", - "description": "Answers with the current uptime of the bot.", - "category": "Info" - }, - "guilds": { - "name": "guilds", - "permission": "owner", - "description": "Answers with the number of guilds the bot has joined.", - "category": "Info" - } - }, - "api": { - "AniList": { - "animeSearch": { - "name": "anime", - "permission": "all", - "description": "Answers the anime found for that name on AniList.", - "category": "AniList", - "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/commands/servercommands.json b/commands/servercommands.json deleted file mode 100644 index e83b393..0000000 --- a/commands/servercommands.json +++ /dev/null @@ -1,203 +0,0 @@ -{ - "utils": { - "roles": { - "name": "roles", - "permission": "all", - "description": "Shows the roles used for commands on the server.", - "category": "Utility" - }, - "savecmd": { - "name": "savecmd", - "permission": "moderator", - "description": "Saves a sequence of commands under a new name. ~save [cmdsequence] [cmdname]. Semicoli must be escaped with \\ (Backslash)", - "category": "Utility", - "response": { - "no_recursion": "You are **not** allowed to execute another saved command in this sequence. This is a safety measure to avoid endlessly recursive calls.", - "sequence_too_long": "This command sequence is too long!" - } - }, - "savedcmd": { - "name": "savedcmd", - "permission": "all", - "description": "Displays the saved commands.", - "category": "Utility", - "response": { - "no_commands": "There are no saved commands." - } - }, - "deletecmd": { - "name": "deletecmd", - "permission": "moderator", - "description": "Delete a saved command.", - "args": [ - "cmdname" - ], - "category": "Utility" - }, - "execute": { - "name": "execute", - "permission": "all", - "args": [ - "cmdname" - ], - "description": "Execute saved commands.", - "category": "Utility", - "response": { - "not_found": "This command could not be found." - } - } - }, - "music": { - "play": { - "name": "play", - "permission": "all", - "args": [ - "url" - ], - "description": "Adds the url to the YouTube video/playlist into the queue.", - "category": "Music", - "response": { - "success": "Added Song/Playlist to the queue.", - "failure": "Failed adding Song/Playlist to the queue.", - "url_invalid": "This is not a valid url!", - "no_url": "I need an url to a video to play!", - "no_voicechannel": "You need to join a voicechannel to do that!" - } - }, - "playnext": { - "name": "playnext", - "permission": "all", - "args": [ - "url" - ], - "description": "Adds the url to the YouTube video as next song to the queue.", - "category": "Music", - "response": { - "success": "Added Song as next Song to the queue.", - "failure": "Failed adding Song as next Song to the queue.", - "url_invalid": "This is not a valid url!", - "no_url": "I need an url to a video to play", - "no_voicechannel": "You need to join a voicechannel to do that!" - } - }, - "join": { - "name": "join", - "permission": "all", - "description": "Joins the VC you are in.", - "category": "Music", - "response": { - "not_connected": "You are not connected to a Voice Channel." - } - }, - "stop": { - "name": "stop", - "permission": "dj", - "description": "Stops playing music and leaves.", - "category": "Music", - "response": { - "success": "Stopping now...", - "not_playing": "I'm not playing music at the moment." - } - }, - "pause": { - "name": "pause", - "permission": "all", - "description": "Pauses playing.", - "category": "Music", - "response": { - "success": "Pausing playback.", - "not_playing": "I'm not playing music at the moment." - } - }, - "resume": { - "name": "resume", - "permission": "all", - "description": "Resumes playing.", - "category": "Music", - "response": { - "success": "Resuming playback.", - "not_playing": "I'm not playing music at the moment." - } - }, - "skip": { - "name": "skip", - "permission": "dj", - "description": "Skips the current song.", - "category": "Music", - "response": { - "success": "Skipping to the next song.", - "not_playing": "I'm not playing music at the moment." - } - }, - "clear": { - "name": "clear", - "permission": "dj", - "description": "Clears the queue.", - "category": "Music", - "response": { - "success": "The Queue has been cleared." - } - }, - "playlist": { - "name": "queue", - "permission": "all", - "description": "Shows the next ten songs.", - "category": "Music" - }, - "current": { - "name": "np", - "permission": "all", - "description": "Shows the currently playing song.", - "category": "Music", - "response": { - "not_playing": "I'm not playing music at the moment." - } - }, - "shuffle": { - "name": "shuffle", - "permission": "all", - "description": "Shuffles the playlist.", - "category": "Music", - "response": { - "success": "The Queue has been shuffled." - } - }, - "repeat": { - "name": "repeat", - "permission": "all", - "description": "Toggle listening on repeat.", - "category": "Music", - "response": { - "repeat_true": "Listening on repeat now!", - "repeat_false": "Not listening on repeat anymore." - } - }, - "savemedia": { - "name": "savemedia", - "permission": "dj", - "args": [ - "url" - ], - "description": "Saves the YouTube song/playlist with a specific name", - "category": "Music" - }, - "savedmedia": { - "name": "savedmedia", - "permission": "all", - "description": "Prints out all saved playlists and songs.", - "category": "Music", - "response": { - "no_saved": "There are no saved songs/playlists :(" - } - }, - "deletemedia": { - "name": "deletemedia", - "permission": "dj", - "description": "Deletes a saved media entry. ~deletemedia [name]", - "category": "Music", - "response": { - "no_name": "You must provide a name for the media that shall be deleted." - } - } - } -} diff --git a/lib/CommandLib.js b/lib/CommandLib.js index 5e8526b..01674e2 100644 --- a/lib/CommandLib.js +++ b/lib/CommandLib.js @@ -1,6 +1,7 @@ const Discord = require('discord.js'), yaml = require('js-yaml'), fsx = require('fs-extra'), + logging = require('./logging'), config = require('../config.json'), utils = require('./utils'); @@ -96,6 +97,7 @@ class CommandHandler { this.prefix = prefix; this.scope = scope; this.commands = {}; + this._logger = new logging.Logger(`${this.constructor.name}@${Object.keys(scopes)[this.scope]}`); } /** @@ -105,9 +107,11 @@ class CommandHandler { * @returns {Boolean | String | Promise} */ 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+/, ''); @@ -117,17 +121,21 @@ class CommandHandler { 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 "You don't have permission for this command"; } else { + this._logger.silly(`Command ${commandName} not found.`); return false; } } else { + this._logger.silly(`No prefix found in command ${commandName}`); return false; } } @@ -139,6 +147,7 @@ class CommandHandler { registerCommand(command) { command.prefix = this.prefix; this.commands[command.name] = command; + this._logger.debug(`Registered ${command.name} on handler`); return this; } @@ -174,6 +183,7 @@ class CommandModule { */ constructor(scope) { this.scope = scope; + this._logger = new logging.Logger(this); } /** @@ -183,6 +193,7 @@ class CommandModule { */ async _loadTemplate(file) { let templateString = await fsx.readFile(this.templateFile || file, {encoding: 'utf-8'}); + this._logger.silly(`Loaded Template file ${this.templateFile || file}`); this.template = yaml.safeLoad(templateString); } diff --git a/lib/MessageLib.js b/lib/MessageLib.js index dd69d54..3642b43 100644 --- a/lib/MessageLib.js +++ b/lib/MessageLib.js @@ -1,6 +1,7 @@ const cmdLib = require('./CommandLib'), config = require('../config.json'), Discord = require('discord.js'), + logging = require('./logging'), promiseWaterfall = require('promise-waterfall'); /* eslint no-useless-escape: 0 */ @@ -11,11 +12,10 @@ 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; + constructor (client) { this.discordClient = client; + this.logger = new logging.Logger(this); this.globalCmdHandler = new cmdLib.CommandHandler(config.prefix, cmdLib.CommandScopes.Global); this.userCmdHandler = new cmdLib.CommandHandler(config.prefix, @@ -70,7 +70,7 @@ class MessageHandler { _registerEvents() { this.logger.debug('Registering message event...'); this.discordClient.on('message', async (msg) => { - this.logger.debug(`<${msg.guild? msg.channel.name+'@'+msg.guild.name : 'PRIVATE'}> ${msg.author.tag}: ${msg.content}`); + this.logger.verbose(`<${msg.guild? msg.channel.name+'@'+msg.guild.name : 'PRIVATE'}> ${msg.author.tag}: ${msg.content}`); if (msg.author !== this.discordClient.user && this._checkPrefixStart(msg.content) && !this._checkRateReached(msg.author)) { @@ -90,7 +90,7 @@ class MessageHandler { * @private */ _parseSyntax(message) { - this.logger.debug('Parsing command sequence...'); + this.logger.silly('Parsing command sequence...'); let commandSequence = []; let content = message.content; let strings = content.match(/".+?"/g) || []; @@ -114,14 +114,14 @@ class MessageHandler { * Executes a sequence of commands */ async executeCommandSequence(cmdSequence, message) { - this.logger.debug(`Executing command sequence: ${JSON.stringify(cmdSequence)} ...`); + this.logger.silly(`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}`); + this.logger.silly(`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}`); + this.logger.silly(`globalResult: ${globalResult}, scopeResult: ${scopeResult}`); if (scopeResult) this._answerMessage(message, scopeResult); diff --git a/lib/MusicLib.js b/lib/MusicLib.js index 7467e9c..d6558da 100644 --- a/lib/MusicLib.js +++ b/lib/MusicLib.js @@ -3,15 +3,9 @@ const ytdl = require("ytdl-core"), yttl = require('get-youtube-title'), config = require('../config.json'), utils = require('./utils.js'), + logging = require('./logging'), ytapiKey = config.api.youTubeApiKey; - -let logger = require('winston'); - -exports.setLogger = function (newLogger) { - logger = newLogger; -}; - /** * The Music Player class is used to handle music playing tasks on Discord Servers (Guilds). * @type {MusicPlayer} @@ -28,6 +22,8 @@ class MusicPlayer { this.voiceChannel = voiceChannel; this.quality = 'lowest'; this.exitTimeout = null; + this._logger = new logging.Logger(this); + this._logger.silly('Initialized Music Player'); } /** @@ -45,9 +41,9 @@ class MusicPlayer { } else if (voiceChannel) this.voiceChannel = voiceChannel; - logger.verbose(`Connecting to voiceChannel ${this.voiceChannel.name}`); + this._logger.verbose(`Connecting to voiceChannel ${this.voiceChannel.name}`); let connection = await this.voiceChannel.join(); - logger.info(`Connected to Voicechannel ${this.voiceChannel.name}`); + this._logger.info(`Connected to Voicechannel ${this.voiceChannel.name}`); this.conn = connection; } @@ -59,7 +55,6 @@ class MusicPlayer { this.repeat = value; if (this.current) this.queue.push(this.current); - } /** @@ -81,7 +76,7 @@ class MusicPlayer { updateChannel(voiceChannel) { if (voiceChannel) { this.voiceChannel = voiceChannel; - logger.debug(`Updated voiceChannel to ${this.voiceChannel.name}`); + this._logger.debug(`Updated voiceChannel to ${this.voiceChannel.name}`); } } @@ -96,7 +91,7 @@ class MusicPlayer { this.disp = this.conn.playFile(filename); this.playing = true; } else { - logger.warn("Not connected to a voicechannel. Connection now."); + this._logger.warn("Not connected to a voicechannel. Connection now."); this.connect(this.voiceChannel).then(() => { this.playFile(filename); }); @@ -111,13 +106,13 @@ class MusicPlayer { if (this.exitTimeout) { clearTimeout(this.exitTimeout); this.exitTimeout = null; - logger.debug(`Cleared exit timout for ${this.voiceChannel.name}`); + this._logger.debug(`Cleared exit timout for ${this.voiceChannel.name}`); } if (this.connected && this.voiceChannel.members.size === 1) { - logger.debug(`Set exit timout for ${this.voiceChannel.name}`); + this._logger.debug(`Set exit timout for ${this.voiceChannel.name}`); this.exitTimeout = setTimeout(() => { if (this.connected && this.voiceChannel.members.size === 1) - logger.verbose(`Exiting ${this.voiceChannel.name}`); + this._logger.verbose(`Exiting ${this.voiceChannel.name}`); this.stop(); }, config.music.timeout || 300000); } @@ -133,7 +128,7 @@ class MusicPlayer { async playYouTube(url, playnext) { let plist = utils.YouTube.getPlaylistIdFromUrl(url); if (plist) { - logger.debug(`Adding playlist ${plist} to queue`); + this._logger.debug(`Adding playlist ${plist} to queue`); let playlistItems = await ypi(ytapiKey, plist); let firstSong = utils.YouTube.getVideoUrlFromId(playlistItems.shift().resourceId.videoId); let firstSongTitle = null; @@ -141,14 +136,14 @@ class MusicPlayer { firstSongTitle = await this.getVideoName(firstSong); } catch(err) { if (err.message !== 'Not found') { - logger.warn(err.message); - logger.debug(err.stack); + this._logger.warn(err.message); + this._logger.debug(err.stack); } } if (this.repeat) this.queue.push({'url': firstSong, 'title': firstSongTitle}); - this.playYouTube(firstSong).catch((err) => logger.warn(err.message)); + this.playYouTube(firstSong).catch((err) => this._logger.warn(err.message)); for (let item of playlistItems) { let vurl = utils.YouTube.getVideoUrlFromId(item.resourceId.videoId); @@ -156,14 +151,14 @@ class MusicPlayer { this.queue.push({'url': vurl, 'title': await this.getVideoName(vurl)}); //eslint-disable-line no-await-in-loop } catch (err) { if (err.message !== 'Not found') { - logger.warn(err.message); - logger.debug(err.stack); + this._logger.warn(err.message); + this._logger.debug(err.stack); } } } - logger.debug(`Added ${playlistItems.length} songs to the queue`); + this._logger.debug(`Added ${playlistItems.length} songs to the queue`); } else if (!this.playing || !this.disp) { - logger.debug(`Playing ${url}`); + this._logger.debug(`Playing ${url}`); this.current = ({'url': url, 'title': await this.getVideoName(url)}); this.disp = this.conn.playStream(ytdl(url, @@ -178,7 +173,7 @@ class MusicPlayer { this.current = this.queue.shift(); if (this.repeat) // listen on repeat this.queue.push(this.current); - this.playYouTube(this.current.url).catch((err) => logger.warn(err.message)); + this.playYouTube(this.current.url).catch((err) => this._logger.warn(err.message)); } else { this.stop(); } @@ -186,7 +181,7 @@ class MusicPlayer { }); this.playing = true; } else { - logger.debug(`Added ${url} to the queue`); + this._logger.debug(`Added ${url} to the queue`); if (playnext) this.queue.unshift({'url': url, 'title': await this.getVideoName(url)}); else @@ -204,7 +199,7 @@ class MusicPlayer { return new Promise((resolve, reject) => { yttl(utils.YouTube.getVideoIdFromUrl(url), (err, title) => { if (err) { - logger.debug(JSON.stringify(err)); + this._logger.debug(JSON.stringify(err)); reject(err); } else { resolve(title); @@ -218,12 +213,12 @@ class MusicPlayer { * @param percentage {Number} */ setVolume(percentage) { - logger.verbose(`Setting volume to ${percentage}`); + this._logger.verbose(`Setting volume to ${percentage}`); if (this.disp !== null) { this.volume = percentage; this.disp.setVolume(percentage); } else { - logger.warn("No dispatcher found."); + this._logger.warn("No dispatcher found."); } } @@ -231,11 +226,11 @@ class MusicPlayer { * Pauses if a dispatcher exists */ pause() { - logger.verbose("Pausing music..."); + this._logger.verbose("Pausing music..."); if (this.disp !== null) this.disp.pause(); else - logger.warn("No dispatcher found"); + this._logger.warn("No dispatcher found"); } @@ -243,11 +238,11 @@ class MusicPlayer { * Resumes if a dispatcher exists */ resume() { - logger.verbose("Resuming music..."); + this._logger.verbose("Resuming music..."); if (this.disp !== null) this.disp.resume(); else - logger.warn("No dispatcher found"); + this._logger.warn("No dispatcher found"); } @@ -259,25 +254,25 @@ class MusicPlayer { this.playing = false; this.queue = []; this.current = null; - logger.verbose("Stopping music..."); + this._logger.verbose("Stopping music..."); try { if (this.disp) { this.disp.end('stop'); this.disp = null; - logger.debug("Ended dispatcher"); + this._logger.debug("Ended dispatcher"); } if (this.conn) { this.conn.disconnect(); this.conn = null; - logger.debug("Ended connection"); + this._logger.debug("Ended connection"); } if (this.voiceChannel) { this.voiceChannel.leave(); - logger.debug("Left VoiceChannel"); - logger.info(`Disconnected from Voicechannel ${this.voiceChannel.name}`); + this._logger.debug("Left VoiceChannel"); + this._logger.info(`Disconnected from Voicechannel ${this.voiceChannel.name}`); } } catch (error) { - logger.verbose(JSON.stringify(error)); + this._logger.verbose(JSON.stringify(error)); } } @@ -287,7 +282,7 @@ class MusicPlayer { * It tries to play the next song with playYouTube */ skip() { - logger.debug("Skipping song"); + this._logger.debug("Skipping song"); if (this.disp !== null) { this.disp.end(); } else { @@ -295,8 +290,8 @@ class MusicPlayer { if (this.queue.length > 0) { this.current = this.queue.shift(); this.playYouTube(this.current.url).catch((err) => { - logger.error(err.message); - logger.debug(err.stack); + this._logger.error(err.message); + this._logger.debug(err.stack); }); } else { this.stop(); diff --git a/lib/WebLib.js b/lib/WebLib.js index 2949816..3134530 100644 --- a/lib/WebLib.js +++ b/lib/WebLib.js @@ -4,6 +4,7 @@ const express = require('express'), compression = require('compression'), md5 = require('js-md5'), sha512 = require('js-sha512'), + logging = require('./logging'), fs = require('fs'), session = require('express-session'), SQLiteStore = require('connect-sqlite3')(session), @@ -12,12 +13,6 @@ const express = require('express'), config = require('../config.json'), utils = require('../lib/utils'); -let logger = require('winston'); - -exports.setLogger = function (newLogger) { - logger = newLogger; -}; - exports.WebServer = class { constructor(port) { this.app = express(); @@ -25,6 +20,7 @@ exports.WebServer = class { this.port = port; this.schema = buildSchema(fs.readFileSync('./lib/api/graphql/schema.gql', 'utf-8')); this.root = {}; + this._logger = new logging.Logger(this); } /** @@ -68,7 +64,7 @@ exports.WebServer = class { } else { let user = await this.maindb.get('SELECT * FROM users WHERE username = ? AND password = ?', [req.body.username, req.body.password]); if (!user) { - logger.debug(`User ${req.body.username} failed to authenticate`); + this._logger.debug(`User ${req.body.username} failed to authenticate`); res.render('login', {msg: 'Login failed!'}); } else { req.session.user = user; @@ -107,10 +103,10 @@ exports.WebServer = class { if (config.webinterface.https.certFile) sslCert = fs.readFileSync(config.webinterface.https.certFile, 'utf-8'); if (sslKey && sslCert) { - logger.verbose('Creating https server.'); + this._logger.verbose('Creating https server.'); this.server = require('https').createServer({key: sslKey, cert: sslCert}, this.app); } else { - logger.warn('Key or certificate file not found. Fallback to http server.'); + this._logger.warn('Key or certificate file not found. Fallback to http server.'); this.server = require('http').createServer(this.app); } } else { @@ -175,7 +171,7 @@ exports.WebServer = class { .slice(args.offset, args.offset + args.first) .map(async (x) => new Guild(x, await objects.getGuildHandler(x)))); } catch (err) { - logger.error(err.stack); + this._logger.error(err.stack); return null; } diff --git a/lib/commands/AnilistApiCommands/index.js b/lib/commands/AnilistApiCommands/index.js index b6dc426..6ee5628 100644 --- a/lib/commands/AnilistApiCommands/index.js +++ b/lib/commands/AnilistApiCommands/index.js @@ -59,8 +59,11 @@ class AniListCommandModule extends cmdLib.CommandModule { new cmdLib.Answer(async (m, k, s) => { try { let animeData = await anilistApi.searchAnimeByName(s); + this._logger.silly(`Anime Query returned ${JSON.stringify(animeData)}`); return new RichMediaInfo(animeData); } catch (err) { + if (err.message) + this._logger.verbose(err.message); return this.template.anime_search.not_found; } })); @@ -70,8 +73,11 @@ class AniListCommandModule extends cmdLib.CommandModule { new cmdLib.Answer(async (m, k, s) => { try { let mangaData = await anilistApi.searchMangaByName(s); + this._logger.silly(`Manga Query returned ${JSON.stringify(mangaData)}`); return new RichMediaInfo(mangaData); } catch (err) { + if (err.message) + this._logger.verbose(err.message); return this.template.manga_search.not_found; } }) diff --git a/lib/commands/InfoCommands/index.js b/lib/commands/InfoCommands/index.js index d243897..e2cc39b 100644 --- a/lib/commands/InfoCommands/index.js +++ b/lib/commands/InfoCommands/index.js @@ -39,6 +39,7 @@ class InfoCommandModule extends cmdLib.CommandModule { helpEmbed.addField(cat, catCommands[cat]); helpEmbed.setFooter(prefix + 'help [command] for more info to each command'); + this._logger.silly('Created help embed'); return helpEmbed; } diff --git a/lib/commands/MusicCommands/index.js b/lib/commands/MusicCommands/index.js index c667706..d368d87 100644 --- a/lib/commands/MusicCommands/index.js +++ b/lib/commands/MusicCommands/index.js @@ -11,13 +11,11 @@ class MusicCommandModule extends cmdLib.CommandModule { /** * @param opts {Object} properties: * getGuildHandler - a function to get the guild handler for a guild. - * logger - the logger instance */ constructor(opts) { super(cmdLib.CommandScopes.Guild); this.templateFile = location + '/MusicCommandsTemplate.yaml'; this._getGuildHandler = opts.getGuildHandler; - this._logger = opts.logger; } /** diff --git a/lib/commands/ServerUtilityCommands/index.js b/lib/commands/ServerUtilityCommands/index.js index 2198c5c..92dcb8c 100644 --- a/lib/commands/ServerUtilityCommands/index.js +++ b/lib/commands/ServerUtilityCommands/index.js @@ -10,7 +10,6 @@ class ServerUtilityCommandModule extends cmdLib.CommandModule { * @param opts {Object} properties: * getGuildHandler - a function to get the guild handler for the guild * messagehandler - the MessageHandler instance - * logger - the instance of the logger. * config - the config object */ constructor(opts) { @@ -18,7 +17,6 @@ class ServerUtilityCommandModule extends cmdLib.CommandModule { this.templateFile = location + '/ServerUtilityCommandsTemplate.yaml'; this._messageHandler = opts.messageHandler; this._getGuildHandler = opts.getGuildHandler; - this._logger = opts.logger; this._config = opts.config; } diff --git a/lib/commands/UtilityCommands/index.js b/lib/commands/UtilityCommands/index.js index d7e1a6a..49167e4 100644 --- a/lib/commands/UtilityCommands/index.js +++ b/lib/commands/UtilityCommands/index.js @@ -14,14 +14,12 @@ class UtilityCommandModule extends cmdLib.CommandModule { /** * @param opts {Object} properties: * bot - the instance of the bot. - * logger - the instance of the logger. * config - the config object */ constructor(opts) { super(cmdLib.CommandScopes.User); this.templateFile = location + '/UtilityCommandsTemplate.yaml'; this._bot = opts.bot; - this._logger = opts.logger; this._config = opts.config; } diff --git a/lib/guilding.js b/lib/guilding.js index ed5bb75..e2276fc 100644 --- a/lib/guilding.js +++ b/lib/guilding.js @@ -2,16 +2,10 @@ const music = require('./MusicLib'), utils = require('./utils'), config = require('../config.json'), sqliteAsync = require('./sqliteAsync'), + logging = require('./logging'), fs = require('fs-extra'), dataDir = config.dataPath || './data'; -let logger = require('winston'); - -exports.setLogger = function (newLogger) { - logger = newLogger; - music.setLogger(logger); -}; - /** * The Guild Handler handles guild settings and data. * @type {GuildHandler} @@ -20,7 +14,9 @@ class GuildHandler { constructor(guild) { this.guild = guild; + this._logger = new logging.Logger(`${this.constructor.name}@${this.guild}`); this.musicPlayer = new music.MusicPlayer(null); + this._logger.silly('Initialized Guild Handler'); } /** @@ -28,10 +24,12 @@ class GuildHandler { * @returns {Promise} */ async initDatabase() { + this._logger.silly('Initializing Database'); await fs.ensureDir(dataDir + '/gdb'); this.db = new sqliteAsync.Database(`${dataDir}/gdb/${this.guild}.db`); await this.db.init(); - logger.debug(`Connected to the database for ${this.guild}`); + this._logger.debug(`Connected to the database for ${this.guild}`); + this._logger.debug('Creating Databases'); await this.createTables(); } @@ -39,7 +37,9 @@ class GuildHandler { * Destroys the guild handler */ destroy() { + this._logger.debug('Ending musicPlayer'); this.musicPlayer.stop(); + this._logger.debug('Ending Database'); this.db.close(); } @@ -57,16 +57,19 @@ class GuildHandler { author_name VARCHAR(128), content TEXT NOT NULL )`); + this._logger.silly('Created Table messages'); await this.db.run(`${utils.sql.tableExistCreate} playlists ( ${utils.sql.pkIdSerial}, name VARCHAR(32) UNIQUE NOT NULL, url VARCHAR(255) NOT NULL )`); + this._logger.silly('Created Table playlists'); await this.db.run(`${utils.sql.tableExistCreate} commands ( ${utils.sql.pkIdSerial}, name VARCHAR(32) UNIQUE NOT NULL, command VARCHAR(255) NOT NULL )`); + this._logger.silly('Created Table commands'); } } diff --git a/lib/logging.js b/lib/logging.js index 8d8caa4..98166ef 100644 --- a/lib/logging.js +++ b/lib/logging.js @@ -1,66 +1,122 @@ /* eslint-disable no-unused-vars */ const winston = require('winston'), DailyRotateFile = require('winston-daily-rotate-file'), - args = require('args-parser')(process.argv), + args = require('args-parser')(process.argv); - fileLoggingFormat = winston.format.printf(info => { - return `${info.timestamp} ${info.level.toUpperCase()}: ${JSON.stringify(info.message)}`; // the logging format for files - }), - consoleLoggingFormat = winston.format.printf(info => { - return `${info.timestamp} {${info.label}} [${info.level}] ${JSON.stringify(info.message)}`; //the logging format for the console +/** + * Set console format to simple string format + * @type {Format} + */ +const consoleLoggingFormat = winston.format.printf(info => { + return `${info.timestamp} {${info.module || info.m || 'DEFAULT'}} [${info.level}] ${JSON.stringify(info.message)}`; //the logging format for the console +}); + +/** + * Set full format to combination of formats + * @type {Format} + */ +const loggingFullFormat = winston.format.combine( + winston.format.timestamp({ + format: 'YY-MM-DD HH:mm:ss.SSS' }), - 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() - ); -let logger = winston.createLogger({ - level: winston.config.npm.levels, // logs with npm levels - format: loggingFullFormat, - transports: [ - new winston.transports.Console({ - format: winston.format.combine( - winston.format.colorize(), - winston.format.splat(), - winston.format.timestamp({ - format: 'YY-MM-DD HH:mm:ss.SSS' - }), - winston.format.label({label: ''}), - consoleLoggingFormat - ), - level: args.loglevel || 'info' - }), - new winston.transports.File({ - level: 'debug', - filename: './.log/latest.log', - options: {flags: 'w'} // overwrites the file on restart + winston.format.json() +); +/** + * Define all transports used. + * @type {any[]} + */ +let transports = [ + new winston.transports.Console({ + format: winston.format.combine( + winston.format.colorize(), + winston.format.splat(), + winston.format.timestamp({ + format: 'YY-MM-DD HH:mm:ss.SSS' }), - new DailyRotateFile({ - level: 'verbose', - filename: './.log/%DATE%.log', - datePattern: 'YYYY-MM-DD', - zippedArchive: true, - maxSize: '32m', - maxFiles: '30d', - json: true - }) - ] - }); - -//class SpecialLogger extends winston. + winston.format.label({label: ''}), + consoleLoggingFormat + ), + level: args.loglevel || 'info' + }), + new winston.transports.File({ + level: 'debug', + filename: './.log/latest.log', + options: {flags: 'w'} // overwrites the file on restart + }), + new DailyRotateFile({ + level: 'verbose', + filename: './.log/%DATE%.log', + datePattern: 'YYYY-MM-DD', + zippedArchive: true, + maxSize: '32m', + maxFiles: '30d', + json: true + }) +]; /** - * A function to return the logger that has been created after appending an exception handler - * @returns {Object} + * Define the logger + * @type {winston.Logger} */ -exports.getLogger = function () { - logger.exceptions.handle( - new winston.transports.File({ - filename: './.log/exceptions.log' - }) - ); - return logger; -}; +let logger = winston.createLogger({ + level: winston.config.npm.levels, + format: loggingFullFormat, + transports: transports +}); + + +// Define exception handling +logger.exceptions.handle( + new winston.transports.File({ + filename: './.log/exceptions.log' + }) +); + +class ModuleLogger { + + constructor(moduleInstance) { + this.logger = logger; + if (moduleInstance.constructor) { + switch (moduleInstance.constructor.name) { + case 'String': + this.logName = moduleInstance; + break; + case 'Number': + this.logName = moduleInstance.toString(); + break; + default: + this.logName = moduleInstance.constructor.name; + } + } else { + this.logName = moduleInstance.toString(); + } + } + + silly(msg, meta) { + logger.silly(msg, {m: this.logName, ...meta}); + } + + debug(msg, meta) { + logger.debug(msg, {m: this.logName, ...meta}); + } + + verbose(msg, meta) { + logger.verbose(msg, {m: this.logName, ...meta}); + } + + info(msg, meta) { + logger.info(msg, {m: this.logName, ...meta}); + } + warn(msg, meta) { + logger.warn(msg, {m: this.logName, ...meta}); + } + + error(msg, meta) { + logger.error(msg, {m: this.logName, ...meta}); + } +} + +Object.assign(exports, { + logger: logger, + Logger: ModuleLogger +}); diff --git a/lib/utils.js b/lib/utils.js index 747af42..620689c 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -11,7 +11,7 @@ function noOp() { * @param {String} filename The name of the file. * @return {String} A string that represents the file-extension. */ -exports.getExtension = function (filename) { +function getFileExtension (filename) { if (!filename) return null; try { @@ -24,7 +24,7 @@ exports.getExtension = function (filename) { console.error(error); return null; } -}; +} /** * Walks the path to the objects attribute and returns the value. @@ -32,7 +32,7 @@ exports.getExtension = function (filename) { * @param attributePath * @returns {undefined/Object} */ -exports.objectDeepFind = function (object, attributePath) { +function objectDeepFind (object, attributePath) { let current = object, paths = attributePath.split('.'); for (let path of paths) @@ -42,7 +42,7 @@ exports.objectDeepFind = function (object, attributePath) { return undefined; return current; -}; +} /** * Shuffles an array with Fisher-Yates Shuffle @@ -74,7 +74,7 @@ exports.shuffleArray = function(array) { * @constructor * @author CanyonCasa & Pier-Luc Gendreau on StackOverflow */ -exports.Cleanup = function Cleanup(callback) { +function Cleanup(callback) { // attach user callback to the process event emitter // if no callback, it will still exit gracefully on Ctrl-C @@ -98,9 +98,9 @@ exports.Cleanup = function Cleanup(callback) { console.log(e.stack); process.exit(99); }); -}; +} -exports.getSplitDuration = function (duration) { +function getSplitDuration (duration) { let dur = duration; let retObj = {}; retObj.milliseconds = dur % 1000; @@ -113,23 +113,23 @@ exports.getSplitDuration = function (duration) { dur = Math.floor(dur / 24); retObj.days = dur; return retObj; -}; +} /** * Resolves a nested promise by resolving it iterative. * @param promise * @returns {Promise<*>} */ -exports.resolveNestedPromise = async function(promise) { +async function resolveNestedPromise (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 { +class YouTube { /** * returns if an url is a valid youtube url (without checking for an entity id) * @param url @@ -220,9 +220,9 @@ exports.YouTube = class { let id = exports.YouTube.getVideoIdFromUrl(url); return id? `https://i3.ytimg.com/vi/${id}/maxresdefault.jpg` : null; } -}; +} -exports.ConfigVerifyer = class { +class ConfigVerifyer { /** * @param confObj * @param required {Array} the attributes that are required for the bot to work @@ -255,7 +255,7 @@ exports.ConfigVerifyer = class { logger.error(`Missing required Attributes ${this.missingAttributes.join(', ')}`); } -}; +} exports.sql = { tableExistCreate: 'CREATE TABLE IF NOT EXISTS', @@ -270,3 +270,14 @@ exports.logLevels = { 'warn': 3, 'error:': 4 }; + +Object.assign(exports, { + resolveNestedPromise: resolveNestedPromise, + YouTube: YouTube, + ConfigVerifyer: ConfigVerifyer, + getSplitDuration: getSplitDuration, + getExtension: getFileExtension, + getFileExtension: getFileExtension, + objectDeepFind: objectDeepFind, + Cleanup: Cleanup +}); diff --git a/test/test.js b/test/test.js index 0b18266..5eb5b33 100644 --- a/test/test.js +++ b/test/test.js @@ -3,7 +3,6 @@ const mockobjects = require('./mockobjects.js'), sinon = require('sinon'), assert = require('assert'), rewire = require('rewire'); -let Discord = require("discord.js"); mockobjects.mockLogger = { error: () => {}, @@ -180,7 +179,7 @@ describe('lib/utils', function() { describe('lib/music', function() { - const music = rewire('../lib/music'); + const music = rewire('../lib/MusicLib'); const Readable = require('stream').Readable; music.__set__("logger", mockobjects.mockLogger); @@ -204,7 +203,7 @@ describe('lib/music', function() { describe('#MusicPlayer', function () { it('connects to a VoiceChannel', function (done) { - let dj = new music.DJ(mockobjects.mockVoicechannel); + let dj = new music.MusicPlayer(mockobjects.mockVoicechannel); dj.connect().then(()=> { assert(dj.connected); done(); @@ -212,7 +211,7 @@ describe('lib/music', function() { }); it('listens on Repeat', function () { - let dj = new music.DJ(mockobjects.mockVoicechannel); + let dj = new music.MusicPlayer(mockobjects.mockVoicechannel); dj.current = {'url': '', 'title': ''}; dj.listenOnRepeat = true; @@ -222,7 +221,7 @@ describe('lib/music', function() { it('plays Files', function (done) { - let dj = new music.DJ(mockobjects.mockVoicechannel); + let dj = new music.MusicPlayer(mockobjects.mockVoicechannel); dj.connect().then(() => { dj.playFile(); assert(dj.playing); @@ -231,7 +230,7 @@ describe('lib/music', function() { }); it('plays YouTube urls', function (done) { - let dj = new music.DJ(mockobjects.mockVoicechannel); + let dj = new music.MusicPlayer(mockobjects.mockVoicechannel); dj.connect().then(() => { dj.playYouTube('http://www.youtube.com/watch?v=ABCDEFGHIJK'); setTimeout(() => { @@ -242,7 +241,7 @@ describe('lib/music', function() { }); it('gets the video name', function (done) { - let dj = new music.DJ(mockobjects.mockVoicechannel); + let dj = new music.MusicPlayer(mockobjects.mockVoicechannel); dj.getVideoName('http://www.youtube.com/watch?v=ABCDEFGHIJK').then((name) => { assert(name === 'test'); done(); @@ -250,7 +249,7 @@ describe('lib/music', function() { }); it('sets the volume', function(done) { - let dj = new music.DJ(mockobjects.mockVoicechannel); + let dj = new music.MusicPlayer(mockobjects.mockVoicechannel); dj.connect().then(() => { dj.playFile(); dj.setVolume(100); @@ -260,7 +259,7 @@ describe('lib/music', function() { }); it('pauses playback', function(done) { - let dj = new music.DJ(mockobjects.mockVoicechannel); + let dj = new music.MusicPlayer(mockobjects.mockVoicechannel); dj.connect().then(() => { dj.playFile(); dj.pause(); @@ -269,7 +268,7 @@ describe('lib/music', function() { }); it('resumes playback', function(done) { - let dj = new music.DJ(mockobjects.mockVoicechannel); + let dj = new music.MusicPlayer(mockobjects.mockVoicechannel); dj.connect().then(() => { dj.playFile(); dj.resume(); @@ -278,7 +277,7 @@ describe('lib/music', function() { }); it('stops playback', function(done) { - let dj = new music.DJ(mockobjects.mockVoicechannel); + let dj = new music.MusicPlayer(mockobjects.mockVoicechannel); dj.connect().then(() => { dj.playFile(); assert(dj.playing); @@ -289,7 +288,7 @@ describe('lib/music', function() { }); it('skips songs', function(done) { - let dj = new music.DJ(mockobjects.mockVoicechannel); + let dj = new music.MusicPlayer(mockobjects.mockVoicechannel); dj.connect().then(() => { dj.playYouTube('http://www.youtube.com/watch?v=ABCDEFGHIJK'); dj.playYouTube('http://www.youtube.com/watch?v=ABCDEFGHIJK'); @@ -302,7 +301,7 @@ describe('lib/music', function() { }); it('returns a playlist', function(done) { - let dj = new music.DJ(mockobjects.mockVoicechannel); + let dj = new music.MusicPlayer(mockobjects.mockVoicechannel); dj.connect().then(() => { dj.queue = [{ 'title': 'title', @@ -315,7 +314,7 @@ describe('lib/music', function() { }); it('clears the queue', function(done) { - let dj = new music.DJ(mockobjects.mockVoicechannel); + let dj = new music.MusicPlayer(mockobjects.mockVoicechannel); dj.connect().then(() => { dj.queue = [{ 'title': 'title', @@ -330,163 +329,63 @@ describe('lib/music', function() { }); }); -describe('lib/cmd', function() { - const cmd = rewire('../lib/cmd'); - cmd.__set__("logger", mockobjects.mockLogger); +describe('lib/CommandLib', function() { + let cmdLib = require('../lib/CommandLib'); - describe('#Servant', function() { + describe('Answer', function() { - it('creates commands', function() { - let servant = new cmd.Servant(''); - servant.createCommand(mockobjects.mockCommand, mockobjects.mockCommand.textReply); - assert(servant.commands['test']); - servant.createCommand(mockobjects.mockCommand, mockobjects.mockCommand.promiseReply); - assert(servant.commands['test']); - servant.createCommand(mockobjects.mockCommand, mockobjects.mockCommand.richEmbedReply); - assert(servant.commands['test']); + it('evaluates synchronous', async function() { + let answer = new cmdLib.Answer(() => 'RESPONSE'); + assert((await answer.evaluate({}, {}, {})) === 'RESPONSE'); }); - it('removes commands', function() { - let servant = new cmd.Servant(''); - servant.createCommand(mockobjects.mockCommand, mockobjects.mockCommand.textReply); - assert(servant.commands['test']); - servant.removeCommand('test'); - assert(!servant.commands['test']); - }); - - it('parses commands', function() { - let spy = sinon.spy(); - let servant = new cmd.Servant(''); - servant.createCommand(mockobjects.mockCommand, spy); - assert(servant.commands['test']); - assert(!spy.called); - servant.parseCommand({ - content: 'test', - author: { - tag: undefined - } + it('evaluates asynchronous', async function() { + let answer = new cmdLib.Answer(async () => { + return 'RESPONSE'; }); - assert(spy.called); - }); + assert((await answer.evaluate({}, {}, {})) === 'RESPONSE'); + }) }); -}); -describe('lib/guilding', function*() { // deactivated because of problems with sqlite3 and rewire - const guilding = rewire('../lib/guilding'); - const servercommands = require('../commands/servercommands'); - guilding.__set__("sqliteAsync", null); - guilding.__set__("fs-extra", { - ensureDir: async() => { - return true; - } + describe('Command', function() { + + it('answers with Answer objects', async function() { + let cmd = new cmdLib.Command({ + name: 'TEST', + prefix: '', + description: 'TESTDESCRIPTION', + permission: 'TESTPERM', + usage: 'TESTUSAGE' + },new cmdLib.Answer(() => 'RESPONSE')); + assert((await cmd.answer({}, {}, {})) === 'RESPONSE'); + }); + + it('generates help for itself', function() { + let cmd = new cmdLib.Command({ + name: 'TEST', + prefix: '', + description: 'TESTDESCRIPTION', + permission: 'TESTPERM', + usage: 'TESTUSAGE' + },new cmdLib.Answer(() => 'RESPONSE')); + assert(cmd.help); + }) }); - guilding.setLogger(mockobjects.mockLogger); - - describe('#GuildHandler', function() { - - it('initializes', function() { - let gh = new guilding.GuildHandler('test', ''); - gh.db = new mockobjects.MockDatabase('', ()=>{}); - gh.createTables(); - gh.registerMusicCommands(); - gh.ready = true; - assert(gh.ready); - }); - - it('destroyes itself', function() { - let gh = new guilding.GuildHandler('test', ''); - gh.db = new mockobjects.MockDatabase('', ()=>{}); - gh.createTables(); - gh.registerMusicCommands(); - gh.ready = true; - gh.destroy(); - assert(!gh.dj.conn); - }); - - it('answers messages', function() { - let gh = new guilding.GuildHandler('test', ''); - gh.db = new mockobjects.MockDatabase('', ()=>{}); - gh.createTables(); - gh.registerMusicCommands(); - gh.ready = true; - let msgSpy = sinon.spy(); - gh.answerMessage({ - content: 'test', - author: { - tag: undefined - }, - reply: msgSpy, - channel: { - send: msgSpy - } - }, 'Answer'); - assert(msgSpy.called); - }); - - it('handles messages', function() { - let gh = new guilding.GuildHandler('test', '~'); - gh.db = new mockobjects.MockDatabase('', ()=>{}); - gh.ready = true; - let cbSpy = sinon.spy(); - gh.servant.createCommand(mockobjects.mockCommand, cbSpy); - assert(gh.servant.commands['~test']); - gh.handleMessage({ - content: '~test', - author: { - tag: undefined - }}); - assert(cbSpy.called); - }); - - it('connects and plays', function(done) { - const music = rewire('../lib/music'); - const Readable = require('stream').Readable; - - music.__set__("logger", mockobjects.mockLogger); - music.__set__("yttl", (id, cb) => { - cb(null, 'test'); - }); - music.__set__('ytdl', () => { - let s = new Readable(); - s._read = () => {}; - s.push('chunkofdataabc'); - s.push(null); - return s; - }); - let gh = new guilding.GuildHandler('test', '~'); - gh.db = new mockobjects.MockDatabase('', ()=>{}); - gh.ready = true; - gh.musicPlayer = new music.DJ(mockobjects.mockVoicechannel); - gh.connectAndPlay(mockobjects.mockVoicechannel, 'test', false).then(() => { - done(); - }); - }); - - it('handles all servercommands', function() { - let gh = new guilding.GuildHandler('test', '~'); - gh.db = new mockobjects.MockDatabase('', ()=>{}); - gh.registerMusicCommands(); - gh.ready = true; - let msgSpy = sinon.spy(); - let msg = { - content: 'test', - author: { - tag: undefined - }, - reply: msgSpy, - channel: { - send: msgSpy - } - }; - - for (let category of Object.keys(servercommands)) - for (let command of Object.keys(servercommands[category])) { - msg.content = '~' + command; - gh.handleMessage(msg); - } - +}); - assert(msgSpy.called); - }); +describe('lib/MessageLib', function() { + let msgLib = require('../lib/MessageLib'); + + describe('MessageHandler', function() { + it ('parses a command syntax', function() { + let msgHandler = new msgLib.MessageHandler({ + on: () => {} + }); + let parsedSyntax = msgHandler.parseSyntaxString('_help cmd&& _ping; _uptime'); + assert(parsedSyntax.length === 2); + assert(parsedSyntax[0].length === 2); + assert(parsedSyntax[1].length === 1); + }); }); + }); From ff9d78566b2932e3836d25f9a09edfdf0c2a4511 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sun, 3 Mar 2019 01:02:13 +0100 Subject: [PATCH 21/26] Implemented basic AniList api features - changed anilist query files - added autocropping to ExtendedRichEmbed - updated Readme --- README.md | 13 +- lib/CommandLib.js | 25 +- lib/WebLib.js | 4 +- lib/api/AnilistApi.js | 138 +++++++--- lib/api/graphql/AnilistApi/AnimeById.gql | 47 ---- lib/api/graphql/AnilistApi/AnimeQuery.gql | 22 ++ lib/api/graphql/AnilistApi/CharacterQuery.gql | 27 ++ .../{MangaById.gql => Fragments.yaml} | 60 +++-- lib/api/graphql/AnilistApi/MangaQuery.gql | 9 + .../graphql/AnilistApi/MediaSearchByName.gql | 11 - lib/api/graphql/AnilistApi/StaffQuery.gql | 57 +++++ .../AniListCommandsTemplate.yaml | 49 +++- lib/commands/AnilistApiCommands/index.js | 235 ++++++++++++++++-- web/http/scripts/query.js | 1 - 14 files changed, 552 insertions(+), 146 deletions(-) delete mode 100644 lib/api/graphql/AnilistApi/AnimeById.gql create mode 100644 lib/api/graphql/AnilistApi/AnimeQuery.gql create mode 100644 lib/api/graphql/AnilistApi/CharacterQuery.gql rename lib/api/graphql/AnilistApi/{MangaById.gql => Fragments.yaml} (55%) create mode 100644 lib/api/graphql/AnilistApi/MangaQuery.gql delete mode 100644 lib/api/graphql/AnilistApi/MediaSearchByName.gql create mode 100644 lib/api/graphql/AnilistApi/StaffQuery.gql diff --git a/README.md b/README.md index 47a751c..90a933f 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ At the moment the bot can... - [x] ...log stuff in a database - [x] ...execute multiple commands as a sequence - [x] ...save command sequences with a given name +- [x] ...query AniList - [ ] ...transform into a cow Presences @@ -76,15 +77,19 @@ Command Sequences A command sequence is a single message with several commands seperated by a semicolon. In a sequence the command can be ommitted if it is the same as the previous one. That means you can add several videos to the queue and shuffle it afterwards with the sequence - `~play [video1]; [video2]; [video3]; ~shuffle`. + `~play [video1] && ~play [video2]; ~play [video3] && ~shuffle`. - A command sequence can be saved with `~savecmd [sequence] [commandname]`. - In this case the semicolon must be escaped with a backslash so it won't get interpreted as a seperate command. + A command sequence can be saved with `~savecmd [commandname] [sequence]`. + In this case the semicolon must be escaped with a backslash so it won't get interpreted as a seperate command. You can also escape sequences with `~play "whatever &&; you want"` (doublequotes). Command sequences with `&&` are executed in serial while command sequences with `;` are executed in parallel. A saved command can be executed with `~execute [commandname]`. +References +--- + +You can test a running version of the bot. [Invite bot server](https://discordapp.com/oauth2/authorize?client_id=374703138575351809&scope=bot&permissions=1983380544) + Ideas --- - command replies saved in file (server specific file and global file) - reddit api -- anilist api - othercoolstuff api diff --git a/lib/CommandLib.js b/lib/CommandLib.js index 01674e2..7cf44c3 100644 --- a/lib/CommandLib.js +++ b/lib/CommandLib.js @@ -226,7 +226,7 @@ class ExtendedRichEmbed extends Discord.RichEmbed { * @returns {ExtendedRichEmbed} */ addNonemptyField(name, content) { - if (name && name.length > 0 && content) + if (name && name.length > 0 && content && content.length > 0) this.addField(name, content); return this; } @@ -241,6 +241,29 @@ class ExtendedRichEmbed extends Discord.RichEmbed { this.addNonemptyField(name, value); return this; } + + /** + * Sets the description by shortening the value string to a fitting length for discord. + * @param value + */ + setDescription(value) { + let croppedValue = value.substring(0, 1024); + if (croppedValue.length < value.length) + croppedValue = croppedValue.replace(/\n.*$/g, ''); + super.setDescription(croppedValue); + } + + /** + * Sets the field by shortening the value stirn to a fitting length for discord. + * @param name + * @param value + */ + addField(name, value) { + let croppedValue = value.substring(0, 1024); + if (croppedValue.length < value.length) + croppedValue = croppedValue.replace(/\n.*$/g, ''); + super.addField(name, croppedValue); + } } // -- exports -- // diff --git a/lib/WebLib.js b/lib/WebLib.js index 3134530..1713438 100644 --- a/lib/WebLib.js +++ b/lib/WebLib.js @@ -277,7 +277,7 @@ class MusicPlayer { } queue(args) { - let queue = this.dj.queue.map((x) => { + let queue = this.musicPlayer.queue.map((x) => { return { id: generateID(['Media', x.url]), name: x.title, @@ -301,7 +301,7 @@ class MusicPlayer { } get paused() { - return this.musicPlayer.disp? this.dj.disp.paused : false; + return this.musicPlayer.disp? this.musicPlayer.disp.paused : false; } get queueCount() { diff --git a/lib/api/AnilistApi.js b/lib/api/AnilistApi.js index ed728d5..e18efc4 100644 --- a/lib/api/AnilistApi.js +++ b/lib/api/AnilistApi.js @@ -1,15 +1,26 @@ const fetch = require('node-fetch'), fsx = require('fs-extra'), + yaml = require('js-yaml'), queryPath = './lib/api/graphql/AnilistApi', alApiEndpoint = 'https://graphql.anilist.co'; +async function getFragments() { + let fragments = await fsx.readFile(`${queryPath}/Fragments.yaml`, {encoding: 'utf-8'}); + return yaml.safeLoad(fragments); +} + /** * Return a graphql query read from a file from a configured path. * @param name * @returns {Promise} */ async function getGraphqlQuery(name) { - return await fsx.readFile(`${queryPath}/${name}.gql`, {encoding: 'utf-8'}); + let query = await fsx.readFile(`${queryPath}/${name}.gql`, {encoding: 'utf-8'}); + let fragments = await getFragments(); + for (let [key, value] of Object.entries(fragments)) + if (query.includes(`...${key}`)) + query += '\n' + value; + return query; } /** @@ -27,7 +38,7 @@ function postGraphqlQuery(queryName, queryVariables) { 'Accept': 'application/json' }, body: JSON.stringify({ - query: await getGraphqlQuery(queryName), + query: (await getGraphqlQuery(queryName)), variables: queryVariables }) }).then(async (response) => { @@ -39,65 +50,128 @@ function postGraphqlQuery(queryName, queryVariables) { /** * Get an anime by id. - * @param id + * @param id {Number} + * @param withStaff {Boolean} Include Staff information? + * @param withMetadata {Boolean} Include Metadata? * @returns {Promise} */ -exports.getAnimeById = async function(id) { - let data = await postGraphqlQuery('AnimeById', {id: id}); - if (data.Media) +async function getAnimeById(id, withStaff, withMoreData) { + let data = await postGraphqlQuery('AnimeQuery', + {id: id, withStaff: withStaff, withMoreData: withMoreData}); + if (data && data.Media) return data.Media; else return null; -}; +} /** * Get a manga by id. - * @param id + * @param id {Number} + * @param withStaff {Boolean} Include Staff information? + * @param withMoreData {Boolean} Include Metadata? * @returns {Promise} */ -exports.getMangaById = async function(id) { - let data = await postGraphqlQuery('MangaById', {id: id}); - if (data.Media) +async function getMangaById(id, withStaff, withMoreData) { + let data = await postGraphqlQuery('MangaQuery', + {id: id, withStaff: withStaff, withMoreData: withMoreData}); + if (data && data.Media) return data.Media; else return null; -}; +} /** - * Search for a media entry by name and return it. - * @param name - * @returns {Promise} + * Returns a staff member by id. + * @param id {Number} + * @returns {Promise<*>} */ -exports.searchMediaByName = async function(name) { - let data = await postGraphqlQuery('MediaSearchByName', {name: name}); - if (data.Media) - return data.Media; +async function getStaffById(id) { + let data = await postGraphqlQuery('StaffQuery', {id: id}); + if (data && data.Staff) + return data.Staff; else return null; -}; +} + +/** + * Returns a character by id. + * @param id {Number} + * @returns {Promise<*>} + */ +async function getCharacterById(id) { + let data = await postGraphqlQuery('CharacterQuery', {id: id}); + if (data && data.Character) + return data.Character; + else + return null; +} /** * Search for an anime by name and get it by id. - * @param name + * @param name {String} + * @param withStaff {Boolean} Include Staff information? + * @param withMoreData {Boolean} Include Metadata? * @returns {Promise<*>} */ -exports.searchAnimeByName = async function(name) { - let data = await postGraphqlQuery('MediaSearchByName', {name: name, type: 'ANIME'}); - if (data && data.Media && data.Media.id) - return await exports.getAnimeById(data.Media.id); +async function searchAnimeByName(name, withStaff, withMoreData) { + let data = await postGraphqlQuery('AnimeQuery', + {name: name, withStaff: withStaff, withMoreData: withMoreData}); + if (data && data.Media) + return data.Media; else return null; -}; +} /** * Search for a manga by name and get it by id. - * @param name + * @param name {String} + * @param withStaff {Boolean} Include Staff information? + * @param withMoreData {Boolean} Include Metadata? + * @returns {Promise<*>} + */ +async function searchMangaByName(name, withStaff, withMoreData) { + let data = await postGraphqlQuery('MangaQuery', + {name: name, withStaff: withStaff, withMoreData: withMoreData}); + if (data && data.Media) + return data.Media; + else + return null; +} + +/** + * Search for a staff member by name and get information. + * @param name {String} The name of the staff member * @returns {Promise<*>} */ -exports.searchMangaByName = async function(name) { - let data = await postGraphqlQuery('MediaSearchByName', {name: name, type: 'MANGA'}); - if (data && data.Media && data.Media.id) - return await exports.getMangaById(data.Media.id); +async function searchStaffByName(name) { + let data = await postGraphqlQuery('StaffQuery', {name: name}); + if (data && data.Staff) + return data.Staff; else return null; -}; +} + +/** + * Seach for a character by name and get information. + * @param name {String} Character Name + * @returns {Promise<*>} + */ +async function searchCharacterByName(name) { + let data = await postGraphqlQuery('CharacterQuery', {name: name}); + if (data && data.Character) + return data.Character; + else + return null; +} + +// exports +Object.assign(exports, { + getAnimeById: getAnimeById, + getMangaById: getMangaById, + getStaffById: getStaffById, + getCharacterById: getCharacterById, + searchAnimeByName: searchAnimeByName, + searchMangaByName: searchMangaByName, + searchStaffByName: searchStaffByName, + searchCharacterByName: searchCharacterByName +}); diff --git a/lib/api/graphql/AnilistApi/AnimeById.gql b/lib/api/graphql/AnilistApi/AnimeById.gql deleted file mode 100644 index b9c5589..0000000 --- a/lib/api/graphql/AnilistApi/AnimeById.gql +++ /dev/null @@ -1,47 +0,0 @@ -query ($id: Int) { - Media (id: $id, type: ANIME) { - id - title { - romaji - english - native - } - status - startDate { - year - month - day - } - endDate { - year - month - day - } - format - season - episodes - duration - genres - siteUrl - coverImage { - large - medium - color - } - description(asHtml: false) - averageScore - favourites - studios(isMain: true) { - studioList: nodes { - id - name - siteUrl - } - } - nextAiringEpisode { - id - airingAt - episode - } - } -} diff --git a/lib/api/graphql/AnilistApi/AnimeQuery.gql b/lib/api/graphql/AnilistApi/AnimeQuery.gql new file mode 100644 index 0000000..b0fd9cd --- /dev/null +++ b/lib/api/graphql/AnilistApi/AnimeQuery.gql @@ -0,0 +1,22 @@ +query AnimeData($name: String, $id: Int, $withStaff: Boolean = false, $withMoreData: Boolean = true) { + Media (id: $id, search: $name, type: ANIME) { + ...mediaMetadata + ...mediaAdditionalMetadata @include(if: $withMoreData) + ...staffFields @include(if: $withStaff) + season @include(if: $withMoreData) + episodes @include(if: $withMoreData) + duration @include(if: $withMoreData) + studios(isMain: true) @include(if: $withMoreData) { + studioList: nodes { + id + name + siteUrl + } + } + nextAiringEpisode @include(if: $withMoreData) { + id + airingAt + episode + } + } +} diff --git a/lib/api/graphql/AnilistApi/CharacterQuery.gql b/lib/api/graphql/AnilistApi/CharacterQuery.gql new file mode 100644 index 0000000..a55bf3a --- /dev/null +++ b/lib/api/graphql/AnilistApi/CharacterQuery.gql @@ -0,0 +1,27 @@ +query ($name: String, $id: Int) { + Character(search: $name, id: $id) { + id + name { + first + last + native + } + description + image { + large + medium + } + siteUrl + media { + edges { + characterRole + voiceActors(language: JAPANESE) { + ...staffMetadata + } + node { + ...mediaMetadata + } + } + } + } +} diff --git a/lib/api/graphql/AnilistApi/MangaById.gql b/lib/api/graphql/AnilistApi/Fragments.yaml similarity index 55% rename from lib/api/graphql/AnilistApi/MangaById.gql rename to lib/api/graphql/AnilistApi/Fragments.yaml index 146bf9b..bef54d0 100644 --- a/lib/api/graphql/AnilistApi/MangaById.gql +++ b/lib/api/graphql/AnilistApi/Fragments.yaml @@ -1,12 +1,27 @@ -query ($id: Int) { - Media (id: $id, type: MANGA) { +mediaMetadata: | + fragment mediaMetadata on Media { id + siteUrl title { romaji english native } + coverImage { + large + medium + color + } + } + +mediaAdditionalMetadata: | + fragment mediaAdditionalMetadata on Media { status + description(asHtml: false) + format + genres + averageScore + favourites startDate { year month @@ -17,37 +32,32 @@ query ($id: Int) { month day } - format - chapters - volumes - genres - siteUrl - coverImage { + } + +staffMetadata: | + fragment staffMetadata on Staff { + id + name { + first + last + native + } + image { large medium - color } + language + siteUrl + } + +staffFields: | + fragment staffFields on Media { staff { edges { node { - id - name { - first - last - native - } - image { - large - medium - } - language - siteUrl + ...staffMetadata } role } } - description(asHtml: false) - averageScore - favourites } -} diff --git a/lib/api/graphql/AnilistApi/MangaQuery.gql b/lib/api/graphql/AnilistApi/MangaQuery.gql new file mode 100644 index 0000000..64cf9ae --- /dev/null +++ b/lib/api/graphql/AnilistApi/MangaQuery.gql @@ -0,0 +1,9 @@ +query MangaData($name: String, $id: Int, $withStaff: Boolean = false, $withMoreData: Boolean = true) { + Media (id: $id, search: $name, type: MANGA) { + ...mediaMetadata + ...mediaAdditionalMetadata @include(if: $withMoreData) + ...staffFields @include(if: $withStaff) + chapters @include(if: $withMoreData) + volumes @include(if: $withMoreData) + } +} diff --git a/lib/api/graphql/AnilistApi/MediaSearchByName.gql b/lib/api/graphql/AnilistApi/MediaSearchByName.gql deleted file mode 100644 index 90a6afb..0000000 --- a/lib/api/graphql/AnilistApi/MediaSearchByName.gql +++ /dev/null @@ -1,11 +0,0 @@ -query ($name: String, $type: MediaType) { - Media (search: $name, type: $type) { - id - title { - romaji - english - native - } - type - } -} diff --git a/lib/api/graphql/AnilistApi/StaffQuery.gql b/lib/api/graphql/AnilistApi/StaffQuery.gql new file mode 100644 index 0000000..ace6ba6 --- /dev/null +++ b/lib/api/graphql/AnilistApi/StaffQuery.gql @@ -0,0 +1,57 @@ +query StaffData($name: String, $id: Int) { + Staff(id: $id, search: $name) { + id + name { + first + last + native + } + language + image { + large + medium + } + staffMedia(page: 0, perPage: 10) { + edges { + node { + id + title { + romaji + english + native + } + siteUrl + } + characters { + id + name { + first + last + } + siteUrl + image { + large + medium + } + } + staffRole + } + } + characters(page: 0, perPage: 10) { + nodes { + id + name { + first + last + } + siteUrl + image { + large + medium + } + } + } + description(asHtml: false) + siteUrl + } +} diff --git a/lib/commands/AnilistApiCommands/AniListCommandsTemplate.yaml b/lib/commands/AnilistApiCommands/AniListCommandsTemplate.yaml index df61a40..f7961af 100644 --- a/lib/commands/AnilistApiCommands/AniListCommandsTemplate.yaml +++ b/lib/commands/AnilistApiCommands/AniListCommandsTemplate.yaml @@ -1,23 +1,58 @@ anime_search: - name: anime + name: alAnime permission: all - usage: anime [search query] + usage: alAnime [search query] description: > - Searches AniList.co for the anime title and returns information about - it if there is a result. + Searches [AniList.co](https://anilist.co) for the anime *title* or *id* and returns information about + it if there is a result. The staff members are not included because the message would grow too big. + category: AniList + response: + not_found: > + I couldn't find the anime you were searching for :( + +anime_staff_search: + name: alAnimeStaff + permission: all + usage: alAnimeStaff [search query] + description: > + Searches [AniList.co](https://anilist.co) for the anime *title* or *id* and returns all staff members. category: AniList response: not_found: > I couldn't find the anime you were searching for :( manga_search: - name: manga + name: alManga permission: all - usage: manga [search query] + usage: alManga [search query] description: > - Searches AniList.co for the manga title and returns information about + Searches [AniList.co](https://anilist.co) for the manga *title* or *id* and returns information about it if there is a result. category: AniList response: not_found: > I couldn't find the manga you were searching for :( + +staff_search: + name: alStaff + permission: all + usage: alStaff [search query] + description: > + Searches [AniList.co](https://anilist.co) for the staff member *name* or *id* and returns information about + the member aswell as roles in media. + category: AniList + response: + not_found: > + I couldn't find the staff member you were searching for :( + +character_search: + name: alCharacter + permission: all + usage: alCharacter [search query] + description: > + Searches [AniList.co](https://anilist.co) for the character *name* or *id* and returns information about + the character aswell as media roles. + category: AniList + response: + not_found: > + I couldn't find the character member you were searching for :( diff --git a/lib/commands/AnilistApiCommands/index.js b/lib/commands/AnilistApiCommands/index.js index 6ee5628..97a431d 100644 --- a/lib/commands/AnilistApiCommands/index.js +++ b/lib/commands/AnilistApiCommands/index.js @@ -2,6 +2,21 @@ const cmdLib = require('../../CommandLib'), anilistApi = require('../../api/AnilistApi'), location = './lib/commands/AnilistApiCommands'; +/** + * Returns a string for a name. + * @param nameNode {String} The AniList name node in format {first, last, native} + */ +function getNameString(nameNode) { + let name = ''; + if (nameNode.first) + name = nameNode.first; + if (nameNode.last) + name += ' ' + nameNode.last; + if (name.length === 0) + name = nameNode.native; + return name; +} + class RichMediaInfo extends cmdLib.ExtendedRichEmbed { /** @@ -10,16 +25,22 @@ class RichMediaInfo extends cmdLib.ExtendedRichEmbed { */ constructor(mediaInfo) { super(mediaInfo.title.romaji); - this.setDescription(mediaInfo.description.replace(/<\/?.*?>/g, '')) - .setThumbnail(mediaInfo.coverImage.large) + this.setThumbnail(mediaInfo.coverImage.large || mediaInfo.coverImage.medium) .setURL(mediaInfo.siteUrl) .setColor(mediaInfo.coverImage.color) - .setFooter('Provided by AniList.co'); + .setFooter('Powered by AniList.co'); + if (mediaInfo.description) + this.setDescription(mediaInfo.description + .replace(/<\/?.*?>/g, '') + .replace(/~!.*?!~/g, '') + .replace(/\n\n\n/g, '')); let fields = { - 'Genres': mediaInfo.genres.join(' '), + 'Genres': mediaInfo.genres? mediaInfo.genres.join(' ') : null, 'Studios': mediaInfo.studios? mediaInfo.studios.studioList.map(x => `[${x.name}](${x.siteUrl})`) : null, - 'Scoring': `**AverageScore**: ${mediaInfo.averageScore}\n**Favourites**${mediaInfo.favourites}`, + 'Scoring': mediaInfo.averageScore? `**AverageScore**: ${mediaInfo.averageScore}\n**Favourites:** ${mediaInfo.favourites}`: null, 'Episodes': mediaInfo.episodes, + 'Volumes': mediaInfo.volumes, + 'Chapters': mediaInfo.chapters, 'Duration': null, 'Season': mediaInfo.season, 'Status': mediaInfo.status, @@ -27,18 +48,109 @@ class RichMediaInfo extends cmdLib.ExtendedRichEmbed { }; if (mediaInfo.duration) fields['Episode Duration'] = `${mediaInfo.duration} min`; - if (mediaInfo.startDate.day) + if (mediaInfo.startDate && 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) + if (mediaInfo.endDate && mediaInfo.endDate.day) fields['End Date'] = `${mediaInfo.endDate.day}.${mediaInfo.endDate.month}.${mediaInfo.endDate.year}`; + if (mediaInfo.staff && mediaInfo.staff.edges) { + let staffContent = mediaInfo.staff.edges.map((x) => { + let url = x.node.siteUrl; + let name = getNameString(x.node.name); + return `**${x.role}:** [${name}](${url})`; + }); + let staffFieldValue = staffContent.join('\n'); + if (staffFieldValue.length > 1024) { + let staffValues = []; + let currentValue = ''; + + for (let staffLine of staffContent) { + let concatValue = currentValue + '\n' + staffLine; + if (concatValue.length > 1024) { + staffValues.push(currentValue); + currentValue = staffLine; + } else { + currentValue = concatValue; + } + } + staffValues.push(currentValue); + for (let i = 0; i < staffValues.length; i++) + fields[`Staff part ${i + 1}`] = staffValues[i]; + } else { + fields['Staff'] = staffFieldValue; + } + } + this.addFields(fields); } } +class RichStaffInfo extends cmdLib.ExtendedRichEmbed { + + /** + * A Rich Embed with informatin about an AniList staff member. + * @param staffInfo + */ + constructor(staffInfo) { + super(getNameString(staffInfo.name)); + this.setThumbnail(staffInfo.image.large || staffInfo.image.medium) + .setURL(staffInfo.siteUrl); + let fields = { + 'Language': staffInfo.language + }; + if (staffInfo.staffMedia && staffInfo.staffMedia.edges) + fields['Staff Media Roles (first 10)'] = staffInfo.staffMedia.edges.map(x => { + let node = x.node; + let title = node.title.romaji; + let url = node.siteUrl; + return `[**${title}**](${url}): ${x.staffRole}`; + }).join('\n'); + if (staffInfo.characters && staffInfo.characters.nodes) + fields['Staff Character Roles (first 10)'] = staffInfo.characters.nodes.map(x => { + let name = getNameString(x.name); + let url = x.siteUrl; + return `[${name}](${url})`; + }).join('\n'); + + + this.addFields(fields); + } +} + +class RichCharacterInfo extends cmdLib.ExtendedRichEmbed { + + /** + * A RichEmbed with information about an AniList character. + * @param characterInfo {Object} + */ + constructor(characterInfo) { + super(getNameString(characterInfo.name)); + this.setURL(characterInfo.siteUrl) + .setThumbnail(characterInfo.image.large || characterInfo.image.medium); + if (characterInfo.description) + this.setDescription(characterInfo.description + .replace(/<\/?.*?>/g, '') + .replace(/~!.*?!~/g, '') + .replace(/\n\n\n/g, '')); + if (characterInfo.media && characterInfo.media.edges) + this.addNonemptyField( + 'Media Appeareance', + characterInfo.media.edges.map(x => { + let media = x.node; + let informationString = `**[${media.title.romaji}](${media.siteUrl})**: ${x.characterRole}`; + if (x.voiceActors && x.voiceActors.length > 0) + informationString += ` voice by ${x.voiceActors.map(y => { + `[${getNameString(y.name)}](${y.siteUrl})`; + }).join(', ')}`; + return informationString; + }).join('\n') + ); + } +} + // -- initialize -- // /** @@ -54,38 +166,129 @@ class AniListCommandModule extends cmdLib.CommandModule { 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); + let animeData = {}; + if (/^\d+$/.test(s)) + animeData = await anilistApi.getAnimeById(s, false, true); + else + animeData = await anilistApi.searchAnimeByName(s, false, true); + this._logger.silly(`Anime Query returned ${JSON.stringify(animeData)}`); + return new RichMediaInfo(animeData); + } catch (err) { + if (err.message) { + this._logger.verbose(err.message); + this._logger.silly(err.stack); + } else if (err.errors) { + this._logger.silly(`Graphql Errors ${JSON.stringify(err.errors)}`); + } + return this.template.anime_search.response.not_found; + } + }) + ); + + let animeStaffSearch = new cmdLib.Command( + this.template.anime_staff_search, + new cmdLib.Answer(async (m, k, s) => { + try { + let animeData = {}; + if (/^\d+$/.test(s)) + animeData = await anilistApi.getAnimeById(s, true, false); + else + animeData = await anilistApi.searchAnimeByName(s, true, false); this._logger.silly(`Anime Query returned ${JSON.stringify(animeData)}`); return new RichMediaInfo(animeData); } catch (err) { - if (err.message) + if (err.message) { this._logger.verbose(err.message); - return this.template.anime_search.not_found; + this._logger.silly(err.stack); + } else if (err.errors) { + this._logger.silly(`Graphql Errors ${JSON.stringify(err.errors)}`); + } + return this.template.anime_staff_search.response.not_found; } - })); + }) + ); let mangaSearch = new cmdLib.Command( this.template.manga_search, new cmdLib.Answer(async (m, k, s) => { try { - let mangaData = await anilistApi.searchMangaByName(s); + let mangaData = {}; + if (/^\d+$/.test(s)) + mangaData = await anilistApi.getMangaById(s, true, true); + else + mangaData= await anilistApi.searchMangaByName(s, true, true); this._logger.silly(`Manga Query returned ${JSON.stringify(mangaData)}`); return new RichMediaInfo(mangaData); } catch (err) { - if (err.message) + if (err.message) { + this._logger.verbose(err.message); + this._logger.silly(err.stack); + } else if (err.errors) { + this._logger.silly(`Graphql Errors ${JSON.stringify(err.errors)}`); + } + return this.template.manga_search.response.not_found; + } + }) + ); + + let staffSearch = new cmdLib.Command( + this.template.staff_search, + new cmdLib.Answer(async (m, k, s) => { + try { + let staffData = {}; + if (/^\d+$/.test(s)) + staffData = await anilistApi.getStaffById(s); + else + staffData = await anilistApi.searchStaffByName(s); + this._logger.silly(`Staff Query returned ${JSON.stringify(staffData)}`); + return new RichStaffInfo(staffData); + } catch (err) { + if (err.message) { + this._logger.verbose(err.message); + this._logger.silly(err.stack); + } else if (err.errors) { + this._logger.silly(`Graphql Errors ${JSON.stringify(err.errors)}`); + } + return this.template.staff_search.response.not_found; + } + }) + ); + + let characterSearch = new cmdLib.Command( + this.template.character_search, + new cmdLib.Answer(async (m, k, s) => { + try { + let characterData = {}; + if (/^\d+$/.test(s)) + characterData = await anilistApi.getCharacterById(s); + else + characterData = await anilistApi.searchCharacterByName(s); + this._logger.silly(`Character Query returned ${JSON.stringify(characterData)}`); + return new RichCharacterInfo(characterData) + } catch (err) { + if (err.message) { this._logger.verbose(err.message); - return this.template.manga_search.not_found; + this._logger.silly(err.stack); + } else if (err.errors) { + this._logger.silly(`Graphql Errors ${JSON.stringify(err.errors)}`); + } + return this.template.character_search.response.not_found; } }) ); // registering commands - commandHandler.registerCommand(animeSearch) - .registerCommand(mangaSearch); + commandHandler + .registerCommand(animeSearch) + .registerCommand(mangaSearch) + .registerCommand(staffSearch) + .registerCommand(animeStaffSearch) + .registerCommand(characterSearch); } } diff --git a/web/http/scripts/query.js b/web/http/scripts/query.js index 04690c4..90660a3 100644 --- a/web/http/scripts/query.js +++ b/web/http/scripts/query.js @@ -140,7 +140,6 @@ function queryGuildStatus(guildId) { } } } - config }`; postQuery(query).then((res) => { let guild = res.data.client.guilds[0]; From 5114228134cdf7e716f009bc2ce826c05725335c Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sun, 3 Mar 2019 11:26:15 +0100 Subject: [PATCH 22/26] Added Music voting - users that don't own the role dj or botcommander or botowner need to vote for skipping. The vote is passed if more or exactly 50% voted to skip --- .../MusicCommands/MusicCommandsTemplate.yaml | 8 ++-- lib/commands/MusicCommands/index.js | 47 ++++++++++++++++--- lib/guilding.js | 28 ++++++++++- 3 files changed, 71 insertions(+), 12 deletions(-) diff --git a/lib/commands/MusicCommands/MusicCommandsTemplate.yaml b/lib/commands/MusicCommands/MusicCommandsTemplate.yaml index e2d7587..9b574ea 100644 --- a/lib/commands/MusicCommands/MusicCommandsTemplate.yaml +++ b/lib/commands/MusicCommands/MusicCommandsTemplate.yaml @@ -53,7 +53,7 @@ stop: name: stop description: > Stops the media playback and leaves the VoiceChannel. - permission: musicPlayer + permission: all category: Music response: success: > @@ -89,7 +89,7 @@ skip: name: skip description: > Skips the currently playing song. - permission: musicPlayer + permission: all category: Music response: success: > @@ -150,7 +150,7 @@ save_media: name: savemedia description: > Saves the YouTube URL with a specific name. - permission: musicPlayer + permission: dj category: Music args: - url @@ -160,7 +160,7 @@ delete_media: name: deletemedia description: > Deletes a saved YouTube URL from saved media. - permission: musicPlayer + permission: dj category: Music usage: deletemedia [name] response: diff --git a/lib/commands/MusicCommands/index.js b/lib/commands/MusicCommands/index.js index d368d87..5af3cee 100644 --- a/lib/commands/MusicCommands/index.js +++ b/lib/commands/MusicCommands/index.js @@ -1,7 +1,20 @@ const cmdLib = require('../../CommandLib'), utils = require('../../utils'), + config = require('../../../config'), location = './lib/commands/MusicCommands'; +function 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; +} + /** * Music commands provide commands to control the bots music functions. * These commands are for server music functionalities. @@ -102,9 +115,20 @@ class MusicCommandModule extends cmdLib.CommandModule { this.template.stop, new cmdLib.Answer(async (m) => { let gh = await this._getGuildHandler(m.guild); - if (gh.musicPlayer.connected) { - gh.musicPlayer.stop(); - return this.template.stop.success; + let vc = gh.musicPlayer.voiceChannel || m.member.voiceChannel; + if (gh.musicPlayer.connected && vc) { + let votes = gh.updateCommandVote(stop.name, m.author.tag); + let neededVotes = Math.ceil(vc.members.size/2); + + if (neededVotes <= votes.count || checkPermission(m, 'dj')) { + this._logger.debug(`Vote passed. ${votes.count} out of ${neededVotes} for stop or permission granted`); + gh.musicPlayer.stop(); + gh.resetCommandVote(stop.name); + return this.template.stop.success; + } else { + this._logger.silly(`Vote count increased. ${votes.count} out of ${neededVotes} for stop`); + return `${votes.count} out of ${neededVotes} needed voted to stop.`; + } } else { return this.template.stop.not_playing; } @@ -141,9 +165,20 @@ class MusicCommandModule extends cmdLib.CommandModule { this.template.skip, new cmdLib.Answer(async (m) => { let gh = await this._getGuildHandler(m.guild); - if (gh.musicPlayer.playing) { - gh.musicPlayer.skip(); - return this.template.skip.response.success; + let vc = gh.musicPlayer.voiceChannel || m.member.voiceChannel; + if (gh.musicPlayer.playing && vc) { + let votes = gh.updateCommandVote(skip.name, m.author.tag); + let neededVotes = Math.ceil(vc.members.size/2); + + if (neededVotes <= votes.count || checkPermission(m, 'dj')) { + this._logger.debug(`Vote passed. ${votes.count} out of ${neededVotes} for skip or permission granted`); + gh.musicPlayer.skip(); + gh.resetCommandVote(skip.name); + return this.template.skip.response.success; + } else { + this._logger.silly(`Vote count increased. ${votes.count} out of ${neededVotes} for skip`); + return `${votes.count} out of ${neededVotes} needed voted to skip.`; + } } else { return this.template.skip.response.not_playing; } diff --git a/lib/guilding.js b/lib/guilding.js index e2276fc..63d03b3 100644 --- a/lib/guilding.js +++ b/lib/guilding.js @@ -17,6 +17,7 @@ class GuildHandler { this._logger = new logging.Logger(`${this.constructor.name}@${this.guild}`); this.musicPlayer = new music.MusicPlayer(null); this._logger.silly('Initialized Guild Handler'); + this._votes = {}; } /** @@ -30,7 +31,7 @@ class GuildHandler { await this.db.init(); this._logger.debug(`Connected to the database for ${this.guild}`); this._logger.debug('Creating Databases'); - await this.createTables(); + await this._createTables(); } /** @@ -49,7 +50,7 @@ class GuildHandler { * messages - logs all messages send on the server * playlists - save playlists to play them later */ - async createTables() { + async _createTables() { await this.db.run(`${utils.sql.tableExistCreate} messages ( ${utils.sql.pkIdSerial}, creation_timestamp DATETIME NOT NULL, @@ -71,6 +72,29 @@ class GuildHandler { )`); this._logger.silly('Created Table commands'); } + + /** + * Sets the vote counter for a command up and adds the user. + * @param command {String} + * @param user {String} + */ + updateCommandVote(command, user) { + if (!this._votes[command]) + this._votes[command] = {count: 0, users: []}; + if (!this._votes[command].users.includes(user)) { + this._votes[command].count++; + this._votes[command].users.push(user); + } + return this._votes[command]; + } + + /** + * Resets the vote counter and voted users for a command. + * @param command {String} + */ + resetCommandVote(command) { + this._votes[command] = {count: 0, users: []}; + } } Object.assign(exports, { From 3ae7fbc6fafcd78364d58b49bd9c0a6200457bfb Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sun, 3 Mar 2019 11:29:57 +0100 Subject: [PATCH 23/26] Fixed Style issues --- lib/commands/AnilistApiCommands/index.js | 13 +++++++++++-- lib/logging.js | 6 +++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/lib/commands/AnilistApiCommands/index.js b/lib/commands/AnilistApiCommands/index.js index 97a431d..d66ee42 100644 --- a/lib/commands/AnilistApiCommands/index.js +++ b/lib/commands/AnilistApiCommands/index.js @@ -2,6 +2,10 @@ const cmdLib = require('../../CommandLib'), anilistApi = require('../../api/AnilistApi'), location = './lib/commands/AnilistApiCommands'; +/** + * The AniList commands are all commands that interact with the anilist api. + */ + /** * Returns a string for a name. * @param nameNode {String} The AniList name node in format {first, last, native} @@ -56,6 +60,12 @@ class RichMediaInfo extends cmdLib.ExtendedRichEmbed { } if (mediaInfo.endDate && mediaInfo.endDate.day) fields['End Date'] = `${mediaInfo.endDate.day}.${mediaInfo.endDate.month}.${mediaInfo.endDate.year}`; + this.addStaffInfo(mediaInfo); + this.addFields(fields); + } + + addStaffInfo(mediaInfo) { + let fields = {}; if (mediaInfo.staff && mediaInfo.staff.edges) { let staffContent = mediaInfo.staff.edges.map((x) => { let url = x.node.siteUrl; @@ -83,7 +93,6 @@ class RichMediaInfo extends cmdLib.ExtendedRichEmbed { fields['Staff'] = staffFieldValue; } } - this.addFields(fields); } } @@ -269,7 +278,7 @@ class AniListCommandModule extends cmdLib.CommandModule { else characterData = await anilistApi.searchCharacterByName(s); this._logger.silly(`Character Query returned ${JSON.stringify(characterData)}`); - return new RichCharacterInfo(characterData) + return new RichCharacterInfo(characterData); } catch (err) { if (err.message) { this._logger.verbose(err.message); diff --git a/lib/logging.js b/lib/logging.js index 98166ef..6cfc1e2 100644 --- a/lib/logging.js +++ b/lib/logging.js @@ -76,7 +76,7 @@ class ModuleLogger { constructor(moduleInstance) { this.logger = logger; - if (moduleInstance.constructor) { + if (moduleInstance.constructor) switch (moduleInstance.constructor.name) { case 'String': this.logName = moduleInstance; @@ -87,9 +87,9 @@ class ModuleLogger { default: this.logName = moduleInstance.constructor.name; } - } else { + else this.logName = moduleInstance.toString(); - } + } silly(msg, meta) { From 169d83fccfd1ed3e68ce967e038b6e39f70909a7 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sun, 3 Mar 2019 11:55:51 +0100 Subject: [PATCH 24/26] Added Changelog - added CHANGELOG.md - modified README.md --- CHANGELOG.md | 32 ++++++++++++++++++++++++++++++++ README.md | 4 ++++ 2 files changed, 36 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2bba3a9 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,32 @@ +# Changelog +All notable changes to the discord bot will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.10.0] - 2019-03-03 +### Added +- AniList api commands powered by [AniList.co](https://www.anilist.co) +- MessageHandler - handles all incoming messages, parses the syntax, executes the syntax and handles rate limits +- CommandHandler - handles all single commands, checks command Permission and executes the command +- Command - represents a single command with the necessary metadata and answer instance +- Answer - represents a commands answer with own syntax parsing (can be overwritten) +- CommandModule - represents a single module of a command with the initialization and registring of command to the command handler. Each module owns an instance of the logger +- ExtendedRichEmbed - extends the functinality of the default discord.js RichEmbed with auto cropping of too long field values, functions to add an Object with fields that are not empty and automatic timestamp addition + +### Changed +- Command Syntax now orients more on linux/unix style with `&&` and `;` +- GuildHandler now doesn't handle commands anymore +- the default logger is now a wrapper around the winston.js logger that loggs the current module's name +- all commands are now defined in the lib/commands folder with a folder for each command that contains a `index.js` and a `CommandTemplate.yaml`. +- Rate Limits now only affect commands +- Music commands `~skip` and `~stop` now are votable when the user doesn't have the role *dj* or *botcommander* +- renamed the lib/music to lib/MusicLib and the DJ class to MusicHandler class +- renamed the lib/weblib to lib/WebLib +- changed graphql schema to fit the new internal names +- changed interface to fit the new graphql schema +- changed module export definition to `Object.assign(exports, {...})` at the end of the module file +- added section `commandSettings` to config.js file + +### Removed +- removed lib/cmd because all functionalities are now adapted to the MessageHandler and CommadnHandlers diff --git a/README.md b/README.md index 90a933f..42ab00f 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,10 @@ The arguments are optional because the token and youtube-api-key that the bot ne "keyFile": "PATH TO YOUR SSL KEY FILE", "certFile": "PATH TO YOUR SSL CERTIFICATE FILE" } + }, + "commandSettings": { + "maxSequenceParallel": 5, // the maximum number of commands executed in parallel + "maxSequenceSerial": 10 // the maximum number of commands executed in serial } } ``` From 5fdbe40c881dd50ce6374ad31b62876346e64461 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sun, 3 Mar 2019 12:21:26 +0100 Subject: [PATCH 25/26] Bug Fixes - fixed ExtendedRichEmbed bug for string.substring if the method doesn't exist on the value - added log module information to webinterface logs - changed logging meta module info from `m` to `module` --- CHANGELOG.md | 1 + bot.js | 18 ++++++++-------- lib/CommandLib.js | 12 +++++++---- lib/WebLib.js | 1 + lib/api/graphql/AnilistApi/Fragments.yaml | 24 ++++++++++----------- lib/api/graphql/schema.gql | 1 + lib/commands/ServerUtilityCommands/index.js | 2 +- lib/logging.js | 12 +++++------ web/http/scripts/query.js | 5 +++++ 9 files changed, 44 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bba3a9..8ca41d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - changed interface to fit the new graphql schema - changed module export definition to `Object.assign(exports, {...})` at the end of the module file - added section `commandSettings` to config.js file +- added module information to webinterface log ### Removed - removed lib/cmd because all functionalities are now adapted to the MessageHandler and CommadnHandlers diff --git a/bot.js b/bot.js index 0edd8db..c2fe03c 100644 --- a/bot.js +++ b/bot.js @@ -246,19 +246,19 @@ class Bot { // Executing the main function if (typeof require !== 'undefined' && require.main === module) { - let logger = logging.logger; - logger.info("Starting up... ", {m: 'bot.init'}); - logger.debug('Calling constructor...', {m: 'bot.init'}); + let logger = new logging.Logger('MAIN-init'); + logger.info("Starting up... "); + logger.debug('Calling constructor...'); let discordBot = new Bot(); - logger.debug('Initializing services...', {m: 'bot.init'}); + logger.debug('Initializing services...'); discordBot.initServices().then(() => { - logger.debug('Starting Bot...', {m: 'bot.init'}); + logger.debug('Starting Bot...'); discordBot.start().catch((err) => { //eslint-disable-line promise/no-nesting - logger.error(err.message, {m: 'bot.init'}); - logger.debug(err.stack, {m: 'bot.init'}); + logger.error(err.message); + logger.debug(err.stack); }); }).catch((err) => { - logger.error(err.message, {m: 'bot.init'}); - logger.debug(err.stack, {m: 'bot.init'}); + logger.error(err.message); + logger.debug(err.stack); }); } diff --git a/lib/CommandLib.js b/lib/CommandLib.js index 7cf44c3..527c794 100644 --- a/lib/CommandLib.js +++ b/lib/CommandLib.js @@ -247,7 +247,9 @@ class ExtendedRichEmbed extends Discord.RichEmbed { * @param value */ setDescription(value) { - let croppedValue = value.substring(0, 1024); + let croppedValue = value; + if (value.substring) + croppedValue = value.substring(0, 1024); if (croppedValue.length < value.length) croppedValue = croppedValue.replace(/\n.*$/g, ''); super.setDescription(croppedValue); @@ -259,9 +261,11 @@ class ExtendedRichEmbed extends Discord.RichEmbed { * @param value */ addField(name, value) { - let croppedValue = value.substring(0, 1024); - if (croppedValue.length < value.length) - croppedValue = croppedValue.replace(/\n.*$/g, ''); + let croppedValue = value; + if (value.substring) + croppedValue = value.substring(0, 1024); + if (croppedValue.length < value.length) + croppedValue = croppedValue.replace(/\n.*$/g, ''); super.addField(name, croppedValue); } } diff --git a/lib/WebLib.js b/lib/WebLib.js index 1713438..6bbcc96 100644 --- a/lib/WebLib.js +++ b/lib/WebLib.js @@ -472,5 +472,6 @@ class LogEntry { this.message = entry.message; this.timestamp = entry.timestamp; this.level = entry.level; + this.module = entry.module || entry.m || 'DEFAULT'; } } diff --git a/lib/api/graphql/AnilistApi/Fragments.yaml b/lib/api/graphql/AnilistApi/Fragments.yaml index bef54d0..ed844da 100644 --- a/lib/api/graphql/AnilistApi/Fragments.yaml +++ b/lib/api/graphql/AnilistApi/Fragments.yaml @@ -34,6 +34,18 @@ mediaAdditionalMetadata: | } } +staffFields: | + fragment staffFields on Media { + staff { + edges { + node { + ...staffMetadata + } + role + } + } + } + staffMetadata: | fragment staffMetadata on Staff { id @@ -49,15 +61,3 @@ staffMetadata: | language siteUrl } - -staffFields: | - fragment staffFields on Media { - staff { - edges { - node { - ...staffMetadata - } - role - } - } - } diff --git a/lib/api/graphql/schema.gql b/lib/api/graphql/schema.gql index 1ba4323..bd14138 100644 --- a/lib/api/graphql/schema.gql +++ b/lib/api/graphql/schema.gql @@ -73,6 +73,7 @@ type LogEntry { message: String level: String timestamp: String + module: String } type Query { client: Client diff --git a/lib/commands/ServerUtilityCommands/index.js b/lib/commands/ServerUtilityCommands/index.js index 92dcb8c..dc833fb 100644 --- a/lib/commands/ServerUtilityCommands/index.js +++ b/lib/commands/ServerUtilityCommands/index.js @@ -54,7 +54,7 @@ class ServerUtilityCommandModule extends cmdLib.CommandModule { .replace(/;/g, '\\;'); sequenceString = sequenceString .replace(/"/g, '') - .replace("'", '"'); + .replace(/'/g, '"'); let sequence = this._messageHandler.parseSyntaxString(sequenceString); let execCommand = this._config.prefix + this.template.execute.name; let maxSqPar = this._config.commandSettings.maxSequenceParallel; diff --git a/lib/logging.js b/lib/logging.js index 6cfc1e2..2278909 100644 --- a/lib/logging.js +++ b/lib/logging.js @@ -93,26 +93,26 @@ class ModuleLogger { } silly(msg, meta) { - logger.silly(msg, {m: this.logName, ...meta}); + logger.silly(msg, {module: this.logName, ...meta}); } debug(msg, meta) { - logger.debug(msg, {m: this.logName, ...meta}); + logger.debug(msg, {module: this.logName, ...meta}); } verbose(msg, meta) { - logger.verbose(msg, {m: this.logName, ...meta}); + logger.verbose(msg, {module: this.logName, ...meta}); } info(msg, meta) { - logger.info(msg, {m: this.logName, ...meta}); + logger.info(msg, {module: this.logName, ...meta}); } warn(msg, meta) { - logger.warn(msg, {m: this.logName, ...meta}); + logger.warn(msg, {module: this.logName, ...meta}); } error(msg, meta) { - logger.error(msg, {m: this.logName, ...meta}); + logger.error(msg, {module: this.logName, ...meta}); } } diff --git a/web/http/scripts/query.js b/web/http/scripts/query.js index 90660a3..5106bce 100644 --- a/web/http/scripts/query.js +++ b/web/http/scripts/query.js @@ -240,6 +240,7 @@ function queryLogs(count) { level message timestamp + module } }`; postQuery(query).then((res) => { @@ -256,6 +257,10 @@ function queryLogs(count) { lvlSpan.innerText = logEntry.level; lvlSpan.setAttribute('class', 'text-left'); infoDiv.appendChild(lvlSpan); + let moduleSpan = document.createElement('span'); + moduleSpan.innerText = logEntry.module; + moduleSpan.setAttribute('class', 'text-left'); + infoDiv.appendChild(moduleSpan); let tsSpan = document.createElement('span'); tsSpan.setAttribute('timestamp', logEntry.timestamp); tsSpan.innerText = moment(logEntry.timestamp, 'YY-MM-DD-HH-mm-ss').format('MMM Do HH:mm:ss'); From 7d04e53e022bc2d6bdf7421fdcc4de20cf162d43 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sun, 3 Mar 2019 12:29:48 +0100 Subject: [PATCH 26/26] Fixed bug in AniList commands - fixed character information voiced by not showing any names --- lib/commands/AnilistApiCommands/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/commands/AnilistApiCommands/index.js b/lib/commands/AnilistApiCommands/index.js index d66ee42..76414cc 100644 --- a/lib/commands/AnilistApiCommands/index.js +++ b/lib/commands/AnilistApiCommands/index.js @@ -152,7 +152,7 @@ class RichCharacterInfo extends cmdLib.ExtendedRichEmbed { let informationString = `**[${media.title.romaji}](${media.siteUrl})**: ${x.characterRole}`; if (x.voiceActors && x.voiceActors.length > 0) informationString += ` voice by ${x.voiceActors.map(y => { - `[${getNameString(y.name)}](${y.siteUrl})`; + return `[${getNameString(y.name)}](${y.siteUrl})`; }).join(', ')}`; return informationString; }).join('\n')