Files
mautrix-telegram/pkg/connector/telegramfmt/convert.go
T
2024-08-05 15:19:53 -06:00

160 lines
5.3 KiB
Go

// mautrix-telegram - A Matrix-Telegram puppeting bridge.
// Copyright (C) 2024 Sumner Evans
//
// 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 telegramfmt
import (
"context"
"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"
"maunium.net/go/mautrix/id"
"go.mau.fi/mautrix-telegram/pkg/connector/ids"
)
type UserInfo struct {
MXID id.UserID
Name string
}
type FormatParams struct {
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) {
if strings.HasPrefix(fp.CustomEmojis[emojiID], "mxc://") {
return "", id.ContentURIString(fp.CustomEmojis[emojiID])
} else {
return fp.CustomEmojis[emojiID], ""
}
}
func (fp FormatParams) WithCustomEmojis(emojis map[networkid.EmojiID]string) FormatParams {
return FormatParams{
CustomEmojis: emojis,
GetUserInfoByUsername: fp.GetUserInfoByUsername,
GetUserInfoByID: fp.GetUserInfoByID,
NormalizeURL: fp.NormalizeURL,
}
}
type formatContext struct {
IsInCodeblock bool
}
func (ctx formatContext) TextToHTML(text string) string {
if ctx.IsInCodeblock {
return html.EscapeString(text)
}
return event.TextToHTML(text)
}
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,
Mentions: &event.Mentions{},
}
if len(entities) == 0 {
return content, nil
}
lrt := &LinkedRangeTree{}
mentions := map[id.UserID]struct{}{}
utf16Message := NewUTF16String(message)
maxLength := len(utf16Message)
for _, e := range entities {
br := BodyRange{
Start: e.GetOffset(),
Length: e.GetLength(),
}.TruncateEnd(maxLength)
switch entity := e.(type) {
case *tg.MessageEntityMention:
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:
br.Value = Style{Type: StyleBotCommand}
case *tg.MessageEntityURL:
br.Value = Style{Type: StyleURL, URL: params.NormalizeURL(ctx, utf16Message[e.GetOffset():e.GetOffset()+e.GetLength()].String())}
case *tg.MessageEntityEmail:
br.Value = Style{Type: StyleEmail}
case *tg.MessageEntityBold:
br.Value = Style{Type: StyleBold}
case *tg.MessageEntityItalic:
br.Value = Style{Type: StyleItalic}
case *tg.MessageEntityCode:
br.Value = Style{Type: StyleCode}
case *tg.MessageEntityPre:
br.Value = Style{Type: StylePre, Language: entity.Language}
case *tg.MessageEntityTextURL:
br.Value = Style{Type: StyleURL, URL: params.NormalizeURL(ctx, entity.URL)}
case *tg.MessageEntityMentionName:
userInfo, err := params.GetUserInfoByID(ctx, entity.UserID)
if err != nil {
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}
case *tg.MessageEntityPhone:
br.Value = Style{Type: StylePhone}
case *tg.MessageEntityCashtag:
br.Value = Style{Type: StyleCashtag}
case *tg.MessageEntityUnderline:
br.Value = Style{Type: StyleUnderline}
case *tg.MessageEntityStrike:
br.Value = Style{Type: StyleStrikethrough}
case *tg.MessageEntityBankCard:
br.Value = Style{Type: StyleBankCard}
case *tg.MessageEntitySpoiler:
br.Value = Style{Type: StyleSpoiler}
case *tg.MessageEntityCustomEmoji:
emoji, contentURI := params.GetCustomEmoji(ids.MakeEmojiIDFromDocumentID(entity.DocumentID))
if emoji != "" {
br.Value = Style{Type: StyleCustomEmoji, Emoji: emoji}
} else {
br.Value = Style{Type: StyleCustomEmoji, EmojiURI: contentURI}
}
case *tg.MessageEntityBlockquote:
br.Value = Style{Type: StyleBlockquote}
}
lrt.Add(br)
}
content.Mentions.UserIDs = maps.Keys(mentions)
content.FormattedBody = lrt.Format(utf16Message, formatContext{})
content.Format = event.FormatHTML
return content, nil
}