This repository has been archived by the owner on Dec 2, 2023. It is now read-only.
/
ChatCommand.ts
364 lines (357 loc) · 14.5 KB
/
ChatCommand.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
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
import { Message, Interaction, CommandInteractionOption } from "discord.js";
import { DefaultParameter, InputParameter, ObjectID, Parameter, ParameterSchema } from "../structures/Parameter.js";
import { ChatCommandObject, TextCommandOptionChoiceObject, ChatCommandOptionObject, ChatCommandOptionType } from "../structures/apiTypes.js";
import { ChildCommandInit, ChildCommandResolvable, ChildCommands, ChildCommandType, CommandRegExps } from "./commandsTypes.js";
import { CommandManager } from "../structures/CommandManager.js";
import { PermissionGuildCommand, PermissionGuildCommandInit } from "./base/PermissionGuildCommand.js";
import { generateUsageFromArguments } from "../utils/generateUsageFromArguments.js";
import { SubCommand, SubCommandInit } from "./SubCommand.js";
import { SubCommandGroup } from "./SubCommandGroup.js";
import { applicationState } from "../state.js";
import { InputManager } from "../structures/InputManager.js";
import { SubCommandGroupInit } from "./SubCommandGroup.js";
/**
* Intialization options of chat command
* @interface
* @extends {PermissionGuildCommandInit}
*/
export interface ChatCommandInit extends PermissionGuildCommandInit {
/**
* List of object defining all parameters of the command
* @type {?ParameterSchema[] | "simple" | "no_input"}
*/
parameters?: ParameterSchema[] | "simple" | "no_input";
/**
* Different string that can be used with prefix to invoke the command
* @type {?Array<string>}
*/
aliases?: string[] | string;
/**
* Command description
* @type {?string}
*/
description?: string;
/**
* Command usage (if *undefined*, the usage will be automatically generated using parameters)
* @type {?string}
*/
usage?: string;
/**
* Whether this command is visible in the help message
* @type {?boolean}
*/
visible?: boolean;
/**
* Whether this command should be registered as a slash command
* @type {?boolean}
*/
slash?: boolean;
}
/**
* A representation of CHAT_INPUT command (also known as a slash command)
* @class
* @extends {PermissionGuildCommand}
*/
export class ChatCommand extends PermissionGuildCommand {
/**
* Subcommands and groups of this command
* @type {Array<ChildCommandResolvable>}
* @private
* @readonly
*/
private readonly _children: ChildCommandResolvable[] = [];
/**
* List of parameters that can passed to this command
* @type {Array<Parameter<any>>}
* @public
* @readonly
*/
public readonly parameters: Parameter<any>[];
/**
* List of different names that can be used to invoke a command (when using prefix interactions)
* @type {?Array<string>}
* @public
* @readonly
*/
public readonly aliases?: string[];
/**
* Command description displayed in the help message or in slash commands menu (Default description: "No description")
* @type {string}
* @public
* @readonly
*/
public readonly description: string;
/**
* Command usage displayed in the help message
* @type {?string}
* @public
* @readonly
*/
public readonly usage?: string;
/**
* Whether this command is visible in the help message (default: true)
* @type {boolean}
* @public
* @readonly
*/
public readonly visible: boolean;
/**
* Whether this command should be registered as a slash command (default: true)
* @type {boolean}
* @public
* @readonly
*/
public readonly slash: boolean;
/**
* ChatCommand constructor
* @constructor
* @param {CommandManager} manager - a manager that this command belongs to
* @param {ChatCommandInit} options - {@link ChatCommandInit} object containing all options needed to create a {@link ChatCommand}
*/
constructor(manager: CommandManager, options: ChatCommandInit) {
super(manager, "CHAT", {
name: options.name,
function: options.function,
announceSuccess: options.announceSuccess,
guilds: options.guilds,
permissions: options.permissions,
dm: options.dm,
ephemeral: options.ephemeral,
});
if (options.parameters == "no_input" || !options.parameters) {
this.parameters = [];
} else if (options.parameters == "simple") {
this.parameters = [new DefaultParameter(this)];
} else {
this.parameters = options.parameters.map((ps) => new Parameter(this, ps));
}
this.aliases = options.aliases ? (Array.isArray(options.aliases) ? options.aliases : [options.aliases]) : undefined;
this.description = options.description ?? "No description";
this.usage = options.usage ?? generateUsageFromArguments(this);
this.visible = options.visible !== undefined ? options.visible : true;
this.slash = options.slash !== undefined ? options.slash : true;
if (!CommandRegExps.chatName.test(this.name)) {
throw new Error(`"${this.name}" is not a valid command name (regexp: ${CommandRegExps.chatName})`);
}
if (this.description && !CommandRegExps.chatDescription.test(this.description)) {
throw new Error(`The description of "${this.name}" doesn't match the regular expression ${CommandRegExps.chatDescription}`);
}
if (this.aliases) {
if (Array.isArray(this.aliases)) {
this.aliases.map((a) => {
if (!CommandRegExps.chatName.test(a)) {
throw new Error(`"${a}" is not a valid alias name (regexp: ${CommandRegExps.chatName})`);
}
});
} else {
if (!CommandRegExps.chatName.test(this.aliases)) {
throw new Error(`"${this.aliases}" is not a valid alias name (regexp: ${CommandRegExps.chatName})`);
}
}
}
if (this.aliases && this.aliases.length > 0 && this.aliases.find((a) => this.manager.get(a, this.type))) {
throw new Error(`One of aliases from "${this.name}" command is already a registered name in the manager and cannot be reused.`);
}
}
/**
* Returns *true* if the command has subcommands attached
* @type {boolean}
*/
get hasSubCommands() {
return this._children.length > 0;
}
/**
* Returns list of attached subcommands
* @type {Array<ChildCommandResolvable>}
* @readonly
*/
get children() {
return Object.freeze([...this._children]);
}
/**
* Invoke the command
* @param {InputManager} input - input data manager
* @returns {Promise<void>}
* @public
* @async
*/
public async start(input: InputManager): Promise<void> {
if (!this.slash && input.interaction instanceof Interaction) {
throw new Error("This command is not available as a slash command");
}
await super.start(input);
}
/**
* Attaches subcommand or subcommand group to this ChatCommand
* @param {T} type - subcommand type
* @param {ChildCommandInit<T>} options - initialization options
* @returns {ChildCommands<T>} A computed subcommand object
* @public
* @remarks After appending a subcommand or a subcommand group the main command can only be invoked using prefix interactions
*/
public append<T extends ChildCommandType>(type: T, options: ChildCommandInit<T>): ChildCommands<T> {
const command =
type === "COMMAND"
? (new SubCommand(this, options as SubCommandInit) as ChildCommands<T>)
: type === "GROUP"
? (new SubCommandGroup(this, options as SubCommandGroupInit) as ChildCommands<T>)
: null;
if (!command) {
throw new Error("Incorrect command type");
}
if (applicationState.running) {
console.warn(`[❌ ERROR] Cannot add command "${command.name}" while the application is running.`);
return command;
}
this._children.push(command);
return command;
}
/**
*
* @param {Array<CommandInteractionOption>} options - parameter options
* @param {Interaction | Message} interaction - Discord interaction
* @returns {?InputManager} an {@link InputManager} containing all interaction-related data or *null*
* @public
*/
public fetchSubcommand(options: CommandInteractionOption[], interaction: Interaction | Message): InputManager | null {
if (!this.hasSubCommands) return null;
if (options[0]) {
if (options[0].type === "SUB_COMMAND_GROUP") {
const grName = options[0].name;
const group = this._children.filter((c) => c instanceof SubCommandGroup).find((c) => c.name === grName) as SubCommandGroup;
const scOpt = options[0].options;
if (group && scOpt) {
const scName = scOpt[0].name;
const cmd = group.children.filter((c) => c instanceof SubCommand).find((c) => c.name === scName) as SubCommand;
if (cmd && scOpt[0].options) {
return new InputManager(
cmd,
interaction,
cmd.parameters.map((p, index) => {
if (p.type === "user" || p.type === "role" || p.type === "channel" || p.type === "mentionable") {
return new InputParameter(p, new ObjectID(scOpt[0].options?.[index].value?.toString() ?? "", p.type, interaction.guild ?? undefined));
} else {
return new InputParameter(p, scOpt[0].options?.[index].value ?? null);
}
})
);
} else {
return null;
}
} else {
return null;
}
} else if (options[0].type === "SUB_COMMAND") {
const cmd = this._children.filter((c) => c instanceof SubCommand).find((c) => c.name === options[0].name) as SubCommand;
if (cmd) {
return new InputManager(
cmd,
interaction,
cmd.parameters.map((p, index) => {
if (p.type === "user" || p.type === "role" || p.type === "channel" || p.type === "mentionable") {
return new InputParameter(p, new ObjectID(options[0].options?.[index].value?.toString() ?? "", p.type, interaction.guild ?? undefined));
} else {
return new InputParameter(p, options[0].options?.[index].value ?? null);
}
})
);
} else {
return null;
}
} else {
return null;
}
} else {
return null;
}
}
/**
*
* @param {string} name - subcommand name
* @param {?string} [group] - name of the group (if any)
* @returns {?SubCommand} a {@link SubCommand} object or *null*
*/
public getSubcommand(name: string, group?: string): SubCommand | null {
if (!this.hasSubCommands) return null;
if (group) {
const gr = this._children.filter((c) => c instanceof SubCommandGroup).find((g) => g.name === group) as SubCommandGroup;
if (gr) {
return gr.children.find((c) => c.name === name) || null;
} else {
return null;
}
} else {
return (this._children.filter((c) => c instanceof SubCommand).find((c) => c.name === name) as SubCommand) || null;
}
}
/**
* Converts {@link ChatCommand} instance to object that is recognized by the Discord API
* @returns {ChatCommandObject} Discord API object
* @public
*/
public toObject(): ChatCommandObject {
const obj: ChatCommandObject = {
...super.toObject(),
type: 1,
description: this.description,
};
let options: ChatCommandOptionObject[] = [];
if (this.parameters) {
options = this.parameters
.map((p) => {
let type = 3;
switch (p.type) {
case "boolean":
type = 5;
break;
case "user":
type = 6;
break;
case "channel":
type = 7;
break;
case "role":
type = 8;
break;
case "mentionable":
type = 9;
break;
case "number":
type = 10;
break;
case "target":
throw new Error(`"target" parameter cannot be used in chat commands`);
default:
type = 3;
break;
}
const choices: TextCommandOptionChoiceObject[] = [];
if (p.choices) {
p.choices.map((c) => {
choices.push({ name: c, value: c });
});
}
const optionObj: ChatCommandOptionObject = {
name: p.name,
description: p.description,
required: !p.optional,
type: p.choices ? 3 : (type as ChatCommandOptionType),
choices: choices.length > 0 ? choices : undefined,
};
return optionObj;
})
.sort((a, b) => {
if (a.required && !b.required) {
return -1;
} else if (a.required && b.required) {
return 0;
} else if (!a.required && b.required) {
return 1;
}
return 0;
});
obj.options = this.hasSubCommands ? this._children.map((sc) => sc.toObject()) : options;
}
return obj;
}
}