-
Notifications
You must be signed in to change notification settings - Fork 5k
Feature: Endpoint para Descriptografar e Visualizar Votos de Enquetes #2297
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Conversation
Introduces a new API endpoint and supporting logic to decrypt WhatsApp poll votes. Adds DecryptPollVoteDto, validation schema, controller method, and service logic to process and aggregate poll vote results based on poll creation message key.
Updated DecryptPollVoteDto to use a nested message.key structure and moved remoteJid to the top level. Adjusted the controller and validation schema to match the new structure for consistency and clarity.
Reviewer's GuideAdds a new authenticated POST /chat/getPollVote/:instanceName endpoint that validates a poll message key and remoteJid, then uses a Baileys-based service method to locate the poll creation message and related pollUpdateMessage records, decrypt votes (handling multiple key/JID formats and already-decrypted payloads), and return an aggregated, user-friendly summary of poll results. Sequence diagram for decrypting and viewing WhatsApp poll votessequenceDiagram
actor Client
participant ChatRouter
participant ChatController
participant WaMonitor
participant BaileysStartupService
participant PrismaRepository
participant BaileysLib
Client->>ChatRouter: POST /chat/getPollVote/:instanceName
ChatRouter->>ChatRouter: dataValidate(request, decryptPollVoteSchema, DecryptPollVoteDto)
ChatRouter->>ChatController: decryptPollVote(instanceDto, decryptPollVoteDto)
ChatController->>WaMonitor: waInstances[instanceName]
WaMonitor-->>ChatController: BaileysStartupService
ChatController->>BaileysStartupService: baileysDecryptPollVote(pollCreationMessageKey)
BaileysStartupService->>BaileysStartupService: getMessage(pollCreationMessageKey, true)
BaileysStartupService-->>BaileysStartupService: pollCreationMessage
BaileysStartupService->>BaileysStartupService: getMessage(pollCreationMessageKey)
BaileysStartupService-->>BaileysStartupService: pollMessageSecret
BaileysStartupService->>BaileysStartupService: normalize pollEncKey
BaileysStartupService->>PrismaRepository: message.findMany(instanceId, messageType=pollUpdateMessage)
PrismaRepository-->>BaileysStartupService: allPollUpdateMessages
BaileysStartupService->>BaileysStartupService: filter pollUpdateMessages by pollCreationMessageKey
loop For each pollUpdateMessage
BaileysStartupService->>BaileysStartupService: build creatorCandidates and voterCandidates
alt vote has selectedOptions as strings
BaileysStartupService->>BaileysStartupService: use existing option names
else vote has selectedOptions as hashes
BaileysStartupService->>BaileysStartupService: hash pollOptions and match to selectedOptions
else vote has encPayload
loop creatorCandidates x voterCandidates
BaileysStartupService->>BaileysLib: decryptPollVote(pollVote, pollCreatorJid, pollMsgId, pollEncKey, voterJid)
BaileysLib-->>BaileysStartupService: decryptedVote or error
end
BaileysStartupService->>BaileysStartupService: map decrypted hashes to option names
else invalid vote payload
BaileysStartupService->>BaileysStartupService: skip vote
end
BaileysStartupService->>BaileysStartupService: keep most recent vote per voter in votesByUser
end
BaileysStartupService->>BaileysStartupService: aggregate results by option
BaileysStartupService-->>ChatController: { poll: { name, totalVotes, results } }
ChatController-->>ChatRouter: response payload
ChatRouter-->>Client: 200 OK with poll results
Class diagram for new poll vote decryption endpointclassDiagram
class DecryptPollVoteDto {
+object message
+string remoteJid
}
class DecryptPollVoteDto_message {
+object key
}
class DecryptPollVoteDto_key {
+string id
}
DecryptPollVoteDto --> DecryptPollVoteDto_message : has
DecryptPollVoteDto_message --> DecryptPollVoteDto_key : has
class ChatController {
+WaMonitor waMonitor
+Promise blockUser(InstanceDto instanceDto, BlockUserDto data)
+Promise decryptPollVote(InstanceDto instanceDto, DecryptPollVoteDto data)
}
class WaMonitor {
+Record~string, BaileysStartupService~ waInstances
}
class BaileysStartupService {
+string instanceId
+object instance
+object client
+PrismaRepository prismaRepository
+Logger logger
+Promise baileysDecryptPollVote(protoIMessageKey pollCreationMessageKey)
}
class PrismaRepository {
+MessageRepository message
}
class MessageRepository {
+Promise findMany(object whereSelect)
}
class BlockUserDto {
+string number
+string status
}
class InstanceDto {
+string instanceName
}
class decryptPollVoteSchema {
+string $id
+string type
+object properties
+string[] required
}
ChatController --> WaMonitor : uses
WaMonitor --> BaileysStartupService : contains
ChatController --> DecryptPollVoteDto : uses
ChatController --> InstanceDto : uses
ChatController --> BlockUserDto : uses
BaileysStartupService --> PrismaRepository : uses
PrismaRepository --> MessageRepository : has
ChatRouter --> ChatController : uses
ChatRouter --> decryptPollVoteSchema : validates
class ChatRouter {
+string basePath
+Router router
+post(string path, middleware guards, function handler)
+Promise dataValidate(object options)
}
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey there - I've reviewed your changes and found some issues that need to be addressed.
- In
baileysDecryptPollVote, the broadtry/catchwrapsNotFoundExceptioncases (e.g., poll creation/options/key not found) into a genericInternalServerErrorException, which loses the more accurate 4xx semantics; consider only wrapping unexpected errors or rethrowing known HTTP exceptions unchanged. - The query that loads all
pollUpdateMessagerecords for an instance and then filters them in memory can become expensive on large datasets; consider adding DB-level filters onmessage.pollUpdateMessage.pollCreationMessageKey(id/remoteJid) if possible to reduce the result set size.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `baileysDecryptPollVote`, the broad `try/catch` wraps `NotFoundException` cases (e.g., poll creation/options/key not found) into a generic `InternalServerErrorException`, which loses the more accurate 4xx semantics; consider only wrapping unexpected errors or rethrowing known HTTP exceptions unchanged.
- The query that loads all `pollUpdateMessage` records for an instance and then filters them in memory can become expensive on large datasets; consider adding DB-level filters on `message.pollUpdateMessage.pollCreationMessageKey` (id/remoteJid) if possible to reduce the result set size.
## Individual Comments
### Comment 1
<location> `src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts:5164-5173` </location>
<code_context>
+ }
+
+ // Buscar todas as mensagens de atualização de votos
+ const allPollUpdateMessages = await this.prismaRepository.message.findMany({
+ where: {
+ instanceId: this.instanceId,
+ messageType: 'pollUpdateMessage',
+ },
+ select: {
+ id: true,
+ key: true,
+ message: true,
+ messageTimestamp: true,
+ },
+ });
+
+ this.logger.verbose(`Found ${allPollUpdateMessages.length} pollUpdateMessage messages in database`);
+
+ // Filtrar apenas mensagens relacionadas a esta enquete específica
</code_context>
<issue_to_address>
**suggestion (performance):** Filter poll update messages at the DB level to avoid loading all messages into memory.
This query only filters by `instanceId` and `messageType` and then narrows to the specific poll in memory. On instances with many polls, this can load a large `pollUpdateMessage` set on every `baileysDecryptPollVote` call. Where possible, add more selective criteria to the DB query (e.g., store and filter by `pollCreationMessageKey.id` / `remoteJid` or a derived index) so only updates for the target poll are loaded.
Suggested implementation:
```typescript
// Buscar apenas mensagens de atualização de votos relacionadas a esta enquete específica
const allPollUpdateMessages = await this.prismaRepository.message.findMany({
where: {
instanceId: this.instanceId,
messageType: 'pollUpdateMessage',
// Filtrar por remoteJid da enquete de criação, para reduzir o conjunto de mensagens
key: {
remoteJid: pollCreationMessageKey.remoteJid,
},
// Se o campo `message` for JSON/JSONB, usamos filtro por caminho para restringir ao ID da mensagem de criação
// Isso evita carregar atualizações de outras enquetes no mesmo grupo/contato
message: {
path: ['pollUpdateMessage', 'pollCreationMessageKey', 'id'],
equals: pollCreationMessageKey.id,
} as any,
},
select: {
id: true,
key: true,
message: true,
messageTimestamp: true,
},
});
this.logger.verbose(
`Found ${allPollUpdateMessages.length} pollUpdateMessage messages in database for poll ${pollCreationMessageKey.id} / ${pollCreationMessageKey.remoteJid}`,
);
// Filtrar apenas mensagens relacionadas a esta enquete específica (filtro em memória, como camada extra de segurança)
const pollUpdateMessages = allPollUpdateMessages.filter((msg) => {
const pollUpdate = (msg.message as any)?.pollUpdateMessage;
if (!pollUpdate) return false;
const creationKey = pollUpdate.pollCreationMessageKey;
if (!creationKey) return false;
return (
creationKey.id === pollCreationMessageKey.id &&
jidNormalizedUser(creationKey.remoteJid || '') === jidNormalizedUser(pollCreationMessageKey.remoteJid || '')
```
If your Prisma model does not currently support JSON path filtering on `message`, you will need to:
1. Ensure the `message` field is of type `Json` (`Json` / `Json?`) in your Prisma schema and that your DB column is `jsonb` (Postgres) or equivalent.
2. If JSON path filters are not available in your Prisma version, replace the `message` filter with whatever is supported in your setup (e.g., a custom index or a different column storing `pollCreationMessageKey.id`), while keeping the new `key.remoteJid` filter.
3. Optionally, add a DB index on `(instanceId, messageType, key->>'remoteJid', message->'pollUpdateMessage'->'pollCreationMessageKey'->>'id')` or equivalent in your DB to make this query efficient at scale.
</issue_to_address>
### Comment 2
<location> `src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts:5361-5362` </location>
<code_context>
+ },
+ };
+ } catch (error) {
+ this.logger.error(`Error decrypting poll votes: ${error}`);
+ throw new InternalServerErrorException('Error decrypting poll votes', error.toString());
+ }
+ }
</code_context>
<issue_to_address>
**🚨 issue (security):** Avoid including raw error content in the HTTP error description to limit information leakage.
Using `error.toString()` in the exception body risks exposing internal details (e.g., stack traces, DB errors) to clients, depending on serialization. Instead, log the full error for observability and throw `InternalServerErrorException` with only a generic, user-safe message like `Error decrypting poll votes`, or a sanitized subset of the error if truly needed.
</issue_to_address>
### Comment 3
<location> `src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts:5240` </location>
<code_context>
+ if (selectedOptions.length > 0 && typeof selectedOptions[0] === 'string') {
+ // Já está descriptografado como nomes de opções
+ selectedOptionNames = selectedOptions;
+ successfulVoterJid = uniqueVoters[0];
+ this.logger.verbose(`Using already decrypted vote: voter=${successfulVoterJid}, options=${selectedOptionNames.join(',')}`);
+ } else {
</code_context>
<issue_to_address>
**suggestion (bug_risk):** When using pre-decrypted `selectedOptions`, the voter JID is assumed rather than derived, which may be fragile.
In the branch where `selectedOptions` is already decrypted, `successfulVoterJid` is taken as `uniqueVoters[0]` without verifying it matches the actual sender/participant. If `voterCandidates` can contain multiple JIDs, this may attribute a vote to the wrong user. Consider preferring a definitive participant JID from the message metadata if available, or explicitly ordering `voterCandidates` by reliability and documenting that assumption.
Suggested implementation:
```typescript
// Verificar se são strings (já descriptografado) ou buffers (precisa descriptografar)
if (selectedOptions.length > 0 && typeof selectedOptions[0] === 'string') {
// Já está descriptografado como nomes de opções
selectedOptionNames = selectedOptions;
// Preferir um JID de participante confiável a partir dos metadados da mensagem
// antes de recorrer a uma suposição baseada em listas agregadas.
const definitiveVoterJid =
// JID do participante definido pelo próprio evento/mensagem (mais confiável)
(pollVote.participant as string | undefined) ||
(pollVote.sender as string | undefined) ||
// Se houver candidatos de votante, assumir que estão ordenados por confiabilidade
(Array.isArray(voterCandidates) && voterCandidates.length > 0
? (voterCandidates[0] as string)
: undefined) ||
// Fallback final para a lista de votantes únicos
(Array.isArray(uniqueVoters) && uniqueVoters.length > 0
? (uniqueVoters[0] as string)
: undefined);
successfulVoterJid = definitiveVoterJid;
if (!successfulVoterJid) {
this.logger.warn(
`Unable to reliably determine voter JID for already decrypted vote; options=${selectedOptionNames.join(',')}`,
);
} else if (Array.isArray(voterCandidates) && voterCandidates.length > 1) {
this.logger.verbose(
`Multiple voterCandidates found for already decrypted vote; candidates=${voterCandidates.join(
',',
)}, chosen=${successfulVoterJid}`,
);
}
this.logger.verbose(
`Using already decrypted vote: voter=${successfulVoterJid ?? 'unknown'}, options=${selectedOptionNames.join(',')}`,
);
} else {
```
1. Ensure that `pollVote.participant` and/or `pollVote.sender` are the correct fields exposed by the Baileys poll vote event in your codebase. If your structure differs, adjust the `definitiveVoterJid` derivation to use the appropriate metadata fields (e.g. `pollVote.key.participant` or similar).
2. Confirm that `voterCandidates` is in scope at this point and that its elements are ordered by reliability as assumed; if not, you may want to sort or filter it earlier in the code to enforce the intended reliability ordering.
3. If you use strict TypeScript settings, you may wish to refine the types for `pollVote`, `voterCandidates`, and `uniqueVoters` instead of relying on `as string | undefined` casts here.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| const allPollUpdateMessages = await this.prismaRepository.message.findMany({ | ||
| where: { | ||
| instanceId: this.instanceId, | ||
| messageType: 'pollUpdateMessage', | ||
| }, | ||
| select: { | ||
| id: true, | ||
| key: true, | ||
| message: true, | ||
| messageTimestamp: true, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (performance): Filter poll update messages at the DB level to avoid loading all messages into memory.
This query only filters by instanceId and messageType and then narrows to the specific poll in memory. On instances with many polls, this can load a large pollUpdateMessage set on every baileysDecryptPollVote call. Where possible, add more selective criteria to the DB query (e.g., store and filter by pollCreationMessageKey.id / remoteJid or a derived index) so only updates for the target poll are loaded.
Suggested implementation:
// Buscar apenas mensagens de atualização de votos relacionadas a esta enquete específica
const allPollUpdateMessages = await this.prismaRepository.message.findMany({
where: {
instanceId: this.instanceId,
messageType: 'pollUpdateMessage',
// Filtrar por remoteJid da enquete de criação, para reduzir o conjunto de mensagens
key: {
remoteJid: pollCreationMessageKey.remoteJid,
},
// Se o campo `message` for JSON/JSONB, usamos filtro por caminho para restringir ao ID da mensagem de criação
// Isso evita carregar atualizações de outras enquetes no mesmo grupo/contato
message: {
path: ['pollUpdateMessage', 'pollCreationMessageKey', 'id'],
equals: pollCreationMessageKey.id,
} as any,
},
select: {
id: true,
key: true,
message: true,
messageTimestamp: true,
},
});
this.logger.verbose(
`Found ${allPollUpdateMessages.length} pollUpdateMessage messages in database for poll ${pollCreationMessageKey.id} / ${pollCreationMessageKey.remoteJid}`,
);
// Filtrar apenas mensagens relacionadas a esta enquete específica (filtro em memória, como camada extra de segurança)
const pollUpdateMessages = allPollUpdateMessages.filter((msg) => {
const pollUpdate = (msg.message as any)?.pollUpdateMessage;
if (!pollUpdate) return false;
const creationKey = pollUpdate.pollCreationMessageKey;
if (!creationKey) return false;
return (
creationKey.id === pollCreationMessageKey.id &&
jidNormalizedUser(creationKey.remoteJid || '') === jidNormalizedUser(pollCreationMessageKey.remoteJid || '')If your Prisma model does not currently support JSON path filtering on message, you will need to:
- Ensure the
messagefield is of typeJson(Json/Json?) in your Prisma schema and that your DB column isjsonb(Postgres) or equivalent. - If JSON path filters are not available in your Prisma version, replace the
messagefilter with whatever is supported in your setup (e.g., a custom index or a different column storingpollCreationMessageKey.id), while keeping the newkey.remoteJidfilter. - Optionally, add a DB index on
(instanceId, messageType, key->>'remoteJid', message->'pollUpdateMessage'->'pollCreationMessageKey'->>'id')or equivalent in your DB to make this query efficient at scale.
| if (selectedOptions.length > 0 && typeof selectedOptions[0] === 'string') { | ||
| // Já está descriptografado como nomes de opções | ||
| selectedOptionNames = selectedOptions; | ||
| successfulVoterJid = uniqueVoters[0]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (bug_risk): When using pre-decrypted selectedOptions, the voter JID is assumed rather than derived, which may be fragile.
In the branch where selectedOptions is already decrypted, successfulVoterJid is taken as uniqueVoters[0] without verifying it matches the actual sender/participant. If voterCandidates can contain multiple JIDs, this may attribute a vote to the wrong user. Consider preferring a definitive participant JID from the message metadata if available, or explicitly ordering voterCandidates by reliability and documenting that assumption.
Suggested implementation:
// Verificar se são strings (já descriptografado) ou buffers (precisa descriptografar)
if (selectedOptions.length > 0 && typeof selectedOptions[0] === 'string') {
// Já está descriptografado como nomes de opções
selectedOptionNames = selectedOptions;
// Preferir um JID de participante confiável a partir dos metadados da mensagem
// antes de recorrer a uma suposição baseada em listas agregadas.
const definitiveVoterJid =
// JID do participante definido pelo próprio evento/mensagem (mais confiável)
(pollVote.participant as string | undefined) ||
(pollVote.sender as string | undefined) ||
// Se houver candidatos de votante, assumir que estão ordenados por confiabilidade
(Array.isArray(voterCandidates) && voterCandidates.length > 0
? (voterCandidates[0] as string)
: undefined) ||
// Fallback final para a lista de votantes únicos
(Array.isArray(uniqueVoters) && uniqueVoters.length > 0
? (uniqueVoters[0] as string)
: undefined);
successfulVoterJid = definitiveVoterJid;
if (!successfulVoterJid) {
this.logger.warn(
`Unable to reliably determine voter JID for already decrypted vote; options=${selectedOptionNames.join(',')}`,
);
} else if (Array.isArray(voterCandidates) && voterCandidates.length > 1) {
this.logger.verbose(
`Multiple voterCandidates found for already decrypted vote; candidates=${voterCandidates.join(
',',
)}, chosen=${successfulVoterJid}`,
);
}
this.logger.verbose(
`Using already decrypted vote: voter=${successfulVoterJid ?? 'unknown'}, options=${selectedOptionNames.join(',')}`,
);
} else {- Ensure that
pollVote.participantand/orpollVote.senderare the correct fields exposed by the Baileys poll vote event in your codebase. If your structure differs, adjust thedefinitiveVoterJidderivation to use the appropriate metadata fields (e.g.pollVote.key.participantor similar). - Confirm that
voterCandidatesis in scope at this point and that its elements are ordered by reliability as assumed; if not, you may want to sort or filter it earlier in the code to enforce the intended reliability ordering. - If you use strict TypeScript settings, you may wish to refine the types for
pollVote,voterCandidates, anduniqueVotersinstead of relying onas string | undefinedcasts here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bom demais veiiiiiiiiiiii!!
Bora botar isso pra produção!!!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Booora! 😄
Tá ficando muito massa mesmo. Só reforçando um ponto rápido antes de ir pra produção: na parte em que usamos selectedOptions já descriptografado, hoje o voto é associado ao uniqueVoters[0]. Se em algum cenário vierem múltiplos candidatos de JID, pode ter um risco pequeno de atribuir o voto pro usuário errado.
Se vocês já validaram que na prática sempre vem um único candidato ou que a ordem dos candidatos é confiável, tranquilo seguir assim. Caso contrário, vale considerar depois aquela melhoria de escolher o JID mais “definitivo” a partir dos metadados da mensagem (tipo participant/sender) e só cair para a lista agregada como fallback.
Mas, no geral, a feature tá muito bem amarrada — logs bons, tratamento de erro decente e agregação correta dos votos. 👏
Se quiser, posso ajudar a desenhar uns testes específicos pra esse cenário de múltiplos JIDs antes/depois de subir em produção.
🗳️ Feature: Endpoint para Descriptografar e Visualizar Votos de Enquetes
📋 Resumo
Implementação de um novo endpoint
/chat/getPollVote/{instanceName}que permite descriptografar e visualizar os resultados de enquetes criadas no WhatsApp através do ID da mensagem de criação da enquete.🎯 Objetivo
Permitir que a Evolution API possa descriptografar e exibir os votos de enquetes do WhatsApp de forma resumida e fácil de visualizar, mostrando:
🔧 Mudanças Implementadas
Novos Arquivos
Arquivos Modificados
1.
src/api/dto/chat.dto.tsDecryptPollVoteDtocom estrutura para receber a chave da mensagem e remoteJid2.
src/validate/message.schema.tsdecryptPollVoteSchemapara validação JSONSchema7 dos dados de entrada3.
src/api/integrations/channel/whatsapp/whatsapp.baileys.service.tsbaileysDecryptPollVoteque:decryptPollVoteda Baileys4.
src/api/controllers/chat.controller.tsdecryptPollVoteque delega a chamada para o serviço Baileys5.
src/api/routes/chat.router.tsPOST /chat/getPollVote/:instanceNamecom validação e guards de autenticação📡 Endpoint
Rota
Headers
Request Body
{ "message": { "key": { "id": "3EB08E889E46BFC5F32313" } }, "remoteJid": "[email protected]" }Response (200 OK)
{ "poll": { "name": "Nome da Enquete", "totalVotes": 3, "results": { "Opção 1": { "votes": 1, "voters": ["[email protected]"] }, "Opção 2": { "votes": 2, "voters": [ "[email protected]", "[email protected]" ] }, "Opção 3": { "votes": 0, "voters": [] } } } }Exemplo com cURL
✨ Funcionalidades
1. Descriptografia de Votos
encPayload) e descriptografa usando a funçãodecryptPollVoteda BaileysselectedOptionsjá está presente)2. Tratamento de Chaves de Criptografia
{ type: 'Buffer', data: [...] })3. Agregação de Votos
4. Normalização de JIDs
instance.wuidclient.user.lidkey.participantkey.participantAltkey.remoteJidkey.remoteJidAltjidNormalizedUserpara normalizar os JIDs antes de comparar🐛 Tratamento de Erros
Erros Possíveis
Poll creation message not found (404)
ideremoteJidestão corretosPoll options not found (404)
Poll encryption key not found (404)
Failed to decrypt vote (Log Warning)
📝 Logs e Debug
O sistema possui logs detalhados para facilitar o debug:
Starting poll vote decryption processFound X pollUpdateMessage messages in databaseFiltered to X matching poll update messagesProcessing X poll update messages for decryptionVote already has selectedOptions, checking formatUsing already decrypted vote: voter=..., options=...Successfully decrypted vote for voter: ..., creator: ...Failed to decrypt vote. Last error: ...Para ver os logs, configure o nível de log como
verboseoudebug.🧪 Testes
Cenários de Teste Recomendados
Enquete com votos simples
Usuário muda de voto
Votos já descriptografados
Enquete sem votos
🔐 Segurança
📈 Performance
messageType)Setpara remoção eficiente de duplicatas🔄 Compatibilidade
decryptPollVote📚 Dependências
baileys:decryptPollVote: Descriptografa votos de enquetesjidNormalizedUser: Normaliza JIDs para comparação✅ Checklist
🚀 Próximos Passos (Opcional)
Possíveis melhorias futuras:
Cache de Resultados
Paginação
Estatísticas Adicionais
Webhook de Atualização
Tipo: Feature
Breaking Changes: Não
Requires Migration: Não
Summary by Sourcery
Add support for decrypting and aggregating WhatsApp poll votes and exposing the results via a new chat API endpoint.
New Features:
Enhancements: