mirror of
https://github.com/mautrix/telegram.git
synced 2026-05-17 07:25:46 +03:00
commands: add support for bridging image packs
This commit is contained in:
2
go.mod
2
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
|
||||
)
|
||||
|
||||
|
||||
4
go.sum
4
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=
|
||||
|
||||
@@ -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: "<upload/download/list/help> [args...]",
|
||||
},
|
||||
RequiresLogin: true,
|
||||
}
|
||||
|
||||
const emojiPackHelp = `This command can be used to transfer emoji packs between Matrix and Telegram.
|
||||
|
||||
* $cmdprefix emoji-pack upload <room ID> <pack key> - Transfer a pack from Matrix to Telegram.
|
||||
* $cmdprefix emoji-pack download <pack shortcode or link> - 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 <upload/download/list/help> [args...]`")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
515
pkg/connector/imagepack.go
Normal file
515
pkg/connector/imagepack.go
Normal file
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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 <room ID> <state key> <telegram shortcode>`")
|
||||
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 <pack shortcode or link>`")
|
||||
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()))
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user