From a4d18629828234f43f03d1bd4851d4b727c6903b Mon Sep 17 00:00:00 2001 From: Parbez Date: Thu, 7 Jul 2022 00:12:51 +0530 Subject: [PATCH] refactor(builder): remove `unsafe*Builder`s (#8074) --- packages/builders/src/components/ActionRow.ts | 4 +- .../builders/src/components/Assertions.ts | 83 +++++--- .../builders/src/components/Components.ts | 12 +- .../builders/src/components/button/Button.ts | 88 ++++++-- .../src/components/button/UnsafeButton.ts | 85 -------- .../src/components/selectMenu/SelectMenu.ts | 102 +++++++-- .../components/selectMenu/SelectMenuOption.ts | 67 ++++-- .../components/selectMenu/UnsafeSelectMenu.ts | 108 ---------- .../selectMenu/UnsafeSelectMenuOption.ts | 65 ------ .../src/components/textInput/Assertions.ts | 20 +- .../src/components/textInput/TextInput.ts | 107 ++++++++-- .../components/textInput/UnsafeTextInput.ts | 104 --------- packages/builders/src/index.ts | 7 +- .../contextMenuCommands/Assertions.ts | 10 +- .../src/interactions/modals/Assertions.ts | 13 +- .../builders/src/interactions/modals/Modal.ts | 80 ++++++- .../src/interactions/modals/UnsafeModal.ts | 76 ------- .../interactions/slashCommands/Assertions.ts | 19 +- .../builders/src/messages/embed/Assertions.ts | 89 +++++--- packages/builders/src/messages/embed/Embed.ts | 200 +++++++++++++++--- .../src/messages/embed/UnsafeEmbed.ts | 189 ----------------- packages/builders/src/util/validation.ts | 5 + packages/discord.js/typings/index.d.ts | 2 +- packages/discord.js/typings/index.test-d.ts | 13 +- 24 files changed, 705 insertions(+), 843 deletions(-) delete mode 100644 packages/builders/src/components/button/UnsafeButton.ts delete mode 100644 packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts delete mode 100644 packages/builders/src/components/selectMenu/UnsafeSelectMenuOption.ts delete mode 100644 packages/builders/src/components/textInput/UnsafeTextInput.ts delete mode 100644 packages/builders/src/interactions/modals/UnsafeModal.ts delete mode 100644 packages/builders/src/messages/embed/UnsafeEmbed.ts create mode 100644 packages/builders/src/util/validation.ts diff --git a/packages/builders/src/components/ActionRow.ts b/packages/builders/src/components/ActionRow.ts index 52ba0455e..68edb04aa 100644 --- a/packages/builders/src/components/ActionRow.ts +++ b/packages/builders/src/components/ActionRow.ts @@ -7,7 +7,9 @@ import { } from 'discord-api-types/v10'; import { ComponentBuilder } from './Component'; import { createComponentBuilder } from './Components'; -import type { ButtonBuilder, SelectMenuBuilder, TextInputBuilder } from '..'; +import type { ButtonBuilder } from './button/Button'; +import type { SelectMenuBuilder } from './selectMenu/SelectMenu'; +import type { TextInputBuilder } from './textInput/TextInput'; import { normalizeArray, type RestOrArray } from '../util/normalizeArray'; export type MessageComponentBuilder = diff --git a/packages/builders/src/components/Assertions.ts b/packages/builders/src/components/Assertions.ts index 033f76c9c..7b3309e40 100644 --- a/packages/builders/src/components/Assertions.ts +++ b/packages/builders/src/components/Assertions.ts @@ -1,55 +1,78 @@ import { s } from '@sapphire/shapeshift'; import { APIMessageComponentEmoji, ButtonStyle } from 'discord-api-types/v10'; -import type { SelectMenuOptionBuilder } from './selectMenu/SelectMenuOption'; -import { UnsafeSelectMenuOptionBuilder } from './selectMenu/UnsafeSelectMenuOption'; +import { SelectMenuOptionBuilder } from './selectMenu/SelectMenuOption'; +import { isValidationEnabled } from '../util/validation'; -export const customIdValidator = s.string.lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(100); +export const customIdValidator = s.string + .lengthGreaterThanOrEqual(1) + .lengthLessThanOrEqual(100) + .setValidationEnabled(isValidationEnabled); -export const emojiValidator = s.object({ - id: s.string, - name: s.string, - animated: s.boolean, -}).partial.strict; +export const emojiValidator = s + .object({ + id: s.string, + name: s.string, + animated: s.boolean, + }) + .partial.strict.setValidationEnabled(isValidationEnabled); export const disabledValidator = s.boolean; -export const buttonLabelValidator = s.string.lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(80); +export const buttonLabelValidator = s.string + .lengthGreaterThanOrEqual(1) + .lengthLessThanOrEqual(80) + .setValidationEnabled(isValidationEnabled); export const buttonStyleValidator = s.nativeEnum(ButtonStyle); -export const placeholderValidator = s.string.lengthLessThanOrEqual(150); -export const minMaxValidator = s.number.int.greaterThanOrEqual(0).lessThanOrEqual(25); +export const placeholderValidator = s.string.lengthLessThanOrEqual(150).setValidationEnabled(isValidationEnabled); +export const minMaxValidator = s.number.int + .greaterThanOrEqual(0) + .lessThanOrEqual(25) + .setValidationEnabled(isValidationEnabled); -export const labelValueDescriptionValidator = s.string.lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(100); -export const optionValidator = s.union( - s.object({ - label: labelValueDescriptionValidator, - value: labelValueDescriptionValidator, - description: labelValueDescriptionValidator.optional, - emoji: emojiValidator.optional, - default: s.boolean.optional, - }), - s.instance(UnsafeSelectMenuOptionBuilder), -); -export const optionsValidator = optionValidator.array.lengthGreaterThanOrEqual(0); -export const optionsLengthValidator = s.number.int.greaterThanOrEqual(0).lessThanOrEqual(25); +export const labelValueDescriptionValidator = s.string + .lengthGreaterThanOrEqual(1) + .lengthLessThanOrEqual(100) + .setValidationEnabled(isValidationEnabled); +export const optionValidator = s + .union( + s.object({ + label: labelValueDescriptionValidator, + value: labelValueDescriptionValidator, + description: labelValueDescriptionValidator.optional, + emoji: emojiValidator.optional, + default: s.boolean.optional, + }), + s.instance(SelectMenuOptionBuilder), + ) + .setValidationEnabled(isValidationEnabled); + +export const optionsValidator = optionValidator.array + .lengthGreaterThanOrEqual(0) + .setValidationEnabled(isValidationEnabled); +export const optionsLengthValidator = s.number.int + .greaterThanOrEqual(0) + .lessThanOrEqual(25) + .setValidationEnabled(isValidationEnabled); export function validateRequiredSelectMenuParameters(options: SelectMenuOptionBuilder[], customId?: string) { customIdValidator.parse(customId); optionsValidator.parse(options); } -export const labelValueValidator = s.string.lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(100); export const defaultValidator = s.boolean; export function validateRequiredSelectMenuOptionParameters(label?: string, value?: string) { - labelValueValidator.parse(label); - labelValueValidator.parse(value); + labelValueDescriptionValidator.parse(label); + labelValueDescriptionValidator.parse(value); } -export const urlValidator = s.string.url({ - allowedProtocols: ['http:', 'https:', 'discord:'], -}); +export const urlValidator = s.string + .url({ + allowedProtocols: ['http:', 'https:', 'discord:'], + }) + .setValidationEnabled(isValidationEnabled); export function validateRequiredButtonParameters( style?: ButtonStyle, diff --git a/packages/builders/src/components/Components.ts b/packages/builders/src/components/Components.ts index 13a1b1603..b45ae95e9 100644 --- a/packages/builders/src/components/Components.ts +++ b/packages/builders/src/components/Components.ts @@ -1,6 +1,14 @@ import { APIMessageComponent, APIModalComponent, ComponentType } from 'discord-api-types/v10'; -import type { AnyComponentBuilder, MessageComponentBuilder, ModalComponentBuilder } from './ActionRow'; -import { ActionRowBuilder, ButtonBuilder, ComponentBuilder, SelectMenuBuilder, TextInputBuilder } from '../index'; +import { + ActionRowBuilder, + type AnyComponentBuilder, + type MessageComponentBuilder, + type ModalComponentBuilder, +} from './ActionRow'; +import { ComponentBuilder } from './Component'; +import { ButtonBuilder } from './button/Button'; +import { SelectMenuBuilder } from './selectMenu/SelectMenu'; +import { TextInputBuilder } from './textInput/TextInput'; export interface MappedComponentTypes { [ComponentType.ActionRow]: ActionRowBuilder; diff --git a/packages/builders/src/components/button/Button.ts b/packages/builders/src/components/button/Button.ts index e6570366f..7c92593a7 100644 --- a/packages/builders/src/components/button/Button.ts +++ b/packages/builders/src/components/button/Button.ts @@ -1,11 +1,11 @@ -import type { +import { + ComponentType, ButtonStyle, - APIMessageComponentEmoji, - APIButtonComponent, - APIButtonComponentWithCustomId, - APIButtonComponentWithURL, + type APIMessageComponentEmoji, + type APIButtonComponent, + type APIButtonComponentWithURL, + type APIButtonComponentWithCustomId, } from 'discord-api-types/v10'; -import { UnsafeButtonBuilder } from './UnsafeButton'; import { buttonLabelValidator, buttonStyleValidator, @@ -15,36 +15,77 @@ import { urlValidator, validateRequiredButtonParameters, } from '../Assertions'; +import { ComponentBuilder } from '../Component'; /** - * Represents a validated button component + * Represents a button component */ -export class ButtonBuilder extends UnsafeButtonBuilder { - public override setStyle(style: ButtonStyle) { - return super.setStyle(buttonStyleValidator.parse(style)); +export class ButtonBuilder extends ComponentBuilder { + public constructor(data?: Partial) { + super({ type: ComponentType.Button, ...data }); } - public override setURL(url: string) { - return super.setURL(urlValidator.parse(url)); + /** + * Sets the style of this button + * + * @param style - The style of the button + */ + public setStyle(style: ButtonStyle) { + this.data.style = buttonStyleValidator.parse(style); + return this; } - public override setCustomId(customId: string) { - return super.setCustomId(customIdValidator.parse(customId)); + /** + * Sets the URL for this button + * + * @param url - The URL to open when this button is clicked + */ + public setURL(url: string) { + (this.data as APIButtonComponentWithURL).url = urlValidator.parse(url); + return this; } - public override setEmoji(emoji: APIMessageComponentEmoji) { - return super.setEmoji(emojiValidator.parse(emoji)); + /** + * Sets the custom id for this button + * + * @param customId - The custom id to use for this button + */ + public setCustomId(customId: string) { + (this.data as APIButtonComponentWithCustomId).custom_id = customIdValidator.parse(customId); + return this; } - public override setDisabled(disabled = true) { - return super.setDisabled(disabledValidator.parse(disabled)); + /** + * Sets the emoji to display on this button + * + * @param emoji - The emoji to display on this button + */ + public setEmoji(emoji: APIMessageComponentEmoji) { + this.data.emoji = emojiValidator.parse(emoji); + return this; } - public override setLabel(label: string) { - return super.setLabel(buttonLabelValidator.parse(label)); + /** + * Sets whether this button is disabled + * + * @param disabled - Whether to disable this button + */ + public setDisabled(disabled = true) { + this.data.disabled = disabledValidator.parse(disabled); + return this; } - public override toJSON(): APIButtonComponent { + /** + * Sets the label for this button + * + * @param label - The label to display on this button + */ + public setLabel(label: string) { + this.data.label = buttonLabelValidator.parse(label); + return this; + } + + public toJSON(): APIButtonComponent { validateRequiredButtonParameters( this.data.style, this.data.label, @@ -52,6 +93,9 @@ export class ButtonBuilder extends UnsafeButtonBuilder { (this.data as APIButtonComponentWithCustomId).custom_id, (this.data as APIButtonComponentWithURL).url, ); - return super.toJSON(); + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return { + ...this.data, + } as APIButtonComponent; } } diff --git a/packages/builders/src/components/button/UnsafeButton.ts b/packages/builders/src/components/button/UnsafeButton.ts deleted file mode 100644 index 19d722db2..000000000 --- a/packages/builders/src/components/button/UnsafeButton.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { - ComponentType, - ButtonStyle, - type APIMessageComponentEmoji, - type APIButtonComponent, - type APIButtonComponentWithURL, - type APIButtonComponentWithCustomId, -} from 'discord-api-types/v10'; -import { ComponentBuilder } from '../Component'; - -/** - * Represents a non-validated button component - */ -export class UnsafeButtonBuilder extends ComponentBuilder { - public constructor(data?: Partial) { - super({ type: ComponentType.Button, ...data }); - } - - /** - * Sets the style of this button - * - * @param style - The style of the button - */ - public setStyle(style: ButtonStyle) { - this.data.style = style; - return this; - } - - /** - * Sets the URL for this button - * - * @param url - The URL to open when this button is clicked - */ - public setURL(url: string) { - (this.data as APIButtonComponentWithURL).url = url; - return this; - } - - /** - * Sets the custom Id for this button - * - * @param customId - The custom id to use for this button - */ - public setCustomId(customId: string) { - (this.data as APIButtonComponentWithCustomId).custom_id = customId; - return this; - } - - /** - * Sets the emoji to display on this button - * - * @param emoji - The emoji to display on this button - */ - public setEmoji(emoji: APIMessageComponentEmoji) { - this.data.emoji = emoji; - return this; - } - - /** - * Sets whether this button is disable or not - * - * @param disabled - Whether or not to disable this button or not - */ - public setDisabled(disabled = true) { - this.data.disabled = disabled; - return this; - } - - /** - * Sets the label for this button - * - * @param label - The label to display on this button - */ - public setLabel(label: string) { - this.data.label = label; - return this; - } - - public toJSON(): APIButtonComponent { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - return { - ...this.data, - } as APIButtonComponent; - } -} diff --git a/packages/builders/src/components/selectMenu/SelectMenu.ts b/packages/builders/src/components/selectMenu/SelectMenu.ts index 126219ee3..dd4ae6cd6 100644 --- a/packages/builders/src/components/selectMenu/SelectMenu.ts +++ b/packages/builders/src/components/selectMenu/SelectMenu.ts @@ -1,6 +1,5 @@ -import type { APISelectMenuComponent, APISelectMenuOption } from 'discord-api-types/v10'; -import { UnsafeSelectMenuBuilder } from './UnsafeSelectMenu'; -import { UnsafeSelectMenuOptionBuilder } from './UnsafeSelectMenuOption'; +import { APISelectMenuOption, ComponentType, type APISelectMenuComponent } from 'discord-api-types/v10'; +import { SelectMenuOptionBuilder } from './SelectMenuOption'; import { normalizeArray, type RestOrArray } from '../../util/normalizeArray'; import { customIdValidator, @@ -11,61 +10,118 @@ import { placeholderValidator, validateRequiredSelectMenuParameters, } from '../Assertions'; +import { ComponentBuilder } from '../Component'; /** - * Represents a validated select menu component + * Represents a select menu component */ -export class SelectMenuBuilder extends UnsafeSelectMenuBuilder { - public override setPlaceholder(placeholder: string) { - return super.setPlaceholder(placeholderValidator.parse(placeholder)); +export class SelectMenuBuilder extends ComponentBuilder { + /** + * The options within this select menu + */ + public readonly options: SelectMenuOptionBuilder[]; + + public constructor(data?: Partial) { + const { options, ...initData } = data ?? {}; + super({ type: ComponentType.SelectMenu, ...initData }); + this.options = options?.map((o) => new SelectMenuOptionBuilder(o)) ?? []; } - public override setMinValues(minValues: number) { - return super.setMinValues(minMaxValidator.parse(minValues)); + /** + * Sets the placeholder for this select menu + * + * @param placeholder - The placeholder to use for this select menu + */ + public setPlaceholder(placeholder: string) { + this.data.placeholder = placeholderValidator.parse(placeholder); + return this; } - public override setMaxValues(maxValues: number) { - return super.setMaxValues(minMaxValidator.parse(maxValues)); + /** + * Sets the minimum values that must be selected in the select menu + * + * @param minValues - The minimum values that must be selected + */ + public setMinValues(minValues: number) { + this.data.min_values = minMaxValidator.parse(minValues); + return this; } - public override setCustomId(customId: string) { - return super.setCustomId(customIdValidator.parse(customId)); + /** + * Sets the maximum values that must be selected in the select menu + * + * @param maxValues - The maximum values that must be selected + */ + public setMaxValues(maxValues: number) { + this.data.max_values = minMaxValidator.parse(maxValues); + return this; } - public override setDisabled(disabled = true) { - return super.setDisabled(disabledValidator.parse(disabled)); + /** + * Sets the custom id for this select menu + * + * @param customId - The custom id to use for this select menu + */ + public setCustomId(customId: string) { + this.data.custom_id = customIdValidator.parse(customId); + return this; } - public override addOptions(...options: RestOrArray) { + /** + * Sets whether this select menu is disabled + * + * @param disabled - Whether this select menu is disabled + */ + public setDisabled(disabled = true) { + this.data.disabled = disabledValidator.parse(disabled); + return this; + } + + /** + * Adds options to this select menu + * + * @param options - The options to add to this select menu + * @returns + */ + public addOptions(...options: RestOrArray) { options = normalizeArray(options); optionsLengthValidator.parse(this.options.length + options.length); this.options.push( ...options.map((option) => - option instanceof UnsafeSelectMenuOptionBuilder + option instanceof SelectMenuOptionBuilder ? option - : new UnsafeSelectMenuOptionBuilder(optionValidator.parse(option)), + : new SelectMenuOptionBuilder(optionValidator.parse(option)), ), ); return this; } - public override setOptions(...options: RestOrArray) { + /** + * Sets the options on this select menu + * + * @param options - The options to set on this select menu + */ + public setOptions(...options: RestOrArray) { options = normalizeArray(options); optionsLengthValidator.parse(options.length); this.options.splice( 0, this.options.length, ...options.map((option) => - option instanceof UnsafeSelectMenuOptionBuilder + option instanceof SelectMenuOptionBuilder ? option - : new UnsafeSelectMenuOptionBuilder(optionValidator.parse(option)), + : new SelectMenuOptionBuilder(optionValidator.parse(option)), ), ); return this; } - public override toJSON(): APISelectMenuComponent { + public toJSON(): APISelectMenuComponent { validateRequiredSelectMenuParameters(this.options, this.data.custom_id); - return super.toJSON(); + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return { + ...this.data, + options: this.options.map((o) => o.toJSON()), + } as APISelectMenuComponent; } } diff --git a/packages/builders/src/components/selectMenu/SelectMenuOption.ts b/packages/builders/src/components/selectMenu/SelectMenuOption.ts index 200782034..3f9d50203 100644 --- a/packages/builders/src/components/selectMenu/SelectMenuOption.ts +++ b/packages/builders/src/components/selectMenu/SelectMenuOption.ts @@ -1,30 +1,73 @@ import type { APIMessageComponentEmoji, APISelectMenuOption } from 'discord-api-types/v10'; -import { UnsafeSelectMenuOptionBuilder } from './UnsafeSelectMenuOption'; + import { defaultValidator, emojiValidator, - labelValueValidator, + labelValueDescriptionValidator, validateRequiredSelectMenuOptionParameters, } from '../Assertions'; /** - * Represents a validated option within a select menu component + * Represents a option within a select menu component */ -export class SelectMenuOptionBuilder extends UnsafeSelectMenuOptionBuilder { - public override setDescription(description: string) { - return super.setDescription(labelValueValidator.parse(description)); +export class SelectMenuOptionBuilder { + public constructor(public data: Partial = {}) {} + + /** + * Sets the label of this option + * + * @param label - The label to show on this option + */ + public setLabel(label: string) { + this.data.label = labelValueDescriptionValidator.parse(label); + return this; } - public override setDefault(isDefault = true) { - return super.setDefault(defaultValidator.parse(isDefault)); + /** + * Sets the value of this option + * + * @param value - The value of this option + */ + public setValue(value: string) { + this.data.value = labelValueDescriptionValidator.parse(value); + return this; } - public override setEmoji(emoji: APIMessageComponentEmoji) { - return super.setEmoji(emojiValidator.parse(emoji)); + /** + * Sets the description of this option + * + * @param description - The description of this option + */ + public setDescription(description: string) { + this.data.description = labelValueDescriptionValidator.parse(description); + return this; } - public override toJSON(): APISelectMenuOption { + /** + * Sets whether this option is selected by default + * + * @param isDefault - Whether this option is selected by default + */ + public setDefault(isDefault = true) { + this.data.default = defaultValidator.parse(isDefault); + return this; + } + + /** + * Sets the emoji to display on this option + * + * @param emoji - The emoji to display on this option + */ + public setEmoji(emoji: APIMessageComponentEmoji) { + this.data.emoji = emojiValidator.parse(emoji); + return this; + } + + public toJSON(): APISelectMenuOption { validateRequiredSelectMenuOptionParameters(this.data.label, this.data.value); - return super.toJSON(); + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return { + ...this.data, + } as APISelectMenuOption; } } diff --git a/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts b/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts deleted file mode 100644 index 3329d74b3..000000000 --- a/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { APISelectMenuOption, ComponentType, type APISelectMenuComponent } from 'discord-api-types/v10'; -import { UnsafeSelectMenuOptionBuilder } from './UnsafeSelectMenuOption'; -import { normalizeArray, type RestOrArray } from '../../util/normalizeArray'; -import { ComponentBuilder } from '../Component'; - -/** - * Represents a non-validated select menu component - */ -export class UnsafeSelectMenuBuilder extends ComponentBuilder { - /** - * The options within this select menu - */ - public readonly options: UnsafeSelectMenuOptionBuilder[]; - - public constructor(data?: Partial) { - const { options, ...initData } = data ?? {}; - super({ type: ComponentType.SelectMenu, ...initData }); - this.options = options?.map((o) => new UnsafeSelectMenuOptionBuilder(o)) ?? []; - } - - /** - * Sets the placeholder for this select menu - * - * @param placeholder - The placeholder to use for this select menu - */ - public setPlaceholder(placeholder: string) { - this.data.placeholder = placeholder; - return this; - } - - /** - * Sets the minimum values that must be selected in the select menu - * - * @param minValues - The minimum values that must be selected - */ - public setMinValues(minValues: number) { - this.data.min_values = minValues; - return this; - } - - /** - * Sets the maximum values that must be selected in the select menu - * - * @param minValues - The maximum values that must be selected - */ - public setMaxValues(maxValues: number) { - this.data.max_values = maxValues; - return this; - } - - /** - * Sets the custom Id for this select menu - * - * @param customId - The custom id to use for this select menu - */ - public setCustomId(customId: string) { - this.data.custom_id = customId; - return this; - } - - /** - * Sets whether or not this select menu is disabled - * - * @param disabled - Whether or not this select menu is disabled - */ - public setDisabled(disabled = true) { - this.data.disabled = disabled; - return this; - } - - /** - * Adds options to this select menu - * - * @param options - The options to add to this select menu - */ - public addOptions(...options: RestOrArray) { - this.options.push( - ...normalizeArray(options).map((option) => - option instanceof UnsafeSelectMenuOptionBuilder ? option : new UnsafeSelectMenuOptionBuilder(option), - ), - ); - return this; - } - - /** - * Sets the options on this select menu - * - * @param options - The options to set on this select menu - */ - public setOptions(...options: RestOrArray) { - this.options.splice( - 0, - this.options.length, - ...normalizeArray(options).map((option) => - option instanceof UnsafeSelectMenuOptionBuilder ? option : new UnsafeSelectMenuOptionBuilder(option), - ), - ); - return this; - } - - public toJSON(): APISelectMenuComponent { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - return { - ...this.data, - options: this.options.map((o) => o.toJSON()), - } as APISelectMenuComponent; - } -} diff --git a/packages/builders/src/components/selectMenu/UnsafeSelectMenuOption.ts b/packages/builders/src/components/selectMenu/UnsafeSelectMenuOption.ts deleted file mode 100644 index 5b065c0bd..000000000 --- a/packages/builders/src/components/selectMenu/UnsafeSelectMenuOption.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type { APIMessageComponentEmoji, APISelectMenuOption } from 'discord-api-types/v10'; - -/** - * Represents a non-validated option within a select menu component - */ -export class UnsafeSelectMenuOptionBuilder { - public constructor(public data: Partial = {}) {} - - /** - * Sets the label of this option - * - * @param label - The label to show on this option - */ - public setLabel(label: string) { - this.data.label = label; - return this; - } - - /** - * Sets the value of this option - * - * @param value - The value of this option - */ - public setValue(value: string) { - this.data.value = value; - return this; - } - - /** - * Sets the description of this option. - * - * @param description - The description of this option - */ - public setDescription(description: string) { - this.data.description = description; - return this; - } - - /** - * Sets whether this option is selected by default - * - * @param isDefault - Whether this option is selected by default - */ - public setDefault(isDefault = true) { - this.data.default = isDefault; - return this; - } - - /** - * Sets the emoji to display on this option - * - * @param emoji - The emoji to display on this option - */ - public setEmoji(emoji: APIMessageComponentEmoji) { - this.data.emoji = emoji; - return this; - } - - public toJSON(): APISelectMenuOption { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - return { - ...this.data, - } as APISelectMenuOption; - } -} diff --git a/packages/builders/src/components/textInput/Assertions.ts b/packages/builders/src/components/textInput/Assertions.ts index c74a8548d..b468c0029 100644 --- a/packages/builders/src/components/textInput/Assertions.ts +++ b/packages/builders/src/components/textInput/Assertions.ts @@ -1,14 +1,24 @@ import { s } from '@sapphire/shapeshift'; import { TextInputStyle } from 'discord-api-types/v10'; +import { isValidationEnabled } from '../../util/validation'; import { customIdValidator } from '../Assertions'; export const textInputStyleValidator = s.nativeEnum(TextInputStyle); -export const minLengthValidator = s.number.int.greaterThanOrEqual(0).lessThanOrEqual(4000); -export const maxLengthValidator = s.number.int.greaterThanOrEqual(1).lessThanOrEqual(4000); +export const minLengthValidator = s.number.int + .greaterThanOrEqual(0) + .lessThanOrEqual(4000) + .setValidationEnabled(isValidationEnabled); +export const maxLengthValidator = s.number.int + .greaterThanOrEqual(1) + .lessThanOrEqual(4000) + .setValidationEnabled(isValidationEnabled); export const requiredValidator = s.boolean; -export const valueValidator = s.string.lengthLessThanOrEqual(4000); -export const placeholderValidator = s.string.lengthLessThanOrEqual(100); -export const labelValidator = s.string.lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(45); +export const valueValidator = s.string.lengthLessThanOrEqual(4000).setValidationEnabled(isValidationEnabled); +export const placeholderValidator = s.string.lengthLessThanOrEqual(100).setValidationEnabled(isValidationEnabled); +export const labelValidator = s.string + .lengthGreaterThanOrEqual(1) + .lengthLessThanOrEqual(45) + .setValidationEnabled(isValidationEnabled); export function validateRequiredParameters(customId?: string, style?: TextInputStyle, label?: string) { customIdValidator.parse(customId); diff --git a/packages/builders/src/components/textInput/TextInput.ts b/packages/builders/src/components/textInput/TextInput.ts index eff72b851..9b2934a1f 100644 --- a/packages/builders/src/components/textInput/TextInput.ts +++ b/packages/builders/src/components/textInput/TextInput.ts @@ -1,4 +1,5 @@ -import type { APITextInputComponent, TextInputStyle } from 'discord-api-types/v10'; +import { ComponentType, type TextInputStyle, type APITextInputComponent } from 'discord-api-types/v10'; +import isEqual from 'fast-deep-equal'; import { maxLengthValidator, minLengthValidator, @@ -9,44 +10,108 @@ import { labelValidator, textInputStyleValidator, } from './Assertions'; -import { UnsafeTextInputBuilder } from './UnsafeTextInput'; +import { isJSONEncodable, type JSONEncodable } from '../../util/jsonEncodable'; import { customIdValidator } from '../Assertions'; +import { ComponentBuilder } from '../Component'; -export class TextInputBuilder extends UnsafeTextInputBuilder { - public override setCustomId(customId: string): this { - return super.setCustomId(customIdValidator.parse(customId)); +export class TextInputBuilder extends ComponentBuilder { + public constructor(data?: APITextInputComponent & { type?: ComponentType.TextInput }) { + super({ type: ComponentType.TextInput, ...data }); } - public override setLabel(label: string): this { - return super.setLabel(labelValidator.parse(label)); + /** + * Sets the custom id for this text input + * + * @param customId - The custom id of this text input + */ + public setCustomId(customId: string) { + this.data.custom_id = customIdValidator.parse(customId); + return this; } - public override setStyle(style: TextInputStyle): this { - return super.setStyle(textInputStyleValidator.parse(style)); + /** + * Sets the label for this text input + * + * @param label - The label for this text input + */ + public setLabel(label: string) { + this.data.label = labelValidator.parse(label); + return this; } - public override setMinLength(minLength: number) { - return super.setMinLength(minLengthValidator.parse(minLength)); + /** + * Sets the style for this text input + * + * @param style - The style for this text input + */ + public setStyle(style: TextInputStyle) { + this.data.style = textInputStyleValidator.parse(style); + return this; } - public override setMaxLength(maxLength: number) { - return super.setMaxLength(maxLengthValidator.parse(maxLength)); + /** + * Sets the minimum length of text for this text input + * + * @param minLength - The minimum length of text for this text input + */ + public setMinLength(minLength: number) { + this.data.min_length = minLengthValidator.parse(minLength); + return this; } - public override setPlaceholder(placeholder: string) { - return super.setPlaceholder(placeholderValidator.parse(placeholder)); + /** + * Sets the maximum length of text for this text input + * + * @param maxLength - The maximum length of text for this text input + */ + public setMaxLength(maxLength: number) { + this.data.max_length = maxLengthValidator.parse(maxLength); + return this; } - public override setValue(value: string) { - return super.setValue(valueValidator.parse(value)); + /** + * Sets the placeholder of this text input + * + * @param placeholder - The placeholder of this text input + */ + public setPlaceholder(placeholder: string) { + this.data.placeholder = placeholderValidator.parse(placeholder); + return this; } - public override setRequired(required = true) { - return super.setRequired(requiredValidator.parse(required)); + /** + * Sets the value of this text input + * + * @param value - The value for this text input + */ + public setValue(value: string) { + this.data.value = valueValidator.parse(value); + return this; } - public override toJSON(): APITextInputComponent { + /** + * Sets whether this text input is required + * + * @param required - Whether this text input is required + */ + public setRequired(required = true) { + this.data.required = requiredValidator.parse(required); + return this; + } + + public toJSON(): APITextInputComponent { validateRequiredParameters(this.data.custom_id, this.data.style, this.data.label); - return super.toJSON(); + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return { + ...this.data, + } as APITextInputComponent; + } + + public equals(other: JSONEncodable | APITextInputComponent): boolean { + if (isJSONEncodable(other)) { + return isEqual(other.toJSON(), this.data); + } + + return isEqual(other, this.data); } } diff --git a/packages/builders/src/components/textInput/UnsafeTextInput.ts b/packages/builders/src/components/textInput/UnsafeTextInput.ts deleted file mode 100644 index 437d4253c..000000000 --- a/packages/builders/src/components/textInput/UnsafeTextInput.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { ComponentType, type TextInputStyle, type APITextInputComponent } from 'discord-api-types/v10'; -import isEqual from 'fast-deep-equal'; -import { ComponentBuilder } from '../../index'; - -export class UnsafeTextInputBuilder extends ComponentBuilder { - public constructor(data?: APITextInputComponent & { type?: ComponentType.TextInput }) { - super({ type: ComponentType.TextInput, ...data }); - } - - /** - * Sets the custom id for this text input - * - * @param customId - The custom id of this text input - */ - public setCustomId(customId: string) { - this.data.custom_id = customId; - return this; - } - - /** - * Sets the label for this text input - * - * @param label - The label for this text input - */ - public setLabel(label: string) { - this.data.label = label; - return this; - } - - /** - * Sets the style for this text input - * - * @param style - The style for this text input - */ - public setStyle(style: TextInputStyle) { - this.data.style = style; - return this; - } - - /** - * Sets the minimum length of text for this text input - * - * @param minLength - The minimum length of text for this text input - */ - public setMinLength(minLength: number) { - this.data.min_length = minLength; - return this; - } - - /** - * Sets the maximum length of text for this text input - * - * @param maxLength - The maximum length of text for this text input - */ - public setMaxLength(maxLength: number) { - this.data.max_length = maxLength; - return this; - } - - /** - * Sets the placeholder of this text input - * - * @param placeholder - The placeholder of this text input - */ - public setPlaceholder(placeholder: string) { - this.data.placeholder = placeholder; - return this; - } - - /** - * Sets the value of this text input - * - * @param value - The value for this text input - */ - public setValue(value: string) { - this.data.value = value; - return this; - } - - /** - * Sets whether this text input is required or not - * - * @param required - Whether this text input is required or not - */ - public setRequired(required = true) { - this.data.required = required; - return this; - } - - public toJSON(): APITextInputComponent { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - return { - ...this.data, - } as APITextInputComponent; - } - - public equals(other: UnsafeTextInputBuilder | APITextInputComponent): boolean { - if (other instanceof UnsafeTextInputBuilder) { - return isEqual(other.data, this.data); - } - - return isEqual(other, this.data); - } -} diff --git a/packages/builders/src/index.ts b/packages/builders/src/index.ts index f3e30d6ae..ba087db94 100644 --- a/packages/builders/src/index.ts +++ b/packages/builders/src/index.ts @@ -1,7 +1,6 @@ export * as EmbedAssertions from './messages/embed/Assertions'; export * from './messages/embed/Embed'; export * from './messages/formatters'; -export * from './messages/embed/UnsafeEmbed'; export * as ComponentAssertions from './components/Assertions'; export * from './components/ActionRow'; @@ -10,15 +9,10 @@ export * from './components/Component'; export * from './components/Components'; export * from './components/textInput/TextInput'; export * as TextInputAssertions from './components/textInput/Assertions'; -export * from './components/textInput/UnsafeTextInput'; -export * from './interactions/modals/UnsafeModal'; export * from './interactions/modals/Modal'; export * as ModalAssertions from './interactions/modals/Assertions'; export * from './components/selectMenu/SelectMenu'; export * from './components/selectMenu/SelectMenuOption'; -export * from './components/button/UnsafeButton'; -export * from './components/selectMenu/UnsafeSelectMenu'; -export * from './components/selectMenu/UnsafeSelectMenuOption'; export * as SlashCommandAssertions from './interactions/slashCommands/Assertions'; export * from './interactions/slashCommands/SlashCommandBuilder'; @@ -46,3 +40,4 @@ export * from './util/jsonEncodable'; export * from './util/equatable'; export * from './util/componentUtil'; export * from './util/normalizeArray'; +export * from './util/validation'; diff --git a/packages/builders/src/interactions/contextMenuCommands/Assertions.ts b/packages/builders/src/interactions/contextMenuCommands/Assertions.ts index d719b4f79..af5ffa9b1 100644 --- a/packages/builders/src/interactions/contextMenuCommands/Assertions.ts +++ b/packages/builders/src/interactions/contextMenuCommands/Assertions.ts @@ -1,14 +1,16 @@ import { s } from '@sapphire/shapeshift'; import { ApplicationCommandType } from 'discord-api-types/v10'; import type { ContextMenuCommandType } from './ContextMenuCommandBuilder'; +import { isValidationEnabled } from '../../util/validation'; const namePredicate = s.string .lengthGreaterThanOrEqual(1) .lengthLessThanOrEqual(32) - .regex(/^( *[\p{L}\p{N}\p{sc=Devanagari}\p{sc=Thai}_-]+ *)+$/u); - -const typePredicate = s.union(s.literal(ApplicationCommandType.User), s.literal(ApplicationCommandType.Message)); - + .regex(/^( *[\p{L}\p{N}\p{sc=Devanagari}\p{sc=Thai}_-]+ *)+$/u) + .setValidationEnabled(isValidationEnabled); +const typePredicate = s + .union(s.literal(ApplicationCommandType.User), s.literal(ApplicationCommandType.Message)) + .setValidationEnabled(isValidationEnabled); const booleanPredicate = s.boolean; export function validateDefaultPermission(value: unknown): asserts value is boolean { diff --git a/packages/builders/src/interactions/modals/Assertions.ts b/packages/builders/src/interactions/modals/Assertions.ts index c466057fe..2b972f23d 100644 --- a/packages/builders/src/interactions/modals/Assertions.ts +++ b/packages/builders/src/interactions/modals/Assertions.ts @@ -1,9 +1,16 @@ import { s } from '@sapphire/shapeshift'; -import { ActionRowBuilder, type ModalActionRowComponentBuilder } from '../..'; +import { ActionRowBuilder, type ModalActionRowComponentBuilder } from '../../components/ActionRow'; import { customIdValidator } from '../../components/Assertions'; +import { isValidationEnabled } from '../../util/validation'; -export const titleValidator = s.string.lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(45); -export const componentsValidator = s.instance(ActionRowBuilder).array.lengthGreaterThanOrEqual(1); +export const titleValidator = s.string + .lengthGreaterThanOrEqual(1) + .lengthLessThanOrEqual(45) + .setValidationEnabled(isValidationEnabled); +export const componentsValidator = s + .instance(ActionRowBuilder) + .array.lengthGreaterThanOrEqual(1) + .setValidationEnabled(isValidationEnabled); export function validateRequiredParameters( customId?: string, diff --git a/packages/builders/src/interactions/modals/Modal.ts b/packages/builders/src/interactions/modals/Modal.ts index 2ebf859e1..91230fd9d 100644 --- a/packages/builders/src/interactions/modals/Modal.ts +++ b/packages/builders/src/interactions/modals/Modal.ts @@ -1,19 +1,81 @@ -import type { APIModalInteractionResponseCallbackData } from 'discord-api-types/v10'; +import type { + APIActionRowComponent, + APIModalActionRowComponent, + APIModalInteractionResponseCallbackData, +} from 'discord-api-types/v10'; import { titleValidator, validateRequiredParameters } from './Assertions'; -import { UnsafeModalBuilder } from './UnsafeModal'; +import { ActionRowBuilder, type ModalActionRowComponentBuilder } from '../../components/ActionRow'; import { customIdValidator } from '../../components/Assertions'; +import { createComponentBuilder } from '../../components/Components'; +import type { JSONEncodable } from '../../util/jsonEncodable'; +import { normalizeArray, type RestOrArray } from '../../util/normalizeArray'; -export class ModalBuilder extends UnsafeModalBuilder { - public override setCustomId(customId: string): this { - return super.setCustomId(customIdValidator.parse(customId)); +export class ModalBuilder implements JSONEncodable { + public readonly data: Partial; + public readonly components: ActionRowBuilder[] = []; + + public constructor({ components, ...data }: Partial = {}) { + this.data = { ...data }; + this.components = (components?.map((c) => createComponentBuilder(c)) ?? + []) as ActionRowBuilder[]; } - public override setTitle(title: string) { - return super.setTitle(titleValidator.parse(title)); + /** + * Sets the title of the modal + * + * @param title - The title of the modal + */ + public setTitle(title: string) { + this.data.title = titleValidator.parse(title); + return this; } - public override toJSON(): APIModalInteractionResponseCallbackData { + /** + * Sets the custom id of the modal + * + * @param customId - The custom id of this modal + */ + public setCustomId(customId: string) { + this.data.custom_id = customIdValidator.parse(customId); + return this; + } + + /** + * Adds components to this modal + * + * @param components - The components to add to this modal + */ + public addComponents( + ...components: RestOrArray< + ActionRowBuilder | APIActionRowComponent + > + ) { + this.components.push( + ...normalizeArray(components).map((component) => + component instanceof ActionRowBuilder + ? component + : new ActionRowBuilder(component), + ), + ); + return this; + } + + /** + * Sets the components in this modal + * + * @param components - The components to set this modal to + */ + public setComponents(...components: RestOrArray>) { + this.components.splice(0, this.components.length, ...normalizeArray(components)); + return this; + } + + public toJSON(): APIModalInteractionResponseCallbackData { validateRequiredParameters(this.data.custom_id, this.data.title, this.components); - return super.toJSON(); + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return { + ...this.data, + components: this.components.map((component) => component.toJSON()), + } as APIModalInteractionResponseCallbackData; } } diff --git a/packages/builders/src/interactions/modals/UnsafeModal.ts b/packages/builders/src/interactions/modals/UnsafeModal.ts deleted file mode 100644 index 4306b7fe8..000000000 --- a/packages/builders/src/interactions/modals/UnsafeModal.ts +++ /dev/null @@ -1,76 +0,0 @@ -import type { - APIActionRowComponent, - APIModalActionRowComponent, - APIModalInteractionResponseCallbackData, -} from 'discord-api-types/v10'; -import { ActionRowBuilder, createComponentBuilder, JSONEncodable, ModalActionRowComponentBuilder } from '../../index'; -import { normalizeArray, type RestOrArray } from '../../util/normalizeArray'; - -export class UnsafeModalBuilder implements JSONEncodable { - public readonly data: Partial; - public readonly components: ActionRowBuilder[] = []; - - public constructor({ components, ...data }: Partial = {}) { - this.data = { ...data }; - this.components = (components?.map((c) => createComponentBuilder(c)) ?? - []) as ActionRowBuilder[]; - } - - /** - * Sets the title of the modal - * - * @param title - The title of the modal - */ - public setTitle(title: string) { - this.data.title = title; - return this; - } - - /** - * Sets the custom id of the modal - * - * @param customId - The custom id of this modal - */ - public setCustomId(customId: string) { - this.data.custom_id = customId; - return this; - } - - /** - * Adds components to this modal - * - * @param components - The components to add to this modal - */ - public addComponents( - ...components: RestOrArray< - ActionRowBuilder | APIActionRowComponent - > - ) { - this.components.push( - ...normalizeArray(components).map((component) => - component instanceof ActionRowBuilder - ? component - : new ActionRowBuilder(component), - ), - ); - return this; - } - - /** - * Sets the components in this modal - * - * @param components - The components to set this modal to - */ - public setComponents(...components: RestOrArray>) { - this.components.splice(0, this.components.length, ...normalizeArray(components)); - return this; - } - - public toJSON(): APIModalInteractionResponseCallbackData { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - return { - ...this.data, - components: this.components.map((component) => component.toJSON()), - } as APIModalInteractionResponseCallbackData; - } -} diff --git a/packages/builders/src/interactions/slashCommands/Assertions.ts b/packages/builders/src/interactions/slashCommands/Assertions.ts index 6ed1df790..39f38343f 100644 --- a/packages/builders/src/interactions/slashCommands/Assertions.ts +++ b/packages/builders/src/interactions/slashCommands/Assertions.ts @@ -3,24 +3,29 @@ import { type APIApplicationCommandOptionChoice, Locale, LocalizationMap } from import type { ToAPIApplicationCommandOptions } from './SlashCommandBuilder'; import type { SlashCommandSubcommandBuilder, SlashCommandSubcommandGroupBuilder } from './SlashCommandSubcommands'; import type { ApplicationCommandOptionBase } from './mixins/ApplicationCommandOptionBase'; +import { isValidationEnabled } from '../../util/validation'; const namePredicate = s.string .lengthGreaterThanOrEqual(1) .lengthLessThanOrEqual(32) - .regex(/^[\P{Lu}\p{N}\p{sc=Devanagari}\p{sc=Thai}_-]+$/u); + .regex(/^[\P{Lu}\p{N}\p{sc=Devanagari}\p{sc=Thai}_-]+$/u) + .setValidationEnabled(isValidationEnabled); export function validateName(name: unknown): asserts name is string { namePredicate.parse(name); } -const descriptionPredicate = s.string.lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(100); +const descriptionPredicate = s.string + .lengthGreaterThanOrEqual(1) + .lengthLessThanOrEqual(100) + .setValidationEnabled(isValidationEnabled); const localePredicate = s.nativeEnum(Locale); export function validateDescription(description: unknown): asserts description is string { descriptionPredicate.parse(description); } -const maxArrayLengthPredicate = s.unknown.array.lengthLessThanOrEqual(25); +const maxArrayLengthPredicate = s.unknown.array.lengthLessThanOrEqual(25).setValidationEnabled(isValidationEnabled); export function validateLocale(locale: unknown) { return localePredicate.parse(locale); } @@ -54,7 +59,7 @@ export function validateRequired(required: unknown): asserts required is boolean booleanPredicate.parse(required); } -const choicesLengthPredicate = s.number.lessThanOrEqual(25); +const choicesLengthPredicate = s.number.lessThanOrEqual(25).setValidationEnabled(isValidationEnabled); export function validateChoicesLength(amountAdding: number, choices?: APIApplicationCommandOptionChoice[]): void { choicesLengthPredicate.parse((choices?.length ?? 0) + amountAdding); @@ -66,9 +71,9 @@ export function assertReturnOfBuilder< s.instance(ExpectedInstanceOf).parse(input); } -export const localizationMapPredicate = s.object( - Object.fromEntries(Object.values(Locale).map((locale) => [locale, s.string.nullish])), -).strict.nullish; +export const localizationMapPredicate = s + .object(Object.fromEntries(Object.values(Locale).map((locale) => [locale, s.string.nullish]))) + .strict.nullish.setValidationEnabled(isValidationEnabled); export function validateLocalizationMap(value: unknown): asserts value is LocalizationMap { localizationMapPredicate.parse(value); diff --git a/packages/builders/src/messages/embed/Assertions.ts b/packages/builders/src/messages/embed/Assertions.ts index 1e63adf91..909575d88 100644 --- a/packages/builders/src/messages/embed/Assertions.ts +++ b/packages/builders/src/messages/embed/Assertions.ts @@ -1,57 +1,84 @@ import { s } from '@sapphire/shapeshift'; import type { APIEmbedField } from 'discord-api-types/v10'; +import { isValidationEnabled } from '../../util/validation'; -export const fieldNamePredicate = s.string.lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(256); +export const fieldNamePredicate = s.string + .lengthGreaterThanOrEqual(1) + .lengthLessThanOrEqual(256) + .setValidationEnabled(isValidationEnabled); -export const fieldValuePredicate = s.string.lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(1024); +export const fieldValuePredicate = s.string + .lengthGreaterThanOrEqual(1) + .lengthLessThanOrEqual(1024) + .setValidationEnabled(isValidationEnabled); export const fieldInlinePredicate = s.boolean.optional; -export const embedFieldPredicate = s.object({ - name: fieldNamePredicate, - value: fieldValuePredicate, - inline: fieldInlinePredicate, -}); +export const embedFieldPredicate = s + .object({ + name: fieldNamePredicate, + value: fieldValuePredicate, + inline: fieldInlinePredicate, + }) + .setValidationEnabled(isValidationEnabled); -export const embedFieldsArrayPredicate = embedFieldPredicate.array; +export const embedFieldsArrayPredicate = embedFieldPredicate.array.setValidationEnabled(isValidationEnabled); -export const fieldLengthPredicate = s.number.lessThanOrEqual(25); +export const fieldLengthPredicate = s.number.lessThanOrEqual(25).setValidationEnabled(isValidationEnabled); export function validateFieldLength(amountAdding: number, fields?: APIEmbedField[]): void { fieldLengthPredicate.parse((fields?.length ?? 0) + amountAdding); } -export const authorNamePredicate = fieldNamePredicate.nullable; +export const authorNamePredicate = fieldNamePredicate.nullable.setValidationEnabled(isValidationEnabled); -export const imageURLPredicate = s.string.url({ - allowedProtocols: ['http:', 'https:', 'attachment:'], -}).nullish; +export const imageURLPredicate = s.string + .url({ + allowedProtocols: ['http:', 'https:', 'attachment:'], + }) + .nullish.setValidationEnabled(isValidationEnabled); -export const urlPredicate = s.string.url({ - allowedProtocols: ['http:', 'https:'], -}).nullish; +export const urlPredicate = s.string + .url({ + allowedProtocols: ['http:', 'https:'], + }) + .nullish.setValidationEnabled(isValidationEnabled); -export const embedAuthorPredicate = s.object({ - name: authorNamePredicate, - iconURL: imageURLPredicate, - url: urlPredicate, -}); +export const embedAuthorPredicate = s + .object({ + name: authorNamePredicate, + iconURL: imageURLPredicate, + url: urlPredicate, + }) + .setValidationEnabled(isValidationEnabled); -export const RGBPredicate = s.number.int.greaterThanOrEqual(0).lessThanOrEqual(255); +export const RGBPredicate = s.number.int + .greaterThanOrEqual(0) + .lessThanOrEqual(255) + .setValidationEnabled(isValidationEnabled); export const colorPredicate = s.number.int .greaterThanOrEqual(0) .lessThanOrEqual(0xffffff) - .or(s.tuple([RGBPredicate, RGBPredicate, RGBPredicate])).nullable; + .or(s.tuple([RGBPredicate, RGBPredicate, RGBPredicate])) + .nullable.setValidationEnabled(isValidationEnabled); -export const descriptionPredicate = s.string.lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(4096).nullable; +export const descriptionPredicate = s.string + .lengthGreaterThanOrEqual(1) + .lengthLessThanOrEqual(4096) + .nullable.setValidationEnabled(isValidationEnabled); -export const footerTextPredicate = s.string.lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(2048).nullable; +export const footerTextPredicate = s.string + .lengthGreaterThanOrEqual(1) + .lengthLessThanOrEqual(2048) + .nullable.setValidationEnabled(isValidationEnabled); -export const embedFooterPredicate = s.object({ - text: footerTextPredicate, - iconURL: imageURLPredicate, -}); +export const embedFooterPredicate = s + .object({ + text: footerTextPredicate, + iconURL: imageURLPredicate, + }) + .setValidationEnabled(isValidationEnabled); -export const timestampPredicate = s.union(s.number, s.date).nullable; +export const timestampPredicate = s.union(s.number, s.date).nullable.setValidationEnabled(isValidationEnabled); -export const titlePredicate = fieldNamePredicate.nullable; +export const titlePredicate = fieldNamePredicate.nullable.setValidationEnabled(isValidationEnabled); diff --git a/packages/builders/src/messages/embed/Embed.ts b/packages/builders/src/messages/embed/Embed.ts index 727fd71cf..d3221f920 100644 --- a/packages/builders/src/messages/embed/Embed.ts +++ b/packages/builders/src/messages/embed/Embed.ts @@ -1,4 +1,4 @@ -import type { APIEmbedField } from 'discord-api-types/v10'; +import type { APIEmbed, APIEmbedAuthor, APIEmbedField, APIEmbedFooter, APIEmbedImage } from 'discord-api-types/v10'; import { colorPredicate, descriptionPredicate, @@ -11,84 +11,228 @@ import { urlPredicate, validateFieldLength, } from './Assertions'; -import { EmbedAuthorOptions, EmbedFooterOptions, RGBTuple, UnsafeEmbedBuilder } from './UnsafeEmbed'; import { normalizeArray, type RestOrArray } from '../../util/normalizeArray'; +export type RGBTuple = [red: number, green: number, blue: number]; + +export interface IconData { + /** + * The URL of the icon + */ + iconURL?: string; + /** + * The proxy URL of the icon + */ + proxyIconURL?: string; +} + +export type EmbedAuthorData = Omit & IconData; + +export type EmbedAuthorOptions = Omit; + +export type EmbedFooterData = Omit & IconData; + +export type EmbedFooterOptions = Omit; + +export interface EmbedImageData extends Omit { + /** + * The proxy URL for the image + */ + proxyURL?: string; +} /** - * Represents a validated embed in a message (image/video preview, rich embed, etc.) + * Represents a embed in a message (image/video preview, rich embed, etc.) */ -export class EmbedBuilder extends UnsafeEmbedBuilder { - public override addFields(...fields: RestOrArray): this { +export class EmbedBuilder { + public readonly data: APIEmbed; + + public constructor(data: APIEmbed = {}) { + this.data = { ...data }; + if (data.timestamp) this.data.timestamp = new Date(data.timestamp).toISOString(); + } + + /** + * Adds fields to the embed (max 25) + * + * @param fields The fields to add + */ + public addFields(...fields: RestOrArray): this { fields = normalizeArray(fields); // Ensure adding these fields won't exceed the 25 field limit validateFieldLength(fields.length, this.data.fields); // Data assertions - return super.addFields(...embedFieldsArrayPredicate.parse(fields)); + embedFieldsArrayPredicate.parse(fields); + + if (this.data.fields) this.data.fields.push(...fields); + else this.data.fields = fields; + return this; } - public override spliceFields(index: number, deleteCount: number, ...fields: APIEmbedField[]): this { + /** + * Removes, replaces, or inserts fields in the embed (max 25) + * + * @param index The index to start at + * @param deleteCount The number of fields to remove + * @param fields The replacing field objects + */ + public spliceFields(index: number, deleteCount: number, ...fields: APIEmbedField[]): this { // Ensure adding these fields won't exceed the 25 field limit validateFieldLength(fields.length - deleteCount, this.data.fields); // Data assertions - return super.spliceFields(index, deleteCount, ...embedFieldsArrayPredicate.parse(fields)); + embedFieldsArrayPredicate.parse(fields); + if (this.data.fields) this.data.fields.splice(index, deleteCount, ...fields); + else this.data.fields = fields; + return this; } - public override setAuthor(options: EmbedAuthorOptions | null): this { + /** + * Sets the embed's fields (max 25). + * @param fields The fields to set + */ + public setFields(...fields: RestOrArray) { + this.spliceFields(0, this.data.fields?.length ?? 0, ...normalizeArray(fields)); + return this; + } + + /** + * Sets the author of this embed + * + * @param options The options for the author + */ + + public setAuthor(options: EmbedAuthorOptions | null): this { if (options === null) { - return super.setAuthor(null); + this.data.author = undefined; + return this; } // Data assertions embedAuthorPredicate.parse(options); - return super.setAuthor(options); + this.data.author = { name: options.name, url: options.url, icon_url: options.iconURL }; + return this; } - public override setColor(color: number | RGBTuple | null): this { + /** + * Sets the color of this embed + * + * @param color The color of the embed + */ + public setColor(color: number | RGBTuple | null): this { // Data assertions - return super.setColor(colorPredicate.parse(color)); + colorPredicate.parse(color); + + if (Array.isArray(color)) { + const [red, green, blue] = color; + this.data.color = (red << 16) + (green << 8) + blue; + return this; + } + this.data.color = color ?? undefined; + return this; } - public override setDescription(description: string | null): this { + /** + * Sets the description of this embed + * + * @param description The description + */ + public setDescription(description: string | null): this { // Data assertions - return super.setDescription(descriptionPredicate.parse(description)); + descriptionPredicate.parse(description); + + this.data.description = description ?? undefined; + return this; } - public override setFooter(options: EmbedFooterOptions | null): this { + /** + * Sets the footer of this embed + * + * @param options The options for the footer + */ + public setFooter(options: EmbedFooterOptions | null): this { if (options === null) { - return super.setFooter(null); + this.data.footer = undefined; + return this; } // Data assertions embedFooterPredicate.parse(options); - return super.setFooter(options); + this.data.footer = { text: options.text, icon_url: options.iconURL }; + return this; } - public override setImage(url: string | null): this { + /** + * Sets the image of this embed + * + * @param url The URL of the image + */ + public setImage(url: string | null): this { // Data assertions - return super.setImage(imageURLPredicate.parse(url)); + imageURLPredicate.parse(url); + + this.data.image = url ? { url } : undefined; + return this; } - public override setThumbnail(url: string | null): this { + /** + * Sets the thumbnail of this embed + * + * @param url The URL of the thumbnail + */ + public setThumbnail(url: string | null): this { // Data assertions - return super.setThumbnail(imageURLPredicate.parse(url)); + imageURLPredicate.parse(url); + + this.data.thumbnail = url ? { url } : undefined; + return this; } - public override setTimestamp(timestamp: number | Date | null = Date.now()): this { + /** + * Sets the timestamp of this embed + * + * @param timestamp The timestamp or date + */ + public setTimestamp(timestamp: number | Date | null = Date.now()): this { // Data assertions - return super.setTimestamp(timestampPredicate.parse(timestamp)); + timestampPredicate.parse(timestamp); + + this.data.timestamp = timestamp ? new Date(timestamp).toISOString() : undefined; + return this; } - public override setTitle(title: string | null): this { + /** + * Sets the title of this embed + * + * @param title The title + */ + public setTitle(title: string | null): this { // Data assertions - return super.setTitle(titlePredicate.parse(title)); + titlePredicate.parse(title); + + this.data.title = title ?? undefined; + return this; } - public override setURL(url: string | null): this { + /** + * Sets the URL of this embed + * + * @param url The URL + */ + public setURL(url: string | null): this { // Data assertions - return super.setURL(urlPredicate.parse(url)); + urlPredicate.parse(url); + + this.data.url = url ?? undefined; + return this; + } + + /** + * Transforms the embed to a plain object + */ + public toJSON(): APIEmbed { + return { ...this.data }; } } diff --git a/packages/builders/src/messages/embed/UnsafeEmbed.ts b/packages/builders/src/messages/embed/UnsafeEmbed.ts deleted file mode 100644 index f2ef5de34..000000000 --- a/packages/builders/src/messages/embed/UnsafeEmbed.ts +++ /dev/null @@ -1,189 +0,0 @@ -import type { APIEmbed, APIEmbedAuthor, APIEmbedField, APIEmbedFooter, APIEmbedImage } from 'discord-api-types/v10'; -import { normalizeArray, type RestOrArray } from '../../util/normalizeArray'; - -export type RGBTuple = [red: number, green: number, blue: number]; - -export interface IconData { - /** - * The URL of the icon - */ - iconURL?: string; - /** - * The proxy URL of the icon - */ - proxyIconURL?: string; -} - -export type EmbedAuthorData = Omit & IconData; - -export type EmbedAuthorOptions = Omit; - -export type EmbedFooterData = Omit & IconData; - -export type EmbedFooterOptions = Omit; - -export interface EmbedImageData extends Omit { - /** - * The proxy URL for the image - */ - proxyURL?: string; -} - -/** - * Represents a non-validated embed in a message (image/video preview, rich embed, etc.) - */ -export class UnsafeEmbedBuilder { - public readonly data: APIEmbed; - - public constructor(data: APIEmbed = {}) { - this.data = { ...data }; - if (data.timestamp) this.data.timestamp = new Date(data.timestamp).toISOString(); - } - - /** - * Adds fields to the embed (max 25) - * - * @param fields - The fields to add - */ - public addFields(...fields: RestOrArray): this { - fields = normalizeArray(fields); - if (this.data.fields) this.data.fields.push(...fields); - else this.data.fields = fields; - return this; - } - - /** - * Removes, replaces, or inserts fields in the embed (max 25) - * - * @param index - The index to start at - * @param deleteCount - The number of fields to remove - * @param fields - The replacing field objects - */ - public spliceFields(index: number, deleteCount: number, ...fields: APIEmbedField[]): this { - if (this.data.fields) this.data.fields.splice(index, deleteCount, ...fields); - else this.data.fields = fields; - return this; - } - - /** - * Sets the embed's fields (max 25). - * - * @param fields - The fields to set - */ - public setFields(...fields: RestOrArray) { - this.spliceFields(0, this.data.fields?.length ?? 0, ...normalizeArray(fields)); - return this; - } - - /** - * Sets the author of this embed - * - * @param options - The options for the author - */ - public setAuthor(options: EmbedAuthorOptions | null): this { - if (options === null) { - this.data.author = undefined; - return this; - } - - this.data.author = { name: options.name, url: options.url, icon_url: options.iconURL }; - return this; - } - - /** - * Sets the color of this embed - * - * @param color - The color of the embed - */ - public setColor(color: number | RGBTuple | null): this { - if (Array.isArray(color)) { - const [red, green, blue] = color; - this.data.color = (red << 16) + (green << 8) + blue; - return this; - } - this.data.color = color ?? undefined; - return this; - } - - /** - * Sets the description of this embed - * - * @param description - The description - */ - public setDescription(description: string | null): this { - this.data.description = description ?? undefined; - return this; - } - - /** - * Sets the footer of this embed - * - * @param options - The options for the footer - */ - public setFooter(options: EmbedFooterOptions | null): this { - if (options === null) { - this.data.footer = undefined; - return this; - } - - this.data.footer = { text: options.text, icon_url: options.iconURL }; - return this; - } - - /** - * Sets the image of this embed - * - * @param url - The URL of the image - */ - public setImage(url: string | null): this { - this.data.image = url ? { url } : undefined; - return this; - } - - /** - * Sets the thumbnail of this embed - * - * @param url - The URL of the thumbnail - */ - public setThumbnail(url: string | null): this { - this.data.thumbnail = url ? { url } : undefined; - return this; - } - - /** - * Sets the timestamp of this embed - * - * @param timestamp - The timestamp or date - */ - public setTimestamp(timestamp: number | Date | null = Date.now()): this { - this.data.timestamp = timestamp ? new Date(timestamp).toISOString() : undefined; - return this; - } - - /** - * Sets the title of this embed - * - * @param title - The title - */ - public setTitle(title: string | null): this { - this.data.title = title ?? undefined; - return this; - } - - /** - * Sets the URL of this embed - * - * @param url - The URL - */ - public setURL(url: string | null): this { - this.data.url = url ?? undefined; - return this; - } - - /** - * Transforms the embed to a plain object - */ - public toJSON(): APIEmbed { - return { ...this.data }; - } -} diff --git a/packages/builders/src/util/validation.ts b/packages/builders/src/util/validation.ts new file mode 100644 index 000000000..c2830f999 --- /dev/null +++ b/packages/builders/src/util/validation.ts @@ -0,0 +1,5 @@ +let validate = true; + +export const enableValidators = () => (validate = true); +export const disableValidators = () => (validate = false); +export const isValidationEnabled = () => validate; diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 7d4ef4779..a134711ec 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -17,7 +17,7 @@ import { roleMention, SelectMenuBuilder as BuilderSelectMenuComponent, TextInputBuilder as BuilderTextInputComponent, - UnsafeSelectMenuOptionBuilder as BuildersSelectMenuOption, + SelectMenuOptionBuilder as BuildersSelectMenuOption, spoiler, strikethrough, time, diff --git a/packages/discord.js/typings/index.test-d.ts b/packages/discord.js/typings/index.test-d.ts index a0e77af9d..8c79b1f01 100644 --- a/packages/discord.js/typings/index.test-d.ts +++ b/packages/discord.js/typings/index.test-d.ts @@ -132,13 +132,7 @@ import { ShardEvents, } from '.'; import { expectAssignable, expectNotAssignable, expectNotType, expectType } from 'tsd'; -import { - ContextMenuCommandBuilder, - SlashCommandBuilder, - UnsafeButtonBuilder, - UnsafeEmbedBuilder, - UnsafeSelectMenuBuilder, -} from '@discordjs/builders'; +import type { ContextMenuCommandBuilder, SlashCommandBuilder } from '@discordjs/builders'; // Test type transformation: declare const serialize: (value: T) => Serialized; @@ -879,7 +873,6 @@ client.on('messageCreate', async message => { type: ComponentType.ActionRow, components: [ new ButtonBuilder(), - new UnsafeButtonBuilder(), { type: ComponentType.Button, label: 'test', style: ButtonStyle.Primary, customId: 'test' }, { type: ComponentType.Button, @@ -893,7 +886,6 @@ client.on('messageCreate', async message => { type: ComponentType.ActionRow, components: [ new SelectMenuBuilder(), - new UnsafeSelectMenuBuilder(), { type: ComponentType.SelectMenu, label: 'select menu', @@ -903,9 +895,8 @@ client.on('messageCreate', async message => { ], }; - const buildersEmbed = new UnsafeEmbedBuilder(); const embedData = { description: 'test', color: 0xff0000 }; - channel.send({ components: [row, buttonsRow, selectsRow], embeds: [embed, buildersEmbed, embedData] }); + channel.send({ components: [row, buttonsRow, selectsRow], embeds: [embed, embedData] }); }); client.on('threadCreate', thread => {