galactic-bot/structure/client/components/observers/CommandHandler.js
2020-05-23 23:50:35 -04:00

545 lines
20 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 { stripIndents } = require('common-tags');
const escapeRegex = require('escape-string-regexp');
const { Argument, 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)]
];
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 content = message.content;
const args = content.split(' ');
const { command, newArgs } = await this._getCommand(message, args);
if(!command) return undefined;
message.command = command;
return await this.handleCommand(message, newArgs);
}
async _getCommand(message, [arg1, arg2, ...args]) {
if(message.guild) await message.guild.settings();
const prefix = message.guild?.prefix;
let command = null;
let 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}>)`, '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;
message._caller = commandName; //Used for hidden commands as aliases.
//Eventually search for custom commands here.
return command;
}
/* Command Handling */
async handleCommand(message, args) {
const inhibitor = await this._handleInhibitors(message);
if(inhibitor.error) return this._handleError({ type: 'inhibitor', info: inhibitor , message });
const response = await this._parseArguments(args, message.command.arguments, message.guild);
if(response.error) {
return this._handleError({
...response,
message
});
} else {
message.parameters = message.command.parameterType === 'PLAIN' ? response.newArgs.join(' ') : response.newArgs;
message.args = response.parsedArguments;
}
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({ type: 'command', message });
}
}
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 _handleError({ type, message, info }) {
const errorMessages = {
command: (message) => {
return stripIndents`The command **${message.command.moduleResolveable}** had issues running. **\`[${message.id}]\`**
Contact **the bot owner(s)** about this issue. You can also find support here: <${this.client._options.bot.invite}>`;
},
inhibitor: (message, info) => {
return `${info.message} **\`[${info.inhibitor.resolveable}]\`**`;
},
argument: (message, { argument, missing }) => {
return stripIndents`The argument **${argument.name}:${argument.type.toLowerCase()}** is required and ${missing ? "was not provided." : "did not meet the requirements."} Expecting a \`${argument.type.toLowerCase()}\` value.`;
}
};
await message.respond(errorMessages[type](message, info), { emoji: 'failure' });
}
async _parseArguments(args = [], passedArguments = [], guild = null, debug = false) { //Only need guild parameter if using a resolver type in your arguments e.g. channel, user, member, role
args = this._getWords(args.join(' ')).map(w=>w[0]);
const { shortFlags, longFlags, keys } = await this._createFlags(passedArguments);
const regex = new RegExp(`([0-9]*)(${Object.keys(longFlags).map(k=>escapeRegex(k)).join('|')})([0-9]*)`, 'i');
let parsedArguments = [];
let params = [];
let currentArgument = null;
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)) {
params.push(word);
continue;
}
currentArgument = shortFlags[name];
if(currentArgument.type === 'BOOLEAN') {
currentArgument.value = currentArgument.default;
parsedArguments.push(currentArgument);
currentArgument = null;
continue;
}
if(currentArgument.required && !args[i+1]) {
return {
error: true,
type: 'argument',
info: { argument: currentArgument, word, missing: true }
}
// return this._handleError({ type: 'argument', info: { argument: currentArgument, word, missing: true }, message });
}
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)) {
params.push(word);
continue;
}
currentArgument = longFlags[name];
if(currentArgument.type === 'BOOLEAN') {
currentArgument.value = currentArgument.default;
parsedArguments.push(currentArgument);
currentArgument = null;
continue;
}
if(currentArgument.required && !args[i+1]) {
return {
error: true,
type: 'argument',
info: { argument: currentArgument, word, missing: true }
}
// return this._handleError({ type: 'argument', info: { argument: currentArgument, word, missing: true }, message });
}
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
const lastItem = params[params.length-1];
const beforeError = await this._handleTypeParsing(currentArgument, lastItem, guild);
if(beforeError) {
continue;
} else {
params.pop();
currentArgument.value = lastItem;
parsedArguments.push(currentArgument);
currentArgument = null;
continue;
}
}
const value = match[1] || match[3] || null;
const error = await this._handleTypeParsing(currentArgument, value, guild); //CULPRIT
if(value) {
if(error) {
if(currentArgument.required) {
return {
error: true,
type: 'argument',
info: { argument: currentArgument, word, missing: false }
}
// return this._handleError({ type: 'argument', info: { argument: currentArgument, word, missing: false }, message });
} 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, guild);
if(error) {
if(currentArgument.default !== null) {
params.push(word);
currentArgument.value = currentArgument.default;
parsedArguments.push(currentArgument);
currentArgument = null;
continue;
}
if(currentArgument.required) {
if(currentArgument.infinite) {
if(currentArgument.value.length === 0) {
return {
error: true,
type: 'argument',
info: { argument: currentArgument, word, missing: false }
}
// return this._handleError({ type: 'argument', info: { argument: currentArgument, word, missing: false }, message });
} else {
parsedArguments.push(currentArgument);
currentArgument = null;
params.push(word);
continue;
}
} else {
return {
error: true,
type: 'argument',
info: { argument: currentArgument, word, missing: false }
}
// return this._handleError({ type: 'argument', info: { argument: currentArgument, word, missing: false }, message });
}
} else {
currentArgument = null;
params.push(word);
continue;
}
} else {
if(currentArgument.infinite) continue;
parsedArguments.push(currentArgument);
currentArgument = null;
continue;
}
} else {
const lastArgument = parsedArguments[parsedArguments.length-1];
if(lastArgument && lastArgument.type === 'BOOLEAN' && lastArgument.value === lastArgument.default) {
const error = await this._handleTypeParsing(lastArgument, word, guild);
if(!error) continue;
}
params.push(word);
continue;
}
}
}
}
if(currentArgument) parsedArguments.push(currentArgument);
const blah = parsedArguments.filter(a=>a.requiredArgument && !a.value); //check if array, too lazy.
const missingArgument = blah[0];
if(missingArgument) return {
error: true,
type: 'argument',
info: { argument: missingArgument, missing: true }
};
//fucking kill me
const fff = {};
parsedArguments.map(a=>fff[a.name] = a);
return { parsedArguments: fff, newArgs: params };
}
async _handleTypeParsing(argument, string, guild) {
const parse = async (argument, string, guild) => {
const { error, value } = await this.constructor.parseType(argument.type, string, this.client.resolver, guild); //Cannot access static functions through "this".
if(error || value === null) 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 };
}
}
if(argument.options.length > 0) {
let found = null;
for(const option of argument.options) {
if(option !== value) {
continue;
} else {
found = option;
}
}
if(!found) return { error: true };
else return { error: false, value: found };
}
return { error: false, value };
};
const { error, value } = await parse(argument, string, guild);
if(error || value === '') return true;
argument.infinite
? argument.value.push(value)
: argument.value = value;
return error;
}
async _createFlags(args) {
let shortFlags = {};
let longFlags = {};
let keys = [];
for(let arg of args) {
arg = new Argument(this.client, arg)
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 };
}
_getWords(string = '') {
let quoted = false,
wordStart = true,
startQuote = '',
endQuote = false,
isQuote = false,
word = '',
words = [],
chars = string.split('');
chars.forEach((char) => {
if(/\s/.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/).forEach((subWord, i) => {
if (i === 0) {
words.push([ startQuote+subWord, false ]);
} else {
words.push([ subWord, false ]);
}
});
}
return words;
}
static async parseType(type, str, resolver, guild) {
//INTEGER AND FLOAT ARE SAME FUNCTION
const types = {
STRING: (str) => {
return { 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 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 };
},
USER: (str) => { //eslint-disable-line no-unused-vars
},
MEMBER: (str) => { //eslint-disable-line no-unused-vars
},
CHANNEL: async (str, resolver, guild) => { //eslint-disable-line no-unused-vars
const channels = await resolver.resolveChannels(str, guild, true);
if(channels.length === 0) return { error: true };
else return { error: false, value: channels[0] }
}
};
return await types[type](str, resolver, guild);
}
}
module.exports = CommandHandler;
const Constants = {
QuotePairs: {
'"': '"',
"'": "'",
'': ''
}
};