From 34a006e351e30b7c63a2c8b09e2d1eba8dd157d4 Mon Sep 17 00:00:00 2001 From: noolaan Date: Wed, 8 Apr 2020 10:08:46 -0600 Subject: [PATCH] fucking mess --- Manager.js | 6 +- Registry.js | 66 +++++- index.js | 9 + middleware/Shard.js | 214 +++++++++++++++++- middleware/ShardManager.js | 159 ++++++++++++- middleware/WebhookManager.js | 0 middleware/client/DiscordClient.js | 41 ++++ middleware/client/Dispatcher.js | 25 ++ middleware/client/EventHooker.js | 45 ++++ middleware/client/Resolver.js | 134 +++++++++++ package.json | 8 +- storage/Provider.js | 13 ++ storage/providers/Mariadb.js | 73 ++++++ storage/providers/Mongodb.js | 23 +- .../components/observers/CommandHandler.js | 108 +++++++++ structure/extensions/Guild.js | 22 ++ structure/extensions/Message.js | 21 ++ structure/extensions/User.js | 22 ++ structure/extensions/index.js | 5 + structure/interfaces/Command.js | 44 +++- structure/interfaces/Component.js | 79 +++++++ structure/interfaces/Inhibitor.js | 27 ++- structure/interfaces/Module.js | 25 ++ structure/interfaces/Observer.js | 38 ++++ structure/interfaces/Setting.js | 42 +++- structure/interfaces/index.js | 1 + util/Util.js | 33 +++ util/index.js | 4 +- yarn.lock | 42 +++- 29 files changed, 1301 insertions(+), 28 deletions(-) create mode 100644 index.js create mode 100644 middleware/WebhookManager.js create mode 100644 middleware/client/DiscordClient.js create mode 100644 middleware/client/Dispatcher.js create mode 100644 middleware/client/EventHooker.js create mode 100644 middleware/client/Resolver.js create mode 100644 structure/components/observers/CommandHandler.js create mode 100644 structure/extensions/Guild.js create mode 100644 structure/extensions/Message.js create mode 100644 structure/extensions/User.js create mode 100644 structure/extensions/index.js create mode 100644 structure/interfaces/Component.js create mode 100644 structure/interfaces/Module.js create mode 100644 structure/interfaces/Observer.js create mode 100644 util/Util.js diff --git a/Manager.js b/Manager.js index ecf326e..94dd6c0 100644 --- a/Manager.js +++ b/Manager.js @@ -1,13 +1,17 @@ const { EventEmitter } = require('events'); -const options = require('./options.json'); +const ShardManager = require('./middleware/ShardManager.js'); +const StorageManager = require('./storage/StorageManager.js'); +const { Command, Setting, Inhibitor } = require('./structure/interfaces/'); class Manager extends EventEmitter { constructor(options) { this.registry = new Registry(this); + this.shardManager = new ShardManager(this, './middleware/client/DiscordClient.js', options.shard) + .spawn(); this.storageManager = new StorageManager(this, options.storage) .initialize(); diff --git a/Registry.js b/Registry.js index 4949840..92c5e44 100644 --- a/Registry.js +++ b/Registry.js @@ -1,8 +1,72 @@ const { EventEmitter } = require('events'); +const { Collection, Util } = require('./util/'); class Registry extends EventEmitter { - + constructor(manager) { + + this.components = new Collection(); + + } + + async loadComponents() { + + const directory = path.join(process.cwd(), 'structure/', dir); //Finds directory of component folder relative to current working directory. + const files = Util.readdirRecursive(directory); //Loops through all folders in the directory and returns the files. + + const loaded = []; + + for(const path of files) { + const func = require(path); + if(typeof func !== 'function') { + //this.client.logger.error("Attempted to index an invalid function as a component."); + continue; + } + + const component = new func(this.client); //Instantiates the component class. + if(classToHandle && !(component instanceof classToHandle)) { + //this.client.logger.error("Attempted to load an invalid class."); + continue; + } + + loaded.push(await this.loadComponent(component, path)); + + } + + return loaded; + + } + + async loadComponent(component, directory) { + + if(!(component instanceof Component)) { + //this.client.logger.error("Attempted to load an invalid component."); + return null; + } + + if(this.components.has(component.resolveable)) { + //this.client.logger.error("Attempted to reload an existing component."); + return null; + } + + if(directory) component.directory = directory; + if(component.module && typeof component.module === 'string') { //Sets modules or "groups" for each component, specified by their properties. + let module = this.components.get(`module:${component.module}`); + if(!module) module = await this.loadComponent(new Module(this.manager, { name: component.module })); + this.components.set(module.resolveable, module); + + component.module = module; + module.components.set(component.resolveable, component); + } + + this.components.set(component.resolveable, component); + this.emit('componentUpdate', { component, type: 'LOAD' }); + return component; + } + + async unloadComponent(component) { + this.components.delete(component.id); + } } diff --git a/index.js b/index.js new file mode 100644 index 0000000..ca49972 --- /dev/null +++ b/index.js @@ -0,0 +1,9 @@ +const Manager = require('./Manager.js'); +const options = require('./options.json'); + +new Manager(options) + .initialize(); + +process.on("unhandledRejection", (error) => { + console.error("Unhandled promise rejection:", error); //eslint-disable-line no-console +}); \ No newline at end of file diff --git a/middleware/Shard.js b/middleware/Shard.js index bcbe5ff..1101385 100644 --- a/middleware/Shard.js +++ b/middleware/Shard.js @@ -1,7 +1,219 @@ -const { EventEmitter } = require('events'); +/* Adopted from Discord.js */ + +const path = require('path'); +const EventEmitter = require('events'); + +const Util = require('../../util/Util.js'); + +let childProcess = null; +let Worker = null; class Shard extends EventEmitter { + constructor(shardManager, id) { + + super(); + + this.manager = shardManager.manager; + + if(shardManager.mode === 'process') childProcess = require('child_process'); + else if(shardManager.mode === 'worker') Worker = require('worker_threads').Worker; + + this.shardManager = shardManager; + this.id = id; + this.args = shardManager.shardArgs || []; + this.execArgv = shardManager.execArgv; + this.env = Object.assign({}, process.env, { + SHARDING_shardManager: true, + SHARDS: this.id, + TOTAL_SHARD_COUNT: this.shardManager.totalShards, + DISCORD_TOKEN: this.shardManager.token + }); + + this.ready = false; + this.process = null; + this.worker = null; + + this._evals = new Map(); + this._fetches = new Map(); + + this._exitListener = this._handleExit.bind(this, undefined); + + } + + async spawn(waitForReady = true) { + if(this.process) throw new Error(`[shard${this.id}] Sharding process already exists.`); + if(this.worker) throw new Error(`[shard${this.id}] Sharding worker already exists.`); + + if(this.shardManager.mode === 'process') { + this.process = childProcess.fork(path.resolve(this.shardManager.file), this.args, { + env: this.env, + execArgv: this.execArgv + }) + .on('message', this._handleMessage.bind(this)) + .on('exit', this._exitListener); + } else if(this.shardManager.mode === 'worker') { + this.worker = new Worker(path.resolve(this.shardManager.file), { workerData: this.env }) + .on('message', this._handleMessage.bind(this)) + .on('exit', this._exitListener); + } + + this.emit('spawn', this.process || this.worker); + + if(!waitForReady) return this.process || this.worker; + await new Promise((resolve, reject) => { + this.once('ready', resolve); + this.once('disconnect', () => reject(new Error(`[shard${this.id}] Shard disconnected while readying.`))); + this.once('death', () => reject(new Error(`[shard${this.id}] Shard died while readying.`))); + setTimeout(() => reject(new Error(`[shard${this.id}] Shard timed out while readying.`)), 30000); + }); + + return this.process || this.worker; + + } + + kill() { + if(this.process) { + this.process.removeListener('exit', this._exitListener); + this.process.kill(); + } else { + this.worker.removeListener('exit', this._exitListener); + this.worker.terminate(); + } + + this._handleExit(false); + + } + + async respawn(delay = 500, waitForReady = true) { + this.kill(); + if(delay > 0) await Util.delayFor(delay); + return this.spawn(waitForReady); + } + + send(message) { + return new Promise((resolve, reject) => { + if(this.process) { + this.process.send(message, error => { + if(error) reject(error); else resolve(this); + }); + } else { + this.worker.postMessage(message); + resolve(this); + } + }); + } + + fetchClientValue(prop) { + if(this._fetches.has(prop)) return this._fetches.get(prop); + + const promise = new Promise((resolve, reject) => { + const child = this.process || this.worker; + + const listener = message => { + if(!message || message._fetchProp !== prop) return; + child.removeListener('message', listener); + this._fetches.delete(prop); + resolve(message._result); + }; + child.on('message', listener); + + this.send({ _fetchProp: prop }).catch(err => { + child.removeListener('message', listener); + this._fetches.delete(prop); + reject(err); + }); + }); + + this._fetches.set(prop, promise); + return promise; + + } + + eval(script) { + + if(this._evals.has(script)) return this._evals.get(script); + + const promise = new Promise((resolve, reject) => { + const child = this.process || this.worker; + + const listener = message => { + if(!message || message._eval !== script) return; + child.removeListener('message', listener); + this._evals.delete(script); + if(!message._error) resolve(message._result); else reject(new Error(message._error)); + }; + child.on('message', listener); + + const _eval = typeof script === 'function' ? `(${script})(this)` : script; + this.send({ _eval }).catch(err => { + child.removeListener('message', listener); + this._evals.delete(script); + reject(err); + }); + }); + + this._evals.set(script, promise); + return promise; + + } + + _handleMessage(message) { + if(message) { + if(message._ready) { //Shard ready + this.ready = true; + this.emit('ready'); + return; + } + if(message._disconnect) { //Shard disconnected + this.ready = false; + this.emit('disconnect'); + return; + } + if(message._reconnecting) { //Shard attempting to reconnect + this.ready = false; + this.emit('reconnecting'); + return; + } + if(message._sFetchProp) { //Shard requesting property fetch + this.shardManager.fetchClientValues(message._sFetchProp).then( + results => this.send({ _sFetchProp: message._sFetchProp, _result: results }), + err => this.send({ _sFetchProp: message._sFetchProp, _error: Util.makePlainError(err) }) + ); + return; + } + if(message._sEval) { //Shard requesting eval broadcast + this.shardManager.broadcastEval(message._sEval).then( + results => this.send({ _sEval: message._sEval, _result: results }), + err => this.send({ _sEval: message._sEval, _error: Util.makePlainError(err) }) + ); + return; + } + if(message._sRespawnAll) { //Shard requesting to respawn all shards. + const { shardDelay, respawnDelay, waitForReady } = message._sRespawnAll; + this.shardManager.respawnAll(shardDelay, respawnDelay, waitForReady).catch(() => { + }); + return; + } + } + + this.shardManager.emit('message', this, message); + + } + + _handleExit(respawn = this.shardManager.respawn) { + this.emit('death', this.process || this.worker); + + this.ready = false; + this.process = null; + this.worker = null; + this._evals.clear(); + this._fetches.clear(); + + if(respawn) this.spawn().catch(err => this.emit('error', err)); + + } + } module.exports = Shard; \ No newline at end of file diff --git a/middleware/ShardManager.js b/middleware/ShardManager.js index ced997e..2debc3a 100644 --- a/middleware/ShardManager.js +++ b/middleware/ShardManager.js @@ -1,3 +1,156 @@ -class ShardManager { - -} \ No newline at end of file +/* Adopted from Discord.js */ + +const path = require('path'); +const fs = require('fs'); +const EventEmitter = require('events'); + +const Shard = require('./Shard.js'); +const Collection = require('../../util/interfaces/Collection.js'); +const Util = require('../../util/Util.js'); + +class ShardManager extends EventEmitter { + + constructor(manager, file, options = {}) { + + super(); + + options = Util.mergeDefault({ + totalShards: 'auto', + mode: 'process', + respawn: true, + shardArgs: [], + execArgv: [], + token: options.bot.token + }, options.shard); + + this.manager = manager; + + this.file = file; + if(!file) throw new Error("[shardmanager] File must be specified."); + if(!path.isAbsolute(file)) this.file = path.resolve(process.cwd(), file); + + const stats = fs.statSync(this.file); + if(!stats.isFile()) throw new Error("[shardmanager] File path does not point to a valid file."); + + this.shardList = options.shardList || 'auto'; + if(this.shardList !== 'auto') { + if(!Array.isArray(this.shardList)) { + throw new TypeError("[shardmanager] ShardList must be an array."); + } + this.shardList = [...new Set(this.shardList)]; + if(this.shardList.length < 1) throw new RangeError("[shardmanager] ShardList must have one ID."); + if(this.shardList.some(shardID => typeof shardID !== 'number' + || isNaN(shardID) + || !Number.isInteger(shardID) + || shardID < 0) + ) { + throw new TypeError("[shardmanager] ShardList must be an array of positive integers."); + } + } + + this.totalShards = options.totalShards || 'auto'; + if(this.totalShards !== 'auto') { + if(typeof this.totalShards !== 'number' || isNaN(this.totalShards)) { + throw new TypeError("[shardmanager] TotalShards must be an integer."); + } + if(this.totalShards < 1) throw new RangeError("[shardmanager] TotalShards must be at least one."); + if(!Number.isInteger(this.totalShards)) { + throw new RangeError("[shardmanager] TotalShards must be an integer."); + } + } + + this.mode = options.mode; + if(this.mode !== 'process' && this.mode !== 'worker') { + throw new RangeError("[shardmanager] Mode must be either 'worker' or 'process'."); + } + + this.respawn = options.respawn; + this.shardArgs = options.shardArgs; + this.execArgv = options.execArgv; + this.token = options.token; + this.shards = new Collection(); + + process.env.SHARDING_MANAGER = true; + process.env.SHARDING_MANAGER_MODE = this.mode; + process.env.DISCORD_TOKEN = this.token; + + } + + createShard(id = this.shards.size) { + const shard = new Shard(this, id); + this.shards.set(id, shard); + + this.emit('shardCreate', shard); + return shard; + } + + async spawn(amount = this.totalShards, delay = 5500, waitForReady = true) { + + if(amount === 'auto') { + amount = await Util.fetchRecommendedShards(this.token); + } else { + if(typeof amount !== 'number' || isNaN(amount)) { + throw new TypeError("[shardmanager] Amount of shards must be a number."); + } + if(amount < 1) throw new RangeError("[shardmanager] Amount of shards must be at least one."); + if(!Number.isInteger(amount)) { + throw new TypeError("[shardmanager] Amount of shards must be an integer."); + } + } + + if(this.shards.size >= amount) throw new Error("[shardmanager] Already spawned all necessary shards."); + if(this.shardList === 'auto' || this.totalShards === 'auto' || this.totalShards !== amount) { + this.shardList = [...Array(amount).keys()]; + } + if(this.totalShards === 'auto' || this.totalShards !== amount) { + this.totalShards = amount; + } + if(this.shardList.some(id => id >= amount)) { + throw new RangeError("[shardmanager] Amount of shards cannot be larger than the highest shard ID."); + } + + for(const shardID of this.shardList) { + const promises = []; + const shard = this.createShard(shardID); + promises.push(shard.spawn(waitForReady)); + if(delay > 0 && this.shards.size !== this.shardList.length - 1) promises.push(Util.delayFor(delay)); + await Promise.all(promises); + } + + return this.shards; + + } + + broadcast(message) { + const promises = []; + for(const shard of this.shards.values()) promises.push(shard.send(message)); + return Promise.all(promises); + } + + broadcastEval(script) { + const promises = []; + for(const shard of this.shards.values()) promises.push(shard.eval(script)); + return Promise.all(promises); + } + + fetchClientValues(prop) { + if(this.shards.size === 0) return Promise.reject(new Error("[shardmanager] No shards available.")); + if(this.shards.size !== this.totalShards) return Promise.reject(new Error("[shardmanager] Sharding in progress.")); + const promises = []; + for(const shard of this.shards.values()) promises.push(shard.fetchClientValue(prop)); + return Promise.all(promises); + } + + async respawnAll(shardDelay = 5000, respawnDelay = 500, waitForReady = true) { + let s = 0; + for(const shard of this.shards.values()) { + const promises = [shard.respawn(respawnDelay, waitForReady)]; + if(++s < this.shards.size && shardDelay > 0) promises.push(Util.delayFor(shardDelay)); + await Promise.all(promises); + } + return this.shards; + } + +} + +module.exports = ShardManager; \ No newline at end of file diff --git a/middleware/WebhookManager.js b/middleware/WebhookManager.js new file mode 100644 index 0000000..e69de29 diff --git a/middleware/client/DiscordClient.js b/middleware/client/DiscordClient.js new file mode 100644 index 0000000..8df3e02 --- /dev/null +++ b/middleware/client/DiscordClient.js @@ -0,0 +1,41 @@ +const { Client } = require('discord.js'); + +const EventHooker = require('./EventHooker.js'); +const Dispatcher = require('./Dispatcher.js') +const Resolver = require('./Resolver.js'); + +const { Guild, User, Message } = require('../../structure/extensions/'); + +class DiscordClient extends Client { + + constructor(manager, options) { + + this.manager = manager; + this.registry = this.manager.registry; + + this.eventHooker = new EventHooker(this); + this.dispatcher = new Dispatcher(this); + this.resolver = new Resolver(this); + + + this._options = options; + this._built = false; + + } + + async build() { + + + + this._built = true; + + } + + + +} + +module.exports = DiscordClient; + +const client = new DiscordClient(); +client.build(); \ No newline at end of file diff --git a/middleware/client/Dispatcher.js b/middleware/client/Dispatcher.js new file mode 100644 index 0000000..7733074 --- /dev/null +++ b/middleware/client/Dispatcher.js @@ -0,0 +1,25 @@ +class Dispatcher { + + constructor(client) { + + this.client = client; + + } + + async dispatch() { + + const observers = this.client.registry.components + .filter(c=>c.type === 'observer' && !c.disabled) + .sort((a, b) => b.priority - a.priority); + + for(const observer of observers.values()) { + for(let [hook, func] of observer.hooks) { + this.client.hooker.hook(hook, func); + } + } + + } + +} + +module.exports = Dispatcher; \ No newline at end of file diff --git a/middleware/client/EventHooker.js b/middleware/client/EventHooker.js new file mode 100644 index 0000000..a212dd6 --- /dev/null +++ b/middleware/client/EventHooker.js @@ -0,0 +1,45 @@ +const { EventEmitter } = require('events'); + +class EventHooker { + + constructor(target) { + if(!(target instanceof EventEmitter)) return new TypeError('Invalid EventEmitter passed to EventHooker.'); + + this.target = target; + this.events = new Map(); + + } + + hook(eventName, func) { + if(this.events.has(eventName)) { + const funcs = this.events.get(eventName); + this.events.set(eventName, [ ...funcs, func ]); + } else { + this.events.set(eventName, [ func ]); + this._handleEvent(eventName); + } + } + + unhook(eventName, func) { + if(this.events.has(eventName)) { + let funcs = this.events.get(eventName); + const index = funcs.indexOf(func); + if(index > -1) { + funcs.splice(index, 1); + this.events.set(eventName, funcs); + } + } + } + + async _handleEvent(eventName) { + this.target.on(eventName, (...args) => { + this.events.get(eventName).forEach(async (f) => { + const result = f(...args); + if(f instanceof Promise) await result; + }); + }); + } + +} + +module.exports = EventHooker; diff --git a/middleware/client/Resolver.js b/middleware/client/Resolver.js new file mode 100644 index 0000000..c8bdb19 --- /dev/null +++ b/middleware/client/Resolver.js @@ -0,0 +1,134 @@ +class Resolver { + + constructor(client) { + this.client = client; + } + + components(str = '', type, exact = true) { //used for CommandHandler + + const string = str.toLowerCase(); + + const components = this.client.registry.components + .filter(c => c.type === type) + .filter(exact ? filterExact(string) : filterInexact(string)) + .array(); + + return components || []; + + } + + async resolveMemberAndUser(string, guild) { + + const str = string.toLowerCase(); + const index = guild ? guild.members : this.client.users; + + let member = null; + if(/<@!?(\d{17,21})>/iy.test(str)) { //mentions + const matches = /<@!?(\d{17,21})>/iy.exec(str); + member = index.get(matches[1]); + if(!member) { + try { + member = await index.fetch(matches[1]); + } catch(e) { + try { + member = await this.client.users.fetch(matches[1]); + } catch(e) {} //eslint-disable-line no-empty + } //eslint-disable-line no-empty + } + } else if(/\d{17,21}/iy.test(str)) { //id + const matches = /(\d{17,21})/iy.exec(str); + member = index.get(matches[1]); + if(!member) { + try { + member = await index.fetch(matches[1]); + } catch(e) { + try { + member = await this.client.users.fetch(matches[1]); + } catch(e) {} //eslint-disable-line no-empty + } //eslint-disable-line no-empty + } + } else if(/(.{2,32})#(\d{4})/iy.test(str)) { //username#discrim + const matches = /(.{2,32})#(\d{4})/iy.exec(str); + member = guild + ? guild.members.filter(m=>m.user.username === matches[1] && m.user.discriminator === matches[2]).first() + : this.client.users.filter(u=>u.username === matches[1] && u.discriminator === matches[2]).first(); + } + return member || null; + + } + + async resolveUsers(resolveables = []) { + + if(typeof resolveables === 'string') resolveables = [ resolveables ]; + if(resolveables.length === 0) return false; + let users = this.client.users; + let resolved = []; + + for(let resolveable of resolveables) { + + if(/<@!?([0-9]{17,21})>/.test(resolveable)) { + + let id = resolveable.match(/<@!?([0-9]{17,21})>/)[1]; + let user = await users.fetch(id).catch(err => { if(err.code === 10013) return false; else { console.warn(err); return false; } }); + if(user) resolved.push(user); + + } else if(/(id\:)?([0-9]{17,21})/.test(resolveable)) { + + let id = resolveable.match(/(id\:)?([0-9]{17,21})/)[2]; + let user = await users.fetch(id).catch(err => { if(err.code === 10013) return false; else { console.warn(err); return false; } }); + if(user) resolved.push(user); + + } else if(/^\@?([\S\s]{1,32})\#([0-9]{4})/.test(resolveable)) { + + let m = resolveable.match(/^\@?([\S\s]{1,32})\#([0-9]{4})/); + let username = m[1].toLowerCase(); + let discrim = m[2].toLowerCase(); + let user = users.cache.filter(u => { + return u.username.toLowerCase() === username && u.discriminator === discrim + }).first(1); + if(user) resolved.push(user); + + } + + } + + return resolved; + + } + + async resolveMember(resolveables = [], strict = false, guild) { + + if(typeof resolveables === 'string') resolveables = [ resolveables ]; + if(resolveables.length === 0) return false; + let members = guild.members; + let resolved = []; + + for(let resolveable of resolveables) { + + if(/<@!?([0-9]{17,21})>/.test(resolveable)) { + + let id = resolveable.match(/<@!?([0-9]{17,21})>/)[1]; + let member = await members.fetch(id).catch(err => { if(err.code === 10007) return false; else { console.warn(err); return false; } }); + if(member) resolved.push(member); + + } + + } + + } + +} + +const filterExact = (search) => { + return comp => comp.id.toLowerCase() === search || + comp.resolveable.toLowerCase() === search || + (comp.aliases && (comp.aliases.some(ali => `${comp.type}:${ali}`.toLowerCase() === search) || + comp.aliases.some(ali => ali.toLowerCase() === search))); +}; + +const filterInexact = (search) => { + return comp => comp.id.toLowerCase().includes(search) || + comp.resolveable.toLowerCase().includes(search) || + (comp.aliases && (comp.aliases.some(ali => `${comp.type}:${ali}`.toLowerCase().includes(search)) || + comp.aliases.some(ali => ali.toLowerCase().includes(search)))); +}; \ No newline at end of file diff --git a/package.json b/package.json index 95bf71c..eddc222 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "description": "New iteration of GalacticBot", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "echo \"Error: no test specified\" && exit 1", + "start": "node index.js" }, "repository": { "type": "git", @@ -17,8 +18,11 @@ }, "homepage": "https://github.com/Navy-gif/New-GBot#readme", "dependencies": { + "common-tags": "^1.8.0", "discord.js": "discordjs/discord.js", + "escape-string-regexp": "^3.0.0", "eslint": "^6.8.0", - "mongodb": "^3.5.5" + "mongodb": "^3.5.5", + "mysql": "^2.18.1" } } diff --git a/storage/Provider.js b/storage/Provider.js index 9e17942..5b726bd 100644 --- a/storage/Provider.js +++ b/storage/Provider.js @@ -1,5 +1,18 @@ class Provider { + constructor(manager, config) { + + if(!config) throw new Error('No config file provided!'); + if(config && (!config.database || !config.url)) throw new Error('Invalid config file provided!'); + + this.manager = manager; + this.config = config; + this.db; + this.loaded = false; + + + } + } module.exports = Provider; \ No newline at end of file diff --git a/storage/providers/Mariadb.js b/storage/providers/Mariadb.js index e69de29..7871f5f 100644 --- a/storage/providers/Mariadb.js +++ b/storage/providers/Mariadb.js @@ -0,0 +1,73 @@ +const Provider = require('./Provider.js'); +const MySQL = require('mysql'); + +class MariaDBProvider extends Provider { + + constructor(manager, config) { + + super(manager, config); + + } + + async init() { + + try { + + this.db = MySQL.createPool(this.config); + + this.db.on('connection', async connection => { + + // this.manager.logger.log('MariaDB connected.'); + + connection.on('error', err => { + // this.manager.logger.error('MariaDB errored.', err); + }); + + connection.on('close', data => { + // this.manager.logger.log('MariaDB connection closed.', data); + }) + + }); + + this.loaded = true; + + } catch(err) { + + // this.manager.logger.error('MariaDB connection failed.', err); + + } + + return this; + + } + + close() { + this.db.end(); + } + + /** + * Query using SQL to MariaDB + * + * @param {string} query SQL query string. + * @param {array} values Array of values to replace ? with in the query string + * @returns {object} Returns an object containing the query result + * @memberof MariaDBProvider + */ + query(query, values) { + + if(!this.loaded) throw new Error('MariaDB not connected'); + + return new Promise((resolve, reject) => { + + this.db.query(query, values, (err, result) => { + if(err) reject(err); + resolve(result); + }); + + }); + + } + +} + +module.exports = MariaDBProvider; \ No newline at end of file diff --git a/storage/providers/Mongodb.js b/storage/providers/Mongodb.js index 0ee2e17..34bf0a2 100644 --- a/storage/providers/Mongodb.js +++ b/storage/providers/Mongodb.js @@ -1,19 +1,13 @@ const Provider = require('./Provider.js'); const { MongoClient } = require('mongodb'); -class MongodbProvider extends Provider { +class MongoDBProvider extends Provider { constructor(manager, config) { super(manager, config); - if(!config) throw new Error('No config file provided!'); - if(config && (!config.database || !config.url)) throw new Error('Invalid config file provided!'); - - this.config = config; this.client; - this.db; - this.manager = manager; } @@ -26,6 +20,7 @@ class MongodbProvider extends Provider { this.client = await MongoClient.connect(this.config.url+this.config.database, { useUnifiedTopology: true }); this.manager.db = await this.client.db(this.config.database); this.db = this.manager.db; + this.loaded = true; // this.manager.logger.print('Database connected.'); } catch(err) { @@ -49,6 +44,8 @@ class MongodbProvider extends Provider { find(db, query) { + if(!this.loaded) throw new Error('MongoDB not connected'); + //if(this.manager.debug) this.manager.logger.debug(`Incoming find query for ${db} with parameters ${JSON.stringify(query)}`); return new Promise((resolve, reject) => { @@ -74,6 +71,8 @@ class MongodbProvider extends Provider { findOne(db, query) { + if(!this.loaded) throw new Error('MongoDB not connected'); + //if(this.manager.debug) this.manager.logger.debug(`Incoming findOne query for ${db} with parameters ${JSON.stringify(query)}`); return new Promise((resolve, reject) => { @@ -100,7 +99,9 @@ class MongodbProvider extends Provider { */ updateOne(db, query, data, upsert = false) { - if(this.manager.debug) this.manager.logger.debug(`Incoming updateOne query for ${db} with parameters ${JSON.stringify(filter)}`); + if(!this.loaded) throw new Error('MongoDB not connected'); + + //if(this.manager.debug) this.manager.logger.debug(`Incoming updateOne query for ${db} with parameters ${JSON.stringify(filter)}`); return new Promise((resolve, reject) => { this.db.collection(db).updateOne(query, { $set: data }, { upsert: upsert }, async (error, result) => { @@ -129,6 +130,8 @@ class MongodbProvider extends Provider { */ push(db, query, data, upsert = false) { + if(!this.loaded) throw new Error('MongoDB not connected'); + //if(this.manager.debug) this.manager.logger.debug(`Incoming push query for ${db}, with upsert ${upsert} and with parameters ${JSON.stringify(filter)} and data ${JSON.stringify(data)}`); return new Promise((resolve, reject) => { @@ -154,6 +157,8 @@ class MongodbProvider extends Provider { */ random(db, query = {}, amount = 1) { + if(!this.loaded) throw new Error('MongoDB not connected'); + //if(this.manager.debug) this.manager.logger.debug(`Incoming random query for ${db} with parameters ${JSON.stringify(filter)} and amount ${amount}`); if(amount > 100) amount = 100; @@ -172,4 +177,4 @@ class MongodbProvider extends Provider { } -module.exports = MongodbProvider; \ No newline at end of file +module.exports = MongoDBProvider; \ No newline at end of file diff --git a/structure/components/observers/CommandHandler.js b/structure/components/observers/CommandHandler.js new file mode 100644 index 0000000..39fcf8b --- /dev/null +++ b/structure/components/observers/CommandHandler.js @@ -0,0 +1,108 @@ +const { stripIndents } = require('common-tags'); +const { escapeRegex } = require('escape-string-regexp'); + +const { Observer } = require('../../interfaces'); + +class CommandHandler extends Observer { + + constructor(manager) { + + super(manager, { + name: 'commandHandler', + priority: 5, + guarded: true + }); + + this.manager = manager; + + this.hooks = [ + ['message', this.handleMessage.bind(this)] + ]; + + this.commandPatterns = new Map(); + + + } + + async handleMessage(message) { + + const client = message.client; + + if(!this.manager._built + || !client._built + || message.webhookID + || message.author.bot + || (message.guild && !message.guild.available)) return undefined; + + if(message.guild && !message.member) { + await message.guild.members.fetch(message.author.id); + } + + const content = message.cleanContent; + const args = content.split(' '); + const command = await this._getCommand(message, args); + if(!command) return undefined; + + return await this.handleCommand(message, command, args); + + } + + async handleCommand(message, command, args) { + + } + + async _getCommand(message, args = []) { + + const pattern = await this._getCommandPattern(message.guild); + + let command = await this._matchCommand(message, args, pattern, 2); + if(!command && !message.guild) command = await this._matchCommand(message, args, /^([^\s]+)/i); + + return command || null; + + } + + async _getCommandPattern(guild) { + + const createCommandPattern = (guild = null) => { + + const prefix = this.client._options.discord.prefix; + + const escapedPrefix = escapeRegex(prefix); + const pattern = new RegExp(`^(${escapedPrefix}\\s*|<@!?${this.client.user.id}>\\s+(?:${escapedPrefix})?)([^\\s]+)`, 'i'); + + const obj = { pattern, prefix }; + if(guild) { + this.client.logger.debug(`Created command pattern ${guild.name}: ${pattern}`); + this.commandPatterns.set(guild.id, obj); + } + + return obj; + }; + + if(!guild) return createCommandPattern().pattern; + let commandPattern = this.commandPatterns.get(guild.id); + + return commandPattern.pattern; + + } + + async _matchCommand(message, args, pattern, index = 1) { + + const matches = pattern.exec(message.cleanContent); + if(!matches) return null; + + const command = message.client.resolver.components(matches[index], 'command', true)[0]; + if(!command) return null; + + const indice = message.content.startsWith('<@') ? 2 : 1; + args.splice(0, indice); + + return command; + + } + + +} + +module.exports = CommandHandler; \ No newline at end of file diff --git a/structure/extensions/Guild.js b/structure/extensions/Guild.js new file mode 100644 index 0000000..ace5089 --- /dev/null +++ b/structure/extensions/Guild.js @@ -0,0 +1,22 @@ +const { Structures } = require('discord.js'); + +const Guild = Structures.extend('Guild', (Guild) => { + + class ExtendedGuild extends Guild { + + constructor(...args) { + + super(...args); + + this.storageManager = this.manager.storageManager; + this._settings = null; //internal cache of current guild's settings; should ALWAYS stay the same as database. + + } + + } + + return ExtendedGuild; + +}); + +module.exports = Guild; \ No newline at end of file diff --git a/structure/extensions/Message.js b/structure/extensions/Message.js new file mode 100644 index 0000000..bcb86fa --- /dev/null +++ b/structure/extensions/Message.js @@ -0,0 +1,21 @@ +const { Structures } = require('discord.js'); + +const Message = Structures.extend('Message', (Message) => { + + class ExtendedMessage extends Message { + + constructor(...args) { + + super(...args); + + this.command = null; //Will set to command if the message induces a command. + + } + + } + + return ExtendedMessage; + +}); + +module.exports = Message; \ No newline at end of file diff --git a/structure/extensions/User.js b/structure/extensions/User.js new file mode 100644 index 0000000..7603870 --- /dev/null +++ b/structure/extensions/User.js @@ -0,0 +1,22 @@ +const { Structures } = require('discord.js'); + +const User = Structures.extend('User', (User) => { + + class ExtendedUser extends User { + + constructor(...args) { + + super(...args); + + this.storageManager = this.manager.storageManager; + this._settings = null; //internal cache of current users' settings; should ALWAYS stay the same as database. + + } + + } + + return ExtendedUser; + +}); + +module.exports = User; \ No newline at end of file diff --git a/structure/extensions/index.js b/structure/extensions/index.js new file mode 100644 index 0000000..153bfe4 --- /dev/null +++ b/structure/extensions/index.js @@ -0,0 +1,5 @@ +module.exports = { + Message: require('./Message.js'), + Guild: require('./Guild.js'), + User: require('./User.js') +}; \ No newline at end of file diff --git a/structure/interfaces/Command.js b/structure/interfaces/Command.js index a40a2ac..6a1b7ca 100644 --- a/structure/interfaces/Command.js +++ b/structure/interfaces/Command.js @@ -1,4 +1,46 @@ -class Command { +const Component = require('./Component.js'); + +class Command extends Component { + + constructor(client, opts = {}) { + if(!opts) return null; + + super(client, { + id: opts.name, + type: 'command', + disabled: opts.disabled || false, + guarded: opts.guarded || false + }); + + Object.defineProperty(this, 'client', { value: client }); + + this.name = opts.name; + this.module = opts.module; + this.aliases = opts.aliases || []; + + this.description = opts.description || "A basic command."; + this.examples = opts.examples || []; + this.usage = opts.usage || null; + + this.restricted = Boolean(opts.restricted); + this.archivable = opts.archivable === undefined ? false : Boolean(opts.archivable); + this.guildOnly = Boolean(opts.guildOnly); + + this.clientPermissions = opts.clientPermissions || []; + this.memberPermissions = opts.memberPermissions || []; + + this.throttling = opts.throttling || { + usages: 5, + duration: 10 + }; + + this._throttles = new Map(); + + } + + get moduleResolveable() { + return `${this.module.id}:${this.id}`; + } } diff --git a/structure/interfaces/Component.js b/structure/interfaces/Component.js new file mode 100644 index 0000000..2c231d4 --- /dev/null +++ b/structure/interfaces/Component.js @@ -0,0 +1,79 @@ +class Component { + + constructor(manager, opts = {}) { + if(!opts) return null; + + this.manager = manager; + + this.id = opts.id; + this.type = opts.type; + + this.directory = null; + + this.guarded = Boolean(opts.guarded); + this.disabled = Boolean(opts.disabled); + + this.registry = this.manager.registry; + + } + + enable() { + if(this.guarded) return { error: true, code: 'GUARDED' }; + this.disabled = false; + this.registry.emit('componentUpdate', { component: this, type: 'ENABLE' }); + return { error: false }; + } + + disable() { + if(this.guarded) return { error: true, code: 'GUARDED' }; + this.disabled = true; + this.registry.emit('componentUpdate', { component: this, type: 'DISABLE' }); + return { error: false }; + } + + unload() { + if(this.guarded) return { error: true, code: 'GUARDED' }; + if(!this.directory) return { error: true, code: 'MISSING_DIRECTORY' }; + + this.registry.unloadComponent(this); + delete require.cache[this.filePath]; + + this.registry.emit('componentUpdate', { component: this, type: 'UNLOAD' }); + return { error: false }; + } + + reload(bypass = false) { + if(this.type === 'module') return { error: false }; + if(this.guarded && !bypass) return { error: true, code: 'GUARDED' }; + if(!this.directory || !require.cache[this.directory]) return { error: true, code: 'MISSING_DIRECTORY' }; + + let cached, newModule; + + try { + cached = require.cache[this.directory]; + delete require.cache[this.directory]; + newModule = require(this.directory); + + if(typeof newModule === 'function') { + newModule = new newModule(this.manager); + } + + this.registry.unloadComponent(this); + this.registry.emit('componentUpdate', { component: this, type: 'UNLOAD' }); + this.registry.loadComponent(newModule, this.directory); + } catch(error) { + if(cached) require.cache[this.directory] = cached; + return { error: true, code: 'MISSING_MODULE' }; + } + + return { error: false }; + + } + + get resolveable() { + return `${this.type}:${this.id}`; + } + +} + +module.exports = Component; \ No newline at end of file diff --git a/structure/interfaces/Inhibitor.js b/structure/interfaces/Inhibitor.js index 0eb1ed4..add5443 100644 --- a/structure/interfaces/Inhibitor.js +++ b/structure/interfaces/Inhibitor.js @@ -1,4 +1,29 @@ -class Inhibitor { +const Component = require('./Component.js'); + +class Inhibitor extends Component { + + constructor(client, opts = {}) { + + super(client, { + id: opts.name, + type: 'inhibitor', + guarded: opts.guarded, + disabled: opts.disabled + }); + + this.name = opts.name; + this.guild = Boolean(opts.guild); + this.priority = opts.priority || 1; + + } + + _succeed() { + return { error: false, inhibitor: this }; + } + + _fail(message) { + return { error: true, inhibitor: this, message }; + } } diff --git a/structure/interfaces/Module.js b/structure/interfaces/Module.js new file mode 100644 index 0000000..08c7a67 --- /dev/null +++ b/structure/interfaces/Module.js @@ -0,0 +1,25 @@ +const Component = require('./Component.js'); +const Collection = require('../../../util/interfaces/Collection.js'); + +class Module extends Component { + + constructor(client, opts = {}) { + if(!opts) return null; + + super(client, { + id: opts.name, + type: 'module' + }); + + Object.defineProperty(this, 'client', { + value: client + }); + + this.name = opts.name; + this.components = new Collection(); + + } + +} + +module.exports = Module; \ No newline at end of file diff --git a/structure/interfaces/Observer.js b/structure/interfaces/Observer.js new file mode 100644 index 0000000..92c7b03 --- /dev/null +++ b/structure/interfaces/Observer.js @@ -0,0 +1,38 @@ +const Component = require('./Component.js'); + +class Observer extends Component { + + constructor(client, opts = {}) { + + super(client, { + id: opts.name, + type: 'observer', + guarded: opts.guarded, + disabled: opts.disabled + }); + + this.name = opts.name; + this.priority = opts.priority || 1; + this.hooks = opts.hooks || []; + + Object.defineProperty(this, 'client', { + value: client + }); + + } + + execute() { + return this._continue(); + } + + _continue() { + return { error: false, observer: this }; + } + + _stop() { + return { error: true, observer: this }; + } + +} + +module.exports = Observer; \ No newline at end of file diff --git a/structure/interfaces/Setting.js b/structure/interfaces/Setting.js index 31d0418..e75b564 100644 --- a/structure/interfaces/Setting.js +++ b/structure/interfaces/Setting.js @@ -1,6 +1,44 @@ -class Setting { +const Component = require('./Component.js'); + +class Setting extends Component { + + constructor(client, opts = {}) { + + if(!opts) return null; + + super(client, { + id: opts.name, + type: 'setting', + guarded: opts.guarded, + disabled: opts.disabled + }); + + this.name = opts.name; + this.module = opts.module; + this.restricted = Boolean(opts.restricted); + + this.description = opts.description || "A basic setting."; + this.archiveable = Boolean(opts.archiveable); + + this.index = opts.index || opts.name; + this.aliases = opts.aliases || []; + this.resolve = (opts.resolve && Constants.Resolves.includes(opts.resolve)) ? opts.resolve : 'GUILD'; + this.default = opts.default; + + this.memberPermissions = opts.memberPermissions || []; + this.clientPermissions = opts.clientPermissions || []; + + } + } -module.exports = Setting; \ No newline at end of file +module.exports = Setting; + +const Constants = { + Resolves: [ + 'GUILD', + 'USER' + ] +}; \ No newline at end of file diff --git a/structure/interfaces/index.js b/structure/interfaces/index.js index c989c8a..b127906 100644 --- a/structure/interfaces/index.js +++ b/structure/interfaces/index.js @@ -1,5 +1,6 @@ module.exports = { Command: require('./Command.js'), + Component: require('./Component.js'), Inhibitor: require('./Inhibitor.js'), Setting: require('./Setting.js') } \ No newline at end of file diff --git a/util/Util.js b/util/Util.js new file mode 100644 index 0000000..e074ddc --- /dev/null +++ b/util/Util.js @@ -0,0 +1,33 @@ +const path = require('path'); +const fs = require('fs'); + +class Util { + + constructor() { + throw new Error("Class may not be instantiated."); + } + + static readdirRecursive(directory) { + + const result = []; + + (function read(directory) { + const files = fs.readdirSync(directory); + for(const file of files) { + const filePath = path.join(directory, file); + + if(fs.statSync(filePath).isDirectory()) { + read(filePath); + } else { + result.push(filePath); + } + } + }(directory)); + + return result; + + } + +} + +module.exports = Util; \ No newline at end of file diff --git a/util/index.js b/util/index.js index 7567068..a576971 100644 --- a/util/index.js +++ b/util/index.js @@ -1,3 +1,5 @@ module.exports = { - Collection: require('./Collection.js'); + Collection: require('./Collection.js'), + Util: require('./Util.js'), + Resolver: require('../middleware/client/Resolver.js') } \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 75870f9..70065a1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -114,6 +114,11 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= +bignumber.js@9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.0.tgz#805880f84a329b5eac6e7cb6f8274b6d82bdf075" + integrity sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A== + bl@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/bl/-/bl-2.2.0.tgz#e1a574cdf528e4053019bb800b041c0ac88da493" @@ -205,6 +210,11 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" +common-tags@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937" + integrity sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -283,6 +293,11 @@ escape-string-regexp@^1.0.5: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= +escape-string-regexp@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-3.0.0.tgz#1dad9cc28aed682be0de197280f79911a5fccd61" + integrity sha512-11dXIUC3umvzEViLP117d0KN6LJzZxh5+9F4E/7WLAAw7GrHk8NpUR+g9iJi/pe9C0py4F8rs0hreyRCwlAuZg== + eslint-scope@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.0.0.tgz#e87c8887c73e8d1ec84f1ca591645c358bfc8fb9" @@ -695,6 +710,16 @@ mute-stream@0.0.8: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== +mysql@^2.18.1: + version "2.18.1" + resolved "https://registry.yarnpkg.com/mysql/-/mysql-2.18.1.tgz#2254143855c5a8c73825e4522baf2ea021766717" + integrity sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig== + dependencies: + bignumber.js "9.0.0" + readable-stream "2.3.7" + safe-buffer "5.1.2" + sqlstring "2.3.1" + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -783,7 +808,7 @@ punycode@^2.1.0: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== -readable-stream@^2.3.5: +readable-stream@2.3.7, readable-stream@^2.3.5: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -848,16 +873,16 @@ rxjs@^6.5.3: dependencies: tslib "^1.9.0" +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + safe-buffer@^5.1.1, safe-buffer@^5.1.2: version "5.2.0" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== -safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - "safer-buffer@>= 2.1.2 < 3": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" @@ -923,6 +948,11 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= +sqlstring@2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/sqlstring/-/sqlstring-2.3.1.tgz#475393ff9e91479aea62dcaf0ca3d14983a7fb40" + integrity sha1-R1OT/56RR5rqYtyvDKPRSYOn+0A= + string-width@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961"