Thumbnail

rani/matterbridge.git

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

Viewing file on branch master

1package bslack
2
3import (
4 "fmt"
5 "regexp"
6 "strings"
7 "time"
8
9 "github.com/matterbridge-org/matterbridge/bridge/config"
10 "github.com/sirupsen/logrus"
11 "github.com/slack-go/slack"
12)
13
14// populateReceivedMessage shapes the initial Matterbridge message that we will forward to the
15// router before we apply message-dependent modifications.
16func (b *Bslack) populateReceivedMessage(ev *slack.MessageEvent) (*config.Message, error) {
17 // Use our own func because rtm.GetChannelInfo doesn't work for private channels.
18 channel, err := b.channels.getChannelByID(ev.Channel)
19 if err != nil {
20 return nil, err
21 }
22
23 rmsg := &config.Message{
24 Text: ev.Text,
25 Channel: channel.Name,
26 Account: b.Account,
27 ID: ev.Timestamp,
28 Extra: make(map[string][]interface{}),
29 ParentID: ev.ThreadTimestamp,
30 Protocol: b.Protocol,
31 }
32 if b.useChannelID {
33 rmsg.Channel = "ID:" + channel.ID
34 }
35
36 // Handle 'edit' messages.
37 if ev.SubMessage != nil && !b.GetBool(editDisableConfig) {
38 rmsg.ID = ev.SubMessage.Timestamp
39 if ev.SubMessage.ThreadTimestamp != ev.SubMessage.Timestamp {
40 b.Log.Debugf("SubMessage %#v", ev.SubMessage)
41 rmsg.Text = ev.SubMessage.Text + b.GetString(editSuffixConfig)
42 }
43 }
44
45 // For edits, only submessage has thread ts.
46 // Ensures edits to threaded messages maintain their prefix hint on the
47 // unthreaded end.
48 if ev.SubMessage != nil {
49 rmsg.ParentID = ev.SubMessage.ThreadTimestamp
50 }
51
52 if err = b.populateMessageWithUserInfo(ev, rmsg); err != nil {
53 return nil, err
54 }
55 return rmsg, err
56}
57
58func (b *Bslack) populateMessageWithUserInfo(ev *slack.MessageEvent, rmsg *config.Message) error {
59 if ev.SubType == sMessageDeleted || ev.SubType == sFileComment {
60 return nil
61 }
62
63 // First, deal with bot-originating messages but only do so when not using webhooks: we
64 // would not be able to distinguish which bot would be sending them.
65 if err := b.populateMessageWithBotInfo(ev, rmsg); err != nil {
66 return err
67 }
68
69 // Second, deal with "real" users if we have the necessary information.
70 var userID string
71 switch {
72 case ev.User != "":
73 userID = ev.User
74 case ev.SubMessage != nil && ev.SubMessage.User != "":
75 userID = ev.SubMessage.User
76 default:
77 return nil
78 }
79
80 user := b.users.getUser(userID)
81 if user == nil {
82 return fmt.Errorf("could not find information for user with id %s", ev.User)
83 }
84
85 rmsg.UserID = user.ID
86 rmsg.Username = user.Name
87 if user.Profile.DisplayName != "" {
88 rmsg.Username = user.Profile.DisplayName
89 }
90 if b.GetBool("UseFullName") && user.Profile.RealName != "" {
91 rmsg.Username = user.Profile.RealName
92 }
93 return nil
94}
95
96func (b *Bslack) populateMessageWithBotInfo(ev *slack.MessageEvent, rmsg *config.Message) error {
97 if ev.BotID == "" || b.GetString(outgoingWebhookConfig) != "" {
98 return nil
99 }
100
101 var err error
102 var bot *slack.Bot
103 for {
104 bot, err = b.rtm.GetBotInfo(slack.GetBotInfoParameters{
105 Bot: ev.BotID,
106 })
107 if err == nil {
108 break
109 }
110
111 if err = handleRateLimit(b.Log, err); err != nil {
112 b.Log.Errorf("Could not retrieve bot information: %#v", err)
113 return err
114 }
115 }
116 b.Log.Debugf("Found bot %#v", bot)
117
118 if bot.Name != "" {
119 rmsg.Username = bot.Name
120 if ev.Username != "" {
121 rmsg.Username = ev.Username
122 }
123 rmsg.UserID = bot.ID
124 }
125 return nil
126}
127
128var (
129 mentionRE = regexp.MustCompile(`<@([a-zA-Z0-9]+)>`)
130 channelRE = regexp.MustCompile(`<#[a-zA-Z0-9]+\|(.+?)>`)
131 variableRE = regexp.MustCompile(`<!((?:subteam\^)?[a-zA-Z0-9]+)(?:\|@?(.+?))?>`)
132 urlRE = regexp.MustCompile(`<([^<\|]+)\|([^>]+)>`)
133 codeFenceRE = regexp.MustCompile(`(?m)^` + "```" + `\w+$`)
134 topicOrPurposeRE = regexp.MustCompile(`(?s)(@.+) (cleared|set)(?: the)? channel (topic|purpose)(?:: (.*))?`)
135)
136
137func (b *Bslack) extractTopicOrPurpose(text string) (string, string) {
138 r := topicOrPurposeRE.FindStringSubmatch(text)
139 if len(r) == 5 {
140 action, updateType, extracted := r[2], r[3], r[4]
141 switch action {
142 case "set":
143 return updateType, extracted
144 case "cleared":
145 return updateType, ""
146 }
147 }
148 b.Log.Warnf("Encountered channel topic or purpose change message with unexpected format: %s", text)
149 return "unknown", ""
150}
151
152// @see https://api.slack.com/docs/message-formatting#linking_to_channels_and_users
153func (b *Bslack) replaceMention(text string) string {
154 replaceFunc := func(match string) string {
155 userID := strings.Trim(match, "@<>")
156 if username := b.users.getUsername(userID); userID != "" {
157 return "@" + username
158 }
159 return match
160 }
161 return mentionRE.ReplaceAllStringFunc(text, replaceFunc)
162}
163
164// @see https://api.slack.com/docs/message-formatting#linking_to_channels_and_users
165func (b *Bslack) replaceChannel(text string) string {
166 for _, r := range channelRE.FindAllStringSubmatch(text, -1) {
167 text = strings.Replace(text, r[0], "#"+r[1], 1)
168 }
169 return text
170}
171
172// @see https://api.slack.com/docs/message-formatting#variables
173func (b *Bslack) replaceVariable(text string) string {
174 for _, r := range variableRE.FindAllStringSubmatch(text, -1) {
175 if r[2] != "" {
176 text = strings.Replace(text, r[0], "@"+r[2], 1)
177 } else {
178 text = strings.Replace(text, r[0], "@"+r[1], 1)
179 }
180 }
181 return text
182}
183
184// @see https://api.slack.com/docs/message-formatting#linking_to_urls
185func (b *Bslack) replaceURL(text string) string {
186 return urlRE.ReplaceAllString(text, "[${2}](${1})")
187}
188
189func (b *Bslack) replaceb0rkedMarkDown(text string) string {
190 // taken from https://github.com/mattermost/mattermost-server/blob/master/app/slackimport.go
191 //
192 regexReplaceAllString := []struct {
193 regex *regexp.Regexp
194 rpl string
195 }{
196 // bold
197 {
198 regexp.MustCompile(`(^|[\s.;,])\*(\S[^*\n]+)\*`),
199 "$1**$2**",
200 },
201 // strikethrough
202 {
203 regexp.MustCompile(`(^|[\s.;,])\~(\S[^~\n]+)\~`),
204 "$1~~$2~~",
205 },
206 // single paragraph blockquote
207 // Slack converts > character to &gt;
208 {
209 regexp.MustCompile(`(?sm)^&gt;`),
210 ">",
211 },
212 }
213 for _, rule := range regexReplaceAllString {
214 text = rule.regex.ReplaceAllString(text, rule.rpl)
215 }
216 return text
217}
218
219func (b *Bslack) replaceCodeFence(text string) string {
220 return codeFenceRE.ReplaceAllString(text, "```")
221}
222
223// getUsersInConversation returns an array of userIDs that are members of channelID
224func (b *Bslack) getUsersInConversation(channelID string) ([]string, error) {
225 channelMembers := []string{}
226 for {
227 queryParams := &slack.GetUsersInConversationParameters{
228 ChannelID: channelID,
229 }
230
231 members, nextCursor, err := b.sc.GetUsersInConversation(queryParams)
232 if err != nil {
233 if err = handleRateLimit(b.Log, err); err != nil {
234 return channelMembers, fmt.Errorf("Could not retrieve users in channels: %#v", err)
235 }
236 continue
237 }
238
239 channelMembers = append(channelMembers, members...)
240
241 if nextCursor == "" {
242 break
243 }
244 queryParams.Cursor = nextCursor
245 }
246 return channelMembers, nil
247}
248
249func handleRateLimit(log *logrus.Entry, err error) error {
250 rateLimit, ok := err.(*slack.RateLimitedError)
251 if !ok {
252 return err
253 }
254 log.Infof("Rate-limited by Slack. Sleeping for %v", rateLimit.RetryAfter)
255 time.Sleep(rateLimit.RetryAfter)
256 return nil
257}
258