media: major refactor of downloading/direct URL
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
This commit is contained in:
@@ -240,11 +240,11 @@ func (t *TelegramClient) GetChatInfo(ctx context.Context, portal *bridgev2.Porta
|
|||||||
return nil, fmt.Errorf("full chat is not %T", chatFull)
|
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{
|
avatar = &bridgev2.Avatar{
|
||||||
ID: ids.MakeAvatarID(photo.ID),
|
ID: ids.MakeAvatarID(photo.GetID()),
|
||||||
Get: func(ctx context.Context) (data []byte, err error) {
|
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
|
return
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -307,11 +307,7 @@ func (t *TelegramClient) getUserInfoFromTelegramUser(user *tg.User) (*bridgev2.U
|
|||||||
avatar = &bridgev2.Avatar{
|
avatar = &bridgev2.Avatar{
|
||||||
ID: ids.MakeAvatarID(photo.PhotoID),
|
ID: ids.MakeAvatarID(photo.PhotoID),
|
||||||
Get: func(ctx context.Context) (data []byte, err error) {
|
Get: func(ctx context.Context) (data []byte, err error) {
|
||||||
data, _, err = media.DownloadFileLocation(ctx, t.client.API(), &tg.InputPeerPhotoFileLocation{
|
data, _, err = media.NewTransferer(t.client.API()).WithUserPhoto(user, photo.PhotoID).Download(ctx)
|
||||||
Peer: &tg.InputPeerUser{UserID: user.ID},
|
|
||||||
PhotoID: photo.PhotoID,
|
|
||||||
Big: true,
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,41 +80,26 @@ func (tc *TelegramConnector) Download(ctx context.Context, mediaID networkid.Med
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var data []byte
|
transferer := media.NewTransferer(client.client.API())
|
||||||
var mimeType string
|
var readyTransferer *media.ReadyTransferer
|
||||||
switch msgMedia := msgMedia.(type) {
|
switch msgMedia := msgMedia.(type) {
|
||||||
case *tg.MessageMediaPhoto:
|
case *tg.MessageMediaPhoto:
|
||||||
data, _, _, mimeType, err = media.DownloadPhotoMedia(ctx, client.client.API(), msgMedia)
|
readyTransferer = transferer.WithPhoto(msgMedia.Photo)
|
||||||
case *tg.MessageMediaDocument:
|
case *tg.MessageMediaDocument:
|
||||||
document, ok := msgMedia.Document.(*tg.Document)
|
readyTransferer = transferer.WithDocument(msgMedia.Document, info.Thumbnail)
|
||||||
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)
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unhandled media type %T", msgMedia)
|
return nil, fmt.Errorf("unhandled media type %T", msgMedia)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data, fileInfo, err := readyTransferer.Download(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &mediaproxy.GetMediaResponseData{
|
return &mediaproxy.GetMediaResponseData{
|
||||||
Reader: io.NopCloser(bytes.NewBuffer(data)),
|
Reader: io.NopCloser(bytes.NewBuffer(data)),
|
||||||
ContentType: mimeType,
|
ContentType: fileInfo.MimeType,
|
||||||
ContentLength: int64(len(data)),
|
ContentLength: int64(fileInfo.Size),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+235
-31
@@ -1,20 +1,62 @@
|
|||||||
package media
|
package media
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"github.com/gotd/td/telegram/downloader"
|
"github.com/gotd/td/telegram/downloader"
|
||||||
"github.com/gotd/td/tg"
|
"github.com/gotd/td/tg"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
"maunium.net/go/mautrix/bridgev2"
|
"maunium.net/go/mautrix/bridgev2"
|
||||||
"maunium.net/go/mautrix/event"
|
"maunium.net/go/mautrix/event"
|
||||||
"maunium.net/go/mautrix/id"
|
"maunium.net/go/mautrix/id"
|
||||||
|
|
||||||
"go.mau.fi/util/lottie"
|
"go.mau.fi/util/lottie"
|
||||||
|
|
||||||
|
"go.mau.fi/mautrix-telegram/pkg/connector/ids"
|
||||||
"go.mau.fi/mautrix-telegram/pkg/connector/store"
|
"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],
|
// getLocationID converts a Telegram [tg.Document],
|
||||||
// [tg.InputDocumentFileLocation], [tg.InputPeerPhotoFileLocation],
|
// [tg.InputDocumentFileLocation], [tg.InputPeerPhotoFileLocation],
|
||||||
// [tg.InputFileLocation], or [tg.InputPhotoFileLocation] into a [LocationID]
|
// [tg.InputFileLocation], or [tg.InputPhotoFileLocation] into a [LocationID]
|
||||||
@@ -56,72 +98,234 @@ func (c AnimatedStickerConfig) WebmConvert() bool {
|
|||||||
return c.ConvertFromWebm && c.Target != "webm"
|
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 {
|
type Transferer struct {
|
||||||
RoomID id.RoomID
|
client downloader.Client
|
||||||
Filename string
|
|
||||||
IsSticker bool
|
roomID id.RoomID
|
||||||
Config AnimatedStickerConfig
|
filename string
|
||||||
|
animatedStickerConfig *AnimatedStickerConfig
|
||||||
|
|
||||||
|
fileInfo event.FileInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTransferer(cfg AnimatedStickerConfig) *Transferer {
|
type ReadyTransferer struct {
|
||||||
return &Transferer{Config: cfg}
|
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 {
|
func (t *Transferer) WithRoomID(roomID id.RoomID) *Transferer {
|
||||||
t.RoomID = roomID
|
t.roomID = roomID
|
||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithFilename sets the filename for the [Transferer].
|
||||||
func (t *Transferer) WithFilename(filename string) *Transferer {
|
func (t *Transferer) WithFilename(filename string) *Transferer {
|
||||||
t.Filename = filename
|
t.filename = filename
|
||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transferer) WithIsSticker(isSticker bool) *Transferer {
|
func (t *Transferer) WithMIMEType(mimeType string) *Transferer {
|
||||||
t.IsSticker = isSticker
|
t.fileInfo.MimeType = mimeType
|
||||||
return t
|
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) {
|
// WithStickerConfig sets the animated sticker config for the [Transferer].
|
||||||
locationID := getLocationID(loc)
|
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 {
|
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 {
|
} 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, _, err := t.Download(ctx)
|
||||||
data, mimeType, err = DownloadFileLocation(ctx, client, loc)
|
|
||||||
if err != nil {
|
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 t.inner.animatedStickerConfig != nil {
|
||||||
if lottie.Supported() && t.Config.TGSConvert() && mimeType == "application/x-gzip" {
|
if lottie.Supported() && t.inner.animatedStickerConfig.TGSConvert() && t.inner.fileInfo.MimeType == "application/x-tgsticker" {
|
||||||
data, err = lottie.ConvertBytes(ctx, data, t.Config.Target, t.Config.Args.Width, t.Config.Args.Height, fmt.Sprintf("%d", t.Config.Args.FPS))
|
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 {
|
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
|
// TODO support ffmpeg conversion
|
||||||
// } else if ffmpeg.Supported() && t.Config.WebmConvert() && mimeType == "video/webm" {
|
// } 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 {
|
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 := store.TelegramFile.New()
|
||||||
file.LocationID = locationID
|
file.LocationID = locationID
|
||||||
file.MXC = mxcURI
|
file.MXC = mxc
|
||||||
file.Size = len(data)
|
file.Size = t.inner.fileInfo.Size
|
||||||
file.MIMEType = mimeType
|
file.MIMEType = t.inner.fileInfo.MimeType
|
||||||
// TODO width, height, thumbnail?
|
|
||||||
if err = file.Insert(ctx); err != nil {
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ func (mc *MessageConverter) ToMatrix(ctx context.Context, portal *bridgev2.Porta
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO formatting
|
// TODO formatting
|
||||||
|
// TODO combine with other media
|
||||||
cm.Parts = append(cm.Parts, &bridgev2.ConvertedMessagePart{
|
cm.Parts = append(cm.Parts, &bridgev2.ConvertedMessagePart{
|
||||||
ID: networkid.PartID("caption"),
|
ID: networkid.PartID("caption"),
|
||||||
Type: event.EventMessage,
|
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 {
|
if pc, ok := webpage.GetPhoto(); ok && pc.TypeID() == tg.PhotoTypeID {
|
||||||
var data []byte
|
var fileInfo *event.FileInfo
|
||||||
data, preview.ImageWidth, preview.ImageHeight, preview.ImageType, err = media.DownloadPhoto(ctx, mc.client.API(), pc.(*tg.Photo))
|
preview.ImageURL, preview.ImageEncryption, fileInfo, err = media.NewTransferer(mc.client.API()).
|
||||||
if err != nil {
|
WithPhoto(pc).
|
||||||
return nil, err
|
Transfer(ctx, mc.store, intent)
|
||||||
}
|
|
||||||
preview.ImageSize = len(data)
|
|
||||||
preview.ImageURL, preview.ImageEncryption, err = intent.UploadMedia(ctx, "", data, "", preview.ImageType)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
preview.ImageSize, preview.ImageWidth, preview.ImageHeight = fileInfo.Size, fileInfo.Width, fileInfo.Height
|
||||||
}
|
}
|
||||||
|
|
||||||
return preview, nil
|
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) {
|
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 partID networkid.PartID
|
||||||
var msgType event.MessageType
|
var content event.MessageEventContent
|
||||||
var filename string
|
|
||||||
var audio *event.MSC1767Audio
|
transferer := media.NewTransferer(mc.client.API()).WithRoomID(portal.MXID)
|
||||||
var voice *event.MSC3245Voice
|
var mediaTransferer *media.ReadyTransferer
|
||||||
var info event.FileInfo
|
|
||||||
|
|
||||||
// Determine the filename and some other information
|
// Determine the filename and some other information
|
||||||
switch msgMedia := msgMedia.(type) {
|
switch msgMedia := msgMedia.(type) {
|
||||||
case *tg.MessageMediaPhoto:
|
case *tg.MessageMediaPhoto:
|
||||||
partID = networkid.PartID("photo")
|
partID = networkid.PartID("photo")
|
||||||
msgType = event.MsgImage
|
content.MsgType = event.MsgImage
|
||||||
filename = "image"
|
content.Body = "image"
|
||||||
if photo, ok := msgMedia.Photo.(*tg.Photo); ok {
|
mediaTransferer = transferer.WithPhoto(msgMedia.Photo)
|
||||||
info.Width, info.Height, _ = media.GetLargestPhotoSize(photo.GetSizes())
|
|
||||||
}
|
|
||||||
case *tg.MessageMediaDocument:
|
case *tg.MessageMediaDocument:
|
||||||
partID = networkid.PartID("document")
|
|
||||||
msgType = event.MsgFile
|
|
||||||
document, ok := msgMedia.Document.(*tg.Document)
|
document, ok := msgMedia.Document.(*tg.Document)
|
||||||
info.Size = int(document.Size)
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, nil, fmt.Errorf("unrecognized document type %T", msgMedia.Document)
|
return nil, nil, fmt.Errorf("unrecognized document type %T", msgMedia.Document)
|
||||||
}
|
}
|
||||||
|
|
||||||
if thumbSizes, ok := document.GetThumbs(); ok {
|
partID = networkid.PartID("document")
|
||||||
info.ThumbnailInfo = &event.FileInfo{}
|
content.MsgType = event.MsgFile
|
||||||
var largestThumbnail tg.PhotoSizeClass
|
|
||||||
info.ThumbnailInfo.Width, info.ThumbnailInfo.Height, largestThumbnail = media.GetLargestPhotoSize(thumbSizes)
|
|
||||||
|
|
||||||
|
if _, ok := document.GetThumbs(); ok {
|
||||||
|
var thumbnailURL id.ContentURIString
|
||||||
|
var thumbnailFile *event.EncryptedFileInfo
|
||||||
|
var thumbnailInfo *event.FileInfo
|
||||||
var err error
|
var err error
|
||||||
info.ThumbnailInfo.ThumbnailURL, err = mc.directMedia(ctx, portal, msgID, true)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if info.ThumbnailInfo.ThumbnailURL == "" {
|
thumbnailTransferer := media.NewTransferer(mc.client.API()).
|
||||||
info.ThumbnailInfo.ThumbnailURL, info.ThumbnailInfo.ThumbnailFile, info.ThumbnailInfo.Size, info.ThumbnailInfo.MimeType, err = media.NewTransferer(mc.animatedStickerConfig).
|
WithRoomID(portal.MXID).
|
||||||
WithRoomID(portal.MXID).
|
WithDocument(document, true)
|
||||||
Transfer(ctx, mc.store, mc.client.API(), intent, &tg.InputDocumentFileLocation{
|
if mc.useDirectMedia {
|
||||||
ID: document.GetID(),
|
thumbnailURL, thumbnailInfo, err = thumbnailTransferer.DirectDownloadURL(ctx, portal, msgID, true)
|
||||||
AccessHash: document.GetAccessHash(),
|
|
||||||
FileReference: document.GetFileReference(),
|
|
||||||
ThumbSize: largestThumbnail.GetType(),
|
|
||||||
})
|
|
||||||
if err != nil {
|
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() {
|
for _, attr := range document.GetAttributes() {
|
||||||
switch a := attr.(type) {
|
switch a := attr.(type) {
|
||||||
case *tg.DocumentAttributeFilename:
|
case *tg.DocumentAttributeFilename:
|
||||||
filename = a.GetFileName()
|
content.Body = a.GetFileName()
|
||||||
case *tg.DocumentAttributeVideo:
|
case *tg.DocumentAttributeVideo:
|
||||||
msgType = event.MsgVideo
|
content.MsgType = event.MsgVideo
|
||||||
info.Width, info.Height = a.W, a.H
|
transferer = transferer.WithVideo(a)
|
||||||
info.Duration = int(a.Duration * 1000)
|
|
||||||
case *tg.DocumentAttributeAudio:
|
case *tg.DocumentAttributeAudio:
|
||||||
msgType = event.MsgAudio
|
content.MsgType = event.MsgAudio
|
||||||
audio = &event.MSC1767Audio{
|
content.MSC1767Audio = &event.MSC1767Audio{
|
||||||
Duration: a.Duration * 1000,
|
Duration: a.Duration * 1000,
|
||||||
}
|
}
|
||||||
if wf, ok := a.GetWaveform(); ok {
|
if wf, ok := a.GetWaveform(); ok {
|
||||||
for _, v := range waveform.Decode(wf) {
|
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 {
|
if a.Voice {
|
||||||
voice = &event.MSC3245Voice{}
|
content.MSC3245Voice = &event.MSC3245Voice{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mediaTransferer = transferer.
|
||||||
|
WithFilename(content.Body).
|
||||||
|
WithDocument(msgMedia.Document, false)
|
||||||
default:
|
default:
|
||||||
return nil, nil, fmt.Errorf("unhandled media type %T", msgMedia)
|
return nil, nil, fmt.Errorf("unhandled media type %T", msgMedia)
|
||||||
}
|
}
|
||||||
|
|
||||||
var encryptedFileInfo *event.EncryptedFileInfo
|
var err error
|
||||||
|
if mc.useDirectMedia {
|
||||||
mxcURI, err := mc.directMedia(ctx, portal, msgID, false)
|
content.URL, content.Info, err = mediaTransferer.DirectDownloadURL(ctx, portal, msgID, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
log.Err(err).Msg("error getting direct download URL for media")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if content.URL == "" {
|
||||||
if mxcURI == "" {
|
content.URL, content.File, content.Info, err = mediaTransferer.Transfer(ctx, mc.store, intent)
|
||||||
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 err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, fmt.Errorf("error transferring media: %w", err)
|
||||||
}
|
}
|
||||||
|
if msgMedia.TypeID() == tg.MessageMediaPhotoTypeID {
|
||||||
mxcURI, encryptedFileInfo, err = intent.UploadMedia(ctx, portal.MXID, data, filename, info.MimeType)
|
content.Body = content.Body + exmime.ExtensionFromMimetype(content.Info.MimeType)
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,6 +263,9 @@ func (mc *MessageConverter) convertMediaRequiringUpload(ctx context.Context, por
|
|||||||
var disappearingSetting *database.DisappearingSetting
|
var disappearingSetting *database.DisappearingSetting
|
||||||
if t, ok := msgMedia.(ttlable); ok {
|
if t, ok := msgMedia.(ttlable); ok {
|
||||||
if ttl, ok := t.GetTTLSeconds(); ok {
|
if ttl, ok := t.GetTTLSeconds(); ok {
|
||||||
|
if msgMedia.TypeID() == tg.MessageMediaPhotoTypeID {
|
||||||
|
content.Body = "disappearing_" + content.Body
|
||||||
|
}
|
||||||
disappearingSetting = &database.DisappearingSetting{
|
disappearingSetting = &database.DisappearingSetting{
|
||||||
Type: database.DisappearingTypeAfterSend,
|
Type: database.DisappearingTypeAfterSend,
|
||||||
Timer: time.Duration(ttl) * time.Second,
|
Timer: time.Duration(ttl) * time.Second,
|
||||||
@@ -312,18 +274,10 @@ func (mc *MessageConverter) convertMediaRequiringUpload(ctx context.Context, por
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &bridgev2.ConvertedMessagePart{
|
return &bridgev2.ConvertedMessagePart{
|
||||||
ID: partID,
|
ID: partID,
|
||||||
Type: event.EventMessage,
|
Type: event.EventMessage,
|
||||||
Content: &event.MessageEventContent{
|
Content: &content,
|
||||||
MsgType: msgType,
|
Extra: extra,
|
||||||
Body: filename,
|
|
||||||
URL: mxcURI,
|
|
||||||
File: encryptedFileInfo,
|
|
||||||
Info: &info,
|
|
||||||
MSC1767Audio: audio,
|
|
||||||
MSC3245Voice: voice,
|
|
||||||
},
|
|
||||||
Extra: extra,
|
|
||||||
}, disappearingSetting, nil
|
}, disappearingSetting, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -369,18 +369,14 @@ func (t *TelegramClient) transferEmojisToMatrix(ctx context.Context, customEmoji
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, customEmojiDocument := range customEmojiDocuments {
|
for _, customEmojiDocument := range customEmojiDocuments {
|
||||||
document := customEmojiDocument.(*tg.Document)
|
mxcURI, _, _, err := media.NewTransferer(t.client.API()).
|
||||||
mxcURI, _, _, _, err := media.NewTransferer(t.main.Config.AnimatedSticker).
|
WithStickerConfig(t.main.Config.AnimatedSticker).
|
||||||
WithIsSticker(true).
|
WithDocument(customEmojiDocument, false).
|
||||||
Transfer(ctx, t.main.Store, t.client.API(), t.main.Bridge.Bot, &tg.InputDocumentFileLocation{
|
Transfer(ctx, t.main.Store, t.main.Bridge.Bot)
|
||||||
ID: document.GetID(),
|
|
||||||
AccessHash: document.GetAccessHash(),
|
|
||||||
FileReference: document.GetFileReference(),
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
result[ids.MakeEmojiIDFromDocumentID(document.ID)] = string(mxcURI)
|
result[ids.MakeEmojiIDFromDocumentID(customEmojiDocument.GetID())] = string(mxcURI)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user