瀏覽代碼

Merge branch 'base' into custom_master

Thisnthat 2 年之前
父節點
當前提交
52f93a6c80
共有 32 個文件被更改,包括 3328 次插入985 次删除
  1. 13 0
      .github/release.yml
  2. 60 0
      .github/workflows/ci.yml
  3. 19 0
      .golangci.yml
  4. 1 1
      README.md
  5. 60 11
      components.go
  6. 10 112
      discord.go
  7. 137 38
      discord_test.go
  8. 二進制
      docs/img/discordgo.png
  9. 45 0
      docs/img/discordgo.svg
  10. 86 72
      endpoints.go
  11. 1 1
      event.go
  12. 309 45
      eventhandlers.go
  13. 77 0
      events.go
  14. 255 0
      examples/autocomplete/main.go
  15. 1 1
      examples/avatar/main.go
  16. 2 1
      examples/components/main.go
  17. 160 0
      examples/modals/main.go
  18. 84 0
      examples/scheduled_events/main.go
  19. 48 11
      examples/slash_commands/main.go
  20. 74 0
      examples/threads/main.go
  21. 143 30
      interactions.go
  22. 83 0
      locales.go
  23. 62 16
      message.go
  24. 6 25
      oauth2.go
  25. 538 239
      restapi.go
  26. 0 58
      restapi_test.go
  27. 221 58
      state.go
  28. 785 170
      structs.go
  29. 0 57
      types.go
  30. 0 24
      types_test.go
  31. 15 15
      user.go
  32. 33 0
      util.go

+ 13 - 0
.github/release.yml

@@ -0,0 +1,13 @@
+changelog:
+  categories:
+    - title: "Breaking changes"
+      labels:
+        - breaking changes
+    
+    - title: "New features"
+      labels:
+        - feature
+
+    - title: "Other changes"
+      labels:
+        - "*"

+ 60 - 0
.github/workflows/ci.yml

@@ -0,0 +1,60 @@
+on:
+  push:
+  pull_request:
+
+name: CI
+
+jobs:
+  format:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Install Go
+        uses: actions/setup-go@v2
+        with:
+          go-version: 1.17
+      - name: Code
+        uses: actions/checkout@v2
+      - name: Check diff between gofmt and code
+        run: diff <(gofmt -d .) <(echo -n)
+  
+  test:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        go-version: [1.13, 1.14, 1.15, 1.16, 1.17]
+    steps:
+      - name: Install Go
+        uses: actions/setup-go@v2
+        with:
+          go-version: ${{ matrix.go-version }}
+      - name: Code
+        uses: actions/checkout@v2
+      - run: go test -v -race ./...
+  
+  lint:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Install Go
+        uses: actions/setup-go@v2
+        with:
+          go-version: 1.17
+      - name: Code
+        uses: actions/checkout@v2
+      - name: Go vet
+        run: go vet -x ./...
+
+      - name: GolangCI-Lint
+        uses: golangci/golangci-lint-action@v2.5.2
+        if: github.event.name == 'pull_request'
+        with:
+          only-new-issues: true
+          skip-pkg-cache: true
+          skip-build-cache: true
+      
+      - name: GolangCI-Lint
+        if: github.event.name != 'pull_request' # See https://github.com/golangci/golangci-lint-action/issues/362
+        run: |
+          curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.43.0
+
+          $(go env GOPATH)/bin/golangci-lint run
+

+ 19 - 0
.golangci.yml

@@ -0,0 +1,19 @@
+linters:
+  disable-all: true
+  enable:
+    # - staticcheck
+    # - unused
+    - golint
+
+linters-settings:
+  staticcheck:
+    go: "1.13"
+
+    checks: ["all"]
+
+  unused:
+    go: "1.13"
+
+issues:
+  include:
+    - EXC0002

File diff suppressed because it is too large
+ 1 - 1
README.md


+ 60 - 11
components.go

@@ -2,6 +2,7 @@ package discordgo
 
 import (
 	"encoding/json"
+	"fmt"
 )
 
 // ComponentType is type of component.
@@ -12,6 +13,7 @@ const (
 	ActionsRowComponent ComponentType = 1
 	ButtonComponent     ComponentType = 2
 	SelectMenuComponent ComponentType = 3
+	TextInputComponent  ComponentType = 4
 )
 
 // MessageComponent is a base interface for all message components.
@@ -34,22 +36,29 @@ func (umc *unmarshalableMessageComponent) UnmarshalJSON(src []byte) error {
 		return err
 	}
 
-	var data MessageComponent
 	switch v.Type {
 	case ActionsRowComponent:
-		v := ActionsRow{}
-		err = json.Unmarshal(src, &v)
-		data = v
+		umc.MessageComponent = &ActionsRow{}
 	case ButtonComponent:
-		v := Button{}
-		err = json.Unmarshal(src, &v)
-		data = v
+		umc.MessageComponent = &Button{}
+	case SelectMenuComponent:
+		umc.MessageComponent = &SelectMenu{}
+	case TextInputComponent:
+		umc.MessageComponent = &TextInput{}
+	default:
+		return fmt.Errorf("unknown component type: %d", v.Type)
 	}
+	return json.Unmarshal(src, umc.MessageComponent)
+}
+
+// MessageComponentFromJSON is a helper function for unmarshaling message components
+func MessageComponentFromJSON(b []byte) (MessageComponent, error) {
+	var u unmarshalableMessageComponent
+	err := u.UnmarshalJSON(b)
 	if err != nil {
-		return err
+		return nil, fmt.Errorf("failed to unmarshal into MessageComponent: %w", err)
 	}
-	umc.MessageComponent = data
-	return err
+	return u.MessageComponent, nil
 }
 
 // ActionsRow is a container for components within one row.
@@ -166,11 +175,12 @@ type SelectMenu struct {
 	// The text which will be shown in the menu if there's no default options or all options was deselected and component was closed.
 	Placeholder string `json:"placeholder"`
 	// This value determines the minimal amount of selected items in the menu.
-	MinValues int `json:"min_values,omitempty"`
+	MinValues *int `json:"min_values,omitempty"`
 	// This value determines the maximal amount of selected items in the menu.
 	// If MaxValues or MinValues are greater than one then the user can select multiple items in the component.
 	MaxValues int                `json:"max_values,omitempty"`
 	Options   []SelectMenuOption `json:"options"`
+	Disabled  bool               `json:"disabled"`
 }
 
 // Type is a method to get the type of a component.
@@ -190,3 +200,42 @@ func (m SelectMenu) MarshalJSON() ([]byte, error) {
 		Type:       m.Type(),
 	})
 }
+
+// TextInput represents text input component.
+type TextInput struct {
+	CustomID    string         `json:"custom_id"`
+	Label       string         `json:"label"`
+	Style       TextInputStyle `json:"style"`
+	Placeholder string         `json:"placeholder,omitempty"`
+	Value       string         `json:"value,omitempty"`
+	Required    bool           `json:"required"`
+	MinLength   int            `json:"min_length,omitempty"`
+	MaxLength   int            `json:"max_length,omitempty"`
+}
+
+// Type is a method to get the type of a component.
+func (TextInput) Type() ComponentType {
+	return TextInputComponent
+}
+
+// MarshalJSON is a method for marshaling TextInput to a JSON object.
+func (m TextInput) MarshalJSON() ([]byte, error) {
+	type inputText TextInput
+
+	return json.Marshal(struct {
+		inputText
+		Type ComponentType `json:"type"`
+	}{
+		inputText: inputText(m),
+		Type:      m.Type(),
+	})
+}
+
+// TextInputStyle is style of text in TextInput component.
+type TextInputStyle uint
+
+// Text styles
+const (
+	TextInputShort     TextInputStyle = 1
+	TextInputParagraph TextInputStyle = 2
+)

+ 10 - 112
discord.go

@@ -14,42 +14,20 @@
 package discordgo
 
 import (
-	"errors"
-	"fmt"
 	"net/http"
 	"runtime"
 	"time"
 )
 
 // VERSION of DiscordGo, follows Semantic Versioning. (http://semver.org/)
-const VERSION = "0.23.0"
+const VERSION = "0.24.0"
 
-// ErrMFA will be risen by New when the user has 2FA.
-var ErrMFA = errors.New("account has 2FA enabled")
-
-// New creates a new Discord session and will automate some startup
-// tasks if given enough information to do so.  Currently you can pass zero
-// arguments and it will return an empty Discord session.
-// There are 3 ways to call New:
-//     With a single auth token - All requests will use the token blindly
-//         (just tossing it into the HTTP Authorization header);
-//         no verification of the token will be done and requests may fail.
-//         IF THE TOKEN IS FOR A BOT, IT MUST BE PREFIXED WITH `BOT `
-//         eg: `"Bot <token>"`
-//         IF IT IS AN OAUTH2 ACCESS TOKEN, IT MUST BE PREFIXED WITH `Bearer `
-//         eg: `"Bearer <token>"`
-//     With an email and password - Discord will sign in with the provided
-//         credentials.
-//     With an email, password and auth token - Discord will verify the auth
-//         token, if it is invalid it will sign in with the provided
-//         credentials. This is the Discord recommended way to sign in.
-//
-// NOTE: While email/pass authentication is supported by DiscordGo it is
-// HIGHLY DISCOURAGED by Discord. Please only use email/pass to obtain a token
-// and then use that authentication token for all future connections.
-// Also, doing any form of automation with a user (non Bot) account may result
-// in that account being permanently banned from Discord.
-func New(args ...interface{}) (s *Session, err error) {
+// New creates a new Discord session with provided token.
+// If the token is for a bot, it must be prefixed with "Bot "
+// 		e.g. "Bot ..."
+// Or if it is an OAuth2 token, it must be prefixed with "Bearer "
+//		e.g. "Bearer ..."
+func New(token string) (s *Session, err error) {
 
 	// Create an empty Session interface.
 	s = &Session{
@@ -71,91 +49,11 @@ func New(args ...interface{}) (s *Session, err error) {
 	// These can be modified prior to calling Open()
 	s.Identify.Compress = true
 	s.Identify.LargeThreshold = 250
-	s.Identify.GuildSubscriptions = true
 	s.Identify.Properties.OS = runtime.GOOS
 	s.Identify.Properties.Browser = "DiscordGo v" + VERSION
-	s.Identify.Intents = MakeIntent(IntentsAllWithoutPrivileged)
-
-	// If no arguments are passed return the empty Session interface.
-	if args == nil {
-		return
-	}
-
-	// Variables used below when parsing func arguments
-	var auth, pass string
-
-	// Parse passed arguments
-	for _, arg := range args {
-
-		switch v := arg.(type) {
-
-		case []string:
-			if len(v) > 3 {
-				err = fmt.Errorf("too many string parameters provided")
-				return
-			}
-
-			// First string is either token or username
-			if len(v) > 0 {
-				auth = v[0]
-			}
-
-			// If second string exists, it must be a password.
-			if len(v) > 1 {
-				pass = v[1]
-			}
-
-			// If third string exists, it must be an auth token.
-			if len(v) > 2 {
-				s.Identify.Token = v[2]
-				s.Token = v[2] // TODO: Remove, Deprecated - Kept for backwards compatibility.
-			}
-
-		case string:
-			// First string must be either auth token or username.
-			// Second string must be a password.
-			// Only 2 input strings are supported.
-
-			if auth == "" {
-				auth = v
-			} else if pass == "" {
-				pass = v
-			} else if s.Token == "" {
-				s.Identify.Token = v
-				s.Token = v // TODO: Remove, Deprecated - Kept for backwards compatibility.
-			} else {
-				err = fmt.Errorf("too many string parameters provided")
-				return
-			}
-
-			//		case Config:
-			// TODO: Parse configuration struct
-
-		default:
-			err = fmt.Errorf("unsupported parameter type provided")
-			return
-		}
-	}
-
-	// If only one string was provided, assume it is an auth token.
-	// Otherwise get auth token from Discord, if a token was specified
-	// Discord will verify it for free, or log the user in if it is
-	// invalid.
-	if pass == "" {
-		s.Identify.Token = auth
-		s.Token = auth // TODO: Remove, Deprecated - Kept for backwards compatibility.
-	} else {
-		err = s.Login(auth, pass)
-		// TODO: Remove last s.Token part, Deprecated - Kept for backwards compatibility.
-		if err != nil || s.Identify.Token == "" || s.Token == "" {
-			if s.MFA {
-				err = ErrMFA
-			} else {
-				err = fmt.Errorf("Unable to fetch discord authentication token. %v", err)
-			}
-			return
-		}
-	}
+	s.Identify.Intents = IntentsAllWithoutPrivileged
+	s.Identify.Token = token
+	s.Token = token
 
 	return
 }

+ 137 - 38
discord_test.go

@@ -15,14 +15,15 @@ var (
 	dg    *Session // Stores a global discordgo user session
 	dgBot *Session // Stores a global discordgo bot session
 
-	envToken    = os.Getenv("DGU_TOKEN")  // Token to use when authenticating the user account
-	envBotToken = os.Getenv("DGB_TOKEN")  // Token to use when authenticating the bot account
-	envGuild    = os.Getenv("DG_GUILD")   // Guild ID to use for tests
-	envChannel  = os.Getenv("DG_CHANNEL") // Channel ID to use for tests
-	envAdmin    = os.Getenv("DG_ADMIN")   // User ID of admin user to use for tests
+	envOAuth2Token  = os.Getenv("DG_OAUTH2_TOKEN")  // Token to use when authenticating using OAuth2 token
+	envBotToken     = os.Getenv("DGB_TOKEN")        // Token to use when authenticating the bot account
+	envGuild        = os.Getenv("DG_GUILD")         // Guild ID to use for tests
+	envChannel      = os.Getenv("DG_CHANNEL")       // Channel ID to use for tests
+	envVoiceChannel = os.Getenv("DG_VOICE_CHANNEL") // Channel ID to use for tests
+	envAdmin        = os.Getenv("DG_ADMIN")         // User ID of admin user to use for tests
 )
 
-func init() {
+func TestMain(m *testing.M) {
 	fmt.Println("Init is being called.")
 	if envBotToken != "" {
 		if d, err := New(envBotToken); err == nil {
@@ -30,48 +31,30 @@ func init() {
 		}
 	}
 
-	if d, err := New(envToken); err == nil {
-		dg = d
-	} else {
-		fmt.Println("dg is nil, error", err)
+	if envOAuth2Token == "" {
+		envOAuth2Token = os.Getenv("DGU_TOKEN")
 	}
-}
-
-//////////////////////////////////////////////////////////////////////////////
-/////////////////////////////////////////////////////////////// START OF TESTS
 
-// TestNew tests the New() function without any arguments.  This should return
-// a valid Session{} struct and no errors.
-func TestNew(t *testing.T) {
-
-	_, err := New()
-	if err != nil {
-		t.Errorf("New() returned error: %+v", err)
-	}
-}
-
-// TestInvalidToken tests the New() function with an invalid token
-func TestInvalidToken(t *testing.T) {
-	d, err := New("asjkldhflkjasdh")
-	if err != nil {
-		t.Fatalf("New(InvalidToken) returned error: %+v", err)
+	if envOAuth2Token != "" {
+		if d, err := New(envOAuth2Token); err == nil {
+			dg = d
+		}
 	}
 
-	// New with just a token does not do any communication, so attempt an api call.
-	_, err = d.UserSettings()
-	if err == nil {
-		t.Errorf("New(InvalidToken), d.UserSettings returned nil error.")
-	}
+	os.Exit(m.Run())
 }
 
+//////////////////////////////////////////////////////////////////////////////
+/////////////////////////////////////////////////////////////// START OF TESTS
+
 // TestNewToken tests the New() function with a Token.
 func TestNewToken(t *testing.T) {
 
-	if envToken == "" {
+	if envOAuth2Token == "" {
 		t.Skip("Skipping New(token), DGU_TOKEN not set")
 	}
 
-	d, err := New(envToken)
+	d, err := New(envOAuth2Token)
 	if err != nil {
 		t.Fatalf("New(envToken) returned error: %+v", err)
 	}
@@ -86,11 +69,11 @@ func TestNewToken(t *testing.T) {
 }
 
 func TestOpenClose(t *testing.T) {
-	if envToken == "" {
+	if envOAuth2Token == "" {
 		t.Skip("Skipping TestClose, DGU_TOKEN not set")
 	}
 
-	d, err := New(envToken)
+	d, err := New(envOAuth2Token)
 	if err != nil {
 		t.Fatalf("TestClose, New(envToken) returned error: %+v", err)
 	}
@@ -201,3 +184,119 @@ func TestRemoveHandler(t *testing.T) {
 		t.Fatalf("testHandler was not called once.")
 	}
 }
+
+func TestScheduledEvents(t *testing.T) {
+	if dgBot == nil {
+		t.Skip("Skipping, dgBot not set.")
+	}
+
+	beginAt := time.Now().Add(1 * time.Hour)
+	endAt := time.Now().Add(2 * time.Hour)
+	event, err := dgBot.GuildScheduledEventCreate(envGuild, &GuildScheduledEventParams{
+		Name:               "Test Event",
+		PrivacyLevel:       GuildScheduledEventPrivacyLevelGuildOnly,
+		ScheduledStartTime: &beginAt,
+		ScheduledEndTime:   &endAt,
+		Description:        "Awesome Test Event created on livestream",
+		EntityType:         GuildScheduledEventEntityTypeExternal,
+		EntityMetadata: &GuildScheduledEventEntityMetadata{
+			Location: "https://discord.com",
+		},
+	})
+	defer dgBot.GuildScheduledEventDelete(envGuild, event.ID)
+
+	if err != nil || event.Name != "Test Event" {
+		t.Fatal(err)
+	}
+
+	events, err := dgBot.GuildScheduledEvents(envGuild, true)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	var foundEvent *GuildScheduledEvent
+	for _, e := range events {
+		if e.ID == event.ID {
+			foundEvent = e
+			break
+		}
+	}
+	if foundEvent.Name != event.Name {
+		t.Fatal("err on GuildScheduledEvents endpoint. Missing Scheduled Event")
+	}
+
+	getEvent, err := dgBot.GuildScheduledEvent(envGuild, event.ID, true)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if getEvent.Name != event.Name {
+		t.Fatal("err on GuildScheduledEvent endpoint. Mismatched Scheduled Event")
+	}
+
+	eventUpdated, err := dgBot.GuildScheduledEventEdit(envGuild, event.ID, &GuildScheduledEventParams{Name: "Test Event Updated"})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if eventUpdated.Name != "Test Event Updated" {
+		t.Fatal("err on GuildScheduledEventUpdate endpoint. Scheduled Event Name mismatch")
+	}
+
+	// Usage of 1 and 1 is just the pseudo data with the purpose to run all branches in the function without crashes.
+	// see https://github.com/bwmarrin/discordgo/pull/1032#discussion_r815438303 for more details.
+	users, err := dgBot.GuildScheduledEventUsers(envGuild, event.ID, 1, true, "1", "1")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(users) != 0 {
+		t.Fatal("err on GuildScheduledEventUsers. Mismatch of event maybe occured")
+	}
+
+	err = dgBot.GuildScheduledEventDelete(envGuild, event.ID)
+	if err != nil {
+		t.Fatal(err)
+	}
+}
+
+func TestComplexScheduledEvents(t *testing.T) {
+	if dgBot == nil {
+		t.Skip("Skipping, dgBot not set.")
+	}
+
+	beginAt := time.Now().Add(1 * time.Hour)
+	endAt := time.Now().Add(2 * time.Hour)
+	event, err := dgBot.GuildScheduledEventCreate(envGuild, &GuildScheduledEventParams{
+		Name:               "Test Voice Event",
+		PrivacyLevel:       GuildScheduledEventPrivacyLevelGuildOnly,
+		ScheduledStartTime: &beginAt,
+		ScheduledEndTime:   &endAt,
+		Description:        "Test event on voice channel",
+		EntityType:         GuildScheduledEventEntityTypeVoice,
+		ChannelID:          envVoiceChannel,
+	})
+	if err != nil || event.Name != "Test Voice Event" {
+		t.Fatal(err)
+	}
+	defer dgBot.GuildScheduledEventDelete(envGuild, event.ID)
+
+	_, err = dgBot.GuildScheduledEventEdit(envGuild, event.ID, &GuildScheduledEventParams{
+		EntityType: GuildScheduledEventEntityTypeExternal,
+		EntityMetadata: &GuildScheduledEventEntityMetadata{
+			Location: "https://discord.com",
+		},
+	})
+
+	if err != nil {
+		t.Fatal("err on GuildScheduledEventEdit. Change of entity type to external failed")
+	}
+
+	_, err = dgBot.GuildScheduledEventEdit(envGuild, event.ID, &GuildScheduledEventParams{
+		ChannelID:      envVoiceChannel,
+		EntityType:     GuildScheduledEventEntityTypeVoice,
+		EntityMetadata: nil,
+	})
+
+	if err != nil {
+		t.Fatal("err on GuildScheduledEventEdit. Change of entity type to voice failed")
+	}
+}

二進制
docs/img/discordgo.png


File diff suppressed because it is too large
+ 45 - 0
docs/img/discordgo.svg


+ 86 - 72
endpoints.go

@@ -14,7 +14,7 @@ package discordgo
 import "strconv"
 
 // APIVersion is the Discord API version used for the REST and Websocket API.
-var APIVersion = "8"
+var APIVersion = "9"
 
 // Known Discord API Endpoints.
 var (
@@ -31,6 +31,7 @@ var (
 	EndpointGateway    = EndpointAPI + "gateway"
 	EndpointGatewayBot = EndpointGateway + "/bot"
 	EndpointWebhooks   = EndpointAPI + "webhooks/"
+	EndpointStickers   = EndpointAPI + "stickers/"
 
 	EndpointCDN             = "https://cdn.discordapp.com/"
 	EndpointCDNAttachments  = EndpointCDN + "attachments/"
@@ -39,27 +40,12 @@ var (
 	EndpointCDNSplashes     = EndpointCDN + "splashes/"
 	EndpointCDNChannelIcons = EndpointCDN + "channel-icons/"
 	EndpointCDNBanners      = EndpointCDN + "banners/"
-
-	EndpointAuth           = EndpointAPI + "auth/"
-	EndpointLogin          = EndpointAuth + "login"
-	EndpointLogout         = EndpointAuth + "logout"
-	EndpointVerify         = EndpointAuth + "verify"
-	EndpointVerifyResend   = EndpointAuth + "verify/resend"
-	EndpointForgotPassword = EndpointAuth + "forgot"
-	EndpointResetPassword  = EndpointAuth + "reset"
-	EndpointRegister       = EndpointAuth + "register"
+	EndpointCDNGuilds       = EndpointCDN + "guilds/"
 
 	EndpointVoice        = EndpointAPI + "/voice/"
 	EndpointVoiceRegions = EndpointVoice + "regions"
-	EndpointVoiceIce     = EndpointVoice + "ice"
-
-	EndpointTutorial           = EndpointAPI + "tutorial/"
-	EndpointTutorialIndicators = EndpointTutorial + "indicators"
 
-	EndpointTrack        = EndpointAPI + "track"
-	EndpointSso          = EndpointAPI + "sso"
-	EndpointReport       = EndpointAPI + "report"
-	EndpointIntegrations = EndpointAPI + "integrations"
+	// TODO: EndpointUserGuildMember
 
 	EndpointUser               = func(uID string) string { return EndpointUsers + uID }
 	EndpointUserAvatar         = func(uID, aID string) string { return EndpointCDNAvatars + uID + "/" + aID + ".png" }
@@ -68,57 +54,85 @@ var (
 		uDiscriminatorInt, _ := strconv.Atoi(uDiscriminator)
 		return EndpointCDN + "embed/avatars/" + strconv.Itoa(uDiscriminatorInt%5) + ".png"
 	}
-	EndpointUserSettings      = func(uID string) string { return EndpointUsers + uID + "/settings" }
-	EndpointUserGuilds        = func(uID string) string { return EndpointUsers + uID + "/guilds" }
-	EndpointUserGuild         = func(uID, gID string) string { return EndpointUsers + uID + "/guilds/" + gID }
-	EndpointUserGuildSettings = func(uID, gID string) string { return EndpointUsers + uID + "/guilds/" + gID + "/settings" }
-	EndpointUserChannels      = func(uID string) string { return EndpointUsers + uID + "/channels" }
-	EndpointUserDevices       = func(uID string) string { return EndpointUsers + uID + "/devices" }
-	EndpointUserConnections   = func(uID string) string { return EndpointUsers + uID + "/connections" }
-	EndpointUserNotes         = func(uID string) string { return EndpointUsers + "@me/notes/" + uID }
-
-	EndpointGuild                = func(gID string) string { return EndpointGuilds + gID }
-	EndpointGuildPreview         = func(gID string) string { return EndpointGuilds + gID + "/preview" }
-	EndpointGuildChannels        = func(gID string) string { return EndpointGuilds + gID + "/channels" }
-	EndpointGuildMembers         = func(gID string) string { return EndpointGuilds + gID + "/members" }
-	EndpointGuildMember          = func(gID, uID string) string { return EndpointGuilds + gID + "/members/" + uID }
-	EndpointGuildMemberRole      = func(gID, uID, rID string) string { return EndpointGuilds + gID + "/members/" + uID + "/roles/" + rID }
-	EndpointGuildBans            = func(gID string) string { return EndpointGuilds + gID + "/bans" }
-	EndpointGuildBan             = func(gID, uID string) string { return EndpointGuilds + gID + "/bans/" + uID }
-	EndpointGuildIntegrations    = func(gID string) string { return EndpointGuilds + gID + "/integrations" }
-	EndpointGuildIntegration     = func(gID, iID string) string { return EndpointGuilds + gID + "/integrations/" + iID }
-	EndpointGuildIntegrationSync = func(gID, iID string) string { return EndpointGuilds + gID + "/integrations/" + iID + "/sync" }
-	EndpointGuildRoles           = func(gID string) string { return EndpointGuilds + gID + "/roles" }
-	EndpointGuildRole            = func(gID, rID string) string { return EndpointGuilds + gID + "/roles/" + rID }
-	EndpointGuildInvites         = func(gID string) string { return EndpointGuilds + gID + "/invites" }
-	EndpointGuildWidget          = func(gID string) string { return EndpointGuilds + gID + "/widget" }
-	EndpointGuildEmbed           = EndpointGuildWidget
-	EndpointGuildPrune           = func(gID string) string { return EndpointGuilds + gID + "/prune" }
-	EndpointGuildIcon            = func(gID, hash string) string { return EndpointCDNIcons + gID + "/" + hash + ".png" }
-	EndpointGuildIconAnimated    = func(gID, hash string) string { return EndpointCDNIcons + gID + "/" + hash + ".gif" }
-	EndpointGuildSplash          = func(gID, hash string) string { return EndpointCDNSplashes + gID + "/" + hash + ".png" }
-	EndpointGuildWebhooks        = func(gID string) string { return EndpointGuilds + gID + "/webhooks" }
-	EndpointGuildAuditLogs       = func(gID string) string { return EndpointGuilds + gID + "/audit-logs" }
-	EndpointGuildEmojis          = func(gID string) string { return EndpointGuilds + gID + "/emojis" }
-	EndpointGuildEmoji           = func(gID, eID string) string { return EndpointGuilds + gID + "/emojis/" + eID }
-	EndpointGuildBanner          = func(gID, hash string) string { return EndpointCDNBanners + gID + "/" + hash + ".png" }
-
-	EndpointChannel                   = func(cID string) string { return EndpointChannels + cID }
-	EndpointChannelPermissions        = func(cID string) string { return EndpointChannels + cID + "/permissions" }
-	EndpointChannelPermission         = func(cID, tID string) string { return EndpointChannels + cID + "/permissions/" + tID }
-	EndpointChannelInvites            = func(cID string) string { return EndpointChannels + cID + "/invites" }
-	EndpointChannelTyping             = func(cID string) string { return EndpointChannels + cID + "/typing" }
-	EndpointChannelMessages           = func(cID string) string { return EndpointChannels + cID + "/messages" }
-	EndpointChannelMessage            = func(cID, mID string) string { return EndpointChannels + cID + "/messages/" + mID }
-	EndpointChannelMessageAck         = func(cID, mID string) string { return EndpointChannels + cID + "/messages/" + mID + "/ack" }
-	EndpointChannelMessagesBulkDelete = func(cID string) string { return EndpointChannel(cID) + "/messages/bulk-delete" }
-	EndpointChannelMessagesPins       = func(cID string) string { return EndpointChannel(cID) + "/pins" }
-	EndpointChannelMessagePin         = func(cID, mID string) string { return EndpointChannel(cID) + "/pins/" + mID }
-	EndpointChannelMessageCrosspost   = func(cID, mID string) string { return EndpointChannel(cID) + "/messages/" + mID + "/crosspost" }
-	EndpointChannelFollow             = func(cID string) string { return EndpointChannel(cID) + "/followers" }
+	EndpointUserBanner = func(uID, cID string) string {
+		return EndpointCDNBanners + uID + "/" + cID + ".png"
+	}
+	EndpointUserBannerAnimated = func(uID, cID string) string {
+		return EndpointCDNBanners + uID + "/" + cID + ".gif"
+	}
+
+	EndpointUserGuilds      = func(uID string) string { return EndpointUsers + uID + "/guilds" }
+	EndpointUserGuild       = func(uID, gID string) string { return EndpointUsers + uID + "/guilds/" + gID }
+	EndpointUserChannels    = func(uID string) string { return EndpointUsers + uID + "/channels" }
+	EndpointUserConnections = func(uID string) string { return EndpointUsers + uID + "/connections" }
+
+	EndpointGuild                    = func(gID string) string { return EndpointGuilds + gID }
+	EndpointGuildThreads             = func(gID string) string { return EndpointGuild(gID) + "/threads" }
+	EndpointGuildActiveThreads       = func(gID string) string { return EndpointGuildThreads(gID) + "/active" }
+	EndpointGuildPreview             = func(gID string) string { return EndpointGuilds + gID + "/preview" }
+	EndpointGuildChannels            = func(gID string) string { return EndpointGuilds + gID + "/channels" }
+	EndpointGuildMembers             = func(gID string) string { return EndpointGuilds + gID + "/members" }
+	EndpointGuildMember              = func(gID, uID string) string { return EndpointGuilds + gID + "/members/" + uID }
+	EndpointGuildMemberRole          = func(gID, uID, rID string) string { return EndpointGuilds + gID + "/members/" + uID + "/roles/" + rID }
+	EndpointGuildBans                = func(gID string) string { return EndpointGuilds + gID + "/bans" }
+	EndpointGuildBan                 = func(gID, uID string) string { return EndpointGuilds + gID + "/bans/" + uID }
+	EndpointGuildIntegrations        = func(gID string) string { return EndpointGuilds + gID + "/integrations" }
+	EndpointGuildIntegration         = func(gID, iID string) string { return EndpointGuilds + gID + "/integrations/" + iID }
+	EndpointGuildRoles               = func(gID string) string { return EndpointGuilds + gID + "/roles" }
+	EndpointGuildRole                = func(gID, rID string) string { return EndpointGuilds + gID + "/roles/" + rID }
+	EndpointGuildInvites             = func(gID string) string { return EndpointGuilds + gID + "/invites" }
+	EndpointGuildWidget              = func(gID string) string { return EndpointGuilds + gID + "/widget" }
+	EndpointGuildEmbed               = EndpointGuildWidget
+	EndpointGuildPrune               = func(gID string) string { return EndpointGuilds + gID + "/prune" }
+	EndpointGuildIcon                = func(gID, hash string) string { return EndpointCDNIcons + gID + "/" + hash + ".png" }
+	EndpointGuildIconAnimated        = func(gID, hash string) string { return EndpointCDNIcons + gID + "/" + hash + ".gif" }
+	EndpointGuildSplash              = func(gID, hash string) string { return EndpointCDNSplashes + gID + "/" + hash + ".png" }
+	EndpointGuildWebhooks            = func(gID string) string { return EndpointGuilds + gID + "/webhooks" }
+	EndpointGuildAuditLogs           = func(gID string) string { return EndpointGuilds + gID + "/audit-logs" }
+	EndpointGuildEmojis              = func(gID string) string { return EndpointGuilds + gID + "/emojis" }
+	EndpointGuildEmoji               = func(gID, eID string) string { return EndpointGuilds + gID + "/emojis/" + eID }
+	EndpointGuildBanner              = func(gID, hash string) string { return EndpointCDNBanners + gID + "/" + hash + ".png" }
+	EndpointGuildStickers            = func(gID string) string { return EndpointGuilds + gID + "/stickers" }
+	EndpointGuildSticker             = func(gID, sID string) string { return EndpointGuilds + gID + "/stickers/" + sID }
+	EndpointGuildScheduledEvents     = func(gID string) string { return EndpointGuilds + gID + "/scheduled-events" }
+	EndpointGuildScheduledEvent      = func(gID, eID string) string { return EndpointGuilds + gID + "/scheduled-events/" + eID }
+	EndpointGuildScheduledEventUsers = func(gID, eID string) string { return EndpointGuildScheduledEvent(gID, eID) + "/users" }
+	EndpointGuildTemplate            = func(tID string) string { return EndpointGuilds + "/templates/" + tID }
+	EndpointGuildTemplates           = func(gID string) string { return EndpointGuilds + gID + "/templates" }
+	EndpointGuildTemplateSync        = func(gID, tID string) string { return EndpointGuilds + gID + "/templates/" + tID }
+	EndpointGuildMemberAvatar        = func(gId, uID, aID string) string {
+		return EndpointCDNGuilds + gId + "/users/" + uID + "/avatars/" + aID + ".png"
+	}
+	EndpointGuildMemberAvatarAnimated = func(gId, uID, aID string) string {
+		return EndpointCDNGuilds + gId + "/users/" + uID + "/avatars/" + aID + ".gif"
+	}
+
+	EndpointChannel                             = func(cID string) string { return EndpointChannels + cID }
+	EndpointChannelThreads                      = func(cID string) string { return EndpointChannel(cID) + "/threads" }
+	EndpointChannelActiveThreads                = func(cID string) string { return EndpointChannelThreads(cID) + "/active" }
+	EndpointChannelPublicArchivedThreads        = func(cID string) string { return EndpointChannelThreads(cID) + "/archived/public" }
+	EndpointChannelPrivateArchivedThreads       = func(cID string) string { return EndpointChannelThreads(cID) + "/archived/private" }
+	EndpointChannelJoinedPrivateArchivedThreads = func(cID string) string { return EndpointChannel(cID) + "/users/@me/threads/archived/private" }
+	EndpointChannelPermissions                  = func(cID string) string { return EndpointChannels + cID + "/permissions" }
+	EndpointChannelPermission                   = func(cID, tID string) string { return EndpointChannels + cID + "/permissions/" + tID }
+	EndpointChannelInvites                      = func(cID string) string { return EndpointChannels + cID + "/invites" }
+	EndpointChannelTyping                       = func(cID string) string { return EndpointChannels + cID + "/typing" }
+	EndpointChannelMessages                     = func(cID string) string { return EndpointChannels + cID + "/messages" }
+	EndpointChannelMessage                      = func(cID, mID string) string { return EndpointChannels + cID + "/messages/" + mID }
+	EndpointChannelMessageThread                = func(cID, mID string) string { return EndpointChannelMessage(cID, mID) + "/threads" }
+	EndpointChannelMessagesBulkDelete           = func(cID string) string { return EndpointChannel(cID) + "/messages/bulk-delete" }
+	EndpointChannelMessagesPins                 = func(cID string) string { return EndpointChannel(cID) + "/pins" }
+	EndpointChannelMessagePin                   = func(cID, mID string) string { return EndpointChannel(cID) + "/pins/" + mID }
+	EndpointChannelMessageCrosspost             = func(cID, mID string) string { return EndpointChannel(cID) + "/messages/" + mID + "/crosspost" }
+	EndpointChannelFollow                       = func(cID string) string { return EndpointChannel(cID) + "/followers" }
+	EndpointThreadMembers                       = func(tID string) string { return EndpointChannel(tID) + "/thread-members" }
+	EndpointThreadMember                        = func(tID, mID string) string { return EndpointThreadMembers(tID) + "/" + mID }
 
 	EndpointGroupIcon = func(cID, hash string) string { return EndpointCDNChannelIcons + cID + "/" + hash + ".png" }
 
+	EndpointSticker            = func(sID string) string { return EndpointStickers + sID }
+	EndpointNitroStickersPacks = EndpointAPI + "/sticker-packs"
+
 	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 }
@@ -149,6 +163,12 @@ var (
 	EndpointApplicationGuildCommand = func(aID, gID, cID string) string {
 		return EndpointApplicationGuildCommands(aID, gID) + "/" + cID
 	}
+	EndpointApplicationCommandPermissions = func(aID, gID, cID string) string {
+		return EndpointApplicationGuildCommand(aID, gID, cID) + "/permissions"
+	}
+	EndpointApplicationCommandsGuildPermissions = func(aID, gID string) string {
+		return EndpointApplicationGuildCommands(aID, gID) + "/permissions"
+	}
 	EndpointInteraction = func(aID, iToken string) string {
 		return EndpointAPI + "interactions/" + aID + "/" + iToken
 	}
@@ -165,16 +185,10 @@ var (
 		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" }
-
 	EndpointGuildCreate = EndpointAPI + "guilds"
 
 	EndpointInvite = func(iID string) string { return EndpointAPI + "invites/" + iID }
 
-	EndpointIntegrationsJoin = func(iID string) string { return EndpointAPI + "integrations/" + iID + "/join" }
-
 	EndpointEmoji         = func(eID string) string { return EndpointCDN + "emojis/" + eID + ".png" }
 	EndpointEmojiAnimated = func(eID string) string { return EndpointCDN + "emojis/" + eID + ".gif" }
 

+ 1 - 1
event.go

@@ -157,7 +157,7 @@ func (s *Session) removeEventHandlerInstance(t string, ehi *eventHandlerInstance
 	onceHandlers := s.onceHandlers[t]
 	for i := range onceHandlers {
 		if onceHandlers[i] == ehi {
-			s.onceHandlers[t] = append(onceHandlers[:i], handlers[i+1:]...)
+			s.onceHandlers[t] = append(onceHandlers[:i], onceHandlers[i+1:]...)
 		}
 	}
 }

+ 309 - 45
eventhandlers.go

@@ -7,51 +7,62 @@ package discordgo
 // Event type values are used to match the events returned by Discord.
 // EventTypes surrounded by __ are synthetic and are internal to DiscordGo.
 const (
-	channelCreateEventType            = "CHANNEL_CREATE"
-	channelDeleteEventType            = "CHANNEL_DELETE"
-	channelPinsUpdateEventType        = "CHANNEL_PINS_UPDATE"
-	channelUpdateEventType            = "CHANNEL_UPDATE"
-	connectEventType                  = "__CONNECT__"
-	disconnectEventType               = "__DISCONNECT__"
-	eventEventType                    = "__EVENT__"
-	guildBanAddEventType              = "GUILD_BAN_ADD"
-	guildBanRemoveEventType           = "GUILD_BAN_REMOVE"
-	guildCreateEventType              = "GUILD_CREATE"
-	guildDeleteEventType              = "GUILD_DELETE"
-	guildEmojisUpdateEventType        = "GUILD_EMOJIS_UPDATE"
-	guildIntegrationsUpdateEventType  = "GUILD_INTEGRATIONS_UPDATE"
-	guildMemberAddEventType           = "GUILD_MEMBER_ADD"
-	guildMemberRemoveEventType        = "GUILD_MEMBER_REMOVE"
-	guildMemberUpdateEventType        = "GUILD_MEMBER_UPDATE"
-	guildMembersChunkEventType        = "GUILD_MEMBERS_CHUNK"
-	guildRoleCreateEventType          = "GUILD_ROLE_CREATE"
-	guildRoleDeleteEventType          = "GUILD_ROLE_DELETE"
-	guildRoleUpdateEventType          = "GUILD_ROLE_UPDATE"
-	guildUpdateEventType              = "GUILD_UPDATE"
-	interactionCreateEventType        = "INTERACTION_CREATE"
-	messageAckEventType               = "MESSAGE_ACK"
-	messageCreateEventType            = "MESSAGE_CREATE"
-	messageDeleteEventType            = "MESSAGE_DELETE"
-	messageDeleteBulkEventType        = "MESSAGE_DELETE_BULK"
-	messageReactionAddEventType       = "MESSAGE_REACTION_ADD"
-	messageReactionRemoveEventType    = "MESSAGE_REACTION_REMOVE"
-	messageReactionRemoveAllEventType = "MESSAGE_REACTION_REMOVE_ALL"
-	messageUpdateEventType            = "MESSAGE_UPDATE"
-	presenceUpdateEventType           = "PRESENCE_UPDATE"
-	presencesReplaceEventType         = "PRESENCES_REPLACE"
-	rateLimitEventType                = "__RATE_LIMIT__"
-	readyEventType                    = "READY"
-	relationshipAddEventType          = "RELATIONSHIP_ADD"
-	relationshipRemoveEventType       = "RELATIONSHIP_REMOVE"
-	resumedEventType                  = "RESUMED"
-	typingStartEventType              = "TYPING_START"
-	userGuildSettingsUpdateEventType  = "USER_GUILD_SETTINGS_UPDATE"
-	userNoteUpdateEventType           = "USER_NOTE_UPDATE"
-	userSettingsUpdateEventType       = "USER_SETTINGS_UPDATE"
-	userUpdateEventType               = "USER_UPDATE"
-	voiceServerUpdateEventType        = "VOICE_SERVER_UPDATE"
-	voiceStateUpdateEventType         = "VOICE_STATE_UPDATE"
-	webhooksUpdateEventType           = "WEBHOOKS_UPDATE"
+	channelCreateEventType             = "CHANNEL_CREATE"
+	channelDeleteEventType             = "CHANNEL_DELETE"
+	channelPinsUpdateEventType         = "CHANNEL_PINS_UPDATE"
+	channelUpdateEventType             = "CHANNEL_UPDATE"
+	connectEventType                   = "__CONNECT__"
+	disconnectEventType                = "__DISCONNECT__"
+	eventEventType                     = "__EVENT__"
+	guildBanAddEventType               = "GUILD_BAN_ADD"
+	guildBanRemoveEventType            = "GUILD_BAN_REMOVE"
+	guildCreateEventType               = "GUILD_CREATE"
+	guildDeleteEventType               = "GUILD_DELETE"
+	guildEmojisUpdateEventType         = "GUILD_EMOJIS_UPDATE"
+	guildIntegrationsUpdateEventType   = "GUILD_INTEGRATIONS_UPDATE"
+	guildMemberAddEventType            = "GUILD_MEMBER_ADD"
+	guildMemberRemoveEventType         = "GUILD_MEMBER_REMOVE"
+	guildMemberUpdateEventType         = "GUILD_MEMBER_UPDATE"
+	guildMembersChunkEventType         = "GUILD_MEMBERS_CHUNK"
+	guildRoleCreateEventType           = "GUILD_ROLE_CREATE"
+	guildRoleDeleteEventType           = "GUILD_ROLE_DELETE"
+	guildRoleUpdateEventType           = "GUILD_ROLE_UPDATE"
+	guildUpdateEventType               = "GUILD_UPDATE"
+	guildScheduledEventCreateEventType = "GUILD_SCHEDULED_EVENT_CREATE"
+	guildScheduledEventUpdateEventType = "GUILD_SCHEDULED_EVENT_UPDATE"
+	guildScheduledEventDeleteEventType = "GUILD_SCHEDULED_EVENT_DELETE"
+	interactionCreateEventType         = "INTERACTION_CREATE"
+	inviteCreateEventType              = "INVITE_CREATE"
+	inviteDeleteEventType              = "INVITE_DELETE"
+	messageAckEventType                = "MESSAGE_ACK"
+	messageCreateEventType             = "MESSAGE_CREATE"
+	messageDeleteEventType             = "MESSAGE_DELETE"
+	messageDeleteBulkEventType         = "MESSAGE_DELETE_BULK"
+	messageReactionAddEventType        = "MESSAGE_REACTION_ADD"
+	messageReactionRemoveEventType     = "MESSAGE_REACTION_REMOVE"
+	messageReactionRemoveAllEventType  = "MESSAGE_REACTION_REMOVE_ALL"
+	messageUpdateEventType             = "MESSAGE_UPDATE"
+	presenceUpdateEventType            = "PRESENCE_UPDATE"
+	presencesReplaceEventType          = "PRESENCES_REPLACE"
+	rateLimitEventType                 = "__RATE_LIMIT__"
+	readyEventType                     = "READY"
+	relationshipAddEventType           = "RELATIONSHIP_ADD"
+	relationshipRemoveEventType        = "RELATIONSHIP_REMOVE"
+	resumedEventType                   = "RESUMED"
+	threadCreateEventType              = "THREAD_CREATE"
+	threadDeleteEventType              = "THREAD_DELETE"
+	threadListSyncEventType            = "THREAD_LIST_SYNC"
+	threadMemberUpdateEventType        = "THREAD_MEMBER_UPDATE"
+	threadMembersUpdateEventType       = "THREAD_MEMBERS_UPDATE"
+	threadUpdateEventType              = "THREAD_UPDATE"
+	typingStartEventType               = "TYPING_START"
+	userGuildSettingsUpdateEventType   = "USER_GUILD_SETTINGS_UPDATE"
+	userNoteUpdateEventType            = "USER_NOTE_UPDATE"
+	userSettingsUpdateEventType        = "USER_SETTINGS_UPDATE"
+	userUpdateEventType                = "USER_UPDATE"
+	voiceServerUpdateEventType         = "VOICE_SERVER_UPDATE"
+	voiceStateUpdateEventType          = "VOICE_STATE_UPDATE"
+	webhooksUpdateEventType            = "WEBHOOKS_UPDATE"
 )
 
 // channelCreateEventHandler is an event handler for ChannelCreate events.
@@ -299,6 +310,66 @@ func (eh guildIntegrationsUpdateEventHandler) Handle(s *Session, i interface{})
 	}
 }
 
+// guildScheduledEventCreateEventHandler is an event handler for GuildScheduledEventCreate events.
+type guildScheduledEventCreateEventHandler func(*Session, *GuildScheduledEventCreate)
+
+// Type returns the event type for GuildScheduledEventCreate events.
+func (eh guildScheduledEventCreateEventHandler) Type() string {
+	return guildScheduledEventCreateEventType
+}
+
+// New returns a new instance of GuildScheduledEventCreate.
+func (eh guildScheduledEventCreateEventHandler) New() interface{} {
+	return &GuildScheduledEventCreate{}
+}
+
+// Handle is the handler for GuildScheduledEventCreate events.
+func (eh guildScheduledEventCreateEventHandler) Handle(s *Session, i interface{}) {
+	if t, ok := i.(*GuildScheduledEventCreate); ok {
+		eh(s, t)
+	}
+}
+
+// guildScheduledEventUpdateEventHandler is an event handler for GuildScheduledEventUpdate events.
+type guildScheduledEventUpdateEventHandler func(*Session, *GuildScheduledEventUpdate)
+
+// Type returns the event type for GuildScheduledEventUpdate events.
+func (eh guildScheduledEventUpdateEventHandler) Type() string {
+	return guildScheduledEventUpdateEventType
+}
+
+// New returns a new instance of GuildScheduledEventUpdate.
+func (eh guildScheduledEventUpdateEventHandler) New() interface{} {
+	return &GuildScheduledEventUpdate{}
+}
+
+// Handle is the handler for GuildScheduledEventUpdate events.
+func (eh guildScheduledEventUpdateEventHandler) Handle(s *Session, i interface{}) {
+	if t, ok := i.(*GuildScheduledEventUpdate); ok {
+		eh(s, t)
+	}
+}
+
+// guildScheduledEventDeleteEventHandler is an event handler for GuildScheduledEventDelete events.
+type guildScheduledEventDeleteEventHandler func(*Session, *GuildScheduledEventDelete)
+
+// Type returns the event type for GuildScheduledEventDelete events.
+func (eh guildScheduledEventDeleteEventHandler) Type() string {
+	return guildScheduledEventDeleteEventType
+}
+
+// New returns a new instance of GuildScheduledEventDelete.
+func (eh guildScheduledEventDeleteEventHandler) New() interface{} {
+	return &GuildScheduledEventDelete{}
+}
+
+// Handle is the handler for GuildScheduledEventDelete events.
+func (eh guildScheduledEventDeleteEventHandler) Handle(s *Session, i interface{}) {
+	if t, ok := i.(*GuildScheduledEventDelete); ok {
+		eh(s, t)
+	}
+}
+
 // guildMemberAddEventHandler is an event handler for GuildMemberAdd events.
 type guildMemberAddEventHandler func(*Session, *GuildMemberAdd)
 
@@ -479,6 +550,46 @@ func (eh interactionCreateEventHandler) Handle(s *Session, i interface{}) {
 	}
 }
 
+// inviteCreateEventHandler is an event handler for InviteCreate events.
+type inviteCreateEventHandler func(*Session, *InviteCreate)
+
+// Type returns the event type for InviteCreate events.
+func (eh inviteCreateEventHandler) Type() string {
+	return inviteCreateEventType
+}
+
+// New returns a new instance of InviteCreate.
+func (eh inviteCreateEventHandler) New() interface{} {
+	return &InviteCreate{}
+}
+
+// Handle is the handler for InviteCreate events.
+func (eh inviteCreateEventHandler) Handle(s *Session, i interface{}) {
+	if t, ok := i.(*InviteCreate); ok {
+		eh(s, t)
+	}
+}
+
+// inviteDeleteEventHandler is an event handler for InviteDelete events.
+type inviteDeleteEventHandler func(*Session, *InviteDelete)
+
+// Type returns the event type for InviteDelete events.
+func (eh inviteDeleteEventHandler) Type() string {
+	return inviteDeleteEventType
+}
+
+// New returns a new instance of InviteDelete.
+func (eh inviteDeleteEventHandler) New() interface{} {
+	return &InviteDelete{}
+}
+
+// Handle is the handler for InviteDelete events.
+func (eh inviteDeleteEventHandler) Handle(s *Session, i interface{}) {
+	if t, ok := i.(*InviteDelete); ok {
+		eh(s, t)
+	}
+}
+
 // messageAckEventHandler is an event handler for MessageAck events.
 type messageAckEventHandler func(*Session, *MessageAck)
 
@@ -774,6 +885,126 @@ func (eh resumedEventHandler) Handle(s *Session, i interface{}) {
 	}
 }
 
+// threadCreateEventHandler is an event handler for ThreadCreate events.
+type threadCreateEventHandler func(*Session, *ThreadCreate)
+
+// Type returns the event type for ThreadCreate events.
+func (eh threadCreateEventHandler) Type() string {
+	return threadCreateEventType
+}
+
+// New returns a new instance of ThreadCreate.
+func (eh threadCreateEventHandler) New() interface{} {
+	return &ThreadCreate{}
+}
+
+// Handle is the handler for ThreadCreate events.
+func (eh threadCreateEventHandler) Handle(s *Session, i interface{}) {
+	if t, ok := i.(*ThreadCreate); ok {
+		eh(s, t)
+	}
+}
+
+// threadDeleteEventHandler is an event handler for ThreadDelete events.
+type threadDeleteEventHandler func(*Session, *ThreadDelete)
+
+// Type returns the event type for ThreadDelete events.
+func (eh threadDeleteEventHandler) Type() string {
+	return threadDeleteEventType
+}
+
+// New returns a new instance of ThreadDelete.
+func (eh threadDeleteEventHandler) New() interface{} {
+	return &ThreadDelete{}
+}
+
+// Handle is the handler for ThreadDelete events.
+func (eh threadDeleteEventHandler) Handle(s *Session, i interface{}) {
+	if t, ok := i.(*ThreadDelete); ok {
+		eh(s, t)
+	}
+}
+
+// threadListSyncEventHandler is an event handler for ThreadListSync events.
+type threadListSyncEventHandler func(*Session, *ThreadListSync)
+
+// Type returns the event type for ThreadListSync events.
+func (eh threadListSyncEventHandler) Type() string {
+	return threadListSyncEventType
+}
+
+// New returns a new instance of ThreadListSync.
+func (eh threadListSyncEventHandler) New() interface{} {
+	return &ThreadListSync{}
+}
+
+// Handle is the handler for ThreadListSync events.
+func (eh threadListSyncEventHandler) Handle(s *Session, i interface{}) {
+	if t, ok := i.(*ThreadListSync); ok {
+		eh(s, t)
+	}
+}
+
+// threadMemberUpdateEventHandler is an event handler for ThreadMemberUpdate events.
+type threadMemberUpdateEventHandler func(*Session, *ThreadMemberUpdate)
+
+// Type returns the event type for ThreadMemberUpdate events.
+func (eh threadMemberUpdateEventHandler) Type() string {
+	return threadMemberUpdateEventType
+}
+
+// New returns a new instance of ThreadMemberUpdate.
+func (eh threadMemberUpdateEventHandler) New() interface{} {
+	return &ThreadMemberUpdate{}
+}
+
+// Handle is the handler for ThreadMemberUpdate events.
+func (eh threadMemberUpdateEventHandler) Handle(s *Session, i interface{}) {
+	if t, ok := i.(*ThreadMemberUpdate); ok {
+		eh(s, t)
+	}
+}
+
+// threadMembersUpdateEventHandler is an event handler for ThreadMembersUpdate events.
+type threadMembersUpdateEventHandler func(*Session, *ThreadMembersUpdate)
+
+// Type returns the event type for ThreadMembersUpdate events.
+func (eh threadMembersUpdateEventHandler) Type() string {
+	return threadMembersUpdateEventType
+}
+
+// New returns a new instance of ThreadMembersUpdate.
+func (eh threadMembersUpdateEventHandler) New() interface{} {
+	return &ThreadMembersUpdate{}
+}
+
+// Handle is the handler for ThreadMembersUpdate events.
+func (eh threadMembersUpdateEventHandler) Handle(s *Session, i interface{}) {
+	if t, ok := i.(*ThreadMembersUpdate); ok {
+		eh(s, t)
+	}
+}
+
+// threadUpdateEventHandler is an event handler for ThreadUpdate events.
+type threadUpdateEventHandler func(*Session, *ThreadUpdate)
+
+// Type returns the event type for ThreadUpdate events.
+func (eh threadUpdateEventHandler) Type() string {
+	return threadUpdateEventType
+}
+
+// New returns a new instance of ThreadUpdate.
+func (eh threadUpdateEventHandler) New() interface{} {
+	return &ThreadUpdate{}
+}
+
+// Handle is the handler for ThreadUpdate events.
+func (eh threadUpdateEventHandler) Handle(s *Session, i interface{}) {
+	if t, ok := i.(*ThreadUpdate); ok {
+		eh(s, t)
+	}
+}
+
 // typingStartEventHandler is an event handler for TypingStart events.
 type typingStartEventHandler func(*Session, *TypingStart)
 
@@ -964,6 +1195,12 @@ func handlerForInterface(handler interface{}) EventHandler {
 		return guildEmojisUpdateEventHandler(v)
 	case func(*Session, *GuildIntegrationsUpdate):
 		return guildIntegrationsUpdateEventHandler(v)
+	case func(*Session, *GuildScheduledEventCreate):
+		return guildScheduledEventCreateEventHandler(v)
+	case func(*Session, *GuildScheduledEventUpdate):
+		return guildScheduledEventUpdateEventHandler(v)
+	case func(*Session, *GuildScheduledEventDelete):
+		return guildScheduledEventDeleteEventHandler(v)
 	case func(*Session, *GuildMemberAdd):
 		return guildMemberAddEventHandler(v)
 	case func(*Session, *GuildMemberRemove):
@@ -982,6 +1219,10 @@ func handlerForInterface(handler interface{}) EventHandler {
 		return guildUpdateEventHandler(v)
 	case func(*Session, *InteractionCreate):
 		return interactionCreateEventHandler(v)
+	case func(*Session, *InviteCreate):
+		return inviteCreateEventHandler(v)
+	case func(*Session, *InviteDelete):
+		return inviteDeleteEventHandler(v)
 	case func(*Session, *MessageAck):
 		return messageAckEventHandler(v)
 	case func(*Session, *MessageCreate):
@@ -1012,6 +1253,18 @@ func handlerForInterface(handler interface{}) EventHandler {
 		return relationshipRemoveEventHandler(v)
 	case func(*Session, *Resumed):
 		return resumedEventHandler(v)
+	case func(*Session, *ThreadCreate):
+		return threadCreateEventHandler(v)
+	case func(*Session, *ThreadDelete):
+		return threadDeleteEventHandler(v)
+	case func(*Session, *ThreadListSync):
+		return threadListSyncEventHandler(v)
+	case func(*Session, *ThreadMemberUpdate):
+		return threadMemberUpdateEventHandler(v)
+	case func(*Session, *ThreadMembersUpdate):
+		return threadMembersUpdateEventHandler(v)
+	case func(*Session, *ThreadUpdate):
+		return threadUpdateEventHandler(v)
 	case func(*Session, *TypingStart):
 		return typingStartEventHandler(v)
 	case func(*Session, *UserGuildSettingsUpdate):
@@ -1044,6 +1297,9 @@ func init() {
 	registerInterfaceProvider(guildDeleteEventHandler(nil))
 	registerInterfaceProvider(guildEmojisUpdateEventHandler(nil))
 	registerInterfaceProvider(guildIntegrationsUpdateEventHandler(nil))
+	registerInterfaceProvider(guildScheduledEventCreateEventHandler(nil))
+	registerInterfaceProvider(guildScheduledEventUpdateEventHandler(nil))
+	registerInterfaceProvider(guildScheduledEventDeleteEventHandler(nil))
 	registerInterfaceProvider(guildMemberAddEventHandler(nil))
 	registerInterfaceProvider(guildMemberRemoveEventHandler(nil))
 	registerInterfaceProvider(guildMemberUpdateEventHandler(nil))
@@ -1053,6 +1309,8 @@ func init() {
 	registerInterfaceProvider(guildRoleUpdateEventHandler(nil))
 	registerInterfaceProvider(guildUpdateEventHandler(nil))
 	registerInterfaceProvider(interactionCreateEventHandler(nil))
+	registerInterfaceProvider(inviteCreateEventHandler(nil))
+	registerInterfaceProvider(inviteDeleteEventHandler(nil))
 	registerInterfaceProvider(messageAckEventHandler(nil))
 	registerInterfaceProvider(messageCreateEventHandler(nil))
 	registerInterfaceProvider(messageDeleteEventHandler(nil))
@@ -1067,6 +1325,12 @@ func init() {
 	registerInterfaceProvider(relationshipAddEventHandler(nil))
 	registerInterfaceProvider(relationshipRemoveEventHandler(nil))
 	registerInterfaceProvider(resumedEventHandler(nil))
+	registerInterfaceProvider(threadCreateEventHandler(nil))
+	registerInterfaceProvider(threadDeleteEventHandler(nil))
+	registerInterfaceProvider(threadListSyncEventHandler(nil))
+	registerInterfaceProvider(threadMemberUpdateEventHandler(nil))
+	registerInterfaceProvider(threadMembersUpdateEventHandler(nil))
+	registerInterfaceProvider(threadUpdateEventHandler(nil))
 	registerInterfaceProvider(typingStartEventHandler(nil))
 	registerInterfaceProvider(userGuildSettingsUpdateEventHandler(nil))
 	registerInterfaceProvider(userNoteUpdateEventHandler(nil))

+ 77 - 0
events.go

@@ -73,6 +73,53 @@ type ChannelPinsUpdate struct {
 	GuildID          string `json:"guild_id,omitempty"`
 }
 
+// ThreadCreate is the data for a ThreadCreate event.
+type ThreadCreate struct {
+	*Channel
+	NewlyCreated bool `json:"newly_created"`
+}
+
+// ThreadUpdate is the data for a ThreadUpdate event.
+type ThreadUpdate struct {
+	*Channel
+	BeforeUpdate *Channel `json:"-"`
+}
+
+// ThreadDelete is the data for a ThreadDelete event.
+type ThreadDelete struct {
+	*Channel
+}
+
+// ThreadListSync is the data for a ThreadListSync event.
+type ThreadListSync struct {
+	// The id of the guild
+	GuildID string `json:"guild_id"`
+	// The parent channel ids whose threads are being synced.
+	// If omitted, then threads were synced for the entire guild.
+	// This array may contain channel_ids that have no active threads as well, so you know to clear that data.
+	ChannelIDs []string `json:"channel_ids"`
+	// All active threads in the given channels that the current user can access
+	Threads []*Channel `json:"threads"`
+	// All thread member objects from the synced threads for the current user,
+	// indicating which threads the current user has been added to
+	Members []*ThreadMember `json:"members"`
+}
+
+// ThreadMemberUpdate is the data for a ThreadMemberUpdate event.
+type ThreadMemberUpdate struct {
+	*ThreadMember
+	GuildID string `json:"guild_id"`
+}
+
+// ThreadMembersUpdate is the data for a ThreadMembersUpdate event.
+type ThreadMembersUpdate struct {
+	ID             string              `json:"id"`
+	GuildID        string              `json:"guild_id"`
+	MemberCount    int                 `json:"member_count"`
+	AddedMembers   []AddedThreadMember `json:"added_members"`
+	RemovedMembers []string            `json:"removed_member_ids"`
+}
+
 // GuildCreate is the data for a GuildCreate event.
 type GuildCreate struct {
 	*Guild
@@ -152,6 +199,21 @@ type GuildIntegrationsUpdate struct {
 	GuildID string `json:"guild_id"`
 }
 
+// GuildScheduledEventCreate is the data for a GuildScheduledEventCreate event.
+type GuildScheduledEventCreate struct {
+	*GuildScheduledEvent
+}
+
+// GuildScheduledEventUpdate is the data for a GuildScheduledEventUpdate event.
+type GuildScheduledEventUpdate struct {
+	*GuildScheduledEvent
+}
+
+// GuildScheduledEventDelete is the data for a GuildScheduledEventDelete event.
+type GuildScheduledEventDelete struct {
+	*GuildScheduledEvent
+}
+
 // MessageAck is the data for a MessageAck event.
 type MessageAck struct {
 	MessageID string `json:"message_id"`
@@ -194,6 +256,7 @@ func (m *MessageDelete) UnmarshalJSON(b []byte) error {
 // MessageReactionAdd is the data for a MessageReactionAdd event.
 type MessageReactionAdd struct {
 	*MessageReaction
+	Member *Member `json:"member,omitempty"`
 }
 
 // MessageReactionRemove is the data for a MessageReactionRemove event.
@@ -293,3 +356,17 @@ type InteractionCreate struct {
 func (i *InteractionCreate) UnmarshalJSON(b []byte) error {
 	return json.Unmarshal(b, &i.Interaction)
 }
+
+// InviteCreate is the data for a InviteCreate event
+type InviteCreate struct {
+	*Invite
+	ChannelID string `json:"channel_id"`
+	GuildID   string `json:"guild_id"`
+}
+
+// InviteDelete is the data for a InviteDelete event
+type InviteDelete struct {
+	ChannelID string `json:"channel_id"`
+	GuildID   string `json:"guild_id"`
+	Code      string `json:"code"`
+}

+ 255 - 0
examples/autocomplete/main.go

@@ -0,0 +1,255 @@
+package main
+
+import (
+	"flag"
+	"fmt"
+	"log"
+	"os"
+	"os/signal"
+
+	"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:        "single-autocomplete",
+			Description: "Showcase of single autocomplete option",
+			Type:        discordgo.ChatApplicationCommand,
+			Options: []*discordgo.ApplicationCommandOption{
+				{
+					Name:         "autocomplete-option",
+					Description:  "Autocomplete option",
+					Type:         discordgo.ApplicationCommandOptionString,
+					Required:     true,
+					Autocomplete: true,
+				},
+			},
+		},
+		{
+			Name:        "multi-autocomplete",
+			Description: "Showcase of multiple autocomplete option",
+			Type:        discordgo.ChatApplicationCommand,
+			Options: []*discordgo.ApplicationCommandOption{
+				{
+					Name:         "autocomplete-option-1",
+					Description:  "Autocomplete option 1",
+					Type:         discordgo.ApplicationCommandOptionString,
+					Required:     true,
+					Autocomplete: true,
+				},
+				{
+					Name:         "autocomplete-option-2",
+					Description:  "Autocomplete option 2",
+					Type:         discordgo.ApplicationCommandOptionString,
+					Required:     true,
+					Autocomplete: true,
+				},
+			},
+		},
+	}
+
+	commandHandlers = map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){
+		"single-autocomplete": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
+			switch i.Type {
+			case discordgo.InteractionApplicationCommand:
+				data := i.ApplicationCommandData()
+				err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
+					Type: discordgo.InteractionResponseChannelMessageWithSource,
+					Data: &discordgo.InteractionResponseData{
+						Content: fmt.Sprintf(
+							"You picked %q autocompletion",
+							// Autocompleted options do not affect usual flow of handling application command. They are ordinary options at this stage
+							data.Options[0].StringValue(),
+						),
+					},
+				})
+				if err != nil {
+					panic(err)
+				}
+			// Autocomplete options introduce a new interaction type (8) for returning custom autocomplete results.
+			case discordgo.InteractionApplicationCommandAutocomplete:
+				data := i.ApplicationCommandData()
+				choices := []*discordgo.ApplicationCommandOptionChoice{
+					{
+						Name:  "Autocomplete",
+						Value: "autocomplete",
+					},
+					{
+						Name:  "Autocomplete is best!",
+						Value: "autocomplete_is_best",
+					},
+					{
+						Name:  "Choice 3",
+						Value: "choice3",
+					},
+					{
+						Name:  "Choice 4",
+						Value: "choice4",
+					},
+					{
+						Name:  "Choice 5",
+						Value: "choice5",
+					},
+					// And so on, up to 25 choices
+				}
+
+				if data.Options[0].StringValue() != "" {
+					choices = append(choices, &discordgo.ApplicationCommandOptionChoice{
+						Name:  data.Options[0].StringValue(), // To get user input you just get value of the autocomplete option.
+						Value: "choice_custom",
+					})
+				}
+
+				err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
+					Type: discordgo.InteractionApplicationCommandAutocompleteResult,
+					Data: &discordgo.InteractionResponseData{
+						Choices: choices, // This is basically the whole purpose of autocomplete interaction - return custom options to the user.
+					},
+				})
+				if err != nil {
+					panic(err)
+				}
+			}
+		},
+		"multi-autocomplete": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
+			switch i.Type {
+			case discordgo.InteractionApplicationCommand:
+				data := i.ApplicationCommandData()
+				err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
+					Type: discordgo.InteractionResponseChannelMessageWithSource,
+					Data: &discordgo.InteractionResponseData{
+						Content: fmt.Sprintf(
+							"Option 1: %s\nOption 2: %s",
+							data.Options[0].StringValue(),
+							data.Options[1].StringValue(),
+						),
+					},
+				})
+				if err != nil {
+					panic(err)
+				}
+			case discordgo.InteractionApplicationCommandAutocomplete:
+				data := i.ApplicationCommandData()
+				var choices []*discordgo.ApplicationCommandOptionChoice
+				switch {
+				// In this case there are multiple autocomplete options. The Focused field shows which option user is focused on.
+				case data.Options[0].Focused:
+					choices = []*discordgo.ApplicationCommandOptionChoice{
+						{
+							Name:  "Autocomplete 4 first option",
+							Value: "autocomplete_default",
+						},
+						{
+							Name:  "Choice 3",
+							Value: "choice3",
+						},
+						{
+							Name:  "Choice 4",
+							Value: "choice4",
+						},
+						{
+							Name:  "Choice 5",
+							Value: "choice5",
+						},
+					}
+					if data.Options[0].StringValue() != "" {
+						choices = append(choices, &discordgo.ApplicationCommandOptionChoice{
+							Name:  data.Options[0].StringValue(),
+							Value: "choice_custom",
+						})
+					}
+
+				case data.Options[1].Focused:
+					choices = []*discordgo.ApplicationCommandOptionChoice{
+						{
+							Name:  "Autocomplete 4 second option",
+							Value: "autocomplete_1_default",
+						},
+						{
+							Name:  "Choice 3.1",
+							Value: "choice3_1",
+						},
+						{
+							Name:  "Choice 4.1",
+							Value: "choice4_1",
+						},
+						{
+							Name:  "Choice 5.1",
+							Value: "choice5_1",
+						},
+					}
+					if data.Options[1].StringValue() != "" {
+						choices = append(choices, &discordgo.ApplicationCommandOptionChoice{
+							Name:  data.Options[1].StringValue(),
+							Value: "choice_custom_2",
+						})
+					}
+				}
+
+				err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
+					Type: discordgo.InteractionApplicationCommandAutocompleteResult,
+					Data: &discordgo.InteractionResponseData{
+						Choices: choices,
+					},
+				})
+				if err != nil {
+					panic(err)
+				}
+			}
+		},
+	}
+)
+
+func main() {
+	s.AddHandler(func(s *discordgo.Session, r *discordgo.Ready) { log.Println("Bot is up!") })
+	s.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) {
+		if h, ok := commandHandlers[i.ApplicationCommandData().Name]; ok {
+			h(s, i)
+		}
+	})
+	err := s.Open()
+	if err != nil {
+		log.Fatalf("Cannot open the session: %v", err)
+	}
+	defer s.Close()
+
+	createdCommands, err := s.ApplicationCommandBulkOverwrite(s.State.User.ID, *GuildID, commands)
+
+	if err != nil {
+		log.Fatalf("Cannot register commands: %v", err)
+	}
+
+	stop := make(chan os.Signal, 1)
+	signal.Notify(stop, os.Interrupt)
+	<-stop
+	log.Println("Gracefully shutting down")
+
+	if *RemoveCommands {
+		for _, cmd := range createdCommands {
+			err := s.ApplicationCommandDelete(s.State.User.ID, *GuildID, cmd.ID)
+			if err != nil {
+				log.Fatalf("Cannot delete %q command: %v", cmd.Name, err)
+			}
+		}
+	}
+}

+ 1 - 1
examples/avatar/main.go

@@ -82,7 +82,7 @@ func main() {
 	// Now lets format our base64 image into the proper format Discord wants
 	// and then call UserUpdate to set it as our user's Avatar.
 	avatar := fmt.Sprintf("data:%s;base64,%s", contentType, base64img)
-	_, err = dg.UserUpdate("", "", "", avatar, "")
+	_, err = dg.UserUpdate("", avatar)
 	if err != nil {
 		fmt.Println(err)
 	}

+ 2 - 1
examples/components/main.go

@@ -314,6 +314,7 @@ var (
 					},
 				}
 			case "multi":
+				minValues := 1
 				response = &discordgo.InteractionResponse{
 					Type: discordgo.InteractionResponseChannelMessageWithSource,
 					Data: &discordgo.InteractionResponseData{
@@ -328,7 +329,7 @@ var (
 										Placeholder: "Select tags to search on StackOverflow",
 										// This is where confusion comes from. If you don't specify these things you will get single item select.
 										// These fields control the minimum and maximum amount of selected items.
-										MinValues: 1,
+										MinValues: &minValues,
 										MaxValues: 3,
 										Options: []discordgo.SelectMenuOption{
 											{

+ 160 - 0
examples/modals/main.go

@@ -0,0 +1,160 @@
+package main
+
+import (
+	"flag"
+	"fmt"
+	"log"
+	"os"
+	"os/signal"
+	"strings"
+
+	"github.com/bwmarrin/discordgo"
+)
+
+// Bot parameters
+var (
+	GuildID        = flag.String("guild", "", "Test guild ID")
+	BotToken       = flag.String("token", "", "Bot access token")
+	AppID          = flag.String("app", "", "Application ID")
+	Cleanup        = flag.Bool("cleanup", true, "Cleanup of commands")
+	ResultsChannel = flag.String("results", "", "Channel where send survey results to")
+)
+
+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:        "modals-survey",
+			Description: "Take a survey about modals",
+		},
+	}
+	commandsHandlers = map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){
+		"modals-survey": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
+			err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
+				Type: discordgo.InteractionResponseModal,
+				Data: &discordgo.InteractionResponseData{
+					CustomID: "modals_survey_" + i.Interaction.Member.User.ID,
+					Title:    "Modals survey",
+					Components: []discordgo.MessageComponent{
+						discordgo.ActionsRow{
+							Components: []discordgo.MessageComponent{
+								discordgo.TextInput{
+									CustomID:    "opinion",
+									Label:       "What is your opinion on them?",
+									Style:       discordgo.TextInputShort,
+									Placeholder: "Don't be shy, share your opinion with us",
+									Required:    true,
+									MaxLength:   300,
+									MinLength:   10,
+								},
+							},
+						},
+						discordgo.ActionsRow{
+							Components: []discordgo.MessageComponent{
+								discordgo.TextInput{
+									CustomID:  "suggestions",
+									Label:     "What would you suggest to improve them?",
+									Style:     discordgo.TextInputParagraph,
+									Required:  false,
+									MaxLength: 2000,
+								},
+							},
+						},
+					},
+				},
+			})
+			if err != nil {
+				panic(err)
+			}
+		},
+	}
+)
+
+func main() {
+	s.AddHandler(func(s *discordgo.Session, r *discordgo.Ready) {
+		log.Println("Bot is up!")
+	})
+
+	s.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) {
+		switch i.Type {
+		case discordgo.InteractionApplicationCommand:
+			if h, ok := commandsHandlers[i.ApplicationCommandData().Name]; ok {
+				h(s, i)
+			}
+		case discordgo.InteractionModalSubmit:
+			err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
+				Type: discordgo.InteractionResponseChannelMessageWithSource,
+				Data: &discordgo.InteractionResponseData{
+					Content: "Thank you for taking your time to fill this survey",
+					Flags:   1 << 6,
+				},
+			})
+			if err != nil {
+				panic(err)
+			}
+			data := i.ModalSubmitData()
+
+			if !strings.HasPrefix(data.CustomID, "modals_survey") {
+				return
+			}
+
+			userid := strings.Split(data.CustomID, "_")[2]
+			_, err = s.ChannelMessageSend(*ResultsChannel, fmt.Sprintf(
+				"Feedback received. From <@%s>\n\n**Opinion**:\n%s\n\n**Suggestions**:\n%s",
+				userid,
+				data.Components[0].(*discordgo.ActionsRow).Components[0].(*discordgo.TextInput).Value,
+				data.Components[1].(*discordgo.ActionsRow).Components[0].(*discordgo.TextInput).Value,
+			))
+			if err != nil {
+				panic(err)
+			}
+		}
+	})
+
+	cmdIDs := make(map[string]string, len(commands))
+
+	for _, cmd := range commands {
+		rcmd, err := s.ApplicationCommandCreate(*AppID, *GuildID, &cmd)
+		if err != nil {
+			log.Fatalf("Cannot create slash command %q: %v", cmd.Name, err)
+		}
+
+		cmdIDs[rcmd.ID] = rcmd.Name
+	}
+
+	err := s.Open()
+	if err != nil {
+		log.Fatalf("Cannot open the session: %v", err)
+	}
+	defer s.Close()
+
+	stop := make(chan os.Signal, 1)
+	signal.Notify(stop, os.Interrupt)
+	<-stop
+	log.Println("Graceful shutdown")
+
+	if !*Cleanup {
+		return
+	}
+
+	for id, name := range cmdIDs {
+		err := s.ApplicationCommandDelete(*AppID, *GuildID, id)
+		if err != nil {
+			log.Fatalf("Cannot delete slash command %q: %v", name, err)
+		}
+	}
+
+}

+ 84 - 0
examples/scheduled_events/main.go

@@ -0,0 +1,84 @@
+package main
+
+import (
+	"flag"
+	"fmt"
+	"log"
+	"os"
+	"os/signal"
+	"time"
+
+	"github.com/bwmarrin/discordgo"
+)
+
+// Flags
+var (
+	GuildID        = flag.String("guild", "", "Test guild ID")
+	VoiceChannelID = flag.String("voice", "", "Test voice channel ID")
+	BotToken       = flag.String("token", "", "Bot token")
+)
+
+func init() { flag.Parse() }
+
+func main() {
+	s, _ := discordgo.New("Bot " + *BotToken)
+	s.AddHandler(func(s *discordgo.Session, r *discordgo.Ready) {
+		fmt.Println("Bot is ready")
+	})
+
+	err := s.Open()
+	if err != nil {
+		log.Fatalf("Cannot open the session: %v", err)
+	}
+	defer s.Close()
+
+	event := createAmazingEvent(s)
+	transformEventToExternalEvent(s, event)
+
+	stop := make(chan os.Signal, 1)
+	signal.Notify(stop, os.Interrupt)
+	<-stop
+	log.Println("Graceful shutdown")
+
+}
+
+// Create a new event on guild
+func createAmazingEvent(s *discordgo.Session) *discordgo.GuildScheduledEvent {
+	// Define the starting time (must be in future)
+	startingTime := time.Now().Add(1 * time.Hour)
+	// Define the ending time (must be after starting time)
+	endingTime := startingTime.Add(30 * time.Minute)
+	// Create the event
+	scheduledEvent, err := s.GuildScheduledEventCreate(*GuildID, &discordgo.GuildScheduledEventParams{
+		Name:               "Amazing Event",
+		Description:        "This event will start in 1 hour and last 30 minutes",
+		ScheduledStartTime: &startingTime,
+		ScheduledEndTime:   &endingTime,
+		EntityType:         discordgo.GuildScheduledEventEntityTypeVoice,
+		ChannelID:          *VoiceChannelID,
+		PrivacyLevel:       discordgo.GuildScheduledEventPrivacyLevelGuildOnly,
+	})
+	if err != nil {
+		log.Printf("Error creating scheduled event: %v", err)
+		return nil
+	}
+
+	fmt.Println("Created scheduled event:", scheduledEvent.Name)
+	return scheduledEvent
+}
+
+func transformEventToExternalEvent(s *discordgo.Session, event *discordgo.GuildScheduledEvent) {
+	scheduledEvent, err := s.GuildScheduledEventEdit(*GuildID, event.ID, &discordgo.GuildScheduledEventParams{
+		Name:       "Amazing Event @ Discord Website",
+		EntityType: discordgo.GuildScheduledEventEntityTypeExternal,
+		EntityMetadata: &discordgo.GuildScheduledEventEntityMetadata{
+			Location: "https://discord.com",
+		},
+	})
+	if err != nil {
+		log.Printf("Error during transformation of scheduled voice event into external event: %v", err)
+		return
+	}
+
+	fmt.Println("Created scheduled event:", scheduledEvent.Name)
+}

+ 48 - 11
examples/slash_commands/main.go

@@ -32,6 +32,8 @@ func init() {
 }
 
 var (
+	integerOptionMinValue = 1.0
+
 	commands = []*discordgo.ApplicationCommand{
 		{
 			Name: "basic-command",
@@ -59,6 +61,15 @@ var (
 					Type:        discordgo.ApplicationCommandOptionInteger,
 					Name:        "integer-option",
 					Description: "Integer option",
+					MinValue:    &integerOptionMinValue,
+					MaxValue:    10,
+					Required:    true,
+				},
+				{
+					Type:        discordgo.ApplicationCommandOptionNumber,
+					Name:        "number-option",
+					Description: "Float option",
+					MaxValue:    10.1,
 					Required:    true,
 				},
 				{
@@ -193,24 +204,26 @@ var (
 				// but this is much simpler
 				i.ApplicationCommandData().Options[0].StringValue(),
 				i.ApplicationCommandData().Options[1].IntValue(),
-				i.ApplicationCommandData().Options[2].BoolValue(),
+				i.ApplicationCommandData().Options[2].FloatValue(),
+				i.ApplicationCommandData().Options[3].BoolValue(),
 			}
 			msgformat :=
 				` Now you just learned how to use command options. Take a look to the value of which you've just entered:
 				> string_option: %s
 				> integer_option: %d
+				> number_option: %f
 				> bool_option: %v
 `
-			if len(i.ApplicationCommandData().Options) >= 4 {
-				margs = append(margs, i.ApplicationCommandData().Options[3].ChannelValue(nil).ID)
+			if len(i.ApplicationCommandData().Options) >= 5 {
+				margs = append(margs, i.ApplicationCommandData().Options[4].ChannelValue(nil).ID)
 				msgformat += "> channel-option: <#%s>\n"
 			}
-			if len(i.ApplicationCommandData().Options) >= 5 {
-				margs = append(margs, i.ApplicationCommandData().Options[4].UserValue(nil).ID)
+			if len(i.ApplicationCommandData().Options) >= 6 {
+				margs = append(margs, i.ApplicationCommandData().Options[5].UserValue(nil).ID)
 				msgformat += "> user-option: <@%s>\n"
 			}
-			if len(i.ApplicationCommandData().Options) >= 6 {
-				margs = append(margs, i.ApplicationCommandData().Options[5].RoleValue(nil, "").ID)
+			if len(i.ApplicationCommandData().Options) >= 7 {
+				margs = append(margs, i.ApplicationCommandData().Options[6].RoleValue(nil, "").ID)
 				msgformat += "> role-option: <@&%s>\n"
 			}
 			s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
@@ -363,24 +376,48 @@ func init() {
 
 func main() {
 	s.AddHandler(func(s *discordgo.Session, r *discordgo.Ready) {
-		log.Println("Bot is up!")
+		log.Printf("Logged in as: %v#%v", s.State.User.Username, s.State.User.Discriminator)
 	})
 	err := s.Open()
 	if err != nil {
 		log.Fatalf("Cannot open the session: %v", err)
 	}
 
-	for _, v := range commands {
-		_, err := s.ApplicationCommandCreate(s.State.User.ID, *GuildID, v)
+	log.Println("Adding commands...")
+	registeredCommands := make([]*discordgo.ApplicationCommand, len(commands))
+	for i, v := range commands {
+		cmd, err := s.ApplicationCommandCreate(s.State.User.ID, *GuildID, v)
 		if err != nil {
 			log.Panicf("Cannot create '%v' command: %v", v.Name, err)
 		}
+		registeredCommands[i] = cmd
 	}
 
 	defer s.Close()
 
-	stop := make(chan os.Signal)
+	stop := make(chan os.Signal, 1)
 	signal.Notify(stop, os.Interrupt)
+	log.Println("Press Ctrl+C to exit")
 	<-stop
+
+	if *RemoveCommands {
+		log.Println("Removing commands...")
+		// // We need to fetch the commands, since deleting requires the command ID.
+		// // We are doing this from the returned commands on line 375, because using
+		// // this will delete all the commands, which might not be desirable, so we
+		// // are deleting only the commands that we added.
+		// registeredCommands, err := s.ApplicationCommands(s.State.User.ID, *GuildID)
+		// if err != nil {
+		// 	log.Fatalf("Could not fetch registered commands: %v", err)
+		// }
+
+		for _, v := range registeredCommands {
+			err := s.ApplicationCommandDelete(s.State.User.ID, *GuildID, v.ID)
+			if err != nil {
+				log.Panicf("Cannot delete '%v' command: %v", v.Name, err)
+			}
+		}
+	}
+
 	log.Println("Gracefully shutdowning")
 }

+ 74 - 0
examples/threads/main.go

@@ -0,0 +1,74 @@
+package main
+
+import (
+	"flag"
+	"fmt"
+	"log"
+	"os"
+	"os/signal"
+	"strings"
+	"time"
+
+	"github.com/bwmarrin/discordgo"
+)
+
+// Flags
+var (
+	BotToken = flag.String("token", "", "Bot token")
+)
+
+const timeout time.Duration = time.Second * 10
+
+var games map[string]time.Time = make(map[string]time.Time)
+
+func init() { flag.Parse() }
+
+func main() {
+	s, _ := discordgo.New("Bot " + *BotToken)
+	s.AddHandler(func(s *discordgo.Session, r *discordgo.Ready) {
+		fmt.Println("Bot is ready")
+	})
+	s.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate) {
+		if strings.Contains(m.Content, "ping") {
+			if ch, err := s.State.Channel(m.ChannelID); err != nil || !ch.IsThread() {
+				thread, err := s.MessageThreadStartComplex(m.ChannelID, m.ID, &discordgo.ThreadStart{
+					Name:                "Pong game with " + m.Author.Username,
+					AutoArchiveDuration: 60,
+					Invitable:           false,
+					RateLimitPerUser:    10,
+				})
+				if err != nil {
+					panic(err)
+				}
+				_, _ = s.ChannelMessageSend(thread.ID, "pong")
+				m.ChannelID = thread.ID
+			} else {
+				_, _ = s.ChannelMessageSendReply(m.ChannelID, "pong", m.Reference())
+			}
+			games[m.ChannelID] = time.Now()
+			<-time.After(timeout)
+			if time.Since(games[m.ChannelID]) >= timeout {
+				_, err := s.ChannelEditComplex(m.ChannelID, &discordgo.ChannelEdit{
+					Archived: true,
+					Locked:   true,
+				})
+				if err != nil {
+					panic(err)
+				}
+			}
+		}
+	})
+	s.Identify.Intents = discordgo.MakeIntent(discordgo.IntentsAllWithoutPrivileged)
+
+	err := s.Open()
+	if err != nil {
+		log.Fatalf("Cannot open the session: %v", err)
+	}
+	defer s.Close()
+
+	stop := make(chan os.Signal, 1)
+	signal.Notify(stop, os.Interrupt)
+	<-stop
+	log.Println("Graceful shutdown")
+
+}

+ 143 - 30
interactions.go

@@ -30,15 +30,17 @@ const (
 
 // ApplicationCommand represents an application's slash command.
 type ApplicationCommand struct {
-	ID            string                 `json:"id,omitempty"`
-	ApplicationID string                 `json:"application_id,omitempty"`
-	Type          ApplicationCommandType `json:"type,omitempty"`
-	Name          string                 `json:"name"`
-	// NOTE: Chat commands only. Otherwise it mustn't be set.
-	Description string `json:"description,omitempty"`
-	Version     string `json:"version,omitempty"`
+	ID                string                 `json:"id,omitempty"`
+	ApplicationID     string                 `json:"application_id,omitempty"`
+	Version           string                 `json:"version,omitempty"`
+	Type              ApplicationCommandType `json:"type,omitempty"`
+	Name              string                 `json:"name"`
+	DefaultPermission *bool                  `json:"default_permission,omitempty"`
+
 	// NOTE: Chat commands only. Otherwise it mustn't be set.
-	Options []*ApplicationCommandOption `json:"options"`
+
+	Description string                      `json:"description,omitempty"`
+	Options     []*ApplicationCommandOption `json:"options"`
 }
 
 // ApplicationCommandOptionType indicates the type of a slash command's option.
@@ -55,6 +57,8 @@ const (
 	ApplicationCommandOptionChannel         ApplicationCommandOptionType = 7
 	ApplicationCommandOptionRole            ApplicationCommandOptionType = 8
 	ApplicationCommandOptionMentionable     ApplicationCommandOptionType = 9
+	ApplicationCommandOptionNumber          ApplicationCommandOptionType = 10
+	ApplicationCommandOptionAttachment      ApplicationCommandOptionType = 11
 )
 
 func (t ApplicationCommandOptionType) String() string {
@@ -77,6 +81,10 @@ func (t ApplicationCommandOptionType) String() string {
 		return "Role"
 	case ApplicationCommandOptionMentionable:
 		return "Mentionable"
+	case ApplicationCommandOptionNumber:
+		return "Number"
+	case ApplicationCommandOptionAttachment:
+		return "Attachment"
 	}
 	return fmt.Sprintf("ApplicationCommandOptionType(%d)", t)
 }
@@ -89,10 +97,18 @@ type ApplicationCommandOption struct {
 	// 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"`
+
+	ChannelTypes []ChannelType               `json:"channel_types"`
+	Required     bool                        `json:"required"`
+	Options      []*ApplicationCommandOption `json:"options"`
+
+	// NOTE: mutually exclusive with Choices.
+	Autocomplete bool                              `json:"autocomplete"`
 	Choices      []*ApplicationCommandOptionChoice `json:"choices"`
-	Options      []*ApplicationCommandOption       `json:"options"`
-	ChannelTypes []ChannelType                     `json:"channel_types"`
+	// Minimal value of number/integer option.
+	MinValue *float64 `json:"min_value,omitempty"`
+	// Maximum value of number/integer option.
+	MaxValue float64 `json:"max_value,omitempty"`
 }
 
 // ApplicationCommandOptionChoice represents a slash command option choice.
@@ -101,14 +117,45 @@ type ApplicationCommandOptionChoice struct {
 	Value interface{} `json:"value"`
 }
 
+// ApplicationCommandPermissions represents a single user or role permission for a command.
+type ApplicationCommandPermissions struct {
+	ID         string                           `json:"id"`
+	Type       ApplicationCommandPermissionType `json:"type"`
+	Permission bool                             `json:"permission"`
+}
+
+// ApplicationCommandPermissionsList represents a list of ApplicationCommandPermissions, needed for serializing to JSON.
+type ApplicationCommandPermissionsList struct {
+	Permissions []*ApplicationCommandPermissions `json:"permissions"`
+}
+
+// GuildApplicationCommandPermissions represents all permissions for a single guild command.
+type GuildApplicationCommandPermissions struct {
+	ID            string                           `json:"id"`
+	ApplicationID string                           `json:"application_id"`
+	GuildID       string                           `json:"guild_id"`
+	Permissions   []*ApplicationCommandPermissions `json:"permissions"`
+}
+
+// ApplicationCommandPermissionType indicates whether a permission is user or role based.
+type ApplicationCommandPermissionType uint8
+
+// Application command permission types.
+const (
+	ApplicationCommandPermissionTypeRole ApplicationCommandPermissionType = 1
+	ApplicationCommandPermissionTypeUser ApplicationCommandPermissionType = 2
+)
+
 // InteractionType indicates the type of an interaction event.
 type InteractionType uint8
 
 // Interaction types
 const (
-	InteractionPing               InteractionType = 1
-	InteractionApplicationCommand InteractionType = 2
-	InteractionMessageComponent   InteractionType = 3
+	InteractionPing                           InteractionType = 1
+	InteractionApplicationCommand             InteractionType = 2
+	InteractionMessageComponent               InteractionType = 3
+	InteractionApplicationCommandAutocomplete InteractionType = 4
+	InteractionModalSubmit                    InteractionType = 5
 )
 
 func (t InteractionType) String() string {
@@ -119,6 +166,8 @@ func (t InteractionType) String() string {
 		return "ApplicationCommand"
 	case InteractionMessageComponent:
 		return "MessageComponent"
+	case InteractionModalSubmit:
+		return "ModalSubmit"
 	}
 	return fmt.Sprintf("InteractionType(%d)", t)
 }
@@ -127,7 +176,7 @@ func (t InteractionType) String() string {
 type Interaction struct {
 	ID        string          `json:"id"`
 	Type      InteractionType `json:"type"`
-	Data      InteractionData `json:"-"`
+	Data      InteractionData `json:"data"`
 	GuildID   string          `json:"guild_id"`
 	ChannelID string          `json:"channel_id"`
 
@@ -146,6 +195,12 @@ type Interaction struct {
 	// Make sure to check for `nil` before using this field.
 	User *User `json:"user"`
 
+	// The user's discord client locale.
+	Locale Locale `json:"locale"`
+	// The guild's locale. This defaults to EnglishUS
+	// NOTE: this field is only filled when the interaction was invoked in a guild.
+	GuildLocale *Locale `json:"guild_locale"`
+
 	Token   string `json:"token"`
 	Version int    `json:"version"`
 }
@@ -168,7 +223,7 @@ func (i *Interaction) UnmarshalJSON(raw []byte) error {
 	*i = Interaction(tmp.interaction)
 
 	switch tmp.Type {
-	case InteractionApplicationCommand:
+	case InteractionApplicationCommand, InteractionApplicationCommandAutocomplete:
 		v := ApplicationCommandInteractionData{}
 		err = json.Unmarshal(tmp.Data, &v)
 		if err != nil {
@@ -182,6 +237,13 @@ func (i *Interaction) UnmarshalJSON(raw []byte) error {
 			return err
 		}
 		i.Data = v
+	case InteractionModalSubmit:
+		v := ModalSubmitInteractionData{}
+		err = json.Unmarshal(tmp.Data, &v)
+		if err != nil {
+			return err
+		}
+		i.Data = v
 	}
 	return nil
 }
@@ -198,12 +260,21 @@ func (i Interaction) MessageComponentData() (data MessageComponentInteractionDat
 // ApplicationCommandData is helper function to assert the inner InteractionData to ApplicationCommandInteractionData.
 // Make sure to check that the Type of the interaction is InteractionApplicationCommand before calling.
 func (i Interaction) ApplicationCommandData() (data ApplicationCommandInteractionData) {
-	if i.Type != InteractionApplicationCommand {
+	if i.Type != InteractionApplicationCommand && i.Type != InteractionApplicationCommandAutocomplete {
 		panic("ApplicationCommandData called on interaction of type " + i.Type.String())
 	}
 	return i.Data.(ApplicationCommandInteractionData)
 }
 
+// ModalSubmitData is helper function to assert the inner InteractionData to ModalSubmitInteractionData.
+// Make sure to check that the Type of the interaction is InteractionModalSubmit before calling.
+func (i Interaction) ModalSubmitData() (data ModalSubmitInteractionData) {
+	if i.Type != InteractionModalSubmit {
+		panic("ModalSubmitData called on interaction of type " + i.Type.String())
+	}
+	return i.Data.(ModalSubmitInteractionData)
+}
+
 // InteractionData is a common interface for all types of interaction data.
 type InteractionData interface {
 	Type() InteractionType
@@ -226,11 +297,12 @@ type ApplicationCommandInteractionData struct {
 // Partial Member objects are missing user, deaf and mute fields.
 // Partial Channel objects only have id, name, type and permissions fields.
 type ApplicationCommandInteractionDataResolved struct {
-	Users    map[string]*User    `json:"users"`
-	Members  map[string]*Member  `json:"members"`
-	Roles    map[string]*Role    `json:"roles"`
-	Channels map[string]*Channel `json:"channels"`
-	Messages map[string]*Message `json:"messages"`
+	Users       map[string]*User              `json:"users"`
+	Members     map[string]*Member            `json:"members"`
+	Roles       map[string]*Role              `json:"roles"`
+	Channels    map[string]*Channel           `json:"channels"`
+	Messages    map[string]*Message           `json:"messages"`
+	Attachments map[string]*MessageAttachment `json:"attachments"`
 }
 
 // Type returns the type of interaction data.
@@ -252,6 +324,36 @@ func (MessageComponentInteractionData) Type() InteractionType {
 	return InteractionMessageComponent
 }
 
+// ModalSubmitInteractionData contains the data of modal submit interaction.
+type ModalSubmitInteractionData struct {
+	CustomID   string             `json:"custom_id"`
+	Components []MessageComponent `json:"-"`
+}
+
+// Type returns the type of interaction data.
+func (ModalSubmitInteractionData) Type() InteractionType {
+	return InteractionModalSubmit
+}
+
+// UnmarshalJSON is a helper function to correctly unmarshal Components.
+func (d *ModalSubmitInteractionData) UnmarshalJSON(data []byte) error {
+	type modalSubmitInteractionData ModalSubmitInteractionData
+	var v struct {
+		modalSubmitInteractionData
+		RawComponents []unmarshalableMessageComponent `json:"components"`
+	}
+	err := json.Unmarshal(data, &v)
+	if err != nil {
+		return err
+	}
+	*d = ModalSubmitInteractionData(v.modalSubmitInteractionData)
+	d.Components = make([]MessageComponent, len(v.RawComponents))
+	for i, v := range v.RawComponents {
+		d.Components[i] = v.MessageComponent
+	}
+	return err
+}
+
 // ApplicationCommandInteractionDataOption represents an option of a slash command.
 type ApplicationCommandInteractionDataOption struct {
 	Name string                       `json:"name"`
@@ -259,6 +361,9 @@ type ApplicationCommandInteractionDataOption struct {
 	// NOTE: Contains the value specified by Type.
 	Value   interface{}                                `json:"value,omitempty"`
 	Options []*ApplicationCommandInteractionDataOption `json:"options,omitempty"`
+
+	// NOTE: autocomplete interaction only.
+	Focused bool `json:"focused,omitempty"`
 }
 
 // IntValue is a utility function for casting option value to integer
@@ -279,12 +384,10 @@ func (o ApplicationCommandInteractionDataOption) UintValue() uint64 {
 
 // FloatValue is a utility function for casting option value to float
 func (o ApplicationCommandInteractionDataOption) FloatValue() float64 {
-	// TODO: limit calls to Number type once it is released
-	if v, ok := o.Value.(float64); ok {
-		return v
+	if o.Type != ApplicationCommandOptionNumber {
+		panic("FloatValue called on data option of type " + o.Type.String())
 	}
-
-	return 0.0
+	return o.Value.(float64)
 }
 
 // StringValue is a utility function for casting option value to string
@@ -389,6 +492,10 @@ const (
 	InteractionResponseDeferredMessageUpdate InteractionResponseType = 6
 	// InteractionResponseUpdateMessage is for updating the message to which message component was attached.
 	InteractionResponseUpdateMessage InteractionResponseType = 7
+	// InteractionApplicationCommandAutocompleteResult shows autocompletion results. Autocomplete interaction only.
+	InteractionApplicationCommandAutocompleteResult InteractionResponseType = 8
+	// InteractionResponseModal is for responding to an interaction with a modal window.
+	InteractionResponseModal InteractionResponseType = 9
 )
 
 // InteractionResponse represents a response for an interaction event.
@@ -402,12 +509,18 @@ type InteractionResponseData struct {
 	TTS             bool                    `json:"tts"`
 	Content         string                  `json:"content"`
 	Components      []MessageComponent      `json:"components"`
-	Embeds          []*MessageEmbed         `json:"embeds,omitempty"`
+	Embeds          []*MessageEmbed         `json:"embeds"`
 	AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"`
+	Flags           uint64                  `json:"flags,omitempty"`
+	Files           []*File                 `json:"-"`
+
+	// NOTE: autocomplete interaction only.
+	Choices []*ApplicationCommandOptionChoice `json:"choices,omitempty"`
 
-	Flags uint64 `json:"flags,omitempty"`
+	// NOTE: modal interaction only.
 
-	Files []*File `json:"-"`
+	CustomID string `json:"custom_id,omitempty"`
+	Title    string `json:"title,omitempty"`
 }
 
 // VerifyInteraction implements message verification of the discord interactions api

+ 83 - 0
locales.go

@@ -0,0 +1,83 @@
+package discordgo
+
+// Locale represents the accepted languages for Discord.
+// https://discord.com/developers/docs/reference#locales
+type Locale string
+
+// String returns the human-readable string of the locale
+func (l Locale) String() string {
+	if name, ok := Locales[l]; ok {
+		return name
+	}
+	return Unknown.String()
+}
+
+// All defined locales in Discord
+const (
+	EnglishUS    Locale = "en-US"
+	EnglishGB    Locale = "en-GB"
+	Bulgarian    Locale = "bg"
+	ChineseCN    Locale = "zh-CN"
+	ChineseTW    Locale = "zh-TW"
+	Croatian     Locale = "hr"
+	Czech        Locale = "cs"
+	Danish       Locale = "da"
+	Dutch        Locale = "nl"
+	Finnish      Locale = "fi"
+	French       Locale = "fr"
+	German       Locale = "de"
+	Greek        Locale = "el"
+	Hindi        Locale = "hi"
+	Hungarian    Locale = "hu"
+	Italian      Locale = "it"
+	Japanese     Locale = "ja"
+	Korean       Locale = "ko"
+	Lithuanian   Locale = "lt"
+	Norwegian    Locale = "no"
+	Polish       Locale = "pl"
+	PortugueseBR Locale = "pt-BR"
+	Romanian     Locale = "ro"
+	Russian      Locale = "ru"
+	SpanishES    Locale = "es-ES"
+	Swedish      Locale = "sv-SE"
+	Thai         Locale = "th"
+	Turkish      Locale = "tr"
+	Ukrainian    Locale = "uk"
+	Vietnamese   Locale = "vi"
+	Unknown      Locale = ""
+)
+
+// Locales is a map of all the languages codes to their names.
+var Locales = map[Locale]string{
+	EnglishUS:    "English (United States)",
+	EnglishGB:    "English (Great Britain)",
+	Bulgarian:    "Bulgarian",
+	ChineseCN:    "Chinese (China)",
+	ChineseTW:    "Chinese (Taiwan)",
+	Croatian:     "Croatian",
+	Czech:        "Czech",
+	Danish:       "Danish",
+	Dutch:        "Dutch",
+	Finnish:      "Finnish",
+	French:       "French",
+	German:       "German",
+	Greek:        "Greek",
+	Hindi:        "Hindi",
+	Hungarian:    "Hungarian",
+	Italian:      "Italian",
+	Japanese:     "Japanese",
+	Korean:       "Korean",
+	Lithuanian:   "Lithuanian",
+	Norwegian:    "Norwegian",
+	Polish:       "Polish",
+	PortugueseBR: "Portuguese (Brazil)",
+	Romanian:     "Romanian",
+	Russian:      "Russian",
+	SpanishES:    "Spanish (Spain)",
+	Swedish:      "Swedish",
+	Thai:         "Thai",
+	Turkish:      "Turkish",
+	Ukrainian:    "Ukrainian",
+	Vietnamese:   "Vietnamese",
+	Unknown:      "unknown",
+}

+ 62 - 16
message.go

@@ -14,6 +14,7 @@ import (
 	"io"
 	"regexp"
 	"strings"
+	"time"
 )
 
 // MessageType is the type of Message
@@ -37,8 +38,10 @@ const (
 	MessageTypeChannelFollowAdd                      MessageType = 12
 	MessageTypeGuildDiscoveryDisqualified            MessageType = 14
 	MessageTypeGuildDiscoveryRequalified             MessageType = 15
+	MessageTypeThreadCreated                         MessageType = 18
 	MessageTypeReply                                 MessageType = 19
 	MessageTypeChatInputCommand                      MessageType = 20
+	MessageTypeThreadStarterMessage                  MessageType = 21
 	MessageTypeContextMenuCommand                    MessageType = 23
 )
 
@@ -60,11 +63,11 @@ type Message struct {
 	// CAUTION: this field may be removed in a
 	// future API version; it is safer to calculate
 	// the creation time via the ID.
-	Timestamp Timestamp `json:"timestamp"`
+	Timestamp time.Time `json:"timestamp"`
 
 	// The time at which the last edit of the message
 	// occurred, if it has been edited.
-	EditedTimestamp Timestamp `json:"edited_timestamp"`
+	EditedTimestamp *time.Time `json:"edited_timestamp"`
 
 	// The roles mentioned in the message.
 	MentionRoles []string `json:"mention_roles"`
@@ -125,10 +128,28 @@ type Message struct {
 	// To generate a reference to this message, use (*Message).Reference().
 	MessageReference *MessageReference `json:"message_reference"`
 
+	// The message associated with the message_reference
+	// NOTE: This field is only returned for messages with a type of 19 (REPLY) or 21 (THREAD_STARTER_MESSAGE).
+	// If the message is a reply but the referenced_message field is not present,
+	// the backend did not attempt to fetch the message that was being replied to, so its state is unknown.
+	// If the field exists but is null, the referenced message was deleted.
+	ReferencedMessage *Message `json:"referenced_message"`
+
+	// Is sent when the message is a response to an Interaction, without an existing message.
+	// This means responses to message component interactions do not include this property,
+	// instead including a MessageReference, as components exist on preexisting messages.
+	Interaction *MessageInteraction `json:"interaction"`
+
 	// The flags of the message, which describe extra features of a message.
 	// This is a combination of bit masks; the presence of a certain permission can
 	// be checked by performing a bitwise AND between this int and the flag.
 	Flags MessageFlags `json:"flags"`
+
+	// The thread that was started from this message, includes thread member object
+	Thread *Channel `json:"thread,omitempty"`
+
+	// An array of Sticker objects, if any were sent.
+	StickerItems []*Sticker `json:"sticker_items"`
 }
 
 // UnmarshalJSON is a helper function to unmarshal the Message.
@@ -174,11 +195,24 @@ type MessageFlags int
 
 // Valid MessageFlags values
 const (
-	MessageFlagsCrossPosted          MessageFlags = 1 << 0
-	MessageFlagsIsCrossPosted        MessageFlags = 1 << 1
-	MessageFlagsSupressEmbeds        MessageFlags = 1 << 2
+	// MessageFlagsCrossPosted This message has been published to subscribed channels (via Channel Following).
+	MessageFlagsCrossPosted MessageFlags = 1 << 0
+	// MessageFlagsIsCrossPosted this message originated from a message in another channel (via Channel Following).
+	MessageFlagsIsCrossPosted MessageFlags = 1 << 1
+	// MessageFlagsSupressEmbeds do not include any embeds when serializing this message.
+	MessageFlagsSupressEmbeds MessageFlags = 1 << 2
+	// MessageFlagsSourceMessageDeleted the source message for this crosspost has been deleted (via Channel Following).
 	MessageFlagsSourceMessageDeleted MessageFlags = 1 << 3
-	MessageFlagsUrgent               MessageFlags = 1 << 4
+	// MessageFlagsUrgent this message came from the urgent message system.
+	MessageFlagsUrgent MessageFlags = 1 << 4
+	// MessageFlagsHasThread this message has an associated thread, with the same id as the message.
+	MessageFlagsHasThread MessageFlags = 1 << 5
+	// MessageFlagsEphemeral this message is only visible to the user who invoked the Interaction.
+	MessageFlagsEphemeral MessageFlags = 1 << 6
+	// MessageFlagsLoading this message is an Interaction Response and the bot is "thinking".
+	MessageFlagsLoading MessageFlags = 1 << 7
+	// MessageFlagsFailedToMentionSomeRolesInThread this message failed to mention some roles and add their members to the thread.
+	MessageFlagsFailedToMentionSomeRolesInThread MessageFlags = 1 << 8
 )
 
 // File stores info about files you e.g. send in messages.
@@ -191,7 +225,7 @@ type File struct {
 // MessageSend stores all parameters you can send with ChannelMessageSendComplex.
 type MessageSend struct {
 	Content         string                  `json:"content,omitempty"`
-	Embeds          []*MessageEmbed         `json:"embeds,omitempty"`
+	Embeds          []*MessageEmbed         `json:"embeds"`
 	TTS             bool                    `json:"tts"`
 	Components      []MessageComponent      `json:"components"`
 	Files           []*File                 `json:"-"`
@@ -210,7 +244,7 @@ type MessageSend struct {
 type MessageEdit struct {
 	Content         *string                 `json:"content,omitempty"`
 	Components      []MessageComponent      `json:"components"`
-	Embeds          []*MessageEmbed         `json:"embeds,omitempty"`
+	Embeds          []*MessageEmbed         `json:"embeds"`
 	AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"`
 
 	ID      string
@@ -288,14 +322,15 @@ type MessageAllowedMentions struct {
 
 // A MessageAttachment stores data for message attachments.
 type MessageAttachment struct {
-	ID        string `json:"id"`
-	URL       string `json:"url"`
-	ProxyURL  string `json:"proxy_url"`
-	Filename  string `json:"filename"`
-	Width     int    `json:"width"`
-	Height    int    `json:"height"`
-	Size      int    `json:"size"`
-	Ephemeral bool   `json:"ephemeral"`
+	ID          string `json:"id"`
+	URL         string `json:"url"`
+	ProxyURL    string `json:"proxy_url"`
+	Filename    string `json:"filename"`
+	ContentType string `json:"content_type"`
+	Width       int    `json:"width"`
+	Height      int    `json:"height"`
+	Size        int    `json:"size"`
+	Ephemeral   bool   `json:"ephemeral"`
 }
 
 // MessageEmbedFooter is a part of a MessageEmbed struct.
@@ -493,3 +528,14 @@ func (m *Message) ContentWithMoreMentionsReplaced(s *Session) (content string, e
 	})
 	return
 }
+
+// MessageInteraction contains information about the application command interaction which generated the message.
+type MessageInteraction struct {
+	ID   string          `json:"id"`
+	Type InteractionType `json:"type"`
+	Name string          `json:"name"`
+	User *User           `json:"user"`
+
+	// Member is only present when the interaction is from a guild.
+	Member *Member `json:"member"`
+}

+ 6 - 25
oauth2.go

@@ -40,23 +40,6 @@ type Team struct {
 	Members     []*TeamMember `json:"members"`
 }
 
-// An Application struct stores values for a Discord OAuth2 Application
-type Application struct {
-	ID                  string    `json:"id,omitempty"`
-	Name                string    `json:"name"`
-	Description         string    `json:"description,omitempty"`
-	Icon                string    `json:"icon,omitempty"`
-	Secret              string    `json:"secret,omitempty"`
-	RedirectURIs        *[]string `json:"redirect_uris,omitempty"`
-	BotRequireCodeGrant bool      `json:"bot_require_code_grant,omitempty"`
-	BotPublic           bool      `json:"bot_public,omitempty"`
-	RPCApplicationState int       `json:"rpc_application_state,omitempty"`
-	Flags               int       `json:"flags,omitempty"`
-	Owner               *User     `json:"owner"`
-	Bot                 *User     `json:"bot"`
-	Team                *Team     `json:"team"`
-}
-
 // Application returns an Application structure of a specific Application
 //   appID : The ID of an Application
 func (s *Session) Application(appID string) (st *Application, err error) {
@@ -88,10 +71,9 @@ func (s *Session) Applications() (st []*Application, err error) {
 func (s *Session) ApplicationCreate(ap *Application) (st *Application, err error) {
 
 	data := struct {
-		Name         string    `json:"name"`
-		Description  string    `json:"description"`
-		RedirectURIs *[]string `json:"redirect_uris,omitempty"`
-	}{ap.Name, ap.Description, ap.RedirectURIs}
+		Name        string `json:"name"`
+		Description string `json:"description"`
+	}{ap.Name, ap.Description}
 
 	body, err := s.RequestWithBucketID("POST", EndpointOAuth2Applications, data, EndpointOAuth2Applications)
 	if err != nil {
@@ -107,10 +89,9 @@ func (s *Session) ApplicationCreate(ap *Application) (st *Application, err error
 func (s *Session) ApplicationUpdate(appID string, ap *Application) (st *Application, err error) {
 
 	data := struct {
-		Name         string    `json:"name"`
-		Description  string    `json:"description"`
-		RedirectURIs *[]string `json:"redirect_uris,omitempty"`
-	}{ap.Name, ap.Description, ap.RedirectURIs}
+		Name        string `json:"name"`
+		Description string `json:"description"`
+	}{ap.Name, ap.Description}
 
 	body, err := s.RequestWithBucketID("PUT", EndpointOAuth2Application(appID), data, EndpointOAuth2Application(""))
 	if err != nil {

+ 538 - 239
restapi.go

@@ -39,6 +39,40 @@ var (
 	ErrUnauthorized            = errors.New("HTTP request was unauthorized. This could be because the provided token was not a bot token. Please add \"Bot \" to the start of your token. https://discord.com/developers/docs/reference#authentication-example-bot-token-authorization-header")
 )
 
+// RESTError stores error information about a request with a bad response code.
+// Message is not always present, there are cases where api calls can fail
+// without returning a json message.
+type RESTError struct {
+	Request      *http.Request
+	Response     *http.Response
+	ResponseBody []byte
+
+	Message *APIErrorMessage // Message may be nil.
+}
+
+// newRestError returns a new REST API error.
+func newRestError(req *http.Request, resp *http.Response, body []byte) *RESTError {
+	restErr := &RESTError{
+		Request:      req,
+		Response:     resp,
+		ResponseBody: body,
+	}
+
+	// Attempt to decode the error and assume no message was provided if it fails
+	var msg *APIErrorMessage
+	err := json.Unmarshal(body, &msg)
+	if err == nil {
+		restErr.Message = msg
+	}
+
+	return restErr
+}
+
+// Error returns a Rest API Error with its status code and body.
+func (r RESTError) Error() string {
+	return "HTTP " + r.Response.Status + ", " + string(r.ResponseBody)
+}
+
 // Request is the same as RequestWithBucketID but the bucket id is the same as the urlStr
 func (s *Session) Request(method, urlStr string, data interface{}) (response []byte, err error) {
 	return s.RequestWithBucketID(method, urlStr, data, strings.SplitN(urlStr, "?", 2)[0])
@@ -108,7 +142,7 @@ func (s *Session) RequestWithLockedBucket(method, urlStr, contentType string, b
 	}
 	defer func() {
 		err2 := resp.Body.Close()
-		if err2 != nil {
+		if s.Debug && err2 != nil {
 			log.Println("error closing resp body")
 		}
 	}()
@@ -182,91 +216,6 @@ func unmarshal(data []byte, v interface{}) error {
 	return nil
 }
 
-// ------------------------------------------------------------------------------------------------
-// Functions specific to Discord Sessions
-// ------------------------------------------------------------------------------------------------
-
-// Login asks the Discord server for an authentication token.
-//
-// NOTE: While email/pass authentication is supported by DiscordGo it is
-// HIGHLY DISCOURAGED by Discord. Please only use email/pass to obtain a token
-// and then use that authentication token for all future connections.
-// Also, doing any form of automation with a user (non Bot) account may result
-// in that account being permanently banned from Discord.
-func (s *Session) Login(email, password string) (err error) {
-
-	data := struct {
-		Email    string `json:"email"`
-		Password string `json:"password"`
-	}{email, password}
-
-	response, err := s.RequestWithBucketID("POST", EndpointLogin, data, EndpointLogin)
-	if err != nil {
-		return
-	}
-
-	temp := struct {
-		Token string `json:"token"`
-		MFA   bool   `json:"mfa"`
-	}{}
-
-	err = unmarshal(response, &temp)
-	if err != nil {
-		return
-	}
-
-	s.Token = temp.Token
-	s.MFA = temp.MFA
-	return
-}
-
-// Register sends a Register request to Discord, and returns the authentication token
-// Note that this account is temporary and should be verified for future use.
-// Another option is to save the authentication token external, but this isn't recommended.
-func (s *Session) Register(username string) (token string, err error) {
-
-	data := struct {
-		Username string `json:"username"`
-	}{username}
-
-	response, err := s.RequestWithBucketID("POST", EndpointRegister, data, EndpointRegister)
-	if err != nil {
-		return
-	}
-
-	temp := struct {
-		Token string `json:"token"`
-	}{}
-
-	err = unmarshal(response, &temp)
-	if err != nil {
-		return
-	}
-
-	token = temp.Token
-	return
-}
-
-// Logout sends a logout request to Discord.
-// This does not seem to actually invalidate the token.  So you can still
-// make API calls even after a Logout.  So, it seems almost pointless to
-// even use.
-func (s *Session) Logout() (err error) {
-
-	//  _, err = s.Request("POST", LOGOUT, `{"token": "` + s.Token + `"}`)
-
-	if s.Token == "" {
-		return
-	}
-
-	data := struct {
-		Token string `json:"token"`
-	}{s.Token}
-
-	_, err = s.RequestWithBucketID("POST", EndpointLogout, data, EndpointLogout)
-	return
-}
-
 // ------------------------------------------------------------------------------------------------
 // Functions specific to Discord Users
 // ------------------------------------------------------------------------------------------------
@@ -307,8 +256,8 @@ func (s *Session) UserAvatarDecode(u *User) (img image.Image, err error) {
 	return
 }
 
-// UserUpdate updates a users settings.
-func (s *Session) UserUpdate(email, password, username, avatar, newPassword string) (st *User, err error) {
+// UserUpdate updates current user settings.
+func (s *Session) UserUpdate(username, avatar string) (st *User, err error) {
 
 	// NOTE: Avatar must be either the hash/id of existing Avatar or
 	// data:image/png;base64,BASE64_STRING_OF_NEW_AVATAR_PNG
@@ -316,12 +265,9 @@ func (s *Session) UserUpdate(email, password, username, avatar, newPassword stri
 	// If left blank, avatar will be set to null/blank
 
 	data := struct {
-		Email       string `json:"email,omitempty"`
-		Password    string `json:"password,omitempty"`
-		Username    string `json:"username,omitempty"`
-		Avatar      string `json:"avatar,omitempty"`
-		NewPassword string `json:"new_password,omitempty"`
-	}{email, password, username, avatar, newPassword}
+		Username string `json:"username,omitempty"`
+		Avatar   string `json:"avatar,omitempty"`
+	}{username, avatar}
 
 	body, err := s.RequestWithBucketID("PATCH", EndpointUser("@me"), data, EndpointUsers)
 	if err != nil {
@@ -332,39 +278,6 @@ func (s *Session) UserUpdate(email, password, username, avatar, newPassword stri
 	return
 }
 
-// UserSettings returns the settings for a given user
-func (s *Session) UserSettings() (st *Settings, err error) {
-
-	body, err := s.RequestWithBucketID("GET", EndpointUserSettings("@me"), nil, EndpointUserSettings(""))
-	if err != nil {
-		return
-	}
-
-	err = unmarshal(body, &st)
-	return
-}
-
-// UserUpdateStatus update the user status
-// status   : The new status (Actual valid status are 'online','idle','dnd','invisible')
-func (s *Session) UserUpdateStatus(status Status) (st *Settings, err error) {
-	if status == StatusOffline {
-		err = ErrStatusOffline
-		return
-	}
-
-	data := struct {
-		Status Status `json:"status"`
-	}{status}
-
-	body, err := s.RequestWithBucketID("PATCH", EndpointUserSettings("@me"), data, EndpointUserSettings(""))
-	if err != nil {
-		return
-	}
-
-	err = unmarshal(body, &st)
-	return
-}
-
 // UserConnections returns the user's connections
 func (s *Session) UserConnections() (conn []*UserConnection, err error) {
 	response, err := s.RequestWithBucketID("GET", EndpointUserConnections("@me"), nil, EndpointUserConnections("@me"))
@@ -380,19 +293,6 @@ func (s *Session) UserConnections() (conn []*UserConnection, err error) {
 	return
 }
 
-// UserChannels returns an array of Channel structures for all private
-// channels.
-func (s *Session) UserChannels() (st []*Channel, err error) {
-
-	body, err := s.RequestWithBucketID("GET", EndpointUserChannels("@me"), nil, EndpointUserChannels(""))
-	if err != nil {
-		return
-	}
-
-	err = unmarshal(body, &st)
-	return
-}
-
 // UserChannelCreate creates a new User (Private) Channel with another User
 // recipientID : A user ID for the user to which this channel is opened with.
 func (s *Session) UserChannelCreate(recipientID string) (st *Channel, err error) {
@@ -443,20 +343,6 @@ func (s *Session) UserGuilds(limit int, beforeID, afterID string) (st []*UserGui
 	return
 }
 
-// UserGuildSettingsEdit Edits the users notification settings for a guild
-// guildID   : The ID of the guild to edit the settings on
-// settings  : The settings to update
-func (s *Session) UserGuildSettingsEdit(guildID string, settings *UserGuildSettingsEdit) (st *UserGuildSettings, err error) {
-
-	body, err := s.RequestWithBucketID("PATCH", EndpointUserGuildSettings("@me", guildID), settings, EndpointUserGuildSettings("", guildID))
-	if err != nil {
-		return
-	}
-
-	err = unmarshal(body, &st)
-	return
-}
-
 // UserChannelPermissions returns the permission of a user in a channel.
 // userID    : The ID of the user to calculate permissions for.
 // channelID : The ID of the channel to calculate permission for.
@@ -790,6 +676,8 @@ func (s *Session) GuildMember(guildID, userID string) (st *Member, err error) {
 	}
 
 	err = unmarshal(body, &st)
+	// The returned object doesn't have the GuildID attribute so we will set it here.
+	st.GuildID = guildID
 	return
 }
 
@@ -903,6 +791,20 @@ func (s *Session) GuildMemberMute(guildID string, userID string, mute bool) (err
 	return
 }
 
+// GuildMemberTimeout times out a guild member
+//  guildID   : The ID of a Guild.
+//  userID    : The ID of a User.
+//  until     : The timestamp for how long a member should be timed out.
+//              Set to nil to remove timeout.
+func (s *Session) GuildMemberTimeout(guildID string, userID string, until *time.Time) (err error) {
+	data := struct {
+		CommunicationDisabledUntil *time.Time `json:"communication_disabled_until"`
+	}{until}
+
+	_, err = s.RequestWithBucketID("PATCH", EndpointGuildMember(guildID, userID), data, EndpointGuildMember(guildID, ""))
+	return
+}
+
 // GuildMemberDeafen server deafens a guild member
 //  guildID   : The ID of a Guild.
 //  userID    : The ID of a User.
@@ -1234,15 +1136,6 @@ func (s *Session) GuildIntegrationDelete(guildID, integrationID string) (err err
 	return
 }
 
-// GuildIntegrationSync syncs an integration.
-// guildID          : The ID of a Guild.
-// integrationID    : The ID of an integration.
-func (s *Session) GuildIntegrationSync(guildID, integrationID string) (err error) {
-
-	_, err = s.RequestWithBucketID("POST", EndpointGuildIntegrationSync(guildID, integrationID), nil, EndpointGuildIntegration(guildID, ""))
-	return
-}
-
 // GuildIcon returns an image.Image of a guild icon.
 // guildID   : The ID of a Guild.
 func (s *Session) GuildIcon(guildID string) (img image.Image, err error) {
@@ -1411,6 +1304,111 @@ func (s *Session) GuildEmojiDelete(guildID, emojiID string) (err error) {
 	return
 }
 
+// GuildTemplate returns a GuildTemplate for the given code
+// templateCode: The Code of a GuildTemplate
+func (s *Session) GuildTemplate(templateCode string) (st *GuildTemplate, err error) {
+
+	body, err := s.RequestWithBucketID("GET", EndpointGuildTemplate(templateCode), nil, EndpointGuildTemplate(templateCode))
+	if err != nil {
+		return
+	}
+
+	err = unmarshal(body, &st)
+	return
+}
+
+// GuildCreateWithTemplate creates a guild based on a GuildTemplate
+// templateCode: The Code of a GuildTemplate
+// name: The name of the guild (2-100) characters
+// icon: base64 encoded 128x128 image for the guild icon
+func (s *Session) GuildCreateWithTemplate(templateCode, name, icon string) (st *Guild, err error) {
+
+	data := struct {
+		Name string `json:"name"`
+		Icon string `json:"icon"`
+	}{name, icon}
+
+	body, err := s.RequestWithBucketID("POST", EndpointGuildTemplate(templateCode), data, EndpointGuildTemplate(templateCode))
+	if err != nil {
+		return
+	}
+
+	err = unmarshal(body, &st)
+	return
+}
+
+// GuildTemplates returns all of GuildTemplates
+// guildID: The ID of the guild
+func (s *Session) GuildTemplates(guildID string) (st []*GuildTemplate, err error) {
+
+	body, err := s.RequestWithBucketID("GET", EndpointGuildTemplates(guildID), nil, EndpointGuildTemplates(guildID))
+	if err != nil {
+		return
+	}
+
+	err = unmarshal(body, &st)
+	return
+}
+
+// GuildTemplateCreate creates a template for the guild
+// guildID: The ID of the guild
+// name: The name of the template (1-100 characters)
+// description: The description for the template (0-120 characters)
+func (s *Session) GuildTemplateCreate(guildID, name, description string) (st *GuildTemplate) {
+
+	data := struct {
+		Name        string `json:"name"`
+		Description string `json:"description"`
+	}{name, description}
+
+	body, err := s.RequestWithBucketID("POST", EndpointGuildTemplates(guildID), data, EndpointGuildTemplates(guildID))
+	if err != nil {
+		return
+	}
+
+	err = unmarshal(body, &st)
+	return
+}
+
+// GuildTemplateSync syncs the template to the guild's current state
+// guildID: The ID of the guild
+// templateCode: The code of the template
+func (s *Session) GuildTemplateSync(guildID, templateCode string) (err error) {
+
+	_, err = s.RequestWithBucketID("PUT", EndpointGuildTemplateSync(guildID, templateCode), nil, EndpointGuildTemplateSync(guildID, ""))
+	return
+}
+
+// GuildTemplateEdit modifies the template's metadata
+// guildID: The ID of the guild
+// templateCode: The code of the template
+// name: The name of the template (1-100 characters)
+// description: The description for the template (0-120 characters)
+func (s *Session) GuildTemplateEdit(guildID, templateCode, name, description string) (st *GuildTemplate, err error) {
+
+	data := struct {
+		Name        string `json:"name,omitempty"`
+		Description string `json:"description,omitempty"`
+	}{name, description}
+
+	body, err := s.RequestWithBucketID("PATCH", EndpointGuildTemplateSync(guildID, templateCode), data, EndpointGuildTemplateSync(guildID, ""))
+	if err != nil {
+		return
+	}
+
+	err = unmarshal(body, &st)
+	return
+}
+
+// GuildTemplateDelete deletes the template
+// guildID: The ID of the guild
+// templateCode: The code of the template
+func (s *Session) GuildTemplateDelete(guildID, templateCode string) (err error) {
+
+	_, err = s.RequestWithBucketID("DELETE", EndpointGuildTemplateSync(guildID, templateCode), nil, EndpointGuildTemplateSync(guildID, ""))
+	return
+}
+
 // ------------------------------------------------------------------------------------------------
 // Functions specific to Discord Channels
 // ------------------------------------------------------------------------------------------------
@@ -1522,21 +1520,6 @@ func (s *Session) ChannelMessage(channelID, messageID string) (st *Message, err
 	return
 }
 
-// ChannelMessageAck acknowledges and marks the given message as read
-// channeld  : The ID of a Channel
-// messageID : the ID of a Message
-// lastToken : token returned by last ack
-func (s *Session) ChannelMessageAck(channelID, messageID, lastToken string) (st *Ack, err error) {
-
-	body, err := s.RequestWithBucketID("POST", EndpointChannelMessageAck(channelID, messageID), &Ack{Token: lastToken}, EndpointChannelMessageAck(channelID, ""))
-	if err != nil {
-		return
-	}
-
-	err = unmarshal(body, &st)
-	return
-}
-
 // ChannelMessageSend sends a message to the given channel.
 // channelID : The ID of a Channel.
 // content   : The message to send.
@@ -1944,18 +1927,6 @@ func (s *Session) VoiceRegions() (st []*VoiceRegion, err error) {
 	return
 }
 
-// VoiceICE returns the voice server ICE information
-func (s *Session) VoiceICE() (st *VoiceICE, err error) {
-
-	body, err := s.RequestWithBucketID("GET", EndpointVoiceIce, nil, EndpointVoiceIce)
-	if err != nil {
-		return
-	}
-
-	err = unmarshal(body, &st)
-	return
-}
-
 // ------------------------------------------------------------------------------------------------
 // Functions specific to Discord Websockets
 // ------------------------------------------------------------------------------------------------
@@ -2158,15 +2129,19 @@ func (s *Session) WebhookDeleteWithToken(webhookID, token string) (st *Webhook,
 	return
 }
 
-// WebhookExecute executes a webhook.
-// webhookID: The ID of a webhook.
-// token    : The auth token for the webhook
-// wait     : Waits for server confirmation of message send and ensures that the return struct is populated (it is nil otherwise)
-func (s *Session) WebhookExecute(webhookID, token string, wait bool, data *WebhookParams) (st *Message, err error) {
+func (s *Session) webhookExecute(webhookID, token string, wait bool, threadID string, data *WebhookParams) (st *Message, err error) {
 	uri := EndpointWebhookToken(webhookID, token)
 
+	v := url.Values{}
 	if wait {
-		uri += "?wait=true"
+		v.Set("wait", "true")
+	}
+
+	if threadID != "" {
+		v.Set("thread_id", threadID)
+	}
+	if len(v) != 0 {
+		uri += "?" + v.Encode()
 	}
 
 	var response []byte
@@ -2188,6 +2163,23 @@ func (s *Session) WebhookExecute(webhookID, token string, wait bool, data *Webho
 	return
 }
 
+// WebhookExecute executes a webhook.
+// webhookID: The ID of a webhook.
+// token    : The auth token for the webhook
+// wait     : Waits for server confirmation of message send and ensures that the return struct is populated (it is nil otherwise)
+func (s *Session) WebhookExecute(webhookID, token string, wait bool, data *WebhookParams) (st *Message, err error) {
+	return s.webhookExecute(webhookID, token, wait, "", data)
+}
+
+// WebhookThreadExecute executes a webhook in a thread.
+// webhookID: The ID of a webhook.
+// token    : The auth token for the webhook
+// wait     : Waits for server confirmation of message send and ensures that the return struct is populated (it is nil otherwise)
+// threadID :	Sends a message to the specified thread within a webhook's channel. The thread will automatically be unarchived.
+func (s *Session) WebhookThreadExecute(webhookID, token string, wait bool, threadID string, data *WebhookParams) (st *Message, err error) {
+	return s.webhookExecute(webhookID, token, wait, threadID, data)
+}
+
 // WebhookMessage gets a webhook message.
 // webhookID : The ID of a webhook
 // token     : The auth token for the webhook
@@ -2335,82 +2327,222 @@ func (s *Session) MessageReactions(channelID, messageID, emojiID string, limit i
 }
 
 // ------------------------------------------------------------------------------------------------
-// Functions specific to user notes
+// Functions specific to threads
 // ------------------------------------------------------------------------------------------------
 
-// UserNoteSet sets the note for a specific user.
-func (s *Session) UserNoteSet(userID string, message string) (err error) {
-	data := struct {
-		Note string `json:"note"`
-	}{message}
+// MessageThreadStartComplex creates a new thread from an existing message.
+// channelID : Channel to create thread in
+// messageID : Message to start thread from
+// data : Parameters of the thread
+func (s *Session) MessageThreadStartComplex(channelID, messageID string, data *ThreadStart) (ch *Channel, err error) {
+	endpoint := EndpointChannelMessageThread(channelID, messageID)
+	var body []byte
+	body, err = s.RequestWithBucketID("POST", endpoint, data, endpoint)
+	if err != nil {
+		return
+	}
 
-	_, err = s.RequestWithBucketID("PUT", EndpointUserNotes(userID), data, EndpointUserNotes(""))
+	err = unmarshal(body, &ch)
 	return
 }
 
-// ------------------------------------------------------------------------------------------------
-// Functions specific to Discord Relationships (Friends list)
-// ------------------------------------------------------------------------------------------------
+// MessageThreadStart creates a new thread from an existing message.
+// channelID       : Channel to create thread in
+// messageID       : Message to start thread from
+// name            : Name of the thread
+// archiveDuration : Auto archive duration (in minutes)
+func (s *Session) MessageThreadStart(channelID, messageID string, name string, archiveDuration int) (ch *Channel, err error) {
+	return s.MessageThreadStartComplex(channelID, messageID, &ThreadStart{
+		Name:                name,
+		AutoArchiveDuration: archiveDuration,
+	})
+}
 
-// RelationshipsGet returns an array of all the relationships of the user.
-func (s *Session) RelationshipsGet() (r []*Relationship, err error) {
-	body, err := s.RequestWithBucketID("GET", EndpointRelationships(), nil, EndpointRelationships())
+// ThreadStartComplex creates a new thread.
+// channelID : Channel to create thread in
+// data : Parameters of the thread
+func (s *Session) ThreadStartComplex(channelID string, data *ThreadStart) (ch *Channel, err error) {
+	endpoint := EndpointChannelThreads(channelID)
+	var body []byte
+	body, err = s.RequestWithBucketID("POST", endpoint, data, endpoint)
 	if err != nil {
 		return
 	}
 
-	err = unmarshal(body, &r)
+	err = unmarshal(body, &ch)
 	return
 }
 
-// relationshipCreate creates a new relationship. (I.e. send or accept a friend request, block a user.)
-// relationshipType : 1 = friend, 2 = blocked, 3 = incoming friend req, 4 = sent friend req
-func (s *Session) relationshipCreate(userID string, relationshipType int) (err error) {
-	data := struct {
-		Type int `json:"type"`
-	}{relationshipType}
+// ThreadStart creates a new thread.
+// channelID       : Channel to create thread in
+// name            : Name of the thread
+// archiveDuration : Auto archive duration (in minutes)
+func (s *Session) ThreadStart(channelID, name string, typ ChannelType, archiveDuration int) (ch *Channel, err error) {
+	return s.ThreadStartComplex(channelID, &ThreadStart{
+		Name:                name,
+		Type:                typ,
+		AutoArchiveDuration: archiveDuration,
+	})
+}
+
+// ThreadJoin adds current user to a thread
+func (s *Session) ThreadJoin(id string) error {
+	endpoint := EndpointThreadMember(id, "@me")
+	_, err := s.RequestWithBucketID("PUT", endpoint, nil, endpoint)
+	return err
+}
+
+// ThreadLeave removes current user to a thread
+func (s *Session) ThreadLeave(id string) error {
+	endpoint := EndpointThreadMember(id, "@me")
+	_, err := s.RequestWithBucketID("DELETE", endpoint, nil, endpoint)
+	return err
+}
+
+// ThreadMemberAdd adds another member to a thread
+func (s *Session) ThreadMemberAdd(threadID, memberID string) error {
+	endpoint := EndpointThreadMember(threadID, memberID)
+	_, err := s.RequestWithBucketID("PUT", endpoint, nil, endpoint)
+	return err
+}
 
-	_, err = s.RequestWithBucketID("PUT", EndpointRelationship(userID), data, EndpointRelationships())
+// ThreadMemberRemove removes another member from a thread
+func (s *Session) ThreadMemberRemove(threadID, memberID string) error {
+	endpoint := EndpointThreadMember(threadID, memberID)
+	_, err := s.RequestWithBucketID("DELETE", endpoint, nil, endpoint)
+	return err
+}
+
+// ThreadMember returns thread member object for the specified member of a thread
+func (s *Session) ThreadMember(threadID, memberID string) (member *ThreadMember, err error) {
+	endpoint := EndpointThreadMember(threadID, memberID)
+	var body []byte
+	body, err = s.RequestWithBucketID("GET", endpoint, nil, endpoint)
+
+	if err != nil {
+		return
+	}
+
+	err = unmarshal(body, &member)
 	return
 }
 
-// RelationshipFriendRequestSend sends a friend request to a user.
-// userID: ID of the user.
-func (s *Session) RelationshipFriendRequestSend(userID string) (err error) {
-	err = s.relationshipCreate(userID, 4)
+// ThreadMembers returns all members of specified thread.
+func (s *Session) ThreadMembers(threadID string) (members []*ThreadMember, err error) {
+	var body []byte
+	body, err = s.RequestWithBucketID("GET", EndpointThreadMembers(threadID), nil, EndpointThreadMembers(threadID))
+
+	if err != nil {
+		return
+	}
+
+	err = unmarshal(body, &members)
 	return
 }
 
-// RelationshipFriendRequestAccept accepts a friend request from a user.
-// userID: ID of the user.
-func (s *Session) RelationshipFriendRequestAccept(userID string) (err error) {
-	err = s.relationshipCreate(userID, 1)
+// ThreadsActive returns all active threads for specified channel.
+func (s *Session) ThreadsActive(channelID string) (threads *ThreadsList, err error) {
+	var body []byte
+	body, err = s.RequestWithBucketID("GET", EndpointChannelActiveThreads(channelID), nil, EndpointChannelActiveThreads(channelID))
+	if err != nil {
+		return
+	}
+
+	err = unmarshal(body, &threads)
 	return
 }
 
-// RelationshipUserBlock blocks a user.
-// userID: ID of the user.
-func (s *Session) RelationshipUserBlock(userID string) (err error) {
-	err = s.relationshipCreate(userID, 2)
+// GuildThreadsActive returns all active threads for specified guild.
+func (s *Session) GuildThreadsActive(guildID string) (threads *ThreadsList, err error) {
+	var body []byte
+	body, err = s.RequestWithBucketID("GET", EndpointGuildActiveThreads(guildID), nil, EndpointGuildActiveThreads(guildID))
+	if err != nil {
+		return
+	}
+
+	err = unmarshal(body, &threads)
 	return
 }
 
-// RelationshipDelete removes the relationship with a user.
-// userID: ID of the user.
-func (s *Session) RelationshipDelete(userID string) (err error) {
-	_, err = s.RequestWithBucketID("DELETE", EndpointRelationship(userID), nil, EndpointRelationships())
+// ThreadsArchived returns archived threads for specified channel.
+// before : If specified returns only threads before the timestamp
+// limit  : Optional maximum amount of threads to return.
+func (s *Session) ThreadsArchived(channelID string, before *time.Time, limit int) (threads *ThreadsList, err error) {
+	endpoint := EndpointChannelPublicArchivedThreads(channelID)
+	v := url.Values{}
+	if before != nil {
+		v.Set("before", before.Format(time.RFC3339))
+	}
+
+	if limit > 0 {
+		v.Set("limit", strconv.Itoa(limit))
+	}
+
+	if len(v) > 0 {
+		endpoint += "?" + v.Encode()
+	}
+
+	var body []byte
+	body, err = s.RequestWithBucketID("GET", endpoint, nil, endpoint)
+	if err != nil {
+		return
+	}
+
+	err = unmarshal(body, &threads)
 	return
 }
 
-// RelationshipsMutualGet returns an array of all the users both @me and the given user is friends with.
-// userID: ID of the user.
-func (s *Session) RelationshipsMutualGet(userID string) (mf []*User, err error) {
-	body, err := s.RequestWithBucketID("GET", EndpointRelationshipsMutual(userID), nil, EndpointRelationshipsMutual(userID))
+// ThreadsPrivateArchived returns archived private threads for specified channel.
+// before : If specified returns only threads before the timestamp
+// limit  : Optional maximum amount of threads to return.
+func (s *Session) ThreadsPrivateArchived(channelID string, before *time.Time, limit int) (threads *ThreadsList, err error) {
+	endpoint := EndpointChannelPrivateArchivedThreads(channelID)
+	v := url.Values{}
+	if before != nil {
+		v.Set("before", before.Format(time.RFC3339))
+	}
+
+	if limit > 0 {
+		v.Set("limit", strconv.Itoa(limit))
+	}
+
+	if len(v) > 0 {
+		endpoint += "?" + v.Encode()
+	}
+	var body []byte
+	body, err = s.RequestWithBucketID("GET", endpoint, nil, endpoint)
 	if err != nil {
 		return
 	}
 
-	err = unmarshal(body, &mf)
+	err = unmarshal(body, &threads)
+	return
+}
+
+// ThreadsPrivateJoinedArchived returns archived joined private threads for specified channel.
+// before : If specified returns only threads before the timestamp
+// limit  : Optional maximum amount of threads to return.
+func (s *Session) ThreadsPrivateJoinedArchived(channelID string, before *time.Time, limit int) (threads *ThreadsList, err error) {
+	endpoint := EndpointChannelJoinedPrivateArchivedThreads(channelID)
+	v := url.Values{}
+	if before != nil {
+		v.Set("before", before.Format(time.RFC3339))
+	}
+
+	if limit > 0 {
+		v.Set("limit", strconv.Itoa(limit))
+	}
+
+	if len(v) > 0 {
+		endpoint += "?" + v.Encode()
+	}
+	var body []byte
+	body, err = s.RequestWithBucketID("GET", endpoint, nil, endpoint)
+	if err != nil {
+		return
+	}
+
+	err = unmarshal(body, &threads)
 	return
 }
 
@@ -2532,8 +2664,63 @@ func (s *Session) ApplicationCommands(appID, guildID string) (cmd []*Application
 	return
 }
 
+// GuildApplicationCommandsPermissions returns permissions for application commands in a guild.
+// appID       : The application ID
+// guildID     : Guild ID to retrieve application commands permissions for.
+func (s *Session) GuildApplicationCommandsPermissions(appID, guildID string) (permissions []*GuildApplicationCommandPermissions, err error) {
+	endpoint := EndpointApplicationCommandsGuildPermissions(appID, guildID)
+
+	var body []byte
+	body, err = s.RequestWithBucketID("GET", endpoint, nil, endpoint)
+	if err != nil {
+		return
+	}
+
+	err = unmarshal(body, &permissions)
+	return
+}
+
+// ApplicationCommandPermissions returns all permissions of an application command
+// appID       : The Application ID
+// guildID     : The guild ID containing the application command
+// cmdID       : The command ID to retrieve the permissions of
+func (s *Session) ApplicationCommandPermissions(appID, guildID, cmdID string) (permissions *GuildApplicationCommandPermissions, err error) {
+	endpoint := EndpointApplicationCommandPermissions(appID, guildID, cmdID)
+
+	var body []byte
+	body, err = s.RequestWithBucketID("GET", endpoint, nil, endpoint)
+	if err != nil {
+		return
+	}
+
+	err = unmarshal(body, &permissions)
+	return
+}
+
+// ApplicationCommandPermissionsEdit edits the permissions of an application command
+// appID       : The Application ID
+// guildID     : The guild ID containing the application command
+// cmdID       : The command ID to edit the permissions of
+// permissions : An object containing a list of permissions for the application command
+func (s *Session) ApplicationCommandPermissionsEdit(appID, guildID, cmdID string, permissions *ApplicationCommandPermissionsList) (err error) {
+	endpoint := EndpointApplicationCommandPermissions(appID, guildID, cmdID)
+
+	_, err = s.RequestWithBucketID("PUT", endpoint, permissions, endpoint)
+	return
+}
+
+// ApplicationCommandPermissionsBatchEdit edits the permissions of a batch of commands
+// appID       : The Application ID
+// guildID     : The guild ID to batch edit commands of
+// permissions : A list of permissions paired with a command ID, guild ID, and application ID per application command
+func (s *Session) ApplicationCommandPermissionsBatchEdit(appID, guildID string, permissions []*GuildApplicationCommandPermissions) (err error) {
+	endpoint := EndpointApplicationCommandsGuildPermissions(appID, guildID)
+
+	_, err = s.RequestWithBucketID("PUT", endpoint, permissions, endpoint)
+	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) (err error) {
@@ -2603,3 +2790,115 @@ func (s *Session) FollowupMessageEdit(appID string, interaction *Interaction, me
 func (s *Session) FollowupMessageDelete(appID string, interaction *Interaction, messageID string) error {
 	return s.WebhookMessageDelete(appID, interaction.Token, messageID)
 }
+
+// ------------------------------------------------------------------------------------------------
+// Functions specific to guilds scheduled events
+// ------------------------------------------------------------------------------------------------
+
+// GuildScheduledEvents returns an array of GuildScheduledEvent for a guild
+// guildID        : The ID of a Guild
+// userCount      : Whether to include the user count in the response
+func (s *Session) GuildScheduledEvents(guildID string, userCount bool) (st []*GuildScheduledEvent, err error) {
+	uri := EndpointGuildScheduledEvents(guildID)
+	if userCount {
+		uri += "?with_user_count=true"
+	}
+
+	body, err := s.RequestWithBucketID("GET", uri, nil, EndpointGuildScheduledEvents(guildID))
+	if err != nil {
+		return
+	}
+
+	err = unmarshal(body, &st)
+	return
+}
+
+// GuildScheduledEvent returns a specific GuildScheduledEvent in a guild
+// guildID        : The ID of a Guild
+// eventID        : The ID of the event
+// userCount      : Whether to include the user count in the response
+func (s *Session) GuildScheduledEvent(guildID, eventID string, userCount bool) (st *GuildScheduledEvent, err error) {
+	uri := EndpointGuildScheduledEvent(guildID, eventID)
+	if userCount {
+		uri += "?with_user_count=true"
+	}
+
+	body, err := s.RequestWithBucketID("GET", uri, nil, EndpointGuildScheduledEvent(guildID, eventID))
+	if err != nil {
+		return
+	}
+
+	err = unmarshal(body, &st)
+	return
+}
+
+// GuildScheduledEventCreate creates a GuildScheduledEvent for a guild and returns it
+// guildID   : The ID of a Guild
+// eventID   : The ID of the event
+func (s *Session) GuildScheduledEventCreate(guildID string, event *GuildScheduledEventParams) (st *GuildScheduledEvent, err error) {
+	body, err := s.RequestWithBucketID("POST", EndpointGuildScheduledEvents(guildID), event, EndpointGuildScheduledEvents(guildID))
+	if err != nil {
+		return
+	}
+
+	err = unmarshal(body, &st)
+	return
+}
+
+// GuildScheduledEventEdit updates a specific event for a guild and returns it.
+// guildID   : The ID of a Guild
+// eventID   : The ID of the event
+func (s *Session) GuildScheduledEventEdit(guildID, eventID string, event *GuildScheduledEventParams) (st *GuildScheduledEvent, err error) {
+	body, err := s.RequestWithBucketID("PATCH", EndpointGuildScheduledEvent(guildID, eventID), event, EndpointGuildScheduledEvent(guildID, eventID))
+	if err != nil {
+		return
+	}
+
+	err = unmarshal(body, &st)
+	return
+}
+
+// GuildScheduledEventDelete deletes a specific GuildScheduledEvent in a guild
+// guildID   : The ID of a Guild
+// eventID   : The ID of the event
+func (s *Session) GuildScheduledEventDelete(guildID, eventID string) (err error) {
+	_, err = s.RequestWithBucketID("DELETE", EndpointGuildScheduledEvent(guildID, eventID), nil, EndpointGuildScheduledEvent(guildID, eventID))
+	return
+}
+
+// GuildScheduledEventUsers returns an array of GuildScheduledEventUser for a particular event in a guild
+// guildID    : The ID of a Guild
+// eventID    : The ID of the event
+// limit      : The maximum number of users to return (Max 100)
+// withMember : Whether to include the member object in the response
+// beforeID   : If is not empty all returned users entries will be before the given ID
+// afterID    : If is not empty all returned users entries will be after the given ID
+func (s *Session) GuildScheduledEventUsers(guildID, eventID string, limit int, withMember bool, beforeID, afterID string) (st []*GuildScheduledEventUser, err error) {
+	uri := EndpointGuildScheduledEventUsers(guildID, eventID)
+
+	queryParams := url.Values{}
+	if withMember {
+		queryParams.Set("with_member", "true")
+	}
+	if limit > 0 {
+		queryParams.Set("limit", strconv.Itoa(limit))
+	}
+	if beforeID != "" {
+		queryParams.Set("before", beforeID)
+	}
+	if afterID != "" {
+		queryParams.Set("after", afterID)
+	}
+
+	if len(queryParams) > 0 {
+		uri += "?" + queryParams.Encode()
+	}
+
+	body, err := s.RequestWithBucketID("GET", uri, nil, EndpointGuildScheduledEventUsers(guildID, eventID))
+	if err != nil {
+		return
+	}
+
+	err = unmarshal(body, &st)
+	return
+}

+ 0 - 58
restapi_test.go

@@ -99,17 +99,6 @@ func TestUserChannelCreate(t *testing.T) {
 	// TODO make sure the channel was added
 }
 
-func TestUserChannels(t *testing.T) {
-	if dg == nil {
-		t.Skip("Cannot TestUserChannels, dg not set.")
-	}
-
-	_, err := dg.UserChannels()
-	if err != nil {
-		t.Errorf(err.Error())
-	}
-}
-
 func TestUserGuilds(t *testing.T) {
 	if dg == nil {
 		t.Skip("Cannot TestUserGuilds, dg not set.")
@@ -121,41 +110,6 @@ func TestUserGuilds(t *testing.T) {
 	}
 }
 
-func TestUserSettings(t *testing.T) {
-	if dg == nil {
-		t.Skip("Cannot TestUserSettings, dg not set.")
-	}
-
-	_, err := dg.UserSettings()
-	if err != nil {
-		t.Errorf(err.Error())
-	}
-}
-
-func TestUserUpdateStatus(t *testing.T) {
-	if dg == nil {
-		t.Skip("Cannot TestUserSettings, dg not set.")
-	}
-
-	_, err := dg.UserUpdateStatus(StatusDoNotDisturb)
-	if err != nil {
-		t.Errorf(err.Error())
-	}
-}
-
-// TestLogout tests the Logout() function. This should not return an error.
-func TestLogout(t *testing.T) {
-
-	if dg == nil {
-		t.Skip("Cannot TestLogout, dg not set.")
-	}
-
-	err := dg.Logout()
-	if err != nil {
-		t.Errorf("Logout() returned error: %+v", err)
-	}
-}
-
 func TestGateway(t *testing.T) {
 
 	if dg == nil {
@@ -178,18 +132,6 @@ func TestGatewayBot(t *testing.T) {
 	}
 }
 
-func TestVoiceICE(t *testing.T) {
-
-	if dg == nil {
-		t.Skip("Skipping, dg not set.")
-	}
-
-	_, err := dg.VoiceICE()
-	if err != nil {
-		t.Errorf("VoiceICE() returned error: %+v", err)
-	}
-}
-
 func TestVoiceRegions(t *testing.T) {
 
 	if dg == nil {

+ 221 - 58
state.go

@@ -38,13 +38,15 @@ type State struct {
 	Ready
 
 	// MaxMessageCount represents how many messages per channel the state will store.
-	MaxMessageCount int
-	TrackChannels   bool
-	TrackEmojis     bool
-	TrackMembers    bool
-	TrackRoles      bool
-	TrackVoice      bool
-	TrackPresences  bool
+	MaxMessageCount    int
+	TrackChannels      bool
+	TrackThreads       bool
+	TrackEmojis        bool
+	TrackMembers       bool
+	TrackThreadMembers bool
+	TrackRoles         bool
+	TrackVoice         bool
+	TrackPresences     bool
 
 	guildMap   map[string]*Guild
 	channelMap map[string]*Channel
@@ -58,15 +60,17 @@ func NewState() *State {
 			PrivateChannels: []*Channel{},
 			Guilds:          []*Guild{},
 		},
-		TrackChannels:  true,
-		TrackEmojis:    true,
-		TrackMembers:   true,
-		TrackRoles:     true,
-		TrackVoice:     true,
-		TrackPresences: true,
-		guildMap:       make(map[string]*Guild),
-		channelMap:     make(map[string]*Channel),
-		memberMap:      make(map[string]map[string]*Member),
+		TrackChannels:      true,
+		TrackThreads:       true,
+		TrackEmojis:        true,
+		TrackMembers:       true,
+		TrackThreadMembers: true,
+		TrackRoles:         true,
+		TrackVoice:         true,
+		TrackPresences:     true,
+		guildMap:           make(map[string]*Guild),
+		channelMap:         make(map[string]*Channel),
+		memberMap:          make(map[string]map[string]*Member),
 	}
 }
 
@@ -93,6 +97,11 @@ func (s *State) GuildAdd(guild *Guild) error {
 		s.channelMap[c.ID] = c
 	}
 
+	// Add all the threads to the state in case of thread sync list.
+	for _, t := range guild.Threads {
+		s.channelMap[t.ID] = t
+	}
+
 	// If this guild contains a new member slice, we must regenerate the member map so the pointers stay valid
 	if guild.Members != nil {
 		s.createMemberMap(guild)
@@ -122,6 +131,9 @@ func (s *State) GuildAdd(guild *Guild) error {
 		if guild.Channels == nil {
 			guild.Channels = g.Channels
 		}
+		if guild.Threads == nil {
+			guild.Threads = g.Threads
+		}
 		if guild.VoiceStates == nil {
 			guild.VoiceStates = g.VoiceStates
 		}
@@ -180,21 +192,12 @@ func (s *State) Guild(guildID string) (*Guild, error) {
 	return nil, ErrStateNotFound
 }
 
-// PresenceAdd adds a presence to the current world state, or
-// updates it if it already exists.
-func (s *State) PresenceAdd(guildID string, presence *Presence) error {
-	if s == nil {
-		return ErrNilState
-	}
-
-	guild, err := s.Guild(guildID)
-	if err != nil {
-		return err
+func (s *State) presenceAdd(guildID string, presence *Presence) error {
+	guild, ok := s.guildMap[guildID]
+	if !ok {
+		return ErrStateNotFound
 	}
 
-	s.Lock()
-	defer s.Unlock()
-
 	for i, p := range guild.Presences {
 		if p.User.ID == presence.User.ID {
 			//guild.Presences[i] = presence
@@ -233,6 +236,19 @@ func (s *State) PresenceAdd(guildID string, presence *Presence) error {
 	return nil
 }
 
+// PresenceAdd adds a presence to the current world state, or
+// updates it if it already exists.
+func (s *State) PresenceAdd(guildID string, presence *Presence) error {
+	if s == nil {
+		return ErrNilState
+	}
+
+	s.Lock()
+	defer s.Unlock()
+
+	return s.presenceAdd(guildID, presence)
+}
+
 // PresenceRemove removes a presence from the current world state.
 func (s *State) PresenceRemove(guildID string, presence *Presence) error {
 	if s == nil {
@@ -279,21 +295,12 @@ func (s *State) Presence(guildID, userID string) (*Presence, error) {
 
 // TODO: Consider moving Guild state update methods onto *Guild.
 
-// MemberAdd adds a member to the current world state, or
-// updates it if it already exists.
-func (s *State) MemberAdd(member *Member) error {
-	if s == nil {
-		return ErrNilState
-	}
-
-	guild, err := s.Guild(member.GuildID)
-	if err != nil {
-		return err
+func (s *State) memberAdd(member *Member) error {
+	guild, ok := s.guildMap[member.GuildID]
+	if !ok {
+		return ErrStateNotFound
 	}
 
-	s.Lock()
-	defer s.Unlock()
-
 	members, ok := s.memberMap[member.GuildID]
 	if !ok {
 		return ErrStateNotFound
@@ -306,15 +313,27 @@ func (s *State) MemberAdd(member *Member) error {
 	} else {
 		// We are about to replace `m` in the state with `member`, but first we need to
 		// make sure we preserve any fields that the `member` doesn't contain from `m`.
-		if member.JoinedAt == "" {
+		if member.JoinedAt.IsZero() {
 			member.JoinedAt = m.JoinedAt
 		}
 		*m = *member
 	}
-
 	return nil
 }
 
+// MemberAdd adds a member to the current world state, or
+// updates it if it already exists.
+func (s *State) MemberAdd(member *Member) error {
+	if s == nil {
+		return ErrNilState
+	}
+
+	s.Lock()
+	defer s.Unlock()
+
+	return s.memberAdd(member)
+}
+
 // MemberRemove removes a member from current world state.
 func (s *State) MemberRemove(member *Member) error {
 	if s == nil {
@@ -465,6 +484,9 @@ func (s *State) ChannelAdd(channel *Channel) error {
 		if channel.PermissionOverwrites == nil {
 			channel.PermissionOverwrites = c.PermissionOverwrites
 		}
+		if channel.ThreadMetadata == nil {
+			channel.ThreadMetadata = c.ThreadMetadata
+		}
 
 		*c = *channel
 		return nil
@@ -472,12 +494,18 @@ func (s *State) ChannelAdd(channel *Channel) error {
 
 	if channel.Type == ChannelTypeDM || channel.Type == ChannelTypeGroupDM {
 		s.PrivateChannels = append(s.PrivateChannels, channel)
-	} else {
-		guild, ok := s.guildMap[channel.GuildID]
-		if !ok {
-			return ErrStateNotFound
-		}
+		s.channelMap[channel.ID] = channel
+		return nil
+	}
+
+	guild, ok := s.guildMap[channel.GuildID]
+	if !ok {
+		return ErrStateNotFound
+	}
 
+	if channel.IsThread() {
+		guild.Threads = append(guild.Threads, channel)
+	} else {
 		guild.Channels = append(guild.Channels, channel)
 	}
 
@@ -507,15 +535,26 @@ func (s *State) ChannelRemove(channel *Channel) error {
 				break
 			}
 		}
-	} else {
-		guild, err := s.Guild(channel.GuildID)
-		if err != nil {
-			return err
-		}
+		delete(s.channelMap, channel.ID)
+		return nil
+	}
 
-		s.Lock()
-		defer s.Unlock()
+	guild, err := s.Guild(channel.GuildID)
+	if err != nil {
+		return err
+	}
 
+	s.Lock()
+	defer s.Unlock()
+
+	if channel.IsThread() {
+		for i, t := range guild.Threads {
+			if t.ID == channel.ID {
+				guild.Threads = append(guild.Threads[:i], guild.Threads[i+1:]...)
+				break
+			}
+		}
+	} else {
 		for i, c := range guild.Channels {
 			if c.ID == channel.ID {
 				guild.Channels = append(guild.Channels[:i], guild.Channels[i+1:]...)
@@ -529,6 +568,99 @@ func (s *State) ChannelRemove(channel *Channel) error {
 	return nil
 }
 
+// ThreadListSync syncs guild threads with provided ones.
+func (s *State) ThreadListSync(tls *ThreadListSync) error {
+	guild, err := s.Guild(tls.GuildID)
+	if err != nil {
+		return err
+	}
+
+	s.Lock()
+	defer s.Unlock()
+
+	// This algorithm filters out archived or
+	// threads which are children of channels in channelIDs
+	// and then it adds all synced threads to guild threads and cache
+	index := 0
+outer:
+	for _, t := range guild.Threads {
+		if !t.ThreadMetadata.Archived && tls.ChannelIDs != nil {
+			for _, v := range tls.ChannelIDs {
+				if t.ParentID == v {
+					delete(s.channelMap, t.ID)
+					continue outer
+				}
+			}
+			guild.Threads[index] = t
+			index++
+		} else {
+			delete(s.channelMap, t.ID)
+		}
+	}
+	guild.Threads = guild.Threads[:index]
+	for _, t := range tls.Threads {
+		s.channelMap[t.ID] = t
+		guild.Threads = append(guild.Threads, t)
+	}
+
+	for _, m := range tls.Members {
+		if c, ok := s.channelMap[m.ID]; ok {
+			c.Member = m
+		}
+	}
+
+	return nil
+}
+
+// ThreadMembersUpdate updates thread members list
+func (s *State) ThreadMembersUpdate(tmu *ThreadMembersUpdate) error {
+	thread, err := s.Channel(tmu.ID)
+	if err != nil {
+		return err
+	}
+	s.Lock()
+	defer s.Unlock()
+
+	for idx, member := range thread.Members {
+		for _, removedMember := range tmu.RemovedMembers {
+			if member.ID == removedMember {
+				thread.Members = append(thread.Members[:idx], thread.Members[idx+1:]...)
+				break
+			}
+		}
+	}
+
+	for _, addedMember := range tmu.AddedMembers {
+		thread.Members = append(thread.Members, addedMember.ThreadMember)
+		if addedMember.Member != nil {
+			err = s.memberAdd(addedMember.Member)
+			if err != nil {
+				return err
+			}
+		}
+		if addedMember.Presence != nil {
+			err = s.presenceAdd(tmu.GuildID, addedMember.Presence)
+			if err != nil {
+				return err
+			}
+		}
+	}
+	thread.MemberCount = tmu.MemberCount
+
+	return nil
+}
+
+// ThreadMemberUpdate sets or updates member data for the current user.
+func (s *State) ThreadMemberUpdate(mu *ThreadMemberUpdate) error {
+	thread, err := s.Channel(mu.ID)
+	if err != nil {
+		return err
+	}
+
+	thread.Member = mu.ThreadMember
+	return nil
+}
+
 // GuildChannel gets a channel by ID from a guild.
 // This method is Deprecated, use Channel(channelID)
 func (s *State) GuildChannel(guildID, channelID string) (*Channel, error) {
@@ -637,7 +769,7 @@ func (s *State) MessageAdd(message *Message) error {
 			if message.Content != "" {
 				m.Content = message.Content
 			}
-			if message.EditedTimestamp != "" {
+			if message.EditedTimestamp != nil {
 				m.EditedTimestamp = message.EditedTimestamp
 			}
 			if message.Mentions != nil {
@@ -649,7 +781,7 @@ func (s *State) MessageAdd(message *Message) error {
 			if message.Attachments != nil {
 				m.Attachments = message.Attachments
 			}
-			if message.Timestamp != "" {
+			if !message.Timestamp.IsZero() {
 				m.Timestamp = message.Timestamp
 			}
 			if message.Author != nil {
@@ -668,6 +800,7 @@ func (s *State) MessageAdd(message *Message) error {
 	if len(c.Messages) > s.MaxMessageCount {
 		c.Messages = c.Messages[len(c.Messages)-s.MaxMessageCount:]
 	}
+
 	return nil
 }
 
@@ -693,6 +826,7 @@ func (s *State) messageRemoveByID(channelID, messageID string) error {
 	for i, m := range c.Messages {
 		if m.ID == messageID {
 			c.Messages = append(c.Messages[:i], c.Messages[i+1:]...)
+
 			return nil
 		}
 	}
@@ -913,6 +1047,35 @@ func (s *State) OnInterface(se *Session, i interface{}) (err error) {
 		if s.TrackChannels {
 			err = s.ChannelRemove(t.Channel)
 		}
+	case *ThreadCreate:
+		if s.TrackThreads {
+			err = s.ChannelAdd(t.Channel)
+		}
+	case *ThreadUpdate:
+		if s.TrackThreads {
+			old, err := s.Channel(t.ID)
+			if err == nil {
+				oldCopy := *old
+				t.BeforeUpdate = &oldCopy
+			}
+			err = s.ChannelAdd(t.Channel)
+		}
+	case *ThreadDelete:
+		if s.TrackThreads {
+			err = s.ChannelRemove(t.Channel)
+		}
+	case *ThreadMemberUpdate:
+		if s.TrackThreads {
+			err = s.ThreadMemberUpdate(t)
+		}
+	case *ThreadMembersUpdate:
+		if s.TrackThreadMembers {
+			err = s.ThreadMembersUpdate(t)
+		}
+	case *ThreadListSync:
+		if s.TrackThreads {
+			err = s.ThreadListSync(t)
+		}
 	case *MessageCreate:
 		if s.MaxMessageCount != 0 {
 			err = s.MessageAdd(t.Message)

File diff suppressed because it is too large
+ 785 - 170
structs.go


+ 0 - 57
types.go

@@ -1,57 +0,0 @@
-// Discordgo - Discord bindings for Go
-// Available at https://github.com/bwmarrin/discordgo
-
-// Copyright 2015-2016 Bruce Marriner <bruce@sqls.net>.  All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-// This file contains custom types, currently only a timestamp wrapper.
-
-package discordgo
-
-import (
-	"encoding/json"
-	"net/http"
-	"time"
-)
-
-// Timestamp stores a timestamp, as sent by the Discord API.
-type Timestamp string
-
-// Parse parses a timestamp string into a time.Time object.
-// The only time this can fail is if Discord changes their timestamp format.
-func (t Timestamp) Parse() (time.Time, error) {
-	return time.Parse(time.RFC3339, string(t))
-}
-
-// RESTError stores error information about a request with a bad response code.
-// Message is not always present, there are cases where api calls can fail
-// without returning a json message.
-type RESTError struct {
-	Request      *http.Request
-	Response     *http.Response
-	ResponseBody []byte
-
-	Message *APIErrorMessage // Message may be nil.
-}
-
-func newRestError(req *http.Request, resp *http.Response, body []byte) *RESTError {
-	restErr := &RESTError{
-		Request:      req,
-		Response:     resp,
-		ResponseBody: body,
-	}
-
-	// Attempt to decode the error and assume no message was provided if it fails
-	var msg *APIErrorMessage
-	err := json.Unmarshal(body, &msg)
-	if err == nil {
-		restErr.Message = msg
-	}
-
-	return restErr
-}
-
-func (r RESTError) Error() string {
-	return "HTTP " + r.Response.Status + ", " + string(r.ResponseBody)
-}

+ 0 - 24
types_test.go

@@ -1,24 +0,0 @@
-package discordgo
-
-import (
-	"testing"
-	"time"
-)
-
-func TestTimestampParse(t *testing.T) {
-	ts, err := Timestamp("2016-03-24T23:15:59.605000+00:00").Parse()
-	if err != nil {
-		t.Fatal(err)
-	}
-	if ts.Year() != 2016 || ts.Month() != time.March || ts.Day() != 24 {
-		t.Error("Incorrect date")
-	}
-	if ts.Hour() != 23 || ts.Minute() != 15 || ts.Second() != 59 {
-		t.Error("Incorrect time")
-	}
-
-	_, offset := ts.Zone()
-	if offset != 0 {
-		t.Error("Incorrect timezone")
-	}
-}

+ 15 - 15
user.go

@@ -1,7 +1,5 @@
 package discordgo
 
-import "strings"
-
 // UserFlags is the flags of "user" (see UserFlags* consts)
 // https://discord.com/developers/docs/resources/user#user-object-user-flags
 type UserFlags int
@@ -56,6 +54,12 @@ type User struct {
 	// Whether the user has multi-factor authentication enabled.
 	MFAEnabled bool `json:"mfa_enabled"`
 
+	// The hash of the user's banner image.
+	Banner string `json:"banner"`
+
+	// User's banner color, encoded as an integer representation of hexadecimal color code
+	AccentColor int `json:"accent_color"`
+
 	// Whether the user is a bot.
 	Bot bool `json:"bot"`
 
@@ -91,17 +95,13 @@ func (u *User) Mention() string {
 //             if size is an empty string, no size parameter will
 //             be added to the URL.
 func (u *User) AvatarURL(size string) string {
-	var URL string
-	if u.Avatar == "" {
-		URL = EndpointDefaultUserAvatar(u.Discriminator)
-	} else if strings.HasPrefix(u.Avatar, "a_") {
-		URL = EndpointUserAvatarAnimated(u.ID, u.Avatar)
-	} else {
-		URL = EndpointUserAvatar(u.ID, u.Avatar)
-	}
-
-	if size != "" {
-		return URL + "?size=" + size
-	}
-	return URL
+	return avatarURL(u.Avatar, EndpointDefaultUserAvatar(u.Discriminator),
+		EndpointUserAvatar(u.ID, u.Avatar), EndpointUserAvatarAnimated(u.ID, u.Avatar), size)
+}
+
+// BannerURL returns the URL of the users's banner image.
+//    size:    The size of the desired banner image as a power of two
+//             Image size can be any power of two between 16 and 4096.
+func (u *User) BannerURL(size string) string {
+	return bannerURL(u.Banner, EndpointUserBanner(u.ID, u.Banner), EndpointUserBannerAnimated(u.ID, u.Banner), size)
 }

+ 33 - 0
util.go

@@ -8,6 +8,7 @@ import (
 	"mime/multipart"
 	"net/textproto"
 	"strconv"
+	"strings"
 	"time"
 )
 
@@ -75,3 +76,35 @@ func MultipartBodyWithJSON(data interface{}, files []*File) (requestContentType
 
 	return bodywriter.FormDataContentType(), body.Bytes(), nil
 }
+
+func avatarURL(avatarHash, defaultAvatarURL, staticAvatarURL, animatedAvatarURL, size string) string {
+	var URL string
+	if avatarHash == "" {
+		URL = defaultAvatarURL
+	} else if strings.HasPrefix(avatarHash, "a_") {
+		URL = animatedAvatarURL
+	} else {
+		URL = staticAvatarURL
+	}
+
+	if size != "" {
+		return URL + "?size=" + size
+	}
+	return URL
+}
+
+func bannerURL(bannerHash, staticBannerURL, animatedBannerURL, size string) string {
+	var URL string
+	if bannerHash == "" {
+		return ""
+	} else if strings.HasPrefix(bannerHash, "a_") {
+		URL = animatedBannerURL
+	} else {
+		URL = staticBannerURL
+	}
+
+	if size != "" {
+		return URL + "?size=" + size
+	}
+	return URL
+}