diff --git a/discord.go b/discord.go index 9a6da40..9b5faf1 100644 --- a/discord.go +++ b/discord.go @@ -122,11 +122,11 @@ func New(args ...interface{}) (s *Session, err error) { return } -// AddHandler allows you to add an event handler that will be fired anytime -// the given event is triggered. -func (s *Session) AddHandler(handler interface{}) { - s.initialize() - +// validateHandler takes an event handler func, and returns the type of event. +// eg. +// Session.validateHandler(func (s *discordgo.Session, m *discordgo.MessageCreate)) +// will return the reflect.Type of *discordgo.MessageCreate +func (s *Session) validateHandler(handler interface{}) reflect.Type { handlerType := reflect.TypeOf(handler) if handlerType.NumIn() != 2 { @@ -137,9 +137,6 @@ func (s *Session) AddHandler(handler interface{}) { panic("Unable to add event handler, first argument must be of type *discordgo.Session.") } - s.Lock() - defer s.Unlock() - eventType := handlerType.In(1) // Support handlers of type interface{}, this is a special handler, which is triggered on every event. @@ -147,18 +144,62 @@ func (s *Session) AddHandler(handler interface{}) { eventType = nil } + return eventType +} + +// AddHandler allows you to add an event handler that will be fired anytime +// the Discord WSAPI event that matches the interface fires. +// eventToInterface in events.go has a list of all the Discord WSAPI events +// and their respective interface. +// eg: +// Session.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate) { +// }) +// +// or: +// Session.AddHandler(func(s *discordgo.Session, m *discordgo.PresenceUpdate) { +// }) +// The return value of this method is a function, that when called will remove the +// event handler. +func (s *Session) AddHandler(handler interface{}) func() { + s.initialize() + + eventType := s.validateHandler(handler) + + s.handlersMu.Lock() + defer s.handlersMu.Unlock() + + h := reflect.ValueOf(handler) + handlers := s.handlers[eventType] if handlers == nil { handlers = []reflect.Value{} } - s.handlers[eventType] = append(handlers, reflect.ValueOf(handler)) + s.handlers[eventType] = append(handlers, h) + + // This must be done as we need a consistent reference to the + // reflected value, otherwise a RemoveHandler method would have + // been nice. + return func() { + s.handlersMu.Lock() + defer s.handlersMu.Unlock() + + handlers := s.handlers[eventType] + for i, v := range handlers { + if h == v { + s.handlers[eventType] = append(handlers[:i], handlers[i+1:]...) + return + } + } + } } +// handle calls any handlers that match the event type and any handlers of +// interface{}. func (s *Session) handle(event interface{}) { s.initialize() - s.RLock() - defer s.RUnlock() + s.handlersMu.RLock() + defer s.handlersMu.RUnlock() handlerParameters := []reflect.Value{reflect.ValueOf(s), reflect.ValueOf(event)} @@ -177,16 +218,16 @@ func (s *Session) handle(event interface{}) { // initialize adds all internal handlers and state tracking handlers. func (s *Session) initialize() { - s.RLock() + s.handlersMu.RLock() if s.handlers != nil { - s.RUnlock() + s.handlersMu.RUnlock() return } - s.RUnlock() + s.handlersMu.RUnlock() - s.Lock() + s.handlersMu.Lock() s.handlers = map[interface{}][]reflect.Value{} - s.Unlock() + s.handlersMu.Unlock() s.AddHandler(s.onEvent) s.AddHandler(s.onReady) diff --git a/discord_test.go b/discord_test.go index 7442651..8f68e43 100644 --- a/discord_test.go +++ b/discord_test.go @@ -225,15 +225,15 @@ func TestOpenClose(t *testing.T) { } } -func TestHandlers(t *testing.T) { - testHandlerCalled := false - testHandler := func(s *Session, t *testing.T) { - testHandlerCalled = true +func TestAddHandler(t *testing.T) { + testHandlerCalled := 0 + testHandler := func(s *Session, m *MessageCreate) { + testHandlerCalled++ } - interfaceHandlerCalled := false + interfaceHandlerCalled := 0 interfaceHandler := func(s *Session, i interface{}) { - interfaceHandlerCalled = true + interfaceHandlerCalled++ } bogusHandlerCalled := false @@ -243,20 +243,45 @@ func TestHandlers(t *testing.T) { d := Session{} d.AddHandler(testHandler) + d.AddHandler(testHandler) + d.AddHandler(interfaceHandler) d.AddHandler(bogusHandler) - d.handle(t) + d.handle(&MessageCreate{}) + d.handle(&MessageDelete{}) - if !testHandlerCalled { - t.Fatalf("testHandler was not called.") + // testHandler will be called twice because it was added twice. + if testHandlerCalled != 2 { + t.Fatalf("testHandler was not called twice.") } - if !interfaceHandlerCalled { - t.Fatalf("interfaceHandler was not called.") + // interfaceHandler will be called twice for each event. + if interfaceHandlerCalled != 2 { + t.Fatalf("interfaceHandler was not called twice.") } if bogusHandlerCalled { t.Fatalf("bogusHandler was called.") } } + +func TestRemoveHandler(t *testing.T) { + testHandlerCalled := 0 + testHandler := func(s *Session, m *MessageCreate) { + testHandlerCalled++ + } + + d := Session{} + r := d.AddHandler(testHandler) + + d.handle(&MessageCreate{}) + + r() + + d.handle(&MessageCreate{}) + + if testHandlerCalled != 1 { + t.Fatalf("testHandler was not called once.") + } +} diff --git a/events.go b/events.go index d81ecef..d21f22a 100644 --- a/events.go +++ b/events.go @@ -1,5 +1,47 @@ package discordgo +// eventToInterface is a mapping of Discord WSAPI events to their +// DiscordGo event container. +// Each Discord WSAPI event maps to a unique interface. +// Use Session.AddHandler with one of these types to handle that +// type of event. +// eg: +// Session.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate) { +// }) +// +// or: +// Session.AddHandler(func(s *discordgo.Session, m *discordgo.PresenceUpdate) { +// }) +var eventToInterface = map[string]interface{}{ + "CHANNEL_CREATE": ChannelCreate{}, + "CHANNEL_UPDATE": ChannelUpdate{}, + "CHANNEL_DELETE": ChannelDelete{}, + "GUILD_CREATE": GuildCreate{}, + "GUILD_UPDATE": GuildUpdate{}, + "GUILD_DELETE": GuildDelete{}, + "GUILD_BAN_ADD": GuildBanAdd{}, + "GUILD_BAN_REMOVE": GuildBanRemove{}, + "GUILD_MEMBER_ADD": GuildMemberAdd{}, + "GUILD_MEMBER_UPDATE": GuildMemberUpdate{}, + "GUILD_MEMBER_REMOVE": GuildMemberRemove{}, + "GUILD_ROLE_CREATE": GuildRoleCreate{}, + "GUILD_ROLE_UPDATE": GuildRoleUpdate{}, + "GUILD_ROLE_DELETE": GuildRoleDelete{}, + "GUILD_INTEGRATIONS_UPDATE": GuildIntegrationsUpdate{}, + "GUILD_EMOJIS_UPDATE": GuildEmojisUpdate{}, + "MESSAGE_ACK": MessageAck{}, + "MESSAGE_CREATE": MessageCreate{}, + "MESSAGE_UPDATE": MessageUpdate{}, + "MESSAGE_DELETE": MessageDelete{}, + "PRESENCE_UPDATE": PresenceUpdate{}, + "READY": Ready{}, + "USER_UPDATE": UserUpdate{}, + "USER_SETTINGS_UPDATE": UserSettingsUpdate{}, + "TYPING_START": TypingStart{}, + "VOICE_SERVER_UPDATE": VoiceServerUpdate{}, + "VOICE_STATE_UPDATE": VoiceStateUpdate{}, +} + // Connect is an empty struct for an event. type Connect struct{} diff --git a/structs.go b/structs.go index 0d10c1c..5bffead 100644 --- a/structs.go +++ b/structs.go @@ -61,7 +61,8 @@ type Session struct { // StateEnabled is true. State *State - // This is a mapping of event structs to a reflected value + handlersMu sync.RWMutex + // This is a mapping of event struct to a reflected value // for event handlers. // We store the reflected value instead of the function // reference as it is more performant, instead of re-reflecting diff --git a/wsapi.go b/wsapi.go index 6090da9..a2bbef5 100644 --- a/wsapi.go +++ b/wsapi.go @@ -245,38 +245,6 @@ func (s *Session) UpdateStatus(idle int, game string) (err error) { return } -// eventToInterface is a mapping of Discord WSAPI events to their -// DiscordGo event container. -var eventToInterface = map[string]interface{}{ - "CHANNEL_CREATE": ChannelCreate{}, - "CHANNEL_UPDATE": ChannelUpdate{}, - "CHANNEL_DELETE": ChannelDelete{}, - "GUILD_CREATE": GuildCreate{}, - "GUILD_UPDATE": GuildUpdate{}, - "GUILD_DELETE": GuildDelete{}, - "GUILD_BAN_ADD": GuildBanAdd{}, - "GUILD_BAN_REMOVE": GuildBanRemove{}, - "GUILD_MEMBER_ADD": GuildMemberAdd{}, - "GUILD_MEMBER_UPDATE": GuildMemberUpdate{}, - "GUILD_MEMBER_REMOVE": GuildMemberRemove{}, - "GUILD_ROLE_CREATE": GuildRoleCreate{}, - "GUILD_ROLE_UPDATE": GuildRoleUpdate{}, - "GUILD_ROLE_DELETE": GuildRoleDelete{}, - "GUILD_INTEGRATIONS_UPDATE": GuildIntegrationsUpdate{}, - "GUILD_EMOJIS_UPDATE": GuildEmojisUpdate{}, - "MESSAGE_ACK": MessageAck{}, - "MESSAGE_CREATE": MessageCreate{}, - "MESSAGE_UPDATE": MessageUpdate{}, - "MESSAGE_DELETE": MessageDelete{}, - "PRESENCE_UPDATE": PresenceUpdate{}, - "READY": Ready{}, - "USER_UPDATE": UserUpdate{}, - "USER_SETTINGS_UPDATE": UserSettingsUpdate{}, - "TYPING_START": TypingStart{}, - "VOICE_SERVER_UPDATE": VoiceServerUpdate{}, - "VOICE_STATE_UPDATE": VoiceStateUpdate{}, -} - // Front line handler for all Websocket Events. Determines the // event type and passes the message along to the next handler.