This repository was archived by the owner on Dec 24, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 236
Expand file tree
/
Copy pathbase.js
More file actions
574 lines (520 loc) · 21.9 KB
/
base.js
File metadata and controls
574 lines (520 loc) · 21.9 KB
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
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
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 `The \`${this.name}\` command can only be used by the bot owner.`;
}
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}\` command requires you to have the "${permissions[missing[0]]}" permission.`;
}
return oneLine`
The \`${this.name}\` command requires you to have the following permissions:
${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(`The \`${this.name}\` command must be used in a server channel.`);
case 'nsfw':
return message.reply(`The \`${this.name}\` command can only be used in NSFW channels.`);
case 'permission': {
if(data.response) return message.reply(data.response);
return message.reply(`You do not have permission to use the \`${this.name}\` command.`);
}
case 'clientPermissions': {
if(data.missing.length === 1) {
return message.reply(
`I need the "${permissions[data.missing[0]]}" permission for the \`${this.name}\` command to work.`
);
}
return message.reply(oneLine`
I need the following permissions for the \`${this.name}\` command to work:
${data.missing.map(perm => permissions[perm]).join(', ')}
`);
}
case 'throttling': {
return message.reply(
`You may not use the \`${this.name}\` command again for another ${data.remaining.toFixed(1)} seconds.`
);
}
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`
An error occurred while running the command: \`${err.name}: ${err.message}\`
You shouldn't ever receive an error like this.
Please contact ${ownerList || 'the bot owner'}${invite ? ` in this server: ${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('Guild must not be undefined.');
if(typeof enabled === 'undefined') throw new TypeError('Enabled must not be undefined.');
if(this.guarded) throw new Error('The command is guarded.');
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('Cannot find module')) 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('Command cannot be unloaded.');
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
* @param {boolean} [mentionPrefix] - Value, wether or not mention as prefix is enabled.
* @return {string}
*/
static usage(command, prefix = null, user = null, mentionPrefix = 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 && mentionPrefix) {
mentionPart = `\`@${user.username.replace(/ /g, '\xa0')}#${user.discriminator}\xa0${nbcmd}\``;
}
return `${prefixPart || ''}${prefix && user && mentionPrefix ? ' 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('A client must be specified.');
if(typeof info !== 'object') throw new TypeError('Command info must be an Object.');
if(typeof info.name !== 'string') throw new TypeError('Command name must be a string.');
if(info.name !== info.name.toLowerCase()) throw new Error('Command name must be lowercase.');
if(info.aliases && (!Array.isArray(info.aliases) || info.aliases.some(ali => typeof ali !== 'string'))) {
throw new TypeError('Command aliases must be an Array of strings.');
}
if(info.aliases && info.aliases.some(ali => ali !== ali.toLowerCase())) {
throw new RangeError('Command aliases must be lowercase.');
}
if(typeof info.group !== 'string') throw new TypeError('Command group must be a string.');
if(info.group !== info.group.toLowerCase()) throw new RangeError('Command group must be lowercase.');
if(typeof info.memberName !== 'string') throw new TypeError('Command memberName must be a string.');
if(info.memberName !== info.memberName.toLowerCase()) throw new Error('Command memberName must be lowercase.');
if(typeof info.description !== 'string') throw new TypeError('Command description must be a string.');
if('format' in info && typeof info.format !== 'string') throw new TypeError('Command format must be a string.');
if('details' in info && typeof info.details !== 'string') throw new TypeError('Command details must be a string.');
if(info.examples && (!Array.isArray(info.examples) || info.examples.some(ex => typeof ex !== 'string'))) {
throw new TypeError('Command examples must be an Array of strings.');
}
if(info.clientPermissions) {
if(!Array.isArray(info.clientPermissions)) {
throw new TypeError('Command clientPermissions must be an Array of permission key strings.');
}
for(const perm of info.clientPermissions) {
if(!permissions[perm]) throw new RangeError(`Invalid command clientPermission: ${perm}`);
}
}
if(info.userPermissions) {
if(!Array.isArray(info.userPermissions)) {
throw new TypeError('Command userPermissions must be an Array of permission key strings.');
}
for(const perm of info.userPermissions) {
if(!permissions[perm]) throw new RangeError(`Invalid command userPermission: ${perm}`);
}
}
if(info.throttling) {
if(typeof info.throttling !== 'object') throw new TypeError('Command throttling must be an Object.');
if(typeof info.throttling.usages !== 'number' || isNaN(info.throttling.usages)) {
throw new TypeError('Command throttling usages must be a number.');
}
if(info.throttling.usages < 1) throw new RangeError('Command throttling usages must be at least 1.');
if(typeof info.throttling.duration !== 'number' || isNaN(info.throttling.duration)) {
throw new TypeError('Command throttling duration must be a number.');
}
if(info.throttling.duration < 1) throw new RangeError('Command throttling duration must be at least 1.');
}
if(info.args && !Array.isArray(info.args)) throw new TypeError('Command args must be an Array.');
if('argsPromptLimit' in info && typeof info.argsPromptLimit !== 'number') {
throw new TypeError('Command argsPromptLimit must be a number.');
}
if('argsPromptLimit' in info && info.argsPromptLimit < 0) {
throw new RangeError('Command argsPromptLimit must be at least 0.');
}
if(info.argsType && !['single', 'multiple'].includes(info.argsType)) {
throw new RangeError('Command argsType must be one of "single" or "multiple".');
}
if(info.argsType === 'multiple' && info.argsCount && info.argsCount < 2) {
throw new RangeError('Command argsCount must be at least 2.');
}
if(info.patterns && (!Array.isArray(info.patterns) || info.patterns.some(pat => !(pat instanceof RegExp)))) {
throw new TypeError('Command patterns must be an Array of regular expressions.');
}
}
}
module.exports = Command;