| 1 | package bslack |
| 2 | |
| 3 | import ( |
| 4 | "errors" |
| 5 | "fmt" |
| 6 | "html" |
| 7 | "time" |
| 8 | |
| 9 | "github.com/matterbridge-org/matterbridge/bridge/config" |
| 10 | "github.com/matterbridge-org/matterbridge/bridge/helper" |
| 11 | "github.com/slack-go/slack" |
| 12 | ) |
| 13 | |
| 14 | // ErrEventIgnored is for events that should be ignored |
| 15 | var ErrEventIgnored = errors.New("this event message should ignored") |
| 16 | |
| 17 | func (b *Bslack) handleSlack() { |
| 18 | messages := make(chan *config.Message) |
| 19 | if b.GetString(incomingWebhookConfig) != "" && b.GetString(tokenConfig) == "" { |
| 20 | b.Log.Debugf("Choosing webhooks based receiving") |
| 21 | go b.handleMatterHook(messages) |
| 22 | } else { |
| 23 | b.Log.Debugf("Choosing token based receiving") |
| 24 | go b.handleSlackClient(messages) |
| 25 | } |
| 26 | time.Sleep(time.Second) |
| 27 | b.Log.Debug("Start listening for Slack messages") |
| 28 | for message := range messages { |
| 29 | // don't do any action on deleted/typing messages |
| 30 | if message.Event != config.EventUserTyping && message.Event != config.EventMsgDelete && |
| 31 | message.Event != config.EventFileDelete { |
| 32 | b.Log.Debugf("<= Sending message from %s on %s to gateway", message.Username, b.Account) |
| 33 | // cleanup the message |
| 34 | message.Text = b.replaceMention(message.Text) |
| 35 | message.Text = b.replaceVariable(message.Text) |
| 36 | message.Text = b.replaceChannel(message.Text) |
| 37 | message.Text = b.replaceURL(message.Text) |
| 38 | message.Text = b.replaceb0rkedMarkDown(message.Text) |
| 39 | message.Text = html.UnescapeString(message.Text) |
| 40 | |
| 41 | // Add the avatar |
| 42 | message.Avatar = b.users.getAvatar(message.UserID) |
| 43 | } |
| 44 | |
| 45 | b.Log.Debugf("<= Message is %#v", message) |
| 46 | b.Remote <- *message |
| 47 | } |
| 48 | } |
| 49 | |
| 50 | func (b *Bslack) handleSlackClient(messages chan *config.Message) { |
| 51 | for msg := range b.rtm.IncomingEvents { |
| 52 | if msg.Type != sUserTyping && msg.Type != sHello && msg.Type != sLatencyReport { |
| 53 | b.Log.Debugf("== Receiving event %#v", msg.Data) |
| 54 | } |
| 55 | switch ev := msg.Data.(type) { |
| 56 | case *slack.UserTypingEvent: |
| 57 | if !b.GetBool("ShowUserTyping") { |
| 58 | continue |
| 59 | } |
| 60 | rmsg, err := b.handleTypingEvent(ev) |
| 61 | if err == ErrEventIgnored { |
| 62 | continue |
| 63 | } else if err != nil { |
| 64 | b.Log.Errorf("%#v", err) |
| 65 | continue |
| 66 | } |
| 67 | |
| 68 | messages <- rmsg |
| 69 | case *slack.MessageEvent: |
| 70 | if b.skipMessageEvent(ev) { |
| 71 | b.Log.Debugf("Skipped message: %#v", ev) |
| 72 | continue |
| 73 | } |
| 74 | rmsg, err := b.handleMessageEvent(ev) |
| 75 | if err != nil { |
| 76 | b.Log.Errorf("%#v", err) |
| 77 | continue |
| 78 | } |
| 79 | messages <- rmsg |
| 80 | case *slack.FileDeletedEvent: |
| 81 | rmsg, err := b.handleFileDeletedEvent(ev) |
| 82 | if err != nil { |
| 83 | b.Log.Printf("%#v", err) |
| 84 | continue |
| 85 | } |
| 86 | messages <- rmsg |
| 87 | case *slack.OutgoingErrorEvent: |
| 88 | b.Log.Debugf("%#v", ev.Error()) |
| 89 | case *slack.ChannelJoinedEvent: |
| 90 | // When we join a channel we update the full list of users as |
| 91 | // well as the information for the channel that we joined as this |
| 92 | // should now tell that we are a member of it. |
| 93 | b.channels.registerChannel(ev.Channel) |
| 94 | case *slack.ConnectedEvent: |
| 95 | b.si = ev.Info |
| 96 | b.channels.populateChannels(true) |
| 97 | b.users.populateUsers(true) |
| 98 | case *slack.InvalidAuthEvent: |
| 99 | b.Log.Fatalf("Invalid Token %#v", ev) |
| 100 | case *slack.ConnectionErrorEvent: |
| 101 | b.Log.Errorf("Connection failed %#v %#v", ev.Error(), ev.ErrorObj) |
| 102 | case *slack.MemberJoinedChannelEvent: |
| 103 | b.users.populateUser(ev.User) |
| 104 | case *slack.HelloEvent, *slack.LatencyReport, *slack.ConnectingEvent: |
| 105 | continue |
| 106 | case *slack.UserChangeEvent: |
| 107 | b.users.invalidateUser(ev.User.ID) |
| 108 | default: |
| 109 | b.Log.Debugf("Unhandled incoming event: %T", ev) |
| 110 | } |
| 111 | } |
| 112 | } |
| 113 | |
| 114 | func (b *Bslack) handleMatterHook(messages chan *config.Message) { |
| 115 | for { |
| 116 | message := b.mh.Receive() |
| 117 | b.Log.Debugf("receiving from matterhook (slack) %#v", message) |
| 118 | if message.UserName == "slackbot" { |
| 119 | continue |
| 120 | } |
| 121 | messages <- &config.Message{ |
| 122 | Username: message.UserName, |
| 123 | Text: message.Text, |
| 124 | Channel: message.ChannelName, |
| 125 | } |
| 126 | } |
| 127 | } |
| 128 | |
| 129 | // skipMessageEvent skips event that need to be skipped :-) |
| 130 | func (b *Bslack) skipMessageEvent(ev *slack.MessageEvent) bool { |
| 131 | switch ev.SubType { |
| 132 | case sChannelLeave, sChannelJoin: |
| 133 | return b.GetBool(noSendJoinConfig) |
| 134 | case sPinnedItem, sUnpinnedItem: |
| 135 | return true |
| 136 | case sChannelTopic, sChannelPurpose: |
| 137 | // Skip the event if our bot/user account changed the topic/purpose |
| 138 | if ev.User == b.si.User.ID { |
| 139 | return true |
| 140 | } |
| 141 | } |
| 142 | |
| 143 | // Check for our callback ID |
| 144 | hasOurCallbackID := false |
| 145 | if len(ev.Blocks.BlockSet) == 1 { |
| 146 | block, ok := ev.Blocks.BlockSet[0].(*slack.SectionBlock) |
| 147 | hasOurCallbackID = ok && block.BlockID == "matterbridge_"+b.uuid |
| 148 | } |
| 149 | |
| 150 | if ev.SubMessage != nil { |
| 151 | // It seems ev.SubMessage.Edited == nil when slack unfurls. |
| 152 | // Do not forward these messages. See Github issue #266. |
| 153 | if ev.SubMessage.ThreadTimestamp != ev.SubMessage.Timestamp && |
| 154 | ev.SubMessage.Edited == nil { |
| 155 | return true |
| 156 | } |
| 157 | // see hidden subtypes at https://api.slack.com/events/message |
| 158 | // these messages are sent when we add a message to a thread #709 |
| 159 | if ev.SubType == "message_replied" && ev.Hidden { |
| 160 | return true |
| 161 | } |
| 162 | if len(ev.SubMessage.Blocks.BlockSet) == 1 { |
| 163 | block, ok := ev.SubMessage.Blocks.BlockSet[0].(*slack.SectionBlock) |
| 164 | hasOurCallbackID = ok && block.BlockID == "matterbridge_"+b.uuid |
| 165 | } |
| 166 | } |
| 167 | |
| 168 | // Skip any messages that we made ourselves or from 'slackbot' (see #527). |
| 169 | if ev.Username == sSlackBotUser || |
| 170 | (b.rtm != nil && ev.Username == b.si.User.Name) || hasOurCallbackID { |
| 171 | return true |
| 172 | } |
| 173 | |
| 174 | if len(ev.Files) > 0 { |
| 175 | return b.filesCached(ev.Files) |
| 176 | } |
| 177 | return false |
| 178 | } |
| 179 | |
| 180 | func (b *Bslack) filesCached(files []slack.File) bool { |
| 181 | for i := range files { |
| 182 | if !b.fileCached(&files[i]) { |
| 183 | return false |
| 184 | } |
| 185 | } |
| 186 | return true |
| 187 | } |
| 188 | |
| 189 | // handleMessageEvent handles the message events. Together with any called sub-methods, |
| 190 | // this method implements the following event processing pipeline: |
| 191 | // |
| 192 | // 1. Check if the message should be ignored. |
| 193 | // NOTE: This is not actually part of the method below but is done just before it |
| 194 | // is called via the 'skipMessageEvent()' method. |
| 195 | // 2. Populate the Matterbridge message that will be sent to the router based on the |
| 196 | // received event and logic that is common to all events that are not skipped. |
| 197 | // 3. Detect and handle any message that is "status" related (think join channel, etc.). |
| 198 | // This might result in an early exit from the pipeline and passing of the |
| 199 | // pre-populated message to the Matterbridge router. |
| 200 | // 4. Handle the specific case of messages that edit existing messages depending on |
| 201 | // configuration. |
| 202 | // 5. Handle any attachments of the received event. |
| 203 | // 6. Check that the Matterbridge message that we end up with after at the end of the |
| 204 | // pipeline is valid before sending it to the Matterbridge router. |
| 205 | func (b *Bslack) handleMessageEvent(ev *slack.MessageEvent) (*config.Message, error) { |
| 206 | rmsg, err := b.populateReceivedMessage(ev) |
| 207 | if err != nil { |
| 208 | return nil, err |
| 209 | } |
| 210 | |
| 211 | // Handle some message types early. |
| 212 | if b.handleStatusEvent(ev, rmsg) { |
| 213 | return rmsg, nil |
| 214 | } |
| 215 | |
| 216 | b.handleAttachments(ev, rmsg) |
| 217 | |
| 218 | // Verify that we have the right information and the message |
| 219 | // is well-formed before sending it out to the router. |
| 220 | if len(ev.Files) == 0 && (rmsg.Text == "" || rmsg.Username == "") { |
| 221 | if ev.BotID != "" { |
| 222 | // This is probably a webhook we couldn't resolve. |
| 223 | return nil, fmt.Errorf("message handling resulted in an empty bot message (probably an incoming webhook we couldn't resolve): %#v", ev) |
| 224 | } |
| 225 | if ev.SubMessage != nil { |
| 226 | return nil, fmt.Errorf("message handling resulted in an empty message: %#v with submessage %#v", ev, ev.SubMessage) |
| 227 | } |
| 228 | return nil, fmt.Errorf("message handling resulted in an empty message: %#v", ev) |
| 229 | } |
| 230 | return rmsg, nil |
| 231 | } |
| 232 | |
| 233 | func (b *Bslack) handleFileDeletedEvent(ev *slack.FileDeletedEvent) (*config.Message, error) { |
| 234 | if rawChannel, ok := b.cache.Get(cfileDownloadChannel + ev.FileID); ok { |
| 235 | channel, err := b.channels.getChannelByID(rawChannel.(string)) |
| 236 | if err != nil { |
| 237 | return nil, err |
| 238 | } |
| 239 | |
| 240 | return &config.Message{ |
| 241 | Event: config.EventFileDelete, |
| 242 | Text: config.EventFileDelete, |
| 243 | Channel: channel.Name, |
| 244 | Account: b.Account, |
| 245 | ID: ev.FileID, |
| 246 | Protocol: b.Protocol, |
| 247 | }, nil |
| 248 | } |
| 249 | |
| 250 | return nil, fmt.Errorf("channel ID for file ID %s not found", ev.FileID) |
| 251 | } |
| 252 | |
| 253 | func (b *Bslack) handleStatusEvent(ev *slack.MessageEvent, rmsg *config.Message) bool { |
| 254 | switch ev.SubType { |
| 255 | case sChannelJoined, sMemberJoined: |
| 256 | // There's no further processing needed on channel events |
| 257 | // so we return 'true'. |
| 258 | return true |
| 259 | case sChannelJoin, sChannelLeave: |
| 260 | rmsg.Username = sSystemUser |
| 261 | rmsg.Event = config.EventJoinLeave |
| 262 | case sChannelTopic, sChannelPurpose: |
| 263 | b.channels.populateChannels(false) |
| 264 | rmsg.Event = config.EventTopicChange |
| 265 | case sMessageChanged: |
| 266 | rmsg.Text = ev.SubMessage.Text |
| 267 | // handle deleted thread starting messages |
| 268 | if ev.SubMessage.Text == "This message was deleted." { |
| 269 | rmsg.Event = config.EventMsgDelete |
| 270 | return true |
| 271 | } |
| 272 | case sMessageDeleted: |
| 273 | rmsg.Text = config.EventMsgDelete |
| 274 | rmsg.Event = config.EventMsgDelete |
| 275 | rmsg.ID = ev.DeletedTimestamp |
| 276 | // If a message is being deleted we do not need to process |
| 277 | // the event any further so we return 'true'. |
| 278 | return true |
| 279 | case sMeMessage: |
| 280 | rmsg.Event = config.EventUserAction |
| 281 | } |
| 282 | return false |
| 283 | } |
| 284 | |
| 285 | func getMessageTitle(attach *slack.Attachment) string { |
| 286 | if attach.TitleLink != "" { |
| 287 | return fmt.Sprintf("[%s](%s)\n", attach.Title, attach.TitleLink) |
| 288 | } |
| 289 | return attach.Title |
| 290 | } |
| 291 | |
| 292 | func (b *Bslack) handleAttachments(ev *slack.MessageEvent, rmsg *config.Message) { |
| 293 | // File comments are set by the system (because there is no username given). |
| 294 | if ev.SubType == sFileComment { |
| 295 | rmsg.Username = sSystemUser |
| 296 | } |
| 297 | |
| 298 | // See if we have some text in the attachments. |
| 299 | if rmsg.Text == "" { |
| 300 | for i, attach := range ev.Attachments { |
| 301 | if attach.Text != "" { |
| 302 | if attach.Title != "" { |
| 303 | rmsg.Text = getMessageTitle(&ev.Attachments[i]) |
| 304 | } |
| 305 | rmsg.Text += attach.Text |
| 306 | if attach.Footer != "" { |
| 307 | rmsg.Text += "\n\n" + attach.Footer |
| 308 | } |
| 309 | } else { |
| 310 | rmsg.Text = attach.Fallback |
| 311 | } |
| 312 | } |
| 313 | } |
| 314 | |
| 315 | // Save the attachments, so that we can send them to other slack (compatible) bridges. |
| 316 | if len(ev.Attachments) > 0 { |
| 317 | rmsg.Extra[sSlackAttachment] = append(rmsg.Extra[sSlackAttachment], ev.Attachments) |
| 318 | } |
| 319 | |
| 320 | // If we have files attached, download them (in memory) and put a pointer to it in msg.Extra. |
| 321 | for i := range ev.Files { |
| 322 | // keep reference in cache on which channel we added this file |
| 323 | b.cache.Add(cfileDownloadChannel+ev.Files[i].ID, ev.Channel) |
| 324 | if err := b.handleDownloadFile(rmsg, &ev.Files[i], false); err != nil { |
| 325 | b.Log.Errorf("Could not download incoming file: %#v", err) |
| 326 | } |
| 327 | } |
| 328 | } |
| 329 | |
| 330 | func (b *Bslack) handleTypingEvent(ev *slack.UserTypingEvent) (*config.Message, error) { |
| 331 | if ev.User == b.si.User.ID { |
| 332 | return nil, ErrEventIgnored |
| 333 | } |
| 334 | channelInfo, err := b.channels.getChannelByID(ev.Channel) |
| 335 | if err != nil { |
| 336 | return nil, err |
| 337 | } |
| 338 | return &config.Message{ |
| 339 | Channel: channelInfo.Name, |
| 340 | Account: b.Account, |
| 341 | Event: config.EventUserTyping, |
| 342 | }, nil |
| 343 | } |
| 344 | |
| 345 | // handleDownloadFile handles file download |
| 346 | func (b *Bslack) handleDownloadFile(rmsg *config.Message, file *slack.File, retry bool) error { |
| 347 | if b.fileCached(file) { |
| 348 | return nil |
| 349 | } |
| 350 | // Check that the file is neither too large nor blacklisted. |
| 351 | if err := helper.HandleDownloadSize(b.Log, rmsg, file.Name, int64(file.Size), b.General); err != nil { |
| 352 | b.Log.WithError(err).Infof("Skipping download of incoming file.") |
| 353 | return nil |
| 354 | } |
| 355 | |
| 356 | // Actually download the file. |
| 357 | data, err := helper.DownloadFileAuth(file.URLPrivateDownload, "Bearer "+b.GetString(tokenConfig)) |
| 358 | if err != nil { |
| 359 | return fmt.Errorf("download %s failed %#v", file.URLPrivateDownload, err) |
| 360 | } |
| 361 | |
| 362 | if len(*data) != file.Size && !retry { |
| 363 | b.Log.Debugf("Data size (%d) is not equal to size declared (%d)\n", len(*data), file.Size) |
| 364 | time.Sleep(1 * time.Second) |
| 365 | return b.handleDownloadFile(rmsg, file, true) |
| 366 | } |
| 367 | |
| 368 | // If a comment is attached to the file(s) it is in the 'Text' field of the Slack messge event |
| 369 | // and should be added as comment to only one of the files. We reset the 'Text' field to ensure |
| 370 | // that the comment is not duplicated. |
| 371 | comment := rmsg.Text |
| 372 | rmsg.Text = "" |
| 373 | helper.HandleDownloadData2(b.Log, rmsg, file.Name, file.ID, comment, file.URLPrivateDownload, data, b.General) |
| 374 | return nil |
| 375 | } |
| 376 | |
| 377 | // handleGetChannelMembers handles messages containing the GetChannelMembers event |
| 378 | // Sends a message to the router containing *config.ChannelMembers |
| 379 | func (b *Bslack) handleGetChannelMembers(rmsg *config.Message) bool { |
| 380 | if rmsg.Event != config.EventGetChannelMembers { |
| 381 | return false |
| 382 | } |
| 383 | |
| 384 | cMembers := b.channels.getChannelMembers(b.users) |
| 385 | |
| 386 | extra := make(map[string][]interface{}) |
| 387 | extra[config.EventGetChannelMembers] = append(extra[config.EventGetChannelMembers], cMembers) |
| 388 | msg := config.Message{ |
| 389 | Extra: extra, |
| 390 | Event: config.EventGetChannelMembers, |
| 391 | Account: b.Account, |
| 392 | } |
| 393 | |
| 394 | b.Log.Debugf("sending msg to remote %#v", msg) |
| 395 | b.Remote <- msg |
| 396 | |
| 397 | return true |
| 398 | } |
| 399 | |
| 400 | // fileCached implements Matterbridge's caching logic for files |
| 401 | // shared via Slack. |
| 402 | // |
| 403 | // We consider that a file was cached if its ID was added in the last minute or |
| 404 | // it's name was registered in the last 10 seconds. This ensures that an |
| 405 | // identically named file but with different content will be uploaded correctly |
| 406 | // (the assumption is that such name collisions will not occur within the given |
| 407 | // timeframes). |
| 408 | func (b *Bslack) fileCached(file *slack.File) bool { |
| 409 | if ts, ok := b.cache.Get("file" + file.ID); ok && time.Since(ts.(time.Time)) < time.Minute { |
| 410 | return true |
| 411 | } else if ts, ok = b.cache.Get("filename" + file.Name); ok && time.Since(ts.(time.Time)) < 10*time.Second { |
| 412 | return true |
| 413 | } |
| 414 | return false |
| 415 | } |
| 416 | |