This commit is contained in:
Alexander Bolshakov 2026-03-15 15:38:17 +01:00 committed by GitHub
commit f2739985f5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 520 additions and 14 deletions

View File

@ -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`

View File

@ -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,12 +237,53 @@ 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(
{
action: "deleteMessage",
chatId,
messageId,
accountId: accountId ?? undefined,
},
cfg,
{ mediaLocalRoots },
);
}
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( return await handleTelegramAction(
{ {
action: "deleteMessage", action: "deleteForumTopic",
chatId, chatId,
messageId, topicId,
accountId: accountId ?? undefined, accountId: accountId ?? undefined,
}, },
cfg, cfg,

View File

@ -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();

View File

@ -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(),

View File

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

View File

@ -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
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

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

View File

@ -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(

View File

@ -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", () => {

View File

@ -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(

View File

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

View File

@ -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,

View File

@ -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?.({

View File

@ -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",

View File

@ -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",