capabilities: update to new format

This commit is contained in:
Tulir Asokan
2025-01-10 21:17:10 +02:00
parent f4052dcfd3
commit 386cfa4cfb
9 changed files with 254 additions and 36 deletions
+4 -4
View File
@@ -2,7 +2,7 @@ module go.mau.fi/mautrix-telegram
go 1.22.0
toolchain go1.23.3
toolchain go1.23.4
require (
github.com/gorilla/mux v1.8.0
@@ -10,12 +10,12 @@ require (
github.com/gotd/td v0.111.0
github.com/rs/zerolog v1.33.0
github.com/stretchr/testify v1.10.0
go.mau.fi/util v0.8.4-0.20250106152331-30b8c95e7d7a
go.mau.fi/util v0.8.4-0.20250110124612-64d4dbbec957
go.mau.fi/zerozap v0.1.1
go.uber.org/zap v1.27.0
golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329
golang.org/x/net v0.33.0
maunium.net/go/mautrix v0.22.2-0.20250109192415-9748015309bd
golang.org/x/net v0.34.0
maunium.net/go/mautrix v0.22.2-0.20250110154103-bbcb1904e268
)
require (
+6 -6
View File
@@ -76,8 +76,8 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
go.mau.fi/util v0.8.4-0.20250106152331-30b8c95e7d7a h1:D9RCHBFjxah9F/YB7amvRJjT2IEOFWcz8jpcEY8dBV0=
go.mau.fi/util v0.8.4-0.20250106152331-30b8c95e7d7a/go.mod h1:MOfGTs1CBuK6ERTcSL4lb5YU7/ujz09eOPVEDckuazY=
go.mau.fi/util v0.8.4-0.20250110124612-64d4dbbec957 h1:tsLt3t6ARc55niz+JMgJy6U4sL210Z0K/nyxF09xT0E=
go.mau.fi/util v0.8.4-0.20250110124612-64d4dbbec957/go.mod h1:MOfGTs1CBuK6ERTcSL4lb5YU7/ujz09eOPVEDckuazY=
go.mau.fi/zeroconfig v0.1.3 h1:As9wYDKmktjmNZW5i1vn8zvJlmGKHeVxHVIBMXsm4kM=
go.mau.fi/zeroconfig v0.1.3/go.mod h1:NcSJkf180JT+1IId76PcMuLTNa1CzsFFZ0nBygIQM70=
go.mau.fi/zerozap v0.1.1 h1:mxE/dW4wtkqBYOXOEEzXldk5qKB+ahsZXjoTGnvEhZQ=
@@ -98,8 +98,8 @@ golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 h1:9kj3STMvgqy3YA4VQXBrN7925ICMxD5wzMRcgA30588=
golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
@@ -119,8 +119,8 @@ 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.22.2-0.20250109192415-9748015309bd h1:v83tY7Tp8vF1/dOH8nP762eDwm5cAuijnj3sjrjYzvc=
maunium.net/go/mautrix v0.22.2-0.20250109192415-9748015309bd/go.mod h1:FmwzK7RSzrd1OfGDgJzFWXl7nYmYm8/P0Y77sy/A1Uw=
maunium.net/go/mautrix v0.22.2-0.20250110154103-bbcb1904e268 h1:p+3TofdhqiVYIkLjgzidayg2XriGUEbj+nbWs3/UQbk=
maunium.net/go/mautrix v0.22.2-0.20250110154103-bbcb1904e268/go.mod h1:07i96D7BALyuAqxFhRzvaId8FC9NABgRQBPY5HWndf4=
nhooyr.io/websocket v1.8.17 h1:KEVeLJkUywCKVsnLIDlD/5gtayKp8VoCkksHCGGfT9Y=
nhooyr.io/websocket v1.8.17/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
+187
View File
@@ -0,0 +1,187 @@
// mautrix-telegram - A Matrix-Telegram puppeting bridge.
// Copyright (C) 2025 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 (
"context"
"crypto/sha256"
"encoding/hex"
"go.mau.fi/util/ptr"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/event"
)
func (tg *TelegramConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilities {
return &bridgev2.NetworkGeneralCapabilities{
DisappearingMessages: true,
}
}
func (tg *TelegramConnector) GetBridgeInfoVersion() (info, capabilities int) {
return 1, 1
}
// TODO get these from getConfig instead of hardcoding?
const MaxTextLength = 4096
const MaxCaptionLength = 1024
const MaxFileSize = 2 * 1024 * 1024 * 1024
var formattingCaps = event.FormattingFeatureMap{
event.FmtBold: event.CapLevelFullySupported,
event.FmtItalic: event.CapLevelFullySupported,
event.FmtUnderline: event.CapLevelFullySupported,
event.FmtStrikethrough: event.CapLevelFullySupported,
event.FmtInlineCode: event.CapLevelFullySupported,
event.FmtCodeBlock: event.CapLevelFullySupported,
event.FmtSyntaxHighlighting: event.CapLevelFullySupported,
event.FmtBlockquote: event.CapLevelFullySupported,
event.FmtInlineLink: event.CapLevelFullySupported,
event.FmtUserLink: event.CapLevelFullySupported,
// TODO support room links and event links (convert to appropriate t.me links)
event.FmtUnorderedList: event.CapLevelPartialSupport,
event.FmtOrderedList: event.CapLevelPartialSupport,
event.FmtListStart: event.CapLevelPartialSupport,
event.FmtListJumpValue: event.CapLevelDropped,
// TODO support custom emojis in messages
event.FmtCustomEmoji: event.CapLevelDropped,
event.FmtSpoiler: event.CapLevelFullySupported,
event.FmtSpoilerReason: event.CapLevelDropped,
event.FmtHeaders: event.CapLevelPartialSupport,
}
var fileCaps = event.FileFeatureMap{
event.MsgImage: {
MimeTypes: map[string]event.CapabilitySupportLevel{
"image/jpeg": event.CapLevelFullySupported,
"image/png": event.CapLevelPartialSupport,
},
Caption: event.CapLevelFullySupported,
MaxCaptionLength: MaxCaptionLength,
MaxSize: 10 * 1024 * 1024,
},
event.MsgVideo: {
MimeTypes: map[string]event.CapabilitySupportLevel{
"video/mp4": event.CapLevelFullySupported,
},
Caption: event.CapLevelFullySupported,
MaxCaptionLength: MaxCaptionLength,
MaxSize: MaxFileSize,
},
event.MsgAudio: {
MimeTypes: map[string]event.CapabilitySupportLevel{
"audio/mpeg": event.CapLevelFullySupported,
"audio/mp4": event.CapLevelFullySupported,
// TODO some other formats are probably supported too
},
Caption: event.CapLevelFullySupported,
MaxCaptionLength: MaxCaptionLength,
MaxSize: MaxFileSize,
},
event.MsgFile: {
MimeTypes: map[string]event.CapabilitySupportLevel{
"*/*": event.CapLevelFullySupported,
},
Caption: event.CapLevelFullySupported,
MaxCaptionLength: MaxCaptionLength,
MaxSize: MaxFileSize,
},
event.CapMsgGIF: {
MimeTypes: map[string]event.CapabilitySupportLevel{
"image/gif": event.CapLevelPartialSupport,
"video/mp4": event.CapLevelFullySupported,
},
Caption: event.CapLevelFullySupported,
MaxCaptionLength: MaxCaptionLength,
MaxSize: MaxFileSize,
},
event.CapMsgSticker: {
MimeTypes: map[string]event.CapabilitySupportLevel{
"image/webp": event.CapLevelFullySupported,
// TODO
//"image/lottie+json": event.CapLevelFullySupported,
//"video/webm": event.CapLevelFullySupported,
},
},
event.CapMsgVoice: {
MimeTypes: map[string]event.CapabilitySupportLevel{
"audio/ogg": event.CapLevelFullySupported,
"audio/mpeg": event.CapLevelFullySupported,
"audio/mp4": event.CapLevelFullySupported,
},
Caption: event.CapLevelFullySupported,
MaxCaptionLength: MaxCaptionLength,
MaxSize: MaxFileSize,
},
}
var premiumFileCaps event.FileFeatureMap
func init() {
premiumFileCaps = make(event.FileFeatureMap, len(fileCaps))
for k, v := range fileCaps {
cloned := ptr.Clone(v)
if k == event.MsgFile || k == event.MsgVideo || k == event.MsgAudio {
cloned.MaxSize *= 2
}
cloned.MaxCaptionLength *= 2
premiumFileCaps[k] = cloned
}
}
func hashEmojiList(emojis []string) string {
hasher := sha256.New()
for _, emoji := range emojis {
hasher.Write([]byte(emoji))
}
return hex.EncodeToString(hasher.Sum(nil))[:8]
}
func (t *TelegramClient) GetCapabilities(ctx context.Context, portal *bridgev2.Portal) *event.RoomFeatures {
baseID := "fi.mau.telegram.capabilities.2025_01_10"
feat := &event.RoomFeatures{
Formatting: formattingCaps,
File: fileCaps,
MaxTextLength: MaxTextLength,
LocationMessage: event.CapLevelFullySupported,
Reply: event.CapLevelFullySupported,
Edit: event.CapLevelFullySupported,
Delete: event.CapLevelFullySupported,
Reaction: event.CapLevelFullySupported,
ReadReceipts: true,
TypingNotifications: true,
}
// TODO non-admins can only edit messages within 48 hours
reactions := portal.Metadata.(*PortalMetadata).AllowedReactions
if reactions == nil {
baseID += "+reactions_any"
feat.AllowedReactions, feat.CustomEmojiReactions = t.getAvailableReactionsForCapability(ctx)
} else if len(reactions) == 0 {
baseID += "+reactions_none"
feat.Reaction = event.CapLevelRejected
} else {
baseID += "+reactions_" + hashEmojiList(reactions)
feat.AllowedReactions = reactions
}
if t.isPremiumCache.Load() {
baseID += "+premium"
feat.File = premiumFileCaps
}
feat.ID = baseID
return feat
}
+29 -1
View File
@@ -4,6 +4,7 @@ import (
"context"
"crypto/sha256"
"fmt"
"slices"
"time"
"github.com/gotd/td/tg"
@@ -124,7 +125,34 @@ func (t *TelegramClient) getGroupChatInfo(fullChat *tg.MessagesChatFull, chatID
},
CanBackfill: true,
ExtraUpdates: func(ctx context.Context, p *bridgev2.Portal) bool {
return p.Metadata.(*PortalMetadata).SetIsSuperGroup(isMegagroup)
meta := p.Metadata.(*PortalMetadata)
changed := meta.SetIsSuperGroup(isMegagroup)
if reactions, ok := fullChat.FullChat.GetAvailableReactions(); ok {
switch typedReactions := reactions.(type) {
case *tg.ChatReactionsAll:
changed = meta.AllowedReactions != nil
meta.AllowedReactions = nil
case *tg.ChatReactionsNone:
changed = meta.AllowedReactions == nil || len(meta.AllowedReactions) > 0
meta.AllowedReactions = []string{}
case *tg.ChatReactionsSome:
allowedReactions := make([]string, 0, len(typedReactions.Reactions))
for _, react := range typedReactions.Reactions {
emoji, ok := react.(*tg.ReactionEmoji)
if ok {
allowedReactions = append(allowedReactions, emoji.Emoticon)
}
}
slices.Sort(allowedReactions)
if !slices.Equal(meta.AllowedReactions, allowedReactions) {
changed = true
meta.AllowedReactions = allowedReactions
}
}
}
return changed
},
}
+3 -16
View File
@@ -9,6 +9,7 @@ import (
"strconv"
"strings"
"sync"
"sync/atomic"
"syscall"
"time"
@@ -58,6 +59,8 @@ type TelegramClient struct {
availableReactions map[string]struct{}
availableReactionsHash int
availableReactionsFetched time.Time
availableReactionsList []string
isPremiumCache atomic.Bool
telegramFmtParams *telegramfmt.FormatParams
matrixParser *matrixfmt.HTMLParser
@@ -717,22 +720,6 @@ func (t *TelegramClient) IsThisUser(ctx context.Context, userID networkid.UserID
return userID == networkid.UserID(t.userLogin.ID)
}
func (t *TelegramClient) GetCapabilities(ctx context.Context, portal *bridgev2.Portal) *bridgev2.NetworkRoomCapabilities {
return &bridgev2.NetworkRoomCapabilities{
FormattedText: true,
UserMentions: true,
RoomMentions: true, // TODO?
LocationMessages: true,
Captions: true,
Threads: false, // TODO
Replies: true,
Edits: true,
Deletes: true,
ReadReceipts: true,
Reactions: true,
}
}
func (t *TelegramClient) mySender() bridgev2.EventSender {
return bridgev2.EventSender{
IsFromMe: true,
-6
View File
@@ -68,9 +68,3 @@ func (tg *TelegramConnector) GetName() bridgev2.BridgeName {
DefaultCommandPrefix: "!tg",
}
}
func (tg *TelegramConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilities {
return &bridgev2.NetworkGeneralCapabilities{
DisappearingMessages: true,
}
}
+4
View File
@@ -106,6 +106,10 @@ func (t *TelegramClient) transferMediaToTelegram(ctx context.Context, content *e
attributes = append(attributes, &tg.DocumentAttributeImageSize{W: content.Info.Width, H: content.Info.Height})
}
if content.Info != nil && content.Info.MauGIF {
attributes = append(attributes, &tg.DocumentAttributeAnimated{})
}
if sticker {
attributes = append(attributes, &tg.DocumentAttributeSticker{
Alt: content.Body,
+4 -3
View File
@@ -29,9 +29,10 @@ type GhostMetadata struct {
}
type PortalMetadata struct {
IsSuperGroup bool `json:"is_supergroup,omitempty"`
ReadUpTo int `json:"read_up_to,omitempty"`
MessagesTTL int `json:"messages_ttl,omitempty"`
IsSuperGroup bool `json:"is_supergroup,omitempty"`
ReadUpTo int `json:"read_up_to,omitempty"`
MessagesTTL int `json:"messages_ttl,omitempty"`
AllowedReactions []string `json:"allowed_reactions"`
}
func (pm *PortalMetadata) SetIsSuperGroup(isSupergroup bool) (changed bool) {
+17
View File
@@ -898,6 +898,14 @@ func (t *TelegramClient) getAppConfigCached(ctx context.Context) (map[string]any
return t.appConfig, nil
}
func (t *TelegramClient) getAvailableReactionsForCapability(ctx context.Context) ([]string, bool) {
_, err := t.getAvailableReactions(ctx)
if err != nil {
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to get available reactions for capability listing")
}
return t.availableReactionsList, t.isPremiumCache.Load()
}
func (t *TelegramClient) getAvailableReactions(ctx context.Context) (map[string]struct{}, error) {
log := zerolog.Ctx(ctx).With().Str("handler", "get_available_reactions").Logger()
t.availableReactionsLock.Lock()
@@ -929,6 +937,15 @@ func (t *TelegramClient) getAvailableReactions(ctx context.Context) (map[string]
}
t.availableReactionsHash = availableReactions.Hash
if myGhost.Metadata.(*GhostMetadata).IsPremium {
// All reactions are allowed via the unicodemojipack feature
t.availableReactionsList = nil
t.isPremiumCache.Store(true)
} else {
t.availableReactionsList = maps.Keys(t.availableReactions)
t.isPremiumCache.Store(false)
slices.Sort(t.availableReactionsList)
}
case *tg.MessagesAvailableReactionsNotModified:
log.Debug().Msg("Available reactions not modified")
default: