imagepack: add support for direct media

This commit is contained in:
Tulir Asokan
2026-05-06 12:34:05 +03:00
parent 9d1fb4609b
commit ca650734ae
5 changed files with 125 additions and 12 deletions

View File

@@ -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)

View File

@@ -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))
}

View File

@@ -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)

View File

@@ -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()

View File

@@ -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
}