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
for signature, strategy := range it.exchangeStrategies {
if _, ok := strategy.(PositionCloser); ok {
reply.AddButton(signature, strategy, signature)
reply.AddButton(signature, signature, signature)
found = true
}
}

View File

@ -16,6 +16,8 @@ type Reply interface {
// AddButton adds the button to the reply
AddButton(text string, name, value string)
RequireTextInput(title, message string, textFields ...TextField)
// RemoveKeyboard hides the keyboard from the client user interface
RemoveKeyboard()
}

View File

@ -3,17 +3,16 @@ package interact
import (
"context"
"fmt"
"log"
stdlog "log"
"os"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
log "github.com/sirupsen/logrus"
"github.com/slack-go/slack"
"github.com/slack-go/slack/slackevents"
"github.com/slack-go/slack/socketmode"
)
type SlackReply struct {
// uuid is the unique id of this question
// can be used as the callback id
@ -25,7 +24,7 @@ type SlackReply struct {
message string
accessories []*slack.Accessory
buttons []Button
}
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
)
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
}
}
@ -48,18 +47,11 @@ func (reply *SlackReply) Message(message string) {
func (reply *SlackReply) RemoveKeyboard() {}
func (reply *SlackReply) AddButton(text string, name string, value string) {
actionID := reply.uuid + ":" + value
reply.accessories = append(reply.accessories, slack.NewAccessory(
slack.NewButtonBlockElement(
// action id should be unique
actionID,
value,
&slack.TextBlockObject{
Type: slack.PlainTextType,
Text: text,
},
),
))
reply.buttons = append(reply.buttons, Button{
Text: text,
Name: name,
Value: value,
})
}
func (reply *SlackReply) build() map[string]interface{} {
@ -72,16 +64,27 @@ func (reply *SlackReply) build() map[string]interface{} {
},
nil, // fields
nil, // accessory
nil, // options
slack.SectionBlockOptionBlockID(reply.uuid),
))
blocks = append(blocks, slack.NewActionBlock("",
slack.NewButtonBlockElement(
"actionID",
"value",
slack.NewTextBlockObject(
slack.PlainTextType, "text", true, false),
)))
if len(reply.buttons) > 0 {
var buttons []slack.BlockElement
for _, btn := range reply.buttons {
actionID := reply.uuid + ":" + btn.Value
buttons = append(buttons,
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{}{
"blocks": blocks,
@ -100,17 +103,19 @@ type SlackSession struct {
questions map[string]interface{}
}
func NewSlackSession() *SlackSession {
func NewSlackSession(userID string) *SlackSession {
return &SlackSession{
UserID: userID,
questions: make(map[string]interface{}),
}
}
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
type Slack struct {
@ -119,14 +124,15 @@ type Slack struct {
sessions SlackSessionMap
commands []*Command
commands map[string]*Command
commandResponders map[string]Responder
// textMessageResponder is used for interact to register its message handler
textMessageResponder Responder
authorizedCallbacks []func(userSession *SlackSession)
eventsApiCallbacks []func(slackevents.EventsAPIEvent)
eventsApiCallbacks []func(evt slackevents.EventsAPIEvent)
}
func NewSlack(client *slack.Client) *Slack {
@ -134,13 +140,16 @@ func NewSlack(client *slack.Client) *Slack {
client,
socketmode.OptionDebug(true),
socketmode.OptionLog(
log.New(os.Stdout, "socketmode: ",
log.Lshortfile|log.LstdFlags)),
stdlog.New(os.Stdout, "socketmode: ",
stdlog.Lshortfile|stdlog.LstdFlags)),
)
return &Slack{
client: client,
socket: socket,
client: client,
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) {
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() {
for evt := range s.socket.Events {
log.Debugf("event: %+v", evt)
switch evt.Type {
case socketmode.EventTypeConnecting:
fmt.Println("Connecting to Slack with Socket Mode...")
case socketmode.EventTypeConnectionError:
fmt.Println("Connection failed. Retrying later...")
case socketmode.EventTypeConnected:
fmt.Println("Connected to Slack with Socket Mode.")
case socketmode.EventTypeEventsAPI:
eventsAPIEvent, ok := evt.Data.(slackevents.EventsAPIEvent)
if !ok {
logrus.Debugf("ignored %+v", evt)
log.Debugf("ignored %+v", evt)
continue
}
logrus.Debugf("event received: %+v", eventsAPIEvent)
log.Debugf("event received: %+v", eventsAPIEvent)
s.socket.Ack(*evt.Request)
s.EmitEventsApi(eventsAPIEvent)
@ -191,18 +210,21 @@ func (s *Slack) listen() {
case socketmode.EventTypeInteractive:
callback, ok := evt.Data.(slack.InteractionCallback)
if !ok {
logrus.Debugf("ignored %+v", evt)
log.Debugf("ignored %+v", evt)
continue
}
logrus.Debugf("interaction received: %+v", callback)
log.Debugf("interaction received: %+v", callback)
var payload interface{}
switch callback.Type {
case slack.InteractionTypeBlockActions:
// 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.InteractionTypeViewSubmission:
@ -214,31 +236,55 @@ func (s *Slack) listen() {
s.socket.Ack(*evt.Request, payload)
case socketmode.EventTypeHello:
log.Debugf("hello command received: %+v", evt)
case socketmode.EventTypeSlashCommand:
cmd, ok := evt.Data.(slack.SlashCommand)
slashCmd, ok := evt.Data.(slack.SlashCommand)
if !ok {
logrus.Debugf("ignored %+v", evt)
log.Debugf("ignored %+v", evt)
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)
reply := s.newReply(session)
if err := s.textMessageResponder(session, "", reply); err != nil {
continue
req := generateTextInputModalRequest("Authentication", "Please enter your code", TextField{
Label: "First Name",
Name: "first_name",
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:
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 {
return NewSlackSession()
func (s *Slack) findSession(evt socketmode.Event, userID string) *SlackSession {
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 {
@ -251,6 +297,50 @@ func (s *Slack) newReply(session *SlackSession) *SlackReply {
func (s *Slack) Start(ctx context.Context) {
go s.listen()
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)
}
func (s *Slack) EmitEventsApi(slackevents.EventsAPIEvent) {
func (s *Slack) EmitEventsApi(evt slackevents.EventsAPIEvent) {
for _, cb := range s.eventsApiCallbacks {
cb()
cb(evt)
}
}

View File

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