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) { //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 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]; const error = await this._handleTypeParsing(currentArgument, value, guild); 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); 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) 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) { //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: { '"': '"', "'": "'", '‘': '’' } };