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

@@ -2,6 +2,12 @@ export type ActiveWebSendOptions = {
gifPlayback?: boolean;
};
export type PollOptions = {
question: string;
options: string[];
selectableCount?: number;
};
export type ActiveWebListener = {
sendMessage: (
to: string,
@@ -10,6 +16,7 @@ export type ActiveWebListener = {
mediaType?: string,
options?: ActiveWebSendOptions,
) => Promise<{ messageId: string }>;
sendPoll: (to: string, poll: PollOptions) => Promise<{ messageId: string }>;
sendComposingTo: (to: string) => Promise<void>;
close?: () => Promise<void>;
};

View File

@@ -464,6 +464,24 @@ export async function monitorWebInbox(options: {
const jid = toWhatsappJid(to);
await sock.sendPresenceUpdate("composing", jid);
},
/**
* Send a poll message through this connection's socket.
* Used by IPC to create WhatsApp polls in groups or chats.
*/
sendPoll: async (
to: string,
poll: { question: string; options: string[]; selectableCount?: number },
): Promise<{ messageId: string }> => {
const jid = toWhatsappJid(to);
const result = await sock.sendMessage(jid, {
poll: {
name: poll.question,
values: poll.options,
selectableCount: poll.selectableCount ?? 1,
},
});
return { messageId: result?.key?.id ?? "unknown" };
},
} as const;
}

View File

@@ -4,6 +4,7 @@ import { createSubsystemLogger, getChildLogger } from "../logging.js";
import { toWhatsappJid } from "../utils.js";
import {
type ActiveWebSendOptions,
type PollOptions,
getActiveWebListener,
} from "./active-listener.js";
import { loadWebMedia } from "./media.js";
@@ -85,3 +86,44 @@ export async function sendMessageWhatsApp(
throw err;
}
}
export async function sendPollWhatsApp(
to: string,
poll: PollOptions,
options: { verbose: boolean },
): Promise<{ messageId: string; toJid: string }> {
const correlationId = randomUUID();
const startedAt = Date.now();
const active = getActiveWebListener();
if (!active) {
throw new Error(
"No active gateway listener. Start the gateway before sending WhatsApp polls.",
);
}
const logger = getChildLogger({
module: "web-outbound",
correlationId,
to,
});
try {
const jid = toWhatsappJid(to);
outboundLog.info(`Sending poll -> ${jid}: "${poll.question}"`);
logger.info(
{ jid, question: poll.question, optionCount: poll.options.length },
"sending poll",
);
const result = await active.sendPoll(to, poll);
const messageId =
(result as { messageId?: string })?.messageId ?? "unknown";
const durationMs = Date.now() - startedAt;
outboundLog.info(`Sent poll ${messageId} -> ${jid} (${durationMs}ms)`);
logger.info({ jid, messageId }, "sent poll");
return { messageId, toJid: jid };
} catch (err) {
logger.error(
{ err: String(err), to, question: poll.question },
"failed to send poll via web session",
);
throw err;
}
}