Pular para o conteúdo principal

Como Criar um Bot de Enquete no WhatsApp Como Criar um Bot de Enquete no WhatsApp

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.

Hashes dos votos

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

Votos das enquetes são criptografados

Você precisa chamar decryptPollVote() para ler os votos. A pollUpdateMessage bruta contém dados criptografados que são ilegíveis sem descriptografia.

Resultados em memória

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.

Loops de eco

Sempre verifique info.isFromMe. O bot cria enquetes que disparam eventos de mensagem — sem essa verificação, ele poderia reagir às próprias enquetes.