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-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
}
2022-01-21 16:49:50 +00:00
var blocks slack . Blocks
2022-01-20 16:45:43 +00:00
if len ( reply . message ) > 0 {
2022-01-21 16:49:50 +00:00
blocks . BlockSet = append ( blocks . BlockSet , slack . NewSectionBlock (
& slack . TextBlockObject {
Type : slack . MarkdownType ,
Text : reply . message ,
} ,
nil , // fields
nil , // accessory
// slack.SectionBlockOptionBlockID(reply.uuid),
) )
return blocks
2022-01-20 16:45:43 +00:00
}
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
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-21 16:49:50 +00:00
func NewSlackSession ( userID , channelID string ) * SlackSession {
2022-01-19 05:07:25 +00:00
return & SlackSession {
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
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-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 :
// 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" )
return
}
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" )
}
}
// 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 ) {
case slack . Blocks :
payload = map [ string ] interface { } {
"response_action" : "clear" ,
// "errors": { "ticket-due-date": "You may not select a due date in the past" },
"blocks" : response . BlockSet ,
}
}
}
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 ) {
case * slack . ModalViewRequest :
2022-01-21 16:49:50 +00:00
if resp , err := s . socket . OpenView ( slashCmd . TriggerID , * o ) ; err != nil {
log . WithError ( err ) . Error ( "[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 )
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-20 16:01:22 +00:00
if session , ok := s . sessions [ userID ] ; ok {
return session
}
2022-01-21 16:49:50 +00:00
session := NewSlackSession ( userID , channelID )
2022-01-20 16:01:22 +00:00
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 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 )
}