mirror of https://github.com/openclaw/openclaw.git
Merge d8fae6c4d7 into c4265a5f16
This commit is contained in:
commit
f2739985f5
|
|
@ -424,10 +424,14 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
|
||||||
- `sendMessage` (`to`, `content`, optional `mediaUrl`, `replyToMessageId`, `messageThreadId`)
|
- `sendMessage` (`to`, `content`, optional `mediaUrl`, `replyToMessageId`, `messageThreadId`)
|
||||||
- `react` (`chatId`, `messageId`, `emoji`)
|
- `react` (`chatId`, `messageId`, `emoji`)
|
||||||
- `deleteMessage` (`chatId`, `messageId`)
|
- `deleteMessage` (`chatId`, `messageId`)
|
||||||
|
- `deleteForumTopic` (`chatId`, `topicId`)
|
||||||
- `editMessage` (`chatId`, `messageId`, `content`)
|
- `editMessage` (`chatId`, `messageId`, `content`)
|
||||||
- `createForumTopic` (`chatId`, `name`, optional `iconColor`, `iconCustomEmojiId`)
|
- `createForumTopic` (`chatId`, `name`, optional `iconColor`, `iconCustomEmojiId`)
|
||||||
|
|
||||||
Channel message actions expose ergonomic aliases (`send`, `react`, `delete`, `edit`, `sticker`, `sticker-search`, `topic-create`).
|
Channel message actions expose ergonomic aliases (`send`, `react`, `delete`, `topic-delete`, `edit`, `sticker`, `sticker-search`, `topic-create`).
|
||||||
|
- `delete`: deletes a message and requires `messageId`.
|
||||||
|
- `topic-delete`: deletes a forum topic and requires explicit `threadId`/`topicId`.
|
||||||
|
Backward compatibility: `delete` with explicit `threadId`/`topicId` is still accepted for now.
|
||||||
|
|
||||||
Gating controls:
|
Gating controls:
|
||||||
|
|
||||||
|
|
@ -969,7 +973,7 @@ Telegram-specific high-signal fields:
|
||||||
- formatting/delivery: `textChunkLimit`, `chunkMode`, `linkPreview`, `responsePrefix`
|
- formatting/delivery: `textChunkLimit`, `chunkMode`, `linkPreview`, `responsePrefix`
|
||||||
- media/network: `mediaMaxMb`, `timeoutSeconds`, `retry`, `network.autoSelectFamily`, `proxy`
|
- media/network: `mediaMaxMb`, `timeoutSeconds`, `retry`, `network.autoSelectFamily`, `proxy`
|
||||||
- webhook: `webhookUrl`, `webhookSecret`, `webhookPath`, `webhookHost`
|
- webhook: `webhookUrl`, `webhookSecret`, `webhookPath`, `webhookHost`
|
||||||
- actions/capabilities: `capabilities.inlineButtons`, `actions.sendMessage|editMessage|deleteMessage|reactions|sticker`
|
- actions/capabilities: `capabilities.inlineButtons`, `actions.sendMessage|editMessage|deleteMessage|reactions|sticker` (both `delete` and `topic-delete` route through `actions.deleteMessage`)
|
||||||
- reactions: `reactionNotifications`, `reactionLevel`
|
- reactions: `reactionNotifications`, `reactionLevel`
|
||||||
- writes/history: `configWrites`, `historyLimit`, `dmHistoryLimit`, `dms.*.historyLimit`
|
- writes/history: `configWrites`, `historyLimit`, `dmHistoryLimit`, `dms.*.historyLimit`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,15 +62,49 @@ function readTelegramChatIdParam(params: Record<string, unknown>): string | numb
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function readTelegramMessageIdParam(params: Record<string, unknown>): number {
|
function readTelegramMessageIdParam(
|
||||||
|
params: Record<string, unknown>,
|
||||||
|
options?: { required?: boolean },
|
||||||
|
): number | undefined {
|
||||||
|
const required = options?.required ?? true;
|
||||||
const messageId = readNumberParam(params, "messageId", {
|
const messageId = readNumberParam(params, "messageId", {
|
||||||
required: true,
|
required,
|
||||||
integer: true,
|
integer: true,
|
||||||
|
strict: true,
|
||||||
|
strictInteger: true,
|
||||||
});
|
});
|
||||||
if (typeof messageId !== "number") {
|
if (required && typeof messageId !== "number") {
|
||||||
throw new Error("messageId is required.");
|
throw new Error("messageId is required.");
|
||||||
}
|
}
|
||||||
return messageId;
|
return typeof messageId === "number" ? messageId : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readTelegramTopicIdParam(params: Record<string, unknown>): number | undefined {
|
||||||
|
const hasTopicIdParam = Object.hasOwn(params, "topicId") || Object.hasOwn(params, "topic_id");
|
||||||
|
const topicId = readNumberParam(params, "topicId", {
|
||||||
|
integer: true,
|
||||||
|
strict: true,
|
||||||
|
strictInteger: true,
|
||||||
|
});
|
||||||
|
if (hasTopicIdParam && typeof topicId !== "number") {
|
||||||
|
throw new Error("topicId must be a valid integer when provided.");
|
||||||
|
}
|
||||||
|
if (typeof topicId === "number") {
|
||||||
|
return topicId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasThreadIdParam =
|
||||||
|
Object.hasOwn(params, "threadId") || Object.hasOwn(params, "thread_id");
|
||||||
|
const threadId = readNumberParam(params, "threadId", {
|
||||||
|
integer: true,
|
||||||
|
strict: true,
|
||||||
|
strictInteger: true,
|
||||||
|
});
|
||||||
|
if (hasThreadIdParam && typeof threadId !== "number") {
|
||||||
|
throw new Error("threadId must be a valid integer when provided.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return threadId;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const telegramMessageActions: ChannelMessageActionAdapter = {
|
export const telegramMessageActions: ChannelMessageActionAdapter = {
|
||||||
|
|
@ -104,6 +138,7 @@ export const telegramMessageActions: ChannelMessageActionAdapter = {
|
||||||
}
|
}
|
||||||
if (isEnabled("deleteMessage")) {
|
if (isEnabled("deleteMessage")) {
|
||||||
actions.add("delete");
|
actions.add("delete");
|
||||||
|
actions.add("topic-delete");
|
||||||
}
|
}
|
||||||
if (isEnabled("editMessage")) {
|
if (isEnabled("editMessage")) {
|
||||||
actions.add("edit");
|
actions.add("edit");
|
||||||
|
|
@ -202,7 +237,13 @@ export const telegramMessageActions: ChannelMessageActionAdapter = {
|
||||||
|
|
||||||
if (action === "delete") {
|
if (action === "delete") {
|
||||||
const chatId = readTelegramChatIdParam(params);
|
const chatId = readTelegramChatIdParam(params);
|
||||||
const messageId = readTelegramMessageIdParam(params);
|
const hasMessageIdParam =
|
||||||
|
Object.hasOwn(params, "messageId") || Object.hasOwn(params, "message_id");
|
||||||
|
const messageId = readTelegramMessageIdParam(params, { required: false });
|
||||||
|
if (hasMessageIdParam && typeof messageId !== "number") {
|
||||||
|
throw new Error("messageId must be a valid number for action=delete.");
|
||||||
|
}
|
||||||
|
if (typeof messageId === "number") {
|
||||||
return await handleTelegramAction(
|
return await handleTelegramAction(
|
||||||
{
|
{
|
||||||
action: "deleteMessage",
|
action: "deleteMessage",
|
||||||
|
|
@ -215,6 +256,41 @@ export const telegramMessageActions: ChannelMessageActionAdapter = {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const topicId = readTelegramTopicIdParam(params);
|
||||||
|
if (typeof topicId === "number") {
|
||||||
|
return await handleTelegramAction(
|
||||||
|
{
|
||||||
|
action: "deleteForumTopic",
|
||||||
|
chatId,
|
||||||
|
topicId,
|
||||||
|
accountId: accountId ?? undefined,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
{ mediaLocalRoots },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("messageId is required for action=delete.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "topic-delete") {
|
||||||
|
const chatId = readTelegramChatIdParam(params);
|
||||||
|
const topicId = readTelegramTopicIdParam(params);
|
||||||
|
if (typeof topicId !== "number") {
|
||||||
|
throw new Error("threadId/topicId is required for action=topic-delete.");
|
||||||
|
}
|
||||||
|
return await handleTelegramAction(
|
||||||
|
{
|
||||||
|
action: "deleteForumTopic",
|
||||||
|
chatId,
|
||||||
|
topicId,
|
||||||
|
accountId: accountId ?? undefined,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
{ mediaLocalRoots },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (action === "edit") {
|
if (action === "edit") {
|
||||||
const chatId = readTelegramChatIdParam(params);
|
const chatId = readTelegramChatIdParam(params);
|
||||||
const messageId = readTelegramMessageIdParam(params);
|
const messageId = readTelegramMessageIdParam(params);
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ const { botApi, botCtorSpy } = vi.hoisted(() => ({
|
||||||
sendMessage: vi.fn(),
|
sendMessage: vi.fn(),
|
||||||
setMessageReaction: vi.fn(),
|
setMessageReaction: vi.fn(),
|
||||||
deleteMessage: vi.fn(),
|
deleteMessage: vi.fn(),
|
||||||
|
deleteForumTopic: vi.fn(),
|
||||||
},
|
},
|
||||||
botCtorSpy: vi.fn(),
|
botCtorSpy: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
@ -52,6 +53,7 @@ vi.mock("grammy", () => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
deleteForumTopicTelegram,
|
||||||
deleteMessageTelegram,
|
deleteMessageTelegram,
|
||||||
reactMessageTelegram,
|
reactMessageTelegram,
|
||||||
resetTelegramClientOptionsCacheForTests,
|
resetTelegramClientOptionsCacheForTests,
|
||||||
|
|
@ -86,6 +88,7 @@ describe("telegram proxy client", () => {
|
||||||
botApi.sendMessage.mockResolvedValue({ message_id: 1, chat: { id: "123" } });
|
botApi.sendMessage.mockResolvedValue({ message_id: 1, chat: { id: "123" } });
|
||||||
botApi.setMessageReaction.mockResolvedValue(undefined);
|
botApi.setMessageReaction.mockResolvedValue(undefined);
|
||||||
botApi.deleteMessage.mockResolvedValue(true);
|
botApi.deleteMessage.mockResolvedValue(true);
|
||||||
|
botApi.deleteForumTopic.mockResolvedValue(true);
|
||||||
botCtorSpy.mockClear();
|
botCtorSpy.mockClear();
|
||||||
loadConfig.mockReturnValue({
|
loadConfig.mockReturnValue({
|
||||||
channels: { telegram: { accounts: { foo: { proxy: proxyUrl } } } },
|
channels: { telegram: { accounts: { foo: { proxy: proxyUrl } } } },
|
||||||
|
|
@ -134,6 +137,10 @@ describe("telegram proxy client", () => {
|
||||||
name: "deleteMessage",
|
name: "deleteMessage",
|
||||||
run: () => deleteMessageTelegram("123", "456", { token: "tok", accountId: "foo" }),
|
run: () => deleteMessageTelegram("123", "456", { token: "tok", accountId: "foo" }),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "deleteForumTopic",
|
||||||
|
run: () => deleteForumTopicTelegram("123", "456", { token: "tok", accountId: "foo" }),
|
||||||
|
},
|
||||||
])("uses proxy fetch for $name", async (testCase) => {
|
])("uses proxy fetch for $name", async (testCase) => {
|
||||||
const { fetchImpl } = prepareProxyFetch();
|
const { fetchImpl } = prepareProxyFetch();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import type { MockFn } from "../../../src/test-utils/vitest-mock-fn.js";
|
||||||
const { botApi, botCtorSpy } = vi.hoisted(() => ({
|
const { botApi, botCtorSpy } = vi.hoisted(() => ({
|
||||||
botApi: {
|
botApi: {
|
||||||
deleteMessage: vi.fn(),
|
deleteMessage: vi.fn(),
|
||||||
|
deleteForumTopic: vi.fn(),
|
||||||
editMessageText: vi.fn(),
|
editMessageText: vi.fn(),
|
||||||
sendChatAction: vi.fn(),
|
sendChatAction: vi.fn(),
|
||||||
sendMessage: vi.fn(),
|
sendMessage: vi.fn(),
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ const { botApi, botCtorSpy, loadConfig, loadWebMedia, maybePersistResolvedTelegr
|
||||||
const {
|
const {
|
||||||
buildInlineKeyboard,
|
buildInlineKeyboard,
|
||||||
createForumTopicTelegram,
|
createForumTopicTelegram,
|
||||||
|
deleteForumTopicTelegram,
|
||||||
editMessageTelegram,
|
editMessageTelegram,
|
||||||
reactMessageTelegram,
|
reactMessageTelegram,
|
||||||
sendMessageTelegram,
|
sendMessageTelegram,
|
||||||
|
|
@ -1950,6 +1951,38 @@ describe("sendPollTelegram", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("deleteForumTopicTelegram", () => {
|
||||||
|
const cases = [
|
||||||
|
{
|
||||||
|
name: "uses base chat id when target includes topic suffix",
|
||||||
|
target: "telegram:group:-1001234567890:topic:271",
|
||||||
|
topicId: 271,
|
||||||
|
expectedCall: ["-1001234567890", 271] as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "accepts plain chat ids",
|
||||||
|
target: "-1001234567890",
|
||||||
|
topicId: 300,
|
||||||
|
expectedCall: ["-1001234567890", 300] as const,
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
for (const testCase of cases) {
|
||||||
|
it(testCase.name, async () => {
|
||||||
|
const deleteForumTopic = vi.fn().mockResolvedValue(true);
|
||||||
|
const api = { deleteForumTopic } as unknown as Bot["api"];
|
||||||
|
|
||||||
|
const result = await deleteForumTopicTelegram(testCase.target, testCase.topicId, {
|
||||||
|
token: "tok",
|
||||||
|
api,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(deleteForumTopic).toHaveBeenCalledWith(...testCase.expectedCall);
|
||||||
|
expect(result).toEqual({ ok: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
describe("createForumTopicTelegram", () => {
|
describe("createForumTopicTelegram", () => {
|
||||||
const cases = [
|
const cases = [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1438,6 +1438,49 @@ export async function sendPollTelegram(
|
||||||
return { messageId: String(messageId), chatId: resolvedChatId, pollId };
|
return { messageId: String(messageId), chatId: resolvedChatId, pollId };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Forum topic deletion
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type TelegramDeleteForumTopicOpts = {
|
||||||
|
cfg?: ReturnType<typeof loadConfig>;
|
||||||
|
token?: string;
|
||||||
|
accountId?: string;
|
||||||
|
verbose?: boolean;
|
||||||
|
api?: TelegramApiOverride;
|
||||||
|
retry?: RetryConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function deleteForumTopicTelegram(
|
||||||
|
chatIdInput: string | number,
|
||||||
|
topicIdInput: string | number,
|
||||||
|
opts: TelegramDeleteForumTopicOpts = {},
|
||||||
|
): Promise<{ ok: true }> {
|
||||||
|
const { cfg, account, api } = resolveTelegramApiContext(opts);
|
||||||
|
const parsedTarget = parseTelegramTarget(String(chatIdInput));
|
||||||
|
const chatId = await resolveAndPersistChatId({
|
||||||
|
cfg,
|
||||||
|
api,
|
||||||
|
lookupTarget: parsedTarget.chatId,
|
||||||
|
persistTarget: String(chatIdInput),
|
||||||
|
verbose: opts.verbose,
|
||||||
|
});
|
||||||
|
const topicId = normalizeMessageId(topicIdInput);
|
||||||
|
const requestWithDiag = createTelegramRequestWithDiag({
|
||||||
|
cfg,
|
||||||
|
account,
|
||||||
|
retry: opts.retry,
|
||||||
|
verbose: opts.verbose,
|
||||||
|
shouldRetry: (err) => isRecoverableTelegramNetworkError(err, { context: "send" }),
|
||||||
|
});
|
||||||
|
if (typeof api.deleteForumTopic !== "function") {
|
||||||
|
throw new Error("Telegram forum topic deletion is unavailable in this bot API.");
|
||||||
|
}
|
||||||
|
await requestWithDiag(() => api.deleteForumTopic(chatId, topicId), "deleteForumTopic");
|
||||||
|
logVerbose(`[telegram] Deleted forum topic ${topicId} from chat ${chatId}`);
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Forum topic creation
|
// Forum topic creation
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,13 @@ describe("readNumberParam", () => {
|
||||||
expect(readNumberParam(params, "messageId", { integer: true })).toBe(42);
|
expect(readNumberParam(params, "messageId", { integer: true })).toBe(42);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("rejects fractional values when strictInteger is true", () => {
|
||||||
|
const params = { messageId: "42.9" };
|
||||||
|
expect(() =>
|
||||||
|
readNumberParam(params, "messageId", { integer: true, strictInteger: true }),
|
||||||
|
).toThrow(/messageId must be an integer/);
|
||||||
|
});
|
||||||
|
|
||||||
it("accepts snake_case aliases for camelCase keys", () => {
|
it("accepts snake_case aliases for camelCase keys", () => {
|
||||||
const params = { message_id: "42" };
|
const params = { message_id: "42" };
|
||||||
expect(readNumberParam(params, "messageId")).toBe(42);
|
expect(readNumberParam(params, "messageId")).toBe(42);
|
||||||
|
|
|
||||||
|
|
@ -116,9 +116,21 @@ export function readStringOrNumberParam(
|
||||||
export function readNumberParam(
|
export function readNumberParam(
|
||||||
params: Record<string, unknown>,
|
params: Record<string, unknown>,
|
||||||
key: string,
|
key: string,
|
||||||
options: { required?: boolean; label?: string; integer?: boolean; strict?: boolean } = {},
|
options: {
|
||||||
|
required?: boolean;
|
||||||
|
label?: string;
|
||||||
|
integer?: boolean;
|
||||||
|
strict?: boolean;
|
||||||
|
strictInteger?: boolean;
|
||||||
|
} = {},
|
||||||
): number | undefined {
|
): number | undefined {
|
||||||
const { required = false, label = key, integer = false, strict = false } = options;
|
const {
|
||||||
|
required = false,
|
||||||
|
label = key,
|
||||||
|
integer = false,
|
||||||
|
strict = false,
|
||||||
|
strictInteger = false,
|
||||||
|
} = options;
|
||||||
const raw = readParamRaw(params, key);
|
const raw = readParamRaw(params, key);
|
||||||
let value: number | undefined;
|
let value: number | undefined;
|
||||||
if (typeof raw === "number" && Number.isFinite(raw)) {
|
if (typeof raw === "number" && Number.isFinite(raw)) {
|
||||||
|
|
@ -138,7 +150,13 @@ export function readNumberParam(
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return integer ? Math.trunc(value) : value;
|
if (integer) {
|
||||||
|
if (strictInteger && !Number.isInteger(value)) {
|
||||||
|
throw new ToolInputError(`${label} must be an integer`);
|
||||||
|
}
|
||||||
|
return Math.trunc(value);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readStringArrayParam(
|
export function readStringArrayParam(
|
||||||
|
|
|
||||||
|
|
@ -353,7 +353,7 @@ describe("message tool description", () => {
|
||||||
label: "Telegram",
|
label: "Telegram",
|
||||||
docsPath: "/channels/telegram",
|
docsPath: "/channels/telegram",
|
||||||
blurb: "Telegram test plugin.",
|
blurb: "Telegram test plugin.",
|
||||||
actions: ["send", "react", "delete", "edit", "topic-create"],
|
actions: ["send", "react", "delete", "edit", "topic-create", "topic-delete"],
|
||||||
});
|
});
|
||||||
|
|
||||||
setActivePluginRegistry(
|
setActivePluginRegistry(
|
||||||
|
|
@ -372,7 +372,9 @@ describe("message tool description", () => {
|
||||||
expect(tool.description).toContain("Current channel (signal) supports: react, send.");
|
expect(tool.description).toContain("Current channel (signal) supports: react, send.");
|
||||||
// Other configured channels are also listed
|
// Other configured channels are also listed
|
||||||
expect(tool.description).toContain("Other configured channels:");
|
expect(tool.description).toContain("Other configured channels:");
|
||||||
expect(tool.description).toContain("telegram (delete, edit, react, send, topic-create)");
|
expect(tool.description).toContain(
|
||||||
|
"telegram (delete, edit, react, send, topic-create, topic-delete)",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not include 'Other configured channels' when only one channel is configured", () => {
|
it("does not include 'Other configured channels' when only one channel is configured", () => {
|
||||||
|
|
|
||||||
|
|
@ -194,6 +194,12 @@ function buildSendSchema(options: {
|
||||||
filePath: Type.Optional(Type.String()),
|
filePath: Type.Optional(Type.String()),
|
||||||
replyTo: Type.Optional(Type.String()),
|
replyTo: Type.Optional(Type.String()),
|
||||||
threadId: Type.Optional(Type.String()),
|
threadId: Type.Optional(Type.String()),
|
||||||
|
topicId: Type.Optional(
|
||||||
|
Type.String({
|
||||||
|
description:
|
||||||
|
"Topic/thread id alias for forum-style channels (e.g., Telegram). Supported for topic deletion paths.",
|
||||||
|
}),
|
||||||
|
),
|
||||||
asVoice: Type.Optional(Type.Boolean()),
|
asVoice: Type.Optional(Type.Boolean()),
|
||||||
silent: Type.Optional(Type.Boolean()),
|
silent: Type.Optional(Type.Boolean()),
|
||||||
quoteText: Type.Optional(
|
quoteText: Type.Optional(
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ const createForumTopicTelegram = vi.fn(async () => ({
|
||||||
name: "Topic",
|
name: "Topic",
|
||||||
chatId: "123",
|
chatId: "123",
|
||||||
}));
|
}));
|
||||||
|
const deleteForumTopicTelegram = vi.fn(async () => ({ ok: true }));
|
||||||
let envSnapshot: ReturnType<typeof captureEnv>;
|
let envSnapshot: ReturnType<typeof captureEnv>;
|
||||||
|
|
||||||
vi.mock("../../../extensions/telegram/src/send.js", () => ({
|
vi.mock("../../../extensions/telegram/src/send.js", () => ({
|
||||||
|
|
@ -44,6 +45,8 @@ vi.mock("../../../extensions/telegram/src/send.js", () => ({
|
||||||
editMessageTelegram(...args),
|
editMessageTelegram(...args),
|
||||||
createForumTopicTelegram: (...args: Parameters<typeof createForumTopicTelegram>) =>
|
createForumTopicTelegram: (...args: Parameters<typeof createForumTopicTelegram>) =>
|
||||||
createForumTopicTelegram(...args),
|
createForumTopicTelegram(...args),
|
||||||
|
deleteForumTopicTelegram: (...args: Parameters<typeof deleteForumTopicTelegram>) =>
|
||||||
|
deleteForumTopicTelegram(...args),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe("handleTelegramAction", () => {
|
describe("handleTelegramAction", () => {
|
||||||
|
|
@ -106,6 +109,7 @@ describe("handleTelegramAction", () => {
|
||||||
deleteMessageTelegram.mockClear();
|
deleteMessageTelegram.mockClear();
|
||||||
editMessageTelegram.mockClear();
|
editMessageTelegram.mockClear();
|
||||||
createForumTopicTelegram.mockClear();
|
createForumTopicTelegram.mockClear();
|
||||||
|
deleteForumTopicTelegram.mockClear();
|
||||||
process.env.TELEGRAM_BOT_TOKEN = "tok";
|
process.env.TELEGRAM_BOT_TOKEN = "tok";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -594,6 +598,75 @@ describe("handleTelegramAction", () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("rejects malformed message ids for deleteMessage", async () => {
|
||||||
|
const cfg = {
|
||||||
|
channels: { telegram: { botToken: "tok" } },
|
||||||
|
} as OpenClawConfig;
|
||||||
|
await expect(
|
||||||
|
handleTelegramAction(
|
||||||
|
{
|
||||||
|
action: "deleteMessage",
|
||||||
|
chatId: "123",
|
||||||
|
messageId: "456oops",
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
),
|
||||||
|
).rejects.toThrow(/messageId required/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("deletes a forum topic", async () => {
|
||||||
|
const cfg = {
|
||||||
|
channels: { telegram: { botToken: "tok" } },
|
||||||
|
} as OpenClawConfig;
|
||||||
|
await handleTelegramAction(
|
||||||
|
{
|
||||||
|
action: "deleteForumTopic",
|
||||||
|
chatId: "-100123",
|
||||||
|
topicId: 271,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
expect(deleteForumTopicTelegram).toHaveBeenCalledWith(
|
||||||
|
"-100123",
|
||||||
|
271,
|
||||||
|
expect.objectContaining({ cfg, token: "tok" }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects malformed topic ids for deleteForumTopic", async () => {
|
||||||
|
const cfg = {
|
||||||
|
channels: { telegram: { botToken: "tok" } },
|
||||||
|
} as OpenClawConfig;
|
||||||
|
await expect(
|
||||||
|
handleTelegramAction(
|
||||||
|
{
|
||||||
|
action: "deleteForumTopic",
|
||||||
|
chatId: "-100123",
|
||||||
|
topicId: "271abc",
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
),
|
||||||
|
).rejects.toThrow(/topicId required/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("respects deleteMessage gating for deleteForumTopic", async () => {
|
||||||
|
const cfg = {
|
||||||
|
channels: {
|
||||||
|
telegram: { botToken: "tok", actions: { deleteMessage: false } },
|
||||||
|
},
|
||||||
|
} as OpenClawConfig;
|
||||||
|
await expect(
|
||||||
|
handleTelegramAction(
|
||||||
|
{
|
||||||
|
action: "deleteForumTopic",
|
||||||
|
chatId: "-100123",
|
||||||
|
topicId: 271,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
),
|
||||||
|
).rejects.toThrow(/Telegram forum topic deletion is disabled/);
|
||||||
|
});
|
||||||
|
|
||||||
it("respects deleteMessage gating", async () => {
|
it("respects deleteMessage gating", async () => {
|
||||||
const cfg = {
|
const cfg = {
|
||||||
channels: {
|
channels: {
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import {
|
||||||
import { resolveTelegramReactionLevel } from "../../../extensions/telegram/src/reaction-level.js";
|
import { resolveTelegramReactionLevel } from "../../../extensions/telegram/src/reaction-level.js";
|
||||||
import {
|
import {
|
||||||
createForumTopicTelegram,
|
createForumTopicTelegram,
|
||||||
|
deleteForumTopicTelegram,
|
||||||
deleteMessageTelegram,
|
deleteMessageTelegram,
|
||||||
editMessageTelegram,
|
editMessageTelegram,
|
||||||
reactMessageTelegram,
|
reactMessageTelegram,
|
||||||
|
|
@ -135,6 +136,8 @@ export async function handleTelegramAction(
|
||||||
});
|
});
|
||||||
const messageId = readNumberParam(params, "messageId", {
|
const messageId = readNumberParam(params, "messageId", {
|
||||||
integer: true,
|
integer: true,
|
||||||
|
strict: true,
|
||||||
|
strictInteger: true,
|
||||||
});
|
});
|
||||||
if (typeof messageId !== "number" || !Number.isFinite(messageId) || messageId <= 0) {
|
if (typeof messageId !== "number" || !Number.isFinite(messageId) || messageId <= 0) {
|
||||||
return jsonResult({
|
return jsonResult({
|
||||||
|
|
@ -326,6 +329,8 @@ export async function handleTelegramAction(
|
||||||
const messageId = readNumberParam(params, "messageId", {
|
const messageId = readNumberParam(params, "messageId", {
|
||||||
required: true,
|
required: true,
|
||||||
integer: true,
|
integer: true,
|
||||||
|
strict: true,
|
||||||
|
strictInteger: true,
|
||||||
});
|
});
|
||||||
const token = resolveTelegramToken(cfg, { accountId }).token;
|
const token = resolveTelegramToken(cfg, { accountId }).token;
|
||||||
if (!token) {
|
if (!token) {
|
||||||
|
|
@ -341,6 +346,35 @@ export async function handleTelegramAction(
|
||||||
return jsonResult({ ok: true, deleted: true });
|
return jsonResult({ ok: true, deleted: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (action === "deleteForumTopic") {
|
||||||
|
if (!isActionEnabled("deleteMessage")) {
|
||||||
|
throw new Error(
|
||||||
|
"Telegram forum topic deletion is disabled. Set channels.telegram.actions.deleteMessage to true.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const chatId = readStringOrNumberParam(params, "chatId", {
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
const topicId = readNumberParam(params, "topicId", {
|
||||||
|
required: true,
|
||||||
|
integer: true,
|
||||||
|
strict: true,
|
||||||
|
strictInteger: 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 ?? "", topicId ?? 0, {
|
||||||
|
cfg,
|
||||||
|
token,
|
||||||
|
accountId: accountId ?? undefined,
|
||||||
|
});
|
||||||
|
return jsonResult({ ok: true, deleted: true, topicId });
|
||||||
|
}
|
||||||
|
|
||||||
if (action === "editMessage") {
|
if (action === "editMessage") {
|
||||||
if (!isActionEnabled("editMessage")) {
|
if (!isActionEnabled("editMessage")) {
|
||||||
throw new Error("Telegram editMessage is disabled.");
|
throw new Error("Telegram editMessage is disabled.");
|
||||||
|
|
@ -351,6 +385,8 @@ export async function handleTelegramAction(
|
||||||
const messageId = readNumberParam(params, "messageId", {
|
const messageId = readNumberParam(params, "messageId", {
|
||||||
required: true,
|
required: true,
|
||||||
integer: true,
|
integer: true,
|
||||||
|
strict: true,
|
||||||
|
strictInteger: true,
|
||||||
});
|
});
|
||||||
const content = readStringParam(params, "content", {
|
const content = readStringParam(params, "content", {
|
||||||
required: true,
|
required: true,
|
||||||
|
|
|
||||||
|
|
@ -777,6 +777,48 @@ describe("telegramMessageActions", () => {
|
||||||
accountId: undefined,
|
accountId: undefined,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "delete maps to deleteMessage when messageId is provided",
|
||||||
|
action: "delete" as const,
|
||||||
|
params: {
|
||||||
|
to: "-1001234567890",
|
||||||
|
messageId: 42,
|
||||||
|
},
|
||||||
|
expectedPayload: {
|
||||||
|
action: "deleteMessage",
|
||||||
|
chatId: "-1001234567890",
|
||||||
|
messageId: 42,
|
||||||
|
accountId: undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "topic-delete maps to deleteForumTopic when threadId is provided",
|
||||||
|
action: "topic-delete" as const,
|
||||||
|
params: {
|
||||||
|
to: "-1001234567890",
|
||||||
|
threadId: 271,
|
||||||
|
},
|
||||||
|
expectedPayload: {
|
||||||
|
action: "deleteForumTopic",
|
||||||
|
chatId: "-1001234567890",
|
||||||
|
topicId: 271,
|
||||||
|
accountId: undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "delete keeps backward compatibility for explicit topic ids",
|
||||||
|
action: "delete" as const,
|
||||||
|
params: {
|
||||||
|
to: "-1001234567890",
|
||||||
|
topicId: 271,
|
||||||
|
},
|
||||||
|
expectedPayload: {
|
||||||
|
action: "deleteForumTopic",
|
||||||
|
chatId: "-1001234567890",
|
||||||
|
topicId: 271,
|
||||||
|
accountId: undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "topic-create maps to createForumTopic",
|
name: "topic-create maps to createForumTopic",
|
||||||
action: "topic-create" as const,
|
action: "topic-create" as const,
|
||||||
|
|
@ -806,6 +848,162 @@ describe("telegramMessageActions", () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("rejects delete when messageId is not provided", async () => {
|
||||||
|
const cfg = telegramCfg();
|
||||||
|
const handleAction = telegramMessageActions.handleAction;
|
||||||
|
if (!handleAction) {
|
||||||
|
throw new Error("telegram handleAction unavailable");
|
||||||
|
}
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
handleAction({
|
||||||
|
channel: "telegram",
|
||||||
|
action: "delete",
|
||||||
|
params: {
|
||||||
|
to: "-1001234567890",
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(/messageId is required for action=delete/i);
|
||||||
|
|
||||||
|
expect(handleTelegramAction).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects invalid delete messageId instead of falling back to topic deletion", async () => {
|
||||||
|
const cfg = telegramCfg();
|
||||||
|
const handleAction = telegramMessageActions.handleAction;
|
||||||
|
if (!handleAction) {
|
||||||
|
throw new Error("telegram handleAction unavailable");
|
||||||
|
}
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
handleAction({
|
||||||
|
channel: "telegram",
|
||||||
|
action: "delete",
|
||||||
|
params: {
|
||||||
|
to: "-1001234567890",
|
||||||
|
messageId: "oops",
|
||||||
|
topicId: 271,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(/messageId must be a valid number for action=delete/i);
|
||||||
|
|
||||||
|
expect(handleTelegramAction).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects invalid snake_case delete message_id instead of falling back to topic deletion", async () => {
|
||||||
|
const cfg = telegramCfg();
|
||||||
|
const handleAction = telegramMessageActions.handleAction;
|
||||||
|
if (!handleAction) {
|
||||||
|
throw new Error("telegram handleAction unavailable");
|
||||||
|
}
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
handleAction({
|
||||||
|
channel: "telegram",
|
||||||
|
action: "delete",
|
||||||
|
params: {
|
||||||
|
to: "-1001234567890",
|
||||||
|
message_id: "oops",
|
||||||
|
topicId: 271,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(/messageId must be a valid number for action=delete/i);
|
||||||
|
|
||||||
|
expect(handleTelegramAction).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects topic-delete when threadId/topicId is missing", async () => {
|
||||||
|
const cfg = telegramCfg();
|
||||||
|
const handleAction = telegramMessageActions.handleAction;
|
||||||
|
if (!handleAction) {
|
||||||
|
throw new Error("telegram handleAction unavailable");
|
||||||
|
}
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
handleAction({
|
||||||
|
channel: "telegram",
|
||||||
|
action: "topic-delete",
|
||||||
|
params: {
|
||||||
|
to: "-1001234567890",
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(/threadId\/topicId is required for action=topic-delete/i);
|
||||||
|
|
||||||
|
expect(handleTelegramAction).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects non-integer topic-delete ids before telegram-actions", async () => {
|
||||||
|
const cfg = telegramCfg();
|
||||||
|
const handleAction = telegramMessageActions.handleAction;
|
||||||
|
if (!handleAction) {
|
||||||
|
throw new Error("telegram handleAction unavailable");
|
||||||
|
}
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
handleAction({
|
||||||
|
channel: "telegram",
|
||||||
|
action: "topic-delete",
|
||||||
|
params: {
|
||||||
|
to: "-1001234567890",
|
||||||
|
topicId: "271abc",
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(/topicId must be a valid integer when provided/i);
|
||||||
|
|
||||||
|
expect(handleTelegramAction).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects malformed topicId even when threadId alias is valid", async () => {
|
||||||
|
const cfg = telegramCfg();
|
||||||
|
const handleAction = telegramMessageActions.handleAction;
|
||||||
|
if (!handleAction) {
|
||||||
|
throw new Error("telegram handleAction unavailable");
|
||||||
|
}
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
handleAction({
|
||||||
|
channel: "telegram",
|
||||||
|
action: "topic-delete",
|
||||||
|
params: {
|
||||||
|
to: "-1001234567890",
|
||||||
|
topicId: "oops",
|
||||||
|
threadId: 271,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(/topicId must be a valid integer when provided/i);
|
||||||
|
|
||||||
|
expect(handleTelegramAction).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects malformed delete topicId even when threadId alias is valid", async () => {
|
||||||
|
const cfg = telegramCfg();
|
||||||
|
const handleAction = telegramMessageActions.handleAction;
|
||||||
|
if (!handleAction) {
|
||||||
|
throw new Error("telegram handleAction unavailable");
|
||||||
|
}
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
handleAction({
|
||||||
|
channel: "telegram",
|
||||||
|
action: "delete",
|
||||||
|
params: {
|
||||||
|
to: "-1001234567890",
|
||||||
|
topicId: "oops",
|
||||||
|
threadId: 271,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(/topicId must be a valid integer when provided/i);
|
||||||
|
|
||||||
|
expect(handleTelegramAction).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("forwards trusted mediaLocalRoots for send", async () => {
|
it("forwards trusted mediaLocalRoots for send", async () => {
|
||||||
const cfg = telegramCfg();
|
const cfg = telegramCfg();
|
||||||
await telegramMessageActions.handleAction?.({
|
await telegramMessageActions.handleAction?.({
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ export const CHANNEL_MESSAGE_ACTION_NAMES = [
|
||||||
"category-edit",
|
"category-edit",
|
||||||
"category-delete",
|
"category-delete",
|
||||||
"topic-create",
|
"topic-create",
|
||||||
|
"topic-delete",
|
||||||
"voice-status",
|
"voice-status",
|
||||||
"event-list",
|
"event-list",
|
||||||
"event-create",
|
"event-create",
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ export const MESSAGE_ACTION_TARGET_MODE: Record<ChannelMessageActionName, Messag
|
||||||
"category-edit": "none",
|
"category-edit": "none",
|
||||||
"category-delete": "none",
|
"category-delete": "none",
|
||||||
"topic-create": "to",
|
"topic-create": "to",
|
||||||
|
"topic-delete": "to",
|
||||||
"voice-status": "none",
|
"voice-status": "none",
|
||||||
"event-list": "none",
|
"event-list": "none",
|
||||||
"event-create": "none",
|
"event-create": "none",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue