From cffb0b588f573df785124b34a10048b9c0a550d8 Mon Sep 17 00:00:00 2001 From: Jiralite <33201955+Jiralite@users.noreply.github.com> Date: Sun, 19 Jul 2026 23:35:39 +0100 Subject: [PATCH] feat: `file types` for discord.js (#11580) * feat: file types * fix(ApplicationCommandNonOptions): make it actually work --- .../src/structures/ApplicationCommand.js | 16 +++++++ packages/discord.js/src/util/APITypes.js | 5 +++ packages/discord.js/src/util/Components.js | 2 + packages/discord.js/typings/index.d.ts | 14 ++++++- packages/discord.js/typings/index.test-d.ts | 42 +++++++++++++++++++ 5 files changed, 77 insertions(+), 2 deletions(-) diff --git a/packages/discord.js/src/structures/ApplicationCommand.js b/packages/discord.js/src/structures/ApplicationCommand.js index 1f9d6d422..bb3af8075 100644 --- a/packages/discord.js/src/structures/ApplicationCommand.js +++ b/packages/discord.js/src/structures/ApplicationCommand.js @@ -264,6 +264,9 @@ class ApplicationCommand extends Base { * @property {ApplicationCommandOptionData[]} [options] Additional options if this option is a subcommand (group) * @property {ChannelType[]} [channelTypes] When the option type is channel, * the allowed types of channels that can be selected + * @property {FileUploadType[]} [fileTypes] When the option type is attachment, + * the allowed types of files that can be uploaded. When only using extensions, include `.jpg` + * for images and both `.mp4` and `.mov` for videos for mobile compatibility * @property {number} [minValue] The minimum value for an {@link ApplicationCommandOptionType.Integer} or * {@link ApplicationCommandOptionType.Number} option * @property {number} [maxValue] The maximum value for an {@link ApplicationCommandOptionType.Integer} or @@ -495,6 +498,7 @@ class ApplicationCommand extends Base { option.choices?.length !== existing.choices?.length || option.options?.length !== existing.options?.length || (option.channelTypes ?? option.channel_types)?.length !== existing.channelTypes?.length || + (option.fileTypes ?? option.file_types)?.length !== existing.fileTypes?.length || (option.minValue ?? option.min_value) !== existing.minValue || (option.maxValue ?? option.max_value) !== existing.maxValue || (option.minLength ?? option.min_length) !== existing.minLength || @@ -539,6 +543,13 @@ class ApplicationCommand extends Base { } } + if (existing.fileTypes) { + const newTypes = option.fileTypes ?? option.file_types; + for (const type of existing.fileTypes) { + if (!newTypes.includes(type)) return false; + } + } + if (existing.options) { return this.optionsEqual(existing.options, option.options, enforceOptionOrder); } @@ -563,6 +574,9 @@ class ApplicationCommand extends Base { * @property {ApplicationCommandOption[]} [options] Additional options if this option is a subcommand (group) * @property {ApplicationCommandOptionAllowedChannelTypes[]} [channelTypes] When the option type is channel, * the allowed types of channels that can be selected + * @property {FileUploadType[]} [fileTypes] When the option type is attachment, + * the allowed types of files that can be uploaded. When only using extensions, include `.jpg` + * for images and both `.mp4` and `.mov` for videos for mobile compatibility * @property {number} [minValue] The minimum value for an {@link ApplicationCommandOptionType.Integer} or * {@link ApplicationCommandOptionType.Number} option * @property {number} [maxValue] The maximum value for an {@link ApplicationCommandOptionType.Integer} or @@ -591,6 +605,7 @@ class ApplicationCommand extends Base { */ static transformOption(option, received) { const channelTypesKey = received ? 'channelTypes' : 'channel_types'; + const fileTypesKey = received ? 'fileTypes' : 'file_types'; const minValueKey = received ? 'minValue' : 'min_value'; const maxValueKey = received ? 'maxValue' : 'max_value'; const minLengthKey = received ? 'minLength' : 'min_length'; @@ -622,6 +637,7 @@ class ApplicationCommand extends Base { })), options: option.options?.map(opt => this.transformOption(opt, received)), [channelTypesKey]: option.channelTypes ?? option.channel_types, + [fileTypesKey]: option.fileTypes ?? option.file_types, [minValueKey]: option.minValue ?? option.min_value, [maxValueKey]: option.maxValue ?? option.max_value, [minLengthKey]: option.minLength ?? option.min_length, diff --git a/packages/discord.js/src/util/APITypes.js b/packages/discord.js/src/util/APITypes.js index dca194dba..731a3d43c 100644 --- a/packages/discord.js/src/util/APITypes.js +++ b/packages/discord.js/src/util/APITypes.js @@ -385,6 +385,11 @@ * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/EntryPointCommandHandlerType} */ +/** + * @external FileUploadType + * @see {@link https://discord-api-types.dev/api/discord-api-types-v10#FileUploadType} + */ + /** * @external ForumLayoutType * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/ForumLayoutType} diff --git a/packages/discord.js/src/util/Components.js b/packages/discord.js/src/util/Components.js index 204a6c3db..110f9fcaa 100644 --- a/packages/discord.js/src/util/Components.js +++ b/packages/discord.js/src/util/Components.js @@ -50,6 +50,8 @@ const { ComponentType } = require('discord-api-types/v10'); * @property {string} customId The custom id of the file upload * @property {number} [minValues] The minimum number of files that must be uploaded (0-10) * @property {number} [maxValues] The maximum number of files that can be uploaded (1-10) + * @property {FileUploadType[]} [fileTypes] The allowed types of files that can be uploaded (maximum of 10). + * When only using extensions, include `.jpg` for images and both `.mp4` and `.mov` for videos for mobile compatibility * @property {boolean} [required] Whether this component is required in modals */ diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 8be7ec02e..a4f3822d9 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -82,6 +82,7 @@ import { ButtonStyle, ChannelType, ComponentType, + FileUploadType, GatewayDispatchEvents, GatewayVoiceServerUpdateDispatchData, GatewayVoiceStateUpdateDispatchData, @@ -5671,7 +5672,14 @@ export interface ApplicationCommandMentionableOption extends BaseApplicationComm type: ApplicationCommandOptionType.Mentionable; } +export interface ApplicationCommandAttachmentOptionData extends BaseApplicationCommandOptionsData { + fileTypes?: readonly FileUploadType[]; + file_types?: readonly FileUploadType[]; + type: ApplicationCommandOptionType.Attachment; +} + export interface ApplicationCommandAttachmentOption extends BaseApplicationCommandOptionsData { + fileTypes?: readonly FileUploadType[]; type: ApplicationCommandOptionType.Attachment; } @@ -5797,15 +5805,16 @@ export interface ApplicationCommandSubCommand extends Omit; } export interface ApplicationCommandNonOptions extends BaseApplicationCommandOptionsData { - type: Exclude; + type: Exclude; } export type ApplicationCommandOptionData = | ApplicationCommandSubGroupData + | ApplicationCommandAttachmentOptionData | ApplicationCommandNonOptionsData | ApplicationCommandChannelOptionData | ApplicationCommandAutocompleteNumericOptionData @@ -7545,6 +7554,7 @@ export interface TextInputComponentData extends BaseComponentData { export interface FileUploadComponentData extends BaseComponentData { customId: string; + fileTypes?: readonly FileUploadType[]; maxValues?: number; minValues?: number; required?: boolean; diff --git a/packages/discord.js/typings/index.test-d.ts b/packages/discord.js/typings/index.test-d.ts index 7aab67ad8..8420c59ed 100644 --- a/packages/discord.js/typings/index.test-d.ts +++ b/packages/discord.js/typings/index.test-d.ts @@ -41,6 +41,8 @@ import { } from 'discord-api-types/v10'; import { ApplicationCommand, + ApplicationCommandAttachmentOption, + ApplicationCommandAttachmentOptionData, ApplicationCommandData, ApplicationCommandManager, ApplicationCommandOptionData, @@ -62,6 +64,7 @@ import { CommandOptionNonChoiceResolvableType, ContextMenuCommandInteraction, DMChannel, + FileUploadComponentData, Guild, GuildApplicationCommandManager, GuildChannelManager, @@ -1641,6 +1644,21 @@ declare const applicationCommandChannelOption: ApplicationCommandChannelOption; applicationCommandChannelOption.channelTypes = [] as const; } +declare const applicationCommandAttachmentOptionData: ApplicationCommandAttachmentOptionData; +declare const applicationCommandAttachmentOption: ApplicationCommandAttachmentOption; +{ + applicationCommandAttachmentOptionData.fileTypes = ['image', '.pdf'] as const; + applicationCommandAttachmentOptionData.file_types = ['video', '.mov'] as const; + applicationCommandAttachmentOption.fileTypes = ['audio', '.flac'] as const; + + expectNotAssignable({ + description: 'Upload a file', + fileTypes: ['pdf'], + name: 'file', + type: ApplicationCommandOptionType.Attachment, + }); +} + declare const applicationNonChoiceOptionData: ApplicationCommandOptionData & { type: CommandOptionNonChoiceResolvableType; }; @@ -2579,6 +2597,15 @@ chatInputInteraction.showModal({ ], type: ComponentType.ActionRow, }, + { + component: { + type: ComponentType.FileUpload, + custom_id: 'upload', + file_types: ['image', '.pdf'], + }, + type: ComponentType.Label, + label: 'upload', + }, ], }); @@ -2632,6 +2659,15 @@ chatInputInteraction.showModal({ type: ComponentType.RoleSelect, }, }, + { + type: ComponentType.Label, + component: { + type: ComponentType.FileUpload, + customId: 'upload', + fileTypes: ['video', '.mp4', '.mov'], + }, + label: 'upload', + }, ], }); @@ -2650,6 +2686,12 @@ ChannelSelectMenuBuilder.from(channelSelectMenuData); declare const mentionableSelectMenuData: APIMentionableSelectComponent; MentionableSelectMenuBuilder.from(mentionableSelectMenuData); +expectNotAssignable({ + customId: 'upload', + fileTypes: ['pdf'], + type: ComponentType.FileUpload, +}); + declare const stringSelectMenuComp: StringSelectMenuComponent; StringSelectMenuBuilder.from(stringSelectMenuComp);