fix: validate telegram action integers

This commit is contained in:
Peter Steinberger
2026-05-28 18:46:17 -04:00
parent 444dd19a28
commit b0e9569ebd
5 changed files with 166 additions and 25 deletions

View File

@@ -407,6 +407,24 @@ describe("handleTelegramAction", () => {
expect(reactMessageTelegram).not.toHaveBeenCalled();
});
it("soft-fails fractional reaction message ids", async () => {
const result = await handleTelegramAction(
{
action: "react",
chatId: "123",
messageId: 456.5,
emoji: "✅",
},
reactionConfig("minimal"),
);
expect(resultDetails(result)).toMatchObject({
ok: false,
reason: "missing_message_id",
});
expect(reactMessageTelegram).not.toHaveBeenCalled();
});
it("removes reactions on empty emoji", async () => {
await handleTelegramAction(
{
@@ -482,6 +500,52 @@ describe("handleTelegramAction", () => {
expect(options.messageThreadId).toBe(11);
});
it("treats null primary id aliases as absent", async () => {
await handleTelegramAction(
{
action: "sendSticker",
to: "123",
fileId: "sticker",
replyToMessageId: null,
replyTo: 9,
messageThreadId: null,
threadId: 11,
},
telegramConfig({ actions: { sticker: true } }),
);
const call = mockCall(sendStickerTelegram, 0, "sticker null aliases");
const options = requireRecord(call[2], "sticker null alias options");
expect(options.replyToMessageId).toBe(9);
expect(options.messageThreadId).toBe(11);
});
it("rejects fractional Telegram thread and reply ids before sending", async () => {
await expect(
handleTelegramAction(
{
action: "sendMessage",
to: "123",
content: "hello",
replyToMessageId: 9.5,
},
telegramConfig(),
),
).rejects.toThrow("replyToMessageId must be a positive integer.");
await expect(
handleTelegramAction(
{
action: "sendSticker",
to: "123",
fileId: "sticker",
threadId: 11.5,
},
telegramConfig({ actions: { sticker: true } }),
),
).rejects.toThrow("threadId must be a positive integer.");
expect(sendDurableMessageBatch).not.toHaveBeenCalled();
expect(sendStickerTelegram).not.toHaveBeenCalled();
});
it("removes reactions when remove flag set", async () => {
const cfg = reactionConfig("extensive");
await handleTelegramAction(
@@ -991,6 +1055,22 @@ describe("handleTelegramAction", () => {
expect(details.pollId).toBe("poll-1");
});
it("rejects fractional poll durations before sending", async () => {
await expect(
handleTelegramAction(
{
action: "poll",
to: "@testchannel",
question: "Ready?",
answers: ["Yes", "No"],
durationSeconds: 60.5,
},
telegramConfig(),
),
).rejects.toThrow("durationSeconds must be a positive integer.");
expect(sendPollTelegram).not.toHaveBeenCalled();
});
it("accepts shared poll action aliases", async () => {
await handleTelegramAction(
{
@@ -1467,6 +1547,36 @@ describe("handleTelegramAction", () => {
expect(requireRecord(call[2], "delete message options").token).toBe("tok");
});
it("rejects fractional message ids before mutating messages", async () => {
const cfg = {
channels: { telegram: { botToken: "tok" } },
} as OpenClawConfig;
await expect(
handleTelegramAction(
{
action: "deleteMessage",
chatId: "123",
messageId: 456.5,
},
cfg,
),
).rejects.toThrow("messageId must be a positive integer.");
await expect(
handleTelegramAction(
{
action: "editMessage",
chatId: "123",
messageId: 456.5,
content: "updated",
},
cfg,
),
).rejects.toThrow("messageId must be a positive integer.");
expect(deleteMessageTelegram).not.toHaveBeenCalled();
expect(editMessageTelegram).not.toHaveBeenCalled();
});
it("surfaces non-fatal delete warnings", async () => {
deleteMessageTelegram.mockResolvedValueOnce({
ok: false,

View File

@@ -2,7 +2,7 @@ import type { AgentToolResult } from "openclaw/plugin-sdk/agent-core";
import { readBooleanParam } from "openclaw/plugin-sdk/boolean-param";
import {
jsonResult,
readNumberParam,
readPositiveIntegerParam,
readReactionParams,
readStringArrayParam,
readStringOrNumberParam,
@@ -97,7 +97,9 @@ type TelegramForumTopicIconColor = (typeof TELEGRAM_FORUM_TOPIC_ICON_COLORS)[num
function readTelegramForumTopicIconColor(
params: Record<string, unknown>,
): TelegramForumTopicIconColor | undefined {
const iconColor = readNumberParam(params, "iconColor", { integer: true });
const iconColor = readPositiveIntegerParam(params, "iconColor", {
message: "iconColor must be one of Telegram's supported forum topic colors.",
});
if (iconColor == null) {
return undefined;
}
@@ -124,8 +126,12 @@ function readTelegramChatId(params: Record<string, unknown>) {
function readTelegramThreadId(params: Record<string, unknown>) {
return (
readNumberParam(params, "messageThreadId", { integer: true }) ??
readNumberParam(params, "threadId", { integer: true })
readPositiveIntegerParam(params, "messageThreadId", {
message: "messageThreadId must be a positive integer.",
}) ??
readPositiveIntegerParam(params, "threadId", {
message: "threadId must be a positive integer.",
})
);
}
@@ -147,8 +153,12 @@ function formatTelegramDeliveryTarget(to: string, messageThreadId?: number | nul
function readTelegramReplyToMessageId(params: Record<string, unknown>) {
return (
readNumberParam(params, "replyToMessageId", { integer: true }) ??
readNumberParam(params, "replyTo", { integer: true })
readPositiveIntegerParam(params, "replyToMessageId", {
message: "replyToMessageId must be a positive integer.",
}) ??
readPositiveIntegerParam(params, "replyTo", {
message: "replyTo must be a positive integer.",
})
);
}
@@ -353,9 +363,19 @@ export async function handleTelegramAction(
});
}
const chatId = readTelegramChatId(params);
const messageId =
readNumberParam(params, "messageId", { integer: true }) ??
resolveReactionMessageId({ args: params });
let explicitMessageId: number | undefined;
try {
explicitMessageId = readPositiveIntegerParam(params, "messageId", {
message: "messageId must be a positive integer.",
});
} catch {
return jsonResult({
ok: false,
reason: "missing_message_id",
hint: "Telegram reaction requires a valid messageId (or inbound context fallback). Do not retry.",
});
}
const messageId = explicitMessageId ?? resolveReactionMessageId({ args: params });
if (typeof messageId !== "number" || !Number.isFinite(messageId) || messageId <= 0) {
return jsonResult({
ok: false,
@@ -547,16 +567,18 @@ export async function handleTelegramAction(
const allowMultiselect =
readBooleanParam(params, "allowMultiselect") ?? readBooleanParam(params, "pollMulti");
const durationSeconds =
readNumberParam(params, "durationSeconds", { integer: true }) ??
readNumberParam(params, "pollDurationSeconds", {
integer: true,
strict: true,
readPositiveIntegerParam(params, "durationSeconds", {
message: "durationSeconds must be a positive integer.",
}) ??
readPositiveIntegerParam(params, "pollDurationSeconds", {
message: "pollDurationSeconds must be a positive integer.",
});
const durationHours =
readNumberParam(params, "durationHours", { integer: true }) ??
readNumberParam(params, "pollDurationHours", {
integer: true,
strict: true,
readPositiveIntegerParam(params, "durationHours", {
message: "durationHours must be a positive integer.",
}) ??
readPositiveIntegerParam(params, "pollDurationHours", {
message: "pollDurationHours must be a positive integer.",
});
const replyToMessageId = readTelegramReplyToMessageId(params);
const messageThreadId = readTelegramThreadId(params);
@@ -607,10 +629,12 @@ export async function handleTelegramAction(
throw new Error("Telegram deleteMessage is disabled.");
}
const chatId = readTelegramChatId(params);
const messageId = readNumberParam(params, "messageId", {
required: true,
integer: true,
const messageId = readPositiveIntegerParam(params, "messageId", {
message: "messageId must be a positive integer.",
});
if (messageId === undefined) {
throw new Error("messageId required");
}
const token = resolveTelegramToken(cfg, { accountId }).token;
if (!token) {
throw new Error(
@@ -634,10 +658,12 @@ export async function handleTelegramAction(
throw new Error("Telegram editMessage is disabled.");
}
const chatId = readTelegramChatId(params);
const messageId = readNumberParam(params, "messageId", {
required: true,
integer: true,
const messageId = readPositiveIntegerParam(params, "messageId", {
message: "messageId must be a positive integer.",
});
if (messageId === undefined) {
throw new Error("messageId required");
}
const content =
readStringParam(params, "content", { allowEmpty: false }) ??
readStringParam(params, "message", { required: true, allowEmpty: false });
@@ -722,7 +748,10 @@ export async function handleTelegramAction(
);
}
const query = readStringParam(params, "query", { required: true });
const limit = readNumberParam(params, "limit", { integer: true }) ?? 5;
const limit =
readPositiveIntegerParam(params, "limit", {
message: "limit must be a positive integer.",
}) ?? 5;
const results = telegramActionRuntime.searchStickers(query, limit);
return jsonResult({
ok: true,

View File

@@ -94,6 +94,7 @@ describe("readNumberParam", () => {
it("throws for invalid present positive integer params", () => {
expect(readPositiveIntegerParam({ timeoutMs: "42" }, "timeoutMs")).toBe(42);
expect(readPositiveIntegerParam({ timeoutMs: null }, "timeoutMs")).toBeUndefined();
expect(() => readPositiveIntegerParam({ timeoutMs: "42.5" }, "timeoutMs")).toThrow(
"timeoutMs must be a positive integer",
);

View File

@@ -218,7 +218,7 @@ export function readPositiveIntegerParam(
positiveInteger: true,
strict: true,
});
if (value === undefined && readParamRaw(params, key) !== undefined) {
if (value === undefined && readParamRaw(params, key) != null) {
throw new ToolInputError(options.message ?? `${key} must be a positive integer`);
}
if (value !== undefined && options.max !== undefined && value > options.max) {

View File

@@ -13,6 +13,7 @@ export {
jsonResult,
parseAvailableTags,
readNumberParam,
readPositiveIntegerParam,
readReactionParams,
readStringArrayParam,
readStringOrNumberParam,