// mautrix-telegram - A Matrix-Telegram puppeting bridge. // Copyright (C) 2026 Tulir Asokan // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package connector import ( "bytes" "cmp" "context" "fmt" "image" _ "image/gif" "image/png" "net/http" "regexp" "slices" "strconv" "strings" "time" "github.com/rs/zerolog" "go.mau.fi/util/emojishortcodes" "go.mau.fi/util/exmaps" "go.mau.fi/util/ffmpeg" "go.mau.fi/util/variationselector" "golang.org/x/image/draw" _ "golang.org/x/image/webp" "maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2/commands" "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/format" "maunium.net/go/mautrix/id" "go.mau.fi/mautrix-telegram/pkg/connector/media" "go.mau.fi/mautrix-telegram/pkg/connector/store" "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" ) func (tc *TelegramClient) fnListEmojiPacks(ce *commands.Event) { resp, err := tc.client.API().MessagesGetAllStickers(ce.Ctx, 0) if err != nil { ce.Reply("Failed to list image packs: %v", err) return } casted, ok := resp.(*tg.MessagesAllStickers) if !ok { ce.Reply("Unexpected response type: %T", resp) return } lines := make([]string, len(casted.Sets)) for i, set := range casted.Sets { packType := "stickers" if set.Emojis { packType = "emojis" } lines[i] = fmt.Sprintf( "* %s (%s, %s)", format.EscapeMarkdown(set.Title), packType, format.SafeMarkdownCode(set.ShortName), ) } ce.Reply("Your packs:\n\n%s", strings.Join(lines, "\n")) } func (tc *TelegramClient) fnUploadEmojiPack(ce *commands.Event) { if len(ce.Args) < 3 || !strings.HasPrefix(ce.Args[1], "!") { ce.Reply("Usage: `$cmdprefix emoji-pack upload `") return } dbl := ce.User.DoublePuppet(ce.Ctx) if dbl == nil { ce.Reply("Double puppeting is required to fetch emoji packs from Matrix") return } mx, ok := dbl.(bridgev2.MatrixAPIWithArbitraryRoomState) if !ok { ce.Reply("Matrix connector does not implement required interface") return } tgPackShortcode := ce.Args[0] roomID := id.RoomID(ce.Args[1]) packStateKey := strings.Join(ce.Args[2:], " ") err := tc.main.Bridge.Bot.EnsureJoined(ce.Ctx, roomID) if err != nil { ce.Reply("Failed to join room: %v", err) return } evt, err := mx.GetStateEvent(ce.Ctx, roomID, event.Type{Type: "im.ponies.room_emotes", Class: event.StateEventType}, packStateKey) if err != nil { ce.Reply("Failed to get state event: %v", err) return } pack, ok := evt.Content.Parsed.(*event.ImagePackEventContent) if !ok { ce.Reply("Unexpected parsed content type %T", evt.Content.Parsed) return } evtID := ce.React("\u23f3\ufe0f") defer redactReaction(ce, evtID) link, err := tc.synchronizeEmojiPack(ce.Ctx, ce, pack, tgPackShortcode) if err != nil { ce.Reply("Failed to synchronize emoji pack: %v", err) return } ce.Reply("Successfully synchronized %s", link) } func resizeEmoji(src image.Image, size int) *image.RGBA { resized := image.NewRGBA(image.Rect(0, 0, size, size)) bounds := src.Bounds() srcW, srcH := bounds.Dx(), bounds.Dy() if srcW <= 0 || srcH <= 0 { return resized } dstW, dstH := size, size if srcW > srcH { dstH = srcH * size / srcW if dstH < 1 { dstH = 1 } } else if srcH > srcW { dstW = srcW * size / srcH if dstW < 1 { dstW = 1 } } left := (size - dstW) / 2 top := (size - dstH) / 2 dstRect := image.Rect(left, top, left+dstW, top+dstH) draw.BiLinear.Scale(resized, dstRect, src, bounds, draw.Over, nil) return resized } func resizeSticker(src image.Image, maxSide int) *image.RGBA { var dstW, dstH int bounds := src.Bounds() srcW, srcH := bounds.Dx(), bounds.Dy() if srcW == srcH { dstW = maxSide dstH = maxSide } else if srcW > srcH { dstW = maxSide dstH = srcH * maxSide / srcW } else { dstH = maxSide dstW = srcW * maxSide / srcH } resized := image.NewRGBA(image.Rect(0, 0, dstW, dstH)) draw.BiLinear.Scale(resized, resized.Bounds(), src, bounds, draw.Over, nil) return resized } func reencodeImage(data []byte, resizer func(image.Image, int) *image.RGBA, size int) ([]byte, string, error) { decoded, _, err := image.Decode(bytes.NewReader(data)) if err != nil { return nil, "", fmt.Errorf("failed to decode image: %w", err) } var buf bytes.Buffer err = png.Encode(&buf, resizer(decoded, size)) if err != nil { return nil, "", fmt.Errorf("failed to re-encode image: %w", err) } return buf.Bytes(), "image/png", nil } func convertGIFToWebM(ctx context.Context, data []byte, scaleFilter string) ([]byte, string, error) { if !ffmpeg.Supported() { return nil, "", fmt.Errorf("ffmpeg is not available") } webmData, err := ffmpeg.ConvertBytes(ctx, data, ".webm", nil, []string{ "-vf", scaleFilter, "-c:v", "libvpx-vp9", "-pix_fmt", "yuva420p", "-t", "3", "-f", "webm", }, "image/gif") if err != nil { return nil, "", fmt.Errorf("failed to convert gif to webm: %w", err) } return webmData, "video/webm", nil } func normalizeImage(ctx context.Context, data []byte, info *event.FileInfo, emoji bool) (convertedData []byte, convertedMime string, err error) { if emoji { if info.MimeType == "image/gif" { return convertGIFToWebM(ctx, data, "fps=fps='min(source_fps,30)',scale=100:100:force_original_aspect_ratio=decrease:flags=lanczos,pad=100:100:(ow-iw)/2:(oh-ih)/2:color=0x00000000") } if info.Width == 100 && info.Height == 100 { return data, info.MimeType, nil } return reencodeImage(data, resizeEmoji, 100) } else { if info.Width == 512 || info.Height == 512 { return data, info.MimeType, nil } if info.MimeType == "image/gif" { return convertGIFToWebM(ctx, data, "fps=fps='min(source_fps,30)',scale=512:512:force_original_aspect_ratio=decrease:flags=lanczos") } return reencodeImage(data, resizeSticker, 512) } } func (tc *TelegramClient) synchronizeEmoji( ctx context.Context, shortcode string, img *event.ImagePackImage, emoji bool, ) (*tg.InputStickerSetItem, func(int64) error, error) { data, err := tc.main.Bridge.Bot.DownloadMedia(ctx, img.URL, nil) if err != nil { return nil, nil, fmt.Errorf("failed to download %s (%s): %w", shortcode, img.URL, err) } if img.Info == nil { img.Info = &event.FileInfo{} } if img.Info.MimeType == "" { img.Info.MimeType = http.DetectContentType(data) } origWidth, origHeight := img.Info.Width, img.Info.Height cfg, _, err := image.DecodeConfig(bytes.NewReader(data)) if err != nil { return nil, nil, fmt.Errorf("failed to decode image config for %s: %w", shortcode, err) } img.Info.Width = cfg.Width img.Info.Height = cfg.Height if origWidth == 0 || origHeight == 0 { origWidth, origHeight = cfg.Width, cfg.Height } data, mime, err := normalizeImage(ctx, data, img.Info, emoji) if err != nil { return nil, nil, fmt.Errorf("failed to normalize image for %s: %w", shortcode, err) } up, err := uploader.NewUploader(tc.client.API()).FromBytes(ctx, "", data) if err != nil { return nil, nil, fmt.Errorf("failed to reupload %s: %w", shortcode, err) } uploaded, err := tc.client.API().MessagesUploadMedia(ctx, &tg.MessagesUploadMediaRequest{ Media: &tg.InputMediaUploadedDocument{ File: up, ForceFile: true, MimeType: mime, }, Peer: &tg.InputPeerSelf{}, }) if err != nil { return nil, nil, fmt.Errorf("failed to finalize reuploaded media for %s: %w", shortcode, err) } doc, ok := uploaded.(*tg.MessageMediaDocument) if !ok { return nil, nil, fmt.Errorf("unexpected uploaded media type %T for %s", uploaded, shortcode) } fakeDoc, ok := doc.Document.(*tg.Document) if !ok { return nil, nil, fmt.Errorf("unexpected document type %T for %s", doc.Document, shortcode) } cacheRealDoc := func(realDocID int64) error { if realDocID == 0 { return fmt.Errorf("failed to get real document ID for %s/%d", shortcode, fakeDoc.ID) } err = tc.main.Store.TelegramFile.Insert(ctx, &store.TelegramFile{ LocationID: store.TelegramFileLocationID(strconv.FormatInt(realDocID, 10)), MXC: img.URL, MIMEType: img.Info.MimeType, Size: len(data), Width: origWidth, Height: origHeight, Timestamp: time.Now(), }) if err != nil { return fmt.Errorf("failed to cache mxc for %s/%d: %w", shortcode, realDocID, err) } return nil } return &tg.InputStickerSetItem{ Document: fakeDoc.AsInput(), Emoji: "\u2728\ufe0f", Keywords: shortcode, }, cacheRealDoc, nil } func extractNewDocID(oldSet tg.MessagesStickerSetClass, newSetBox tg.MessagesStickerSetClass) int64 { newSet, ok := newSetBox.(*tg.MessagesStickerSet) if !ok { return 0 } oldDocIDs := make(exmaps.Set[int64]) if oldSet != nil { for _, doc := range oldSet.(*tg.MessagesStickerSet).Documents { oldDocIDs.Add(doc.GetID()) } } var found int64 for _, doc := range newSet.Documents { if !oldDocIDs.Has(doc.GetID()) { if found == 0 { found = doc.GetID() } else { return 0 } } } return found } func (tc *TelegramClient) synchronizeEmojiPack(ctx context.Context, ce *commands.Event, pack *event.ImagePackEventContent, packShortcode string) (string, error) { resp, err := tc.client.API().StickersCheckShortName(ctx, packShortcode) if err != nil && !tgerr.Is(err, tg.ErrShortNameOccupied) { return "", fmt.Errorf("failed to check if shortcode is available: %w", err) } isEmojiPack := slices.Contains(pack.Metadata.Usage, event.ImagePackUsageEmoji) || len(pack.Metadata.Usage) == 0 var rawSet tg.MessagesStickerSetClass if resp { var shortcode string var img *event.ImagePackImage for shortcode, img = range pack.Images { break } if img == nil { return "", fmt.Errorf("pack must contain at least one image") } item, saveCache, err := tc.synchronizeEmoji(ctx, shortcode, img, isEmojiPack) if err != nil { return "", fmt.Errorf("failed to synchronize emoji %s: %w", shortcode, err) } rawSet, err = tc.client.API().StickersCreateStickerSet(ctx, &tg.StickersCreateStickerSetRequest{ Emojis: isEmojiPack, UserID: &tg.InputUserSelf{}, Title: cmp.Or(pack.Metadata.DisplayName, packShortcode), ShortName: packShortcode, Stickers: []tg.InputStickerSetItem{*item}, }) if err != nil { return "", fmt.Errorf("failed to create pack: %w", err) } err = saveCache(extractNewDocID(nil, rawSet)) if err != nil { return "", fmt.Errorf("failed to cache document ID for new pack: %w", err) } } else { rawSet, err = tc.client.API().MessagesGetStickerSet(ctx, &tg.MessagesGetStickerSetRequest{ Stickerset: &tg.InputStickerSetShortName{ShortName: packShortcode}, }) if err != nil { return "", fmt.Errorf("failed to get pack: %w", err) } } set, ok := rawSet.(*tg.MessagesStickerSet) if !ok { return "", fmt.Errorf("unexpected set type %T", rawSet) } if !set.Set.Creator { return "", fmt.Errorf("set %s was created by someone else", packShortcode) } isEmojiPack = set.Set.Emojis inputSet := &tg.InputStickerSetID{ ID: set.Set.ID, AccessHash: set.Set.AccessHash, } deletedMXCs := make(map[id.ContentURIString]*tg.InputDocument, len(set.Documents)) existingMXCs := make(exmaps.Set[id.ContentURIString], len(set.Documents)) for _, doc := range set.Documents { file, err := tc.main.Store.TelegramFile.GetByLocationID(ctx, store.TelegramFileLocationID(strconv.FormatInt(doc.GetID(), 10))) if err != nil { return "", fmt.Errorf("failed to get cached file for doc %d: %w", doc.GetID(), err) } else if file != nil { deletedMXCs[file.MXC] = doc.(*tg.Document).AsInput() existingMXCs.Add(file.MXC) } } for shortcode, img := range pack.Images { if existingMXCs.Has(img.URL) { delete(deletedMXCs, img.URL) continue } existingMXCs.Add(img.URL) item, saveCache, err := tc.synchronizeEmoji(ctx, shortcode, img, isEmojiPack) if err != nil { ce.Reply("Failed to reupload %s: %v", shortcode, err) continue } rawNewSet, err := tc.client.API().StickersAddStickerToSet(ctx, &tg.StickersAddStickerToSetRequest{ Stickerset: inputSet, Sticker: *item, }) if err != nil { if tgerr.Is(err, tg.ErrStickerpackStickersTooMuch) || tgerr.Is(err, tg.ErrStickersTooMuch) { return "", err } ce.Reply("Failed to add %s/%d to pack: %v", shortcode, item.Document.(*tg.InputDocument).ID, err) continue } err = saveCache(extractNewDocID(rawSet, rawNewSet)) if err != nil { return "", fmt.Errorf("failed to cache document ID for new pack: %w", err) } rawSet = rawNewSet } for mxc, inputDoc := range deletedMXCs { _, err = tc.client.API().StickersRemoveStickerFromSet(ctx, inputDoc) if err != nil { return "", fmt.Errorf("failed to remove %s/%d from set: %w", mxc, inputDoc.ID, err) } } linktype := "addstickers" if isEmojiPack { linktype = "addemoji" } return fmt.Sprintf("https://t.me/%s/%s", linktype, set.Set.ShortName), nil } var addStickersRegex = regexp.MustCompile(`^(?:(?:https?://)?(?:t|telegram)\.(?:me|dog)/(?:addstickers|addemoji)/)?([A-Za-z0-9-_]+)(?:\.json)?$`) var packShortcodeRegex = regexp.MustCompile(`^[A-Za-z0-9-_]+$`) func redactReaction(ce *commands.Event, evtID id.EventID) { if evtID == "" { return } _, _ = ce.Bot.SendMessage(ce.Ctx, ce.OrigRoomID, event.EventRedaction, &event.Content{ Parsed: &event.RedactionEventContent{ Redacts: evtID, }, }, nil) } func (tc *TelegramClient) fnDownloadEmojiPack(ce *commands.Event) { if len(ce.Args) == 0 { ce.Reply("Usage: `$cmdprefix emoji-pack download `") return } spaceRoom, err := tc.userLogin.GetSpaceRoom(ce.Ctx) if err != nil { ce.Reply("Failed to get space room: %v", err) return } else if spaceRoom == "" { ce.Reply("Can't bridge image packs if personal filtering spaces are disabled") return } evtID := ce.React("\u23f3\ufe0f") defer redactReaction(ce, evtID) pack, err := tc.DownloadImagePack(ce.Ctx, ce.Args[0]) if err != nil { ce.Reply("Failed to import pack: %v", err) return } if pack.Shortcode == "" && pack.Content.Metadata.BridgedPack != nil { pack.Shortcode = pack.Content.Metadata.BridgedPack.URL } _, err = tc.main.Bridge.Bot.SendState(ce.Ctx, spaceRoom, event.StateUnstableImagePack, pack.Shortcode, &event.Content{ Parsed: pack.Content, Raw: pack.Extra, }, time.Now()) if err != nil { ce.Reply("Failed to send image pack to space: %v", err) } else { ce.Reply( "Successfully bridged image pack to %s", format.MarkdownLink("your personal filtering space", spaceRoom.URI(tc.main.Bridge.Matrix.ServerName()).MatrixToURL())) } } func (tc *TelegramClient) ListImagePacks(ctx context.Context) ([]*event.ImagePackMetadata, error) { resp, err := tc.client.API().MessagesGetAllStickers(ctx, 0) if err != nil { return nil, err } casted, ok := resp.(*tg.MessagesAllStickers) if !ok { return nil, fmt.Errorf("unexpected response type: %T", resp) } packs := make([]*event.ImagePackMetadata, len(casted.Sets)) for i, set := range casted.Sets { packs[i] = tc.makeImagePackMetadata(ctx, set) } return packs, nil } func (tc *TelegramClient) makeImagePackMetadata(ctx context.Context, pack tg.StickerSet) *event.ImagePackMetadata { linkType := "addstickers" usage := event.ImagePackUsageSticker if pack.Emojis { linkType = "addemoji" usage = event.ImagePackUsageEmoji } packURL := fmt.Sprintf("https://t.me/%s/%s", linkType, pack.ShortName) return &event.ImagePackMetadata{ DisplayName: pack.Title, AvatarURL: "", // TODO Usage: []event.ImagePackUsage{usage}, Attribution: fmt.Sprintf("Imported from %s", packURL), BridgedPack: &event.BridgedStickerPack{ Network: StickerSourceID, URL: packURL, }, } } func (tc *TelegramClient) DownloadImagePack(ctx context.Context, url string) (*bridgev2.ImportedImagePack, error) { var shortName string if match := addStickersRegex.FindStringSubmatch(url); match != nil { shortName = match[1] } else if packShortcodeRegex.MatchString(url) { shortName = url } else { return nil, fmt.Errorf("invalid pack shortcode or link: %s", url) } rawSet, err := tc.client.API().MessagesGetStickerSet(ctx, &tg.MessagesGetStickerSetRequest{Stickerset: &tg.InputStickerSetShortName{ShortName: shortName}}) if err != nil { return nil, err } set, ok := rawSet.(*tg.MessagesStickerSet) if !ok { return nil, fmt.Errorf("unexpected response type: %T", rawSet) } tc.addStickerPackToCache(set, true) pack := &event.ImagePackEventContent{ Images: make(map[string]*event.ImagePackImage, len(set.Documents)), Metadata: *tc.makeImagePackMetadata(ctx, set.Set), } 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) emojiLists := make(map[int64][]string) for _, kw := range set.Keywords { keywords[kw.DocumentID] = kw.Keyword } for _, emojiPack := range set.Packs { emoji := variationselector.Add(emojiPack.Emoticon) for _, doc := range emojiPack.Documents { emojiLists[doc] = append(emojiLists[doc], emoji) } } for i, rawDoc := range set.Documents { // TODO use direct media mxc, _, info, err := 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) 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) } kws := keywords[rawDoc.GetID()] imageEmojis := emojiLists[rawDoc.GetID()] var key string for _, kw := range kws { _, alreadySet := pack.Images[kw] if alreadySet { continue } key = kw break } var firstShortcode string if key == "" { for _, emoji := range imageEmojis { shortcode := emojishortcodes.Get(emoji) if shortcode == "" { continue } shortcode = fmt.Sprintf("%s_%s", set.Set.ShortName, shortcode) if firstShortcode == "" { firstShortcode = shortcode } _, alreadySet := pack.Images[shortcode] if alreadySet { continue } key = shortcode break } } if key == "" && firstShortcode != "" { for i := 2; i < 10000; i++ { kw := fmt.Sprintf("%s%d", firstShortcode, i) _, alreadySet := pack.Images[kw] if alreadySet { continue } key = kw } } if key == "" { key = fmt.Sprintf("%s_img%d", set.Set.ShortName, i+1) } var emoji string if len(imageEmojis) > 0 { emoji = 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.BridgedSticker = &event.BridgedSticker{ Network: StickerSourceID, ID: strconv.FormatInt(rawDoc.GetID(), 10), PackURL: StickerPackURLPrefix + set.Set.ShortName, Emoji: emoji, } } pack.Images[key] = &event.ImagePackImage{ URL: mxc, Body: cmp.Or(emoji, key), Info: info, } } return &bridgev2.ImportedImagePack{ Content: pack, Extra: topLevelExtra, Shortcode: set.Set.ShortName, }, nil } 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 = tc.addStickerPackToCache(set, false) } return cache, nil } func (tc *TelegramClient) addStickerPackToCache(set *tg.MessagesStickerSet, lock bool) *stickerPackCache { if lock { tc.stickerPackCacheLock.Lock() defer tc.stickerPackCacheLock.Unlock() } 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 } 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 }