mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-31 03:41:51 +00:00
fix(telegram): validate replyToMessageId before sending to Telegram API (#56587)
Add shared normalizeTelegramReplyToMessageId() that rejects non-numeric, NaN, and mixed-content strings before they reach the Telegram Bot API. Apply at all four API sinks: direct send, bot delivery, draft stream, and bot helpers. Prevents GrammyError 400 when non-numeric values from session metadata slip through typed boundaries. Fixes #37222
This commit is contained in:
@@ -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<string, unknown> {
|
||||
const threadParams = buildTelegramThreadParams(opts?.thread);
|
||||
const params: Record<string, unknown> = {};
|
||||
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) {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = [
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user