From 8fdd1d2f05effc29d31f048fbec8e34a469b1813 Mon Sep 17 00:00:00 2001 From: scoootscooob Date: Mon, 2 Mar 2026 14:48:49 -0800 Subject: [PATCH] fix(telegram): exclude forum topic system messages from implicitMention When a Telegram Forum topic is created by the bot, Telegram generates a system message with from.id=botId and empty text. Every subsequent user message in that topic has reply_to_message pointing to this system message, causing the implicitMention check to fire and bypassing requireMention for every single message. Add a guard that recognises system messages (is_bot=true with no text) and excludes them from implicit mention detection, so that only genuine replies to bot messages trigger the bypass. Closes #32256 Co-Authored-By: Claude Opus 4.6 --- ...t-message-context.implicit-mention.test.ts | 98 +++++++++++++++++++ src/telegram/bot-message-context.ts | 8 +- 2 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 src/telegram/bot-message-context.implicit-mention.test.ts diff --git a/src/telegram/bot-message-context.implicit-mention.test.ts b/src/telegram/bot-message-context.implicit-mention.test.ts new file mode 100644 index 00000000000..028c5f29ee1 --- /dev/null +++ b/src/telegram/bot-message-context.implicit-mention.test.ts @@ -0,0 +1,98 @@ +import { describe, expect, it } from "vitest"; +import { buildTelegramMessageContextForTest } from "./bot-message-context.test-harness.js"; + +describe("buildTelegramMessageContext implicitMention forum system messages", () => { + /** + * Build a group message context where the user sends a message inside a + * forum topic that has `reply_to_message` pointing to a message from the + * bot. Callers control whether the reply target looks like a system + * message (empty text) or a real bot reply (non-empty text). + */ + async function buildGroupReplyCtx(params: { + replyToMessageText?: string; + replyFromIsBot?: boolean; + replyFromId?: number; + }) { + const BOT_ID = 7; // matches test harness primaryCtx.me.id + return await buildTelegramMessageContextForTest({ + message: { + message_id: 100, + chat: { id: -1001234567890, type: "supergroup", title: "Forum Group" }, + date: 1700000000, + text: "hello everyone", + from: { id: 42, first_name: "Alice" }, + reply_to_message: { + message_id: 1, + text: params.replyToMessageText ?? undefined, + from: { + id: params.replyFromId ?? BOT_ID, + first_name: "OpenClaw", + is_bot: params.replyFromIsBot ?? true, + }, + }, + }, + resolveGroupActivation: () => true, + resolveGroupRequireMention: () => true, + resolveTelegramGroupConfig: () => ({ + groupConfig: { requireMention: true }, + topicConfig: undefined, + }), + }); + } + + it("does NOT trigger implicitMention for forum topic system messages (empty-text bot message)", async () => { + // System message: bot created the topic → text is empty, from.is_bot = true + const ctx = await buildGroupReplyCtx({ + replyToMessageText: undefined, + replyFromIsBot: true, + }); + + // With requireMention and no explicit @mention, the message should be + // skipped (null) because implicitMention should NOT fire. + expect(ctx).toBeNull(); + }); + + it("does NOT trigger implicitMention for empty-string text system messages", async () => { + const ctx = await buildGroupReplyCtx({ + replyToMessageText: "", + replyFromIsBot: true, + }); + + expect(ctx).toBeNull(); + }); + + it("DOES trigger implicitMention for real bot replies (non-empty text)", async () => { + const ctx = await buildGroupReplyCtx({ + replyToMessageText: "Here is my answer", + replyFromIsBot: true, + }); + + // Real bot reply → implicitMention fires → message is NOT skipped. + expect(ctx).not.toBeNull(); + expect(ctx?.ctxPayload?.WasMentioned).toBe(true); + }); + + it("DOES trigger implicitMention for bot reply with whitespace-only text", async () => { + // A bot message that has actual whitespace text is NOT a system message, + // so it should still count as an implicit mention. (Telegram's forum + // system messages have undefined / empty text, not whitespace.) + const ctx = await buildGroupReplyCtx({ + replyToMessageText: " ", + replyFromIsBot: true, + }); + + expect(ctx).not.toBeNull(); + expect(ctx?.ctxPayload?.WasMentioned).toBe(true); + }); + + it("does NOT trigger implicitMention when reply is from a different user", async () => { + const ctx = await buildGroupReplyCtx({ + replyToMessageText: "some message", + replyFromIsBot: false, + replyFromId: 999, + }); + + // Different user's message → not an implicit mention → skipped. + expect(ctx).toBeNull(); + }); +}); diff --git a/src/telegram/bot-message-context.ts b/src/telegram/bot-message-context.ts index 6de7c8e5f87..a61c5b4b2e4 100644 --- a/src/telegram/bot-message-context.ts +++ b/src/telegram/bot-message-context.ts @@ -471,9 +471,15 @@ export const buildTelegramMessageContext = async ({ return null; } // Reply-chain detection: replying to a bot message acts like an implicit mention. + // Exclude forum-topic system messages (auto-generated "Topic created" messages by the + // bot that have empty text) so that every message inside a bot-created topic does not + // incorrectly bypass requireMention (#32256). const botId = primaryCtx.me?.id; const replyFromId = msg.reply_to_message?.from?.id; - const implicitMention = botId != null && replyFromId === botId; + const replyToBotMessage = botId != null && replyFromId === botId; + const isReplyToSystemMessage = + replyToBotMessage && msg.reply_to_message?.from?.is_bot === true && !msg.reply_to_message?.text; + const implicitMention = replyToBotMessage && !isReplyToSystemMessage; const canDetectMention = Boolean(botUsername) || mentionRegexes.length > 0; const mentionGate = resolveMentionGatingWithBypass({ isGroup,