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