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
20 changes: 14 additions & 6 deletions internal/im/mattermost_adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,7 @@ func (a *mattermostAdapter) handleWSEvent(ctx context.Context, event map[string]
if !isFree && a.requireMention {
hasMention := a.hasMention(messageText)
if !hasMention {
debug.Log("mattermost", "adapter=%s drop: no mention of @%s in channel %s", a.name, a.botUsername, channelID)
return
}
messageText = a.stripMention(messageText)
Expand Down Expand Up @@ -469,12 +470,17 @@ func (a *mattermostAdapter) handleWSEvent(ctx context.Context, event map[string]

func (a *mattermostAdapter) hasMention(text string) bool {
lower := strings.ToLower(text)
// Word-boundary match: bot name "al" must not fire on "@alex" (#963).
// Boundary match: bot name "al" must not fire on "@alex" (#963).
// The boundary is username-charset-aware, not ASCII \b: Mattermost
// usernames allow ^[a-z0-9.\-_]+$ and may legally end in "-" or ".",
// where RE2 \b can never match (both sides non-word chars) and real
// mentions were silently dropped (#4322). A mention ends only when the
// next char is NOT a username char.
for _, id := range []string{a.botUsername, a.botUserID} {
if id == "" {
continue
}
re := regexp.MustCompile(`(?i)@` + regexp.QuoteMeta(id) + `\b`)
re := regexp.MustCompile(`(?i)@` + regexp.QuoteMeta(id) + `(?:$|[^a-z0-9.\-_])`)
if re.MatchString(lower) {
return true
}
Expand All @@ -483,14 +489,16 @@ func (a *mattermostAdapter) hasMention(text string) bool {
}

func (a *mattermostAdapter) stripMention(text string) string {
// Strip @bot_username and @bot_user_id mentions, with word boundary so a
// short bot name ("al") doesn't carve text out of "@alex" (#963).
// Strip @bot_username and @bot_user_id mentions, with a username-charset
// boundary so a short bot name ("al") doesn't carve text out of "@alex"
// (#963) while names ending in "-"/"." still strip (#4322). The boundary
// char (when present) is captured and preserved in the replacement.
for _, pattern := range []string{a.botUsername, a.botUserID} {
if pattern == "" {
continue
}
re := regexp.MustCompile(`(?i)@` + regexp.QuoteMeta(pattern) + `\b`)
text = re.ReplaceAllString(text, "")
re := regexp.MustCompile(`(?i)@` + regexp.QuoteMeta(pattern) + `(?:$|([^a-z0-9.\-_]))`)
text = re.ReplaceAllString(text, "$1")
}
return strings.TrimSpace(text)
}
Expand Down
43 changes: 43 additions & 0 deletions internal/im/zz_mention_boundary_4322_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package im

import "testing"

// #4322 regression: Mattermost usernames may end in "-" or "." - the old
// ASCII \b boundary could never match there, silently dropping real mentions.
func TestMentionBoundaryUsernameCharsetName(t *testing.T) {
cases := []struct {
bot string
name string
text string
wantHit bool
}{
{"dev-", "trailing dash", "@dev- help with logs", true},
{"dev-", "trailing dash end-of-text", "ping @dev-", true},
{"ops1.", "trailing dot", "@ops1. check logs", true},
{"al", "plain name mention", "@al hello", true},
{"al", "substring no false hit", "@alex hello", false},
{"dev", "longer username no carve", "@dev-extra text", false},
{"dev-", "no mention", "hello world", false},
}
for _, c := range cases {
a := &mattermostAdapter{botUsername: c.bot}
if got := a.hasMention(c.text); got != c.wantHit {
t.Errorf("bot=%q case=%q hasMention(%q) = %v, want %v", c.bot, c.name, c.text, got, c.wantHit)
}
}
}

func TestStripMentionBoundaryPreservesSeparator(t *testing.T) {
a := &mattermostAdapter{botUsername: "dev-"}
if got := a.stripMention("@dev- help with logs"); got != "help with logs" {
t.Errorf("strip = %q, want %q", got, "help with logs")
}
b := &mattermostAdapter{botUsername: "al"}
if got := b.stripMention("@al hello there"); got != "hello there" {
t.Errorf("strip = %q, want %q", got, "hello there")
}
// "@alex" must not be carved by short bot "al".
if got := b.stripMention("@alex hello"); got != "@alex hello" {
t.Errorf("strip = %q, want unchanged %q", got, "@alex hello")
}
}
Loading