fix(telegram): strip own @mention from inbound content

Agents previously saw their own Telegram handle (e.g. "@viet_super_bot")
in user messages and mistook it for a different bot, replying NO_REPLY.
The username was only used for the mention gate, never removed from the
content passed to the LLM.

Slack and Feishu already strip their own bot mentions (handlers_mention.go
stripBotMention, bot_parse.go resolveMentions); Telegram was the odd one out.

Implementation:
- Add stripBotMention helper with leading/trailing word-boundary anchors so
  inline matches inside words (e.g. contact@viet_super_bot.com) are not
  falsely stripped.
- Apply in handleMessage right after the mention gate, before pairing/media
  processing, so history recording for unmentioned messages keeps raw text.
- Restore "[empty message]" placeholder when a message consisting only of
  "@botname" becomes empty after stripping.
This commit is contained in:
viettranx
2026-04-23 08:31:53 +07:00
parent 3a85d625a6
commit e392fca14d
3 changed files with 92 additions and 0 deletions
+9
View File
@@ -361,6 +361,15 @@ func (c *Channel) handleMessage(ctx context.Context, update telego.Update) {
}
}
// Strip bot's own @mention so the LLM sees clean content and does not
// mistake itself for another bot (cross-channel parity with Slack/Feishu).
// Re-check empty state: a message containing only "@botname" becomes empty
// after stripping, so we restore the placeholder used for originally-empty inbounds.
content = stripBotMention(content, c.bot.Username())
if content == "" {
content = "[empty message]"
}
// --- Group pairing gate (only reached when bot is mentioned) ---
if isGroup && topicCfg.groupPolicy == "pairing" && c.PairingService() != nil {
if !c.IsGroupApproved(chatIDStr) {
@@ -1,11 +1,30 @@
package telegram
import (
"regexp"
"strings"
"github.com/mymmrac/telego"
)
// stripBotMention removes @botUsername tokens from text (case-insensitive).
// Applied after the mention gate passes so the LLM does not see its own Telegram handle
// and mistake itself for another bot (e.g. persona "Tiểu Hổ" receiving "@viet_super_bot vẽ...").
//
// Boundary rules match valid Telegram mentions:
// - Leading: start-of-string OR a non-word char (whitespace/punct). Prevents false strips
// inside words like "contact@viet_super_bot.com".
// - Trailing: \b (word-boundary). Prevents matching "@bot" inside "@bot_2".
//
// The leading non-word char is preserved via capture group $1.
func stripBotMention(text, botUsername string) string {
if botUsername == "" || text == "" {
return text
}
pattern := `(?i)(^|[^\w])@` + regexp.QuoteMeta(botUsername) + `\b`
return strings.TrimSpace(regexp.MustCompile(pattern).ReplaceAllString(text, "$1"))
}
// detectMention checks if a Telegram message mentions the bot.
// Checks both msg.Text/Entities (text messages) and msg.Caption/CaptionEntities (photo/media messages).
func (c *Channel) detectMention(msg *telego.Message, botUsername string) bool {
@@ -208,6 +208,70 @@ func TestHasOtherMention_CaptionWithOtherMention(t *testing.T) {
}
}
// --- stripBotMention ---
func TestStripBotMention_RemovesMention(t *testing.T) {
got := stripBotMention("@viet_super_bot vẽ ảnh minh họa", "viet_super_bot")
want := "vẽ ảnh minh họa"
if got != want {
t.Errorf("stripBotMention = %q, want %q", got, want)
}
}
func TestStripBotMention_CaseInsensitive(t *testing.T) {
got := stripBotMention("@Viet_Super_Bot hello", "viet_super_bot")
if got != "hello" {
t.Errorf("stripBotMention case-insensitive = %q, want %q", got, "hello")
}
}
func TestStripBotMention_PreservesOtherMentions(t *testing.T) {
got := stripBotMention("@viet_super_bot hỏi @alice về X", "viet_super_bot")
want := "hỏi @alice về X"
if got != want {
t.Errorf("stripBotMention = %q, want %q", got, want)
}
}
func TestStripBotMention_WordBoundary(t *testing.T) {
// @viet_super_bot2 must NOT match @viet_super_bot (different bot with similar prefix).
got := stripBotMention("@viet_super_bot2 hello", "viet_super_bot")
if got != "@viet_super_bot2 hello" {
t.Errorf("stripBotMention should not match prefix; got %q", got)
}
}
func TestStripBotMention_EmptyUsername(t *testing.T) {
text := "@anything else"
if got := stripBotMention(text, ""); got != text {
t.Errorf("stripBotMention with empty botUsername should return text unchanged; got %q", got)
}
}
func TestStripBotMention_MultipleOccurrences(t *testing.T) {
got := stripBotMention("hey @viet_super_bot, @viet_super_bot help!", "viet_super_bot")
// Both removed; internal spacing/punctuation preserved.
want := "hey , help!"
if got != want {
t.Errorf("stripBotMention multi = %q, want %q", got, want)
}
}
func TestStripBotMention_PreservesEmailLike(t *testing.T) {
// "@viet_super_bot" embedded inside a word (e.g. email/URL) must NOT be stripped.
// Telegram mentions require a leading word-boundary, so inline matches are false positives.
in := "contact@viet_super_bot.com please"
if got := stripBotMention(in, "viet_super_bot"); got != in {
t.Errorf("stripBotMention should not strip mention embedded in word; got %q, want %q", got, in)
}
}
func TestStripBotMention_OnlyMentionBecomesEmpty(t *testing.T) {
if got := stripBotMention("@viet_super_bot", "viet_super_bot"); got != "" {
t.Errorf("mention-only input should become empty; got %q", got)
}
}
// --- isServiceMessage ---
func TestIsServiceMessage_WithText(t *testing.T) {