mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 09:07:39 +00:00
Harden Telegram poll gating and schema consistency (#36547)
Merged via squash.
Prepared head SHA: f77824419e
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
committed by
GitHub
parent
f771ba8de9
commit
6dfd39c32f
@@ -329,6 +329,44 @@ describe("handleDiscordMessageAction", () => {
|
||||
answers: ["Yes", "No"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "parses string booleans for discord poll adapter params",
|
||||
input: {
|
||||
action: "poll" as const,
|
||||
params: {
|
||||
to: "channel:123",
|
||||
pollQuestion: "Ready?",
|
||||
pollOption: ["Yes", "No"],
|
||||
pollMulti: "true",
|
||||
},
|
||||
},
|
||||
expected: {
|
||||
action: "poll",
|
||||
to: "channel:123",
|
||||
question: "Ready?",
|
||||
answers: ["Yes", "No"],
|
||||
allowMultiselect: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "rejects partially numeric poll duration for discord poll adapter params",
|
||||
input: {
|
||||
action: "poll" as const,
|
||||
params: {
|
||||
to: "channel:123",
|
||||
pollQuestion: "Ready?",
|
||||
pollOption: ["Yes", "No"],
|
||||
pollDurationHours: "24h",
|
||||
},
|
||||
},
|
||||
expected: {
|
||||
action: "poll",
|
||||
to: "channel:123",
|
||||
question: "Ready?",
|
||||
answers: ["Yes", "No"],
|
||||
durationHours: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "forwards accountId for thread replies",
|
||||
input: {
|
||||
@@ -496,6 +534,71 @@ describe("handleDiscordMessageAction", () => {
|
||||
});
|
||||
|
||||
describe("telegramMessageActions", () => {
|
||||
it("lists poll when telegram is configured", () => {
|
||||
const actions = telegramMessageActions.listActions?.({ cfg: telegramCfg() }) ?? [];
|
||||
|
||||
expect(actions).toContain("poll");
|
||||
});
|
||||
|
||||
it("omits poll when sendMessage is disabled", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
telegram: {
|
||||
botToken: "tok",
|
||||
actions: { sendMessage: false },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const actions = telegramMessageActions.listActions?.({ cfg }) ?? [];
|
||||
|
||||
expect(actions).not.toContain("poll");
|
||||
});
|
||||
|
||||
it("omits poll when poll actions are disabled", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
telegram: {
|
||||
botToken: "tok",
|
||||
actions: { poll: false },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const actions = telegramMessageActions.listActions?.({ cfg }) ?? [];
|
||||
|
||||
expect(actions).not.toContain("poll");
|
||||
});
|
||||
|
||||
it("omits poll when sendMessage and poll are split across accounts", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
telegram: {
|
||||
accounts: {
|
||||
senderOnly: {
|
||||
botToken: "tok-send",
|
||||
actions: {
|
||||
sendMessage: true,
|
||||
poll: false,
|
||||
},
|
||||
},
|
||||
pollOnly: {
|
||||
botToken: "tok-poll",
|
||||
actions: {
|
||||
sendMessage: false,
|
||||
poll: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const actions = telegramMessageActions.listActions?.({ cfg }) ?? [];
|
||||
|
||||
expect(actions).not.toContain("poll");
|
||||
});
|
||||
|
||||
it("lists sticker actions only when enabled by config", () => {
|
||||
const cases = [
|
||||
{
|
||||
@@ -595,6 +698,85 @@ describe("telegramMessageActions", () => {
|
||||
accountId: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "poll maps to telegram poll action",
|
||||
action: "poll" as const,
|
||||
params: {
|
||||
to: "123",
|
||||
pollQuestion: "Ready?",
|
||||
pollOption: ["Yes", "No"],
|
||||
pollMulti: true,
|
||||
pollDurationSeconds: 60,
|
||||
pollPublic: true,
|
||||
replyTo: 55,
|
||||
threadId: 77,
|
||||
silent: true,
|
||||
},
|
||||
expectedPayload: {
|
||||
action: "poll",
|
||||
to: "123",
|
||||
question: "Ready?",
|
||||
answers: ["Yes", "No"],
|
||||
allowMultiselect: true,
|
||||
durationHours: undefined,
|
||||
durationSeconds: 60,
|
||||
replyToMessageId: 55,
|
||||
messageThreadId: 77,
|
||||
isAnonymous: false,
|
||||
silent: true,
|
||||
accountId: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "poll parses string booleans before telegram action handoff",
|
||||
action: "poll" as const,
|
||||
params: {
|
||||
to: "123",
|
||||
pollQuestion: "Ready?",
|
||||
pollOption: ["Yes", "No"],
|
||||
pollMulti: "true",
|
||||
pollPublic: "true",
|
||||
silent: "true",
|
||||
},
|
||||
expectedPayload: {
|
||||
action: "poll",
|
||||
to: "123",
|
||||
question: "Ready?",
|
||||
answers: ["Yes", "No"],
|
||||
allowMultiselect: true,
|
||||
durationHours: undefined,
|
||||
durationSeconds: undefined,
|
||||
replyToMessageId: undefined,
|
||||
messageThreadId: undefined,
|
||||
isAnonymous: false,
|
||||
silent: true,
|
||||
accountId: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "poll rejects partially numeric duration strings before telegram action handoff",
|
||||
action: "poll" as const,
|
||||
params: {
|
||||
to: "123",
|
||||
pollQuestion: "Ready?",
|
||||
pollOption: ["Yes", "No"],
|
||||
pollDurationSeconds: "60s",
|
||||
},
|
||||
expectedPayload: {
|
||||
action: "poll",
|
||||
to: "123",
|
||||
question: "Ready?",
|
||||
answers: ["Yes", "No"],
|
||||
allowMultiselect: undefined,
|
||||
durationHours: undefined,
|
||||
durationSeconds: undefined,
|
||||
replyToMessageId: undefined,
|
||||
messageThreadId: undefined,
|
||||
isAnonymous: undefined,
|
||||
silent: undefined,
|
||||
accountId: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "topic-create maps to createForumTopic",
|
||||
action: "topic-create" as const,
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
import { readDiscordParentIdParam } from "../../../../agents/tools/discord-actions-shared.js";
|
||||
import { handleDiscordAction } from "../../../../agents/tools/discord-actions.js";
|
||||
import { resolveDiscordChannelId } from "../../../../discord/targets.js";
|
||||
import { readBooleanParam } from "../../../../plugin-sdk/boolean-param.js";
|
||||
import type { ChannelMessageActionContext } from "../../types.js";
|
||||
import { resolveReactionMessageId } from "../reaction-message-id.js";
|
||||
import { tryHandleDiscordMessageActionGuildAdmin } from "./handle-action.guild-admin.js";
|
||||
@@ -38,7 +39,7 @@ export async function handleDiscordMessageAction(
|
||||
|
||||
if (action === "send") {
|
||||
const to = readStringParam(params, "to", { required: true });
|
||||
const asVoice = params.asVoice === true;
|
||||
const asVoice = readBooleanParam(params, "asVoice") === true;
|
||||
const rawComponents = params.components;
|
||||
const hasComponents =
|
||||
Boolean(rawComponents) &&
|
||||
@@ -57,7 +58,7 @@ export async function handleDiscordMessageAction(
|
||||
const replyTo = readStringParam(params, "replyTo");
|
||||
const rawEmbeds = params.embeds;
|
||||
const embeds = Array.isArray(rawEmbeds) ? rawEmbeds : undefined;
|
||||
const silent = params.silent === true;
|
||||
const silent = readBooleanParam(params, "silent") === true;
|
||||
const sessionKey = readStringParam(params, "__sessionKey");
|
||||
const agentId = readStringParam(params, "__agentId");
|
||||
return await handleDiscordAction(
|
||||
@@ -86,10 +87,11 @@ export async function handleDiscordMessageAction(
|
||||
const question = readStringParam(params, "pollQuestion", {
|
||||
required: true,
|
||||
});
|
||||
const answers = readStringArrayParam(params, "pollOption", { required: true }) ?? [];
|
||||
const allowMultiselect = typeof params.pollMulti === "boolean" ? params.pollMulti : undefined;
|
||||
const answers = readStringArrayParam(params, "pollOption", { required: true });
|
||||
const allowMultiselect = readBooleanParam(params, "pollMulti");
|
||||
const durationHours = readNumberParam(params, "pollDurationHours", {
|
||||
integer: true,
|
||||
strict: true,
|
||||
});
|
||||
return await handleDiscordAction(
|
||||
{
|
||||
@@ -116,7 +118,7 @@ export async function handleDiscordMessageAction(
|
||||
);
|
||||
}
|
||||
const emoji = readStringParam(params, "emoji", { allowEmpty: true });
|
||||
const remove = typeof params.remove === "boolean" ? params.remove : undefined;
|
||||
const remove = readBooleanParam(params, "remove");
|
||||
return await handleDiscordAction(
|
||||
{
|
||||
action: "react",
|
||||
|
||||
@@ -6,10 +6,13 @@ import {
|
||||
} from "../../../agents/tools/common.js";
|
||||
import { handleTelegramAction } from "../../../agents/tools/telegram-actions.js";
|
||||
import type { TelegramActionConfig } from "../../../config/types.telegram.js";
|
||||
import { readBooleanParam } from "../../../plugin-sdk/boolean-param.js";
|
||||
import { extractToolSend } from "../../../plugin-sdk/tool-send.js";
|
||||
import { resolveTelegramPollVisibility } from "../../../poll-params.js";
|
||||
import {
|
||||
createTelegramActionGate,
|
||||
listEnabledTelegramAccounts,
|
||||
resolveTelegramPollActionGateState,
|
||||
} from "../../../telegram/accounts.js";
|
||||
import { isTelegramInlineButtonsEnabled } from "../../../telegram/inline-buttons.js";
|
||||
import type { ChannelMessageActionAdapter, ChannelMessageActionName } from "../types.js";
|
||||
@@ -27,8 +30,8 @@ function readTelegramSendParams(params: Record<string, unknown>) {
|
||||
const replyTo = readStringParam(params, "replyTo");
|
||||
const threadId = readStringParam(params, "threadId");
|
||||
const buttons = params.buttons;
|
||||
const asVoice = typeof params.asVoice === "boolean" ? params.asVoice : undefined;
|
||||
const silent = typeof params.silent === "boolean" ? params.silent : undefined;
|
||||
const asVoice = readBooleanParam(params, "asVoice");
|
||||
const silent = readBooleanParam(params, "silent");
|
||||
const quoteText = readStringParam(params, "quoteText");
|
||||
return {
|
||||
to,
|
||||
@@ -78,6 +81,16 @@ export const telegramMessageActions: ChannelMessageActionAdapter = {
|
||||
const isEnabled = (key: keyof TelegramActionConfig, defaultValue = true) =>
|
||||
gate(key, defaultValue);
|
||||
const actions = new Set<ChannelMessageActionName>(["send"]);
|
||||
const pollEnabledForAnyAccount = accounts.some((account) => {
|
||||
const accountGate = createTelegramActionGate({
|
||||
cfg,
|
||||
accountId: account.accountId,
|
||||
});
|
||||
return resolveTelegramPollActionGateState(accountGate).enabled;
|
||||
});
|
||||
if (pollEnabledForAnyAccount) {
|
||||
actions.add("poll");
|
||||
}
|
||||
if (isEnabled("reactions")) {
|
||||
actions.add("react");
|
||||
}
|
||||
@@ -125,7 +138,7 @@ export const telegramMessageActions: ChannelMessageActionAdapter = {
|
||||
if (action === "react") {
|
||||
const messageId = resolveReactionMessageId({ args: params, toolContext });
|
||||
const emoji = readStringParam(params, "emoji", { allowEmpty: true });
|
||||
const remove = typeof params.remove === "boolean" ? params.remove : undefined;
|
||||
const remove = readBooleanParam(params, "remove");
|
||||
return await handleTelegramAction(
|
||||
{
|
||||
action: "react",
|
||||
@@ -140,6 +153,45 @@ export const telegramMessageActions: ChannelMessageActionAdapter = {
|
||||
);
|
||||
}
|
||||
|
||||
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 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);
|
||||
|
||||
@@ -336,6 +336,12 @@ export type ChannelToolSend = {
|
||||
};
|
||||
|
||||
export type ChannelMessageActionAdapter = {
|
||||
/**
|
||||
* Advertise agent-discoverable actions for this channel.
|
||||
* Keep this aligned with any gated capability checks. Poll discovery is
|
||||
* not inferred from `outbound.sendPoll`, so channels that want agents to
|
||||
* create polls should include `"poll"` here when enabled.
|
||||
*/
|
||||
listActions?: (params: { cfg: OpenClawConfig }) => ChannelMessageActionName[];
|
||||
supportsAction?: (params: { action: ChannelMessageActionName }) => boolean;
|
||||
supportsButtons?: (params: { cfg: OpenClawConfig }) => boolean;
|
||||
|
||||
Reference in New Issue
Block a user