diff options
| author | Mistivia <i@mistivia.com> | 2025-11-02 15:27:18 +0800 |
|---|---|---|
| committer | Mistivia <i@mistivia.com> | 2025-11-02 15:27:18 +0800 |
| commit | e9c24f4af7ed56760f6db7941827d09f6db9020b (patch) | |
| tree | 62128c43b883ce5e3148113350978755779bb5de /teleirc/matterbridge/bridge/slack/handlers.go | |
| parent | 58d5e7cfda4781d8a57ec52aefd02983835c301a (diff) | |
add matterbridge
Diffstat (limited to 'teleirc/matterbridge/bridge/slack/handlers.go')
| -rw-r--r-- | teleirc/matterbridge/bridge/slack/handlers.go | 415 |
1 files changed, 415 insertions, 0 deletions
diff --git a/teleirc/matterbridge/bridge/slack/handlers.go b/teleirc/matterbridge/bridge/slack/handlers.go new file mode 100644 index 0000000..2424442 --- /dev/null +++ b/teleirc/matterbridge/bridge/slack/handlers.go @@ -0,0 +1,415 @@ +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.Printf("%#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 getMessageTitle(attach *slack.Attachment) string { + if attach.TitleLink != "" { + return fmt.Sprintf("[%s](%s)\n", attach.Title, attach.TitleLink) + } + return attach.Title +} + +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 i, attach := range ev.Attachments { + if attach.Text != "" { + if attach.Title != "" { + rmsg.Text = getMessageTitle(&ev.Attachments[i]) + } + rmsg.Text += attach.Text + if attach.Footer != "" { + rmsg.Text += "\n\n" + attach.Footer + } + } 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 +} |
