restructuring changes & command handling

This commit is contained in:
noolaan 2020-04-09 15:08:28 -06:00
parent 69ba7f0291
commit 8116a5a004
27 changed files with 364 additions and 1383 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
options.json
node_modules
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,13 +1,25 @@
const Winston = require('winston');
const winston = require('winston');
const moment = require('moment');
class Logger {
constructor(manager) {
this.manager = manager;
this.logger = winston.createLogger({
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: `${Date.now()}.log` }),
new winston.transports.File({ filename: `${Date.now()}-error.log`, level: 'error' })
]
});
}
get date() {
return moment().format("MM/DD/YYYY hh:mm:ss");
}
}
module.exports = Logger;

View File

@ -2,8 +2,6 @@ const { EventEmitter } = require('events');
const ShardManager = require('./middleware/ShardManager.js');
const StorageManager = require('./storage/StorageManager.js');
const Registry = require('./Registry.js');
const Intercom = require('./Intercom.js');
const Logger = require('./Logger.js');
const { Command, Setting, Inhibitor } = require('./structure/interfaces/');
@ -14,13 +12,10 @@ class Manager extends EventEmitter {
super();
this.registry = new Registry(this);
this.shardManager = new ShardManager('./middleware/client/DiscordClient.js', options);
this.shardManager = new ShardManager('./structure/client/DiscordClient.js', options);
this.storageManager = new StorageManager(this, options.storage)
.initialize();
this.intercom = new Intercom(this, this.shardManager);
this.logger = new Logger(this);
this._built = false;
@ -28,13 +23,8 @@ class Manager extends EventEmitter {
}
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;

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

@ -22,6 +22,7 @@
"discord.js": "discordjs/discord.js",
"escape-string-regexp": "^3.0.0",
"eslint": "^6.8.0",
"moment": "^2.24.0",
"mongodb": "^3.5.5",
"mysql": "^2.18.1",
"node-fetch": "^2.6.0",

View File

@ -2,12 +2,14 @@ const { Client } = require('discord.js');
const options = require('../../options.json');
const Registry = require('./Registry.js')
const EventHooker = require('./EventHooker.js');
const Dispatcher = require('./Dispatcher.js')
const Resolver = require('./Resolver.js');
const Transporter = require('./Transporter.js');
const { Guild, User, Message } = require('../../structure/extensions/');
const { Command, Observer, Inhibitor, Setting } = require('../../structure/interfaces/');
class DiscordClient extends Client {
@ -15,6 +17,7 @@ class DiscordClient extends Client {
super(options.bot.clientOptions);
this.registry = new Registry(this);
this.eventHooker = new EventHooker(this);
this.dispatcher = new Dispatcher(this);
this.resolver = new Resolver(this);
@ -23,27 +26,18 @@ class DiscordClient extends Client {
this._options = options;
this._built = false;
process.send({
});
process.on('message', (message) => {
});
}
async build() {
if(this._built) return undefined;
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) => {
console.log(message);
if(message.content === "kms") {
message.reply("ok");
}
});
await this.dispatcher.dispatch();
this._built = true;

View File

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

View File

@ -1,21 +1,24 @@
const path = require('path');
const { EventEmitter } = require('events');
const { Collection, Util } = require('./util/');
const { Collection, Util } = require('../../util/');
const { Component, Module } = require('../../structure/interfaces');
class Registry extends EventEmitter {
constructor(manager) {
constructor(client) {
super();
this.client = client;
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 loaded = [];
@ -53,10 +56,12 @@ class Registry extends EventEmitter {
return null;
}
console.log(directory);
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 }));
if(!module) module = await this.loadComponent(new Module(this.client, { name: component.module }));
this.components.set(module.resolveable, module);
component.module = module;

View File

@ -0,0 +1,12 @@
class Transporter {
constructor(manager) {
this.manager = manager;
}
}
module.exports = Transporter;

View File

@ -0,0 +1,38 @@
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
})
]
});
this.client = client;
}
async execute(message) {
message.reply("test");
}
}
module.exports = PingCommand;

View File

@ -0,0 +1,186 @@
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)]
];
this.commandPatterns = new Map();
}
async handleMessage(message) {
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;
return await this.handleCommand(message, newArgs);
}
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 flags = {};
const { shortFlags, longFlags, keys } = await this._createFlags(command.arguments);
console.log(shortFlags, longFlags, keys);
message.channel.send(keys);
let currentFlag = null;
for(const word of args) {
// if(currentFlag)
for(const key of keys) {
const regex = new RegExp('/[0-9]*([a-zA-Z]+)[0-9]*/g', 'g');
const match = regex.exec(word);
console.log(match);
if(!match) continue;
}
}
}
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.name;
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.name;
}
}
return { shortFlags, longFlags, keys };
}
}
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,10 @@ const Message = Structures.extend('Message', (Message) => {
}
async respond() {
}
}
return ExtendedMessage;

View File

@ -8,8 +8,14 @@ const User = Structures.extend('User', (User) => {
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,56 @@
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.
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.
}
}
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'
],
ArgumentTypes: [
'FLAG',
'VERBAL'
]
};

View File

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

View File

@ -1,9 +1,9 @@
class Component {
constructor(manager, opts = {}) {
constructor(client, opts = {}) {
if(!opts) return null;
this.manager = manager;
this.client = client;
this.id = opts.id;
this.type = opts.type;
@ -13,7 +13,7 @@ class Component {
this.guarded = Boolean(opts.guarded);
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);
if(typeof newModule === 'function') {
newModule = new newModule(this.manager);
newModule = new newModule(this.client);
}
this.registry.unloadComponent(this);

View File

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

View File

@ -11,14 +11,12 @@ class Observer extends Component {
disabled: opts.disabled
});
this.client = client;
this.name = opts.name;
this.priority = opts.priority || 1;
this.hooks = opts.hooks || [];
Object.defineProperty(this, 'client', {
value: client
});
}
execute() {

View File

@ -1,6 +1,9 @@
module.exports = {
Argument: require('./Argument.js'),
Command: require('./Command.js'),
Component: require('./Component.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 = {
Collection: require('./Collection.js'),
Util: require('./Util.js'),
Resolver: require('../middleware/client/Resolver.js')
Util: require('./Util.js')
}

File diff suppressed because it is too large Load Diff

View File

@ -787,6 +787,11 @@ mkdirp@^0.5.1:
dependencies:
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:
version "3.5.5"
resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.5.5.tgz#1334c3e5a384469ac7ef0dea69d59acc829a496a"