Generic Database Connection

- you can now select one type of database either sqlite or postgres in the config.json (see README)
- fixed bugs in generic SQL Statements
- added generic database class
- changed all sql scripts to generic type
- added settings table to guild to manage settings of the guild
- added extended database class for the guild with predefined sql statements
- added music command volume to change the volume (role dj)
- added music command quality to change the musics quality (role owner)
feature/api-rewrite
Trivernis 5 years ago
parent 1b08edd278
commit ec5ed87c49

@ -18,12 +18,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- renamed libfolders to lowercase and removed the lib suffix - renamed libfolders to lowercase and removed the lib suffix
- moved commands outside of `lib` - moved commands outside of `lib`
- switched from opusscript to node-opus for voice - switched from opusscript to node-opus for voice
- all hard coded sql statements to generic sql generation
### Added ### Added
- state lib with `EventRouter` and `EventGroup` and `Event` classes - state lib with `EventRouter` and `EventGroup` and `Event` classes
- Subclasses of EventRouter for client events groupes `Client`, `Channel`, `Message` and `Guild` - Subclasses of EventRouter for client events groupes `Client`, `Channel`, `Message` and `Guild`
- Utility classes for generic SQL Statements - Utility classes for generic SQL Statements
- logging of unrejected promises - logging of unrejected promises
- database class for database abstraction (lib/database)
- config entry for `database` with supported values `postgresql` or `sqlite`
- config entry for `databaseConnection` for postgresql (`user`, `host`, `password`, `database`, `port`)
- table `settings` to each guild to store guild specific settings
## [0.11.0-beta] - 2019-03-03 ## [0.11.0-beta] - 2019-03-03
### Changed ### Changed

@ -48,6 +48,14 @@ The arguments are optional because the token and youtube-api-key that the bot ne
"commandSettings": { "commandSettings": {
"maxSequenceParallel": 5, // the maximum number of commands executed in parallel "maxSequenceParallel": 5, // the maximum number of commands executed in parallel
"maxSequenceSerial": 10 // the maximum number of commands executed in serial "maxSequenceSerial": 10 // the maximum number of commands executed in serial
},
"database": "postgres or sqlite", // choose one
"databaseConnection": {
"user": "USERNAME",
"host": "HOSTNAME OR IP",
"password": "DATABASE USERPASSWORD",
"database": "BOT DATABASE NAME", // the database needs to exist
"port": 5432 // the port of the database server
} }
} }
``` ```

@ -2,11 +2,11 @@ const Discord = require("discord.js"),
fs = require('fs-extra'), fs = require('fs-extra'),
logging = require('./lib/utils/logging'), logging = require('./lib/utils/logging'),
msgLib = require('./lib/message'), msgLib = require('./lib/message'),
guilding = require('./lib/guilding'), guilding = require('./lib/guilds'),
utils = require('./lib/utils'), utils = require('./lib/utils'),
config = require('./config.json'), config = require('./config.json'),
args = require('args-parser')(process.argv), args = require('args-parser')(process.argv),
sqliteAsync = require('./lib/utils/sqliteAsync'), dblib = require('./lib/database'),
authToken = args.token || config.api.botToken, authToken = args.token || config.api.botToken,
prefix = args.prefix || config.prefix || '~', prefix = args.prefix || config.prefix || '~',
gamepresence = args.game || config.presence; gamepresence = args.game || config.presence;
@ -112,13 +112,14 @@ class Bot {
this.logger.debug('Checking for ./data/ existence'); this.logger.debug('Checking for ./data/ existence');
await fs.ensureDir('./data'); await fs.ensureDir('./data');
this.logger.verbose('Connecting to main database'); this.logger.verbose('Connecting to main database');
this.maindb = new sqliteAsync.Database('./data/main.db'); this.maindb = new dblib.Database('main');
await this.maindb.init(); await this.maindb.initDatabase();
let sql = this.maindb.sql;
await this.maindb.run(`${utils.sql.tableExistCreate} presences ( await this.maindb.run(sql.createTableIfNotExists('presences', [
${utils.sql.pkIdSerial}, sql.templates.idcolumn,
text VARCHAR(255) UNIQUE NOT NULL new dblib.Column('text', sql.types.getVarchar(255),
)`); [sql.constraints.unique, sql.constraints.notNull])
]));
this.logger.debug('Loading Presences...'); this.logger.debug('Loading Presences...');
await this.loadPresences(); await this.loadPresences();
} }
@ -145,33 +146,37 @@ class Bot {
/** /**
* If a data/presences.txt exists, it is read and each line is put into the presences array. * 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. * Each line is also stored in the dbot-main.db database. After the file is completely read, it get's deleted.
* Then the data is read from the database and if the presence doesn't exist in the presences array, it get's * Then the data is read from the database and if the presence doesn't exist in the presences array, it get's
* pushed in there. If the presences.txt file does not exist, the data is just read from the database. In the end * pushed in there. If the presences.txt file does not exist, the data is just read from the database. In the end
* a rotator is created that rotates the presence every configured duration. * a rotator is created that rotates the presence every configured duration.
*/ */
async loadPresences() { async loadPresences() {
let sql = this.maindb.sql;
if (await fs.pathExists('./data/presences.txt')) { if (await fs.pathExists('./data/presences.txt')) {
let lineReader = require('readline').createInterface({ let lineReader = require('readline').createInterface({
input: require('fs').createReadStream('./data/presences.txt') input: require('fs').createReadStream('./data/presences.txt')
}); });
lineReader.on('line', (line) => { this.maindb.begin();
this.maindb.run('INSERT INTO presences (text) VALUES (?)', [line], (err) => { lineReader.on('line', async (line) => {
if (err) try {
this.logger.warn(err.message); await this.maindb.query(sql.insert('presences', {text: sql.parameter(1)}), [line]);
this.presences.push(line);
}); } catch (err) {
this.presences.push(line); this.logger.warn(err.message);
this.logger.debug(err.stack);
}
}); });
await this.maindb.commit();
this.rotator = this.client.setInterval(() => this.rotatePresence(), this.rotator = this.client.setInterval(() => this.rotatePresence(),
config.presence_duration || 360000); config.presence_duration || 360000);
await fs.unlink('./data/presences.txt'); await fs.unlink('./data/presences.txt');
let rows = await this.maindb.all('SELECT text FROM presences'); let rows = await this.maindb.all(sql.select('presences', false, ['text']));
for (let row of rows) for (let row of rows)
if (!(row[0] in this.presences)) if (!(row[0] in this.presences))
this.presences.push(row.text); this.presences.push(row.text);
} else { } else {
let rows = await this.maindb.all('SELECT text FROM presences'); let rows = await this.maindb.all(sql.select('presences', false, ['text']));
for (let row of rows) for (let row of rows)
this.presences.push(row.text); this.presences.push(row.text);
this.rotator = this.client.setInterval(() => this.rotatePresence(), this.rotator = this.client.setInterval(() => this.rotatePresence(),
@ -238,6 +243,7 @@ class Bot {
if (!this.guildHandlers[guild.id]) { if (!this.guildHandlers[guild.id]) {
let newGuildHandler = new guilding.GuildHandler(guild); let newGuildHandler = new guilding.GuildHandler(guild);
await newGuildHandler.initDatabase(); await newGuildHandler.initDatabase();
await newGuildHandler.applySettings();
this.guildHandlers[guild.id] = newGuildHandler; this.guildHandlers[guild.id] = newGuildHandler;
} }
return this.guildHandlers[guild.id]; return this.guildHandlers[guild.id];

@ -42,9 +42,9 @@ class MusicCommandModule extends cmdLib.CommandModule {
async _connectAndPlay(gh, vc, url, next) { async _connectAndPlay(gh, vc, url, next) {
if (!gh.musicPlayer.connected) { if (!gh.musicPlayer.connected) {
await gh.musicPlayer.connect(vc); await gh.musicPlayer.connect(vc);
await gh.musicPlayer.playYouTube(url, next); return await gh.musicPlayer.playYouTube(url, next);
} else { } else {
await gh.musicPlayer.playYouTube(url, next); return await gh.musicPlayer.playYouTube(url, next);
} }
} }
@ -68,17 +68,24 @@ class MusicCommandModule extends cmdLib.CommandModule {
return t.response.no_url; return t.response.no_url;
if (!utils.YouTube.isValidEntityUrl(url)) { if (!utils.YouTube.isValidEntityUrl(url)) {
url = s; url = s;
let row = await gh.db.get('SELECT url FROM playlists WHERE name = ?', [url]); let row = await gh.db.get(gh.db.sql.select('playlists', false, ['url'],
gh.db.sql.where('name', '=', gh.db.sql.parameter(1))), [url]);
if (!row) { if (!row) {
this._logger.debug('Got invalid url for play command.'); this._logger.debug('Got invalid url for play command.');
return t.response.url_invalid; return t.response.url_invalid;
} else { } else {
await this._connectAndPlay(gh, vc, row.url, n); let songcount = await this._connectAndPlay(gh, vc, row.url, n);
return t.response.success; if (songcount)
return `Added ${songcount} songs to the queue.`;
else
return t.response.success;
} }
} else { } else {
await this._connectAndPlay(gh, vc, url, n); let songcount = await this._connectAndPlay(gh, vc, url, n);
return t.response.success; if (songcount)
return `Added ${songcount} songs to the queue.`;
else
return t.response.success;
} }
} }
@ -249,14 +256,15 @@ class MusicCommandModule extends cmdLib.CommandModule {
new cmdLib.Answer(async (m, k, s) => { new cmdLib.Answer(async (m, k, s) => {
let gh = await this._getGuildHandler(m.guild); let gh = await this._getGuildHandler(m.guild);
let saveName = s.replace(k.url + ' ', ''); let saveName = s.replace(k.url + ' ', '');
let row = await gh.db let row = await gh.db.get(gh.db.sql.select('playlists', false,
.get('SELECT COUNT(*) count FROM playlists WHERE name = ?', [saveName]); [gh.db.sql.count('*')], gh.db.sql.where('name', '=', gh.db.sql.parameter(1))), [saveName]);
if (!row || row.count === 0) if (!row || Number(row.count) === 0)
await gh.db.run('INSERT INTO playlists (name, url) VALUES (?, ?)', await gh.db.run(gh.db.sql.insert('playlists',
[saveName, k.url]); {name: gh.db.sql.parameter(1), url: gh.db.sql.parameter(2)}), [saveName, k.url]);
else else
await gh.db.run('UPDATE playlists SET url = ? WHERE name = ?', await gh.db.run(gh.db.sql.update('playlists',
[k.url, saveName]); {url: gh.db.sql.parameter(1)},
gh.db.sql.where('name', '=', gh.db.sql.parameter(2))), [k.url, saveName]);
return `Saved song/playlist as ${saveName}`; return `Saved song/playlist as ${saveName}`;
}) })
); );
@ -268,7 +276,7 @@ class MusicCommandModule extends cmdLib.CommandModule {
if (!s) { if (!s) {
return this.template.delete_media.response.no_name; return this.template.delete_media.response.no_name;
} else { } else {
await gh.db.run('DELETE FROM playlists WHERE name = ?', [s]); await gh.db.run(gh.db.sql.delete('playlists', gh.db.sql.where('name', '=', gh.db.sql.parameter(1))), [s]);
return `Deleted ${s} from saved media`; return `Deleted ${s} from saved media`;
} }
}) })
@ -279,7 +287,7 @@ class MusicCommandModule extends cmdLib.CommandModule {
new cmdLib.Answer(async (m) => { new cmdLib.Answer(async (m) => {
let gh = await this._getGuildHandler(m.guild); let gh = await this._getGuildHandler(m.guild);
let response = ''; let response = '';
let rows = await gh.db.all('SELECT name, url FROM playlists'); let rows = await gh.db.all(gh.db.sql.select('playlists', false, ['name', 'url']));
for (let row of rows) for (let row of rows)
response += `[${row.name}](${row.url})\n`; response += `[${row.name}](${row.url})\n`;
@ -292,6 +300,36 @@ class MusicCommandModule extends cmdLib.CommandModule {
}) })
); );
let volume = new cmdLib.Command(
this.template.volume,
new cmdLib.Answer(async (m, k) => {
let volume = Number(k.volume);
if (volume && volume <= 100 && volume >= 0) {
let gh = await this._getGuildHandler(m.guild);
gh.musicPlayer.setVolume(Math.round(volume)/100);
await gh.db.setSetting('musicPlayerVolume', Math.round(volume)/100);
return `Set music volume to **${volume}**`;
} else {
return this.template.volume.response.invalid;
}
})
);
let quality = new cmdLib.Command(
this.template.quality,
new cmdLib.Answer(async (m, k) => {
let allowed = ['highest', 'lowest', 'highestaudio', 'lowestaudio'];
if (allowed.includes(k.quality)) {
let gh = await this._getGuildHandler(m.guild);
gh.musicPlayer.quality = k.quality;
await gh.db.setSetting('musicPlayerQuality', k.quality);
return `Set music quality to **${k.quality}**`;
} else {
return this.template.quality.response.invalid;
}
})
);
// register commands // register commands
commandHandler commandHandler
.registerCommand(play) .registerCommand(play)
@ -308,7 +346,9 @@ class MusicCommandModule extends cmdLib.CommandModule {
.registerCommand(toggleRepeat) .registerCommand(toggleRepeat)
.registerCommand(saveMedia) .registerCommand(saveMedia)
.registerCommand(deleteMedia) .registerCommand(deleteMedia)
.registerCommand(savedMedia); .registerCommand(savedMedia)
.registerCommand(volume)
.registerCommand(quality);
} }
} }

@ -168,3 +168,28 @@ saved_media:
response: response:
no_saved: > no_saved: >
There are no saved YouTube URLs :( There are no saved YouTube URLs :(
volume:
<<: *METADATA
name: volume
permission: dj
args:
- volume
description: >
Sets the volume of the Music Player.
response:
invalid: >
The value you entered is an invalid volume.
quality:
<<: *METADATA
name: quality
permission: owner
args:
- quality
description: >
Sets the quality of the music of the Music Player.
The setting will be applied on the next song.
response:
invalid: >
You entered an invalid quality value.

@ -66,14 +66,15 @@ class ServerUtilityCommandModule extends cmdLib.CommandModule {
} else if (sequence.find(x => x.length > maxSqSer)) { } else if (sequence.find(x => x.length > maxSqSer)) {
return this.template.save_cmd.response.sequence_too_many_serial; return this.template.save_cmd.response.sequence_too_many_serial;
} else { } else {
let row = await gh.db let sql = gh.db.sql;
.get('SELECT COUNT(*) count FROM commands WHERE name = ?', [k.name]); let row = await gh.db.get(sql.select('commands', false, [sql.count('*')],
if (!row || row.count === 0) sql.where('name', '=', sql.parameter(1))), [k.name]);
await gh.db if (!row || Number(row.count) === 0)
.run('INSERT INTO commands (name, command) VALUES (?, ?)', [k.name, JSON.stringify(sequence)]); await gh.db.run(sql.insert('commands', {name: sql.parameter(1), command: sql.parameter(2)}),
[k.name, JSON.stringify(sequence)]);
else else
await await gh.db await gh.db.run(sql.update('commands', {command: sql.parameter(1)}, sql.where('name', '=', sql.parameter(2))),
.run('UPDATE commands SET command = ? WHERE name = ?', [JSON.stringify(sequence), k.name]); [JSON.stringify(sequence), k.name]);
} }
}) })
); );
@ -82,7 +83,7 @@ class ServerUtilityCommandModule extends cmdLib.CommandModule {
this.template.delete_cmd, this.template.delete_cmd,
new cmdLib.Answer(async (m, k) => { new cmdLib.Answer(async (m, k) => {
let gh = await this._getGuildHandler(m.guild); let gh = await this._getGuildHandler(m.guild);
await gh.db.run('DELETE FROM commands WHERE name = ?', [k.name]); await gh.db.run(gh.db.sql.delete('commands', gh.db.sql.where('name', '=', gh.db.sql.parameter(1)), ), [k.name]);
return `Deleted command ${k.name}`; return `Deleted command ${k.name}`;
}) })
); );
@ -93,7 +94,7 @@ class ServerUtilityCommandModule extends cmdLib.CommandModule {
let gh = await this._getGuildHandler(m.guild); let gh = await this._getGuildHandler(m.guild);
let response = new cmdLib.ExtendedRichEmbed('Saved Commands') let response = new cmdLib.ExtendedRichEmbed('Saved Commands')
.setFooter(`Execute a saved entry with ${this._config.prefix}execute [Entryname]`); .setFooter(`Execute a saved entry with ${this._config.prefix}execute [Entryname]`);
let rows = await gh.db.all('SELECT name, command FROM commands'); let rows = await gh.db.all(gh.db.sql.select('commands', ['name', 'command']));
if (rows.length === 0) if (rows.length === 0)
return this.template.saved_cmd.response.no_commands; return this.template.saved_cmd.response.no_commands;
else else
@ -107,8 +108,8 @@ class ServerUtilityCommandModule extends cmdLib.CommandModule {
this.template.execute, this.template.execute,
new cmdLib.Answer(async (m, k) => { new cmdLib.Answer(async (m, k) => {
let gh = await this._getGuildHandler(m.guild); let gh = await this._getGuildHandler(m.guild);
let row = await gh.db let row = await gh.db.get(gh.db.sql.select('commands',false, ['command'],
.get('SELECT command FROM commands WHERE name = ?', [k.name]); gh.db.sql.where('name', '=', gh.db.sql.parameter(1))), [k.name]);
if (row) if (row)
await this._messageHandler await this._messageHandler
.executeCommandSequence(JSON.parse(row.command), m); .executeCommandSequence(JSON.parse(row.command), m);

@ -24,12 +24,13 @@ class UtilityCommandModule extends cmdLib.CommandModule {
async register(commandHandler) { async register(commandHandler) {
await this._loadTemplate(); await this._loadTemplate();
let sql = this._bot.maindb.sql;
let addPresence = new cmdLib.Command( let addPresence = new cmdLib.Command(
this.template.add_presence, this.template.add_presence,
new cmdLib.Answer(async (m, k, s) => { new cmdLib.Answer(async (m, k, s) => {
this._bot.presences.push(s); this._bot.presences.push(s);
await this._bot.maindb.run('INSERT INTO presences (text) VALUES (?)', [s]); await this._bot.maindb.run(sql.insert('presences', {text: sql.parameter(1)}), [s]);
return `Added Presence \`${s}\``; return `Added Presence \`${s}\``;
}) })
); );

@ -0,0 +1,177 @@
const genericSql = require('../utils/genericSql'),
logging = require('../utils/logging'),
config = require('../../config.json');
class Database {
/**
* Creates a new database.
* @param name {String} - the name of the database.
*/
constructor(name) {
this.name = name;
this._logger = new logging.Logger(`Database@${name}`);
this._dbType = config.database? config.database : 'sqlite';
if (this._dbType === 'sqlite')
this.database = new (require('../utils/sqliteAsync')).Database(`./data/${this.name}.db`);
else if (this._dbType === 'postgresql')
this.database = new (require('pg')).Pool({
user: config.databaseConnection.user,
host: config.databaseConnection.host,
database: config.databaseConnection.database,
password: config.databaseConnection.password,
port: config.databaseConnection.port
});
this.sql = new genericSql.GenericSql(this._dbType);
}
/**
* Initializes the database.
* @returns {Promise<void>}
*/
async initDatabase() {
if (this._dbType === 'sqlite') {
await this.database.init();
} else if (this._dbType === 'postgresql') {
await this.database.connect();
await this.begin();
await this.database.query(`CREATE SCHEMA IF NOT EXISTS ${this.name.replace(/\W/g, '')}`);
await this.database.query(`SET search_path TO ${this.name.replace(/\W/g, '')}`);
await this.commit();
}
this._logger.verbose(`Connected to ${this._dbType} database ${this.name}`);
}
/**
* Run a sql statement with seperate values and no return.
* Autocommit.
* @param sql {String}
* @param [values] {Array<String|Number>}
* @returns {Promise<*>}
*/
async run(sql, values) {
this._logger.debug(`Running SQL "${sql}" with values ${values}`);
if (this._dbType === 'sqlite')
await this.database.run(sql, values);
else if (this._dbType === 'postgresql')
try {
await this.begin();
await this.database.query(sql, values);
await this.commit();
} catch (err) {
this._logger.error(err.message);
this._logger.verbose(err.stack);
await this.rollback();
}
}
/**
* Begin. Part of Postgresqls BEGIN / COMMIT / ROLLBACK
* @returns {Promise<void>}
*/
async begin() {
if (this._dbType === 'postgresql') {
await this.database.query('BEGIN');
await this.database.query(`SET search_path TO ${this.name.replace(/\W/g, '')};`);
}
}
/**
* Add a query to the current changes. No autocommit (except on sqlite).
* @param sql
* @param values
* @returns {Promise<void>}
*/
async query(sql, values) {
if (this._dbType === 'sqlite') {
await this.run(sql, values);
} else if (this._dbType === 'postgresql') {
await this.database.query(sql, values);
this._logger.debug(`Running SQL "${sql}" with values ${values}`);
}
}
/**
* Commit. Part of Postgresqls BEGIN / COMMIT / ROLLBACK.
* Writes data to the database, ROLLBACK on error. (has no effect on sqlite)
* @returns {Promise<void>}
*/
async commit() {
if (this._dbType === 'postgresql')
try {
await this.database.query('COMMIT');
} catch (err) {
await this.database.query('ROLLBACK');
this._logger.error(err.message);
this._logger.verbose(err.stack);
}
}
/**
* Rollback. Part of Postgresqls BEGIN / COMMIT / ROLLBACK.
* Reverts changes done in the current commit. (has no effect on sqlite)
* @returns {Promise<void>}
*/
async rollback() {
if (this._dbType === 'postgresql')
this.database.query('ROLLBACK');
}
/**
* Run a sql statement with seperate values and first result row as return.
* @param sql {String} - the sql statement with escaped values ($1, $2... for postgres, ? for sqlite)
* @param [values] {Array<String|Number>}
* @returns {Promise<void>}
*/
async get(sql, values) {
this._logger.debug(`Running SQL "${sql}" with values ${values}`);
let result = null;
if (this._dbType === 'sqlite') {
result = await this.database.get(sql, values);
} else if (this._dbType === 'postgresql') {
await this.database.query(`SET search_path TO ${this.name.replace(/\W/g, '')};`);
result = (await this.database.query({
text: sql,
values: values
})).rows;
}
if (result instanceof Array && result.length > 0)
return result[0];
else
return result;
}
/**
* Run a sql statement with seperate values and all result rows as return.
* @param sql {String} - the sql statement with escaped values ($1, $2... for postgres, ? for sqlite)
* @param [values] {Array<String|Number>} - the seperate values
* @returns {Promise<void>}
*/
async all(sql, values) {
this._logger.debug(`Running SQL "${sql}" with values ${values}`);
if (this._dbType === 'sqlite') {
return await this.database.all(sql, values);
} else if (this._dbType === 'postgresql') {
await this.database.query(`SET search_path TO ${this.name.replace(/\W/g, '')};`);
return (await this.database.query({
text: sql,
values: values
})).rows;
}
}
/**
* Closes the connection to the database.
*/
close() {
if (this._dbType === 'sqlite')
this.database.close();
else if (this._dbType === 'postgresql')
this.database.release();
}
}
Object.assign(exports, {
Column: genericSql.Column,
Database: Database
});

@ -1,102 +0,0 @@
const music = require('./music'),
utils = require('./utils'),
config = require('../config.json'),
sqliteAsync = require('./utils/sqliteAsync'),
logging = require('./utils/logging'),
fs = require('fs-extra'),
dataDir = config.dataPath || './data';
/**
* The Guild Handler handles guild settings and data.
* @type {GuildHandler}
*/
class GuildHandler {
constructor(guild) {
this.guild = guild;
this._logger = new logging.Logger(`${this.constructor.name}@${this.guild}`);
this.musicPlayer = new music.MusicPlayer(null);
this._logger.silly('Initialized Guild Handler');
this._votes = {};
}
/**
* Initializes the database
* @returns {Promise<void>}
*/
async initDatabase() {
this._logger.silly('Initializing Database');
await fs.ensureDir(dataDir + '/gdb');
this.db = new sqliteAsync.Database(`${dataDir}/gdb/${this.guild}.db`);
await this.db.init();
this._logger.debug(`Connected to the database for ${this.guild}`);
this._logger.debug('Creating Databases');
await this._createTables();
}
/**
* Destroys the guild handler
*/
destroy() {
this._logger.debug('Ending musicPlayer');
this.musicPlayer.stop();
this._logger.debug('Ending Database');
this.db.close();
}
/**
* Creates all tables needed in the Database.
* These are at the moment:
* messages - logs all messages send on the server
* playlists - save playlists to play them later
*/
async _createTables() {
await this.db.run(`${utils.sql.tableExistCreate} messages (
${utils.sql.pkIdSerial},
creation_timestamp DATETIME NOT NULL,
author VARCHAR(128) NOT NULL,
author_name VARCHAR(128),
content TEXT NOT NULL
)`);
this._logger.silly('Created Table messages');
await this.db.run(`${utils.sql.tableExistCreate} playlists (
${utils.sql.pkIdSerial},
name VARCHAR(32) UNIQUE NOT NULL,
url VARCHAR(255) NOT NULL
)`);
this._logger.silly('Created Table playlists');
await this.db.run(`${utils.sql.tableExistCreate} commands (
${utils.sql.pkIdSerial},
name VARCHAR(32) UNIQUE NOT NULL,
command VARCHAR(255) NOT NULL
)`);
this._logger.silly('Created Table commands');
}
/**
* Sets the vote counter for a command up and adds the user.
* @param command {String}
* @param user {String}
*/
updateCommandVote(command, user) {
if (!this._votes[command])
this._votes[command] = {count: 0, users: []};
if (!this._votes[command].users.includes(user)) {
this._votes[command].count++;
this._votes[command].users.push(user);
}
return this._votes[command];
}
/**
* Resets the vote counter and voted users for a command.
* @param command {String}
*/
resetCommandVote(command) {
this._votes[command] = {count: 0, users: []};
}
}
Object.assign(exports, {
GuildHandler: GuildHandler
});

@ -0,0 +1,166 @@
const music = require('../music'),
utils = require('../utils'),
config = require('../../config.json'),
dblib = require('../database'),
logging = require('../utils/logging'),
fs = require('fs-extra'),
dataDir = config.dataPath || './data';
/**
* GuildDatabase class has abstraction for some sql statements.
*/
class GuildDatabase extends dblib.Database {
/**
* Constructor.
* @param name
*/
constructor(name) {
super(name);
}
/**
* Creates all tables needed in the guilds Database.
*/
async createTables() {
let sql = this.sql;
await this.run(sql.createTableIfNotExists('playlists', [
sql.templates.idcolumn,
new dblib.Column('name', sql.types.getVarchar(32), [sql.constraints.unique, sql.constraints.notNull]),
new dblib.Column('url', sql.types.getVarchar(255), [sql.constraints.notNull])
]));
this._logger.silly('Created Table playlists.');
await this.run(sql.createTableIfNotExists('commands', [
sql.templates.idcolumn,
new dblib.Column('name', sql.types.getVarchar(32), [sql.constraints.unique, sql.constraints.notNull]),
new dblib.Column('command', sql.types.getVarchar(255), [sql.constraints.notNull])
]));
this._logger.silly('Created Table commands.');
await this.run(sql.createTableIfNotExists('settings', [
sql.templates.idcolumn,
new dblib.Column('key', sql.types.getVarchar(32), [sql.constraints.unique, sql.constraints.notNull]),
new dblib.Column('value', sql.types.getVarchar(32), [])
]));
this._logger.silly('Created Table settings.');
}
/**
* Get the value of a setting
* @param name
* @returns {Promise<*>}
*/
async getSetting(name) {
let result = await this.get(this.sql.select('settings', false, 'value',
this.sql.where('key', '=', this.sql.parameter(1))), [name]);
if (result)
return result.value;
else
return null;
}
/**
* Get all settings as object.
* @returns {Promise<void>}
*/
async getSettings() {
let rows = await this.all(this.sql.select('settings', false, ['key', 'value'], [], []));
let retObj = {};
if (rows)
for (let row of rows)
retObj[row.key] = row.value;
return retObj;
}
/**
* Insert or update a setting parameter in the settings database.
* @param name
* @param value
* @returns {Promise<void>}
*/
async setSetting(name, value) {
let row = await this.get(this.sql.select('settings', false, [this.sql.count('*')],
this.sql.where('key', '=', this.sql.parameter(1))), [name]);
if (!row || Number(row.count) === 0)
await this.run(this.sql.insert('settings', {key: this.sql.parameter(1), value: this.sql.parameter(2)}),
[name, value]);
else
await this.run(this.sql.update('settings', {value: this.sql.parameter(1)},
this.sql.where('key', '=', this.sql.parameter(2))), [value, name]);
}
}
/**
* The Guild Handler handles guild settings and data.
* @type {GuildHandler}
*/
class GuildHandler {
constructor(guild) {
this.guild = guild;
this._logger = new logging.Logger(`${this.constructor.name}@${this.guild}`);
this.musicPlayer = new music.MusicPlayer(null);
this._logger.silly('Initialized Guild Handler');
this._votes = {};
this.settings = {};
}
/**
* Initializes the database
* @returns {Promise<void>}
*/
async initDatabase() {
this._logger.silly('Initializing Database');
this.db = new GuildDatabase(`guild_${this.guild.name.replace(/\s/g, '_').replace(/\W/g, '')}`);
await this.db.initDatabase();
this._logger.debug(`Connected to the database for ${this.guild}`);
this._logger.debug('Creating Databases');
await this.db.createTables();
}
/**
* Applies all relevant guild settings.
* @returns {Promise<void>}
*/
async applySettings() {
this.settings = await this.db.getSettings();
this.musicPlayer.setVolume(Number(this.settings.musicPlayerVolume) || 0.5);
this.musicPlayer.quality = this.settings.musicPlayerQuality || 'lowest';
}
/**
* Destroys the guild handler
*/
destroy() {
this._logger.debug('Ending musicPlayer');
this.musicPlayer.stop();
this._logger.debug('Ending Database');
this.db.close();
}
/**
* Sets the vote counter for a command up and adds the user.
* @param command {String}
* @param user {String}
*/
updateCommandVote(command, user) {
if (!this._votes[command])
this._votes[command] = {count: 0, users: []};
if (!this._votes[command].users.includes(user)) {
this._votes[command].count++;
this._votes[command].users.push(user);
}
return this._votes[command];
}
/**
* Resets the vote counter and voted users for a command.
* @param command {String}
*/
resetCommandVote(command) {
this._votes[command] = {count: 0, users: []};
}
}
Object.assign(exports, {
GuildHandler: GuildHandler
});

@ -1,6 +1,5 @@
const ytdl = require("ytdl-core"), const ytdl = require("ytdl-core"),
ypi = require('youtube-playlist-info'), ypi = require('youtube-playlist-info'),
yttl = require('get-youtube-title'),
config = require('../../config.json'), config = require('../../config.json'),
utils = require('../utils/index.js'), utils = require('../utils/index.js'),
logging = require('../utils/logging'), logging = require('../utils/logging'),
@ -20,10 +19,11 @@ class MusicPlayer {
this.repeat = false; this.repeat = false;
this.volume = 0.5; this.volume = 0.5;
this.voiceChannel = voiceChannel; this.voiceChannel = voiceChannel;
this.quality = 'lowest';
this.exitTimeout = null; this.exitTimeout = null;
this._logger = new logging.Logger(this); this._logger = new logging.Logger(this);
this._logger.silly('Initialized Music Player'); this._logger.silly('Initialized Music Player');
config.music? this.quality = config.music.quality || 'lowest' : this.quality = 'lowest';
config.music? this.liveBuffer = config.music.liveBuffer || 10000 : 10000;
} }
/** /**
@ -151,12 +151,13 @@ class MusicPlayer {
this.queue.push({'url': vurl, 'title': await this.getVideoName(vurl)}); //eslint-disable-line no-await-in-loop this.queue.push({'url': vurl, 'title': await this.getVideoName(vurl)}); //eslint-disable-line no-await-in-loop
} catch (err) { } catch (err) {
if (err.message !== 'Not found') { if (err.message !== 'Not found') {
this._logger.warn(err.message); this._logger.verbose(err.message);
this._logger.debug(err.stack); this._logger.silly(err.stack);
} }
} }
} }
this._logger.debug(`Added ${playlistItems.length} songs to the queue`); this._logger.debug(`Added ${playlistItems.length} songs to the queue`);
return playlistItems.length;
} else if (!this.playing || !this.disp) { } else if (!this.playing || !this.disp) {
this._logger.debug(`Playing ${url}`); this._logger.debug(`Playing ${url}`);
this.current = ({'url': url, 'title': await this.getVideoName(url)}); this.current = ({'url': url, 'title': await this.getVideoName(url)});
@ -164,11 +165,9 @@ class MusicPlayer {
this.queue.push(this.current); this.queue.push(this.current);
this.disp = this.conn.playStream(ytdl(url, this.disp = this.conn.playStream(ytdl(url,
{filter: 'audioonly', quality: this.quality, liveBuffer: config.music.livePuffer || 20000}), {filter: 'audioonly', quality: this.quality, liveBuffer: this.liveBuffer}),
{volume: this.volume}); {volume: this.volume});
this.disp.on('debug', (dbgmsg) => this._logger.silly(dbgmsg));
this.disp.on('error', (err) => { this.disp.on('error', (err) => {
this._logger.error(err.message); this._logger.error(err.message);
this._logger.debug(err.stack); this._logger.debug(err.stack);
@ -201,21 +200,12 @@ class MusicPlayer {
/** /**
* Gets the name of the YouTube Video at url * Gets the name of the YouTube Video at url
* TODO: ytdl.getInfo
* @param url {String} * @param url {String}
* @returns {Promise<>} * @returns {String}
*/ */
getVideoName(url) { async getVideoName(url) {
return new Promise((resolve, reject) => { let info = await ytdl.getBasicInfo(url);
yttl(utils.YouTube.getVideoIdFromUrl(url), (err, title) => { return info.title;
if (err) {
this._logger.debug(JSON.stringify(err));
reject(err);
} else {
resolve(title);
}
});
});
} }
/** /**
@ -224,12 +214,9 @@ class MusicPlayer {
*/ */
setVolume(percentage) { setVolume(percentage) {
this._logger.verbose(`Setting volume to ${percentage}`); this._logger.verbose(`Setting volume to ${percentage}`);
if (this.disp !== null) { this.volume = percentage;
this.volume = percentage; if (this.disp !== null)
this.disp.setVolume(percentage); this.disp.setVolume(percentage);
} else {
this._logger.warn("No dispatcher found.");
}
} }
/** /**

@ -85,6 +85,16 @@ class GenericTypes {
} }
} }
get serialPK() {
switch (this.database) {
case 'sqlite':
return 'INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL';
case 'postgresql':
default:
return 'SERIAL PRIMARY KEY UNIQUE';
}
}
/** /**
* Returns the VARCHAR type with the specified length. * Returns the VARCHAR type with the specified length.
* @param length {Number} * @param length {Number}
@ -117,6 +127,22 @@ class GenericSql {
any: 'ANY', any: 'ANY',
all: 'ALL' all: 'ALL'
}; };
this.templates = {
idcolumn: new Column('id', this.types.serialPK, [])
};
}
/**
* Returns a value placeholder for the specified number.
* @param number {Number} - the variables position.
*/
parameter(number) {
switch (this.database) {
case 'postgresql':
return `$${number}`;
case 'sqlite':
return '?';
}
} }
/** /**
@ -180,7 +206,7 @@ class GenericSql {
switch (this.database) { switch (this.database) {
case 'postgresql': case 'postgresql':
case 'sqlite': case 'sqlite':
return `COUNT(${colname})`; return `COUNT(${colname}) count`;
} }
} }
@ -287,10 +313,12 @@ class GenericSql {
* Updates the table with the rowValueObject. * Updates the table with the rowValueObject.
* @param table {String} - the table name * @param table {String} - the table name
* @param colValueObj {Object} - an object with keys as columnnames and values as columnvalues * @param colValueObj {Object} - an object with keys as columnnames and values as columnvalues
* @param conditions {Array<String>} - conditions for the update row selection (WHERE ... [OR ...][AND ...] * @param conditions {Array<String>|String} - conditions for the update row selection (WHERE ... [OR ...][AND ...]
* @returns {string} * @returns {string}
*/ */
update(table, colValueObj, conditions) { update(table, colValueObj, conditions) {
if (!(conditions instanceof Array))
conditions = [conditions];
switch (this.database) { switch (this.database) {
case 'postgresql': case 'postgresql':
case 'sqlite': case 'sqlite':
@ -302,16 +330,37 @@ class GenericSql {
* Selects from a table * Selects from a table
* @param table {String} - the tablename * @param table {String} - the tablename
* @param distinct {String|boolean} - should distinct values be selected? If yes provide distinct keyword. * @param distinct {String|boolean} - should distinct values be selected? If yes provide distinct keyword.
* @param colnames {Array<String>} - the rows to select * @param colnames {Array<String>|String} - the rows to select
* @param conditions {Array<String>} - conditions for the row selection (WHERE ... [OR ...][AND ...] * @param conditions {Array<String>|String} - conditions for the row selection (WHERE ... [OR ...][AND ...]
* @param operations {Array<String>} - operations on the selected rows * @param operations {Array<String>|String} - operations on the selected rows
* @returns {String} * @returns {String}
*/ */
select(table, distinct, colnames, conditions, operations) { select(table, distinct, colnames, conditions, operations) {
if (!(colnames instanceof Array))
colnames = [colnames];
if (!(conditions instanceof Array))
conditions = [conditions];
if (!(operations instanceof Array))
operations = [operations];
switch (this.database) {
case 'postgresql':
case 'sqlite':
return `SELECT${distinct? ' ' + distinct : ''} ${colnames.join(', ')} FROM ${table} ${conditions.join(' ')} ${operations.join(' ')}`;
}
}
/**
* Deletes from a table
* @param table {String} - the table name
* @param conditions {Array<String>|String} - conditions for the row selection (WHERE ... [OR ...][AND ...]
*/
delete(table, conditions) {
if (!(conditions instanceof Array))
conditions = [conditions];
switch (this.database) { switch (this.database) {
case 'postgresql': case 'postgresql':
case 'sqlite': case 'sqlite':
return `SELECT${distinct? ' ' + distinct : ''} ${colnames.join(' ')} FROM ${table} ${conditions.join(' ')} ${operations.join(' ')}`; return `DELETE FROM ${table} ${conditions.join(' ')}`;
} }
} }
} }
@ -327,6 +376,8 @@ class Column {
this.name = name; this.name = name;
this.type = type; this.type = type;
this.constraints = constraints || []; this.constraints = constraints || [];
if (!(constraints instanceof Array))
this.constraints = [constraints];
} }
/** /**
@ -338,12 +389,12 @@ class Column {
} }
get sql() { get sql() {
return `${this.name} ${this.type} ${this.constraints.join(',')}`; return `${this.name} ${this.type} ${this.constraints.join(' ')}`;
} }
} }
Object.assign(exports, { Object.assign(exports, {
GenericSql: GenericSql, GenericSql: GenericSql,
GenericTypes: GenericSql, GenericTypes: GenericTypes,
Column: Column Column: Column
}); });

@ -8,6 +8,7 @@ const express = require('express'),
fs = require('fs'), fs = require('fs'),
session = require('express-session'), session = require('express-session'),
SQLiteStore = require('connect-sqlite3')(session), SQLiteStore = require('connect-sqlite3')(session),
dblib = require('../../lib/database'),
bodyParser = require('body-parser'), bodyParser = require('body-parser'),
compileSass = require('express-compile-sass'), compileSass = require('express-compile-sass'),
config = require('../../config.json'), config = require('../../config.json'),
@ -62,7 +63,11 @@ exports.WebServer = class {
if (!req.body.username || !req.body.password) { if (!req.body.username || !req.body.password) {
res.render('login', {msg: 'Please enter username and password.'}); res.render('login', {msg: 'Please enter username and password.'});
} else { } else {
let user = await this.maindb.get('SELECT * FROM users WHERE username = ? AND password = ?', [req.body.username, req.body.password]); 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) { if (!user) {
this._logger.debug(`User ${req.body.username} failed to authenticate`); this._logger.debug(`User ${req.body.username} failed to authenticate`);
res.render('login', {msg: 'Login failed!'}); res.render('login', {msg: 'Login failed!'});
@ -150,13 +155,14 @@ exports.WebServer = class {
*/ */
async setReferenceObjects(objects) { async setReferenceObjects(objects) {
this.maindb = objects.maindb; this.maindb = objects.maindb;
await this.maindb.run(`${utils.sql.tableExistCreate} users ( let sql = this.maindb.sql;
${utils.sql.pkIdSerial}, await this.maindb.run(sql.createTableIfNotExists('users', [
username VARCHAR(32) UNIQUE NOT NULL, sql.templates.idcolumn,
password VARCHAR(255) NOT NULL, new dblib.Column('username', sql.types.getVarchar(32), [sql.constraints.unique, sql.constraints.notNull]),
token VARCHAR(255) UNIQUE NOT NULL, new dblib.Column('password', sql.types.getVarchar(255), [sql.constraints.notNull]),
scope INTEGER NOT NULL DEFAULT 0 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.root = { this.root = {
client: { client: {
guilds: async (args) => { guilds: async (args) => {
@ -360,7 +366,7 @@ class Guild {
async querySaved() { async querySaved() {
if (this.guildHandler.db) { if (this.guildHandler.db) {
let saved = []; let saved = [];
let rows = await this.guildHandler.db.all('SELECT * FROM playlists'); let rows = await this.guildHandler.db.all(this.guildHandler.db.sql.select('playlists', false, '*'));
for (let row of rows) for (let row of rows)
saved.push({ saved.push({
id: generateID(['Media', row.url]), id: generateID(['Media', row.url]),

@ -19,14 +19,14 @@
"express-session": "1.15.6", "express-session": "1.15.6",
"ffmpeg-binaries": "4.0.0", "ffmpeg-binaries": "4.0.0",
"fs-extra": "7.0.1", "fs-extra": "7.0.1",
"get-youtube-title": "1.0.0",
"graphql": "14.1.1", "graphql": "14.1.1",
"js-md5": "0.7.3", "js-md5": "0.7.3",
"js-sha512": "0.8.0", "js-sha512": "0.8.0",
"js-yaml": "latest", "js-yaml": "latest",
"node-fetch": "^2.3.0", "node-fetch": "^2.3.0",
"node-sass": "4.11.0",
"node-opus": "0.3.1", "node-opus": "0.3.1",
"node-sass": "4.11.0",
"pg": "^7.8.2",
"promise-waterfall": "0.1.0", "promise-waterfall": "0.1.0",
"pug": "2.0.3", "pug": "2.0.3",
"sqlite3": "4.0.6", "sqlite3": "4.0.6",
@ -44,7 +44,8 @@
"sinon": "7.2.6", "sinon": "7.2.6",
"eslint-plugin-graphql": "3.0.3", "eslint-plugin-graphql": "3.0.3",
"eslint": "5.15.0", "eslint": "5.15.0",
"eslint-plugin-promise": "4.0.1" "eslint-plugin-promise": "4.0.1",
"opusscript": "0.0.6"
}, },
"eslintConfig": { "eslintConfig": {
"parserOptions": { "parserOptions": {

Loading…
Cancel
Save