From 83e8139da2acd7fc7c24794c379f4a369ea5fb4d Mon Sep 17 00:00:00 2001 From: Junjun Zhang Date: Fri, 25 Sep 2026 12:33:32 +0800 Subject: [PATCH] fix(im): lowercase matrix mention needle for mixed-case localparts (#2749 follow-up) The post-match boundary rewrite dropped the old (?i) case-insensitivity: the body is lowered by callers but the needle ('@' + localPart) was not, so mixed-case localparts (@Bot-:server on self-hosted homeservers) never matched - the #2749 miss resurfacing as a case variant. Lower the needle in both mentionsLocalPart and stripLocalPartMention. Tests: mixed-case non-word ending, uppercase body, lowercase-body-of- uppercase-localpart, and stripMention round trip. --- internal/im/matrix_adapter.go | 8 ++++++-- internal/im/zz_issue2749_test.go | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/internal/im/matrix_adapter.go b/internal/im/matrix_adapter.go index 6feb55bab..9515d4a17 100644 --- a/internal/im/matrix_adapter.go +++ b/internal/im/matrix_adapter.go @@ -832,7 +832,10 @@ const matrixLocalPartContChars = "abcdefghijklmnopqrstuvwxyz0123456789._=-+/" // mentionsLocalPart reports whether text contains "@localpart" NOT followed // by a legal localpart continuation character (RE2-safe boundary). func mentionsLocalPart(lower, localPart string) bool { - needle := "@" + localPart + // #2749 case follow-up: the old (?i) regex was case-insensitive, and + // self-hosted homeservers may issue mixed-case localparts (@Bot:...). + // The body is already lowered by callers; lower the needle too. + needle := "@" + strings.ToLower(localPart) for start := 0; ; { i := strings.Index(lower[start:], needle) if i < 0 { @@ -851,7 +854,8 @@ func mentionsLocalPart(lower, localPart string) bool { // prefixes of a longer handle (same boundary rule as mentionsLocalPart). func stripLocalPartMention(text, localPart string) string { lower := strings.ToLower(text) - needle := "@" + localPart + // #2749 case follow-up: lower the needle to match the (?i) behavior. + needle := "@" + strings.ToLower(localPart) var b strings.Builder for i := 0; i < len(text); { if strings.HasPrefix(lower[i:], needle) { diff --git a/internal/im/zz_issue2749_test.go b/internal/im/zz_issue2749_test.go index 5247e1d8e..52a1e228d 100644 --- a/internal/im/zz_issue2749_test.go +++ b/internal/im/zz_issue2749_test.go @@ -58,3 +58,22 @@ func TestIssue2749_WordEndingLocalpartUnchanged(t *testing.T) { t.Errorf("prefix confusion regression") } } + +func TestIssue2749_MixedCaseLocalpart(t *testing.T) { + // #2749 case follow-up: self-hosted homeservers may issue mixed-case + // localparts; the old (?i) regex matched them. Lower both sides. + a := &matrixAdapter{userID: "@Bot-:matrix.org"} + if !a.hasMention("@Bot- check the build", nil) { + t.Errorf("mixed-case non-word-ending mention must match") + } + if !a.hasMention("hey @BOT- run tests", nil) { + t.Errorf("uppercase body mention of mixed-case localpart must match") + } + b := &matrixAdapter{userID: "@Al:matrix.org"} + if !b.hasMention("@al do it", nil) { + t.Errorf("lowercase body mention of uppercase localpart must match") + } + if got := a.stripMention("@Bot- check the build"); got != "check the build" { + t.Errorf("stripMention mixed-case = %q, want %q", got, "check the build") + } +}