diff --git a/README.md b/README.md index 39b04ce83..42012ebec 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,13 @@ See https://github.com/slack-go/slack/blob/master/examples/websocket/websocket.g See https://github.com/slack-go/slack/blob/master/examples/eventsapi/events.go +## Socketmode Event Handler (Experimental) +When using socket mode, dealing with an event can be pretty lengthy as it requires you to route the event to the right place. + +Instead, you can use `SocketmodeHandler` much like you use an HTTP handler to register which event you would like to listen to and what callback function will process that event when it occurs. + +See [./examples/socketmode_handler/socketmode_handler.go](./examples/socketmode_handler/socketmode_handler.go) ## Contributing You are more than welcome to contribute to this project. Fork and diff --git a/examples/socketmode_handler/socketmode_handler.go b/examples/socketmode_handler/socketmode_handler.go new file mode 100644 index 000000000..9991e8c3c --- /dev/null +++ b/examples/socketmode_handler/socketmode_handler.go @@ -0,0 +1,205 @@ +package main + +import ( + "fmt" + "log" + "os" + "strings" + + "github.com/slack-go/slack/slackevents" + "github.com/slack-go/slack/socketmode" + + "github.com/slack-go/slack" +) + +func main() { + appToken := os.Getenv("SLACK_APP_TOKEN") + if appToken == "" { + + } + + if !strings.HasPrefix(appToken, "xapp-") { + panic("SLACK_APP_TOKEN must have the prefix \"xapp-\".") + } + + botToken := os.Getenv("SLACK_BOT_TOKEN") + if botToken == "" { + panic("SLACK_BOT_TOKEN must be set.\n") + } + + if !strings.HasPrefix(botToken, "xoxb-") { + panic("SLACK_BOT_TOKEN must have the prefix \"xoxb-\".") + } + + api := slack.New( + botToken, + slack.OptionDebug(true), + slack.OptionLog(log.New(os.Stdout, "api: ", log.Lshortfile|log.LstdFlags)), + slack.OptionAppLevelToken(appToken), + ) + + client := socketmode.New( + api, + socketmode.OptionDebug(true), + socketmode.OptionLog(log.New(os.Stdout, "socketmode: ", log.Lshortfile|log.LstdFlags)), + ) + + socketmodeHandler := socketmode.NewSocketmodeHandler(client) + + socketmodeHandler.Handle(socketmode.EventTypeConnecting, middlewareConnecting) + socketmodeHandler.Handle(socketmode.EventTypeConnectionError, middlewareConnectionError) + socketmodeHandler.Handle(socketmode.EventTypeConnected, middlewareConnected) + + //\\ EventTypeEventsAPI //\\ + // Handle all EventsAPI + socketmodeHandler.Handle(socketmode.EventTypeEventsAPI, middlewareEventsAPI) + + // Handle a specific event from EventsAPI + socketmodeHandler.HandleEvents(slackevents.AppMention, middlewareAppMentionEvent) + + //\\ EventTypeInteractive //\\ + // Handle all Interactive Events + socketmodeHandler.Handle(socketmode.EventTypeInteractive, middlewareInteractive) + + // Handle a specific Interaction + socketmodeHandler.HandleInteraction(slack.InteractionTypeBlockActions, middlewareInteractionTypeBlockActions) + + // Handle all SlashCommand + socketmodeHandler.Handle(socketmode.EventTypeSlashCommand, middlewareSlashCommand) + socketmodeHandler.HandleSlashCommand("/rocket", middlewareSlashCommand) + + // socketmodeHandler.HandleDefault(middlewareDefault) + + socketmodeHandler.RunEventLoop() +} + +func middlewareConnecting(evt *socketmode.Event, client *socketmode.Client) { + fmt.Println("Connecting to Slack with Socket Mode...") +} + +func middlewareConnectionError(evt *socketmode.Event, client *socketmode.Client) { + fmt.Println("Connection failed. Retrying later...") +} + +func middlewareConnected(evt *socketmode.Event, client *socketmode.Client) { + fmt.Println("Connected to Slack with Socket Mode.") +} + +func middlewareEventsAPI(evt *socketmode.Event, client *socketmode.Client) { + fmt.Println("middlewareEventsAPI") + eventsAPIEvent, ok := evt.Data.(slackevents.EventsAPIEvent) + if !ok { + fmt.Printf("Ignored %+v\n", evt) + return + } + + fmt.Printf("Event received: %+v\n", eventsAPIEvent) + + client.Ack(*evt.Request) + + switch eventsAPIEvent.Type { + case slackevents.CallbackEvent: + innerEvent := eventsAPIEvent.InnerEvent + switch ev := innerEvent.Data.(type) { + case *slackevents.AppMentionEvent: + fmt.Printf("We have been mentionned in %v", ev.Channel) + _, _, err := client.Client.PostMessage(ev.Channel, slack.MsgOptionText("Yes, hello.", false)) + if err != nil { + fmt.Printf("failed posting message: %v", err) + } + case *slackevents.MemberJoinedChannelEvent: + fmt.Printf("user %q joined to channel %q", ev.User, ev.Channel) + } + default: + client.Debugf("unsupported Events API event received") + } +} + +func middlewareAppMentionEvent(evt *socketmode.Event, client *socketmode.Client) { + + eventsAPIEvent, ok := evt.Data.(slackevents.EventsAPIEvent) + if !ok { + fmt.Printf("Ignored %+v\n", evt) + return + } + + client.Ack(*evt.Request) + + ev, ok := eventsAPIEvent.InnerEvent.Data.(*slackevents.AppMentionEvent) + if !ok { + fmt.Printf("Ignored %+v\n", ev) + return + } + + fmt.Printf("We have been mentionned in %v\n", ev.Channel) + _, _, err := client.Client.PostMessage(ev.Channel, slack.MsgOptionText("Yes, hello.", false)) + if err != nil { + fmt.Printf("failed posting message: %v", err) + } +} + +func middlewareInteractive(evt *socketmode.Event, client *socketmode.Client) { + callback, ok := evt.Data.(slack.InteractionCallback) + if !ok { + fmt.Printf("Ignored %+v\n", evt) + return + } + + fmt.Printf("Interaction received: %+v\n", callback) + + var payload interface{} + + switch callback.Type { + case slack.InteractionTypeBlockActions: + // See https://api.slack.com/apis/connections/socket-implement#button + client.Debugf("button clicked!") + case slack.InteractionTypeShortcut: + case slack.InteractionTypeViewSubmission: + // See https://api.slack.com/apis/connections/socket-implement#modal + case slack.InteractionTypeDialogSubmission: + default: + + } + + client.Ack(*evt.Request, payload) +} + +func middlewareInteractionTypeBlockActions(evt *socketmode.Event, client *socketmode.Client) { + client.Debugf("button clicked!") +} + +func middlewareSlashCommand(evt *socketmode.Event, client *socketmode.Client) { + cmd, ok := evt.Data.(slack.SlashCommand) + if !ok { + fmt.Printf("Ignored %+v\n", evt) + return + } + + client.Debugf("Slash command received: %+v", cmd) + + payload := map[string]interface{}{ + "blocks": []slack.Block{ + slack.NewSectionBlock( + &slack.TextBlockObject{ + Type: slack.MarkdownType, + Text: "foo", + }, + nil, + slack.NewAccessory( + slack.NewButtonBlockElement( + "", + "somevalue", + &slack.TextBlockObject{ + Type: slack.PlainTextType, + Text: "bar", + }, + ), + ), + ), + }} + client.Ack(*evt.Request, payload) +} + +func middlewareDefault(evt *socketmode.Event, client *socketmode.Client) { + // fmt.Fprintf(os.Stderr, "Unexpected event type received: %s\n", evt.Type) +} diff --git a/slackevents/inner_events.go b/slackevents/inner_events.go index de98c280e..1c262e73a 100644 --- a/slackevents/inner_events.go +++ b/slackevents/inner_events.go @@ -429,71 +429,73 @@ func (e MessageEvent) IsEdited() bool { e.Message.Edited != nil } +type EventsAPIType string + const ( // AppMention is an Events API subscribable event - AppMention = "app_mention" + AppMention = EventsAPIType("app_mention") // AppHomeOpened Your Slack app home was opened - AppHomeOpened = "app_home_opened" + AppHomeOpened = EventsAPIType("app_home_opened") // AppUninstalled Your Slack app was uninstalled. - AppUninstalled = "app_uninstalled" + AppUninstalled = EventsAPIType("app_uninstalled") // ChannelCreated is sent when a new channel is created. - ChannelCreated = "channel_created" + ChannelCreated = EventsAPIType("channel_created") // ChannelDeleted is sent when a channel is deleted. - ChannelDeleted = "channel_deleted" + ChannelDeleted = EventsAPIType("channel_deleted") // ChannelArchive is sent when a channel is archived. - ChannelArchive = "channel_archive" + ChannelArchive = EventsAPIType("channel_archive") // ChannelUnarchive is sent when a channel is unarchived. - ChannelUnarchive = "channel_unarchive" + ChannelUnarchive = EventsAPIType("channel_unarchive") // ChannelLeft is sent when a channel is left. - ChannelLeft = "channel_left" + ChannelLeft = EventsAPIType("channel_left") // ChannelRename is sent when a channel is rename. - ChannelRename = "channel_rename" + ChannelRename = EventsAPIType("channel_rename") // ChannelIDChanged is sent when a channel identifier is changed. ChannelIDChanged = "channel_id_changed" // GroupDeleted is sent when a group is deleted. - GroupDeleted = "group_deleted" + GroupDeleted = EventsAPIType("group_deleted") // GroupArchive is sent when a group is archived. - GroupArchive = "group_archive" + GroupArchive = EventsAPIType("group_archive") // GroupUnarchive is sent when a group is unarchived. - GroupUnarchive = "group_unarchive" + GroupUnarchive = EventsAPIType("group_unarchive") // GroupLeft is sent when a group is left. - GroupLeft = "group_left" + GroupLeft = EventsAPIType("group_left") // GroupRename is sent when a group is renamed. - GroupRename = "group_rename" + GroupRename = EventsAPIType("group_rename") // GridMigrationFinished An enterprise grid migration has finished on this workspace. - GridMigrationFinished = "grid_migration_finished" + GridMigrationFinished = EventsAPIType("grid_migration_finished") // GridMigrationStarted An enterprise grid migration has started on this workspace. - GridMigrationStarted = "grid_migration_started" + GridMigrationStarted = EventsAPIType("grid_migration_started") // LinkShared A message was posted containing one or more links relevant to your application - LinkShared = "link_shared" + LinkShared = EventsAPIType("link_shared") // Message A message was posted to a channel, private channel (group), im, or mim - Message = "message" + Message = EventsAPIType("message") // Member Joined Channel - MemberJoinedChannel = "member_joined_channel" + MemberJoinedChannel = EventsAPIType("member_joined_channel") // Member Left Channel - MemberLeftChannel = "member_left_channel" + MemberLeftChannel = EventsAPIType("member_left_channel") // PinAdded An item was pinned to a channel - PinAdded = "pin_added" + PinAdded = EventsAPIType("pin_added") // PinRemoved An item was unpinned from a channel - PinRemoved = "pin_removed" + PinRemoved = EventsAPIType("pin_removed") // ReactionAdded An reaction was added to a message - ReactionAdded = "reaction_added" + ReactionAdded = EventsAPIType("reaction_added") // ReactionRemoved An reaction was removed from a message - ReactionRemoved = "reaction_removed" + ReactionRemoved = EventsAPIType("reaction_removed") // TeamJoin A new user joined the workspace - TeamJoin = "team_join" + TeamJoin = EventsAPIType("team_join") // TokensRevoked APP's API tokes are revoked - TokensRevoked = "tokens_revoked" + TokensRevoked = EventsAPIType("tokens_revoked") // EmojiChanged A custom emoji has been added or changed - EmojiChanged = "emoji_changed" + EmojiChanged = EventsAPIType("emoji_changed") // WorkflowStepExecute Happens, if a workflow step of your app is invoked - WorkflowStepExecute = "workflow_step_execute" + WorkflowStepExecute = EventsAPIType("workflow_step_execute") ) // EventsAPIInnerEventMapping maps INNER Event API events to their corresponding struct // implementations. The structs should be instances of the unmarshalling // target for the matching event type. -var EventsAPIInnerEventMapping = map[string]interface{}{ +var EventsAPIInnerEventMapping = map[EventsAPIType]interface{}{ AppMention: AppMentionEvent{}, AppHomeOpened: AppHomeOpenedEvent{}, AppUninstalled: AppUninstalledEvent{}, diff --git a/slackevents/parsers.go b/slackevents/parsers.go index 0cc584c76..114dba867 100644 --- a/slackevents/parsers.go +++ b/slackevents/parsers.go @@ -19,7 +19,7 @@ func eventsMap(t string) (interface{}, bool) { // Must parse EventsAPI FIRST as both RTM and EventsAPI // have a type: "Message" event. // TODO: Handle these cases more explicitly. - v, exists := EventsAPIInnerEventMapping[t] + v, exists := EventsAPIInnerEventMapping[EventsAPIType(t)] if exists { return v, exists } diff --git a/slacktest/data.go b/slacktest/data.go index 37d413a0d..e33b901dc 100644 --- a/slacktest/data.go +++ b/slacktest/data.go @@ -35,6 +35,12 @@ var okWebResponse = slack.SlackResponse{ Ok: true, } +var defaultOkJSON = fmt.Sprintf(` + { + "ok": true + } + `) + var defaultChannelsListJSON = fmt.Sprintf(` { "ok": true, diff --git a/slacktest/handlers.go b/slacktest/handlers.go index 8dfe451df..6ac37c289 100644 --- a/slacktest/handlers.go +++ b/slacktest/handlers.go @@ -108,6 +108,16 @@ func inviteConversationHandler(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(inviteConversationJSON)) } +// handle groups.list +func listGroupsHandler(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write([]byte(defaultGroupsListJSON)) +} + +// handle reaction.Add +func reactionAddHandler(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write([]byte(defaultOkJSON)) +} + // handle chat.postMessage func (sts *Server) postMessageHandler(w http.ResponseWriter, r *http.Request) { serverAddr := r.Context().Value(ServerBotHubNameContextKey).(string) diff --git a/slacktest/server.go b/slacktest/server.go index aa6c0a180..1b18923a7 100644 --- a/slacktest/server.go +++ b/slacktest/server.go @@ -59,6 +59,7 @@ func NewTestServer(custom ...binder) *Server { s.Handle("/users.lookupByEmail", usersInfoHandler) s.Handle("/bots.info", botsInfoHandler) s.Handle("/auth.test", authTestHandler) + s.Handle("/reactions.add", reactionAddHandler) httpserver := httptest.NewUnstartedServer(s.mux) addr := httpserver.Listener.Addr().String() diff --git a/socketmode/socketmode_handler.go b/socketmode/socketmode_handler.go new file mode 100644 index 000000000..636feefa5 --- /dev/null +++ b/socketmode/socketmode_handler.go @@ -0,0 +1,243 @@ +package socketmode + +import ( + "github.com/slack-go/slack" + "github.com/slack-go/slack/slackevents" +) + +type SocketmodeHandler struct { + Client *Client + + //lvl 1 - the most generic type of event + EventMap map[EventType][]SocketmodeHandlerFunc + //lvl 2 - Manage event by inner type + InteractionEventMap map[slack.InteractionType][]SocketmodeHandlerFunc + EventApiMap map[slackevents.EventsAPIType][]SocketmodeHandlerFunc + //lvl 3 - the most userfriendly way of managing event + InteractionBlockActionEventMap map[string]SocketmodeHandlerFunc + SlashCommandMap map[string]SocketmodeHandlerFunc + + Default SocketmodeHandlerFunc +} + +// Handler have access to the event and socketmode client +type SocketmodeHandlerFunc func(*Event, *Client) + +// Middleware accept SocketmodeHandlerFunc, and return SocketmodeHandlerFunc +type SocketmodeMiddlewareFunc func(SocketmodeHandlerFunc) SocketmodeHandlerFunc + +// Initialization constructor for SocketmodeHandler +func NewSocketmodeHandler(client *Client) *SocketmodeHandler { + eventMap := make(map[EventType][]SocketmodeHandlerFunc) + interactionEventMap := make(map[slack.InteractionType][]SocketmodeHandlerFunc) + eventApiMap := make(map[slackevents.EventsAPIType][]SocketmodeHandlerFunc) + + interactionBlockActionEventMap := make(map[string]SocketmodeHandlerFunc) + slackCommandMap := make(map[string]SocketmodeHandlerFunc) + + return &SocketmodeHandler{ + Client: client, + EventMap: eventMap, + EventApiMap: eventApiMap, + InteractionEventMap: interactionEventMap, + InteractionBlockActionEventMap: interactionBlockActionEventMap, + SlashCommandMap: slackCommandMap, + Default: func(e *Event, c *Client) { + c.log.Printf("Unexpected event type received: %v\n", e.Type) + }, + } +} + +// Register a middleware or handler for an Event from socketmode +// This most general entrypoint +func (r *SocketmodeHandler) Handle(et EventType, f SocketmodeHandlerFunc) { + r.EventMap[et] = append(r.EventMap[et], f) +} + +// Register a middleware or handler for an Interaction +// There is several types of interactions, decated functions lets you better handle them +// See +// * HandleInteractionBlockAction +// * (Not Implemented) HandleShortcut +// * (Not Implemented) HandleView +func (r *SocketmodeHandler) HandleInteraction(et slack.InteractionType, f SocketmodeHandlerFunc) { + r.InteractionEventMap[et] = append(r.InteractionEventMap[et], f) +} + +// Register a middleware or handler for a Block Action referenced by its ActionID +func (r *SocketmodeHandler) HandleInteractionBlockAction(actionID string, f SocketmodeHandlerFunc) { + if actionID == "" { + panic("invalid command cannot be empty") + } + if f == nil { + panic("invalid handler cannot be nil") + } + if _, exist := r.InteractionBlockActionEventMap[actionID]; exist { + panic("multiple registrations for actionID" + actionID) + } + r.InteractionBlockActionEventMap[actionID] = f +} + +// Register a middleware or handler for an Event (from slackevents) +func (r *SocketmodeHandler) HandleEvents(et slackevents.EventsAPIType, f SocketmodeHandlerFunc) { + r.EventApiMap[et] = append(r.EventApiMap[et], f) +} + +// Register a middleware or handler for a Slash Command +func (r *SocketmodeHandler) HandleSlashCommand(command string, f SocketmodeHandlerFunc) { + if command == "" { + panic("invalid command cannot be empty") + } + if f == nil { + panic("invalid handler cannot be nil") + } + if _, exist := r.SlashCommandMap[command]; exist { + panic("multiple registrations for command" + command) + } + r.SlashCommandMap[command] = f +} + +// Register a middleware or handler to use as a last resort +func (r *SocketmodeHandler) HandleDefault(f SocketmodeHandlerFunc) { + r.Default = f +} + +// RunSlackEventLoop receives the event via the socket +func (r *SocketmodeHandler) RunEventLoop() error { + + go r.runEventLoop() + + return r.Client.Run() +} + +// Call the dispatcher for each incomming event +func (r *SocketmodeHandler) runEventLoop() { + for evt := range r.Client.Events { + r.dispatcher(evt) + } +} + +// Dispatch events to the specialized dispatcher +func (r *SocketmodeHandler) dispatcher(evt Event) { + var ishandled bool + + // Some eventType can be further decomposed + switch evt.Type { + case EventTypeInteractive: + ishandled = r.interactionDispatcher(&evt) + case EventTypeEventsAPI: + ishandled = r.eventAPIDispatcher(&evt) + case EventTypeSlashCommand: + ishandled = r.slashCommandDispatcher(&evt) + default: + ishandled = r.socketmodeDispatcher(&evt) + } + + if !ishandled { + go r.Default(&evt, r.Client) + } +} + +// Dispatch socketmode events to the registered middleware +func (r *SocketmodeHandler) socketmodeDispatcher(evt *Event) bool { + if handlers, ok := r.EventMap[evt.Type]; ok { + // If we registered an event + for _, f := range handlers { + go f(evt, r.Client) + } + + return true + } + + return false +} + +// Dispatch interactions to the registered middleware +func (r *SocketmodeHandler) interactionDispatcher(evt *Event) bool { + var ishandled bool = false + + interaction, ok := evt.Data.(slack.InteractionCallback) + if !ok { + r.Client.log.Printf("Ignored %+v\n", evt) + return false + } + + // Level 1 - socketmode EventType + ishandled = r.socketmodeDispatcher(evt) + + // Level 2 - interaction EventType + if handlers, ok := r.InteractionEventMap[interaction.Type]; ok { + // If we registered an event + for _, f := range handlers { + go f(evt, r.Client) + } + + ishandled = true + } + + // Level 3 - interaction with actionID + blockActions := interaction.ActionCallback.BlockActions + // outmoded approach won`t be implemented + // attachments_actions := interaction.ActionCallback.AttachmentActions + + for _, action := range blockActions { + if handler, ok := r.InteractionBlockActionEventMap[action.ActionID]; ok { + + go handler(evt, r.Client) + + ishandled = true + } + } + return ishandled +} + +// Dispatch eventAPI events to the registered middleware +func (r *SocketmodeHandler) eventAPIDispatcher(evt *Event) bool { + var ishandled bool = false + eventsAPIEvent, ok := evt.Data.(slackevents.EventsAPIEvent) + if !ok { + r.Client.log.Printf("Ignored %+v\n", evt) + return false + } + + innerEventType := slackevents.EventsAPIType(eventsAPIEvent.InnerEvent.Type) + + // Level 1 - socketmode EventType + ishandled = r.socketmodeDispatcher(evt) + + // Level 2 - EventAPI EventType + if handlers, ok := r.EventApiMap[innerEventType]; ok { + // If we registered an event + for _, f := range handlers { + go f(evt, r.Client) + } + + ishandled = true + } + + return ishandled +} + +// Dispatch SlashCommands events to the registered middleware +func (r *SocketmodeHandler) slashCommandDispatcher(evt *Event) bool { + var ishandled bool = false + slashCommandEvent, ok := evt.Data.(slack.SlashCommand) + if !ok { + r.Client.log.Printf("Ignored %+v\n", evt) + return false + } + + // Level 1 - socketmode EventType + ishandled = r.socketmodeDispatcher(evt) + + // Level 2 - SlackCommand by name + if handler, ok := r.SlashCommandMap[slashCommandEvent.Command]; ok { + + go handler(evt, r.Client) + + ishandled = true + } + + return ishandled + +} diff --git a/socketmode/socketmode_handler_test.go b/socketmode/socketmode_handler_test.go new file mode 100644 index 000000000..623e15ef9 --- /dev/null +++ b/socketmode/socketmode_handler_test.go @@ -0,0 +1,515 @@ +package socketmode + +import ( + "log" + "os" + "reflect" + "runtime" + "testing" + + "github.com/slack-go/slack" + "github.com/slack-go/slack/slackevents" +) + +func init_SocketmodeHandler() *SocketmodeHandler { + eventMap := make(map[EventType][]SocketmodeHandlerFunc) + interactioneventMap := make(map[slack.InteractionType][]SocketmodeHandlerFunc) + eventApiMap := make(map[slackevents.EventsAPIType][]SocketmodeHandlerFunc) + interactionBlockActionEventMap := make(map[string]SocketmodeHandlerFunc) + slashCommandMap := make(map[string]SocketmodeHandlerFunc) + + return &SocketmodeHandler{ + Client: &Client{ + log: log.New(os.Stderr, "slack-go/slack/socketmode", log.LstdFlags|log.Lshortfile), + }, + EventMap: eventMap, + EventApiMap: eventApiMap, + InteractionEventMap: interactioneventMap, + InteractionBlockActionEventMap: interactionBlockActionEventMap, + SlashCommandMap: slashCommandMap, + } +} + +// The goal of this function is to catch the name of the function that is behing called +// This let us validate that the dispatcher did its job correctly +func testing_wrapper(ch chan<- string, f SocketmodeHandlerFunc) SocketmodeHandlerFunc { + return SocketmodeHandlerFunc(func(e *Event, c *Client) { + f(e, c) + + var name_f string + + // test with the name of the function we called + v := reflect.ValueOf(f) + if v.Kind() == reflect.Func { + if rf := runtime.FuncForPC(v.Pointer()); rf != nil { + name_f = rf.Name() + } + } else { + name_f = v.String() + } + + ch <- name_f + }) +} + +func middleware_interaction(evt *Event, client *Client) { + //do nothing +} + +func middleware_interaction_block_action(evt *Event, client *Client) { + //do nothing +} + +func middleware_eventapi(evt *Event, client *Client) { + //do nothing +} + +func middleware(evt *Event, client *Client) { + //do nothing +} + +func defaultmiddleware(evt *Event, client *Client) { + //do nothing +} + +func middleware_slach_command(evt *Event, client *Client) { + //do nothing +} + +func TestSocketmodeHandler_Handle(t *testing.T) { + type args struct { + evt Event + evt_type EventType + } + tests := []struct { + name string + args args + want string //what is the name of the function we want to be called + }{ + { + name: "Event Match registered function", + args: args{ + evt: Event{ + Type: EventTypeConnecting, + }, + evt_type: EventTypeConnecting, + }, + want: "github.com/slack-go/slack/socketmode.middleware", + }, { + name: "Event do not registered function", + args: args{ + evt: Event{ + Type: EventTypeConnected, + }, + evt_type: EventTypeConnecting, + }, + want: "github.com/slack-go/slack/socketmode.defaultmiddleware", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := init_SocketmodeHandler() + + c := make(chan string) + + r.Handle(tt.args.evt_type, testing_wrapper(c, middleware)) + r.HandleDefault(testing_wrapper(c, defaultmiddleware)) + + r.dispatcher(tt.args.evt) + + got := <-c + + if got != tt.want { + t.Fatalf("middleware was not called for EventTy(\"%v\"), got %v", tt.args.evt_type, got) + } + }) + } +} + +func TestSocketmodeHandler_HandleInteraction(t *testing.T) { + type args struct { + evt Event + register func(*SocketmodeHandler, chan<- string) + } + tests := []struct { + name string + args args + want string //what is the name of the function we want to be called + }{ + { + name: "Event Match registered function", + args: args{ + evt: Event{ + Type: EventTypeInteractive, + Data: slack.InteractionCallback{ + Type: slack.InteractionTypeBlockActions, + }, + }, + register: func(r *SocketmodeHandler, c chan<- string) { + r.HandleInteraction(slack.InteractionTypeBlockActions, testing_wrapper(c, middleware_interaction)) + }, + }, + want: "github.com/slack-go/slack/socketmode.middleware_interaction", + }, { + name: "Event do not Match any registered function", + args: args{ + evt: Event{ + Type: EventTypeInteractive, + Data: slack.InteractionCallback{ + Type: slack.InteractionTypeBlockActions, + }, + }, + register: func(r *SocketmodeHandler, c chan<- string) { + r.HandleInteraction(slack.InteractionTypeBlockSuggestion, testing_wrapper(c, middleware_interaction)) + }, + }, + want: "github.com/slack-go/slack/socketmode.defaultmiddleware", + }, { + name: "Event with invalid data is handled by default middleware", + args: args{ + evt: Event{ + Type: EventTypeInteractive, + Data: map[string]string{ + "brokendata": "test", + }, + }, + register: func(r *SocketmodeHandler, c chan<- string) { + r.HandleInteraction(slack.InteractionTypeBlockActions, testing_wrapper(c, middleware_interaction)) + }, + }, + want: "github.com/slack-go/slack/socketmode.defaultmiddleware", + }, { + name: "Event is handled as EventTypeInteractive", + args: args{ + evt: Event{ + Type: EventTypeInteractive, + Data: slack.InteractionCallback{ + Type: slack.InteractionTypeBlockActions, + }, + }, + register: func(r *SocketmodeHandler, c chan<- string) { + r.Handle(EventTypeInteractive, testing_wrapper(c, middleware)) + }, + }, + want: "github.com/slack-go/slack/socketmode.middleware", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := init_SocketmodeHandler() + + c := make(chan string) + + tt.args.register(r, c) + r.HandleDefault(testing_wrapper(c, defaultmiddleware)) + + r.dispatcher(tt.args.evt) + + got := <-c + + if got != tt.want { + t.Fatalf("%s was not called for EventTy(\"%v\"), got %v", tt.want, tt.args.evt.Type, got) + } + }) + } +} + +func TestSocketmodeHandler_HandleEvents(t *testing.T) { + type args struct { + evt Event + register func(*SocketmodeHandler, chan<- string) + } + tests := []struct { + name string + args args + want string //what is the name of the function we want to be called + }{ + { + name: "Event Match registered function", + args: args{ + evt: Event{ + Type: EventTypeEventsAPI, + Data: slackevents.EventsAPIEvent{ + Type: "event_callback", + InnerEvent: slackevents.EventsAPIInnerEvent{ + Type: string(slackevents.AppMention), + }, + }, + }, + register: func(r *SocketmodeHandler, c chan<- string) { + r.HandleEvents(slackevents.AppMention, testing_wrapper(c, middleware_eventapi)) + }, + }, + want: "github.com/slack-go/slack/socketmode.middleware_eventapi", + }, { + name: "Event do not Match any registered function", + args: args{ + evt: Event{ + Type: EventTypeEventsAPI, + Data: slackevents.EventsAPIEvent{ + Type: "event_callback", + InnerEvent: slackevents.EventsAPIInnerEvent{ + Type: string(slackevents.MemberJoinedChannel), + }, + }, + }, + register: func(r *SocketmodeHandler, c chan<- string) { + r.HandleEvents(slackevents.AppMention, testing_wrapper(c, middleware_eventapi)) + }, + }, + want: "github.com/slack-go/slack/socketmode.defaultmiddleware", + }, { + name: "Event with invalid data is handled by default middleware", + args: args{ + evt: Event{ + Type: EventTypeEventsAPI, + Data: map[string]string{ + "brokendata": "test", + }, + }, + register: func(r *SocketmodeHandler, c chan<- string) { + r.HandleEvents(slackevents.AppMention, testing_wrapper(c, middleware_eventapi)) + }, + }, + want: "github.com/slack-go/slack/socketmode.defaultmiddleware", + }, { + name: "Event is handled as EventTypeInteractive", + args: args{ + evt: Event{ + Type: EventTypeEventsAPI, + Data: slackevents.EventsAPIEvent{ + Type: "event_callback", + InnerEvent: slackevents.EventsAPIInnerEvent{ + Type: string(slackevents.MemberJoinedChannel), + }, + }, + }, + register: func(r *SocketmodeHandler, c chan<- string) { + r.Handle(EventTypeEventsAPI, testing_wrapper(c, middleware)) + }, + }, + want: "github.com/slack-go/slack/socketmode.middleware", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := init_SocketmodeHandler() + + c := make(chan string) + + tt.args.register(r, c) + r.HandleDefault(testing_wrapper(c, defaultmiddleware)) + + r.dispatcher(tt.args.evt) + + got := <-c + + if got != tt.want { + t.Fatalf("%s was not called for EventTy(\"%v\"), got %v", tt.want, tt.args.evt.Type, got) + } + }) + } +} + +func TestSocketmodeHandler_HandleInteractionBlockAction(t *testing.T) { + type args struct { + evt Event + register func(*SocketmodeHandler, chan<- string) + } + tests := []struct { + name string + args args + want string //what is the name of the function we want to be called + }{ + { + name: "Event Match registered function", + args: args{ + evt: Event{ + Type: EventTypeInteractive, + Data: slack.InteractionCallback{ + Type: slack.InteractionTypeBlockActions, + ActionCallback: slack.ActionCallbacks{ + BlockActions: []*slack.BlockAction{ + { + ActionID: "add_note", + Text: slack.TextBlockObject{ + Type: "plain_text", + Text: "Add a Stickie", + }, + }, + }, + }, + }, + }, + register: func(r *SocketmodeHandler, c chan<- string) { + r.HandleInteractionBlockAction("add_note", testing_wrapper(c, middleware_interaction_block_action)) + }, + }, + want: "github.com/slack-go/slack/socketmode.middleware_interaction_block_action", + }, { + name: "Event do not Match any registered function", + args: args{ + evt: Event{ + Type: EventTypeInteractive, + Data: slack.InteractionCallback{ + Type: slack.InteractionTypeBlockActions, + }, + }, + register: func(r *SocketmodeHandler, c chan<- string) { + r.HandleInteractionBlockAction("add_note", testing_wrapper(c, middleware_interaction_block_action)) + }, + }, + want: "github.com/slack-go/slack/socketmode.defaultmiddleware", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := init_SocketmodeHandler() + + c := make(chan string) + + tt.args.register(r, c) + r.HandleDefault(testing_wrapper(c, defaultmiddleware)) + + r.dispatcher(tt.args.evt) + + got := <-c + + if got != tt.want { + t.Fatalf("%s was not called for EventTy(\"%v\"), got %v", tt.want, tt.args.evt.Type, got) + } + }) + } +} + +func TestSocketmodeHandler_HandleSlashCommand(t *testing.T) { + type args struct { + evt Event + register func(*SocketmodeHandler, chan<- string) + } + tests := []struct { + name string + args args + want string //what is the name of the function we want to be called + }{ + { + name: "Event Match registered function", + args: args{ + evt: Event{ + Type: EventTypeSlashCommand, + Data: slack.SlashCommand{ + Command: "/rocket", + Text: "key=value", + }, + }, + register: func(r *SocketmodeHandler, c chan<- string) { + r.HandleSlashCommand("/rocket", testing_wrapper(c, middleware_slach_command)) + }, + }, + want: "github.com/slack-go/slack/socketmode.middleware_slach_command", + }, { + name: "Event do not Match any registered function", + args: args{ + evt: Event{ + Type: EventTypeSlashCommand, + Data: slack.SlashCommand{ + Command: "/broken_rocket", + Text: "key=value", + }, + }, + register: func(r *SocketmodeHandler, c chan<- string) { + r.HandleSlashCommand("/rocket", testing_wrapper(c, middleware_slach_command)) + }, + }, + want: "github.com/slack-go/slack/socketmode.defaultmiddleware", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := init_SocketmodeHandler() + + c := make(chan string) + + tt.args.register(r, c) + r.HandleDefault(testing_wrapper(c, defaultmiddleware)) + + r.dispatcher(tt.args.evt) + + got := <-c + + if got != tt.want { + t.Fatalf("%s was not called for EventTy(\"%v\"), got %v", tt.want, tt.args.evt.Type, got) + } + }) + } +} + +func TestSocketmodeHandler_Handle_errors(t *testing.T) { + type args struct { + register func(*SocketmodeHandler, chan<- string) + } + tests := []struct { + name string + args args + }{ + { + name: "Attempt to register empty command", + args: args{ + register: func(r *SocketmodeHandler, c chan<- string) { + r.HandleSlashCommand("", testing_wrapper(c, middleware_slach_command)) + }, + }, + }, { + name: "Attempt to register nil handler", + args: args{ + register: func(r *SocketmodeHandler, c chan<- string) { + r.HandleSlashCommand("/command", nil) + }, + }, + }, { + name: "Attempt to register duplicate command", + args: args{ + register: func(r *SocketmodeHandler, c chan<- string) { + r.HandleSlashCommand("/command", testing_wrapper(c, middleware_slach_command)) + r.HandleSlashCommand("/command", testing_wrapper(c, middleware_slach_command)) + }, + }, + }, { + name: "Attempt to register empty Block ActionID", + args: args{ + register: func(r *SocketmodeHandler, c chan<- string) { + r.HandleInteractionBlockAction("", testing_wrapper(c, middleware_interaction_block_action)) + }, + }, + }, { + name: "Attempt to register nil handler", + args: args{ + register: func(r *SocketmodeHandler, c chan<- string) { + r.HandleInteractionBlockAction("action_id", nil) + }, + }, + }, { + name: "Attempt to register duplicate Block ActionID", + args: args{ + register: func(r *SocketmodeHandler, c chan<- string) { + r.HandleInteractionBlockAction("action_id", testing_wrapper(c, middleware_interaction_block_action)) + r.HandleInteractionBlockAction("action_id", testing_wrapper(c, middleware_interaction_block_action)) + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := init_SocketmodeHandler() + + c := make(chan string) + + defer func() { recover() }() + + tt.args.register(r, c) + + t.Errorf("should have panicked") + + }) + } +} diff --git a/socketmode/socketmode_test.go b/socketmode/socketmode_test.go index 015a34604..1f6c69857 100644 --- a/socketmode/socketmode_test.go +++ b/socketmode/socketmode_test.go @@ -228,9 +228,9 @@ func TestEventParsing(t *testing.T) { EventContext: "1-app_mention-redacted-redacted", }, InnerEvent: slackevents.EventsAPIInnerEvent{ - Type: slackevents.AppMention, + Type: string(slackevents.AppMention), Data: &slackevents.AppMentionEvent{ - Type: slackevents.AppMention, + Type: string(slackevents.AppMention), User: "redacted", Text: "<@U01JKSB8T7Y> test", TimeStamp: "1610927831.000200",