slack: add reply and session struct

This commit is contained in:
c9s 2022-01-19 13:07:25 +08:00
parent 2cf29bd1ec
commit f5f8f15670
6 changed files with 178 additions and 45 deletions

View File

@ -45,7 +45,7 @@ func (m *PositionInteraction) Commands(i *interact.Interact) {
// send symbol options // send symbol options
reply.Message("Choose your position") reply.Message("Choose your position")
for _, symbol := range []string{"BTCUSDT", "ETHUSDT"} { for _, symbol := range []string{"BTCUSDT", "ETHUSDT"} {
reply.AddButton(symbol) reply.AddButton(symbol, symbol, symbol)
} }
return nil return nil
@ -68,7 +68,7 @@ func (m *PositionInteraction) Commands(i *interact.Interact) {
reply.Message("Choose or enter the percentage to close") reply.Message("Choose or enter the percentage to close")
for _, symbol := range []string{"25%", "50%", "100%"} { for _, symbol := range []string{"25%", "50%", "100%"} {
reply.AddButton(symbol) reply.AddButton(symbol, symbol, symbol)
} }
// send percentage options // send percentage options
@ -85,7 +85,7 @@ func (m *PositionInteraction) Commands(i *interact.Interact) {
// send confirmation // send confirmation
reply.Message("Are you sure to close the position?") reply.Message("Are you sure to close the position?")
reply.AddButton("Yes") reply.AddButton("Yes", "confirm", "yes")
return nil return nil
}).Next(func(reply interact.Reply, confirm string) error { }).Next(func(reply interact.Reply, confirm string) error {
switch strings.ToLower(confirm) { switch strings.ToLower(confirm) {

View File

@ -44,6 +44,12 @@ func NewCoreInteraction(environment *Environment, trader *Trader) *CoreInteracti
func (it *CoreInteraction) Commands(i *interact.Interact) { func (it *CoreInteraction) Commands(i *interact.Interact) {
i.PrivateCommand("/sessions", "List Exchange Sessions", func(reply interact.Reply) error { i.PrivateCommand("/sessions", "List Exchange Sessions", func(reply interact.Reply) error {
switch r := reply.(type) {
case *interact.SlackReply:
// call slack specific api to build the reply object
_ = r
}
message := "Your connected sessions:\n" message := "Your connected sessions:\n"
for name, session := range it.environment.Sessions() { for name, session := range it.environment.Sessions() {
message += "- " + name + " (" + session.ExchangeName.String() + ")\n" message += "- " + name + " (" + session.ExchangeName.String() + ")\n"
@ -56,7 +62,7 @@ func (it *CoreInteraction) Commands(i *interact.Interact) {
i.PrivateCommand("/balances", "Show balances", func(reply interact.Reply) error { i.PrivateCommand("/balances", "Show balances", func(reply interact.Reply) error {
reply.Message("Please select an exchange session") reply.Message("Please select an exchange session")
for name := range it.environment.Sessions() { for name := range it.environment.Sessions() {
reply.AddButton(name) reply.AddButton(name, "session", name)
} }
return nil return nil
}).Next(func(sessionName string, reply interact.Reply) error { }).Next(func(sessionName string, reply interact.Reply) error {
@ -86,7 +92,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.(PositionReader); ok { if _, ok := strategy.(PositionReader); ok {
reply.AddButton(signature) reply.AddButton(signature, "strategy", signature)
found = true found = true
} }
} }
@ -131,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) reply.AddButton(signature, strategy, signature)
found = true found = true
} }
} }
@ -173,8 +179,8 @@ func (it *CoreInteraction) Commands(i *interact.Interact) {
} }
reply.Message("Choose or enter the percentage to close") reply.Message("Choose or enter the percentage to close")
for _, symbol := range []string{"5%", "25%", "50%", "80%", "100%"} { for _, p := range []string{"5%", "25%", "50%", "80%", "100%"} {
reply.AddButton(symbol) reply.AddButton(p, "percentage", p)
} }
return nil return nil

View File

@ -1,8 +1,33 @@
package interact package interact
type Button struct {
Text string
Name string
Value string
}
type Reply interface { type Reply interface {
// Send sends the message directly to the client's session
Send(message string) Send(message string)
// Message sets the message to the reply
Message(message string) Message(message string)
AddButton(text string)
// AddButton adds the button to the reply
AddButton(text string, name, value string)
// RemoveKeyboard hides the keyboard from the client user interface
RemoveKeyboard() RemoveKeyboard()
} }
// ButtonReply can be used if your reply needs button user interface.
type ButtonReply interface {
// AddButton adds the button to the reply
AddButton(text string)
}
// DialogReply can be used if your reply needs Dialog user interface
type DialogReply interface {
// AddButton adds the button to the reply
Dialog(title, text string, buttons []string)
}

View File

@ -3,6 +3,10 @@ package interact
// Responder defines the logic of responding the message // Responder defines the logic of responding the message
type Responder func(session Session, message string, reply Reply, ctxObjects ...interface{}) error type Responder func(session Session, message string, reply Reply, ctxObjects ...interface{}) error
type CallbackResponder interface {
SetCallbackResponder(responder Responder)
}
type TextMessageResponder interface { type TextMessageResponder interface {
SetTextMessageResponder(responder Responder) SetTextMessageResponder(responder Responder)
} }

View File

@ -6,14 +6,108 @@ import (
"log" "log"
"os" "os"
"github.com/google/uuid"
"github.com/sirupsen/logrus" "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 {
// uuid is the unique id of this question
// can be used as the callback id
uuid string
session *SlackSession
client *slack.Client
message string
accessories []*slack.Accessory
}
func (reply *SlackReply) Send(message string) {
cID, tsID, err := reply.client.PostMessage(
reply.session.ChannelID,
slack.MsgOptionText(message, false),
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)
return
}
}
func (reply *SlackReply) Message(message string) {
reply.message = message
}
// RemoveKeyboard is not supported by Slack
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,
},
),
))
}
func (reply *SlackReply) build() map[string]interface{} {
var blocks []slack.Block
blocks = append(blocks, slack.NewSectionBlock(
&slack.TextBlockObject{
Type: slack.MarkdownType,
Text: reply.message,
},
nil, // fields
nil, // accessory
nil, // options
))
blocks = append(blocks, slack.NewActionBlock("",
slack.NewButtonBlockElement(
"actionID",
"value",
slack.NewTextBlockObject(
slack.PlainTextType, "text", true, false),
)))
var payload = map[string]interface{}{
"blocks": blocks,
}
return payload
}
type SlackSession struct { type SlackSession struct {
BaseSession BaseSession
ChannelID 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() *SlackSession {
return &SlackSession{
questions: make(map[string]interface{}),
}
}
func (s *SlackSession) ID() string {
return fmt.Sprintf("%s-%s", s.UserID, s.ChannelID)
} }
type SlackSessionMap map[int64]*SlackSession type SlackSessionMap map[int64]*SlackSession
@ -31,6 +125,8 @@ type Slack struct {
textMessageResponder Responder textMessageResponder Responder
authorizedCallbacks []func(userSession *SlackSession) authorizedCallbacks []func(userSession *SlackSession)
eventsApiCallbacks []func(slackevents.EventsAPIEvent)
} }
func NewSlack(client *slack.Client) *Slack { func NewSlack(client *slack.Client) *Slack {
@ -68,15 +164,15 @@ func (s *Slack) listen() {
case socketmode.EventTypeEventsAPI: case socketmode.EventTypeEventsAPI:
eventsAPIEvent, ok := evt.Data.(slackevents.EventsAPIEvent) eventsAPIEvent, ok := evt.Data.(slackevents.EventsAPIEvent)
if !ok { if !ok {
fmt.Printf("Ignored %+v\n", evt) logrus.Debugf("ignored %+v", evt)
continue continue
} }
fmt.Printf("Event received: %+v\n", eventsAPIEvent) logrus.Debugf("event received: %+v", eventsAPIEvent)
s.socket.Ack(*evt.Request) s.socket.Ack(*evt.Request)
s.EmitEventsApi(eventsAPIEvent)
switch eventsAPIEvent.Type { switch eventsAPIEvent.Type {
case slackevents.CallbackEvent: case slackevents.CallbackEvent:
innerEvent := eventsAPIEvent.InnerEvent innerEvent := eventsAPIEvent.InnerEvent
@ -95,20 +191,19 @@ 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 {
fmt.Printf("Ignored %+v\n", evt) logrus.Debugf("ignored %+v", evt)
continue continue
} }
fmt.Printf("Interaction received: %+v\n", callback) logrus.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!")
s.socket.Debugf("button clicked!")
case slack.InteractionTypeShortcut: case slack.InteractionTypeShortcut:
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
@ -118,47 +213,44 @@ func (s *Slack) listen() {
} }
s.socket.Ack(*evt.Request, payload) s.socket.Ack(*evt.Request, payload)
case socketmode.EventTypeSlashCommand: case socketmode.EventTypeSlashCommand:
cmd, ok := evt.Data.(slack.SlashCommand) cmd, ok := evt.Data.(slack.SlashCommand)
if !ok { if !ok {
fmt.Printf("Ignored %+v\n", evt) logrus.Debugf("ignored %+v", evt)
continue continue
} }
s.socket.Debugf("Slash command received: %+v", cmd) logrus.Debugf("slash command received: %+v", cmd)
payload := map[string]interface{}{ session := s.newSession(evt)
"blocks": []slack.Block{ reply := s.newReply(session)
slack.NewSectionBlock( if err := s.textMessageResponder(session, "", reply); err != nil {
&slack.TextBlockObject{ continue
Type: slack.MarkdownType, }
Text: "foo",
},
nil,
slack.NewAccessory(
slack.NewButtonBlockElement(
"",
"somevalue",
&slack.TextBlockObject{
Type: slack.PlainTextType,
Text: "bar",
},
),
),
),
}}
payload := reply.build()
s.socket.Ack(*evt.Request, payload) s.socket.Ack(*evt.Request, payload)
default: default:
fmt.Fprintf(os.Stderr, "Unexpected event type received: %s\n", evt.Type) logrus.Debugf("unexpected event type received: %s", evt.Type)
} }
} }
} }
func (s *Slack) newSession(evt socketmode.Event) *SlackSession {
return NewSlackSession()
}
func (s *Slack) newReply(session *SlackSession) *SlackReply {
return &SlackReply{
uuid: uuid.New().String(),
session: session,
}
}
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") logrus.WithError(err).Errorf("slack socketmode error")
} }
} }

View File

@ -70,7 +70,7 @@ func (r *TelegramReply) RemoveKeyboard() {
r.set = true r.set = true
} }
func (r *TelegramReply) AddButton(text string) { func (r *TelegramReply) AddButton(text string, name string, value string) {
var button = r.menu.Text(text) var button = r.menu.Text(text)
if len(r.buttons) == 0 { if len(r.buttons) == 0 {
r.buttons = append(r.buttons, []telebot.Btn{}) r.buttons = append(r.buttons, []telebot.Btn{})
@ -101,6 +101,8 @@ type Telegram struct {
// textMessageResponder is used for interact to register its message handler // textMessageResponder is used for interact to register its message handler
textMessageResponder Responder textMessageResponder Responder
callbackResponder CallbackResponder
commands []*Command commands []*Command
authorizedCallbacks []func(s *TelegramSession) authorizedCallbacks []func(s *TelegramSession)
@ -114,8 +116,12 @@ func NewTelegram(bot *telebot.Bot) *Telegram {
} }
} }
func (tm *Telegram) SetTextMessageResponder(textMessageResponder Responder) { func (tm *Telegram) SetCallbackResponder(responder CallbackResponder) {
tm.textMessageResponder = textMessageResponder tm.callbackResponder = responder
}
func (tm *Telegram) SetTextMessageResponder(responder Responder) {
tm.textMessageResponder = responder
} }
func (tm *Telegram) Start(context.Context) { func (tm *Telegram) Start(context.Context) {