interact: add RequireTextInput method to Reply interface

This commit is contained in:
c9s 2022-01-21 00:01:22 +08:00
parent ce54a64208
commit 0af5fc0530
5 changed files with 159 additions and 57 deletions

View File

@ -137,7 +137,7 @@ func (it *CoreInteraction) Commands(i *interact.Interact) {
found := false found := false
for signature, strategy := range it.exchangeStrategies { for signature, strategy := range it.exchangeStrategies {
if _, ok := strategy.(PositionCloser); ok { if _, ok := strategy.(PositionCloser); ok {
reply.AddButton(signature, strategy, signature) reply.AddButton(signature, signature, signature)
found = true found = true
} }
} }

View File

@ -16,6 +16,8 @@ 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)
RequireTextInput(title, message string, textFields ...TextField)
// RemoveKeyboard hides the keyboard from the client user interface // RemoveKeyboard hides the keyboard from the client user interface
RemoveKeyboard() RemoveKeyboard()
} }

View File

@ -3,17 +3,16 @@ package interact
import ( import (
"context" "context"
"fmt" "fmt"
"log" stdlog "log"
"os" "os"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/slack-go/slack" "github.com/slack-go/slack"
"github.com/slack-go/slack/slackevents" "github.com/slack-go/slack/slackevents"
"github.com/slack-go/slack/socketmode" "github.com/slack-go/slack/socketmode"
) )
type SlackReply struct { type SlackReply struct {
// uuid is the unique id of this question // uuid is the unique id of this question
// can be used as the callback id // can be used as the callback id
@ -25,7 +24,7 @@ type SlackReply struct {
message string message string
accessories []*slack.Accessory buttons []Button
} }
func (reply *SlackReply) Send(message string) { func (reply *SlackReply) Send(message string) {
@ -35,7 +34,7 @@ func (reply *SlackReply) Send(message string) {
slack.MsgOptionAsUser(false), // Add this if you want that the bot would post message as a user, otherwise it will send response using the default slackbot slack.MsgOptionAsUser(false), // Add this if you want that the bot would post message as a user, otherwise it will send response using the default slackbot
) )
if err != nil { if err != nil {
logrus.WithError(err).Errorf("slack post message error: channel=%s thread=%s", cID, tsID) log.WithError(err).Errorf("slack post message error: channel=%s thread=%s", cID, tsID)
return return
} }
} }
@ -48,18 +47,11 @@ func (reply *SlackReply) Message(message string) {
func (reply *SlackReply) RemoveKeyboard() {} func (reply *SlackReply) RemoveKeyboard() {}
func (reply *SlackReply) AddButton(text string, name string, value string) { func (reply *SlackReply) AddButton(text string, name string, value string) {
actionID := reply.uuid + ":" + value reply.buttons = append(reply.buttons, Button{
reply.accessories = append(reply.accessories, slack.NewAccessory( Text: text,
slack.NewButtonBlockElement( Name: name,
// action id should be unique Value: value,
actionID, })
value,
&slack.TextBlockObject{
Type: slack.PlainTextType,
Text: text,
},
),
))
} }
func (reply *SlackReply) build() map[string]interface{} { func (reply *SlackReply) build() map[string]interface{} {
@ -72,16 +64,27 @@ func (reply *SlackReply) build() map[string]interface{} {
}, },
nil, // fields nil, // fields
nil, // accessory nil, // accessory
nil, // options slack.SectionBlockOptionBlockID(reply.uuid),
)) ))
blocks = append(blocks, slack.NewActionBlock("", if len(reply.buttons) > 0 {
slack.NewButtonBlockElement( var buttons []slack.BlockElement
"actionID", for _, btn := range reply.buttons {
"value", actionID := reply.uuid + ":" + btn.Value
slack.NewTextBlockObject( buttons = append(buttons,
slack.PlainTextType, "text", true, false), slack.NewButtonBlockElement(
))) // action id should be unique
actionID,
btn.Value,
&slack.TextBlockObject{
Type: slack.PlainTextType,
Text: btn.Text,
},
),
)
}
blocks = append(blocks, slack.NewActionBlock(reply.uuid, buttons...))
}
var payload = map[string]interface{}{ var payload = map[string]interface{}{
"blocks": blocks, "blocks": blocks,
@ -100,17 +103,19 @@ type SlackSession struct {
questions map[string]interface{} questions map[string]interface{}
} }
func NewSlackSession() *SlackSession { func NewSlackSession(userID string) *SlackSession {
return &SlackSession{ return &SlackSession{
UserID: userID,
questions: make(map[string]interface{}), questions: make(map[string]interface{}),
} }
} }
func (s *SlackSession) ID() string { func (s *SlackSession) ID() string {
return fmt.Sprintf("%s-%s", s.UserID, s.ChannelID) return s.UserID
// return fmt.Sprintf("%s-%s", s.UserID, s.ChannelID)
} }
type SlackSessionMap map[int64]*SlackSession type SlackSessionMap map[string]*SlackSession
//go:generate callbackgen -type Slack //go:generate callbackgen -type Slack
type Slack struct { type Slack struct {
@ -119,14 +124,15 @@ type Slack struct {
sessions SlackSessionMap sessions SlackSessionMap
commands []*Command commands map[string]*Command
commandResponders map[string]Responder
// textMessageResponder is used for interact to register its message handler // textMessageResponder is used for interact to register its message handler
textMessageResponder Responder textMessageResponder Responder
authorizedCallbacks []func(userSession *SlackSession) authorizedCallbacks []func(userSession *SlackSession)
eventsApiCallbacks []func(slackevents.EventsAPIEvent) eventsApiCallbacks []func(evt slackevents.EventsAPIEvent)
} }
func NewSlack(client *slack.Client) *Slack { func NewSlack(client *slack.Client) *Slack {
@ -134,13 +140,16 @@ func NewSlack(client *slack.Client) *Slack {
client, client,
socketmode.OptionDebug(true), socketmode.OptionDebug(true),
socketmode.OptionLog( socketmode.OptionLog(
log.New(os.Stdout, "socketmode: ", stdlog.New(os.Stdout, "socketmode: ",
log.Lshortfile|log.LstdFlags)), stdlog.Lshortfile|stdlog.LstdFlags)),
) )
return &Slack{ return &Slack{
client: client, client: client,
socket: socket, socket: socket,
sessions: make(SlackSessionMap),
commands: make(map[string]*Command),
commandResponders: make(map[string]Responder),
} }
} }
@ -149,26 +158,36 @@ func (s *Slack) SetTextMessageResponder(responder Responder) {
} }
func (s *Slack) AddCommand(command *Command, responder Responder) { func (s *Slack) AddCommand(command *Command, responder Responder) {
s.commands = append(s.commands, command) if _, exists := s.commands[command.Name]; exists {
panic(fmt.Errorf("command %s already exists, can not be re-defined", command.Name))
}
s.commands[command.Name] = command
s.commandResponders[command.Name] = responder
} }
func (s *Slack) listen() { func (s *Slack) listen() {
for evt := range s.socket.Events { for evt := range s.socket.Events {
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...") fmt.Println("Connecting to Slack with Socket Mode...")
case socketmode.EventTypeConnectionError: case socketmode.EventTypeConnectionError:
fmt.Println("Connection failed. Retrying later...") fmt.Println("Connection failed. Retrying later...")
case socketmode.EventTypeConnected: case socketmode.EventTypeConnected:
fmt.Println("Connected to Slack with Socket Mode.") fmt.Println("Connected to Slack with Socket Mode.")
case socketmode.EventTypeEventsAPI: case socketmode.EventTypeEventsAPI:
eventsAPIEvent, ok := evt.Data.(slackevents.EventsAPIEvent) eventsAPIEvent, ok := evt.Data.(slackevents.EventsAPIEvent)
if !ok { if !ok {
logrus.Debugf("ignored %+v", evt) log.Debugf("ignored %+v", evt)
continue continue
} }
logrus.Debugf("event received: %+v", eventsAPIEvent) log.Debugf("event received: %+v", eventsAPIEvent)
s.socket.Ack(*evt.Request) s.socket.Ack(*evt.Request)
s.EmitEventsApi(eventsAPIEvent) s.EmitEventsApi(eventsAPIEvent)
@ -191,18 +210,21 @@ func (s *Slack) listen() {
case socketmode.EventTypeInteractive: case socketmode.EventTypeInteractive:
callback, ok := evt.Data.(slack.InteractionCallback) callback, ok := evt.Data.(slack.InteractionCallback)
if !ok { if !ok {
logrus.Debugf("ignored %+v", evt) log.Debugf("ignored %+v", evt)
continue continue
} }
logrus.Debugf("interaction received: %+v", callback) log.Debugf("interaction received: %+v", callback)
var payload interface{} var payload interface{}
switch callback.Type { switch callback.Type {
case slack.InteractionTypeBlockActions: case slack.InteractionTypeBlockActions:
// See https://api.slack.com/apis/connections/socket-implement#button // See https://api.slack.com/apis/connections/socket-implement#button
logrus.Debugf("button clicked!") log.Debugf("button clicked!")
// TODO: check and find what's the response handler for the reply
// we need to find the session first,
// and then look up the state, call the function to transit the state with the given value
case slack.InteractionTypeShortcut: case slack.InteractionTypeShortcut:
case slack.InteractionTypeViewSubmission: case slack.InteractionTypeViewSubmission:
@ -214,31 +236,55 @@ func (s *Slack) listen() {
s.socket.Ack(*evt.Request, payload) s.socket.Ack(*evt.Request, payload)
case socketmode.EventTypeHello:
log.Debugf("hello command received: %+v", evt)
case socketmode.EventTypeSlashCommand: case socketmode.EventTypeSlashCommand:
cmd, ok := evt.Data.(slack.SlashCommand) slashCmd, ok := evt.Data.(slack.SlashCommand)
if !ok { if !ok {
logrus.Debugf("ignored %+v", evt) log.Debugf("ignored %+v", evt)
continue continue
} }
logrus.Debugf("slash command received: %+v", cmd) log.Debugf("slash command received: %+v", slashCmd)
if responder, exists := s.commandResponders[slashCmd.Command]; exists {
session := s.findSession(evt, slashCmd.UserID)
reply := s.newReply(session)
if err := responder(session, slashCmd.Text, reply); err != nil {
log.WithError(err).Errorf("responder returns error")
continue
}
session := s.newSession(evt) req := generateTextInputModalRequest("Authentication", "Please enter your code", TextField{
reply := s.newReply(session) Label: "First Name",
if err := s.textMessageResponder(session, "", reply); err != nil { Name: "first_name",
continue PlaceHolder: "Enter your first name",
})
// s.socket.Ack(*evt.Request, req)
if resp, err := s.client.OpenView(slashCmd.TriggerID, req); err != nil {
log.WithError(err).Error("view open error, resp: %+v", resp)
}
payload := reply.build()
s.socket.Ack(*evt.Request, payload)
} else {
log.Errorf("command %s does not exist", slashCmd.Command)
} }
payload := reply.build()
s.socket.Ack(*evt.Request, payload)
default: default:
logrus.Debugf("unexpected event type received: %s", evt.Type) log.Debugf("unexpected event type received: %s", evt.Type)
} }
} }
} }
func (s *Slack) newSession(evt socketmode.Event) *SlackSession { func (s *Slack) findSession(evt socketmode.Event, userID string) *SlackSession {
return NewSlackSession() if session, ok := s.sessions[userID]; ok {
return session
}
session := NewSlackSession(userID)
s.sessions[userID] = session
return session
} }
func (s *Slack) newReply(session *SlackSession) *SlackReply { func (s *Slack) newReply(session *SlackSession) *SlackReply {
@ -251,6 +297,50 @@ 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()
if err := s.socket.Run(); err != nil { if err := s.socket.Run(); err != nil {
logrus.WithError(err).Errorf("slack socketmode error") log.WithError(err).Errorf("slack socketmode error")
} }
} }
type TextField struct {
// Label is the field label
Label string
// Name is the form field name
Name string
// PlaceHolder is the sample text in the text input
PlaceHolder string
}
func generateTextInputModalRequest(title string, prompt string, textFields ...TextField) slack.ModalViewRequest {
// create a ModalViewRequest with a header and two inputs
titleText := slack.NewTextBlockObject("plain_text", title, false, false)
closeText := slack.NewTextBlockObject("plain_text", "Close", false, false)
submitText := slack.NewTextBlockObject("plain_text", "Submit", false, false)
headerText := slack.NewTextBlockObject("mrkdwn", prompt, false, false)
headerSection := slack.NewSectionBlock(headerText, nil, nil)
blocks := slack.Blocks{
BlockSet: []slack.Block{
headerSection,
},
}
for _, textField := range textFields {
firstNameText := slack.NewTextBlockObject("plain_text", textField.Label, false, false)
firstNamePlaceholder := slack.NewTextBlockObject("plain_text", textField.PlaceHolder, false, false)
firstNameElement := slack.NewPlainTextInputBlockElement(firstNamePlaceholder, textField.Name)
// Notice that blockID is a unique identifier for a block
firstName := slack.NewInputBlock(textField.Name, firstNameText, firstNameElement)
blocks.BlockSet = append(blocks.BlockSet, firstName)
}
var modalRequest slack.ModalViewRequest
modalRequest.Type = slack.ViewType("modal")
modalRequest.Title = titleText
modalRequest.Close = closeText
modalRequest.Submit = submitText
modalRequest.Blocks = blocks
return modalRequest
}

View File

@ -16,12 +16,12 @@ func (s *Slack) EmitAuthorized(userSession *SlackSession) {
} }
} }
func (s *Slack) OnEventsApi(cb func(slackevents.EventsAPIEvent)) { func (s *Slack) OnEventsApi(cb func(evt slackevents.EventsAPIEvent)) {
s.eventsApiCallbacks = append(s.eventsApiCallbacks, cb) s.eventsApiCallbacks = append(s.eventsApiCallbacks, cb)
} }
func (s *Slack) EmitEventsApi(slackevents.EventsAPIEvent) { func (s *Slack) EmitEventsApi(evt slackevents.EventsAPIEvent) {
for _, cb := range s.eventsApiCallbacks { for _, cb := range s.eventsApiCallbacks {
cb() cb(evt)
} }
} }

View File

@ -10,6 +10,11 @@ import (
"gopkg.in/tucnak/telebot.v2" "gopkg.in/tucnak/telebot.v2"
) )
func init() {
// force interface type check
_ = Reply(&TelegramReply{})
}
type TelegramSessionMap map[int64]*TelegramSession type TelegramSessionMap map[int64]*TelegramSession
type TelegramSession struct { type TelegramSession struct {
@ -56,6 +61,7 @@ type TelegramReply struct {
set bool set bool
} }
func (r *TelegramReply) Send(message string) { func (r *TelegramReply) Send(message string) {
checkSendErr(r.bot.Send(r.session.Chat, message)) checkSendErr(r.bot.Send(r.session.Chat, message))
} }
@ -65,6 +71,10 @@ func (r *TelegramReply) Message(message string) {
r.set = true r.set = true
} }
func (r *TelegramReply) RequireTextInput(title, message string, textFields ...TextField) {
r.message = message
}
func (r *TelegramReply) RemoveKeyboard() { func (r *TelegramReply) RemoveKeyboard() {
r.menu.ReplyKeyboardRemove = true r.menu.ReplyKeyboardRemove = true
r.set = true r.set = true