diff --git a/pkg/connector/client.go b/pkg/connector/client.go index c6ac0609..b980bd2b 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -240,11 +240,11 @@ func (t *TelegramClient) GetChatInfo(ctx context.Context, portal *bridgev2.Porta return nil, fmt.Errorf("full chat is not %T", chatFull) } - if photo, ok := chatFull.ChatPhoto.(*tg.Photo); ok { + if photo, ok := chatFull.GetChatPhoto(); ok { avatar = &bridgev2.Avatar{ - ID: ids.MakeAvatarID(photo.ID), + ID: ids.MakeAvatarID(photo.GetID()), Get: func(ctx context.Context) (data []byte, err error) { - data, _, _, _, err = media.DownloadPhoto(ctx, t.client.API(), photo) + data, _, err = media.NewTransferer(t.client.API()).WithPhoto(photo).Download(ctx) return }, } @@ -307,11 +307,7 @@ func (t *TelegramClient) getUserInfoFromTelegramUser(user *tg.User) (*bridgev2.U avatar = &bridgev2.Avatar{ ID: ids.MakeAvatarID(photo.PhotoID), Get: func(ctx context.Context) (data []byte, err error) { - data, _, err = media.DownloadFileLocation(ctx, t.client.API(), &tg.InputPeerPhotoFileLocation{ - Peer: &tg.InputPeerUser{UserID: user.ID}, - PhotoID: photo.PhotoID, - Big: true, - }) + data, _, err = media.NewTransferer(t.client.API()).WithUserPhoto(user, photo.PhotoID).Download(ctx) return }, } diff --git a/pkg/connector/directdownload.go b/pkg/connector/directdownload.go index 13a35a4f..898c9473 100644 --- a/pkg/connector/directdownload.go +++ b/pkg/connector/directdownload.go @@ -80,41 +80,26 @@ func (tc *TelegramConnector) Download(ctx context.Context, mediaID networkid.Med } } - var data []byte - var mimeType string + transferer := media.NewTransferer(client.client.API()) + var readyTransferer *media.ReadyTransferer switch msgMedia := msgMedia.(type) { case *tg.MessageMediaPhoto: - data, _, _, mimeType, err = media.DownloadPhotoMedia(ctx, client.client.API(), msgMedia) + readyTransferer = transferer.WithPhoto(msgMedia.Photo) case *tg.MessageMediaDocument: - document, ok := msgMedia.Document.(*tg.Document) - if !ok { - return nil, fmt.Errorf("unrecognized document type %T", msgMedia.Document) - } - - if info.Thumbnail { - // Download the thumbnail for this media rather than the media itself. - _, _, largestThumbnail := media.GetLargestPhotoSize(document.Thumbs) - data, mimeType, err = media.DownloadFileLocation(ctx, client.client.API(), &tg.InputDocumentFileLocation{ - ID: document.GetID(), - AccessHash: document.GetAccessHash(), - FileReference: document.GetFileReference(), - ThumbSize: largestThumbnail.GetType(), - }) - } else { - mimeType = document.GetMimeType() - data, err = media.DownloadDocument(ctx, client.client.API(), document) - } + readyTransferer = transferer.WithDocument(msgMedia.Document, info.Thumbnail) default: return nil, fmt.Errorf("unhandled media type %T", msgMedia) } + + data, fileInfo, err := readyTransferer.Download(ctx) if err != nil { return nil, err } return &mediaproxy.GetMediaResponseData{ Reader: io.NopCloser(bytes.NewBuffer(data)), - ContentType: mimeType, - ContentLength: int64(len(data)), + ContentType: fileInfo.MimeType, + ContentLength: int64(fileInfo.Size), }, nil } diff --git a/pkg/connector/media/document.go b/pkg/connector/media/document.go deleted file mode 100644 index 13594ab5..00000000 --- a/pkg/connector/media/document.go +++ /dev/null @@ -1,17 +0,0 @@ -package media - -import ( - "context" - - "github.com/gotd/td/telegram/downloader" - "github.com/gotd/td/tg" -) - -func DownloadDocument(ctx context.Context, client downloader.Client, document *tg.Document) ([]byte, error) { - data, _, err := DownloadFileLocation(ctx, client, &tg.InputDocumentFileLocation{ - ID: document.GetID(), - AccessHash: document.GetAccessHash(), - FileReference: document.GetFileReference(), - }) - return data, err -} diff --git a/pkg/connector/media/download.go b/pkg/connector/media/download.go deleted file mode 100644 index 1a2a8541..00000000 --- a/pkg/connector/media/download.go +++ /dev/null @@ -1,40 +0,0 @@ -package media - -import ( - "bytes" - "context" - "net/http" - - "github.com/gotd/td/telegram/downloader" - "github.com/gotd/td/tg" -) - -func DownloadFileLocation(ctx context.Context, client downloader.Client, loc tg.InputFileLocationClass) (data []byte, mimeType string, err error) { - // TODO convert entire function to streaming? Maybe at least stream to file? - var buf bytes.Buffer - storageFileTypeClass, err := downloader.NewDownloader().Download(client, loc).Stream(ctx, &buf) - if err != nil { - return nil, "", err - } - switch storageFileTypeClass.(type) { - case *tg.StorageFileJpeg: - mimeType = "image/jpeg" - case *tg.StorageFileGif: - mimeType = "image/gif" - case *tg.StorageFilePng: - mimeType = "image/png" - case *tg.StorageFilePdf: - mimeType = "application/pdf" - case *tg.StorageFileMp3: - mimeType = "audio/mp3" - case *tg.StorageFileMov: - mimeType = "video/quicktime" - case *tg.StorageFileMp4: - mimeType = "video/mp4" - case *tg.StorageFileWebp: - mimeType = "image/webp" - default: - mimeType = http.DetectContentType(buf.Bytes()) - } - return buf.Bytes(), mimeType, nil -} diff --git a/pkg/connector/media/photo.go b/pkg/connector/media/photo.go deleted file mode 100644 index 164d39b3..00000000 --- a/pkg/connector/media/photo.go +++ /dev/null @@ -1,69 +0,0 @@ -package media - -import ( - "context" - "fmt" - - "github.com/gotd/td/telegram/downloader" - "github.com/gotd/td/tg" -) - -type dimensionable interface { - GetW() int - GetH() int -} - -func GetLargestPhotoSize(sizes []tg.PhotoSizeClass) (width, height int, largest tg.PhotoSizeClass) { - if len(sizes) == 0 { - panic("cannot get largest size from empty list of sizes") - } - - var maxSize int - for _, s := range sizes { - var currentSize int - switch size := s.(type) { - case *tg.PhotoSize: - currentSize = size.GetSize() - case *tg.PhotoCachedSize: - currentSize = max(size.GetW(), size.GetH()) - case *tg.PhotoSizeProgressive: - currentSize = max(size.GetW(), size.GetH()) - case *tg.PhotoPathSize: - currentSize = len(size.GetBytes()) - case *tg.PhotoStrippedSize: - currentSize = len(size.GetBytes()) - } - - if currentSize > maxSize { - maxSize = currentSize - largest = s - if d, ok := s.(dimensionable); ok { - width = d.GetW() - height = d.GetH() - } - } - } - return -} - -func DownloadPhoto(ctx context.Context, client downloader.Client, photo *tg.Photo) (data []byte, width, height int, mimeType string, err error) { - var largest tg.PhotoSizeClass - width, height, largest = GetLargestPhotoSize(photo.GetSizes()) - data, mimeType, err = DownloadFileLocation(ctx, client, &tg.InputPhotoFileLocation{ - ID: photo.GetID(), - AccessHash: photo.GetAccessHash(), - FileReference: photo.GetFileReference(), - ThumbSize: largest.GetType(), - }) - return -} - -func DownloadPhotoMedia(ctx context.Context, client downloader.Client, media *tg.MessageMediaPhoto) (data []byte, width, height int, mimeType string, err error) { - if p, ok := media.GetPhoto(); !ok { - return nil, 0, 0, "", fmt.Errorf("photo message sent without a photo") - } else if photo, ok := p.(*tg.Photo); !ok { - return nil, 0, 0, "", fmt.Errorf("unrecognized photo type %T", p) - } else { - return DownloadPhoto(ctx, client, photo) - } -} diff --git a/pkg/connector/media/transfer.go b/pkg/connector/media/transfer.go index a35d4fa3..f9493356 100644 --- a/pkg/connector/media/transfer.go +++ b/pkg/connector/media/transfer.go @@ -1,20 +1,62 @@ package media import ( + "bytes" "context" "fmt" + "net/http" "github.com/gotd/td/telegram/downloader" "github.com/gotd/td/tg" + "github.com/rs/zerolog" "maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" "go.mau.fi/util/lottie" + "go.mau.fi/mautrix-telegram/pkg/connector/ids" "go.mau.fi/mautrix-telegram/pkg/connector/store" ) +type dimensionable interface { + GetW() int + GetH() int +} + +func getLargestPhotoSize(sizes []tg.PhotoSizeClass) (width, height int, largest tg.PhotoSizeClass) { + if len(sizes) == 0 { + panic("cannot get largest size from empty list of sizes") + } + + var maxSize int + for _, s := range sizes { + var currentSize int + switch size := s.(type) { + case *tg.PhotoSize: + currentSize = size.GetSize() + case *tg.PhotoCachedSize: + currentSize = max(size.GetW(), size.GetH()) + case *tg.PhotoSizeProgressive: + currentSize = max(size.GetW(), size.GetH()) + case *tg.PhotoPathSize: + currentSize = len(size.GetBytes()) + case *tg.PhotoStrippedSize: + currentSize = len(size.GetBytes()) + } + + if currentSize > maxSize { + maxSize = currentSize + largest = s + if d, ok := s.(dimensionable); ok { + width = d.GetW() + height = d.GetH() + } + } + } + return +} + // getLocationID converts a Telegram [tg.Document], // [tg.InputDocumentFileLocation], [tg.InputPeerPhotoFileLocation], // [tg.InputFileLocation], or [tg.InputPhotoFileLocation] into a [LocationID] @@ -56,72 +98,234 @@ func (c AnimatedStickerConfig) WebmConvert() bool { return c.ConvertFromWebm && c.Target != "webm" } +// Transferer is a utility for downloading media from Telegram and uploading it +// to Matrix. +// TODO better name? type Transferer struct { - RoomID id.RoomID - Filename string - IsSticker bool - Config AnimatedStickerConfig + client downloader.Client + + roomID id.RoomID + filename string + animatedStickerConfig *AnimatedStickerConfig + + fileInfo event.FileInfo } -func NewTransferer(cfg AnimatedStickerConfig) *Transferer { - return &Transferer{Config: cfg} +type ReadyTransferer struct { + inner *Transferer + loc tg.InputFileLocationClass } +// NewTransferer creates a new [Transferer] with the given [downloader.Client]. +// The client is used to download the media from Telegram. +func NewTransferer(client downloader.Client) *Transferer { + return &Transferer{client: client} +} + +// WithRoomID sets the room ID for the [Transferer]. func (t *Transferer) WithRoomID(roomID id.RoomID) *Transferer { - t.RoomID = roomID + t.roomID = roomID return t } +// WithFilename sets the filename for the [Transferer]. func (t *Transferer) WithFilename(filename string) *Transferer { - t.Filename = filename + t.filename = filename return t } -func (t *Transferer) WithIsSticker(isSticker bool) *Transferer { - t.IsSticker = isSticker +func (t *Transferer) WithMIMEType(mimeType string) *Transferer { + t.fileInfo.MimeType = mimeType return t } -func (t *Transferer) Transfer(ctx context.Context, store *store.Container, client downloader.Client, intent bridgev2.MatrixAPI, loc tg.InputFileLocationClass) (mxc id.ContentURIString, encryptedFileInfo *event.EncryptedFileInfo, size int, mimeType string, err error) { - locationID := getLocationID(loc) +// WithStickerConfig sets the animated sticker config for the [Transferer]. +func (t *Transferer) WithStickerConfig(cfg AnimatedStickerConfig) *Transferer { + t.animatedStickerConfig = &cfg + return t +} + +func (t *Transferer) WithThumbnail(uri id.ContentURIString, file *event.EncryptedFileInfo, info *event.FileInfo) *Transferer { + t.fileInfo.ThumbnailURL = uri + t.fileInfo.ThumbnailFile = file + t.fileInfo.ThumbnailInfo = info + return t +} + +func (t *Transferer) WithVideo(attr *tg.DocumentAttributeVideo) *Transferer { + t.fileInfo.Width, t.fileInfo.Height = attr.W, attr.H + t.fileInfo.Duration = int(attr.Duration * 1000) + return t +} + +// WithDocument transforms a [Transferer] to a [ReadyTransferer] by setting the +// given document as the location that will be downloaded by the +// [ReadyTransferer]. +func (t *Transferer) WithDocument(doc tg.DocumentClass, thumbnail bool) *ReadyTransferer { + document := doc.(*tg.Document) + documentFileLocation := tg.InputDocumentFileLocation{ + ID: document.GetID(), + AccessHash: document.GetAccessHash(), + FileReference: document.GetFileReference(), + } + if thumbnail { + _, _, largestThumbnail := getLargestPhotoSize(document.Thumbs) + documentFileLocation.ThumbSize = largestThumbnail.GetType() + } else { + t.fileInfo.Size = int(document.Size) + t.fileInfo.MimeType = document.GetMimeType() + } + return &ReadyTransferer{t, &documentFileLocation} +} + +// WithPhoto transforms a [Transferer] to a [ReadyTransferer] by setting the +// given photo as the location that will be downloaded by the +// [ReadyTransferer]. +func (t *Transferer) WithPhoto(pc tg.PhotoClass) *ReadyTransferer { + photo := pc.(*tg.Photo) + var largest tg.PhotoSizeClass + t.fileInfo.Width, t.fileInfo.Height, largest = getLargestPhotoSize(photo.GetSizes()) + return &ReadyTransferer{ + inner: t, + loc: &tg.InputPhotoFileLocation{ + ID: photo.GetID(), + AccessHash: photo.GetAccessHash(), + FileReference: photo.GetFileReference(), + ThumbSize: largest.GetType(), + }, + } +} + +// WithUser transforms a [Transferer] to a [ReadyTransferer] by setting the +// given user's photo as the location that will be downloaded by the +// [ReadyTransferer]. +func (t *Transferer) WithUserPhoto(user *tg.User, photoID int64) *ReadyTransferer { + return &ReadyTransferer{ + inner: t, + loc: &tg.InputPeerPhotoFileLocation{ + Peer: &tg.InputPeerUser{UserID: user.GetID()}, + PhotoID: photoID, + Big: true, + }, + } +} + +// Transfer downloads the media from Telegram and uploads it to Matrix. +// +// If the file is already in the database, the MXC URI will be reused. The +// file's MXC URI will only be cached if the room ID is unset or if the room is +// not encrypted. +// +// If there is a sticker config on the [Transferer], this function converts +// animated stickers to the target format specified by the specified +// [AnimatedStickerConfig]. +func (t *ReadyTransferer) Transfer(ctx context.Context, store *store.Container, intent bridgev2.MatrixAPI) (mxc id.ContentURIString, encryptedFileInfo *event.EncryptedFileInfo, outFileInfo *event.FileInfo, err error) { + locationID := getLocationID(t.loc) + log := zerolog.Ctx(ctx).With(). + Str("component", "media_transfer"). + Str("location_id", string(locationID)). + Logger() + if file, err := store.TelegramFile.GetByLocationID(ctx, locationID); err != nil { - return "", nil, 0, "", fmt.Errorf("failed to search for Telegram file by location ID: %w", err) + return "", nil, nil, fmt.Errorf("failed to search for Telegram file by location ID: %w", err) } else if file != nil { - return file.MXC, nil, file.Size, file.MIMEType, nil + t.inner.fileInfo.Size, t.inner.fileInfo.MimeType = file.Size, file.MIMEType + return file.MXC, nil, &t.inner.fileInfo, nil } - var data []byte - data, mimeType, err = DownloadFileLocation(ctx, client, loc) + data, _, err := t.Download(ctx) if err != nil { - return "", nil, 0, "", fmt.Errorf("downloading file failed: %w", err) + return "", nil, nil, fmt.Errorf("downloading file failed: %w", err) } - if t.IsSticker { - if lottie.Supported() && t.Config.TGSConvert() && mimeType == "application/x-gzip" { - data, err = lottie.ConvertBytes(ctx, data, t.Config.Target, t.Config.Args.Width, t.Config.Args.Height, fmt.Sprintf("%d", t.Config.Args.FPS)) + if t.inner.animatedStickerConfig != nil { + if lottie.Supported() && t.inner.animatedStickerConfig.TGSConvert() && t.inner.fileInfo.MimeType == "application/x-tgsticker" { + newData, err := lottie.ConvertBytes(ctx, data, + t.inner.animatedStickerConfig.Target, + t.inner.animatedStickerConfig.Args.Width, + t.inner.animatedStickerConfig.Args.Height, + fmt.Sprintf("%d", t.inner.animatedStickerConfig.Args.FPS)) if err != nil { - return "", nil, 0, "", err + log.Err(err).Msg("failed to convert animated sticker") + } else { + data = newData + t.inner.fileInfo.Size = len(data) + t.inner.fileInfo.MimeType = fmt.Sprintf("image/%s", t.inner.animatedStickerConfig.Target) } - mimeType = fmt.Sprintf("image/%s", t.Config.Target) // TODO support ffmpeg conversion // } else if ffmpeg.Supported() && t.Config.WebmConvert() && mimeType == "video/webm" { } } - mxcURI, encryptedFileInfo, err := intent.UploadMedia(ctx, t.RoomID, data, t.Filename, mimeType) + mxc, encryptedFileInfo, err = intent.UploadMedia(ctx, t.inner.roomID, data, t.inner.filename, t.inner.fileInfo.MimeType) if err != nil { - return "", nil, 0, "", err + return "", nil, nil, fmt.Errorf("failed to upload media to Matrix: %w", err) } - if len(mxcURI) > 0 { + + // If it's an unencrypted file, cache the MXC URI corresponding to the + // location ID. + if len(mxc) > 0 { file := store.TelegramFile.New() file.LocationID = locationID - file.MXC = mxcURI - file.Size = len(data) - file.MIMEType = mimeType - // TODO width, height, thumbnail? + file.MXC = mxc + file.Size = t.inner.fileInfo.Size + file.MIMEType = t.inner.fileInfo.MimeType if err = file.Insert(ctx); err != nil { - return "", nil, 0, "", fmt.Errorf("failed to insert Telegram file into database: %w", err) + log.Err(err).Msg("failed to insert Telegram file into database") } } - return mxcURI, encryptedFileInfo, len(data), mimeType, nil + return mxc, encryptedFileInfo, &t.inner.fileInfo, nil +} + +// Download downloads the media from Telegram. +func (t *ReadyTransferer) Download(ctx context.Context) (data []byte, fileInfo *event.FileInfo, err error) { + // TODO convert entire function to streaming? Maybe at least stream to file? + var buf bytes.Buffer + storageFileTypeClass, err := downloader.NewDownloader().Download(t.inner.client, t.loc).Stream(ctx, &buf) + if err != nil { + return nil, nil, err + } + if t.inner.fileInfo.MimeType == "" { + switch storageFileTypeClass.(type) { + case *tg.StorageFileJpeg: + t.inner.fileInfo.MimeType = "image/jpeg" + case *tg.StorageFileGif: + t.inner.fileInfo.MimeType = "image/gif" + case *tg.StorageFilePng: + t.inner.fileInfo.MimeType = "image/png" + case *tg.StorageFilePdf: + t.inner.fileInfo.MimeType = "application/pdf" + case *tg.StorageFileMp3: + t.inner.fileInfo.MimeType = "audio/mp3" + case *tg.StorageFileMov: + t.inner.fileInfo.MimeType = "video/quicktime" + case *tg.StorageFileMp4: + t.inner.fileInfo.MimeType = "video/mp4" + case *tg.StorageFileWebp: + t.inner.fileInfo.MimeType = "image/webp" + default: + t.inner.fileInfo.MimeType = http.DetectContentType(buf.Bytes()) + } + } + t.inner.fileInfo.Size = len(data) + return buf.Bytes(), &t.inner.fileInfo, nil +} + +// DirectDownloadURL returns the direct download URL for the media. +func (t *ReadyTransferer) DirectDownloadURL(ctx context.Context, portal *bridgev2.Portal, msgID int, thumbnail bool) (id.ContentURIString, *event.FileInfo, error) { + peerType, chatID, err := ids.ParsePortalID(portal.ID) + if err != nil { + return "", nil, err + } + mediaID, err := ids.DirectMediaInfo{ + PeerType: peerType, + ChatID: chatID, + MessageID: int64(msgID), + Thumbnail: thumbnail, + }.AsMediaID() + if err != nil { + return "", nil, err + } + mxc, err := portal.Bridge.Matrix.GenerateContentURI(ctx, mediaID) + return mxc, &t.inner.fileInfo, err } diff --git a/pkg/connector/msgconv/tomatrix.go b/pkg/connector/msgconv/tomatrix.go index b3295ea4..52deed68 100644 --- a/pkg/connector/msgconv/tomatrix.go +++ b/pkg/connector/msgconv/tomatrix.go @@ -47,6 +47,7 @@ func (mc *MessageConverter) ToMatrix(ctx context.Context, portal *bridgev2.Porta } // TODO formatting + // TODO combine with other media cm.Parts = append(cm.Parts, &bridgev2.ConvertedMessagePart{ ID: networkid.PartID("caption"), Type: event.EventMessage, @@ -134,158 +135,116 @@ func (mc *MessageConverter) webpageToBeeperLinkPreview(ctx context.Context, inte } if pc, ok := webpage.GetPhoto(); ok && pc.TypeID() == tg.PhotoTypeID { - var data []byte - data, preview.ImageWidth, preview.ImageHeight, preview.ImageType, err = media.DownloadPhoto(ctx, mc.client.API(), pc.(*tg.Photo)) - if err != nil { - return nil, err - } - preview.ImageSize = len(data) - preview.ImageURL, preview.ImageEncryption, err = intent.UploadMedia(ctx, "", data, "", preview.ImageType) + var fileInfo *event.FileInfo + preview.ImageURL, preview.ImageEncryption, fileInfo, err = media.NewTransferer(mc.client.API()). + WithPhoto(pc). + Transfer(ctx, mc.store, intent) if err != nil { return nil, err } + preview.ImageSize, preview.ImageWidth, preview.ImageHeight = fileInfo.Size, fileInfo.Width, fileInfo.Height } return preview, nil } -func (mc *MessageConverter) directMedia(ctx context.Context, portal *bridgev2.Portal, msgID int, thumbnail bool) (uri id.ContentURIString, err error) { - if !mc.useDirectMedia { - return "", nil - } - - peerType, chatID, err := ids.ParsePortalID(portal.ID) - if err != nil { - return "", err - } - mediaID, err := ids.DirectMediaInfo{ - PeerType: peerType, - ChatID: chatID, - MessageID: int64(msgID), - Thumbnail: thumbnail, - }.AsMediaID() - if err != nil { - return "", err - } - return portal.Bridge.Matrix.GenerateContentURI(ctx, mediaID) -} - func (mc *MessageConverter) convertMediaRequiringUpload(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI, msgID int, msgMedia tg.MessageMediaClass) (*bridgev2.ConvertedMessagePart, *database.DisappearingSetting, error) { + log := zerolog.Ctx(ctx).With(). + Str("conversion_direction", "to_matrix"). + Str("portal_id", string(portal.ID)). + Int("msg_id", msgID). + Logger() var partID networkid.PartID - var msgType event.MessageType - var filename string - var audio *event.MSC1767Audio - var voice *event.MSC3245Voice - var info event.FileInfo + var content event.MessageEventContent + + transferer := media.NewTransferer(mc.client.API()).WithRoomID(portal.MXID) + var mediaTransferer *media.ReadyTransferer // Determine the filename and some other information switch msgMedia := msgMedia.(type) { case *tg.MessageMediaPhoto: partID = networkid.PartID("photo") - msgType = event.MsgImage - filename = "image" - if photo, ok := msgMedia.Photo.(*tg.Photo); ok { - info.Width, info.Height, _ = media.GetLargestPhotoSize(photo.GetSizes()) - } + content.MsgType = event.MsgImage + content.Body = "image" + mediaTransferer = transferer.WithPhoto(msgMedia.Photo) case *tg.MessageMediaDocument: - partID = networkid.PartID("document") - msgType = event.MsgFile document, ok := msgMedia.Document.(*tg.Document) - info.Size = int(document.Size) if !ok { return nil, nil, fmt.Errorf("unrecognized document type %T", msgMedia.Document) } - if thumbSizes, ok := document.GetThumbs(); ok { - info.ThumbnailInfo = &event.FileInfo{} - var largestThumbnail tg.PhotoSizeClass - info.ThumbnailInfo.Width, info.ThumbnailInfo.Height, largestThumbnail = media.GetLargestPhotoSize(thumbSizes) + partID = networkid.PartID("document") + content.MsgType = event.MsgFile + if _, ok := document.GetThumbs(); ok { + var thumbnailURL id.ContentURIString + var thumbnailFile *event.EncryptedFileInfo + var thumbnailInfo *event.FileInfo var err error - info.ThumbnailInfo.ThumbnailURL, err = mc.directMedia(ctx, portal, msgID, true) - if err != nil { - return nil, nil, err - } - if info.ThumbnailInfo.ThumbnailURL == "" { - info.ThumbnailInfo.ThumbnailURL, info.ThumbnailInfo.ThumbnailFile, info.ThumbnailInfo.Size, info.ThumbnailInfo.MimeType, err = media.NewTransferer(mc.animatedStickerConfig). - WithRoomID(portal.MXID). - Transfer(ctx, mc.store, mc.client.API(), intent, &tg.InputDocumentFileLocation{ - ID: document.GetID(), - AccessHash: document.GetAccessHash(), - FileReference: document.GetFileReference(), - ThumbSize: largestThumbnail.GetType(), - }) + thumbnailTransferer := media.NewTransferer(mc.client.API()). + WithRoomID(portal.MXID). + WithDocument(document, true) + if mc.useDirectMedia { + thumbnailURL, thumbnailInfo, err = thumbnailTransferer.DirectDownloadURL(ctx, portal, msgID, true) if err != nil { - return nil, nil, err + log.Err(err).Msg("error getting direct download URL for thumbnail") } } + if thumbnailURL == "" { + thumbnailURL, thumbnailFile, thumbnailInfo, err = thumbnailTransferer.Transfer(ctx, mc.store, intent) + if err != nil { + return nil, nil, fmt.Errorf("error transferring thumbnail: %w", err) + } + } + + transferer = transferer.WithThumbnail(thumbnailURL, thumbnailFile, thumbnailInfo) } for _, attr := range document.GetAttributes() { switch a := attr.(type) { case *tg.DocumentAttributeFilename: - filename = a.GetFileName() + content.Body = a.GetFileName() case *tg.DocumentAttributeVideo: - msgType = event.MsgVideo - info.Width, info.Height = a.W, a.H - info.Duration = int(a.Duration * 1000) + content.MsgType = event.MsgVideo + transferer = transferer.WithVideo(a) case *tg.DocumentAttributeAudio: - msgType = event.MsgAudio - audio = &event.MSC1767Audio{ + content.MsgType = event.MsgAudio + content.MSC1767Audio = &event.MSC1767Audio{ Duration: a.Duration * 1000, } if wf, ok := a.GetWaveform(); ok { for _, v := range waveform.Decode(wf) { - audio.Waveform = append(audio.Waveform, int(v)<<5) + content.MSC1767Audio.Waveform = append(content.MSC1767Audio.Waveform, int(v)<<5) } } if a.Voice { - voice = &event.MSC3245Voice{} + content.MSC3245Voice = &event.MSC3245Voice{} } } } + + mediaTransferer = transferer. + WithFilename(content.Body). + WithDocument(msgMedia.Document, false) default: return nil, nil, fmt.Errorf("unhandled media type %T", msgMedia) } - var encryptedFileInfo *event.EncryptedFileInfo - - mxcURI, err := mc.directMedia(ctx, portal, msgID, false) - if err != nil { - return nil, nil, err + var err error + if mc.useDirectMedia { + content.URL, content.Info, err = mediaTransferer.DirectDownloadURL(ctx, portal, msgID, false) + if err != nil { + log.Err(err).Msg("error getting direct download URL for media") + } } - - if mxcURI == "" { - var data []byte - switch msgMedia := msgMedia.(type) { - case *tg.MessageMediaPhoto: - // TODO convert to Transfer - data, _, _, info.MimeType, err = media.DownloadPhotoMedia(ctx, mc.client.API(), msgMedia) - if _, ok := msgMedia.GetTTLSeconds(); ok { - filename = "disappearing_image" + exmime.ExtensionFromMimetype(info.MimeType) - } else { - filename = "image" + exmime.ExtensionFromMimetype(info.MimeType) - } - case *tg.MessageMediaDocument: - document, ok := msgMedia.Document.(*tg.Document) - if !ok { - return nil, nil, fmt.Errorf("unrecognized document type %T", msgMedia.Document) - } - - info.MimeType = document.GetMimeType() - // TODO convert to Transfer - data, err = media.DownloadDocument(ctx, mc.client.API(), document) - default: - return nil, nil, fmt.Errorf("unhandled media type %T", msgMedia) - } + if content.URL == "" { + content.URL, content.File, content.Info, err = mediaTransferer.Transfer(ctx, mc.store, intent) if err != nil { - return nil, nil, err + return nil, nil, fmt.Errorf("error transferring media: %w", err) } - - mxcURI, encryptedFileInfo, err = intent.UploadMedia(ctx, portal.MXID, data, filename, info.MimeType) - if err != nil { - return nil, nil, err + if msgMedia.TypeID() == tg.MessageMediaPhotoTypeID { + content.Body = content.Body + exmime.ExtensionFromMimetype(content.Info.MimeType) } } @@ -304,6 +263,9 @@ func (mc *MessageConverter) convertMediaRequiringUpload(ctx context.Context, por var disappearingSetting *database.DisappearingSetting if t, ok := msgMedia.(ttlable); ok { if ttl, ok := t.GetTTLSeconds(); ok { + if msgMedia.TypeID() == tg.MessageMediaPhotoTypeID { + content.Body = "disappearing_" + content.Body + } disappearingSetting = &database.DisappearingSetting{ Type: database.DisappearingTypeAfterSend, Timer: time.Duration(ttl) * time.Second, @@ -312,18 +274,10 @@ func (mc *MessageConverter) convertMediaRequiringUpload(ctx context.Context, por } return &bridgev2.ConvertedMessagePart{ - ID: partID, - Type: event.EventMessage, - Content: &event.MessageEventContent{ - MsgType: msgType, - Body: filename, - URL: mxcURI, - File: encryptedFileInfo, - Info: &info, - MSC1767Audio: audio, - MSC3245Voice: voice, - }, - Extra: extra, + ID: partID, + Type: event.EventMessage, + Content: &content, + Extra: extra, }, disappearingSetting, nil } diff --git a/pkg/connector/telegram.go b/pkg/connector/telegram.go index ba3da241..e28c64b5 100644 --- a/pkg/connector/telegram.go +++ b/pkg/connector/telegram.go @@ -369,18 +369,14 @@ func (t *TelegramClient) transferEmojisToMatrix(ctx context.Context, customEmoji } for _, customEmojiDocument := range customEmojiDocuments { - document := customEmojiDocument.(*tg.Document) - mxcURI, _, _, _, err := media.NewTransferer(t.main.Config.AnimatedSticker). - WithIsSticker(true). - Transfer(ctx, t.main.Store, t.client.API(), t.main.Bridge.Bot, &tg.InputDocumentFileLocation{ - ID: document.GetID(), - AccessHash: document.GetAccessHash(), - FileReference: document.GetFileReference(), - }) + mxcURI, _, _, err := media.NewTransferer(t.client.API()). + WithStickerConfig(t.main.Config.AnimatedSticker). + WithDocument(customEmojiDocument, false). + Transfer(ctx, t.main.Store, t.main.Bridge.Bot) if err != nil { return nil, err } - result[ids.MakeEmojiIDFromDocumentID(document.ID)] = string(mxcURI) + result[ids.MakeEmojiIDFromDocumentID(customEmojiDocument.GetID())] = string(mxcURI) } } return