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/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/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 5284e77..2cabbc9 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,10 +315,12 @@ 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) + go s.handleInteractionEvent(ctx, &callback, *socketEvent.Request) default: if s.unsupportedEventHandler != nil { @@ -440,7 +444,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...) @@ -461,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() @@ -482,14 +494,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...) } }