galactic-bot/structure/client/components/observers/CommandHandler.js
2020-04-12 15:35:13 +03:00

360 lines
12 KiB
JavaScript

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;