From c617fcbcc9656df441dc5880e29b0be5396bc3e6 Mon Sep 17 00:00:00 2001 From: hotfingerdip Date: Wed, 28 Jan 2026 01:18:38 +0000 Subject: [PATCH] feat(telegram): add forum topic management actions Add support for creating, editing, closing, reopening, and deleting Telegram forum topics via the message tool. New actions: - topic-create: Create a forum topic (name required, optional iconColor and iconCustomEmojiId) - topic-edit: Edit topic name/icon (requires messageThreadId) - topic-close: Close an open topic - topic-reopen: Reopen a closed topic - topic-delete: Delete a topic and all its messages These actions are gated behind channels.telegram.actions.forumTopics (defaults to false/disabled). The bot needs can_manage_topics admin rights in the supergroup. Implementation: - Add 5 new action names to message-action-names.ts - Add target modes in message-action-spec.ts - Add forum topic API functions in telegram/send.ts using grammY's built-in createForumTopic/editForumTopic/closeForumTopic/ reopenForumTopic/deleteForumTopic methods - Wire actions through telegram-actions.ts and the channel plugin adapter in channels/plugins/actions/telegram.ts - Add forumTopics flag to TelegramActionConfig type - Add schema properties (iconColor, iconCustomEmojiId, messageThreadId) to the message tool schema - Add 9 new tests covering all actions and validation Closes #3056 --- src/agents/tools/message-tool.ts | 22 ++ src/agents/tools/telegram-actions.ts | 146 +++++++++++ src/channels/plugins/actions/telegram.test.ts | 205 +++++++++++++++ src/channels/plugins/actions/telegram.ts | 107 ++++++++ src/channels/plugins/message-action-names.ts | 5 + src/config/types.telegram.ts | 2 + src/infra/outbound/message-action-spec.ts | 5 + src/telegram/send.ts | 233 ++++++++++++++++++ 8 files changed, 725 insertions(+) diff --git a/src/agents/tools/message-tool.ts b/src/agents/tools/message-tool.ts index 4ea178a54..a42c728bd 100644 --- a/src/agents/tools/message-tool.ts +++ b/src/agents/tools/message-tool.ts @@ -188,6 +188,27 @@ function buildGatewaySchema() { }; } +function buildForumTopicSchema() { + return { + iconColor: Type.Optional( + Type.Number({ + description: + "Topic icon color in RGB (e.g. 0x6FB9F0). Only for Telegram forum topic-create.", + }), + ), + iconCustomEmojiId: Type.Optional( + Type.String({ + description: "Custom emoji id for the topic icon. For Telegram topic-create/topic-edit.", + }), + ), + messageThreadId: Type.Optional( + Type.Number({ + description: "Forum topic thread id. For Telegram topic-edit/close/reopen/delete.", + }), + ), + }; +} + function buildChannelManagementSchema() { return { name: Type.Optional(Type.String()), @@ -220,6 +241,7 @@ function buildMessageToolSchemaProps(options: { includeButtons: boolean; include ...buildModerationSchema(), ...buildGatewaySchema(), ...buildChannelManagementSchema(), + ...buildForumTopicSchema(), }; } diff --git a/src/agents/tools/telegram-actions.ts b/src/agents/tools/telegram-actions.ts index 515ff8c47..4a8f28919 100644 --- a/src/agents/tools/telegram-actions.ts +++ b/src/agents/tools/telegram-actions.ts @@ -2,9 +2,14 @@ import type { AgentToolResult } from "@mariozechner/pi-agent-core"; import type { MoltbotConfig } from "../../config/config.js"; import { resolveTelegramReactionLevel } from "../../telegram/reaction-level.js"; import { + closeForumTopicTelegram, + createForumTopicTelegram, + deleteForumTopicTelegram, deleteMessageTelegram, + editForumTopicTelegram, editMessageTelegram, reactMessageTelegram, + reopenForumTopicTelegram, sendMessageTelegram, sendStickerTelegram, } from "../../telegram/send.js"; @@ -318,5 +323,146 @@ export async function handleTelegramAction( return jsonResult({ ok: true, ...stats }); } + if (action === "createForumTopic") { + if (!isActionEnabled("forumTopics", false)) { + throw new Error( + "Telegram forum topic actions are disabled. Set channels.telegram.actions.forumTopics to true.", + ); + } + const chatId = readStringOrNumberParam(params, "chatId", { + required: true, + }); + const name = readStringParam(params, "name", { required: true }); + const iconColor = readNumberParam(params, "iconColor", { integer: true }); + 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 createForumTopicTelegram(chatId ?? "", name, { + token, + accountId: accountId ?? undefined, + iconColor: iconColor ?? undefined, + iconCustomEmojiId: iconCustomEmojiId ?? undefined, + }); + return jsonResult({ + ok: true, + messageThreadId: result.messageThreadId, + name: result.name, + iconColor: result.iconColor, + iconCustomEmojiId: result.iconCustomEmojiId, + }); + } + + if (action === "editForumTopic") { + if (!isActionEnabled("forumTopics", false)) { + throw new Error( + "Telegram forum topic actions are disabled. Set channels.telegram.actions.forumTopics to true.", + ); + } + const chatId = readStringOrNumberParam(params, "chatId", { + required: true, + }); + const messageThreadId = readNumberParam(params, "messageThreadId", { + required: true, + integer: true, + }); + 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.", + ); + } + await editForumTopicTelegram(chatId ?? "", messageThreadId ?? 0, { + token, + accountId: accountId ?? undefined, + name: name ?? undefined, + iconCustomEmojiId: iconCustomEmojiId ?? undefined, + }); + return jsonResult({ ok: true, edited: true }); + } + + if (action === "closeForumTopic") { + if (!isActionEnabled("forumTopics", false)) { + throw new Error( + "Telegram forum topic actions are disabled. Set channels.telegram.actions.forumTopics to true.", + ); + } + const chatId = readStringOrNumberParam(params, "chatId", { + required: true, + }); + const messageThreadId = readNumberParam(params, "messageThreadId", { + 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 closeForumTopicTelegram(chatId ?? "", messageThreadId ?? 0, { + token, + accountId: accountId ?? undefined, + }); + return jsonResult({ ok: true, closed: true }); + } + + if (action === "reopenForumTopic") { + if (!isActionEnabled("forumTopics", false)) { + throw new Error( + "Telegram forum topic actions are disabled. Set channels.telegram.actions.forumTopics to true.", + ); + } + const chatId = readStringOrNumberParam(params, "chatId", { + required: true, + }); + const messageThreadId = readNumberParam(params, "messageThreadId", { + 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 reopenForumTopicTelegram(chatId ?? "", messageThreadId ?? 0, { + token, + accountId: accountId ?? undefined, + }); + return jsonResult({ ok: true, reopened: true }); + } + + if (action === "deleteForumTopic") { + if (!isActionEnabled("forumTopics", false)) { + throw new Error( + "Telegram forum topic actions are disabled. Set channels.telegram.actions.forumTopics to true.", + ); + } + const chatId = readStringOrNumberParam(params, "chatId", { + required: true, + }); + const messageThreadId = readNumberParam(params, "messageThreadId", { + 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 ?? "", messageThreadId ?? 0, { + token, + accountId: accountId ?? undefined, + }); + return jsonResult({ ok: true, deleted: true }); + } + throw new Error(`Unsupported Telegram action: ${action}`); } diff --git a/src/channels/plugins/actions/telegram.test.ts b/src/channels/plugins/actions/telegram.test.ts index 623ecc292..72eb3788e 100644 --- a/src/channels/plugins/actions/telegram.test.ts +++ b/src/channels/plugins/actions/telegram.test.ts @@ -118,4 +118,209 @@ describe("telegramMessageActions", () => { expect(handleTelegramAction).not.toHaveBeenCalled(); }); + + it("excludes forum topic actions when not enabled", () => { + const cfg = { channels: { telegram: { botToken: "tok" } } } as MoltbotConfig; + const actions = telegramMessageActions.listActions({ cfg }); + expect(actions).not.toContain("topic-create"); + expect(actions).not.toContain("topic-edit"); + expect(actions).not.toContain("topic-close"); + expect(actions).not.toContain("topic-reopen"); + expect(actions).not.toContain("topic-delete"); + }); + + it("includes forum topic actions when forumTopics is enabled", () => { + const cfg = { + channels: { telegram: { botToken: "tok", actions: { forumTopics: true } } }, + } as MoltbotConfig; + const actions = telegramMessageActions.listActions({ cfg }); + expect(actions).toContain("topic-create"); + expect(actions).toContain("topic-edit"); + expect(actions).toContain("topic-close"); + expect(actions).toContain("topic-reopen"); + expect(actions).toContain("topic-delete"); + }); + + it("maps topic-create action to createForumTopic", async () => { + handleTelegramAction.mockClear(); + const cfg = { + channels: { telegram: { botToken: "tok", actions: { forumTopics: true } } }, + } as MoltbotConfig; + + await telegramMessageActions.handleAction({ + action: "topic-create", + params: { + to: "-1001234567890", + name: "My New Topic", + iconColor: 0x6fb9f0, + iconCustomEmojiId: "emoji123", + }, + cfg, + accountId: undefined, + }); + + expect(handleTelegramAction).toHaveBeenCalledWith( + { + action: "createForumTopic", + chatId: "-1001234567890", + name: "My New Topic", + iconColor: 0x6fb9f0, + iconCustomEmojiId: "emoji123", + accountId: undefined, + }, + cfg, + ); + }); + + it("maps topic-edit action to editForumTopic", async () => { + handleTelegramAction.mockClear(); + const cfg = { + channels: { telegram: { botToken: "tok", actions: { forumTopics: true } } }, + } as MoltbotConfig; + + await telegramMessageActions.handleAction({ + action: "topic-edit", + params: { + to: "-1001234567890", + messageThreadId: 42, + name: "Renamed Topic", + }, + cfg, + accountId: undefined, + }); + + expect(handleTelegramAction).toHaveBeenCalledWith( + { + action: "editForumTopic", + chatId: "-1001234567890", + messageThreadId: 42, + name: "Renamed Topic", + iconCustomEmojiId: undefined, + accountId: undefined, + }, + cfg, + ); + }); + + it("maps topic-close action to closeForumTopic", async () => { + handleTelegramAction.mockClear(); + const cfg = { + channels: { telegram: { botToken: "tok", actions: { forumTopics: true } } }, + } as MoltbotConfig; + + await telegramMessageActions.handleAction({ + action: "topic-close", + params: { + to: "-1001234567890", + messageThreadId: 42, + }, + cfg, + accountId: undefined, + }); + + expect(handleTelegramAction).toHaveBeenCalledWith( + { + action: "closeForumTopic", + chatId: "-1001234567890", + messageThreadId: 42, + accountId: undefined, + }, + cfg, + ); + }); + + it("maps topic-reopen action to reopenForumTopic", async () => { + handleTelegramAction.mockClear(); + const cfg = { + channels: { telegram: { botToken: "tok", actions: { forumTopics: true } } }, + } as MoltbotConfig; + + await telegramMessageActions.handleAction({ + action: "topic-reopen", + params: { + to: "-1001234567890", + messageThreadId: 42, + }, + cfg, + accountId: undefined, + }); + + expect(handleTelegramAction).toHaveBeenCalledWith( + { + action: "reopenForumTopic", + chatId: "-1001234567890", + messageThreadId: 42, + accountId: undefined, + }, + cfg, + ); + }); + + it("maps topic-delete action to deleteForumTopic", async () => { + handleTelegramAction.mockClear(); + const cfg = { + channels: { telegram: { botToken: "tok", actions: { forumTopics: true } } }, + } as MoltbotConfig; + + await telegramMessageActions.handleAction({ + action: "topic-delete", + params: { + to: "-1001234567890", + messageThreadId: 42, + }, + cfg, + accountId: undefined, + }); + + expect(handleTelegramAction).toHaveBeenCalledWith( + { + action: "deleteForumTopic", + chatId: "-1001234567890", + messageThreadId: 42, + accountId: undefined, + }, + cfg, + ); + }); + + it("requires messageThreadId for topic-edit", async () => { + handleTelegramAction.mockClear(); + const cfg = { + channels: { telegram: { botToken: "tok", actions: { forumTopics: true } } }, + } as MoltbotConfig; + + await expect( + telegramMessageActions.handleAction({ + action: "topic-edit", + params: { + to: "-1001234567890", + name: "Renamed", + }, + cfg, + accountId: undefined, + }), + ).rejects.toThrow(); + + expect(handleTelegramAction).not.toHaveBeenCalled(); + }); + + it("requires name for topic-create", async () => { + handleTelegramAction.mockClear(); + const cfg = { + channels: { telegram: { botToken: "tok", actions: { forumTopics: true } } }, + } as MoltbotConfig; + + await expect( + telegramMessageActions.handleAction({ + action: "topic-create", + params: { + to: "-1001234567890", + }, + cfg, + accountId: undefined, + }), + ).rejects.toThrow(); + + expect(handleTelegramAction).not.toHaveBeenCalled(); + }); }); diff --git a/src/channels/plugins/actions/telegram.ts b/src/channels/plugins/actions/telegram.ts index 693e94492..fa93cfb29 100644 --- a/src/channels/plugins/actions/telegram.ts +++ b/src/channels/plugins/actions/telegram.ts @@ -52,6 +52,13 @@ export const telegramMessageActions: ChannelMessageActionAdapter = { actions.add("sticker"); actions.add("sticker-search"); } + if (gate("forumTopics", false)) { + actions.add("topic-create"); + actions.add("topic-edit"); + actions.add("topic-close"); + actions.add("topic-reopen"); + actions.add("topic-delete"); + } return Array.from(actions); }, supportsButtons: ({ cfg }) => { @@ -183,6 +190,106 @@ export const telegramMessageActions: ChannelMessageActionAdapter = { ); } + if (action === "topic-create") { + const chatId = + readStringOrNumberParam(params, "chatId") ?? + readStringParam(params, "to", { required: true }); + const name = readStringParam(params, "name", { required: true }); + const iconColor = readNumberParam(params, "iconColor", { integer: true }); + const iconCustomEmojiId = readStringParam(params, "iconCustomEmojiId"); + return await handleTelegramAction( + { + action: "createForumTopic", + chatId, + name, + iconColor: iconColor ?? undefined, + iconCustomEmojiId: iconCustomEmojiId ?? undefined, + accountId: accountId ?? undefined, + }, + cfg, + ); + } + + if (action === "topic-edit") { + const chatId = + readStringOrNumberParam(params, "chatId") ?? + readStringParam(params, "to", { required: true }); + const messageThreadId = readNumberParam(params, "messageThreadId", { + required: true, + integer: true, + }); + 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, + ); + } + + if (action === "topic-close") { + const chatId = + readStringOrNumberParam(params, "chatId") ?? + readStringParam(params, "to", { required: true }); + const messageThreadId = readNumberParam(params, "messageThreadId", { + required: true, + integer: true, + }); + return await handleTelegramAction( + { + action: "closeForumTopic", + chatId, + messageThreadId, + accountId: accountId ?? undefined, + }, + cfg, + ); + } + + if (action === "topic-reopen") { + const chatId = + readStringOrNumberParam(params, "chatId") ?? + readStringParam(params, "to", { required: true }); + const messageThreadId = readNumberParam(params, "messageThreadId", { + required: true, + integer: true, + }); + return await handleTelegramAction( + { + action: "reopenForumTopic", + chatId, + messageThreadId, + accountId: accountId ?? undefined, + }, + cfg, + ); + } + + if (action === "topic-delete") { + const chatId = + readStringOrNumberParam(params, "chatId") ?? + readStringParam(params, "to", { required: true }); + const messageThreadId = readNumberParam(params, "messageThreadId", { + required: true, + integer: true, + }); + return await handleTelegramAction( + { + action: "deleteForumTopic", + chatId, + messageThreadId, + accountId: accountId ?? undefined, + }, + cfg, + ); + } + throw new Error(`Action ${action} is not supported for provider ${providerId}.`); }, }; diff --git a/src/channels/plugins/message-action-names.ts b/src/channels/plugins/message-action-names.ts index 1884cacb0..15db74bd4 100644 --- a/src/channels/plugins/message-action-names.ts +++ b/src/channels/plugins/message-action-names.ts @@ -48,6 +48,11 @@ export const CHANNEL_MESSAGE_ACTION_NAMES = [ "timeout", "kick", "ban", + "topic-create", + "topic-edit", + "topic-close", + "topic-reopen", + "topic-delete", ] as const; export type ChannelMessageActionName = (typeof CHANNEL_MESSAGE_ACTION_NAMES)[number]; diff --git a/src/config/types.telegram.ts b/src/config/types.telegram.ts index 9a96bce45..802252ffd 100644 --- a/src/config/types.telegram.ts +++ b/src/config/types.telegram.ts @@ -18,6 +18,8 @@ export type TelegramActionConfig = { editMessage?: boolean; /** Enable sticker actions (send and search). */ sticker?: boolean; + /** Enable forum topic management actions (create, edit, close, reopen, delete). */ + forumTopics?: boolean; }; export type TelegramNetworkConfig = { diff --git a/src/infra/outbound/message-action-spec.ts b/src/infra/outbound/message-action-spec.ts index 639e641d0..fe557506f 100644 --- a/src/infra/outbound/message-action-spec.ts +++ b/src/infra/outbound/message-action-spec.ts @@ -53,6 +53,11 @@ export const MESSAGE_ACTION_TARGET_MODE: Record> = { diff --git a/src/telegram/send.ts b/src/telegram/send.ts index e3f3ac30e..9c81ce27b 100644 --- a/src/telegram/send.ts +++ b/src/telegram/send.ts @@ -722,3 +722,236 @@ export async function sendStickerTelegram( return { messageId, chatId: resolvedChatId }; } + +// --------------------------------------------------------------------------- +// Forum topic management +// --------------------------------------------------------------------------- + +type TelegramForumTopicOpts = { + token?: string; + accountId?: string; + verbose?: boolean; + api?: Bot["api"]; + retry?: RetryConfig; +}; + +type TelegramCreateForumTopicResult = { + messageThreadId: number; + name: string; + iconColor: number; + iconCustomEmojiId?: string; +}; + +export async function createForumTopicTelegram( + chatIdInput: string | number, + name: string, + opts: TelegramForumTopicOpts & { + iconColor?: number; + iconCustomEmojiId?: string; + } = {}, +): Promise { + if (!name?.trim()) { + throw new Error("Forum topic name is required (1-128 characters)"); + } + const cfg = loadConfig(); + const account = resolveTelegramAccount({ + cfg, + accountId: opts.accountId, + }); + const token = resolveToken(opts.token, account); + const chatId = normalizeChatId(String(chatIdInput)); + const client = resolveTelegramClientOptions(account); + const api = opts.api ?? new Bot(token, client ? { client } : undefined).api; + const request = createTelegramRetryRunner({ + retry: opts.retry, + configRetry: account.config.retry, + verbose: opts.verbose, + shouldRetry: (err) => isRecoverableTelegramNetworkError(err, { context: "send" }), + }); + const logHttpError = createTelegramHttpLogger(cfg); + const requestWithDiag = (fn: () => Promise, label?: string) => + withTelegramApiErrorLogging({ + operation: label ?? "request", + fn: () => request(fn, label), + }).catch((err) => { + logHttpError(label ?? "request", err); + throw err; + }); + + const other: Record = {}; + if (opts.iconColor != null) other.icon_color = opts.iconColor; + if (opts.iconCustomEmojiId) other.icon_custom_emoji_id = opts.iconCustomEmojiId; + + const result = await requestWithDiag( + () => + api.createForumTopic(chatId, name.trim(), Object.keys(other).length > 0 ? other : undefined), + "createForumTopic", + ); + + logVerbose(`[telegram] Created forum topic "${name}" in chat ${chatId}`); + return { + messageThreadId: result.message_thread_id, + name: result.name, + iconColor: result.icon_color, + iconCustomEmojiId: result.icon_custom_emoji_id, + }; +} + +export async function editForumTopicTelegram( + chatIdInput: string | number, + messageThreadId: number, + opts: TelegramForumTopicOpts & { + name?: string; + iconCustomEmojiId?: string; + } = {}, +): Promise<{ ok: true }> { + const cfg = loadConfig(); + const account = resolveTelegramAccount({ + cfg, + accountId: opts.accountId, + }); + const token = resolveToken(opts.token, account); + const chatId = normalizeChatId(String(chatIdInput)); + const client = resolveTelegramClientOptions(account); + const api = opts.api ?? new Bot(token, client ? { client } : undefined).api; + const request = createTelegramRetryRunner({ + retry: opts.retry, + configRetry: account.config.retry, + verbose: opts.verbose, + shouldRetry: (err) => isRecoverableTelegramNetworkError(err, { context: "send" }), + }); + const logHttpError = createTelegramHttpLogger(cfg); + const requestWithDiag = (fn: () => Promise, label?: string) => + withTelegramApiErrorLogging({ + operation: label ?? "request", + fn: () => request(fn, label), + }).catch((err) => { + logHttpError(label ?? "request", err); + throw err; + }); + + const other: Record = {}; + if (opts.name != null) other.name = opts.name.trim(); + if (opts.iconCustomEmojiId != null) other.icon_custom_emoji_id = opts.iconCustomEmojiId; + + await requestWithDiag( + () => + api.editForumTopic( + chatId, + messageThreadId, + Object.keys(other).length > 0 ? other : undefined, + ), + "editForumTopic", + ); + + logVerbose(`[telegram] Edited forum topic ${messageThreadId} in chat ${chatId}`); + return { ok: true }; +} + +export async function closeForumTopicTelegram( + chatIdInput: string | number, + messageThreadId: number, + opts: TelegramForumTopicOpts = {}, +): Promise<{ ok: true }> { + const cfg = loadConfig(); + const account = resolveTelegramAccount({ + cfg, + accountId: opts.accountId, + }); + const token = resolveToken(opts.token, account); + const chatId = normalizeChatId(String(chatIdInput)); + const client = resolveTelegramClientOptions(account); + const api = opts.api ?? new Bot(token, client ? { client } : undefined).api; + const request = createTelegramRetryRunner({ + retry: opts.retry, + configRetry: account.config.retry, + verbose: opts.verbose, + shouldRetry: (err) => isRecoverableTelegramNetworkError(err, { context: "send" }), + }); + const logHttpError = createTelegramHttpLogger(cfg); + const requestWithDiag = (fn: () => Promise, label?: string) => + withTelegramApiErrorLogging({ + operation: label ?? "request", + fn: () => request(fn, label), + }).catch((err) => { + logHttpError(label ?? "request", err); + throw err; + }); + + await requestWithDiag(() => api.closeForumTopic(chatId, messageThreadId), "closeForumTopic"); + + logVerbose(`[telegram] Closed forum topic ${messageThreadId} in chat ${chatId}`); + return { ok: true }; +} + +export async function reopenForumTopicTelegram( + chatIdInput: string | number, + messageThreadId: number, + opts: TelegramForumTopicOpts = {}, +): Promise<{ ok: true }> { + const cfg = loadConfig(); + const account = resolveTelegramAccount({ + cfg, + accountId: opts.accountId, + }); + const token = resolveToken(opts.token, account); + const chatId = normalizeChatId(String(chatIdInput)); + const client = resolveTelegramClientOptions(account); + const api = opts.api ?? new Bot(token, client ? { client } : undefined).api; + const request = createTelegramRetryRunner({ + retry: opts.retry, + configRetry: account.config.retry, + verbose: opts.verbose, + shouldRetry: (err) => isRecoverableTelegramNetworkError(err, { context: "send" }), + }); + const logHttpError = createTelegramHttpLogger(cfg); + const requestWithDiag = (fn: () => Promise, label?: string) => + withTelegramApiErrorLogging({ + operation: label ?? "request", + fn: () => request(fn, label), + }).catch((err) => { + logHttpError(label ?? "request", err); + throw err; + }); + + await requestWithDiag(() => api.reopenForumTopic(chatId, messageThreadId), "reopenForumTopic"); + + logVerbose(`[telegram] Reopened forum topic ${messageThreadId} in chat ${chatId}`); + return { ok: true }; +} + +export async function deleteForumTopicTelegram( + chatIdInput: string | number, + messageThreadId: number, + opts: TelegramForumTopicOpts = {}, +): Promise<{ ok: true }> { + const cfg = loadConfig(); + const account = resolveTelegramAccount({ + cfg, + accountId: opts.accountId, + }); + const token = resolveToken(opts.token, account); + const chatId = normalizeChatId(String(chatIdInput)); + const client = resolveTelegramClientOptions(account); + const api = opts.api ?? new Bot(token, client ? { client } : undefined).api; + const request = createTelegramRetryRunner({ + retry: opts.retry, + configRetry: account.config.retry, + verbose: opts.verbose, + shouldRetry: (err) => isRecoverableTelegramNetworkError(err, { context: "send" }), + }); + const logHttpError = createTelegramHttpLogger(cfg); + const requestWithDiag = (fn: () => Promise, label?: string) => + withTelegramApiErrorLogging({ + operation: label ?? "request", + fn: () => request(fn, label), + }).catch((err) => { + logHttpError(label ?? "request", err); + throw err; + }); + + await requestWithDiag(() => api.deleteForumTopic(chatId, messageThreadId), "deleteForumTopic"); + + logVerbose(`[telegram] Deleted forum topic ${messageThreadId} in chat ${chatId}`); + return { ok: true }; +}