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
+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: