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;