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