603 lines
23 KiB
JavaScript
603 lines
23 KiB
JavaScript
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.
|
||
|
||
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();
|
||
await message.author.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.toLowerCase(); //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.stack || resolved.message}`);
|
||
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 };
|
||
},
|
||
TIME: async(str) => {
|
||
const time = await this.client.resolver.resolveTime(str);
|
||
if(!time) return { error: true };
|
||
return { error: false, value: time };
|
||
},
|
||
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.
|
||
// 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) {
|
||
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,
|
||
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 });
|
||
}
|
||
};
|
||
|
||
//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; |