galactic-bot/structure/client/components/observers/CommandHandler.js
2020-07-11 23:40:05 +03:00

587 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const escapeRegex = require('escape-string-regexp');
const { Argument, Observer } = require('../../../interfaces/');
const Constants = {
QuotePairs: {
'"': '"', //regular double
"'": "'", //regular single
"": "", //smart single
"“": "”" //smart double
}
};
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._startQuotes = Object.keys(Constants.QuotePairs); //Used for the "getQuotes" function. Parses arguments with quotes in it to combine them into one array argument.
this._quoteMarks = this._startQuotes + Object.values(Constants.QuotePairs) // ^
.join('');
}
async handleMessage(message) {
if(!this.client._built
|| message.webhookID
|| message.author.bot
|| message.guild && (!message.guild.available || message.guild.banned)) return undefined;
//Don't parse commands if client isn't built (commands aren't loaded), is a webhook, bot, or if the guild isn't available/is banned.
if (message.guild) {
if (!message.member) await message.guild.members.fetch(message.author.id); //Fetch member if discord.js doesn't fetch them automatically.
}
const { command, parameters } = await this._getCommand(message);
if(!command) return undefined;
message.command = command;
// const timestamp1 = new Date().getTime();
const response = await this._parseArguments(parameters, command.arguments, message.guild);
if(response.error) {
return this.handleError(message, { type: 'argument', ...response });
}
if(command.keepQuotes) {
message.parameters = response.parameters.map(([param, isQuote]) => (isQuote ? `"${param}"` : param));
} else {
message.parameters = response.parameters.map((p) => p[0]);
}
// const timestamp2 = new Date().getTime();
// this.client.logger.debug(`Client took ${timestamp2-timestamp1}ms to parse arguments.`);
message.arguments = response.args;
return this.handleCommand(message, response.parameters);
}
async _parseArguments(parameters = [], args = [], guild = null) {
parameters = this._getQuotes(parameters.join(' '));
const { shortArgs, longArgs } = await this._createArguments(args);
const regex = new RegExp(`([0-9]*)(${Object.keys(longArgs).sort((a, b) => b.length - a.length)
.map((k) => escapeRegex(k))
.join('|')})([0-9]*)`, 'iu');
const findArgument = (word) => {
const matches = regex.exec(word);
const [one, two, ...chars] = word.split('');
let arg = null,
value = null;
if(one === '-' && two !== '-') {
const name = [two, ...chars].join('');
const shortFlag = shortArgs[name];
if(shortFlag) {
arg = shortFlag;
arg._flag = true;
}
} else if(one === '-' && two === '-' || one === '—' && two !== '—') {
const name = one === '—'
? [two, ...chars].join('')
: chars.join('');
const longFlag = longArgs[name];
if(longFlag) {
arg = longFlag;
arg._flag = true;
}
} else if(matches && (matches[1] || matches[3])) {
const [,, name] = matches;
const number = matches[1] || matches[3];
const argument = longArgs[name];
if(argument && argument.types.includes('VERBAL')) {
arg = longArgs[name];
value = number;
}
} else if(longArgs[word.toLowerCase()]) {
const argument = longArgs[word.toLowerCase()];
if(argument && argument.types.includes('VERBAL')) {
arg = longArgs[word.toLowerCase()];
}
}
return { arg, value };
};
const newParameters = [],
newArguments = [];
const lookBehind = async (argument) => { //Checks previous argument for an integer or float value, "15 points".
let response = {};
if(newParameters.length > 0 && ['INTEGER', 'FLOAT'].includes(argument.type)) {
const [lastWord] = newParameters[newParameters.length-1];
response = await this._parseArgumentType(argument, lastWord, guild);
if(!response.error) {
newParameters.pop(); //Deletes latest parameter.
newArguments.push(argument); //Adds argument with value of the latest parameter.
return { error: false };
}
}
return { error: true, ...response };
};
const existing = (argument) => {
let exists = false;
for(const { name } of newArguments) {
if(argument && argument.name === name) exists = true;
}
return exists;
};
let error = null,
currentArgument = null;
for(let i = 0; i < parameters.length; i++) {
const [word, isQuote] = parameters[i];
const { arg, value } = findArgument(word);
if(currentArgument) { //One of the previous words had an argument, trying to parse the type until error.
let response = await this._parseArgumentType(currentArgument, word, guild);
if(response.error) {
if(response.force) { //Overrides error if min/max (stupid)
error = { argument: currentArgument, ...response };
break;
}
const behind = await lookBehind(currentArgument); //Check for "15 points" (lookbehind the argument)
if(!behind.error) {
if(!currentArgument.infinite) currentArgument = null;
} else {
if(currentArgument.required) {
if(currentArgument.empty) {
if(behind.force) response = behind; //Overrides error if min/max (stupid)
error = { argument: currentArgument, ...response };
break;
}
} else if(currentArgument.empty) {
currentArgument.setDefault();
}
newArguments.push(currentArgument);
currentArgument = null;
}
if(arg && !existing(arg)) { //TODO: Add a function for this repetitive code
if(value) { //Pre-matched value (15pts/pts15) found by regex, won't need a separate word to parse the argument.
const response = await this._parseArgumentType(arg, value, guild);
if(arg.required) {
if(response.error) {
error = { argument: arg, ...response };
break;
}
}
newArguments.push(arg);
if(arg.infinite) currentArgument = arg;
} else {
currentArgument = arg;
}
} else {
newParameters.push([word, isQuote]);
}
} else {
newArguments.push(currentArgument);
if(!currentArgument.infinite) currentArgument = null;
}
} else if(arg && !existing(arg)) { //TODO: Add a function for this repetitive code //Trying to find a new argument to parse or add as a parameter.
if(value) { //Pre-matched value (15pts/pts15) found by regex, won't need a separate word to parse the argument.
const response = await this._parseArgumentType(arg, value, guild);
if(arg.required) {
if(response.error) {
error = { argument: arg, ...response };
break;
}
}
newArguments.push(arg);
if(arg.infinite) currentArgument = arg;
} else {
currentArgument = arg;
}
} else {
newParameters.push([ word, isQuote ]);
}
}
if(error) return error;
if(currentArgument) { //Add the last argument awaiting to be parsed (argument was left hanging at the end w/o a value)
const behind = await lookBehind(currentArgument);
if(!behind.error) {
newArguments.push(currentArgument);
} else {
if(currentArgument.empty) {
if(currentArgument.required) {
if(behind.force) return { argument: currentArgument, ...behind };
return {
index: 'COMMANDHANDLER_TYPE_ERROR',
argument: currentArgument,
error: true
};
}
currentArgument.setDefault();
}
newArguments.push(currentArgument);
}
}
const object = {};
newArguments.map((a) => object[a.name] = a); //eslint-disable-line no-return-assign
return { parameters: newParameters, args: object, error: false };
}
async _parseArgumentType(argument, string, guild) { //Parsing argument values to make sure they're correct.
const parse = async(argument, string, guild) => {
let { error, value } = await this.parseType(argument.type, string, guild); //eslint-disable-line prefer-const
if(error) {
return {
index: 'COMMANDHANDLER_TYPE_ERROR',
error
};
}
if(['INTEGER', 'FLOAT'].includes(argument.type)) {
const { min, max } = argument;
if(max !== null && value > max) {
if(!argument.ignoreInvalid) return {
index: `COMMANDHANDLER_NUMBERMAX_ERROR`,
args: { min, max },
force: true,
error: true
};
value = argument.max;
} else if(min !== null && value < min) {
if(!argument.ignoreInvalid) return {
index: `COMMANDHANDLER_NUMBERMIN_ERROR`,
args: { min, max },
force: true,
error: true
};
value = argument.min;
}
}
if(argument.options.length > 0 && !argument.options.includes(value)) return {
index: `COMMANDHANDLER_OPTIONS_ERROR`,
args: { options: argument.options.map((o) => `\`${o}\``).join(', ') },
error: true
};
return { error: false, value };
};
const response = await parse(argument, string, guild);
if(response.error) {
return response;
}
const { value } = response;
if(value) {
if(argument.infinite) argument.value.push(value);
else argument.value = value;
} else {
argument.setDefault(guild);
}
return response;
}
async _createArguments(args) {
const shortArgs = {},
longArgs = {};
let argKeys = [];
for(let arg of args) {
arg = new Argument(this.client, arg);
const letters = [];
const names = [ arg.name, ...arg.aliases ];
argKeys = [...argKeys, ...names];
for(const name of names) {
longArgs[name] = arg;
if(!arg.types.includes('FLAG')) continue;
let letter = name.slice(0, 1);
if(letters.includes(letter)) continue;
if(argKeys.includes(letter)) letter = letter.toUpperCase();
if(argKeys.includes(letter)) {
// this.client.logger.warn(`Command has too many arguments with the same first letter: ${argKeys.join(', ')}`);
break;
}
argKeys.push(letter);
letters.push(letter);
shortArgs[letter] = arg;
}
}
return { shortArgs, longArgs };
}
async _getCommand(message) {
const [ arg1, arg2, ...args ] = message.content.split(' ');
if(message.guild) await message.guild.settings();
const { prefix } = message;
let command = null,
remains = [];
if(arg1 && arg1.startsWith(prefix)) {
const commandName = arg1.slice(prefix.length); //Grabs the command name by slicing off the prefix.
command = await this._matchCommand(message, commandName);
remains = [arg2, ...args];
} else if(arg1 && arg2 && arg1.startsWith('<@')) { //Checks if the first argument is a mention and if a command is after it.
const pattern = new RegExp(`^(<@!?${this.client.user.id}>)`, 'iu');
if(arg2 && pattern.test(arg1)) {
command = await this._matchCommand(message, arg2);
}
remains = args;
}
return { command, parameters: remains };
}
async _matchCommand(message, commandName) {
const [ command ] = this.client.resolver.components(commandName, 'command', true);
if(!command) return null;
//Eventually search for custom commands here.
message._caller = commandName; //Used for hidden commands as aliases.
return command;
}
/* Command Handling */
async handleCommand(message) {
const inhibitors = await this._handleInhibitors(message);
const silent = inhibitors.filter((i) => i.inhibitor.silent);
const nonsilent = inhibitors.filter((i) => !i.inhibitor.silent);
if(nonsilent.length === 0 && silent.length > 0) return undefined;
if(nonsilent.length > 0) return this.handleError(message, { type: 'inhibitor', ...nonsilent[0] });
const resolved = await message.resolve();
if(resolved.error) {
this.client.logger.error(`Command Error | ${message.command.resolveable} | Message ID: ${message.id}\n${resolved.message}`);
return this.handleError(message, { type: 'command' });
}
return true;
}
async _handleInhibitors(message) {
const inhibitors = this.client.registry.components.filter((c) => c.type === 'inhibitor' && !c.disabled);
if(inhibitors.size === 0) return [];
const promises = [];
for(const inhibitor of inhibitors.values()) { // Loops through all inhibitors, executing them.
if(inhibitor.guild && !message.guild) continue;
promises.push((async () => {
let inhibited = inhibitor.execute(message, message.command);
if(inhibited instanceof Promise) inhibited = await inhibited;
return inhibited;
})());
}
const reasons = (await Promise.all(promises)).filter((p) => p.error); // Filters out inhibitors with only errors.
if(reasons.length === 0) return [];
reasons.sort((a, b) => b.inhibitor.priority - a.inhibitor.priority); // Sorts inhibitor errors by most important.
return reasons;
}
async parseType(type, str, guild) { //Types used for parsing argument types.
const types = {
STRING: (str) => ({ error: false, value: str }),
INTEGER: (str) => {
const int = parseInt(str);
if(Math.round(int) !== int) return { error: true };
if(isNaN(int)) return { error: true };
return { error: false, value: int };
},
FLOAT: (str) => {
const float = parseInt(str);
if(isNaN(float)) return { error: true };
return { error: false, value: float };
},
BOOLEAN: (str) => {
const bool = this.client.resolver.resolveBoolean(str);
if(bool === null) return { error: true };
return { error: false, value: bool };
},
USER: async (str) => {
const user = await this.client.resolver.resolveUser(str, true);
if(!user) return { error: true };
return { error: false, value: user };
},
MEMBER: async (str, guild) => {
const member = await this.client.resolver.resolveMember(str, true, guild);
if(!member) return { error: true };
return { error: false, value: member };
},
TEXTCHANNEL: async (str, guild) => {
const channel = await this.client.resolver.resolveChannel(str, true, guild, (channel) => channel.type === 'text');
if(!channel) return { error: true };
return { error: false, value: channel };
},
VOICECHANNEL: async(str, guild) => {
const channel = await this.client.resolver.resolveChannel(str, true, guild, (channel) => channel.type === 'voice');
if(!channel) return { error: true };
return { error: false, value: channel };
},
CHANNEL: async (str, guild) => {
const channel = await this.client.resolver.resolveChannel(str, true, guild);
if(!channel) return { error: true };
return { error: false, value: channel };
},
ROLE: async (str, guild) => {
const role = await this.client.resolver.resolveRole(str, true, guild);
if(!role) return { error: true };
return { error: false, value: role };
},
TIME: async(str) => {
const time = await this.client.resolver.resolveTime(str);
if(!time) return { error: true };
return { error: false, value: time };
}
};
return types[type](str, guild);
}
/* Made by my friend Qwerasd#5202 */
// I'm not entirely sure how this works, except for the fact that it loops through each CHARACTER and tries to match quotes together.
// Supposedly quicker than regex, and I'd agree with that statement. Big, messy, but quick.
_getQuotes(string) {
if(!string) return [];
let quoted = false,
wordStart = true,
startQuote = '',
endQuote = false,
isQuote = false,
word = '';
const words = [],
chars = string.split('');
chars.forEach((char) => {
if((/\s/u).test(char)) {
if(endQuote) {
quoted = false;
endQuote = false;
isQuote = true;
}
if(quoted) {
word += char;
} else if(word !== '') {
words.push([ word, isQuote ]);
isQuote = false;
startQuote = '';
word = '';
wordStart = true;
}
} else if(this._quoteMarks.includes(char)) {
if (endQuote) {
word += endQuote;
endQuote = false;
}
if(quoted) {
if(char === Constants.QuotePairs[startQuote]) {
endQuote = char;
} else {
word += char;
}
} else if(wordStart && this._startQuotes.includes(char)) {
quoted = true;
startQuote = char;
} else {
word += char;
}
} else {
if(endQuote) {
word += endQuote;
endQuote = false;
}
word += char;
wordStart = false;
}
});
if (endQuote) {
words.push([ word, true ]);
} else {
word.split(/\s/u).forEach((subWord, i) => {
if (i === 0) {
words.push([ startQuote+subWord, false ]);
} else {
words.push([ subWord, false ]);
}
});
}
return words;
}
async handleError(message, error) { //Handle different types of errors
const messages = {
command: async () => message.format('COMMANDHANDLER_COMMAND_ERROR', {
invite: this.client._options.bot.invite,
id: message.id,
command: message.command.moduleResolveable
}),
inhibitor: ({ inhibitor, args }) => `${message.format(inhibitor.index, { command: message.command.moduleResolveable, ...args })} **\`[${inhibitor.resolveable}]\`**`,
argument: ({ index, args, argument }) => {
const type = message.format('COMMANDHANDLER_TYPES', { type: argument.type }, true);
return message.format(index, { type, arg: `${message.command.name}:${argument.name}`, ...args });
}
};
//return this.client.rateLimiter.limitSend(message.channel, await messages[error.type](error));
return message.limitedRespond(await messages[error.type](error), { emoji: 'failure', limit: 10, utility: error.type });
}
}
module.exports = CommandHandler;