Add support for intentional mentions in outgoing messages

This commit is contained in:
Tulir Asokan 2023-05-24 12:41:54 +03:00
parent 7aa0cc1b7a
commit 10aa66a128
3 changed files with 73 additions and 19 deletions

View file

@ -1,3 +1,7 @@
# v0.8.6 (unreleased)
* Implemented intentional mentions for outgoing messages.
# v0.8.5 (2023-05-16) # v0.8.5 (2023-05-16)
* Added option to disable reply fallbacks entirely. * Added option to disable reply fallbacks entirely.

View file

@ -20,10 +20,11 @@ import (
"fmt" "fmt"
"html" "html"
"regexp" "regexp"
"sort"
"strings" "strings"
"go.mau.fi/whatsmeow/types" "go.mau.fi/whatsmeow/types"
"golang.org/x/exp/slices"
"maunium.net/go/mautrix/event" "maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/format" "maunium.net/go/mautrix/format"
"maunium.net/go/mautrix/id" "maunium.net/go/mautrix/id"
@ -36,7 +37,7 @@ var codeBlockRegex = regexp.MustCompile("```(?:.|\n)+?```")
var inlineURLRegex = regexp.MustCompile(`\[(.+?)]\((.+?)\)`) var inlineURLRegex = regexp.MustCompile(`\[(.+?)]\((.+?)\)`)
const mentionedJIDsContextKey = "fi.mau.whatsapp.mentioned_jids" const mentionedJIDsContextKey = "fi.mau.whatsapp.mentioned_jids"
const disableMentionsContextKey = "fi.mau.whatsapp.no_mentions" const allowedMentionsContextKey = "fi.mau.whatsapp.allowed_mentions"
type Formatter struct { type Formatter struct {
bridge *WABridge bridge *WABridge
@ -56,17 +57,24 @@ func NewFormatter(bridge *WABridge) *Formatter {
Newline: "\n", Newline: "\n",
PillConverter: func(displayname, mxid, eventID string, ctx format.Context) string { PillConverter: func(displayname, mxid, eventID string, ctx format.Context) string {
_, disableMentions := ctx.ReturnData[disableMentionsContextKey] allowedMentions, _ := ctx.ReturnData[allowedMentionsContextKey].(map[types.JID]bool)
if mxid[0] == '@' && !disableMentions { if mxid[0] == '@' {
puppet := bridge.GetPuppetByMXID(id.UserID(mxid)) var jid types.JID
if puppet != nil { if puppet := bridge.GetPuppetByMXID(id.UserID(mxid)); puppet != nil {
jids, ok := ctx.ReturnData[mentionedJIDsContextKey].([]string) jid = puppet.JID
if !ok { } else if user := bridge.GetUserByMXIDIfExists(id.UserID(mxid)); user != nil {
ctx.ReturnData[mentionedJIDsContextKey] = []string{puppet.JID.String()} jid = user.JID.ToNonAD()
} else { }
ctx.ReturnData[mentionedJIDsContextKey] = append(jids, puppet.JID.String()) 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 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.Mentions.UserIDs = append(content.Mentions.UserIDs, mxid)
} }
} }
content.UnstableMentions = content.Mentions
if output != content.Body || forceHTML { if output != content.Body || forceHTML {
output = strings.ReplaceAll(output, "\n", "<br/>") output = strings.ReplaceAll(output, "\n", "<br/>")
content.FormattedBody = output 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() 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) 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 return result, mentionedJIDs
} }
func (formatter *Formatter) ParseMatrixWithoutMentions(html string) string { func (formatter *Formatter) ParseMatrixWithoutMentions(html string) string {
ctx := format.NewContext() ctx := format.NewContext()
ctx.ReturnData[disableMentionsContextKey] = true ctx.ReturnData[allowedMentionsContextKey] = map[types.JID]struct{}{}
return formatter.matrixHTMLParser.Parse(html, ctx) return formatter.matrixHTMLParser.Parse(html, ctx)
} }

View file

@ -43,6 +43,7 @@ import (
"github.com/chai2010/webp" "github.com/chai2010/webp"
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
"golang.org/x/exp/slices"
"golang.org/x/image/draw" "golang.org/x/image/draw"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
@ -1880,6 +1881,22 @@ func (portal *Portal) MainIntent() *appservice.IntentAPI {
return portal.bridge.Bot 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 { func (portal *Portal) SetReply(content *event.MessageEventContent, replyTo *ReplyInfo, isBackfill bool) bool {
if replyTo == nil { if replyTo == nil {
return false return false
@ -1908,10 +1925,13 @@ func (portal *Portal) SetReply(content *event.MessageEventContent, replyTo *Repl
if message == nil || message.IsFakeMXID() { if message == nil || message.IsFakeMXID() {
if isBackfill && portal.bridge.Config.Homeserver.Software == bridgeconfig.SoftwareHungry { if isBackfill && portal.bridge.Config.Homeserver.Software == bridgeconfig.SoftwareHungry {
content.RelatesTo = (&event.RelatesTo{}).SetReplyTo(targetPortal.deterministicEventID(replyTo.Sender, replyTo.MessageID, "")) content.RelatesTo = (&event.RelatesTo{}).SetReplyTo(targetPortal.deterministicEventID(replyTo.Sender, replyTo.MessageID, ""))
portal.addReplyMention(content, replyTo.Sender)
return true return true
} }
return false return false
} }
// TODO store sender mxid in db message
portal.addReplyMention(content, message.Sender)
content.RelatesTo = (&event.RelatesTo{}).SetReplyTo(message.MXID) content.RelatesTo = (&event.RelatesTo{}).SetReplyTo(message.MXID)
if portal.bridge.Config.Bridge.DisableReplyFallbacks { if portal.bridge.Config.Bridge.DisableReplyFallbacks {
return true return true
@ -3395,7 +3415,7 @@ func (portal *Portal) preprocessMatrixMedia(ctx context.Context, sender *User, r
hasHTMLCaption = content.Format == event.FormatHTML hasHTMLCaption = content.Format == event.FormatHTML
} }
if relaybotFormatted || hasHTMLCaption { 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 var file *event.EncryptedFileInfo
@ -3621,7 +3641,7 @@ func (portal *Portal) msc1767ToWhatsApp(msg MSC1767Message, mentions bool) (stri
} }
if msg.HTML != "" { if msg.HTML != "" {
if mentions { if mentions {
return portal.bridge.Formatter.ParseMatrix(msg.HTML) return portal.bridge.Formatter.ParseMatrix(msg.HTML, nil)
} else { } else {
return portal.bridge.Formatter.ParseMatrixWithoutMentions(msg.HTML), nil 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 return nil, sender, extraMeta, errMNoticeDisabled
} }
if content.Format == event.FormatHTML { 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 { if content.MsgType == event.MsgEmote && !relaybotFormatted {
text = "/me " + text text = "/me " + text