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

478 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 { stripIndents } = require('common-tags');
const escapeRegex = require('escape-string-regexp');
const { 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;
await message.author.settings();
if (message.guild) {
await message.guild.settings();
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]) {
const prefix = this.client._options.bot.prefix; //Change this for guild prefix settings.
let command = null;
let remains = [];
if(arg1 && arg1.startsWith(prefix)) {
const commandName = arg1.slice(1);
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;
//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)) 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 });
//console.error(`Argument ${currentArgument.name} is required and was not provided.`);
}
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)) 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 });
//console.error(`Argument ${currentArgument.name} is required and was not provided.`);
}
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);
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);
if(value) {
if(error) {
if(currentArgument.required) {
return this._handleError({ type: 'argument', info: { argument: currentArgument, word, missing: false }, message });
// console.error(`Argument ${currentArgument.name} is required and failed to meet requirements.`);
} 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);
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 });
// console.error(`Argument ${currentArgument.name} is required and failed to meet requirements.`);
} else {
parsedArguments.push(currentArgument);
currentArgument = null;
params.push(word);
continue;
}
} else {
return this._handleError({ type: 'argument', info: { argument: currentArgument, word, missing: false }, message });
// console.error(`Argument ${currentArgument.name} is required and failed to meet requirements.`);
}
} 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);
if(!error) continue;
}
params.push(word);
continue;
}
}
}
}
//fucking kill me
const fff = {};
parsedArguments.map(a=>fff[a.name] = a);
return { parsedArguments: fff, newArgs: params };
}
async _handleTypeParsing(argument, string) {
const parse = async (argument, string) => {
const { error, value } = await this.constructor.parseType(argument.type, string); //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 };
}
}
return { error: false, value };
};
const { error, value } = await parse(argument, string);
if(!error) {
argument.infinite
? argument.value.push(value)
: argument.value = value;
}
return error;
}
async _createFlags(args) {
let shortFlags = {};
let longFlags = {};
let keys = [];
for(const arg of args) {
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) { //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 };
}
};
return await types[type](str);
}
}
module.exports = CommandHandler;
const Constants = {
QuotePairs: {
'"': '"',
"'": "'",
'': ''
}
};