From edace17a131f857547163a3acf4bb6fec0c1e415 Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Fri, 25 Apr 2025 22:43:09 +0200 Subject: [PATCH] feat: components v2 in v14 (#10781) Co-authored-by: Naiyar <137700126+imnaiyar@users.noreply.github.com> Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com> Co-authored-by: Vlad Frangu Co-authored-by: Timo --- .../SlashCommands/SlashCommands.test.ts | 2 +- packages/discord.js/package.json | 4 +- 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 | 8 +- .../structures/MessageComponentInteraction.js | 7 +- .../src/structures/MessagePayload.js | 3 +- .../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 | 49 ++++- packages/discord.js/src/util/Components.js | 149 +++++++++++-- packages/discord.js/typings/index.d.ts | 208 ++++++++++++++++-- packages/discord.js/typings/index.test-d.ts | 67 +++++- packages/rest/package.json | 2 +- packages/rest/src/lib/utils/constants.ts | 8 +- pnpm-lock.yaml | 122 +++++----- 24 files changed, 883 insertions(+), 123 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/builders/__tests__/interactions/SlashCommands/SlashCommands.test.ts b/packages/builders/__tests__/interactions/SlashCommands/SlashCommands.test.ts index 64e9d97a7..8efc62d41 100644 --- a/packages/builders/__tests__/interactions/SlashCommands/SlashCommands.test.ts +++ b/packages/builders/__tests__/interactions/SlashCommands/SlashCommands.test.ts @@ -565,7 +565,7 @@ describe('Slash Commands', () => { }); describe('integration types', () => { - test('GIVEN a builder with valid integraton types THEN does not throw an error', () => { + test('GIVEN a builder with valid integration types THEN does not throw an error', () => { expect(() => getBuilder().setIntegrationTypes([ ApplicationIntegrationType.GuildInstall, diff --git a/packages/discord.js/package.json b/packages/discord.js/package.json index 9d3e2b617..136a3e432 100644 --- a/packages/discord.js/package.json +++ b/packages/discord.js/package.json @@ -65,14 +65,14 @@ "homepage": "https://discord.js.org", "funding": "https://github.com/discordjs/discord.js?sponsor", "dependencies": { - "@discordjs/builders": "^1.10.1", + "@discordjs/builders": "^1.11.0", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.0", "@discordjs/rest": "workspace:^", "@discordjs/util": "workspace:^", "@discordjs/ws": "^1.2.1", "@sapphire/snowflake": "3.5.3", - "discord-api-types": "^0.37.119", + "discord-api-types": "^0.38.1", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.10.0", diff --git a/packages/discord.js/src/index.js b/packages/discord.js/src/index.js index a2ba3f436..243f1af41 100644 --- a/packages/discord.js/src/index.js +++ b/packages/discord.js/src/index.js @@ -124,12 +124,14 @@ exports.CommandInteraction = require('./structures/CommandInteraction'); exports.Collector = require('./structures/interfaces/Collector'); exports.CommandInteractionOptionResolver = require('./structures/CommandInteractionOptionResolver'); exports.Component = require('./structures/Component'); +exports.ContainerComponent = require('./structures/ContainerComponent'); exports.ContextMenuCommandInteraction = require('./structures/ContextMenuCommandInteraction'); exports.DMChannel = require('./structures/DMChannel'); exports.Embed = require('./structures/Embed'); exports.EmbedBuilder = require('./structures/EmbedBuilder'); exports.Emoji = require('./structures/Emoji').Emoji; exports.Entitlement = require('./structures/Entitlement').Entitlement; +exports.FileComponent = require('./structures/FileComponent'); exports.ForumChannel = require('./structures/ForumChannel'); exports.Guild = require('./structures/Guild').Guild; exports.GuildAuditLogs = require('./structures/GuildAuditLogs'); @@ -162,6 +164,8 @@ exports.Attachment = require('./structures/Attachment'); exports.AttachmentBuilder = require('./structures/AttachmentBuilder'); exports.ModalBuilder = require('./structures/ModalBuilder'); exports.MediaChannel = require('./structures/MediaChannel'); +exports.MediaGalleryComponent = require('./structures/MediaGalleryComponent'); +exports.MediaGalleryItem = require('./structures/MediaGalleryItem'); exports.MessageCollector = require('./structures/MessageCollector'); exports.MessageComponentInteraction = require('./structures/MessageComponentInteraction'); exports.MessageContextMenuCommandInteraction = require('./structures/MessageContextMenuCommandInteraction'); @@ -181,6 +185,7 @@ exports.ReactionCollector = require('./structures/ReactionCollector'); exports.ReactionEmoji = require('./structures/ReactionEmoji'); exports.RichPresenceAssets = require('./structures/Presence').RichPresenceAssets; exports.Role = require('./structures/Role').Role; +exports.SectionComponent = require('./structures/SectionComponent'); exports.SelectMenuBuilder = require('./structures/SelectMenuBuilder'); exports.ChannelSelectMenuBuilder = require('./structures/ChannelSelectMenuBuilder'); exports.MentionableSelectMenuBuilder = require('./structures/MentionableSelectMenuBuilder'); @@ -202,6 +207,7 @@ exports.RoleSelectMenuInteraction = require('./structures/RoleSelectMenuInteract exports.StringSelectMenuInteraction = require('./structures/StringSelectMenuInteraction'); exports.UserSelectMenuInteraction = require('./structures/UserSelectMenuInteraction'); exports.SelectMenuOptionBuilder = require('./structures/SelectMenuOptionBuilder'); +exports.SeparatorComponent = require('./structures/SeparatorComponent'); exports.SKU = require('./structures/SKU').SKU; exports.SoundboardSound = require('./structures/SoundboardSound.js').SoundboardSound; exports.StringSelectMenuOptionBuilder = require('./structures/StringSelectMenuOptionBuilder'); @@ -213,12 +219,15 @@ exports.StickerPack = require('./structures/StickerPack'); exports.Team = require('./structures/Team'); exports.TeamMember = require('./structures/TeamMember'); exports.TextChannel = require('./structures/TextChannel'); +exports.TextDisplayComponent = require('./structures/TextDisplayComponent'); exports.TextInputBuilder = require('./structures/TextInputBuilder'); exports.TextInputComponent = require('./structures/TextInputComponent'); exports.ThreadChannel = require('./structures/ThreadChannel'); exports.ThreadMember = require('./structures/ThreadMember'); exports.ThreadOnlyChannel = require('./structures/ThreadOnlyChannel'); +exports.ThumbnailComponent = require('./structures/ThumbnailComponent'); exports.Typing = require('./structures/Typing'); +exports.UnfurledMediaItem = require('./structures/UnfurledMediaItem'); exports.User = require('./structures/User'); exports.UserContextMenuCommandInteraction = require('./structures/UserContextMenuCommandInteraction'); exports.VoiceChannelEffect = require('./structures/VoiceChannelEffect'); diff --git a/packages/discord.js/src/structures/Component.js b/packages/discord.js/src/structures/Component.js index 10ba27d05..7bdff5e7a 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 000000000..80a5ddc0d --- /dev/null +++ b/packages/discord.js/src/structures/ContainerComponent.js @@ -0,0 +1,60 @@ +'use strict'; + +const Component = require('./Component'); +const { createComponent } = require('../util/Components'); + +/** + * 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()) }; + } +} + +module.exports = ContainerComponent; diff --git a/packages/discord.js/src/structures/FileComponent.js b/packages/discord.js/src/structures/FileComponent.js new file mode 100644 index 000000000..adbdf6166 --- /dev/null +++ b/packages/discord.js/src/structures/FileComponent.js @@ -0,0 +1,40 @@ +'use strict'; + +const Component = require('./Component'); +const UnfurledMediaItem = require('./UnfurledMediaItem'); + +/** + * 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() }; + } +} + +module.exports = FileComponent; diff --git a/packages/discord.js/src/structures/MediaGalleryComponent.js b/packages/discord.js/src/structures/MediaGalleryComponent.js new file mode 100644 index 000000000..c848bb438 --- /dev/null +++ b/packages/discord.js/src/structures/MediaGalleryComponent.js @@ -0,0 +1,31 @@ +'use strict'; + +const Component = require('./Component'); +const MediaGalleryItem = require('./MediaGalleryItem'); + +/** + * 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()) }; + } +} + +module.exports = MediaGalleryComponent; diff --git a/packages/discord.js/src/structures/MediaGalleryItem.js b/packages/discord.js/src/structures/MediaGalleryItem.js new file mode 100644 index 000000000..17fdbf5f9 --- /dev/null +++ b/packages/discord.js/src/structures/MediaGalleryItem.js @@ -0,0 +1,51 @@ +'use strict'; + +const UnfurledMediaItem = require('./UnfurledMediaItem'); + +/** + * 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() }; + } +} + +module.exports = MediaGalleryItem; diff --git a/packages/discord.js/src/structures/Message.js b/packages/discord.js/src/structures/Message.js index 02c0c6014..7aff0f65f 100644 --- a/packages/discord.js/src/structures/Message.js +++ b/packages/discord.js/src/structures/Message.js @@ -23,7 +23,7 @@ const ReactionCollector = require('./ReactionCollector'); const { Sticker } = require('./Sticker'); const { DiscordjsError, ErrorCodes } = require('../errors'); const ReactionManager = require('../managers/ReactionManager'); -const { createComponent } = require('../util/Components'); +const { createComponent, findComponentByCustomId } = require('../util/Components'); const { NonSystemMessageTypes, MaxBulkDeletableMessageAge, UndeletableMessageTypes } = require('../util/Constants'); const MessageFlagsBitField = require('../util/MessageFlagsBitField'); const PermissionsBitField = require('../util/PermissionsBitField'); @@ -151,10 +151,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 { @@ -1055,7 +1055,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 2e6df11e5..f27050c15 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'); const InteractionWebhook = require('./InteractionWebhook'); const InteractionResponses = require('./interfaces/InteractionResponses'); +const { findComponentByCustomId } = require('../util/Components'); const getMessage = lazy(() => require('./Message').Message); @@ -79,13 +80,11 @@ 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 diff --git a/packages/discord.js/src/structures/MessagePayload.js b/packages/discord.js/src/structures/MessagePayload.js index a1816807b..cf53dc755 100644 --- a/packages/discord.js/src/structures/MessagePayload.js +++ b/packages/discord.js/src/structures/MessagePayload.js @@ -4,7 +4,6 @@ const { Buffer } = require('node:buffer'); const { lazy, isJSONEncodable } = require('@discordjs/util'); const { DiscordSnowflake } = require('@sapphire/snowflake'); const { MessageFlags, MessageReferenceType } = require('discord-api-types/v10'); -const ActionRowBuilder = require('./ActionRowBuilder'); const { DiscordjsError, DiscordjsRangeError, ErrorCodes } = require('../errors'); const { resolveFile } = require('../util/DataResolver'); const MessageFlagsBitField = require('../util/MessageFlagsBitField'); @@ -149,7 +148,7 @@ class MessagePayload { } const components = this.options.components?.map(component => - (isJSONEncodable(component) ? component : new ActionRowBuilder(component)).toJSON(), + isJSONEncodable(component) ? component.toJSON() : this.target.client.options.jsonTransformer(component), ); let username; diff --git a/packages/discord.js/src/structures/SectionComponent.js b/packages/discord.js/src/structures/SectionComponent.js new file mode 100644 index 000000000..41fde4392 --- /dev/null +++ b/packages/discord.js/src/structures/SectionComponent.js @@ -0,0 +1,42 @@ +'use strict'; + +const Component = require('./Component'); +const { createComponent } = require('../util/Components'); + +/** + * 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()), + }; + } +} + +module.exports = SectionComponent; diff --git a/packages/discord.js/src/structures/SeparatorComponent.js b/packages/discord.js/src/structures/SeparatorComponent.js new file mode 100644 index 000000000..e2b443e9f --- /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'); + +/** + * 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; + } +} + +module.exports = SeparatorComponent; diff --git a/packages/discord.js/src/structures/TextDisplayComponent.js b/packages/discord.js/src/structures/TextDisplayComponent.js new file mode 100644 index 000000000..8e2b4bcb4 --- /dev/null +++ b/packages/discord.js/src/structures/TextDisplayComponent.js @@ -0,0 +1,20 @@ +'use strict'; + +const Component = require('./Component'); + +/** + * 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; + } +} + +module.exports = TextDisplayComponent; diff --git a/packages/discord.js/src/structures/ThumbnailComponent.js b/packages/discord.js/src/structures/ThumbnailComponent.js new file mode 100644 index 000000000..ff62596e6 --- /dev/null +++ b/packages/discord.js/src/structures/ThumbnailComponent.js @@ -0,0 +1,49 @@ +'use strict'; + +const Component = require('./Component'); +const UnfurledMediaItem = require('./UnfurledMediaItem'); + +/** + * 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() }; + } +} + +module.exports = ThumbnailComponent; diff --git a/packages/discord.js/src/structures/UnfurledMediaItem.js b/packages/discord.js/src/structures/UnfurledMediaItem.js new file mode 100644 index 000000000..d52c4dd01 --- /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; + } +} + +module.exports = UnfurledMediaItem; diff --git a/packages/discord.js/src/structures/interfaces/TextBasedChannel.js b/packages/discord.js/src/structures/interfaces/TextBasedChannel.js index 5bfec44a8..41025fbf1 100644 --- a/packages/discord.js/src/structures/interfaces/TextBasedChannel.js +++ b/packages/discord.js/src/structures/interfaces/TextBasedChannel.js @@ -78,8 +78,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. */ /** @@ -107,7 +110,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 8d8abbd9f..d63f2113b 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} @@ -205,6 +230,16 @@ * @see {@link https://discord-api-types.dev/api/discord-api-types-v10#APIRoleSelectComponent} */ +/** + * @external APISectionComponent + * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APISectionComponent} + */ + +/** + * @external APISeparatorComponent + * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APISeparatorComponent} + */ + /** * @external APISelectMenuOption * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APISelectMenuOption} @@ -225,6 +260,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 8c256e173..405b8436c 100644 --- a/packages/discord.js/src/util/Components.js +++ b/packages/discord.js/src/util/Components.js @@ -5,6 +5,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 */ @@ -16,30 +17,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 */ /** @@ -51,15 +52,76 @@ 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 */ /** @@ -67,6 +129,11 @@ const { ComponentType } = require('discord-api-types/v10'); * @typedef {APIMessageComponentEmoji|string} ComponentEmojiResolvable */ +/** + * @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 @@ -95,6 +162,20 @@ 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); } @@ -133,7 +214,30 @@ function createComponentBuilder(data) { } } -module.exports = { createComponent, createComponentBuilder }; +/** + * 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 + ); +} + +module.exports = { createComponent, createComponentBuilder, findComponentByCustomId }; const ActionRow = require('../structures/ActionRow'); const ActionRowBuilder = require('../structures/ActionRowBuilder'); @@ -142,13 +246,20 @@ const ButtonComponent = require('../structures/ButtonComponent'); const ChannelSelectMenuBuilder = require('../structures/ChannelSelectMenuBuilder'); const ChannelSelectMenuComponent = require('../structures/ChannelSelectMenuComponent'); const Component = require('../structures/Component'); +const ContainerComponent = require('../structures/ContainerComponent'); +const FileComponent = require('../structures/FileComponent'); +const MediaGalleryComponent = require('../structures/MediaGalleryComponent'); const MentionableSelectMenuBuilder = require('../structures/MentionableSelectMenuBuilder'); const MentionableSelectMenuComponent = require('../structures/MentionableSelectMenuComponent'); const RoleSelectMenuBuilder = require('../structures/RoleSelectMenuBuilder'); const RoleSelectMenuComponent = require('../structures/RoleSelectMenuComponent'); +const SectionComponent = require('../structures/SectionComponent'); +const SeparatorComponent = require('../structures/SeparatorComponent'); const StringSelectMenuBuilder = require('../structures/StringSelectMenuBuilder'); const StringSelectMenuComponent = require('../structures/StringSelectMenuComponent'); +const TextDisplayComponent = require('../structures/TextDisplayComponent'); const TextInputBuilder = require('../structures/TextInputBuilder'); const TextInputComponent = require('../structures/TextInputComponent'); +const ThumbnailComponent = require('../structures/ThumbnailComponent'); const UserSelectMenuBuilder = require('../structures/UserSelectMenuBuilder'); const UserSelectMenuComponent = require('../structures/UserSelectMenuComponent'); diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index abe1c6f5d..39b63bd7c 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -110,13 +110,13 @@ import { AuditLogEvent, APIMessageComponentEmoji, EmbedType, - APIActionRowComponentTypes, + APIComponentInActionRow, APIModalInteractionResponseCallbackData, APIModalSubmitInteraction, - APIMessageActionRowComponent, + APIComponentInMessageActionRow, TextInputStyle, APITextInputComponent, - APIModalActionRowComponent, + APIComponentInModalActionRow, APIModalComponent, APISelectMenuOption, APIEmbedField, @@ -201,6 +201,18 @@ import { APIChatInputApplicationCommandInteractionData, APIContextMenuInteractionData, APISoundboardSound, + APIComponentInContainer, + APIContainerComponent, + APIThumbnailComponent, + APISectionComponent, + APITextDisplayComponent, + APIUnfurledMediaItem, + APIMediaGalleryItem, + APIMediaGalleryComponent, + APISeparatorComponent, + SeparatorSpacingSize, + APIFileComponent, + APIMessageTopLevelComponent, } from 'discord-api-types/v10'; import { ChildProcess } from 'node:child_process'; import { EventEmitter } from 'node:events'; @@ -297,11 +309,12 @@ export class Activity { export type ActivityFlagsString = keyof typeof ActivityFlags; export interface BaseComponentData { + id?: number; type: ComponentType; } export type MessageActionRowComponentData = - | JSONEncodable + | JSONEncodable | ButtonComponentData | StringSelectMenuComponentData | UserSelectMenuComponentData @@ -309,13 +322,13 @@ export type MessageActionRowComponentData = | MentionableSelectMenuComponentData | ChannelSelectMenuComponentData; -export type ModalActionRowComponentData = JSONEncodable | TextInputComponentData; +export type ModalActionRowComponentData = JSONEncodable | TextInputComponentData; export type ActionRowComponentData = MessageActionRowComponentData | ModalActionRowComponentData; export type ActionRowComponent = MessageActionRowComponent | ModalActionRowComponent; -export interface ActionRowData | ActionRowComponentData> +export interface ActionRowData | ActionRowComponentData> extends BaseComponentData { components: readonly ComponentType[]; } @@ -325,8 +338,8 @@ export class ActionRowBuilder< > extends BuilderActionRow { public constructor( data?: Partial< - | ActionRowData> - | APIActionRowComponent + | ActionRowData> + | APIActionRowComponent >, ); public static from( @@ -346,9 +359,9 @@ export type MessageActionRowComponent = export type ModalActionRowComponent = TextInputComponent; export class ActionRow extends Component< - APIActionRowComponent + APIActionRowComponent > { - private constructor(data: APIActionRowComponent); + private constructor(data: APIActionRowComponent); public readonly components: ComponentType[]; public toJSON(): APIActionRowComponent>; } @@ -782,15 +795,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; @@ -1194,6 +1229,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 { @@ -1587,6 +1656,16 @@ export class Guild extends AnonymousGuild { public toJSON(): unknown; } +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 GuildAuditLogs { private constructor(guild: Guild, data: RawGuildAuditLogData); private applicationCommands: Collection; @@ -2185,6 +2264,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; @@ -2257,7 +2357,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; @@ -2391,9 +2491,9 @@ export class MessageComponentInteraction e public get component(): CacheTypeReducer< Cached, MessageActionRowComponent, - APIMessageActionRowComponent, - MessageActionRowComponent | APIMessageActionRowComponent, - MessageActionRowComponent | APIMessageActionRowComponent + APIComponentInMessageActionRow, + MessageActionRowComponent | APIComponentInMessageActionRow, + MessageActionRowComponent | APIComponentInMessageActionRow >; public componentType: MessageComponentType; public customId: string; @@ -2601,7 +2701,7 @@ export interface ModalComponentData { customId: string; title: string; components: readonly ( - | JSONEncodable> + | JSONEncodable> | ActionRowData )[]; } @@ -2983,6 +3083,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 { @@ -3106,6 +3220,16 @@ export type AnySelectMenuInteraction = 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: []; @@ -3468,6 +3592,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; @@ -3567,6 +3700,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; @@ -3586,6 +3732,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 { @@ -3762,7 +3918,9 @@ export class Formatters extends null { export type ComponentData = | MessageActionRowComponentData | ModalActionRowComponentData - | ActionRowData; + | ComponentInContainerData + | ContainerComponentData + | ThumbnailComponentData; export interface SendSoundboardSoundOptions { soundId: Snowflake; @@ -6654,8 +6812,11 @@ export interface InteractionReplyOptions extends BaseMessageOptionsWithPoll { fetchReply?: boolean; flags?: | BitFieldResolvable< - Extract, - MessageFlags.Ephemeral | MessageFlags.SuppressEmbeds | MessageFlags.SuppressNotifications + Extract, + | MessageFlags.Ephemeral + | MessageFlags.SuppressEmbeds + | MessageFlags.SuppressNotifications + | MessageFlags.IsComponentsV2 > | undefined; } @@ -6836,9 +6997,10 @@ export interface BaseMessageOptions { | AttachmentPayload )[]; components?: readonly ( - | JSONEncodable> + | JSONEncodable + | TopLevelComponentData | ActionRowData - | APIActionRowComponent + | APIMessageTopLevelComponent )[]; } @@ -6855,8 +7017,8 @@ export interface MessageCreateOptions 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 b6e8b295f..1e39d76f3 100644 --- a/packages/discord.js/typings/index.test-d.ts +++ b/packages/discord.js/typings/index.test-d.ts @@ -25,7 +25,7 @@ import { ApplicationCommandType, APIMessage, APIActionRowComponent, - APIActionRowComponentTypes, + APIComponentInActionRow, APIStringSelectComponent, APIUserSelectComponent, APIRoleSelectComponent, @@ -36,6 +36,7 @@ import { GuildScheduledEventRecurrenceRuleFrequency, GuildScheduledEventRecurrenceRuleMonth, GuildScheduledEventRecurrenceRuleWeekday, + MessageFlags, } from 'discord-api-types/v10'; import { ApplicationCommand, @@ -219,6 +220,15 @@ import { InteractionCallbackResponse, GuildScheduledEventRecurrenceRuleOptions, ThreadOnlyChannel, + SectionComponentData, + TextDisplayComponentData, + ThumbnailComponentData, + UnfurledMediaItemData, + MediaGalleryComponentData, + MediaGalleryItemData, + SeparatorComponentData, + FileComponentData, + ContainerComponentData, } from '.'; import { expectAssignable, @@ -642,6 +652,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)); @@ -2412,7 +2473,7 @@ EmbedBuilder.from(embedData); declare const embedComp: Embed; EmbedBuilder.from(embedComp); -declare const actionRowData: APIActionRowComponent; +declare const actionRowData: APIActionRowComponent; ActionRowBuilder.from(actionRowData); declare const actionRowComp: ActionRow; @@ -2424,7 +2485,7 @@ declare const buttonsActionRowComp: ActionRow; expectType>(ActionRowBuilder.from(buttonsActionRowData)); expectType>(ActionRowBuilder.from(buttonsActionRowComp)); -declare const anyComponentsActionRowData: APIActionRowComponent; +declare const anyComponentsActionRowData: APIActionRowComponent; declare const anyComponentsActionRowComp: ActionRow; expectType(ActionRowBuilder.from(anyComponentsActionRowData)); diff --git a/packages/rest/package.json b/packages/rest/package.json index 6ebd8330b..de8ef7612 100644 --- a/packages/rest/package.json +++ b/packages/rest/package.json @@ -88,7 +88,7 @@ "@sapphire/async-queue": "^1.5.3", "@sapphire/snowflake": "^3.5.3", "@vladfrangu/async_event_emitter": "^2.4.6", - "discord-api-types": "^0.37.119", + "discord-api-types": "^0.38.1", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.1" diff --git a/packages/rest/src/lib/utils/constants.ts b/packages/rest/src/lib/utils/constants.ts index 91375b5a6..ed78b1c1d 100644 --- a/packages/rest/src/lib/utils/constants.ts +++ b/packages/rest/src/lib/utils/constants.ts @@ -1,8 +1,11 @@ import { getUserAgentAppendix } from '@discordjs/util'; +import type { ImageSize } from 'discord-api-types/v10'; import { APIVersion } from 'discord-api-types/v10'; import { getDefaultStrategy } from '../../environment.js'; import type { RESTOptions, ResponseLike } from './types.js'; +export type { ImageSize } from 'discord-api-types/v10'; + export const DefaultUserAgent = `DiscordBot (https://discord.js.org, [VI]{{inject}}[/VI])` as `DiscordBot (https://discord.js.org, ${string})`; @@ -48,11 +51,12 @@ export enum RESTEvents { export const ALLOWED_EXTENSIONS = ['webp', 'png', 'jpg', 'jpeg', 'gif'] as const satisfies readonly string[]; export const ALLOWED_STICKER_EXTENSIONS = ['png', 'json', 'gif'] as const satisfies readonly string[]; -export const ALLOWED_SIZES = [16, 32, 64, 128, 256, 512, 1_024, 2_048, 4_096] as const satisfies readonly number[]; +export const ALLOWED_SIZES: readonly number[] = [ + 16, 32, 64, 128, 256, 512, 1_024, 2_048, 4_096, +] satisfies readonly ImageSize[]; export type ImageExtension = (typeof ALLOWED_EXTENSIONS)[number]; export type StickerExtension = (typeof ALLOWED_STICKER_EXTENSIONS)[number]; -export type ImageSize = (typeof ALLOWED_SIZES)[number]; export const OverwrittenMimeTypes = { // https://github.com/discordjs/discord.js/issues/8557 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7171fcdca..4edf14bab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -920,8 +920,8 @@ importers: packages/discord.js: dependencies: '@discordjs/builders': - specifier: ^1.10.1 - version: 1.10.1 + specifier: ^1.11.0 + version: 1.11.0 '@discordjs/collection': specifier: 1.5.3 version: 1.5.3 @@ -941,8 +941,8 @@ importers: specifier: 3.5.3 version: 3.5.3 discord-api-types: - specifier: ^0.37.119 - version: 0.37.119 + specifier: ^0.38.1 + version: 0.38.1 fast-deep-equal: specifier: 3.1.3 version: 3.1.3 @@ -1310,8 +1310,8 @@ importers: specifier: ^2.4.6 version: 2.4.6 discord-api-types: - specifier: ^0.37.119 - version: 0.37.119 + specifier: ^0.38.1 + version: 0.38.1 magic-bytes.js: specifier: ^1.10.0 version: 1.10.0 @@ -2604,20 +2604,20 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} - '@definitelytyped/header-parser@0.2.16': - resolution: {integrity: sha512-UFsgPft5bhZn07UNGz/9ck4AhdKgLFEOmi2DNr7gXcGL89zbe3u5oVafKUT8j1HOtSBjT8ZEQsXHKlbq+wwF/Q==} + '@definitelytyped/header-parser@0.2.19': + resolution: {integrity: sha512-zu+RxQpUCgorYUQZoyyrRIn9CljL1CeM4qak3NDeMO1r7tjAkodfpAGnVzx/6JR2OUk0tAgwmZxNMSwd9LVgxw==} engines: {node: '>=18.18.0'} - '@definitelytyped/typescript-versions@0.1.6': - resolution: {integrity: sha512-gQpXFteIKrOw4ldmBZQfBrD3WobaIG1SwOr/3alXWkcYbkOWa2NRxQbiaYQ2IvYTGaZK26miJw0UOAFiuIs4gA==} + '@definitelytyped/typescript-versions@0.1.8': + resolution: {integrity: sha512-iz6q9aTwWW7CzN2g8jFQfZ955D63LA+wdIAKz4+2pCc/7kokmEHie1/jVWSczqLFOlmH+69bWQxIurryBP/sig==} engines: {node: '>=18.18.0'} '@definitelytyped/utils@0.1.8': resolution: {integrity: sha512-4JINx4Rttha29f50PBsJo48xZXx/He5yaIWJRwVarhYAN947+S84YciHl+AIhQNRPAFkg8+5qFngEGtKxQDWXA==} engines: {node: '>=18.18.0'} - '@discordjs/builders@1.10.1': - resolution: {integrity: sha512-OWo1fY4ztL1/M/DUyRPShB4d/EzVfuUvPTRRHRIt/YxBrUYSz0a+JicD5F5zHFoNs2oTuWavxCOVFV1UljHTng==} + '@discordjs/builders@1.11.0': + resolution: {integrity: sha512-JL+mkXDoaOi1xPE9iUmloiWDBEneA+/U0oM4kKqnQJAr3Iz3Vk4Rd9SnfYKPJjRjGUDvV5RFpOBJJaYI6ii6fA==} engines: {node: '>=16.11.0'} '@discordjs/collection@1.5.3': @@ -7646,6 +7646,9 @@ packages: discord-api-types@0.37.119: resolution: {integrity: sha512-WasbGFXEB+VQWXlo6IpW3oUv73Yuau1Ig4AZF/m13tXcTKnMpc/mHjpztIlz4+BM9FG9BHQkEXiPto3bKduQUg==} + discord-api-types@0.38.1: + resolution: {integrity: sha512-vsjsqjAuxsPhiwbPjTBeGQaDPlizFmSkU0mTzFGMgRxqCDIRBR7iTY74HacpzrDV0QtERHRKQEk1tq7drZUtHg==} + dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} @@ -8110,6 +8113,7 @@ packages: eslint-plugin-i@2.29.1: resolution: {integrity: sha512-ORizX37MelIWLbMyqI7hi8VJMf7A0CskMmYkB+lkCX3aF4pkGV7kwx5bSEb4qx7Yce2rAf9s34HqDRPjGRZPNQ==} engines: {node: '>=12'} + deprecated: Please migrate to the brand new `eslint-plugin-import-x` instead peerDependencies: eslint: ^7.2.0 || ^8 @@ -8234,6 +8238,7 @@ packages: eslint@8.57.0: resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true espree@10.1.0: @@ -12321,6 +12326,7 @@ packages: stream-connect@1.0.2: resolution: {integrity: sha512-68Kl+79cE0RGKemKkhxTSg8+6AGrqBt+cbZAXevg2iJ6Y3zX4JhA/sZeGzLpxW9cXhmqAcE7KnJCisUmIUfnFQ==} engines: {node: '>=0.10.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. stream-to-array@2.3.0: resolution: {integrity: sha512-UsZtOYEn4tWU2RGLOXr/o/xjRBftZRlG3dEWoaHr8j4GuypJ3isitGbVyjQKAuMu+xbiop8q224TjiZWc4XTZA==} @@ -14850,13 +14856,13 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 - '@definitelytyped/header-parser@0.2.16': + '@definitelytyped/header-parser@0.2.19': dependencies: - '@definitelytyped/typescript-versions': 0.1.6 + '@definitelytyped/typescript-versions': 0.1.8 '@definitelytyped/utils': 0.1.8 semver: 7.6.3 - '@definitelytyped/typescript-versions@0.1.6': {} + '@definitelytyped/typescript-versions@0.1.8': {} '@definitelytyped/utils@0.1.8': dependencies: @@ -14869,12 +14875,12 @@ snapshots: tar-stream: 3.1.7 which: 4.0.0 - '@discordjs/builders@1.10.1': + '@discordjs/builders@1.11.0': dependencies: '@discordjs/formatters': 0.6.0 '@discordjs/util': 1.1.1 '@sapphire/shapeshift': 4.0.0 - discord-api-types: 0.37.119 + discord-api-types: 0.38.1 fast-deep-equal: 3.1.3 ts-mixer: 6.0.4 tslib: 2.6.3 @@ -15450,7 +15456,7 @@ snapshots: '@jest/console@29.7.0': dependencies: '@jest/types': 29.6.3 - '@types/node': 18.19.45 + '@types/node': 18.19.74 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -15530,7 +15536,7 @@ snapshots: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 18.19.45 + '@types/node': 18.19.74 jest-mock: 29.7.0 '@jest/expect-utils@29.7.0': @@ -15548,7 +15554,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 18.19.45 + '@types/node': 18.19.74 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -15570,7 +15576,7 @@ snapshots: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.25 - '@types/node': 18.19.45 + '@types/node': 18.19.74 chalk: 4.1.2 collect-v8-coverage: 1.0.2 exit: 0.1.2 @@ -15832,7 +15838,7 @@ snapshots: '@rushstack/ts-command-line': 4.19.1(@types/node@16.18.105) lodash: 4.17.21 minimatch: 3.0.8 - resolve: 1.22.8 + resolve: 1.22.10 semver: 7.5.4 source-map: 0.6.1 typescript: 5.4.2 @@ -15851,7 +15857,7 @@ snapshots: '@rushstack/ts-command-line': 4.19.1(@types/node@18.17.9) lodash: 4.17.21 minimatch: 3.0.8 - resolve: 1.22.8 + resolve: 1.22.10 semver: 7.5.4 source-map: 0.6.1 typescript: 5.4.2 @@ -15870,7 +15876,7 @@ snapshots: '@rushstack/ts-command-line': 4.19.1(@types/node@18.19.45) lodash: 4.17.21 minimatch: 3.0.8 - resolve: 1.22.8 + resolve: 1.22.10 semver: 7.5.4 source-map: 0.6.1 typescript: 5.4.2 @@ -15888,7 +15894,7 @@ snapshots: '@rushstack/ts-command-line': 4.19.1(@types/node@20.16.1) lodash: 4.17.21 minimatch: 3.0.8 - resolve: 1.22.8 + resolve: 1.22.10 semver: 7.5.4 source-map: 0.6.1 typescript: 5.4.2 @@ -18937,25 +18943,25 @@ snapshots: '@types/body-parser@1.19.5': dependencies: '@types/connect': 3.4.38 - '@types/node': 18.19.45 + '@types/node': 18.19.74 '@types/concat-stream@2.0.3': dependencies: - '@types/node': 18.19.45 + '@types/node': 18.19.74 '@types/connect@3.4.38': dependencies: - '@types/node': 18.19.45 + '@types/node': 18.19.74 '@types/conventional-commits-parser@5.0.0': dependencies: - '@types/node': 18.19.45 + '@types/node': 18.19.74 '@types/cookiejar@2.1.5': {} '@types/cross-spawn@6.0.6': dependencies: - '@types/node': 18.19.45 + '@types/node': 18.19.74 '@types/debug@4.1.12': dependencies: @@ -18987,7 +18993,7 @@ snapshots: '@types/express-serve-static-core@4.19.5': dependencies: - '@types/node': 18.19.45 + '@types/node': 18.19.74 '@types/qs': 6.9.15 '@types/range-parser': 1.2.7 '@types/send': 0.17.4 @@ -19004,11 +19010,11 @@ snapshots: '@types/glob@7.2.0': dependencies: '@types/minimatch': 5.1.2 - '@types/node': 18.19.45 + '@types/node': 18.19.74 '@types/graceful-fs@4.1.9': dependencies: - '@types/node': 18.19.45 + '@types/node': 18.19.74 '@types/hast@2.3.10': dependencies: @@ -19090,7 +19096,7 @@ snapshots: '@types/node-fetch@2.6.11': dependencies: - '@types/node': 18.19.45 + '@types/node': 18.19.74 form-data: 4.0.0 '@types/node@16.18.105': {} @@ -19117,7 +19123,7 @@ snapshots: '@types/pg@8.11.6': dependencies: - '@types/node': 18.19.45 + '@types/node': 18.19.74 pg-protocol: 1.6.1 pg-types: 4.0.2 @@ -19150,12 +19156,12 @@ snapshots: '@types/send@0.17.4': dependencies: '@types/mime': 1.3.5 - '@types/node': 18.19.45 + '@types/node': 18.19.74 '@types/serve-static@1.15.7': dependencies: '@types/http-errors': 2.0.4 - '@types/node': 18.19.45 + '@types/node': 18.19.74 '@types/send': 0.17.4 '@types/stack-utils@2.0.3': {} @@ -19176,7 +19182,7 @@ snapshots: '@types/through@0.0.33': dependencies: - '@types/node': 18.19.45 + '@types/node': 18.19.74 '@types/tinycolor2@1.4.6': {} @@ -19320,7 +19326,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 8.2.0(typescript@5.5.4) '@typescript-eslint/utils': 8.2.0(eslint@8.57.0)(typescript@5.5.4) - debug: 4.3.6 + debug: 4.4.0 ts-api-utils: 1.3.0(typescript@5.5.4) optionalDependencies: typescript: 5.5.4 @@ -19384,7 +19390,7 @@ snapshots: dependencies: '@typescript-eslint/types': 8.2.0 '@typescript-eslint/visitor-keys': 8.2.0 - debug: 4.3.6 + debug: 4.4.0 globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.5 @@ -21525,6 +21531,8 @@ snapshots: discord-api-types@0.37.119: {} + discord-api-types@0.38.1: {} + dlv@1.1.3: {} dmd@6.2.3: @@ -21574,7 +21582,7 @@ snapshots: dts-critic@3.3.11(typescript@5.5.4): dependencies: - '@definitelytyped/header-parser': 0.2.16 + '@definitelytyped/header-parser': 0.2.19 command-exists: 1.2.9 rimraf: 3.0.2 semver: 6.3.1 @@ -21584,8 +21592,8 @@ snapshots: dtslint@4.2.1(typescript@5.5.4): dependencies: - '@definitelytyped/header-parser': 0.2.16 - '@definitelytyped/typescript-versions': 0.1.6 + '@definitelytyped/header-parser': 0.2.19 + '@definitelytyped/typescript-versions': 0.1.8 '@definitelytyped/utils': 0.1.8 dts-critic: 3.3.11(typescript@5.5.4) fs-extra: 6.0.1 @@ -23929,7 +23937,7 @@ snapshots: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 18.19.45 + '@types/node': 18.19.74 chalk: 4.1.2 co: 4.6.0 dedent: 1.5.3 @@ -24104,7 +24112,7 @@ snapshots: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 18.19.45 + '@types/node': 18.19.74 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -24114,7 +24122,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 18.19.45 + '@types/node': 18.19.74 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -24153,7 +24161,7 @@ snapshots: jest-mock@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 18.19.45 + '@types/node': 18.19.74 jest-util: 29.7.0 jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): @@ -24188,7 +24196,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 18.19.45 + '@types/node': 18.19.74 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -24216,7 +24224,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 18.19.45 + '@types/node': 18.19.74 chalk: 4.1.2 cjs-module-lexer: 1.3.1 collect-v8-coverage: 1.0.2 @@ -24262,7 +24270,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 18.19.45 + '@types/node': 18.19.74 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -24281,7 +24289,7 @@ snapshots: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 18.19.45 + '@types/node': 18.19.74 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -24295,7 +24303,7 @@ snapshots: jest-worker@29.7.0: dependencies: - '@types/node': 18.19.45 + '@types/node': 18.19.74 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -26059,7 +26067,7 @@ snapshots: isbinaryfile: 4.0.10 lodash.get: 4.4.2 mkdirp: 0.5.6 - resolve: 1.22.8 + resolve: 1.22.10 node-releases@2.0.18: {} @@ -26769,7 +26777,7 @@ snapshots: '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.0 - '@types/node': 18.19.45 + '@types/node': 18.19.74 long: 5.2.3 proxy-addr@2.0.7: @@ -26780,7 +26788,7 @@ snapshots: proxy-agent@6.4.0: dependencies: agent-base: 7.1.1 - debug: 4.3.6 + debug: 4.4.0 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.5 lru-cache: 7.18.3 @@ -26952,7 +26960,7 @@ snapshots: '@types/doctrine': 0.0.9 '@types/resolve': 1.20.6 doctrine: 3.0.0 - resolve: 1.22.8 + resolve: 1.22.10 strip-indent: 4.0.0 transitivePeerDependencies: - supports-color @@ -28104,7 +28112,7 @@ snapshots: dependencies: component-emitter: 1.3.1 cookiejar: 2.1.4 - debug: 4.3.6 + debug: 4.4.0 fast-safe-stringify: 2.1.1 form-data: 4.0.0 formidable: 3.5.1