attachments.go 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. package main
  2. import (
  3. "bytes"
  4. "fmt"
  5. "image"
  6. "io"
  7. "net/http"
  8. "strings"
  9. "time"
  10. "github.com/bwmarrin/discordgo"
  11. "github.com/gabriel-vasile/mimetype"
  12. "maunium.net/go/mautrix"
  13. "maunium.net/go/mautrix/appservice"
  14. "maunium.net/go/mautrix/crypto/attachment"
  15. "maunium.net/go/mautrix/event"
  16. "maunium.net/go/mautrix/id"
  17. "go.mau.fi/mautrix-discord/database"
  18. )
  19. func downloadDiscordAttachment(url string) ([]byte, error) {
  20. req, err := http.NewRequest(http.MethodGet, url, nil)
  21. if err != nil {
  22. return nil, err
  23. }
  24. for key, value := range discordgo.DroidDownloadHeaders {
  25. req.Header.Set(key, value)
  26. }
  27. resp, err := http.DefaultClient.Do(req)
  28. if err != nil {
  29. return nil, err
  30. }
  31. defer resp.Body.Close()
  32. if resp.StatusCode > 300 {
  33. data, _ := io.ReadAll(resp.Body)
  34. return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, data)
  35. }
  36. return io.ReadAll(resp.Body)
  37. }
  38. func uploadDiscordAttachment(url string, data []byte) error {
  39. req, err := http.NewRequest(http.MethodPut, url, bytes.NewReader(data))
  40. if err != nil {
  41. return err
  42. }
  43. for key, value := range discordgo.DroidFetchHeaders {
  44. req.Header.Set(key, value)
  45. }
  46. resp, err := http.DefaultClient.Do(req)
  47. if err != nil {
  48. return err
  49. }
  50. defer resp.Body.Close()
  51. if resp.StatusCode > 300 {
  52. respData, _ := io.ReadAll(resp.Body)
  53. return fmt.Errorf("unexpected status %d: %s", resp.StatusCode, respData)
  54. }
  55. return nil
  56. }
  57. func downloadMatrixAttachment(intent *appservice.IntentAPI, content *event.MessageEventContent) ([]byte, error) {
  58. var file *event.EncryptedFileInfo
  59. rawMXC := content.URL
  60. if content.File != nil {
  61. file = content.File
  62. rawMXC = file.URL
  63. }
  64. mxc, err := rawMXC.Parse()
  65. if err != nil {
  66. return nil, err
  67. }
  68. data, err := intent.DownloadBytes(mxc)
  69. if err != nil {
  70. return nil, err
  71. }
  72. if file != nil {
  73. err = file.DecryptInPlace(data)
  74. if err != nil {
  75. return nil, err
  76. }
  77. }
  78. return data, nil
  79. }
  80. func (br *DiscordBridge) uploadMatrixAttachment(intent *appservice.IntentAPI, data []byte, url string, encrypt bool, meta AttachmentMeta) (*database.File, error) {
  81. dbFile := br.DB.File.New()
  82. dbFile.Timestamp = time.Now()
  83. dbFile.URL = url
  84. dbFile.ID = meta.AttachmentID
  85. dbFile.EmojiName = meta.EmojiName
  86. dbFile.Size = len(data)
  87. dbFile.MimeType = mimetype.Detect(data).String()
  88. if meta.MimeType == "" {
  89. meta.MimeType = dbFile.MimeType
  90. }
  91. if strings.HasPrefix(meta.MimeType, "image/") {
  92. cfg, _, _ := image.DecodeConfig(bytes.NewReader(data))
  93. dbFile.Width = cfg.Width
  94. dbFile.Height = cfg.Height
  95. }
  96. uploadMime := meta.MimeType
  97. if encrypt {
  98. dbFile.Encrypted = true
  99. dbFile.DecryptionInfo = attachment.NewEncryptedFile()
  100. dbFile.DecryptionInfo.EncryptInPlace(data)
  101. uploadMime = "application/octet-stream"
  102. }
  103. req := mautrix.ReqUploadMedia{
  104. ContentBytes: data,
  105. ContentType: uploadMime,
  106. }
  107. if br.Config.Homeserver.AsyncMedia {
  108. resp, err := intent.UnstableCreateMXC()
  109. if err != nil {
  110. return nil, err
  111. }
  112. dbFile.MXC = resp.ContentURI
  113. req.UnstableMXC = resp.ContentURI
  114. req.UploadURL = resp.UploadURL
  115. go func() {
  116. _, err = intent.UploadMedia(req)
  117. if err != nil {
  118. br.Log.Errorfln("Failed to upload %s: %v", req.UnstableMXC, err)
  119. dbFile.Delete()
  120. }
  121. }()
  122. } else {
  123. uploaded, err := intent.UploadMedia(req)
  124. if err != nil {
  125. return nil, err
  126. }
  127. dbFile.MXC = uploaded.ContentURI
  128. }
  129. return dbFile, nil
  130. }
  131. type AttachmentMeta struct {
  132. AttachmentID string
  133. MimeType string
  134. EmojiName string
  135. }
  136. func (br *DiscordBridge) copyAttachmentToMatrix(intent *appservice.IntentAPI, url string, encrypt bool, meta *AttachmentMeta) (*database.File, error) {
  137. dbFile := br.DB.File.Get(url, encrypt)
  138. if dbFile == nil {
  139. data, err := downloadDiscordAttachment(url)
  140. if err != nil {
  141. return nil, err
  142. }
  143. if meta == nil {
  144. meta = &AttachmentMeta{}
  145. }
  146. dbFile, err = br.uploadMatrixAttachment(intent, data, url, encrypt, *meta)
  147. if err != nil {
  148. return nil, err
  149. }
  150. // TODO add option to cache encrypted files too?
  151. if !dbFile.Encrypted {
  152. dbFile.Insert(nil)
  153. }
  154. }
  155. return dbFile, nil
  156. }
  157. func (portal *Portal) getEmojiMXCByDiscordID(emojiID, name string, animated bool) id.ContentURI {
  158. var url, mimeType string
  159. if animated {
  160. url = discordgo.EndpointEmojiAnimated(emojiID)
  161. mimeType = "image/gif"
  162. } else {
  163. url = discordgo.EndpointEmoji(emojiID)
  164. mimeType = "image/png"
  165. }
  166. dbFile, err := portal.bridge.copyAttachmentToMatrix(portal.MainIntent(), url, false, &AttachmentMeta{
  167. AttachmentID: emojiID,
  168. MimeType: mimeType,
  169. EmojiName: name,
  170. })
  171. if err != nil {
  172. portal.log.Warnfln("Failed to download emoji %s from discord: %v", emojiID, err)
  173. return id.ContentURI{}
  174. }
  175. return dbFile.MXC
  176. }