diff --git a/vendor/github.com/bwmarrin/discordgo/discord.go b/vendor/github.com/bwmarrin/discordgo/discord.go index 2f0b6fd4..40eabe18 100644 --- a/vendor/github.com/bwmarrin/discordgo/discord.go +++ b/vendor/github.com/bwmarrin/discordgo/discord.go @@ -21,7 +21,7 @@ import ( ) // 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. var ErrMFA = errors.New("account has 2FA enabled") @@ -59,6 +59,7 @@ func New(args ...interface{}) (s *Session, err error) { MaxRestRetries: 3, Client: &http.Client{Timeout: (20 * time.Second)}, sequence: new(int64), + LastHeartbeatAck: time.Now().UTC(), } // If no arguments are passed return the empty Session interface. diff --git a/vendor/github.com/bwmarrin/discordgo/endpoints.go b/vendor/github.com/bwmarrin/discordgo/endpoints.go index 96bcf28b..b10f9589 100644 --- a/vendor/github.com/bwmarrin/discordgo/endpoints.go +++ b/vendor/github.com/bwmarrin/discordgo/endpoints.go @@ -11,6 +11,9 @@ package discordgo +// APIVersion is the Discord API version used for the REST and Websocket API. +var APIVersion = "6" + // Known Discord API Endpoints. var ( EndpointStatus = "https://status.discordapp.com/api/v2/" @@ -18,13 +21,14 @@ var ( EndpointSmActive = EndpointSm + "active.json" EndpointSmUpcoming = EndpointSm + "upcoming.json" - EndpointDiscord = "https://discordapp.com/" - EndpointAPI = EndpointDiscord + "api/" - EndpointGuilds = EndpointAPI + "guilds/" - EndpointChannels = EndpointAPI + "channels/" - EndpointUsers = EndpointAPI + "users/" - EndpointGateway = EndpointAPI + "gateway" - EndpointWebhooks = EndpointAPI + "webhooks/" + EndpointDiscord = "https://discordapp.com/" + EndpointAPI = EndpointDiscord + "api/v" + APIVersion + "/" + EndpointGuilds = EndpointAPI + "guilds/" + EndpointChannels = EndpointAPI + "channels/" + EndpointUsers = EndpointAPI + "users/" + EndpointGateway = EndpointAPI + "gateway" + EndpointGatewayBot = EndpointGateway + "/bot" + EndpointWebhooks = EndpointAPI + "webhooks/" EndpointCDN = "https://cdn.discordapp.com/" EndpointCDNAttachments = EndpointCDN + "attachments/" @@ -54,16 +58,17 @@ var ( EndpointReport = EndpointAPI + "report" EndpointIntegrations = EndpointAPI + "integrations" - EndpointUser = func(uID string) string { return EndpointUsers + uID } - EndpointUserAvatar = func(uID, aID string) string { return EndpointCDNAvatars + uID + "/" + aID + ".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 } + 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 } EndpointGuild = func(gID string) string { return EndpointGuilds + gID } EndpointGuildInivtes = func(gID string) string { return EndpointGuilds + gID + "/invites" } @@ -103,6 +108,9 @@ var ( EndpointWebhook = func(wID string) string { return EndpointWebhooks + wID } 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 { return EndpointChannelMessage(cID, mID) + "/reactions/" + eID } diff --git a/vendor/github.com/bwmarrin/discordgo/event.go b/vendor/github.com/bwmarrin/discordgo/event.go index 906c2aa8..3a03f46d 100644 --- a/vendor/github.com/bwmarrin/discordgo/event.go +++ b/vendor/github.com/bwmarrin/discordgo/event.go @@ -156,12 +156,20 @@ func (s *Session) removeEventHandlerInstance(t string, ehi *eventHandlerInstance // Handles calling permanent and once handlers for an event type. func (s *Session) handle(t string, i interface{}) { for _, eh := range s.handlers[t] { - go eh.eventHandler.Handle(s, i) + if s.SyncEvents { + eh.eventHandler.Handle(s, i) + } else { + go eh.eventHandler.Handle(s, i) + } } if len(s.onceHandlers[t]) > 0 { for _, eh := range s.onceHandlers[t] { - go eh.eventHandler.Handle(s, i) + if s.SyncEvents { + eh.eventHandler.Handle(s, i) + } else { + go eh.eventHandler.Handle(s, i) + } } s.onceHandlers[t] = nil } @@ -216,7 +224,7 @@ func (s *Session) onInterface(i interface{}) { case *VoiceStateUpdate: go s.onVoiceStateUpdate(t) } - err := s.State.onInterface(s, i) + err := s.State.OnInterface(s, i) if err != nil { s.log(LogDebug, "error dispatching internal event, %s", err) } diff --git a/vendor/github.com/bwmarrin/discordgo/message.go b/vendor/github.com/bwmarrin/discordgo/message.go index 13c2da07..19345b95 100644 --- a/vendor/github.com/bwmarrin/discordgo/message.go +++ b/vendor/github.com/bwmarrin/discordgo/message.go @@ -10,9 +10,24 @@ package discordgo import ( - "fmt" "io" "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. @@ -30,12 +45,14 @@ type Message struct { Embeds []*MessageEmbed `json:"embeds"` Mentions []*User `json:"mentions"` Reactions []*MessageReactions `json:"reactions"` + Type MessageType `json:"type"` } // File stores info about files you e.g. send in messages. type File struct { - Name string - Reader io.Reader + Name string + ContentType string + Reader io.Reader } // MessageSend stores all parameters you can send with ChannelMessageSendComplex. @@ -43,7 +60,10 @@ type MessageSend struct { Content string `json:"content,omitempty"` Embed *MessageEmbed `json:"embed,omitempty"` 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 @@ -168,13 +188,65 @@ type MessageReactions struct { // ContentWithMentionsReplaced will replace all @ mentions with the // username of the mention. -func (m *Message) ContentWithMentionsReplaced() string { - if m.Mentions == nil { - return m.Content - } - content := m.Content +func (m *Message) ContentWithMentionsReplaced() (content string) { + content = m.Content + 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 @ 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 } diff --git a/vendor/github.com/bwmarrin/discordgo/ratelimit.go b/vendor/github.com/bwmarrin/discordgo/ratelimit.go index 876e98a9..223c0d04 100644 --- a/vendor/github.com/bwmarrin/discordgo/ratelimit.go +++ b/vendor/github.com/bwmarrin/discordgo/ratelimit.go @@ -3,17 +3,26 @@ package discordgo import ( "net/http" "strconv" + "strings" "sync" "sync/atomic" "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 type RateLimiter struct { sync.Mutex - global *int64 - buckets map[string]*Bucket - globalRateLimit time.Duration + global *int64 + buckets map[string]*Bucket + globalRateLimit time.Duration + customRateLimits []*customRateLimit } // NewRatelimiter returns a new RateLimiter @@ -22,6 +31,13 @@ func NewRatelimiter() *RateLimiter { return &RateLimiter{ buckets: make(map[string]*Bucket), 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, } + // 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 return b } @@ -76,13 +100,28 @@ type Bucket struct { limit int reset time.Time global *int64 + + lastReset time.Time + customRateLimit *customRateLimit } // 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. func (b *Bucket) Release(headers http.Header) error { - 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 { return nil } diff --git a/vendor/github.com/bwmarrin/discordgo/restapi.go b/vendor/github.com/bwmarrin/discordgo/restapi.go index cb482e68..836e4a41 100644 --- a/vendor/github.com/bwmarrin/discordgo/restapi.go +++ b/vendor/github.com/bwmarrin/discordgo/restapi.go @@ -23,6 +23,7 @@ import ( "log" "mime/multipart" "net/http" + "net/textproto" "net/url" "strconv" "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 data := struct { - Email string `json:"email"` - Password string `json:"password"` + Email string `json:"email,omitempty"` + Password string `json:"password,omitempty"` Username string `json:"username,omitempty"` Avatar string `json:"avatar,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 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 } @@ -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. // channelID : The ID of a Channel. // data : The message struct to send. @@ -1326,48 +1343,62 @@ func (s *Session) ChannelMessageSendComplex(channelID string, data *MessageSend) endpoint := EndpointChannelMessages(channelID) - var response []byte + // TODO: Remove this when compatibility is not required. + files := data.Files 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{} bodywriter := multipart.NewWriter(body) - // What's a better way of doing this? Reflect? Generator? I'm open to suggestions - - 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) + var payload []byte + payload, err = json.Marshal(data) if err != nil { 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 { 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() if err != nil { return @@ -1685,6 +1716,28 @@ func (s *Session) Gateway() (gateway string, err error) { 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 // 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 // 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) - if err != nil { - return - } - - err = unmarshal(body, &st) + _, err = s.RequestWithBucketID("DELETE", EndpointWebhook(webhookID), nil, EndpointWebhooks) return } @@ -1875,6 +1923,16 @@ func (s *Session) MessageReactionRemove(channelID, messageID, emojiID, userID st 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. // channelID : The channel ID. // messageID : The message ID. diff --git a/vendor/github.com/bwmarrin/discordgo/state.go b/vendor/github.com/bwmarrin/discordgo/state.go index 7400ef62..35a8e757 100644 --- a/vendor/github.com/bwmarrin/discordgo/state.go +++ b/vendor/github.com/bwmarrin/discordgo/state.go @@ -42,6 +42,7 @@ type State struct { guildMap map[string]*Guild channelMap map[string]*Channel + memberMap map[string]map[string]*Member } // NewState creates an empty state. @@ -59,9 +60,18 @@ func NewState() *State { TrackPresences: true, guildMap: make(map[string]*Guild), 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 // updates it if it already exists. func (s *State) GuildAdd(guild *Guild) error { @@ -77,6 +87,14 @@ func (s *State) GuildAdd(guild *Guild) error { 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 { // 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`. @@ -271,14 +289,19 @@ func (s *State) MemberAdd(member *Member) error { s.Lock() defer s.Unlock() - for i, m := range guild.Members { - if m.User.ID == member.User.ID { - guild.Members[i] = member - return nil - } + members, ok := s.memberMap[member.GuildID] + if !ok { + return ErrStateNotFound + } + + m, ok := members[member.User.ID] + if !ok { + 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 } - guild.Members = append(guild.Members, member) return nil } @@ -296,6 +319,17 @@ func (s *State) MemberRemove(member *Member) error { s.Lock() 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 { if m.User.ID == member.User.ID { 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 } - guild, err := s.Guild(guildID) - if err != nil { - return nil, err - } - s.RLock() defer s.RUnlock() - for _, m := range guild.Members { - if m.User.ID == userID { - return m, nil - } + members, ok := s.memberMap[guildID] + if !ok { + return nil, ErrStateNotFound + } + + m, ok := members[userID] + if ok { + return m, nil } return nil, ErrStateNotFound @@ -427,7 +460,7 @@ func (s *State) ChannelAdd(channel *Channel) error { return nil } - if channel.IsPrivate { + if channel.Type == ChannelTypeDM || channel.Type == ChannelTypeGroupDM { s.PrivateChannels = append(s.PrivateChannels, channel) } else { guild, ok := s.guildMap[channel.GuildID] @@ -454,7 +487,7 @@ func (s *State) ChannelRemove(channel *Channel) error { return err } - if channel.IsPrivate { + if channel.Type == ChannelTypeDM || channel.Type == ChannelTypeGroupDM { s.Lock() defer s.Unlock() @@ -735,6 +768,7 @@ func (s *State) onReady(se *Session, r *Ready) (err error) { for _, g := range s.Guilds { s.guildMap[g.ID] = g + s.createMemberMap(g) for _, c := range g.Channels { s.channelMap[c.ID] = c @@ -748,8 +782,8 @@ func (s *State) onReady(se *Session, r *Ready) (err error) { return nil } -// onInterface handles all events related to states. -func (s *State) onInterface(se *Session, i interface{}) (err error) { +// OnInterface handles all events related to states. +func (s *State) OnInterface(se *Session, i interface{}) (err error) { if s == nil { return ErrNilState } diff --git a/vendor/github.com/bwmarrin/discordgo/structs.go b/vendor/github.com/bwmarrin/discordgo/structs.go index 32f435ce..c3e39566 100644 --- a/vendor/github.com/bwmarrin/discordgo/structs.go +++ b/vendor/github.com/bwmarrin/discordgo/structs.go @@ -50,6 +50,10 @@ type Session struct { // active guilds and the members of the guilds. 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. // Whether the Data Websocket is ready @@ -78,6 +82,9 @@ type Session struct { // The http client used for REST requests Client *http.Client + // Stores the last HeartbeatAck that was recieved (in UTC) + LastHeartbeatAck time.Time + // Event handlers handlersMu sync.RWMutex handlers map[string][]*eventHandlerInstance @@ -141,18 +148,30 @@ type Invite struct { 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. type Channel struct { ID string `json:"id"` GuildID string `json:"guild_id"` Name string `json:"name"` Topic string `json:"topic"` - Type string `json:"type"` + Type ChannelType `json:"type"` LastMessageID string `json:"last_message_id"` + NSFW bool `json:"nsfw"` Position int `json:"position"` Bitrate int `json:"bitrate"` - IsPrivate bool `json:"is_private"` - Recipient *User `json:"recipient"` + Recipients []*User `json:"recipient"` Messages []*Message `json:"-"` PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites"` } @@ -292,13 +311,14 @@ type Presence struct { Game *Game `json:"game"` Nick string `json:"nick"` Roles []string `json:"roles"` + Since *int `json:"since"` } // A Game struct holds the name of the "playing .." game for a user type Game struct { Name string `json:"name"` Type int `json:"type"` - URL string `json:"url"` + URL string `json:"url,omitempty"` } // UnmarshalJSON unmarshals json to Game struct @@ -509,6 +529,12 @@ type MessageReaction struct { 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 const ( PermissionReadMessages = 1 << (iota + 10) @@ -579,3 +605,56 @@ const ( PermissionManageServer | 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 +) diff --git a/vendor/github.com/bwmarrin/discordgo/user.go b/vendor/github.com/bwmarrin/discordgo/user.go index b3a7e4b2..76abdd1d 100644 --- a/vendor/github.com/bwmarrin/discordgo/user.go +++ b/vendor/github.com/bwmarrin/discordgo/user.go @@ -1,6 +1,9 @@ package discordgo -import "fmt" +import ( + "fmt" + "strings" +) // A User stores all data for an individual Discord user. type User struct { @@ -24,3 +27,16 @@ func (u *User) String() string { func (u *User) Mention() string { 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 +} diff --git a/vendor/github.com/bwmarrin/discordgo/voice.go b/vendor/github.com/bwmarrin/discordgo/voice.go index da7b8c90..8f033aa0 100644 --- a/vendor/github.com/bwmarrin/discordgo/voice.go +++ b/vendor/github.com/bwmarrin/discordgo/voice.go @@ -796,7 +796,7 @@ func (v *VoiceConnection) opusReceiver(udpConn *net.UDPConn, close <-chan struct } // For now, skip anything except audio. - if rlen < 12 || recvbuf[0] != 0x80 { + if rlen < 12 || (recvbuf[0] != 0x80 && recvbuf[0] != 0x90) { continue } @@ -810,8 +810,17 @@ func (v *VoiceConnection) opusReceiver(udpConn *net.UDPConn, close <-chan struct copy(nonce[:], recvbuf[0:12]) 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 { - c <- &p + select { + case c <- &p: + case <-close: + return + } } } } diff --git a/vendor/github.com/bwmarrin/discordgo/wsapi.go b/vendor/github.com/bwmarrin/discordgo/wsapi.go index 09128505..df87092e 100644 --- a/vendor/github.com/bwmarrin/discordgo/wsapi.go +++ b/vendor/github.com/bwmarrin/discordgo/wsapi.go @@ -15,7 +15,6 @@ import ( "compress/zlib" "encoding/json" "errors" - "fmt" "io" "net/http" "runtime" @@ -87,7 +86,7 @@ func (s *Session) Open() (err error) { } // 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{} @@ -131,6 +130,7 @@ func (s *Session) Open() (err error) { // lock. s.listening = make(chan interface{}) go s.listen(s.wsConn, s.listening) + s.LastHeartbeatAck = time.Now().UTC() s.Unlock() @@ -199,10 +199,13 @@ type helloOp struct { 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 // is still connected. If you do not send these heartbeats Discord will // 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") @@ -211,20 +214,26 @@ func (s *Session) heartbeat(wsConn *websocket.Conn, listening <-chan interface{} } var err error - ticker := time.NewTicker(i * time.Millisecond) + ticker := time.NewTicker(heartbeatIntervalMsec * time.Millisecond) defer ticker.Stop() for { + s.RLock() + last := s.LastHeartbeatAck + s.RUnlock() sequence := atomic.LoadInt64(s.sequence) s.log(LogInformational, "sending gateway websocket heartbeat seq %d", sequence) s.wsMutex.Lock() err = wsConn.WriteJSON(heartbeatOp{1, sequence}) s.wsMutex.Unlock() - if err != nil { - s.log(LogError, "error sending heartbeat to gateway %s, %s", s.gateway, err) - s.Lock() - s.DataReady = false - s.Unlock() + if err != nil || time.Now().UTC().Sub(last) > (heartbeatIntervalMsec*FailedHeartbeatAcks) { + if err != nil { + s.log(LogError, "error sending heartbeat to gateway %s, %s", s.gateway, err) + } else { + s.log(LogError, "haven't gotten a heartbeat ACK in %v, triggering a reconnection", time.Now().UTC().Sub(last)) + } + s.Close() + s.reconnect() return } s.Lock() @@ -241,8 +250,10 @@ func (s *Session) heartbeat(wsConn *websocket.Conn, listening <-chan interface{} } type updateStatusData struct { - IdleSince *int `json:"idle_since"` - Game *Game `json:"game"` + IdleSince *int `json:"since"` + Game *Game `json:"game"` + AFK bool `json:"afk"` + Status string `json:"status"` } type updateStatusOp struct { @@ -265,7 +276,10 @@ func (s *Session) UpdateStreamingStatus(idle int, game string, url string) (err return ErrWSNotFound } - var usd updateStatusData + usd := updateStatusData{ + Status: "online", + } + if idle > 0 { usd.IdleSince = &idle } @@ -398,7 +412,10 @@ func (s *Session) onEvent(messageType int, message []byte) { // Reconnect // Must immediately disconnect from gateway and reconnect to new gateway. if e.Operation == 7 { - // TODO + s.log(LogInformational, "Closing and reconnecting in response to Op7") + s.Close() + s.reconnect() + return } // Invalid Session @@ -426,6 +443,14 @@ func (s *Session) onEvent(messageType int, message []byte) { 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 if e.Operation != 0 { // But we probably should be doing something with them. @@ -688,6 +713,13 @@ func (s *Session) reconnect() { 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) <-time.After(wait * time.Second) diff --git a/vendor/manifest b/vendor/manifest index d10ffa30..0861b044 100644 --- a/vendor/manifest +++ b/vendor/manifest @@ -61,7 +61,7 @@ "importpath": "github.com/bwmarrin/discordgo", "repository": "https://github.com/bwmarrin/discordgo", "vcs": "git", - "revision": "d420e28024ad527390b43aa7f64e029083e11989", + "revision": "2fda7ce223a66a5b70b66987c22c3c94d022ee66", "branch": "master", "notests": true },