
Como Criar um Bot de Enquete no WhatsApp
O whatsmeow-node permite criar enquetes, receber votos criptografados, descriptografá-los e contabilizar resultados — tudo programaticamente. Isso é ótimo para bots de grupo que fazem votações, pesquisas ou fluxos de tomada de decisão.
Pré-requisitos
- Uma sessão pareada do whatsmeow-node (Como Parear)
- Um grupo para testar (enquetes funcionam melhor em chats de grupo)
Passo 1: Criar uma Enquete
const resp = await client.sendPollCreation(
groupJid,
"Where should we eat?", // question
["Pizza", "Sushi", "Tacos"], // options
1, // max selectable (1 = single choice)
);
console.log(`Poll sent with ID: ${resp.id}`);
Defina selectableCount para permitir múltiplas seleções:
// Multi-select poll — voters can pick up to 3
await client.sendPollCreation(
groupJid,
"Which topics should we cover?",
["Frontend", "Backend", "DevOps", "Mobile", "AI"],
3,
);
Passo 2: Ouvir os Votos
Os votos das enquetes chegam como eventos message com dados criptografados. Use decryptPollVote() para lê-los.
decryptPollVote() retorna selectedOptions como hashes SHA-256 (codificados em base64), não o texto das opções. Você precisa fazer o hash das suas opções conhecidas e comparar para identificar quais opções foram selecionadas.
import { createHash } from "node:crypto";
// Hash an option name the same way WhatsApp does
function hashOption(option: string): string {
return createHash("sha256").update(option).digest("base64");
}
client.on("message", async ({ info, message }) => {
const pollUpdate = message.pollUpdateMessage as Record<string, unknown> | undefined;
if (!pollUpdate) return;
// Get the poll message ID from the poll update
const pollKey = pollUpdate.pollCreationMessageKey as { id?: string } | undefined;
const pollId = pollKey?.id;
if (!pollId) return;
try {
const vote = await client.decryptPollVote(
info as unknown as Record<string, unknown>,
message,
);
const hashes = (vote.selectedOptions ?? []) as string[];
console.log(`${info.sender} voted (${hashes.length} options)`);
} catch (err) {
console.error("Failed to decrypt poll vote:", err);
}
});
Passo 3: Rastrear Resultados
Armazene as enquetes com um lookup hash-para-opção para resolver os hashes dos votos de volta para os nomes das opções:
import { createHash } from "node:crypto";
function hashOption(option: string): string {
return createHash("sha256").update(option).digest("base64");
}
type PollData = {
question: string;
hashToOption: Map<string, string>; // SHA-256 hash → option name
results: Map<string, string[]>; // option name → voter JIDs
};
const polls = new Map<string, PollData>();
// When creating a poll, store option hashes
async function createPoll(groupJid: string, question: string, options: string[]) {
const resp = await client.sendPollCreation(groupJid, question, options, 1);
const hashToOption = new Map<string, string>();
const results = new Map<string, string[]>();
for (const opt of options) {
hashToOption.set(hashOption(opt), opt);
results.set(opt, []);
}
polls.set(resp.id, { question, hashToOption, results });
return resp;
}
// When receiving a vote, resolve hashes and tally
function recordVote(pollId: string, voter: string, selectedHashes: string[]) {
const poll = polls.get(pollId);
if (!poll) return;
// Remove previous votes from this voter
for (const [, voters] of poll.results) {
const idx = voters.indexOf(voter);
if (idx !== -1) voters.splice(idx, 1);
}
// Resolve hashes to option names and add votes
for (const hash of selectedHashes) {
const option = poll.hashToOption.get(hash);
if (option) {
poll.results.get(option)?.push(voter);
}
}
}
Passo 4: Anunciar Resultados
Envie um resumo com o comando !results:
function formatResults(pollId: string): string | null {
const poll = polls.get(pollId);
if (!poll) return null;
let text = `Poll: ${poll.question}\n\n`;
const sorted = [...poll.results.entries()].sort((a, b) => b[1].length - a[1].length);
for (const [option, voters] of sorted) {
const bar = "█".repeat(voters.length);
text += `${option}: ${bar} ${voters.length}\n`;
}
const total = sorted.reduce((sum, [, v]) => sum + v.length, 0);
text += `\nTotal votes: ${total}`;
return text;
}
Exemplo Completo
Um bot de grupo que trata comandos !poll e !results:
import { createClient } from "@whatsmeow-node/whatsmeow-node";
import { createHash } from "node:crypto";
const client = createClient({ store: "session.db" });
function hashOption(option: string): string {
return createHash("sha256").update(option).digest("base64");
}
type PollData = {
question: string;
chat: string;
hashToOption: Map<string, string>;
results: Map<string, string[]>;
};
const polls = new Map<string, PollData>();
let lastPollId: string | null = null;
client.on("message", async ({ info, message }) => {
if (info.isFromMe) return;
// Handle poll votes
const pollUpdate = message.pollUpdateMessage as Record<string, unknown> | undefined;
if (pollUpdate) {
const pollKey = pollUpdate.pollCreationMessageKey as { id?: string } | undefined;
const pollId = pollKey?.id;
if (!pollId) return;
const poll = polls.get(pollId);
if (!poll) return;
try {
const vote = await client.decryptPollVote(
info as unknown as Record<string, unknown>,
message,
);
const selectedHashes = (vote.selectedOptions ?? []) as string[];
// Remove previous votes from this voter
for (const [, voters] of poll.results) {
const idx = voters.indexOf(info.sender);
if (idx !== -1) voters.splice(idx, 1);
}
// Resolve hashes to option names and tally
for (const hash of selectedHashes) {
const option = poll.hashToOption.get(hash);
if (option) {
poll.results.get(option)?.push(info.sender);
}
}
} catch {
// Ignore decryption errors for polls we don't track
}
return;
}
// Handle text commands
const text =
(message.conversation as string) ??
(message.extendedTextMessage as { text?: string } | undefined)?.text;
if (!text?.startsWith("!")) return;
// !poll Where should we eat? | Pizza | Sushi | Tacos
if (text.startsWith("!poll ")) {
const parts = text.slice(6).split("|").map((s) => s.trim());
if (parts.length < 3) {
await client.sendMessage(info.chat, {
conversation: "Usage: !poll Question | Option 1 | Option 2 | ...",
});
return;
}
const [question, ...options] = parts;
const resp = await client.sendPollCreation(info.chat, question, options, 1);
const hashToOption = new Map<string, string>();
const results = new Map<string, string[]>();
for (const opt of options) {
hashToOption.set(hashOption(opt), opt);
results.set(opt, []);
}
polls.set(resp.id, { question, chat: info.chat, hashToOption, results });
lastPollId = resp.id;
return;
}
// !results — show results for the last poll
if (text === "!results") {
if (!lastPollId || !polls.has(lastPollId)) {
await client.sendMessage(info.chat, {
conversation: "No active poll. Create one with !poll",
});
return;
}
const poll = polls.get(lastPollId)!;
let summary = `Poll: ${poll.question}\n\n`;
const sorted = [...poll.results.entries()].sort((a, b) => b[1].length - a[1].length);
for (const [option, voters] of sorted) {
summary += `${option}: ${"█".repeat(voters.length)} ${voters.length}\n`;
}
const total = sorted.reduce((sum, [, v]) => sum + v.length, 0);
summary += `\nTotal votes: ${total}`;
await client.sendMessage(info.chat, { conversation: summary });
return;
}
});
async function main() {
const { jid } = await client.init();
if (!jid) {
console.error("Not paired!");
process.exit(1);
}
await client.connect();
await client.sendPresence("available");
console.log("Poll bot is online! Commands: !poll, !results");
process.on("SIGINT", async () => {
await client.sendPresence("unavailable");
await client.disconnect();
client.close();
process.exit(0);
});
}
main().catch(console.error);
Erros Comuns
Você precisa chamar decryptPollVote() para ler os votos. A pollUpdateMessage bruta contém dados criptografados que são ilegíveis sem descriptografia.
O exemplo armazena dados da enquete em um Map que é perdido ao reiniciar. Para produção, persista enquetes e votos em um banco de dados.
Sempre verifique info.isFromMe. O bot cria enquetes que disparam eventos de mensagem — sem essa verificação, ele poderia reagir às próprias enquetes.





