@lunibee/builders 0.1.6 → 0.1.8

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