Update vendor bwmarrin/discordgo

This commit is contained in:
Wim 2018-02-14 22:22:35 +01:00
parent 2522158127
commit fd0fe3390b
13 changed files with 328 additions and 200 deletions

View File

@ -21,7 +21,7 @@ import (
) )
// VERSION of DiscordGo, follows Semantic Versioning. (http://semver.org/) // VERSION of DiscordGo, follows Semantic Versioning. (http://semver.org/)
const VERSION = "0.17.0" const VERSION = "0.18.0"
// ErrMFA will be risen by New when the user has 2FA. // ErrMFA will be risen by New when the user has 2FA.
var ErrMFA = errors.New("account has 2FA enabled") var ErrMFA = errors.New("account has 2FA enabled")
@ -50,7 +50,7 @@ func New(args ...interface{}) (s *Session, err error) {
// Create an empty Session interface. // Create an empty Session interface.
s = &Session{ s = &Session{
State: NewState(), State: NewState(),
ratelimiter: NewRatelimiter(), Ratelimiter: NewRatelimiter(),
StateEnabled: true, StateEnabled: true,
Compress: true, Compress: true,
ShouldReconnectOnError: true, ShouldReconnectOnError: true,

View File

@ -71,7 +71,6 @@ var (
EndpointUserNotes = func(uID string) string { return EndpointUsers + "@me/notes/" + uID } EndpointUserNotes = func(uID string) string { return EndpointUsers + "@me/notes/" + uID }
EndpointGuild = func(gID string) string { return EndpointGuilds + gID } EndpointGuild = func(gID string) string { return EndpointGuilds + gID }
EndpointGuildInivtes = func(gID string) string { return EndpointGuilds + gID + "/invites" }
EndpointGuildChannels = func(gID string) string { return EndpointGuilds + gID + "/channels" } EndpointGuildChannels = func(gID string) string { return EndpointGuilds + gID + "/channels" }
EndpointGuildMembers = func(gID string) string { return EndpointGuilds + gID + "/members" } EndpointGuildMembers = func(gID string) string { return EndpointGuilds + gID + "/members" }
EndpointGuildMember = func(gID, uID string) string { return EndpointGuilds + gID + "/members/" + uID } EndpointGuildMember = func(gID, uID string) string { return EndpointGuilds + gID + "/members/" + uID }
@ -98,7 +97,7 @@ var (
EndpointChannelMessages = func(cID string) string { return EndpointChannels + cID + "/messages" } EndpointChannelMessages = func(cID string) string { return EndpointChannels + cID + "/messages" }
EndpointChannelMessage = func(cID, mID string) string { return EndpointChannels + cID + "/messages/" + mID } EndpointChannelMessage = func(cID, mID string) string { return EndpointChannels + cID + "/messages/" + mID }
EndpointChannelMessageAck = func(cID, mID string) string { return EndpointChannels + cID + "/messages/" + mID + "/ack" } EndpointChannelMessageAck = func(cID, mID string) string { return EndpointChannels + cID + "/messages/" + mID + "/ack" }
EndpointChannelMessagesBulkDelete = func(cID string) string { return EndpointChannel(cID) + "/messages/bulk_delete" } EndpointChannelMessagesBulkDelete = func(cID string) string { return EndpointChannel(cID) + "/messages/bulk-delete" }
EndpointChannelMessagesPins = func(cID string) string { return EndpointChannel(cID) + "/pins" } EndpointChannelMessagesPins = func(cID string) string { return EndpointChannel(cID) + "/pins" }
EndpointChannelMessagePin = func(cID, mID string) string { return EndpointChannel(cID) + "/pins/" + mID } EndpointChannelMessagePin = func(cID, mID string) string { return EndpointChannel(cID) + "/pins/" + mID }
@ -122,6 +121,8 @@ var (
EndpointRelationship = func(uID string) string { return EndpointRelationships() + "/" + uID } EndpointRelationship = func(uID string) string { return EndpointRelationships() + "/" + uID }
EndpointRelationshipsMutual = func(uID string) string { return EndpointUsers + uID + "/relationships" } EndpointRelationshipsMutual = func(uID string) string { return EndpointUsers + uID + "/relationships" }
EndpointGuildCreate = EndpointAPI + "guilds"
EndpointInvite = func(iID string) string { return EndpointAPI + "invite/" + iID } EndpointInvite = func(iID string) string { return EndpointAPI + "invite/" + iID }
EndpointIntegrationsJoin = func(iID string) string { return EndpointAPI + "integrations/" + iID + "/join" } EndpointIntegrationsJoin = func(iID string) string { return EndpointAPI + "integrations/" + iID + "/join" }

View File

@ -6,7 +6,7 @@ type EventHandler interface {
Type() string Type() string
// Handle is called whenever an event of Type() happens. // Handle is called whenever an event of Type() happens.
// It is the recievers responsibility to type assert that the interface // It is the receivers responsibility to type assert that the interface
// is the expected struct. // is the expected struct.
Handle(*Session, interface{}) Handle(*Session, interface{})
} }

View File

@ -79,7 +79,7 @@ func main() {
ap.Name = Name ap.Name = Name
ap, err = dg.ApplicationCreate(ap) ap, err = dg.ApplicationCreate(ap)
if err != nil { if err != nil {
fmt.Println("error creating new applicaiton,", err) fmt.Println("error creating new application,", err)
return return
} }

View File

@ -23,7 +23,7 @@ const (
LogError int = iota LogError int = iota
// LogWarning level is used for very abnormal events and errors that are // LogWarning level is used for very abnormal events and errors that are
// also returend to a calling function. // also returned to a calling function.
LogWarning LogWarning
// LogInformational level is used for normal non-error activity // LogInformational level is used for normal non-error activity
@ -34,14 +34,21 @@ const (
LogDebug LogDebug
) )
// 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 consistancy for discordgo
// the format, a... portion this command follows that of fmt.Printf // the format, a... portion this command follows that of fmt.Printf
// msgL : LogLevel of the message // msgL : LogLevel of the message
// caller : 1 + the number of callers away from the message source // caller : 1 + the number of callers away from the message source
// format : Printf style message format // format : Printf style message format
// a ... : comma seperated list of values to pass // a ... : comma separated list of values to pass
func msglog(msgL, caller int, format string, a ...interface{}) { func msglog(msgL, caller int, format string, a ...interface{}) {
if Logger != nil {
Logger(msgL, caller, format, a...)
} else {
pc, file, line, _ := runtime.Caller(caller) pc, file, line, _ := runtime.Caller(caller)
files := strings.Split(file, "/") files := strings.Split(file, "/")
@ -54,6 +61,7 @@ func msglog(msgL, caller int, format string, a ...interface{}) {
msg := fmt.Sprintf(format, a...) msg := fmt.Sprintf(format, a...)
log.Printf("[DG%d] %s:%d:%s() %s\n", msgL, file, line, name, msg) log.Printf("[DG%d] %s:%d:%s() %s\n", msgL, file, line, name, msg)
}
} }
// helper function that wraps msglog for the Session struct // helper function that wraps msglog for the Session struct

View File

@ -41,8 +41,8 @@ func NewRatelimiter() *RateLimiter {
} }
} }
// getBucket retrieves or creates a bucket // GetBucket retrieves or creates a bucket
func (r *RateLimiter) getBucket(key string) *Bucket { func (r *RateLimiter) GetBucket(key string) *Bucket {
r.Lock() r.Lock()
defer r.Unlock() defer r.Unlock()
@ -51,7 +51,7 @@ func (r *RateLimiter) getBucket(key string) *Bucket {
} }
b := &Bucket{ b := &Bucket{
remaining: 1, Remaining: 1,
Key: key, Key: key,
global: r.global, global: r.global,
} }
@ -68,27 +68,37 @@ func (r *RateLimiter) getBucket(key string) *Bucket {
return b return b
} }
// LockBucket Locks until a request can be made // GetWaitTime returns the duration you should wait for a Bucket
func (r *RateLimiter) LockBucket(bucketID string) *Bucket { func (r *RateLimiter) GetWaitTime(b *Bucket, minRemaining int) time.Duration {
b := r.getBucket(bucketID)
b.Lock()
// If we ran out of calls and the reset time is still ahead of us // If we ran out of calls and the reset time is still ahead of us
// then we need to take it easy and relax a little // then we need to take it easy and relax a little
if b.remaining < 1 && b.reset.After(time.Now()) { if b.Remaining < minRemaining && b.reset.After(time.Now()) {
time.Sleep(b.reset.Sub(time.Now())) return b.reset.Sub(time.Now())
} }
// Check for global ratelimits // Check for global ratelimits
sleepTo := time.Unix(0, atomic.LoadInt64(r.global)) sleepTo := time.Unix(0, atomic.LoadInt64(r.global))
if now := time.Now(); now.Before(sleepTo) { if now := time.Now(); now.Before(sleepTo) {
time.Sleep(sleepTo.Sub(now)) return sleepTo.Sub(now)
} }
b.remaining-- return 0
}
// LockBucket Locks until a request can be made
func (r *RateLimiter) LockBucket(bucketID string) *Bucket {
return r.LockBucketObject(r.GetBucket(bucketID))
}
// LockBucketObject Locks an already resolved bucket until a request can be made
func (r *RateLimiter) LockBucketObject(b *Bucket) *Bucket {
b.Lock()
if wait := r.GetWaitTime(b, 1); wait > 0 {
time.Sleep(wait)
}
b.Remaining--
return b return b
} }
@ -96,13 +106,14 @@ func (r *RateLimiter) LockBucket(bucketID string) *Bucket {
type Bucket struct { type Bucket struct {
sync.Mutex sync.Mutex
Key string Key string
remaining int Remaining int
limit int limit int
reset time.Time reset time.Time
global *int64 global *int64
lastReset time.Time lastReset time.Time
customRateLimit *customRateLimit customRateLimit *customRateLimit
Userdata interface{}
} }
// Release unlocks the bucket and reads the headers to update the buckets ratelimit info // Release unlocks the bucket and reads the headers to update the buckets ratelimit info
@ -113,10 +124,10 @@ func (b *Bucket) Release(headers http.Header) error {
// Check if the bucket uses a custom ratelimiter // Check if the bucket uses a custom ratelimiter
if rl := b.customRateLimit; rl != nil { if rl := b.customRateLimit; rl != nil {
if time.Now().Sub(b.lastReset) >= rl.reset { if time.Now().Sub(b.lastReset) >= rl.reset {
b.remaining = rl.requests - 1 b.Remaining = rl.requests - 1
b.lastReset = time.Now() b.lastReset = time.Now()
} }
if b.remaining < 1 { if b.Remaining < 1 {
b.reset = time.Now().Add(rl.reset) b.reset = time.Now().Add(rl.reset)
} }
return nil return nil
@ -176,7 +187,7 @@ func (b *Bucket) Release(headers http.Header) error {
if err != nil { if err != nil {
return err return err
} }
b.remaining = int(parsedRemaining) b.Remaining = int(parsedRemaining)
} }
return nil return nil

View File

@ -65,9 +65,11 @@ func (s *Session) request(method, urlStr, contentType string, b []byte, bucketID
if bucketID == "" { if bucketID == "" {
bucketID = strings.SplitN(urlStr, "?", 2)[0] bucketID = strings.SplitN(urlStr, "?", 2)[0]
} }
return s.RequestWithLockedBucket(method, urlStr, contentType, b, s.Ratelimiter.LockBucket(bucketID), sequence)
}
bucket := s.ratelimiter.LockBucket(bucketID) // RequestWithLockedBucket makes a request using a bucket that's already been locked
func (s *Session) RequestWithLockedBucket(method, urlStr, contentType string, b []byte, bucket *Bucket, sequence int) (response []byte, err error) {
if s.Debug { if s.Debug {
log.Printf("API REQUEST %8s :: %s\n", method, urlStr) log.Printf("API REQUEST %8s :: %s\n", method, urlStr)
log.Printf("API REQUEST PAYLOAD :: [%s]\n", string(b)) log.Printf("API REQUEST PAYLOAD :: [%s]\n", string(b))
@ -139,7 +141,7 @@ func (s *Session) request(method, urlStr, contentType string, b []byte, bucketID
if sequence < s.MaxRestRetries { if sequence < s.MaxRestRetries {
s.log(LogInformational, "%s Failed (%s), Retrying...", urlStr, resp.Status) s.log(LogInformational, "%s Failed (%s), Retrying...", urlStr, resp.Status)
response, err = s.request(method, urlStr, contentType, b, bucketID, sequence+1) response, err = s.RequestWithLockedBucket(method, urlStr, contentType, b, s.Ratelimiter.LockBucketObject(bucket), sequence+1)
} else { } else {
err = fmt.Errorf("Exceeded Max retries HTTP %s, %s", resp.Status, response) err = fmt.Errorf("Exceeded Max retries HTTP %s, %s", resp.Status, response)
} }
@ -158,7 +160,7 @@ func (s *Session) request(method, urlStr, contentType string, b []byte, bucketID
// we can make the above smarter // we can make the above smarter
// this method can cause longer delays than required // this method can cause longer delays than required
response, err = s.request(method, urlStr, contentType, b, bucketID, sequence) response, err = s.RequestWithLockedBucket(method, urlStr, contentType, b, s.Ratelimiter.LockBucketObject(bucket), sequence)
default: // Error condition default: // Error condition
err = newRestError(req, resp, response) err = newRestError(req, resp, response)
@ -585,7 +587,7 @@ func (s *Session) GuildCreate(name string) (st *Guild, err error) {
Name string `json:"name"` Name string `json:"name"`
}{name} }{name}
body, err := s.RequestWithBucketID("POST", EndpointGuilds, data, EndpointGuilds) body, err := s.RequestWithBucketID("POST", EndpointGuildCreate, data, EndpointGuildCreate)
if err != nil { if err != nil {
return return
} }
@ -907,7 +909,7 @@ func (s *Session) GuildChannelsReorder(guildID string, channels []*Channel) (err
// GuildInvites returns an array of Invite structures for the given guild // GuildInvites returns an array of Invite structures for the given guild
// guildID : The ID of a Guild. // guildID : The ID of a Guild.
func (s *Session) GuildInvites(guildID string) (st []*Invite, err error) { func (s *Session) GuildInvites(guildID string) (st []*Invite, err error) {
body, err := s.RequestWithBucketID("GET", EndpointGuildInvites(guildID), nil, EndpointGuildInivtes(guildID)) body, err := s.RequestWithBucketID("GET", EndpointGuildInvites(guildID), nil, EndpointGuildInvites(guildID))
if err != nil { if err != nil {
return return
} }
@ -957,6 +959,7 @@ func (s *Session) GuildRoleEdit(guildID, roleID, name string, color int, hoist b
// Prevent sending a color int that is too big. // Prevent sending a color int that is too big.
if color > 0xFFFFFF { if color > 0xFFFFFF {
err = fmt.Errorf("color value cannot be larger than 0xFFFFFF") err = fmt.Errorf("color value cannot be larger than 0xFFFFFF")
return nil, err
} }
data := struct { data := struct {
@ -1020,6 +1023,9 @@ func (s *Session) GuildPruneCount(guildID string, days uint32) (count uint32, er
uri := EndpointGuildPrune(guildID) + fmt.Sprintf("?days=%d", days) uri := EndpointGuildPrune(guildID) + fmt.Sprintf("?days=%d", days)
body, err := s.RequestWithBucketID("GET", uri, nil, EndpointGuildPrune(guildID)) body, err := s.RequestWithBucketID("GET", uri, nil, EndpointGuildPrune(guildID))
if err != nil {
return
}
err = unmarshal(body, &p) err = unmarshal(body, &p)
if err != nil { if err != nil {
@ -1204,7 +1210,7 @@ func (s *Session) GuildEmbedEdit(guildID string, enabled bool, channelID string)
// Functions specific to Discord Channels // Functions specific to Discord Channels
// ------------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------------
// Channel returns a Channel strucutre of a specific Channel. // Channel returns a Channel structure of a specific Channel.
// channelID : The ID of the Channel you want returned. // channelID : The ID of the Channel you want returned.
func (s *Session) Channel(channelID string) (st *Channel, err error) { func (s *Session) Channel(channelID string) (st *Channel, err error) {
body, err := s.RequestWithBucketID("GET", EndpointChannel(channelID), nil, EndpointChannel(channelID)) body, err := s.RequestWithBucketID("GET", EndpointChannel(channelID), nil, EndpointChannel(channelID))
@ -1219,12 +1225,16 @@ func (s *Session) Channel(channelID string) (st *Channel, err error) {
// ChannelEdit edits the given channel // ChannelEdit edits the given channel
// channelID : The ID of a Channel // channelID : The ID of a Channel
// name : The new name to assign the channel. // name : The new name to assign the channel.
func (s *Session) ChannelEdit(channelID, name string) (st *Channel, err error) { func (s *Session) ChannelEdit(channelID, name string) (*Channel, error) {
return s.ChannelEditComplex(channelID, &ChannelEdit{
data := struct { Name: name,
Name string `json:"name"` })
}{name} }
// ChannelEditComplex edits an existing channel, replacing the parameters entirely with ChannelEdit struct
// channelID : The ID of a Channel
// data : The channel struct to send
func (s *Session) ChannelEditComplex(channelID string, data *ChannelEdit) (st *Channel, err error) {
body, err := s.RequestWithBucketID("PATCH", EndpointChannel(channelID), data, EndpointChannel(channelID)) body, err := s.RequestWithBucketID("PATCH", EndpointChannel(channelID), data, EndpointChannel(channelID))
if err != nil { if err != nil {
return return
@ -1476,7 +1486,7 @@ func (s *Session) ChannelMessageDelete(channelID, messageID string) (err error)
} }
// ChannelMessagesBulkDelete bulk deletes the messages from the channel for the provided messageIDs. // ChannelMessagesBulkDelete bulk deletes the messages from the channel for the provided messageIDs.
// If only one messageID is in the slice call channelMessageDelete funciton. // If only one messageID is in the slice call channelMessageDelete function.
// If the slice is empty do nothing. // If the slice is empty do nothing.
// channelID : The ID of the channel for the messages to delete. // channelID : The ID of the channel for the messages to delete.
// messages : The IDs of the messages to be deleted. A slice of string IDs. A maximum of 100 messages. // messages : The IDs of the messages to be deleted. A slice of string IDs. A maximum of 100 messages.
@ -1569,16 +1579,14 @@ func (s *Session) ChannelInvites(channelID string) (st []*Invite, err error) {
// ChannelInviteCreate creates a new invite for the given channel. // ChannelInviteCreate creates a new invite for the given channel.
// channelID : The ID of a Channel // channelID : The ID of a Channel
// i : An Invite struct with the values MaxAge, MaxUses, Temporary, // i : An Invite struct with the values MaxAge, MaxUses and Temporary defined.
// and XkcdPass defined.
func (s *Session) ChannelInviteCreate(channelID string, i Invite) (st *Invite, err error) { func (s *Session) ChannelInviteCreate(channelID string, i Invite) (st *Invite, err error) {
data := struct { data := struct {
MaxAge int `json:"max_age"` MaxAge int `json:"max_age"`
MaxUses int `json:"max_uses"` MaxUses int `json:"max_uses"`
Temporary bool `json:"temporary"` Temporary bool `json:"temporary"`
XKCDPass string `json:"xkcdpass"` }{i.MaxAge, i.MaxUses, i.Temporary}
}{i.MaxAge, i.MaxUses, i.Temporary, i.XkcdPass}
body, err := s.RequestWithBucketID("POST", EndpointChannelInvites(channelID), data, EndpointChannelInvites(channelID)) body, err := s.RequestWithBucketID("POST", EndpointChannelInvites(channelID), data, EndpointChannelInvites(channelID))
if err != nil { if err != nil {
@ -1618,7 +1626,7 @@ func (s *Session) ChannelPermissionDelete(channelID, targetID string) (err error
// ------------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------------
// Invite returns an Invite structure of the given invite // Invite returns an Invite structure of the given invite
// inviteID : The invite code (or maybe xkcdpass?) // inviteID : The invite code
func (s *Session) Invite(inviteID string) (st *Invite, err error) { func (s *Session) Invite(inviteID string) (st *Invite, err error) {
body, err := s.RequestWithBucketID("GET", EndpointInvite(inviteID), nil, EndpointInvite("")) body, err := s.RequestWithBucketID("GET", EndpointInvite(inviteID), nil, EndpointInvite(""))
@ -1631,7 +1639,7 @@ func (s *Session) Invite(inviteID string) (st *Invite, err error) {
} }
// InviteDelete deletes an existing invite // InviteDelete deletes an existing invite
// inviteID : the code (or maybe xkcdpass?) of an invite // inviteID : the code of an invite
func (s *Session) InviteDelete(inviteID string) (st *Invite, err error) { func (s *Session) InviteDelete(inviteID string) (st *Invite, err error) {
body, err := s.RequestWithBucketID("DELETE", EndpointInvite(inviteID), nil, EndpointInvite("")) body, err := s.RequestWithBucketID("DELETE", EndpointInvite(inviteID), nil, EndpointInvite(""))
@ -1644,7 +1652,7 @@ func (s *Session) InviteDelete(inviteID string) (st *Invite, err error) {
} }
// InviteAccept accepts an Invite to a Guild or Channel // InviteAccept accepts an Invite to a Guild or Channel
// inviteID : The invite code (or maybe xkcdpass?) // inviteID : The invite code
func (s *Session) InviteAccept(inviteID string) (st *Invite, err error) { func (s *Session) InviteAccept(inviteID string) (st *Invite, err error) {
body, err := s.RequestWithBucketID("POST", EndpointInvite(inviteID), nil, EndpointInvite("")) body, err := s.RequestWithBucketID("POST", EndpointInvite(inviteID), nil, EndpointInvite(""))

View File

@ -531,7 +531,7 @@ func (s *State) PrivateChannel(channelID string) (*Channel, error) {
return s.Channel(channelID) return s.Channel(channelID)
} }
// Channel gets a channel by ID, it will look in all guilds an private channels. // Channel gets a channel by ID, it will look in all guilds and private channels.
func (s *State) Channel(channelID string) (*Channel, error) { func (s *State) Channel(channelID string) (*Channel, error) {
if s == nil { if s == nil {
return nil, ErrNilState return nil, ErrNilState
@ -816,6 +816,13 @@ func (s *State) OnInterface(se *Session, i interface{}) (err error) {
if s.TrackMembers { if s.TrackMembers {
err = s.MemberRemove(t.Member) err = s.MemberRemove(t.Member)
} }
case *GuildMembersChunk:
if s.TrackMembers {
for i := range t.Members {
t.Members[i].GuildID = t.GuildID
err = s.MemberAdd(t.Members[i])
}
}
case *GuildRoleCreate: case *GuildRoleCreate:
if s.TrackRoles { if s.TrackRoles {
err = s.RoleAdd(t.GuildID, t.Role) err = s.RoleAdd(t.GuildID, t.Role)

View File

@ -14,7 +14,6 @@ package discordgo
import ( import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"strconv"
"sync" "sync"
"time" "time"
@ -85,6 +84,9 @@ type Session struct {
// Stores the last HeartbeatAck that was recieved (in UTC) // Stores the last HeartbeatAck that was recieved (in UTC)
LastHeartbeatAck time.Time LastHeartbeatAck time.Time
// used to deal with rate limits
Ratelimiter *RateLimiter
// Event handlers // Event handlers
handlersMu sync.RWMutex handlersMu sync.RWMutex
handlers map[string][]*eventHandlerInstance handlers map[string][]*eventHandlerInstance
@ -96,9 +98,6 @@ type Session struct {
// When nil, the session is not listening. // When nil, the session is not listening.
listening chan interface{} listening chan interface{}
// used to deal with rate limits
ratelimiter *RateLimiter
// sequence tracks the current gateway api websocket sequence number // sequence tracks the current gateway api websocket sequence number
sequence *int64 sequence *int64
@ -143,9 +142,9 @@ type Invite struct {
MaxAge int `json:"max_age"` MaxAge int `json:"max_age"`
Uses int `json:"uses"` Uses int `json:"uses"`
MaxUses int `json:"max_uses"` MaxUses int `json:"max_uses"`
XkcdPass string `json:"xkcdpass"`
Revoked bool `json:"revoked"` Revoked bool `json:"revoked"`
Temporary bool `json:"temporary"` Temporary bool `json:"temporary"`
Unique bool `json:"unique"`
} }
// ChannelType is the type of a Channel // ChannelType is the type of a Channel
@ -171,9 +170,22 @@ type Channel struct {
NSFW bool `json:"nsfw"` NSFW bool `json:"nsfw"`
Position int `json:"position"` Position int `json:"position"`
Bitrate int `json:"bitrate"` Bitrate int `json:"bitrate"`
Recipients []*User `json:"recipient"` Recipients []*User `json:"recipients"`
Messages []*Message `json:"-"` Messages []*Message `json:"-"`
PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites"` PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites"`
ParentID string `json:"parent_id"`
}
// A ChannelEdit holds Channel Feild data for a channel edit.
type ChannelEdit struct {
Name string `json:"name,omitempty"`
Topic string `json:"topic,omitempty"`
NSFW bool `json:"nsfw,omitempty"`
Position int `json:"position"`
Bitrate int `json:"bitrate,omitempty"`
UserLimit int `json:"user_limit,omitempty"`
PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites,omitempty"`
ParentID string `json:"parent_id,omitempty"`
} }
// A PermissionOverwrite holds permission overwrite data for a Channel // A PermissionOverwrite holds permission overwrite data for a Channel
@ -191,6 +203,7 @@ type Emoji struct {
Roles []string `json:"roles"` Roles []string `json:"roles"`
Managed bool `json:"managed"` Managed bool `json:"managed"`
RequireColons bool `json:"require_colons"` RequireColons bool `json:"require_colons"`
Animated bool `json:"animated"`
} }
// APIName returns an correctly formatted API name for use in the MessageReactions endpoints. // APIName returns an correctly formatted API name for use in the MessageReactions endpoints.
@ -204,7 +217,7 @@ func (e *Emoji) APIName() string {
return e.ID return e.ID
} }
// VerificationLevel type defination // VerificationLevel type definition
type VerificationLevel int type VerificationLevel int
// Constants for VerificationLevel levels from 0 to 3 inclusive // Constants for VerificationLevel levels from 0 to 3 inclusive
@ -314,43 +327,56 @@ type Presence struct {
Since *int `json:"since"` Since *int `json:"since"`
} }
// GameType is the type of "game" (see GameType* consts) in the Game struct
type GameType int
// Valid GameType values
const (
GameTypeGame GameType = iota
GameTypeStreaming
)
// A Game struct holds the name of the "playing .." game for a user // A Game struct holds the name of the "playing .." game for a user
type Game struct { type Game struct {
Name string `json:"name"` Name string `json:"name"`
Type int `json:"type"` Type GameType `json:"type"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
Details string `json:"details,omitempty"`
State string `json:"state,omitempty"`
TimeStamps TimeStamps `json:"timestamps,omitempty"`
Assets Assets `json:"assets,omitempty"`
ApplicationID string `json:"application_id,omitempty"`
Instance int8 `json:"instance,omitempty"`
// TODO: Party and Secrets (unknown structure)
} }
// UnmarshalJSON unmarshals json to Game struct // A TimeStamps struct contains start and end times used in the rich presence "playing .." Game
func (g *Game) UnmarshalJSON(bytes []byte) error { type TimeStamps struct {
temp := &struct { EndTimestamp int64 `json:"end,omitempty"`
Name json.Number `json:"name"` StartTimestamp int64 `json:"start,omitempty"`
Type json.RawMessage `json:"type"` }
URL string `json:"url"`
// UnmarshalJSON unmarshals JSON into TimeStamps struct
func (t *TimeStamps) UnmarshalJSON(b []byte) error {
temp := struct {
End float64 `json:"end,omitempty"`
Start float64 `json:"start,omitempty"`
}{} }{}
err := json.Unmarshal(bytes, temp) err := json.Unmarshal(b, &temp)
if err != nil { if err != nil {
return err return err
} }
g.URL = temp.URL t.EndTimestamp = int64(temp.End)
g.Name = temp.Name.String() t.StartTimestamp = int64(temp.Start)
if temp.Type != nil {
err = json.Unmarshal(temp.Type, &g.Type)
if err == nil {
return nil return nil
} }
s := "" // An Assets struct contains assets and labels used in the rich presence "playing .." Game
err = json.Unmarshal(temp.Type, &s) type Assets struct {
if err == nil { LargeImageID string `json:"large_image,omitempty"`
g.Type, err = strconv.Atoi(s) SmallImageID string `json:"small_image,omitempty"`
} LargeText string `json:"large_text,omitempty"`
SmallText string `json:"small_text,omitempty"`
return err
}
return nil
} }
// A Member stores user information for Guild members. // A Member stores user information for Guild members.
@ -383,7 +409,7 @@ type Settings struct {
DeveloperMode bool `json:"developer_mode"` DeveloperMode bool `json:"developer_mode"`
} }
// Status type defination // Status type definition
type Status string type Status string
// Constants for Status with the different current available status // Constants for Status with the different current available status

View File

@ -30,6 +30,8 @@ func (u *User) Mention() string {
// AvatarURL returns a URL to the user's avatar. // AvatarURL returns a URL to the user's avatar.
// size: The size of the user's avatar as a power of two // size: The size of the user's avatar as a power of two
// if size is an empty string, no size parameter will
// be added to the URL.
func (u *User) AvatarURL(size string) string { func (u *User) AvatarURL(size string) string {
var URL string var URL string
if strings.HasPrefix(u.Avatar, "a_") { if strings.HasPrefix(u.Avatar, "a_") {
@ -38,5 +40,8 @@ func (u *User) AvatarURL(size string) string {
URL = EndpointUserAvatar(u.ID, u.Avatar) URL = EndpointUserAvatar(u.ID, u.Avatar)
} }
if size != "" {
return URL + "?size=" + size return URL + "?size=" + size
}
return URL
} }

View File

@ -13,7 +13,6 @@ import (
"encoding/binary" "encoding/binary"
"encoding/json" "encoding/json"
"fmt" "fmt"
"log"
"net" "net"
"strings" "strings"
"sync" "sync"
@ -69,7 +68,7 @@ type VoiceConnection struct {
voiceSpeakingUpdateHandlers []VoiceSpeakingUpdateHandler voiceSpeakingUpdateHandlers []VoiceSpeakingUpdateHandler
} }
// VoiceSpeakingUpdateHandler type provides a function defination for the // VoiceSpeakingUpdateHandler type provides a function definition for the
// VoiceSpeakingUpdate event // VoiceSpeakingUpdate event
type VoiceSpeakingUpdateHandler func(vc *VoiceConnection, vs *VoiceSpeakingUpdate) type VoiceSpeakingUpdateHandler func(vc *VoiceConnection, vs *VoiceSpeakingUpdate)
@ -104,7 +103,7 @@ func (v *VoiceConnection) Speaking(b bool) (err error) {
defer v.Unlock() defer v.Unlock()
if err != nil { if err != nil {
v.speaking = false v.speaking = false
log.Println("Speaking() write json error:", err) v.log(LogError, "Speaking() write json error:", err)
return return
} }
@ -181,7 +180,7 @@ func (v *VoiceConnection) Close() {
v.log(LogInformational, "closing udp") v.log(LogInformational, "closing udp")
err := v.udpConn.Close() err := v.udpConn.Close()
if err != nil { if err != nil {
log.Println("error closing udp connection: ", err) v.log(LogError, "error closing udp connection: ", err)
} }
v.udpConn = nil v.udpConn = nil
} }
@ -247,7 +246,7 @@ type voiceOP2 struct {
} }
// WaitUntilConnected waits for the Voice Connection to // WaitUntilConnected waits for the Voice Connection to
// become ready, if it does not become ready it retuns an err // become ready, if it does not become ready it returns an err
func (v *VoiceConnection) waitUntilConnected() error { func (v *VoiceConnection) waitUntilConnected() error {
v.log(LogInformational, "called") v.log(LogInformational, "called")
@ -858,7 +857,7 @@ func (v *VoiceConnection) reconnect() {
} }
if v.session.DataReady == false || v.session.wsConn == nil { if v.session.DataReady == false || v.session.wsConn == nil {
v.log(LogInformational, "cannot reconenct to channel %s with unready session", v.ChannelID) v.log(LogInformational, "cannot reconnect to channel %s with unready session", v.ChannelID)
continue continue
} }

View File

@ -15,6 +15,7 @@ import (
"compress/zlib" "compress/zlib"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"io" "io"
"net/http" "net/http"
"runtime" "runtime"
@ -45,19 +46,114 @@ type resumePacket struct {
} `json:"d"` } `json:"d"`
} }
// Open opens a websocket connection to Discord. // Open creates a websocket connection to Discord.
func (s *Session) Open() (err error) { // See: https://discordapp.com/developers/docs/topics/gateway#connecting
func (s *Session) Open() error {
s.log(LogInformational, "called") s.log(LogInformational, "called")
var err error
// Prevent Open or other major Session functions from
// being called while Open is still running.
s.Lock() s.Lock()
defer func() { defer s.Unlock()
// If the websock is already open, bail out here.
if s.wsConn != nil {
return ErrWSAlreadyOpen
}
// Get the gateway to use for the Websocket connection
if s.gateway == "" {
s.gateway, err = s.Gateway()
if err != nil { if err != nil {
s.Unlock() return err
}
// Add the version and encoding to the URL
s.gateway = s.gateway + "?v=" + APIVersion + "&encoding=json"
}
// Connect to the Gateway
s.log(LogInformational, "connecting to gateway %s", s.gateway)
header := http.Header{}
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.gateway = "" // clear cached gateway
s.wsConn = nil // Just to be safe.
return err
}
defer func() {
// because of this, all code below must set err to the error
// when exiting with an error :) Maybe someone has a better
// way :)
if err != nil {
s.wsConn.Close()
s.wsConn = nil
} }
}() }()
// The first response from Discord should be an Op 10 (Hello) Packet.
// When processed by onEvent the heartbeat goroutine will be started.
mt, m, err := s.wsConn.ReadMessage()
if err != nil {
return err
}
e, err := s.onEvent(mt, m)
if err != nil {
return err
}
if e.Operation != 10 {
err = fmt.Errorf("expecting Op 10, got Op %d instead", e.Operation)
return err
}
s.log(LogInformational, "Op 10 Hello Packet received from Discord")
s.LastHeartbeatAck = time.Now().UTC()
var h helloOp
if err = json.Unmarshal(e.RawData, &h); err != nil {
err = fmt.Errorf("error unmarshalling helloOp, %s", err)
return err
}
// Now we send either an Op 2 Identity if this is a brand new
// connection or Op 6 Resume if we are resuming an existing connection.
sequence := atomic.LoadInt64(s.sequence)
if s.sessionID == "" && sequence == 0 {
// Send Op 2 Identity Packet
err = s.identify()
if err != nil {
err = fmt.Errorf("error sending identify packet to gateway, %s, %s", s.gateway, err)
return err
}
} else {
// Send Op 6 Resume Packet
p := resumePacket{}
p.Op = 6
p.Data.Token = s.Token
p.Data.SessionID = s.sessionID
p.Data.Sequence = sequence
s.log(LogInformational, "sending resume packet to gateway")
s.wsMutex.Lock()
err = s.wsConn.WriteJSON(p)
s.wsMutex.Unlock()
if err != nil {
err = fmt.Errorf("error sending gateway resume packet, %s, %s", s.gateway, err)
return err
}
}
// A basic state is a hard requirement for Voice. // A basic state is a hard requirement for Voice.
// We create it here so the below READY/RESUMED packet can populate
// the state :)
// XXX: Move to New() func?
if s.State == nil { if s.State == nil {
state := NewState() state := NewState()
state.TrackChannels = false state.TrackChannels = false
@ -68,77 +164,42 @@ func (s *Session) Open() (err error) {
s.State = state s.State = state
} }
if s.wsConn != nil { // Now Discord should send us a READY or RESUMED packet.
err = ErrWSAlreadyOpen mt, m, err = s.wsConn.ReadMessage()
return if err != nil {
return err
} }
e, err = s.onEvent(mt, m)
if err != nil {
return err
}
if e.Type != `READY` && e.Type != `RESUMED` {
// This is not fatal, but it does not follow their API documentation.
s.log(LogWarning, "Expected READY/RESUMED, instead got:\n%#v\n", e)
}
s.log(LogInformational, "First Packet:\n%#v\n", e)
s.log(LogInformational, "We are now connected to Discord, emitting connect event")
s.handleEvent(connectEventType, &Connect{})
// A VoiceConnections map is a hard requirement for Voice.
// XXX: can this be moved to when opening a voice connection?
if s.VoiceConnections == nil { if s.VoiceConnections == nil {
s.log(LogInformational, "creating new VoiceConnections map") s.log(LogInformational, "creating new VoiceConnections map")
s.VoiceConnections = make(map[string]*VoiceConnection) s.VoiceConnections = make(map[string]*VoiceConnection)
} }
// Get the gateway to use for the Websocket connection // Create listening chan outside of listen, as it needs to happen inside the
if s.gateway == "" { // mutex lock and needs to exist before calling heartbeat and listen
s.gateway, err = s.Gateway() // go rountines.
if err != nil {
return
}
// Add the version and encoding to the URL
s.gateway = s.gateway + "?v=" + APIVersion + "&encoding=json"
}
header := http.Header{}
header.Add("accept-encoding", "zlib")
s.log(LogInformational, "connecting to gateway %s", s.gateway)
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.gateway = "" // clear cached gateway
// TODO: should we add a retry block here?
return
}
sequence := atomic.LoadInt64(s.sequence)
if s.sessionID != "" && sequence > 0 {
p := resumePacket{}
p.Op = 6
p.Data.Token = s.Token
p.Data.SessionID = s.sessionID
p.Data.Sequence = sequence
s.log(LogInformational, "sending resume packet to gateway")
err = s.wsConn.WriteJSON(p)
if err != nil {
s.log(LogWarning, "error sending gateway resume packet, %s, %s", s.gateway, err)
return
}
} else {
err = s.identify()
if err != nil {
s.log(LogWarning, "error sending gateway identify packet, %s, %s", s.gateway, err)
return
}
}
// Create listening outside of listen, as it needs to happen inside the mutex
// lock.
s.listening = make(chan interface{}) s.listening = make(chan interface{})
// Start sending heartbeats and reading messages from Discord.
go s.heartbeat(s.wsConn, s.listening, h.HeartbeatInterval)
go s.listen(s.wsConn, s.listening) go s.listen(s.wsConn, s.listening)
s.LastHeartbeatAck = time.Now().UTC()
s.Unlock()
s.log(LogInformational, "emit connect event")
s.handleEvent(connectEventType, &Connect{})
s.log(LogInformational, "exiting") s.log(LogInformational, "exiting")
return return nil
} }
// listen polls the websocket connection for events, it will stop when the // listen polls the websocket connection for events, it will stop when the
@ -249,7 +310,8 @@ func (s *Session) heartbeat(wsConn *websocket.Conn, listening <-chan interface{}
} }
} }
type updateStatusData struct { // UpdateStatusData ia provided to UpdateStatusComplex()
type UpdateStatusData struct {
IdleSince *int `json:"since"` IdleSince *int `json:"since"`
Game *Game `json:"game"` Game *Game `json:"game"`
AFK bool `json:"afk"` AFK bool `json:"afk"`
@ -258,7 +320,7 @@ type updateStatusData struct {
type updateStatusOp struct { type updateStatusOp struct {
Op int `json:"op"` Op int `json:"op"`
Data updateStatusData `json:"d"` Data UpdateStatusData `json:"d"`
} }
// UpdateStreamingStatus is used to update the user's streaming status. // UpdateStreamingStatus is used to update the user's streaming status.
@ -270,13 +332,7 @@ func (s *Session) UpdateStreamingStatus(idle int, game string, url string) (err
s.log(LogInformational, "called") s.log(LogInformational, "called")
s.RLock() usd := UpdateStatusData{
defer s.RUnlock()
if s.wsConn == nil {
return ErrWSNotFound
}
usd := updateStatusData{
Status: "online", Status: "online",
} }
@ -285,9 +341,9 @@ func (s *Session) UpdateStreamingStatus(idle int, game string, url string) (err
} }
if game != "" { if game != "" {
gameType := 0 gameType := GameTypeGame
if url != "" { if url != "" {
gameType = 1 gameType = GameTypeStreaming
} }
usd.Game = &Game{ usd.Game = &Game{
Name: game, Name: game,
@ -296,6 +352,18 @@ func (s *Session) UpdateStreamingStatus(idle int, game string, url string) (err
} }
} }
return s.UpdateStatusComplex(usd)
}
// UpdateStatusComplex allows for sending the raw status update data untouched by discordgo.
func (s *Session) UpdateStatusComplex(usd UpdateStatusData) (err error) {
s.RLock()
defer s.RUnlock()
if s.wsConn == nil {
return ErrWSNotFound
}
s.wsMutex.Lock() s.wsMutex.Lock()
err = s.wsConn.WriteJSON(updateStatusOp{3, usd}) err = s.wsConn.WriteJSON(updateStatusOp{3, usd})
s.wsMutex.Unlock() s.wsMutex.Unlock()
@ -357,9 +425,7 @@ func (s *Session) RequestGuildMembers(guildID, query string, limit int) (err err
// //
// If you use the AddHandler() function to register a handler for the // If you use the AddHandler() function to register a handler for the
// "OnEvent" event then all events will be passed to that handler. // "OnEvent" event then all events will be passed to that handler.
// func (s *Session) onEvent(messageType int, message []byte) (*Event, error) {
// TODO: You may also register a custom event handler entirely using...
func (s *Session) onEvent(messageType int, message []byte) {
var err error var err error
var reader io.Reader var reader io.Reader
@ -371,7 +437,7 @@ func (s *Session) onEvent(messageType int, message []byte) {
z, err2 := zlib.NewReader(reader) z, err2 := zlib.NewReader(reader)
if err2 != nil { if err2 != nil {
s.log(LogError, "error uncompressing websocket message, %s", err) s.log(LogError, "error uncompressing websocket message, %s", err)
return return nil, err2
} }
defer func() { defer func() {
@ -389,7 +455,7 @@ func (s *Session) onEvent(messageType int, message []byte) {
decoder := json.NewDecoder(reader) decoder := json.NewDecoder(reader)
if err = decoder.Decode(&e); err != nil { if err = decoder.Decode(&e); err != nil {
s.log(LogError, "error decoding websocket message, %s", err) s.log(LogError, "error decoding websocket message, %s", err)
return return e, err
} }
s.log(LogDebug, "Op: %d, Seq: %d, Type: %s, Data: %s\n\n", e.Operation, e.Sequence, e.Type, string(e.RawData)) s.log(LogDebug, "Op: %d, Seq: %d, Type: %s, Data: %s\n\n", e.Operation, e.Sequence, e.Type, string(e.RawData))
@ -403,10 +469,10 @@ func (s *Session) onEvent(messageType int, message []byte) {
s.wsMutex.Unlock() s.wsMutex.Unlock()
if err != nil { if err != nil {
s.log(LogError, "error sending heartbeat in response to Op1") s.log(LogError, "error sending heartbeat in response to Op1")
return return e, err
} }
return return e, nil
} }
// Reconnect // Reconnect
@ -415,7 +481,7 @@ func (s *Session) onEvent(messageType int, message []byte) {
s.log(LogInformational, "Closing and reconnecting in response to Op7") s.log(LogInformational, "Closing and reconnecting in response to Op7")
s.Close() s.Close()
s.reconnect() s.reconnect()
return return e, nil
} }
// Invalid Session // Invalid Session
@ -427,20 +493,15 @@ func (s *Session) onEvent(messageType int, message []byte) {
err = s.identify() err = s.identify()
if err != nil { if err != nil {
s.log(LogWarning, "error sending gateway identify packet, %s, %s", s.gateway, err) s.log(LogWarning, "error sending gateway identify packet, %s, %s", s.gateway, err)
return return e, err
} }
return return e, nil
} }
if e.Operation == 10 { if e.Operation == 10 {
var h helloOp // Op10 is handled by Open()
if err = json.Unmarshal(e.RawData, &h); err != nil { return e, nil
s.log(LogError, "error unmarshalling helloOp, %s", err)
} else {
go s.heartbeat(s.wsConn, s.listening, h.HeartbeatInterval)
}
return
} }
if e.Operation == 11 { if e.Operation == 11 {
@ -448,7 +509,7 @@ func (s *Session) onEvent(messageType int, message []byte) {
s.LastHeartbeatAck = time.Now().UTC() s.LastHeartbeatAck = time.Now().UTC()
s.Unlock() s.Unlock()
s.log(LogInformational, "got heartbeat ACK") s.log(LogInformational, "got heartbeat ACK")
return return e, nil
} }
// Do not try to Dispatch a non-Dispatch Message // Do not try to Dispatch a non-Dispatch Message
@ -456,7 +517,7 @@ func (s *Session) onEvent(messageType int, message []byte) {
// But we probably should be doing something with them. // But we probably should be doing something with them.
// TEMP // TEMP
s.log(LogWarning, "unknown Op: %d, Seq: %d, Type: %s, Data: %s, message: %s", e.Operation, e.Sequence, e.Type, string(e.RawData), string(message)) s.log(LogWarning, "unknown Op: %d, Seq: %d, Type: %s, Data: %s, message: %s", e.Operation, e.Sequence, e.Type, string(e.RawData), string(message))
return return e, nil
} }
// Store the message sequence // Store the message sequence
@ -485,6 +546,8 @@ func (s *Session) onEvent(messageType int, message []byte) {
// For legacy reasons, we send the raw event also, this could be useful for handling unknown events. // For legacy reasons, we send the raw event also, this could be useful for handling unknown events.
s.handleEvent(eventEventType, e) s.handleEvent(eventEventType, e)
return e, nil
} }
// ------------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------------
@ -610,7 +673,7 @@ func (s *Session) onVoiceServerUpdate(st *VoiceServerUpdate) {
voice.GuildID = st.GuildID voice.GuildID = st.GuildID
voice.Unlock() voice.Unlock()
// Open a conenction to the voice server // Open a connection to the voice server
err := voice.open() err := voice.open()
if err != nil { if err != nil {
s.log(LogError, "onVoiceServerUpdate voice.open, %s", err) s.log(LogError, "onVoiceServerUpdate voice.open, %s", err)

2
vendor/manifest vendored
View File

@ -61,7 +61,7 @@
"importpath": "github.com/bwmarrin/discordgo", "importpath": "github.com/bwmarrin/discordgo",
"repository": "https://github.com/bwmarrin/discordgo", "repository": "https://github.com/bwmarrin/discordgo",
"vcs": "git", "vcs": "git",
"revision": "2fda7ce223a66a5b70b66987c22c3c94d022ee66", "revision": "8d5ab59c63e553fd2030706008a806a8b553188c",
"branch": "master", "branch": "master",
"notests": true "notests": true
}, },