2022-01-16 11:06:26 +00:00
package interact
import (
"context"
2022-01-20 18:01:41 +00:00
"encoding/json"
2022-01-16 11:06:26 +00:00
"fmt"
2022-01-20 16:01:22 +00:00
stdlog "log"
2022-01-16 11:06:26 +00:00
"os"
2022-01-22 17:35:27 +00:00
"time"
2022-01-16 11:06:26 +00:00
2022-01-19 05:07:25 +00:00
"github.com/google/uuid"
2022-01-20 16:01:22 +00:00
log "github.com/sirupsen/logrus"
2022-01-16 11:06:26 +00:00
"github.com/slack-go/slack"
"github.com/slack-go/slack/slackevents"
"github.com/slack-go/slack/socketmode"
2022-03-14 13:20:53 +00:00
"github.com/c9s/bbgo/pkg/util"
2022-01-16 11:06:26 +00:00
)
2022-01-19 05:07:25 +00:00
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
2022-01-20 16:01:22 +00:00
buttons [ ] Button
2022-01-20 16:45:43 +00:00
textInputModalViewRequest * slack . ModalViewRequest
2022-01-19 05:07:25 +00:00
}
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 {
2022-01-20 16:01:22 +00:00
log . WithError ( err ) . Errorf ( "slack post message error: channel=%s thread=%s" , cID , tsID )
2022-01-19 05:07:25 +00:00
return
}
}
2022-01-22 17:35:27 +00:00
func ( reply * SlackReply ) InputText ( prompt string , textFields ... TextField ) {
reply . message = prompt
reply . textInputModalViewRequest = generateTextInputModalRequest ( prompt , prompt , textFields ... )
}
2022-01-20 16:45:43 +00:00
func ( reply * SlackReply ) Choose ( prompt string , options ... Option ) {
}
2022-01-19 05:07:25 +00:00
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 ) {
2022-01-20 16:01:22 +00:00
reply . buttons = append ( reply . buttons , Button {
Text : text ,
Name : name ,
Value : value ,
} )
2022-01-19 05:07:25 +00:00
}
2022-01-20 16:45:43 +00:00
func ( reply * SlackReply ) build ( ) interface { } {
2022-01-22 17:35:27 +00:00
// you should avoid using this modal view request, because it interrupts the interaction flow
// once we send the modal view request, we can't go back to the channel.
// (we don't know which channel the user started the interaction)
2022-01-20 16:45:43 +00:00
if reply . textInputModalViewRequest != nil {
return reply . textInputModalViewRequest
}
if len ( reply . message ) > 0 {
2022-01-22 17:35:27 +00:00
return reply . message
2022-01-20 16:45:43 +00:00
}
2022-01-22 17:35:27 +00:00
var blocks slack . Blocks
2022-01-21 16:49:50 +00:00
blocks . BlockSet = append ( blocks . BlockSet , slack . NewSectionBlock (
2022-01-19 05:07:25 +00:00
& slack . TextBlockObject {
Type : slack . MarkdownType ,
Text : reply . message ,
} ,
nil , // fields
nil , // accessory
2022-01-20 16:01:22 +00:00
slack . SectionBlockOptionBlockID ( reply . uuid ) ,
2022-01-19 05:07:25 +00:00
) )
2022-01-20 16:01:22 +00:00
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 ,
} ,
) ,
)
}
2022-01-21 16:49:50 +00:00
blocks . BlockSet = append ( blocks . BlockSet , slack . NewActionBlock ( reply . uuid , buttons ... ) )
2022-01-20 16:01:22 +00:00
}
2022-01-19 05:07:25 +00:00
2022-01-21 16:49:50 +00:00
return blocks
2022-01-19 05:07:25 +00:00
}
2022-01-16 11:06:26 +00:00
type SlackSession struct {
BaseSession
2022-01-19 05:07:25 +00:00
2022-01-22 17:35:27 +00:00
slack * Slack
2022-01-19 05:07:25 +00:00
ChannelID string
UserID string
}
2022-01-22 17:35:27 +00:00
func NewSlackSession ( slack * Slack , userID , channelID string ) * SlackSession {
2022-01-19 05:07:25 +00:00
return & SlackSession {
2022-01-22 17:35:27 +00:00
BaseSession : BaseSession {
OriginState : StatePublic ,
CurrentState : StatePublic ,
Authorized : false ,
authorizing : false ,
StartedTime : time . Now ( ) ,
} ,
slack : slack ,
2022-01-20 16:01:22 +00:00
UserID : userID ,
2022-01-21 16:49:50 +00:00
ChannelID : channelID ,
2022-01-19 05:07:25 +00:00
}
}
func ( s * SlackSession ) ID ( ) string {
2022-01-22 17:35:27 +00:00
return fmt . Sprintf ( "%s-%s" , s . UserID , s . ChannelID )
}
func ( s * SlackSession ) SetAuthorized ( ) {
s . BaseSession . SetAuthorized ( )
s . slack . EmitAuthorized ( s )
2022-01-16 11:06:26 +00:00
}
2022-01-20 16:01:22 +00:00
type SlackSessionMap map [ string ] * SlackSession
2022-01-16 11:06:26 +00:00
//go:generate callbackgen -type Slack
type Slack struct {
client * slack . Client
socket * socketmode . Client
sessions SlackSessionMap
2022-01-20 16:01:22 +00:00
commands map [ string ] * Command
commandResponders map [ string ] Responder
2022-01-16 11:06:26 +00:00
// textMessageResponder is used for interact to register its message handler
textMessageResponder Responder
authorizedCallbacks [ ] func ( userSession * SlackSession )
2022-01-19 05:07:25 +00:00
2022-01-20 16:01:22 +00:00
eventsApiCallbacks [ ] func ( evt slackevents . EventsAPIEvent )
2022-01-16 11:06:26 +00:00
}
func NewSlack ( client * slack . Client ) * Slack {
2022-03-14 13:20:53 +00:00
var opts = [ ] socketmode . Option {
2022-01-16 11:06:26 +00:00
socketmode . OptionLog (
2022-01-20 16:01:22 +00:00
stdlog . New ( os . Stdout , "socketmode: " ,
stdlog . Lshortfile | stdlog . LstdFlags ) ) ,
2022-03-14 13:20:53 +00:00
}
if b , ok := util . GetEnvVarBool ( "DEBUG_SLACK" ) ; ok {
opts = append ( opts , socketmode . OptionDebug ( b ) )
}
2022-01-16 11:06:26 +00:00
2022-03-14 13:20:53 +00:00
socket := socketmode . New ( client , opts ... )
2022-01-16 11:06:26 +00:00
return & Slack {
2022-01-20 16:01:22 +00:00
client : client ,
socket : socket ,
sessions : make ( SlackSessionMap ) ,
commands : make ( map [ string ] * Command ) ,
commandResponders : make ( map [ string ] Responder ) ,
2022-01-16 11:06:26 +00:00
}
}
func ( s * Slack ) SetTextMessageResponder ( responder Responder ) {
s . textMessageResponder = responder
}
func ( s * Slack ) AddCommand ( command * Command , responder Responder ) {
2022-01-20 16:01:22 +00:00
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
2022-01-16 11:06:26 +00:00
}
2022-01-22 17:35:27 +00:00
func ( s * Slack ) listen ( ctx context . Context ) {
2022-01-16 11:06:26 +00:00
for evt := range s . socket . Events {
2022-01-20 16:01:22 +00:00
log . Debugf ( "event: %+v" , evt )
2022-01-16 11:06:26 +00:00
switch evt . Type {
case socketmode . EventTypeConnecting :
2022-01-22 17:35:27 +00:00
log . Infof ( "connecting to slack with socket mode..." )
2022-01-20 16:01:22 +00:00
2022-01-16 11:06:26 +00:00
case socketmode . EventTypeConnectionError :
2022-01-22 17:35:27 +00:00
log . Infof ( "connection failed. retrying later..." )
2022-01-20 16:01:22 +00:00
2022-01-16 11:06:26 +00:00
case socketmode . EventTypeConnected :
2022-01-22 17:35:27 +00:00
log . Infof ( "connected to slack with socket mode." )
case socketmode . EventTypeDisconnect :
log . Infof ( "slack socket mode disconnected" )
2022-01-20 16:01:22 +00:00
2022-01-16 11:06:26 +00:00
case socketmode . EventTypeEventsAPI :
eventsAPIEvent , ok := evt . Data . ( slackevents . EventsAPIEvent )
if ! ok {
2022-01-20 16:01:22 +00:00
log . Debugf ( "ignored %+v" , evt )
2022-01-16 11:06:26 +00:00
continue
}
2022-01-20 16:01:22 +00:00
log . Debugf ( "event received: %+v" , eventsAPIEvent )
2022-01-22 17:35:27 +00:00
// events api don't have response trigger, we can't set the response
2022-01-16 11:06:26 +00:00
s . socket . Ack ( * evt . Request )
2022-01-19 05:07:25 +00:00
s . EmitEventsApi ( eventsAPIEvent )
2022-01-16 11:06:26 +00:00
switch eventsAPIEvent . Type {
case slackevents . CallbackEvent :
innerEvent := eventsAPIEvent . InnerEvent
switch ev := innerEvent . Data . ( type ) {
2022-01-22 17:35:27 +00:00
case * slackevents . MessageEvent :
log . Infof ( "message event: text=%+v" , ev . Text )
if len ( ev . BotID ) > 0 {
log . Debug ( "skip bot message" )
continue
}
session := s . loadSession ( evt , ev . User , ev . Channel )
if ! session . authorizing && ! session . Authorized {
log . Warn ( "[slack] session is not authorizing nor authorized, skipping message handler" )
continue
}
if s . textMessageResponder != nil {
reply := s . newReply ( session )
if err := s . textMessageResponder ( session , ev . Text , reply ) ; err != nil {
log . WithError ( err ) . Errorf ( "[slack] response handling error" )
continue
}
// build the response
response := reply . build ( )
log . Debugln ( "response payload" , toJson ( response ) )
switch response := response . ( type ) {
case string :
_ , _ , err := s . client . PostMessage ( ev . Channel , slack . MsgOptionText ( response , false ) )
if err != nil {
log . WithError ( err ) . Error ( "failed posting plain text message" )
}
case slack . Blocks :
_ , _ , err := s . client . PostMessage ( ev . Channel , slack . MsgOptionBlocks ( response . BlockSet ... ) )
if err != nil {
log . WithError ( err ) . Error ( "failed posting blocks message" )
}
default :
log . Errorf ( "[slack] unexpected message type %T: %+v" , response , response )
}
2022-01-16 11:06:26 +00:00
}
2022-01-22 17:35:27 +00:00
case * slackevents . AppMentionEvent :
log . Infof ( "app mention event: %+v" , ev )
s . socket . Ack ( * evt . Request )
2022-01-16 11:06:26 +00:00
case * slackevents . MemberJoinedChannelEvent :
2022-01-22 17:35:27 +00:00
log . Infof ( "user %q joined to channel %q" , ev . User , ev . Channel )
s . socket . Ack ( * evt . Request )
2022-01-16 11:06:26 +00:00
}
default :
s . socket . Debugf ( "unsupported Events API event received" )
}
2022-01-22 17:35:27 +00:00
2022-01-16 11:06:26 +00:00
case socketmode . EventTypeInteractive :
callback , ok := evt . Data . ( slack . InteractionCallback )
if ! ok {
2022-01-20 16:01:22 +00:00
log . Debugf ( "ignored %+v" , evt )
2022-01-16 11:06:26 +00:00
continue
}
2022-01-20 16:01:22 +00:00
log . Debugf ( "interaction received: %+v" , callback )
2022-01-16 11:06:26 +00:00
var payload interface { }
switch callback . Type {
case slack . InteractionTypeBlockActions :
// See https://api.slack.com/apis/connections/socket-implement#button
2022-01-21 16:49:50 +00:00
log . Debugf ( "InteractionTypeBlockActions: %+v" , callback )
2022-01-16 11:06:26 +00:00
case slack . InteractionTypeShortcut :
2022-01-21 16:49:50 +00:00
log . Debugf ( "InteractionTypeShortcut: %+v" , callback )
2022-01-16 11:06:26 +00:00
case slack . InteractionTypeViewSubmission :
2022-01-22 17:35:27 +00:00
2022-01-16 11:06:26 +00:00
// See https://api.slack.com/apis/connections/socket-implement#modal
2022-01-21 16:49:50 +00:00
log . Debugf ( "[slack] InteractionTypeViewSubmission: %+v" , callback )
var values = simplifyStateValues ( callback . View . State )
if len ( values ) > 1 {
log . Warnf ( "[slack] more than 1 values received from the modal view submission, the value choosen from the state values might be incorrect" )
}
log . Debugln ( toJson ( values ) )
if inputValue , ok := takeOneValue ( values ) ; ok {
session := s . loadSession ( evt , callback . User . ID , callback . Channel . ID )
if ! session . authorizing && ! session . Authorized {
log . Warn ( "[slack] telegram is set to private mode, skipping message" )
2022-01-22 17:35:27 +00:00
continue
2022-01-21 16:49:50 +00:00
}
reply := s . newReply ( session )
if s . textMessageResponder != nil {
if err := s . textMessageResponder ( session , inputValue , reply ) ; err != nil {
log . WithError ( err ) . Errorf ( "[slack] response handling error" )
2022-01-22 17:35:27 +00:00
continue
2022-01-21 16:49:50 +00:00
}
}
// close the modal view by sending a null payload
s . socket . Ack ( * evt . Request )
// build the response
response := reply . build ( )
log . Debugln ( "response payload" , toJson ( response ) )
switch response := response . ( type ) {
2022-01-22 17:35:27 +00:00
case string :
payload = map [ string ] interface { } {
"blocks" : [ ] slack . Block {
translateMessageToBlock ( response ) ,
} ,
}
2022-01-21 16:49:50 +00:00
case slack . Blocks :
payload = map [ string ] interface { } {
2022-01-22 17:35:27 +00:00
"blocks" : response . BlockSet ,
2022-01-21 16:49:50 +00:00
}
2022-01-22 17:35:27 +00:00
default :
s . socket . Ack ( * evt . Request , response )
2022-01-21 16:49:50 +00:00
}
}
2022-01-20 18:01:41 +00:00
2022-01-16 11:06:26 +00:00
case slack . InteractionTypeDialogSubmission :
2022-01-21 16:49:50 +00:00
log . Debugf ( "[slack] InteractionTypeDialogSubmission: %+v" , callback )
2022-01-16 11:06:26 +00:00
default :
2022-01-21 16:49:50 +00:00
log . Debugf ( "[slack] unexpected callback type: %+v" , callback )
2022-01-16 11:06:26 +00:00
}
s . socket . Ack ( * evt . Request , payload )
2022-01-19 05:07:25 +00:00
2022-01-20 16:01:22 +00:00
case socketmode . EventTypeHello :
2022-01-21 16:49:50 +00:00
log . Debugf ( "[slack] hello command received: %+v" , evt )
2022-01-20 16:01:22 +00:00
2022-01-16 11:06:26 +00:00
case socketmode . EventTypeSlashCommand :
2022-01-20 16:01:22 +00:00
slashCmd , ok := evt . Data . ( slack . SlashCommand )
2022-01-16 11:06:26 +00:00
if ! ok {
2022-01-21 16:49:50 +00:00
log . Debugf ( "[slack] ignored %+v" , evt )
2022-01-16 11:06:26 +00:00
continue
}
2022-01-21 16:49:50 +00:00
log . Debugf ( "[slack] slash command received: %+v" , slashCmd )
2022-01-20 16:45:43 +00:00
responder , exists := s . commandResponders [ slashCmd . Command ]
if ! exists {
2022-01-21 16:49:50 +00:00
log . Errorf ( "[slack] command %s does not exist" , slashCmd . Command )
2022-01-20 16:45:43 +00:00
s . socket . Ack ( * evt . Request )
continue
}
2022-01-20 16:01:22 +00:00
2022-01-21 16:49:50 +00:00
session := s . loadSession ( evt , slashCmd . UserID , slashCmd . ChannelID )
2022-01-20 16:45:43 +00:00
reply := s . newReply ( session )
if err := responder ( session , slashCmd . Text , reply ) ; err != nil {
2022-01-21 16:49:50 +00:00
log . WithError ( err ) . Errorf ( "[slack] responder returns error" )
2022-01-20 16:45:43 +00:00
s . socket . Ack ( * evt . Request )
continue
}
payload := reply . build ( )
if payload == nil {
2022-01-21 16:49:50 +00:00
log . Warnf ( "[slack] reply returns nil payload" )
2022-01-20 16:45:43 +00:00
// ack with empty payload
s . socket . Ack ( * evt . Request )
continue
}
switch o := payload . ( type ) {
2022-01-22 17:35:27 +00:00
case string :
s . socket . Ack ( * evt . Request , map [ string ] interface { } {
"blocks" : [ ] slack . Block {
translateMessageToBlock ( o ) ,
} ,
} )
2022-01-20 16:45:43 +00:00
case * slack . ModalViewRequest :
2022-01-21 16:49:50 +00:00
if resp , err := s . socket . OpenView ( slashCmd . TriggerID , * o ) ; err != nil {
2022-01-23 06:21:20 +00:00
log . WithError ( err ) . Errorf ( "[slack] view open error, resp: %+v" , resp )
2022-01-20 16:01:22 +00:00
}
2022-01-20 16:45:43 +00:00
s . socket . Ack ( * evt . Request )
2022-01-22 17:35:27 +00:00
case slack . Blocks :
s . socket . Ack ( * evt . Request , map [ string ] interface { } {
"blocks" : o . BlockSet ,
} )
2022-01-20 16:45:43 +00:00
default :
s . socket . Ack ( * evt . Request , o )
2022-01-19 05:07:25 +00:00
}
2022-01-16 11:06:26 +00:00
default :
2022-01-21 16:49:50 +00:00
log . Debugf ( "[slack] unexpected event type received: %s" , evt . Type )
2022-01-16 11:06:26 +00:00
}
}
}
2022-01-21 16:49:50 +00:00
func ( s * Slack ) loadSession ( evt socketmode . Event , userID , channelID string ) * SlackSession {
2022-01-22 17:35:27 +00:00
key := userID + "-" + channelID
if session , ok := s . sessions [ key ] ; ok {
log . Infof ( "[slack] an existing session %q found, session: %+v" , key , session )
2022-01-20 16:01:22 +00:00
return session
}
2022-01-22 17:35:27 +00:00
session := NewSlackSession ( s , userID , channelID )
s . sessions [ key ] = session
log . Infof ( "[slack] allocated a new session %q, session: %+v" , key , session )
2022-01-20 16:01:22 +00:00
return session
2022-01-19 05:07:25 +00:00
}
func ( s * Slack ) newReply ( session * SlackSession ) * SlackReply {
return & SlackReply {
uuid : uuid . New ( ) . String ( ) ,
session : session ,
}
}
2022-01-16 11:06:26 +00:00
func ( s * Slack ) Start ( ctx context . Context ) {
2022-01-22 17:35:27 +00:00
go s . listen ( ctx )
2022-01-19 05:07:25 +00:00
if err := s . socket . Run ( ) ; err != nil {
2022-01-20 16:01:22 +00:00
log . WithError ( err ) . Errorf ( "slack socketmode error" )
2022-01-16 11:06:26 +00:00
}
}
2022-01-20 16:01:22 +00:00
2022-01-20 16:47:28 +00:00
// generateTextInputModalRequest generates a general slack modal view request with the given text fields
// see also https://api.slack.com/surfaces/modals/using#opening
2022-01-20 16:45:43 +00:00
func generateTextInputModalRequest ( title string , prompt string , textFields ... TextField ) * slack . ModalViewRequest {
2022-01-20 16:01:22 +00:00
// 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 {
2022-01-20 16:45:43 +00:00
labelObject := slack . NewTextBlockObject ( "plain_text" , textField . Label , false , false )
placeHolderObject := slack . NewTextBlockObject ( "plain_text" , textField . PlaceHolder , false , false )
textInputObject := slack . NewPlainTextInputBlockElement ( placeHolderObject , textField . Name )
2022-01-20 18:01:41 +00:00
2022-01-20 16:01:22 +00:00
// Notice that blockID is a unique identifier for a block
2022-01-20 18:01:41 +00:00
inputBlock := slack . NewInputBlock ( "block-" + textField . Name + "-" + uuid . NewString ( ) , labelObject , textInputObject )
2022-01-20 16:45:43 +00:00
blocks . BlockSet = append ( blocks . BlockSet , inputBlock )
2022-01-20 16:01:22 +00:00
}
var modalRequest slack . ModalViewRequest
modalRequest . Type = slack . ViewType ( "modal" )
modalRequest . Title = titleText
modalRequest . Close = closeText
modalRequest . Submit = submitText
modalRequest . Blocks = blocks
2022-01-20 16:45:43 +00:00
return & modalRequest
2022-01-20 16:01:22 +00:00
}
2022-01-20 18:01:41 +00:00
2022-01-21 16:49:50 +00:00
// simplifyStateValues simplifies the multi-layer structured values into just name=value mapping
func simplifyStateValues ( state * slack . ViewState ) map [ string ] string {
var values = make ( map [ string ] string )
if state == nil {
return values
}
for blockID , fields := range state . Values {
_ = blockID
for fieldName , fieldValues := range fields {
values [ fieldName ] = fieldValues . Value
}
}
return values
}
func takeOneValue ( values map [ string ] string ) ( string , bool ) {
for _ , v := range values {
return v , true
}
return "" , false
}
2022-01-20 18:01:41 +00:00
func toJson ( v interface { } ) string {
o , err := json . MarshalIndent ( v , "" , " " )
if err != nil {
log . WithError ( err ) . Errorf ( "json marshal error" )
return ""
}
return string ( o )
}
2022-01-22 17:35:27 +00:00
func translateMessageToBlock ( message string ) slack . Block {
return slack . NewSectionBlock (
& slack . TextBlockObject {
Type : slack . MarkdownType ,
Text : message ,
} ,
nil , // fields
nil , // accessory
// slack.SectionBlockOptionBlockID(reply.uuid),
)
}