diff --git a/pkg/connector/client.go b/pkg/connector/client.go index f078b59f..29731a0f 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -17,8 +17,8 @@ import ( "maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2/networkid" - "go.mau.fi/mautrix-telegram/pkg/connector/download" "go.mau.fi/mautrix-telegram/pkg/connector/ids" + "go.mau.fi/mautrix-telegram/pkg/connector/media" "go.mau.fi/mautrix-telegram/pkg/connector/msgconv" "go.mau.fi/mautrix-telegram/pkg/connector/util" ) @@ -241,7 +241,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 = media.DownloadPhoto(ctx, t.client.API(), photo) return }, } @@ -304,7 +304,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 = download.DownloadPhotoFileLocation(ctx, t.client.API(), &tg.InputPeerPhotoFileLocation{ + data, _, err = media.DownloadFileLocation(ctx, t.client.API(), &tg.InputPeerPhotoFileLocation{ Peer: &tg.InputPeerUser{UserID: user.ID}, PhotoID: photo.PhotoID, Big: true, diff --git a/pkg/connector/directdownload.go b/pkg/connector/directdownload.go index 77ab7e6a..15e54409 100644 --- a/pkg/connector/directdownload.go +++ b/pkg/connector/directdownload.go @@ -12,8 +12,8 @@ import ( "maunium.net/go/mautrix/bridgev2/networkid" "maunium.net/go/mautrix/mediaproxy" - "go.mau.fi/mautrix-telegram/pkg/connector/download" "go.mau.fi/mautrix-telegram/pkg/connector/ids" + "go.mau.fi/mautrix-telegram/pkg/connector/media" ) var _ bridgev2.DirectMediableNetwork = (*TelegramConnector)(nil) @@ -63,14 +63,14 @@ func (tc *TelegramConnector) Download(ctx context.Context, mediaID networkid.Med return nil, err } - var media tg.MessageMediaClass + var msgMedia tg.MessageMediaClass if m, ok := messages.(getMessages); !ok { return nil, fmt.Errorf("unknown message type %T", messages) } else { var found bool for _, message := range m.GetMessages() { if msg, ok := message.(*tg.Message); ok && msg.ID == int(info.MessageID) { - media = msg.Media + msgMedia = msg.Media found = true break } @@ -82,18 +82,19 @@ func (tc *TelegramConnector) Download(ctx context.Context, mediaID networkid.Med var data []byte var mimeType string - switch media := media.(type) { + switch msgMedia := msgMedia.(type) { case *tg.MessageMediaPhoto: - data, _, _, mimeType, err = download.DownloadPhotoMedia(ctx, client.client.API(), media) + data, _, _, mimeType, err = media.DownloadPhotoMedia(ctx, client.client.API(), msgMedia) case *tg.MessageMediaDocument: - document, ok := media.Document.(*tg.Document) + document, ok := msgMedia.Document.(*tg.Document) if !ok { - return nil, fmt.Errorf("unrecognized document type %T", media.Document) + return nil, fmt.Errorf("unrecognized document type %T", msgMedia.Document) } + // Download the thumbnail for this media rather than the media itself. if info.Thumbnail { - _, _, largestThumbnail := download.GetLargestPhotoSize(document.Thumbs) - data, mimeType, err = download.DownloadPhotoFileLocation(ctx, client.client.API(), &tg.InputDocumentFileLocation{ + _, _, 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(), @@ -101,10 +102,10 @@ func (tc *TelegramConnector) Download(ctx context.Context, mediaID networkid.Med }) } else { mimeType = document.GetMimeType() - data, err = download.DownloadDocument(ctx, client.client.API(), document) + data, err = media.DownloadDocument(ctx, client.client.API(), document) } default: - return nil, fmt.Errorf("unhandled media type %T", media) + return nil, fmt.Errorf("unhandled media type %T", msgMedia) } if err != nil { return nil, err diff --git a/pkg/connector/download/document.go b/pkg/connector/media/document.go similarity index 62% rename from pkg/connector/download/document.go rename to pkg/connector/media/document.go index 422c0379..13594ab5 100644 --- a/pkg/connector/download/document.go +++ b/pkg/connector/media/document.go @@ -1,7 +1,6 @@ -package download +package media import ( - "bytes" "context" "github.com/gotd/td/telegram/downloader" @@ -9,12 +8,10 @@ import ( ) func DownloadDocument(ctx context.Context, client downloader.Client, document *tg.Document) ([]byte, error) { - file := tg.InputDocumentFileLocation{ + data, _, err := DownloadFileLocation(ctx, client, &tg.InputDocumentFileLocation{ ID: document.GetID(), AccessHash: document.GetAccessHash(), FileReference: document.GetFileReference(), - } - var buf bytes.Buffer - _, err := downloader.NewDownloader().Download(client, &file).Stream(ctx, &buf) - return buf.Bytes(), err + }) + return data, err } diff --git a/pkg/connector/media/download.go b/pkg/connector/media/download.go new file mode 100644 index 00000000..1425a61d --- /dev/null +++ b/pkg/connector/media/download.go @@ -0,0 +1,40 @@ +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, file tg.InputFileLocationClass) (data []byte, mimeType string, err error) { + // TODO convert to streaming? stream to file? + var buf bytes.Buffer + storageFileTypeClass, err := downloader.NewDownloader().Download(client, file).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/download/photo.go b/pkg/connector/media/photo.go similarity index 53% rename from pkg/connector/download/photo.go rename to pkg/connector/media/photo.go index 638f3879..164d39b3 100644 --- a/pkg/connector/download/photo.go +++ b/pkg/connector/media/photo.go @@ -1,10 +1,8 @@ -package download +package media import ( - "bytes" "context" "fmt" - "net/http" "github.com/gotd/td/telegram/downloader" "github.com/gotd/td/tg" @@ -48,54 +46,10 @@ func GetLargestPhotoSize(sizes []tg.PhotoSizeClass) (width, height int, largest 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 -} - -func DownloadPhotoFileLocation(ctx context.Context, client downloader.Client, file tg.InputFileLocationClass) (data []byte, mimeType string, err error) { - // TODO convert to streaming? - var buf bytes.Buffer - storageFileTypeClass, err := downloader.NewDownloader().Download(client, file).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 -} - 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{ + data, mimeType, err = DownloadFileLocation(ctx, client, &tg.InputPhotoFileLocation{ ID: photo.GetID(), AccessHash: photo.GetAccessHash(), FileReference: photo.GetFileReference(), @@ -105,13 +59,11 @@ func DownloadPhoto(ctx context.Context, client downloader.Client, photo *tg.Phot } 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 { + if p, ok := media.GetPhoto(); !ok { return nil, 0, 0, "", fmt.Errorf("photo message sent without a photo") - } - photo, ok := p.(*tg.Photo) - if !ok { + } 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) } - return DownloadPhoto(ctx, client, photo) } diff --git a/pkg/connector/media/transfer.go b/pkg/connector/media/transfer.go new file mode 100644 index 00000000..b15e42c4 --- /dev/null +++ b/pkg/connector/media/transfer.go @@ -0,0 +1,45 @@ +package media + +import ( + "context" + "fmt" + + "github.com/gotd/td/telegram/downloader" + "github.com/gotd/td/tg" + "maunium.net/go/mautrix/bridgev2" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" +) + +// LocationToID converts a Telegram [tg.Document], +// [tg.InputDocumentFileLocation], [tg.InputPeerPhotoFileLocation], +// [tg.InputFileLocation], or [tg.InputPhotoFileLocation] into a key for use in +// the telegram_file table. +func LocationToID(location any) (id string) { + switch location := location.(type) { + case *tg.Document: + return fmt.Sprintf("%d", location.ID) + case *tg.InputDocumentFileLocation: + return fmt.Sprintf("%d-%s", location.ID, location.ThumbSize) + case *tg.InputPhotoFileLocation: + return fmt.Sprintf("%d-%s", location.ID, location.ThumbSize) + case *tg.InputFileLocation: + return fmt.Sprintf("%d-%d", location.VolumeID, location.LocalID) + case *tg.InputPeerPhotoFileLocation: + return fmt.Sprintf("%d", location.PhotoID) + default: + panic(fmt.Errorf("unknown location type %T", location)) + } +} + +func TransferToMatrix(ctx context.Context, roomID id.RoomID, client downloader.Client, intent bridgev2.MatrixAPI, file tg.InputFileLocationClass, filenameOpt ...string) (id.ContentURIString, *event.EncryptedFileInfo, error) { + data, mimeType, err := DownloadFileLocation(ctx, client, file) + if err != nil { + return "", nil, fmt.Errorf("downloading file failed: %w", err) + } + var filename string + if len(filenameOpt) > 0 { + filename = filenameOpt[0] + } + return intent.UploadMedia(ctx, roomID, data, filename, mimeType) +} diff --git a/pkg/connector/msgconv/tomatrix.go b/pkg/connector/msgconv/tomatrix.go index 7c40f594..000709de 100644 --- a/pkg/connector/msgconv/tomatrix.go +++ b/pkg/connector/msgconv/tomatrix.go @@ -16,8 +16,8 @@ import ( "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" - "go.mau.fi/mautrix-telegram/pkg/connector/download" "go.mau.fi/mautrix-telegram/pkg/connector/ids" + "go.mau.fi/mautrix-telegram/pkg/connector/media" "go.mau.fi/mautrix-telegram/pkg/connector/util" "go.mau.fi/mautrix-telegram/pkg/connector/waveform" ) @@ -119,8 +119,8 @@ 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) (preview *event.BeeperLinkPreview, err error) { - webpage, ok := media.(*tg.MessageMediaWebPage).Webpage.(*tg.WebPage) +func (mc *MessageConverter) webpageToBeeperLinkPreview(ctx context.Context, intent bridgev2.MatrixAPI, msgMedia tg.MessageMediaClass) (preview *event.BeeperLinkPreview, err error) { + webpage, ok := msgMedia.(*tg.MessageMediaWebPage).Webpage.(*tg.WebPage) if !ok { return nil, nil } @@ -135,7 +135,7 @@ 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 = download.DownloadPhoto(ctx, mc.client.API(), pc.(*tg.Photo)) + data, preview.ImageWidth, preview.ImageHeight, preview.ImageType, err = media.DownloadPhoto(ctx, mc.client.API(), pc.(*tg.Photo)) if err != nil { return nil, err } @@ -170,7 +170,7 @@ func (mc *MessageConverter) directMedia(ctx context.Context, portal *bridgev2.Po 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) { +func (mc *MessageConverter) convertMediaRequiringUpload(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI, msgID int, msgMedia tg.MessageMediaClass) (*bridgev2.ConvertedMessagePart, *database.DisappearingSetting, error) { var partID networkid.PartID var msgType event.MessageType var filename string @@ -179,27 +179,27 @@ func (mc *MessageConverter) convertMediaRequiringUpload(ctx context.Context, por var info event.FileInfo // Determine the filename and some other information - switch media := media.(type) { + switch msgMedia := msgMedia.(type) { case *tg.MessageMediaPhoto: 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()) + if photo, ok := msgMedia.Photo.(*tg.Photo); ok { + info.Width, info.Height, _ = media.GetLargestPhotoSize(photo.GetSizes()) } case *tg.MessageMediaDocument: partID = networkid.PartID("document") msgType = event.MsgFile - document, ok := media.Document.(*tg.Document) + document, ok := msgMedia.Document.(*tg.Document) info.Size = int(document.Size) if !ok { - return nil, nil, fmt.Errorf("unrecognized document type %T", media.Document) + 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 = download.GetLargestPhotoSize(thumbSizes) + info.ThumbnailInfo.Width, info.ThumbnailInfo.Height, largestThumbnail = media.GetLargestPhotoSize(thumbSizes) var err error info.ThumbnailInfo.ThumbnailURL, err = mc.directMedia(ctx, portal, msgID, true) @@ -208,17 +208,12 @@ func (mc *MessageConverter) convertMediaRequiringUpload(ctx context.Context, por } if info.ThumbnailInfo.ThumbnailURL == "" { - data, mimeType, err := download.DownloadPhotoFileLocation(ctx, mc.client.API(), &tg.InputDocumentFileLocation{ + info.ThumbnailInfo.ThumbnailURL, info.ThumbnailInfo.ThumbnailFile, err = media.TransferToMatrix(ctx, portal.MXID, mc.client.API(), intent, &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 } @@ -249,7 +244,7 @@ func (mc *MessageConverter) convertMediaRequiringUpload(ctx context.Context, por } } default: - return nil, nil, fmt.Errorf("unhandled media type %T", media) + return nil, nil, fmt.Errorf("unhandled media type %T", msgMedia) } var encryptedFileInfo *event.EncryptedFileInfo @@ -263,31 +258,31 @@ func (mc *MessageConverter) convertMediaRequiringUpload(ctx context.Context, por var data []byte var mimeType string var err error - switch media := media.(type) { + switch msgMedia := msgMedia.(type) { case *tg.MessageMediaPhoto: - if _, ok := media.GetTTLSeconds(); ok { + if _, ok := msgMedia.GetTTLSeconds(); ok { filename = "disappearing_image" + exmime.ExtensionFromMimetype(mimeType) } else { filename = "image" + exmime.ExtensionFromMimetype(mimeType) } - data, _, _, mimeType, err = download.DownloadPhotoMedia(ctx, mc.client.API(), media) + data, _, _, mimeType, err = media.DownloadPhotoMedia(ctx, mc.client.API(), msgMedia) case *tg.MessageMediaDocument: - document, ok := media.Document.(*tg.Document) + document, ok := msgMedia.Document.(*tg.Document) if !ok { - return nil, nil, fmt.Errorf("unrecognized document type %T", media.Document) + return nil, nil, fmt.Errorf("unrecognized document type %T", msgMedia.Document) } mimeType = document.GetMimeType() - data, err = download.DownloadDocument(ctx, mc.client.API(), document) + data, err = media.DownloadDocument(ctx, mc.client.API(), document) default: - return nil, nil, fmt.Errorf("unhandled media type %T", media) + return nil, nil, fmt.Errorf("unhandled media type %T", msgMedia) } if err != nil { return nil, nil, err } - mxcURI, encryptedFileInfo, err = intent.UploadMedia(ctx, "", data, filename, mimeType) + mxcURI, encryptedFileInfo, err = intent.UploadMedia(ctx, portal.MXID, data, filename, mimeType) if err != nil { return nil, nil, err } @@ -297,7 +292,7 @@ func (mc *MessageConverter) convertMediaRequiringUpload(ctx context.Context, por // Handle spoilers // See: https://github.com/matrix-org/matrix-spec-proposals/pull/3725 - if s, ok := media.(spoilable); ok && s.GetSpoiler() { + if s, ok := msgMedia.(spoilable); ok && s.GetSpoiler() { extra["town.robin.msc3725.content_warning"] = map[string]any{ "type": "town.robin.msc3725.spoiler", } @@ -306,7 +301,7 @@ func (mc *MessageConverter) convertMediaRequiringUpload(ctx context.Context, por // Handle disappearing messages var disappearingSetting *database.DisappearingSetting - if t, ok := media.(ttlable); ok { + if t, ok := msgMedia.(ttlable); ok { if ttl, ok := t.GetTTLSeconds(); ok { disappearingSetting = &database.DisappearingSetting{ Type: database.DisappearingTypeAfterSend,