Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions pkg/connector/mentions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package connector

import (
"fmt"
"html"
"regexp"
"strings"

"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
)

var mentionPattern = regexp.MustCompile(`(?:^|[^\w@/])(@[\w-]{3,20})`)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚪️ Nit · Usernames longer than 20 chars can be linked by their prefix

(@[\w-]{3,20}) has no trailing boundary. For @some_user_name_that_is_long the first 20 chars are captured, and if a member's name equals that prefix they get linked and pinged while the rest of the token stays plain text. Reddit usernames max out at 20 chars, so a longer token is never a real mention. Require a non-username char or end of string after the capture.

Found by Indent Review Agent


// Reddit sends mentions as plain "@username" text with no Matrix mention data.
func (r *RedditClient) convertMentions(portal *bridgev2.Portal, content *event.MessageEventContent) {
meta, _ := portal.Metadata.(*PortalMetadata)
if content.Format != "" || meta == nil || meta.State == nil {
return
}
formatted, mentioned := linkMentions(content.Body, meta.State.Members, r.beeperMXID)
if len(mentioned) == 0 {
return
}
content.Format = event.FormatHTML
content.FormattedBody = formatted
content.Mentions = &event.Mentions{UserIDs: mentioned}
}

func (r *RedditClient) beeperMXID(redditMXID id.UserID) id.UserID {
if userID := makeUserID(redditMXID); userID != r.userID {
return r.main.Bridge.Matrix.FormatGhostMXID(userID)
}
return r.userLogin.UserMXID
}

func linkMentions(body string, members map[id.UserID]RoomMember, beeperMXID func(id.UserID) id.UserID) (string, []id.UserID) {
usernames := make(map[string]id.UserID, len(members))
for mxid, member := range members {
if member.Name != "" {
usernames[strings.ToLower(stripUserPrefix(member.Name))] = mxid
}
}
var formatted strings.Builder
var mentioned []id.UserID
last := 0
for _, match := range mentionPattern.FindAllStringSubmatchIndex(body, -1) {
start, end := match[2], match[3]
redditMXID, ok := usernames[strings.ToLower(body[start+1:end])]
if !ok {
continue
}
target := beeperMXID(redditMXID)
formatted.WriteString(plainToHTML(body[last:start]))
_, _ = fmt.Fprintf(&formatted, `<a href="%s">%s</a>`, target.URI().MatrixToURL(), html.EscapeString(body[start:end]))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚪️ Nit · Same user mentioned twice is listed twice in m.mentions

target is appended to mentioned on every match, so "@other ping @other" yields the same MXID twice in m.mentions.user_ids. Dedupe with a seen set but keep linking every occurrence in the HTML.

Found by Indent Review Agent

mentioned = append(mentioned, target)
last = end
}
formatted.WriteString(plainToHTML(body[last:]))
return formatted.String(), mentioned
}

func plainToHTML(text string) string {
return strings.ReplaceAll(html.EscapeString(text), "\n", "<br>")
}
34 changes: 34 additions & 0 deletions pkg/connector/mentions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package connector

import (
"slices"
"testing"

"maunium.net/go/mautrix/id"
)

func TestLinkMentions(t *testing.T) {
members := map[id.UserID]RoomMember{
"@t2_a:reddit.com": {Name: "Some_User-1"},
"@t2_b:reddit.com": {Name: "other"},
"@t2_c:reddit.com": {},
}
beeperMXID := func(mxid id.UserID) id.UserID { return "@ghost_" + id.UserID(makeUserID(mxid)) + ":beeper.local" }
for _, tc := range []struct {
name, body, want string
mentioned []id.UserID
}{
{"start", "@some_user-1 hi", `<a href="https://matrix.to/#/@ghost_t2_a:beeper.local">@some_user-1</a> hi`, []id.UserID{"@ghost_t2_a:beeper.local"}},
{"two", "hey @other & @Some_User-1!", `hey <a href="https://matrix.to/#/@ghost_t2_b:beeper.local">@other</a> &amp; <a href="https://matrix.to/#/@ghost_t2_a:beeper.local">@Some_User-1</a>!`, []id.UserID{"@ghost_t2_b:beeper.local", "@ghost_t2_a:beeper.local"}},
{"not a member", "@stranger <b>", "@stranger &lt;b&gt;", nil},
{"email and url", "a@other.com reddit.com/@other", "a@other.com reddit.com/@other", nil},
{"newline", "line\n@other", `line<br><a href="https://matrix.to/#/@ghost_t2_b:beeper.local">@other</a>`, []id.UserID{"@ghost_t2_b:beeper.local"}},
} {
t.Run(tc.name, func(t *testing.T) {
formatted, mentioned := linkMentions(tc.body, members, beeperMXID)
if formatted != tc.want || !slices.Equal(mentioned, tc.mentioned) {
t.Fatalf("linkMentions(%q) = %q, %v", tc.body, formatted, mentioned)
}
})
}
}
3 changes: 3 additions & 0 deletions pkg/connector/tomatrix.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ func (r *RedditClient) convertMessage(ctx context.Context, portal *bridgev2.Port
return nil, err
}
case event.MsgText, event.MsgNotice, event.MsgEmote:
if portal != nil {
r.convertMentions(portal, &content)
}
default:
return nil, bridgev2.ErrUnsupportedMessageType
}
Expand Down
Loading