Compare commits

...

20 Commits

Author SHA1 Message Date
Tulir Asokan e3bb26aee1 handlematrix: allow bridging cached custom emoji reactions with any scheme 2026-04-30 16:55:03 +03:00
Tulir Asokan 7c2c72bbde imagepack: implement listing interface 2026-04-30 15:49:10 +03:00
Tulir Asokan 2ffbde7448 .github: add another item to bug report template 2026-04-30 13:23:26 +03:00
Tulir Asokan 2a0da7801a imagepack: move emoji shortcodes to go-util 2026-04-30 12:24:08 +03:00
Tulir Asokan eaf387abfe imagepack: switch to bridgev2 API for importing 2026-04-29 18:01:36 +03:00
Tulir Asokan 64d80c3d1d imagepack: populate cache when importing pack 2026-04-29 16:18:52 +03:00
Tulir Asokan c78b1abd2d imagepack: use emoji shortcode as fallback when importing packs 2026-04-29 14:51:38 +03:00
Tulir Asokan 12f900a7bd dependencies: update mautrix-go 2026-04-29 09:10:02 +03:00
Tulir Asokan cdb77f938a tomatrix: include external_url field in messages 2026-04-28 22:01:54 +03:00
Tulir Asokan 5a1a478992 matrixfmt: convert matrix.to links in other direction too 2026-04-28 21:46:07 +03:00
Tulir Asokan d2a06ebbbe capabilities: mark lottie and webm as allowed sticker formats 2026-04-28 16:09:13 +03:00
Tulir Asokan e6243d8935 imagepack: switch to new shared metadata field 2026-04-27 20:24:10 +03:00
Tulir Asokan 9e1c42a992 matrixfmt: fix trimming all-space entity string 2026-04-27 20:24:10 +03:00
Tulir Asokan 6eacf38d74 tomatrix: use extra field in info for custom fields 2026-04-27 20:24:10 +03:00
Gerardo Rodriguez 65fcf712d3 client: treat pool.ErrConnDead as transient in onPing (#1066) 2026-04-24 13:58:43 +03:00
Tulir Asokan 8512cfe6a6 commands/imagepack: include pack metadata in sticker info 2026-04-23 14:26:52 +03:00
Tulir Asokan 7a6d1bf17a dependencies: update mautrix-go 2026-04-20 23:22:06 +03:00
Tulir Asokan 18f831553d changelog: update 2026-04-20 16:51:51 +03:00
Tulir Asokan dce0c4dbe1 handletelegram: add support for updateBotMessageReaction
Fixes #1064
2026-04-19 17:30:20 +03:00
Tulir Asokan ac2a2c2980 legacymigrate: fix mx_room_state migration on sqlite 2026-04-16 23:11:15 +03:00
16 changed files with 596 additions and 167 deletions
+2 -1
View File
@@ -11,7 +11,8 @@ type: Bug
### Checklist
<!-- Both items below are mandatory. Issues not following the rules may be closed without comment. -->
<!-- All items below are mandatory. Issues not following the rules may be closed without comment. -->
* [ ] This is an actual bug, not just a setup issue (see the [troubleshooting docs](https://docs.mau.fi/bridges/general/troubleshooting.html) or ask in the Matrix room for setup help).
* [ ] I am certain that sufficient information is included. Ask in the Matrix room first if not.
* [ ] The bug is still present on the main branch.
+7
View File
@@ -1,3 +1,10 @@
# unreleased
* Added support for bridging message reactions from Telegram when logged in as
a bot.
* Fixed `mx_room_state` table not being migrated correctly from the Python
bridge in SQLite databases.
# v26.04
* Rewrote bridge in Go using bridgev2 architecture.
+3
View File
@@ -262,6 +262,9 @@ CREATE TABLE new_mx_room_state (
INSERT INTO new_mx_room_state (room_id, encryption, power_levels, create_event, members_fetched)
SELECT room_id, encryption, power_levels, create_event, COALESCE(has_full_member_list, false)
FROM mx_room_state;
DROP TABLE mx_room_state;
ALTER TABLE new_mx_room_state RENAME TO mx_room_state;
-- end only sqlite
ALTER TABLE mx_user_profile ADD COLUMN name_skeleton bytea;
+2 -2
View File
@@ -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.20260430092340-8772e7714ea5
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.0
maunium.net/go/mautrix v0.27.1-0.20260430124810-125ac2c48014
rsc.io/qr v0.2.0
)
+4 -4
View File
@@ -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.20260430092340-8772e7714ea5 h1:cNm4gkt7j907g1Q4XvyNKW8tTM8BaU91Kbfa5GGyiCs=
go.mau.fi/util v0.9.9-0.20260430092340-8772e7714ea5/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.0 h1:yfEYwoIluVWkofUgbZl9gP4i5nQTF+QNsxtb+r5bKlM=
maunium.net/go/mautrix v0.27.0/go.mod h1:7QpEQiTy6p4LHkXXaZI+N46tGYy8HMhD0JjzZAFoFWs=
maunium.net/go/mautrix v0.27.1-0.20260430124810-125ac2c48014 h1:KwXGBWwUHYJKVTYWgbZEFcaM6uYLMvfjzHJg/TLwvKc=
maunium.net/go/mautrix v0.27.1-0.20260430124810-125ac2c48014/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=
+4 -3
View File
@@ -36,6 +36,7 @@ func (tc *TelegramConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilit
return &bridgev2.NetworkGeneralCapabilities{
DisappearingMessages: true,
Provisioning: bridgev2.ProvisioningCapabilities{
ImagePackImport: true,
ResolveIdentifier: bridgev2.ResolveIdentifierCapabilities{
CreateDM: true,
LookupPhone: true,
@@ -145,9 +146,9 @@ var fileCaps = event.FileFeatureMap{
// These are converted to webp
"image/jpeg": event.CapLevelPartialSupport,
"image/png": event.CapLevelPartialSupport,
// TODO
//"video/lottie+json": event.CapLevelFullySupported,
//"video/webm": event.CapLevelFullySupported,
// These will only go through if they're from an imported Telegram pack
"video/lottie+json": event.CapLevelPartialSupport,
"video/webm": event.CapLevelPartialSupport,
},
},
event.CapMsgVoice: {
+15 -29
View File
@@ -40,13 +40,13 @@ import (
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/bridgev2/simplevent"
"maunium.net/go/mautrix/bridgev2/status"
"maunium.net/go/mautrix/id"
"go.mau.fi/mautrix-telegram/pkg/connector/humanise"
"go.mau.fi/mautrix-telegram/pkg/connector/ids"
"go.mau.fi/mautrix-telegram/pkg/connector/matrixfmt"
"go.mau.fi/mautrix-telegram/pkg/connector/store"
"go.mau.fi/mautrix-telegram/pkg/connector/telegramfmt"
"go.mau.fi/mautrix-telegram/pkg/gotd/pool"
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram"
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram/auth"
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram/updates"
@@ -117,6 +117,10 @@ type TelegramClient struct {
prevReactionPoll map[networkid.PortalKey]time.Time
prevReactionPollLock sync.Mutex
stickerPacksByName map[string]*stickerPackCache
stickerPacksByID map[int64]*stickerPackCache
stickerPackCacheLock sync.Mutex
}
var _ bridgev2.NetworkAPI = (*TelegramClient)(nil)
@@ -171,7 +175,9 @@ func NewTelegramClient(ctx context.Context, tc *TelegramConnector, login *bridge
takeoutAccepted: exsync.NewEvent(),
prevReactionPoll: map[networkid.PortalKey]time.Time{},
prevReactionPoll: map[networkid.PortalKey]time.Time{},
stickerPacksByName: map[string]*stickerPackCache{},
stickerPacksByID: map[int64]*stickerPackCache{},
recentMessageRooms: exsync.NewRingBuffer[networkid.MessageID, networkid.PortalKey](32),
@@ -341,29 +347,9 @@ func NewTelegramClient(ctx context.Context, tc *TelegramConnector, login *bridge
},
}
client.matrixParser = &matrixfmt.HTMLParser{
Store: tc.Store,
GetGhostDetails: func(ctx context.Context, portal *bridgev2.Portal, ui id.UserID) (networkid.UserID, string, int64, bool) {
userID, ok := tc.Bridge.Matrix.ParseGhostMXID(ui)
if !ok {
user, err := tc.Bridge.GetExistingUserByMXID(ctx, ui)
if err != nil || user == nil {
return "", "", 0, false
} else if login, _, _ := portal.FindPreferredLogin(ctx, user, false); login != nil {
userID = ids.UserLoginIDToUserID(login.ID)
} else {
return "", "", 0, false
}
}
if peerType, telegramUserID, err := ids.ParseUserID(userID); err != nil {
return "", "", 0, false
} else if accessHash, err := client.ScopedStore.GetAccessHash(ctx, peerType, telegramUserID); err != nil || accessHash == 0 {
return "", "", 0, false
} else if username, err := client.main.Store.Username.Get(ctx, peerType, telegramUserID); err != nil {
return "", "", 0, false
} else {
return userID, username, accessHash, true
}
},
Store: tc.Store,
Bridge: tc.Bridge,
ScopedStore: client.ScopedStore,
}
return &client, err
@@ -418,12 +404,12 @@ func (tc *TelegramClient) onPing() {
me, err := tc.client.Self(ctx)
if auth.IsUnauthorized(err) {
tc.onAuthError(err)
} else if errors.Is(err, syscall.EPIPE) {
// This is a pipe error, try disconnecting which will force the
// updatesManager to fail and cause the client to reconnect.
} else if errors.Is(err, syscall.EPIPE) || errors.Is(err, pool.ErrConnDead) {
// Connectivity error — connection died during the Self() call.
// Keep as transient; gotd's backoff will reconnect.
tc.userLogin.BridgeState.Send(status.BridgeState{
StateEvent: status.StateTransientDisconnect,
Error: "pipe-error",
Error: "connectivity-error",
Message: humanise.Error(err),
})
} else if err != nil {
+1 -1
View File
@@ -222,7 +222,7 @@ var cmdEmojiPack = &commands.FullHandler{
Name: "emoji-pack",
Aliases: []string{"pack", "sticker-pack", "emojipack", "stickerpack"},
Help: commands.HelpMeta{
Section: commands.HelpSectionChats,
Section: commands.HelpSectionMisc,
Description: "Bridge emoji packs between Matrix and Telegram.",
Args: "<upload/download/list/help> [args...]",
},
+36 -13
View File
@@ -54,9 +54,11 @@ import (
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
"go.mau.fi/mautrix-telegram/pkg/connector/media"
"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 +244,17 @@ 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) 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 {
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
}
}
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") {
@@ -267,10 +276,17 @@ func (tc *TelegramClient) transferMediaToTelegram(ctx context.Context, content *
} else if sticker && (info.MimeType != "video/webm" && info.MimeType != "application/x-tgsticker") {
uploadFilename, err = ffmpeg.ConvertPath(ctx, uploadFilename, ".webp", []string{}, []string{}, false)
if err != nil {
return fmt.Errorf("failed to convert sticker to webm: %+w", err)
return fmt.Errorf("failed to convert sticker to webm: %w", err)
}
defer os.Remove(uploadFilename)
info.MimeType = "image/webp"
} else if sticker && info.MimeType == "video/lottie+json" {
uploadFilename, err = media.CompressGZip(f)
if err != nil {
return fmt.Errorf("failed to compress lottie sticker: %w", err)
}
defer os.Remove(uploadFilename)
info.MimeType = "application/x-tgsticker"
} else if cfg, _, err := image.DecodeConfig(f); err != nil {
forceDocument = true
} else if fileInfo, err := f.Stat(); err != nil {
@@ -458,19 +474,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 +508,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 +673,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
}
@@ -746,7 +769,7 @@ func (tc *TelegramClient) PreHandleMatrixReaction(ctx context.Context, msg *brid
keyNoVariation := variationselector.Remove(msg.Content.RelatesTo.Key)
emojiID := ids.MakeEmojiIDFromEmoticon(msg.Content.RelatesTo.Key)
if strings.HasPrefix(msg.Content.RelatesTo.Key, "mxc://") {
if strings.Contains(msg.Content.RelatesTo.Key, "://") {
if file, err := tc.main.Store.TelegramFile.GetByMXC(ctx, id.ContentURIString(msg.Content.RelatesTo.Key)); err != nil {
return resp, err
} else if file == nil {
+74
View File
@@ -923,6 +923,8 @@ func (tc *TelegramClient) onUpdate(ctx context.Context, e tg.Entities, upd tg.Up
return tc.onMessageEdit(ctx, update)
case *tg.UpdateMessageReactions:
return tc.onMessageReactions(ctx, update)
case *tg.UpdateBotMessageReaction:
return tc.onBotMessageReaction(ctx, update)
case *tg.UpdateUserTyping:
return tc.handleTyping(tc.makePortalKeyFromID(ids.PeerTypeUser, update.UserID, 0), tc.senderForUserID(update.UserID), update.Action)
case *tg.UpdateChatUserTyping:
@@ -964,6 +966,78 @@ func (tc *TelegramClient) onMessageReactions(ctx context.Context, update *tg.Upd
return tc.handleTelegramReactions(ctx, update.Peer, update.TopMsgID, update.MsgID, update.Reactions, "updateMessageReactions")
}
func (tc *TelegramClient) onBotMessageReaction(ctx context.Context, update *tg.UpdateBotMessageReaction) error {
wrappedMessageID := ids.MakeMessageID(update.Peer, update.MsgID)
var portalKey networkid.PortalKey
var ok bool
if portalKey, ok = tc.recentMessageRooms.Get(wrappedMessageID); ok {
// key found in cache
} else if parts, err := tc.main.Bridge.DB.Message.GetAllPartsByID(ctx, tc.loginID, wrappedMessageID); err != nil {
return err
} else if len(parts) > 0 {
portalKey = parts[0].Room
} else {
// This won't work for topics, but hopefully the cases above will cover most messages
portalKey = tc.makePortalKeyFromPeer(update.Peer, 0)
}
var eventSender bridgev2.EventSender
switch update.Actor.(type) {
case *tg.PeerUser, *tg.PeerChannel:
eventSender = tc.getPeerSender(update.Actor)
default:
zerolog.Ctx(ctx).Warn().
Type("actor_type", update.Actor).
Msg("Unexpected actor type in bot message reaction")
return nil
}
var customEmojiIDs []int64
for _, reaction := range update.NewReactions {
if e, ok := reaction.(*tg.ReactionCustomEmoji); ok {
customEmojiIDs = append(customEmojiIDs, e.DocumentID)
}
}
customEmojis, err := tc.transferEmojisToMatrix(ctx, customEmojiIDs)
if err != nil {
return fmt.Errorf("failed to transfer custom emojis for bot message reaction: %w", err)
}
reactions := make([]*bridgev2.BackfillReaction, 0, len(update.NewReactions))
for _, reaction := range update.NewReactions {
emojiID, emoji, err := computeEmojiAndID(reaction, customEmojis)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to compute emoji and ID for reaction")
continue
}
reactions = append(reactions, &bridgev2.BackfillReaction{
Timestamp: time.Unix(int64(update.Date), 0),
Sender: eventSender,
EmojiID: emojiID,
Emoji: emoji,
})
}
return resultToError(tc.main.Bridge.QueueRemoteEvent(tc.userLogin, &simplevent.ReactionSync{
EventMeta: simplevent.EventMeta{
Type: bridgev2.RemoteEventReactionSync,
LogContext: func(c zerolog.Context) zerolog.Context {
return c.
Int("message_id", update.MsgID).
Any("peer_id", update.Peer).
Str("sync_source", "updateBotMessageReaction")
},
PortalKey: portalKey,
},
TargetMessage: wrappedMessageID,
Reactions: &bridgev2.ReactionSyncData{
Users: map[networkid.UserID]*bridgev2.ReactionSyncUser{
eventSender.Sender: {
Reactions: reactions,
HasAllReactions: true,
},
},
},
}))
}
func (tc *TelegramClient) onMessageEdit(ctx context.Context, update IGetMessage) error {
msg, ok := update.GetMessage().(*tg.Message)
if !ok {
+280 -80
View File
@@ -31,6 +31,8 @@ import (
"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"
@@ -448,89 +450,20 @@ func (tc *TelegramClient) fnDownloadEmojiPack(ce *commands.Event) {
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 := tc.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(tc.client.API()).
WithStickerConfig(tc.main.Config.AnimatedSticker).
WithForceWebmStickerConvert(set.Set.Emojis).
WithDocument(rawDoc, false).
Transfer(ce.Ctx, tc.main.Store, tc.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,
}
pack, err := tc.DownloadImagePack(ce.Ctx, ce.Args[0])
if err != nil {
ce.Reply("Failed to import pack: %v", err)
return
}
_, err = tc.main.Bridge.Bot.SendState(ce.Ctx, spaceRoom, event.StateUnstableImagePack, set.Set.ShortName, &event.Content{Parsed: pack}, time.Now())
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 {
@@ -540,3 +473,270 @@ func (tc *TelegramClient) fnDownloadEmojiPack(ce *commands.Event) {
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
}
+58 -8
View File
@@ -24,6 +24,7 @@ import (
"strconv"
"strings"
"github.com/rs/zerolog"
"golang.org/x/net/html"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/networkid"
@@ -31,6 +32,7 @@ import (
"maunium.net/go/mautrix/id"
"go.mau.fi/mautrix-telegram/pkg/connector/emojis"
"go.mau.fi/mautrix-telegram/pkg/connector/ids"
"go.mau.fi/mautrix-telegram/pkg/connector/store"
"go.mau.fi/mautrix-telegram/pkg/connector/telegramfmt"
)
@@ -108,6 +110,10 @@ func (es *EntityString) TrimSpace() *EntityString {
}
break
}
if cutStart == len(es.String) {
DebugLog(" -> ALLSPACE\n")
return &EntityString{}
}
for cutEnd = len(es.String) - 1; cutEnd >= 0; cutEnd-- {
switch es.String[cutEnd] {
case '\t', '\n', '\v', '\f', '\r', ' ', 0x85, 0xA0:
@@ -254,8 +260,9 @@ func (ctx Context) WithIncrementedListDepth() Context {
// HTMLParser is a somewhat customizable Matrix HTML parser.
type HTMLParser struct {
GetGhostDetails func(context.Context, *bridgev2.Portal, id.UserID) (networkid.UserID, string, int64, bool)
Store *store.Container
Bridge *bridgev2.Bridge
Store *store.Container
ScopedStore *store.ScopedStore
}
// TaggedString is a string that also contains a HTML tag.
@@ -369,13 +376,38 @@ func (parser *HTMLParser) headerToString(node *html.Node, ctx Context) *EntitySt
return NewEntityString(prefix).Append(parser.nodeToString(node.FirstChild, ctx)).Format(telegramfmt.Style{Type: telegramfmt.StyleBold})
}
func (parser *HTMLParser) getGhostDetails(ctx context.Context, portal *bridgev2.Portal, ui id.UserID) (networkid.UserID, string, int64, bool) {
userID, ok := parser.Bridge.Matrix.ParseGhostMXID(ui)
if !ok {
user, err := parser.Bridge.GetExistingUserByMXID(ctx, ui)
if err != nil || user == nil {
return "", "", 0, false
} else if login, _, _ := portal.FindPreferredLogin(ctx, user, false); login != nil {
userID = ids.UserLoginIDToUserID(login.ID)
} else {
return "", "", 0, false
}
}
if peerType, telegramUserID, err := ids.ParseUserID(userID); err != nil {
return "", "", 0, false
} else if accessHash, err := parser.ScopedStore.GetAccessHash(ctx, peerType, telegramUserID); err != nil || accessHash == 0 {
return "", "", 0, false
} else if username, err := parser.Store.Username.Get(ctx, peerType, telegramUserID); err != nil {
return "", "", 0, false
} else {
return userID, username, accessHash, true
}
}
func (parser *HTMLParser) linkToString(node *html.Node, ctx Context) *EntityString {
str := parser.nodeToTagAwareString(node.FirstChild, ctx)
href := parser.getAttribute(node, "href")
if len(href) == 0 {
return str
}
ent := NewEntityString(str.String.String())
linkText := str.String.String()
linkTextEnt := NewEntityString(linkText)
isRawLink := linkText == href
parsedMatrix, err := id.ParseMatrixURIOrMatrixToURL(href)
if err == nil && parsedMatrix != nil && parsedMatrix.Sigil1 == '@' {
@@ -384,19 +416,37 @@ func (parser *HTMLParser) linkToString(node *html.Node, ctx Context) *EntityStri
// Mention not allowed, use name as-is
return str
}
userID, username, accessHash, ok := parser.GetGhostDetails(ctx.Ctx, ctx.Portal, mxid)
userID, username, accessHash, ok := parser.getGhostDetails(ctx.Ctx, ctx.Portal, mxid)
if !ok {
return str
} else if username == "" {
return ent.Format(telegramfmt.Mention{UserID: userID, AccessHash: accessHash})
return linkTextEnt.Format(telegramfmt.Mention{UserID: userID, AccessHash: accessHash})
} else {
return NewEntityString("@" + username).Format(telegramfmt.Mention{UserID: userID, Username: username})
}
}
if str.String.String() == href {
return ent.Format(telegramfmt.Style{Type: telegramfmt.StyleURL, URL: href})
if parsedMatrix != nil && parsedMatrix.Sigil1 == '!' && parsedMatrix.Sigil2 == '$' {
msg, err := parser.Bridge.DB.Message.GetPartByMXID(ctx.Ctx, parsedMatrix.EventID())
if err != nil {
zerolog.Ctx(ctx.Ctx).Err(err).Msg("Failed to get message for event ID in link")
} else if msg != nil {
_, chatID, topicID, _ := ids.ParsePortalID(msg.Room.ID)
_, msgID, _ := ids.ParseMessageID(msg.ID)
if msgID != 0 && chatID != 0 {
href = fmt.Sprintf("https://t.me/c/%d/%d", chatID, msgID)
if topicID > 0 {
href = fmt.Sprintf("https://t.me/c/%d/%d/%d", chatID, topicID, msgID)
}
if isRawLink {
linkTextEnt = NewEntityString(href)
}
}
}
}
if isRawLink {
return linkTextEnt.Format(telegramfmt.Style{Type: telegramfmt.StyleURL, URL: href})
} else {
return ent.Format(telegramfmt.Style{Type: telegramfmt.StyleTextURL, URL: href})
return linkTextEnt.Format(telegramfmt.Style{Type: telegramfmt.StyleTextURL, URL: href})
}
}
+24
View File
@@ -110,6 +110,30 @@ func (c *AnimatedStickerConfig) convertWebm(ctx context.Context, src *os.File) *
}
}
func CompressGZip(src *os.File) (replPath string, err error) {
tempFile, err := os.CreateTemp("", "telegram-sticker-gzip-*.tgs")
if err != nil {
return "", fmt.Errorf("failed to create temp file: %w", err)
}
writer := gzip.NewWriter(tempFile)
defer func() {
_ = tempFile.Close()
_ = writer.Close()
if replPath == "" {
_ = os.Remove(tempFile.Name())
}
}()
_, err = io.Copy(writer, src)
if err != nil {
return "", fmt.Errorf("failed to compress lottie gzip: %w", err)
}
err = writer.Close()
if err != nil {
return "", fmt.Errorf("failed to close gzip writer: %w", err)
}
return tempFile.Name(), nil
}
func extractGZip(src *os.File) (*ConvertedSticker, error) {
reader, err := gzip.NewReader(src)
if err != nil {
+10
View File
@@ -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
@@ -197,6 +202,11 @@ func (t *Transferer) WithVideo(attr *tg.DocumentAttributeVideo) *Transferer {
return t
}
func (t *Transferer) WithAudio(attr *tg.DocumentAttributeAudio) *Transferer {
t.fileInfo.Duration = attr.Duration * 1000
return t
}
func (t *Transferer) WithImageSize(attr *tg.DocumentAttributeImageSize) *Transferer {
t.fileInfo.Width, t.fileInfo.Height = attr.W, attr.H
t.adjustStickerSize()
@@ -0,0 +1,40 @@
// 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 upgrades
import (
"context"
"go.mau.fi/util/dbutil"
)
func init() {
Table.Register(-1, 9, 2, "Fix bug in legacy migration", dbutil.TxnModeOn, func(ctx context.Context, db *dbutil.Database) error {
if db.Dialect != dbutil.SQLite {
return nil
}
exists, err := db.TableExists(ctx, "new_mx_room_state")
if !exists || err != nil {
return err
}
_, err = db.Exec(ctx, `
DROP TABLE mx_room_state;
ALTER TABLE new_mx_room_state RENAME TO mx_room_state;
`)
return err
})
}
+36 -26
View File
@@ -257,6 +257,12 @@ func (tc *TelegramClient) convertToMatrix(
log.Warn().Type("reply_to", replyTo).Msg("unhandled reply to type")
}
}
if cm.Parts[0].Extra == nil {
cm.Parts[0].Extra = make(map[string]any)
}
if externalURL := getMessageLink(msg); externalURL != "" {
cm.Parts[0].Extra["external_url"] = externalURL
}
if len(cm.Parts) > 1 {
log.Warn().Int("part_count", len(cm.Parts)).Msg("Message has multiple parts")
for i, part := range cm.Parts[1:] {
@@ -274,6 +280,23 @@ func (tc *TelegramClient) convertToMatrix(
return
}
func getMessageLink(msg *tg.Message) string {
var chatID int64
switch peer := msg.PeerID.(type) {
case *tg.PeerChat:
chatID = peer.ChatID
case *tg.PeerChannel:
chatID = peer.ChannelID
default: // also PeerUser
return ""
}
topicID := rawGetTopicID(msg.ReplyTo)
if topicID > 0 {
return fmt.Sprintf("https://t.me/c/%d/%d/%d", chatID, topicID, msg.ID)
}
return fmt.Sprintf("https://t.me/c/%d/%d", chatID, msg.ID)
}
func (tc *TelegramClient) addForwardHeader(ctx context.Context, part *bridgev2.ConvertedMessagePart, fwd tg.MessageFwdHeader) error {
var fwdFromText, fwdFromHTML string
switch from := fwd.FromID.(type) {
@@ -584,11 +607,10 @@ func (tc *TelegramClient) convertMediaRequiringUpload(
if a.RoundMessage {
extraInfo["fi.mau.telegram.round_message"] = a.RoundMessage
}
extraInfo["duration"] = int(a.Duration * 1000)
case *tg.DocumentAttributeAudio:
if content.MsgType != event.MsgVideo {
content.MsgType = event.MsgAudio
extraInfo["duration"] = int(a.Duration * 1000) // only set the duration is not already set by the video handling logic
transferer = transferer.WithAudio(a) // only set the duration is not already set by the video handling logic
}
content.MSC1767Audio = &event.MSC1767Audio{
Duration: a.Duration * 1000,
@@ -606,7 +628,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"
@@ -619,20 +641,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),
}
} 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
@@ -662,14 +673,6 @@ func (tc *TelegramClient) convertMediaRequiringUpload(
}
}
if isVideoGif {
extraInfo["fi.mau.gif"] = true
extraInfo["fi.mau.loop"] = true
extraInfo["fi.mau.autoplay"] = true
extraInfo["fi.mau.hide_controls"] = true
extraInfo["fi.mau.no_audio"] = true
}
if _, ok := document.GetThumbs(); ok && eventType != event.EventSticker {
var thumbnailURL id.ContentURIString
var thumbnailFile *event.EncryptedFileInfo
@@ -751,6 +754,13 @@ func (tc *TelegramClient) convertMediaRequiringUpload(
content.FileName = content.FileName + exmime.ExtensionFromMimetype(content.Info.MimeType)
}
}
if isVideoGif {
content.Info.MauGIF = true
extraInfo["fi.mau.loop"] = true
extraInfo["fi.mau.autoplay"] = true
extraInfo["fi.mau.hide_controls"] = true
extraInfo["fi.mau.no_audio"] = true
}
// Handle spoilers
// See: https://github.com/matrix-org/matrix-spec-proposals/pull/3725
@@ -762,7 +772,7 @@ func (tc *TelegramClient) convertMediaRequiringUpload(
extraInfo["fi.mau.telegram.spoiler"] = true
}
if len(extraInfo) > 0 {
extra["info"] = extraInfo
content.Info.Extra = extraInfo
}
converted = &bridgev2.ConvertedMessagePart{