refactor(telegram): unify action normalization
This commit is contained in:
parent
4c9028439c
commit
a89cb3e10e
@ -236,6 +236,31 @@ describe("handleTelegramAction", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts shared sticker action aliases", async () => {
|
||||
const cfg = {
|
||||
channels: { telegram: { botToken: "tok", actions: { sticker: true } } },
|
||||
} as OpenClawConfig;
|
||||
await handleTelegramAction(
|
||||
{
|
||||
action: "sticker",
|
||||
target: "123",
|
||||
stickerId: ["sticker"],
|
||||
replyTo: 9,
|
||||
threadId: 11,
|
||||
},
|
||||
cfg,
|
||||
);
|
||||
expect(sendStickerTelegram).toHaveBeenCalledWith(
|
||||
"123",
|
||||
"sticker",
|
||||
expect.objectContaining({
|
||||
token: "tok",
|
||||
replyToMessageId: 9,
|
||||
messageThreadId: 11,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("removes reactions when remove flag set", async () => {
|
||||
const cfg = reactionConfig("extensive");
|
||||
await handleTelegramAction(
|
||||
@ -320,6 +345,26 @@ describe("handleTelegramAction", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts shared send action aliases", async () => {
|
||||
await handleTelegramAction(
|
||||
{
|
||||
action: "send",
|
||||
to: "@testchannel",
|
||||
message: "Hello from alias",
|
||||
media: "https://example.com/image.jpg",
|
||||
},
|
||||
telegramConfig(),
|
||||
);
|
||||
expect(sendMessageTelegram).toHaveBeenCalledWith(
|
||||
"@testchannel",
|
||||
"Hello from alias",
|
||||
expect.objectContaining({
|
||||
token: "tok",
|
||||
mediaUrl: "https://example.com/image.jpg",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("sends a poll", async () => {
|
||||
const result = await handleTelegramAction(
|
||||
{
|
||||
@ -357,6 +402,41 @@ describe("handleTelegramAction", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts shared poll action aliases", async () => {
|
||||
await handleTelegramAction(
|
||||
{
|
||||
action: "poll",
|
||||
to: "@testchannel",
|
||||
pollQuestion: "Ready?",
|
||||
pollOption: ["Yes", "No"],
|
||||
pollMulti: "true",
|
||||
pollPublic: "true",
|
||||
pollDurationSeconds: 60,
|
||||
replyTo: 55,
|
||||
threadId: 77,
|
||||
silent: "true",
|
||||
},
|
||||
telegramConfig(),
|
||||
);
|
||||
expect(sendPollTelegram).toHaveBeenCalledWith(
|
||||
"@testchannel",
|
||||
{
|
||||
question: "Ready?",
|
||||
options: ["Yes", "No"],
|
||||
maxSelections: 2,
|
||||
durationSeconds: 60,
|
||||
durationHours: undefined,
|
||||
},
|
||||
expect.objectContaining({
|
||||
token: "tok",
|
||||
isAnonymous: false,
|
||||
replyToMessageId: 55,
|
||||
messageThreadId: 77,
|
||||
silent: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("parses string booleans for poll flags", async () => {
|
||||
await handleTelegramAction(
|
||||
{
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||
import { readBooleanParam } from "openclaw/plugin-sdk/boolean-param";
|
||||
import { resolveReactionMessageId } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import { resolveTelegramPollVisibility } from "openclaw/plugin-sdk/telegram";
|
||||
import {
|
||||
jsonResult,
|
||||
readNumberParam,
|
||||
@ -13,6 +15,7 @@ import {
|
||||
} from "openclaw/plugin-sdk/telegram-core";
|
||||
import { createTelegramActionGate, resolveTelegramPollActionGateState } from "./accounts.js";
|
||||
import type { TelegramButtonStyle, TelegramInlineButtons } from "./button-types.js";
|
||||
import { resolveTelegramInlineButtons } from "./button-types.js";
|
||||
import {
|
||||
resolveTelegramInlineButtonsScope,
|
||||
resolveTelegramTargetChatType,
|
||||
@ -45,6 +48,27 @@ export const telegramActionRuntime = {
|
||||
};
|
||||
|
||||
const TELEGRAM_BUTTON_STYLES: readonly TelegramButtonStyle[] = ["danger", "success", "primary"];
|
||||
const TELEGRAM_ACTION_ALIASES = {
|
||||
createForumTopic: "createForumTopic",
|
||||
delete: "deleteMessage",
|
||||
deleteMessage: "deleteMessage",
|
||||
edit: "editMessage",
|
||||
editForumTopic: "editForumTopic",
|
||||
editMessage: "editMessage",
|
||||
poll: "poll",
|
||||
react: "react",
|
||||
searchSticker: "searchSticker",
|
||||
send: "sendMessage",
|
||||
sendMessage: "sendMessage",
|
||||
sendSticker: "sendSticker",
|
||||
sticker: "sendSticker",
|
||||
stickerCacheStats: "stickerCacheStats",
|
||||
"sticker-search": "searchSticker",
|
||||
"topic-create": "createForumTopic",
|
||||
"topic-edit": "editForumTopic",
|
||||
} as const;
|
||||
|
||||
type TelegramActionName = (typeof TELEGRAM_ACTION_ALIASES)[keyof typeof TELEGRAM_ACTION_ALIASES];
|
||||
|
||||
export function readTelegramButtons(
|
||||
params: Record<string, unknown>,
|
||||
@ -101,6 +125,58 @@ export function readTelegramButtons(
|
||||
return filtered.length > 0 ? filtered : undefined;
|
||||
}
|
||||
|
||||
function normalizeTelegramActionName(action: string): TelegramActionName {
|
||||
const normalized = TELEGRAM_ACTION_ALIASES[action as keyof typeof TELEGRAM_ACTION_ALIASES];
|
||||
if (!normalized) {
|
||||
throw new Error(`Unsupported Telegram action: ${action}`);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function readTelegramChatId(params: Record<string, unknown>) {
|
||||
return (
|
||||
readStringOrNumberParam(params, "chatId") ??
|
||||
readStringOrNumberParam(params, "channelId") ??
|
||||
readStringOrNumberParam(params, "to", { required: true })
|
||||
);
|
||||
}
|
||||
|
||||
function readTelegramThreadId(params: Record<string, unknown>) {
|
||||
return (
|
||||
readNumberParam(params, "messageThreadId", { integer: true }) ??
|
||||
readNumberParam(params, "threadId", { integer: true })
|
||||
);
|
||||
}
|
||||
|
||||
function readTelegramReplyToMessageId(params: Record<string, unknown>) {
|
||||
return (
|
||||
readNumberParam(params, "replyToMessageId", { integer: true }) ??
|
||||
readNumberParam(params, "replyTo", { integer: true })
|
||||
);
|
||||
}
|
||||
|
||||
function resolveTelegramButtonsFromParams(params: Record<string, unknown>) {
|
||||
return resolveTelegramInlineButtons({
|
||||
buttons: readTelegramButtons(params),
|
||||
interactive: params.interactive,
|
||||
});
|
||||
}
|
||||
|
||||
function readTelegramSendContent(params: {
|
||||
args: Record<string, unknown>;
|
||||
mediaUrl?: string;
|
||||
hasButtons: boolean;
|
||||
}) {
|
||||
const content =
|
||||
readStringParam(params.args, "content", { allowEmpty: true }) ??
|
||||
readStringParam(params.args, "message", { allowEmpty: true }) ??
|
||||
readStringParam(params.args, "caption", { allowEmpty: true });
|
||||
if (content == null && !params.mediaUrl && !params.hasButtons) {
|
||||
throw new Error("content required.");
|
||||
}
|
||||
return content ?? "";
|
||||
}
|
||||
|
||||
export async function handleTelegramAction(
|
||||
params: Record<string, unknown>,
|
||||
cfg: OpenClawConfig,
|
||||
@ -109,7 +185,7 @@ export async function handleTelegramAction(
|
||||
},
|
||||
): Promise<AgentToolResult<unknown>> {
|
||||
const { action, accountId } = {
|
||||
action: readStringParam(params, "action", { required: true }),
|
||||
action: normalizeTelegramActionName(readStringParam(params, "action", { required: true })),
|
||||
accountId: readStringParam(params, "accountId"),
|
||||
};
|
||||
const isActionEnabled = createTelegramActionGate({
|
||||
@ -139,12 +215,10 @@ export async function handleTelegramAction(
|
||||
hint: "Telegram reactions are disabled via actions.reactions. Do not retry.",
|
||||
});
|
||||
}
|
||||
const chatId = readStringOrNumberParam(params, "chatId", {
|
||||
required: true,
|
||||
});
|
||||
const messageId = readNumberParam(params, "messageId", {
|
||||
integer: true,
|
||||
});
|
||||
const chatId = readTelegramChatId(params);
|
||||
const messageId =
|
||||
readNumberParam(params, "messageId", { integer: true }) ??
|
||||
resolveReactionMessageId({ args: params });
|
||||
if (typeof messageId !== "number" || !Number.isFinite(messageId) || messageId <= 0) {
|
||||
return jsonResult({
|
||||
ok: false,
|
||||
@ -205,14 +279,17 @@ export async function handleTelegramAction(
|
||||
throw new Error("Telegram sendMessage is disabled.");
|
||||
}
|
||||
const to = readStringParam(params, "to", { required: true });
|
||||
const mediaUrl = readStringParam(params, "mediaUrl");
|
||||
// Allow content to be omitted when sending media-only (e.g., voice notes)
|
||||
const content =
|
||||
readStringParam(params, "content", {
|
||||
required: !mediaUrl,
|
||||
allowEmpty: true,
|
||||
}) ?? "";
|
||||
const buttons = readTelegramButtons(params);
|
||||
const mediaUrl =
|
||||
readStringParam(params, "mediaUrl") ??
|
||||
readStringParam(params, "media", {
|
||||
trim: false,
|
||||
});
|
||||
const buttons = resolveTelegramButtonsFromParams(params);
|
||||
const content = readTelegramSendContent({
|
||||
args: params,
|
||||
mediaUrl: mediaUrl ?? undefined,
|
||||
hasButtons: Array.isArray(buttons) && buttons.length > 0,
|
||||
});
|
||||
if (buttons) {
|
||||
const inlineButtonsScope = resolveTelegramInlineButtonsScope({
|
||||
cfg,
|
||||
@ -241,12 +318,8 @@ export async function handleTelegramAction(
|
||||
}
|
||||
}
|
||||
// Optional threading parameters for forum topics and reply chains
|
||||
const replyToMessageId = readNumberParam(params, "replyToMessageId", {
|
||||
integer: true,
|
||||
});
|
||||
const messageThreadId = readNumberParam(params, "messageThreadId", {
|
||||
integer: true,
|
||||
});
|
||||
const replyToMessageId = readTelegramReplyToMessageId(params);
|
||||
const messageThreadId = readTelegramThreadId(params);
|
||||
const quoteText = readStringParam(params, "quoteText");
|
||||
const token = resolveTelegramToken(cfg, { accountId }).token;
|
||||
if (!token) {
|
||||
@ -284,18 +357,34 @@ export async function handleTelegramAction(
|
||||
throw new Error("Telegram polls are disabled.");
|
||||
}
|
||||
const to = readStringParam(params, "to", { required: true });
|
||||
const question = readStringParam(params, "question", { required: true });
|
||||
const answers = readStringArrayParam(params, "answers", { required: true });
|
||||
const allowMultiselect = readBooleanParam(params, "allowMultiselect") ?? false;
|
||||
const durationSeconds = readNumberParam(params, "durationSeconds", { integer: true });
|
||||
const durationHours = readNumberParam(params, "durationHours", { integer: true });
|
||||
const replyToMessageId = readNumberParam(params, "replyToMessageId", {
|
||||
integer: true,
|
||||
});
|
||||
const messageThreadId = readNumberParam(params, "messageThreadId", {
|
||||
integer: true,
|
||||
});
|
||||
const isAnonymous = readBooleanParam(params, "isAnonymous");
|
||||
const question =
|
||||
readStringParam(params, "question") ??
|
||||
readStringParam(params, "pollQuestion", { required: true });
|
||||
const answers =
|
||||
readStringArrayParam(params, "answers") ??
|
||||
readStringArrayParam(params, "pollOption", { required: true });
|
||||
const allowMultiselect =
|
||||
readBooleanParam(params, "allowMultiselect") ?? readBooleanParam(params, "pollMulti");
|
||||
const durationSeconds =
|
||||
readNumberParam(params, "durationSeconds", { integer: true }) ??
|
||||
readNumberParam(params, "pollDurationSeconds", {
|
||||
integer: true,
|
||||
strict: true,
|
||||
});
|
||||
const durationHours =
|
||||
readNumberParam(params, "durationHours", { integer: true }) ??
|
||||
readNumberParam(params, "pollDurationHours", {
|
||||
integer: true,
|
||||
strict: true,
|
||||
});
|
||||
const replyToMessageId = readTelegramReplyToMessageId(params);
|
||||
const messageThreadId = readTelegramThreadId(params);
|
||||
const isAnonymous =
|
||||
readBooleanParam(params, "isAnonymous") ??
|
||||
resolveTelegramPollVisibility({
|
||||
pollAnonymous: readBooleanParam(params, "pollAnonymous"),
|
||||
pollPublic: readBooleanParam(params, "pollPublic"),
|
||||
});
|
||||
const silent = readBooleanParam(params, "silent");
|
||||
const token = resolveTelegramToken(cfg, { accountId }).token;
|
||||
if (!token) {
|
||||
@ -308,7 +397,7 @@ export async function handleTelegramAction(
|
||||
{
|
||||
question,
|
||||
options: answers,
|
||||
maxSelections: resolvePollMaxSelections(answers.length, allowMultiselect),
|
||||
maxSelections: resolvePollMaxSelections(answers.length, allowMultiselect ?? false),
|
||||
durationSeconds: durationSeconds ?? undefined,
|
||||
durationHours: durationHours ?? undefined,
|
||||
},
|
||||
@ -334,9 +423,7 @@ export async function handleTelegramAction(
|
||||
if (!isActionEnabled("deleteMessage")) {
|
||||
throw new Error("Telegram deleteMessage is disabled.");
|
||||
}
|
||||
const chatId = readStringOrNumberParam(params, "chatId", {
|
||||
required: true,
|
||||
});
|
||||
const chatId = readTelegramChatId(params);
|
||||
const messageId = readNumberParam(params, "messageId", {
|
||||
required: true,
|
||||
integer: true,
|
||||
@ -359,18 +446,15 @@ export async function handleTelegramAction(
|
||||
if (!isActionEnabled("editMessage")) {
|
||||
throw new Error("Telegram editMessage is disabled.");
|
||||
}
|
||||
const chatId = readStringOrNumberParam(params, "chatId", {
|
||||
required: true,
|
||||
});
|
||||
const chatId = readTelegramChatId(params);
|
||||
const messageId = readNumberParam(params, "messageId", {
|
||||
required: true,
|
||||
integer: true,
|
||||
});
|
||||
const content = readStringParam(params, "content", {
|
||||
required: true,
|
||||
allowEmpty: false,
|
||||
});
|
||||
const buttons = readTelegramButtons(params);
|
||||
const content =
|
||||
readStringParam(params, "content", { allowEmpty: false }) ??
|
||||
readStringParam(params, "message", { required: true, allowEmpty: false });
|
||||
const buttons = resolveTelegramButtonsFromParams(params);
|
||||
if (buttons) {
|
||||
const inlineButtonsScope = resolveTelegramInlineButtonsScope({
|
||||
cfg,
|
||||
@ -412,14 +496,15 @@ export async function handleTelegramAction(
|
||||
"Telegram sticker actions are disabled. Set channels.telegram.actions.sticker to true.",
|
||||
);
|
||||
}
|
||||
const to = readStringParam(params, "to", { required: true });
|
||||
const fileId = readStringParam(params, "fileId", { required: true });
|
||||
const replyToMessageId = readNumberParam(params, "replyToMessageId", {
|
||||
integer: true,
|
||||
});
|
||||
const messageThreadId = readNumberParam(params, "messageThreadId", {
|
||||
integer: true,
|
||||
});
|
||||
const to =
|
||||
readStringParam(params, "to") ?? readStringParam(params, "target", { required: true });
|
||||
const fileId =
|
||||
readStringParam(params, "fileId") ?? readStringArrayParam(params, "stickerId")?.[0];
|
||||
if (!fileId) {
|
||||
throw new Error("fileId is required.");
|
||||
}
|
||||
const replyToMessageId = readTelegramReplyToMessageId(params);
|
||||
const messageThreadId = readTelegramThreadId(params);
|
||||
const token = resolveTelegramToken(cfg, { accountId }).token;
|
||||
if (!token) {
|
||||
throw new Error(
|
||||
@ -470,9 +555,7 @@ export async function handleTelegramAction(
|
||||
if (!isActionEnabled("createForumTopic")) {
|
||||
throw new Error("Telegram createForumTopic is disabled.");
|
||||
}
|
||||
const chatId = readStringOrNumberParam(params, "chatId", {
|
||||
required: true,
|
||||
});
|
||||
const chatId = readTelegramChatId(params);
|
||||
const name = readStringParam(params, "name", { required: true });
|
||||
const iconColor = readNumberParam(params, "iconColor", { integer: true });
|
||||
const iconCustomEmojiId = readStringParam(params, "iconCustomEmojiId");
|
||||
@ -501,12 +584,8 @@ export async function handleTelegramAction(
|
||||
if (!isActionEnabled("editForumTopic")) {
|
||||
throw new Error("Telegram editForumTopic is disabled.");
|
||||
}
|
||||
const chatId = readStringOrNumberParam(params, "chatId", {
|
||||
required: true,
|
||||
});
|
||||
const messageThreadId =
|
||||
readNumberParam(params, "messageThreadId", { integer: true }) ??
|
||||
readNumberParam(params, "threadId", { integer: true });
|
||||
const chatId = readTelegramChatId(params);
|
||||
const messageThreadId = readTelegramThreadId(params);
|
||||
if (typeof messageThreadId !== "number") {
|
||||
throw new Error("messageThreadId or threadId is required.");
|
||||
}
|
||||
|
||||
@ -42,8 +42,14 @@ describe("telegramMessageActions", () => {
|
||||
expect.objectContaining({
|
||||
action: "sendMessage",
|
||||
to: "123456",
|
||||
content: "",
|
||||
buttons: [[{ text: "Approve", callback_data: "approve", style: "success" }]],
|
||||
interactive: {
|
||||
blocks: [
|
||||
{
|
||||
type: "buttons",
|
||||
buttons: [{ label: "Approve", value: "approve", style: "success" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
accountId: "default",
|
||||
}),
|
||||
expect.anything(),
|
||||
|
||||
@ -1,25 +1,15 @@
|
||||
import {
|
||||
readNumberParam,
|
||||
readStringArrayParam,
|
||||
readStringOrNumberParam,
|
||||
readStringParam,
|
||||
} from "openclaw/plugin-sdk/agent-runtime";
|
||||
import { readBooleanParam } from "openclaw/plugin-sdk/boolean-param";
|
||||
import { resolveReactionMessageId } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import {
|
||||
createMessageToolButtonsSchema,
|
||||
createTelegramPollExtraToolSchemas,
|
||||
createUnionActionGate,
|
||||
listTokenSourcedAccounts,
|
||||
} from "openclaw/plugin-sdk/channel-runtime";
|
||||
import type {
|
||||
ChannelMessageActionAdapter,
|
||||
ChannelMessageActionName,
|
||||
ChannelMessageToolDiscovery,
|
||||
ChannelMessageToolSchemaContribution,
|
||||
resolveReactionMessageId,
|
||||
type ChannelMessageActionAdapter,
|
||||
type ChannelMessageActionName,
|
||||
type ChannelMessageToolDiscovery,
|
||||
type ChannelMessageToolSchemaContribution,
|
||||
} from "openclaw/plugin-sdk/channel-runtime";
|
||||
import type { TelegramActionConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { resolveTelegramPollVisibility } from "openclaw/plugin-sdk/telegram";
|
||||
import { extractToolSend } from "openclaw/plugin-sdk/tool-send";
|
||||
import {
|
||||
createTelegramActionGate,
|
||||
@ -27,15 +17,28 @@ import {
|
||||
resolveTelegramPollActionGateState,
|
||||
} from "./accounts.js";
|
||||
import { handleTelegramAction } from "./action-runtime.js";
|
||||
import { resolveTelegramInlineButtons } from "./button-types.js";
|
||||
import { isTelegramInlineButtonsEnabled } from "./inline-buttons.js";
|
||||
|
||||
const providerId = "telegram";
|
||||
|
||||
export const telegramMessageActionRuntime = {
|
||||
handleTelegramAction,
|
||||
};
|
||||
|
||||
const TELEGRAM_MESSAGE_ACTION_MAP = {
|
||||
delete: "deleteMessage",
|
||||
edit: "editMessage",
|
||||
poll: "poll",
|
||||
react: "react",
|
||||
send: "sendMessage",
|
||||
sticker: "sendSticker",
|
||||
"sticker-search": "searchSticker",
|
||||
"topic-create": "createForumTopic",
|
||||
"topic-edit": "editForumTopic",
|
||||
} as const satisfies Partial<Record<ChannelMessageActionName, string>>;
|
||||
|
||||
function resolveTelegramMessageActionName(action: ChannelMessageActionName) {
|
||||
return TELEGRAM_MESSAGE_ACTION_MAP[action as keyof typeof TELEGRAM_MESSAGE_ACTION_MAP];
|
||||
}
|
||||
|
||||
function resolveTelegramActionDiscovery(cfg: Parameters<typeof listEnabledTelegramAccounts>[0]) {
|
||||
const accounts = listTokenSourcedAccounts(listEnabledTelegramAccounts(cfg));
|
||||
if (accounts.length === 0) {
|
||||
@ -122,249 +125,29 @@ function describeTelegramMessageTool({
|
||||
};
|
||||
}
|
||||
|
||||
function readTelegramSendParams(params: Record<string, unknown>) {
|
||||
const to = readStringParam(params, "to", { required: true });
|
||||
const mediaUrl = readStringParam(params, "media", { trim: false });
|
||||
const buttons = resolveTelegramInlineButtons({
|
||||
buttons: params.buttons as ReturnType<typeof resolveTelegramInlineButtons>,
|
||||
interactive: params.interactive,
|
||||
});
|
||||
const hasButtons = Array.isArray(buttons) && buttons.length > 0;
|
||||
const message = readStringParam(params, "message", {
|
||||
required: !mediaUrl && !hasButtons,
|
||||
allowEmpty: true,
|
||||
});
|
||||
const caption = readStringParam(params, "caption", { allowEmpty: true });
|
||||
const content = message || caption || "";
|
||||
const replyTo = readStringParam(params, "replyTo");
|
||||
const threadId = readStringParam(params, "threadId");
|
||||
const asVoice = readBooleanParam(params, "asVoice");
|
||||
const silent = readBooleanParam(params, "silent");
|
||||
const forceDocument = readBooleanParam(params, "forceDocument");
|
||||
const quoteText = readStringParam(params, "quoteText");
|
||||
return {
|
||||
to,
|
||||
content,
|
||||
mediaUrl: mediaUrl ?? undefined,
|
||||
replyToMessageId: replyTo ?? undefined,
|
||||
messageThreadId: threadId ?? undefined,
|
||||
buttons,
|
||||
asVoice,
|
||||
silent,
|
||||
forceDocument,
|
||||
quoteText: quoteText ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function readTelegramChatIdParam(params: Record<string, unknown>): string | number {
|
||||
return (
|
||||
readStringOrNumberParam(params, "chatId") ??
|
||||
readStringOrNumberParam(params, "channelId") ??
|
||||
readStringParam(params, "to", { required: true })
|
||||
);
|
||||
}
|
||||
|
||||
function readTelegramMessageIdParam(params: Record<string, unknown>): number {
|
||||
const messageId = readNumberParam(params, "messageId", {
|
||||
required: true,
|
||||
integer: true,
|
||||
});
|
||||
if (typeof messageId !== "number") {
|
||||
throw new Error("messageId is required.");
|
||||
}
|
||||
return messageId;
|
||||
}
|
||||
|
||||
export const telegramMessageActions: ChannelMessageActionAdapter = {
|
||||
describeMessageTool: describeTelegramMessageTool,
|
||||
extractToolSend: ({ args }) => {
|
||||
return extractToolSend(args, "sendMessage");
|
||||
},
|
||||
handleAction: async ({ action, params, cfg, accountId, mediaLocalRoots, toolContext }) => {
|
||||
if (action === "send") {
|
||||
const sendParams = readTelegramSendParams(params);
|
||||
return await telegramMessageActionRuntime.handleTelegramAction(
|
||||
{
|
||||
action: "sendMessage",
|
||||
...sendParams,
|
||||
accountId: accountId ?? undefined,
|
||||
},
|
||||
cfg,
|
||||
{ mediaLocalRoots },
|
||||
);
|
||||
const telegramAction = resolveTelegramMessageActionName(action);
|
||||
if (!telegramAction) {
|
||||
throw new Error(`Unsupported Telegram action: ${action}`);
|
||||
}
|
||||
|
||||
if (action === "react") {
|
||||
const messageId = resolveReactionMessageId({ args: params, toolContext });
|
||||
const emoji = readStringParam(params, "emoji", { allowEmpty: true });
|
||||
const remove = readBooleanParam(params, "remove");
|
||||
return await telegramMessageActionRuntime.handleTelegramAction(
|
||||
{
|
||||
action: "react",
|
||||
chatId: readTelegramChatIdParam(params),
|
||||
messageId,
|
||||
emoji,
|
||||
remove,
|
||||
accountId: accountId ?? undefined,
|
||||
},
|
||||
cfg,
|
||||
{ mediaLocalRoots },
|
||||
);
|
||||
}
|
||||
|
||||
if (action === "poll") {
|
||||
const to = readStringParam(params, "to", { required: true });
|
||||
const question = readStringParam(params, "pollQuestion", { required: true });
|
||||
const answers = readStringArrayParam(params, "pollOption", { required: true });
|
||||
const durationHours = readNumberParam(params, "pollDurationHours", {
|
||||
integer: true,
|
||||
strict: true,
|
||||
});
|
||||
const durationSeconds = readNumberParam(params, "pollDurationSeconds", {
|
||||
integer: true,
|
||||
strict: true,
|
||||
});
|
||||
const replyToMessageId = readNumberParam(params, "replyTo", { integer: true });
|
||||
const messageThreadId = readNumberParam(params, "threadId", { integer: true });
|
||||
const allowMultiselect = readBooleanParam(params, "pollMulti");
|
||||
const pollAnonymous = readBooleanParam(params, "pollAnonymous");
|
||||
const pollPublic = readBooleanParam(params, "pollPublic");
|
||||
const isAnonymous = resolveTelegramPollVisibility({ pollAnonymous, pollPublic });
|
||||
const silent = readBooleanParam(params, "silent");
|
||||
return await telegramMessageActionRuntime.handleTelegramAction(
|
||||
{
|
||||
action: "poll",
|
||||
to,
|
||||
question,
|
||||
answers,
|
||||
allowMultiselect,
|
||||
durationHours: durationHours ?? undefined,
|
||||
durationSeconds: durationSeconds ?? undefined,
|
||||
replyToMessageId: replyToMessageId ?? undefined,
|
||||
messageThreadId: messageThreadId ?? undefined,
|
||||
isAnonymous,
|
||||
silent,
|
||||
accountId: accountId ?? undefined,
|
||||
},
|
||||
cfg,
|
||||
{ mediaLocalRoots },
|
||||
);
|
||||
}
|
||||
|
||||
if (action === "delete") {
|
||||
const chatId = readTelegramChatIdParam(params);
|
||||
const messageId = readTelegramMessageIdParam(params);
|
||||
return await telegramMessageActionRuntime.handleTelegramAction(
|
||||
{
|
||||
action: "deleteMessage",
|
||||
chatId,
|
||||
messageId,
|
||||
accountId: accountId ?? undefined,
|
||||
},
|
||||
cfg,
|
||||
{ mediaLocalRoots },
|
||||
);
|
||||
}
|
||||
|
||||
if (action === "edit") {
|
||||
const chatId = readTelegramChatIdParam(params);
|
||||
const messageId = readTelegramMessageIdParam(params);
|
||||
const message = readStringParam(params, "message", { required: true, allowEmpty: false });
|
||||
const buttons = params.buttons;
|
||||
return await telegramMessageActionRuntime.handleTelegramAction(
|
||||
{
|
||||
action: "editMessage",
|
||||
chatId,
|
||||
messageId,
|
||||
content: message,
|
||||
buttons,
|
||||
accountId: accountId ?? undefined,
|
||||
},
|
||||
cfg,
|
||||
{ mediaLocalRoots },
|
||||
);
|
||||
}
|
||||
|
||||
if (action === "sticker") {
|
||||
const to =
|
||||
readStringParam(params, "to") ?? readStringParam(params, "target", { required: true });
|
||||
// Accept stickerId (array from shared schema) and use first element as fileId
|
||||
const stickerIds = readStringArrayParam(params, "stickerId");
|
||||
const fileId = stickerIds?.[0] ?? readStringParam(params, "fileId", { required: true });
|
||||
const replyToMessageId = readNumberParam(params, "replyTo", { integer: true });
|
||||
const messageThreadId = readNumberParam(params, "threadId", { integer: true });
|
||||
return await telegramMessageActionRuntime.handleTelegramAction(
|
||||
{
|
||||
action: "sendSticker",
|
||||
to,
|
||||
fileId,
|
||||
replyToMessageId: replyToMessageId ?? undefined,
|
||||
messageThreadId: messageThreadId ?? undefined,
|
||||
accountId: accountId ?? undefined,
|
||||
},
|
||||
cfg,
|
||||
{ mediaLocalRoots },
|
||||
);
|
||||
}
|
||||
|
||||
if (action === "sticker-search") {
|
||||
const query = readStringParam(params, "query", { required: true });
|
||||
const limit = readNumberParam(params, "limit", { integer: true });
|
||||
return await telegramMessageActionRuntime.handleTelegramAction(
|
||||
{
|
||||
action: "searchSticker",
|
||||
query,
|
||||
limit: limit ?? undefined,
|
||||
accountId: accountId ?? undefined,
|
||||
},
|
||||
cfg,
|
||||
{ mediaLocalRoots },
|
||||
);
|
||||
}
|
||||
|
||||
if (action === "topic-create") {
|
||||
const chatId = readTelegramChatIdParam(params);
|
||||
const name = readStringParam(params, "name", { required: true });
|
||||
const iconColor = readNumberParam(params, "iconColor", { integer: true });
|
||||
const iconCustomEmojiId = readStringParam(params, "iconCustomEmojiId");
|
||||
return await telegramMessageActionRuntime.handleTelegramAction(
|
||||
{
|
||||
action: "createForumTopic",
|
||||
chatId,
|
||||
name,
|
||||
iconColor: iconColor ?? undefined,
|
||||
iconCustomEmojiId: iconCustomEmojiId ?? undefined,
|
||||
accountId: accountId ?? undefined,
|
||||
},
|
||||
cfg,
|
||||
{ mediaLocalRoots },
|
||||
);
|
||||
}
|
||||
|
||||
if (action === "topic-edit") {
|
||||
const chatId = readTelegramChatIdParam(params);
|
||||
const messageThreadId =
|
||||
readNumberParam(params, "messageThreadId", { integer: true }) ??
|
||||
readNumberParam(params, "threadId", { integer: true });
|
||||
if (typeof messageThreadId !== "number") {
|
||||
throw new Error("messageThreadId or threadId is required.");
|
||||
}
|
||||
const name = readStringParam(params, "name");
|
||||
const iconCustomEmojiId = readStringParam(params, "iconCustomEmojiId");
|
||||
return await telegramMessageActionRuntime.handleTelegramAction(
|
||||
{
|
||||
action: "editForumTopic",
|
||||
chatId,
|
||||
messageThreadId,
|
||||
name: name ?? undefined,
|
||||
iconCustomEmojiId: iconCustomEmojiId ?? undefined,
|
||||
accountId: accountId ?? undefined,
|
||||
},
|
||||
cfg,
|
||||
{ mediaLocalRoots },
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(`Action ${action} is not supported for provider ${providerId}.`);
|
||||
return await telegramMessageActionRuntime.handleTelegramAction(
|
||||
{
|
||||
...params,
|
||||
action: telegramAction,
|
||||
accountId: accountId ?? undefined,
|
||||
...(action === "react"
|
||||
? {
|
||||
messageId: resolveReactionMessageId({ args: params, toolContext }),
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
cfg,
|
||||
{ mediaLocalRoots },
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@ -25,9 +25,9 @@ vi.mock("../../../../extensions/slack/src/action-runtime.js", () => ({
|
||||
handleSlackAction,
|
||||
}));
|
||||
|
||||
let discordMessageActions: typeof import("./discord.js").discordMessageActions;
|
||||
let handleDiscordMessageAction: typeof import("./discord/handle-action.js").handleDiscordMessageAction;
|
||||
let telegramMessageActions: typeof import("./telegram.js").telegramMessageActions;
|
||||
let discordMessageActions: typeof import("../../../../extensions/discord/src/channel-actions.js").discordMessageActions;
|
||||
let handleDiscordMessageAction: typeof import("../../../../extensions/discord/src/actions/handle-action.js").handleDiscordMessageAction;
|
||||
let telegramMessageActions: typeof import("../../../../extensions/telegram/src/channel-actions.js").telegramMessageActions;
|
||||
let signalMessageActions: typeof import("../../../../extensions/signal/src/message-actions.js").signalMessageActions;
|
||||
let createSlackActions: typeof import("../../../../extensions/slack/src/channel-actions.js").createSlackActions;
|
||||
|
||||
@ -201,9 +201,12 @@ async function expectSlackSendRejected(params: Record<string, unknown>, error: R
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
({ discordMessageActions } = await import("./discord.js"));
|
||||
({ handleDiscordMessageAction } = await import("./discord/handle-action.js"));
|
||||
({ telegramMessageActions } = await import("./telegram.js"));
|
||||
({ discordMessageActions } =
|
||||
await import("../../../../extensions/discord/src/channel-actions.js"));
|
||||
({ handleDiscordMessageAction } =
|
||||
await import("../../../../extensions/discord/src/actions/handle-action.js"));
|
||||
({ telegramMessageActions } =
|
||||
await import("../../../../extensions/telegram/src/channel-actions.js"));
|
||||
({ signalMessageActions } = await import("../../../../extensions/signal/src/message-actions.js"));
|
||||
({ createSlackActions } = await import("../../../../extensions/slack/src/channel-actions.js"));
|
||||
vi.clearAllMocks();
|
||||
@ -708,7 +711,7 @@ describe("telegramMessageActions", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("maps action params into telegram actions", async () => {
|
||||
it("forwards telegram action aliases into the runtime seam", async () => {
|
||||
const cases = [
|
||||
{
|
||||
name: "media-only send preserves asVoice",
|
||||
@ -721,8 +724,7 @@ describe("telegramMessageActions", () => {
|
||||
expectedPayload: expect.objectContaining({
|
||||
action: "sendMessage",
|
||||
to: "123",
|
||||
content: "",
|
||||
mediaUrl: "https://example.com/voice.ogg",
|
||||
media: "https://example.com/voice.ogg",
|
||||
asVoice: true,
|
||||
}),
|
||||
},
|
||||
@ -737,7 +739,7 @@ describe("telegramMessageActions", () => {
|
||||
expectedPayload: expect.objectContaining({
|
||||
action: "sendMessage",
|
||||
to: "456",
|
||||
content: "Silent notification test",
|
||||
message: "Silent notification test",
|
||||
silent: true,
|
||||
}),
|
||||
},
|
||||
@ -754,7 +756,7 @@ describe("telegramMessageActions", () => {
|
||||
action: "editMessage",
|
||||
chatId: "123",
|
||||
messageId: 42,
|
||||
content: "Updated",
|
||||
message: "Updated",
|
||||
buttons: [],
|
||||
accountId: undefined,
|
||||
},
|
||||
@ -776,20 +778,19 @@ describe("telegramMessageActions", () => {
|
||||
expectedPayload: {
|
||||
action: "poll",
|
||||
to: "123",
|
||||
question: "Ready?",
|
||||
answers: ["Yes", "No"],
|
||||
allowMultiselect: true,
|
||||
durationHours: undefined,
|
||||
durationSeconds: 60,
|
||||
replyToMessageId: 55,
|
||||
messageThreadId: 77,
|
||||
isAnonymous: false,
|
||||
pollQuestion: "Ready?",
|
||||
pollOption: ["Yes", "No"],
|
||||
pollMulti: true,
|
||||
pollDurationSeconds: 60,
|
||||
pollPublic: true,
|
||||
replyTo: 55,
|
||||
threadId: 77,
|
||||
silent: true,
|
||||
accountId: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "poll parses string booleans before telegram action handoff",
|
||||
name: "poll forwards raw alias flags to telegram runtime",
|
||||
action: "poll" as const,
|
||||
params: {
|
||||
to: "123",
|
||||
@ -802,20 +803,16 @@ describe("telegramMessageActions", () => {
|
||||
expectedPayload: {
|
||||
action: "poll",
|
||||
to: "123",
|
||||
question: "Ready?",
|
||||
answers: ["Yes", "No"],
|
||||
allowMultiselect: true,
|
||||
durationHours: undefined,
|
||||
durationSeconds: undefined,
|
||||
replyToMessageId: undefined,
|
||||
messageThreadId: undefined,
|
||||
isAnonymous: false,
|
||||
silent: true,
|
||||
pollQuestion: "Ready?",
|
||||
pollOption: ["Yes", "No"],
|
||||
pollMulti: "true",
|
||||
pollPublic: "true",
|
||||
silent: "true",
|
||||
accountId: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "poll rejects partially numeric duration strings before telegram action handoff",
|
||||
name: "poll forwards duration strings for runtime validation",
|
||||
action: "poll" as const,
|
||||
params: {
|
||||
to: "123",
|
||||
@ -826,15 +823,9 @@ describe("telegramMessageActions", () => {
|
||||
expectedPayload: {
|
||||
action: "poll",
|
||||
to: "123",
|
||||
question: "Ready?",
|
||||
answers: ["Yes", "No"],
|
||||
allowMultiselect: undefined,
|
||||
durationHours: undefined,
|
||||
durationSeconds: undefined,
|
||||
replyToMessageId: undefined,
|
||||
messageThreadId: undefined,
|
||||
isAnonymous: undefined,
|
||||
silent: undefined,
|
||||
pollQuestion: "Ready?",
|
||||
pollOption: ["Yes", "No"],
|
||||
pollDurationSeconds: "60s",
|
||||
accountId: undefined,
|
||||
},
|
||||
},
|
||||
@ -847,10 +838,8 @@ describe("telegramMessageActions", () => {
|
||||
},
|
||||
expectedPayload: {
|
||||
action: "createForumTopic",
|
||||
chatId: "telegram:group:-1001234567890:topic:271",
|
||||
to: "telegram:group:-1001234567890:topic:271",
|
||||
name: "Build Updates",
|
||||
iconColor: undefined,
|
||||
iconCustomEmojiId: undefined,
|
||||
accountId: undefined,
|
||||
},
|
||||
},
|
||||
@ -865,8 +854,8 @@ describe("telegramMessageActions", () => {
|
||||
},
|
||||
expectedPayload: {
|
||||
action: "editForumTopic",
|
||||
chatId: "telegram:group:-1001234567890:topic:271",
|
||||
messageThreadId: 271,
|
||||
to: "telegram:group:-1001234567890:topic:271",
|
||||
threadId: 271,
|
||||
name: "Build Updates",
|
||||
iconCustomEmojiId: "emoji-123",
|
||||
accountId: undefined,
|
||||
@ -885,30 +874,6 @@ describe("telegramMessageActions", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects non-integer messageId for edit before reaching telegram-actions", async () => {
|
||||
const cfg = telegramCfg();
|
||||
const handleAction = telegramMessageActions.handleAction;
|
||||
if (!handleAction) {
|
||||
throw new Error("telegram handleAction unavailable");
|
||||
}
|
||||
|
||||
await expect(
|
||||
handleAction({
|
||||
channel: "telegram",
|
||||
action: "edit",
|
||||
params: {
|
||||
chatId: "123",
|
||||
messageId: "nope",
|
||||
message: "Updated",
|
||||
},
|
||||
cfg,
|
||||
accountId: undefined,
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
|
||||
expect(handleTelegramAction).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("inherits top-level reaction gate when account overrides sticker only", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
@ -941,7 +906,8 @@ describe("telegramMessageActions", () => {
|
||||
emoji: "ok",
|
||||
},
|
||||
toolContext: undefined,
|
||||
expectedChatId: "123",
|
||||
expectedChannelField: "channelId",
|
||||
expectedChannelValue: "123",
|
||||
expectedMessageId: "456",
|
||||
},
|
||||
{
|
||||
@ -952,7 +918,8 @@ describe("telegramMessageActions", () => {
|
||||
emoji: "ok",
|
||||
},
|
||||
toolContext: undefined,
|
||||
expectedChatId: "123",
|
||||
expectedChannelField: "channelId",
|
||||
expectedChannelValue: "123",
|
||||
expectedMessageId: "456",
|
||||
},
|
||||
{
|
||||
@ -962,7 +929,8 @@ describe("telegramMessageActions", () => {
|
||||
emoji: "ok",
|
||||
},
|
||||
toolContext: { currentMessageId: "9001" },
|
||||
expectedChatId: "123",
|
||||
expectedChannelField: "chatId",
|
||||
expectedChannelValue: "123",
|
||||
expectedMessageId: "9001",
|
||||
},
|
||||
{
|
||||
@ -972,7 +940,8 @@ describe("telegramMessageActions", () => {
|
||||
emoji: "ok",
|
||||
},
|
||||
toolContext: undefined,
|
||||
expectedChatId: "123",
|
||||
expectedChannelField: "chatId",
|
||||
expectedChannelValue: "123",
|
||||
expectedMessageId: undefined,
|
||||
},
|
||||
] as const) {
|
||||
@ -995,7 +964,9 @@ describe("telegramMessageActions", () => {
|
||||
}
|
||||
const callPayload = call as Record<string, unknown>;
|
||||
expect(callPayload.action, testCase.name).toBe("react");
|
||||
expect(String(callPayload.chatId), testCase.name).toBe(testCase.expectedChatId);
|
||||
expect(String(callPayload[testCase.expectedChannelField]), testCase.name).toBe(
|
||||
testCase.expectedChannelValue,
|
||||
);
|
||||
if (testCase.expectedMessageId === undefined) {
|
||||
expect(callPayload.messageId, testCase.name).toBeUndefined();
|
||||
} else {
|
||||
@ -1048,7 +1019,7 @@ it("forwards trusted mediaLocalRoots for send actions", async () => {
|
||||
expect(handleTelegramAction).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
action: "sendMessage",
|
||||
mediaUrl: "/tmp/voice.ogg",
|
||||
media: "/tmp/voice.ogg",
|
||||
}),
|
||||
expect.any(Object),
|
||||
expect.objectContaining({ mediaLocalRoots: ["/tmp/agent-root"] }),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user