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); }