diff --git a/bridge/slack/handlers.go b/bridge/slack/handlers.go index c469b9a5..0d197556 100644 --- a/bridge/slack/handlers.go +++ b/bridge/slack/handlers.go @@ -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 { diff --git a/bridge/slack/helpers.go b/bridge/slack/helpers.go index c970b0d7..0c98a619 100644 --- a/bridge/slack/helpers.go +++ b/bridge/slack/helpers.go @@ -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 } @@ -90,7 +91,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 } @@ -98,7 +99,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 } diff --git a/bridge/slack/slack.go b/bridge/slack/slack.go index 55231c38..6e0c3982 100644 --- a/bridge/slack/slack.go +++ b/bridge/slack/slack.go @@ -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 }