forked from Supositware/Haha-Yes
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
283 lines
9.3 KiB
JavaScript
283 lines
9.3 KiB
JavaScript
6 years ago
|
const escapeRegex = require('escape-string-regexp');
|
||
|
const CommandMessage = require('./commands/message');
|
||
|
|
||
|
/** Handles parsing messages and running commands from them */
|
||
|
class CommandDispatcher {
|
||
|
/**
|
||
|
* @param {CommandoClient} client - Client the dispatcher is for
|
||
|
* @param {CommandRegistry} 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 {CommandRegistry}
|
||
|
*/
|
||
|
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, CommandMessage>}
|
||
|
* @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();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* A function that can block the usage of a command - these functions are passed the command message that is
|
||
|
* triggering the command. They should return `false` if the command should *not* be blocked. If the command *should*
|
||
|
* be blocked, they should return one of the following:
|
||
|
* - A single string identifying the reason the command is blocked
|
||
|
* - An array of the above string as element 0, and a response promise or `null` as element 1
|
||
|
* @typedef {Function} Inhibitor
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* 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 ['cool', 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);
|
||
|
}
|
||
|
|
||
|
// eslint-disable-next-line valid-jsdoc
|
||
|
/**
|
||
|
* 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) {
|
||
|
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)) {
|
||
|
responses = await cmdMsg.reply(`The \`${cmdMsg.command.name}\` command is disabled.`);
|
||
|
} else if(!oldMessage || typeof oldCmdMsg !== 'undefined') {
|
||
|
responses = await cmdMsg.run();
|
||
|
if(typeof responses === 'undefined') responses = null; // eslint-disable-line max-depth
|
||
|
}
|
||
|
} else {
|
||
|
/**
|
||
|
* Emitted when an unknown command is triggered
|
||
|
* @event CommandoClient#unknownCommand
|
||
|
* @param {CommandMessage} message - Command message that triggered the command
|
||
|
*/
|
||
|
this.client.emit('unknownCommand', cmdMsg);
|
||
|
if(this.client.options.unknownCommandResponse) {
|
||
|
responses = await cmdMsg.reply(
|
||
|
`Unknown command. Use ${cmdMsg.anyUsage(
|
||
|
'help',
|
||
|
message.guild ? undefined : null,
|
||
|
message.guild ? undefined : null
|
||
|
)} to view the list of all commands.`
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
} else {
|
||
|
responses = await inhibited[1];
|
||
|
}
|
||
|
|
||
|
cmdMsg.finalize(responses);
|
||
|
} else if(oldCmdMsg) {
|
||
|
oldCmdMsg.finalize(null);
|
||
|
if(!this.client.options.nonCommandEditable) this._results.delete(message.id);
|
||
|
}
|
||
|
|
||
|
this.cacheCommandMessage(message, oldMessage, cmdMsg, responses);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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) {
|
||
|
if(message.author.bot) return false;
|
||
|
else if(this.client.options.selfbot && message.author.id !== this.client.user.id) return false;
|
||
|
else if(!this.client.options.selfbot && 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 {CommandMessage} cmdMsg - Command message to inhibit
|
||
|
* @return {?Array} [reason, ?response]
|
||
|
* @private
|
||
|
*/
|
||
|
inhibit(cmdMsg) {
|
||
|
for(const inhibitor of this.inhibitors) {
|
||
|
const inhibited = inhibitor(cmdMsg);
|
||
|
if(inhibited) {
|
||
|
this.client.emit('commandBlocked', cmdMsg, inhibited instanceof Array ? inhibited[0] : inhibited);
|
||
|
return inhibited instanceof Array ? inhibited : [inhibited, undefined];
|
||
|
}
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Caches a command message to be editable
|
||
|
* @param {Message} message - Triggering message
|
||
|
* @param {Message} oldMessage - Triggering message's old version
|
||
|
* @param {CommandMessage} cmdMsg - Command message to cache
|
||
|
* @param {Message|Message[]} responses - Responses to the message
|
||
|
* @private
|
||
|
*/
|
||
|
cacheCommandMessage(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 {?CommandMessage}
|
||
|
* @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 new CommandMessage(message, 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 && !this.client.options.selfbot) cmdMsg = this.matchDefault(message, /^([^\s]+)/i);
|
||
|
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
|
||
|
* @return {?CommandMessage}
|
||
|
* @private
|
||
|
*/
|
||
|
matchDefault(message, pattern, commandNameIndex = 1) {
|
||
|
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 new CommandMessage(message, null);
|
||
|
const argString = message.content.substring(matches[1].length + (matches[2] ? matches[2].length : 0));
|
||
|
return new CommandMessage(message, 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;
|