From ca650734aee98a6acba69f6a85cf35c3f8792a98 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 6 May 2026 12:34:05 +0300 Subject: [PATCH] imagepack: add support for direct media --- pkg/connector/directdownload.go | 18 ++++++++- pkg/connector/ids/ids.go | 7 +++- pkg/connector/imagepack.go | 13 +++++-- pkg/connector/media/sticker.go | 65 ++++++++++++++++++++++++++++++++- pkg/connector/media/transfer.go | 34 ++++++++++++++--- 5 files changed, 125 insertions(+), 12 deletions(-) diff --git a/pkg/connector/directdownload.go b/pkg/connector/directdownload.go index 9fc070d1..acc3aa73 100644 --- a/pkg/connector/directdownload.go +++ b/pkg/connector/directdownload.go @@ -133,7 +133,23 @@ func (tc *TelegramConnector) Download(ctx context.Context, mediaID networkid.Med transferer := media.NewTransferer(client.client.API()) var readyTransferer *media.ReadyTransferer - if info.MessageID > 0 { + if info.PeerType == ids.FakePeerTypeSticker { + pack, err := client.GetCachedStickerPack(ctx, "", &tg.InputStickerSetID{ + ID: info.PeerID, + AccessHash: info.MessageID, // sticker pack direct media abuses the user ID field for access hashes + }, false) + if err != nil { + return nil, fmt.Errorf("failed to get sticker pack: %w", err) + } + doc, ok := pack.docs[info.ID] + if !ok { + return nil, fmt.Errorf("sticker %d not found in pack %s", info.ID, pack.meta.ShortName) + } + readyTransferer = transferer. + WithStickerConfig(tc.Config.AnimatedSticker). + WithForceWebmStickerConvert(pack.meta.Emojis). + WithDocument(doc, info.Thumbnail) + } else if info.MessageID > 0 { rawMsgMedia, err := client.refetchMedia(ctx, info.PeerType, info.PeerID, int(info.MessageID)) if err != nil { return nil, fmt.Errorf("failed to refetch media message: %w", err) diff --git a/pkg/connector/ids/ids.go b/pkg/connector/ids/ids.go index 8cfbc36e..0701b61f 100644 --- a/pkg/connector/ids/ids.go +++ b/pkg/connector/ids/ids.go @@ -135,7 +135,8 @@ const ( PeerTypeChat PeerType = "chat" PeerTypeChannel PeerType = "channel" - FakePeerTypeEmoji PeerType = "emoji" + FakePeerTypeEmoji PeerType = "emoji" + FakePeerTypeSticker PeerType = "sticker" ) func PeerTypeFromByte(pt byte) (PeerType, error) { @@ -148,6 +149,8 @@ func PeerTypeFromByte(pt byte) (PeerType, error) { return PeerTypeChannel, nil case 0x04: return FakePeerTypeEmoji, nil + case 0x05: + return FakePeerTypeSticker, nil default: return "", fmt.Errorf("unknown peer type %d", pt) } @@ -163,6 +166,8 @@ func (pt PeerType) AsByte() byte { return 0x03 case FakePeerTypeEmoji: return 0x04 + case FakePeerTypeSticker: + return 0x05 default: panic(fmt.Errorf("unknown peer type %s", pt)) } diff --git a/pkg/connector/imagepack.go b/pkg/connector/imagepack.go index 99686835..662f9cc1 100644 --- a/pkg/connector/imagepack.go +++ b/pkg/connector/imagepack.go @@ -551,12 +551,17 @@ func (tc *TelegramClient) DownloadImagePack(ctx context.Context, url string) (*b } } for i, rawDoc := range set.Documents { - // TODO use direct media - mxc, _, info, err := media.NewTransferer(tc.client.API()). + var mxc id.ContentURIString + var info *event.FileInfo + xfer := media.NewTransferer(tc.client.API()). WithStickerConfig(tc.main.Config.AnimatedSticker). WithForceWebmStickerConvert(set.Set.Emojis). - WithDocument(rawDoc, false). - Transfer(ctx, tc.main.Store, tc.main.Bridge.Bot) + WithDocument(rawDoc, false) + if tc.main.useDirectMedia { + mxc, info, err = xfer.StickerDirectDownloadURL(ctx, tc.main.Bridge, set.Set, tc.telegramUserID) + } else { + mxc, _, info, err = xfer.Transfer(ctx, tc.main.Store, tc.main.Bridge.Bot) + } if err != nil { zerolog.Ctx(ctx).Err(err).Msg("Failed to transfer image in pack") return nil, fmt.Errorf("failed to transfer document %d: %w", rawDoc.GetID(), err) diff --git a/pkg/connector/media/sticker.go b/pkg/connector/media/sticker.go index e5f4c521..b679f972 100644 --- a/pkg/connector/media/sticker.go +++ b/pkg/connector/media/sticker.go @@ -39,6 +39,69 @@ type AnimatedStickerConfig struct { } `yaml:"args"` } +func (c *AnimatedStickerConfig) Supported(inputMime string) bool { + if inputMime == "application/x-tgsticker" { + switch c.Target { + case "disable": + return true + case "gif", "png": + return lottie.Supported() + case "webm", "webp": + return lottie.Supported() && ffmpeg.Supported() + } + } else if inputMime == "video/webm" { + if !c.ConvertFromWebm { + return true + } + switch c.Target { + case "disable", "webm": + return true + case "webp", "png", "gif": + return ffmpeg.Supported() + } + } + return false +} + +func (c *AnimatedStickerConfig) TargetMime(inputMime string) string { + if c == nil || !c.Supported(inputMime) { + return "" + } + switch inputMime { + case "application/x-tgsticker": + switch c.Target { + case "png": + return "image/png" + case "gif": + return "image/gif" + case "webm": + return "video/webm" + case "webp": + return "image/webp" + case "disable": + return "video/lottie+json" + default: + return "" + } + case "video/webm": + if !c.ConvertFromWebm { + return "" + } + switch c.Target { + case "png": + return "image/png" + case "gif": + return "image/gif" + case "webp": + return "image/webp" + default: + return "" + } + default: + return "" + } +} + type ConvertedSticker struct { Success bool NewPath string @@ -51,7 +114,7 @@ type ConvertedSticker struct { } func (c *AnimatedStickerConfig) convertWebm(ctx context.Context, src *os.File) *ConvertedSticker { - if !c.ConvertFromWebm || c.Target == "webm" { + if !c.ConvertFromWebm || c.Target == "webm" || c.Target == "disable" { return nil } log := zerolog.Ctx(ctx).With().Str("animated_sticker_target", c.Target).Logger() diff --git a/pkg/connector/media/transfer.go b/pkg/connector/media/transfer.go index d56ab544..44faf317 100644 --- a/pkg/connector/media/transfer.go +++ b/pkg/connector/media/transfer.go @@ -331,8 +331,7 @@ func (t *ReadyTransferer) Transfer(ctx context.Context, db *store.Container, int return "", nil, nil, fmt.Errorf("downloading file failed: %w", err) } - needStickerConvert := t.inner.animatedStickerConfig != nil && (t.inner.fileInfo.MimeType == "application/x-tgsticker" || - (t.inner.fileInfo.MimeType == "video/webm" && t.inner.animatedStickerConfig.ConvertFromWebm && t.inner.animatedStickerConfig.Target != "webm")) + needStickerConvert := t.expectedStickerConvertMime() != "" needsDimensions := strings.HasPrefix(t.inner.fileInfo.MimeType, "image/") && t.inner.fileInfo.Width == 0 && t.inner.fileInfo.Height == 0 @@ -457,6 +456,10 @@ func (t *ReadyTransferer) Stream(ctx context.Context) (r io.Reader, mimeType str return r, t.inner.fileInfo.MimeType, t.inner.fileInfo.Size, nil } +func (t *ReadyTransferer) expectedStickerConvertMime() string { + return t.inner.animatedStickerConfig.TargetMime(t.inner.fileInfo.MimeType) +} + func (t *ReadyTransferer) ToDirectMediaResponse(ctx context.Context) (mediaproxy.GetMediaResponse, error) { if t == nil { return nil, fmt.Errorf("invalid direct media request") @@ -472,7 +475,7 @@ func (t *ReadyTransferer) ToDirectMediaResponse(ctx context.Context) (mediaproxy Int("size", size). Msg("Started downloading media successfully") - if t.inner.animatedStickerConfig != nil { + if t.expectedStickerConvertMime() != "" { return &mediaproxy.GetMediaResponseFile{ Callback: func(w *os.File) (*mediaproxy.FileMeta, error) { _, err = io.Copy(w, r) @@ -515,6 +518,27 @@ func (t *ReadyTransferer) DownloadBytes(ctx context.Context) ([]byte, error) { return buf.Bytes(), err } +func (t *ReadyTransferer) StickerDirectDownloadURL(ctx context.Context, br *bridgev2.Bridge, set tg.StickerSet, loggedInUserID int64) (id.ContentURIString, *event.FileInfo, error) { + mediaID, err := ids.DirectMediaInfo{ + PeerType: ids.FakePeerTypeSticker, + PeerID: set.ID, + UserID: loggedInUserID, + MessageID: set.AccessHash, // sticker pack direct media abuses the user ID field for access hashes + ID: t.loc.(*tg.InputDocumentFileLocation).ID, + }.AsMediaID() + if err != nil { + return "", nil, err + } + mxc, err := br.Matrix.GenerateContentURI(ctx, mediaID) + if t.inner.fileInfo.MimeType == "" { + t.inner.fileInfo.MimeType = "application/octet-stream" + } + if convertMime := t.expectedStickerConvertMime(); convertMime != "" { + t.inner.fileInfo.MimeType = convertMime + } + return mxc, &t.inner.fileInfo, err +} + // DirectDownloadURL returns the direct download URL for the media. func (t *ReadyTransferer) DirectDownloadURL(ctx context.Context, loggedInUserID int64, portal *bridgev2.Portal, msgID int, thumbnail bool, telegramMediaID int64) (id.ContentURIString, *event.FileInfo, error) { peerType, chatID, _, err := ids.ParsePortalID(portal.ID) @@ -536,8 +560,8 @@ func (t *ReadyTransferer) DirectDownloadURL(ctx context.Context, loggedInUserID if t.inner.fileInfo.MimeType == "" { t.inner.fileInfo.MimeType = "application/octet-stream" } - if t.inner.fileInfo.MimeType == "application/x-tgsticker" { - t.inner.fileInfo.MimeType = "video/lottie+json" + if convertMime := t.expectedStickerConvertMime(); convertMime != "" { + t.inner.fileInfo.MimeType = convertMime } return mxc, &t.inner.fileInfo, err }