From f70d58cd9da0c5f7d1eb502955a51326cb2b4ad3 Mon Sep 17 00:00:00 2001 From: Alexander Bolshakov Date: Mon, 9 Mar 2026 21:41:24 +0100 Subject: [PATCH] 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