feat(components): Add unsafe message component builders (#7387)

This commit is contained in:
Suneet Tipirneni
2022-02-04 14:29:41 -05:00
committed by GitHub
parent 04502ce702
commit 6b6222bf51
12 changed files with 566 additions and 430 deletions

View File

@@ -5,7 +5,7 @@ import {
ComponentType,
} from 'discord-api-types/v9';
import { buttonLabelValidator, buttonStyleValidator } from '../../src/components/Assertions';
import { ButtonComponent } from '../../src/components/Button';
import { ButtonComponent } from '../../src/components/button/Button';
const buttonComponent = () => new ButtonComponent();

View File

@@ -0,0 +1,42 @@
import type { ButtonStyle, APIMessageComponentEmoji, APIButtonComponent } from 'discord-api-types/v9';
import {
buttonLabelValidator,
buttonStyleValidator,
customIdValidator,
disabledValidator,
emojiValidator,
urlValidator,
validateRequiredButtonParameters,
} from '../Assertions';
import { UnsafeButtonComponent } from './UnsafeButton';
export class ButtonComponent extends UnsafeButtonComponent {
public override setStyle(style: ButtonStyle) {
return super.setStyle(buttonStyleValidator.parse(style));
}
public override setURL(url: string) {
return super.setURL(urlValidator.parse(url));
}
public override setCustomId(customId: string) {
return super.setCustomId(customIdValidator.parse(customId));
}
public override setEmoji(emoji: APIMessageComponentEmoji) {
return super.setEmoji(emojiValidator.parse(emoji));
}
public override setDisabled(disabled: boolean) {
return super.setDisabled(disabledValidator.parse(disabled));
}
public override setLabel(label: string) {
return super.setLabel(buttonLabelValidator.parse(label));
}
public override toJSON(): APIButtonComponent {
validateRequiredButtonParameters(this.style, this.label, this.emoji, this.custom_id, this.url);
return super.toJSON();
}
}

View File

@@ -1,16 +1,12 @@
import { APIButtonComponent, APIMessageComponentEmoji, ButtonStyle, ComponentType } from 'discord-api-types/v9';
import {
buttonLabelValidator,
buttonStyleValidator,
customIdValidator,
disabledValidator,
emojiValidator,
urlValidator,
validateRequiredButtonParameters,
} from './Assertions';
import type { Component } from './Component';
ComponentType,
ButtonStyle,
type APIMessageComponentEmoji,
type APIButtonComponent,
} from 'discord-api-types/v9';
import type { Component } from '../Component';
export class ButtonComponent implements Component {
export class UnsafeButtonComponent implements Component {
public readonly type = ComponentType.Button as const;
public readonly style!: ButtonStyle;
public readonly label?: string;
@@ -41,7 +37,6 @@ export class ButtonComponent implements Component {
* @param style The style of the button
*/
public setStyle(style: ButtonStyle) {
buttonStyleValidator.parse(style);
Reflect.set(this, 'style', style);
return this;
}
@@ -51,7 +46,6 @@ export class ButtonComponent implements Component {
* @param url The URL to open when this button is clicked
*/
public setURL(url: string) {
urlValidator.parse(url);
Reflect.set(this, 'url', url);
return this;
}
@@ -61,7 +55,6 @@ export class ButtonComponent implements Component {
* @param customId The custom ID to use for this button
*/
public setCustomId(customId: string) {
customIdValidator.parse(customId);
Reflect.set(this, 'custom_id', customId);
return this;
}
@@ -71,7 +64,6 @@ export class ButtonComponent implements Component {
* @param emoji The emoji to display on this button
*/
public setEmoji(emoji: APIMessageComponentEmoji) {
emojiValidator.parse(emoji);
Reflect.set(this, 'emoji', emoji);
return this;
}
@@ -81,7 +73,6 @@ export class ButtonComponent implements Component {
* @param disabled Whether or not to disable this button or not
*/
public setDisabled(disabled: boolean) {
disabledValidator.parse(disabled);
Reflect.set(this, 'disabled', disabled);
return this;
}
@@ -91,13 +82,11 @@ export class ButtonComponent implements Component {
* @param label The label to display on this button
*/
public setLabel(label: string) {
buttonLabelValidator.parse(label);
Reflect.set(this, 'label', label);
return this;
}
public toJSON(): APIButtonComponent {
validateRequiredButtonParameters(this.style, this.label, this.emoji, this.custom_id, this.url);
return {
...this,
};

View File

@@ -1,4 +1,4 @@
import { APISelectMenuComponent, ComponentType } from 'discord-api-types/v9';
import type { APISelectMenuComponent } from 'discord-api-types/v9';
import {
customIdValidator,
disabledValidator,
@@ -6,106 +6,34 @@ import {
placeholderValidator,
validateRequiredSelectMenuParameters,
} from '../Assertions';
import type { Component } from '../Component';
import { SelectMenuOption } from './SelectMenuOption';
import { UnsafeSelectMenuComponent } from './UnsafeSelectMenu';
/**
* Represents a select menu component
*/
export class SelectMenuComponent implements Component {
public readonly type = ComponentType.SelectMenu as const;
public readonly options: SelectMenuOption[];
public readonly placeholder?: string;
public readonly min_values?: number;
public readonly max_values?: number;
public readonly custom_id!: string;
public readonly disabled?: boolean;
public constructor(data?: APISelectMenuComponent & { type?: ComponentType.SelectMenu }) {
this.options = data?.options.map((option) => new SelectMenuOption(option)) ?? [];
this.placeholder = data?.placeholder;
this.min_values = data?.min_values;
this.max_values = data?.max_values;
/* eslint-disable @typescript-eslint/non-nullable-type-assertion-style */
this.custom_id = data?.custom_id as string;
/* eslint-enable @typescript-eslint/non-nullable-type-assertion-style */
this.disabled = data?.disabled;
export class SelectMenuComponent extends UnsafeSelectMenuComponent {
public override setPlaceholder(placeholder: string) {
return super.setPlaceholder(placeholderValidator.parse(placeholder));
}
/**
* Sets the placeholder for this select menu
* @param placeholder The placeholder to use for this select menu
*/
public setPlaceholder(placeholder: string) {
placeholderValidator.parse(placeholder);
Reflect.set(this, 'placeholder', placeholder);
return this;
public override setMinValues(minValues: number) {
return super.setMinValues(minMaxValidator.parse(minValues));
}
/**
* 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) {
minMaxValidator.parse(minValues);
Reflect.set(this, 'min_values', minValues);
return this;
public override setMaxValues(maxValues: number) {
return super.setMaxValues(minMaxValidator.parse(maxValues));
}
/**
* 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) {
minMaxValidator.parse(maxValues);
Reflect.set(this, 'max_values', maxValues);
return this;
public override setCustomId(customId: string) {
return super.setCustomId(customIdValidator.parse(customId));
}
/**
* Sets the custom Id for this select menu
* @param customId The custom ID to use for this select menu
*/
public setCustomId(customId: string) {
customIdValidator.parse(customId);
Reflect.set(this, 'custom_id', customId);
return this;
public override setDisabled(disabled: boolean) {
return super.setDisabled(disabledValidator.parse(disabled));
}
/**
* Sets whether or not this select menu is disabled
* @param disabled Whether or not this select menu is disabled
*/
public setDisabled(disabled: boolean) {
disabledValidator.parse(disabled);
Reflect.set(this, 'disabled', disabled);
return this;
}
/**
* Adds options to this select menu
* @param options The options to add to this select menu
* @returns
*/
public addOptions(...options: SelectMenuOption[]) {
this.options.push(...options);
return this;
}
/**
* Sets the options on this select menu
* @param options The options to set on this select menu
*/
public setOptions(options: SelectMenuOption[]) {
Reflect.set(this, 'options', [...options]);
return this;
}
public toJSON(): APISelectMenuComponent {
public override toJSON(): APISelectMenuComponent {
validateRequiredSelectMenuParameters(this.options, this.custom_id);
return {
...this,
options: this.options.map((option) => option.toJSON()),
};
return super.toJSON();
}
}

View File

@@ -5,79 +5,26 @@ import {
labelValueValidator,
validateRequiredSelectMenuOptionParameters,
} from '../Assertions';
import { UnsafeSelectMenuOption } from './UnsafeSelectMenuOption';
/**
* Represents an option within a select menu component
*/
export class SelectMenuOption {
public readonly label!: string;
public readonly value!: string;
public readonly description?: string;
public readonly emoji?: APIMessageComponentEmoji;
public readonly default?: boolean;
public constructor(data?: APISelectMenuOption) {
/* eslint-disable @typescript-eslint/non-nullable-type-assertion-style */
this.label = data?.label as string;
this.value = data?.value as string;
/* eslint-enable @typescript-eslint/non-nullable-type-assertion-style */
this.description = data?.description;
this.emoji = data?.emoji;
this.default = data?.default;
export class SelectMenuOption extends UnsafeSelectMenuOption {
public override setDescription(description: string) {
return super.setDescription(labelValueValidator.parse(description));
}
/**
* Sets the label of this option
* @param label The label to show on this option
*/
public setLabel(label: string) {
Reflect.set(this, 'label', label);
return this;
public override setDefault(isDefault: boolean) {
return super.setDefault(defaultValidator.parse(isDefault));
}
/**
* Sets the value of this option
* @param value The value of this option
*/
public setValue(value: string) {
Reflect.set(this, 'value', 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) {
labelValueValidator.parse(description);
Reflect.set(this, 'description', description);
return this;
}
/**
* Sets whether this option is selected by default
* @param isDefault Whether or not this option is selected by default
*/
public setDefault(isDefault: boolean) {
defaultValidator.parse(isDefault);
Reflect.set(this, 'default', isDefault);
return this;
}
/**
* Sets the emoji to display on this button
* @param emoji The emoji to display on this button
*/
public setEmoji(emoji: APIMessageComponentEmoji) {
emojiValidator.parse(emoji);
Reflect.set(this, 'emoji', emoji);
return this;
}
public toJSON(): APISelectMenuOption {
public override toJSON(): APISelectMenuOption {
validateRequiredSelectMenuOptionParameters(this.label, this.value);
return {
...this,
};
return super.toJSON();
}
}

View File

@@ -0,0 +1,98 @@
import { ComponentType, type APISelectMenuComponent } from 'discord-api-types/v9';
import type { Component } from '../Component';
import { SelectMenuOption } from './SelectMenuOption';
/**
* Represents a non-validated select menu component
*/
export class UnsafeSelectMenuComponent implements Component {
public readonly type = ComponentType.SelectMenu as const;
public readonly options: SelectMenuOption[];
public readonly placeholder?: string;
public readonly min_values?: number;
public readonly max_values?: number;
public readonly custom_id!: string;
public readonly disabled?: boolean;
public constructor(data?: APISelectMenuComponent) {
this.options = data?.options.map((option) => new SelectMenuOption(option)) ?? [];
this.placeholder = data?.placeholder;
this.min_values = data?.min_values;
this.max_values = data?.max_values;
/* eslint-disable @typescript-eslint/non-nullable-type-assertion-style */
this.custom_id = data?.custom_id as string;
/* eslint-enable @typescript-eslint/non-nullable-type-assertion-style */
this.disabled = data?.disabled;
}
/**
* Sets the placeholder for this select menu
* @param placeholder The placeholder to use for this select menu
*/
public setPlaceholder(placeholder: string) {
Reflect.set(this, 'placeholder', placeholder);
return this;
}
/**
* Sets thes minimum values that must be selected in the select menu
* @param minValues The minimum values that must be selected
*/
public setMinValues(minValues: number) {
Reflect.set(this, 'min_values', minValues);
return this;
}
/**
* Sets thes maximum values that must be selected in the select menu
* @param minValues The maximum values that must be selected
*/
public setMaxValues(maxValues: number) {
Reflect.set(this, '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) {
Reflect.set(this, '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: boolean) {
Reflect.set(this, 'disabled', disabled);
return this;
}
/**
* Adds options to this select menu
* @param options The options to add to this select menu
* @returns
*/
public addOptions(...options: SelectMenuOption[]) {
this.options.push(...options);
return this;
}
/**
* Sets the options on this select menu
* @param options The options to set on this select menu
*/
public setOptions(options: SelectMenuOption[]) {
Reflect.set(this, 'options', [...options]);
return this;
}
public toJSON(): APISelectMenuComponent {
return {
...this,
options: this.options.map((option) => option.toJSON()),
};
}
}

View File

@@ -0,0 +1,73 @@
import type { APIMessageComponentEmoji, APISelectMenuOption } from 'discord-api-types/v9';
/**
* Represents a non-validated option within a select menu component
*/
export class UnsafeSelectMenuOption {
public readonly label!: string;
public readonly value!: string;
public readonly description?: string;
public readonly emoji?: APIMessageComponentEmoji;
public readonly default?: boolean;
public constructor(data?: APISelectMenuOption) {
/* eslint-disable @typescript-eslint/non-nullable-type-assertion-style */
this.label = data?.label as string;
this.value = data?.value as string;
/* eslint-enable @typescript-eslint/non-nullable-type-assertion-style */
this.description = data?.description;
this.emoji = data?.emoji;
this.default = data?.default;
}
/**
* Sets the label of this option
* @param label The label to show on this option
*/
public setLabel(label: string) {
Reflect.set(this, 'label', label);
return this;
}
/**
* Sets the value of this option
* @param value The value of this option
*/
public setValue(value: string) {
Reflect.set(this, 'value', value);
return this;
}
/**
* Sets the description of this option.
* @param description The description of this option
*/
public setDescription(description: string) {
Reflect.set(this, 'description', description);
return this;
}
/**
* Sets whether this option is selected by default
* @param isDefault Whether or not this option is selected by default
*/
public setDefault(isDefault: boolean) {
Reflect.set(this, 'default', isDefault);
return this;
}
/**
* Sets the emoji to display on this button
* @param emoji The emoji to display on this button
*/
public setEmoji(emoji: APIMessageComponentEmoji) {
Reflect.set(this, 'emoji', emoji);
return this;
}
public toJSON(): APISelectMenuOption {
return {
...this,
};
}
}

View File

@@ -1,14 +1,18 @@
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';
export * from './components/Button';
export * from './components/button/Button';
export * from './components/Component';
export * from './components/Components';
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';

View File

@@ -1,13 +1,4 @@
import type {
APIEmbed,
APIEmbedAuthor,
APIEmbedField,
APIEmbedFooter,
APIEmbedImage,
APIEmbedProvider,
APIEmbedThumbnail,
APIEmbedVideo,
} from 'discord-api-types/v9';
import type { APIEmbedField } from 'discord-api-types/v9';
import {
authorNamePredicate,
colorPredicate,
@@ -22,300 +13,86 @@ import {
urlPredicate,
validateFieldLength,
} from './Assertions';
export interface AuthorOptions {
name: string;
url?: string;
iconURL?: string;
}
export interface FooterOptions {
text: string;
iconURL?: string;
}
import { AuthorOptions, FooterOptions, UnsafeEmbed } from './UnsafeEmbed';
/**
* Represents an embed in a message (image/video preview, rich embed, etc.)
*/
export class Embed implements APIEmbed {
/**
* An array of fields of this embed
*/
public readonly fields: APIEmbedField[];
/**
* The embed title
*/
public readonly title?: string;
/**
* The embed description
*/
public readonly description?: string;
/**
* The embed url
*/
public readonly url?: string;
/**
* The embed color
*/
public readonly color?: number;
/**
* The timestamp of the embed in the ISO format
*/
public readonly timestamp?: string;
/**
* The embed thumbnail data
*/
public readonly thumbnail?: APIEmbedThumbnail;
/**
* The embed image data
*/
public readonly image?: APIEmbedImage;
/**
* Received video data
*/
public readonly video?: APIEmbedVideo;
/**
* The embed author data
*/
public readonly author?: APIEmbedAuthor;
/**
* Received data about the embed provider
*/
public readonly provider?: APIEmbedProvider;
/**
* The embed footer data
*/
public readonly footer?: APIEmbedFooter;
public constructor(data: APIEmbed = {}) {
this.title = data.title;
this.description = data.description;
this.url = data.url;
this.color = data.color;
this.thumbnail = data.thumbnail;
this.image = data.image;
this.video = data.video;
this.author = data.author;
this.provider = data.provider;
this.footer = data.footer;
this.fields = data.fields ?? [];
if (data.timestamp) this.timestamp = new Date(data.timestamp).toISOString();
}
/**
* The accumulated length for the embed title, description, fields, footer text, and author name
*/
public get length(): number {
return (
(this.title?.length ?? 0) +
(this.description?.length ?? 0) +
this.fields.reduce((prev, curr) => prev + curr.name.length + curr.value.length, 0) +
(this.footer?.text.length ?? 0) +
(this.author?.name.length ?? 0)
);
}
/**
* Adds a field to the embed (max 25)
*
* @param field The field to add.
*/
public addField(field: APIEmbedField): this {
return this.addFields(field);
}
/**
* Adds fields to the embed (max 25)
*
* @param fields The fields to add
*/
public addFields(...fields: APIEmbedField[]): this {
// Data assertions
embedFieldsArrayPredicate.parse(fields);
export class Embed extends UnsafeEmbed {
public override addFields(...fields: APIEmbedField[]): this {
// Ensure adding these fields won't exceed the 25 field limit
validateFieldLength(this.fields, fields.length);
this.fields.push(...Embed.normalizeFields(...fields));
return this;
// Data assertions
return super.addFields(...embedFieldsArrayPredicate.parse(fields));
}
/**
* 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 {
// Data assertions
embedFieldsArrayPredicate.parse(fields);
public override spliceFields(index: number, deleteCount: number, ...fields: APIEmbedField[]): this {
// Ensure adding these fields won't exceed the 25 field limit
validateFieldLength(this.fields, fields.length - deleteCount);
this.fields.splice(index, deleteCount, ...Embed.normalizeFields(...fields));
return this;
// Data assertions
return super.spliceFields(index, deleteCount, ...embedFieldsArrayPredicate.parse(fields));
}
/**
* Sets the embed's fields (max 25).
* @param fields The fields to set
*/
public setFields(...fields: APIEmbedField[]) {
this.spliceFields(0, this.fields.length, ...fields);
return this;
}
/**
* Sets the author of this embed
*
* @param options The options for the author
*/
public setAuthor(options: AuthorOptions | null): this {
public override setAuthor(options: AuthorOptions | null): this {
if (options === null) {
Reflect.set(this, 'author', undefined);
return this;
return super.setAuthor(null);
}
const { name, iconURL, url } = options;
// Data assertions
authorNamePredicate.parse(name);
urlPredicate.parse(iconURL);
urlPredicate.parse(url);
authorNamePredicate.parse(options.name);
urlPredicate.parse(options.iconURL);
urlPredicate.parse(options.url);
Reflect.set(this, 'author', { name, url, icon_url: iconURL });
return this;
return super.setAuthor(options);
}
/**
* Sets the color of this embed
*
* @param color The color of the embed
*/
public setColor(color: number | null): this {
public override setColor(color: number | null): this {
// Data assertions
colorPredicate.parse(color);
Reflect.set(this, 'color', color ?? undefined);
return this;
return super.setColor(colorPredicate.parse(color));
}
/**
* Sets the description of this embed
*
* @param description The description
*/
public setDescription(description: string | null): this {
public override setDescription(description: string | null): this {
// Data assertions
descriptionPredicate.parse(description);
Reflect.set(this, 'description', description ?? undefined);
return this;
return super.setDescription(descriptionPredicate.parse(description));
}
/**
* Sets the footer of this embed
*
* @param options The options for the footer
*/
public setFooter(options: FooterOptions | null): this {
public override setFooter(options: FooterOptions | null): this {
if (options === null) {
Reflect.set(this, 'footer', undefined);
return this;
return super.setFooter(null);
}
const { text, iconURL } = options;
// Data assertions
footerTextPredicate.parse(text);
urlPredicate.parse(iconURL);
footerTextPredicate.parse(options.text);
urlPredicate.parse(options.iconURL);
Reflect.set(this, 'footer', { text, icon_url: iconURL });
return this;
return super.setFooter(options);
}
/**
* Sets the image of this embed
*
* @param url The URL of the image
*/
public setImage(url: string | null): this {
public override setImage(url: string | null): this {
// Data assertions
urlPredicate.parse(url);
Reflect.set(this, 'image', url ? { url } : undefined);
return this;
return super.setImage(urlPredicate.parse(url)!);
}
/**
* Sets the thumbnail of this embed
*
* @param url The URL of the thumbnail
*/
public setThumbnail(url: string | null): this {
public override setThumbnail(url: string | null): this {
// Data assertions
urlPredicate.parse(url);
Reflect.set(this, 'thumbnail', url ? { url } : undefined);
return this;
return super.setThumbnail(urlPredicate.parse(url)!);
}
/**
* Sets the timestamp of this embed
*
* @param timestamp The timestamp or date
*/
public setTimestamp(timestamp: number | Date | null = Date.now()): this {
public override setTimestamp(timestamp: number | Date | null = Date.now()): this {
// Data assertions
timestampPredicate.parse(timestamp);
Reflect.set(this, 'timestamp', timestamp ? new Date(timestamp).toISOString() : undefined);
return this;
return super.setTimestamp(timestampPredicate.parse(timestamp));
}
/**
* Sets the title of this embed
*
* @param title The title
*/
public setTitle(title: string | null): this {
public override setTitle(title: string | null): this {
// Data assertions
titlePredicate.parse(title);
Reflect.set(this, 'title', title ?? undefined);
return this;
return super.setTitle(titlePredicate.parse(title));
}
/**
* Sets the URL of this embed
*
* @param url The URL
*/
public setURL(url: string | null): this {
public override setURL(url: string | null): this {
// Data assertions
urlPredicate.parse(url);
Reflect.set(this, 'url', url ?? undefined);
return this;
}
/**
* Transforms the embed to a plain object
*/
public toJSON(): APIEmbed {
return { ...this };
return super.setURL(urlPredicate.parse(url)!);
}
/**
@@ -323,7 +100,7 @@ export class Embed implements APIEmbed {
*
* @param fields Fields to normalize
*/
public static normalizeFields(...fields: APIEmbedField[]): APIEmbedField[] {
public static override normalizeFields(...fields: APIEmbedField[]): APIEmbedField[] {
return fields.flat(Infinity).map((field) => {
fieldNamePredicate.parse(field.name);
fieldValuePredicate.parse(field.value);

View File

@@ -0,0 +1,271 @@
import type {
APIEmbed,
APIEmbedAuthor,
APIEmbedField,
APIEmbedFooter,
APIEmbedImage,
APIEmbedProvider,
APIEmbedThumbnail,
APIEmbedVideo,
} from 'discord-api-types/v9';
import { Embed } from './Embed';
export interface AuthorOptions {
name: string;
url?: string;
iconURL?: string;
}
export interface FooterOptions {
text: string;
iconURL?: string;
}
export class UnsafeEmbed implements APIEmbed {
/**
* An array of fields of this embed
*/
public readonly fields: APIEmbedField[];
/**
* The embed title
*/
public readonly title?: string;
/**
* The embed description
*/
public readonly description?: string;
/**
* The embed url
*/
public readonly url?: string;
/**
* The embed color
*/
public readonly color?: number;
/**
* The timestamp of the embed in the ISO format
*/
public readonly timestamp?: string;
/**
* The embed thumbnail data
*/
public readonly thumbnail?: APIEmbedThumbnail;
/**
* The embed image data
*/
public readonly image?: APIEmbedImage;
/**
* Received video data
*/
public readonly video?: APIEmbedVideo;
/**
* The embed author data
*/
public readonly author?: APIEmbedAuthor;
/**
* Received data about the embed provider
*/
public readonly provider?: APIEmbedProvider;
/**
* The embed footer data
*/
public readonly footer?: APIEmbedFooter;
public constructor(data: APIEmbed = {}) {
this.title = data.title;
this.description = data.description;
this.url = data.url;
this.color = data.color;
this.thumbnail = data.thumbnail;
this.image = data.image;
this.video = data.video;
this.author = data.author;
this.provider = data.provider;
this.footer = data.footer;
this.fields = data.fields ?? [];
if (data.timestamp) this.timestamp = new Date(data.timestamp).toISOString();
}
/**
* The accumulated length for the embed title, description, fields, footer text, and author name
*/
public get length(): number {
return (
(this.title?.length ?? 0) +
(this.description?.length ?? 0) +
this.fields.reduce((prev, curr) => prev + curr.name.length + curr.value.length, 0) +
(this.footer?.text.length ?? 0) +
(this.author?.name.length ?? 0)
);
}
/**
* Adds a field to the embed (max 25)
*
* @param field The field to add.
*/
public addField(field: APIEmbedField): this {
return this.addFields(field);
}
/**
* Adds fields to the embed (max 25)
*
* @param fields The fields to add
*/
public addFields(...fields: APIEmbedField[]): this {
this.fields.push(...Embed.normalizeFields(...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 {
this.fields.splice(index, deleteCount, ...Embed.normalizeFields(...fields));
return this;
}
/**
* Sets the embed's fields (max 25).
* @param fields The fields to set
*/
public setFields(...fields: APIEmbedField[]) {
this.spliceFields(0, this.fields.length, ...fields);
return this;
}
/**
* Sets the author of this embed
*
* @param options The options for the author
*/
public setAuthor(options: AuthorOptions | null): this {
if (options === null) {
Reflect.set(this, 'author', undefined);
return this;
}
Reflect.set(this, '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 | null): this {
Reflect.set(this, 'color', color ?? undefined);
return this;
}
/**
* Sets the description of this embed
*
* @param description The description
*/
public setDescription(description: string | null): this {
Reflect.set(this, 'description', description ?? undefined);
return this;
}
/**
* Sets the footer of this embed
*
* @param options The options for the footer
*/
public setFooter(options: FooterOptions | null): this {
if (options === null) {
Reflect.set(this, 'footer', undefined);
return this;
}
Reflect.set(this, '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 {
Reflect.set(this, '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 {
Reflect.set(this, '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 {
Reflect.set(this, '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 {
Reflect.set(this, 'title', title ?? undefined);
return this;
}
/**
* Sets the URL of this embed
*
* @param url The URL
*/
public setURL(url: string | null): this {
Reflect.set(this, 'url', url ?? undefined);
return this;
}
/**
* Transforms the embed to a plain object
*/
public toJSON(): APIEmbed {
return { ...this };
}
/**
* Normalizes field input and resolves strings
*
* @param fields Fields to normalize
*/
public static normalizeFields(...fields: APIEmbedField[]): APIEmbedField[] {
return fields
.flat(Infinity)
.map((field) => ({ name: field.name, value: field.value, inline: field.inline ?? undefined }));
}
}

View File

@@ -89,6 +89,7 @@ exports.CommandInteractionOptionResolver = require('./structures/CommandInteract
exports.ContextMenuCommandInteraction = require('./structures/ContextMenuCommandInteraction');
exports.DMChannel = require('./structures/DMChannel');
exports.Embed = require('@discordjs/builders').Embed;
exports.UnsafeEmbed = require('@discordjs/builders').UnsafeEmbed;
exports.Emoji = require('./structures/Emoji').Emoji;
exports.Guild = require('./structures/Guild').Guild;
exports.GuildAuditLogs = require('./structures/GuildAuditLogs');
@@ -182,8 +183,11 @@ exports.UserFlags = require('discord-api-types/v9').UserFlags;
exports.WebhookType = require('discord-api-types/v9').WebhookType;
exports.ActionRow = require('@discordjs/builders').ActionRow;
exports.ButtonComponent = require('@discordjs/builders').ButtonComponent;
exports.UnsafeButtonComponent = require('@discordjs/builders').UnsafeButtonComponent;
exports.SelectMenuComponent = require('@discordjs/builders').SelectMenuComponent;
exports.UnsafeSelectMenuComponent = require('@discordjs/builders').UnsafeButtonComponent;
exports.SelectMenuOption = require('@discordjs/builders').SelectMenuOption;
exports.UnsafeSelectMenuOption = require('@discordjs/builders').UnsafeSelectMenuOption;
exports.DiscordAPIError = require('@discordjs/rest').DiscordAPIError;
exports.HTTPError = require('@discordjs/rest').HTTPError;
exports.RateLimitError = require('@discordjs/rest').RateLimitError;

View File

@@ -4615,8 +4615,6 @@ export interface MessageMentionOptions {
export type MessageMentionTypes = 'roles' | 'users' | 'everyone';
export { Embed };
export interface MessageOptions {
tts?: boolean;
nonce?: string | number;
@@ -5216,8 +5214,13 @@ export {
export {
ActionRow,
ButtonComponent,
UnsafeButtonComponent,
SelectMenuComponent,
UnsafeSelectMenuComponent,
SelectMenuOption,
UnsafeSelectMenuOption,
ActionRowComponent,
Embed,
UnsafeEmbed,
} from '@discordjs/builders';
export { DiscordAPIError, HTTPError, RateLimitError } from '@discordjs/rest';