mirror of
https://github.com/42wim/matterbridge.git
synced 2024-12-24 12:12:39 +01:00
6438a3dba3
We create a new event EventFileDelete which will be used to delete specific uploaded files using the Extra["file"] in the config.Message. We also add a new NativeID key to the FileInfo struct which will contain the native file ID of the sending bridge. When a new file is added to the config.Message.Extra["file"] map, now the bridge native file ID should be added here. When the receiving bridge receives such a message, it should keep an internal mapping of NativeID <> bridge fileid/message id. In the case of discord we map it to the resulted discord message ID after uploading it. Now when a bridge deletes a file, it should send a EventFileDelete and setting the ID to the native file ID of the bridge. When the receiving bridge will get this event it'll look into the NativeID <> bridge id mapping to find their internal ID and use it to delete the specific file on their side. For now this is implemented for slack to discord but this will be add to other bridges where useful.
406 lines
13 KiB
Go
406 lines
13 KiB
Go
package bslack
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"html"
|
|
"time"
|
|
|
|
"github.com/42wim/matterbridge/bridge/config"
|
|
"github.com/42wim/matterbridge/bridge/helper"
|
|
"github.com/slack-go/slack"
|
|
)
|
|
|
|
// ErrEventIgnored is for events that should be ignored
|
|
var ErrEventIgnored = errors.New("this event message should ignored")
|
|
|
|
func (b *Bslack) handleSlack() {
|
|
messages := make(chan *config.Message)
|
|
if b.GetString(incomingWebhookConfig) != "" && b.GetString(tokenConfig) == "" {
|
|
b.Log.Debugf("Choosing webhooks based receiving")
|
|
go b.handleMatterHook(messages)
|
|
} else {
|
|
b.Log.Debugf("Choosing token based receiving")
|
|
go b.handleSlackClient(messages)
|
|
}
|
|
time.Sleep(time.Second)
|
|
b.Log.Debug("Start listening for Slack messages")
|
|
for message := range messages {
|
|
// don't do any action on deleted/typing messages
|
|
if message.Event != config.EventUserTyping && message.Event != config.EventMsgDelete &&
|
|
message.Event != config.EventFileDelete {
|
|
b.Log.Debugf("<= Sending message from %s on %s to gateway", message.Username, b.Account)
|
|
// cleanup the message
|
|
message.Text = b.replaceMention(message.Text)
|
|
message.Text = b.replaceVariable(message.Text)
|
|
message.Text = b.replaceChannel(message.Text)
|
|
message.Text = b.replaceURL(message.Text)
|
|
message.Text = b.replaceb0rkedMarkDown(message.Text)
|
|
message.Text = html.UnescapeString(message.Text)
|
|
|
|
// Add the avatar
|
|
message.Avatar = b.users.getAvatar(message.UserID)
|
|
}
|
|
|
|
b.Log.Debugf("<= Message is %#v", message)
|
|
b.Remote <- *message
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
rmsg, err := b.handleTypingEvent(ev)
|
|
if err == ErrEventIgnored {
|
|
continue
|
|
} else if err != nil {
|
|
b.Log.Errorf("%#v", err)
|
|
continue
|
|
}
|
|
|
|
messages <- rmsg
|
|
case *slack.MessageEvent:
|
|
if b.skipMessageEvent(ev) {
|
|
b.Log.Debugf("Skipped message: %#v", ev)
|
|
continue
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (b *Bslack) handleMatterHook(messages chan *config.Message) {
|
|
for {
|
|
message := b.mh.Receive()
|
|
b.Log.Debugf("receiving from matterhook (slack) %#v", message)
|
|
if message.UserName == "slackbot" {
|
|
continue
|
|
}
|
|
messages <- &config.Message{
|
|
Username: message.UserName,
|
|
Text: message.Text,
|
|
Channel: message.ChannelName,
|
|
}
|
|
}
|
|
}
|
|
|
|
// skipMessageEvent skips event that need to be skipped :-)
|
|
func (b *Bslack) skipMessageEvent(ev *slack.MessageEvent) bool {
|
|
switch ev.SubType {
|
|
case sChannelLeave, sChannelJoin:
|
|
return b.GetBool(noSendJoinConfig)
|
|
case sPinnedItem, sUnpinnedItem:
|
|
return true
|
|
case sChannelTopic, sChannelPurpose:
|
|
// Skip the event if our bot/user account changed the topic/purpose
|
|
if ev.User == b.si.User.ID {
|
|
return true
|
|
}
|
|
}
|
|
|
|
// Check for our callback ID
|
|
hasOurCallbackID := false
|
|
if len(ev.Blocks.BlockSet) == 1 {
|
|
block, ok := ev.Blocks.BlockSet[0].(*slack.SectionBlock)
|
|
hasOurCallbackID = ok && block.BlockID == "matterbridge_"+b.uuid
|
|
}
|
|
|
|
if ev.SubMessage != 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 {
|
|
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)
|
|
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 {
|
|
return true
|
|
}
|
|
|
|
if len(ev.Files) > 0 {
|
|
return b.filesCached(ev.Files)
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (b *Bslack) filesCached(files []slack.File) bool {
|
|
for i := range files {
|
|
if !b.fileCached(&files[i]) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// handleMessageEvent handles the message events. Together with any called sub-methods,
|
|
// this method implements the following event processing pipeline:
|
|
//
|
|
// 1. Check if the message should be ignored.
|
|
// NOTE: This is not actually part of the method below but is done just before it
|
|
// is called via the 'skipMessageEvent()' method.
|
|
// 2. Populate the Matterbridge message that will be sent to the router based on the
|
|
// received event and logic that is common to all events that are not skipped.
|
|
// 3. Detect and handle any message that is "status" related (think join channel, etc.).
|
|
// This might result in an early exit from the pipeline and passing of the
|
|
// pre-populated message to the Matterbridge router.
|
|
// 4. Handle the specific case of messages that edit existing messages depending on
|
|
// configuration.
|
|
// 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) {
|
|
rmsg, err := b.populateReceivedMessage(ev)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Handle some message types early.
|
|
if b.handleStatusEvent(ev, rmsg) {
|
|
return rmsg, nil
|
|
}
|
|
|
|
b.handleAttachments(ev, rmsg)
|
|
|
|
// Verify that we have the right information and the message
|
|
// is well-formed before sending it out to the router.
|
|
if len(ev.Files) == 0 && (rmsg.Text == "" || rmsg.Username == "") {
|
|
if ev.BotID != "" {
|
|
// 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)
|
|
}
|
|
return nil, fmt.Errorf("message handling resulted in an empty message: %#v", ev)
|
|
}
|
|
return rmsg, nil
|
|
}
|
|
|
|
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))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &config.Message{
|
|
Event: config.EventFileDelete,
|
|
Text: config.EventFileDelete,
|
|
Channel: channel.Name,
|
|
Account: b.Account,
|
|
ID: ev.FileID,
|
|
Protocol: b.Protocol,
|
|
}, nil
|
|
}
|
|
|
|
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 {
|
|
switch ev.SubType {
|
|
case sChannelJoined, sMemberJoined:
|
|
// There's no further processing needed on channel events
|
|
// so we return 'true'.
|
|
return true
|
|
case sChannelJoin, sChannelLeave:
|
|
rmsg.Username = sSystemUser
|
|
rmsg.Event = config.EventJoinLeave
|
|
case sChannelTopic, sChannelPurpose:
|
|
b.channels.populateChannels(false)
|
|
rmsg.Event = config.EventTopicChange
|
|
case sMessageChanged:
|
|
rmsg.Text = ev.SubMessage.Text
|
|
// handle deleted thread starting messages
|
|
if ev.SubMessage.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
|
|
// If a message is being deleted we do not need to process
|
|
// the event any further so we return 'true'.
|
|
return true
|
|
case sMeMessage:
|
|
rmsg.Event = config.EventUserAction
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (b *Bslack) handleAttachments(ev *slack.MessageEvent, rmsg *config.Message) {
|
|
// File comments are set by the system (because there is no username given).
|
|
if ev.SubType == sFileComment {
|
|
rmsg.Username = sSystemUser
|
|
}
|
|
|
|
// See if we have some text in the attachments.
|
|
if rmsg.Text == "" {
|
|
for _, attach := range ev.Attachments {
|
|
if attach.Text != "" {
|
|
if attach.Title != "" {
|
|
rmsg.Text = attach.Title + "\n"
|
|
}
|
|
rmsg.Text += attach.Text
|
|
} else {
|
|
rmsg.Text = attach.Fallback
|
|
}
|
|
}
|
|
}
|
|
|
|
// Save the attachments, so that we can send them to other slack (compatible) bridges.
|
|
if len(ev.Attachments) > 0 {
|
|
rmsg.Extra[sSlackAttachment] = append(rmsg.Extra[sSlackAttachment], ev.Attachments)
|
|
}
|
|
|
|
// If we have files attached, download them (in memory) and put a pointer to it in msg.Extra.
|
|
for i := range ev.Files {
|
|
// keep reference in cache on which channel we added this file
|
|
b.cache.Add(cfileDownloadChannel+ev.Files[i].ID, ev.Channel)
|
|
if err := b.handleDownloadFile(rmsg, &ev.Files[i], false); err != nil {
|
|
b.Log.Errorf("Could not download incoming file: %#v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
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 {
|
|
if b.fileCached(file) {
|
|
return nil
|
|
}
|
|
// Check that the file is neither too large nor blacklisted.
|
|
if err := helper.HandleDownloadSize(b.Log, rmsg, file.Name, int64(file.Size), b.General); err != nil {
|
|
b.Log.WithError(err).Infof("Skipping download of incoming file.")
|
|
return nil
|
|
}
|
|
|
|
// Actually download the file.
|
|
data, err := helper.DownloadFileAuth(file.URLPrivateDownload, "Bearer "+b.GetString(tokenConfig))
|
|
if err != nil {
|
|
return fmt.Errorf("download %s failed %#v", file.URLPrivateDownload, err)
|
|
}
|
|
|
|
if len(*data) != file.Size && !retry {
|
|
b.Log.Debugf("Data size (%d) is not equal to size declared (%d)\n", len(*data), file.Size)
|
|
time.Sleep(1 * time.Second)
|
|
return b.handleDownloadFile(rmsg, file, true)
|
|
}
|
|
|
|
// If a comment is attached to the file(s) it is in the 'Text' field of the Slack messge event
|
|
// and should be added as comment to only one of the files. We reset the 'Text' field to ensure
|
|
// that the comment is not duplicated.
|
|
comment := rmsg.Text
|
|
rmsg.Text = ""
|
|
helper.HandleDownloadData2(b.Log, rmsg, file.Name, file.ID, comment, file.URLPrivateDownload, data, b.General)
|
|
return nil
|
|
}
|
|
|
|
// handleGetChannelMembers handles messages containing the GetChannelMembers event
|
|
// Sends a message to the router containing *config.ChannelMembers
|
|
func (b *Bslack) handleGetChannelMembers(rmsg *config.Message) bool {
|
|
if rmsg.Event != config.EventGetChannelMembers {
|
|
return false
|
|
}
|
|
|
|
cMembers := b.channels.getChannelMembers(b.users)
|
|
|
|
extra := make(map[string][]interface{})
|
|
extra[config.EventGetChannelMembers] = append(extra[config.EventGetChannelMembers], cMembers)
|
|
msg := config.Message{
|
|
Extra: extra,
|
|
Event: config.EventGetChannelMembers,
|
|
Account: b.Account,
|
|
}
|
|
|
|
b.Log.Debugf("sending msg to remote %#v", msg)
|
|
b.Remote <- msg
|
|
|
|
return true
|
|
}
|
|
|
|
// fileCached implements Matterbridge's caching logic for files
|
|
// shared via Slack.
|
|
//
|
|
// We consider that a file was cached if its ID was added in the last minute or
|
|
// it's name was registered in the last 10 seconds. This ensures that an
|
|
// 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 {
|
|
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 {
|
|
return true
|
|
}
|
|
return false
|
|
}
|