diff --git a/extensions/telegram/src/bot/delivery.send.ts b/extensions/telegram/src/bot/delivery.send.ts index 0274b7f8ac1..1e9aae4ff4e 100644 --- a/extensions/telegram/src/bot/delivery.send.ts +++ b/extensions/telegram/src/bot/delivery.send.ts @@ -3,6 +3,7 @@ import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env"; import { formatErrorMessage } from "openclaw/plugin-sdk/ssrf-runtime"; import { withTelegramApiErrorLogging } from "../api-logging.js"; import { markdownToTelegramHtml } from "../format.js"; +import { normalizeTelegramReplyToMessageId } from "../outbound-params.js"; import { buildInlineKeyboard } from "../send.js"; import { buildTelegramThreadParams, type TelegramThreadSpec } from "./helpers.js"; @@ -82,8 +83,9 @@ export function buildTelegramSendParams(opts?: { }): Record { const threadParams = buildTelegramThreadParams(opts?.thread); const params: Record = {}; - if (opts?.replyToMessageId) { - params.reply_to_message_id = opts.replyToMessageId; + const replyToMessageId = normalizeTelegramReplyToMessageId(opts?.replyToMessageId); + if (replyToMessageId != null) { + params.reply_to_message_id = replyToMessageId; params.allow_sending_without_reply = true; } if (threadParams) { diff --git a/extensions/telegram/src/bot/helpers.ts b/extensions/telegram/src/bot/helpers.ts index 3d5037f153f..1a0cceb313f 100644 --- a/extensions/telegram/src/bot/helpers.ts +++ b/extensions/telegram/src/bot/helpers.ts @@ -9,6 +9,7 @@ import type { import { readChannelAllowFromStore } from "openclaw/plugin-sdk/conversation-runtime"; import { normalizeAccountId } from "openclaw/plugin-sdk/routing"; import { firstDefined, normalizeAllowFrom, type NormalizedAllowFrom } from "../bot-access.js"; +import { normalizeTelegramReplyToMessageId } from "../outbound-params.js"; import type { TelegramGetChat, TelegramStreamMode } from "./types.js"; const TELEGRAM_GENERAL_TOPIC_ID = 1; @@ -415,14 +416,7 @@ export function expandTextLinks(text: string, entities?: TelegramTextLinkEntity[ } export function resolveTelegramReplyId(raw?: string): number | undefined { - if (!raw) { - return undefined; - } - const parsed = Number(raw); - if (!Number.isFinite(parsed)) { - return undefined; - } - return parsed; + return normalizeTelegramReplyToMessageId(raw); } export type TelegramReplyTarget = { diff --git a/extensions/telegram/src/draft-stream.ts b/extensions/telegram/src/draft-stream.ts index d44122e14d0..51d37cee254 100644 --- a/extensions/telegram/src/draft-stream.ts +++ b/extensions/telegram/src/draft-stream.ts @@ -2,6 +2,7 @@ import type { Bot } from "grammy"; import { createFinalizableDraftLifecycle } from "openclaw/plugin-sdk/channel-lifecycle"; import { buildTelegramThreadParams, type TelegramThreadSpec } from "./bot/helpers.js"; import { isSafeToRetrySendError, isTelegramClientRejection } from "./network-errors.js"; +import { normalizeTelegramReplyToMessageId } from "./outbound-params.js"; const TELEGRAM_STREAM_MAX_CHARS = 4096; const DEFAULT_THROTTLE_MS = 1000; @@ -145,11 +146,12 @@ export function createTelegramDraftStream(params: { ? false : params.thread?.scope === "dm"; const threadParams = buildTelegramThreadParams(params.thread); + const replyToMessageId = normalizeTelegramReplyToMessageId(params.replyToMessageId); const replyParams = - params.replyToMessageId != null + replyToMessageId != null ? { ...threadParams, - reply_to_message_id: params.replyToMessageId, + reply_to_message_id: replyToMessageId, allow_sending_without_reply: true, } : threadParams; diff --git a/extensions/telegram/src/outbound-params.ts b/extensions/telegram/src/outbound-params.ts index 7dd3b7f1169..33126f8192c 100644 --- a/extensions/telegram/src/outbound-params.ts +++ b/extensions/telegram/src/outbound-params.ts @@ -1,11 +1,3 @@ -export function parseTelegramReplyToMessageId(replyToId?: string | null): number | undefined { - if (!replyToId) { - return undefined; - } - const parsed = Number.parseInt(replyToId, 10); - return Number.isFinite(parsed) ? parsed : undefined; -} - function parseIntegerId(value: string): number | undefined { if (!/^-?\d+$/.test(value)) { return undefined; @@ -14,6 +6,21 @@ function parseIntegerId(value: string): number | undefined { return Number.isFinite(parsed) ? parsed : undefined; } +export function normalizeTelegramReplyToMessageId(value: unknown): number | undefined { + if (typeof value === "number") { + return Number.isFinite(value) ? Math.trunc(value) : undefined; + } + if (typeof value !== "string") { + return undefined; + } + const trimmed = value.trim(); + return trimmed ? parseIntegerId(trimmed) : undefined; +} + +export function parseTelegramReplyToMessageId(replyToId?: string | null): number | undefined { + return normalizeTelegramReplyToMessageId(replyToId); +} + export function parseTelegramThreadId(threadId?: string | number | null): number | undefined { if (threadId == null) { return undefined; diff --git a/extensions/telegram/src/send.test.ts b/extensions/telegram/src/send.test.ts index 4d3b90eb891..9a3c8c364cf 100644 --- a/extensions/telegram/src/send.test.ts +++ b/extensions/telegram/src/send.test.ts @@ -1919,6 +1919,50 @@ describe("shared send behaviors", () => { } }); + it("omits invalid reply_to_message_id values before calling Telegram", async () => { + const invalidReplyToMessageIds = ["session-meta-id", "123abc", Number.NaN] as const; + + for (const invalidReplyToMessageId of invalidReplyToMessageIds) { + const chatId = "123"; + const sendMessage = vi.fn().mockResolvedValue({ + message_id: 56, + chat: { id: chatId }, + }); + const sendSticker = vi.fn().mockResolvedValue({ + message_id: 102, + chat: { id: chatId }, + }); + const api = { sendMessage, sendSticker } as unknown as { + sendMessage: typeof sendMessage; + sendSticker: typeof sendSticker; + }; + + await sendMessageTelegram(chatId, "reply text", { + token: "tok", + api, + replyToMessageId: invalidReplyToMessageId as unknown as number, + }); + await sendStickerTelegram(chatId, "CAACAgIAAxkBAAI...sticker_file_id", { + token: "tok", + api, + replyToMessageId: invalidReplyToMessageId as unknown as number, + }); + + expect(sendMessage, String(invalidReplyToMessageId)).toHaveBeenCalledWith( + chatId, + "reply text", + { + parse_mode: "HTML", + }, + ); + expect(sendSticker, String(invalidReplyToMessageId)).toHaveBeenCalledWith( + chatId, + "CAACAgIAAxkBAAI...sticker_file_id", + undefined, + ); + } + }); + it("wraps chat-not-found with actionable context", async () => { const cases = [ { diff --git a/extensions/telegram/src/send.ts b/extensions/telegram/src/send.ts index dc7082b5866..fa0f1607bde 100644 --- a/extensions/telegram/src/send.ts +++ b/extensions/telegram/src/send.ts @@ -42,6 +42,7 @@ import { normalizeTelegramLookupTarget, parseTelegramTarget, } from "./targets.js"; +import { normalizeTelegramReplyToMessageId } from "./outbound-params.js"; import { resolveTelegramVoiceSend } from "./voice.js"; type TelegramApi = Bot["api"]; @@ -416,8 +417,8 @@ function buildTelegramThreadReplyParams(params: { const threadIdParams = buildTelegramThreadParams(threadSpec); const threadParams: TelegramThreadReplyParams = threadIdParams ? { ...threadIdParams } : {}; - if (params.replyToMessageId != null) { - const replyToMessageId = Math.trunc(params.replyToMessageId); + const replyToMessageId = normalizeTelegramReplyToMessageId(params.replyToMessageId); + if (replyToMessageId != null) { if (params.quoteText?.trim()) { threadParams.reply_parameters = { message_id: replyToMessageId,