Gateway: Split GuildCreateEvent (#116)

* Session: fix event handler loop not getting properly closed

* Implement #113

* Session: move guild events to state

* Session: close hStop
This commit is contained in:
Maximilian von Lindern 2020-06-06 22:47:15 +02:00 committed by GitHub
parent 943ca00ae5
commit de3d0e2160
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 263 additions and 40 deletions

View File

@ -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.

View File

@ -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"))

View File

@ -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)
}
}
}

75
state/event_dispatcher.go Normal file
View File

@ -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,
})
}
}

42
state/events.go Normal file
View File

@ -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
}
)

View File

@ -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()

View File

@ -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")
}
}

View File

@ -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
}