Thumbnail

rani/matterbridge.git

Clone URL: https://git.buni.party/rani/matterbridge.git

Viewing file on branch master

1package bslack
2
3import (
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
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 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
50func (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
114func (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 :-)
130func (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
180func (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.
205func (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
233func (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
253func (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
285func 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
292func (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
330func (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
346func (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
379func (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).
408func (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