muskytape
Version:
Framework não oficial do Discord.js
306 lines (275 loc) • 10 kB
JavaScript
const { escapeRegex } = require('./util');
/** Handles parsing messages and running commands from them */
class CommandDispatcher {
/**
* @param {CommandoClient} client - Client the dispatcher is for
* @param {CommandoRegistry} registry - Registry the dispatcher will use
*/
constructor(client, registry) {
/**
* Client this dispatcher handles messages for
* @name CommandDispatcher#client
* @type {CommandoClient}
* @readonly
*/
Object.defineProperty(this, 'client', { value: client });
/**
* Registry this dispatcher uses
* @type {CommandoRegistry}
*/
this.registry = registry;
/**
* Functions that can block commands from running
* @type {Set<Function>}
*/
this.inhibitors = new Set();
/**
* Map object of {@link RegExp}s that match command messages, mapped by string prefix
* @type {Object}
* @private
*/
this._commandPatterns = {};
/**
* Old command message results, mapped by original message ID
* @type {Map<string, CommandoMessage>}
* @private
*/
this._results = new Map();
/**
* Tuples in string form of user ID and channel ID that are currently awaiting messages from a user in a channel
* @type {Set<string>}
* @private
*/
this._awaiting = new Set();
}
/**
* @typedef {Object} Inhibition
* @property {string} reason - Identifier for the reason the command is being blocked
* @property {?Promise<Message>} response - Response being sent to the user
*/
/**
* A function that decides whether the usage of a command should be blocked
* @callback Inhibitor
* @param {CommandoMessage} msg - Message triggering the command
* @return {boolean|string|Inhibition} `false` if the command should *not* be blocked.
* If the command *should* be blocked, then one of the following:
* - A single string identifying the reason the command is blocked
* - An Inhibition object
*/
/**
* Adds an inhibitor
* @param {Inhibitor} inhibitor - The inhibitor function to add
* @return {boolean} Whether the addition was successful
* @example
* client.dispatcher.addInhibitor(msg => {
* if(blacklistedUsers.has(msg.author.id)) return 'blacklisted';
* });
* @example
* client.dispatcher.addInhibitor(msg => {
* if(!coolUsers.has(msg.author.id)) return { reason: 'cool', response: msg.reply('You\'re not cool enough!') };
* });
*/
addInhibitor(inhibitor) {
if(typeof inhibitor !== 'function') throw new TypeError('The inhibitor must be a function.');
if(this.inhibitors.has(inhibitor)) return false;
this.inhibitors.add(inhibitor);
return true;
}
/**
* Removes an inhibitor
* @param {Inhibitor} inhibitor - The inhibitor function to remove
* @return {boolean} Whether the removal was successful
*/
removeInhibitor(inhibitor) {
if(typeof inhibitor !== 'function') throw new TypeError('The inhibitor must be a function.');
return this.inhibitors.delete(inhibitor);
}
/**
* Handle a new message or a message update
* @param {Message} message - The message to handle
* @param {Message} [oldMessage] - The old message before the update
* @return {Promise<void>}
* @private
*/
async handleMessage(message, oldMessage) {
/* eslint-disable max-depth */
if(!this.shouldHandleMessage(message, oldMessage)) return;
// Parse the message, and get the old result if it exists
let cmdMsg, oldCmdMsg;
if(oldMessage) {
oldCmdMsg = this._results.get(oldMessage.id);
if(!oldCmdMsg && !this.client.options.nonCommandEditable) return;
cmdMsg = this.parseMessage(message);
if(cmdMsg && oldCmdMsg) {
cmdMsg.responses = oldCmdMsg.responses;
cmdMsg.responsePositions = oldCmdMsg.responsePositions;
}
} else {
cmdMsg = this.parseMessage(message);
}
// Run the command, or reply with an error
let responses;
if(cmdMsg) {
const inhibited = this.inhibit(cmdMsg);
if(!inhibited) {
if(cmdMsg.command) {
if(!cmdMsg.command.isEnabledIn(message.guild)) {
if(!cmdMsg.command.unknown) {
responses = await cmdMsg.reply(`The \`${cmdMsg.command.name}\` command is disabled.`);
} else {
/**
* Emitted when an unknown command is triggered
* @event CommandoClient#unknownCommand
* @param {CommandoMessage} message - Command message that triggered the command
*/
this.client.emit('unknownCommand', cmdMsg);
responses = undefined;
}
} else if(!oldMessage || typeof oldCmdMsg !== 'undefined') {
responses = await cmdMsg.run();
if(typeof responses === 'undefined') responses = null;
if(Array.isArray(responses)) responses = await Promise.all(responses);
}
} else {
this.client.emit('unknownCommand', cmdMsg);
responses = undefined;
}
} else {
responses = await inhibited.response;
}
cmdMsg.finalize(responses);
} else if(oldCmdMsg) {
oldCmdMsg.finalize(null);
if(!this.client.options.nonCommandEditable) this._results.delete(message.id);
}
this.cacheCommandoMessage(message, oldMessage, cmdMsg, responses);
/* eslint-enable max-depth */
}
/**
* Check whether a message should be handled
* @param {Message} message - The message to handle
* @param {Message} [oldMessage] - The old message before the update
* @return {boolean}
* @private
*/
shouldHandleMessage(message, oldMessage) {
// Ignore partial messages
if(message.partial) return false;
if(message.author.bot) return false;
else if(message.author.id === this.client.user.id) return false;
// Ignore messages from users that the bot is already waiting for input from
if(this._awaiting.has(message.author.id + message.channel.id)) return false;
// Make sure the edit actually changed the message content
if(oldMessage && message.content === oldMessage.content) return false;
return true;
}
/**
* Inhibits a command message
* @param {CommandoMessage} cmdMsg - Command message to inhibit
* @return {?Inhibition}
* @private
*/
inhibit(cmdMsg) {
for(const inhibitor of this.inhibitors) {
let inhibit = inhibitor(cmdMsg);
if(inhibit) {
if(typeof inhibit !== 'object') inhibit = { reason: inhibit, response: undefined };
const valid = typeof inhibit.reason === 'string' && (
typeof inhibit.response === 'undefined' ||
inhibit.response === null ||
inhibit.response instanceof Promise
);
if(!valid) {
throw new TypeError(
`Inhibitor "${inhibitor.name}" had an invalid result; must be a string or an Inhibition object.`
);
}
this.client.emit('commandBlock', cmdMsg, inhibit.reason, inhibit);
return inhibit;
}
}
return null;
}
/**
* Caches a command message to be editable
* @param {Message} message - Triggering message
* @param {Message} oldMessage - Triggering message's old version
* @param {CommandoMessage} cmdMsg - Command message to cache
* @param {Message|Message[]} responses - Responses to the message
* @private
*/
cacheCommandoMessage(message, oldMessage, cmdMsg, responses) {
if(this.client.options.commandEditableDuration <= 0) return;
if(!cmdMsg && !this.client.options.nonCommandEditable) return;
if(responses !== null) {
this._results.set(message.id, cmdMsg);
if(!oldMessage) {
setTimeout(() => { this._results.delete(message.id); }, this.client.options.commandEditableDuration * 1000);
}
} else {
this._results.delete(message.id);
}
}
/**
* Parses a message to find details about command usage in it
* @param {Message} message - The message
* @return {?CommandoMessage}
* @private
*/
parseMessage(message) {
// Find the command to run by patterns
for(const command of this.registry.commands.values()) {
if(!command.patterns) continue;
for(const pattern of command.patterns) {
const matches = pattern.exec(message.content);
if(matches) return message.initCommand(command, null, matches);
}
}
// Find the command to run with default command handling
const prefix = message.guild ? message.guild.commandPrefix : this.client.commandPrefix;
if(!this._commandPatterns[prefix]) this.buildCommandPattern(prefix);
let cmdMsg = this.matchDefault(message, this._commandPatterns[prefix], 2);
if(!cmdMsg && !message.guild) cmdMsg = this.matchDefault(message, /^([^\s]+)/i, 1, true);
return cmdMsg;
}
/**
* Matches a message against a guild command pattern
* @param {Message} message - The message
* @param {RegExp} pattern - The pattern to match against
* @param {number} commandNameIndex - The index of the command name in the pattern matches
* @param {boolean} prefixless - Whether the match is happening for a prefixless usage
* @return {?CommandoMessage}
* @private
*/
matchDefault(message, pattern, commandNameIndex = 1, prefixless = false) {
const matches = pattern.exec(message.content);
if(!matches) return null;
const commands = this.registry.findCommands(matches[commandNameIndex], true);
if(commands.length !== 1 || !commands[0].defaultHandling) {
return message.initCommand(this.registry.unknownCommand, prefixless ? message.content : matches[1]);
}
const argString = message.content.substring(matches[1].length + (matches[2] ? matches[2].length : 0));
return message.initCommand(commands[0], argString);
}
/**
* Creates a regular expression to match the command prefix and name in a message
* @param {?string} prefix - Prefix to build the pattern for
* @return {RegExp}
* @private
*/
buildCommandPattern(prefix) {
let pattern;
if(prefix) {
const escapedPrefix = escapeRegex(prefix);
pattern = new RegExp(
`^(<@!?${this.client.user.id}>\\s+(?:${escapedPrefix}\\s*)?|${escapedPrefix}\\s*)([^\\s]+)`, 'i'
);
} else {
pattern = new RegExp(`(^<@!?${this.client.user.id}>\\s+)([^\\s]+)`, 'i');
}
this._commandPatterns[prefix] = pattern;
this.client.emit('debug', `Built command pattern for prefix "${prefix}": ${pattern}`);
return pattern;
}
}
module.exports = CommandDispatcher;