mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-25 23:43:33 +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
@@ -236,6 +236,72 @@ describe("runMessageAction context isolation", () => {
|
||||
).rejects.toThrow(/message required/i);
|
||||
});
|
||||
|
||||
it("rejects send actions that include poll creation params", async () => {
|
||||
await expect(
|
||||
runDrySend({
|
||||
cfg: slackConfig,
|
||||
actionParams: {
|
||||
channel: "slack",
|
||||
target: "#C12345678",
|
||||
message: "hi",
|
||||
pollQuestion: "Ready?",
|
||||
pollOption: ["Yes", "No"],
|
||||
},
|
||||
toolContext: { currentChannelId: "C12345678" },
|
||||
}),
|
||||
).rejects.toThrow(/use action "poll" instead of "send"/i);
|
||||
});
|
||||
|
||||
it("rejects send actions that include string-encoded poll params", async () => {
|
||||
await expect(
|
||||
runDrySend({
|
||||
cfg: slackConfig,
|
||||
actionParams: {
|
||||
channel: "slack",
|
||||
target: "#C12345678",
|
||||
message: "hi",
|
||||
pollDurationSeconds: "60",
|
||||
pollPublic: "true",
|
||||
},
|
||||
toolContext: { currentChannelId: "C12345678" },
|
||||
}),
|
||||
).rejects.toThrow(/use action "poll" instead of "send"/i);
|
||||
});
|
||||
|
||||
it("rejects send actions that include snake_case poll params", async () => {
|
||||
await expect(
|
||||
runDrySend({
|
||||
cfg: slackConfig,
|
||||
actionParams: {
|
||||
channel: "slack",
|
||||
target: "#C12345678",
|
||||
message: "hi",
|
||||
poll_question: "Ready?",
|
||||
poll_option: ["Yes", "No"],
|
||||
poll_public: "true",
|
||||
},
|
||||
toolContext: { currentChannelId: "C12345678" },
|
||||
}),
|
||||
).rejects.toThrow(/use action "poll" instead of "send"/i);
|
||||
});
|
||||
|
||||
it("allows send when poll booleans are explicitly false", async () => {
|
||||
const result = await runDrySend({
|
||||
cfg: slackConfig,
|
||||
actionParams: {
|
||||
channel: "slack",
|
||||
target: "#C12345678",
|
||||
message: "hi",
|
||||
pollMulti: false,
|
||||
pollAnonymous: false,
|
||||
pollPublic: false,
|
||||
},
|
||||
toolContext: { currentChannelId: "C12345678" },
|
||||
});
|
||||
|
||||
expect(result.kind).toBe("send");
|
||||
});
|
||||
|
||||
it("blocks send when target differs from current channel", async () => {
|
||||
const result = await runDrySend({
|
||||
cfg: slackConfig,
|
||||
@@ -902,6 +968,114 @@ describe("runMessageAction card-only send behavior", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("runMessageAction telegram plugin poll forwarding", () => {
|
||||
const handleAction = vi.fn(async ({ params }: { params: Record<string, unknown> }) =>
|
||||
jsonResult({
|
||||
ok: true,
|
||||
forwarded: {
|
||||
to: params.to ?? null,
|
||||
pollQuestion: params.pollQuestion ?? null,
|
||||
pollOption: params.pollOption ?? null,
|
||||
pollDurationSeconds: params.pollDurationSeconds ?? null,
|
||||
pollPublic: params.pollPublic ?? null,
|
||||
threadId: params.threadId ?? null,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const telegramPollPlugin: ChannelPlugin = {
|
||||
id: "telegram",
|
||||
meta: {
|
||||
id: "telegram",
|
||||
label: "Telegram",
|
||||
selectionLabel: "Telegram",
|
||||
docsPath: "/channels/telegram",
|
||||
blurb: "Telegram poll forwarding test plugin.",
|
||||
},
|
||||
capabilities: { chatTypes: ["direct"] },
|
||||
config: createAlwaysConfiguredPluginConfig(),
|
||||
messaging: {
|
||||
targetResolver: {
|
||||
looksLikeId: () => true,
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
listActions: () => ["poll"],
|
||||
supportsAction: ({ action }) => action === "poll",
|
||||
handleAction,
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePluginRegistry(
|
||||
createTestRegistry([
|
||||
{
|
||||
pluginId: "telegram",
|
||||
source: "test",
|
||||
plugin: telegramPollPlugin,
|
||||
},
|
||||
]),
|
||||
);
|
||||
handleAction.mockClear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
setActivePluginRegistry(createTestRegistry([]));
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("forwards telegram poll params through plugin dispatch", async () => {
|
||||
const result = await runMessageAction({
|
||||
cfg: {
|
||||
channels: {
|
||||
telegram: {
|
||||
botToken: "tok",
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
action: "poll",
|
||||
params: {
|
||||
channel: "telegram",
|
||||
target: "telegram:123",
|
||||
pollQuestion: "Lunch?",
|
||||
pollOption: ["Pizza", "Sushi"],
|
||||
pollDurationSeconds: 120,
|
||||
pollPublic: true,
|
||||
threadId: "42",
|
||||
},
|
||||
dryRun: false,
|
||||
});
|
||||
|
||||
expect(result.kind).toBe("poll");
|
||||
expect(result.handledBy).toBe("plugin");
|
||||
expect(handleAction).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
action: "poll",
|
||||
channel: "telegram",
|
||||
params: expect.objectContaining({
|
||||
to: "telegram:123",
|
||||
pollQuestion: "Lunch?",
|
||||
pollOption: ["Pizza", "Sushi"],
|
||||
pollDurationSeconds: 120,
|
||||
pollPublic: true,
|
||||
threadId: "42",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(result.payload).toMatchObject({
|
||||
ok: true,
|
||||
forwarded: {
|
||||
to: "telegram:123",
|
||||
pollQuestion: "Lunch?",
|
||||
pollOption: ["Pizza", "Sushi"],
|
||||
pollDurationSeconds: 120,
|
||||
pollPublic: true,
|
||||
threadId: "42",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("runMessageAction components parsing", () => {
|
||||
const handleAction = vi.fn(async ({ params }: { params: Record<string, unknown> }) =>
|
||||
jsonResult({
|
||||
|
||||
@@ -14,6 +14,8 @@ import type {
|
||||
} from "../../channels/plugins/types.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { getAgentScopedMediaLocalRoots } from "../../media/local-roots.js";
|
||||
import { hasPollCreationParams, resolveTelegramPollVisibility } from "../../poll-params.js";
|
||||
import { resolvePollMaxSelections } from "../../polls.js";
|
||||
import { buildChannelAccountBindings } from "../../routing/bindings.js";
|
||||
import { normalizeAgentId } from "../../routing/session-key.js";
|
||||
import { type GatewayClientMode, type GatewayClientName } from "../../utils/message-channel.js";
|
||||
@@ -307,7 +309,7 @@ async function handleBroadcastAction(
|
||||
if (!broadcastEnabled) {
|
||||
throw new Error("Broadcast is disabled. Set tools.message.broadcast.enabled to true.");
|
||||
}
|
||||
const rawTargets = readStringArrayParam(params, "targets", { required: true }) ?? [];
|
||||
const rawTargets = readStringArrayParam(params, "targets", { required: true });
|
||||
if (rawTargets.length === 0) {
|
||||
throw new Error("Broadcast requires at least one target in --targets.");
|
||||
}
|
||||
@@ -571,7 +573,7 @@ async function handlePollAction(ctx: ResolvedActionContext): Promise<MessageActi
|
||||
const question = readStringParam(params, "pollQuestion", {
|
||||
required: true,
|
||||
});
|
||||
const options = readStringArrayParam(params, "pollOption", { required: true }) ?? [];
|
||||
const options = readStringArrayParam(params, "pollOption", { required: true });
|
||||
if (options.length < 2) {
|
||||
throw new Error("pollOption requires at least two values");
|
||||
}
|
||||
@@ -579,17 +581,16 @@ async function handlePollAction(ctx: ResolvedActionContext): Promise<MessageActi
|
||||
const allowMultiselect = readBooleanParam(params, "pollMulti") ?? false;
|
||||
const pollAnonymous = readBooleanParam(params, "pollAnonymous");
|
||||
const pollPublic = readBooleanParam(params, "pollPublic");
|
||||
if (pollAnonymous && pollPublic) {
|
||||
throw new Error("pollAnonymous and pollPublic are mutually exclusive");
|
||||
}
|
||||
const isAnonymous = pollAnonymous ? true : pollPublic ? false : undefined;
|
||||
const isAnonymous = resolveTelegramPollVisibility({ pollAnonymous, pollPublic });
|
||||
const durationHours = readNumberParam(params, "pollDurationHours", {
|
||||
integer: true,
|
||||
strict: true,
|
||||
});
|
||||
const durationSeconds = readNumberParam(params, "pollDurationSeconds", {
|
||||
integer: true,
|
||||
strict: true,
|
||||
});
|
||||
const maxSelections = allowMultiselect ? Math.max(2, options.length) : 1;
|
||||
const maxSelections = resolvePollMaxSelections(options.length, allowMultiselect);
|
||||
|
||||
if (durationSeconds !== undefined && channel !== "telegram") {
|
||||
throw new Error("pollDurationSeconds is only supported for Telegram polls");
|
||||
@@ -766,6 +767,10 @@ export async function runMessageAction(
|
||||
cfg,
|
||||
});
|
||||
|
||||
if (action === "send" && hasPollCreationParams(params)) {
|
||||
throw new Error('Poll fields require action "poll"; use action "poll" instead of "send".');
|
||||
}
|
||||
|
||||
const gateway = resolveGateway(input);
|
||||
|
||||
if (action === "send") {
|
||||
|
||||
Reference in New Issue
Block a user