forked from Galactic/galactic-bot
550 lines
20 KiB
JavaScript
550 lines
20 KiB
JavaScript
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 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');
|
||
|
||
//console.log('args')
|
||
//console.log(args)
|
||
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);
|
||
//console.log(match)
|
||
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 {
|
||
//console.log(currentArgument?.name)
|
||
if(currentArgument) {
|
||
const error = await this._handleTypeParsing(currentArgument, word, guild);
|
||
//console.log(error)
|
||
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: {
|
||
'"': '"',
|
||
"'": "'",
|
||
'‘': '’'
|
||
}
|
||
}; |