1package bslack 2 3import ( 4 "bytes" 5 "errors" 6 "fmt" 7 "strings" 8 "sync" 9 "time" 10 11 "github.com/42wim/matterbridge/bridge" 12 "github.com/42wim/matterbridge/bridge/config" 13 "github.com/42wim/matterbridge/bridge/helper" 14 "github.com/42wim/matterbridge/matterhook" 15 lru "github.com/hashicorp/golang-lru" 16 "github.com/rs/xid" 17 "github.com/slack-go/slack" 18) 19 20type Bslack struct { 21 sync.RWMutex 22 *bridge.Config 23 24 mh *matterhook.Client 25 sc *slack.Client 26 rtm *slack.RTM 27 si *slack.Info 28 29 cache *lru.Cache 30 uuid string 31 useChannelID bool 32 33 channels *channels 34 users *users 35 legacy bool 36} 37 38const ( 39 sHello = "hello" 40 sChannelJoin = "channel_join" 41 sChannelLeave = "channel_leave" 42 sChannelJoined = "channel_joined" 43 sMemberJoined = "member_joined_channel" 44 sMessageChanged = "message_changed" 45 sMessageDeleted = "message_deleted" 46 sSlackAttachment = "slack_attachment" 47 sPinnedItem = "pinned_item" 48 sUnpinnedItem = "unpinned_item" 49 sChannelTopic = "channel_topic" 50 sChannelPurpose = "channel_purpose" 51 sFileComment = "file_comment" 52 sMeMessage = "me_message" 53 sUserTyping = "user_typing" 54 sLatencyReport = "latency_report" 55 sSystemUser = "system" 56 sSlackBotUser = "slackbot" 57 58 tokenConfig = "Token" 59 incomingWebhookConfig = "WebhookBindAddress" 60 outgoingWebhookConfig = "WebhookURL" 61 skipTLSConfig = "SkipTLSVerify" 62 useNickPrefixConfig = "PrefixMessagesWithNick" 63 editDisableConfig = "EditDisable" 64 editSuffixConfig = "EditSuffix" 65 iconURLConfig = "iconurl" 66 noSendJoinConfig = "nosendjoinpart" 67 messageLength = 3000 68) 69 70func New(cfg *bridge.Config) bridge.Bridger { 71 // Print a deprecation warning for legacy non-bot tokens (#527). 72 token := cfg.GetString(tokenConfig) 73 if token != "" && !strings.HasPrefix(token, "xoxb") { 74 cfg.Log.Warn("Non-bot token detected. It is STRONGLY recommended to use a proper bot-token instead.") 75 cfg.Log.Warn("Legacy tokens may be deprecated by Slack at short notice. See the Matterbridge GitHub wiki for a migration guide.") 76 cfg.Log.Warn("See https://github.com/42wim/matterbridge/wiki/Slack-bot-setup") 77 return NewLegacy(cfg) 78 } 79 return newBridge(cfg) 80} 81 82func newBridge(cfg *bridge.Config) *Bslack { 83 newCache, err := lru.New(5000) 84 if err != nil { 85 cfg.Log.Fatalf("Could not create LRU cache for Slack bridge: %v", err) 86 } 87 b := &Bslack{ 88 Config: cfg, 89 uuid: xid.New().String(), 90 cache: newCache, 91 } 92 return b 93} 94 95func (b *Bslack) Command(cmd string) string { 96 return "" 97} 98 99func (b *Bslack) Connect() error { 100 b.RLock() 101 defer b.RUnlock() 102 103 if b.GetString(incomingWebhookConfig) == "" && b.GetString(outgoingWebhookConfig) == "" && b.GetString(tokenConfig) == "" { 104 return errors.New("no connection method found: WebhookBindAddress, WebhookURL or Token need to be configured") 105 } 106 107 // If we have a token we use the Slack websocket-based RTM for both sending and receiving. 108 if token := b.GetString(tokenConfig); token != "" { 109 b.Log.Info("Connecting using token") 110 111 b.sc = slack.New(token, slack.OptionDebug(b.GetBool("Debug"))) 112 113 b.channels = newChannelManager(b.Log, b.sc) 114 b.users = newUserManager(b.Log, b.sc) 115 116 b.rtm = b.sc.NewRTM() 117 go b.rtm.ManageConnection() 118 go b.handleSlack() 119 return nil 120 } 121 122 // In absence of a token we fall back to incoming and outgoing Webhooks. 123 b.mh = matterhook.New( 124 "", 125 matterhook.Config{ 126 InsecureSkipVerify: b.GetBool("SkipTLSVerify"), 127 DisableServer: true, 128 }, 129 ) 130 if b.GetString(outgoingWebhookConfig) != "" { 131 b.Log.Info("Using specified webhook for outgoing messages.") 132 b.mh.Url = b.GetString(outgoingWebhookConfig) 133 } 134 if b.GetString(incomingWebhookConfig) != "" { 135 b.Log.Info("Setting up local webhook for incoming messages.") 136 b.mh.BindAddress = b.GetString(incomingWebhookConfig) 137 b.mh.DisableServer = false 138 go b.handleSlack() 139 } 140 return nil 141} 142 143func (b *Bslack) Disconnect() error { 144 return b.rtm.Disconnect() 145} 146 147// JoinChannel only acts as a verification method that checks whether Matterbridge's 148// Slack integration is already member of the channel. This is because Slack does not 149// allow apps or bots to join channels themselves and they need to be invited 150// manually by a user. 151func (b *Bslack) JoinChannel(channel config.ChannelInfo) error { 152 // We can only join a channel through the Slack API. 153 if b.sc == nil { 154 return nil 155 } 156 157 // try to join a channel when in legacy 158 if b.legacy { 159 _, _, _, err := b.sc.JoinConversation(channel.Name) 160 if err != nil { 161 switch err.Error() { 162 case "name_taken", "restricted_action": 163 case "default": 164 return err 165 } 166 } 167 } 168 169 b.channels.populateChannels(false) 170 171 channelInfo, err := b.channels.getChannel(channel.Name) 172 if err != nil { 173 return fmt.Errorf("could not join channel: %#v", err) 174 } 175 176 if strings.HasPrefix(channel.Name, "ID:") { 177 b.useChannelID = true 178 channel.Name = channelInfo.Name 179 } 180 181 // we can't join a channel unless we are using legacy tokens #651 182 if !channelInfo.IsMember && !b.legacy { 183 return fmt.Errorf("slack integration that matterbridge is using is not member of channel '%s', please add it manually", channelInfo.Name) 184 } 185 return nil 186} 187 188func (b *Bslack) Reload(cfg *bridge.Config) (string, error) { 189 return "", nil 190} 191 192func (b *Bslack) Send(msg config.Message) (string, error) { 193 // Too noisy to log like other events 194 if msg.Event != config.EventUserTyping { 195 b.Log.Debugf("=> Receiving %#v", msg) 196 } 197 198 msg.Text = helper.ClipMessage(msg.Text, messageLength, b.GetString("MessageClipped")) 199 msg.Text = b.replaceCodeFence(msg.Text) 200 201 // Make a action /me of the message 202 if msg.Event == config.EventUserAction { 203 msg.Text = "_" + msg.Text + "_" 204 } 205 206 // Use webhook to send the message 207 if b.GetString(outgoingWebhookConfig) != "" && b.GetString(tokenConfig) == "" { 208 return "", b.sendWebhook(msg) 209 } 210 return b.sendRTM(msg) 211} 212 213// sendWebhook uses the configured WebhookURL to send the message 214func (b *Bslack) sendWebhook(msg config.Message) error { 215 // Skip events. 216 if msg.Event != "" { 217 return nil 218 } 219 220 if b.GetBool(useNickPrefixConfig) { 221 msg.Text = msg.Username + msg.Text 222 } 223 224 if msg.Extra != nil { 225 // This sends a message only if we received a config.EVENT_FILE_FAILURE_SIZE. 226 for _, rmsg := range helper.HandleExtra(&msg, b.General) { 227 rmsg := rmsg // scopelint 228 iconURL := config.GetIconURL(&rmsg, b.GetString(iconURLConfig)) 229 matterMessage := matterhook.OMessage{ 230 IconURL: iconURL, 231 Channel: msg.Channel, 232 UserName: rmsg.Username, 233 Text: rmsg.Text, 234 } 235 if err := b.mh.Send(matterMessage); err != nil { 236 b.Log.Errorf("Failed to send message: %v", err) 237 } 238 } 239 240 // Webhook doesn't support file uploads, so we add the URL manually. 241 for _, f := range msg.Extra["file"] { 242 fi, ok := f.(config.FileInfo) 243 if !ok { 244 b.Log.Errorf("Received a file with unexpected content: %#v", f) 245 continue 246 } 247 if fi.URL != "" { 248 msg.Text += " " + fi.URL 249 } 250 } 251 } 252 253 // If we have native slack_attachments add them. 254 var attachs []slack.Attachment 255 for _, attach := range msg.Extra[sSlackAttachment] { 256 attachs = append(attachs, attach.([]slack.Attachment)...) 257 } 258 259 iconURL := config.GetIconURL(&msg, b.GetString(iconURLConfig)) 260 matterMessage := matterhook.OMessage{ 261 IconURL: iconURL, 262 Attachments: attachs, 263 Channel: msg.Channel, 264 UserName: msg.Username, 265 Text: msg.Text, 266 } 267 if msg.Avatar != "" { 268 matterMessage.IconURL = msg.Avatar 269 } 270 if err := b.mh.Send(matterMessage); err != nil { 271 b.Log.Errorf("Failed to send message via webhook: %#v", err) 272 return err 273 } 274 return nil 275} 276 277func (b *Bslack) sendRTM(msg config.Message) (string, error) { 278 // Handle channelmember messages. 279 if handled := b.handleGetChannelMembers(&msg); handled { 280 return "", nil 281 } 282 283 channelInfo, err := b.channels.getChannel(msg.Channel) 284 if err != nil { 285 return "", fmt.Errorf("could not send message: %v", err) 286 } 287 if msg.Event == config.EventUserTyping { 288 if b.GetBool("ShowUserTyping") { 289 b.rtm.SendMessage(b.rtm.NewTypingMessage(channelInfo.ID)) 290 } 291 return "", nil 292 } 293 294 var handled bool 295 296 // Handle topic/purpose updates. 297 if handled, err = b.handleTopicOrPurpose(&msg, channelInfo); handled { 298 return "", err 299 } 300 301 // Handle prefix hint for unthreaded messages. 302 if msg.ParentNotFound() { 303 msg.ParentID = "" 304 msg.Text = fmt.Sprintf("[thread]: %s", msg.Text) 305 } 306 307 // Handle message deletions. 308 if handled, err = b.deleteMessage(&msg, channelInfo); handled { 309 return msg.ID, err 310 } 311 312 // Prepend nickname if configured. 313 if b.GetBool(useNickPrefixConfig) { 314 msg.Text = msg.Username + msg.Text 315 } 316 317 // Handle message edits. 318 if handled, err = b.editMessage(&msg, channelInfo); handled { 319 return msg.ID, err 320 } 321 322 // Upload a file if it exists. 323 if msg.Extra != nil { 324 extraMsgs := helper.HandleExtra(&msg, b.General) 325 for i := range extraMsgs { 326 rmsg := &extraMsgs[i] 327 rmsg.Text = rmsg.Username + rmsg.Text 328 _, err = b.postMessage(rmsg, channelInfo) 329 if err != nil { 330 b.Log.Error(err) 331 } 332 } 333 // Upload files if necessary (from Slack, Telegram or Mattermost). 334 b.uploadFile(&msg, channelInfo.ID) 335 } 336 337 // Post message. 338 return b.postMessage(&msg, channelInfo) 339} 340 341func (b *Bslack) updateTopicOrPurpose(msg *config.Message, channelInfo *slack.Channel) error { 342 var updateFunc func(channelID string, value string) (*slack.Channel, error) 343 344 incomingChangeType, text := b.extractTopicOrPurpose(msg.Text) 345 switch incomingChangeType { 346 case "topic": 347 updateFunc = b.rtm.SetTopicOfConversation 348 case "purpose": 349 updateFunc = b.rtm.SetPurposeOfConversation 350 default: 351 b.Log.Errorf("Unhandled type received from extractTopicOrPurpose: %s", incomingChangeType) 352 return nil 353 } 354 for { 355 _, err := updateFunc(channelInfo.ID, text) 356 if err == nil { 357 return nil 358 } 359 if err = handleRateLimit(b.Log, err); err != nil { 360 return err 361 } 362 } 363} 364 365// handles updating topic/purpose and determining whether to further propagate update messages. 366func (b *Bslack) handleTopicOrPurpose(msg *config.Message, channelInfo *slack.Channel) (bool, error) { 367 if msg.Event != config.EventTopicChange { 368 return false, nil 369 } 370 371 if b.GetBool("SyncTopic") { 372 return true, b.updateTopicOrPurpose(msg, channelInfo) 373 } 374 375 // Pass along to normal message handlers. 376 if b.GetBool("ShowTopicChange") { 377 return false, nil 378 } 379 380 // Swallow message as handled no-op. 381 return true, nil 382} 383 384func (b *Bslack) deleteMessage(msg *config.Message, channelInfo *slack.Channel) (bool, error) { 385 if msg.Event != config.EventMsgDelete { 386 return false, nil 387 } 388 389 // Some protocols echo deletes, but with an empty ID. 390 if msg.ID == "" { 391 return true, nil 392 } 393 394 for { 395 _, _, err := b.rtm.DeleteMessage(channelInfo.ID, msg.ID) 396 if err == nil { 397 return true, nil 398 } 399 400 if err = handleRateLimit(b.Log, err); err != nil { 401 b.Log.Errorf("Failed to delete user message from Slack: %#v", err) 402 return true, err 403 } 404 } 405} 406 407func (b *Bslack) editMessage(msg *config.Message, channelInfo *slack.Channel) (bool, error) { 408 if msg.ID == "" { 409 return false, nil 410 } 411 messageOptions := b.prepareMessageOptions(msg) 412 for { 413 _, _, _, err := b.rtm.UpdateMessage(channelInfo.ID, msg.ID, messageOptions...) 414 if err == nil { 415 return true, nil 416 } 417 418 if err = handleRateLimit(b.Log, err); err != nil { 419 b.Log.Errorf("Failed to edit user message on Slack: %#v", err) 420 return true, err 421 } 422 } 423} 424 425func (b *Bslack) postMessage(msg *config.Message, channelInfo *slack.Channel) (string, error) { 426 // don't post empty messages 427 if msg.Text == "" { 428 return "", nil 429 } 430 messageOptions := b.prepareMessageOptions(msg) 431 for { 432 _, id, err := b.rtm.PostMessage(channelInfo.ID, messageOptions...) 433 if err == nil { 434 return id, nil 435 } 436 437 if err = handleRateLimit(b.Log, err); err != nil { 438 b.Log.Errorf("Failed to sent user message to Slack: %#v", err) 439 return "", err 440 } 441 } 442} 443 444// uploadFile handles native upload of files 445func (b *Bslack) uploadFile(msg *config.Message, channelID string) { 446 for _, f := range msg.Extra["file"] { 447 fi, ok := f.(config.FileInfo) 448 if !ok { 449 b.Log.Errorf("Received a file with unexpected content: %#v", f) 450 continue 451 } 452 if msg.Text == fi.Comment { 453 msg.Text = "" 454 } 455 // Because the result of the UploadFile is slower than the MessageEvent from slack 456 // we can't match on the file ID yet, so we have to match on the filename too. 457 ts := time.Now() 458 b.Log.Debugf("Adding file %s to cache at %s with timestamp", fi.Name, ts.String()) 459 b.cache.Add("filename"+fi.Name, ts) 460 initialComment := fmt.Sprintf("File from %s", msg.Username) 461 if fi.Comment != "" { 462 initialComment += fmt.Sprintf("with comment: %s", fi.Comment) 463 } 464 res, err := b.sc.UploadFile(slack.FileUploadParameters{ 465 Reader: bytes.NewReader(*fi.Data), 466 Filename: fi.Name, 467 Channels: []string{channelID}, 468 InitialComment: initialComment, 469 ThreadTimestamp: msg.ParentID, 470 }) 471 if err != nil { 472 b.Log.Errorf("uploadfile %#v", err) 473 return 474 } 475 if res.ID != "" { 476 b.Log.Debugf("Adding file ID %s to cache with timestamp %s", res.ID, ts.String()) 477 b.cache.Add("file"+res.ID, ts) 478 } 479 } 480} 481 482func (b *Bslack) prepareMessageOptions(msg *config.Message) []slack.MsgOption { 483 params := slack.NewPostMessageParameters() 484 if b.GetBool(useNickPrefixConfig) { 485 params.AsUser = true 486 } 487 params.Username = msg.Username 488 params.LinkNames = 1 // replace mentions 489 params.IconURL = config.GetIconURL(msg, b.GetString(iconURLConfig)) 490 params.ThreadTimestamp = msg.ParentID 491 if msg.Avatar != "" { 492 params.IconURL = msg.Avatar 493 } 494 495 var attachments []slack.Attachment 496 // add file attachments 497 attachments = append(attachments, b.createAttach(msg.Extra)...) 498 // add slack attachments (from another slack bridge) 499 if msg.Extra != nil { 500 for _, attach := range msg.Extra[sSlackAttachment] { 501 attachments = append(attachments, attach.([]slack.Attachment)...) 502 } 503 } 504 505 var opts []slack.MsgOption 506 opts = append(opts, 507 // provide regular text field (fallback used in Slack notifications, etc.) 508 slack.MsgOptionText(msg.Text, false), 509 510 // add a callback ID so we can see we created it 511 slack.MsgOptionBlocks(slack.NewSectionBlock( 512 slack.NewTextBlockObject(slack.MarkdownType, msg.Text, false, false), 513 nil, nil, 514 slack.SectionBlockOptionBlockID("matterbridge_"+b.uuid), 515 )), 516 517 slack.MsgOptionEnableLinkUnfurl(), 518 ) 519 opts = append(opts, slack.MsgOptionAttachments(attachments...)) 520 opts = append(opts, slack.MsgOptionPostMessageParameters(params)) 521 return opts 522} 523 524func (b *Bslack) createAttach(extra map[string][]interface{}) []slack.Attachment { 525 var attachements []slack.Attachment 526 for _, v := range extra["attachments"] { 527 entry := v.(map[string]interface{}) 528 s := slack.Attachment{ 529 Fallback: extractStringField(entry, "fallback"), 530 Color: extractStringField(entry, "color"), 531 Pretext: extractStringField(entry, "pretext"), 532 AuthorName: extractStringField(entry, "author_name"), 533 AuthorLink: extractStringField(entry, "author_link"), 534 AuthorIcon: extractStringField(entry, "author_icon"), 535 Title: extractStringField(entry, "title"), 536 TitleLink: extractStringField(entry, "title_link"), 537 Text: extractStringField(entry, "text"), 538 ImageURL: extractStringField(entry, "image_url"), 539 ThumbURL: extractStringField(entry, "thumb_url"), 540 Footer: extractStringField(entry, "footer"), 541 FooterIcon: extractStringField(entry, "footer_icon"), 542 } 543 attachements = append(attachements, s) 544 } 545 return attachements 546} 547 548func extractStringField(data map[string]interface{}, field string) string { 549 if rawValue, found := data[field]; found { 550 if value, ok := rawValue.(string); ok { 551 return value 552 } 553 } 554 return "" 555} 556