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
- moved commands outside of `lib`
- switched from opusscript to node-opus for voice
- all hard coded sql statements to generic sql generation
### Added
- state lib with `EventRouter` and `EventGroup` and `Event` classes
- Subclasses of EventRouter for client events groupes `Client`, `Channel`, `Message` and `Guild`
- Utility classes for generic SQL Statements
- 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
### Changed

@ -48,6 +48,14 @@ The arguments are optional because the token and youtube-api-key that the bot ne
"commandSettings": {
"maxSequenceParallel": 5, // the maximum number of commands executed in parallel
"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'),
logging = require('./lib/utils/logging'),
msgLib = require('./lib/message'),
guilding = require('./lib/guilding'),
guilding = require('./lib/guilds'),
utils = require('./lib/utils'),
config = require('./config.json'),
args = require('args-parser')(process.argv),
sqliteAsync = require('./lib/utils/sqliteAsync'),
dblib = require('./lib/database'),
authToken = args.token || config.api.botToken,
prefix = args.prefix || config.prefix || '~',
gamepresence = args.game || config.presence;
@ -112,13 +112,14 @@ class Bot {
this.logger.debug('Checking for ./data/ existence');
await fs.ensureDir('./data');
this.logger.verbose('Connecting to main database');
this.maindb = new sqliteAsync.Database('./data/main.db');
await this.maindb.init();
await this.maindb.run(`${utils.sql.tableExistCreate} presences (
${utils.sql.pkIdSerial},
text VARCHAR(255) UNIQUE NOT NULL
)`);
this.maindb = new dblib.Database('main');
await this.maindb.initDatabase();
let sql = this.maindb.sql;
await this.maindb.run(sql.createTableIfNotExists('presences', [
sql.templates.idcolumn,
new dblib.Column('text', sql.types.getVarchar(255),
[sql.constraints.unique, sql.constraints.notNull])
]));
this.logger.debug('Loading Presences...');
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.
* 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
* 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.
*/
async loadPresences() {
let sql = this.maindb.sql;
if (await fs.pathExists('./data/presences.txt')) {
let lineReader = require('readline').createInterface({
input: require('fs').createReadStream('./data/presences.txt')
});
lineReader.on('line', (line) => {
this.maindb.run('INSERT INTO presences (text) VALUES (?)', [line], (err) => {
if (err)
this.logger.warn(err.message);
});
this.presences.push(line);
this.maindb.begin();
lineReader.on('line', async (line) => {
try {
await this.maindb.query(sql.insert('presences', {text: sql.parameter(1)}), [line]);
this.presences.push(line);
} catch (err) {
this.logger.warn(err.message);
this.logger.debug(err.stack);
}
});
await this.maindb.commit();
this.rotator = this.client.setInterval(() => this.rotatePresence(),
config.presence_duration || 360000);
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)
if (!(row[0] in this.presences))
this.presences.push(row.text);
} 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)
this.presences.push(row.text);
this.rotator = this.client.setInterval(() => this.rotatePresence(),
@ -238,6 +243,7 @@ class Bot {
if (!this.guildHandlers[guild.id]) {
let newGuildHandler = new guilding.GuildHandler(guild);
await newGuildHandler.initDatabase();
await newGuildHandler.applySettings();
this.guildHandlers[guild.id] = newGuildHandler;
}
return this.guildHandlers[guild.id];

@ -42,9 +42,9 @@ class MusicCommandModule extends cmdLib.CommandModule {
async _connectAndPlay(gh, vc, url, next) {
if (!gh.musicPlayer.connected) {
await gh.musicPlayer.connect(vc);
await gh.musicPlayer.playYouTube(url, next);
return await gh.musicPlayer.playYouTube(url, next);
} 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;
if (!utils.YouTube.isValidEntityUrl(url)) {
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) {
this._logger.debug('Got invalid url for play command.');
return t.response.url_invalid;
} else {
await this._connectAndPlay(gh, vc, row.url, n);
return t.response.success;
let songcount = await this._connectAndPlay(gh, vc, row.url, n);
if (songcount)
return `Added ${songcount} songs to the queue.`;
else
return t.response.success;
}
} else {
await this._connectAndPlay(gh, vc, url, n);
return t.response.success;
let songcount = await this._connectAndPlay(gh, vc, url, n);
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) => {
let gh = await this._getGuildHandler(m.guild);
let saveName = s.replace(k.url + ' ', '');
let row = await gh.db
.get('SELECT COUNT(*) count FROM playlists WHERE name = ?', [saveName]);
if (!row || row.count === 0)
await gh.db.run('INSERT INTO playlists (name, url) VALUES (?, ?)',
[saveName, k.url]);
let row = await gh.db.get(gh.db.sql.select('playlists', false,
[gh.db.sql.count('*')], gh.db.sql.where('name', '=', gh.db.sql.parameter(1))), [saveName]);
if (!row || Number(row.count) === 0)
await gh.db.run(gh.db.sql.insert('playlists',
{name: gh.db.sql.parameter(1), url: gh.db.sql.parameter(2)}), [saveName, k.url]);
else
await gh.db.run('UPDATE playlists SET url = ? WHERE name = ?',
[k.url, saveName]);
await gh.db.run(gh.db.sql.update('playlists',
{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}`;
})
);
@ -268,7 +276,7 @@ class MusicCommandModule extends cmdLib.CommandModule {
if (!s) {
return this.template.delete_media.response.no_name;
} 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`;
}
})
@ -279,7 +287,7 @@ class MusicCommandModule extends cmdLib.CommandModule {
new cmdLib.Answer(async (m) => {
let gh = await this._getGuildHandler(m.guild);
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)
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
commandHandler
.registerCommand(play)
@ -308,7 +346,9 @@ class MusicCommandModule extends cmdLib.CommandModule {
.registerCommand(toggleRepeat)
.registerCommand(saveMedia)
.registerCommand(deleteMedia)
.registerCommand(savedMedia);
.registerCommand(savedMedia)
.registerCommand(volume)
.registerCommand(quality);
}
}

@ -168,3 +168,28 @@ saved_media:
response:
no_saved: >
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)) {
return this.template.save_cmd.response.sequence_too_many_serial;
} else {
let row = await gh.db
.get('SELECT COUNT(*) count FROM commands WHERE name = ?', [k.name]);
if (!row || row.count === 0)
await gh.db
.run('INSERT INTO commands (name, command) VALUES (?, ?)', [k.name, JSON.stringify(sequence)]);
let sql = gh.db.sql;
let row = await gh.db.get(sql.select('commands', false, [sql.count('*')],
sql.where('name', '=', sql.parameter(1))), [k.name]);
if (!row || Number(row.count) === 0)
await gh.db.run(sql.insert('commands', {name: sql.parameter(1), command: sql.parameter(2)}),
[k.name, JSON.stringify(sequence)]);
else
await await gh.db
.run('UPDATE commands SET command = ? WHERE name = ?', [JSON.stringify(sequence), k.name]);
await gh.db.run(sql.update('commands', {command: sql.parameter(1)}, sql.where('name', '=', sql.parameter(2))),
[JSON.stringify(sequence), k.name]);
}
})
);
@ -82,7 +83,7 @@ class ServerUtilityCommandModule extends cmdLib.CommandModule {
this.template.delete_cmd,
new cmdLib.Answer(async (m, k) => {
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}`;
})
);
@ -93,7 +94,7 @@ class ServerUtilityCommandModule extends cmdLib.CommandModule {
let gh = await this._getGuildHandler(m.guild);
let response = new cmdLib.ExtendedRichEmbed('Saved Commands')
.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)
return this.template.saved_cmd.response.no_commands;
else
@ -107,8 +108,8 @@ class ServerUtilityCommandModule extends cmdLib.CommandModule {
this.template.execute,
new cmdLib.Answer(async (m, k) => {
let gh = await this._getGuildHandler(m.guild);
let row = await gh.db
.get('SELECT command FROM commands WHERE name = ?', [k.name]);
let row = await gh.db.get(gh.db.sql.select('commands',false, ['command'],
gh.db.sql.where('name', '=', gh.db.sql.parameter(1))), [k.name]);
if (row)
await this._messageHandler
.executeCommandSequence(JSON.parse(row.command), m);

@ -24,12 +24,13 @@ class UtilityCommandModule extends cmdLib.CommandModule {
async register(commandHandler) {
await this._loadTemplate();
let sql = this._bot.maindb.sql;
let addPresence = new cmdLib.Command(
this.template.add_presence,
new cmdLib.Answer(async (m, k, 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}\``;
})
);

@ -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"),
ypi = require('youtube-playlist-info'),
yttl = require('get-youtube-title'),
config = require('../../config.json'),
utils = require('../utils/index.js'),
logging = require('../utils/logging'),
@ -20,10 +19,11 @@ class MusicPlayer {
this.repeat = false;
this.volume = 0.5;
this.voiceChannel = voiceChannel;
this.quality = 'lowest';
this.exitTimeout = null;
this._logger = new logging.Logger(this);
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
} catch (err) {
if (err.message !== 'Not found') {
this._logger.warn(err.message);
this._logger.debug(err.stack);
this._logger.verbose(err.message);
this._logger.silly(err.stack);
}
}
}
this._logger.debug(`Added ${playlistItems.length} songs to the queue`);
return playlistItems.length;
} else if (!this.playing || !this.disp) {
this._logger.debug(`Playing ${url}`);
this.current = ({'url': url, 'title': await this.getVideoName(url)});
@ -164,11 +165,9 @@ class MusicPlayer {
this.queue.push(this.current);
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});
this.disp.on('debug', (dbgmsg) => this._logger.silly(dbgmsg));
this.disp.on('error', (err) => {
this._logger.error(err.message);
this._logger.debug(err.stack);
@ -201,21 +200,12 @@ class MusicPlayer {
/**
* Gets the name of the YouTube Video at url
* TODO: ytdl.getInfo
* @param url {String}
* @returns {Promise<>}
* @returns {String}
*/
getVideoName(url) {
return new Promise((resolve, reject) => {
yttl(utils.YouTube.getVideoIdFromUrl(url), (err, title) => {
if (err) {
this._logger.debug(JSON.stringify(err));
reject(err);
} else {
resolve(title);
}
});
});
async getVideoName(url) {
let info = await ytdl.getBasicInfo(url);
return info.title;
}
/**
@ -224,12 +214,9 @@ class MusicPlayer {
*/
setVolume(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);
} 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.
* @param length {Number}
@ -117,6 +127,22 @@ class GenericSql {
any: 'ANY',
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) {
case 'postgresql':
case 'sqlite':
return `COUNT(${colname})`;
return `COUNT(${colname}) count`;
}
}
@ -287,10 +313,12 @@ class GenericSql {
* Updates the table with the rowValueObject.
* @param table {String} - the table name
* @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}
*/
update(table, colValueObj, conditions) {
if (!(conditions instanceof Array))
conditions = [conditions];
switch (this.database) {
case 'postgresql':
case 'sqlite':
@ -302,16 +330,37 @@ class GenericSql {
* Selects from a table
* @param table {String} - the tablename
* @param distinct {String|boolean} - should distinct values be selected? If yes provide distinct keyword.
* @param colnames {Array<String>} - the rows to select
* @param conditions {Array<String>} - conditions for the row selection (WHERE ... [OR ...][AND ...]
* @param operations {Array<String>} - operations on the selected rows
* @param colnames {Array<String>|String} - the rows to select
* @param conditions {Array<String>|String} - conditions for the row selection (WHERE ... [OR ...][AND ...]
* @param operations {Array<String>|String} - operations on the selected rows
* @returns {String}
*/
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) {
case 'postgresql':
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.type = type;
this.constraints = constraints || [];
if (!(constraints instanceof Array))
this.constraints = [constraints];
}
/**
@ -338,12 +389,12 @@ class Column {
}
get sql() {
return `${this.name} ${this.type} ${this.constraints.join(',')}`;
return `${this.name} ${this.type} ${this.constraints.join(' ')}`;
}
}
Object.assign(exports, {
GenericSql: GenericSql,
GenericTypes: GenericSql,
GenericTypes: GenericTypes,
Column: Column
});

@ -8,6 +8,7 @@ const express = require('express'),
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'),
@ -62,7 +63,11 @@ exports.WebServer = class {
if (!req.body.username || !req.body.password) {
res.render('login', {msg: 'Please enter username and password.'});
} 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) {
this._logger.debug(`User ${req.body.username} failed to authenticate`);
res.render('login', {msg: 'Login failed!'});
@ -150,13 +155,14 @@ exports.WebServer = class {
*/
async setReferenceObjects(objects) {
this.maindb = objects.maindb;
await this.maindb.run(`${utils.sql.tableExistCreate} users (
${utils.sql.pkIdSerial},
username VARCHAR(32) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
token VARCHAR(255) UNIQUE NOT NULL,
scope INTEGER NOT NULL DEFAULT 0
)`);
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.root = {
client: {
guilds: async (args) => {
@ -360,7 +366,7 @@ class Guild {
async querySaved() {
if (this.guildHandler.db) {
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)
saved.push({
id: generateID(['Media', row.url]),

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

Loading…
Cancel
Save