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:
Suneet Tipirneni
2022-03-12 13:39:23 -05:00
committed by GitHub
parent 230c0c4cb1
commit 549716e4fc
44 changed files with 974 additions and 705 deletions

View File

@@ -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()),
});
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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}`);
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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()),
});
}
}

View File

@@ -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

View File

@@ -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));
}

View File

@@ -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);
}

View File

@@ -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';

View File

@@ -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);

View File

@@ -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));
}

View File

@@ -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;
}

View File

@@ -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));

View File

@@ -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;
}
}

View 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)
);
}