mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-17 03:53:29 +01:00
refactor: Don't return builders from API data (#7584)
* refactor: don't return builders from API data * Update packages/discord.js/src/structures/ActionRow.js Co-authored-by: Antonio Román <kyradiscord@gmail.com> * fix: circular dependency * fix: circular dependency pt.2 * chore: make requested changes * chore: bump dapi-types * chore: convert text input * chore: convert text input * feat: handle cases of unknown component types better * refactor: refactor modal to builder * feat: add #from for easy builder conversions * refactor: make requested changes * chore: make requested changes * style: fix linting error Co-authored-by: Antonio Román <kyradiscord@gmail.com> Co-authored-by: almeidx <almeidx@pm.me>
This commit is contained in:
@@ -1,26 +1,29 @@
|
||||
import {
|
||||
APIActionRowComponent,
|
||||
type APIActionRowComponent,
|
||||
ComponentType,
|
||||
APIMessageActionRowComponent,
|
||||
APIModalActionRowComponent,
|
||||
ComponentType,
|
||||
} from 'discord-api-types/v9';
|
||||
import type { ButtonComponent, SelectMenuComponent, TextInputComponent } from '../index';
|
||||
import { Component } from './Component';
|
||||
import { createComponent } from './Components';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import type { ButtonBuilder, SelectMenuBuilder, TextInputBuilder } from '..';
|
||||
import { ComponentBuilder } from './Component';
|
||||
import { createComponentBuilder } from './Components';
|
||||
|
||||
export type MessageComponent = MessageActionRowComponent | ActionRow<MessageActionRowComponent>;
|
||||
export type ModalComponent = ModalActionRowComponent | ActionRow<ModalActionRowComponent>;
|
||||
export type MessageComponentBuilder =
|
||||
| MessageActionRowComponentBuilder
|
||||
| ActionRowBuilder<MessageActionRowComponentBuilder>;
|
||||
export type ModalComponentBuilder = ModalActionRowComponentBuilder | ActionRowBuilder<ModalActionRowComponentBuilder>;
|
||||
|
||||
export type MessageActionRowComponent = ButtonComponent | SelectMenuComponent;
|
||||
export type ModalActionRowComponent = TextInputComponent;
|
||||
export type MessageActionRowComponentBuilder = ButtonBuilder | SelectMenuBuilder;
|
||||
export type ModalActionRowComponentBuilder = TextInputBuilder;
|
||||
|
||||
/**
|
||||
* Represents an action row component
|
||||
*/
|
||||
export class ActionRow<
|
||||
T extends ModalActionRowComponent | MessageActionRowComponent = ModalActionRowComponent | MessageActionRowComponent,
|
||||
> extends Component<
|
||||
export class ActionRowBuilder<
|
||||
T extends MessageActionRowComponentBuilder | ModalActionRowComponentBuilder =
|
||||
| MessageActionRowComponentBuilder
|
||||
| ModalActionRowComponentBuilder,
|
||||
> extends ComponentBuilder<
|
||||
Omit<
|
||||
Partial<APIActionRowComponent<APIMessageActionRowComponent | APIModalActionRowComponent>> & {
|
||||
type: ComponentType.ActionRow;
|
||||
@@ -31,14 +34,14 @@ export class ActionRow<
|
||||
/**
|
||||
* The components within this action row
|
||||
*/
|
||||
public readonly components: T[];
|
||||
private readonly components: T[];
|
||||
|
||||
public constructor({
|
||||
components,
|
||||
...data
|
||||
}: Partial<APIActionRowComponent<APIMessageActionRowComponent | APIModalActionRowComponent>> = {}) {
|
||||
super({ type: ComponentType.ActionRow, ...data });
|
||||
this.components = (components?.map((c) => createComponent(c)) ?? []) as T[];
|
||||
this.components = (components?.map((c) => createComponentBuilder(c)) ?? []) as T[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,14 +69,4 @@ export class ActionRow<
|
||||
components: this.components.map((component) => component.toJSON()) as ReturnType<T['toJSON']>[],
|
||||
};
|
||||
}
|
||||
|
||||
public equals(other: APIActionRowComponent<APIMessageActionRowComponent | APIModalActionRowComponent> | ActionRow) {
|
||||
if (other instanceof ActionRow) {
|
||||
return isEqual(other.data, this.data) && isEqual(other.components, this.components);
|
||||
}
|
||||
return isEqual(other, {
|
||||
...this.data,
|
||||
components: this.components.map((component) => component.toJSON()),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { APIMessageComponentEmoji, ButtonStyle } from 'discord-api-types/v9';
|
||||
import { z } from 'zod';
|
||||
import type { SelectMenuOption } from './selectMenu/SelectMenuOption';
|
||||
import type { SelectMenuOptionBuilder } from './selectMenu/SelectMenuOption';
|
||||
|
||||
export const customIdValidator = z.string().min(1).max(100);
|
||||
|
||||
@@ -24,7 +24,7 @@ export const minMaxValidator = z.number().int().min(0).max(25);
|
||||
|
||||
export const optionsValidator = z.object({}).array().nonempty();
|
||||
|
||||
export function validateRequiredSelectMenuParameters(options: SelectMenuOption[], customId?: string) {
|
||||
export function validateRequiredSelectMenuParameters(options: SelectMenuOptionBuilder[], customId?: string) {
|
||||
customIdValidator.parse(customId);
|
||||
optionsValidator.parse(options);
|
||||
}
|
||||
|
||||
@@ -4,17 +4,16 @@ import type {
|
||||
APIActionRowComponentTypes,
|
||||
APIBaseComponent,
|
||||
APIMessageActionRowComponent,
|
||||
APIModalActionRowComponent,
|
||||
APIMessageComponent,
|
||||
ComponentType,
|
||||
APIModalActionRowComponent,
|
||||
APIModalComponent,
|
||||
ComponentType,
|
||||
} from 'discord-api-types/v9';
|
||||
import type { Equatable } from '../util/equatable';
|
||||
|
||||
/**
|
||||
* Represents a discord component
|
||||
*/
|
||||
export abstract class Component<
|
||||
export abstract class ComponentBuilder<
|
||||
DataType extends Partial<APIBaseComponent<ComponentType>> & {
|
||||
type: ComponentType;
|
||||
} = APIBaseComponent<ComponentType>,
|
||||
@@ -23,11 +22,6 @@ export abstract class Component<
|
||||
| APIModalComponent
|
||||
| APIMessageComponent
|
||||
| APIActionRowComponent<APIModalActionRowComponent | APIMessageActionRowComponent>
|
||||
>,
|
||||
Equatable<
|
||||
| Component
|
||||
| APIActionRowComponentTypes
|
||||
| APIActionRowComponent<APIModalActionRowComponent | APIMessageActionRowComponent>
|
||||
>
|
||||
{
|
||||
/**
|
||||
@@ -39,21 +33,7 @@ export abstract class Component<
|
||||
| APIActionRowComponentTypes
|
||||
| APIActionRowComponent<APIModalActionRowComponent | APIMessageActionRowComponent>;
|
||||
|
||||
public abstract equals(
|
||||
other:
|
||||
| Component
|
||||
| APIActionRowComponentTypes
|
||||
| APIActionRowComponent<APIModalActionRowComponent | APIMessageActionRowComponent>,
|
||||
): boolean;
|
||||
|
||||
public constructor(data: DataType) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of this component
|
||||
*/
|
||||
public get type(): DataType['type'] {
|
||||
return this.data.type;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,41 @@
|
||||
import { APIBaseComponent, APIMessageComponent, APIModalComponent, ComponentType } from 'discord-api-types/v9';
|
||||
import { ActionRow, ButtonComponent, Component, SelectMenuComponent, TextInputComponent } from '../index';
|
||||
import type { MessageComponent, ModalActionRowComponent } from './ActionRow';
|
||||
import { APIMessageComponent, APIModalComponent, ComponentType } from 'discord-api-types/v9';
|
||||
import { ActionRowBuilder, ButtonBuilder, ComponentBuilder, SelectMenuBuilder, TextInputBuilder } from '../index';
|
||||
import type { MessageComponentBuilder, ModalComponentBuilder } from './ActionRow';
|
||||
|
||||
export interface MappedComponentTypes {
|
||||
[ComponentType.ActionRow]: ActionRow;
|
||||
[ComponentType.Button]: ButtonComponent;
|
||||
[ComponentType.SelectMenu]: SelectMenuComponent;
|
||||
[ComponentType.TextInput]: TextInputComponent;
|
||||
[ComponentType.ActionRow]: ActionRowBuilder;
|
||||
[ComponentType.Button]: ButtonBuilder;
|
||||
[ComponentType.SelectMenu]: SelectMenuBuilder;
|
||||
[ComponentType.TextInput]: TextInputBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory for creating components from API data
|
||||
* @param data The api data to transform to a component class
|
||||
*/
|
||||
export function createComponent<T extends keyof MappedComponentTypes>(
|
||||
export function createComponentBuilder<T extends keyof MappedComponentTypes>(
|
||||
data: (APIMessageComponent | APIModalComponent) & { type: T },
|
||||
): MappedComponentTypes[T];
|
||||
export function createComponent<C extends MessageComponent | ModalActionRowComponent>(data: C): C;
|
||||
export function createComponent(data: APIModalComponent | APIMessageComponent | Component): Component {
|
||||
if (data instanceof Component) {
|
||||
export function createComponentBuilder<C extends MessageComponentBuilder | ModalComponentBuilder>(data: C): C;
|
||||
export function createComponentBuilder(
|
||||
data: APIMessageComponent | APIModalComponent | MessageComponentBuilder,
|
||||
): ComponentBuilder {
|
||||
if (data instanceof ComponentBuilder) {
|
||||
return data;
|
||||
}
|
||||
|
||||
switch (data.type) {
|
||||
case ComponentType.ActionRow:
|
||||
return new ActionRow(data);
|
||||
return new ActionRowBuilder(data);
|
||||
case ComponentType.Button:
|
||||
return new ButtonComponent(data);
|
||||
return new ButtonBuilder(data);
|
||||
case ComponentType.SelectMenu:
|
||||
return new SelectMenuComponent(data);
|
||||
return new SelectMenuBuilder(data);
|
||||
case ComponentType.TextInput:
|
||||
return new TextInputComponent(data);
|
||||
return new TextInputBuilder(data);
|
||||
default:
|
||||
throw new Error(`Cannot serialize component type: ${(data as APIBaseComponent<ComponentType>).type}`);
|
||||
// @ts-expect-error
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
throw new Error(`Cannot properly serialize component type: ${data.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import type { ButtonStyle, APIMessageComponentEmoji, APIButtonComponent } from 'discord-api-types/v9';
|
||||
import type {
|
||||
ButtonStyle,
|
||||
APIMessageComponentEmoji,
|
||||
APIButtonComponent,
|
||||
APIButtonComponentWithCustomId,
|
||||
APIButtonComponentWithURL,
|
||||
} from 'discord-api-types/v9';
|
||||
import {
|
||||
buttonLabelValidator,
|
||||
buttonStyleValidator,
|
||||
@@ -8,12 +14,12 @@ import {
|
||||
urlValidator,
|
||||
validateRequiredButtonParameters,
|
||||
} from '../Assertions';
|
||||
import { UnsafeButtonComponent } from './UnsafeButton';
|
||||
import { UnsafeButtonBuilder } from './UnsafeButton';
|
||||
|
||||
/**
|
||||
* Represents a validated button component
|
||||
*/
|
||||
export class ButtonComponent extends UnsafeButtonComponent {
|
||||
export class ButtonBuilder extends UnsafeButtonBuilder {
|
||||
public override setStyle(style: ButtonStyle) {
|
||||
return super.setStyle(buttonStyleValidator.parse(style));
|
||||
}
|
||||
@@ -39,7 +45,13 @@ export class ButtonComponent extends UnsafeButtonComponent {
|
||||
}
|
||||
|
||||
public override toJSON(): APIButtonComponent {
|
||||
validateRequiredButtonParameters(this.style, this.label, this.emoji, this.customId, this.url);
|
||||
validateRequiredButtonParameters(
|
||||
this.data.style,
|
||||
this.data.label,
|
||||
this.data.emoji,
|
||||
(this.data as APIButtonComponentWithCustomId).custom_id,
|
||||
(this.data as APIButtonComponentWithURL).url,
|
||||
);
|
||||
return super.toJSON();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,59 +6,18 @@ import {
|
||||
type APIButtonComponentWithURL,
|
||||
type APIButtonComponentWithCustomId,
|
||||
} from 'discord-api-types/v9';
|
||||
import { Component } from '../Component';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import { ComponentBuilder } from '../Component';
|
||||
|
||||
/**
|
||||
* Represents a non-validated button component
|
||||
*/
|
||||
export class UnsafeButtonComponent extends Component<Partial<APIButtonComponent> & { type: ComponentType.Button }> {
|
||||
export class UnsafeButtonBuilder extends ComponentBuilder<
|
||||
Partial<APIButtonComponent> & { type: ComponentType.Button }
|
||||
> {
|
||||
public constructor(data?: Partial<APIButtonComponent>) {
|
||||
super({ type: ComponentType.Button, ...data });
|
||||
}
|
||||
|
||||
/**
|
||||
* The style of this button
|
||||
*/
|
||||
public get style() {
|
||||
return this.data.style;
|
||||
}
|
||||
|
||||
/**
|
||||
* The label of this button
|
||||
*/
|
||||
public get label() {
|
||||
return this.data.label;
|
||||
}
|
||||
|
||||
/**
|
||||
* The emoji used in this button
|
||||
*/
|
||||
public get emoji() {
|
||||
return this.data.emoji;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this button is disabled
|
||||
*/
|
||||
public get disabled() {
|
||||
return this.data.disabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* The custom id of this button (only defined on non-link buttons)
|
||||
*/
|
||||
public get customId(): string | undefined {
|
||||
return (this.data as APIButtonComponentWithCustomId).custom_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* The URL of this button (only defined on link buttons)
|
||||
*/
|
||||
public get url(): string | undefined {
|
||||
return (this.data as APIButtonComponentWithURL).url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the style of this button
|
||||
* @param style The style of the button
|
||||
@@ -119,11 +78,4 @@ export class UnsafeButtonComponent extends Component<Partial<APIButtonComponent>
|
||||
...this.data,
|
||||
} as APIButtonComponent;
|
||||
}
|
||||
|
||||
public equals(other: APIButtonComponent | UnsafeButtonComponent) {
|
||||
if (other instanceof UnsafeButtonComponent) {
|
||||
return isEqual(other.data, this.data);
|
||||
}
|
||||
return isEqual(other, this.data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,12 @@ import {
|
||||
placeholderValidator,
|
||||
validateRequiredSelectMenuParameters,
|
||||
} from '../Assertions';
|
||||
import { UnsafeSelectMenuComponent } from './UnsafeSelectMenu';
|
||||
import { UnsafeSelectMenuBuilder } from './UnsafeSelectMenu';
|
||||
|
||||
/**
|
||||
* Represents a validated select menu component
|
||||
*/
|
||||
export class SelectMenuComponent extends UnsafeSelectMenuComponent {
|
||||
export class SelectMenuBuilder extends UnsafeSelectMenuBuilder {
|
||||
public override setPlaceholder(placeholder: string) {
|
||||
return super.setPlaceholder(placeholderValidator.parse(placeholder));
|
||||
}
|
||||
@@ -33,7 +33,7 @@ export class SelectMenuComponent extends UnsafeSelectMenuComponent {
|
||||
}
|
||||
|
||||
public override toJSON(): APISelectMenuComponent {
|
||||
validateRequiredSelectMenuParameters(this.options, this.customId);
|
||||
validateRequiredSelectMenuParameters(this.options, this.data.custom_id);
|
||||
return super.toJSON();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,12 @@ import {
|
||||
labelValueValidator,
|
||||
validateRequiredSelectMenuOptionParameters,
|
||||
} from '../Assertions';
|
||||
import { UnsafeSelectMenuOption } from './UnsafeSelectMenuOption';
|
||||
import { UnsafeSelectMenuOptionBuilder } from './UnsafeSelectMenuOption';
|
||||
|
||||
/**
|
||||
* Represents a validated option within a select menu component
|
||||
*/
|
||||
export class SelectMenuOption extends UnsafeSelectMenuOption {
|
||||
export class SelectMenuOptionBuilder extends UnsafeSelectMenuOptionBuilder {
|
||||
public override setDescription(description: string) {
|
||||
return super.setDescription(labelValueValidator.parse(description));
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export class SelectMenuOption extends UnsafeSelectMenuOption {
|
||||
}
|
||||
|
||||
public override toJSON(): APISelectMenuOption {
|
||||
validateRequiredSelectMenuOptionParameters(this.label, this.value);
|
||||
validateRequiredSelectMenuOptionParameters(this.data.label, this.data.value);
|
||||
return super.toJSON();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,58 +1,22 @@
|
||||
import { APISelectMenuOption, ComponentType, type APISelectMenuComponent } from 'discord-api-types/v9';
|
||||
import { Component } from '../Component';
|
||||
import { UnsafeSelectMenuOption } from './UnsafeSelectMenuOption';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import { ComponentBuilder } from '../Component';
|
||||
import { UnsafeSelectMenuOptionBuilder } from './UnsafeSelectMenuOption';
|
||||
|
||||
/**
|
||||
* Represents a non-validated select menu component
|
||||
*/
|
||||
export class UnsafeSelectMenuComponent extends Component<
|
||||
export class UnsafeSelectMenuBuilder extends ComponentBuilder<
|
||||
Partial<Omit<APISelectMenuComponent, 'options'>> & { type: ComponentType.SelectMenu }
|
||||
> {
|
||||
/**
|
||||
* The options within this select menu
|
||||
*/
|
||||
public readonly options: UnsafeSelectMenuOption[];
|
||||
protected readonly options: UnsafeSelectMenuOptionBuilder[];
|
||||
|
||||
public constructor(data?: Partial<APISelectMenuComponent>) {
|
||||
const { options, ...initData } = data ?? {};
|
||||
super({ type: ComponentType.SelectMenu, ...initData });
|
||||
this.options = options?.map((o) => new UnsafeSelectMenuOption(o)) ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* The placeholder for this select menu
|
||||
*/
|
||||
public get placeholder() {
|
||||
return this.data.placeholder;
|
||||
}
|
||||
|
||||
/**
|
||||
* The maximum amount of options that can be selected
|
||||
*/
|
||||
public get maxValues() {
|
||||
return this.data.max_values;
|
||||
}
|
||||
|
||||
/**
|
||||
* The minimum amount of options that must be selected
|
||||
*/
|
||||
public get minValues() {
|
||||
return this.data.min_values;
|
||||
}
|
||||
|
||||
/**
|
||||
* The custom id of this select menu
|
||||
*/
|
||||
public get customId() {
|
||||
return this.data.custom_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this select menu is disabled
|
||||
*/
|
||||
public get disabled() {
|
||||
return this.data.disabled;
|
||||
this.options = options?.map((o) => new UnsafeSelectMenuOptionBuilder(o)) ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -105,10 +69,10 @@ export class UnsafeSelectMenuComponent extends Component<
|
||||
* @param options The options to add to this select menu
|
||||
* @returns
|
||||
*/
|
||||
public addOptions(...options: (UnsafeSelectMenuOption | APISelectMenuOption)[]) {
|
||||
public addOptions(...options: (UnsafeSelectMenuOptionBuilder | APISelectMenuOption)[]) {
|
||||
this.options.push(
|
||||
...options.map((option) =>
|
||||
option instanceof UnsafeSelectMenuOption ? option : new UnsafeSelectMenuOption(option),
|
||||
option instanceof UnsafeSelectMenuOptionBuilder ? option : new UnsafeSelectMenuOptionBuilder(option),
|
||||
),
|
||||
);
|
||||
return this;
|
||||
@@ -118,12 +82,12 @@ export class UnsafeSelectMenuComponent extends Component<
|
||||
* Sets the options on this select menu
|
||||
* @param options The options to set on this select menu
|
||||
*/
|
||||
public setOptions(...options: (UnsafeSelectMenuOption | APISelectMenuOption)[]) {
|
||||
public setOptions(...options: (UnsafeSelectMenuOptionBuilder | APISelectMenuOption)[]) {
|
||||
this.options.splice(
|
||||
0,
|
||||
this.options.length,
|
||||
...options.map((option) =>
|
||||
option instanceof UnsafeSelectMenuOption ? option : new UnsafeSelectMenuOption(option),
|
||||
option instanceof UnsafeSelectMenuOptionBuilder ? option : new UnsafeSelectMenuOptionBuilder(option),
|
||||
),
|
||||
);
|
||||
return this;
|
||||
@@ -136,14 +100,4 @@ export class UnsafeSelectMenuComponent extends Component<
|
||||
options: this.options.map((o) => o.toJSON()),
|
||||
} as APISelectMenuComponent;
|
||||
}
|
||||
|
||||
public equals(other: APISelectMenuComponent | UnsafeSelectMenuComponent): boolean {
|
||||
if (other instanceof UnsafeSelectMenuComponent) {
|
||||
return isEqual(other.data, this.data) && isEqual(other.options, this.options);
|
||||
}
|
||||
return isEqual(other, {
|
||||
...this.data,
|
||||
options: this.options.map((o) => o.toJSON()),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,43 +3,8 @@ import type { APIMessageComponentEmoji, APISelectMenuOption } from 'discord-api-
|
||||
/**
|
||||
* Represents a non-validated option within a select menu component
|
||||
*/
|
||||
export class UnsafeSelectMenuOption {
|
||||
public constructor(protected data: Partial<APISelectMenuOption> = {}) {}
|
||||
|
||||
/**
|
||||
* The label for this option
|
||||
*/
|
||||
public get label() {
|
||||
return this.data.label;
|
||||
}
|
||||
|
||||
/**
|
||||
* The value for this option
|
||||
*/
|
||||
public get value() {
|
||||
return this.data.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* The description for this option
|
||||
*/
|
||||
public get description() {
|
||||
return this.data.description;
|
||||
}
|
||||
|
||||
/**
|
||||
* The emoji for this option
|
||||
*/
|
||||
public get emoji() {
|
||||
return this.data.emoji;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this option is selected by default
|
||||
*/
|
||||
public get default() {
|
||||
return this.data.default;
|
||||
}
|
||||
export class UnsafeSelectMenuOptionBuilder {
|
||||
public constructor(public data: Partial<APISelectMenuOption> = {}) {}
|
||||
|
||||
/**
|
||||
* Sets the label of this option
|
||||
|
||||
@@ -7,9 +7,9 @@ import {
|
||||
valueValidator,
|
||||
validateRequiredParameters,
|
||||
} from './Assertions';
|
||||
import { UnsafeTextInputComponent } from './UnsafeTextInput';
|
||||
import { UnsafeTextInputBuilder } from './UnsafeTextInput';
|
||||
|
||||
export class TextInputComponent extends UnsafeTextInputComponent {
|
||||
export class TextInputBuilder extends UnsafeTextInputBuilder {
|
||||
public override setMinLength(minLength: number) {
|
||||
return super.setMinLength(minLengthValidator.parse(minLength));
|
||||
}
|
||||
|
||||
@@ -1,73 +1,17 @@
|
||||
import { ComponentType, type TextInputStyle, type APITextInputComponent } from 'discord-api-types/v9';
|
||||
import { Component } from '../../index';
|
||||
import { ComponentBuilder } from '../../index';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
|
||||
export class UnsafeTextInputComponent extends Component<
|
||||
export class UnsafeTextInputBuilder extends ComponentBuilder<
|
||||
Partial<APITextInputComponent> & { type: ComponentType.TextInput }
|
||||
> {
|
||||
public constructor(data?: APITextInputComponent & { type?: ComponentType.TextInput }) {
|
||||
super({ type: ComponentType.TextInput, ...data });
|
||||
}
|
||||
|
||||
/**
|
||||
* The style of this text input
|
||||
*/
|
||||
public get style() {
|
||||
return this.data.style;
|
||||
}
|
||||
|
||||
/**
|
||||
* The custom id of this text input
|
||||
*/
|
||||
public get customId() {
|
||||
return this.data.custom_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* The label for this text input
|
||||
*/
|
||||
public get label() {
|
||||
return this.data.label;
|
||||
}
|
||||
|
||||
/**
|
||||
* The placeholder text for this text input
|
||||
*/
|
||||
public get placeholder() {
|
||||
return this.data.placeholder;
|
||||
}
|
||||
|
||||
/**
|
||||
* The default value for this text input
|
||||
*/
|
||||
public get value() {
|
||||
return this.data.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* The minimum length of this text input
|
||||
*/
|
||||
public get minLength() {
|
||||
return this.data.min_length;
|
||||
}
|
||||
|
||||
/**
|
||||
* The maximum length of this text input
|
||||
*/
|
||||
public get maxLength() {
|
||||
return this.data.max_length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this text input is required
|
||||
*/
|
||||
public get required() {
|
||||
return this.data.required;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the custom id for this text input
|
||||
* @param customId The custom id of this text input
|
||||
* @param customId The custom id of this text inputå
|
||||
*/
|
||||
public setCustomId(customId: string) {
|
||||
this.data.custom_id = customId;
|
||||
@@ -144,8 +88,8 @@ export class UnsafeTextInputComponent extends Component<
|
||||
} as APITextInputComponent;
|
||||
}
|
||||
|
||||
public equals(other: UnsafeTextInputComponent | APITextInputComponent): boolean {
|
||||
if (other instanceof UnsafeTextInputComponent) {
|
||||
public equals(other: UnsafeTextInputBuilder | APITextInputComponent): boolean {
|
||||
if (other instanceof UnsafeTextInputBuilder) {
|
||||
return isEqual(other.data, this.data);
|
||||
}
|
||||
|
||||
|
||||
@@ -37,3 +37,5 @@ export * as ContextMenuCommandAssertions from './interactions/contextMenuCommand
|
||||
export * from './interactions/contextMenuCommands/ContextMenuCommandBuilder';
|
||||
|
||||
export * from './util/jsonEncodable';
|
||||
export * from './util/equatable';
|
||||
export * from './util/componentUtil';
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { z } from 'zod';
|
||||
import { ActionRow, type ModalActionRowComponent } from '../..';
|
||||
import { ActionRowBuilder, type ModalActionRowComponentBuilder } from '../..';
|
||||
import { customIdValidator } from '../../components/Assertions';
|
||||
|
||||
export const titleValidator = z.string().min(1).max(45);
|
||||
export const componentsValidator = z.array(z.instanceof(ActionRow)).min(1);
|
||||
export const componentsValidator = z.array(z.instanceof(ActionRowBuilder)).min(1);
|
||||
|
||||
export function validateRequiredParameters(
|
||||
customId?: string,
|
||||
title?: string,
|
||||
components?: ActionRow<ModalActionRowComponent>[],
|
||||
components?: ActionRowBuilder<ModalActionRowComponentBuilder>[],
|
||||
) {
|
||||
customIdValidator.parse(customId);
|
||||
titleValidator.parse(title);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { APIModalInteractionResponseCallbackData } from 'discord-api-types/v9';
|
||||
import { customIdValidator } from '../../components/Assertions';
|
||||
import { titleValidator, validateRequiredParameters } from './Assertions';
|
||||
import { UnsafeModal } from './UnsafeModal';
|
||||
import { UnsafeModalBuilder } from './UnsafeModal';
|
||||
|
||||
export class Modal extends UnsafeModal {
|
||||
export class ModalBuilder extends UnsafeModalBuilder {
|
||||
public override setCustomId(customId: string): this {
|
||||
return super.setCustomId(customIdValidator.parse(customId));
|
||||
}
|
||||
|
||||
@@ -3,29 +3,16 @@ import type {
|
||||
APIModalActionRowComponent,
|
||||
APIModalInteractionResponseCallbackData,
|
||||
} from 'discord-api-types/v9';
|
||||
import { ActionRow, createComponent, JSONEncodable, ModalActionRowComponent } from '../../index';
|
||||
import { ActionRowBuilder, createComponentBuilder, JSONEncodable, ModalActionRowComponentBuilder } from '../../index';
|
||||
|
||||
export class UnsafeModal implements JSONEncodable<APIModalInteractionResponseCallbackData> {
|
||||
export class UnsafeModalBuilder implements JSONEncodable<APIModalInteractionResponseCallbackData> {
|
||||
protected readonly data: Partial<Omit<APIModalInteractionResponseCallbackData, 'components'>>;
|
||||
public readonly components: ActionRow<ModalActionRowComponent>[] = [];
|
||||
public readonly components: ActionRowBuilder<ModalActionRowComponentBuilder>[] = [];
|
||||
|
||||
public constructor({ components, ...data }: Partial<APIModalInteractionResponseCallbackData> = {}) {
|
||||
this.data = { ...data };
|
||||
this.components = (components?.map((c) => createComponent(c)) ?? []) as ActionRow<ModalActionRowComponent>[];
|
||||
}
|
||||
|
||||
/**
|
||||
* The custom id of this modal
|
||||
*/
|
||||
public get customId() {
|
||||
return this.data.custom_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* The title of this modal
|
||||
*/
|
||||
public get title() {
|
||||
return this.data.title;
|
||||
this.components = (components?.map((c) => createComponentBuilder(c)) ??
|
||||
[]) as ActionRowBuilder<ModalActionRowComponentBuilder>[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,11 +38,16 @@ export class UnsafeModal implements JSONEncodable<APIModalInteractionResponseCal
|
||||
* @param components The components to add to this modal
|
||||
*/
|
||||
public addComponents(
|
||||
...components: (ActionRow<ModalActionRowComponent> | APIActionRowComponent<APIModalActionRowComponent>)[]
|
||||
...components: (
|
||||
| ActionRowBuilder<ModalActionRowComponentBuilder>
|
||||
| APIActionRowComponent<APIModalActionRowComponent>
|
||||
)[]
|
||||
) {
|
||||
this.components.push(
|
||||
...components.map((component) =>
|
||||
component instanceof ActionRow ? component : new ActionRow<ModalActionRowComponent>(component),
|
||||
component instanceof ActionRowBuilder
|
||||
? component
|
||||
: new ActionRowBuilder<ModalActionRowComponentBuilder>(component),
|
||||
),
|
||||
);
|
||||
return this;
|
||||
@@ -65,7 +57,7 @@ export class UnsafeModal implements JSONEncodable<APIModalInteractionResponseCal
|
||||
* Sets the components in this modal
|
||||
* @param components The components to set this modal to
|
||||
*/
|
||||
public setComponents(...components: ActionRow<ModalActionRowComponent>[]) {
|
||||
public setComponents(...components: ActionRowBuilder<ModalActionRowComponentBuilder>[]) {
|
||||
this.components.splice(0, this.components.length, ...components);
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -10,15 +10,15 @@ import {
|
||||
urlPredicate,
|
||||
validateFieldLength,
|
||||
} from './Assertions';
|
||||
import { EmbedAuthorOptions, EmbedFooterOptions, RGBTuple, UnsafeEmbed } from './UnsafeEmbed';
|
||||
import { EmbedAuthorOptions, EmbedFooterOptions, RGBTuple, UnsafeEmbedBuilder } from './UnsafeEmbed';
|
||||
|
||||
/**
|
||||
* Represents a validated embed in a message (image/video preview, rich embed, etc.)
|
||||
*/
|
||||
export class Embed extends UnsafeEmbed {
|
||||
export class EmbedBuilder extends UnsafeEmbedBuilder {
|
||||
public override addFields(...fields: APIEmbedField[]): this {
|
||||
// Ensure adding these fields won't exceed the 25 field limit
|
||||
validateFieldLength(fields.length, this.fields);
|
||||
validateFieldLength(fields.length, this.data.fields);
|
||||
|
||||
// Data assertions
|
||||
return super.addFields(...embedFieldsArrayPredicate.parse(fields));
|
||||
@@ -26,7 +26,7 @@ export class Embed extends UnsafeEmbed {
|
||||
|
||||
public override spliceFields(index: number, deleteCount: number, ...fields: APIEmbedField[]): this {
|
||||
// Ensure adding these fields won't exceed the 25 field limit
|
||||
validateFieldLength(fields.length - deleteCount, this.fields);
|
||||
validateFieldLength(fields.length - deleteCount, this.data.fields);
|
||||
|
||||
// Data assertions
|
||||
return super.spliceFields(index, deleteCount, ...embedFieldsArrayPredicate.parse(fields));
|
||||
|
||||
@@ -1,13 +1,4 @@
|
||||
import type {
|
||||
APIEmbed,
|
||||
APIEmbedAuthor,
|
||||
APIEmbedField,
|
||||
APIEmbedFooter,
|
||||
APIEmbedImage,
|
||||
APIEmbedVideo,
|
||||
} from 'discord-api-types/v9';
|
||||
import type { Equatable } from '../../util/equatable';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import type { APIEmbed, APIEmbedAuthor, APIEmbedField, APIEmbedFooter, APIEmbedImage } from 'discord-api-types/v9';
|
||||
|
||||
export type RGBTuple = [red: number, green: number, blue: number];
|
||||
|
||||
@@ -40,7 +31,7 @@ export interface EmbedImageData extends Omit<APIEmbedImage, 'proxy_url'> {
|
||||
/**
|
||||
* Represents a non-validated embed in a message (image/video preview, rich embed, etc.)
|
||||
*/
|
||||
export class UnsafeEmbed implements Equatable<APIEmbed | UnsafeEmbed> {
|
||||
export class UnsafeEmbedBuilder {
|
||||
public readonly data: APIEmbed;
|
||||
|
||||
public constructor(data: APIEmbed = {}) {
|
||||
@@ -48,133 +39,6 @@ export class UnsafeEmbed implements Equatable<APIEmbed | UnsafeEmbed> {
|
||||
if (data.timestamp) this.data.timestamp = new Date(data.timestamp).toISOString();
|
||||
}
|
||||
|
||||
/**
|
||||
* An array of fields of this embed
|
||||
*/
|
||||
public get fields() {
|
||||
return this.data.fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* The embed title
|
||||
*/
|
||||
public get title() {
|
||||
return this.data.title;
|
||||
}
|
||||
|
||||
/**
|
||||
* The embed description
|
||||
*/
|
||||
public get description() {
|
||||
return this.data.description;
|
||||
}
|
||||
|
||||
/**
|
||||
* The embed URL
|
||||
*/
|
||||
public get url() {
|
||||
return this.data.url;
|
||||
}
|
||||
|
||||
/**
|
||||
* The embed color
|
||||
*/
|
||||
public get color() {
|
||||
return this.data.color;
|
||||
}
|
||||
|
||||
/**
|
||||
* The timestamp of the embed in an ISO 8601 format
|
||||
*/
|
||||
public get timestamp() {
|
||||
return this.data.timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* The embed thumbnail data
|
||||
*/
|
||||
public get thumbnail(): EmbedImageData | undefined {
|
||||
if (!this.data.thumbnail) return undefined;
|
||||
return {
|
||||
url: this.data.thumbnail.url,
|
||||
proxyURL: this.data.thumbnail.proxy_url,
|
||||
height: this.data.thumbnail.height,
|
||||
width: this.data.thumbnail.width,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The embed image data
|
||||
*/
|
||||
public get image(): EmbedImageData | undefined {
|
||||
if (!this.data.image) return undefined;
|
||||
return {
|
||||
url: this.data.image.url,
|
||||
proxyURL: this.data.image.proxy_url,
|
||||
height: this.data.image.height,
|
||||
width: this.data.image.width,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Received video data
|
||||
*/
|
||||
public get video(): APIEmbedVideo | undefined {
|
||||
return this.data.video;
|
||||
}
|
||||
|
||||
/**
|
||||
* The embed author data
|
||||
*/
|
||||
public get author(): EmbedAuthorData | undefined {
|
||||
if (!this.data.author) return undefined;
|
||||
return {
|
||||
name: this.data.author.name,
|
||||
url: this.data.author.url,
|
||||
iconURL: this.data.author.icon_url,
|
||||
proxyIconURL: this.data.author.proxy_icon_url,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Received data about the embed provider
|
||||
*/
|
||||
public get provider() {
|
||||
return this.data.provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* The embed footer data
|
||||
*/
|
||||
public get footer(): EmbedFooterData | undefined {
|
||||
if (!this.data.footer) return undefined;
|
||||
return {
|
||||
text: this.data.footer.text,
|
||||
iconURL: this.data.footer.icon_url,
|
||||
proxyIconURL: this.data.footer.proxy_icon_url,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The accumulated length for the embed title, description, fields, footer text, and author name
|
||||
*/
|
||||
public get length(): number {
|
||||
return (
|
||||
(this.data.title?.length ?? 0) +
|
||||
(this.data.description?.length ?? 0) +
|
||||
(this.data.fields?.reduce((prev, curr) => prev + curr.name.length + curr.value.length, 0) ?? 0) +
|
||||
(this.data.footer?.text.length ?? 0) +
|
||||
(this.data.author?.name.length ?? 0)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The hex color of the current color of the embed
|
||||
*/
|
||||
public get hexColor() {
|
||||
return typeof this.data.color === 'number' ? `#${this.data.color.toString(16).padStart(6, '0')}` : this.data.color;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds fields to the embed (max 25)
|
||||
*
|
||||
@@ -204,7 +68,7 @@ export class UnsafeEmbed implements Equatable<APIEmbed | UnsafeEmbed> {
|
||||
* @param fields The fields to set
|
||||
*/
|
||||
public setFields(...fields: APIEmbedField[]) {
|
||||
this.spliceFields(0, this.fields?.length ?? 0, ...fields);
|
||||
this.spliceFields(0, this.data.fields?.length ?? 0, ...fields);
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -319,11 +183,4 @@ export class UnsafeEmbed implements Equatable<APIEmbed | UnsafeEmbed> {
|
||||
public toJSON(): APIEmbed {
|
||||
return { ...this.data };
|
||||
}
|
||||
|
||||
public equals(other: UnsafeEmbed | APIEmbed) {
|
||||
const { image: thisImage, thumbnail: thisThumbnail, ...thisData } = this.data;
|
||||
const data = other instanceof UnsafeEmbed ? other.data : other;
|
||||
const { image, thumbnail, ...otherData } = data;
|
||||
return isEqual(otherData, thisData) && image?.url === thisImage?.url && thumbnail?.url === thisThumbnail?.url;
|
||||
}
|
||||
}
|
||||
|
||||
11
packages/builders/src/util/componentUtil.ts
Normal file
11
packages/builders/src/util/componentUtil.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { APIEmbed } from 'discord-api-types/v9';
|
||||
|
||||
export function embedLength(data: APIEmbed) {
|
||||
return (
|
||||
(data.title?.length ?? 0) +
|
||||
(data.description?.length ?? 0) +
|
||||
(data.fields?.reduce((prev, curr) => prev + curr.name.length + curr.value.length, 0) ?? 0) +
|
||||
(data.footer?.text.length ?? 0) +
|
||||
(data.author?.name.length ?? 0)
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user