@lunibee/builders 0.2.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/commands.ts CHANGED
@@ -1,720 +1,621 @@
1
- /** Discord application command option types. */
2
- export const ApplicationCommandOptionEnum = {
3
- Subcommand: 1,
4
- SubcommandGroup: 2,
5
- String: 3,
6
- Integer: 4,
7
- Boolean: 5,
8
- User: 6,
9
- Channel: 7,
10
- Role: 8,
11
- Mentionable: 9,
12
- Number: 10,
13
- Attachment: 11,
14
- } as const;
15
- /** One {@link ApplicationCommandOptionEnum} value, usable as a type. */
16
- export type ApplicationCommandOptionEnum =
17
- (typeof ApplicationCommandOptionEnum)[keyof typeof ApplicationCommandOptionEnum];
18
-
19
- /** Throws unless every context is 0 (guilds), 1 (the bot's DMs) or 2 (other DMs). */
20
- function validateContexts(contexts: number[]): void {
21
- if (
22
- contexts.some(
23
- (context) => context !== 0 && context !== 1 && context !== 2,
24
- )
25
- )
26
- throw new RangeError("Command contexts must be 0, 1 or 2.");
27
- }
28
-
29
- /** Builds Discord application command payloads. */
30
- export class CreateSlashCommand {
31
- readonly #data: Record<string, unknown> = { type: 1 };
32
- /** Sets the command name. */
33
- public setName(name: string): this {
34
- validateName(name, "Command name");
35
- this.#data.name = name;
36
- return this;
37
- }
38
- /** Sets the command description. */
39
- public setDescription(description: string): this {
40
- validateText(description, "Command description", 100);
41
- this.#data.description = description;
42
- return this;
43
- }
44
- /** Sets where the command can be used: 0 guilds, 1 the bot's DMs, 2 other DMs. @throws {RangeError} For any other value. */
45
- public setContexts(...contexts: number[]): this {
46
- validateContexts(contexts);
47
- this.#data.contexts = [...contexts];
48
- return this;
49
- }
50
- /** @deprecated Discord deprecated `dm_permission`. Use {@link CreateSlashCommand.setContexts}: `setContexts(0)` for guilds only, `setContexts(0, 1)` to allow the bot's DMs. Removed in 0.3.0. */
51
- public setDMPermission(enabled: boolean): this {
52
- this.#data.dm_permission = enabled;
53
- return this;
54
- }
55
- /** Sets whether the command is NSFW. */
56
- public setNSFW(nsfw = true): this {
57
- this.#data.nsfw = nsfw;
58
- return this;
59
- }
60
- /** Sets whether the command is NSFW. */
61
- public setNsfw(nsfw = true): this {
62
- return this.setNSFW(nsfw);
63
- }
64
- /** Sets command default member permissions. */
65
- public setDefaultMemberPermissions(
66
- permissions: bigint | number | string | null,
67
- ): this {
68
- this.#data.default_member_permissions =
69
- permissions === null ? null : BigInt(permissions).toString();
70
- return this;
71
- }
72
- /** Sets command integration types. */
73
- public setIntegrationTypes(...types: number[]): this {
74
- this.#data.integration_types = [...types];
75
- return this;
76
- }
77
- /** Adds a string option. */
78
- public addStringOption(
79
- configure: (option: CreateStringOption) => CreateStringOption,
80
- ): this {
81
- return this.addOption(configure(new CreateStringOption()));
82
- }
83
- /** Adds an integer option. */
84
- public addIntegerOption(
85
- configure: (option: CreateIntegerOption) => CreateIntegerOption,
86
- ): this {
87
- return this.addOption(configure(new CreateIntegerOption()));
88
- }
89
- /** Adds a number option. */
90
- public addNumberOption(
91
- configure: (option: CreateNumberOption) => CreateNumberOption,
92
- ): this {
93
- return this.addOption(configure(new CreateNumberOption()));
94
- }
95
- /** Adds a boolean option. */
96
- public addBooleanOption(
97
- configure: (option: CreateBooleanOption) => CreateBooleanOption,
98
- ): this {
99
- return this.addOption(configure(new CreateBooleanOption()));
100
- }
101
- /** Adds a user option. */
102
- public addUserOption(
103
- configure: (option: CreateUserOption) => CreateUserOption,
104
- ): this {
105
- return this.addOption(configure(new CreateUserOption()));
106
- }
107
- /** Adds a channel option. */
108
- public addChannelOption(
109
- configure: (option: CreateChannelOption) => CreateChannelOption,
110
- ): this {
111
- return this.addOption(configure(new CreateChannelOption()));
112
- }
113
- /** Adds a role option. */
114
- public addRoleOption(
115
- configure: (option: CreateRoleOption) => CreateRoleOption,
116
- ): this {
117
- return this.addOption(configure(new CreateRoleOption()));
118
- }
119
- /** Adds a mentionable option. */
120
- public addMentionableOption(
121
- configure: (option: CreateMentionableOption) => CreateMentionableOption,
122
- ): this {
123
- return this.addOption(configure(new CreateMentionableOption()));
124
- }
125
- /** Adds an attachment option. */
126
- public addAttachmentOption(
127
- configure: (option: CreateAttachmentOption) => CreateAttachmentOption,
128
- ): this {
129
- return this.addOption(configure(new CreateAttachmentOption()));
130
- }
131
- /** Adds a subcommand. */
132
- public addSubcommand(
133
- configure: (option: CreateSubcommand) => CreateSubcommand,
134
- ): this {
135
- return this.addOption(configure(new CreateSubcommand()));
136
- }
137
- /** Adds a subcommand group. */
138
- public addSubcommandGroup(
139
- configure: (option: CreateSubcommandGroup) => CreateSubcommandGroup,
140
- ): this {
141
- return this.addOption(configure(new CreateSubcommandGroup()));
142
- }
143
- /** Serializes the command payload. */
144
- public toJSON(): Record<string, unknown> {
145
- return structuredClone(this.#data);
146
- }
147
- /** Adds a validated top-level command option. */
148
- protected addOption(option: CreateCommandOption): this {
149
- this.#data.options = appendOption(
150
- this.#data.options,
151
- option.toJSON(),
152
- "application command",
153
- );
154
- return this;
155
- }
156
- }
157
-
158
- /** Base builder for command options. */
159
- export class CreateCommandOption {
160
- protected readonly data: Record<string, unknown>;
161
- /** Creates an option builder. */
162
- public constructor(type: number) {
163
- this.data = { type };
164
- }
165
- /** Sets the option name. */
166
- public setName(name: string): this {
167
- validateName(name, "Option name");
168
- this.data.name = name;
169
- return this;
170
- }
171
- /** Sets the option description. */
172
- public setDescription(description: string): this {
173
- validateText(description, "Option description", 100);
174
- this.data.description = description;
175
- return this;
176
- }
177
- /** Makes the option required or optional. */
178
- public setRequired(required = true): this {
179
- this.data.required = required;
180
- return this;
181
- }
182
- /** Sets autocomplete. */
183
- public setAutocomplete(enabled = true): this {
184
- this.data.autocomplete = enabled;
185
- return this;
186
- }
187
- /** Serializes the option payload. */
188
- public toJSON(): Record<string, unknown> {
189
- return structuredClone(this.data);
190
- }
191
- }
192
-
193
- /** Builds a string command option. */
194
- export class CreateStringOption extends CreateCommandOption {
195
- /** Creates a string option. */
196
- public constructor() {
197
- super(ApplicationCommandOptionEnum.String);
198
- }
199
- /** Adds string choices. */
200
- public addChoices(
201
- ...choices: Array<{ name: string; value: string }>
202
- ): this {
203
- for (const choice of choices)
204
- validateText(choice.value, "Choice value", 100);
205
- appendChoices(this.data, choices);
206
- return this;
207
- }
208
- /** Sets the minimum string length. */
209
- public setMinLength(value: number): this {
210
- validateIntegerRange(value, 0, 6000, "min_length");
211
- this.data.min_length = value;
212
- return this;
213
- }
214
- /** Sets the maximum string length. */
215
- public setMaxLength(value: number): this {
216
- validateIntegerRange(value, 1, 6000, "max_length");
217
- if (
218
- (this.data.min_length as number | undefined) !== undefined &&
219
- (this.data.min_length as number) > value
220
- )
221
- throw new RangeError("min_length cannot exceed max_length.");
222
- this.data.max_length = value;
223
- return this;
224
- }
225
- }
226
- /** Shared bounds, choices and autocomplete rules of integer and number options. */
227
- export class CreateNumericOption extends CreateCommandOption {
228
- readonly #integer: boolean;
229
- /** Creates a numeric option; `integer` rejects fractional bounds. */
230
- public constructor(type: number, integer: boolean) {
231
- super(type);
232
- this.#integer = integer;
233
- }
234
- /** Sets the minimum value. */
235
- public setMinValue(value: number): this {
236
- validateNumberRange(
237
- value,
238
- -MAX_SAFE,
239
- MAX_SAFE,
240
- "min_value",
241
- this.#integer,
242
- );
243
- this.data.min_value = value;
244
- return this;
245
- }
246
- /** Sets the maximum value; it cannot be below the minimum. */
247
- public setMaxValue(value: number): this {
248
- validateNumberRange(
249
- value,
250
- -MAX_SAFE,
251
- MAX_SAFE,
252
- "max_value",
253
- this.#integer,
254
- );
255
- const min = this.data.min_value as number | undefined;
256
- if (min !== undefined && min > value)
257
- throw new RangeError("min_value cannot exceed max_value.");
258
- this.data.max_value = value;
259
- return this;
260
- }
261
- /** Enables autocomplete; options with choices cannot use it. */
262
- public override setAutocomplete(enabled = true): this {
263
- if (
264
- enabled &&
265
- Array.isArray(this.data.choices) &&
266
- this.data.choices.length
267
- )
268
- throw new RangeError("Autocomplete options cannot define choices.");
269
- this.data.autocomplete = enabled;
270
- return this;
271
- }
272
- /** Adds fixed choices. */
273
- public addChoices(...choices: { name: string; value: number }[]): this {
274
- appendChoices(this.data, choices);
275
- return this;
276
- }
277
- }
278
- /** Builds an integer command option. */
279
- export class CreateIntegerOption extends CreateNumericOption {
280
- public constructor() {
281
- super(ApplicationCommandOptionEnum.Integer, true);
282
- }
283
- }
284
- /** Builds a number command option. */
285
- export class CreateNumberOption extends CreateNumericOption {
286
- public constructor() {
287
- super(ApplicationCommandOptionEnum.Number, false);
288
- }
289
- }
290
- /** Builds a boolean command option. */
291
- export class CreateBooleanOption extends CreateCommandOption {
292
- /** Creates a boolean option. */
293
- public constructor() {
294
- super(ApplicationCommandOptionEnum.Boolean);
295
- }
296
- }
297
- /** Builds a user command option. */
298
- export class CreateUserOption extends CreateCommandOption {
299
- /** Creates a user option. */
300
- public constructor() {
301
- super(ApplicationCommandOptionEnum.User);
302
- }
303
- }
304
- /** Builds a role command option. */
305
- export class CreateRoleOption extends CreateCommandOption {
306
- /** Creates a role option. */
307
- public constructor() {
308
- super(ApplicationCommandOptionEnum.Role);
309
- }
310
- }
311
- /** Builds a mentionable command option. */
312
- export class CreateMentionableOption extends CreateCommandOption {
313
- /** Creates a mentionable option. */
314
- public constructor() {
315
- super(ApplicationCommandOptionEnum.Mentionable);
316
- }
317
- }
318
- /** Builds an attachment command option. */
319
- export class CreateAttachmentOption extends CreateCommandOption {
320
- /** Creates an attachment option. */
321
- public constructor() {
322
- super(ApplicationCommandOptionEnum.Attachment);
323
- }
324
- }
325
- /** Builds a channel command option. */
326
- export class CreateChannelOption extends CreateCommandOption {
327
- /** Creates a channel option. */
328
- public constructor() {
329
- super(ApplicationCommandOptionEnum.Channel);
330
- }
331
- /** Restricts accepted channel types. */
332
- public addChannelTypes(...types: number[]): this {
333
- if (types.some((type) => !Number.isInteger(type) || type < 0))
334
- throw new RangeError(
335
- "Channel types must be non-negative integers.",
336
- );
337
- this.data.channel_types = [...new Set(types)];
338
- return this;
339
- }
340
- }
341
- /** Builds a nested subcommand. */
342
- export class CreateSubcommand extends CreateCommandOption {
343
- /** Creates a subcommand. */
344
- public constructor() {
345
- super(ApplicationCommandOptionEnum.Subcommand);
346
- }
347
- /** Adds a string option to this subcommand. */
348
- public addStringOption(
349
- configure: (option: CreateStringOption) => CreateStringOption,
350
- ): this {
351
- return this.addChildOption(configure(new CreateStringOption()));
352
- }
353
- /** Adds an integer option to this subcommand. */
354
- public addIntegerOption(
355
- configure: (option: CreateIntegerOption) => CreateIntegerOption,
356
- ): this {
357
- return this.addChildOption(configure(new CreateIntegerOption()));
358
- }
359
- /** Adds a number option to this subcommand. */
360
- public addNumberOption(
361
- configure: (option: CreateNumberOption) => CreateNumberOption,
362
- ): this {
363
- return this.addChildOption(configure(new CreateNumberOption()));
364
- }
365
- /** Adds a boolean option to this subcommand. */
366
- public addBooleanOption(
367
- configure: (option: CreateBooleanOption) => CreateBooleanOption,
368
- ): this {
369
- return this.addChildOption(configure(new CreateBooleanOption()));
370
- }
371
- /** Adds a user option to this subcommand. */
372
- public addUserOption(
373
- configure: (option: CreateUserOption) => CreateUserOption,
374
- ): this {
375
- return this.addChildOption(configure(new CreateUserOption()));
376
- }
377
- /** Adds a channel option to this subcommand. */
378
- public addChannelOption(
379
- configure: (option: CreateChannelOption) => CreateChannelOption,
380
- ): this {
381
- return this.addChildOption(configure(new CreateChannelOption()));
382
- }
383
- /** Adds a role option to this subcommand. */
384
- public addRoleOption(
385
- configure: (option: CreateRoleOption) => CreateRoleOption,
386
- ): this {
387
- return this.addChildOption(configure(new CreateRoleOption()));
388
- }
389
- /** Adds a mentionable option to this subcommand. */
390
- public addMentionableOption(
391
- configure: (option: CreateMentionableOption) => CreateMentionableOption,
392
- ): this {
393
- return this.addChildOption(configure(new CreateMentionableOption()));
394
- }
395
- /** Adds an attachment option to this subcommand. */
396
- public addAttachmentOption(
397
- configure: (option: CreateAttachmentOption) => CreateAttachmentOption,
398
- ): this {
399
- return this.addChildOption(configure(new CreateAttachmentOption()));
400
- }
401
- protected addChildOption(option: CreateCommandOption): this {
402
- const options =
403
- (this.data.options as OptionPayload[] | undefined) ?? [];
404
- if (
405
- options.some(
406
- (existing) =>
407
- existing.type === ApplicationCommandOptionEnum.Subcommand,
408
- )
409
- )
410
- throw new RangeError(
411
- "Subcommands cannot contain nested subcommands.",
412
- );
413
- this.data.options = appendOption(
414
- options,
415
- option.toJSON(),
416
- "subcommand",
417
- );
418
- return this;
419
- }
420
- }
421
- /** Builds a nested subcommand group. */
422
- export class CreateSubcommandGroup extends CreateCommandOption {
423
- /** Creates a subcommand group. */
424
- public constructor() {
425
- super(ApplicationCommandOptionEnum.SubcommandGroup);
426
- }
427
- /** Adds a subcommand to this group. */
428
- public addSubcommand(
429
- configure: (option: CreateSubcommand) => CreateSubcommand,
430
- ): this {
431
- const options = (this.data.options as unknown[] | undefined) ?? [];
432
- if (options.length >= 25)
433
- throw new RangeError(
434
- "A subcommand group cannot contain more than 25 subcommands.",
435
- );
436
- const payload = configure(new CreateSubcommand()).toJSON();
437
- if (
438
- options.some(
439
- (existing) =>
440
- (existing as Record<string, unknown>).name === payload.name,
441
- )
442
- )
443
- throw new RangeError(
444
- `Duplicate subcommand name: ${String(payload.name)}.`,
445
- );
446
- if (payload.type !== ApplicationCommandOptionEnum.Subcommand)
447
- throw new TypeError(
448
- "Subcommand group children must be subcommands.",
449
- );
450
- options.push(payload);
451
- this.data.options = options;
452
- return this;
453
- }
454
- }
455
- const MAX_SAFE = Number.MAX_SAFE_INTEGER;
456
-
457
- /** A serialized option as stored on its parent. */
458
- type OptionPayload = Record<string, unknown>;
459
-
460
- /**
461
- * Returns `options` with `payload` appended, enforcing Discord's limit of 25,
462
- * unique names, and required options before optional ones.
463
- */
464
- function appendOption(
465
- options: unknown,
466
- payload: OptionPayload,
467
- parent: "application command" | "subcommand",
468
- ): OptionPayload[] {
469
- const list = (options as OptionPayload[] | undefined) ?? [];
470
- if (list.length >= 25)
471
- throw new RangeError(
472
- `${parent === "subcommand" ? "A subcommand" : "An application command"} cannot contain more than 25 options.`,
473
- );
474
- if (list.some((existing) => existing.name === payload.name))
475
- throw new RangeError(`Duplicate option name: ${String(payload.name)}.`);
476
- if (
477
- payload.required === true &&
478
- list.some((existing) => existing.required !== true)
479
- )
480
- throw new RangeError(
481
- `Required ${parent} options must be placed before optional options.`,
482
- );
483
- return [...list, payload];
484
- }
485
-
486
- /** Appends choices to an option, enforcing the shared choice rules. */
487
- function appendChoices(
488
- data: OptionPayload,
489
- choices: { name: string; value: string | number }[],
490
- ): void {
491
- const current = (data.choices as unknown[] | undefined) ?? [];
492
- if (!choices.length)
493
- throw new TypeError("At least one choice is required.");
494
- if (data.autocomplete === true)
495
- throw new RangeError("Autocomplete options cannot define choices.");
496
- if (current.length + choices.length > 25)
497
- throw new RangeError("An option cannot contain more than 25 choices.");
498
- for (const choice of choices) validateText(choice.name, "Choice name", 100);
499
- data.choices = [...current, ...choices.map((choice) => ({ ...choice }))];
500
- }
501
-
502
- function validateName(value: string, field: string): void {
503
- if (!/^[\p{L}\p{N}_-]{1,32}$/u.test(value) || value !== value.toLowerCase())
504
- throw new RangeError(
505
- `${field} must contain 1-32 lowercase letters, numbers, underscores, or hyphens.`,
506
- );
507
- }
508
- function validateText(value: string, field: string, max: number): void {
509
- if (typeof value !== "string" || !value.trim() || value.length > max)
510
- throw new RangeError(`${field} must contain 1-${max} characters.`);
511
- }
512
- function validateIntegerRange(
513
- value: number,
514
- minimum: number,
515
- maximum: number,
516
- field: string,
517
- ): void {
518
- if (!Number.isInteger(value) || value < minimum || value > maximum)
519
- throw new RangeError(
520
- `${field} must be an integer between ${minimum} and ${maximum}.`,
521
- );
522
- }
523
- function validateNumberRange(
524
- value: number,
525
- minimum: number,
526
- maximum: number,
527
- field: string,
528
- integerOnly = true,
529
- ): void {
530
- if (
531
- !Number.isFinite(value) ||
532
- (integerOnly && !Number.isInteger(value)) ||
533
- value < minimum ||
534
- value > maximum
535
- )
536
- throw new RangeError(
537
- `${field} must be ${integerOnly ? "an integer" : "a number"} between ${minimum} and ${maximum}.`,
538
- );
539
- }
540
-
541
- // ─── Context Menu Builders ────────────────────────────────────────────────────
542
-
543
- /** Builder for application commands that appear in right-click context menus.
544
- * Discord.js-familiar: `new CreateContextMenuCommand().setName("x").setType(2)`. */
545
- export class CreateContextMenuCommand {
546
- protected readonly data: Record<string, unknown>;
547
- public constructor(type: 2 | 3 = 2) {
548
- this.data = { type };
549
- }
550
- /** Sets the command type: 2 = USER, 3 = MESSAGE. @throws {RangeError} For any other type. */
551
- public setType(type: 2 | 3): this {
552
- if (type !== 2 && type !== 3)
553
- throw new RangeError("Context menu command type must be 2 or 3.");
554
- this.data.type = type;
555
- return this;
556
- }
557
- /** Sets the command name (shown in the right-click menu).
558
- * Unlike CHAT_INPUT commands, USER (type 2) and MESSAGE (type 3) context-menu
559
- * command names may contain uppercase letters and spaces, so no lowercase/charset
560
- * validation is applied here — only Discord's 1-32 length limit and a non-empty
561
- * (non-whitespace) requirement are enforced.
562
- * @param name Display name; 1-32 characters, mixed case and spaces allowed.
563
- * @throws {RangeError} If the name is empty/whitespace-only or exceeds 32 characters.
564
- */
565
- public setName(name: string): this {
566
- if (
567
- typeof name !== "string" ||
568
- name.trim().length < 1 ||
569
- name.length > 32
570
- )
571
- throw new RangeError("Command name must contain 1-32 characters.");
572
- this.data.name = name;
573
- return this;
574
- }
575
- /** Sets default member permissions required to see this command. */
576
- public setDefaultMemberPermissions(
577
- permissions: bigint | number | string | null,
578
- ): this {
579
- this.data.default_member_permissions =
580
- permissions === null ? null : BigInt(permissions).toString();
581
- return this;
582
- }
583
- /** @deprecated Discord deprecated `dm_permission`. Use {@link CreateContextMenuCommand.setContexts}: `setContexts(0)` for guilds only, `setContexts(0, 1)` to allow the bot's DMs. Removed in 0.3.0. */
584
- public setDMPermission(enabled: boolean): this {
585
- this.data.dm_permission = enabled;
586
- return this;
587
- }
588
- /** Sets command integration types (0 guild install, 1 user install). */
589
- public setIntegrationTypes(...types: number[]): this {
590
- this.data.integration_types = [...types];
591
- return this;
592
- }
593
- /** Sets where the command can be used: 0 guilds, 1 the bot's DMs, 2 other DMs. @throws {RangeError} For any other value. */
594
- public setContexts(...contexts: number[]): this {
595
- validateContexts(contexts);
596
- this.data.contexts = [...contexts];
597
- return this;
598
- }
599
- /** Sets the name shown per locale, e.g. `{ "es-ES": "Ver perfil" }`; null clears it. */
600
- public setNameLocalizations(
601
- localizations: Record<string, string> | null,
602
- ): this {
603
- this.data.name_localizations =
604
- localizations === null ? null : { ...localizations };
605
- return this;
606
- }
607
- /** Serializes the command payload for the Discord API. */
608
- public toJSON(): Record<string, unknown> {
609
- return structuredClone(this.data);
610
- }
611
- }
612
-
613
- /** Builds a User context menu command (appears when right-clicking a user, type 2).
614
- * @example
615
- * new CreateUserCommand().setName("View Profile").toJSON()
616
- */
617
- export class CreateUserCommand extends CreateContextMenuCommand {
618
- public constructor() {
619
- super(2);
620
- }
621
- }
622
-
623
- /** Builds a Message context menu command (appears when right-clicking a message, type 3).
624
- * @example
625
- * new CreateMessageCommand().setName("Translate Message").toJSON()
626
- */
627
- export class CreateMessageCommand extends CreateContextMenuCommand {
628
- public constructor() {
629
- super(3);
630
- }
631
- }
632
-
633
- // ─── Deprecated names (0.2.2), removed in 0.3.0 ────────────────────────────────
634
- // Builders are now `CreateX` (`ButtonBuilder` → `CreateButton`); `…Style` became
635
- // `…Type` and `…Type` became `…Enum`.
636
-
637
- /** @deprecated Use {@link CreateAttachmentOption}. Removed in 0.3.0. */
638
- export const AttachmentOptionBuilder = CreateAttachmentOption;
639
- /** @deprecated Use {@link CreateAttachmentOption}. Removed in 0.3.0. */
640
- export type AttachmentOptionBuilder = CreateAttachmentOption;
641
-
642
- /** @deprecated Use {@link CreateBooleanOption}. Removed in 0.3.0. */
643
- export const BooleanOptionBuilder = CreateBooleanOption;
644
- /** @deprecated Use {@link CreateBooleanOption}. Removed in 0.3.0. */
645
- export type BooleanOptionBuilder = CreateBooleanOption;
646
-
647
- /** @deprecated Use {@link CreateChannelOption}. Removed in 0.3.0. */
648
- export const ChannelOptionBuilder = CreateChannelOption;
649
- /** @deprecated Use {@link CreateChannelOption}. Removed in 0.3.0. */
650
- export type ChannelOptionBuilder = CreateChannelOption;
651
-
652
- /** @deprecated Use {@link CreateCommandOption}. Removed in 0.3.0. */
653
- export const CommandOptionBuilder = CreateCommandOption;
654
- /** @deprecated Use {@link CreateCommandOption}. Removed in 0.3.0. */
655
- export type CommandOptionBuilder = CreateCommandOption;
656
-
657
- /** @deprecated Use {@link CreateContextMenuCommand}. Removed in 0.3.0. */
658
- export const ContextMenuCommandBuilder = CreateContextMenuCommand;
659
- /** @deprecated Use {@link CreateContextMenuCommand}. Removed in 0.3.0. */
660
- export type ContextMenuCommandBuilder = CreateContextMenuCommand;
661
-
662
- /** @deprecated Use {@link CreateIntegerOption}. Removed in 0.3.0. */
663
- export const IntegerOptionBuilder = CreateIntegerOption;
664
- /** @deprecated Use {@link CreateIntegerOption}. Removed in 0.3.0. */
665
- export type IntegerOptionBuilder = CreateIntegerOption;
666
-
667
- /** @deprecated Use {@link CreateMentionableOption}. Removed in 0.3.0. */
668
- export const MentionableOptionBuilder = CreateMentionableOption;
669
- /** @deprecated Use {@link CreateMentionableOption}. Removed in 0.3.0. */
670
- export type MentionableOptionBuilder = CreateMentionableOption;
671
-
672
- /** @deprecated Use {@link CreateMessageCommand}. Removed in 0.3.0. */
673
- export const MessageCommandBuilder = CreateMessageCommand;
674
- /** @deprecated Use {@link CreateMessageCommand}. Removed in 0.3.0. */
675
- export type MessageCommandBuilder = CreateMessageCommand;
676
-
677
- /** @deprecated Use {@link CreateNumberOption}. Removed in 0.3.0. */
678
- export const NumberOptionBuilder = CreateNumberOption;
679
- /** @deprecated Use {@link CreateNumberOption}. Removed in 0.3.0. */
680
- export type NumberOptionBuilder = CreateNumberOption;
681
-
682
- /** @deprecated Use {@link CreateRoleOption}. Removed in 0.3.0. */
683
- export const RoleOptionBuilder = CreateRoleOption;
684
- /** @deprecated Use {@link CreateRoleOption}. Removed in 0.3.0. */
685
- export type RoleOptionBuilder = CreateRoleOption;
686
-
687
- /** @deprecated Use {@link CreateSlashCommand}. Removed in 0.3.0. */
688
- export const SlashCommandBuilder = CreateSlashCommand;
689
- /** @deprecated Use {@link CreateSlashCommand}. Removed in 0.3.0. */
690
- export type SlashCommandBuilder = CreateSlashCommand;
691
-
692
- /** @deprecated Use {@link CreateStringOption}. Removed in 0.3.0. */
693
- export const StringOptionBuilder = CreateStringOption;
694
- /** @deprecated Use {@link CreateStringOption}. Removed in 0.3.0. */
695
- export type StringOptionBuilder = CreateStringOption;
696
-
697
- /** @deprecated Use {@link CreateSubcommand}. Removed in 0.3.0. */
698
- export const SubcommandBuilder = CreateSubcommand;
699
- /** @deprecated Use {@link CreateSubcommand}. Removed in 0.3.0. */
700
- export type SubcommandBuilder = CreateSubcommand;
701
-
702
- /** @deprecated Use {@link CreateSubcommandGroup}. Removed in 0.3.0. */
703
- export const SubcommandGroupBuilder = CreateSubcommandGroup;
704
- /** @deprecated Use {@link CreateSubcommandGroup}. Removed in 0.3.0. */
705
- export type SubcommandGroupBuilder = CreateSubcommandGroup;
706
-
707
- /** @deprecated Use {@link CreateUserCommand}. Removed in 0.3.0. */
708
- export const UserCommandBuilder = CreateUserCommand;
709
- /** @deprecated Use {@link CreateUserCommand}. Removed in 0.3.0. */
710
- export type UserCommandBuilder = CreateUserCommand;
711
-
712
- /** @deprecated Use {@link CreateUserOption}. Removed in 0.3.0. */
713
- export const UserOptionBuilder = CreateUserOption;
714
- /** @deprecated Use {@link CreateUserOption}. Removed in 0.3.0. */
715
- export type UserOptionBuilder = CreateUserOption;
716
-
717
- /** @deprecated Use {@link ApplicationCommandOptionEnum}. Removed in 0.3.0. */
718
- export const ApplicationCommandOptionType = ApplicationCommandOptionEnum;
719
- /** @deprecated Use {@link ApplicationCommandOptionEnum}. Removed in 0.3.0. */
720
- export type ApplicationCommandOptionType = ApplicationCommandOptionEnum;
1
+ /** Discord application command option types. */
2
+ export const ApplicationCommandOptionEnum = {
3
+ Subcommand: 1,
4
+ SubcommandGroup: 2,
5
+ String: 3,
6
+ Integer: 4,
7
+ Boolean: 5,
8
+ User: 6,
9
+ Channel: 7,
10
+ Role: 8,
11
+ Mentionable: 9,
12
+ Number: 10,
13
+ Attachment: 11,
14
+ } as const;
15
+ /** One {@link ApplicationCommandOptionEnum} value, usable as a type. */
16
+ export type ApplicationCommandOptionEnum =
17
+ (typeof ApplicationCommandOptionEnum)[keyof typeof ApplicationCommandOptionEnum];
18
+
19
+ /** Throws unless every context is 0 (guilds), 1 (the bot's DMs) or 2 (other DMs). */
20
+ function validateContexts(contexts: number[]): void {
21
+ if (
22
+ contexts.some(
23
+ (context) => context !== 0 && context !== 1 && context !== 2,
24
+ )
25
+ )
26
+ throw new RangeError("Command contexts must be 0, 1 or 2.");
27
+ }
28
+
29
+ /** Builds Discord application command payloads. */
30
+ export class CreateSlashCommand {
31
+ readonly #data: Record<string, unknown> = { type: 1 };
32
+ /** Sets the command name. */
33
+ public setName(name: string): this {
34
+ validateName(name, "Command name");
35
+ this.#data.name = name;
36
+ return this;
37
+ }
38
+ /** Sets the command description. */
39
+ public setDescription(description: string): this {
40
+ validateText(description, "Command description", 100);
41
+ this.#data.description = description;
42
+ return this;
43
+ }
44
+ /** Sets where the command can be used: 0 guilds, 1 the bot's DMs, 2 other DMs. @throws {RangeError} For any other value. */
45
+ public setContexts(...contexts: number[]): this {
46
+ validateContexts(contexts);
47
+ this.#data.contexts = [...contexts];
48
+ return this;
49
+ }
50
+ /** Sets whether the command is NSFW. */
51
+ public setNSFW(nsfw = true): this {
52
+ this.#data.nsfw = nsfw;
53
+ return this;
54
+ }
55
+ /** Sets whether the command is NSFW. */
56
+ public setNsfw(nsfw = true): this {
57
+ return this.setNSFW(nsfw);
58
+ }
59
+ /** Sets command default member permissions. */
60
+ public setDefaultMemberPermissions(
61
+ permissions: bigint | number | string | null,
62
+ ): this {
63
+ this.#data.default_member_permissions =
64
+ permissions === null ? null : BigInt(permissions).toString();
65
+ return this;
66
+ }
67
+ /** Sets command integration types. */
68
+ public setIntegrationTypes(...types: number[]): this {
69
+ this.#data.integration_types = [...types];
70
+ return this;
71
+ }
72
+ /** Adds a string option. */
73
+ public addStringOption(
74
+ configure: (option: CreateStringOption) => CreateStringOption,
75
+ ): this {
76
+ return this.addOption(configure(new CreateStringOption()));
77
+ }
78
+ /** Adds an integer option. */
79
+ public addIntegerOption(
80
+ configure: (option: CreateIntegerOption) => CreateIntegerOption,
81
+ ): this {
82
+ return this.addOption(configure(new CreateIntegerOption()));
83
+ }
84
+ /** Adds a number option. */
85
+ public addNumberOption(
86
+ configure: (option: CreateNumberOption) => CreateNumberOption,
87
+ ): this {
88
+ return this.addOption(configure(new CreateNumberOption()));
89
+ }
90
+ /** Adds a boolean option. */
91
+ public addBooleanOption(
92
+ configure: (option: CreateBooleanOption) => CreateBooleanOption,
93
+ ): this {
94
+ return this.addOption(configure(new CreateBooleanOption()));
95
+ }
96
+ /** Adds a user option. */
97
+ public addUserOption(
98
+ configure: (option: CreateUserOption) => CreateUserOption,
99
+ ): this {
100
+ return this.addOption(configure(new CreateUserOption()));
101
+ }
102
+ /** Adds a channel option. */
103
+ public addChannelOption(
104
+ configure: (option: CreateChannelOption) => CreateChannelOption,
105
+ ): this {
106
+ return this.addOption(configure(new CreateChannelOption()));
107
+ }
108
+ /** Adds a role option. */
109
+ public addRoleOption(
110
+ configure: (option: CreateRoleOption) => CreateRoleOption,
111
+ ): this {
112
+ return this.addOption(configure(new CreateRoleOption()));
113
+ }
114
+ /** Adds a mentionable option. */
115
+ public addMentionableOption(
116
+ configure: (option: CreateMentionableOption) => CreateMentionableOption,
117
+ ): this {
118
+ return this.addOption(configure(new CreateMentionableOption()));
119
+ }
120
+ /** Adds an attachment option. */
121
+ public addAttachmentOption(
122
+ configure: (option: CreateAttachmentOption) => CreateAttachmentOption,
123
+ ): this {
124
+ return this.addOption(configure(new CreateAttachmentOption()));
125
+ }
126
+ /** Adds a subcommand. */
127
+ public addSubcommand(
128
+ configure: (option: CreateSubcommand) => CreateSubcommand,
129
+ ): this {
130
+ return this.addOption(configure(new CreateSubcommand()));
131
+ }
132
+ /** Adds a subcommand group. */
133
+ public addSubcommandGroup(
134
+ configure: (option: CreateSubcommandGroup) => CreateSubcommandGroup,
135
+ ): this {
136
+ return this.addOption(configure(new CreateSubcommandGroup()));
137
+ }
138
+ /** Serializes the command payload. */
139
+ public toJSON(): Record<string, unknown> {
140
+ return structuredClone(this.#data);
141
+ }
142
+ /** Adds a validated top-level command option. */
143
+ protected addOption(option: CreateCommandOption): this {
144
+ this.#data.options = appendOption(
145
+ this.#data.options,
146
+ option.toJSON(),
147
+ "application command",
148
+ );
149
+ return this;
150
+ }
151
+ }
152
+
153
+ /** Base builder for command options. */
154
+ export class CreateCommandOption {
155
+ protected readonly data: Record<string, unknown>;
156
+ /** Creates an option builder. */
157
+ public constructor(type: number) {
158
+ this.data = { type };
159
+ }
160
+ /** Sets the option name. */
161
+ public setName(name: string): this {
162
+ validateName(name, "Option name");
163
+ this.data.name = name;
164
+ return this;
165
+ }
166
+ /** Sets the option description. */
167
+ public setDescription(description: string): this {
168
+ validateText(description, "Option description", 100);
169
+ this.data.description = description;
170
+ return this;
171
+ }
172
+ /** Makes the option required or optional. */
173
+ public setRequired(required = true): this {
174
+ this.data.required = required;
175
+ return this;
176
+ }
177
+ /** Sets autocomplete. */
178
+ public setAutocomplete(enabled = true): this {
179
+ this.data.autocomplete = enabled;
180
+ return this;
181
+ }
182
+ /** Serializes the option payload. */
183
+ public toJSON(): Record<string, unknown> {
184
+ return structuredClone(this.data);
185
+ }
186
+ }
187
+
188
+ /** Builds a string command option. */
189
+ export class CreateStringOption extends CreateCommandOption {
190
+ /** Creates a string option. */
191
+ public constructor() {
192
+ super(ApplicationCommandOptionEnum.String);
193
+ }
194
+ /** Adds string choices. */
195
+ public addChoices(
196
+ ...choices: Array<{ name: string; value: string }>
197
+ ): this {
198
+ for (const choice of choices)
199
+ validateText(choice.value, "Choice value", 100);
200
+ appendChoices(this.data, choices);
201
+ return this;
202
+ }
203
+ /** Sets the minimum string length. */
204
+ public setMinLength(value: number): this {
205
+ validateIntegerRange(value, 0, 6000, "min_length");
206
+ this.data.min_length = value;
207
+ return this;
208
+ }
209
+ /** Sets the maximum string length. */
210
+ public setMaxLength(value: number): this {
211
+ validateIntegerRange(value, 1, 6000, "max_length");
212
+ if (
213
+ (this.data.min_length as number | undefined) !== undefined &&
214
+ (this.data.min_length as number) > value
215
+ )
216
+ throw new RangeError("min_length cannot exceed max_length.");
217
+ this.data.max_length = value;
218
+ return this;
219
+ }
220
+ }
221
+ /** Shared bounds, choices and autocomplete rules of integer and number options. */
222
+ export class CreateNumericOption extends CreateCommandOption {
223
+ readonly #integer: boolean;
224
+ /** Creates a numeric option; `integer` rejects fractional bounds. */
225
+ public constructor(type: number, integer: boolean) {
226
+ super(type);
227
+ this.#integer = integer;
228
+ }
229
+ /** Sets the minimum value. */
230
+ public setMinValue(value: number): this {
231
+ validateNumberRange(
232
+ value,
233
+ -MAX_SAFE,
234
+ MAX_SAFE,
235
+ "min_value",
236
+ this.#integer,
237
+ );
238
+ this.data.min_value = value;
239
+ return this;
240
+ }
241
+ /** Sets the maximum value; it cannot be below the minimum. */
242
+ public setMaxValue(value: number): this {
243
+ validateNumberRange(
244
+ value,
245
+ -MAX_SAFE,
246
+ MAX_SAFE,
247
+ "max_value",
248
+ this.#integer,
249
+ );
250
+ const min = this.data.min_value as number | undefined;
251
+ if (min !== undefined && min > value)
252
+ throw new RangeError("min_value cannot exceed max_value.");
253
+ this.data.max_value = value;
254
+ return this;
255
+ }
256
+ /** Enables autocomplete; options with choices cannot use it. */
257
+ public override setAutocomplete(enabled = true): this {
258
+ if (
259
+ enabled &&
260
+ Array.isArray(this.data.choices) &&
261
+ this.data.choices.length
262
+ )
263
+ throw new RangeError("Autocomplete options cannot define choices.");
264
+ this.data.autocomplete = enabled;
265
+ return this;
266
+ }
267
+ /** Adds fixed choices. */
268
+ public addChoices(...choices: { name: string; value: number }[]): this {
269
+ appendChoices(this.data, choices);
270
+ return this;
271
+ }
272
+ }
273
+ /** Builds an integer command option. */
274
+ export class CreateIntegerOption extends CreateNumericOption {
275
+ public constructor() {
276
+ super(ApplicationCommandOptionEnum.Integer, true);
277
+ }
278
+ }
279
+ /** Builds a number command option. */
280
+ export class CreateNumberOption extends CreateNumericOption {
281
+ public constructor() {
282
+ super(ApplicationCommandOptionEnum.Number, false);
283
+ }
284
+ }
285
+ /** Builds a boolean command option. */
286
+ export class CreateBooleanOption extends CreateCommandOption {
287
+ /** Creates a boolean option. */
288
+ public constructor() {
289
+ super(ApplicationCommandOptionEnum.Boolean);
290
+ }
291
+ }
292
+ /** Builds a user command option. */
293
+ export class CreateUserOption extends CreateCommandOption {
294
+ /** Creates a user option. */
295
+ public constructor() {
296
+ super(ApplicationCommandOptionEnum.User);
297
+ }
298
+ }
299
+ /** Builds a role command option. */
300
+ export class CreateRoleOption extends CreateCommandOption {
301
+ /** Creates a role option. */
302
+ public constructor() {
303
+ super(ApplicationCommandOptionEnum.Role);
304
+ }
305
+ }
306
+ /** Builds a mentionable command option. */
307
+ export class CreateMentionableOption extends CreateCommandOption {
308
+ /** Creates a mentionable option. */
309
+ public constructor() {
310
+ super(ApplicationCommandOptionEnum.Mentionable);
311
+ }
312
+ }
313
+ /** Builds an attachment command option. */
314
+ export class CreateAttachmentOption extends CreateCommandOption {
315
+ /** Creates an attachment option. */
316
+ public constructor() {
317
+ super(ApplicationCommandOptionEnum.Attachment);
318
+ }
319
+ }
320
+ /** Builds a channel command option. */
321
+ export class CreateChannelOption extends CreateCommandOption {
322
+ /** Creates a channel option. */
323
+ public constructor() {
324
+ super(ApplicationCommandOptionEnum.Channel);
325
+ }
326
+ /** Restricts accepted channel types. */
327
+ public addChannelTypes(...types: number[]): this {
328
+ if (types.some((type) => !Number.isInteger(type) || type < 0))
329
+ throw new RangeError(
330
+ "Channel types must be non-negative integers.",
331
+ );
332
+ this.data.channel_types = [...new Set(types)];
333
+ return this;
334
+ }
335
+ }
336
+ /** Builds a nested subcommand. */
337
+ export class CreateSubcommand extends CreateCommandOption {
338
+ /** Creates a subcommand. */
339
+ public constructor() {
340
+ super(ApplicationCommandOptionEnum.Subcommand);
341
+ }
342
+ /** Adds a string option to this subcommand. */
343
+ public addStringOption(
344
+ configure: (option: CreateStringOption) => CreateStringOption,
345
+ ): this {
346
+ return this.addChildOption(configure(new CreateStringOption()));
347
+ }
348
+ /** Adds an integer option to this subcommand. */
349
+ public addIntegerOption(
350
+ configure: (option: CreateIntegerOption) => CreateIntegerOption,
351
+ ): this {
352
+ return this.addChildOption(configure(new CreateIntegerOption()));
353
+ }
354
+ /** Adds a number option to this subcommand. */
355
+ public addNumberOption(
356
+ configure: (option: CreateNumberOption) => CreateNumberOption,
357
+ ): this {
358
+ return this.addChildOption(configure(new CreateNumberOption()));
359
+ }
360
+ /** Adds a boolean option to this subcommand. */
361
+ public addBooleanOption(
362
+ configure: (option: CreateBooleanOption) => CreateBooleanOption,
363
+ ): this {
364
+ return this.addChildOption(configure(new CreateBooleanOption()));
365
+ }
366
+ /** Adds a user option to this subcommand. */
367
+ public addUserOption(
368
+ configure: (option: CreateUserOption) => CreateUserOption,
369
+ ): this {
370
+ return this.addChildOption(configure(new CreateUserOption()));
371
+ }
372
+ /** Adds a channel option to this subcommand. */
373
+ public addChannelOption(
374
+ configure: (option: CreateChannelOption) => CreateChannelOption,
375
+ ): this {
376
+ return this.addChildOption(configure(new CreateChannelOption()));
377
+ }
378
+ /** Adds a role option to this subcommand. */
379
+ public addRoleOption(
380
+ configure: (option: CreateRoleOption) => CreateRoleOption,
381
+ ): this {
382
+ return this.addChildOption(configure(new CreateRoleOption()));
383
+ }
384
+ /** Adds a mentionable option to this subcommand. */
385
+ public addMentionableOption(
386
+ configure: (option: CreateMentionableOption) => CreateMentionableOption,
387
+ ): this {
388
+ return this.addChildOption(configure(new CreateMentionableOption()));
389
+ }
390
+ /** Adds an attachment option to this subcommand. */
391
+ public addAttachmentOption(
392
+ configure: (option: CreateAttachmentOption) => CreateAttachmentOption,
393
+ ): this {
394
+ return this.addChildOption(configure(new CreateAttachmentOption()));
395
+ }
396
+ protected addChildOption(option: CreateCommandOption): this {
397
+ const options =
398
+ (this.data.options as OptionPayload[] | undefined) ?? [];
399
+ if (
400
+ options.some(
401
+ (existing) =>
402
+ existing.type === ApplicationCommandOptionEnum.Subcommand,
403
+ )
404
+ )
405
+ throw new RangeError(
406
+ "Subcommands cannot contain nested subcommands.",
407
+ );
408
+ this.data.options = appendOption(
409
+ options,
410
+ option.toJSON(),
411
+ "subcommand",
412
+ );
413
+ return this;
414
+ }
415
+ }
416
+ /** Builds a nested subcommand group. */
417
+ export class CreateSubcommandGroup extends CreateCommandOption {
418
+ /** Creates a subcommand group. */
419
+ public constructor() {
420
+ super(ApplicationCommandOptionEnum.SubcommandGroup);
421
+ }
422
+ /** Adds a subcommand to this group. */
423
+ public addSubcommand(
424
+ configure: (option: CreateSubcommand) => CreateSubcommand,
425
+ ): this {
426
+ const options = (this.data.options as unknown[] | undefined) ?? [];
427
+ if (options.length >= 25)
428
+ throw new RangeError(
429
+ "A subcommand group cannot contain more than 25 subcommands.",
430
+ );
431
+ const payload = configure(new CreateSubcommand()).toJSON();
432
+ if (
433
+ options.some(
434
+ (existing) =>
435
+ (existing as Record<string, unknown>).name === payload.name,
436
+ )
437
+ )
438
+ throw new RangeError(
439
+ `Duplicate subcommand name: ${String(payload.name)}.`,
440
+ );
441
+ if (payload.type !== ApplicationCommandOptionEnum.Subcommand)
442
+ throw new TypeError(
443
+ "Subcommand group children must be subcommands.",
444
+ );
445
+ options.push(payload);
446
+ this.data.options = options;
447
+ return this;
448
+ }
449
+ }
450
+ const MAX_SAFE = Number.MAX_SAFE_INTEGER;
451
+
452
+ /** A serialized option as stored on its parent. */
453
+ type OptionPayload = Record<string, unknown>;
454
+
455
+ /**
456
+ * Returns `options` with `payload` appended, enforcing Discord's limit of 25,
457
+ * unique names, and required options before optional ones.
458
+ */
459
+ function appendOption(
460
+ options: unknown,
461
+ payload: OptionPayload,
462
+ parent: "application command" | "subcommand",
463
+ ): OptionPayload[] {
464
+ const list = (options as OptionPayload[] | undefined) ?? [];
465
+ if (list.length >= 25)
466
+ throw new RangeError(
467
+ `${parent === "subcommand" ? "A subcommand" : "An application command"} cannot contain more than 25 options.`,
468
+ );
469
+ if (list.some((existing) => existing.name === payload.name))
470
+ throw new RangeError(`Duplicate option name: ${String(payload.name)}.`);
471
+ if (
472
+ payload.required === true &&
473
+ list.some((existing) => existing.required !== true)
474
+ )
475
+ throw new RangeError(
476
+ `Required ${parent} options must be placed before optional options.`,
477
+ );
478
+ return [...list, payload];
479
+ }
480
+
481
+ /** Appends choices to an option, enforcing the shared choice rules. */
482
+ function appendChoices(
483
+ data: OptionPayload,
484
+ choices: { name: string; value: string | number }[],
485
+ ): void {
486
+ const current = (data.choices as unknown[] | undefined) ?? [];
487
+ if (!choices.length)
488
+ throw new TypeError("At least one choice is required.");
489
+ if (data.autocomplete === true)
490
+ throw new RangeError("Autocomplete options cannot define choices.");
491
+ if (current.length + choices.length > 25)
492
+ throw new RangeError("An option cannot contain more than 25 choices.");
493
+ for (const choice of choices) validateText(choice.name, "Choice name", 100);
494
+ data.choices = [...current, ...choices.map((choice) => ({ ...choice }))];
495
+ }
496
+
497
+ function validateName(value: string, field: string): void {
498
+ if (!/^[\p{L}\p{N}_-]{1,32}$/u.test(value) || value !== value.toLowerCase())
499
+ throw new RangeError(
500
+ `${field} must contain 1-32 lowercase letters, numbers, underscores, or hyphens.`,
501
+ );
502
+ }
503
+ function validateText(value: string, field: string, max: number): void {
504
+ if (typeof value !== "string" || !value.trim() || value.length > max)
505
+ throw new RangeError(`${field} must contain 1-${max} characters.`);
506
+ }
507
+ function validateIntegerRange(
508
+ value: number,
509
+ minimum: number,
510
+ maximum: number,
511
+ field: string,
512
+ ): void {
513
+ if (!Number.isInteger(value) || value < minimum || value > maximum)
514
+ throw new RangeError(
515
+ `${field} must be an integer between ${minimum} and ${maximum}.`,
516
+ );
517
+ }
518
+ function validateNumberRange(
519
+ value: number,
520
+ minimum: number,
521
+ maximum: number,
522
+ field: string,
523
+ integerOnly = true,
524
+ ): void {
525
+ if (
526
+ !Number.isFinite(value) ||
527
+ (integerOnly && !Number.isInteger(value)) ||
528
+ value < minimum ||
529
+ value > maximum
530
+ )
531
+ throw new RangeError(
532
+ `${field} must be ${integerOnly ? "an integer" : "a number"} between ${minimum} and ${maximum}.`,
533
+ );
534
+ }
535
+
536
+ // ─── Context Menu Builders ────────────────────────────────────────────────────
537
+
538
+ /** Builder for application commands that appear in right-click context menus.
539
+ * Discord.js-familiar: `new CreateContextMenuCommand().setName("x").setType(2)`. */
540
+ export class CreateContextMenuCommand {
541
+ protected readonly data: Record<string, unknown>;
542
+ public constructor(type: 2 | 3 = 2) {
543
+ this.data = { type };
544
+ }
545
+ /** Sets the command type: 2 = USER, 3 = MESSAGE. @throws {RangeError} For any other type. */
546
+ public setType(type: 2 | 3): this {
547
+ if (type !== 2 && type !== 3)
548
+ throw new RangeError("Context menu command type must be 2 or 3.");
549
+ this.data.type = type;
550
+ return this;
551
+ }
552
+ /** Sets the command name (shown in the right-click menu).
553
+ * Unlike CHAT_INPUT commands, USER (type 2) and MESSAGE (type 3) context-menu
554
+ * command names may contain uppercase letters and spaces, so no lowercase/charset
555
+ * validation is applied here — only Discord's 1-32 length limit and a non-empty
556
+ * (non-whitespace) requirement are enforced.
557
+ * @param name Display name; 1-32 characters, mixed case and spaces allowed.
558
+ * @throws {RangeError} If the name is empty/whitespace-only or exceeds 32 characters.
559
+ */
560
+ public setName(name: string): this {
561
+ if (
562
+ typeof name !== "string" ||
563
+ name.trim().length < 1 ||
564
+ name.length > 32
565
+ )
566
+ throw new RangeError("Command name must contain 1-32 characters.");
567
+ this.data.name = name;
568
+ return this;
569
+ }
570
+ /** Sets default member permissions required to see this command. */
571
+ public setDefaultMemberPermissions(
572
+ permissions: bigint | number | string | null,
573
+ ): this {
574
+ this.data.default_member_permissions =
575
+ permissions === null ? null : BigInt(permissions).toString();
576
+ return this;
577
+ }
578
+ /** Sets command integration types (0 guild install, 1 user install). */
579
+ public setIntegrationTypes(...types: number[]): this {
580
+ this.data.integration_types = [...types];
581
+ return this;
582
+ }
583
+ /** Sets where the command can be used: 0 guilds, 1 the bot's DMs, 2 other DMs. @throws {RangeError} For any other value. */
584
+ public setContexts(...contexts: number[]): this {
585
+ validateContexts(contexts);
586
+ this.data.contexts = [...contexts];
587
+ return this;
588
+ }
589
+ /** Sets the name shown per locale, e.g. `{ "es-ES": "Ver perfil" }`; null clears it. */
590
+ public setNameLocalizations(
591
+ localizations: Record<string, string> | null,
592
+ ): this {
593
+ this.data.name_localizations =
594
+ localizations === null ? null : { ...localizations };
595
+ return this;
596
+ }
597
+ /** Serializes the command payload for the Discord API. */
598
+ public toJSON(): Record<string, unknown> {
599
+ return structuredClone(this.data);
600
+ }
601
+ }
602
+
603
+ /** Builds a User context menu command (appears when right-clicking a user, type 2).
604
+ * @example
605
+ * new CreateUserCommand().setName("View Profile").toJSON()
606
+ */
607
+ export class CreateUserCommand extends CreateContextMenuCommand {
608
+ public constructor() {
609
+ super(2);
610
+ }
611
+ }
612
+
613
+ /** Builds a Message context menu command (appears when right-clicking a message, type 3).
614
+ * @example
615
+ * new CreateMessageCommand().setName("Translate Message").toJSON()
616
+ */
617
+ export class CreateMessageCommand extends CreateContextMenuCommand {
618
+ public constructor() {
619
+ super(3);
620
+ }
621
+ }