UNPKG

muskytape

Version:

Framework não oficial do Discord.js

572 lines (517 loc) 22.5 kB
const path = require('path'); const { escapeMarkdown } = require('discord.js'); const { oneLine, stripIndents } = require('common-tags'); const ArgumentCollector = require('./collector'); const { permissions } = require('../util'); /** A command that can be run in a client */ class Command { /** * @typedef {Object} ThrottlingOptions * @property {number} usages - Maximum number of usages of the command allowed in the time frame. * @property {number} duration - Amount of time to count the usages of the command within (in seconds). */ /** * @typedef {Object} CommandInfo * @property {string} name - The name of the command (must be lowercase) * @property {string[]} [aliases] - Alternative names for the command (all must be lowercase) * @property {boolean} [autoAliases=true] - Whether automatic aliases should be added * @property {string} group - The ID of the group the command belongs to (must be lowercase) * @property {string} memberName - The member name of the command in the group (must be lowercase) * @property {string} description - A short description of the command * @property {string} [format] - The command usage format string - will be automatically generated if not specified, * and `args` is specified * @property {string} [details] - A detailed description of the command and its functionality * @property {string[]} [examples] - Usage examples of the command * @property {boolean} [guildOnly=false] - Whether or not the command should only function in a guild channel * @property {boolean} [ownerOnly=false] - Whether or not the command is usable only by an owner * @property {PermissionResolvable[]} [clientPermissions] - Permissions required by the client to use the command. * @property {PermissionResolvable[]} [userPermissions] - Permissions required by the user to use the command. * @property {boolean} [nsfw=false] - Whether the command is usable only in NSFW channels. * @property {ThrottlingOptions} [throttling] - Options for throttling usages of the command. * @property {boolean} [defaultHandling=true] - Whether or not the default command handling should be used. * If false, then only patterns will trigger the command. * @property {ArgumentInfo[]} [args] - Arguments for the command. * @property {number} [argsPromptLimit=Infinity] - Maximum number of times to prompt a user for a single argument. * Only applicable if `args` is specified. * @property {string} [argsType=single] - One of 'single' or 'multiple'. Only applicable if `args` is not specified. * When 'single', the entire argument string will be passed to run as one argument. * When 'multiple', it will be passed as multiple arguments. * @property {number} [argsCount=0] - The number of arguments to parse from the command string. * Only applicable when argsType is 'multiple'. If nonzero, it should be at least 2. * When this is 0, the command argument string will be split into as many arguments as it can be. * When nonzero, it will be split into a maximum of this number of arguments. * @property {boolean} [argsSingleQuotes=true] - Whether or not single quotes should be allowed to box-in arguments * in the command string. * @property {RegExp[]} [patterns] - Patterns to use for triggering the command * @property {boolean} [guarded=false] - Whether the command should be protected from disabling * @property {boolean} [hidden=false] - Whether the command should be hidden from the help command * @property {boolean} [unknown=false] - Whether the command should be run when an unknown command is used - there * may only be one command registered with this property as `true`. */ /** * @param {CommandoClient} client - The client the command is for * @param {CommandInfo} info - The command information */ // eslint-disable-next-line complexity constructor(client, info) { this.constructor.validateInfo(client, info); /** * Client that this command is for * @name Command#client * @type {CommandoClient} * @readonly */ Object.defineProperty(this, 'client', { value: client }); /** * Name of this command * @type {string} */ this.name = info.name; /** * Aliases for this command * @type {string[]} */ this.aliases = info.aliases || []; if(typeof info.autoAliases === 'undefined' || info.autoAliases) { if(this.name.includes('-')) this.aliases.push(this.name.replace(/-/g, '')); for(const alias of this.aliases) { if(alias.includes('-')) this.aliases.push(alias.replace(/-/g, '')); } } /** * ID of the group the command belongs to * @type {string} */ this.groupID = info.group; /** * The group the command belongs to, assigned upon registration * @type {?CommandGroup} */ this.group = null; /** * Name of the command within the group * @type {string} */ this.memberName = info.memberName; /** * Short description of the command * @type {string} */ this.description = info.description; /** * Usage format string of the command * @type {string} */ this.format = info.format || null; /** * Long description of the command * @type {?string} */ this.details = info.details || null; /** * Example usage strings * @type {?string[]} */ this.examples = info.examples || null; /** * Whether the command can only be run in a guild channel * @type {boolean} */ this.guildOnly = Boolean(info.guildOnly); /** * Whether the command can only be used by an owner * @type {boolean} */ this.ownerOnly = Boolean(info.ownerOnly); /** * Permissions required by the client to use the command. * @type {?PermissionResolvable[]} */ this.clientPermissions = info.clientPermissions || null; /** * Permissions required by the user to use the command. * @type {?PermissionResolvable[]} */ this.userPermissions = info.userPermissions || null; /** * Whether the command can only be used in NSFW channels * @type {boolean} */ this.nsfw = Boolean(info.nsfw); /** * Whether the default command handling is enabled for the command * @type {boolean} */ this.defaultHandling = 'defaultHandling' in info ? info.defaultHandling : true; /** * Options for throttling command usages * @type {?ThrottlingOptions} */ this.throttling = info.throttling || null; /** * The argument collector for the command * @type {?ArgumentCollector} */ this.argsCollector = info.args && info.args.length ? new ArgumentCollector(client, info.args, info.argsPromptLimit) : null; if(this.argsCollector && typeof info.format === 'undefined') { this.format = this.argsCollector.args.reduce((prev, arg) => { const wrapL = arg.default !== null ? '[' : '<'; const wrapR = arg.default !== null ? ']' : '>'; return `${prev}${prev ? ' ' : ''}${wrapL}${arg.label}${arg.infinite ? '...' : ''}${wrapR}`; }, ''); } /** * How the arguments are split when passed to the command's run method * @type {string} */ this.argsType = info.argsType || 'single'; /** * Maximum number of arguments that will be split * @type {number} */ this.argsCount = info.argsCount || 0; /** * Whether single quotes are allowed to encapsulate an argument * @type {boolean} */ this.argsSingleQuotes = 'argsSingleQuotes' in info ? info.argsSingleQuotes : true; /** * Regular expression triggers * @type {RegExp[]} */ this.patterns = info.patterns || null; /** * Whether the command is protected from being disabled * @type {boolean} */ this.guarded = Boolean(info.guarded); /** * Whether the command should be hidden from the help command * @type {boolean} */ this.hidden = Boolean(info.hidden); /** * Whether the command will be run when an unknown command is used * @type {boolean} */ this.unknown = Boolean(info.unknown); /** * Whether the command is enabled globally * @type {boolean} * @private */ this._globalEnabled = true; /** * Current throttle objects for the command, mapped by user ID * @type {Map<string, Object>} * @private */ this._throttles = new Map(); } /** * Checks whether the user has permission to use the command * @param {CommandoMessage} message - The triggering command message * @param {boolean} [ownerOverride=true] - Whether the bot owner(s) will always have permission * @return {boolean|string} Whether the user has permission, or an error message to respond with if they don't */ hasPermission(message, ownerOverride = true) { if(!this.ownerOnly && !this.userPermissions) return true; if(ownerOverride && this.client.isOwner(message.author)) return true; if(this.ownerOnly && (ownerOverride || !this.client.isOwner(message.author))) { return `O comando só pode ser usado pelo proprietário do bot.`; } if(message.channel.type === 'text' && this.userPermissions) { const missing = message.channel.permissionsFor(message.author).missing(this.userPermissions); if(missing.length > 0) { if(missing.length === 1) { return `The \`${this.name}\` comando requer que você tenha a permissão "${permissions[missing[0]]}".`; } return oneLine` O comando requer que você tenha as seguintes permissões: ${missing.map(perm => permissions[perm]).join(', ')} `; } } return true; } /** * Runs the command * @param {CommandoMessage} message - The message the command is being run for * @param {Object|string|string[]} args - The arguments for the command, or the matches from a pattern. * If args is specified on the command, thise will be the argument values object. If argsType is single, then only * one string will be passed. If multiple, an array of strings will be passed. When fromPattern is true, this is the * matches array from the pattern match * (see [RegExp#exec](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec)). * @param {boolean} fromPattern - Whether or not the command is being run from a pattern match * @param {?ArgumentCollectorResult} result - Result from obtaining the arguments from the collector (if applicable) * @return {Promise<?Message|?Array<Message>>} * @abstract */ async run(message, args, fromPattern, result) { // eslint-disable-line no-unused-vars, require-await throw new Error(`${this.constructor.name} doesn't have a run() method.`); } /** * Called when the command is prevented from running * @param {CommandMessage} message - Command message that the command is running from * @param {string} reason - Reason that the command was blocked * (built-in reasons are `guildOnly`, `nsfw`, `permission`, `throttling`, and `clientPermissions`) * @param {Object} [data] - Additional data associated with the block. Built-in reason data properties: * - guildOnly: none * - nsfw: none * - permission: `response` ({@link string}) to send * - throttling: `throttle` ({@link Object}), `remaining` ({@link number}) time in seconds * - clientPermissions: `missing` ({@link Array}<{@link string}>) permission names * @returns {Promise<?Message|?Array<Message>>} */ onBlock(message, reason, data) { switch(reason) { case 'guildOnly': return message.reply(`O comando deve ser usado em um canal de servidor.`); case 'nsfw': return message.reply(`O comando só pode ser usado em canais NSFW.`); case 'permission': { if(data.response) return message.reply(data.response); return message.reply(`Você não tem permissão para usar o comando.`); } case 'clientPermissions': { if(data.missing.length === 1) { return message.reply( `Eu preciso da permissão "${permissions[data.missing[0]]}" para o comando funcionar.` ); } return message.reply(oneLine` Preciso das seguintes permissões para o comando funcionar.: ${data.missing.map(perm => permissions[perm]).join(', ')} `); } case 'throttling': { return message.reply( `Você não pode usar o comando novamente por mais ${data.remaining.toFixed(1)} segundos.` ); } default: return null; } } /** * Called when the command produces an error while running * @param {Error} err - Error that was thrown * @param {CommandMessage} message - Command message that the command is running from (see {@link Command#run}) * @param {Object|string|string[]} args - Arguments for the command (see {@link Command#run}) * @param {boolean} fromPattern - Whether the args are pattern matches (see {@link Command#run}) * @param {?ArgumentCollectorResult} result - Result from obtaining the arguments from the collector * (if applicable - see {@link Command#run}) * @returns {Promise<?Message|?Array<Message>>} */ onError(err, message, args, fromPattern, result) { // eslint-disable-line no-unused-vars const owners = this.client.owners; const ownerList = owners ? owners.map((usr, i) => { const or = i === owners.length - 1 && owners.length > 1 ? 'or ' : ''; return `${or}${escapeMarkdown(usr.username)}#${usr.discriminator}`; }).join(owners.length > 2 ? ', ' : ' ') : ''; const invite = this.client.options.invite; return message.reply(stripIndents` Ocorreu um erro ao executar o comando: \`${err.name}: ${err.message}\` Você nunca deve receber um erro como este. Por favor entre em contato ${ownerList || 'o dono do bot'}${invite ? ` neste servidor: ${invite}` : '.'} `); } /** * Creates/obtains the throttle object for a user, if necessary (owners are excluded) * @param {string} userID - ID of the user to throttle for * @return {?Object} * @private */ throttle(userID) { if(!this.throttling || this.client.isOwner(userID)) return null; let throttle = this._throttles.get(userID); if(!throttle) { throttle = { start: Date.now(), usages: 0, timeout: this.client.setTimeout(() => { this._throttles.delete(userID); }, this.throttling.duration * 1000) }; this._throttles.set(userID, throttle); } return throttle; } /** * Enables or disables the command in a guild * @param {?GuildResolvable} guild - Guild to enable/disable the command in * @param {boolean} enabled - Whether the command should be enabled or disabled */ setEnabledIn(guild, enabled) { if(typeof guild === 'undefined') throw new TypeError('Guilda não deve ser indefinida.'); if(typeof enabled === 'undefined') throw new TypeError('Ativado não deve ser indefinido.'); if(this.guarded) throw new Error('O comando está salvo.'); if(!guild) { this._globalEnabled = enabled; this.client.emit('commandStatusChange', null, this, enabled); return; } guild = this.client.guilds.resolve(guild); guild.setCommandEnabled(this, enabled); } /** * Checks if the command is enabled in a guild * @param {?GuildResolvable} guild - Guild to check in * @param {boolean} [bypassGroup] - Whether to bypass checking the group's status * @return {boolean} */ isEnabledIn(guild, bypassGroup) { if(this.guarded) return true; if(!guild) return this.group._globalEnabled && this._globalEnabled; guild = this.client.guilds.resolve(guild); return (bypassGroup || guild.isGroupEnabled(this.group)) && guild.isCommandEnabled(this); } /** * Checks if the command is usable for a message * @param {?Message} message - The message * @return {boolean} */ isUsable(message = null) { if(!message) return this._globalEnabled; if(this.guildOnly && message && !message.guild) return false; const hasPermission = this.hasPermission(message); return this.isEnabledIn(message.guild) && hasPermission && typeof hasPermission !== 'string'; } /** * Creates a usage string for the command * @param {string} [argString] - A string of arguments for the command * @param {string} [prefix=this.client.commandPrefix] - Prefix to use for the prefixed command format * @param {User} [user=this.client.user] - User to use for the mention command format * @return {string} */ usage(argString, prefix = this.client.commandPrefix, user = this.client.user) { return this.constructor.usage(`${this.name}${argString ? ` ${argString}` : ''}`, prefix, user); } /** * Reloads the command */ reload() { let cmdPath, cached, newCmd; try { cmdPath = this.client.registry.resolveCommandPath(this.groupID, this.memberName); cached = require.cache[cmdPath]; delete require.cache[cmdPath]; newCmd = require(cmdPath); } catch(err) { if(cached) require.cache[cmdPath] = cached; try { cmdPath = path.join(__dirname, this.groupID, `${this.memberName}.js`); cached = require.cache[cmdPath]; delete require.cache[cmdPath]; newCmd = require(cmdPath); } catch(err2) { if(cached) require.cache[cmdPath] = cached; if(err2.message.includes('Não é possível encontrar módulo')) throw err; else throw err2; } } this.client.registry.reregisterCommand(newCmd, this); } /** * Unloads the command */ unload() { const cmdPath = this.client.registry.resolveCommandPath(this.groupID, this.memberName); if(!require.cache[cmdPath]) throw new Error('O comando não pode ser descarregado.'); delete require.cache[cmdPath]; this.client.registry.unregisterCommand(this); } /** * Creates a usage string for a command * @param {string} command - A command + arg string * @param {string} [prefix] - Prefix to use for the prefixed command format * @param {User} [user] - User to use for the mention command format * @return {string} */ static usage(command, prefix = null, user = null) { const nbcmd = command.replace(/ /g, '\xa0'); if(!prefix && !user) return `\`\`${nbcmd}\`\``; let prefixPart; if(prefix) { if(prefix.length > 1 && !prefix.endsWith(' ')) prefix += ' '; prefix = prefix.replace(/ /g, '\xa0'); prefixPart = `\`\`${prefix}${nbcmd}\`\``; } let mentionPart; if(user) mentionPart = `\`\`@${user.username.replace(/ /g, '\xa0')}#${user.discriminator}\xa0${nbcmd}\`\``; return `${prefixPart || ''}${prefix && user ? ' or ' : ''}${mentionPart || ''}`; } /** * Validates the constructor parameters * @param {CommandoClient} client - Client to validate * @param {CommandInfo} info - Info to validate * @private */ static validateInfo(client, info) { // eslint-disable-line complexity if(!client) throw new Error('Um cliente deve ser especificado.'); if(typeof info !== 'object') throw new TypeError('As informações do comando devem ser um objeto.'); if(typeof info.name !== 'string') throw new TypeError('O nome do comando deve ser uma string.'); if(info.name !== info.name.toLowerCase()) throw new Error('O nome do comando deve ser minúsculo.'); if(info.aliases && (!Array.isArray(info.aliases) || info.aliases.some(ali => typeof ali !== 'string'))) { throw new TypeError('Os aliases de comando devem ser uma matriz de strings.'); } if(info.aliases && info.aliases.some(ali => ali !== ali.toLowerCase())) { throw new RangeError('Os aliases de comando devem ser minúsculos.'); } if(typeof info.group !== 'string') throw new TypeError('O grupo de comando deve ser uma string.'); if(info.group !== info.group.toLowerCase()) throw new RangeError('O grupo de comandos deve ser minúsculo.'); if(typeof info.memberName !== 'string') throw new TypeError('O comando memberName deve ser uma string.'); if(info.memberName !== info.memberName.toLowerCase()) throw new Error('O comando memberName deve estar em minúsculas.'); if(typeof info.description !== 'string') throw new TypeError('A descrição do comando deve ser uma string.'); if('format' in info && typeof info.format !== 'string') throw new TypeError('O formato do comando deve ser uma string.'); if('details' in info && typeof info.details !== 'string') throw new TypeError('Os detalhes do comando devem ser uma string.'); if(info.examples && (!Array.isArray(info.examples) || info.examples.some(ex => typeof ex !== 'string'))) { throw new TypeError('Os exemplos de comando devem ser uma matriz de strings.'); } if(info.clientPermissions) { if(!Array.isArray(info.clientPermissions)) { throw new TypeError('O comando clientPermissions deve ser um array de strings de chave de permissão.'); } for(const perm of info.clientPermissions) { if(!permissions[perm]) throw new RangeError(`ClientPermission de comando inválido: ${perm}`); } } if(info.userPermissions) { if(!Array.isArray(info.userPermissions)) { throw new TypeError('O comando userPermissions deve ser um array de strings de chave de permissão.'); } for(const perm of info.userPermissions) { if(!permissions[perm]) throw new RangeError(`UserPermission de comando inválido: ${perm}`); } } if(info.throttling) { if(typeof info.throttling !== 'object') throw new TypeError('O controle de fluxo de comando deve ser um objeto.'); if(typeof info.throttling.usages !== 'number' || isNaN(info.throttling.usages)) { throw new TypeError('Os usos de controle de fluxo de comando devem ser um número.'); } if(info.throttling.usages < 1) throw new RangeError('Os usos de controle de fluxo de comando devem ser pelo menos 1.'); if(typeof info.throttling.duration !== 'number' || isNaN(info.throttling.duration)) { throw new TypeError('A duração da limitação do comando deve ser um número.'); } if(info.throttling.duration < 1) throw new RangeError('A duração da aceleração do comando deve ser pelo menos 1.'); } if(info.args && !Array.isArray(info.args)) throw new TypeError('Args de comando deve ser um Array.'); if('argsPromptLimit' in info && typeof info.argsPromptLimit !== 'number') { throw new TypeError('O comando argsPromptLimit deve ser um número.'); } if('argsPromptLimit' in info && info.argsPromptLimit < 0) { throw new RangeError('O comando argsPromptLimit deve ser pelo menos 0.'); } if(info.argsType && !['single', 'multiple'].includes(info.argsType)) { throw new RangeError('O comando argsType deve ser "single" ou "multiple".'); } if(info.argsType === 'multiple' && info.argsCount && info.argsCount < 2) { throw new RangeError('O comando argsCount deve ser pelo menos 2.'); } if(info.patterns && (!Array.isArray(info.patterns) || info.patterns.some(pat => !(pat instanceof RegExp)))) { throw new TypeError('Os padrões de comando devem ser uma matriz de expressões regulares.'); } } } module.exports = Command;