Browse Source

Slash commands (#856)

* UnknownBan error code addition

* GuildBan method implementation

* Gofmt fix

Gofmt fix

* Interactions: application commands basic API and gateway integration

* Some gitignore update

* Application commands and interactions API implementation

* Some fixes

* Some improvements of slash-commands example and slash-commands API

* OAuth2 endpoints backward compatibility

* Gofmt fix

* Requested fixes and documentation improvement for application commands

* Some fixes

* New and more interesting example of slash-commands usage, merging "interaction.go" and "interactions.go" into a single file. And some fixes.

* Gofmt and documentation fixes

* More fixes

* Gofmt fixes

* More fixes!

* Doc and endpoint fixes

* Gofmt fix

* Remove dependence on open gateway connection

* Remove redundant command ID checks

* Fix typo in ApplicationCommandCreate comment

* Tidy up function calls returning body

* Add upcoming API changes

* Correct return value name, swap parameter order

* Add Version field to ApplicationCommand

* Fix up language in comments

* Remove redundant conversion to float64

Co-authored-by: Carson Hoffman <c@rsonhoffman.com>
Fedor Lapshin 3 years ago
parent
commit
b0fa920925
11 changed files with 921 additions and 49 deletions
  1. 3 0
      .gitignore
  2. 47 5
      endpoints.go
  3. 24 0
      eventhandlers.go
  4. 5 0
      events.go
  5. 375 0
      examples/slash_commands/main.go
  6. 237 0
      interactions.go
  7. 7 7
      oauth2.go
  8. 1 1
      ratelimit.go
  9. 178 0
      restapi.go
  10. 0 36
      structs.go
  11. 44 0
      webhook.go

+ 3 - 0
.gitignore

@@ -1,2 +1,5 @@
 # IDE-specific metadata
 .idea/
+
+# Environment variables. Useful for examples.
+.env

+ 47 - 5
endpoints.go

@@ -122,6 +122,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"
@@ -133,6 +136,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" }
@@ -146,9 +178,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
 )

+ 24 - 0
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))

+ 5 - 0
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
+}

+ 375 - 0
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")
+}

+ 237 - 0
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

+ 7 - 7
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
 	}

+ 1 - 1
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,

+ 178 - 0
restapi.go

@@ -2180,7 +2180,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
 }
 
@@ -2351,3 +2373,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)
+}

+ 0 - 36
structs.go

@@ -1087,42 +1087,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"`

+ 44 - 0
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"`
+}