Procházet zdrojové kódy

Add support for intentional mentions in outgoing messages

Tulir Asokan před 2 roky
rodič
revize
10aa66a128
3 změnil soubory, kde provedl 73 přidání a 19 odebrání
  1. 4 0
      CHANGELOG.md
  2. 46 16
      formatting.go
  3. 23 3
      portal.go

+ 4 - 0
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.

+ 46 - 16
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", "<br/>")
 		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)
 }

+ 23 - 3
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