diff --git a/endpoints.go b/endpoints.go index 06e3e9e..5c1d71d 100644 --- a/endpoints.go +++ b/endpoints.go @@ -108,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/event.go b/event.go index 906c2aa..1524f32 100644 --- a/event.go +++ b/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 } diff --git a/ratelimit.go b/ratelimit.go index 876e98a..223c0d0 100644 --- a/ratelimit.go +++ b/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/restapi.go b/restapi.go index 59c4907..0bcd2ba 100644 --- a/restapi.go +++ b/restapi.go @@ -767,7 +767,7 @@ func (s *Session) GuildMemberDelete(guildID, userID string) (err error) { return s.GuildMemberDeleteWithReason(guildID, userID, "") } -// GuildMemberDelete removes the given user from the given guild. +// 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 @@ -1932,6 +1932,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/state.go b/state.go index 4ebfb1e..b545c53 100644 --- a/state.go +++ b/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 @@ -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 diff --git a/structs.go b/structs.go index d9f9699..e811e9e 100644 --- a/structs.go +++ b/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 @@ -162,6 +166,7 @@ type Channel struct { Topic string `json:"topic"` Type ChannelType `json:"type"` LastMessageID string `json:"last_message_id"` + NSFW bool `json:"nsfw"` Position int `json:"position"` Bitrate int `json:"bitrate"` Recipients []*User `json:"recipient"` @@ -598,3 +603,55 @@ const ( PermissionManageServer | PermissionAdministrator ) + +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/wsapi.go b/wsapi.go index d050ffd..baec234 100644 --- a/wsapi.go +++ b/wsapi.go @@ -199,7 +199,7 @@ type helloOp struct { Trace []string `json:"_trace"` } -// Number of heartbeat intervals to wait until forcing a connection restart. +// 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