portal.go 26 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042
  1. package bridge
  2. import (
  3. "bytes"
  4. "fmt"
  5. "strings"
  6. "sync"
  7. "time"
  8. "github.com/bwmarrin/discordgo"
  9. log "maunium.net/go/maulogger/v2"
  10. "maunium.net/go/mautrix"
  11. "maunium.net/go/mautrix/appservice"
  12. "maunium.net/go/mautrix/event"
  13. "maunium.net/go/mautrix/id"
  14. "gitlab.com/beeper/discord/database"
  15. )
  16. type portalDiscordMessage struct {
  17. msg interface{}
  18. user *User
  19. }
  20. type portalMatrixMessage struct {
  21. evt *event.Event
  22. user *User
  23. }
  24. type Portal struct {
  25. *database.Portal
  26. bridge *Bridge
  27. log log.Logger
  28. roomCreateLock sync.Mutex
  29. discordMessages chan portalDiscordMessage
  30. matrixMessages chan portalMatrixMessage
  31. }
  32. var (
  33. portalCreationDummyEvent = event.Type{Type: "fi.mau.dummy.portal_created", Class: event.MessageEventType}
  34. )
  35. func (b *Bridge) loadPortal(dbPortal *database.Portal, key *database.PortalKey) *Portal {
  36. // If we weren't given a portal we'll attempt to create it if a key was
  37. // provided.
  38. if dbPortal == nil {
  39. if key == nil {
  40. return nil
  41. }
  42. dbPortal = b.db.Portal.New()
  43. dbPortal.Key = *key
  44. dbPortal.Insert()
  45. }
  46. portal := b.NewPortal(dbPortal)
  47. // No need to lock, it is assumed that our callers have already acquired
  48. // the lock.
  49. b.portalsByID[portal.Key] = portal
  50. if portal.MXID != "" {
  51. b.portalsByMXID[portal.MXID] = portal
  52. }
  53. return portal
  54. }
  55. func (b *Bridge) GetPortalByMXID(mxid id.RoomID) *Portal {
  56. b.portalsLock.Lock()
  57. defer b.portalsLock.Unlock()
  58. portal, ok := b.portalsByMXID[mxid]
  59. if !ok {
  60. return b.loadPortal(b.db.Portal.GetByMXID(mxid), nil)
  61. }
  62. return portal
  63. }
  64. func (b *Bridge) GetPortalByID(key database.PortalKey) *Portal {
  65. b.portalsLock.Lock()
  66. defer b.portalsLock.Unlock()
  67. portal, ok := b.portalsByID[key]
  68. if !ok {
  69. return b.loadPortal(b.db.Portal.GetByID(key), &key)
  70. }
  71. return portal
  72. }
  73. func (b *Bridge) GetAllPortals() []*Portal {
  74. return b.dbPortalsToPortals(b.db.Portal.GetAll())
  75. }
  76. func (b *Bridge) GetAllPortalsByID(id string) []*Portal {
  77. return b.dbPortalsToPortals(b.db.Portal.GetAllByID(id))
  78. }
  79. func (b *Bridge) dbPortalsToPortals(dbPortals []*database.Portal) []*Portal {
  80. b.portalsLock.Lock()
  81. defer b.portalsLock.Unlock()
  82. output := make([]*Portal, len(dbPortals))
  83. for index, dbPortal := range dbPortals {
  84. if dbPortal == nil {
  85. continue
  86. }
  87. portal, ok := b.portalsByID[dbPortal.Key]
  88. if !ok {
  89. portal = b.loadPortal(dbPortal, nil)
  90. }
  91. output[index] = portal
  92. }
  93. return output
  94. }
  95. func (b *Bridge) NewPortal(dbPortal *database.Portal) *Portal {
  96. portal := &Portal{
  97. Portal: dbPortal,
  98. bridge: b,
  99. log: b.log.Sub(fmt.Sprintf("Portal/%s", dbPortal.Key)),
  100. discordMessages: make(chan portalDiscordMessage, b.Config.Bridge.PortalMessageBuffer),
  101. matrixMessages: make(chan portalMatrixMessage, b.Config.Bridge.PortalMessageBuffer),
  102. }
  103. go portal.messageLoop()
  104. return portal
  105. }
  106. func (p *Portal) handleMatrixInvite(sender *User, evt *event.Event) {
  107. // Look up an existing puppet or create a new one.
  108. puppet := p.bridge.GetPuppetByMXID(id.UserID(evt.GetStateKey()))
  109. if puppet != nil {
  110. p.log.Infoln("no puppet for %v", sender)
  111. // Open a conversation on the discord side?
  112. }
  113. p.log.Infoln("puppet:", puppet)
  114. }
  115. func (p *Portal) messageLoop() {
  116. for {
  117. select {
  118. case msg := <-p.matrixMessages:
  119. p.handleMatrixMessages(msg)
  120. case msg := <-p.discordMessages:
  121. p.handleDiscordMessages(msg)
  122. }
  123. }
  124. }
  125. func (p *Portal) IsPrivateChat() bool {
  126. return p.Type == discordgo.ChannelTypeDM
  127. }
  128. func (p *Portal) MainIntent() *appservice.IntentAPI {
  129. if p.IsPrivateChat() && p.DMUser != "" {
  130. return p.bridge.GetPuppetByID(p.DMUser).DefaultIntent()
  131. }
  132. return p.bridge.bot
  133. }
  134. func (p *Portal) createMatrixRoom(user *User, channel *discordgo.Channel) error {
  135. // If we have a matrix id the room should exist so we have nothing to do.
  136. if p.MXID != "" {
  137. return nil
  138. }
  139. p.roomCreateLock.Lock()
  140. defer p.roomCreateLock.Unlock()
  141. p.Type = channel.Type
  142. if p.Type == discordgo.ChannelTypeDM {
  143. p.DMUser = channel.Recipients[0].ID
  144. }
  145. intent := p.MainIntent()
  146. if err := intent.EnsureRegistered(); err != nil {
  147. return err
  148. }
  149. name, err := p.bridge.Config.Bridge.FormatChannelname(channel, user.Session)
  150. if err != nil {
  151. p.log.Warnfln("failed to format name, proceeding with generic name: %v", err)
  152. p.Name = channel.Name
  153. } else {
  154. p.Name = name
  155. }
  156. p.Topic = channel.Topic
  157. // TODO: get avatars figured out
  158. // p.Avatar = puppet.Avatar
  159. // p.AvatarURL = puppet.AvatarURL
  160. p.log.Infoln("Creating Matrix room for channel:", p.Portal.Key.ChannelID)
  161. initialState := []*event.Event{}
  162. creationContent := make(map[string]interface{})
  163. creationContent["m.federate"] = false
  164. var invite []id.UserID
  165. if p.IsPrivateChat() {
  166. invite = append(invite, p.bridge.bot.UserID)
  167. }
  168. resp, err := intent.CreateRoom(&mautrix.ReqCreateRoom{
  169. Visibility: "private",
  170. Name: p.Name,
  171. Topic: p.Topic,
  172. Preset: "private_chat",
  173. IsDirect: p.IsPrivateChat(),
  174. InitialState: initialState,
  175. CreationContent: creationContent,
  176. })
  177. if err != nil {
  178. p.log.Warnln("Failed to create room:", err)
  179. return err
  180. }
  181. p.MXID = resp.RoomID
  182. p.Update()
  183. p.bridge.portalsLock.Lock()
  184. p.bridge.portalsByMXID[p.MXID] = p
  185. p.bridge.portalsLock.Unlock()
  186. p.ensureUserInvited(user)
  187. user.syncChatDoublePuppetDetails(p, true)
  188. p.syncParticipants(user, channel.Recipients)
  189. if p.IsPrivateChat() {
  190. puppet := user.bridge.GetPuppetByID(p.Key.Receiver)
  191. chats := map[id.UserID][]id.RoomID{puppet.MXID: {p.MXID}}
  192. user.updateDirectChats(chats)
  193. }
  194. firstEventResp, err := p.MainIntent().SendMessageEvent(p.MXID, portalCreationDummyEvent, struct{}{})
  195. if err != nil {
  196. p.log.Errorln("Failed to send dummy event to mark portal creation:", err)
  197. } else {
  198. p.FirstEventID = firstEventResp.EventID
  199. p.Update()
  200. }
  201. return nil
  202. }
  203. func (p *Portal) handleDiscordMessages(msg portalDiscordMessage) {
  204. if p.MXID == "" {
  205. p.log.Debugln("Creating Matrix room from incoming message")
  206. discordMsg := msg.msg.(*discordgo.MessageCreate)
  207. channel, err := msg.user.Session.Channel(discordMsg.ChannelID)
  208. if err != nil {
  209. p.log.Errorln("Failed to find channel for message:", err)
  210. return
  211. }
  212. if err := p.createMatrixRoom(msg.user, channel); err != nil {
  213. p.log.Errorln("Failed to create portal room:", err)
  214. return
  215. }
  216. }
  217. switch msg.msg.(type) {
  218. case *discordgo.MessageCreate:
  219. p.handleDiscordMessageCreate(msg.user, msg.msg.(*discordgo.MessageCreate).Message)
  220. case *discordgo.MessageUpdate:
  221. p.handleDiscordMessagesUpdate(msg.user, msg.msg.(*discordgo.MessageUpdate).Message)
  222. case *discordgo.MessageDelete:
  223. p.handleDiscordMessageDelete(msg.user, msg.msg.(*discordgo.MessageDelete).Message)
  224. case *discordgo.MessageReactionAdd:
  225. p.handleDiscordReaction(msg.user, msg.msg.(*discordgo.MessageReactionAdd).MessageReaction, true)
  226. case *discordgo.MessageReactionRemove:
  227. p.handleDiscordReaction(msg.user, msg.msg.(*discordgo.MessageReactionRemove).MessageReaction, false)
  228. default:
  229. p.log.Warnln("unknown message type")
  230. }
  231. }
  232. func (p *Portal) ensureUserInvited(user *User) bool {
  233. return user.ensureInvited(p.MainIntent(), p.MXID, p.IsPrivateChat())
  234. }
  235. func (p *Portal) markMessageHandled(msg *database.Message, discordID string, mxid id.EventID, authorID string, timestamp time.Time) *database.Message {
  236. if msg == nil {
  237. msg := p.bridge.db.Message.New()
  238. msg.Channel = p.Key
  239. msg.DiscordID = discordID
  240. msg.MatrixID = mxid
  241. msg.AuthorID = authorID
  242. msg.Timestamp = timestamp
  243. msg.Insert()
  244. } else {
  245. msg.UpdateMatrixID(mxid)
  246. }
  247. return msg
  248. }
  249. func (p *Portal) sendMediaFailedMessage(intent *appservice.IntentAPI, bridgeErr error) {
  250. content := &event.MessageEventContent{
  251. Body: fmt.Sprintf("Failed to bridge media: %v", bridgeErr),
  252. MsgType: event.MsgNotice,
  253. }
  254. _, err := intent.SendMessageEvent(p.MXID, event.EventMessage, content)
  255. if err != nil {
  256. p.log.Warnfln("failed to send error message to matrix: %v", err)
  257. }
  258. }
  259. func (p *Portal) handleDiscordAttachment(intent *appservice.IntentAPI, msgID string, attachment *discordgo.MessageAttachment) {
  260. // var captionContent *event.MessageEventContent
  261. // if attachment.Description != "" {
  262. // captionContent = &event.MessageEventContent{
  263. // Body: attachment.Description,
  264. // MsgType: event.MsgNotice,
  265. // }
  266. // }
  267. // p.log.Debugfln("captionContent: %#v", captionContent)
  268. content := &event.MessageEventContent{
  269. Body: attachment.Filename,
  270. Info: &event.FileInfo{
  271. Height: attachment.Height,
  272. MimeType: attachment.ContentType,
  273. Width: attachment.Width,
  274. // This gets overwritten later after the file is uploaded to the homeserver
  275. Size: attachment.Size,
  276. },
  277. }
  278. switch strings.ToLower(strings.Split(attachment.ContentType, "/")[0]) {
  279. case "audio":
  280. content.MsgType = event.MsgAudio
  281. case "image":
  282. content.MsgType = event.MsgImage
  283. case "video":
  284. content.MsgType = event.MsgVideo
  285. default:
  286. content.MsgType = event.MsgFile
  287. }
  288. data, err := p.downloadDiscordAttachment(attachment.URL)
  289. if err != nil {
  290. p.sendMediaFailedMessage(intent, err)
  291. return
  292. }
  293. err = p.uploadMatrixAttachment(intent, data, content)
  294. if err != nil {
  295. p.sendMediaFailedMessage(intent, err)
  296. return
  297. }
  298. resp, err := intent.SendMessageEvent(p.MXID, event.EventMessage, content)
  299. if err != nil {
  300. p.log.Warnfln("failed to send media message to matrix: %v", err)
  301. }
  302. dbAttachment := p.bridge.db.Attachment.New()
  303. dbAttachment.Channel = p.Key
  304. dbAttachment.DiscordMessageID = msgID
  305. dbAttachment.DiscordAttachmentID = attachment.ID
  306. dbAttachment.MatrixEventID = resp.EventID
  307. dbAttachment.Insert()
  308. }
  309. func (p *Portal) handleDiscordMessageCreate(user *User, msg *discordgo.Message) {
  310. if p.MXID == "" {
  311. p.log.Warnln("handle message called without a valid portal")
  312. return
  313. }
  314. existing := p.bridge.db.Message.GetByDiscordID(p.Key, msg.ID)
  315. if existing != nil {
  316. p.log.Debugln("not handling duplicate message", msg.ID)
  317. return
  318. }
  319. intent := p.bridge.GetPuppetByID(msg.Author.ID).IntentFor(p)
  320. if msg.Content != "" {
  321. content := &event.MessageEventContent{
  322. Body: msg.Content,
  323. MsgType: event.MsgText,
  324. }
  325. resp, err := intent.SendMessageEvent(p.MXID, event.EventMessage, content)
  326. if err != nil {
  327. p.log.Warnfln("failed to send message %q to matrix: %v", msg.ID, err)
  328. return
  329. }
  330. ts, _ := msg.Timestamp.Parse()
  331. p.markMessageHandled(existing, msg.ID, resp.EventID, msg.Author.ID, ts)
  332. }
  333. // now run through any attachments the message has
  334. for _, attachment := range msg.Attachments {
  335. p.handleDiscordAttachment(intent, msg.ID, attachment)
  336. }
  337. }
  338. func (p *Portal) handleDiscordMessagesUpdate(user *User, msg *discordgo.Message) {
  339. if msg.Author != nil && user.ID == msg.Author.ID {
  340. return
  341. }
  342. if p.MXID == "" {
  343. p.log.Warnln("handle message called without a valid portal")
  344. return
  345. }
  346. intent := p.bridge.GetPuppetByID(msg.Author.ID).IntentFor(p)
  347. existing := p.bridge.db.Message.GetByDiscordID(p.Key, msg.ID)
  348. if existing == nil {
  349. // Due to the differences in Discord and Matrix attachment handling,
  350. // existing will return nil if the original message was empty as we
  351. // don't store/save those messages so we can determine when we're
  352. // working against an attachment and do the attachment lookup instead.
  353. // Find all the existing attachments and drop them in a map so we can
  354. // figure out which, if any have been deleted and clean them up on the
  355. // matrix side.
  356. attachmentMap := map[string]*database.Attachment{}
  357. attachments := p.bridge.db.Attachment.GetAllByDiscordMessageID(p.Key, msg.ID)
  358. for _, attachment := range attachments {
  359. attachmentMap[attachment.DiscordAttachmentID] = attachment
  360. }
  361. // Now run through the list of attachments on this message and remove
  362. // them from the map.
  363. for _, attachment := range msg.Attachments {
  364. if _, found := attachmentMap[attachment.ID]; found {
  365. delete(attachmentMap, attachment.ID)
  366. }
  367. }
  368. // Finally run through any attachments still in the map and delete them
  369. // on the matrix side and our database.
  370. for _, attachment := range attachmentMap {
  371. _, err := intent.RedactEvent(p.MXID, attachment.MatrixEventID)
  372. if err != nil {
  373. p.log.Warnfln("Failed to remove attachment %s: %v", attachment.MatrixEventID, err)
  374. }
  375. attachment.Delete()
  376. }
  377. return
  378. }
  379. content := &event.MessageEventContent{
  380. Body: msg.Content,
  381. MsgType: event.MsgText,
  382. }
  383. content.SetEdit(existing.MatrixID)
  384. _, err := intent.SendMessageEvent(p.MXID, event.EventMessage, content)
  385. if err != nil {
  386. p.log.Warnfln("failed to send message %q to matrix: %v", msg.ID, err)
  387. return
  388. }
  389. // It appears that matrix updates only work against the original event id
  390. // so updating it to the new one from an edit makes it so you can't update
  391. // it anyways. So we just don't update anything and we can keep updating
  392. // the message.
  393. }
  394. func (p *Portal) handleDiscordMessageDelete(user *User, msg *discordgo.Message) {
  395. // The discord delete message object is pretty empty and doesn't include
  396. // the author so we have to use the DMUser from the portal that was added
  397. // at creation time if we're a DM. We'll might have similar issues when we
  398. // add guild message support, but we'll cross that bridge when we get
  399. // there.
  400. // Find the message that we're working with. This could correctly return
  401. // nil if the message was just one or more attachments.
  402. existing := p.bridge.db.Message.GetByDiscordID(p.Key, msg.ID)
  403. var intent *appservice.IntentAPI
  404. if p.Type == discordgo.ChannelTypeDM {
  405. intent = p.bridge.GetPuppetByID(p.DMUser).IntentFor(p)
  406. } else {
  407. p.log.Errorfln("no guilds yet...")
  408. }
  409. if existing != nil {
  410. _, err := intent.RedactEvent(p.MXID, existing.MatrixID)
  411. if err != nil {
  412. p.log.Warnfln("Failed to remove message %s: %v", existing.MatrixID, err)
  413. }
  414. existing.Delete()
  415. }
  416. // Now delete all of the existing attachments.
  417. attachments := p.bridge.db.Attachment.GetAllByDiscordMessageID(p.Key, msg.ID)
  418. for _, attachment := range attachments {
  419. _, err := intent.RedactEvent(p.MXID, attachment.MatrixEventID)
  420. if err != nil {
  421. p.log.Warnfln("Failed to remove attachment %s: %v", attachment.MatrixEventID, err)
  422. }
  423. attachment.Delete()
  424. }
  425. }
  426. func (p *Portal) syncParticipants(source *User, participants []*discordgo.User) {
  427. for _, participant := range participants {
  428. puppet := p.bridge.GetPuppetByID(participant.ID)
  429. puppet.SyncContact(source)
  430. user := p.bridge.GetUserByID(participant.ID)
  431. if user != nil {
  432. p.ensureUserInvited(user)
  433. }
  434. if user == nil || !puppet.IntentFor(p).IsCustomPuppet {
  435. if err := puppet.IntentFor(p).EnsureJoined(p.MXID); err != nil {
  436. p.log.Warnfln("Failed to make puppet of %s join %s: %v", participant.ID, p.MXID, err)
  437. }
  438. }
  439. }
  440. }
  441. func (p *Portal) handleMatrixMessages(msg portalMatrixMessage) {
  442. switch msg.evt.Type {
  443. case event.EventMessage:
  444. p.handleMatrixMessage(msg.user, msg.evt)
  445. default:
  446. p.log.Debugln("unknown event type", msg.evt.Type)
  447. }
  448. }
  449. func (p *Portal) handleMatrixMessage(sender *User, evt *event.Event) {
  450. if p.IsPrivateChat() && sender.ID != p.Key.Receiver {
  451. return
  452. }
  453. existing := p.bridge.db.Message.GetByMatrixID(p.Key, evt.ID)
  454. if existing != nil {
  455. p.log.Debugln("not handling duplicate message", evt.ID)
  456. return
  457. }
  458. content, ok := evt.Content.Parsed.(*event.MessageEventContent)
  459. if !ok {
  460. p.log.Debugfln("Failed to handle event %s: unexpected parsed content type %T", evt.ID, evt.Content.Parsed)
  461. return
  462. }
  463. if content.RelatesTo != nil && content.RelatesTo.Type == event.RelReplace {
  464. existing := p.bridge.db.Message.GetByMatrixID(p.Key, content.RelatesTo.EventID)
  465. if existing != nil && existing.DiscordID != "" {
  466. // we don't have anything to save for the update message right now
  467. // as we're not tracking edited timestamps.
  468. _, err := sender.Session.ChannelMessageEdit(p.Key.ChannelID,
  469. existing.DiscordID, content.NewContent.Body)
  470. if err != nil {
  471. p.log.Errorln("Failed to update message %s: %v", existing.DiscordID, err)
  472. return
  473. }
  474. }
  475. return
  476. }
  477. var msg *discordgo.Message
  478. var err error
  479. switch content.MsgType {
  480. case event.MsgText, event.MsgEmote, event.MsgNotice:
  481. msg, err = sender.Session.ChannelMessageSend(p.Key.ChannelID, content.Body)
  482. case event.MsgAudio, event.MsgFile, event.MsgImage, event.MsgVideo:
  483. data, err := p.downloadMatrixAttachment(evt.ID, content)
  484. if err != nil {
  485. p.log.Errorfln("Failed to download matrix attachment: %v", err)
  486. return
  487. }
  488. msgSend := &discordgo.MessageSend{
  489. Files: []*discordgo.File{
  490. &discordgo.File{
  491. Name: content.Body,
  492. ContentType: content.Info.MimeType,
  493. Reader: bytes.NewReader(data),
  494. },
  495. },
  496. }
  497. msg, err = sender.Session.ChannelMessageSendComplex(p.Key.ChannelID, msgSend)
  498. default:
  499. p.log.Warnln("unknown message type:", content.MsgType)
  500. return
  501. }
  502. if err != nil {
  503. p.log.Errorfln("Failed to send message: %v", err)
  504. return
  505. }
  506. if msg != nil {
  507. dbMsg := p.bridge.db.Message.New()
  508. dbMsg.Channel = p.Key
  509. dbMsg.DiscordID = msg.ID
  510. dbMsg.MatrixID = evt.ID
  511. dbMsg.AuthorID = sender.ID
  512. dbMsg.Timestamp = time.Now()
  513. dbMsg.Insert()
  514. }
  515. }
  516. func (p *Portal) handleMatrixLeave(sender *User) {
  517. p.log.Debugln("User left private chat portal, cleaning up and deleting...")
  518. p.delete()
  519. p.cleanup(false)
  520. // TODO: figure out how to close a dm from the API.
  521. p.cleanupIfEmpty()
  522. }
  523. func (p *Portal) leave(sender *User) {
  524. if p.MXID == "" {
  525. return
  526. }
  527. intent := p.bridge.GetPuppetByID(sender.ID).IntentFor(p)
  528. intent.LeaveRoom(p.MXID)
  529. }
  530. func (p *Portal) delete() {
  531. p.Portal.Delete()
  532. p.bridge.portalsLock.Lock()
  533. delete(p.bridge.portalsByID, p.Key)
  534. if p.MXID != "" {
  535. delete(p.bridge.portalsByMXID, p.MXID)
  536. }
  537. p.bridge.portalsLock.Unlock()
  538. }
  539. func (p *Portal) cleanupIfEmpty() {
  540. users, err := p.getMatrixUsers()
  541. if err != nil {
  542. p.log.Errorfln("Failed to get Matrix user list to determine if portal needs to be cleaned up: %v", err)
  543. return
  544. }
  545. if len(users) == 0 {
  546. p.log.Infoln("Room seems to be empty, cleaning up...")
  547. p.delete()
  548. p.cleanup(false)
  549. }
  550. }
  551. func (p *Portal) cleanup(puppetsOnly bool) {
  552. if p.MXID != "" {
  553. return
  554. }
  555. if p.IsPrivateChat() {
  556. _, err := p.MainIntent().LeaveRoom(p.MXID)
  557. if err != nil {
  558. p.log.Warnln("Failed to leave private chat portal with main intent:", err)
  559. }
  560. return
  561. }
  562. intent := p.MainIntent()
  563. members, err := intent.JoinedMembers(p.MXID)
  564. if err != nil {
  565. p.log.Errorln("Failed to get portal members for cleanup:", err)
  566. return
  567. }
  568. for member := range members.Joined {
  569. if member == intent.UserID {
  570. continue
  571. }
  572. puppet := p.bridge.GetPuppetByMXID(member)
  573. if p != nil {
  574. _, err = puppet.DefaultIntent().LeaveRoom(p.MXID)
  575. if err != nil {
  576. p.log.Errorln("Error leaving as puppet while cleaning up portal:", err)
  577. }
  578. } else if !puppetsOnly {
  579. _, err = intent.KickUser(p.MXID, &mautrix.ReqKickUser{UserID: member, Reason: "Deleting portal"})
  580. if err != nil {
  581. p.log.Errorln("Error kicking user while cleaning up portal:", err)
  582. }
  583. }
  584. }
  585. _, err = intent.LeaveRoom(p.MXID)
  586. if err != nil {
  587. p.log.Errorln("Error leaving with main intent while cleaning up portal:", err)
  588. }
  589. }
  590. func (p *Portal) getMatrixUsers() ([]id.UserID, error) {
  591. members, err := p.MainIntent().JoinedMembers(p.MXID)
  592. if err != nil {
  593. return nil, fmt.Errorf("failed to get member list: %w", err)
  594. }
  595. var users []id.UserID
  596. for userID := range members.Joined {
  597. _, isPuppet := p.bridge.ParsePuppetMXID(userID)
  598. if !isPuppet && userID != p.bridge.bot.UserID {
  599. users = append(users, userID)
  600. }
  601. }
  602. return users, nil
  603. }
  604. func (p *Portal) handleMatrixKick(sender *User, target *Puppet) {
  605. // TODO: need to learn how to make this happen as discordgo proper doesn't
  606. // support group dms and it looks like it's a binary blob.
  607. }
  608. func (p *Portal) handleMatrixReaction(evt *event.Event) {
  609. user := p.bridge.GetUserByMXID(evt.Sender)
  610. if user == nil {
  611. p.log.Errorf("failed to find user for %s", evt.Sender)
  612. return
  613. }
  614. if user.ID != p.Key.Receiver {
  615. return
  616. }
  617. reaction := evt.Content.AsReaction()
  618. if reaction.RelatesTo.Type != event.RelAnnotation {
  619. p.log.Errorfln("Ignoring reaction %s due to unknown m.relates_to data", evt.ID)
  620. return
  621. }
  622. var discordID string
  623. msg := p.bridge.db.Message.GetByMatrixID(p.Key, reaction.RelatesTo.EventID)
  624. // Due to the differences in attachments between Discord and Matrix, if a
  625. // user reacts to a media message on discord our lookup above will fail
  626. // because the relation of matrix media messages to attachments in handled
  627. // in the attachments table instead of messages so we need to check that
  628. // before continuing.
  629. //
  630. // This also leads to interesting problems when a Discord message comes in
  631. // with multiple attachments. A user can react to each one individually on
  632. // Matrix, which will cause us to send it twice. Discord tends to ignore
  633. // this, but if the user removes one of them, discord removes it and now
  634. // they're out of sync. Perhaps we should add a counter to the reactions
  635. // table to keep them in sync and to avoid sending duplicates to Discord.
  636. if msg == nil {
  637. attachment := p.bridge.db.Attachment.GetByMatrixID(p.Key, reaction.RelatesTo.EventID)
  638. discordID = attachment.DiscordMessageID
  639. } else {
  640. if msg.DiscordID == "" {
  641. p.log.Debugf("Message %s has not yet been sent to discord", reaction.RelatesTo.EventID)
  642. return
  643. }
  644. discordID = msg.DiscordID
  645. }
  646. // Figure out if this is a custom emoji or not.
  647. emojiID := reaction.RelatesTo.Key
  648. if strings.HasPrefix(emojiID, "mxc://") {
  649. uri, _ := id.ParseContentURI(emojiID)
  650. emoji := p.bridge.db.Emoji.GetByMatrixURL(uri)
  651. if emoji == nil {
  652. p.log.Errorfln("failed to find emoji for %s", emojiID)
  653. return
  654. }
  655. emojiID = emoji.APIName()
  656. }
  657. err := user.Session.MessageReactionAdd(p.Key.ChannelID, discordID, emojiID)
  658. if err != nil {
  659. p.log.Debugf("Failed to send reaction %s id:%s: %v", p.Key, discordID, err)
  660. return
  661. }
  662. dbReaction := p.bridge.db.Reaction.New()
  663. dbReaction.Channel.ChannelID = p.Key.ChannelID
  664. dbReaction.Channel.Receiver = p.Key.Receiver
  665. dbReaction.MatrixEventID = evt.ID
  666. dbReaction.DiscordMessageID = discordID
  667. dbReaction.AuthorID = user.ID
  668. dbReaction.MatrixName = reaction.RelatesTo.Key
  669. dbReaction.DiscordID = emojiID
  670. dbReaction.Insert()
  671. }
  672. func (p *Portal) handleDiscordReaction(user *User, reaction *discordgo.MessageReaction, add bool) {
  673. if user.ID == reaction.UserID {
  674. return
  675. }
  676. intent := p.bridge.GetPuppetByID(reaction.UserID).IntentFor(p)
  677. var discordID string
  678. var matrixID string
  679. if reaction.Emoji.ID != "" {
  680. dbEmoji := p.bridge.db.Emoji.GetByDiscordID(reaction.Emoji.ID)
  681. if dbEmoji == nil {
  682. data, mimeType, err := p.downloadDiscordEmoji(reaction.Emoji.ID, reaction.Emoji.Animated)
  683. if err != nil {
  684. p.log.Warnfln("Failed to download emoji %s from discord: %v", reaction.Emoji.ID, err)
  685. return
  686. }
  687. uri, err := p.uploadMatrixEmoji(intent, data, mimeType)
  688. if err != nil {
  689. p.log.Warnfln("Failed to upload discord emoji %s to homeserver: %v", reaction.Emoji.ID, err)
  690. return
  691. }
  692. dbEmoji = p.bridge.db.Emoji.New()
  693. dbEmoji.DiscordID = reaction.Emoji.ID
  694. dbEmoji.DiscordName = reaction.Emoji.Name
  695. dbEmoji.MatrixURL = uri
  696. dbEmoji.Insert()
  697. }
  698. discordID = dbEmoji.DiscordID
  699. matrixID = dbEmoji.MatrixURL.String()
  700. } else {
  701. discordID = reaction.Emoji.Name
  702. matrixID = reaction.Emoji.Name
  703. }
  704. // Find the message that we're working with.
  705. message := p.bridge.db.Message.GetByDiscordID(p.Key, reaction.MessageID)
  706. if message == nil {
  707. p.log.Debugfln("failed to add reaction to message %s: message not found", reaction.MessageID)
  708. return
  709. }
  710. // Lookup an existing reaction
  711. existing := p.bridge.db.Reaction.GetByDiscordID(p.Key, message.DiscordID, discordID)
  712. if !add {
  713. if existing == nil {
  714. p.log.Debugln("Failed to remove reaction for unknown message", reaction.MessageID)
  715. return
  716. }
  717. _, err := intent.RedactEvent(p.MXID, existing.MatrixEventID)
  718. if err != nil {
  719. p.log.Warnfln("Failed to remove reaction from %s: %v", p.MXID, err)
  720. }
  721. existing.Delete()
  722. return
  723. }
  724. content := event.Content{Parsed: &event.ReactionEventContent{
  725. RelatesTo: event.RelatesTo{
  726. EventID: message.MatrixID,
  727. Type: event.RelAnnotation,
  728. Key: matrixID,
  729. },
  730. }}
  731. resp, err := intent.Client.SendMessageEvent(p.MXID, event.EventReaction, &content)
  732. if err != nil {
  733. p.log.Errorfln("failed to send reaction from %s: %v", reaction.MessageID, err)
  734. return
  735. }
  736. if existing == nil {
  737. dbReaction := p.bridge.db.Reaction.New()
  738. dbReaction.Channel = p.Key
  739. dbReaction.DiscordMessageID = message.DiscordID
  740. dbReaction.MatrixEventID = resp.EventID
  741. dbReaction.AuthorID = reaction.UserID
  742. dbReaction.MatrixName = matrixID
  743. dbReaction.DiscordID = discordID
  744. dbReaction.Insert()
  745. }
  746. }
  747. func (p *Portal) handleMatrixRedaction(evt *event.Event) {
  748. user := p.bridge.GetUserByMXID(evt.Sender)
  749. if user.ID != p.Key.Receiver {
  750. return
  751. }
  752. // First look if we're redacting a message
  753. message := p.bridge.db.Message.GetByMatrixID(p.Key, evt.Redacts)
  754. if message != nil {
  755. if message.DiscordID != "" {
  756. err := user.Session.ChannelMessageDelete(p.Key.ChannelID, message.DiscordID)
  757. if err != nil {
  758. p.log.Debugfln("Failed to delete discord message %s: %v", message.DiscordID, err)
  759. } else {
  760. message.Delete()
  761. }
  762. }
  763. return
  764. }
  765. // Now check if it's a reaction.
  766. reaction := p.bridge.db.Reaction.GetByMatrixID(p.Key, evt.Redacts)
  767. if reaction != nil {
  768. if reaction.DiscordID != "" {
  769. err := user.Session.MessageReactionRemove(p.Key.ChannelID, reaction.DiscordMessageID, reaction.DiscordID, reaction.AuthorID)
  770. if err != nil {
  771. p.log.Debugfln("Failed to delete reaction %s for message %s: %v", reaction.DiscordID, reaction.DiscordMessageID, err)
  772. } else {
  773. reaction.Delete()
  774. }
  775. }
  776. return
  777. }
  778. p.log.Warnfln("Failed to redact %s@%s: no event found", p.Key, evt.Redacts)
  779. }
  780. func (p *Portal) update(user *User, channel *discordgo.Channel) {
  781. name, err := p.bridge.Config.Bridge.FormatChannelname(channel, user.Session)
  782. if err != nil {
  783. p.log.Warnln("Failed to format channel name, using existing:", err)
  784. } else {
  785. p.Name = name
  786. }
  787. intent := p.MainIntent()
  788. if p.Name != name {
  789. _, err = intent.SetRoomName(p.MXID, p.Name)
  790. if err != nil {
  791. p.log.Warnln("Failed to update room name:", err)
  792. }
  793. }
  794. if p.Topic != channel.Topic {
  795. p.Topic = channel.Topic
  796. _, err = intent.SetRoomTopic(p.MXID, p.Topic)
  797. if err != nil {
  798. p.log.Warnln("Failed to update room topic:", err)
  799. }
  800. }
  801. if p.Avatar != channel.Icon {
  802. p.Avatar = channel.Icon
  803. var url string
  804. if p.Type == discordgo.ChannelTypeDM {
  805. dmUser, err := user.Session.User(p.DMUser)
  806. if err != nil {
  807. p.log.Warnln("failed to lookup the dmuser", err)
  808. } else {
  809. url = dmUser.AvatarURL("")
  810. }
  811. } else {
  812. url = discordgo.EndpointGroupIcon(channel.ID, channel.Icon)
  813. }
  814. p.AvatarURL = id.ContentURI{}
  815. if url != "" {
  816. uri, err := uploadAvatar(intent, url)
  817. if err != nil {
  818. p.log.Warnf("failed to upload avatar", err)
  819. } else {
  820. p.AvatarURL = uri
  821. }
  822. }
  823. intent.SetRoomAvatar(p.MXID, p.AvatarURL)
  824. }
  825. p.Update()
  826. p.log.Debugln("portal updated")
  827. }