fucking mess

This commit is contained in:
noolaan 2020-04-08 10:08:46 -06:00
parent da8871b0d6
commit 34a006e351
29 changed files with 1301 additions and 28 deletions

View File

@ -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();

View File

@ -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);
}
}

9
index.js Normal file
View File

@ -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
});

View File

@ -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;

View File

@ -1,3 +1,156 @@
class ShardManager {
/* 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;

View File

View File

@ -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();

View File

@ -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;

View File

@ -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;

View File

@ -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))));
};

View File

@ -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"
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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;
module.exports = MongoDBProvider;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,5 @@
module.exports = {
Message: require('./Message.js'),
Guild: require('./Guild.js'),
User: require('./User.js')
};

View File

@ -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}`;
}
}

View File

@ -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;

View File

@ -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 };
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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;
const Constants = {
Resolves: [
'GUILD',
'USER'
]
};

View File

@ -1,5 +1,6 @@
module.exports = {
Command: require('./Command.js'),
Component: require('./Component.js'),
Inhibitor: require('./Inhibitor.js'),
Setting: require('./Setting.js')
}

33
util/Util.js Normal file
View File

@ -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;

View File

@ -1,3 +1,5 @@
module.exports = {
Collection: require('./Collection.js');
Collection: require('./Collection.js'),
Util: require('./Util.js'),
Resolver: require('../middleware/client/Resolver.js')
}

View File

@ -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"