2022-01-16 11:06:26 +00:00
package interact
import (
"context"
"fmt"
2022-01-20 16:01:22 +00:00
stdlog "log"
2022-01-16 11:06:26 +00:00
"os"
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-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-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
}
2022-01-20 16:45:43 +00:00
func ( reply * SlackReply ) InputText ( prompt string , textFields ... TextField ) {
reply . message = prompt
reply . textInputModalViewRequest = generateTextInputModalRequest ( prompt , prompt , textFields ... )
}
2022-01-19 05:07:25 +00:00
// 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 { } {
if reply . textInputModalViewRequest != nil {
return reply . textInputModalViewRequest
}
if len ( reply . message ) > 0 {
return reply . message
}
2022-01-19 05:07:25 +00:00
var blocks [ ] slack . Block
blocks = append ( blocks , slack . NewSectionBlock (
& 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 ,
} ,
) ,
)
}
blocks = append ( blocks , slack . NewActionBlock ( reply . uuid , buttons ... ) )
}
2022-01-19 05:07:25 +00:00
var payload = map [ string ] interface { } {
"blocks" : blocks ,
}
return payload
}
2022-01-16 11:06:26 +00:00
type SlackSession struct {
BaseSession
2022-01-19 05:07:25 +00:00
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 { }
}
2022-01-20 16:01:22 +00:00
func NewSlackSession ( userID string ) * SlackSession {
2022-01-19 05:07:25 +00:00
return & SlackSession {
2022-01-20 16:01:22 +00:00
UserID : userID ,
2022-01-19 05:07:25 +00:00
questions : make ( map [ string ] interface { } ) ,
}
}
func ( s * SlackSession ) ID ( ) string {
2022-01-20 16:01:22 +00:00
return s . UserID
// return fmt.Sprintf("%s-%s", s.UserID, s.ChannelID)
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 {
socket := socketmode . New (
client ,
socketmode . OptionDebug ( true ) ,
socketmode . OptionLog (
2022-01-20 16:01:22 +00:00
stdlog . New ( os . Stdout , "socketmode: " ,
stdlog . Lshortfile | stdlog . LstdFlags ) ) ,
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
}
func ( s * Slack ) listen ( ) {
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 :
fmt . Println ( "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 :
fmt . Println ( "Connection failed. Retrying later..." )
2022-01-20 16:01:22 +00:00
2022-01-16 11:06:26 +00:00
case socketmode . EventTypeConnected :
fmt . Println ( "Connected to Slack with Socket Mode." )
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-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 ) {
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 {
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-20 16:01:22 +00:00
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
2022-01-16 11:06:26 +00:00
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 )
2022-01-19 05:07:25 +00:00
2022-01-20 16:01:22 +00:00
case socketmode . EventTypeHello :
log . Debugf ( "hello command received: %+v" , evt )
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-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 ( "slash command received: %+v" , slashCmd )
2022-01-20 16:45:43 +00:00
responder , exists := s . commandResponders [ slashCmd . Command ]
if ! exists {
log . Errorf ( "command %s does not exist" , slashCmd . Command )
s . socket . Ack ( * evt . Request )
continue
}
2022-01-20 16:01:22 +00:00
2022-01-20 16:45:43 +00:00
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" )
s . socket . Ack ( * evt . Request )
continue
}
payload := reply . build ( )
if payload == nil {
log . Warnf ( "reply returns nil payload" )
// ack with empty payload
s . socket . Ack ( * evt . Request )
continue
}
switch o := payload . ( type ) {
case * slack . ModalViewRequest :
if resp , err := s . client . OpenView ( slashCmd . TriggerID , * o ) ; err != nil {
2022-01-20 16:01:22 +00:00
log . WithError ( err ) . Error ( "view open error, resp: %+v" , resp )
}
2022-01-20 16:45:43 +00:00
s . socket . Ack ( * evt . Request )
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-20 16:01:22 +00:00
log . Debugf ( "unexpected event type received: %s" , evt . Type )
2022-01-16 11:06:26 +00:00
}
}
}
2022-01-20 16:01:22 +00:00
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
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 ) {
go s . listen ( )
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 16:01:22 +00:00
// Notice that blockID is a unique identifier for a block
2022-01-20 16:45:43 +00:00
inputBlock := slack . NewInputBlock ( textField . Name , labelObject , textInputObject )
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
}