From e16965c86f791e28458748b2015d4b4d59e60000 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sun, 23 Dec 2018 02:25:31 +0100 Subject: [PATCH] Added supporting classes - added class GuildHandler -> use this for commands that sould be run on a server - added class Servant -> handles commands, used by GuildHandler - added connected getter to DJ - changed from checking for Connection to checking if connected - added possibility to assign a VoiceChannel at connecting - commands now contain the prefix instead of being a child element of it - added passing additional arguments as third parameter of the command callback - removed complex command parsing function, rewrote it with better pattern matching - removed music commands from bot.js and moved them to GuildHandler class - changed from cmd.createCommand to createGlobalCommand --- bot.js | 113 ++++----------------------------- lib/cmd.js | 151 +++++++++++++++++++++++++------------------- lib/guilding.js | 162 ++++++++++++++++++++++++++++++++++++++++++++++++ lib/music.js | 66 +++++++++++++++----- 4 files changed, 313 insertions(+), 179 deletions(-) create mode 100644 lib/guilding.js diff --git a/bot.js b/bot.js index 25ccf8e..f048a39 100644 --- a/bot.js +++ b/bot.js @@ -3,6 +3,7 @@ const Discord = require("discord.js"), logger = require('./lib/logging').getLogger(), music = require('./lib/music'), cmd = require("./lib/cmd"), + guilding = require('./lib/guilding'), client = new Discord.Client(), args = require('args-parser')(process.argv), authToken = args.token, @@ -32,108 +33,15 @@ function savePlaylist(url, name) { } function registerCommands() { - cmd.createCommand(prefix, 'play', (msg, argv) => { - let vc = msg.member.voiceChannel; - let url = argv['url']; - if (!url) return 'No url given.'; - if (!url.match(/http/g)) { - if (savedplaylists[url]) { - url = savedplaylists[url]; - } - } - try { - return music.play(vc, url); - } catch(err) { - logger.error(err); - msg.reply(`${JSON.stringify(err)}`); - } - }, ['url'], "Adds the url to the YouTube video/playlist into the queue."); - - cmd.createCommand(prefix, 'playnext', (msg, argv) => { - let vc = msg.member.voiceChannel; - let url = argv['url']; - if (!url) return 'No url given.'; - if (!url.match(/http/g)) { - if (savedplaylists[url]) { - url = savedplaylists[url]; - } - } - try { - return music.playnext(vc, url); - } catch(err) { - logger.error(err); - msg.reply(`${JSON.stringify(err)}`); - } - }, ['url'], "Plays the YouTube video after the currently playing song."); - - cmd.createCommand(prefix, 'ping', () => { + cmd.createGlobalCommand(prefix + 'ping', () => { return 'Pong!'; }, [], "Try it yourself."); - cmd.createCommand(prefix, 'join', (msg) => { - if (msg.member.voiceChannel) { - music.connect(msg.member.voiceChannel); - } - else { - msg.reply("You are not connected to a voicechannel."); - } - }, [], "Joins the VC you are in."); - - cmd.createCommand(prefix, 'stop', (msg) => { - let gid = msg.guild.id; - music.stop(gid); - }, [], "Stops playling music and leavs."); - - cmd.createCommand(prefix, 'pause', (msg) => { - let gid = msg.guild.id; - music.pause(gid); - }, [], "Pauses playing."); + cmd.createGlobalCommand(prefix + 'repeatafterme', (msg, argv, args) => { + return args.join(' '); + },[], "Repeats what you say"); - cmd.createCommand(prefix, 'resume', (msg) => { - let gid = msg.guild.id; - music.resume(gid); - }, [], "Resumes playing."); - - cmd.createCommand(prefix, 'skip', (msg) => { - let gid = msg.guild.id; - music.skip(gid); - }, [], "Skips the current song."); - - cmd.createCommand(prefix, 'clear', (msg) => { - let gid = msg.guild.id; - music.clearQueue(gid); - return "All songs have been deleted, commander :no_mouth: " - }, [],"Clears the playlist."); - - cmd.createCommand(prefix, 'playlist', (msg) => { - let gid = msg.guild.id; - let songs = music.getQueue(gid); - logger.debug(`found ${songs.length} songs`); - let songlist = `**${songs.length} Songs in playlist**\n`; - for (let i = 0; i < songs.length; i++) { - if (i > 10) break; - songlist += songs[i] + '\n'; - } - return songlist; - }, [], "Shows the next ten songs."); - - cmd.createCommand(prefix, 'shuffle', (msg) => { - let gid = msg.guild.id; - music.shuffle(gid); - return "The queue has successfully been shuffled :slight_smile:" - }, [], "Shuffles the playlist."); - - cmd.createCommand(prefix, 'current', (msg) => { - let gid = msg.guild.id; - let song = music.nowPlaying(gid); - return `Playing: ${song.title}\n ${song.url}`; - }, [], "Shows the currently playing song."); - - cmd.createCommand(prefix, 'repeatafterme', (msg, argv) => { - return argv['word']; - }, ['word'], "Repeats a single word you say."); - - cmd.createCommand(prefix, 'save', (msg, argv) => { + cmd.createGlobalCommand(prefix + 'save', (msg, argv) => { savePlaylist(argv['url'], argv['name']); return `Saved song/playlist as ${argv['name']}` }, ['url', 'name'], "Saves the YouTube song/playlist with a specific name"); @@ -153,10 +61,11 @@ client.on('message', msg => { return; } logger.verbose(`<${msg.author.username}>: ${msg.content}`); - let reply = cmd.parseMessage(msg); - if (reply) { - msg.reply(reply); - return; + if (!msg.guild) { + let reply = cmd.parseMessage(msg); + if (reply) msg.channel.send(reply); + } else { + guilding.getHandler(msg.guild, prefix).handleMessage(msg); } } catch (err) { logger.error(err.stack); diff --git a/lib/cmd.js b/lib/cmd.js index 50ecf8a..0230ccc 100644 --- a/lib/cmd.js +++ b/lib/cmd.js @@ -2,10 +2,61 @@ /* Variable Definition */ let logger = require('winston'), - commands = {}; + globCommands = {}; /* Function Definition */ +/** + * TODO: Configure commander with functions: + * - parsing commands from messages + * - add/remove commands + * - prefix settings + * @type {Commander} + */ +exports.Servant = class { + constructor(prefix) { + this.commands = {}; + this.createCommand(((prefix || '~')+'help') || "~help", () => { + let helpstr = "```markdown\n"; + helpstr += "Commands\n---\n"; + Object.entries(globCommands).concat(Object.entries(this.commands)).forEach(([key, value]) => { + let cmdhelp = `${key} [${value.args.join('] [')}]`.padEnd(32, ' '); + cmdhelp += value.description || ''; + helpstr += `\n${cmdhelp}\n`; + }); + helpstr += "```"; + return helpstr; + }, [], "Shows this help."); + } + + createCommand(command, call, args, description) { + this.commands[command] = { + 'args': args, + 'description': description, + 'callback': call + }; + } + + parseCommand(msg) { + let globResult = parseGlobalCommand(msg); + logger.debug(`Global command result is ${globResult}`); + let content = msg.content; + let command = (content.match(/^.\w+/) || [])[0]; + if (!command || !this.commands[command]) return globResult; + let cmd = this.commands[command]; + 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}`); + return cmd.callback(msg, kwargs, argv) || globResult; + } + +}; + /** * Getting the logger * @param {Object} newLogger @@ -14,80 +65,54 @@ exports.setLogger = function(newLogger) { logger = newLogger; }; -exports.createCommand = function(prefix, command, call, argv, description) { - try { - logger.debug(`Creating command ${command} with prefix ${prefix} and arguments ${argv}`); - if (!commands[prefix]) commands[prefix] = {}; // create Object commands prefix - commands[prefix][command] = { // assign the command - args: argv || [], - callback: call, - description: description - }; - logger.debug(`Created command ${prefix}${command}`); - } catch (err) { - logger.error(JSON.stringify(err)); - } +exports.createGlobalCommand = function(command, call, args, description) { + globCommands[command] = { + 'args': args || [], + 'description': description, + 'callback': call + }; + logger.debug(`Created command: ${command}, args: ${args}`); }; -/** - * Parses the message by calling the assigned function for the command with arguments - * @param msg - * @returns {string} - */ + exports.parseMessage = function(msg) { - logger.debug(`Recieved message ${msg.content} from ${msg.author.username}`); - let content = msg.content; - let matches = content.match(/^./g); // match with first symbol - logger.debug(matches); - if (matches) { - logger.debug(matches); - logger.debug(`Found prefix ${matches[0]} in message`); - let prefix = matches[0]; - let prefixData = commands[prefix]; - matches = content.replace(prefix, '').match(/^\w+/g); // match with the second word - if (matches && prefixData) { - logger.debug(`found command ${matches[0]} in message`); - let command = matches[0]; - let commandFunction = prefixData[command]; - let args = content - .replace(prefix, '') - .replace(command, '') - .replace(/^\s+/g, '') - .split(' '); - if (commandFunction) { - let argv = {}; - if (commandFunction.args) { - for (let i = 0; i < commandFunction.args.length; i++) { - let arg = commandFunction.args[i]; - argv[arg] = args[i]; - } - } - if (commandFunction.callback) { - logger.debug(`Found callback and args ${JSON.stringify(argv)} in message`); - return commandFunction.callback(msg, argv); // call the command function and return the result - } - } - } - } -}; + return parseGlobalCommand(msg); +} /** * Initializes the module by creating a help command */ exports.init = function(prefix) { logger.verbose("Created help command"); - this.createCommand(prefix || '~', "help", () => { + this.createGlobalCommand((prefix+'help') || "~help", () => { let helpstr = "```markdown\n"; helpstr += "Commands\n---\n"; - Object.keys(commands).forEach((key) => { - Object.keys(commands[key]).forEach((cmd) => { - helpstr += "\n" + key + cmd + " " + JSON.stringify(commands[key][cmd].args).replace(/"|\[\]/g, ''); - if (commands[key][cmd].description) { - helpstr += '\t' + commands[key][cmd].description + '\n'; - } - }); + Object.entries(globCommands).forEach(([key, value]) => { + let cmdhelp = `${key} [${value.args.join('] [')}]`.padEnd(32, ' '); + cmdhelp += value.description || ''; + helpstr += `\n${cmdhelp}\n`; }); helpstr += "```"; return helpstr; }, [], "Shows this help."); -}; \ No newline at end of file +}; + +/** + * Parses the message by calling the assigned function for the command with arguments + * @param msg + */ +function parseGlobalCommand(msg) { + let content = msg.content; + let command = (content.match(/^.\w+/) || [])[0]; + if (!command || !globCommands[command]) return false; + let cmd = globCommands[command]; + 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: ${JSON.stringify(kwargs)}, argv: ${argv}`); + return cmd.callback(msg, kwargs, argv); +} \ No newline at end of file diff --git a/lib/guilding.js b/lib/guilding.js new file mode 100644 index 0000000..1cbdaaf --- /dev/null +++ b/lib/guilding.js @@ -0,0 +1,162 @@ +const cmd = require('./cmd'), + music = require('./music'), + handlers = {}; +let logger = require('winston'); + +exports.setLogger = function(newLogger) { + logger = newLogger; +}; + +exports.GuildHandler = class{ + constructor(guild, prefix) { + this.guild = guild; + this.dj = null; + this.servant = null; + this.mention = false; + this.prefix = prefix || '~'; + this.data = { + savedplaylists: {} + }; + this.registerMusicCommands(); + } + + registerMusicCommands(cmdPrefix) { + let prefix = cmdPrefix || this.prefix; + this.dj = new music.DJ(); + + // play command + this.createCommand(prefix + 'play', (msg, argv) => { + let vc = msg.member.voiceChannel; + let url = argv['url']; + if (!vc) + return 'You are not connected to a VoiceChannel'; + if (!url) + return 'No url given.'; + if (!url.match(/http/g)) { + if (this.data.savedplaylists[url]) { + url = this.data.savedplaylists[url]; + } else { + return 'Not a valid url.'; + } + } + try { + if (!this.dj.connected) { + this.dj.connect(vc).then(() => { + this.dj.playYouTube(url); + }); + }else { + return this.dj.playYouTube(url); + } + } catch(err) { + logger.error(err); + return `${JSON.stringify(err)}`; + } + }, ['url'], "Adds the url to the YouTube video/playlist into the queue."); + + // playnext command + this.createCommand(prefix + 'playnext', (msg, argv) => { + let vc = msg.member.voiceChannel; + if (!this.dj.connected) this.dj.voiceChannel = vc; + let url = argv['url']; + if (!url) return 'No url given.'; + if (!url.match(/http/g)) { + if (this.data.savedplaylists[url]) { + url = this.data.savedplaylists[url]; + } else { + return 'Not a valid url'; + } + } + try { + return this.dj.playYouTube(url, true); + } catch(err) { + logger.error(err); + return `${JSON.stringify(err)}`; + } + }, ['url'], "Adds the url to the YouTube video as next song to the queue."); + + // join command + this.createCommand(prefix + 'join', (msg) => { + if (msg.member.voiceChannel) { + this.dj.connect(msg.member.voiceChannel); + } + else { + msg.reply("You are not connected to a voicechannel."); + } + }, [], "Joins the VC you are in."); + + // stop command + this.createCommand(prefix + 'stop', () => { + this.dj.stop(); + return "Stopping now"; + }, [], "Stops playing music and leaves."); + + // pause command + this.createCommand(prefix + 'pause', () => { + this.dj.pause(); + return "Pausing playing"; + }, [], "Pauses playing."); + + // resume command + this.createCommand(prefix + 'resume', () => { + this.dj.resume(); + return "Resuming playing"; + }, [], "Resumes playing."); + + // skip command + this.createCommand(prefix + 'skip', () => { + this.dj.skip(); + return "Skipping Song"; + }, [], "Skips the current song."); + + // clear command + this.createCommand(prefix + 'clear', () => { + this.dj.clear(); + return "DJ-Queue cleared"; + }, [],"Clears the playlist."); + + // playlist command + this.createCommand(prefix + 'playlist', () => { + let songs = this.dj.playlist; + logger.debug(`found ${songs.length} songs`); + let songlist = `**${songs.length} Songs in playlist**\n`; + for (let i = 0; i < songs.length; i++) { + if (i > 10) break; + songlist += songs[i] + '\n'; + } + return songlist; + }, [], "Shows the next ten songs."); + + // np command + this.createCommand(prefix + 'np', () => { + let song = this.dj.song; + return `Playing: ${song.title}\n ${song.url}`; + }, [], "Shows the currently playing song."); + + // shuffle command + this.createCommand(prefix + 'shuffle', () => { + this.dj.shuffle(); + return "Randomized the order of the queue." + }, [], "Shuffles the playlist."); + } + + createCommand(command, call, args, description) { + if (!this.servant) this.servant = new cmd.Servant(this.prefix); + this.servant.createCommand(command, call, args, description); + } + + handleMessage(msg) { + if (!this.servant) this.servant = new cmd.Servant(this.prefix); + let answer = this.servant.parseCommand(msg); + if (!answer) return; + if (this.mention) { + msg.reply(answer); + } else { + msg.channel.send(answer); + } + } +}; + +exports.getHandler = function(guild, prefix) { + if (!handlers[guild.id]) handlers[guild.id] = new this.GuildHandler(guild, prefix); + return handlers[guild.id]; +}; \ No newline at end of file diff --git a/lib/music.js b/lib/music.js index 5814f28..17ef252 100644 --- a/lib/music.js +++ b/lib/music.js @@ -10,7 +10,7 @@ let connections = {}; /* Function Definition */ -class DJ { +exports.DJ = class{ /* TODO: Disconnect when no user is left in the channel after a (not constant) amout of time. Could be accomplished by checking after every song, if the VoiceChannel (saved in class) still contains @@ -31,26 +31,35 @@ class DJ { * Connects to the given voice channel. Disconnects from the previous one if it exists. * When the bot was moved and connect is executed again, it connects to the initial VoiceChannel because the * VoiceChannel is saved as object variable. - * @returns {Promise} */ - connect() { - if (this.conn) { + connect(voiceChannel) { + this.voiceChannel = voiceChannel || this.voiceChannel; + if (this.connected) { this.stop(); } logger.verbose(`Connecting to voiceChannel ${this.voiceChannel.name}`); return this.voiceChannel.join().then(connection => { logger.info(`Connected to Voicechannel ${this.voiceChannel.name}`); this.conn = connection; + this.checkListeners(); }); } + get connected() { + return ( + this.conn !== null && + this.conn !== undefined && + this.conn.status !== 4 // status 4 means disconnected. + ); + } + /** * Plays a file for the given filename. * TODO: Implement queue * @param filename */ playFile(filename) { - if (this.conn !== null) { + if (this.connected) { this.disp = this.conn.playFile(filename); this.playing = true; } else { @@ -61,14 +70,34 @@ class DJ { } } + /** + * Checks if there are still members listening and sets an exit timeout (5 min) before checking again + * and exiting when noone is listening. Once this function is executed, it calls itself every 10 seconds. + * TODO: Make this work + */ + checkListeners() { + if (this.connected && this.conn.channel.members.size === 0) { + logger.verbose(`Set exit timout for ${this.voiceChannel.name}`); + setTimeout(() => { + if (this.voiceChannel && this.voiceChannel.members.size === 0) + logger.verbose(`Exiting ${this.voiceChannel.name}`); + this.stop(); + }, 300000); + } else if (this.connected) + setTimeout(() => this.checkListeners(), 10000); + } + /** * 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 (regex match), 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 */ playYouTube(url, playnext) { - if (!this.conn) this.connect().then(this.playYouTube(url)); + if (!this.connected) { + this.connect().then(this.playYouTube(url)); + } let plist = url.match(/(?<=\?list=)[\w\-]+/g); if (plist) { logger.debug(`Adding playlist ${plist} to queue`); @@ -180,13 +209,22 @@ class DJ { stop() { this.queue = []; logger.verbose("Stopping music..."); - if (this.disp !== null) { - this.disp.end(); // FIXME: Triggers the dispatcher end event that calls stop again - logger.debug("Ended dispatcher"); - } - if (this.conn !== null) { - this.conn.disconnect(); - logger.debug("Ended connection"); + try { + if (this.disp) { + this.disp.end(); + logger.debug("Ended dispatcher"); + } + if (this.conn) { + this.conn.channel.leave(); + this.conn.disconnect(); + logger.debug("Ended connection"); + } + if (this.voiceChannel) { + this.voiceChannel.leave(); + logger.debug("Left VoiceChannel"); + } + } catch(error) { + logger.verbose(JSON.stringify(error)); } } @@ -250,7 +288,7 @@ exports.setLogger = function (newLogger) { */ exports.connect = function(voiceChannel) { let gid = voiceChannel.guild.id; - let voiceDJ = new DJ(voiceChannel); + let voiceDJ = new this.DJ(voiceChannel); djs[gid] = voiceDJ; return voiceDJ.connect(); };