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