Browse Source

Release version v0.21.0

Carson Hoffman 4 years ago
parent
commit
1294b313b9
21 changed files with 350 additions and 104 deletions
  1. 2 0
      .gitignore
  2. 2 1
      .travis.yml
  3. 3 3
      README.md
  4. 23 9
      discord.go
  5. 3 3
      docs/GettingStarted.md
  6. 3 3
      docs/index.md
  7. 2 2
      endpoints.go
  8. 1 1
      event.go
  9. 6 2
      events.go
  10. 1 1
      examples/appmaker/README.md
  11. 1 1
      examples/pingpong/main.go
  12. 2 0
      go.mod
  13. 1 1
      logging.go
  14. 45 7
      message.go
  15. 15 12
      restapi.go
  16. 13 0
      state.go
  17. 122 10
      structs.go
  18. 1 1
      util.go
  19. 21 0
      util_test.go
  20. 19 0
      voice.go
  21. 64 47
      wsapi.go

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
+# IDE-specific metadata
+.idea/

+ 2 - 1
.travis.yml

@@ -1,8 +1,9 @@
 language: go
 language: go
 go:
 go:
-    - 1.9.x
     - 1.10.x
     - 1.10.x
     - 1.11.x
     - 1.11.x
+    - 1.12.x
+    - 1.13.x
 install:
 install:
     - go get github.com/bwmarrin/discordgo
     - go get github.com/bwmarrin/discordgo
     - go get -v .
     - go get -v .

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


+ 23 - 9
discord.go

@@ -17,11 +17,12 @@ import (
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
 	"net/http"
 	"net/http"
+	"runtime"
 	"time"
 	"time"
 )
 )
 
 
 // VERSION of DiscordGo, follows Semantic Versioning. (http://semver.org/)
 // VERSION of DiscordGo, follows Semantic Versioning. (http://semver.org/)
-const VERSION = "0.20.3"
+const VERSION = "0.21.0"
 
 
 // ErrMFA will be risen by New when the user has 2FA.
 // ErrMFA will be risen by New when the user has 2FA.
 var ErrMFA = errors.New("account has 2FA enabled")
 var ErrMFA = errors.New("account has 2FA enabled")
@@ -30,10 +31,13 @@ var ErrMFA = errors.New("account has 2FA enabled")
 // tasks if given enough information to do so.  Currently you can pass zero
 // tasks if given enough information to do so.  Currently you can pass zero
 // arguments and it will return an empty Discord session.
 // arguments and it will return an empty Discord session.
 // There are 3 ways to call New:
 // There are 3 ways to call New:
-//     With a single auth token - All requests will use the token blindly,
+//     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.
 //         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 `
 //         IF THE TOKEN IS FOR A BOT, IT MUST BE PREFIXED WITH `BOT `
 //         eg: `"Bot <token>"`
 //         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
 //     With an email and password - Discord will sign in with the provided
 //         credentials.
 //         credentials.
 //     With an email, password and auth token - Discord will verify the auth
 //     With an email, password and auth token - Discord will verify the auth
@@ -63,6 +67,15 @@ func New(args ...interface{}) (s *Session, err error) {
 		LastHeartbeatAck:       time.Now().UTC(),
 		LastHeartbeatAck:       time.Now().UTC(),
 	}
 	}
 
 
+	// Initilize the Identify Package with defaults
+	// 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 no arguments are passed return the empty Session interface.
 	if args == nil {
 	if args == nil {
 		return
 		return
@@ -94,7 +107,8 @@ func New(args ...interface{}) (s *Session, err error) {
 
 
 			// If third string exists, it must be an auth token.
 			// If third string exists, it must be an auth token.
 			if len(v) > 2 {
 			if len(v) > 2 {
-				s.Token = v[2]
+				s.Identify.Token = v[2]
+				s.Token = v[2] // TODO: Remove, Deprecated - Kept for backwards compatibility.
 			}
 			}
 
 
 		case string:
 		case string:
@@ -107,7 +121,8 @@ func New(args ...interface{}) (s *Session, err error) {
 			} else if pass == "" {
 			} else if pass == "" {
 				pass = v
 				pass = v
 			} else if s.Token == "" {
 			} else if s.Token == "" {
-				s.Token = v
+				s.Identify.Token = v
+				s.Token = v // TODO: Remove, Deprecated - Kept for backwards compatibility.
 			} else {
 			} else {
 				err = fmt.Errorf("too many string parameters provided")
 				err = fmt.Errorf("too many string parameters provided")
 				return
 				return
@@ -127,10 +142,12 @@ func New(args ...interface{}) (s *Session, err error) {
 	// Discord will verify it for free, or log the user in if it is
 	// Discord will verify it for free, or log the user in if it is
 	// invalid.
 	// invalid.
 	if pass == "" {
 	if pass == "" {
-		s.Token = auth
+		s.Identify.Token = auth
+		s.Token = auth // TODO: Remove, Deprecated - Kept for backwards compatibility.
 	} else {
 	} else {
 		err = s.Login(auth, pass)
 		err = s.Login(auth, pass)
-		if err != nil || s.Token == "" {
+		// TODO: Remove last s.Token part, Deprecated - Kept for backwards compatibility.
+		if err != nil || s.Identify.Token == "" || s.Token == "" {
 			if s.MFA {
 			if s.MFA {
 				err = ErrMFA
 				err = ErrMFA
 			} else {
 			} else {
@@ -140,8 +157,5 @@ func New(args ...interface{}) (s *Session, err error) {
 		}
 		}
 	}
 	}
 
 
-	// The Session is now able to have RestAPI methods called on it.
-	// It is recommended that you now call Open() so that events will trigger.
-
 	return
 	return
 }
 }

+ 3 - 3
docs/GettingStarted.md

@@ -58,7 +58,7 @@ support multi-server voice connections and some other features that are
 exclusive to Bot accounts only.
 exclusive to Bot accounts only.
 
 
 To create a new user account (if you have not done so already) visit the 
 To create a new user account (if you have not done so already) visit the 
-[Discord](https://discordapp.com/) website and click on the 
+[Discord](https://discord.com/) website and click on the 
 **Try Discord Now, It's Free** button then follow the steps to setup your
 **Try Discord Now, It's Free** button then follow the steps to setup your
 new account.
 new account.
 
 
@@ -77,12 +77,12 @@ have access to some user client specific features however they gain access to
 many Bot specific features.
 many Bot specific features.
 
 
 To create a new bot account first create yourself a normal user account on 
 To create a new bot account first create yourself a normal user account on 
-Discord then visit the [My Applications](https://discordapp.com/developers/applications/me)
+Discord then visit the [My Applications](https://discord.com/developers/applications/me)
 page and click on the **New Application** box.  Follow the prompts from there
 page and click on the **New Application** box.  Follow the prompts from there
 to finish creating your account.
 to finish creating your account.
 
 
 
 
-**More information about Bots vs Client accounts can be found [here](https://discordapp.com/developers/docs/topics/oauth2#bot-vs-user-accounts)**
+**More information about Bots vs Client accounts can be found [here](https://discord.com/developers/docs/topics/oauth2#bot-vs-user-accounts)**
 
 
 # Requirements
 # Requirements
 
 

+ 3 - 3
docs/index.md

@@ -2,12 +2,12 @@
 <hr>
 <hr>
 <img align="right" src="http://bwmarrin.github.io/discordgo/img/discordgo.png">
 <img align="right" src="http://bwmarrin.github.io/discordgo/img/discordgo.png">
 
 
-[Go](https://golang.org/) (golang) interface for the [Discord](https://discordapp.com/) 
+[Go](https://golang.org/) (golang) interface for the [Discord](https://discord.com/) 
 chat service.  Provides both low-level direct bindings to the 
 chat service.  Provides both low-level direct bindings to the 
 Discord API and helper functions that allow you to make custom clients and chat 
 Discord API and helper functions that allow you to make custom clients and chat 
 bot applications easily.
 bot applications easily.
 
 
-[Discord](https://discordapp.com/) is an all-in-one voice and text chat for 
+[Discord](https://discord.com/) is an all-in-one voice and text chat for 
 gamers that's free, secure, and works on both your desktop and phone. 
 gamers that's free, secure, and works on both your desktop and phone. 
  
  
 ### Why DiscordGo?
 ### Why DiscordGo?
@@ -30,4 +30,4 @@ information and support for DiscordGo.  There's also a chance to make some
 friends :)
 friends :)
 
 
 * Join the [Discord Gophers](https://discord.gg/0f1SbxBZjYoCtNPP) chat server dedicated to Go programming.
 * Join the [Discord Gophers](https://discord.gg/0f1SbxBZjYoCtNPP) chat server dedicated to Go programming.
-* Join the [Discord API](https://discordapp.com/invite/discord-API) chat server dedicated to the Discord API.
+* Join the [Discord API](https://discord.com/invite/discord-API) chat server dedicated to the Discord API.

+ 2 - 2
endpoints.go

@@ -18,12 +18,12 @@ var APIVersion = "6"
 
 
 // Known Discord API Endpoints.
 // Known Discord API Endpoints.
 var (
 var (
-	EndpointStatus     = "https://status.discordapp.com/api/v2/"
+	EndpointStatus     = "https://status.discord.com/api/v2/"
 	EndpointSm         = EndpointStatus + "scheduled-maintenances/"
 	EndpointSm         = EndpointStatus + "scheduled-maintenances/"
 	EndpointSmActive   = EndpointSm + "active.json"
 	EndpointSmActive   = EndpointSm + "active.json"
 	EndpointSmUpcoming = EndpointSm + "upcoming.json"
 	EndpointSmUpcoming = EndpointSm + "upcoming.json"
 
 
-	EndpointDiscord    = "https://discordapp.com/"
+	EndpointDiscord    = "https://discord.com/"
 	EndpointAPI        = EndpointDiscord + "api/v" + APIVersion + "/"
 	EndpointAPI        = EndpointDiscord + "api/v" + APIVersion + "/"
 	EndpointGuilds     = EndpointAPI + "guilds/"
 	EndpointGuilds     = EndpointAPI + "guilds/"
 	EndpointChannels   = EndpointAPI + "channels/"
 	EndpointChannels   = EndpointAPI + "channels/"

+ 1 - 1
event.go

@@ -110,7 +110,7 @@ func (s *Session) addEventHandlerOnce(eventHandler EventHandler) func() {
 //     })
 //     })
 //
 //
 // List of events can be found at this page, with corresponding names in the
 // List of events can be found at this page, with corresponding names in the
-// library for each event: https://discordapp.com/developers/docs/topics/gateway#event-names
+// library for each event: https://discord.com/developers/docs/topics/gateway#event-names
 // There are also synthetic events fired by the library internally which are
 // There are also synthetic events fired by the library internally which are
 // available for handling, like Connect, Disconnect, and RateLimit.
 // available for handling, like Connect, Disconnect, and RateLimit.
 // events.go contains all of the Discord WSAPI and synthetic events that can be handled.
 // events.go contains all of the Discord WSAPI and synthetic events that can be handled.

+ 6 - 2
events.go

@@ -139,8 +139,11 @@ type GuildEmojisUpdate struct {
 
 
 // A GuildMembersChunk is the data for a GuildMembersChunk event.
 // A GuildMembersChunk is the data for a GuildMembersChunk event.
 type GuildMembersChunk struct {
 type GuildMembersChunk struct {
-	GuildID string    `json:"guild_id"`
-	Members []*Member `json:"members"`
+	GuildID    string      `json:"guild_id"`
+	Members    []*Member   `json:"members"`
+	ChunkIndex int         `json:"chunk_index"`
+	ChunkCount int         `json:"chunk_count"`
+	Presences  []*Presence `json:"presences,omitempty"`
 }
 }
 
 
 // GuildIntegrationsUpdate is the data for a GuildIntegrationsUpdate event.
 // GuildIntegrationsUpdate is the data for a GuildIntegrationsUpdate event.
@@ -169,6 +172,7 @@ type MessageUpdate struct {
 // MessageDelete is the data for a MessageDelete event.
 // MessageDelete is the data for a MessageDelete event.
 type MessageDelete struct {
 type MessageDelete struct {
 	*Message
 	*Message
+	BeforeDelete *Message `json:"-"`
 }
 }
 
 
 // MessageReactionAdd is the data for a MessageReactionAdd event.
 // MessageReactionAdd is the data for a MessageReactionAdd event.

+ 1 - 1
examples/appmaker/README.md

@@ -6,7 +6,7 @@ This example demonstrates how to utilize DiscordGo to create, view, and delete
 Bot Applications on your account.
 Bot Applications on your account.
 
 
 These tasks are normally accomplished from the 
 These tasks are normally accomplished from the 
-[Discord Developers](https://discordapp.com/developers/applications/me) site.
+[Discord Developers](https://discord.com/developers/applications/me) site.
 
 
 **Join [Discord Gophers](https://discord.gg/0f1SbxBZjYoCtNPP)
 **Join [Discord Gophers](https://discord.gg/0f1SbxBZjYoCtNPP)
 Discord chat channel for support.**
 Discord chat channel for support.**

+ 1 - 1
examples/pingpong/main.go

@@ -51,7 +51,7 @@ func main() {
 }
 }
 
 
 // This function will be called (due to AddHandler above) every time a new
 // This function will be called (due to AddHandler above) every time a new
-// message is created on any channel that the autenticated bot has access to.
+// message is created on any channel that the authenticated bot has access to.
 func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
 func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
 
 
 	// Ignore all messages created by the bot itself
 	// Ignore all messages created by the bot itself

+ 2 - 0
go.mod

@@ -4,3 +4,5 @@ require (
 	github.com/gorilla/websocket v1.4.0
 	github.com/gorilla/websocket v1.4.0
 	golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16
 	golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16
 )
 )
+
+go 1.10

+ 1 - 1
logging.go

@@ -37,7 +37,7 @@ const (
 // Logger can be used to replace the standard logging for discordgo
 // Logger can be used to replace the standard logging for discordgo
 var Logger func(msgL, caller int, format string, a ...interface{})
 var Logger func(msgL, caller int, format string, a ...interface{})
 
 
-// msglog provides package wide logging consistancy for discordgo
+// msglog provides package wide logging consistency for discordgo
 // the format, a...  portion this command follows that of fmt.Printf
 // the format, a...  portion this command follows that of fmt.Printf
 //   msgL   : LogLevel of the message
 //   msgL   : LogLevel of the message
 //   caller : 1 + the number of callers away from the message source
 //   caller : 1 + the number of callers away from the message source

+ 45 - 7
message.go

@@ -63,7 +63,7 @@ type Message struct {
 	MentionRoles []string `json:"mention_roles"`
 	MentionRoles []string `json:"mention_roles"`
 
 
 	// Whether the message is text-to-speech.
 	// Whether the message is text-to-speech.
-	Tts bool `json:"tts"`
+	TTS bool `json:"tts"`
 
 
 	// Whether the message mentions everyone.
 	// Whether the message mentions everyone.
 	MentionEveryone bool `json:"mention_everyone"`
 	MentionEveryone bool `json:"mention_everyone"`
@@ -129,10 +129,11 @@ type File struct {
 
 
 // MessageSend stores all parameters you can send with ChannelMessageSendComplex.
 // MessageSend stores all parameters you can send with ChannelMessageSendComplex.
 type MessageSend struct {
 type MessageSend struct {
-	Content string        `json:"content,omitempty"`
-	Embed   *MessageEmbed `json:"embed,omitempty"`
-	Tts     bool          `json:"tts"`
-	Files   []*File       `json:"-"`
+	Content         string                  `json:"content,omitempty"`
+	Embed           *MessageEmbed           `json:"embed,omitempty"`
+	TTS             bool                    `json:"tts"`
+	Files           []*File                 `json:"-"`
+	AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"`
 
 
 	// TODO: Remove this when compatibility is not required.
 	// TODO: Remove this when compatibility is not required.
 	File *File `json:"-"`
 	File *File `json:"-"`
@@ -141,8 +142,9 @@ type MessageSend struct {
 // MessageEdit is used to chain parameters via ChannelMessageEditComplex, which
 // MessageEdit is used to chain parameters via ChannelMessageEditComplex, which
 // is also where you should get the instance from.
 // is also where you should get the instance from.
 type MessageEdit struct {
 type MessageEdit struct {
-	Content *string       `json:"content,omitempty"`
-	Embed   *MessageEmbed `json:"embed,omitempty"`
+	Content         *string                 `json:"content,omitempty"`
+	Embed           *MessageEmbed           `json:"embed,omitempty"`
+	AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"`
 
 
 	ID      string
 	ID      string
 	Channel string
 	Channel string
@@ -171,6 +173,42 @@ func (m *MessageEdit) SetEmbed(embed *MessageEmbed) *MessageEdit {
 	return m
 	return m
 }
 }
 
 
+// AllowedMentionType describes the types of mentions used
+// in the MessageAllowedMentions type.
+type AllowedMentionType string
+
+// The types of mentions used in MessageAllowedMentions.
+const (
+	AllowedMentionTypeRoles    AllowedMentionType = "roles"
+	AllowedMentionTypeUsers    AllowedMentionType = "users"
+	AllowedMentionTypeEveryone AllowedMentionType = "everyone"
+)
+
+// MessageAllowedMentions allows the user to specify which mentions
+// Discord is allowed to parse in this message. This is useful when
+// sending user input as a message, as it prevents unwanted mentions.
+// If this type is used, all mentions must be explicitly whitelisted,
+// either by putting an AllowedMentionType in the Parse slice
+// (allowing all mentions of that type) or, in the case of roles and
+// users, explicitly allowing those mentions on an ID-by-ID basis.
+// For more information on this functionality, see:
+// https://discordapp.com/developers/docs/resources/channel#allowed-mentions-object-allowed-mentions-reference
+type MessageAllowedMentions struct {
+	// The mention types that are allowed to be parsed in this message.
+	// Please note that this is purposely **not** marked as omitempty,
+	// so if a zero-value MessageAllowedMentions object is provided no
+	// mentions will be allowed.
+	Parse []AllowedMentionType `json:"parse"`
+
+	// A list of role IDs to allow. This cannot be used when specifying
+	// AllowedMentionTypeRoles in the Parse slice.
+	Roles []string `json:"roles,omitempty"`
+
+	// A list of user IDs to allow. This cannot be used when specifying
+	// AllowedMentionTypeUsers in the Parse slice.
+	Users []string `json:"users,omitempty"`
+}
+
 // A MessageAttachment stores data for message attachments.
 // A MessageAttachment stores data for message attachments.
 type MessageAttachment struct {
 type MessageAttachment struct {
 	ID       string `json:"id"`
 	ID       string `json:"id"`

+ 15 - 12
restapi.go

@@ -38,7 +38,7 @@ var (
 	ErrPruneDaysBounds         = errors.New("the number of days should be more than or equal to 1")
 	ErrPruneDaysBounds         = errors.New("the number of days should be more than or equal to 1")
 	ErrGuildNoIcon             = errors.New("guild does not have an icon set")
 	ErrGuildNoIcon             = errors.New("guild does not have an icon set")
 	ErrGuildNoSplash           = errors.New("guild does not have a splash set")
 	ErrGuildNoSplash           = errors.New("guild does not have a splash set")
-	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://discordapp.com/developers/docs/reference#authentication-example-bot-token-authorization-header")
+	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")
 )
 )
 
 
 // Request is the same as RequestWithBucketID but the bucket id is the same as the urlStr
 // Request is the same as RequestWithBucketID but the bucket id is the same as the urlStr
@@ -506,7 +506,7 @@ func (s *Session) UserChannelPermissions(userID, channelID string) (apermissions
 }
 }
 
 
 // Calculates the permissions for a member.
 // Calculates the permissions for a member.
-// https://support.discordapp.com/hc/en-us/articles/206141927-How-is-the-permission-hierarchy-structured-
+// https://support.discord.com/hc/en-us/articles/206141927-How-is-the-permission-hierarchy-structured-
 func memberPermissions(guild *Guild, channel *Channel, member *Member) (apermissions int) {
 func memberPermissions(guild *Guild, channel *Channel, member *Member) (apermissions int) {
 	userID := member.User.ID
 	userID := member.User.ID
 
 
@@ -583,14 +583,6 @@ func memberPermissions(guild *Guild, channel *Channel, member *Member) (apermiss
 // Guild returns a Guild structure of a specific Guild.
 // Guild returns a Guild structure of a specific Guild.
 // guildID   : The ID of a Guild
 // guildID   : The ID of a Guild
 func (s *Session) Guild(guildID string) (st *Guild, err error) {
 func (s *Session) Guild(guildID string) (st *Guild, err error) {
-	if s.StateEnabled {
-		// Attempt to grab the guild from State first.
-		st, err = s.State.Guild(guildID)
-		if err == nil && !st.Unavailable {
-			return
-		}
-	}
-
 	body, err := s.RequestWithBucketID("GET", EndpointGuild(guildID), nil, EndpointGuild(guildID))
 	body, err := s.RequestWithBucketID("GET", EndpointGuild(guildID), nil, EndpointGuild(guildID))
 	if err != nil {
 	if err != nil {
 		return
 		return
@@ -931,6 +923,8 @@ type GuildChannelCreateData struct {
 	Topic                string                 `json:"topic,omitempty"`
 	Topic                string                 `json:"topic,omitempty"`
 	Bitrate              int                    `json:"bitrate,omitempty"`
 	Bitrate              int                    `json:"bitrate,omitempty"`
 	UserLimit            int                    `json:"user_limit,omitempty"`
 	UserLimit            int                    `json:"user_limit,omitempty"`
+	RateLimitPerUser     int                    `json:"rate_limit_per_user,omitempty"`
+	Position             int                    `json:"position,omitempty"`
 	PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites,omitempty"`
 	PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites,omitempty"`
 	ParentID             string                 `json:"parent_id,omitempty"`
 	ParentID             string                 `json:"parent_id,omitempty"`
 	NSFW                 bool                   `json:"nsfw,omitempty"`
 	NSFW                 bool                   `json:"nsfw,omitempty"`
@@ -1593,7 +1587,7 @@ func (s *Session) ChannelMessageSendComplex(channelID string, data *MessageSend)
 func (s *Session) ChannelMessageSendTTS(channelID string, content string) (*Message, error) {
 func (s *Session) ChannelMessageSendTTS(channelID string, content string) (*Message, error) {
 	return s.ChannelMessageSendComplex(channelID, &MessageSend{
 	return s.ChannelMessageSendComplex(channelID, &MessageSend{
 		Content: content,
 		Content: content,
-		Tts:     true,
+		TTS:     true,
 	})
 	})
 }
 }
 
 
@@ -2132,7 +2126,9 @@ func (s *Session) MessageReactionsRemoveAll(channelID, messageID string) error {
 // messageID : The message ID.
 // messageID : The message ID.
 // emojiID   : Either the unicode emoji for the reaction, or a guild emoji identifier.
 // emojiID   : Either the unicode emoji for the reaction, or a guild emoji identifier.
 // limit    : max number of users to return (max 100)
 // limit    : max number of users to return (max 100)
-func (s *Session) MessageReactions(channelID, messageID, emojiID string, limit int) (st []*User, err error) {
+// beforeID  : If provided all reactions returned will be before given ID.
+// afterID   : If provided all reactions returned will be after given ID.
+func (s *Session) MessageReactions(channelID, messageID, emojiID string, limit int, beforeID, afterID string) (st []*User, err error) {
 	// emoji such as  #⃣ need to have # escaped
 	// emoji such as  #⃣ need to have # escaped
 	emojiID = strings.Replace(emojiID, "#", "%23", -1)
 	emojiID = strings.Replace(emojiID, "#", "%23", -1)
 	uri := EndpointMessageReactions(channelID, messageID, emojiID)
 	uri := EndpointMessageReactions(channelID, messageID, emojiID)
@@ -2143,6 +2139,13 @@ func (s *Session) MessageReactions(channelID, messageID, emojiID string, limit i
 		v.Set("limit", strconv.Itoa(limit))
 		v.Set("limit", strconv.Itoa(limit))
 	}
 	}
 
 
+	if afterID != "" {
+		v.Set("after", afterID)
+	}
+	if beforeID != "" {
+		v.Set("before", beforeID)
+	}
+
 	if len(v) > 0 {
 	if len(v) > 0 {
 		uri += "?" + v.Encode()
 		uri += "?" + v.Encode()
 	}
 	}

+ 13 - 0
state.go

@@ -848,6 +848,12 @@ func (s *State) OnInterface(se *Session, i interface{}) (err error) {
 				err = s.MemberAdd(t.Members[i])
 				err = s.MemberAdd(t.Members[i])
 			}
 			}
 		}
 		}
+
+		if s.TrackPresences {
+			for _, p := range t.Presences {
+				err = s.PresenceAdd(t.GuildID, p)
+			}
+		}
 	case *GuildRoleCreate:
 	case *GuildRoleCreate:
 		if s.TrackRoles {
 		if s.TrackRoles {
 			err = s.RoleAdd(t.GuildID, t.Role)
 			err = s.RoleAdd(t.GuildID, t.Role)
@@ -893,6 +899,13 @@ func (s *State) OnInterface(se *Session, i interface{}) (err error) {
 		}
 		}
 	case *MessageDelete:
 	case *MessageDelete:
 		if s.MaxMessageCount != 0 {
 		if s.MaxMessageCount != 0 {
+			var old *Message
+			old, err = s.Message(t.ChannelID, t.ID)
+			if err == nil {
+				oldCopy := *old
+				t.BeforeDelete = &oldCopy
+			}
+
 			err = s.MessageRemove(t.Message)
 			err = s.MessageRemove(t.Message)
 		}
 		}
 	case *MessageDeleteBulk:
 	case *MessageDeleteBulk:

+ 122 - 10
structs.go

@@ -29,8 +29,10 @@ type Session struct {
 	// General configurable settings.
 	// General configurable settings.
 
 
 	// Authentication token for this session
 	// Authentication token for this session
+	// TODO: Remove Below, Deprecated, Use Identify struct
 	Token string
 	Token string
-	MFA   bool
+
+	MFA bool
 
 
 	// Debug for printing JSON request/responses
 	// Debug for printing JSON request/responses
 	Debug    bool // Deprecated, will be removed.
 	Debug    bool // Deprecated, will be removed.
@@ -39,6 +41,11 @@ type Session struct {
 	// Should the session reconnect the websocket on errors.
 	// Should the session reconnect the websocket on errors.
 	ShouldReconnectOnError bool
 	ShouldReconnectOnError bool
 
 
+	// Identify is sent during initial handshake with the discord gateway.
+	// https://discord.com/developers/docs/topics/gateway#identify
+	Identify Identify
+
+	// TODO: Remove Below, Deprecated, Use Identify struct
 	// Should the session request compressed websocket data.
 	// Should the session request compressed websocket data.
 	Compress bool
 	Compress bool
 
 
@@ -587,12 +594,13 @@ type VoiceState struct {
 
 
 // A Presence stores the online, offline, or idle and game status of Guild members.
 // A Presence stores the online, offline, or idle and game status of Guild members.
 type Presence struct {
 type Presence struct {
-	User   *User    `json:"user"`
-	Status Status   `json:"status"`
-	Game   *Game    `json:"game"`
-	Nick   string   `json:"nick"`
-	Roles  []string `json:"roles"`
-	Since  *int     `json:"since"`
+	User       *User    `json:"user"`
+	Status     Status   `json:"status"`
+	Game       *Game    `json:"game"`
+	Activities []*Game  `json:"activities"`
+	Nick       string   `json:"nick"`
+	Roles      []string `json:"roles"`
+	Since      *int     `json:"since"`
 }
 }
 
 
 // GameType is the type of "game" (see GameType* consts) in the Game struct
 // GameType is the type of "game" (see GameType* consts) in the Game struct
@@ -604,6 +612,7 @@ const (
 	GameTypeStreaming
 	GameTypeStreaming
 	GameTypeListening
 	GameTypeListening
 	GameTypeWatching
 	GameTypeWatching
+	GameTypeCustom
 )
 )
 
 
 // A Game struct holds the name of the "playing .." game for a user
 // A Game struct holds the name of the "playing .." game for a user
@@ -687,7 +696,7 @@ type Settings struct {
 	RenderEmbeds           bool               `json:"render_embeds"`
 	RenderEmbeds           bool               `json:"render_embeds"`
 	InlineEmbedMedia       bool               `json:"inline_embed_media"`
 	InlineEmbedMedia       bool               `json:"inline_embed_media"`
 	InlineAttachmentMedia  bool               `json:"inline_attachment_media"`
 	InlineAttachmentMedia  bool               `json:"inline_attachment_media"`
-	EnableTtsCommand       bool               `json:"enable_tts_command"`
+	EnableTTSCommand       bool               `json:"enable_tts_command"`
 	MessageDisplayCompact  bool               `json:"message_display_compact"`
 	MessageDisplayCompact  bool               `json:"message_display_compact"`
 	ShowCurrentGame        bool               `json:"show_current_game"`
 	ShowCurrentGame        bool               `json:"show_current_game"`
 	ConvertEmoticons       bool               `json:"convert_emoticons"`
 	ConvertEmoticons       bool               `json:"convert_emoticons"`
@@ -909,8 +918,63 @@ type GatewayBotResponse struct {
 	Shards int    `json:"shards"`
 	Shards int    `json:"shards"`
 }
 }
 
 
+// GatewayStatusUpdate is sent by the client to indicate a presence or status update
+// https://discord.com/developers/docs/topics/gateway#update-status-gateway-status-update-structure
+type GatewayStatusUpdate struct {
+	Since  int      `json:"since"`
+	Game   Activity `json:"game"`
+	Status string   `json:"status"`
+	AFK    bool     `json:"afk"`
+}
+
+// Activity defines the Activity sent with GatewayStatusUpdate
+// https://discord.com/developers/docs/topics/gateway#activity-object
+type Activity struct {
+	Name string
+	Type ActivityType
+	URL  string
+}
+
+// ActivityType is the type of Activity (see ActivityType* consts) in the Activity struct
+// https://discord.com/developers/docs/topics/gateway#activity-object-activity-types
+type ActivityType int
+
+// Valid ActivityType values
+// https://discord.com/developers/docs/topics/gateway#activity-object-activity-types
+const (
+	ActivityTypeGame GameType = iota
+	ActivityTypeStreaming
+	ActivityTypeListening
+	//	ActivityTypeWatching // not valid in this use case?
+	ActivityTypeCustom = 4
+)
+
+// Identify is sent during initial handshake with the discord gateway.
+// https://discord.com/developers/docs/topics/gateway#identify
+type Identify struct {
+	Token              string              `json:"token"`
+	Properties         IdentifyProperties  `json:"properties"`
+	Compress           bool                `json:"compress"`
+	LargeThreshold     int                 `json:"large_threshold"`
+	Shard              *[2]int             `json:"shard,omitempty"`
+	Presence           GatewayStatusUpdate `json:"presence,omitempty"`
+	GuildSubscriptions bool                `json:"guild_subscriptions"`
+	Intents            *Intent             `json:"intents,omitempty"`
+}
+
+// IdentifyProperties contains the "properties" portion of an Identify packet
+// https://discord.com/developers/docs/topics/gateway#identify-identify-connection-properties
+type IdentifyProperties struct {
+	OS              string `json:"$os"`
+	Browser         string `json:"$browser"`
+	Device          string `json:"$device"`
+	Referer         string `json:"$referer"`
+	ReferringDomain string `json:"$referring_domain"`
+}
+
 // Constants for the different bit offsets of text channel permissions
 // Constants for the different bit offsets of text channel permissions
 const (
 const (
+	// Deprecated: PermissionReadMessages has been replaced with PermissionViewChannel for text and voice channels
 	PermissionReadMessages = 1 << (iota + 10)
 	PermissionReadMessages = 1 << (iota + 10)
 	PermissionSendMessages
 	PermissionSendMessages
 	PermissionSendTTSMessages
 	PermissionSendTTSMessages
@@ -952,8 +1016,9 @@ const (
 	PermissionManageServer
 	PermissionManageServer
 	PermissionAddReactions
 	PermissionAddReactions
 	PermissionViewAuditLogs
 	PermissionViewAuditLogs
+	PermissionViewChannel = 1 << (iota + 2)
 
 
-	PermissionAllText = PermissionReadMessages |
+	PermissionAllText = PermissionViewChannel |
 		PermissionSendMessages |
 		PermissionSendMessages |
 		PermissionSendTTSMessages |
 		PermissionSendTTSMessages |
 		PermissionManageMessages |
 		PermissionManageMessages |
@@ -961,7 +1026,8 @@ const (
 		PermissionAttachFiles |
 		PermissionAttachFiles |
 		PermissionReadMessageHistory |
 		PermissionReadMessageHistory |
 		PermissionMentionEveryone
 		PermissionMentionEveryone
-	PermissionAllVoice = PermissionVoiceConnect |
+	PermissionAllVoice = PermissionViewChannel |
+		PermissionVoiceConnect |
 		PermissionVoiceSpeak |
 		PermissionVoiceSpeak |
 		PermissionVoiceMuteMembers |
 		PermissionVoiceMuteMembers |
 		PermissionVoiceDeafenMembers |
 		PermissionVoiceDeafenMembers |
@@ -1037,3 +1103,49 @@ const (
 
 
 	ErrCodeReactionBlocked = 90001
 	ErrCodeReactionBlocked = 90001
 )
 )
+
+// Intent is the type of a Gateway Intent
+// https://discord.com/developers/docs/topics/gateway#gateway-intents
+type Intent int
+
+// Constants for the different bit offsets of intents
+const (
+	IntentsGuilds Intent = 1 << iota
+	IntentsGuildMembers
+	IntentsGuildBans
+	IntentsGuildEmojis
+	IntentsGuildIntegrations
+	IntentsGuildWebhooks
+	IntentsGuildInvites
+	IntentsGuildVoiceStates
+	IntentsGuildPresences
+	IntentsGuildMessages
+	IntentsGuildMessageReactions
+	IntentsGuildMessageTyping
+	IntentsDirectMessages
+	IntentsDirectMessageReactions
+	IntentsDirectMessageTyping
+
+	IntentsAllWithoutPrivileged = IntentsGuilds |
+		IntentsGuildBans |
+		IntentsGuildEmojis |
+		IntentsGuildIntegrations |
+		IntentsGuildWebhooks |
+		IntentsGuildInvites |
+		IntentsGuildVoiceStates |
+		IntentsGuildMessages |
+		IntentsGuildMessageReactions |
+		IntentsGuildMessageTyping |
+		IntentsDirectMessages |
+		IntentsDirectMessageReactions |
+		IntentsDirectMessageTyping
+	IntentsAll = IntentsAllWithoutPrivileged |
+		IntentsGuildMembers |
+		IntentsGuildPresences
+	IntentsNone Intent = 0
+)
+
+// MakeIntent helps convert a gateway intent value for use in the Identify structure.
+func MakeIntent(intents Intent) *Intent {
+	return &intents
+}

+ 1 - 1
util.go

@@ -12,6 +12,6 @@ func SnowflakeTimestamp(ID string) (t time.Time, err error) {
 		return
 		return
 	}
 	}
 	timestamp := (i >> 22) + 1420070400000
 	timestamp := (i >> 22) + 1420070400000
-	t = time.Unix(timestamp/1000, 0)
+	t = time.Unix(0, timestamp*1000000)
 	return
 	return
 }
 }

+ 21 - 0
util_test.go

@@ -0,0 +1,21 @@
+package discordgo
+
+import (
+	"testing"
+	"time"
+)
+
+func TestSnowflakeTimestamp(t *testing.T) {
+	// #discordgo channel ID :)
+	id := "155361364909621248"
+	parsedTimestamp, err := SnowflakeTimestamp(id)
+
+	if err != nil {
+		t.Errorf("returned error incorrect: got %v, want nil", err)
+	}
+
+	correctTimestamp := time.Date(2016, time.March, 4, 17, 10, 35, 869*1000000, time.UTC)
+	if !parsedTimestamp.Equal(correctTimestamp) {
+		t.Errorf("parsed time incorrect: got %v, want %v", parsedTimestamp, correctTimestamp)
+	}
+}

+ 19 - 0
voice.go

@@ -346,6 +346,25 @@ func (v *VoiceConnection) wsListen(wsConn *websocket.Conn, close <-chan struct{}
 	for {
 	for {
 		_, message, err := v.wsConn.ReadMessage()
 		_, message, err := v.wsConn.ReadMessage()
 		if err != nil {
 		if err != nil {
+			// 4014 indicates a manual disconnection by someone in the guild;
+			// we shouldn't reconnect.
+			if websocket.IsCloseError(err, 4014) {
+				v.log(LogInformational, "received 4014 manual disconnection")
+
+				// Abandon the voice WS connection
+				v.Lock()
+				v.wsConn = nil
+				v.Unlock()
+
+				v.session.Lock()
+				delete(v.session.VoiceConnections, v.GuildID)
+				v.session.Unlock()
+
+				v.Close()
+
+				return
+			}
+
 			// Detect if we have been closed manually. If a Close() has already
 			// Detect if we have been closed manually. If a Close() has already
 			// happened, the websocket we are listening on will be different to the
 			// happened, the websocket we are listening on will be different to the
 			// current session.
 			// current session.

+ 64 - 47
wsapi.go

@@ -18,7 +18,6 @@ import (
 	"fmt"
 	"fmt"
 	"io"
 	"io"
 	"net/http"
 	"net/http"
-	"runtime"
 	"sync/atomic"
 	"sync/atomic"
 	"time"
 	"time"
 
 
@@ -47,7 +46,7 @@ type resumePacket struct {
 }
 }
 
 
 // Open creates a websocket connection to Discord.
 // Open creates a websocket connection to Discord.
-// See: https://discordapp.com/developers/docs/topics/gateway#connecting
+// See: https://discord.com/developers/docs/topics/gateway#connecting
 func (s *Session) Open() error {
 func (s *Session) Open() error {
 	s.log(LogInformational, "called")
 	s.log(LogInformational, "called")
 
 
@@ -80,7 +79,7 @@ func (s *Session) Open() error {
 	header.Add("accept-encoding", "zlib")
 	header.Add("accept-encoding", "zlib")
 	s.wsConn, _, err = websocket.DefaultDialer.Dial(s.gateway, header)
 	s.wsConn, _, err = websocket.DefaultDialer.Dial(s.gateway, header)
 	if err != nil {
 	if err != nil {
-		s.log(LogWarning, "error connecting to gateway %s, %s", s.gateway, err)
+		s.log(LogError, "error connecting to gateway %s, %s", s.gateway, err)
 		s.gateway = "" // clear cached gateway
 		s.gateway = "" // clear cached gateway
 		s.wsConn = nil // Just to be safe.
 		s.wsConn = nil // Just to be safe.
 		return err
 		return err
@@ -399,9 +398,10 @@ func (s *Session) UpdateStatusComplex(usd UpdateStatusData) (err error) {
 }
 }
 
 
 type requestGuildMembersData struct {
 type requestGuildMembersData struct {
-	GuildID string `json:"guild_id"`
-	Query   string `json:"query"`
-	Limit   int    `json:"limit"`
+	GuildIDs  []string `json:"guild_id"`
+	Query     string   `json:"query"`
+	Limit     int      `json:"limit"`
+	Presences bool     `json:"presences"`
 }
 }
 
 
 type requestGuildMembersOp struct {
 type requestGuildMembersOp struct {
@@ -411,10 +411,39 @@ type requestGuildMembersOp struct {
 
 
 // RequestGuildMembers requests guild members from the gateway
 // RequestGuildMembers requests guild members from the gateway
 // The gateway responds with GuildMembersChunk events
 // The gateway responds with GuildMembersChunk events
-// guildID  : The ID of the guild to request members of
-// query    : String that username starts with, leave empty to return all members
-// limit    : Max number of items to return, or 0 to request all members matched
-func (s *Session) RequestGuildMembers(guildID, query string, limit int) (err error) {
+// guildID   : Single Guild ID to request members of
+// query     : String that username starts with, leave empty to return all members
+// limit     : Max number of items to return, or 0 to request all members matched
+// presences : Whether to request presences of guild members
+func (s *Session) RequestGuildMembers(guildID string, query string, limit int, presences bool) (err error) {
+	data := requestGuildMembersData{
+		GuildIDs:  []string{guildID},
+		Query:     query,
+		Limit:     limit,
+		Presences: presences,
+	}
+	err = s.requestGuildMembers(data)
+	return
+}
+
+// RequestGuildMembersBatch requests guild members from the gateway
+// The gateway responds with GuildMembersChunk events
+// guildID   : Slice of guild IDs to request members of
+// query     : String that username starts with, leave empty to return all members
+// limit     : Max number of items to return, or 0 to request all members matched
+// presences : Whether to request presences of guild members
+func (s *Session) RequestGuildMembersBatch(guildIDs []string, query string, limit int, presences bool) (err error) {
+	data := requestGuildMembersData{
+		GuildIDs:  guildIDs,
+		Query:     query,
+		Limit:     limit,
+		Presences: presences,
+	}
+	err = s.requestGuildMembers(data)
+	return
+}
+
+func (s *Session) requestGuildMembers(data requestGuildMembersData) (err error) {
 	s.log(LogInformational, "called")
 	s.log(LogInformational, "called")
 
 
 	s.RLock()
 	s.RLock()
@@ -423,12 +452,6 @@ func (s *Session) RequestGuildMembers(guildID, query string, limit int) (err err
 		return ErrWSNotFound
 		return ErrWSNotFound
 	}
 	}
 
 
-	data := requestGuildMembersData{
-		GuildID: guildID,
-		Query:   query,
-		Limit:   limit,
-	}
-
 	s.wsMutex.Lock()
 	s.wsMutex.Lock()
 	err = s.wsConn.WriteJSON(requestGuildMembersOp{8, data})
 	err = s.wsConn.WriteJSON(requestGuildMembersOp{8, data})
 	s.wsMutex.Unlock()
 	s.wsMutex.Unlock()
@@ -722,55 +745,42 @@ func (s *Session) onVoiceServerUpdate(st *VoiceServerUpdate) {
 	}
 	}
 }
 }
 
 
-type identifyProperties struct {
-	OS              string `json:"$os"`
-	Browser         string `json:"$browser"`
-	Device          string `json:"$device"`
-	Referer         string `json:"$referer"`
-	ReferringDomain string `json:"$referring_domain"`
-}
-
-type identifyData struct {
-	Token          string             `json:"token"`
-	Properties     identifyProperties `json:"properties"`
-	LargeThreshold int                `json:"large_threshold"`
-	Compress       bool               `json:"compress"`
-	Shard          *[2]int            `json:"shard,omitempty"`
-}
-
 type identifyOp struct {
 type identifyOp struct {
-	Op   int          `json:"op"`
-	Data identifyData `json:"d"`
+	Op   int      `json:"op"`
+	Data Identify `json:"d"`
 }
 }
 
 
 // identify sends the identify packet to the gateway
 // identify sends the identify packet to the gateway
 func (s *Session) identify() error {
 func (s *Session) identify() error {
+	s.log(LogDebug, "called")
 
 
-	properties := identifyProperties{runtime.GOOS,
-		"Discordgo v" + VERSION,
-		"",
-		"",
-		"",
+	// TODO: This is a temporary block of code to help
+	// maintain backwards compatability
+	if s.Compress == false {
+		s.Identify.Compress = false
 	}
 	}
 
 
-	data := identifyData{s.Token,
-		properties,
-		250,
-		s.Compress,
-		nil,
+	// TODO: This is a temporary block of code to help
+	// maintain backwards compatability
+	if s.Token != "" && s.Identify.Token == "" {
+		s.Identify.Token = s.Token
 	}
 	}
 
 
+	// TODO: Below block should be refactored so ShardID and ShardCount
+	// can be deprecated and their usage moved to the Session.Identify
+	// struct
 	if s.ShardCount > 1 {
 	if s.ShardCount > 1 {
 
 
 		if s.ShardID >= s.ShardCount {
 		if s.ShardID >= s.ShardCount {
 			return ErrWSShardBounds
 			return ErrWSShardBounds
 		}
 		}
 
 
-		data.Shard = &[2]int{s.ShardID, s.ShardCount}
+		s.Identify.Shard = &[2]int{s.ShardID, s.ShardCount}
 	}
 	}
 
 
-	op := identifyOp{2, data}
-
+	// Send Identify packet to Discord
+	op := identifyOp{2, s.Identify}
+	s.log(LogDebug, "Identify Packet: \n%#v", op)
 	s.wsMutex.Lock()
 	s.wsMutex.Lock()
 	err := s.wsConn.WriteJSON(op)
 	err := s.wsConn.WriteJSON(op)
 	s.wsMutex.Unlock()
 	s.wsMutex.Unlock()
@@ -838,6 +848,13 @@ func (s *Session) Close() error {
 }
 }
 
 
 // Close closes a websocket and stops all listening/heartbeat goroutines.
 // Close closes a websocket and stops all listening/heartbeat goroutines.
+// TODO: Add support for Voice WS/UDP
+func (s *Session) Close() error {
+	return s.CloseWithCode(websocket.CloseNormalClosure)
+}
+
+// CloseWithCode closes a websocket using the provided closeCode and stops all
+// listening/heartbeat goroutines.
 // TODO: Add support for Voice WS/UDP connections
 // TODO: Add support for Voice WS/UDP connections
 func (s *Session) CloseWithCode(closeCode int) (err error) {
 func (s *Session) CloseWithCode(closeCode int) (err error) {