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

512 lines
19 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 || this.client._options.bot.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 { parsedArguments, newArgs } = await this._parseArguments(message, args);
message.parameters = newArgs;
message.args = 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(message, args = []) {
args = this._getWords(args.join(' ')).map(w=>w[0]);
const command = message.command;
const { shortFlags, longFlags, keys } = await this._createFlags(command.arguments);
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 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 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, message.guild);
if(beforeError) {
continue;
} else {
params.pop();
currentArgument.value = lastItem;
parsedArguments.push(currentArgument);
currentArgument = null;
continue;
}
}
const value = match[1] || match[3];
const error = await this._handleTypeParsing(currentArgument, value, message.guild);
if(value) {
if(error) {
if(currentArgument.required) {
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, message.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 this._handleError({ type: 'argument', info: { argument: currentArgument, word, missing: false }, message });
} else {
parsedArguments.push(currentArgument);
currentArgument = null;
params.push(word);
continue;
}
} else {
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, message.guild);
if(!error) continue;
}
params.push(word);
continue;
}
}
}
}
if(currentArgument) parsedArguments.push(currentArgument);
const blah = parsedArguments.filter(a=>a.requiredArgument && !a.value);
const missingArgument = blah[0];
if(missingArgument) return this._handleError({ type: 'argument', info: { argument: missingArgument, missing: true }, message });
//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) 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 !== undefined) {
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) { //this is in the class for a reason, will soon reference to a user resolver etc.
//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(Number.isNaN(int)) return { error: true };
return { error: false, value: int };
},
FLOAT: (str) => {
const float = parseInt(str);
if(Number.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: {
'"': '"',
"'": "'",
'': ''
}
};