(telegram|matrix)fmt: mention formatting

Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
This commit is contained in:
Sumner Evans
2024-08-05 14:40:55 -06:00
parent b539e5d63d
commit f56f520308
14 changed files with 123 additions and 59 deletions
+43 -4
View File
@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"regexp"
"slices"
"strconv"
"strings"
"sync"
@@ -18,8 +19,10 @@ import (
"maunium.net/go/mautrix/bridge/status"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/id"
"go.mau.fi/mautrix-telegram/pkg/connector/ids"
"go.mau.fi/mautrix-telegram/pkg/connector/matrixfmt"
"go.mau.fi/mautrix-telegram/pkg/connector/media"
"go.mau.fi/mautrix-telegram/pkg/connector/store"
"go.mau.fi/mautrix-telegram/pkg/connector/telegramfmt"
@@ -42,6 +45,7 @@ type TelegramClient struct {
appConfigHash int
telegramFmtParams *telegramfmt.FormatParams
matrixParser *matrixfmt.HTMLParser
}
var (
@@ -150,13 +154,31 @@ func NewTelegramClient(ctx context.Context, tc *TelegramConnector, login *bridge
client.reactionMessageLocks = map[int]*sync.Mutex{}
client.telegramFmtParams = &telegramfmt.FormatParams{
GetUserInfo: func(ctx context.Context, id networkid.UserID) (telegramfmt.UserInfo, error) {
ghost, err := tc.Bridge.GetGhostByID(ctx, id)
GetUserInfoByID: func(ctx context.Context, id int64) (telegramfmt.UserInfo, error) {
ghost, err := tc.Bridge.GetGhostByID(ctx, ids.MakeUserID(id))
if err != nil {
return telegramfmt.UserInfo{}, err
}
userInfo := telegramfmt.UserInfo{MXID: ghost.Intent.GetMXID(), Name: ghost.Name}
if id == client.userID {
if id == client.telegramUserID {
userInfo.MXID = client.userLogin.UserMXID
}
return userInfo, nil
},
GetUserInfoByUsername: func(ctx context.Context, username string) (telegramfmt.UserInfo, error) {
ghosts, err := tc.Bridge.DB.Ghost.GetByMetadata(ctx, "username", username)
if err != nil {
return telegramfmt.UserInfo{}, err
}
if len(ghosts) != 1 {
return telegramfmt.UserInfo{}, fmt.Errorf("username %s not found", username)
}
ghost, err := tc.Bridge.GetGhostByID(ctx, ghosts[0].ID)
if err != nil {
return telegramfmt.UserInfo{}, err
}
userInfo := telegramfmt.UserInfo{MXID: ghost.Intent.GetMXID(), Name: ghost.Name}
if ghosts[0].ID == client.userID {
userInfo.MXID = client.userLogin.UserMXID
}
return userInfo, nil
@@ -204,6 +226,17 @@ func NewTelegramClient(ctx context.Context, tc *TelegramConnector, login *bridge
return fmt.Sprintf("https://matrix.to/#/%s/%s", portal.MXID, message.MXID)
},
}
client.matrixParser = &matrixfmt.HTMLParser{
GetGhostDetails: func(ctx context.Context, ui id.UserID) (networkid.UserID, string, int64, bool) {
if userID, ok := tc.Bridge.Matrix.ParseGhostMXID(ui); !ok {
return "", "", 0, false
} else if ghost, err := tc.Bridge.GetGhostByID(ctx, userID); err != nil {
return "", "", 0, false
} else {
return userID, ghost.Metadata.(*GhostMetadata).Username, ghost.Metadata.(*GhostMetadata).AccessHash, true
}
},
}
go func() {
err = updatesManager.Run(ctx, client.client.API(), telegramUserID, updates.AuthOptions{})
@@ -302,6 +335,9 @@ func (t *TelegramClient) getUserInfoFromTelegramUser(u tg.UserClass) (*bridgev2.
}
var identifiers []string
if !user.Min {
if username, ok := user.GetUsername(); ok {
identifiers = append(identifiers, fmt.Sprintf("telegram:%s", username))
}
for _, username := range user.Usernames {
identifiers = append(identifiers, fmt.Sprintf("telegram:%s", username.Username))
}
@@ -309,6 +345,8 @@ func (t *TelegramClient) getUserInfoFromTelegramUser(u tg.UserClass) (*bridgev2.
identifiers = append(identifiers, fmt.Sprintf("tel:+%s", strings.TrimPrefix(phone, "+")))
}
}
slices.Sort(identifiers)
identifiers = slices.Compact(identifiers)
var avatar *bridgev2.Avatar
if p, ok := user.GetPhoto(); ok && p.TypeID() == tg.UserProfilePhotoTypeID {
@@ -331,9 +369,10 @@ func (t *TelegramClient) getUserInfoFromTelegramUser(u tg.UserClass) (*bridgev2.
ExtraUpdates: func(ctx context.Context, ghost *bridgev2.Ghost) (changed bool) {
meta := ghost.Metadata.(*GhostMetadata)
if !user.Min {
changed = changed || meta.IsPremium != user.Premium || meta.IsBot != user.Bot
changed = changed || meta.IsPremium != user.Premium || meta.IsBot != user.Bot || meta.Username != user.Username
meta.IsPremium = user.Premium
meta.IsBot = user.Bot
meta.Username = user.Username
}
changed = changed || meta.AccessHash != user.AccessHash
meta.AccessHash = user.AccessHash
+4 -3
View File
@@ -88,9 +88,10 @@ func (tg *TelegramConnector) GetDBMetaTypes() database.MetaTypes {
}
type GhostMetadata struct {
IsPremium bool `json:"is_premium"`
IsBot bool `json:"is_bot"`
AccessHash int64 `json:"access_hash"`
AccessHash int64 `json:"access_hash"`
IsPremium bool `json:"is_premium,omitempty"`
IsBot bool `json:"is_bot,omitempty"`
Username string `json:"username,omitempty"`
}
type MessageMetadata struct {
+2 -6
View File
@@ -87,9 +87,7 @@ func (t *TelegramClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.
noWebpage := msg.Content.BeeperLinkPreviews != nil && len(msg.Content.BeeperLinkPreviews) == 0
message, entities := matrixfmt.Parse(ctx, &matrixfmt.HTMLParser{
ParseGhostMXID: t.main.Bridge.Matrix.ParseGhostMXID,
}, msg.Content)
message, entities := matrixfmt.Parse(ctx, t.matrixParser, msg.Content)
var replyTo tg.InputReplyToClass
if msg.ReplyTo != nil {
@@ -212,9 +210,7 @@ func (t *TelegramClient) HandleMatrixEdit(ctx context.Context, msg *bridgev2.Mat
return err
}
message, entities := matrixfmt.Parse(ctx, &matrixfmt.HTMLParser{
ParseGhostMXID: t.main.Bridge.Matrix.ParseGhostMXID,
}, msg.Content)
message, entities := matrixfmt.Parse(ctx, t.matrixParser, msg.Content)
var newContentURI id.ContentURIString
req := tg.MessagesEditMessageRequest{
+13 -9
View File
@@ -29,11 +29,15 @@ import (
func toTelegramEntity(br telegramfmt.BodyRange) tg.MessageEntityClass {
switch val := br.Value.(type) {
case telegramfmt.Mention:
userID, _ := ids.ParseUserID(val.UserID)
return &tg.MessageEntityMentionName{
Offset: br.Start,
Length: br.Length,
UserID: userID,
if val.Username != "" {
return &tg.MessageEntityMention{Offset: br.Start, Length: br.Length}
} else {
userID, _ := ids.ParseUserID(val.UserID)
return &tg.InputMessageEntityMentionName{
Offset: br.Start,
Length: br.Length,
UserID: &tg.InputUser{UserID: userID, AccessHash: val.AccessHash},
}
}
case telegramfmt.Style:
switch val.Type {
@@ -92,12 +96,12 @@ func Parse(ctx context.Context, parser *HTMLParser, content *event.MessageEventC
if parsed == nil {
return "", nil
}
var bodyRanges []tg.MessageEntityClass
var entities []tg.MessageEntityClass
if len(parsed.Entities) > 0 {
bodyRanges = make([]tg.MessageEntityClass, len(parsed.Entities))
entities = make([]tg.MessageEntityClass, len(parsed.Entities))
for i, ent := range parsed.Entities {
bodyRanges[i] = toTelegramEntity(ent)
entities[i] = toTelegramEntity(ent)
}
}
return parsed.String.String(), bodyRanges
return parsed.String.String(), entities
}
+6 -3
View File
@@ -243,7 +243,7 @@ func (ctx Context) WithWhitespace() Context {
// HTMLParser is a somewhat customizable Matrix HTML parser.
type HTMLParser struct {
ParseGhostMXID func(id.UserID) (networkid.UserID, bool)
GetGhostDetails func(context.Context, id.UserID) (networkid.UserID, string, int64, bool)
}
// TaggedString is a string that also contains a HTML tag.
@@ -367,11 +367,14 @@ func (parser *HTMLParser) linkToString(node *html.Node, ctx Context) *EntityStri
// Mention not allowed, use name as-is
return str
}
userID, ok := parser.ParseGhostMXID(mxid)
userID, username, accessHash, ok := parser.GetGhostDetails(ctx.Ctx, mxid)
if !ok {
return str
} else if username == "" {
return ent.Format(telegramfmt.Mention{UserID: userID, AccessHash: accessHash})
} else {
return NewEntityString("@" + username).Format(telegramfmt.Mention{UserID: userID, Username: username})
}
return ent.Format(telegramfmt.Mention{UserID: userID})
}
if str.String.String() == href {
return ent.Format(telegramfmt.Style{Type: telegramfmt.StyleURL, URL: href})
+3 -1
View File
@@ -1,4 +1,4 @@
-- v0 -> v1: Latest revision
-- v0 -> v2: Latest revision
CREATE TABLE telegram_session (
user_id INTEGER PRIMARY KEY,
@@ -37,3 +37,5 @@ CREATE TABLE telegram_file (
mime_type TEXT,
size BIGINT
);
CREATE INDEX idx_ghost_username ON ghost ((metadata->>'username'));
@@ -0,0 +1,3 @@
-- v2: Add index for ghost username metadata field
CREATE INDEX idx_ghost_username ON ghost ((metadata->>'username'));
+22 -13
View File
@@ -18,11 +18,11 @@ package telegramfmt
import (
"context"
"fmt"
"html"
"strings"
"github.com/gotd/td/tg"
"github.com/rs/zerolog"
"golang.org/x/exp/maps"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/event"
@@ -37,9 +37,10 @@ type UserInfo struct {
}
type FormatParams struct {
CustomEmojis map[networkid.EmojiID]string
GetUserInfo func(ctx context.Context, id networkid.UserID) (UserInfo, error)
NormalizeURL func(ctx context.Context, url string) string
CustomEmojis map[networkid.EmojiID]string
GetUserInfoByUsername func(ctx context.Context, username string) (UserInfo, error)
GetUserInfoByID func(ctx context.Context, id int64) (UserInfo, error)
NormalizeURL func(ctx context.Context, url string) string
}
func (fp FormatParams) GetCustomEmoji(emojiID networkid.EmojiID) (string, id.ContentURIString) {
@@ -52,9 +53,10 @@ func (fp FormatParams) GetCustomEmoji(emojiID networkid.EmojiID) (string, id.Con
func (fp FormatParams) WithCustomEmojis(emojis map[networkid.EmojiID]string) FormatParams {
return FormatParams{
CustomEmojis: emojis,
GetUserInfo: fp.GetUserInfo,
NormalizeURL: fp.NormalizeURL,
CustomEmojis: emojis,
GetUserInfoByUsername: fp.GetUserInfoByUsername,
GetUserInfoByID: fp.GetUserInfoByID,
NormalizeURL: fp.NormalizeURL,
}
}
@@ -70,6 +72,7 @@ func (ctx formatContext) TextToHTML(text string) string {
}
func Parse(ctx context.Context, message string, entities []tg.MessageEntityClass, params FormatParams) (*event.MessageEventContent, error) {
log := zerolog.Ctx(ctx).With().Str("func", "Parse").Logger()
content := &event.MessageEventContent{
MsgType: event.MsgText,
Body: message,
@@ -90,8 +93,14 @@ func Parse(ctx context.Context, message string, entities []tg.MessageEntityClass
}.TruncateEnd(maxLength)
switch entity := e.(type) {
case *tg.MessageEntityMention:
// TODO
fmt.Printf("mention = %+v\n", entity)
username := utf16Message[e.GetOffset()+1 : e.GetOffset()+e.GetLength()].String()
userInfo, err := params.GetUserInfoByUsername(ctx, username)
if err != nil {
log.Warn().Err(err).Str("username", username).Msg("Failed to get user info for mention")
continue // Skip this mention
}
mentions[userInfo.MXID] = struct{}{}
br.Value = Mention{UserInfo: userInfo, Username: username}
case *tg.MessageEntityHashtag:
br.Value = Style{Type: StyleHashtag}
case *tg.MessageEntityBotCommand:
@@ -111,13 +120,13 @@ func Parse(ctx context.Context, message string, entities []tg.MessageEntityClass
case *tg.MessageEntityTextURL:
br.Value = Style{Type: StyleURL, URL: params.NormalizeURL(ctx, entity.URL)}
case *tg.MessageEntityMentionName:
userID := ids.MakeUserID(entity.UserID)
userInfo, err := params.GetUserInfo(ctx, userID)
userInfo, err := params.GetUserInfoByID(ctx, entity.UserID)
if err != nil {
return nil, err
log.Warn().Err(err).Int64("user_id", entity.UserID).Msg("Failed to get user info for mention")
continue // Skip this mention
}
mentions[userInfo.MXID] = struct{}{}
br.Value = Mention{UserInfo: userInfo, UserID: userID}
br.Value = Mention{UserInfo: userInfo}
case *tg.MessageEntityPhone:
br.Value = Style{Type: StylePhone}
case *tg.MessageEntityCashtag:
+3 -4
View File
@@ -24,7 +24,6 @@ import (
"github.com/gotd/td/tg"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
@@ -33,15 +32,15 @@ import (
func TestParse(t *testing.T) {
formatParams := telegramfmt.FormatParams{
GetUserInfo: func(ctx context.Context, userID networkid.UserID) (telegramfmt.UserInfo, error) {
if userID == "real" {
GetUserInfoByID: func(ctx context.Context, userID int64) (telegramfmt.UserInfo, error) {
if userID == 1 {
return telegramfmt.UserInfo{
MXID: "@test:example.com",
Name: "Matrix User",
}, nil
} else {
return telegramfmt.UserInfo{
MXID: id.UserID(fmt.Sprintf("@telegram_%s:example.com", userID)),
MXID: id.UserID(fmt.Sprintf("@telegram_%d:example.com", userID)),
Name: "Signal User",
}, nil
}
+5 -1
View File
@@ -23,7 +23,11 @@ import (
)
func (m Mention) Format(message string) string {
return fmt.Sprintf(`<a href="%s">%s</a>`, m.MXID.URI().MatrixToURL(), m.Name)
if m.Username != "" {
return fmt.Sprintf(`<a href="%s">@%s</a>`, m.MXID.URI().MatrixToURL(), m.Username)
} else {
return fmt.Sprintf(`<a href="%s">%s</a>`, m.MXID.URI().MatrixToURL(), m.Name)
}
}
func (s Style) Format(message string) string {
+4 -2
View File
@@ -31,13 +31,15 @@ type BodyRangeValue interface {
type Mention struct {
UserInfo
UserID networkid.UserID
UserID networkid.UserID
AccessHash int64
Username string
}
var _ BodyRangeValue = Mention{}
func (m Mention) String() string {
return fmt.Sprintf("Mention{MXID: id.UserID(%q), Name: %q}", m.MXID, m.Name)
return fmt.Sprintf("Mention{MXID: id.UserID(%q), Username: %q, Name: %q}", m.MXID, m.Username, m.Name)
}
func (m Mention) IsCode() bool {
-1
View File
@@ -182,7 +182,6 @@ func (t *TelegramClient) parseBodyAndHTML(ctx context.Context, message string, e
if err != nil {
return nil, err
}
fmt.Printf("ce %+v\n", customEmojis) // TODO DEBUG
return telegramfmt.Parse(ctx, message, entities, t.telegramFmtParams.WithCustomEmojis(customEmojis))
}