// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// Copyright (C) 2024 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
[long message cut off]"
}
}
func logOversizedPart(ctx context.Context, part *bridgev2.ConvertedMessagePart) {
if part == nil || part.Content == nil {
return
}
contentJSON, _ := json.Marshal(part.Content)
extraJSON, _ := json.Marshal(part.Extra)
if len(contentJSON)+len(extraJSON) <= 40000 {
return
}
extraKeys := make([]string, 0, len(part.Extra))
for key := range part.Extra {
extraKeys = append(extraKeys, key)
}
var mentionCount int
if part.Content.Mentions != nil {
mentionCount = len(part.Content.Mentions.UserIDs)
}
linkPreviewJSON, _ := json.Marshal(part.Content.BeeperLinkPreviews)
zerolog.Ctx(ctx).Warn().
Str("msg_type", string(part.Content.MsgType)).
Int("content_json_size", len(contentJSON)).
Int("extra_json_size", len(extraJSON)).
Int("body_size", len(part.Content.Body)).
Int("formatted_body_size", len(part.Content.FormattedBody)).
Int("mention_count", mentionCount).
Int("link_preview_json_size", len(linkPreviewJSON)).
Strs("extra_keys", extraKeys).
Msg("Converted message part is larger than the cut-off limit")
}
func (mc *MessageConverter) parseFormatting(content *event.MessageEventContent, allowInlineURL, forceHTML bool) {
parsedHTML := parseWAFormattingToHTML(content.Body, allowInlineURL)
if forceHTML || parsedHTML != event.TextToHTML(content.Body) {
content.FormattedBody = parsedHTML
content.Format = event.FormatHTML
}
}
var italicRegex = regexp.MustCompile(`([\s>~*]|^)_(.+?)_([^a-zA-Z\d]|$)`)
var boldRegex = regexp.MustCompile(`([\s>_~]|^)\*(.+?)\*([^a-zA-Z\d]|$)`)
var strikethroughRegex = regexp.MustCompile(`([\s>_*]|^)~(.+?)~([^a-zA-Z\d]|$)`)
var inlineCodeRegex = regexp.MustCompile("([\\s>_*~]|^)`(.+?)`([^a-zA-Z\\d]|$)")
var inlineURLRegex = regexp.MustCompile(`\[(.+?)]\((.+?)\)`)
var orderedListItemRegex = regexp.MustCompile(`^(\d{1,2})\. `)
var waReplString = map[*regexp.Regexp]string{
italicRegex: "$1$2$3",
boldRegex: "$1$2$3",
strikethroughRegex: "$1$2$3",
inlineCodeRegex: "$1$2$3",
}
func parseWASubFormattingLineToHTML(text string, allowInlineURL bool) string {
text = html.EscapeString(text)
for regex, replacement := range waReplString {
text = regex.ReplaceAllString(text, replacement)
}
if allowInlineURL {
text = inlineURLRegex.ReplaceAllStringFunc(text, func(s string) string {
groups := inlineURLRegex.FindStringSubmatch(s)
return fmt.Sprintf(`%s`, groups[2], groups[1])
})
}
return text
}
func parseWASubFormattingToHTML(text string, allowInlineURL bool, output *strings.Builder) {
lines := strings.Split(text, "\n")
orderedListIdx := -1
inBulletedList := false
wasBlockQuote := false
for i, line := range lines {
if i != 0 && orderedListIdx < 0 && !inBulletedList && !wasBlockQuote {
output.WriteString("
")
}
wasBlockQuote = false
if strings.HasPrefix(line, "* ") || strings.HasPrefix(line, "- ") {
if orderedListIdx >= 0 {
orderedListIdx = -1
output.WriteString("")
}
if !inBulletedList {
output.WriteString("
%s", parseWASubFormattingLineToHTML(line[2:], allowInlineURL)) wasBlockQuote = true } else { if orderedListIdx >= 0 { orderedListIdx = -1 output.WriteString("
%s", html.EscapeString(content))
} else {
_, _ = fmt.Fprintf(&output, "%s", html.EscapeString(content))
}
}
if codeBlockPtr < len(text) {
parseWASubFormattingToHTML(text[codeBlockPtr:], allowInlineURL, &output)
}
return output.String()
}