From 4663e0ea1723419082799556b31d0c11008a5f9d Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Fri, 25 Apr 2025 19:46:34 +0200 Subject: [PATCH 01/12] feat: components v2 --- packages/discord.js/src/index.js | 9 + .../discord.js/src/structures/Component.js | 9 + .../src/structures/ContainerComponent.js | 60 ++++++ .../src/structures/FileComponent.js | 40 ++++ .../src/structures/MediaGalleryComponent.js | 31 +++ .../src/structures/MediaGalleryItem.js | 51 +++++ packages/discord.js/src/structures/Message.js | 31 +-- .../structures/MessageComponentInteraction.js | 29 ++- .../src/structures/SectionComponent.js | 42 ++++ .../src/structures/SeparatorComponent.js | 30 +++ .../src/structures/TextDisplayComponent.js | 20 ++ .../src/structures/ThumbnailComponent.js | 49 +++++ .../src/structures/UnfurledMediaItem.js | 25 +++ .../structures/interfaces/TextBasedChannel.js | 11 +- packages/discord.js/src/util/APITypes.js | 44 ++++- packages/discord.js/src/util/Components.js | 148 +++++++++++++-- packages/discord.js/typings/index.d.ts | 179 +++++++++++++++++- packages/discord.js/typings/index.test-d.ts | 61 ++++++ 18 files changed, 807 insertions(+), 62 deletions(-) create mode 100644 packages/discord.js/src/structures/ContainerComponent.js create mode 100644 packages/discord.js/src/structures/FileComponent.js create mode 100644 packages/discord.js/src/structures/MediaGalleryComponent.js create mode 100644 packages/discord.js/src/structures/MediaGalleryItem.js create mode 100644 packages/discord.js/src/structures/SectionComponent.js create mode 100644 packages/discord.js/src/structures/SeparatorComponent.js create mode 100644 packages/discord.js/src/structures/TextDisplayComponent.js create mode 100644 packages/discord.js/src/structures/ThumbnailComponent.js create mode 100644 packages/discord.js/src/structures/UnfurledMediaItem.js diff --git a/packages/discord.js/src/index.js b/packages/discord.js/src/index.js index 668b6f080dff..6894eb1bca92 100644 --- a/packages/discord.js/src/index.js +++ b/packages/discord.js/src/index.js @@ -130,12 +130,14 @@ exports.CommandInteraction = require('./structures/CommandInteraction.js').Comma exports.CommandInteractionOptionResolver = require('./structures/CommandInteractionOptionResolver.js').CommandInteractionOptionResolver; exports.Component = require('./structures/Component.js').Component; +exports.ContainerComponent = require('./structures/ContainerComponent.js').ContainerComponent; exports.ContextMenuCommandInteraction = require('./structures/ContextMenuCommandInteraction.js').ContextMenuCommandInteraction; exports.DMChannel = require('./structures/DMChannel.js').DMChannel; exports.Embed = require('./structures/Embed.js').Embed; exports.Emoji = require('./structures/Emoji.js').Emoji; exports.Entitlement = require('./structures/Entitlement.js').Entitlement; +exports.FileComponent = require('./structures/FileComponent.js').FileComponent; exports.ForumChannel = require('./structures/ForumChannel.js').ForumChannel; exports.Guild = require('./structures/Guild.js').Guild; exports.GuildAuditLogs = require('./structures/GuildAuditLogs.js').GuildAuditLogs; @@ -164,6 +166,8 @@ exports.InteractionWebhook = require('./structures/InteractionWebhook.js').Inter exports.Invite = require('./structures/Invite.js').Invite; exports.InviteGuild = require('./structures/InviteGuild.js').InviteGuild; exports.MediaChannel = require('./structures/MediaChannel.js').MediaChannel; +exports.MediaGalleryComponent = require('./structures/MediaGalleryComponent.js').MediaGalleryComponent; +exports.MediaGalleryItem = require('./structures/MediaGalleryItem.js').MediaGalleryItem; exports.MentionableSelectMenuComponent = require('./structures/MentionableSelectMenuComponent.js').MentionableSelectMenuComponent; exports.MentionableSelectMenuInteraction = @@ -195,6 +199,8 @@ exports.RichPresenceAssets = require('./structures/Presence.js').RichPresenceAss exports.Role = require('./structures/Role.js').Role; exports.RoleSelectMenuComponent = require('./structures/RoleSelectMenuComponent.js').RoleSelectMenuComponent; exports.RoleSelectMenuInteraction = require('./structures/RoleSelectMenuInteraction.js').RoleSelectMenuInteraction; +exports.SectionComponent = require('./structures/SectionComponent.js').SectionComponent; +exports.SeparatorComponent = require('./structures/SeparatorComponent.js').SeparatorComponent; exports.SKU = require('./structures/SKU.js').SKU; exports.StageChannel = require('./structures/StageChannel.js').StageChannel; exports.StageInstance = require('./structures/StageInstance.js').StageInstance; @@ -207,11 +213,14 @@ exports.Subscription = require('./structures/Subscription.js').Subscription; exports.Team = require('./structures/Team.js').Team; exports.TeamMember = require('./structures/TeamMember.js').TeamMember; exports.TextChannel = require('./structures/TextChannel.js').TextChannel; +exports.TextDisplayComponent = require('./structures/TextDisplayComponent.js').TextDisplayComponent; exports.TextInputComponent = require('./structures/TextInputComponent.js').TextInputComponent; exports.ThreadChannel = require('./structures/ThreadChannel.js').ThreadChannel; exports.ThreadMember = require('./structures/ThreadMember.js').ThreadMember; exports.ThreadOnlyChannel = require('./structures/ThreadOnlyChannel.js').ThreadOnlyChannel; +exports.ThumbnailComponent = require('./structures/ThumbnailComponent.js').ThumbnailComponent; exports.Typing = require('./structures/Typing.js').Typing; +exports.UnfurledMediaItem = require('./structures/UnfurledMediaItem.js').UnfurledMediaItem; exports.User = require('./structures/User.js').User; exports.UserContextMenuCommandInteraction = require('./structures/UserContextMenuCommandInteraction.js').UserContextMenuCommandInteraction; diff --git a/packages/discord.js/src/structures/Component.js b/packages/discord.js/src/structures/Component.js index fbe464f76278..773335bba4b4 100644 --- a/packages/discord.js/src/structures/Component.js +++ b/packages/discord.js/src/structures/Component.js @@ -14,6 +14,15 @@ class Component { this.data = data; } + /** + * The id of this component + * @type {number} + * @readonly + */ + get id() { + return this.data.id; + } + /** * The type of the component * @type {ComponentType} diff --git a/packages/discord.js/src/structures/ContainerComponent.js b/packages/discord.js/src/structures/ContainerComponent.js new file mode 100644 index 000000000000..3eb412e96c18 --- /dev/null +++ b/packages/discord.js/src/structures/ContainerComponent.js @@ -0,0 +1,60 @@ +'use strict'; + +const { Component } = require('./Component.js'); +const { createComponent } = require('../util/Components.js'); + +/** + * Represents a container component + * @extends {Component} + */ +class ContainerComponent extends Component { + constructor({ components, ...data }) { + super(data); + + /** + * The components in this container + * @type {Component[]} + * @readonly + */ + this.components = components.map(component => createComponent(component)); + } + + /** + * The accent color of this container + * @type {?number} + * @readonly + */ + get accentColor() { + return this.data.accent_color ?? null; + } + + /** + * The hex accent color of this container + * @type {?string} + * @readonly + */ + get hexAccentColor() { + return typeof this.data.accent_color === 'number' + ? `#${this.data.accent_color.toString(16).padStart(6, '0')}` + : (this.data.accent_color ?? null); + } + + /** + * Whether this container is spoilered + * @type {boolean} + * @readonly + */ + get spoiler() { + return this.data.spoiler ?? false; + } + + /** + * Returns the API-compatible JSON for this component + * @returns {APIContainerComponent} + */ + toJSON() { + return { ...this.data, components: this.components.map(component => component.toJSON()) }; + } +} + +exports.ContainerComponent = ContainerComponent; diff --git a/packages/discord.js/src/structures/FileComponent.js b/packages/discord.js/src/structures/FileComponent.js new file mode 100644 index 000000000000..e63750281da4 --- /dev/null +++ b/packages/discord.js/src/structures/FileComponent.js @@ -0,0 +1,40 @@ +'use strict'; + +const Component = require('./Component.js'); +const UnfurledMediaItem = require('./UnfurledMediaItem.js'); + +/** + * Represents a file component + * @extends {Component} + */ +class FileComponent extends Component { + constructor({ file, ...data }) { + super(data); + + /** + * The media associated with this file + * @type {UnfurledMediaItem} + * @readonly + */ + this.file = new UnfurledMediaItem(file); + } + + /** + * Whether this thumbnail is spoilered + * @type {boolean} + * @readonly + */ + get spoiler() { + return this.data.spoiler ?? false; + } + + /** + * Returns the API-compatible JSON for this component + * @returns {APIFileComponent} + */ + toJSON() { + return { ...this.data, file: this.file.toJSON() }; + } +} + +exports.FileComponent = FileComponent; diff --git a/packages/discord.js/src/structures/MediaGalleryComponent.js b/packages/discord.js/src/structures/MediaGalleryComponent.js new file mode 100644 index 000000000000..3531e3c163d9 --- /dev/null +++ b/packages/discord.js/src/structures/MediaGalleryComponent.js @@ -0,0 +1,31 @@ +'use strict'; + +const { Component } = require('./Component.js'); +const { MediaGalleryItem } = require('./MediaGalleryItem.js'); + +/** + * Represents a media gallery component + * @extends {Component} + */ +class MediaGalleryComponent extends Component { + constructor({ items, ...data }) { + super(data); + + /** + * The items in this media gallery + * @type {MediaGalleryItem[]} + * @readonly + */ + this.items = items.map(item => new MediaGalleryItem(item)); + } + + /** + * Returns the API-compatible JSON for this component + * @returns {APIMediaGalleryComponent} + */ + toJSON() { + return { ...this.data, items: this.items.map(item => item.toJSON()) }; + } +} + +exports.MediaGalleryComponent = MediaGalleryComponent; diff --git a/packages/discord.js/src/structures/MediaGalleryItem.js b/packages/discord.js/src/structures/MediaGalleryItem.js new file mode 100644 index 000000000000..7b608dcdace6 --- /dev/null +++ b/packages/discord.js/src/structures/MediaGalleryItem.js @@ -0,0 +1,51 @@ +'use strict'; + +const { UnfurledMediaItem } = require('./UnfurledMediaItem.js'); + +/** + * Represents an item in a media gallery + */ +class MediaGalleryItem { + constructor({ media, ...data }) { + /** + * The API data associated with this component + * @type {APIMediaGalleryItem} + */ + this.data = data; + + /** + * The media associated with this media gallery item + * @type {UnfurledMediaItem} + * @readonly + */ + this.media = new UnfurledMediaItem(media); + } + + /** + * The description of this media gallery item + * @type {?string} + * @readonly + */ + get description() { + return this.data.description ?? null; + } + + /** + * Whether this media gallery item is spoilered + * @type {boolean} + * @readonly + */ + get spoiler() { + return this.data.spoiler ?? false; + } + + /** + * Returns the API-compatible JSON for this component + * @returns {APIMediaGalleryItem} + */ + toJSON() { + return { ...this.data, media: this.media.toJSON() }; + } +} + +exports.MediaGalleryItem = MediaGalleryItem; diff --git a/packages/discord.js/src/structures/Message.js b/packages/discord.js/src/structures/Message.js index 8a07ed612e80..d5438f1c3668 100644 --- a/packages/discord.js/src/structures/Message.js +++ b/packages/discord.js/src/structures/Message.js @@ -14,6 +14,7 @@ const { const { Attachment } = require('./Attachment.js'); const { Base } = require('./Base.js'); const { ClientApplication } = require('./ClientApplication.js'); +const { Component } = require('./Component.js'); const { Embed } = require('./Embed.js'); const { InteractionCollector } = require('./InteractionCollector.js'); const { MessageMentions } = require('./MessageMentions.js'); @@ -23,7 +24,7 @@ const { ReactionCollector } = require('./ReactionCollector.js'); const { Sticker } = require('./Sticker.js'); const { DiscordjsError, ErrorCodes } = require('../errors/index.js'); const { ReactionManager } = require('../managers/ReactionManager.js'); -const { createComponent } = require('../util/Components.js'); +const { createComponent, findComponentByCustomId } = require('../util/Components.js'); const { NonSystemMessageTypes, MaxBulkDeletableMessageAge, UndeletableMessageTypes } = require('../util/Constants.js'); const { MessageFlagsBitField } = require('../util/MessageFlagsBitField.js'); const { PermissionsBitField } = require('../util/PermissionsBitField.js'); @@ -151,10 +152,10 @@ class Message extends Base { if ('components' in data) { /** - * An array of action rows in the message. + * An array of components in the message. * This property requires the {@link GatewayIntentBits.MessageContent} privileged intent * in a guild for messages that do not mention the client. - * @type {ActionRow[]} + * @type {Component[]} */ this.components = data.components.map(component => createComponent(component)); } else { @@ -683,8 +684,8 @@ class Message extends Base { get editable() { const precheck = Boolean( this.author.id === this.client.user.id && - (!this.guild || this.channel?.viewable) && - this.reference?.type !== MessageReferenceType.Forward, + (!this.guild || this.channel?.viewable) && + this.reference?.type !== MessageReferenceType.Forward, ); // Regardless of permissions thread messages cannot be edited if @@ -755,9 +756,9 @@ class Message extends Base { const { channel } = this; return Boolean( !this.system && - (!this.guild || - (channel?.viewable && - channel?.permissionsFor(this.client.user)?.has(PermissionFlagsBits.ManageMessages, false))), + (!this.guild || + (channel?.viewable && + channel?.permissionsFor(this.client.user)?.has(PermissionFlagsBits.ManageMessages, false))), ); } @@ -787,12 +788,12 @@ class Message extends Base { const { channel } = this; return Boolean( channel?.type === ChannelType.GuildAnnouncement && - !this.flags.has(MessageFlags.Crossposted) && - this.reference?.type !== MessageReferenceType.Forward && - this.type === MessageType.Default && - !this.poll && - channel.viewable && - channel.permissionsFor(this.client.user)?.has(bitfield, false), + !this.flags.has(MessageFlags.Crossposted) && + this.reference?.type !== MessageReferenceType.Forward && + this.type === MessageType.Default && + !this.poll && + channel.viewable && + channel.permissionsFor(this.client.user)?.has(bitfield, false), ); } @@ -1032,7 +1033,7 @@ class Message extends Base { * @returns {?MessageActionRowComponent} */ resolveComponent(customId) { - return this.components.flatMap(row => row.components).find(component => component.customId === customId) ?? null; + return findComponentByCustomId(this.components, customId); } /** diff --git a/packages/discord.js/src/structures/MessageComponentInteraction.js b/packages/discord.js/src/structures/MessageComponentInteraction.js index 296451d84259..2089547e4778 100644 --- a/packages/discord.js/src/structures/MessageComponentInteraction.js +++ b/packages/discord.js/src/structures/MessageComponentInteraction.js @@ -4,6 +4,7 @@ const { lazy } = require('@discordjs/util'); const { BaseInteraction } = require('./BaseInteraction.js'); const { InteractionWebhook } = require('./InteractionWebhook.js'); const { InteractionResponses } = require('./interfaces/InteractionResponses.js'); +const { findComponentByCustomId } = require('../util/Components.js'); const getMessage = lazy(() => require('./Message.js').Message); @@ -79,28 +80,26 @@ class MessageComponentInteraction extends BaseInteraction { /** * The component which was interacted with - * @type {MessageActionRowComponent|APIMessageActionRowComponent} + * @type {MessageActionRowComponent|APIComponentInMessageActionRow} * @readonly */ get component() { - return this.message.components - .flatMap(row => row.components) - .find(component => (component.customId ?? component.custom_id) === this.customId); + return findComponentByCustomId(this.message.components, this.customId); } // These are here only for documentation purposes - they are implemented by InteractionResponses /* eslint-disable no-empty-function */ - deferReply() {} - reply() {} - fetchReply() {} - editReply() {} - deleteReply() {} - followUp() {} - deferUpdate() {} - update() {} - launchActivity() {} - showModal() {} - awaitModalSubmit() {} + deferReply() { } + reply() { } + fetchReply() { } + editReply() { } + deleteReply() { } + followUp() { } + deferUpdate() { } + update() { } + launchActivity() { } + showModal() { } + awaitModalSubmit() { } } InteractionResponses.applyToClass(MessageComponentInteraction); diff --git a/packages/discord.js/src/structures/SectionComponent.js b/packages/discord.js/src/structures/SectionComponent.js new file mode 100644 index 000000000000..6161332c4dd5 --- /dev/null +++ b/packages/discord.js/src/structures/SectionComponent.js @@ -0,0 +1,42 @@ +'use strict'; + +const { Component } = require('./Component.js'); +const { createComponent } = require('../util/Components.js'); + +/** + * Represents a section component + * @extends {Component} + */ +class SectionComponent extends Component { + constructor({ accessory, components, ...data }) { + super(data); + + /** + * The components in this section + * @type {Component[]} + * @readonly + */ + this.components = components.map(component => createComponent(component)); + + /** + * The accessory component of this section + * @type {Component} + * @readonly + */ + this.accessory = createComponent(accessory); + } + + /** + * Returns the API-compatible JSON for this component + * @returns {APISectionComponent} + */ + toJSON() { + return { + ...this.data, + accessory: this.accessory.toJSON(), + components: this.components.map(component => component.toJSON()), + }; + } +} + +exports.SectionComponent = SectionComponent; diff --git a/packages/discord.js/src/structures/SeparatorComponent.js b/packages/discord.js/src/structures/SeparatorComponent.js new file mode 100644 index 000000000000..d4861cd0623f --- /dev/null +++ b/packages/discord.js/src/structures/SeparatorComponent.js @@ -0,0 +1,30 @@ +'use strict'; + +const { SeparatorSpacingSize } = require('discord-api-types/v10'); +const { Component } = require('./Component.js'); + +/** + * Represents a separator component + * @extends {Component} + */ +class SeparatorComponent extends Component { + /** + * The spacing of this separator + * @type {SeparatorSpacingSize} + * @readonly + */ + get spacing() { + return this.data.spacing ?? SeparatorSpacingSize.Small; + } + + /** + * Whether this separator is a divider + * @type {boolean} + * @readonly + */ + get divider() { + return this.data.divider ?? true; + } +} + +exports.SeparatorComponent = SeparatorComponent; diff --git a/packages/discord.js/src/structures/TextDisplayComponent.js b/packages/discord.js/src/structures/TextDisplayComponent.js new file mode 100644 index 000000000000..b31624f27326 --- /dev/null +++ b/packages/discord.js/src/structures/TextDisplayComponent.js @@ -0,0 +1,20 @@ +'use strict'; + +const { Component } = require('./Component.js'); + +/** + * Represents a text display component + * @extends {Component} + */ +class TextDisplayComponent extends Component { + /** + * The content of this text display + * @type {string} + * @readonly + */ + get content() { + return this.data.content; + } +} + +exports.TextDisplayComponent = TextDisplayComponent; diff --git a/packages/discord.js/src/structures/ThumbnailComponent.js b/packages/discord.js/src/structures/ThumbnailComponent.js new file mode 100644 index 000000000000..9a8c70369908 --- /dev/null +++ b/packages/discord.js/src/structures/ThumbnailComponent.js @@ -0,0 +1,49 @@ +'use strict'; + +const { Component } = require('./Component.js'); +const { UnfurledMediaItem } = require('./UnfurledMediaItem.js'); + +/** + * Represents a thumbnail component + * @extends {Component} + */ +class ThumbnailComponent extends Component { + constructor({ media, ...data }) { + super(data); + + /** + * The media associated with this thumbnail + * @type {UnfurledMediaItem} + * @readonly + */ + this.media = new UnfurledMediaItem(media); + } + + /** + * The description of this thumbnail + * @type {?string} + * @readonly + */ + get description() { + return this.data.description ?? null; + } + + /** + * Whether this thumbnail is spoilered + * @type {boolean} + * @readonly + */ + get spoiler() { + return this.data.spoiler ?? false; + } + + /** + * Returns the API-compatible JSON for this component + * @returns {APIThumbnailComponent} + */ + toJSON() { + return { ...this.data, media: this.media.toJSON() }; + } +} + +exports.ThumbnailComponent = ThumbnailComponent; diff --git a/packages/discord.js/src/structures/UnfurledMediaItem.js b/packages/discord.js/src/structures/UnfurledMediaItem.js new file mode 100644 index 000000000000..79e0dcf3bc10 --- /dev/null +++ b/packages/discord.js/src/structures/UnfurledMediaItem.js @@ -0,0 +1,25 @@ +'use strict'; + +/** + * Represents a media item in a component + */ +class UnfurledMediaItem { + constructor(data) { + /** + * The API data associated with this media item + * @type {APIUnfurledMediaItem} + */ + this.data = data; + } + + /** + * The URL of this media gallery item + * @type {string} + * @readonly + */ + get url() { + return this.data.url; + } +} + +exports.UnfurledMediaItem = UnfurledMediaItem; diff --git a/packages/discord.js/src/structures/interfaces/TextBasedChannel.js b/packages/discord.js/src/structures/interfaces/TextBasedChannel.js index d7ab76b6e6d5..8b32dee1ad9c 100644 --- a/packages/discord.js/src/structures/interfaces/TextBasedChannel.js +++ b/packages/discord.js/src/structures/interfaces/TextBasedChannel.js @@ -77,8 +77,11 @@ class TextBasedChannel { * (see {@link https://discord.com/developers/docs/resources/message#allowed-mentions-object here} for more details) * @property {Array<(AttachmentBuilder|Attachment|AttachmentPayload|BufferResolvable)>} [files] * The files to send with the message. - * @property {Array<(ActionRowBuilder|ActionRow|APIActionRowComponent)>} [components] - * Action rows containing interactive components for the message (buttons, select menus) + * @property {Array<(ActionRowBuilder|MessageTopLevelComponent|APIMessageTopLevelComponent)>} [components] + * Action rows containing interactive components for the message (buttons, select menus) and other + * top-level components. + * When using components v2, the flag {@link MessageFlags.IsComponentsV2} needs to be set + * and `content`, `embeds`, `stickers`, and `poll` cannot be used. */ /** @@ -98,7 +101,9 @@ class TextBasedChannel { * that message will be returned and no new message will be created * @property {StickerResolvable[]} [stickers=[]] The stickers to send in the message * @property {MessageFlags} [flags] Which flags to set for the message. - * Only `MessageFlags.SuppressEmbeds` and `MessageFlags.SuppressNotifications` can be set. + * Only {@link MessageFlags.SuppressEmbeds}, {@link MessageFlags.SuppressNotifications} and + * {@link MessageFlags.IsComponentsV2} can be set. + * {@link MessageFlags.IsComponentsV2} is required if passing components that aren't action rows */ /** diff --git a/packages/discord.js/src/util/APITypes.js b/packages/discord.js/src/util/APITypes.js index c40af6c289cc..60025d98a772 100644 --- a/packages/discord.js/src/util/APITypes.js +++ b/packages/discord.js/src/util/APITypes.js @@ -60,6 +60,11 @@ * @see {@link https://discord-api-types.dev/api/discord-api-types-v10#APIChannelSelectComponent} */ +/** + * @external APIContainerComponent + * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIContainerComponent} + */ + /** * @external APIEmbed * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIEmbed} @@ -80,6 +85,11 @@ * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIEmoji} */ +/** + * @external APIFileComponent + * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIFileComponent} + */ + /** * @external APIGuild * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIGuild} @@ -135,6 +145,16 @@ * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIInteractionGuildMember} */ +/** + * @external APIMediaGalleryComponent + * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIMediaGalleryComponent} + */ + +/** + * @external APIMediaGalleryItem + * @se {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIMediaGalleryItem} + */ + /** * @external APIMentionableSelectComponent * @see {@link https://discord-api-types.dev/api/discord-api-types-v10#APIMentionableSelectComponent} @@ -146,8 +166,8 @@ */ /** - * @external APIMessageActionRowComponent - * @see {@link https://discord-api-types.dev/api/discord-api-types-v10#APIMessageActionRowComponent} + * @external APIComponentInMessageActionRow + * @see {@link https://discord-api-types.dev/api/discord-api-types-v10#APIComponentInMessageActionRow} */ /** @@ -165,6 +185,11 @@ * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIMessageInteractionMetadata} */ +/** + * @external APIMessageTopLevelComponent + * @see {@link https://discord-api-types.dev/api/discord-api-types-v10#APIMessageTopLevelComponent} + */ + /** * @external APIModalInteractionResponse * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIModalInteractionResponse} @@ -210,6 +235,11 @@ * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APISelectMenuOption} */ +/** + * @external APISectionComponent + * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APISectionComponent} + */ + /** * @external APISticker * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APISticker} @@ -225,6 +255,16 @@ * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APITextInputComponent} */ +/** + * @external APIThumbnailComponent + * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIThumbnailComponent} + */ + +/** + * @external APIUnfurledMediaItem + * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIUnfurledMediaItem} + */ + /** * @external APIUser * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIUser} diff --git a/packages/discord.js/src/util/Components.js b/packages/discord.js/src/util/Components.js index 5144de04564e..9d010d18df86 100644 --- a/packages/discord.js/src/util/Components.js +++ b/packages/discord.js/src/util/Components.js @@ -4,6 +4,7 @@ const { ComponentType } = require('discord-api-types/v10'); /** * @typedef {Object} BaseComponentData + * @property {number} [id] the id of this component * @property {ComponentType} type The type of component */ @@ -15,30 +16,30 @@ const { ComponentType } = require('discord-api-types/v10'); /** * @typedef {BaseComponentData} ButtonComponentData * @property {ButtonStyle} style The style of the button - * @property {?boolean} disabled Whether this button is disabled + * @property {boolean} [disabled] Whether this button is disabled * @property {string} label The label of this button - * @property {?APIMessageComponentEmoji} emoji The emoji on this button - * @property {?string} customId The custom id of the button - * @property {?string} url The URL of the button + * @property {APIMessageComponentEmoji} [emoji] The emoji on this button + * @property {string} [customId] The custom id of the button + * @property {string} [url] The URL of the button */ /** * @typedef {object} SelectMenuComponentOptionData * @property {string} label The label of the option * @property {string} value The value of the option - * @property {?string} description The description of the option - * @property {?APIMessageComponentEmoji} emoji The emoji on the option - * @property {?boolean} default Whether this option is selected by default + * @property {string} [description] The description of the option + * @property {APIMessageComponentEmoji} [emoji] The emoji on the option + * @property {boolean} [default] Whether this option is selected by default */ /** * @typedef {BaseComponentData} SelectMenuComponentData * @property {string} customId The custom id of the select menu - * @property {?boolean} disabled Whether the select menu is disabled or not - * @property {?number} maxValues The maximum amount of options that can be selected - * @property {?number} minValues The minimum amount of options that can be selected - * @property {?SelectMenuComponentOptionData[]} options The options in this select menu - * @property {?string} placeholder The placeholder of the select menu + * @property {boolean} [disabled] Whether the select menu is disabled or not + * @property {number} [maxValues] The maximum amount of options that can be selected + * @property {number} [minValues] The minimum amount of options that can be selected + * @property {SelectMenuComponentOptionData[]} [options] The options in this select menu + * @property {string} [placeholder] The placeholder of the select menu */ /** @@ -50,17 +51,83 @@ const { ComponentType } = require('discord-api-types/v10'); * @property {string} customId The custom id of the text input * @property {TextInputStyle} style The style of the text input * @property {string} label The text that appears on top of the text input field - * @property {?number} minLength The minimum number of characters that can be entered in the text input - * @property {?number} maxLength The maximum number of characters that can be entered in the text input - * @property {?boolean} required Whether or not the text input is required or not - * @property {?string} value The pre-filled text in the text input - * @property {?string} placeholder Placeholder for the text input + * @property {number} [minLength] The minimum number of characters that can be entered in the text input + * @property {number} [maxLength] The maximum number of characters that can be entered in the text input + * @property {boolean} [required] Whether or not the text input is required or not + * @property {string} [value] The pre-filled text in the text input + * @property {string} [placeholder] Placeholder for the text input */ /** - * @typedef {ActionRowData|ButtonComponentData|SelectMenuComponentData|TextInputComponentData} ComponentData + * @typedef {Object} UnfurledMediaItemData + * @property {string} url The url of this media item. Accepts either http:, https: or attachment: protocol */ +/** + * @typedef {BaseComponentData} ThumbnailComponentData + * @property {UnfurledMediaItemData} media The media for the thumbnail + * @property {string} [description] The description of the thumbnail + * @property {boolean} [spoiler] Whether the thumbnail should be spoilered + */ + +/** + * @typedef {BaseComponentData} FileComponentData + * @property {UnfurledMediaItemData} file The file media in this component + * @property {boolean} [spoiler] Whether the file should be spoilered + */ + +/** + * @typedef {Object} MediaGalleryItemData + * @property {UnfurledMediaItemData} media The media for the media gallery item + * @property {string} [description] The description of the media gallery item + * @property {boolean} [spoiler] Whether the media gallery item should be spoilered + */ + +/** + * @typedef {BaseComponentData} MediaGalleryComponentData + * @property {MediaGalleryItemData[]} items The media gallery items in this media gallery component + */ + +/** + * @typedef {BaseComponentData} SeparatorComponentData + * @property {SeparatorSpacingSize} [spacing] The spacing size of this component + * @property {boolean} [divider] Whether the separator shows as a divider + */ + +/** + * @typedef {BaseComponentData} SectionComponentData + * @property {Components[]} components The components in this section + * @property {ButtonComponentData|ThumbnailComponentData} accessory The accessory shown next to this section + */ + +/** + * @typedef {BaseComponentData} TextDisplayComponentData + * @property {string} content The content displayed in this component + */ + +/** + * @typedef {ActionRowData|FileComponentData|MediaGalleryComponentData|SectionComponentData| + * SeparatorComponentData|TextDisplayComponentData} ComponentInContainerData +*/ + +/** +* @typedef {BaseComponentData} ContainerComponentData +* @property {ComponentInContainerData} components The components in this container +* @property {?number} [accentColor] The accent color of this container +* @property {boolean} [spoiler] Whether the container should be spoilered +*/ + +/** +* @typedef {ActionRowData|ButtonComponentData|SelectMenuComponentData|TextInputComponentData| +* ThumbnailComponentData|FileComponentData|MediaGalleryComponentData|SeparatorComponentData| +* SectionComponentData|TextDisplayComponentData|ContainerComponentData} ComponentData + */ + +/** + * @typedef {ActionRow|ContainerComponent|FileComponent|MediaGalleryComponent| + * SectionComponent|SeparatorComponent|TextDisplayComponent} MessageTopLevelComponent +*/ + /** * Transforms API data into a component * @param {APIMessageComponent|Component} data The data to create the component from @@ -89,19 +156,64 @@ function createComponent(data) { return new MentionableSelectMenuComponent(data); case ComponentType.ChannelSelect: return new ChannelSelectMenuComponent(data); + case ComponentType.Container: + return new ContainerComponent(data); + case ComponentType.TextDisplay: + return new TextDisplayComponent(data); + case ComponentType.File: + return new FileComponent(data); + case ComponentType.MediaGallery: + return new MediaGalleryComponent(data); + case ComponentType.Section: + return new SectionComponent(data); + case ComponentType.Separator: + return new SeparatorComponent(data); + case ComponentType.Thumbnail: + return new ThumbnailComponent(data); default: return new Component(data); } } +/** + * Finds a component by customId in nested components + * @param {Array} components The components to search in + * @param {string} customId The customId to search for + * @returns {Component|APIMessageComponent} + */ +function findComponentByCustomId(components, customId) { + return ( + components + .flatMap(component => { + switch (component.type) { + case ComponentType.ActionRow: + return component.components; + case ComponentType.Section: + return [component.accessory]; + default: + return [component]; + } + }) + .find(component => (component.customId ?? component.custom_id) === customId) ?? null + ); +} + exports.createComponent = createComponent; +exports.findComponentByCustomId = findComponentByCustomId; const { ActionRow } = require('../structures/ActionRow.js'); const { ButtonComponent } = require('../structures/ButtonComponent.js'); const { ChannelSelectMenuComponent } = require('../structures/ChannelSelectMenuComponent.js'); const { Component } = require('../structures/Component.js'); +const { ContainerComponent } = require('../structures/ContainerComponent.js'); +const { FileComponent } = require('../structures/FileComponent.js'); +const { MediaGalleryComponent } = require('../structures/MediaGalleryComponent.js'); const { MentionableSelectMenuComponent } = require('../structures/MentionableSelectMenuComponent.js'); const { RoleSelectMenuComponent } = require('../structures/RoleSelectMenuComponent.js'); +const { SectionComponent } = require('../structures/SectionComponent.js'); +const { SeparatorComponent } = require('../structures/SeparatorComponent.js'); const { StringSelectMenuComponent } = require('../structures/StringSelectMenuComponent.js'); +const { TextDisplayComponent } = require('../structures/TextDisplayComponent.js'); const { TextInputComponent } = require('../structures/TextInputComponent.js'); +const { ThumbnailComponent } = require('../structures/ThumbnailComponent.js'); const { UserSelectMenuComponent } = require('../structures/UserSelectMenuComponent.js'); diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 5e209fcd9d4b..e29cb873e759 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -160,6 +160,18 @@ import { GatewayVoiceChannelEffectSendDispatchData, RESTAPIPoll, EntryPointCommandHandlerType, + APIComponentInContainer, + APIContainerComponent, + APIThumbnailComponent, + APISectionComponent, + APITextDisplayComponent, + APIUnfurledMediaItem, + APIMediaGalleryItem, + APIMediaGalleryComponent, + APISeparatorComponent, + SeparatorSpacingSize, + APIFileComponent, + APIMessageTopLevelComponent, } from 'discord-api-types/v10'; import { ChildProcess } from 'node:child_process'; import { Stream } from 'node:stream'; @@ -689,15 +701,37 @@ export class ButtonInteraction extends Mes export type AnyComponent = | APIMessageComponent | APIModalComponent - | APIActionRowComponent; + | APIActionRowComponent + | AnyComponentV2; export class Component { public readonly data: Readonly; + public get id(): RawComponentData['id']; public get type(): RawComponentData['type']; public toJSON(): RawComponentData; public equals(other: this | RawComponentData): boolean; } +export type AnyComponentV2 = APIComponentInContainer | APIContainerComponent | APIThumbnailComponent; + +export type TopLevelComponent = + | ActionRow + | ContainerComponent + | FileComponent + | MediaGalleryComponent + | SectionComponent + | SeparatorComponent + | TextDisplayComponent; + +export type TopLevelComponentData = + | ActionRowData + | ContainerComponentData + | FileComponentData + | MediaGalleryComponentData + | SectionComponentData + | SeparatorComponentData + | TextDisplayComponentData; + export class ButtonComponent extends Component { private constructor(data: APIButtonComponent); public get style(): ButtonStyle; @@ -978,6 +1012,40 @@ export class ClientVoiceManager { public adapters: Map; } +export type ComponentInContainer = + | ActionRow + | FileComponent + | MediaGalleryComponent + | SectionComponent + | SeparatorComponent + | TextDisplayComponent; + +export type ComponentInContainerData = + | ActionRowData + | FileComponentData + | MediaGalleryComponentData + | SectionComponentData + | SeparatorComponentData + | TextDisplayComponentData; + +export interface ContainerComponentData< + ComponentType extends JSONEncodable | ComponentInContainerData = + | JSONEncodable + | ComponentInContainerData, +> extends BaseComponentData { + components: readonly ComponentType[]; + accentColor?: number; + spoiler?: boolean; +} + +export class ContainerComponent extends Component { + private constructor(data: APIContainerComponent); + public get accentColor(): number; + public get hexAccentColor(): HexColorString; + public get spoiler(): boolean; + public readonly components: ComponentInContainer[]; +} + export { Collection, ReadonlyCollection } from '@discordjs/collection'; export interface CollectorEventTypes { @@ -1275,6 +1343,16 @@ export class Entitlement extends Base { public isGuildSubscription(): this is this & { guildId: Snowflake; guild: Guild }; } +export interface FileComponentData extends BaseComponentData { + file: UnfurledMediaItemData; + spoiler?: boolean; +} +export class FileComponent extends Component { + private constructor(data: APIFileComponent); + public readonly file: UnfurledMediaItem; + public get spoiler(): boolean; +} + export class Guild extends AnonymousGuild { private constructor(client: Client, data: RawGuildData); private _sortedRoles(): Collection; @@ -1964,6 +2042,27 @@ export class LimitedCollection extends Collection { public keepOverLimit: ((value: Value, key: Key, collection: this) => boolean) | null; } +export interface MediaGalleryComponentData extends BaseComponentData { + items: readonly MediaGalleryItemData[]; +} +export class MediaGalleryComponent extends Component { + private constructor(data: APIMediaGalleryComponent); + public readonly items: MediaGalleryItem[]; +} + +export interface MediaGalleryItemData { + media: UnfurledMediaItemData; + description?: string; + spoiler?: boolean; +} +export class MediaGalleryItem { + private constructor(data: APIMediaGalleryItem); + public readonly data: APIMediaGalleryItem; + public readonly media: UnfurledMediaItem; + public get description(): string | null; + public get spoiler(): boolean; +} + export interface MessageCall { get endedAt(): Date | null; endedTimestamp: number | null; @@ -2036,7 +2135,7 @@ export class Message extends Base { public get channel(): If; public channelId: Snowflake; public get cleanContent(): string; - public components: ActionRow[]; + public components: TopLevelComponent[]; public content: string; public get createdAt(): Date; public createdTimestamp: number; @@ -2771,6 +2870,20 @@ export class RoleFlagsBitField extends BitField { public static resolve(bit?: BitFieldResolvable): number; } +export interface SectionComponentData extends BaseComponentData { + accessory: ButtonComponentData | ThumbnailComponentData; + components: readonly TextDisplayComponentData[]; +} + +export class SectionComponent< + AccessoryType extends ButtonComponent | ThumbnailComponent = ButtonComponent | ThumbnailComponent, +> extends Component { + private constructor(data: APISectionComponent); + public readonly accessory: AccessoryType; + public readonly components: TextDisplayComponent[]; + public toJSON(): APISectionComponent; +} + export class StringSelectMenuInteraction< Cached extends CacheType = CacheType, > extends MessageComponentInteraction { @@ -2886,6 +2999,16 @@ export type SelectMenuInteraction = export type SelectMenuType = APISelectMenuComponent['type']; +export interface SeparatorComponentData extends BaseComponentData { + spacing?: SeparatorSpacingSize; + dividier?: boolean; +} +export class SeparatorComponent extends Component { + private constructor(data: APISeparatorComponent); + public get spacing(): SeparatorSpacingSize; + public get divider(): boolean; +} + export interface ShardEventTypes { death: [process: ChildProcess | Worker]; disconnect: []; @@ -3232,6 +3355,15 @@ export class TextChannel extends BaseGuildTextChannel { public type: ChannelType.GuildText; } +export interface TextDisplayComponentData extends BaseComponentData { + content: string; +} + +export class TextDisplayComponent extends Component { + private constructor(data: APITextDisplayComponent); + public readonly content: string; +} + export type ForumThreadChannel = PublicThreadChannel; export type TextThreadChannel = PublicThreadChannel | PrivateThreadChannel; export type AnyThreadChannel = TextThreadChannel | ForumThreadChannel; @@ -3327,6 +3459,19 @@ export class ThreadMemberFlagsBitField extends BitField public static resolve(bit?: BitFieldResolvable): number; } +export interface ThumbnailComponentData extends BaseComponentData { + media: UnfurledMediaItemData; + description?: string; + spoiler?: boolean; +} + +export class ThumbnailComponent extends Component { + private constructor(data: APIThumbnailComponent); + public readonly media: UnfurledMediaItem; + public get description(): string | null; + public get spoiler(): boolean; +} + export class Typing extends Base { private constructor(channel: TextBasedChannel, user: PartialUser, data?: RawTypingData); public channel: TextBasedChannel; @@ -3346,6 +3491,16 @@ export interface AvatarDecorationData { skuId: Snowflake; } +export interface UnfurledMediaItemData { + url: string; +} + +export class UnfurledMediaItem { + private constructor(data: APIUnfurledMediaItem); + public readonly data: APIUnfurledMediaItem; + public get url(): string; +} + // tslint:disable-next-line no-empty-interface export interface User extends PartialTextBasedChannelFields {} export class User extends Base { @@ -3478,7 +3633,9 @@ export function createChannel( export type ComponentData = | MessageActionRowComponentData | ModalActionRowComponentData - | ActionRowData; + | ComponentInContainerData + | ContainerComponentData + | ThumbnailComponentData; export class VoiceChannel extends BaseGuildVoiceChannel { public get speakable(): boolean; @@ -6111,8 +6268,11 @@ export interface InteractionReplyOptions extends BaseMessageOptionsWithPoll { withResponse?: boolean; flags?: | BitFieldResolvable< - Extract, - MessageFlags.Ephemeral | MessageFlags.SuppressEmbeds | MessageFlags.SuppressNotifications + Extract, + | MessageFlags.Ephemeral + | MessageFlags.SuppressEmbeds + | MessageFlags.SuppressNotifications + | MessageFlags.IsComponentsV2 > | undefined; } @@ -6273,9 +6433,10 @@ export interface BaseMessageOptions { | AttachmentPayload )[]; components?: readonly ( - | JSONEncodable> + | JSONEncodable + | TopLevelComponentData | ActionRowData - | APIActionRowComponent + | APIMessageTopLevelComponent )[]; } @@ -6290,8 +6451,8 @@ export interface BaseMessageCreateOptions extends BaseMessageOptionsWithPoll { stickers?: readonly StickerResolvable[]; flags?: | BitFieldResolvable< - Extract, - MessageFlags.SuppressEmbeds | MessageFlags.SuppressNotifications + Extract, + MessageFlags.SuppressEmbeds | MessageFlags.SuppressNotifications | MessageFlags.IsComponentsV2 > | undefined; } diff --git a/packages/discord.js/typings/index.test-d.ts b/packages/discord.js/typings/index.test-d.ts index 4e41a80fd7ee..9484c1b5676c 100644 --- a/packages/discord.js/typings/index.test-d.ts +++ b/packages/discord.js/typings/index.test-d.ts @@ -29,6 +29,7 @@ import { GuildScheduledEventRecurrenceRuleMonth, GuildScheduledEventRecurrenceRuleWeekday, APIButtonComponentWithCustomId, + MessageFlags, } from 'discord-api-types/v10'; import { ApplicationCommand, @@ -214,6 +215,15 @@ import { PrimaryButtonBuilder, resolveColor, createComponentBuilder, + SectionComponentData, + TextDisplayComponentData, + ThumbnailComponentData, + UnfurledMediaItemData, + MediaGalleryComponentData, + MediaGalleryItemData, + SeparatorComponentData, + FileComponentData, + ContainerComponentData, } from './index.js'; import { expectAssignable, expectNotAssignable, expectNotType, expectType } from 'tsd'; import type { ContextMenuCommandBuilder, ChatInputCommandBuilder } from '@discordjs/builders'; @@ -654,6 +664,57 @@ client.on('messageCreate', async message => { components: [row, rawButtonsRow, buttonsRow, rawStringSelectMenuRow, stringSelectRow], embeds: [embed, embedData], }); + + const rawTextDisplay: TextDisplayComponentData = { + type: ComponentType.TextDisplay, + content: 'test', + }; + + const rawMedia: UnfurledMediaItemData = { url: 'https://discord.js.org' }; + + const rawThumbnail: ThumbnailComponentData = { + type: ComponentType.Thumbnail, + media: rawMedia, + spoiler: true, + description: 'test', + }; + + const rawSection: SectionComponentData = { + type: ComponentType.Section, + components: [rawTextDisplay], + accessory: rawThumbnail, + }; + + const rawMediaGalleryItem: MediaGalleryItemData = { + media: rawMedia, + description: 'test', + spoiler: false, + }; + + const rawMediaGallery: MediaGalleryComponentData = { + type: ComponentType.MediaGallery, + items: [rawMediaGalleryItem, rawMediaGalleryItem, rawMediaGalleryItem], + }; + + const rawSeparator: SeparatorComponentData = { + type: ComponentType.Separator, + spacing: 1, + dividier: false, + }; + + const rawFile: FileComponentData = { + type: ComponentType.File, + file: rawMedia, + }; + + const rawContainer: ContainerComponentData = { + type: ComponentType.Container, + components: [rawSection, rawSeparator, rawMediaGallery, rawFile], + accentColor: 0xff00ff, + spoiler: true, + }; + + channel.send({ flags: MessageFlags.IsComponentsV2, components: [rawContainer] }); }); client.on('messageDelete', ({ client }) => expectType>(client)); From f2671ac70d0fb0f630be15b8a59b4ec5c9576441 Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Fri, 25 Apr 2025 21:59:58 +0200 Subject: [PATCH 02/12] fix: tests --- packages/discord.js/typings/index.d.ts | 1 + packages/discord.js/typings/index.test-d.ts | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index e29cb873e759..518552ae5db7 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -6433,6 +6433,7 @@ export interface BaseMessageOptions { | AttachmentPayload )[]; components?: readonly ( + | JSONEncodable> | JSONEncodable | TopLevelComponentData | ActionRowData diff --git a/packages/discord.js/typings/index.test-d.ts b/packages/discord.js/typings/index.test-d.ts index 9484c1b5676c..7bd5402e7c8b 100644 --- a/packages/discord.js/typings/index.test-d.ts +++ b/packages/discord.js/typings/index.test-d.ts @@ -30,6 +30,7 @@ import { GuildScheduledEventRecurrenceRuleWeekday, APIButtonComponentWithCustomId, MessageFlags, + APITextInputComponent, } from 'discord-api-types/v10'; import { ApplicationCommand, @@ -363,8 +364,6 @@ client.on('interactionCreate', async interaction => { // @ts-expect-error interaction.reply({ content: 'Hi!', components: [[button]] }); - void new ActionRowBuilder({}); - // @ts-expect-error await interaction.reply({ content: 'Hi!', components: [button] }); @@ -2591,8 +2590,11 @@ new PrimaryButtonBuilder(buttonData); declare const buttonComp: ButtonComponent; createComponentBuilder(buttonComp.toJSON()); +declare const textInputData: APITextInputComponent; +new TextInputBuilder(textInputData); + declare const textInputComp: TextInputComponent; -new TextInputBuilder(textInputComp); +new TextInputBuilder(textInputComp.toJSON()); declare const embedData: APIEmbed; new EmbedBuilder(embedData); From b1db31779278f7eb07cfb3bdebedde3e6b9df95c Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Fri, 25 Apr 2025 22:11:44 +0200 Subject: [PATCH 03/12] fix: merge --- packages/discord.js/typings/index.d.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 231503d77664..bc3ec27eff67 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -3645,11 +3645,6 @@ export interface SendSoundboardSoundOptions { guildId?: Snowflake; } -export interface SendSoundboardSoundOptions { - soundId: Snowflake; - guildId?: Snowflake; -} - export class VoiceChannel extends BaseGuildVoiceChannel { public get speakable(): boolean; public type: ChannelType.GuildVoice; From ac640d6cb2f3276d53abbeef85149e01be640c7b Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Fri, 25 Apr 2025 22:25:33 +0200 Subject: [PATCH 04/12] fix: lint --- .../src/structures/ContainerComponent.js | 86 +++++++++---------- .../src/structures/FileComponent.js | 48 +++++------ .../src/structures/MediaGalleryComponent.js | 30 +++---- .../src/structures/MediaGalleryItem.js | 66 +++++++------- packages/discord.js/src/structures/Message.js | 23 +++-- .../structures/MessageComponentInteraction.js | 22 ++--- .../src/structures/SectionComponent.js | 50 +++++------ .../src/structures/SeparatorComponent.js | 32 +++---- .../src/structures/TextDisplayComponent.js | 16 ++-- .../src/structures/ThumbnailComponent.js | 60 ++++++------- .../src/structures/UnfurledMediaItem.js | 28 +++--- packages/discord.js/src/util/Components.js | 20 ++--- 12 files changed, 240 insertions(+), 241 deletions(-) diff --git a/packages/discord.js/src/structures/ContainerComponent.js b/packages/discord.js/src/structures/ContainerComponent.js index 3eb412e96c18..11eddddefb39 100644 --- a/packages/discord.js/src/structures/ContainerComponent.js +++ b/packages/discord.js/src/structures/ContainerComponent.js @@ -8,53 +8,53 @@ const { createComponent } = require('../util/Components.js'); * @extends {Component} */ class ContainerComponent extends Component { - constructor({ components, ...data }) { - super(data); - - /** - * The components in this container - * @type {Component[]} - * @readonly - */ - this.components = components.map(component => createComponent(component)); - } - - /** - * The accent color of this container - * @type {?number} - * @readonly - */ - get accentColor() { - return this.data.accent_color ?? null; - } - - /** - * The hex accent color of this container - * @type {?string} - * @readonly - */ - get hexAccentColor() { - return typeof this.data.accent_color === 'number' - ? `#${this.data.accent_color.toString(16).padStart(6, '0')}` - : (this.data.accent_color ?? null); - } + constructor({ components, ...data }) { + super(data); /** - * Whether this container is spoilered - * @type {boolean} + * The components in this container + * @type {Component[]} * @readonly */ - get spoiler() { - return this.data.spoiler ?? false; - } - - /** - * Returns the API-compatible JSON for this component - * @returns {APIContainerComponent} - */ - toJSON() { - return { ...this.data, components: this.components.map(component => component.toJSON()) }; - } + this.components = components.map(component => createComponent(component)); + } + + /** + * The accent color of this container + * @type {?number} + * @readonly + */ + get accentColor() { + return this.data.accent_color ?? null; + } + + /** + * The hex accent color of this container + * @type {?string} + * @readonly + */ + get hexAccentColor() { + return typeof this.data.accent_color === 'number' + ? `#${this.data.accent_color.toString(16).padStart(6, '0')}` + : (this.data.accent_color ?? null); + } + + /** + * Whether this container is spoilered + * @type {boolean} + * @readonly + */ + get spoiler() { + return this.data.spoiler ?? false; + } + + /** + * Returns the API-compatible JSON for this component + * @returns {APIContainerComponent} + */ + toJSON() { + return { ...this.data, components: this.components.map(component => component.toJSON()) }; + } } exports.ContainerComponent = ContainerComponent; diff --git a/packages/discord.js/src/structures/FileComponent.js b/packages/discord.js/src/structures/FileComponent.js index e63750281da4..63b7b0275d8a 100644 --- a/packages/discord.js/src/structures/FileComponent.js +++ b/packages/discord.js/src/structures/FileComponent.js @@ -1,40 +1,40 @@ 'use strict'; -const Component = require('./Component.js'); -const UnfurledMediaItem = require('./UnfurledMediaItem.js'); +const { Component } = require('./Component.js'); +const { UnfurledMediaItem } = require('./UnfurledMediaItem.js'); /** * Represents a file component * @extends {Component} */ class FileComponent extends Component { - constructor({ file, ...data }) { - super(data); - - /** - * The media associated with this file - * @type {UnfurledMediaItem} - * @readonly - */ - this.file = new UnfurledMediaItem(file); - } + constructor({ file, ...data }) { + super(data); /** - * Whether this thumbnail is spoilered - * @type {boolean} + * The media associated with this file + * @type {UnfurledMediaItem} * @readonly */ - get spoiler() { - return this.data.spoiler ?? false; - } + this.file = new UnfurledMediaItem(file); + } - /** - * Returns the API-compatible JSON for this component - * @returns {APIFileComponent} - */ - toJSON() { - return { ...this.data, file: this.file.toJSON() }; - } + /** + * Whether this thumbnail is spoilered + * @type {boolean} + * @readonly + */ + get spoiler() { + return this.data.spoiler ?? false; + } + + /** + * Returns the API-compatible JSON for this component + * @returns {APIFileComponent} + */ + toJSON() { + return { ...this.data, file: this.file.toJSON() }; + } } exports.FileComponent = FileComponent; diff --git a/packages/discord.js/src/structures/MediaGalleryComponent.js b/packages/discord.js/src/structures/MediaGalleryComponent.js index 3531e3c163d9..6b797b3ef1e3 100644 --- a/packages/discord.js/src/structures/MediaGalleryComponent.js +++ b/packages/discord.js/src/structures/MediaGalleryComponent.js @@ -8,24 +8,24 @@ const { MediaGalleryItem } = require('./MediaGalleryItem.js'); * @extends {Component} */ class MediaGalleryComponent extends Component { - constructor({ items, ...data }) { - super(data); - - /** - * The items in this media gallery - * @type {MediaGalleryItem[]} - * @readonly - */ - this.items = items.map(item => new MediaGalleryItem(item)); - } + constructor({ items, ...data }) { + super(data); /** - * Returns the API-compatible JSON for this component - * @returns {APIMediaGalleryComponent} + * The items in this media gallery + * @type {MediaGalleryItem[]} + * @readonly */ - toJSON() { - return { ...this.data, items: this.items.map(item => item.toJSON()) }; - } + this.items = items.map(item => new MediaGalleryItem(item)); + } + + /** + * Returns the API-compatible JSON for this component + * @returns {APIMediaGalleryComponent} + */ + toJSON() { + return { ...this.data, items: this.items.map(item => item.toJSON()) }; + } } exports.MediaGalleryComponent = MediaGalleryComponent; diff --git a/packages/discord.js/src/structures/MediaGalleryItem.js b/packages/discord.js/src/structures/MediaGalleryItem.js index 7b608dcdace6..7b3acc81f626 100644 --- a/packages/discord.js/src/structures/MediaGalleryItem.js +++ b/packages/discord.js/src/structures/MediaGalleryItem.js @@ -6,46 +6,46 @@ const { UnfurledMediaItem } = require('./UnfurledMediaItem.js'); * Represents an item in a media gallery */ class MediaGalleryItem { - constructor({ media, ...data }) { - /** - * The API data associated with this component - * @type {APIMediaGalleryItem} - */ - this.data = data; - - /** - * The media associated with this media gallery item - * @type {UnfurledMediaItem} - * @readonly - */ - this.media = new UnfurledMediaItem(media); - } - + constructor({ media, ...data }) { /** - * The description of this media gallery item - * @type {?string} - * @readonly + * The API data associated with this component + * @type {APIMediaGalleryItem} */ - get description() { - return this.data.description ?? null; - } + this.data = data; /** - * Whether this media gallery item is spoilered - * @type {boolean} + * The media associated with this media gallery item + * @type {UnfurledMediaItem} * @readonly */ - get spoiler() { - return this.data.spoiler ?? false; - } + this.media = new UnfurledMediaItem(media); + } - /** - * Returns the API-compatible JSON for this component - * @returns {APIMediaGalleryItem} - */ - toJSON() { - return { ...this.data, media: this.media.toJSON() }; - } + /** + * The description of this media gallery item + * @type {?string} + * @readonly + */ + get description() { + return this.data.description ?? null; + } + + /** + * Whether this media gallery item is spoilered + * @type {boolean} + * @readonly + */ + get spoiler() { + return this.data.spoiler ?? false; + } + + /** + * Returns the API-compatible JSON for this component + * @returns {APIMediaGalleryItem} + */ + toJSON() { + return { ...this.data, media: this.media.toJSON() }; + } } exports.MediaGalleryItem = MediaGalleryItem; diff --git a/packages/discord.js/src/structures/Message.js b/packages/discord.js/src/structures/Message.js index d5438f1c3668..f257842da3cf 100644 --- a/packages/discord.js/src/structures/Message.js +++ b/packages/discord.js/src/structures/Message.js @@ -14,7 +14,6 @@ const { const { Attachment } = require('./Attachment.js'); const { Base } = require('./Base.js'); const { ClientApplication } = require('./ClientApplication.js'); -const { Component } = require('./Component.js'); const { Embed } = require('./Embed.js'); const { InteractionCollector } = require('./InteractionCollector.js'); const { MessageMentions } = require('./MessageMentions.js'); @@ -684,8 +683,8 @@ class Message extends Base { get editable() { const precheck = Boolean( this.author.id === this.client.user.id && - (!this.guild || this.channel?.viewable) && - this.reference?.type !== MessageReferenceType.Forward, + (!this.guild || this.channel?.viewable) && + this.reference?.type !== MessageReferenceType.Forward, ); // Regardless of permissions thread messages cannot be edited if @@ -756,9 +755,9 @@ class Message extends Base { const { channel } = this; return Boolean( !this.system && - (!this.guild || - (channel?.viewable && - channel?.permissionsFor(this.client.user)?.has(PermissionFlagsBits.ManageMessages, false))), + (!this.guild || + (channel?.viewable && + channel?.permissionsFor(this.client.user)?.has(PermissionFlagsBits.ManageMessages, false))), ); } @@ -788,12 +787,12 @@ class Message extends Base { const { channel } = this; return Boolean( channel?.type === ChannelType.GuildAnnouncement && - !this.flags.has(MessageFlags.Crossposted) && - this.reference?.type !== MessageReferenceType.Forward && - this.type === MessageType.Default && - !this.poll && - channel.viewable && - channel.permissionsFor(this.client.user)?.has(bitfield, false), + !this.flags.has(MessageFlags.Crossposted) && + this.reference?.type !== MessageReferenceType.Forward && + this.type === MessageType.Default && + !this.poll && + channel.viewable && + channel.permissionsFor(this.client.user)?.has(bitfield, false), ); } diff --git a/packages/discord.js/src/structures/MessageComponentInteraction.js b/packages/discord.js/src/structures/MessageComponentInteraction.js index 2089547e4778..edc25bf74eba 100644 --- a/packages/discord.js/src/structures/MessageComponentInteraction.js +++ b/packages/discord.js/src/structures/MessageComponentInteraction.js @@ -89,17 +89,17 @@ class MessageComponentInteraction extends BaseInteraction { // These are here only for documentation purposes - they are implemented by InteractionResponses /* eslint-disable no-empty-function */ - deferReply() { } - reply() { } - fetchReply() { } - editReply() { } - deleteReply() { } - followUp() { } - deferUpdate() { } - update() { } - launchActivity() { } - showModal() { } - awaitModalSubmit() { } + deferReply() {} + reply() {} + fetchReply() {} + editReply() {} + deleteReply() {} + followUp() {} + deferUpdate() {} + update() {} + launchActivity() {} + showModal() {} + awaitModalSubmit() {} } InteractionResponses.applyToClass(MessageComponentInteraction); diff --git a/packages/discord.js/src/structures/SectionComponent.js b/packages/discord.js/src/structures/SectionComponent.js index 6161332c4dd5..2ab625f4e3e8 100644 --- a/packages/discord.js/src/structures/SectionComponent.js +++ b/packages/discord.js/src/structures/SectionComponent.js @@ -8,35 +8,35 @@ const { createComponent } = require('../util/Components.js'); * @extends {Component} */ class SectionComponent extends Component { - constructor({ accessory, components, ...data }) { - super(data); + constructor({ accessory, components, ...data }) { + super(data); - /** - * The components in this section - * @type {Component[]} - * @readonly - */ - this.components = components.map(component => createComponent(component)); - - /** - * The accessory component of this section - * @type {Component} - * @readonly - */ - this.accessory = createComponent(accessory); - } + /** + * The components in this section + * @type {Component[]} + * @readonly + */ + this.components = components.map(component => createComponent(component)); /** - * Returns the API-compatible JSON for this component - * @returns {APISectionComponent} + * The accessory component of this section + * @type {Component} + * @readonly */ - toJSON() { - return { - ...this.data, - accessory: this.accessory.toJSON(), - components: this.components.map(component => component.toJSON()), - }; - } + this.accessory = createComponent(accessory); + } + + /** + * Returns the API-compatible JSON for this component + * @returns {APISectionComponent} + */ + toJSON() { + return { + ...this.data, + accessory: this.accessory.toJSON(), + components: this.components.map(component => component.toJSON()), + }; + } } exports.SectionComponent = SectionComponent; diff --git a/packages/discord.js/src/structures/SeparatorComponent.js b/packages/discord.js/src/structures/SeparatorComponent.js index d4861cd0623f..3df1934bb1c6 100644 --- a/packages/discord.js/src/structures/SeparatorComponent.js +++ b/packages/discord.js/src/structures/SeparatorComponent.js @@ -8,23 +8,23 @@ const { Component } = require('./Component.js'); * @extends {Component} */ class SeparatorComponent extends Component { - /** - * The spacing of this separator - * @type {SeparatorSpacingSize} - * @readonly - */ - get spacing() { - return this.data.spacing ?? SeparatorSpacingSize.Small; - } + /** + * The spacing of this separator + * @type {SeparatorSpacingSize} + * @readonly + */ + get spacing() { + return this.data.spacing ?? SeparatorSpacingSize.Small; + } - /** - * Whether this separator is a divider - * @type {boolean} - * @readonly - */ - get divider() { - return this.data.divider ?? true; - } + /** + * Whether this separator is a divider + * @type {boolean} + * @readonly + */ + get divider() { + return this.data.divider ?? true; + } } exports.SeparatorComponent = SeparatorComponent; diff --git a/packages/discord.js/src/structures/TextDisplayComponent.js b/packages/discord.js/src/structures/TextDisplayComponent.js index b31624f27326..5b8fd04badb3 100644 --- a/packages/discord.js/src/structures/TextDisplayComponent.js +++ b/packages/discord.js/src/structures/TextDisplayComponent.js @@ -7,14 +7,14 @@ const { Component } = require('./Component.js'); * @extends {Component} */ class TextDisplayComponent extends Component { - /** - * The content of this text display - * @type {string} - * @readonly - */ - get content() { - return this.data.content; - } + /** + * The content of this text display + * @type {string} + * @readonly + */ + get content() { + return this.data.content; + } } exports.TextDisplayComponent = TextDisplayComponent; diff --git a/packages/discord.js/src/structures/ThumbnailComponent.js b/packages/discord.js/src/structures/ThumbnailComponent.js index 9a8c70369908..7c85e465b69a 100644 --- a/packages/discord.js/src/structures/ThumbnailComponent.js +++ b/packages/discord.js/src/structures/ThumbnailComponent.js @@ -8,42 +8,42 @@ const { UnfurledMediaItem } = require('./UnfurledMediaItem.js'); * @extends {Component} */ class ThumbnailComponent extends Component { - constructor({ media, ...data }) { - super(data); - - /** - * The media associated with this thumbnail - * @type {UnfurledMediaItem} - * @readonly - */ - this.media = new UnfurledMediaItem(media); - } + constructor({ media, ...data }) { + super(data); /** - * The description of this thumbnail - * @type {?string} + * The media associated with this thumbnail + * @type {UnfurledMediaItem} * @readonly */ - get description() { - return this.data.description ?? null; - } + this.media = new UnfurledMediaItem(media); + } - /** - * Whether this thumbnail is spoilered - * @type {boolean} - * @readonly - */ - get spoiler() { - return this.data.spoiler ?? false; - } + /** + * The description of this thumbnail + * @type {?string} + * @readonly + */ + get description() { + return this.data.description ?? null; + } - /** - * Returns the API-compatible JSON for this component - * @returns {APIThumbnailComponent} - */ - toJSON() { - return { ...this.data, media: this.media.toJSON() }; - } + /** + * Whether this thumbnail is spoilered + * @type {boolean} + * @readonly + */ + get spoiler() { + return this.data.spoiler ?? false; + } + + /** + * Returns the API-compatible JSON for this component + * @returns {APIThumbnailComponent} + */ + toJSON() { + return { ...this.data, media: this.media.toJSON() }; + } } exports.ThumbnailComponent = ThumbnailComponent; diff --git a/packages/discord.js/src/structures/UnfurledMediaItem.js b/packages/discord.js/src/structures/UnfurledMediaItem.js index 79e0dcf3bc10..156096aca51a 100644 --- a/packages/discord.js/src/structures/UnfurledMediaItem.js +++ b/packages/discord.js/src/structures/UnfurledMediaItem.js @@ -4,22 +4,22 @@ * Represents a media item in a component */ class UnfurledMediaItem { - constructor(data) { - /** - * The API data associated with this media item - * @type {APIUnfurledMediaItem} - */ - this.data = data; - } - + constructor(data) { /** - * The URL of this media gallery item - * @type {string} - * @readonly + * The API data associated with this media item + * @type {APIUnfurledMediaItem} */ - get url() { - return this.data.url; - } + this.data = data; + } + + /** + * The URL of this media gallery item + * @type {string} + * @readonly + */ + get url() { + return this.data.url; + } } exports.UnfurledMediaItem = UnfurledMediaItem; diff --git a/packages/discord.js/src/util/Components.js b/packages/discord.js/src/util/Components.js index 9d010d18df86..4d3512f85af6 100644 --- a/packages/discord.js/src/util/Components.js +++ b/packages/discord.js/src/util/Components.js @@ -108,25 +108,25 @@ const { ComponentType } = require('discord-api-types/v10'); /** * @typedef {ActionRowData|FileComponentData|MediaGalleryComponentData|SectionComponentData| * SeparatorComponentData|TextDisplayComponentData} ComponentInContainerData -*/ + */ /** -* @typedef {BaseComponentData} ContainerComponentData -* @property {ComponentInContainerData} components The components in this container -* @property {?number} [accentColor] The accent color of this container -* @property {boolean} [spoiler] Whether the container should be spoilered -*/ + * @typedef {BaseComponentData} ContainerComponentData + * @property {ComponentInContainerData} components The components in this container + * @property {?number} [accentColor] The accent color of this container + * @property {boolean} [spoiler] Whether the container should be spoilered + */ /** -* @typedef {ActionRowData|ButtonComponentData|SelectMenuComponentData|TextInputComponentData| -* ThumbnailComponentData|FileComponentData|MediaGalleryComponentData|SeparatorComponentData| -* SectionComponentData|TextDisplayComponentData|ContainerComponentData} ComponentData + * @typedef {ActionRowData|ButtonComponentData|SelectMenuComponentData|TextInputComponentData| + * ThumbnailComponentData|FileComponentData|MediaGalleryComponentData|SeparatorComponentData| + * SectionComponentData|TextDisplayComponentData|ContainerComponentData} ComponentData */ /** * @typedef {ActionRow|ContainerComponent|FileComponent|MediaGalleryComponent| * SectionComponent|SeparatorComponent|TextDisplayComponent} MessageTopLevelComponent -*/ + */ /** * Transforms API data into a component From c321b8f7aabd257c69c80032f46d03cffd2b342f Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Sat, 26 Apr 2025 02:36:35 +0300 Subject: [PATCH 05/12] Update packages/discord.js/src/util/Components.js --- packages/discord.js/src/util/Components.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/discord.js/src/util/Components.js b/packages/discord.js/src/util/Components.js index 4d3512f85af6..891e0497d13c 100644 --- a/packages/discord.js/src/util/Components.js +++ b/packages/discord.js/src/util/Components.js @@ -189,7 +189,7 @@ function findComponentByCustomId(components, customId) { case ComponentType.ActionRow: return component.components; case ComponentType.Section: - return [component.accessory]; + return [...component.components, component.accessory]; default: return [component]; } From 15290b0507d5c89551f5075d1cb85c901ac15b69 Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Sat, 26 Apr 2025 12:59:45 +0200 Subject: [PATCH 06/12] fix: forward-port fixes from v14 --- packages/discord.js/src/structures/MessagePayload.js | 3 ++- packages/discord.js/typings/index.d.ts | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/discord.js/src/structures/MessagePayload.js b/packages/discord.js/src/structures/MessagePayload.js index bf07b026b743..f4784a86f269 100644 --- a/packages/discord.js/src/structures/MessagePayload.js +++ b/packages/discord.js/src/structures/MessagePayload.js @@ -217,7 +217,8 @@ class MessagePayload { components, username, avatar_url: avatarURL, - allowed_mentions: content === undefined && message_reference === undefined ? undefined : allowedMentions, + allowed_mentions: + this.isMessage() && this.target.author.id !== this.target.client.user.id ? undefined : allowedMentions, flags, message_reference, attachments: this.options.attachments, diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index bc3ec27eff67..befe6479a5c8 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -6557,7 +6557,12 @@ export interface MessageEditAttachmentData { export interface MessageEditOptions extends Omit { content?: string | null; attachments?: readonly (Attachment | MessageEditAttachmentData)[]; - flags?: BitFieldResolvable, MessageFlags.SuppressEmbeds> | undefined; + flags?: + | BitFieldResolvable< + Extract, + MessageFlags.SuppressEmbeds | MessageFlags.IsComponentsV2 + > + | undefined; } export type MessageReactionResolvable = MessageReaction | Snowflake | string; From 2fb6e00f0d9b54676c28148f91233620129f18c1 Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Sat, 26 Apr 2025 17:39:31 +0200 Subject: [PATCH 07/12] fix: getter --- packages/discord.js/src/structures/MessagePayload.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/discord.js/src/structures/MessagePayload.js b/packages/discord.js/src/structures/MessagePayload.js index f4784a86f269..9c8fafe0e856 100644 --- a/packages/discord.js/src/structures/MessagePayload.js +++ b/packages/discord.js/src/structures/MessagePayload.js @@ -218,7 +218,7 @@ class MessagePayload { username, avatar_url: avatarURL, allowed_mentions: - this.isMessage() && this.target.author.id !== this.target.client.user.id ? undefined : allowedMentions, + this.isMessage && this.target.author.id !== this.target.client.user.id ? undefined : allowedMentions, flags, message_reference, attachments: this.options.attachments, From 8b916a7d6d4f527a1055159c30bcef14d5de85d4 Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Sat, 26 Apr 2025 19:25:06 +0200 Subject: [PATCH 08/12] fix: missing UnfurledMediaItem#toJSON() --- packages/discord.js/src/structures/UnfurledMediaItem.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/discord.js/src/structures/UnfurledMediaItem.js b/packages/discord.js/src/structures/UnfurledMediaItem.js index 156096aca51a..1f1409c5b5fd 100644 --- a/packages/discord.js/src/structures/UnfurledMediaItem.js +++ b/packages/discord.js/src/structures/UnfurledMediaItem.js @@ -20,6 +20,14 @@ class UnfurledMediaItem { get url() { return this.data.url; } + + /** + * Returns the API-compatible JSON for this media item + * @returns {APIUnfurledMediaItem} + */ + toJSON() { + return { ...this.data }; + } } exports.UnfurledMediaItem = UnfurledMediaItem; From 3ed27b334fee533d50407707a165b6f0bd10a4b2 Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Sun, 27 Apr 2025 18:57:23 +0200 Subject: [PATCH 09/12] fix: find interactive component in container --- packages/discord.js/src/util/Components.js | 31 +++++++++++++++------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/packages/discord.js/src/util/Components.js b/packages/discord.js/src/util/Components.js index 891e0497d13c..380bce5750bb 100644 --- a/packages/discord.js/src/util/Components.js +++ b/packages/discord.js/src/util/Components.js @@ -175,25 +175,36 @@ function createComponent(data) { } } +/** + * Extracts all interactive components from the component tree + * @param {Component|APIMessageComponent} component The component to find all interactive components in + * @returns {Array} + * @ignore + */ +function extractInteractiveComponents(component) { + switch (component.type) { + case ComponentType.ActionRow: + return component.components; + case ComponentType.Section: + return [...component.components, component.accessory]; + case ComponentType.Container: + return component.components.flatMap(extractInteractiveComponents); + default: + return [component]; + } +} + /** * Finds a component by customId in nested components * @param {Array} components The components to search in * @param {string} customId The customId to search for * @returns {Component|APIMessageComponent} + * @ignore */ function findComponentByCustomId(components, customId) { return ( components - .flatMap(component => { - switch (component.type) { - case ComponentType.ActionRow: - return component.components; - case ComponentType.Section: - return [...component.components, component.accessory]; - default: - return [component]; - } - }) + .flatMap(extractInteractiveComponents) .find(component => (component.customId ?? component.custom_id) === customId) ?? null ); } From 5cfa9b03b228be5ca1c1ed454251243bbe404105 Mon Sep 17 00:00:00 2001 From: Jiralite <33201955+Jiralite@users.noreply.github.com> Date: Tue, 29 Apr 2025 18:26:27 +0100 Subject: [PATCH 10/12] docs(APIMediaGalleryItem): Correct tag --- packages/discord.js/src/util/APITypes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/discord.js/src/util/APITypes.js b/packages/discord.js/src/util/APITypes.js index 60025d98a772..3772ebb1d1b8 100644 --- a/packages/discord.js/src/util/APITypes.js +++ b/packages/discord.js/src/util/APITypes.js @@ -152,7 +152,7 @@ /** * @external APIMediaGalleryItem - * @se {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIMediaGalleryItem} + * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIMediaGalleryItem} */ /** From bcd07c15ae92d440045826157452315b729474b6 Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Fri, 2 May 2025 21:32:47 +0200 Subject: [PATCH 11/12] fix: forward port --- packages/discord.js/.lintstagedrc.json | 2 +- .../src/structures/MessagePayload.js | 4 +- packages/discord.js/test/messages.js | 156 ++++++++++++++++++ packages/discord.js/typings/index.d.ts | 2 +- packages/discord.js/typings/index.test-d.ts | 2 +- 5 files changed, 162 insertions(+), 4 deletions(-) create mode 100644 packages/discord.js/test/messages.js diff --git a/packages/discord.js/.lintstagedrc.json b/packages/discord.js/.lintstagedrc.json index e4e385774d7f..88488d113874 100644 --- a/packages/discord.js/.lintstagedrc.json +++ b/packages/discord.js/.lintstagedrc.json @@ -1,5 +1,5 @@ { "$schema": "https://json.schemastore.org/lintstagedrc.schema.json", "*": "prettier --ignore-unknown --write", - "{src/**,test/**,typings/**,scripts/**}.{mjs,js,ts}": "cross-env ESLINT_USE_FLAT_CONFIG=false eslint --ext mjs,js,ts --fix" + "{src/**,test/**,typings/**,scripts/**}.{mjs,js,ts}": "cross-env ESLINT_USE_FLAT_CONFIG=false eslint --fix --format=pretty" } diff --git a/packages/discord.js/src/structures/MessagePayload.js b/packages/discord.js/src/structures/MessagePayload.js index 9c8fafe0e856..eb542ddcf29e 100644 --- a/packages/discord.js/src/structures/MessagePayload.js +++ b/packages/discord.js/src/structures/MessagePayload.js @@ -218,7 +218,9 @@ class MessagePayload { username, avatar_url: avatarURL, allowed_mentions: - this.isMessage && this.target.author.id !== this.target.client.user.id ? undefined : allowedMentions, + this.isMessage && message_reference === undefined && this.target.author.id !== this.target.client.user.id + ? undefined + : allowedMentions, flags, message_reference, attachments: this.options.attachments, diff --git a/packages/discord.js/test/messages.js b/packages/discord.js/test/messages.js new file mode 100644 index 000000000000..4b8b169f11ee --- /dev/null +++ b/packages/discord.js/test/messages.js @@ -0,0 +1,156 @@ +'use strict'; + +const { Buffer } = require('node:buffer'); +const { createReadStream } = require('node:fs'); +const { readFile } = require('node:fs/promises'); +const path = require('node:path'); +const process = require('node:process'); +const { setTimeout: sleep } = require('node:timers/promises'); +const { fetch } = require('undici'); +const { + Client, + GatewayIntentBits, + AttachmentBuilder, + EmbedBuilder, + MessageFlags, + ComponentType, +} = require('../src/index.js'); + +const client = new Client({ + intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent], +}); + +const buffer = l => + fetch(l) + .then(res => res.arrayBuffer()) + .then(Buffer.from); + +const linkA = 'https://discord.js.org/static/logo.svg'; +const fileA = path.join(__dirname, 'blobReach.png'); + +const embed = () => new EmbedBuilder(); +const attach = (attachment, name) => new AttachmentBuilder(attachment, { name }); + +const tests = [ + m => m.channel.send('x'), + + m => m.channel.send({ content: 'x', embeds: [{ description: 'a' }] }), + m => m.channel.send({ embeds: [{ description: 'a' }] }), + m => m.channel.send({ files: [{ attachment: fileA }] }), + m => + m.channel.send({ + embeds: [{ description: 'a' }], + files: [{ attachment: fileA, name: 'xyz.png' }], + }), + + m => m.channel.send({ content: 'x', embeds: [embed().setDescription('a')] }), + m => m.channel.send({ embeds: [embed().setDescription('a')] }), + m => m.channel.send({ embeds: [embed().setDescription('a'), embed().setDescription('b')] }), + + m => m.channel.send({ content: 'x', files: [attach(fileA)] }), + m => m.channel.send({ files: [fileA] }), + m => m.channel.send({ files: [attach(fileA)] }), + async m => m.channel.send({ files: [await buffer(linkA)] }), + async m => m.channel.send({ files: [{ attachment: await buffer(linkA) }] }), + m => m.channel.send({ files: [attach(fileA), attach(fileA)] }), + + m => m.channel.send({ embeds: [{ description: 'a' }] }).then(m2 => m2.edit('x')), + m => m.channel.send({ embeds: [embed().setDescription('a')] }).then(m2 => m2.edit('x')), + + m => m.channel.send('x').then(m2 => m2.edit({ embeds: [{ description: 'a' }] })), + m => m.channel.send('x').then(m2 => m2.edit({ embeds: [embed().setDescription('a')] })), + + m => m.channel.send({ embeds: [{ description: 'a' }] }).then(m2 => m2.edit({ content: 'x', embeds: [] })), + m => m.channel.send({ embeds: [embed().setDescription('a')] }).then(m2 => m2.edit({ content: 'x', embeds: [] })), + + m => m.channel.send({ content: 'x', embeds: [embed().setDescription('a')], files: [attach(fileA)] }), + m => m.channel.send({ content: 'x', files: [attach(fileA), attach(fileA)] }), + + m => m.channel.send({ embeds: [embed().setDescription('a')], files: [attach(fileA)] }), + m => + m.channel.send({ + embeds: [embed().setImage('attachment://two.png')], + files: [attach(fileA, 'two.png')], + }), + m => m.channel.send({ content: 'x', files: [attach(fileA)] }), + m => m.channel.send({ files: [fileA] }), + m => m.channel.send({ files: [attach(fileA)] }), + async m => m.channel.send({ files: [await readFile(fileA)] }), + + m => m.channel.send({ content: 'x', files: [attach(createReadStream(fileA))] }), + m => m.channel.send({ files: [createReadStream(fileA)] }), + m => m.channel.send({ files: [{ attachment: createReadStream(fileA) }] }), + + m => m.reply({ content: 'x', allowedMentions: { repliedUser: false } }), + m => m.reply({ content: 'x', allowedMentions: { repliedUser: true } }), + m => m.reply({ content: 'x' }), + m => m.reply({ content: `${m.author}`, allowedMentions: { repliedUser: false } }), + m => m.reply({ content: `${m.author}`, allowedMentions: { parse: ['users'], repliedUser: false } }), + m => m.reply({ content: `${m.author}`, allowedMentions: { parse: ['users'], repliedUser: true } }), + m => m.reply({ content: `${m.author}` }), + + m => m.edit({ flags: MessageFlags.SuppressEmbeds }), + m => m.edit({ flags: MessageFlags.SuppressEmbeds, allowedMentions: { repliedUser: false } }), + + m => + m + .reply({ content: 'x', allowedMentions: { repliedUser: false } }) + .then(msg => msg.edit({ content: 'a', allowedMentions: { repliedUser: true } })), + + m => + m.channel.send({ + components: [{ type: ComponentType.TextDisplay, content: `${m.author}` }], + flags: MessageFlags.IsComponentsV2, + }), + m => + m.channel.send({ + components: [{ type: ComponentType.TextDisplay, content: `${m.author}` }], + flags: MessageFlags.IsComponentsV2, + allowedMentions: { parse: ['users'] }, + }), + m => + m.channel.send({ + components: [{ type: ComponentType.TextDisplay, content: `${m.author}` }], + flags: MessageFlags.IsComponentsV2, + allowedMentions: { parse: [] }, + }), + m => + m.reply({ + components: [{ type: ComponentType.TextDisplay, content: `${m.author}` }], + flags: MessageFlags.IsComponentsV2, + allowedMentions: { parse: [], repliedUser: true }, + }), + m => + m.reply({ + components: [{ type: ComponentType.TextDisplay, content: `${m.author}` }], + flags: MessageFlags.IsComponentsV2, + allowedMentions: { parse: [], repliedUser: false }, + }), + + m => m.channel.send('Done!'), +]; + +client.on('messageCreate', async message => { + if (message.author.id !== process.env.OWNER) return; + const match = message.content.match(/^do (.+)$/); + if (match?.[1] === 'it') { + /* eslint-disable no-await-in-loop */ + for (const [i, test] of tests.entries()) { + await message.channel.send(`**#${i}**\n\`\`\`js\n${test.toString()}\`\`\``); + await test(message).catch(e => message.channel.send(`Error!\n\`\`\`\n${e}\`\`\``)); + await sleep(1_000); + } + /* eslint-enable no-await-in-loop */ + } else if (match) { + const n = parseInt(match[1]) || 0; + const test = tests.slice(n)[0]; + const i = tests.indexOf(test); + await message.channel.send(`**#${i}**\n\`\`\`js\n${test.toString()}\`\`\``); + await test(message).catch(e => message.channel.send(`Error!\n\`\`\`\n${e}\`\`\``)); + } +}); + +client.login(); + +// eslint-disable-next-line no-console +process.on('unhandledRejection', console.error); diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index befe6479a5c8..7bf1cac8bbd1 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -3004,7 +3004,7 @@ export type SelectMenuType = APISelectMenuComponent['type']; export interface SeparatorComponentData extends BaseComponentData { spacing?: SeparatorSpacingSize; - dividier?: boolean; + divider?: boolean; } export class SeparatorComponent extends Component { private constructor(data: APISeparatorComponent); diff --git a/packages/discord.js/typings/index.test-d.ts b/packages/discord.js/typings/index.test-d.ts index 7bd5402e7c8b..eacfcfdaa429 100644 --- a/packages/discord.js/typings/index.test-d.ts +++ b/packages/discord.js/typings/index.test-d.ts @@ -698,7 +698,7 @@ client.on('messageCreate', async message => { const rawSeparator: SeparatorComponentData = { type: ComponentType.Separator, spacing: 1, - dividier: false, + divider: false, }; const rawFile: FileComponentData = { From 3d9e6fb2ae995df2c164c3ee2910ea5fd3f516ce Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Sun, 11 May 2025 22:36:45 +0200 Subject: [PATCH 12/12] Apply suggestions from code review Co-authored-by: Danial Raza --- packages/discord.js/src/structures/UnfurledMediaItem.js | 2 +- packages/discord.js/typings/index.d.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/discord.js/src/structures/UnfurledMediaItem.js b/packages/discord.js/src/structures/UnfurledMediaItem.js index 1f1409c5b5fd..3e525e96640c 100644 --- a/packages/discord.js/src/structures/UnfurledMediaItem.js +++ b/packages/discord.js/src/structures/UnfurledMediaItem.js @@ -13,7 +13,7 @@ class UnfurledMediaItem { } /** - * The URL of this media gallery item + * The URL of this media item * @type {string} * @readonly */ diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 865bb9d35de1..0eb807c076dd 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -3364,7 +3364,7 @@ export interface TextDisplayComponentData extends BaseComponentData { export class TextDisplayComponent extends Component { private constructor(data: APITextDisplayComponent); - public readonly content: string; + public get content(): string; } export type ForumThreadChannel = PublicThreadChannel;