Modifications

- replaced most promises with async/await
- added sqlite3 session storage
- restyled login page
- added session requirement to graphql
- deactivated incompatible
- modified mockobjects
- added fs-extra for async file operations
pull/33/head
Trivernis 6 years ago
parent 7ad8fc7ff2
commit 2a2f4abe66

192
bot.js

@ -1,12 +1,12 @@
const Discord = require("discord.js"), const Discord = require("discord.js"),
fs = require('fs'), fs = require('fs-extra'),
logger = require('./lib/logging').getLogger(), logger = require('./lib/logging').getLogger(),
cmd = require("./lib/cmd"), cmd = require("./lib/cmd"),
guilding = require('./lib/guilding'), guilding = require('./lib/guilding'),
utils = require('./lib/utils'), utils = require('./lib/utils'),
config = require('./config.json'), config = require('./config.json'),
args = require('args-parser')(process.argv), args = require('args-parser')(process.argv),
sqlite3 = require('sqlite3'), sqliteAsync = require('./lib/sqliteAsync'),
authToken = args.token || config.api.botToken, authToken = args.token || config.api.botToken,
prefix = args.prefix || config.prefix || '~', prefix = args.prefix || config.prefix || '~',
gamepresence = args.game || config.presence; gamepresence = args.game || config.presence;
@ -14,7 +14,7 @@ const Discord = require("discord.js"),
let weblib = null; let weblib = null;
class Bot { class Bot {
constructor(callback) { constructor() {
this.client = new Discord.Client(); this.client = new Discord.Client();
this.mention = false; this.mention = false;
this.rotator = null; this.rotator = null;
@ -22,21 +22,6 @@ class Bot {
this.presences = []; this.presences = [];
this.guildHandlers = []; this.guildHandlers = [];
logger.verbose('Registering cleanup function');
utils.Cleanup(() => {
for (let gh in Object.values(this.guildHandlers))
if (gh instanceof guilding.GuildHandler)
gh.destroy();
this.client.destroy().then(() => {
logger.debug('destroyed client');
}).catch((err) => {
logger.error(err.message);
logger.debug(err.stack);
});
this.maindb.close();
});
cmd.setLogger(logger);
logger.verbose('Verifying config'); logger.verbose('Verifying config');
let configVerifyer = new utils.ConfigVerifyer(config, [ let configVerifyer = new utils.ConfigVerifyer(config, [
@ -45,66 +30,77 @@ class Bot {
if (!configVerifyer.verifyConfig(logger)) if (!configVerifyer.verifyConfig(logger))
if (!args.i) { if (!args.i) {
logger.info('Invalid config. Exiting'); logger.info('Invalid config. Exiting');
logger.flush().then(() => {
process.exit(1); process.exit(1);
});
} }
cmd.setLogger(logger);
guilding.setLogger(logger); guilding.setLogger(logger);
cmd.init(prefix); }
logger.verbose('Registering commands');
this.registerCommands();
logger.debug('Checking for ./data/ existence');
utils.dirExistence('./data', () => { /**
logger.verbose('Connecting to main database'); * Initializes all services.
this.maindb = new sqlite3.Database('./data/main.db', (err) => { * @returns {Promise<void>}
if (err) { */
logger.error(err.message); async initServices() {
logger.debug(err.stack); logger.verbose('Registering cleanup function');
} else { utils.Cleanup(() => {
this.maindb.run(`${utils.sql.tableExistCreate} presences ( for (let gh in Object.values(this.guildHandlers))
${utils.sql.pkIdSerial}, if (gh instanceof guilding.GuildHandler)
text VARCHAR(255) UNIQUE NOT NULL gh.destroy();
)`, (err) => {
if (err) { this.client.destroy().then(() => {
logger.debug('destroyed client');
}).catch((err) => {
logger.error(err.message); logger.error(err.message);
logger.debug(err.stack); logger.debug(err.stack);
} else {
logger.debug('Loading presences');
this.loadPresences();
}
});
if (config.webservice && config.webservice.enabled)
this.initializeWebserver();
callback();
}
}); });
this.maindb.close();
}); });
await this.initializeDatabase();
if (config.webservice && config.webservice.enabled)
await this.initializeWebserver();
logger.verbose('Registering commands');
this.registerCommands();
this.registerCallbacks(); this.registerCallbacks();
cmd.init(prefix);
} }
/** /**
* Starting the bot by connecting to the discord service and starting the webservice. * Starting the bot by connecting to the discord service and starting the webservice.
* @returns {Promise<any>} * @returns {Promise<any>}
*/ */
start() { async start() {
return new Promise((resolve, reject) => { await this.client.login(authToken);
this.client.login(authToken).then(() => {
logger.debug("Logged in"); logger.debug("Logged in");
resolve();
}).catch((err) => {
reject(err);
});
if (this.webServer) { if (this.webServer) {
this.webServer.start(); this.webServer.start();
logger.info(`WebServer runing on port ${this.webServer.port}`); logger.info(`WebServer runing on port ${this.webServer.port}`);
} }
}); }
/**
* Initializes the database by checking first for the existence of the data folder.
* @returns {Promise<void>}
*/
async initializeDatabase() {
logger.debug('Checking for ./data/ existence');
await fs.ensureDir('./data');
logger.verbose('Connecting to main database');
this.maindb = new sqliteAsync.Database('./data/main.db');
await this.maindb.init();
await this.maindb.run(`${utils.sql.tableExistCreate} presences (
${utils.sql.pkIdSerial},
text VARCHAR(255) UNIQUE NOT NULL
)`);
logger.debug('Loading Presences...');
await this.loadPresences();
} }
/** /**
* initializes the api webserver * initializes the api webserver
*/ */
initializeWebserver() { async initializeWebserver() {
logger.verbose('Importing weblib'); logger.verbose('Importing weblib');
weblib = require('./lib/weblib'); weblib = require('./lib/weblib');
weblib.setLogger(logger); weblib.setLogger(logger);
@ -112,7 +108,7 @@ class Bot {
this.webServer = new weblib.WebServer(config.webservice.port || 8080); this.webServer = new weblib.WebServer(config.webservice.port || 8080);
logger.debug('Setting Reference Objects to webserver'); logger.debug('Setting Reference Objects to webserver');
this.webServer.setReferenceObjects({ await this.webServer.setReferenceObjects({
client: this.client, client: this.client,
presences: this.presences, presences: this.presences,
maindb: this.maindb, maindb: this.maindb,
@ -129,8 +125,8 @@ class Bot {
* pushed in there. If the presences.txt file does not exist, the data is just read from the database. In the end * 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. * a rotator is created that rotates the presence every configured duration.
*/ */
loadPresences() { async loadPresences() {
if (fs.existsSync('./data/presences.txt')) { if (await fs.pathExists('./data/presences.txt')) {
let lineReader = require('readline').createInterface({ let lineReader = require('readline').createInterface({
input: require('fs').createReadStream('./data/presences.txt') input: require('fs').createReadStream('./data/presences.txt')
}); });
@ -144,32 +140,17 @@ class Bot {
}); });
this.rotator = this.client.setInterval(() => this.rotatePresence(), this.rotator = this.client.setInterval(() => this.rotatePresence(),
config.presence_duration || 360000); config.presence_duration || 360000);
fs.unlink('./data/presences.txt', (err) => { await fs.unlink('./data/presences.txt');
if (err) let rows = await this.maindb.all('SELECT text FROM presences');
logger.warn(err.message);
});
this.maindb.all('SELECT text FROM presences', (err, rows) => {
if (err)
logger.warn(err.message);
else
for (let row of rows) for (let row of rows)
if (!(row[0] in this.presences)) if (!(row[0] in this.presences))
this.presences.push(row.text); this.presences.push(row.text);
});
} else { } else {
this.maindb.all('SELECT text FROM presences', (err, rows) => { let rows = await this.maindb.all('SELECT text FROM presences');
if (err)
logger.warn(err.message);
else
for (let row of rows) for (let row of rows)
this.presences.push(row.text); this.presences.push(row.text);
this.rotator = this.client.setInterval(() => this.rotatePresence(), this.rotator = this.client.setInterval(() => this.rotatePresence(),
config.presence_duration || 360000); config.presence_duration || 360000);
});
} }
} }
@ -183,42 +164,38 @@ class Bot {
}, [], "Repeats what you say"); }, [], "Repeats what you say");
// adds a presence that will be saved in the presence file and added to the rotation // adds a presence that will be saved in the presence file and added to the rotation
cmd.createGlobalCommand(prefix + 'addpresence', (msg, argv, args) => { cmd.createGlobalCommand(prefix + 'addpresence', async (msg, argv, args) => {
let p = args.join(' '); let p = args.join(' ');
this.presences.push(p); this.presences.push(p);
this.maindb.run('INSERT INTO presences (text) VALUES (?)', [p], (err) => { await this.maindb.run('INSERT INTO presences (text) VALUES (?)', [p]);
if (err)
logger.warn(err.message);
});
return `Added Presence \`${p}\``; return `Added Presence \`${p}\``;
}, [], "Adds a presence to the rotation.", 'owner'); }, [], "Adds a presence to the rotation.", 'owner');
// shuts down the bot after destroying the client // shuts down the bot after destroying the client
cmd.createGlobalCommand(prefix + 'shutdown', (msg) => { cmd.createGlobalCommand(prefix + 'shutdown', async (msg) => {
try {
msg.reply('Shutting down...').catch((err) => { await msg.reply('Shutting down...');
logger.error(err.message);
logger.debug(err.stack);
}).finally(() => {
logger.debug('Destroying client...'); logger.debug('Destroying client...');
} catch(err) {
this.client.destroy().catch((err) => {
logger.error(err.message); logger.error(err.message);
logger.debug(err.stack); logger.debug(err.stack);
}).finally(() => { }
try {
await this.client.destroy();
logger.debug('Exiting server...'); logger.debug('Exiting server...');
} catch (err) {
this.webServer.stop().then(() => { logger.error(err.message);
logger.debug(err.stack);
}
try {
await this.webServer.stop();
logger.debug(`Exiting Process...`); logger.debug(`Exiting Process...`);
process.exit(0); process.exit(0);
}).catch((err) => { } catch(err) {
logger.error(err.message); logger.error(err.message);
logger.debug(err.stack); logger.debug(err.stack);
process.exit(0); }
});
});
});
}, [], "Shuts the bot down.", 'owner'); }, [], "Shuts the bot down.", 'owner');
// forces a presence rotation // forces a presence rotation
@ -310,7 +287,7 @@ class Bot {
}); });
}); });
this.client.on('message', msg => { this.client.on('message', async (msg) => {
try { try {
if (msg.author === this.client.user) { if (msg.author === this.client.user) {
logger.verbose(`ME: ${msg.content}`); logger.verbose(`ME: ${msg.content}`);
@ -321,7 +298,8 @@ class Bot {
let reply = cmd.parseMessage(msg); let reply = cmd.parseMessage(msg);
this.answerMessage(msg, reply); this.answerMessage(msg, reply);
} else { } else {
this.getGuildHandler(msg.guild, prefix).handleMessage(msg); let gh = await this.getGuildHandler(msg.guild, prefix);
await gh.handleMessage(msg);
} }
} catch (err) { } catch (err) {
logger.error(err.message); logger.error(err.message);
@ -358,9 +336,12 @@ class Bot {
* @param prefix * @param prefix
* @returns {*} * @returns {*}
*/ */
getGuildHandler(guild, prefix) { async getGuildHandler(guild, prefix) {
if (!this.guildHandlers[guild.id]) if (!this.guildHandlers[guild.id]) {
this.guildHandlers[guild.id] = new guilding.GuildHandler(guild, prefix); let newGuildHandler = new guilding.GuildHandler(guild, prefix);
await newGuildHandler.initDatabase();
this.guildHandlers[guild.id] = newGuildHandler;
}
return this.guildHandlers[guild.id]; return this.guildHandlers[guild.id];
} }
} }
@ -369,10 +350,17 @@ 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... "); // log the current date so that the logfile is better to read. logger.info("Starting up... "); // log the current date so that the logfile is better to read.
let discordBot = new Bot(() => { logger.debug('Calling constructor...');
discordBot.start().catch((err) => { let discordBot = new Bot();
logger.debug('Initializing services...');
discordBot.initServices().then(() => {
logger.debug('Starting Bot...');
discordBot.start().catch((err) => { //eslint-disable-line promise/no-nesting
logger.error(err.message); logger.error(err.message);
logger.debug(err.stack); logger.debug(err.stack);
}); });
}).catch((err) => {
logger.error(err.message);
logger.debug(err.stack);
}); });
} }

@ -36,7 +36,8 @@
"success": "Added Song as next Song to the queue.", "success": "Added Song as next Song to the queue.",
"failure": "Failed adding 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!", "url_invalid": "This is not a valid url!",
"no_url": "I need an url to a video to play" "no_url": "I need an url to a video to play",
"no_voicechannel": "You need to join a voicechannel to do that!"
} }
}, },
"join": { "join": {

@ -2,10 +2,10 @@ const cmd = require('./cmd'),
music = require('./music'), music = require('./music'),
utils = require('./utils'), utils = require('./utils'),
config = require('../config.json'), config = require('../config.json'),
sqliteAsync = require('./sqliteAsync'),
fs = require('fs-extra'),
servercmd = require('../commands/servercommands'), servercmd = require('../commands/servercommands'),
sqlite3 = require('sqlite3'),
Discord = require('discord.js'), Discord = require('discord.js'),
handlers = {},
dataDir = config.dataPath || './data'; dataDir = config.dataPath || './data';
let logger = require('winston'); let logger = require('winston');
@ -26,26 +26,16 @@ exports.GuildHandler = class {
this.mention = false; this.mention = false;
this.prefix = prefix || config.prefix; this.prefix = prefix || config.prefix;
this.servant = new cmd.Servant(this.prefix); this.servant = new cmd.Servant(this.prefix);
this.ready = false; }
this.msgsQueue = [];
// checking if the data direcotry exists and if the gdb directory exists and creates them if they don't async initDatabase() {
utils.dirExistence(dataDir, () => { await fs.ensureDir(dataDir + '/gdb');
utils.dirExistence(dataDir + '/gdb', () => { this.db = new sqliteAsync.Database(`${dataDir}/gdb/${this.guild}.db`);
this.db = new sqlite3.Database(`${dataDir}/gdb/${guild}.db`, (err) => { await this.db.init();
if (err) logger.debug(`Connected to the database for ${this.guild}`);
logger.error(err.message); await this.createTables();
logger.debug(`Connected to the database for ${guild}`);
this.createTables();
// register commands // register commands
this.registerMusicCommands(); this.registerMusicCommands();
this.ready = true;
// handle all messages that have been received while not being ready
for (let i = 0; i < this.msgsQueue.length; i++)
this.handleMessage(this.msgsQueue.shift());
});
});
});
} }
/** /**
@ -62,15 +52,15 @@ exports.GuildHandler = class {
* messages - logs all messages send on the server * messages - logs all messages send on the server
* playlists - save playlists to play them later * playlists - save playlists to play them later
*/ */
createTables() { async createTables() {
this.db.run(`${utils.sql.tableExistCreate} messages ( await this.db.run(`${utils.sql.tableExistCreate} messages (
${utils.sql.pkIdSerial}, ${utils.sql.pkIdSerial},
creation_timestamp DATETIME NOT NULL, creation_timestamp DATETIME NOT NULL,
author VARCHAR(128) NOT NULL, author VARCHAR(128) NOT NULL,
author_name VARCHAR(128), author_name VARCHAR(128),
content TEXT NOT NULL content TEXT NOT NULL
)`); )`);
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
@ -88,14 +78,13 @@ exports.GuildHandler = class {
(this.mention) ? msg.reply('', answer) : msg.channel.send('', answer); (this.mention) ? msg.reply('', answer) : msg.channel.send('', answer);
} else if (answer instanceof Promise) { } else if (answer instanceof Promise) {
answer answer
.then((answer) => this.answerMessage(msg, answer)) .then((resolvedAnswer) => this.answerMessage(msg, resolvedAnswer))
.catch((error) => this.answerMessage(msg, error)); .catch((error) => this.answerMessage(msg, error));
} else { } else {
(this.mention) ? msg.reply(answer) : msg.channel.send(answer); (this.mention) ? msg.reply(answer) : msg.channel.send(answer);
} }
else else
logger.debug(`Empty answer won't be send.`); logger.debug(`Empty answer won't be send.`);
} }
/** /**
@ -103,22 +92,14 @@ exports.GuildHandler = class {
* replies or just sends the answer. * replies or just sends the answer.
* @param msg * @param msg
*/ */
handleMessage(msg) { async handleMessage(msg) {
if (this.ready) {
if (this.db) if (this.db)
this.db.run( await this.db.run(
'INSERT INTO messages (author, creation_timestamp, author_name, content) values (?, ?, ?, ?)', 'INSERT INTO messages (author, creation_timestamp, author_name, content) values (?, ?, ?, ?)',
[msg.author.id, msg.createdTimestamp, msg.author.username, msg.content], [msg.author.id, msg.createdTimestamp, msg.author.username, msg.content]
(err) => {
if (err)
logger.error(err.message);
}
); );
this.answerMessage(msg, this.servant.parseCommand(msg)); this.answerMessage(msg, this.servant.parseCommand(msg));
} else {
this.msgsQueue.push(msg);
}
} }
/** /**
@ -127,18 +108,13 @@ exports.GuildHandler = class {
* @param url * @param url
* @param next * @param next
*/ */
connectAndPlay(vc, url, next) { async connectAndPlay(vc, url, next) {
return new Promise((resolve, reject) => {
if (!this.dj.connected) { if (!this.dj.connected) {
this.dj.connect(vc).then(() => { await this.dj.connect(vc);
this.dj.playYouTube(url, next); this.dj.playYouTube(url, next);
resolve();
}).catch((err) => reject(err));
} else { } else {
this.dj.playYouTube(url, next); this.dj.playYouTube(url, next);
resolve();
} }
});
} }
/** /**
@ -147,82 +123,38 @@ exports.GuildHandler = class {
registerMusicCommands() { registerMusicCommands() {
this.dj = new music.DJ(); this.dj = new music.DJ();
// play command let playCb = async (msg, kwargs, argv, template, next) => {
this.servant.createCommand(servercmd.music.play, (msg, kwargs, argv) => {
return new Promise((resolve, reject) => {
let vc = this.dj.voiceChannel || msg.member.voiceChannel; let vc = this.dj.voiceChannel || msg.member.voiceChannel;
let url = kwargs['url']; let url = kwargs['url'];
if (!vc) if (!vc)
reject(servercmd.music.play.response.no_voicechannel); return template.response.no_voicechannel;
if (!url) if (!url)
reject(servercmd.music.play.response.no_url); return template.response.no_url;
if (!utils.YouTube.isValidEntityUrl(url)) { if (!utils.YouTube.isValidEntityUrl(url)) {
if (argv && argv.length > 0) if (argv && argv.length > 0)
url += ' ' + argv.join(' '); // join to get the whole expression behind the command url += ' ' + argv.join(' '); // join to get the whole expression behind the command
this.db.get('SELECT url FROM playlists WHERE name = ?', [url], (err, row) => { let row = await this.db.get('SELECT url FROM playlists WHERE name = ?', [url]);
if (err)
logger.error(err.message);
if (!row) { if (!row) {
reject(servercmd.music.play.response.url_invalid); logger.debug('Got invalid url for play command.');
logger.verbose('Got invalid url for play command.'); return template.response.url_invalid;
} else { } else {
url = row.url; await this.connectAndPlay(vc, row.url, next);
return template.response.success;
this.connectAndPlay(vc, url).then(() => {
resolve(servercmd.music.play.response.success);
}).catch((err) => {
logger.error(err.message);
reject(servercmd.music.play.response.failure);
});
} }
});
} else { } else {
this.connectAndPlay(vc, url).then(() => { await this.connectAndPlay(vc, url, next);
resolve(servercmd.music.play.response.success); return template.response.success;
}).catch((err) => {
logger.error(err.message);
reject(servercmd.music.play.response.failure);
});
} }
}); };
// play command
this.servant.createCommand(servercmd.music.play, async (msg, kwargs, argv) => {
return await playCb(msg, kwargs, argv, servercmd.music.play, false);
}); });
// playnext command // playnext command
this.servant.createCommand(servercmd.music.playnext, (msg, kwargs, argv) => { this.servant.createCommand(servercmd.music.playnext, async (msg, kwargs, argv) => {
return new Promise((resolve, reject) => { return await playCb(msg, kwargs, argv, servercmd.music.playnext, true);
let vc = msg.member.voiceChannel;
if (!this.dj.connected) this.dj.voiceChannel = vc;
let url = kwargs['url'];
if (!url) reject(servercmd.music.playnext.response.no_url);
if (!utils.YouTube.isValidEntityUrl(url)) {
if (argv)
url += ' ' + argv.join(' ');
this.db.get('SELECT url FROM playlists WHERE name = ?', [url], (err, row) => {
if (err)
logger.error(err.message);
if (!row) {
reject(servercmd.music.play.response.url_invalid);
} else {
url = row.url;
this.connectAndPlay(vc, url, true).then(() => {
resolve(servercmd.music.playnext.response.success);
}).catch((err) => {
logger.error(err.message);
reject(servercmd.music.play.response.failure);
});
}
});
} else {
this.connectAndPlay(vc, url, true).then(() => {
resolve(servercmd.music.playnext.response.success);
}).catch((err) => {
logger.error(err);
reject(servercmd.music.playnext.response.failure);
});
}
});
}); });
// join command // join command
@ -326,75 +258,29 @@ exports.GuildHandler = class {
}); });
// saves playlists // saves playlists
this.servant.createCommand(servercmd.music.save, (msg, kwargs, argv) => { this.servant.createCommand(servercmd.music.save, async (msg, kwargs, argv) => {
return new Promise((resolve, reject) => {
let saveName = argv.join(' '); let saveName = argv.join(' ');
this.db.get('SELECT COUNT(*) count FROM playlists WHERE name = ?', [saveName], (err, row) => { let row = await this.db.get('SELECT COUNT(*) count FROM playlists WHERE name = ?', [saveName]);
if (err) {
logger.error(err.message);
reject();
}
let cb = (err) => { // defining the callback for usage below
if (err)
logger.error(err.message);
else
resolve(`Saved song/playlist as ${saveName}`);
};
if (!row || row.count === 0) if (!row || row.count === 0)
this.db.run('INSERT INTO playlists (name, url) VALUES (?, ?)', [saveName, kwargs.url], cb); await this.db.run('INSERT INTO playlists (name, url) VALUES (?, ?)', [saveName, kwargs.url]);
else else
this.db.run('UPDATE playlists SET url = ? WHERE name = ?', [kwargs.url, saveName], cb); await this.db.run('UPDATE playlists SET url = ? WHERE name = ?', [kwargs.url, saveName]);
return `Saved song/playlist as ${saveName}`;
});
});
}); });
// saved command - prints out saved playlists // saved command - prints out saved playlists
this.servant.createCommand(servercmd.music.saved, (msg) => { this.servant.createCommand(servercmd.music.saved, async (msg) => {
return new Promise((resolve, reject) => {
let response = ''; let response = '';
this.db.all('SELECT name, url FROM playlists', (err, rows) => { let rows = await this.db.all('SELECT name, url FROM playlists');
if (err) {
logger.error(err.message);
reject();
}
for (let row of rows) for (let row of rows)
response += `[${row.name}](${row.url})\n`; response += `[${row.name}](${row.url})\n`;
if (rows.length === 0) { if (rows.length === 0)
msg.channel.send(servercmd.music.saved.response.no_saved); msg.channel.send(servercmd.music.saved.response.no_saved);
} else { else
let richEmbed = new Discord.RichEmbed() return new Discord.RichEmbed()
.setTitle('Saved Songs and Playlists') .setTitle('Saved Songs and Playlists')
.setDescription(response); .setDescription(response);
resolve(richEmbed);
}
});
});
}); });
} }
}; };
/**
* @param guild
* @param prefix
* @returns {GuildHandler}
* @deprecated use Bot class method instead
*/
exports.getHandler = function (guild, prefix) {
if (!handlers[guild.id])
handlers[guild.id] = new this.GuildHandler(guild, prefix);
return handlers[guild.id];
};
/**
* Destroy all handlers to safely end all sql3-clients.
* @deprecated automated in Bot class cleanup
*/
exports.destroyAll = function () {
logger.debug('Destroying all handlers...');
for (let key in Object.keys(handlers))
if (handlers[key])
handlers[key].destroy();
};

@ -7,10 +7,13 @@ const ytdl = require("ytdl-core"),
ytapiKey = args.ytapi || config.api.youTubeApiKey; ytapiKey = args.ytapi || config.api.youTubeApiKey;
/* Variable Definition */ /* Variable Definition */
let logger = require('winston'); let logger = require('winston');
let djs = {};
/* Function Definition */ /* Function Definition */
exports.setLogger = function (newLogger) {
logger = newLogger;
};
exports.DJ = class { exports.DJ = class {
constructor(voiceChannel) { constructor(voiceChannel) {
this.conn = null; this.conn = null;
@ -29,20 +32,16 @@ exports.DJ = class {
* When the bot was moved and connect is executed again, it connects to the initial VoiceChannel because the * When the bot was moved and connect is executed again, it connects to the initial VoiceChannel because the
* VoiceChannel is saved as object variable. * VoiceChannel is saved as object variable.
*/ */
connect(voiceChannel) { async connect(voiceChannel) {
return new Promise((resolve, reject) => {
this.voiceChannel = voiceChannel || this.voiceChannel; this.voiceChannel = voiceChannel || this.voiceChannel;
if (this.connected) if (this.connected)
this.stop(); this.stop();
logger.verbose(`Connecting to voiceChannel ${this.voiceChannel.name}`); logger.verbose(`Connecting to voiceChannel ${this.voiceChannel.name}`);
this.voiceChannel.join().then(connection => { let connection = await this.voiceChannel.join();
logger.info(`Connected to Voicechannel ${this.voiceChannel.name}`); logger.info(`Connected to Voicechannel ${this.voiceChannel.name}`);
this.conn = connection; this.conn = connection;
this.checkListeners(); this.checkListeners();
resolve();
}).catch((error) => reject(error));
});
} }
/** /**
@ -117,7 +116,15 @@ setTimeout(() => this.checkListeners(), 10000);
logger.debug(`Adding playlist ${plist} to queue`); 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 = await this.getVideoName(firstSong); let firstSongTitle = null;
try {
firstSongTitle = await this.getVideoName(firstSong);
} catch(err) {
if (err.message !== 'Not found') {
logger.warn(err.message);
logger.debug(err.stack);
}
}
if (this.repeat) if (this.repeat)
this.queue.push({'url': firstSong, 'title': firstSongTitle}); this.queue.push({'url': firstSong, 'title': firstSongTitle});
@ -125,12 +132,18 @@ setTimeout(() => this.checkListeners(), 10000);
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);
try {
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) {
if (err.message !== 'Not found') {
logger.warn(err.message);
logger.debug(err.stack);
}
}
} }
logger.debug(`Added ${playlistItems.length} songs to the queue`); 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}`); 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,
@ -158,7 +171,6 @@ setTimeout(() => this.checkListeners(), 10000);
this.queue.unshift({'url': url, 'title': await this.getVideoName(url)}); this.queue.unshift({'url': url, 'title': await this.getVideoName(url)});
else else
this.queue.push({'url': url, 'title': await this.getVideoName(url)}); this.queue.push({'url': url, 'title': await this.getVideoName(url)});
} }
} }
@ -263,7 +275,10 @@ setTimeout(() => this.checkListeners(), 10000);
this.playing = false; this.playing = false;
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); this.playYouTube(this.current.url).catch((err) => {
logger.error(err.message);
logger.debug(err.stack);
});
} else { } else {
this.stop(); this.stop();
} }
@ -292,149 +307,3 @@ setTimeout(() => this.checkListeners(), 10000);
this.queue = []; this.queue = [];
} }
}; };
/**
* Getting the logger;
* @param {Object} newLogger
*/
exports.setLogger = function (newLogger) {
logger = newLogger;
};
/**
* Connects to a voicechannel
* @param voiceChannel
* @deprecated
*/
exports.connect = function (voiceChannel) {
let gid = voiceChannel.guild.id;
let voiceDJ = new this.DJ(voiceChannel);
djs[gid] = voiceDJ;
return voiceDJ.connect();
};
/**
* Plays a file
* @param filename
* @param guildId
* @deprecated
*/
exports.playFile = function (guildId, filename) {
djs[guildId].playFile(filename);
};
/**
* Plays a YT Url
* @param voiceChannel
* @param url
* @deprecated
*/
exports.play = function (voiceChannel, url) {
let guildId = voiceChannel.guild.id;
if (!djs[guildId])
this.connect(voiceChannel).then(() => {
djs[guildId].playYouTube(url);
});
else
djs[guildId].playYouTube(url);
};
/**
* plays the given url as next song
* @param voiceChannel
* @param url
* @deprecated
*/
exports.playnext = function (voiceChannel, url) {
let guildId = voiceChannel.guild.id;
if (!djs[guildId])
this.connect(voiceChannel).then(() => {
djs[guildId].playYouTube(url, true);
});
else
djs[guildId].playYouTube(url, true);
};
/**
* Sets the volume of the music
* @param percentage
* @param guildId
* @deprecated
*/
exports.setVolume = function (guildId, percentage) {
djs[guildId].setVolume(percentage);
};
/**
* pauses the music
* @deprecated
*/
exports.pause = function (guildId) {
djs[guildId].pause();
};
/**
* Resumes the music
* @param guildId
* @deprecated
*/
exports.resume = function (guildId) {
djs[guildId].resume();
};
/**
* Stops the music
* @param guildId
* @deprecated
*/
exports.stop = function (guildId) {
djs[guildId].stop();
delete djs[guildId];
};
/**
* Skips the song
* @param guildId
* @deprecated
*/
exports.skip = function (guildId) {
djs[guildId].skip();
};
/**
* Clears the playlist
* @param guildId
* @deprecated
*/
exports.clearQueue = function (guildId) {
djs[guildId].clear();
};
/**
* Returns the queue
* @param guildId
* @deprecated
*/
exports.getQueue = function (guildId) {
return djs[guildId].playlist;
};
/**
* evokes the callback function with the title of the current song
* @param guildId
* @deprecated
*/
exports.nowPlaying = function (guildId) {
return djs[guildId].song;
};
/**
* shuffles the queue
* @param guildId
* @deprecated
*/
exports.shuffle = function (guildId) {
djs[guildId].shuffle();
};

@ -0,0 +1,112 @@
const sqlite3 = require('sqlite3');
/**
* Promise function wrappers for sqlite3
* @type {Database}
*/
exports.Database = class {
constructor(path) {
this.path = path;
this.database = null;
}
/**
* Promise wrapper for sqlite3/Database constructor
* @returns {Promise<any>}
*/
init() {
return new Promise((resolve, reject) => {
this.database = new sqlite3.Database(this.path, (err) => {
if (err)
reject(err);
else
resolve();
});
});
}
/**
* Promise wrapper for sqlite3/Database run
* @param SQL
* @param values
* @returns {Promise<any>}
*/
run(SQL, values) {
return new Promise((resolve, reject) => {
if (values !== null && values instanceof Array)
this.database.run(SQL, values, (err) => {
if (err)
reject(err);
else
resolve();
});
else
this.database.run(SQL, (err) => {
if (err)
reject(err);
else
resolve();
});
});
}
/**
* Promise wrapper for sqlite3/Database get
* @param SQL
* @param values
* @returns {Promise<any>}
*/
get(SQL, values) {
return new Promise((resolve, reject) => {
if (values !== null && values instanceof Array)
this.database.get(SQL, values, (err, row) => {
if (err)
reject(err);
else
resolve(row);
});
else
this.database.get(SQL, (err, row) => {
if (err)
reject(err);
else
resolve(row);
});
});
}
/**
* Promise wrapper for sqlite3/Database all
* @param SQL
* @param values
* @returns {Promise<any>}
*/
all(SQL, values) {
return new Promise((resolve, reject) => {
if (values !== null && values instanceof Array)
this.database.all(SQL, values, (err, rows) => {
if (err)
reject(err);
else
resolve(rows);
});
else
this.database.all(SQL, (err, rows) => {
if (err)
reject(err);
else
resolve(rows);
});
});
}
/**
* Wrapper for sqlite3/Database close
*/
close() {
this.database.close();
}
};

@ -2,7 +2,6 @@
/** /**
* A Series of utility functions * A Series of utility functions
*/ */
const fs = require('fs');
function noOp() { function noOp() {
} }
@ -116,21 +115,6 @@ exports.getSplitDuration = function (duration) {
return retObj; return retObj;
}; };
/* FS */
exports.dirExistence = function (path, callback) {
fs.exists(path, (exist) => {
if (!exist)
fs.mkdir(path, (err) => {
if (!err)
callback();
});
else
callback();
});
};
/* Classes */ /* Classes */
exports.YouTube = class { exports.YouTube = class {

@ -6,6 +6,7 @@ const express = require('express'),
sha512 = require('js-sha512'), sha512 = require('js-sha512'),
fs = require('fs'), fs = require('fs'),
session = require('express-session'), session = require('express-session'),
SQLiteStore = require('connect-sqlite3')(session),
bodyParser = require('body-parser'), bodyParser = require('body-parser'),
compileSass = require('express-compile-sass'), compileSass = require('express-compile-sass'),
config = require('../config.json'), config = require('../config.json'),
@ -18,14 +19,12 @@ exports.setLogger = function (newLogger) {
}; };
exports.WebServer = class { exports.WebServer = class {
constructor(port, schema, root, referenceObjects) { constructor(port) {
this.app = express(); this.app = express();
this.server = null; this.server = null;
this.port = port; this.port = port;
this.schema = buildSchema(fs.readFileSync('./web/graphql/schema.graphql', 'utf-8')); this.schema = buildSchema(fs.readFileSync('./web/graphql/schema.graphql', 'utf-8'));
this.root = {}; this.root = {};
if (referenceObjects)
this.setReferenceObjects(referenceObjects);
} }
configureExpress() { configureExpress() {
@ -38,6 +37,7 @@ exports.WebServer = class {
this.app.use(require('cors')()); this.app.use(require('cors')());
this.app.use(session({ this.app.use(session({
store: new SQLiteStore({dir: './data', db: 'sessions.db'}),
secret: config.webservice.sessionSecret, secret: config.webservice.sessionSecret,
resave: false, resave: false,
saveUninitialized: true, saveUninitialized: true,
@ -56,29 +56,22 @@ exports.WebServer = class {
} }
})); }));
this.app.use('/graphql', graphqlHTTP({
schema: this.schema,
rootValue: this.root,
graphiql: config.webservice.graphiql || false
}));
this.app.use(compileSass({ this.app.use(compileSass({
root: './web/http/' root: './web/http/'
})); }));
this.app.post('/', (req, res) => { this.app.post('/', async (req, res) => {
if (!req.body.username || !req.body.password) if (!req.body.username || !req.body.password) {
res.render('login', {msg: 'Please enter username and password.'}); res.render('login', {msg: 'Please enter username and password.'});
else } else {
this.maindb.get('SELECT * FROM users WHERE username = ? AND password = ?', [req.body.username, req.body.password], (err, user) => { let user = await this.maindb.get('SELECT * FROM users WHERE username = ? AND password = ?', [req.body.username, req.body.password]);
if (err || !user) { if (!user) {
if (err)
logger.warn(err.message);
logger.debug(`User ${req.body.username} failed to authenticate`); 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;
res.render('index'); res.render('index');
} }
}); }
}); });
this.app.use('/scripts', express.static('./web/http/scripts')); this.app.use('/scripts', express.static('./web/http/scripts'));
this.app.use((req, res, next) => { this.app.use((req, res, next) => {
@ -86,9 +79,15 @@ exports.WebServer = class {
next(); next();
else else
res.render('login'); res.render('login');
}, (req, res) => { });
this.app.get('/', (req, res) => {
res.render('index'); res.render('index');
}); });
this.app.use('/graphql', graphqlHTTP({
schema: this.schema,
rootValue: this.root,
graphiql: config.webservice.graphiql || false
}));
} }
/** /**
@ -138,52 +137,40 @@ exports.WebServer = class {
* @param pwIsHash Is the password already a hash string? * @param pwIsHash Is the password already a hash string?
* @returns {Promise<any>} * @returns {Promise<any>}
*/ */
createUser(username, password, scope, pwIsHash) { async createUser(username, password, scope, pwIsHash) {
if (!pwIsHash) password = sha512(password); if (!pwIsHash) password = sha512(password);
return new Promise((resolve, reject) => {
let token = generateUUID(['TOKEN', username]); let token = generateUUID(['TOKEN', username]);
this.maindb.run('INSERT INTO users (username, password, token, scope) VALUES (?, ?, ?, ?)', await this.maindb.run('INSERT INTO users (username, password, token, scope) VALUES (?, ?, ?, ?)',
[username, password, token, scope], (err) => { [username, password, token, scope]);
if (err) { return token;
logger.warn(err.message);
reject(err);
} else {
resolve(token);
}
});
});
} }
/** /**
* Setting all objects that web can query * Setting all objects that web can query
* @param objects * @param objects
*/ */
setReferenceObjects(objects) { async setReferenceObjects(objects) {
this.maindb = objects.maindb; this.maindb = objects.maindb;
this.maindb.run(`${utils.sql.tableExistCreate} users ( await this.maindb.run(`${utils.sql.tableExistCreate} users (
${utils.sql.pkIdSerial}, ${utils.sql.pkIdSerial},
username VARCHAR(32) UNIQUE NOT NULL, username VARCHAR(32) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL, password VARCHAR(255) NOT NULL,
token VARCHAR(255) UNIQUE NOT NULL, token VARCHAR(255) UNIQUE NOT NULL,
scope INTEGER NOT NULL DEFAULT 0 scope INTEGER NOT NULL DEFAULT 0
)`, (err) => { )`);
if (err)
logger.error(err.message);
});
this.root = { this.root = {
client: { client: {
guilds: (args) => { guilds: async (args) => {
let dcGuilds = objects.client.guilds.values(); let dcGuilds = objects.client.guilds.values();
if (args.id) if (args.id)
return [Array.from(dcGuilds) return [(await Promise.all(Array.from(dcGuilds)
.map((x) => new Guild(x, objects.getGuildHandler(x))) .map(async (x) => new Guild(x, await objects.getGuildHandler(x)))))
.find(x => (x.id === args.id))]; .find(x => (x.id === args.id))];
else else
try { try {
return Array.from(dcGuilds) return await Promise.all(Array.from(dcGuilds)
.slice(args.offset, args.offset + args.first) .slice(args.offset, args.offset + args.first)
.map((x) => new Guild(x, objects.getGuildHandler(x))); .map(async (x) => new Guild(x, await objects.getGuildHandler(x))));
} catch (err) { } catch (err) {
logger.error(err.stack); logger.error(err.stack);
return null; return null;
@ -371,15 +358,10 @@ class Guild {
this.dj = this.guildHandler.dj ? new DJ(this.guildHandler.dj) : null; this.dj = this.guildHandler.dj ? new DJ(this.guildHandler.dj) : null;
} }
querySaved() { async querySaved() {
return new Promise((resolve) => {
if (this.guildHandler.db) { if (this.guildHandler.db) {
let saved = []; let saved = [];
this.guildHandler.db.all('SELECT * FROM playlists', (err, rows) => { let rows = await this.guildHandler.db.all('SELECT * FROM playlists');
if (err) {
logger.error(err.message);
resolve(null);
} else {
for (let row of rows) for (let row of rows)
saved.push({ saved.push({
id: generateID(['Media', row.url]), id: generateID(['Media', row.url]),
@ -387,33 +369,18 @@ class Guild {
url: row.url, url: row.url,
thumbnail: utils.YouTube.getVideoThumbnailUrlFromUrl(row.url) thumbnail: utils.YouTube.getVideoThumbnailUrlFromUrl(row.url)
}); });
return saved;
resolve(saved);
}
});
} else {
resolve(null);
} }
});
} }
saved(args) { async saved(args) {
return new Promise((resolve) => { let result = await this.querySaved();
this.querySaved().then((result) => { if (args.id)
if (result) return [result.find(x => (x.id === args.id))];
if (args.id) { else if (args.name)
resolve([result.find(x => (x.id === args.id))]); return [result.find(x => (x.name === args.name))];
} else if (args.name) {
resolve([result.find(x => (x.name === args.name))]);
} else {
resolve(result.slice(args.offset, args.offset + args.first));
}
else else
resolve(null); return result.slice(args.offset, args.offset + args.first);
});
});
} }
roles(args) { roles(args) {

@ -10,6 +10,7 @@
"args-parser": "1.1.0", "args-parser": "1.1.0",
"body-parser": "1.18.3", "body-parser": "1.18.3",
"compression": "1.7.3", "compression": "1.7.3",
"connect-sqlite3": "0.9.11",
"cors": "2.8.5", "cors": "2.8.5",
"discord.js": "11.4.2", "discord.js": "11.4.2",
"express": "4.16.4", "express": "4.16.4",
@ -17,6 +18,7 @@
"express-graphql": "0.7.1", "express-graphql": "0.7.1",
"express-session": "1.15.6", "express-session": "1.15.6",
"ffmpeg-binaries": "4.0.0", "ffmpeg-binaries": "4.0.0",
"fs-extra": "^7.0.1",
"get-youtube-title": "1.0.0", "get-youtube-title": "1.0.0",
"graphql": "14.1.1", "graphql": "14.1.1",
"js-md5": "0.7.3", "js-md5": "0.7.3",

@ -76,22 +76,20 @@ exports.mockCommand = {
}; };
exports.MockDatabase = class { exports.MockDatabase = class {
constructor(file, callback) { constructor(file) {
callback();
} }
run(sql, values, callback) { async init() {
if(callback)
callback();
} }
get() { async run(sql, values) {
return null;
} }
all() { async get() {
return null; }
async all() {
} }
close() { close() {

@ -365,16 +365,14 @@ describe('lib/cmd', function() {
}); });
}); });
describe('lib/guilding', function() { describe('lib/guilding', function*() { // deactivated because of problems with sqlite3 and rewire
const guilding = rewire('../lib/guilding'); const guilding = rewire('../lib/guilding');
const servercommands = require('../commands/servercommands'); const servercommands = require('../commands/servercommands');
const utils = require('../lib/utils'); guilding.__set__("sqliteAsync", null);
guilding.__set__("sqlite3", null); guilding.__set__("fs-extra", {
guilding.__set__("utils", { ensureDir: async() => {
dirExistence: (file, callback) => { return true;
}, }
sql: utils.sql,
YouTube: utils.YouTube
}); });
guilding.setLogger(mockobjects.mockLogger); guilding.setLogger(mockobjects.mockLogger);

@ -64,7 +64,7 @@ head
a#songinfo-container a#songinfo-container
span#dj-songname span#dj-songname
img#dj-songImg(src='' alt='') img#dj-songImg(src='' alt='')
#dj-songProgress #dj-songProgress(style='display:none')
span#dj-songCurrentTS span#dj-songCurrentTS
#dj-queue-container #dj-queue-container
span.cell.label(id='Queue Song count') span.cell.label(id='Queue Song count')

@ -6,8 +6,9 @@ html
script(src='https://code.jquery.com/jquery-3.3.1.min.js') script(src='https://code.jquery.com/jquery-3.3.1.min.js')
script(type='text/javascript' src='scripts/login.js') script(type='text/javascript' src='scripts/login.js')
body body
h1 Login .listContainer
h3 #{message} h1(class='cell') Login
input(id='username' name='username' type='text' required placeholder='Username') h3(class='cell') #{message}
input(id='password' name='password' type='password' required placeholder='Password') input(class='cell' id='username' name='username' type='text' required placeholder='Username' onkeypress=handleSubmit)
button(type='submit' onclick='login()') Log in input(class='cell' id='password' name='password' type='password' required placeholder='Password' onkeypress=handleSubmit)
button(class='cell' type='submit' onclick='login()') Log in

@ -18,6 +18,30 @@ body
background: lighten($cBackground, 5) background: lighten($cBackground, 5)
border-radius: 10px border-radius: 10px
input
color: $cPrimary
background: $cBackground
border: 2px solid $cPrimary
border-radius: 12px
padding: 5px
margin: auto
input:focus
background: $cBackgroundVariant
input::placeholder
color: darken($cPrimary, 20)
input.cell
margin: 10px auto
button
background: $cBackgroundVariant
border: none
border-radius: 12px
color: $cPrimary
padding: 10px
.column .column
display: table-column display: table-column
padding: 20px padding: 20px
@ -117,7 +141,6 @@ div.cell > *
display: table-cell display: table-cell
.songEntry .songEntry
display: flex
display: flex display: flex
background: lighten($cBackgroundVariant, 5) background: lighten($cBackgroundVariant, 5)
padding: 2px padding: 2px
@ -125,19 +148,16 @@ div.cell > *
border-radius: 5px border-radius: 5px
text-decoration: none text-decoration: none
color: $cPrimary color: $cPrimary
> *
.songEntry > *
display: table-column display: table-column
margin: auto margin: auto
img
.songEntry img
max-height: 30px max-height: 30px
max-width: 20% max-width: 20%
height: auto height: auto
width: auto width: auto
border-radius: 2px border-radius: 2px
a
.songEntry a
width: 80% width: 80%
text-decoration: none text-decoration: none
color: $cPrimary color: $cPrimary
@ -238,7 +258,6 @@ div.cell > *
padding: 0 5px 5px padding: 0 5px 5px
#dj-songname #dj-songname
font-weight: bold
font-weight: bold font-weight: bold
font-size: 120% font-size: 120%

@ -11,6 +11,16 @@ function login() {
}), }),
contentType: "application/json" contentType: "application/json"
}).done((res) => { }).done((res) => {
document.write(res); window.location.reload();
}); });
} }
function handleSubmit(e) {
if (!e)
e = window.event;
if (e.which === 13) {
login();
}
}
window.addEventListener('keydown', handleSubmit, false);

Loading…
Cancel
Save