diff --git a/examples/socketmode_handler/socketmode_handler.go b/examples/socketmode_handler/socketmode_handler.go index 221c3ea23..6ab82582b 100644 --- a/examples/socketmode_handler/socketmode_handler.go +++ b/examples/socketmode_handler/socketmode_handler.go @@ -199,7 +199,3 @@ func middlewareSlashCommand(evt *socketmode.Event, client *socketmode.Client) { }} 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/go.mod b/go.mod index 5cc8e1a7e..e2f54d40f 100644 --- a/go.mod +++ b/go.mod @@ -9,4 +9,5 @@ require ( github.com/gorilla/websocket v1.4.2 github.com/pmezard/go-difflib v1.0.0 // indirect github.com/stretchr/testify v1.2.2 + go.mills.io/logger v0.0.0-20230806012737-485dbd691907 ) diff --git a/go.sum b/go.sum index 194956433..98a5c0c66 100644 --- a/go.sum +++ b/go.sum @@ -10,5 +10,7 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +go.mills.io/logger v0.0.0-20230806012737-485dbd691907 h1:KXwGupN4n3h/t9HyTLykODg1ope7KtXaknALkBMaaz4= +go.mills.io/logger v0.0.0-20230806012737-485dbd691907/go.mod h1:A+23JY9iOHzujHnRYbFKVzLLAQVObxHnsap8kjAjuQ8= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/slacktest/funcs.go b/slacktest/funcs.go index d083af2b5..4b480c3fb 100644 --- a/slacktest/funcs.go +++ b/slacktest/funcs.go @@ -89,6 +89,15 @@ func BotNameFromContext(ctx context.Context) string { return botname } +// ServerWSFromContext returns the server websocket endpoint from a provided context +func ServerWSFromContext(ctx context.Context) string { + url, ok := ctx.Value(ServerWSContextKey).(string) + if !ok { + return "ws://wtf?!" + } + return url +} + // BotIDFromContext returns the bot userid from a provided context func BotIDFromContext(ctx context.Context) string { botname, ok := ctx.Value(ServerBotIDContextKey).(string) @@ -117,6 +126,16 @@ func nowAsJSONTime() slack.JSONTime { return slack.JSONTime(time.Now().Unix()) } +func defaultAppsConnectionsJSON(ctx context.Context) string { + url := ServerWSFromContext(ctx) + return fmt.Sprintf(` + { + "ok":true, + "url": "%s" + } + `, url) +} + func defaultBotInfoJSON(ctx context.Context) string { botid := BotIDFromContext(ctx) botname := BotNameFromContext(ctx) diff --git a/slacktest/handlers.go b/slacktest/handlers.go index b4a0227f3..4f2d2e216 100644 --- a/slacktest/handlers.go +++ b/slacktest/handlers.go @@ -15,6 +15,11 @@ import ( slack "github.com/slack-go/slack" ) +var ( + defaultPingPeriod = time.Second * 15 + defaultWriteDeadline = time.Second * 3 +) + func contextHandler(server *Server, next http.HandlerFunc) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := context.WithValue(r.Context(), ServerURLContextKey, server.GetAPIURL()) @@ -277,10 +282,20 @@ func rtmStartHandler(w http.ResponseWriter, r *http.Request) { } } +// handle apps.connections.open +func appsConnectionsOpenHandler(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte(defaultAppsConnectionsJSON(r.Context()))) +} + func (sts *Server) wsHandler(w http.ResponseWriter, r *http.Request) { Websocket(func(c *websocket.Conn) { serverAddr := r.Context().Value(ServerBotHubNameContextKey).(string) + doneCh := make(chan struct{}, 1) + defer func() { + doneCh <- struct{}{} + }() go handlePendingMessages(c, serverAddr) + go handlePeriodicPings(c, doneCh) for { var ( err error @@ -309,6 +324,21 @@ func (sts *Server) wsHandler(w http.ResponseWriter, r *http.Request) { })(w, r) } +func handlePeriodicPings(c *websocket.Conn, done chan struct{}) { + ticker := time.NewTicker(defaultPingPeriod) + defer ticker.Stop() + for { + select { + case <-ticker.C: + if err := c.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(defaultWriteDeadline)); err != nil { + log.Println("error sending ping:", err) + } + case <-done: + return + } + } +} + // Websocket handler func Websocket(delegate func(c *websocket.Conn)) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { diff --git a/slacktest/server.go b/slacktest/server.go index 6d9849451..c9c152003 100644 --- a/slacktest/server.go +++ b/slacktest/server.go @@ -6,9 +6,11 @@ import ( "log" "net/http" "net/http/httptest" + "regexp" "time" "github.com/slack-go/slack" + "go.mills.io/logger" ) func newMessageChannels() *messageChannels { @@ -61,8 +63,10 @@ func NewTestServer(custom ...Binder) *Server { s.Handle("/bots.info", botsInfoHandler) s.Handle("/auth.test", authTestHandler) s.Handle("/reactions.add", reactionAddHandler) + s.Handle("/apps.connections.open", appsConnectionsOpenHandler) httpserver := httptest.NewUnstartedServer(s.mux) + httpserver.Config.Handler = logger.New().Handler(httpserver.Config.Handler) addr := httpserver.Listener.Addr().String() s.ServerAddr = addr @@ -138,8 +142,26 @@ func (sts *Server) SawOutgoingMessage(msg string) bool { return false } -// SawMessage checks if an incoming message was seen -func (sts *Server) SawMessage(msg string) bool { +// SawOutgoingMessageMatching checks if a message was sent to connected websocket clients that matches the given pattern +func (sts *Server) SawOutgoingMessageMatching(pattern string) bool { + sts.seenOutboundMessages.RLock() + defer sts.seenOutboundMessages.RUnlock() + for _, m := range sts.seenOutboundMessages.messages { + evt := &slack.MessageEvent{} + jErr := json.Unmarshal([]byte(m), evt) + if jErr != nil { + continue + } + + if ok, err := regexp.MatchString(pattern, evt.Text); err == nil && ok { + return true + } + } + return false +} + +// SawIncomingMessage checks if an incoming message was seen +func (sts *Server) SawIncomingMessage(msg string) bool { sts.seenInboundMessages.RLock() defer sts.seenInboundMessages.RUnlock() for _, m := range sts.seenInboundMessages.messages { @@ -156,6 +178,24 @@ func (sts *Server) SawMessage(msg string) bool { return false } +// SawIncomingMessageMatching checks if an incoming message was seen that matches a given pattern +func (sts *Server) SawIncomingMessageMatching(pattern string) bool { + sts.seenInboundMessages.RLock() + defer sts.seenInboundMessages.RUnlock() + for _, m := range sts.seenInboundMessages.messages { + evt := &slack.MessageEvent{} + jErr := json.Unmarshal([]byte(m), evt) + if jErr != nil { + // This event isn't a message event so we'll skip it + continue + } + if ok, err := regexp.MatchString(pattern, evt.Text); err == nil && ok { + return true + } + } + return false +} + // GetAPIURL returns the api url you can pass to slack.SLACK_API func (sts *Server) GetAPIURL() string { return "http://" + sts.ServerAddr + "/" @@ -302,7 +342,7 @@ func (sts *Server) SendBotGroupInvite() { // GetTestRTMInstance will give you an RTM instance in the context of the current fake server func (sts *Server) GetTestRTMInstance() *slack.RTM { - api := slack.New("ABCEFG", slack.OptionAPIURL(sts.GetAPIURL())) - rtm := api.NewRTM() + api := slack.New("ABCEFG", slack.OptionDebug(true), slack.OptionAPIURL(sts.GetAPIURL())) + rtm := api.NewRTM(slack.RTMOptionPingInterval(5 * time.Second)) return rtm } diff --git a/slacktest/server_test.go b/slacktest/server_test.go index 45f620858..560caa1e8 100644 --- a/slacktest/server_test.go +++ b/slacktest/server_test.go @@ -102,7 +102,7 @@ func TestGetSeenInboundMessages(t *testing.T) { } } assert.True(t, hadMessage, "did not see my sent message") - assert.True(t, s.SawMessage("should see this inbound message")) + assert.True(t, s.SawIncomingMessage("should see this inbound message")) } func TestSendChannelInvite(t *testing.T) { @@ -166,7 +166,7 @@ func TestSendGroupInvite(t *testing.T) { func TestServerSawMessage(t *testing.T) { s := NewTestServer() go s.Start() - assert.False(t, s.SawMessage("foo"), "should not have seen any message") + assert.False(t, s.SawIncomingMessage("foo"), "should not have seen any message") } func TestServerSawOutgoingMessage(t *testing.T) { diff --git a/slacktest/types.go b/slacktest/types.go index 4c90ed9ba..6acd45cc2 100644 --- a/slacktest/types.go +++ b/slacktest/types.go @@ -1,7 +1,6 @@ package slacktest import ( - "log" "net/http" "net/http/httptest" "sync" @@ -64,7 +63,6 @@ type Server struct { registered map[string]struct{} server *httptest.Server mux *http.ServeMux - Logger *log.Logger BotName string BotID string ServerAddr string diff --git a/socketmode/socket_mode_managed_conn.go b/socketmode/socket_mode_managed_conn.go index b94456f49..3974333f6 100644 --- a/socketmode/socket_mode_managed_conn.go +++ b/socketmode/socket_mode_managed_conn.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "io" + "log" "net/http" "sync" "time" @@ -312,6 +313,7 @@ func (smc *Client) openAndDial(ctx context.Context, additionalPingHandler func(s return nil, nil, err } if additionalPingHandler == nil { + log.Print("no ping handler, default to null handler") additionalPingHandler = func(_ string) error { return nil } } @@ -380,6 +382,7 @@ func (smc *Client) runRequestHandler(ctx context.Context, websocket chan json.Ra // listen for incoming messages that need to be parsed evt, err := smc.parseEvent(message) if err != nil { + log.Printf("error parsing event %q: %s", message, err) smc.sendEvent(ctx, newEvent(EventTypeErrorBadMessage, &ErrorBadMessage{ Cause: err, Message: message, diff --git a/socketmode/socketmode.go b/socketmode/socketmode.go index 6ca8f487c..d6a8ee2af 100644 --- a/socketmode/socketmode.go +++ b/socketmode/socketmode.go @@ -91,6 +91,7 @@ func OptionPingInterval(d time.Duration) Option { // OptionDebug enable debugging for the client func OptionDebug(b bool) func(*Client) { return func(c *Client) { + c.log.Printf("Using debug mode: %t", b) c.debug = b } } diff --git a/socketmode/socketmode_handler.go b/socketmode/socketmode_handler.go index 0cfe7555d..7973ec9b2 100644 --- a/socketmode/socketmode_handler.go +++ b/socketmode/socketmode_handler.go @@ -2,6 +2,7 @@ package socketmode import ( "context" + "github.com/slack-go/slack" "github.com/slack-go/slack/slackevents" ) @@ -14,7 +15,7 @@ type SocketmodeHandler struct { //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 + //lvl 3 - the most user friendly way of managing event InteractionBlockActionEventMap map[string]SocketmodeHandlerFunc SlashCommandMap map[string]SocketmodeHandlerFunc @@ -44,18 +45,18 @@ func NewSocketmodeHandler(client *Client) *SocketmodeHandler { InteractionBlockActionEventMap: interactionBlockActionEventMap, SlashCommandMap: slackCommandMap, Default: func(e *Event, c *Client) { - c.log.Printf("Unexpected event type received: %v\n", e.Type) + c.log.Printf("Unexpected event type received: %v", e.Type) }, } } -// Register a middleware or handler for an Event from socketmode +// Handle adds 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 +// HandleInteraction adds a middleware or handler for an Interaction // There is several types of interactions, decated functions lets you better handle them // See // * HandleInteractionBlockAction @@ -65,7 +66,7 @@ func (r *SocketmodeHandler) HandleInteraction(et slack.InteractionType, f Socket r.InteractionEventMap[et] = append(r.InteractionEventMap[et], f) } -// Register a middleware or handler for a Block Action referenced by its ActionID +// HandleInteractionBlockAction adds 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") @@ -79,12 +80,12 @@ func (r *SocketmodeHandler) HandleInteractionBlockAction(actionID string, f Sock r.InteractionBlockActionEventMap[actionID] = f } -// Register a middleware or handler for an Event (from slackevents) +// HandleEvents adds 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 +// HandleSlashCommand adds a middleware or handler for a Slash Command func (r *SocketmodeHandler) HandleSlashCommand(command string, f SocketmodeHandlerFunc) { if command == "" { panic("invalid command cannot be empty") @@ -98,12 +99,12 @@ func (r *SocketmodeHandler) HandleSlashCommand(command string, f SocketmodeHandl r.SlashCommandMap[command] = f } -// Register a middleware or handler to use as a last resort +// HandleDefault adds 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 +// RunEventLoop receives the event via the socket func (r *SocketmodeHandler) RunEventLoop() error { go r.runEventLoop(context.Background()) @@ -111,13 +112,14 @@ func (r *SocketmodeHandler) RunEventLoop() error { return r.Client.Run() } +// RunEventLoopContext is the context-aware version of RunEventLoop() func (r *SocketmodeHandler) RunEventLoopContext(ctx context.Context) error { go r.runEventLoop(ctx) return r.Client.RunContext(ctx) } -// Call the dispatcher for each incomming event +// Call the dispatcher for each incoming event func (r *SocketmodeHandler) runEventLoop(ctx context.Context) { for { select { @@ -136,21 +138,21 @@ func (r *SocketmodeHandler) runEventLoop(ctx context.Context) { // Dispatch events to the specialized dispatcher func (r *SocketmodeHandler) dispatcher(evt Event) { - var ishandled bool + var isHandled bool // Some eventType can be further decomposed switch evt.Type { case EventTypeInteractive: - ishandled = r.interactionDispatcher(&evt) + isHandled = r.interactionDispatcher(&evt) case EventTypeEventsAPI: - ishandled = r.eventAPIDispatcher(&evt) + isHandled = r.eventAPIDispatcher(&evt) case EventTypeSlashCommand: - ishandled = r.slashCommandDispatcher(&evt) + isHandled = r.slashCommandDispatcher(&evt) default: - ishandled = r.socketmodeDispatcher(&evt) + isHandled = r.socketmodeDispatcher(&evt) } - if !ishandled { + if !isHandled { go r.Default(&evt, r.Client) } } @@ -171,7 +173,7 @@ func (r *SocketmodeHandler) socketmodeDispatcher(evt *Event) bool { // Dispatch interactions to the registered middleware func (r *SocketmodeHandler) interactionDispatcher(evt *Event) bool { - var ishandled bool = false + var isHandled bool interaction, ok := evt.Data.(slack.InteractionCallback) if !ok { @@ -180,7 +182,7 @@ func (r *SocketmodeHandler) interactionDispatcher(evt *Event) bool { } // Level 1 - socketmode EventType - ishandled = r.socketmodeDispatcher(evt) + isHandled = r.socketmodeDispatcher(evt) // Level 2 - interaction EventType if handlers, ok := r.InteractionEventMap[interaction.Type]; ok { @@ -189,7 +191,7 @@ func (r *SocketmodeHandler) interactionDispatcher(evt *Event) bool { go f(evt, r.Client) } - ishandled = true + isHandled = true } // Level 3 - interaction with actionID @@ -202,15 +204,15 @@ func (r *SocketmodeHandler) interactionDispatcher(evt *Event) bool { go handler(evt, r.Client) - ishandled = true + isHandled = true } } - return ishandled + return isHandled } // Dispatch eventAPI events to the registered middleware func (r *SocketmodeHandler) eventAPIDispatcher(evt *Event) bool { - var ishandled bool = false + var isHandled bool eventsAPIEvent, ok := evt.Data.(slackevents.EventsAPIEvent) if !ok { r.Client.log.Printf("Ignored %+v\n", evt) @@ -220,7 +222,7 @@ func (r *SocketmodeHandler) eventAPIDispatcher(evt *Event) bool { innerEventType := slackevents.EventsAPIType(eventsAPIEvent.InnerEvent.Type) // Level 1 - socketmode EventType - ishandled = r.socketmodeDispatcher(evt) + isHandled = r.socketmodeDispatcher(evt) // Level 2 - EventAPI EventType if handlers, ok := r.EventApiMap[innerEventType]; ok { @@ -229,15 +231,15 @@ func (r *SocketmodeHandler) eventAPIDispatcher(evt *Event) bool { go f(evt, r.Client) } - ishandled = true + isHandled = true } - return ishandled + return isHandled } // Dispatch SlashCommands events to the registered middleware func (r *SocketmodeHandler) slashCommandDispatcher(evt *Event) bool { - var ishandled bool = false + var isHandled bool slashCommandEvent, ok := evt.Data.(slack.SlashCommand) if !ok { r.Client.log.Printf("Ignored %+v\n", evt) @@ -245,16 +247,16 @@ func (r *SocketmodeHandler) slashCommandDispatcher(evt *Event) bool { } // Level 1 - socketmode EventType - ishandled = r.socketmodeDispatcher(evt) + isHandled = r.socketmodeDispatcher(evt) // Level 2 - SlackCommand by name if handler, ok := r.SlashCommandMap[slashCommandEvent.Command]; ok { go handler(evt, r.Client) - ishandled = true + isHandled = true } - return ishandled + return isHandled }