From 00bf779b5cda6ef0dd81b1dd4951353beb967481 Mon Sep 17 00:00:00 2001 From: Vlad Melnik Date: Mon, 16 Sep 2024 17:05:15 +0300 Subject: [PATCH 1/4] feat: exposing request from event in interaction handler --- executors.go | 6 ++++-- handler.go | 4 +++- slacker.go | 9 +++++---- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/executors.go b/executors.go index e9899ef..6c7d169 100644 --- a/executors.go +++ b/executors.go @@ -1,5 +1,7 @@ package slacker +import "github.com/slack-go/slack/socketmode" + func executeCommand(ctx *CommandContext, handler CommandHandler, middlewares ...CommandMiddlewareHandler) { if handler == nil { return @@ -12,7 +14,7 @@ func executeCommand(ctx *CommandContext, handler CommandHandler, middlewares ... handler(ctx) } -func executeInteraction(ctx *InteractionContext, handler InteractionHandler, middlewares ...InteractionMiddlewareHandler) { +func executeInteraction(ctx *InteractionContext, handler InteractionHandler, request *socketmode.Request, middlewares ...InteractionMiddlewareHandler) { if handler == nil { return } @@ -21,7 +23,7 @@ func executeInteraction(ctx *InteractionContext, handler InteractionHandler, mid handler = middlewares[i](handler) } - handler(ctx) + handler(ctx, request) } func executeJob(ctx *JobContext, handler JobHandler, middlewares ...JobMiddlewareHandler) func() { diff --git a/handler.go b/handler.go index 0b56486..b275ef4 100644 --- a/handler.go +++ b/handler.go @@ -1,5 +1,7 @@ package slacker +import "github.com/slack-go/slack/socketmode" + // CommandMiddlewareHandler represents the command middleware handler function type CommandMiddlewareHandler func(CommandHandler) CommandHandler @@ -10,7 +12,7 @@ type CommandHandler func(*CommandContext) type InteractionMiddlewareHandler func(InteractionHandler) InteractionHandler // InteractionHandler represents the interaction handler function -type InteractionHandler func(*InteractionContext) +type InteractionHandler func(*InteractionContext, *socketmode.Request) // JobMiddlewareHandler represents the job middleware handler function type JobMiddlewareHandler func(JobHandler) JobHandler diff --git a/slacker.go b/slacker.go index 5284e77..dd70935 100644 --- a/slacker.go +++ b/slacker.go @@ -316,7 +316,7 @@ func (s *Slacker) Listen(ctx context.Context) error { // Acknowledge receiving the request s.socketModeClient.Ack(*socketEvent.Request) - go s.handleInteractionEvent(ctx, &callback) + go s.handleInteractionEvent(ctx, &callback, *socketEvent.Request) default: if s.unsupportedEventHandler != nil { @@ -440,7 +440,7 @@ func (s *Slacker) startCronJobs(ctx context.Context) { s.cronClient.Start() } -func (s *Slacker) handleInteractionEvent(ctx context.Context, callback *slack.InteractionCallback) { +func (s *Slacker) handleInteractionEvent(ctx context.Context, callback *slack.InteractionCallback, request socketmode.Request) { middlewares := make([]InteractionMiddlewareHandler, 0) middlewares = append(middlewares, s.interactionMiddlewares...) @@ -482,14 +482,15 @@ func (s *Slacker) handleInteractionEvent(ctx context.Context, callback *slack.In if interaction != nil { interactionCtx := newInteractionContext(ctx, s.logger, s.slackClient, callback, definition) middlewares = append(middlewares, definition.Middlewares...) - executeInteraction(interactionCtx, definition.Handler, middlewares...) + executeInteraction(interactionCtx, definition.Handler, &request, middlewares...) return } s.logger.Debug("unsupported interaction type", "type", callback.Type) + if s.unsupportedInteractionHandler != nil { interactionCtx := newInteractionContext(ctx, s.logger, s.slackClient, callback, nil) - executeInteraction(interactionCtx, s.unsupportedInteractionHandler, middlewares...) + executeInteraction(interactionCtx, s.unsupportedInteractionHandler, &request, middlewares...) } } From e3fa0886dd5b81e39ec53c5e4bfd86a8fcc53e71 Mon Sep 17 00:00:00 2001 From: Vlad Melnik Date: Wed, 18 Sep 2024 18:07:20 +0300 Subject: [PATCH 2/4] feat: self ack option for interactive events in for more interactivity in view --- options.go | 8 ++++++++ slacker.go | 8 ++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/options.go b/options.go index b3cd724..82bb19b 100644 --- a/options.go +++ b/options.go @@ -44,9 +44,16 @@ func WithCronLocation(location *time.Location) ClientOption { } } +func WithSelfAck(selfAck bool) ClientOption { + return func(defaults *clientOptions) { + defaults.SelfAck = selfAck + } +} + type clientOptions struct { APIURL string Debug bool + SelfAck bool BotMode BotMode Logger Logger CronLocation *time.Location @@ -58,6 +65,7 @@ func newClientOptions(options ...ClientOption) *clientOptions { Debug: false, BotMode: BotModeIgnoreAll, CronLocation: time.Local, + SelfAck: false, } for _, option := range options { diff --git a/slacker.go b/slacker.go index dd70935..66b2519 100644 --- a/slacker.go +++ b/slacker.go @@ -45,6 +45,7 @@ func NewClient(botToken, appToken string, clientOptions ...ClientOption) *Slacke sanitizeEventTextHandler: defaultEventTextSanitizer, logger: options.Logger, interactions: make(map[slack.InteractionType][]*Interaction), + selfAck: options.SelfAck, } return slacker } @@ -73,6 +74,7 @@ type Slacker struct { botInteractionMode BotMode sanitizeEventTextHandler func(string) string logger Logger + selfAck bool } // GetCommandGroups returns Command Groups @@ -313,8 +315,10 @@ func (s *Slacker) Listen(ctx context.Context) error { continue } - // Acknowledge receiving the request - s.socketModeClient.Ack(*socketEvent.Request) + // Acknowledge receiving the request if self Acknowledge is disabled + if !s.selfAck { + s.socketModeClient.Ack(*socketEvent.Request) + } go s.handleInteractionEvent(ctx, &callback, *socketEvent.Request) From d5d79d1ba3b83c0a5ec5e831205a19fdb200eb8c Mon Sep 17 00:00:00 2001 From: vlad melnik Date: Sat, 17 May 2025 14:42:18 +0300 Subject: [PATCH 3/4] Interction example (#1) * fix: New example of adding validation to input and showing error interactivly In new example there validation of input added, if input is not valid error showing in the slack view, in order to view the error messege we need to Ack the messege manually this is the reson for adding client to the interaction handler, also fixed errors in all other examples, provided request which is now mandatory in all interaction handlers * adding new block suggestion event --------- Co-authored-by: Vlad Melnik --- examples/interaction-middleware/main.go | 9 +- examples/interaction-shortcut/main.go | 5 +- examples/interaction-sink/main.go | 5 +- examples/interaction-view-with-error/main.go | 137 +++++++++++++++++++ examples/interaction-view/main.go | 5 +- examples/interaction/main.go | 5 +- slacker.go | 2 +- 7 files changed, 155 insertions(+), 13 deletions(-) create mode 100644 examples/interaction-view-with-error/main.go diff --git a/examples/interaction-middleware/main.go b/examples/interaction-middleware/main.go index 36706a9..30d8e30 100644 --- a/examples/interaction-middleware/main.go +++ b/examples/interaction-middleware/main.go @@ -5,8 +5,9 @@ import ( "log" "os" - "github.com/slack-io/slacker" "github.com/slack-go/slack" + "github.com/slack-go/slack/socketmode" + "github.com/slack-io/slacker" ) // Show cases interaction middlewares @@ -48,7 +49,7 @@ func slackerCmd(blockID string) slacker.CommandHandler { } } -func slackerInteractive(ctx *slacker.InteractionContext) { +func slackerInteractive(ctx *slacker.InteractionContext, req *socketmode.Request) { text := "" action := ctx.Callback().ActionCallback.BlockActions[0] switch action.ActionID { @@ -65,14 +66,14 @@ func slackerInteractive(ctx *slacker.InteractionContext) { func LoggingInteractionMiddleware() slacker.InteractionMiddlewareHandler { return func(next slacker.InteractionHandler) slacker.InteractionHandler { - return func(ctx *slacker.InteractionContext) { + return func(ctx *slacker.InteractionContext, req *socketmode.Request) { ctx.Logger().Info("logging interaction middleware", "user_id", ctx.Callback().User.ID, "interaction_id", ctx.Definition().InteractionID, "action_id", ctx.Callback().ActionCallback.BlockActions[0].ActionID, "channel_id", ctx.Callback().Channel.ID, ) - next(ctx) + next(ctx, req) } } } diff --git a/examples/interaction-shortcut/main.go b/examples/interaction-shortcut/main.go index b9aa6f4..a1cf301 100644 --- a/examples/interaction-shortcut/main.go +++ b/examples/interaction-shortcut/main.go @@ -6,8 +6,9 @@ import ( "log" "os" - "github.com/slack-io/slacker" "github.com/slack-go/slack" + "github.com/slack-go/slack/socketmode" + "github.com/slack-io/slacker" ) // Implements a basic interactive command with modal view. @@ -40,7 +41,7 @@ func main() { } } -func moodShortcutHandler(ctx *slacker.InteractionContext) { +func moodShortcutHandler(ctx *slacker.InteractionContext, req *socketmode.Request) { switch ctx.Callback().Type { case slack.InteractionTypeMessageAction: { diff --git a/examples/interaction-sink/main.go b/examples/interaction-sink/main.go index 0dd998d..1d44b81 100644 --- a/examples/interaction-sink/main.go +++ b/examples/interaction-sink/main.go @@ -5,8 +5,9 @@ import ( "log" "os" - "github.com/slack-io/slacker" "github.com/slack-go/slack" + "github.com/slack-go/slack/socketmode" + "github.com/slack-io/slacker" ) // Show cases having one handler for all interactions @@ -14,7 +15,7 @@ import ( func main() { bot := slacker.NewClient(os.Getenv("SLACK_BOT_TOKEN"), os.Getenv("SLACK_APP_TOKEN")) - bot.UnsupportedInteractionHandler(func(ctx *slacker.InteractionContext) { + bot.UnsupportedInteractionHandler(func(ctx *slacker.InteractionContext, req *socketmode.Request) { callback := ctx.Callback() if callback.Type != slack.InteractionTypeBlockActions { return diff --git a/examples/interaction-view-with-error/main.go b/examples/interaction-view-with-error/main.go new file mode 100644 index 0000000..2916be9 --- /dev/null +++ b/examples/interaction-view-with-error/main.go @@ -0,0 +1,137 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + "regexp" + + "github.com/slack-go/slack" + "github.com/slack-go/slack/socketmode" + "github.com/slack-io/slacker" +) + +var moodSurveyView = slack.ModalViewRequest{ + Type: "modal", + CallbackID: "mood-survey-callback-id", + Title: &slack.TextBlockObject{ + Type: "plain_text", + Text: "Which mood are you in?", + }, + Submit: &slack.TextBlockObject{ + Type: "plain_text", + Text: "Submit", + }, + NotifyOnClose: true, + Blocks: slack.Blocks{ + BlockSet: []slack.Block{ + &slack.InputBlock{ + Type: "input", + DispatchAction: true, + BlockID: "mood", + Label: &slack.TextBlockObject{ + Type: slack.PlainTextType, + Text: "Input mood", + }, + Element: &slack.PlainTextInputBlockElement{ + MaxLength: 23, + Type: slack.METPlainTextInput, + ActionID: "mood", + Placeholder: &slack.TextBlockObject{ + Type: slack.PlainTextType, + Text: "Enter your mood", + }, + }, + }, + }, + }, +} + +// Implements a basic interactive command with modal view. +func main() { + bot := slacker.NewClient( + os.Getenv("SLACK_BOT_TOKEN"), + os.Getenv("SLACK_APP_TOKEN"), + slacker.WithDebug(false), + slacker.WithSelfAck(true), + ) + + bot.AddCommand(&slacker.CommandDefinition{ + Command: "mood", + Handler: moodCmdHandler, + }) + + bot.AddInteraction(&slacker.InteractionDefinition{ + InteractionID: "mood-survey-callback-id", + Handler: moodViewHanlerWithBotClient(bot), + Type: slack.InteractionTypeViewSubmission, + }) + + bot.AddInteraction(&slacker.InteractionDefinition{ + InteractionID: "mood-survey-callback-id", + Handler: moodViewHanlerWithBotClient(bot), + Type: slack.InteractionTypeViewClosed, + }) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + err := bot.Listen(ctx) + if err != nil { + log.Fatal(err) + } +} + +func moodCmdHandler(ctx *slacker.CommandContext) { + _, err := ctx.SlackClient().OpenView( + ctx.Event().Data.(*slack.SlashCommand).TriggerID, + moodSurveyView, + ) + if err != nil { + log.Printf("ERROR openEscalationModal: %v", err) + } +} + +func moodViewHanlerWithBotClient(bot *slacker.Slacker) slacker.InteractionHandler { + return func(ctx *slacker.InteractionContext, req *socketmode.Request) { + switch ctx.Callback().Type { + case slack.InteractionTypeViewSubmission: + { + + valid, errorMessage := validateInput(ctx.Callback().View.State.Values["mood"]["mood"].Value) + if !valid { + errorResponse := map[string]interface{}{ + "response_action": "errors", + "errors": map[string]string{ + "mood": errorMessage, // Block ID for the input field + }, + } + + bot.SocketModeClient().Ack(*req, errorResponse) + + } else { + bot.SocketModeClient().Ack(*req) + + } + viewState := ctx.Callback().View.State.Values + fmt.Printf( + "Mood view submitted.\nMood: %s\n", + viewState["mood"]["mood"].SelectedOption.Value, + ) + } + case slack.InteractionTypeViewClosed: + { + fmt.Print("Mood view closed.\n") + } + } + } +} + +func validateInput(input string) (bool, string) { + re := regexp.MustCompile(`^[A-Za-z0-9-]{1,23}$`) + if !re.MatchString(input) { + return false, "Mood can not contain special characters" + } + return true, "" +} diff --git a/examples/interaction-view/main.go b/examples/interaction-view/main.go index dba2355..907b811 100644 --- a/examples/interaction-view/main.go +++ b/examples/interaction-view/main.go @@ -6,8 +6,9 @@ import ( "log" "os" - "github.com/slack-io/slacker" "github.com/slack-go/slack" + "github.com/slack-go/slack/socketmode" + "github.com/slack-io/slacker" ) var moodSurveyView = slack.ModalViewRequest{ @@ -100,7 +101,7 @@ func moodCmdHandler(ctx *slacker.CommandContext) { } } -func moodViewHandler(ctx *slacker.InteractionContext) { +func moodViewHandler(ctx *slacker.InteractionContext, req *socketmode.Request) { switch ctx.Callback().Type { case slack.InteractionTypeViewSubmission: { diff --git a/examples/interaction/main.go b/examples/interaction/main.go index bc892bb..437f08d 100644 --- a/examples/interaction/main.go +++ b/examples/interaction/main.go @@ -5,8 +5,9 @@ import ( "log" "os" - "github.com/slack-io/slacker" "github.com/slack-go/slack" + "github.com/slack-go/slack/socketmode" + "github.com/slack-io/slacker" ) // Implements a basic interactive command. @@ -47,7 +48,7 @@ func slackerCmd(blockID string) slacker.CommandHandler { } } -func slackerInteractive(ctx *slacker.InteractionContext) { +func slackerInteractive(ctx *slacker.InteractionContext, req *socketmode.Request) { text := "" action := ctx.Callback().ActionCallback.BlockActions[0] switch action.ActionID { diff --git a/slacker.go b/slacker.go index 66b2519..5b7b0c9 100644 --- a/slacker.go +++ b/slacker.go @@ -452,7 +452,7 @@ func (s *Slacker) handleInteractionEvent(ctx context.Context, callback *slack.In var definition *InteractionDefinition switch callback.Type { - case slack.InteractionTypeBlockActions: + case slack.InteractionTypeBlockActions, slack.InteractionTypeBlockSuggestion: for _, i := range s.interactions[callback.Type] { for _, a := range callback.ActionCallback.BlockActions { definition = i.Definition() From 586b8672621dba26390c14176d35cd1d336e7c8c Mon Sep 17 00:00:00 2001 From: Vlad Melnik Date: Sun, 18 May 2025 17:56:46 +0300 Subject: [PATCH 4/4] fix: adding suggestion interaction type to the interaction event handler --- slacker.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/slacker.go b/slacker.go index 5b7b0c9..2cabbc9 100644 --- a/slacker.go +++ b/slacker.go @@ -452,7 +452,7 @@ func (s *Slacker) handleInteractionEvent(ctx context.Context, callback *slack.In var definition *InteractionDefinition switch callback.Type { - case slack.InteractionTypeBlockActions, slack.InteractionTypeBlockSuggestion: + case slack.InteractionTypeBlockActions: for _, i := range s.interactions[callback.Type] { for _, a := range callback.ActionCallback.BlockActions { definition = i.Definition() @@ -465,6 +465,14 @@ func (s *Slacker) handleInteractionEvent(ctx context.Context, callback *slack.In break } } + case slack.InteractionTypeBlockSuggestion: + for _, i := range s.interactions[callback.Type] { + definition = i.Definition() + if definition.InteractionID == callback.ActionID { + interaction = i + break + } + } case slack.InteractionTypeViewClosed, slack.InteractionTypeViewSubmission: for _, i := range s.interactions[callback.Type] { definition = i.Definition()