From ee96094473d757c458a202f7f493c62234f46230 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sun, 20 Jan 2019 15:59:03 +0100 Subject: [PATCH 01/18] Rich Embed for help command --- bot.js | 10 +++- commands/globalcommands.json | 6 +- commands/servercommands.json | 23 ++++++-- lib/cmd.js | 109 +++++++++++++++++++++++------------ lib/guilding.js | 22 ++++--- 5 files changed, 120 insertions(+), 50 deletions(-) diff --git a/bot.js b/bot.js index b8e4547..e55a4f9 100644 --- a/bot.js +++ b/bot.js @@ -96,7 +96,7 @@ function registerCommands() { // 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'); + }, [], 'Returns the number of guilds the bot has joined', 'owner'); } function rotatePresence() { @@ -120,7 +120,13 @@ client.on('message', msg => { logger.verbose(`<${msg.author.tag}>: ${msg.content}`); if (!msg.guild) { let reply = cmd.parseMessage(msg); - if (reply) msg.channel.send(reply); + if (reply) { + if (reply.isPrototypeOf(Discord.RichEmbed)) { + msg.channel.send('', reply); + } else { + msg.channel.send(reply) + } + } } else { guilding.getHandler(msg.guild, prefix).handleMessage(msg); } diff --git a/commands/globalcommands.json b/commands/globalcommands.json index 967c044..e544b7d 100644 --- a/commands/globalcommands.json +++ b/commands/globalcommands.json @@ -3,7 +3,11 @@ "help": { "name": "help", "permission": "all", - "description": "Shows this help command" + "description": "Shows this help command", + "category": "Utility", + "args": [ + "command" + ] } } } \ No newline at end of file diff --git a/commands/servercommands.json b/commands/servercommands.json index 7fe40c0..b31200e 100644 --- a/commands/servercommands.json +++ b/commands/servercommands.json @@ -3,7 +3,8 @@ "roles": { "name": "roles", "permission": "all", - "description": "Shows the roles used for commands on the server." + "description": "Shows the roles used for commands on the server.", + "category": "Utility" } }, "music": { @@ -14,6 +15,7 @@ "url" ], "description": "Adds the url to the YouTube video/playlist into the queue.", + "category": "Music", "response": { "success": "Added Song/Playlist to the queue.", "failure": "Failed adding Song/Playlist to the queue.", @@ -28,6 +30,7 @@ "url" ], "description": "Adds the url to the YouTube video as next song to the queue.", + "category": "Music", "response": { "success": "Added Song as next Song to the queue.", "failure": "Failed adding Song as next Song to the queue.", @@ -39,6 +42,7 @@ "name": "join", "permission": "all", "description": "Joins the VC you are in.", + "category": "Music", "response": { "not_connected": "You are not connected to a Voice Channel." } @@ -47,6 +51,7 @@ "name": "stop", "permission": "dj", "description": "Stops playing music and leaves.", + "category": "Music", "response": { "success": "Stopping now..." } @@ -55,6 +60,7 @@ "name": "pause", "permission": "all", "description": "Pauses playing.", + "category": "Music", "response": { "success": "Pausing playback." } @@ -63,6 +69,7 @@ "name": "resume", "permission": "all", "description": "Resumes playing.", + "category": "Music", "response": { "success": "Resuming playback." } @@ -71,6 +78,7 @@ "name": "skip", "permission": "dj", "description": "Skips the current song.", + "category": "Music", "response": { "success": "Skipping to the next song." } @@ -79,6 +87,7 @@ "name": "clear", "permission": "dj", "description": "Clears the queue.", + "category": "Music", "response": { "success": "The Queue has been cleared." } @@ -86,17 +95,20 @@ "playlist": { "name": "queue", "permission": "all", - "description": "Shows the next ten songs." + "description": "Shows the next ten songs.", + "category": "Music" }, "current": { "name": "np", "permission": "all", - "description": "Shows the currently playing song." + "description": "Shows the currently playing song.", + "category": "Music" }, "shuffle": { "name": "shuffle", "permission": "all", "description": "Shuffles the playlist.", + "category": "Music", "response": { "success": "The Queue has been shuffled." } @@ -105,6 +117,7 @@ "name": "repeat", "permission": "all", "description": "Toggle listening on repeat.", + "category": "Music", "response": { "repeat_true": "Listening on repeat now!", "repeat_false": "Not listening on repeat anymore." @@ -116,12 +129,14 @@ "args": [ "url" ], - "description": "Saves the YouTube song/playlist with a specific name" + "description": "Saves the YouTube song/playlist with a specific name", + "category": "Music" }, "saved": { "name": "saved", "permission": "all", "description": "Prints out all saved playlists.", + "category": "Music", "response": { "no_saved": "There are no saved songs/playlists :(" } diff --git a/lib/cmd.js b/lib/cmd.js index 844d57a..d4b374d 100644 --- a/lib/cmd.js +++ b/lib/cmd.js @@ -7,7 +7,8 @@ let logger = require('winston'), config = require('../config.json'), args = require('args-parser')(process.argv), gcmdTempl = require('../commands/globalcommands'), - scmdTempl = require('../commands/servercommands'); + scmdTempl = require('../commands/servercommands'), + Discord = require('discord.js'); /* Function Definition */ @@ -19,29 +20,49 @@ exports.Servant = class { 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) => { - let helpstr = "```markdown\n"; - helpstr += "Commands\n===\n"; - helpstr += 'Global Commands\n---\n'; - Object.entries(globCommands).sort().forEach(([key, value]) => { - 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`; + 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 :('; } - }); - helpstr += '\nServer Commands\n---\n'; - Object.entries(this.commands).sort().forEach(([key, value]) => { - 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`; + } else { + let helpEmbed = new Discord.RichEmbed() + .setTitle('Commands'); + let globHelp = ''; + Object.entries(globCommands).sort().forEach(([key, value]) => { + if (value.role !== 'owner' || checkPermission(msg, 'owner')) { + globHelp += `\`${key}\` \t`; + } + }); + helpEmbed.addField('Global Commands', globHelp); + let categories = []; + let catCommands = {}; + Object.entries(this.commands).sort().forEach(([key, value]) => { + if (value.role !== 'owner' || checkPermission(msg, 'owner')) { + if (!categories.includes(value.category)) { + categories.push(value.category); + catCommands[value.category] = `\`${key}\` \t` + } else { + catCommands[value.category] += `\`${key}\` \t` + } + } + }); + for (let cat of categories) { + helpEmbed.addField(cat, catCommands[cat]); } - }); - helpstr += "```"; - return helpstr; + helpEmbed.setFooter( prefix + 'help [command] for more info to each command'); + return helpEmbed; + } }); // show all roles that are used by commands @@ -68,7 +89,8 @@ exports.Servant = class { 'args': template.args || [], 'description': template.description, 'callback': call, - 'role': template.permission + 'role': template.permission, + 'category': template.category || 'Other' }; logger.debug(`Created server command: ${this.prefix + template.name}, args: ${template.args}`); } @@ -154,20 +176,35 @@ exports.parseMessage = function (msg) { */ exports.init = function (prefix) { logger.verbose("Created help command"); - 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' || checkPermission(msg, 'owner')) { - let cmdhelp = `${key} [${value.args.join('] [')}]`.replace('[]', '').padEnd(25, ' '); - cmdhelp += value.description || ''; - cmdhelp += `\nPermission: ${value.role||'all'}`; - helpstr += `\n${cmdhelp}\n`; + this.createGlobalCommand(((prefix || config.prefix) + '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'); } - }); - helpstr += "```"; - return helpstr; - }, [], "Shows this help."); + } else { + let helpEmbed = new Discord.RichEmbed() + .setTitle('Global Commands') + .setTimestamp(); + let description = ''; + Object.entries(globCommands).sort().forEach(([key, value]) => { + if (value.role === 'owner' && checkPermission(msg, 'owner')) { + description += `\`${key}\` \t`; + } else if (value.role !== 'owner') { + description += `\`${key}\` \t`; + } + }); + helpEmbed.setFooter( prefix + 'help [command] for more info to each command'); + helpEmbed.setDescription(description); + return helpEmbed; + } + }, ['command'], "Shows this help."); }; /** diff --git a/lib/guilding.js b/lib/guilding.js index 19d6f33..3f28b39 100644 --- a/lib/guilding.js +++ b/lib/guilding.js @@ -4,6 +4,7 @@ const cmd = require('./cmd'), config = require('../config.json'), servercmd = require('../commands/servercommands'), sqlite3 = require('sqlite3'), + Discord = require('discord.js'), handlers = {}, dbDir = './data/gdb'; let logger = require('winston'); @@ -94,9 +95,17 @@ exports.GuildHandler = class { let answer = this.servant.parseCommand(msg); if (!answer) return; if (this.mention) { - msg.reply(answer); + if (answer.isPrototypeOf(Discord.RichEmbed)) { + msg.reply('', answer); + } else { + msg.reply(answer); + } } else { - msg.channel.send(answer); + if (answer.isPrototypeOf(Discord.RichEmbed)) { + msg.channel.send('', answer); + } else { + msg.channel.send(answer); + } } } else { this.msgsQueue.push(msg); @@ -306,11 +315,10 @@ exports.GuildHandler = class { if (rows.length === 0) { msg.channel.send(servercmd.music.saved.response.no_saved); } else { - msg.channel.send('', {embed: { - "title": "Saved Songs and Playlists", - "description": response, - "fields": [] - }}); + let richEmbed = new Discord.RichEmbed() + .setTitle('Saved Songs and Playlists') + .setDescription(response); + msg.channel.send('', richEmbed); } }); }); From 0729c9b31cf1676c267bc5ffdf7894b7b9efa9ab Mon Sep 17 00:00:00 2001 From: Trivernis Date: Mon, 21 Jan 2019 20:42:32 +0100 Subject: [PATCH 02/18] Command Promises - Command functions are allowed to return promises - resolve and reject both are the answer the bot will send - Implemented promises for things that couldn't use them before --- commands/servercommands.json | 3 +- lib/cmd.js | 5 +- lib/guilding.js | 250 ++++++++++++++++++++--------------- lib/music.js | 27 ++-- 4 files changed, 163 insertions(+), 122 deletions(-) diff --git a/commands/servercommands.json b/commands/servercommands.json index b31200e..b150023 100644 --- a/commands/servercommands.json +++ b/commands/servercommands.json @@ -20,7 +20,8 @@ "success": "Added Song/Playlist to the queue.", "failure": "Failed adding Song/Playlist to the queue.", "url_invalid": "This is not a valid url!", - "no_url": "I need an url to a video to play" + "no_url": "I need an url to a video to play!", + "no_voicechannel": "You need to join a voicechannel to do that!" } }, "playnext": { diff --git a/lib/cmd.js b/lib/cmd.js index d4b374d..1c4cbb3 100644 --- a/lib/cmd.js +++ b/lib/cmd.js @@ -126,7 +126,10 @@ exports.Servant = class { let argv = argvars.slice(nLength); logger.debug(`Executing callback for command: ${command}, kwargs: ${kwargs}, argv: ${argv}`); try { - return cmd.callback(msg, kwargs, argv) || globResult; + let locResult = cmd.callback(msg, kwargs, argv); + if (locResult instanceof Promise) + return locResult; // because Promise equals false in conditional + return locResult || globResult; } catch (err) { logger.error(err.message); return `The command \`${command}\` has thrown an error.`; diff --git a/lib/guilding.js b/lib/guilding.js index 3f28b39..a08265c 100644 --- a/lib/guilding.js +++ b/lib/guilding.js @@ -75,6 +75,26 @@ exports.GuildHandler = class { )`); } + /** + * Answers a message via mention if mentioning is active or with just sending it to the same channel. + * @param msg + * @param answer + */ + 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) { + answer + .then((answer) => this.answerMessage(msg, answer)) + .catch((error) => this.answerMessage(msg, error)); + } else { + (this.mention)? msg.reply(answer) : msg.channel.send(answer); + } + } else { + logger.warning(`Empty answer won't be send.`); + } + } /** * handles the message by letting the servant parse the command. Depending on the message setting it * replies or just sends the answer. @@ -92,21 +112,7 @@ exports.GuildHandler = class { } ); } - let answer = this.servant.parseCommand(msg); - if (!answer) return; - if (this.mention) { - if (answer.isPrototypeOf(Discord.RichEmbed)) { - msg.reply('', answer); - } else { - msg.reply(answer); - } - } else { - if (answer.isPrototypeOf(Discord.RichEmbed)) { - msg.channel.send('', answer); - } else { - msg.channel.send(answer); - } - } + this.answerMessage(msg, this.servant.parseCommand(msg)); } else { this.msgsQueue.push(msg); } @@ -116,15 +122,20 @@ exports.GuildHandler = class { * Connect to a voice-channel if not connected and play the url * @param vc * @param url + * @param next */ - connectAndPlay(vc, url) { - if (!this.dj.connected) { - this.dj.connect(vc).then(() => { - this.dj.playYouTube(url); - }); - } else { - this.dj.playYouTube(url); - } + connectAndPlay(vc, url, next) { + return new Promise((resolve, reject) => { + if (!this.dj.connected) { + this.dj.connect(vc).then(() => { + this.dj.playYouTube(url, next); + resolve(); + }); + } else { + this.dj.playYouTube(url, next); + resolve(); + } + }); } /** @@ -137,74 +148,86 @@ exports.GuildHandler = class { // play command this.servant.createCommand(servercmd.music.play, (msg, kwargs, argv) => { - let vc = msg.member.voiceChannel; - let url = kwargs['url']; - if (!vc) - return 'You are not connected to a VoiceChannel'; - if (!url) - return servercmd.music.play.response.no_url; - if (!url.match(/http/g)) { - if (argv) - url += ' ' + argv.join(' '); - this.db.get('SELECT url FROM playlists WHERE name = ?', [url], (err, row) => { - if (err) { - console.error(err.message); - } - if (!row) { - return servercmd.music.play.response.url_invalid; - } - url = row.url; + return new Promise((resolve, reject) => { + let vc = this.dj.voiceChannel || msg.member.voiceChannel; + let url = kwargs['url']; + if (!vc) + reject(servercmd.music.play.response.no_voicechannel); + if (!url) + reject(servercmd.music.play.response.no_url); + if (!url.match(/http/g)) { + if (argv && argv.length > 0) + url += ' ' + argv.join(' '); // join to get the whole expression behind the command + this.db.get('SELECT url FROM playlists WHERE name = ?', [url], (err, row) => { + if (err) { + console.error(err.message); + } + if (!row) { + reject(servercmd.music.play.response.url_invalid); + logger.verbose('Got invalid url for play command.'); + } else { + url = row.url; + try { + this.connectAndPlay(vc, url).then(() => { + resolve(servercmd.music.play.response.success); + }); + } catch (err) { + logger.error(err.message); + reject(servercmd.music.play.response.failure); + } + } + }); + } else { try { - this.connectAndPlay(vc, url); + this.connectAndPlay(vc, url).then(() => { + resolve(servercmd.music.play.response.success); + }); } catch (err) { logger.error(err.message); - return servercmd.music.play.response.failure; + reject(servercmd.music.play.response.failure); } - }); - } else { - try { - this.connectAndPlay(vc, url); - } catch (err) { - logger.error(err.message); - return servercmd.music.play.response.failure; } - } - return servercmd.music.play.response.success; + }) }); // playnext command this.servant.createCommand(servercmd.music.playnext,(msg, kwargs, argv) => { - let vc = msg.member.voiceChannel; - if (!this.dj.connected) this.dj.voiceChannel = vc; - let url = kwargs['url']; - if (!url) return servercmd.music.playnext.response.no_url; - if (!url.match(/http/g)) { - if (argv) - url += ' ' + argv.join(' '); - this.db.get('SELECT url FROM playlists WHERE name = ?', [url], (err, row) => { - if (err) { - console.error(err.message); - } - if (!row) { - return servercmd.music.play.response.url_invalid; - } - url = row.url; + return new Promise((resolve, reject) => { + let vc = msg.member.voiceChannel; + if (!this.dj.connected) this.dj.voiceChannel = vc; + let url = kwargs['url']; + if (!url) reject(servercmd.music.playnext.response.no_url); + if (!url.match(/http/g)) { + if (argv) + url += ' ' + argv.join(' '); + this.db.get('SELECT url FROM playlists WHERE name = ?', [url], (err, row) => { + if (err) { + console.error(err.message); + } + if (!row) { + reject(servercmd.music.play.response.url_invalid); + } + url = row.url; + try { + this.connectAndPlay(url, true).then(() => { + resolve(servercmd.music.playnext.response.success); + }); + } catch (err) { + logger.error(err.message); + reject(servercmd.music.play.response.failure); + } + }); + } else { try { - this.dj.playYouTube(url, true); + this.connectAndPlay(url, true).then(() => { + resolve(servercmd.music.playnext.response.success); + }); } catch (err) { - logger.error(err.message); - return servercmd.music.play.response.failure; + logger.error(err); + reject(servercmd.music.playnext.response.failure); } - }); - } else { - try { - this.dj.playYouTube(url, true); - } catch (err) { - logger.error(err); - return servercmd.music.playnext.response.failure; } - } - return servercmd.music.playnext.response.success; + }) }); // join command @@ -283,43 +306,50 @@ exports.GuildHandler = class { // saves playlists this.servant.createCommand(servercmd.music.save, (msg, kwargs, argv) => { - let saveName = argv.join(' '); - this.db.get('SELECT COUNT(*) count FROM playlists WHERE name = ?', [saveName], (err, row) => { - if(err) { - logger.error(err.message); - } - if (!row || row.count === 0) { - this.db.run('INSERT INTO playlists (name, url) VALUES (?, ?)', [saveName, kwargs.url], (err) => { - if (err) - logger.error(err.message); - }); - } else { - this.db.run('UPDATE playlists SET url = ? WHERE name = ?', [kwargs.url, saveName], (err) => { - if (err) - logger.error(err.message); - }); - } + return new Promise((resolve, reject) => { + let saveName = argv.join(' '); + this.db.get('SELECT COUNT(*) count FROM playlists WHERE name = ?', [saveName], (err, row) => { + if(err) { + logger.error(err.message); + } + if (!row || row.count === 0) { + this.db.run('INSERT INTO playlists (name, url) VALUES (?, ?)', [saveName, kwargs.url], (err) => { + if (err) + logger.error(err.message); + else + resolve(`Saved song/playlist as ${saveName}`); + }); + } else { + this.db.run('UPDATE playlists SET url = ? WHERE name = ?', [kwargs.url, saveName], (err) => { + if (err) + logger.error(err.message); + else + resolve(`Saved song/playlist as ${saveName}`); + }); + } + }); }); - return `Saved song/playlist as ${saveName}` }); // saved command - prints out saved playlists this.servant.createCommand(servercmd.music.saved, (msg) => { - let response = ''; - this.db.all('SELECT name, url FROM playlists', (err, rows) => { - if (err) - logger.error(err.message); - for (let row of rows) { - response += `[${row.name}](${row.url})\n`; - } - if (rows.length === 0) { - msg.channel.send(servercmd.music.saved.response.no_saved); - } else { - let richEmbed = new Discord.RichEmbed() - .setTitle('Saved Songs and Playlists') - .setDescription(response); - msg.channel.send('', richEmbed); - } + return new Promise((resolve, reject) => { + let response = ''; + this.db.all('SELECT name, url FROM playlists', (err, rows) => { + if (err) + logger.error(err.message); + for (let row of rows) { + response += `[${row.name}](${row.url})\n`; + } + if (rows.length === 0) { + msg.channel.send(servercmd.music.saved.response.no_saved); + } else { + let richEmbed = new Discord.RichEmbed() + .setTitle('Saved Songs and Playlists') + .setDescription(response); + resolve(richEmbed); + } + }); }); }); } diff --git a/lib/music.js b/lib/music.js index 05984dc..ded5583 100644 --- a/lib/music.js +++ b/lib/music.js @@ -31,16 +31,19 @@ exports.DJ = class { * VoiceChannel is saved as object variable. */ 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(); - }); + return new Promise((resolve, reject) => { + this.voiceChannel = voiceChannel || this.voiceChannel; + if (this.connected) { + this.stop(); + } + logger.verbose(`Connecting to voiceChannel ${this.voiceChannel.name}`); + this.voiceChannel.join().then(connection => { + logger.info(`Connected to Voicechannel ${this.voiceChannel.name}`); + this.conn = connection; + this.checkListeners(); + resolve(); + }).catch((error) => reject(error)); + }) } /** @@ -97,9 +100,13 @@ exports.DJ = class { * @param playnext */ playYouTube(url, playnext) { + /** Commented because it causes an connection overflow error. + * TODO: Decide to either fix this with promises or ignore it because connection checks are performed by the guild handler.**/ + /* 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`); From bcfbada46c78a1318f63086571f1461951ece01d Mon Sep 17 00:00:00 2001 From: Trivernis Date: Tue, 22 Jan 2019 21:21:53 +0100 Subject: [PATCH 03/18] TestScripts 1 - added circleci configuration - added mockobjects for testing without the discord api - added a testscript for the music library --- .circleci/config.yml | 41 ++++++++++++++++++++++++++++++++++++++ package.json | 3 +++ testscripts/mockobjects.js | 38 +++++++++++++++++++++++++++++++++++ testscripts/musicTest.js | 32 +++++++++++++++++++++++++++++ 4 files changed, 114 insertions(+) create mode 100644 .circleci/config.yml create mode 100644 testscripts/mockobjects.js create mode 100644 testscripts/musicTest.js diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..79e9b34 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,41 @@ +# Javascript Node CircleCI 2.0 configuration file +# +# Check https://circleci.com/docs/2.0/language-javascript/ for more details +# +version: 2 +jobs: + build: + docker: + # specify the version you desire here + - image: circleci/node:7.10 + + # Specify service dependencies here if necessary + # CircleCI maintains a library of pre-built images + # documented at https://circleci.com/docs/2.0/circleci-images/ + # - image: circleci/mongo:3.4.4 + + working_directory: ~/repo + + steps: + - checkout + + # Download and cache dependencies + - restore_cache: + keys: + - v1-dependencies-{{ checksum "package.json" }} + # fallback to using the latest cache if no exact match is found + - v1-dependencies- + + - run: + name: Installing dependencies + command: npm install + + - save_cache: + paths: + - node_modules + key: v1-dependencies-{{ checksum "package.json" }} + + # run tests! + - run: + name: Testing ./lib/music + command: node ./test/musicTest.js \ No newline at end of file diff --git a/package.json b/package.json index 3b16f26..f6b6d9a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,9 @@ { "name": "discordbot", "version": "1.0.0", + "scripts": { + "start": "node bot.js" + }, "dependencies": { "args-parser": "1.1.0", "discord.js": "11.4.2", diff --git a/testscripts/mockobjects.js b/testscripts/mockobjects.js new file mode 100644 index 0000000..db66e94 --- /dev/null +++ b/testscripts/mockobjects.js @@ -0,0 +1,38 @@ +exports.mockDispatcher = { + pause: () => console.log('pause();'), + resume: () => console.log('resume();'), + setVolume: (perc) => console.log(`setVolume(${perc});`), + on: (event, callback) => console.log(`on(${event}, ${callback});`), + end: () => console.log('end();') +}; + +exports.mockConnection = { + channel: { + members: { + size: 10 + }, + leave: () => console.log('leave();') + }, + status: 0, + playFile: (fname) => { + console.log(`playFile(${fname});`); + return exports.mockDispatcher; + }, + playStream: (stream, opts) => { + console.log(`playStream(ytdl, ${opts};`); + return exports.mockDispatcher; + }, + disconnect: () => console.log('disconnect();') +}; + +exports.mockVoicechannel = { + name: 'mockVoicechannel', + join: () => { + console.log('join();'); + return new Promise((rs, rj) => rs(exports.mockConnection)); + }, + members: { + size: 10 + }, + leave: () => console.log('leave();') +}; \ No newline at end of file diff --git a/testscripts/musicTest.js b/testscripts/musicTest.js new file mode 100644 index 0000000..917ecec --- /dev/null +++ b/testscripts/musicTest.js @@ -0,0 +1,32 @@ +const music = require('../lib/music.js'), + mockclasses = require('./mockobjects.js'); + +function main() { + let dj = new music.DJ(mockclasses.mockVoicechannel) + music.setLogger({ + error: () => {}, + warn: () => {}, + info: () => {}, + verbose: () => {}, + debug: () => {} + }); + dj.connect().then(() => { + console.log('connected', dj.connected); + dj.playFile('test'); + dj.playYouTube('https://www.youtube.com/watch?v=TEST'); + dj.setVolume(1); + dj.pause(); + dj.resume(); + dj.skip(); + dj.stop(); + dj.shuffle(); + console.log(dj.playlist); + console.log(dj.song); + dj.clear(); + }); +} + +// Executing the main function +if (typeof require !== 'undefined' && require.main === module) { + main(); +} \ No newline at end of file From c4b25987e0086e283a88bff2a22813c58082601c Mon Sep 17 00:00:00 2001 From: Trivernis Date: Tue, 22 Jan 2019 21:24:25 +0100 Subject: [PATCH 04/18] Changes to test configuration - changed relative path to file in circleci configuration --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 79e9b34..cda5966 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -38,4 +38,4 @@ jobs: # run tests! - run: name: Testing ./lib/music - command: node ./test/musicTest.js \ No newline at end of file + command: node ../test/musicTest.js \ No newline at end of file From fbea478d4766e144fcb2bfc4b1bbdf992ac85658 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Tue, 22 Jan 2019 21:24:25 +0100 Subject: [PATCH 05/18] Revert "Changes to test configuration" This reverts commit c4b25987e0086e283a88bff2a22813c58082601c. --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index cda5966..79e9b34 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -38,4 +38,4 @@ jobs: # run tests! - run: name: Testing ./lib/music - command: node ../test/musicTest.js \ No newline at end of file + command: node ./test/musicTest.js \ No newline at end of file From 1797ed3fd7d27cd0ef8440cca3e2f38781fe056b Mon Sep 17 00:00:00 2001 From: Trivernis Date: Tue, 22 Jan 2019 21:26:21 +0100 Subject: [PATCH 06/18] Changes to test configuration - changed directory name for test files in configuration --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 79e9b34..e091506 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -38,4 +38,4 @@ jobs: # run tests! - run: name: Testing ./lib/music - command: node ./test/musicTest.js \ No newline at end of file + command: node ./testscripts/musicTest.js \ No newline at end of file From 2e9d58983f4110ff4a7e4b45acac1ea6e9d22ba2 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Tue, 22 Jan 2019 21:28:40 +0100 Subject: [PATCH 07/18] Changes to test tasks - automatically creates config file --- .circleci/config.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index e091506..7ce1a29 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -35,6 +35,10 @@ jobs: - node_modules key: v1-dependencies-{{ checksum "package.json" }} + - run: + name: Creating config file + command: echo {} >> config.json + # run tests! - run: name: Testing ./lib/music From abd3ccb4c6ddce1fc0bb9a9af3117636011cb42d Mon Sep 17 00:00:00 2001 From: Julius Riegel Date: Wed, 23 Jan 2019 08:49:00 +0100 Subject: [PATCH 08/18] Changed musicTest configuration --- testscripts/musicTest.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/testscripts/musicTest.js b/testscripts/musicTest.js index 917ecec..8708327 100644 --- a/testscripts/musicTest.js +++ b/testscripts/musicTest.js @@ -28,5 +28,11 @@ function main() { // Executing the main function if (typeof require !== 'undefined' && require.main === module) { + process.on('unhandledRejection', (reason, p) => { + console.error('Unhandled Rejection at: Promise', p, 'reason:', reason); + throw Error('Promise rejection'); + }); + + setTimeout(() => process.exit(1), 60000); main(); } \ No newline at end of file From 1db9ec2083ee5bc5b900764b3ada8258abbf0f84 Mon Sep 17 00:00:00 2001 From: Julius Riegel Date: Wed, 23 Jan 2019 10:17:31 +0100 Subject: [PATCH 09/18] Changes to lib/music for CI - changed RegularExpression that resulted in failure --- lib/music.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/music.js b/lib/music.js index ded5583..e00e2bd 100644 --- a/lib/music.js +++ b/lib/music.js @@ -107,7 +107,7 @@ exports.DJ = class { this.connect().then(this.playYouTube(url)); } */ - let plist = url.match(/(?<=\?list=)[\w\-]+/g); + let plist = url.match(/(?<=\?list=)[\w\-]+/); if (plist) { logger.debug(`Adding playlist ${plist} to queue`); ypi(ytapiKey, plist).then(items => { From 33c1c0e3237a3cd3583fa03c2c208405ef0e4a07 Mon Sep 17 00:00:00 2001 From: Julius Riegel Date: Wed, 23 Jan 2019 10:24:10 +0100 Subject: [PATCH 10/18] Changes to circleci config --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 7ce1a29..270c801 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,7 +7,7 @@ jobs: build: docker: # specify the version you desire here - - image: circleci/node:7.10 + - image: circleci/node:10.11 # Specify service dependencies here if necessary # CircleCI maintains a library of pre-built images From 1c8da414fccf6da9c8e6e16aa2ab1e82c4a7068f Mon Sep 17 00:00:00 2001 From: Julius Riegel Date: Wed, 23 Jan 2019 10:25:37 +0100 Subject: [PATCH 11/18] Auto-Exit of musicTest - exiting after last command was invoked --- testscripts/musicTest.js | 1 + 1 file changed, 1 insertion(+) diff --git a/testscripts/musicTest.js b/testscripts/musicTest.js index 8708327..fe9f801 100644 --- a/testscripts/musicTest.js +++ b/testscripts/musicTest.js @@ -23,6 +23,7 @@ function main() { console.log(dj.playlist); console.log(dj.song); dj.clear(); + process.exit(0); }); } From f9cd4db4e12c424ccd0a4c6146e5e0c7241e04a3 Mon Sep 17 00:00:00 2001 From: Julius Riegel Date: Wed, 23 Jan 2019 10:39:59 +0100 Subject: [PATCH 12/18] More verbose test logging --- .circleci/config.yml | 2 +- testscripts/mockobjects.js | 20 ++++++++++---------- testscripts/musicTest.js | 14 +++++++------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 270c801..a28047a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -4,7 +4,7 @@ # version: 2 jobs: - build: + build_and_test: docker: # specify the version you desire here - image: circleci/node:10.11 diff --git a/testscripts/mockobjects.js b/testscripts/mockobjects.js index db66e94..cd34cdf 100644 --- a/testscripts/mockobjects.js +++ b/testscripts/mockobjects.js @@ -1,9 +1,9 @@ exports.mockDispatcher = { - pause: () => console.log('pause();'), - resume: () => console.log('resume();'), - setVolume: (perc) => console.log(`setVolume(${perc});`), - on: (event, callback) => console.log(`on(${event}, ${callback});`), - end: () => console.log('end();') + pause: () => console.log('Dispatcher.pause();'), + resume: () => console.log('Dispatcher.resume();'), + setVolume: (perc) => console.log(`Dispatcher.setVolume(${perc});`), + on: (event, callback) => console.log(`Dispatcher.on(${event}, ${callback});`), + end: () => console.log('Dispatcher.end();') }; exports.mockConnection = { @@ -11,24 +11,24 @@ exports.mockConnection = { members: { size: 10 }, - leave: () => console.log('leave();') + leave: () => console.log('Connection.leave();') }, status: 0, playFile: (fname) => { - console.log(`playFile(${fname});`); + console.log(`Connection.playFile(${fname});`); return exports.mockDispatcher; }, playStream: (stream, opts) => { - console.log(`playStream(ytdl, ${opts};`); + console.log(`Connection.playStream(ytdl, ${opts};`); return exports.mockDispatcher; }, - disconnect: () => console.log('disconnect();') + disconnect: () => console.log('Connection.disconnect();') }; exports.mockVoicechannel = { name: 'mockVoicechannel', join: () => { - console.log('join();'); + console.log('Voicechannel.join();'); return new Promise((rs, rj) => rs(exports.mockConnection)); }, members: { diff --git a/testscripts/musicTest.js b/testscripts/musicTest.js index fe9f801..7212711 100644 --- a/testscripts/musicTest.js +++ b/testscripts/musicTest.js @@ -4,11 +4,11 @@ const music = require('../lib/music.js'), function main() { let dj = new music.DJ(mockclasses.mockVoicechannel) music.setLogger({ - error: () => {}, - warn: () => {}, - info: () => {}, - verbose: () => {}, - debug: () => {} + error: (msg) => console.error('error: ', msg), + warn: (msg) => console.error('warn: ', msg), + info: (msg) => console.log('info: ', msg), + verbose: (msg) => console.log('verbose: ', msg), + debug: (msg) => console.log('debug: ', msg) }); dj.connect().then(() => { console.log('connected', dj.connected); @@ -20,8 +20,8 @@ function main() { dj.skip(); dj.stop(); dj.shuffle(); - console.log(dj.playlist); - console.log(dj.song); + console.log('dj.playlist: ', dj.playlist); + console.log('dj.song: ', dj.song); dj.clear(); process.exit(0); }); From 2ed844da1dc62a1a91624ed7820365273b79d9d8 Mon Sep 17 00:00:00 2001 From: Julius Riegel Date: Wed, 23 Jan 2019 10:41:45 +0100 Subject: [PATCH 13/18] Changes to config.yml - seperate build and test tasks --- .circleci/config.yml | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index a28047a..292c0fe 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -4,7 +4,33 @@ # version: 2 jobs: - build_and_test: + build: + docker: + # specify the version you desire here + - image: circleci/node:10.11 + + working_directory: ~/repo + + steps: + - checkout + + # Download and cache dependencies + - restore_cache: + keys: + - v1-dependencies-{{ checksum "package.json" }} + # fallback to using the latest cache if no exact match is found + - v1-dependencies- + + - run: + name: Installing dependencies + command: npm install + + - save_cache: + paths: + - node_modules + key: v1-dependencies-{{ checksum "package.json" }} + + test: docker: # specify the version you desire here - image: circleci/node:10.11 From 48dda0782ea4fab6f22d8c5d894989776ff66ac5 Mon Sep 17 00:00:00 2001 From: Julius Riegel Date: Wed, 23 Jan 2019 10:44:52 +0100 Subject: [PATCH 14/18] Revert changes to config.yml --- .circleci/config.yml | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 292c0fe..270c801 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -5,32 +5,6 @@ version: 2 jobs: build: - docker: - # specify the version you desire here - - image: circleci/node:10.11 - - working_directory: ~/repo - - steps: - - checkout - - # Download and cache dependencies - - restore_cache: - keys: - - v1-dependencies-{{ checksum "package.json" }} - # fallback to using the latest cache if no exact match is found - - v1-dependencies- - - - run: - name: Installing dependencies - command: npm install - - - save_cache: - paths: - - node_modules - key: v1-dependencies-{{ checksum "package.json" }} - - test: docker: # specify the version you desire here - image: circleci/node:10.11 From fc6cbd336e0d72d50896f2e747eae9bef4b4c947 Mon Sep 17 00:00:00 2001 From: Julius Riegel Date: Wed, 23 Jan 2019 11:17:56 +0100 Subject: [PATCH 15/18] Added cmdTest script - testing lib/cmd --- .circleci/config.yml | 6 +++++- testscripts/cmdTest.js | 39 ++++++++++++++++++++++++++++++++++++++ testscripts/mockobjects.js | 8 ++++++++ testscripts/musicTest.js | 12 +++--------- 4 files changed, 55 insertions(+), 10 deletions(-) create mode 100644 testscripts/cmdTest.js diff --git a/.circleci/config.yml b/.circleci/config.yml index 270c801..c3bc4bb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -42,4 +42,8 @@ jobs: # run tests! - run: name: Testing ./lib/music - command: node ./testscripts/musicTest.js \ No newline at end of file + command: node ./testscripts/musicTest.js + + - run: + name: Testing ./lib/cmd + command: node ./testscripts/cmdTest.js \ No newline at end of file diff --git a/testscripts/cmdTest.js b/testscripts/cmdTest.js new file mode 100644 index 0000000..eae8b1a --- /dev/null +++ b/testscripts/cmdTest.js @@ -0,0 +1,39 @@ +const cmd = require("../lib/cmd.js"), + mockobjects = require("./mockobjects.js"), + servercmd = require('../commands/servercommands'); + +function main() { + cmd.setLogger(mockobjects.mockLogger); + console.log('Creating new servant instance'); + let servant = new cmd.Servant('#'); + console.log('registering all music commands...'); + + for (let [key, value] of Object.entries(servercmd.music)) { + servant.createCommand(value, () => { + console.log(` - invoked ${value.name} callback`); + }); + } + + console.log('parsing and deleting all music commands...'); + for (let [key, value] of Object.entries(servercmd.music)) { + servant.parseCommand({ + content: '#' + value.name, + author: { + tag: undefined + } + }); + servant.removeCommand(value.name); + } + + process.exit(0); +} + +if (typeof require !== "undefined" && require.main === module) { + process.on("unhandledRejection", (reason, p) => { + console.error("Unhandled Rejection at: Promise", p, "reason:", reason); + throw Error("Promise rejection"); + }); + + setTimeout(() => process.exit(1), 60000); + main(); +} \ No newline at end of file diff --git a/testscripts/mockobjects.js b/testscripts/mockobjects.js index cd34cdf..276d795 100644 --- a/testscripts/mockobjects.js +++ b/testscripts/mockobjects.js @@ -1,3 +1,11 @@ +exports.mockLogger = { + error: (msg) => console.error('error: ', msg), + warn: (msg) => console.error('warn: ', msg), + info: (msg) => console.log('info: ', msg), + verbose: (msg) => console.log('verbose: ', msg), + debug: (msg) => console.log('debug: ', msg) +} + exports.mockDispatcher = { pause: () => console.log('Dispatcher.pause();'), resume: () => console.log('Dispatcher.resume();'), diff --git a/testscripts/musicTest.js b/testscripts/musicTest.js index 7212711..1c90aab 100644 --- a/testscripts/musicTest.js +++ b/testscripts/musicTest.js @@ -1,15 +1,9 @@ const music = require('../lib/music.js'), - mockclasses = require('./mockobjects.js'); + mockobjects = require('./mockobjects.js'); function main() { - let dj = new music.DJ(mockclasses.mockVoicechannel) - music.setLogger({ - error: (msg) => console.error('error: ', msg), - warn: (msg) => console.error('warn: ', msg), - info: (msg) => console.log('info: ', msg), - verbose: (msg) => console.log('verbose: ', msg), - debug: (msg) => console.log('debug: ', msg) - }); + let dj = new music.DJ(mockobjects.mockVoicechannel) + music.setLogger(mockobjects.mockLogger); dj.connect().then(() => { console.log('connected', dj.connected); dj.playFile('test'); From 66de9764bd93a06cc5629b983911ffda507d5e67 Mon Sep 17 00:00:00 2001 From: Julius Riegel Date: Wed, 23 Jan 2019 11:40:43 +0100 Subject: [PATCH 16/18] Added lib/guilding testscript --- .circleci/config.yml | 6 ++++- testscripts/guildingTest.js | 44 +++++++++++++++++++++++++++++++++++++ testscripts/mockobjects.js | 21 +++++++++++------- 3 files changed, 62 insertions(+), 9 deletions(-) create mode 100644 testscripts/guildingTest.js diff --git a/.circleci/config.yml b/.circleci/config.yml index c3bc4bb..36e90b2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -46,4 +46,8 @@ jobs: - run: name: Testing ./lib/cmd - command: node ./testscripts/cmdTest.js \ No newline at end of file + command: node ./testscripts/cmdTest.js + + - run: + name: Testing ./lib/guilding + command: node ./testscripts/guildingTest.js \ No newline at end of file diff --git a/testscripts/guildingTest.js b/testscripts/guildingTest.js new file mode 100644 index 0000000..fcad9c1 --- /dev/null +++ b/testscripts/guildingTest.js @@ -0,0 +1,44 @@ +const guilding = require("../lib/guilding.js") + music = require("../lib/music.js"), + mockobjects = require("./mockobjects.js"), + servercmd = require("../commands/servercommands"); + +function main() { + guilding.setLogger(mockobjects.mockLogger); + music.setLogger(mockobjects.mockLogger); + console.log('Creating guildHandler instance'); + let guildHandler = new guilding.GuildHandler('TEST', '#'); + guildHandler.dj = new music.DJ(mockobjects.mockVoicechannel); + + setTimeout(() => { + for (let [key, value] of Object.entries(servercmd.music)) { + guildHandler.handleMessage({ + content: '#' + value.name + ' arg1 arg2 arg3 arg4', + author: { + tag: undefined, + id: 0, + createdTimestamp: new Date(), + username: 'TEST' + }, + member: { + voiceChannel: mockobjects.mockVoicechannel + }, + channel: mockobjects.mockChannel, + reply: mockobjects.mockChannel.send + }); + } + + guildHandler.destroy(); + process.exit(0); + }, 1000); +} + +if (typeof require !== "undefined" && require.main === module) { + process.on("unhandledRejection", (reason, p) => { + console.error("Unhandled Rejection at: Promise", p, "reason:", reason); + throw Error("Promise rejection"); + }); + + setTimeout(() => process.exit(1), 60000); + main(); +} \ No newline at end of file diff --git a/testscripts/mockobjects.js b/testscripts/mockobjects.js index 276d795..86e7ed0 100644 --- a/testscripts/mockobjects.js +++ b/testscripts/mockobjects.js @@ -1,10 +1,11 @@ exports.mockLogger = { - error: (msg) => console.error('error: ', msg), - warn: (msg) => console.error('warn: ', msg), - info: (msg) => console.log('info: ', msg), - verbose: (msg) => console.log('verbose: ', msg), - debug: (msg) => console.log('debug: ', msg) -} + error: msg => raise(msg), + warn: msg => console.error("warn: ", msg), + warning: msg => console.error("warn: ", msg), + info: msg => console.log("info: ", msg), + verbose: msg => console.log("verbose: ", msg), + debug: msg => console.log("debug: ", msg) +}; exports.mockDispatcher = { pause: () => console.log('Dispatcher.pause();'), @@ -42,5 +43,9 @@ exports.mockVoicechannel = { members: { size: 10 }, - leave: () => console.log('leave();') -}; \ No newline at end of file + leave: () => console.log('Voicechannel.leave();') +}; + +exports.mockChannel = { + send: (msg) => console.log('Send: ', msg) +} \ No newline at end of file From 09f99327da6a2333826726e6987e1497928f1481 Mon Sep 17 00:00:00 2001 From: Julius Riegel Date: Wed, 23 Jan 2019 11:42:58 +0100 Subject: [PATCH 17/18] Adding additional dependency installation task --- .circleci/config.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 36e90b2..9188ea1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -30,6 +30,10 @@ jobs: name: Installing dependencies command: npm install + - run: + name: installing additional dependencies + command: npm install sqlite3 + - save_cache: paths: - node_modules From e5fc4b306e13f4ba58984d26595696c0247ca521 Mon Sep 17 00:00:00 2001 From: Julius Riegel Date: Wed, 23 Jan 2019 14:44:41 +0100 Subject: [PATCH 18/18] Added CircleCi and Codefactor Badge to Readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a28a6e9..d1ae1bc 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -discordbot +discordbot [![CircleCI](https://circleci.com/gh/Trivernis/discordbot.js.svg?style=svg)](https://circleci.com/gh/Trivernis/discordbot.js) [![CodeFactor](https://www.codefactor.io/repository/github/trivernis/discordbot.js/badge)](https://www.codefactor.io/repository/github/trivernis/discordbot.js) === A bot that does the discord thing.