feat(telegram): add createForumTopic support for thread-create action

- Add createForumTopicTelegram() to telegram/send.ts
- Add createForumTopic action handler in telegram-actions.ts
- Register thread-create action in telegram message plugin
- Add createForumTopic to TelegramActionConfig type

Allows agents to create forum topics in Telegram supergroups via:
message({ action: 'thread-create', channel: 'telegram', target: '<chatId>', threadName: 'Topic Name' })

Requires bot to have 'Manage Topics' admin permission in the group.
This commit is contained in:
Tyler Diaz 2026-01-28 13:36:18 -08:00
parent a7534dc223
commit d68139a8b2
4 changed files with 146 additions and 0 deletions

View File

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

View File

@ -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}.`);
},
};

View File

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

View File

@ -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<CreateForumTopicResult> {
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 = <T>(fn: () => Promise<T>, label?: string) =>
withTelegramApiErrorLogging({
operation: label ?? "request",
fn: () => request(fn, label),
}).catch((err) => {
logHttpError(label ?? "request", err);
throw err;
});
const params: Record<string, unknown> = {};
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<typeof mediaKindFromMime>) {
switch (kind) {
case "image":