feat(types,bot): Add Label component and new modal stuff (#4304)

* feat(types,bot): Add Label component and new modal stuff

Since now there are some fields that are only in responses the types got a bit more complicated

* Add char limits to label label & desc

* update comments

* fix format

* Move Require to shared.ts

* code review

---------

Co-authored-by: Awesome Stickz <awesome@stickz.dev>
This commit is contained in:
Fleny
2025-09-17 21:20:59 +02:00
committed by GitHub
parent 35133a1575
commit 87d16dd9ba
9 changed files with 329 additions and 46 deletions

View File

@@ -338,6 +338,8 @@ export function createDesiredPropertiesObject<T extends RecursivePartial<Transfo
accentColor: defaultValue,
name: defaultValue,
size: defaultValue,
component: defaultValue,
values: defaultValue,
...desiredProperties.component,
},
forumTag: {

View File

@@ -47,6 +47,7 @@ import type {
DiscordMessage,
DiscordMessageCall,
DiscordMessageComponent,
DiscordMessageComponentFromModalInteractionResponse,
DiscordMessageInteractionMetadata,
DiscordMessagePin,
DiscordMessageSnapshot,
@@ -266,7 +267,14 @@ export type TransformerFunctions<TProps extends TransformersDesiredProperties, T
avatarDecorationData: TransformerFunction<TProps, TBehavior, DiscordAvatarDecorationData, AvatarDecorationData>
channel: TransformerFunction<TProps, TBehavior, DiscordChannel, Channel, { guildId?: BigString }>
collectibles: TransformerFunction<TProps, TBehavior, DiscordCollectibles, Collectibles>
component: TransformerFunction<TProps, TBehavior, DiscordMessageComponent, Component, {}, 'unchanged'>
component: TransformerFunction<
TProps,
TBehavior,
DiscordMessageComponent | DiscordMessageComponentFromModalInteractionResponse,
Component,
{},
'unchanged'
>
defaultReactionEmoji: TransformerFunction<TProps, TBehavior, DiscordDefaultReactionEmoji, DefaultReactionEmoji>
embed: TransformerFunction<TProps, TBehavior, DiscordEmbed, Embed, {}, 'unchanged'>
emoji: TransformerFunction<TProps, TBehavior, DiscordEmoji, Emoji>
@@ -352,7 +360,7 @@ export type Transformers<TProps extends TransformersDesiredProperties, TBehavior
applicationCommandOption: (bot: Bot<TProps, TBehavior>, payload: ApplicationCommandOption) => DiscordApplicationCommandOption
applicationCommandOptionChoice: (bot: Bot<TProps, TBehavior>, payload: ApplicationCommandOptionChoice) => DiscordApplicationCommandOptionChoice
attachment: (bot: Bot<TProps, TBehavior>, payload: SetupDesiredProps<Attachment, TProps, TBehavior>) => DiscordAttachment
component: (bot: Bot<TProps, TBehavior>, payload: Component) => DiscordMessageComponent
component: (bot: Bot<TProps, TBehavior>, payload: Component) => DiscordMessageComponent | DiscordMessageComponentFromModalInteractionResponse
embed: (bot: Bot<TProps, TBehavior>, payload: Embed) => DiscordEmbed
mediaGalleryItem: (bot: Bot<TProps, TBehavior>, payload: MediaGalleryItem) => DiscordMediaGalleryItem
member: (bot: Bot<TProps, TBehavior>, payload: SetupDesiredProps<Member, TProps, TBehavior>) => DiscordMember

View File

@@ -3,14 +3,18 @@ import {
type DiscordButtonComponent,
type DiscordContainerComponent,
type DiscordFileComponent,
type DiscordLabelComponent,
type DiscordMediaGalleryComponent,
type DiscordMediaGalleryItem,
type DiscordMessageComponent,
type DiscordMessageComponentFromModalInteractionResponse,
type DiscordSectionComponent,
type DiscordSelectMenuComponent,
type DiscordSeparatorComponent,
type DiscordStringSelectInteractionResponseFromModal,
type DiscordTextDisplayComponent,
type DiscordTextInputComponent,
type DiscordTextInputInteractionResponse,
type DiscordThumbnailComponent,
type DiscordUnfurledMediaItem,
MessageComponentTypes,
@@ -18,7 +22,7 @@ import {
import type { Bot } from '../bot.js'
import type { Component, MediaGalleryItem, UnfurledMediaItem } from './types.js'
export function transformComponent(bot: Bot, payload: DiscordMessageComponent): Component {
export function transformComponent(bot: Bot, payload: DiscordMessageComponent | DiscordMessageComponentFromModalInteractionResponse): Component {
let component: Component
// This switch is exhaustive, so we dont need the default case and TS does not error out for the un-initialized component variable
@@ -60,6 +64,9 @@ export function transformComponent(bot: Bot, payload: DiscordMessageComponent):
case MessageComponentTypes.TextDisplay:
component = transformTextDisplayComponent(bot, payload)
break
case MessageComponentTypes.Label:
component = transformLabelComponent(bot, payload)
break
}
return bot.transformers.customizers.component(bot, payload, component)
@@ -131,55 +138,65 @@ function transformButtonComponent(bot: Bot, payload: DiscordButtonComponent): Co
return button
}
function transformInputTextComponent(bot: Bot, payload: DiscordTextInputComponent): Component {
function transformInputTextComponent(bot: Bot, payload: DiscordTextInputComponent | DiscordTextInputInteractionResponse): Component {
const props = bot.transformers.desiredProperties.component
const input = {} as Component
if (props.type && payload.type) input.type = payload.type
if (props.id && payload.id) input.id = payload.id
if (props.style && payload.style) input.style = payload.style
if (props.required && payload.required) input.required = payload.required
if (props.customId && payload.custom_id) input.customId = payload.custom_id
if (props.label && payload.label) input.label = payload.label
if (props.placeholder && payload.placeholder) input.placeholder = payload.placeholder
if (props.minLength && payload.min_length) input.minLength = payload.min_length
if (props.maxLength && payload.max_length) input.maxLength = payload.max_length
if (props.value && payload.value) input.value = payload.value
// Check if it is the component or the response
if ('style' in payload) {
if (props.style && payload.style) input.style = payload.style
if (props.required && payload.required) input.required = payload.required
if (props.customId && payload.custom_id) input.customId = payload.custom_id
if (props.label && payload.label) input.label = payload.label
if (props.placeholder && payload.placeholder) input.placeholder = payload.placeholder
if (props.minLength && payload.min_length) input.minLength = payload.min_length
if (props.maxLength && payload.max_length) input.maxLength = payload.max_length
}
return input
}
function transformSelectMenuComponent(bot: Bot, payload: DiscordSelectMenuComponent): Component {
function transformSelectMenuComponent(bot: Bot, payload: DiscordSelectMenuComponent | DiscordStringSelectInteractionResponseFromModal): Component {
const props = bot.transformers.desiredProperties.component
const select = {} as Component
if (props.type && payload.type) select.type = payload.type
if (props.id && payload.id) select.id = payload.id
if (props.customId && payload.custom_id) select.customId = payload.custom_id
if (props.placeholder && payload.placeholder) select.placeholder = payload.placeholder
if (props.minValues && payload.min_values) select.minValues = payload.min_values
if (props.maxValues && payload.max_values) select.maxValues = payload.max_values
if (props.defaultValues && payload.default_values)
select.defaultValues = payload.default_values.map((defaultValue) => ({
id: bot.transformers.snowflake(defaultValue.id),
type: defaultValue.type,
}))
if (props.channelTypes && payload.channel_types) select.channelTypes = payload.channel_types
if (props.options && payload.options)
select.options = payload.options.map((option) => ({
label: option.label,
value: option.value,
description: option.description,
emoji: option.emoji
? {
id: option.emoji.id ? bot.transformers.snowflake(option.emoji.id) : undefined,
name: option.emoji.name ?? undefined,
animated: option.emoji.animated,
}
: undefined,
default: option.default,
}))
if (props.disabled && payload.disabled) select.disabled = payload.disabled
// Check if this is the string select response
if ('values' in payload) {
if (props.values && payload.values) select.values = payload.values
} else {
if (props.placeholder && payload.placeholder) select.placeholder = payload.placeholder
if (props.minValues && payload.min_values) select.minValues = payload.min_values
if (props.maxValues && payload.max_values) select.maxValues = payload.max_values
if (props.defaultValues && payload.default_values)
select.defaultValues = payload.default_values.map((defaultValue) => ({
id: bot.transformers.snowflake(defaultValue.id),
type: defaultValue.type,
}))
if (props.channelTypes && payload.channel_types) select.channelTypes = payload.channel_types
if (props.options && payload.options)
select.options = payload.options.map((option) => ({
label: option.label,
value: option.value,
description: option.description,
emoji: option.emoji
? {
id: option.emoji.id ? bot.transformers.snowflake(option.emoji.id) : undefined,
name: option.emoji.name ?? undefined,
animated: option.emoji.animated,
}
: undefined,
default: option.default,
}))
if (props.disabled && payload.disabled) select.disabled = payload.disabled
}
return select
}
@@ -256,3 +273,16 @@ function transformSeparatorComponent(bot: Bot, payload: DiscordSeparatorComponen
return separator
}
function transformLabelComponent(bot: Bot, payload: DiscordLabelComponent): Component {
const props = bot.transformers.desiredProperties.component
const label = {} as Component
if (props.type && payload.type) label.type = payload.type
if (props.id && payload.id) label.id = payload.id
if (props.label && payload.label) label.label = payload.label
if (props.description && payload.description) label.description = payload.description
if (props.component && payload.component) label.component = bot.transformers.component(bot, payload.component)
return label
}

View File

@@ -4,13 +4,17 @@ import {
type DiscordButtonComponent,
type DiscordContainerComponent,
type DiscordFileComponent,
type DiscordLabelComponent,
type DiscordMediaGalleryComponent,
type DiscordMediaGalleryItem,
type DiscordMessageComponent,
type DiscordMessageComponentFromModalInteractionResponse,
type DiscordSectionComponent,
type DiscordSelectMenuComponent,
type DiscordStringSelectInteractionResponseFromModal,
type DiscordTextDisplayComponent,
type DiscordTextInputComponent,
type DiscordTextInputInteractionResponse,
type DiscordThumbnailComponent,
type DiscordUnfurledMediaItem,
MessageComponentTypes,
@@ -19,7 +23,10 @@ import {
import type { Bot } from '../../bot.js'
import type { Component, MediaGalleryItem, UnfurledMediaItem } from '../types.js'
export function transformComponentToDiscordComponent(bot: Bot, payload: Component): DiscordMessageComponent {
export function transformComponentToDiscordComponent(
bot: Bot,
payload: Component,
): DiscordMessageComponent | DiscordMessageComponentFromModalInteractionResponse {
// This switch should include all cases
switch (payload.type) {
case MessageComponentTypes.ActionRow:
@@ -44,6 +51,8 @@ export function transformComponentToDiscordComponent(bot: Bot, payload: Componen
return transformMediaGalleryComponent(bot, payload)
case MessageComponentTypes.Thumbnail:
return transformThumbnailComponent(bot, payload)
case MessageComponentTypes.Label:
return transformLabelComponent(bot, payload)
case MessageComponentTypes.Separator:
case MessageComponentTypes.TextDisplay:
// As of now they are compatible
@@ -111,7 +120,7 @@ function transformButtonComponent(bot: Bot, payload: Component): DiscordButtonCo
}
}
function transformInputTextComponent(_bot: Bot, payload: Component): DiscordTextInputComponent {
function transformInputTextComponent(_bot: Bot, payload: Component): DiscordTextInputComponent | DiscordTextInputInteractionResponse {
// Since Component is a merge of all components, some casts are necessary
return {
type: MessageComponentTypes.TextInput,
@@ -127,7 +136,16 @@ function transformInputTextComponent(_bot: Bot, payload: Component): DiscordText
}
}
function transformSelectMenuComponent(bot: Bot, payload: Component): DiscordSelectMenuComponent {
function transformSelectMenuComponent(bot: Bot, payload: Component): DiscordSelectMenuComponent | DiscordStringSelectInteractionResponseFromModal {
if (payload.values) {
return {
type: MessageComponentTypes.StringSelect,
values: payload.values,
custom_id: payload.customId!,
id: payload.id!,
}
}
return {
type: payload.type as DiscordSelectMenuComponent['type'],
id: payload.id,
@@ -154,6 +172,7 @@ function transformSelectMenuComponent(bot: Bot, payload: Component): DiscordSele
default: option.default,
})),
placeholder: payload.placeholder,
required: payload.required,
}
}
@@ -194,3 +213,13 @@ function transformThumbnailComponent(bot: Bot, payload: Component): DiscordThumb
spoiler: payload.spoiler,
}
}
function transformLabelComponent(bot: Bot, payload: Component): DiscordLabelComponent {
return {
type: MessageComponentTypes.Label,
id: payload.id,
label: payload.label!,
description: payload.description,
component: bot.transformers.reverse.component(bot, payload.component!) as DiscordLabelComponent['component'],
}
}

View File

@@ -572,6 +572,10 @@ export interface Component {
name?: string
/** The size of the file in bytes. This field is ignored and provided by the API as part of the response */
size?: number
/** The component within the label */
component?: Component
/** The text of the selected options */
values?: string[]
}
export interface UnfurledMediaItem {

View File

@@ -1,7 +1,9 @@
/** Types for: https://discord.com/developers/docs/components/reference */
import type { Require } from '../shared.js'
import type { ChannelTypes } from './channel.js'
import type { DiscordEmoji } from './emoji.js'
import type { DiscordInteractionDataResolved } from './interactions.js'
/** https://discord.com/developers/docs/components/reference#component-object-component-types */
export enum MessageComponentTypes {
@@ -35,6 +37,8 @@ export enum MessageComponentTypes {
Separator,
/** Container that visually groups a set of components */
Container = 17,
/** Container associating a label and description with a component */
Label,
}
export type DiscordMessageComponents = DiscordMessageComponent[]
@@ -50,6 +54,26 @@ export type DiscordMessageComponent =
| DiscordSeparatorComponent
| DiscordContainerComponent
| DiscordFileComponent
| DiscordLabelComponent
export type DiscordMessageComponentResponse =
| DiscordTextInputInteractionResponse
| DiscordRoleSelectInteractionResponse
| DiscordUserSelectInteractionResponse
| DiscordStringSelectInteractionResponse
| DiscordChannelSelectInteractionResponse
| DiscordMentionableSelectInteractionResponse
export type DiscordMessageComponentFromModalInteractionResponse =
| DiscordTextInputInteractionResponse
| DiscordStringSelectInteractionResponseFromModal
export type DiscordMessageComponentFromMessageComponentInteractionResponse =
| DiscordRoleSelectInteractionResponse
| DiscordUserSelectInteractionResponse
| DiscordStringSelectInteractionResponseFromMessageComponent
| DiscordChannelSelectInteractionResponse
| DiscordMentionableSelectInteractionResponse
/** https://discord.com/developers/docs/components/reference#anatomy-of-a-component */
export interface DiscordBaseComponent {
@@ -68,6 +92,9 @@ export interface DiscordActionRow extends DiscordBaseComponent {
*
* @remarks
* Up to 5 button components, a single select component or a single text input component
*
* Using a {@link DiscordTextInputComponent} inside the Action Row is deprecated,
* use a {@link DiscordLabelComponent} for modals
*/
components: (DiscordButtonComponent | DiscordSelectMenuComponent | DiscordTextInputComponent)[]
}
@@ -161,9 +188,21 @@ export interface DiscordSelectMenuComponent extends DiscordBaseComponent {
min_values?: number
/** The maximum number of items that can be selected. Default 1. Max 25. */
max_values?: number
/**
* Whether this component is required to be filled
*
* @remarks
* This value is only valid for string select menus in modals
*
* @default true
*/
required?: boolean
/**
* Whether select menu is disabled
*
* @remarks
* This value cannot be set for select menus in modals
*
* @default false
*/
disabled?: boolean
@@ -192,6 +231,34 @@ export interface DiscordSelectOption {
default?: boolean
}
/** https://discord.com/developers/docs/components/reference#string-select-strings-select-interaction-response-structure */
export interface DiscordStringSelectInteractionResponse {
/**
* @remarks
* This is only returned for interaction responses from modals
*/
type?: MessageComponentTypes.StringSelect
/**
* @remarks
* This is only returned for interaction responses from message interactions
*/
component_type?: MessageComponentTypes.StringSelect
/** 32 bit integer used as an optional identifier for component */
id: number
/** The custom id for the string select menu */
custom_id: string
/** The text of the selected options */
values: string[]
}
/** https://discord.com/developers/docs/components/reference#string-select-string-select-interaction-response-structure */
export type DiscordStringSelectInteractionResponseFromModal = Require<Omit<DiscordStringSelectInteractionResponse, 'component_type'>, 'type'>
/** https://discord.com/developers/docs/components/reference#string-select-string-select-interaction-response-structure */
export type DiscordStringSelectInteractionResponseFromMessageComponent = Require<
Omit<DiscordStringSelectInteractionResponse, 'type'>,
'component_type'
>
/** https://discord.com/developers/docs/components/reference#text-input-text-input-structure */
export interface DiscordTextInputComponent extends DiscordBaseComponent {
type: MessageComponentTypes.TextInput
@@ -200,8 +267,15 @@ export interface DiscordTextInputComponent extends DiscordBaseComponent {
custom_id: string
/** The style of the InputText */
style: TextStyles
/** The label of the InputText (max 45 characters) */
label: string
/**
* The label of the InputText.
*
* @remarks
* Maximum 45 characters
*
* @deprecated Use the `label` and `description` from the {@link DiscordLabelComponent}
*/
label?: string
/** The minimum length of the text the user has to provide */
min_length?: number
/** The maximum length of the text the user has to provide */
@@ -222,6 +296,17 @@ export enum TextStyles {
Paragraph = 2,
}
/** https://discord.com/developers/docs/components/reference#text-input-text-input-interaction-response-structure */
export interface DiscordTextInputInteractionResponse {
type: MessageComponentTypes.TextInput
/** 32 bit integer used as an optional identifier for component */
id: number
/** The custom id for the text input */
custom_id: string
/** The user's input text */
value: string
}
/** https://discord.com/developers/docs/components/reference#user-select-select-default-value-structure */
export interface DiscordSelectMenuDefaultValue {
/** ID of a user, role, or channel */
@@ -230,6 +315,58 @@ export interface DiscordSelectMenuDefaultValue {
type: 'user' | 'role' | 'channel'
}
/** https://discord.com/developers/docs/components/reference#user-select-user-select-interaction-response-structure */
export interface DiscordUserSelectInteractionResponse {
component_type: MessageComponentTypes.UserSelect
/** 32 bit integer used as an optional identifier for component */
id: number
/** The custom id for the user select */
custom_id: string
/** Resolved entities from selected options */
resolved: DiscordInteractionDataResolved
/** IDs of the selected users */
values: string[]
}
/** https://discord.com/developers/docs/components/reference#role-select-role-select-interaction-response-structure */
export interface DiscordRoleSelectInteractionResponse {
component_type: MessageComponentTypes.RoleSelect
/** 32 bit integer used as an optional identifier for component */
id: number
/** The custom id for the role select */
custom_id: string
/** Resolved entities from selected options */
resolved: DiscordInteractionDataResolved
/** IDs of the selected roles */
values: string[]
}
/** https://discord.com/developers/docs/components/reference#mentionable-select-mentionable-select-interaction-response-structure */
export interface DiscordMentionableSelectInteractionResponse {
component_type: MessageComponentTypes.MentionableSelect
/** 32 bit integer used as an optional identifier for component */
id: number
/** The custom id for the mentionable select */
custom_id: string
/** Resolved entities from selected options */
resolved: DiscordInteractionDataResolved
/** IDs of the selected mentionables */
values: string[]
}
/** https://discord.com/developers/docs/components/reference#channel-select-channel-select-interaction-response-structure */
export interface DiscordChannelSelectInteractionResponse {
component_type: MessageComponentTypes.ChannelSelect
/** 32 bit integer used as an optional identifier for component */
id: number
/** The custom id for the channel select */
custom_id: string
/** Resolved entities from selected options */
resolved: DiscordInteractionDataResolved
/** IDs of the selected channels */
values: string[]
}
/** https://discord.com/developers/docs/components/reference#section-section-structure */
export interface DiscordSectionComponent extends DiscordBaseComponent {
type: MessageComponentTypes.Section
@@ -326,6 +463,28 @@ export interface DiscordContainerComponent extends DiscordBaseComponent {
spoiler?: boolean
}
/** https://discord.com/developers/docs/components/reference#label-label-structure */
export interface DiscordLabelComponent extends DiscordBaseComponent {
type: MessageComponentTypes.Label
/**
* The label text
*
* @remarks
* Max 45 characters.
*/
label: string
/**
* An optional description text for the label
*
* @remarks
* Max 100 characters.
*/
description?: string
/** The component within the label */
component: DiscordTextInputComponent | DiscordSelectMenuComponent
}
/** https://discord.com/developers/docs/components/reference#unfurled-media-item-structure */
export interface DiscordUnfurledMediaItem {
/** Supports arbitrary urls and attachment://<filename> references */

View File

@@ -6,7 +6,7 @@
import type { DiscordApplicationIntegrationType } from './application.js'
import type { ChannelTypes, DiscordChannel } from './channel.js'
import type { DiscordMessageComponents, MessageComponentTypes } from './components.js'
import type { DiscordMessageComponentFromModalInteractionResponse, MessageComponentTypes } from './components.js'
import type { DiscordEntitlement } from './entitlement.js'
import type { DiscordGuild, DiscordMember, DiscordMemberWithUser } from './guild.js'
import type { DiscordAttachment, DiscordMessage } from './message.js'
@@ -131,7 +131,7 @@ export interface DiscordInteractionData {
// Modal Submit Data
/** The components if its a Modal Submit interaction. */
components?: DiscordMessageComponents
components?: DiscordMessageComponentFromModalInteractionResponse[]
}
/** https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-resolved-data-structure */

View File

@@ -28,6 +28,7 @@ export type MessageComponent =
| SeparatorComponent
| ContainerComponent
| FileComponent
| LabelComponent
/** https://discord.com/developers/docs/components/reference#anatomy-of-a-component */
export interface BaseComponent {
@@ -46,6 +47,9 @@ export interface ActionRow extends BaseComponent {
*
* @remarks
* Up to 5 button components, a single select component or a single text input component
*
* Using a {@link TextInputComponent} inside the Action Row is deprecated,
* use a {@link LabelComponent} for modals
*/
components: (
| ButtonComponent
@@ -106,7 +110,23 @@ export interface StringSelectComponent extends BaseComponent {
minValues?: number
/** The maximum number of items that can be selected. Default 1. Max 25. */
maxValues?: number
/** Whether or not this select is disabled */
/**
* Whether this component is required to be filled
*
* @remarks
* This value is only valid for string select menus in modals
*
* @default true
*/
required?: boolean
/**
* Whether select menu is disabled
*
* @remarks
* This value cannot be set for select menus in modals
*
* @default false
*/
disabled?: boolean
}
@@ -140,8 +160,15 @@ export interface TextInputComponent extends BaseComponent {
customId: string
/** The style of the InputText */
style: TextStyles
/** The label of the InputText. Maximum 45 characters */
label: string
/**
* The label of the InputText.
*
* @remarks
* Maximum 45 characters
*
* @deprecated Use the `label` and `description` from the {@link LabelComponent}
*/
label?: string
/** The minimum length of the text the user has to provide */
minLength?: number
/** The maximum length of the text the user has to provide */
@@ -321,3 +348,25 @@ export interface ContainerComponent extends BaseComponent {
/** Whether the container should be a spoiler (or blurred out). Defaults to `false` */
spoiler?: boolean
}
/** https://discord.com/developers/docs/components/reference#label-label-structure */
export interface LabelComponent extends BaseComponent {
type: MessageComponentTypes.Label
/**
* The label text
*
* @remarks
* Max 45 characters.
*/
label: string
/**
* An optional description text for the label
*
* @remarks
* Max 100 characters.
*/
description?: string
/** The component within the label */
component: TextInputComponent | StringSelectComponent
}

View File

@@ -24,3 +24,5 @@ export type PickPartial<T, K extends keyof T> = { [P in keyof T]?: T[P] | undefi
// Functions are objects for TS, so we need to check for them explicitly
export type RecursivePartial<T> = T extends object ? (T extends (...args: never[]) => unknown ? T : { [K in keyof T]?: RecursivePartial<T[K]> }) : T
export type Require<T, K extends keyof T> = Omit<T, K> & { [P in K]-?: T[P] }