This commit is contained in:
Erik 2020-04-13 17:50:11 +03:00
commit 194942f5c4
33 changed files with 837 additions and 1407 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
options.json options.json
node_modules node_modules
.vscode/launch.json .vscode/launch.json
yarn-error.log

View File

@ -1,22 +0,0 @@
class Intercom {
constructor(manager, shardManager) {
this.manager = manager;
this.shardManager = shardManager;
// this.shardManager.on('message', this.receive.bind(this));
}
send(shard, message) {
shard.send(message);
}
receive(...args) {
console.log(args);
}
}
module.exports = Intercom;

View File

@ -1,11 +1,58 @@
const Winston = require('winston'); const { createLogger, format, transports, config } = require('winston');
const { combine, label, printf } = format;
const moment = require('moment');
const chalk = require('chalk');
class Logger { class Logger {
constructor(manager) { constructor(manager) {
this.manager = manager; this.manager = manager;
this.logger = createLogger({
transports: [
new transports.Console(),
new transports.File({ filename: `logs/${this.date.replace(/ /g, '-')}.log` }),
new transports.File({ filename: `logs/${this.date.replace(/ /g, '-')}-error.log`, level: 'error' })
]
});
this.client
.on('shardCreate', (shard) => this.write(shard, "Shard created.", 'DEBUG'))
.on('message', (shard, message) => this._handleMessage(shard, message));
}
//Messages coming from the shards process.send functions.
async _handleMessage(shard, message) {
if(message._ready
|| message._disconnect
|| message._reconnecting
|| message._sFetchProp
|| message._sEval
|| message._sRespawnAll) return undefined; //Properties used for discord.js internal sharding, must filter for.
}
//The MAIN function for writing everything to the logger.
async write(shard, string = '', type = 'silly') {
if(!config.npm.levels[type]) return undefined;
const header = `[${this.date}][shard-${this._shardId(shard)}]`;
//[04/02/2020 12:52:20][shard-00]
this.logger.log(type, string)
}
_shardId(shard) {
const id = shard.id;
return `${id}`.length === 1 ? `0${id}` : `${id}`;
}
get date() {
return moment().format("MM-DD-YYYY hh:mm:ss");
} }
} }

View File

@ -2,8 +2,6 @@ const { EventEmitter } = require('events');
const ShardManager = require('./middleware/ShardManager.js'); const ShardManager = require('./middleware/ShardManager.js');
const StorageManager = require('./storage/StorageManager.js'); const StorageManager = require('./storage/StorageManager.js');
const Registry = require('./Registry.js');
const Intercom = require('./Intercom.js');
const Logger = require('./Logger.js'); const Logger = require('./Logger.js');
const { Command, Setting, Inhibitor } = require('./structure/interfaces/'); const { Command, Setting, Inhibitor } = require('./structure/interfaces/');
@ -14,13 +12,10 @@ class Manager extends EventEmitter {
super(); super();
this.registry = new Registry(this); this.shardManager = new ShardManager('./structure/client/DiscordClient.js', options);
this.shardManager = new ShardManager('./middleware/client/DiscordClient.js', options);
this.storageManager = new StorageManager(this, options.storage) this.storageManager = new StorageManager(this, options.storage)
.initialize(); .initialize();
this.intercom = new Intercom(this, this.shardManager);
this.logger = new Logger(this); this.logger = new Logger(this);
this._built = false; this._built = false;
@ -28,13 +23,8 @@ class Manager extends EventEmitter {
} }
async build() { async build() {
try {
await this.shardManager.spawn();
}catch(e) {
console.log(e);
}
await this.registry.loadComponents('components/commands/', Command);
await this.shardManager.spawn();
this._built = true; this._built = true;

View File

@ -63,7 +63,7 @@ class Shard extends EventEmitter {
this.once('ready', resolve); this.once('ready', resolve);
this.once('disconnect', () => reject(new Error(`[shard${this.id}] Shard disconnected while readying.`))); 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.`))); 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.`)), 10000); setTimeout(() => reject(new Error(`[shard${this.id}] Shard timed out while readying.`)), 30000);
}); });
return this.process || this.worker; return this.process || this.worker;

View File

@ -1,23 +0,0 @@
class Transporter {
constructor(client, opts) {
this.client = client;
// process.on('message', this.receive.bind(this));
}
async send() {
process.send();
}
async receive(message) {
}
}
module.exports = Transporter;

View File

@ -18,10 +18,12 @@
}, },
"homepage": "https://github.com/Navy-gif/New-GBot#readme", "homepage": "https://github.com/Navy-gif/New-GBot#readme",
"dependencies": { "dependencies": {
"chalk": "^4.0.0",
"common-tags": "^1.8.0", "common-tags": "^1.8.0",
"discord.js": "discordjs/discord.js", "discord.js": "discordjs/discord.js",
"escape-string-regexp": "^3.0.0", "escape-string-regexp": "^3.0.0",
"eslint": "^6.8.0", "eslint": "^6.8.0",
"moment": "^2.24.0",
"mongodb": "^3.5.5", "mongodb": "^3.5.5",
"mysql": "^2.18.1", "mysql": "^2.18.1",
"node-fetch": "^2.6.0", "node-fetch": "^2.6.0",

View File

@ -1,14 +1,15 @@
class Provider { class Provider {
constructor(manager, config) { constructor(manager, config, name) {
if(!config) throw new Error('No config file provided!'); 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[name];
if(config && (!this.config.database || !this.config.host)) throw new Error('Invalid config file provided!' + JSON.stringify(this.config));
this.manager = manager; this.manager = manager;
this.config = config;
this.db; this.db;
this.loaded = false; this.loaded = false;
this.name = name;
} }

View File

@ -1,15 +1,34 @@
const { Collection } = require('../util/'); const { Collection } = require('../util/');
const path = require('path');
const fs = require('fs');
class StorageManager { class StorageManager {
constructor(manager) { constructor(manager, options = {}) {
this.providers = new Collection(); this.providers = new Collection();
this.manager = manager;
this.options = options;
} }
async initialize() { async initialize() {
console.log('Initiating storage providers');
let _providers = path.join(process.cwd(), 'storage', 'providers');
let providers = fs.readdirSync(_providers);
for(let _provider of providers) {
let provider = require(path.join(_providers, _provider));
provider = new provider(this.manager, this.options);
await provider.init();
this.providers.set(provider.name, provider);
}
} }
} }

View File

@ -1,11 +1,11 @@
const Provider = require('./Provider.js'); const Provider = require('../Provider.js');
const MySQL = require('mysql'); const MySQL = require('mysql');
class MariaDBProvider extends Provider { class MariaDBProvider extends Provider {
constructor(manager, config) { constructor(manager, config) {
super(manager, config); super(manager, config, 'mariadb');
} }
@ -49,7 +49,7 @@ class MariaDBProvider extends Provider {
* Query using SQL to MariaDB * Query using SQL to MariaDB
* *
* @param {string} query SQL query string. * @param {string} query SQL query string.
* @param {array} values Array of values to replace ? with in the query string * @param {array<Object>} values Array of values to replace ? with in the query string
* @returns {object} Returns an object containing the query result * @returns {object} Returns an object containing the query result
* @memberof MariaDBProvider * @memberof MariaDBProvider
*/ */

View File

@ -1,11 +1,11 @@
const Provider = require('./Provider.js'); const Provider = require('../Provider.js');
const { MongoClient } = require('mongodb'); const { MongoClient } = require('mongodb');
class MongoDBProvider extends Provider { class MongoDBProvider extends Provider {
constructor(manager, config) { constructor(manager, config) {
super(manager, config); super(manager, config, 'mongodb');
this.client; this.client;
@ -13,7 +13,7 @@ class MongoDBProvider extends Provider {
async init() { async init() {
this.manager.logger.log('Initializing mongodb.'); //this.manager.logger.log('Initializing mongodb.');
try { try {
@ -44,11 +44,11 @@ class MongoDBProvider extends Provider {
find(db, query) { 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)}`); //if(this.manager.debug) this.manager.logger.debug(`Incoming find query for ${db} with parameters ${JSON.stringify(query)}`);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if(!this.loaded) reject(new Error('MongoDB not connected'));
this.db.collection(db).find(query, async (error, cursor) => { this.db.collection(db).find(query, async (error, cursor) => {
if(error) return reject(error); if(error) return reject(error);
@ -71,11 +71,11 @@ class MongoDBProvider extends Provider {
findOne(db, query) { 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)}`); //if(this.manager.debug) this.manager.logger.debug(`Incoming findOne query for ${db} with parameters ${JSON.stringify(query)}`);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if(!this.loaded) reject(new Error('MongoDB not connected'));
this.db.collection(db).findOne(query, async (error, item) => { this.db.collection(db).findOne(query, async (error, item) => {
if(error) return reject(error); if(error) return reject(error);
@ -99,11 +99,11 @@ class MongoDBProvider extends Provider {
*/ */
updateOne(db, query, data, upsert = false) { updateOne(db, query, data, upsert = false) {
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)}`); //if(this.manager.debug) this.manager.logger.debug(`Incoming updateOne query for ${db} with parameters ${JSON.stringify(filter)}`);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if(!this.loaded) reject(new Error('MongoDB not connected'));
this.db.collection(db).updateOne(query, { $set: data }, { upsert: upsert }, async (error, result) => { this.db.collection(db).updateOne(query, { $set: data }, { upsert: upsert }, async (error, result) => {
if(error) return reject(error); if(error) return reject(error);
@ -130,11 +130,11 @@ class MongoDBProvider extends Provider {
*/ */
push(db, query, data, upsert = false) { 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)}`); //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) => { return new Promise((resolve, reject) => {
if(!this.loaded) reject(new Error('MongoDB not connected'));
this.db.collection(db).updateOne(query, { $push: data }, { upsert: upsert }, async (error, result) => { this.db.collection(db).updateOne(query, { $push: data }, { upsert: upsert }, async (error, result) => {
if(error) return reject(error); if(error) return reject(error);
@ -157,13 +157,13 @@ class MongoDBProvider extends Provider {
*/ */
random(db, query = {}, amount = 1) { 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(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; if(amount > 100) amount = 100;
return new Promise((resolve, reject)=>{ return new Promise((resolve, reject)=>{
if(!this.loaded) reject(new Error('MongoDB not connected'));
this.db.collection(db).aggregate([{ $match: query }, { $sample: {size: amount}}], function(err, item) { this.db.collection(db).aggregate([{ $match: query }, { $sample: {size: amount}}], function(err, item) {
if(err) return reject(err); if(err) return reject(err);

View File

@ -2,12 +2,14 @@ const { Client } = require('discord.js');
const options = require('../../options.json'); const options = require('../../options.json');
const Registry = require('./Registry.js')
const EventHooker = require('./EventHooker.js'); const EventHooker = require('./EventHooker.js');
const Dispatcher = require('./Dispatcher.js') const Dispatcher = require('./Dispatcher.js')
const Resolver = require('./Resolver.js'); const Resolver = require('./Resolver.js');
const Transporter = require('./Transporter.js'); const Transporter = require('./Transporter.js');
const { Guild, User, Message } = require('../../structure/extensions/'); const { Guild, User, Message } = require('../../structure/extensions/');
const { Command, Observer, Inhibitor, Setting } = require('../../structure/interfaces/');
class DiscordClient extends Client { class DiscordClient extends Client {
@ -15,6 +17,7 @@ class DiscordClient extends Client {
super(options.bot.clientOptions); super(options.bot.clientOptions);
this.registry = new Registry(this);
this.eventHooker = new EventHooker(this); this.eventHooker = new EventHooker(this);
this.dispatcher = new Dispatcher(this); this.dispatcher = new Dispatcher(this);
this.resolver = new Resolver(this); this.resolver = new Resolver(this);
@ -23,30 +26,33 @@ class DiscordClient extends Client {
this._options = options; this._options = options;
this._built = false; this._built = false;
process.send({
});
process.on('message', (message) => {
});
} }
async build() { async build() {
if(this._built) return undefined;
console.log('Building Discord client');
await super.login(this._options.bot.token); await super.login(this._options.bot.token);
await this.registry.loadComponents('components/commands/', Command);
await this.registry.loadComponents('components/observers', Observer);
this.on('message', (message) => { await this.dispatcher.dispatch();
console.log(message);
if(message.content === "kms") {
message.reply("ok");
}
});
this._built = true; this._built = true;
this.on('ready', () => {
console.log('Client websocket is ready.');
});
this.registry.on('componentUpdate', (comp, type) => {
console.log(`[registry][${type}] ${comp.resolveable}`)
});
console.log('Client built');
} }
@ -57,3 +63,7 @@ module.exports = DiscordClient;
const client = new DiscordClient(options); const client = new DiscordClient(options);
client.build(); client.build();
process.on("unhandledRejection", (error) => {
console.error("Unhandled promise rejection:", error); //eslint-disable-line no-console
});

View File

@ -14,7 +14,7 @@ class Dispatcher {
for(const observer of observers.values()) { for(const observer of observers.values()) {
for(let [hook, func] of observer.hooks) { for(let [hook, func] of observer.hooks) {
this.client.hooker.hook(hook, func); this.client.eventHooker.hook(hook, func);
} }
} }

View File

@ -0,0 +1,62 @@
const chalk = require('chalk');
class Logger {
constructor(client) {
this.client = client;
this.client.hooker.hook('ready', () => {
this.transport(`Client connected to ${chalk.bold(this.client.user.tag)} with ${chalk.bold(`${this.client.guilds.size} guild${this.client.guilds.size === 1 ? '' : 's'}`)}.`, { embed: true, type: 'SUCCESS' });
});
this.client.hooker.hook('componentUpdate', ({ component, type }) => {
this.info(`Component ${chalk.bold(component.resolveable)} was ${chalk.bold(Constants.ComponentTypes[type])}.`);
});
this.client.hooker.hook('reconnect', () => {
this.warn(`Shard is reconnecting.`, { embed: true });
});
}
async transport(message = 'N/A', opts = {}) {
process.send({ message, ...opts });
}
/* Quick & Dirty Functions */
log(message, opts = {}) {
this.transport(message, { ...opts, type: 'LOG' });
}
info(message, opts = {}) {
this.transport(message, { ...opts, type: 'INFO' });
}
warn(message, opts = {}) {
this.transport(message, { ...opts, type: 'WARN' });
}
debug(message, opts = {}) {
this.transport(message, { ...opts, type: 'DEBUG' });
}
error(message, opts = {}) {
this.transport(message, { ...opts, type: 'ERROR' });
}
}
module.exports = Logger;
const Constants = {
ComponentTypes: {
LOAD: 'loaded',
UNLOAD: 'unloaded',
RELOAD: 'reloaded',
ENABLE: 'enabled',
DISABLE: 'disabled'
}
};

View File

@ -1,21 +1,24 @@
const path = require('path'); const path = require('path');
const { EventEmitter } = require('events'); const { EventEmitter } = require('events');
const { Collection, Util } = require('./util/'); const { Collection, Util } = require('../../util/');
const { Component, Module } = require('../../structure/interfaces');
class Registry extends EventEmitter { class Registry extends EventEmitter {
constructor(manager) { constructor(client) {
super(); super();
this.client = client;
this.components = new Collection(); this.components = new Collection();
} }
async loadComponents(dir) { async loadComponents(dir, classToHandle) {
const directory = path.join(process.cwd(), 'structure/', dir); //Finds directory of component folder relative to current working directory. const directory = path.join(process.cwd(), 'structure/client/', 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 files = Util.readdirRecursive(directory); //Loops through all folders in the directory and returns the files.
const loaded = []; const loaded = [];
@ -56,7 +59,7 @@ class Registry extends EventEmitter {
if(directory) component.directory = directory; if(directory) component.directory = directory;
if(component.module && typeof component.module === 'string') { //Sets modules or "groups" for each component, specified by their properties. 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}`); let module = this.components.get(`module:${component.module}`);
if(!module) module = await this.loadComponent(new Module(this.manager, { name: component.module })); if(!module) module = await this.loadComponent(new Module(this.client, { name: component.module }));
this.components.set(module.resolveable, module); this.components.set(module.resolveable, module);
component.module = module; component.module = module;

View File

@ -57,6 +57,14 @@ class Resolver {
} }
/**
* Resolve several user resolveables
*
* @param {array<string>} [resolveables=[]] an array of user resolveables (name, id, tag)
* @param {boolean} [strict=false] whether or not to attempt resolving by partial usernames
* @returns {array || boolean} Array of resolved users or false if none were resolved
* @memberof Resolver
*/
async resolveUsers(resolveables = [], strict = false) { async resolveUsers(resolveables = [], strict = false) {
if(typeof resolveables === 'string') resolveables = [ resolveables ]; if(typeof resolveables === 'string') resolveables = [ resolveables ];
@ -100,11 +108,20 @@ class Resolver {
} }
return resolved; return resolved.length > 0 ? resolved : false;
} }
async resolveMember(resolveables = [], strict = false, guild) { /**
* Resolve multiple member resolveables
*
* @param {array<string>} [resolveables=[]] an array of member resolveables (name, nickname, tag, id)
* @param {boolean} [strict=false] whether or not to attempt resolving by partial matches
* @param {Guild} guild the guild in which to look for members
* @returns {array<GuildMember> || boolean} an array of resolved members or false if none were resolved
* @memberof Resolver
*/
async resolveMembers(resolveables = [], guild, strict = false) {
if(typeof resolveables === 'string') resolveables = [ resolveables ]; if(typeof resolveables === 'string') resolveables = [ resolveables ];
if(resolveables.length === 0) return false; if(resolveables.length === 0) return false;
@ -151,7 +168,109 @@ class Resolver {
} }
return resolved; return resolved.length > 0 ? resolved : false;
}
/**
* Resolve multiple channels
*
* @param {array<string>} [resolveables=[]] an array of channel resolveables (name, id)
* @param {guild} guild the guild in which to look for channels
* @param {boolean} [strict=false] whether or not partial names are resolved
* @returns {array<GuildChannel> || false} an array of guild channels or false if none were resolved
* @memberof Resolver
*/
async resolveChannels(resolveables = [], guild, strict = false) {
if(typeof resolveables === 'string') resolveables = [ resolveables ];
if(resolveables.length === 0) return false;
let channels = guild.channels;
let resolved = [];
for(let resolveable of resolveables) {
let channel = channels.resolve(resolveable);
if(channel) {
resolved.push(channel);
continue;
}
let name = /^\#?([a-z0-9\-\_0]*)/i;
let id = /^\<\#([0-9]*)\>/i;
if(name.test(resolveable)) {
let match = resolveable.match(name);
let ch = match[1].toLowerCase();
let channel = channels.cache.filter(c => {
if(!strict) return c.name.toLowerCase().includes(ch)
return c.name.toLowerCase() === ch;
}).first(1);
if(channel) resolved.push(channel);
} else if(id.test(resolveable)) {
let match = resolveable.match(id);
let ch = match[1];
let channel = channels.resolve(ch);
if(channel) resolved.push(channel);
}
}
return resolved.length > 0 ? resolved : false;
}
/**
* Resolve multiple roles
*
* @param {array<string>} [resolveables=[]] an array of roles resolveables (name, id)
* @param {Guild} guild the guild in which to look for roles
* @param {boolean} [strict=false] whether or not partial names are resolved
* @returns {array<GuildRole> || false} an array of roles or false if none were resolved
* @memberof Resolver
*/
async resolveRoles(resolveables = [], guild, strict = false) {
if(typeof resolveables === 'string') resolveables = [ resolveables ];
if(resolveables.length === 0) return false;
let roles = guild.roles;
let resolved = [];
for(let resolveable of resolveables) {
let id = /^(<@&)?([0-9]{16,22})>?/i;
if(id.test(resolveable)) {
let match = resolveable.match(id);
let r_id = match[2];
let role = await roles.fetch(r_id).catch(console.error);
if(role) resolved.push(role);
} else {
let role = roles.cache.filter(r => {
if(!strict) return r.name.toLowerCase().includes(resolveable.toLowerCase());
return r.name.toLowerCase() === resolveable.toLowerCase();
}).first(1);
if(role) resolved.push(role);
}
}
return resolved.length > 0 ? reoslved : false;
} }

View File

@ -0,0 +1,45 @@
const { Command, Argument } = require('../../../../interfaces/');
class PingCommand extends Command {
constructor(client) {
super(client, {
name: 'ping',
module: 'utility',
description: "Determines the ping of the bot.",
arguments: [
new Argument(client, {
name: 'apple',
type: 'BOOLEAN',
types: ['VERBAL']
}),
new Argument(client, {
name: 'banana',
aliases: ['bans', 'bananas'],
type: 'INTEGER',
types: ['FLAG', 'VERBAL'],
default: 0
}),
new Argument(client, {
name: 'carrot',
aliases: ['cars', 'carrots'],
type: 'STRING',
required: true,
types: ['FLAG', 'VERBAL']
})
]
});
this.client = client;
}
async execute(message) {
message.reply("test");
}
}
module.exports = PingCommand;

View File

@ -0,0 +1,360 @@
const { stripIndents } = require('common-tags');
const escapeRegex = require('escape-string-regexp');
const { Observer } = require('../../../interfaces/');
class CommandHandler extends Observer {
constructor(client) {
super(client, {
name: 'commandHandler',
priority: 5,
guarded: true
});
this.client = client;
this.hooks = [
['message', this.handleMessage.bind(this)]
];
}
async handleMessage(message) {
//const time1 = new Date().getTime();
if(!this.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.content;
const args = content.split(' ');
const { command, newArgs } = await this._getCommand(message, args);
if(!command) return undefined;
message.command = command;
await this.handleCommand(message, newArgs);
//const time2 = new Date().getTime();
//console.log(`${time2-time1}ms`);
}
async _getCommand(message, [arg1, arg2, ...args]) {
const prefix = this.client._options.bot.prefix; //Change this for guild prefix settings.
let command = null;
let remains = [];
if(arg1 && arg1.startsWith(prefix)) {
const commandName = arg1.slice(1);
command = await this._matchCommand(message, commandName);
remains = [arg2, ...args];
} else if(arg1 && arg2 && arg1.startsWith('<@')){
const pattern = new RegExp(`^(<@!?${this.client.user.id}>)`, 'i');
if(arg2 && pattern.test(arg1)) {
command = await this._matchCommand(message, arg2);
}
remains = args
}
return { command, newArgs: remains };
}
async _matchCommand(message, commandName) {
const command = this.client.resolver.components(commandName, 'command', true)[0];
if(!command) return null;
//Eventually search for custom commands here.
return command;
}
/* Command Handling */
async handleCommand(message, args) {
const inhibitor = await this._handleInhibitors(message);
if(inhibitor.error) {
message.channel.send(`${inhibitor.resolveable} failed to pass.`);
return undefined;
}
const parsedArguments = await this._parseArguments(message, args);
}
async _handleInhibitors(message) {
const inhibitors = this.client.registry.components.filter(c=>c.type === 'inhibitor' && !c.disabled);
if(inhibitors.size === 0) return { error: false };
const promises = [];
for(const inhibitor of inhibitors.values()) {
if(inhibitor.guild && !message.guild) continue;
promises.push((async () => {
let inhibited = inhibitor.execute(message);
if(inhibited instanceof Promise) inhibited = await inhibited;
return inhibited;
})());
}
const reasons = (await Promise.all(promises)).filter(p=>p.error);
if(reasons.length === 0) return { error: false };
reasons.sort((a, b) => b.inhibitor.priority - a.inhibitor.priority);
return reasons[0];
}
async _parseArguments(message, args = []) {
const command = message.command;
let parsedArguments = [];
const parsedFlags = {};
const { shortFlags, longFlags, keys } = await this._createFlags(command.arguments);
const regex = new RegExp(`([0-9]*)(${Object.keys(longFlags).map(k=>escapeRegex(k)).join('|')})([0-9]*)`, 'i');
console.log(regex);
let currentArgument = null;
let params = [];
for(let i=0; i<args.length; i++) {
const word = args[i];
if(!word) continue;
const [one,two,...chars] = word.split('');
if(one === '-' && two !== '-') {
const name = [ two, ...chars ].join('');
if(!keys.includes(name)) continue;
currentArgument = shortFlags[name];
if(currentArgument.required && !args[i+1]) {
console.error(`Argument ${currentArgument.name} is required and was not provided.`);
return undefined;
}
continue;
} else if((one === '-' && two === '-') || one === '—') { //Handling for "long dash" on mobile phones x_x
const name = one === '—'
? [ two, ...chars ].join('').toLowerCase()
: chars.join('').toLowerCase(); //can convert to lowercase now that shortFlags are out of the way.
if(!keys.includes(name)) continue;
currentArgument = longFlags[name];
if(currentArgument.required && !args[i+1]) {
console.error(`Argument ${currentArgument.name} is required and was not provided.`);
return undefined;
}
continue;
} else {
let match = regex.exec(word);
if(match && match[2]) {
currentArgument = longFlags[match[2]];
if(params.length > 0 && ['INTEGER', 'FLOAT'].includes(currentArgument.type)) { //15 pts
console.log("asgsaiughasiguassag")
const lastItem = params[params.length-1];
const beforeError = await this._handleTypeParsing(currentArgument, lastItem);
if(beforeError) {
continue;
} else {
params.pop();
currentArgument.value = lastItem;
parsedArguments.push(currentArgument);
currentArgument = null;
continue;
}
}
const value = match[1] || match[3];
const error = await this._handleTypeParsing(currentArgument, value);
if(value) {
if(error) {
if(currentArgument.required) {
console.error(`Argument ${currentArgument.name} is required and failed to meet requirements.`);
return undefined;
} else {
parsedArguments.push(currentArgument);
currentArgument = null;
continue;
}
} else {
currentArgument.value = value;
parsedArguments.push(currentArgument);
currentArgument = null;
continue;
}
} else {
continue;
}
} else {
if(currentArgument) {
const error = await this._handleTypeParsing(currentArgument, word);
if(error) {
if(currentArgument.default) {
params.push(word);
currentArgument.value = currentArgument.default;
parsedArguments.push(currentArgument);
currentArgument = null;
continue;
}
if(currentArgument.required) {
if(currentArgument.infinite) {
if(currentArgument.value.length === 0) {
console.error(`1 Argument ${currentArgument.name} is required and failed to meet requirements.`);
return undefined;
} else {
parsedArguments.push(currentArgument);
currentArgument = null;
params.push(word);
continue;
}
} else {
console.error(`2 Argument ${currentArgument.name} is required and failed to meet requirements.`);
return undefined;
}
} else {
currentArgument = null;
params.push(word);
continue;
}
} else {
if(currentArgument.infinite) continue;
parsedArguments.push(currentArgument);
currentArgument = null;
continue;
}
} else {
params.push(word);
continue;
}
}
}
}
await message.channel.send(stripIndents`**arguments:** ${parsedArguments.map(a=>`${a.name}: ${a.value}`).join(' | ')}
**words:** ${params.join(', ')}`);
}
async _handleTypeParsing(argument, string) {
const parse = async (argument, string) => {
const { error, value } = await this.constructor.parseType(argument.type, string); //Cannot access static functions through "this".
if(error) return { error: true };
if(['INTEGER', 'FLOAT'].includes(argument.type)) {
const { min, max } = argument;
if(value > max && max !== null) {
return { error: true };
}
if(value < min && min !== null) {
return { error: true };
}
}
return { error: false, value };
}
const { error, value } = await parse(argument, string);
if(!error) {
argument.infinite
? argument.value.push(value)
: argument.value = value;
}
return error;
}
async _createFlags(args) {
let shortFlags = {};
let longFlags = {};
let keys = [];
for(const arg of args) {
let letters = [];
let names = [ arg.name, ...arg.aliases ];
keys = [...keys, ...names];
for(const name of names) {
longFlags[name] = arg;
if(!arg.types.includes('FLAG')) continue;
let letter = name.slice(0, 1);
if(letters.includes(letter)) continue;
if(keys.includes(letter)) letter = letter.toUpperCase();
if(keys.includes(letter)) break;
keys.push(letter);
letters.push(letter);
shortFlags[letter] = arg;
}
}
return { shortFlags, longFlags, keys };
}
static async parseType(type, str) { //this is in the class for a reason, will soon reference to a user resolver etc.
//INTEGER AND FLOAT ARE SAME FUNCTION
const types = {
STRING: (str) => {
return { error: false, value: `${str}` };
},
INTEGER: (str) => {
const int = parseInt(str);
if(Number.isNaN(int)) return { error: true }
return { error: false, value: int };
},
FLOAT: (str) => {
const float = parseInt(str);
if(Number.isNaN(float)) return { error: true }
return { error: false, value: float };
},
BOOLEAN: (str) => {
const truthy = ['yes', 'y', 'true', 't', 'on', 'enable'];
const falsey = ['no', 'n', 'false', 'f', 'off', 'disable'];
if(typeof str === 'boolean') return { error: false, value: str };
if(typeof str === 'string') str = str.toLowerCase();
if(truthy.includes(str)) return { error: false, value: true };
if(falsey.includes(str)) return { error: false, value: false };
return { error: true };
}
}
return await types[type](str);
}
}
module.exports = CommandHandler;

View File

@ -1,108 +0,0 @@
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

@ -12,6 +12,23 @@ const Guild = Structures.extend('Guild', (Guild) => {
} }
async resolveMembers(members, strict = false) {
return await this.client.resolver.resolveMembers(members, this, strict);
}
async resolveChannels(channels, strict = false) {
return await this.client.resolver.resolveChannels(channels, this, strict);
}
async resolveRoles(roles, strict = false) {
return await this.client.resolver.resolveRoles(roles, this, strict);
}
} }
return ExtendedGuild; return ExtendedGuild;

View File

@ -12,6 +12,10 @@ const Message = Structures.extend('Message', (Message) => {
} }
async respond() {
}
} }
return ExtendedMessage; return ExtendedMessage;

View File

@ -8,8 +8,14 @@ const User = Structures.extend('User', (User) => {
super(...args); super(...args);
this._settings = null; //internal cache of current users' settings; should ALWAYS stay the same as database.
this._settings = null; //internal cache of current users' settings; should ALWAYS stay the same as database.
this._cached = Date.now();
}
get timeSinceCached() {
return Date.now()-this._cached;
} }
} }

View File

@ -0,0 +1,62 @@
const { Util } = require('../../util');
class Argument {
constructor(client, options = {}) {
this.client = client;
this.name = options.name;
this.description = options.description;
this.aliases = options.aliases || []; //Aliases will work for both verbal and flag types. Careful for multiple aliases starting with different letters: more flags, more confusing.
this.type = (options.type && Constants.Types.includes(options.type) ? options.type : 'BOOLEAN'); //What type the argument is ['STRING', 'INTEGER', 'FLOAT', 'BOOLEAN'].
this.types = options.types || ['FLAG', 'VERBAL']; //['FLAG'], ['VERBAL'], or ['FLAG', 'VERBAL']. Declares if argument can be used verbally-only, flag-only, or both.
this.prompts = options.prompts || {
MISSING: `Argument **${this.name}** is missing and is required.`,
INVALID: `Argument **${this.name}** must be a \`${this.type}\` value.`
} //Default prompts to be replied to the user if an argument is missing or invalid.
//NOTE: Instead of telling the person the argument is missing and is required, ask them and continue the command execution afterwards. More work to do.
this.required = options.type === 'BOOLEAN' ? false : Boolean(options.required); //If the argument must be required for the command to work. Booleans
this.default = options.default || Constants.Defaults[options.type];
this.infinite = Boolean(options.infinite); //Accepts infinite amount of arguments e.g. -u @nolan @navy. If false, will only detect one user.
// Will turn value into an array instead of a string!!!
this.min = typeof options.min === 'number' ? options.min : null; //Min/max will only be used for INTEGER/FLOAT types.
this.max = typeof options.max === 'number' ? options.max : null;
this.parser = options.parser || null; //Option to pass a function to verify values.
this.value = this.infinite ? [] : null; //The value provided to the flag; assigned in the command handler.
}
}
module.exports = Argument;
const Constants = {
Defaults: { //these dont really mean anything, just default values. Most important one is the boolean one.
STRING: 'okay',
INTEGER: 5,
FLOAT: 2.5,
BOOLEAN: true
},
Types: [
'STRING',
'INTEGER',
'FLOAT',
'BOOLEAN',
'MEMBER',
'CHANNEL'
],
ArgumentTypes: [
'FLAG',
'VERBAL'
]
};

View File

@ -2,17 +2,17 @@ const Component = require('./Component.js');
class Command extends Component { class Command extends Component {
constructor(client, opts = {}) { constructor(manager, opts = {}) {
if(!opts) return null; if(!opts) return null;
super(client, { super(manager, {
id: opts.name, id: opts.name,
type: 'command', type: 'command',
disabled: opts.disabled || false, disabled: opts.disabled || false,
guarded: opts.guarded || false guarded: opts.guarded || false
}); });
Object.defineProperty(this, 'client', { value: client }); this.manager = manager;
this.name = opts.name; this.name = opts.name;
this.module = opts.module; this.module = opts.module;
@ -25,6 +25,7 @@ class Command extends Component {
this.restricted = Boolean(opts.restricted); this.restricted = Boolean(opts.restricted);
this.archivable = opts.archivable === undefined ? false : Boolean(opts.archivable); this.archivable = opts.archivable === undefined ? false : Boolean(opts.archivable);
this.guildOnly = Boolean(opts.guildOnly); this.guildOnly = Boolean(opts.guildOnly);
this.arguments = opts.arguments || [];
this.clientPermissions = opts.clientPermissions || []; this.clientPermissions = opts.clientPermissions || [];
this.memberPermissions = opts.memberPermissions || []; this.memberPermissions = opts.memberPermissions || [];

View File

@ -1,9 +1,9 @@
class Component { class Component {
constructor(manager, opts = {}) { constructor(client, opts = {}) {
if(!opts) return null; if(!opts) return null;
this.manager = manager; this.client = client;
this.id = opts.id; this.id = opts.id;
this.type = opts.type; this.type = opts.type;
@ -13,7 +13,7 @@ class Component {
this.guarded = Boolean(opts.guarded); this.guarded = Boolean(opts.guarded);
this.disabled = Boolean(opts.disabled); this.disabled = Boolean(opts.disabled);
this.registry = this.manager.registry; this.registry = this.client.registry;
} }
@ -55,7 +55,7 @@ class Component {
newModule = require(this.directory); newModule = require(this.directory);
if(typeof newModule === 'function') { if(typeof newModule === 'function') {
newModule = new newModule(this.manager); newModule = new newModule(this.client);
} }
this.registry.unloadComponent(this); this.registry.unloadComponent(this);

View File

@ -1,19 +1,17 @@
const Component = require('./Component.js'); const Component = require('./Component.js');
const Collection = require('../../../util/interfaces/Collection.js'); const { Collection } = require('../../util');
class Module extends Component { class Module extends Component {
constructor(client, opts = {}) { constructor(manager, opts = {}) {
if(!opts) return null; if(!opts) return null;
super(client, { super(manager, {
id: opts.name, id: opts.name,
type: 'module' type: 'module'
}); });
Object.defineProperty(this, 'client', { this.manager = manager;
value: client
});
this.name = opts.name; this.name = opts.name;
this.components = new Collection(); this.components = new Collection();

View File

@ -15,10 +15,6 @@ class Observer extends Component {
this.priority = opts.priority || 1; this.priority = opts.priority || 1;
this.hooks = opts.hooks || []; this.hooks = opts.hooks || [];
Object.defineProperty(this, 'client', {
value: client
});
} }
execute() { execute() {

View File

@ -1,6 +1,9 @@
module.exports = { module.exports = {
Argument: require('./Argument.js'),
Command: require('./Command.js'), Command: require('./Command.js'),
Component: require('./Component.js'), Component: require('./Component.js'),
Inhibitor: require('./Inhibitor.js'), Inhibitor: require('./Inhibitor.js'),
Setting: require('./Setting.js') Setting: require('./Setting.js'),
} Module: require('./Module.js'),
Observer: require('./Observer.js')
};

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -169,6 +169,14 @@ chalk@^3.0.0:
ansi-styles "^4.1.0" ansi-styles "^4.1.0"
supports-color "^7.1.0" supports-color "^7.1.0"
chalk@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.0.0.tgz#6e98081ed2d17faab615eb52ac66ec1fe6209e72"
integrity sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==
dependencies:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
chardet@^0.7.0: chardet@^0.7.0:
version "0.7.0" version "0.7.0"
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
@ -787,6 +795,11 @@ mkdirp@^0.5.1:
dependencies: dependencies:
minimist "^1.2.5" minimist "^1.2.5"
moment@^2.24.0:
version "2.24.0"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==
mongodb@^3.5.5: mongodb@^3.5.5:
version "3.5.5" version "3.5.5"
resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.5.5.tgz#1334c3e5a384469ac7ef0dea69d59acc829a496a" resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.5.5.tgz#1334c3e5a384469ac7ef0dea69d59acc829a496a"