From 6ab5c55716b619ad75375958048f7cc277191250 Mon Sep 17 00:00:00 2001 From: Alexander Bolshakov Date: Mon, 9 Mar 2026 21:21:02 +0100 Subject: [PATCH 01/12] telegram: support forum topic deletion in message delete action --- docs/channels/telegram.md | 2 + extensions/telegram/src/channel-actions.ts | 67 +++++++++++++++----- extensions/telegram/src/send.proxy.test.ts | 7 ++ extensions/telegram/src/send.test-harness.ts | 1 + extensions/telegram/src/send.test.ts | 33 ++++++++++ extensions/telegram/src/send.ts | 42 ++++++++++++ src/agents/tools/message-tool.ts | 6 ++ src/agents/tools/telegram-actions.test.ts | 41 ++++++++++++ src/agents/tools/telegram-actions.ts | 25 ++++++++ src/channels/plugins/actions/actions.test.ts | 49 ++++++++++++++ 10 files changed, 258 insertions(+), 15 deletions(-) diff --git a/docs/channels/telegram.md b/docs/channels/telegram.md index 37be3bf1111..890d59565f8 100644 --- a/docs/channels/telegram.md +++ b/docs/channels/telegram.md @@ -424,10 +424,12 @@ curl "https://api.telegram.org/bot/getUpdates" - `sendMessage` (`to`, `content`, optional `mediaUrl`, `replyToMessageId`, `messageThreadId`) - `react` (`chatId`, `messageId`, `emoji`) - `deleteMessage` (`chatId`, `messageId`) + - `deleteForumTopic` (`chatId`, `topicId`) - `editMessage` (`chatId`, `messageId`, `content`) - `createForumTopic` (`chatId`, `name`, optional `iconColor`, `iconCustomEmojiId`) Channel message actions expose ergonomic aliases (`send`, `react`, `delete`, `edit`, `sticker`, `sticker-search`, `topic-create`). + For `delete`, pass `messageId` to delete a message (existing behavior), or pass `threadId`/`topicId` to delete a forum topic. Gating controls: diff --git a/extensions/telegram/src/channel-actions.ts b/extensions/telegram/src/channel-actions.ts index 29095e7bc7c..e64b5c9c06c 100644 --- a/extensions/telegram/src/channel-actions.ts +++ b/extensions/telegram/src/channel-actions.ts @@ -24,6 +24,7 @@ import { resolveTelegramPollActionGateState, } from "./accounts.js"; import { isTelegramInlineButtonsEnabled } from "./inline-buttons.js"; +import { parseTelegramTarget } from "./targets.js"; const providerId = "telegram"; @@ -62,15 +63,33 @@ function readTelegramChatIdParam(params: Record): string | numb ); } -function readTelegramMessageIdParam(params: Record): number { +function readTelegramMessageIdParam( + params: Record, + options?: { required?: boolean }, +): number | undefined { + const required = options?.required ?? true; const messageId = readNumberParam(params, "messageId", { - required: true, + required, integer: true, }); - if (typeof messageId !== "number") { + if (required && typeof messageId !== "number") { throw new Error("messageId is required."); } - return messageId; + return typeof messageId === "number" ? messageId : undefined; +} + +function readTelegramTopicIdParam(params: Record): number | undefined { + const explicitTopicId = + readNumberParam(params, "topicId", { integer: true }) ?? + readNumberParam(params, "threadId", { integer: true }); + if (typeof explicitTopicId === "number") { + return explicitTopicId; + } + const targetLike = readStringParam(params, "to") ?? readStringParam(params, "chatId"); + if (!targetLike) { + return undefined; + } + return parseTelegramTarget(targetLike).messageThreadId; } export const telegramMessageActions: ChannelMessageActionAdapter = { @@ -202,17 +221,35 @@ export const telegramMessageActions: ChannelMessageActionAdapter = { if (action === "delete") { const chatId = readTelegramChatIdParam(params); - const messageId = readTelegramMessageIdParam(params); - return await handleTelegramAction( - { - action: "deleteMessage", - chatId, - messageId, - accountId: accountId ?? undefined, - }, - cfg, - { mediaLocalRoots }, - ); + const messageId = readTelegramMessageIdParam(params, { required: false }); + if (typeof messageId === "number") { + return await handleTelegramAction( + { + action: "deleteMessage", + chatId, + messageId, + accountId: accountId ?? undefined, + }, + cfg, + { mediaLocalRoots }, + ); + } + + const topicId = readTelegramTopicIdParam(params); + if (typeof topicId === "number") { + return await handleTelegramAction( + { + action: "deleteForumTopic", + chatId, + topicId, + accountId: accountId ?? undefined, + }, + cfg, + { mediaLocalRoots }, + ); + } + + throw new Error("messageId or threadId/topicId is required."); } if (action === "edit") { diff --git a/extensions/telegram/src/send.proxy.test.ts b/extensions/telegram/src/send.proxy.test.ts index 6c17b33fe38..89327b64250 100644 --- a/extensions/telegram/src/send.proxy.test.ts +++ b/extensions/telegram/src/send.proxy.test.ts @@ -5,6 +5,7 @@ const { botApi, botCtorSpy } = vi.hoisted(() => ({ sendMessage: vi.fn(), setMessageReaction: vi.fn(), deleteMessage: vi.fn(), + deleteForumTopic: vi.fn(), }, botCtorSpy: vi.fn(), })); @@ -52,6 +53,7 @@ vi.mock("grammy", () => ({ })); import { + deleteForumTopicTelegram, deleteMessageTelegram, reactMessageTelegram, resetTelegramClientOptionsCacheForTests, @@ -86,6 +88,7 @@ describe("telegram proxy client", () => { botApi.sendMessage.mockResolvedValue({ message_id: 1, chat: { id: "123" } }); botApi.setMessageReaction.mockResolvedValue(undefined); botApi.deleteMessage.mockResolvedValue(true); + botApi.deleteForumTopic.mockResolvedValue(true); botCtorSpy.mockClear(); loadConfig.mockReturnValue({ channels: { telegram: { accounts: { foo: { proxy: proxyUrl } } } }, @@ -134,6 +137,10 @@ describe("telegram proxy client", () => { name: "deleteMessage", run: () => deleteMessageTelegram("123", "456", { token: "tok", accountId: "foo" }), }, + { + name: "deleteForumTopic", + run: () => deleteForumTopicTelegram("123", "456", { token: "tok", accountId: "foo" }), + }, ])("uses proxy fetch for $name", async (testCase) => { const { fetchImpl } = prepareProxyFetch(); diff --git a/extensions/telegram/src/send.test-harness.ts b/extensions/telegram/src/send.test-harness.ts index 6d53a3d20e7..dbbd6c2ce0a 100644 --- a/extensions/telegram/src/send.test-harness.ts +++ b/extensions/telegram/src/send.test-harness.ts @@ -4,6 +4,7 @@ import type { MockFn } from "../../../src/test-utils/vitest-mock-fn.js"; const { botApi, botCtorSpy } = vi.hoisted(() => ({ botApi: { deleteMessage: vi.fn(), + deleteForumTopic: vi.fn(), editMessageText: vi.fn(), sendChatAction: vi.fn(), sendMessage: vi.fn(), diff --git a/extensions/telegram/src/send.test.ts b/extensions/telegram/src/send.test.ts index 8dc4aff0c2d..c4c5da89ffd 100644 --- a/extensions/telegram/src/send.test.ts +++ b/extensions/telegram/src/send.test.ts @@ -15,6 +15,7 @@ const { botApi, botCtorSpy, loadConfig, loadWebMedia, maybePersistResolvedTelegr const { buildInlineKeyboard, createForumTopicTelegram, + deleteForumTopicTelegram, editMessageTelegram, reactMessageTelegram, sendMessageTelegram, @@ -1953,6 +1954,38 @@ describe("sendPollTelegram", () => { }); }); +describe("deleteForumTopicTelegram", () => { + const cases = [ + { + name: "uses base chat id when target includes topic suffix", + target: "telegram:group:-1001234567890:topic:271", + topicId: 271, + expectedCall: ["-1001234567890", 271] as const, + }, + { + name: "accepts plain chat ids", + target: "-1001234567890", + topicId: 300, + expectedCall: ["-1001234567890", 300] as const, + }, + ] as const; + + for (const testCase of cases) { + it(testCase.name, async () => { + const deleteForumTopic = vi.fn().mockResolvedValue(true); + const api = { deleteForumTopic } as unknown as Bot["api"]; + + const result = await deleteForumTopicTelegram(testCase.target, testCase.topicId, { + token: "tok", + api, + }); + + expect(deleteForumTopic).toHaveBeenCalledWith(...testCase.expectedCall); + expect(result).toEqual({ ok: true }); + }); + } +}); + describe("createForumTopicTelegram", () => { const cases = [ { diff --git a/extensions/telegram/src/send.ts b/extensions/telegram/src/send.ts index e7d2c48e9fc..1914f29905c 100644 --- a/extensions/telegram/src/send.ts +++ b/extensions/telegram/src/send.ts @@ -1438,6 +1438,48 @@ export async function sendPollTelegram( return { messageId: String(messageId), chatId: resolvedChatId, pollId }; } +// --------------------------------------------------------------------------- +// Forum topic deletion +// --------------------------------------------------------------------------- + +type TelegramDeleteForumTopicOpts = { + token?: string; + accountId?: string; + verbose?: boolean; + api?: TelegramApiOverride; + retry?: RetryConfig; +}; + +export async function deleteForumTopicTelegram( + chatIdInput: string | number, + topicIdInput: string | number, + opts: TelegramDeleteForumTopicOpts = {}, +): Promise<{ ok: true }> { + const { cfg, account, api } = resolveTelegramApiContext(opts); + const parsedTarget = parseTelegramTarget(String(chatIdInput)); + const chatId = await resolveAndPersistChatId({ + cfg, + api, + lookupTarget: parsedTarget.chatId, + persistTarget: String(chatIdInput), + verbose: opts.verbose, + }); + const topicId = normalizeMessageId(topicIdInput); + const requestWithDiag = createTelegramRequestWithDiag({ + cfg, + account, + retry: opts.retry, + verbose: opts.verbose, + shouldRetry: (err) => isRecoverableTelegramNetworkError(err, { context: "send" }), + }); + if (typeof api.deleteForumTopic !== "function") { + throw new Error("Telegram forum topic deletion is unavailable in this bot API."); + } + await requestWithDiag(() => api.deleteForumTopic(chatId, topicId), "deleteForumTopic"); + logVerbose(`[telegram] Deleted forum topic ${topicId} from chat ${chatId}`); + return { ok: true }; +} + // --------------------------------------------------------------------------- // Forum topic creation // --------------------------------------------------------------------------- diff --git a/src/agents/tools/message-tool.ts b/src/agents/tools/message-tool.ts index 63963ab5f38..b69727e081d 100644 --- a/src/agents/tools/message-tool.ts +++ b/src/agents/tools/message-tool.ts @@ -194,6 +194,12 @@ function buildSendSchema(options: { filePath: Type.Optional(Type.String()), replyTo: Type.Optional(Type.String()), threadId: Type.Optional(Type.String()), + topicId: Type.Optional( + Type.String({ + description: + "Topic/thread id alias for forum-style channels (e.g., Telegram). Supported for topic deletion paths.", + }), + ), asVoice: Type.Optional(Type.Boolean()), silent: Type.Optional(Type.Boolean()), quoteText: Type.Optional( diff --git a/src/agents/tools/telegram-actions.test.ts b/src/agents/tools/telegram-actions.test.ts index 5963a64b667..18675c5ddfa 100644 --- a/src/agents/tools/telegram-actions.test.ts +++ b/src/agents/tools/telegram-actions.test.ts @@ -28,6 +28,7 @@ const createForumTopicTelegram = vi.fn(async () => ({ name: "Topic", chatId: "123", })); +const deleteForumTopicTelegram = vi.fn(async () => ({ ok: true })); let envSnapshot: ReturnType; vi.mock("../../../extensions/telegram/src/send.js", () => ({ @@ -44,6 +45,8 @@ vi.mock("../../../extensions/telegram/src/send.js", () => ({ editMessageTelegram(...args), createForumTopicTelegram: (...args: Parameters) => createForumTopicTelegram(...args), + deleteForumTopicTelegram: (...args: Parameters) => + deleteForumTopicTelegram(...args), })); describe("handleTelegramAction", () => { @@ -106,6 +109,7 @@ describe("handleTelegramAction", () => { deleteMessageTelegram.mockClear(); editMessageTelegram.mockClear(); createForumTopicTelegram.mockClear(); + deleteForumTopicTelegram.mockClear(); process.env.TELEGRAM_BOT_TOKEN = "tok"; }); @@ -594,6 +598,43 @@ describe("handleTelegramAction", () => { ); }); + it("deletes a forum topic", async () => { + const cfg = { + channels: { telegram: { botToken: "tok" } }, + } as OpenClawConfig; + await handleTelegramAction( + { + action: "deleteForumTopic", + chatId: "-100123", + topicId: 271, + }, + cfg, + ); + expect(deleteForumTopicTelegram).toHaveBeenCalledWith( + "-100123", + 271, + expect.objectContaining({ token: "tok" }), + ); + }); + + it("respects createForumTopic gating for deleteForumTopic", async () => { + const cfg = { + channels: { + telegram: { botToken: "tok", actions: { createForumTopic: false } }, + }, + } as OpenClawConfig; + await expect( + handleTelegramAction( + { + action: "deleteForumTopic", + chatId: "-100123", + topicId: 271, + }, + cfg, + ), + ).rejects.toThrow(/Telegram createForumTopic is disabled/); + }); + it("respects deleteMessage gating", async () => { const cfg = { channels: { diff --git a/src/agents/tools/telegram-actions.ts b/src/agents/tools/telegram-actions.ts index 6c8d4f84204..2f81d208b41 100644 --- a/src/agents/tools/telegram-actions.ts +++ b/src/agents/tools/telegram-actions.ts @@ -14,6 +14,7 @@ import { import { resolveTelegramReactionLevel } from "../../../extensions/telegram/src/reaction-level.js"; import { createForumTopicTelegram, + deleteForumTopicTelegram, deleteMessageTelegram, editMessageTelegram, reactMessageTelegram, @@ -341,6 +342,30 @@ export async function handleTelegramAction( return jsonResult({ ok: true, deleted: true }); } + if (action === "deleteForumTopic") { + if (!isActionEnabled("createForumTopic")) { + throw new Error("Telegram createForumTopic is disabled."); + } + const chatId = readStringOrNumberParam(params, "chatId", { + required: true, + }); + const topicId = readNumberParam(params, "topicId", { + required: true, + integer: true, + }); + const token = resolveTelegramToken(cfg, { accountId }).token; + if (!token) { + throw new Error( + "Telegram bot token missing. Set TELEGRAM_BOT_TOKEN or channels.telegram.botToken.", + ); + } + await deleteForumTopicTelegram(chatId ?? "", topicId ?? 0, { + token, + accountId: accountId ?? undefined, + }); + return jsonResult({ ok: true, deleted: true, topicId }); + } + if (action === "editMessage") { if (!isActionEnabled("editMessage")) { throw new Error("Telegram editMessage is disabled."); diff --git a/src/channels/plugins/actions/actions.test.ts b/src/channels/plugins/actions/actions.test.ts index 055d660524f..4f6f904ba97 100644 --- a/src/channels/plugins/actions/actions.test.ts +++ b/src/channels/plugins/actions/actions.test.ts @@ -777,6 +777,34 @@ describe("telegramMessageActions", () => { accountId: undefined, }, }, + { + name: "delete maps to deleteMessage when messageId is provided", + action: "delete" as const, + params: { + to: "-1001234567890", + messageId: 42, + }, + expectedPayload: { + action: "deleteMessage", + chatId: "-1001234567890", + messageId: 42, + accountId: undefined, + }, + }, + { + name: "delete maps to deleteForumTopic when threadId is provided", + action: "delete" as const, + params: { + to: "-1001234567890", + threadId: 271, + }, + expectedPayload: { + action: "deleteForumTopic", + chatId: "-1001234567890", + topicId: 271, + accountId: undefined, + }, + }, { name: "topic-create maps to createForumTopic", action: "topic-create" as const, @@ -806,6 +834,27 @@ describe("telegramMessageActions", () => { } }); + it("rejects delete when neither messageId nor topic/thread id is provided", async () => { + const cfg = telegramCfg(); + const handleAction = telegramMessageActions.handleAction; + if (!handleAction) { + throw new Error("telegram handleAction unavailable"); + } + + await expect( + handleAction({ + channel: "telegram", + action: "delete", + params: { + to: "-1001234567890", + }, + cfg, + }), + ).rejects.toThrow(/messageId or threadId\/topicId is required/i); + + expect(handleTelegramAction).not.toHaveBeenCalled(); + }); + it("forwards trusted mediaLocalRoots for send", async () => { const cfg = telegramCfg(); await telegramMessageActions.handleAction?.({ From f70d58cd9da0c5f7d1eb502955a51326cb2b4ad3 Mon Sep 17 00:00:00 2001 From: Alexander Bolshakov Date: Mon, 9 Mar 2026 21:41:24 +0100 Subject: [PATCH 02/12] telegram: add dedicated topic-delete action --- docs/channels/telegram.md | 8 ++-- extensions/telegram/src/channel-actions.ts | 35 ++++++++++------ src/agents/tools/message-tool.test.ts | 6 ++- src/agents/tools/telegram-actions.test.ts | 6 +-- src/agents/tools/telegram-actions.ts | 6 ++- src/channels/plugins/actions/actions.test.ts | 43 ++++++++++++++++++-- src/channels/plugins/message-action-names.ts | 1 + src/infra/outbound/message-action-spec.ts | 1 + 8 files changed, 80 insertions(+), 26 deletions(-) diff --git a/docs/channels/telegram.md b/docs/channels/telegram.md index 890d59565f8..d28b6c2b798 100644 --- a/docs/channels/telegram.md +++ b/docs/channels/telegram.md @@ -428,8 +428,10 @@ curl "https://api.telegram.org/bot/getUpdates" - `editMessage` (`chatId`, `messageId`, `content`) - `createForumTopic` (`chatId`, `name`, optional `iconColor`, `iconCustomEmojiId`) - Channel message actions expose ergonomic aliases (`send`, `react`, `delete`, `edit`, `sticker`, `sticker-search`, `topic-create`). - For `delete`, pass `messageId` to delete a message (existing behavior), or pass `threadId`/`topicId` to delete a forum topic. + Channel message actions expose ergonomic aliases (`send`, `react`, `delete`, `topic-delete`, `edit`, `sticker`, `sticker-search`, `topic-create`). + - `delete`: deletes a message and requires `messageId`. + - `topic-delete`: deletes a forum topic and requires explicit `threadId`/`topicId`. + Backward compatibility: `delete` with explicit `threadId`/`topicId` is still accepted for now. Gating controls: @@ -971,7 +973,7 @@ Telegram-specific high-signal fields: - formatting/delivery: `textChunkLimit`, `chunkMode`, `linkPreview`, `responsePrefix` - media/network: `mediaMaxMb`, `timeoutSeconds`, `retry`, `network.autoSelectFamily`, `proxy` - webhook: `webhookUrl`, `webhookSecret`, `webhookPath`, `webhookHost` -- actions/capabilities: `capabilities.inlineButtons`, `actions.sendMessage|editMessage|deleteMessage|reactions|sticker` +- actions/capabilities: `capabilities.inlineButtons`, `actions.sendMessage|editMessage|deleteMessage|reactions|sticker` (both `delete` and `topic-delete` route through `actions.deleteMessage`) - reactions: `reactionNotifications`, `reactionLevel` - writes/history: `configWrites`, `historyLimit`, `dmHistoryLimit`, `dms.*.historyLimit` diff --git a/extensions/telegram/src/channel-actions.ts b/extensions/telegram/src/channel-actions.ts index e64b5c9c06c..a0351aaac62 100644 --- a/extensions/telegram/src/channel-actions.ts +++ b/extensions/telegram/src/channel-actions.ts @@ -24,7 +24,6 @@ import { resolveTelegramPollActionGateState, } from "./accounts.js"; import { isTelegramInlineButtonsEnabled } from "./inline-buttons.js"; -import { parseTelegramTarget } from "./targets.js"; const providerId = "telegram"; @@ -79,17 +78,10 @@ function readTelegramMessageIdParam( } function readTelegramTopicIdParam(params: Record): number | undefined { - const explicitTopicId = + return ( readNumberParam(params, "topicId", { integer: true }) ?? - readNumberParam(params, "threadId", { integer: true }); - if (typeof explicitTopicId === "number") { - return explicitTopicId; - } - const targetLike = readStringParam(params, "to") ?? readStringParam(params, "chatId"); - if (!targetLike) { - return undefined; - } - return parseTelegramTarget(targetLike).messageThreadId; + readNumberParam(params, "threadId", { integer: true }) + ); } export const telegramMessageActions: ChannelMessageActionAdapter = { @@ -123,6 +115,7 @@ export const telegramMessageActions: ChannelMessageActionAdapter = { } if (isEnabled("deleteMessage")) { actions.add("delete"); + actions.add("topic-delete"); } if (isEnabled("editMessage")) { actions.add("edit"); @@ -249,7 +242,25 @@ export const telegramMessageActions: ChannelMessageActionAdapter = { ); } - throw new Error("messageId or threadId/topicId is required."); + throw new Error("messageId is required for action=delete."); + } + + if (action === "topic-delete") { + const chatId = readTelegramChatIdParam(params); + const topicId = readTelegramTopicIdParam(params); + if (typeof topicId !== "number") { + throw new Error("threadId/topicId is required for action=topic-delete."); + } + return await handleTelegramAction( + { + action: "deleteForumTopic", + chatId, + topicId, + accountId: accountId ?? undefined, + }, + cfg, + { mediaLocalRoots }, + ); } if (action === "edit") { diff --git a/src/agents/tools/message-tool.test.ts b/src/agents/tools/message-tool.test.ts index 930f8d95a25..29ee69013f5 100644 --- a/src/agents/tools/message-tool.test.ts +++ b/src/agents/tools/message-tool.test.ts @@ -353,7 +353,7 @@ describe("message tool description", () => { label: "Telegram", docsPath: "/channels/telegram", blurb: "Telegram test plugin.", - actions: ["send", "react", "delete", "edit", "topic-create"], + actions: ["send", "react", "delete", "edit", "topic-create", "topic-delete"], }); setActivePluginRegistry( @@ -372,7 +372,9 @@ describe("message tool description", () => { expect(tool.description).toContain("Current channel (signal) supports: react, send."); // Other configured channels are also listed expect(tool.description).toContain("Other configured channels:"); - expect(tool.description).toContain("telegram (delete, edit, react, send, topic-create)"); + expect(tool.description).toContain( + "telegram (delete, edit, react, send, topic-create, topic-delete)", + ); }); it("does not include 'Other configured channels' when only one channel is configured", () => { diff --git a/src/agents/tools/telegram-actions.test.ts b/src/agents/tools/telegram-actions.test.ts index 18675c5ddfa..02d918bb64d 100644 --- a/src/agents/tools/telegram-actions.test.ts +++ b/src/agents/tools/telegram-actions.test.ts @@ -617,10 +617,10 @@ describe("handleTelegramAction", () => { ); }); - it("respects createForumTopic gating for deleteForumTopic", async () => { + it("respects deleteMessage gating for deleteForumTopic", async () => { const cfg = { channels: { - telegram: { botToken: "tok", actions: { createForumTopic: false } }, + telegram: { botToken: "tok", actions: { deleteMessage: false } }, }, } as OpenClawConfig; await expect( @@ -632,7 +632,7 @@ describe("handleTelegramAction", () => { }, cfg, ), - ).rejects.toThrow(/Telegram createForumTopic is disabled/); + ).rejects.toThrow(/Telegram forum topic deletion is disabled/); }); it("respects deleteMessage gating", async () => { diff --git a/src/agents/tools/telegram-actions.ts b/src/agents/tools/telegram-actions.ts index 2f81d208b41..4f812f5f8f6 100644 --- a/src/agents/tools/telegram-actions.ts +++ b/src/agents/tools/telegram-actions.ts @@ -343,8 +343,10 @@ export async function handleTelegramAction( } if (action === "deleteForumTopic") { - if (!isActionEnabled("createForumTopic")) { - throw new Error("Telegram createForumTopic is disabled."); + if (!isActionEnabled("deleteMessage")) { + throw new Error( + "Telegram forum topic deletion is disabled. Set channels.telegram.actions.deleteMessage to true.", + ); } const chatId = readStringOrNumberParam(params, "chatId", { required: true, diff --git a/src/channels/plugins/actions/actions.test.ts b/src/channels/plugins/actions/actions.test.ts index 4f6f904ba97..21f1e720e0e 100644 --- a/src/channels/plugins/actions/actions.test.ts +++ b/src/channels/plugins/actions/actions.test.ts @@ -792,8 +792,8 @@ describe("telegramMessageActions", () => { }, }, { - name: "delete maps to deleteForumTopic when threadId is provided", - action: "delete" as const, + name: "topic-delete maps to deleteForumTopic when threadId is provided", + action: "topic-delete" as const, params: { to: "-1001234567890", threadId: 271, @@ -805,6 +805,20 @@ describe("telegramMessageActions", () => { accountId: undefined, }, }, + { + name: "delete keeps backward compatibility for explicit topic ids", + action: "delete" as const, + params: { + to: "-1001234567890", + topicId: 271, + }, + expectedPayload: { + action: "deleteForumTopic", + chatId: "-1001234567890", + topicId: 271, + accountId: undefined, + }, + }, { name: "topic-create maps to createForumTopic", action: "topic-create" as const, @@ -834,7 +848,7 @@ describe("telegramMessageActions", () => { } }); - it("rejects delete when neither messageId nor topic/thread id is provided", async () => { + it("rejects delete when messageId is not provided", async () => { const cfg = telegramCfg(); const handleAction = telegramMessageActions.handleAction; if (!handleAction) { @@ -850,7 +864,28 @@ describe("telegramMessageActions", () => { }, cfg, }), - ).rejects.toThrow(/messageId or threadId\/topicId is required/i); + ).rejects.toThrow(/messageId is required for action=delete/i); + + expect(handleTelegramAction).not.toHaveBeenCalled(); + }); + + it("rejects topic-delete when threadId/topicId is missing", async () => { + const cfg = telegramCfg(); + const handleAction = telegramMessageActions.handleAction; + if (!handleAction) { + throw new Error("telegram handleAction unavailable"); + } + + await expect( + handleAction({ + channel: "telegram", + action: "topic-delete", + params: { + to: "-1001234567890", + }, + cfg, + }), + ).rejects.toThrow(/threadId\/topicId is required for action=topic-delete/i); expect(handleTelegramAction).not.toHaveBeenCalled(); }); diff --git a/src/channels/plugins/message-action-names.ts b/src/channels/plugins/message-action-names.ts index 809d239be2c..ee9df49aef9 100644 --- a/src/channels/plugins/message-action-names.ts +++ b/src/channels/plugins/message-action-names.ts @@ -44,6 +44,7 @@ export const CHANNEL_MESSAGE_ACTION_NAMES = [ "category-edit", "category-delete", "topic-create", + "topic-delete", "voice-status", "event-list", "event-create", diff --git a/src/infra/outbound/message-action-spec.ts b/src/infra/outbound/message-action-spec.ts index b49a60c6991..1ec1854ed3c 100644 --- a/src/infra/outbound/message-action-spec.ts +++ b/src/infra/outbound/message-action-spec.ts @@ -49,6 +49,7 @@ export const MESSAGE_ACTION_TARGET_MODE: Record Date: Mon, 9 Mar 2026 22:35:17 +0100 Subject: [PATCH 03/12] telegram: harden delete fallback and bump googlechat peer floor --- extensions/telegram/src/channel-actions.ts | 4 ++++ src/channels/plugins/actions/actions.test.ts | 23 ++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/extensions/telegram/src/channel-actions.ts b/extensions/telegram/src/channel-actions.ts index a0351aaac62..8cb8ac43934 100644 --- a/extensions/telegram/src/channel-actions.ts +++ b/extensions/telegram/src/channel-actions.ts @@ -214,7 +214,11 @@ export const telegramMessageActions: ChannelMessageActionAdapter = { if (action === "delete") { const chatId = readTelegramChatIdParam(params); + const hasMessageIdParam = Object.hasOwn(params, "messageId"); const messageId = readTelegramMessageIdParam(params, { required: false }); + if (hasMessageIdParam && typeof messageId !== "number") { + throw new Error("messageId must be a valid number for action=delete."); + } if (typeof messageId === "number") { return await handleTelegramAction( { diff --git a/src/channels/plugins/actions/actions.test.ts b/src/channels/plugins/actions/actions.test.ts index 21f1e720e0e..45b35ac7738 100644 --- a/src/channels/plugins/actions/actions.test.ts +++ b/src/channels/plugins/actions/actions.test.ts @@ -869,6 +869,29 @@ describe("telegramMessageActions", () => { expect(handleTelegramAction).not.toHaveBeenCalled(); }); + it("rejects invalid delete messageId instead of falling back to topic deletion", async () => { + const cfg = telegramCfg(); + const handleAction = telegramMessageActions.handleAction; + if (!handleAction) { + throw new Error("telegram handleAction unavailable"); + } + + await expect( + handleAction({ + channel: "telegram", + action: "delete", + params: { + to: "-1001234567890", + messageId: "oops", + topicId: 271, + }, + cfg, + }), + ).rejects.toThrow(/messageId must be a valid number for action=delete/i); + + expect(handleTelegramAction).not.toHaveBeenCalled(); + }); + it("rejects topic-delete when threadId/topicId is missing", async () => { const cfg = telegramCfg(); const handleAction = telegramMessageActions.handleAction; From a280f31a435a02c435f9734b92b730971bdfea94 Mon Sep 17 00:00:00 2001 From: Alexander Bolshakov Date: Mon, 9 Mar 2026 22:50:10 +0100 Subject: [PATCH 04/12] telegram: require strict integer ids for destructive paths --- extensions/telegram/src/channel-actions.ts | 5 +++-- src/channels/plugins/actions/actions.test.ts | 22 ++++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/extensions/telegram/src/channel-actions.ts b/extensions/telegram/src/channel-actions.ts index 8cb8ac43934..50f7826da70 100644 --- a/extensions/telegram/src/channel-actions.ts +++ b/extensions/telegram/src/channel-actions.ts @@ -70,6 +70,7 @@ function readTelegramMessageIdParam( const messageId = readNumberParam(params, "messageId", { required, integer: true, + strict: true, }); if (required && typeof messageId !== "number") { throw new Error("messageId is required."); @@ -79,8 +80,8 @@ function readTelegramMessageIdParam( function readTelegramTopicIdParam(params: Record): number | undefined { return ( - readNumberParam(params, "topicId", { integer: true }) ?? - readNumberParam(params, "threadId", { integer: true }) + readNumberParam(params, "topicId", { integer: true, strict: true }) ?? + readNumberParam(params, "threadId", { integer: true, strict: true }) ); } diff --git a/src/channels/plugins/actions/actions.test.ts b/src/channels/plugins/actions/actions.test.ts index 45b35ac7738..b9e8f8fdb55 100644 --- a/src/channels/plugins/actions/actions.test.ts +++ b/src/channels/plugins/actions/actions.test.ts @@ -913,6 +913,28 @@ describe("telegramMessageActions", () => { expect(handleTelegramAction).not.toHaveBeenCalled(); }); + it("rejects non-integer topic-delete ids before telegram-actions", async () => { + const cfg = telegramCfg(); + const handleAction = telegramMessageActions.handleAction; + if (!handleAction) { + throw new Error("telegram handleAction unavailable"); + } + + await expect( + handleAction({ + channel: "telegram", + action: "topic-delete", + params: { + to: "-1001234567890", + topicId: "271abc", + }, + cfg, + }), + ).rejects.toThrow(/threadId\/topicId is required for action=topic-delete/i); + + expect(handleTelegramAction).not.toHaveBeenCalled(); + }); + it("forwards trusted mediaLocalRoots for send", async () => { const cfg = telegramCfg(); await telegramMessageActions.handleAction?.({ From b5bdfd8b6ae3c8cfd025f9a42d340d486ed1cf02 Mon Sep 17 00:00:00 2001 From: Alexander Bolshakov Date: Mon, 9 Mar 2026 23:21:37 +0100 Subject: [PATCH 05/12] telegram: strict topic ids in tool action and relax peer pins --- .../OpenClawProtocol/GatewayModels.swift | 96 ------------------- src/agents/tools/telegram-actions.test.ts | 32 +++++++ src/agents/tools/telegram-actions.ts | 2 + 3 files changed, 34 insertions(+), 96 deletions(-) diff --git a/apps/macos/Sources/OpenClawProtocol/GatewayModels.swift b/apps/macos/Sources/OpenClawProtocol/GatewayModels.swift index 3003ae79f7b..aaac880619d 100644 --- a/apps/macos/Sources/OpenClawProtocol/GatewayModels.swift +++ b/apps/macos/Sources/OpenClawProtocol/GatewayModels.swift @@ -942,102 +942,6 @@ public struct NodeEventParams: Codable, Sendable { } } -public struct NodePendingDrainParams: Codable, Sendable { - public let maxitems: Int? - - public init( - maxitems: Int?) - { - self.maxitems = maxitems - } - - private enum CodingKeys: String, CodingKey { - case maxitems = "maxItems" - } -} - -public struct NodePendingDrainResult: Codable, Sendable { - public let nodeid: String - public let revision: Int - public let items: [[String: AnyCodable]] - public let hasmore: Bool - - public init( - nodeid: String, - revision: Int, - items: [[String: AnyCodable]], - hasmore: Bool) - { - self.nodeid = nodeid - self.revision = revision - self.items = items - self.hasmore = hasmore - } - - private enum CodingKeys: String, CodingKey { - case nodeid = "nodeId" - case revision - case items - case hasmore = "hasMore" - } -} - -public struct NodePendingEnqueueParams: Codable, Sendable { - public let nodeid: String - public let type: String - public let priority: String? - public let expiresinms: Int? - public let wake: Bool? - - public init( - nodeid: String, - type: String, - priority: String?, - expiresinms: Int?, - wake: Bool?) - { - self.nodeid = nodeid - self.type = type - self.priority = priority - self.expiresinms = expiresinms - self.wake = wake - } - - private enum CodingKeys: String, CodingKey { - case nodeid = "nodeId" - case type - case priority - case expiresinms = "expiresInMs" - case wake - } -} - -public struct NodePendingEnqueueResult: Codable, Sendable { - public let nodeid: String - public let revision: Int - public let queued: [String: AnyCodable] - public let waketriggered: Bool - - public init( - nodeid: String, - revision: Int, - queued: [String: AnyCodable], - waketriggered: Bool) - { - self.nodeid = nodeid - self.revision = revision - self.queued = queued - self.waketriggered = waketriggered - } - - private enum CodingKeys: String, CodingKey { - case nodeid = "nodeId" - case revision - case queued - case waketriggered = "wakeTriggered" - } -} - public struct NodeInvokeRequestEvent: Codable, Sendable { public let id: String public let nodeid: String diff --git a/src/agents/tools/telegram-actions.test.ts b/src/agents/tools/telegram-actions.test.ts index 02d918bb64d..c8c961ffcc8 100644 --- a/src/agents/tools/telegram-actions.test.ts +++ b/src/agents/tools/telegram-actions.test.ts @@ -598,6 +598,22 @@ describe("handleTelegramAction", () => { ); }); + it("rejects malformed message ids for deleteMessage", async () => { + const cfg = { + channels: { telegram: { botToken: "tok" } }, + } as OpenClawConfig; + await expect( + handleTelegramAction( + { + action: "deleteMessage", + chatId: "123", + messageId: "456oops", + }, + cfg, + ), + ).rejects.toThrow(/messageId required/); + }); + it("deletes a forum topic", async () => { const cfg = { channels: { telegram: { botToken: "tok" } }, @@ -617,6 +633,22 @@ describe("handleTelegramAction", () => { ); }); + it("rejects malformed topic ids for deleteForumTopic", async () => { + const cfg = { + channels: { telegram: { botToken: "tok" } }, + } as OpenClawConfig; + await expect( + handleTelegramAction( + { + action: "deleteForumTopic", + chatId: "-100123", + topicId: "271abc", + }, + cfg, + ), + ).rejects.toThrow(/topicId required/); + }); + it("respects deleteMessage gating for deleteForumTopic", async () => { const cfg = { channels: { diff --git a/src/agents/tools/telegram-actions.ts b/src/agents/tools/telegram-actions.ts index 4f812f5f8f6..ec270ab32b0 100644 --- a/src/agents/tools/telegram-actions.ts +++ b/src/agents/tools/telegram-actions.ts @@ -327,6 +327,7 @@ export async function handleTelegramAction( const messageId = readNumberParam(params, "messageId", { required: true, integer: true, + strict: true, }); const token = resolveTelegramToken(cfg, { accountId }).token; if (!token) { @@ -354,6 +355,7 @@ export async function handleTelegramAction( const topicId = readNumberParam(params, "topicId", { required: true, integer: true, + strict: true, }); const token = resolveTelegramToken(cfg, { accountId }).token; if (!token) { From af83a787f25922d14d699bae78d68a7806a08567 Mon Sep 17 00:00:00 2001 From: Alexander Bolshakov Date: Mon, 9 Mar 2026 23:25:07 +0100 Subject: [PATCH 06/12] protocol: include pending node models in macOS schema output --- .../OpenClawProtocol/GatewayModels.swift | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/apps/macos/Sources/OpenClawProtocol/GatewayModels.swift b/apps/macos/Sources/OpenClawProtocol/GatewayModels.swift index aaac880619d..3003ae79f7b 100644 --- a/apps/macos/Sources/OpenClawProtocol/GatewayModels.swift +++ b/apps/macos/Sources/OpenClawProtocol/GatewayModels.swift @@ -942,6 +942,102 @@ public struct NodeEventParams: Codable, Sendable { } } +public struct NodePendingDrainParams: Codable, Sendable { + public let maxitems: Int? + + public init( + maxitems: Int?) + { + self.maxitems = maxitems + } + + private enum CodingKeys: String, CodingKey { + case maxitems = "maxItems" + } +} + +public struct NodePendingDrainResult: Codable, Sendable { + public let nodeid: String + public let revision: Int + public let items: [[String: AnyCodable]] + public let hasmore: Bool + + public init( + nodeid: String, + revision: Int, + items: [[String: AnyCodable]], + hasmore: Bool) + { + self.nodeid = nodeid + self.revision = revision + self.items = items + self.hasmore = hasmore + } + + private enum CodingKeys: String, CodingKey { + case nodeid = "nodeId" + case revision + case items + case hasmore = "hasMore" + } +} + +public struct NodePendingEnqueueParams: Codable, Sendable { + public let nodeid: String + public let type: String + public let priority: String? + public let expiresinms: Int? + public let wake: Bool? + + public init( + nodeid: String, + type: String, + priority: String?, + expiresinms: Int?, + wake: Bool?) + { + self.nodeid = nodeid + self.type = type + self.priority = priority + self.expiresinms = expiresinms + self.wake = wake + } + + private enum CodingKeys: String, CodingKey { + case nodeid = "nodeId" + case type + case priority + case expiresinms = "expiresInMs" + case wake + } +} + +public struct NodePendingEnqueueResult: Codable, Sendable { + public let nodeid: String + public let revision: Int + public let queued: [String: AnyCodable] + public let waketriggered: Bool + + public init( + nodeid: String, + revision: Int, + queued: [String: AnyCodable], + waketriggered: Bool) + { + self.nodeid = nodeid + self.revision = revision + self.queued = queued + self.waketriggered = waketriggered + } + + private enum CodingKeys: String, CodingKey { + case nodeid = "nodeId" + case revision + case queued + case waketriggered = "wakeTriggered" + } +} + public struct NodeInvokeRequestEvent: Codable, Sendable { public let id: String public let nodeid: String From cda6b856d733eeb45a341f6522c9b2e24f74bc9e Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Wed, 11 Mar 2026 14:39:50 +0100 Subject: [PATCH 09/12] telegram: reject fractional ids in strict integer paths --- extensions/telegram/src/channel-actions.ts | 1 + src/agents/tools/common.params.test.ts | 7 +++++++ src/agents/tools/common.ts | 19 ++++++++++++++++--- src/agents/tools/telegram-actions.ts | 5 +++++ 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/extensions/telegram/src/channel-actions.ts b/extensions/telegram/src/channel-actions.ts index 50f7826da70..26cf762fe77 100644 --- a/extensions/telegram/src/channel-actions.ts +++ b/extensions/telegram/src/channel-actions.ts @@ -71,6 +71,7 @@ function readTelegramMessageIdParam( required, integer: true, strict: true, + strictInteger: true, }); if (required && typeof messageId !== "number") { throw new Error("messageId is required."); diff --git a/src/agents/tools/common.params.test.ts b/src/agents/tools/common.params.test.ts index 32eb63d036e..b8aef7b631e 100644 --- a/src/agents/tools/common.params.test.ts +++ b/src/agents/tools/common.params.test.ts @@ -63,6 +63,13 @@ describe("readNumberParam", () => { expect(readNumberParam(params, "messageId", { integer: true })).toBe(42); }); + it("rejects fractional values when strictInteger is true", () => { + const params = { messageId: "42.9" }; + expect(() => + readNumberParam(params, "messageId", { integer: true, strictInteger: true }), + ).toThrow(/messageId must be an integer/); + }); + it("accepts snake_case aliases for camelCase keys", () => { const params = { message_id: "42" }; expect(readNumberParam(params, "messageId")).toBe(42); diff --git a/src/agents/tools/common.ts b/src/agents/tools/common.ts index 81d3f4efc00..4a1146f34de 100644 --- a/src/agents/tools/common.ts +++ b/src/agents/tools/common.ts @@ -116,9 +116,16 @@ export function readStringOrNumberParam( export function readNumberParam( params: Record, key: string, - options: { required?: boolean; label?: string; integer?: boolean; strict?: boolean } = {}, + options: { + required?: boolean; + label?: string; + integer?: boolean; + strict?: boolean; + strictInteger?: boolean; + } = {}, ): number | undefined { - const { required = false, label = key, integer = false, strict = false } = options; + const { required = false, label = key, integer = false, strict = false, strictInteger = false } = + options; const raw = readParamRaw(params, key); let value: number | undefined; if (typeof raw === "number" && Number.isFinite(raw)) { @@ -138,7 +145,13 @@ export function readNumberParam( } return undefined; } - return integer ? Math.trunc(value) : value; + if (integer) { + if (strictInteger && !Number.isInteger(value)) { + throw new ToolInputError(`${label} must be an integer`); + } + return Math.trunc(value); + } + return value; } export function readStringArrayParam( diff --git a/src/agents/tools/telegram-actions.ts b/src/agents/tools/telegram-actions.ts index ec270ab32b0..3294ce1d917 100644 --- a/src/agents/tools/telegram-actions.ts +++ b/src/agents/tools/telegram-actions.ts @@ -136,6 +136,8 @@ export async function handleTelegramAction( }); const messageId = readNumberParam(params, "messageId", { integer: true, + strict: true, + strictInteger: true, }); if (typeof messageId !== "number" || !Number.isFinite(messageId) || messageId <= 0) { return jsonResult({ @@ -328,6 +330,7 @@ export async function handleTelegramAction( required: true, integer: true, strict: true, + strictInteger: true, }); const token = resolveTelegramToken(cfg, { accountId }).token; if (!token) { @@ -380,6 +383,8 @@ export async function handleTelegramAction( const messageId = readNumberParam(params, "messageId", { required: true, integer: true, + strict: true, + strictInteger: true, }); const content = readStringParam(params, "content", { required: true, From 55d47c1045343fc260bb4872c65aa97bc6beca12 Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Wed, 11 Mar 2026 14:48:03 +0100 Subject: [PATCH 10/12] style: format strict integer parser options --- src/agents/tools/common.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/agents/tools/common.ts b/src/agents/tools/common.ts index 4a1146f34de..ab6fab56400 100644 --- a/src/agents/tools/common.ts +++ b/src/agents/tools/common.ts @@ -124,8 +124,13 @@ export function readNumberParam( strictInteger?: boolean; } = {}, ): number | undefined { - const { required = false, label = key, integer = false, strict = false, strictInteger = false } = - options; + const { + required = false, + label = key, + integer = false, + strict = false, + strictInteger = false, + } = options; const raw = readParamRaw(params, key); let value: number | undefined; if (typeof raw === "number" && Number.isFinite(raw)) { From c065d432f0a4933ca8968a813e9a6790113338c7 Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Wed, 11 Mar 2026 17:31:07 +0100 Subject: [PATCH 11/12] telegram: treat message_id as explicit delete input --- extensions/telegram/src/channel-actions.ts | 3 ++- src/channels/plugins/actions/actions.test.ts | 23 ++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/extensions/telegram/src/channel-actions.ts b/extensions/telegram/src/channel-actions.ts index 26cf762fe77..460a34f4dbe 100644 --- a/extensions/telegram/src/channel-actions.ts +++ b/extensions/telegram/src/channel-actions.ts @@ -216,7 +216,8 @@ export const telegramMessageActions: ChannelMessageActionAdapter = { if (action === "delete") { const chatId = readTelegramChatIdParam(params); - const hasMessageIdParam = Object.hasOwn(params, "messageId"); + const hasMessageIdParam = + Object.hasOwn(params, "messageId") || Object.hasOwn(params, "message_id"); const messageId = readTelegramMessageIdParam(params, { required: false }); if (hasMessageIdParam && typeof messageId !== "number") { throw new Error("messageId must be a valid number for action=delete."); diff --git a/src/channels/plugins/actions/actions.test.ts b/src/channels/plugins/actions/actions.test.ts index b9e8f8fdb55..e8fa82b3fdd 100644 --- a/src/channels/plugins/actions/actions.test.ts +++ b/src/channels/plugins/actions/actions.test.ts @@ -892,6 +892,29 @@ describe("telegramMessageActions", () => { expect(handleTelegramAction).not.toHaveBeenCalled(); }); + it("rejects invalid snake_case delete message_id instead of falling back to topic deletion", async () => { + const cfg = telegramCfg(); + const handleAction = telegramMessageActions.handleAction; + if (!handleAction) { + throw new Error("telegram handleAction unavailable"); + } + + await expect( + handleAction({ + channel: "telegram", + action: "delete", + params: { + to: "-1001234567890", + message_id: "oops", + topicId: 271, + }, + cfg, + }), + ).rejects.toThrow(/messageId must be a valid number for action=delete/i); + + expect(handleTelegramAction).not.toHaveBeenCalled(); + }); + it("rejects topic-delete when threadId/topicId is missing", async () => { const cfg = telegramCfg(); const handleAction = telegramMessageActions.handleAction; From d8fae6c4d76040b707c3fe377eabe097a363c1e2 Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Wed, 11 Mar 2026 17:46:06 +0100 Subject: [PATCH 12/12] telegram: tighten topic alias validation and pass cfg --- extensions/telegram/src/channel-actions.ts | 29 ++++++++++-- extensions/telegram/src/send.ts | 1 + src/agents/tools/telegram-actions.test.ts | 2 +- src/agents/tools/telegram-actions.ts | 2 + src/channels/plugins/actions/actions.test.ts | 48 +++++++++++++++++++- 5 files changed, 76 insertions(+), 6 deletions(-) diff --git a/extensions/telegram/src/channel-actions.ts b/extensions/telegram/src/channel-actions.ts index 460a34f4dbe..003b7113a78 100644 --- a/extensions/telegram/src/channel-actions.ts +++ b/extensions/telegram/src/channel-actions.ts @@ -80,10 +80,31 @@ function readTelegramMessageIdParam( } function readTelegramTopicIdParam(params: Record): number | undefined { - return ( - readNumberParam(params, "topicId", { integer: true, strict: true }) ?? - readNumberParam(params, "threadId", { integer: true, strict: true }) - ); + const hasTopicIdParam = Object.hasOwn(params, "topicId") || Object.hasOwn(params, "topic_id"); + const topicId = readNumberParam(params, "topicId", { + integer: true, + strict: true, + strictInteger: true, + }); + if (hasTopicIdParam && typeof topicId !== "number") { + throw new Error("topicId must be a valid integer when provided."); + } + if (typeof topicId === "number") { + return topicId; + } + + const hasThreadIdParam = + Object.hasOwn(params, "threadId") || Object.hasOwn(params, "thread_id"); + const threadId = readNumberParam(params, "threadId", { + integer: true, + strict: true, + strictInteger: true, + }); + if (hasThreadIdParam && typeof threadId !== "number") { + throw new Error("threadId must be a valid integer when provided."); + } + + return threadId; } export const telegramMessageActions: ChannelMessageActionAdapter = { diff --git a/extensions/telegram/src/send.ts b/extensions/telegram/src/send.ts index 1914f29905c..e6c09c106b6 100644 --- a/extensions/telegram/src/send.ts +++ b/extensions/telegram/src/send.ts @@ -1443,6 +1443,7 @@ export async function sendPollTelegram( // --------------------------------------------------------------------------- type TelegramDeleteForumTopicOpts = { + cfg?: ReturnType; token?: string; accountId?: string; verbose?: boolean; diff --git a/src/agents/tools/telegram-actions.test.ts b/src/agents/tools/telegram-actions.test.ts index c8c961ffcc8..409b9a50f5f 100644 --- a/src/agents/tools/telegram-actions.test.ts +++ b/src/agents/tools/telegram-actions.test.ts @@ -629,7 +629,7 @@ describe("handleTelegramAction", () => { expect(deleteForumTopicTelegram).toHaveBeenCalledWith( "-100123", 271, - expect.objectContaining({ token: "tok" }), + expect.objectContaining({ cfg, token: "tok" }), ); }); diff --git a/src/agents/tools/telegram-actions.ts b/src/agents/tools/telegram-actions.ts index 3294ce1d917..b0a9abf4c95 100644 --- a/src/agents/tools/telegram-actions.ts +++ b/src/agents/tools/telegram-actions.ts @@ -359,6 +359,7 @@ export async function handleTelegramAction( required: true, integer: true, strict: true, + strictInteger: true, }); const token = resolveTelegramToken(cfg, { accountId }).token; if (!token) { @@ -367,6 +368,7 @@ export async function handleTelegramAction( ); } await deleteForumTopicTelegram(chatId ?? "", topicId ?? 0, { + cfg, token, accountId: accountId ?? undefined, }); diff --git a/src/channels/plugins/actions/actions.test.ts b/src/channels/plugins/actions/actions.test.ts index e8fa82b3fdd..b0a0c0123d9 100644 --- a/src/channels/plugins/actions/actions.test.ts +++ b/src/channels/plugins/actions/actions.test.ts @@ -953,7 +953,53 @@ describe("telegramMessageActions", () => { }, cfg, }), - ).rejects.toThrow(/threadId\/topicId is required for action=topic-delete/i); + ).rejects.toThrow(/topicId must be a valid integer when provided/i); + + expect(handleTelegramAction).not.toHaveBeenCalled(); + }); + + it("rejects malformed topicId even when threadId alias is valid", async () => { + const cfg = telegramCfg(); + const handleAction = telegramMessageActions.handleAction; + if (!handleAction) { + throw new Error("telegram handleAction unavailable"); + } + + await expect( + handleAction({ + channel: "telegram", + action: "topic-delete", + params: { + to: "-1001234567890", + topicId: "oops", + threadId: 271, + }, + cfg, + }), + ).rejects.toThrow(/topicId must be a valid integer when provided/i); + + expect(handleTelegramAction).not.toHaveBeenCalled(); + }); + + it("rejects malformed delete topicId even when threadId alias is valid", async () => { + const cfg = telegramCfg(); + const handleAction = telegramMessageActions.handleAction; + if (!handleAction) { + throw new Error("telegram handleAction unavailable"); + } + + await expect( + handleAction({ + channel: "telegram", + action: "delete", + params: { + to: "-1001234567890", + topicId: "oops", + threadId: 271, + }, + cfg, + }), + ).rejects.toThrow(/topicId must be a valid integer when provided/i); expect(handleTelegramAction).not.toHaveBeenCalled(); });