Merge pull request #34 from Trivernis/develop

Webinterface
pull/32/head
Trivernis 6 years ago committed by GitHub
commit 7ededc255e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -21,11 +21,17 @@ The arguments are optional because the token and youtube-api-key that the bot ne
],
"music": {
"timeout": 300000
},
"webservice": { // optional
"enabled": true, // enable the server
"port": 8080, // set the port
"graphiql": false // switch the graphiql interface on/off
}
}
```
If the keys are missing from the config file, the bot exits. This behaviour can be deactivated by setting the `-i` commandline flag.
You need to generate an api-token to access the graphql webservice. You can generate one with the owner-command `tokengen` uses via PM.
Keys
---

295
bot.js

@ -1,16 +1,18 @@
const Discord = require("discord.js"),
fs = require('fs'),
fs = require('fs-extra'),
logger = require('./lib/logging').getLogger(),
cmd = require("./lib/cmd"),
guilding = require('./lib/guilding'),
utils = require('./lib/utils'),
config = require('./config.json'),
args = require('args-parser')(process.argv),
sqlite3 = require('sqlite3'),
sqliteAsync = require('./lib/sqliteAsync'),
authToken = args.token || config.api.botToken,
prefix = args.prefix || config.prefix || '~',
gamepresence = args.game || config.presence;
let weblib = null;
class Bot {
constructor() {
this.client = new Discord.Client();
@ -20,65 +22,100 @@ class Bot {
this.presences = [];
this.guildHandlers = [];
logger.verbose('Registering cleanup function');
utils.Cleanup(() => {
for (let gh in Object.values(this.guildHandlers)) {
if (gh)
gh.destroy();
}
this.client.destroy().then(() => {
logger.debug('destroyed client');
});
});
cmd.setLogger(logger);
logger.verbose('Verifying config');
let configVerifyer = new utils.ConfigVerifyer(config, [
"api.botToken", "api.youTubeApiKey"
]);
if (!configVerifyer.verifyConfig(logger)) {
if (!configVerifyer.verifyConfig(logger))
if (!args.i) {
logger.info('Invalid config. Exiting');
process.exit(1);
logger.flush().then(() => {
process.exit(1);
});
}
}
cmd.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');
this.maindb = new sqlite3.Database('./data/main.db', (err) => {
if (err) {
logger.error(err.message);
} else {
this.maindb.run(`${utils.sql.tableExistCreate} presences (
${utils.sql.pkIdSerial},
text VARCHAR(255) UNIQUE NOT NULL
)`, (err) => {
if (err) {
logger.error(err.message);
} else {
logger.debug('Loading presences');
this.loadPresences();
}
});
}
}
/**
* Initializes all services.
* @returns {Promise<void>}
*/
async initServices() {
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();
});
await this.initializeDatabase();
if (config.webservice && config.webservice.enabled)
await this.initializeWebserver();
logger.verbose('Registering commands');
this.registerCommands();
this.registerCallbacks();
cmd.init(prefix);
}
start() {
return new Promise((resolve, reject) => {
this.client.login(authToken).then(() => {
logger.debug("Logged in");
resolve();
}).catch((err) => {
reject(err);
});
})
/**
* Starting the bot by connecting to the discord service and starting the webservice.
* @returns {Promise<any>}
*/
async start() {
await this.client.login(authToken);
logger.debug("Logged in");
if (this.webServer) {
this.webServer.start();
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
*/
async initializeWebserver() {
logger.verbose('Importing weblib');
weblib = require('./lib/weblib');
weblib.setLogger(logger);
logger.verbose('Creating WebServer');
this.webServer = new weblib.WebServer(config.webservice.port || 8080);
logger.debug('Setting Reference Objects to webserver');
await this.webServer.setReferenceObjects({
client: this.client,
presences: this.presences,
maindb: this.maindb,
prefix: prefix,
getGuildHandler: (guild) => this.getGuildHandler(guild, prefix),
guildHandlers: this.guildHandlers
});
}
/**
@ -88,45 +125,32 @@ class Bot {
* 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.
*/
loadPresences() {
if (fs.existsSync('./data/presences.txt')) {
async loadPresences() {
if (await fs.pathExists('./data/presences.txt')) {
let lineReader = require('readline').createInterface({
input: require('fs').createReadStream('./data/presences.txt')
});
lineReader.on('line', (line) => {
this.maindb.run('INSERT INTO presences (text) VALUES (?)', [line], (err) => {
if (err) {
if (err)
logger.warn(err.message);
}
});
this.presences.push(line);
});
this.rotator = this.client.setInterval(() => this.rotatePresence(), config.presence_duration || 360000);
fs.unlink('./data/presences.txt', (err) => {
if (err)
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) {
if (!(row[0] in this.presences))
this.presences.push(row.text);
}
}
})
this.rotator = this.client.setInterval(() => this.rotatePresence(),
config.presence_duration || 360000);
await fs.unlink('./data/presences.txt');
let rows = await this.maindb.all('SELECT text FROM presences');
for (let row of rows)
if (!(row[0] in this.presences))
this.presences.push(row.text);
} else {
this.maindb.all('SELECT text FROM presences', (err, rows) => {
if (err) {
logger.warn(err.message);
} else {
for (let row of rows) {
this.presences.push(row.text);
}
}
this.rotator = this.client.setInterval(() => this.rotatePresence(), config.presence_duration || 360000);
})
let rows = await this.maindb.all('SELECT text FROM presences');
for (let row of rows)
this.presences.push(row.text);
this.rotator = this.client.setInterval(() => this.rotatePresence(),
config.presence_duration || 360000);
}
}
@ -140,28 +164,38 @@ class Bot {
}, [], "Repeats what you say");
// adds a presence that will be saved in the presence file and added to the rotation
cmd.createGlobalCommand(prefix + 'addpresence', (msg, argv, args) => {
cmd.createGlobalCommand(prefix + 'addpresence', async (msg, argv, args) => {
let p = args.join(' ');
this.presences.push(p);
this.maindb.run('INSERT INTO presences (text) VALUES (?)', [p], (err) => {
if (err)
logger.warn(err.message);
});
await this.maindb.run('INSERT INTO presences (text) VALUES (?)', [p]);
return `Added Presence \`${p}\``;
}, [], "Adds a presence to the rotation.", 'owner');
// shuts down the bot after destroying the client
cmd.createGlobalCommand(prefix + 'shutdown', (msg) => {
msg.reply('Shutting down...').finally(() => {
cmd.createGlobalCommand(prefix + 'shutdown', async (msg) => {
try {
await msg.reply('Shutting down...');
logger.debug('Destroying client...');
this.client.destroy().finally(() => {
logger.debug(`Exiting Process...`);
process.exit(0);
});
});
} catch(err) {
logger.error(err.message);
logger.debug(err.stack);
}
try {
await this.client.destroy();
logger.debug('Exiting server...');
} catch (err) {
logger.error(err.message);
logger.debug(err.stack);
}
try {
await this.webServer.stop();
logger.debug(`Exiting Process...`);
process.exit(0);
} catch(err) {
logger.error(err.message);
logger.debug(err.stack);
}
}, [], "Shuts the bot down.", 'owner');
// forces a presence rotation
@ -171,7 +205,7 @@ class Bot {
this.rotatePresence();
this.rotator = this.client.setInterval(() => this.rotatePresence(), config.presence_duration);
} catch (error) {
logger.warn(JSON.stringify(error));
logger.warn(error.message);
}
}, [], 'Force presence rotation', 'owner');
@ -194,8 +228,26 @@ class Bot {
// returns the numbe of guilds, the bot has joined
cmd.createGlobalCommand(prefix + 'guilds', () => {
return `Number of guilds: \`${this.client.guilds.size}\``
return `Number of guilds: \`${this.client.guilds.size}\``;
}, [], 'Returns the number of guilds the bot has joined', 'owner');
cmd.createGlobalCommand(prefix + 'createUser', (msg, argv) => {
return new Promise((resolve, reject) => {
if (msg.guild) {
resolve("It's not save here! Try again via PM.");
} else if (argv.username && argv.scope) {
logger.debug(`Creating user entry ${argv.username}, scope: ${argv.scope}`);
this.webServer.createUser(argv.username, argv.password, argv.scope, false).then((token) => {
resolve(`Created entry
username: ${argv.username},
scope: ${argv.scope},
token: ${token}
`);
}).catch((err) => reject(err.message));
}
});
}, ['username', 'password', 'scope'], 'Generates a token for a username and returns it.', 'owner');
}
/**
@ -204,29 +256,38 @@ class Bot {
rotatePresence() {
let pr = this.presences.shift();
this.presences.push(pr);
this.client.user.setPresence({game: {name: `${gamepresence} | ${pr}`, type: "PLAYING"}, status: 'online'});
logger.debug(`Presence rotation to ${pr}`);
this.client.user.setPresence({
game: {name: `${gamepresence} | ${pr}`, type: "PLAYING"},
status: 'online'
}).then(() => logger.debug(`Presence rotation to ${pr}`))
.catch((err) => logger.warn(err.message));
}
/**
* Registeres callbacks for client events
* Registeres callbacks for client events message and ready
*/
registerCallbacks() {
this.client.on('error', (err) => {
logger.error(err.message);
logger.debug(err.stack);
});
this.client.on('ready', () => {
logger.info(`logged in as ${this.client.user.tag}!`);
this.client.user.setPresence({game: {name: gamepresence, type: "PLAYING"}, status: 'online'})
this.client.user.setPresence({
game: {
name: gamepresence, type: "PLAYING"
}, status: 'online'
})
.catch((err) => {
if (err)
logger.warn(err.message);
});
});
this.client.on('message', msg => {
this.client.on('message', async (msg) => {
try {
if (msg.author === this.client.user) {
logger.verbose(`ME: ${msg.content}`);
@ -237,10 +298,12 @@ class Bot {
let reply = cmd.parseMessage(msg);
this.answerMessage(msg, reply);
} else {
this.getGuildHandler(msg.guild, prefix).handleMessage(msg);
let gh = await this.getGuildHandler(msg.guild, prefix);
await gh.handleMessage(msg);
}
} catch (err) {
logger.error(err.stack);
logger.error(err.message);
logger.debug(err.stack);
}
});
}
@ -252,19 +315,19 @@ class Bot {
* @param answer
*/
answerMessage(msg, answer) {
if (answer instanceof Promise || answer) {
if (answer instanceof Promise || answer)
if (answer instanceof Discord.RichEmbed) {
(this.mention) ? msg.reply('', answer) : msg.channel.send('', answer);
} else if (answer instanceof Promise) {
answer
.then((answer) => answerMessage(msg, answer))
.catch((error) => answerMessage(msg, error));
.then((answer) => this.answerMessage(msg, answer))
.catch((error) => this.answerMessage(msg, error));
} else {
(this.mention) ? msg.reply(answer) : msg.channel.send(answer);
}
} else {
logger.warn(`Empty answer won't be send.`);
}
else
logger.verbose(`Empty answer won't be send.`);
}
/**
@ -273,9 +336,12 @@ class Bot {
* @param prefix
* @returns {*}
*/
getGuildHandler(guild, prefix) {
if (!this.guildHandlers[guild.id])
this.guildHandlers[guild.id] = new guilding.GuildHandler(guild, prefix);
async getGuildHandler(guild, prefix) {
if (!this.guildHandlers[guild.id]) {
let newGuildHandler = new guilding.GuildHandler(guild, prefix);
await newGuildHandler.initDatabase();
this.guildHandlers[guild.id] = newGuildHandler;
}
return this.guildHandlers[guild.id];
}
}
@ -284,8 +350,17 @@ class Bot {
// Executing the main function
if (typeof require !== 'undefined' && require.main === module) {
logger.info("Starting up... "); // log the current date so that the logfile is better to read.
logger.debug('Calling constructor...');
let discordBot = new Bot();
discordBot.start().catch((err) => {
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.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.",
"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_url": "I need an url to a video to play",
"no_voicechannel": "You need to join a voicechannel to do that!"
}
},
"join": {
@ -54,7 +55,8 @@
"description": "Stops playing music and leaves.",
"category": "Music",
"response": {
"success": "Stopping now..."
"success": "Stopping now...",
"not_playing": "I'm not playing music at the moment."
}
},
"pause": {
@ -63,7 +65,8 @@
"description": "Pauses playing.",
"category": "Music",
"response": {
"success": "Pausing playback."
"success": "Pausing playback.",
"not_playing": "I'm not playing music at the moment."
}
},
"resume": {
@ -72,7 +75,8 @@
"description": "Resumes playing.",
"category": "Music",
"response": {
"success": "Resuming playback."
"success": "Resuming playback.",
"not_playing": "I'm not playing music at the moment."
}
},
"skip": {
@ -81,7 +85,8 @@
"description": "Skips the current song.",
"category": "Music",
"response": {
"success": "Skipping to the next song."
"success": "Skipping to the next song.",
"not_playing": "I'm not playing music at the moment."
}
},
"clear": {
@ -146,4 +151,4 @@
}
}
}
}
}

@ -24,40 +24,40 @@ exports.Servant = class {
let allCommands = {...globCommands, ...this.commands};
if (cmd.charAt(0) !== prefix)
cmd = this.prefix + cmd;
if (allCommands[cmd]) {
if (allCommands[cmd])
return new Discord.RichEmbed()
.setTitle(`Help for ${cmd}`)
.addField('Usage', `\`${cmd} [${allCommands[cmd].args.join('] [')}]\``.replace('[]', ''))
.addField('Description', allCommands[cmd].description)
.addField('Permission Role', allCommands[cmd].role || 'all');
} else {
else
return 'Command not found :(';
}
} else {
let helpEmbed = new Discord.RichEmbed()
.setTitle('Commands');
let globHelp = '';
Object.entries(globCommands).sort().forEach(([key, value]) => {
if (value.role !== 'owner' || checkPermission(msg, 'owner')) {
if (value.role !== 'owner' || checkPermission(msg, 'owner'))
globHelp += `\`${key}\` \t`;
}
});
helpEmbed.addField('Global Commands', globHelp);
let categories = [];
let catCommands = {};
Object.entries(this.commands).sort().forEach(([key, value]) => {
if (value.role !== 'owner' || checkPermission(msg, 'owner')) {
if (value.role !== 'owner' || checkPermission(msg, 'owner'))
if (!categories.includes(value.category)) {
categories.push(value.category);
catCommands[value.category] = `\`${key}\` \t`
catCommands[value.category] = `\`${key}\` \t`;
} else {
catCommands[value.category] += `\`${key}\` \t`
catCommands[value.category] += `\`${key}\` \t`;
}
}
});
for (let cat of categories) {
for (let cat of categories)
helpEmbed.addField(cat, catCommands[cat]);
}
helpEmbed.setFooter(prefix + 'help [command] for more info to each command');
return helpEmbed;
}
@ -66,7 +66,7 @@ exports.Servant = class {
// show all roles that are used by commands
this.createCommand(scmdTempl.utils.roles, () => {
let roles = [];
Object.entries(globCommands).concat(Object.entries(this.commands)).sort().forEach(([key, value]) => {
Object.values(globCommands).concat(Object.values(this.commands)).sort().forEach((value) => {
roles.push(value.role || 'all');
});
return `**Roles**\n${[...new Set(roles)].join('\n')}`;
@ -119,9 +119,9 @@ exports.Servant = class {
let argvars = content.match(/(?<= )\S+/g) || [];
let kwargs = {};
let nLength = Math.min(cmd.args.length, argvars.length);
for (let i = 0; i < nLength; i++) {
for (let i = 0; i < nLength; i++)
kwargs[cmd.args[i]] = argvars[i];
}
let argv = argvars.slice(nLength);
logger.debug(`Executing callback for command: ${command}, kwargs: ${kwargs}, argv: ${argv}`);
try {
@ -183,24 +183,24 @@ exports.init = function (prefix) {
let cmd = kwargs.command;
if (cmd.charAt(0) !== prefix)
cmd = prefix + cmd;
if (globCommands[cmd]) {
if (globCommands[cmd])
return new Discord.RichEmbed()
.setTitle(`Help for ${cmd}`)
.addField('Usage', `\`${cmd} [${globCommands[cmd].args.join('] [')}]\``.replace('[]', ''))
.addField('Description', globCommands[cmd].description)
.addField('Permission Role', globCommands[cmd].role || 'all');
}
} else {
let helpEmbed = new Discord.RichEmbed()
.setTitle('Global Commands')
.setTimestamp();
let description = '';
Object.entries(globCommands).sort().forEach(([key, value]) => {
if (value.role === 'owner' && checkPermission(msg, 'owner')) {
if (value.role === 'owner' && checkPermission(msg, 'owner'))
description += `\`${key}\` \t`;
} else if (value.role !== 'owner') {
else if (value.role !== 'owner')
description += `\`${key}\` \t`;
}
});
helpEmbed.setFooter(prefix + 'help [command] for more info to each command');
helpEmbed.setDescription(description);
@ -223,9 +223,9 @@ function parseGlobalCommand(msg) {
let argvars = content.match(/(?<= )\S+/g) || [];
let kwargs = {};
let nLength = Math.min(cmd.args.length, argvars.length);
for (let i = 0; i < nLength; i++) {
for (let i = 0; i < nLength; i++)
kwargs[cmd.args[i]] = argvars[i];
}
let argv = argvars.slice(nLength);
logger.debug(`Executing callback for command: ${command}, kwargs: ${JSON.stringify(kwargs)}, argv: ${argv}`);
return cmd.callback(msg, kwargs, argv);
@ -239,11 +239,11 @@ function parseGlobalCommand(msg) {
function checkPermission(msg, rolePerm) {
if (!rolePerm || ['all', 'any', 'everyone'].includes(rolePerm))
return true;
if (msg.author.tag === args.owner || config.owners.includes(msg.author.tag)) {
if (msg.author.tag === args.owner || config.owners.includes(msg.author.tag))
return true;
} else {
else
if (msg.member && rolePerm && msg.member.roles.some(role => role.name.toLowerCase() === rolePerm.toLowerCase()))
return true
}
return false
}
return true;
return false;
}

@ -2,10 +2,10 @@ const cmd = require('./cmd'),
music = require('./music'),
utils = require('./utils'),
config = require('../config.json'),
sqliteAsync = require('./sqliteAsync'),
fs = require('fs-extra'),
servercmd = require('../commands/servercommands'),
sqlite3 = require('sqlite3'),
Discord = require('discord.js'),
handlers = {},
dataDir = config.dataPath || './data';
let logger = require('winston');
@ -26,26 +26,16 @@ exports.GuildHandler = class {
this.mention = false;
this.prefix = prefix || config.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
utils.dirExistence(dataDir, () => {
utils.dirExistence(dataDir + '/gdb', () => {
this.db = new sqlite3.Database(`${dataDir}/gdb/${guild}.db`, (err) => {
if (err)
logger.error(err.message);
logger.debug(`Connected to the database for ${guild}`);
this.createTables();
// register commands
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());
}
});
})
});
}
async initDatabase() {
await fs.ensureDir(dataDir + '/gdb');
this.db = new sqliteAsync.Database(`${dataDir}/gdb/${this.guild}.db`);
await this.db.init();
logger.debug(`Connected to the database for ${this.guild}`);
await this.createTables();
// register commands
this.registerMusicCommands();
}
/**
@ -62,15 +52,15 @@ exports.GuildHandler = class {
* messages - logs all messages send on the server
* playlists - save playlists to play them later
*/
createTables() {
this.db.run(`${utils.sql.tableExistCreate} messages (
async createTables() {
await this.db.run(`${utils.sql.tableExistCreate} messages (
${utils.sql.pkIdSerial},
creation_timestamp DATETIME NOT NULL,
author VARCHAR(128) NOT NULL,
author_name VARCHAR(128),
content TEXT NOT NULL
)`);
this.db.run(`${utils.sql.tableExistCreate} playlists (
await this.db.run(`${utils.sql.tableExistCreate} playlists (
${utils.sql.pkIdSerial},
name VARCHAR(32) UNIQUE NOT NULL,
url VARCHAR(255) NOT NULL
@ -83,19 +73,18 @@ exports.GuildHandler = class {
* @param answer
*/
answerMessage(msg, answer) {
if (answer instanceof Promise || answer) {
if (answer instanceof Promise || answer)
if (answer instanceof Discord.RichEmbed) {
(this.mention) ? msg.reply('', answer) : msg.channel.send('', answer);
} else if (answer instanceof Promise) {
answer
.then((answer) => this.answerMessage(msg, answer))
.then((resolvedAnswer) => this.answerMessage(msg, resolvedAnswer))
.catch((error) => this.answerMessage(msg, error));
} else {
(this.mention) ? msg.reply(answer) : msg.channel.send(answer);
}
} else {
else
logger.debug(`Empty answer won't be send.`);
}
}
/**
@ -103,22 +92,14 @@ exports.GuildHandler = class {
* replies or just sends the answer.
* @param msg
*/
handleMessage(msg) {
if (this.ready) {
if (this.db) {
this.db.run(
'INSERT INTO messages (author, creation_timestamp, author_name, content) values (?, ?, ?, ?)',
[msg.author.id, msg.createdTimestamp, msg.author.username, msg.content],
(err) => {
if (err)
logger.error(err.message);
}
);
}
this.answerMessage(msg, this.servant.parseCommand(msg));
} else {
this.msgsQueue.push(msg);
}
async handleMessage(msg) {
if (this.db)
await this.db.run(
'INSERT INTO messages (author, creation_timestamp, author_name, content) values (?, ?, ?, ?)',
[msg.author.id, msg.createdTimestamp, msg.author.username, msg.content]
);
this.answerMessage(msg, this.servant.parseCommand(msg));
}
/**
@ -127,135 +108,103 @@ exports.GuildHandler = class {
* @param url
* @param next
*/
connectAndPlay(vc, url, next) {
return new Promise((resolve, reject) => {
if (!this.dj.connected) {
this.dj.connect(vc).then(() => {
this.dj.playYouTube(url, next);
resolve();
}).catch((err) => reject(err));
} else {
this.dj.playYouTube(url, next);
resolve();
}
});
async connectAndPlay(vc, url, next) {
if (!this.dj.connected) {
await this.dj.connect(vc);
this.dj.playYouTube(url, next);
} else {
this.dj.playYouTube(url, next);
}
}
/**
* registers all music commands and initializes a dj
* @param cmdPrefix
*/
registerMusicCommands(cmdPrefix) {
registerMusicCommands() {
this.dj = new music.DJ();
// play command
this.servant.createCommand(servercmd.music.play, (msg, kwargs, argv) => {
return new Promise((resolve, reject) => {
let vc = this.dj.voiceChannel || msg.member.voiceChannel;
let url = kwargs['url'];
if (!vc)
reject(servercmd.music.play.response.no_voicechannel);
if (!url)
reject(servercmd.music.play.response.no_url);
if (!utils.YouTube.isValidEntityUrl(url)) {
if (argv && argv.length > 0)
url += ' ' + argv.join(' '); // join to get the whole expression behind the command
this.db.get('SELECT url FROM playlists WHERE name = ?', [url], (err, row) => {
if (err)
console.error(err.message);
if (!row) {
reject(servercmd.music.play.response.url_invalid);
logger.verbose('Got invalid url for play command.');
} else {
url = row.url;
this.connectAndPlay(vc, url).then(() => {
resolve(servercmd.music.play.response.success);
}).catch((err) => {
logger.error(err.message);
reject(servercmd.music.play.response.failure);
});
}
});
let playCb = async (msg, kwargs, argv, template, next) => {
let vc = this.dj.voiceChannel || msg.member.voiceChannel;
let url = kwargs['url'];
if (!vc)
return template.response.no_voicechannel;
if (!url)
return template.response.no_url;
if (!utils.YouTube.isValidEntityUrl(url)) {
if (argv && argv.length > 0)
url += ' ' + argv.join(' '); // join to get the whole expression behind the command
let row = await this.db.get('SELECT url FROM playlists WHERE name = ?', [url]);
if (!row) {
logger.debug('Got invalid url for play command.');
return template.response.url_invalid;
} else {
this.connectAndPlay(vc, url).then(() => {
resolve(servercmd.music.play.response.success);
}).catch((err) => {
logger.error(err.message);
reject(servercmd.music.play.response.failure);
});
await this.connectAndPlay(vc, row.url, next);
return template.response.success;
}
})
} else {
await this.connectAndPlay(vc, url, next);
return template.response.success;
}
};
// play command
this.servant.createCommand(servercmd.music.play, async (msg, kwargs, argv) => {
return await playCb(msg, kwargs, argv, servercmd.music.play, false);
});
// playnext command
this.servant.createCommand(servercmd.music.playnext, (msg, kwargs, argv) => {
return new Promise((resolve, reject) => {
let vc = msg.member.voiceChannel;
if (!this.dj.connected) this.dj.voiceChannel = vc;
let url = kwargs['url'];
if (!url) reject(servercmd.music.playnext.response.no_url);
if (!utils.YouTube.isValidEntityUrl(url)) {
if (argv)
url += ' ' + argv.join(' ');
this.db.get('SELECT url FROM playlists WHERE name = ?', [url], (err, row) => {
if (err)
console.error(err.message);
if (!row) {
reject(servercmd.music.play.response.url_invalid);
} else {
url = row.url;
this.connectAndPlay(url, true).then(() => {
resolve(servercmd.music.playnext.response.success);
}).catch((err) => {
logger.error(err.message);
reject(servercmd.music.play.response.failure);
});
}
});
} else {
this.connectAndPlay(url, true).then(() => {
resolve(servercmd.music.playnext.response.success);
}).catch((err) => {
logger.error(err);
reject(servercmd.music.playnext.response.failure);
});
}
})
this.servant.createCommand(servercmd.music.playnext, async (msg, kwargs, argv) => {
return await playCb(msg, kwargs, argv, servercmd.music.playnext, true);
});
// join command
this.servant.createCommand(servercmd.music.join, (msg) => {
if (msg.member.voiceChannel) {
if (msg.member.voiceChannel)
this.dj.connect(msg.member.voiceChannel);
} else {
else
return servercmd.music.join.response.not_connected;
}
});
// stop command
this.servant.createCommand(servercmd.music.stop, () => {
this.dj.stop();
return servercmd.music.stop.response.success;
if (this.dj.connected) {
this.dj.stop();
return servercmd.music.stop.response.success;
} else {
return servercmd.music.stop.response.not_playing;
}
});
// pause command
this.servant.createCommand(servercmd.music.pause, () => {
this.dj.pause();
return servercmd.music.pause.response.success;
if (this.dj.playing) {
this.dj.pause();
return servercmd.music.pause.response.success;
} else {
return servercmd.music.pause.response.not_playing;
}
});
// resume command
this.servant.createCommand(servercmd.music.resume, () => {
this.dj.resume();
return servercmd.music.resume.response.success;
if (this.dj.playing) {
this.dj.resume();
return servercmd.music.resume.response.success;
} else {
return servercmd.music.resume.response.not_playing;
}
});
// skip command
this.servant.createCommand(servercmd.music.skip, () => {
this.dj.skip();
return servercmd.music.skip.response.success;
if (this.dj.playing) {
this.dj.skip();
return servercmd.music.skip.response.success;
} else {
return servercmd.music.skip.response.not_playing;
}
});
// clear command
@ -280,15 +229,15 @@ exports.GuildHandler = class {
// np command
this.servant.createCommand(servercmd.music.current, () => {
let song = this.dj.song;
if (song) {
if (song)
return new Discord.RichEmbed()
.setTitle('Now playing:')
.setDescription(`[${song.title}](${song.url})`)
.setImage(utils.YouTube.getVideoThumbnailUrlFromUrl(song.url))
.setColor(0x00aaff);
} else {
else
return servercmd.music.current.response.not_playing;
}
});
// shuffle command
@ -309,75 +258,29 @@ exports.GuildHandler = class {
});
// saves playlists
this.servant.createCommand(servercmd.music.save, (msg, kwargs, argv) => {
return new Promise((resolve, reject) => {
let saveName = argv.join(' ');
this.db.get('SELECT COUNT(*) count FROM playlists WHERE name = ?', [saveName], (err, row) => {
if (err) {
logger.error(err.message);
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) {
this.db.run('INSERT INTO playlists (name, url) VALUES (?, ?)', [saveName, kwargs.url], cb);
} else {
this.db.run('UPDATE playlists SET url = ? WHERE name = ?', [kwargs.url, saveName], cb)
}
});
});
this.servant.createCommand(servercmd.music.save, async (msg, kwargs, argv) => {
let saveName = argv.join(' ');
let row = await this.db.get('SELECT COUNT(*) count FROM playlists WHERE name = ?', [saveName]);
if (!row || row.count === 0)
await this.db.run('INSERT INTO playlists (name, url) VALUES (?, ?)', [saveName, kwargs.url]);
else
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
this.servant.createCommand(servercmd.music.saved, (msg) => {
return new Promise((resolve, reject) => {
let response = '';
this.db.all('SELECT name, url FROM playlists', (err, rows) => {
if (err) {
logger.error(err.message);
reject();
}
for (let row of rows) {
response += `[${row.name}](${row.url})\n`;
}
if (rows.length === 0) {
msg.channel.send(servercmd.music.saved.response.no_saved);
} else {
let richEmbed = new Discord.RichEmbed()
.setTitle('Saved Songs and Playlists')
.setDescription(response);
resolve(richEmbed);
}
});
});
this.servant.createCommand(servercmd.music.saved, async (msg) => {
let response = '';
let rows = await this.db.all('SELECT name, url FROM playlists');
for (let row of rows)
response += `[${row.name}](${row.url})\n`;
if (rows.length === 0)
msg.channel.send(servercmd.music.saved.response.no_saved);
else
return new Discord.RichEmbed()
.setTitle('Saved Songs and Playlists')
.setDescription(response);
});
}
};
/**
* @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();
}
};

@ -1,3 +1,4 @@
/* eslint-disable no-unused-vars */
const winston = require('winston'),
DailyRotateFile = require('winston-daily-rotate-file'),
args = require('args-parser')(process.argv),
@ -11,37 +12,38 @@ const winston = require('winston'),
loggingFullFormat = winston.format.combine(
winston.format.splat(),
winston.format.timestamp({
format: 'MM-DD HH:mm:ss.SSS' // don't include the year because the filename already tells
format: 'YY-MM-DD HH:mm:ss.SSS'
}),
fileLoggingFormat // the logging format for files that logs with a capitalized level
winston.format.json()
),
logger = winston.createLogger({
level: winston.config.npm.levels, // logs with npm levels
format: loggingFullFormat, // the full format for files
format: loggingFullFormat,
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(), // colorizes the console logging output
winston.format.colorize(),
winston.format.splat(),
winston.format.timestamp({
format: 'YY-MM-DD HH:mm:ss.SSS' // logs with the year to the console
format: 'YY-MM-DD HH:mm:ss.SSS'
}),
consoleLoggingFormat // logs with the custom console format
consoleLoggingFormat
),
level: args.loglevel || 'info' // logs to the console with the arg loglevel or info if it is not given
level: args.loglevel || 'info'
}),
new winston.transports.File({
level: 'debug', // logs with debug level to the active file
filename: './.log/latest.log', // the filename of the current file,
level: 'debug',
filename: './.log/latest.log',
options: {flags: 'w'} // overwrites the file on restart
}),
new DailyRotateFile({
level: 'verbose', // log verbose in the rotating logvile
filename: './.log/%DATE%.log', // the pattern of the filename
datePattern: 'YYYY-MM-DD', // the pattern of %DATE%
zippedArchive: true, // indicates that old logfiles should get zipped
maxSize: '32m', // the maximum filesize
maxFiles: '30d' // the maximum files to keep
level: 'verbose',
filename: './.log/%DATE%.log',
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '32m',
maxFiles: '30d',
json: true
})
]
});
@ -57,4 +59,4 @@ exports.getLogger = function () {
})
);
return logger;
};
};

@ -7,10 +7,13 @@ const ytdl = require("ytdl-core"),
ytapiKey = args.ytapi || config.api.youTubeApiKey;
/* Variable Definition */
let logger = require('winston');
let djs = {};
/* Function Definition */
exports.setLogger = function (newLogger) {
logger = newLogger;
};
exports.DJ = class {
constructor(voiceChannel) {
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
* VoiceChannel is saved as object variable.
*/
connect(voiceChannel) {
return new Promise((resolve, reject) => {
this.voiceChannel = voiceChannel || this.voiceChannel;
if (this.connected) {
this.stop();
}
logger.verbose(`Connecting to voiceChannel ${this.voiceChannel.name}`);
this.voiceChannel.join().then(connection => {
logger.info(`Connected to Voicechannel ${this.voiceChannel.name}`);
this.conn = connection;
this.checkListeners();
resolve();
}).catch((error) => reject(error));
})
async connect(voiceChannel) {
this.voiceChannel = voiceChannel || this.voiceChannel;
if (this.connected)
this.stop();
logger.verbose(`Connecting to voiceChannel ${this.voiceChannel.name}`);
let connection = await this.voiceChannel.join();
logger.info(`Connected to Voicechannel ${this.voiceChannel.name}`);
this.conn = connection;
this.checkListeners();
}
/**
@ -51,9 +50,9 @@ exports.DJ = class {
*/
set listenOnRepeat(value) {
this.repeat = value;
if (this.current) {
if (this.current)
this.queue.push(this.current);
}
}
/**
@ -72,6 +71,7 @@ exports.DJ = class {
* Plays a file for the given filename.
* TODO: Implement queue
* @param filename
* @todo
*/
playFile(filename) {
if (this.connected) {
@ -98,79 +98,83 @@ exports.DJ = class {
logger.verbose(`Exiting ${this.voiceChannel.name}`);
this.stop();
}, config.music.timeout || 300000);
} else if (this.connected)
} else if (this.connected) {
setTimeout(() => this.checkListeners(), 10000);
}
}
/**
* Plays the url of the current song if there is no song playing or puts it in the queue.
* If the url is a playlist (regex match), the videos of the playlist are fetched and put
* If the url is a playlist, the videos of the playlist are fetched and put
* in the queue. For each song the title is saved in the queue too.
* @param url
* @param playnext
*/
playYouTube(url, playnext) {
async playYouTube(url, playnext) {
let plist = utils.YouTube.getPlaylistIdFromUrl(url);
if (plist) {
logger.debug(`Adding playlist ${plist} to queue`);
ypi(ytapiKey, plist).then(items => {
let firstSong = utils.YouTube.getVideoUrlFromId(items.shift().resourceId.videoId);
this.getVideoName(firstSong).then((title) => { // getting the first song to start playing music
if (this.repeat) // listen on repeat
this.queue.push({'url': firstSong, 'title': title}); // put the current song back at the end of the queue
this.playYouTube(firstSong); // call with single url that gets queued if a song is already playing
}).catch((err) => logger.error(err.message));
for (let item of items) {
let vurl = utils.YouTube.getVideoUrlFromId(item.resourceId.videoId);
this.getVideoName(vurl).then((title) => {
this.queue.push({'url': vurl, 'title': title});
}).catch((err) => logger.error(err.message));
let playlistItems = await ypi(ytapiKey, plist);
let firstSong = utils.YouTube.getVideoUrlFromId(playlistItems.shift().resourceId.videoId);
let firstSongTitle = null;
try {
firstSongTitle = await this.getVideoName(firstSong);
} catch(err) {
if (err.message !== 'Not found') {
logger.warn(err.message);
logger.debug(err.stack);
}
logger.debug(`Added ${items.length} songs to the queue`);
});
} else {
if (!this.playing || !this.disp) {
logger.debug(`Playing ${url}`);
this.getVideoName(url).then((title) => {
this.current = ({'url': url, 'title': title});
this.disp = this.conn.playStream(ytdl(url, {
filter: 'audioonly', quality: this.quality, liveBuffer: 40000
}), {volume: this.volume});
}
this.disp.on('end', (reason) => { // end event triggers the next song to play when the reason is not stop
if (reason !== 'stop') {
this.playing = false;
this.current = null;
if (this.queue.length > 0) {
this.current = this.queue.shift();
if (this.repeat) // listen on repeat
this.queue.push(this.current);
this.playYouTube(this.current.url);
} else {
this.stop();
}
}
});
this.playing = true;
});
} else {
logger.debug(`Added ${url} to the queue`);
if (playnext) {
this.getVideoName(url).then((title) => {
this.queue.unshift({'url': url, 'title': title});
}).catch((err) => logger.error(err.message));
} else {
this.getVideoName(url).then((title) => {
this.queue.push({'url': url, 'title': title});
}).catch((err) => logger.error(err.message));
if (this.repeat)
this.queue.push({'url': firstSong, 'title': firstSongTitle});
this.playYouTube(firstSong).catch((err) => logger.warn(err.message));
for (let item of playlistItems) {
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
} catch (err) {
if (err.message !== 'Not found') {
logger.warn(err.message);
logger.debug(err.stack);
}
}
}
logger.debug(`Added ${playlistItems.length} songs to the queue`);
} else if (!this.playing || !this.disp) {
logger.debug(`Playing ${url}`);
this.current = ({'url': url, 'title': await this.getVideoName(url)});
this.disp = this.conn.playStream(ytdl(url,
{filter: 'audioonly', quality: this.quality, liveBuffer: 40000}),
{volume: this.volume});
this.disp.on('end', (reason) => { // end event triggers the next song to play when the reason is not stop
if (reason !== 'stop') {
this.playing = false;
this.current = null;
if (this.queue.length > 0) {
this.current = this.queue.shift();
if (this.repeat) // listen on repeat
this.queue.push(this.current);
this.playYouTube(this.current.url).catch((err) => logger.warn(err.message));
} else {
this.stop();
}
}
});
this.playing = true;
} else {
logger.debug(`Added ${url} to the queue`);
if (playnext)
this.queue.unshift({'url': url, 'title': await this.getVideoName(url)});
else
this.queue.push({'url': url, 'title': await this.getVideoName(url)});
}
}
/**
* Gets the name of the YouTube Video at url
* @param url
@ -199,7 +203,7 @@ exports.DJ = class {
this.volume = percentage;
this.disp.setVolume(percentage);
} else {
logger.warn("No dispatcher found.")
logger.warn("No dispatcher found.");
}
}
@ -208,11 +212,11 @@ exports.DJ = class {
*/
pause() {
logger.verbose("Pausing music...");
if (this.disp !== null) {
if (this.disp !== null)
this.disp.pause();
} else {
else
logger.warn("No dispatcher found");
}
}
/**
@ -220,18 +224,21 @@ exports.DJ = class {
*/
resume() {
logger.verbose("Resuming music...");
if (this.disp !== null) {
if (this.disp !== null)
this.disp.resume();
} else {
else
logger.warn("No dispatcher found");
}
}
/**
* Stops playing music by ending the Dispatcher and disconnecting
* Stops playing music by ending the Dispatcher and disconnecting.
* Also sets playing to false and clears the queue and the current song.
*/
stop() {
this.playing = false;
this.queue = [];
this.current = null;
logger.verbose("Stopping music...");
try {
if (this.disp) {
@ -248,6 +255,7 @@ exports.DJ = class {
if (this.voiceChannel) {
this.voiceChannel.leave();
logger.debug("Left VoiceChannel");
logger.info(`Disconnected from Voicechannel ${this.voiceChannel.name}`);
}
} catch (error) {
logger.verbose(JSON.stringify(error));
@ -267,7 +275,10 @@ exports.DJ = class {
this.playing = false;
if (this.queue.length > 0) {
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 {
this.stop();
}
@ -296,149 +307,3 @@ exports.DJ = class {
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();
}
};

@ -1,7 +1,7 @@
/* eslint-disable no-console*/
/**
* A Series of utility functions
*/
const fs = require('fs');
function noOp() {
}
@ -35,12 +35,12 @@ exports.getExtension = function (filename) {
exports.objectDeepFind = function (object, attributePath) {
let current = object,
paths = attributePath.split('.');
for (let path of paths) {
for (let path of paths)
if (current[path] !== undefined && current[path] !== null)
current = current[path];
else
return undefined;
}
return current;
};
@ -66,7 +66,7 @@ exports.shuffleArray = function(array) {
}
return array;
}
};
/**
* lets you define a cleanup for your program exit
@ -104,32 +104,17 @@ exports.getSplitDuration = function (duration) {
let dur = duration;
let retObj = {};
retObj.milliseconds = dur % 1000;
dur = Math.round(dur / 1000);
dur = Math.floor(dur / 1000);
retObj.seconds = dur % 60;
dur = Math.round(dur / 60);
dur = Math.floor(dur / 60);
retObj.minutes = dur % 60;
dur = Math.round(dur / 60);
dur = Math.floor(dur / 60);
retObj.hours = dur % 24;
dur = Math.round(dur / 24);
dur = Math.floor(dur / 24);
retObj.days = dur;
return retObj;
};
/* FS */
exports.dirExistence = function (path, callback) {
fs.exists(path, (exist) => {
if (!exist) {
fs.mkdir(path, (err) => {
if (!err)
callback();
});
} else {
callback();
}
})
};
/* Classes */
exports.YouTube = class {
@ -179,7 +164,7 @@ exports.YouTube = class {
static getPlaylistIdFromUrl(url) {
if (!exports.YouTube.isValidPlaylistUrl(url))
return null;
let matches = url.match(/(?<=\?list=)[\w\-]+/);
let matches = url.match(/(?<=\?list=)[\w-]+/);
if (matches)
return matches[0];
else
@ -194,11 +179,11 @@ exports.YouTube = class {
static getVideoIdFromUrl(url) {
if (!exports.YouTube.isValidVideoUrl(url))
return null;
let matches1 = url.match(/(?<=\?v=)[\w\-]+/);
if (matches1)
let matches1 = url.match(/(?<=\?v=)[\w-]+/);
if (matches1) {
return matches1[0];
else {
let matches2 = url.match(/(?<=youtu\.be\/)[\w\-]+/);
} else {
let matches2 = url.match(/(?<=youtu\.be\/)[\w-]+/);
if (matches2)
return matches2[0];
else
@ -220,7 +205,8 @@ exports.YouTube = class {
* @returns {string}
*/
static getVideoThumbnailUrlFromUrl(url) {
return `https://i3.ytimg.com/vi/${exports.YouTube.getVideoIdFromUrl(url)}/maxresdefault.jpg`
let id = exports.YouTube.getVideoIdFromUrl(url);
return id? `https://i3.ytimg.com/vi/${id}/maxresdefault.jpg` : null;
}
};
@ -239,10 +225,10 @@ exports.ConfigVerifyer = class {
*/
verifyConfig(logger) {
let missing = [];
for (let reqAttr of this.requiredAttributes) {
for (let reqAttr of this.requiredAttributes)
if (exports.objectDeepFind(this.config, reqAttr) === undefined)
missing.push(reqAttr);
}
this.missingAttributes = missing;
this.logMissing(logger);
return this.missingAttributes.length === 0;
@ -253,13 +239,22 @@ exports.ConfigVerifyer = class {
* @param logger
*/
logMissing(logger) {
if (this.missingAttributes.length > 0) {
if (this.missingAttributes.length > 0)
logger.error(`Missing required Attributes ${this.missingAttributes.join(', ')}`);
}
}
};
exports.sql = {
tableExistCreate: 'CREATE TABLE IF NOT EXISTS',
pkIdSerial: 'id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL'
};
};
exports.logLevels = {
'debug': 0,
'verbose': 1,
'info': 2,
'warning': 3,
'warn': 3,
'error:': 4
};

@ -0,0 +1,477 @@
const express = require('express'),
graphqlHTTP = require('express-graphql'),
{buildSchema} = require('graphql'),
compression = require('compression'),
md5 = require('js-md5'),
sha512 = require('js-sha512'),
fs = require('fs'),
session = require('express-session'),
SQLiteStore = require('connect-sqlite3')(session),
bodyParser = require('body-parser'),
compileSass = require('express-compile-sass'),
config = require('../config.json'),
utils = require('../lib/utils');
let logger = require('winston');
exports.setLogger = function (newLogger) {
logger = newLogger;
};
exports.WebServer = class {
constructor(port) {
this.app = express();
this.server = null;
this.port = port;
this.schema = buildSchema(fs.readFileSync('./web/graphql/schema.graphql', 'utf-8'));
this.root = {};
}
configureExpress() {
this.app.set('view engine', 'pug');
this.app.set('trust proxy', 1);
this.app.set('views', './web/http/');
if (this.app.get('env') === 'devlopment')
this.app.use(require('cors')());
this.app.use(require('cors')());
this.app.use(session({
store: new SQLiteStore({dir: './data', db: 'sessions.db'}),
secret: config.webservice.sessionSecret,
resave: false,
saveUninitialized: true,
cookie: {secure: 'auto'},
genid: () => generateUUID('Session')
}));
this.app.use(bodyParser.json());
this.app.use(bodyParser.urlencoded({extended: true}));
this.app.use(compression({
filter: (req, res) => {
if (req.headers['x-no-compression'])
return false;
else
return compression.filter(req, res);
}
}));
this.app.use(compileSass({
root: './web/http/'
}));
this.app.post('/', async (req, res) => {
if (!req.body.username || !req.body.password) {
res.render('login', {msg: 'Please enter username and password.'});
} else {
let user = await this.maindb.get('SELECT * FROM users WHERE username = ? AND password = ?', [req.body.username, req.body.password]);
if (!user) {
logger.debug(`User ${req.body.username} failed to authenticate`);
res.render('login', {msg: 'Login failed!'});
} else {
req.session.user = user;
res.render('index');
}
}
});
this.app.use('/scripts', express.static('./web/http/scripts'));
this.app.use((req, res, next) => {
if (req.session.user)
next();
else
res.render('login');
});
this.app.get('/', (req, res) => {
res.render('index');
});
this.app.use('/graphql', graphqlHTTP({
schema: this.schema,
rootValue: this.root,
graphiql: config.webservice.graphiql || false
}));
}
/**
* Starting the api webserver
*/
start() {
this.configureExpress();
if (config.webservice.https && config.webservice.https.enabled) {
let sslKey = null;
let sslCert = null;
if (config.webservice.https.keyFile)
sslKey = fs.readFileSync(config.webservice.https.keyFile, 'utf-8');
if (config.webservice.https.certFile)
sslCert = fs.readFileSync(config.webservice.https.certFile, 'utf-8');
if (sslKey && sslCert) {
logger.verbose('Creating https server.');
this.server = require('https').createServer({key: sslKey, cert: sslCert}, this.app);
} else {
logger.warn('Key or certificate file not found. Fallback to http server.');
this.server = require('http').createServer(this.app);
}
} else {
this.server = require('http').createServer(this.app);
}
this.server.listen(this.port);
}
/**
* Stopping the webserver
* @returns {Promise<any>}
*/
stop() {
return new Promise((resolve) => {
if (this.server)
this.server.close(resolve);
else
resolve();
});
}
/**
* Generates a token for a given username
* @param username
* @param scope
* @param password
* @param pwIsHash Is the password already a hash string?
* @returns {Promise<any>}
*/
async createUser(username, password, scope, pwIsHash) {
if (!pwIsHash) password = sha512(password);
let token = generateUUID(['TOKEN', username]);
await this.maindb.run('INSERT INTO users (username, password, token, scope) VALUES (?, ?, ?, ?)',
[username, password, token, scope]);
return token;
}
/**
* Setting all objects that web can query
* @param objects
*/
async setReferenceObjects(objects) {
this.maindb = objects.maindb;
await this.maindb.run(`${utils.sql.tableExistCreate} users (
${utils.sql.pkIdSerial},
username VARCHAR(32) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
token VARCHAR(255) UNIQUE NOT NULL,
scope INTEGER NOT NULL DEFAULT 0
)`);
this.root = {
client: {
guilds: async (args) => {
let dcGuilds = objects.client.guilds.values();
if (args.id)
return [(await Promise.all(Array.from(dcGuilds)
.map(async (x) => new Guild(x, await objects.getGuildHandler(x)))))
.find(x => (x.id === args.id))];
else
try {
return await Promise.all(Array.from(dcGuilds)
.slice(args.offset, args.offset + args.first)
.map(async (x) => new Guild(x, await objects.getGuildHandler(x))));
} catch (err) {
logger.error(err.stack);
return null;
}
},
guildCount: () => {
return Array.from(objects.client.guilds.values()).length;
},
user: () => {
return new User(objects.client.user);
},
ping: () => {
return objects.client.ping;
},
status: () => {
return objects.client.status;
},
uptime: () => {
return objects.client.uptime;
},
voiceConnectionCount: () => {
let dcGuilds = Array.from(objects.client.guilds.values());
return dcGuilds.filter((x) => {
let gh = objects.guildHandlers[x.id];
if (gh)
if (gh.dj)
return gh.dj.playing;
else
return false;
else
return false;
}).length;
}
},
prefix: objects.prefix,
presences: objects.presences,
config: () => {
let newConfig = JSON.parse(JSON.stringify(config));
delete newConfig.api;
return JSON.stringify(newConfig, null, ' ');
},
logs: (args) => {
return new Promise((resolve) => {
let logEntries = [];
let lineReader = require('readline').createInterface({
input: require('fs').createReadStream('./.log/latest.log')
});
lineReader.on('line', (line) => {
logEntries.push(new LogEntry(JSON.parse(line)));
});
lineReader.on('close', () => {
if (args.level)
logEntries = logEntries
.filter(x => (utils.logLevels[x.level] >= utils.logLevels[args.level]));
if (args.id)
logEntries = [logEntries.find(x => (x.id === args.id))];
if (args.first)
logEntries = logEntries.slice(args.offset, args.offset + args.first);
else
logEntries = logEntries.slice(logEntries.length - args.last);
resolve(logEntries);
});
});
}
};
}
};
/**
* generating an id
* @param valArr
* @returns {*}
*/
function generateID(valArr) {
let b64 = Buffer.from(valArr.map(x => {
if (x)
return x.toString();
else
return 'null';
}).join('_')).toString('base64');
return md5(b64);
}
/**
* generating an unique id
* @param input
* @returns {*}
*/
function generateUUID(input) {
return generateID([input, (new Date()).getMilliseconds()]) + Date.now();
}
/**
* Used for graphql attribute access to the lib/music/DJ
*/
class DJ {
constructor(musicDj) {
this.dj = musicDj;
this.quality = musicDj.quality;
}
queue(args) {
let queue = this.dj.queue.map((x) => {
return {
id: generateID(['Media', x.url]),
name: x.title,
url: x.url,
thumbnail: utils.YouTube.getVideoThumbnailUrlFromUrl(x.url)
};
});
if (args.id)
return [queue.find(x => (x.id === args.id))];
else
return queue.slice(args.offset, args.offset + args.first);
}
get playing() {
return this.dj.playing;
}
get connected() {
return this.dj.connected;
}
get paused() {
return this.dj.disp? this.dj.disp.paused : false;
}
get queueCount() {
return this.dj.queue.length;
}
get songStartTime() {
return this.dj.disp.player.streamingData.startTime;
}
get volume() {
return this.dj.volume;
}
get repeat() {
return this.dj.repeat;
}
get currentSong() {
let x = this.dj.current;
return {
id: generateID(['Media', x.url]),
name: x.title,
url: x.url,
thumbnail: utils.YouTube.getVideoThumbnailUrlFromUrl(x.url)
};
}
get voiceChannel() {
return this.dj.voiceChannel.name;
}
}
/**
* Used for graphql access to the discord.js Guild and lib/guilding/GuildHandler
*/
class Guild {
constructor(discordGuild, guildHandler) {
this.id = generateID(['Guild', discordGuild.id]);
this.discordId = discordGuild.id;
this.name = discordGuild.name;
this.owner = new GuildMember(discordGuild.owner);
this.memberCount = discordGuild.memberCount;
this.icon = discordGuild.iconURL;
this.prMembers = Array.from(discordGuild.members.values())
.map((x) => new GuildMember(x));
this.prRoles = Array.from(discordGuild.roles.values())
.map((x) => new Role(x));
guildHandler = guildHandler || {};
this.ready = guildHandler.ready;
this.prSaved = null;
this.guildHandler = guildHandler;
this.dj = this.guildHandler.dj ? new DJ(this.guildHandler.dj) : null;
}
async querySaved() {
if (this.guildHandler.db) {
let saved = [];
let rows = await this.guildHandler.db.all('SELECT * FROM playlists');
for (let row of rows)
saved.push({
id: generateID(['Media', row.url]),
name: row.name,
url: row.url,
thumbnail: utils.YouTube.getVideoThumbnailUrlFromUrl(row.url)
});
return saved;
}
}
async saved(args) {
let result = await this.querySaved();
if (args.id)
return [result.find(x => (x.id === args.id))];
else if (args.name)
return [result.find(x => (x.name === args.name))];
else
return result.slice(args.offset, args.offset + args.first);
}
roles(args) {
if (args.id)
return [this.prRoles.find(x => (x.id === args.id))];
else
return this.prRoles.slice(args.offset, args.offset + args.first);
}
members(args) {
if (args.id)
return [this.prMembers.find(x => (x.id === args.id))];
else
return this.prMembers.slice(args.offset, args.offset + args.first);
}
}
/**
* Used for graphql access to the discord.js Role
*/
class Role {
constructor(discordRole) {
this.id = generateID(['Role', discordRole.id]);
this.discordId = discordRole.id;
this.name = discordRole.name;
this.color = discordRole.hexColor;
this.prMembers = Array.from(discordRole.members.values)
.map((x) => new GuildMember(x));
}
members(args) {
if (args.id)
return [this.prMembers.find(x => (x.id === args.id))];
else
return this.prMembers.slice(args.offset, args.offset + args.first);
}
}
/**
* Used for graphql access to the discord.js GuildMember
*/
class GuildMember {
constructor(discordGuildMember) {
this.id = generateID(['GuildMember', discordGuildMember.id]);
this.discordId = discordGuildMember.id;
this.user = new User(discordGuildMember.user);
this.nickname = discordGuildMember.nickname;
this.prRoles = Array.from(discordGuildMember.roles.values())
.map((x) => new Role(x));
this.highestRole = new Role(discordGuildMember.highestRole);
}
roles(args) {
if (args.id)
return [this.prRoles.find(x => (x.id === args.id))];
else
return this.prRoles.slice(args.offset, args.offset + args.first);
}
}
/**
* Used for graphql access to the discord.js User
*/
class User {
constructor(discordUser) {
this.id = generateID(['User', discordUser.id]);
this.discordId = discordUser.id;
this.name = discordUser.username;
this.avatar = discordUser.avatarURL;
this.bot = discordUser.bot;
this.tag = discordUser.tag;
this.tag = discordUser.tag;
this.presence = {
game: discordUser.presence.game? discordUser.presence.game.name : null,
status: discordUser.presence.status
};
}
}
/**
* Used for graphql access to log entries
*/
class LogEntry {
constructor(entry) {
this.id = generateID(['LogEntry', entry.level, entry.timestamp]);
this.message = entry.message;
this.timestamp = entry.timestamp;
this.level = entry.level;
}
}

@ -8,10 +8,24 @@
},
"dependencies": {
"args-parser": "1.1.0",
"body-parser": "1.18.3",
"compression": "1.7.3",
"connect-sqlite3": "0.9.11",
"cors": "2.8.5",
"discord.js": "11.4.2",
"express": "4.16.4",
"express-compile-sass": "4.0.0",
"express-graphql": "0.7.1",
"express-session": "1.15.6",
"ffmpeg-binaries": "4.0.0",
"fs-extra": "^7.0.1",
"get-youtube-title": "1.0.0",
"graphql": "14.1.1",
"js-md5": "0.7.3",
"js-sha512": "0.8.0",
"node-sass": "4.11.0",
"opusscript": "0.0.6",
"pug": "2.0.3",
"sqlite3": "4.0.6",
"winston": "3.2.1",
"winston-daily-rotate-file": "3.6.0",
@ -19,11 +33,73 @@
"ytdl-core": "0.29.1"
},
"devDependencies": {
"assert": "^1.4.1",
"chai": "^4.2.0",
"mocha": "^5.2.0",
"nyc": "^13.1.0",
"rewire": "^4.0.1",
"sinon": "^7.2.3"
"assert": "1.4.1",
"chai": "4.2.0",
"mocha": "5.2.0",
"nyc": "13.2.0",
"rewire": "4.0.1",
"sinon": "7.2.3",
"eslint-plugin-graphql": "3.0.3",
"eslint": "5.13.0",
"eslint-plugin-promise": "4.0.1"
},
"eslintConfig": {
"parserOptions": {
"ecmaVersion": 2018
},
"env": {
"node": true,
"browser": true,
"jquery": true,
"es6": true
},
"extends": [
"eslint:recommended",
"plugin:promise/recommended"
],
"rules": {
"semi": "error",
"semi-style": [
"error",
"last"
],
"no-await-in-loop": "warn",
"curly": [
"warn",
"multi",
"consistent"
],
"block-spacing": [
"warn",
"always"
],
"array-bracket-newline": [
"warn",
"consistent"
],
"camelcase": [
"error",
{
"properties": "always"
}
],
"comma-spacing": [
"error",
{
"after": true
}
],
"brace-style": [
"error",
"1tbs"
],
"promise/no-promise-in-callback": "off",
"promise/always-return": "off",
"promise/catch-or-return": "off"
},
"plugins": [
"eslint-plugin-graphql",
"eslint-plugin-promise"
]
}
}

@ -1,3 +1,4 @@
/* eslint-disable */
exports.mockLogger = {
error: msg => {
throw new Error(msg);
@ -75,25 +76,23 @@ exports.mockCommand = {
};
exports.MockDatabase = class {
constructor(file, callback) {
callback();
constructor(file) {
}
run(sql, values, callback) {
if(callback) {
callback();
}
async init() {
}
async run(sql, values) {
}
get() {
return null;
async get() {
}
all() {
return null
async all() {
}
close() {
return true;
}
};
};

@ -1,3 +1,4 @@
/* eslint-disable */
const sinon = require('sinon'),
chai = require('chai');
@ -7,4 +8,4 @@ beforeEach(() => {
afterEach(() => {
this.sandbox.restore();
});
});

@ -1,3 +1,4 @@
/* eslint-disable */
const mockobjects = require('./mockobjects.js'),
sinon = require('sinon'),
assert = require('assert'),
@ -135,7 +136,7 @@ describe('lib/utils', function() {
assert('https://i3.ytimg.com/vi/VIDID/maxresdefault.jpg', getTh4Id(getVid4Id('VIDID')));
assert('https://i3.ytimg.com/vi/1234/maxresdefault.jpg', getTh4Id(getVid4Id('1234')));
done();
})
});
});
describe('#ConfigVerifyer', function() {
@ -173,7 +174,7 @@ describe('lib/utils', function() {
confVer = new utils.ConfigVerifyer(testObj, ['key1', 'key1.key2', 'key7.key8.0.key9']);
assert(!confVer.verifyConfig(modifiedMockLogger));
done();
})
});
});
});
@ -239,7 +240,7 @@ describe('lib/music', function() {
dj.getVideoName('http://www.youtube.com/watch?v=ABCDEFGHIJK').then((name) => {
assert(name === 'test');
done();
})
});
});
it('sets the volume', function(done) {
@ -249,7 +250,7 @@ describe('lib/music', function() {
dj.setVolume(100);
assert(dj.volume === 100);
done();
})
});
});
it('pauses playback', function(done) {
@ -258,7 +259,7 @@ describe('lib/music', function() {
dj.playFile();
dj.pause();
done();
})
});
});
it('resumes playback', function(done) {
@ -267,7 +268,7 @@ describe('lib/music', function() {
dj.playFile();
dj.resume();
done();
})
});
});
it('stops playback', function(done) {
@ -319,7 +320,7 @@ describe('lib/music', function() {
assert(dj.queue.length === 0);
done();
}).catch(() => done());
})
});
});
});
@ -344,7 +345,7 @@ describe('lib/cmd', function() {
servant.createCommand(mockobjects.mockCommand, mockobjects.mockCommand.textReply);
assert(servant.commands['test']);
servant.removeCommand('test');
assert(!servant.commands['test'])
assert(!servant.commands['test']);
});
it('parses commands', function() {
@ -364,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 servercommands = require('../commands/servercommands');
const utils = require('../lib/utils');
guilding.__set__("sqlite3", null);
guilding.__set__("utils", {
dirExistence: (file, callback) => {
},
sql: utils.sql,
YouTube: utils.YouTube
guilding.__set__("sqliteAsync", null);
guilding.__set__("fs-extra", {
ensureDir: async() => {
return true;
}
});
guilding.setLogger(mockobjects.mockLogger);
@ -454,7 +453,7 @@ describe('lib/guilding', function() {
gh.dj = new music.DJ(mockobjects.mockVoicechannel);
gh.connectAndPlay(mockobjects.mockVoicechannel, 'test', false).then(() => {
done();
})
});
});
it('handles all servercommands', function() {
@ -474,14 +473,14 @@ describe('lib/guilding', function() {
}
};
for (let category of Object.keys(servercommands)) {
for (let category of Object.keys(servercommands))
for (let command of Object.keys(servercommands[category])) {
msg.content = '~' + command;
gh.handleMessage(msg);
}
}
assert(msgSpy.called);
});
});
});
});

@ -0,0 +1,83 @@
type Presence {
game: String
status: String
}
type User {
id: ID!
discordId: String
name: String!
avatar: String
bot: Boolean
tag: String!
presence: Presence
}
type Role {
id: ID!
discordId: String
name: String
color: String
members(first: Int = 10, offset: Int = 0, id: String): [GuildMember]
}
type GuildMember {
id: ID!
discordId: String
user: User
nickname: String
roles(first: Int = 10, offset: Int = 0, id: String): [Role]
highestRole: Role
}
type DJ {
queue(first: Int = 10, offset: Int = 0, id: String): [MediaEntry]
queueCount: Int!
songStartTime: String
playing: Boolean!
volume: Float
repeat: Boolean
currentSong: MediaEntry
quality: String
voiceChannel: String
connected: Boolean!
paused: Boolean!
}
type Guild {
id: ID!
discordId: String
name: String
owner: GuildMember
dj: DJ
members(first: Int = 10, offset: Int = 0, id: String): [GuildMember]
memberCount: Int!
roles(first: Int = 10, offset: Int = 0, id: String): [Role]
icon: String
ready: Boolean
saved(first: Int = 10, offset: Int = 0, id: String, name: String): [MediaEntry!]
savedCount: Int!
}
type Client {
guilds(first: Int = 10, offset: Int = 0, id: String): [Guild]
guildCount: Int
voiceConnectionCount: Int
user: User
ping: Float
status: Int
uptime: Int
}
type MediaEntry {
id: ID!
url: String!
name: String!
thumbnail: String
}
type LogEntry {
id: ID!
message: String
level: String
timestamp: String
}
type Query {
client: Client
presences: [String]!
config: String
prefix: String
logs(first: Int, offset: Int = 0, id: String, last: Int = 10, level: String): [LogEntry]
}

@ -0,0 +1,79 @@
doctype html
head
meta(charset='UTF-8')
title Dashboard
script(src='https://code.jquery.com/jquery-3.3.1.min.js')
script(type='text/javascript' src='https://momentjs.com/downloads/moment.min.js')
link(type='text/css' rel='stylesheet' href='sass/style.sass')
script(type='text/javascript' src='scripts/query.js')
#content
#column-left.column
h2.cell Logs
#log-container.listContainer
#column-middle.column
#avatar-container
#status-indicator
img#user-avatar.cell(src='' alt='Avatar')
h3#user-tag.cell
h4#user-game.cell
.space
h2.cell Status
.cell
span.label.text-right Ping:
span#client-ping.text-left
.cell
span.label.text-right Uptime:
span#client-uptime.text-left
.cell
span.label.text-right Socket Status:
span#client-status.text-left
.cell
span.label.text-right Guild Count:
span#client-guildCount.text-left
.cell
span.label.text-right Active Voice Connections:
span#client-vcCount.text-left
#column-right.column
select#guild-select.cell
option(value='select-default') -Select a guild-
#guildinfo(style='display: none')
.listContainer
#guild-icon-container.cell
img#guild-icon(src='' alt='Icon')
#guild-nameAndIcon.listContainer
h2#guild-name.cell
p.cell by
h3#guild-owner.cell
.space
h3.cell Stats
.cell
span.label.text-right Member Count:
span#guild-memberCount.text-left
.space
h3.cell DJ
.cell
span.label.text-right State:
span#guild-djStatus.text-left
.cell
span.label.text-right Repeat:
span#dj-repeat.text-left
.cell
span.label.text-right Voice Channel:
span#dj-voiceChannel.text-left
#dj-songinfo.listContainer(style='display: none')
a#songinfo-container
span#dj-songname
img#dj-songImg(src='' alt='')
#dj-songProgress(style='display:none')
span#dj-songCurrentTS
#dj-queue-container
span.cell.label(id='Queue Song count')
span#dj-queueCount
| Songs in Queue
span.cell
| Next
span#dj-queueDisplayCount 0
| Songs:
#dj-songQueue
script.
startUpdating();

@ -0,0 +1,14 @@
doctype html
html
head
link(type='text/css' rel='stylesheet' href='sass/style.sass')
script(type='text/javascript' src='scripts/lib/sha512.min.js')
script(src='https://code.jquery.com/jquery-3.3.1.min.js')
script(type='text/javascript' src='scripts/login.js')
body
.listContainer
h1(class='cell') Login
h3(class='cell') #{message}
input(class='cell' id='username' name='username' type='text' required placeholder='Username' onkeypress=handleSubmit)
input(class='cell' id='password' name='password' type='password' required placeholder='Password' onkeypress=handleSubmit)
button(class='cell' type='submit' onclick='login()') Log in

@ -0,0 +1,280 @@
@import url('https://fonts.googleapis.com/css?family=Ubuntu')
@import vars
body
font-family: $fNormal
color: $cPrimary
background-color: $cBackground
overflow: hidden
max-height: 100%
max-width: 100%
::-webkit-scrollbar
width: 12px
height: 12px
::-webkit-scrollbar-thumb
background: darken($cBackground, 5)
border-radius: 10px
::-webkit-scrollbar-track
background: lighten($cBackground, 5)
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
display: table-column
padding: 20px
align-content: center
margin: 0 auto
text-align: center
max-height: 100vh
height: 100vh
.cell
display: list-item
list-style: none
align-content: center
text-align: center
margin: auto
user-select: none
.space
height: 20px
h2.cell
padding: 5px
div.cell
display: flex
align-items: center
width: 100%
position: relative
div.cell > *
display: table-cell
align-items: center
width: 100%
padding: 2px 5px
.text-left
text-align: left
.text-right
text-align: right
.label
font-weight: bold
.listContainer
display: grid
width: 100%
text-align: left
overflow: hidden
position: relative
max-height: 90vh
.listContainer:hover
overflow: auto
.logEntry
display: list-item
list-style: none
padding: 5px
border-radius: 10px
margin: 5px
color: $cOnSurfaceVariant
user-select: none
position: relative
font-size: 110%
.logEntry[level=debug]
background: $cDebug
.logEntry[level=verbose]
background: $cVerbose
.logEntry[level=info]
background: $cInfo
.logEntry[level=warn]
background: $cWarn
.logEntry[level=warning]
background: $cWarn
user-select: all
.logEntry[level=error]
background: $cError
user-select: all
.logEntry .infodiv
display: flex
list-style: none
font-size: 75%
width: 100%
.logEntry .infodiv span
padding: 0 2px
margin: auto
width: 50%
display: table-cell
.songEntry
display: flex
background: lighten($cBackgroundVariant, 5)
padding: 2px
margin: 5px
border-radius: 5px
text-decoration: none
color: $cPrimary
> *
display: table-column
margin: auto
img
max-height: 30px
max-width: 20%
height: auto
width: auto
border-radius: 2px
a
width: 80%
text-decoration: none
color: $cPrimary
#content
display: flex
height: 100%
width: 100%
background-color: $cBackground
#column-left, #column-middle, #column-right
width: 33%
#column-middle
background: $cBackgroundVariant
border-radius: 20px
height: 100%
#column-right
padding: 0 20px 20px
display: grid
align-content: start
#user-avatar
max-width: 300px
width: 100%
height: auto
border-radius: 25%
#avatar-container
max-width: 300px
width: 100%
margin: auto
position: relative
#status-indicator
height: 20%
width: 20%
position: absolute
left: 0
top: 0
border-radius: 25%
display: block
z-index: 200
#status-indicator[status=online]
background-color: $cOnline
#status-indicator[status=idle]
background-color: $cIdle
#status-indicator[status=dnd]
background-color: $cDnd
#status-indicator[status=offline]
background-color: $cOffline
#guild-select
background: $cBackgroundVariant
color: $cPrimary
font-size: 150%
font-family: $fNormal
padding: 10px
width: 100%
margin: auto
border: none
height: 52px
border-radius: 12px
-webkit-appearance: none
#guild-icon-container
padding: 10px 0 0 0
display: flex
#guild-icon
max-width: 100px
width: 50%
height: auto
border-radius: 25%
#guild-nameAndIcon
width: 50%
#dj-songinfo
background-color: $cBackgroundVariant
border-radius: 20px
overflow-x: hidden
#songinfo-container
display: list-item
text-decoration: none
color: $cPrimary
padding: 10px
width: calc(100% - 20px)
#dj-queue-container
display: grid
padding: 0 5px 5px
#dj-songname
font-weight: bold
font-size: 120%
#dj-songImg
align-self: center
width: 80%
height: auto
margin: 0 10%
border-radius: 5%
#guildinfo
max-height: 90vh
overflow-y: hidden
#guildinfo:hover
overflow-y: auto
#dj-songQueue
display: grid
max-height: 100%

@ -0,0 +1,31 @@
$cPrimary: #fff
$cPrimaryVariant: #4c10a5
$cSecondary: #c889f5
$cSecondaryVariant: #740bce
$cBackground: #77f
$cBackgroundVariant: #55b
$cSurface: #fff
$cSurfaceVariant: #000
$cError: #f59289
$cErrorVariant: #b20a00
$cOnPrimary: #fff
$cOnSecondary: #000
$cOnSurface: #000
$cOnSurfaceShadow: lighten($cOnSurface, 30%)
$cOnSurfaceVariant: #fff
$cOnBackground: #000
$cOnBackgroundShadow: lighten($cOnBackground, 30%)
$cOnBackgroundVariant: #fff
$cOnError: #000
$cOnErrorVariant: #fff
$cOnline: #0f0
$cIdle: #ff0
$cDnd: #f00
$cOffline: #888
$cDebug: #00f
$cVerbose: #088
$cInfo: #890
$cWarn: #a60
$cError: #a00
$fNormal: Ubuntu, sans-serif

File diff suppressed because one or more lines are too long

@ -0,0 +1,26 @@
/* eslint-disable */
function login() {
let username = document.querySelector('#username').value;
let password = sha512(document.querySelector('#password').value);
$.post({
url: "/",
data: JSON.stringify({
username: username,
password: password
}),
contentType: "application/json"
}).done((res) => {
window.location.reload();
});
}
function handleSubmit(e) {
if (!e)
e = window.event;
if (e.which === 13) {
login();
}
}
window.addEventListener('keydown', handleSubmit, false);

@ -0,0 +1,308 @@
/* eslint-disable */
let latestLogs = [];
let status = {
0: 'ready',
1: 'connecting',
2: 'reconnecting',
3: 'idle',
4: 'nearly',
5: 'disconnected'
};
function getSplitDuration (duration) {
let dur = duration;
let retObj = {};
retObj.milliseconds = dur % 1000;
dur = Math.floor(dur / 1000);
retObj.seconds = dur % 60;
dur = Math.floor(dur / 60);
retObj.minutes = dur % 60;
dur = Math.floor(dur / 60);
retObj.hours = dur % 24;
dur = Math.floor(dur / 24);
retObj.days = dur;
return retObj;
}
function postQuery(query) {
return new Promise((resolve) => {
$.post({
url: "/graphql",
data: JSON.stringify({
query: query
}),
contentType: "application/json"
}).done((res) => resolve(res));
});
}
function queryStatic() {
let query = `{
client {
user {
tag
avatar
}
}
}`;
postQuery(query).then((res) => {
let d = res.data;
document.querySelector('#user-avatar').setAttribute('src', d.client.user.avatar);
document.querySelector('#user-tag').innerText = d.client.user.tag;
});
}
function queryGuilds() {
let query = `{
client {
guilds {
id
name
dj {
playing
}
}
}
}`;
postQuery(query).then((res) => {
for (let guild of res.data.client.guilds)
if ($(`option[value=${guild.id}]`).length === 0) {
let option = document.createElement('option');
option.setAttribute('value', guild.id);
if (guild.dj)
option.innerText = guild.dj.playing? guild.name + ' 🎶' : guild.name;
let guildSelect = document.querySelector('#guild-select');
guildSelect.appendChild(option);
}
});
}
function queryGuild(guildId) {
let query = `{
client {
guilds(id: "${guildId}") {
name
icon
memberCount
owner {
id
user {
tag
}
}
}
}
config
}`;
postQuery(query).then((res) => {
let guild = res.data.client.guilds[0];
document.querySelector('#guild-icon').setAttribute('src', guild.icon);
document.querySelector('#guild-name').innerText = guild.name;
document.querySelector('#guild-owner').innerText = guild.owner.user.tag;
document.querySelector('#guild-owner').setAttribute('owner-id', guild.owner.id);
document.querySelector('#guild-memberCount').innerText = guild.memberCount;
queryGuildStatus(guildId);
let serverinfo = $('#guildinfo');
if (serverinfo.is(':hidden'))
serverinfo.show();
});
}
/**
* @param guildId
*/
function queryGuildStatus(guildId) {
let query = `{
client {
guilds(id: "${guildId}") {
dj {
playing
connected
repeat
voiceChannel
songStartTime
paused
currentSong {
name
url
thumbnail
}
queueCount
queue(first: 5) {
id
name
url
thumbnail
}
}
}
}
config
}`;
postQuery(query).then((res) => {
let guild = res.data.client.guilds[0];
document.querySelector('#dj-repeat').innerText = guild.dj.repeat? 'on': 'off';
document.querySelector('#guild-djStatus').innerText = guild.dj.connected? 'connected' : 'disconnected';
if (guild.dj.connected) {
let songinfoContainer = $('#dj-songinfo');
songinfoContainer.show();
document.querySelector('#guild-djStatus').innerText = guild.dj.playing? 'playing' : 'connected';
document.querySelector('#dj-voiceChannel').innerText = guild.dj.voiceChannel;
if (guild.dj.playing) {
if (songinfoContainer.is(':hidden'))
songinfoContainer.show();
document.querySelector('#guild-djStatus').innerText = guild.dj.paused? 'paused' : 'playing';
document.querySelector('#songinfo-container').setAttribute('href', guild.dj.currentSong.url);
document.querySelector('#dj-songname').innerText = guild.dj.currentSong.name;
document.querySelector('#dj-songImg').setAttribute('src', guild.dj.currentSong.thumbnail.replace('maxresdefault', 'mqdefault'));
let songSd = getSplitDuration(Date.now() - guild.dj.songStartTime);
document.querySelector('#dj-songCurrentTS').innerText = `${songSd.minutes}:${songSd.seconds.toString().padStart(2, '0')}`;
document.querySelector('#dj-songCurrentTS').setAttribute('start-ts', guild.dj.songStartTime);
document.querySelector('#dj-queueCount').innerText = guild.dj.queueCount;
let songContainer = document.querySelector('#dj-songQueue');
$('.songEntry').remove();
for (let song of guild.dj.queue) {
let songEntry = document.createElement('a');
songEntry.setAttribute('href', song.url);
songEntry.setAttribute('class', 'songEntry');
songEntry.setAttribute('song-id', song.id);
let imageEntry = document.createElement('img');
imageEntry.setAttribute('src', song.thumbnail.replace('maxresdefault', 'mqdefault'));
songEntry.appendChild(imageEntry);
let nameEntry = document.createElement('a');
nameEntry.innerText = song.name;
songEntry.appendChild(nameEntry);
songContainer.appendChild(songEntry);
}
document.querySelector('#dj-queueDisplayCount').innerText = document.querySelectorAll('.songEntry').length;
} else {
if (songinfoContainer.is(':not(:hidden)'))
songinfoContainer.hide();
}
} else {
$('#dj-songinfo').hide();
document.querySelector('#dj-voiceChannel').innerText = 'None';
}
});
}
function queryStatus() {
let query = `{
client {
ping
status
uptime
guildCount
voiceConnectionCount
user {
presence {
game
status
}
}
}
}`;
postQuery(query).then((res) => {
let d = res.data;
document.querySelector('#client-ping').innerText = Math.round(d.client.ping * 10)/10 + ' ms';
document.querySelector('#client-status').innerText = status[d.client.status];
let sd = getSplitDuration(d.client.uptime);
document.querySelector('#client-uptime')
.innerText = `${sd.days}d ${sd.hours}h ${sd.minutes}min ${sd.seconds}s`;
document.querySelector('#client-guildCount').innerText = d.client.guildCount;
document.querySelector('#client-vcCount').innerText = d.client.voiceConnectionCount;
if (d.client.status !== 0)
document.querySelector('#status-indicator').setAttribute('status', 'offline');
else
document.querySelector('#status-indicator').setAttribute('status', d.client.user.presence.status);
document.querySelector('#user-game').innerText = d.client.user.presence.game;
setTimeout(() => {
let sd = getSplitDuration(d.client.uptime + 1000);
document.querySelector('#client-uptime')
.innerText = `${sd.days}d ${sd.hours}h ${sd.minutes}min ${sd.seconds}s`;
}, 1000);
});
}
function queryLogs(count) {
count = count || 5;
let query = `{
logs(last: ${count}, level: "verbose"){
id
level
message
timestamp
}
}`;
postQuery(query).then((res) => {
let d = res.data;
for (let logEntry of d.logs)
if (!latestLogs.find((x) => x.id === logEntry.id)) {
let entryElem = document.createElement('div');
entryElem.setAttribute('class', 'logEntry text-left');
entryElem.setAttribute('log-id', logEntry.id);
entryElem.setAttribute('level', logEntry.level);
let infoDiv = document.createElement('div');
infoDiv.setAttribute('class', 'infodiv');
let lvlSpan = document.createElement('span');
lvlSpan.innerText = logEntry.level;
lvlSpan.setAttribute('class', 'text-left');
infoDiv.appendChild(lvlSpan);
let tsSpan = document.createElement('span');
tsSpan.setAttribute('timestamp', logEntry.timestamp);
tsSpan.innerText = moment(logEntry.timestamp, 'YY-MM-DD-HH-mm-ss').format('MMM Do HH:mm:ss');
tsSpan.setAttribute('class', 'text-right');
infoDiv.appendChild(tsSpan);
entryElem.appendChild(infoDiv);
let msgSpan = document.createElement('span');
msgSpan.innerText = logEntry.message;
msgSpan.setAttribute('class', 'message');
entryElem.appendChild(msgSpan);
let logContainer = document.querySelector('#log-container');
logContainer.insertBefore(entryElem, logContainer.firstChild);
}
latestLogs = d.logs;
});
}
function startUpdating() {
queryStatic();
setInterval(queryStatic, 3600000);
queryStatus();
setInterval(queryStatus, 2000);
queryLogs(50);
setInterval(queryLogs, 5000);
queryGuilds();
setInterval(queryGuilds, 60000);
setInterval(() => {
let gid = $('#guild-select')[0].value;
if (gid && gid !== 'select-default')
queryGuildStatus(gid);
}, 5000);
setInterval(() => {
let gid = $('#guild-select')[0].value;
if (gid && gid !== 'select-default')
queryGuild(gid);
}, 600000);
$('#guild-select').on('change', (ev) => {
let fch = document.querySelector('#guild-select').firstElementChild;
if (fch.getAttribute('value') === 'select-default')
fch.remove();
let guildId = ev.target.value;
queryGuild(guildId);
});
setInterval(() => {
let songSd = getSplitDuration(Date.now() - $('#dj-songCurrentTS').attr('start-ts'));
document.querySelector('#dj-songCurrentTS').innerText = `${songSd.minutes}:${songSd.seconds.toString().padStart(2, '0')}`;
}, 500);
}
Loading…
Cancel
Save