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