This repository has been archived by the owner on Mar 15, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Command.ts
286 lines (259 loc) · 12.1 KB
/
Command.ts
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
import { Message, User, PermissionString, Guild, GuildMember } from "discord.js";
import { CommandSet } from "./CommandSet";
import { CommandSetOptions } from "./CommandSetOptions";
import { CommandData } from "./CommandData";
import { CommandDefinition } from "./definition/CommandDefinition";
import { ArgDefinition } from "./definition/ArgDefinition";
import { FlagDefinition } from "./definition/FlagDefinition";
import { Char } from "../utils/char";
import { CommandExecutor } from "./callbacks/CommandExecutor";
import { parseFlags } from "../other/parsing/parseFlags";
import { parseArgs } from "../other/parsing/parseArgs";
import { RestDefinition } from "./definition/RestDefinition";
import { CommandResultUtils } from "./CommandResult";
import { CommandResultError } from "./errors/CommandResultError";
import { ReadonlyCommandCollection, CommandCollection } from "./CommandCollection";
import { CanUseCommandHandler } from "./callbacks/CanUseCommandHandler";
import { HelpHandler } from "./callbacks/HelpHandler";
import { parseValue } from "../other/parsing/parseValue";
import { ParsableType } from "./ParsableType";
import { ThrottlingDefinition } from "./definition/ThrottlingDefinition";
import { Throttler } from "./Throttler";
import { CommandLoadError } from "./errors/CommandLoadError";
import { defaultHelp } from "../other/HelpUtils";
export class Command {
private readonly _throttler: Throttler | null | undefined;
/** Either or not this command's throttler also includes users with administrator permission. */
public readonly _throttlingIncludeAdmins: boolean;
private constructor(
/** Path to the file that contains the command if it's a top-most command, `null` otherwise. */
public readonly filepath: string | null,
/** Name of this command. */
public readonly name: string,
/** Aliases of this command. */
public readonly aliases: readonly string[],
/** The list of permissions the bot's user require to execute this command. */
public readonly clientPermissions: readonly PermissionString[],
/** The list of permissions the user require to execute this command. */
public readonly userPermissions: readonly PermissionString[] | undefined,
/** The list of exemples for this command. */
public readonly examples: readonly string[],
/** The description of the command. */
public readonly description: string,
/** This command's parent or `null` if it's a top-most command. */
public readonly parent: Command | null,
/** The [[CommandSet]] that contains this command. */
public readonly commandSet: CommandSet,
/** A [[ReadonlyCommandCollection]] of this command's sub-commands. */
public readonly subs: ReadonlyCommandCollection,
/** A `ReadonlyMap` with this command's arguments' [[ArgDefinition]] */
public readonly args: ReadonlyMap<string, ArgDefinition>,
/** A [[RestDefinition]] if this command use the rest argument, `undefined` otherwise. */
public readonly rest: Readonly<RestDefinition> | undefined,
/** A `ReadonlyMap` with this command's flags' [[FlagDefinition]] */
public readonly flags: ReadonlyMap<string, FlagDefinition>,
private readonly _flagsShortcuts: ReadonlyMap<Char, string>,
private readonly _executor: CommandExecutor<any> | undefined, // eslint-disable-line @typescript-eslint/no-explicit-any
private readonly _canUse: CanUseCommandHandler | undefined,
private readonly _help: HelpHandler | undefined,
throttling: ThrottlingDefinition | null | undefined,
private readonly _useThrottlerOnSubs: boolean,
/** Either or not this command is ignored. */
public readonly ignored: boolean,
/** Either or not this command can only be used by dev (see [[CommandSetOptions.devIDs]]). */
public readonly devOnly: boolean,
/** Either or not this command can only be used from a guild. */
public readonly guildOnly: boolean,
/** Either or not the message that executed this command is deleted after the command execution. */
public readonly deleteMessage: boolean,
) {
this._throttler = throttling ? new Throttler(throttling.count, throttling.duration) : throttling;
this._throttlingIncludeAdmins = throttling?.includeAdmins ?? false;
}
/** @internal */
static load(filepath: string, commandSet: CommandSet): Command {
const module = require(filepath); // eslint-disable-line @typescript-eslint/no-var-requires
if (!module.default) throw new CommandLoadError("Command data must be exported as default.");
return Command._build(filepath, commandSet, module.default, null, undefined);
}
private static _build<T extends CommandDefinition>(
filepath: string | null,
commandSet: CommandSet,
data: CommandData<T>,
parent: Command | null,
parentHelp: HelpHandler | undefined,
): Command {
function resolveInheritance<K extends keyof Command>(prop: K, defaultValue: Command[K]): Command[K] {
return (data.def.inherit ?? true) && parent ? parent[prop] : defaultValue;
}
const subs = new CommandCollection();
const cmd = new Command(
filepath,
data.name,
data.def.aliases ?? [],
data.def.clientPermissions ?? [],
data.def.userPermissions,
data.def.examples ?? [],
data.def.description ?? "",
parent,
commandSet,
subs,
new Map(data.def.args ? Object.entries(data.def.args) : []),
data.def.rest,
new Map(data.def.flags ? Object.entries(data.def.flags) : []),
new Map(
data.def.flags
? Object.entries(data.def.flags)
.filter(function (a): a is [string, FlagDefinition & { shortcut: Char }] {
return a[1].shortcut !== undefined;
})
.map(([k, v]) => [v.shortcut, k])
: [],
),
data.executor,
data.def.canUse,
data.def.help ?? parentHelp,
data.def.throttling,
data.def.useThrottlerForSubs ?? true,
data.def.ignore ?? resolveInheritance("ignored", false),
data.def.devOnly ?? resolveInheritance("devOnly", false),
data.def.guildOnly ?? resolveInheritance("guildOnly", false),
data.def.deleteMessage ?? resolveInheritance("deleteMessage", false),
);
for (const subName in data.subs)
subs.add(
Command._build(
null,
commandSet,
data.subs[subName],
cmd,
(data.def.useHelpOnSubs ?? false) || (!data.def.help && !!parentHelp) ? cmd._help : undefined,
),
);
return cmd;
}
// === Getter =====================================================
/**
* The [[Throttler]] used by this command.
*/
get throttler(): Throttler | undefined {
if (this._throttler === null) return undefined;
if (this._throttler) return this._throttler;
if (this.parent && this.parent._useThrottlerOnSubs) return this.parent.throttler;
return undefined;
}
/**
* Either or not this command has an executor.
*/
get hasExecutor(): boolean {
return this._executor !== undefined;
}
/**
* Returns an array containing all parents of this command, ordered from top-most command to this command (included).
* @returns An array of this command's parent [[Command]].
*/
getParents(): Command[] {
const parents: Command[] = [];
parents.unshift(this);
for (let parent = this.parent; parent; parent = parent.parent) parents.unshift(parent);
return parents;
}
/**
* Determines if the bot's user have required permissions to execute this command from the guild.
* @param guild The guild from which check permissions.
* @returns Either or not the bot's user have required permissions to execute this command from the guild.
*/
hasClientPermissions(guild: Guild): boolean {
return guild.me ? guild.me.hasPermission(this.clientPermissions) : false;
}
/**
* Determines if a member have required permissions to execute this command.
* @param member
* @returns Either or not the member have required permissions to execute this command.
*/
hasPermissions(member: GuildMember): boolean {
if (!this.userPermissions) {
if (this.parent) return this.parent.hasPermissions(member);
else return true;
}
return member.hasPermission(this.userPermissions);
}
// =====================================================
/**
* Call the `canUse` handler of this command and its parents (see [[CommandDefinition.canUse]])
* and return the first negative result (`false` or a `string`) or `true`.
* @param user
* @param message
* @returns Result of `canUse` handlers.
*/
canUse(user: User, message: Message): boolean | string {
if (this.parent) {
const res = this.parent.canUse(user, message);
if (res !== true) return res;
}
if (this._canUse) return this._canUse(user, message);
return true;
}
/**
* Determines of the message author pass the [[canUse]] and the [[hasPermissions]] checks.
* @param message
* @returns Either or not the message author pass the [[canUse]] and the [[hasPermissions]] checks.
*/
checkPermissions(message: Message): boolean {
return (
this.canUse(message.author, message) === true && (!message.member || this.hasPermissions(message.member))
);
}
/**
* Call the suitable help handler for this command.
* @param message
* @param options
*/
async help(message: Message, options: CommandSetOptions): Promise<void> {
const context = {
message,
options,
commandSet: this.commandSet,
};
if (this._help) {
await this._help(this, context);
} else if (this.commandSet.helpHandler) {
await this.commandSet.helpHandler(this, context);
} else {
await defaultHelp(this, context);
}
}
/** @internal */
async execute(message: Message, inputArguments: string[], options: CommandSetOptions, commandSet: CommandSet) {
if (message.guild && !this.hasClientPermissions(message.guild))
throw new CommandResultError(CommandResultUtils.clientPermissions(this));
/** This command throttler if is required and defined, undefined otherwise. */
const throttler =
!options.devIDs.includes(message.author.id) &&
!(!this._throttlingIncludeAdmins && message.member && message.member.permissions.has("ADMINISTRATOR"))
? this.throttler
: undefined;
if (throttler?.throttled) throw new CommandResultError(CommandResultUtils.throttling(this));
if (!this._executor) throw new CommandResultError(CommandResultUtils.noExecutor(this));
const flags = parseFlags(message, inputArguments, this.flags, this._flagsShortcuts);
const args = parseArgs(message, flags.args, this.args);
const rest: ParsableType[] = [];
if (this.rest) {
for (const e of args.rest) {
const parsed = parseValue(this.rest, message, e);
if (parsed.value !== undefined) rest.push(parsed.value);
}
}
if (throttler) throttler.add();
return await this._executor(Object.fromEntries(args.argValues), Object.fromEntries(flags.flagValues), {
rest: rest as any, // eslint-disable-line @typescript-eslint/no-explicit-any
message,
guild: message.guild,
member: message.member,
channel: message.channel,
options,
commandSet,
command: this,
});
}
}