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)
* Added option to disable reply fallbacks entirely.

View file

@ -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)
}

View file

@ -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