This repository has been archived by the owner on Dec 24, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 242
/
dispatcher.js
306 lines (276 loc) · 9.8 KB
/
dispatcher.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
const { escapeRegex } = require('./util');
const isPromise = require('is-promise');
/** 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 ||
isPromise(inhibit.response)
);
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;