From 1b08edd2783c273128e5f42bedf1e490bcd2ec79 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sat, 9 Mar 2019 21:01:42 +0100 Subject: [PATCH] Bug Fixes, Utility Classes - fixed bugs in `AniListApi` - fixed typo in the music commands template - added logging of uncaught promise rejections - added generic SQL Statement classes - updated README --- CHANGELOG.md | 4 + README.md | 8 + bot.js | 7 + commands/MusicCommands/template.yaml | 4 +- lib/api/AniListApi/index.js | 2 +- lib/music/index.js | 10 + lib/utils/genericSql.js | 349 +++++++++++++++++++++++++++ package.json | 6 +- 8 files changed, 384 insertions(+), 6 deletions(-) create mode 100644 lib/utils/genericSql.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 43e4648..b22563a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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 ### Changed @@ -16,10 +17,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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 ### 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 ## [0.11.0-beta] - 2019-03-03 ### Changed diff --git a/README.md b/README.md index 42ab00f..7be1884 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: diff --git a/bot.js b/bot.js index e45269c..d2135b8 100644 --- a/bot.js +++ b/bot.js @@ -248,10 +248,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/commands/MusicCommands/template.yaml b/commands/MusicCommands/template.yaml index 74d6b83..8f89469 100644 --- a/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. diff --git a/lib/api/AniListApi/index.js b/lib/api/AniListApi/index.js index 0c46c90..9cf8d67 100644 --- a/lib/api/AniListApi/index.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/AnilistApi/graphql', + queryPath = __dirname + '/graphql', alApiEndpoint = 'https://graphql.anilist.co'; async function getFragments() { diff --git a/lib/music/index.js b/lib/music/index.js index 4090f58..b877e36 100644 --- a/lib/music/index.js +++ b/lib/music/index.js @@ -160,11 +160,20 @@ class MusicPlayer { } else if (!this.playing || !this.disp) { this._logger.debug(`Playing ${url}`); this.current = ({'url': url, 'title': await this.getVideoName(url)}); + if (this.repeat) + this.queue.push(this.current); this.disp = this.conn.playStream(ytdl(url, {filter: 'audioonly', quality: this.quality, liveBuffer: config.music.livePuffer || 20000}), {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); + }); + 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; @@ -192,6 +201,7 @@ class MusicPlayer { /** * Gets the name of the YouTube Video at url + * TODO: ytdl.getInfo * @param url {String} * @returns {Promise<>} */ diff --git a/lib/utils/genericSql.js b/lib/utils/genericSql.js new file mode 100644 index 0000000..c92ac4d --- /dev/null +++ b/lib/utils/genericSql.js @@ -0,0 +1,349 @@ +/** + * 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'; + } + } + + /** + * 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' + }; + } + + /** + * 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})`; + } + } + + /** + * 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}`; + } + } + + /** + * 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} - conditions for the update row selection (WHERE ... [OR ...][AND ...] + * @returns {string} + */ + update(table, colValueObj, 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} - the rows to select + * @param conditions {Array} - conditions for the row selection (WHERE ... [OR ...][AND ...] + * @param operations {Array} - operations on the selected rows + * @returns {String} + */ + select(table, distinct, colnames, conditions, operations) { + switch (this.database) { + case 'postgresql': + case 'sqlite': + return `SELECT${distinct? ' ' + distinct : ''} ${colnames.join(' ')} FROM ${table} ${conditions.join(' ')} ${operations.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 || []; + } + + /** + * 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: GenericSql, + Column: Column +}); diff --git a/package.json b/package.json index 56a023f..4d7f224 100644 --- a/package.json +++ b/package.json @@ -23,17 +23,17 @@ "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", - "opusscript": "0.0.6", + "node-opus": "0.3.1", "promise-waterfall": "0.1.0", "pug": "2.0.3", "sqlite3": "4.0.6", "winston": "3.2.1", "winston-daily-rotate-file": "3.8.0", "youtube-playlist-info": "1.1.2", - "ytdl-core": "0.29.1", - "js-yaml": "latest" + "ytdl-core": "0.29.1" }, "devDependencies": { "assert": "1.4.1",