Changes to webservice

- added more query parameters
- started working on webinterface
- tokens for authentication
pull/33/head
Trivernis 6 years ago
parent 72794a2f63
commit c46f34fd59

@ -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
---

@ -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,10 +194,13 @@ class Bot {
logger.debug('Destroying client...');
this.client.destroy().finally(() => {
logger.debug('Exiting server...')
this.webServer.stop().then(() => {
logger.debug(`Exiting Process...`);
process.exit(0);
});
});
});
}, [], "Shuts the bot down.", 'owner');
// forces a presence rotation
@ -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();
let discordBot = new Bot(() => {
discordBot.start().catch((err) => {
logger.error(err.message);
});
});
}

@ -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));
}
}
}

@ -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;
}
};
@ -263,3 +264,12 @@ 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
};

@ -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<any>}
*/
stop() {
return new Promise((resolve) => {
if (this.server)
this.server.close(() => resolve());
else
resolve();
})
}
/**
* Generates a token for a given username
* @param username
* @param scope
* @returns {Promise<any>}
*/
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
}
}
}

@ -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",

@ -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]
}

@ -0,0 +1,52 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Dashboard</title>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<link type="text/css" rel="stylesheet" href="sass/style.sass"/>
<script type="text/javascript" src="scripts/query.js"></script>
<script type="text/javascript" src="http://momentjs.com/downloads/moment.min.js"></script>
</head>
<body>
<div id="content">
<div class="column" id="column-left">
<pre class="cell text-left" id="bot-config"></pre>
<h2 class="cell">Logs</h2>
<div id="log-container" class="listContainer"></div>
</div>
<div class="column" id="column-middle">
<div id="avatar-container">
<div id="status-indicator"></div>
<img class="cell" id="user-avatar" src="" alt="Avatar"/>
</div>
<h3 class="cell" id="user-tag"></h3>
<h4 class="cell" id="user-game"></h4>
<div class="space"></div>
<h2 class="cell">Status</h2>
<div class="cell">
<span class="label text-right">Ping: </span>
<span class="text-left" id="client-ping"></span>
</div>
<div class="cell">
<span class="label text-right">Uptime: </span>
<span class="text-left" id="client-uptime"></span>
</div>
<div class="cell">
<span class="label text-right">Socket Status: </span>
<span class="text-left" id="client-status"></span>
</div>
<div class="cell">
<span class="label text-right">Guild Count: </span>
<span class="text-left" id="client-guildCount"></span>
</div>
</div>
<div class="column" id="column-right">
<p>right</p>
</div>
</div>
<script>
startUpdating();
</script>
</body>
</html>

@ -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

@ -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

@ -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);
}
Loading…
Cancel
Save