diff --git a/src/agents/tools/telegram-actions.ts b/src/agents/tools/telegram-actions.ts index 515ff8c47..e96c384bb 100644 --- a/src/agents/tools/telegram-actions.ts +++ b/src/agents/tools/telegram-actions.ts @@ -2,6 +2,7 @@ import type { AgentToolResult } from "@mariozechner/pi-agent-core"; import type { MoltbotConfig } from "../../config/config.js"; import { resolveTelegramReactionLevel } from "../../telegram/reaction-level.js"; import { + createForumTopicTelegram, deleteMessageTelegram, editMessageTelegram, reactMessageTelegram, @@ -318,5 +319,38 @@ export async function handleTelegramAction( return jsonResult({ ok: true, ...stats }); } + if (action === "createForumTopic") { + if (!isActionEnabled("createForumTopic", false)) { + throw new Error( + "Telegram createForumTopic is disabled. Set channels.telegram.actions.createForumTopic 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, + }); + } + throw new Error(`Unsupported Telegram action: ${action}`); } diff --git a/src/channels/plugins/actions/telegram.ts b/src/channels/plugins/actions/telegram.ts index 693e94492..01759c01f 100644 --- a/src/channels/plugins/actions/telegram.ts +++ b/src/channels/plugins/actions/telegram.ts @@ -52,6 +52,9 @@ export const telegramMessageActions: ChannelMessageActionAdapter = { actions.add("sticker"); actions.add("sticker-search"); } + if (gate("createForumTopic", false)) { + actions.add("thread-create"); + } return Array.from(actions); }, supportsButtons: ({ cfg }) => { @@ -183,6 +186,24 @@ export const telegramMessageActions: ChannelMessageActionAdapter = { ); } + if (action === "thread-create") { + const chatId = + readStringOrNumberParam(params, "chatId") ?? + readStringOrNumberParam(params, "channelId") ?? + readStringParam(params, "target") ?? + readStringParam(params, "to", { required: true }); + const name = readStringParam(params, "threadName", { required: true }); + return await handleTelegramAction( + { + action: "createForumTopic", + chatId, + name, + accountId: accountId ?? undefined, + }, + cfg, + ); + } + throw new Error(`Action ${action} is not supported for provider ${providerId}.`); }, }; diff --git a/src/config/types.telegram.ts b/src/config/types.telegram.ts index 9a96bce45..354ec7d76 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 creating forum topics in supergroups. */ + createForumTopic?: boolean; }; export type TelegramNetworkConfig = { diff --git a/src/telegram/send.ts b/src/telegram/send.ts index e3f3ac30e..2cfc7f698 100644 --- a/src/telegram/send.ts +++ b/src/telegram/send.ts @@ -617,6 +617,95 @@ export async function editMessageTelegram( return { ok: true, messageId: String(messageId), chatId }; } +type CreateForumTopicOpts = { + token?: string; + accountId?: string; + verbose?: boolean; + api?: Bot["api"]; + retry?: RetryConfig; + /** Color of the topic icon in RGB format (one of Telegram's allowed colors). */ + iconColor?: number; + /** Custom emoji ID for the topic icon. */ + iconCustomEmojiId?: string; +}; + +export type CreateForumTopicResult = { + ok: boolean; + messageThreadId?: number; + name?: string; + iconColor?: number; + iconCustomEmojiId?: string; +}; + +/** + * Create a new forum topic in a Telegram supergroup with topics enabled. + * @param chatIdInput - Chat ID of the supergroup + * @param name - Name of the topic (1-128 characters) + * @param opts - Optional configuration + */ +export async function createForumTopicTelegram( + chatIdInput: string | number, + name: string, + opts: CreateForumTopicOpts = {}, +): Promise { + if (!name?.trim()) { + throw new Error("Forum topic name is required"); + } + + 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, + }); + 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 params: Record = {}; + if (opts.iconColor != null) { + params.icon_color = opts.iconColor; + } + if (opts.iconCustomEmojiId) { + params.icon_custom_emoji_id = opts.iconCustomEmojiId; + } + + const res = await requestWithDiag( + () => + Object.keys(params).length > 0 + ? api.createForumTopic(chatId, name, params) + : api.createForumTopic(chatId, name), + "createForumTopic", + ); + + logVerbose( + `[telegram] Created forum topic "${name}" in chat ${chatId}, thread_id=${res?.message_thread_id}`, + ); + + return { + ok: true, + messageThreadId: res?.message_thread_id, + name: res?.name, + iconColor: res?.icon_color, + iconCustomEmojiId: res?.icon_custom_emoji_id, + }; +} + function inferFilename(kind: ReturnType) { switch (kind) { case "image":