diff --git a/README.md b/README.md index 43cfff4..9ee1646 100644 --- a/README.md +++ b/README.md @@ -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 --- diff --git a/bot.js b/bot.js index f13a31c..3ec77c2 100644 --- a/bot.js +++ b/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} + */ + 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} + */ + 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} + */ + 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); }); -} \ No newline at end of file +} diff --git a/commands/servercommands.json b/commands/servercommands.json index e1c26ab..36b2efc 100644 --- a/commands/servercommands.json +++ b/commands/servercommands.json @@ -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 @@ } } } -} \ No newline at end of file +} diff --git a/lib/cmd.js b/lib/cmd.js index f9971b9..539c2b7 100644 --- a/lib/cmd.js +++ b/lib/cmd.js @@ -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 -} \ No newline at end of file + return true; + + return false; +} diff --git a/lib/guilding.js b/lib/guilding.js index 7a2f743..b525df6 100644 --- a/lib/guilding.js +++ b/lib/guilding.js @@ -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(); - } -}; \ No newline at end of file diff --git a/lib/logging.js b/lib/logging.js index d73087c..2ff994a 100644 --- a/lib/logging.js +++ b/lib/logging.js @@ -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; -}; \ No newline at end of file +}; diff --git a/lib/music.js b/lib/music.js index 8ac2403..2564037 100644 --- a/lib/music.js +++ b/lib/music.js @@ -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(); -}; \ No newline at end of file diff --git a/lib/sqliteAsync.js b/lib/sqliteAsync.js new file mode 100644 index 0000000..134c015 --- /dev/null +++ b/lib/sqliteAsync.js @@ -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} + */ + 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} + */ + 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} + */ + 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} + */ + 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(); + } +}; diff --git a/lib/utils.js b/lib/utils.js index b2c07e9..3975f2d 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -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' -}; \ No newline at end of file +}; + +exports.logLevels = { + 'debug': 0, + 'verbose': 1, + 'info': 2, + 'warning': 3, + 'warn': 3, + 'error:': 4 +}; diff --git a/lib/weblib.js b/lib/weblib.js new file mode 100644 index 0000000..6704709 --- /dev/null +++ b/lib/weblib.js @@ -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} + */ + 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} + */ + 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; + } +} diff --git a/package.json b/package.json index a416cfb..3778775 100644 --- a/package.json +++ b/package.json @@ -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" + ] } } diff --git a/test/mockobjects.js b/test/mockobjects.js index 62da037..1495df4 100644 --- a/test/mockobjects.js +++ b/test/mockobjects.js @@ -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; } -}; \ No newline at end of file +}; diff --git a/test/test-setup.spec.js b/test/test-setup.spec.js index f7c976a..daa1d6c 100644 --- a/test/test-setup.spec.js +++ b/test/test-setup.spec.js @@ -1,3 +1,4 @@ +/* eslint-disable */ const sinon = require('sinon'), chai = require('chai'); @@ -7,4 +8,4 @@ beforeEach(() => { afterEach(() => { this.sandbox.restore(); -}); \ No newline at end of file +}); diff --git a/test/test.js b/test/test.js index 924568a..041ad6e 100644 --- a/test/test.js +++ b/test/test.js @@ -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); }); }); -}); \ No newline at end of file +}); diff --git a/web/graphql/schema.graphql b/web/graphql/schema.graphql new file mode 100644 index 0000000..bed89c0 --- /dev/null +++ b/web/graphql/schema.graphql @@ -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] +} diff --git a/web/http/index.pug b/web/http/index.pug new file mode 100644 index 0000000..145e2c6 --- /dev/null +++ b/web/http/index.pug @@ -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(); diff --git a/web/http/login.pug b/web/http/login.pug new file mode 100644 index 0000000..6cffcf2 --- /dev/null +++ b/web/http/login.pug @@ -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 diff --git a/web/http/sass/style.sass b/web/http/sass/style.sass new file mode 100644 index 0000000..50ab4a8 --- /dev/null +++ b/web/http/sass/style.sass @@ -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% diff --git a/web/http/sass/vars.sass b/web/http/sass/vars.sass new file mode 100644 index 0000000..c367af8 --- /dev/null +++ b/web/http/sass/vars.sass @@ -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 \ No newline at end of file diff --git a/web/http/scripts/lib/sha512.min.js b/web/http/scripts/lib/sha512.min.js new file mode 100644 index 0000000..7130221 --- /dev/null +++ b/web/http/scripts/lib/sha512.min.js @@ -0,0 +1,9 @@ +/* + * [js-sha512]{@link https://github.com/emn178/js-sha512} + * + * @version 0.8.0 + * @author Chen, Yi-Cyuan [emn178@gmail.com] + * @copyright Chen, Yi-Cyuan 2014-2018 + * @license MIT + */ +!function(){"use strict";function h(h,t){t?(p[0]=p[1]=p[2]=p[3]=p[4]=p[5]=p[6]=p[7]=p[8]=p[9]=p[10]=p[11]=p[12]=p[13]=p[14]=p[15]=p[16]=p[17]=p[18]=p[19]=p[20]=p[21]=p[22]=p[23]=p[24]=p[25]=p[26]=p[27]=p[28]=p[29]=p[30]=p[31]=p[32]=0,this.blocks=p):this.blocks=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],384==h?(this.h0h=3418070365,this.h0l=3238371032,this.h1h=1654270250,this.h1l=914150663,this.h2h=2438529370,this.h2l=812702999,this.h3h=355462360,this.h3l=4144912697,this.h4h=1731405415,this.h4l=4290775857,this.h5h=2394180231,this.h5l=1750603025,this.h6h=3675008525,this.h6l=1694076839,this.h7h=1203062813,this.h7l=3204075428):256==h?(this.h0h=573645204,this.h0l=4230739756,this.h1h=2673172387,this.h1l=3360449730,this.h2h=596883563,this.h2l=1867755857,this.h3h=2520282905,this.h3l=1497426621,this.h4h=2519219938,this.h4l=2827943907,this.h5h=3193839141,this.h5l=1401305490,this.h6h=721525244,this.h6l=746961066,this.h7h=246885852,this.h7l=2177182882):224==h?(this.h0h=2352822216,this.h0l=424955298,this.h1h=1944164710,this.h1l=2312950998,this.h2h=502970286,this.h2l=855612546,this.h3h=1738396948,this.h3l=1479516111,this.h4h=258812777,this.h4l=2077511080,this.h5h=2011393907,this.h5l=79989058,this.h6h=1067287976,this.h6l=1780299464,this.h7h=286451373,this.h7l=2446758561):(this.h0h=1779033703,this.h0l=4089235720,this.h1h=3144134277,this.h1l=2227873595,this.h2h=1013904242,this.h2l=4271175723,this.h3h=2773480762,this.h3l=1595750129,this.h4h=1359893119,this.h4l=2917565137,this.h5h=2600822924,this.h5l=725511199,this.h6h=528734635,this.h6l=4215389547,this.h7h=1541459225,this.h7l=327033209),this.bits=h,this.block=this.start=this.bytes=this.hBytes=0,this.finalized=this.hashed=!1}function t(t,s,e){var r,n=typeof t;if("string"!==n){if("object"!==n)throw new Error(i);if(null===t)throw new Error(i);if(a&&t.constructor===ArrayBuffer)t=new Uint8Array(t);else if(!(Array.isArray(t)||a&&ArrayBuffer.isView(t)))throw new Error(i);r=!0}var o=t.length;if(!r){for(var l,f=[],c=(o=t.length,0),u=0;u>6,f[c++]=128|63&l):l<55296||l>=57344?(f[c++]=224|l>>12,f[c++]=128|l>>6&63,f[c++]=128|63&l):(l=65536+((1023&l)<<10|1023&t.charCodeAt(++u)),f[c++]=240|l>>18,f[c++]=128|l>>12&63,f[c++]=128|l>>6&63,f[c++]=128|63&l);t=f}t.length>128&&(t=new h(s,!0).update(t).array());var y=[],p=[];for(u=0;u<128;++u){var d=t[u]||0;y[u]=92^d,p[u]=54^d}h.call(this,s,e),this.update(p),this.oKeyPad=y,this.inner=!0,this.sharedMemory=e}var i="input is invalid type",s="object"==typeof window,e=s?window:{};e.JS_SHA512_NO_WINDOW&&(s=!1);var r=!s&&"object"==typeof self;!e.JS_SHA512_NO_NODE_JS&&"object"==typeof process&&process.versions&&process.versions.node?e=global:r&&(e=self);var n=!e.JS_SHA512_NO_COMMON_JS&&"object"==typeof module&&module.exports,o="function"==typeof define&&define.amd,a=!e.JS_SHA512_NO_ARRAY_BUFFER&&"undefined"!=typeof ArrayBuffer,l="0123456789abcdef".split(""),f=[-2147483648,8388608,32768,128],c=[24,16,8,0],u=[1116352408,3609767458,1899447441,602891725,3049323471,3964484399,3921009573,2173295548,961987163,4081628472,1508970993,3053834265,2453635748,2937671579,2870763221,3664609560,3624381080,2734883394,310598401,1164996542,607225278,1323610764,1426881987,3590304994,1925078388,4068182383,2162078206,991336113,2614888103,633803317,3248222580,3479774868,3835390401,2666613458,4022224774,944711139,264347078,2341262773,604807628,2007800933,770255983,1495990901,1249150122,1856431235,1555081692,3175218132,1996064986,2198950837,2554220882,3999719339,2821834349,766784016,2952996808,2566594879,3210313671,3203337956,3336571891,1034457026,3584528711,2466948901,113926993,3758326383,338241895,168717936,666307205,1188179964,773529912,1546045734,1294757372,1522805485,1396182291,2643833823,1695183700,2343527390,1986661051,1014477480,2177026350,1206759142,2456956037,344077627,2730485921,1290863460,2820302411,3158454273,3259730800,3505952657,3345764771,106217008,3516065817,3606008344,3600352804,1432725776,4094571909,1467031594,275423344,851169720,430227734,3100823752,506948616,1363258195,659060556,3750685593,883997877,3785050280,958139571,3318307427,1322822218,3812723403,1537002063,2003034995,1747873779,3602036899,1955562222,1575990012,2024104815,1125592928,2227730452,2716904306,2361852424,442776044,2428436474,593698344,2756734187,3733110249,3204031479,2999351573,3329325298,3815920427,3391569614,3928383900,3515267271,566280711,3940187606,3454069534,4118630271,4000239992,116418474,1914138554,174292421,2731055270,289380356,3203993006,460393269,320620315,685471733,587496836,852142971,1086792851,1017036298,365543100,1126000580,2618297676,1288033470,3409855158,1501505948,4234509866,1607167915,987167468,1816402316,1246189591],y=["hex","array","digest","arrayBuffer"],p=[];!e.JS_SHA512_NO_NODE_JS&&Array.isArray||(Array.isArray=function(h){return"[object Array]"===Object.prototype.toString.call(h)}),!a||!e.JS_SHA512_NO_ARRAY_BUFFER_IS_VIEW&&ArrayBuffer.isView||(ArrayBuffer.isView=function(h){return"object"==typeof h&&h.buffer&&h.buffer.constructor===ArrayBuffer});var d=function(t,i){return function(s){return new h(i,!0).update(s)[t]()}},b=function(t){var i=d("hex",t);i.create=function(){return new h(t)},i.update=function(h){return i.create().update(h)};for(var s=0;s>2]|=h[n]<>2]|=e<>2]|=(192|e>>6)<>2]|=(128|63&e)<=57344?(l[r>>2]|=(224|e>>12)<>2]|=(128|e>>6&63)<>2]|=(128|63&e)<>2]|=(240|e>>18)<>2]|=(128|e>>12&63)<>2]|=(128|e>>6&63)<>2]|=(128|63&e)<=128?(this.block=l[32],this.start=r-128,this.hash(),this.hashed=!0):this.start=r}return this.bytes>4294967295&&(this.hBytes+=this.bytes/4294967296<<0,this.bytes=this.bytes%4294967296),this},h.prototype.finalize=function(){if(!this.finalized){this.finalized=!0;var h=this.blocks,t=this.lastByteIndex;h[32]=this.block,h[t>>2]|=f[3&t],this.block=h[32],t>=112&&(this.hashed||this.hash(),h[0]=this.block,h[1]=h[2]=h[3]=h[4]=h[5]=h[6]=h[7]=h[8]=h[9]=h[10]=h[11]=h[12]=h[13]=h[14]=h[15]=h[16]=h[17]=h[18]=h[19]=h[20]=h[21]=h[22]=h[23]=h[24]=h[25]=h[26]=h[27]=h[28]=h[29]=h[30]=h[31]=h[32]=0),h[30]=this.hBytes<<3|this.bytes>>>29,h[31]=this.bytes<<3,this.hash()}},h.prototype.hash=function(){var h,t,i,s,e,r,n,o,a,l,f,c,y,p,d,b,w,A,_,v,B,U,S,g,k,z=this.h0h,E=this.h0l,O=this.h1h,m=this.h1l,x=this.h2h,N=this.h2l,j=this.h3h,J=this.h3l,H=this.h4h,I=this.h4l,R=this.h5h,V=this.h5l,C=this.h6h,K=this.h6l,P=this.h7h,D=this.h7l,F=this.blocks;for(h=32;h<160;h+=2)t=((v=F[h-30])>>>1|(B=F[h-29])<<31)^(v>>>8|B<<24)^v>>>7,i=(B>>>1|v<<31)^(B>>>8|v<<24)^(B>>>7|v<<25),s=((v=F[h-4])>>>19|(B=F[h-3])<<13)^(B>>>29|v<<3)^v>>>6,e=(B>>>19|v<<13)^(v>>>29|B<<3)^(B>>>6|v<<26),v=F[h-32],B=F[h-31],a=((U=F[h-14])>>>16)+(v>>>16)+(t>>>16)+(s>>>16)+((o=(65535&U)+(65535&v)+(65535&t)+(65535&s)+((n=((S=F[h-13])>>>16)+(B>>>16)+(i>>>16)+(e>>>16)+((r=(65535&S)+(65535&B)+(65535&i)+(65535&e))>>>16))>>>16))>>>16),F[h]=a<<16|65535&o,F[h+1]=n<<16|65535&r;var M=z,T=E,W=O,Y=m,q=x,G=N,L=j,Q=J,X=H,Z=I,$=R,hh=V,th=C,ih=K,sh=P,eh=D;for(b=W&q,w=Y&G,h=0;h<160;h+=8)t=(M>>>28|T<<4)^(T>>>2|M<<30)^(T>>>7|M<<25),i=(T>>>28|M<<4)^(M>>>2|T<<30)^(M>>>7|T<<25),s=(X>>>14|Z<<18)^(X>>>18|Z<<14)^(Z>>>9|X<<23),e=(Z>>>14|X<<18)^(Z>>>18|X<<14)^(X>>>9|Z<<23),A=(l=M&W)^M&q^b,_=(f=T&Y)^T&G^w,g=X&$^~X&th,k=Z&hh^~Z&ih,v=F[h],B=F[h+1],v=(a=((U=u[h])>>>16)+(v>>>16)+(g>>>16)+(s>>>16)+(sh>>>16)+((o=(65535&U)+(65535&v)+(65535&g)+(65535&s)+(65535&sh)+((n=((S=u[h+1])>>>16)+(B>>>16)+(k>>>16)+(e>>>16)+(eh>>>16)+((r=(65535&S)+(65535&B)+(65535&k)+(65535&e)+(65535&eh))>>>16))>>>16))>>>16))<<16|65535&o,B=n<<16|65535&r,U=(a=(A>>>16)+(t>>>16)+((o=(65535&A)+(65535&t)+((n=(_>>>16)+(i>>>16)+((r=(65535&_)+(65535&i))>>>16))>>>16))>>>16))<<16|65535&o,S=n<<16|65535&r,sh=(a=(L>>>16)+(v>>>16)+((o=(65535&L)+(65535&v)+((n=(Q>>>16)+(B>>>16)+((r=(65535&Q)+(65535&B))>>>16))>>>16))>>>16))<<16|65535&o,eh=n<<16|65535&r,t=((L=(a=(U>>>16)+(v>>>16)+((o=(65535&U)+(65535&v)+((n=(S>>>16)+(B>>>16)+((r=(65535&S)+(65535&B))>>>16))>>>16))>>>16))<<16|65535&o)>>>28|(Q=n<<16|65535&r)<<4)^(Q>>>2|L<<30)^(Q>>>7|L<<25),i=(Q>>>28|L<<4)^(L>>>2|Q<<30)^(L>>>7|Q<<25),s=(sh>>>14|eh<<18)^(sh>>>18|eh<<14)^(eh>>>9|sh<<23),e=(eh>>>14|sh<<18)^(eh>>>18|sh<<14)^(sh>>>9|eh<<23),A=(c=L&M)^L&W^l,_=(y=Q&T)^Q&Y^f,g=sh&X^~sh&$,k=eh&Z^~eh&hh,v=F[h+2],B=F[h+3],v=(a=((U=u[h+2])>>>16)+(v>>>16)+(g>>>16)+(s>>>16)+(th>>>16)+((o=(65535&U)+(65535&v)+(65535&g)+(65535&s)+(65535&th)+((n=((S=u[h+3])>>>16)+(B>>>16)+(k>>>16)+(e>>>16)+(ih>>>16)+((r=(65535&S)+(65535&B)+(65535&k)+(65535&e)+(65535&ih))>>>16))>>>16))>>>16))<<16|65535&o,B=n<<16|65535&r,U=(a=(A>>>16)+(t>>>16)+((o=(65535&A)+(65535&t)+((n=(_>>>16)+(i>>>16)+((r=(65535&_)+(65535&i))>>>16))>>>16))>>>16))<<16|65535&o,S=n<<16|65535&r,th=(a=(q>>>16)+(v>>>16)+((o=(65535&q)+(65535&v)+((n=(G>>>16)+(B>>>16)+((r=(65535&G)+(65535&B))>>>16))>>>16))>>>16))<<16|65535&o,ih=n<<16|65535&r,t=((q=(a=(U>>>16)+(v>>>16)+((o=(65535&U)+(65535&v)+((n=(S>>>16)+(B>>>16)+((r=(65535&S)+(65535&B))>>>16))>>>16))>>>16))<<16|65535&o)>>>28|(G=n<<16|65535&r)<<4)^(G>>>2|q<<30)^(G>>>7|q<<25),i=(G>>>28|q<<4)^(q>>>2|G<<30)^(q>>>7|G<<25),s=(th>>>14|ih<<18)^(th>>>18|ih<<14)^(ih>>>9|th<<23),e=(ih>>>14|th<<18)^(ih>>>18|th<<14)^(th>>>9|ih<<23),A=(p=q&L)^q&M^c,_=(d=G&Q)^G&T^y,g=th&sh^~th&X,k=ih&eh^~ih&Z,v=F[h+4],B=F[h+5],v=(a=((U=u[h+4])>>>16)+(v>>>16)+(g>>>16)+(s>>>16)+($>>>16)+((o=(65535&U)+(65535&v)+(65535&g)+(65535&s)+(65535&$)+((n=((S=u[h+5])>>>16)+(B>>>16)+(k>>>16)+(e>>>16)+(hh>>>16)+((r=(65535&S)+(65535&B)+(65535&k)+(65535&e)+(65535&hh))>>>16))>>>16))>>>16))<<16|65535&o,B=n<<16|65535&r,U=(a=(A>>>16)+(t>>>16)+((o=(65535&A)+(65535&t)+((n=(_>>>16)+(i>>>16)+((r=(65535&_)+(65535&i))>>>16))>>>16))>>>16))<<16|65535&o,S=n<<16|65535&r,$=(a=(W>>>16)+(v>>>16)+((o=(65535&W)+(65535&v)+((n=(Y>>>16)+(B>>>16)+((r=(65535&Y)+(65535&B))>>>16))>>>16))>>>16))<<16|65535&o,hh=n<<16|65535&r,t=((W=(a=(U>>>16)+(v>>>16)+((o=(65535&U)+(65535&v)+((n=(S>>>16)+(B>>>16)+((r=(65535&S)+(65535&B))>>>16))>>>16))>>>16))<<16|65535&o)>>>28|(Y=n<<16|65535&r)<<4)^(Y>>>2|W<<30)^(Y>>>7|W<<25),i=(Y>>>28|W<<4)^(W>>>2|Y<<30)^(W>>>7|Y<<25),s=($>>>14|hh<<18)^($>>>18|hh<<14)^(hh>>>9|$<<23),e=(hh>>>14|$<<18)^(hh>>>18|$<<14)^($>>>9|hh<<23),A=(b=W&q)^W&L^p,_=(w=Y&G)^Y&Q^d,g=$&th^~$&sh,k=hh&ih^~hh&eh,v=F[h+6],B=F[h+7],v=(a=((U=u[h+6])>>>16)+(v>>>16)+(g>>>16)+(s>>>16)+(X>>>16)+((o=(65535&U)+(65535&v)+(65535&g)+(65535&s)+(65535&X)+((n=((S=u[h+7])>>>16)+(B>>>16)+(k>>>16)+(e>>>16)+(Z>>>16)+((r=(65535&S)+(65535&B)+(65535&k)+(65535&e)+(65535&Z))>>>16))>>>16))>>>16))<<16|65535&o,B=n<<16|65535&r,U=(a=(A>>>16)+(t>>>16)+((o=(65535&A)+(65535&t)+((n=(_>>>16)+(i>>>16)+((r=(65535&_)+(65535&i))>>>16))>>>16))>>>16))<<16|65535&o,S=n<<16|65535&r,X=(a=(M>>>16)+(v>>>16)+((o=(65535&M)+(65535&v)+((n=(T>>>16)+(B>>>16)+((r=(65535&T)+(65535&B))>>>16))>>>16))>>>16))<<16|65535&o,Z=n<<16|65535&r,M=(a=(U>>>16)+(v>>>16)+((o=(65535&U)+(65535&v)+((n=(S>>>16)+(B>>>16)+((r=(65535&S)+(65535&B))>>>16))>>>16))>>>16))<<16|65535&o,T=n<<16|65535&r;a=(z>>>16)+(M>>>16)+((o=(65535&z)+(65535&M)+((n=(E>>>16)+(T>>>16)+((r=(65535&E)+(65535&T))>>>16))>>>16))>>>16),this.h0h=a<<16|65535&o,this.h0l=n<<16|65535&r,a=(O>>>16)+(W>>>16)+((o=(65535&O)+(65535&W)+((n=(m>>>16)+(Y>>>16)+((r=(65535&m)+(65535&Y))>>>16))>>>16))>>>16),this.h1h=a<<16|65535&o,this.h1l=n<<16|65535&r,a=(x>>>16)+(q>>>16)+((o=(65535&x)+(65535&q)+((n=(N>>>16)+(G>>>16)+((r=(65535&N)+(65535&G))>>>16))>>>16))>>>16),this.h2h=a<<16|65535&o,this.h2l=n<<16|65535&r,a=(j>>>16)+(L>>>16)+((o=(65535&j)+(65535&L)+((n=(J>>>16)+(Q>>>16)+((r=(65535&J)+(65535&Q))>>>16))>>>16))>>>16),this.h3h=a<<16|65535&o,this.h3l=n<<16|65535&r,a=(H>>>16)+(X>>>16)+((o=(65535&H)+(65535&X)+((n=(I>>>16)+(Z>>>16)+((r=(65535&I)+(65535&Z))>>>16))>>>16))>>>16),this.h4h=a<<16|65535&o,this.h4l=n<<16|65535&r,a=(R>>>16)+($>>>16)+((o=(65535&R)+(65535&$)+((n=(V>>>16)+(hh>>>16)+((r=(65535&V)+(65535&hh))>>>16))>>>16))>>>16),this.h5h=a<<16|65535&o,this.h5l=n<<16|65535&r,a=(C>>>16)+(th>>>16)+((o=(65535&C)+(65535&th)+((n=(K>>>16)+(ih>>>16)+((r=(65535&K)+(65535&ih))>>>16))>>>16))>>>16),this.h6h=a<<16|65535&o,this.h6l=n<<16|65535&r,a=(P>>>16)+(sh>>>16)+((o=(65535&P)+(65535&sh)+((n=(D>>>16)+(eh>>>16)+((r=(65535&D)+(65535&eh))>>>16))>>>16))>>>16),this.h7h=a<<16|65535&o,this.h7l=n<<16|65535&r},h.prototype.hex=function(){this.finalize();var h=this.h0h,t=this.h0l,i=this.h1h,s=this.h1l,e=this.h2h,r=this.h2l,n=this.h3h,o=this.h3l,a=this.h4h,f=this.h4l,c=this.h5h,u=this.h5l,y=this.h6h,p=this.h6l,d=this.h7h,b=this.h7l,w=this.bits,A=l[h>>28&15]+l[h>>24&15]+l[h>>20&15]+l[h>>16&15]+l[h>>12&15]+l[h>>8&15]+l[h>>4&15]+l[15&h]+l[t>>28&15]+l[t>>24&15]+l[t>>20&15]+l[t>>16&15]+l[t>>12&15]+l[t>>8&15]+l[t>>4&15]+l[15&t]+l[i>>28&15]+l[i>>24&15]+l[i>>20&15]+l[i>>16&15]+l[i>>12&15]+l[i>>8&15]+l[i>>4&15]+l[15&i]+l[s>>28&15]+l[s>>24&15]+l[s>>20&15]+l[s>>16&15]+l[s>>12&15]+l[s>>8&15]+l[s>>4&15]+l[15&s]+l[e>>28&15]+l[e>>24&15]+l[e>>20&15]+l[e>>16&15]+l[e>>12&15]+l[e>>8&15]+l[e>>4&15]+l[15&e]+l[r>>28&15]+l[r>>24&15]+l[r>>20&15]+l[r>>16&15]+l[r>>12&15]+l[r>>8&15]+l[r>>4&15]+l[15&r]+l[n>>28&15]+l[n>>24&15]+l[n>>20&15]+l[n>>16&15]+l[n>>12&15]+l[n>>8&15]+l[n>>4&15]+l[15&n];return w>=256&&(A+=l[o>>28&15]+l[o>>24&15]+l[o>>20&15]+l[o>>16&15]+l[o>>12&15]+l[o>>8&15]+l[o>>4&15]+l[15&o]),w>=384&&(A+=l[a>>28&15]+l[a>>24&15]+l[a>>20&15]+l[a>>16&15]+l[a>>12&15]+l[a>>8&15]+l[a>>4&15]+l[15&a]+l[f>>28&15]+l[f>>24&15]+l[f>>20&15]+l[f>>16&15]+l[f>>12&15]+l[f>>8&15]+l[f>>4&15]+l[15&f]+l[c>>28&15]+l[c>>24&15]+l[c>>20&15]+l[c>>16&15]+l[c>>12&15]+l[c>>8&15]+l[c>>4&15]+l[15&c]+l[u>>28&15]+l[u>>24&15]+l[u>>20&15]+l[u>>16&15]+l[u>>12&15]+l[u>>8&15]+l[u>>4&15]+l[15&u]),512==w&&(A+=l[y>>28&15]+l[y>>24&15]+l[y>>20&15]+l[y>>16&15]+l[y>>12&15]+l[y>>8&15]+l[y>>4&15]+l[15&y]+l[p>>28&15]+l[p>>24&15]+l[p>>20&15]+l[p>>16&15]+l[p>>12&15]+l[p>>8&15]+l[p>>4&15]+l[15&p]+l[d>>28&15]+l[d>>24&15]+l[d>>20&15]+l[d>>16&15]+l[d>>12&15]+l[d>>8&15]+l[d>>4&15]+l[15&d]+l[b>>28&15]+l[b>>24&15]+l[b>>20&15]+l[b>>16&15]+l[b>>12&15]+l[b>>8&15]+l[b>>4&15]+l[15&b]),A},h.prototype.toString=h.prototype.hex,h.prototype.digest=function(){this.finalize();var h=this.h0h,t=this.h0l,i=this.h1h,s=this.h1l,e=this.h2h,r=this.h2l,n=this.h3h,o=this.h3l,a=this.h4h,l=this.h4l,f=this.h5h,c=this.h5l,u=this.h6h,y=this.h6l,p=this.h7h,d=this.h7l,b=this.bits,w=[h>>24&255,h>>16&255,h>>8&255,255&h,t>>24&255,t>>16&255,t>>8&255,255&t,i>>24&255,i>>16&255,i>>8&255,255&i,s>>24&255,s>>16&255,s>>8&255,255&s,e>>24&255,e>>16&255,e>>8&255,255&e,r>>24&255,r>>16&255,r>>8&255,255&r,n>>24&255,n>>16&255,n>>8&255,255&n];return b>=256&&w.push(o>>24&255,o>>16&255,o>>8&255,255&o),b>=384&&w.push(a>>24&255,a>>16&255,a>>8&255,255&a,l>>24&255,l>>16&255,l>>8&255,255&l,f>>24&255,f>>16&255,f>>8&255,255&f,c>>24&255,c>>16&255,c>>8&255,255&c),512==b&&w.push(u>>24&255,u>>16&255,u>>8&255,255&u,y>>24&255,y>>16&255,y>>8&255,255&y,p>>24&255,p>>16&255,p>>8&255,255&p,d>>24&255,d>>16&255,d>>8&255,255&d),w},h.prototype.array=h.prototype.digest,h.prototype.arrayBuffer=function(){this.finalize();var h=this.bits,t=new ArrayBuffer(h/8),i=new DataView(t);return i.setUint32(0,this.h0h),i.setUint32(4,this.h0l),i.setUint32(8,this.h1h),i.setUint32(12,this.h1l),i.setUint32(16,this.h2h),i.setUint32(20,this.h2l),i.setUint32(24,this.h3h),h>=256&&i.setUint32(28,this.h3l),h>=384&&(i.setUint32(32,this.h4h),i.setUint32(36,this.h4l),i.setUint32(40,this.h5h),i.setUint32(44,this.h5l)),512==h&&(i.setUint32(48,this.h6h),i.setUint32(52,this.h6l),i.setUint32(56,this.h7h),i.setUint32(60,this.h7l)),t},h.prototype.clone=function(){var t=new h(this.bits,!1);return this.copyTo(t),t},h.prototype.copyTo=function(h){var t=0,i=["h0h","h0l","h1h","h1l","h2h","h2l","h3h","h3l","h4h","h4l","h5h","h5l","h6h","h6l","h7h","h7l","start","bytes","hBytes","finalized","hashed","lastByteIndex"];for(t=0;t { + window.location.reload(); + }); +} + +function handleSubmit(e) { + if (!e) + e = window.event; + if (e.which === 13) { + login(); + } +} + +window.addEventListener('keydown', handleSubmit, false); diff --git a/web/http/scripts/query.js b/web/http/scripts/query.js new file mode 100644 index 0000000..424abf2 --- /dev/null +++ b/web/http/scripts/query.js @@ -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); +}