UNPKG

discord-akairo

Version:

A highly customizable bot framework for Discord.js.

1,195 lines (1,039 loc) 40.9 kB
const AkairoError = require('../../util/AkairoError'); const AkairoHandler = require('../AkairoHandler'); const { BuiltInReasons, CommandHandlerEvents } = require('../../util/Constants'); const { Collection } = require('discord.js'); const Command = require('./Command'); const CommandUtil = require('./CommandUtil'); const Flag = require('./Flag'); const { deepAssign, flatMap, intoArray, intoCallable, isPromise, prefixCompare } = require('../../util/Util'); const TypeResolver = require('./arguments/TypeResolver'); /** * Loads commands and handles messages. * @param {AkairoClient} client - The Akairo client. * @param {CommandHandlerOptions} options - Options. * @extends {AkairoHandler} */ class CommandHandler extends AkairoHandler { constructor(client, { directory, classToHandle = Command, extensions = ['.js', '.ts'], automateCategories, loadFilter, blockClient = true, blockBots = true, fetchMembers = false, handleEdits = false, storeMessages = false, commandUtil, commandUtilLifetime = 3e5, commandUtilSweepInterval = 3e5, defaultCooldown = 0, ignoreCooldown = client.ownerID, ignorePermissions = [], argumentDefaults = {}, prefix = '!', allowMention = true, aliasReplacement } = {}) { if (!(classToHandle.prototype instanceof Command || classToHandle === Command)) { throw new AkairoError('INVALID_CLASS_TO_HANDLE', classToHandle.name, Command.name); } super(client, { directory, classToHandle, extensions, automateCategories, loadFilter }); /** * The type resolver. * @type {TypeResolver} */ this.resolver = new TypeResolver(this); /** * Collecion of command aliases. * @type {Collection<string, string>} */ this.aliases = new Collection(); /** * Regular expression to automatically make command aliases for. * @type {?RegExp} */ this.aliasReplacement = aliasReplacement; /** * Collection of prefix overwrites to commands. * @type {Collection<string|PrefixSupplier, Set<string>>} */ this.prefixes = new Collection(); /** * Whether or not to block self. * @type {boolean} */ this.blockClient = Boolean(blockClient); /** * Whether or not to block bots. * @type {boolean} */ this.blockBots = Boolean(blockBots); /** * Whether or not members are fetched on each message author from a guild. * @type {boolean} */ this.fetchMembers = Boolean(fetchMembers); /** * Whether or not edits are handled. * @type {boolean} */ this.handleEdits = Boolean(handleEdits); /** * Whether or not to store messages in CommandUtil. * @type {boolean} */ this.storeMessages = Boolean(storeMessages); /** * Whether or not `message.util` is assigned. * @type {boolean} */ this.commandUtil = Boolean(commandUtil); if ((this.handleEdits || this.storeMessages) && !this.commandUtil) { throw new AkairoError('COMMAND_UTIL_EXPLICIT'); } /** * Milliseconds a message should exist for before its command util instance is marked for removal. * @type {number} */ this.commandUtilLifetime = commandUtilLifetime; /** * Time interval in milliseconds for sweeping command util instances. * @type {number} */ this.commandUtilSweepInterval = commandUtilSweepInterval; if (this.commandUtilSweepInterval > 0) { this.client.setInterval(() => this.sweepCommandUtil(), this.commandUtilSweepInterval); } /** * Collection of CommandUtils. * @type {Collection<string, CommandUtil>} */ this.commandUtils = new Collection(); /** * Collection of cooldowns. * <info>The elements in the collection are objects with user IDs as keys * and {@link CooldownData} objects as values</info> * @type {Collection<string, Object>} */ this.cooldowns = new Collection(); /** * Default cooldown for commands. * @type {number} */ this.defaultCooldown = defaultCooldown; /** * ID of user(s) to ignore cooldown or a function to ignore. * @type {Snowflake|Snowflake[]|IgnoreCheckPredicate} */ this.ignoreCooldown = typeof ignoreCooldown === 'function' ? ignoreCooldown.bind(this) : ignoreCooldown; /** * ID of user(s) to ignore `userPermissions` checks or a function to ignore. * @type {Snowflake|Snowflake[]|IgnoreCheckPredicate} */ this.ignorePermissions = typeof ignorePermissions === 'function' ? ignorePermissions.bind(this) : ignorePermissions; /** * Collection of sets of ongoing argument prompts. * @type {Collection<string, Set<string>>} */ this.prompts = new Collection(); /** * Default argument options. * @type {DefaultArgumentOptions} */ this.argumentDefaults = deepAssign({ prompt: { start: '', retry: '', timeout: '', ended: '', cancel: '', retries: 1, time: 30000, cancelWord: 'cancel', stopWord: 'stop', optional: false, infinite: false, limit: Infinity, breakout: true } }, argumentDefaults); /** * The prefix(es) for command parsing. * @type {string|string[]|PrefixSupplier} */ this.prefix = typeof prefix === 'function' ? prefix.bind(this) : prefix; /** * Whether or not mentions are allowed for prefixing. * @type {boolean|MentionPrefixPredicate} */ this.allowMention = typeof allowMention === 'function' ? allowMention.bind(this) : Boolean(allowMention); /** * Inhibitor handler to use. * @type {?InhibitorHandler} */ this.inhibitorHandler = null; /** * Directory to commands. * @name CommandHandler#directory * @type {string} */ /** * Commands loaded, mapped by ID to Command. * @name CommandHandler#modules * @type {Collection<string, Command>} */ this.setup(); } setup() { this.client.once('ready', () => { this.client.on('message', async m => { if (m.partial) await m.fetch(); this.handle(m); }); if (this.handleEdits) { this.client.on('messageUpdate', async (o, m) => { if (o.partial) await o.fetch(); if (m.partial) await m.fetch(); if (o.content === m.content) return; if (this.handleEdits) this.handle(m); }); } }); } /** * Registers a module. * @param {Command} command - Module to use. * @param {string} [filepath] - Filepath of module. * @returns {void} */ register(command, filepath) { super.register(command, filepath); for (let alias of command.aliases) { const conflict = this.aliases.get(alias.toLowerCase()); if (conflict) throw new AkairoError('ALIAS_CONFLICT', alias, command.id, conflict); alias = alias.toLowerCase(); this.aliases.set(alias, command.id); if (this.aliasReplacement) { const replacement = alias.replace(this.aliasReplacement, ''); if (replacement !== alias) { const replacementConflict = this.aliases.get(replacement); if (replacementConflict) throw new AkairoError('ALIAS_CONFLICT', replacement, command.id, replacementConflict); this.aliases.set(replacement, command.id); } } } if (command.prefix != null) { let newEntry = false; if (Array.isArray(command.prefix)) { for (const prefix of command.prefix) { const prefixes = this.prefixes.get(prefix); if (prefixes) { prefixes.add(command.id); } else { this.prefixes.set(prefix, new Set([command.id])); newEntry = true; } } } else { const prefixes = this.prefixes.get(command.prefix); if (prefixes) { prefixes.add(command.id); } else { this.prefixes.set(command.prefix, new Set([command.id])); newEntry = true; } } if (newEntry) { this.prefixes = this.prefixes.sort((aVal, bVal, aKey, bKey) => prefixCompare(aKey, bKey)); } } } /** * Deregisters a module. * @param {Command} command - Module to use. * @returns {void} */ deregister(command) { for (let alias of command.aliases) { alias = alias.toLowerCase(); this.aliases.delete(alias); if (this.aliasReplacement) { const replacement = alias.replace(this.aliasReplacement, ''); if (replacement !== alias) this.aliases.delete(replacement); } } if (command.prefix != null) { if (Array.isArray(command.prefix)) { for (const prefix of command.prefix) { const prefixes = this.prefixes.get(prefix); if (prefixes.size === 1) { this.prefixes.delete(prefix); } else { prefixes.delete(prefix); } } } else { const prefixes = this.prefixes.get(command.prefix); if (prefixes.size === 1) { this.prefixes.delete(command.prefix); } else { prefixes.delete(command.prefix); } } } super.deregister(command); } /** * Handles a message. * @param {Message} message - Message to handle. * @returns {Promise<?boolean>} */ async handle(message) { try { if (this.fetchMembers && message.guild && !message.member && !message.webhookID) { await message.guild.members.fetch(message.author); } if (await this.runAllTypeInhibitors(message)) { return false; } if (this.commandUtil) { if (this.commandUtils.has(message.id)) { message.util = this.commandUtils.get(message.id); } else { message.util = new CommandUtil(this, message); this.commandUtils.set(message.id, message.util); } } if (await this.runPreTypeInhibitors(message)) { return false; } let parsed = await this.parseCommand(message); if (!parsed.command) { const overParsed = await this.parseCommandOverwrittenPrefixes(message); if (overParsed.command || (parsed.prefix == null && overParsed.prefix != null)) { parsed = overParsed; } } if (this.commandUtil) { message.util.parsed = parsed; } let ran; if (!parsed.command) { ran = await this.handleRegexAndConditionalCommands(message); } else { ran = await this.handleDirectCommand(message, parsed.content, parsed.command); } if (ran === false) { this.emit(CommandHandlerEvents.MESSAGE_INVALID, message); return false; } return ran; } catch (err) { this.emitError(err, message); return null; } } /** * Handles normal commands. * @param {Message} message - Message to handle. * @param {string} content - Content of message without command. * @param {Command} command - Command instance. * @param {boolean} [ignore=false] - Ignore inhibitors and other checks. * @returns {Promise<?boolean>} */ async handleDirectCommand(message, content, command, ignore = false) { let key; try { if (!ignore) { if (message.edited && !command.editable) return false; if (await this.runPostTypeInhibitors(message, command)) return false; } const before = command.before(message); if (isPromise(before)) await before; const args = await command.parse(message, content); if (Flag.is(args, 'cancel')) { this.emit(CommandHandlerEvents.COMMAND_CANCELLED, message, command); return true; } else if (Flag.is(args, 'retry')) { this.emit(CommandHandlerEvents.COMMAND_BREAKOUT, message, command, args.message); return this.handle(args.message); } else if (Flag.is(args, 'continue')) { const continueCommand = this.modules.get(args.command); return this.handleDirectCommand(message, args.rest, continueCommand, args.ignore); } if (!ignore) { if (command.lock) key = command.lock(message, args); if (isPromise(key)) key = await key; if (key) { if (command.locker.has(key)) { key = null; this.emit(CommandHandlerEvents.COMMAND_LOCKED, message, command); return true; } command.locker.add(key); } } return await this.runCommand(message, command, args); } catch (err) { this.emitError(err, message, command); return null; } finally { if (key) command.locker.delete(key); } } /** * Handles regex and conditional commands. * @param {Message} message - Message to handle. * @returns {Promise<boolean>} */ async handleRegexAndConditionalCommands(message) { const ran1 = await this.handleRegexCommands(message); const ran2 = await this.handleConditionalCommands(message); return ran1 || ran2; } /** * Handles regex commands. * @param {Message} message - Message to handle. * @returns {Promise<boolean>} */ async handleRegexCommands(message) { const hasRegexCommands = []; for (const command of this.modules.values()) { if (message.edited ? command.editable : true) { const regex = typeof command.regex === 'function' ? command.regex(message) : command.regex; if (regex) hasRegexCommands.push({ command, regex }); } } const matchedCommands = []; for (const entry of hasRegexCommands) { const match = message.content.match(entry.regex); if (!match) continue; const matches = []; if (entry.regex.global) { let matched; while ((matched = entry.regex.exec(message.content)) != null) { matches.push(matched); } } matchedCommands.push({ command: entry.command, match, matches }); } if (!matchedCommands.length) { return false; } const promises = []; for (const { command, match, matches } of matchedCommands) { promises.push((async () => { try { if (await this.runPostTypeInhibitors(message, command)) return; const before = command.before(message); if (isPromise(before)) await before; await this.runCommand(message, command, { match, matches }); } catch (err) { this.emitError(err, message, command); } })()); } await Promise.all(promises); return true; } /** * Handles conditional commands. * @param {Message} message - Message to handle. * @returns {Promise<boolean>} */ async handleConditionalCommands(message) { const trueCommands = []; const filterPromises = []; for (const command of this.modules.values()) { if (message.edited && !command.editable) continue; filterPromises.push((async () => { let cond = command.condition(message); if (isPromise(cond)) cond = await cond; if (cond) trueCommands.push(command); })()); } await Promise.all(filterPromises); if (!trueCommands.length) { return false; } const promises = []; for (const command of trueCommands) { promises.push((async () => { try { if (await this.runPostTypeInhibitors(message, command)) return; const before = command.before(message); if (isPromise(before)) await before; await this.runCommand(message, command, {}); } catch (err) { this.emitError(err, message, command); } })()); } await Promise.all(promises); return true; } /** * Runs inhibitors with the all type. * @param {Message} message - Message to handle. * @returns {Promise<boolean>} */ async runAllTypeInhibitors(message) { const reason = this.inhibitorHandler ? await this.inhibitorHandler.test('all', message) : null; if (reason != null) { this.emit(CommandHandlerEvents.MESSAGE_BLOCKED, message, reason); } else if (this.blockClient && message.author.id === this.client.user.id) { this.emit(CommandHandlerEvents.MESSAGE_BLOCKED, message, BuiltInReasons.CLIENT); } else if (this.blockBots && message.author.bot) { this.emit(CommandHandlerEvents.MESSAGE_BLOCKED, message, BuiltInReasons.BOT); } else if (this.hasPrompt(message.channel, message.author)) { this.emit(CommandHandlerEvents.IN_PROMPT, message); } else { return false; } return true; } /** * Runs inhibitors with the pre type. * @param {Message} message - Message to handle. * @returns {Promise<boolean>} */ async runPreTypeInhibitors(message) { const reason = this.inhibitorHandler ? await this.inhibitorHandler.test('pre', message) : null; if (reason != null) { this.emit(CommandHandlerEvents.MESSAGE_BLOCKED, message, reason); } else { return false; } return true; } /** * Runs inhibitors with the post type. * @param {Message} message - Message to handle. * @param {Command} command - Command to handle. * @returns {Promise<boolean>} */ async runPostTypeInhibitors(message, command) { if (command.ownerOnly) { const isOwner = this.client.isOwner(message.author); if (!isOwner) { this.emit(CommandHandlerEvents.COMMAND_BLOCKED, message, command, BuiltInReasons.OWNER); return true; } } if (command.channel === 'guild' && !message.guild) { this.emit(CommandHandlerEvents.COMMAND_BLOCKED, message, command, BuiltInReasons.GUILD); return true; } if (command.channel === 'dm' && message.guild) { this.emit(CommandHandlerEvents.COMMAND_BLOCKED, message, command, BuiltInReasons.DM); return true; } if (await this.runPermissionChecks(message, command)) { return true; } const reason = this.inhibitorHandler ? await this.inhibitorHandler.test('post', message, command) : null; if (reason != null) { this.emit(CommandHandlerEvents.COMMAND_BLOCKED, message, command, reason); return true; } if (this.runCooldowns(message, command)) { return true; } return false; } /** * Runs permission checks. * @param {Message} message - Message that called the command. * @param {Command} command - Command to cooldown. * @returns {Promise<boolean>} */ async runPermissionChecks(message, command) { if (command.clientPermissions) { if (typeof command.clientPermissions === 'function') { let missing = command.clientPermissions(message); if (isPromise(missing)) missing = await missing; if (missing != null) { this.emit(CommandHandlerEvents.MISSING_PERMISSIONS, message, command, 'client', missing); return true; } } else if (message.guild) { const missing = message.channel.permissionsFor(this.client.user).missing(command.clientPermissions); if (missing.length) { this.emit(CommandHandlerEvents.MISSING_PERMISSIONS, message, command, 'client', missing); return true; } } } if (command.userPermissions) { const ignorer = command.ignorePermissions || this.ignorePermissions; const isIgnored = Array.isArray(ignorer) ? ignorer.includes(message.author.id) : typeof ignorer === 'function' ? ignorer(message, command) : message.author.id === ignorer; if (!isIgnored) { if (typeof command.userPermissions === 'function') { let missing = command.userPermissions(message); if (isPromise(missing)) missing = await missing; if (missing != null) { this.emit(CommandHandlerEvents.MISSING_PERMISSIONS, message, command, 'user', missing); return true; } } else if (message.guild) { const missing = message.channel.permissionsFor(message.author).missing(command.userPermissions); if (missing.length) { this.emit(CommandHandlerEvents.MISSING_PERMISSIONS, message, command, 'user', missing); return true; } } } } return false; } /** * Runs cooldowns and checks if a user is under cooldown. * @param {Message} message - Message that called the command. * @param {Command} command - Command to cooldown. * @returns {boolean} */ runCooldowns(message, command) { const ignorer = command.ignoreCooldown || this.ignoreCooldown; const isIgnored = Array.isArray(ignorer) ? ignorer.includes(message.author.id) : typeof ignorer === 'function' ? ignorer(message, command) : message.author.id === ignorer; if (isIgnored) return false; const time = command.cooldown != null ? command.cooldown : this.defaultCooldown; if (!time) return false; const endTime = message.createdTimestamp + time; const id = message.author.id; if (!this.cooldowns.has(id)) this.cooldowns.set(id, {}); if (!this.cooldowns.get(id)[command.id]) { this.cooldowns.get(id)[command.id] = { timer: this.client.setTimeout(() => { if (this.cooldowns.get(id)[command.id]) { this.client.clearTimeout(this.cooldowns.get(id)[command.id].timer); } this.cooldowns.get(id)[command.id] = null; if (!Object.keys(this.cooldowns.get(id)).length) { this.cooldowns.delete(id); } }, time), end: endTime, uses: 0 }; } const entry = this.cooldowns.get(id)[command.id]; if (entry.uses >= command.ratelimit) { const end = this.cooldowns.get(message.author.id)[command.id].end; const diff = end - message.createdTimestamp; this.emit(CommandHandlerEvents.COOLDOWN, message, command, diff); return true; } entry.uses++; return false; } /** * Runs a command. * @param {Message} message - Message to handle. * @param {Command} command - Command to handle. * @param {any} args - Arguments to use. * @returns {Promise<void>} */ async runCommand(message, command, args) { if (command.typing) { message.channel.startTyping(); } try { this.emit(CommandHandlerEvents.COMMAND_STARTED, message, command, args); const ret = await command.exec(message, args); this.emit(CommandHandlerEvents.COMMAND_FINISHED, message, command, args, ret); } finally { if (command.typing) { message.channel.stopTyping(); } } } /** * Parses the command and its argument list. * @param {Message} message - Message that called the command. * @returns {Promise<ParsedComponentData>} */ async parseCommand(message) { let prefixes = intoArray(await intoCallable(this.prefix)(message)); const allowMention = await intoCallable(this.prefix)(message); if (allowMention) { const mentions = [`<@${this.client.user.id}>`, `<@!${this.client.user.id}>`]; prefixes = [...mentions, ...prefixes]; } prefixes.sort(prefixCompare); return this.parseMultiplePrefixes(message, prefixes.map(p => [p, null])); } /** * Parses the command and its argument list using prefix overwrites. * @param {Message} message - Message that called the command. * @returns {Promise<ParsedComponentData>} */ async parseCommandOverwrittenPrefixes(message) { if (!this.prefixes.size) { return {}; } const promises = this.prefixes.map(async (cmds, provider) => { const prefixes = intoArray(await intoCallable(provider)(message)); return prefixes.map(p => [p, cmds]); }); const pairs = flatMap(await Promise.all(promises), x => x); pairs.sort(([a], [b]) => prefixCompare(a, b)); return this.parseMultiplePrefixes(message, pairs); } /** * Runs parseWithPrefix on multiple prefixes and returns the best parse. * @param {Message} message - Message to parse. * @param {any[]} pairs - Pairs of prefix to associated commands. * That is, `[string, Set<string> | null][]`. * @returns {ParsedComponentData} */ parseMultiplePrefixes(message, pairs) { const parses = pairs.map(([prefix, cmds]) => this.parseWithPrefix(message, prefix, cmds)); const result = parses.find(parsed => parsed.command); if (result) { return result; } const guess = parses.find(parsed => parsed.prefix != null); if (guess) { return guess; } return {}; } /** * Tries to parse a message with the given prefix and associated commands. * Associated commands refer to when a prefix is used in prefix overrides. * @param {Message} message - Message to parse. * @param {string} prefix - Prefix to use. * @param {Set<string>} [associatedCommands=null] - Associated commands. * @returns {ParsedComponentData} */ parseWithPrefix(message, prefix, associatedCommands = null) { const lowerContent = message.content.toLowerCase(); if (!lowerContent.startsWith(prefix.toLowerCase())) { return {}; } const endOfPrefix = lowerContent.indexOf(prefix.toLowerCase()) + prefix.length; const startOfArgs = message.content.slice(endOfPrefix).search(/\S/) + prefix.length; const alias = message.content.slice(startOfArgs).split(/\s{1,}|\n{1,}/)[0]; const command = this.findCommand(alias); const content = message.content.slice(startOfArgs + alias.length + 1).trim(); const afterPrefix = message.content.slice(prefix.length).trim(); if (!command) { return { prefix, alias, content, afterPrefix }; } if (associatedCommands == null) { if (command.prefix != null) { return { prefix, alias, content, afterPrefix }; } } else if (!associatedCommands.has(command.id)) { return { prefix, alias, content, afterPrefix }; } return { command, prefix, alias, content, afterPrefix }; } /** * Handles errors from the handling. * @param {Error} err - The error. * @param {Message} message - Message that called the command. * @param {Command} [command] - Command that errored. * @returns {void} */ emitError(err, message, command) { if (command && command.typing) message.channel.stopTyping(); if (this.listenerCount(CommandHandlerEvents.ERROR)) { this.emit(CommandHandlerEvents.ERROR, err, message, command); return; } throw err; } /** * Sweep command util instances from cache and returns amount sweeped. * @param {number} lifetime - Messages older than this will have their command util instance sweeped. * This is in milliseconds and defaults to the `commandUtilLifetime` option. * @returns {number} */ sweepCommandUtil(lifetime = this.commandUtilLifetime) { let count = 0; for (const commandUtil of this.commandUtils.values()) { const now = Date.now(); const message = commandUtil.message; if (now - (message.editedTimestamp || message.createdTimestamp) > lifetime) { count++; this.commandUtils.delete(message.id); } } return count; } /** * Adds an ongoing prompt in order to prevent command usage in the channel. * @param {Channel} channel - Channel to add to. * @param {User} user - User to add. * @returns {void} */ addPrompt(channel, user) { let users = this.prompts.get(channel.id); if (!users) this.prompts.set(channel.id, new Set()); users = this.prompts.get(channel.id); users.add(user.id); } /** * Removes an ongoing prompt. * @param {Channel} channel - Channel to remove from. * @param {User} user - User to remove. * @returns {void} */ removePrompt(channel, user) { const users = this.prompts.get(channel.id); if (!users) return; users.delete(user.id); if (!users.size) this.prompts.delete(user.id); } /** * Checks if there is an ongoing prompt. * @param {Channel} channel - Channel to check. * @param {User} user - User to check. * @returns {boolean} */ hasPrompt(channel, user) { const users = this.prompts.get(channel.id); if (!users) return false; return users.has(user.id); } /** * Finds a command by alias. * @param {string} name - Alias to find with. * @returns {Command} */ findCommand(name) { return this.modules.get(this.aliases.get(name.toLowerCase())); } /** * Set the inhibitor handler to use. * @param {InhibitorHandler} inhibitorHandler - The inhibitor handler. * @returns {CommandHandler} */ useInhibitorHandler(inhibitorHandler) { this.inhibitorHandler = inhibitorHandler; this.resolver.inhibitorHandler = inhibitorHandler; return this; } /** * Set the listener handler to use. * @param {ListenerHandler} listenerHandler - The listener handler. * @returns {CommandHandler} */ useListenerHandler(listenerHandler) { this.resolver.listenerHandler = listenerHandler; return this; } /** * Loads a command. * @method * @name CommandHandler#load * @param {string|Command} thing - Module or path to module. * @returns {Command} */ /** * Reads all commands from the directory and loads them. * @method * @name CommandHandler#loadAll * @param {string} [directory] - Directory to load from. * Defaults to the directory passed in the constructor. * @param {LoadPredicate} [filter] - Filter for files, where true means it should be loaded. * @returns {CommandHandler} */ /** * Removes a command. * @method * @name CommandHandler#remove * @param {string} id - ID of the command. * @returns {Command} */ /** * Removes all commands. * @method * @name CommandHandler#removeAll * @returns {CommandHandler} */ /** * Reloads a command. * @method * @name CommandHandler#reload * @param {string} id - ID of the command. * @returns {Command} */ /** * Reloads all commands. * @method * @name CommandHandler#reloadAll * @returns {CommandHandler} */ } module.exports = CommandHandler; /** * Emitted when a message is blocked by a pre-message inhibitor. * The built-in inhibitors are 'client' and 'bot'. * @event CommandHandler#messageBlocked * @param {Message} message - Message sent. * @param {string} reason - Reason for the block. */ /** * Emitted when a message does not start with the prefix or match a command. * @event CommandHandler#messageInvalid * @param {Message} message - Message sent. */ /** * Emitted when a command is found disabled. * @event CommandHandler#commandDisabled * @param {Message} message - Message sent. * @param {Command} command - Command found. */ /** * Emitted when a command is blocked by a post-message inhibitor. * The built-in inhibitors are 'owner', 'guild', and 'dm'. * @event CommandHandler#commandBlocked * @param {Message} message - Message sent. * @param {Command} command - Command blocked. * @param {string} reason - Reason for the block. */ /** * Emitted when a command starts execution. * @event CommandHandler#commandStarted * @param {Message} message - Message sent. * @param {Command} command - Command executed. * @param {any} args - The args passed to the command. */ /** * Emitted when a command finishes execution. * @event CommandHandler#commandFinished * @param {Message} message - Message sent. * @param {Command} command - Command executed. * @param {any} args - The args passed to the command. * @param {any} returnValue - The command's return value. */ /** * Emitted when a command is cancelled via prompt or argument cancel. * @event CommandHandler#commandCancelled * @param {Message} message - Message sent. * @param {Command} command - Command executed. * @param {?Message} retryMessage - Message to retry with. * This is passed when a prompt was broken out of with a message that looks like a command. */ /** * Emitted when a command is found on cooldown. * @event CommandHandler#cooldown * @param {Message} message - Message sent. * @param {Command} command - Command blocked. * @param {number} remaining - Remaining time in milliseconds for cooldown. */ /** * Emitted when a user is in a command argument prompt. * Used to prevent usage of commands during a prompt. * @event CommandHandler#inPrompt * @param {Message} message - Message sent. */ /** * Emitted when a permissions check is failed. * @event CommandHandler#missingPermissions * @param {Message} message - Message sent. * @param {Command} command - Command blocked. * @param {string} type - Either 'client' or 'user'. * @param {any} missing - The missing permissions. */ /** * Emitted when a command or inhibitor errors. * @event CommandHandler#error * @param {Error} error - The error. * @param {Message} message - Message sent. * @param {?Command} command - Command executed. */ /** * Emitted when a command is loaded. * @event CommandHandler#load * @param {Command} command - Module loaded. * @param {boolean} isReload - Whether or not this was a reload. */ /** * Emitted when a command is removed. * @event CommandHandler#remove * @param {Command} command - Command removed. */ /** * Also includes properties from AkairoHandlerOptions. * @typedef {AkairoHandlerOptions} CommandHandlerOptions * @prop {boolean} [blockClient=true] - Whether or not to block self. * @prop {boolean} [blockBots=true] - Whether or not to block bots. * @prop {string|string[]|PrefixSupplier} [prefix='!'] - Default command prefix(es). * @prop {boolean|MentionPrefixPredicate} [allowMention=true] - Whether or not to allow mentions to the client user as a prefix. * @prop {RegExp} [aliasReplacement] - Regular expression to automatically make command aliases. * For example, using `/-/g` would mean that aliases containing `-` would be valid with and without it. * So, the alias `command-name` is valid as both `command-name` and `commandname`. * @prop {boolean} [handleEdits=false] - Whether or not to handle edited messages using CommandUtil. * @prop {boolean} [storeMessages=false] - Whether or not to have CommandUtil store all prompts and their replies. * @prop {boolean} [commandUtil=false] - Whether or not to assign `message.util`. * @prop {number} [commandUtilLifetime=3e5] - Milliseconds a message should exist for before its command util instance is marked for removal. * If 0, CommandUtil instances will never be removed and will cause memory to increase indefinitely. * @prop {number} [commandUtilSweepInterval=3e5] - Time interval in milliseconds for sweeping command util instances. * If 0, CommandUtil instances will never be removed and will cause memory to increase indefinitely. * @prop {boolean} [fetchMembers=false] - Whether or not to fetch member on each message from a guild. * @prop {number} [defaultCooldown=0] - The default cooldown for commands. * @prop {Snowflake|Snowflake[]|IgnoreCheckPredicate} [ignoreCooldown] - ID of user(s) to ignore cooldown or a function to ignore. * Defaults to the client owner(s). * @prop {Snowflake|Snowflake[]|IgnoreCheckPredicate} [ignorePermissions=[]] - ID of user(s) to ignore `userPermissions` checks or a function to ignore. * @prop {DefaultArgumentOptions} [argumentDefaults] - The default argument options. */ /** * Data for managing cooldowns. * @typedef {Object} CooldownData * @prop {Timeout} timer - Timeout object. * @prop {number} end - When the cooldown ends. * @prop {number} uses - Number of times the command has been used. */ /** * Various parsed components of the message. * @typedef {Object} ParsedComponentData * @prop {?Command} command - The command used. * @prop {?string} prefix - The prefix used. * @prop {?string} alias - The alias used. * @prop {?string} content - The content to the right of the alias. * @prop {?string} afterPrefix - The content to the right of the prefix. */ /** * A function that returns whether this message should be ignored for a certain check. * @typedef {Function} IgnoreCheckPredicate * @param {Message} message - Message to check. * @param {Command} command - Command to check. * @returns {boolean} */ /** * A function that returns whether mentions can be used as a prefix. * @typedef {Function} MentionPrefixPredicate * @param {Message} message - Message to option for. * @returns {boolean} */ /** * A function that returns the prefix(es) to use. * @typedef {Function} PrefixSupplier * @param {Message} message - Message to get prefix for. * @returns {string|string[]} */