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