This commit is contained in:
Ivan Zuev
2022-04-17 13:42:13 +02:00
committed by GitHub
17 changed files with 1923 additions and 141 deletions

1
.gitignore vendored
View File

@@ -7,3 +7,4 @@ matterbridge.toml
# Exclude IDE Files
.vscode
.idea

View File

@@ -9,6 +9,8 @@ import (
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/slack-go/slack"
"github.com/slack-go/slack/slackevents"
"github.com/slack-go/slack/socketmode"
)
// ErrEventIgnored is for events that should be ignored
@@ -48,65 +50,74 @@ func (b *Bslack) handleSlack() {
}
func (b *Bslack) handleSlackClient(messages chan *config.Message) {
for msg := range b.rtm.IncomingEvents {
if msg.Type != sUserTyping && msg.Type != sHello && msg.Type != sLatencyReport {
b.Log.Debugf("== Receiving event %#v", msg.Data)
}
switch ev := msg.Data.(type) {
case *slack.UserTypingEvent:
if !b.GetBool("ShowUserTyping") {
continue
for msg := range b.smc.Events {
switch msg.Type {
case socketmode.EventTypeConnected:
if authTest, authErr := b.smc.AuthTest(); authErr == nil {
if botInfo, infoErr := b.smc.GetBotInfo(authTest.BotID); infoErr == nil {
b.si = botInfo
b.channels.populateChannels(true)
b.users.populateUsers(true)
} else {
b.Log.Fatalf("Unable to identify bot user")
}
} else {
b.Log.Fatalf("Unable to identify bot user")
}
rmsg, err := b.handleTypingEvent(ev)
if err == ErrEventIgnored {
continue
} else if err != nil {
b.Log.Errorf("%#v", err)
case socketmode.EventTypeConnectionError:
ev, _ := msg.Data.(slack.ConnectionErrorEvent)
b.Log.Errorf("Connection failed %#v %#v", ev.Error(), ev.ErrorObj)
case socketmode.EventTypeErrorWriteFailed:
ev, _ := msg.Data.(socketmode.ErrorWriteFailed)
b.Log.Debugf("%#v", ev.Cause.Error())
case socketmode.EventTypeInvalidAuth:
ev, _ := msg.Data.(slack.InvalidAuthEvent)
b.Log.Fatalf("Invalid Token %#v", ev)
case socketmode.EventTypeHello, socketmode.EventTypeConnecting:
continue
case socketmode.EventTypeEventsAPI:
b.smc.Ack(*msg.Request)
eventsAPIEvent, ok := msg.Data.(slackevents.EventsAPIEvent)
if !ok {
b.Log.Debugf("Ignored %+v", eventsAPIEvent)
continue
}
messages <- rmsg
case *slack.MessageEvent:
if b.skipMessageEvent(ev) {
b.Log.Debugf("Skipped message: %#v", ev)
continue
switch innerEventData := eventsAPIEvent.InnerEvent.Data.(type) {
case *slackevents.MessageEvent:
if b.skipMessageEvent(innerEventData) {
b.Log.Debugf("Skipped message: %#v", innerEventData)
continue
}
rmsg, err := b.handleMessageEvent(innerEventData)
if err != nil {
b.Log.Errorf("%#v", err)
continue
}
messages <- rmsg
case *slackevents.MemberJoinedChannelEvent:
if innerEventData.User == b.si.UserID {
channel, err := b.smc.GetConversationInfo(innerEventData.Channel, false)
if err != nil {
b.Log.Errorf("Unable to get conversation info for channel %s", innerEventData.Channel)
}
b.channels.registerChannel(*channel)
} else {
b.users.populateUser(innerEventData.User)
}
}
rmsg, err := b.handleMessageEvent(ev)
if err != nil {
b.Log.Errorf("%#v", err)
continue
}
messages <- rmsg
case *slack.FileDeletedEvent:
rmsg, err := b.handleFileDeletedEvent(ev)
if err != nil {
b.Log.Errorf("%#v", err)
continue
}
messages <- rmsg
case *slack.OutgoingErrorEvent:
b.Log.Debugf("%#v", ev.Error())
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 *slack.ConnectedEvent:
b.si = ev.Info
b.channels.populateChannels(true)
b.users.populateUsers(true)
case *slack.InvalidAuthEvent:
b.Log.Fatalf("Invalid Token %#v", ev)
case *slack.ConnectionErrorEvent:
b.Log.Errorf("Connection failed %#v %#v", ev.Error(), ev.ErrorObj)
case *slack.MemberJoinedChannelEvent:
b.users.populateUser(ev.User)
case *slack.HelloEvent, *slack.LatencyReport, *slack.ConnectingEvent:
continue
case *slack.UserChangeEvent:
b.users.invalidateUser(ev.User.ID)
default:
b.Log.Debugf("Unhandled incoming event: %T", ev)
b.Log.Debugf("== Receiving event %#v", msg.Data)
}
}
}
@@ -127,7 +138,7 @@ func (b *Bslack) handleMatterHook(messages chan *config.Message) {
}
// skipMessageEvent skips event that need to be skipped :-)
func (b *Bslack) skipMessageEvent(ev *slack.MessageEvent) bool {
func (b *Bslack) skipMessageEvent(ev *slackevents.MessageEvent) bool {
switch ev.SubType {
case sChannelLeave, sChannelJoin:
return b.GetBool(noSendJoinConfig)
@@ -135,39 +146,36 @@ func (b *Bslack) skipMessageEvent(ev *slack.MessageEvent) bool {
return true
case sChannelTopic, sChannelPurpose:
// Skip the event if our bot/user account changed the topic/purpose
if ev.User == b.si.User.ID {
if ev.BotID == b.si.ID {
return true
}
}
// Check for our callback ID
hasOurCallbackID := false
if len(ev.Blocks.BlockSet) == 1 {
block, ok := ev.Blocks.BlockSet[0].(*slack.SectionBlock)
if len(ev.Attachments) == 1 {
block, ok := ev.Attachments[0].Blocks.BlockSet[0].(*slack.SectionBlock)
hasOurCallbackID = ok && block.BlockID == "matterbridge_"+b.uuid
}
if ev.SubMessage != nil {
if ev.Message != nil {
// It seems ev.SubMessage.Edited == nil when slack unfurls.
// Do not forward these messages. See Github issue #266.
if ev.SubMessage.ThreadTimestamp != ev.SubMessage.Timestamp &&
ev.SubMessage.Edited == nil {
if ev.Message.ThreadTimeStamp != ev.Message.TimeStamp &&
ev.Message.Edited == nil {
return true
}
// see hidden subtypes at https://api.slack.com/events/message
// these messages are sent when we add a message to a thread #709
if ev.SubType == "message_replied" && ev.Hidden {
return true
}
if len(ev.SubMessage.Blocks.BlockSet) == 1 {
block, ok := ev.SubMessage.Blocks.BlockSet[0].(*slack.SectionBlock)
if len(ev.Message.Attachments) == 1 {
block, ok := ev.Message.Attachments[0].Blocks.BlockSet[0].(*slack.SectionBlock)
hasOurCallbackID = ok && block.BlockID == "matterbridge_"+b.uuid
}
}
// Skip any messages that we made ourselves or from 'slackbot' (see #527).
if ev.Username == sSlackBotUser ||
(b.rtm != nil && ev.Username == b.si.User.Name) || hasOurCallbackID {
(b.smc != nil && ev.BotID == b.si.ID) || hasOurCallbackID {
return true
}
@@ -177,7 +185,7 @@ func (b *Bslack) skipMessageEvent(ev *slack.MessageEvent) bool {
return false
}
func (b *Bslack) filesCached(files []slack.File) bool {
func (b *Bslack) filesCached(files []slackevents.File) bool {
for i := range files {
if !b.fileCached(&files[i]) {
return false
@@ -202,7 +210,7 @@ func (b *Bslack) filesCached(files []slack.File) bool {
// 5. Handle any attachments of the received event.
// 6. Check that the Matterbridge message that we end up with after at the end of the
// pipeline is valid before sending it to the Matterbridge router.
func (b *Bslack) handleMessageEvent(ev *slack.MessageEvent) (*config.Message, error) {
func (b *Bslack) handleMessageEvent(ev *slackevents.MessageEvent) (*config.Message, error) {
rmsg, err := b.populateReceivedMessage(ev)
if err != nil {
return nil, err
@@ -222,14 +230,15 @@ func (b *Bslack) handleMessageEvent(ev *slack.MessageEvent) (*config.Message, er
// This is probably a webhook we couldn't resolve.
return nil, fmt.Errorf("message handling resulted in an empty bot message (probably an incoming webhook we couldn't resolve): %#v", ev)
}
if ev.SubMessage != nil {
return nil, fmt.Errorf("message handling resulted in an empty message: %#v with submessage %#v", ev, ev.SubMessage)
if ev.Message != nil {
return nil, fmt.Errorf("message handling resulted in an empty message: %#v with submessage %#v", ev, ev.Message)
}
return nil, fmt.Errorf("message handling resulted in an empty message: %#v", ev)
}
return rmsg, nil
}
// TODO: implement file deletion handle when slack-go library will expose this event
func (b *Bslack) handleFileDeletedEvent(ev *slack.FileDeletedEvent) (*config.Message, error) {
if rawChannel, ok := b.cache.Get(cfileDownloadChannel + ev.FileID); ok {
channel, err := b.channels.getChannelByID(rawChannel.(string))
@@ -250,7 +259,7 @@ func (b *Bslack) handleFileDeletedEvent(ev *slack.FileDeletedEvent) (*config.Mes
return nil, fmt.Errorf("channel ID for file ID %s not found", ev.FileID)
}
func (b *Bslack) handleStatusEvent(ev *slack.MessageEvent, rmsg *config.Message) bool {
func (b *Bslack) handleStatusEvent(ev *slackevents.MessageEvent, rmsg *config.Message) bool {
switch ev.SubType {
case sChannelJoined, sMemberJoined:
// There's no further processing needed on channel events
@@ -263,16 +272,16 @@ func (b *Bslack) handleStatusEvent(ev *slack.MessageEvent, rmsg *config.Message)
b.channels.populateChannels(false)
rmsg.Event = config.EventTopicChange
case sMessageChanged:
rmsg.Text = ev.SubMessage.Text
rmsg.Text = ev.Message.Text
// handle deleted thread starting messages
if ev.SubMessage.Text == "This message was deleted." {
if ev.Message.Text == "This message was deleted." {
rmsg.Event = config.EventMsgDelete
return true
}
case sMessageDeleted:
rmsg.Text = config.EventMsgDelete
rmsg.Event = config.EventMsgDelete
rmsg.ID = ev.DeletedTimestamp
rmsg.ID = ev.PreviousMessage.TimeStamp
// If a message is being deleted we do not need to process
// the event any further so we return 'true'.
return true
@@ -282,7 +291,7 @@ func (b *Bslack) handleStatusEvent(ev *slack.MessageEvent, rmsg *config.Message)
return false
}
func (b *Bslack) handleAttachments(ev *slack.MessageEvent, rmsg *config.Message) {
func (b *Bslack) handleAttachments(ev *slackevents.MessageEvent, rmsg *config.Message) {
// File comments are set by the system (because there is no username given).
if ev.SubType == sFileComment {
rmsg.Username = sSystemUser
@@ -317,23 +326,8 @@ func (b *Bslack) handleAttachments(ev *slack.MessageEvent, rmsg *config.Message)
}
}
func (b *Bslack) handleTypingEvent(ev *slack.UserTypingEvent) (*config.Message, error) {
if ev.User == b.si.User.ID {
return nil, ErrEventIgnored
}
channelInfo, err := b.channels.getChannelByID(ev.Channel)
if err != nil {
return nil, err
}
return &config.Message{
Channel: channelInfo.Name,
Account: b.Account,
Event: config.EventUserTyping,
}, nil
}
// handleDownloadFile handles file download
func (b *Bslack) handleDownloadFile(rmsg *config.Message, file *slack.File, retry bool) error {
func (b *Bslack) handleDownloadFile(rmsg *config.Message, file *slackevents.File, retry bool) error {
if b.fileCached(file) {
return nil
}
@@ -395,7 +389,7 @@ func (b *Bslack) handleGetChannelMembers(rmsg *config.Message) bool {
// identically named file but with different content will be uploaded correctly
// (the assumption is that such name collisions will not occur within the given
// timeframes).
func (b *Bslack) fileCached(file *slack.File) bool {
func (b *Bslack) fileCached(file *slackevents.File) bool {
if ts, ok := b.cache.Get("file" + file.ID); ok && time.Since(ts.(time.Time)) < time.Minute {
return true
} else if ts, ok = b.cache.Get("filename" + file.Name); ok && time.Since(ts.(time.Time)) < 10*time.Second {

View File

@@ -9,11 +9,12 @@ import (
"github.com/42wim/matterbridge/bridge/config"
"github.com/sirupsen/logrus"
"github.com/slack-go/slack"
"github.com/slack-go/slack/slackevents"
)
// populateReceivedMessage shapes the initial Matterbridge message that we will forward to the
// router before we apply message-dependent modifications.
func (b *Bslack) populateReceivedMessage(ev *slack.MessageEvent) (*config.Message, error) {
func (b *Bslack) populateReceivedMessage(ev *slackevents.MessageEvent) (*config.Message, error) {
// Use our own func because rtm.GetChannelInfo doesn't work for private channels.
channel, err := b.channels.getChannelByID(ev.Channel)
if err != nil {
@@ -24,9 +25,9 @@ func (b *Bslack) populateReceivedMessage(ev *slack.MessageEvent) (*config.Messag
Text: ev.Text,
Channel: channel.Name,
Account: b.Account,
ID: ev.Timestamp,
ID: ev.TimeStamp,
Extra: make(map[string][]interface{}),
ParentID: ev.ThreadTimestamp,
ParentID: ev.ThreadTimeStamp,
Protocol: b.Protocol,
}
if b.useChannelID {
@@ -34,19 +35,19 @@ func (b *Bslack) populateReceivedMessage(ev *slack.MessageEvent) (*config.Messag
}
// Handle 'edit' messages.
if ev.SubMessage != nil && !b.GetBool(editDisableConfig) {
rmsg.ID = ev.SubMessage.Timestamp
if ev.SubMessage.ThreadTimestamp != ev.SubMessage.Timestamp {
b.Log.Debugf("SubMessage %#v", ev.SubMessage)
rmsg.Text = ev.SubMessage.Text + b.GetString(editSuffixConfig)
if ev.Message != nil && !b.GetBool(editDisableConfig) {
rmsg.ID = ev.Message.TimeStamp
if ev.Message.ThreadTimeStamp != ev.Message.TimeStamp {
b.Log.Debugf("SubMessage %#v", ev.Message)
rmsg.Text = ev.Message.Text + b.GetString(editSuffixConfig)
}
}
// For edits, only submessage has thread ts.
// Ensures edits to threaded messages maintain their prefix hint on the
// unthreaded end.
if ev.SubMessage != nil {
rmsg.ParentID = ev.SubMessage.ThreadTimestamp
if ev.Message != nil {
rmsg.ParentID = ev.Message.ThreadTimeStamp
}
if err = b.populateMessageWithUserInfo(ev, rmsg); err != nil {
@@ -55,7 +56,7 @@ func (b *Bslack) populateReceivedMessage(ev *slack.MessageEvent) (*config.Messag
return rmsg, err
}
func (b *Bslack) populateMessageWithUserInfo(ev *slack.MessageEvent, rmsg *config.Message) error {
func (b *Bslack) populateMessageWithUserInfo(ev *slackevents.MessageEvent, rmsg *config.Message) error {
if ev.SubType == sMessageDeleted || ev.SubType == sFileComment {
return nil
}
@@ -71,8 +72,8 @@ func (b *Bslack) populateMessageWithUserInfo(ev *slack.MessageEvent, rmsg *confi
switch {
case ev.User != "":
userID = ev.User
case ev.SubMessage != nil && ev.SubMessage.User != "":
userID = ev.SubMessage.User
case ev.Message != nil && ev.Message.User != "":
userID = ev.Message.User
default:
return nil
}
@@ -93,7 +94,7 @@ func (b *Bslack) populateMessageWithUserInfo(ev *slack.MessageEvent, rmsg *confi
return nil
}
func (b *Bslack) populateMessageWithBotInfo(ev *slack.MessageEvent, rmsg *config.Message) error {
func (b *Bslack) populateMessageWithBotInfo(ev *slackevents.MessageEvent, rmsg *config.Message) error {
if ev.BotID == "" || b.GetString(outgoingWebhookConfig) != "" {
return nil
}
@@ -101,7 +102,7 @@ func (b *Bslack) populateMessageWithBotInfo(ev *slack.MessageEvent, rmsg *config
var err error
var bot *slack.Bot
for {
bot, err = b.rtm.GetBotInfo(ev.BotID)
bot, err = b.smc.GetBotInfo(ev.BotID)
if err == nil {
break
}

View File

@@ -15,6 +15,7 @@ import (
lru "github.com/hashicorp/golang-lru"
"github.com/rs/xid"
"github.com/slack-go/slack"
"github.com/slack-go/slack/socketmode"
)
type Bslack struct {
@@ -24,7 +25,8 @@ type Bslack struct {
mh *matterhook.Client
sc *slack.Client
rtm *slack.RTM
si *slack.Info
si *slack.Bot
smc *socketmode.Client
cache *lru.Cache
uuid string
@@ -36,27 +38,25 @@ type Bslack struct {
}
const (
sHello = "hello"
sChannelJoin = "channel_join"
sChannelLeave = "channel_leave"
sChannelJoined = "channel_joined"
sMemberJoined = "member_joined_channel"
sMessageChanged = "message_changed"
sMessageDeleted = "message_deleted"
sSlackAttachment = "slack_attachment"
sPinnedItem = "pinned_item"
sUnpinnedItem = "unpinned_item"
sChannelTopic = "channel_topic"
sChannelPurpose = "channel_purpose"
sFileComment = "file_comment"
sMeMessage = "me_message"
sUserTyping = "user_typing"
sLatencyReport = "latency_report"
sChannelJoin = "channel_join"
sChannelLeave = "channel_leave"
sChannelJoined = "channel_joined"
sMemberJoined = "member_joined_channel"
sMessageChanged = "message_changed"
sMessageDeleted = "message_deleted"
sSlackAttachment = "slack_attachment"
sPinnedItem = "pinned_item"
sUnpinnedItem = "unpinned_item"
sChannelTopic = "channel_topic"
sChannelPurpose = "channel_purpose"
sFileComment = "file_comment"
sMeMessage = "me_message"
sSystemUser = "system"
sSlackBotUser = "slackbot"
cfileDownloadChannel = "file_download_channel"
tokenConfig = "Token"
appTokenConfig = "AppToken"
incomingWebhookConfig = "WebhookBindAddress"
outgoingWebhookConfig = "WebhookURL"
skipTLSConfig = "SkipTLSVerify"
@@ -71,12 +71,18 @@ const (
func New(cfg *bridge.Config) bridge.Bridger {
// Print a deprecation warning for legacy non-bot tokens (#527).
token := cfg.GetString(tokenConfig)
appToken := cfg.GetString(appTokenConfig)
if token != "" && !strings.HasPrefix(token, "xoxb") {
cfg.Log.Warn("Non-bot token detected. It is STRONGLY recommended to use a proper bot-token instead.")
cfg.Log.Warn("Legacy tokens may be deprecated by Slack at short notice. See the Matterbridge GitHub wiki for a migration guide.")
cfg.Log.Warn("See https://github.com/42wim/matterbridge/wiki/Slack-bot-setup")
return NewLegacy(cfg)
}
if appToken == "" || !strings.HasPrefix(appToken, "xapp-") {
cfg.Log.Fatalf("%s must have the prefix xapp-", appTokenConfig)
}
return newBridge(cfg)
}
@@ -105,18 +111,22 @@ func (b *Bslack) Connect() error {
return errors.New("no connection method found: WebhookBindAddress, WebhookURL or Token need to be configured")
}
token := b.GetString(tokenConfig)
appToken := b.GetString(appTokenConfig)
// If we have a token we use the Slack websocket-based RTM for both sending and receiving.
if token := b.GetString(tokenConfig); token != "" {
if token != "" && appToken != "" {
b.Log.Info("Connecting using token")
b.sc = slack.New(token, slack.OptionDebug(b.GetBool("Debug")))
b.sc = slack.New(token, slack.OptionDebug(b.GetBool("Debug")), slack.OptionAppLevelToken(appToken))
b.smc = socketmode.New(b.sc, socketmode.OptionDebug(b.GetBool("Debug")))
b.channels = newChannelManager(b.Log, b.sc)
b.users = newUserManager(b.Log, b.sc)
b.rtm = b.sc.NewRTM()
go b.rtm.ManageConnection()
go b.handleSlack()
go b.smc.Run()
return nil
}
@@ -141,10 +151,6 @@ func (b *Bslack) Connect() error {
return nil
}
func (b *Bslack) Disconnect() error {
return b.rtm.Disconnect()
}
// JoinChannel only acts as a verification method that checks whether Matterbridge's
// Slack integration is already member of the channel. This is because Slack does not
// allow apps or bots to join channels themselves and they need to be invited
@@ -285,12 +291,12 @@ func (b *Bslack) sendRTM(msg config.Message) (string, error) {
if err != nil {
return "", fmt.Errorf("could not send message: %v", err)
}
if msg.Event == config.EventUserTyping {
/*if msg.Event == config.EventUserTyping {
if b.GetBool("ShowUserTyping") {
b.rtm.SendMessage(b.rtm.NewTypingMessage(channelInfo.ID))
}
return "", nil
}
}*/
var handled bool
@@ -345,9 +351,9 @@ func (b *Bslack) updateTopicOrPurpose(msg *config.Message, channelInfo *slack.Ch
incomingChangeType, text := b.extractTopicOrPurpose(msg.Text)
switch incomingChangeType {
case "topic":
updateFunc = b.rtm.SetTopicOfConversation
updateFunc = b.smc.SetTopicOfConversation
case "purpose":
updateFunc = b.rtm.SetPurposeOfConversation
updateFunc = b.smc.SetPurposeOfConversation
default:
b.Log.Errorf("Unhandled type received from extractTopicOrPurpose: %s", incomingChangeType)
return nil
@@ -393,7 +399,7 @@ func (b *Bslack) deleteMessage(msg *config.Message, channelInfo *slack.Channel)
}
for {
_, _, err := b.rtm.DeleteMessage(channelInfo.ID, msg.ID)
_, _, err := b.smc.DeleteMessage(channelInfo.ID, msg.ID)
if err == nil {
return true, nil
}
@@ -411,7 +417,7 @@ func (b *Bslack) editMessage(msg *config.Message, channelInfo *slack.Channel) (b
}
messageOptions := b.prepareMessageOptions(msg)
for {
_, _, _, err := b.rtm.UpdateMessage(channelInfo.ID, msg.ID, messageOptions...)
_, _, _, err := b.smc.UpdateMessage(channelInfo.ID, msg.ID, messageOptions...)
if err == nil {
return true, nil
}
@@ -430,7 +436,7 @@ func (b *Bslack) postMessage(msg *config.Message, channelInfo *slack.Channel) (s
}
messageOptions := b.prepareMessageOptions(msg)
for {
_, id, err := b.rtm.PostMessage(channelInfo.ID, messageOptions...)
_, id, err := b.smc.PostMessage(channelInfo.ID, messageOptions...)
if err == nil {
return id, nil
}

View 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"`
}

View File

@@ -0,0 +1,506 @@
// inner_events.go provides EventsAPI particular inner events
package slackevents
import (
"encoding/json"
"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 json.Number `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"`
}
// AppHomeOpenedEvent Your Slack app home was opened.
type AppHomeOpenedEvent struct {
Type string `json:"type"`
User string `json:"user"`
Channel string `json:"channel"`
EventTimeStamp json.Number `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"`
}
// ChannelArchiveEvent represents the Channel archive event
type ChannelArchiveEvent struct {
Type string `json:"type"`
Channel string `json:"channel"`
User string `json:"user"`
}
// ChannelUnarchiveEvent represents the Channel unarchive event
type ChannelUnarchiveEvent struct {
Type string `json:"type"`
Channel string `json:"channel"`
User string `json:"user"`
}
// ChannelLeftEvent represents the Channel left event
type ChannelLeftEvent struct {
Type string `json:"type"`
Channel string `json:"channel"`
}
// ChannelRenameEvent represents the Channel rename event
type ChannelRenameEvent struct {
Type string `json:"type"`
Channel ChannelRenameInfo `json:"channel"`
}
// 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"`
}
// GroupArchiveEvent represents the Group archive event
type GroupArchiveEvent struct {
Type string `json:"type"`
Channel string `json:"channel"`
}
// GroupUnarchiveEvent represents the Group unarchive event
type GroupUnarchiveEvent struct {
Type string `json:"type"`
Channel string `json:"channel"`
}
// GroupLeftEvent represents the Group left event
type GroupLeftEvent struct {
Type string `json:"type"`
Channel string `json:"channel"`
}
// GroupRenameEvent represents the Group rename event
type GroupRenameEvent struct {
Type string `json:"type"`
Channel GroupRenameInfo `json:"channel"`
}
// 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"`
}
// 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"`
}
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 json.Number `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"`
// 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"`
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"`
}
// 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"`
}
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"`
}
// TokensRevokedEvent APP's API tokes are revoked - https://api.slack.com/events/tokens_revoked
type TokensRevokedEvent struct {
Type string `json:"type"`
Tokens tokens `json:"tokens"`
}
// EmojiChangedEvent is the event of custom emoji has been added or changed
type EmojiChangedEvent struct {
Type string `json:"type"`
Subtype string `json:"subtype"`
EventTimeStamp json.Number `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"`
}
// 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
}
const (
// AppMention is an Events API subscribable event
AppMention = "app_mention"
// AppHomeOpened Your Slack app home was opened
AppHomeOpened = "app_home_opened"
// AppUninstalled Your Slack app was uninstalled.
AppUninstalled = "app_uninstalled"
// ChannelCreated is sent when a new channel is created.
ChannelCreated = "channel_created"
// ChannelDeleted is sent when a channel is deleted.
ChannelDeleted = "channel_deleted"
// ChannelArchive is sent when a channel is archived.
ChannelArchive = "channel_archive"
// ChannelUnarchive is sent when a channel is unarchived.
ChannelUnarchive = "channel_unarchive"
// ChannelLeft is sent when a channel is left.
ChannelLeft = "channel_left"
// ChannelRename is sent when a channel is rename.
ChannelRename = "channel_rename"
// ChannelIDChanged is sent when a channel identifier is changed.
ChannelIDChanged = "channel_id_changed"
// GroupDeleted is sent when a group is deleted.
GroupDeleted = "group_deleted"
// GroupArchive is sent when a group is archived.
GroupArchive = "group_archive"
// GroupUnarchive is sent when a group is unarchived.
GroupUnarchive = "group_unarchive"
// GroupLeft is sent when a group is left.
GroupLeft = "group_left"
// GroupRename is sent when a group is renamed.
GroupRename = "group_rename"
// GridMigrationFinished An enterprise grid migration has finished on this workspace.
GridMigrationFinished = "grid_migration_finished"
// GridMigrationStarted An enterprise grid migration has started on this workspace.
GridMigrationStarted = "grid_migration_started"
// LinkShared A message was posted containing one or more links relevant to your application
LinkShared = "link_shared"
// Message A message was posted to a channel, private channel (group), im, or mim
Message = "message"
// Member Joined Channel
MemberJoinedChannel = "member_joined_channel"
// Member Left Channel
MemberLeftChannel = "member_left_channel"
// PinAdded An item was pinned to a channel
PinAdded = "pin_added"
// PinRemoved An item was unpinned from a channel
PinRemoved = "pin_removed"
// ReactionAdded An reaction was added to a message
ReactionAdded = "reaction_added"
// ReactionRemoved An reaction was removed from a message
ReactionRemoved = "reaction_removed"
// TeamJoin A new user joined the workspace
TeamJoin = "team_join"
// TokensRevoked APP's API tokes are revoked
TokensRevoked = "tokens_revoked"
// EmojiChanged A custom emoji has been added or changed
EmojiChanged = "emoji_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[string]interface{}{
AppMention: AppMentionEvent{},
AppHomeOpened: AppHomeOpenedEvent{},
AppUninstalled: AppUninstalledEvent{},
ChannelCreated: ChannelCreatedEvent{},
ChannelDeleted: ChannelDeletedEvent{},
ChannelArchive: ChannelArchiveEvent{},
ChannelUnarchive: ChannelUnarchiveEvent{},
ChannelLeft: ChannelLeftEvent{},
ChannelRename: ChannelRenameEvent{},
ChannelIDChanged: ChannelIDChangedEvent{},
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{},
TeamJoin: TeamJoinEvent{},
TokensRevoked: TokensRevokedEvent{},
EmojiChanged: EmojiChangedEvent{},
}

View File

@@ -0,0 +1,71 @@
// 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"`
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 OUTTER 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
View 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 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[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,
"",
&slack.UnmarshallingErrorEvent{ErrorObj: err},
EventsAPIInnerEvent{},
}, err
}
v, exists := eventsMap(iE.Type)
if !exists {
return EventsAPIEvent{
e.Token,
e.TeamID,
iE.Type,
e.APIAppID,
"",
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,
"",
&slack.UnmarshallingErrorEvent{ErrorObj: err},
EventsAPIInnerEvent{},
}, err
}
return EventsAPIEvent{
e.Token,
e.TeamID,
e.Type,
e.APIAppID,
"",
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 outter 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
View 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
}

31
vendor/github.com/slack-go/slack/socketmode/deadman.go generated vendored Normal file
View File

@@ -0,0 +1,31 @@
package socketmode
import "time"
type deadmanTimer struct {
timeout time.Duration
timer *time.Timer
}
func newDeadmanTimer(timeout time.Duration) *deadmanTimer {
return &deadmanTimer{
timeout: timeout,
timer: time.NewTimer(timeout),
}
}
func (smc *deadmanTimer) Elapsed() <-chan time.Time {
return smc.timer.C
}
func (smc *deadmanTimer) Reset() {
// Note that this is the correct way to Reset a non-expired timer
if !smc.timer.Stop() {
select {
case <-smc.timer.C:
default:
}
}
smc.timer.Reset(smc.timeout)
}

30
vendor/github.com/slack-go/slack/socketmode/event.go generated vendored Normal file
View 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
View 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
View 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"`
}

View File

@@ -0,0 +1,6 @@
package socketmode
type Response struct {
EnvelopeID string `json:"envelope_id"`
Payload interface{} `json:"payload,omitempty"`
}

View File

@@ -0,0 +1,567 @@
package socketmode
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"sync"
"time"
"github.com/slack-go/slack"
"github.com/slack-go/slack/internal/backoff"
"github.com/slack-go/slack/internal/misc"
"github.com/slack-go/slack/slackevents"
"github.com/gorilla/websocket"
"github.com/slack-go/slack/internal/timex"
)
// 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)
defer close(messages)
deadmanTimer := newDeadmanTimer(smc.maxPingInterval)
pingHandler := func(_ string) error {
deadmanTimer.Reset()
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.Events <- 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
var (
wg sync.WaitGroup
firstErr error
firstErrOnce sync.Once
)
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 {
firstErrOnce.Do(func() {
firstErr = 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 {
firstErrOnce.Do(func() {
firstErr = err
})
}
}()
wg.Add(1)
go func() {
defer wg.Done()
defer cancel()
// 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 {
firstErrOnce.Do(func() {
firstErr = err
})
}
}()
wg.Add(1)
go func() {
defer wg.Done()
select {
case <-ctx.Done():
// Detect when the connection is dead and try close connection.
if err = conn.Close(); err != nil {
smc.Debugf("Failed to close connection: %v", err)
}
case <-deadmanTimer.Elapsed():
firstErrOnce.Do(func() {
firstErr = errors.New("ping timeout: Slack did not send us WebSocket PING for more than Client.maxInterval")
})
cancel()
}
}()
wg.Wait()
if firstErr == context.Canceled {
return firstErr
}
// wg.Wait() finishes only after any of the above go routines finishes.
// Also, we can expect firstErr to be not nil, as goroutines can finish only on error.
smc.Debugf("Reconnecting due to %v", firstErr)
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.Events <- 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:
}
switch actual := err.(type) {
case misc.StatusCodeError:
if actual.Code == http.StatusNotFound {
smc.Debugf("invalid auth when connecting with Socket Mode: %s", err)
smc.Events <- newEvent(EventTypeInvalidAuth, &slack.InvalidAuthEvent{})
return nil, nil, err
}
case *slack.RateLimitedError:
backoff = actual.RetryAfter
default:
}
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.Events <- 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.
select {
case <-time.After(backoff): // retry after the backoff.
case <-ctx.Done():
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
}
conn.SetPingHandler(func(appData string) error {
if additionalPingHandler != nil {
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.Events <- 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 := <-websocket:
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.Events <- 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.Events <- *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{})
if err := conn.WriteJSON(res); err != nil {
return err
}
return nil
}
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{}) {
res := Response{
EnvelopeID: req.EnvelopeID,
}
if len(payload) > 0 {
res.Payload = payload[0]
}
smc.Send(res)
}
// 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) {
js, err := json.Marshal(res)
if err != nil {
panic(err)
}
smc.Debugf("Scheduling Socket Mode response for envelope ID %s: %s", res.EnvelopeID, js)
smc.socketModeResponses <- &res
}
// 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)
// check if the connection was closed.
if websocket.IsUnexpectedCloseError(err) {
return err
}
switch {
case 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
case err != nil:
// All other errors from ReadJSON come from NextReader, and should
// kill the read loop and force a reconnect.
smc.Events <- newEvent(EventTypeIncomingError, &slack.IncomingEventError{
ErrorObj: err,
})
return err
case len(event) == 0:
smc.Debugln("Received empty event")
default:
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: %v", 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: %v", 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: %v", 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: %v", 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)
}
}

View File

@@ -0,0 +1,121 @@
package socketmode
import (
"context"
"log"
"os"
"time"
"github.com/slack-go/slack"
"github.com/gorilla/websocket"
)
// 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
}

2
vendor/modules.txt vendored
View File

@@ -403,7 +403,9 @@ github.com/slack-go/slack/internal/backoff
github.com/slack-go/slack/internal/errorsx
github.com/slack-go/slack/internal/misc
github.com/slack-go/slack/internal/timex
github.com/slack-go/slack/slackevents
github.com/slack-go/slack/slackutilsx
github.com/slack-go/slack/socketmode
# github.com/spf13/afero v1.6.0
## explicit; go 1.13
github.com/spf13/afero