From 44e1b0d0ec2655d477d790da8e0a2761f67a1571 Mon Sep 17 00:00:00 2001 From: topi314 Date: Mon, 24 Feb 2025 02:09:21 +0100 Subject: [PATCH 1/5] wip types for components v2 --- _examples/componentsv2/example.go | 60 +++++ discord/component.go | 409 +++++++++++++++++++++++------- discord/interaction_component.go | 12 +- discord/message.go | 54 ++-- 4 files changed, 420 insertions(+), 115 deletions(-) create mode 100644 _examples/componentsv2/example.go diff --git a/_examples/componentsv2/example.go b/_examples/componentsv2/example.go new file mode 100644 index 000000000..9c918892b --- /dev/null +++ b/_examples/componentsv2/example.go @@ -0,0 +1,60 @@ +package main + +import ( + "context" + "log/slog" + "os" + "os/signal" + "syscall" + + "github.com/disgoorg/disgo" + "github.com/disgoorg/disgo/bot" + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/disgo/events" + "github.com/disgoorg/disgo/gateway" +) + +var ( + token = os.Getenv("disgo_token") +) + +func main() { + slog.Info("starting example...") + slog.Info("disgo version", slog.String("version", disgo.Version)) + + client, err := disgo.New(token, + bot.WithGatewayConfigOpts(gateway.WithIntents(gateway.IntentGuilds, gateway.IntentGuildMessages, gateway.IntentDirectMessages)), + bot.WithEventListenerFunc(func(event *events.MessageCreate) { + if event.Message.Author.Bot || event.Message.Author.System { + return + } + if event.Message.Content == "test" { + _, _ = event.Client().Rest().CreateMessage(event.ChannelID, discord.NewMessageCreateBuilder(). + AddActionRow(discord.NewDangerButton("danger", "danger")). + SetMessageReferenceByID(event.Message.ID). + Build(), + ) + } + }), + bot.WithEventListenerFunc(func(event *events.ComponentInteractionCreate) { + if event.ButtonInteractionData().CustomID() == "danger" { + _ = event.CreateMessage(discord.NewMessageCreateBuilder().SetEphemeral(true).SetContent("Ey that was danger").Build()) + } + }), + ) + if err != nil { + slog.Error("error while building bot", slog.Any("err", err)) + return + } + defer client.Close(context.TODO()) + + if err = client.OpenGateway(context.TODO()); err != nil { + slog.Error("error while connecting to gateway", slog.Any("err", err)) + return + } + + slog.Info("example is now running. Press CTRL-C to exit.") + s := make(chan os.Signal, 1) + signal.Notify(s, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) + <-s +} diff --git a/discord/component.go b/discord/component.go index f43611e74..8fe8290c8 100644 --- a/discord/component.go +++ b/discord/component.go @@ -20,23 +20,29 @@ const ( ComponentTypeRoleSelectMenu ComponentTypeMentionableSelectMenu ComponentTypeChannelSelectMenu + ComponentTypeSection + ComponentTypeTextDisplay + ComponentTypeThumbnail + ComponentTypeMediaGallery + ComponentTypeFile + ComponentTypeSeparator + ComponentTypeContainer ) type Component interface { json.Marshaler Type() ComponentType - component() } -type ContainerComponent interface { +type LayoutComponent interface { Component - Components() []InteractiveComponent + Components() []Component containerComponent() } type InteractiveComponent interface { Component - ID() string + ComponentCustomID() string interactiveComponent() } @@ -60,42 +66,77 @@ func (u *UnmarshalComponent) UnmarshalJSON(data []byte) error { switch cType.Type { case ComponentTypeActionRow: - v := ActionRowComponent{} + var v ActionRowComponent err = json.Unmarshal(data, &v) component = v case ComponentTypeButton: - v := ButtonComponent{} + var v ButtonComponent err = json.Unmarshal(data, &v) component = v case ComponentTypeStringSelectMenu: - v := StringSelectMenuComponent{} + var v StringSelectMenuComponent err = json.Unmarshal(data, &v) component = v case ComponentTypeTextInput: - v := TextInputComponent{} + var v TextInputComponent err = json.Unmarshal(data, &v) component = v case ComponentTypeUserSelectMenu: - v := UserSelectMenuComponent{} + var v UserSelectMenuComponent err = json.Unmarshal(data, &v) component = v case ComponentTypeRoleSelectMenu: - v := RoleSelectMenuComponent{} + var v RoleSelectMenuComponent err = json.Unmarshal(data, &v) component = v case ComponentTypeMentionableSelectMenu: - v := MentionableSelectMenuComponent{} + var v MentionableSelectMenuComponent err = json.Unmarshal(data, &v) component = v case ComponentTypeChannelSelectMenu: - v := ChannelSelectMenuComponent{} + var v ChannelSelectMenuComponent + err = json.Unmarshal(data, &v) + component = v + + case ComponentTypeSection: + var v SectionComponent + err = json.Unmarshal(data, &v) + component = v + + case ComponentTypeTextDisplay: + var v TextDisplayComponent + err = json.Unmarshal(data, &v) + component = v + + case ComponentTypeThumbnail: + var v ThumbnailComponent + err = json.Unmarshal(data, &v) + component = v + + case ComponentTypeMediaGallery: + var v MediaGalleryComponent + err = json.Unmarshal(data, &v) + component = v + + case ComponentTypeFile: + var v FileComponent + err = json.Unmarshal(data, &v) + component = v + + case ComponentTypeSeparator: + var v SeparatorComponent + err = json.Unmarshal(data, &v) + component = v + + case ComponentTypeContainer: + var v ContainerComponent err = json.Unmarshal(data, &v) component = v @@ -117,61 +158,40 @@ type ComponentEmoji struct { } var ( - _ Component = (*ActionRowComponent)(nil) - _ ContainerComponent = (*ActionRowComponent)(nil) + _ Component = (*ActionRowComponent)(nil) ) -func NewActionRow(components ...InteractiveComponent) ActionRowComponent { - return components +func NewActionRow(components ...Component) ActionRowComponent { + return ActionRowComponent{ + Components: components, + } } -type ActionRowComponent []InteractiveComponent +type ActionRowComponent struct { + ID int `json:"id,omitempty"` + Components []Component `json:"components"` +} func (c ActionRowComponent) MarshalJSON() ([]byte, error) { + type actionRowComponent ActionRowComponent return json.Marshal(struct { - Type ComponentType `json:"type"` - Components []InteractiveComponent `json:"components"` + Type ComponentType `json:"type"` + actionRowComponent }{ - Type: c.Type(), - Components: c, + Type: c.Type(), + actionRowComponent: actionRowComponent(c), }) } -func (c *ActionRowComponent) UnmarshalJSON(data []byte) error { - var actionRow struct { - Components []UnmarshalComponent `json:"components"` - } - - if err := json.Unmarshal(data, &actionRow); err != nil { - return err - } - - if len(actionRow.Components) > 0 { - *c = make([]InteractiveComponent, len(actionRow.Components)) - for i, component := range actionRow.Components { - (*c)[i] = component.Component.(InteractiveComponent) - } - } - - return nil -} - func (ActionRowComponent) Type() ComponentType { return ComponentTypeActionRow } -func (ActionRowComponent) component() {} -func (ActionRowComponent) containerComponent() {} - -func (c ActionRowComponent) Components() []InteractiveComponent { - return c -} - // Buttons returns all ButtonComponent(s) in the ActionRowComponent func (c ActionRowComponent) Buttons() []ButtonComponent { var buttons []ButtonComponent - for i := range c { - if button, ok := c[i].(ButtonComponent); ok { + for i := range c.Components { + if button, ok := c.Components[i].(ButtonComponent); ok { buttons = append(buttons, button) } } @@ -181,8 +201,8 @@ func (c ActionRowComponent) Buttons() []ButtonComponent { // SelectMenus returns all SelectMenuComponent(s) in the ActionRowComponent func (c ActionRowComponent) SelectMenus() []SelectMenuComponent { var selectMenus []SelectMenuComponent - for i := range c { - if selectMenu, ok := c[i].(SelectMenuComponent); ok { + for i := range c.Components { + if selectMenu, ok := c.Components[i].(SelectMenuComponent); ok { selectMenus = append(selectMenus, selectMenu) } } @@ -192,8 +212,8 @@ func (c ActionRowComponent) SelectMenus() []SelectMenuComponent { // TextInputs returns all TextInputComponent(s) in the ActionRowComponent func (c ActionRowComponent) TextInputs() []TextInputComponent { var textInputs []TextInputComponent - for i := range c { - if textInput, ok := c[i].(TextInputComponent); ok { + for i := range c.Components { + if textInput, ok := c.Components[i].(TextInputComponent); ok { textInputs = append(textInputs, textInput) } } @@ -201,25 +221,27 @@ func (c ActionRowComponent) TextInputs() []TextInputComponent { } // UpdateComponent returns a new ActionRowComponent with the Component which has the customID replaced -func (c ActionRowComponent) UpdateComponent(customID string, component InteractiveComponent) ActionRowComponent { - for i, cc := range c { - if cc.ID() == customID { - c[i] = component - return c - } - } +// TODO: fix this +func (c ActionRowComponent) UpdateComponent(customID string, component Component) ActionRowComponent { + //for i, cc := range c.Components { + // if cc.ID() == customID { + // c[i] = component + // return c + // } + //} return c } // AddComponents returns a new ActionRowComponent with the provided Component(s) added -func (c ActionRowComponent) AddComponents(components ...InteractiveComponent) ActionRowComponent { - return append(c, components...) +func (c ActionRowComponent) AddComponents(components ...Component) ActionRowComponent { + c.Components = append(c.Components, components...) + return c } // RemoveComponent returns a new ActionRowComponent with the provided Component at the index removed func (c ActionRowComponent) RemoveComponent(index int) ActionRowComponent { - if len(c) > index { - return append(c[:index], c[index+1:]...) + if len(c.Components) > index { + c.Components = append(c.Components[:index], c.Components[index+1:]...) } return c } @@ -302,11 +324,11 @@ func NewPremiumButton(skuID snowflake.ID) ButtonComponent { } var ( - _ Component = (*ButtonComponent)(nil) - _ InteractiveComponent = (*ButtonComponent)(nil) + _ Component = (*ButtonComponent)(nil) ) type ButtonComponent struct { + ID int `json:"id,omitempty"` Style ButtonStyle `json:"style"` Label string `json:"label,omitempty"` Emoji *ComponentEmoji `json:"emoji,omitempty"` @@ -331,18 +353,6 @@ func (ButtonComponent) Type() ComponentType { return ComponentTypeButton } -func (c ButtonComponent) ID() string { - return c.CustomID -} - -func (c ButtonComponent) SetID(id string) InteractiveComponent { - c.CustomID = id - return c -} - -func (ButtonComponent) component() {} -func (ButtonComponent) interactiveComponent() {} - // WithStyle returns a new ButtonComponent with the provided style func (c ButtonComponent) WithStyle(style ButtonStyle) ButtonComponent { c.Style = style @@ -398,8 +408,7 @@ func (c ButtonComponent) WithDisabled(disabled bool) ButtonComponent { } var ( - _ Component = (*TextInputComponent)(nil) - _ InteractiveComponent = (*TextInputComponent)(nil) + _ Component = (*TextInputComponent)(nil) ) func NewTextInput(customID string, style TextInputStyle, label string) TextInputComponent { @@ -419,6 +428,7 @@ func NewParagraphTextInput(customID string, label string) TextInputComponent { } type TextInputComponent struct { + ID int `json:"id,omitempty"` CustomID string `json:"custom_id"` Style TextInputStyle `json:"style"` Label string `json:"label"` @@ -444,13 +454,6 @@ func (TextInputComponent) Type() ComponentType { return ComponentTypeTextInput } -func (c TextInputComponent) ID() string { - return c.CustomID -} - -func (TextInputComponent) component() {} -func (TextInputComponent) interactiveComponent() {} - // WithCustomID returns a new SelectMenuComponent with the provided customID func (c TextInputComponent) WithCustomID(customID string) TextInputComponent { c.CustomID = customID @@ -499,3 +502,235 @@ const ( TextInputStyleShort TextInputStyle = iota + 1 TextInputStyleParagraph ) + +type UnfurledMediaItem struct { + // URL supports arbitrary urls and attachment:// references + URL string `json:"url"` +} + +var ( + _ Component = (*SectionComponent)(nil) +) + +type SectionComponent struct { + Components []Component `json:"components"` + Accessory Component `json:"accessory"` +} + +func (c SectionComponent) MarshalJSON() ([]byte, error) { + type sectionComponent SectionComponent + return json.Marshal(struct { + Type ComponentType `json:"type"` + sectionComponent + }{ + Type: c.Type(), + sectionComponent: sectionComponent(c), + }) +} + +func (SectionComponent) Type() ComponentType { + return ComponentTypeSection +} + +var ( + _ Component = (*TextDisplayComponent)(nil) +) + +type TextDisplayComponent struct { + Content string `json:"content"` +} + +func (c TextDisplayComponent) MarshalJSON() ([]byte, error) { + type textDisplayComponent TextDisplayComponent + return json.Marshal(struct { + Type ComponentType `json:"type"` + textDisplayComponent + }{ + Type: c.Type(), + textDisplayComponent: textDisplayComponent(c), + }) +} + +func (TextDisplayComponent) Type() ComponentType { + return ComponentTypeTextDisplay +} + +var ( + _ Component = (*ThumbnailComponent)(nil) +) + +type ThumbnailComponent struct { + Media UnfurledMediaItem `json:"media"` + Description string `json:"description,omitempty"` + Spoiler bool `json:"spoiler,omitempty"` +} + +func (c ThumbnailComponent) MarshalJSON() ([]byte, error) { + type thumbnailComponent ThumbnailComponent + return json.Marshal(struct { + Type ComponentType `json:"type"` + thumbnailComponent + }{ + Type: c.Type(), + thumbnailComponent: thumbnailComponent(c), + }) +} + +func (ThumbnailComponent) Type() ComponentType { + return ComponentTypeThumbnail +} + +type MediaGalleryItem struct { + Media UnfurledMediaItem `json:"media"` + Description string `json:"description,omitempty"` + Spoiler bool `json:"spoiler,omitempty"` +} + +var ( + _ Component = (*MediaGalleryComponent)(nil) +) + +type MediaGalleryComponent struct { + Items []MediaGalleryItem `json:"items"` +} + +func (c MediaGalleryComponent) MarshalJSON() ([]byte, error) { + type mediaGalleryComponent MediaGalleryComponent + return json.Marshal(struct { + Type ComponentType `json:"type"` + mediaGalleryComponent + }{ + Type: c.Type(), + mediaGalleryComponent: mediaGalleryComponent(c), + }) +} + +func (MediaGalleryComponent) Type() ComponentType { + return ComponentTypeMediaGallery +} + +type SeparatorSpacingSize int + +const ( + SeparatorSpacingSizeNone SeparatorSpacingSize = iota + SeparatorSpacingSizeSmall + SeparatorSpacingSizeLarge +) + +var ( + _ Component = (*SeparatorComponent)(nil) +) + +type SeparatorComponent struct { + Divider bool `json:"divider,omitempty"` + Spacing SeparatorSpacingSize `json:"spacing,omitempty"` +} + +func (c SeparatorComponent) MarshalJSON() ([]byte, error) { + type separatorComponent SeparatorComponent + return json.Marshal(struct { + Type ComponentType `json:"type"` + separatorComponent + }{ + Type: c.Type(), + separatorComponent: separatorComponent(c), + }) +} + +func (SeparatorComponent) Type() ComponentType { + return ComponentTypeSeparator +} + +var ( + _ Component = (*FileComponent)(nil) +) + +type FileComponent struct { + // File only supports attachment:// references + File UnfurledMediaItem `json:"file"` + Spoiler bool `json:"spoiler,omitempty"` +} + +func (c FileComponent) MarshalJSON() ([]byte, error) { + type fileComponent FileComponent + return json.Marshal(struct { + Type ComponentType `json:"type"` + fileComponent + }{ + Type: c.Type(), + fileComponent: fileComponent(c), + }) +} + +func (FileComponent) Type() ComponentType { + return ComponentTypeFile +} + +var ( + _ Component = (*ContainerComponent)(nil) +) + +type ContainerComponent struct { + AccentColor *int `json:"accent_color,omitempty"` + Spoiler bool `json:"spoiler,omitempty"` + Components []Component `json:"components"` +} + +func (c ContainerComponent) MarshalJSON() ([]byte, error) { + type containerComponent ContainerComponent + return json.Marshal(struct { + Type ComponentType `json:"type"` + containerComponent + }{ + Type: c.Type(), + containerComponent: containerComponent(c), + }) +} + +func (ContainerComponent) Type() ComponentType { + return ComponentTypeContainer +} + +var ( + _ Component = (*UnknownComponent)(nil) +) + +type UnknownComponent struct { + type_ ComponentType + id int + Data json.RawMessage +} + +func (c UnknownComponent) MarshalJSON() ([]byte, error) { + data, err := json.Marshal(struct { + Type ComponentType `json:"type"` + ID int `json:"id,omitempty"` + }{ + Type: c.type_, + ID: c.id, + }) + if err != nil { + return nil, err + } + + return json.Merge(c.Data, data) +} + +func (c *UnknownComponent) UnmarshalJSON(data []byte) error { + var unknownComponent struct { + Type ComponentType `json:"type"` + ID int `json:"id,omitempty"` + } + if err := json.Unmarshal(data, &unknownComponent); err != nil { + return err + } + + c.type_ = unknownComponent.Type + c.id = unknownComponent.ID + c.Data = data + return nil +} + +func (c UnknownComponent) Type() ComponentType { + return c.type_ +} diff --git a/discord/interaction_component.go b/discord/interaction_component.go index 8ab312237..803fcfe70 100644 --- a/discord/interaction_component.go +++ b/discord/interaction_component.go @@ -40,32 +40,32 @@ func (i *ComponentInteraction) UnmarshalJSON(data []byte) error { ) switch cType.Type { case ComponentTypeButton: - v := ButtonInteractionData{} + var v ButtonInteractionData err = json.Unmarshal(interaction.Data, &v) interactionData = v case ComponentTypeStringSelectMenu: - v := StringSelectMenuInteractionData{} + var v StringSelectMenuInteractionData err = json.Unmarshal(interaction.Data, &v) interactionData = v case ComponentTypeUserSelectMenu: - v := UserSelectMenuInteractionData{} + var v UserSelectMenuInteractionData err = json.Unmarshal(interaction.Data, &v) interactionData = v case ComponentTypeRoleSelectMenu: - v := RoleSelectMenuInteractionData{} + var v RoleSelectMenuInteractionData err = json.Unmarshal(interaction.Data, &v) interactionData = v case ComponentTypeMentionableSelectMenu: - v := MentionableSelectMenuInteractionData{} + var v MentionableSelectMenuInteractionData err = json.Unmarshal(interaction.Data, &v) interactionData = v case ComponentTypeChannelSelectMenu: - v := ChannelSelectMenuInteractionData{} + var v ChannelSelectMenuInteractionData err = json.Unmarshal(interaction.Data, &v) interactionData = v diff --git a/discord/message.go b/discord/message.go index 9435d827e..e0642660c 100644 --- a/discord/message.go +++ b/discord/message.go @@ -95,13 +95,22 @@ func MessageURL(guildID snowflake.ID, channelID snowflake.ID, messageID snowflak // Message is a struct for messages sent in discord text-based channels type Message struct { - ID snowflake.ID `json:"id"` - GuildID *snowflake.ID `json:"guild_id"` - Reactions []MessageReaction `json:"reactions"` - Attachments []Attachment `json:"attachments"` - TTS bool `json:"tts"` - Embeds []Embed `json:"embeds,omitempty"` - Components []ContainerComponent `json:"components,omitempty"` + ID snowflake.ID `json:"id"` + GuildID *snowflake.ID `json:"guild_id"` + Reactions []MessageReaction `json:"reactions"` + Attachments []Attachment `json:"attachments"` + TTS bool `json:"tts"` + Embeds []Embed `json:"embeds,omitempty"` + // Components can be any of: + // ActionRowComponent, + // SectionComponent, + // TextDisplayComponent, + // MediaGalleryComponent, + // SeparatorComponent, + // FileComponent, + // ContainerComponent, + // UnknownComponent + Components []Component `json:"components,omitempty"` CreatedAt time.Time `json:"timestamp"` Mentions []User `json:"mentions"` MentionEveryone bool `json:"mention_everyone"` @@ -439,18 +448,18 @@ type MessageSnapshot struct { } type PartialMessage struct { - Type MessageType `json:"type"` - Content string `json:"content,omitempty"` - Embeds []Embed `json:"embeds,omitempty"` - Attachments []Attachment `json:"attachments"` - CreatedAt time.Time `json:"timestamp"` - EditedTimestamp *time.Time `json:"edited_timestamp"` - Flags MessageFlags `json:"flags"` - Mentions []User `json:"mentions"` - MentionRoles []snowflake.ID `json:"mention_roles"` - Stickers []Sticker `json:"stickers"` - StickerItems []MessageSticker `json:"sticker_items,omitempty"` - Components []ContainerComponent `json:"components,omitempty"` + Type MessageType `json:"type"` + Content string `json:"content,omitempty"` + Embeds []Embed `json:"embeds,omitempty"` + Attachments []Attachment `json:"attachments"` + CreatedAt time.Time `json:"timestamp"` + EditedTimestamp *time.Time `json:"edited_timestamp"` + Flags MessageFlags `json:"flags"` + Mentions []User `json:"mentions"` + MentionRoles []snowflake.ID `json:"mention_roles"` + Stickers []Sticker `json:"stickers"` + StickerItems []MessageSticker `json:"sticker_items,omitempty"` + Components []Component `json:"components,omitempty"` } func (m *PartialMessage) UnmarshalJSON(data []byte) error { @@ -505,6 +514,7 @@ const ( MessageFlagSuppressNotifications MessageFlagIsVoiceMessage MessageFlagHasSnapshot + MessageFlagIsComponentsV2 MessageFlagsNone MessageFlags = 0 ) @@ -560,10 +570,10 @@ type MessageCall struct { EndedTimestamp *time.Time `json:"ended_timestamp"` } -func unmarshalComponents(components []UnmarshalComponent) []ContainerComponent { - containerComponents := make([]ContainerComponent, len(components)) +func unmarshalComponents(components []UnmarshalComponent) []Component { + containerComponents := make([]Component, len(components)) for i := range components { - containerComponents[i] = components[i].Component.(ContainerComponent) + containerComponents[i] = components[i].Component } return containerComponents } From c8682548514ea412c16d2e4f4e1048d3bfebce07 Mon Sep 17 00:00:00 2001 From: topi314 Date: Fri, 28 Feb 2025 21:28:34 +0100 Subject: [PATCH 2/5] Refactor component interfaces to include GetID and GetCustomID methods --- discord/component.go | 178 +++++++++++++++++++++++++++++++++++++---- discord/message.go | 31 +++---- discord/select_menu.go | 48 ++++++++--- 3 files changed, 210 insertions(+), 47 deletions(-) diff --git a/discord/component.go b/discord/component.go index 8fe8290c8..fc05ce8cd 100644 --- a/discord/component.go +++ b/discord/component.go @@ -32,18 +32,32 @@ const ( type Component interface { json.Marshaler Type() ComponentType + GetID() int + component() } +type InteractiveComponent interface { + Component + GetCustomID() string + interactiveComponent() +} + +// LayoutComponent is an interface for all components that can be present as a top level component in a [Message]. +// [ActionRowComponent] +// [SectionComponent] +// [TextDisplayComponent] +// [MediaGalleryComponent] +// [FileComponent] +// [SeparatorComponent] +// [ContainerComponent] type LayoutComponent interface { Component - Components() []Component - containerComponent() + layoutComponent() } -type InteractiveComponent interface { +type MessageComponent interface { Component - ComponentCustomID() string - interactiveComponent() + messageComponent() } type UnmarshalComponent struct { @@ -187,6 +201,17 @@ func (ActionRowComponent) Type() ComponentType { return ComponentTypeActionRow } +func (c ActionRowComponent) GetID() int { + return c.ID +} + +func (c ActionRowComponent) GetComponents() []Component { + return c.Components +} + +func (ActionRowComponent) component() {} +func (ActionRowComponent) layoutComponent() {} + // Buttons returns all ButtonComponent(s) in the ActionRowComponent func (c ActionRowComponent) Buttons() []ButtonComponent { var buttons []ButtonComponent @@ -353,6 +378,17 @@ func (ButtonComponent) Type() ComponentType { return ComponentTypeButton } +func (c ButtonComponent) GetID() int { + return c.ID +} + +func (c ButtonComponent) GetCustomID() string { + return c.CustomID +} + +func (ButtonComponent) component() {} +func (ButtonComponent) interactiveComponent() {} + // WithStyle returns a new ButtonComponent with the provided style func (c ButtonComponent) WithStyle(style ButtonStyle) ButtonComponent { c.Style = style @@ -454,6 +490,17 @@ func (TextInputComponent) Type() ComponentType { return ComponentTypeTextInput } +func (c TextInputComponent) GetID() int { + return c.ID +} + +func (c TextInputComponent) GetCustomID() string { + return c.CustomID +} + +func (TextInputComponent) component() {} +func (TextInputComponent) interactiveComponent() {} + // WithCustomID returns a new SelectMenuComponent with the provided customID func (c TextInputComponent) WithCustomID(customID string) TextInputComponent { c.CustomID = customID @@ -509,10 +556,12 @@ type UnfurledMediaItem struct { } var ( - _ Component = (*SectionComponent)(nil) + _ Component = (*SectionComponent)(nil) + _ LayoutComponent = (*SectionComponent)(nil) ) type SectionComponent struct { + ID int `json:"id,omitempty"` Components []Component `json:"components"` Accessory Component `json:"accessory"` } @@ -532,11 +581,23 @@ func (SectionComponent) Type() ComponentType { return ComponentTypeSection } +func (c SectionComponent) GetID() int { + return c.ID +} + +func (c SectionComponent) GetComponents() []Component { + return c.Components +} + +func (SectionComponent) component() {} +func (SectionComponent) layoutComponent() {} + var ( _ Component = (*TextDisplayComponent)(nil) ) type TextDisplayComponent struct { + ID int `json:"id,omitempty"` Content string `json:"content"` } @@ -555,11 +616,18 @@ func (TextDisplayComponent) Type() ComponentType { return ComponentTypeTextDisplay } +func (c TextDisplayComponent) GetID() int { + return c.ID +} + +func (TextDisplayComponent) component() {} + var ( _ Component = (*ThumbnailComponent)(nil) ) type ThumbnailComponent struct { + ID int `json:"id,omitempty"` Media UnfurledMediaItem `json:"media"` Description string `json:"description,omitempty"` Spoiler bool `json:"spoiler,omitempty"` @@ -580,6 +648,12 @@ func (ThumbnailComponent) Type() ComponentType { return ComponentTypeThumbnail } +func (c ThumbnailComponent) GetID() int { + return c.ID +} + +func (ThumbnailComponent) component() {} + type MediaGalleryItem struct { Media UnfurledMediaItem `json:"media"` Description string `json:"description,omitempty"` @@ -591,6 +665,7 @@ var ( ) type MediaGalleryComponent struct { + ID int `json:"id,omitempty"` Items []MediaGalleryItem `json:"items"` } @@ -609,6 +684,12 @@ func (MediaGalleryComponent) Type() ComponentType { return ComponentTypeMediaGallery } +func (c MediaGalleryComponent) GetID() int { + return c.ID +} + +func (MediaGalleryComponent) component() {} + type SeparatorSpacingSize int const ( @@ -622,6 +703,7 @@ var ( ) type SeparatorComponent struct { + ID int `json:"id,omitempty"` Divider bool `json:"divider,omitempty"` Spacing SeparatorSpacingSize `json:"spacing,omitempty"` } @@ -641,11 +723,18 @@ func (SeparatorComponent) Type() ComponentType { return ComponentTypeSeparator } +func (c SeparatorComponent) GetID() int { + return c.ID +} + +func (SeparatorComponent) component() {} + var ( _ Component = (*FileComponent)(nil) ) type FileComponent struct { + ID int `json:"id,omitempty"` // File only supports attachment:// references File UnfurledMediaItem `json:"file"` Spoiler bool `json:"spoiler,omitempty"` @@ -666,11 +755,19 @@ func (FileComponent) Type() ComponentType { return ComponentTypeFile } +func (c FileComponent) GetID() int { + return c.ID +} + +func (FileComponent) component() {} + var ( - _ Component = (*ContainerComponent)(nil) + _ Component = (*ContainerComponent)(nil) + _ LayoutComponent = (*ContainerComponent)(nil) ) type ContainerComponent struct { + ID int `json:"id,omitempty"` AccentColor *int `json:"accent_color,omitempty"` Spoiler bool `json:"spoiler,omitempty"` Components []Component `json:"components"` @@ -691,14 +788,28 @@ func (ContainerComponent) Type() ComponentType { return ComponentTypeContainer } +func (c ContainerComponent) GetID() int { + return c.ID +} + +func (c ContainerComponent) GetComponents() []Component { + return c.Components +} + +func (ContainerComponent) component() {} +func (ContainerComponent) layoutComponent() {} + var ( - _ Component = (*UnknownComponent)(nil) + _ Component = (*UnknownComponent)(nil) + _ InteractiveComponent = (*UnknownComponent)(nil) + _ LayoutComponent = (*UnknownComponent)(nil) + _ SelectMenuComponent = (*UnknownComponent)(nil) ) type UnknownComponent struct { - type_ ComponentType - id int - Data json.RawMessage + ComponentType ComponentType + ID int + Data json.RawMessage } func (c UnknownComponent) MarshalJSON() ([]byte, error) { @@ -706,8 +817,8 @@ func (c UnknownComponent) MarshalJSON() ([]byte, error) { Type ComponentType `json:"type"` ID int `json:"id,omitempty"` }{ - Type: c.type_, - ID: c.id, + Type: c.ComponentType, + ID: c.ID, }) if err != nil { return nil, err @@ -725,12 +836,47 @@ func (c *UnknownComponent) UnmarshalJSON(data []byte) error { return err } - c.type_ = unknownComponent.Type - c.id = unknownComponent.ID + c.ComponentType = unknownComponent.Type + c.ID = unknownComponent.ID c.Data = data return nil } func (c UnknownComponent) Type() ComponentType { - return c.type_ + return c.ComponentType +} + +func (c UnknownComponent) GetID() int { + return c.ID +} + +func (c UnknownComponent) GetCustomID() string { + var data struct { + CustomID string `json:"custom_id"` + } + if err := json.Unmarshal(c.Data, &data); err != nil { + return "" + } + + return data.CustomID } + +func (c UnknownComponent) GetComponents() []Component { + var data struct { + Components []UnmarshalComponent `json:"components"` + } + if err := json.Unmarshal(c.Data, &data); err != nil { + return nil + } + + var components []Component + for _, component := range data.Components { + components = append(components, component.Component) + } + return components +} + +func (UnknownComponent) component() {} +func (UnknownComponent) interactiveComponent() {} +func (UnknownComponent) layoutComponent() {} +func (UnknownComponent) selectMenuComponent() {} diff --git a/discord/message.go b/discord/message.go index e0642660c..38c0055e5 100644 --- a/discord/message.go +++ b/discord/message.go @@ -95,22 +95,13 @@ func MessageURL(guildID snowflake.ID, channelID snowflake.ID, messageID snowflak // Message is a struct for messages sent in discord text-based channels type Message struct { - ID snowflake.ID `json:"id"` - GuildID *snowflake.ID `json:"guild_id"` - Reactions []MessageReaction `json:"reactions"` - Attachments []Attachment `json:"attachments"` - TTS bool `json:"tts"` - Embeds []Embed `json:"embeds,omitempty"` - // Components can be any of: - // ActionRowComponent, - // SectionComponent, - // TextDisplayComponent, - // MediaGalleryComponent, - // SeparatorComponent, - // FileComponent, - // ContainerComponent, - // UnknownComponent - Components []Component `json:"components,omitempty"` + ID snowflake.ID `json:"id"` + GuildID *snowflake.ID `json:"guild_id"` + Reactions []MessageReaction `json:"reactions"` + Attachments []Attachment `json:"attachments"` + TTS bool `json:"tts"` + Embeds []Embed `json:"embeds,omitempty"` + Components []LayoutComponent `json:"components,omitempty"` CreatedAt time.Time `json:"timestamp"` Mentions []User `json:"mentions"` MentionEveryone bool `json:"mention_everyone"` @@ -570,12 +561,12 @@ type MessageCall struct { EndedTimestamp *time.Time `json:"ended_timestamp"` } -func unmarshalComponents(components []UnmarshalComponent) []Component { - containerComponents := make([]Component, len(components)) +func unmarshalComponents(components []UnmarshalComponent) []LayoutComponent { + c := make([]LayoutComponent, len(components)) for i := range components { - containerComponents[i] = components[i].Component + c[i] = components[i].Component.(LayoutComponent) } - return containerComponents + return c } // Nonce is a string or int used when sending a message to discord. diff --git a/discord/select_menu.go b/discord/select_menu.go index 6f7105dee..0c8c94a9c 100644 --- a/discord/select_menu.go +++ b/discord/select_menu.go @@ -7,7 +7,7 @@ import ( type SelectMenuComponent interface { InteractiveComponent - selectMenu() + selectMenuComponent() } var ( @@ -26,6 +26,7 @@ func NewStringSelectMenu(customID string, placeholder string, options ...StringS } type StringSelectMenuComponent struct { + ID int `json:"id,omitempty"` CustomID string `json:"custom_id"` Placeholder string `json:"placeholder,omitempty"` MinValues *int `json:"min_values,omitempty"` @@ -49,13 +50,17 @@ func (StringSelectMenuComponent) Type() ComponentType { return ComponentTypeStringSelectMenu } -func (c StringSelectMenuComponent) ID() string { +func (c StringSelectMenuComponent) GetID() int { + return c.ID +} + +func (c StringSelectMenuComponent) GetCustomID() string { return c.CustomID } func (StringSelectMenuComponent) component() {} func (StringSelectMenuComponent) interactiveComponent() {} -func (StringSelectMenuComponent) selectMenu() {} +func (StringSelectMenuComponent) selectMenuComponent() {} // WithCustomID returns a new StringSelectMenuComponent with the provided customID func (c StringSelectMenuComponent) WithCustomID(customID string) StringSelectMenuComponent { @@ -140,6 +145,7 @@ func NewStringSelectMenuOption(label string, value string) StringSelectMenuOptio // StringSelectMenuOption represents an option in a StringSelectMenuComponent type StringSelectMenuOption struct { + ID int `json:"id,omitempty"` Label string `json:"label"` Value string `json:"value"` Description string `json:"description,omitempty"` @@ -192,6 +198,7 @@ func NewUserSelectMenu(customID string, placeholder string) UserSelectMenuCompon } type UserSelectMenuComponent struct { + ID int `json:"id,omitempty"` CustomID string `json:"custom_id"` Placeholder string `json:"placeholder,omitempty"` DefaultValues []SelectMenuDefaultValue `json:"default_values,omitempty"` @@ -215,13 +222,17 @@ func (UserSelectMenuComponent) Type() ComponentType { return ComponentTypeUserSelectMenu } -func (c UserSelectMenuComponent) ID() string { +func (c UserSelectMenuComponent) GetID() int { + return c.ID +} + +func (c UserSelectMenuComponent) GetCustomID() string { return c.CustomID } func (UserSelectMenuComponent) component() {} func (UserSelectMenuComponent) interactiveComponent() {} -func (UserSelectMenuComponent) selectMenu() {} +func (UserSelectMenuComponent) selectMenuComponent() {} // WithCustomID returns a new UserSelectMenuComponent with the provided customID func (c UserSelectMenuComponent) WithCustomID(customID string) UserSelectMenuComponent { @@ -304,6 +315,7 @@ func NewRoleSelectMenu(customID string, placeholder string) RoleSelectMenuCompon } type RoleSelectMenuComponent struct { + ID int `json:"id,omitempty"` CustomID string `json:"custom_id"` Placeholder string `json:"placeholder,omitempty"` DefaultValues []SelectMenuDefaultValue `json:"default_values,omitempty"` @@ -327,13 +339,17 @@ func (RoleSelectMenuComponent) Type() ComponentType { return ComponentTypeRoleSelectMenu } -func (c RoleSelectMenuComponent) ID() string { +func (c RoleSelectMenuComponent) GetID() int { + return c.ID +} + +func (c RoleSelectMenuComponent) GetCustomID() string { return c.CustomID } func (RoleSelectMenuComponent) component() {} func (RoleSelectMenuComponent) interactiveComponent() {} -func (RoleSelectMenuComponent) selectMenu() {} +func (RoleSelectMenuComponent) selectMenuComponent() {} // WithCustomID returns a new RoleSelectMenuComponent with the provided customID func (c RoleSelectMenuComponent) WithCustomID(customID string) RoleSelectMenuComponent { @@ -416,6 +432,7 @@ func NewMentionableSelectMenu(customID string, placeholder string) MentionableSe } type MentionableSelectMenuComponent struct { + ID int `json:"id,omitempty"` CustomID string `json:"custom_id"` Placeholder string `json:"placeholder,omitempty"` DefaultValues []SelectMenuDefaultValue `json:"default_values,omitempty"` @@ -439,13 +456,17 @@ func (MentionableSelectMenuComponent) Type() ComponentType { return ComponentTypeMentionableSelectMenu } -func (c MentionableSelectMenuComponent) ID() string { +func (c MentionableSelectMenuComponent) GetID() int { + return c.ID +} + +func (c MentionableSelectMenuComponent) GetCustomID() string { return c.CustomID } func (MentionableSelectMenuComponent) component() {} func (MentionableSelectMenuComponent) interactiveComponent() {} -func (MentionableSelectMenuComponent) selectMenu() {} +func (MentionableSelectMenuComponent) selectMenuComponent() {} // WithCustomID returns a new MentionableSelectMenuComponent with the provided customID func (c MentionableSelectMenuComponent) WithCustomID(customID string) MentionableSelectMenuComponent { @@ -525,6 +546,7 @@ func NewChannelSelectMenu(customID string, placeholder string) ChannelSelectMenu } type ChannelSelectMenuComponent struct { + ID int `json:"id,omitempty"` CustomID string `json:"custom_id"` Placeholder string `json:"placeholder,omitempty"` DefaultValues []SelectMenuDefaultValue `json:"default_values,omitempty"` @@ -549,13 +571,17 @@ func (ChannelSelectMenuComponent) Type() ComponentType { return ComponentTypeChannelSelectMenu } -func (c ChannelSelectMenuComponent) ID() string { +func (c ChannelSelectMenuComponent) GetID() int { + return c.ID +} + +func (c ChannelSelectMenuComponent) GetCustomID() string { return c.CustomID } func (ChannelSelectMenuComponent) component() {} func (ChannelSelectMenuComponent) interactiveComponent() {} -func (ChannelSelectMenuComponent) selectMenu() {} +func (ChannelSelectMenuComponent) selectMenuComponent() {} // WithCustomID returns a new ChannelSelectMenuComponent with the provided customID func (c ChannelSelectMenuComponent) WithCustomID(customID string) ChannelSelectMenuComponent { From 30b44e30f022592a1e6d6f0aa501c46687eaea69 Mon Sep 17 00:00:00 2001 From: topi314 Date: Mon, 3 Mar 2025 22:23:02 +0100 Subject: [PATCH 3/5] implement missing interfaces to have strong component typing --- _examples/componentsv2/example.go | 4 +- discord/component.go | 233 +++++++++++++++------- discord/interaction_modal_submit.go | 17 +- discord/message.go | 209 ++----------------- discord/message_create.go | 26 +-- discord/message_create_builder.go | 26 +-- discord/message_update.go | 12 +- discord/message_update_builder.go | 34 ++-- discord/modal_create.go | 32 +-- discord/select_menu.go | 7 + discord/webhook_message_create.go | 26 +-- discord/webhook_message_create_builder.go | 26 +-- discord/webhook_message_update.go | 14 +- discord/webhook_message_update_builder.go | 34 ++-- 14 files changed, 308 insertions(+), 392 deletions(-) diff --git a/_examples/componentsv2/example.go b/_examples/componentsv2/example.go index 9c918892b..0722db7b4 100644 --- a/_examples/componentsv2/example.go +++ b/_examples/componentsv2/example.go @@ -12,10 +12,12 @@ import ( "github.com/disgoorg/disgo/discord" "github.com/disgoorg/disgo/events" "github.com/disgoorg/disgo/gateway" + "github.com/disgoorg/snowflake/v2" ) var ( - token = os.Getenv("disgo_token") + token = os.Getenv("disgo_token") + guildID = snowflake.GetEnv("disgo_guild_id") ) func main() { diff --git a/discord/component.go b/discord/component.go index fc05ce8cd..abeac40a6 100644 --- a/discord/component.go +++ b/discord/component.go @@ -29,16 +29,49 @@ const ( ComponentTypeContainer ) +// Component is an interface for all components. +// [ActionRowComponent] +// [ButtonComponent] +// [StringSelectMenuComponent] +// [TextInputComponent] +// [UserSelectMenuComponent] +// [RoleSelectMenuComponent] +// [MentionableSelectMenuComponent] +// [ChannelSelectMenuComponent] +// [SectionComponent] +// [TextDisplayComponent] +// [ThumbnailComponent] +// [MediaGalleryComponent] +// [FileComponent] +// [SeparatorComponent] +// [ContainerComponent] +// [UnknownComponent] type Component interface { json.Marshaler + // Type returns the ComponentType of the Component. Type() ComponentType + // GetID returns the id of the Component. This is used to uniquely identify a Component in a [Message] and needs to be unique. GetID() int + // component is a marker to simulate unions. component() } +// InteractiveComponent is an interface for all components that can be present in an [ActionRowComponent]. +// [ButtonComponent] +// [StringSelectMenuComponent] +// [TextInputComponent] (currently only supported in modals) +// [UserSelectMenuComponent] +// [RoleSelectMenuComponent] +// [MentionableSelectMenuComponent] +// [ChannelSelectMenuComponent] +// [ButtonComponent] +// [SelectMenuComponent] +// [UnknownComponent] type InteractiveComponent interface { Component + // GetCustomID returns the customID of the Component. This can be used to identify or transport data with the Component. GetCustomID() string + // interactiveComponent is a marker to simulate unions. interactiveComponent() } @@ -50,14 +83,24 @@ type InteractiveComponent interface { // [FileComponent] // [SeparatorComponent] // [ContainerComponent] +// [UnknownComponent] type LayoutComponent interface { Component layoutComponent() } -type MessageComponent interface { +// ContainerSubComponent is an interface for all components that can be present in a [ContainerComponent]. +// [ActionRowComponent] +// [SectionComponent] +// [TextDisplayComponent] +// [MediaGalleryComponent] +// [FileComponent] +// [SeparatorComponent] +// [UnknownComponent] +type ContainerSubComponent interface { Component - messageComponent() + // containerSubComponent is a marker to simulate unions. + containerSubComponent() } type UnmarshalComponent struct { @@ -172,18 +215,20 @@ type ComponentEmoji struct { } var ( - _ Component = (*ActionRowComponent)(nil) + _ Component = (*ActionRowComponent)(nil) + _ LayoutComponent = (*ActionRowComponent)(nil) + _ ContainerSubComponent = (*ActionRowComponent)(nil) ) -func NewActionRow(components ...Component) ActionRowComponent { +func NewActionRow(components ...InteractiveComponent) ActionRowComponent { return ActionRowComponent{ Components: components, } } type ActionRowComponent struct { - ID int `json:"id,omitempty"` - Components []Component `json:"components"` + ID int `json:"id,omitempty"` + Components []InteractiveComponent `json:"components"` } func (c ActionRowComponent) MarshalJSON() ([]byte, error) { @@ -197,6 +242,24 @@ func (c ActionRowComponent) MarshalJSON() ([]byte, error) { }) } +func (c *ActionRowComponent) UnmarshalJSON(data []byte) error { + var actionRowComponent struct { + ID int `json:"id,omitempty"` + Components []UnmarshalComponent `json:"components"` + } + if err := json.Unmarshal(data, &actionRowComponent); err != nil { + return err + } + + c.ID = actionRowComponent.ID + components := make([]InteractiveComponent, 0, len(actionRowComponent.Components)) + for _, component := range actionRowComponent.Components { + components = append(components, component.Component.(InteractiveComponent)) + } + c.Components = components + return nil +} + func (ActionRowComponent) Type() ComponentType { return ComponentTypeActionRow } @@ -205,14 +268,11 @@ func (c ActionRowComponent) GetID() int { return c.ID } -func (c ActionRowComponent) GetComponents() []Component { - return c.Components -} +func (ActionRowComponent) component() {} +func (ActionRowComponent) layoutComponent() {} +func (ActionRowComponent) containerSubComponent() {} -func (ActionRowComponent) component() {} -func (ActionRowComponent) layoutComponent() {} - -// Buttons returns all ButtonComponent(s) in the ActionRowComponent +// Buttons returns all ButtonComponent(s) in the ActionRowComponent. func (c ActionRowComponent) Buttons() []ButtonComponent { var buttons []ButtonComponent for i := range c.Components { @@ -223,7 +283,7 @@ func (c ActionRowComponent) Buttons() []ButtonComponent { return buttons } -// SelectMenus returns all SelectMenuComponent(s) in the ActionRowComponent +// SelectMenus returns all SelectMenuComponent(s) in the ActionRowComponent. func (c ActionRowComponent) SelectMenus() []SelectMenuComponent { var selectMenus []SelectMenuComponent for i := range c.Components { @@ -234,7 +294,7 @@ func (c ActionRowComponent) SelectMenus() []SelectMenuComponent { return selectMenus } -// TextInputs returns all TextInputComponent(s) in the ActionRowComponent +// TextInputs returns all TextInputComponent(s) in the ActionRowComponent. func (c ActionRowComponent) TextInputs() []TextInputComponent { var textInputs []TextInputComponent for i := range c.Components { @@ -245,33 +305,37 @@ func (c ActionRowComponent) TextInputs() []TextInputComponent { return textInputs } -// UpdateComponent returns a new ActionRowComponent with the Component which has the customID replaced -// TODO: fix this -func (c ActionRowComponent) UpdateComponent(customID string, component Component) ActionRowComponent { - //for i, cc := range c.Components { - // if cc.ID() == customID { - // c[i] = component - // return c - // } - //} +// UpdateComponent returns a new ActionRowComponent with the Component which has the id replaced with the provided Component. +func (c ActionRowComponent) UpdateComponent(id int, component InteractiveComponent) ActionRowComponent { + for i, cc := range c.Components { + if cc.GetID() == id { + c.Components[i] = component + return c + } + } return c } // AddComponents returns a new ActionRowComponent with the provided Component(s) added -func (c ActionRowComponent) AddComponents(components ...Component) ActionRowComponent { +func (c ActionRowComponent) AddComponents(components ...InteractiveComponent) ActionRowComponent { c.Components = append(c.Components, components...) return c } -// RemoveComponent returns a new ActionRowComponent with the provided Component at the index removed -func (c ActionRowComponent) RemoveComponent(index int) ActionRowComponent { - if len(c.Components) > index { - c.Components = append(c.Components[:index], c.Components[index+1:]...) +// RemoveComponent returns a new ActionRowComponent with the provided Component which has the provided id removed. +func (c ActionRowComponent) RemoveComponent(id int) ActionRowComponent { + for i, cc := range c.Components { + if cc.GetID() == id { + c.Components = append(c.Components[:i], c.Components[i+1:]...) + return c + } } return c } -// ButtonStyle defines how the ButtonComponent looks like (https://discord.com/assets/7bb017ce52cfd6575e21c058feb3883b.png) +// ButtonStyle defines how the ButtonComponent looks like. [Discord Docs] +// +// [Discord Docs]: https://discord.com/developers/docs/interactions/message-components#button-object-button-styles type ButtonStyle int // Supported ButtonStyle(s) @@ -284,7 +348,7 @@ const ( ButtonStylePremium ) -// NewButton creates a new ButtonComponent with the provided parameters. Link ButtonComponent(s) need a URL and other ButtonComponent(s) need a customID +// NewButton creates a new [ButtonComponent] with the provided parameters. Link ButtonComponent(s) need a URL and other ButtonComponent(s) need a customID func NewButton(style ButtonStyle, label string, customID string, url string, skuID snowflake.ID) ButtonComponent { return ButtonComponent{ Style: style, @@ -447,6 +511,7 @@ var ( _ Component = (*TextInputComponent)(nil) ) +// NewTextInput creates a new [TextInputComponent] with the provided parameters. func NewTextInput(customID string, style TextInputStyle, label string) TextInputComponent { return TextInputComponent{ CustomID: customID, @@ -455,14 +520,19 @@ func NewTextInput(customID string, style TextInputStyle, label string) TextInput } } +// NewShortTextInput creates a new [TextInputComponent] with [TextInputStyleShort] & the provided parameters func NewShortTextInput(customID string, label string) TextInputComponent { return NewTextInput(customID, TextInputStyleShort, label) } +// NewParagraphTextInput creates a new [TextInputComponent] with [TextInputStyleParagraph] & the provided parameters func NewParagraphTextInput(customID string, label string) TextInputComponent { return NewTextInput(customID, TextInputStyleParagraph, label) } +// TextInputComponent is a component that allows users to input text. [Discord Docs] +// +// [Discord Docs]: https://discord.com/developers/docs/interactions/message-components#text-inputs type TextInputComponent struct { ID int `json:"id,omitempty"` CustomID string `json:"custom_id"` @@ -556,8 +626,9 @@ type UnfurledMediaItem struct { } var ( - _ Component = (*SectionComponent)(nil) - _ LayoutComponent = (*SectionComponent)(nil) + _ Component = (*SectionComponent)(nil) + _ LayoutComponent = (*SectionComponent)(nil) + _ ContainerSubComponent = (*SectionComponent)(nil) ) type SectionComponent struct { @@ -589,13 +660,16 @@ func (c SectionComponent) GetComponents() []Component { return c.Components } -func (SectionComponent) component() {} -func (SectionComponent) layoutComponent() {} +func (SectionComponent) component() {} +func (SectionComponent) layoutComponent() {} +func (SectionComponent) containerSubComponent() {} var ( - _ Component = (*TextDisplayComponent)(nil) + _ Component = (*TextDisplayComponent)(nil) + _ ContainerSubComponent = (*TextDisplayComponent)(nil) ) +// TextDisplayComponent is a component that displays text. type TextDisplayComponent struct { ID int `json:"id,omitempty"` Content string `json:"content"` @@ -620,7 +694,8 @@ func (c TextDisplayComponent) GetID() int { return c.ID } -func (TextDisplayComponent) component() {} +func (TextDisplayComponent) component() {} +func (TextDisplayComponent) containerSubComponent() {} var ( _ Component = (*ThumbnailComponent)(nil) @@ -661,7 +736,9 @@ type MediaGalleryItem struct { } var ( - _ Component = (*MediaGalleryComponent)(nil) + _ Component = (*MediaGalleryComponent)(nil) + _ LayoutComponent = (*MediaGalleryComponent)(nil) + _ ContainerSubComponent = (*MediaGalleryComponent)(nil) ) type MediaGalleryComponent struct { @@ -688,7 +765,9 @@ func (c MediaGalleryComponent) GetID() int { return c.ID } -func (MediaGalleryComponent) component() {} +func (MediaGalleryComponent) component() {} +func (MediaGalleryComponent) layoutComponent() {} +func (MediaGalleryComponent) containerSubComponent() {} type SeparatorSpacingSize int @@ -699,7 +778,9 @@ const ( ) var ( - _ Component = (*SeparatorComponent)(nil) + _ Component = (*SeparatorComponent)(nil) + _ LayoutComponent = (*MediaGalleryComponent)(nil) + _ ContainerSubComponent = (*MediaGalleryComponent)(nil) ) type SeparatorComponent struct { @@ -727,7 +808,9 @@ func (c SeparatorComponent) GetID() int { return c.ID } -func (SeparatorComponent) component() {} +func (SeparatorComponent) component() {} +func (SeparatorComponent) layoutComponent() {} +func (SeparatorComponent) containerSubComponent() {} var ( _ Component = (*FileComponent)(nil) @@ -767,10 +850,10 @@ var ( ) type ContainerComponent struct { - ID int `json:"id,omitempty"` - AccentColor *int `json:"accent_color,omitempty"` - Spoiler bool `json:"spoiler,omitempty"` - Components []Component `json:"components"` + ID int `json:"id,omitempty"` + AccentColor *int `json:"accent_color,omitempty"` + Spoiler bool `json:"spoiler,omitempty"` + Components []ContainerSubComponent `json:"components"` } func (c ContainerComponent) MarshalJSON() ([]byte, error) { @@ -784,6 +867,29 @@ func (c ContainerComponent) MarshalJSON() ([]byte, error) { }) } +func (c *ContainerComponent) UnmarshalJSON(data []byte) error { + var containerComponent struct { + ID int `json:"id,omitempty"` + AccentColor *int `json:"accent_color,omitempty"` + Spoiler bool `json:"spoiler,omitempty"` + Components []UnmarshalComponent `json:"components"` + } + if err := json.Unmarshal(data, &containerComponent); err != nil { + return err + } + + c.ID = containerComponent.ID + c.AccentColor = containerComponent.AccentColor + c.Spoiler = containerComponent.Spoiler + + components := make([]ContainerSubComponent, 0, len(containerComponent.Components)) + for _, component := range containerComponent.Components { + components = append(components, component.Component.(ContainerSubComponent)) + } + c.Components = components + return nil +} + func (ContainerComponent) Type() ComponentType { return ComponentTypeContainer } @@ -792,18 +898,15 @@ func (c ContainerComponent) GetID() int { return c.ID } -func (c ContainerComponent) GetComponents() []Component { - return c.Components -} - func (ContainerComponent) component() {} func (ContainerComponent) layoutComponent() {} var ( - _ Component = (*UnknownComponent)(nil) - _ InteractiveComponent = (*UnknownComponent)(nil) - _ LayoutComponent = (*UnknownComponent)(nil) - _ SelectMenuComponent = (*UnknownComponent)(nil) + _ Component = (*UnknownComponent)(nil) + _ InteractiveComponent = (*UnknownComponent)(nil) + _ LayoutComponent = (*UnknownComponent)(nil) + _ SelectMenuComponent = (*UnknownComponent)(nil) + _ ContainerSubComponent = (*UnknownComponent)(nil) ) type UnknownComponent struct { @@ -861,22 +964,8 @@ func (c UnknownComponent) GetCustomID() string { return data.CustomID } -func (c UnknownComponent) GetComponents() []Component { - var data struct { - Components []UnmarshalComponent `json:"components"` - } - if err := json.Unmarshal(c.Data, &data); err != nil { - return nil - } - - var components []Component - for _, component := range data.Components { - components = append(components, component.Component) - } - return components -} - -func (UnknownComponent) component() {} -func (UnknownComponent) interactiveComponent() {} -func (UnknownComponent) layoutComponent() {} -func (UnknownComponent) selectMenuComponent() {} +func (UnknownComponent) component() {} +func (UnknownComponent) interactiveComponent() {} +func (UnknownComponent) layoutComponent() {} +func (UnknownComponent) selectMenuComponent() {} +func (UnknownComponent) containerSubComponent() {} diff --git a/discord/interaction_modal_submit.go b/discord/interaction_modal_submit.go index 204fdb458..daf21f254 100644 --- a/discord/interaction_modal_submit.go +++ b/discord/interaction_modal_submit.go @@ -93,14 +93,15 @@ func (d *ModalSubmitInteractionData) UnmarshalJSON(data []byte) error { *d = ModalSubmitInteractionData(iData.modalSubmitInteractionData) - if len(iData.Components) > 0 { - d.Components = make(map[string]InteractiveComponent, len(iData.Components)) - for _, containerComponent := range iData.Components { - for _, component := range containerComponent.Component.(ContainerComponent).Components() { - d.Components[component.ID()] = component - } - } - } + // TODO: implement component iterator once we bumped go version to 1.24 + //if len(iData.Components) > 0 { + // d.Components = make(map[string]InteractiveComponent, len(iData.Components)) + // for _, containerComponent := range iData.Components { + // for _, component := range containerComponent.Component.(ContainerComponent).Components() { + // d.Components[component.ID()] = component + // } + // } + //} return nil } diff --git a/discord/message.go b/discord/message.go index 38c0055e5..d47a35622 100644 --- a/discord/message.go +++ b/discord/message.go @@ -159,190 +159,7 @@ func (m *Message) UnmarshalJSON(data []byte) error { return nil } -// ActionRows returns all ActionRowComponent(s) from this Message -func (m Message) ActionRows() []ActionRowComponent { - var actionRows []ActionRowComponent - for i := range m.Components { - if actionRow, ok := m.Components[i].(ActionRowComponent); ok { - actionRows = append(actionRows, actionRow) - } - } - return actionRows -} - -// InteractiveComponents returns the InteractiveComponent(s) from this Message -func (m Message) InteractiveComponents() []InteractiveComponent { - var interactiveComponents []InteractiveComponent - for i := range m.Components { - for ii := range m.Components[i].Components() { - interactiveComponents = append(interactiveComponents, m.Components[i].Components()[ii]) - } - } - return interactiveComponents -} - -// ComponentByID returns the Component with the specific CustomID -func (m Message) ComponentByID(customID string) InteractiveComponent { - for i := range m.Components { - for ii := range m.Components[i].Components() { - if m.Components[i].Components()[ii].ID() == customID { - return m.Components[i].Components()[ii] - } - } - } - return nil -} - -// Buttons returns all ButtonComponent(s) from this Message -func (m Message) Buttons() []ButtonComponent { - var buttons []ButtonComponent - for i := range m.Components { - for ii := range m.Components[i].Components() { - if button, ok := m.Components[i].Components()[ii].(ButtonComponent); ok { - buttons = append(buttons, button) - } - } - } - return buttons -} - -// ButtonByID returns a ButtonComponent with the specific customID from this Message -func (m Message) ButtonByID(customID string) (ButtonComponent, bool) { - for i := range m.Components { - for ii := range m.Components[i].Components() { - if button, ok := m.Components[i].Components()[ii].(ButtonComponent); ok && button.ID() == customID { - return button, true - } - } - } - return ButtonComponent{}, false -} - -// SelectMenus returns all SelectMenuComponent(s) from this Message -func (m Message) SelectMenus() []SelectMenuComponent { - var selectMenus []SelectMenuComponent - for i := range m.Components { - for ii := range m.Components[i].Components() { - if selectMenu, ok := m.Components[i].Components()[ii].(SelectMenuComponent); ok { - selectMenus = append(selectMenus, selectMenu) - } - } - } - return selectMenus -} - -// SelectMenuByID returns a SelectMenuComponent with the specific customID from this Message -func (m Message) SelectMenuByID(customID string) (SelectMenuComponent, bool) { - for i := range m.Components { - for ii := range m.Components[i].Components() { - if selectMenu, ok := m.Components[i].Components()[ii].(SelectMenuComponent); ok && selectMenu.ID() == customID { - return selectMenu, true - } - } - } - return nil, false -} - -// UserSelectMenus returns all UserSelectMenuComponent(s) from this Message -func (m Message) UserSelectMenus() []UserSelectMenuComponent { - var userSelectMenus []UserSelectMenuComponent - for i := range m.Components { - for ii := range m.Components[i].Components() { - if userSelectMenu, ok := m.Components[i].Components()[ii].(UserSelectMenuComponent); ok { - userSelectMenus = append(userSelectMenus, userSelectMenu) - } - } - } - return userSelectMenus -} - -// UserSelectMenuByID returns a UserSelectMenuComponent with the specific customID from this Message -func (m Message) UserSelectMenuByID(customID string) (UserSelectMenuComponent, bool) { - for i := range m.Components { - for ii := range m.Components[i].Components() { - if userSelectMenu, ok := m.Components[i].Components()[ii].(UserSelectMenuComponent); ok && userSelectMenu.ID() == customID { - return userSelectMenu, true - } - } - } - return UserSelectMenuComponent{}, false -} - -// RoleSelectMenus returns all RoleSelectMenuComponent(s) from this Message -func (m Message) RoleSelectMenus() []RoleSelectMenuComponent { - var roleSelectMenus []RoleSelectMenuComponent - for i := range m.Components { - for ii := range m.Components[i].Components() { - if roleSelectMenu, ok := m.Components[i].Components()[ii].(RoleSelectMenuComponent); ok { - roleSelectMenus = append(roleSelectMenus, roleSelectMenu) - } - } - } - return roleSelectMenus -} - -// RoleSelectMenuByID returns a RoleSelectMenuComponent with the specific customID from this Message -func (m Message) RoleSelectMenuByID(customID string) (RoleSelectMenuComponent, bool) { - for i := range m.Components { - for ii := range m.Components[i].Components() { - if roleSelectMenu, ok := m.Components[i].Components()[ii].(RoleSelectMenuComponent); ok && roleSelectMenu.ID() == customID { - return roleSelectMenu, true - } - } - } - return RoleSelectMenuComponent{}, false -} - -// MentionableSelectMenus returns all MentionableSelectMenuComponent(s) from this Message -func (m Message) MentionableSelectMenus() []MentionableSelectMenuComponent { - var mentionableSelectMenus []MentionableSelectMenuComponent - for i := range m.Components { - for ii := range m.Components[i].Components() { - if mentionableSelectMenu, ok := m.Components[i].Components()[ii].(MentionableSelectMenuComponent); ok { - mentionableSelectMenus = append(mentionableSelectMenus, mentionableSelectMenu) - } - } - } - return mentionableSelectMenus -} - -// MentionableSelectMenuByID returns a MentionableSelectMenuComponent with the specific customID from this Message -func (m Message) MentionableSelectMenuByID(customID string) (MentionableSelectMenuComponent, bool) { - for i := range m.Components { - for ii := range m.Components[i].Components() { - if mentionableSelectMenu, ok := m.Components[i].Components()[ii].(MentionableSelectMenuComponent); ok && mentionableSelectMenu.ID() == customID { - return mentionableSelectMenu, true - } - } - } - return MentionableSelectMenuComponent{}, false -} - -// ChannelSelectMenus returns all ChannelSelectMenuComponent(s) from this Message -func (m Message) ChannelSelectMenus() []ChannelSelectMenuComponent { - var channelSelectMenus []ChannelSelectMenuComponent - for i := range m.Components { - for ii := range m.Components[i].Components() { - if channelSelectMenu, ok := m.Components[i].Components()[ii].(ChannelSelectMenuComponent); ok { - channelSelectMenus = append(channelSelectMenus, channelSelectMenu) - } - } - } - return channelSelectMenus -} - -// ChannelSelectMenuByID returns a ChannelSelectMenuComponent with the specific customID from this Message -func (m Message) ChannelSelectMenuByID(customID string) (ChannelSelectMenuComponent, bool) { - for i := range m.Components { - for ii := range m.Components[i].Components() { - if channelSelectMenu, ok := m.Components[i].Components()[ii].(ChannelSelectMenuComponent); ok && channelSelectMenu.ID() == customID { - return channelSelectMenu, true - } - } - } - return ChannelSelectMenuComponent{}, false -} - +// JumpURL returns the URL which can be used to jump to the message in the discord client. func (m Message) JumpURL() string { guildID := "@me" if m.GuildID != nil { @@ -439,18 +256,18 @@ type MessageSnapshot struct { } type PartialMessage struct { - Type MessageType `json:"type"` - Content string `json:"content,omitempty"` - Embeds []Embed `json:"embeds,omitempty"` - Attachments []Attachment `json:"attachments"` - CreatedAt time.Time `json:"timestamp"` - EditedTimestamp *time.Time `json:"edited_timestamp"` - Flags MessageFlags `json:"flags"` - Mentions []User `json:"mentions"` - MentionRoles []snowflake.ID `json:"mention_roles"` - Stickers []Sticker `json:"stickers"` - StickerItems []MessageSticker `json:"sticker_items,omitempty"` - Components []Component `json:"components,omitempty"` + Type MessageType `json:"type"` + Content string `json:"content,omitempty"` + Embeds []Embed `json:"embeds,omitempty"` + Attachments []Attachment `json:"attachments"` + CreatedAt time.Time `json:"timestamp"` + EditedTimestamp *time.Time `json:"edited_timestamp"` + Flags MessageFlags `json:"flags"` + Mentions []User `json:"mentions"` + MentionRoles []snowflake.ID `json:"mention_roles"` + Stickers []Sticker `json:"stickers"` + StickerItems []MessageSticker `json:"sticker_items,omitempty"` + Components []LayoutComponent `json:"components,omitempty"` } func (m *PartialMessage) UnmarshalJSON(data []byte) error { diff --git a/discord/message_create.go b/discord/message_create.go index e21cc4cba..15f7e2b03 100644 --- a/discord/message_create.go +++ b/discord/message_create.go @@ -6,19 +6,19 @@ import ( // MessageCreate is the struct to create a new Message with type MessageCreate struct { - Nonce string `json:"nonce,omitempty"` - Content string `json:"content,omitempty"` - TTS bool `json:"tts,omitempty"` - Embeds []Embed `json:"embeds,omitempty"` - Components []ContainerComponent `json:"components,omitempty"` - StickerIDs []snowflake.ID `json:"sticker_ids,omitempty"` - Files []*File `json:"-"` - Attachments []AttachmentCreate `json:"attachments,omitempty"` - AllowedMentions *AllowedMentions `json:"allowed_mentions,omitempty"` - MessageReference *MessageReference `json:"message_reference,omitempty"` - Flags MessageFlags `json:"flags,omitempty"` - EnforceNonce bool `json:"enforce_nonce,omitempty"` - Poll *PollCreate `json:"poll,omitempty"` + Nonce string `json:"nonce,omitempty"` + Content string `json:"content,omitempty"` + TTS bool `json:"tts,omitempty"` + Embeds []Embed `json:"embeds,omitempty"` + Components []LayoutComponent `json:"components,omitempty"` + StickerIDs []snowflake.ID `json:"sticker_ids,omitempty"` + Files []*File `json:"-"` + Attachments []AttachmentCreate `json:"attachments,omitempty"` + AllowedMentions *AllowedMentions `json:"allowed_mentions,omitempty"` + MessageReference *MessageReference `json:"message_reference,omitempty"` + Flags MessageFlags `json:"flags,omitempty"` + EnforceNonce bool `json:"enforce_nonce,omitempty"` + Poll *PollCreate `json:"poll,omitempty"` } func (MessageCreate) interactionCallbackData() {} diff --git a/discord/message_create_builder.go b/discord/message_create_builder.go index cf1178c17..dddc88701 100644 --- a/discord/message_create_builder.go +++ b/discord/message_create_builder.go @@ -84,14 +84,14 @@ func (b *MessageCreateBuilder) RemoveEmbed(i int) *MessageCreateBuilder { return b } -// SetContainerComponents sets the discord.ContainerComponent(s) of the Message -func (b *MessageCreateBuilder) SetContainerComponents(containerComponents ...ContainerComponent) *MessageCreateBuilder { - b.Components = containerComponents +// SetLayoutComponents sets the discord.LayoutComponent(s) of the Message +func (b *MessageCreateBuilder) SetLayoutComponents(LayoutComponents ...LayoutComponent) *MessageCreateBuilder { + b.Components = LayoutComponents return b } -// SetContainerComponent sets the provided discord.InteractiveComponent at the index of discord.InteractiveComponent(s) -func (b *MessageCreateBuilder) SetContainerComponent(i int, container ContainerComponent) *MessageCreateBuilder { +// SetLayoutComponent sets the provided discord.InteractiveComponent at the index of discord.InteractiveComponent(s) +func (b *MessageCreateBuilder) SetLayoutComponent(i int, container LayoutComponent) *MessageCreateBuilder { if len(b.Components) > i { b.Components[i] = container } @@ -100,27 +100,27 @@ func (b *MessageCreateBuilder) SetContainerComponent(i int, container ContainerC // AddActionRow adds a new discord.ActionRowComponent with the provided discord.InteractiveComponent(s) to the Message func (b *MessageCreateBuilder) AddActionRow(components ...InteractiveComponent) *MessageCreateBuilder { - b.Components = append(b.Components, ActionRowComponent(components)) + b.Components = append(b.Components, ActionRowComponent{Components: components}) return b } -// AddContainerComponents adds the discord.ContainerComponent(s) to the Message -func (b *MessageCreateBuilder) AddContainerComponents(containers ...ContainerComponent) *MessageCreateBuilder { +// AddLayoutComponents adds the discord.LayoutComponent(s) to the Message +func (b *MessageCreateBuilder) AddLayoutComponents(containers ...LayoutComponent) *MessageCreateBuilder { b.Components = append(b.Components, containers...) return b } -// RemoveContainerComponent removes a discord.ActionRowComponent from the Message -func (b *MessageCreateBuilder) RemoveContainerComponent(i int) *MessageCreateBuilder { +// RemoveLayoutComponent removes a discord.ActionRowComponent from the Message +func (b *MessageCreateBuilder) RemoveLayoutComponent(i int) *MessageCreateBuilder { if len(b.Components) > i { b.Components = append(b.Components[:i], b.Components[i+1:]...) } return b } -// ClearContainerComponents removes all the discord.ContainerComponent(s) of the Message -func (b *MessageCreateBuilder) ClearContainerComponents() *MessageCreateBuilder { - b.Components = []ContainerComponent{} +// ClearLayoutComponents removes all the discord.LayoutComponent(s) of the Message +func (b *MessageCreateBuilder) ClearLayoutComponents() *MessageCreateBuilder { + b.Components = []LayoutComponent{} return b } diff --git a/discord/message_update.go b/discord/message_update.go index ddcd48a09..b7454c713 100644 --- a/discord/message_update.go +++ b/discord/message_update.go @@ -2,12 +2,12 @@ package discord // MessageUpdate is used to edit a Message type MessageUpdate struct { - Content *string `json:"content,omitempty"` - Embeds *[]Embed `json:"embeds,omitempty"` - Components *[]ContainerComponent `json:"components,omitempty"` - Attachments *[]AttachmentUpdate `json:"attachments,omitempty"` - Files []*File `json:"-"` - AllowedMentions *AllowedMentions `json:"allowed_mentions,omitempty"` + Content *string `json:"content,omitempty"` + Embeds *[]Embed `json:"embeds,omitempty"` + Components *[]LayoutComponent `json:"components,omitempty"` + Attachments *[]AttachmentUpdate `json:"attachments,omitempty"` + Files []*File `json:"-"` + AllowedMentions *AllowedMentions `json:"allowed_mentions,omitempty"` // Flags are the MessageFlags of the message. // Be careful not to override the current flags when editing messages from other users - this will result in a permission error. // Use MessageFlags.Add for flags like discord.MessageFlagSuppressEmbeds. diff --git a/discord/message_update_builder.go b/discord/message_update_builder.go index c81ca37c5..0706977f2 100644 --- a/discord/message_update_builder.go +++ b/discord/message_update_builder.go @@ -79,19 +79,19 @@ func (b *MessageUpdateBuilder) RemoveEmbed(i int) *MessageUpdateBuilder { return b } -// SetContainerComponents sets the discord.ContainerComponent(s) of the Message -func (b *MessageUpdateBuilder) SetContainerComponents(containerComponents ...ContainerComponent) *MessageUpdateBuilder { +// SetLayoutComponents sets the discord.LayoutComponent(s) of the Message +func (b *MessageUpdateBuilder) SetLayoutComponents(LayoutComponents ...LayoutComponent) *MessageUpdateBuilder { if b.Components == nil { - b.Components = new([]ContainerComponent) + b.Components = new([]LayoutComponent) } - *b.Components = containerComponents + *b.Components = LayoutComponents return b } -// SetContainerComponent sets the provided discord.InteractiveComponent at the index of discord.InteractiveComponent(s) -func (b *MessageUpdateBuilder) SetContainerComponent(i int, container ContainerComponent) *MessageUpdateBuilder { +// SetLayoutComponent sets the provided discord.InteractiveComponent at the index of discord.InteractiveComponent(s) +func (b *MessageUpdateBuilder) SetLayoutComponent(i int, container LayoutComponent) *MessageUpdateBuilder { if b.Components == nil { - b.Components = new([]ContainerComponent) + b.Components = new([]LayoutComponent) } if len(*b.Components) > i { (*b.Components)[i] = container @@ -102,23 +102,23 @@ func (b *MessageUpdateBuilder) SetContainerComponent(i int, container ContainerC // AddActionRow adds a new discord.ActionRowComponent with the provided discord.InteractiveComponent(s) to the Message func (b *MessageUpdateBuilder) AddActionRow(components ...InteractiveComponent) *MessageUpdateBuilder { if b.Components == nil { - b.Components = new([]ContainerComponent) + b.Components = new([]LayoutComponent) } - *b.Components = append(*b.Components, ActionRowComponent(components)) + *b.Components = append(*b.Components, ActionRowComponent{Components: components}) return b } -// AddContainerComponents adds the discord.ContainerComponent(s) to the Message -func (b *MessageUpdateBuilder) AddContainerComponents(containers ...ContainerComponent) *MessageUpdateBuilder { +// AddLayoutComponents adds the discord.LayoutComponent(s) to the Message +func (b *MessageUpdateBuilder) AddLayoutComponents(containers ...LayoutComponent) *MessageUpdateBuilder { if b.Components == nil { - b.Components = new([]ContainerComponent) + b.Components = new([]LayoutComponent) } *b.Components = append(*b.Components, containers...) return b } -// RemoveContainerComponent removes a discord.ContainerComponent from the Message -func (b *MessageUpdateBuilder) RemoveContainerComponent(i int) *MessageUpdateBuilder { +// RemoveLayoutComponent removes a discord.LayoutComponent from the Message +func (b *MessageUpdateBuilder) RemoveLayoutComponent(i int) *MessageUpdateBuilder { if b.Components == nil { return b } @@ -128,9 +128,9 @@ func (b *MessageUpdateBuilder) RemoveContainerComponent(i int) *MessageUpdateBui return b } -// ClearContainerComponents removes all the discord.ContainerComponent(s) of the Message -func (b *MessageUpdateBuilder) ClearContainerComponents() *MessageUpdateBuilder { - b.Components = &[]ContainerComponent{} +// ClearLayoutComponents removes all the discord.LayoutComponent(s) of the Message +func (b *MessageUpdateBuilder) ClearLayoutComponents() *MessageUpdateBuilder { + b.Components = &[]LayoutComponent{} return b } diff --git a/discord/modal_create.go b/discord/modal_create.go index 21291e362..0edcc6fb3 100644 --- a/discord/modal_create.go +++ b/discord/modal_create.go @@ -3,9 +3,9 @@ package discord var _ InteractionResponseData = (*ModalCreate)(nil) type ModalCreate struct { - CustomID string `json:"custom_id"` - Title string `json:"title"` - Components []ContainerComponent `json:"components"` + CustomID string `json:"custom_id"` + Title string `json:"title"` + Components []LayoutComponent `json:"components"` } func (ModalCreate) interactionCallbackData() {} @@ -31,14 +31,14 @@ func (b *ModalCreateBuilder) SetTitle(title string) *ModalCreateBuilder { return b } -// SetContainerComponents sets the discord.ContainerComponent(s) of the ModalCreate -func (b *ModalCreateBuilder) SetContainerComponents(containerComponents ...ContainerComponent) *ModalCreateBuilder { - b.Components = containerComponents +// SetLayoutComponents sets the discord.LayoutComponent(s) of the ModalCreate +func (b *ModalCreateBuilder) SetLayoutComponents(LayoutComponents ...LayoutComponent) *ModalCreateBuilder { + b.Components = LayoutComponents return b } -// SetContainerComponent sets the provided discord.InteractiveComponent at the index of discord.InteractiveComponent(s) -func (b *ModalCreateBuilder) SetContainerComponent(i int, container ContainerComponent) *ModalCreateBuilder { +// SetLayoutComponent sets the provided discord.InteractiveComponent at the index of discord.InteractiveComponent(s) +func (b *ModalCreateBuilder) SetLayoutComponent(i int, container LayoutComponent) *ModalCreateBuilder { if len(b.Components) > i { b.Components[i] = container } @@ -47,27 +47,27 @@ func (b *ModalCreateBuilder) SetContainerComponent(i int, container ContainerCom // AddActionRow adds a new discord.ActionRowComponent with the provided discord.InteractiveComponent(s) to the ModalCreate func (b *ModalCreateBuilder) AddActionRow(components ...InteractiveComponent) *ModalCreateBuilder { - b.Components = append(b.Components, ActionRowComponent(components)) + b.Components = append(b.Components, ActionRowComponent{Components: components}) return b } -// AddContainerComponents adds the discord.ContainerComponent(s) to the ModalCreate -func (b *ModalCreateBuilder) AddContainerComponents(containers ...ContainerComponent) *ModalCreateBuilder { +// AddLayoutComponents adds the discord.LayoutComponent(s) to the ModalCreate +func (b *ModalCreateBuilder) AddLayoutComponents(containers ...LayoutComponent) *ModalCreateBuilder { b.Components = append(b.Components, containers...) return b } -// RemoveContainerComponent removes a discord.ActionRowComponent from the ModalCreate -func (b *ModalCreateBuilder) RemoveContainerComponent(i int) *ModalCreateBuilder { +// RemoveLayoutComponent removes a discord.ActionRowComponent from the ModalCreate +func (b *ModalCreateBuilder) RemoveLayoutComponent(i int) *ModalCreateBuilder { if len(b.Components) > i { b.Components = append(b.Components[:i], b.Components[i+1:]...) } return b } -// ClearContainerComponents removes all the discord.ContainerComponent(s) of the ModalCreate -func (b *ModalCreateBuilder) ClearContainerComponents() *ModalCreateBuilder { - b.Components = []ContainerComponent{} +// ClearLayoutComponents removes all the discord.LayoutComponent(s) of the ModalCreate +func (b *ModalCreateBuilder) ClearLayoutComponents() *ModalCreateBuilder { + b.Components = []LayoutComponent{} return b } diff --git a/discord/select_menu.go b/discord/select_menu.go index 0c8c94a9c..80fd2db0c 100644 --- a/discord/select_menu.go +++ b/discord/select_menu.go @@ -5,6 +5,13 @@ import ( "github.com/disgoorg/snowflake/v2" ) +// SelectMenuComponent is an interface for all components that are select menus. +// [StringSelectMenuComponent] +// [UserSelectMenuComponent] +// [RoleSelectMenuComponent] +// [MentionableSelectMenuComponent] +// [ChannelSelectMenuComponent] +// [UnknownComponent] type SelectMenuComponent interface { InteractiveComponent selectMenuComponent() diff --git a/discord/webhook_message_create.go b/discord/webhook_message_create.go index f84e4ea8d..7f7ac78ce 100644 --- a/discord/webhook_message_create.go +++ b/discord/webhook_message_create.go @@ -3,19 +3,19 @@ package discord import "github.com/disgoorg/snowflake/v2" type WebhookMessageCreate struct { - Content string `json:"content,omitempty"` - Username string `json:"username,omitempty"` - AvatarURL string `json:"avatar_url,omitempty"` - TTS bool `json:"tts,omitempty"` - Embeds []Embed `json:"embeds,omitempty"` - Components []ContainerComponent `json:"components,omitempty"` - Attachments []AttachmentCreate `json:"attachments,omitempty"` - Files []*File `json:"-"` - AllowedMentions *AllowedMentions `json:"allowed_mentions,omitempty"` - Flags MessageFlags `json:"flags,omitempty"` - ThreadName string `json:"thread_name,omitempty"` - AppliedTags []snowflake.ID `json:"applied_tags,omitempty"` - Poll *PollCreate `json:"poll,omitempty"` + Content string `json:"content,omitempty"` + Username string `json:"username,omitempty"` + AvatarURL string `json:"avatar_url,omitempty"` + TTS bool `json:"tts,omitempty"` + Embeds []Embed `json:"embeds,omitempty"` + Components []LayoutComponent `json:"components,omitempty"` + Attachments []AttachmentCreate `json:"attachments,omitempty"` + Files []*File `json:"-"` + AllowedMentions *AllowedMentions `json:"allowed_mentions,omitempty"` + Flags MessageFlags `json:"flags,omitempty"` + ThreadName string `json:"thread_name,omitempty"` + AppliedTags []snowflake.ID `json:"applied_tags,omitempty"` + Poll *PollCreate `json:"poll,omitempty"` } // ToBody returns the MessageCreate ready for body diff --git a/discord/webhook_message_create_builder.go b/discord/webhook_message_create_builder.go index 4c8666959..5a81b9141 100644 --- a/discord/webhook_message_create_builder.go +++ b/discord/webhook_message_create_builder.go @@ -80,14 +80,14 @@ func (b *WebhookMessageCreateBuilder) RemoveEmbed(i int) *WebhookMessageCreateBu return b } -// SetContainerComponents sets the discord.ContainerComponent(s) of the Message -func (b *WebhookMessageCreateBuilder) SetContainerComponents(containerComponents ...ContainerComponent) *WebhookMessageCreateBuilder { - b.Components = containerComponents +// SetLayoutComponents sets the discord.LayoutComponent(s) of the Message +func (b *WebhookMessageCreateBuilder) SetLayoutComponents(LayoutComponents ...LayoutComponent) *WebhookMessageCreateBuilder { + b.Components = LayoutComponents return b } -// SetContainerComponent sets the provided discord.InteractiveComponent at the index of discord.InteractiveComponent(s) -func (b *WebhookMessageCreateBuilder) SetContainerComponent(i int, container ContainerComponent) *WebhookMessageCreateBuilder { +// SetLayoutComponent sets the provided discord.InteractiveComponent at the index of discord.InteractiveComponent(s) +func (b *WebhookMessageCreateBuilder) SetLayoutComponent(i int, container LayoutComponent) *WebhookMessageCreateBuilder { if len(b.Components) > i { b.Components[i] = container } @@ -96,27 +96,27 @@ func (b *WebhookMessageCreateBuilder) SetContainerComponent(i int, container Con // AddActionRow adds a new discord.ActionRowComponent with the provided discord.InteractiveComponent(s) to the Message func (b *WebhookMessageCreateBuilder) AddActionRow(components ...InteractiveComponent) *WebhookMessageCreateBuilder { - b.Components = append(b.Components, ActionRowComponent(components)) + b.Components = append(b.Components, ActionRowComponent{Components: components}) return b } -// AddContainerComponents adds the discord.ContainerComponent(s) to the Message -func (b *WebhookMessageCreateBuilder) AddContainerComponents(containers ...ContainerComponent) *WebhookMessageCreateBuilder { +// AddLayoutComponents adds the discord.LayoutComponent(s) to the Message +func (b *WebhookMessageCreateBuilder) AddLayoutComponents(containers ...LayoutComponent) *WebhookMessageCreateBuilder { b.Components = append(b.Components, containers...) return b } -// RemoveContainerComponent removes a discord.ActionRowComponent from the Message -func (b *WebhookMessageCreateBuilder) RemoveContainerComponent(i int) *WebhookMessageCreateBuilder { +// RemoveLayoutComponent removes a discord.ActionRowComponent from the Message +func (b *WebhookMessageCreateBuilder) RemoveLayoutComponent(i int) *WebhookMessageCreateBuilder { if len(b.Components) > i { b.Components = append(b.Components[:i], b.Components[i+1:]...) } return b } -// ClearContainerComponents removes all the discord.ContainerComponent(s) of the Message -func (b *WebhookMessageCreateBuilder) ClearContainerComponents() *WebhookMessageCreateBuilder { - b.Components = []ContainerComponent{} +// ClearLayoutComponents removes all the discord.LayoutComponent(s) of the Message +func (b *WebhookMessageCreateBuilder) ClearLayoutComponents() *WebhookMessageCreateBuilder { + b.Components = []LayoutComponent{} return b } diff --git a/discord/webhook_message_update.go b/discord/webhook_message_update.go index c756d1d39..5e92758e2 100644 --- a/discord/webhook_message_update.go +++ b/discord/webhook_message_update.go @@ -2,13 +2,13 @@ package discord // WebhookMessageUpdate is used to edit a Message type WebhookMessageUpdate struct { - Content *string `json:"content,omitempty"` - Embeds *[]Embed `json:"embeds,omitempty"` - Components *[]ContainerComponent `json:"components,omitempty"` - Attachments *[]AttachmentUpdate `json:"attachments,omitempty"` - Files []*File `json:"-"` - AllowedMentions *AllowedMentions `json:"allowed_mentions,omitempty"` - Poll *PollCreate `json:"poll,omitempty"` + Content *string `json:"content,omitempty"` + Embeds *[]Embed `json:"embeds,omitempty"` + Components *[]LayoutComponent `json:"components,omitempty"` + Attachments *[]AttachmentUpdate `json:"attachments,omitempty"` + Files []*File `json:"-"` + AllowedMentions *AllowedMentions `json:"allowed_mentions,omitempty"` + Poll *PollCreate `json:"poll,omitempty"` } // ToBody returns the WebhookMessageUpdate ready for body diff --git a/discord/webhook_message_update_builder.go b/discord/webhook_message_update_builder.go index b031fa642..4c2fe7007 100644 --- a/discord/webhook_message_update_builder.go +++ b/discord/webhook_message_update_builder.go @@ -83,19 +83,19 @@ func (b *WebhookMessageUpdateBuilder) RemoveEmbed(i int) *WebhookMessageUpdateBu return b } -// SetContainerComponents sets the discord.ContainerComponent(s) of the Message -func (b *WebhookMessageUpdateBuilder) SetContainerComponents(containerComponents ...ContainerComponent) *WebhookMessageUpdateBuilder { +// SetLayoutComponents sets the discord.LayoutComponent(s) of the Message +func (b *WebhookMessageUpdateBuilder) SetLayoutComponents(LayoutComponents ...LayoutComponent) *WebhookMessageUpdateBuilder { if b.Components == nil { - b.Components = new([]ContainerComponent) + b.Components = new([]LayoutComponent) } - *b.Components = containerComponents + *b.Components = LayoutComponents return b } -// SetContainerComponent sets the provided discord.InteractiveComponent at the index of discord.InteractiveComponent(s) -func (b *WebhookMessageUpdateBuilder) SetContainerComponent(i int, container ContainerComponent) *WebhookMessageUpdateBuilder { +// SetLayoutComponent sets the provided discord.InteractiveComponent at the index of discord.InteractiveComponent(s) +func (b *WebhookMessageUpdateBuilder) SetLayoutComponent(i int, container LayoutComponent) *WebhookMessageUpdateBuilder { if b.Components == nil { - b.Components = new([]ContainerComponent) + b.Components = new([]LayoutComponent) } if len(*b.Components) > i { (*b.Components)[i] = container @@ -106,23 +106,23 @@ func (b *WebhookMessageUpdateBuilder) SetContainerComponent(i int, container Con // AddActionRow adds a new discord.ActionRowComponent with the provided discord.InteractiveComponent(s) to the Message func (b *WebhookMessageUpdateBuilder) AddActionRow(components ...InteractiveComponent) *WebhookMessageUpdateBuilder { if b.Components == nil { - b.Components = new([]ContainerComponent) + b.Components = new([]LayoutComponent) } - *b.Components = append(*b.Components, ActionRowComponent(components)) + *b.Components = append(*b.Components, ActionRowComponent{Components: components}) return b } -// AddContainerComponents adds the discord.ContainerComponent(s) to the Message -func (b *WebhookMessageUpdateBuilder) AddContainerComponents(containers ...ContainerComponent) *WebhookMessageUpdateBuilder { +// AddLayoutComponents adds the discord.LayoutComponent(s) to the Message +func (b *WebhookMessageUpdateBuilder) AddLayoutComponents(containers ...LayoutComponent) *WebhookMessageUpdateBuilder { if b.Components == nil { - b.Components = new([]ContainerComponent) + b.Components = new([]LayoutComponent) } *b.Components = append(*b.Components, containers...) return b } -// RemoveContainerComponent removes a discord.ContainerComponent from the Message -func (b *WebhookMessageUpdateBuilder) RemoveContainerComponent(i int) *WebhookMessageUpdateBuilder { +// RemoveLayoutComponent removes a discord.LayoutComponent from the Message +func (b *WebhookMessageUpdateBuilder) RemoveLayoutComponent(i int) *WebhookMessageUpdateBuilder { if b.Components == nil { return b } @@ -132,9 +132,9 @@ func (b *WebhookMessageUpdateBuilder) RemoveContainerComponent(i int) *WebhookMe return b } -// ClearContainerComponents removes all the discord.ContainerComponent(s) of the Message -func (b *WebhookMessageUpdateBuilder) ClearContainerComponents() *WebhookMessageUpdateBuilder { - b.Components = &[]ContainerComponent{} +// ClearLayoutComponents removes all the discord.LayoutComponent(s) of the Message +func (b *WebhookMessageUpdateBuilder) ClearLayoutComponents() *WebhookMessageUpdateBuilder { + b.Components = &[]LayoutComponent{} return b } From a4c16e4ef3c1403da34f1f6b293f13c55a9d1125 Mon Sep 17 00:00:00 2001 From: topi314 Date: Mon, 3 Mar 2025 23:36:32 +0100 Subject: [PATCH 4/5] add missing union interface and add iter over all components in a message/modal --- discord/component.go | 260 +++++++++++++++++++++++----- discord/component_iter.go | 24 +++ discord/component_iter_test.go | 39 +++++ discord/interaction_modal_submit.go | 25 +-- discord/message.go | 10 ++ discord/select_menu.go | 5 + 6 files changed, 306 insertions(+), 57 deletions(-) create mode 100644 discord/component_iter.go create mode 100644 discord/component_iter_test.go diff --git a/discord/component.go b/discord/component.go index abeac40a6..536c29aac 100644 --- a/discord/component.go +++ b/discord/component.go @@ -2,6 +2,7 @@ package discord import ( "fmt" + "iter" "github.com/disgoorg/json" "github.com/disgoorg/snowflake/v2" @@ -56,6 +57,11 @@ type Component interface { component() } +// ComponentIter is an optional interface a Component can implement to return an iterator over its children. +type ComponentIter interface { + Children() iter.Seq[Component] +} + // InteractiveComponent is an interface for all components that can be present in an [ActionRowComponent]. // [ButtonComponent] // [StringSelectMenuComponent] @@ -89,6 +95,24 @@ type LayoutComponent interface { layoutComponent() } +// SectionSubComponent is an interface for all components that can be present in a [SectionComponent]. +// [TextDisplayComponent] +// [UnknownComponent] +type SectionSubComponent interface { + Component + // sectionSubComponent is a marker to simulate unions. + sectionSubComponent() +} + +// SectionAccessoryComponent is an interface for all components that can be present as an accessory in [SectionComponent.Accessory]. +// [ThumbnailComponent] +// [UnknownComponent] +type SectionAccessoryComponent interface { + Component + // sectionAccessoryComponent is a marker to simulate unions. + sectionAccessoryComponent() +} + // ContainerSubComponent is an interface for all components that can be present in a [ContainerComponent]. // [ActionRowComponent] // [SectionComponent] @@ -218,6 +242,7 @@ var ( _ Component = (*ActionRowComponent)(nil) _ LayoutComponent = (*ActionRowComponent)(nil) _ ContainerSubComponent = (*ActionRowComponent)(nil) + _ ComponentIter = (*ActionRowComponent)(nil) ) func NewActionRow(components ...InteractiveComponent) ActionRowComponent { @@ -272,37 +297,19 @@ func (ActionRowComponent) component() {} func (ActionRowComponent) layoutComponent() {} func (ActionRowComponent) containerSubComponent() {} -// Buttons returns all ButtonComponent(s) in the ActionRowComponent. -func (c ActionRowComponent) Buttons() []ButtonComponent { - var buttons []ButtonComponent - for i := range c.Components { - if button, ok := c.Components[i].(ButtonComponent); ok { - buttons = append(buttons, button) +func (c ActionRowComponent) Children() iter.Seq[Component] { + return func(yield func(Component) bool) { + for _, cc := range c.Components { + if !yield(cc) { + return + } } } - return buttons } -// SelectMenus returns all SelectMenuComponent(s) in the ActionRowComponent. -func (c ActionRowComponent) SelectMenus() []SelectMenuComponent { - var selectMenus []SelectMenuComponent - for i := range c.Components { - if selectMenu, ok := c.Components[i].(SelectMenuComponent); ok { - selectMenus = append(selectMenus, selectMenu) - } - } - return selectMenus -} - -// TextInputs returns all TextInputComponent(s) in the ActionRowComponent. -func (c ActionRowComponent) TextInputs() []TextInputComponent { - var textInputs []TextInputComponent - for i := range c.Components { - if textInput, ok := c.Components[i].(TextInputComponent); ok { - textInputs = append(textInputs, textInput) - } - } - return textInputs +func (c ActionRowComponent) WithID(id int) ActionRowComponent { + c.ID = id + return c } // UpdateComponent returns a new ActionRowComponent with the Component which has the id replaced with the provided Component. @@ -413,7 +420,9 @@ func NewPremiumButton(skuID snowflake.ID) ButtonComponent { } var ( - _ Component = (*ButtonComponent)(nil) + _ Component = (*ButtonComponent)(nil) + _ InteractiveComponent = (*ButtonComponent)(nil) + _ SectionAccessoryComponent = (*ButtonComponent)(nil) ) type ButtonComponent struct { @@ -450,8 +459,15 @@ func (c ButtonComponent) GetCustomID() string { return c.CustomID } -func (ButtonComponent) component() {} -func (ButtonComponent) interactiveComponent() {} +func (ButtonComponent) component() {} +func (ButtonComponent) interactiveComponent() {} +func (ButtonComponent) sectionAccessoryComponent() {} + +// WithID returns a new ButtonComponent with the provided id +func (c ButtonComponent) WithID(id int) ButtonComponent { + c.ID = id + return c +} // WithStyle returns a new ButtonComponent with the provided style func (c ButtonComponent) WithStyle(style ButtonStyle) ButtonComponent { @@ -571,6 +587,12 @@ func (c TextInputComponent) GetCustomID() string { func (TextInputComponent) component() {} func (TextInputComponent) interactiveComponent() {} +// WithID returns a new TextInputComponent with the provided id +func (c TextInputComponent) WithID(id int) TextInputComponent { + c.ID = id + return c +} + // WithCustomID returns a new SelectMenuComponent with the provided customID func (c TextInputComponent) WithCustomID(customID string) TextInputComponent { c.CustomID = customID @@ -631,10 +653,16 @@ var ( _ ContainerSubComponent = (*SectionComponent)(nil) ) +func NewSection(components ...SectionSubComponent) SectionComponent { + return SectionComponent{ + Components: components, + } +} + type SectionComponent struct { - ID int `json:"id,omitempty"` - Components []Component `json:"components"` - Accessory Component `json:"accessory"` + ID int `json:"id,omitempty"` + Components []SectionSubComponent `json:"components"` + Accessory SectionAccessoryComponent `json:"accessory"` } func (c SectionComponent) MarshalJSON() ([]byte, error) { @@ -648,6 +676,28 @@ func (c SectionComponent) MarshalJSON() ([]byte, error) { }) } +func (c *SectionComponent) UnmarshalJSON(data []byte) error { + var sectionComponent struct { + ID int `json:"id,omitempty"` + Components []UnmarshalComponent `json:"components"` + Accessory UnmarshalComponent `json:"accessory"` + } + if err := json.Unmarshal(data, §ionComponent); err != nil { + return err + } + + c.ID = sectionComponent.ID + + components := make([]SectionSubComponent, 0, len(sectionComponent.Components)) + for _, component := range sectionComponent.Components { + components = append(components, component.Component.(SectionSubComponent)) + } + c.Components = components + + c.Accessory = sectionComponent.Accessory.Component.(SectionAccessoryComponent) + return nil +} + func (SectionComponent) Type() ComponentType { return ComponentTypeSection } @@ -656,19 +706,43 @@ func (c SectionComponent) GetID() int { return c.ID } -func (c SectionComponent) GetComponents() []Component { - return c.Components -} - func (SectionComponent) component() {} func (SectionComponent) layoutComponent() {} func (SectionComponent) containerSubComponent() {} +func (c SectionComponent) Children() iter.Seq[Component] { + return func(yield func(Component) bool) { + for _, cc := range c.Components { + if !yield(cc) { + return + } + } + + if c.Accessory != nil { + if !yield(c.Accessory) { + return + } + } + } +} + +func (c SectionComponent) WithID(id int) SectionComponent { + c.ID = id + return c +} + var ( _ Component = (*TextDisplayComponent)(nil) _ ContainerSubComponent = (*TextDisplayComponent)(nil) + _ SectionSubComponent = (*TextDisplayComponent)(nil) ) +func NewTextDisplay(content string) TextDisplayComponent { + return TextDisplayComponent{ + Content: content, + } +} + // TextDisplayComponent is a component that displays text. type TextDisplayComponent struct { ID int `json:"id,omitempty"` @@ -695,8 +769,14 @@ func (c TextDisplayComponent) GetID() int { } func (TextDisplayComponent) component() {} +func (TextDisplayComponent) sectionSubComponent() {} func (TextDisplayComponent) containerSubComponent() {} +func (c TextDisplayComponent) WithID(i int) SectionSubComponent { + c.ID = i + return c +} + var ( _ Component = (*ThumbnailComponent)(nil) ) @@ -729,6 +809,11 @@ func (c ThumbnailComponent) GetID() int { func (ThumbnailComponent) component() {} +func (c ThumbnailComponent) WithID(id int) ThumbnailComponent { + c.ID = id + return c +} + type MediaGalleryItem struct { Media UnfurledMediaItem `json:"media"` Description string `json:"description,omitempty"` @@ -741,6 +826,12 @@ var ( _ ContainerSubComponent = (*MediaGalleryComponent)(nil) ) +func NewMediaGallery(items ...MediaGalleryItem) MediaGalleryComponent { + return MediaGalleryComponent{ + Items: items, + } +} + type MediaGalleryComponent struct { ID int `json:"id,omitempty"` Items []MediaGalleryItem `json:"items"` @@ -769,6 +860,11 @@ func (MediaGalleryComponent) component() {} func (MediaGalleryComponent) layoutComponent() {} func (MediaGalleryComponent) containerSubComponent() {} +func (c MediaGalleryComponent) WithID(id int) MediaGalleryComponent { + c.ID = id + return c +} + type SeparatorSpacingSize int const ( @@ -783,6 +879,20 @@ var ( _ ContainerSubComponent = (*MediaGalleryComponent)(nil) ) +func NewSeparator(spacing SeparatorSpacingSize) SeparatorComponent { + return SeparatorComponent{ + Spacing: spacing, + } +} + +func NewSmallSeparator() SeparatorComponent { + return NewSeparator(SeparatorSpacingSizeSmall) +} + +func NewLargeSeparator() SeparatorComponent { + return NewSeparator(SeparatorSpacingSizeLarge) +} + type SeparatorComponent struct { ID int `json:"id,omitempty"` Divider bool `json:"divider,omitempty"` @@ -812,10 +922,24 @@ func (SeparatorComponent) component() {} func (SeparatorComponent) layoutComponent() {} func (SeparatorComponent) containerSubComponent() {} +func (c SeparatorComponent) WithID(i int) LayoutComponent { + c.ID = i + return c +} + var ( _ Component = (*FileComponent)(nil) ) +func NewFileComponent(url string, spoiler bool) FileComponent { + return FileComponent{ + File: UnfurledMediaItem{ + URL: url, + }, + Spoiler: spoiler, + } +} + type FileComponent struct { ID int `json:"id,omitempty"` // File only supports attachment:// references @@ -844,11 +968,27 @@ func (c FileComponent) GetID() int { func (FileComponent) component() {} +func (c FileComponent) WithID(id int) FileComponent { + c.ID = id + return c +} + +func (c FileComponent) WithSpoiler(spoiler bool) FileComponent { + c.Spoiler = spoiler + return c +} + var ( _ Component = (*ContainerComponent)(nil) _ LayoutComponent = (*ContainerComponent)(nil) ) +func NewContainer(components ...ContainerSubComponent) ContainerComponent { + return ContainerComponent{ + Components: components, + } +} + type ContainerComponent struct { ID int `json:"id,omitempty"` AccentColor *int `json:"accent_color,omitempty"` @@ -901,12 +1041,36 @@ func (c ContainerComponent) GetID() int { func (ContainerComponent) component() {} func (ContainerComponent) layoutComponent() {} +func (c ContainerComponent) WithID(id int) ContainerComponent { + c.ID = id + return c +} + +func (c ContainerComponent) Children() iter.Seq[Component] { + return func(yield func(Component) bool) { + for _, cc := range c.Components { + if !yield(cc) { + return + } + if ic, ok := cc.(ComponentIter); ok { + for cc := range ic.Children() { + if !yield(cc) { + return + } + } + } + } + } +} + var ( - _ Component = (*UnknownComponent)(nil) - _ InteractiveComponent = (*UnknownComponent)(nil) - _ LayoutComponent = (*UnknownComponent)(nil) - _ SelectMenuComponent = (*UnknownComponent)(nil) - _ ContainerSubComponent = (*UnknownComponent)(nil) + _ Component = (*UnknownComponent)(nil) + _ InteractiveComponent = (*UnknownComponent)(nil) + _ LayoutComponent = (*UnknownComponent)(nil) + _ SelectMenuComponent = (*UnknownComponent)(nil) + _ SectionSubComponent = (*UnknownComponent)(nil) + _ SectionAccessoryComponent = (*UnknownComponent)(nil) + _ ContainerSubComponent = (*UnknownComponent)(nil) ) type UnknownComponent struct { @@ -964,8 +1128,10 @@ func (c UnknownComponent) GetCustomID() string { return data.CustomID } -func (UnknownComponent) component() {} -func (UnknownComponent) interactiveComponent() {} -func (UnknownComponent) layoutComponent() {} -func (UnknownComponent) selectMenuComponent() {} -func (UnknownComponent) containerSubComponent() {} +func (UnknownComponent) component() {} +func (UnknownComponent) interactiveComponent() {} +func (UnknownComponent) layoutComponent() {} +func (UnknownComponent) selectMenuComponent() {} +func (UnknownComponent) containerSubComponent() {} +func (UnknownComponent) sectionSubComponent() {} +func (UnknownComponent) sectionAccessoryComponent() {} diff --git a/discord/component_iter.go b/discord/component_iter.go new file mode 100644 index 000000000..b501901bf --- /dev/null +++ b/discord/component_iter.go @@ -0,0 +1,24 @@ +package discord + +import "iter" + +// componentIter returns an iterator over the given components and their children. +func componentIter(components []LayoutComponent) iter.Seq[Component] { + return func(yield func(Component) bool) { + for _, c := range components { + if !yield(c) { + return + } + ic, ok := c.(ComponentIter) + if !ok { + continue + } + + for cc := range ic.Children() { + if !yield(cc) { + return + } + } + } + } +} diff --git a/discord/component_iter_test.go b/discord/component_iter_test.go new file mode 100644 index 000000000..e9d821d13 --- /dev/null +++ b/discord/component_iter_test.go @@ -0,0 +1,39 @@ +package discord + +import "testing" + +func TestComponentIter(t *testing.T) { + components := []LayoutComponent{ + NewActionRow( + NewPrimaryButton("button1", "button1").WithID(2), + NewSecondaryButton("button2", "button2").WithID(3), + ).WithID(1), + NewSmallSeparator().WithID(4), + NewActionRow( + NewStringSelectMenu("select1", "select1", + StringSelectMenuOption{Label: "option1", Value: "option1"}, + StringSelectMenuOption{Label: "option2", Value: "option21"}, + ).WithID(6), + ).WithID(5), + NewSection( + NewTextDisplay("text1").WithID(8), + NewTextDisplay("text2").WithID(9), + ).WithID(7), + NewContainer( + NewActionRow( + NewPrimaryButton("button2", "button2").WithID(12), + ).WithID(11), + NewSection( + NewTextDisplay("text3").WithID(14), + ).WithID(13), + ).WithID(10), + } + + i := 1 + for c := range componentIter(components) { + if c.GetID() != i { + t.Errorf("expected component with ID %d, got %d", i, c.GetID()) + } + i++ + } +} diff --git a/discord/interaction_modal_submit.go b/discord/interaction_modal_submit.go index daf21f254..ef0050d9d 100644 --- a/discord/interaction_modal_submit.go +++ b/discord/interaction_modal_submit.go @@ -76,7 +76,8 @@ func (ModalSubmitInteraction) Type() InteractionType { func (ModalSubmitInteraction) interaction() {} type ModalSubmitInteractionData struct { - CustomID string `json:"custom_id"` + CustomID string `json:"custom_id"` + // TODO: rethink this since you might want to access other components. Components map[string]InteractiveComponent `json:"components"` } @@ -93,15 +94,19 @@ func (d *ModalSubmitInteractionData) UnmarshalJSON(data []byte) error { *d = ModalSubmitInteractionData(iData.modalSubmitInteractionData) - // TODO: implement component iterator once we bumped go version to 1.24 - //if len(iData.Components) > 0 { - // d.Components = make(map[string]InteractiveComponent, len(iData.Components)) - // for _, containerComponent := range iData.Components { - // for _, component := range containerComponent.Component.(ContainerComponent).Components() { - // d.Components[component.ID()] = component - // } - // } - //} + if len(iData.Components) > 0 { + d.Components = make(map[string]InteractiveComponent) + + var layoutComponents []LayoutComponent + for _, containerComponent := range iData.Components { + layoutComponents = append(layoutComponents, containerComponent.Component.(LayoutComponent)) + } + for component := range componentIter(layoutComponents) { + if ic, ok := component.(InteractiveComponent); ok { + d.Components[ic.GetCustomID()] = ic + } + } + } return nil } diff --git a/discord/message.go b/discord/message.go index d47a35622..eaa52f4dd 100644 --- a/discord/message.go +++ b/discord/message.go @@ -3,6 +3,7 @@ package discord import ( "bytes" "fmt" + "iter" "strconv" "time" @@ -159,6 +160,11 @@ func (m *Message) UnmarshalJSON(data []byte) error { return nil } +// AllComponents returns an [iter.Seq] of all components in the message. +func (m Message) AllComponents() iter.Seq[Component] { + return componentIter(m.Components) +} + // JumpURL returns the URL which can be used to jump to the message in the discord client. func (m Message) JumpURL() string { guildID := "@me" @@ -290,6 +296,10 @@ func (m *PartialMessage) UnmarshalJSON(data []byte) error { return nil } +func (m PartialMessage) AllComponents() iter.Seq[Component] { + return componentIter(m.Components) +} + // MessageInteraction is sent on the Message object when the message is a response to an interaction type MessageInteraction struct { ID snowflake.ID `json:"id"` diff --git a/discord/select_menu.go b/discord/select_menu.go index 80fd2db0c..f3238990a 100644 --- a/discord/select_menu.go +++ b/discord/select_menu.go @@ -142,6 +142,11 @@ func (c StringSelectMenuComponent) RemoveOption(index int) StringSelectMenuCompo return c } +func (c StringSelectMenuComponent) WithID(i int) InteractiveComponent { + c.ID = i + return c +} + // NewStringSelectMenuOption builds a new StringSelectMenuOption func NewStringSelectMenuOption(label string, value string) StringSelectMenuOption { return StringSelectMenuOption{ From bac98377b52ac46e250a51e91a7b9ec8111ba0de Mon Sep 17 00:00:00 2001 From: topi314 Date: Tue, 4 Mar 2025 18:21:54 +0100 Subject: [PATCH 5/5] add missing interface implementations & With methods --- _examples/componentsv2/example.go | 86 ++++-- _examples/componentsv2/thumbnail.jpg | Bin 0 -> 35953 bytes _examples/test/examplebot.go | 3 +- discord/component.go | 326 +++++++++++++++++----- discord/component_iter.go | 2 +- discord/interaction_modal_submit.go | 44 +-- discord/message_create_builder.go | 30 +- discord/message_update_builder.go | 20 +- discord/modal_create.go | 20 +- discord/webhook_message_create_builder.go | 30 +- discord/webhook_message_update_builder.go | 20 +- 11 files changed, 422 insertions(+), 159 deletions(-) create mode 100644 _examples/componentsv2/thumbnail.jpg diff --git a/_examples/componentsv2/example.go b/_examples/componentsv2/example.go index 0722db7b4..71557db5c 100644 --- a/_examples/componentsv2/example.go +++ b/_examples/componentsv2/example.go @@ -1,48 +1,53 @@ package main import ( + "bytes" "context" + _ "embed" "log/slog" "os" "os/signal" "syscall" + "github.com/disgoorg/snowflake/v2" + "github.com/disgoorg/disgo" "github.com/disgoorg/disgo/bot" "github.com/disgoorg/disgo/discord" "github.com/disgoorg/disgo/events" - "github.com/disgoorg/disgo/gateway" - "github.com/disgoorg/snowflake/v2" + "github.com/disgoorg/disgo/handler" ) var ( token = os.Getenv("disgo_token") guildID = snowflake.GetEnv("disgo_guild_id") + + //go:embed thumbnail.jpg + thumbnail []byte + + commands = []discord.ApplicationCommandCreate{ + discord.SlashCommandCreate{ + Name: "test", + Description: "test", + Options: []discord.ApplicationCommandOption{ + discord.ApplicationCommandOptionBool{ + Name: "ephemeral", + Description: "if the message should be ephemeral", + Required: false, + }, + }, + }, + } ) func main() { slog.Info("starting example...") slog.Info("disgo version", slog.String("version", disgo.Version)) + slog.SetLogLoggerLevel(slog.LevelDebug) client, err := disgo.New(token, - bot.WithGatewayConfigOpts(gateway.WithIntents(gateway.IntentGuilds, gateway.IntentGuildMessages, gateway.IntentDirectMessages)), - bot.WithEventListenerFunc(func(event *events.MessageCreate) { - if event.Message.Author.Bot || event.Message.Author.System { - return - } - if event.Message.Content == "test" { - _, _ = event.Client().Rest().CreateMessage(event.ChannelID, discord.NewMessageCreateBuilder(). - AddActionRow(discord.NewDangerButton("danger", "danger")). - SetMessageReferenceByID(event.Message.ID). - Build(), - ) - } - }), - bot.WithEventListenerFunc(func(event *events.ComponentInteractionCreate) { - if event.ButtonInteractionData().CustomID() == "danger" { - _ = event.CreateMessage(discord.NewMessageCreateBuilder().SetEphemeral(true).SetContent("Ey that was danger").Build()) - } - }), + bot.WithDefaultGateway(), + bot.WithEventListenerFunc(onCommand), ) if err != nil { slog.Error("error while building bot", slog.Any("err", err)) @@ -50,6 +55,10 @@ func main() { } defer client.Close(context.TODO()) + if err = handler.SyncCommands(client, commands, []snowflake.ID{guildID}); err != nil { + slog.Error("error while syncing commands", slog.Any("err", err)) + } + if err = client.OpenGateway(context.TODO()); err != nil { slog.Error("error while connecting to gateway", slog.Any("err", err)) return @@ -60,3 +69,40 @@ func main() { signal.Notify(s, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) <-s } + +func onCommand(e *events.ApplicationCommandInteractionCreate) { + switch data := e.Data.(type) { + case discord.SlashCommandInteractionData: + flags := discord.MessageFlagIsComponentsV2 + if ephemeral, ok := data.OptBool("ephemeral"); !ok || ephemeral { + flags = flags.Add(discord.MessageFlagEphemeral) + } + if err := e.CreateMessage(discord.MessageCreate{ + Flags: flags, + Components: []discord.LayoutComponent{ + discord.NewContainer( + discord.NewSection( + discord.NewTextDisplay("**Name: [Seeing Red](https://open.spotify.com/track/65qBr6ToDUjTD1RiE1H4Gl)**"), + discord.NewTextDisplay("**Artist: [Architects](https://open.spotify.com/artist/3ZztVuWxHzNpl0THurTFCv)**"), + discord.NewTextDisplay("**Album: [The Sky, The Earth & All Between](https://open.spotify.com/album/2W82VyyIFAXigJEiLm5TT1)**"), + ).WithAccessory(discord.NewThumbnail("attachment://thumbnail.png")), + discord.NewTextDisplay("`0:08`/`3:40`"), + discord.NewTextDisplay("[🔘▬▬▬▬▬▬▬▬▬]"), + discord.NewSmallSeparator(), + discord.NewActionRow( + discord.NewPrimaryButton("", "/player/previous").WithEmoji(discord.ComponentEmoji{Name: "⏮"}), + discord.NewPrimaryButton("", "/player/pause_play").WithEmoji(discord.ComponentEmoji{Name: "⏯"}), + discord.NewPrimaryButton("", "/player/next").WithEmoji(discord.ComponentEmoji{Name: "⏭"}), + discord.NewDangerButton("", "/player/stop").WithEmoji(discord.ComponentEmoji{Name: "⏹"}), + discord.NewPrimaryButton("", "/player/like").WithEmoji(discord.ComponentEmoji{Name: "❤️"}), + ), + ).WithAccentColor(0x5c5fea), + }, + Files: []*discord.File{ + discord.NewFile("thumbnail.png", "", bytes.NewReader(thumbnail)), + }, + }); err != nil { + slog.Error("error while sending message", slog.Any("err", err)) + } + } +} diff --git a/_examples/componentsv2/thumbnail.jpg b/_examples/componentsv2/thumbnail.jpg new file mode 100644 index 0000000000000000000000000000000000000000..68a49cf63cf1de461444709b59e4e6afa1587370 GIT binary patch literal 35953 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 Ij$FV0*?I%#H2?qr literal 0 HcmV?d00001 diff --git a/_examples/test/examplebot.go b/_examples/test/examplebot.go index 0d72c9776..4cf513a2c 100644 --- a/_examples/test/examplebot.go +++ b/_examples/test/examplebot.go @@ -8,12 +8,13 @@ import ( "os/signal" "syscall" + "github.com/disgoorg/snowflake/v2" + "github.com/disgoorg/disgo" "github.com/disgoorg/disgo/bot" "github.com/disgoorg/disgo/cache" "github.com/disgoorg/disgo/discord" "github.com/disgoorg/disgo/gateway" - "github.com/disgoorg/snowflake/v2" ) var ( diff --git a/discord/component.go b/discord/component.go index 536c29aac..5e7dad7a4 100644 --- a/discord/component.go +++ b/discord/component.go @@ -27,6 +27,9 @@ const ( ComponentTypeMediaGallery ComponentTypeFile ComponentTypeSeparator + _ + // ComponentTypeContentInventoryEntry cannot be used by bots. + ComponentTypeContentInventoryEntry ComponentTypeContainer ) @@ -57,9 +60,9 @@ type Component interface { component() } -// ComponentIter is an optional interface a Component can implement to return an iterator over its children. +// ComponentIter is an optional interface a Component can implement to return an iterator over its sub components. type ComponentIter interface { - Children() iter.Seq[Component] + SubComponents() iter.Seq[Component] } // InteractiveComponent is an interface for all components that can be present in an [ActionRowComponent]. @@ -105,6 +108,7 @@ type SectionSubComponent interface { } // SectionAccessoryComponent is an interface for all components that can be present as an accessory in [SectionComponent.Accessory]. +// [ButtonComponent] // [ThumbnailComponent] // [UnknownComponent] type SectionAccessoryComponent interface { @@ -238,6 +242,12 @@ type ComponentEmoji struct { Animated bool `json:"animated,omitempty"` } +func NewActionRow(components ...InteractiveComponent) ActionRowComponent { + return ActionRowComponent{ + Components: components, + } +} + var ( _ Component = (*ActionRowComponent)(nil) _ LayoutComponent = (*ActionRowComponent)(nil) @@ -245,12 +255,6 @@ var ( _ ComponentIter = (*ActionRowComponent)(nil) ) -func NewActionRow(components ...InteractiveComponent) ActionRowComponent { - return ActionRowComponent{ - Components: components, - } -} - type ActionRowComponent struct { ID int `json:"id,omitempty"` Components []InteractiveComponent `json:"components"` @@ -297,7 +301,8 @@ func (ActionRowComponent) component() {} func (ActionRowComponent) layoutComponent() {} func (ActionRowComponent) containerSubComponent() {} -func (c ActionRowComponent) Children() iter.Seq[Component] { +// SubComponents returns an [iter.Seq[Component]] over the sub Component(s) of the ActionRowComponent. +func (c ActionRowComponent) SubComponents() iter.Seq[Component] { return func(yield func(Component) bool) { for _, cc := range c.Components { if !yield(cc) { @@ -307,11 +312,18 @@ func (c ActionRowComponent) Children() iter.Seq[Component] { } } +// WithID returns a new ActionRowComponent with the provided id func (c ActionRowComponent) WithID(id int) ActionRowComponent { c.ID = id return c } +// WithComponents returns a new ActionRowComponent with the provided Component(s) +func (c ActionRowComponent) WithComponents(components ...InteractiveComponent) ActionRowComponent { + c.Components = components + return c +} + // UpdateComponent returns a new ActionRowComponent with the Component which has the id replaced with the provided Component. func (c ActionRowComponent) UpdateComponent(id int, component InteractiveComponent) ActionRowComponent { for i, cc := range c.Components { @@ -523,8 +535,11 @@ func (c ButtonComponent) WithDisabled(disabled bool) ButtonComponent { return c } -var ( - _ Component = (*TextInputComponent)(nil) +type TextInputStyle int + +const ( + TextInputStyleShort TextInputStyle = iota + 1 + TextInputStyleParagraph ) // NewTextInput creates a new [TextInputComponent] with the provided parameters. @@ -546,6 +561,10 @@ func NewParagraphTextInput(customID string, label string) TextInputComponent { return NewTextInput(customID, TextInputStyleParagraph, label) } +var ( + _ Component = (*TextInputComponent)(nil) +) + // TextInputComponent is a component that allows users to input text. [Discord Docs] // // [Discord Docs]: https://discord.com/developers/docs/interactions/message-components#text-inputs @@ -635,16 +654,36 @@ func (c TextInputComponent) WithValue(value string) TextInputComponent { return c } -type TextInputStyle int +type UnfurledMediaItemLoadingState int const ( - TextInputStyleShort TextInputStyle = iota + 1 - TextInputStyleParagraph + UnfurledMediaItemLoadingStateUnknown UnfurledMediaItemLoadingState = iota + UnfurledMediaItemLoadingStateLoading + UnfurledMediaItemLoadingStateLoadedSuccess + UnfurledMediaItemLoadingStateLoadedNotFound ) +// UnfurledMediaItem is a media item that can be displayed in a [ThumbnailComponent] or [FileComponent]. type UnfurledMediaItem struct { // URL supports arbitrary urls and attachment:// references URL string `json:"url"` + // ProxyURL is a proxied version of the URL. This can't be set by bots. + ProxyURL string `json:"proxy_url,omitempty"` + // Height is the height of the media item in pixels. This can't be set by bots. + Height int `json:"height,omitempty"` + // Width is the width of the media item in pixels. This can't be set by bots. + Width int `json:"width,omitempty"` + // ContentType is the content type of the media item. This can't be set by bots. + ContentType string `json:"content_type,omitempty"` + // LoadingState is the loading state of the media item. This can't be set by bots. + LoadingState UnfurledMediaItemLoadingState `json:"loading_state,omitempty"` +} + +// NewSection creates a new [SectionComponent] with the provided components. +func NewSection(components ...SectionSubComponent) SectionComponent { + return SectionComponent{ + Components: components, + } } var ( @@ -653,12 +692,7 @@ var ( _ ContainerSubComponent = (*SectionComponent)(nil) ) -func NewSection(components ...SectionSubComponent) SectionComponent { - return SectionComponent{ - Components: components, - } -} - +// SectionComponent is a component that can contain up to 3 TextDisplayComponent(s) and an optional SectionAccessoryComponent. type SectionComponent struct { ID int `json:"id,omitempty"` Components []SectionSubComponent `json:"components"` @@ -710,7 +744,8 @@ func (SectionComponent) component() {} func (SectionComponent) layoutComponent() {} func (SectionComponent) containerSubComponent() {} -func (c SectionComponent) Children() iter.Seq[Component] { +// SubComponents returns an [iter.Seq[Component]] over the sub Component(s) and Accessory of the SectionComponent. +func (c SectionComponent) SubComponents() iter.Seq[Component] { return func(yield func(Component) bool) { for _, cc := range c.Components { if !yield(cc) { @@ -726,23 +761,65 @@ func (c SectionComponent) Children() iter.Seq[Component] { } } +// WithID returns a new SectionComponent with the provided id func (c SectionComponent) WithID(id int) SectionComponent { c.ID = id return c } -var ( - _ Component = (*TextDisplayComponent)(nil) - _ ContainerSubComponent = (*TextDisplayComponent)(nil) - _ SectionSubComponent = (*TextDisplayComponent)(nil) -) +// WithAccessory returns a new SectionComponent with the provided accessory +func (c SectionComponent) WithAccessory(accessory SectionAccessoryComponent) SectionComponent { + c.Accessory = accessory + return c +} +// WithComponents returns a new SectionComponent with the provided components +func (c SectionComponent) WithComponents(components ...SectionSubComponent) SectionComponent { + c.Components = components + return c +} + +// UpdateComponent returns a new SectionComponent with the Component which has the id replaced with the provided Component. +func (c SectionComponent) UpdateComponent(id int, component SectionSubComponent) SectionComponent { + for i, cc := range c.Components { + if cc.GetID() == id { + c.Components[i] = component + return c + } + } + return c +} + +// AddComponents returns a new SectionComponent with the provided Component(s) added +func (c SectionComponent) AddComponents(components ...SectionSubComponent) SectionComponent { + c.Components = append(c.Components, components...) + return c +} + +// RemoveComponent returns a new SectionComponent with the provided Component which has the provided id removed. +func (c SectionComponent) RemoveComponent(id int) SectionComponent { + for i, cc := range c.Components { + if cc.GetID() == id { + c.Components = append(c.Components[:i], c.Components[i+1:]...) + return c + } + } + return c +} + +// NewTextDisplay creates a new [TextDisplayComponent] with the provided content. func NewTextDisplay(content string) TextDisplayComponent { return TextDisplayComponent{ Content: content, } } +var ( + _ Component = (*TextDisplayComponent)(nil) + _ ContainerSubComponent = (*TextDisplayComponent)(nil) + _ SectionSubComponent = (*TextDisplayComponent)(nil) +) + // TextDisplayComponent is a component that displays text. type TextDisplayComponent struct { ID int `json:"id,omitempty"` @@ -777,8 +854,22 @@ func (c TextDisplayComponent) WithID(i int) SectionSubComponent { return c } +func (c TextDisplayComponent) WithContent(content string) TextDisplayComponent { + c.Content = content + return c +} + +func NewThumbnail(url string) ThumbnailComponent { + return ThumbnailComponent{ + Media: UnfurledMediaItem{ + URL: url, + }, + } +} + var ( - _ Component = (*ThumbnailComponent)(nil) + _ Component = (*ThumbnailComponent)(nil) + _ SectionAccessoryComponent = (*ThumbnailComponent)(nil) ) type ThumbnailComponent struct { @@ -807,31 +898,49 @@ func (c ThumbnailComponent) GetID() int { return c.ID } -func (ThumbnailComponent) component() {} +func (ThumbnailComponent) component() {} +func (ThumbnailComponent) sectionAccessoryComponent() {} func (c ThumbnailComponent) WithID(id int) ThumbnailComponent { c.ID = id return c } +func (c ThumbnailComponent) WithMediaURL(url string) ThumbnailComponent { + c.Media.URL = url + return c +} + +func (c ThumbnailComponent) WithDescription(description string) ThumbnailComponent { + c.Description = description + return c +} + +func (c ThumbnailComponent) WithSpoiler(spoiler bool) ThumbnailComponent { + c.Spoiler = spoiler + return c +} + type MediaGalleryItem struct { Media UnfurledMediaItem `json:"media"` Description string `json:"description,omitempty"` Spoiler bool `json:"spoiler,omitempty"` } -var ( - _ Component = (*MediaGalleryComponent)(nil) - _ LayoutComponent = (*MediaGalleryComponent)(nil) - _ ContainerSubComponent = (*MediaGalleryComponent)(nil) -) - +// NewMediaGallery creates a new [MediaGalleryComponent] with the provided items. func NewMediaGallery(items ...MediaGalleryItem) MediaGalleryComponent { return MediaGalleryComponent{ Items: items, } } +var ( + _ Component = (*MediaGalleryComponent)(nil) + _ LayoutComponent = (*MediaGalleryComponent)(nil) + _ ContainerSubComponent = (*MediaGalleryComponent)(nil) +) + +// MediaGalleryComponent is a component that displays a gallery of media. type MediaGalleryComponent struct { ID int `json:"id,omitempty"` Items []MediaGalleryItem `json:"items"` @@ -860,42 +969,53 @@ func (MediaGalleryComponent) component() {} func (MediaGalleryComponent) layoutComponent() {} func (MediaGalleryComponent) containerSubComponent() {} +// WithID returns a new MediaGalleryComponent with the provided id func (c MediaGalleryComponent) WithID(id int) MediaGalleryComponent { c.ID = id return c } +// WithItems returns a new MediaGalleryComponent with the provided items +func (c MediaGalleryComponent) WithItems(items ...MediaGalleryItem) MediaGalleryComponent { + c.Items = items + return c +} + type SeparatorSpacingSize int const ( - SeparatorSpacingSizeNone SeparatorSpacingSize = iota - SeparatorSpacingSizeSmall + SeparatorSpacingSizeSmall SeparatorSpacingSize = iota + 1 SeparatorSpacingSizeLarge ) -var ( - _ Component = (*SeparatorComponent)(nil) - _ LayoutComponent = (*MediaGalleryComponent)(nil) - _ ContainerSubComponent = (*MediaGalleryComponent)(nil) -) - -func NewSeparator(spacing SeparatorSpacingSize) SeparatorComponent { - return SeparatorComponent{ - Spacing: spacing, - } -} - +// NewSmallSeparator creates a new [SeparatorComponent] with the small spacing. func NewSmallSeparator() SeparatorComponent { return NewSeparator(SeparatorSpacingSizeSmall) } +// NewLargeSeparator creates a new [SeparatorComponent] with the large spacing. func NewLargeSeparator() SeparatorComponent { return NewSeparator(SeparatorSpacingSizeLarge) } +// NewSeparator creates a new [SeparatorComponent] with the provided spacing. +func NewSeparator(spacing SeparatorSpacingSize) SeparatorComponent { + return SeparatorComponent{ + Spacing: spacing, + } +} + +var ( + _ Component = (*SeparatorComponent)(nil) + _ LayoutComponent = (*MediaGalleryComponent)(nil) + _ ContainerSubComponent = (*MediaGalleryComponent)(nil) +) + +// SeparatorComponent is a component that adds a visual divider between components. type SeparatorComponent struct { - ID int `json:"id,omitempty"` - Divider bool `json:"divider,omitempty"` + ID int `json:"id,omitempty"` + // Divider determines if the separator should have a divider line. (default: true) + Divider *bool `json:"divider,omitempty"` Spacing SeparatorSpacingSize `json:"spacing,omitempty"` } @@ -927,19 +1047,32 @@ func (c SeparatorComponent) WithID(i int) LayoutComponent { return c } -var ( - _ Component = (*FileComponent)(nil) -) +func (c SeparatorComponent) WithDivider(divider bool) SeparatorComponent { + c.Divider = ÷r + return c +} + +func (c SeparatorComponent) WithSpacing(spacing SeparatorSpacingSize) SeparatorComponent { + c.Spacing = spacing + return c +} -func NewFileComponent(url string, spoiler bool) FileComponent { +// NewFileComponent creates a new [FileComponent] with the provided url. +func NewFileComponent(url string) FileComponent { return FileComponent{ File: UnfurledMediaItem{ URL: url, }, - Spoiler: spoiler, } } +var ( + _ Component = (*FileComponent)(nil) + _ ContainerSubComponent = (*FileComponent)(nil) + _ LayoutComponent = (*FileComponent)(nil) +) + +// FileComponent is a component that displays a file. type FileComponent struct { ID int `json:"id,omitempty"` // File only supports attachment:// references @@ -966,32 +1099,43 @@ func (c FileComponent) GetID() int { return c.ID } -func (FileComponent) component() {} +func (FileComponent) component() {} +func (FileComponent) layoutComponent() {} +func (FileComponent) containerSubComponent() {} func (c FileComponent) WithID(id int) FileComponent { c.ID = id return c } +func (c FileComponent) WithMediaURL(url string) FileComponent { + c.File.URL = url + return c +} + func (c FileComponent) WithSpoiler(spoiler bool) FileComponent { c.Spoiler = spoiler return c } -var ( - _ Component = (*ContainerComponent)(nil) - _ LayoutComponent = (*ContainerComponent)(nil) -) - +// NewContainer creates a new [ContainerComponent] with the provided components. func NewContainer(components ...ContainerSubComponent) ContainerComponent { return ContainerComponent{ Components: components, } } +var ( + _ Component = (*ContainerComponent)(nil) + _ LayoutComponent = (*ContainerComponent)(nil) +) + +// ContainerComponent is a component which lets you group components together. +// It looks similar to embeds and supports an accent color and spoiler state. type ContainerComponent struct { - ID int `json:"id,omitempty"` - AccentColor *int `json:"accent_color,omitempty"` + ID int `json:"id,omitempty"` + // AccentColor is the color of the left border of the container. (use 0 for no color) + AccentColor int `json:"accent_color,omitempty"` Spoiler bool `json:"spoiler,omitempty"` Components []ContainerSubComponent `json:"components"` } @@ -1010,7 +1154,7 @@ func (c ContainerComponent) MarshalJSON() ([]byte, error) { func (c *ContainerComponent) UnmarshalJSON(data []byte) error { var containerComponent struct { ID int `json:"id,omitempty"` - AccentColor *int `json:"accent_color,omitempty"` + AccentColor int `json:"accent_color,omitempty"` Spoiler bool `json:"spoiler,omitempty"` Components []UnmarshalComponent `json:"components"` } @@ -1046,14 +1190,24 @@ func (c ContainerComponent) WithID(id int) ContainerComponent { return c } -func (c ContainerComponent) Children() iter.Seq[Component] { +func (c ContainerComponent) WithAccentColor(accentColor int) ContainerComponent { + c.AccentColor = accentColor + return c +} + +func (c ContainerComponent) WithSpoiler(spoiler bool) ContainerComponent { + c.Spoiler = spoiler + return c +} + +func (c ContainerComponent) SubComponents() iter.Seq[Component] { return func(yield func(Component) bool) { for _, cc := range c.Components { if !yield(cc) { return } if ic, ok := cc.(ComponentIter); ok { - for cc := range ic.Children() { + for cc := range ic.SubComponents() { if !yield(cc) { return } @@ -1063,6 +1217,45 @@ func (c ContainerComponent) Children() iter.Seq[Component] { } } +func (c ContainerComponent) WithComponents(components ...ContainerSubComponent) ContainerComponent { + c.Components = components + return c +} + +func (c ContainerComponent) UpdateComponent(id int, component ContainerSubComponent) ContainerComponent { + for i, cc := range c.Components { + if cc.GetID() == id { + c.Components[i] = component + return c + } + } + return c +} + +func (c ContainerComponent) AddComponents(components ...ContainerSubComponent) ContainerComponent { + c.Components = append(c.Components, components...) + return c +} + +func (c ContainerComponent) RemoveComponent(id int) ContainerComponent { + for i, cc := range c.Components { + if cc.GetID() == id { + c.Components = append(c.Components[:i], c.Components[i+1:]...) + return c + } + } + return c +} + +// NewUnknownComponent creates a new [UnknownComponent] with the provided type and data. +// This is useful for handling unknown components, but should be avoided if possible. +func NewUnknownComponent(componentType ComponentType, data json.RawMessage) UnknownComponent { + return UnknownComponent{ + ComponentType: componentType, + Data: data, + } +} + var ( _ Component = (*UnknownComponent)(nil) _ InteractiveComponent = (*UnknownComponent)(nil) @@ -1073,6 +1266,7 @@ var ( _ ContainerSubComponent = (*UnknownComponent)(nil) ) +// UnknownComponent is a component that is not recognized by the library. type UnknownComponent struct { ComponentType ComponentType ID int diff --git a/discord/component_iter.go b/discord/component_iter.go index b501901bf..5e5b125ab 100644 --- a/discord/component_iter.go +++ b/discord/component_iter.go @@ -14,7 +14,7 @@ func componentIter(components []LayoutComponent) iter.Seq[Component] { continue } - for cc := range ic.Children() { + for cc := range ic.SubComponents() { if !yield(cc) { return } diff --git a/discord/interaction_modal_submit.go b/discord/interaction_modal_submit.go index ef0050d9d..78fc87cca 100644 --- a/discord/interaction_modal_submit.go +++ b/discord/interaction_modal_submit.go @@ -1,6 +1,10 @@ package discord -import "github.com/disgoorg/json" +import ( + "iter" + + "github.com/disgoorg/json" +) var ( _ Interaction = (*ModalSubmitInteraction)(nil) @@ -76,43 +80,41 @@ func (ModalSubmitInteraction) Type() InteractionType { func (ModalSubmitInteraction) interaction() {} type ModalSubmitInteractionData struct { - CustomID string `json:"custom_id"` - // TODO: rethink this since you might want to access other components. - Components map[string]InteractiveComponent `json:"components"` + CustomID string `json:"custom_id"` + Components []LayoutComponent `json:"components"` } func (d *ModalSubmitInteractionData) UnmarshalJSON(data []byte) error { - type modalSubmitInteractionData ModalSubmitInteractionData var iData struct { + CustomID string `json:"custom_id"` Components []UnmarshalComponent `json:"components"` - modalSubmitInteractionData } if err := json.Unmarshal(data, &iData); err != nil { return err } - *d = ModalSubmitInteractionData(iData.modalSubmitInteractionData) - - if len(iData.Components) > 0 { - d.Components = make(map[string]InteractiveComponent) + d.CustomID = iData.CustomID - var layoutComponents []LayoutComponent - for _, containerComponent := range iData.Components { - layoutComponents = append(layoutComponents, containerComponent.Component.(LayoutComponent)) - } - for component := range componentIter(layoutComponents) { - if ic, ok := component.(InteractiveComponent); ok { - d.Components[ic.GetCustomID()] = ic - } - } + components := make([]LayoutComponent, 0, len(iData.Components)) + for _, containerComponent := range iData.Components { + components = append(components, containerComponent.Component.(LayoutComponent)) } + d.Components = components return nil } +func (d ModalSubmitInteractionData) AllComponents() iter.Seq[Component] { + return componentIter(d.Components) +} + func (d ModalSubmitInteractionData) Component(customID string) (InteractiveComponent, bool) { - component, ok := d.Components[customID] - return component, ok + for component := range d.AllComponents() { + if ic, ok := component.(InteractiveComponent); ok && ic.GetCustomID() == customID { + return ic, true + } + } + return nil, false } func (d ModalSubmitInteractionData) TextInputComponent(customID string) (TextInputComponent, bool) { diff --git a/discord/message_create_builder.go b/discord/message_create_builder.go index dddc88701..5303b0afa 100644 --- a/discord/message_create_builder.go +++ b/discord/message_create_builder.go @@ -84,14 +84,14 @@ func (b *MessageCreateBuilder) RemoveEmbed(i int) *MessageCreateBuilder { return b } -// SetLayoutComponents sets the discord.LayoutComponent(s) of the Message -func (b *MessageCreateBuilder) SetLayoutComponents(LayoutComponents ...LayoutComponent) *MessageCreateBuilder { +// SetComponents sets the discord.LayoutComponent(s) of the Message +func (b *MessageCreateBuilder) SetComponents(LayoutComponents ...LayoutComponent) *MessageCreateBuilder { b.Components = LayoutComponents return b } -// SetLayoutComponent sets the provided discord.InteractiveComponent at the index of discord.InteractiveComponent(s) -func (b *MessageCreateBuilder) SetLayoutComponent(i int, container LayoutComponent) *MessageCreateBuilder { +// SetComponent sets the provided discord.InteractiveComponent at the index of discord.InteractiveComponent(s) +func (b *MessageCreateBuilder) SetComponent(i int, container LayoutComponent) *MessageCreateBuilder { if len(b.Components) > i { b.Components[i] = container } @@ -104,22 +104,22 @@ func (b *MessageCreateBuilder) AddActionRow(components ...InteractiveComponent) return b } -// AddLayoutComponents adds the discord.LayoutComponent(s) to the Message -func (b *MessageCreateBuilder) AddLayoutComponents(containers ...LayoutComponent) *MessageCreateBuilder { +// AddComponents adds the discord.LayoutComponent(s) to the Message +func (b *MessageCreateBuilder) AddComponents(containers ...LayoutComponent) *MessageCreateBuilder { b.Components = append(b.Components, containers...) return b } -// RemoveLayoutComponent removes a discord.ActionRowComponent from the Message -func (b *MessageCreateBuilder) RemoveLayoutComponent(i int) *MessageCreateBuilder { +// RemoveComponent removes a discord.ActionRowComponent from the Message +func (b *MessageCreateBuilder) RemoveComponent(i int) *MessageCreateBuilder { if len(b.Components) > i { b.Components = append(b.Components[:i], b.Components[i+1:]...) } return b } -// ClearLayoutComponents removes all the discord.LayoutComponent(s) of the Message -func (b *MessageCreateBuilder) ClearLayoutComponents() *MessageCreateBuilder { +// ClearComponents removes all the discord.LayoutComponent(s) of the Message +func (b *MessageCreateBuilder) ClearComponents() *MessageCreateBuilder { b.Components = []LayoutComponent{} return b } @@ -241,6 +241,16 @@ func (b *MessageCreateBuilder) SetEphemeral(ephemeral bool) *MessageCreateBuilde return b } +// SetIsComponentsV2 adds/removes discord.MessageFlagIsComponentsV2 to the Message flags +func (b *MessageCreateBuilder) SetIsComponentsV2(isComponentV2 bool) *MessageCreateBuilder { + if isComponentV2 { + b.Flags = b.Flags.Add(MessageFlagIsComponentsV2) + } else { + b.Flags = b.Flags.Remove(MessageFlagIsComponentsV2) + } + return b +} + // SetSuppressEmbeds adds/removes discord.MessageFlagSuppressEmbeds to the Message flags func (b *MessageCreateBuilder) SetSuppressEmbeds(suppressEmbeds bool) *MessageCreateBuilder { if suppressEmbeds { diff --git a/discord/message_update_builder.go b/discord/message_update_builder.go index 0706977f2..a3a0d305f 100644 --- a/discord/message_update_builder.go +++ b/discord/message_update_builder.go @@ -79,8 +79,8 @@ func (b *MessageUpdateBuilder) RemoveEmbed(i int) *MessageUpdateBuilder { return b } -// SetLayoutComponents sets the discord.LayoutComponent(s) of the Message -func (b *MessageUpdateBuilder) SetLayoutComponents(LayoutComponents ...LayoutComponent) *MessageUpdateBuilder { +// SetComponents sets the discord.LayoutComponent(s) of the Message +func (b *MessageUpdateBuilder) SetComponents(LayoutComponents ...LayoutComponent) *MessageUpdateBuilder { if b.Components == nil { b.Components = new([]LayoutComponent) } @@ -88,8 +88,8 @@ func (b *MessageUpdateBuilder) SetLayoutComponents(LayoutComponents ...LayoutCom return b } -// SetLayoutComponent sets the provided discord.InteractiveComponent at the index of discord.InteractiveComponent(s) -func (b *MessageUpdateBuilder) SetLayoutComponent(i int, container LayoutComponent) *MessageUpdateBuilder { +// SetComponent sets the provided discord.InteractiveComponent at the index of discord.InteractiveComponent(s) +func (b *MessageUpdateBuilder) SetComponent(i int, container LayoutComponent) *MessageUpdateBuilder { if b.Components == nil { b.Components = new([]LayoutComponent) } @@ -108,8 +108,8 @@ func (b *MessageUpdateBuilder) AddActionRow(components ...InteractiveComponent) return b } -// AddLayoutComponents adds the discord.LayoutComponent(s) to the Message -func (b *MessageUpdateBuilder) AddLayoutComponents(containers ...LayoutComponent) *MessageUpdateBuilder { +// AddComponents adds the discord.LayoutComponent(s) to the Message +func (b *MessageUpdateBuilder) AddComponents(containers ...LayoutComponent) *MessageUpdateBuilder { if b.Components == nil { b.Components = new([]LayoutComponent) } @@ -117,8 +117,8 @@ func (b *MessageUpdateBuilder) AddLayoutComponents(containers ...LayoutComponent return b } -// RemoveLayoutComponent removes a discord.LayoutComponent from the Message -func (b *MessageUpdateBuilder) RemoveLayoutComponent(i int) *MessageUpdateBuilder { +// RemoveComponent removes a discord.LayoutComponent from the Message +func (b *MessageUpdateBuilder) RemoveComponent(i int) *MessageUpdateBuilder { if b.Components == nil { return b } @@ -128,8 +128,8 @@ func (b *MessageUpdateBuilder) RemoveLayoutComponent(i int) *MessageUpdateBuilde return b } -// ClearLayoutComponents removes all the discord.LayoutComponent(s) of the Message -func (b *MessageUpdateBuilder) ClearLayoutComponents() *MessageUpdateBuilder { +// ClearComponents removes all the discord.LayoutComponent(s) of the Message +func (b *MessageUpdateBuilder) ClearComponents() *MessageUpdateBuilder { b.Components = &[]LayoutComponent{} return b } diff --git a/discord/modal_create.go b/discord/modal_create.go index 0edcc6fb3..42e481806 100644 --- a/discord/modal_create.go +++ b/discord/modal_create.go @@ -31,14 +31,14 @@ func (b *ModalCreateBuilder) SetTitle(title string) *ModalCreateBuilder { return b } -// SetLayoutComponents sets the discord.LayoutComponent(s) of the ModalCreate -func (b *ModalCreateBuilder) SetLayoutComponents(LayoutComponents ...LayoutComponent) *ModalCreateBuilder { +// SetComponents sets the discord.LayoutComponent(s) of the ModalCreate +func (b *ModalCreateBuilder) SetComponents(LayoutComponents ...LayoutComponent) *ModalCreateBuilder { b.Components = LayoutComponents return b } -// SetLayoutComponent sets the provided discord.InteractiveComponent at the index of discord.InteractiveComponent(s) -func (b *ModalCreateBuilder) SetLayoutComponent(i int, container LayoutComponent) *ModalCreateBuilder { +// SetComponent sets the provided discord.InteractiveComponent at the index of discord.InteractiveComponent(s) +func (b *ModalCreateBuilder) SetComponent(i int, container LayoutComponent) *ModalCreateBuilder { if len(b.Components) > i { b.Components[i] = container } @@ -51,22 +51,22 @@ func (b *ModalCreateBuilder) AddActionRow(components ...InteractiveComponent) *M return b } -// AddLayoutComponents adds the discord.LayoutComponent(s) to the ModalCreate -func (b *ModalCreateBuilder) AddLayoutComponents(containers ...LayoutComponent) *ModalCreateBuilder { +// AddComponents adds the discord.LayoutComponent(s) to the ModalCreate +func (b *ModalCreateBuilder) AddComponents(containers ...LayoutComponent) *ModalCreateBuilder { b.Components = append(b.Components, containers...) return b } -// RemoveLayoutComponent removes a discord.ActionRowComponent from the ModalCreate -func (b *ModalCreateBuilder) RemoveLayoutComponent(i int) *ModalCreateBuilder { +// RemoveComponent removes a discord.ActionRowComponent from the ModalCreate +func (b *ModalCreateBuilder) RemoveComponent(i int) *ModalCreateBuilder { if len(b.Components) > i { b.Components = append(b.Components[:i], b.Components[i+1:]...) } return b } -// ClearLayoutComponents removes all the discord.LayoutComponent(s) of the ModalCreate -func (b *ModalCreateBuilder) ClearLayoutComponents() *ModalCreateBuilder { +// ClearComponents removes all the discord.LayoutComponent(s) of the ModalCreate +func (b *ModalCreateBuilder) ClearComponents() *ModalCreateBuilder { b.Components = []LayoutComponent{} return b } diff --git a/discord/webhook_message_create_builder.go b/discord/webhook_message_create_builder.go index 5a81b9141..ab1588b5c 100644 --- a/discord/webhook_message_create_builder.go +++ b/discord/webhook_message_create_builder.go @@ -80,14 +80,14 @@ func (b *WebhookMessageCreateBuilder) RemoveEmbed(i int) *WebhookMessageCreateBu return b } -// SetLayoutComponents sets the discord.LayoutComponent(s) of the Message -func (b *WebhookMessageCreateBuilder) SetLayoutComponents(LayoutComponents ...LayoutComponent) *WebhookMessageCreateBuilder { +// SetComponents sets the discord.LayoutComponent(s) of the Message +func (b *WebhookMessageCreateBuilder) SetComponents(LayoutComponents ...LayoutComponent) *WebhookMessageCreateBuilder { b.Components = LayoutComponents return b } -// SetLayoutComponent sets the provided discord.InteractiveComponent at the index of discord.InteractiveComponent(s) -func (b *WebhookMessageCreateBuilder) SetLayoutComponent(i int, container LayoutComponent) *WebhookMessageCreateBuilder { +// SetComponent sets the provided discord.InteractiveComponent at the index of discord.InteractiveComponent(s) +func (b *WebhookMessageCreateBuilder) SetComponent(i int, container LayoutComponent) *WebhookMessageCreateBuilder { if len(b.Components) > i { b.Components[i] = container } @@ -100,22 +100,22 @@ func (b *WebhookMessageCreateBuilder) AddActionRow(components ...InteractiveComp return b } -// AddLayoutComponents adds the discord.LayoutComponent(s) to the Message -func (b *WebhookMessageCreateBuilder) AddLayoutComponents(containers ...LayoutComponent) *WebhookMessageCreateBuilder { +// AddComponents adds the discord.LayoutComponent(s) to the Message +func (b *WebhookMessageCreateBuilder) AddComponents(containers ...LayoutComponent) *WebhookMessageCreateBuilder { b.Components = append(b.Components, containers...) return b } -// RemoveLayoutComponent removes a discord.ActionRowComponent from the Message -func (b *WebhookMessageCreateBuilder) RemoveLayoutComponent(i int) *WebhookMessageCreateBuilder { +// RemoveComponent removes a discord.ActionRowComponent from the Message +func (b *WebhookMessageCreateBuilder) RemoveComponent(i int) *WebhookMessageCreateBuilder { if len(b.Components) > i { b.Components = append(b.Components[:i], b.Components[i+1:]...) } return b } -// ClearLayoutComponents removes all the discord.LayoutComponent(s) of the Message -func (b *WebhookMessageCreateBuilder) ClearLayoutComponents() *WebhookMessageCreateBuilder { +// ClearComponents removes all the discord.LayoutComponent(s) of the Message +func (b *WebhookMessageCreateBuilder) ClearComponents() *WebhookMessageCreateBuilder { b.Components = []LayoutComponent{} return b } @@ -194,6 +194,16 @@ func (b *WebhookMessageCreateBuilder) ClearFlags() *WebhookMessageCreateBuilder return b.SetFlags(MessageFlagsNone) } +// SetIsComponentsV2 adds/removes discord.MessageFlagIsComponentsV2 to the Message flags +func (b *WebhookMessageCreateBuilder) SetIsComponentsV2(isComponentV2 bool) *WebhookMessageCreateBuilder { + if isComponentV2 { + b.Flags = b.Flags.Add(MessageFlagIsComponentsV2) + } else { + b.Flags = b.Flags.Remove(MessageFlagIsComponentsV2) + } + return b +} + // SetSuppressEmbeds adds/removes discord.MessageFlagSuppressEmbeds to the Message flags func (b *WebhookMessageCreateBuilder) SetSuppressEmbeds(suppressEmbeds bool) *WebhookMessageCreateBuilder { if suppressEmbeds { diff --git a/discord/webhook_message_update_builder.go b/discord/webhook_message_update_builder.go index 4c2fe7007..abee0e852 100644 --- a/discord/webhook_message_update_builder.go +++ b/discord/webhook_message_update_builder.go @@ -83,8 +83,8 @@ func (b *WebhookMessageUpdateBuilder) RemoveEmbed(i int) *WebhookMessageUpdateBu return b } -// SetLayoutComponents sets the discord.LayoutComponent(s) of the Message -func (b *WebhookMessageUpdateBuilder) SetLayoutComponents(LayoutComponents ...LayoutComponent) *WebhookMessageUpdateBuilder { +// SetComponents sets the discord.LayoutComponent(s) of the Message +func (b *WebhookMessageUpdateBuilder) SetComponents(LayoutComponents ...LayoutComponent) *WebhookMessageUpdateBuilder { if b.Components == nil { b.Components = new([]LayoutComponent) } @@ -92,8 +92,8 @@ func (b *WebhookMessageUpdateBuilder) SetLayoutComponents(LayoutComponents ...La return b } -// SetLayoutComponent sets the provided discord.InteractiveComponent at the index of discord.InteractiveComponent(s) -func (b *WebhookMessageUpdateBuilder) SetLayoutComponent(i int, container LayoutComponent) *WebhookMessageUpdateBuilder { +// SetComponent sets the provided discord.InteractiveComponent at the index of discord.InteractiveComponent(s) +func (b *WebhookMessageUpdateBuilder) SetComponent(i int, container LayoutComponent) *WebhookMessageUpdateBuilder { if b.Components == nil { b.Components = new([]LayoutComponent) } @@ -112,8 +112,8 @@ func (b *WebhookMessageUpdateBuilder) AddActionRow(components ...InteractiveComp return b } -// AddLayoutComponents adds the discord.LayoutComponent(s) to the Message -func (b *WebhookMessageUpdateBuilder) AddLayoutComponents(containers ...LayoutComponent) *WebhookMessageUpdateBuilder { +// AddComponents adds the discord.LayoutComponent(s) to the Message +func (b *WebhookMessageUpdateBuilder) AddComponents(containers ...LayoutComponent) *WebhookMessageUpdateBuilder { if b.Components == nil { b.Components = new([]LayoutComponent) } @@ -121,8 +121,8 @@ func (b *WebhookMessageUpdateBuilder) AddLayoutComponents(containers ...LayoutCo return b } -// RemoveLayoutComponent removes a discord.LayoutComponent from the Message -func (b *WebhookMessageUpdateBuilder) RemoveLayoutComponent(i int) *WebhookMessageUpdateBuilder { +// RemoveComponent removes a discord.LayoutComponent from the Message +func (b *WebhookMessageUpdateBuilder) RemoveComponent(i int) *WebhookMessageUpdateBuilder { if b.Components == nil { return b } @@ -132,8 +132,8 @@ func (b *WebhookMessageUpdateBuilder) RemoveLayoutComponent(i int) *WebhookMessa return b } -// ClearLayoutComponents removes all the discord.LayoutComponent(s) of the Message -func (b *WebhookMessageUpdateBuilder) ClearLayoutComponents() *WebhookMessageUpdateBuilder { +// ClearComponents removes all the discord.LayoutComponent(s) of the Message +func (b *WebhookMessageUpdateBuilder) ClearComponents() *WebhookMessageUpdateBuilder { b.Components = &[]LayoutComponent{} return b }