diff --git a/discord/application_command.go b/discord/application_command.go index 72d96d90..faa9bb15 100644 --- a/discord/application_command.go +++ b/discord/application_command.go @@ -14,6 +14,7 @@ const ( ApplicationCommandTypeSlash ApplicationCommandType = iota + 1 ApplicationCommandTypeUser ApplicationCommandTypeMessage + ApplicationCommandTypePrimaryEntryPoint ) type ApplicationCommand interface { @@ -69,6 +70,11 @@ func (u *UnmarshalApplicationCommand) UnmarshalJSON(data []byte) error { err = json.Unmarshal(data, &v) applicationCommand = v + case ApplicationCommandTypePrimaryEntryPoint: + var v EntryPointCommand + err = json.Unmarshal(data, &v) + applicationCommand = v + default: err = fmt.Errorf("unknown application command with type %d received", cType.Type) } @@ -180,6 +186,7 @@ func (c SlashCommand) NameLocalized() string { func (c SlashCommand) DefaultMemberPermissions() Permissions { return c.defaultMemberPermissions } + func (c SlashCommand) DMPermission() bool { return c.dmPermission } @@ -297,6 +304,7 @@ func (c UserCommand) NameLocalized() string { func (c UserCommand) DefaultMemberPermissions() Permissions { return c.defaultMemberPermissions } + func (c UserCommand) DMPermission() bool { return c.dmPermission } @@ -410,6 +418,7 @@ func (c MessageCommand) NameLocalized() string { func (c MessageCommand) DefaultMemberPermissions() Permissions { return c.defaultMemberPermissions } + func (c MessageCommand) DMPermission() bool { return c.dmPermission } @@ -435,3 +444,127 @@ func (c MessageCommand) CreatedAt() time.Time { } func (MessageCommand) applicationCommand() {} + +var _ ApplicationCommand = (*EntryPointCommand)(nil) + +type EntryPointCommand struct { + id snowflake.ID + applicationID snowflake.ID + guildID *snowflake.ID + name string + nameLocalizations map[Locale]string + nameLocalized string + defaultMemberPermissions Permissions + dmPermission bool + nsfw bool + integrationTypes []ApplicationIntegrationType + contexts []InteractionContextType + version snowflake.ID + Handler EntryPointCommandHandlerType +} + +func (c *EntryPointCommand) UnmarshalJSON(data []byte) error { + var v rawEntryPointCommand + if err := json.Unmarshal(data, &v); err != nil { + return err + } + + c.id = v.ID + c.applicationID = v.ApplicationID + c.guildID = v.GuildID + c.name = v.Name + c.nameLocalizations = v.NameLocalizations + c.nameLocalized = v.NameLocalized + c.defaultMemberPermissions = v.DefaultMemberPermissions + c.dmPermission = v.DMPermission + c.nsfw = v.NSFW + c.integrationTypes = v.IntegrationTypes + c.contexts = v.Contexts + c.version = v.Version + c.Handler = v.Handler + return nil +} + +func (c EntryPointCommand) MarshalJSON() ([]byte, error) { + return json.Marshal(rawEntryPointCommand{ + ID: c.id, + Type: c.Type(), + ApplicationID: c.applicationID, + GuildID: c.guildID, + Name: c.name, + NameLocalizations: c.nameLocalizations, + NameLocalized: c.nameLocalized, + DefaultMemberPermissions: c.defaultMemberPermissions, + DMPermission: c.dmPermission, + NSFW: c.nsfw, + IntegrationTypes: c.integrationTypes, + Contexts: c.contexts, + Version: c.version, + Handler: c.Handler, + }) +} + +func (c EntryPointCommand) ID() snowflake.ID { + return c.id +} + +func (EntryPointCommand) Type() ApplicationCommandType { + return ApplicationCommandTypePrimaryEntryPoint +} + +func (c EntryPointCommand) ApplicationID() snowflake.ID { + return c.applicationID +} + +func (c EntryPointCommand) GuildID() *snowflake.ID { + return c.guildID +} + +func (c EntryPointCommand) Name() string { + return c.name +} + +func (c EntryPointCommand) NameLocalizations() map[Locale]string { + return c.nameLocalizations +} + +func (c EntryPointCommand) NameLocalized() string { + return c.nameLocalized +} + +func (c EntryPointCommand) DefaultMemberPermissions() Permissions { + return c.defaultMemberPermissions +} + +func (c EntryPointCommand) DMPermission() bool { + return c.dmPermission +} + +func (c EntryPointCommand) NSFW() bool { + return c.nsfw +} + +func (c EntryPointCommand) IntegrationTypes() []ApplicationIntegrationType { + return c.integrationTypes +} + +func (c EntryPointCommand) Contexts() []InteractionContextType { + return c.contexts +} + +func (c EntryPointCommand) Version() snowflake.ID { + return c.version +} + +func (c EntryPointCommand) CreatedAt() time.Time { + return c.id.Time() +} + +func (EntryPointCommand) applicationCommand() {} + +type EntryPointCommandHandlerType int + +const ( + EntryPointCommandHandlerTypeAppHandler EntryPointCommandHandlerType = iota + 1 + EntryPointCommandHandlerTypeDiscordLaunchActivity +) diff --git a/discord/application_command_create.go b/discord/application_command_create.go index 61104ed7..cbe04f18 100644 --- a/discord/application_command_create.go +++ b/discord/application_command_create.go @@ -107,3 +107,36 @@ func (c MessageCommandCreate) CommandName() string { } func (MessageCommandCreate) applicationCommandCreate() {} + +type EntryPointCommandCreate struct { + Name string `json:"name"` + NameLocalizations map[Locale]string `json:"name_localizations,omitempty"` + DefaultMemberPermissions *json.Nullable[Permissions] `json:"default_member_permissions,omitempty"` + // Deprecated: Use Contexts instead + DMPermission *bool `json:"dm_permission,omitempty"` + IntegrationTypes []ApplicationIntegrationType `json:"integration_types,omitempty"` + Contexts []InteractionContextType `json:"contexts,omitempty"` + NSFW *bool `json:"nsfw,omitempty"` + Handler EntryPointCommandHandlerType `json:"handler,omitempty"` +} + +func (c EntryPointCommandCreate) MarshalJSON() ([]byte, error) { + type entryPointCommandCreate EntryPointCommandCreate + return json.Marshal(struct { + Type ApplicationCommandType `json:"type"` + entryPointCommandCreate + }{ + Type: c.Type(), + entryPointCommandCreate: entryPointCommandCreate(c), + }) +} + +func (EntryPointCommandCreate) Type() ApplicationCommandType { + return ApplicationCommandTypePrimaryEntryPoint +} + +func (c EntryPointCommandCreate) CommandName() string { + return c.Name +} + +func (EntryPointCommandCreate) applicationCommandCreate() {} diff --git a/discord/application_command_raw.go b/discord/application_command_raw.go index 404bfef1..aee24e8b 100644 --- a/discord/application_command_raw.go +++ b/discord/application_command_raw.go @@ -62,3 +62,20 @@ type rawContextCommand struct { Contexts []InteractionContextType `json:"contexts"` Version snowflake.ID `json:"version"` } + +type rawEntryPointCommand struct { + ID snowflake.ID `json:"id"` + Type ApplicationCommandType `json:"type"` + ApplicationID snowflake.ID `json:"application_id"` + GuildID *snowflake.ID `json:"guild_id,omitempty"` + Name string `json:"name"` + NameLocalizations map[Locale]string `json:"name_localizations,omitempty"` + NameLocalized string `json:"name_localized,omitempty"` + DefaultMemberPermissions Permissions `json:"default_member_permissions"` + DMPermission bool `json:"dm_permission"` + NSFW bool `json:"nsfw"` + IntegrationTypes []ApplicationIntegrationType `json:"integration_types"` + Contexts []InteractionContextType `json:"contexts"` + Version snowflake.ID `json:"version"` + Handler EntryPointCommandHandlerType `json:"handler"` +} diff --git a/discord/application_command_update.go b/discord/application_command_update.go index a91c89c0..97904cf2 100644 --- a/discord/application_command_update.go +++ b/discord/application_command_update.go @@ -107,3 +107,36 @@ func (c MessageCommandUpdate) CommandName() *string { } func (MessageCommandUpdate) applicationCommandUpdate() {} + +type EntryPointCommandUpdate struct { + Name *string `json:"name,omitempty"` + NameLocalizations *map[Locale]string `json:"name_localizations,omitempty"` + DefaultMemberPermissions *json.Nullable[Permissions] `json:"default_member_permissions,omitempty"` + // Deprecated: Use Contexts instead + DMPermission *bool `json:"dm_permission,omitempty"` + IntegrationTypes *[]ApplicationIntegrationType `json:"integration_types,omitempty"` + Contexts *[]InteractionContextType `json:"contexts,omitempty"` + NSFW *bool `json:"nsfw,omitempty"` + Handler *EntryPointCommandHandlerType `json:"handler,omitempty"` +} + +func (c EntryPointCommandUpdate) MarshalJSON() ([]byte, error) { + type entryPointCommandUpdate EntryPointCommandUpdate + return json.Marshal(struct { + Type ApplicationCommandType `json:"type"` + entryPointCommandUpdate + }{ + Type: c.Type(), + entryPointCommandUpdate: entryPointCommandUpdate(c), + }) +} + +func (EntryPointCommandUpdate) Type() ApplicationCommandType { + return ApplicationCommandTypePrimaryEntryPoint +} + +func (c EntryPointCommandUpdate) CommandName() *string { + return c.Name +} + +func (EntryPointCommandUpdate) applicationCommandUpdate() {} diff --git a/discord/interaction_application_command.go b/discord/interaction_application_command.go index d3c0292a..0ea38a5f 100644 --- a/discord/interaction_application_command.go +++ b/discord/interaction_application_command.go @@ -58,6 +58,10 @@ func (i *ApplicationCommandInteraction) UnmarshalJSON(data []byte) error { v.Resolved.Messages[id] = msg } } + case ApplicationCommandTypePrimaryEntryPoint: + v := EntryPointCommandInteractionData{} + err = json.Unmarshal(interaction.Data, &v) + interactionData = v default: return fmt.Errorf("unknown application rawInteraction data with type %d received", cType.Type) @@ -131,6 +135,10 @@ func (i ApplicationCommandInteraction) MessageCommandInteractionData() MessageCo return i.Data.(MessageCommandInteractionData) } +func (i ApplicationCommandInteraction) EntryPointCommandInteractionData() EntryPointCommandInteractionData { + return i.Data.(EntryPointCommandInteractionData) +} + func (ApplicationCommandInteraction) interaction() {} type ApplicationCommandInteractionData interface { @@ -687,3 +695,54 @@ func (MessageCommandInteractionData) contextCommandInteractionData() {} type MessageCommandResolved struct { Messages map[snowflake.ID]Message `json:"messages,omitempty"` } + +var ( + _ ApplicationCommandInteractionData = (*EntryPointCommandInteractionData)(nil) +) + +type rawEntryPointCommandInteractionData struct { + ID snowflake.ID `json:"id"` + Name string `json:"name"` + Type ApplicationCommandType `json:"type"` +} + +type EntryPointCommandInteractionData struct { + id snowflake.ID + name string +} + +func (d *EntryPointCommandInteractionData) UnmarshalJSON(data []byte) error { + var iData rawEntryPointCommandInteractionData + if err := json.Unmarshal(data, &iData); err != nil { + return err + } + d.id = iData.ID + d.name = iData.Name + return nil +} + +func (d *EntryPointCommandInteractionData) MarshalJSON() ([]byte, error) { + return json.Marshal(rawEntryPointCommandInteractionData{ + ID: d.id, + Name: d.name, + Type: d.Type(), + }) +} + +func (EntryPointCommandInteractionData) Type() ApplicationCommandType { + return ApplicationCommandTypePrimaryEntryPoint +} + +func (d EntryPointCommandInteractionData) CommandID() snowflake.ID { + return d.id +} + +func (d EntryPointCommandInteractionData) CommandName() string { + return d.name +} + +func (d EntryPointCommandInteractionData) GuildID() *snowflake.ID { + return nil +} + +func (EntryPointCommandInteractionData) applicationCommandInteractionData() {} diff --git a/discord/interaction_callback.go b/discord/interaction_callback.go new file mode 100644 index 00000000..cab381ba --- /dev/null +++ b/discord/interaction_callback.go @@ -0,0 +1,27 @@ +package discord + +import "github.com/disgoorg/snowflake/v2" + +type InteractionCallbackResponse struct { + Interaction InteractionCallback `json:"interaction"` + Resource *InteractionCallbackResource `json:"resource"` +} + +type InteractionCallback struct { + ID snowflake.ID `json:"id"` + Type InteractionType `json:"type"` + ActivityInstanceID string `json:"activity_instance_id"` + ResponseMessageID snowflake.ID `json:"response_message_id"` + ResponseMessageLoading bool `json:"response_message_loading"` + ResponseMessageEphemeral bool `json:"response_message_ephemeral"` +} + +type InteractionCallbackResource struct { + Type InteractionResponseType `json:"type"` + ActivityInstance *InteractionCallbackActivityInstance `json:"activity_instance"` + Message *Message `json:"message"` +} + +type InteractionCallbackActivityInstance struct { + ID string `json:"id"` +} diff --git a/discord/interaction_response.go b/discord/interaction_response.go index 1dd5a3c3..a82b6fd8 100644 --- a/discord/interaction_response.go +++ b/discord/interaction_response.go @@ -15,6 +15,8 @@ const ( InteractionResponseTypeAutocompleteResult InteractionResponseTypeModal InteractionResponseTypePremiumRequired + _ + InteractionResponseTypeLaunchActivity ) // InteractionResponse is how you answer interactions. If an answer is not sent within 3 seconds of receiving it, the interaction is failed, and you will be unable to respond to it. diff --git a/events/interaction_events.go b/events/interaction_events.go index a53a6588..efc372d9 100644 --- a/events/interaction_events.go +++ b/events/interaction_events.go @@ -67,6 +67,11 @@ func (e *ApplicationCommandInteractionCreate) PremiumRequired(opts ...rest.Reque return e.Respond(discord.InteractionResponseTypePremiumRequired, nil, opts...) } +// LaunchActivity responds to the interaction by launching activity associated with the app. +func (e *ApplicationCommandInteractionCreate) LaunchActivity(opts ...rest.RequestOpt) error { + return e.Respond(discord.InteractionResponseTypeLaunchActivity, nil, opts...) +} + // ComponentInteractionCreate indicates that a new component interaction has been created. type ComponentInteractionCreate struct { *GenericEvent @@ -119,6 +124,11 @@ func (e *ComponentInteractionCreate) PremiumRequired(opts ...rest.RequestOpt) er return e.Respond(discord.InteractionResponseTypePremiumRequired, nil, opts...) } +// LaunchActivity responds to the interaction by launching activity associated with the app. +func (e *ComponentInteractionCreate) LaunchActivity(opts ...rest.RequestOpt) error { + return e.Respond(discord.InteractionResponseTypeLaunchActivity, nil, opts...) +} + // AutocompleteInteractionCreate indicates that a new autocomplete interaction has been created. type AutocompleteInteractionCreate struct { *GenericEvent @@ -187,3 +197,8 @@ func (e *ModalSubmitInteractionCreate) DeferUpdateMessage(opts ...rest.RequestOp func (e *ModalSubmitInteractionCreate) PremiumRequired(opts ...rest.RequestOpt) error { return e.Respond(discord.InteractionResponseTypePremiumRequired, nil, opts...) } + +// LaunchActivity responds to the interaction by launching activity associated with the app. +func (e *ModalSubmitInteractionCreate) LaunchActivity(opts ...rest.RequestOpt) error { + return e.Respond(discord.InteractionResponseTypeLaunchActivity, nil, opts...) +} diff --git a/handler/handler.go b/handler/handler.go index 18099147..b01c65eb 100644 --- a/handler/handler.go +++ b/handler/handler.go @@ -120,6 +120,17 @@ func (h *handlerHolder[T]) Handle(path string, event *InteractionEvent) error { Vars: event.Vars, Ctx: event.Ctx, }) + case EntryPointCommandHandler: + commandInteraction := event.Interaction.(discord.ApplicationCommandInteraction) + return handler(commandInteraction.Data.(discord.EntryPointCommandInteractionData), &CommandEvent{ + ApplicationCommandInteractionCreate: &events.ApplicationCommandInteractionCreate{ + GenericEvent: event.GenericEvent, + ApplicationCommandInteraction: commandInteraction, + Respond: event.Respond, + }, + Vars: event.Vars, + Ctx: event.Ctx, + }) case AutocompleteHandler: return handler(&AutocompleteEvent{ AutocompleteInteractionCreate: &events.AutocompleteInteractionCreate{ diff --git a/handler/interaction.go b/handler/interaction.go index 3d2c58f7..61df07f1 100644 --- a/handler/interaction.go +++ b/handler/interaction.go @@ -46,6 +46,11 @@ func (e *InteractionEvent) PremiumRequired(opts ...rest.RequestOpt) error { return e.Respond(discord.InteractionResponseTypePremiumRequired, nil, opts...) } +// LaunchActivity responds to the interaction by launching activity associated with the app. +func (e *InteractionEvent) LaunchActivity(opts ...rest.RequestOpt) error { + return e.Respond(discord.InteractionResponseTypeLaunchActivity, nil, opts...) +} + // Modal responds to the interaction with a new modal. func (e *InteractionEvent) Modal(modalCreate discord.ModalCreate, opts ...rest.RequestOpt) error { return e.Respond(discord.InteractionResponseTypeModal, modalCreate, opts...) diff --git a/handler/mux.go b/handler/mux.go index 5dfffbff..ce865c1b 100644 --- a/handler/mux.go +++ b/handler/mux.go @@ -231,6 +231,17 @@ func (r *Mux) MessageCommand(pattern string, h MessageCommandHandler) { }) } +// EntryPointCommand registers the given EntryPointCommandHandler to the current Router. +func (r *Mux) EntryPointCommand(pattern string, h EntryPointCommandHandler) { + checkPattern(pattern) + r.handle(&handlerHolder[EntryPointCommandHandler]{ + pattern: pattern, + handler: h, + t: discord.InteractionTypeApplicationCommand, + t2: []int{int(discord.ApplicationCommandTypePrimaryEntryPoint)}, + }) +} + // Autocomplete registers the given AutocompleteHandler to the current Router. func (r *Mux) Autocomplete(pattern string, h AutocompleteHandler) { checkPattern(pattern) diff --git a/handler/mux_test.go b/handler/mux_test.go index c2de3f0c..b0982c6e 100644 --- a/handler/mux_test.go +++ b/handler/mux_test.go @@ -36,6 +36,9 @@ func TestCommandMux(t *testing.T) { messageData, err := os.ReadFile("testdata/command/message_command.json") assert.NoError(t, err) + entryPointData, err := os.ReadFile("testdata/command/entry_point_command.json") + assert.NoError(t, err) + data := []struct { data []byte expected *discord.InteractionResponse @@ -67,6 +70,15 @@ func TestCommandMux(t *testing.T) { }, }, }, + { + data: entryPointData, + expected: &discord.InteractionResponse{ + Type: discord.InteractionResponseTypeCreateMessage, + Data: discord.MessageCreate{ + Content: "bar4", + }, + }, + }, } mux := New() @@ -85,6 +97,11 @@ func TestCommandMux(t *testing.T) { Content: "bar3", }) }) + mux.EntryPointCommand("/foo", func(data discord.EntryPointCommandInteractionData, e *CommandEvent) error { + return e.CreateMessage(discord.MessageCreate{ + Content: "bar4", + }) + }) for _, d := range data { interaction, err := discord.UnmarshalInteraction(d.data) diff --git a/handler/router.go b/handler/router.go index dc5df61b..4f36e51d 100644 --- a/handler/router.go +++ b/handler/router.go @@ -11,6 +11,7 @@ type ( SlashCommandHandler func(data discord.SlashCommandInteractionData, e *CommandEvent) error UserCommandHandler func(data discord.UserCommandInteractionData, e *CommandEvent) error MessageCommandHandler func(data discord.MessageCommandInteractionData, e *CommandEvent) error + EntryPointCommandHandler func(data discord.EntryPointCommandInteractionData, e *CommandEvent) error AutocompleteHandler func(e *AutocompleteEvent) error ComponentHandler func(e *ComponentEvent) error ButtonComponentHandler func(data discord.ButtonInteractionData, e *ComponentEvent) error @@ -73,6 +74,9 @@ type Router interface { // MessageCommand registers the given MessageCommandHandler to the current Router. MessageCommand(pattern string, h MessageCommandHandler) + // EntryPointCommand registers the given EntryPointCommandHandler to the current Router. + EntryPointCommand(pattern string, h EntryPointCommandHandler) + // Autocomplete registers the given AutocompleteHandler to the current Router. Autocomplete(pattern string, h AutocompleteHandler) diff --git a/handler/testdata/command/entry_point_command.json b/handler/testdata/command/entry_point_command.json new file mode 100644 index 00000000..5504b384 --- /dev/null +++ b/handler/testdata/command/entry_point_command.json @@ -0,0 +1,36 @@ +{ + "application_id": "775799577604522054", + "channel_id": "772908445358620702", + "data": { + "id": "866818195033292851", + "name": "foo", + "type": 4 + }, + "guild_id": "772904309264089089", + "guild_locale": "en-US", + "app_permissions": "442368", + "id": "867793873336926249", + "locale": "en-US", + "member": { + "avatar": null, + "deaf": false, + "is_pending": false, + "joined_at": "2020-11-02T20:46:57.364000+00:00", + "mute": false, + "nick": null, + "pending": false, + "permissions": "274877906943", + "premium_since": null, + "roles": ["785609923542777878"], + "user": { + "avatar": "a_f03401914fb4f3caa9037578ab980920", + "discriminator": "6538", + "id": "167348773423415296", + "public_flags": 1, + "username": "ian" + } + }, + "token": "UNIQUE_TOKEN", + "type": 2, + "version": 1 +} \ No newline at end of file diff --git a/rest/interactions.go b/rest/interactions.go index 17747c6f..9d687c51 100644 --- a/rest/interactions.go +++ b/rest/interactions.go @@ -15,6 +15,7 @@ func NewInteractions(client Client) Interactions { type Interactions interface { GetInteractionResponse(applicationID snowflake.ID, interactionToken string, opts ...RequestOpt) (*discord.Message, error) CreateInteractionResponse(interactionID snowflake.ID, interactionToken string, interactionResponse discord.InteractionResponse, opts ...RequestOpt) error + CreateInteractionResponseWithCallback(interactionID snowflake.ID, interactionToken string, interactionResponse discord.InteractionResponse, opts ...RequestOpt) (*discord.InteractionCallbackResponse, error) UpdateInteractionResponse(applicationID snowflake.ID, interactionToken string, messageUpdate discord.MessageUpdate, opts ...RequestOpt) (*discord.Message, error) DeleteInteractionResponse(applicationID snowflake.ID, interactionToken string, opts ...RequestOpt) error @@ -33,6 +34,8 @@ func (s *interactionImpl) GetInteractionResponse(interactionID snowflake.ID, int return } +// CreateInteractionResponse responds to the interaction without returning the callback. +// If you need the callback, use CreateInteractionResponseWithCallback. func (s *interactionImpl) CreateInteractionResponse(interactionID snowflake.ID, interactionToken string, interactionResponse discord.InteractionResponse, opts ...RequestOpt) error { body, err := interactionResponse.ToBody() if err != nil { @@ -42,6 +45,18 @@ func (s *interactionImpl) CreateInteractionResponse(interactionID snowflake.ID, return s.client.Do(CreateInteractionResponse.Compile(nil, interactionID, interactionToken), body, nil, opts...) } +func (s *interactionImpl) CreateInteractionResponseWithCallback(interactionID snowflake.ID, interactionToken string, interactionResponse discord.InteractionResponse, opts ...RequestOpt) (callback *discord.InteractionCallbackResponse, err error) { + body, err := interactionResponse.ToBody() + if err != nil { + return nil, err + } + values := discord.QueryValues{ + "with_response": true, + } + err = s.client.Do(CreateInteractionResponse.Compile(values, interactionID, interactionToken), body, &callback, opts...) + return +} + func (s *interactionImpl) UpdateInteractionResponse(applicationID snowflake.ID, interactionToken string, messageUpdate discord.MessageUpdate, opts ...RequestOpt) (message *discord.Message, err error) { body, err := messageUpdate.ToBody() if err != nil {