Browse Source

Merge remote-tracking branch 'upstream/master'

Gregory DALMAR 6 years ago
parent
commit
624ff560d4
19 changed files with 852 additions and 201 deletions
  1. 3 3
      .travis.yml
  2. 10 10
      README.md
  3. 3 3
      discord.go
  4. 19 9
      endpoints.go
  5. 10 1
      event.go
  6. 24 0
      eventhandlers.go
  7. 9 0
      events.go
  8. 6 0
      go.mod
  9. 4 0
      go.sum
  10. 54 15
      message.go
  11. 1 2
      message_test.go
  12. 1 1
      oauth2_test.go
  13. 202 31
      restapi.go
  14. 27 2
      state.go
  15. 366 73
      structs.go
  16. 1 2
      types.go
  17. 37 15
      user.go
  18. 5 5
      voice.go
  19. 70 29
      wsapi.go

+ 3 - 3
.travis.yml

@@ -1,12 +1,12 @@
 language: go
 go:
-    - 1.7.x
-    - 1.8.x
     - 1.9.x
+    - 1.10.x
+    - 1.11.x
 install:
     - go get github.com/bwmarrin/discordgo
     - go get -v .
-    - go get -v github.com/golang/lint/golint
+    - go get -v golang.org/x/lint/golint
 script:
     - diff <(gofmt -d .) <(echo -n)
     - go vet -x ./...

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


+ 3 - 3
discord.go

@@ -6,8 +6,8 @@
 // license that can be found in the LICENSE file.
 
 // This file contains high level helper functions and easy entry points for the
-// entire discordgo package.  These functions are beling developed and are very
-// experimental at this point.  They will most likley change so please use the
+// entire discordgo package.  These functions are being developed and are very
+// experimental at this point.  They will most likely change so please use the
 // low level functions if that's a problem.
 
 // Package discordgo provides Discord binding for Go
@@ -21,7 +21,7 @@ import (
 )
 
 // VERSION of DiscordGo, follows Semantic Versioning. (http://semver.org/)
-const VERSION = "0.18.0"
+const VERSION = "0.19.0"
 
 // ErrMFA will be risen by New when the user has 2FA.
 var ErrMFA = errors.New("account has 2FA enabled")

+ 19 - 9
endpoints.go

@@ -11,6 +11,8 @@
 
 package discordgo
 
+import "strconv"
+
 // APIVersion is the Discord API version used for the REST and Websocket API.
 var APIVersion = "6"
 
@@ -61,14 +63,18 @@ var (
 	EndpointUser               = func(uID string) string { return EndpointUsers + uID }
 	EndpointUserAvatar         = func(uID, aID string) string { return EndpointCDNAvatars + uID + "/" + aID + ".png" }
 	EndpointUserAvatarAnimated = func(uID, aID string) string { return EndpointCDNAvatars + uID + "/" + aID + ".gif" }
-	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 }
+	EndpointDefaultUserAvatar  = func(uDiscriminator string) string {
+		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 }
 	EndpointGuildChannels        = func(gID string) string { return EndpointGuilds + gID + "/channels" }
@@ -88,6 +94,9 @@ var (
 	EndpointGuildIcon            = func(gID, hash string) string { return EndpointCDNIcons + gID + "/" + hash + ".png" }
 	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 }
 
 	EndpointChannel                   = func(cID string) string { return EndpointChannels + cID }
 	EndpointChannelPermissions        = func(cID string) string { return EndpointChannels + cID + "/permissions" }
@@ -127,7 +136,8 @@ var (
 
 	EndpointIntegrationsJoin = func(iID string) string { return EndpointAPI + "integrations/" + iID + "/join" }
 
-	EndpointEmoji = func(eID string) string { return EndpointAPI + "emojis/" + eID + ".png" }
+	EndpointEmoji         = func(eID string) string { return EndpointAPI + "emojis/" + eID + ".png" }
+	EndpointEmojiAnimated = func(eID string) string { return EndpointAPI + "emojis/" + eID + ".gif" }
 
 	EndpointOauth2          = EndpointAPI + "oauth2/"
 	EndpointApplications    = EndpointOauth2 + "applications"

+ 10 - 1
event.go

@@ -98,7 +98,9 @@ func (s *Session) addEventHandlerOnce(eventHandler EventHandler) func() {
 
 // AddHandler allows you to add an event handler that will be fired anytime
 // the Discord WSAPI event that matches the function fires.
-// events.go contains all the Discord WSAPI events that can be fired.
+// The first parameter is a *Session, and the second parameter is a pointer
+// to a struct corresponding to the event for which you want to listen.
+//
 // eg:
 //     Session.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate) {
 //     })
@@ -106,6 +108,13 @@ func (s *Session) addEventHandlerOnce(eventHandler EventHandler) func() {
 // or:
 //     Session.AddHandler(func(s *discordgo.Session, m *discordgo.PresenceUpdate) {
 //     })
+//
+// 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
+// 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.
+//
 // The return value of this method is a function, that when called will remove the
 // event handler.
 func (s *Session) AddHandler(handler interface{}) func() {

+ 24 - 0
eventhandlers.go

@@ -50,6 +50,7 @@ const (
 	userUpdateEventType               = "USER_UPDATE"
 	voiceServerUpdateEventType        = "VOICE_SERVER_UPDATE"
 	voiceStateUpdateEventType         = "VOICE_STATE_UPDATE"
+	webhooksUpdateEventType           = "WEBHOOKS_UPDATE"
 )
 
 // channelCreateEventHandler is an event handler for ChannelCreate events.
@@ -892,6 +893,26 @@ func (eh voiceStateUpdateEventHandler) Handle(s *Session, i interface{}) {
 	}
 }
 
+// webhooksUpdateEventHandler is an event handler for WebhooksUpdate events.
+type webhooksUpdateEventHandler func(*Session, *WebhooksUpdate)
+
+// Type returns the event type for WebhooksUpdate events.
+func (eh webhooksUpdateEventHandler) Type() string {
+	return webhooksUpdateEventType
+}
+
+// New returns a new instance of WebhooksUpdate.
+func (eh webhooksUpdateEventHandler) New() interface{} {
+	return &WebhooksUpdate{}
+}
+
+// Handle is the handler for WebhooksUpdate events.
+func (eh webhooksUpdateEventHandler) Handle(s *Session, i interface{}) {
+	if t, ok := i.(*WebhooksUpdate); ok {
+		eh(s, t)
+	}
+}
+
 func handlerForInterface(handler interface{}) EventHandler {
 	switch v := handler.(type) {
 	case func(*Session, interface{}):
@@ -982,6 +1003,8 @@ func handlerForInterface(handler interface{}) EventHandler {
 		return voiceServerUpdateEventHandler(v)
 	case func(*Session, *VoiceStateUpdate):
 		return voiceStateUpdateEventHandler(v)
+	case func(*Session, *WebhooksUpdate):
+		return webhooksUpdateEventHandler(v)
 	}
 
 	return nil
@@ -1027,4 +1050,5 @@ func init() {
 	registerInterfaceProvider(userUpdateEventHandler(nil))
 	registerInterfaceProvider(voiceServerUpdateEventHandler(nil))
 	registerInterfaceProvider(voiceStateUpdateEventHandler(nil))
+	registerInterfaceProvider(webhooksUpdateEventHandler(nil))
 }

+ 9 - 0
events.go

@@ -70,6 +70,7 @@ type ChannelDelete struct {
 type ChannelPinsUpdate struct {
 	LastPinTimestamp string `json:"last_pin_timestamp"`
 	ChannelID        string `json:"channel_id"`
+	GuildID          string `json:"guild_id,omitempty"`
 }
 
 // GuildCreate is the data for a GuildCreate event.
@@ -212,6 +213,7 @@ type RelationshipRemove struct {
 type TypingStart struct {
 	UserID    string `json:"user_id"`
 	ChannelID string `json:"channel_id"`
+	GuildID   string `json:"guild_id,omitempty"`
 	Timestamp int    `json:"timestamp"`
 }
 
@@ -250,4 +252,11 @@ type VoiceStateUpdate struct {
 type MessageDeleteBulk struct {
 	Messages  []string `json:"ids"`
 	ChannelID string   `json:"channel_id"`
+	GuildID   string   `json:"guild_id"`
+}
+
+// WebhooksUpdate is the data for a WebhooksUpdate event
+type WebhooksUpdate struct {
+	GuildID   string `json:"guild_id"`
+	ChannelID string `json:"channel_id"`
 }

+ 6 - 0
go.mod

@@ -0,0 +1,6 @@
+module github.com/bwmarrin/discordgo
+
+require (
+	github.com/gorilla/websocket v1.4.0
+	golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16
+)

+ 4 - 0
go.sum

@@ -0,0 +1,4 @@
+github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
+github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 h1:y6ce7gCWtnH+m3dCjzQ1PCuwl28DDIc3VNnvY29DlIA=
+golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=

+ 54 - 15
message.go

@@ -32,20 +32,59 @@ const (
 
 // A Message stores all data related to a specific Discord message.
 type Message struct {
-	ID              string               `json:"id"`
-	ChannelID       string               `json:"channel_id"`
-	Content         string               `json:"content"`
-	Timestamp       Timestamp            `json:"timestamp"`
-	EditedTimestamp Timestamp            `json:"edited_timestamp"`
-	MentionRoles    []string             `json:"mention_roles"`
-	Tts             bool                 `json:"tts"`
-	MentionEveryone bool                 `json:"mention_everyone"`
-	Author          *User                `json:"author"`
-	Attachments     []*MessageAttachment `json:"attachments"`
-	Embeds          []*MessageEmbed      `json:"embeds"`
-	Mentions        []*User              `json:"mentions"`
-	Reactions       []*MessageReactions  `json:"reactions"`
-	Type            MessageType          `json:"type"`
+	// The ID of the message.
+	ID string `json:"id"`
+
+	// The ID of the channel in which the message was sent.
+	ChannelID string `json:"channel_id"`
+
+	// The ID of the guild in which the message was sent.
+	GuildID string `json:"guild_id,omitempty"`
+
+	// The content of the message.
+	Content string `json:"content"`
+
+	// The time at which the messsage was sent.
+	// 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"`
+
+	// The time at which the last edit of the message
+	// occurred, if it has been edited.
+	EditedTimestamp Timestamp `json:"edited_timestamp"`
+
+	// The roles mentioned in the message.
+	MentionRoles []string `json:"mention_roles"`
+
+	// Whether the message is text-to-speech.
+	Tts bool `json:"tts"`
+
+	// Whether the message mentions everyone.
+	MentionEveryone bool `json:"mention_everyone"`
+
+	// The author of the message. This is not guaranteed to be a
+	// valid user (webhook-sent messages do not possess a full author).
+	Author *User `json:"author"`
+
+	// A list of attachments present in the message.
+	Attachments []*MessageAttachment `json:"attachments"`
+
+	// A list of embeds present in the message. Multiple
+	// embeds can currently only be sent by webhooks.
+	Embeds []*MessageEmbed `json:"embeds"`
+
+	// A list of users mentioned in the message.
+	Mentions []*User `json:"mentions"`
+
+	// A list of reactions to the message.
+	Reactions []*MessageReactions `json:"reactions"`
+
+	// The type of the message.
+	Type MessageType `json:"type"`
+
+	// The webhook ID of the message, if it was generated by a webhook
+	WebhookID string `json:"webhook_id"`
 }
 
 // File stores info about files you e.g. send in messages.
@@ -237,7 +276,7 @@ func (m *Message) ContentWithMoreMentionsReplaced(s *Session) (content string, e
 			continue
 		}
 
-		content = strings.Replace(content, "<&"+role.ID+">", "@"+role.Name, -1)
+		content = strings.Replace(content, "<@&"+role.ID+">", "@"+role.Name, -1)
 	}
 
 	content = patternChannels.ReplaceAllStringFunc(content, func(mention string) string {

+ 1 - 2
message_test.go

@@ -12,7 +12,6 @@ func TestContentWithMoreMentionsReplaced(t *testing.T) {
 		Username: "User Name",
 	}
 
-	s.StateEnabled = true
 	s.State.GuildAdd(&Guild{ID: "guild"})
 	s.State.RoleAdd("guild", &Role{
 		ID:          "role",
@@ -30,7 +29,7 @@ func TestContentWithMoreMentionsReplaced(t *testing.T) {
 		ID:      "channel",
 	})
 	m := &Message{
-		Content:      "<&role> <@!user> <@user> <#channel>",
+		Content:      "<@&role> <@!user> <@user> <#channel>",
 		ChannelID:    "channel",
 		MentionRoles: []string{"role"},
 		Mentions:     []*User{user},

+ 1 - 1
oauth2_test.go

@@ -9,7 +9,7 @@ import (
 
 func ExampleApplication() {
 
-	// Authentication Token pulled from environment variable DG_TOKEN
+	// Authentication Token pulled from environment variable DGU_TOKEN
 	Token := os.Getenv("DGU_TOKEN")
 	if Token == "" {
 		return

+ 202 - 31
restapi.go

@@ -38,6 +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")
 )
 
 // Request is the same as RequestWithBucketID but the bucket id is the same as the urlStr
@@ -89,7 +90,7 @@ func (s *Session) RequestWithLockedBucket(method, urlStr, contentType string, b
 
 	req.Header.Set("Content-Type", contentType)
 	// TODO: Make a configurable static variable.
-	req.Header.Set("User-Agent", fmt.Sprintf("DiscordBot (https://github.com/bwmarrin/discordgo, v%s)", VERSION))
+	req.Header.Set("User-Agent", "DiscordBot (https://github.com/bwmarrin/discordgo, v"+VERSION+")")
 
 	if s.Debug {
 		for k, v := range req.Header {
@@ -129,13 +130,9 @@ func (s *Session) RequestWithLockedBucket(method, urlStr, contentType string, b
 	}
 
 	switch resp.StatusCode {
-
 	case http.StatusOK:
 	case http.StatusCreated:
 	case http.StatusNoContent:
-
-		// TODO check for 401 response, invalidate token if we get one.
-
 	case http.StatusBadGateway:
 		// Retry sending request if possible
 		if sequence < s.MaxRestRetries {
@@ -145,7 +142,6 @@ func (s *Session) RequestWithLockedBucket(method, urlStr, contentType string, b
 		} else {
 			err = fmt.Errorf("Exceeded Max retries HTTP %s, %s", resp.Status, response)
 		}
-
 	case 429: // TOO MANY REQUESTS - Rate limiting
 		rl := TooManyRequests{}
 		err = json.Unmarshal(response, &rl)
@@ -161,7 +157,12 @@ func (s *Session) RequestWithLockedBucket(method, urlStr, contentType string, b
 		// this method can cause longer delays than required
 
 		response, err = s.RequestWithLockedBucket(method, urlStr, contentType, b, s.Ratelimiter.LockBucketObject(bucket), sequence)
-
+	case http.StatusUnauthorized:
+		if strings.Index(s.Token, "Bot ") != 0 {
+			s.log(LogInformational, ErrUnauthorized.Error())
+			err = ErrUnauthorized
+		}
+		fallthrough
 	default: // Error condition
 		err = newRestError(req, resp, response)
 	}
@@ -249,7 +250,7 @@ func (s *Session) Register(username string) (token string, err error) {
 // even use.
 func (s *Session) Logout() (err error) {
 
-	//  _, err = s.Request("POST", LOGOUT, fmt.Sprintf(`{"token": "%s"}`, s.Token))
+	//  _, err = s.Request("POST", LOGOUT, `{"token": "` + s.Token + `"}`)
 
 	if s.Token == "" {
 		return
@@ -361,6 +362,21 @@ func (s *Session) UserUpdateStatus(status Status) (st *Settings, err error) {
 	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"))
+	if err != nil {
+		return nil, err
+	}
+
+	err = unmarshal(response, &conn)
+	if err != nil {
+		return
+	}
+
+	return
+}
+
 // UserChannels returns an array of Channel structures for all private
 // channels.
 func (s *Session) UserChannels() (st []*Channel, err error) {
@@ -412,7 +428,7 @@ func (s *Session) UserGuilds(limit int, beforeID, afterID string) (st []*UserGui
 	uri := EndpointUserGuilds("@me")
 
 	if len(v) > 0 {
-		uri = fmt.Sprintf("%s?%s", uri, v.Encode())
+		uri += "?" + v.Encode()
 	}
 
 	body, err := s.RequestWithBucketID("GET", uri, nil, EndpointUserGuilds(""))
@@ -565,7 +581,7 @@ 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 {
+		if err == nil && !st.Unavailable {
 			return
 		}
 	}
@@ -735,7 +751,7 @@ func (s *Session) GuildMembers(guildID string, after string, limit int) (st []*M
 	}
 
 	if len(v) > 0 {
-		uri = fmt.Sprintf("%s?%s", uri, v.Encode())
+		uri += "?" + v.Encode()
 	}
 
 	body, err := s.RequestWithBucketID("GET", uri, nil, EndpointGuildMembers(guildID))
@@ -761,6 +777,32 @@ func (s *Session) GuildMember(guildID, userID string) (st *Member, err error) {
 	return
 }
 
+// GuildMemberAdd force joins a user to the guild.
+//  accessToken   : Valid access_token for the user.
+//  guildID       : The ID of a Guild.
+//  userID        : The ID of a User.
+//  nick          : Value to set users nickname to
+//  roles         : A list of role ID's to set on the member.
+//  mute          : If the user is muted.
+//  deaf          : If the user is deafened.
+func (s *Session) GuildMemberAdd(accessToken, guildID, userID, nick string, roles []string, mute, deaf bool) (err error) {
+
+	data := struct {
+		AccessToken string   `json:"access_token"`
+		Nick        string   `json:"nick,omitempty"`
+		Roles       []string `json:"roles,omitempty"`
+		Mute        bool     `json:"mute,omitempty"`
+		Deaf        bool     `json:"deaf,omitempty"`
+	}{accessToken, nick, roles, mute, deaf}
+
+	_, err = s.RequestWithBucketID("PUT", EndpointGuildMember(guildID, userID), data, EndpointGuildMember(guildID, ""))
+	if err != nil {
+		return err
+	}
+
+	return err
+}
+
 // GuildMemberDelete removes the given user from the given guild.
 // guildID   : The ID of a Guild.
 // userID    : The ID of a User
@@ -877,17 +919,22 @@ func (s *Session) GuildChannels(guildID string) (st []*Channel, err error) {
 	return
 }
 
-// GuildChannelCreate creates a new channel in the given guild
-// guildID   : The ID of a Guild.
-// name      : Name of the channel (2-100 chars length)
-// ctype     : Tpye of the channel (voice or text)
-func (s *Session) GuildChannelCreate(guildID, name, ctype string) (st *Channel, err error) {
-
-	data := struct {
-		Name string `json:"name"`
-		Type string `json:"type"`
-	}{name, ctype}
+// GuildChannelCreateData is provided to GuildChannelCreateComplex
+type GuildChannelCreateData struct {
+	Name                 string                 `json:"name"`
+	Type                 ChannelType            `json:"type"`
+	Topic                string                 `json:"topic,omitempty"`
+	Bitrate              int                    `json:"bitrate,omitempty"`
+	UserLimit            int                    `json:"user_limit,omitempty"`
+	PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites,omitempty"`
+	ParentID             string                 `json:"parent_id,omitempty"`
+	NSFW                 bool                   `json:"nsfw,omitempty"`
+}
 
+// GuildChannelCreateComplex creates a new channel in the given guild
+// guildID      : The ID of a Guild
+// data         : A data struct describing the new Channel, Name and Type are mandatory, other fields depending on the type
+func (s *Session) GuildChannelCreateComplex(guildID string, data GuildChannelCreateData) (st *Channel, err error) {
 	body, err := s.RequestWithBucketID("POST", EndpointGuildChannels(guildID), data, EndpointGuildChannels(guildID))
 	if err != nil {
 		return
@@ -897,12 +944,33 @@ func (s *Session) GuildChannelCreate(guildID, name, ctype string) (st *Channel,
 	return
 }
 
+// GuildChannelCreate creates a new channel in the given guild
+// guildID   : The ID of a Guild.
+// name      : Name of the channel (2-100 chars length)
+// ctype     : Type of the channel
+func (s *Session) GuildChannelCreate(guildID, name string, ctype ChannelType) (st *Channel, err error) {
+	return s.GuildChannelCreateComplex(guildID, GuildChannelCreateData{
+		Name: name,
+		Type: ctype,
+	})
+}
+
 // GuildChannelsReorder updates the order of channels in a guild
 // guildID   : The ID of a Guild.
 // channels  : Updated channels.
 func (s *Session) GuildChannelsReorder(guildID string, channels []*Channel) (err error) {
 
-	_, err = s.RequestWithBucketID("PATCH", EndpointGuildChannels(guildID), channels, EndpointGuildChannels(guildID))
+	data := make([]struct {
+		ID       string `json:"id"`
+		Position int    `json:"position"`
+	}, len(channels))
+
+	for i, c := range channels {
+		data[i].ID = c.ID
+		data[i].Position = c.Position
+	}
+
+	_, err = s.RequestWithBucketID("PATCH", EndpointGuildChannels(guildID), data, EndpointGuildChannels(guildID))
 	return
 }
 
@@ -1021,7 +1089,7 @@ func (s *Session) GuildPruneCount(guildID string, days uint32) (count uint32, er
 		Pruned uint32 `json:"pruned"`
 	}{}
 
-	uri := EndpointGuildPrune(guildID) + fmt.Sprintf("?days=%d", days)
+	uri := EndpointGuildPrune(guildID) + "?days=" + strconv.FormatUint(uint64(days), 10)
 	body, err := s.RequestWithBucketID("GET", uri, nil, EndpointGuildPrune(guildID))
 	if err != nil {
 		return
@@ -1075,7 +1143,7 @@ func (s *Session) GuildPrune(guildID string, days uint32) (count uint32, err err
 
 // GuildIntegrations returns an array of Integrations for a guild.
 // guildID   : The ID of a Guild.
-func (s *Session) GuildIntegrations(guildID string) (st []*GuildIntegration, err error) {
+func (s *Session) GuildIntegrations(guildID string) (st []*Integration, err error) {
 
 	body, err := s.RequestWithBucketID("GET", EndpointGuildIntegrations(guildID), nil, EndpointGuildIntegrations(guildID))
 	if err != nil {
@@ -1206,6 +1274,94 @@ func (s *Session) GuildEmbedEdit(guildID string, enabled bool, channelID string)
 	return
 }
 
+// GuildAuditLog returns the audit log for a Guild.
+// guildID     : The ID of a Guild.
+// userID      : If provided the log will be filtered for the given ID.
+// beforeID    : If provided all log entries returned will be before the given ID.
+// actionType  : If provided the log will be filtered for the given Action Type.
+// limit       : The number messages that can be returned. (default 50, min 1, max 100)
+func (s *Session) GuildAuditLog(guildID, userID, beforeID string, actionType, limit int) (st *GuildAuditLog, err error) {
+
+	uri := EndpointGuildAuditLogs(guildID)
+
+	v := url.Values{}
+	if userID != "" {
+		v.Set("user_id", userID)
+	}
+	if beforeID != "" {
+		v.Set("before", beforeID)
+	}
+	if actionType > 0 {
+		v.Set("action_type", strconv.Itoa(actionType))
+	}
+	if limit > 0 {
+		v.Set("limit", strconv.Itoa(limit))
+	}
+	if len(v) > 0 {
+		uri = fmt.Sprintf("%s?%s", uri, v.Encode())
+	}
+
+	body, err := s.RequestWithBucketID("GET", uri, nil, EndpointGuildAuditLogs(guildID))
+	if err != nil {
+		return
+	}
+
+	err = unmarshal(body, &st)
+	return
+}
+
+// GuildEmojiCreate creates a new emoji
+// guildID : The ID of a Guild.
+// name    : The Name of the Emoji.
+// image   : The base64 encoded emoji image, has to be smaller than 256KB.
+// roles   : The roles for which this emoji will be whitelisted, can be nil.
+func (s *Session) GuildEmojiCreate(guildID, name, image string, roles []string) (emoji *Emoji, err error) {
+
+	data := struct {
+		Name  string   `json:"name"`
+		Image string   `json:"image"`
+		Roles []string `json:"roles,omitempty"`
+	}{name, image, roles}
+
+	body, err := s.RequestWithBucketID("POST", EndpointGuildEmojis(guildID), data, EndpointGuildEmojis(guildID))
+	if err != nil {
+		return
+	}
+
+	err = unmarshal(body, &emoji)
+	return
+}
+
+// GuildEmojiEdit modifies an emoji
+// guildID : The ID of a Guild.
+// emojiID : The ID of an Emoji.
+// name    : The Name of the Emoji.
+// roles   : The roles for which this emoji will be whitelisted, can be nil.
+func (s *Session) GuildEmojiEdit(guildID, emojiID, name string, roles []string) (emoji *Emoji, err error) {
+
+	data := struct {
+		Name  string   `json:"name"`
+		Roles []string `json:"roles,omitempty"`
+	}{name, roles}
+
+	body, err := s.RequestWithBucketID("PATCH", EndpointGuildEmoji(guildID, emojiID), data, EndpointGuildEmojis(guildID))
+	if err != nil {
+		return
+	}
+
+	err = unmarshal(body, &emoji)
+	return
+}
+
+// GuildEmojiDelete deletes an Emoji.
+// guildID : The ID of a Guild.
+// emojiID : The ID of an Emoji.
+func (s *Session) GuildEmojiDelete(guildID, emojiID string) (err error) {
+
+	_, err = s.RequestWithBucketID("DELETE", EndpointGuildEmoji(guildID, emojiID), nil, EndpointGuildEmojis(guildID))
+	return
+}
+
 // ------------------------------------------------------------------------------------------------
 // Functions specific to Discord Channels
 // ------------------------------------------------------------------------------------------------
@@ -1291,7 +1447,7 @@ func (s *Session) ChannelMessages(channelID string, limit int, beforeID, afterID
 		v.Set("around", aroundID)
 	}
 	if len(v) > 0 {
-		uri = fmt.Sprintf("%s?%s", uri, v.Encode())
+		uri += "?" + v.Encode()
 	}
 
 	body, err := s.RequestWithBucketID("GET", uri, nil, EndpointChannelMessages(channelID))
@@ -1586,7 +1742,8 @@ func (s *Session) ChannelInviteCreate(channelID string, i Invite) (st *Invite, e
 		MaxAge    int  `json:"max_age"`
 		MaxUses   int  `json:"max_uses"`
 		Temporary bool `json:"temporary"`
-	}{i.MaxAge, i.MaxUses, i.Temporary}
+		Unique    bool `json:"unique"`
+	}{i.MaxAge, i.MaxUses, i.Temporary, i.Unique}
 
 	body, err := s.RequestWithBucketID("POST", EndpointChannelInvites(channelID), data, EndpointChannelInvites(channelID))
 	if err != nil {
@@ -1638,6 +1795,19 @@ func (s *Session) Invite(inviteID string) (st *Invite, err error) {
 	return
 }
 
+// InviteWithCounts returns an Invite structure of the given invite including approximate member counts
+// inviteID : The invite code
+func (s *Session) InviteWithCounts(inviteID string) (st *Invite, err error) {
+
+	body, err := s.RequestWithBucketID("GET", EndpointInvite(inviteID)+"?with_counts=true", nil, EndpointInvite(""))
+	if err != nil {
+		return
+	}
+
+	err = unmarshal(body, &st)
+	return
+}
+
 // InviteDelete deletes an existing invite
 // inviteID   : the code of an invite
 func (s *Session) InviteDelete(inviteID string) (st *Invite, err error) {
@@ -1830,12 +2000,13 @@ func (s *Session) WebhookWithToken(webhookID, token string) (st *Webhook, err er
 // webhookID: The ID of a webhook.
 // name     : The name of the webhook.
 // avatar   : The avatar of the webhook.
-func (s *Session) WebhookEdit(webhookID, name, avatar string) (st *Role, err error) {
+func (s *Session) WebhookEdit(webhookID, name, avatar, channelID string) (st *Role, err error) {
 
 	data := struct {
-		Name   string `json:"name,omitempty"`
-		Avatar string `json:"avatar,omitempty"`
-	}{name, avatar}
+		Name      string `json:"name,omitempty"`
+		Avatar    string `json:"avatar,omitempty"`
+		ChannelID string `json:"channel_id,omitempty"`
+	}{name, avatar, channelID}
 
 	body, err := s.RequestWithBucketID("PATCH", EndpointWebhook(webhookID), data, EndpointWebhooks)
 	if err != nil {
@@ -1965,7 +2136,7 @@ func (s *Session) MessageReactions(channelID, messageID, emojiID string, limit i
 	}
 
 	if len(v) > 0 {
-		uri = fmt.Sprintf("%s?%s", uri, v.Encode())
+		uri += "?" + v.Encode()
 	}
 
 	body, err := s.RequestWithBucketID("GET", uri, nil, EndpointMessageReaction(channelID, "", "", ""))

+ 27 - 2
state.go

@@ -32,6 +32,7 @@ type State struct {
 	sync.RWMutex
 	Ready
 
+	// MaxMessageCount represents how many messages per channel the state will store.
 	MaxMessageCount int
 	TrackChannels   bool
 	TrackEmojis     bool
@@ -98,6 +99,9 @@ func (s *State) GuildAdd(guild *Guild) error {
 	if g, ok := s.guildMap[guild.ID]; ok {
 		// We are about to replace `g` in the state with `guild`, but first we need to
 		// make sure we preserve any fields that the `guild` doesn't contain from `g`.
+		if guild.MemberCount == 0 {
+			guild.MemberCount = g.MemberCount
+		}
 		if guild.Roles == nil {
 			guild.Roles = g.Roles
 		}
@@ -299,7 +303,12 @@ func (s *State) MemberAdd(member *Member) error {
 		members[member.User.ID] = member
 		guild.Members = append(guild.Members, member)
 	} else {
-		*m = *member // Update the actual data, which will also update the member pointer in the slice
+		// 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 == "" {
+			member.JoinedAt = m.JoinedAt
+		}
+		*m = *member
 	}
 
 	return nil
@@ -607,7 +616,7 @@ func (s *State) EmojisAdd(guildID string, emojis []*Emoji) error {
 
 // MessageAdd adds a message to the current world state, or updates it if it exists.
 // If the channel cannot be found, the message is discarded.
-// Messages are kept in state up to s.MaxMessageCount
+// Messages are kept in state up to s.MaxMessageCount per channel.
 func (s *State) MessageAdd(message *Message) error {
 	if s == nil {
 		return ErrNilState
@@ -805,6 +814,14 @@ func (s *State) OnInterface(se *Session, i interface{}) (err error) {
 	case *GuildDelete:
 		err = s.GuildRemove(t.Guild)
 	case *GuildMemberAdd:
+		// Updates the MemberCount of the guild.
+		guild, err := s.Guild(t.Member.GuildID)
+		if err != nil {
+			return err
+		}
+		guild.MemberCount++
+
+		// Caches member if tracking is enabled.
 		if s.TrackMembers {
 			err = s.MemberAdd(t.Member)
 		}
@@ -813,6 +830,14 @@ func (s *State) OnInterface(se *Session, i interface{}) (err error) {
 			err = s.MemberAdd(t.Member)
 		}
 	case *GuildMemberRemove:
+		// Updates the MemberCount of the guild.
+		guild, err := s.Guild(t.Member.GuildID)
+		if err != nil {
+			return err
+		}
+		guild.MemberCount--
+
+		// Removes member from the cache if tracking is enabled.
 		if s.TrackMembers {
 			err = s.MemberRemove(t.Member)
 		}

+ 366 - 73
structs.go

@@ -13,6 +13,7 @@ package discordgo
 
 import (
 	"encoding/json"
+	"fmt"
 	"net/http"
 	"sync"
 	"time"
@@ -84,6 +85,9 @@ type Session struct {
 	// Stores the last HeartbeatAck that was recieved (in UTC)
 	LastHeartbeatAck time.Time
 
+	// Stores the last Heartbeat sent (in UTC)
+	LastHeartbeatSent time.Time
+
 	// used to deal with rate limits
 	Ratelimiter *RateLimiter
 
@@ -111,6 +115,37 @@ type Session struct {
 	wsMutex sync.Mutex
 }
 
+// UserConnection is a Connection returned from the UserConnections endpoint
+type UserConnection struct {
+	ID           string         `json:"id"`
+	Name         string         `json:"name"`
+	Type         string         `json:"type"`
+	Revoked      bool           `json:"revoked"`
+	Integrations []*Integration `json:"integrations"`
+}
+
+// Integration stores integration information
+type Integration struct {
+	ID                string             `json:"id"`
+	Name              string             `json:"name"`
+	Type              string             `json:"type"`
+	Enabled           bool               `json:"enabled"`
+	Syncing           bool               `json:"syncing"`
+	RoleID            string             `json:"role_id"`
+	ExpireBehavior    int                `json:"expire_behavior"`
+	ExpireGracePeriod int                `json:"expire_grace_period"`
+	User              *User              `json:"user"`
+	Account           IntegrationAccount `json:"account"`
+	SyncedAt          Timestamp          `json:"synced_at"`
+}
+
+// IntegrationAccount is integration account information
+// sent by the UserConnections endpoint
+type IntegrationAccount struct {
+	ID   string `json:"id"`
+	Name string `json:"name"`
+}
+
 // A VoiceRegion stores data for a specific voice region server.
 type VoiceRegion struct {
 	ID       string `json:"id"`
@@ -145,6 +180,10 @@ type Invite struct {
 	Revoked   bool      `json:"revoked"`
 	Temporary bool      `json:"temporary"`
 	Unique    bool      `json:"unique"`
+
+	// will only be filled when using InviteWithCounts
+	ApproximatePresenceCount int `json:"approximate_presence_count"`
+	ApproximateMemberCount   int `json:"approximate_member_count"`
 }
 
 // ChannelType is the type of a Channel
@@ -161,22 +200,61 @@ const (
 
 // A Channel holds all data related to an individual Discord channel.
 type Channel struct {
-	ID                   string                 `json:"id"`
-	GuildID              string                 `json:"guild_id"`
-	Name                 string                 `json:"name"`
-	Topic                string                 `json:"topic"`
-	Type                 ChannelType            `json:"type"`
-	LastMessageID        string                 `json:"last_message_id"`
-	NSFW                 bool                   `json:"nsfw"`
-	Position             int                    `json:"position"`
-	Bitrate              int                    `json:"bitrate"`
-	Recipients           []*User                `json:"recipients"`
-	Messages             []*Message             `json:"-"`
+	// The ID of the channel.
+	ID string `json:"id"`
+
+	// The ID of the guild to which the channel belongs, if it is in a guild.
+	// Else, this ID is empty (e.g. DM channels).
+	GuildID string `json:"guild_id"`
+
+	// The name of the channel.
+	Name string `json:"name"`
+
+	// The topic of the channel.
+	Topic string `json:"topic"`
+
+	// The type of the channel.
+	Type ChannelType `json:"type"`
+
+	// The ID of the last message sent in the channel. This is not
+	// guaranteed to be an ID of a valid message.
+	LastMessageID string `json:"last_message_id"`
+
+	// Whether the channel is marked as NSFW.
+	NSFW bool `json:"nsfw"`
+
+	// Icon of the group DM channel.
+	Icon string `json:"icon"`
+
+	// The position of the channel, used for sorting in client.
+	Position int `json:"position"`
+
+	// The bitrate of the channel, if it is a voice channel.
+	Bitrate int `json:"bitrate"`
+
+	// The recipients of the channel. This is only populated in DM channels.
+	Recipients []*User `json:"recipients"`
+
+	// The messages in the channel. This is only present in state-cached channels,
+	// and State.MaxMessageCount must be non-zero.
+	Messages []*Message `json:"-"`
+
+	// A list of permission overwrites present for the channel.
 	PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites"`
-	ParentID             string                 `json:"parent_id"`
+
+	// The user limit of the voice channel.
+	UserLimit int `json:"user_limit"`
+
+	// The ID of the parent channel, if the channel is under a category
+	ParentID string `json:"parent_id"`
+}
+
+// Mention returns a string which mentions the channel
+func (c *Channel) Mention() string {
+	return fmt.Sprintf("<#%s>", c.ID)
 }
 
-// A ChannelEdit holds Channel Feild data for a channel edit.
+// A ChannelEdit holds Channel Field data for a channel edit.
 type ChannelEdit struct {
 	Name                 string                 `json:"name,omitempty"`
 	Topic                string                 `json:"topic,omitempty"`
@@ -186,6 +264,7 @@ type ChannelEdit struct {
 	UserLimit            int                    `json:"user_limit,omitempty"`
 	PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites,omitempty"`
 	ParentID             string                 `json:"parent_id,omitempty"`
+	RateLimitPerUser     int                    `json:"rate_limit_per_user,omitempty"`
 }
 
 // A PermissionOverwrite holds permission overwrite data for a Channel
@@ -206,6 +285,19 @@ type Emoji struct {
 	Animated      bool     `json:"animated"`
 }
 
+// MessageFormat returns a correctly formatted Emoji for use in Message content and embeds
+func (e *Emoji) MessageFormat() string {
+	if e.ID != "" && e.Name != "" {
+		if e.Animated {
+			return "<a:" + e.APIName() + ">"
+		}
+
+		return "<:" + e.APIName() + ">"
+	}
+
+	return e.APIName()
+}
+
 // APIName returns an correctly formatted API name for use in the MessageReactions endpoints.
 func (e *Emoji) APIName() string {
 	if e.ID != "" && e.Name != "" {
@@ -228,31 +320,129 @@ const (
 	VerificationLevelHigh
 )
 
+// ExplicitContentFilterLevel type definition
+type ExplicitContentFilterLevel int
+
+// Constants for ExplicitContentFilterLevel levels from 0 to 2 inclusive
+const (
+	ExplicitContentFilterDisabled ExplicitContentFilterLevel = iota
+	ExplicitContentFilterMembersWithoutRoles
+	ExplicitContentFilterAllMembers
+)
+
+// MfaLevel type definition
+type MfaLevel int
+
+// Constants for MfaLevel levels from 0 to 1 inclusive
+const (
+	MfaLevelNone MfaLevel = iota
+	MfaLevelElevated
+)
+
 // A Guild holds all data related to a specific Discord Guild.  Guilds are also
 // sometimes referred to as Servers in the Discord client.
 type Guild struct {
-	ID                          string            `json:"id"`
-	Name                        string            `json:"name"`
-	Icon                        string            `json:"icon"`
-	Region                      string            `json:"region"`
-	AfkChannelID                string            `json:"afk_channel_id"`
-	EmbedChannelID              string            `json:"embed_channel_id"`
-	OwnerID                     string            `json:"owner_id"`
-	JoinedAt                    Timestamp         `json:"joined_at"`
-	Splash                      string            `json:"splash"`
-	AfkTimeout                  int               `json:"afk_timeout"`
-	MemberCount                 int               `json:"member_count"`
-	VerificationLevel           VerificationLevel `json:"verification_level"`
-	EmbedEnabled                bool              `json:"embed_enabled"`
-	Large                       bool              `json:"large"` // ??
-	DefaultMessageNotifications int               `json:"default_message_notifications"`
-	Roles                       []*Role           `json:"roles"`
-	Emojis                      []*Emoji          `json:"emojis"`
-	Members                     []*Member         `json:"members"`
-	Presences                   []*Presence       `json:"presences"`
-	Channels                    []*Channel        `json:"channels"`
-	VoiceStates                 []*VoiceState     `json:"voice_states"`
-	Unavailable                 bool              `json:"unavailable"`
+	// The ID of the guild.
+	ID string `json:"id"`
+
+	// The name of the guild. (2–100 characters)
+	Name string `json:"name"`
+
+	// The hash of the guild's icon. Use Session.GuildIcon
+	// to retrieve the icon itself.
+	Icon string `json:"icon"`
+
+	// The voice region of the guild.
+	Region string `json:"region"`
+
+	// The ID of the AFK voice channel.
+	AfkChannelID string `json:"afk_channel_id"`
+
+	// The ID of the embed channel ID, used for embed widgets.
+	EmbedChannelID string `json:"embed_channel_id"`
+
+	// The user ID of the owner of the guild.
+	OwnerID string `json:"owner_id"`
+
+	// The time at which the current user joined the guild.
+	// This field is only present in GUILD_CREATE events and websocket
+	// update events, and thus is only present in state-cached guilds.
+	JoinedAt Timestamp `json:"joined_at"`
+
+	// The hash of the guild's splash.
+	Splash string `json:"splash"`
+
+	// The timeout, in seconds, before a user is considered AFK in voice.
+	AfkTimeout int `json:"afk_timeout"`
+
+	// The number of members in the guild.
+	// This field is only present in GUILD_CREATE events and websocket
+	// update events, and thus is only present in state-cached guilds.
+	MemberCount int `json:"member_count"`
+
+	// The verification level required for the guild.
+	VerificationLevel VerificationLevel `json:"verification_level"`
+
+	// Whether the guild has embedding enabled.
+	EmbedEnabled bool `json:"embed_enabled"`
+
+	// Whether the guild is considered large. This is
+	// determined by a member threshold in the identify packet,
+	// and is currently hard-coded at 250 members in the library.
+	Large bool `json:"large"`
+
+	// The default message notification setting for the guild.
+	// 0 == all messages, 1 == mentions only.
+	DefaultMessageNotifications int `json:"default_message_notifications"`
+
+	// A list of roles in the guild.
+	Roles []*Role `json:"roles"`
+
+	// A list of the custom emojis present in the guild.
+	Emojis []*Emoji `json:"emojis"`
+
+	// A list of the members in the guild.
+	// This field is only present in GUILD_CREATE events and websocket
+	// update events, and thus is only present in state-cached guilds.
+	Members []*Member `json:"members"`
+
+	// A list of partial presence objects for members in the guild.
+	// This field is only present in GUILD_CREATE events and websocket
+	// update events, and thus is only present in state-cached guilds.
+	Presences []*Presence `json:"presences"`
+
+	// A list of channels in the guild.
+	// This field is only present in GUILD_CREATE events and websocket
+	// update events, and thus is only present in state-cached guilds.
+	Channels []*Channel `json:"channels"`
+
+	// A list of voice states for the guild.
+	// This field is only present in GUILD_CREATE events and websocket
+	// update events, and thus is only present in state-cached guilds.
+	VoiceStates []*VoiceState `json:"voice_states"`
+
+	// Whether this guild is currently unavailable (most likely due to outage).
+	// This field is only present in GUILD_CREATE events and websocket
+	// update events, and thus is only present in state-cached guilds.
+	Unavailable bool `json:"unavailable"`
+
+	// The explicit content filter level
+	ExplicitContentFilter ExplicitContentFilterLevel `json:"explicit_content_filter"`
+
+	// The list of enabled guild features
+	Features []string `json:"features"`
+
+	// Required MFA level for the guild
+	MfaLevel MfaLevel `json:"mfa_level"`
+
+	// Whether or not the Server Widget is enabled
+	WidgetEnabled bool `json:"widget_enabled"`
+
+	// The Channel ID for the Server Widget
+	WidgetChannelID string `json:"widget_channel_id"`
+
+	// The Channel ID to which system messages are sent (eg join and leave messages)
+	SystemChannelID string `json:"system_channel_id"`
 }
 
 // A UserGuild holds a brief version of a Guild
@@ -279,14 +469,37 @@ type GuildParams struct {
 
 // A Role stores information about Discord guild member roles.
 type Role struct {
-	ID          string `json:"id"`
-	Name        string `json:"name"`
-	Managed     bool   `json:"managed"`
-	Mentionable bool   `json:"mentionable"`
-	Hoist       bool   `json:"hoist"`
-	Color       int    `json:"color"`
-	Position    int    `json:"position"`
-	Permissions int    `json:"permissions"`
+	// The ID of the role.
+	ID string `json:"id"`
+
+	// The name of the role.
+	Name string `json:"name"`
+
+	// Whether this role is managed by an integration, and
+	// thus cannot be manually added to, or taken from, members.
+	Managed bool `json:"managed"`
+
+	// Whether this role is mentionable.
+	Mentionable bool `json:"mentionable"`
+
+	// Whether this role is hoisted (shows up separately in member list).
+	Hoist bool `json:"hoist"`
+
+	// The hex color of this role.
+	Color int `json:"color"`
+
+	// The position of this role in the guild's role hierarchy.
+	Position int `json:"position"`
+
+	// The permissions of the role on the guild (doesn't include channel overrides).
+	// 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 permission.
+	Permissions int `json:"permissions"`
+}
+
+// Mention returns a string which mentions the role
+func (r *Role) Mention() string {
+	return fmt.Sprintf("<@&%s>", r.ID)
 }
 
 // Roles are a collection of Role
@@ -334,6 +547,8 @@ type GameType int
 const (
 	GameTypeGame GameType = iota
 	GameTypeStreaming
+	GameTypeListening
+	GameTypeWatching
 )
 
 // A Game struct holds the name of the "playing .." game for a user
@@ -379,15 +594,34 @@ type Assets struct {
 	SmallText    string `json:"small_text,omitempty"`
 }
 
-// A Member stores user information for Guild members.
+// A Member stores user information for Guild members. A guild
+// member represents a certain user's presence in a guild.
 type Member struct {
-	GuildID  string   `json:"guild_id"`
-	JoinedAt string   `json:"joined_at"`
-	Nick     string   `json:"nick"`
-	Deaf     bool     `json:"deaf"`
-	Mute     bool     `json:"mute"`
-	User     *User    `json:"user"`
-	Roles    []string `json:"roles"`
+	// The guild ID on which the member exists.
+	GuildID string `json:"guild_id"`
+
+	// The time at which the member joined the guild, in ISO8601.
+	JoinedAt Timestamp `json:"joined_at"`
+
+	// The nickname of the member, if they have one.
+	Nick string `json:"nick"`
+
+	// Whether the member is deafened at a guild level.
+	Deaf bool `json:"deaf"`
+
+	// Whether the member is muted at a guild level.
+	Mute bool `json:"mute"`
+
+	// The underlying user on which the member is based.
+	User *User `json:"user"`
+
+	// A list of IDs of the roles which are possessed by the member.
+	Roles []string `json:"roles"`
+}
+
+// Mention creates a member mention
+func (m *Member) Mention() string {
+	return "<@!" + m.User.ID + ">"
 }
 
 // A Settings stores data for a specific users Discord client settings.
@@ -467,33 +701,88 @@ type GuildBan struct {
 	User   *User  `json:"user"`
 }
 
-// A GuildIntegration stores data for a guild integration.
-type GuildIntegration struct {
-	ID                string                   `json:"id"`
-	Name              string                   `json:"name"`
-	Type              string                   `json:"type"`
-	Enabled           bool                     `json:"enabled"`
-	Syncing           bool                     `json:"syncing"`
-	RoleID            string                   `json:"role_id"`
-	ExpireBehavior    int                      `json:"expire_behavior"`
-	ExpireGracePeriod int                      `json:"expire_grace_period"`
-	User              *User                    `json:"user"`
-	Account           *GuildIntegrationAccount `json:"account"`
-	SyncedAt          int                      `json:"synced_at"`
-}
-
-// A GuildIntegrationAccount stores data for a guild integration account.
-type GuildIntegrationAccount struct {
-	ID   string `json:"id"`
-	Name string `json:"name"`
-}
-
 // A GuildEmbed stores data for a guild embed.
 type GuildEmbed struct {
 	Enabled   bool   `json:"enabled"`
 	ChannelID string `json:"channel_id"`
 }
 
+// A GuildAuditLog stores data for a guild audit log.
+type GuildAuditLog struct {
+	Webhooks []struct {
+		ChannelID string `json:"channel_id"`
+		GuildID   string `json:"guild_id"`
+		ID        string `json:"id"`
+		Avatar    string `json:"avatar"`
+		Name      string `json:"name"`
+	} `json:"webhooks,omitempty"`
+	Users []struct {
+		Username      string `json:"username"`
+		Discriminator string `json:"discriminator"`
+		Bot           bool   `json:"bot"`
+		ID            string `json:"id"`
+		Avatar        string `json:"avatar"`
+	} `json:"users,omitempty"`
+	AuditLogEntries []struct {
+		TargetID string `json:"target_id"`
+		Changes  []struct {
+			NewValue interface{} `json:"new_value"`
+			OldValue interface{} `json:"old_value"`
+			Key      string      `json:"key"`
+		} `json:"changes,omitempty"`
+		UserID     string `json:"user_id"`
+		ID         string `json:"id"`
+		ActionType int    `json:"action_type"`
+		Options    struct {
+			DeleteMembersDay string `json:"delete_member_days"`
+			MembersRemoved   string `json:"members_removed"`
+			ChannelID        string `json:"channel_id"`
+			Count            string `json:"count"`
+			ID               string `json:"id"`
+			Type             string `json:"type"`
+			RoleName         string `json:"role_name"`
+		} `json:"options,omitempty"`
+		Reason string `json:"reason"`
+	} `json:"audit_log_entries"`
+}
+
+// Block contains Discord Audit Log Action Types
+const (
+	AuditLogActionGuildUpdate = 1
+
+	AuditLogActionChannelCreate          = 10
+	AuditLogActionChannelUpdate          = 11
+	AuditLogActionChannelDelete          = 12
+	AuditLogActionChannelOverwriteCreate = 13
+	AuditLogActionChannelOverwriteUpdate = 14
+	AuditLogActionChannelOverwriteDelete = 15
+
+	AuditLogActionMemberKick       = 20
+	AuditLogActionMemberPrune      = 21
+	AuditLogActionMemberBanAdd     = 22
+	AuditLogActionMemberBanRemove  = 23
+	AuditLogActionMemberUpdate     = 24
+	AuditLogActionMemberRoleUpdate = 25
+
+	AuditLogActionRoleCreate = 30
+	AuditLogActionRoleUpdate = 31
+	AuditLogActionRoleDelete = 32
+
+	AuditLogActionInviteCreate = 40
+	AuditLogActionInviteUpdate = 41
+	AuditLogActionInviteDelete = 42
+
+	AuditLogActionWebhookCreate = 50
+	AuditLogActionWebhookUpdate = 51
+	AuditLogActionWebhookDelete = 52
+
+	AuditLogActionEmojiCreate = 60
+	AuditLogActionEmojiUpdate = 61
+	AuditLogActionEmojiDelete = 62
+
+	AuditLogActionMessageDelete = 72
+)
+
 // A UserGuildSettingsChannelOverride stores data for a channel override for a users guild settings.
 type UserGuildSettingsChannelOverride struct {
 	Muted                bool   `json:"muted"`
@@ -553,6 +842,7 @@ type MessageReaction struct {
 	MessageID string `json:"message_id"`
 	Emoji     Emoji  `json:"emoji"`
 	ChannelID string `json:"channel_id"`
+	GuildID   string `json:"guild_id,omitempty"`
 }
 
 // GatewayBotResponse stores the data for the gateway/bot response
@@ -629,7 +919,9 @@ const (
 		PermissionKickMembers |
 		PermissionBanMembers |
 		PermissionManageServer |
-		PermissionAdministrator
+		PermissionAdministrator |
+		PermissionManageWebhooks |
+		PermissionManageEmojis
 )
 
 // Block contains Discord JSON Error Response codes
@@ -648,6 +940,7 @@ const (
 	ErrCodeUnknownToken       = 10012
 	ErrCodeUnknownUser        = 10013
 	ErrCodeUnknownEmoji       = 10014
+	ErrCodeUnknownWebhook     = 10015
 
 	ErrCodeBotsCannotUseEndpoint  = 20001
 	ErrCodeOnlyBotsCanUseEndpoint = 20002

+ 1 - 2
types.go

@@ -11,7 +11,6 @@ package discordgo
 
 import (
 	"encoding/json"
-	"fmt"
 	"net/http"
 	"time"
 )
@@ -54,5 +53,5 @@ func newRestError(req *http.Request, resp *http.Response, body []byte) *RESTErro
 }
 
 func (r RESTError) Error() string {
-	return fmt.Sprintf("HTTP %s, %s", r.Response.Status, r.ResponseBody)
+	return "HTTP " + r.Response.Status + ", " + string(r.ResponseBody)
 }

+ 37 - 15
user.go

@@ -1,31 +1,51 @@
 package discordgo
 
-import (
-	"fmt"
-	"strings"
-)
+import "strings"
 
 // A User stores all data for an individual Discord user.
 type User struct {
-	ID            string `json:"id"`
-	Email         string `json:"email"`
-	Username      string `json:"username"`
-	Avatar        string `json:"avatar"`
+	// The ID of the user.
+	ID string `json:"id"`
+
+	// The email of the user. This is only present when
+	// the application possesses the email scope for the user.
+	Email string `json:"email"`
+
+	// The user's username.
+	Username string `json:"username"`
+
+	// The hash of the user's avatar. Use Session.UserAvatar
+	// to retrieve the avatar itself.
+	Avatar string `json:"avatar"`
+
+	// The user's chosen language option.
+	Locale string `json:"locale"`
+
+	// The discriminator of the user (4 numbers after name).
 	Discriminator string `json:"discriminator"`
-	Token         string `json:"token"`
-	Verified      bool   `json:"verified"`
-	MFAEnabled    bool   `json:"mfa_enabled"`
-	Bot           bool   `json:"bot"`
+
+	// The token of the user. This is only present for
+	// the user represented by the current session.
+	Token string `json:"token"`
+
+	// Whether the user's email is verified.
+	Verified bool `json:"verified"`
+
+	// Whether the user has multi-factor authentication enabled.
+	MFAEnabled bool `json:"mfa_enabled"`
+
+	// Whether the user is a bot.
+	Bot bool `json:"bot"`
 }
 
 // String returns a unique identifier of the form username#discriminator
 func (u *User) String() string {
-	return fmt.Sprintf("%s#%s", u.Username, u.Discriminator)
+	return u.Username + "#" + u.Discriminator
 }
 
 // Mention return a string which mentions the user
 func (u *User) Mention() string {
-	return fmt.Sprintf("<@%s>", u.ID)
+	return "<@" + u.ID + ">"
 }
 
 // AvatarURL returns a URL to the user's avatar.
@@ -34,7 +54,9 @@ func (u *User) Mention() string {
 //             be added to the URL.
 func (u *User) AvatarURL(size string) string {
 	var URL string
-	if strings.HasPrefix(u.Avatar, "a_") {
+	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)

+ 5 - 5
voice.go

@@ -14,6 +14,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"net"
+	"strconv"
 	"strings"
 	"sync"
 	"time"
@@ -103,7 +104,7 @@ func (v *VoiceConnection) Speaking(b bool) (err error) {
 	defer v.Unlock()
 	if err != nil {
 		v.speaking = false
-		v.log(LogError, "Speaking() write json error:", err)
+		v.log(LogError, "Speaking() write json error, %s", err)
 		return
 	}
 
@@ -135,7 +136,6 @@ func (v *VoiceConnection) ChangeChannel(channelID string, mute, deaf bool) (err
 
 // Disconnect disconnects from this voice channel and closes the websocket
 // and udp connections to Discord.
-// !!! NOTE !!! this function may be removed in favour of ChannelVoiceLeave
 func (v *VoiceConnection) Disconnect() (err error) {
 
 	// Send a OP4 with a nil channel to disconnect
@@ -180,7 +180,7 @@ func (v *VoiceConnection) Close() {
 		v.log(LogInformational, "closing udp")
 		err := v.udpConn.Close()
 		if err != nil {
-			v.log(LogError, "error closing udp connection: ", err)
+			v.log(LogError, "error closing udp connection, %s", err)
 		}
 		v.udpConn = nil
 	}
@@ -299,7 +299,7 @@ func (v *VoiceConnection) open() (err error) {
 	}
 
 	// Connect to VoiceConnection Websocket
-	vg := fmt.Sprintf("wss://%s", strings.TrimSuffix(v.endpoint, ":80"))
+	vg := "wss://" + strings.TrimSuffix(v.endpoint, ":80")
 	v.log(LogInformational, "connecting to voice endpoint %s", vg)
 	v.wsConn, _, err = websocket.DefaultDialer.Dial(vg, nil)
 	if err != nil {
@@ -542,7 +542,7 @@ func (v *VoiceConnection) udpOpen() (err error) {
 		return fmt.Errorf("empty endpoint")
 	}
 
-	host := fmt.Sprintf("%s:%d", strings.TrimSuffix(v.endpoint, ":80"), v.op2.Port)
+	host := strings.TrimSuffix(v.endpoint, ":80") + ":" + strconv.Itoa(v.op2.Port)
 	addr, err := net.ResolveUDPAddr("udp", host)
 	if err != nil {
 		v.log(LogWarning, "error resolving udp host %s, %s", host, err)

+ 70 - 29
wsapi.go

@@ -86,6 +86,10 @@ func (s *Session) Open() error {
 		return err
 	}
 
+	s.wsConn.SetCloseHandler(func(code int, text string) error {
+		return nil
+	})
+
 	defer func() {
 		// because of this, all code below must set err to the error
 		// when exiting with an error :)  Maybe someone has a better
@@ -263,6 +267,13 @@ type helloOp struct {
 // FailedHeartbeatAcks is the Number of heartbeat intervals to wait until forcing a connection restart.
 const FailedHeartbeatAcks time.Duration = 5 * time.Millisecond
 
+// HeartbeatLatency returns the latency between heartbeat acknowledgement and heartbeat send.
+func (s *Session) HeartbeatLatency() time.Duration {
+
+	return s.LastHeartbeatAck.Sub(s.LastHeartbeatSent)
+
+}
+
 // heartbeat sends regular heartbeats to Discord so it knows the client
 // is still connected.  If you do not send these heartbeats Discord will
 // disconnect the websocket connection after a few seconds.
@@ -283,8 +294,9 @@ func (s *Session) heartbeat(wsConn *websocket.Conn, listening <-chan interface{}
 		last := s.LastHeartbeatAck
 		s.RUnlock()
 		sequence := atomic.LoadInt64(s.sequence)
-		s.log(LogInformational, "sending gateway websocket heartbeat seq %d", sequence)
+		s.log(LogDebug, "sending gateway websocket heartbeat seq %d", sequence)
 		s.wsMutex.Lock()
+		s.LastHeartbeatSent = time.Now().UTC()
 		err = wsConn.WriteJSON(heartbeatOp{1, sequence})
 		s.wsMutex.Unlock()
 		if err != nil || time.Now().UTC().Sub(last) > (heartbeatIntervalMsec*FailedHeartbeatAcks) {
@@ -323,16 +335,8 @@ type updateStatusOp struct {
 	Data UpdateStatusData `json:"d"`
 }
 
-// UpdateStreamingStatus is used to update the user's streaming status.
-// If idle>0 then set status to idle.
-// If game!="" then set game.
-// If game!="" and url!="" then set the status type to streaming with the URL set.
-// if otherwise, set status to active, and no game.
-func (s *Session) UpdateStreamingStatus(idle int, game string, url string) (err error) {
-
-	s.log(LogInformational, "called")
-
-	usd := UpdateStatusData{
+func newUpdateStatusData(idle int, gameType GameType, game, url string) *UpdateStatusData {
+	usd := &UpdateStatusData{
 		Status: "online",
 	}
 
@@ -341,10 +345,6 @@ func (s *Session) UpdateStreamingStatus(idle int, game string, url string) (err
 	}
 
 	if game != "" {
-		gameType := GameTypeGame
-		if url != "" {
-			gameType = GameTypeStreaming
-		}
 		usd.Game = &Game{
 			Name: game,
 			Type: gameType,
@@ -352,7 +352,35 @@ func (s *Session) UpdateStreamingStatus(idle int, game string, url string) (err
 		}
 	}
 
-	return s.UpdateStatusComplex(usd)
+	return usd
+}
+
+// UpdateStatus is used to update the user's status.
+// If idle>0 then set status to idle.
+// If game!="" then set game.
+// if otherwise, set status to active, and no game.
+func (s *Session) UpdateStatus(idle int, game string) (err error) {
+	return s.UpdateStatusComplex(*newUpdateStatusData(idle, GameTypeGame, game, ""))
+}
+
+// UpdateStreamingStatus is used to update the user's streaming status.
+// If idle>0 then set status to idle.
+// If game!="" then set game.
+// If game!="" and url!="" then set the status type to streaming with the URL set.
+// if otherwise, set status to active, and no game.
+func (s *Session) UpdateStreamingStatus(idle int, game string, url string) (err error) {
+	gameType := GameTypeGame
+	if url != "" {
+		gameType = GameTypeStreaming
+	}
+	return s.UpdateStatusComplex(*newUpdateStatusData(idle, gameType, game, url))
+}
+
+// UpdateListeningStatus is used to set the user to "Listening to..."
+// If game!="" then set to what user is listening to
+// Else, set user to active and no game.
+func (s *Session) UpdateListeningStatus(game string) (err error) {
+	return s.UpdateStatusComplex(*newUpdateStatusData(0, GameTypeListening, game, ""))
 }
 
 // UpdateStatusComplex allows for sending the raw status update data untouched by discordgo.
@@ -371,14 +399,6 @@ func (s *Session) UpdateStatusComplex(usd UpdateStatusData) (err error) {
 	return
 }
 
-// UpdateStatus is used to update the user's status.
-// If idle>0 then set status to idle.
-// If game!="" then set game.
-// if otherwise, set status to active, and no game.
-func (s *Session) UpdateStatus(idle int, game string) (err error) {
-	return s.UpdateStreamingStatus(idle, game, "")
-}
-
 type requestGuildMembersData struct {
 	GuildID string `json:"guild_id"`
 	Query   string `json:"query"`
@@ -508,7 +528,7 @@ func (s *Session) onEvent(messageType int, message []byte) (*Event, error) {
 		s.Lock()
 		s.LastHeartbeatAck = time.Now().UTC()
 		s.Unlock()
-		s.log(LogInformational, "got heartbeat ACK")
+		s.log(LogDebug, "got heartbeat ACK")
 		return e, nil
 	}
 
@@ -615,6 +635,30 @@ func (s *Session) ChannelVoiceJoin(gID, cID string, mute, deaf bool) (voice *Voi
 	return
 }
 
+// ChannelVoiceJoinManual initiates a voice session to a voice channel, but does not complete it.
+//
+// This should only be used when the VoiceServerUpdate will be intercepted and used elsewhere.
+//
+//    gID     : Guild ID of the channel to join.
+//    cID     : Channel ID of the channel to join.
+//    mute    : If true, you will be set to muted upon joining.
+//    deaf    : If true, you will be set to deafened upon joining.
+func (s *Session) ChannelVoiceJoinManual(gID, cID string, mute, deaf bool) (err error) {
+
+	s.log(LogInformational, "called")
+
+	// Send the request to Discord that we want to join the voice channel
+	data := voiceChannelJoinOp{4, voiceChannelJoinData{&gID, &cID, mute, deaf}}
+	s.wsMutex.Lock()
+	err = s.wsConn.WriteJSON(data)
+	s.wsMutex.Unlock()
+	if err != nil {
+		return
+	}
+
+	return
+}
+
 // onVoiceStateUpdate handles Voice State Update events on the data websocket.
 func (s *Session) onVoiceStateUpdate(st *VoiceStateUpdate) {
 
@@ -732,11 +776,8 @@ func (s *Session) identify() error {
 	s.wsMutex.Lock()
 	err := s.wsConn.WriteJSON(op)
 	s.wsMutex.Unlock()
-	if err != nil {
-		return err
-	}
 
-	return nil
+	return err
 }
 
 func (s *Session) reconnect() {