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
ce54a64208
commit
0af5fc0530
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue
Block a user