attachments.go 5.1 KB

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