telegram: add dedicated topic-delete action

This commit is contained in:
Alexander Bolshakov 2026-03-09 21:41:24 +01:00 committed by OpenClaw Agent
parent 6ab5c55716
commit f70d58cd9d
8 changed files with 80 additions and 26 deletions

View File

@ -428,8 +428,10 @@ curl "https://api.telegram.org/bot<bot_token>/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`

View File

@ -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<string, unknown>): 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") {

View File

@ -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", () => {

View File

@ -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 () => {

View File

@ -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,

View File

@ -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();
});

View File

@ -44,6 +44,7 @@ export const CHANNEL_MESSAGE_ACTION_NAMES = [
"category-edit",
"category-delete",
"topic-create",
"topic-delete",
"voice-status",
"event-list",
"event-create",

View File

@ -49,6 +49,7 @@ export const MESSAGE_ACTION_TARGET_MODE: Record<ChannelMessageActionName, Messag
"category-edit": "none",
"category-delete": "none",
"topic-create": "to",
"topic-delete": "to",
"voice-status": "none",
"event-list": "none",
"event-create": "none",