diff --git a/CHANGELOG.md b/CHANGELOG.md index 63a5a6c..85216b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,43 @@ All notable changes to the discord bot will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.11.0] - 2019-03-03 +## [Unreleased] +### Fixed +- bug where the bot counts itself when calculating needed votes to skip/stop music +- bug on the `ExtendedRichEmbed` where `addField` and `setDescription` throws an error when the value is null or undefined +- bug on `AnilistApiCommands` where the `RichCharacterInfo` uses a nonexistent function of the `ExtendedRichEmbed` +- bug on`AnilistApi` where the `.gql` files couldn't be found. +- Typo in changelog +- bug on `~np` message that causes the player to crash +- database handler using release on pooled client + +### Changed +- name of MiscCommands module from `TemplateCommandModule` to `MiscCommandModule` +- moved everything in `lib` to subfolders with the same name as the files and renamed the files to `index.js` +- 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 +- MusicPlayer to extend the default EventEmitter +- MessageHandler to accept instances of Response and redirect events to it + +### Added +- 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 +- table `messages` to main database where messages are stored for statistical analysis and bug handling +- ExtendedEventEmitter class in lib/utils/extended-events.js +- Response object that allows the registration of events for messages +- Handling of error event for every VoiceConnection + +### Removed +- `~volume` command because volume can't be controlled anymore +- volume functions and properties from the MusicPlayer + +## [0.11.0-beta] - 2019-03-03 ### Changed - template Files to name `template.yaml` - loading template file form CommandModule property `templateFile` to loading the `template.yaml` file from the `_templateDir` property (still supporting loading form templateFile) @@ -19,12 +55,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed - `ExtendedRichEmbed.addNonemptyField` because the overide of `.addField` does the same -## [0.10.1] - 2019-03-03 +## [0.10.1]-beta - 2019-03-03 ### Changed - Bugfix on RichEmbed not returning itself on addField and setDescription because of method overide - AniList CommandModule bug fix on `~alCharacter` not returning voice actor names -## [0.10.0] - 2019-03-03 +## [0.10.0-beta] - 2019-03-03 ### Added - AniList api commands powered by [AniList.co](https://www.anilist.co) - MessageHandler - handles all incoming messages, parses the syntax, executes the syntax and handles rate limits diff --git a/README.md b/README.md index 42ab00f..f15e00c 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,14 @@ discordbot [![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blu A bot that does the discord thing. +Installation +--- + +You can easily install everything with npm `npm i`. If you run into an error see the [discord.js installation guide](https://github.com/discordjs/discord.js#installation) or open an issue. If you run into an error with `ffmpeg-binaries` try using nodejs `v10.15.0` + +Running +--- + `node bot.node [--token=] [--ytapi=] [--owner=] [--prefix=] [--game=] [-i=]` The arguments are optional because the token and youtube-api-key that the bot needs to run can also be defined in the config.json in the bot's directory: @@ -40,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 } } ``` diff --git a/bot.js b/bot.js index 155ec78..bd63aa0 100644 --- a/bot.js +++ b/bot.js @@ -1,12 +1,12 @@ const Discord = require("discord.js"), fs = require('fs-extra'), - logging = require('./lib/logging'), - msgLib = require('./lib/MessageLib'), - guilding = require('./lib/guilding'), + logging = require('./lib/utils/logging'), + msgLib = require('./lib/message'), + guilding = require('./lib/guilds'), utils = require('./lib/utils'), config = require('./config.json'), args = require('args-parser')(process.argv), - sqliteAsync = require('./lib/sqliteAsync'), + dblib = require('./lib/database'), authToken = args.token || config.api.botToken, prefix = args.prefix || config.prefix || '~', gamepresence = args.game || config.presence; @@ -69,24 +69,24 @@ class Bot { if (config.webinterface && config.webinterface.enabled) await this.initializeWebserver(); this.logger.verbose('Registering commands'); - await this.messageHandler.registerCommandModule(require('./lib/commands/AnilistApiCommands').module, {}); - await this.messageHandler.registerCommandModule(require('./lib/commands/UtilityCommands').module, { + await this.messageHandler.registerCommandModule(require('./commands/AnilistApiCommands').module, {}); + await this.messageHandler.registerCommandModule(require('./commands/UtilityCommands').module, { bot: this, config: config }); - await this.messageHandler.registerCommandModule(require('./lib/commands/InfoCommands').module, { + await this.messageHandler.registerCommandModule(require('./commands/InfoCommands').module, { client: this.client, messageHandler: this.messageHandler }); - await this.messageHandler.registerCommandModule(require('./lib/commands/MusicCommands').module, { + await this.messageHandler.registerCommandModule(require('./commands/MusicCommands').module, { getGuildHandler: async (g) => await this.getGuildHandler(g) }); - await this.messageHandler.registerCommandModule(require('./lib/commands/ServerUtilityCommands').module, { + await this.messageHandler.registerCommandModule(require('./commands/ServerUtilityCommands').module, { getGuildHandler: async (g) => await this.getGuildHandler(g), messageHandler: this.messageHandler, config: config }); - await this.messageHandler.registerCommandModule(require('./lib/commands/MiscCommands').module, {}); + await this.messageHandler.registerCommandModule(require('./commands/MiscCommands').module, {}); this.registerEvents(); } @@ -112,13 +112,22 @@ 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]) + ])); + await this.maindb.run(sql.createTableIfNotExists('messages', [ + sql.templates.idcolumn, + new dblib.Column('server', sql.types.getVarchar(255)), + new dblib.Column('channel', sql.types.getVarchar(255)), + new dblib.Column('username', sql.types.getVarchar(255), [sql.constraints.notNull]), + new dblib.Column('message', sql.types.text), + new dblib.Column('timestamp', sql.types.datetime, [sql.constraints.notNull, sql.default('NOW()')]) + ])); this.logger.debug('Loading Presences...'); await this.loadPresences(); } @@ -128,7 +137,7 @@ class Bot { */ async initializeWebserver() { this.logger.verbose('Importing weblib'); - weblib = require('./lib/WebLib'); + weblib = require('./lib/web'); this.logger.verbose('Creating WebServer'); this.webServer = new weblib.WebServer(config.webinterface.port || 8080); this.logger.debug('Setting Reference Objects to webserver'); @@ -145,33 +154,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 +251,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]; @@ -248,10 +262,17 @@ class Bot { // Executing the main function if (typeof require !== 'undefined' && require.main === module) { let logger = new logging.Logger('MAIN-init'); + process.on('unhandledRejection', err => { + // Will print "unhandledRejection err is not defined" + logger.warn(err.message); + logger.debug(err.stack); + }); + logger.info("Starting up... "); logger.debug('Calling constructor...'); let discordBot = new Bot(); logger.debug('Initializing services...'); + discordBot.initServices().then(() => { logger.debug('Starting Bot...'); discordBot.start().catch((err) => { //eslint-disable-line promise/no-nesting diff --git a/lib/commands/.template/index.js b/commands/.template/index.js similarity index 95% rename from lib/commands/.template/index.js rename to commands/.template/index.js index 399ac59..966beba 100644 --- a/lib/commands/.template/index.js +++ b/commands/.template/index.js @@ -1,5 +1,5 @@ /* template index.js. Doesn't implement actual commands */ -const cmdLib = require('../../CommandLib'); // required for command objects +const cmdLib = require('../../lib/command'); // required for command objects /** * A description what the command module includes and why. Doesn't need to list commands but explains diff --git a/lib/commands/.template/template.yaml b/commands/.template/template.yaml similarity index 100% rename from lib/commands/.template/template.yaml rename to commands/.template/template.yaml diff --git a/lib/commands/AnilistApiCommands/index.js b/commands/AnilistApiCommands/index.js similarity index 98% rename from lib/commands/AnilistApiCommands/index.js rename to commands/AnilistApiCommands/index.js index 1e8e4fd..9354d4c 100644 --- a/lib/commands/AnilistApiCommands/index.js +++ b/commands/AnilistApiCommands/index.js @@ -1,5 +1,5 @@ -const cmdLib = require('../../CommandLib'), - anilistApi = require('../../api/AnilistApi'); +const cmdLib = require('../../lib/command'), + anilistApi = require('../../lib/api/AniListApi'); /** * The AniList commands are all commands that interact with the anilist api. @@ -144,7 +144,7 @@ class RichCharacterInfo extends cmdLib.ExtendedRichEmbed { .replace(/~!.*?!~/g, '') .replace(/\n\n\n/g, '')); if (characterInfo.media && characterInfo.media.edges) - this.addNonemptyField( + this.addField( 'Media Appeareance', characterInfo.media.edges.map(x => { let media = x.node; diff --git a/lib/commands/AnilistApiCommands/template.yaml b/commands/AnilistApiCommands/template.yaml similarity index 100% rename from lib/commands/AnilistApiCommands/template.yaml rename to commands/AnilistApiCommands/template.yaml diff --git a/lib/commands/InfoCommands/index.js b/commands/InfoCommands/index.js similarity index 98% rename from lib/commands/InfoCommands/index.js rename to commands/InfoCommands/index.js index fbff255..e81d97a 100644 --- a/lib/commands/InfoCommands/index.js +++ b/commands/InfoCommands/index.js @@ -1,6 +1,6 @@ -const cmdLib = require('../../CommandLib'), +const cmdLib = require('../../lib/command'), fsx = require('fs-extra'), - utils = require('../../utils'); + utils = require('../../lib/utils'); /** * Info commands provide information about the bot. These informations are diff --git a/lib/commands/InfoCommands/template.yaml b/commands/InfoCommands/template.yaml similarity index 100% rename from lib/commands/InfoCommands/template.yaml rename to commands/InfoCommands/template.yaml diff --git a/lib/commands/MiscCommands/index.js b/commands/MiscCommands/index.js similarity index 94% rename from lib/commands/MiscCommands/index.js rename to commands/MiscCommands/index.js index 8cfc852..be12770 100644 --- a/lib/commands/MiscCommands/index.js +++ b/commands/MiscCommands/index.js @@ -1,5 +1,5 @@ /* template index.js. Doesn't implement actual commands */ -const cmdLib = require('../../CommandLib'); +const cmdLib = require('../../lib/command'); /** * Several commands that are that special that they can't be included in any other module. @@ -15,7 +15,7 @@ function delay(seconds) { }); } -class TemplateCommandModule extends cmdLib.CommandModule { +class MiscCommandModule extends cmdLib.CommandModule { constructor() { super(cmdLib.CommandScopes.Global); @@ -78,5 +78,5 @@ class TemplateCommandModule extends cmdLib.CommandModule { Object.assign(exports, { - module: TemplateCommandModule + module: MiscCommandModule }); diff --git a/lib/commands/MiscCommands/template.yaml b/commands/MiscCommands/template.yaml similarity index 100% rename from lib/commands/MiscCommands/template.yaml rename to commands/MiscCommands/template.yaml diff --git a/lib/commands/MusicCommands/index.js b/commands/MusicCommands/index.js similarity index 71% rename from lib/commands/MusicCommands/index.js rename to commands/MusicCommands/index.js index c701326..fa6ec77 100644 --- a/lib/commands/MusicCommands/index.js +++ b/commands/MusicCommands/index.js @@ -1,6 +1,6 @@ -const cmdLib = require('../../CommandLib'), - utils = require('../../utils'), - config = require('../../../config'); +const cmdLib = require('../../lib/command'), + utils = require('../../lib/utils'), + config = require('../../config'); function checkPermission(msg, rolePerm) { if (!rolePerm || ['all', 'any', 'everyone'].includes(rolePerm)) @@ -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; } } @@ -117,7 +124,7 @@ class MusicCommandModule extends cmdLib.CommandModule { let vc = gh.musicPlayer.voiceChannel || m.member.voiceChannel; if (gh.musicPlayer.connected && vc) { let votes = gh.updateCommandVote(stop.name, m.author.tag); - let neededVotes = Math.ceil(vc.members.size/2); + let neededVotes = Math.ceil((vc.members.size - 1) / 2); if (neededVotes <= votes.count || checkPermission(m, 'dj')) { this._logger.debug(`Vote passed. ${votes.count} out of ${neededVotes} for stop or permission granted`); @@ -167,7 +174,7 @@ class MusicCommandModule extends cmdLib.CommandModule { let vc = gh.musicPlayer.voiceChannel || m.member.voiceChannel; if (gh.musicPlayer.playing && vc) { let votes = gh.updateCommandVote(skip.name, m.author.tag); - let neededVotes = Math.ceil(vc.members.size/2); + let neededVotes = Math.ceil((vc.members.size - 1) / 2); if (neededVotes <= votes.count || checkPermission(m, 'dj')) { this._logger.debug(`Vote passed. ${votes.count} out of ${neededVotes} for skip or permission granted`); @@ -221,6 +228,28 @@ class MusicCommandModule extends cmdLib.CommandModule { .setColor(0x00aaff); else return this.template.media_current.response.not_playing; + }, async (response) => { + let message = response.message; + let gh = await this._getGuildHandler(message.guild); + + if (message.editable && gh.musicPlayer) { + let next = (song) => { + message.edit('', new cmdLib.ExtendedRichEmbed('Now playing:') + .setDescription(`[${song.title}](${song.url})`) + .setImage(utils.YouTube.getVideoThumbnailUrlFromUrl(song.url)) + .setColor(0x00aaff)); + if (message.id !== message.channel.lastMessageID) { + gh.musicPlayer.removeListener('next', next); + message.delete(); + } + }; + gh.musicPlayer.on('next', next); + gh.musicPlayer.on('stop', () => { + gh.musicPlayer.off('next', next); + message.delete(); + }); + response.on('delete', () => gh.musicPlayer.off('next', next)); + } }) ); @@ -249,14 +278,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 +298,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 +309,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 +322,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,10 +368,12 @@ class MusicCommandModule extends cmdLib.CommandModule { .registerCommand(toggleRepeat) .registerCommand(saveMedia) .registerCommand(deleteMedia) - .registerCommand(savedMedia); + .registerCommand(savedMedia) + .registerCommand(volume) + .registerCommand(quality); } } Object.assign(exports, { - 'module': MusicCommandModule + module: MusicCommandModule }); diff --git a/lib/commands/MusicCommands/template.yaml b/commands/MusicCommands/template.yaml similarity index 84% rename from lib/commands/MusicCommands/template.yaml rename to commands/MusicCommands/template.yaml index 74d6b83..4728d0c 100644 --- a/lib/commands/MusicCommands/template.yaml +++ b/commands/MusicCommands/template.yaml @@ -17,7 +17,7 @@ play: url_invalid: > The URL you provided is not a valid YouTube video or Playlist URL. no_url: > - You need to provide an URL to a YouTube viceo or Playlist. + You need to provide an URL to a YouTube video or Playlist. no_voicechannel: > You need to join a VoiceChannel to request media playback. @@ -37,7 +37,7 @@ play_next: url_invalid: > The URL you provided is not a valid YouTube video or Playlist URL. no_url: > - You need to provide an URL to a YouTube viceo or Playlist. + You need to provide an URL to a YouTube video or Playlist. no_voicechannel: > You need to join a VoiceChannel to request media playback. @@ -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. diff --git a/lib/commands/ServerUtilityCommands/index.js b/commands/ServerUtilityCommands/index.js similarity index 80% rename from lib/commands/ServerUtilityCommands/index.js rename to commands/ServerUtilityCommands/index.js index b9c98ca..c3ef5fe 100644 --- a/lib/commands/ServerUtilityCommands/index.js +++ b/commands/ServerUtilityCommands/index.js @@ -1,4 +1,4 @@ -const cmdLib = require('../../CommandLib'); +const cmdLib = require('../../lib/command'); /** * This command module includes utility commands for the server. @@ -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); diff --git a/lib/commands/ServerUtilityCommands/template.yaml b/commands/ServerUtilityCommands/template.yaml similarity index 100% rename from lib/commands/ServerUtilityCommands/template.yaml rename to commands/ServerUtilityCommands/template.yaml diff --git a/lib/commands/UtilityCommands/index.js b/commands/UtilityCommands/index.js similarity index 95% rename from lib/commands/UtilityCommands/index.js rename to commands/UtilityCommands/index.js index 5071ad6..4e9ed9d 100644 --- a/lib/commands/UtilityCommands/index.js +++ b/commands/UtilityCommands/index.js @@ -1,4 +1,4 @@ -const cmdLib = require('../../CommandLib'); +const cmdLib = require('../../lib/command'); /** * Utility commands are all commands that allow the user to control the behaviour of the @@ -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}\``; }) ); diff --git a/lib/commands/UtilityCommands/template.yaml b/commands/UtilityCommands/template.yaml similarity index 100% rename from lib/commands/UtilityCommands/template.yaml rename to commands/UtilityCommands/template.yaml diff --git a/lib/api/graphql/AnilistApi/AnimeQuery.gql b/lib/api/AniListApi/graphql/AnimeQuery.gql similarity index 100% rename from lib/api/graphql/AnilistApi/AnimeQuery.gql rename to lib/api/AniListApi/graphql/AnimeQuery.gql diff --git a/lib/api/graphql/AnilistApi/CharacterQuery.gql b/lib/api/AniListApi/graphql/CharacterQuery.gql similarity index 100% rename from lib/api/graphql/AnilistApi/CharacterQuery.gql rename to lib/api/AniListApi/graphql/CharacterQuery.gql diff --git a/lib/api/graphql/AnilistApi/Fragments.yaml b/lib/api/AniListApi/graphql/Fragments.yaml similarity index 100% rename from lib/api/graphql/AnilistApi/Fragments.yaml rename to lib/api/AniListApi/graphql/Fragments.yaml diff --git a/lib/api/graphql/AnilistApi/MangaQuery.gql b/lib/api/AniListApi/graphql/MangaQuery.gql similarity index 100% rename from lib/api/graphql/AnilistApi/MangaQuery.gql rename to lib/api/AniListApi/graphql/MangaQuery.gql diff --git a/lib/api/graphql/AnilistApi/StaffQuery.gql b/lib/api/AniListApi/graphql/StaffQuery.gql similarity index 100% rename from lib/api/graphql/AnilistApi/StaffQuery.gql rename to lib/api/AniListApi/graphql/StaffQuery.gql diff --git a/lib/api/AnilistApi.js b/lib/api/AniListApi/index.js similarity index 99% rename from lib/api/AnilistApi.js rename to lib/api/AniListApi/index.js index e18efc4..9cf8d67 100644 --- a/lib/api/AnilistApi.js +++ b/lib/api/AniListApi/index.js @@ -1,7 +1,7 @@ const fetch = require('node-fetch'), fsx = require('fs-extra'), yaml = require('js-yaml'), - queryPath = './lib/api/graphql/AnilistApi', + queryPath = __dirname + '/graphql', alApiEndpoint = 'https://graphql.anilist.co'; async function getFragments() { diff --git a/lib/CommandLib.js b/lib/command/index.js similarity index 75% rename from lib/CommandLib.js rename to lib/command/index.js index a14b1a3..45fd34f 100644 --- a/lib/CommandLib.js +++ b/lib/command/index.js @@ -1,24 +1,44 @@ const Discord = require('discord.js'), yaml = require('js-yaml'), fsx = require('fs-extra'), - logging = require('./logging'), - config = require('../config.json'), - utils = require('./utils'); + logging = require('../utils/logging'), + config = require('../../config.json'), + xevents = require('../utils/extended-events'), + utils = require('../utils'); -const scopes = { - 'Global': 0, - 'User': 1, - 'Guild': 2 -}; +const scopes = new utils.Enum([ + 'Global', + 'User', + 'Guild' +]); + +/** + * The answer message object that is used for easyer access to events. + */ +class Response extends xevents.ExtendedEventEmitter { + + /** + * Constructor. + * @param content + */ + constructor(content) { + super(); + this.content = content; + this.message = null; + } +} class Answer { /** * Creates an new Answer object with _func as answer logic. - * @param func + * @param func {function} - the function to evaluate the answer + * @param [onSent] {function} - executed when the response was sent */ - constructor(func) { + constructor(func, onSent) { this._func = func; + this.listeners = onSent? {sent: onSent} : {}; + this.lastResponse = null; } /** @@ -27,14 +47,28 @@ class Answer { * @param message * @param kwargs * @param argsString - * @returns {Promise<*>} + * @returns {Promise} */ async evaluate(message, kwargs, argsString) { let result = this._func(message, kwargs, argsString); if (result instanceof Promise) - return await utils.resolveNestedPromise(result); + return this._getResponseInstance(await utils.resolveNestedPromise(result)); else - return result; + return this._getResponseInstance(result); + } + + /** + * Returns a response instance with listeners attached if defined. + * @param responseContent + * @returns {Response} + * @private + */ + _getResponseInstance(responseContent) { + this.lastResponse = new Response(responseContent); + + if (this.listeners) + this.lastResponse.addListeners(this.listeners); + return this.lastResponse; } } @@ -66,7 +100,7 @@ class Command { * @param message {Discord.Message} * @param kwargs {JSON} * @param argsString {String} The raw argument string. - * @returns {String} + * @returns {Response} */ async answer(message, kwargs, argsString) { return await this.answObj.evaluate(message, kwargs, argsString); @@ -104,7 +138,7 @@ class CommandHandler { * Handles the command and responds to the message. * @param commandMessage {String} * @param message {Discord.Message} - * @returns {Boolean | String | Promise} + * @returns {Response | Promise} */ handleCommand(commandMessage, message) { this._logger.debug(`Handling command ${commandMessage}`); @@ -129,14 +163,14 @@ class CommandHandler { return command.answer(message, kwargs, argsString); } else if (command) { this._logger.silly(`Permission ${command.permission} denied for command ${commandName}`); - return "You don't have permission for this command"; + return new Response("You don't have permission for this command"); } else { this._logger.silly(`Command ${commandName} not found.`); - return false; + return null; } } else { this._logger.silly(`No prefix found in command ${commandName}`); - return false; + return null; } } @@ -239,13 +273,16 @@ class ExtendedRichEmbed extends Discord.RichEmbed { * @param value */ setDescription(value) { - let croppedValue = value; - if (value.substring) - croppedValue = value.substring(0, 1024); - if (croppedValue.length < value.length) - croppedValue = croppedValue.replace(/\n.*$/g, ''); - if (croppedValue && croppedValue.replace(/\n/g, '').length > 0) - super.setDescription(croppedValue); + if (value) { + let croppedValue = value; + if (value.substring) + croppedValue = value.substring(0, 1024); + if (croppedValue.length < value.length && croppedValue.replace) + croppedValue = croppedValue.replace(/\n.*$/g, ''); + if (croppedValue && croppedValue.replace + && croppedValue.replace(/\n/g, '').length > 0) + super.setDescription(croppedValue); + } return this; } @@ -255,15 +292,16 @@ class ExtendedRichEmbed extends Discord.RichEmbed { * @param value */ addField(name, value) { - let croppedValue = value; - if (value.substring) - croppedValue = value.substring(0, 1024); - if (croppedValue.length < value.length) + if (name && value) { + let croppedValue = value; + if (value.substring) + croppedValue = value.substring(0, 1024); + if (croppedValue && croppedValue.length < value.length && croppedValue.replace) croppedValue = croppedValue.replace(/\n.*$/g, ''); - if (name && croppedValue - && croppedValue.replace(/\n/g, '').length > 0 && name.replace(/\n/g, '').length > 0) - super.addField(name, croppedValue); - + if (croppedValue && croppedValue.replace + && croppedValue.replace(/\n/g, '').length > 0 && name.replace(/\n/g, '').length > 0) + super.addField(name, croppedValue); + } return this; } } diff --git a/lib/database/index.js b/lib/database/index.js new file mode 100644 index 0000000..fc2f088 --- /dev/null +++ b/lib/database/index.js @@ -0,0 +1,175 @@ +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} + */ + 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} + * @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} + */ + 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} + */ + 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} + */ + 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} + */ + 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} + * @returns {Promise} + */ + 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} - the seperate values + * @returns {Promise} + */ + 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(); + } +} + +Object.assign(exports, { + Column: genericSql.Column, + Database: Database +}); diff --git a/lib/guilding.js b/lib/guilding.js deleted file mode 100644 index 63d03b3..0000000 --- a/lib/guilding.js +++ /dev/null @@ -1,102 +0,0 @@ -const music = require('./MusicLib'), - utils = require('./utils'), - config = require('../config.json'), - sqliteAsync = require('./sqliteAsync'), - logging = require('./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} - */ - 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 -}); diff --git a/lib/guilds/index.js b/lib/guilds/index.js new file mode 100644 index 0000000..cbc771f --- /dev/null +++ b/lib/guilds/index.js @@ -0,0 +1,195 @@ +const music = require('../music'), + dblib = require('../database'), + logging = require('../utils/logging'); + +/** + * 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.'); + } + + /** + * Returns the value of the column where the key has the value keyvalue + * @param table {String} - the table name + * @param column {String} - the name of the column + * @param keyname {String} - the name of the key + * @param keyvalue {*} - the value of the key + * @returns {Promise<*>} + */ + async getSingleValue(table, column, keyname, keyvalue) { + let result = await this.get(this.sql.select(table, false, column, + this.sql.where(this.sql.parameter(1), '=', this.sql.parameter(2))), + [keyname, keyvalue]); + if (result) + return result[column]; + else + return null; + } + + /** + * Returns either the whole table or a limited version + * @param tablename + * @param limit + * @returns {Promise} + */ + async getTableContent(tablename, limit) { + if (limit) + return await this.all(this.sql.select(tablename, false, ['*'], [], [ + this.sql.limit(limit) + ])); + else + return await this.all(this.sql.select(tablename, false, ['*'], [], [])); + } + + /** + * 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} + */ + 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} + */ + 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} + */ + 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} + */ + 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 +}); diff --git a/lib/MessageLib.js b/lib/message/index.js similarity index 74% rename from lib/MessageLib.js rename to lib/message/index.js index 3642b43..ea61aba 100644 --- a/lib/MessageLib.js +++ b/lib/message/index.js @@ -1,7 +1,7 @@ -const cmdLib = require('./CommandLib'), - config = require('../config.json'), +const cmdLib = require('../command'), + config = require('../../config.json'), Discord = require('discord.js'), - logging = require('./logging'), + logging = require('../utils/logging'), promiseWaterfall = require('promise-waterfall'); /* eslint no-useless-escape: 0 */ @@ -23,6 +23,7 @@ class MessageHandler { this.guildCmdHandler = new cmdLib.CommandHandler(config.prefix, cmdLib.CommandScopes.Guild); this.userRates = {}; + this.registeredResponses = {}; this._registerEvents(); } @@ -68,7 +69,8 @@ class MessageHandler { * @private */ _registerEvents() { - this.logger.debug('Registering message event...'); + this.logger.debug('Registering message events...'); + this.discordClient.on('message', async (msg) => { this.logger.verbose(`<${msg.guild? msg.channel.name+'@'+msg.guild.name : 'PRIVATE'}> ${msg.author.tag}: ${msg.content}`); if (msg.author !== this.discordClient.user @@ -81,6 +83,32 @@ class MessageHandler { this.logger.debug('Executed command sequence'); } }); + + this.discordClient.on('messageReactionAdd', (messageReaction, user) => { + let responseInstance = this.registeredResponses[messageReaction.message]; + if (responseInstance) + responseInstance.emit('reactionAdd', messageReaction, user); + }); + + this.discordClient.on('messageReactionRemove', (messageReaction, user) => { + let responseInstance = this.registeredResponses[messageReaction.message]; + if (responseInstance) + responseInstance.emit('reactionRemove', messageReaction, user); + }); + + this.discordClient.on('messageReactionRemoveAll', (message) => { + let responseInstance = this.registeredResponses[message]; + if (responseInstance) + responseInstance.emit('reactionRemoveAll', message); + }); + + this.discordClient.on('messageDelete', (message) => { + let responseInstance = this.registeredResponses[message]; + if (responseInstance) { + responseInstance.on('delete', message); + delete this.registeredResponses[message]; + } + }); } /** @@ -124,9 +152,9 @@ class MessageHandler { this.logger.silly(`globalResult: ${globalResult}, scopeResult: ${scopeResult}`); if (scopeResult) - this._answerMessage(message, scopeResult); + await this._answerMessage(message, scopeResult); else if (globalResult) - this._answerMessage(message, globalResult); + await this._answerMessage(message, globalResult); } catch (err) { this.logger.verbose(err.message); this.logger.silly(err.stack); @@ -149,16 +177,25 @@ class MessageHandler { /** * Answers * @param message {Discord.Message} - * @param answer {String | Discord.RichEmbed} + * @param response {Response} * @private */ - _answerMessage(message, answer) { - this.logger.debug(`Sending answer ${answer}`); - if (answer) - if (answer instanceof Discord.RichEmbed) - message.channel.send('', answer); + async _answerMessage(message, response) { + this.logger.debug(`Sending answer ${response.content}`); + if (response && response.content) { + let responseMessage = null; + + if (response.content instanceof Discord.RichEmbed) + responseMessage = await message.channel.send('', response.content); else - message.channel.send(answer); + responseMessage = await message.channel.send(response.content); + + if (response.hasListeners) + this.registeredResponses[responseMessage] = response; + + response.message = responseMessage; + response.emit('sent', response); + } } /** diff --git a/lib/MusicLib.js b/lib/music/index.js similarity index 78% rename from lib/MusicLib.js rename to lib/music/index.js index d6558da..9375876 100644 --- a/lib/MusicLib.js +++ b/lib/music/index.js @@ -1,17 +1,23 @@ const ytdl = require("ytdl-core"), ypi = require('youtube-playlist-info'), yttl = require('get-youtube-title'), - config = require('../config.json'), - utils = require('./utils.js'), - logging = require('./logging'), + config = require('../../config.json'), + utils = require('../utils'), + xevents = require('../utils/extended-events'), + logging = require('../utils/logging'), ytapiKey = config.api.youTubeApiKey; /** * The Music Player class is used to handle music playing tasks on Discord Servers (Guilds). * @type {MusicPlayer} */ -class MusicPlayer { +class MusicPlayer extends xevents.ExtendedEventEmitter { + /** + * Constructor + * @param [voiceChannel] {Discord.VoiceChannel} + */ constructor(voiceChannel) { + super(); this.conn = null; this.disp = null; this.queue = []; @@ -20,10 +26,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; } /** @@ -43,8 +50,14 @@ class MusicPlayer { this.voiceChannel = voiceChannel; this._logger.verbose(`Connecting to voiceChannel ${this.voiceChannel.name}`); let connection = await this.voiceChannel.join(); + + connection.on('error', (err) => { + this._logger.error(err.message); + this._logger.debug(err.stack); + }); this._logger.info(`Connected to Voicechannel ${this.voiceChannel.name}`); this.conn = connection; + this.emit('connected'); } /** @@ -55,6 +68,7 @@ class MusicPlayer { this.repeat = value; if (this.current) this.queue.push(this.current); + this.emit('listenOnRepeat', this.repeat); } /** @@ -123,7 +137,7 @@ class MusicPlayer { * If the url is a playlist, the videos of the playlist are fetched and put * in the queue. For each song the title is saved in the queue too. * @param url {String} - * @param playnext {Boolean} + * @param [playnext] {Boolean} */ async playYouTube(url, playnext) { let plist = utils.YouTube.getPlaylistIdFromUrl(url); @@ -134,7 +148,7 @@ class MusicPlayer { let firstSongTitle = null; try { firstSongTitle = await this.getVideoName(firstSong); - } catch(err) { + } catch (err) { if (err.message !== 'Not found') { this._logger.warn(err.message); this._logger.debug(err.stack); @@ -151,35 +165,47 @@ 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)}); - - this.disp = this.conn.playStream(ytdl(url, - {filter: 'audioonly', quality: this.quality, liveBuffer: config.music.livePuffer || 20000}), - {volume: this.volume}); - - this.disp.on('end', (reason) => { // end event triggers the next song to play when the reason is not stop - if (reason !== 'stop') { - this.playing = false; + if (this.repeat) + this.queue.push(this.current); + let toggleNext = () => { + if (this.queue.length > 0) { + this.disp = null; + this.current = this.queue.shift(); + this.emit('next', this.current); + this.playYouTube(this.current.url).catch((err) => this._logger.warn(err.message)); + } else { + this.stop(); this.current = null; - if (this.queue.length > 0) { - this.current = this.queue.shift(); - if (this.repeat) // listen on repeat - this.queue.push(this.current); - this.playYouTube(this.current.url).catch((err) => this._logger.warn(err.message)); - } else { - this.stop(); - } + this.playing = false; } - }); - this.playing = true; + }; + try { + this.disp = this.conn.playStream(await ytdl(url, + {filter: 'audioonly', quality: this.quality, liveBuffer: this.liveBuffer}, {volume: this.volume})); + this.disp.on('error', (err) => { + this._logger.error(err.message); + this._logger.debug(err.stack); + }); + this.disp.on('end', (reason) => { // end event triggers the next song to play when the reason is not stop + if (reason !== 'stop') + toggleNext(); + }); + this.playing = true; + } catch (err) { + this._logger.verbose(err.message); + this._logger.silly(err.stack); + toggleNext(); + } } else { this._logger.debug(`Added ${url} to the queue`); if (playnext) @@ -193,7 +219,7 @@ class MusicPlayer { /** * Gets the name of the YouTube Video at url * @param url {String} - * @returns {Promise<>} + * @returns {Promise} */ getVideoName(url) { return new Promise((resolve, reject) => { @@ -214,12 +240,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."); - } } /** @@ -229,7 +252,7 @@ class MusicPlayer { this._logger.verbose("Pausing music..."); if (this.disp !== null) this.disp.pause(); - else + else this._logger.warn("No dispatcher found"); } @@ -241,7 +264,7 @@ class MusicPlayer { this._logger.verbose("Resuming music..."); if (this.disp !== null) this.disp.resume(); - else + else this._logger.warn("No dispatcher found"); } @@ -274,6 +297,7 @@ class MusicPlayer { } catch (error) { this._logger.verbose(JSON.stringify(error)); } + this.emit('stop'); } /** @@ -284,7 +308,9 @@ class MusicPlayer { skip() { this._logger.debug("Skipping song"); if (this.disp !== null) { - this.disp.end(); + let disp = this.disp; + this.disp = null; + disp.end(); } else { this.playing = false; if (this.queue.length > 0) { @@ -297,6 +323,7 @@ class MusicPlayer { this.stop(); } } + this.emit('skip', this.current); } /** @@ -312,6 +339,7 @@ class MusicPlayer { */ shuffle() { this.queue = utils.shuffleArray(this.queue); + this.emit('shuffle'); } /** @@ -319,6 +347,7 @@ class MusicPlayer { */ clear() { this.queue = []; + this.emit('clear'); } } diff --git a/lib/utils/extended-events.js b/lib/utils/extended-events.js new file mode 100644 index 0000000..fc15186 --- /dev/null +++ b/lib/utils/extended-events.js @@ -0,0 +1,76 @@ +const logging = require('../utils/logging'), + EventEmitter = require('events'); + +/** + * Extends the event emitter with some useful features. + */ +class ExtendedEventEmitter extends EventEmitter { + + /** + * Constructor. + * @param [name] {String} + */ + constructor(name) { + super(); + this._logger = new logging.Logger(`${name}-${this.constructor.name}`); + this._registerDefault(); + } + + /** + * Registeres the error event to the logger so it won't crash the bot. + * @private + */ + _registerDefault() { + this.on('error', (err) => { + this._logger.error(err.message); + this._logger.debug(err.stack); + }); + } + + /** + * Adds an object of events with listeners to the bot. + * @param eventListenerObject + * @returns {ExtendedEventEmitter} + */ + addListeners(eventListenerObject) { + for (let [event, listener] of Object.entries(eventListenerObject)) + this.on(event, listener); + return this; + } + + /** + * Returns all registered events. + * @returns {*|Array|string[]} + */ + get events() { + return this.eventNames(); + } + + /** + * Wrapper around getMaxListeners function + * @returns {*|number} + */ + get maxListeners() { + return this.getMaxListeners(); + } + + /** + * Wrapper around setMaxListeners function. + * @param n + * @returns {this | this | Cluster | *} + */ + set maxListeners(n) { + return this.setMaxListeners(n); + } + + /** + * Returns if the emitter has additional listeners apart from the error listener. + */ + get hasListeners() { + return this.events.count > 1; + } +} + +Object.assign(exports, { + ExtendedEventEmitter: ExtendedEventEmitter, +}); diff --git a/lib/utils/genericSql.js b/lib/utils/genericSql.js new file mode 100644 index 0000000..607fe1f --- /dev/null +++ b/lib/utils/genericSql.js @@ -0,0 +1,413 @@ +/** + * Returns types based on the database. + */ +class GenericTypes { + /** + * Constructor. + * @param database {String} + */ + constructor(database) { + this.database = database; + } + + get null() { + switch(this.database) { + case 'postgresql': + case 'sqlite': + default: + return 'NULL'; + } + } + + get integer() { + switch(this.database) { + case 'postgresql': + case 'sqlite': + default: + return 'INTEGER'; + } + } + + get real() { + switch(this.database) { + case 'sqlite': + return 'REAL'; + case 'postgresql': + default: + return 'FLOAT'; + } + } + + get text() { + switch (this.database) { + case 'postgresql': + case 'sqlite': + default: + return 'TEXT'; + } + } + + get varchar() { + switch (this.database) { + case 'postgresql': + case 'sqlite': + default: + return 'VARCHAR'; + } + } + + get date() { + switch (this.database) { + case 'postgresql': + case 'sqlite': + default: + return 'DATE'; + } + } + + get datetime() { + switch (this.database) { + case 'postgresql': + return 'TIMESTAMP'; + case 'sqlite': + default: + return 'DATETIME'; + } + } + + get serial() { + switch (this.database) { + case 'sqlite': + return 'INTEGER AUTOINCREMENT NOT NULL'; + case 'postgresql': + default: + return 'SERIAL'; + } + } + + 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} + */ + getVarchar(length) { + return `${this.varchar}(${length})`; + } +} + +/** + * Returns sql statements based on the database. + */ +class GenericSql { + /** + * Constructor. + * @param database {String} + */ + constructor(database) { + this.database = database; + this.types = new GenericTypes(database); + this.constraints = { + primaryKey: 'PRIMARY KEY', + notNull: 'NOT NULL', + unique: 'UNIQUE', + like: 'LIKE', + exists: 'EXISTS', + and: 'AND', + or: 'OR', + in: 'IN', + 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 '?'; + } + } + + /** + * A sum selector - calculates the sum of all values of the column + * @param colname {String} - the name of the column where the sum is selected. + * @returns {string} + */ + sum(colname) { + switch (this.database) { + case 'postgresql': + case 'sqlite': + return `SUM(${colname})`; + } + } + + /** + * A avg selector - selects the average + * @param colname {String} - the name of the column where the avg value is selected. + * @returns {string} + */ + avg(colname) { + switch (this.database) { + case 'postgresql': + case 'sqlite': + return `AVG(${colname})`; + } + } + + /** + * A min selector - selects the minimum + * @param colname {String} - the name of the column where the min value is selected. + * @returns {string} + */ + min(colname) { + switch (this.database) { + case 'postgresql': + case 'sqlite': + return `MIN(${colname})`; + } + } + + /** + * A max selector - selects the maximum + * @param colname {String} - the name of the column where the max value is selected. + * @returns {string} + */ + max(colname) { + switch (this.database) { + case 'postgresql': + case 'sqlite': + return `MAX(${colname})`; + } + } + + /** + * A count selector - counts the results + * @param colname {String} - the name of the column to be counted. + * @returns {string} + */ + count(colname) { + switch (this.database) { + case 'postgresql': + case 'sqlite': + return `COUNT(${colname}) count`; + } + } + + /** + * A default constraint + * @param expression {String} - the expression to generate the default value. + * @returns {string} + */ + default(expression) { + switch (this.database) { + case 'postgresql': + case 'sqlite': + return `DEFAULT ${expression}`; + } + } + + /** + * A where statement + * @param row {String} - the row + * @param operator {String} - the comparison operator + * @param comparator {String} the value or row to compare to + */ + and(row, operator, comparator) { + switch (this.database) { + case 'postgresql': + case 'sqlite': + return `AND ${row} ${operator} ${comparator}`; + } + } + + /** + * A or statement + * @param row {String} - the row + * @param operator {String} - the comparison operator + * @param comparator {String} the value or row to compare to + */ + or(row, operator, comparator) { + switch (this.database) { + case 'postgresql': + case 'sqlite': + return `OR ${row} ${operator} ${comparator}`; + } + } + + /** + * A where statement + * @param row {String} - the row + * @param operator {String} - the comparison operator + * @param comparator {String} the value or row to compare to + */ + where(row, operator, comparator) { + switch (this.database) { + case 'postgresql': + case 'sqlite': + return `WHERE ${row} ${operator} ${comparator}`; + } + } + + /** + * A limit statement. + * @param count {Number} - the number of rows to return + * @returns {string} + */ + limit(count) { + switch (this.database) { + case 'postgresql': + case 'sqlite': + return `LIMIT ${count}`; + } + } + + /** + * Create Table statement + * @param table {String} + * @param rows {Array} + * @returns {string} + */ + createTable(table, rows) { + switch (this.database) { + case 'postgresql': + case 'sqlite': + return `CREATE TABLE ${table} (${rows.map(x => x.sql).join(',')})`; + } + } + + /** + * Create Table if it doesn't exist statement + * @param table {String} + * @param columns {Array} + * @returns {string} + */ + createTableIfNotExists(table, columns) { + switch (this.database) { + case 'postgresql': + case 'sqlite': + return `CREATE TABLE IF NOT EXISTS ${table} (${columns.map(x => x.sql).join(',')})`; + } + } + + /** + * Insert into the table. + * @param table {String} - the table name + * @param colValueObj {Object} - an object with keys as columnnames and values as columnvalues + * @returns {string} + */ + insert(table, colValueObj) { + let rownames = Object.keys(colValueObj); + let values = Object.values(colValueObj); + switch (this.database) { + case 'postgresql': + case 'sqlite': + return `INSERT INTO ${table} (${rownames.join(',')}) values (${values.join(',')})`; + } + } + + /** + * 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 ...] + * @returns {string} + */ + update(table, colValueObj, conditions) { + if (!(conditions instanceof Array)) + conditions = [conditions]; + switch (this.database) { + case 'postgresql': + case 'sqlite': + return `UPDATE ${table} SET ${Object.entries(colValueObj).map(x => `${x[0]} = ${x[1]}`).join(',')} ${conditions.join(' ')}`; + } + } + + /** + * 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 + * @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} - 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 `DELETE FROM ${table} ${conditions.join(' ')}`; + } + } +} + +class Column { + /** + * Create a column for usage in the generic sql statements + * @param name {String} + * @param [type] {String} + * @param [constraints] {Array} + */ + constructor(name, type, constraints) { + this.name = name; + this.type = type; + this.constraints = constraints || []; + if (!(constraints instanceof Array)) + this.constraints = [constraints]; + } + + /** + * Sets the datatype of the row. + * @param constraint {String} + */ + addConstraint(constraint) { + this.constraints.push(constraint); + } + + get sql() { + return `${this.name} ${this.type} ${this.constraints.join(' ')}`; + } +} + +Object.assign(exports, { + GenericSql: GenericSql, + GenericTypes: GenericTypes, + Column: Column +}); diff --git a/lib/utils.js b/lib/utils/index.js similarity index 96% rename from lib/utils.js rename to lib/utils/index.js index 620689c..14e2fb5 100644 --- a/lib/utils.js +++ b/lib/utils/index.js @@ -47,7 +47,7 @@ function objectDeepFind (object, attributePath) { /** * Shuffles an array with Fisher-Yates Shuffle * @param array - * @returns {Array}# + * @returns {Array} */ exports.shuffleArray = function(array) { let currentIndex = array.length, temporaryValue, randomIndex; @@ -129,6 +129,18 @@ async function resolveNestedPromise (promise) { /* Classes */ +class Enum { + /** + * Constructor. + * @param symbols {Array} + */ + constructor(symbols) { + for (let symbol in symbols) + this[symbol] = symbols; + Object.freeze(this); + } +} + class YouTube { /** * returns if an url is a valid youtube url (without checking for an entity id) @@ -279,5 +291,6 @@ Object.assign(exports, { getExtension: getFileExtension, getFileExtension: getFileExtension, objectDeepFind: objectDeepFind, - Cleanup: Cleanup + Cleanup: Cleanup, + Enum: Enum }); diff --git a/lib/logging.js b/lib/utils/logging.js similarity index 100% rename from lib/logging.js rename to lib/utils/logging.js diff --git a/lib/sqliteAsync.js b/lib/utils/sqliteAsync.js similarity index 100% rename from lib/sqliteAsync.js rename to lib/utils/sqliteAsync.js diff --git a/lib/WebLib.js b/lib/web/index.js similarity index 92% rename from lib/WebLib.js rename to lib/web/index.js index 6bbcc96..ed3b9ca 100644 --- a/lib/WebLib.js +++ b/lib/web/index.js @@ -4,21 +4,22 @@ const express = require('express'), compression = require('compression'), md5 = require('js-md5'), sha512 = require('js-sha512'), - logging = require('./logging'), + 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('../lib/utils'); + 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('./lib/api/graphql/schema.gql', 'utf-8')); + this.schema = buildSchema(fs.readFileSync('./web/api/graphql/schema.gql', 'utf-8')); this.root = {}; this._logger = new logging.Logger(this); } @@ -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]), diff --git a/package.json b/package.json index 56a023f..b667b60 100644 --- a/package.json +++ b/package.json @@ -8,43 +8,47 @@ }, "dependencies": { "args-parser": "1.1.0", - "body-parser": "1.18.3", - "compression": "1.7.3", + "body-parser": "1.19.0", + "compression": "1.7.4", "connect-sqlite3": "0.9.11", "cors": "2.8.5", - "discord.js": "11.4.2", + "discord.js": "^11.5.1", "express": "4.16.4", + "discord.js": "11.5.1", + "express": "4.17.1", "express-compile-sass": "4.0.0", - "express-graphql": "0.7.1", - "express-session": "1.15.6", + "express-graphql": "0.9.0", + "express-session": "1.16.2", "ffmpeg-binaries": "4.0.0", - "fs-extra": "7.0.1", + "fs-extra": "8.1.0", "get-youtube-title": "1.0.0", - "graphql": "14.1.1", + "graphql": "14.5.8", "js-md5": "0.7.3", "js-sha512": "0.8.0", - "node-fetch": "^2.3.0", - "node-sass": "4.11.0", - "opusscript": "0.0.6", + "js-yaml": "latest", + "node-fetch": "2.6.0", + "node-opus": "0.3.2", + "node-sass": "4.12.0", + "pg": "7.12.1", "promise-waterfall": "0.1.0", - "pug": "2.0.3", - "sqlite3": "4.0.6", + "pug": "2.0.4", + "sqlite3": "4.1.0", "winston": "3.2.1", - "winston-daily-rotate-file": "3.8.0", + "winston-daily-rotate-file": "4.2.1", "youtube-playlist-info": "1.1.2", - "ytdl-core": "0.29.1", - "js-yaml": "latest" + "ytdl-core": "^1.0.0", }, "devDependencies": { - "assert": "1.4.1", + "assert": "2.0.0", "chai": "4.2.0", - "mocha": "6.0.2", - "nyc": "13.3.0", + "mocha": "6.2.1", + "nyc": "14.1.1", "rewire": "4.0.1", - "sinon": "7.2.6", - "eslint-plugin-graphql": "3.0.3", - "eslint": "5.15.0", - "eslint-plugin-promise": "4.0.1" + "sinon": "7.5.0", + "eslint-plugin-graphql": "3.1.0", + "eslint": "6.5.1", + "eslint-plugin-promise": "4.2.1", + "opusscript": "0.0.7" }, "eslintConfig": { "parserOptions": { diff --git a/test/test.js b/test/test.js index 5eb5b33..032c1b9 100644 --- a/test/test.js +++ b/test/test.js @@ -13,7 +13,7 @@ mockobjects.mockLogger = { }; describe('lib/utils', function() { - const utils = require('../lib/utils.js'); + const utils = require('../lib/utils/index.js'); describe('#getSplitDuration', function() { it('returns an object from milliseconds', function() { @@ -28,7 +28,7 @@ describe('lib/utils', function() { it('returns the correct extension for a filename', function(done) { assert(utils.getExtension('test.txt') === '.txt'); assert(utils.getExtension('test.tar.gz') === '.gz'); - assert(utils.getExtension('../lib/utils.js') === '.js'); + assert(utils.getExtension('../lib/index.js') === '.js'); assert(utils.getExtension('.gitignore') === '.gitignore'); done(); }); @@ -179,7 +179,7 @@ describe('lib/utils', function() { describe('lib/music', function() { - const music = rewire('../lib/MusicLib'); + const music = rewire('../lib/music'); const Readable = require('stream').Readable; music.__set__("logger", mockobjects.mockLogger); @@ -329,8 +329,8 @@ describe('lib/music', function() { }); }); -describe('lib/CommandLib', function() { - let cmdLib = require('../lib/CommandLib'); +describe('lib/command', function() { + let cmdLib = require('../lib/command'); describe('Answer', function() { @@ -373,8 +373,8 @@ describe('lib/CommandLib', function() { }); }); -describe('lib/MessageLib', function() { - let msgLib = require('../lib/MessageLib'); +describe('lib/message', function() { + let msgLib = require('../lib/message'); describe('MessageHandler', function() { it ('parses a command syntax', function() { diff --git a/lib/api/graphql/schema.gql b/web/api/graphql/schema.gql similarity index 100% rename from lib/api/graphql/schema.gql rename to web/api/graphql/schema.gql diff --git a/web/http/sass/vars.sass b/web/http/sass/vars.sass index c367af8..ee76512 100644 --- a/web/http/sass/vars.sass +++ b/web/http/sass/vars.sass @@ -2,8 +2,8 @@ $cPrimary: #fff $cPrimaryVariant: #4c10a5 $cSecondary: #c889f5 $cSecondaryVariant: #740bce -$cBackground: #77f -$cBackgroundVariant: #55b +$cBackground: #1f1f2f +$cBackgroundVariant: #3f3f55 $cSurface: #fff $cSurfaceVariant: #000 $cError: #f59289 @@ -28,4 +28,4 @@ $cInfo: #890 $cWarn: #a60 $cError: #a00 -$fNormal: Ubuntu, sans-serif \ No newline at end of file +$fNormal: Ubuntu, sans-serif