Added Unit Tests & Logging

- Added own logger class that includes the module name
- added unit tests for MessageHandler, Command and Answer
- removed unit tests for GuildHandler and lib/cmd
- removed old command templates
pull/51/head
Trivernis 6 years ago
parent 6f890dacee
commit e74fa83ed3

@ -1,6 +1,6 @@
const Discord = require("discord.js"), const Discord = require("discord.js"),
fs = require('fs-extra'), fs = require('fs-extra'),
logger = require('./lib/logging').getLogger(), logging = require('./lib/logging'),
msgLib = require('./lib/MessageLib'), msgLib = require('./lib/MessageLib'),
guilding = require('./lib/guilding'), guilding = require('./lib/guilding'),
utils = require('./lib/utils'), utils = require('./lib/utils'),
@ -21,27 +21,27 @@ class Bot {
constructor() { constructor() {
this.client = new Discord.Client({autoReconnect: true}); this.client = new Discord.Client({autoReconnect: true});
this.logger = new logging.Logger(this);
this.rotator = null; this.rotator = null;
this.maindb = null; this.maindb = null;
this.presences = []; this.presences = [];
this.messageHandler = new msgLib.MessageHandler(this.client, logger); this.messageHandler = new msgLib.MessageHandler(this.client);
this.guildHandlers = {}; this.guildHandlers = {};
logger.verbose('Verifying config'); this.logger.verbose('Verifying config');
let configVerifyer = new utils.ConfigVerifyer(config, [ let configVerifyer = new utils.ConfigVerifyer(config, [
"api.botToken", "api.youTubeApiKey", "api.botToken", "api.youTubeApiKey",
"commandSettings.maxSequenceParallel", "commandSettings.maxSequenceParallel",
"commandSettings.maxSequenceSerial" "commandSettings.maxSequenceSerial"
]); ]);
if (!configVerifyer.verifyConfig(logger)) if (!configVerifyer.verifyConfig(this.logger))
if (!args.i) { if (!args.i) {
logger.info('Invalid config. Exiting'); this.logger.info('Invalid config. Exiting');
logger.flush().then(() => { this.logger.flush().then(() => {
process.exit(1); process.exit(1);
}); });
} }
guilding.setLogger(logger);
} }
/** /**
@ -49,7 +49,7 @@ class Bot {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async initServices() { async initServices() {
logger.verbose('Registering cleanup function'); this.logger.verbose('Registering cleanup function');
utils.Cleanup(() => { utils.Cleanup(() => {
for (let gh in Object.values(this.guildHandlers)) for (let gh in Object.values(this.guildHandlers))
@ -57,10 +57,10 @@ class Bot {
gh.destroy(); gh.destroy();
this.client.destroy().then(() => { this.client.destroy().then(() => {
logger.debug('destroyed client'); this.logger.debug('destroyed client');
}).catch((err) => { }).catch((err) => {
logger.error(err.message); this.logger.error(err.message);
logger.debug(err.stack); this.logger.debug(err.stack);
}); });
this.maindb.close(); this.maindb.close();
}); });
@ -68,22 +68,21 @@ class Bot {
if (config.webinterface && config.webinterface.enabled) if (config.webinterface && config.webinterface.enabled)
await this.initializeWebserver(); await this.initializeWebserver();
logger.verbose('Registering commands'); this.logger.verbose('Registering commands');
await this.messageHandler await this.messageHandler.registerCommandModule(require('./lib/commands/AnilistApiCommands').module, {});
.registerCommandModule(require('./lib/commands/AnilistApiCommands').module, {}); await this.messageHandler.registerCommandModule(require('./lib/commands/UtilityCommands').module, {
await this.messageHandler bot: this,
.registerCommandModule(require('./lib/commands/UtilityCommands').module, {bot: this, logger: logger, config: config}); config: config
await this.messageHandler });
.registerCommandModule(require('./lib/commands/InfoCommands').module, {client: this.client, messageHandler: this.messageHandler}); await this.messageHandler.registerCommandModule(require('./lib/commands/InfoCommands').module, {
await this.messageHandler client: this.client,
.registerCommandModule(require('./lib/commands/MusicCommands').module, { messageHandler: this.messageHandler
getGuildHandler: async (g) => await this.getGuildHandler(g), });
logger: logger await this.messageHandler.registerCommandModule(require('./lib/commands/MusicCommands').module, {
getGuildHandler: async (g) => await this.getGuildHandler(g)
}); });
await this.messageHandler await this.messageHandler.registerCommandModule(require('./lib/commands/ServerUtilityCommands').module, {
.registerCommandModule(require('./lib/commands/ServerUtilityCommands').module, {
getGuildHandler: async (g) => await this.getGuildHandler(g), getGuildHandler: async (g) => await this.getGuildHandler(g),
logger: logger,
messageHandler: this.messageHandler, messageHandler: this.messageHandler,
config: config config: config
}); });
@ -96,11 +95,11 @@ class Bot {
*/ */
async start() { async start() {
await this.client.login(authToken); await this.client.login(authToken);
logger.debug("Logged in"); this.logger.debug("Logged in");
if (this.webServer) { if (this.webServer) {
this.webServer.start(); this.webServer.start();
logger.info(`WebServer runing on port ${this.webServer.port}`); this.logger.info(`WebServer runing on port ${this.webServer.port}`);
} }
} }
@ -109,9 +108,9 @@ class Bot {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async initializeDatabase() { async initializeDatabase() {
logger.debug('Checking for ./data/ existence'); this.logger.debug('Checking for ./data/ existence');
await fs.ensureDir('./data'); await fs.ensureDir('./data');
logger.verbose('Connecting to main database'); this.logger.verbose('Connecting to main database');
this.maindb = new sqliteAsync.Database('./data/main.db'); this.maindb = new sqliteAsync.Database('./data/main.db');
await this.maindb.init(); await this.maindb.init();
@ -119,7 +118,7 @@ class Bot {
${utils.sql.pkIdSerial}, ${utils.sql.pkIdSerial},
text VARCHAR(255) UNIQUE NOT NULL text VARCHAR(255) UNIQUE NOT NULL
)`); )`);
logger.debug('Loading Presences...'); this.logger.debug('Loading Presences...');
await this.loadPresences(); await this.loadPresences();
} }
@ -127,12 +126,11 @@ class Bot {
* initializes the api webserver * initializes the api webserver
*/ */
async initializeWebserver() { async initializeWebserver() {
logger.verbose('Importing weblib'); this.logger.verbose('Importing weblib');
weblib = require('./lib/WebLib'); weblib = require('./lib/WebLib');
weblib.setLogger(logger); this.logger.verbose('Creating WebServer');
logger.verbose('Creating WebServer');
this.webServer = new weblib.WebServer(config.webinterface.port || 8080); this.webServer = new weblib.WebServer(config.webinterface.port || 8080);
logger.debug('Setting Reference Objects to webserver'); this.logger.debug('Setting Reference Objects to webserver');
await this.webServer.setReferenceObjects({ await this.webServer.setReferenceObjects({
client: this.client, client: this.client,
@ -159,7 +157,7 @@ class Bot {
lineReader.on('line', (line) => { lineReader.on('line', (line) => {
this.maindb.run('INSERT INTO presences (text) VALUES (?)', [line], (err) => { this.maindb.run('INSERT INTO presences (text) VALUES (?)', [line], (err) => {
if (err) if (err)
logger.warn(err.message); this.logger.warn(err.message);
}); });
this.presences.push(line); this.presences.push(line);
@ -190,8 +188,8 @@ class Bot {
this.client.user.setPresence({ this.client.user.setPresence({
game: {name: `${gamepresence} | ${pr}`, type: "PLAYING"}, game: {name: `${gamepresence} | ${pr}`, type: "PLAYING"},
status: 'online' status: 'online'
}).then(() => logger.debug(`Presence rotation to ${pr}`)) }).then(() => this.logger.debug(`Presence rotation to ${pr}`))
.catch((err) => logger.warn(err.message)); .catch((err) => this.logger.warn(err.message));
} }
@ -200,12 +198,12 @@ class Bot {
*/ */
registerEvents() { registerEvents() {
this.client.on('error', (err) => { this.client.on('error', (err) => {
logger.error(err.message); this.logger.error(err.message);
logger.debug(err.stack); this.logger.debug(err.stack);
}); });
this.client.on('ready', () => { this.client.on('ready', () => {
logger.info(`logged in as ${this.client.user.tag}!`); this.logger.info(`logged in as ${this.client.user.tag}!`);
this.client.user.setPresence({ this.client.user.setPresence({
game: { game: {
@ -213,7 +211,7 @@ class Bot {
}, status: 'online' }, status: 'online'
}).catch((err) => { }).catch((err) => {
if (err) if (err)
logger.warn(err.message); this.logger.warn(err.message);
}); });
}); });
@ -248,18 +246,19 @@ class Bot {
// Executing the main function // Executing the main function
if (typeof require !== 'undefined' && require.main === module) { if (typeof require !== 'undefined' && require.main === module) {
logger.info("Starting up... "); let logger = logging.logger;
logger.debug('Calling constructor...'); logger.info("Starting up... ", {m: 'bot.init'});
logger.debug('Calling constructor...', {m: 'bot.init'});
let discordBot = new Bot(); let discordBot = new Bot();
logger.debug('Initializing services...'); logger.debug('Initializing services...', {m: 'bot.init'});
discordBot.initServices().then(() => { discordBot.initServices().then(() => {
logger.debug('Starting Bot...'); logger.debug('Starting Bot...', {m: 'bot.init'});
discordBot.start().catch((err) => { //eslint-disable-line promise/no-nesting discordBot.start().catch((err) => { //eslint-disable-line promise/no-nesting
logger.error(err.message); logger.error(err.message, {m: 'bot.init'});
logger.debug(err.stack); logger.debug(err.stack, {m: 'bot.init'});
}); });
}).catch((err) => { }).catch((err) => {
logger.error(err.message); logger.error(err.message, {m: 'bot.init'});
logger.debug(err.stack); logger.debug(err.stack, {m: 'bot.init'});
}); });
} }

@ -1,109 +0,0 @@
{
"utils": {
"help": {
"name": "help",
"permission": "all",
"description": "Shows this help command.",
"category": "Utility",
"args": [
"command"
]
},
"say": {
"name": "say",
"permission": "all",
"description": "Says something. ~say [String].",
"category": "Utility"
},
"addpresence": {
"name": "addpresence",
"permission": "owner",
"description": "Adds a presence to presences.",
"category": "Utility"
},
"shutdown": {
"name": "shutdown",
"description": "Shuts the bot down.",
"permission": "owner",
"category": "Utility"
},
"rotate": {
"name": "rotate",
"description": "Forces a presence rotation",
"permission": "owner",
"category": "Utility"
},
"createUser": {
"name": "createUser",
"permission": "owner",
"description": "Creates a new user for the webinterface.",
"category": "Utility",
"args": [
"username",
"password",
"scope"
]
},
"bugreport": {
"name": "bug",
"permission": "all",
"description": "Get info about how to report a bug",
"category": "Utility",
"response": {
"bug_report": "Please report your bugs [here](https://github.com/Trivernis/discordbot.js/issues)"
}
}
},
"info": {
"about": {
"name": "about",
"permission": "all",
"description": "Shows information about this bot.",
"category": "Info",
"response": {
"about_icon": "This icon war created by [blackrose14344](https://www.deviantart.com/blackrose14344). \n [Original](https://www.deviantart.com/blackrose14344/art/2B-Chi-B-685771489)",
"about_creator": "This bot was created by Trivernis. More about this bot [here](https://github.com/Trivernis/discordbot.js)."
}
},
"ping": {
"name": "ping",
"permission": "owner",
"description": "Answers with the current average ping of the bot.",
"category": "Info"
},
"uptime": {
"name": "uptime",
"permission": "owner",
"description": "Answers with the current uptime of the bot.",
"category": "Info"
},
"guilds": {
"name": "guilds",
"permission": "owner",
"description": "Answers with the number of guilds the bot has joined.",
"category": "Info"
}
},
"api": {
"AniList": {
"animeSearch": {
"name": "anime",
"permission": "all",
"description": "Answers the anime found for that name on AniList.",
"category": "AniList",
"response": {
"not_found": "The Anime was not found :("
}
},
"mangaSearch": {
"name": "manga",
"permission": "all",
"description": "Answers the manga found for that name on AniList.",
"category": "AniList",
"response": {
"not_found": "The Manga was not found :("
}
}
}
}
}

@ -1,203 +0,0 @@
{
"utils": {
"roles": {
"name": "roles",
"permission": "all",
"description": "Shows the roles used for commands on the server.",
"category": "Utility"
},
"savecmd": {
"name": "savecmd",
"permission": "moderator",
"description": "Saves a sequence of commands under a new name. ~save [cmdsequence] [cmdname]. Semicoli must be escaped with \\ (Backslash)",
"category": "Utility",
"response": {
"no_recursion": "You are **not** allowed to execute another saved command in this sequence. This is a safety measure to avoid endlessly recursive calls.",
"sequence_too_long": "This command sequence is too long!"
}
},
"savedcmd": {
"name": "savedcmd",
"permission": "all",
"description": "Displays the saved commands.",
"category": "Utility",
"response": {
"no_commands": "There are no saved commands."
}
},
"deletecmd": {
"name": "deletecmd",
"permission": "moderator",
"description": "Delete a saved command.",
"args": [
"cmdname"
],
"category": "Utility"
},
"execute": {
"name": "execute",
"permission": "all",
"args": [
"cmdname"
],
"description": "Execute saved commands.",
"category": "Utility",
"response": {
"not_found": "This command could not be found."
}
}
},
"music": {
"play": {
"name": "play",
"permission": "all",
"args": [
"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.",
"url_invalid": "This is not a valid url!",
"no_url": "I need an url to a video to play!",
"no_voicechannel": "You need to join a voicechannel to do that!"
}
},
"playnext": {
"name": "playnext",
"permission": "all",
"args": [
"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.",
"url_invalid": "This is not a valid url!",
"no_url": "I need an url to a video to play",
"no_voicechannel": "You need to join a voicechannel to do that!"
}
},
"join": {
"name": "join",
"permission": "all",
"description": "Joins the VC you are in.",
"category": "Music",
"response": {
"not_connected": "You are not connected to a Voice Channel."
}
},
"stop": {
"name": "stop",
"permission": "dj",
"description": "Stops playing music and leaves.",
"category": "Music",
"response": {
"success": "Stopping now...",
"not_playing": "I'm not playing music at the moment."
}
},
"pause": {
"name": "pause",
"permission": "all",
"description": "Pauses playing.",
"category": "Music",
"response": {
"success": "Pausing playback.",
"not_playing": "I'm not playing music at the moment."
}
},
"resume": {
"name": "resume",
"permission": "all",
"description": "Resumes playing.",
"category": "Music",
"response": {
"success": "Resuming playback.",
"not_playing": "I'm not playing music at the moment."
}
},
"skip": {
"name": "skip",
"permission": "dj",
"description": "Skips the current song.",
"category": "Music",
"response": {
"success": "Skipping to the next song.",
"not_playing": "I'm not playing music at the moment."
}
},
"clear": {
"name": "clear",
"permission": "dj",
"description": "Clears the queue.",
"category": "Music",
"response": {
"success": "The Queue has been cleared."
}
},
"playlist": {
"name": "queue",
"permission": "all",
"description": "Shows the next ten songs.",
"category": "Music"
},
"current": {
"name": "np",
"permission": "all",
"description": "Shows the currently playing song.",
"category": "Music",
"response": {
"not_playing": "I'm not playing music at the moment."
}
},
"shuffle": {
"name": "shuffle",
"permission": "all",
"description": "Shuffles the playlist.",
"category": "Music",
"response": {
"success": "The Queue has been shuffled."
}
},
"repeat": {
"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."
}
},
"savemedia": {
"name": "savemedia",
"permission": "dj",
"args": [
"url"
],
"description": "Saves the YouTube song/playlist with a specific name",
"category": "Music"
},
"savedmedia": {
"name": "savedmedia",
"permission": "all",
"description": "Prints out all saved playlists and songs.",
"category": "Music",
"response": {
"no_saved": "There are no saved songs/playlists :("
}
},
"deletemedia": {
"name": "deletemedia",
"permission": "dj",
"description": "Deletes a saved media entry. ~deletemedia [name]",
"category": "Music",
"response": {
"no_name": "You must provide a name for the media that shall be deleted."
}
}
}
}

@ -1,6 +1,7 @@
const Discord = require('discord.js'), const Discord = require('discord.js'),
yaml = require('js-yaml'), yaml = require('js-yaml'),
fsx = require('fs-extra'), fsx = require('fs-extra'),
logging = require('./logging'),
config = require('../config.json'), config = require('../config.json'),
utils = require('./utils'); utils = require('./utils');
@ -96,6 +97,7 @@ class CommandHandler {
this.prefix = prefix; this.prefix = prefix;
this.scope = scope; this.scope = scope;
this.commands = {}; this.commands = {};
this._logger = new logging.Logger(`${this.constructor.name}@${Object.keys(scopes)[this.scope]}`);
} }
/** /**
@ -105,9 +107,11 @@ class CommandHandler {
* @returns {Boolean | String | Promise<String|Discord.RichEmbed>} * @returns {Boolean | String | Promise<String|Discord.RichEmbed>}
*/ */
handleCommand(commandMessage, message) { handleCommand(commandMessage, message) {
this._logger.debug(`Handling command ${commandMessage}`);
let commandName = commandMessage.match(/^\S+/); let commandName = commandMessage.match(/^\S+/);
if (commandName.length > 0) if (commandName.length > 0)
commandName = commandName[0]; commandName = commandName[0];
this._logger.silly(`Command name is ${commandName}`);
if (commandName.indexOf(this.prefix) >= 0) { if (commandName.indexOf(this.prefix) >= 0) {
commandName = commandName.replace(this.prefix, ''); commandName = commandName.replace(this.prefix, '');
let argsString = commandMessage.replace(/^\S+/, ''); let argsString = commandMessage.replace(/^\S+/, '');
@ -117,17 +121,21 @@ class CommandHandler {
let args = argsString.match(/\S+/g); let args = argsString.match(/\S+/g);
let command = this.commands[commandName]; let command = this.commands[commandName];
if (command && this._checkPermission(message, command.permission)) { if (command && this._checkPermission(message, command.permission)) {
this._logger.silly(`Permission ${command.permission} granted for command ${commandName}`);
let kwargs = {}; let kwargs = {};
if (args) if (args)
for (let i = 0; i < Math.min(command.args.length, args.length); i++) for (let i = 0; i < Math.min(command.args.length, args.length); i++)
kwargs[command.args[i]] = args[i]; kwargs[command.args[i]] = args[i];
return command.answer(message, kwargs, argsString); return command.answer(message, kwargs, argsString);
} else if (command) { } else if (command) {
this._logger.silly(`Permission ${command.permission} denied for command ${commandName}`);
return "You don't have permission for this command"; return "You don't have permission for this command";
} else { } else {
this._logger.silly(`Command ${commandName} not found.`);
return false; return false;
} }
} else { } else {
this._logger.silly(`No prefix found in command ${commandName}`);
return false; return false;
} }
} }
@ -139,6 +147,7 @@ class CommandHandler {
registerCommand(command) { registerCommand(command) {
command.prefix = this.prefix; command.prefix = this.prefix;
this.commands[command.name] = command; this.commands[command.name] = command;
this._logger.debug(`Registered ${command.name} on handler`);
return this; return this;
} }
@ -174,6 +183,7 @@ class CommandModule {
*/ */
constructor(scope) { constructor(scope) {
this.scope = scope; this.scope = scope;
this._logger = new logging.Logger(this);
} }
/** /**
@ -183,6 +193,7 @@ class CommandModule {
*/ */
async _loadTemplate(file) { async _loadTemplate(file) {
let templateString = await fsx.readFile(this.templateFile || file, {encoding: 'utf-8'}); let templateString = await fsx.readFile(this.templateFile || file, {encoding: 'utf-8'});
this._logger.silly(`Loaded Template file ${this.templateFile || file}`);
this.template = yaml.safeLoad(templateString); this.template = yaml.safeLoad(templateString);
} }

@ -1,6 +1,7 @@
const cmdLib = require('./CommandLib'), const cmdLib = require('./CommandLib'),
config = require('../config.json'), config = require('../config.json'),
Discord = require('discord.js'), Discord = require('discord.js'),
logging = require('./logging'),
promiseWaterfall = require('promise-waterfall'); promiseWaterfall = require('promise-waterfall');
/* eslint no-useless-escape: 0 */ /* eslint no-useless-escape: 0 */
@ -11,11 +12,10 @@ class MessageHandler {
* Message Handler to handle messages. Listens on the * Message Handler to handle messages. Listens on the
* _client message event. * _client message event.
* @param client {Discord.Client} * @param client {Discord.Client}
* @param logger {winston._logger}
*/ */
constructor (client, logger) { constructor (client) {
this.logger = logger;
this.discordClient = client; this.discordClient = client;
this.logger = new logging.Logger(this);
this.globalCmdHandler = new cmdLib.CommandHandler(config.prefix, this.globalCmdHandler = new cmdLib.CommandHandler(config.prefix,
cmdLib.CommandScopes.Global); cmdLib.CommandScopes.Global);
this.userCmdHandler = new cmdLib.CommandHandler(config.prefix, this.userCmdHandler = new cmdLib.CommandHandler(config.prefix,
@ -70,7 +70,7 @@ class MessageHandler {
_registerEvents() { _registerEvents() {
this.logger.debug('Registering message event...'); this.logger.debug('Registering message event...');
this.discordClient.on('message', async (msg) => { this.discordClient.on('message', async (msg) => {
this.logger.debug(`<${msg.guild? msg.channel.name+'@'+msg.guild.name : 'PRIVATE'}> ${msg.author.tag}: ${msg.content}`); this.logger.verbose(`<${msg.guild? msg.channel.name+'@'+msg.guild.name : 'PRIVATE'}> ${msg.author.tag}: ${msg.content}`);
if (msg.author !== this.discordClient.user if (msg.author !== this.discordClient.user
&& this._checkPrefixStart(msg.content) && this._checkPrefixStart(msg.content)
&& !this._checkRateReached(msg.author)) { && !this._checkRateReached(msg.author)) {
@ -90,7 +90,7 @@ class MessageHandler {
* @private * @private
*/ */
_parseSyntax(message) { _parseSyntax(message) {
this.logger.debug('Parsing command sequence...'); this.logger.silly('Parsing command sequence...');
let commandSequence = []; let commandSequence = [];
let content = message.content; let content = message.content;
let strings = content.match(/".+?"/g) || []; let strings = content.match(/".+?"/g) || [];
@ -114,14 +114,14 @@ class MessageHandler {
* Executes a sequence of commands * Executes a sequence of commands
*/ */
async executeCommandSequence(cmdSequence, message) { async executeCommandSequence(cmdSequence, message) {
this.logger.debug(`Executing command sequence: ${JSON.stringify(cmdSequence)} ...`); this.logger.silly(`Executing command sequence: ${JSON.stringify(cmdSequence)} ...`);
let scopeCmdHandler = this.getScopeHandler(message); let scopeCmdHandler = this.getScopeHandler(message);
await Promise.all(cmdSequence.map((sq) => promiseWaterfall(sq.map((cmd) => async () => { await Promise.all(cmdSequence.map((sq) => promiseWaterfall(sq.map((cmd) => async () => {
try { try {
this.logger.debug(`Executing command ${cmd}`); this.logger.silly(`Executing command ${cmd}`);
let globalResult = await this.globalCmdHandler.handleCommand(cmd, message); let globalResult = await this.globalCmdHandler.handleCommand(cmd, message);
let scopeResult = await scopeCmdHandler.handleCommand(cmd, message); let scopeResult = await scopeCmdHandler.handleCommand(cmd, message);
this.logger.debug(`globalResult: ${globalResult}, scopeResult: ${scopeResult}`); this.logger.silly(`globalResult: ${globalResult}, scopeResult: ${scopeResult}`);
if (scopeResult) if (scopeResult)
this._answerMessage(message, scopeResult); this._answerMessage(message, scopeResult);

@ -3,15 +3,9 @@ const ytdl = require("ytdl-core"),
yttl = require('get-youtube-title'), yttl = require('get-youtube-title'),
config = require('../config.json'), config = require('../config.json'),
utils = require('./utils.js'), utils = require('./utils.js'),
logging = require('./logging'),
ytapiKey = config.api.youTubeApiKey; ytapiKey = config.api.youTubeApiKey;
let logger = require('winston');
exports.setLogger = function (newLogger) {
logger = newLogger;
};
/** /**
* The Music Player class is used to handle music playing tasks on Discord Servers (Guilds). * The Music Player class is used to handle music playing tasks on Discord Servers (Guilds).
* @type {MusicPlayer} * @type {MusicPlayer}
@ -28,6 +22,8 @@ class MusicPlayer {
this.voiceChannel = voiceChannel; this.voiceChannel = voiceChannel;
this.quality = 'lowest'; this.quality = 'lowest';
this.exitTimeout = null; this.exitTimeout = null;
this._logger = new logging.Logger(this);
this._logger.silly('Initialized Music Player');
} }
/** /**
@ -45,9 +41,9 @@ class MusicPlayer {
} }
else if (voiceChannel) else if (voiceChannel)
this.voiceChannel = voiceChannel; this.voiceChannel = voiceChannel;
logger.verbose(`Connecting to voiceChannel ${this.voiceChannel.name}`); this._logger.verbose(`Connecting to voiceChannel ${this.voiceChannel.name}`);
let connection = await this.voiceChannel.join(); let connection = await this.voiceChannel.join();
logger.info(`Connected to Voicechannel ${this.voiceChannel.name}`); this._logger.info(`Connected to Voicechannel ${this.voiceChannel.name}`);
this.conn = connection; this.conn = connection;
} }
@ -59,7 +55,6 @@ class MusicPlayer {
this.repeat = value; this.repeat = value;
if (this.current) if (this.current)
this.queue.push(this.current); this.queue.push(this.current);
} }
/** /**
@ -81,7 +76,7 @@ class MusicPlayer {
updateChannel(voiceChannel) { updateChannel(voiceChannel) {
if (voiceChannel) { if (voiceChannel) {
this.voiceChannel = voiceChannel; this.voiceChannel = voiceChannel;
logger.debug(`Updated voiceChannel to ${this.voiceChannel.name}`); this._logger.debug(`Updated voiceChannel to ${this.voiceChannel.name}`);
} }
} }
@ -96,7 +91,7 @@ class MusicPlayer {
this.disp = this.conn.playFile(filename); this.disp = this.conn.playFile(filename);
this.playing = true; this.playing = true;
} else { } else {
logger.warn("Not connected to a voicechannel. Connection now."); this._logger.warn("Not connected to a voicechannel. Connection now.");
this.connect(this.voiceChannel).then(() => { this.connect(this.voiceChannel).then(() => {
this.playFile(filename); this.playFile(filename);
}); });
@ -111,13 +106,13 @@ class MusicPlayer {
if (this.exitTimeout) { if (this.exitTimeout) {
clearTimeout(this.exitTimeout); clearTimeout(this.exitTimeout);
this.exitTimeout = null; this.exitTimeout = null;
logger.debug(`Cleared exit timout for ${this.voiceChannel.name}`); this._logger.debug(`Cleared exit timout for ${this.voiceChannel.name}`);
} }
if (this.connected && this.voiceChannel.members.size === 1) { if (this.connected && this.voiceChannel.members.size === 1) {
logger.debug(`Set exit timout for ${this.voiceChannel.name}`); this._logger.debug(`Set exit timout for ${this.voiceChannel.name}`);
this.exitTimeout = setTimeout(() => { this.exitTimeout = setTimeout(() => {
if (this.connected && this.voiceChannel.members.size === 1) if (this.connected && this.voiceChannel.members.size === 1)
logger.verbose(`Exiting ${this.voiceChannel.name}`); this._logger.verbose(`Exiting ${this.voiceChannel.name}`);
this.stop(); this.stop();
}, config.music.timeout || 300000); }, config.music.timeout || 300000);
} }
@ -133,7 +128,7 @@ class MusicPlayer {
async playYouTube(url, playnext) { async playYouTube(url, playnext) {
let plist = utils.YouTube.getPlaylistIdFromUrl(url); let plist = utils.YouTube.getPlaylistIdFromUrl(url);
if (plist) { if (plist) {
logger.debug(`Adding playlist ${plist} to queue`); this._logger.debug(`Adding playlist ${plist} to queue`);
let playlistItems = await ypi(ytapiKey, plist); let playlistItems = await ypi(ytapiKey, plist);
let firstSong = utils.YouTube.getVideoUrlFromId(playlistItems.shift().resourceId.videoId); let firstSong = utils.YouTube.getVideoUrlFromId(playlistItems.shift().resourceId.videoId);
let firstSongTitle = null; let firstSongTitle = null;
@ -141,14 +136,14 @@ class MusicPlayer {
firstSongTitle = await this.getVideoName(firstSong); firstSongTitle = await this.getVideoName(firstSong);
} catch(err) { } catch(err) {
if (err.message !== 'Not found') { if (err.message !== 'Not found') {
logger.warn(err.message); this._logger.warn(err.message);
logger.debug(err.stack); this._logger.debug(err.stack);
} }
} }
if (this.repeat) if (this.repeat)
this.queue.push({'url': firstSong, 'title': firstSongTitle}); this.queue.push({'url': firstSong, 'title': firstSongTitle});
this.playYouTube(firstSong).catch((err) => logger.warn(err.message)); this.playYouTube(firstSong).catch((err) => this._logger.warn(err.message));
for (let item of playlistItems) { for (let item of playlistItems) {
let vurl = utils.YouTube.getVideoUrlFromId(item.resourceId.videoId); let vurl = utils.YouTube.getVideoUrlFromId(item.resourceId.videoId);
@ -156,14 +151,14 @@ class MusicPlayer {
this.queue.push({'url': vurl, 'title': await this.getVideoName(vurl)}); //eslint-disable-line no-await-in-loop this.queue.push({'url': vurl, 'title': await this.getVideoName(vurl)}); //eslint-disable-line no-await-in-loop
} catch (err) { } catch (err) {
if (err.message !== 'Not found') { if (err.message !== 'Not found') {
logger.warn(err.message); this._logger.warn(err.message);
logger.debug(err.stack); this._logger.debug(err.stack);
} }
} }
} }
logger.debug(`Added ${playlistItems.length} songs to the queue`); this._logger.debug(`Added ${playlistItems.length} songs to the queue`);
} else if (!this.playing || !this.disp) { } else if (!this.playing || !this.disp) {
logger.debug(`Playing ${url}`); this._logger.debug(`Playing ${url}`);
this.current = ({'url': url, 'title': await this.getVideoName(url)}); this.current = ({'url': url, 'title': await this.getVideoName(url)});
this.disp = this.conn.playStream(ytdl(url, this.disp = this.conn.playStream(ytdl(url,
@ -178,7 +173,7 @@ class MusicPlayer {
this.current = this.queue.shift(); this.current = this.queue.shift();
if (this.repeat) // listen on repeat if (this.repeat) // listen on repeat
this.queue.push(this.current); this.queue.push(this.current);
this.playYouTube(this.current.url).catch((err) => logger.warn(err.message)); this.playYouTube(this.current.url).catch((err) => this._logger.warn(err.message));
} else { } else {
this.stop(); this.stop();
} }
@ -186,7 +181,7 @@ class MusicPlayer {
}); });
this.playing = true; this.playing = true;
} else { } else {
logger.debug(`Added ${url} to the queue`); this._logger.debug(`Added ${url} to the queue`);
if (playnext) if (playnext)
this.queue.unshift({'url': url, 'title': await this.getVideoName(url)}); this.queue.unshift({'url': url, 'title': await this.getVideoName(url)});
else else
@ -204,7 +199,7 @@ class MusicPlayer {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
yttl(utils.YouTube.getVideoIdFromUrl(url), (err, title) => { yttl(utils.YouTube.getVideoIdFromUrl(url), (err, title) => {
if (err) { if (err) {
logger.debug(JSON.stringify(err)); this._logger.debug(JSON.stringify(err));
reject(err); reject(err);
} else { } else {
resolve(title); resolve(title);
@ -218,12 +213,12 @@ class MusicPlayer {
* @param percentage {Number} * @param percentage {Number}
*/ */
setVolume(percentage) { setVolume(percentage) {
logger.verbose(`Setting volume to ${percentage}`); this._logger.verbose(`Setting volume to ${percentage}`);
if (this.disp !== null) { if (this.disp !== null) {
this.volume = percentage; this.volume = percentage;
this.disp.setVolume(percentage); this.disp.setVolume(percentage);
} else { } else {
logger.warn("No dispatcher found."); this._logger.warn("No dispatcher found.");
} }
} }
@ -231,11 +226,11 @@ class MusicPlayer {
* Pauses if a dispatcher exists * Pauses if a dispatcher exists
*/ */
pause() { pause() {
logger.verbose("Pausing music..."); this._logger.verbose("Pausing music...");
if (this.disp !== null) if (this.disp !== null)
this.disp.pause(); this.disp.pause();
else else
logger.warn("No dispatcher found"); this._logger.warn("No dispatcher found");
} }
@ -243,11 +238,11 @@ class MusicPlayer {
* Resumes if a dispatcher exists * Resumes if a dispatcher exists
*/ */
resume() { resume() {
logger.verbose("Resuming music..."); this._logger.verbose("Resuming music...");
if (this.disp !== null) if (this.disp !== null)
this.disp.resume(); this.disp.resume();
else else
logger.warn("No dispatcher found"); this._logger.warn("No dispatcher found");
} }
@ -259,25 +254,25 @@ class MusicPlayer {
this.playing = false; this.playing = false;
this.queue = []; this.queue = [];
this.current = null; this.current = null;
logger.verbose("Stopping music..."); this._logger.verbose("Stopping music...");
try { try {
if (this.disp) { if (this.disp) {
this.disp.end('stop'); this.disp.end('stop');
this.disp = null; this.disp = null;
logger.debug("Ended dispatcher"); this._logger.debug("Ended dispatcher");
} }
if (this.conn) { if (this.conn) {
this.conn.disconnect(); this.conn.disconnect();
this.conn = null; this.conn = null;
logger.debug("Ended connection"); this._logger.debug("Ended connection");
} }
if (this.voiceChannel) { if (this.voiceChannel) {
this.voiceChannel.leave(); this.voiceChannel.leave();
logger.debug("Left VoiceChannel"); this._logger.debug("Left VoiceChannel");
logger.info(`Disconnected from Voicechannel ${this.voiceChannel.name}`); this._logger.info(`Disconnected from Voicechannel ${this.voiceChannel.name}`);
} }
} catch (error) { } catch (error) {
logger.verbose(JSON.stringify(error)); this._logger.verbose(JSON.stringify(error));
} }
} }
@ -287,7 +282,7 @@ class MusicPlayer {
* It tries to play the next song with playYouTube * It tries to play the next song with playYouTube
*/ */
skip() { skip() {
logger.debug("Skipping song"); this._logger.debug("Skipping song");
if (this.disp !== null) { if (this.disp !== null) {
this.disp.end(); this.disp.end();
} else { } else {
@ -295,8 +290,8 @@ class MusicPlayer {
if (this.queue.length > 0) { if (this.queue.length > 0) {
this.current = this.queue.shift(); this.current = this.queue.shift();
this.playYouTube(this.current.url).catch((err) => { this.playYouTube(this.current.url).catch((err) => {
logger.error(err.message); this._logger.error(err.message);
logger.debug(err.stack); this._logger.debug(err.stack);
}); });
} else { } else {
this.stop(); this.stop();

@ -4,6 +4,7 @@ const express = require('express'),
compression = require('compression'), compression = require('compression'),
md5 = require('js-md5'), md5 = require('js-md5'),
sha512 = require('js-sha512'), sha512 = require('js-sha512'),
logging = require('./logging'),
fs = require('fs'), fs = require('fs'),
session = require('express-session'), session = require('express-session'),
SQLiteStore = require('connect-sqlite3')(session), SQLiteStore = require('connect-sqlite3')(session),
@ -12,12 +13,6 @@ const express = require('express'),
config = require('../config.json'), config = require('../config.json'),
utils = require('../lib/utils'); utils = require('../lib/utils');
let logger = require('winston');
exports.setLogger = function (newLogger) {
logger = newLogger;
};
exports.WebServer = class { exports.WebServer = class {
constructor(port) { constructor(port) {
this.app = express(); this.app = express();
@ -25,6 +20,7 @@ exports.WebServer = class {
this.port = port; this.port = port;
this.schema = buildSchema(fs.readFileSync('./lib/api/graphql/schema.gql', 'utf-8')); this.schema = buildSchema(fs.readFileSync('./lib/api/graphql/schema.gql', 'utf-8'));
this.root = {}; this.root = {};
this._logger = new logging.Logger(this);
} }
/** /**
@ -68,7 +64,7 @@ exports.WebServer = class {
} else { } else {
let user = await this.maindb.get('SELECT * FROM users WHERE username = ? AND password = ?', [req.body.username, req.body.password]); let user = await this.maindb.get('SELECT * FROM users WHERE username = ? AND password = ?', [req.body.username, req.body.password]);
if (!user) { if (!user) {
logger.debug(`User ${req.body.username} failed to authenticate`); this._logger.debug(`User ${req.body.username} failed to authenticate`);
res.render('login', {msg: 'Login failed!'}); res.render('login', {msg: 'Login failed!'});
} else { } else {
req.session.user = user; req.session.user = user;
@ -107,10 +103,10 @@ exports.WebServer = class {
if (config.webinterface.https.certFile) if (config.webinterface.https.certFile)
sslCert = fs.readFileSync(config.webinterface.https.certFile, 'utf-8'); sslCert = fs.readFileSync(config.webinterface.https.certFile, 'utf-8');
if (sslKey && sslCert) { if (sslKey && sslCert) {
logger.verbose('Creating https server.'); this._logger.verbose('Creating https server.');
this.server = require('https').createServer({key: sslKey, cert: sslCert}, this.app); this.server = require('https').createServer({key: sslKey, cert: sslCert}, this.app);
} else { } else {
logger.warn('Key or certificate file not found. Fallback to http server.'); this._logger.warn('Key or certificate file not found. Fallback to http server.');
this.server = require('http').createServer(this.app); this.server = require('http').createServer(this.app);
} }
} else { } else {
@ -175,7 +171,7 @@ exports.WebServer = class {
.slice(args.offset, args.offset + args.first) .slice(args.offset, args.offset + args.first)
.map(async (x) => new Guild(x, await objects.getGuildHandler(x)))); .map(async (x) => new Guild(x, await objects.getGuildHandler(x))));
} catch (err) { } catch (err) {
logger.error(err.stack); this._logger.error(err.stack);
return null; return null;
} }

@ -59,8 +59,11 @@ class AniListCommandModule extends cmdLib.CommandModule {
new cmdLib.Answer(async (m, k, s) => { new cmdLib.Answer(async (m, k, s) => {
try { try {
let animeData = await anilistApi.searchAnimeByName(s); let animeData = await anilistApi.searchAnimeByName(s);
this._logger.silly(`Anime Query returned ${JSON.stringify(animeData)}`);
return new RichMediaInfo(animeData); return new RichMediaInfo(animeData);
} catch (err) { } catch (err) {
if (err.message)
this._logger.verbose(err.message);
return this.template.anime_search.not_found; return this.template.anime_search.not_found;
} }
})); }));
@ -70,8 +73,11 @@ class AniListCommandModule extends cmdLib.CommandModule {
new cmdLib.Answer(async (m, k, s) => { new cmdLib.Answer(async (m, k, s) => {
try { try {
let mangaData = await anilistApi.searchMangaByName(s); let mangaData = await anilistApi.searchMangaByName(s);
this._logger.silly(`Manga Query returned ${JSON.stringify(mangaData)}`);
return new RichMediaInfo(mangaData); return new RichMediaInfo(mangaData);
} catch (err) { } catch (err) {
if (err.message)
this._logger.verbose(err.message);
return this.template.manga_search.not_found; return this.template.manga_search.not_found;
} }
}) })

@ -39,6 +39,7 @@ class InfoCommandModule extends cmdLib.CommandModule {
helpEmbed.addField(cat, catCommands[cat]); helpEmbed.addField(cat, catCommands[cat]);
helpEmbed.setFooter(prefix + 'help [command] for more info to each command'); helpEmbed.setFooter(prefix + 'help [command] for more info to each command');
this._logger.silly('Created help embed');
return helpEmbed; return helpEmbed;
} }

@ -11,13 +11,11 @@ class MusicCommandModule extends cmdLib.CommandModule {
/** /**
* @param opts {Object} properties: * @param opts {Object} properties:
* getGuildHandler - a function to get the guild handler for a guild. * getGuildHandler - a function to get the guild handler for a guild.
* logger - the logger instance
*/ */
constructor(opts) { constructor(opts) {
super(cmdLib.CommandScopes.Guild); super(cmdLib.CommandScopes.Guild);
this.templateFile = location + '/MusicCommandsTemplate.yaml'; this.templateFile = location + '/MusicCommandsTemplate.yaml';
this._getGuildHandler = opts.getGuildHandler; this._getGuildHandler = opts.getGuildHandler;
this._logger = opts.logger;
} }
/** /**

@ -10,7 +10,6 @@ class ServerUtilityCommandModule extends cmdLib.CommandModule {
* @param opts {Object} properties: * @param opts {Object} properties:
* getGuildHandler - a function to get the guild handler for the guild * getGuildHandler - a function to get the guild handler for the guild
* messagehandler - the MessageHandler instance * messagehandler - the MessageHandler instance
* logger - the instance of the logger.
* config - the config object * config - the config object
*/ */
constructor(opts) { constructor(opts) {
@ -18,7 +17,6 @@ class ServerUtilityCommandModule extends cmdLib.CommandModule {
this.templateFile = location + '/ServerUtilityCommandsTemplate.yaml'; this.templateFile = location + '/ServerUtilityCommandsTemplate.yaml';
this._messageHandler = opts.messageHandler; this._messageHandler = opts.messageHandler;
this._getGuildHandler = opts.getGuildHandler; this._getGuildHandler = opts.getGuildHandler;
this._logger = opts.logger;
this._config = opts.config; this._config = opts.config;
} }

@ -14,14 +14,12 @@ class UtilityCommandModule extends cmdLib.CommandModule {
/** /**
* @param opts {Object} properties: * @param opts {Object} properties:
* bot - the instance of the bot. * bot - the instance of the bot.
* logger - the instance of the logger.
* config - the config object * config - the config object
*/ */
constructor(opts) { constructor(opts) {
super(cmdLib.CommandScopes.User); super(cmdLib.CommandScopes.User);
this.templateFile = location + '/UtilityCommandsTemplate.yaml'; this.templateFile = location + '/UtilityCommandsTemplate.yaml';
this._bot = opts.bot; this._bot = opts.bot;
this._logger = opts.logger;
this._config = opts.config; this._config = opts.config;
} }

@ -2,16 +2,10 @@ const music = require('./MusicLib'),
utils = require('./utils'), utils = require('./utils'),
config = require('../config.json'), config = require('../config.json'),
sqliteAsync = require('./sqliteAsync'), sqliteAsync = require('./sqliteAsync'),
logging = require('./logging'),
fs = require('fs-extra'), fs = require('fs-extra'),
dataDir = config.dataPath || './data'; dataDir = config.dataPath || './data';
let logger = require('winston');
exports.setLogger = function (newLogger) {
logger = newLogger;
music.setLogger(logger);
};
/** /**
* The Guild Handler handles guild settings and data. * The Guild Handler handles guild settings and data.
* @type {GuildHandler} * @type {GuildHandler}
@ -20,7 +14,9 @@ class GuildHandler {
constructor(guild) { constructor(guild) {
this.guild = guild; this.guild = guild;
this._logger = new logging.Logger(`${this.constructor.name}@${this.guild}`);
this.musicPlayer = new music.MusicPlayer(null); this.musicPlayer = new music.MusicPlayer(null);
this._logger.silly('Initialized Guild Handler');
} }
/** /**
@ -28,10 +24,12 @@ class GuildHandler {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async initDatabase() { async initDatabase() {
this._logger.silly('Initializing Database');
await fs.ensureDir(dataDir + '/gdb'); await fs.ensureDir(dataDir + '/gdb');
this.db = new sqliteAsync.Database(`${dataDir}/gdb/${this.guild}.db`); this.db = new sqliteAsync.Database(`${dataDir}/gdb/${this.guild}.db`);
await this.db.init(); await this.db.init();
logger.debug(`Connected to the database for ${this.guild}`); this._logger.debug(`Connected to the database for ${this.guild}`);
this._logger.debug('Creating Databases');
await this.createTables(); await this.createTables();
} }
@ -39,7 +37,9 @@ class GuildHandler {
* Destroys the guild handler * Destroys the guild handler
*/ */
destroy() { destroy() {
this._logger.debug('Ending musicPlayer');
this.musicPlayer.stop(); this.musicPlayer.stop();
this._logger.debug('Ending Database');
this.db.close(); this.db.close();
} }
@ -57,16 +57,19 @@ class GuildHandler {
author_name VARCHAR(128), author_name VARCHAR(128),
content TEXT NOT NULL content TEXT NOT NULL
)`); )`);
this._logger.silly('Created Table messages');
await this.db.run(`${utils.sql.tableExistCreate} playlists ( await this.db.run(`${utils.sql.tableExistCreate} playlists (
${utils.sql.pkIdSerial}, ${utils.sql.pkIdSerial},
name VARCHAR(32) UNIQUE NOT NULL, name VARCHAR(32) UNIQUE NOT NULL,
url VARCHAR(255) NOT NULL url VARCHAR(255) NOT NULL
)`); )`);
this._logger.silly('Created Table playlists');
await this.db.run(`${utils.sql.tableExistCreate} commands ( await this.db.run(`${utils.sql.tableExistCreate} commands (
${utils.sql.pkIdSerial}, ${utils.sql.pkIdSerial},
name VARCHAR(32) UNIQUE NOT NULL, name VARCHAR(32) UNIQUE NOT NULL,
command VARCHAR(255) NOT NULL command VARCHAR(255) NOT NULL
)`); )`);
this._logger.silly('Created Table commands');
} }
} }

@ -1,26 +1,31 @@
/* eslint-disable no-unused-vars */ /* eslint-disable no-unused-vars */
const winston = require('winston'), const winston = require('winston'),
DailyRotateFile = require('winston-daily-rotate-file'), DailyRotateFile = require('winston-daily-rotate-file'),
args = require('args-parser')(process.argv), args = require('args-parser')(process.argv);
fileLoggingFormat = winston.format.printf(info => { /**
return `${info.timestamp} ${info.level.toUpperCase()}: ${JSON.stringify(info.message)}`; // the logging format for files * Set console format to simple string format
}), * @type {Format}
consoleLoggingFormat = winston.format.printf(info => { */
return `${info.timestamp} {${info.label}} [${info.level}] ${JSON.stringify(info.message)}`; //the logging format for the console const consoleLoggingFormat = winston.format.printf(info => {
}), return `${info.timestamp} {${info.module || info.m || 'DEFAULT'}} [${info.level}] ${JSON.stringify(info.message)}`; //the logging format for the console
loggingFullFormat = winston.format.combine( });
winston.format.splat(),
/**
* Set full format to combination of formats
* @type {Format}
*/
const loggingFullFormat = winston.format.combine(
winston.format.timestamp({ winston.format.timestamp({
format: 'YY-MM-DD HH:mm:ss.SSS' format: 'YY-MM-DD HH:mm:ss.SSS'
}), }),
winston.format.label({label: ''}),
winston.format.json() winston.format.json()
); );
let logger = winston.createLogger({ /**
level: winston.config.npm.levels, // logs with npm levels * Define all transports used.
format: loggingFullFormat, * @type {any[]}
transports: [ */
let transports = [
new winston.transports.Console({ new winston.transports.Console({
format: winston.format.combine( format: winston.format.combine(
winston.format.colorize(), winston.format.colorize(),
@ -47,20 +52,71 @@ let logger = winston.createLogger({
maxFiles: '30d', maxFiles: '30d',
json: true json: true
}) })
] ];
});
//class SpecialLogger extends winston.
/** /**
* A function to return the logger that has been created after appending an exception handler * Define the logger
* @returns {Object} * @type {winston.Logger}
*/ */
exports.getLogger = function () { let logger = winston.createLogger({
level: winston.config.npm.levels,
format: loggingFullFormat,
transports: transports
});
// Define exception handling
logger.exceptions.handle( logger.exceptions.handle(
new winston.transports.File({ new winston.transports.File({
filename: './.log/exceptions.log' filename: './.log/exceptions.log'
}) })
); );
return logger;
}; class ModuleLogger {
constructor(moduleInstance) {
this.logger = logger;
if (moduleInstance.constructor) {
switch (moduleInstance.constructor.name) {
case 'String':
this.logName = moduleInstance;
break;
case 'Number':
this.logName = moduleInstance.toString();
break;
default:
this.logName = moduleInstance.constructor.name;
}
} else {
this.logName = moduleInstance.toString();
}
}
silly(msg, meta) {
logger.silly(msg, {m: this.logName, ...meta});
}
debug(msg, meta) {
logger.debug(msg, {m: this.logName, ...meta});
}
verbose(msg, meta) {
logger.verbose(msg, {m: this.logName, ...meta});
}
info(msg, meta) {
logger.info(msg, {m: this.logName, ...meta});
}
warn(msg, meta) {
logger.warn(msg, {m: this.logName, ...meta});
}
error(msg, meta) {
logger.error(msg, {m: this.logName, ...meta});
}
}
Object.assign(exports, {
logger: logger,
Logger: ModuleLogger
});

@ -11,7 +11,7 @@ function noOp() {
* @param {String} filename The name of the file. * @param {String} filename The name of the file.
* @return {String} A string that represents the file-extension. * @return {String} A string that represents the file-extension.
*/ */
exports.getExtension = function (filename) { function getFileExtension (filename) {
if (!filename) if (!filename)
return null; return null;
try { try {
@ -24,7 +24,7 @@ exports.getExtension = function (filename) {
console.error(error); console.error(error);
return null; return null;
} }
}; }
/** /**
* Walks the path to the objects attribute and returns the value. * Walks the path to the objects attribute and returns the value.
@ -32,7 +32,7 @@ exports.getExtension = function (filename) {
* @param attributePath * @param attributePath
* @returns {undefined/Object} * @returns {undefined/Object}
*/ */
exports.objectDeepFind = function (object, attributePath) { function objectDeepFind (object, attributePath) {
let current = object, let current = object,
paths = attributePath.split('.'); paths = attributePath.split('.');
for (let path of paths) for (let path of paths)
@ -42,7 +42,7 @@ exports.objectDeepFind = function (object, attributePath) {
return undefined; return undefined;
return current; return current;
}; }
/** /**
* Shuffles an array with Fisher-Yates Shuffle * Shuffles an array with Fisher-Yates Shuffle
@ -74,7 +74,7 @@ exports.shuffleArray = function(array) {
* @constructor * @constructor
* @author CanyonCasa & Pier-Luc Gendreau on StackOverflow * @author CanyonCasa & Pier-Luc Gendreau on StackOverflow
*/ */
exports.Cleanup = function Cleanup(callback) { function Cleanup(callback) {
// attach user callback to the process event emitter // attach user callback to the process event emitter
// if no callback, it will still exit gracefully on Ctrl-C // if no callback, it will still exit gracefully on Ctrl-C
@ -98,9 +98,9 @@ exports.Cleanup = function Cleanup(callback) {
console.log(e.stack); console.log(e.stack);
process.exit(99); process.exit(99);
}); });
}; }
exports.getSplitDuration = function (duration) { function getSplitDuration (duration) {
let dur = duration; let dur = duration;
let retObj = {}; let retObj = {};
retObj.milliseconds = dur % 1000; retObj.milliseconds = dur % 1000;
@ -113,23 +113,23 @@ exports.getSplitDuration = function (duration) {
dur = Math.floor(dur / 24); dur = Math.floor(dur / 24);
retObj.days = dur; retObj.days = dur;
return retObj; return retObj;
}; }
/** /**
* Resolves a nested promise by resolving it iterative. * Resolves a nested promise by resolving it iterative.
* @param promise * @param promise
* @returns {Promise<*>} * @returns {Promise<*>}
*/ */
exports.resolveNestedPromise = async function(promise) { async function resolveNestedPromise (promise) {
let result = await promise; let result = await promise;
while (result instanceof Promise) while (result instanceof Promise)
result = await result; // eslint-disable-line no-await-in-loop result = await result; // eslint-disable-line no-await-in-loop
return result; return result;
}; }
/* Classes */ /* Classes */
exports.YouTube = class { class YouTube {
/** /**
* returns if an url is a valid youtube url (without checking for an entity id) * returns if an url is a valid youtube url (without checking for an entity id)
* @param url * @param url
@ -220,9 +220,9 @@ exports.YouTube = class {
let id = exports.YouTube.getVideoIdFromUrl(url); let id = exports.YouTube.getVideoIdFromUrl(url);
return id? `https://i3.ytimg.com/vi/${id}/maxresdefault.jpg` : null; return id? `https://i3.ytimg.com/vi/${id}/maxresdefault.jpg` : null;
} }
}; }
exports.ConfigVerifyer = class { class ConfigVerifyer {
/** /**
* @param confObj * @param confObj
* @param required {Array} the attributes that are required for the bot to work * @param required {Array} the attributes that are required for the bot to work
@ -255,7 +255,7 @@ exports.ConfigVerifyer = class {
logger.error(`Missing required Attributes ${this.missingAttributes.join(', ')}`); logger.error(`Missing required Attributes ${this.missingAttributes.join(', ')}`);
} }
}; }
exports.sql = { exports.sql = {
tableExistCreate: 'CREATE TABLE IF NOT EXISTS', tableExistCreate: 'CREATE TABLE IF NOT EXISTS',
@ -270,3 +270,14 @@ exports.logLevels = {
'warn': 3, 'warn': 3,
'error:': 4 'error:': 4
}; };
Object.assign(exports, {
resolveNestedPromise: resolveNestedPromise,
YouTube: YouTube,
ConfigVerifyer: ConfigVerifyer,
getSplitDuration: getSplitDuration,
getExtension: getFileExtension,
getFileExtension: getFileExtension,
objectDeepFind: objectDeepFind,
Cleanup: Cleanup
});

@ -3,7 +3,6 @@ const mockobjects = require('./mockobjects.js'),
sinon = require('sinon'), sinon = require('sinon'),
assert = require('assert'), assert = require('assert'),
rewire = require('rewire'); rewire = require('rewire');
let Discord = require("discord.js");
mockobjects.mockLogger = { mockobjects.mockLogger = {
error: () => {}, error: () => {},
@ -180,7 +179,7 @@ describe('lib/utils', function() {
describe('lib/music', function() { describe('lib/music', function() {
const music = rewire('../lib/music'); const music = rewire('../lib/MusicLib');
const Readable = require('stream').Readable; const Readable = require('stream').Readable;
music.__set__("logger", mockobjects.mockLogger); music.__set__("logger", mockobjects.mockLogger);
@ -204,7 +203,7 @@ describe('lib/music', function() {
describe('#MusicPlayer', function () { describe('#MusicPlayer', function () {
it('connects to a VoiceChannel', function (done) { it('connects to a VoiceChannel', function (done) {
let dj = new music.DJ(mockobjects.mockVoicechannel); let dj = new music.MusicPlayer(mockobjects.mockVoicechannel);
dj.connect().then(()=> { dj.connect().then(()=> {
assert(dj.connected); assert(dj.connected);
done(); done();
@ -212,7 +211,7 @@ describe('lib/music', function() {
}); });
it('listens on Repeat', function () { it('listens on Repeat', function () {
let dj = new music.DJ(mockobjects.mockVoicechannel); let dj = new music.MusicPlayer(mockobjects.mockVoicechannel);
dj.current = {'url': '', 'title': ''}; dj.current = {'url': '', 'title': ''};
dj.listenOnRepeat = true; dj.listenOnRepeat = true;
@ -222,7 +221,7 @@ describe('lib/music', function() {
it('plays Files', function (done) { it('plays Files', function (done) {
let dj = new music.DJ(mockobjects.mockVoicechannel); let dj = new music.MusicPlayer(mockobjects.mockVoicechannel);
dj.connect().then(() => { dj.connect().then(() => {
dj.playFile(); dj.playFile();
assert(dj.playing); assert(dj.playing);
@ -231,7 +230,7 @@ describe('lib/music', function() {
}); });
it('plays YouTube urls', function (done) { it('plays YouTube urls', function (done) {
let dj = new music.DJ(mockobjects.mockVoicechannel); let dj = new music.MusicPlayer(mockobjects.mockVoicechannel);
dj.connect().then(() => { dj.connect().then(() => {
dj.playYouTube('http://www.youtube.com/watch?v=ABCDEFGHIJK'); dj.playYouTube('http://www.youtube.com/watch?v=ABCDEFGHIJK');
setTimeout(() => { setTimeout(() => {
@ -242,7 +241,7 @@ describe('lib/music', function() {
}); });
it('gets the video name', function (done) { it('gets the video name', function (done) {
let dj = new music.DJ(mockobjects.mockVoicechannel); let dj = new music.MusicPlayer(mockobjects.mockVoicechannel);
dj.getVideoName('http://www.youtube.com/watch?v=ABCDEFGHIJK').then((name) => { dj.getVideoName('http://www.youtube.com/watch?v=ABCDEFGHIJK').then((name) => {
assert(name === 'test'); assert(name === 'test');
done(); done();
@ -250,7 +249,7 @@ describe('lib/music', function() {
}); });
it('sets the volume', function(done) { it('sets the volume', function(done) {
let dj = new music.DJ(mockobjects.mockVoicechannel); let dj = new music.MusicPlayer(mockobjects.mockVoicechannel);
dj.connect().then(() => { dj.connect().then(() => {
dj.playFile(); dj.playFile();
dj.setVolume(100); dj.setVolume(100);
@ -260,7 +259,7 @@ describe('lib/music', function() {
}); });
it('pauses playback', function(done) { it('pauses playback', function(done) {
let dj = new music.DJ(mockobjects.mockVoicechannel); let dj = new music.MusicPlayer(mockobjects.mockVoicechannel);
dj.connect().then(() => { dj.connect().then(() => {
dj.playFile(); dj.playFile();
dj.pause(); dj.pause();
@ -269,7 +268,7 @@ describe('lib/music', function() {
}); });
it('resumes playback', function(done) { it('resumes playback', function(done) {
let dj = new music.DJ(mockobjects.mockVoicechannel); let dj = new music.MusicPlayer(mockobjects.mockVoicechannel);
dj.connect().then(() => { dj.connect().then(() => {
dj.playFile(); dj.playFile();
dj.resume(); dj.resume();
@ -278,7 +277,7 @@ describe('lib/music', function() {
}); });
it('stops playback', function(done) { it('stops playback', function(done) {
let dj = new music.DJ(mockobjects.mockVoicechannel); let dj = new music.MusicPlayer(mockobjects.mockVoicechannel);
dj.connect().then(() => { dj.connect().then(() => {
dj.playFile(); dj.playFile();
assert(dj.playing); assert(dj.playing);
@ -289,7 +288,7 @@ describe('lib/music', function() {
}); });
it('skips songs', function(done) { it('skips songs', function(done) {
let dj = new music.DJ(mockobjects.mockVoicechannel); let dj = new music.MusicPlayer(mockobjects.mockVoicechannel);
dj.connect().then(() => { 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');
@ -302,7 +301,7 @@ describe('lib/music', function() {
}); });
it('returns a playlist', function(done) { it('returns a playlist', function(done) {
let dj = new music.DJ(mockobjects.mockVoicechannel); let dj = new music.MusicPlayer(mockobjects.mockVoicechannel);
dj.connect().then(() => { dj.connect().then(() => {
dj.queue = [{ dj.queue = [{
'title': 'title', 'title': 'title',
@ -315,7 +314,7 @@ describe('lib/music', function() {
}); });
it('clears the queue', function(done) { it('clears the queue', function(done) {
let dj = new music.DJ(mockobjects.mockVoicechannel); let dj = new music.MusicPlayer(mockobjects.mockVoicechannel);
dj.connect().then(() => { dj.connect().then(() => {
dj.queue = [{ dj.queue = [{
'title': 'title', 'title': 'title',
@ -330,163 +329,63 @@ describe('lib/music', function() {
}); });
}); });
describe('lib/cmd', function() { describe('lib/CommandLib', function() {
const cmd = rewire('../lib/cmd'); let cmdLib = require('../lib/CommandLib');
cmd.__set__("logger", mockobjects.mockLogger);
describe('#Servant', function() { describe('Answer', function() {
it('creates commands', function() { it('evaluates synchronous', async function() {
let servant = new cmd.Servant(''); let answer = new cmdLib.Answer(() => 'RESPONSE');
servant.createCommand(mockobjects.mockCommand, mockobjects.mockCommand.textReply); assert((await answer.evaluate({}, {}, {})) === 'RESPONSE');
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('removes commands', function() { it('evaluates asynchronous', async function() {
let servant = new cmd.Servant(''); let answer = new cmdLib.Answer(async () => {
servant.createCommand(mockobjects.mockCommand, mockobjects.mockCommand.textReply); return 'RESPONSE';
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);
});
}); });
assert((await answer.evaluate({}, {}, {})) === 'RESPONSE');
})
}); });
describe('lib/guilding', function*() { // deactivated because of problems with sqlite3 and rewire describe('Command', function() {
const guilding = rewire('../lib/guilding');
const servercommands = require('../commands/servercommands');
guilding.__set__("sqliteAsync", null);
guilding.__set__("fs-extra", {
ensureDir: async() => {
return true;
}
});
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); it('answers with Answer objects', async function() {
music.__set__("yttl", (id, cb) => { let cmd = new cmdLib.Command({
cb(null, 'test'); name: 'TEST',
}); prefix: '',
music.__set__('ytdl', () => { description: 'TESTDESCRIPTION',
let s = new Readable(); permission: 'TESTPERM',
s._read = () => {}; usage: 'TESTUSAGE'
s.push('chunkofdataabc'); },new cmdLib.Answer(() => 'RESPONSE'));
s.push(null); assert((await cmd.answer({}, {}, {})) === 'RESPONSE');
return s;
}); });
let gh = new guilding.GuildHandler('test', '~');
gh.db = new mockobjects.MockDatabase('', ()=>{}); it('generates help for itself', function() {
gh.ready = true; let cmd = new cmdLib.Command({
gh.musicPlayer = new music.DJ(mockobjects.mockVoicechannel); name: 'TEST',
gh.connectAndPlay(mockobjects.mockVoicechannel, 'test', false).then(() => { prefix: '',
done(); description: 'TESTDESCRIPTION',
permission: 'TESTPERM',
usage: 'TESTUSAGE'
},new cmdLib.Answer(() => 'RESPONSE'));
assert(cmd.help);
})
}); });
}); });
it('handles all servercommands', function() { describe('lib/MessageLib', function() {
let gh = new guilding.GuildHandler('test', '~'); let msgLib = require('../lib/MessageLib');
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);
}
describe('MessageHandler', function() {
assert(msgSpy.called); it ('parses a command syntax', function() {
let msgHandler = new msgLib.MessageHandler({
on: () => {}
});
let parsedSyntax = msgHandler.parseSyntaxString('_help cmd&& _ping; _uptime');
assert(parsedSyntax.length === 2);
assert(parsedSyntax[0].length === 2);
assert(parsedSyntax[1].length === 1);
}); });
}); });
}); });

Loading…
Cancel
Save