diff --git a/.gitignore b/.gitignore index 34d2efa5b..681a96b19 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ # IDE-specific metadata .idea/ + +# Environment variables. Useful for examples. +.env diff --git a/endpoints.go b/endpoints.go index 89d56edab..ac8d4488a 100644 --- a/endpoints.go +++ b/endpoints.go @@ -121,6 +121,9 @@ var ( EndpointChannelWebhooks = func(cID string) string { return EndpointChannel(cID) + "/webhooks" } EndpointWebhook = func(wID string) string { return EndpointWebhooks + wID } EndpointWebhookToken = func(wID, token string) string { return EndpointWebhooks + wID + "/" + token } + EndpointWebhookMessage = func(wID, token, messageID string) string { + return EndpointWebhookToken(wID, token) + "/messages/" + messageID + } EndpointMessageReactionsAll = func(cID, mID string) string { return EndpointChannelMessage(cID, mID) + "/reactions" @@ -132,6 +135,35 @@ var ( return EndpointMessageReactions(cID, mID, eID) + "/" + uID } + EndpointApplicationGlobalCommands = func(aID string) string { + return EndpointApplication(aID) + "/commands" + } + EndpointApplicationGlobalCommand = func(aID, cID string) string { + return EndpointApplicationGlobalCommands(aID) + "/" + cID + } + + EndpointApplicationGuildCommands = func(aID, gID string) string { + return EndpointApplication(aID) + "/guilds/" + gID + "/commands" + } + EndpointApplicationGuildCommand = func(aID, gID, cID string) string { + return EndpointApplicationGuildCommands(aID, gID) + "/" + cID + } + EndpointInteraction = func(aID, iToken string) string { + return EndpointAPI + "interactions/" + aID + "/" + iToken + } + EndpointInteractionResponse = func(iID, iToken string) string { + return EndpointInteraction(iID, iToken) + "/callback" + } + EndpointInteractionResponseActions = func(aID, iToken string) string { + return EndpointWebhookMessage(aID, iToken, "@original") + } + EndpointFollowupMessage = func(aID, iToken string) string { + return EndpointWebhookToken(aID, iToken) + } + EndpointFollowupMessageActions = func(aID, iToken, mID string) string { + return EndpointWebhookMessage(aID, iToken, mID) + } + EndpointRelationships = func() string { return EndpointUsers + "@me" + "/relationships" } EndpointRelationship = func(uID string) string { return EndpointRelationships() + "/" + uID } EndpointRelationshipsMutual = func(uID string) string { return EndpointUsers + uID + "/relationships" } @@ -145,9 +177,19 @@ var ( EndpointEmoji = func(eID string) string { return EndpointCDN + "emojis/" + eID + ".png" } EndpointEmojiAnimated = func(eID string) string { return EndpointCDN + "emojis/" + eID + ".gif" } - EndpointOauth2 = EndpointAPI + "oauth2/" - EndpointApplications = EndpointOauth2 + "applications" - EndpointApplication = func(aID string) string { return EndpointApplications + "/" + aID } - EndpointApplicationsBot = func(aID string) string { return EndpointApplications + "/" + aID + "/bot" } - EndpointApplicationAssets = func(aID string) string { return EndpointApplications + "/" + aID + "/assets" } + EndpointApplications = EndpointAPI + "applications" + EndpointApplication = func(aID string) string { return EndpointApplications + "/" + aID } + + EndpointOAuth2 = EndpointAPI + "oauth2/" + EndpointOAuth2Applications = EndpointOAuth2 + "applications" + EndpointOAuth2Application = func(aID string) string { return EndpointOAuth2Applications + "/" + aID } + EndpointOAuth2ApplicationsBot = func(aID string) string { return EndpointOAuth2Applications + "/" + aID + "/bot" } + EndpointOAuth2ApplicationAssets = func(aID string) string { return EndpointOAuth2Applications + "/" + aID + "/assets" } + + // TODO: Deprecated, remove in the next release + EndpointOauth2 = EndpointOAuth2 + EndpointOauth2Applications = EndpointOAuth2Applications + EndpointOauth2Application = EndpointOAuth2Application + EndpointOauth2ApplicationsBot = EndpointOAuth2ApplicationsBot + EndpointOauth2ApplicationAssets = EndpointOAuth2ApplicationAssets ) diff --git a/eventhandlers.go b/eventhandlers.go index d2b9a98b7..4f5c4d92e 100644 --- a/eventhandlers.go +++ b/eventhandlers.go @@ -28,6 +28,7 @@ const ( guildRoleDeleteEventType = "GUILD_ROLE_DELETE" guildRoleUpdateEventType = "GUILD_ROLE_UPDATE" guildUpdateEventType = "GUILD_UPDATE" + interactionCreateEventType = "INTERACTION_CREATE" messageAckEventType = "MESSAGE_ACK" messageCreateEventType = "MESSAGE_CREATE" messageDeleteEventType = "MESSAGE_DELETE" @@ -458,6 +459,26 @@ func (eh guildUpdateEventHandler) Handle(s *Session, i interface{}) { } } +// interactionCreateEventHandler is an event handler for InteractionCreate events. +type interactionCreateEventHandler func(*Session, *InteractionCreate) + +// Type returns the event type for InteractionCreate events. +func (eh interactionCreateEventHandler) Type() string { + return interactionCreateEventType +} + +// New returns a new instance of InteractionCreate. +func (eh interactionCreateEventHandler) New() interface{} { + return &InteractionCreate{} +} + +// Handle is the handler for InteractionCreate events. +func (eh interactionCreateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*InteractionCreate); ok { + eh(s, t) + } +} + // messageAckEventHandler is an event handler for MessageAck events. type messageAckEventHandler func(*Session, *MessageAck) @@ -959,6 +980,8 @@ func handlerForInterface(handler interface{}) EventHandler { return guildRoleUpdateEventHandler(v) case func(*Session, *GuildUpdate): return guildUpdateEventHandler(v) + case func(*Session, *InteractionCreate): + return interactionCreateEventHandler(v) case func(*Session, *MessageAck): return messageAckEventHandler(v) case func(*Session, *MessageCreate): @@ -1029,6 +1052,7 @@ func init() { registerInterfaceProvider(guildRoleDeleteEventHandler(nil)) registerInterfaceProvider(guildRoleUpdateEventHandler(nil)) registerInterfaceProvider(guildUpdateEventHandler(nil)) + registerInterfaceProvider(interactionCreateEventHandler(nil)) registerInterfaceProvider(messageAckEventHandler(nil)) registerInterfaceProvider(messageCreateEventHandler(nil)) registerInterfaceProvider(messageDeleteEventHandler(nil)) diff --git a/events.go b/events.go index 7488dcc75..87ad7d9aa 100644 --- a/events.go +++ b/events.go @@ -267,3 +267,8 @@ type WebhooksUpdate struct { GuildID string `json:"guild_id"` ChannelID string `json:"channel_id"` } + +// InteractionCreate is the data for a InteractionCreate event +type InteractionCreate struct { + *Interaction +} diff --git a/examples/slash_commands/main.go b/examples/slash_commands/main.go new file mode 100644 index 000000000..3adcea851 --- /dev/null +++ b/examples/slash_commands/main.go @@ -0,0 +1,375 @@ +package main + +import ( + "flag" + "fmt" + "log" + "os" + "os/signal" + "time" + + "github.com/bwmarrin/discordgo" +) + +// Bot parameters +var ( + GuildID = flag.String("guild", "", "Test guild ID. If not passed - bot registers commands globally") + BotToken = flag.String("token", "", "Bot access token") + RemoveCommands = flag.Bool("rmcmd", true, "Remove all commands after shutdowning or not") +) + +var s *discordgo.Session + +func init() { flag.Parse() } + +func init() { + var err error + s, err = discordgo.New("Bot " + *BotToken) + if err != nil { + log.Fatalf("Invalid bot parameters: %v", err) + } +} + +var ( + commands = []*discordgo.ApplicationCommand{ + { + Name: "basic-command", + // All commands and options must have an description + // Commands/options without description will fail the registration + // of the command. + Description: "Basic command", + }, + { + Name: "options", + Description: "Command for demonstrating options", + Options: []*discordgo.ApplicationCommandOption{ + + { + Type: discordgo.ApplicationCommandOptionString, + Name: "string-option", + Description: "String option", + Required: true, + }, + { + Type: discordgo.ApplicationCommandOptionInteger, + Name: "integer-option", + Description: "Integer option", + Required: true, + }, + { + Type: discordgo.ApplicationCommandOptionBoolean, + Name: "bool-option", + Description: "Boolean option", + Required: true, + }, + + // Required options must be listed first, because + // like everyone knows - optional parameters is on the back. + // The same concept applies to Discord's Slash-commands API + + { + Type: discordgo.ApplicationCommandOptionChannel, + Name: "channel-option", + Description: "Channel option", + Required: false, + }, + { + Type: discordgo.ApplicationCommandOptionUser, + Name: "user-option", + Description: "User option", + Required: false, + }, + { + Type: discordgo.ApplicationCommandOptionRole, + Name: "role-option", + Description: "Role option", + Required: false, + }, + }, + }, + { + Name: "subcommands", + Description: "Subcommands and command groups example", + Options: []*discordgo.ApplicationCommandOption{ + // When command have subcommands/subcommand groups + // It must not have top-level options, they aren't accesible in the UI + // in this case (at least, yet), so if command is with + // subcommands/subcommand groups registering top-level options + // will fail the registration of the command + + { + Name: "scmd-grp", + Description: "Subcommands group", + Options: []*discordgo.ApplicationCommandOption{ + // Also, subcommand groups isn't capable of + // containg options, by the name of them, you can see + // they can contain only subcommands + { + Name: "nst-subcmd", + Description: "Nested subcommand", + Type: discordgo.ApplicationCommandOptionSubCommand, + }, + }, + Type: discordgo.ApplicationCommandOptionSubCommandGroup, + }, + // Also, you can create both subcommand groups and subcommands + // in the command at the same time. But, there's some limits to + // nesting, count of subcommands (top level and nested) and options. + // Read the intro of slash-commands docs on Discord dev portal + // to get more information + { + Name: "subcmd", + Description: "Top-level subcommand", + Type: discordgo.ApplicationCommandOptionSubCommand, + }, + }, + }, + { + Name: "responses", + Description: "Interaction responses testing initiative", + Options: []*discordgo.ApplicationCommandOption{ + { + Name: "resp-type", + Description: "Response type", + Type: discordgo.ApplicationCommandOptionInteger, + Choices: []*discordgo.ApplicationCommandOptionChoice{ + { + Name: "Acknowledge", + Value: 2, + }, + { + Name: "Channel message", + Value: 3, + }, + { + Name: "Channel message with source", + Value: 4, + }, + { + Name: "Acknowledge with source", + Value: 5, + }, + }, + }, + }, + }, + { + Name: "followups", + Description: "Followup messages", + }, + } + commandHandlers = map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){ + "basic-command": func(s *discordgo.Session, i *discordgo.InteractionCreate) { + s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionApplicationCommandResponseData{ + Content: "Hey there! Congratulations, you just executed your first slash command", + }, + }) + }, + "options": func(s *discordgo.Session, i *discordgo.InteractionCreate) { + margs := []interface{}{ + // Here we need to convert raw interface{} value to wanted type. + // Also, as you can see, here is used utility functions to convert the value + // to particular type. Yeah, you can use just switch type, + // but this is much simpler + i.Data.Options[0].StringValue(), + i.Data.Options[1].IntValue(), + i.Data.Options[2].BoolValue(), + } + msgformat := + ` Now you just leared how to use command options. Take a look to the value of which you've just entered: + > string_option: %s + > integer_option: %d + > bool_option: %v +` + if len(i.Data.Options) >= 4 { + margs = append(margs, i.Data.Options[3].ChannelValue(nil).ID) + msgformat += "> channel-option: <#%s>\n" + } + if len(i.Data.Options) >= 5 { + margs = append(margs, i.Data.Options[4].UserValue(nil).ID) + msgformat += "> user-option: <@%s>\n" + } + if len(i.Data.Options) >= 6 { + margs = append(margs, i.Data.Options[5].RoleValue(nil, "").ID) + msgformat += "> role-option: <@&%s>\n" + } + s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + // Ignore type for now, we'll discuss them in "responses" part + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionApplicationCommandResponseData{ + Content: fmt.Sprintf( + msgformat, + margs..., + ), + }, + }) + }, + "subcommands": func(s *discordgo.Session, i *discordgo.InteractionCreate) { + content := "" + + // As you can see, the name of subcommand (nested, top-level) or subcommand group + // is provided through arguments. + switch i.Data.Options[0].Name { + case "subcmd": + content = + "The top-level subcommand is executed. Now try to execute nested one." + default: + if i.Data.Options[0].Name != "scmd-grp" { + return + } + switch i.Data.Options[0].Options[0].Name { + case "nst-subcmd": + content = "Nice, now you know how to execute nested commands too" + default: + // I added this in the case something might go wrong + content = "Oops, something gone wrong.\n" + + "Hol' up, you aren't supposed to see this message." + } + } + s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionApplicationCommandResponseData{ + Content: content, + }, + }) + }, + "responses": func(s *discordgo.Session, i *discordgo.InteractionCreate) { + // Responses to a command is really important thing. + // First of all, because you need to react to the interaction + // by sending the response in 3 seconds after receiving, otherwise + // interaction will be considered invalid and you can no longer + // use interaction token and ID for responding to the user's request + + content := "" + // As you can see, response type names saying by themselvs + // how they're used, but for those who want to get + // more information - read the official documentation + switch i.Data.Options[0].IntValue() { + case int64(discordgo.InteractionResponseChannelMessage): + content = + "Well, you just responded to an interaction, and sent a message.\n" + + "That's all what I wanted to say, yeah." + content += + "\nAlso... you can edit your response, wait 5 seconds and this message will be changed" + case int64(discordgo.InteractionResponseChannelMessageWithSource): + content = + "You just responded to an interaction, sent a message and showed the original one. " + + "Congratulations!" + content += + "\nAlso... you can edit your response, wait 5 seconds and this message will be changed" + default: + err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseType(i.Data.Options[0].IntValue()), + }) + if err != nil { + s.FollowupMessageCreate("", i.Interaction, true, &discordgo.WebhookParams{ + Content: "Something gone wrong", + }) + } + return + } + + err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseType(i.Data.Options[0].IntValue()), + Data: &discordgo.InteractionApplicationCommandResponseData{ + Content: content, + }, + }) + if err != nil { + s.FollowupMessageCreate("", i.Interaction, true, &discordgo.WebhookParams{ + Content: "Something gone wrong", + }) + return + } + time.AfterFunc(time.Second*5, func() { + err = s.InteractionResponseEdit("", i.Interaction, &discordgo.WebhookEdit{ + Content: content + "\n\nWell, now you know how to create and edit responses. " + + "But you still don't know how to delete them... so... wait 10 seconds and this " + + "message will be deleted.", + }) + if err != nil { + s.FollowupMessageCreate("", i.Interaction, true, &discordgo.WebhookParams{ + Content: "Something gone wrong", + }) + return + } + time.Sleep(time.Second * 10) + s.InteractionResponseDelete("", i.Interaction) + }) + }, + "followups": func(s *discordgo.Session, i *discordgo.InteractionCreate) { + // Followup messages is basically regular messages (you can create as many of them as you wish), + // but working as they is created by webhooks and their functional + // is for handling additional messages after sending response. + + s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionApplicationCommandResponseData{ + // Note: this isn't documented, but you can use that if you want to. + // This flag just allows to create messages visible only for the caller (user who triggered the command) + // of the command + Flags: 1 << 6, + Content: "Surprise!", + }, + }) + msg, err := s.FollowupMessageCreate("", i.Interaction, true, &discordgo.WebhookParams{ + Content: "Followup message has created, after 5 seconds it will be edited", + }) + if err != nil { + s.FollowupMessageCreate("", i.Interaction, true, &discordgo.WebhookParams{ + Content: "Something gone wrong", + }) + return + } + time.Sleep(time.Second * 5) + + s.FollowupMessageEdit("", i.Interaction, msg.ID, &discordgo.WebhookEdit{ + Content: "Now original message is gone and after 10 seconds this message will ~~self-destruct~~ be deleted.", + }) + + time.Sleep(time.Second * 10) + + s.FollowupMessageDelete("", i.Interaction, msg.ID) + + s.FollowupMessageCreate("", i.Interaction, true, &discordgo.WebhookParams{ + Content: "For those, who didn't skip anything and followed tutorial along fairly, " + + "take a unicorn :unicorn: as reward!\n" + + "Also, as bonus..., look at the original interaction response :D", + }) + }, + } +) + +func init() { + s.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) { + if h, ok := commandHandlers[i.Data.Name]; ok { + h(s, i) + } + }) +} + +func main() { + s.AddHandler(func(s *discordgo.Session, r *discordgo.Ready) { + log.Println("Bot is up!") + }) + err := s.Open() + if err != nil { + log.Fatalf("Cannot open the session: %v", err) + } + + for _, v := range commands { + _, err := s.ApplicationCommandCreate("", *GuildID, v) + if err != nil { + log.Panicf("Cannot create '%v' command: %v", v.Name, err) + } + } + + defer s.Close() + + stop := make(chan os.Signal) + signal.Notify(stop, os.Interrupt) + <-stop + log.Println("Gracefully shutdowning") +} diff --git a/interactions.go b/interactions.go index 6fc2f55ed..d4ccbe817 100644 --- a/interactions.go +++ b/interactions.go @@ -7,8 +7,245 @@ import ( "io" "io/ioutil" "net/http" + "time" ) +// InteractionDeadline is the time allowed to respond to an interaction. +const InteractionDeadline = time.Second * 3 + +// ApplicationCommand represents an application's slash command. +type ApplicationCommand struct { + ID string `json:"id"` + ApplicationID string `json:"application_id,omitempty"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Version string `json:"version,omitempty"` + Options []*ApplicationCommandOption `json:"options"` +} + +// ApplicationCommandOptionType indicates the type of a slash command's option. +type ApplicationCommandOptionType uint8 + +// Application command option types. +const ( + ApplicationCommandOptionSubCommand = ApplicationCommandOptionType(iota + 1) + ApplicationCommandOptionSubCommandGroup + ApplicationCommandOptionString + ApplicationCommandOptionInteger + ApplicationCommandOptionBoolean + ApplicationCommandOptionUser + ApplicationCommandOptionChannel + ApplicationCommandOptionRole +) + +// ApplicationCommandOption represents an option/subcommand/subcommands group. +type ApplicationCommandOption struct { + Type ApplicationCommandOptionType `json:"type"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + // NOTE: This feature was on the API, but at some point developers decided to remove it. + // So I commented it, until it will be officially on the docs. + // Default bool `json:"default"` + Required bool `json:"required"` + Choices []*ApplicationCommandOptionChoice `json:"choices"` + Options []*ApplicationCommandOption `json:"options"` +} + +// ApplicationCommandOptionChoice represents a slash command option choice. +type ApplicationCommandOptionChoice struct { + Name string `json:"name"` + Value interface{} `json:"value"` +} + +// InteractionType indicates the type of an interaction event. +type InteractionType uint8 + +// Interaction types +const ( + InteractionPing = InteractionType(iota + 1) + InteractionApplicationCommand +) + +// Interaction represents an interaction event created via a slash command. +type Interaction struct { + ID string `json:"id"` + Type InteractionType `json:"type"` + Data ApplicationCommandInteractionData `json:"data"` + GuildID string `json:"guild_id"` + ChannelID string `json:"channel_id"` + Member *Member `json:"member"` + Token string `json:"token"` + Version int `json:"version"` +} + +// ApplicationCommandInteractionData contains data received in an interaction event. +type ApplicationCommandInteractionData struct { + ID string `json:"id"` + Name string `json:"name"` + Options []*ApplicationCommandInteractionDataOption `json:"options"` +} + +// ApplicationCommandInteractionDataOption represents an option of a slash command. +type ApplicationCommandInteractionDataOption struct { + Name string `json:"name"` + // NOTE: Contains the value specified by InteractionType. + Value interface{} `json:"value,omitempty"` + Options []*ApplicationCommandInteractionDataOption `json:"options,omitempty"` +} + +// IntValue is a utility function for casting option value to integer +func (o ApplicationCommandInteractionDataOption) IntValue() int64 { + if v, ok := o.Value.(float64); ok { + return int64(v) + } + + return 0 +} + +// UintValue is a utility function for casting option value to unsigned integer +func (o ApplicationCommandInteractionDataOption) UintValue() uint64 { + if v, ok := o.Value.(float64); ok { + return uint64(v) + } + + return 0 +} + +// FloatValue is a utility function for casting option value to float +func (o ApplicationCommandInteractionDataOption) FloatValue() float64 { + if v, ok := o.Value.(float64); ok { + return v + } + + return 0.0 +} + +// StringValue is a utility function for casting option value to string +func (o ApplicationCommandInteractionDataOption) StringValue() string { + if v, ok := o.Value.(string); ok { + return v + } + + return "" +} + +// BoolValue is a utility function for casting option value to bool +func (o ApplicationCommandInteractionDataOption) BoolValue() bool { + if v, ok := o.Value.(bool); ok { + return v + } + + return false +} + +// ChannelValue is a utility function for casting option value to channel object. +// s : Session object, if not nil, function additionaly fetches all channel's data +func (o ApplicationCommandInteractionDataOption) ChannelValue(s *Session) *Channel { + chanID := o.StringValue() + if chanID == "" { + return nil + } + + if s == nil { + return &Channel{ID: chanID} + } + + ch, err := s.State.Channel(chanID) + if err != nil { + ch, err = s.Channel(chanID) + if err != nil { + return &Channel{ID: chanID} + } + } + + return ch +} + +// RoleValue is a utility function for casting option value to role object. +// s : Session object, if not nil, function additionaly fetches all role's data +func (o ApplicationCommandInteractionDataOption) RoleValue(s *Session, gID string) *Role { + roleID := o.StringValue() + if roleID == "" { + return nil + } + + if s == nil || gID == "" { + return &Role{ID: roleID} + } + + r, err := s.State.Role(roleID, gID) + if err != nil { + roles, err := s.GuildRoles(gID) + if err == nil { + for _, r = range roles { + if r.ID == roleID { + return r + } + } + } + return &Role{ID: roleID} + } + + return r +} + +// UserValue is a utility function for casting option value to user object. +// s : Session object, if not nil, function additionaly fetches all user's data +func (o ApplicationCommandInteractionDataOption) UserValue(s *Session) *User { + userID := o.StringValue() + if userID == "" { + return nil + } + + if s == nil { + return &User{ID: userID} + } + + u, err := s.User(userID) + if err != nil { + return &User{ID: userID} + } + + return u +} + +// InteractionResponseType is type of interaction response. +type InteractionResponseType uint8 + +// Interaction response types. +const ( + // InteractionResponsePong is for ACK ping event. + InteractionResponsePong = InteractionResponseType(iota + 1) + // InteractionResponseAcknowledge is for ACK a command without sending a message, eating the user's input. + // NOTE: this type is being imminently deprecated, and **will be removed when this occurs.** + InteractionResponseAcknowledge + // InteractionResponseChannelMessage is for responding with a message, eating the user's input. + // NOTE: this type is being imminently deprecated, and **will be removed when this occurs.** + InteractionResponseChannelMessage + // InteractionResponseChannelMessageWithSource is for responding with a message, showing the user's input. + InteractionResponseChannelMessageWithSource + // InteractionResponseDeferredChannelMessageWithSource acknowledges that the event was received, and that a follow-up will come later. + // It was previously named InteractionResponseACKWithSource. + InteractionResponseDeferredChannelMessageWithSource +) + +// InteractionResponse represents a response for an interaction event. +type InteractionResponse struct { + Type InteractionResponseType `json:"type,omitempty"` + Data *InteractionApplicationCommandResponseData `json:"data,omitempty"` +} + +// InteractionApplicationCommandResponseData is response data for a slash command interaction. +type InteractionApplicationCommandResponseData struct { + TTS bool `json:"tts,omitempty"` + Content string `json:"content,omitempty"` + Embeds []*MessageEmbed `json:"embeds,omitempty"` + AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"` + + // NOTE: Undocumented feature, be careful with it. + Flags uint64 `json:"flags,omitempty"` +} + // VerifyInteraction implements message verification of the discord interactions api // signing algorithm, as documented here: // https://discord.com/developers/docs/interactions/slash-commands#security-and-authorization diff --git a/oauth2.go b/oauth2.go index 289eca949..cafc04070 100644 --- a/oauth2.go +++ b/oauth2.go @@ -61,7 +61,7 @@ type Application struct { // appID : The ID of an Application func (s *Session) Application(appID string) (st *Application, err error) { - body, err := s.RequestWithBucketID("GET", EndpointApplication(appID), nil, EndpointApplication("")) + body, err := s.RequestWithBucketID("GET", EndpointOAuth2Application(appID), nil, EndpointOAuth2Application("")) if err != nil { return } @@ -73,7 +73,7 @@ func (s *Session) Application(appID string) (st *Application, err error) { // Applications returns all applications for the authenticated user func (s *Session) Applications() (st []*Application, err error) { - body, err := s.RequestWithBucketID("GET", EndpointApplications, nil, EndpointApplications) + body, err := s.RequestWithBucketID("GET", EndpointOAuth2Applications, nil, EndpointOAuth2Applications) if err != nil { return } @@ -93,7 +93,7 @@ func (s *Session) ApplicationCreate(ap *Application) (st *Application, err error RedirectURIs *[]string `json:"redirect_uris,omitempty"` }{ap.Name, ap.Description, ap.RedirectURIs} - body, err := s.RequestWithBucketID("POST", EndpointApplications, data, EndpointApplications) + body, err := s.RequestWithBucketID("POST", EndpointOAuth2Applications, data, EndpointOAuth2Applications) if err != nil { return } @@ -112,7 +112,7 @@ func (s *Session) ApplicationUpdate(appID string, ap *Application) (st *Applicat RedirectURIs *[]string `json:"redirect_uris,omitempty"` }{ap.Name, ap.Description, ap.RedirectURIs} - body, err := s.RequestWithBucketID("PUT", EndpointApplication(appID), data, EndpointApplication("")) + body, err := s.RequestWithBucketID("PUT", EndpointOAuth2Application(appID), data, EndpointOAuth2Application("")) if err != nil { return } @@ -125,7 +125,7 @@ func (s *Session) ApplicationUpdate(appID string, ap *Application) (st *Applicat // appID : The ID of an Application func (s *Session) ApplicationDelete(appID string) (err error) { - _, err = s.RequestWithBucketID("DELETE", EndpointApplication(appID), nil, EndpointApplication("")) + _, err = s.RequestWithBucketID("DELETE", EndpointOAuth2Application(appID), nil, EndpointOAuth2Application("")) if err != nil { return } @@ -143,7 +143,7 @@ type Asset struct { // ApplicationAssets returns an application's assets func (s *Session) ApplicationAssets(appID string) (ass []*Asset, err error) { - body, err := s.RequestWithBucketID("GET", EndpointApplicationAssets(appID), nil, EndpointApplicationAssets("")) + body, err := s.RequestWithBucketID("GET", EndpointOAuth2ApplicationAssets(appID), nil, EndpointOAuth2ApplicationAssets("")) if err != nil { return } @@ -163,7 +163,7 @@ func (s *Session) ApplicationAssets(appID string) (ass []*Asset, err error) { // NOTE: func name may change, if I can think up something better. func (s *Session) ApplicationBotCreate(appID string) (st *User, err error) { - body, err := s.RequestWithBucketID("POST", EndpointApplicationsBot(appID), nil, EndpointApplicationsBot("")) + body, err := s.RequestWithBucketID("POST", EndpointOAuth2ApplicationsBot(appID), nil, EndpointOAuth2ApplicationsBot("")) if err != nil { return } diff --git a/ratelimit.go b/ratelimit.go index cd96eadf6..c992fd45e 100644 --- a/ratelimit.go +++ b/ratelimit.go @@ -33,7 +33,7 @@ func NewRatelimiter() *RateLimiter { buckets: make(map[string]*Bucket), global: new(int64), customRateLimits: []*customRateLimit{ - &customRateLimit{ + { suffix: "//reactions//", requests: 1, reset: 200 * time.Millisecond, diff --git a/restapi.go b/restapi.go index fc89e7fe0..cfbe320cc 100644 --- a/restapi.go +++ b/restapi.go @@ -2168,7 +2168,29 @@ func (s *Session) WebhookExecute(webhookID, token string, wait bool, data *Webho } err = unmarshal(response, &st) + return +} + +// WebhookMessageEdit edits a webhook message. +// webhookID : The ID of a webhook +// token : The auth token for the webhook +// messageID : The ID of message to edit +func (s *Session) WebhookMessageEdit(webhookID, token, messageID string, data *WebhookEdit) (err error) { + uri := EndpointWebhookMessage(webhookID, token, messageID) + + _, err = s.RequestWithBucketID("PATCH", uri, data, EndpointWebhookToken("", "")) + + return +} + +// WebhookMessageDelete deletes a webhook message. +// webhookID : The ID of a webhook +// token : The auth token for the webhook +// messageID : The ID of message to edit +func (s *Session) WebhookMessageDelete(webhookID, token, messageID string) (err error) { + uri := EndpointWebhookMessage(webhookID, token, messageID) + _, err = s.RequestWithBucketID("DELETE", uri, nil, EndpointWebhookToken("", "")) return } @@ -2339,3 +2361,159 @@ func (s *Session) RelationshipsMutualGet(userID string) (mf []*User, err error) err = unmarshal(body, &mf) return } + +// ------------------------------------------------------------------------------------------------ +// Functions specific to application (slash) commands +// ------------------------------------------------------------------------------------------------ + +// ApplicationCommandCreate creates a global application command and returns it. +// appID : The application ID. +// guildID : Guild ID to create guild-specific application command. If empty - creates global application command. +// cmd : New application command data. +func (s *Session) ApplicationCommandCreate(appID string, guildID string, cmd *ApplicationCommand) (ccmd *ApplicationCommand, err error) { + endpoint := EndpointApplicationGlobalCommands(appID) + if guildID != "" { + endpoint = EndpointApplicationGuildCommands(appID, guildID) + } + + body, err := s.RequestWithBucketID("POST", endpoint, *cmd, endpoint) + if err != nil { + return + } + + err = unmarshal(body, &ccmd) + + return +} + +// ApplicationCommandEdit edits application command and returns new command data. +// appID : The application ID. +// cmdID : Application command ID to edit. +// guildID : Guild ID to edit guild-specific application command. If empty - edits global application command. +// cmd : Updated application command data. +func (s *Session) ApplicationCommandEdit(appID, guildID, cmdID string, cmd *ApplicationCommand) (updated *ApplicationCommand, err error) { + endpoint := EndpointApplicationGlobalCommand(appID, cmdID) + if guildID != "" { + endpoint = EndpointApplicationGuildCommand(appID, guildID, cmdID) + } + + body, err := s.RequestWithBucketID("PATCH", endpoint, *cmd, endpoint) + if err != nil { + return + } + + err = unmarshal(body, &updated) + + return +} + +// ApplicationCommandDelete deletes application command by ID. +// appID : The application ID. +// cmdID : Application command ID to delete. +// guildID : Guild ID to delete guild-specific application command. If empty - deletes global application command. +func (s *Session) ApplicationCommandDelete(appID, guildID, cmdID string) error { + endpoint := EndpointApplicationGlobalCommand(appID, cmdID) + if guildID != "" { + endpoint = EndpointApplicationGuildCommand(appID, guildID, cmdID) + } + + _, err := s.RequestWithBucketID("DELETE", endpoint, nil, endpoint) + + return err +} + +// ApplicationCommand retrieves an application command by given ID. +// appID : The application ID. +// cmdID : Application command ID. +// guildID : Guild ID to retrieve guild-specific application command. If empty - retrieves global application command. +func (s *Session) ApplicationCommand(appID, guildID, cmdID string) (cmd *ApplicationCommand, err error) { + endpoint := EndpointApplicationGlobalCommand(appID, cmdID) + if guildID != "" { + endpoint = EndpointApplicationGuildCommand(appID, guildID, cmdID) + } + + body, err := s.RequestWithBucketID("GET", endpoint, nil, endpoint) + if err != nil { + return + } + + err = unmarshal(body, &cmd) + + return +} + +// ApplicationCommands retrieves all commands in application. +// appID : The application ID. +// guildID : Guild ID to retrieve all guild-specific application commands. If empty - retrieves global application commands. +func (s *Session) ApplicationCommands(appID, guildID string) (cmd []*ApplicationCommand, err error) { + endpoint := EndpointApplicationGlobalCommands(appID) + if guildID != "" { + endpoint = EndpointApplicationGuildCommands(appID, guildID) + } + + body, err := s.RequestWithBucketID("GET", endpoint, nil, endpoint) + if err != nil { + return + } + + err = unmarshal(body, &cmd) + + return +} + +// InteractionRespond creates the response to an interaction. +// appID : The application ID. +// interaction : Interaction instance. +// resp : Response message data. +func (s *Session) InteractionRespond(interaction *Interaction, resp *InteractionResponse) error { + endpoint := EndpointInteractionResponse(interaction.ID, interaction.Token) + + _, err := s.RequestWithBucketID("POST", endpoint, *resp, endpoint) + + return err +} + +// InteractionResponseEdit edits the response to an interaction. +// appID : The application ID. +// interaction : Interaction instance. +// newresp : Updated response message data. +func (s *Session) InteractionResponseEdit(appID string, interaction *Interaction, newresp *WebhookEdit) error { + return s.WebhookMessageEdit(appID, interaction.Token, "@original", newresp) +} + +// InteractionResponseDelete deletes the response to an interaction. +// appID : The application ID. +// interaction : Interaction instance. +func (s *Session) InteractionResponseDelete(appID string, interaction *Interaction) error { + endpoint := EndpointInteractionResponseActions(appID, interaction.Token) + + _, err := s.RequestWithBucketID("DELETE", endpoint, nil, endpoint) + + return err +} + +// FollowupMessageCreate creates the followup message for an interaction. +// appID : The application ID. +// interaction : Interaction instance. +// wait : Waits for server confirmation of message send and ensures that the return struct is populated (it is nil otherwise) +// data : Data of the message to send. +func (s *Session) FollowupMessageCreate(appID string, interaction *Interaction, wait bool, data *WebhookParams) (*Message, error) { + return s.WebhookExecute(appID, interaction.Token, wait, data) +} + +// FollowupMessageEdit edits a followup message of an interaction. +// appID : The application ID. +// interaction : Interaction instance. +// messageID : The followup message ID. +// data : Data to update the message +func (s *Session) FollowupMessageEdit(appID string, interaction *Interaction, messageID string, data *WebhookEdit) error { + return s.WebhookMessageEdit(appID, interaction.Token, messageID, data) +} + +// FollowupMessageDelete deletes a followup message of an interaction. +// appID : The application ID. +// interaction : Interaction instance. +// messageID : The followup message ID. +func (s *Session) FollowupMessageDelete(appID string, interaction *Interaction, messageID string) error { + return s.WebhookMessageDelete(appID, interaction.Token, messageID) +} diff --git a/structs.go b/structs.go index eb84ea80f..c457b304f 100644 --- a/structs.go +++ b/structs.go @@ -1053,42 +1053,6 @@ type APIErrorMessage struct { Message string `json:"message"` } -// Webhook stores the data for a webhook. -type Webhook struct { - ID string `json:"id"` - Type WebhookType `json:"type"` - GuildID string `json:"guild_id"` - ChannelID string `json:"channel_id"` - User *User `json:"user"` - Name string `json:"name"` - Avatar string `json:"avatar"` - Token string `json:"token"` - - // ApplicationID is the bot/OAuth2 application that created this webhook - ApplicationID string `json:"application_id,omitempty"` -} - -// WebhookType is the type of Webhook (see WebhookType* consts) in the Webhook struct -// https://discord.com/developers/docs/resources/webhook#webhook-object-webhook-types -type WebhookType int - -// Valid WebhookType values -const ( - WebhookTypeIncoming WebhookType = iota - WebhookTypeChannelFollower -) - -// WebhookParams is a struct for webhook params, used in the WebhookExecute command. -type WebhookParams struct { - Content string `json:"content,omitempty"` - Username string `json:"username,omitempty"` - AvatarURL string `json:"avatar_url,omitempty"` - TTS bool `json:"tts,omitempty"` - File string `json:"file,omitempty"` - Embeds []*MessageEmbed `json:"embeds,omitempty"` - AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"` -} - // MessageReaction stores the data for a message reaction. type MessageReaction struct { UserID string `json:"user_id"` diff --git a/webhook.go b/webhook.go new file mode 100644 index 000000000..b8b3abcde --- /dev/null +++ b/webhook.go @@ -0,0 +1,44 @@ +package discordgo + +// Webhook stores the data for a webhook. +type Webhook struct { + ID string `json:"id"` + Type WebhookType `json:"type"` + GuildID string `json:"guild_id"` + ChannelID string `json:"channel_id"` + User *User `json:"user"` + Name string `json:"name"` + Avatar string `json:"avatar"` + Token string `json:"token"` + + // ApplicationID is the bot/OAuth2 application that created this webhook + ApplicationID string `json:"application_id,omitempty"` +} + +// WebhookType is the type of Webhook (see WebhookType* consts) in the Webhook struct +// https://discord.com/developers/docs/resources/webhook#webhook-object-webhook-types +type WebhookType int + +// Valid WebhookType values +const ( + WebhookTypeIncoming WebhookType = iota + WebhookTypeChannelFollower +) + +// WebhookParams is a struct for webhook params, used in the WebhookExecute command. +type WebhookParams struct { + Content string `json:"content,omitempty"` + Username string `json:"username,omitempty"` + AvatarURL string `json:"avatar_url,omitempty"` + TTS bool `json:"tts,omitempty"` + File string `json:"file,omitempty"` + Embeds []*MessageEmbed `json:"embeds,omitempty"` + AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"` +} + +// WebhookEdit stores data for editing of a webhook message. +type WebhookEdit struct { + Content string `json:"content,omitempty"` + Embeds []*MessageEmbed `json:"embeds,omitempty"` + AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"` +}