diff --git a/go.mod b/go.mod index 85eec102..790dffb5 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index a8c61fd1..5bc03546 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/pkg/connector/client.go b/pkg/connector/client.go index f466c926..e3df2e13 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -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), diff --git a/pkg/connector/handlematrix.go b/pkg/connector/handlematrix.go index 221b7852..62e3f4fe 100644 --- a/pkg/connector/handlematrix.go +++ b/pkg/connector/handlematrix.go @@ -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 diff --git a/pkg/connector/imagepack.go b/pkg/connector/imagepack.go index 7d5398f5..7a72e95e 100644 --- a/pkg/connector/imagepack.go +++ b/pkg/connector/imagepack.go @@ -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 +} diff --git a/pkg/connector/media/transfer.go b/pkg/connector/media/transfer.go index 4e8d9708..d56ab544 100644 --- a/pkg/connector/media/transfer.go +++ b/pkg/connector/media/transfer.go @@ -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 diff --git a/pkg/connector/tomatrix.go b/pkg/connector/tomatrix.go index 4566370b..bbeb810d 100644 --- a/pkg/connector/tomatrix.go +++ b/pkg/connector/tomatrix.go @@ -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