mirror of
https://github.com/42wim/matterbridge.git
synced 2024-11-21 10:12:00 -08:00
Add support new version of Slack applocation
- Added support of Event API in socketMode - Fix files upload for new applications
This commit is contained in:
parent
d16645c952
commit
5b86262fdd
@ -163,6 +163,7 @@ type Protocol struct {
|
|||||||
TeamID string // msteams
|
TeamID string // msteams
|
||||||
TenantID string // msteams
|
TenantID string // msteams
|
||||||
Token string // gitter, slack, discord, api, matrix
|
Token string // gitter, slack, discord, api, matrix
|
||||||
|
AppToken string // slack
|
||||||
Topic string // zulip
|
Topic string // zulip
|
||||||
URL string // mattermost, slack // DEPRECATED
|
URL string // mattermost, slack // DEPRECATED
|
||||||
UseAPI bool // mattermost, slack
|
UseAPI bool // mattermost, slack
|
||||||
|
@ -9,6 +9,9 @@ import (
|
|||||||
"github.com/42wim/matterbridge/bridge/config"
|
"github.com/42wim/matterbridge/bridge/config"
|
||||||
"github.com/42wim/matterbridge/bridge/helper"
|
"github.com/42wim/matterbridge/bridge/helper"
|
||||||
"github.com/slack-go/slack"
|
"github.com/slack-go/slack"
|
||||||
|
"github.com/slack-go/slack/socketmode"
|
||||||
|
"github.com/slack-go/slack/slackevents"
|
||||||
|
"encoding/json"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrEventIgnored is for events that should be ignored
|
// ErrEventIgnored is for events that should be ignored
|
||||||
@ -19,6 +22,9 @@ func (b *Bslack) handleSlack() {
|
|||||||
if b.GetString(incomingWebhookConfig) != "" && b.GetString(tokenConfig) == "" {
|
if b.GetString(incomingWebhookConfig) != "" && b.GetString(tokenConfig) == "" {
|
||||||
b.Log.Debugf("Choosing webhooks based receiving")
|
b.Log.Debugf("Choosing webhooks based receiving")
|
||||||
go b.handleMatterHook(messages)
|
go b.handleMatterHook(messages)
|
||||||
|
} else if b.GetString(appTokenConfig) != "" && b.GetString(tokenConfig) != "" {
|
||||||
|
b.Log.Debugf("Choosing socket mode based receiving")
|
||||||
|
go b.handleSlackClientSocketMode(messages)
|
||||||
} else {
|
} else {
|
||||||
b.Log.Debugf("Choosing token based receiving")
|
b.Log.Debugf("Choosing token based receiving")
|
||||||
go b.handleSlackClient(messages)
|
go b.handleSlackClient(messages)
|
||||||
@ -47,6 +53,135 @@ func (b *Bslack) handleSlack() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *Bslack) handleSlackClientSocketMode(messages chan *config.Message) {
|
||||||
|
|
||||||
|
for evt := range b.smc.Events {
|
||||||
|
switch evt.Type {
|
||||||
|
case socketmode.EventTypeConnecting:
|
||||||
|
b.Log.Debug("Connecting to Slack with Socket Mode...")
|
||||||
|
case socketmode.EventTypeConnectionError:
|
||||||
|
b.Log.Debug("Connection failed. Retrying later...")
|
||||||
|
case socketmode.EventTypeConnected:
|
||||||
|
b.Log.Debug("Connected to Slack with Socket Mode.")
|
||||||
|
if info, err := b.rtm.AuthTest(); err == nil {
|
||||||
|
b.si = &slack.Info {
|
||||||
|
User: &slack.UserDetails{
|
||||||
|
ID: info.UserID,
|
||||||
|
Name: info.User,
|
||||||
|
},
|
||||||
|
Team: &slack.Team{
|
||||||
|
ID: info.TeamID,
|
||||||
|
Name: info.Team,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
b.channels.populateChannels(true)
|
||||||
|
b.users.populateUsers(true)
|
||||||
|
} else {
|
||||||
|
b.Log.Fatalf("Get user info error %+v", err)
|
||||||
|
}
|
||||||
|
case socketmode.EventTypeEventsAPI:
|
||||||
|
eventsAPIEvent, ok := evt.Data.(slackevents.EventsAPIEvent)
|
||||||
|
if !ok {
|
||||||
|
b.Log.Printf("Ignored %+v\n", evt)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
b.smc.Ack(*evt.Request)
|
||||||
|
|
||||||
|
switch eventsAPIEvent.Type {
|
||||||
|
case slackevents.CallbackEvent:
|
||||||
|
innerEvent := eventsAPIEvent.InnerEvent
|
||||||
|
// b.Log.Debugf("Event received %+v", innerEvent)
|
||||||
|
switch ev := innerEvent.Data.(type) {
|
||||||
|
case *slackevents.MessageEvent:
|
||||||
|
// Workaround for handler compability
|
||||||
|
evString, _ := json.Marshal(ev)
|
||||||
|
b.Log.Debugf("Message event: %s", evString)
|
||||||
|
slackEvent := &slack.MessageEvent{}
|
||||||
|
if err := json.Unmarshal(evString, &slackEvent); err != nil {
|
||||||
|
b.Log.Errorf("Skipped message: %#v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.skipMessageEvent(slackEvent) {
|
||||||
|
b.Log.Debugf("Skipped message: %#v", slackEvent)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rmsg, err := b.handleMessageEvent(slackEvent)
|
||||||
|
if err != nil {
|
||||||
|
b.Log.Errorf("%#v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
messages <- rmsg
|
||||||
|
case *slackevents.FileDeletedEvent:
|
||||||
|
slackEvent := &slack.FileDeletedEvent{
|
||||||
|
Type: ev.Type,
|
||||||
|
EventTimestamp: ev.EventTimestamp,
|
||||||
|
FileID: ev.FileID,
|
||||||
|
}
|
||||||
|
rmsg, err := b.handleFileDeletedEvent(slackEvent)
|
||||||
|
if err != nil {
|
||||||
|
b.Log.Printf("%#v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
messages <- rmsg
|
||||||
|
case *slackevents.MemberJoinedChannelEvent:
|
||||||
|
b.users.populateUser(ev.User)
|
||||||
|
case *slackevents.UserProfileChangedEvent:
|
||||||
|
b.users.invalidateUser(ev.User.ID)
|
||||||
|
|
||||||
|
// TODO not implemented
|
||||||
|
// case *slack.ChannelJoinedEvent:
|
||||||
|
// // When we join a channel we update the full list of users as
|
||||||
|
// // well as the information for the channel that we joined as this
|
||||||
|
// // should now tell that we are a member of it.
|
||||||
|
// b.channels.registerChannel(ev.Channel)
|
||||||
|
|
||||||
|
case *slackevents.AppMentionEvent:
|
||||||
|
default:
|
||||||
|
b.Log.Debugf("Unhandled incoming event: %T", ev)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
b.Log.Printf("Unsupported Events API event received: %+v", eventsAPIEvent.Type)
|
||||||
|
}
|
||||||
|
case socketmode.EventTypeInteractive:
|
||||||
|
callback, ok := evt.Data.(slack.InteractionCallback)
|
||||||
|
if !ok {
|
||||||
|
b.Log.Printf("Ignored %+v\n", evt)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Log.Debugf("Interaction skipped: %+v\n", callback)
|
||||||
|
|
||||||
|
var payload interface{}
|
||||||
|
|
||||||
|
switch callback.Type {
|
||||||
|
case slack.InteractionTypeBlockActions:
|
||||||
|
case slack.InteractionTypeShortcut:
|
||||||
|
case slack.InteractionTypeViewSubmission:
|
||||||
|
case slack.InteractionTypeDialogSubmission:
|
||||||
|
default:
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
b.smc.Ack(*evt.Request, payload)
|
||||||
|
case socketmode.EventTypeSlashCommand:
|
||||||
|
cmd, ok := evt.Data.(slack.SlashCommand)
|
||||||
|
if !ok {
|
||||||
|
b.Log.Printf("Ignored %+v\n", evt)
|
||||||
|
} else {
|
||||||
|
b.Log.Debugf("Slash command skipped: %+v", cmd)
|
||||||
|
}
|
||||||
|
var payload interface{}
|
||||||
|
b.smc.Ack(*evt.Request, payload)
|
||||||
|
case "hello":
|
||||||
|
continue
|
||||||
|
default:
|
||||||
|
b.Log.Errorf("Unexpected event type received: %s\n", evt.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (b *Bslack) handleSlackClient(messages chan *config.Message) {
|
func (b *Bslack) handleSlackClient(messages chan *config.Message) {
|
||||||
for msg := range b.rtm.IncomingEvents {
|
for msg := range b.rtm.IncomingEvents {
|
||||||
if msg.Type != sUserTyping && msg.Type != sHello && msg.Type != sLatencyReport {
|
if msg.Type != sUserTyping && msg.Type != sHello && msg.Type != sLatencyReport {
|
||||||
|
@ -15,6 +15,7 @@ import (
|
|||||||
lru "github.com/hashicorp/golang-lru"
|
lru "github.com/hashicorp/golang-lru"
|
||||||
"github.com/rs/xid"
|
"github.com/rs/xid"
|
||||||
"github.com/slack-go/slack"
|
"github.com/slack-go/slack"
|
||||||
|
"github.com/slack-go/slack/socketmode"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Bslack struct {
|
type Bslack struct {
|
||||||
@ -24,6 +25,7 @@ type Bslack struct {
|
|||||||
mh *matterhook.Client
|
mh *matterhook.Client
|
||||||
sc *slack.Client
|
sc *slack.Client
|
||||||
rtm *slack.RTM
|
rtm *slack.RTM
|
||||||
|
smc *socketmode.Client
|
||||||
si *slack.Info
|
si *slack.Info
|
||||||
|
|
||||||
cache *lru.Cache
|
cache *lru.Cache
|
||||||
@ -57,6 +59,7 @@ const (
|
|||||||
cfileDownloadChannel = "file_download_channel"
|
cfileDownloadChannel = "file_download_channel"
|
||||||
|
|
||||||
tokenConfig = "Token"
|
tokenConfig = "Token"
|
||||||
|
appTokenConfig = "AppToken"
|
||||||
incomingWebhookConfig = "WebhookBindAddress"
|
incomingWebhookConfig = "WebhookBindAddress"
|
||||||
outgoingWebhookConfig = "WebhookURL"
|
outgoingWebhookConfig = "WebhookURL"
|
||||||
skipTLSConfig = "SkipTLSVerify"
|
skipTLSConfig = "SkipTLSVerify"
|
||||||
@ -109,14 +112,26 @@ func (b *Bslack) Connect() error {
|
|||||||
if token := b.GetString(tokenConfig); token != "" {
|
if token := b.GetString(tokenConfig); token != "" {
|
||||||
b.Log.Info("Connecting using token")
|
b.Log.Info("Connecting using token")
|
||||||
|
|
||||||
b.sc = slack.New(token, slack.OptionDebug(b.GetBool("Debug")))
|
appToken := b.GetString(appTokenConfig)
|
||||||
|
b.sc = slack.New(token, slack.OptionDebug(b.GetBool("Debug")), slack.OptionAppLevelToken(appToken))
|
||||||
|
|
||||||
b.channels = newChannelManager(b.Log, b.sc)
|
b.channels = newChannelManager(b.Log, b.sc)
|
||||||
b.users = newUserManager(b.Log, b.sc)
|
b.users = newUserManager(b.Log, b.sc)
|
||||||
|
|
||||||
b.rtm = b.sc.NewRTM()
|
b.rtm = b.sc.NewRTM()
|
||||||
go b.rtm.ManageConnection()
|
|
||||||
|
if appToken != "" {
|
||||||
|
b.smc = socketmode.New(
|
||||||
|
b.sc,
|
||||||
|
socketmode.OptionDebug(b.GetBool("Debug")),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
go b.rtm.ManageConnection()
|
||||||
|
}
|
||||||
go b.handleSlack()
|
go b.handleSlack()
|
||||||
|
if b.smc != nil {
|
||||||
|
go b.smc.Run()
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -457,35 +472,60 @@ func (b *Bslack) uploadFile(msg *config.Message, channelID string) (string, erro
|
|||||||
// Because the result of the UploadFile is slower than the MessageEvent from slack
|
// Because the result of the UploadFile is slower than the MessageEvent from slack
|
||||||
// we can't match on the file ID yet, so we have to match on the filename too.
|
// we can't match on the file ID yet, so we have to match on the filename too.
|
||||||
ts := time.Now()
|
ts := time.Now()
|
||||||
b.Log.Debugf("Adding file %s to cache at %s with timestamp", fi.Name, ts.String())
|
fSize := int(fi.Size)
|
||||||
|
if fSize == 0 {
|
||||||
|
fSize = len(*fi.Data)
|
||||||
|
}
|
||||||
|
b.Log.Debugf("Adding file %s to cache at %s with timestamp, size %d", fi.Name, ts.String(), fSize)
|
||||||
b.cache.Add("filename"+fi.Name, ts)
|
b.cache.Add("filename"+fi.Name, ts)
|
||||||
initialComment := fmt.Sprintf("File from %s", msg.Username)
|
initialComment := fmt.Sprintf("File from %s", msg.Username)
|
||||||
if fi.Comment != "" {
|
if fi.Comment != "" {
|
||||||
initialComment += fmt.Sprintf(" with comment: %s", fi.Comment)
|
initialComment += fmt.Sprintf(" with comment: %s", fi.Comment)
|
||||||
}
|
}
|
||||||
res, err := b.sc.UploadFile(slack.FileUploadParameters{
|
|
||||||
Reader: bytes.NewReader(*fi.Data),
|
|
||||||
Filename: fi.Name,
|
|
||||||
Channels: []string{channelID},
|
|
||||||
InitialComment: initialComment,
|
|
||||||
ThreadTimestamp: msg.ParentID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
b.Log.Errorf("uploadfile %#v", err)
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if res.ID != "" {
|
|
||||||
b.Log.Debugf("Adding file ID %s to cache with timestamp %s", res.ID, ts.String())
|
|
||||||
b.cache.Add("file"+res.ID, ts)
|
|
||||||
|
|
||||||
// search for message id by uploaded file in private/public channels, get thread timestamp from uploaded file
|
if b.smc != nil {
|
||||||
if v, ok := res.Shares.Private[channelID]; ok && len(v) > 0 {
|
res, err := b.sc.UploadFileV2(slack.UploadFileV2Parameters{
|
||||||
messageID = v[0].Ts
|
Reader: bytes.NewReader(*fi.Data),
|
||||||
|
Filename: fi.Name,
|
||||||
|
FileSize: fSize,
|
||||||
|
Channel: channelID,
|
||||||
|
InitialComment: initialComment,
|
||||||
|
ThreadTimestamp: msg.ParentID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
b.Log.Errorf("uploadfile %#v", err)
|
||||||
|
return "", err
|
||||||
}
|
}
|
||||||
if v, ok := res.Shares.Public[channelID]; ok && len(v) > 0 {
|
if res.ID != "" {
|
||||||
messageID = v[0].Ts
|
b.Log.Debugf("Adding file ID %s to cache with timestamp %s", res.ID, ts.String())
|
||||||
|
b.cache.Add("file"+res.ID, ts)
|
||||||
|
messageID = res.ID // TODO
|
||||||
|
}
|
||||||
|
} else { // Deprecated version
|
||||||
|
res, err := b.sc.UploadFile(slack.FileUploadParameters{
|
||||||
|
Reader: bytes.NewReader(*fi.Data),
|
||||||
|
Filename: fi.Name,
|
||||||
|
Channels: []string{channelID},
|
||||||
|
InitialComment: initialComment,
|
||||||
|
ThreadTimestamp: msg.ParentID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
b.Log.Errorf("uploadfile %#v", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if res.ID != "" {
|
||||||
|
b.Log.Debugf("Adding file ID %s to cache with timestamp %s", res.ID, ts.String())
|
||||||
|
b.cache.Add("file"+res.ID, ts)
|
||||||
|
// search for message id by uploaded file in private/public channels, get thread timestamp from uploaded file
|
||||||
|
if v, ok := res.Shares.Private[channelID]; ok && len(v) > 0 {
|
||||||
|
messageID = v[0].Ts
|
||||||
|
}
|
||||||
|
if v, ok := res.Shares.Public[channelID]; ok && len(v) > 0 {
|
||||||
|
messageID = v[0].Ts
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
return messageID, nil
|
return messageID, nil
|
||||||
}
|
}
|
||||||
|
@ -644,6 +644,11 @@ PreserveThreading=false
|
|||||||
#Use https://api.slack.com/custom-integrations/legacy-tokens
|
#Use https://api.slack.com/custom-integrations/legacy-tokens
|
||||||
#REQUIRED (when not using webhooks)
|
#REQUIRED (when not using webhooks)
|
||||||
Token="yourslacktoken"
|
Token="yourslacktoken"
|
||||||
|
#Socket mode token to use modern application API (Events API + socket mode)
|
||||||
|
#See https://github.com/42wim/matterbridge/issues/2159
|
||||||
|
#REQUIRED (when not using webhooks or legacy application)
|
||||||
|
#AppToken="yourslackapptoken"
|
||||||
|
|
||||||
|
|
||||||
#Extra slack specific debug info, warning this generates a lot of output.
|
#Extra slack specific debug info, warning this generates a lot of output.
|
||||||
#OPTIONAL (default false)
|
#OPTIONAL (default false)
|
||||||
|
36
vendor/github.com/slack-go/slack/slackevents/action_events.go
generated
vendored
Normal file
36
vendor/github.com/slack-go/slack/slackevents/action_events.go
generated
vendored
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package slackevents
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/slack-go/slack"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MessageActionResponse struct {
|
||||||
|
ResponseType string `json:"response_type"`
|
||||||
|
ReplaceOriginal bool `json:"replace_original"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MessageActionEntity struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MessageAction struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Actions []slack.AttachmentAction `json:"actions"`
|
||||||
|
CallbackID string `json:"callback_id"`
|
||||||
|
Team MessageActionEntity `json:"team"`
|
||||||
|
Channel MessageActionEntity `json:"channel"`
|
||||||
|
User MessageActionEntity `json:"user"`
|
||||||
|
ActionTimestamp json.Number `json:"action_ts"`
|
||||||
|
MessageTimestamp json.Number `json:"message_ts"`
|
||||||
|
AttachmentID json.Number `json:"attachment_id"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
Message slack.Message `json:"message"`
|
||||||
|
OriginalMessage slack.Message `json:"original_message"`
|
||||||
|
ResponseURL string `json:"response_url"`
|
||||||
|
TriggerID string `json:"trigger_id"`
|
||||||
|
}
|
790
vendor/github.com/slack-go/slack/slackevents/inner_events.go
generated
vendored
Normal file
790
vendor/github.com/slack-go/slack/slackevents/inner_events.go
generated
vendored
Normal file
@ -0,0 +1,790 @@
|
|||||||
|
// inner_events.go provides EventsAPI particular inner events
|
||||||
|
|
||||||
|
package slackevents
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/slack-go/slack"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EventsAPIInnerEvent the inner event of a EventsAPI event_callback Event.
|
||||||
|
type EventsAPIInnerEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Data interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppMentionEvent is an (inner) EventsAPI subscribable event.
|
||||||
|
type AppMentionEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
User string `json:"user"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
TimeStamp string `json:"ts"`
|
||||||
|
ThreadTimeStamp string `json:"thread_ts"`
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
EventTimeStamp string `json:"event_ts"`
|
||||||
|
|
||||||
|
// When Message comes from a channel that is shared between workspaces
|
||||||
|
UserTeam string `json:"user_team,omitempty"`
|
||||||
|
SourceTeam string `json:"source_team,omitempty"`
|
||||||
|
|
||||||
|
// BotID is filled out when a bot triggers the app_mention event
|
||||||
|
BotID string `json:"bot_id,omitempty"`
|
||||||
|
|
||||||
|
// When the app is mentioned in the edited message
|
||||||
|
Edited *Edited `json:"edited,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppHomeOpenedEvent Your Slack app home was opened.
|
||||||
|
type AppHomeOpenedEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
User string `json:"user"`
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
EventTimeStamp string `json:"event_ts"`
|
||||||
|
Tab string `json:"tab"`
|
||||||
|
View slack.View `json:"view"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppUninstalledEvent Your Slack app was uninstalled.
|
||||||
|
type AppUninstalledEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChannelCreatedEvent represents the Channel created event
|
||||||
|
type ChannelCreatedEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Channel ChannelCreatedInfo `json:"channel"`
|
||||||
|
EventTimestamp string `json:"event_ts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChannelDeletedEvent represents the Channel deleted event
|
||||||
|
type ChannelDeletedEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
EventTimestamp string `json:"event_ts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChannelArchiveEvent represents the Channel archive event
|
||||||
|
type ChannelArchiveEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
User string `json:"user"`
|
||||||
|
EventTimestamp string `json:"event_ts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChannelUnarchiveEvent represents the Channel unarchive event
|
||||||
|
type ChannelUnarchiveEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
User string `json:"user"`
|
||||||
|
EventTimestamp string `json:"event_ts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChannelLeftEvent represents the Channel left event
|
||||||
|
type ChannelLeftEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
EventTimestamp string `json:"event_ts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChannelRenameEvent represents the Channel rename event
|
||||||
|
type ChannelRenameEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Channel ChannelRenameInfo `json:"channel"`
|
||||||
|
EventTimestamp string `json:"event_ts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChannelIDChangedEvent represents the Channel identifier changed event
|
||||||
|
type ChannelIDChangedEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
OldChannelID string `json:"old_channel_id"`
|
||||||
|
NewChannelID string `json:"new_channel_id"`
|
||||||
|
EventTimestamp string `json:"event_ts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChannelCreatedInfo represents the information associated with the Channel created event
|
||||||
|
type ChannelCreatedInfo struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
IsChannel bool `json:"is_channel"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Created int `json:"created"`
|
||||||
|
Creator string `json:"creator"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChannelRenameInfo represents the information associated with the Channel rename event
|
||||||
|
type ChannelRenameInfo struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Created int `json:"created"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GroupDeletedEvent represents the Group deleted event
|
||||||
|
type GroupDeletedEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
EventTimestamp string `json:"event_ts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GroupArchiveEvent represents the Group archive event
|
||||||
|
type GroupArchiveEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
EventTimestamp string `json:"event_ts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GroupUnarchiveEvent represents the Group unarchive event
|
||||||
|
type GroupUnarchiveEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
EventTimestamp string `json:"event_ts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GroupLeftEvent represents the Group left event
|
||||||
|
type GroupLeftEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
EventTimestamp string `json:"event_ts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GroupRenameEvent represents the Group rename event
|
||||||
|
type GroupRenameEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Channel GroupRenameInfo `json:"channel"`
|
||||||
|
EventTimestamp string `json:"event_ts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GroupRenameInfo represents the information associated with the Group rename event
|
||||||
|
type GroupRenameInfo struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Created int `json:"created"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileChangeEvent represents the information associated with the File change
|
||||||
|
// event.
|
||||||
|
type FileChangeEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
FileID string `json:"file_id"`
|
||||||
|
File FileEventFile `json:"file"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileDeletedEvent represents the information associated with the File deleted
|
||||||
|
// event.
|
||||||
|
type FileDeletedEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
FileID string `json:"file_id"`
|
||||||
|
EventTimestamp string `json:"event_ts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileSharedEvent represents the information associated with the File shared
|
||||||
|
// event.
|
||||||
|
type FileSharedEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
ChannelID string `json:"channel_id"`
|
||||||
|
FileID string `json:"file_id"`
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
File FileEventFile `json:"file"`
|
||||||
|
EventTimestamp string `json:"event_ts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileUnsharedEvent represents the information associated with the File
|
||||||
|
// unshared event.
|
||||||
|
type FileUnsharedEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
FileID string `json:"file_id"`
|
||||||
|
File FileEventFile `json:"file"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileEventFile represents information on the specific file being shared in a
|
||||||
|
// file-related Slack event.
|
||||||
|
type FileEventFile struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GridMigrationFinishedEvent An enterprise grid migration has finished on this workspace.
|
||||||
|
type GridMigrationFinishedEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
EnterpriseID string `json:"enterprise_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GridMigrationStartedEvent An enterprise grid migration has started on this workspace.
|
||||||
|
type GridMigrationStartedEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
EnterpriseID string `json:"enterprise_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LinkSharedEvent A message was posted containing one or more links relevant to your application
|
||||||
|
type LinkSharedEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
User string `json:"user"`
|
||||||
|
TimeStamp string `json:"ts"`
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
// MessageTimeStamp can be both a numeric timestamp if the LinkSharedEvent corresponds to a sent
|
||||||
|
// message and (contrary to the field name) a uuid if the LinkSharedEvent is generated in the
|
||||||
|
// compose text area.
|
||||||
|
MessageTimeStamp string `json:"message_ts"`
|
||||||
|
ThreadTimeStamp string `json:"thread_ts"`
|
||||||
|
Links []SharedLinks `json:"links"`
|
||||||
|
EventTimestamp string `json:"event_ts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SharedLinks struct {
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MessageEvent occurs when a variety of types of messages has been posted.
|
||||||
|
// Parse ChannelType to see which
|
||||||
|
// if ChannelType = "group", this is a private channel message
|
||||||
|
// if ChannelType = "channel", this message was sent to a channel
|
||||||
|
// if ChannelType = "im", this is a private message
|
||||||
|
// if ChannelType = "mim", A message was posted in a multiparty direct message channel
|
||||||
|
// TODO: Improve this so that it is not required to manually parse ChannelType
|
||||||
|
type MessageEvent struct {
|
||||||
|
// Basic Message Event - https://api.slack.com/events/message
|
||||||
|
ClientMsgID string `json:"client_msg_id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
User string `json:"user"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
ThreadTimeStamp string `json:"thread_ts"`
|
||||||
|
TimeStamp string `json:"ts"`
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
ChannelType string `json:"channel_type"`
|
||||||
|
EventTimeStamp string `json:"event_ts"`
|
||||||
|
|
||||||
|
// When Message comes from a channel that is shared between workspaces
|
||||||
|
UserTeam string `json:"user_team,omitempty"`
|
||||||
|
SourceTeam string `json:"source_team,omitempty"`
|
||||||
|
|
||||||
|
// Edited Message
|
||||||
|
Message *MessageEvent `json:"message,omitempty"`
|
||||||
|
PreviousMessage *MessageEvent `json:"previous_message,omitempty"`
|
||||||
|
Edited *Edited `json:"edited,omitempty"`
|
||||||
|
|
||||||
|
// Deleted Message
|
||||||
|
DeletedTimeStamp string `json:"deleted_ts,omitempty"`
|
||||||
|
|
||||||
|
// Message Subtypes
|
||||||
|
SubType string `json:"subtype,omitempty"`
|
||||||
|
|
||||||
|
// bot_message (https://api.slack.com/events/message/bot_message)
|
||||||
|
BotID string `json:"bot_id,omitempty"`
|
||||||
|
Username string `json:"username,omitempty"`
|
||||||
|
Icons *Icon `json:"icons,omitempty"`
|
||||||
|
|
||||||
|
Upload bool `json:"upload"`
|
||||||
|
Files []File `json:"files"`
|
||||||
|
|
||||||
|
Blocks slack.Blocks `json:"blocks,omitempty"`
|
||||||
|
Attachments []slack.Attachment `json:"attachments,omitempty"`
|
||||||
|
|
||||||
|
// Root is the message that was broadcast to the channel when the SubType is
|
||||||
|
// thread_broadcast. If this is not a thread_broadcast message event, this
|
||||||
|
// value is nil.
|
||||||
|
Root *MessageEvent `json:"root"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MemberJoinedChannelEvent A member joined a public or private channel
|
||||||
|
type MemberJoinedChannelEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
User string `json:"user"`
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
ChannelType string `json:"channel_type"`
|
||||||
|
Team string `json:"team"`
|
||||||
|
Inviter string `json:"inviter"`
|
||||||
|
EventTimestamp string `json:"event_ts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MemberLeftChannelEvent A member left a public or private channel
|
||||||
|
type MemberLeftChannelEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
User string `json:"user"`
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
ChannelType string `json:"channel_type"`
|
||||||
|
Team string `json:"team"`
|
||||||
|
EventTimestamp string `json:"event_ts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type pinEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
User string `json:"user"`
|
||||||
|
Item Item `json:"item"`
|
||||||
|
Channel string `json:"channel_id"`
|
||||||
|
EventTimestamp string `json:"event_ts"`
|
||||||
|
HasPins bool `json:"has_pins,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type reactionEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
User string `json:"user"`
|
||||||
|
Reaction string `json:"reaction"`
|
||||||
|
ItemUser string `json:"item_user"`
|
||||||
|
Item Item `json:"item"`
|
||||||
|
EventTimestamp string `json:"event_ts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReactionAddedEvent An reaction was added to a message - https://api.slack.com/events/reaction_added
|
||||||
|
type ReactionAddedEvent reactionEvent
|
||||||
|
|
||||||
|
// ReactionRemovedEvent An reaction was removed from a message - https://api.slack.com/events/reaction_removed
|
||||||
|
type ReactionRemovedEvent reactionEvent
|
||||||
|
|
||||||
|
// PinAddedEvent An item was pinned to a channel - https://api.slack.com/events/pin_added
|
||||||
|
type PinAddedEvent pinEvent
|
||||||
|
|
||||||
|
// PinRemovedEvent An item was unpinned from a channel - https://api.slack.com/events/pin_removed
|
||||||
|
type PinRemovedEvent pinEvent
|
||||||
|
|
||||||
|
type tokens struct {
|
||||||
|
Oauth []string `json:"oauth"`
|
||||||
|
Bot []string `json:"bot"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TeamJoinEvent A new member joined a workspace - https://api.slack.com/events/team_join
|
||||||
|
type TeamJoinEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
User *slack.User `json:"user"`
|
||||||
|
EventTimestamp string `json:"event_ts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokensRevokedEvent APP's API tokens are revoked - https://api.slack.com/events/tokens_revoked
|
||||||
|
type TokensRevokedEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Tokens tokens `json:"tokens"`
|
||||||
|
EventTimestamp string `json:"event_ts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmojiChangedEvent is the event of custom emoji has been added or changed
|
||||||
|
type EmojiChangedEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Subtype string `json:"subtype"`
|
||||||
|
EventTimeStamp string `json:"event_ts"`
|
||||||
|
|
||||||
|
// filled out when custom emoji added
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
|
||||||
|
// filled out when custom emoji removed
|
||||||
|
Names []string `json:"names,omitempty"`
|
||||||
|
|
||||||
|
// filled out when custom emoji renamed
|
||||||
|
OldName string `json:"old_name,omitempty"`
|
||||||
|
NewName string `json:"new_name,omitempty"`
|
||||||
|
|
||||||
|
// filled out when custom emoji added or renamed
|
||||||
|
Value string `json:"value,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// WorkflowStepExecuteEvent is fired, if a workflow step of your app is invoked
|
||||||
|
type WorkflowStepExecuteEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
CallbackID string `json:"callback_id"`
|
||||||
|
WorkflowStep EventWorkflowStep `json:"workflow_step"`
|
||||||
|
EventTimestamp string `json:"event_ts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MessageMetadataPostedEvent is sent, if a message with metadata is posted
|
||||||
|
type MessageMetadataPostedEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
AppId string `json:"app_id"`
|
||||||
|
BotId string `json:"bot_id"`
|
||||||
|
UserId string `json:"user_id"`
|
||||||
|
TeamId string `json:"team_id"`
|
||||||
|
ChannelId string `json:"channel_id"`
|
||||||
|
Metadata *slack.SlackMetadata `json:"metadata"`
|
||||||
|
MessageTimestamp string `json:"message_ts"`
|
||||||
|
EventTimestamp string `json:"event_ts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MessageMetadataUpdatedEvent is sent, if a message with metadata is deleted
|
||||||
|
type MessageMetadataUpdatedEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
ChannelId string `json:"channel_id"`
|
||||||
|
EventTimestamp string `json:"event_ts"`
|
||||||
|
PreviousMetadata *slack.SlackMetadata `json:"previous_metadata"`
|
||||||
|
AppId string `json:"app_id"`
|
||||||
|
BotId string `json:"bot_id"`
|
||||||
|
UserId string `json:"user_id"`
|
||||||
|
TeamId string `json:"team_id"`
|
||||||
|
MessageTimestamp string `json:"message_ts"`
|
||||||
|
Metadata *slack.SlackMetadata `json:"metadata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MessageMetadataDeletedEvent is sent, if a message with metadata is deleted
|
||||||
|
type MessageMetadataDeletedEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
ChannelId string `json:"channel_id"`
|
||||||
|
EventTimestamp string `json:"event_ts"`
|
||||||
|
PreviousMetadata *slack.SlackMetadata `json:"previous_metadata"`
|
||||||
|
AppId string `json:"app_id"`
|
||||||
|
BotId string `json:"bot_id"`
|
||||||
|
UserId string `json:"user_id"`
|
||||||
|
TeamId string `json:"team_id"`
|
||||||
|
MessageTimestamp string `json:"message_ts"`
|
||||||
|
DeletedTimestamp string `json:"deleted_ts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EventWorkflowStep struct {
|
||||||
|
WorkflowStepExecuteID string `json:"workflow_step_execute_id"`
|
||||||
|
WorkflowID string `json:"workflow_id"`
|
||||||
|
WorkflowInstanceID string `json:"workflow_instance_id"`
|
||||||
|
StepID string `json:"step_id"`
|
||||||
|
Inputs *slack.WorkflowStepInputs `json:"inputs,omitempty"`
|
||||||
|
Outputs *[]slack.WorkflowStepOutput `json:"outputs,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSONTime exists so that we can have a String method converting the date
|
||||||
|
type JSONTime int64
|
||||||
|
|
||||||
|
// Comment contains all the information relative to a comment
|
||||||
|
type Comment struct {
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
Created JSONTime `json:"created,omitempty"`
|
||||||
|
Timestamp JSONTime `json:"timestamp,omitempty"`
|
||||||
|
User string `json:"user,omitempty"`
|
||||||
|
Comment string `json:"comment,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// File is a file upload
|
||||||
|
type File struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Created int `json:"created"`
|
||||||
|
Timestamp int `json:"timestamp"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Mimetype string `json:"mimetype"`
|
||||||
|
Filetype string `json:"filetype"`
|
||||||
|
PrettyType string `json:"pretty_type"`
|
||||||
|
User string `json:"user"`
|
||||||
|
Editable bool `json:"editable"`
|
||||||
|
Size int `json:"size"`
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
IsExternal bool `json:"is_external"`
|
||||||
|
ExternalType string `json:"external_type"`
|
||||||
|
IsPublic bool `json:"is_public"`
|
||||||
|
PublicURLShared bool `json:"public_url_shared"`
|
||||||
|
DisplayAsBot bool `json:"display_as_bot"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
URLPrivate string `json:"url_private"`
|
||||||
|
URLPrivateDownload string `json:"url_private_download"`
|
||||||
|
Thumb64 string `json:"thumb_64"`
|
||||||
|
Thumb80 string `json:"thumb_80"`
|
||||||
|
Thumb360 string `json:"thumb_360"`
|
||||||
|
Thumb360W int `json:"thumb_360_w"`
|
||||||
|
Thumb360H int `json:"thumb_360_h"`
|
||||||
|
Thumb480 string `json:"thumb_480"`
|
||||||
|
Thumb480W int `json:"thumb_480_w"`
|
||||||
|
Thumb480H int `json:"thumb_480_h"`
|
||||||
|
Thumb160 string `json:"thumb_160"`
|
||||||
|
Thumb720 string `json:"thumb_720"`
|
||||||
|
Thumb720W int `json:"thumb_720_w"`
|
||||||
|
Thumb720H int `json:"thumb_720_h"`
|
||||||
|
Thumb800 string `json:"thumb_800"`
|
||||||
|
Thumb800W int `json:"thumb_800_w"`
|
||||||
|
Thumb800H int `json:"thumb_800_h"`
|
||||||
|
Thumb960 string `json:"thumb_960"`
|
||||||
|
Thumb960W int `json:"thumb_960_w"`
|
||||||
|
Thumb960H int `json:"thumb_960_h"`
|
||||||
|
Thumb1024 string `json:"thumb_1024"`
|
||||||
|
Thumb1024W int `json:"thumb_1024_w"`
|
||||||
|
Thumb1024H int `json:"thumb_1024_h"`
|
||||||
|
ImageExifRotation int `json:"image_exif_rotation"`
|
||||||
|
OriginalW int `json:"original_w"`
|
||||||
|
OriginalH int `json:"original_h"`
|
||||||
|
Permalink string `json:"permalink"`
|
||||||
|
PermalinkPublic string `json:"permalink_public"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edited is included when a Message is edited
|
||||||
|
type Edited struct {
|
||||||
|
User string `json:"user"`
|
||||||
|
TimeStamp string `json:"ts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Icon is used for bot messages
|
||||||
|
type Icon struct {
|
||||||
|
IconURL string `json:"icon_url,omitempty"`
|
||||||
|
IconEmoji string `json:"icon_emoji,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Item is any type of slack message - message, file, or file comment.
|
||||||
|
type Item struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Channel string `json:"channel,omitempty"`
|
||||||
|
Message *ItemMessage `json:"message,omitempty"`
|
||||||
|
File *File `json:"file,omitempty"`
|
||||||
|
Comment *Comment `json:"comment,omitempty"`
|
||||||
|
Timestamp string `json:"ts,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ItemMessage is the event message
|
||||||
|
type ItemMessage struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
User string `json:"user"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
Timestamp string `json:"ts"`
|
||||||
|
PinnedTo []string `json:"pinned_to"`
|
||||||
|
ReplaceOriginal bool `json:"replace_original"`
|
||||||
|
DeleteOriginal bool `json:"delete_original"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEdited checks if the MessageEvent is caused by an edit
|
||||||
|
func (e MessageEvent) IsEdited() bool {
|
||||||
|
return e.Message != nil &&
|
||||||
|
e.Message.Edited != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TeamAccessGrantedEvent is sent if access to teams was granted for your org-wide app.
|
||||||
|
type TeamAccessGrantedEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
TeamIDs []string `json:"team_ids"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TeamAccessRevokedEvent is sent if access to teams was revoked for your org-wide app.
|
||||||
|
type TeamAccessRevokedEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
TeamIDs []string `json:"team_ids"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserProfileChangedEvent is sent if access to teams was revoked for your org-wide app.
|
||||||
|
type UserProfileChangedEvent struct {
|
||||||
|
User *slack.User `json:"user"`
|
||||||
|
CacheTs int `json:"cache_ts"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
EventTs string `json:"event_ts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SharedChannelInviteApprovedEvent is sent if your invitation has been approved
|
||||||
|
type SharedChannelInviteApprovedEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Invite *SharedInvite `json:"invite"`
|
||||||
|
Channel *slack.Conversation `json:"channel"`
|
||||||
|
ApprovingTeamID string `json:"approving_team_id"`
|
||||||
|
TeamsInChannel []*SlackEventTeam `json:"teams_in_channel"`
|
||||||
|
ApprovingUser *SlackEventUser `json:"approving_user"`
|
||||||
|
EventTs string `json:"event_ts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SharedChannelInviteAcceptedEvent is sent if external org accepts a Slack Connect channel invite
|
||||||
|
type SharedChannelInviteAcceptedEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
ApprovalRequired bool `json:"approval_required"`
|
||||||
|
Invite *SharedInvite `json:"invite"`
|
||||||
|
Channel *SharedChannel `json:"channel"`
|
||||||
|
TeamsInChannel []*SlackEventTeam `json:"teams_in_channel"`
|
||||||
|
AcceptingUser *SlackEventUser `json:"accepting_user"`
|
||||||
|
EventTs string `json:"event_ts"`
|
||||||
|
RequiresSponsorship bool `json:"requires_sponsorship,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SharedChannelInviteDeclinedEvent is sent if external or internal org declines the Slack Connect invite
|
||||||
|
type SharedChannelInviteDeclinedEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Invite *SharedInvite `json:"invite"`
|
||||||
|
Channel *SharedChannel `json:"channel"`
|
||||||
|
DecliningTeamID string `json:"declining_team_id"`
|
||||||
|
TeamsInChannel []*SlackEventTeam `json:"teams_in_channel"`
|
||||||
|
DecliningUser *SlackEventUser `json:"declining_user"`
|
||||||
|
EventTs string `json:"event_ts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SharedChannelInviteReceivedEvent is sent if a bot or app is invited to a Slack Connect channel
|
||||||
|
type SharedChannelInviteReceivedEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Invite *SharedInvite `json:"invite"`
|
||||||
|
Channel *SharedChannel `json:"channel"`
|
||||||
|
EventTs string `json:"event_ts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SlackEventTeam is a struct for teams in ShareChannel events
|
||||||
|
type SlackEventTeam struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Icon *SlackEventIcon `json:"icon,omitempty"`
|
||||||
|
AvatarBaseURL string `json:"avatar_base_url,omitempty"`
|
||||||
|
IsVerified bool `json:"is_verified"`
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
DateCreated int `json:"date_created"`
|
||||||
|
RequiresSponsorship bool `json:"requires_sponsorship,omitempty"`
|
||||||
|
// TeamID string `json:"team_id,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SlackEventIcon is a struct for icons in ShareChannel events
|
||||||
|
type SlackEventIcon struct {
|
||||||
|
ImageDefault bool `json:"image_default,omitempty"`
|
||||||
|
Image34 string `json:"image_34,omitempty"`
|
||||||
|
Image44 string `json:"image_44,omitempty"`
|
||||||
|
Image68 string `json:"image_68,omitempty"`
|
||||||
|
Image88 string `json:"image_88,omitempty"`
|
||||||
|
Image102 string `json:"image_102,omitempty"`
|
||||||
|
Image132 string `json:"image_132,omitempty"`
|
||||||
|
Image230 string `json:"image_230,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SlackEventUser is a struct for users in ShareChannel events
|
||||||
|
type SlackEventUser struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
TeamID string `json:"team_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Updated int `json:"updated,omitempty"`
|
||||||
|
Profile *slack.UserProfile `json:"profile,omitempty"`
|
||||||
|
WhoCanShareContactCard string `json:"who_can_share_contact_card,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SharedChannel is a struct for shared channels in ShareChannel events
|
||||||
|
type SharedChannel struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
IsPrivate bool `json:"is_private"`
|
||||||
|
IsIm bool `json:"is_im"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SharedInvite is a struct for shared invites in ShareChannel events
|
||||||
|
type SharedInvite struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
DateCreated int `json:"date_created"`
|
||||||
|
DateInvalid int `json:"date_invalid"`
|
||||||
|
InvitingTeam *SlackEventTeam `json:"inviting_team,omitempty"`
|
||||||
|
InvitingUser *SlackEventUser `json:"inviting_user,omitempty"`
|
||||||
|
RecipientEmail string `json:"recipient_email,omitempty"`
|
||||||
|
RecipientUserID string `json:"recipient_user_id,omitempty"`
|
||||||
|
IsSponsored bool `json:"is_sponsored,omitempty"`
|
||||||
|
IsExternalLimited bool `json:"is_external_limited,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EventsAPIType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// AppMention is an Events API subscribable event
|
||||||
|
AppMention = EventsAPIType("app_mention")
|
||||||
|
// AppHomeOpened Your Slack app home was opened
|
||||||
|
AppHomeOpened = EventsAPIType("app_home_opened")
|
||||||
|
// AppUninstalled Your Slack app was uninstalled.
|
||||||
|
AppUninstalled = EventsAPIType("app_uninstalled")
|
||||||
|
// ChannelCreated is sent when a new channel is created.
|
||||||
|
ChannelCreated = EventsAPIType("channel_created")
|
||||||
|
// ChannelDeleted is sent when a channel is deleted.
|
||||||
|
ChannelDeleted = EventsAPIType("channel_deleted")
|
||||||
|
// ChannelArchive is sent when a channel is archived.
|
||||||
|
ChannelArchive = EventsAPIType("channel_archive")
|
||||||
|
// ChannelUnarchive is sent when a channel is unarchived.
|
||||||
|
ChannelUnarchive = EventsAPIType("channel_unarchive")
|
||||||
|
// ChannelLeft is sent when a channel is left.
|
||||||
|
ChannelLeft = EventsAPIType("channel_left")
|
||||||
|
// ChannelRename is sent when a channel is rename.
|
||||||
|
ChannelRename = EventsAPIType("channel_rename")
|
||||||
|
// ChannelIDChanged is sent when a channel identifier is changed.
|
||||||
|
ChannelIDChanged = EventsAPIType("channel_id_changed")
|
||||||
|
// GroupDeleted is sent when a group is deleted.
|
||||||
|
GroupDeleted = EventsAPIType("group_deleted")
|
||||||
|
// GroupArchive is sent when a group is archived.
|
||||||
|
GroupArchive = EventsAPIType("group_archive")
|
||||||
|
// GroupUnarchive is sent when a group is unarchived.
|
||||||
|
GroupUnarchive = EventsAPIType("group_unarchive")
|
||||||
|
// GroupLeft is sent when a group is left.
|
||||||
|
GroupLeft = EventsAPIType("group_left")
|
||||||
|
// GroupRename is sent when a group is renamed.
|
||||||
|
GroupRename = EventsAPIType("group_rename")
|
||||||
|
// FileChange is sent when a file is changed.
|
||||||
|
FileChange = EventsAPIType("file_change")
|
||||||
|
// FileDeleted is sent when a file is deleted.
|
||||||
|
FileDeleted = EventsAPIType("file_deleted")
|
||||||
|
// FileShared is sent when a file is shared.
|
||||||
|
FileShared = EventsAPIType("file_shared")
|
||||||
|
// FileUnshared is sent when a file is unshared.
|
||||||
|
FileUnshared = EventsAPIType("file_unshared")
|
||||||
|
// GridMigrationFinished An enterprise grid migration has finished on this workspace.
|
||||||
|
GridMigrationFinished = EventsAPIType("grid_migration_finished")
|
||||||
|
// GridMigrationStarted An enterprise grid migration has started on this workspace.
|
||||||
|
GridMigrationStarted = EventsAPIType("grid_migration_started")
|
||||||
|
// LinkShared A message was posted containing one or more links relevant to your application
|
||||||
|
LinkShared = EventsAPIType("link_shared")
|
||||||
|
// Message A message was posted to a channel, private channel (group), im, or mim
|
||||||
|
Message = EventsAPIType("message")
|
||||||
|
// MemberJoinedChannel is sent if a member joined a channel.
|
||||||
|
MemberJoinedChannel = EventsAPIType("member_joined_channel")
|
||||||
|
// MemberLeftChannel is sent if a member left a channel.
|
||||||
|
MemberLeftChannel = EventsAPIType("member_left_channel")
|
||||||
|
// PinAdded An item was pinned to a channel
|
||||||
|
PinAdded = EventsAPIType("pin_added")
|
||||||
|
// PinRemoved An item was unpinned from a channel
|
||||||
|
PinRemoved = EventsAPIType("pin_removed")
|
||||||
|
// ReactionAdded An reaction was added to a message
|
||||||
|
ReactionAdded = EventsAPIType("reaction_added")
|
||||||
|
// ReactionRemoved An reaction was removed from a message
|
||||||
|
ReactionRemoved = EventsAPIType("reaction_removed")
|
||||||
|
// TeamJoin A new user joined the workspace
|
||||||
|
TeamJoin = EventsAPIType("team_join")
|
||||||
|
// Slack connect app or bot invite received
|
||||||
|
SharedChannelInviteReceived = EventsAPIType("shared_channel_invite_received")
|
||||||
|
// Slack connect channel invite approved
|
||||||
|
SharedChannelInviteApproved = EventsAPIType("shared_channel_invite_approved")
|
||||||
|
// Slack connect channel invite declined
|
||||||
|
SharedChannelInviteDeclined = EventsAPIType("shared_channel_invite_declined")
|
||||||
|
// Slack connect channel invite accepted by an end user
|
||||||
|
SharedChannelInviteAccepted = EventsAPIType("shared_channel_invite_accepted")
|
||||||
|
// TokensRevoked APP's API tokes are revoked
|
||||||
|
TokensRevoked = EventsAPIType("tokens_revoked")
|
||||||
|
// EmojiChanged A custom emoji has been added or changed
|
||||||
|
EmojiChanged = EventsAPIType("emoji_changed")
|
||||||
|
// WorkflowStepExecute Happens, if a workflow step of your app is invoked
|
||||||
|
WorkflowStepExecute = EventsAPIType("workflow_step_execute")
|
||||||
|
// MessageMetadataPosted A message with metadata was posted
|
||||||
|
MessageMetadataPosted = EventsAPIType("message_metadata_posted")
|
||||||
|
// MessageMetadataUpdated A message with metadata was updated
|
||||||
|
MessageMetadataUpdated = EventsAPIType("message_metadata_updated")
|
||||||
|
// MessageMetadataDeleted A message with metadata was deleted
|
||||||
|
MessageMetadataDeleted = EventsAPIType("message_metadata_deleted")
|
||||||
|
// TeamAccessGranted is sent if access to teams was granted for your org-wide app.
|
||||||
|
TeamAccessGranted = EventsAPIType("team_access_granted")
|
||||||
|
// TeamAccessRevoked is sent if access to teams was revoked for your org-wide app.
|
||||||
|
TeamAccessRevoked = EventsAPIType("team_access_revoked")
|
||||||
|
// UserProfileChanged is sent if a user's profile information has changed.
|
||||||
|
UserProfileChanged = EventsAPIType("user_profile_changed")
|
||||||
|
)
|
||||||
|
|
||||||
|
// EventsAPIInnerEventMapping maps INNER Event API events to their corresponding struct
|
||||||
|
// implementations. The structs should be instances of the unmarshalling
|
||||||
|
// target for the matching event type.
|
||||||
|
var EventsAPIInnerEventMapping = map[EventsAPIType]interface{}{
|
||||||
|
AppMention: AppMentionEvent{},
|
||||||
|
AppHomeOpened: AppHomeOpenedEvent{},
|
||||||
|
AppUninstalled: AppUninstalledEvent{},
|
||||||
|
ChannelCreated: ChannelCreatedEvent{},
|
||||||
|
ChannelDeleted: ChannelDeletedEvent{},
|
||||||
|
ChannelArchive: ChannelArchiveEvent{},
|
||||||
|
ChannelUnarchive: ChannelUnarchiveEvent{},
|
||||||
|
ChannelLeft: ChannelLeftEvent{},
|
||||||
|
ChannelRename: ChannelRenameEvent{},
|
||||||
|
ChannelIDChanged: ChannelIDChangedEvent{},
|
||||||
|
FileChange: FileChangeEvent{},
|
||||||
|
FileDeleted: FileDeletedEvent{},
|
||||||
|
FileShared: FileSharedEvent{},
|
||||||
|
FileUnshared: FileUnsharedEvent{},
|
||||||
|
GroupDeleted: GroupDeletedEvent{},
|
||||||
|
GroupArchive: GroupArchiveEvent{},
|
||||||
|
GroupUnarchive: GroupUnarchiveEvent{},
|
||||||
|
GroupLeft: GroupLeftEvent{},
|
||||||
|
GroupRename: GroupRenameEvent{},
|
||||||
|
GridMigrationFinished: GridMigrationFinishedEvent{},
|
||||||
|
GridMigrationStarted: GridMigrationStartedEvent{},
|
||||||
|
LinkShared: LinkSharedEvent{},
|
||||||
|
Message: MessageEvent{},
|
||||||
|
MemberJoinedChannel: MemberJoinedChannelEvent{},
|
||||||
|
MemberLeftChannel: MemberLeftChannelEvent{},
|
||||||
|
PinAdded: PinAddedEvent{},
|
||||||
|
PinRemoved: PinRemovedEvent{},
|
||||||
|
ReactionAdded: ReactionAddedEvent{},
|
||||||
|
ReactionRemoved: ReactionRemovedEvent{},
|
||||||
|
SharedChannelInviteApproved: SharedChannelInviteApprovedEvent{},
|
||||||
|
SharedChannelInviteAccepted: SharedChannelInviteAcceptedEvent{},
|
||||||
|
SharedChannelInviteDeclined: SharedChannelInviteDeclinedEvent{},
|
||||||
|
SharedChannelInviteReceived: SharedChannelInviteReceivedEvent{},
|
||||||
|
TeamJoin: TeamJoinEvent{},
|
||||||
|
TokensRevoked: TokensRevokedEvent{},
|
||||||
|
EmojiChanged: EmojiChangedEvent{},
|
||||||
|
WorkflowStepExecute: WorkflowStepExecuteEvent{},
|
||||||
|
MessageMetadataPosted: MessageMetadataPostedEvent{},
|
||||||
|
MessageMetadataUpdated: MessageMetadataUpdatedEvent{},
|
||||||
|
MessageMetadataDeleted: MessageMetadataDeletedEvent{},
|
||||||
|
TeamAccessGranted: TeamAccessGrantedEvent{},
|
||||||
|
TeamAccessRevoked: TeamAccessRevokedEvent{},
|
||||||
|
UserProfileChanged: UserProfileChangedEvent{},
|
||||||
|
}
|
72
vendor/github.com/slack-go/slack/slackevents/outer_events.go
generated
vendored
Normal file
72
vendor/github.com/slack-go/slack/slackevents/outer_events.go
generated
vendored
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
// outer_events.go provides EventsAPI particular outer events
|
||||||
|
|
||||||
|
package slackevents
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EventsAPIEvent is the base EventsAPIEvent
|
||||||
|
type EventsAPIEvent struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
TeamID string `json:"team_id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
APIAppID string `json:"api_app_id"`
|
||||||
|
EnterpriseID string `json:"enterprise_id"`
|
||||||
|
Data interface{}
|
||||||
|
InnerEvent EventsAPIInnerEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
// EventsAPIURLVerificationEvent received when configuring a EventsAPI driven app
|
||||||
|
type EventsAPIURLVerificationEvent struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
Challenge string `json:"challenge"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChallengeResponse is a response to a EventsAPIEvent URLVerification challenge
|
||||||
|
type ChallengeResponse struct {
|
||||||
|
Challenge string
|
||||||
|
}
|
||||||
|
|
||||||
|
// EventsAPICallbackEvent is the main (outer) EventsAPI event.
|
||||||
|
type EventsAPICallbackEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
TeamID string `json:"team_id"`
|
||||||
|
APIAppID string `json:"api_app_id"`
|
||||||
|
EnterpriseID string `json:"enterprise_id"`
|
||||||
|
InnerEvent *json.RawMessage `json:"event"`
|
||||||
|
AuthedUsers []string `json:"authed_users"`
|
||||||
|
AuthedTeams []string `json:"authed_teams"`
|
||||||
|
EventID string `json:"event_id"`
|
||||||
|
EventTime int `json:"event_time"`
|
||||||
|
EventContext string `json:"event_context"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// EventsAPIAppRateLimited indicates your app's event subscriptions are being rate limited
|
||||||
|
type EventsAPIAppRateLimited struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
TeamID string `json:"team_id"`
|
||||||
|
MinuteRateLimited int `json:"minute_rate_limited"`
|
||||||
|
APIAppID string `json:"api_app_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// CallbackEvent is the "outer" event of an EventsAPI event.
|
||||||
|
CallbackEvent = "event_callback"
|
||||||
|
// URLVerification is an event used when configuring your EventsAPI app
|
||||||
|
URLVerification = "url_verification"
|
||||||
|
// AppRateLimited indicates your app's event subscriptions are being rate limited
|
||||||
|
AppRateLimited = "app_rate_limited"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EventsAPIEventMap maps OUTER Event API events to their corresponding struct
|
||||||
|
// implementations. The structs should be instances of the unmarshalling
|
||||||
|
// target for the matching event type.
|
||||||
|
var EventsAPIEventMap = map[string]interface{}{
|
||||||
|
CallbackEvent: EventsAPICallbackEvent{},
|
||||||
|
URLVerification: EventsAPIURLVerificationEvent{},
|
||||||
|
AppRateLimited: EventsAPIAppRateLimited{},
|
||||||
|
}
|
258
vendor/github.com/slack-go/slack/slackevents/parsers.go
generated
vendored
Normal file
258
vendor/github.com/slack-go/slack/slackevents/parsers.go
generated
vendored
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
package slackevents
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/subtle"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/slack-go/slack"
|
||||||
|
)
|
||||||
|
|
||||||
|
// eventsMap checks both slack.EventsMapping and
|
||||||
|
// and slackevents.EventsAPIInnerEventMapping. If the event
|
||||||
|
// exists, returns the unmarshalled struct instance of
|
||||||
|
// target for the matching event type.
|
||||||
|
// TODO: Consider moving all events into its own package?
|
||||||
|
func eventsMap(t string) (interface{}, bool) {
|
||||||
|
// Must parse EventsAPI FIRST as both RTM and EventsAPI
|
||||||
|
// have a type: "Message" event.
|
||||||
|
// TODO: Handle these cases more explicitly.
|
||||||
|
v, exists := EventsAPIInnerEventMapping[EventsAPIType(t)]
|
||||||
|
if exists {
|
||||||
|
return v, exists
|
||||||
|
}
|
||||||
|
v, exists = slack.EventMapping[t]
|
||||||
|
if exists {
|
||||||
|
return v, exists
|
||||||
|
}
|
||||||
|
return v, exists
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseOuterEvent(rawE json.RawMessage) (EventsAPIEvent, error) {
|
||||||
|
e := &EventsAPIEvent{}
|
||||||
|
err := json.Unmarshal(rawE, e)
|
||||||
|
if err != nil {
|
||||||
|
return EventsAPIEvent{
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"unmarshalling_error",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
&slack.UnmarshallingErrorEvent{ErrorObj: err},
|
||||||
|
EventsAPIInnerEvent{},
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
if e.Type == CallbackEvent {
|
||||||
|
cbEvent := &EventsAPICallbackEvent{}
|
||||||
|
err = json.Unmarshal(rawE, cbEvent)
|
||||||
|
if err != nil {
|
||||||
|
return EventsAPIEvent{
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"unmarshalling_error",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
&slack.UnmarshallingErrorEvent{ErrorObj: err},
|
||||||
|
EventsAPIInnerEvent{},
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
return EventsAPIEvent{
|
||||||
|
e.Token,
|
||||||
|
e.TeamID,
|
||||||
|
e.Type,
|
||||||
|
e.APIAppID,
|
||||||
|
e.EnterpriseID,
|
||||||
|
cbEvent,
|
||||||
|
EventsAPIInnerEvent{},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
urlVE := &EventsAPIURLVerificationEvent{}
|
||||||
|
err = json.Unmarshal(rawE, urlVE)
|
||||||
|
if err != nil {
|
||||||
|
return EventsAPIEvent{
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"unmarshalling_error",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
&slack.UnmarshallingErrorEvent{ErrorObj: err},
|
||||||
|
EventsAPIInnerEvent{},
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
return EventsAPIEvent{
|
||||||
|
e.Token,
|
||||||
|
e.TeamID,
|
||||||
|
e.Type,
|
||||||
|
e.APIAppID,
|
||||||
|
e.EnterpriseID,
|
||||||
|
urlVE,
|
||||||
|
EventsAPIInnerEvent{},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseInnerEvent(e *EventsAPICallbackEvent) (EventsAPIEvent, error) {
|
||||||
|
iE := &slack.Event{}
|
||||||
|
rawInnerJSON := e.InnerEvent
|
||||||
|
err := json.Unmarshal(*rawInnerJSON, iE)
|
||||||
|
if err != nil {
|
||||||
|
return EventsAPIEvent{
|
||||||
|
e.Token,
|
||||||
|
e.TeamID,
|
||||||
|
"unmarshalling_error",
|
||||||
|
e.APIAppID,
|
||||||
|
e.EnterpriseID,
|
||||||
|
&slack.UnmarshallingErrorEvent{ErrorObj: err},
|
||||||
|
EventsAPIInnerEvent{},
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
v, exists := eventsMap(iE.Type)
|
||||||
|
if !exists {
|
||||||
|
return EventsAPIEvent{
|
||||||
|
e.Token,
|
||||||
|
e.TeamID,
|
||||||
|
iE.Type,
|
||||||
|
e.APIAppID,
|
||||||
|
e.EnterpriseID,
|
||||||
|
nil,
|
||||||
|
EventsAPIInnerEvent{},
|
||||||
|
}, fmt.Errorf("inner Event does not exist! %s", iE.Type)
|
||||||
|
}
|
||||||
|
t := reflect.TypeOf(v)
|
||||||
|
recvEvent := reflect.New(t).Interface()
|
||||||
|
err = json.Unmarshal(*rawInnerJSON, recvEvent)
|
||||||
|
if err != nil {
|
||||||
|
return EventsAPIEvent{
|
||||||
|
e.Token,
|
||||||
|
e.TeamID,
|
||||||
|
"unmarshalling_error",
|
||||||
|
e.APIAppID,
|
||||||
|
e.EnterpriseID,
|
||||||
|
&slack.UnmarshallingErrorEvent{ErrorObj: err},
|
||||||
|
EventsAPIInnerEvent{},
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
return EventsAPIEvent{
|
||||||
|
e.Token,
|
||||||
|
e.TeamID,
|
||||||
|
e.Type,
|
||||||
|
e.APIAppID,
|
||||||
|
e.EnterpriseID,
|
||||||
|
e,
|
||||||
|
EventsAPIInnerEvent{iE.Type, recvEvent},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
VerificationToken string
|
||||||
|
TokenVerified bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Option func(cfg *Config)
|
||||||
|
|
||||||
|
type verifier interface {
|
||||||
|
Verify(token string) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func OptionVerifyToken(v verifier) Option {
|
||||||
|
return func(cfg *Config) {
|
||||||
|
cfg.TokenVerified = v.Verify(cfg.VerificationToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OptionNoVerifyToken skips the check of the Slack verification token
|
||||||
|
func OptionNoVerifyToken() Option {
|
||||||
|
return func(cfg *Config) {
|
||||||
|
cfg.TokenVerified = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type TokenComparator struct {
|
||||||
|
VerificationToken string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c TokenComparator) Verify(t string) bool {
|
||||||
|
return subtle.ConstantTimeCompare([]byte(c.VerificationToken), []byte(t)) == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseEvent parses the outer and inner events (if applicable) of an events
|
||||||
|
// api event returning a EventsAPIEvent type. If the event is a url_verification event,
|
||||||
|
// the inner event is empty.
|
||||||
|
func ParseEvent(rawEvent json.RawMessage, opts ...Option) (EventsAPIEvent, error) {
|
||||||
|
e, err := parseOuterEvent(rawEvent)
|
||||||
|
if err != nil {
|
||||||
|
return EventsAPIEvent{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &Config{}
|
||||||
|
cfg.VerificationToken = e.Token
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cfg.TokenVerified {
|
||||||
|
return EventsAPIEvent{}, errors.New("invalid verification token")
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Type == CallbackEvent {
|
||||||
|
cbEvent := e.Data.(*EventsAPICallbackEvent)
|
||||||
|
innerEvent, err := parseInnerEvent(cbEvent)
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf("EventsAPI Error parsing inner event: %s, %s", innerEvent.Type, err)
|
||||||
|
return EventsAPIEvent{
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"unmarshalling_error",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
&slack.UnmarshallingErrorEvent{ErrorObj: err},
|
||||||
|
EventsAPIInnerEvent{},
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
return innerEvent, nil
|
||||||
|
}
|
||||||
|
urlVerificationEvent := &EventsAPIURLVerificationEvent{}
|
||||||
|
err = json.Unmarshal(rawEvent, urlVerificationEvent)
|
||||||
|
if err != nil {
|
||||||
|
return EventsAPIEvent{
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"unmarshalling_error",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
&slack.UnmarshallingErrorEvent{ErrorObj: err},
|
||||||
|
EventsAPIInnerEvent{},
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
return EventsAPIEvent{
|
||||||
|
e.Token,
|
||||||
|
e.TeamID,
|
||||||
|
e.Type,
|
||||||
|
e.APIAppID,
|
||||||
|
e.EnterpriseID,
|
||||||
|
urlVerificationEvent,
|
||||||
|
EventsAPIInnerEvent{},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseActionEvent(payloadString string, opts ...Option) (MessageAction, error) {
|
||||||
|
byteString := []byte(payloadString)
|
||||||
|
action := MessageAction{}
|
||||||
|
err := json.Unmarshal(byteString, &action)
|
||||||
|
if err != nil {
|
||||||
|
return MessageAction{}, errors.New("MessageAction unmarshalling failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &Config{}
|
||||||
|
cfg.VerificationToken = action.Token
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cfg.TokenVerified {
|
||||||
|
return MessageAction{}, errors.New("invalid verification token")
|
||||||
|
} else {
|
||||||
|
return action, nil
|
||||||
|
}
|
||||||
|
}
|
63
vendor/github.com/slack-go/slack/socketmode/client.go
generated
vendored
Normal file
63
vendor/github.com/slack-go/slack/socketmode/client.go
generated
vendored
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
package socketmode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/slack-go/slack"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConnectedEvent struct {
|
||||||
|
ConnectionCount int // 1 = first time, 2 = second time
|
||||||
|
Info *slack.SocketModeConnection
|
||||||
|
}
|
||||||
|
|
||||||
|
type DebugInfo struct {
|
||||||
|
// Host is the name of the host name on the Slack end, that can be something like `applink-7fc4fdbb64-4x5xq`
|
||||||
|
Host string `json:"host"`
|
||||||
|
|
||||||
|
// `hello` type only
|
||||||
|
BuildNumber int `json:"build_number"`
|
||||||
|
ApproximateConnectionTime int `json:"approximate_connection_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConnectionInfo struct {
|
||||||
|
AppID string `json:"app_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SocketModeMessagePayload struct {
|
||||||
|
Event json.RawMessage `json:"event"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client is a Socket Mode client that allows programs to use [Events API](https://api.slack.com/events-api)
|
||||||
|
// and [interactive components](https://api.slack.com/interactivity) over WebSocket.
|
||||||
|
// Please see [Intro to Socket Mode](https://api.slack.com/apis/connections/socket) for more information
|
||||||
|
// on Socket Mode.
|
||||||
|
//
|
||||||
|
// The implementation is highly inspired by https://www.npmjs.com/package/@slack/socket-mode,
|
||||||
|
// but the structure and the design has been adapted as much as possible to that of our RTM client for consistency
|
||||||
|
// within the library.
|
||||||
|
//
|
||||||
|
// You can instantiate the socket mode client with
|
||||||
|
// Client's New() and call Run() to start it. Please see examples/socketmode for the usage.
|
||||||
|
type Client struct {
|
||||||
|
// Client is the main API, embedded
|
||||||
|
slack.Client
|
||||||
|
|
||||||
|
// maxPingInterval is the maximum duration elapsed after the last WebSocket PING sent from Slack
|
||||||
|
// until Client considers the WebSocket connection is dead and needs to be reopened.
|
||||||
|
maxPingInterval time.Duration
|
||||||
|
|
||||||
|
// Connection life-cycle
|
||||||
|
Events chan Event
|
||||||
|
socketModeResponses chan *Response
|
||||||
|
|
||||||
|
// dialer is a gorilla/websocket Dialer. If nil, use the default
|
||||||
|
// Dialer.
|
||||||
|
dialer *websocket.Dialer
|
||||||
|
|
||||||
|
debug bool
|
||||||
|
log ilogger
|
||||||
|
}
|
30
vendor/github.com/slack-go/slack/socketmode/event.go
generated
vendored
Normal file
30
vendor/github.com/slack-go/slack/socketmode/event.go
generated
vendored
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package socketmode
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
|
// Event is the event sent to the consumer of Client
|
||||||
|
type Event struct {
|
||||||
|
Type EventType
|
||||||
|
Data interface{}
|
||||||
|
|
||||||
|
// Request is the json-decoded raw WebSocket message that is received via the Slack Socket Mode
|
||||||
|
// WebSocket connection.
|
||||||
|
Request *Request
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrorBadMessage struct {
|
||||||
|
Cause error
|
||||||
|
Message json.RawMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrorWriteFailed struct {
|
||||||
|
Cause error
|
||||||
|
Response *Response
|
||||||
|
}
|
||||||
|
|
||||||
|
type errorRequestedDisconnect struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e errorRequestedDisconnect) Error() string {
|
||||||
|
return "disconnection requested: Slack requested us to disconnect"
|
||||||
|
}
|
51
vendor/github.com/slack-go/slack/socketmode/log.go
generated
vendored
Normal file
51
vendor/github.com/slack-go/slack/socketmode/log.go
generated
vendored
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package socketmode
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// TODO merge logger, ilogger, and internalLogger with the top-level package's equivalents
|
||||||
|
|
||||||
|
// logger is a logger interface compatible with both stdlib and some
|
||||||
|
// 3rd party loggers.
|
||||||
|
type logger interface {
|
||||||
|
Output(int, string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// ilogger represents the internal logging api we use.
|
||||||
|
type ilogger interface {
|
||||||
|
logger
|
||||||
|
Print(...interface{})
|
||||||
|
Printf(string, ...interface{})
|
||||||
|
Println(...interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// internalLog implements the additional methods used by our internal logging.
|
||||||
|
type internalLog struct {
|
||||||
|
logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// Println replicates the behaviour of the standard logger.
|
||||||
|
func (t internalLog) Println(v ...interface{}) {
|
||||||
|
t.Output(2, fmt.Sprintln(v...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Printf replicates the behaviour of the standard logger.
|
||||||
|
func (t internalLog) Printf(format string, v ...interface{}) {
|
||||||
|
t.Output(2, fmt.Sprintf(format, v...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print replicates the behaviour of the standard logger.
|
||||||
|
func (t internalLog) Print(v ...interface{}) {
|
||||||
|
t.Output(2, fmt.Sprint(v...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (smc *Client) Debugf(format string, v ...interface{}) {
|
||||||
|
if smc.debug {
|
||||||
|
smc.log.Output(2, fmt.Sprintf(format, v...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (smc *Client) Debugln(v ...interface{}) {
|
||||||
|
if smc.debug {
|
||||||
|
smc.log.Output(2, fmt.Sprintln(v...))
|
||||||
|
}
|
||||||
|
}
|
38
vendor/github.com/slack-go/slack/socketmode/request.go
generated
vendored
Normal file
38
vendor/github.com/slack-go/slack/socketmode/request.go
generated
vendored
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package socketmode
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
|
// Request maps to the content of each WebSocket message received via a Socket Mode WebSocket connection
|
||||||
|
//
|
||||||
|
// We call this a "request" rather than e.g. a WebSocket message or an Socket Mode "event" following python-slack-sdk:
|
||||||
|
//
|
||||||
|
// https://github.com/slackapi/python-slack-sdk/blob/3f1c4c6e27bf7ee8af57699b2543e6eb7848bcf9/slack_sdk/socket_mode/request.py#L6
|
||||||
|
//
|
||||||
|
// We know that node-slack-sdk calls it an "event", that makes it hard for us to distinguish our client's own event
|
||||||
|
// that wraps both internal events and Socket Mode "events", vs node-slack-sdk's is for the latter only.
|
||||||
|
//
|
||||||
|
// https://github.com/slackapi/node-slack-sdk/blob/main/packages/socket-mode/src/SocketModeClient.ts#L537
|
||||||
|
type Request struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
|
||||||
|
// `hello` type only
|
||||||
|
NumConnections int `json:"num_connections"`
|
||||||
|
ConnectionInfo ConnectionInfo `json:"connection_info"`
|
||||||
|
|
||||||
|
// `disconnect` type only
|
||||||
|
|
||||||
|
// Reason can be "warning" or else
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
|
||||||
|
// `hello` and `disconnect` types only
|
||||||
|
DebugInfo DebugInfo `json:"debug_info"`
|
||||||
|
|
||||||
|
// `events_api` type only
|
||||||
|
EnvelopeID string `json:"envelope_id"`
|
||||||
|
// TODO Can it really be a non-object type?
|
||||||
|
// See https://github.com/slackapi/python-slack-sdk/blob/3f1c4c6e27bf7ee8af57699b2543e6eb7848bcf9/slack_sdk/socket_mode/request.py#L26-L31
|
||||||
|
Payload json.RawMessage `json:"payload"`
|
||||||
|
AcceptsResponsePayload bool `json:"accepts_response_payload"`
|
||||||
|
RetryAttempt int `json:"retry_attempt"`
|
||||||
|
RetryReason string `json:"retry_reason"`
|
||||||
|
}
|
6
vendor/github.com/slack-go/slack/socketmode/response.go
generated
vendored
Normal file
6
vendor/github.com/slack-go/slack/socketmode/response.go
generated
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package socketmode
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
EnvelopeID string `json:"envelope_id"`
|
||||||
|
Payload interface{} `json:"payload,omitempty"`
|
||||||
|
}
|
633
vendor/github.com/slack-go/slack/socketmode/socket_mode_managed_conn.go
generated
vendored
Normal file
633
vendor/github.com/slack-go/slack/socketmode/socket_mode_managed_conn.go
generated
vendored
Normal file
@ -0,0 +1,633 @@
|
|||||||
|
package socketmode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
|
||||||
|
"github.com/slack-go/slack"
|
||||||
|
"github.com/slack-go/slack/internal/backoff"
|
||||||
|
"github.com/slack-go/slack/internal/timex"
|
||||||
|
"github.com/slack-go/slack/slackevents"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Run is a blocking function that connects the Slack Socket Mode API and handles all incoming
|
||||||
|
// requests and outgoing responses.
|
||||||
|
//
|
||||||
|
// The consumer of the Client and this function should read the Client.Events channel to receive
|
||||||
|
// `socketmode.Event`s that includes the client-specific events that may or may not wrap Socket Mode requests.
|
||||||
|
//
|
||||||
|
// Note that this function automatically reconnect on requested by Slack through a `disconnect` message.
|
||||||
|
// This function exists with an error only when a reconnection is failued due to some reason.
|
||||||
|
// If you want to retry even on reconnection failure, you'd need to write your own wrapper for this function
|
||||||
|
// to do so.
|
||||||
|
func (smc *Client) Run() error {
|
||||||
|
return smc.RunContext(context.TODO())
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunContext is a blocking function that connects the Slack Socket Mode API and handles all incoming
|
||||||
|
// requests and outgoing responses.
|
||||||
|
//
|
||||||
|
// The consumer of the Client and this function should read the Client.Events channel to receive
|
||||||
|
// `socketmode.Event`s that includes the client-specific events that may or may not wrap Socket Mode requests.
|
||||||
|
//
|
||||||
|
// Note that this function automatically reconnect on requested by Slack through a `disconnect` message.
|
||||||
|
// This function exists with an error only when a reconnection is failued due to some reason.
|
||||||
|
// If you want to retry even on reconnection failure, you'd need to write your own wrapper for this function
|
||||||
|
// to do so.
|
||||||
|
func (smc *Client) RunContext(ctx context.Context) error {
|
||||||
|
for connectionCount := 0; ; connectionCount++ {
|
||||||
|
if err := smc.run(ctx, connectionCount); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue and run the loop again to reconnect
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (smc *Client) run(ctx context.Context, connectionCount int) error {
|
||||||
|
messages := make(chan json.RawMessage, 1)
|
||||||
|
|
||||||
|
pingChan := make(chan time.Time, 1)
|
||||||
|
pingHandler := func(_ string) error {
|
||||||
|
select {
|
||||||
|
case pingChan <- time.Now():
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start trying to connect
|
||||||
|
// the returned err is already passed onto the Events channel
|
||||||
|
//
|
||||||
|
// We also configures an additional ping handler for the deadmanTimer that triggers a timeout when
|
||||||
|
// Slack did not send us WebSocket PING for more than Client.maxPingInterval.
|
||||||
|
// We can use `<-smc.pingTimeout.C` to wait for the timeout.
|
||||||
|
info, conn, err := smc.connect(ctx, connectionCount, pingHandler)
|
||||||
|
if err != nil {
|
||||||
|
// when the connection is unsuccessful its fatal, and we need to bail out.
|
||||||
|
smc.Debugf("Failed to connect with Socket Mode on try %d: %s", connectionCount, err)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
smc.sendEvent(ctx, newEvent(EventTypeConnected, &ConnectedEvent{
|
||||||
|
ConnectionCount: connectionCount,
|
||||||
|
Info: info,
|
||||||
|
}))
|
||||||
|
|
||||||
|
smc.Debugf("WebSocket connection succeeded on try %d", connectionCount)
|
||||||
|
|
||||||
|
// We're now connected so we can set up listeners
|
||||||
|
|
||||||
|
wg := new(sync.WaitGroup)
|
||||||
|
// sendErr relies on the buffer of 1 here
|
||||||
|
errc := make(chan error, 1)
|
||||||
|
sendErr := func(err error) {
|
||||||
|
select {
|
||||||
|
case errc <- err:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// The response sender sends Socket Mode responses over the WebSocket conn
|
||||||
|
if err := smc.runResponseSender(ctx, conn); err != nil {
|
||||||
|
sendErr(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// The handler reads Socket Mode requests, and enqueues responses for sending by the response sender
|
||||||
|
if err := smc.runRequestHandler(ctx, messages); err != nil {
|
||||||
|
sendErr(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer cancel()
|
||||||
|
// We close messages here as it is the producer for the channel.
|
||||||
|
defer close(messages)
|
||||||
|
|
||||||
|
// The receiver reads WebSocket messages, and enqueues parsed Socket Mode requests to be handled by
|
||||||
|
// the request handler
|
||||||
|
if err := smc.runMessageReceiver(ctx, conn, messages); err != nil {
|
||||||
|
sendErr(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func(pingInterval time.Duration) {
|
||||||
|
defer wg.Done()
|
||||||
|
defer func() {
|
||||||
|
// Detect when the connection is dead and try close connection.
|
||||||
|
if err := conn.Close(); err != nil {
|
||||||
|
smc.Debugf("Failed to close connection: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
done := ctx.Done()
|
||||||
|
var lastPing time.Time
|
||||||
|
|
||||||
|
// More efficient than constantly resetting a timer w/ Stop+Reset
|
||||||
|
ticker := time.NewTicker(pingInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
return
|
||||||
|
|
||||||
|
case lastPing = <-pingChan:
|
||||||
|
// This case gets the time of the last ping.
|
||||||
|
// If this case never fires then the pingHandler was never called
|
||||||
|
// in which case lastPing is the zero time.Time value, and will 'fail'
|
||||||
|
// the next tick, causing us to exit.
|
||||||
|
|
||||||
|
case now := <-ticker.C:
|
||||||
|
// Our last ping is older than our interval
|
||||||
|
if now.Sub(lastPing) > pingInterval {
|
||||||
|
sendErr(errors.New("ping timeout: Slack did not send us WebSocket PING for more than Client.maxInterval"))
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(smc.maxPingInterval)
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err = <-errc:
|
||||||
|
// Get buffered error
|
||||||
|
default:
|
||||||
|
// Or nothing if they all exited nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, context.Canceled) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// wg.Wait() finishes only after any of the above go routines finishes and cancels the
|
||||||
|
// context, allowing the other threads to shut down gracefully.
|
||||||
|
// Also, we can expect our (first)err to be not nil, as goroutines can finish only on error.
|
||||||
|
smc.Debugf("Reconnecting due to %v", err)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// connect attempts to connect to the slack websocket API. It handles any
|
||||||
|
// errors that occur while connecting and will return once a connection
|
||||||
|
// has been successfully opened.
|
||||||
|
func (smc *Client) connect(ctx context.Context, connectionCount int, additionalPingHandler func(string) error) (*slack.SocketModeConnection, *websocket.Conn, error) {
|
||||||
|
const (
|
||||||
|
errInvalidAuth = "invalid_auth"
|
||||||
|
errInactiveAccount = "account_inactive"
|
||||||
|
errMissingAuthToken = "not_authed"
|
||||||
|
errTokenRevoked = "token_revoked"
|
||||||
|
)
|
||||||
|
|
||||||
|
// used to provide exponential backoff wait time with jitter before trying
|
||||||
|
// to connect to slack again
|
||||||
|
boff := &backoff.Backoff{
|
||||||
|
Max: 5 * time.Minute,
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
var (
|
||||||
|
backoff time.Duration
|
||||||
|
)
|
||||||
|
|
||||||
|
// send connecting event
|
||||||
|
smc.sendEvent(ctx, newEvent(EventTypeConnecting, &slack.ConnectingEvent{
|
||||||
|
Attempt: boff.Attempts() + 1,
|
||||||
|
ConnectionCount: connectionCount,
|
||||||
|
}))
|
||||||
|
|
||||||
|
// attempt to start the connection
|
||||||
|
info, conn, err := smc.openAndDial(ctx, additionalPingHandler)
|
||||||
|
if err == nil {
|
||||||
|
return info, conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for fatal errors
|
||||||
|
switch err.Error() {
|
||||||
|
case errInvalidAuth, errInactiveAccount, errMissingAuthToken, errTokenRevoked:
|
||||||
|
smc.Debugf("invalid auth when connecting with SocketMode: %s", err)
|
||||||
|
return nil, nil, err
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
actual slack.StatusCodeError
|
||||||
|
rlError *slack.RateLimitedError
|
||||||
|
)
|
||||||
|
|
||||||
|
if errors.As(err, &actual) && actual.Code == http.StatusNotFound {
|
||||||
|
smc.Debugf("invalid auth when connecting with Socket Mode: %s", err)
|
||||||
|
smc.sendEvent(ctx, newEvent(EventTypeInvalidAuth, &slack.InvalidAuthEvent{}))
|
||||||
|
|
||||||
|
return nil, nil, err
|
||||||
|
} else if errors.As(err, &rlError) {
|
||||||
|
backoff = rlError.RetryAfter
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we check for errors.Is(err, context.Canceled) here and
|
||||||
|
// return early then we don't send the Event below that some users
|
||||||
|
// may already rely on; ie a behavior change.
|
||||||
|
|
||||||
|
backoff = timex.Max(backoff, boff.Duration())
|
||||||
|
// any other errors are treated as recoverable and we try again after
|
||||||
|
// sending the event along the Events channel
|
||||||
|
smc.sendEvent(ctx, newEvent(EventTypeConnectionError, &slack.ConnectionErrorEvent{
|
||||||
|
Attempt: boff.Attempts(),
|
||||||
|
Backoff: backoff,
|
||||||
|
ErrorObj: err,
|
||||||
|
}))
|
||||||
|
|
||||||
|
// get time we should wait before attempting to connect again
|
||||||
|
smc.Debugf("reconnection %d failed: %s reconnecting in %v\n", boff.Attempts(), err, backoff)
|
||||||
|
|
||||||
|
// wait for one of the following to occur,
|
||||||
|
// backoff duration has elapsed, disconnectCh is signalled, or
|
||||||
|
// the smc finishes disconnecting.
|
||||||
|
timer := time.NewTimer(backoff)
|
||||||
|
select {
|
||||||
|
case <-timer.C: // retry after the backoff.
|
||||||
|
case <-ctx.Done():
|
||||||
|
timer.Stop()
|
||||||
|
return nil, nil, ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// openAndDial attempts to open a Socket Mode connection and dial to the connection endpoint using WebSocket.
|
||||||
|
// It returns the full information returned by the "apps.connections.open" method on the
|
||||||
|
// Slack API.
|
||||||
|
func (smc *Client) openAndDial(ctx context.Context, additionalPingHandler func(string) error) (info *slack.SocketModeConnection, _ *websocket.Conn, err error) {
|
||||||
|
var (
|
||||||
|
url string
|
||||||
|
)
|
||||||
|
|
||||||
|
smc.Debugf("Starting SocketMode")
|
||||||
|
info, url, err = smc.OpenContext(ctx)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
smc.Debugf("Failed to start or connect with SocketMode: %s", err)
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
smc.Debugf("Dialing to websocket on url %s", url)
|
||||||
|
// Only use HTTPS for connections to prevent MITM attacks on the connection.
|
||||||
|
upgradeHeader := http.Header{}
|
||||||
|
upgradeHeader.Add("Origin", "https://api.slack.com")
|
||||||
|
dialer := websocket.DefaultDialer
|
||||||
|
if smc.dialer != nil {
|
||||||
|
dialer = smc.dialer
|
||||||
|
}
|
||||||
|
conn, _, err := dialer.DialContext(ctx, url, upgradeHeader)
|
||||||
|
if err != nil {
|
||||||
|
smc.Debugf("Failed to dial to the websocket: %s", err)
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if additionalPingHandler == nil {
|
||||||
|
additionalPingHandler = func(_ string) error { return nil }
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.SetPingHandler(func(appData string) error {
|
||||||
|
if err := additionalPingHandler(appData); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
smc.handlePing(conn, appData)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// We don't need to conn.SetCloseHandler because the default handler is effective enough that
|
||||||
|
// it sends back the CLOSE message to the server and let conn.ReadJSON() fail with CloseError.
|
||||||
|
// The CloseError must be handled normally in our receiveMessagesInto function.
|
||||||
|
//conn.SetCloseHandler(func(code int, text string) error {
|
||||||
|
// ...
|
||||||
|
// })
|
||||||
|
|
||||||
|
return info, conn, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// runResponseSender runs the handler that reads Socket Mode responses enqueued onto Client.socketModeResponses channel
|
||||||
|
// and sends them one by one over the WebSocket connection.
|
||||||
|
// Gorilla WebSocket is not goroutine safe hence this needs to be the single place you write to the WebSocket connection.
|
||||||
|
func (smc *Client) runResponseSender(ctx context.Context, conn *websocket.Conn) error {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
// 3. listen for messages that need to be sent
|
||||||
|
case res := <-smc.socketModeResponses:
|
||||||
|
smc.Debugf("Sending Socket Mode response with envelope ID %q: %v", res.EnvelopeID, res)
|
||||||
|
|
||||||
|
if err := unsafeWriteSocketModeResponse(conn, res); err != nil {
|
||||||
|
smc.sendEvent(ctx, newEvent(EventTypeErrorWriteFailed, &ErrorWriteFailed{
|
||||||
|
Cause: err,
|
||||||
|
Response: res,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
smc.Debugf("Finished sending Socket Mode response with envelope ID %q", res.EnvelopeID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// runRequestHandler is a blocking function that runs the Socket Mode request receiver.
|
||||||
|
//
|
||||||
|
// It reads WebSocket messages sent from Slack's Socket Mode WebSocket connection,
|
||||||
|
// parses them as Socket Mode requests, and processes them and optionally emit our own events into Client.Events channel.
|
||||||
|
func (smc *Client) runRequestHandler(ctx context.Context, websocket chan json.RawMessage) error {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case message, ok := <-websocket:
|
||||||
|
if !ok {
|
||||||
|
// The producer closed the channel because it encountered an error (or panic),
|
||||||
|
// we need only return.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
smc.Debugf("Received WebSocket message: %s", message)
|
||||||
|
|
||||||
|
// listen for incoming messages that need to be parsed
|
||||||
|
evt, err := smc.parseEvent(message)
|
||||||
|
if err != nil {
|
||||||
|
smc.sendEvent(ctx, newEvent(EventTypeErrorBadMessage, &ErrorBadMessage{
|
||||||
|
Cause: err,
|
||||||
|
Message: message,
|
||||||
|
}))
|
||||||
|
} else if evt != nil {
|
||||||
|
if evt.Type == EventTypeDisconnect {
|
||||||
|
// We treat the `disconnect` request from Slack as an error internally,
|
||||||
|
// so that we can tell the consumer of this function to reopen the connection on it.
|
||||||
|
return errorRequestedDisconnect{}
|
||||||
|
}
|
||||||
|
|
||||||
|
smc.sendEvent(ctx, *evt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// runMessageReceiver monitors the Socket Mode opened WebSocket connection for any incoming
|
||||||
|
// messages. It pushes the raw events into the channel.
|
||||||
|
// The receiver runs until the context is closed.
|
||||||
|
func (smc *Client) runMessageReceiver(ctx context.Context, conn *websocket.Conn, sink chan json.RawMessage) error {
|
||||||
|
for {
|
||||||
|
if err := smc.receiveMessagesInto(ctx, conn, sink); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// unsafeWriteSocketModeResponse sends a WebSocket message back to Slack.
|
||||||
|
// WARNING: Call to this function must be serialized!
|
||||||
|
//
|
||||||
|
// Here's why - Gorilla WebSocket's Writes functions are not concurrency-safe.
|
||||||
|
// That is, we must serialize all the writes to it with e.g. a goroutine or mutex.
|
||||||
|
// We intentionally chose to use goroutine, which makes it harder to propagate write errors to the caller,
|
||||||
|
// but is more computationally efficient.
|
||||||
|
//
|
||||||
|
// See the below for more information on this topic:
|
||||||
|
// https://stackoverflow.com/questions/43225340/how-to-ensure-concurrency-in-golang-gorilla-websocket-package
|
||||||
|
func unsafeWriteSocketModeResponse(conn *websocket.Conn, res *Response) error {
|
||||||
|
// set a write deadline on the connection
|
||||||
|
if err := conn.SetWriteDeadline(time.Now().Add(10 * time.Second)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove write deadline regardless of WriteJSON succeeds or not
|
||||||
|
defer conn.SetWriteDeadline(time.Time{})
|
||||||
|
|
||||||
|
return conn.WriteJSON(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newEvent(tpe EventType, data interface{}, req ...*Request) Event {
|
||||||
|
evt := Event{Type: tpe, Data: data}
|
||||||
|
|
||||||
|
if len(req) > 0 {
|
||||||
|
evt.Request = req[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return evt
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ack acknowledges the Socket Mode request with the payload.
|
||||||
|
//
|
||||||
|
// This tells Slack that the we have received the request denoted by the envelope ID,
|
||||||
|
// by sending back the envelope ID over the WebSocket connection.
|
||||||
|
func (smc *Client) Ack(req Request, payload ...interface{}) {
|
||||||
|
var pld interface{}
|
||||||
|
if len(payload) > 0 {
|
||||||
|
pld = payload[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
smc.AckCtx(context.TODO(), req.EnvelopeID, pld)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AckCtx acknowledges the Socket Mode request envelope ID with the payload.
|
||||||
|
//
|
||||||
|
// This tells Slack that the we have received the request denoted by the request (envelope) ID,
|
||||||
|
// by sending back the ID over the WebSocket connection.
|
||||||
|
func (smc *Client) AckCtx(ctx context.Context, reqID string, payload interface{}) error {
|
||||||
|
return smc.SendCtx(ctx, Response{
|
||||||
|
EnvelopeID: reqID,
|
||||||
|
Payload: payload,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send sends the Socket Mode response over a WebSocket connection.
|
||||||
|
// This is usually used for acknowledging requests, but if you need more control over Client.Ack().
|
||||||
|
// It's normally recommended to use Client.Ack() instead of this.
|
||||||
|
func (smc *Client) Send(res Response) {
|
||||||
|
smc.SendCtx(context.TODO(), res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendCtx sends the Socket Mode response over a WebSocket connection.
|
||||||
|
// This is usually used for acknowledging requests, but if you need more control
|
||||||
|
// it's normally recommended to use Client.AckCtx() instead of this.
|
||||||
|
func (smc *Client) SendCtx(ctx context.Context, res Response) error {
|
||||||
|
if smc.debug {
|
||||||
|
js, err := json.Marshal(res)
|
||||||
|
|
||||||
|
// Log the error so users of `Send` don't see it entirely disappear as that method
|
||||||
|
// does not return an error and used to panic on failure (with or without debug)
|
||||||
|
smc.Debugf("Scheduling Socket Mode response (error: %v) for envelope ID %s: %s", err, res.EnvelopeID, js)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case smc.socketModeResponses <- &res:
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// receiveMessagesInto attempts to receive an event from the WebSocket connection for Socket Mode.
|
||||||
|
// This will block until a frame is available from the WebSocket.
|
||||||
|
// If the read from the WebSocket results in a fatal error, this function will return non-nil.
|
||||||
|
func (smc *Client) receiveMessagesInto(ctx context.Context, conn *websocket.Conn, sink chan json.RawMessage) error {
|
||||||
|
smc.Debugf("Starting to receive message")
|
||||||
|
defer smc.Debugf("Finished to receive message")
|
||||||
|
|
||||||
|
event := json.RawMessage{}
|
||||||
|
err := conn.ReadJSON(&event)
|
||||||
|
if err != nil {
|
||||||
|
// check if the connection was closed.
|
||||||
|
// This version of the gorilla/websocket package also does a type assertion
|
||||||
|
// on the error, rather than unwrapping it, so we'll do the unwrapping then pass
|
||||||
|
// the unwrapped error
|
||||||
|
var wsErr *websocket.CloseError
|
||||||
|
if errors.As(err, &wsErr) && websocket.IsUnexpectedCloseError(wsErr) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, io.ErrUnexpectedEOF) {
|
||||||
|
// EOF's don't seem to signify a failed connection so instead we ignore
|
||||||
|
// them here and detect a failed connection upon attempting to send a
|
||||||
|
// 'PING' message
|
||||||
|
|
||||||
|
// Unlike RTM, we don't ping from the our end as there seem to have no client ping.
|
||||||
|
// We just continue to the next loop so that we `smc.disconnected` should be received if
|
||||||
|
// this EOF error was actually due to disconnection.
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// All other errors from ReadJSON come from NextReader, and should
|
||||||
|
// kill the read loop and force a reconnect.
|
||||||
|
// TODO: Unless it's a JSON unmarshal-type error in which case maybe reconnecting isn't needed...
|
||||||
|
smc.sendEvent(ctx, newEvent(EventTypeIncomingError, &slack.IncomingEventError{
|
||||||
|
ErrorObj: err,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if smc.debug {
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
d := json.NewEncoder(buf)
|
||||||
|
d.SetIndent("", " ")
|
||||||
|
if err := d.Encode(event); err != nil {
|
||||||
|
smc.Debugln("Failed encoding decoded json:", err)
|
||||||
|
}
|
||||||
|
reencoded := buf.String()
|
||||||
|
|
||||||
|
smc.Debugln("Incoming WebSocket message:", reencoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case sink <- event:
|
||||||
|
case <-ctx.Done():
|
||||||
|
smc.Debugln("cancelled while attempting to send raw event")
|
||||||
|
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseEvent takes a raw JSON message received from the slack websocket
|
||||||
|
// and handles the encoded event.
|
||||||
|
// returns the our own event that wraps the socket mode request.
|
||||||
|
func (smc *Client) parseEvent(wsMsg json.RawMessage) (*Event, error) {
|
||||||
|
req := &Request{}
|
||||||
|
err := json.Unmarshal(wsMsg, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshalling WebSocket message: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var evt Event
|
||||||
|
|
||||||
|
// See below two links for all the available message types.
|
||||||
|
// - https://github.com/slackapi/node-slack-sdk/blob/c3f4d7109062a0356fb765d53794b7b5f6b3b5ae/packages/socket-mode/src/SocketModeClient.ts#L533
|
||||||
|
// - https://api.slack.com/apis/connections/socket-implement
|
||||||
|
switch req.Type {
|
||||||
|
case RequestTypeHello:
|
||||||
|
evt = newEvent(EventTypeHello, nil, req)
|
||||||
|
case RequestTypeEventsAPI:
|
||||||
|
payloadEvent := req.Payload
|
||||||
|
|
||||||
|
eventsAPIEvent, err := slackevents.ParseEvent(payloadEvent, slackevents.OptionNoVerifyToken())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parsing Events API event: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
evt = newEvent(EventTypeEventsAPI, eventsAPIEvent, req)
|
||||||
|
case RequestTypeDisconnect:
|
||||||
|
// See https://api.slack.com/apis/connections/socket-implement#disconnect
|
||||||
|
|
||||||
|
evt = newEvent(EventTypeDisconnect, nil, req)
|
||||||
|
case RequestTypeSlashCommands:
|
||||||
|
// See https://api.slack.com/apis/connections/socket-implement#command
|
||||||
|
var cmd slack.SlashCommand
|
||||||
|
|
||||||
|
if err := json.Unmarshal(req.Payload, &cmd); err != nil {
|
||||||
|
return nil, fmt.Errorf("parsing slash command: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
evt = newEvent(EventTypeSlashCommand, cmd, req)
|
||||||
|
case RequestTypeInteractive:
|
||||||
|
// See belows:
|
||||||
|
// - https://api.slack.com/apis/connections/socket-implement#button
|
||||||
|
// - https://api.slack.com/apis/connections/socket-implement#home
|
||||||
|
// - https://api.slack.com/apis/connections/socket-implement#modal
|
||||||
|
// - https://api.slack.com/apis/connections/socket-implement#menu
|
||||||
|
|
||||||
|
var callback slack.InteractionCallback
|
||||||
|
|
||||||
|
if err := json.Unmarshal(req.Payload, &callback); err != nil {
|
||||||
|
return nil, fmt.Errorf("parsing interaction callback: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
evt = newEvent(EventTypeInteractive, callback, req)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("processing WebSocket message: encountered unsupported type %q", req.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &evt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePing handles an incoming 'PONG' message which should be in response to
|
||||||
|
// a previously sent 'PING' message. This is then used to compute the
|
||||||
|
// connection's latency.
|
||||||
|
func (smc *Client) handlePing(conn *websocket.Conn, event string) {
|
||||||
|
smc.Debugf("WebSocket ping message received: %s", event)
|
||||||
|
|
||||||
|
// In WebSocket, we need to respond a PING from the server with a PONG with the same payload as the PING.
|
||||||
|
if err := conn.WriteControl(websocket.PongMessage, []byte(event), time.Now().Add(10*time.Second)); err != nil {
|
||||||
|
smc.Debugf("Failed writing WebSocket PONG message: %v", err)
|
||||||
|
}
|
||||||
|
}
|
133
vendor/github.com/slack-go/slack/socketmode/socketmode.go
generated
vendored
Normal file
133
vendor/github.com/slack-go/slack/socketmode/socketmode.go
generated
vendored
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
package socketmode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
|
||||||
|
"github.com/slack-go/slack"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EventType is the type of events that are emitted by scoketmode.Client.
|
||||||
|
// You receive and handle those events from a socketmode.Client.Events channel.
|
||||||
|
// Those event types does not necessarily match 1:1 to those of Slack Events API events.
|
||||||
|
type EventType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// The following request types are the types of requests sent from Slack via Socket Mode WebSocket connection
|
||||||
|
// and handled internally by the socketmode.Client.
|
||||||
|
// The consumer of socketmode.Client will never see it.
|
||||||
|
|
||||||
|
RequestTypeHello = "hello"
|
||||||
|
RequestTypeEventsAPI = "events_api"
|
||||||
|
RequestTypeDisconnect = "disconnect"
|
||||||
|
RequestTypeSlashCommands = "slash_commands"
|
||||||
|
RequestTypeInteractive = "interactive"
|
||||||
|
|
||||||
|
// The following event types are for events emitted by socketmode.Client itself and
|
||||||
|
// does not originate from Slack.
|
||||||
|
EventTypeConnecting = EventType("connecting")
|
||||||
|
EventTypeInvalidAuth = EventType("invalid_auth")
|
||||||
|
EventTypeConnectionError = EventType("connection_error")
|
||||||
|
EventTypeConnected = EventType("connected")
|
||||||
|
EventTypeIncomingError = EventType("incoming_error")
|
||||||
|
EventTypeErrorWriteFailed = EventType("write_error")
|
||||||
|
EventTypeErrorBadMessage = EventType("error_bad_message")
|
||||||
|
|
||||||
|
//
|
||||||
|
// The following event types are guaranteed to not change unless Slack changes
|
||||||
|
//
|
||||||
|
|
||||||
|
EventTypeHello = EventType("hello")
|
||||||
|
EventTypeDisconnect = EventType("disconnect")
|
||||||
|
EventTypeEventsAPI = EventType("events_api")
|
||||||
|
EventTypeInteractive = EventType("interactive")
|
||||||
|
EventTypeSlashCommand = EventType("slash_commands")
|
||||||
|
|
||||||
|
websocketDefaultTimeout = 10 * time.Second
|
||||||
|
defaultMaxPingInterval = 30 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
// Open calls the "apps.connections.open" endpoint and returns the provided URL and the full Info block.
|
||||||
|
//
|
||||||
|
// To have a fully managed Websocket connection, use `New`, and call `Run()` on it.
|
||||||
|
func (smc *Client) Open() (info *slack.SocketModeConnection, websocketURL string, err error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), websocketDefaultTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
return smc.StartSocketModeContext(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenContext calls the "apps.connections.open" endpoint and returns the provided URL and the full Info block.
|
||||||
|
//
|
||||||
|
// To have a fully managed Websocket connection, use `New`, and call `Run()` on it.
|
||||||
|
func (smc *Client) OpenContext(ctx context.Context) (info *slack.SocketModeConnection, websocketURL string, err error) {
|
||||||
|
return smc.StartSocketModeContext(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option options for the managed Client.
|
||||||
|
type Option func(client *Client)
|
||||||
|
|
||||||
|
// OptionDialer takes a gorilla websocket Dialer and uses it as the
|
||||||
|
// Dialer when opening the websocket for the Socket Mode connection.
|
||||||
|
func OptionDialer(d *websocket.Dialer) Option {
|
||||||
|
return func(smc *Client) {
|
||||||
|
smc.dialer = d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OptionPingInterval determines how often we expect Slack to deliver WebSocket ping to us.
|
||||||
|
// If no ping is delivered to us within this interval after the last ping, we assumes the WebSocket connection
|
||||||
|
// is dead and needs to be reconnected.
|
||||||
|
func OptionPingInterval(d time.Duration) Option {
|
||||||
|
return func(smc *Client) {
|
||||||
|
smc.maxPingInterval = d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OptionDebug enable debugging for the client
|
||||||
|
func OptionDebug(b bool) func(*Client) {
|
||||||
|
return func(c *Client) {
|
||||||
|
c.debug = b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OptionLog set logging for client.
|
||||||
|
func OptionLog(l logger) func(*Client) {
|
||||||
|
return func(c *Client) {
|
||||||
|
c.log = internalLog{logger: l}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a Socket Mode client which provides a fully managed connection to
|
||||||
|
// Slack's Websocket-based Socket Mode.
|
||||||
|
func New(api *slack.Client, options ...Option) *Client {
|
||||||
|
result := &Client{
|
||||||
|
Client: *api,
|
||||||
|
Events: make(chan Event, 50),
|
||||||
|
socketModeResponses: make(chan *Response, 20),
|
||||||
|
maxPingInterval: defaultMaxPingInterval,
|
||||||
|
log: log.New(os.Stderr, "slack-go/slack/socketmode", log.LstdFlags|log.Lshortfile),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range options {
|
||||||
|
opt(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendEvent safely sends an event into the Clients Events channel
|
||||||
|
// and blocks until buffer space is had, or the context is canceled.
|
||||||
|
// This prevents deadlocking in the event that Events buffer is full,
|
||||||
|
// other goroutines are waiting, and/or timing allows receivers to exit
|
||||||
|
// before all senders are finished.
|
||||||
|
func (smc *Client) sendEvent(ctx context.Context, event Event) {
|
||||||
|
select {
|
||||||
|
case smc.Events <- event:
|
||||||
|
case <-ctx.Done():
|
||||||
|
}
|
||||||
|
}
|
260
vendor/github.com/slack-go/slack/socketmode/socketmode_handler.go
generated
vendored
Normal file
260
vendor/github.com/slack-go/slack/socketmode/socketmode_handler.go
generated
vendored
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
package socketmode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/slack-go/slack"
|
||||||
|
"github.com/slack-go/slack/slackevents"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SocketmodeHandler struct {
|
||||||
|
Client *Client
|
||||||
|
|
||||||
|
//lvl 1 - the most generic type of event
|
||||||
|
EventMap map[EventType][]SocketmodeHandlerFunc
|
||||||
|
//lvl 2 - Manage event by inner type
|
||||||
|
InteractionEventMap map[slack.InteractionType][]SocketmodeHandlerFunc
|
||||||
|
EventApiMap map[slackevents.EventsAPIType][]SocketmodeHandlerFunc
|
||||||
|
//lvl 3 - the most userfriendly way of managing event
|
||||||
|
InteractionBlockActionEventMap map[string]SocketmodeHandlerFunc
|
||||||
|
SlashCommandMap map[string]SocketmodeHandlerFunc
|
||||||
|
|
||||||
|
Default SocketmodeHandlerFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler have access to the event and socketmode client
|
||||||
|
type SocketmodeHandlerFunc func(*Event, *Client)
|
||||||
|
|
||||||
|
// Middleware accept SocketmodeHandlerFunc, and return SocketmodeHandlerFunc
|
||||||
|
type SocketmodeMiddlewareFunc func(SocketmodeHandlerFunc) SocketmodeHandlerFunc
|
||||||
|
|
||||||
|
// Initialization constructor for SocketmodeHandler
|
||||||
|
func NewSocketmodeHandler(client *Client) *SocketmodeHandler {
|
||||||
|
eventMap := make(map[EventType][]SocketmodeHandlerFunc)
|
||||||
|
interactionEventMap := make(map[slack.InteractionType][]SocketmodeHandlerFunc)
|
||||||
|
eventApiMap := make(map[slackevents.EventsAPIType][]SocketmodeHandlerFunc)
|
||||||
|
|
||||||
|
interactionBlockActionEventMap := make(map[string]SocketmodeHandlerFunc)
|
||||||
|
slackCommandMap := make(map[string]SocketmodeHandlerFunc)
|
||||||
|
|
||||||
|
return &SocketmodeHandler{
|
||||||
|
Client: client,
|
||||||
|
EventMap: eventMap,
|
||||||
|
EventApiMap: eventApiMap,
|
||||||
|
InteractionEventMap: interactionEventMap,
|
||||||
|
InteractionBlockActionEventMap: interactionBlockActionEventMap,
|
||||||
|
SlashCommandMap: slackCommandMap,
|
||||||
|
Default: func(e *Event, c *Client) {
|
||||||
|
c.log.Printf("Unexpected event type received: %v\n", e.Type)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register a middleware or handler for an Event from socketmode
|
||||||
|
// This most general entrypoint
|
||||||
|
func (r *SocketmodeHandler) Handle(et EventType, f SocketmodeHandlerFunc) {
|
||||||
|
r.EventMap[et] = append(r.EventMap[et], f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register a middleware or handler for an Interaction
|
||||||
|
// There is several types of interactions, decated functions lets you better handle them
|
||||||
|
// See
|
||||||
|
// * HandleInteractionBlockAction
|
||||||
|
// * (Not Implemented) HandleShortcut
|
||||||
|
// * (Not Implemented) HandleView
|
||||||
|
func (r *SocketmodeHandler) HandleInteraction(et slack.InteractionType, f SocketmodeHandlerFunc) {
|
||||||
|
r.InteractionEventMap[et] = append(r.InteractionEventMap[et], f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register a middleware or handler for a Block Action referenced by its ActionID
|
||||||
|
func (r *SocketmodeHandler) HandleInteractionBlockAction(actionID string, f SocketmodeHandlerFunc) {
|
||||||
|
if actionID == "" {
|
||||||
|
panic("invalid command cannot be empty")
|
||||||
|
}
|
||||||
|
if f == nil {
|
||||||
|
panic("invalid handler cannot be nil")
|
||||||
|
}
|
||||||
|
if _, exist := r.InteractionBlockActionEventMap[actionID]; exist {
|
||||||
|
panic("multiple registrations for actionID" + actionID)
|
||||||
|
}
|
||||||
|
r.InteractionBlockActionEventMap[actionID] = f
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register a middleware or handler for an Event (from slackevents)
|
||||||
|
func (r *SocketmodeHandler) HandleEvents(et slackevents.EventsAPIType, f SocketmodeHandlerFunc) {
|
||||||
|
r.EventApiMap[et] = append(r.EventApiMap[et], f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register a middleware or handler for a Slash Command
|
||||||
|
func (r *SocketmodeHandler) HandleSlashCommand(command string, f SocketmodeHandlerFunc) {
|
||||||
|
if command == "" {
|
||||||
|
panic("invalid command cannot be empty")
|
||||||
|
}
|
||||||
|
if f == nil {
|
||||||
|
panic("invalid handler cannot be nil")
|
||||||
|
}
|
||||||
|
if _, exist := r.SlashCommandMap[command]; exist {
|
||||||
|
panic("multiple registrations for command" + command)
|
||||||
|
}
|
||||||
|
r.SlashCommandMap[command] = f
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register a middleware or handler to use as a last resort
|
||||||
|
func (r *SocketmodeHandler) HandleDefault(f SocketmodeHandlerFunc) {
|
||||||
|
r.Default = f
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunSlackEventLoop receives the event via the socket
|
||||||
|
func (r *SocketmodeHandler) RunEventLoop() error {
|
||||||
|
|
||||||
|
go r.runEventLoop(context.Background())
|
||||||
|
|
||||||
|
return r.Client.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SocketmodeHandler) RunEventLoopContext(ctx context.Context) error {
|
||||||
|
go r.runEventLoop(ctx)
|
||||||
|
|
||||||
|
return r.Client.RunContext(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the dispatcher for each incomming event
|
||||||
|
func (r *SocketmodeHandler) runEventLoop(ctx context.Context) {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case evt, ok := <-r.Client.Events:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.dispatcher(evt)
|
||||||
|
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatch events to the specialized dispatcher
|
||||||
|
func (r *SocketmodeHandler) dispatcher(evt Event) {
|
||||||
|
var ishandled bool
|
||||||
|
|
||||||
|
// Some eventType can be further decomposed
|
||||||
|
switch evt.Type {
|
||||||
|
case EventTypeInteractive:
|
||||||
|
ishandled = r.interactionDispatcher(&evt)
|
||||||
|
case EventTypeEventsAPI:
|
||||||
|
ishandled = r.eventAPIDispatcher(&evt)
|
||||||
|
case EventTypeSlashCommand:
|
||||||
|
ishandled = r.slashCommandDispatcher(&evt)
|
||||||
|
default:
|
||||||
|
ishandled = r.socketmodeDispatcher(&evt)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ishandled {
|
||||||
|
go r.Default(&evt, r.Client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatch socketmode events to the registered middleware
|
||||||
|
func (r *SocketmodeHandler) socketmodeDispatcher(evt *Event) bool {
|
||||||
|
if handlers, ok := r.EventMap[evt.Type]; ok {
|
||||||
|
// If we registered an event
|
||||||
|
for _, f := range handlers {
|
||||||
|
go f(evt, r.Client)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatch interactions to the registered middleware
|
||||||
|
func (r *SocketmodeHandler) interactionDispatcher(evt *Event) bool {
|
||||||
|
var ishandled bool = false
|
||||||
|
|
||||||
|
interaction, ok := evt.Data.(slack.InteractionCallback)
|
||||||
|
if !ok {
|
||||||
|
r.Client.log.Printf("Ignored %+v\n", evt)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Level 1 - socketmode EventType
|
||||||
|
ishandled = r.socketmodeDispatcher(evt)
|
||||||
|
|
||||||
|
// Level 2 - interaction EventType
|
||||||
|
if handlers, ok := r.InteractionEventMap[interaction.Type]; ok {
|
||||||
|
// If we registered an event
|
||||||
|
for _, f := range handlers {
|
||||||
|
go f(evt, r.Client)
|
||||||
|
}
|
||||||
|
|
||||||
|
ishandled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Level 3 - interaction with actionID
|
||||||
|
blockActions := interaction.ActionCallback.BlockActions
|
||||||
|
// outmoded approach won`t be implemented
|
||||||
|
// attachments_actions := interaction.ActionCallback.AttachmentActions
|
||||||
|
|
||||||
|
for _, action := range blockActions {
|
||||||
|
if handler, ok := r.InteractionBlockActionEventMap[action.ActionID]; ok {
|
||||||
|
|
||||||
|
go handler(evt, r.Client)
|
||||||
|
|
||||||
|
ishandled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ishandled
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatch eventAPI events to the registered middleware
|
||||||
|
func (r *SocketmodeHandler) eventAPIDispatcher(evt *Event) bool {
|
||||||
|
var ishandled bool = false
|
||||||
|
eventsAPIEvent, ok := evt.Data.(slackevents.EventsAPIEvent)
|
||||||
|
if !ok {
|
||||||
|
r.Client.log.Printf("Ignored %+v\n", evt)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
innerEventType := slackevents.EventsAPIType(eventsAPIEvent.InnerEvent.Type)
|
||||||
|
|
||||||
|
// Level 1 - socketmode EventType
|
||||||
|
ishandled = r.socketmodeDispatcher(evt)
|
||||||
|
|
||||||
|
// Level 2 - EventAPI EventType
|
||||||
|
if handlers, ok := r.EventApiMap[innerEventType]; ok {
|
||||||
|
// If we registered an event
|
||||||
|
for _, f := range handlers {
|
||||||
|
go f(evt, r.Client)
|
||||||
|
}
|
||||||
|
|
||||||
|
ishandled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return ishandled
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatch SlashCommands events to the registered middleware
|
||||||
|
func (r *SocketmodeHandler) slashCommandDispatcher(evt *Event) bool {
|
||||||
|
var ishandled bool = false
|
||||||
|
slashCommandEvent, ok := evt.Data.(slack.SlashCommand)
|
||||||
|
if !ok {
|
||||||
|
r.Client.log.Printf("Ignored %+v\n", evt)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Level 1 - socketmode EventType
|
||||||
|
ishandled = r.socketmodeDispatcher(evt)
|
||||||
|
|
||||||
|
// Level 2 - SlackCommand by name
|
||||||
|
if handler, ok := r.SlashCommandMap[slashCommandEvent.Command]; ok {
|
||||||
|
|
||||||
|
go handler(evt, r.Client)
|
||||||
|
|
||||||
|
ishandled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return ishandled
|
||||||
|
|
||||||
|
}
|
2
vendor/modules.txt
vendored
2
vendor/modules.txt
vendored
@ -381,7 +381,9 @@ github.com/slack-go/slack
|
|||||||
github.com/slack-go/slack/internal/backoff
|
github.com/slack-go/slack/internal/backoff
|
||||||
github.com/slack-go/slack/internal/errorsx
|
github.com/slack-go/slack/internal/errorsx
|
||||||
github.com/slack-go/slack/internal/timex
|
github.com/slack-go/slack/internal/timex
|
||||||
|
github.com/slack-go/slack/slackevents
|
||||||
github.com/slack-go/slack/slackutilsx
|
github.com/slack-go/slack/slackutilsx
|
||||||
|
github.com/slack-go/slack/socketmode
|
||||||
# github.com/sourcegraph/conc v0.3.0
|
# github.com/sourcegraph/conc v0.3.0
|
||||||
## explicit; go 1.19
|
## explicit; go 1.19
|
||||||
github.com/sourcegraph/conc
|
github.com/sourcegraph/conc
|
||||||
|
Loading…
Reference in New Issue
Block a user