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`)
|
||||
- `react` (`chatId`, `messageId`, `emoji`)
|
||||
- `deleteMessage` (`chatId`, `messageId`)
|
||||
- `deleteForumTopic` (`chatId`, `topicId`)
|
||||
- `editMessage` (`chatId`, `messageId`, `content`)
|
||||
- `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:
|
||||
|
||||
|
|
@ -969,7 +973,7 @@ Telegram-specific high-signal fields:
|
|||
- formatting/delivery: `textChunkLimit`, `chunkMode`, `linkPreview`, `responsePrefix`
|
||||
- media/network: `mediaMaxMb`, `timeoutSeconds`, `retry`, `network.autoSelectFamily`, `proxy`
|
||||
- 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`
|
||||
- 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", {
|
||||
required: true,
|
||||
required,
|
||||
integer: true,
|
||||
strict: true,
|
||||
strictInteger: true,
|
||||
});
|
||||
if (typeof messageId !== "number") {
|
||||
if (required && typeof messageId !== "number") {
|
||||
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 = {
|
||||
|
|
@ -104,6 +138,7 @@ export const telegramMessageActions: ChannelMessageActionAdapter = {
|
|||
}
|
||||
if (isEnabled("deleteMessage")) {
|
||||
actions.add("delete");
|
||||
actions.add("topic-delete");
|
||||
}
|
||||
if (isEnabled("editMessage")) {
|
||||
actions.add("edit");
|
||||
|
|
@ -202,7 +237,13 @@ export const telegramMessageActions: ChannelMessageActionAdapter = {
|
|||
|
||||
if (action === "delete") {
|
||||
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(
|
||||
{
|
||||
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") {
|
||||
const chatId = readTelegramChatIdParam(params);
|
||||
const messageId = readTelegramMessageIdParam(params);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ const { botApi, botCtorSpy } = vi.hoisted(() => ({
|
|||
sendMessage: vi.fn(),
|
||||
setMessageReaction: vi.fn(),
|
||||
deleteMessage: vi.fn(),
|
||||
deleteForumTopic: vi.fn(),
|
||||
},
|
||||
botCtorSpy: vi.fn(),
|
||||
}));
|
||||
|
|
@ -52,6 +53,7 @@ vi.mock("grammy", () => ({
|
|||
}));
|
||||
|
||||
import {
|
||||
deleteForumTopicTelegram,
|
||||
deleteMessageTelegram,
|
||||
reactMessageTelegram,
|
||||
resetTelegramClientOptionsCacheForTests,
|
||||
|
|
@ -86,6 +88,7 @@ describe("telegram proxy client", () => {
|
|||
botApi.sendMessage.mockResolvedValue({ message_id: 1, chat: { id: "123" } });
|
||||
botApi.setMessageReaction.mockResolvedValue(undefined);
|
||||
botApi.deleteMessage.mockResolvedValue(true);
|
||||
botApi.deleteForumTopic.mockResolvedValue(true);
|
||||
botCtorSpy.mockClear();
|
||||
loadConfig.mockReturnValue({
|
||||
channels: { telegram: { accounts: { foo: { proxy: proxyUrl } } } },
|
||||
|
|
@ -134,6 +137,10 @@ describe("telegram proxy client", () => {
|
|||
name: "deleteMessage",
|
||||
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) => {
|
||||
const { fetchImpl } = prepareProxyFetch();
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import type { MockFn } from "../../../src/test-utils/vitest-mock-fn.js";
|
|||
const { botApi, botCtorSpy } = vi.hoisted(() => ({
|
||||
botApi: {
|
||||
deleteMessage: vi.fn(),
|
||||
deleteForumTopic: vi.fn(),
|
||||
editMessageText: vi.fn(),
|
||||
sendChatAction: vi.fn(),
|
||||
sendMessage: vi.fn(),
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ const { botApi, botCtorSpy, loadConfig, loadWebMedia, maybePersistResolvedTelegr
|
|||
const {
|
||||
buildInlineKeyboard,
|
||||
createForumTopicTelegram,
|
||||
deleteForumTopicTelegram,
|
||||
editMessageTelegram,
|
||||
reactMessageTelegram,
|
||||
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", () => {
|
||||
const cases = [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1438,6 +1438,49 @@ export async function sendPollTelegram(
|
|||
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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -63,6 +63,13 @@ describe("readNumberParam", () => {
|
|||
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", () => {
|
||||
const params = { message_id: "42" };
|
||||
expect(readNumberParam(params, "messageId")).toBe(42);
|
||||
|
|
|
|||
|
|
@ -116,9 +116,21 @@ export function readStringOrNumberParam(
|
|||
export function readNumberParam(
|
||||
params: Record<string, unknown>,
|
||||
key: string,
|
||||
options: { required?: boolean; label?: string; integer?: boolean; strict?: boolean } = {},
|
||||
options: {
|
||||
required?: boolean;
|
||||
label?: string;
|
||||
integer?: boolean;
|
||||
strict?: boolean;
|
||||
strictInteger?: boolean;
|
||||
} = {},
|
||||
): 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);
|
||||
let value: number | undefined;
|
||||
if (typeof raw === "number" && Number.isFinite(raw)) {
|
||||
|
|
@ -138,7 +150,13 @@ export function readNumberParam(
|
|||
}
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -353,7 +353,7 @@ describe("message tool description", () => {
|
|||
label: "Telegram",
|
||||
docsPath: "/channels/telegram",
|
||||
blurb: "Telegram test plugin.",
|
||||
actions: ["send", "react", "delete", "edit", "topic-create"],
|
||||
actions: ["send", "react", "delete", "edit", "topic-create", "topic-delete"],
|
||||
});
|
||||
|
||||
setActivePluginRegistry(
|
||||
|
|
@ -372,7 +372,9 @@ describe("message tool description", () => {
|
|||
expect(tool.description).toContain("Current channel (signal) supports: react, send.");
|
||||
// Other configured channels are also listed
|
||||
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", () => {
|
||||
|
|
|
|||
|
|
@ -194,6 +194,12 @@ function buildSendSchema(options: {
|
|||
filePath: Type.Optional(Type.String()),
|
||||
replyTo: 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()),
|
||||
silent: Type.Optional(Type.Boolean()),
|
||||
quoteText: Type.Optional(
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ const createForumTopicTelegram = vi.fn(async () => ({
|
|||
name: "Topic",
|
||||
chatId: "123",
|
||||
}));
|
||||
const deleteForumTopicTelegram = vi.fn(async () => ({ ok: true }));
|
||||
let envSnapshot: ReturnType<typeof captureEnv>;
|
||||
|
||||
vi.mock("../../../extensions/telegram/src/send.js", () => ({
|
||||
|
|
@ -44,6 +45,8 @@ vi.mock("../../../extensions/telegram/src/send.js", () => ({
|
|||
editMessageTelegram(...args),
|
||||
createForumTopicTelegram: (...args: Parameters<typeof createForumTopicTelegram>) =>
|
||||
createForumTopicTelegram(...args),
|
||||
deleteForumTopicTelegram: (...args: Parameters<typeof deleteForumTopicTelegram>) =>
|
||||
deleteForumTopicTelegram(...args),
|
||||
}));
|
||||
|
||||
describe("handleTelegramAction", () => {
|
||||
|
|
@ -106,6 +109,7 @@ describe("handleTelegramAction", () => {
|
|||
deleteMessageTelegram.mockClear();
|
||||
editMessageTelegram.mockClear();
|
||||
createForumTopicTelegram.mockClear();
|
||||
deleteForumTopicTelegram.mockClear();
|
||||
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 () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
import { resolveTelegramReactionLevel } from "../../../extensions/telegram/src/reaction-level.js";
|
||||
import {
|
||||
createForumTopicTelegram,
|
||||
deleteForumTopicTelegram,
|
||||
deleteMessageTelegram,
|
||||
editMessageTelegram,
|
||||
reactMessageTelegram,
|
||||
|
|
@ -135,6 +136,8 @@ export async function handleTelegramAction(
|
|||
});
|
||||
const messageId = readNumberParam(params, "messageId", {
|
||||
integer: true,
|
||||
strict: true,
|
||||
strictInteger: true,
|
||||
});
|
||||
if (typeof messageId !== "number" || !Number.isFinite(messageId) || messageId <= 0) {
|
||||
return jsonResult({
|
||||
|
|
@ -326,6 +329,8 @@ export async function handleTelegramAction(
|
|||
const messageId = readNumberParam(params, "messageId", {
|
||||
required: true,
|
||||
integer: true,
|
||||
strict: true,
|
||||
strictInteger: true,
|
||||
});
|
||||
const token = resolveTelegramToken(cfg, { accountId }).token;
|
||||
if (!token) {
|
||||
|
|
@ -341,6 +346,35 @@ export async function handleTelegramAction(
|
|||
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 (!isActionEnabled("editMessage")) {
|
||||
throw new Error("Telegram editMessage is disabled.");
|
||||
|
|
@ -351,6 +385,8 @@ export async function handleTelegramAction(
|
|||
const messageId = readNumberParam(params, "messageId", {
|
||||
required: true,
|
||||
integer: true,
|
||||
strict: true,
|
||||
strictInteger: true,
|
||||
});
|
||||
const content = readStringParam(params, "content", {
|
||||
required: true,
|
||||
|
|
|
|||
|
|
@ -777,6 +777,48 @@ describe("telegramMessageActions", () => {
|
|||
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",
|
||||
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 () => {
|
||||
const cfg = telegramCfg();
|
||||
await telegramMessageActions.handleAction?.({
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ export const CHANNEL_MESSAGE_ACTION_NAMES = [
|
|||
"category-edit",
|
||||
"category-delete",
|
||||
"topic-create",
|
||||
"topic-delete",
|
||||
"voice-status",
|
||||
"event-list",
|
||||
"event-create",
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ export const MESSAGE_ACTION_TARGET_MODE: Record<ChannelMessageActionName, Messag
|
|||
"category-edit": "none",
|
||||
"category-delete": "none",
|
||||
"topic-create": "to",
|
||||
"topic-delete": "to",
|
||||
"voice-status": "none",
|
||||
"event-list": "none",
|
||||
"event-create": "none",
|
||||
|
|
|
|||
Loading…
Reference in New Issue