diff --git a/go.mod b/go.mod index e6864526..e0f4bfd0 100644 --- a/go.mod +++ b/go.mod @@ -42,7 +42,7 @@ require ( golang.org/x/sync v0.20.0 golang.org/x/tools v0.43.0 gopkg.in/yaml.v3 v3.0.1 - maunium.net/go/mautrix v0.26.5-0.20260328215557-463f855b360f + maunium.net/go/mautrix v0.26.5-0.20260329141107-44f8c3fd4d76 rsc.io/qr v0.2.0 ) diff --git a/go.sum b/go.sum index 4c7e417f..c4eff6a3 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.26.5-0.20260328215557-463f855b360f h1:zxH+U8f32zjnCBO+Dzq0RWt30BVeQSa51lN1/Z/BFyc= -maunium.net/go/mautrix v0.26.5-0.20260328215557-463f855b360f/go.mod h1:RUSMBPky3jhXB7Ux+AptfkEvFlJ4ajZKCYiXI8YzxVE= +maunium.net/go/mautrix v0.26.5-0.20260329141107-44f8c3fd4d76 h1:UKXGGIttTasZwodeXFNaWxif1Cm7mRY5/CA4SFKePVE= +maunium.net/go/mautrix v0.26.5-0.20260329141107-44f8c3fd4d76/go.mod h1:RUSMBPky3jhXB7Ux+AptfkEvFlJ4ajZKCYiXI8YzxVE= 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/commands.go b/pkg/connector/commands.go index 1601c325..0e742526 100644 --- a/pkg/connector/commands.go +++ b/pkg/connector/commands.go @@ -18,9 +18,12 @@ package connector import ( "slices" + "strings" + _ "golang.org/x/image/webp" "maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2/commands" + "maunium.net/go/mautrix/bridgev2/networkid" "maunium.net/go/mautrix/format" ) @@ -55,3 +58,60 @@ func fnSyncChats(ce *commands.Event) { } } } + +var cmdEmojiPack = &commands.FullHandler{ + Func: fnEmojiPack, + Name: "emoji-pack", + Aliases: []string{"pack", "sticker-pack", "emojipack", "stickerpack"}, + Help: commands.HelpMeta{ + Section: commands.HelpSectionChats, + Description: "Bridge emoji packs between Matrix and Telegram.", + Args: " [args...]", + }, + RequiresLogin: true, +} + +const emojiPackHelp = `This command can be used to transfer emoji packs between Matrix and Telegram. + +* $cmdprefix emoji-pack upload - Transfer a pack from Matrix to Telegram. +* $cmdprefix emoji-pack download - Transfer a pack from Telegram to Matrix. +* $cmdprefix emoji-pack list - List your current emoji packs on Telegram. +* $cmdprefix emoji-pack help - Show this help message.` + +func fnEmojiPack(ce *commands.Event) { + var login *bridgev2.UserLogin + if len(ce.Args) > 0 { + targetLogin := ce.Bridge.GetCachedUserLoginByID(networkid.UserLoginID(ce.Args[0])) + if targetLogin != nil && targetLogin.UserMXID == ce.User.MXID { + ce.Args = ce.Args[1:] + login = targetLogin + } + } + var command string + if len(ce.Args) > 0 { + command = strings.ToLower(ce.Args[0]) + ce.Args = ce.Args[1:] + } + + if login == nil { + login = ce.User.GetDefaultLogin() + if login == nil { + ce.Reply("You're not logged in.") + return + } + } + client := login.Client.(*TelegramClient) + + switch command { + case "help", "": + ce.Reply(emojiPackHelp) + case "list": + client.fnListEmojiPacks(ce) + case "upload": + client.fnUploadEmojiPack(ce) + case "download": + client.fnDownloadEmojiPack(ce) + default: + ce.Reply("Usage: `$cmdprefix emoji-pack [args...]`") + } +} diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index c254ffa1..cf540559 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -41,7 +41,7 @@ var _ bridgev2.MaxFileSizeingNetwork = (*TelegramConnector)(nil) func (tg *TelegramConnector) Init(bridge *bridgev2.Bridge) { tg.Store = store.NewStore(bridge.DB.Database, dbutil.ZeroLogger(bridge.Log.With().Str("db_section", "telegram").Logger())) tg.Bridge = bridge - tg.Bridge.Commands.(*commands.Processor).AddHandlers(cmdSyncChats) + tg.Bridge.Commands.(*commands.Processor).AddHandlers(cmdSyncChats, cmdEmojiPack) } func (tg *TelegramConnector) Start(ctx context.Context) error { diff --git a/pkg/connector/imagepack.go b/pkg/connector/imagepack.go new file mode 100644 index 00000000..9032d2bf --- /dev/null +++ b/pkg/connector/imagepack.go @@ -0,0 +1,515 @@ +// 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/png" + "net/http" + "regexp" + "slices" + "strconv" + "strings" + "time" + + "go.mau.fi/util/exmaps" + "go.mau.fi/util/ffmpeg" + "go.mau.fi/util/variationselector" + "golang.org/x/image/draw" + "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 (t *TelegramClient) fnListEmojiPacks(ce *commands.Event) { + resp, err := t.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 (t *TelegramClient) fnUploadEmojiPack(ce *commands.Event) { + if len(ce.Args) < 3 || !strings.HasPrefix(ce.Args[0], "!") { + ce.Reply("Usage: `$cmdprefix emoji-pack upload `") + return + } + mx, ok := t.main.Bridge.Matrix.(bridgev2.MatrixConnectorWithArbitraryRoomState) + if !ok { + ce.Reply("Matrix connector does not support fetching room state") + return + } + err := t.main.Bridge.Bot.EnsureJoined(ce.Ctx, id.RoomID(ce.Args[0])) + if err != nil { + ce.Reply("Failed to join room: %v", err) + return + } + evt, err := mx.GetStateEvent(ce.Ctx, id.RoomID(ce.Args[0]), event.Type{Type: "im.ponies.room_emotes", Class: event.StateEventType}, ce.Args[1]) + 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) + err = t.synchronizeEmojiPack(ce.Ctx, pack, ce.Args[2]) + if err != nil { + ce.Reply("Failed to synchronize emoji pack: %v", err) + return + } + ce.Reply("Successfully synchronized https://t.me/addstickers/%s", ce.Args[2]) +} + +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", + "-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 (t *TelegramClient) synchronizeEmoji( + ctx context.Context, shortcode string, img *event.ImagePackImage, emoji bool, +) (*tg.InputStickerSetItem, func(int64) error, error) { + data, err := t.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) + } + if img.Info.Width == 0 || img.Info.Height == 0 { + cfg, _, _ := image.DecodeConfig(bytes.NewReader(data)) + img.Info.Width = cfg.Width + img.Info.Height = 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(t.client.API()).FromBytes(ctx, "", data) + if err != nil { + return nil, nil, fmt.Errorf("failed to reupload %s: %w", shortcode, err) + } + uploaded, err := t.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 = t.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: img.Info.Width, + Height: img.Info.Height, + 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 (t *TelegramClient) synchronizeEmojiPack(ctx context.Context, pack *event.ImagePackEventContent, packShortcode string) error { + resp, err := t.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 := t.synchronizeEmoji(ctx, shortcode, img, isEmojiPack) + if err != nil { + return fmt.Errorf("failed to synchronize emoji %s: %w", shortcode, err) + } + rawSet, err = t.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 = t.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, + } + existingMXCs := make(map[id.ContentURIString]*tg.InputDocument, len(set.Documents)) + for _, doc := range set.Documents { + file, err := t.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 { + existingMXCs[file.MXC] = doc.(*tg.Document).AsInput() + } + } + for shortcode, img := range pack.Images { + _, exists := existingMXCs[img.URL] + if exists { + delete(existingMXCs, img.URL) + continue + } + item, saveCache, err := t.synchronizeEmoji(ctx, shortcode, img, isEmojiPack) + if err != nil { + return fmt.Errorf("failed to synchronize emoji %s: %w", shortcode, err) + } + rawNewSet, err := t.client.API().StickersAddStickerToSet(ctx, &tg.StickersAddStickerToSetRequest{ + Stickerset: inputSet, + Sticker: *item, + }) + if err != nil { + return fmt.Errorf("failed to add %s/%d to set: %w", shortcode, item.Document.(*tg.InputDocument).ID, err) + } + 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 existingMXCs { + _, err = t.client.API().StickersRemoveStickerFromSet(ctx, inputDoc) + if err != nil { + return fmt.Errorf("failed to remove %s/%d from set: %w", mxc, inputDoc.ID, err) + } + } + return 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 (t *TelegramClient) fnDownloadEmojiPack(ce *commands.Event) { + if len(ce.Args) == 0 { + ce.Reply("Usage: `$cmdprefix emoji-pack download `") + return + } + spaceRoom, err := t.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 + } + var input tg.InputStickerSetClass + if match := addStickersRegex.FindStringSubmatch(ce.Args[0]); match != nil { + input = &tg.InputStickerSetShortName{ShortName: match[1]} + } else if packShortcodeRegex.MatchString(ce.Args[0]) { + input = &tg.InputStickerSetShortName{ShortName: ce.Args[0]} + } else { + ce.Reply("Invalid pack shortcode or link.") + return + } + rawSet, err := t.client.API().MessagesGetStickerSet(ce.Ctx, &tg.MessagesGetStickerSetRequest{Stickerset: input}) + if err != nil { + ce.Reply("Failed to get sticker set: %v", err) + return + } + set, ok := rawSet.(*tg.MessagesStickerSet) + if !ok { + ce.Reply("Unexpected response type: %T", rawSet) + return + } + linkType := "addstickers" + usage := event.ImagePackUsageSticker + if set.Set.Emojis { + linkType = "addemoji" + usage = event.ImagePackUsageEmoji + } + 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), + }, + } + keywords := make(map[int64][]string) + emojis := 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 { + emojis[doc] = append(emojis[doc], emoji) + } + } + evtID := ce.React("\u23f3\ufe0f") + defer redactReaction(ce, evtID) + for i, rawDoc := range set.Documents { + mxc, _, info, err := media.NewTransferer(t.client.API()). + WithStickerConfig(t.main.Config.AnimatedSticker). + WithForceWebmStickerConvert(set.Set.Emojis). + WithDocument(rawDoc, false). + Transfer(ce.Ctx, t.main.Store, t.main.Bridge.Bot) + if err != nil { + ce.Log.Err(err).Msg("Failed to transfer image in pack") + ce.Reply("Failed to transfer document `%d`: %v", rawDoc.GetID(), err) + return + } + kws := keywords[rawDoc.GetID()] + imageEmojis := emojis[rawDoc.GetID()] + var key string + for _, kw := range kws { + _, alreadySet := pack.Images[kw] + if alreadySet { + continue + } + key = kw + break + } + if key == "" { + key = fmt.Sprintf("%s_img%d", set.Set.ShortName, i+1) + } + body := key + if len(imageEmojis) > 0 { + body = imageEmojis[0] + } + pack.Images[key] = &event.ImagePackImage{ + URL: mxc, + Body: body, + Info: info, + } + } + _, err = t.main.Bridge.Bot.SendState(ce.Ctx, spaceRoom, event.StateUnstableImagePack, set.Set.ShortName, &event.Content{Parsed: pack}, 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(t.main.Bridge.Matrix.ServerName()).MatrixToURL())) + } +} diff --git a/pkg/connector/media/transfer.go b/pkg/connector/media/transfer.go index 12d3572c..4f1d66a0 100644 --- a/pkg/connector/media/transfer.go +++ b/pkg/connector/media/transfer.go @@ -290,7 +290,7 @@ func (t *Transferer) WithPeerPhoto(peer tg.InputPeerClass, photoID int64) *Ready // If there is a sticker config on the [Transferer], this function converts // animated stickers to the target format specified by the specified // [AnimatedStickerConfig]. -func (t *ReadyTransferer) Transfer(ctx context.Context, store *store.Container, intent bridgev2.MatrixAPI) (mxc id.ContentURIString, encryptedFileInfo *event.EncryptedFileInfo, outFileInfo *event.FileInfo, err error) { +func (t *ReadyTransferer) Transfer(ctx context.Context, db *store.Container, intent bridgev2.MatrixAPI) (mxc id.ContentURIString, encryptedFileInfo *event.EncryptedFileInfo, outFileInfo *event.FileInfo, err error) { locationID := getLocationID(t.loc) log := zerolog.Ctx(ctx).With(). Str("component", "media_transfer"). @@ -299,7 +299,7 @@ func (t *ReadyTransferer) Transfer(ctx context.Context, store *store.Container, ctx = log.WithContext(ctx) log.Debug().Msg("Transferring file from Telegram to Matrix") - if file, err := store.TelegramFile.GetByLocationID(ctx, locationID); err != nil { + if file, err := db.TelegramFile.GetByLocationID(ctx, locationID); err != nil { return "", nil, nil, fmt.Errorf("failed to search for Telegram file by location ID: %w", err) } else if file != nil { t.inner.fileInfo.Size = file.Size @@ -392,15 +392,16 @@ func (t *ReadyTransferer) Transfer(ctx context.Context, store *store.Container, // If it's an unencrypted file, cache the MXC URI corresponding to the // location ID. if len(mxc) > 0 { - file := store.TelegramFile.New() - file.LocationID = locationID - file.MXC = mxc - file.MIMEType = t.inner.fileInfo.MimeType - file.Size = t.inner.fileInfo.Size - file.Width = t.inner.fileInfo.Width - file.Height = t.inner.fileInfo.Height - file.Timestamp = time.Now() - if err = file.Insert(ctx); err != nil { + err = db.TelegramFile.Insert(ctx, &store.TelegramFile{ + LocationID: locationID, + MXC: mxc, + MIMEType: t.inner.fileInfo.MimeType, + Size: t.inner.fileInfo.Size, + Width: t.inner.fileInfo.Width, + Height: t.inner.fileInfo.Height, + Timestamp: time.Now(), + }) + if err != nil { log.Err(err).Msg("failed to insert Telegram file into database") } } diff --git a/pkg/connector/store/telegramfile.go b/pkg/connector/store/telegramfile.go index 9fb2de26..e873f8e1 100644 --- a/pkg/connector/store/telegramfile.go +++ b/pkg/connector/store/telegramfile.go @@ -26,10 +26,13 @@ import ( ) const ( - insertTelegramFileQuery = "INSERT INTO telegram_file (id, mxc, mime_type, size, width, height, timestamp) VALUES ($1, $2, $3, $4, $5, $6, $7)" + insertTelegramFileQuery = ` + INSERT INTO telegram_file (id, mxc, mime_type, size, width, height, timestamp) + VALUES ($1, $2, $3, $4, $5, $6, $7) + ` getTelegramFileSelect = "SELECT id, mxc, mime_type, size, width, height, timestamp FROM telegram_file" getTelegramFileByLocationIDQuery = getTelegramFileSelect + " WHERE id=$1" - getTelegramFileByMXCQuery = getTelegramFileSelect + " WHERE mxc=$1" + getTelegramFileByMXCQuery = getTelegramFileSelect + " WHERE mxc=$1 ORDER BY timestamp DESC LIMIT 1" ) type TelegramFileQuery struct { @@ -39,8 +42,6 @@ type TelegramFileQuery struct { type TelegramFileLocationID string type TelegramFile struct { - qh *dbutil.QueryHelper[*TelegramFile] - LocationID TelegramFileLocationID MXC id.ContentURIString MIMEType string @@ -53,7 +54,7 @@ type TelegramFile struct { var _ dbutil.DataStruct[*TelegramFile] = (*TelegramFile)(nil) func newTelegramFile(qh *dbutil.QueryHelper[*TelegramFile]) *TelegramFile { - return &TelegramFile{qh: qh} + return &TelegramFile{} } func (fq *TelegramFileQuery) GetByLocationID(ctx context.Context, locationID TelegramFileLocationID) (*TelegramFile, error) { @@ -64,6 +65,10 @@ func (fq *TelegramFileQuery) GetByMXC(ctx context.Context, mxc id.ContentURIStri return fq.QueryOne(ctx, getTelegramFileByMXCQuery, mxc) } +func (fq *TelegramFileQuery) Insert(ctx context.Context, f *TelegramFile) error { + return fq.Exec(ctx, insertTelegramFileQuery, f.sqlVariables()...) +} + func (f *TelegramFile) sqlVariables() []any { return []any{ f.LocationID, @@ -76,10 +81,6 @@ func (f *TelegramFile) sqlVariables() []any { } } -func (f *TelegramFile) Insert(ctx context.Context) error { - return f.qh.Exec(ctx, insertTelegramFileQuery, f.sqlVariables()...) -} - func (f *TelegramFile) Scan(row dbutil.Scannable) (*TelegramFile, error) { var mime sql.NullString var size, width, height, timestamp sql.NullInt64