From 8512cfe6a6b78e01f2137bc6fc9ef21a4661680b Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 23 Apr 2026 14:26:52 +0300 Subject: [PATCH] commands/imagepack: include pack metadata in sticker info --- go.mod | 2 +- go.sum | 4 +- pkg/connector/client.go | 4 ++ pkg/connector/handlematrix.go | 110 ++++++++++++++++++++++++++++++---- pkg/connector/imagepack.go | 25 +++++++- pkg/connector/tomatrix.go | 7 ++- 6 files changed, 134 insertions(+), 18 deletions(-) diff --git a/go.mod b/go.mod index b9ab5493..85eec102 100644 --- a/go.mod +++ b/go.mod @@ -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.20260420202037-7783de22f56c + maunium.net/go/mautrix v0.27.1-0.20260422171355-c6fe96e2dea3 rsc.io/qr v0.2.0 ) diff --git a/go.sum b/go.sum index 551e8300..a8c61fd1 100644 --- a/go.sum +++ b/go.sum @@ -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.20260420202037-7783de22f56c h1:qHUNGpgxCoEwK+d+Hqep9vRf+ds9lzOaVy245/Q3LuU= -maunium.net/go/mautrix v0.27.1-0.20260420202037-7783de22f56c/go.mod h1:7QpEQiTy6p4LHkXXaZI+N46tGYy8HMhD0JjzZAFoFWs= +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= 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 aa757276..a6110478 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -117,6 +117,9 @@ type TelegramClient struct { prevReactionPoll map[networkid.PortalKey]time.Time prevReactionPollLock sync.Mutex + + stickerPackCache map[string]map[int64]*tg.Document + stickerPackCacheLock sync.Mutex } var _ bridgev2.NetworkAPI = (*TelegramClient)(nil) @@ -172,6 +175,7 @@ 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{}, recentMessageRooms: exsync.NewRingBuffer[networkid.MessageID, networkid.PortalKey](32), diff --git a/pkg/connector/handlematrix.go b/pkg/connector/handlematrix.go index 0d28dc1f..221b7852 100644 --- a/pkg/connector/handlematrix.go +++ b/pkg/connector/handlematrix.go @@ -57,6 +57,7 @@ import ( "go.mau.fi/mautrix-telegram/pkg/gotd/telegram/message" "go.mau.fi/mautrix-telegram/pkg/gotd/telegram/uploader" "go.mau.fi/mautrix-telegram/pkg/gotd/tg" + "go.mau.fi/mautrix-telegram/pkg/gotd/tgerr" "go.mau.fi/mautrix-telegram/pkg/connector/emojis" "go.mau.fi/mautrix-telegram/pkg/connector/humanise" @@ -242,10 +243,90 @@ func (tc *TelegramClient) pollSponsoredMessage(ctx context.Context, portal *brid return nil } -func (tc *TelegramClient) transferMediaToTelegram(ctx context.Context, content *event.MessageEventContent, sticker, forceDocument bool) (tg.InputMediaClass, error) { +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 { + zerolog.Ctx(ctx).Err(err).Msg("Failed to find original sticker document, falling back to reupload") + } else if origFile != nil { + return origFile, nil + } + } err := tc.main.Bridge.Bot.DownloadMediaToFile(ctx, content.URL, content.File, false, func(f *os.File) (err error) { uploadFilename := f.Name() if sticker && (info.MimeType == "image/png" || info.MimeType == "image/jpeg") { @@ -458,19 +539,26 @@ func (tc *TelegramClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2 var updates tg.UpdatesClass if msg.Event.Type == event.EventSticker { - var media tg.InputMediaClass - media, err = tc.transferMediaToTelegram(ctx, msg.Content, true, false) - if err != nil { - return nil, err - } - updates, err = tc.client.API().MessagesSendMedia(ctx, &tg.MessagesSendMediaRequest{ + mediaReq := &tg.MessagesSendMediaRequest{ Peer: peer, Message: message, Entities: entities, - Media: media, ReplyTo: replyTo, RandomID: randomID, - }) + } + mediaReq.Media, err = tc.transferMediaToTelegram(ctx, msg.Content, true, false, false) + if err != nil { + return nil, err + } + updates, err = tc.client.API().MessagesSendMedia(ctx, mediaReq) + if tgerr.Is(err, tg.ErrFileReferenceExpired) { + zerolog.Ctx(ctx).Debug().AnErr("send_error", err).Msg("Trying to refetch sticker pack") + mediaReq.Media, err = tc.transferMediaToTelegram(ctx, msg.Content, true, true, false) + if err != nil { + return nil, err + } + updates, err = tc.client.API().MessagesSendMedia(ctx, mediaReq) + } } else { switch msg.Content.MsgType { case event.MsgText, event.MsgNotice, event.MsgEmote: @@ -485,7 +573,7 @@ func (tc *TelegramClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2 case event.MsgImage, event.MsgFile, event.MsgAudio, event.MsgVideo: var media tg.InputMediaClass forceDocument, _ := msg.Event.Content.Raw["fi.mau.telegram.force_document"].(bool) - media, err = tc.transferMediaToTelegram(ctx, msg.Content, false, forceDocument) + media, err = tc.transferMediaToTelegram(ctx, msg.Content, false, false, forceDocument) if err != nil { return nil, err } @@ -650,7 +738,7 @@ func (tc *TelegramClient) HandleMatrixEdit(ctx context.Context, msg *bridgev2.Ma } else { log.Info().Msg("media URI changed, re-uploading media") forceDocument, _ := msg.Event.Content.Raw["fi.mau.telegram.force_document"].(bool) - req.Media, err = tc.transferMediaToTelegram(ctx, msg.Content, false, forceDocument) + req.Media, err = tc.transferMediaToTelegram(ctx, msg.Content, false, false, forceDocument) if err != nil { return err } diff --git a/pkg/connector/imagepack.go b/pkg/connector/imagepack.go index 50db2f48..7d5398f5 100644 --- a/pkg/connector/imagepack.go +++ b/pkg/connector/imagepack.go @@ -482,6 +482,13 @@ func (tc *TelegramClient) fnDownloadEmojiPack(ce *commands.Event) { Attribution: fmt.Sprintf("Imported from https://t.me/%s/%s", linkType, set.Set.ShortName), }, } + topLevelExtra := map[string]any{ + "fi.mau.telegram.stickerpack": map[string]any{ + "id": strconv.FormatInt(set.Set.ID, 10), + "short_name": set.Set.ShortName, + "emoji_pack": set.Set.Emojis, + }, + } keywords := make(map[int64][]string) emojis := make(map[int64][]string) for _, kw := range set.Keywords { @@ -524,13 +531,29 @@ func (tc *TelegramClient) fnDownloadEmojiPack(ce *commands.Event) { if len(imageEmojis) > 0 { body = imageEmojis[0] } + 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), + }, + }, + } + } pack.Images[key] = &event.ImagePackImage{ URL: mxc, Body: body, Info: info, } } - _, err = tc.main.Bridge.Bot.SendState(ce.Ctx, spaceRoom, event.StateUnstableImagePack, set.Set.ShortName, &event.Content{Parsed: pack}, time.Now()) + _, err = tc.main.Bridge.Bot.SendState(ce.Ctx, spaceRoom, event.StateUnstableImagePack, set.Set.ShortName, &event.Content{ + Parsed: pack, + Raw: topLevelExtra, + }, time.Now()) if err != nil { ce.Reply("Failed to send image pack to space: %v", err) } else { diff --git a/pkg/connector/tomatrix.go b/pkg/connector/tomatrix.go index ab02a6e7..688a20cd 100644 --- a/pkg/connector/tomatrix.go +++ b/pkg/connector/tomatrix.go @@ -606,7 +606,7 @@ func (tc *TelegramClient) convertMediaRequiringUpload( } case *tg.DocumentAttributeImageSize: transferer = transferer.WithImageSize(a) - if content.MsgType == event.MsgFile { + if content.MsgType == event.MsgFile && !isSticker { content.MsgType = event.MsgImage extra["fi.mau.telegram.force_document"] = true defaultFileName = "image_document" @@ -623,8 +623,9 @@ func (tc *TelegramClient) convertMediaRequiringUpload( 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), + "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{