State now handles MsgCreate's missing Member.User field, some bug fixes

This addresses discord/discord-api-docs#1440.

State documentation has been added, which documents the store and
handlers as well.

Bug fixes include:

- PreHandler being called after the state handler; it is now called
before as documented.
- Minor behavior changes regarding Guild Create events. Refer to State's
documentation.
This commit is contained in:
diamondburned (Forefront) 2020-06-19 00:33:22 -07:00
parent 1373e42fe1
commit 88dd0f8995
3 changed files with 70 additions and 32 deletions

View File

@ -4,23 +4,14 @@ 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{
s.Handler.Call(&GuildReadyEvent{
GuildCreateEvent: &g,
})
}
@ -28,47 +19,39 @@ func (s *State) handleReady(ev *gateway.ReadyEvent) {
}
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{
s.Handler.Call(&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{
s.Handler.Call(&GuildReadyEvent{
GuildCreateEvent: ev,
})
} else { // we don't know this guild, hence we just joined it
s.handleEvent(&GuildJoinEvent{
s.Handler.Call(&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{
s.Handler.Call(&GuildUnavailableEvent{
GuildDeleteEvent: ev,
})
} else {
// it might have been unavailable before we left
s.unavailableGuilds.Delete(ev.ID)
s.handleEvent(&GuildLeaveEvent{
s.Handler.Call(&GuildLeaveEvent{
GuildDeleteEvent: ev,
})
}

View File

@ -20,6 +20,42 @@ var (
MaxFetchGuilds uint = 100
)
// State is the cache to store events coming from Discord as well as data from
// API calls.
//
// Store
//
// The state basically provides abstractions on top of the API and the state
// storage (Store). The state storage is effectively a set of interfaces which
// allow arbitrary backends to be implemented.
//
// The default storage backend is a typical in-memory structure consisting of
// maps and slices. Custom backend implementations could embed this storage
// backend as an in-memory fallback. A good example of this would be embedding
// the default store for messages only, while handling everything else in Redis.
//
// The package also provides a no-op store (NoopStore) that implementations
// could embed. This no-op store will always return an error, which makes the
// state fetch information from the API. The setters are all no-ops, so the
// fetched data won't be updated.
//
// Handler
//
// The state uses its own handler over session's to make all handlers run after
// the state updates itself. A PreHandler is exposed in any case the user needs
// the handlers to run before the state updates itself. Refer to that field's
// documentation.
//
// The state also provides extra events and overrides to make up for Discord's
// inconsistencies in data. The following are known instances of such.
//
// The Guild Create event is split up to make the state's Guild Available, Guild
// Ready and Guild Join events. Refer to these events' documentations for more
// information.
//
// The Message Create and Message Update events with the Member field provided
// will have the User field copied from Author. This is because the User field
// will be empty, while the Member structure expects it to be there.
type State struct {
*session.Session
Store
@ -41,7 +77,7 @@ type State struct {
// Command handler with inherited methods. Ran after PreHandler. You should
// most of the time use this instead of Session's, to avoid race conditions
// with the State
// with the State.
*handler.Handler
unhooker func()

View File

@ -7,23 +7,42 @@ import (
"github.com/diamondburned/arikawa/gateway"
)
func (s *State) hookSession() error {
s.unhooker = s.Session.AddHandler(func(iface interface{}) {
s.onEvent(iface)
func (s *State) hookSession() {
s.unhooker = s.Session.AddHandler(func(event interface{}) {
// Call the pre-handler before the state handler.
if s.PreHandler != nil {
s.PreHandler.Call(event)
}
switch e := iface.(type) {
// Run the state handler.
s.onEvent(event)
// Always call the event on function exit. Extra events constructed by
// handlers will be called before the main event, but that's fine.
defer s.Handler.Call(event)
switch e := event.(type) {
case *gateway.ReadyEvent:
s.handleReady(e)
return
case *gateway.GuildCreateEvent:
s.handleGuildCreate(e)
return
case *gateway.GuildDeleteEvent:
s.handleGuildDelete(e)
default:
s.handleEvent(iface)
return
// https://github.com/discord/discord-api-docs/commit/01665c4
case *gateway.MessageCreateEvent:
if e.Member != nil {
e.Member.User = e.Author
}
case *gateway.MessageUpdateEvent:
if e.Member != nil {
e.Member.User = e.Author
}
}
})
return nil
}
func (s *State) onEvent(iface interface{}) {