mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-10 09:11:55 +00:00
interact: add RequireTextInput method to Reply interface
This commit is contained in:
parent
45e4e0bd9f
commit
16688be64d
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user