diff --git a/pkg/connector/client.go b/pkg/connector/client.go index 3556c339..864e05e7 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -358,7 +358,7 @@ func (t *TelegramClient) GetChatInfo(ctx context.Context, portal *bridgev2.Porta avatar = &bridgev2.Avatar{ ID: ids.MakeAvatarID(photo.ID), Get: func(ctx context.Context) (data []byte, err error) { - data, _, err = download.DownloadPhoto(ctx, t.client.API(), photo) + data, _, _, _, err = download.DownloadPhoto(ctx, t.client.API(), photo) return }, } diff --git a/pkg/connector/directdownload.go b/pkg/connector/directdownload.go index c21ee980..77ab7e6a 100644 --- a/pkg/connector/directdownload.go +++ b/pkg/connector/directdownload.go @@ -84,14 +84,25 @@ func (tc *TelegramConnector) Download(ctx context.Context, mediaID networkid.Med var mimeType string switch media := media.(type) { case *tg.MessageMediaPhoto: - data, mimeType, err = download.DownloadPhotoMedia(ctx, client.client.API(), media) + data, _, _, mimeType, err = download.DownloadPhotoMedia(ctx, client.client.API(), media) case *tg.MessageMediaDocument: document, ok := media.Document.(*tg.Document) if !ok { return nil, fmt.Errorf("unrecognized document type %T", media.Document) } - mimeType = document.GetMimeType() - data, err = download.DownloadDocument(ctx, client.client.API(), document) + + if info.Thumbnail { + _, _, largestThumbnail := download.GetLargestPhotoSize(document.Thumbs) + data, mimeType, err = download.DownloadPhotoFileLocation(ctx, client.client.API(), &tg.InputDocumentFileLocation{ + ID: document.GetID(), + AccessHash: document.GetAccessHash(), + FileReference: document.GetFileReference(), + ThumbSize: largestThumbnail.GetType(), + }) + } else { + mimeType = document.GetMimeType() + data, err = download.DownloadDocument(ctx, client.client.API(), document) + } default: return nil, fmt.Errorf("unhandled media type %T", media) } diff --git a/pkg/connector/download/photo.go b/pkg/connector/download/photo.go index 81181543..638f3879 100644 --- a/pkg/connector/download/photo.go +++ b/pkg/connector/download/photo.go @@ -10,7 +10,16 @@ import ( "github.com/gotd/td/tg" ) -func GetLargestPhotoSize(sizes []tg.PhotoSizeClass) (largest tg.PhotoSizeClass) { +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 @@ -30,6 +39,24 @@ func GetLargestPhotoSize(sizes []tg.PhotoSizeClass) (largest tg.PhotoSizeClass) if currentSize > maxSize { maxSize = currentSize largest = s + if d, ok := s.(dimensionable); ok { + width = d.GetW() + height = d.GetH() + } + } + } + return +} + +func GetLargestDimensions(sizes []tg.PhotoSizeClass) (width, height int) { + for _, s := range sizes { + switch size := s.(type) { + case *tg.PhotoCachedSize: + width = size.GetW() + height = size.GetH() + case *tg.PhotoSizeProgressive: + width = size.GetW() + height = size.GetH() } } return @@ -65,23 +92,26 @@ func DownloadPhotoFileLocation(ctx context.Context, client downloader.Client, fi return buf.Bytes(), mimeType, nil } -func DownloadPhoto(ctx context.Context, client downloader.Client, photo *tg.Photo) (data []byte, mimeType string, err error) { - return DownloadPhotoFileLocation(ctx, client, &tg.InputPhotoFileLocation{ +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 = DownloadPhotoFileLocation(ctx, client, &tg.InputPhotoFileLocation{ ID: photo.GetID(), AccessHash: photo.GetAccessHash(), FileReference: photo.GetFileReference(), - ThumbSize: GetLargestPhotoSize(photo.GetSizes()).GetType(), + ThumbSize: largest.GetType(), }) + return } -func DownloadPhotoMedia(ctx context.Context, client downloader.Client, media *tg.MessageMediaPhoto) (data []byte, mimeType string, err error) { +func DownloadPhotoMedia(ctx context.Context, client downloader.Client, media *tg.MessageMediaPhoto) (data []byte, width, height int, mimeType string, err error) { p, ok := media.GetPhoto() if !ok { - return nil, "", fmt.Errorf("photo message sent without a photo") + return nil, 0, 0, "", fmt.Errorf("photo message sent without a photo") } photo, ok := p.(*tg.Photo) if !ok { - return nil, "", fmt.Errorf("unrecognized photo type %T", p) + return nil, 0, 0, "", fmt.Errorf("unrecognized photo type %T", p) } return DownloadPhoto(ctx, client, photo) } diff --git a/pkg/connector/ids/media.go b/pkg/connector/ids/media.go index 31b4e847..e5f02ec4 100644 --- a/pkg/connector/ids/media.go +++ b/pkg/connector/ids/media.go @@ -13,16 +13,18 @@ import ( // The format of the media ID is as follows (each character represents a single // byte, |'s added for clarity): // -// v|p|CCCCCCCC|TTTTTTTT +// v|p|cccccccc|mmmmmmmm|T // // v (int8) = binary encoding format version. Should be 0. // p (byte) = the peer type of the Telegram chat ID -// CCCCCCCC (int64) = the Telegram chat ID (big endian) -// TTTTTTTT (int64) = the Telegram message ID (big endian) +// cccccccc (int64) = the Telegram chat ID (big endian) +// mmmmmmmm (int64) = the Telegram message ID (big endian) +// T (byte) = 0 or 1 depending on whether it's a thumbnail type DirectMediaInfo struct { PeerType PeerType ChatID int64 MessageID int64 + Thumbnail bool } func (m DirectMediaInfo) AsMediaID() (networkid.MediaID, error) { @@ -32,6 +34,11 @@ func (m DirectMediaInfo) AsMediaID() (networkid.MediaID, error) { } mediaID = binary.BigEndian.AppendUint64(mediaID, uint64(m.ChatID)) // Telegram Chat ID mediaID = binary.BigEndian.AppendUint64(mediaID, uint64(m.MessageID)) // Telegram Message ID + if m.Thumbnail { + mediaID = append(mediaID, 0x01) + } else { + mediaID = append(mediaID, 0x00) + } return mediaID, nil } @@ -44,7 +51,7 @@ func ParseDirectMediaInfo(mediaID networkid.MediaID) (info DirectMediaInfo, err err = fmt.Errorf("invalid version %d", mediaID[0]) return } - if len(mediaID) != 18 { + if len(mediaID) != 18 && len(mediaID) != 19 { err = fmt.Errorf("invalid media ID") return } @@ -54,5 +61,8 @@ func ParseDirectMediaInfo(mediaID networkid.MediaID) (info DirectMediaInfo, err } info.ChatID = int64(binary.BigEndian.Uint64(mediaID[2:])) info.MessageID = int64(binary.BigEndian.Uint64(mediaID[10:])) + if len(mediaID) == 19 { + info.Thumbnail = mediaID[18] == 1 + } return } diff --git a/pkg/connector/msgconv/tomatrix.go b/pkg/connector/msgconv/tomatrix.go index 4ab2f157..de9b59e1 100644 --- a/pkg/connector/msgconv/tomatrix.go +++ b/pkg/connector/msgconv/tomatrix.go @@ -119,12 +119,12 @@ func (mc *MessageConverter) ToMatrix(ctx context.Context, portal *bridgev2.Porta return cm, nil } -func (mc *MessageConverter) webpageToBeeperLinkPreview(ctx context.Context, intent bridgev2.MatrixAPI, media tg.MessageMediaClass) (*event.BeeperLinkPreview, error) { +func (mc *MessageConverter) webpageToBeeperLinkPreview(ctx context.Context, intent bridgev2.MatrixAPI, media tg.MessageMediaClass) (preview *event.BeeperLinkPreview, err error) { webpage, ok := media.(*tg.MessageMediaWebPage).Webpage.(*tg.WebPage) if !ok { return nil, nil } - preview := &event.BeeperLinkPreview{ + preview = &event.BeeperLinkPreview{ MatchedURL: webpage.URL, LinkPreview: event.LinkPreview{ Title: webpage.Title, @@ -135,25 +135,13 @@ func (mc *MessageConverter) webpageToBeeperLinkPreview(ctx context.Context, inte if pc, ok := webpage.GetPhoto(); ok && pc.TypeID() == tg.PhotoTypeID { photo := pc.(*tg.Photo) - for _, s := range photo.GetSizes() { - switch size := s.(type) { - case *tg.PhotoCachedSize: - preview.ImageWidth = size.GetW() - preview.ImageHeight = size.GetH() - case *tg.PhotoSizeProgressive: - preview.ImageWidth = size.GetW() - preview.ImageHeight = size.GetH() - } - } - - data, mimeType, err := download.DownloadPhoto(ctx, mc.client.API(), photo) + var data []byte + data, preview.ImageWidth, preview.ImageHeight, preview.ImageType, err = download.DownloadPhoto(ctx, mc.client.API(), photo) if err != nil { return nil, err } preview.ImageSize = len(data) - preview.ImageType = mimeType - - preview.ImageURL, preview.ImageEncryption, err = intent.UploadMedia(ctx, "", data, "", mimeType) + preview.ImageURL, preview.ImageEncryption, err = intent.UploadMedia(ctx, "", data, "", preview.ImageType) if err != nil { return nil, err } @@ -162,12 +150,34 @@ func (mc *MessageConverter) webpageToBeeperLinkPreview(ctx context.Context, inte 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, media tg.MessageMediaClass) (*bridgev2.ConvertedMessagePart, *database.DisappearingSetting, error) { var partID networkid.PartID var msgType event.MessageType var filename string var audio *event.MSC1767Audio var voice *event.MSC3245Voice + var info event.FileInfo // Determine the filename and some other information switch media := media.(type) { @@ -175,18 +185,55 @@ func (mc *MessageConverter) convertMediaRequiringUpload(ctx context.Context, por partID = networkid.PartID("photo") msgType = event.MsgImage filename = "image" + if photo, ok := media.Photo.(*tg.Photo); ok { + info.Width, info.Height, _ = download.GetLargestPhotoSize(photo.GetSizes()) + } case *tg.MessageMediaDocument: partID = networkid.PartID("document") msgType = event.MsgFile document, ok := media.Document.(*tg.Document) + info.Size = int(document.Size) if !ok { return nil, nil, fmt.Errorf("unrecognized document type %T", media.Document) } + if thumbSizes, ok := document.GetThumbs(); ok { + info.ThumbnailInfo = &event.FileInfo{} + var largestThumbnail tg.PhotoSizeClass + info.ThumbnailInfo.Width, info.ThumbnailInfo.Height, largestThumbnail = download.GetLargestPhotoSize(thumbSizes) + + var err error + info.ThumbnailInfo.ThumbnailURL, err = mc.directMedia(ctx, portal, msgID, true) + if err != nil { + return nil, nil, err + } + + if info.ThumbnailInfo.ThumbnailURL == "" { + data, mimeType, err := download.DownloadPhotoFileLocation(ctx, mc.client.API(), &tg.InputDocumentFileLocation{ + ID: document.GetID(), + AccessHash: document.GetAccessHash(), + FileReference: document.GetFileReference(), + ThumbSize: largestThumbnail.GetType(), + }) + if err != nil { + return nil, nil, fmt.Errorf("downloading thumbnail failed: %w", err) + } + + info.ThumbnailInfo.ThumbnailURL, info.ThumbnailInfo.ThumbnailFile, err = intent.UploadMedia(ctx, "", data, filename, mimeType) + if err != nil { + return nil, nil, err + } + } + } + for _, attr := range document.GetAttributes() { switch a := attr.(type) { case *tg.DocumentAttributeFilename: filename = a.GetFileName() + case *tg.DocumentAttributeVideo: + msgType = event.MsgVideo + info.Width, info.Height = a.W, a.H + info.Duration = int(a.Duration * 1000) case *tg.DocumentAttributeAudio: msgType = event.MsgAudio audio = &event.MSC1767Audio{ @@ -206,27 +253,11 @@ func (mc *MessageConverter) convertMediaRequiringUpload(ctx context.Context, por return nil, nil, fmt.Errorf("unhandled media type %T", media) } - var mxcURI id.ContentURIString var encryptedFileInfo *event.EncryptedFileInfo - if mc.useDirectMedia { - var err error - peerType, chatID, err := ids.ParsePortalID(portal.ID) - if err != nil { - return nil, nil, err - } - mediaID, err := ids.DirectMediaInfo{ - PeerType: peerType, - ChatID: chatID, - MessageID: int64(msgID), - }.AsMediaID() - if err != nil { - return nil, nil, err - } - mxcURI, err = portal.Bridge.Matrix.GenerateContentURI(ctx, mediaID) - if err != nil { - return nil, nil, err - } + mxcURI, err := mc.directMedia(ctx, portal, msgID, false) + if err != nil { + return nil, nil, err } if mxcURI == "" { @@ -241,7 +272,7 @@ func (mc *MessageConverter) convertMediaRequiringUpload(ctx context.Context, por filename = "image" + exmime.ExtensionFromMimetype(mimeType) } - data, mimeType, err = download.DownloadPhotoMedia(ctx, mc.client.API(), media) + data, _, _, mimeType, err = download.DownloadPhotoMedia(ctx, mc.client.API(), media) case *tg.MessageMediaDocument: document, ok := media.Document.(*tg.Document) if !ok { @@ -293,6 +324,7 @@ func (mc *MessageConverter) convertMediaRequiringUpload(ctx context.Context, por Body: filename, URL: mxcURI, File: encryptedFileInfo, + Info: &info, MSC1767Audio: audio, MSC3245Voice: voice, },