interact: fix slack response and slash command handling

This commit is contained in:
c9s 2022-01-23 01:35:27 +08:00
parent 22404da392
commit 28cb881ead
3 changed files with 136 additions and 52 deletions

View File

@ -52,11 +52,7 @@ func (it *AuthInteract) Commands(interact *Interact) {
it.OneTimePasswordKey = key it.OneTimePasswordKey = key
} }
interact.Command("/auth", "authorize", func(reply Reply, session Session) error { interact.Command("/auth", "authorize", func(reply Reply, session Session) error {
reply.InputText("Authentication Token", TextField{ reply.Message("Please enter your authentication token")
Label: "Authentication Token",
Name: "token",
PlaceHolder: "Enter Your Authentication Token",
})
session.SetAuthorizing(true) session.SetAuthorizing(true)
return nil return nil
}).Next(func(token string, reply Reply) error { }).Next(func(token string, reply Reply) error {

View File

@ -38,10 +38,7 @@ type Reply interface {
// AddButton adds the button to the reply // AddButton adds the button to the reply
AddButton(text string, name, value string) AddButton(text string, name, value string)
InputText(prompt string, textFields ...TextField) // Choose(prompt string, options ...Option)
Choose(prompt string, options ...Option)
// Confirm shows the confirm dialog or confirm button in the user interface // Confirm shows the confirm dialog or confirm button in the user interface
// Confirm(prompt string) // Confirm(prompt string)

View File

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
stdlog "log" stdlog "log"
"os" "os"
"time"
"github.com/google/uuid" "github.com/google/uuid"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@ -42,6 +43,11 @@ func (reply *SlackReply) Send(message string) {
} }
} }
func (reply *SlackReply) InputText(prompt string, textFields ...TextField) {
reply.message = prompt
reply.textInputModalViewRequest = generateTextInputModalRequest(prompt, prompt, textFields...)
}
func (reply *SlackReply) Choose(prompt string, options ...Option) { func (reply *SlackReply) Choose(prompt string, options ...Option) {
} }
@ -49,11 +55,6 @@ func (reply *SlackReply) Message(message string) {
reply.message = message reply.message = message
} }
func (reply *SlackReply) InputText(prompt string, textFields ...TextField) {
reply.message = prompt
reply.textInputModalViewRequest = generateTextInputModalRequest(prompt, prompt, textFields...)
}
// RemoveKeyboard is not supported by Slack // RemoveKeyboard is not supported by Slack
func (reply *SlackReply) RemoveKeyboard() {} func (reply *SlackReply) RemoveKeyboard() {}
@ -66,26 +67,18 @@ func (reply *SlackReply) AddButton(text string, name string, value string) {
} }
func (reply *SlackReply) build() interface{} { func (reply *SlackReply) build() interface{} {
// you should avoid using this modal view request, because it interrupts the interaction flow
// once we send the modal view request, we can't go back to the channel.
// (we don't know which channel the user started the interaction)
if reply.textInputModalViewRequest != nil { if reply.textInputModalViewRequest != nil {
return reply.textInputModalViewRequest return reply.textInputModalViewRequest
} }
var blocks slack.Blocks
if len(reply.message) > 0 { if len(reply.message) > 0 {
blocks.BlockSet = append(blocks.BlockSet, slack.NewSectionBlock( return reply.message
&slack.TextBlockObject{
Type: slack.MarkdownType,
Text: reply.message,
},
nil, // fields
nil, // accessory
// slack.SectionBlockOptionBlockID(reply.uuid),
))
return blocks
} }
var blocks slack.Blocks
blocks.BlockSet = append(blocks.BlockSet, slack.NewSectionBlock( blocks.BlockSet = append(blocks.BlockSet, slack.NewSectionBlock(
&slack.TextBlockObject{ &slack.TextBlockObject{
Type: slack.MarkdownType, Type: slack.MarkdownType,
@ -121,25 +114,34 @@ func (reply *SlackReply) build() interface{} {
type SlackSession struct { type SlackSession struct {
BaseSession BaseSession
slack *Slack
ChannelID string ChannelID string
UserID string UserID string
// questions is used to store the questions that we added in the reply
// the key is the client generated callback id
questions map[string]interface{}
} }
func NewSlackSession(userID, channelID string) *SlackSession { func NewSlackSession(slack *Slack, userID, channelID string) *SlackSession {
return &SlackSession{ return &SlackSession{
BaseSession: BaseSession{
OriginState: StatePublic,
CurrentState: StatePublic,
Authorized: false,
authorizing: false,
StartedTime: time.Now(),
},
slack: slack,
UserID: userID, UserID: userID,
ChannelID: channelID, ChannelID: channelID,
questions: make(map[string]interface{}),
} }
} }
func (s *SlackSession) ID() string { func (s *SlackSession) ID() string {
return s.UserID return fmt.Sprintf("%s-%s", s.UserID, s.ChannelID)
// return fmt.Sprintf("%s-%s", s.UserID, s.ChannelID) }
func (s *SlackSession) SetAuthorized() {
s.BaseSession.SetAuthorized()
s.slack.EmitAuthorized(s)
} }
type SlackSessionMap map[string]*SlackSession type SlackSessionMap map[string]*SlackSession
@ -193,19 +195,22 @@ func (s *Slack) AddCommand(command *Command, responder Responder) {
s.commandResponders[command.Name] = responder s.commandResponders[command.Name] = responder
} }
func (s *Slack) listen() { func (s *Slack) listen(ctx context.Context) {
for evt := range s.socket.Events { for evt := range s.socket.Events {
log.Debugf("event: %+v", evt) log.Debugf("event: %+v", evt)
switch evt.Type { switch evt.Type {
case socketmode.EventTypeConnecting: case socketmode.EventTypeConnecting:
fmt.Println("Connecting to Slack with Socket Mode...") log.Infof("connecting to slack with socket mode...")
case socketmode.EventTypeConnectionError: case socketmode.EventTypeConnectionError:
fmt.Println("Connection failed. Retrying later...") log.Infof("connection failed. retrying later...")
case socketmode.EventTypeConnected: case socketmode.EventTypeConnected:
fmt.Println("Connected to Slack with Socket Mode.") log.Infof("connected to slack with socket mode.")
case socketmode.EventTypeDisconnect:
log.Infof("slack socket mode disconnected")
case socketmode.EventTypeEventsAPI: case socketmode.EventTypeEventsAPI:
eventsAPIEvent, ok := evt.Data.(slackevents.EventsAPIEvent) eventsAPIEvent, ok := evt.Data.(slackevents.EventsAPIEvent)
@ -215,6 +220,8 @@ func (s *Slack) listen() {
} }
log.Debugf("event received: %+v", eventsAPIEvent) log.Debugf("event received: %+v", eventsAPIEvent)
// events api don't have response trigger, we can't set the response
s.socket.Ack(*evt.Request) s.socket.Ack(*evt.Request)
s.EmitEventsApi(eventsAPIEvent) s.EmitEventsApi(eventsAPIEvent)
@ -223,17 +230,63 @@ func (s *Slack) listen() {
case slackevents.CallbackEvent: case slackevents.CallbackEvent:
innerEvent := eventsAPIEvent.InnerEvent innerEvent := eventsAPIEvent.InnerEvent
switch ev := innerEvent.Data.(type) { switch ev := innerEvent.Data.(type) {
case *slackevents.AppMentionEvent: case *slackevents.MessageEvent:
_, _, err := s.client.PostMessage(ev.Channel, slack.MsgOptionText("Yes, hello.", false)) log.Infof("message event: text=%+v", ev.Text)
if err != nil {
fmt.Printf("failed posting message: %v", err) if len(ev.BotID) > 0 {
log.Debug("skip bot message")
continue
} }
session := s.loadSession(evt, ev.User, ev.Channel)
if !session.authorizing && !session.Authorized {
log.Warn("[slack] session is not authorizing nor authorized, skipping message handler")
continue
}
if s.textMessageResponder != nil {
reply := s.newReply(session)
if err := s.textMessageResponder(session, ev.Text, reply); err != nil {
log.WithError(err).Errorf("[slack] response handling error")
continue
}
// build the response
response := reply.build()
log.Debugln("response payload", toJson(response))
switch response := response.(type) {
case string:
_, _, err := s.client.PostMessage(ev.Channel, slack.MsgOptionText(response, false))
if err != nil {
log.WithError(err).Error("failed posting plain text message")
}
case slack.Blocks:
_, _, err := s.client.PostMessage(ev.Channel, slack.MsgOptionBlocks(response.BlockSet...))
if err != nil {
log.WithError(err).Error("failed posting blocks message")
}
default:
log.Errorf("[slack] unexpected message type %T: %+v", response, response)
}
}
case *slackevents.AppMentionEvent:
log.Infof("app mention event: %+v", ev)
s.socket.Ack(*evt.Request)
case *slackevents.MemberJoinedChannelEvent: case *slackevents.MemberJoinedChannelEvent:
fmt.Printf("user %q joined to channel %q", ev.User, ev.Channel) log.Infof("user %q joined to channel %q", ev.User, ev.Channel)
s.socket.Ack(*evt.Request)
} }
default: default:
s.socket.Debugf("unsupported Events API event received") s.socket.Debugf("unsupported Events API event received")
} }
case socketmode.EventTypeInteractive: case socketmode.EventTypeInteractive:
callback, ok := evt.Data.(slack.InteractionCallback) callback, ok := evt.Data.(slack.InteractionCallback)
if !ok { if !ok {
@ -254,6 +307,7 @@ func (s *Slack) listen() {
log.Debugf("InteractionTypeShortcut: %+v", callback) log.Debugf("InteractionTypeShortcut: %+v", callback)
case slack.InteractionTypeViewSubmission: case slack.InteractionTypeViewSubmission:
// See https://api.slack.com/apis/connections/socket-implement#modal // See https://api.slack.com/apis/connections/socket-implement#modal
log.Debugf("[slack] InteractionTypeViewSubmission: %+v", callback) log.Debugf("[slack] InteractionTypeViewSubmission: %+v", callback)
var values = simplifyStateValues(callback.View.State) var values = simplifyStateValues(callback.View.State)
@ -268,13 +322,14 @@ func (s *Slack) listen() {
if !session.authorizing && !session.Authorized { if !session.authorizing && !session.Authorized {
log.Warn("[slack] telegram is set to private mode, skipping message") log.Warn("[slack] telegram is set to private mode, skipping message")
return continue
} }
reply := s.newReply(session) reply := s.newReply(session)
if s.textMessageResponder != nil { if s.textMessageResponder != nil {
if err := s.textMessageResponder(session, inputValue, reply); err != nil { if err := s.textMessageResponder(session, inputValue, reply); err != nil {
log.WithError(err).Errorf("[slack] response handling error") log.WithError(err).Errorf("[slack] response handling error")
continue
} }
} }
@ -286,12 +341,20 @@ func (s *Slack) listen() {
log.Debugln("response payload", toJson(response)) log.Debugln("response payload", toJson(response))
switch response := response.(type) { switch response := response.(type) {
case string:
payload = map[string]interface{}{
"blocks": []slack.Block{
translateMessageToBlock(response),
},
}
case slack.Blocks: case slack.Blocks:
payload = map[string]interface{}{ payload = map[string]interface{}{
"response_action": "clear",
// "errors": { "ticket-due-date": "You may not select a due date in the past" },
"blocks": response.BlockSet, "blocks": response.BlockSet,
} }
default:
s.socket.Ack(*evt.Request, response)
} }
} }
@ -340,11 +403,24 @@ func (s *Slack) listen() {
} }
switch o := payload.(type) { switch o := payload.(type) {
case string:
s.socket.Ack(*evt.Request, map[string]interface{}{
"blocks": []slack.Block{
translateMessageToBlock(o),
},
})
case *slack.ModalViewRequest: case *slack.ModalViewRequest:
if resp, err := s.socket.OpenView(slashCmd.TriggerID, *o); err != nil { if resp, err := s.socket.OpenView(slashCmd.TriggerID, *o); err != nil {
log.WithError(err).Error("[slack] view open error, resp: %+v", resp) log.WithError(err).Error("[slack] view open error, resp: %+v", resp)
} }
s.socket.Ack(*evt.Request) s.socket.Ack(*evt.Request)
case slack.Blocks:
s.socket.Ack(*evt.Request, map[string]interface{}{
"blocks": o.BlockSet,
})
default: default:
s.socket.Ack(*evt.Request, o) s.socket.Ack(*evt.Request, o)
} }
@ -356,12 +432,15 @@ func (s *Slack) listen() {
} }
func (s *Slack) loadSession(evt socketmode.Event, userID, channelID string) *SlackSession { func (s *Slack) loadSession(evt socketmode.Event, userID, channelID string) *SlackSession {
if session, ok := s.sessions[userID]; ok { key := userID + "-" + channelID
if session, ok := s.sessions[key]; ok {
log.Infof("[slack] an existing session %q found, session: %+v", key, session)
return session return session
} }
session := NewSlackSession(userID, channelID) session := NewSlackSession(s, userID, channelID)
s.sessions[userID] = session s.sessions[key] = session
log.Infof("[slack] allocated a new session %q, session: %+v", key, session)
return session return session
} }
@ -373,7 +452,7 @@ func (s *Slack) newReply(session *SlackSession) *SlackReply {
} }
func (s *Slack) Start(ctx context.Context) { func (s *Slack) Start(ctx context.Context) {
go s.listen() go s.listen(ctx)
if err := s.socket.Run(); err != nil { if err := s.socket.Run(); err != nil {
log.WithError(err).Errorf("slack socketmode error") log.WithError(err).Errorf("slack socketmode error")
} }
@ -447,3 +526,15 @@ func toJson(v interface{}) string {
} }
return string(o) return string(o)
} }
func translateMessageToBlock(message string) slack.Block {
return slack.NewSectionBlock(
&slack.TextBlockObject{
Type: slack.MarkdownType,
Text: message,
},
nil, // fields
nil, // accessory
// slack.SectionBlockOptionBlockID(reply.uuid),
)
}