Browse Source

Release version v0.21.0

Carson Hoffman 4 năm trước cách đây
mục cha
commit
1294b313b9
21 tập tin đã thay đổi với 350 bổ sung104 xóa
  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
 go:
-    - 1.9.x
     - 1.10.x
     - 1.11.x
+    - 1.12.x
+    - 1.13.x
 install:
     - go get github.com/bwmarrin/discordgo
     - go get -v .

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 3 - 3
README.md


+ 23 - 9
discord.go

@@ -17,11 +17,12 @@ import (
 	"errors"
 	"fmt"
 	"net/http"
+	"runtime"
 	"time"
 )
 
 // 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.
 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
 // 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,
+//     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
@@ -63,6 +67,15 @@ func New(args ...interface{}) (s *Session, err error) {
 		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 args == nil {
 		return
@@ -94,7 +107,8 @@ func New(args ...interface{}) (s *Session, err error) {
 
 			// If third string exists, it must be an auth token.
 			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:
@@ -107,7 +121,8 @@ func New(args ...interface{}) (s *Session, err error) {
 			} else if pass == "" {
 				pass = v
 			} else if s.Token == "" {
-				s.Token = v
+				s.Identify.Token = v
+				s.Token = v // TODO: Remove, Deprecated - Kept for backwards compatibility.
 			} else {
 				err = fmt.Errorf("too many string parameters provided")
 				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
 	// invalid.
 	if pass == "" {
-		s.Token = auth
+		s.Identify.Token = auth
+		s.Token = auth // TODO: Remove, Deprecated - Kept for backwards compatibility.
 	} else {
 		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 {
 				err = ErrMFA
 			} 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
 }

+ 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.
 
 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
 new account.
 
@@ -77,12 +77,12 @@ have access to some user client specific features however they gain access to
 many Bot specific features.
 
 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
 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
 

+ 3 - 3
docs/index.md

@@ -2,12 +2,12 @@
 <hr>
 <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 
 Discord API and helper functions that allow you to make custom clients and chat 
 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. 
  
 ### Why DiscordGo?
@@ -30,4 +30,4 @@ information and support for DiscordGo.  There's also a chance to make some
 friends :)
 
 * 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.
 var (
-	EndpointStatus     = "https://status.discordapp.com/api/v2/"
+	EndpointStatus     = "https://status.discord.com/api/v2/"
 	EndpointSm         = EndpointStatus + "scheduled-maintenances/"
 	EndpointSmActive   = EndpointSm + "active.json"
 	EndpointSmUpcoming = EndpointSm + "upcoming.json"
 
-	EndpointDiscord    = "https://discordapp.com/"
+	EndpointDiscord    = "https://discord.com/"
 	EndpointAPI        = EndpointDiscord + "api/v" + APIVersion + "/"
 	EndpointGuilds     = EndpointAPI + "guilds/"
 	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
-// 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
 // available for handling, like Connect, Disconnect, and RateLimit.
 // 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.
 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.
@@ -169,6 +172,7 @@ type MessageUpdate struct {
 // MessageDelete is the data for a MessageDelete event.
 type MessageDelete struct {
 	*Message
+	BeforeDelete *Message `json:"-"`
 }
 
 // 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.
 
 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)
 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
-// 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) {
 
 	// Ignore all messages created by the bot itself

+ 2 - 0
go.mod

@@ -4,3 +4,5 @@ require (
 	github.com/gorilla/websocket v1.4.0
 	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
 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
 //   msgL   : LogLevel of the message
 //   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"`
 
 	// Whether the message is text-to-speech.
-	Tts bool `json:"tts"`
+	TTS bool `json:"tts"`
 
 	// Whether the message mentions everyone.
 	MentionEveryone bool `json:"mention_everyone"`
@@ -129,10 +129,11 @@ type File struct {
 
 // MessageSend stores all parameters you can send with ChannelMessageSendComplex.
 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.
 	File *File `json:"-"`
@@ -141,8 +142,9 @@ type MessageSend struct {
 // MessageEdit is used to chain parameters via ChannelMessageEditComplex, which
 // is also where you should get the instance from.
 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
 	Channel string
@@ -171,6 +173,42 @@ func (m *MessageEdit) SetEmbed(embed *MessageEmbed) *MessageEdit {
 	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.
 type MessageAttachment struct {
 	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")
 	ErrGuildNoIcon             = errors.New("guild does not have an icon 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
@@ -506,7 +506,7 @@ func (s *Session) UserChannelPermissions(userID, channelID string) (apermissions
 }
 
 // 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) {
 	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.
 // guildID   : The ID of a Guild
 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))
 	if err != nil {
 		return
@@ -931,6 +923,8 @@ type GuildChannelCreateData struct {
 	Topic                string                 `json:"topic,omitempty"`
 	Bitrate              int                    `json:"bitrate,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"`
 	ParentID             string                 `json:"parent_id,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) {
 	return s.ChannelMessageSendComplex(channelID, &MessageSend{
 		Content: content,
-		Tts:     true,
+		TTS:     true,
 	})
 }
 
@@ -2132,7 +2126,9 @@ func (s *Session) MessageReactionsRemoveAll(channelID, messageID string) error {
 // messageID : The message ID.
 // emojiID   : Either the unicode emoji for the reaction, or a guild emoji identifier.
 // 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
 	emojiID = strings.Replace(emojiID, "#", "%23", -1)
 	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))
 	}
 
+	if afterID != "" {
+		v.Set("after", afterID)
+	}
+	if beforeID != "" {
+		v.Set("before", beforeID)
+	}
+
 	if len(v) > 0 {
 		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])
 			}
 		}
+
+		if s.TrackPresences {
+			for _, p := range t.Presences {
+				err = s.PresenceAdd(t.GuildID, p)
+			}
+		}
 	case *GuildRoleCreate:
 		if s.TrackRoles {
 			err = s.RoleAdd(t.GuildID, t.Role)
@@ -893,6 +899,13 @@ func (s *State) OnInterface(se *Session, i interface{}) (err error) {
 		}
 	case *MessageDelete:
 		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)
 		}
 	case *MessageDeleteBulk:

+ 122 - 10
structs.go

@@ -29,8 +29,10 @@ type Session struct {
 	// General configurable settings.
 
 	// Authentication token for this session
+	// TODO: Remove Below, Deprecated, Use Identify struct
 	Token string
-	MFA   bool
+
+	MFA bool
 
 	// Debug for printing JSON request/responses
 	Debug    bool // Deprecated, will be removed.
@@ -39,6 +41,11 @@ type Session struct {
 	// Should the session reconnect the websocket on errors.
 	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.
 	Compress bool
 
@@ -587,12 +594,13 @@ type VoiceState struct {
 
 // A Presence stores the online, offline, or idle and game status of Guild members.
 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
@@ -604,6 +612,7 @@ const (
 	GameTypeStreaming
 	GameTypeListening
 	GameTypeWatching
+	GameTypeCustom
 )
 
 // 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"`
 	InlineEmbedMedia       bool               `json:"inline_embed_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"`
 	ShowCurrentGame        bool               `json:"show_current_game"`
 	ConvertEmoticons       bool               `json:"convert_emoticons"`
@@ -909,8 +918,63 @@ type GatewayBotResponse struct {
 	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
 const (
+	// Deprecated: PermissionReadMessages has been replaced with PermissionViewChannel for text and voice channels
 	PermissionReadMessages = 1 << (iota + 10)
 	PermissionSendMessages
 	PermissionSendTTSMessages
@@ -952,8 +1016,9 @@ const (
 	PermissionManageServer
 	PermissionAddReactions
 	PermissionViewAuditLogs
+	PermissionViewChannel = 1 << (iota + 2)
 
-	PermissionAllText = PermissionReadMessages |
+	PermissionAllText = PermissionViewChannel |
 		PermissionSendMessages |
 		PermissionSendTTSMessages |
 		PermissionManageMessages |
@@ -961,7 +1026,8 @@ const (
 		PermissionAttachFiles |
 		PermissionReadMessageHistory |
 		PermissionMentionEveryone
-	PermissionAllVoice = PermissionVoiceConnect |
+	PermissionAllVoice = PermissionViewChannel |
+		PermissionVoiceConnect |
 		PermissionVoiceSpeak |
 		PermissionVoiceMuteMembers |
 		PermissionVoiceDeafenMembers |
@@ -1037,3 +1103,49 @@ const (
 
 	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
 	}
 	timestamp := (i >> 22) + 1420070400000
-	t = time.Unix(timestamp/1000, 0)
+	t = time.Unix(0, timestamp*1000000)
 	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 {
 		_, message, err := v.wsConn.ReadMessage()
 		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
 			// happened, the websocket we are listening on will be different to the
 			// current session.

+ 64 - 47
wsapi.go

@@ -18,7 +18,6 @@ import (
 	"fmt"
 	"io"
 	"net/http"
-	"runtime"
 	"sync/atomic"
 	"time"
 
@@ -47,7 +46,7 @@ type resumePacket struct {
 }
 
 // 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 {
 	s.log(LogInformational, "called")
 
@@ -80,7 +79,7 @@ func (s *Session) Open() error {
 	header.Add("accept-encoding", "zlib")
 	s.wsConn, _, err = websocket.DefaultDialer.Dial(s.gateway, header)
 	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.wsConn = nil // Just to be safe.
 		return err
@@ -399,9 +398,10 @@ func (s *Session) UpdateStatusComplex(usd UpdateStatusData) (err error) {
 }
 
 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 {
@@ -411,10 +411,39 @@ type requestGuildMembersOp struct {
 
 // RequestGuildMembers requests guild members from the gateway
 // 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.RLock()
@@ -423,12 +452,6 @@ func (s *Session) RequestGuildMembers(guildID, query string, limit int) (err err
 		return ErrWSNotFound
 	}
 
-	data := requestGuildMembersData{
-		GuildID: guildID,
-		Query:   query,
-		Limit:   limit,
-	}
-
 	s.wsMutex.Lock()
 	err = s.wsConn.WriteJSON(requestGuildMembersOp{8, data})
 	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 {
-	Op   int          `json:"op"`
-	Data identifyData `json:"d"`
+	Op   int      `json:"op"`
+	Data Identify `json:"d"`
 }
 
 // identify sends the identify packet to the gateway
 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.ShardID >= s.ShardCount {
 			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()
 	err := s.wsConn.WriteJSON(op)
 	s.wsMutex.Unlock()
@@ -838,6 +848,13 @@ func (s *Session) Close() error {
 }
 
 // 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
 func (s *Session) CloseWithCode(closeCode int) (err error) {