Compare commits
11 Commits
secrets/pr
...
feat/teleg
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1cd55a211 | ||
|
|
88d1b9126d | ||
|
|
b007163057 | ||
|
|
68809ab80a | ||
|
|
ae95bd3ec8 | ||
|
|
6829b3c322 | ||
|
|
d861a42ba8 | ||
|
|
b5bfb73bce | ||
|
|
740dbf6ad9 | ||
|
|
81e9c625b5 | ||
|
|
1754daa0be |
@@ -8,6 +8,7 @@ import {
|
||||
} from "../../telegram/inline-buttons.js";
|
||||
import { resolveTelegramReactionLevel } from "../../telegram/reaction-level.js";
|
||||
import {
|
||||
createForumTopicTelegram,
|
||||
deleteMessageTelegram,
|
||||
editMessageTelegram,
|
||||
reactMessageTelegram,
|
||||
@@ -339,5 +340,35 @@ export async function handleTelegramAction(
|
||||
return jsonResult({ ok: true, ...stats });
|
||||
}
|
||||
|
||||
if (action === "createForumTopic") {
|
||||
if (!isActionEnabled("createForumTopic")) {
|
||||
throw new Error("Telegram createForumTopic is disabled.");
|
||||
}
|
||||
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,
|
||||
topicId: result.topicId,
|
||||
name: result.name,
|
||||
chatId: result.chatId,
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported Telegram action: ${action}`);
|
||||
}
|
||||
|
||||
@@ -498,6 +498,33 @@ describe("telegramMessageActions", () => {
|
||||
expect(String(callPayload.messageId)).toBe("456");
|
||||
expect(callPayload.emoji).toBe("ok");
|
||||
});
|
||||
|
||||
it("maps topic-create params into createForumTopic", async () => {
|
||||
const cfg = { channels: { telegram: { botToken: "tok" } } } as OpenClawConfig;
|
||||
|
||||
await telegramMessageActions.handleAction?.({
|
||||
channel: "telegram",
|
||||
action: "topic-create",
|
||||
params: {
|
||||
to: "telegram:group:-1001234567890:topic:271",
|
||||
name: "Build Updates",
|
||||
},
|
||||
cfg,
|
||||
accountId: undefined,
|
||||
});
|
||||
|
||||
expect(handleTelegramAction).toHaveBeenCalledWith(
|
||||
{
|
||||
action: "createForumTopic",
|
||||
chatId: "telegram:group:-1001234567890:topic:271",
|
||||
name: "Build Updates",
|
||||
iconColor: undefined,
|
||||
iconCustomEmojiId: undefined,
|
||||
accountId: undefined,
|
||||
},
|
||||
cfg,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("signalMessageActions", () => {
|
||||
|
||||
@@ -69,6 +69,9 @@ export const telegramMessageActions: ChannelMessageActionAdapter = {
|
||||
actions.add("sticker");
|
||||
actions.add("sticker-search");
|
||||
}
|
||||
if (gate("createForumTopic")) {
|
||||
actions.add("topic-create");
|
||||
}
|
||||
return Array.from(actions);
|
||||
},
|
||||
supportsButtons: ({ cfg }) => {
|
||||
@@ -199,6 +202,27 @@ export const telegramMessageActions: ChannelMessageActionAdapter = {
|
||||
);
|
||||
}
|
||||
|
||||
if (action === "topic-create") {
|
||||
const chatId =
|
||||
readStringOrNumberParam(params, "chatId") ??
|
||||
readStringOrNumberParam(params, "channelId") ??
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(`Action ${action} is not supported for provider ${providerId}.`);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -42,6 +42,7 @@ export const CHANNEL_MESSAGE_ACTION_NAMES = [
|
||||
"category-create",
|
||||
"category-edit",
|
||||
"category-delete",
|
||||
"topic-create",
|
||||
"voice-status",
|
||||
"event-list",
|
||||
"event-create",
|
||||
|
||||
@@ -18,6 +18,8 @@ export type TelegramActionConfig = {
|
||||
editMessage?: boolean;
|
||||
/** Enable sticker actions (send and search). */
|
||||
sticker?: boolean;
|
||||
/** Enable forum topic creation. */
|
||||
createForumTopic?: boolean;
|
||||
};
|
||||
|
||||
export type TelegramNetworkConfig = {
|
||||
|
||||
@@ -47,6 +47,7 @@ export const MESSAGE_ACTION_TARGET_MODE: Record<ChannelMessageActionName, Messag
|
||||
"category-create": "none",
|
||||
"category-edit": "none",
|
||||
"category-delete": "none",
|
||||
"topic-create": "to",
|
||||
"voice-status": "none",
|
||||
"event-list": "none",
|
||||
"event-create": "none",
|
||||
|
||||
@@ -12,6 +12,7 @@ installTelegramSendTestHooks();
|
||||
const { botApi, botCtorSpy, loadConfig, loadWebMedia } = getTelegramSendTestMocks();
|
||||
const {
|
||||
buildInlineKeyboard,
|
||||
createForumTopicTelegram,
|
||||
editMessageTelegram,
|
||||
reactMessageTelegram,
|
||||
sendMessageTelegram,
|
||||
@@ -1434,3 +1435,49 @@ describe("sendPollTelegram", () => {
|
||||
expect(api.sendPoll).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("createForumTopicTelegram", () => {
|
||||
it("uses base chat id when target includes topic suffix", async () => {
|
||||
const createForumTopic = vi.fn().mockResolvedValue({
|
||||
message_thread_id: 272,
|
||||
name: "Build Updates",
|
||||
});
|
||||
const api = { createForumTopic } as unknown as {
|
||||
createForumTopic: typeof createForumTopic;
|
||||
};
|
||||
|
||||
const result = await createForumTopicTelegram("telegram:group:-1001234567890:topic:271", "x", {
|
||||
token: "tok",
|
||||
api,
|
||||
});
|
||||
|
||||
expect(createForumTopic).toHaveBeenCalledWith("-1001234567890", "x", undefined);
|
||||
expect(result).toEqual({
|
||||
topicId: 272,
|
||||
name: "Build Updates",
|
||||
chatId: "-1001234567890",
|
||||
});
|
||||
});
|
||||
|
||||
it("forwards optional icon fields", async () => {
|
||||
const createForumTopic = vi.fn().mockResolvedValue({
|
||||
message_thread_id: 300,
|
||||
name: "Roadmap",
|
||||
});
|
||||
const api = { createForumTopic } as unknown as {
|
||||
createForumTopic: typeof createForumTopic;
|
||||
};
|
||||
|
||||
await createForumTopicTelegram("-1001234567890", "Roadmap", {
|
||||
token: "tok",
|
||||
api,
|
||||
iconColor: 0x6fb9f0,
|
||||
iconCustomEmojiId: " 1234567890 ",
|
||||
});
|
||||
|
||||
expect(createForumTopic).toHaveBeenCalledWith("-1001234567890", "Roadmap", {
|
||||
icon_color: 0x6fb9f0,
|
||||
icon_custom_emoji_id: "1234567890",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1077,3 +1077,104 @@ export async function sendPollTelegram(
|
||||
|
||||
return { messageId, chatId: resolvedChatId, pollId };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Forum topic creation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type TelegramCreateForumTopicOpts = {
|
||||
token?: string;
|
||||
accountId?: string;
|
||||
api?: Bot["api"];
|
||||
verbose?: boolean;
|
||||
retry?: RetryConfig;
|
||||
/** Icon color for the topic (must be one of 0x6FB9F0, 0xFFD67E, 0xCB86DB, 0x8EEE98, 0xFF93B2, 0xFB6F5F). */
|
||||
iconColor?: number;
|
||||
/** Custom emoji ID for the topic icon. */
|
||||
iconCustomEmojiId?: string;
|
||||
};
|
||||
|
||||
export type TelegramCreateForumTopicResult = {
|
||||
topicId: number;
|
||||
name: string;
|
||||
chatId: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a forum topic in a Telegram supergroup.
|
||||
* Requires the bot to have `can_manage_topics` permission.
|
||||
*
|
||||
* @param chatId - Supergroup chat ID
|
||||
* @param name - Topic name (1-128 characters)
|
||||
* @param opts - Optional configuration
|
||||
*/
|
||||
export async function createForumTopicTelegram(
|
||||
chatId: string,
|
||||
name: string,
|
||||
opts: TelegramCreateForumTopicOpts = {},
|
||||
): Promise<TelegramCreateForumTopicResult> {
|
||||
if (!name?.trim()) {
|
||||
throw new Error("Forum topic name is required");
|
||||
}
|
||||
const trimmedName = name.trim();
|
||||
if (trimmedName.length > 128) {
|
||||
throw new Error("Forum topic name must be 128 characters or fewer");
|
||||
}
|
||||
|
||||
const cfg = loadConfig();
|
||||
const account = resolveTelegramAccount({
|
||||
cfg,
|
||||
accountId: opts.accountId,
|
||||
});
|
||||
const token = resolveToken(opts.token, account);
|
||||
// Accept topic-qualified targets (e.g. telegram:group:<id>:topic:<thread>)
|
||||
// but createForumTopic must always target the base supergroup chat id.
|
||||
const target = parseTelegramTarget(chatId);
|
||||
const normalizedChatId = normalizeChatId(target.chatId);
|
||||
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 = <T>(fn: () => Promise<T>, label?: string) =>
|
||||
withTelegramApiErrorLogging({
|
||||
operation: label ?? "request",
|
||||
fn: () => request(fn, label),
|
||||
}).catch((err) => {
|
||||
logHttpError(label ?? "request", err);
|
||||
throw err;
|
||||
});
|
||||
|
||||
const extra: Record<string, unknown> = {};
|
||||
if (opts.iconColor != null) {
|
||||
extra.icon_color = opts.iconColor;
|
||||
}
|
||||
if (opts.iconCustomEmojiId?.trim()) {
|
||||
extra.icon_custom_emoji_id = opts.iconCustomEmojiId.trim();
|
||||
}
|
||||
|
||||
const hasExtra = Object.keys(extra).length > 0;
|
||||
const result = await requestWithDiag(
|
||||
() => api.createForumTopic(normalizedChatId, trimmedName, hasExtra ? extra : undefined),
|
||||
"createForumTopic",
|
||||
);
|
||||
|
||||
const topicId = result.message_thread_id;
|
||||
|
||||
recordChannelActivity({
|
||||
channel: "telegram",
|
||||
accountId: account.accountId,
|
||||
direction: "outbound",
|
||||
});
|
||||
|
||||
return {
|
||||
topicId,
|
||||
name: result.name ?? trimmedName,
|
||||
chatId: normalizedChatId,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user