mirror of
https://github.com/diamondburned/arikawa.git
synced 2025-05-06 14:04:13 +00:00
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:
parent
943ca00ae5
commit
de3d0e2160
|
@ -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.
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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
75
state/event_dispatcher.go
Normal 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
42
state/events.go
Normal 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
|
||||
}
|
||||
)
|
|
@ -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()
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
51
utils/moreatomic/snowflake_set.go
Normal file
51
utils/moreatomic/snowflake_set.go
Normal 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
|
||||
}
|
Loading…
Reference in a new issue