| 1 | package bdiscord |
| 2 | |
| 3 | import ( |
| 4 | "time" |
| 5 | "strings" |
| 6 | |
| 7 | "github.com/bwmarrin/discordgo" |
| 8 | "github.com/davecgh/go-spew/spew" |
| 9 | "github.com/matterbridge-org/matterbridge/bridge/config" |
| 10 | "github.com/matterbridge-org/matterbridge/bridge/helper" |
| 11 | ) |
| 12 | |
| 13 | func (b *Bdiscord) messageDelete(s *discordgo.Session, m *discordgo.MessageDelete) { //nolint:unparam |
| 14 | if m.GuildID != b.guildID { |
| 15 | b.Log.Debugf("Ignoring messageDelete because it originates from a different guild") |
| 16 | return |
| 17 | } |
| 18 | rmsg := config.Message{Account: b.Account, ID: m.ID, Event: config.EventMsgDelete, Text: config.EventMsgDelete} |
| 19 | rmsg.Channel = b.getChannelName(m.ChannelID) |
| 20 | |
| 21 | b.Log.Debugf("<= Sending message from %s to gateway", b.Account) |
| 22 | b.Log.Debugf("<= Message is %#v", rmsg) |
| 23 | b.Remote <- rmsg |
| 24 | } |
| 25 | |
| 26 | // TODO(qaisjp): if other bridges support bulk deletions, it could be fanned out centrally |
| 27 | func (b *Bdiscord) messageDeleteBulk(s *discordgo.Session, m *discordgo.MessageDeleteBulk) { //nolint:unparam |
| 28 | if m.GuildID != b.guildID { |
| 29 | b.Log.Debugf("Ignoring messageDeleteBulk because it originates from a different guild") |
| 30 | return |
| 31 | } |
| 32 | for _, msgID := range m.Messages { |
| 33 | rmsg := config.Message{ |
| 34 | Account: b.Account, |
| 35 | ID: msgID, |
| 36 | Event: config.EventMsgDelete, |
| 37 | Text: config.EventMsgDelete, |
| 38 | Channel: b.getChannelName(m.ChannelID), |
| 39 | } |
| 40 | |
| 41 | b.Log.Debugf("<= Sending message from %s to gateway", b.Account) |
| 42 | b.Log.Debugf("<= Message is %#v", rmsg) |
| 43 | b.Remote <- rmsg |
| 44 | } |
| 45 | } |
| 46 | |
| 47 | func (b *Bdiscord) messageEvent(s *discordgo.Session, m *discordgo.Event) { |
| 48 | b.Log.Debug(spew.Sdump(m.Struct)) |
| 49 | } |
| 50 | |
| 51 | func (b *Bdiscord) messageTyping(s *discordgo.Session, m *discordgo.TypingStart) { |
| 52 | if m.GuildID != b.guildID { |
| 53 | b.Log.Debugf("Ignoring messageTyping because it originates from a different guild") |
| 54 | return |
| 55 | } |
| 56 | if !b.GetBool("ShowUserTyping") { |
| 57 | return |
| 58 | } |
| 59 | |
| 60 | // Ignore our own typing messages |
| 61 | if m.UserID == b.userID { |
| 62 | return |
| 63 | } |
| 64 | |
| 65 | rmsg := config.Message{Account: b.Account, Event: config.EventUserTyping} |
| 66 | rmsg.Channel = b.getChannelName(m.ChannelID) |
| 67 | b.Remote <- rmsg |
| 68 | } |
| 69 | |
| 70 | func (b *Bdiscord) messageUpdate(s *discordgo.Session, m *discordgo.MessageUpdate) { //nolint:unparam |
| 71 | if m.GuildID != b.guildID { |
| 72 | b.Log.Debugf("Ignoring messageUpdate because it originates from a different guild") |
| 73 | return |
| 74 | } |
| 75 | if b.GetBool("EditDisable") { |
| 76 | return |
| 77 | } |
| 78 | // only when message is actually edited |
| 79 | if m.Message.EditedTimestamp != nil { |
| 80 | // don't relay old edits |
| 81 | // there are often spurrious "ghost" messages |
| 82 | delay := time.Now().Sub(*m.Message.EditedTimestamp) |
| 83 | if delay >= time.Duration(6) * time.Hour { |
| 84 | return |
| 85 | } |
| 86 | b.Log.Debugf("Sending edit message") |
| 87 | m.Content += b.GetString("EditSuffix") |
| 88 | msg := &discordgo.MessageCreate{ |
| 89 | Message: m.Message, |
| 90 | } |
| 91 | b.messageCreate(s, msg) |
| 92 | } |
| 93 | } |
| 94 | |
| 95 | func (b *Bdiscord) handleQuote(s *discordgo.Session, m *discordgo.Message, msg string) string { |
| 96 | if b.GetBool("QuoteDisable") { |
| 97 | return msg |
| 98 | } |
| 99 | if m.MessageReference == nil { |
| 100 | return msg |
| 101 | } |
| 102 | refMsgRef := m.MessageReference |
| 103 | refMsg, err := s.ChannelMessage(refMsgRef.ChannelID, refMsgRef.MessageID) |
| 104 | if err != nil { |
| 105 | b.Log.Errorf("Error getting quoted message %s:%s: %s", refMsgRef.ChannelID, refMsgRef.MessageID, err) |
| 106 | return msg |
| 107 | } |
| 108 | |
| 109 | quoteMessage := refMsg.Content |
| 110 | quoteNick := b.getNick(refMsg.Author, refMsg.GuildID) |
| 111 | fromWebhook := m.WebhookID != "" |
| 112 | if !fromWebhook && b.GetBool("UseDiscriminator") { |
| 113 | quoteNick += "#" + refMsg.Author.Discriminator |
| 114 | |
| 115 | } |
| 116 | |
| 117 | format := b.GetString("quoteformat") |
| 118 | if format == "" { |
| 119 | format = "{MESSAGE} (re @{QUOTENICK}: {QUOTEMESSAGE})" |
| 120 | } |
| 121 | quoteMessagelength := len([]rune(quoteMessage)) |
| 122 | if b.GetInt("QuoteLengthLimit") != 0 && quoteMessagelength >= b.GetInt("QuoteLengthLimit") { |
| 123 | runes := []rune(quoteMessage) |
| 124 | quoteMessage = string(runes[0:b.GetInt("QuoteLengthLimit")]) |
| 125 | if quoteMessagelength > b.GetInt("QuoteLengthLimit") { |
| 126 | quoteMessage += "..." |
| 127 | } |
| 128 | } |
| 129 | |
| 130 | replacer := strings.NewReplacer( |
| 131 | "{MESSAGE}", msg, |
| 132 | "{QUOTENICK}", quoteNick, |
| 133 | "{QUOTEMESSAGE}", quoteMessage, |
| 134 | ) |
| 135 | format = replacer.Replace(format) |
| 136 | return format |
| 137 | } |
| 138 | |
| 139 | func (b *Bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { //nolint:unparam |
| 140 | if m.GuildID != b.guildID { |
| 141 | b.Log.Debugf("Ignoring messageCreate because it originates from a different guild") |
| 142 | return |
| 143 | } |
| 144 | var err error |
| 145 | |
| 146 | // not relay our own messages |
| 147 | if m.Author.Username == b.nick { |
| 148 | return |
| 149 | } |
| 150 | // if using webhooks, do not relay if it's ours |
| 151 | if m.Author.Bot && b.transmitter.HasWebhook(m.Author.ID) { |
| 152 | return |
| 153 | } |
| 154 | |
| 155 | rmsg := config.Message{Account: b.Account, Avatar: "https://cdn.discordapp.com/avatars/" + m.Author.ID + "/" + m.Author.Avatar + ".jpg", UserID: m.Author.ID, ID: m.ID, Extra: make(map[string][]interface{})} |
| 156 | |
| 157 | // add the url of the attachments to content |
| 158 | if len(m.Attachments) > 0 { |
| 159 | first := true |
| 160 | for _, attach := range m.Attachments { |
| 161 | if b.alwaysDownloadFiles { |
| 162 | var url, name, caption string |
| 163 | var data *[]byte |
| 164 | |
| 165 | url = attach.URL |
| 166 | name = attach.Filename |
| 167 | |
| 168 | err = helper.HandleDownloadSize(b.Log, &rmsg, name, int64(attach.Size), b.General) |
| 169 | if err != nil { |
| 170 | return |
| 171 | } |
| 172 | data, err = helper.DownloadFile(url) |
| 173 | if err != nil { |
| 174 | return |
| 175 | } |
| 176 | |
| 177 | if first { |
| 178 | caption = m.Content |
| 179 | if caption == "" { |
| 180 | caption = name |
| 181 | } |
| 182 | first = false |
| 183 | } else { |
| 184 | caption = "" |
| 185 | } |
| 186 | |
| 187 | helper.HandleDownloadData(b.Log, &rmsg, name, caption, "", data, b.General) |
| 188 | } else { |
| 189 | m.Content = m.Content + "\n" + attach.URL |
| 190 | } |
| 191 | } |
| 192 | } |
| 193 | |
| 194 | b.Log.Debugf("== Receiving event %#v", m.Message) |
| 195 | |
| 196 | if m.Content != "" { |
| 197 | m.Message.Content = b.replaceChannelMentions(m.Message.Content) |
| 198 | rmsg.Text, err = m.ContentWithMoreMentionsReplaced(b.c) |
| 199 | if err != nil { |
| 200 | b.Log.Errorf("ContentWithMoreMentionsReplaced failed: %s", err) |
| 201 | rmsg.Text = m.ContentWithMentionsReplaced() |
| 202 | } |
| 203 | } |
| 204 | |
| 205 | // set channel name |
| 206 | rmsg.Channel = b.getChannelName(m.ChannelID) |
| 207 | |
| 208 | fromWebhook := m.WebhookID != "" |
| 209 | if !fromWebhook && !b.GetBool("UseUserName") { |
| 210 | rmsg.Username = b.getNick(m.Author, m.GuildID) |
| 211 | } else { |
| 212 | rmsg.Username = m.Author.Username |
| 213 | if !fromWebhook && b.GetBool("UseDiscriminator") { |
| 214 | rmsg.Username += "#" + m.Author.Discriminator |
| 215 | } |
| 216 | } |
| 217 | |
| 218 | // if we have embedded content add it to text |
| 219 | if b.GetBool("ShowEmbeds") && m.Message.Embeds != nil { |
| 220 | for _, embed := range m.Message.Embeds { |
| 221 | rmsg.Text += handleEmbed(embed) |
| 222 | } |
| 223 | } |
| 224 | |
| 225 | // no empty messages |
| 226 | if rmsg.Text == "" && len(rmsg.Extra["file"]) == 0 { |
| 227 | return |
| 228 | } |
| 229 | |
| 230 | // do we have a /me action |
| 231 | var ok bool |
| 232 | rmsg.Text, ok = b.replaceAction(rmsg.Text) |
| 233 | if ok { |
| 234 | rmsg.Event = config.EventUserAction |
| 235 | } |
| 236 | |
| 237 | // Replace emotes |
| 238 | // rmsg.Text = replaceEmotes(rmsg.Text) |
| 239 | |
| 240 | // Handle Reply thread |
| 241 | rmsg.Text = b.handleQuote(s, m.Message, rmsg.Text) |
| 242 | |
| 243 | // Add our parent id if it exists, and if it's not referring to a message in another channel |
| 244 | if ref := m.MessageReference; ref != nil && ref.ChannelID == m.ChannelID { |
| 245 | rmsg.ParentID = ref.MessageID |
| 246 | } |
| 247 | |
| 248 | b.Log.Debugf("<= Sending message from %s on %s to gateway", m.Author.Username, b.Account) |
| 249 | b.Log.Debugf("<= Message is %#v", rmsg) |
| 250 | b.Remote <- rmsg |
| 251 | } |
| 252 | |
| 253 | func (b *Bdiscord) memberUpdate(s *discordgo.Session, m *discordgo.GuildMemberUpdate) { |
| 254 | if m.GuildID != b.guildID { |
| 255 | b.Log.Debugf("Ignoring memberUpdate because it originates from a different guild") |
| 256 | return |
| 257 | } |
| 258 | if m.Member == nil { |
| 259 | b.Log.Warnf("Received member update with no member information: %#v", m) |
| 260 | } |
| 261 | |
| 262 | b.membersMutex.Lock() |
| 263 | defer b.membersMutex.Unlock() |
| 264 | |
| 265 | if currMember, ok := b.userMemberMap[m.Member.User.ID]; ok { |
| 266 | b.Log.Debugf( |
| 267 | "%s: memberupdate: user %s (nick %s) changes nick to %s", |
| 268 | b.Account, |
| 269 | m.Member.User.Username, |
| 270 | b.userMemberMap[m.Member.User.ID].Nick, |
| 271 | m.Member.Nick, |
| 272 | ) |
| 273 | delete(b.nickMemberMap, currMember.User.Username) |
| 274 | delete(b.nickMemberMap, currMember.Nick) |
| 275 | delete(b.userMemberMap, m.Member.User.ID) |
| 276 | } |
| 277 | b.userMemberMap[m.Member.User.ID] = m.Member |
| 278 | b.nickMemberMap[m.Member.User.Username] = m.Member |
| 279 | if m.Member.Nick != "" { |
| 280 | b.nickMemberMap[m.Member.Nick] = m.Member |
| 281 | } |
| 282 | } |
| 283 | |
| 284 | func (b *Bdiscord) memberAdd(s *discordgo.Session, m *discordgo.GuildMemberAdd) { |
| 285 | if m.GuildID != b.guildID { |
| 286 | b.Log.Debugf("Ignoring memberAdd because it originates from a different guild") |
| 287 | return |
| 288 | } |
| 289 | if b.GetBool("nosendjoinpart") { |
| 290 | return |
| 291 | } |
| 292 | if m.Member == nil { |
| 293 | b.Log.Warnf("Received member update with no member information: %#v", m) |
| 294 | return |
| 295 | } |
| 296 | username := m.Member.User.Username |
| 297 | if m.Member.Nick != "" { |
| 298 | username = m.Member.Nick |
| 299 | } |
| 300 | |
| 301 | rmsg := config.Message{ |
| 302 | Account: b.Account, |
| 303 | Event: config.EventJoinLeave, |
| 304 | Username: "system", |
| 305 | Text: username + " joins", |
| 306 | } |
| 307 | b.Log.Debugf("<= Sending message from %s to gateway", b.Account) |
| 308 | b.Log.Debugf("<= Message is %#v", rmsg) |
| 309 | b.Remote <- rmsg |
| 310 | } |
| 311 | |
| 312 | func (b *Bdiscord) memberRemove(s *discordgo.Session, m *discordgo.GuildMemberRemove) { |
| 313 | if m.GuildID != b.guildID { |
| 314 | b.Log.Debugf("Ignoring memberRemove because it originates from a different guild") |
| 315 | return |
| 316 | } |
| 317 | if b.GetBool("nosendjoinpart") { |
| 318 | return |
| 319 | } |
| 320 | if m.Member == nil { |
| 321 | b.Log.Warnf("Received member update with no member information: %#v", m) |
| 322 | return |
| 323 | } |
| 324 | username := m.Member.User.Username |
| 325 | if m.Member.Nick != "" { |
| 326 | username = m.Member.Nick |
| 327 | } |
| 328 | |
| 329 | rmsg := config.Message{ |
| 330 | Account: b.Account, |
| 331 | Event: config.EventJoinLeave, |
| 332 | Username: "system", |
| 333 | Text: username + " leaves", |
| 334 | } |
| 335 | b.Log.Debugf("<= Sending message from %s to gateway", b.Account) |
| 336 | b.Log.Debugf("<= Message is %#v", rmsg) |
| 337 | b.Remote <- rmsg |
| 338 | } |
| 339 | |
| 340 | func handleEmbed(embed *discordgo.MessageEmbed) string { |
| 341 | var t []string |
| 342 | var result string |
| 343 | |
| 344 | t = append(t, embed.Title) |
| 345 | t = append(t, embed.Description) |
| 346 | |
| 347 | for _, f := range embed.Fields { |
| 348 | field := f.Name + ": " + f.Value |
| 349 | t = append(t, field) |
| 350 | } |
| 351 | |
| 352 | t = append(t, embed.URL) |
| 353 | if embed.Footer != nil { |
| 354 | t = append(t, embed.Footer.Text) |
| 355 | } |
| 356 | |
| 357 | i := 0 |
| 358 | for _, e := range t { |
| 359 | if e == "" { |
| 360 | continue |
| 361 | } |
| 362 | |
| 363 | i++ |
| 364 | if i == 1 { |
| 365 | result += " embed: " + e |
| 366 | continue |
| 367 | } |
| 368 | |
| 369 | result += "\n" + e |
| 370 | } |
| 371 | |
| 372 | if result != "" { |
| 373 | result += "\n" |
| 374 | } |
| 375 | |
| 376 | return result |
| 377 | } |
| 378 | |