feat: Add WhatsApp poll support (#248)

Implements issue #123 - WhatsApp Poll Support

## Gateway Protocol
- Add `poll` RPC method with params: to, question, options (2-12), selectableCount

## ActiveWebListener
- Add `sendPoll(to, poll)` method to interface
- Implementation uses Baileys poll message type

## CLI Command
- `clawdbot poll --to <jid> -q <question> -o <opt1> -o <opt2> [-s count]`
- Supports --dry-run, --json, --verbose flags
- Validates 2-12 options

## Changes
- src/gateway/protocol/schema.ts: Add PollParamsSchema
- src/gateway/protocol/index.ts: Export validator and types
- src/web/active-listener.ts: Add sendPoll to interface
- src/web/inbound.ts: Implement sendPoll using Baileys
- src/web/outbound.ts: Add sendPollWhatsApp function
- src/gateway/server-methods/send.ts: Add poll handler
- src/commands/poll.ts: New CLI command
- src/cli/program.ts: Register poll command

Closes #123
This commit is contained in:
DBH
2026-01-05 23:44:15 -05:00
committed by GitHub
parent ea6ee16461
commit 2737e17c67
8 changed files with 278 additions and 1 deletions

View File

@@ -79,6 +79,8 @@ import {
type ResponseFrame,
ResponseFrameSchema,
SendParamsSchema,
type PollParams,
PollParamsSchema,
type SessionsCompactParams,
SessionsCompactParamsSchema,
type SessionsDeleteParams,
@@ -147,6 +149,7 @@ export const validateResponseFrame =
ajv.compile<ResponseFrame>(ResponseFrameSchema);
export const validateEventFrame = ajv.compile<EventFrame>(EventFrameSchema);
export const validateSendParams = ajv.compile(SendParamsSchema);
export const validatePollParams = ajv.compile<PollParams>(PollParamsSchema);
export const validateAgentParams = ajv.compile(AgentParamsSchema);
export const validateAgentWaitParams = ajv.compile<AgentWaitParams>(
AgentWaitParamsSchema,
@@ -282,6 +285,7 @@ export {
AgentEventSchema,
ChatEventSchema,
SendParamsSchema,
PollParamsSchema,
AgentParamsSchema,
WakeParamsSchema,
NodePairRequestParamsSchema,
@@ -390,4 +394,5 @@ export type {
CronRunParams,
CronRunsParams,
CronRunLogEntry,
PollParams,
};

View File

@@ -198,6 +198,17 @@ export const SendParamsSchema = Type.Object(
{ additionalProperties: false },
);
export const PollParamsSchema = Type.Object(
{
to: NonEmptyString,
question: NonEmptyString,
options: Type.Array(NonEmptyString, { minItems: 2, maxItems: 12 }),
selectableCount: Type.Optional(Type.Integer({ minimum: 1, maximum: 12 })),
idempotencyKey: NonEmptyString,
},
{ additionalProperties: false },
);
export const AgentParamsSchema = Type.Object(
{
message: NonEmptyString,
@@ -831,6 +842,7 @@ export const ProtocolSchemas: Record<string, TSchema> = {
ErrorShape: ErrorShapeSchema,
AgentEvent: AgentEventSchema,
SendParams: SendParamsSchema,
PollParams: PollParamsSchema,
AgentParams: AgentParamsSchema,
AgentWaitParams: AgentWaitParamsSchema,
WakeParams: WakeParamsSchema,
@@ -900,6 +912,7 @@ export type PresenceEntry = Static<typeof PresenceEntrySchema>;
export type ErrorShape = Static<typeof ErrorShapeSchema>;
export type StateVersion = Static<typeof StateVersionSchema>;
export type AgentEvent = Static<typeof AgentEventSchema>;
export type PollParams = Static<typeof PollParamsSchema>;
export type AgentWaitParams = Static<typeof AgentWaitParamsSchema>;
export type WakeParams = Static<typeof WakeParamsSchema>;
export type NodePairRequestParams = Static<typeof NodePairRequestParamsSchema>;

View File

@@ -6,11 +6,12 @@ import { sendMessageSignal } from "../../signal/index.js";
import { sendMessageSlack } from "../../slack/send.js";
import { sendMessageTelegram } from "../../telegram/send.js";
import { resolveTelegramToken } from "../../telegram/token.js";
import { sendMessageWhatsApp } from "../../web/outbound.js";
import { sendMessageWhatsApp, sendPollWhatsApp } from "../../web/outbound.js";
import {
ErrorCodes,
errorShape,
formatValidationErrors,
validatePollParams,
validateSendParams,
} from "../protocol/index.js";
import { formatForLog } from "../ws-log.js";
@@ -178,4 +179,69 @@ export const sendHandlers: GatewayRequestHandlers = {
});
}
},
poll: async ({ params, respond, context }) => {
const p = params as Record<string, unknown>;
if (!validatePollParams(p)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid poll params: ${formatValidationErrors(validatePollParams.errors)}`,
),
);
return;
}
const request = p as {
to: string;
question: string;
options: string[];
selectableCount?: number;
idempotencyKey: string;
};
const idem = request.idempotencyKey;
const cached = context.dedupe.get(`poll:${idem}`);
if (cached) {
respond(cached.ok, cached.payload, cached.error, {
cached: true,
});
return;
}
const to = request.to.trim();
const question = request.question.trim();
const options = request.options.map((o) => o.trim());
const selectableCount = request.selectableCount ?? 1;
try {
const result = await sendPollWhatsApp(
to,
{ question, options, selectableCount },
{ verbose: shouldLogVerbose() },
);
const payload = {
runId: idem,
messageId: result.messageId,
toJid: result.toJid ?? `${to}@s.whatsapp.net`,
provider: "whatsapp",
};
context.dedupe.set(`poll:${idem}`, {
ts: Date.now(),
ok: true,
payload,
});
respond(true, payload, undefined, { provider: "whatsapp" });
} catch (err) {
const error = errorShape(ErrorCodes.UNAVAILABLE, String(err));
context.dedupe.set(`poll:${idem}`, {
ts: Date.now(),
ok: false,
error,
});
respond(false, undefined, error, {
provider: "whatsapp",
error: formatForLog(err),
});
}
},
};