@ovencord/builders 1.11.1

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.
Files changed (84) hide show
  1. package/LICENSE +191 -0
  2. package/README.md +92 -0
  3. package/package.json +70 -0
  4. package/src/Assertions.ts +15 -0
  5. package/src/components/ActionRow.ts +346 -0
  6. package/src/components/Assertions.ts +190 -0
  7. package/src/components/Component.ts +47 -0
  8. package/src/components/Components.ts +275 -0
  9. package/src/components/button/Button.ts +34 -0
  10. package/src/components/button/CustomIdButton.ts +74 -0
  11. package/src/components/button/LinkButton.ts +39 -0
  12. package/src/components/button/PremiumButton.ts +26 -0
  13. package/src/components/button/mixins/EmojiOrLabelButtonMixin.ts +52 -0
  14. package/src/components/fileUpload/Assertions.ts +12 -0
  15. package/src/components/fileUpload/FileUpload.ts +109 -0
  16. package/src/components/label/Assertions.ts +28 -0
  17. package/src/components/label/Label.ts +215 -0
  18. package/src/components/selectMenu/BaseSelectMenu.ts +89 -0
  19. package/src/components/selectMenu/ChannelSelectMenu.ts +115 -0
  20. package/src/components/selectMenu/MentionableSelectMenu.ts +126 -0
  21. package/src/components/selectMenu/RoleSelectMenu.ts +89 -0
  22. package/src/components/selectMenu/StringSelectMenu.ts +165 -0
  23. package/src/components/selectMenu/StringSelectMenuOption.ts +113 -0
  24. package/src/components/selectMenu/UserSelectMenu.ts +89 -0
  25. package/src/components/textInput/Assertions.ts +15 -0
  26. package/src/components/textInput/TextInput.ts +154 -0
  27. package/src/components/v2/Assertions.ts +82 -0
  28. package/src/components/v2/Container.ts +254 -0
  29. package/src/components/v2/File.ts +81 -0
  30. package/src/components/v2/MediaGallery.ts +128 -0
  31. package/src/components/v2/MediaGalleryItem.ts +85 -0
  32. package/src/components/v2/Section.ts +266 -0
  33. package/src/components/v2/Separator.ts +82 -0
  34. package/src/components/v2/TextDisplay.ts +63 -0
  35. package/src/components/v2/Thumbnail.ts +100 -0
  36. package/src/index.ts +109 -0
  37. package/src/interactions/commands/Command.ts +87 -0
  38. package/src/interactions/commands/SharedName.ts +68 -0
  39. package/src/interactions/commands/SharedNameAndDescription.ts +69 -0
  40. package/src/interactions/commands/chatInput/Assertions.ts +180 -0
  41. package/src/interactions/commands/chatInput/ChatInputCommand.ts +40 -0
  42. package/src/interactions/commands/chatInput/ChatInputCommandSubcommands.ts +117 -0
  43. package/src/interactions/commands/chatInput/mixins/ApplicationCommandNumericOptionMinMaxValueMixin.ts +52 -0
  44. package/src/interactions/commands/chatInput/mixins/ApplicationCommandOptionChannelTypesMixin.ts +57 -0
  45. package/src/interactions/commands/chatInput/mixins/ApplicationCommandOptionWithAutocompleteMixin.ts +32 -0
  46. package/src/interactions/commands/chatInput/mixins/ApplicationCommandOptionWithChoicesMixin.ts +43 -0
  47. package/src/interactions/commands/chatInput/mixins/SharedChatInputCommandOptions.ts +204 -0
  48. package/src/interactions/commands/chatInput/mixins/SharedSubcommands.ts +61 -0
  49. package/src/interactions/commands/chatInput/options/ApplicationCommandOptionBase.ts +66 -0
  50. package/src/interactions/commands/chatInput/options/attachment.ts +20 -0
  51. package/src/interactions/commands/chatInput/options/boolean.ts +20 -0
  52. package/src/interactions/commands/chatInput/options/channel.ts +28 -0
  53. package/src/interactions/commands/chatInput/options/integer.ts +34 -0
  54. package/src/interactions/commands/chatInput/options/mentionable.ts +20 -0
  55. package/src/interactions/commands/chatInput/options/number.ts +34 -0
  56. package/src/interactions/commands/chatInput/options/role.ts +20 -0
  57. package/src/interactions/commands/chatInput/options/string.ts +71 -0
  58. package/src/interactions/commands/chatInput/options/user.ts +20 -0
  59. package/src/interactions/commands/contextMenu/Assertions.ts +31 -0
  60. package/src/interactions/commands/contextMenu/ContextMenuCommand.ts +42 -0
  61. package/src/interactions/commands/contextMenu/MessageCommand.ts +19 -0
  62. package/src/interactions/commands/contextMenu/UserCommand.ts +19 -0
  63. package/src/interactions/modals/Assertions.ts +27 -0
  64. package/src/interactions/modals/Modal.ts +158 -0
  65. package/src/messages/AllowedMentions.ts +193 -0
  66. package/src/messages/Assertions.ts +148 -0
  67. package/src/messages/Attachment.ts +209 -0
  68. package/src/messages/Message.ts +692 -0
  69. package/src/messages/MessageReference.ts +111 -0
  70. package/src/messages/embed/Assertions.ts +53 -0
  71. package/src/messages/embed/Embed.ts +352 -0
  72. package/src/messages/embed/EmbedAuthor.ts +83 -0
  73. package/src/messages/embed/EmbedField.ts +67 -0
  74. package/src/messages/embed/EmbedFooter.ts +65 -0
  75. package/src/messages/poll/Assertions.ts +20 -0
  76. package/src/messages/poll/Poll.ts +243 -0
  77. package/src/messages/poll/PollAnswer.ts +77 -0
  78. package/src/messages/poll/PollAnswerMedia.ts +38 -0
  79. package/src/messages/poll/PollMedia.ts +41 -0
  80. package/src/messages/poll/PollQuestion.ts +20 -0
  81. package/src/util/ValidationError.ts +21 -0
  82. package/src/util/normalizeArray.ts +19 -0
  83. package/src/util/resolveBuilder.ts +40 -0
  84. package/src/util/validation.ts +58 -0
@@ -0,0 +1,346 @@
1
+ import type {
2
+ APITextInputComponent,
3
+ APIActionRowComponent,
4
+ APIComponentInActionRow,
5
+ APIChannelSelectComponent,
6
+ APIMentionableSelectComponent,
7
+ APIRoleSelectComponent,
8
+ APIStringSelectComponent,
9
+ APIUserSelectComponent,
10
+ APIButtonComponentWithCustomId,
11
+ APIButtonComponentWithSKUId,
12
+ APIButtonComponentWithURL,
13
+ } from 'discord-api-types/v10';
14
+ import { ComponentType } from 'discord-api-types/v10';
15
+ import { normalizeArray, type RestOrArray } from '../util/normalizeArray.js';
16
+ import { resolveBuilder } from '../util/resolveBuilder.js';
17
+ import { validate } from '../util/validation.js';
18
+ import { actionRowPredicate } from './Assertions.js';
19
+ import { ComponentBuilder } from './Component.js';
20
+ import type { AnyActionRowComponentBuilder } from './Components.js';
21
+ import { createComponentBuilder } from './Components.js';
22
+ import {
23
+ DangerButtonBuilder,
24
+ PrimaryButtonBuilder,
25
+ SecondaryButtonBuilder,
26
+ SuccessButtonBuilder,
27
+ } from './button/CustomIdButton.js';
28
+ import { LinkButtonBuilder } from './button/LinkButton.js';
29
+ import { PremiumButtonBuilder } from './button/PremiumButton.js';
30
+ import { ChannelSelectMenuBuilder } from './selectMenu/ChannelSelectMenu.js';
31
+ import { MentionableSelectMenuBuilder } from './selectMenu/MentionableSelectMenu.js';
32
+ import { RoleSelectMenuBuilder } from './selectMenu/RoleSelectMenu.js';
33
+ import { StringSelectMenuBuilder } from './selectMenu/StringSelectMenu.js';
34
+ import { UserSelectMenuBuilder } from './selectMenu/UserSelectMenu.js';
35
+ import { TextInputBuilder } from './textInput/TextInput.js';
36
+
37
+ export interface ActionRowBuilderData extends Partial<
38
+ Omit<APIActionRowComponent<APIComponentInActionRow>, 'components'>
39
+ > {
40
+ components: AnyActionRowComponentBuilder[];
41
+ }
42
+
43
+ /**
44
+ * A builder that creates API-compatible JSON data for action rows.
45
+ */
46
+ export class ActionRowBuilder extends ComponentBuilder<APIActionRowComponent<APIComponentInActionRow>> {
47
+ /**
48
+ * @internal
49
+ */
50
+ protected readonly data: ActionRowBuilderData;
51
+
52
+ /**
53
+ * The components within this action row.
54
+ */
55
+ public get components(): readonly AnyActionRowComponentBuilder[] {
56
+ return this.data.components;
57
+ }
58
+
59
+ /**
60
+ * Creates a new action row.
61
+ *
62
+ * @param data - The API data to create this action row with
63
+ * @example
64
+ * Creating an action row from an API data object:
65
+ * ```ts
66
+ * const actionRow = new ActionRowBuilder({
67
+ * components: [
68
+ * {
69
+ * custom_id: "custom id",
70
+ * label: "Type something",
71
+ * style: TextInputStyle.Short,
72
+ * type: ComponentType.TextInput,
73
+ * },
74
+ * ],
75
+ * });
76
+ * ```
77
+ * @example
78
+ * Creating an action row using setters and API data:
79
+ * ```ts
80
+ * const actionRow = new ActionRowBuilder({
81
+ * components: [
82
+ * {
83
+ * custom_id: "custom id",
84
+ * label: "Click me",
85
+ * style: ButtonStyle.Primary,
86
+ * type: ComponentType.Button,
87
+ * },
88
+ * ],
89
+ * })
90
+ * .addComponents(button2, button3);
91
+ * ```
92
+ */
93
+ public constructor(data: Partial<APIActionRowComponent<APIComponentInActionRow>> = {}) {
94
+ super();
95
+
96
+ const { components = [], ...rest } = data;
97
+
98
+ this.data = {
99
+ ...structuredClone(rest),
100
+ components: components.map((component) => createComponentBuilder(component)),
101
+ type: ComponentType.ActionRow,
102
+ };
103
+ }
104
+
105
+ /**
106
+ * Adds primary button components to this action row.
107
+ *
108
+ * @param input - The buttons to add
109
+ */
110
+ public addPrimaryButtonComponents(
111
+ ...input: RestOrArray<
112
+ APIButtonComponentWithCustomId | PrimaryButtonBuilder | ((builder: PrimaryButtonBuilder) => PrimaryButtonBuilder)
113
+ >
114
+ ): this {
115
+ const normalized = normalizeArray(input);
116
+ const resolved = normalized.map((component) => resolveBuilder(component, PrimaryButtonBuilder));
117
+
118
+ this.data.components.push(...resolved);
119
+ return this;
120
+ }
121
+
122
+ /**
123
+ * Adds secondary button components to this action row.
124
+ *
125
+ * @param input - The buttons to add
126
+ */
127
+ public addSecondaryButtonComponents(
128
+ ...input: RestOrArray<
129
+ | APIButtonComponentWithCustomId
130
+ | SecondaryButtonBuilder
131
+ | ((builder: SecondaryButtonBuilder) => SecondaryButtonBuilder)
132
+ >
133
+ ): this {
134
+ const normalized = normalizeArray(input);
135
+ const resolved = normalized.map((component) => resolveBuilder(component, SecondaryButtonBuilder));
136
+
137
+ this.data.components.push(...resolved);
138
+ return this;
139
+ }
140
+
141
+ /**
142
+ * Adds success button components to this action row.
143
+ *
144
+ * @param input - The buttons to add
145
+ */
146
+ public addSuccessButtonComponents(
147
+ ...input: RestOrArray<
148
+ APIButtonComponentWithCustomId | SuccessButtonBuilder | ((builder: SuccessButtonBuilder) => SuccessButtonBuilder)
149
+ >
150
+ ): this {
151
+ const normalized = normalizeArray(input);
152
+ const resolved = normalized.map((component) => resolveBuilder(component, SuccessButtonBuilder));
153
+
154
+ this.data.components.push(...resolved);
155
+ return this;
156
+ }
157
+
158
+ /**
159
+ * Adds danger button components to this action row.
160
+ */
161
+ public addDangerButtonComponents(
162
+ ...input: RestOrArray<
163
+ APIButtonComponentWithCustomId | DangerButtonBuilder | ((builder: DangerButtonBuilder) => DangerButtonBuilder)
164
+ >
165
+ ): this {
166
+ const normalized = normalizeArray(input);
167
+ const resolved = normalized.map((component) => resolveBuilder(component, DangerButtonBuilder));
168
+
169
+ this.data.components.push(...resolved);
170
+ return this;
171
+ }
172
+
173
+ /**
174
+ * Generically add any type of component to this action row, only takes in an instance of a component builder.
175
+ */
176
+ public addComponents(...input: RestOrArray<AnyActionRowComponentBuilder>): this {
177
+ const normalized = normalizeArray(input);
178
+ this.data.components.push(...normalized);
179
+
180
+ return this;
181
+ }
182
+
183
+ /**
184
+ * Adds SKU id button components to this action row.
185
+ *
186
+ * @param input - The buttons to add
187
+ */
188
+ public addPremiumButtonComponents(
189
+ ...input: RestOrArray<
190
+ APIButtonComponentWithSKUId | PremiumButtonBuilder | ((builder: PremiumButtonBuilder) => PremiumButtonBuilder)
191
+ >
192
+ ): this {
193
+ const normalized = normalizeArray(input);
194
+ const resolved = normalized.map((component) => resolveBuilder(component, PremiumButtonBuilder));
195
+
196
+ this.data.components.push(...resolved);
197
+ return this;
198
+ }
199
+
200
+ /**
201
+ * Adds URL button components to this action row.
202
+ *
203
+ * @param input - The buttons to add
204
+ */
205
+ public addLinkButtonComponents(
206
+ ...input: RestOrArray<
207
+ APIButtonComponentWithURL | LinkButtonBuilder | ((builder: LinkButtonBuilder) => LinkButtonBuilder)
208
+ >
209
+ ): this {
210
+ const normalized = normalizeArray(input);
211
+ const resolved = normalized.map((component) => resolveBuilder(component, LinkButtonBuilder));
212
+
213
+ this.data.components.push(...resolved);
214
+ return this;
215
+ }
216
+
217
+ /**
218
+ * Adds a channel select menu component to this action row.
219
+ *
220
+ * @param input - A function that returns a component builder or an already built builder
221
+ */
222
+ public addChannelSelectMenuComponent(
223
+ input:
224
+ | APIChannelSelectComponent
225
+ | ChannelSelectMenuBuilder
226
+ | ((builder: ChannelSelectMenuBuilder) => ChannelSelectMenuBuilder),
227
+ ): this {
228
+ this.data.components.push(resolveBuilder(input, ChannelSelectMenuBuilder));
229
+ return this;
230
+ }
231
+
232
+ /**
233
+ * Adds a mentionable select menu component to this action row.
234
+ *
235
+ * @param input - A function that returns a component builder or an already built builder
236
+ */
237
+ public addMentionableSelectMenuComponent(
238
+ input:
239
+ | APIMentionableSelectComponent
240
+ | MentionableSelectMenuBuilder
241
+ | ((builder: MentionableSelectMenuBuilder) => MentionableSelectMenuBuilder),
242
+ ): this {
243
+ this.data.components.push(resolveBuilder(input, MentionableSelectMenuBuilder));
244
+ return this;
245
+ }
246
+
247
+ /**
248
+ * Adds a role select menu component to this action row.
249
+ *
250
+ * @param input - A function that returns a component builder or an already built builder
251
+ */
252
+ public addRoleSelectMenuComponent(
253
+ input: APIRoleSelectComponent | RoleSelectMenuBuilder | ((builder: RoleSelectMenuBuilder) => RoleSelectMenuBuilder),
254
+ ): this {
255
+ this.data.components.push(resolveBuilder(input, RoleSelectMenuBuilder));
256
+ return this;
257
+ }
258
+
259
+ /**
260
+ * Adds a string select menu component to this action row.
261
+ *
262
+ * @param input - A function that returns a component builder or an already built builder
263
+ */
264
+ public addStringSelectMenuComponent(
265
+ input:
266
+ | APIStringSelectComponent
267
+ | StringSelectMenuBuilder
268
+ | ((builder: StringSelectMenuBuilder) => StringSelectMenuBuilder),
269
+ ): this {
270
+ this.data.components.push(resolveBuilder(input, StringSelectMenuBuilder));
271
+ return this;
272
+ }
273
+
274
+ /**
275
+ * Adds a user select menu component to this action row.
276
+ *
277
+ * @param input - A function that returns a component builder or an already built builder
278
+ */
279
+ public addUserSelectMenuComponent(
280
+ input: APIUserSelectComponent | UserSelectMenuBuilder | ((builder: UserSelectMenuBuilder) => UserSelectMenuBuilder),
281
+ ): this {
282
+ this.data.components.push(resolveBuilder(input, UserSelectMenuBuilder));
283
+ return this;
284
+ }
285
+
286
+ /**
287
+ * Adds a text input component to this action row.
288
+ *
289
+ * @param input - A function that returns a component builder or an already built builder
290
+ */
291
+ public addTextInputComponent(
292
+ input: APITextInputComponent | TextInputBuilder | ((builder: TextInputBuilder) => TextInputBuilder),
293
+ ): this {
294
+ this.data.components.push(resolveBuilder(input, TextInputBuilder));
295
+ return this;
296
+ }
297
+
298
+ /**
299
+ * Removes, replaces, or inserts components for this action row.
300
+ *
301
+ * @remarks
302
+ * This method behaves similarly
303
+ * to {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/splice | Array.prototype.splice()}.
304
+ *
305
+ * It's useful for modifying and adjusting order of the already-existing components of an action row.
306
+ * @example
307
+ * Remove the first component:
308
+ * ```ts
309
+ * actionRow.spliceComponents(0, 1);
310
+ * ```
311
+ * @example
312
+ * Remove the first n components:
313
+ * ```ts
314
+ * const n = 4;
315
+ * actionRow.spliceComponents(0, n);
316
+ * ```
317
+ * @example
318
+ * Remove the last component:
319
+ * ```ts
320
+ * actionRow.spliceComponents(-1, 1);
321
+ * ```
322
+ * @param index - The index to start at
323
+ * @param deleteCount - The number of components to remove
324
+ * @param components - The replacing component objects
325
+ */
326
+ public spliceComponents(index: number, deleteCount: number, ...components: AnyActionRowComponentBuilder[]): this {
327
+ this.data.components.splice(index, deleteCount, ...components);
328
+ return this;
329
+ }
330
+
331
+ /**
332
+ * {@inheritDoc ComponentBuilder.toJSON}
333
+ */
334
+ public override toJSON(validationOverride?: boolean): APIActionRowComponent<APIComponentInActionRow> {
335
+ const { components, ...rest } = this.data;
336
+
337
+ const data = {
338
+ ...structuredClone(rest),
339
+ components: components.map((component) => (component as any).toJSON(validationOverride)),
340
+ };
341
+
342
+ validate(actionRowPredicate, data, validationOverride);
343
+
344
+ return data as APIActionRowComponent<APIComponentInActionRow>;
345
+ }
346
+ }
@@ -0,0 +1,190 @@
1
+ import { ButtonStyle, ChannelType, ComponentType, SelectMenuDefaultValueType } from 'discord-api-types/v10';
2
+ import { z } from 'zod';
3
+ import { idPredicate, customIdPredicate, snowflakePredicate } from '../Assertions.js';
4
+
5
+ export const emojiPredicate = z
6
+ .strictObject({
7
+ id: snowflakePredicate.optional(),
8
+ name: z.string().min(1).max(32).optional(),
9
+ animated: z.boolean().optional(),
10
+ })
11
+ .refine((data) => data.id !== undefined || data.name !== undefined, {
12
+ message: "Either 'id' or 'name' must be provided",
13
+ });
14
+
15
+ const buttonPredicateBase = z.strictObject({
16
+ type: z.literal(ComponentType.Button),
17
+ disabled: z.boolean().optional(),
18
+ });
19
+
20
+ const buttonLabelPredicate = z.string().min(1).max(80);
21
+
22
+ const buttonCustomIdPredicateBase = buttonPredicateBase
23
+ .extend({
24
+ custom_id: customIdPredicate,
25
+ emoji: emojiPredicate.optional(),
26
+ label: buttonLabelPredicate.optional(),
27
+ });
28
+
29
+
30
+
31
+ const buttonPrimaryPredicate = buttonCustomIdPredicateBase.extend({ style: z.literal(ButtonStyle.Primary) });
32
+ const buttonSecondaryPredicate = buttonCustomIdPredicateBase.extend({ style: z.literal(ButtonStyle.Secondary) });
33
+ const buttonSuccessPredicate = buttonCustomIdPredicateBase.extend({ style: z.literal(ButtonStyle.Success) });
34
+ const buttonDangerPredicate = buttonCustomIdPredicateBase.extend({ style: z.literal(ButtonStyle.Danger) });
35
+
36
+ const buttonPrimaryRefinedPredicate = buttonPrimaryPredicate.refine((data) => data.emoji !== undefined || data.label !== undefined, {
37
+ message: 'Buttons with a custom id must have either an emoji or a label.',
38
+ });
39
+ const buttonSecondaryRefinedPredicate = buttonSecondaryPredicate.refine((data) => data.emoji !== undefined || data.label !== undefined, {
40
+ message: 'Buttons with a custom id must have either an emoji or a label.',
41
+ });
42
+ const buttonSuccessRefinedPredicate = buttonSuccessPredicate.refine((data) => data.emoji !== undefined || data.label !== undefined, {
43
+ message: 'Buttons with a custom id must have either an emoji or a label.',
44
+ });
45
+ const buttonDangerRefinedPredicate = buttonDangerPredicate.refine((data) => data.emoji !== undefined || data.label !== undefined, {
46
+ message: 'Buttons with a custom id must have either an emoji or a label.',
47
+ });
48
+
49
+ const buttonLinkPredicate = buttonPredicateBase
50
+ .extend({
51
+ style: z.literal(ButtonStyle.Link),
52
+ url: z.string().url().max(512).refine((url) => url.startsWith('http:') || url.startsWith('https:') || url.startsWith('discord:'), { message: 'URL must use http, https, or discord protocol' }),
53
+ emoji: emojiPredicate.optional(),
54
+ label: buttonLabelPredicate.optional(),
55
+ })
56
+ .refine((data) => data.emoji !== undefined || data.label !== undefined, {
57
+ message: 'Link buttons must have either an emoji or a label.',
58
+ });
59
+
60
+ const buttonPremiumPredicate = buttonPredicateBase.extend({
61
+ style: z.literal(ButtonStyle.Premium),
62
+ sku_id: snowflakePredicate,
63
+ });
64
+
65
+ export const buttonPredicate = z.union([
66
+ buttonLinkPredicate,
67
+ buttonPrimaryRefinedPredicate,
68
+ buttonSecondaryRefinedPredicate,
69
+ buttonSuccessRefinedPredicate,
70
+ buttonDangerRefinedPredicate,
71
+ buttonPremiumPredicate,
72
+ ]);
73
+
74
+ const selectMenuBasePredicate = z.object({
75
+ id: idPredicate,
76
+ placeholder: z.string().max(150).optional(),
77
+ min_values: z.number().min(0).max(25).optional(),
78
+ max_values: z.number().min(0).max(25).optional(),
79
+ custom_id: customIdPredicate,
80
+ disabled: z.boolean().optional(),
81
+ });
82
+
83
+ export const selectMenuChannelPredicate = selectMenuBasePredicate.extend({
84
+ type: z.literal(ComponentType.ChannelSelect),
85
+ channel_types: z.nativeEnum(ChannelType).array().optional(),
86
+ default_values: z
87
+ .object({ id: snowflakePredicate, type: z.literal(SelectMenuDefaultValueType.Channel) })
88
+ .array()
89
+ .max(25)
90
+ .optional(),
91
+ });
92
+
93
+ export const selectMenuMentionablePredicate = selectMenuBasePredicate.extend({
94
+ type: z.literal(ComponentType.MentionableSelect),
95
+ default_values: z
96
+ .object({
97
+ id: snowflakePredicate,
98
+ type: z.union([z.literal(SelectMenuDefaultValueType.Role), z.literal(SelectMenuDefaultValueType.User)]),
99
+ })
100
+ .array()
101
+ .max(25)
102
+ .optional(),
103
+ });
104
+
105
+ export const selectMenuRolePredicate = selectMenuBasePredicate.extend({
106
+ type: z.literal(ComponentType.RoleSelect),
107
+ default_values: z
108
+ .object({ id: snowflakePredicate, type: z.literal(SelectMenuDefaultValueType.Role) })
109
+ .array()
110
+ .max(25)
111
+ .optional(),
112
+ });
113
+
114
+ export const selectMenuStringOptionPredicate = z.object({
115
+ label: z.string().min(1).max(100),
116
+ value: z.string().min(1).max(100),
117
+ description: z.string().min(1).max(100).optional(),
118
+ emoji: emojiPredicate.optional(),
119
+ default: z.boolean().optional(),
120
+ });
121
+
122
+ export const selectMenuStringPredicate = selectMenuBasePredicate
123
+ .extend({
124
+ type: z.literal(ComponentType.StringSelect),
125
+ options: selectMenuStringOptionPredicate.array().min(1).max(25),
126
+ })
127
+ .superRefine((value, ctx) => {
128
+ const addIssue = (name: string, minimum: number) =>
129
+ ctx.addIssue({
130
+ code: z.ZodIssueCode.too_small,
131
+ message: `The number of options must be greater than or equal to ${name}`,
132
+ inclusive: true,
133
+ minimum,
134
+ type: 'array',
135
+ path: ['options'],
136
+ });
137
+
138
+ if (value.min_values !== undefined && value.options.length < value.min_values) {
139
+ addIssue('min_values', value.min_values);
140
+ }
141
+
142
+ if (
143
+ value.min_values !== undefined &&
144
+ value.max_values !== undefined &&
145
+ value.min_values > value.max_values
146
+ ) {
147
+ ctx.addIssue({
148
+ code: z.ZodIssueCode.too_big,
149
+ message: `The maximum amount of options must be greater than or equal to the minimum amount of options`,
150
+ inclusive: true,
151
+ maximum: value.max_values,
152
+ type: 'number',
153
+ path: ['min_values'],
154
+ });
155
+ }
156
+ });
157
+
158
+ export const selectMenuUserPredicate = selectMenuBasePredicate.extend({
159
+ type: z.literal(ComponentType.UserSelect),
160
+ default_values: z
161
+ .object({ id: snowflakePredicate, type: z.literal(SelectMenuDefaultValueType.User) })
162
+ .array()
163
+ .max(25)
164
+ .optional(),
165
+ });
166
+
167
+ export const actionRowPredicate = z.object({
168
+ id: idPredicate,
169
+ type: z.literal(ComponentType.ActionRow),
170
+ components: z.union([
171
+ z
172
+ .object({ type: z.literal(ComponentType.Button) })
173
+ .array()
174
+ .min(1)
175
+ .max(5),
176
+ z
177
+ .object({
178
+ type: z.union([
179
+ z.literal(ComponentType.ChannelSelect),
180
+ z.literal(ComponentType.MentionableSelect),
181
+ z.literal(ComponentType.StringSelect),
182
+ z.literal(ComponentType.RoleSelect),
183
+ z.literal(ComponentType.TextInput),
184
+ z.literal(ComponentType.UserSelect),
185
+ ]),
186
+ })
187
+ .array()
188
+ .length(1),
189
+ ]),
190
+ });
@@ -0,0 +1,47 @@
1
+ import type { JSONEncodable } from '@ovencord/util';
2
+ import type { APIBaseComponent, ComponentType } from 'discord-api-types/v10';
3
+
4
+ export interface ComponentBuilderBaseData {
5
+ id?: number | undefined;
6
+ }
7
+
8
+ /**
9
+ * The base component builder that contains common symbols for all sorts of components.
10
+ *
11
+ * @typeParam Component - The type of API data that is stored within the builder
12
+ */
13
+ export abstract class ComponentBuilder<
14
+ Component extends APIBaseComponent<ComponentType>,
15
+ > implements JSONEncodable<Component> {
16
+ /**
17
+ * @internal
18
+ */
19
+ protected abstract readonly data: ComponentBuilderBaseData;
20
+
21
+ /**
22
+ * Sets the id of this component.
23
+ *
24
+ * @param id - The id to use
25
+ */
26
+ public setId(id: number) {
27
+ this.data.id = id;
28
+ return this;
29
+ }
30
+
31
+ /**
32
+ * Clears the id of this component, defaulting to a default incremented id.
33
+ */
34
+ public clearId() {
35
+ this.data.id = undefined;
36
+ return this;
37
+ }
38
+
39
+ /**
40
+ * Serializes this builder to API-compatible JSON data.
41
+ *
42
+ * Note that by disabling validation, there is no guarantee that the resulting object will be valid.
43
+ *
44
+ * @param validationOverride - Force validation to run/not run regardless of your global preference
45
+ */
46
+ public abstract toJSON(validationOverride?: boolean): Component;
47
+ }