diff --git a/src/telegram/bot-message-context.implicit-mention.test.ts b/src/telegram/bot-message-context.implicit-mention.test.ts index b2ec31c4d5d..a5dc9862ede 100644 --- a/src/telegram/bot-message-context.implicit-mention.test.ts +++ b/src/telegram/bot-message-context.implicit-mention.test.ts @@ -1,18 +1,20 @@ import { describe, expect, it } from "vitest"; import { buildTelegramMessageContextForTest } from "./bot-message-context.test-harness.js"; -describe("buildTelegramMessageContext implicitMention forum system messages", () => { +describe("buildTelegramMessageContext implicitMention forum service 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). + * bot. Callers control whether the reply target looks like a forum service + * message (carries `forum_topic_created` etc.) or a real bot reply. */ async function buildGroupReplyCtx(params: { replyToMessageText?: string; replyToMessageCaption?: string; replyFromIsBot?: boolean; replyFromId?: number; + /** Extra fields on reply_to_message (e.g. forum_topic_created). */ + replyToMessageExtra?: Record; }) { const BOT_ID = 7; // matches test harness primaryCtx.me.id return await buildTelegramMessageContextForTest({ @@ -33,6 +35,7 @@ describe("buildTelegramMessageContext implicitMention forum system messages", () first_name: "OpenClaw", is_bot: params.replyFromIsBot ?? true, }, + ...params.replyToMessageExtra, }, }, resolveGroupActivation: () => true, @@ -44,11 +47,14 @@ describe("buildTelegramMessageContext implicitMention forum system messages", () }); } - 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 + it("does NOT trigger implicitMention for forum_topic_created service message", async () => { + // Bot auto-generated "Topic created" message carries forum_topic_created. const ctx = await buildGroupReplyCtx({ replyToMessageText: undefined, replyFromIsBot: true, + replyToMessageExtra: { + forum_topic_created: { name: "New Topic", icon_color: 0x6fb9f0 }, + }, }); // With requireMention and no explicit @mention, the message should be @@ -56,10 +62,21 @@ describe("buildTelegramMessageContext implicitMention forum system messages", () expect(ctx).toBeNull(); }); - it("does NOT trigger implicitMention for empty-string text system messages", async () => { + it("does NOT trigger implicitMention for forum_topic_closed service message", async () => { const ctx = await buildGroupReplyCtx({ - replyToMessageText: "", + replyToMessageText: undefined, replyFromIsBot: true, + replyToMessageExtra: { forum_topic_closed: {} }, + }); + + expect(ctx).toBeNull(); + }); + + it("does NOT trigger implicitMention for general_forum_topic_hidden service message", async () => { + const ctx = await buildGroupReplyCtx({ + replyToMessageText: undefined, + replyFromIsBot: true, + replyToMessageExtra: { general_forum_topic_hidden: {} }, }); expect(ctx).toBeNull(); @@ -76,12 +93,12 @@ describe("buildTelegramMessageContext implicitMention forum system messages", () 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.) + it("DOES trigger implicitMention for bot media messages with caption", async () => { + // Media messages from the bot have caption but no text — they should + // still count as real bot replies, not service messages. const ctx = await buildGroupReplyCtx({ - replyToMessageText: " ", + replyToMessageText: undefined, + replyToMessageCaption: "Check out this image", replyFromIsBot: true, }); @@ -89,13 +106,14 @@ describe("buildTelegramMessageContext implicitMention forum system messages", () expect(ctx?.ctxPayload?.WasMentioned).toBe(true); }); - it("DOES trigger implicitMention for bot media messages with caption (not a system message)", async () => { - // Media messages from the bot have caption but no text — they should - // still count as real bot replies, not system messages. + it("DOES trigger implicitMention for bot sticker/voice (no text, no caption, no service field)", async () => { + // Stickers, voice notes, and captionless photos have neither text nor + // caption, but they are NOT service messages — they are legitimate bot + // replies that should trigger implicitMention. const ctx = await buildGroupReplyCtx({ replyToMessageText: undefined, - replyToMessageCaption: "Check out this image", replyFromIsBot: true, + // No forum_topic_* fields → not a service message }); expect(ctx).not.toBeNull(); diff --git a/src/telegram/bot-message-context.ts b/src/telegram/bot-message-context.ts index 6f4483c7bb6..4879b96821a 100644 --- a/src/telegram/bot-message-context.ts +++ b/src/telegram/bot-message-context.ts @@ -471,18 +471,18 @@ 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). + // Exclude forum-topic service messages (auto-generated "Topic created" etc. messages + // by the bot) so that every message inside a bot-created topic does not incorrectly + // bypass requireMention (#32256). + // We detect service messages by the presence of Telegram's forum_topic_* fields + // rather than by the absence of text/caption, because legitimate bot media messages + // (stickers, voice notes, captionless photos) also lack text/caption. const botId = primaryCtx.me?.id; const replyFromId = msg.reply_to_message?.from?.id; const replyToBotMessage = botId != null && replyFromId === botId; - const isReplyToSystemMessage = - replyToBotMessage && - msg.reply_to_message?.from?.is_bot === true && - !msg.reply_to_message?.text && - !msg.reply_to_message?.caption; - const implicitMention = replyToBotMessage && !isReplyToSystemMessage; + const isReplyToServiceMessage = + replyToBotMessage && isTelegramForumServiceMessage(msg.reply_to_message); + const implicitMention = replyToBotMessage && !isReplyToServiceMessage; const canDetectMention = Boolean(botUsername) || mentionRegexes.length > 0; const mentionGate = resolveMentionGatingWithBypass({ isGroup, @@ -867,3 +867,30 @@ export const buildTelegramMessageContext = async ({ export type TelegramMessageContext = NonNullable< Awaited> >; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/** Telegram forum-topic service-message fields (Bot API). */ +const FORUM_SERVICE_FIELDS = [ + "forum_topic_created", + "forum_topic_edited", + "forum_topic_closed", + "forum_topic_reopened", + "general_forum_topic_hidden", + "general_forum_topic_unhidden", +] as const; + +/** + * Returns `true` when the message is a Telegram forum service message (e.g. + * "Topic created"). These auto-generated messages carry one of the + * `forum_topic_*` / `general_forum_topic_*` fields and should not count as + * regular bot replies for implicit-mention purposes. + */ +function isTelegramForumServiceMessage(msg: Record | undefined | null): boolean { + if (!msg) { + return false; + } + return FORUM_SERVICE_FIELDS.some((f) => msg[f] != null); +}