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

520 lines
18 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 CommandHandler2 extends Observer {
constructor(client) {
super(client, {
name: 'commandHandler2',
priority: 5,
guarded: true
});
this.client = client;
this.hooks = [
['message', this.handleMessage.bind(this)]
];
this._startQuotes = Object.keys(Constants.QuotePairs);
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;
if (message.guild) {
if (!message.member) await message.guild.members.fetch(message.author.id);
}
const { command, parameters } = await this._getCommand(message);
if(!command) return undefined;
message.command = command;
const response = await this._parseArguments(parameters, command.arguments, message.guild);
if(response.error) {
return this.handleError(message, { type: 'argument', ...response });
}
message.parameters = response.parameters;
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).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];
arg = longArgs[name];
value = number;
} else if(longArgs[word.toLowerCase()]) {
arg = longArgs[word.toLowerCase()];
}
return { arg, value };
};
const newParameters = [],
newArguments = [];
const lookBehind = async (argument) => {
let response = {};
if(newParameters.length > 0 && ['INTEGER', 'FLOAT'].includes(argument.type)) {
const lastItem = newParameters[newParameters.length-1];
response = await this._parseArgumentType(argument, lastItem, guild);
if(!response.error) {
newParameters.pop();
newArguments.push(argument);
return { error: false };
}
}
return { error: true, ...response };
};
let error = null,
currentArgument = null;
for(let i = 0; i < parameters.length; i++) {
const word = parameters[i];
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;
}
newParameters.push(word);
} else {
newArguments.push(currentArgument);
if(!currentArgument.infinite) currentArgument = null;
}
} else { //Trying to find a new argument to parse or add as a parameter.
const { arg, value } = findArgument(word);
if(arg) {
if(value) {
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);
}
}
}
if(error) return error;
if(currentArgument) {
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) {
const parse = async(argument, string, guild) => {
const { error, value } = await this.parseType(argument.type, string, guild);
if(error) {
return {
index: 'COMMANDHANDLER_TYPE_ERROR',
error
};
}
if(['INTEGER', 'FLOAT'].includes(argument.type)) {
const { min, max } = argument;
if(max !== null && value > max) {
return {
index: `COMMANDHANDLER_NUMBERMAX_ERROR`,
args: { min, max },
force: true,
error: true
};
} else if(min !== null && value < min) {
return {
index: `COMMANDHANDLER_NUMBERMIN_ERROR`,
args: { min, max },
force: true,
error: true
};
}
}
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;
}
if(argument.infinite) argument.value.push(response.value);
else argument.value = response.value;
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);
command = await this._matchCommand(message, commandName);
remains = [arg2, ...args];
} else if(arg1 && arg2 && arg1.startsWith('<@')) {
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 inhibitor = await this._handleInhibitors(message);
if(inhibitor.error) return this.handleError(message, { type: 'inhibitor', ...inhibitor });
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 { error: false };
const promises = [];
for(const inhibitor of inhibitors.values()) {
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);
if(reasons.length === 0) return { error: false };
reasons.sort((a, b) => b.inhibitor.priority - a.inhibitor.priority);
return reasons[0];
}
async parseType(type, str, guild) {
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, guild, true);
if(!member) return { error: true };
return { error: false, value: member };
},
TEXTCHANNEL: async (str, guild) => {
const channel = await this.client.resolver.resolveChannel(str, guild, true, (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, guild, true, (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, guild, true);
if(!channel) return { error: true };
return { error: false, value: channel };
},
ROLE: async (str, guild) => {
const role = await this.client.resolver.resolveRole(str, guild, true);
if(!role) return { error: true };
return { error: false, value: role };
}
};
return types[type](str, guild);
}
_getQuotes(string) {
if(!string) return [];
let quoted = false,
wordStart = true,
startQuote = '',
endQuote = false,
word = '';
const words = [],
chars = string.split('');
chars.forEach((char) => {
if((/\s/u).test(char)) {
if(endQuote) {
quoted = false;
endQuote = false;
}
if(quoted) {
word += char;
} else if(word !== '') {
words.push(word);
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);
} else {
word.split(/\s/u).forEach((subWord, i) => {
if (i === 0) {
words.push(startQuote+subWord);
} else {
words.push(subWord);
}
});
}
return words;
}
async handleError(message, error) {
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 message.respond(await messages[error.type](error), { emoji: 'failure' });
}
}
module.exports = CommandHandler2;