diff --git a/docs/channels/telegram.md b/docs/channels/telegram.md index 37be3bf1111..d28b6c2b798 100644 --- a/docs/channels/telegram.md +++ b/docs/channels/telegram.md @@ -424,10 +424,14 @@ 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`). + 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: @@ -969,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 29095e7bc7c..003b7113a78 100644 --- a/extensions/telegram/src/channel-actions.ts +++ b/extensions/telegram/src/channel-actions.ts @@ -62,15 +62,49 @@ 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, + strict: true, + strictInteger: 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 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 = { @@ -104,6 +138,7 @@ export const telegramMessageActions: ChannelMessageActionAdapter = { } if (isEnabled("deleteMessage")) { actions.add("delete"); + actions.add("topic-delete"); } if (isEnabled("editMessage")) { actions.add("edit"); @@ -202,12 +237,53 @@ export const telegramMessageActions: ChannelMessageActionAdapter = { if (action === "delete") { const chatId = readTelegramChatIdParam(params); - const messageId = readTelegramMessageIdParam(params); + 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."); + } + 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 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: "deleteMessage", + action: "deleteForumTopic", chatId, - messageId, + topicId, accountId: accountId ?? undefined, }, cfg, 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 7a29ecf07de..aca7abe6fce 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, @@ -1950,6 +1951,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..e6c09c106b6 100644 --- a/extensions/telegram/src/send.ts +++ b/extensions/telegram/src/send.ts @@ -1438,6 +1438,49 @@ export async function sendPollTelegram( return { messageId: String(messageId), chatId: resolvedChatId, pollId }; } +// --------------------------------------------------------------------------- +// Forum topic deletion +// --------------------------------------------------------------------------- + +type TelegramDeleteForumTopicOpts = { + cfg?: ReturnType; + 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/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..ab6fab56400 100644 --- a/src/agents/tools/common.ts +++ b/src/agents/tools/common.ts @@ -116,9 +116,21 @@ 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 +150,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/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/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..409b9a50f5f 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,75 @@ 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" } }, + } as OpenClawConfig; + await handleTelegramAction( + { + action: "deleteForumTopic", + chatId: "-100123", + topicId: 271, + }, + cfg, + ); + expect(deleteForumTopicTelegram).toHaveBeenCalledWith( + "-100123", + 271, + expect.objectContaining({ cfg, token: "tok" }), + ); + }); + + 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: { + telegram: { botToken: "tok", actions: { deleteMessage: false } }, + }, + } as OpenClawConfig; + await expect( + handleTelegramAction( + { + action: "deleteForumTopic", + chatId: "-100123", + topicId: 271, + }, + cfg, + ), + ).rejects.toThrow(/Telegram forum topic deletion 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..b0a9abf4c95 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, @@ -135,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({ @@ -326,6 +329,8 @@ export async function handleTelegramAction( const messageId = readNumberParam(params, "messageId", { required: true, integer: true, + strict: true, + strictInteger: true, }); const token = resolveTelegramToken(cfg, { accountId }).token; if (!token) { @@ -341,6 +346,35 @@ export async function handleTelegramAction( return jsonResult({ ok: true, deleted: true }); } + if (action === "deleteForumTopic") { + 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, + }); + const topicId = readNumberParam(params, "topicId", { + required: true, + integer: true, + strict: true, + strictInteger: 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, { + cfg, + token, + accountId: accountId ?? undefined, + }); + return jsonResult({ ok: true, deleted: true, topicId }); + } + if (action === "editMessage") { if (!isActionEnabled("editMessage")) { throw new Error("Telegram editMessage is disabled."); @@ -351,6 +385,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, diff --git a/src/channels/plugins/actions/actions.test.ts b/src/channels/plugins/actions/actions.test.ts index 055d660524f..b0a0c0123d9 100644 --- a/src/channels/plugins/actions/actions.test.ts +++ b/src/channels/plugins/actions/actions.test.ts @@ -777,6 +777,48 @@ 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: "topic-delete maps to deleteForumTopic when threadId is provided", + action: "topic-delete" as const, + params: { + to: "-1001234567890", + threadId: 271, + }, + expectedPayload: { + action: "deleteForumTopic", + chatId: "-1001234567890", + topicId: 271, + 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, @@ -806,6 +848,162 @@ describe("telegramMessageActions", () => { } }); + it("rejects delete when messageId is not 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 is required for action=delete/i); + + 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 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; + 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(); + }); + + 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(/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(); + }); + it("forwards trusted mediaLocalRoots for send", async () => { const cfg = telegramCfg(); await telegramMessageActions.handleAction?.({ 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