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

@@ -53,6 +53,7 @@
"@sapphire/snowflake": "^3.1.0",
"@types/ws": "^8.2.2",
"discord-api-types": "^0.27.3",
"fast-deep-equal": "^3.1.3",
"lodash.snakecase": "^4.1.1",
"undici": "^4.14.1",
"ws": "^8.5.0"

View File

@@ -71,6 +71,7 @@ exports.WebSocketShard = require('./client/websocket/WebSocketShard');
// Structures
exports.ActionRow = require('./structures/ActionRow');
exports.ActionRowBuilder = require('./structures/ActionRowBuilder');
exports.Activity = require('./structures/Presence').Activity;
exports.AnonymousGuild = require('./structures/AnonymousGuild');
exports.Application = require('./structures/interfaces/Application');
@@ -81,6 +82,7 @@ exports.BaseGuild = require('./structures/BaseGuild');
exports.BaseGuildEmoji = require('./structures/BaseGuildEmoji');
exports.BaseGuildTextChannel = require('./structures/BaseGuildTextChannel');
exports.BaseGuildVoiceChannel = require('./structures/BaseGuildVoiceChannel');
exports.ButtonBuilder = require('./structures/ButtonBuilder');
exports.ButtonComponent = require('./structures/ButtonComponent');
exports.ButtonInteraction = require('./structures/ButtonInteraction');
exports.CategoryChannel = require('./structures/CategoryChannel');
@@ -92,9 +94,11 @@ exports.ClientUser = require('./structures/ClientUser');
exports.CommandInteraction = require('./structures/CommandInteraction');
exports.Collector = require('./structures/interfaces/Collector');
exports.CommandInteractionOptionResolver = require('./structures/CommandInteractionOptionResolver');
exports.Component = require('./structures/Component');
exports.ContextMenuCommandInteraction = require('./structures/ContextMenuCommandInteraction');
exports.DMChannel = require('./structures/DMChannel');
exports.Embed = require('./structures/Embed');
exports.EmbedBuilder = require('./structures/EmbedBuilder');
exports.UnsafeEmbed = require('@discordjs/builders').UnsafeEmbed;
exports.Emoji = require('./structures/Emoji').Emoji;
exports.Guild = require('./structures/Guild').Guild;
@@ -136,6 +140,7 @@ exports.ReactionCollector = require('./structures/ReactionCollector');
exports.ReactionEmoji = require('./structures/ReactionEmoji');
exports.RichPresenceAssets = require('./structures/Presence').RichPresenceAssets;
exports.Role = require('./structures/Role').Role;
exports.SelectMenuBuilder = require('./structures/SelectMenuBuilder');
exports.SelectMenuComponent = require('./structures/SelectMenuComponent');
exports.SelectMenuInteraction = require('./structures/SelectMenuInteraction');
exports.StageChannel = require('./structures/StageChannel');
@@ -146,6 +151,7 @@ exports.StoreChannel = require('./structures/StoreChannel');
exports.Team = require('./structures/Team');
exports.TeamMember = require('./structures/TeamMember');
exports.TextChannel = require('./structures/TextChannel');
exports.TextInputBuilder = require('./structures/TextInputBuilder');
exports.TextInputComponent = require('./structures/TextInputComponent');
exports.ThreadChannel = require('./structures/ThreadChannel');
exports.ThreadMember = require('./structures/ThreadMember');
@@ -191,6 +197,7 @@ exports.InviteTargetType = require('discord-api-types/v9').InviteTargetType;
exports.Locale = require('discord-api-types/v9').Locale;
exports.MessageType = require('discord-api-types/v9').MessageType;
exports.MessageFlags = require('discord-api-types/v9').MessageFlags;
exports.ModalBuilder = require('@discordjs/builders').ModalBuilder;
exports.OAuth2Scopes = require('discord-api-types/v9').OAuth2Scopes;
exports.PermissionFlagsBits = require('discord-api-types/v9').PermissionFlagsBits;
exports.RESTJSONErrorCodes = require('discord-api-types/v9').RESTJSONErrorCodes;
@@ -200,10 +207,10 @@ exports.StickerFormatType = require('discord-api-types/v9').StickerFormatType;
exports.TextInputStyle = require('discord-api-types/v9').TextInputStyle;
exports.UserFlags = require('discord-api-types/v9').UserFlags;
exports.WebhookType = require('discord-api-types/v9').WebhookType;
exports.UnsafeButtonComponent = require('@discordjs/builders').UnsafeButtonComponent;
exports.UnsafeSelectMenuComponent = require('@discordjs/builders').UnsafeSelectMenuComponent;
exports.SelectMenuOption = require('@discordjs/builders').SelectMenuOption;
exports.UnsafeSelectMenuOption = require('@discordjs/builders').UnsafeSelectMenuOption;
exports.UnsafeButtonBuilder = require('@discordjs/builders').UnsafeButtonBuilder;
exports.UnsafeSelectMenuBuilder = require('@discordjs/builders').UnsafeSelectMenuBuilder;
exports.SelectMenuOptionBuilder = require('@discordjs/builders').SelectMenuOptionBuilder;
exports.UnsafeSelectMenuOptionBuilder = require('@discordjs/builders').UnsafeSelectMenuOptionBuilder;
exports.DiscordAPIError = require('@discordjs/rest').DiscordAPIError;
exports.HTTPError = require('@discordjs/rest').HTTPError;
exports.RateLimitError = require('@discordjs/rest').RateLimitError;

View File

@@ -1,14 +1,21 @@
'use strict';
const { ActionRow: BuildersActionRow, Component } = require('@discordjs/builders');
const Transformers = require('../util/Transformers');
const Component = require('./Component');
const Components = require('../util/Components');
class ActionRow extends BuildersActionRow {
constructor({ components, ...data } = {}) {
super({
components: components?.map(c => (c instanceof Component ? c : Transformers.toSnakeCase(c))),
...Transformers.toSnakeCase(data),
});
/**
* Represents an action row
* @extends {Component}
*/
class ActionRow extends Component {
constructor({ components, ...data }) {
super(data);
/**
* The components in this action row
* @type {Component[]}
* @readonly
*/
this.components = components.map(c => Components.createComponent(c));
}
}

View File

@@ -0,0 +1,15 @@
'use strict';
const { ActionRowBuilder: BuildersActionRow, ComponentBuilder } = require('@discordjs/builders');
const Transformers = require('../util/Transformers');
class ActionRowBuilder extends BuildersActionRow {
constructor({ components, ...data } = {}) {
super({
components: components?.map(c => (c instanceof ComponentBuilder ? c : Transformers.toSnakeCase(c))),
...Transformers.toSnakeCase(data),
});
}
}
module.exports = ActionRowBuilder;

View File

@@ -0,0 +1,24 @@
'use strict';
const { ButtonBuilder: BuildersButtonComponent, isJSONEncodable } = require('@discordjs/builders');
const Transformers = require('../util/Transformers');
class ButtonBuilder extends BuildersButtonComponent {
constructor(data) {
super(Transformers.toSnakeCase(data));
}
/**
* Creates a new button builder from json data
* @param {JSONEncodable<APIButtonComponent> | APIButtonComponent} other The other data
* @returns {ButtonBuilder}
*/
static from(other) {
if (isJSONEncodable(other)) {
return new this(other.toJSON());
}
return new this(other);
}
}
module.exports = ButtonBuilder;

View File

@@ -1,11 +1,64 @@
'use strict';
const { ButtonComponent: BuildersButtonComponent } = require('@discordjs/builders');
const Transformers = require('../util/Transformers');
const Component = require('./Component');
class ButtonComponent extends BuildersButtonComponent {
constructor(data) {
super(Transformers.toSnakeCase(data));
/**
* Represents a button component
* @extends {Component}
*/
class ButtonComponent extends Component {
/**
* The style of this button
* @type {ButtonStyle}
* @readonly
*/
get style() {
return this.data.style;
}
/**
* The label of this button
* @type {?string}
* @readonly
*/
get label() {
return this.data.label ?? null;
}
/**
* The emoji used in this button
* @type {?APIMessageComponentEmoji}
* @readonly
*/
get emoji() {
return this.data.emoji ?? null;
}
/**
* Whether this button is disabled
* @type {?boolean}
* @readonly
*/
get disabled() {
return this.data.disabled ?? null;
}
/**
* The custom id of this button (only defined on non-link buttons)
* @type {?string}
* @readonly
*/
get customId() {
return this.data.custom_id ?? null;
}
/**
* The URL of this button (only defined on link buttons)
* @type {?string}
* @readonly
*/
get url() {
return this.data.url ?? null;
}
}

View File

@@ -0,0 +1,52 @@
'use strict';
const isEqual = require('fast-deep-equal');
/**
* Represents a component
*/
class Component {
/**
* Creates a new component from API data
* @param {APIMessageComponent} data The API component data
* @private
*/
constructor(data) {
/**
* The API data associated with this component
* @type {APIMessageComponent}
*/
this.data = data;
}
/**
* The type of the component
* @type {ComponentType}
* @readonly
*/
get type() {
return this.data.type;
}
/**
* Whether or not the given components are equal
* @param {Component|APIMessageComponent} other The component to compare against
* @returns {boolean}
*/
equals(other) {
if (other instanceof Component) {
return isEqual(other.data, this.data);
}
return isEqual(other, this.data);
}
/**
* Returns the API-compatible JSON for this component
* @returns {APIMessageComponent}
*/
toJSON() {
return { ...this.data };
}
}
module.exports = Component;

View File

@@ -1,15 +1,198 @@
'use strict';
const { Embed: BuildersEmbed } = require('@discordjs/builders');
const Transformers = require('../util/Transformers');
const Util = require('../util/Util');
const isEqual = require('fast-deep-equal');
const { Util } = require('../util/Util');
/**
* Represents an embed object
*/
class Embed extends BuildersEmbed {
class Embed {
/**
* Creates a new embed object
* @param {APIEmbed} data API embed data
* @private
*/
constructor(data) {
super(Transformers.toSnakeCase(data));
/**
* The API embed data
* @type {APIEmbed}
* @readonly
*/
this.data = { ...data };
}
/**
* An array of fields of this embed
* @type {?Array<APIEmbedField>}
* @readonly
*/
get fields() {
return this.data.fields ?? null;
}
/**
* The embed title
* @type {?string}
* @readonly
*/
get title() {
return this.data.title ?? null;
}
/**
* The embed description
* @type {?string}
* @readonly
*/
get description() {
return this.data.description ?? null;
}
/**
* The embed URL
* @type {?string}
* @readonly
*/
get url() {
return this.data.url ?? null;
}
/**
* The embed color
* @type {?number}
* @readonly
*/
get color() {
return this.data.color ?? null;
}
/**
* The timestamp of the embed in an ISO 8601 format
* @type {?string}
* @readonly
*/
get timestamp() {
return this.data.timestamp ?? null;
}
/**
* The embed thumbnail data
* @type {?EmbedImageData}
* @readonly
*/
get thumbnail() {
if (!this.data.thumbnail) return null;
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
* @type {?EmbedImageData}
* @readonly
*/
get image() {
if (!this.data.image) return null;
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
* @type {?EmbedVideoData}
* @readonly
*/
get video() {
return this.data.video ?? null;
}
/**
* The embed author data
* @type {?EmbedAuthorData}
* @readonly
*/
get author() {
if (!this.data.author) return null;
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
* @type {?EmbedProvider}
* @readonly
*/
get provider() {
return this.data.provider ?? null;
}
/**
* The embed footer data
* @type {?EmbedFooterData}
* @readonly
*/
get footer() {
if (!this.data.footer) return null;
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
* @type {number}
* @readonly
*/
get length() {
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
* @type {?string}
* @readonly
*/
get hexColor() {
return typeof this.data.color === 'number'
? `#${this.data.color.toString(16).padStart(6, '0')}`
: this.data.color ?? null;
}
/**
* Returns the API-compatible JSON for this embed
* @returns {APIEmbed}
*/
toJSON() {
return { ...this.data };
}
/**
* Whether or not the given embeds are equal
* @param {Embed|APIEmbed} other The embed to compare against
* @returns {boolean}
*/
equals(other) {
if (other instanceof Embed) {
return isEqual(other.data, this.data);
}
return isEqual(other, this.data);
}
/**

View File

@@ -0,0 +1,24 @@
'use strict';
const { EmbedBuilder: BuildersEmbed, isJSONEncodable } = require('@discordjs/builders');
const Transformers = require('../util/Transformers');
class EmbedBuilder extends BuildersEmbed {
constructor(data) {
super(Transformers.toSnakeCase(data));
}
/**
* Creates a new embed builder from json data
* @param {JSONEncodable<APIEmbed> | APIEmbed} other The other data
* @returns {EmbedBuilder}
*/
static from(other) {
if (isJSONEncodable(other)) {
return new this(other.toJSON());
}
return new this(other);
}
}
module.exports = EmbedBuilder;

View File

@@ -1,6 +1,5 @@
'use strict';
const { createComponent, Embed } = require('@discordjs/builders');
const { Collection } = require('@discordjs/collection');
const { DiscordSnowflake } = require('@sapphire/snowflake');
const {
@@ -12,6 +11,7 @@ const {
} = require('discord-api-types/v9');
const Base = require('./Base');
const ClientApplication = require('./ClientApplication');
const Embed = require('./Embed');
const InteractionCollector = require('./InteractionCollector');
const MessageAttachment = require('./MessageAttachment');
const Mentions = require('./MessageMentions');
@@ -20,6 +20,7 @@ const ReactionCollector = require('./ReactionCollector');
const { Sticker } = require('./Sticker');
const { Error } = require('../errors');
const ReactionManager = require('../managers/ReactionManager');
const Components = require('../util/Components');
const { NonSystemMessageTypes } = require('../util/Constants');
const MessageFlagsBitField = require('../util/MessageFlagsBitField');
const PermissionsBitField = require('../util/PermissionsBitField');
@@ -145,7 +146,7 @@ class Message extends Base {
* A list of MessageActionRows in the message
* @type {ActionRow[]}
*/
this.components = data.components.map(c => createComponent(c));
this.components = data.components.map(c => Components.createComponent(c));
} else {
this.components = this.components?.slice() ?? [];
}

View File

@@ -1,12 +0,0 @@
'use strict';
const { Modal: BuildersModal } = require('@discordjs/builders');
const Transformers = require('../util/Transformers');
class Modal extends BuildersModal {
constructor(data) {
super(Transformers.toSnakeCase(data));
}
}
module.exports = Modal;

View File

@@ -0,0 +1,24 @@
'use strict';
const { SelectMenuBuilder: BuildersSelectMenuComponent, isJSONEncodable } = require('@discordjs/builders');
const Transformers = require('../util/Transformers');
class SelectMenuBuilder extends BuildersSelectMenuComponent {
constructor(data) {
super(Transformers.toSnakeCase(data));
}
/**
* Creates a new select menu builder from json data
* @param {JSONEncodable<APISelectMenuComponent> | APISelectMenuComponent} other The other data
* @returns {SelectMenuBuilder}
*/
static from(other) {
if (isJSONEncodable(other)) {
return new this(other.toJSON());
}
return new this(other);
}
}
module.exports = SelectMenuBuilder;

View File

@@ -1,11 +1,64 @@
'use strict';
const { SelectMenuComponent: BuildersSelectMenuComponent } = require('@discordjs/builders');
const Transformers = require('../util/Transformers');
const Component = require('./Component');
class SelectMenuComponent extends BuildersSelectMenuComponent {
constructor(data) {
super(Transformers.toSnakeCase(data));
/**
* Represents a select menu component
* @extends {Component}
*/
class SelectMenuComponent extends Component {
/**
* The placeholder for this select menu
* @type {?string}
* @readonly
*/
get placeholder() {
return this.data.placeholder ?? null;
}
/**
* The maximum amount of options that can be selected
* @type {?number}
* @readonly
*/
get maxValues() {
return this.data.max_values ?? null;
}
/**
* The minimum amount of options that must be selected
* @type {?number}
* @readonly
*/
get minValues() {
return this.data.min_values ?? null;
}
/**
* The custom id of this select menu
* @type {string}
* @readonly
*/
get customId() {
return this.data.custom_id;
}
/**
* Whether this select menu is disabled
* @type {?boolean}
* @readonly
*/
get disabled() {
return this.data.disabled ?? null;
}
/**
* The options in this select menu
* @type {APISelectMenuOption[]}
* @readonly
*/
get options() {
return this.data.options;
}
}

View File

@@ -0,0 +1,24 @@
'use strict';
const { TextInputBuilder: BuildersTextInputComponent, isJSONEncodable } = require('@discordjs/builders');
const Transformers = require('../util/Transformers');
class TextInputBuilder extends BuildersTextInputComponent {
constructor(data) {
super(Transformers.toSnakeCase(data));
}
/**
* Creates a new text input builder from json data
* @param {JSONEncodable<APITextInputComponent> | APITextInputComponent} other The other data
* @returns {TextInputBuilder}
*/
static from(other) {
if (isJSONEncodable(other)) {
return new this(other.toJSON());
}
return new this(other);
}
}
module.exports = TextInputBuilder;

View File

@@ -1,11 +1,20 @@
'use strict';
const { TextInputComponent: BuildersTextInputComponent } = require('@discordjs/builders');
const Transformers = require('../util/Transformers');
const Component = require('./Component');
class TextInputComponent extends BuildersTextInputComponent {
constructor(data) {
super(Transformers.toSnakeCase(data));
class TextInputComponent extends Component {
/**
* The custom id of this text input
*/
get customId() {
return this.data.custom_id;
}
/**
* The value for this text input
*/
get value() {
return this.data.value;
}
}

View File

@@ -1,7 +1,7 @@
'use strict';
// This file contains the typedefs for camel-cased json data
const { ComponentType } = require('discord-api-types/v9');
/**
* @typedef {Object} BaseComponentData
* @property {ComponentType} type The type of component
@@ -56,3 +56,34 @@
/**
* @typedef {ActionRowData|ButtonComponentData|SelectMenuComponentData|TextInputComponentData} ComponentData
*/
class Components extends null {
/**
* Transforms API data into a component
* @param {APIMessageComponent|Component} data The data to create the component from
* @returns {Component}
*/
static createComponent(data) {
if (data instanceof Component) {
return data;
}
switch (data.type) {
case ComponentType.ActionRow:
return new ActionRow(data);
case ComponentType.Button:
return new ButtonComponent(data);
case ComponentType.SelectMenu:
return new SelectMenuComponent(data);
default:
throw new Error(`Found unknown component type: ${data.type}`);
}
}
}
module.exports = Components;
const ActionRow = require('../structures/ActionRow');
const ButtonComponent = require('../structures/ButtonComponent');
const Component = require('../structures/Component');
const SelectMenuComponent = require('../structures/SelectMenuComponent');

View File

@@ -1,24 +1,24 @@
import {
ActionRow as BuilderActionRow,
MessageActionRowComponent,
ActionRowBuilder as BuilderActionRow,
MessageActionRowComponentBuilder,
blockQuote,
bold,
ButtonComponent as BuilderButtonComponent,
ButtonBuilder as BuilderButtonComponent,
channelMention,
codeBlock,
Component,
Embed as BuildersEmbed,
EmbedBuilder as BuildersEmbed,
formatEmoji,
hideLinkEmbed,
hyperlink,
inlineCode,
italic,
JSONEncodable,
MappedComponentTypes,
memberNicknameMention,
Modal as BuilderModal,
quote,
roleMention,
SelectMenuComponent as BuilderSelectMenuComponent,
TextInputComponent as BuilderTextInputComponent,
SelectMenuBuilder as BuilderSelectMenuComponent,
TextInputBuilder as BuilderTextInputComponent,
spoiler,
strikethrough,
time,
@@ -26,7 +26,7 @@ import {
TimestampStylesString,
underscore,
userMention,
ModalActionRowComponent,
ModalActionRowComponentBuilder,
} from '@discordjs/builders';
import { Collection } from '@discordjs/collection';
import { BaseImageURLOptions, ImageURLOptions, RawFile, REST, RESTOptions } from '@discordjs/rest';
@@ -105,6 +105,11 @@ import {
APITextInputComponent,
APIModalActionRowComponent,
APIModalComponent,
APISelectMenuOption,
APIEmbedField,
APIEmbedAuthor,
APIEmbedFooter,
APIEmbedImage,
} from 'discord-api-types/v9';
import { ChildProcess } from 'node:child_process';
import { EventEmitter } from 'node:events';
@@ -220,8 +225,10 @@ export interface ActionRowData<T extends ActionRowComponent | ActionRowComponent
components: T[];
}
export class ActionRow<
T extends MessageActionRowComponent | ModalActionRowComponent = MessageActionRowComponent,
export class ActionRowBuilder<
T extends MessageActionRowComponentBuilder | ModalActionRowComponentBuilder =
| MessageActionRowComponentBuilder
| ModalActionRowComponentBuilder,
> extends BuilderActionRow<T> {
constructor(
data?:
@@ -232,6 +239,14 @@ export class ActionRow<
);
}
export type MessageActionRowComponent = ButtonComponent | SelectMenuComponent;
export type ModalActionRowComponent = TextInputComponent;
export class ActionRow<T extends MessageActionRowComponent | ModalActionRowComponent> {
private constructor(data: APIActionRowComponent<APIMessageActionRowComponent>);
public readonly components: T[];
}
export class ActivityFlagsBitField extends BitField<ActivityFlagsString> {
public static Flags: typeof ActivityFlags;
public static resolve(bit?: BitFieldResolvable<ActivityFlagsString, number>): number;
@@ -356,7 +371,9 @@ export interface InteractionResponseFields<Cached extends CacheType = CacheType>
deferReply(options?: InteractionDeferReplyOptions): Promise<void>;
fetchReply(): Promise<GuildCacheMessage<Cached>>;
followUp(options: string | MessagePayload | InteractionReplyOptions): Promise<GuildCacheMessage<Cached>>;
showModal(modal: Modal | ModalData | APIModalInteractionResponseCallbackData): Promise<void>;
showModal(
modal: JSONEncodable<APIModalInteractionResponseCallbackData> | ModalData | APIModalInteractionResponseCallbackData,
): Promise<void>;
}
export abstract class CommandInteraction<Cached extends CacheType = CacheType> extends Interaction<Cached> {
@@ -395,7 +412,9 @@ export abstract class CommandInteraction<Cached extends CacheType = CacheType> e
public followUp(options: string | MessagePayload | InteractionReplyOptions): Promise<GuildCacheMessage<Cached>>;
public reply(options: InteractionReplyOptions & { fetchReply: true }): Promise<GuildCacheMessage<Cached>>;
public reply(options: string | MessagePayload | InteractionReplyOptions): Promise<void>;
public showModal(modal: Modal | ModalData | APIModalInteractionResponseCallbackData): Promise<void>;
public showModal(
modal: JSONEncodable<APIModalInteractionResponseCallbackData> | ModalData | APIModalInteractionResponseCallbackData,
): Promise<void>;
private transformOption(
option: APIApplicationCommandOption,
resolved: APIApplicationCommandInteractionData['resolved'],
@@ -502,22 +521,53 @@ export class ButtonInteraction<Cached extends CacheType = CacheType> extends Mes
public inRawGuild(): this is ButtonInteraction<'raw'>;
}
export class ButtonComponent extends BuilderButtonComponent {
public constructor(data?: ButtonComponentData | (Omit<APIButtonComponent, 'type'> & { type?: ComponentType.Button }));
export class Component<T extends APIMessageComponent | APIModalComponent = APIMessageComponent | APIModalComponent> {
public readonly data: Readonly<T>;
public get type(): T['type'];
public toJSON(): T;
public equals(other: this | T): boolean;
}
export class SelectMenuComponent extends BuilderSelectMenuComponent {
export class ButtonComponent extends Component<APIButtonComponent> {
private constructor(data: APIButtonComponent);
public get style(): ButtonStyle;
public get label(): string | null;
public get emoji(): APIMessageComponentEmoji | null;
public get disabled(): boolean | null;
public get customId(): string | null;
public get url(): string | null;
}
export class ButtonBuilder extends BuilderButtonComponent {
public constructor(data?: ButtonComponentData | (Omit<APIButtonComponent, 'type'> & { type?: ComponentType.Button }));
public static from(other: JSONEncodable<APIButtonComponent> | APIButtonComponent): ButtonBuilder;
}
export class SelectMenuBuilder extends BuilderSelectMenuComponent {
public constructor(
data?: SelectMenuComponentData | (Omit<APISelectMenuComponent, 'type'> & { type?: ComponentType.SelectMenu }),
);
public static from(other: JSONEncodable<APISelectMenuComponent> | APISelectMenuComponent): SelectMenuBuilder;
}
export class TextInputComponent extends BuilderTextInputComponent {
export class TextInputBuilder extends BuilderTextInputComponent {
public constructor(data?: TextInputComponentData | APITextInputComponent);
public static from(other: JSONEncodable<APITextInputComponent> | APITextInputComponent): TextInputBuilder;
}
export class Modal extends BuilderModal {
public constructor(data?: ModalData | APIModalActionRowComponent);
export class TextInputComponent extends Component<APITextInputComponent> {
public get customId(): string;
public get value(): string;
}
export class SelectMenuComponent extends Component<APISelectMenuComponent> {
private constructor(data: APISelectMenuComponent);
public get placeholder(): string | null;
public get maxValues(): number | null;
public get minValues(): number | null;
public get customId(): string;
public get disabled(): boolean | null;
public get options(): APISelectMenuOption[];
}
export interface EmbedData {
@@ -535,18 +585,43 @@ export interface EmbedData {
fields?: EmbedFieldData[];
}
export interface EmbedImageData {
url?: string;
export interface IconData {
iconURL?: string;
proxyIconURL?: string;
}
export type EmbedAuthorData = Omit<APIEmbedAuthor, 'icon_url' | 'proxy_icon_url'> & IconData;
export type EmbedFooterData = Omit<APIEmbedFooter, 'icon_url' | 'proxy_icon_url'> & IconData;
export interface EmbedProviderData {
name?: string;
url?: string;
}
export class Embed extends BuildersEmbed {
export interface EmbedImageData extends Omit<APIEmbedImage, 'proxy_url'> {
proxyURL?: string;
}
export class EmbedBuilder extends BuildersEmbed {
public constructor(data?: EmbedData | APIEmbed);
public override setColor(color: ColorResolvable | null): this;
public static from(other: JSONEncodable<APIEmbed> | APIEmbed): EmbedBuilder;
}
export class Embed {
private constructor(data: APIEmbed);
public readonly data: Readonly<APIEmbed>;
public get fields(): APIEmbedField[] | null;
public get title(): string | null;
public get description(): string | null;
public get url(): string | null;
public get color(): number | null;
public get timestamp(): string | null;
public get thumbnail(): EmbedImageData | null;
public get image(): EmbedImageData | null;
public equals(other: Embed | APIEmbed): boolean;
public toJSON(): APIEmbed;
}
export interface MappedChannelCategoryTypes {
@@ -1652,7 +1727,9 @@ export class MessageComponentInteraction<Cached extends CacheType = CacheType> e
public reply(options: string | MessagePayload | InteractionReplyOptions): Promise<void>;
public update(options: InteractionUpdateOptions & { fetchReply: true }): Promise<GuildCacheMessage<Cached>>;
public update(options: string | MessagePayload | InteractionUpdateOptions): Promise<void>;
public showModal(modal: Modal | ModalData | APIModalInteractionResponseCallbackData): Promise<void>;
public showModal(
modal: JSONEncodable<APIModalInteractionResponseCallbackData> | ModalData | APIModalInteractionResponseCallbackData,
): Promise<void>;
}
export class MessageContextMenuCommandInteraction<
@@ -1741,17 +1818,11 @@ export class MessageReaction {
public toJSON(): unknown;
}
export interface ModalFieldData {
value: string;
type: ComponentType;
customId: string;
}
export class ModalSubmitFieldsResolver {
constructor(components: ModalFieldData[][]);
public components: ModalFieldData[][];
public fields: Collection<string, ModalFieldData>;
public getField(customId: string): ModalFieldData;
constructor(components: ModalActionRowComponent[][]);
public components: ActionRow<ModalActionRowComponent>;
public fields: Collection<string, ModalActionRowComponent>;
public getField(customId: string): ModalActionRowComponent;
public getTextInputValue(customId: string): string;
}
@@ -1769,7 +1840,7 @@ export interface ModalMessageModalSubmitInteraction<Cached extends CacheType = C
export interface ModalSubmitActionRow {
type: ComponentType.ActionRow;
components: ModalFieldData[];
components: ActionRow<TextInputComponent>[];
}
export class ModalSubmitInteraction<Cached extends CacheType = CacheType> extends Interaction<Cached> {
@@ -2452,6 +2523,14 @@ export class Util extends null {
public static splitMessage(text: string, options?: SplitOptions): string[];
}
export class Components extends null {
public static createComponentBuilder<T extends keyof MappedComponentTypes>(
data: APIMessageComponent & { type: T },
): MappedComponentTypes[T];
public static createComponentBuilder<C extends Component>(data: C): C;
public static createComponentBuilder(data: APIMessageComponent | Component): Component;
}
export class Formatters extends null {
public static blockQuote: typeof blockQuote;
public static bold: typeof bold;
@@ -3966,12 +4045,6 @@ export interface EditGuildTemplateOptions {
description?: string;
}
export interface EmbedAuthorData {
name: string;
url?: string;
iconURL?: string;
}
export interface EmbedField {
name: string;
value: string;
@@ -3984,11 +4057,6 @@ export interface EmbedFieldData {
inline?: boolean;
}
export interface EmbedFooterData {
text: string;
iconURL?: string;
}
export type EmojiIdentifierResolvable = string | EmojiResolvable;
export type EmojiResolvable = Snowflake | GuildEmoji | ReactionEmoji;
@@ -4646,7 +4714,11 @@ export interface MessageCollectorOptions extends CollectorOptions<[Message]> {
maxProcessed?: number;
}
export type MessageComponent = Component | ActionRow<MessageActionRowComponent> | ButtonComponent | SelectMenuComponent;
export type MessageComponent =
| Component
| ActionRowBuilder<MessageActionRowComponentBuilder | ModalActionRowComponentBuilder>
| ButtonComponent
| SelectMenuComponent;
export type MessageComponentCollectorOptions<T extends MessageComponentInteraction> = Omit<
InteractionCollectorOptions<T>,
@@ -4666,6 +4738,7 @@ export interface MessageEditOptions {
flags?: BitFieldResolvable<MessageFlagsString, number>;
allowedMentions?: MessageMentionOptions;
components?: (
| JSONEncodable<APIActionRowComponent<APIMessageActionRowComponent>>
| ActionRow<MessageActionRowComponent>
| (Required<BaseComponentData> & ActionRowData<MessageActionRowComponentData | MessageActionRowComponent>)
| APIActionRowComponent<APIMessageActionRowComponent>
@@ -4705,8 +4778,9 @@ export interface MessageOptions {
tts?: boolean;
nonce?: string | number;
content?: string | null;
embeds?: (Embed | APIEmbed)[];
embeds?: (JSONEncodable<APIEmbed> | APIEmbed)[];
components?: (
| JSONEncodable<APIActionRowComponent<APIMessageActionRowComponent>>
| ActionRow<MessageActionRowComponent>
| (Required<BaseComponentData> & ActionRowData<MessageActionRowComponentData | MessageActionRowComponent>)
| APIActionRowComponent<APIMessageActionRowComponent>
@@ -5255,6 +5329,8 @@ export {
ApplicationCommandType,
ApplicationCommandOptionType,
ApplicationCommandPermissionType,
APIEmbedField,
APISelectMenuOption,
AuditLogEvent,
ButtonStyle,
ChannelType,
@@ -5290,12 +5366,13 @@ export {
WebhookType,
} from 'discord-api-types/v9';
export {
UnsafeButtonComponent,
UnsafeSelectMenuComponent,
SelectMenuOption,
UnsafeSelectMenuOption,
MessageActionRowComponent,
UnsafeEmbed,
ModalActionRowComponent,
UnsafeButtonBuilder,
UnsafeSelectMenuBuilder,
SelectMenuOptionBuilder,
UnsafeSelectMenuOptionBuilder,
MessageActionRowComponentBuilder,
ModalActionRowComponentBuilder,
UnsafeEmbedBuilder,
ModalBuilder,
} from '@discordjs/builders';
export { DiscordAPIError, HTTPError, RateLimitError } from '@discordjs/rest';

View File

@@ -20,6 +20,8 @@ import {
AuditLogEvent,
ButtonStyle,
TextInputStyle,
APITextInputComponent,
APIEmbed,
} from 'discord-api-types/v9';
import {
ApplicationCommand,
@@ -59,7 +61,7 @@ import {
MessageCollector,
MessageComponentInteraction,
MessageReaction,
Modal,
ModalBuilder,
NewsChannel,
Options,
PartialTextBasedChannelFields,
@@ -95,10 +97,10 @@ import {
GuildAuditLogs,
StageInstance,
PartialDMChannel,
ActionRow,
ActionRowBuilder,
ButtonComponent,
SelectMenuComponent,
MessageActionRowComponent,
MessageActionRowComponentBuilder,
InteractionResponseFields,
ThreadChannelType,
Events,
@@ -109,6 +111,12 @@ import {
MessageActionRowComponentData,
PartialThreadMember,
ThreadMemberFlagsBitField,
ButtonBuilder,
EmbedBuilder,
MessageActionRowComponent,
SelectMenuBuilder,
TextInputBuilder,
TextInputComponent,
Embed,
} from '.';
import { expectAssignable, expectDeprecated, expectNotAssignable, expectNotType, expectType } from 'tsd';
@@ -574,7 +582,7 @@ client.on('messageCreate', async message => {
assertIsMessage(channel.send({ embeds: [] }));
const attachment = new MessageAttachment('file.png');
const embed = new Embed();
const embed = new EmbedBuilder();
assertIsMessage(channel.send({ files: [attachment] }));
assertIsMessage(channel.send({ embeds: [embed] }));
assertIsMessage(channel.send({ embeds: [embed], files: [attachment] }));
@@ -744,23 +752,24 @@ client.on('interactionCreate', async interaction => {
if (!interaction.isCommand()) return;
void new ActionRow<MessageActionRowComponent>();
void new ActionRowBuilder<MessageActionRowComponentBuilder>();
const button = new ButtonComponent();
const button = new ButtonBuilder();
const actionRow = new ActionRow<MessageActionRowComponent>({
const actionRow = new ActionRowBuilder<MessageActionRowComponentBuilder>({
type: ComponentType.ActionRow,
components: [button.toJSON()],
});
actionRow.toJSON();
await interaction.reply({ content: 'Hi!', components: [actionRow] });
// @ts-expect-error
interaction.reply({ content: 'Hi!', components: [[button]] });
// @ts-expect-error
void new ActionRow({});
void new ActionRowBuilder({});
// @ts-expect-error
await interaction.reply({ content: 'Hi!', components: [button] });
@@ -1336,34 +1345,34 @@ expectType<CategoryChannel | NewsChannel | StageChannel | StoreChannel | TextCha
);
expectType<NewsChannel | TextChannel | ThreadChannel>(GuildTextBasedChannel);
const button = new ButtonComponent({
const button = new ButtonBuilder({
label: 'test',
style: ButtonStyle.Primary,
customId: 'test',
});
const selectMenu = new SelectMenuComponent({
const selectMenu = new SelectMenuBuilder({
maxValues: 10,
minValues: 2,
customId: 'test',
});
new ActionRow({
new ActionRowBuilder({
components: [selectMenu.toJSON(), button.toJSON()],
});
new SelectMenuComponent({
new SelectMenuBuilder({
customId: 'foo',
});
new ButtonComponent({
new ButtonBuilder({
style: ButtonStyle.Danger,
});
// @ts-expect-error
new Embed().setColor('abc');
new EmbedBuilder().setColor('abc');
new Embed().setColor('#ffffff');
new EmbedBuilder().setColor('#ffffff');
expectNotAssignable<ActionRowData<MessageActionRowComponentData>>({
type: ComponentType.ActionRow,
@@ -1379,7 +1388,7 @@ declare const chatInputInteraction: ChatInputCommandInteraction;
expectType<MessageAttachment>(chatInputInteraction.options.getAttachment('attachment', true));
expectType<MessageAttachment | null>(chatInputInteraction.options.getAttachment('attachment'));
declare const modal: Modal;
declare const modal: ModalBuilder;
chatInputInteraction.showModal(modal);
@@ -1400,3 +1409,27 @@ chatInputInteraction.showModal({
},
],
});
declare const selectMenuData: APISelectMenuComponent;
SelectMenuBuilder.from(selectMenuData);
declare const selectMenuComp: SelectMenuComponent;
SelectMenuBuilder.from(selectMenuComp);
declare const buttonData: APIButtonComponent;
ButtonBuilder.from(buttonData);
declare const buttonComp: ButtonComponent;
ButtonBuilder.from(buttonComp);
declare const textInputData: APITextInputComponent;
TextInputBuilder.from(textInputData);
declare const textInputComp: TextInputComponent;
TextInputBuilder.from(textInputComp);
declare const embedData: APIEmbed;
EmbedBuilder.from(embedData);
declare const embedComp: Embed;
EmbedBuilder.from(embedComp);