imagepack: switch to new shared metadata field

This commit is contained in:
Tulir Asokan
2026-04-27 20:23:06 +03:00
parent 9e1c42a992
commit e6243d8935
7 changed files with 132 additions and 107 deletions

4
go.mod
View File

@@ -27,7 +27,7 @@ require (
github.com/rs/zerolog v1.35.0
github.com/stretchr/testify v1.11.1
github.com/tidwall/gjson v1.18.0
go.mau.fi/util v0.9.8
go.mau.fi/util v0.9.9-0.20260424160448-fd0d9737ad38
go.mau.fi/webp v0.2.0
go.mau.fi/zerozap v0.1.2
go.opentelemetry.io/otel v1.42.0
@@ -42,7 +42,7 @@ require (
golang.org/x/sync v0.20.0
golang.org/x/tools v0.44.0
gopkg.in/yaml.v3 v3.0.1
maunium.net/go/mautrix v0.27.1-0.20260422171355-c6fe96e2dea3
maunium.net/go/mautrix v0.27.1-0.20260427165325-7724eaafca8b
rsc.io/qr v0.2.0
)

8
go.sum
View File

@@ -112,8 +112,8 @@ github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.mau.fi/util v0.9.8 h1:+/jf8eM2dAT2wx9UidmaneH28r/CSCKCniCyby1qWz8=
go.mau.fi/util v0.9.8/go.mod h1:up/5mbzH2M1pSBNXqRxODn8dg/hEKbLJu92W4/SNAX0=
go.mau.fi/util v0.9.9-0.20260424160448-fd0d9737ad38 h1:D4OKITjyvlud39Q10oMnfhdeNkzEIVkXrEeCW6nvgLk=
go.mau.fi/util v0.9.9-0.20260424160448-fd0d9737ad38/go.mod h1:up/5mbzH2M1pSBNXqRxODn8dg/hEKbLJu92W4/SNAX0=
go.mau.fi/webp v0.2.0 h1:QVMenHw7JDb4vall5sV75JNBQj9Hw4u8AKbi1QetHvg=
go.mau.fi/webp v0.2.0/go.mod h1:VSg9MyODn12Mb5pyG0NIyNFhujrmoFSsZBs8syOZD1Q=
go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU=
@@ -236,7 +236,7 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
maunium.net/go/mautrix v0.27.1-0.20260422171355-c6fe96e2dea3 h1:V5L7Yo0fH1fs6lybfR+BUWG1D25xIdUZNWBIPXCV8cY=
maunium.net/go/mautrix v0.27.1-0.20260422171355-c6fe96e2dea3/go.mod h1:7QpEQiTy6p4LHkXXaZI+N46tGYy8HMhD0JjzZAFoFWs=
maunium.net/go/mautrix v0.27.1-0.20260427165325-7724eaafca8b h1:Wfkd74wYRUTb36gqvXSvY5IWW8R2sIxNGjWPM0x60+U=
maunium.net/go/mautrix v0.27.1-0.20260427165325-7724eaafca8b/go.mod h1:4fZ0M0xB5ZtueQI65RilX28J/3794BeK+LaCg4U61Jk=
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=

View File

@@ -119,7 +119,8 @@ type TelegramClient struct {
prevReactionPoll map[networkid.PortalKey]time.Time
prevReactionPollLock sync.Mutex
stickerPackCache map[string]map[int64]*tg.Document
stickerPacksByName map[string]*stickerPackCache
stickerPacksByID map[int64]*stickerPackCache
stickerPackCacheLock sync.Mutex
}
@@ -175,8 +176,9 @@ func NewTelegramClient(ctx context.Context, tc *TelegramConnector, login *bridge
takeoutAccepted: exsync.NewEvent(),
prevReactionPoll: map[networkid.PortalKey]time.Time{},
stickerPackCache: map[string]map[int64]*tg.Document{},
prevReactionPoll: map[networkid.PortalKey]time.Time{},
stickerPacksByName: map[string]*stickerPackCache{},
stickerPacksByID: map[int64]*stickerPackCache{},
recentMessageRooms: exsync.NewRingBuffer[networkid.MessageID, networkid.PortalKey](32),

View File

@@ -243,85 +243,12 @@ func (tc *TelegramClient) pollSponsoredMessage(ctx context.Context, portal *brid
return nil
}
func (tc *TelegramClient) parseInputPack(meta map[string]any) (shortName string, id, accessHash int64) {
shortName, _ = meta["short_name"].(string)
idStr, _ := meta["id"].(string)
if idStr != "" {
id, _ = strconv.ParseInt(idStr, 10, 64)
}
accessHashStr, _ := meta["access_hash"].(string)
accessHashSourceStr, _ := meta["access_hash_source"].(string)
if id != 0 && accessHashStr != "" && accessHashSourceStr != "" {
accessHashSource, _ := strconv.ParseInt(accessHashSourceStr, 10, 64)
if accessHashSource == tc.telegramUserID {
accessHash, _ = strconv.ParseInt(accessHashStr, 10, 64)
}
}
return
}
func (tc *TelegramClient) findOriginalStickerDocument(ctx context.Context, info map[string]any, forceClearCache bool) (tg.InputMediaClass, error) {
stickerIDStr, ok := info["id"].(string)
if !ok {
return nil, nil
}
stickerID, err := strconv.ParseInt(stickerIDStr, 10, 64)
if err != nil {
return nil, nil
}
pack, ok := info["pack"].(map[string]any)
if !ok {
return nil, nil
}
var inputPack tg.InputStickerSetClass
var cacheKey string
packName, packID, packAccessHash := tc.parseInputPack(pack)
if packAccessHash != 0 {
inputPack = &tg.InputStickerSetID{ID: packID, AccessHash: packAccessHash}
cacheKey = fmt.Sprintf("pack_id:%d", packID)
} else if packName != "" {
inputPack = &tg.InputStickerSetShortName{ShortName: packName}
cacheKey = fmt.Sprintf("pack_name:%s", packName)
} else {
return nil, nil
}
tc.stickerPackCacheLock.Lock()
defer tc.stickerPackCacheLock.Unlock()
docMap, ok := tc.stickerPackCache[cacheKey]
if !ok || forceClearCache {
resp, err := tc.client.API().MessagesGetStickerSet(ctx, &tg.MessagesGetStickerSetRequest{Stickerset: inputPack})
if err != nil {
if tgerr.Is(err, tg.ErrStickersetInvalid) {
tc.stickerPackCache[cacheKey] = nil
}
return nil, fmt.Errorf("failed to get sticker set: %w", err)
}
set, ok := resp.AsModified()
if !ok {
tc.stickerPackCache[cacheKey] = nil
return nil, fmt.Errorf("unexpected response type for MessagesGetStickerSet: %T", resp)
}
docMap = set.MapDocuments().DocumentToMap()
tc.stickerPackCache[cacheKey] = docMap
tc.stickerPackCache[fmt.Sprintf("pack_id:%d", set.Set.ID)] = docMap
tc.stickerPackCache[fmt.Sprintf("pack_name:%s", set.Set.ShortName)] = docMap
}
stickerDoc, ok := docMap[stickerID]
if !ok {
return nil, nil
}
return &tg.InputMediaDocument{ID: stickerDoc.AsInput()}, nil
}
func (tc *TelegramClient) transferMediaToTelegram(ctx context.Context, content *event.MessageEventContent, sticker, forceRetry, forceDocument bool) (tg.InputMediaClass, error) {
var upload tg.InputFileClass
filename := getMediaFilename(content)
info := content.GetInfo()
if sticker {
extra, ok := info.Extra["fi.mau.telegram.sticker"].(map[string]any)
if !ok {
// Not from telegram, continue to reupload
} else if origFile, err := tc.findOriginalStickerDocument(ctx, extra, forceRetry); err != nil {
if origFile, err := tc.findOriginalStickerDocument(ctx, info.BridgedSticker, forceRetry); err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to find original sticker document, falling back to reupload")
} else if origFile != nil {
return origFile, nil

View File

@@ -31,6 +31,7 @@ import (
"strings"
"time"
"github.com/rs/zerolog"
"go.mau.fi/util/exmaps"
"go.mau.fi/util/ffmpeg"
"go.mau.fi/util/variationselector"
@@ -473,13 +474,18 @@ func (tc *TelegramClient) fnDownloadEmojiPack(ce *commands.Event) {
linkType = "addemoji"
usage = event.ImagePackUsageEmoji
}
packURL := fmt.Sprintf("https://t.me/%s/%s", linkType, set.Set.ShortName)
pack := &event.ImagePackEventContent{
Images: make(map[string]*event.ImagePackImage, len(set.Documents)),
Metadata: event.ImagePackMetadata{
DisplayName: set.Set.Title,
AvatarURL: "",
Usage: []event.ImagePackUsage{usage},
Attribution: fmt.Sprintf("Imported from https://t.me/%s/%s", linkType, set.Set.ShortName),
Attribution: fmt.Sprintf("Imported from %s", packURL),
BridgedPack: &event.BridgedStickerPack{
Network: StickerSourceID,
URL: packURL,
},
},
}
topLevelExtra := map[string]any{
@@ -534,14 +540,10 @@ func (tc *TelegramClient) fnDownloadEmojiPack(ce *commands.Event) {
if !set.Set.Emojis {
// Stickers need extra info in each sticker so they can be accurately bridged back to Telegram
// Custom emojis don't have space for such info and can be used with just the document ID
info.Extra = map[string]any{
"fi.mau.telegram.sticker": map[string]any{
"id": strconv.FormatInt(rawDoc.GetID(), 10),
"pack": map[string]any{
"short_name": set.Set.ShortName,
"id": strconv.FormatInt(set.Set.ID, 10),
},
},
info.BridgedSticker = &event.BridgedSticker{
Network: StickerSourceID,
ID: strconv.FormatInt(rawDoc.GetID(), 10),
PackURL: StickerPackURLPrefix + set.Set.ShortName,
}
}
pack.Images[key] = &event.ImagePackImage{
@@ -563,3 +565,104 @@ func (tc *TelegramClient) fnDownloadEmojiPack(ce *commands.Event) {
spaceRoom.URI(tc.main.Bridge.Matrix.ServerName()).MatrixToURL()))
}
}
const StickerSourceID = "telegram"
const StickerPackURLPrefix = "https://t.me/addstickers/"
func (tc *TelegramClient) stickerSourceFromAttribute(ctx context.Context, documentID int64, attr *tg.DocumentAttributeSticker) *event.BridgedSticker {
var shortName string
switch set := attr.Stickerset.(type) {
case *tg.InputStickerSetID:
pack, err := tc.GetCachedStickerPack(ctx, "", set, false)
if err != nil {
zerolog.Ctx(ctx).Debug().Err(err).
Int64("pack_id", set.ID).
Msg("Failed to get sticker pack by ID to fill info")
return nil
}
shortName = pack.meta.ShortName
case *tg.InputStickerSetShortName:
shortName = set.ShortName
default:
return nil
}
return &event.BridgedSticker{
Network: StickerSourceID,
ID: strconv.FormatInt(documentID, 10),
Emoji: attr.Alt,
PackURL: StickerPackURLPrefix + shortName,
}
}
type stickerPackCache struct {
docs map[int64]*tg.Document
meta tg.StickerSet
}
func (tc *TelegramClient) GetCachedStickerPack(ctx context.Context, shortName string, id *tg.InputStickerSetID, forceClearCache bool) (*stickerPackCache, error) {
tc.stickerPackCacheLock.Lock()
defer tc.stickerPackCacheLock.Unlock()
cacheName := strings.ToLower(shortName)
cache, ok := tc.stickerPacksByName[cacheName]
if !ok {
cache, ok = tc.stickerPacksByID[id.GetID()]
}
if !ok || forceClearCache {
var inputSet tg.InputStickerSetClass = id
if id == nil {
inputSet = &tg.InputStickerSetShortName{ShortName: shortName}
}
resp, err := tc.client.API().MessagesGetStickerSet(ctx, &tg.MessagesGetStickerSetRequest{Stickerset: inputSet})
if err != nil {
if tgerr.Is(err, tg.ErrStickersetInvalid) {
if cacheName != "" {
tc.stickerPacksByName[cacheName] = nil
}
if id != nil {
tc.stickerPacksByID[id.GetID()] = nil
}
}
return nil, fmt.Errorf("failed to get sticker set: %w", err)
}
set, ok := resp.AsModified()
if !ok {
if cacheName != "" {
tc.stickerPacksByName[cacheName] = nil
}
if id != nil {
tc.stickerPacksByID[id.GetID()] = nil
}
return nil, fmt.Errorf("unexpected response type for MessagesGetStickerSet: %T", resp)
}
cache = &stickerPackCache{
docs: set.MapDocuments().DocumentToMap(),
meta: set.Set,
}
tc.stickerPacksByName[strings.ToLower(set.Set.ShortName)] = cache
tc.stickerPacksByID[set.Set.ID] = cache
}
return cache, nil
}
func (tc *TelegramClient) findOriginalStickerDocument(ctx context.Context, meta *event.BridgedSticker, forceClearCache bool) (tg.InputMediaClass, error) {
if meta == nil || !strings.HasPrefix(meta.PackURL, StickerPackURLPrefix) {
return nil, nil
}
shortName := strings.TrimPrefix(meta.PackURL, StickerPackURLPrefix)
if shortName == "" {
return nil, nil
}
idNum, err := strconv.ParseInt(meta.ID, 10, 64)
if err != nil {
return nil, nil
}
cache, err := tc.GetCachedStickerPack(ctx, shortName, nil, forceClearCache)
if err != nil {
return nil, err
}
stickerDoc, ok := cache.docs[idNum]
if !ok {
return nil, nil
}
return &tg.InputMediaDocument{ID: stickerDoc.AsInput()}, nil
}

View File

@@ -144,6 +144,11 @@ func (t *Transferer) WithStickerConfig(cfg AnimatedStickerConfig) *Transferer {
return t
}
func (t *Transferer) WithStickerMetadata(meta *event.BridgedSticker) *Transferer {
t.fileInfo.BridgedSticker = meta
return t
}
func (t *Transferer) WithForceWebmStickerConvert(force bool) *Transferer {
if force {
t.animatedStickerConfig.ConvertFromWebm = true

View File

@@ -618,21 +618,9 @@ func (tc *TelegramClient) convertMediaRequiringUpload(
content.FileName = content.Body
content.Body = a.Alt
}
stickerInfo := map[string]any{"alt": a.Alt, "id": strconv.FormatInt(document.ID, 10)}
if setID, ok := a.Stickerset.(*tg.InputStickerSetID); ok {
stickerInfo["pack"] = map[string]any{
"id": strconv.FormatInt(setID.ID, 10),
"access_hash": strconv.FormatInt(setID.AccessHash, 10),
"access_hash_source": strconv.FormatInt(tc.telegramUserID, 10),
}
} else if shortName, ok := a.Stickerset.(*tg.InputStickerSetShortName); ok {
stickerInfo["pack"] = map[string]any{
"short_name": shortName.ShortName,
}
}
extraInfo["fi.mau.telegram.sticker"] = stickerInfo
transferer = transferer.WithStickerConfig(tc.main.Config.AnimatedSticker)
transferer = transferer.
WithStickerConfig(tc.main.Config.AnimatedSticker).
WithStickerMetadata(tc.stickerSourceFromAttribute(ctx, document.ID, a))
case *tg.DocumentAttributeAnimated:
isVideoGif = true
extraInfo["fi.mau.telegram.gif"] = true