diff --git a/extensions/telegram/src/action-runtime.test.ts b/extensions/telegram/src/action-runtime.test.ts index ad59933415f..ddcf7c2854a 100644 --- a/extensions/telegram/src/action-runtime.test.ts +++ b/extensions/telegram/src/action-runtime.test.ts @@ -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( { diff --git a/extensions/telegram/src/action-runtime.ts b/extensions/telegram/src/action-runtime.ts index f4083481026..6f823d99ae7 100644 --- a/extensions/telegram/src/action-runtime.ts +++ b/extensions/telegram/src/action-runtime.ts @@ -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, @@ -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) { + return ( + readStringOrNumberParam(params, "chatId") ?? + readStringOrNumberParam(params, "channelId") ?? + readStringOrNumberParam(params, "to", { required: true }) + ); +} + +function readTelegramThreadId(params: Record) { + return ( + readNumberParam(params, "messageThreadId", { integer: true }) ?? + readNumberParam(params, "threadId", { integer: true }) + ); +} + +function readTelegramReplyToMessageId(params: Record) { + return ( + readNumberParam(params, "replyToMessageId", { integer: true }) ?? + readNumberParam(params, "replyTo", { integer: true }) + ); +} + +function resolveTelegramButtonsFromParams(params: Record) { + return resolveTelegramInlineButtons({ + buttons: readTelegramButtons(params), + interactive: params.interactive, + }); +} + +function readTelegramSendContent(params: { + args: Record; + 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, cfg: OpenClawConfig, @@ -109,7 +185,7 @@ export async function handleTelegramAction( }, ): Promise> { 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."); } diff --git a/extensions/telegram/src/channel-actions.test.ts b/extensions/telegram/src/channel-actions.test.ts index 0addd92af78..a75a0f0c71f 100644 --- a/extensions/telegram/src/channel-actions.test.ts +++ b/extensions/telegram/src/channel-actions.test.ts @@ -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(), diff --git a/extensions/telegram/src/channel-actions.ts b/extensions/telegram/src/channel-actions.ts index 56d27817921..867a0951a42 100644 --- a/extensions/telegram/src/channel-actions.ts +++ b/extensions/telegram/src/channel-actions.ts @@ -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>; + +function resolveTelegramMessageActionName(action: ChannelMessageActionName) { + return TELEGRAM_MESSAGE_ACTION_MAP[action as keyof typeof TELEGRAM_MESSAGE_ACTION_MAP]; +} + function resolveTelegramActionDiscovery(cfg: Parameters[0]) { const accounts = listTokenSourcedAccounts(listEnabledTelegramAccounts(cfg)); if (accounts.length === 0) { @@ -122,249 +125,29 @@ function describeTelegramMessageTool({ }; } -function readTelegramSendParams(params: Record) { - const to = readStringParam(params, "to", { required: true }); - const mediaUrl = readStringParam(params, "media", { trim: false }); - const buttons = resolveTelegramInlineButtons({ - buttons: params.buttons as ReturnType, - 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 | number { - return ( - readStringOrNumberParam(params, "chatId") ?? - readStringOrNumberParam(params, "channelId") ?? - readStringParam(params, "to", { required: true }) - ); -} - -function readTelegramMessageIdParam(params: Record): 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 }, + ); }, }; diff --git a/src/channels/plugins/actions/actions.test.ts b/src/channels/plugins/actions/actions.test.ts index f740a6da4ea..f9c8025d3f4 100644 --- a/src/channels/plugins/actions/actions.test.ts +++ b/src/channels/plugins/actions/actions.test.ts @@ -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, 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; 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"] }),