UNPKG

twitch-chat-bot

Version:

an attempt to provide a generic, but highly-configurable platform for developers intending to create Twitch chat bots in Node.js

784 lines (642 loc) 15.2 kB
/** * twitch-chat-bot * * Copyright (c) 2020 WildcardSearch */ const mysql = require('mysql'); const { errorCategories, errorCodes, warningCodes, } = require("./error-codes-mysql.js"); const TwitchChatBotDatabaseService = require("./service.js"); class MYSQL_DatabaseIntegration extends TwitchChatBotDatabaseService { integrationId = "MYSQL"; /** * connect to the db * * @return void */ connect() { this.errorHandler.registerCategories(errorCategories); this.errorHandler.registerCodes(warningCodes); this.errorHandler.registerCodes(errorCodes); this.fields = {}; this.fieldList = []; if (typeof this.options.database !== "object" || typeof this.options.database.table !== "string" && this.options.database.table.length === 0) { return; } if (typeof this.options.database.credentials !== "object" || Object.keys(this.options.database.credentials).length < 4) { return; } this.credentials = this.options.database.credentials; this.table = this.options.database.table; this.connection = mysql.createConnection(this.credentials); this.connection.connect(this.checkConnection.bind(this)); } /** * check in with MySQL * * @param String * @return void */ checkConnection(error) { if (error) { this.errorHandler.throwError("ERROR_DB_MYSQL_CONNECT_FAIL"); this.onConnectionFail(error); return; } this.bot.log("Connected to MYSQL database"); this.valid = true; this.getAllTables(this.checkTableInstall.bind(this)); } /** * start checking component install * * @param String * @param Object * @return void */ checkTableInstall(err, result) { if (err) { this.errorHandler.throwError("ERROR_DB_MYSQL_INSTALL_TABLE_FAIL"); return; } if (this.tableExists(this.table) !== true) { this.install(this.checkFieldInstall.bind(this)); } else { this.checkFieldInstall(); } } /* install */ /** * continue with component installation * * @return void */ checkFieldInstall() { this.getAllFields(this.finalizeInstall.bind(this)); } /** * finish up install * * @return void */ finalizeInstall() { this.installField({ key: "id", type: "id", }); this.registerField({ key: "timestamp", type: "number", }); this.bot.log("MySQL Database Installation: 100%", "connected"); this.onConnect(); } /* initialize */ /** * initialize the db; grab the last stream; and call the TwitchChatBotModule ready event * * @param Function * @param Function * @return void */ init(onSuccess, onFail) { var n = Date.now() - this.options.newStreamMaxDelay; if (typeof onSuccess !== "function") { onSuccess = () => {}; } if (typeof onFail !== "function") { onFail = () => {}; } this.simpleSelect("*", `lastlive > ${n}`, { callback: function checkForCrash(err, result) { if (err) { this.errorHandler.throwError("ERROR_DB_MYSQL_READ_DATA_FAIL"); return; } if (typeof result !== "object" || Array.isArray(result) !== true || result.length === 0) { this.bot.log("new stream"); this.insertStreamRecord(onSuccess, onFail); return; } this.bot.log("recovering from crash"); this.bot.log(result); this.bot.streamId = result[0].id; this.bot.streamData = this.buildStreamData(result[0]); onSuccess(); }.bind(this)}); } /* database access */ /** * start a fresh stream record * * @param Function * @param Function * @return void */ insertStreamRecord(onSuccess, onFail) { let fields = this.buildFieldListSQL(); this.bot.log(`INSERT INTO ${this.table} ${fields};`); this.connection.query(`INSERT INTO ${this.table} ${fields};`, function (err, result) { if (err) { this.errorHandler.throwError("ERROR_DB_MYSQL_INSERT_DATA_FAIL"); onFail(err); return; } this.bot.log(result); if (typeof result !== "object" || typeof result.insertId !== "number" || result.insertId <= 0) { this.errorHandler.throwError("ERROR_DB_MYSQL_READ_ID_FAIL"); onFail(err); return; } this.bot.streamId = result.insertId; this.bot.streamData = this.buildFieldListObject(); onSuccess(); }.bind(this)); } /** * store information from internal/external modules * * @param Object * @return void */ updateStreamInfo(fields) { fields.timestamp = Date.now(); this.updateQuery(fields, `id = ${this.bot.streamId}`); } /** * perform a query using passed info * * @param Array * @param String * @param Object * @return void */ simpleSelect(fields, where, options) { let whereClause = "", orderBy = "", fieldClause = "*", sep = "", queryString = ""; if (typeof fields === "object" && Array.isArray(fields) === true && fields.length > 0) { fieldClause = ""; for (const field of fields) { fieldClause += `${sep}${field}`; sep = ", "; } } if (typeof where === "string" && where.length > 0) { whereClause = ` WHERE ${where}`; } if (typeof options === "object") { if (typeof options.callback !== "function") { options.callback = () => {}; } if (typeof options.orderBy === "string" && options.orderBy.length > 0) { orderBy = ` ORDER ${options.orderBy}`; if (typeof options.orderDir === "string" && options.orderDir.length > 0 && ["asc", "desc"].includes(options.orderDir.toLowerCase()) === true) { orderBy += ` ${options.orderDir.toUpperCase()}`; } } } else { return false; } queryString = `SELECT ${fieldClause} FROM ${this.table}${whereClause}${orderBy}`; this.connection.query(queryString, options.callback); } /** * perform an update query using passed info * * @param Object * @param String * @param Object * @return void */ updateQuery(fields, where, options) { let queryString = "", fieldList = "", sep = ""; fieldList = this.buildFieldList(fields); if (fieldList === false || fieldList.length === 0) { return false; } if (typeof options !== "object") { options = {}; } if (typeof options.callback !== "function") { options.callback = ()=>{}; } queryString = `UPDATE ${this.table} SET ${fieldList} WHERE ${where}`; this.connection.query(queryString, options.callback); } /** * retrieve a list of all database tables * * @param Function * @return void */ getAllTables(callback) { this.dbTableList = []; this.connection.query("SHOW TABLES", (err, result) => { if (err) { this.errorHandler.throwError("ERROR_DB_MYSQL_READ_TABLES_FAIL"); return; } this.storeTables(result); if (typeof callback === "function") { callback(err, result); } }); } /** * retrieve a list of all database columns * * @param Function * @return void */ getAllFields(callback) { this.dbFieldList = []; this.dbFields = {}; this.connection.query("SHOW COLUMNS FROM `"+this.table+"`", (err, result) => { this.storeFields(err, result); if (typeof callback === "function") { callback(err, result); } }); } /** * store database tables * * @param Object * @return void */ storeTables(result) { if (typeof result !== "object" || Array.isArray(result) !== true || result.length === 0) { this.errorHandler.throwError("ERROR_DB_MYSQL_READ_TABLES_FAIL"); return; } this.bot.log("MySQL Database Table List"); this.bot.log(result); for (const t of result) { for (const [k, r] of Object.entries(t)) { this.dbTableList.push(r); } } } /** * store database columns * * @param Object * @return void */ storeFields(err, result) { if (err) { this.errorHandler.throwError("ERROR_DB_MYSQL_STORE_FIELDS_FAIL"); return; } if (typeof result !== "object" || Array.isArray(result) !== true || result.length === 0) { this.errorHandler.throwError("ERROR_DB_MYSQL_READ_FIELDS_FAIL"); return; } this.bot.log("MySQL Database Field List"); this.bot.log(result); for (const f of result) { this.dbFieldList.push(f.Field); this.dbFields } } /* installation */ /** * install the table and default columns * * @param Function * @return void */ install(callback) { this.connection.query("CREATE TABLE `"+this.table+"` (`id` int(10) NOT NULL AUTO_INCREMENT, `timestamp` bigint(20) DEFAULT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;", (err, result) => { if (err) { this.errorHandler.throwError("ERROR_DB_MYSQL_INSTALL_TABLE_FAIL", { message: `error adding table '${this.table}' to '${this.options.database.credentials.database}'`, error: err, }); return false; } this.bot.log(`added MySQL table '${this.table}' to '${this.options.database.credentials.database}'`, result); if (typeof callback === "function") { callback(err, result); } }); } /** * install multiple fields * * @param Object * @return void */ installFields(f) { if (typeof f === "undefined") { return false; } if (Array.isArray(f) !== true) { f = [ f ]; } for (const field of f) { this.installField(field); } } /** * install a single database column * * @param Object * @return void */ installField(f) { if (typeof f !== "object" || typeof f.key !== "string" || f.key.length === 0 || typeof f.type !== "string" || f.type.length === 0) { return false } let name = f.key, type = f.type; if (this.fieldExists(name)) { return true; } let def = "text", defaultValue = ""; switch (type) { case "boolean": def = "tinyint(1) NOT NULL"; defaultValue = " DEFAULT '0'"; break; case "number": def = "bigint(20)"; defaultValue = " DEFAULT NULL"; break; case "id": def = "int(10) NOT NULL AUTO_INCREMENT"; defaultValue = ""; break; } if (typeof f.default !== "undefined" && typeof f.default.toString === "function") { defaultValue = ` DEFAULT '${f.default.toString()}'`; } this.connection.query(`ALTER TABLE ${this.table} ADD COLUMN ${name} ${def}${defaultValue}`, (err, result) => { if (err) { this.errorHandler.throwError("ERROR_DB_MYSQL_INSTALL_FIELD_FAIL", { message: `error adding column '${name}' to '${this.table}'`, err, }); return false; } this.bot.log(`added MySQL DB column '${name}' to '${this.table}'`, result); }); } /* functions */ /** * vet and clean up any stream data available * * @param Object * @return void */ buildStreamData(data) { let streamData = {}, val = ""; for (const k of this.fieldList) { if (typeof this.fields[k] === "undefined") { continue; } let f = this.fields[k]; let d = data[k]; switch (f.type) { case "boolean": val = parseInt(d, 2); break; case "number": if (f.key === "timestamp") { val = Date.now(); break; } case "id": val = parseInt(d, 10); break; case "json": case "json_a": if (typeof d === "string" && d.length > 0) { val = JSON.parse(d); } break; } streamData[k] = val; } return streamData; } /** * build MYSQL column/value string with appropraite default values for column types * * @return void */ buildFieldListSQL() { let fields = "", values = "", sep = "", defVal = null; for (const k of this.fieldList) { if (typeof this.fields[k] === "undefined") { continue; } let f = this.fields[k]; defVal = this.buildDefaultValue(f); if (defVal === false) { continue; } fields += `${sep}${f.key}`; values += `${sep}'${defVal}'`; sep = ", "; } return `(${fields}) VALUES (${values})`; } /** * retrieve an appropriate default value for the field type * * @param Object * @return void */ buildDefaultValue(f) { let type = f.type || "json"; let defaultValue = "{}"; switch (type) { case "boolean": defaultValue = "0"; break; case "number": defaultValue = false; break; case "id": defaultValue = false; break; case "json": defaultValue = "{}"; break; case "json_a": defaultValue = "[]"; break; } return defaultValue; } /** * compile the stream data into an object * * @return void */ buildFieldListObject() { let object = {}, defVal = null; for (const k of this.fieldList) { if (typeof this.fields[k] === "undefined") { continue; } let f = this.fields[k]; defVal = this.buildDefaultValueRaw(f); object[k] = defVal; } return object; } /** * retrieve an appropriate default value for the field type (not encoded) * * @param Object * @return void */ buildDefaultValueRaw(f) { let type = f.type || "json"; let defaultValue = {}; switch (type) { case "boolean": defaultValue = 0; break; case "number": defaultValue = 0; break; case "id": defaultValue = 0; break; case "json": defaultValue = {}; break; case "json_a": defaultValue = []; break; } return defaultValue; } /** * build a field list using passed info * * @param Object * @return String|Boolean */ buildFieldList(fields) { let fieldList = "", sep = ""; if (typeof fields !== "object" || Object.keys(fields).length === 0) { return false; } for (let [k, v] of Object.entries(fields)) { let val = this.buildFieldListValue(v, this.fields[k]); fieldList += `${sep}${k} = '${val}'`; sep = ", "; } return fieldList; } /** * return a clean value for the field type * * @param Boolean|Number|Object * @param Object * @return String|Boolean */ buildFieldListValue(v, f) { let val = ""; switch (f.type) { case "boolean": val = v === true ? "1" : "0"; break; case "number": val = parseInt(v, 10); break; default: if (typeof v === "object") { val = JSON.stringify(v); } } return val; } /** * getter for field existence * * @param String * @return void */ fieldExists(field) { return this.dbFieldList.includes(field) === true; } /** * getter for table existence * * @param String * @return void */ tableExists(table) { return this.dbTableList.includes(table.toLowerCase()) === true; } } module.exports = MYSQL_DatabaseIntegration;