From 275179b990957bf9d6ce5782ad7a0d4873a8f654 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Thu, 31 Jan 2019 20:52:35 +0100 Subject: [PATCH 01/15] Added Graphiql - added some discord objects (see schema for more info) --- README.md | 5 +++ bot.js | 15 +++++++ graphql/schema.graphql | 39 +++++++++++++++++ lib/webapi.js | 98 ++++++++++++++++++++++++++++++++++++++++++ package.json | 9 ++++ 5 files changed, 166 insertions(+) create mode 100644 graphql/schema.graphql create mode 100644 lib/webapi.js diff --git a/README.md b/README.md index 43cfff4..9014935 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,11 @@ 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 } } ``` diff --git a/bot.js b/bot.js index f13a31c..6585572 100644 --- a/bot.js +++ b/bot.js @@ -4,6 +4,7 @@ const Discord = require("discord.js"), cmd = require("./lib/cmd"), guilding = require('./lib/guilding'), utils = require('./lib/utils'), + webapi = require('./lib/webapi'), config = require('./config.json'), args = require('args-parser')(process.argv), sqlite3 = require('sqlite3'), @@ -43,6 +44,7 @@ class Bot { } } guilding.setLogger(logger); + webapi.setLogger(logger); cmd.init(prefix); logger.verbose('Registering commands'); this.registerCommands(); @@ -68,6 +70,15 @@ class Bot { }); }); this.registerCallbacks(); + + if (config.webservice && config.webservice.enabled) { + logger.verbose('Creating WebServer'); + this.webServer = new webapi.WebServer(config.webservice.port || 8080); + logger.debug('Setting Reference Objects to webserver'); + this.webServer.setReferenceObjects({ + client: this.client + }); + } } start() { @@ -78,6 +89,10 @@ class Bot { }).catch((err) => { reject(err); }); + if (this.webServer){ + this.webServer.start(); + logger.info(`WebServer runing on port ${this.webServer.port}`); + } }) } diff --git a/graphql/schema.graphql b/graphql/schema.graphql new file mode 100644 index 0000000..404e47d --- /dev/null +++ b/graphql/schema.graphql @@ -0,0 +1,39 @@ +type User { + id: String + name: String + avatar: String + bot: Boolean + tag: String +} +type Role { + id: String + name: String + color: String + members: [GuildMember] +} +type GuildMember { + id: String + user: User + nickname: String + roles: [Role] + highestRole: Role +} +type Guild { + id: String + name: String + owner: GuildMember + members: [GuildMember] + roles: [Role] + memberCount: Int + icon: String +} +type Client { + guilds(count: Int): [Guild] + user: User + ping: Float + status: Int + uptime: Int +} +type Query { + client: Client +} \ No newline at end of file diff --git a/lib/webapi.js b/lib/webapi.js new file mode 100644 index 0000000..32f578a --- /dev/null +++ b/lib/webapi.js @@ -0,0 +1,98 @@ +const express = require('express'), + graphqlHTTP = require('express-graphql'), + {buildSchema} = require('graphql'), + config = require('../config.json'), + fs = require('fs'); + +let logger = require('winston'); + +exports.setLogger = function (newLogger) { + logger = newLogger; +}; + +exports.WebServer = class { + constructor(port, schema, root) { + this.app = express(); + this.port = port; + this.schema = buildSchema(fs.readFileSync('./graphql/schema.graphql', 'utf-8')); + this.root = {}; + } + + start() { + this.app.use('/graphql', graphqlHTTP({ + schema: this.schema, + rootValue: this.root, + graphiql: config.webservice.graphiql || false + })); + this.app.listen(this.port); + } + + setReferenceObjects(objects) { + this.root = { + client: { + guilds: ({count}) => { + let dcGuilds = objects.client.guilds.values(); + return Array.from(dcGuilds).map((x) => new Guild(x)).slice(0, count); + }, + user: () => { + return new User(objects.client.user); + }, + ping: () => { + return objects.client.ping; + }, + status: () => { + return objects.client.status; + }, + uptime: () => { + return objects.client.uptime; + } + } + } + } +}; + +class Guild { + constructor(discordGuild) { + this.id = discordGuild.id; + this.name = discordGuild.name; + this.owner = new GuildMember(discordGuild.owner); + this.memberCount = discordGuild.memberCount; + this.icon = discordGuild.iconURL; + this.members = Array.from(discordGuild.members.values()) + .map((x) => new GuildMember(x)); + this.roles = Array.from(discordGuild.roles.values()) + .map((x) => new Role(x)); + } +} + +class Role { + constructor(discordRole) { + this.id = discordRole.id; + this.name = discordRole.name; + this.color = discordRole.hexColor; + this.members = Array.from(discordRole.members.values) + .map((x) => new GuildMember(x)); + } +} + +class GuildMember { + constructor(discordGuildMember) { + this.id = discordGuildMember.id; + this.user = new User(discordGuildMember.user); + this.nickname = discordGuildMember.nickname; + this.roles = Array.from(discordGuildMember.roles.values()) + .map((x) => new Role(x)); + this.highestRole = new Role(discordGuildMember.highestRole); + } +} + +class User { + constructor(discordUser) { + this.id = discordUser.id; + this.name = discordUser.username; + this.avatar = discordUser.avatarURL; + this.bot = discordUser.bot; + this.tag = discordUser.tag; + this.tag = discordUser.tag; + } +} \ No newline at end of file diff --git a/package.json b/package.json index a416cfb..0ae3f96 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,12 @@ "dependencies": { "args-parser": "1.1.0", "discord.js": "11.4.2", + "eslint-plugin-graphql": "^3.0.1", + "express": "^4.16.4", + "express-graphql": "^0.7.1", "ffmpeg-binaries": "4.0.0", "get-youtube-title": "1.0.0", + "graphql": "^14.1.1", "opusscript": "0.0.6", "sqlite3": "4.0.6", "winston": "3.2.1", @@ -25,5 +29,10 @@ "nyc": "^13.1.0", "rewire": "^4.0.1", "sinon": "^7.2.3" + }, + "eslintConfig": { + "parserOptions": { + "sourceType": "script" + } } } From 72794a2f633dccf6b34d14aa1ca0d5f8e36c5f87 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Fri, 1 Feb 2019 21:18:41 +0100 Subject: [PATCH 02/15] Added Query entries - changed id to type ID! - changed id to md5 hash of base64 string of properties - added discord Id - added paging to arrays - implemented guildHandler into Guild query type - added saved to retrieve saved songs/playlists - added ready indicator boolean - added querying logs - added presences array - added config as String - added prefix --- bot.js | 41 +++++++--- graphql/schema.graphql | 43 ++++++++--- lib/logging.js | 31 ++++---- lib/webapi.js | 172 ++++++++++++++++++++++++++++++++++++++--- package.json | 2 + 5 files changed, 241 insertions(+), 48 deletions(-) diff --git a/bot.js b/bot.js index 6585572..c7be1c3 100644 --- a/bot.js +++ b/bot.js @@ -4,7 +4,6 @@ const Discord = require("discord.js"), cmd = require("./lib/cmd"), guilding = require('./lib/guilding'), utils = require('./lib/utils'), - webapi = require('./lib/webapi'), config = require('./config.json'), args = require('args-parser')(process.argv), sqlite3 = require('sqlite3'), @@ -12,6 +11,8 @@ const Discord = require("discord.js"), prefix = args.prefix || config.prefix || '~', gamepresence = args.game || config.presence; +let webapi = null; + class Bot { constructor() { this.client = new Discord.Client(); @@ -44,11 +45,11 @@ class Bot { } } guilding.setLogger(logger); - webapi.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) => { @@ -58,7 +59,7 @@ class Bot { this.maindb.run(`${utils.sql.tableExistCreate} presences ( ${utils.sql.pkIdSerial}, text VARCHAR(255) UNIQUE NOT NULL - )`, (err) => { + )`, (err) => { if (err) { logger.error(err.message); } else { @@ -72,15 +73,27 @@ class Bot { this.registerCallbacks(); if (config.webservice && config.webservice.enabled) { + logger.verbose('Importing webapi'); + webapi = require('./lib/webapi'); + webapi.setLogger(logger); logger.verbose('Creating WebServer'); this.webServer = new webapi.WebServer(config.webservice.port || 8080); logger.debug('Setting Reference Objects to webserver'); + this.webServer.setReferenceObjects({ - client: this.client + client: this.client, + presences: this.presences, + maind: this.maindb, + prefix: prefix, + getGuildHandler: (guild) => this.getGuildHandler(guild, prefix) }); } } + /** + * Starting the bot by connecting to the discord service and starting the webservice. + * @returns {Promise} + */ start() { return new Promise((resolve, reject) => { this.client.login(authToken).then(() => { @@ -89,7 +102,7 @@ class Bot { }).catch((err) => { reject(err); }); - if (this.webServer){ + if (this.webServer) { this.webServer.start(); logger.info(`WebServer runing on port ${this.webServer.port}`); } @@ -116,7 +129,8 @@ class Bot { }); this.presences.push(line); }); - this.rotator = this.client.setInterval(() => this.rotatePresence(), config.presence_duration || 360000); + this.rotator = this.client.setInterval(() => this.rotatePresence(), + config.presence_duration || 360000); fs.unlink('./data/presences.txt', (err) => { if (err) logger.warn(err.message); @@ -140,7 +154,8 @@ class Bot { this.presences.push(row.text); } } - this.rotator = this.client.setInterval(() => this.rotatePresence(), config.presence_duration || 360000); + this.rotator = this.client.setInterval(() => this.rotatePresence(), + config.presence_duration || 360000); }) } } @@ -219,8 +234,10 @@ 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}`)); } @@ -234,7 +251,11 @@ class Bot { 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); diff --git a/graphql/schema.graphql b/graphql/schema.graphql index 404e47d..24eca7d 100644 --- a/graphql/schema.graphql +++ b/graphql/schema.graphql @@ -1,39 +1,60 @@ type User { - id: String - name: String + id: ID! + discordId: String + name: String! avatar: String bot: Boolean - tag: String + tag: String! } type Role { - id: String + id: ID! + discordId: String name: String color: String - members: [GuildMember] + members(first: Int = 10, offset: Int = 0, id: String): [GuildMember] } type GuildMember { - id: String + id: ID! + discordId: String user: User nickname: String - roles: [Role] + roles(first: Int = 10, offset: Int = 0, id: String): [Role] highestRole: Role } type Guild { - id: String + id: ID! + discordId: String name: String owner: GuildMember - members: [GuildMember] - roles: [Role] + members(first: Int = 10, offset: Int = 0, id: String): [GuildMember] + roles(first: Int = 10, offset: Int = 0, id: String): [Role] memberCount: Int icon: String + ready: Boolean + saved(first: Int = 10, offset: Int = 0, id: String, name: String): [SavedEntry!] } type Client { - guilds(count: Int): [Guild] + guilds(first: Int = 10, offset: Int = 0, id: String): [Guild] user: User ping: Float status: Int uptime: Int } +type SavedEntry { + id: ID! + url: String! + name: 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): [LogEntry] } \ No newline at end of file diff --git a/lib/logging.js b/lib/logging.js index d73087c..3d8567d 100644 --- a/lib/logging.js +++ b/lib/logging.js @@ -11,37 +11,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 }) ] }); diff --git a/lib/webapi.js b/lib/webapi.js index 32f578a..1e16e23 100644 --- a/lib/webapi.js +++ b/lib/webapi.js @@ -1,6 +1,8 @@ const express = require('express'), graphqlHTTP = require('express-graphql'), {buildSchema} = require('graphql'), + compression = require('compression'), + md5 = require('js-md5'), config = require('../config.json'), fs = require('fs'); @@ -18,7 +20,19 @@ exports.WebServer = class { this.root = {}; } + /** + * Starting the api webserver + */ start() { + this.app.use(compression({ + filter: (req, res) => { + if (req.headers['x-no-compression']) { + return false + } else { + return compression.filter(req, res); + } + } + })); this.app.use('/graphql', graphqlHTTP({ schema: this.schema, rootValue: this.root, @@ -30,9 +44,17 @@ exports.WebServer = class { setReferenceObjects(objects) { this.root = { client: { - guilds: ({count}) => { + guilds: (args) => { let dcGuilds = objects.client.guilds.values(); - return Array.from(dcGuilds).map((x) => new Guild(x)).slice(0, count); + if (args.id) { + return [Array.from(dcGuilds) + .map((x) => new Guild(x, objects.getGuildHandler(x))) + .find(x => (x.id === args.id))]; + } else { + return Array.from(dcGuilds) + .map((x) => new Guild(x, objects.getGuildHandler(x))) + .slice(args.offset, args.offset + args.first); + } }, user: () => { return new User(objects.client.user); @@ -45,54 +67,180 @@ exports.WebServer = class { }, uptime: () => { return objects.client.uptime; - } + }, + }, + prefix: objects.prefix, + presences: objects.presences, + config: JSON.stringify(config), + 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.id) { + resolve([logEntries.find(x => (x.id === args.id))]); + } else if (args.first) { + resolve(logEntries.slice(args.offset, args.offset + args.first)); + } else { + resolve(logEntries.slice(logEntries.length - args.last)); + } + }) + }) } } } }; +function generateID(valArr) { + let b64 = Buffer.from(valArr.map(x => { + if (x) + return x.toString(); + else + return 'null'; + }).join('_')).toString('base64'); + return md5(b64); +} + class Guild { - constructor(discordGuild) { - this.id = discordGuild.id; + 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.members = Array.from(discordGuild.members.values()) + this.prMembers = Array.from(discordGuild.members.values()) .map((x) => new GuildMember(x)); - this.roles = Array.from(discordGuild.roles.values()) + this.prRoles = Array.from(discordGuild.roles.values()) .map((x) => new Role(x)); + guildHandler = guildHandler || {}; + this.ready = guildHandler.ready; + this.prSaved = null; + this.guildHandler = guildHandler; + } + + querySaved() { + return new Promise((resolve) => { + if (this.guildHandler.db) { + let saved = []; + this.guildHandler.db.all('SELECT * FROM playlists', (err, rows) => { + if (err) { + logger.error(err.message); + resolve(null) + } else { + for (let row of rows) { + saved.push({ + id: generateID(['Guild', 'ROW', row.id, row.name]), + name: row.name, + url: row.url + }); + } + resolve(saved); + } + }) + } else { + resolve(null); + } + }); + } + + saved(args) { + return new Promise((resolve) => { + this.querySaved().then((result) => { + if (result) { + if (args.id) { + resolve([result.find(x => (x.id === args.id))]); + } else if (args.name) { + resolve([result.find(x => (x.name === args.name))]); + } else { + resolve(result.slice(args.offset, args.offset + args.first)); + } + } else { + resolve(null); + } + }) + + }) + } + + 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); + } } } class Role { constructor(discordRole) { - this.id = discordRole.id; + this.id = generateID(['Role', discordRole.id]); + this.discordId = discordRole.id; this.name = discordRole.name; this.color = discordRole.hexColor; - this.members = Array.from(discordRole.members.values) + 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); + } + } } class GuildMember { constructor(discordGuildMember) { - this.id = discordGuildMember.id; + this.id = generateID(['GuildMember', discordGuildMember.id]); + this.discordId = discordGuildMember.id; this.user = new User(discordGuildMember.user); this.nickname = discordGuildMember.nickname; - this.roles = Array.from(discordGuildMember.roles.values()) + 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); + } + } } class User { constructor(discordUser) { - this.id = discordUser.id; + 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; } +} + +class LogEntry { + constructor(entry) { + this.id = generateID(['LogEntry', entry.level, entry.timestamp]); + this.message = entry.message; + this.timestamp = entry.timestamp; + this.level = entry.level; + } } \ No newline at end of file diff --git a/package.json b/package.json index 0ae3f96..1b156a1 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ }, "dependencies": { "args-parser": "1.1.0", + "compression": "^1.7.3", "discord.js": "11.4.2", "eslint-plugin-graphql": "^3.0.1", "express": "^4.16.4", @@ -15,6 +16,7 @@ "ffmpeg-binaries": "4.0.0", "get-youtube-title": "1.0.0", "graphql": "^14.1.1", + "js-md5": "^0.7.3", "opusscript": "0.0.6", "sqlite3": "4.0.6", "winston": "3.2.1", From c46f34fd5989ea6e44d9d82e50d39baaa814aa7b Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sat, 2 Feb 2019 18:13:53 +0100 Subject: [PATCH 03/15] Changes to webservice - added more query parameters - started working on webinterface - tokens for authentication --- README.md | 1 + bot.js | 79 ++++++---- lib/music.js | 8 +- lib/utils.js | 12 +- lib/webapi.js | 185 ++++++++++++++++++++++-- package.json | 5 + {graphql => web/graphql}/schema.graphql | 26 +++- web/http/index.html | 52 +++++++ web/http/sass/style.sass | 166 +++++++++++++++++++++ web/http/sass/vars.sass | 31 ++++ web/http/scripts/query.js | 148 +++++++++++++++++++ 11 files changed, 668 insertions(+), 45 deletions(-) rename {graphql => web/graphql}/schema.graphql (72%) create mode 100644 web/http/index.html create mode 100644 web/http/sass/style.sass create mode 100644 web/http/sass/vars.sass create mode 100644 web/http/scripts/query.js diff --git a/README.md b/README.md index 9014935..9ee1646 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ The arguments are optional because the token and youtube-api-key that the bot ne ``` If the keys are missing from the config file, the bot exits. This behaviour can be deactivated by setting the `-i` commandline flag. +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 c7be1c3..1ed5428 100644 --- a/bot.js +++ b/bot.js @@ -14,7 +14,7 @@ const Discord = require("discord.js"), let webapi = null; class Bot { - constructor() { + constructor(callback) { this.client = new Discord.Client(); this.mention = false; this.rotator = null; @@ -67,27 +67,13 @@ class Bot { this.loadPresences(); } }); + if (config.webservice && config.webservice.enabled) + this.initializeWebserver(); + callback(); } }); }); this.registerCallbacks(); - - if (config.webservice && config.webservice.enabled) { - logger.verbose('Importing webapi'); - webapi = require('./lib/webapi'); - webapi.setLogger(logger); - logger.verbose('Creating WebServer'); - this.webServer = new webapi.WebServer(config.webservice.port || 8080); - logger.debug('Setting Reference Objects to webserver'); - - this.webServer.setReferenceObjects({ - client: this.client, - presences: this.presences, - maind: this.maindb, - prefix: prefix, - getGuildHandler: (guild) => this.getGuildHandler(guild, prefix) - }); - } } /** @@ -109,6 +95,26 @@ class Bot { }) } + /** + * initializes the api webserver + */ + initializeWebserver() { + logger.verbose('Importing webapi'); + webapi = require('./lib/webapi'); + webapi.setLogger(logger); + logger.verbose('Creating WebServer'); + this.webServer = new webapi.WebServer(config.webservice.port || 8080); + logger.debug('Setting Reference Objects to webserver'); + + this.webServer.setReferenceObjects({ + client: this.client, + presences: this.presences, + maindb: this.maindb, + prefix: prefix, + getGuildHandler: (guild) => this.getGuildHandler(guild, prefix) + }); + } + /** * If a data/presences.txt exists, it is read and each line is put into the presences array. * Each line is also stored in the main.db database. After the file is completely read, it get's deleted. @@ -188,8 +194,11 @@ class Bot { logger.debug('Destroying client...'); this.client.destroy().finally(() => { - logger.debug(`Exiting Process...`); - process.exit(0); + logger.debug('Exiting server...') + this.webServer.stop().then(() => { + logger.debug(`Exiting Process...`); + process.exit(0); + }); }); }); }, [], "Shuts the bot down.", 'owner'); @@ -226,6 +235,25 @@ class Bot { cmd.createGlobalCommand(prefix + 'guilds', () => { return `Number of guilds: \`${this.client.guilds.size}\`` }, [], 'Returns the number of guilds the bot has joined', 'owner'); + + cmd.createGlobalCommand(prefix + 'tokengen', (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.generateToken(argv.username, argv.scope).then((token) => { + resolve(`Created entry + username: ${argv.username}, + scope: ${argv.scope}, + token: ${token} + `); + }).catch((err) => { + reject(err.message); + }); + } + }); + }, ['username', 'scope'], 'Generates a token for a username and returns it.', 'owner'); } /** @@ -293,8 +321,8 @@ class Bot { (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); } @@ -320,8 +348,9 @@ 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. - let discordBot = new Bot(); - discordBot.start().catch((err) => { - logger.error(err.message); + let discordBot = new Bot(() => { + discordBot.start().catch((err) => { + logger.error(err.message); + }); }); } \ No newline at end of file diff --git a/lib/music.js b/lib/music.js index 8ac2403..fa8c80e 100644 --- a/lib/music.js +++ b/lib/music.js @@ -120,12 +120,12 @@ exports.DJ = class { 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)); + }).catch((err) => logger.verbose(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)); + }).catch((err) => logger.verbose(err.message)); } logger.debug(`Added ${items.length} songs to the queue`); }); @@ -161,11 +161,11 @@ exports.DJ = class { if (playnext) { this.getVideoName(url).then((title) => { this.queue.unshift({'url': url, 'title': title}); - }).catch((err) => logger.error(err.message)); + }).catch((err) => logger.verbose(err.message)); } else { this.getVideoName(url).then((title) => { this.queue.push({'url': url, 'title': title}); - }).catch((err) => logger.error(err.message)); + }).catch((err) => logger.verbose(err.message)); } } } diff --git a/lib/utils.js b/lib/utils.js index b2c07e9..d1cff89 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -220,7 +220,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; } }; @@ -262,4 +263,13 @@ exports.ConfigVerifyer = class { exports.sql = { tableExistCreate: 'CREATE TABLE IF NOT EXISTS', pkIdSerial: 'id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL' +}; + +exports.logLevels = { + 'debug': 0, + 'verbose': 1, + 'info': 2, + 'warning': 3, + 'warn': 3, + 'error:': 4 }; \ No newline at end of file diff --git a/lib/webapi.js b/lib/webapi.js index 1e16e23..f0326bd 100644 --- a/lib/webapi.js +++ b/lib/webapi.js @@ -3,8 +3,11 @@ const express = require('express'), {buildSchema} = require('graphql'), compression = require('compression'), md5 = require('js-md5'), + cors = require('cors'), + fs = require('fs'), + compileSass = require('express-compile-sass'), config = require('../config.json'), - fs = require('fs'); + utils = require('../lib/utils'); let logger = require('winston'); @@ -13,17 +16,24 @@ exports.setLogger = function (newLogger) { }; exports.WebServer = class { - constructor(port, schema, root) { + constructor(port, schema, root, referenceObjects) { this.app = express(); + this.server = null; this.port = port; - this.schema = buildSchema(fs.readFileSync('./graphql/schema.graphql', 'utf-8')); + this.schema = buildSchema(fs.readFileSync('./web/graphql/schema.graphql', 'utf-8')); this.root = {}; + if (referenceObjects) + this.setReferenceObjects(referenceObjects); } /** * Starting the api webserver */ start() { + this.app.use(cors()); + if (config.webservice.useBearers) { + this.app.use('/graphql', (req, res, next) => this.authenticateUser(req, res, next)); + } this.app.use(compression({ filter: (req, res) => { if (req.headers['x-no-compression']) { @@ -38,10 +48,90 @@ exports.WebServer = class { rootValue: this.root, graphiql: config.webservice.graphiql || false })); - this.app.listen(this.port); + this.app.use(compileSass({ + root: './web/http/' + })); + this.app.use('/', express.static('./web/http/')); + this.server = this.app.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 + * @returns {Promise} + */ + generateToken(username, scope) { + return new Promise((resolve, reject) => { + let token = generateID(['TOKEN', username, (new Date()).getMilliseconds()]); + this.maindb.run('INSERT INTO users (username, token, scope) VALUES (?, ?, ?)', + [username, token, scope], (err) => { + if(err) { + logger.warn(err.message); + reject(err); + } else { + resolve(token); + } + }) + }); } + authenticateUser(req, res, next) { + if (req.headers.authorization + && req.headers.authorization.split(' ')[0] === 'Bearer') { + let bearer = req.headers.authorization.split(' ')[1]; + this.maindb.get('SELECT * FROM users WHERE token = ?', [bearer], (err, user) => { + if (err) { + logger.warn(err.message); + logger.debug('Unauthorized access'); + res.status(401); + res.end('Unauthorized Access'); + } else { + if (!user) { + res.status(401); + res.end('Unauthorized Access'); + } else { + req.user = user; + next(); + } + } + }); + } else { + logger.debug('Unauthorized access'); + res.status(401); + res.end('Unauthorized Access'); + } + } + + /** + * Setting all objects that web can query + * @param objects + */ setReferenceObjects(objects) { + this.maindb = objects.maindb; + this.maindb.run(`${utils.sql.tableExistCreate} users ( + ${utils.sql.pkIdSerial}, + username VARCHAR(32) UNIQUE NOT NULL, + token VARCHAR(255) UNIQUE NOT NULL, + scope INTEGER NOT NULL DEFAULT 0 + )`, (err) => { + if (err) { + logger.error(err.message); + } + }); this.root = { client: { guilds: (args) => { @@ -56,6 +146,9 @@ exports.WebServer = class { .slice(args.offset, args.offset + args.first); } }, + guildCount: () => { + return Array.from(objects.client.guilds.values()).length; + }, user: () => { return new User(objects.client.user); }, @@ -71,7 +164,11 @@ exports.WebServer = class { }, prefix: objects.prefix, presences: objects.presences, - config: JSON.stringify(config), + config: () => { + let newConfig = JSON.parse(JSON.stringify(config)); + delete newConfig.api; + return JSON.stringify(newConfig, null, ' ') + }, logs: (args) => { return new Promise((resolve) => { let logEntries = []; @@ -82,13 +179,19 @@ exports.WebServer = class { 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) { - resolve([logEntries.find(x => (x.id === args.id))]); - } else if (args.first) { - resolve(logEntries.slice(args.offset, args.offset + args.first)); + logEntries = [logEntries.find(x => (x.id === args.id))]; + } + if (args.first) { + logEntries = logEntries.slice(args.offset, args.offset + args.first); } else { - resolve(logEntries.slice(logEntries.length - args.last)); + logEntries = logEntries.slice(logEntries.length - args.last); } + resolve(logEntries); }) }) } @@ -96,6 +199,11 @@ exports.WebServer = class { } }; +/** + * generating an unique id + * @param valArr + * @returns {*} + */ function generateID(valArr) { let b64 = Buffer.from(valArr.map(x => { if (x) @@ -106,6 +214,55 @@ function generateID(valArr) { return md5(b64); } +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 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; + } +} + class Guild { constructor(discordGuild, guildHandler) { this.id = generateID(['Guild', discordGuild.id]); @@ -122,6 +279,7 @@ class Guild { this.ready = guildHandler.ready; this.prSaved = null; this.guildHandler = guildHandler; + this.dj = this.guildHandler.dj? new DJ(this.guildHandler.dj) : null; } querySaved() { @@ -135,9 +293,10 @@ class Guild { } else { for (let row of rows) { saved.push({ - id: generateID(['Guild', 'ROW', row.id, row.name]), + id: generateID(['Media', row.url]), name: row.name, - url: row.url + url: row.url, + thumbnail: utils.YouTube.getVideoThumbnailUrlFromUrl(row.url) }); } resolve(saved); @@ -233,6 +392,10 @@ class User { this.bot = discordUser.bot; this.tag = discordUser.tag; this.tag = discordUser.tag; + this.presence = { + game: discordUser.presence.game.name, + status: discordUser.presence.status + } } } diff --git a/package.json b/package.json index 1b156a1..42c5d74 100644 --- a/package.json +++ b/package.json @@ -9,15 +9,20 @@ "dependencies": { "args-parser": "1.1.0", "compression": "^1.7.3", + "cors": "^2.8.5", "discord.js": "11.4.2", "eslint-plugin-graphql": "^3.0.1", "express": "^4.16.4", + "express-compile-sass": "^4.0.0", "express-graphql": "^0.7.1", "ffmpeg-binaries": "4.0.0", "get-youtube-title": "1.0.0", "graphql": "^14.1.1", "js-md5": "^0.7.3", + "node-sass": "^4.11.0", "opusscript": "0.0.6", + "passport": "^0.4.0", + "passport-http-bearer": "^1.0.1", "sqlite3": "4.0.6", "winston": "3.2.1", "winston-daily-rotate-file": "3.6.0", diff --git a/graphql/schema.graphql b/web/graphql/schema.graphql similarity index 72% rename from graphql/schema.graphql rename to web/graphql/schema.graphql index 24eca7d..383948f 100644 --- a/graphql/schema.graphql +++ b/web/graphql/schema.graphql @@ -1,3 +1,7 @@ +type Presence { + game: String + status: String +} type User { id: ID! discordId: String @@ -5,6 +9,7 @@ type User { avatar: String bot: Boolean tag: String! + presence: Presence } type Role { id: ID! @@ -21,29 +26,42 @@ type GuildMember { roles(first: Int = 10, offset: Int = 0, id: String): [Role] highestRole: Role } +type DJ { + queue(first: Int = 10, offset: Int = 0, id: String): [MediaEntry] + playing: Boolean + volume: Float + repeat: Boolean + currentSong: MediaEntry + quality: String + voiceChannel: String +} 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] - memberCount: Int icon: String ready: Boolean - saved(first: Int = 10, offset: Int = 0, id: String, name: String): [SavedEntry!] + 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 user: User ping: Float status: Int uptime: Int } -type SavedEntry { +type MediaEntry { id: ID! url: String! name: String! + thumbnail: String } type LogEntry { id: ID! @@ -56,5 +74,5 @@ type Query { presences: [String]! config: String prefix: String - logs(first: Int, offset: Int = 0, id: String, last: Int = 10): [LogEntry] + logs(first: Int, offset: Int = 0, id: String, last: Int = 10, level: String): [LogEntry] } \ No newline at end of file diff --git a/web/http/index.html b/web/http/index.html new file mode 100644 index 0000000..ecf734d --- /dev/null +++ b/web/http/index.html @@ -0,0 +1,52 @@ + + + + + Dashboard + + + + + + +
+
+

+			

Logs

+
+
+
+
+
+ Avatar +
+

+

+
+

Status

+
+ Ping: + +
+
+ Uptime: + +
+
+ Socket Status: + +
+
+ Guild Count: + +
+
+
+

right

+
+
+ + + \ No newline at end of file diff --git a/web/http/sass/style.sass b/web/http/sass/style.sass new file mode 100644 index 0000000..ae33fe8 --- /dev/null +++ b/web/http/sass/style.sass @@ -0,0 +1,166 @@ +@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 + +.column + display: table-column + padding: 20px + align-content: center + margin: 0 auto + text-align: center + max-height: 100vh + +.cell + //display: table-cell + 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: block + width: 100% + text-align: left + overflow: auto + display: inline-block + position: relative + max-height: 90vh + +.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 + +#content + display: flex + height: 100% + width: 100% + background-color: $cBackground + +#column-left, #column-middle, #column-right + width: 33% + height: 100% + +#column-middle + background: $cBackgroundVariant + border-radius: 20px + +#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 + +#bot-config + background: darken($cBackground, 3) + word-wrap: break-word + display: none \ No newline at end of file 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/query.js b/web/http/scripts/query.js new file mode 100644 index 0000000..1f8078b --- /dev/null +++ b/web/http/scripts/query.js @@ -0,0 +1,148 @@ +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.round(dur / 1000); + retObj.seconds = dur % 60; + dur = Math.round(dur / 60); + retObj.minutes = dur % 60; + dur = Math.round(dur / 60); + retObj.hours = dur % 24; + dur = Math.round(dur / 24); + retObj.days = dur; + return retObj; +} + +function postQuery(query) { + return new Promise((resolve) => { + $.post({ + url: "/graphql", + headers: { + Authorization: `Bearer ${sessionStorage.apiToken}` + }, + data: JSON.stringify({ + query: query + }), + contentType: "application/json" + }).done((res) => resolve(res)); + }) +} + +function queryStatic() { + let query = `{ + client { + user { + tag + avatar + } + } + config + }`; + 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; + document.querySelector('#bot-config').innerText = d.config; + }) +} + +function queryStatus() { + let query = `{ + client { + ping + status + uptime + guildCount + 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('#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() { + if (!sessionStorage.apiToken || sessionStorage.apiToken.length < 0) { + sessionStorage.apiToken = prompt('Please provide an api token: '); + } + queryStatic(); + setInterval(queryStatic, 360000); + queryStatus(); + setInterval(queryStatus, 2000); + queryLogs(50); + setInterval(queryLogs, 5000); +} \ No newline at end of file From bfe29e0ef49164f68402ac68cebc0d0c179d8e7e Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sat, 2 Feb 2019 23:40:41 +0100 Subject: [PATCH 04/15] Dashboard modifications - finished most functionalities of the dashboard - fixed some bugs --- bot.js | 2 +- lib/webapi.js | 41 ++++++----- web/http/index.html | 42 ++++++++++- web/http/sass/style.sass | 68 +++++++++++++++++- web/http/scripts/query.js | 143 ++++++++++++++++++++++++++++++++++++-- 5 files changed, 270 insertions(+), 26 deletions(-) diff --git a/bot.js b/bot.js index 1ed5428..9997b5d 100644 --- a/bot.js +++ b/bot.js @@ -327,7 +327,7 @@ class Bot { (this.mention) ? msg.reply(answer) : msg.channel.send(answer); } } else { - logger.warn(`Empty answer won't be send.`); + logger.verbose(`Empty answer won't be send.`); } } diff --git a/lib/webapi.js b/lib/webapi.js index f0326bd..fdcfd0e 100644 --- a/lib/webapi.js +++ b/lib/webapi.js @@ -60,12 +60,12 @@ exports.WebServer = class { * @returns {Promise} */ stop() { - return new Promise((resolve) => { - if (this.server) - this.server.close(() => resolve()); - else - resolve(); - }) + return new Promise((resolve) => { + if (this.server) + this.server.close(() => resolve()); + else + resolve(); + }) } /** @@ -79,13 +79,13 @@ exports.WebServer = class { let token = generateID(['TOKEN', username, (new Date()).getMilliseconds()]); this.maindb.run('INSERT INTO users (username, token, scope) VALUES (?, ?, ?)', [username, token, scope], (err) => { - if(err) { - logger.warn(err.message); - reject(err); - } else { - resolve(token); - } - }) + if (err) { + logger.warn(err.message); + reject(err); + } else { + resolve(token); + } + }) }); } @@ -141,9 +141,14 @@ exports.WebServer = class { .map((x) => new Guild(x, objects.getGuildHandler(x))) .find(x => (x.id === args.id))]; } else { - return Array.from(dcGuilds) - .map((x) => new Guild(x, objects.getGuildHandler(x))) - .slice(args.offset, args.offset + args.first); + try { + return Array.from(dcGuilds) + .slice(args.offset, args.offset + args.first) + .map((x) => new Guild(x, objects.getGuildHandler(x))); + } catch (err) { + logger.error(err.stack); + return null; + } } }, guildCount: () => { @@ -279,7 +284,7 @@ class Guild { this.ready = guildHandler.ready; this.prSaved = null; this.guildHandler = guildHandler; - this.dj = this.guildHandler.dj? new DJ(this.guildHandler.dj) : null; + this.dj = this.guildHandler.dj ? new DJ(this.guildHandler.dj) : null; } querySaved() { @@ -393,7 +398,7 @@ class User { this.tag = discordUser.tag; this.tag = discordUser.tag; this.presence = { - game: discordUser.presence.game.name, + game: discordUser.presence.game? discordUser.presence.game.name : null, status: discordUser.presence.status } } diff --git a/web/http/index.html b/web/http/index.html index ecf734d..1b44a15 100644 --- a/web/http/index.html +++ b/web/http/index.html @@ -42,7 +42,47 @@
-

right

+ +
- \ No newline at end of file + diff --git a/web/http/sass/style.sass b/web/http/sass/style.sass index e48e325..20c05e5 100644 --- a/web/http/sass/style.sass +++ b/web/http/sass/style.sass @@ -120,6 +120,8 @@ div.cell > * padding: 2px margin: 5px border-radius: 5px + text-decoration: none + color: $cPrimary .songEntry > * display: table-column @@ -130,6 +132,7 @@ div.cell > * max-width: 20% height: auto width: auto + border-radius: 2px .songEntry a width: 80% @@ -184,11 +187,6 @@ div.cell > * #status-indicator[status=offline] background-color: $cOffline -#bot-config - background: darken($cBackground, 3) - word-wrap: break-word - display: none - #guild-select background: $cBackgroundVariant color: $cPrimary @@ -211,14 +209,19 @@ div.cell > * border-radius: 25% #dj-songinfo + display: block + background-color: $cBackgroundVariant + border-radius: 20px + +#songinfo-container + display: list-item text-decoration: none color: $cPrimary padding: 10px + width: calc(100% - 20px) -#songinfo-container - background-color: $cBackgroundVariant - border-radius: 20px - padding: 10px +dj-queue-container + display: list-item #dj-songname font-weight: bold diff --git a/web/http/scripts/query.js b/web/http/scripts/query.js index 92676d1..c78b9fd 100644 --- a/web/http/scripts/query.js +++ b/web/http/scripts/query.js @@ -1,5 +1,4 @@ let latestLogs = []; -let latestSongs = []; let status = { 0: 'ready', @@ -48,13 +47,11 @@ function queryStatic() { avatar } } - config }`; 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; - document.querySelector('#bot-config').innerText = d.config; }) } @@ -121,13 +118,16 @@ function queryGuildStatus(guildId) { guilds(id: "${guildId}") { dj { playing + connected repeat voiceChannel + songStartTime currentSong { name url - thumbnail + thumbnail } + queueCount queue(first: 5) { id name @@ -141,37 +141,47 @@ function queryGuildStatus(guildId) { }`; postQuery(query).then((res) => { let guild = res.data.client.guilds[0]; - document.querySelector('#guild-djStatus').innerText = guild.dj.playing? 'playing' : 'idle'; document.querySelector('#dj-repeat').innerText = guild.dj.repeat? 'on': 'off'; - if (guild.dj.playing) { + document.querySelector('#guild-djStatus').innerText = guild.dj.connected? 'connected' : 'disconnected'; + if (guild.dj.connected) { $('#dj-songinfo').show(); + document.querySelector('#guild-djStatus').innerText = guild.dj.playing? 'playing' : 'connected'; document.querySelector('#dj-voiceChannel').innerText = guild.dj.voiceChannel; - document.querySelector('#dj-songinfo').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); - let songContainer = document.querySelector('#dj-songQueue'); - for (let song of guild.dj.queue) { - if ($(`.songEntry[song-id=${song.id}]`).length === 0) { - let songEntry = document.createElement('div'); + let songinfoContainer = $('#dj-songinfo'); + + if (guild.dj.playing) { + if (songinfoContainer.is(':hidden')) + songinfoContainer.show(); + 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); + imageEntry.setAttribute('src', song.thumbnail.replace('maxresdefault', 'mqdefault')); songEntry.appendChild(imageEntry); let nameEntry = document.createElement('a'); - nameEntry.setAttribute('href', song.url); 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(); } - let songEntries = $('.songEntry'); - if (songEntries.length > 5) { - document.querySelector('#dj-songQueue').firstElementChild.remove(); - } - let latestSongs = guild.dj.queue; } else { $('#dj-songinfo').hide(); + document.querySelector('#dj-voiceChannel').innerText = 'None'; } }); } @@ -201,7 +211,11 @@ function queryStatus() { .innerText = `${sd.days}d ${sd.hours}h ${sd.minutes}min ${sd.seconds}s`; document.querySelector('#client-guildCount').innerText = d.client.guildCount; - document.querySelector('#status-indicator').setAttribute('status', d.client.user.presence.status); + 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(() => { @@ -283,4 +297,8 @@ function startUpdating() { let guildId = ev.target.value; queryGuild(guildId); }); -} \ No newline at end of file + 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); +} From 1d3ce9356185ba296e64ad40f6519801cc6102bf Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sun, 3 Feb 2019 15:28:12 +0100 Subject: [PATCH 07/15] Modified Dashboard - added Song timer - added active VC count - improved layout - added querying for VC count to graphql --- bot.js | 5 ++-- lib/webapi.js | 14 +++++++++ web/graphql/schema.graphql | 1 + web/http/index.html | 60 +++++++++++++++++++++----------------- web/http/sass/style.sass | 42 +++++++++++++++++++------- web/http/scripts/query.js | 2 ++ 6 files changed, 85 insertions(+), 39 deletions(-) diff --git a/bot.js b/bot.js index a6ba469..f103f3d 100644 --- a/bot.js +++ b/bot.js @@ -112,7 +112,8 @@ class Bot { presences: this.presences, maindb: this.maindb, prefix: prefix, - getGuildHandler: (guild) => this.getGuildHandler(guild, prefix) + getGuildHandler: (guild) => this.getGuildHandler(guild, prefix), + guildHandlers: this.guildHandlers }); } @@ -356,4 +357,4 @@ if (typeof require !== 'undefined' && require.main === module) { logger.error(err.message); }); }); -} \ No newline at end of file +} diff --git a/lib/webapi.js b/lib/webapi.js index 7c01981..287ca20 100644 --- a/lib/webapi.js +++ b/lib/webapi.js @@ -166,6 +166,20 @@ exports.WebServer = class { 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, diff --git a/web/graphql/schema.graphql b/web/graphql/schema.graphql index 0083f16..957e27d 100644 --- a/web/graphql/schema.graphql +++ b/web/graphql/schema.graphql @@ -55,6 +55,7 @@ type Guild { type Client { guilds(first: Int = 10, offset: Int = 0, id: String): [Guild] guildCount: Int + voiceConnectionCount: Int user: User ping: Float status: Int diff --git a/web/http/index.html b/web/http/index.html index fb27838..3bfeecd 100644 --- a/web/http/index.html +++ b/web/http/index.html @@ -39,41 +39,47 @@ Guild Count: +
+ Active Voice Connections: + +