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 <me@vladfrangu.dev>
Co-authored-by: Timo <mail@geniustimo.de>
This commit is contained in:
Qjuh
2025-04-25 22:43:09 +02:00
committed by GitHub
parent d3154cf8f1
commit edace17a13
24 changed files with 883 additions and 123 deletions

View File

@@ -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,

View File

@@ -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",

View File

@@ -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');

View File

@@ -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}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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.
* <info>This property requires the {@link GatewayIntentBits.MessageContent} privileged intent
* in a guild for messages that do not mention the client.</info>
* @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);
}
/**

View File

@@ -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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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.
* <info>When using components v2, the flag {@link MessageFlags.IsComponentsV2} needs to be set
* and `content`, `embeds`, `stickers`, and `poll` cannot be used.</info>
*/
/**
@@ -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.
* <info>Only `MessageFlags.SuppressEmbeds` and `MessageFlags.SuppressNotifications` can be set.</info>
* <info>Only {@link MessageFlags.SuppressEmbeds}, {@link MessageFlags.SuppressNotifications} and
* {@link MessageFlags.IsComponentsV2} can be set.</info>
* <info>{@link MessageFlags.IsComponentsV2} is required if passing components that aren't action rows</info>
*/
/**

View File

@@ -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}

View File

@@ -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<Component|APIMessageComponent>} 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');

View File

@@ -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<APIMessageActionRowComponent>
| JSONEncodable<APIComponentInMessageActionRow>
| ButtonComponentData
| StringSelectMenuComponentData
| UserSelectMenuComponentData
@@ -309,13 +322,13 @@ export type MessageActionRowComponentData =
| MentionableSelectMenuComponentData
| ChannelSelectMenuComponentData;
export type ModalActionRowComponentData = JSONEncodable<APIModalActionRowComponent> | TextInputComponentData;
export type ModalActionRowComponentData = JSONEncodable<APIComponentInModalActionRow> | TextInputComponentData;
export type ActionRowComponentData = MessageActionRowComponentData | ModalActionRowComponentData;
export type ActionRowComponent = MessageActionRowComponent | ModalActionRowComponent;
export interface ActionRowData<ComponentType extends JSONEncodable<APIActionRowComponentTypes> | ActionRowComponentData>
export interface ActionRowData<ComponentType extends JSONEncodable<APIComponentInActionRow> | ActionRowComponentData>
extends BaseComponentData {
components: readonly ComponentType[];
}
@@ -325,8 +338,8 @@ export class ActionRowBuilder<
> extends BuilderActionRow<ComponentType> {
public constructor(
data?: Partial<
| ActionRowData<ActionRowComponentData | JSONEncodable<APIActionRowComponentTypes>>
| APIActionRowComponent<APIMessageActionRowComponent | APIModalActionRowComponent>
| ActionRowData<ActionRowComponentData | JSONEncodable<APIComponentInActionRow>>
| APIActionRowComponent<APIComponentInMessageActionRow | APIComponentInModalActionRow>
>,
);
public static from<ComponentType extends AnyComponentBuilder = AnyComponentBuilder>(
@@ -346,9 +359,9 @@ export type MessageActionRowComponent =
export type ModalActionRowComponent = TextInputComponent;
export class ActionRow<ComponentType extends MessageActionRowComponent | ModalActionRowComponent> extends Component<
APIActionRowComponent<APIMessageActionRowComponent | APIModalActionRowComponent>
APIActionRowComponent<APIComponentInMessageActionRow | APIComponentInModalActionRow>
> {
private constructor(data: APIActionRowComponent<APIMessageActionRowComponent | APIModalActionRowComponent>);
private constructor(data: APIActionRowComponent<APIComponentInMessageActionRow | APIComponentInModalActionRow>);
public readonly components: ComponentType[];
public toJSON(): APIActionRowComponent<ReturnType<ComponentType['toJSON']>>;
}
@@ -782,15 +795,37 @@ export class ButtonInteraction<Cached extends CacheType = CacheType> extends Mes
export type AnyComponent =
| APIMessageComponent
| APIModalComponent
| APIActionRowComponent<APIMessageActionRowComponent | APIModalActionRowComponent>;
| APIActionRowComponent<APIComponentInMessageActionRow | APIComponentInModalActionRow>
| AnyComponentV2;
export class Component<RawComponentData extends AnyComponent = AnyComponent> {
public readonly data: Readonly<RawComponentData>;
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<MessageActionRowComponent>
| ContainerComponent
| FileComponent
| MediaGalleryComponent
| SectionComponent
| SeparatorComponent
| TextDisplayComponent;
export type TopLevelComponentData =
| ActionRowData<MessageActionRowComponentData>
| ContainerComponentData
| FileComponentData
| MediaGalleryComponentData
| SectionComponentData
| SeparatorComponentData
| TextDisplayComponentData;
export class ButtonComponent extends Component<APIButtonComponent> {
private constructor(data: APIButtonComponent);
public get style(): ButtonStyle;
@@ -1194,6 +1229,40 @@ export class ClientVoiceManager {
public adapters: Map<Snowflake, InternalDiscordGatewayAdapterLibraryMethods>;
}
export type ComponentInContainer =
| ActionRow<MessageActionRowComponent>
| FileComponent
| MediaGalleryComponent
| SectionComponent
| SeparatorComponent
| TextDisplayComponent;
export type ComponentInContainerData =
| ActionRowData<ActionRowComponentData>
| FileComponentData
| MediaGalleryComponentData
| SectionComponentData
| SeparatorComponentData
| TextDisplayComponentData;
export interface ContainerComponentData<
ComponentType extends JSONEncodable<APIComponentInContainer> | ComponentInContainerData =
| JSONEncodable<APIComponentInContainer>
| ComponentInContainerData,
> extends BaseComponentData {
components: readonly ComponentType[];
accentColor?: number;
spoiler?: boolean;
}
export class ContainerComponent extends Component<APIContainerComponent> {
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<Key, Value, Extras extends unknown[] = []> {
@@ -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<APIFileComponent> {
private constructor(data: APIFileComponent);
public readonly file: UnfurledMediaItem;
public get spoiler(): boolean;
}
export class GuildAuditLogs<Event extends GuildAuditLogsResolvable = AuditLogEvent> {
private constructor(guild: Guild, data: RawGuildAuditLogData);
private applicationCommands: Collection<Snowflake, ApplicationCommand>;
@@ -2185,6 +2264,27 @@ export class LimitedCollection<Key, Value> extends Collection<Key, Value> {
public keepOverLimit: ((value: Value, key: Key, collection: this) => boolean) | null;
}
export interface MediaGalleryComponentData extends BaseComponentData {
items: readonly MediaGalleryItemData[];
}
export class MediaGalleryComponent extends Component<APIMediaGalleryComponent> {
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<InGuild extends boolean = boolean> extends Base {
public get channel(): If<InGuild, GuildTextBasedChannel, TextBasedChannel>;
public channelId: Snowflake;
public get cleanContent(): string;
public components: ActionRow<MessageActionRowComponent>[];
public components: TopLevelComponent[];
public content: string;
public get createdAt(): Date;
public createdTimestamp: number;
@@ -2391,9 +2491,9 @@ export class MessageComponentInteraction<Cached extends CacheType = CacheType> 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<APIActionRowComponent<APIModalActionRowComponent>>
| JSONEncodable<APIActionRowComponent<APIComponentInModalActionRow>>
| ActionRowData<ModalActionRowComponentData>
)[];
}
@@ -2983,6 +3083,20 @@ export class RoleFlagsBitField extends BitField<RoleFlagsString> {
public static resolve(bit?: BitFieldResolvable<RoleFlagsString, number>): number;
}
export interface SectionComponentData extends BaseComponentData {
accessory: ButtonComponentData | ThumbnailComponentData;
components: readonly TextDisplayComponentData[];
}
export class SectionComponent<
AccessoryType extends ButtonComponent | ThumbnailComponent = ButtonComponent | ThumbnailComponent,
> extends Component<APISectionComponent> {
private constructor(data: APISectionComponent);
public readonly accessory: AccessoryType;
public readonly components: TextDisplayComponent[];
public toJSON(): APISectionComponent;
}
export class StringSelectMenuInteraction<
Cached extends CacheType = CacheType,
> extends MessageComponentInteraction<Cached> {
@@ -3106,6 +3220,16 @@ export type AnySelectMenuInteraction<Cached extends CacheType = CacheType> =
export type SelectMenuType = APISelectMenuComponent['type'];
export interface SeparatorComponentData extends BaseComponentData {
spacing?: SeparatorSpacingSize;
dividier?: boolean;
}
export class SeparatorComponent extends Component<APISeparatorComponent> {
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<APITextDisplayComponent> {
private constructor(data: APITextDisplayComponent);
public readonly content: string;
}
export type ForumThreadChannel = PublicThreadChannel<true>;
export type TextThreadChannel = PublicThreadChannel<false> | PrivateThreadChannel;
export type AnyThreadChannel = TextThreadChannel | ForumThreadChannel;
@@ -3567,6 +3700,19 @@ export class ThreadMemberFlagsBitField extends BitField<ThreadMemberFlagsString>
public static resolve(bit?: BitFieldResolvable<ThreadMemberFlagsString, number>): number;
}
export interface ThumbnailComponentData extends BaseComponentData {
media: UnfurledMediaItemData;
description?: string;
spoiler?: boolean;
}
export class ThumbnailComponent extends Component<APIThumbnailComponent> {
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<false> {}
export class User extends Base {
@@ -3762,7 +3918,9 @@ export class Formatters extends null {
export type ComponentData =
| MessageActionRowComponentData
| ModalActionRowComponentData
| ActionRowData<MessageActionRowComponentData | ModalActionRowComponentData>;
| ComponentInContainerData
| ContainerComponentData
| ThumbnailComponentData;
export interface SendSoundboardSoundOptions {
soundId: Snowflake;
@@ -6654,8 +6812,11 @@ export interface InteractionReplyOptions extends BaseMessageOptionsWithPoll {
fetchReply?: boolean;
flags?:
| BitFieldResolvable<
Extract<MessageFlagsString, 'Ephemeral' | 'SuppressEmbeds' | 'SuppressNotifications'>,
MessageFlags.Ephemeral | MessageFlags.SuppressEmbeds | MessageFlags.SuppressNotifications
Extract<MessageFlagsString, 'Ephemeral' | 'SuppressEmbeds' | 'SuppressNotifications' | 'IsComponentsV2'>,
| MessageFlags.Ephemeral
| MessageFlags.SuppressEmbeds
| MessageFlags.SuppressNotifications
| MessageFlags.IsComponentsV2
>
| undefined;
}
@@ -6836,9 +6997,10 @@ export interface BaseMessageOptions {
| AttachmentPayload
)[];
components?: readonly (
| JSONEncodable<APIActionRowComponent<APIMessageActionRowComponent>>
| JSONEncodable<APIMessageTopLevelComponent>
| TopLevelComponentData
| ActionRowData<MessageActionRowComponentData | MessageActionRowComponentBuilder>
| APIActionRowComponent<APIMessageActionRowComponent>
| APIMessageTopLevelComponent
)[];
}
@@ -6855,8 +7017,8 @@ export interface MessageCreateOptions extends BaseMessageOptionsWithPoll {
stickers?: readonly StickerResolvable[];
flags?:
| BitFieldResolvable<
Extract<MessageFlagsString, 'SuppressEmbeds' | 'SuppressNotifications'>,
MessageFlags.SuppressEmbeds | MessageFlags.SuppressNotifications
Extract<MessageFlagsString, 'SuppressEmbeds' | 'SuppressNotifications' | 'IsComponentsV2'>,
MessageFlags.SuppressEmbeds | MessageFlags.SuppressNotifications | MessageFlags.IsComponentsV2
>
| undefined;
}

View File

@@ -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<true>>(client));
@@ -2412,7 +2473,7 @@ EmbedBuilder.from(embedData);
declare const embedComp: Embed;
EmbedBuilder.from(embedComp);
declare const actionRowData: APIActionRowComponent<APIActionRowComponentTypes>;
declare const actionRowData: APIActionRowComponent<APIComponentInActionRow>;
ActionRowBuilder.from(actionRowData);
declare const actionRowComp: ActionRow<ActionRowComponent>;
@@ -2424,7 +2485,7 @@ declare const buttonsActionRowComp: ActionRow<ButtonComponent>;
expectType<ActionRowBuilder<ButtonBuilder>>(ActionRowBuilder.from<ButtonBuilder>(buttonsActionRowData));
expectType<ActionRowBuilder<ButtonBuilder>>(ActionRowBuilder.from<ButtonBuilder>(buttonsActionRowComp));
declare const anyComponentsActionRowData: APIActionRowComponent<APIActionRowComponentTypes>;
declare const anyComponentsActionRowData: APIActionRowComponent<APIComponentInActionRow>;
declare const anyComponentsActionRowComp: ActionRow<ActionRowComponent>;
expectType<ActionRowBuilder>(ActionRowBuilder.from(anyComponentsActionRowData));

View File

@@ -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"

View File

@@ -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