diff --git a/handler/handler.go b/handler/handler.go index 40480d9..91ad46b 100644 --- a/handler/handler.go +++ b/handler/handler.go @@ -75,6 +75,34 @@ func (h *Handler) Call(ev interface{}) { } } +// CallDirect is the same as Call, but only calls those event handlers that +// listen for this specific event, i.e. that aren't interface handlers. +func (h *Handler) CallDirect(ev interface{}) { + var evV = reflect.ValueOf(ev) + var evT = evV.Type() + + h.hmutex.RLock() + defer h.hmutex.RUnlock() + + for _, order := range h.horders { + handler, ok := h.handlers[order] + if !ok { + // This shouldn't ever happen, but we're adding this just in case. + continue + } + + if evT != handler.event { + continue + } + + if h.Synchronous { + handler.call(evV) + } else { + go handler.call(evV) + } + } +} + // WaitFor blocks until there's an event. It's advised to use ChanFor instead, // as WaitFor may skip some events if it's not ran fast enough after the event // arrived. @@ -127,6 +155,14 @@ func (h *Handler) ChanFor(fn func(interface{}) bool) (out <-chan interface{}, ca // AddHandler adds the handler, returning a function that would remove this // handler when called. +// +// GuildCreateEvents and GuildDeleteEvents will not be called on interface{} +// events. Instead their situation-specific version will be fired, as they +// provides more information about the context of the event: +// GuildReadyEvent, GuildAvailableEvent, GuildJoinEvent, GuildUnavailableEvent +// or GuildLeaveEvent +// Listening to directly to GuildCreateEvent or GuildDeleteEvent will still +// work, however. func (h *Handler) AddHandler(handler interface{}) (rm func()) { rm, err := h.addHandler(handler) if err != nil { @@ -137,6 +173,14 @@ func (h *Handler) AddHandler(handler interface{}) (rm func()) { // AddHandlerCheck adds the handler, but safe-guards reflect panics with a // recoverer, returning the error. +// +// GuildCreateEvents and GuildDeleteEvents will not be called on interface{} +// events. Instead their situation-specific version will be fired, as they +// provides more information about the context of the event: +// GuildReadyEvent, GuildAvailableEvent, GuildJoinEvent, GuildUnavailableEvent +// or GuildLeaveEvent +// Listening to directly to GuildCreateEvent or GuildDeleteEvent will still +// work, however. func (h *Handler) AddHandlerCheck(handler interface{}) (rm func(), err error) { // Reflect would actually panic if anything goes wrong, so this is just in // case. diff --git a/handler/handler_test.go b/handler/handler_test.go index 2b0e3c1..136bda4 100644 --- a/handler/handler_test.go +++ b/handler/handler_test.go @@ -35,7 +35,7 @@ func TestCall(t *testing.T) { t.Fatal("Returned results is wrong:", r) } - // Remove handler test + // Delete handler test rm() go h.Call(newMessage("astolfo")) diff --git a/session/session.go b/session/session.go index b54b4bb..6ef009a 100644 --- a/session/session.go +++ b/session/session.go @@ -4,14 +4,17 @@ package session import ( + "github.com/pkg/errors" + "github.com/diamondburned/arikawa/api" "github.com/diamondburned/arikawa/gateway" "github.com/diamondburned/arikawa/handler" - "github.com/pkg/errors" ) +var ErrMFA = errors.New("account has 2FA enabled") + // Closed is an event that's sent to Session's command handler. This works by -// using (*Gateway).AfterError. If the user sets this callback, no Closed events +// using (*Gateway).AfterClose. If the user sets this callback, no Closed events // would be sent. // // Usage @@ -22,8 +25,6 @@ type Closed struct { Error error } -var ErrMFA = errors.New("account has 2FA enabled") - // Session manages both the API and Gateway. As such, Session inherits all of // API's methods, as well has the Handler used for Gateway. type Session struct { @@ -41,19 +42,13 @@ type Session struct { } func New(token string) (*Session, error) { - // Initialize the session and the API interface - s := &Session{} - s.Handler = handler.New() - s.Client = api.NewClient(token) - // Create a gateway g, err := gateway.NewGateway(token) if err != nil { - return nil, errors.Wrap(err, "failed to connect to Gateway") + err = errors.Wrap(err, "failed to connect to Gateway") } - s.Gateway = g - return s, nil + return NewWithGateway(g), err } // Login tries to log in as a normal user account; MFA is optional. @@ -121,7 +116,7 @@ func (s *Session) startHandler(stop <-chan struct{}) { case <-stop: return case ev := <-s.Gateway.Events: - s.Handler.Call(ev) + s.Call(ev) } } } diff --git a/state/event_dispatcher.go b/state/event_dispatcher.go new file mode 100644 index 0000000..fe32a5e --- /dev/null +++ b/state/event_dispatcher.go @@ -0,0 +1,75 @@ +package state + +import ( + "github.com/diamondburned/arikawa/gateway" +) + +func (s *State) handleEvent(ev interface{}) { + if s.PreHandler != nil { + s.PreHandler.Call(ev) + } + s.Handler.Call(ev) +} + +func (s *State) handleReady(ev *gateway.ReadyEvent) { + s.handleEvent(ev) + + for _, g := range ev.Guilds { + // store this so we know when we need to dispatch a belated + // GuildReadyEvent + if g.Unavailable { + s.unreadyGuilds.Add(g.ID) + } else { + s.handleEvent(&GuildReadyEvent{ + GuildCreateEvent: &g, + }) + } + } +} + +func (s *State) handleGuildCreate(ev *gateway.GuildCreateEvent) { + // before we dispatch the specific events, we can already call the handlers + // that subscribed to the generic version + s.handleEvent(ev) + + // this guild was unavailable, but has come back online + if s.unavailableGuilds.Delete(ev.ID) { + s.handleEvent(&GuildAvailableEvent{ + GuildCreateEvent: ev, + }) + + // the guild was already unavailable when connecting to the gateway + // we can dispatch a belated GuildReadyEvent + } else if s.unreadyGuilds.Delete(ev.ID) { + s.handleEvent(&GuildReadyEvent{ + GuildCreateEvent: ev, + }) + } else { // we don't know this guild, hence we just joined it + s.handleEvent(&GuildJoinEvent{ + GuildCreateEvent: ev, + }) + } +} + +func (s *State) handleGuildDelete(ev *gateway.GuildDeleteEvent) { + // before we dispatch the specific events, we can already call the handlers + // that subscribed to the generic version + s.handleEvent(ev) + + // store this so we can later dispatch a GuildAvailableEvent, once the + // guild becomes available again. + if ev.Unavailable { + s.unavailableGuilds.Add(ev.ID) + + s.handleEvent(&GuildUnavailableEvent{ + GuildDeleteEvent: ev, + }) + } else { + // it might have been unavailable before we left + s.unavailableGuilds.Delete(ev.ID) + + s.handleEvent(&GuildLeaveEvent{ + GuildDeleteEvent: ev, + }) + } +} diff --git a/state/events.go b/state/events.go new file mode 100644 index 0000000..2b94e89 --- /dev/null +++ b/state/events.go @@ -0,0 +1,42 @@ +package state + +import "github.com/diamondburned/arikawa/gateway" + +// events that originated from GuildCreate: +type ( + // GuildReady gets fired for every guild the bot/user is in, as found in + // the Ready event. + // + // Guilds that are unavailable when connecting, will not trigger a + // GuildReadyEvent, until they become available again. + GuildReadyEvent struct { + *gateway.GuildCreateEvent + } + + // GuildAvailableEvent gets fired when a guild becomes available again, + // after being previously declared unavailable through a + // GuildUnavailableEvent. This event will not be fired for guilds that + // were already unavailable when connecting to the gateway. + GuildAvailableEvent struct { + *gateway.GuildCreateEvent + } + + // GuildJoinEvent gets fired if the bot/user joins a guild. + GuildJoinEvent struct { + *gateway.GuildCreateEvent + } +) + +// events that originated from GuildDelete: +type ( + // GuildLeaveEvent gets fired if the bot/user left a guild, was removed + // or the owner deleted the guild. + GuildLeaveEvent struct { + *gateway.GuildDeleteEvent + } + + // GuildUnavailableEvent gets fired if a guild becomes unavailable. + GuildUnavailableEvent struct { + *gateway.GuildDeleteEvent + } +) diff --git a/state/state.go b/state/state.go index 3536159..3637485 100644 --- a/state/state.go +++ b/state/state.go @@ -10,6 +10,8 @@ import ( "github.com/diamondburned/arikawa/gateway" "github.com/diamondburned/arikawa/handler" "github.com/diamondburned/arikawa/session" + "github.com/diamondburned/arikawa/utils/moreatomic" + "github.com/pkg/errors" ) @@ -48,6 +50,15 @@ type State struct { // again. fewMessages map[discord.Snowflake]struct{} fewMutex *sync.Mutex + + // unavailableGuilds is a set of discord.Snowflakes of guilds that became + // unavailable when already connected to the gateway, i.e. sent in a + // GuildUnavailableEvent. + unavailableGuilds *moreatomic.SnowflakeSet + // unreadyGuilds is a set of discord.Snowflakes of guilds that were + // unavailable when connecting to the gateway, i.e. they had Unavailable + // set to true during Ready. + unreadyGuilds *moreatomic.SnowflakeSet } func New(token string) (*State, error) { @@ -65,12 +76,14 @@ func NewWithStore(token string, store Store) (*State, error) { func NewFromSession(s *session.Session, store Store) (*State, error) { state := &State{ - Session: s, - Store: store, - Handler: handler.New(), - StateLog: func(err error) {}, - fewMessages: map[discord.Snowflake]struct{}{}, - fewMutex: new(sync.Mutex), + Session: s, + Store: store, + Handler: handler.New(), + StateLog: func(err error) {}, + fewMessages: map[discord.Snowflake]struct{}{}, + fewMutex: new(sync.Mutex), + unavailableGuilds: moreatomic.NewSnowflakeSet(), + unreadyGuilds: moreatomic.NewSnowflakeSet(), } return state, state.hookSession() diff --git a/state/state_events.go b/state/state_events.go index f2150c1..facf4a8 100644 --- a/state/state_events.go +++ b/state/state_events.go @@ -1,18 +1,26 @@ package state import ( + "github.com/pkg/errors" + "github.com/diamondburned/arikawa/discord" "github.com/diamondburned/arikawa/gateway" - "github.com/pkg/errors" ) func (s *State) hookSession() error { s.unhooker = s.Session.AddHandler(func(iface interface{}) { - if s.PreHandler != nil { - s.PreHandler.Call(iface) - } s.onEvent(iface) - s.Handler.Call(iface) + + switch e := iface.(type) { + case *gateway.ReadyEvent: + s.handleReady(e) + case *gateway.GuildCreateEvent: + s.handleGuildCreate(e) + case *gateway.GuildDeleteEvent: + s.handleGuildDelete(e) + default: + s.handleEvent(iface) + } }) return nil @@ -35,7 +43,7 @@ func (s *State) onEvent(iface interface{}) { // Handle guilds for i := range ev.Guilds { - s.batchLog(handleGuildCreate(s.Store, &ev.Guilds[i])...) + s.batchLog(storeGuildCreate(s.Store, &ev.Guilds[i])...) } // Handle private channels @@ -50,16 +58,13 @@ func (s *State) onEvent(iface interface{}) { s.stateErr(err, "failed to set self in state") } - case *gateway.GuildCreateEvent: - s.batchLog(handleGuildCreate(s.Store, ev)...) - case *gateway.GuildUpdateEvent: if err := s.Store.GuildSet((*discord.Guild)(ev)); err != nil { s.stateErr(err, "failed to update guild in state") } case *gateway.GuildDeleteEvent: - if err := s.Store.GuildRemove(ev.ID); err != nil { + if err := s.Store.GuildRemove(ev.ID); err != nil && !ev.Unavailable { s.stateErr(err, "failed to delete guild in state") } @@ -304,30 +309,28 @@ func findReaction(rs []discord.Reaction, emoji discord.Emoji) int { return -1 } -func handleGuildCreate(store Store, guild *gateway.GuildCreateEvent) []error { - // If a guild is unavailable, don't populate it in the state, as the guild - // data is very incomplete. +func storeGuildCreate(store Store, guild *gateway.GuildCreateEvent) []error { if guild.Unavailable { return nil } - stack, error := newErrorStack() + stack, errs := newErrorStack() if err := store.GuildSet(&guild.Guild); err != nil { - error(err, "failed to set guild in Ready") + errs(err, "failed to set guild in Ready") } // Handle guild emojis if guild.Emojis != nil { if err := store.EmojiSet(guild.ID, guild.Emojis); err != nil { - error(err, "failed to set guild emojis") + errs(err, "failed to set guild emojis") } } // Handle guild member for i := range guild.Members { if err := store.MemberSet(guild.ID, &guild.Members[i]); err != nil { - error(err, "failed to set guild member in Ready") + errs(err, "failed to set guild member in Ready") } } @@ -338,21 +341,21 @@ func handleGuildCreate(store Store, guild *gateway.GuildCreateEvent) []error { ch.GuildID = guild.ID if err := store.ChannelSet(&ch); err != nil { - error(err, "failed to set guild channel in Ready") + errs(err, "failed to set guild channel in Ready") } } // Handle guild presences for i := range guild.Presences { if err := store.PresenceSet(guild.ID, &guild.Presences[i]); err != nil { - error(err, "failed to set guild presence in Ready") + errs(err, "failed to set guild presence in Ready") } } // Handle guild voice states for i := range guild.VoiceStates { if err := store.VoiceStateSet(guild.ID, &guild.VoiceStates[i]); err != nil { - error(err, "failed to set guild voice state in Ready") + errs(err, "failed to set guild voice state in Ready") } } diff --git a/utils/moreatomic/snowflake_set.go b/utils/moreatomic/snowflake_set.go new file mode 100644 index 0000000..def705d --- /dev/null +++ b/utils/moreatomic/snowflake_set.go @@ -0,0 +1,51 @@ +package moreatomic + +import ( + "sync" + + "github.com/diamondburned/arikawa/discord" +) + +type SnowflakeSet struct { + set map[discord.Snowflake]struct{} + mut sync.Mutex +} + +// NewSnowflakeSet creates a new SnowflakeSet. +func NewSnowflakeSet() *SnowflakeSet { + return &SnowflakeSet{ + set: make(map[discord.Snowflake]struct{}), + } +} + +// Add adds the passed discord.Snowflake to the set. +func (s *SnowflakeSet) Add(flake discord.Snowflake) { + s.mut.Lock() + + s.set[flake] = struct{}{} + + s.mut.Unlock() +} + +// Contains checks whether the passed discord.Snowflake is present in the set. +func (s *SnowflakeSet) Contains(flake discord.Snowflake) (ok bool) { + s.mut.Lock() + defer s.mut.Unlock() + + _, ok = s.set[flake] + return +} + +// Delete deletes the passed discord.Snowflake from the set and returns true if +// the element is present. If not, Delete is a no-op and returns false. +func (s *SnowflakeSet) Delete(flake discord.Snowflake) bool { + s.mut.Lock() + defer s.mut.Unlock() + + if _, ok := s.set[flake]; ok { + delete(s.set, flake) + return true + } + + return false +}