bbgo_origin/pkg/interact/slack.go
2022-01-22 00:51:43 +08:00

257 lines
6.0 KiB
Go

package interact
import (
"context"
"fmt"
"log"
"os"
"github.com/google/uuid"
"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
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 {
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
//go:generate callbackgen -type Slack
type Slack struct {
client *slack.Client
socket *socketmode.Client
sessions SlackSessionMap
commands []*Command
// textMessageResponder is used for interact to register its message handler
textMessageResponder Responder
authorizedCallbacks []func(userSession *SlackSession)
eventsApiCallbacks []func(slackevents.EventsAPIEvent)
}
func NewSlack(client *slack.Client) *Slack {
socket := socketmode.New(
client,
socketmode.OptionDebug(true),
socketmode.OptionLog(
log.New(os.Stdout, "socketmode: ",
log.Lshortfile|log.LstdFlags)),
)
return &Slack{
client: client,
socket: socket,
}
}
func (s *Slack) SetTextMessageResponder(responder Responder) {
s.textMessageResponder = responder
}
func (s *Slack) AddCommand(command *Command, responder Responder) {
s.commands = append(s.commands, command)
}
func (s *Slack) listen() {
for evt := range s.socket.Events {
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)
continue
}
logrus.Debugf("event received: %+v", eventsAPIEvent)
s.socket.Ack(*evt.Request)
s.EmitEventsApi(eventsAPIEvent)
switch eventsAPIEvent.Type {
case slackevents.CallbackEvent:
innerEvent := eventsAPIEvent.InnerEvent
switch ev := innerEvent.Data.(type) {
case *slackevents.AppMentionEvent:
_, _, err := s.client.PostMessage(ev.Channel, slack.MsgOptionText("Yes, hello.", false))
if err != nil {
fmt.Printf("failed posting message: %v", err)
}
case *slackevents.MemberJoinedChannelEvent:
fmt.Printf("user %q joined to channel %q", ev.User, ev.Channel)
}
default:
s.socket.Debugf("unsupported Events API event received")
}
case socketmode.EventTypeInteractive:
callback, ok := evt.Data.(slack.InteractionCallback)
if !ok {
logrus.Debugf("ignored %+v", evt)
continue
}
logrus.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!")
case slack.InteractionTypeShortcut:
case slack.InteractionTypeViewSubmission:
// See https://api.slack.com/apis/connections/socket-implement#modal
case slack.InteractionTypeDialogSubmission:
default:
}
s.socket.Ack(*evt.Request, payload)
case socketmode.EventTypeSlashCommand:
cmd, ok := evt.Data.(slack.SlashCommand)
if !ok {
logrus.Debugf("ignored %+v", evt)
continue
}
logrus.Debugf("slash command received: %+v", cmd)
session := s.newSession(evt)
reply := s.newReply(session)
if err := s.textMessageResponder(session, "", reply); err != nil {
continue
}
payload := reply.build()
s.socket.Ack(*evt.Request, payload)
default:
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) {
go s.listen()
if err := s.socket.Run(); err != nil {
logrus.WithError(err).Errorf("slack socketmode error")
}
}