Update vendor (bwmarrin/discordgo) apiv6

This commit is contained in:
Wim 2017-09-11 23:23:54 +02:00
parent 812db2d267
commit 49204cafcc
12 changed files with 472 additions and 116 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.16.0" const VERSION = "0.17.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")
@ -59,6 +59,7 @@ func New(args ...interface{}) (s *Session, err error) {
MaxRestRetries: 3, MaxRestRetries: 3,
Client: &http.Client{Timeout: (20 * time.Second)}, Client: &http.Client{Timeout: (20 * time.Second)},
sequence: new(int64), sequence: new(int64),
LastHeartbeatAck: time.Now().UTC(),
} }
// If no arguments are passed return the empty Session interface. // If no arguments are passed return the empty Session interface.

View File

@ -11,6 +11,9 @@
package discordgo package discordgo
// APIVersion is the Discord API version used for the REST and Websocket API.
var APIVersion = "6"
// Known Discord API Endpoints. // Known Discord API Endpoints.
var ( var (
EndpointStatus = "https://status.discordapp.com/api/v2/" EndpointStatus = "https://status.discordapp.com/api/v2/"
@ -19,11 +22,12 @@ var (
EndpointSmUpcoming = EndpointSm + "upcoming.json" EndpointSmUpcoming = EndpointSm + "upcoming.json"
EndpointDiscord = "https://discordapp.com/" EndpointDiscord = "https://discordapp.com/"
EndpointAPI = EndpointDiscord + "api/" EndpointAPI = EndpointDiscord + "api/v" + APIVersion + "/"
EndpointGuilds = EndpointAPI + "guilds/" EndpointGuilds = EndpointAPI + "guilds/"
EndpointChannels = EndpointAPI + "channels/" EndpointChannels = EndpointAPI + "channels/"
EndpointUsers = EndpointAPI + "users/" EndpointUsers = EndpointAPI + "users/"
EndpointGateway = EndpointAPI + "gateway" EndpointGateway = EndpointAPI + "gateway"
EndpointGatewayBot = EndpointGateway + "/bot"
EndpointWebhooks = EndpointAPI + "webhooks/" EndpointWebhooks = EndpointAPI + "webhooks/"
EndpointCDN = "https://cdn.discordapp.com/" EndpointCDN = "https://cdn.discordapp.com/"
@ -56,6 +60,7 @@ var (
EndpointUser = func(uID string) string { return EndpointUsers + uID } EndpointUser = func(uID string) string { return EndpointUsers + uID }
EndpointUserAvatar = func(uID, aID string) string { return EndpointCDNAvatars + uID + "/" + aID + ".png" } 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" } EndpointUserSettings = func(uID string) string { return EndpointUsers + uID + "/settings" }
EndpointUserGuilds = func(uID string) string { return EndpointUsers + uID + "/guilds" } EndpointUserGuilds = func(uID string) string { return EndpointUsers + uID + "/guilds" }
EndpointUserGuild = func(uID, gID string) string { return EndpointUsers + uID + "/guilds/" + gID } EndpointUserGuild = func(uID, gID string) string { return EndpointUsers + uID + "/guilds/" + gID }
@ -103,6 +108,9 @@ var (
EndpointWebhook = func(wID string) string { return EndpointWebhooks + wID } EndpointWebhook = func(wID string) string { return EndpointWebhooks + wID }
EndpointWebhookToken = func(wID, token string) string { return EndpointWebhooks + wID + "/" + token } EndpointWebhookToken = func(wID, token string) string { return EndpointWebhooks + wID + "/" + token }
EndpointMessageReactionsAll = func(cID, mID string) string {
return EndpointChannelMessage(cID, mID) + "/reactions"
}
EndpointMessageReactions = func(cID, mID, eID string) string { EndpointMessageReactions = func(cID, mID, eID string) string {
return EndpointChannelMessage(cID, mID) + "/reactions/" + eID return EndpointChannelMessage(cID, mID) + "/reactions/" + eID
} }

View File

@ -156,13 +156,21 @@ func (s *Session) removeEventHandlerInstance(t string, ehi *eventHandlerInstance
// Handles calling permanent and once handlers for an event type. // Handles calling permanent and once handlers for an event type.
func (s *Session) handle(t string, i interface{}) { func (s *Session) handle(t string, i interface{}) {
for _, eh := range s.handlers[t] { for _, eh := range s.handlers[t] {
if s.SyncEvents {
eh.eventHandler.Handle(s, i)
} else {
go eh.eventHandler.Handle(s, i) go eh.eventHandler.Handle(s, i)
} }
}
if len(s.onceHandlers[t]) > 0 { if len(s.onceHandlers[t]) > 0 {
for _, eh := range s.onceHandlers[t] { for _, eh := range s.onceHandlers[t] {
if s.SyncEvents {
eh.eventHandler.Handle(s, i)
} else {
go eh.eventHandler.Handle(s, i) go eh.eventHandler.Handle(s, i)
} }
}
s.onceHandlers[t] = nil s.onceHandlers[t] = nil
} }
} }
@ -216,7 +224,7 @@ func (s *Session) onInterface(i interface{}) {
case *VoiceStateUpdate: case *VoiceStateUpdate:
go s.onVoiceStateUpdate(t) go s.onVoiceStateUpdate(t)
} }
err := s.State.onInterface(s, i) err := s.State.OnInterface(s, i)
if err != nil { if err != nil {
s.log(LogDebug, "error dispatching internal event, %s", err) s.log(LogDebug, "error dispatching internal event, %s", err)
} }

View File

@ -10,9 +10,24 @@
package discordgo package discordgo
import ( import (
"fmt"
"io" "io"
"regexp" "regexp"
"strings"
)
// MessageType is the type of Message
type MessageType int
// Block contains the valid known MessageType values
const (
MessageTypeDefault MessageType = iota
MessageTypeRecipientAdd
MessageTypeRecipientRemove
MessageTypeCall
MessageTypeChannelNameChange
MessageTypeChannelIconChange
MessageTypeChannelPinnedMessage
MessageTypeGuildMemberJoin
) )
// A Message stores all data related to a specific Discord message. // A Message stores all data related to a specific Discord message.
@ -30,11 +45,13 @@ type Message struct {
Embeds []*MessageEmbed `json:"embeds"` Embeds []*MessageEmbed `json:"embeds"`
Mentions []*User `json:"mentions"` Mentions []*User `json:"mentions"`
Reactions []*MessageReactions `json:"reactions"` Reactions []*MessageReactions `json:"reactions"`
Type MessageType `json:"type"`
} }
// File stores info about files you e.g. send in messages. // File stores info about files you e.g. send in messages.
type File struct { type File struct {
Name string Name string
ContentType string
Reader io.Reader Reader io.Reader
} }
@ -43,7 +60,10 @@ type MessageSend struct {
Content string `json:"content,omitempty"` Content string `json:"content,omitempty"`
Embed *MessageEmbed `json:"embed,omitempty"` Embed *MessageEmbed `json:"embed,omitempty"`
Tts bool `json:"tts"` Tts bool `json:"tts"`
File *File `json:"file"` Files []*File `json:"-"`
// TODO: Remove this when compatibility is not required.
File *File `json:"-"`
} }
// MessageEdit is used to chain parameters via ChannelMessageEditComplex, which // MessageEdit is used to chain parameters via ChannelMessageEditComplex, which
@ -168,13 +188,65 @@ type MessageReactions struct {
// ContentWithMentionsReplaced will replace all @<id> mentions with the // ContentWithMentionsReplaced will replace all @<id> mentions with the
// username of the mention. // username of the mention.
func (m *Message) ContentWithMentionsReplaced() string { func (m *Message) ContentWithMentionsReplaced() (content string) {
if m.Mentions == nil { content = m.Content
return m.Content
}
content := m.Content
for _, user := range m.Mentions { for _, user := range m.Mentions {
content = regexp.MustCompile(fmt.Sprintf("<@!?(%s)>", user.ID)).ReplaceAllString(content, "@"+user.Username) content = strings.NewReplacer(
"<@"+user.ID+">", "@"+user.Username,
"<@!"+user.ID+">", "@"+user.Username,
).Replace(content)
} }
return content return
}
var patternChannels = regexp.MustCompile("<#[^>]*>")
// ContentWithMoreMentionsReplaced will replace all @<id> mentions with the
// username of the mention, but also role IDs and more.
func (m *Message) ContentWithMoreMentionsReplaced(s *Session) (content string, err error) {
content = m.Content
if !s.StateEnabled {
content = m.ContentWithMentionsReplaced()
return
}
channel, err := s.State.Channel(m.ChannelID)
if err != nil {
content = m.ContentWithMentionsReplaced()
return
}
for _, user := range m.Mentions {
nick := user.Username
member, err := s.State.Member(channel.GuildID, user.ID)
if err == nil && member.Nick != "" {
nick = member.Nick
}
content = strings.NewReplacer(
"<@"+user.ID+">", "@"+user.Username,
"<@!"+user.ID+">", "@"+nick,
).Replace(content)
}
for _, roleID := range m.MentionRoles {
role, err := s.State.Role(channel.GuildID, roleID)
if err != nil || !role.Mentionable {
continue
}
content = strings.Replace(content, "<&"+role.ID+">", "@"+role.Name, -1)
}
content = patternChannels.ReplaceAllStringFunc(content, func(mention string) string {
channel, err := s.State.Channel(mention[2 : len(mention)-1])
if err != nil || channel.Type == ChannelTypeGuildVoice {
return mention
}
return "#" + channel.Name
})
return
} }

View File

@ -3,17 +3,26 @@ package discordgo
import ( import (
"net/http" "net/http"
"strconv" "strconv"
"strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
) )
// customRateLimit holds information for defining a custom rate limit
type customRateLimit struct {
suffix string
requests int
reset time.Duration
}
// RateLimiter holds all ratelimit buckets // RateLimiter holds all ratelimit buckets
type RateLimiter struct { type RateLimiter struct {
sync.Mutex sync.Mutex
global *int64 global *int64
buckets map[string]*Bucket buckets map[string]*Bucket
globalRateLimit time.Duration globalRateLimit time.Duration
customRateLimits []*customRateLimit
} }
// NewRatelimiter returns a new RateLimiter // NewRatelimiter returns a new RateLimiter
@ -22,6 +31,13 @@ func NewRatelimiter() *RateLimiter {
return &RateLimiter{ return &RateLimiter{
buckets: make(map[string]*Bucket), buckets: make(map[string]*Bucket),
global: new(int64), global: new(int64),
customRateLimits: []*customRateLimit{
&customRateLimit{
suffix: "//reactions//",
requests: 1,
reset: 200 * time.Millisecond,
},
},
} }
} }
@ -40,6 +56,14 @@ func (r *RateLimiter) getBucket(key string) *Bucket {
global: r.global, global: r.global,
} }
// Check if there is a custom ratelimit set for this bucket ID.
for _, rl := range r.customRateLimits {
if strings.HasSuffix(b.Key, rl.suffix) {
b.customRateLimit = rl
break
}
}
r.buckets[key] = b r.buckets[key] = b
return b return b
} }
@ -76,13 +100,28 @@ type Bucket struct {
limit int limit int
reset time.Time reset time.Time
global *int64 global *int64
lastReset time.Time
customRateLimit *customRateLimit
} }
// 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
// and locks up the whole thing in case if there's a global ratelimit. // and locks up the whole thing in case if there's a global ratelimit.
func (b *Bucket) Release(headers http.Header) error { func (b *Bucket) Release(headers http.Header) error {
defer b.Unlock() defer b.Unlock()
// Check if the bucket uses a custom ratelimiter
if rl := b.customRateLimit; rl != nil {
if time.Now().Sub(b.lastReset) >= rl.reset {
b.remaining = rl.requests - 1
b.lastReset = time.Now()
}
if b.remaining < 1 {
b.reset = time.Now().Add(rl.reset)
}
return nil
}
if headers == nil { if headers == nil {
return nil return nil
} }

View File

@ -23,6 +23,7 @@ import (
"log" "log"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"net/textproto"
"net/url" "net/url"
"strconv" "strconv"
"strings" "strings"
@ -309,8 +310,8 @@ func (s *Session) UserUpdate(email, password, username, avatar, newPassword stri
// If left blank, avatar will be set to null/blank // If left blank, avatar will be set to null/blank
data := struct { data := struct {
Email string `json:"email"` Email string `json:"email,omitempty"`
Password string `json:"password"` Password string `json:"password,omitempty"`
Username string `json:"username,omitempty"` Username string `json:"username,omitempty"`
Avatar string `json:"avatar,omitempty"` Avatar string `json:"avatar,omitempty"`
NewPassword string `json:"new_password,omitempty"` NewPassword string `json:"new_password,omitempty"`
@ -763,7 +764,21 @@ func (s *Session) GuildMember(guildID, userID string) (st *Member, err error) {
// userID : The ID of a User // userID : The ID of a User
func (s *Session) GuildMemberDelete(guildID, userID string) (err error) { func (s *Session) GuildMemberDelete(guildID, userID string) (err error) {
_, err = s.RequestWithBucketID("DELETE", EndpointGuildMember(guildID, userID), nil, EndpointGuildMember(guildID, "")) return s.GuildMemberDeleteWithReason(guildID, userID, "")
}
// GuildMemberDeleteWithReason removes the given user from the given guild.
// guildID : The ID of a Guild.
// userID : The ID of a User
// reason : The reason for the kick
func (s *Session) GuildMemberDeleteWithReason(guildID, userID, reason string) (err error) {
uri := EndpointGuildMember(guildID, userID)
if reason != "" {
uri += "?reason=" + url.QueryEscape(reason)
}
_, err = s.RequestWithBucketID("DELETE", uri, nil, EndpointGuildMember(guildID, ""))
return return
} }
@ -1316,6 +1331,8 @@ func (s *Session) ChannelMessageSend(channelID string, content string) (*Message
}) })
} }
var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
// ChannelMessageSendComplex sends a message to the given channel. // ChannelMessageSendComplex sends a message to the given channel.
// channelID : The ID of a Channel. // channelID : The ID of a Channel.
// data : The message struct to send. // data : The message struct to send.
@ -1326,48 +1343,62 @@ func (s *Session) ChannelMessageSendComplex(channelID string, data *MessageSend)
endpoint := EndpointChannelMessages(channelID) endpoint := EndpointChannelMessages(channelID)
var response []byte // TODO: Remove this when compatibility is not required.
files := data.Files
if data.File != nil { if data.File != nil {
if files == nil {
files = []*File{data.File}
} else {
err = fmt.Errorf("cannot specify both File and Files")
return
}
}
var response []byte
if len(files) > 0 {
body := &bytes.Buffer{} body := &bytes.Buffer{}
bodywriter := multipart.NewWriter(body) bodywriter := multipart.NewWriter(body)
// What's a better way of doing this? Reflect? Generator? I'm open to suggestions var payload []byte
payload, err = json.Marshal(data)
if data.Content != "" {
if err = bodywriter.WriteField("content", data.Content); err != nil {
return
}
}
if data.Embed != nil {
var embed []byte
embed, err = json.Marshal(data.Embed)
if err != nil {
return
}
err = bodywriter.WriteField("embed", string(embed))
if err != nil {
return
}
}
if data.Tts {
if err = bodywriter.WriteField("tts", "true"); err != nil {
return
}
}
var writer io.Writer
writer, err = bodywriter.CreateFormFile("file", data.File.Name)
if err != nil { if err != nil {
return return
} }
_, err = io.Copy(writer, data.File.Reader) var p io.Writer
h := make(textproto.MIMEHeader)
h.Set("Content-Disposition", `form-data; name="payload_json"`)
h.Set("Content-Type", "application/json")
p, err = bodywriter.CreatePart(h)
if err != nil { if err != nil {
return return
} }
if _, err = p.Write(payload); err != nil {
return
}
for i, file := range files {
h := make(textproto.MIMEHeader)
h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="file%d"; filename="%s"`, i, quoteEscaper.Replace(file.Name)))
contentType := file.ContentType
if contentType == "" {
contentType = "application/octet-stream"
}
h.Set("Content-Type", contentType)
p, err = bodywriter.CreatePart(h)
if err != nil {
return
}
if _, err = io.Copy(p, file.Reader); err != nil {
return
}
}
err = bodywriter.Close() err = bodywriter.Close()
if err != nil { if err != nil {
return return
@ -1685,6 +1716,28 @@ func (s *Session) Gateway() (gateway string, err error) {
return return
} }
// GatewayBot returns the websocket Gateway address and the recommended number of shards
func (s *Session) GatewayBot() (st *GatewayBotResponse, err error) {
response, err := s.RequestWithBucketID("GET", EndpointGatewayBot, nil, EndpointGatewayBot)
if err != nil {
return
}
err = unmarshal(response, &st)
if err != nil {
return
}
// Ensure the gateway always has a trailing slash.
// MacOS will fail to connect if we add query params without a trailing slash on the base domain.
if !strings.HasSuffix(st.URL, "/") {
st.URL += "/"
}
return
}
// Functions specific to Webhooks // Functions specific to Webhooks
// WebhookCreate returns a new Webhook. // WebhookCreate returns a new Webhook.
@ -1810,14 +1863,9 @@ func (s *Session) WebhookEditWithToken(webhookID, token, name, avatar string) (s
// WebhookDelete deletes a webhook for a given ID // WebhookDelete deletes a webhook for a given ID
// webhookID: The ID of a webhook. // webhookID: The ID of a webhook.
func (s *Session) WebhookDelete(webhookID string) (st *Webhook, err error) { func (s *Session) WebhookDelete(webhookID string) (err error) {
body, err := s.RequestWithBucketID("DELETE", EndpointWebhook(webhookID), nil, EndpointWebhooks) _, err = s.RequestWithBucketID("DELETE", EndpointWebhook(webhookID), nil, EndpointWebhooks)
if err != nil {
return
}
err = unmarshal(body, &st)
return return
} }
@ -1875,6 +1923,16 @@ func (s *Session) MessageReactionRemove(channelID, messageID, emojiID, userID st
return err return err
} }
// MessageReactionsRemoveAll deletes all reactions from a message
// channelID : The channel ID
// messageID : The message ID.
func (s *Session) MessageReactionsRemoveAll(channelID, messageID string) error {
_, err := s.RequestWithBucketID("DELETE", EndpointMessageReactionsAll(channelID, messageID), nil, EndpointMessageReactionsAll(channelID, messageID))
return err
}
// MessageReactions gets all the users reactions for a specific emoji. // MessageReactions gets all the users reactions for a specific emoji.
// channelID : The channel ID. // channelID : The channel ID.
// messageID : The message ID. // messageID : The message ID.

View File

@ -42,6 +42,7 @@ type State struct {
guildMap map[string]*Guild guildMap map[string]*Guild
channelMap map[string]*Channel channelMap map[string]*Channel
memberMap map[string]map[string]*Member
} }
// NewState creates an empty state. // NewState creates an empty state.
@ -59,9 +60,18 @@ func NewState() *State {
TrackPresences: true, TrackPresences: true,
guildMap: make(map[string]*Guild), guildMap: make(map[string]*Guild),
channelMap: make(map[string]*Channel), channelMap: make(map[string]*Channel),
memberMap: make(map[string]map[string]*Member),
} }
} }
func (s *State) createMemberMap(guild *Guild) {
members := make(map[string]*Member)
for _, m := range guild.Members {
members[m.User.ID] = m
}
s.memberMap[guild.ID] = members
}
// GuildAdd adds a guild to the current world state, or // GuildAdd adds a guild to the current world state, or
// updates it if it already exists. // updates it if it already exists.
func (s *State) GuildAdd(guild *Guild) error { func (s *State) GuildAdd(guild *Guild) error {
@ -77,6 +87,14 @@ func (s *State) GuildAdd(guild *Guild) error {
s.channelMap[c.ID] = c s.channelMap[c.ID] = c
} }
// If this guild contains a new member slice, we must regenerate the member map so the pointers stay valid
if guild.Members != nil {
s.createMemberMap(guild)
} else if _, ok := s.memberMap[guild.ID]; !ok {
// Even if we have no new member slice, we still initialize the member map for this guild if it doesn't exist
s.memberMap[guild.ID] = make(map[string]*Member)
}
if g, ok := s.guildMap[guild.ID]; ok { if g, ok := s.guildMap[guild.ID]; ok {
// We are about to replace `g` in the state with `guild`, but first we need to // 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`. // make sure we preserve any fields that the `guild` doesn't contain from `g`.
@ -271,14 +289,19 @@ func (s *State) MemberAdd(member *Member) error {
s.Lock() s.Lock()
defer s.Unlock() defer s.Unlock()
for i, m := range guild.Members { members, ok := s.memberMap[member.GuildID]
if m.User.ID == member.User.ID { if !ok {
guild.Members[i] = member return ErrStateNotFound
return nil
}
} }
m, ok := members[member.User.ID]
if !ok {
members[member.User.ID] = member
guild.Members = append(guild.Members, member) guild.Members = append(guild.Members, member)
} else {
*m = *member // Update the actual data, which will also update the member pointer in the slice
}
return nil return nil
} }
@ -296,6 +319,17 @@ func (s *State) MemberRemove(member *Member) error {
s.Lock() s.Lock()
defer s.Unlock() defer s.Unlock()
members, ok := s.memberMap[member.GuildID]
if !ok {
return ErrStateNotFound
}
_, ok = members[member.User.ID]
if !ok {
return ErrStateNotFound
}
delete(members, member.User.ID)
for i, m := range guild.Members { for i, m := range guild.Members {
if m.User.ID == member.User.ID { if m.User.ID == member.User.ID {
guild.Members = append(guild.Members[:i], guild.Members[i+1:]...) guild.Members = append(guild.Members[:i], guild.Members[i+1:]...)
@ -312,18 +346,17 @@ func (s *State) Member(guildID, userID string) (*Member, error) {
return nil, ErrNilState return nil, ErrNilState
} }
guild, err := s.Guild(guildID)
if err != nil {
return nil, err
}
s.RLock() s.RLock()
defer s.RUnlock() defer s.RUnlock()
for _, m := range guild.Members { members, ok := s.memberMap[guildID]
if m.User.ID == userID { if !ok {
return m, nil return nil, ErrStateNotFound
} }
m, ok := members[userID]
if ok {
return m, nil
} }
return nil, ErrStateNotFound return nil, ErrStateNotFound
@ -427,7 +460,7 @@ func (s *State) ChannelAdd(channel *Channel) error {
return nil return nil
} }
if channel.IsPrivate { if channel.Type == ChannelTypeDM || channel.Type == ChannelTypeGroupDM {
s.PrivateChannels = append(s.PrivateChannels, channel) s.PrivateChannels = append(s.PrivateChannels, channel)
} else { } else {
guild, ok := s.guildMap[channel.GuildID] guild, ok := s.guildMap[channel.GuildID]
@ -454,7 +487,7 @@ func (s *State) ChannelRemove(channel *Channel) error {
return err return err
} }
if channel.IsPrivate { if channel.Type == ChannelTypeDM || channel.Type == ChannelTypeGroupDM {
s.Lock() s.Lock()
defer s.Unlock() defer s.Unlock()
@ -735,6 +768,7 @@ func (s *State) onReady(se *Session, r *Ready) (err error) {
for _, g := range s.Guilds { for _, g := range s.Guilds {
s.guildMap[g.ID] = g s.guildMap[g.ID] = g
s.createMemberMap(g)
for _, c := range g.Channels { for _, c := range g.Channels {
s.channelMap[c.ID] = c s.channelMap[c.ID] = c
@ -748,8 +782,8 @@ func (s *State) onReady(se *Session, r *Ready) (err error) {
return nil return nil
} }
// onInterface handles all events related to states. // OnInterface handles all events related to states.
func (s *State) onInterface(se *Session, i interface{}) (err error) { func (s *State) OnInterface(se *Session, i interface{}) (err error) {
if s == nil { if s == nil {
return ErrNilState return ErrNilState
} }

View File

@ -50,6 +50,10 @@ type Session struct {
// active guilds and the members of the guilds. // active guilds and the members of the guilds.
StateEnabled bool StateEnabled bool
// Whether or not to call event handlers synchronously.
// e.g false = launch event handlers in their own goroutines.
SyncEvents bool
// Exposed but should not be modified by User. // Exposed but should not be modified by User.
// Whether the Data Websocket is ready // Whether the Data Websocket is ready
@ -78,6 +82,9 @@ type Session struct {
// The http client used for REST requests // The http client used for REST requests
Client *http.Client Client *http.Client
// Stores the last HeartbeatAck that was recieved (in UTC)
LastHeartbeatAck time.Time
// Event handlers // Event handlers
handlersMu sync.RWMutex handlersMu sync.RWMutex
handlers map[string][]*eventHandlerInstance handlers map[string][]*eventHandlerInstance
@ -141,18 +148,30 @@ type Invite struct {
Temporary bool `json:"temporary"` Temporary bool `json:"temporary"`
} }
// ChannelType is the type of a Channel
type ChannelType int
// Block contains known ChannelType values
const (
ChannelTypeGuildText ChannelType = iota
ChannelTypeDM
ChannelTypeGuildVoice
ChannelTypeGroupDM
ChannelTypeGuildCategory
)
// A Channel holds all data related to an individual Discord channel. // A Channel holds all data related to an individual Discord channel.
type Channel struct { type Channel struct {
ID string `json:"id"` ID string `json:"id"`
GuildID string `json:"guild_id"` GuildID string `json:"guild_id"`
Name string `json:"name"` Name string `json:"name"`
Topic string `json:"topic"` Topic string `json:"topic"`
Type string `json:"type"` Type ChannelType `json:"type"`
LastMessageID string `json:"last_message_id"` LastMessageID string `json:"last_message_id"`
NSFW bool `json:"nsfw"`
Position int `json:"position"` Position int `json:"position"`
Bitrate int `json:"bitrate"` Bitrate int `json:"bitrate"`
IsPrivate bool `json:"is_private"` Recipients []*User `json:"recipient"`
Recipient *User `json:"recipient"`
Messages []*Message `json:"-"` Messages []*Message `json:"-"`
PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites"` PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites"`
} }
@ -292,13 +311,14 @@ type Presence struct {
Game *Game `json:"game"` Game *Game `json:"game"`
Nick string `json:"nick"` Nick string `json:"nick"`
Roles []string `json:"roles"` Roles []string `json:"roles"`
Since *int `json:"since"`
} }
// 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 int `json:"type"`
URL string `json:"url"` URL string `json:"url,omitempty"`
} }
// UnmarshalJSON unmarshals json to Game struct // UnmarshalJSON unmarshals json to Game struct
@ -509,6 +529,12 @@ type MessageReaction struct {
ChannelID string `json:"channel_id"` ChannelID string `json:"channel_id"`
} }
// GatewayBotResponse stores the data for the gateway/bot response
type GatewayBotResponse struct {
URL string `json:"url"`
Shards int `json:"shards"`
}
// Constants for the different bit offsets of text channel permissions // Constants for the different bit offsets of text channel permissions
const ( const (
PermissionReadMessages = 1 << (iota + 10) PermissionReadMessages = 1 << (iota + 10)
@ -579,3 +605,56 @@ const (
PermissionManageServer | PermissionManageServer |
PermissionAdministrator PermissionAdministrator
) )
// Block contains Discord JSON Error Response codes
const (
ErrCodeUnknownAccount = 10001
ErrCodeUnknownApplication = 10002
ErrCodeUnknownChannel = 10003
ErrCodeUnknownGuild = 10004
ErrCodeUnknownIntegration = 10005
ErrCodeUnknownInvite = 10006
ErrCodeUnknownMember = 10007
ErrCodeUnknownMessage = 10008
ErrCodeUnknownOverwrite = 10009
ErrCodeUnknownProvider = 10010
ErrCodeUnknownRole = 10011
ErrCodeUnknownToken = 10012
ErrCodeUnknownUser = 10013
ErrCodeUnknownEmoji = 10014
ErrCodeBotsCannotUseEndpoint = 20001
ErrCodeOnlyBotsCanUseEndpoint = 20002
ErrCodeMaximumGuildsReached = 30001
ErrCodeMaximumFriendsReached = 30002
ErrCodeMaximumPinsReached = 30003
ErrCodeMaximumGuildRolesReached = 30005
ErrCodeTooManyReactions = 30010
ErrCodeUnauthorized = 40001
ErrCodeMissingAccess = 50001
ErrCodeInvalidAccountType = 50002
ErrCodeCannotExecuteActionOnDMChannel = 50003
ErrCodeEmbedCisabled = 50004
ErrCodeCannotEditFromAnotherUser = 50005
ErrCodeCannotSendEmptyMessage = 50006
ErrCodeCannotSendMessagesToThisUser = 50007
ErrCodeCannotSendMessagesInVoiceChannel = 50008
ErrCodeChannelVerificationLevelTooHigh = 50009
ErrCodeOAuth2ApplicationDoesNotHaveBot = 50010
ErrCodeOAuth2ApplicationLimitReached = 50011
ErrCodeInvalidOAuthState = 50012
ErrCodeMissingPermissions = 50013
ErrCodeInvalidAuthenticationToken = 50014
ErrCodeNoteTooLong = 50015
ErrCodeTooFewOrTooManyMessagesToDelete = 50016
ErrCodeCanOnlyPinMessageToOriginatingChannel = 50019
ErrCodeCannotExecuteActionOnSystemMessage = 50021
ErrCodeMessageProvidedTooOldForBulkDelete = 50034
ErrCodeInvalidFormBody = 50035
ErrCodeInviteAcceptedToGuildApplicationsBotNotIn = 50036
ErrCodeReactionBlocked = 90001
)

View File

@ -1,6 +1,9 @@
package discordgo package discordgo
import "fmt" import (
"fmt"
"strings"
)
// A User stores all data for an individual Discord user. // A User stores all data for an individual Discord user.
type User struct { type User struct {
@ -24,3 +27,16 @@ func (u *User) String() string {
func (u *User) Mention() string { func (u *User) Mention() string {
return fmt.Sprintf("<@%s>", u.ID) return fmt.Sprintf("<@%s>", u.ID)
} }
// AvatarURL returns a URL to the user's avatar.
// size: The size of the user's avatar as a power of two
func (u *User) AvatarURL(size string) string {
var URL string
if strings.HasPrefix(u.Avatar, "a_") {
URL = EndpointUserAvatarAnimated(u.ID, u.Avatar)
} else {
URL = EndpointUserAvatar(u.ID, u.Avatar)
}
return URL + "?size=" + size
}

View File

@ -796,7 +796,7 @@ func (v *VoiceConnection) opusReceiver(udpConn *net.UDPConn, close <-chan struct
} }
// For now, skip anything except audio. // For now, skip anything except audio.
if rlen < 12 || recvbuf[0] != 0x80 { if rlen < 12 || (recvbuf[0] != 0x80 && recvbuf[0] != 0x90) {
continue continue
} }
@ -810,8 +810,17 @@ func (v *VoiceConnection) opusReceiver(udpConn *net.UDPConn, close <-chan struct
copy(nonce[:], recvbuf[0:12]) copy(nonce[:], recvbuf[0:12])
p.Opus, _ = secretbox.Open(nil, recvbuf[12:rlen], &nonce, &v.op4.SecretKey) p.Opus, _ = secretbox.Open(nil, recvbuf[12:rlen], &nonce, &v.op4.SecretKey)
if len(p.Opus) > 8 && recvbuf[0] == 0x90 {
// Extension bit is set, first 8 bytes is the extended header
p.Opus = p.Opus[8:]
}
if c != nil { if c != nil {
c <- &p select {
case c <- &p:
case <-close:
return
}
} }
} }
} }

View File

@ -15,7 +15,6 @@ import (
"compress/zlib" "compress/zlib"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"io" "io"
"net/http" "net/http"
"runtime" "runtime"
@ -87,7 +86,7 @@ func (s *Session) Open() (err error) {
} }
// Add the version and encoding to the URL // Add the version and encoding to the URL
s.gateway = fmt.Sprintf("%s?v=5&encoding=json", s.gateway) s.gateway = s.gateway + "?v=" + APIVersion + "&encoding=json"
} }
header := http.Header{} header := http.Header{}
@ -131,6 +130,7 @@ func (s *Session) Open() (err error) {
// lock. // lock.
s.listening = make(chan interface{}) s.listening = make(chan interface{})
go s.listen(s.wsConn, s.listening) go s.listen(s.wsConn, s.listening)
s.LastHeartbeatAck = time.Now().UTC()
s.Unlock() s.Unlock()
@ -199,10 +199,13 @@ type helloOp struct {
Trace []string `json:"_trace"` Trace []string `json:"_trace"`
} }
// FailedHeartbeatAcks is the Number of heartbeat intervals to wait until forcing a connection restart.
const FailedHeartbeatAcks time.Duration = 5 * time.Millisecond
// heartbeat sends regular heartbeats to Discord so it knows the client // heartbeat sends regular heartbeats to Discord so it knows the client
// is still connected. If you do not send these heartbeats Discord will // is still connected. If you do not send these heartbeats Discord will
// disconnect the websocket connection after a few seconds. // disconnect the websocket connection after a few seconds.
func (s *Session) heartbeat(wsConn *websocket.Conn, listening <-chan interface{}, i time.Duration) { func (s *Session) heartbeat(wsConn *websocket.Conn, listening <-chan interface{}, heartbeatIntervalMsec time.Duration) {
s.log(LogInformational, "called") s.log(LogInformational, "called")
@ -211,20 +214,26 @@ func (s *Session) heartbeat(wsConn *websocket.Conn, listening <-chan interface{}
} }
var err error var err error
ticker := time.NewTicker(i * time.Millisecond) ticker := time.NewTicker(heartbeatIntervalMsec * time.Millisecond)
defer ticker.Stop() defer ticker.Stop()
for { for {
s.RLock()
last := s.LastHeartbeatAck
s.RUnlock()
sequence := atomic.LoadInt64(s.sequence) sequence := atomic.LoadInt64(s.sequence)
s.log(LogInformational, "sending gateway websocket heartbeat seq %d", sequence) s.log(LogInformational, "sending gateway websocket heartbeat seq %d", sequence)
s.wsMutex.Lock() s.wsMutex.Lock()
err = wsConn.WriteJSON(heartbeatOp{1, sequence}) err = wsConn.WriteJSON(heartbeatOp{1, sequence})
s.wsMutex.Unlock() s.wsMutex.Unlock()
if err != nil || time.Now().UTC().Sub(last) > (heartbeatIntervalMsec*FailedHeartbeatAcks) {
if err != nil { if err != nil {
s.log(LogError, "error sending heartbeat to gateway %s, %s", s.gateway, err) s.log(LogError, "error sending heartbeat to gateway %s, %s", s.gateway, err)
s.Lock() } else {
s.DataReady = false s.log(LogError, "haven't gotten a heartbeat ACK in %v, triggering a reconnection", time.Now().UTC().Sub(last))
s.Unlock() }
s.Close()
s.reconnect()
return return
} }
s.Lock() s.Lock()
@ -241,8 +250,10 @@ func (s *Session) heartbeat(wsConn *websocket.Conn, listening <-chan interface{}
} }
type updateStatusData struct { type updateStatusData struct {
IdleSince *int `json:"idle_since"` IdleSince *int `json:"since"`
Game *Game `json:"game"` Game *Game `json:"game"`
AFK bool `json:"afk"`
Status string `json:"status"`
} }
type updateStatusOp struct { type updateStatusOp struct {
@ -265,7 +276,10 @@ func (s *Session) UpdateStreamingStatus(idle int, game string, url string) (err
return ErrWSNotFound return ErrWSNotFound
} }
var usd updateStatusData usd := updateStatusData{
Status: "online",
}
if idle > 0 { if idle > 0 {
usd.IdleSince = &idle usd.IdleSince = &idle
} }
@ -398,7 +412,10 @@ func (s *Session) onEvent(messageType int, message []byte) {
// Reconnect // Reconnect
// Must immediately disconnect from gateway and reconnect to new gateway. // Must immediately disconnect from gateway and reconnect to new gateway.
if e.Operation == 7 { if e.Operation == 7 {
// TODO s.log(LogInformational, "Closing and reconnecting in response to Op7")
s.Close()
s.reconnect()
return
} }
// Invalid Session // Invalid Session
@ -426,6 +443,14 @@ func (s *Session) onEvent(messageType int, message []byte) {
return return
} }
if e.Operation == 11 {
s.Lock()
s.LastHeartbeatAck = time.Now().UTC()
s.Unlock()
s.log(LogInformational, "got heartbeat ACK")
return
}
// Do not try to Dispatch a non-Dispatch Message // Do not try to Dispatch a non-Dispatch Message
if e.Operation != 0 { if e.Operation != 0 {
// But we probably should be doing something with them. // But we probably should be doing something with them.
@ -688,6 +713,13 @@ func (s *Session) reconnect() {
return return
} }
// Certain race conditions can call reconnect() twice. If this happens, we
// just break out of the reconnect loop
if err == ErrWSAlreadyOpen {
s.log(LogInformational, "Websocket already exists, no need to reconnect")
return
}
s.log(LogError, "error reconnecting to gateway, %s", err) s.log(LogError, "error reconnecting to gateway, %s", err)
<-time.After(wait * time.Second) <-time.After(wait * time.Second)

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": "d420e28024ad527390b43aa7f64e029083e11989", "revision": "2fda7ce223a66a5b70b66987c22c3c94d022ee66",
"branch": "master", "branch": "master",
"notests": true "notests": true
}, },