From a516141bdae5f3a4136f10ce1491452aa0e77f8e Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Mon, 16 Mar 2026 07:49:13 +0530 Subject: [PATCH] feat(telegram): add topic-edit action --- extensions/telegram/src/channel-actions.ts | 27 +++++++++ extensions/telegram/src/send.test.ts | 37 ++++++++++++ extensions/telegram/src/send.ts | 63 ++++++++++++++++---- src/agents/tools/telegram-actions.test.ts | 17 ++++++ src/agents/tools/telegram-actions.ts | 32 ++++++++++ src/channels/plugins/actions/actions.test.ts | 33 ++++++++++ src/channels/plugins/message-action-names.ts | 1 + src/config/types.telegram.ts | 2 + src/config/zod-schema.providers-core.ts | 1 + src/infra/outbound/message-action-spec.ts | 1 + 10 files changed, 204 insertions(+), 10 deletions(-) diff --git a/extensions/telegram/src/channel-actions.ts b/extensions/telegram/src/channel-actions.ts index 29095e7bc7c..1745071c060 100644 --- a/extensions/telegram/src/channel-actions.ts +++ b/extensions/telegram/src/channel-actions.ts @@ -115,6 +115,9 @@ export const telegramMessageActions: ChannelMessageActionAdapter = { if (isEnabled("createForumTopic")) { actions.add("topic-create"); } + if (isEnabled("editForumTopic")) { + actions.add("topic-edit"); + } return Array.from(actions); }, supportsButtons: ({ cfg }) => { @@ -290,6 +293,30 @@ export const telegramMessageActions: ChannelMessageActionAdapter = { ); } + if (action === "topic-edit") { + const chatId = readTelegramChatIdParam(params); + const messageThreadId = + readNumberParam(params, "messageThreadId", { integer: true }) ?? + readNumberParam(params, "threadId", { integer: true }); + if (typeof messageThreadId !== "number") { + throw new Error("messageThreadId or threadId is required."); + } + const name = readStringParam(params, "name"); + const iconCustomEmojiId = readStringParam(params, "iconCustomEmojiId"); + return await handleTelegramAction( + { + action: "editForumTopic", + chatId, + messageThreadId, + name: name ?? undefined, + iconCustomEmojiId: iconCustomEmojiId ?? undefined, + accountId: accountId ?? undefined, + }, + cfg, + { mediaLocalRoots }, + ); + } + throw new Error(`Action ${action} is not supported for provider ${providerId}.`); }, }; diff --git a/extensions/telegram/src/send.test.ts b/extensions/telegram/src/send.test.ts index 8a234ce92cb..ba1863b1b90 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, + editForumTopicTelegram, editMessageTelegram, pinMessageTelegram, reactMessageTelegram, @@ -257,6 +258,42 @@ describe("sendMessageTelegram", () => { }); }); + it("edits a Telegram forum topic name and icon via the shared helper", async () => { + loadConfig.mockReturnValue({ + channels: { + telegram: { + botToken: "tok", + }, + }, + }); + botApi.editForumTopic.mockResolvedValue(true); + + await editForumTopicTelegram("-1001234567890", 271, { + accountId: "default", + name: "Codex Thread", + iconCustomEmojiId: "emoji-123", + }); + + expect(botApi.editForumTopic).toHaveBeenCalledWith("-1001234567890", 271, { + name: "Codex Thread", + icon_custom_emoji_id: "emoji-123", + }); + }); + + it("rejects empty topic edits", async () => { + await expect( + editForumTopicTelegram("-1001234567890", 271, { + accountId: "default", + }), + ).rejects.toThrow("Telegram forum topic update requires a name or iconCustomEmojiId"); + await expect( + editForumTopicTelegram("-1001234567890", 271, { + accountId: "default", + iconCustomEmojiId: " ", + }), + ).rejects.toThrow("Telegram forum topic icon custom emoji ID is required"); + }); + it("applies timeoutSeconds config precedence", async () => { const cases = [ { diff --git a/extensions/telegram/src/send.ts b/extensions/telegram/src/send.ts index 89d6f7d337d..d96e783c51d 100644 --- a/extensions/telegram/src/send.ts +++ b/extensions/telegram/src/send.ts @@ -1128,19 +1128,39 @@ export async function unpinMessageTelegram( }; } -export async function renameForumTopicTelegram( +type TelegramEditForumTopicOpts = TelegramDeleteOpts & { + name?: string; + iconCustomEmojiId?: string; +}; + +export async function editForumTopicTelegram( chatIdInput: string | number, messageThreadIdInput: string | number, - name: string, - opts: TelegramDeleteOpts = {}, -): Promise<{ ok: true; chatId: string; messageThreadId: number; name: string }> { - const trimmedName = name.trim(); - if (!trimmedName) { + opts: TelegramEditForumTopicOpts = {}, +): Promise<{ + ok: true; + chatId: string; + messageThreadId: number; + name?: string; + iconCustomEmojiId?: string; +}> { + const nameProvided = opts.name !== undefined; + const trimmedName = opts.name?.trim(); + if (nameProvided && !trimmedName) { throw new Error("Telegram forum topic name is required"); } - if (trimmedName.length > 128) { + if (trimmedName && trimmedName.length > 128) { throw new Error("Telegram forum topic name must be 128 characters or fewer"); } + const iconProvided = opts.iconCustomEmojiId !== undefined; + const trimmedIconCustomEmojiId = opts.iconCustomEmojiId?.trim(); + if (iconProvided && !trimmedIconCustomEmojiId) { + throw new Error("Telegram forum topic icon custom emoji ID is required"); + } + if (!trimmedName && !trimmedIconCustomEmojiId) { + throw new Error("Telegram forum topic update requires a name or iconCustomEmojiId"); + } + const { cfg, account, api } = resolveTelegramApiContext(opts); const rawTarget = String(chatIdInput); const chatId = await resolveAndPersistChatId({ @@ -1157,16 +1177,39 @@ export async function renameForumTopicTelegram( retry: opts.retry, verbose: opts.verbose, }); + const payload = { + ...(trimmedName ? { name: trimmedName } : {}), + ...(trimmedIconCustomEmojiId ? { icon_custom_emoji_id: trimmedIconCustomEmojiId } : {}), + }; await requestWithDiag( - () => api.editForumTopic(chatId, messageThreadId, { name: trimmedName }), + () => api.editForumTopic(chatId, messageThreadId, payload), "editForumTopic", ); - logVerbose(`[telegram] Renamed forum topic ${messageThreadId} in chat ${chatId}`); + logVerbose(`[telegram] Edited forum topic ${messageThreadId} in chat ${chatId}`); return { ok: true, chatId, messageThreadId, - name: trimmedName, + ...(trimmedName ? { name: trimmedName } : {}), + ...(trimmedIconCustomEmojiId ? { iconCustomEmojiId: trimmedIconCustomEmojiId } : {}), + }; +} + +export async function renameForumTopicTelegram( + chatIdInput: string | number, + messageThreadIdInput: string | number, + name: string, + opts: TelegramDeleteOpts = {}, +): Promise<{ ok: true; chatId: string; messageThreadId: number; name: string }> { + const result = await editForumTopicTelegram(chatIdInput, messageThreadIdInput, { + ...opts, + name, + }); + return { + ok: true, + chatId: result.chatId, + messageThreadId: result.messageThreadId, + name: result.name ?? name.trim(), }; } diff --git a/src/agents/tools/telegram-actions.test.ts b/src/agents/tools/telegram-actions.test.ts index 5963a64b667..997de707765 100644 --- a/src/agents/tools/telegram-actions.test.ts +++ b/src/agents/tools/telegram-actions.test.ts @@ -23,6 +23,12 @@ const editMessageTelegram = vi.fn(async () => ({ messageId: "456", chatId: "123", })); +const editForumTopicTelegram = vi.fn(async () => ({ + ok: true, + chatId: "123", + messageThreadId: 42, + name: "Renamed", +})); const createForumTopicTelegram = vi.fn(async () => ({ topicId: 99, name: "Topic", @@ -42,6 +48,8 @@ vi.mock("../../../extensions/telegram/src/send.js", () => ({ deleteMessageTelegram(...args), editMessageTelegram: (...args: Parameters) => editMessageTelegram(...args), + editForumTopicTelegram: (...args: Parameters) => + editForumTopicTelegram(...args), createForumTopicTelegram: (...args: Parameters) => createForumTopicTelegram(...args), })); @@ -105,6 +113,7 @@ describe("handleTelegramAction", () => { sendStickerTelegram.mockClear(); deleteMessageTelegram.mockClear(); editMessageTelegram.mockClear(); + editForumTopicTelegram.mockClear(); createForumTopicTelegram.mockClear(); process.env.TELEGRAM_BOT_TOKEN = "tok"; }); @@ -457,6 +466,14 @@ describe("handleTelegramAction", () => { readCallOpts: (calls: unknown[][], argIndex: number) => Record, ) => readCallOpts(createForumTopicTelegram.mock.calls as unknown[][], 2), }, + { + name: "editForumTopic", + params: { action: "editForumTopic", chatId: "123", messageThreadId: 42, name: "New" }, + cfg: telegramConfig({ actions: { editForumTopic: true } }), + assertCall: ( + readCallOpts: (calls: unknown[][], argIndex: number) => Record, + ) => readCallOpts(editForumTopicTelegram.mock.calls as unknown[][], 2), + }, ])("forwards resolved cfg for $name action", async ({ params, cfg, assertCall }) => { const readCallOpts = (calls: unknown[][], argIndex: number): Record => { const args = calls[0]; diff --git a/src/agents/tools/telegram-actions.ts b/src/agents/tools/telegram-actions.ts index 6c8d4f84204..ccfc9d5ae13 100644 --- a/src/agents/tools/telegram-actions.ts +++ b/src/agents/tools/telegram-actions.ts @@ -15,6 +15,7 @@ import { resolveTelegramReactionLevel } from "../../../extensions/telegram/src/r import { createForumTopicTelegram, deleteMessageTelegram, + editForumTopicTelegram, editMessageTelegram, reactMessageTelegram, sendMessageTelegram, @@ -478,5 +479,36 @@ export async function handleTelegramAction( }); } + if (action === "editForumTopic") { + if (!isActionEnabled("editForumTopic")) { + throw new Error("Telegram editForumTopic is disabled."); + } + const chatId = readStringOrNumberParam(params, "chatId", { + required: true, + }); + const messageThreadId = + readNumberParam(params, "messageThreadId", { integer: true }) ?? + readNumberParam(params, "threadId", { integer: true }); + if (typeof messageThreadId !== "number") { + throw new Error("messageThreadId or threadId is required."); + } + const name = readStringParam(params, "name"); + const iconCustomEmojiId = readStringParam(params, "iconCustomEmojiId"); + const token = resolveTelegramToken(cfg, { accountId }).token; + if (!token) { + throw new Error( + "Telegram bot token missing. Set TELEGRAM_BOT_TOKEN or channels.telegram.botToken.", + ); + } + const result = await editForumTopicTelegram(chatId ?? "", messageThreadId, { + cfg, + token, + accountId: accountId ?? undefined, + name: name ?? undefined, + iconCustomEmojiId: iconCustomEmojiId ?? undefined, + }); + return jsonResult(result); + } + throw new Error(`Unsupported Telegram action: ${action}`); } diff --git a/src/channels/plugins/actions/actions.test.ts b/src/channels/plugins/actions/actions.test.ts index 055d660524f..bf75f9997d2 100644 --- a/src/channels/plugins/actions/actions.test.ts +++ b/src/channels/plugins/actions/actions.test.ts @@ -540,6 +540,21 @@ describe("telegramMessageActions", () => { expect(actions).toContain("poll"); }); + it("lists topic-edit when telegram topic edits are enabled", () => { + const cfg = { + channels: { + telegram: { + botToken: "tok", + actions: { editForumTopic: true }, + }, + }, + } as OpenClawConfig; + + const actions = telegramMessageActions.listActions?.({ cfg }) ?? []; + + expect(actions).toContain("topic-edit"); + }); + it("omits poll when sendMessage is disabled", () => { const cfg = { channels: { @@ -793,6 +808,24 @@ describe("telegramMessageActions", () => { accountId: undefined, }, }, + { + name: "topic-edit maps to editForumTopic", + action: "topic-edit" as const, + params: { + to: "telegram:group:-1001234567890:topic:271", + threadId: 271, + name: "Build Updates", + iconCustomEmojiId: "emoji-123", + }, + expectedPayload: { + action: "editForumTopic", + chatId: "telegram:group:-1001234567890:topic:271", + messageThreadId: 271, + name: "Build Updates", + iconCustomEmojiId: "emoji-123", + accountId: undefined, + }, + }, ] as const; for (const testCase of cases) { diff --git a/src/channels/plugins/message-action-names.ts b/src/channels/plugins/message-action-names.ts index 809d239be2c..aadff95c77d 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-edit", "voice-status", "event-list", "event-create", diff --git a/src/config/types.telegram.ts b/src/config/types.telegram.ts index 252f66740b2..fe1c5be3962 100644 --- a/src/config/types.telegram.ts +++ b/src/config/types.telegram.ts @@ -26,6 +26,8 @@ export type TelegramActionConfig = { sticker?: boolean; /** Enable forum topic creation. */ createForumTopic?: boolean; + /** Enable forum topic editing (rename / change icon). */ + editForumTopic?: boolean; }; export type TelegramNetworkConfig = { diff --git a/src/config/zod-schema.providers-core.ts b/src/config/zod-schema.providers-core.ts index 5f7dd7b8e48..da81ef61a4f 100644 --- a/src/config/zod-schema.providers-core.ts +++ b/src/config/zod-schema.providers-core.ts @@ -258,6 +258,7 @@ export const TelegramAccountSchemaBase = z editMessage: z.boolean().optional(), sticker: z.boolean().optional(), createForumTopic: z.boolean().optional(), + editForumTopic: z.boolean().optional(), }) .strict() .optional(), diff --git a/src/infra/outbound/message-action-spec.ts b/src/infra/outbound/message-action-spec.ts index b49a60c6991..f4f715d869d 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