From 5c38883bbc75e3a379b99f8b48b2310be05fb92b Mon Sep 17 00:00:00 2001 From: "LTS (Link)" Date: Thu, 1 May 2025 02:18:48 -0700 Subject: [PATCH] feat(types,bot)!: Add desiredProperties for components (#4179) - feat(bot): Add desiredProperties for components, as well as media gallery items and unfurled media items - fix(types)!: Rename the following types to match the newly updated discord docs: - InputText to TextInput - SelectMenu to StringSelect - SelectMenuUsers to UserSelect - SelectMenuRoles to RoleSelect - SelectMenuMentionable to MentionableSelect - SelectMenuChannels to ChannelSelect - Update some JSDOC comments to match the new discord docs Co-authored-by: Fleny --- .../benchmarks/src/benchmarks/transformers.ts | 8 +- packages/bot/src/desiredProperties.ts | 53 ++++ packages/bot/src/transformers/component.ts | 250 +++++++++++------- .../bot/src/transformers/reverse/component.ts | 14 +- packages/types/src/discord/components.ts | 24 +- packages/types/src/discordeno.ts | 70 +++-- 6 files changed, 265 insertions(+), 154 deletions(-) diff --git a/packages/benchmarks/src/benchmarks/transformers.ts b/packages/benchmarks/src/benchmarks/transformers.ts index 4692cd657..94459d4f7 100644 --- a/packages/benchmarks/src/benchmarks/transformers.ts +++ b/packages/benchmarks/src/benchmarks/transformers.ts @@ -202,7 +202,7 @@ await memoryBenchmark( type: 1, components: [ { - type: MessageComponentTypes.InputText, + type: MessageComponentTypes.TextInput, custom_id: 'discordeno', label: 'discordeno', max_length: 100, @@ -218,7 +218,7 @@ await memoryBenchmark( type: 1, components: [ { - type: MessageComponentTypes.SelectMenu, + type: MessageComponentTypes.StringSelect, custom_id: 'discordeno', max_values: 100, min_values: 100, @@ -585,7 +585,7 @@ await memoryBenchmark( type: 1, components: [ { - type: MessageComponentTypes.InputText, + type: MessageComponentTypes.TextInput, custom_id: 'discordeno', label: 'discordeno', max_length: 100, @@ -601,7 +601,7 @@ await memoryBenchmark( type: 1, components: [ { - type: MessageComponentTypes.SelectMenu, + type: MessageComponentTypes.StringSelect, custom_id: 'discordeno', max_values: 100, min_values: 100, diff --git a/packages/bot/src/desiredProperties.ts b/packages/bot/src/desiredProperties.ts index 199c68d2b..dd4a67df4 100644 --- a/packages/bot/src/desiredProperties.ts +++ b/packages/bot/src/desiredProperties.ts @@ -7,6 +7,7 @@ import type { Attachment, AvatarDecorationData, Channel, + Component, DefaultReactionEmoji, Emoji, Entitlement, @@ -24,6 +25,7 @@ import type { InviteStageInstance, Lobby, LobbyMember, + MediaGalleryItem, Member, Message, MessageCall, @@ -44,6 +46,7 @@ import type { StageInstance, Sticker, Subscription, + UnfurledMediaItem, User, VoiceState, Webhook, @@ -60,6 +63,7 @@ export interface TransformersObjects { attachment: Attachment avatarDecorationData: AvatarDecorationData channel: Channel + component: Component defaultReactionEmoji: DefaultReactionEmoji emoji: Emoji entitlement: Entitlement @@ -75,6 +79,7 @@ export interface TransformersObjects { interactionResource: InteractionResource invite: Invite inviteStageInstance: InviteStageInstance + mediaGalleryItem: MediaGalleryItem member: Member message: Message messageCall: MessageCall @@ -93,6 +98,7 @@ export interface TransformersObjects { sku: Sku stageInstance: StageInstance sticker: Sticker + unfurledMediaItem: UnfurledMediaItem user: User voiceState: VoiceState webhook: Webhook @@ -280,6 +286,39 @@ export function createDesiredPropertiesObject bot.transformers.component(bot, component)), - } + const props = bot.transformers.desiredProperties.component + const actionRow = {} as Component + + if (props.type && payload.type) actionRow.type = payload.type + if (props.id && payload.id) actionRow.id = payload.id + if (props.components && payload.components) actionRow.components = payload.components.map((component) => bot.transformers.component(bot, component)) + + return actionRow } function transformContainerComponent(bot: Bot, payload: DiscordContainerComponent): Component { - return { - type: MessageComponentTypes.Container, - id: payload.id, - accentColor: payload.accent_color ?? undefined, - spoiler: payload.spoiler, - components: payload.components.map((component) => bot.transformers.component(bot, component)), - } + const props = bot.transformers.desiredProperties.component + const container = {} as Component + + if (props.type && payload.type) container.type = payload.type + if (props.id && payload.id) container.id = payload.id + if (props.accentColor && payload.accent_color) container.accentColor = payload.accent_color + if (props.spoiler && payload.spoiler) container.spoiler = payload.spoiler + if (props.components && payload.components) container.components = payload.components.map((component) => bot.transformers.component(bot, component)) + + return container } function transformButtonComponent(bot: Bot, payload: DiscordButtonComponent): Component { - return { - type: MessageComponentTypes.Button, - id: payload.id, - label: payload.label, - customId: payload.custom_id, - style: payload.style, - emoji: payload.emoji - ? { - id: payload.emoji.id ? bot.transformers.snowflake(payload.emoji.id) : undefined, - name: payload.emoji.name, - animated: payload.emoji.animated, - } - : undefined, - url: payload.url, - disabled: payload.disabled, - skuId: payload.sku_id ? bot.transformers.snowflake(payload.sku_id) : undefined, - } + const props = bot.transformers.desiredProperties.component + const button = {} as Component + + if (props.type && payload.type) button.type = payload.type + if (props.id && payload.id) button.id = payload.id + if (props.label && payload.label) button.label = payload.label + if (props.customId && payload.custom_id) button.customId = payload.custom_id + if (props.style && payload.style) button.style = payload.style + if (props.emoji && payload.emoji) button.emoji = bot.transformers.emoji(bot, payload.emoji) + if (props.url && payload.url) button.url = payload.url + if (props.disabled && payload.disabled) button.disabled = payload.disabled + if (props.skuId && payload.sku_id) button.skuId = bot.transformers.snowflake(payload.sku_id) + + return button } -function transformInputTextComponent(_bot: Bot, payload: DiscordTextInputComponent): Component { - return { - type: MessageComponentTypes.InputText, - id: payload.id, - style: payload.style, - required: payload.required, - customId: payload.custom_id, - label: payload.label, - placeholder: payload.placeholder, - minLength: payload.min_length, - maxLength: payload.max_length, - value: payload.value, - } +function transformInputTextComponent(bot: Bot, payload: DiscordTextInputComponent): 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 + + return input } function transformSelectMenuComponent(bot: Bot, payload: DiscordSelectMenuComponent): Component { - return { - type: payload.type, - id: payload.id, - customId: payload.custom_id, - placeholder: payload.placeholder, - minValues: payload.min_values, - maxValues: payload.max_values, - defaultValues: payload.default_values?.map((defaultValue) => ({ + 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, - })), - channelTypes: payload.channel_types, - options: payload.options?.map((option) => ({ + })) + 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, @@ -167,47 +181,79 @@ function transformSelectMenuComponent(bot: Bot, payload: DiscordSelectMenuCompon } : undefined, default: option.default, - })), - disabled: payload.disabled, - } + })) + if (props.disabled && payload.disabled) select.disabled = payload.disabled + + return select } function transformSectionComponent(bot: Bot, payload: DiscordSectionComponent): Component { - return { - type: MessageComponentTypes.Section, - id: payload.id, - components: payload.components.map((component) => bot.transformers.component(bot, component)), - accessory: bot.transformers.component(bot, payload.accessory), - } + const props = bot.transformers.desiredProperties.component + const section = {} as Component + + if (props.type && payload.type) section.type = payload.type + if (props.id && payload.id) section.id = payload.id + if (props.components && payload.components) section.components = payload.components.map((component) => bot.transformers.component(bot, component)) + if (props.accessory && payload.accessory) section.accessory = bot.transformers.component(bot, payload.accessory) + + return section } function transformThumbnailComponent(bot: Bot, payload: DiscordThumbnailComponent): Component { - return { - type: MessageComponentTypes.Thumbnail, - id: payload.id, - media: bot.transformers.unfurledMediaItem(bot, payload.media), - description: payload.description, - spoiler: payload.spoiler, - } + const props = bot.transformers.desiredProperties.component + const thumbnail = {} as Component + + if (props.type && payload.type) thumbnail.type = payload.type + if (props.id && payload.id) thumbnail.id = payload.id + if (props.media && payload.media) thumbnail.media = bot.transformers.unfurledMediaItem(bot, payload.media) + if (props.description && payload.description) thumbnail.description = payload.description + if (props.spoiler && payload.spoiler) thumbnail.spoiler = payload.spoiler + + return thumbnail } function transformMediaGalleryComponent(bot: Bot, payload: DiscordMediaGalleryComponent): Component { - return { - type: MessageComponentTypes.MediaGallery, - id: payload.id, - items: payload.items.map((media) => bot.transformers.mediaGalleryItem(bot, media)), - } + const props = bot.transformers.desiredProperties.component + const mediaGallery = {} as Component + + if (props.type && payload.type) mediaGallery.type = payload.type + if (props.id && payload.id) mediaGallery.id = payload.id + if (props.items && payload.items) mediaGallery.items = payload.items.map((item) => bot.transformers.mediaGalleryItem(bot, item)) + + return mediaGallery } function transformFileComponent(bot: Bot, payload: DiscordFileComponent): Component { - return { - type: MessageComponentTypes.File, - id: payload.id, - file: bot.transformers.unfurledMediaItem(bot, payload.file), - spoiler: payload.spoiler, - } + const props = bot.transformers.desiredProperties.component + const file = {} as Component + + if (props.type && payload.type) file.type = payload.type + if (props.id && payload.id) file.id = payload.id + if (props.file && payload.file) file.file = bot.transformers.unfurledMediaItem(bot, payload.file) + if (props.spoiler && payload.spoiler) file.spoiler = payload.spoiler + + return file } -function keepAsIs(_bot: Bot, payload: DiscordTextDisplayComponent | DiscordSeparatorComponent): Component { - return payload +function transformTextDisplayComponent(bot: Bot, payload: DiscordTextDisplayComponent): Component { + const props = bot.transformers.desiredProperties.component + const textDisplay = {} as Component + + if (props.type && payload.type) textDisplay.type = payload.type + if (props.id && payload.id) textDisplay.id = payload.id + if (props.content && payload.content) textDisplay.content = payload.content + + return textDisplay +} + +function transformSeparatorComponent(bot: Bot, payload: DiscordSeparatorComponent): Component { + const props = bot.transformers.desiredProperties.component + const separator = {} as Component + + if (props.type && payload.type) separator.type = payload.type + if (props.id && payload.id) separator.id = payload.id + if (props.divider && payload.divider) separator.divider = payload.divider + if (props.spacing && payload.spacing) separator.spacing = payload.spacing + + return separator } diff --git a/packages/bot/src/transformers/reverse/component.ts b/packages/bot/src/transformers/reverse/component.ts index 0c6c26b83..a48ce1e21 100644 --- a/packages/bot/src/transformers/reverse/component.ts +++ b/packages/bot/src/transformers/reverse/component.ts @@ -27,13 +27,13 @@ export function transformComponentToDiscordComponent(bot: Bot, payload: Componen return transformButtonComponent(bot, payload) case MessageComponentTypes.Container: return transformContainerComponent(bot, payload) - case MessageComponentTypes.InputText: + case MessageComponentTypes.TextInput: return transformInputTextComponent(bot, payload) - case MessageComponentTypes.SelectMenu: - case MessageComponentTypes.SelectMenuChannels: - case MessageComponentTypes.SelectMenuRoles: - case MessageComponentTypes.SelectMenuUsers: - case MessageComponentTypes.SelectMenuUsersAndRoles: + case MessageComponentTypes.StringSelect: + case MessageComponentTypes.ChannelSelect: + case MessageComponentTypes.RoleSelect: + case MessageComponentTypes.UserSelect: + case MessageComponentTypes.MentionableSelect: return transformSelectMenuComponent(bot, payload) case MessageComponentTypes.Section: return transformSectionComponent(bot, payload) @@ -112,7 +112,7 @@ function transformButtonComponent(bot: Bot, payload: Component): DiscordButtonCo function transformInputTextComponent(_bot: Bot, payload: Component): DiscordTextInputComponent { // Since Component is a merge of all components, some casts are necessary return { - type: MessageComponentTypes.InputText, + type: MessageComponentTypes.TextInput, id: payload.id, style: payload.style as TextStyles, custom_id: payload.customId!, diff --git a/packages/types/src/discord/components.ts b/packages/types/src/discord/components.ts index f3d99d5b0..2aa469bda 100644 --- a/packages/types/src/discord/components.ts +++ b/packages/types/src/discord/components.ts @@ -9,17 +9,17 @@ export enum MessageComponentTypes { /** A button object */ Button, /** A select menu for picking from choices */ - SelectMenu, + StringSelect, /** A text input object */ - InputText, + TextInput, /** Select menu for users */ - SelectMenuUsers, + UserSelect, /** Select menu for roles */ - SelectMenuRoles, + RoleSelect, /** Select menu for users and roles */ - SelectMenuUsersAndRoles, + MentionableSelect, /** Select menu for channels */ - SelectMenuChannels, + ChannelSelect, /** Container to display text alongside an accessory component */ Section, /** Markdown text */ @@ -149,11 +149,11 @@ export enum ButtonStyles { */ export interface DiscordSelectMenuComponent extends DiscordBaseComponent { type: - | MessageComponentTypes.SelectMenu - | MessageComponentTypes.SelectMenuUsers - | MessageComponentTypes.SelectMenuRoles - | MessageComponentTypes.SelectMenuUsersAndRoles - | MessageComponentTypes.SelectMenuChannels + | MessageComponentTypes.StringSelect + | MessageComponentTypes.UserSelect + | MessageComponentTypes.RoleSelect + | MessageComponentTypes.MentionableSelect + | MessageComponentTypes.ChannelSelect /** A custom identifier for this component. Maximum 100 characters. */ custom_id: string @@ -205,7 +205,7 @@ export interface DiscordSelectOption { /** https://discord.com/developers/docs/components/reference#text-input-text-input-structure */ export interface DiscordTextInputComponent extends DiscordBaseComponent { - type: MessageComponentTypes.InputText + type: MessageComponentTypes.TextInput /** The customId of the InputText */ custom_id: string diff --git a/packages/types/src/discordeno.ts b/packages/types/src/discordeno.ts index 69ae10053..8b654526c 100644 --- a/packages/types/src/discordeno.ts +++ b/packages/types/src/discordeno.ts @@ -114,12 +114,12 @@ export type MessageComponents = MessageComponent[] export type MessageComponent = | ActionRow | ButtonComponent - | InputTextComponent - | SelectMenuComponent - | SelectMenuChannelsComponent - | SelectMenuRolesComponent - | SelectMenuUsersComponent - | SelectMenuUsersAndRolesComponent + | TextInputComponent + | StringSelectComponent + | ChannelSelectComponent + | RoleSelectComponent + | UserSelectComponent + | MentionableSelectComponent | SectionComponent | TextDisplayComponent | ThumbnailComponent @@ -136,20 +136,28 @@ export interface BaseComponent { id?: number } -/** https://discord.com/developers/docs/interactions/message-components#actionrow */ +/** https://discord.com/developers/docs/components/reference#action-row-action-row-structure */ export interface ActionRow extends BaseComponent { type: MessageComponentTypes.ActionRow - /** The components in this row */ - components: - | [Exclude] - | [ButtonComponent, ButtonComponent] - | [ButtonComponent, ButtonComponent, ButtonComponent] - | [ButtonComponent, ButtonComponent, ButtonComponent, ButtonComponent] - | [ButtonComponent, ButtonComponent, ButtonComponent, ButtonComponent, ButtonComponent] + /** + * The components in this row + * + * @remarks + * Up to 5 button components, a single select component or a single text input component + */ + components: ( + | ButtonComponent + | StringSelectComponent + | UserSelectComponent + | RoleSelectComponent + | MentionableSelectComponent + | ChannelSelectComponent + | TextInputComponent + )[] } -/** https://discord.com/developers/docs/interactions/message-components#button-object-button-structure */ +/** https://discord.com/developers/docs/components/reference#button-button-structure */ export interface ButtonComponent extends BaseComponent { type: MessageComponentTypes.Button @@ -176,9 +184,9 @@ export interface ButtonComponent extends BaseComponent { disabled?: boolean } -/** https://discord.com/developers/docs/interactions/message-components#select-menu-object-select-menu-structure */ -export interface SelectMenuComponent extends BaseComponent { - type: MessageComponentTypes.SelectMenu +/** https://discord.com/developers/docs/components/reference#string-select-string-select-structure */ +export interface StringSelectComponent extends BaseComponent { + type: MessageComponentTypes.StringSelect /** A custom identifier for this component. Maximum 100 characters. */ customId: string @@ -194,8 +202,9 @@ export interface SelectMenuComponent extends BaseComponent { disabled?: boolean } -export interface SelectMenuUsersComponent extends BaseComponent { - type: MessageComponentTypes.SelectMenuUsers +/** https://discord.com/developers/docs/components/reference#user-select-user-select-structure */ +export interface UserSelectComponent extends BaseComponent { + type: MessageComponentTypes.UserSelect /** A custom identifier for this component. Maximum 100 characters. */ customId: string @@ -214,8 +223,9 @@ export interface SelectMenuUsersComponent extends BaseComponent { disabled?: boolean } -export interface SelectMenuRolesComponent extends BaseComponent { - type: MessageComponentTypes.SelectMenuRoles +/** https://discord.com/developers/docs/components/reference#role-select-role-select-structure */ +export interface RoleSelectComponent extends BaseComponent { + type: MessageComponentTypes.RoleSelect /** A custom identifier for this component. Maximum 100 characters. */ customId: string @@ -234,8 +244,9 @@ export interface SelectMenuRolesComponent extends BaseComponent { disabled?: boolean } -export interface SelectMenuUsersAndRolesComponent extends BaseComponent { - type: MessageComponentTypes.SelectMenuUsersAndRoles +/** https://discord.com/developers/docs/components/reference#mentionable-select-mentionable-select-structure */ +export interface MentionableSelectComponent extends BaseComponent { + type: MessageComponentTypes.MentionableSelect /** A custom identifier for this component. Maximum 100 characters. */ customId: string @@ -254,8 +265,9 @@ export interface SelectMenuUsersAndRolesComponent extends BaseComponent { disabled?: boolean } -export interface SelectMenuChannelsComponent extends BaseComponent { - type: MessageComponentTypes.SelectMenuChannels +/** https://discord.com/developers/docs/components/reference#channel-select-channel-select-structure */ +export interface ChannelSelectComponent extends BaseComponent { + type: MessageComponentTypes.ChannelSelect /** A custom identifier for this component. Maximum 100 characters. */ customId: string @@ -303,9 +315,9 @@ export interface SelectMenuDefaultValue { type: 'user' | 'role' | 'channel' } -/** https://discord.com/developers/docs/interactions/message-components#text-inputs-text-input-structure */ -export interface InputTextComponent extends BaseComponent { - type: MessageComponentTypes.InputText +/** https://discord.com/developers/docs/components/reference#text-input-text-input-structure */ +export interface TextInputComponent extends BaseComponent { + type: MessageComponentTypes.TextInput /** The style of the InputText */ style: TextStyles