diff --git a/.gitignore b/.gitignore index 1ef0da8..7472e1c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ .idea data package-lock.json -node_modules \ No newline at end of file +node_modules +config.json \ No newline at end of file diff --git a/bot.js b/bot.js index 59a9054..e205e44 100644 --- a/bot.js +++ b/bot.js @@ -4,11 +4,12 @@ const Discord = require("discord.js"), cmd = require("./lib/cmd"), guilding = require('./lib/guilding'), utils = require('./lib/utils'), + config = require('./config.json'), client = new Discord.Client(), args = require('args-parser')(process.argv), - authToken = args.token, - prefix = args.prefix || '~', - gamepresence = args.game || prefix + 'help'; + authToken = args.token || config.token, + prefix = args.prefix || config.prefix, + gamepresence = args.game || config.presence; let presences = [], rotator = null; @@ -31,7 +32,7 @@ function main() { lineReader.on('line', (line) => { presences.push(line); }); - rotator = setInterval(() => rotatePresence(), 60000); + rotator = client.setInterval(() => rotatePresence(), config.presence_duration); } }) }); @@ -43,14 +44,12 @@ function main() { } function registerCommands() { - cmd.createGlobalCommand(prefix + 'ping', () => { - return 'Pong!'; - }, [], "Try it yourself."); - + // useless test command cmd.createGlobalCommand(prefix + 'repeatafterme', (msg, argv, args) => { return args.join(' '); }, [], "Repeats what you say"); + // adds a presence that will be saved in the presence file and added to the rotation cmd.createGlobalCommand(prefix + 'addpresence', (msg, argv, args) => { let p = args.join(' '); presences.push(p); @@ -59,6 +58,7 @@ function registerCommands() { return `Added Presence \`${p}\``; }, [], "Adds a presence to the rotation.", 'owner'); + // shuts down the bot after destroying the client cmd.createGlobalCommand(prefix + 'shutdown', (msg) => { msg.reply('Shutting down...').finally(() => { logger.debug('Destroying client...'); @@ -69,15 +69,31 @@ function registerCommands() { }); }, [], "Shuts the bot down.", 'owner'); + // forces a presence rotation cmd.createGlobalCommand(prefix + 'rotate', () => { try { - clearInterval(rotator); + client.clearInterval(rotator); rotatePresence(); - rotator = setInterval(() => rotatePresence(), 60000); + rotator = client.setInterval(() => rotatePresence(), config.presence_duration); } catch (error) { logger.warn(JSON.stringify(error)); } }, [], 'Force presence rotation', 'owner'); + + // ping command that returns the ping attribute of the client + cmd.createGlobalCommand(prefix + 'ping', () => { + return `Current average ping: \`${client.ping} ms\``; + }, [], 'Returns the current average ping', 'owner'); + + // returns the time the bot is running + cmd.createGlobalCommand(prefix + 'uptime', () => { + return `Uptime: \`${client.uptime/1000} s\`` + }, [], 'Returns the uptime of the bot', 'owner'); + + // returns the numbe of guilds, the bot has joined + cmd.createGlobalCommand(prefix + 'guilds', () => { + return `Number of guilds: \`${client.guilds.size}\`` + }, [], 'Returns the uptime of the bot', 'owner'); } function rotatePresence() { diff --git a/commands/globalcommands.json b/commands/globalcommands.json new file mode 100644 index 0000000..967c044 --- /dev/null +++ b/commands/globalcommands.json @@ -0,0 +1,9 @@ +{ + "utils": { + "help": { + "name": "help", + "permission": "all", + "description": "Shows this help command" + } + } +} \ No newline at end of file diff --git a/commands/servercommands.json b/commands/servercommands.json new file mode 100644 index 0000000..174b942 --- /dev/null +++ b/commands/servercommands.json @@ -0,0 +1,128 @@ +{ + "utils": { + "roles": { + "name": "roles", + "permission": "all", + "description": "Shows the roles used for commands on the server." + } + }, + "music": { + "play": { + "name": "play", + "permission": "all", + "args": [ + "url" + ], + "description": "Adds the url to the YouTube video/playlist into the queue.", + "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" + } + }, + "playnext": { + "name": "playnext", + "permission": "all", + "args": [ + "url" + ], + "description": "Adds the url to the YouTube video as next song to the queue.", + "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" + } + }, + "join": { + "name": "join", + "permission": "all", + "description": "Joins the VC you are in.", + "response": { + "not_connected": "You are not connected to a Voice Channel." + } + }, + "stop": { + "name": "stop", + "permission": "dj", + "description": "Stops playing music and leaves.", + "response": { + "success": "Stopping now..." + } + }, + "pause": { + "name": "pause", + "permission": "all", + "description": "Pauses playing.", + "response": { + "success": "Pausing playback." + } + }, + "resume": { + "name": "resume", + "permission": "all", + "description": "Resumes playing.", + "response": { + "success": "Resuming playback." + } + }, + "skip": { + "name": "skip", + "permission": "dj", + "description": "Skips the current song.", + "response": { + "success": "Skipping to the next song." + } + }, + "clear": { + "name": "clear", + "permission": "dj", + "description": "Clears the queue.", + "response": { + "success": "The Queue has been cleared." + } + }, + "playlist": { + "name": "queue", + "permission": "all", + "description": "Shows the next ten songs." + }, + "current": { + "name": "np", + "permission": "all", + "description": "Shows the currently playing song." + }, + "shuffle": { + "name": "shuffle", + "permission": "all", + "description": "Shuffles the playlist.", + "response": { + "success": "The Queue has been shuffled." + } + }, + "repeat": { + "name": "repeat", + "permission": "all", + "description": "Toggle listening on repeat.", + "response": { + "repeat_true": "Listening on repeat now!", + "repeat_false": "Not listening on repeat anymore." + } + }, + "save": { + "name": "save", + "permission": "dj", + "args": [ + "url", + "name" + ], + "description": "Saves the YouTube song/playlist with a specific name" + }, + "saved": { + "name": "saved", + "permission": "all", + "description": "Prints out all saved playlists." + } + } +} \ No newline at end of file diff --git a/lib/cmd.js b/lib/cmd.js index d276f0b..8bc65b9 100644 --- a/lib/cmd.js +++ b/lib/cmd.js @@ -4,54 +4,73 @@ let logger = require('winston'), globCommands = {}, ownerCommands = {}, - args = require('args-parser')(process.argv); + config = require('../config.json'), + args = require('args-parser')(process.argv), + gcmdTempl = require('../commands/globalcommands'), + scmdTempl = require('../commands/servercommands'); /* Function Definition */ /** * @type {Servant} */ -exports.Servant = class { +module.exports.Servant = class { constructor(prefix) { this.commands = {}; - this.createCommand(((prefix || '~') + 'help') || "~help", () => { + this.prefix = prefix; + // show all commands (except the owner commands if the user is not an owner) + this.createCommand(gcmdTempl.utils.help, (msg) => { let helpstr = "```markdown\n"; helpstr += "Commands\n===\n"; helpstr += 'Global Commands\n---\n'; Object.entries(globCommands).sort().forEach(([key, value]) => { - if (value.role !== 'owner' || msg.author.tag === args.owner) { + if (value.role !== 'owner' || checkPermission(msg, 'owner')) { let cmdhelp = `${key} [${value.args.join('] [')}]`.replace('[]', '').padEnd(25, ' '); cmdhelp += value.description || ''; + cmdhelp += `\nPermission: ${value.role||'all'}`; helpstr += `\n${cmdhelp}\n`; } }); helpstr += '\nServer Commands\n---\n'; Object.entries(this.commands).sort().forEach(([key, value]) => { - if (value.role !== 'owner' || msg.author.tag === args.owner) { + if (value.role !== 'owner' || checkPermission(msg, 'owner')) { let cmdhelp = `${key} [${value.args.join('] [')}]`.replace('[]', '').padEnd(25, ' '); cmdhelp += value.description || ''; + cmdhelp += `\nPermission: ${value.role||'all'}`; helpstr += `\n${cmdhelp}\n`; } }); helpstr += "```"; return helpstr; - }, [], "Shows this help."); + }); + + // show all roles that are used by commands + this.createCommand(scmdTempl.utils.roles, () => { + let roles = []; + Object.entries(globCommands).concat(Object.entries(this.commands)).sort().forEach(([key, value]) => { + roles.push(value.role || 'all'); + }); + return `**Roles**\n${[...new Set(roles)].join('\n')}`; + }); } /** * Creates a command entry in the private commands dict - * @param command + * @param template * @param call - * @param args - * @param description */ - createCommand(command, call, args, description, role) { - this.commands[command] = { - 'args': args, - 'description': description, + 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': role + 'role': template.role }; + logger.debug(`Created server command: ${this.prefix + template.name}, args: ${template.args}`); } /** @@ -93,7 +112,7 @@ exports.Servant = class { * Getting the logger * @param {Object} newLogger */ -exports.setLogger = function (newLogger) { +module.exports.setLogger = function (newLogger) { logger = newLogger; }; @@ -105,14 +124,14 @@ exports.setLogger = function (newLogger) { * @param description * @param role */ -exports.createGlobalCommand = function (command, call, args, description, role) { +module.exports.createGlobalCommand = function (command, call, args, description, role) { globCommands[command] = { 'args': args || [], 'description': description, 'callback': call, 'role': role }; - logger.debug(`Created command: ${command}, args: ${args}`); + logger.debug(`Created global command: ${command}, args: ${args}`); }; @@ -121,22 +140,23 @@ exports.createGlobalCommand = function (command, call, args, description, role) * @param msg * @returns {boolean|*} */ -exports.parseMessage = function (msg) { +module.exports.parseMessage = function (msg) { return parseGlobalCommand(msg); }; /** * Initializes the module by creating a help command */ -exports.init = function (prefix) { +module.exports.init = function (prefix) { logger.verbose("Created help command"); - this.createGlobalCommand((prefix + 'help') || "~help", (msg) => { + this.createGlobalCommand(((prefix || config.prefix) + 'help'), (msg) => { let helpstr = "```markdown\n"; helpstr += "Commands\n---\n"; Object.entries(globCommands).sort().forEach(([key, value]) => { - if (value.role !== 'owner' || msg.author.tag === args.owner) { + if (value.role !== 'owner' || checkPermission(msg, 'owner')) { let cmdhelp = `${key} [${value.args.join('] [')}]`.replace('[]', '').padEnd(25, ' '); cmdhelp += value.description || ''; + cmdhelp += `\nPermission: ${value.role||'all'}`; helpstr += `\n${cmdhelp}\n`; } }); @@ -167,13 +187,18 @@ function parseGlobalCommand(msg) { return cmd.callback(msg, kwargs, argv); } +/** + * @param msg + * @param role {String} + * @returns {boolean} + */ function checkPermission(msg, role) { if (!role || ['all', 'any', 'everyone'].includes(role)) return true; - if (msg.author.tag === args.owner) { + if (msg.author.tag === args.owner || config.owners.includes(msg.author.tag)) { return true; } else { - if (msg.member && role && msg.member.roles.find("id", role.id)) + if (msg.member && role && msg.member.roles.some(role => role.name.toLowerCase() === role.toLowerCase())) return true } return false diff --git a/lib/guilding.js b/lib/guilding.js index 1afe3f6..f565626 100644 --- a/lib/guilding.js +++ b/lib/guilding.js @@ -1,6 +1,8 @@ const cmd = require('./cmd'), music = require('./music'), data = require('./data'), + config = require('../config.json'), + servercmd = require('../commands/servercommands'), handlers = {}; let logger = require('winston'); @@ -15,9 +17,9 @@ exports.GuildHandler = class { this.guild = guild; this.dataHandler = new data.DataHandler(guild.name); this.dj = null; - this.servant = null; this.mention = false; - this.prefix = prefix || '~'; + this.prefix = prefix || config.prefix; + this.servant = new cmd.Servant(this.prefix); this.registerMusicCommands(); } @@ -57,23 +59,39 @@ exports.GuildHandler = class { * registers all music commands and initializes a dj * @param cmdPrefix */ + + /** + * handles the message by letting the servant parse the command. Depending on the message setting it + * replies or just sends the answer. + * @param msg + */ + handleMessage(msg) { + let answer = this.servant.parseCommand(msg); + if (!answer) return; + if (this.mention) { + msg.reply(answer); + } else { + msg.channel.send(answer); + } + } + registerMusicCommands(cmdPrefix) { let prefix = cmdPrefix || this.prefix; this.dj = new music.DJ(); // play command - this.createCommand(prefix + 'play', (msg, argv) => { + this.servant.createCommand(servercmd.music.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.'; + return servercmd.music.play.response.no_url; if (!url.match(/http/g)) { if (this.getData('savedplaylists') && this.getData('savedplaylists')[url]) { url = this.getData('savedplaylists')[url]; } else { - return 'Not a valid url.'; + return servercmd.music.play.response.url_invalid; } } try { @@ -82,76 +100,78 @@ exports.GuildHandler = class { this.dj.playYouTube(url); }); } else { - return this.dj.playYouTube(url); + this.dj.playYouTube(url); } } catch (err) { logger.error(err); - return `${JSON.stringify(err)}`; + return servercmd.music.play.response.failure; } - }, ['url'], "Adds the url to the YouTube video/playlist into the queue."); + return servercmd.music.play.response.success; + }); // playnext command - this.createCommand(prefix + 'playnext', (msg, argv) => { + this.servant.createCommand(servercmd.music.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) return servercmd.music.playnext.response.no_url; if (!url.match(/http/g)) { if (this.getData('savedplaylists') && this.getData('savedplaylists')[url]) { url = this.getData('savedplaylists')[url]; } else { - return 'Not a valid url'; + return servercmd.music.playnext.response.url_invalid; } } try { - return this.dj.playYouTube(url, true); + this.dj.playYouTube(url, true); } catch (err) { logger.error(err); - return `${JSON.stringify(err)}`; + return servercmd.music.playnext.response.failure; } - }, ['url'], "Adds the url to the YouTube video as next song to the queue."); + return servercmd.music.playnext.response.success; + }); // join command - this.createCommand(prefix + 'join', (msg) => { + this.servant.createCommand(servercmd.music.join, (msg) => { if (msg.member.voiceChannel) { this.dj.connect(msg.member.voiceChannel); } else { - return "You are not connected to a voicechannel."; + return servercmd.music.join.response.not_connected; } - }, [], "Joins the VC you are in."); + }); // stop command - this.createCommand(prefix + 'stop', () => { + this.servant.createCommand(servercmd.music.stop, () => { this.dj.stop(); - return "Stopping now"; - }, [], "Stops playing music and leaves."); + return servercmd.music.stop.response.success; + }); // pause command - this.createCommand(prefix + 'pause', () => { + this.servant.createCommand(servercmd.music.pause, () => { this.dj.pause(); - return "Pausing playing"; - }, [], "Pauses playing."); + return servercmd.music.pause.response.success; + }); // resume command - this.createCommand(prefix + 'resume', () => { + this.servant.createCommand(servercmd.music.resume, () => { this.dj.resume(); - return "Resuming playing"; - }, [], "Resumes playing."); + return servercmd.music.resume.response.success; + }); // skip command - this.createCommand(prefix + 'skip', () => { + this.servant.createCommand(servercmd.music.skip, () => { this.dj.skip(); - return "Skipping Song"; - }, [], "Skips the current song."); + return servercmd.music.skip.response.success; + }); // clear command - this.createCommand(prefix + 'clear', () => { + this.servant.createCommand(servercmd.music.clear, () => { this.dj.clear(); - return "DJ-Queue cleared"; - }, [], "Clears the playlist."); + return servercmd.music.clear.response.success; + }); // playlist command - this.createCommand(prefix + 'playlist', () => { + this.servant.createCommand(servercmd.music.playlist, () => { let songs = this.dj.playlist; logger.debug(`found ${songs.length} songs`); let songlist = `**${songs.length} Songs in playlist**\n`; @@ -160,63 +180,46 @@ exports.GuildHandler = class { songlist += songs[i] + '\n'; } return songlist; - }, [], "Shows the next ten songs."); + }); // np command - this.createCommand(prefix + 'np', () => { + this.servant.createCommand(servercmd.music.current, () => { let song = this.dj.song; return `Playing: ${song.title}\n ${song.url}`; - }, [], "Shows the currently playing song."); + }); // shuffle command - this.createCommand(prefix + 'shuffle', () => { + this.servant.createCommand(servercmd.music.shuffle, () => { this.dj.shuffle(); - return "Randomized the order of the queue." - }, [], "Shuffles the playlist."); + 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 - this.createCommand(prefix + 'save', (msg, argv) => { + this.servant.createCommand(servercmd.music.save, (msg, argv) => { this.appendData('savedplaylists', argv.name, argv.url); return `Saved song/playlist as ${argv['name']}` - }, ['url', 'name'], "Saves the YouTube song/playlist with a specific name"); + }); // saved command - prints out saved playlists - this.createCommand(prefix + 'saved', () => { + this.servant.createCommand(servercmd.music.saved, () => { let response = '```markdown\nSaved Playlists:\n==\n'; Object.entries(this.getData('savedplaylists')).forEach(([key, value]) => { response += `${key.padEnd(10, ' ')} ${value} \n\n`; }); response += '```'; return response; - }, [], "Prints out all saved playlists."); - } - - /** - * creates a servant if not set and lets the servant create a command - * @param command - * @param call - * @param args - * @param description - */ - createCommand(command, call, args, description) { - if (!this.servant) this.servant = new cmd.Servant(this.prefix); - this.servant.createCommand(command, call, args, description); - } - - /** - * handles the message by letting the servant parse the command. Depending on the message setting it - * replies or just sends the answer. - * @param msg - */ - 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); - } + }); } }; diff --git a/lib/music.js b/lib/music.js index a26a175..aa8938f 100644 --- a/lib/music.js +++ b/lib/music.js @@ -3,7 +3,8 @@ const Discord = require("discord.js"), ypi = require('youtube-playlist-info'), yttl = require('get-youtube-title'), args = require('args-parser')(process.argv), - ytapiKey = args.ytapi; + config = require('../config.json'), + ytapiKey = args.ytapi || config.ytapikey; /* Variable Definition */ let logger = require('winston'); let djs = {}; @@ -18,6 +19,7 @@ exports.DJ = class { this.queue = []; this.playing = false; this.current = null; + this.repeat = false; this.volume = 0.5; this.voiceChannel = voiceChannel; } @@ -81,7 +83,7 @@ exports.DJ = class { if (this.voiceChannel && this.voiceChannel.members.size === 1) logger.verbose(`Exiting ${this.voiceChannel.name}`); this.stop(); - }, 300000); + }, config.music.timeout || 300000); } else if (this.connected) setTimeout(() => this.checkListeners(), 10000); } @@ -120,6 +122,7 @@ exports.DJ = class { }); } this.current = this.queue.shift(); + if (this.repeat) this.queue.push(this.current); this.playYouTube(this.current.url); }); return; @@ -135,6 +138,7 @@ exports.DJ = class { this.current = null; if (this.queue.length > 0) { this.current = this.queue.shift(); + if (this.repeat) this.queue.push(this.current); this.playYouTube(this.current.url); } else { this.stop();