// mautrix-signal - A Matrix-Signal puppeting bridge. // Copyright (C) 2023 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 . package signalfmt import ( "context" "html" "slices" "strings" "github.com/google/uuid" "github.com/rs/zerolog" "golang.org/x/exp/maps" "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf" ) type UserInfo struct { MXID id.UserID Name string } type FormatParams struct { GetUserInfo func(ctx context.Context, uuid uuid.UUID) UserInfo } 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, ranges []*signalpb.BodyRange, params *FormatParams) *event.MessageEventContent { content := &event.MessageEventContent{ MsgType: event.MsgText, Body: message, Mentions: &event.Mentions{}, } if len(ranges) == 0 { return content } // LinkedRangeTree.Add depends on the ranges being sorted by increasing start index and then decreasing length. slices.SortFunc(ranges, func(a, b *signalpb.BodyRange) int { if *a.Start == *b.Start { if *a.Length == *b.Length { return 0 } else if *a.Length < *b.Length { return 1 } else { return -1 } } else if *a.Start < *b.Start { return -1 } else { return 1 } }) lrt := &LinkedRangeTree{} mentions := map[id.UserID]struct{}{} utf16Message := NewUTF16String(message) maxLength := len(utf16Message) for _, r := range ranges { br := BodyRange{ Start: int(*r.Start), Length: int(*r.Length), }.TruncateEnd(maxLength) var mentionACI uuid.UUID switch rv := r.GetAssociatedValue().(type) { case *signalpb.BodyRange_Style_: br.Value = Style(rv.Style) case *signalpb.BodyRange_MentionAci: var err error mentionACI, err = uuid.Parse(rv.MentionAci) if err != nil { continue } case *signalpb.BodyRange_MentionAciBinary: if len(rv.MentionAciBinary) != 16 { continue } mentionACI = uuid.UUID(rv.MentionAciBinary) default: zerolog.Ctx(ctx).Warn().Type("value_type", rv).Msg("Unsupported body range type") continue } if mentionACI != uuid.Nil { userInfo := params.GetUserInfo(ctx, mentionACI) if userInfo.MXID == "" { continue } mentions[userInfo.MXID] = struct{}{} // This could replace the wrong thing if there's a mention without fffc. // Maybe use NewUTF16String and do index replacements for the plaintext body too, // or just replace the plaintext body by parsing the generated HTML. content.Body = strings.Replace(content.Body, "\uFFFC", userInfo.Name, 1) br.Value = Mention{UserInfo: userInfo, UUID: mentionACI} } lrt.Add(br) } content.Mentions.UserIDs = maps.Keys(mentions) content.FormattedBody = lrt.Format(utf16Message, formatContext{}) content.Format = event.FormatHTML //content.Body = format.HTMLToText(content.FormattedBody) return content }