From 10aa66a1285fe7146bc3b3e8ea6e0ef9557427c8 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 24 May 2023 12:41:54 +0300 Subject: [PATCH] Add support for intentional mentions in outgoing messages --- CHANGELOG.md | 4 ++++ formatting.go | 62 ++++++++++++++++++++++++++++++++++++++------------- portal.go | 26 ++++++++++++++++++--- 3 files changed, 73 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b695f75..257676d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# v0.8.6 (unreleased) + +* Implemented intentional mentions for outgoing messages. + # v0.8.5 (2023-05-16) * Added option to disable reply fallbacks entirely. diff --git a/formatting.go b/formatting.go index 9bd4360..3a06575 100644 --- a/formatting.go +++ b/formatting.go @@ -20,10 +20,11 @@ import ( "fmt" "html" "regexp" + "sort" "strings" "go.mau.fi/whatsmeow/types" - + "golang.org/x/exp/slices" "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/format" "maunium.net/go/mautrix/id" @@ -36,7 +37,7 @@ var codeBlockRegex = regexp.MustCompile("```(?:.|\n)+?```") var inlineURLRegex = regexp.MustCompile(`\[(.+?)]\((.+?)\)`) const mentionedJIDsContextKey = "fi.mau.whatsapp.mentioned_jids" -const disableMentionsContextKey = "fi.mau.whatsapp.no_mentions" +const allowedMentionsContextKey = "fi.mau.whatsapp.allowed_mentions" type Formatter struct { bridge *WABridge @@ -56,17 +57,24 @@ func NewFormatter(bridge *WABridge) *Formatter { Newline: "\n", PillConverter: func(displayname, mxid, eventID string, ctx format.Context) string { - _, disableMentions := ctx.ReturnData[disableMentionsContextKey] - if mxid[0] == '@' && !disableMentions { - puppet := bridge.GetPuppetByMXID(id.UserID(mxid)) - if puppet != nil { - jids, ok := ctx.ReturnData[mentionedJIDsContextKey].([]string) - if !ok { - ctx.ReturnData[mentionedJIDsContextKey] = []string{puppet.JID.String()} - } else { - ctx.ReturnData[mentionedJIDsContextKey] = append(jids, puppet.JID.String()) + allowedMentions, _ := ctx.ReturnData[allowedMentionsContextKey].(map[types.JID]bool) + if mxid[0] == '@' { + var jid types.JID + if puppet := bridge.GetPuppetByMXID(id.UserID(mxid)); puppet != nil { + jid = puppet.JID + } else if user := bridge.GetUserByMXIDIfExists(id.UserID(mxid)); user != nil { + jid = user.JID.ToNonAD() + } + if !jid.IsEmpty() && (allowedMentions == nil || allowedMentions[jid]) { + if allowedMentions == nil { + jids, ok := ctx.ReturnData[mentionedJIDsContextKey].([]string) + if !ok { + ctx.ReturnData[mentionedJIDsContextKey] = []string{jid.String()} + } else { + ctx.ReturnData[mentionedJIDsContextKey] = append(jids, jid.String()) + } } - return "@" + puppet.JID.User + return "@" + jid.User } } return displayname @@ -143,7 +151,6 @@ func (formatter *Formatter) ParseWhatsApp(roomID id.RoomID, content *event.Messa content.Mentions.UserIDs = append(content.Mentions.UserIDs, mxid) } } - content.UnstableMentions = content.Mentions if output != content.Body || forceHTML { output = strings.ReplaceAll(output, "\n", "
") content.FormattedBody = output @@ -154,15 +161,38 @@ func (formatter *Formatter) ParseWhatsApp(roomID id.RoomID, content *event.Messa } } -func (formatter *Formatter) ParseMatrix(html string) (string, []string) { +func (formatter *Formatter) ParseMatrix(html string, mentions *event.Mentions) (string, []string) { ctx := format.NewContext() + var mentionedJIDs []string + if mentions != nil { + var allowedMentions = make(map[types.JID]bool) + mentionedJIDs = make([]string, 0, len(mentions.UserIDs)) + for _, userID := range mentions.UserIDs { + var jid types.JID + if puppet := formatter.bridge.GetPuppetByMXID(userID); puppet != nil { + jid = puppet.JID + mentionedJIDs = append(mentionedJIDs, puppet.JID.String()) + } else if user := formatter.bridge.GetUserByMXIDIfExists(userID); user != nil { + jid = user.JID.ToNonAD() + } + if !jid.IsEmpty() && !allowedMentions[jid] { + allowedMentions[jid] = true + mentionedJIDs = append(mentionedJIDs, jid.String()) + } + } + ctx.ReturnData[allowedMentionsContextKey] = allowedMentions + } result := formatter.matrixHTMLParser.Parse(html, ctx) - mentionedJIDs, _ := ctx.ReturnData[mentionedJIDsContextKey].([]string) + if mentions == nil { + mentionedJIDs, _ = ctx.ReturnData[mentionedJIDsContextKey].([]string) + sort.Strings(mentionedJIDs) + mentionedJIDs = slices.Compact(mentionedJIDs) + } return result, mentionedJIDs } func (formatter *Formatter) ParseMatrixWithoutMentions(html string) string { ctx := format.NewContext() - ctx.ReturnData[disableMentionsContextKey] = true + ctx.ReturnData[allowedMentionsContextKey] = map[types.JID]struct{}{} return formatter.matrixHTMLParser.Parse(html, ctx) } diff --git a/portal.go b/portal.go index 60178a0..3f73477 100644 --- a/portal.go +++ b/portal.go @@ -43,6 +43,7 @@ import ( "github.com/chai2010/webp" "github.com/tidwall/gjson" + "golang.org/x/exp/slices" "golang.org/x/image/draw" "google.golang.org/protobuf/proto" @@ -1880,6 +1881,22 @@ func (portal *Portal) MainIntent() *appservice.IntentAPI { return portal.bridge.Bot } +func (portal *Portal) addReplyMention(content *event.MessageEventContent, sender types.JID) { + if content.Mentions == nil { + return + } + var mxid id.UserID + if user := portal.bridge.GetUserByJID(sender); user != nil { + mxid = user.MXID + } else { + puppet := portal.bridge.GetPuppetByJID(sender) + mxid = puppet.MXID + } + if slices.Contains(content.Mentions.UserIDs, mxid) { + content.Mentions.UserIDs = append(content.Mentions.UserIDs, mxid) + } +} + func (portal *Portal) SetReply(content *event.MessageEventContent, replyTo *ReplyInfo, isBackfill bool) bool { if replyTo == nil { return false @@ -1908,10 +1925,13 @@ func (portal *Portal) SetReply(content *event.MessageEventContent, replyTo *Repl if message == nil || message.IsFakeMXID() { if isBackfill && portal.bridge.Config.Homeserver.Software == bridgeconfig.SoftwareHungry { content.RelatesTo = (&event.RelatesTo{}).SetReplyTo(targetPortal.deterministicEventID(replyTo.Sender, replyTo.MessageID, "")) + portal.addReplyMention(content, replyTo.Sender) return true } return false } + // TODO store sender mxid in db message + portal.addReplyMention(content, message.Sender) content.RelatesTo = (&event.RelatesTo{}).SetReplyTo(message.MXID) if portal.bridge.Config.Bridge.DisableReplyFallbacks { return true @@ -3395,7 +3415,7 @@ func (portal *Portal) preprocessMatrixMedia(ctx context.Context, sender *User, r hasHTMLCaption = content.Format == event.FormatHTML } if relaybotFormatted || hasHTMLCaption { - caption, mentionedJIDs = portal.bridge.Formatter.ParseMatrix(content.FormattedBody) + caption, mentionedJIDs = portal.bridge.Formatter.ParseMatrix(content.FormattedBody, content.Mentions) } var file *event.EncryptedFileInfo @@ -3621,7 +3641,7 @@ func (portal *Portal) msc1767ToWhatsApp(msg MSC1767Message, mentions bool) (stri } if msg.HTML != "" { if mentions { - return portal.bridge.Formatter.ParseMatrix(msg.HTML) + return portal.bridge.Formatter.ParseMatrix(msg.HTML, nil) } else { return portal.bridge.Formatter.ParseMatrixWithoutMentions(msg.HTML), nil } @@ -3858,7 +3878,7 @@ func (portal *Portal) convertMatrixMessage(ctx context.Context, sender *User, ev return nil, sender, extraMeta, errMNoticeDisabled } if content.Format == event.FormatHTML { - text, ctxInfo.MentionedJid = portal.bridge.Formatter.ParseMatrix(content.FormattedBody) + text, ctxInfo.MentionedJid = portal.bridge.Formatter.ParseMatrix(content.FormattedBody, content.Mentions) } if content.MsgType == event.MsgEmote && !relaybotFormatted { text = "/me " + text