galactic-bot/structure/client/components/observers/CommandHandler.js

602 lines
23 KiB
JavaScript
Raw Normal View History

2020-04-11 15:56:52 +02:00
const escapeRegex = require('escape-string-regexp');
const moment = require('moment');
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.
2020-04-19 21:53:59 +02:00
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 });
2020-06-04 19:59:09 +02:00
}
if(command.keepQuotes) {
message.parameters = response.parameters.map(([param, isQuote]) => isQuote ? `"${param}"` : param);
2020-06-04 19:59:09 +02:00
} else {
message.parameters = response.parameters.map((p) => p[0]);
}
2020-04-14 17:05:56 +02:00
// 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];
2020-06-04 19:59:09 +02:00
const argument = longArgs[name];
if(argument && argument.types.includes('VERBAL')) {
arg = longArgs[name];
value = number;
}
} else if(longArgs[word.toLowerCase()]) {
2020-06-04 19:59:09 +02:00
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)) {
2020-06-04 19:59:09 +02:00
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;
2020-06-04 19:59:09 +02:00
for(let i = 0; i < parameters.length; i++) {
2020-06-04 19:59:09 +02:00
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;
2020-04-11 15:56:52 +02:00
}
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;
2020-04-11 15:56:52 +02:00
}
} else if(currentArgument.empty) {
currentArgument.setDefault();
2020-04-11 15:56:52 +02:00
}
newArguments.push(currentArgument);
currentArgument = null;
2020-04-11 15:56:52 +02:00
}
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]);
}
2020-04-11 15:56:52 +02:00
} 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;
2020-04-11 15:56:52 +02:00
}
}
newArguments.push(arg);
if(arg.infinite) currentArgument = arg;
} else {
currentArgument = arg;
2020-04-11 12:00:53 +02:00
}
} else {
newParameters.push([ word, isQuote ]);
}
}
if(error) return error;
2020-04-21 19:56:31 +02:00
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);
}
}
2020-04-21 19:56:31 +02:00
const object = {};
newArguments.map((a) => object[a.name] = a); //eslint-disable-line no-return-assign
return { parameters: newParameters, args: object, error: false };
2020-04-11 15:56:52 +02:00
}
async _parseArgumentType(argument, string, guild) { //Parsing argument values to make sure they're correct.
const parse = async(argument, string, guild) => {
2020-06-04 19:59:09 +02:00
let { error, value } = await this.parseType(argument.type, string, guild); //eslint-disable-line prefer-const
if(error) {
return {
index: 'COMMANDHANDLER_TYPE_ERROR',
error
};
}
2020-04-11 12:00:53 +02:00
2020-04-11 15:56:52 +02:00
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;
2020-04-11 15:56:52 +02:00
}
2020-04-11 12:00:53 +02:00
}
2020-04-11 15:56:52 +02:00
if(argument.options.length > 0 && !argument.options.includes(value)) return {
index: `COMMANDHANDLER_OPTIONS_ERROR`,
args: { options: argument.options.map((o) => `\`${o}\``).join(', ') },
error: true
};
2020-04-11 15:56:52 +02:00
return { error: false, value };
};
2020-04-11 12:00:53 +02:00
const response = await parse(argument, string, guild);
2020-04-11 15:56:52 +02:00
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);
}
2020-04-11 15:56:52 +02:00
return response;
2020-04-11 12:00:53 +02:00
}
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) {
2020-08-10 02:00:59 +02:00
this.client.logger.error(`Command Error | ${message.command.resolveable} | Message ID: ${message.id}\n${resolved.message.stack || resolved.message}`);
2020-08-09 01:21:05 +02:00
if(resolved.message.code === 50013) {
const missing = message.channel.permissionsFor(message.guild.me).missing(['EMBED_LINKS']);
if(missing.length > 0) {
return message.respond(message.format('COMMANDHANDLER_COMMAND_MISSINGPERMISSIONS'), {
emoji: 'failure'
});
}
}
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 };
2020-06-04 19:59:09 +02:00
},
TIME: async(str) => {
const time = await this.client.resolver.resolveTime(str);
if(!time) return { error: true };
return { error: false, value: time };
2020-08-09 21:53:09 +02:00
},
DATE: async(str) => {
const date = await this.client.resolver.resolveDate(str);
if(!date) return { error: true };
return { error: false, value: date };
}
};
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.
2020-07-16 09:54:39 +02:00
// Supposedly quicker than regex, and I'd agree with that statement. Big, messy, but quick. Also lets you know if the grouped word(s) was a quote or not, which is useful for non-spaced quotes e.g. "Test" vs. "Test Test"
_getQuotes(string) {
2020-06-16 00:15:13 +02:00
if(!string) return [];
let quoted = false,
wordStart = true,
startQuote = '',
endQuote = false,
2020-06-04 19:59:09 +02:00
isQuote = false,
word = '';
2020-06-04 19:59:09 +02:00
const words = [],
chars = string.split('');
chars.forEach((char) => {
if((/\s/u).test(char)) {
if(endQuote) {
quoted = false;
endQuote = false;
2020-06-04 19:59:09 +02:00
isQuote = true;
}
if(quoted) {
word += char;
} else if(word !== '') {
2020-06-04 19:59:09 +02:00
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) {
2020-06-04 19:59:09 +02:00
words.push([ word, true ]);
} else {
word.split(/\s/u).forEach((subWord, i) => {
if (i === 0) {
2020-06-04 19:59:09 +02:00
words.push([ startQuote+subWord, false ]);
} else {
2020-06-04 19:59:09 +02:00
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,
date: moment().format('YYYY-MM-DD')
}),
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 });
2020-04-11 12:00:53 +02:00
}
};
2020-04-11 12:00:53 +02:00
2020-06-19 23:01:58 +02:00
//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 });
2020-04-11 12:00:53 +02:00
}
}
module.exports = CommandHandler;