This commit is contained in:
SynthWorkIO 2026-01-30 14:09:07 +03:00 committed by GitHub
commit 2be4fe4863
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 175 additions and 0 deletions

View File

@ -13,6 +13,7 @@ const sendStickerTelegram = vi.fn(async () => ({
chatId: "123",
}));
const deleteMessageTelegram = vi.fn(async () => ({ ok: true }));
const editForumTopicTelegram = vi.fn(async () => ({ ok: true }));
const originalToken = process.env.TELEGRAM_BOT_TOKEN;
vi.mock("../../telegram/send.js", () => ({
@ -20,6 +21,7 @@ vi.mock("../../telegram/send.js", () => ({
sendMessageTelegram: (...args: unknown[]) => sendMessageTelegram(...args),
sendStickerTelegram: (...args: unknown[]) => sendStickerTelegram(...args),
deleteMessageTelegram: (...args: unknown[]) => deleteMessageTelegram(...args),
editForumTopicTelegram: (...args: unknown[]) => editForumTopicTelegram(...args),
}));
describe("handleTelegramAction", () => {
@ -28,6 +30,7 @@ describe("handleTelegramAction", () => {
sendMessageTelegram.mockClear();
sendStickerTelegram.mockClear();
deleteMessageTelegram.mockClear();
editForumTopicTelegram.mockClear();
process.env.TELEGRAM_BOT_TOKEN = "tok";
});
@ -506,6 +509,80 @@ describe("handleTelegramAction", () => {
}),
);
});
it("edits a forum topic name", async () => {
const cfg = {
channels: { telegram: { botToken: "tok" } },
} as MoltbotConfig;
await handleTelegramAction(
{
action: "editForumTopic",
chatId: "-1001234567890",
messageThreadId: 42,
name: "New Topic Name",
},
cfg,
);
expect(editForumTopicTelegram).toHaveBeenCalledWith(
"-1001234567890",
42,
expect.objectContaining({
token: "tok",
name: "New Topic Name",
iconCustomEmojiId: undefined,
}),
);
});
it("respects editForumTopic gating", async () => {
const cfg = {
channels: {
telegram: { botToken: "tok", actions: { editForumTopic: false } },
},
} as MoltbotConfig;
await expect(
handleTelegramAction(
{
action: "editForumTopic",
chatId: "-1001234567890",
messageThreadId: 42,
name: "New Name",
},
cfg,
),
).rejects.toThrow(/Telegram editForumTopic is disabled/);
});
it("requires at least name or iconCustomEmojiId", async () => {
const cfg = {
channels: { telegram: { botToken: "tok" } },
} as MoltbotConfig;
await expect(
handleTelegramAction(
{
action: "editForumTopic",
chatId: "-1001234567890",
messageThreadId: 42,
},
cfg,
),
).rejects.toThrow(/At least one of name or iconCustomEmojiId is required/);
});
it("requires chatId and messageThreadId", async () => {
const cfg = {
channels: { telegram: { botToken: "tok" } },
} as MoltbotConfig;
await expect(
handleTelegramAction(
{
action: "editForumTopic",
name: "New Name",
},
cfg,
),
).rejects.toThrow(/chatId required/i);
});
});
describe("readTelegramButtons", () => {

View File

@ -3,6 +3,7 @@ import type { OpenClawConfig } from "../../config/config.js";
import { resolveTelegramReactionLevel } from "../../telegram/reaction-level.js";
import {
deleteMessageTelegram,
editForumTopicTelegram,
editMessageTelegram,
reactMessageTelegram,
sendMessageTelegram,
@ -318,5 +319,36 @@ export async function handleTelegramAction(
return jsonResult({ ok: true, ...stats });
}
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", {
required: true,
integer: true,
});
const name = readStringParam(params, "name");
const iconCustomEmojiId = readStringParam(params, "iconCustomEmojiId");
if (!name && !iconCustomEmojiId) {
throw new Error("At least one of name or iconCustomEmojiId is required.");
}
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 });
}
throw new Error(`Unsupported Telegram action: ${action}`);
}

View File

@ -16,6 +16,8 @@ export type TelegramActionConfig = {
sendMessage?: boolean;
deleteMessage?: boolean;
editMessage?: boolean;
/** Enable editing forum topic name/icon. */
editForumTopic?: boolean;
/** Enable sticker actions (send and search). */
sticker?: boolean;
};

View File

@ -630,6 +630,14 @@ function inferFilename(kind: ReturnType<typeof mediaKindFromMime>) {
}
}
type TelegramEditForumTopicOpts = {
token?: string;
accountId?: string;
verbose?: boolean;
api?: Bot["api"];
retry?: RetryConfig;
};
type TelegramStickerOpts = {
token?: string;
accountId?: string;
@ -642,6 +650,62 @@ type TelegramStickerOpts = {
messageThreadId?: number;
};
export async function editForumTopicTelegram(
chatIdInput: string | number,
messageThreadId: number,
opts: TelegramEditForumTopicOpts & {
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 = <T>(fn: () => Promise<T>, label?: string) =>
withTelegramApiErrorLogging({
operation: label ?? "request",
fn: () => request(fn, label),
}).catch((err) => {
logHttpError(label ?? "request", err);
throw err;
});
// The General topic (thread_id = 1) uses a different API method
if (messageThreadId === 1) {
if (!opts.name) {
throw new Error("name is required for editing the General forum topic");
}
await requestWithDiag(
() => api.editGeneralForumTopic(chatId, opts.name),
"editGeneralForumTopic",
);
} else {
const editParams: { name?: string; icon_custom_emoji_id?: string } = {};
if (opts.name) editParams.name = opts.name;
if (opts.iconCustomEmojiId) editParams.icon_custom_emoji_id = opts.iconCustomEmojiId;
await requestWithDiag(
() => api.editForumTopic(chatId, messageThreadId, editParams),
"editForumTopic",
);
}
logVerbose(`[telegram] Edited forum topic ${messageThreadId} in chat ${chatId}`);
return { ok: true };
}
/**
* Send a sticker to a Telegram chat by file_id.
* @param to - Chat ID or username (e.g., "123456789" or "@username")