From a712f41a55ef223b8be3cf12d69cbe07bb0e6426 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Fri, 25 Jan 2019 18:30:43 +0100 Subject: [PATCH 01/13] Changes to Config Syntax - moved the discord token and youtube api key to ` api.botToken` and `api.youTubeApiKey` in the `config.json` file - added `lib.utils.ConfigChecker` class to check for missing required attributes in the config - Updated README to new circumstances --- README.md | 25 ++++++++++++++++------ bot.js | 12 +++++++++-- lib/music.js | 2 +- lib/utils.js | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 89 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 169c320..9dfa28e 100644 --- a/README.md +++ b/README.md @@ -3,19 +3,21 @@ discordbot [![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blu A bot that does the discord thing. -`node bot.js [--token=] [--ytapi=] [--owner=] [--prefix=] [--game=]` +`node bot.js [--token=] [--ytapi=] [--owner=] [--prefix=] [--game=] [-i=]` The arguments are optional because the token and youtube-api-key that the bot needs to run can also be defined in the config.json in the bot's directory: ```json5 // config.json { "prefix": "_", - "token": "DISCORD BOT TOKEN", - "ytapikey": "YOUTUBE API KEY", - "presence": "THE DEFAULT GAME IF NO presences.txt IS FOUND IN ./data/", - "presence_duration": 300000, + "presence": "STRING", // this will be shown when no presences are set in data/presences.txt + "presence_duration": 300000, // how long does the bot have one presence + "api": { + "botToken": "YOUR DISCORD BOT TOKEN", + "youTubeApiKey": "YOUR YOUTUBE API KEY" + }, "owners": [ - "SPECIFY A LIST OF BOT-OWNERS" + "DISCORD NAME" // specify a list of bot owners that can use the owner commands ], "music": { "timeout": 300000 @@ -23,6 +25,17 @@ The arguments are optional because the token and youtube-api-key that the bot ne } ``` +If the keys are missing from the config file, the bot exits. This behaviour can be deactivated by setting the `-i` commandline flag. + +Keys +--- + +You can get the API-Keys here: + +[Discord Bot Token](https://discordapp.com/developers) + +[YouTube API Key](https://console.developers.google.com) + Features --- diff --git a/bot.js b/bot.js index eec117c..d8eee22 100644 --- a/bot.js +++ b/bot.js @@ -7,8 +7,8 @@ const Discord = require("discord.js"), config = require('./config.json'), client = new Discord.Client(), args = require('args-parser')(process.argv), - authToken = args.token || config.token, - prefix = args.prefix || config.prefix, + authToken = args.token || config.api.botToken, + prefix = args.prefix || config.prefix || '~', gamepresence = args.game || config.presence; let presences = [], // loaded from presences.txt file if the file exists @@ -20,6 +20,14 @@ function main() { client.destroy(); }); cmd.setLogger(logger); + let configVerifyer = new utils.ConfigVerifyer(config, [ + "api.botToken", "api.youTubeApiKey" + ]); + if (configVerifyer.verifyConfig(logger)) { + if (!args.i) { + process.exit(1); + } + } guilding.setLogger(logger); cmd.init(prefix); registerCommands(); diff --git a/lib/music.js b/lib/music.js index 47b4069..9d76d24 100644 --- a/lib/music.js +++ b/lib/music.js @@ -5,7 +5,7 @@ const Discord = require("discord.js"), args = require('args-parser')(process.argv), config = require('../config.json'), utils = require('./utils.js'), - ytapiKey = args.ytapi || config.ytapikey; + ytapiKey = args.ytapi || config.api.youTubeApiKey; /* Variable Definition */ let logger = require('winston'); let djs = {}; diff --git a/lib/utils.js b/lib/utils.js index d9333e7..db5f33b 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -26,6 +26,24 @@ exports.getExtension = function (filename) { } }; +/** + * Walks the path to the objects attribute and returns the value. + * @param object + * @param attributePath + * @returns {undefined/Object} + */ +exports.objectDeepFind = function (object, attributePath) { + let current = object, + paths = attributePath.split('.'); + for (let path of paths) { + if (current[path]) + current = current[path]; + else + return undefined; + } + return current; +}; + /** * lets you define a cleanup for your program exit * @param {Function} callback the cleanup function @@ -73,6 +91,8 @@ exports.dirExistence = function (path, callback) { }) }; +/* Classes */ + exports.YouTube = class { /** * returns if an url is a valid youtube url (without checking for an entity id) @@ -153,4 +173,43 @@ exports.YouTube = class { static getVideoThumbnailUrlFromUrl(url) { return `https://i3.ytimg.com/vi/${exports.YouTube.getVideoIdFromUrl(url)}/maxresdefault.jpg` } +}; + +exports.ConfigVerifyer = class { + /** + * @param confFile {String} the file that needs to be verified + * @param required {Array} the attributes that are required for the bot to work + * @param optional {Array} the attributes that are optional (and may provide extra features) + */ + constructor(confObj, required) { + this.config = confObj; + this.requiredAttributes = required; + } + + /** + * @param promtMissing {Boolean} true - everything is fine; false - missing attributes + */ + verifyConfig(logger) { + let missing = []; + for (let reqAttr of this.requiredAttributes) { + if (!exports.objectDeepFind(this.config, reqAttr)) + missing.push(reqAttr); + } + this.missingAttributes = missing; + this.logMissing(logger); + return this.missingAttributes.length > 0; + } + + /** + * Promts the user which attributes are missing + * @param logger + */ + logMissing(logger) { + if (this.missingAttributes.length > 0) { + logger.error("Missing required Attributes"); + for (let misAttr of this.missingAttributes) { + logger.warn(`Missing Attribute ${misAttr} in config.json`); + } + } + } }; \ No newline at end of file From d5d5c121262bd4b8c5d436fc8695ec1df3f39964 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Fri, 25 Jan 2019 21:45:08 +0100 Subject: [PATCH 02/13] Restructured Tests - removed previous testfiles - added test.js where all tests are stored - added tests for lib/utils - added standard test config - added test config to devDependencies in package.json - reconfigured circleci configuration --- .circleci/config.yml | 18 +-- .gitignore | 1 + lib/utils.js | 46 ++++--- package.json | 11 +- {testscripts => test}/cmdTest.js | 0 {testscripts => test}/guildingTest.js | 0 {testscripts => test}/mockobjects.js | 7 +- test/test-setup.spec.js | 10 ++ test/test.js | 190 ++++++++++++++++++++++++++ testscripts/musicTest.js | 33 ----- 10 files changed, 248 insertions(+), 68 deletions(-) rename {testscripts => test}/cmdTest.js (100%) rename {testscripts => test}/guildingTest.js (100%) rename {testscripts => test}/mockobjects.js (94%) create mode 100644 test/test-setup.spec.js create mode 100644 test/test.js delete mode 100644 testscripts/musicTest.js diff --git a/.circleci/config.yml b/.circleci/config.yml index 9f11725..8df2d27 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -30,6 +30,10 @@ jobs: name: Installing dependencies command: npm install + - run: + name: Installing dependencies + command: npm install --save-dev + - run: name: installing additional dependencies command: npm install sqlite3 @@ -41,17 +45,9 @@ jobs: - run: name: Creating config file - command: echo {} >> config.json + command: echo {api:{}} >> config.json # run tests! - run: - name: Testing ./lib/music - command: node ./testscripts/musicTest.js - - - run: - name: Testing ./lib/cmd - command: node ./testscripts/cmdTest.js - - - run: - name: Testing ./lib/guilding - command: node ./testscripts/guildingTest.js \ No newline at end of file + name: Unit testing + command: npm test \ No newline at end of file diff --git a/.gitignore b/.gitignore index 7472e1c..00b2077 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .log .idea +.nyc_output data package-lock.json node_modules diff --git a/lib/utils.js b/lib/utils.js index db5f33b..b230a07 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -36,7 +36,7 @@ exports.objectDeepFind = function (object, attributePath) { let current = object, paths = attributePath.split('.'); for (let path of paths) { - if (current[path]) + if (current[path] !== undefined && current[path] !== null) current = current[path]; else return undefined; @@ -100,8 +100,8 @@ exports.YouTube = class { * @returns {boolean} */ static isValidUrl(url) { - return /https?:\/\/www.youtube.com\/(watch\?v=|playlist\?list=)/g.test(url) || - /https?:\/\/youtu.be\//g.test(url); + return /https?:\/\/(www\.)?youtube\.com\/(watch\?v=|playlist\?list=)/g.test(url) || + /https?:\/\/youtu\.be\//g.test(url); } /** @@ -110,8 +110,8 @@ exports.YouTube = class { * @returns {boolean} */ static isValidEntityUrl(url) { - return /https?:\/\/www.youtube.com\/(watch\?v=.+?|playlist\?list=.+?)/g.test(url) || - /https?:\/\/youtu.be\/.+?/g.test(url); + return /https?:\/\/(www\.)?youtube\.com\/(watch\?v=.+?|playlist\?list=.+?)/g.test(url) || + /https?:\/\/youtu\.be\/.+?/g.test(url); } /** @@ -120,7 +120,7 @@ exports.YouTube = class { * @returns {boolean} */ static isValidPlaylistUrl(url) { - return /https?:\/\/www.youtube.com\/playlist\?list=.+?/g.test(url); + return /https?:\/\/(www\.)?youtube\.com\/playlist\?list=.+?/g.test(url); } /** @@ -129,15 +129,17 @@ exports.YouTube = class { * @returns {boolean} */ static isValidVideoUrl(url) { - return /https?:\/\/www.youtube.com\/watch\?v=.+?/g.test(url) || /https?:\/\/youtu.be\/.+?/g.test(url); + return /https?:\/\/(www\.)?youtube\.com\/watch\?v=.+?/g.test(url) || /https?:\/\/youtu\.be\/.+?/g.test(url); } /** * Returns the id for a youtube video stripped from the url * @param url - * @returns {RegExpMatchArray} + * @returns {String} */ static getPlaylistIdFromUrl(url) { + if (!exports.YouTube.isValidPlaylistUrl(url)) + return null; let matches = url.match(/(?<=\?list=)[\w\-]+/); if (matches) return matches[0]; @@ -148,13 +150,21 @@ exports.YouTube = class { /** * Returns the id for a youtube video stripped from the url * @param url + * @return {String} */ static getVideoIdFromUrl(url) { - let matches = url.match(/(?<=\?v=)[\w\-]+/); - if (matches) - return matches[0]; - else + if (!exports.YouTube.isValidVideoUrl(url)) return null; + let matches1 = url.match(/(?<=\?v=)[\w\-]+/); + if (matches1) + return matches1[0]; + else { + let matches2 = url.match(/(?<=youtu\.be\/)[\w\-]+/); + if (matches2) + return matches2[0]; + else + return null; + } } /** @@ -177,9 +187,8 @@ exports.YouTube = class { exports.ConfigVerifyer = class { /** - * @param confFile {String} the file that needs to be verified + * @param confObj * @param required {Array} the attributes that are required for the bot to work - * @param optional {Array} the attributes that are optional (and may provide extra features) */ constructor(confObj, required) { this.config = confObj; @@ -192,12 +201,12 @@ exports.ConfigVerifyer = class { verifyConfig(logger) { let missing = []; for (let reqAttr of this.requiredAttributes) { - if (!exports.objectDeepFind(this.config, reqAttr)) + if (exports.objectDeepFind(this.config, reqAttr) === undefined) missing.push(reqAttr); } this.missingAttributes = missing; this.logMissing(logger); - return this.missingAttributes.length > 0; + return this.missingAttributes.length === 0; } /** @@ -206,10 +215,7 @@ exports.ConfigVerifyer = class { */ logMissing(logger) { if (this.missingAttributes.length > 0) { - logger.error("Missing required Attributes"); - for (let misAttr of this.missingAttributes) { - logger.warn(`Missing Attribute ${misAttr} in config.json`); - } + logger.error(`Missing required Attributes ${this.missingAttributes.join(', ')}`); } } }; \ No newline at end of file diff --git a/package.json b/package.json index f6b6d9a..898f8b9 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,9 @@ "name": "discordbot", "version": "1.0.0", "scripts": { - "start": "node bot.js" + "start": "node bot.js", + "test": "mocha", + "test-unit": "NODE_ENV=test mocha '/**/*.spec.js'" }, "dependencies": { "args-parser": "1.1.0", @@ -15,5 +17,12 @@ "winston-daily-rotate-file": "3.6.0", "youtube-playlist-info": "1.1.2", "ytdl-core": "0.29.1" + }, + "devDependencies": { + "assert": "^1.4.1", + "chai": "^4.2.0", + "mocha": "^5.2.0", + "nyc": "^13.1.0", + "sinon": "^7.2.3" } } diff --git a/testscripts/cmdTest.js b/test/cmdTest.js similarity index 100% rename from testscripts/cmdTest.js rename to test/cmdTest.js diff --git a/testscripts/guildingTest.js b/test/guildingTest.js similarity index 100% rename from testscripts/guildingTest.js rename to test/guildingTest.js diff --git a/testscripts/mockobjects.js b/test/mockobjects.js similarity index 94% rename from testscripts/mockobjects.js rename to test/mockobjects.js index 86e7ed0..0316b74 100644 --- a/testscripts/mockobjects.js +++ b/test/mockobjects.js @@ -1,7 +1,8 @@ exports.mockLogger = { - error: msg => raise(msg), + error: msg => { + throw new Error(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) @@ -48,4 +49,4 @@ exports.mockVoicechannel = { exports.mockChannel = { send: (msg) => console.log('Send: ', msg) -} \ No newline at end of file +}; \ No newline at end of file diff --git a/test/test-setup.spec.js b/test/test-setup.spec.js new file mode 100644 index 0000000..f7c976a --- /dev/null +++ b/test/test-setup.spec.js @@ -0,0 +1,10 @@ +const sinon = require('sinon'), + chai = require('chai'); + +beforeEach(() => { + this.sandbox = sinon.createSandbox(); +}); + +afterEach(() => { + this.sandbox.restore(); +}); \ No newline at end of file diff --git a/test/test.js b/test/test.js new file mode 100644 index 0000000..76127c5 --- /dev/null +++ b/test/test.js @@ -0,0 +1,190 @@ +const mockobjects = require('./mockobjects.js'), + sinon = require('sinon'); +let Discord = require("discord.js"), + assert = require('assert'), + config = require('../config.json'); + +describe('lib/utils', function() { + const utils = require('../lib/utils.js'); + + describe('#YouTube', function() { + + it('returns if an url is valid', function(done) { + assert(utils.YouTube.isValidUrl('https://www.youtube.com/watch?v=VID-ID')); + assert(utils.YouTube.isValidUrl('https://youtube.com/playlist?list=PL-ID')); + assert(utils.YouTube.isValidUrl('https://youtube.com/watch?v=')); + assert(utils.YouTube.isValidUrl('https://www.youtube.com/playlist?list=')); + assert(utils.YouTube.isValidUrl('https://youtu.be/VIDID')); + assert(utils.YouTube.isValidUrl('https://youtu.be/')); + assert(utils.YouTube.isValidUrl('http://youtube.com/watch?v=VID-ID')); + assert(utils.YouTube.isValidUrl('http://youtube.com/playlist?list=PL-ID')); + assert(utils.YouTube.isValidUrl('http://youtube.com/watch?v=')); + assert(utils.YouTube.isValidUrl('http://youtube.com/playlist?list=')); + assert(!utils.YouTube.isValidUrl('https://github.com')); + assert(!utils.YouTube.isValidUrl('notevenanurl')); + done(); + }); + + it('returns if an url is a valid entity url', function(done) { + assert(utils.YouTube.isValidEntityUrl('https://youtube.com/watch?v=VID-ID')); + assert(utils.YouTube.isValidEntityUrl('https://youtube.com/playlist?list=PL-ID')); + assert(utils.YouTube.isValidEntityUrl('https://youtu.be/VIDID')); + assert(utils.YouTube.isValidEntityUrl('http://www.youtube.com/watch?v=VID-ID')); + assert(utils.YouTube.isValidEntityUrl('http://youtube.com/playlist?list=PL-ID')); + assert(!utils.YouTube.isValidEntityUrl('https://youtube.com/watch?v=')); + assert(!utils.YouTube.isValidEntityUrl('https://youtube.com/playlist?list=')); + assert(!utils.YouTube.isValidEntityUrl('https://youtu.be/')); + assert(!utils.YouTube.isValidEntityUrl('https://github.com')); + assert(!utils.YouTube.isValidEntityUrl('notevenanurl')); + done(); + }); + + it('returns if an url is a valid playlist url', function(done) { + assert(!utils.YouTube.isValidPlaylistUrl('https://youtube.com/watch?v=VID-ID')); + assert(utils.YouTube.isValidPlaylistUrl('https://youtube.com/playlist?list=PL-ID')); + assert(!utils.YouTube.isValidPlaylistUrl('https://youtu.be/VIDID')); + assert(!utils.YouTube.isValidPlaylistUrl('http://www.youtube.com/watch?v=VID-ID')); + assert(utils.YouTube.isValidPlaylistUrl('http://youtube.com/playlist?list=PL-ID')); + assert(!utils.YouTube.isValidPlaylistUrl('http://youtube.com/playlist?list=')); + assert(!utils.YouTube.isValidPlaylistUrl('https://github.com')); + assert(!utils.YouTube.isValidPlaylistUrl('notevenanurl')); + done(); + }); + + it('returns if an url is a valid video url', function(done) { + assert(utils.YouTube.isValidVideoUrl('https://youtube.com/watch?v=VID-ID')); + assert(!utils.YouTube.isValidVideoUrl('https://youtube.com/playlist?list=PL-ID')); + assert(utils.YouTube.isValidVideoUrl('https://youtu.be/VIDID')); + assert(utils.YouTube.isValidVideoUrl('http://www.youtube.com/watch?v=VID-ID')); + assert(!utils.YouTube.isValidVideoUrl('http://youtube.com/playlist?list=PL-ID')); + assert(!utils.YouTube.isValidVideoUrl('https://youtube.com/watch?v=')); + assert(!utils.YouTube.isValidVideoUrl('https://youtu.be/')); + assert(!utils.YouTube.isValidVideoUrl('https://github.com')); + assert(!utils.YouTube.isValidVideoUrl('notevenanurl')); + done(); + }); + + it('returns the id for a playlist url', function(done) { + let getPlId = utils.YouTube.getPlaylistIdFromUrl; + assert('PL-ID' === getPlId('https://youtube.com/playlist?list=PL-ID')); + assert('PL-ID' === getPlId('http://youtube.com/playlist?list=PL-ID')); + assert('PL-ID' === getPlId('https://www.youtube.com/playlist?list=PL-ID')); + assert('PL-ID' === getPlId('https://www.youtube.com/playlist?list=PL-ID')); + assert(null === getPlId('https://www.youtube.com/playlist?list=')); + done(); + }); + + it('returns the id for a video url', function(done) { + let getVidId = utils.YouTube.getVideoIdFromUrl; + assert('VID-ID' === getVidId('https://youtube.com/watch?v=VID-ID')); + assert('VID-ID' === getVidId('http://youtube.com/watch?v=VID-ID')); + assert('VID-ID' === getVidId('https://www.youtube.com/watch?v=VID-ID')); + assert('VID-ID' === getVidId('https://youtu.be/VID-ID')); + assert(null === getVidId('https://www.faketube.com/watch?v=VID-ID')); + assert(null === getVidId('tu.be/VID-ID')); + assert(null === getVidId('https://youtube.com/watch?v=')); + assert(null === getVidId('https://youtu.be/')); + done(); + }); + + it('returns the video url for an id', function(done) { + let getVid4Id = utils.YouTube.getVideoUrlFromId; + assert('https://www.youtube.com/watch?v=VID-ID', getVid4Id('VID-ID')); + assert('https://www.youtube.com/watch?v=12345567885432', getVid4Id('12345567885432')); + done(); + }); + + it('returns the thumbnail url for a video url', function(done) { + let getVid4Id = utils.YouTube.getVideoUrlFromId; + let getTh4Id = utils.YouTube.getVideoThumbnailUrlFromUrl; + assert('https://i3.ytimg.com/vi/VIDID/maxresdefault.jpg', getTh4Id(getVid4Id('VIDID'))); + assert('https://i3.ytimg.com/vi/1234/maxresdefault.jpg', getTh4Id(getVid4Id('1234'))); + done(); + }) + }); + + describe('#ConfigVerifyer', function() { + it('verifies correct configs', function(done) { + const testObj = { + 'key1': { + 'key2': 'value2', + 'key3': 'value3' + }, + 'key4': [], + 'key5': false, + 'key6': 'a longer string', + 'key7': { + 'key8': [{ + 'key9': 'okay...' + }] + } + }; + let confVer = new utils.ConfigVerifyer(testObj, ['key1', 'key1.key3']); + assert(confVer.verifyConfig(mockobjects.mockLogger)); + confVer = new utils.ConfigVerifyer(testObj, ['key1', 'key1.key2', 'key7.key8.0.key9']); + assert(confVer.verifyConfig(mockobjects.mockLogger)); + confVer = new utils.ConfigVerifyer(testObj, ['key4', 'key1.key2', 'key5', 'key7']); + assert(confVer.verifyConfig(mockobjects.mockLogger)); + done(); + }); + + it('rejects invalid configs', function(done) { + const testObj = { + }; + let modifiedMockLogger = mockobjects.mockLogger; + modifiedMockLogger.error = (msg) => {}; + let confVer = new utils.ConfigVerifyer(testObj, ['key1', 'key1.key3']); + assert(!confVer.verifyConfig(mockobjects.mockLogger)); + confVer = new utils.ConfigVerifyer(testObj, ['key1', 'key1.key2', 'key7.key8.0.key9']); + assert(!confVer.verifyConfig(mockobjects.mockLogger)); + done(); + }) + }); +}); + +// TODO: Repair and activate later +describe('The dj class', function *() { + const music = require('../lib/music'); + let ytdl = require("ytdl-core"); + let yttl = require('get-youtube-title'); + let ypi = require('youtube-playlist-info'); + + let ytdlMock = sinon.mock(ytdl); + let yttlMock = sinon.mock(yttl); + let ypiMock = sinon.mock(ypi); + + it('connects to a VoiceChannel', function () { + let dj = new music.DJ(mockobjects.mockVoicechannel); + dj.connect(); + + console.log(dj.connected); + + assert(dj.connected); + }); + + it('listens on Repeat', function() { + let dj = new music.DJ(mockobjects.mockVoicechannel); + dj.current = {'url': '', 'title': ''}; + dj.listenOnRepeat = true; + + assert(dj.repeat); + assert(dj.queue.length > 0); + }); + + it('plays Files', function () { + + let dj = new music.DJ(mockobjects.mockVoicechannel); + dj.connect(); + dj.playFile(); + + assert(dj.playing); + }); + + it('plays YouTube urls', function () { + + let dj = new music.DJ(mockobjects.mockVoicechannel); + dj.playYouTube('http://www.youtube.com/watch?v=abc'); + + assert(dj.playing); + }); +}); \ No newline at end of file diff --git a/testscripts/musicTest.js b/testscripts/musicTest.js deleted file mode 100644 index 1c90aab..0000000 --- a/testscripts/musicTest.js +++ /dev/null @@ -1,33 +0,0 @@ -const music = require('../lib/music.js'), - mockobjects = require('./mockobjects.js'); - -function main() { - let dj = new music.DJ(mockobjects.mockVoicechannel) - music.setLogger(mockobjects.mockLogger); - 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: ', dj.playlist); - console.log('dj.song: ', dj.song); - dj.clear(); - process.exit(0); - }); -} - -// 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 b8a92134fed00ec0f32be516484c77bca614def2 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Fri, 25 Jan 2019 21:47:30 +0100 Subject: [PATCH 03/13] Fixed CircleCi configuration - fixed invalid json for created config.json --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 8df2d27..520827c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -45,7 +45,7 @@ jobs: - run: name: Creating config file - command: echo {api:{}} >> config.json + command: echo {\"api\":{}} >> config.json # run tests! - run: From 990628244ef69d35af308b0fe8057a0c516e99cc Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sat, 26 Jan 2019 11:37:07 +0100 Subject: [PATCH 04/13] Added Tests - added tests for the dj class - moved to rewire for mocking --- lib/utils.js | 11 ++- package.json | 1 + test/test-setup.spec.js | 5 +- test/test.js | 170 +++++++++++++++++++++++++++++++++------- 4 files changed, 154 insertions(+), 33 deletions(-) diff --git a/lib/utils.js b/lib/utils.js index b230a07..5653ce9 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -15,11 +15,14 @@ let sysData = {}; * @return {String} A string that represents the file-extension. */ exports.getExtension = function (filename) { - if (!filename) return null; + if (!filename) + return null; try { - let exts = filename.match(/\.[a-z]+/g); // get the extension by using regex - if (exts) return exts[exts.length - 1]; // return the found extension - else return null; // return null if no extension could be found + let exts = filename.match(/\.\w+/g); // get the extension by using regex + if (exts) + return exts.pop(); // return the found extension + else + return null; // return null if no extension could be found } catch (error) { console.error(error); return null; diff --git a/package.json b/package.json index 898f8b9..b56104e 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "chai": "^4.2.0", "mocha": "^5.2.0", "nyc": "^13.1.0", + "rewire": "^4.0.1", "sinon": "^7.2.3" } } diff --git a/test/test-setup.spec.js b/test/test-setup.spec.js index f7c976a..4081afd 100644 --- a/test/test-setup.spec.js +++ b/test/test-setup.spec.js @@ -1,10 +1,13 @@ const sinon = require('sinon'), - chai = require('chai'); + chai = require('chai'), + rewiremock = require('rewiremock').default; beforeEach(() => { this.sandbox = sinon.createSandbox(); + rewiremock.enable(); }); afterEach(() => { this.sandbox.restore(); + rewiremock.disable(); }); \ No newline at end of file diff --git a/test/test.js b/test/test.js index 76127c5..5a48b0f 100644 --- a/test/test.js +++ b/test/test.js @@ -1,12 +1,30 @@ const mockobjects = require('./mockobjects.js'), - sinon = require('sinon'); -let Discord = require("discord.js"), + sinon = require('sinon'), assert = require('assert'), - config = require('../config.json'); + rewire = require('rewire'); +let Discord = require("discord.js"); describe('lib/utils', function() { const utils = require('../lib/utils.js'); + describe('#getExtension', function() { + it('returns the correct extension for a filename', function(done) { + assert(utils.getExtension('test.txt') === '.txt'); + assert(utils.getExtension('test.tar.gz') === '.gz'); + assert(utils.getExtension('../lib/utils.js') === '.js'); + assert(utils.getExtension('.gitignore') === '.gitignore'); + done(); + }); + + it('returns null if the file has no extension or is no file', function(done) { + assert(utils.getExtension('filenameisstrange') === null); + assert(utils.getExtension('...') === null); + assert(utils.getExtension(Object.create({})) === null); + assert(utils.getExtension(null) === null); + done(); + }); + }); + describe('#YouTube', function() { it('returns if an url is valid', function(done) { @@ -134,35 +152,39 @@ describe('lib/utils', function() { let modifiedMockLogger = mockobjects.mockLogger; modifiedMockLogger.error = (msg) => {}; let confVer = new utils.ConfigVerifyer(testObj, ['key1', 'key1.key3']); - assert(!confVer.verifyConfig(mockobjects.mockLogger)); + assert(!confVer.verifyConfig(modifiedMockLogger)); confVer = new utils.ConfigVerifyer(testObj, ['key1', 'key1.key2', 'key7.key8.0.key9']); - assert(!confVer.verifyConfig(mockobjects.mockLogger)); + assert(!confVer.verifyConfig(modifiedMockLogger)); done(); }) }); }); -// TODO: Repair and activate later -describe('The dj class', function *() { - const music = require('../lib/music'); - let ytdl = require("ytdl-core"); - let yttl = require('get-youtube-title'); - let ypi = require('youtube-playlist-info'); +describe('The dj class', function () { + const music = rewire('../lib/music'); + const Readable = require('stream').Readable; - let ytdlMock = sinon.mock(ytdl); - let yttlMock = sinon.mock(yttl); - let ypiMock = sinon.mock(ypi); + music.__set__("logger", mockobjects.mockLogger); + music.__set__("yttl", (id, cb) => { + cb(null, 'test'); + }); + music.__set__('ytdl', () => { + let s = new Readable(); + s._read = () => {}; + s.push('chunkofdataabc'); + s.push(null); + return s; + }); - it('connects to a VoiceChannel', function () { + it('connects to a VoiceChannel', function (done) { let dj = new music.DJ(mockobjects.mockVoicechannel); - dj.connect(); - - console.log(dj.connected); - - assert(dj.connected); + dj.connect().then(()=> { + assert(dj.connected); + done(); + }); }); - it('listens on Repeat', function() { + it('listens on Repeat', function () { let dj = new music.DJ(mockobjects.mockVoicechannel); dj.current = {'url': '', 'title': ''}; dj.listenOnRepeat = true; @@ -171,20 +193,112 @@ describe('The dj class', function *() { assert(dj.queue.length > 0); }); - it('plays Files', function () { + it('plays Files', function (done) { + + let dj = new music.DJ(mockobjects.mockVoicechannel); + dj.connect().then(() => { + dj.playFile(); + assert(dj.playing); + done(); + }); + }); + + it('plays YouTube urls', function (done) { + let dj = new music.DJ(mockobjects.mockVoicechannel); + dj.connect().then(() => { + dj.playYouTube('http://www.youtube.com/watch?v=ABCDEFGHIJK'); + }); + + setTimeout(() => { + assert(dj.playing); + done(); + }, 211); + }); + + it('gets the video name', function (done) { + let dj = new music.DJ(mockobjects.mockVoicechannel); + dj.getVideoName('http://www.youtube.com/watch?v=ABCDEFGHIJK').then((name) => { + assert(name === 'test'); + done(); + }) + }); + + it('sets the volume', function(done) { + let dj = new music.DJ(mockobjects.mockVoicechannel); + dj.connect().then(() => { + dj.playFile(); + dj.setVolume(100); + assert(dj.volume === 100); + done(); + }) + }); + it('pauses playback', function(done) { let dj = new music.DJ(mockobjects.mockVoicechannel); - dj.connect(); - dj.playFile(); + dj.connect().then(() => { + dj.playFile(); + dj.pause(); + done(); + }) + }); - assert(dj.playing); + it('resumes playback', function(done) { + let dj = new music.DJ(mockobjects.mockVoicechannel); + dj.connect().then(() => { + dj.playFile(); + dj.resume(); + done(); + }) }); - it('plays YouTube urls', function () { + it('stops playback', function(done) { + let dj = new music.DJ(mockobjects.mockVoicechannel); + dj.connect().then(() => { + dj.playFile(); + assert(dj.playing); + dj.stop(); + assert(!dj.conn && !dj.disp); + done(); + }); + }); + it('skips songs', function(done) { let dj = new music.DJ(mockobjects.mockVoicechannel); - dj.playYouTube('http://www.youtube.com/watch?v=abc'); + dj.connect().then(() => { + dj.playYouTube('http://www.youtube.com/watch?v=ABCDEFGHIJK'); + dj.playYouTube('http://www.youtube.com/watch?v=ABCDEFGHIJK'); + dj.playYouTube('http://www.youtube.com/watch?v=ABCDEFGHIJK'); + dj.playYouTube('http://www.youtube.com/watch?v=ABCDEFGHIJK'); + dj.skip(); + dj.skip(); + done(); + }); + }); - assert(dj.playing); + it('returns a playlist', function(done) { + let dj = new music.DJ(mockobjects.mockVoicechannel); + dj.connect().then(() => { + dj.queue = [{ + 'title': 'title', + 'url': 'http://www.youtube.com/watch?v=ABCDEFGHIJK'}, { + 'title': 'title', + 'url': 'http://www.youtube.com/watch?v=ABCDEFGHIJK'}]; + assert(dj.playlist.length > 0); + done(); + }); }); + + it('clears the queue', function(done) { + let dj = new music.DJ(mockobjects.mockVoicechannel); + dj.connect().then(() => { + dj.queue = [{ + 'title': 'title', + 'url': 'http://www.youtube.com/watch?v=ABCDEFGHIJK'}, { + 'title': 'title', + 'url': 'http://www.youtube.com/watch?v=ABCDEFGHIJK'}]; + dj.clear(); + assert(dj.queue.length === 0); + done(); + }); + }) }); \ No newline at end of file From 4f1367e27f5b17a8429c1ac099b62318c7835c1d Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sat, 26 Jan 2019 11:38:11 +0100 Subject: [PATCH 05/13] Removed unused import --- test/test-setup.spec.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/test-setup.spec.js b/test/test-setup.spec.js index 4081afd..0d558dc 100644 --- a/test/test-setup.spec.js +++ b/test/test-setup.spec.js @@ -1,6 +1,5 @@ const sinon = require('sinon'), - chai = require('chai'), - rewiremock = require('rewiremock').default; + chai = require('chai'); beforeEach(() => { this.sandbox = sinon.createSandbox(); From 453170433fd8a1eaf2ef68b62cb5c62d576def41 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sat, 26 Jan 2019 11:39:18 +0100 Subject: [PATCH 06/13] Fixed Bug - fixed reference error in test setup --- test/test-setup.spec.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/test-setup.spec.js b/test/test-setup.spec.js index 0d558dc..f7c976a 100644 --- a/test/test-setup.spec.js +++ b/test/test-setup.spec.js @@ -3,10 +3,8 @@ const sinon = require('sinon'), beforeEach(() => { this.sandbox = sinon.createSandbox(); - rewiremock.enable(); }); afterEach(() => { this.sandbox.restore(); - rewiremock.disable(); }); \ No newline at end of file From 8b2f9c45cf9676736de82136932771e7a7291ed0 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sat, 26 Jan 2019 11:42:20 +0100 Subject: [PATCH 07/13] Added --exit flag to mocha test --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b56104e..cd7f1d2 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "scripts": { "start": "node bot.js", - "test": "mocha", + "test": "mocha --exit", "test-unit": "NODE_ENV=test mocha '/**/*.spec.js'" }, "dependencies": { From 298119511a99958f75866040d13f4ac3a10acfd7 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sat, 26 Jan 2019 12:01:26 +0100 Subject: [PATCH 08/13] Added Tests - added tests for lib/cmd --- test/mockobjects.js | 23 ++++ test/test.js | 253 ++++++++++++++++++++++++++------------------ 2 files changed, 172 insertions(+), 104 deletions(-) diff --git a/test/mockobjects.js b/test/mockobjects.js index 0316b74..d34eb15 100644 --- a/test/mockobjects.js +++ b/test/mockobjects.js @@ -49,4 +49,27 @@ exports.mockVoicechannel = { exports.mockChannel = { send: (msg) => console.log('Send: ', msg) +}; + +exports.mockCommand = { + "name": "test", + "permission": "all", + "description": "Tests everything", + "category": "Test", + "response": { + "success": "Testing successful" + }, + "textReply": () => { + return 'test'; + }, + "promiseReply": () => { + return new Promise((rs, rj) => { + rs('test'); + }); + }, + "richEmbedReply": () => { + return {embed: { + title: 'rich embed' + }}; + } }; \ No newline at end of file diff --git a/test/test.js b/test/test.js index 5a48b0f..a380f04 100644 --- a/test/test.js +++ b/test/test.js @@ -160,7 +160,8 @@ describe('lib/utils', function() { }); }); -describe('The dj class', function () { +describe('lib/music', function() { + const music = rewire('../lib/music'); const Readable = require('stream').Readable; @@ -176,129 +177,173 @@ describe('The dj class', function () { return s; }); - it('connects to a VoiceChannel', function (done) { - let dj = new music.DJ(mockobjects.mockVoicechannel); - dj.connect().then(()=> { - assert(dj.connected); - done(); + describe('#DJ', function () { + + it('connects to a VoiceChannel', function (done) { + let dj = new music.DJ(mockobjects.mockVoicechannel); + dj.connect().then(()=> { + assert(dj.connected); + done(); + }); }); - }); - it('listens on Repeat', function () { - let dj = new music.DJ(mockobjects.mockVoicechannel); - dj.current = {'url': '', 'title': ''}; - dj.listenOnRepeat = true; + it('listens on Repeat', function () { + let dj = new music.DJ(mockobjects.mockVoicechannel); + dj.current = {'url': '', 'title': ''}; + dj.listenOnRepeat = true; - assert(dj.repeat); - assert(dj.queue.length > 0); - }); + assert(dj.repeat); + assert(dj.queue.length > 0); + }); - it('plays Files', function (done) { + it('plays Files', function (done) { - let dj = new music.DJ(mockobjects.mockVoicechannel); - dj.connect().then(() => { - dj.playFile(); - assert(dj.playing); - done(); + let dj = new music.DJ(mockobjects.mockVoicechannel); + dj.connect().then(() => { + dj.playFile(); + assert(dj.playing); + done(); + }); }); - }); - it('plays YouTube urls', function (done) { - let dj = new music.DJ(mockobjects.mockVoicechannel); - dj.connect().then(() => { - dj.playYouTube('http://www.youtube.com/watch?v=ABCDEFGHIJK'); + it('plays YouTube urls', function (done) { + let dj = new music.DJ(mockobjects.mockVoicechannel); + dj.connect().then(() => { + dj.playYouTube('http://www.youtube.com/watch?v=ABCDEFGHIJK'); + }); + + setTimeout(() => { + assert(dj.playing); + done(); + }, 100); }); - setTimeout(() => { - assert(dj.playing); - done(); - }, 211); - }); + it('gets the video name', function (done) { + let dj = new music.DJ(mockobjects.mockVoicechannel); + dj.getVideoName('http://www.youtube.com/watch?v=ABCDEFGHIJK').then((name) => { + assert(name === 'test'); + done(); + }) + }); - it('gets the video name', function (done) { - let dj = new music.DJ(mockobjects.mockVoicechannel); - dj.getVideoName('http://www.youtube.com/watch?v=ABCDEFGHIJK').then((name) => { - assert(name === 'test'); - done(); - }) - }); + it('sets the volume', function(done) { + let dj = new music.DJ(mockobjects.mockVoicechannel); + dj.connect().then(() => { + dj.playFile(); + dj.setVolume(100); + assert(dj.volume === 100); + done(); + }) + }); - it('sets the volume', function(done) { - let dj = new music.DJ(mockobjects.mockVoicechannel); - dj.connect().then(() => { - dj.playFile(); - dj.setVolume(100); - assert(dj.volume === 100); - done(); - }) - }); + it('pauses playback', function(done) { + let dj = new music.DJ(mockobjects.mockVoicechannel); + dj.connect().then(() => { + dj.playFile(); + dj.pause(); + done(); + }) + }); - it('pauses playback', function(done) { - let dj = new music.DJ(mockobjects.mockVoicechannel); - dj.connect().then(() => { - dj.playFile(); - dj.pause(); - done(); - }) - }); + it('resumes playback', function(done) { + let dj = new music.DJ(mockobjects.mockVoicechannel); + dj.connect().then(() => { + dj.playFile(); + dj.resume(); + done(); + }) + }); - it('resumes playback', function(done) { - let dj = new music.DJ(mockobjects.mockVoicechannel); - dj.connect().then(() => { - dj.playFile(); - dj.resume(); - done(); - }) - }); + it('stops playback', function(done) { + let dj = new music.DJ(mockobjects.mockVoicechannel); + dj.connect().then(() => { + dj.playFile(); + assert(dj.playing); + dj.stop(); + assert(!dj.conn && !dj.disp); + done(); + }); + }); - it('stops playback', function(done) { - let dj = new music.DJ(mockobjects.mockVoicechannel); - dj.connect().then(() => { - dj.playFile(); - assert(dj.playing); - dj.stop(); - assert(!dj.conn && !dj.disp); - done(); + it('skips songs', function(done) { + let dj = new music.DJ(mockobjects.mockVoicechannel); + dj.connect().then(() => { + dj.playYouTube('http://www.youtube.com/watch?v=ABCDEFGHIJK'); + dj.playYouTube('http://www.youtube.com/watch?v=ABCDEFGHIJK'); + dj.playYouTube('http://www.youtube.com/watch?v=ABCDEFGHIJK'); + dj.playYouTube('http://www.youtube.com/watch?v=ABCDEFGHIJK'); + dj.skip(); + dj.skip(); + done(); + }); }); - }); - it('skips songs', function(done) { - let dj = new music.DJ(mockobjects.mockVoicechannel); - dj.connect().then(() => { - dj.playYouTube('http://www.youtube.com/watch?v=ABCDEFGHIJK'); - dj.playYouTube('http://www.youtube.com/watch?v=ABCDEFGHIJK'); - dj.playYouTube('http://www.youtube.com/watch?v=ABCDEFGHIJK'); - dj.playYouTube('http://www.youtube.com/watch?v=ABCDEFGHIJK'); - dj.skip(); - dj.skip(); - done(); + it('returns a playlist', function(done) { + let dj = new music.DJ(mockobjects.mockVoicechannel); + dj.connect().then(() => { + dj.queue = [{ + 'title': 'title', + 'url': 'http://www.youtube.com/watch?v=ABCDEFGHIJK'}, { + 'title': 'title', + 'url': 'http://www.youtube.com/watch?v=ABCDEFGHIJK'}]; + assert(dj.playlist.length > 0); + done(); + }); }); + + it('clears the queue', function(done) { + let dj = new music.DJ(mockobjects.mockVoicechannel); + dj.connect().then(() => { + dj.queue = [{ + 'title': 'title', + 'url': 'http://www.youtube.com/watch?v=ABCDEFGHIJK'}, { + 'title': 'title', + 'url': 'http://www.youtube.com/watch?v=ABCDEFGHIJK'}]; + dj.clear(); + assert(dj.queue.length === 0); + done(); + }); + }) }); +}); - it('returns a playlist', function(done) { - let dj = new music.DJ(mockobjects.mockVoicechannel); - dj.connect().then(() => { - dj.queue = [{ - 'title': 'title', - 'url': 'http://www.youtube.com/watch?v=ABCDEFGHIJK'}, { - 'title': 'title', - 'url': 'http://www.youtube.com/watch?v=ABCDEFGHIJK'}]; - assert(dj.playlist.length > 0); - done(); +describe('lib/cmd', function() { + const cmd = rewire('../lib/cmd'); + cmd.__set__("logger", mockobjects.mockLogger); + + describe('#Servant', function() { + + it('creates commands', function() { + let servant = new cmd.Servant(''); + servant.createCommand(mockobjects.mockCommand, mockobjects.mockCommand.textReply); + assert(servant.commands['test']); + servant.createCommand(mockobjects.mockCommand, mockobjects.mockCommand.promiseReply); + assert(servant.commands['test']); + servant.createCommand(mockobjects.mockCommand, mockobjects.mockCommand.richEmbedReply); + assert(servant.commands['test']); }); - }); - it('clears the queue', function(done) { - let dj = new music.DJ(mockobjects.mockVoicechannel); - dj.connect().then(() => { - dj.queue = [{ - 'title': 'title', - 'url': 'http://www.youtube.com/watch?v=ABCDEFGHIJK'}, { - 'title': 'title', - 'url': 'http://www.youtube.com/watch?v=ABCDEFGHIJK'}]; - dj.clear(); - assert(dj.queue.length === 0); - done(); + it('removes commands', function() { + let servant = new cmd.Servant(''); + servant.createCommand(mockobjects.mockCommand, mockobjects.mockCommand.textReply); + assert(servant.commands['test']); + servant.removeCommand('test'); + assert(!servant.commands['test']) }); - }) + + it('parses commands', function() { + let spy = sinon.spy(); + let servant = new cmd.Servant(''); + servant.createCommand(mockobjects.mockCommand, spy); + assert(servant.commands['test']); + assert(!spy.called); + servant.parseCommand({ + content: 'test', + author: { + tag: undefined + } + }); + assert(spy.called); + }); + }); }); \ No newline at end of file From b7e66f8fd5241b5e126fad1c4b8c86f35acb761d Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sat, 26 Jan 2019 13:34:01 +0100 Subject: [PATCH 09/13] Added Tests - added tests for lib/guilding --- lib/guilding.js | 1 + test/cmdTest.js | 39 ------------------- test/guildingTest.js | 44 --------------------- test/mockobjects.js | 24 ++++++++++++ test/test.js | 91 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 116 insertions(+), 83 deletions(-) delete mode 100644 test/cmdTest.js delete mode 100644 test/guildingTest.js diff --git a/lib/guilding.js b/lib/guilding.js index 555fb09..048400b 100644 --- a/lib/guilding.js +++ b/lib/guilding.js @@ -12,6 +12,7 @@ let logger = require('winston'); exports.setLogger = function (newLogger) { logger = newLogger; music.setLogger(logger); + cmd.setLogger(logger); }; /** diff --git a/test/cmdTest.js b/test/cmdTest.js deleted file mode 100644 index eae8b1a..0000000 --- a/test/cmdTest.js +++ /dev/null @@ -1,39 +0,0 @@ -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/test/guildingTest.js b/test/guildingTest.js deleted file mode 100644 index fcad9c1..0000000 --- a/test/guildingTest.js +++ /dev/null @@ -1,44 +0,0 @@ -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/test/mockobjects.js b/test/mockobjects.js index d34eb15..62da037 100644 --- a/test/mockobjects.js +++ b/test/mockobjects.js @@ -72,4 +72,28 @@ exports.mockCommand = { title: 'rich embed' }}; } +}; + +exports.MockDatabase = class { + constructor(file, callback) { + callback(); + } + + run(sql, values, callback) { + if(callback) { + callback(); + } + } + + get() { + return null; + } + + all() { + return null + } + + close() { + return true; + } }; \ No newline at end of file diff --git a/test/test.js b/test/test.js index a380f04..9df1dc0 100644 --- a/test/test.js +++ b/test/test.js @@ -346,4 +346,95 @@ describe('lib/cmd', function() { assert(spy.called); }); }); +}); + +describe('lib/guilding', function() { + const guilding = rewire('../lib/guilding'); + guilding.__set__("sqlite3", null); + guilding.__set__("utils", { + dirExistence: (file, callback) => { + } + }); + guilding.setLogger(mockobjects.mockLogger); + + describe('#GuildHandler', function() { + + it('initializes', function() { + let gh = new guilding.GuildHandler('test', ''); + gh.db = new mockobjects.MockDatabase('', ()=>{}); + gh.createTables(); + gh.registerMusicCommands(); + gh.ready = true; + assert(gh.ready); + }); + + it('destroyes itself', function() { + let gh = new guilding.GuildHandler('test', ''); + gh.db = new mockobjects.MockDatabase('', ()=>{}); + gh.createTables(); + gh.registerMusicCommands(); + gh.ready = true; + gh.destroy(); + assert(!gh.dj.conn); + }); + + it('answers messages', function() { + let gh = new guilding.GuildHandler('test', ''); + gh.db = new mockobjects.MockDatabase('', ()=>{}); + gh.createTables(); + gh.registerMusicCommands(); + gh.ready = true; + let msgSpy = sinon.spy(); + gh.answerMessage({ + content: 'test', + author: { + tag: undefined + }, + reply: msgSpy, + channel: { + send: msgSpy + } + }, 'Answer'); + assert(msgSpy.called); + }); + + it('handles messages', function() { + let gh = new guilding.GuildHandler('test', '~'); + gh.db = new mockobjects.MockDatabase('', ()=>{}); + gh.ready = true; + let cbSpy = sinon.spy(); + gh.servant.createCommand(mockobjects.mockCommand, cbSpy); + assert(gh.servant.commands['~test']); + gh.handleMessage({ + content: '~test', + author: { + tag: undefined + }}); + assert(cbSpy.called); + }); + + it('connects and plays', function(done) { + const music = rewire('../lib/music'); + const Readable = require('stream').Readable; + + music.__set__("logger", mockobjects.mockLogger); + music.__set__("yttl", (id, cb) => { + cb(null, 'test'); + }); + music.__set__('ytdl', () => { + let s = new Readable(); + s._read = () => {}; + s.push('chunkofdataabc'); + s.push(null); + return s; + }); + let gh = new guilding.GuildHandler('test', '~'); + gh.db = new mockobjects.MockDatabase('', ()=>{}); + gh.ready = true; + gh.dj = new music.DJ(mockobjects.mockVoicechannel); + gh.connectAndPlay(mockobjects.mockVoicechannel, 'test', false).then(() => { + done(); + }) + }); + }); }); \ No newline at end of file From 249c711b53aaa43ebebe2abe749b0180fe3e145e Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sat, 26 Jan 2019 17:13:54 +0100 Subject: [PATCH 10/13] Added Test - testing all servercommands in guilding --- test/test.js | 45 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/test/test.js b/test/test.js index 9df1dc0..1149922 100644 --- a/test/test.js +++ b/test/test.js @@ -4,6 +4,14 @@ const mockobjects = require('./mockobjects.js'), rewire = require('rewire'); let Discord = require("discord.js"); +mockobjects.mockLogger = { + error: () => {}, + warn: () => {}, + info: () => {}, + verbose: () => {}, + debug: () => {} +}; + describe('lib/utils', function() { const utils = require('../lib/utils.js'); @@ -210,12 +218,11 @@ describe('lib/music', function() { let dj = new music.DJ(mockobjects.mockVoicechannel); dj.connect().then(() => { dj.playYouTube('http://www.youtube.com/watch?v=ABCDEFGHIJK'); + setTimeout(() => { + assert(dj.playing); + done(); + }, 100); }); - - setTimeout(() => { - assert(dj.playing); - done(); - }, 100); }); it('gets the video name', function (done) { @@ -350,6 +357,7 @@ describe('lib/cmd', function() { describe('lib/guilding', function() { const guilding = rewire('../lib/guilding'); + const servercommands = require('../commands/servercommands'); guilding.__set__("sqlite3", null); guilding.__set__("utils", { dirExistence: (file, callback) => { @@ -436,5 +444,32 @@ describe('lib/guilding', function() { done(); }) }); + + it('handles all servercommands', function() { + let gh = new guilding.GuildHandler('test', '~'); + gh.db = new mockobjects.MockDatabase('', ()=>{}); + gh.registerMusicCommands(); + gh.ready = true; + let msgSpy = sinon.spy(); + let msg = { + content: 'test', + author: { + tag: undefined + }, + reply: msgSpy, + channel: { + send: msgSpy + } + }; + + for (let category of Object.keys(servercommands)) { + for (let command of Object.keys(servercommands[category])) { + msg.content = '~' + command; + gh.handleMessage(msg); + } + } + + assert(msgSpy.called); + }); }); }); \ No newline at end of file From 5d90ff6830dfff892ab345d9a0c77c962bc373d8 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sat, 26 Jan 2019 18:12:15 +0100 Subject: [PATCH 11/13] Changed presences storage - moved presences to main.db - added detailed description to README --- README.md | 5 +++ bot.js | 95 ++++++++++++++++++++++++++++++++++++++++++------- lib/guilding.js | 12 +++---- lib/utils.js | 7 +++- test/test.js | 5 ++- 5 files changed, 102 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 9dfa28e..8ec6e7b 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,11 @@ At the moment the bot can... - [x] ...log stuff in a database - [ ] ...transform into a cow +Presences +--- + +You can add presences to the bot either by owner command `addpresence` or by providing a presences.txt file in the data directory. Each line represents a presence.

When all lines are loaded by the bot, the file gets deleted.

+ Ideas --- - command replies saved in file (server specific file and global file) diff --git a/bot.js b/bot.js index d8eee22..761ad8f 100644 --- a/bot.js +++ b/bot.js @@ -7,44 +7,59 @@ const Discord = require("discord.js"), config = require('./config.json'), client = new Discord.Client(), args = require('args-parser')(process.argv), + sqlite3 = require('sqlite3'), authToken = args.token || config.api.botToken, prefix = args.prefix || config.prefix || '~', gamepresence = args.game || config.presence; let presences = [], // loaded from presences.txt file if the file exists - rotator = null; // an interval id to stop presence duration if needed + rotator = null, // an interval id to stop presence duration if needed + maindb = null; function main() { + logger.verbose('Registering cleanup function'); + utils.Cleanup(() => { guilding.destroyAll(); client.destroy(); }); cmd.setLogger(logger); + logger.verbose('Verifying config'); + let configVerifyer = new utils.ConfigVerifyer(config, [ "api.botToken", "api.youTubeApiKey" ]); - if (configVerifyer.verifyConfig(logger)) { + if (!configVerifyer.verifyConfig(logger)) { if (!args.i) { + logger.info('Invalid config. Exiting'); process.exit(1); } } guilding.setLogger(logger); cmd.init(prefix); + logger.verbose('Registering commands'); registerCommands(); + logger.debug('Checking for ./data/ existence') utils.dirExistence('./data', () => { - fs.exists('./data/presences.txt', (exist) => { - if (exist) { - logger.debug('Loading presences from file...'); - let lineReader = require('readline').createInterface({ - input: require('fs').createReadStream('./data/presences.txt') - }); - lineReader.on('line', (line) => { - presences.push(line); + logger.verbose('Connecting to main database'); + maindb = new sqlite3.Database('./data/main.db', (err) => { + if (err) { + logger.error(err.message); + } else { + maindb.run(`${utils.sql.tableExistCreate} presences ( + ${utils.sql.pkIdSerial}, + text VARCHAR(255) UNIQUE NOT NULL + )`, (err) => { + if (err) { + logger.error(err.message); + } else { + logger.debug('Loading presences'); + loadPresences(); + } }); - rotator = client.setInterval(() => rotatePresence(), config.presence_duration); } - }) + }); }); registerCallbacks(); @@ -53,6 +68,55 @@ function main() { }); } +/** + * If a data/presences.txt exists, it is read and each line is put into the presences array. + * Each line is also stored in the main.db database. After the file is completely read, it get's deleted. + * Then the data is read from the database and if the presence doesn't exist in the presences array, it get's + * pushed in there. If the presences.txt file does not exist, the data is just read from the database. In the end + * a rotator is created that rotates the presence every configured duration. + */ +function loadPresences() { + if(fs.existsSync('./data/presences.txt')) { + let lineReader = require('readline').createInterface({ + input: require('fs').createReadStream('./data/presences.txt') + }); + lineReader.on('line', (line) => { + maindb.run('INSERT INTO presences (text) VALUES (?)', [line], (err) => { + if(err) { + logger.warn(err.message); + } + }); + presences.push(line); + }); + rotator = client.setInterval(() => rotatePresence(), config.presence_duration || 360000); + fs.unlink('./data/presences.txt', (err) => { + if (err) + logger.warn(err.message); + }); + maindb.all('SELECT text FROM presences', (err, rows) => { + if (err) { + logger.warn(err.message); + } else { + for(let row of rows) { + if (!row[0] in presences) + presences.push(row.text); + } + } + }) + } else { + maindb.all('SELECT text FROM presences', (err, rows) => { + if (err) { + logger.warn(err.message); + } else { + for(let row of rows) { + presences.push(row.text); + } + } + rotator = client.setInterval(() => rotatePresence(), config.presence_duration || 360000); + }) + } +} + /** * registeres global commands */ @@ -66,15 +130,20 @@ function registerCommands() { cmd.createGlobalCommand(prefix + 'addpresence', (msg, argv, args) => { let p = args.join(' '); presences.push(p); - fs.writeFile('./data/presences.txt', presences.join('\n'), (err) => { + + maindb.run('INSERT INTO presences (text) VALUES (?)', [p], (err) => { + if (err) + logger.warn(err.message); }); 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...'); + client.destroy().finally(() => { logger.debug(`Exiting Process...`); process.exit(0); diff --git a/lib/guilding.js b/lib/guilding.js index 048400b..76568b2 100644 --- a/lib/guilding.js +++ b/lib/guilding.js @@ -1,6 +1,6 @@ const cmd = require('./cmd'), music = require('./music'), - utils = require('./utils.js'), + utils = require('./utils'), config = require('../config.json'), servercmd = require('../commands/servercommands'), sqlite3 = require('sqlite3'), @@ -60,17 +60,15 @@ exports.GuildHandler = class { * playlists - save playlists to play them later */ createTables() { - let createCmd = 'CREATE TABLE IF NOT EXISTS'; - let autoIdPK = 'id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL'; - this.db.run(`${createCmd} messages ( - ${autoIdPK}, + this.db.run(`${utils.sql.tableExistCreate} messages ( + ${utils.sql.pkIdSerial}, creation_timestamp DATETIME NOT NULL, author VARCHAR(128) NOT NULL, author_name VARCHAR(128), content TEXT NOT NULL )`); - this.db.run(`${createCmd} playlists ( - ${autoIdPK}, + this.db.run(`${utils.sql.tableExistCreate} playlists ( + ${utils.sql.pkIdSerial}, name VARCHAR(32) UNIQUE NOT NULL, url VARCHAR(255) NOT NULL )`); diff --git a/lib/utils.js b/lib/utils.js index 5653ce9..a691ed3 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -199,7 +199,7 @@ exports.ConfigVerifyer = class { } /** - * @param promtMissing {Boolean} true - everything is fine; false - missing attributes + * @param logger set the logger to log to */ verifyConfig(logger) { let missing = []; @@ -221,4 +221,9 @@ exports.ConfigVerifyer = class { logger.error(`Missing required Attributes ${this.missingAttributes.join(', ')}`); } } +}; + +exports.sql = { + tableExistCreate: 'CREATE TABLE IF NOT EXISTS', + pkIdSerial: 'id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL' }; \ No newline at end of file diff --git a/test/test.js b/test/test.js index 1149922..fd080d1 100644 --- a/test/test.js +++ b/test/test.js @@ -358,10 +358,13 @@ describe('lib/cmd', function() { describe('lib/guilding', function() { const guilding = rewire('../lib/guilding'); const servercommands = require('../commands/servercommands'); + const utils = require('../lib/utils'); guilding.__set__("sqlite3", null); guilding.__set__("utils", { dirExistence: (file, callback) => { - } + }, + sql: utils.sql, + YouTube: utils.YouTube }); guilding.setLogger(mockobjects.mockLogger); From e27b46ac6abcfd3f5888a8a09ee4b34e9e10e3c9 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sat, 26 Jan 2019 18:46:30 +0100 Subject: [PATCH 12/13] Added Rich Embeds - rich embed for `queue` - rich embed for `uptime` --- bot.js | 12 +++++++++++- lib/guilding.js | 15 ++++++++------- lib/music.js | 12 ------------ lib/utils.js | 15 +++++++++++++++ test/test.js | 13 +++++++++++-- 5 files changed, 45 insertions(+), 22 deletions(-) diff --git a/bot.js b/bot.js index 761ad8f..e5ab685 100644 --- a/bot.js +++ b/bot.js @@ -169,7 +169,14 @@ function registerCommands() { // returns the time the bot is running cmd.createGlobalCommand(prefix + 'uptime', () => { - return `Uptime: \`${client.uptime / 1000} s\`` + let uptime = utils.getSplitDuration(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 uptime of the bot', 'owner'); // returns the numbe of guilds, the bot has joined @@ -178,6 +185,9 @@ function registerCommands() { }, [], 'Returns the number of guilds the bot has joined', 'owner'); } +/** + * changes the presence of the bot by using one stored in the presences array + */ function rotatePresence() { let pr = presences.shift(); presences.push(pr); diff --git a/lib/guilding.js b/lib/guilding.js index 76568b2..9ec2d28 100644 --- a/lib/guilding.js +++ b/lib/guilding.js @@ -263,14 +263,15 @@ exports.GuildHandler = class { // playlist command 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`; - for (let i = 0; i < songs.length; i++) { - if (i > 10) break; - songlist += songs[i] + '\n'; + 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 songlist; + return new Discord.RichEmbed() + .setTitle(`${this.dj.queue.length} songs in queue`) + .setDescription(describtion); }); // np command diff --git a/lib/music.js b/lib/music.js index 9d76d24..0039739 100644 --- a/lib/music.js +++ b/lib/music.js @@ -276,18 +276,6 @@ exports.DJ = class { } } - /** - * Returns the title for each song saved in the queue - * @returns {Array} - */ - get playlist() { - let songs = []; - this.queue.forEach((entry) => { - songs.push(entry.title); - }); - return songs; - } - /** * Returns the song saved in the private variable 'current' * @returns {null|*} diff --git a/lib/utils.js b/lib/utils.js index a691ed3..6c92e3c 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -79,6 +79,21 @@ exports.Cleanup = function Cleanup(callback) { }); }; +exports.getSplitDuration = function (duration) { + let dur = duration; + let retObj = {}; + retObj.milliseconds = dur % 1000; + dur = Math.round(dur / 1000); + retObj.seconds = dur % 60; + dur = Math.round(dur / 60); + retObj.minutes = dur % 60; + dur = Math.round(dur / 60); + retObj.hours = dur % 24; + dur = Math.round(dur / 24); + retObj.days = dur; + return retObj; +}; + /* FS */ exports.dirExistence = function (path, callback) { diff --git a/test/test.js b/test/test.js index fd080d1..924568a 100644 --- a/test/test.js +++ b/test/test.js @@ -15,6 +15,15 @@ mockobjects.mockLogger = { describe('lib/utils', function() { const utils = require('../lib/utils.js'); + describe('#getSplitDuration', function() { + it('returns an object from milliseconds', function() { + assert(utils.getSplitDuration(1000).seconds === 1); + assert(utils.getSplitDuration(360000).minutes === 6); + assert(utils.getSplitDuration(3600000).hours === 1); + assert(utils.getSplitDuration(100).milliseconds === 100); + }); + }); + describe('#getExtension', function() { it('returns the correct extension for a filename', function(done) { assert(utils.getExtension('test.txt') === '.txt'); @@ -295,7 +304,7 @@ describe('lib/music', function() { 'url': 'http://www.youtube.com/watch?v=ABCDEFGHIJK'}]; assert(dj.playlist.length > 0); done(); - }); + }).catch(() => done()); }); it('clears the queue', function(done) { @@ -309,7 +318,7 @@ describe('lib/music', function() { dj.clear(); assert(dj.queue.length === 0); done(); - }); + }).catch(() => done()); }) }); }); From 07848ac7614362f52d29afc9eb96118166bb95e0 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sat, 26 Jan 2019 19:51:56 +0000 Subject: [PATCH 13/13] Update dependency winston to v3.2.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f6b6d9a..ee6f295 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "get-youtube-title": "1.0.0", "opusscript": "0.0.6", "sqlite3": "4.0.6", - "winston": "3.1.0", + "winston": "3.2.0", "winston-daily-rotate-file": "3.6.0", "youtube-playlist-info": "1.1.2", "ytdl-core": "0.29.1"