Fix Telegram poll action wiring

This commit is contained in:
Krish
2026-02-16 19:54:49 +05:30
committed by Peter Steinberger
parent afd354c482
commit 556b531a14
5 changed files with 168 additions and 0 deletions

View File

@@ -262,8 +262,11 @@ function buildPollSchema() {
return {
pollQuestion: Type.Optional(Type.String()),
pollOption: Type.Optional(Type.Array(Type.String())),
pollDurationSeconds: Type.Optional(Type.Number()),
pollDurationHours: Type.Optional(Type.Number()),
pollMulti: Type.Optional(Type.Boolean()),
pollAnonymous: Type.Optional(Type.Boolean()),
pollPublic: Type.Optional(Type.Boolean()),
};
}

View File

@@ -11,6 +11,11 @@ const sendStickerTelegram = vi.fn(async () => ({
messageId: "456",
chatId: "123",
}));
const sendPollTelegram = vi.fn(async () => ({
messageId: "999",
chatId: "123",
pollId: "poll-1",
}));
const deleteMessageTelegram = vi.fn(async () => ({ ok: true }));
const originalToken = process.env.TELEGRAM_BOT_TOKEN;
@@ -18,6 +23,7 @@ vi.mock("../../telegram/send.js", () => ({
reactMessageTelegram: (...args: unknown[]) => reactMessageTelegram(...args),
sendMessageTelegram: (...args: unknown[]) => sendMessageTelegram(...args),
sendStickerTelegram: (...args: unknown[]) => sendStickerTelegram(...args),
sendPollTelegram: (...args: unknown[]) => sendPollTelegram(...args),
deleteMessageTelegram: (...args: unknown[]) => deleteMessageTelegram(...args),
}));
@@ -49,6 +55,7 @@ describe("handleTelegramAction", () => {
reactMessageTelegram.mockClear();
sendMessageTelegram.mockClear();
sendStickerTelegram.mockClear();
sendPollTelegram.mockClear();
deleteMessageTelegram.mockClear();
process.env.TELEGRAM_BOT_TOKEN = "tok";
});
@@ -362,6 +369,30 @@ describe("handleTelegramAction", () => {
);
});
it("sends a poll", async () => {
const cfg = {
channels: { telegram: { botToken: "tok" } },
} as OpenClawConfig;
await handleTelegramAction(
{
action: "poll",
to: "123",
question: "Ready?",
options: ["Yes", "No"],
},
cfg,
);
expect(sendPollTelegram).toHaveBeenCalledWith(
"123",
expect.objectContaining({
question: "Ready?",
options: ["Yes", "No"],
maxSelections: 1,
}),
expect.objectContaining({ token: "tok" }),
);
});
it("respects deleteMessage gating", async () => {
const cfg = {
channels: {

View File

@@ -12,6 +12,7 @@ import {
editMessageTelegram,
reactMessageTelegram,
sendMessageTelegram,
sendPollTelegram,
sendStickerTelegram,
} from "../../telegram/send.js";
import { getCacheStats, searchStickers } from "../../telegram/sticker-cache.js";
@@ -213,6 +214,66 @@ export async function handleTelegramAction(
});
}
if (action === "poll") {
if (!isActionEnabled("polls")) {
throw new Error("Telegram polls are disabled.");
}
const to = readStringParam(params, "to", { required: true });
const question = readStringParam(params, "question", { required: true });
const options = (params.options ?? params.answers) as unknown;
if (!Array.isArray(options)) {
throw new Error("options must be an array of strings");
}
const pollOptions = options.filter((option): option is string => typeof option === "string");
if (pollOptions.length !== options.length) {
throw new Error("options must be an array of strings");
}
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 maxSelections =
typeof params.allowMultiselect === "boolean" && params.allowMultiselect === true ? 2 : 1;
const token = resolveTelegramToken(cfg, { accountId }).token;
if (!token) {
throw new Error(
"Telegram bot token missing. Set TELEGRAM_BOT_TOKEN or channels.telegram.botToken.",
);
}
const result = await sendPollTelegram(
to,
{
question,
options: pollOptions,
maxSelections,
durationSeconds: durationSeconds ?? undefined,
durationHours: durationHours ?? undefined,
},
{
token,
accountId: accountId ?? undefined,
replyToMessageId: replyToMessageId ?? undefined,
messageThreadId: messageThreadId ?? undefined,
silent: typeof params.silent === "boolean" ? params.silent : undefined,
isAnonymous: typeof params.isAnonymous === "boolean" ? params.isAnonymous : undefined,
},
);
return jsonResult({
ok: true,
messageId: result.messageId,
chatId: result.chatId,
pollId: result.pollId,
});
}
if (action === "deleteMessage") {
if (!isActionEnabled("deleteMessage")) {
throw new Error("Telegram deleteMessage is disabled.");

View File

@@ -128,6 +128,42 @@ describe("discord message actions", () => {
});
});
describe("telegram message actions", () => {
it("lists poll action when telegram is configured", () => {
const cfg = { channels: { telegram: { botToken: "t0" } } } as OpenClawConfig;
const actions = telegramMessageActions.listActions?.({ cfg }) ?? [];
expect(actions).toContain("poll");
});
it("routes poll with normalized params", async () => {
await telegramMessageActions.handleAction({
action: "poll",
params: {
to: "123",
pollQuestion: "Ready?",
pollOption: ["Yes", "No"],
pollMulti: true,
pollDurationSeconds: 60,
},
cfg: { channels: { telegram: { botToken: "tok" } } } as OpenClawConfig,
accountId: "ops",
});
expect(handleTelegramAction).toHaveBeenCalledWith(
expect.objectContaining({
action: "poll",
to: "123",
question: "Ready?",
options: ["Yes", "No"],
allowMultiselect: true,
durationSeconds: 60,
accountId: "ops",
}),
expect.any(Object),
);
});
});
describe("handleDiscordMessageAction", () => {
it("forwards context accountId for send", async () => {
await handleDiscordMessageAction({

View File

@@ -58,6 +58,9 @@ export const telegramMessageActions: ChannelMessageActionAdapter = {
if (gate("deleteMessage")) {
actions.add("delete");
}
if (gate("polls")) {
actions.add("poll");
}
if (gate("editMessage")) {
actions.add("edit");
}
@@ -116,6 +119,40 @@ export const telegramMessageActions: ChannelMessageActionAdapter = {
);
}
if (action === "poll") {
const to = readStringParam(params, "to", { required: true });
const question = readStringParam(params, "pollQuestion", { required: true });
const options = readStringArrayParam(params, "pollOption", { required: true }) ?? [];
const allowMultiselect = typeof params.pollMulti === "boolean" ? params.pollMulti : undefined;
const durationSeconds = readNumberParam(params, "pollDurationSeconds", {
integer: true,
});
const durationHours = readNumberParam(params, "pollDurationHours", {
integer: true,
});
const replyToMessageId = readNumberParam(params, "replyTo", { integer: true });
const messageThreadId = readNumberParam(params, "threadId", { integer: true });
const silent = typeof params.silent === "boolean" ? params.silent : undefined;
const isAnonymous = typeof params.isAnonymous === "boolean" ? params.isAnonymous : undefined;
return await handleTelegramAction(
{
action: "poll",
to,
question,
options,
allowMultiselect,
durationSeconds: durationSeconds ?? undefined,
durationHours: durationHours ?? undefined,
replyToMessageId: replyToMessageId ?? undefined,
messageThreadId: messageThreadId ?? undefined,
silent,
isAnonymous,
accountId: accountId ?? undefined,
},
cfg,
);
}
if (action === "delete") {
const chatId =
readStringOrNumberParam(params, "chatId") ??