Skip to content

Conversation

@oriondesign2015
Copy link
Contributor

@oriondesign2015 oriondesign2015 commented Dec 9, 2025

🗳️ 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:

  • Quantidade de votos por opção
  • Lista de quem votou em cada opção
  • Total de votos únicos (considerando apenas o voto mais recente de cada usuário)

🔧 Mudanças Implementadas

Novos Arquivos

  • Nenhum arquivo novo foi criado

Arquivos Modificados

1. src/api/dto/chat.dto.ts

  • ✅ Adicionado DTO DecryptPollVoteDto com estrutura para receber a chave da mensagem e remoteJid

2. src/validate/message.schema.ts

  • ✅ Adicionado schema decryptPollVoteSchema para validação JSONSchema7 dos dados de entrada

3. src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts

  • ✅ Implementado método baileysDecryptPollVote que:
    • Busca a mensagem de criação da enquete
    • Recupera a chave de criptografia
    • Busca todas as mensagens de atualização de votos no banco de dados
    • Filtra mensagens relacionadas à enquete específica
    • Descriptografa votos usando a função decryptPollVote da Baileys
    • Suporta votos já descriptografados
    • Agrupa votos por opção, considerando apenas o voto mais recente de cada usuário
    • Retorna payload resumido e fácil de visualizar

4. src/api/controllers/chat.controller.ts

  • ✅ Adicionado método decryptPollVote que delega a chamada para o serviço Baileys

5. src/api/routes/chat.router.ts

  • ✅ Adicionada rota POST /chat/getPollVote/:instanceName com validação e guards de autenticação

📡 Endpoint

Rota

POST /chat/getPollVote/{instanceName}

Headers

Content-Type: application/json
apikey: SUA_API_KEY

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

curl -X POST http://localhost:8080/chat/getPollVote/teste \
  -H "Content-Type: application/json" \
  -H "apikey: SUA_API_KEY" \
  -d '{
    "message": {
      "key": {
        "id": "3EB08E889E46BFC5F32313"
      }
    },
    "remoteJid": "[email protected]"
  }'

✨ Funcionalidades

1. Descriptografia de Votos

  • Suporta votos criptografados (encPayload) e descriptografa usando a função decryptPollVote da Baileys
  • Suporta votos já descriptografados (quando selectedOptions já está presente)
  • Tenta múltiplas combinações de JIDs para encontrar a correta

2. Tratamento de Chaves de Criptografia

  • Suporta múltiplos formatos de chave:
    • String base64
    • Buffer
    • Objeto Buffer serializado ({ type: 'Buffer', data: [...] })
    • Buffer de 44 bytes (formato específico do WhatsApp)

3. Agregação de Votos

  • Remove duplicatas considerando apenas o voto mais recente de cada usuário
  • Agrupa votos por opção
  • Calcula total de votos únicos

4. Normalização de JIDs

  • Tenta múltiplas variações de JID para encontrar a combinação correta:
    • instance.wuid
    • client.user.lid
    • key.participant
    • key.participantAlt
    • key.remoteJid
    • key.remoteJidAlt
  • Usa jidNormalizedUser para normalizar os JIDs antes de comparar

🐛 Tratamento de Erros

Erros Possíveis

  1. Poll creation message not found (404)

    • A mensagem de criação da enquete não foi encontrada
    • Verifique se o id e remoteJid estão corretos
  2. Poll options not found (404)

    • A enquete não possui opções definidas
    • Enquete pode estar corrompida
  3. Poll encryption key not found (404)

    • A chave de criptografia não foi encontrada
    • A mensagem pode não ter sido salva corretamente
  4. Failed to decrypt vote (Log Warning)

    • Não foi possível descriptografar um voto específico
    • O sistema tenta múltiplas combinações de JIDs
    • Se todas falharem, o voto é ignorado (não quebra a execução)

📝 Logs e Debug

O sistema possui logs detalhados para facilitar o debug:

  • Starting poll vote decryption process
  • Found X pollUpdateMessage messages in database
  • Filtered to X matching poll update messages
  • Processing X poll update messages for decryption
  • Vote already has selectedOptions, checking format
  • Using 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 verbose ou debug.

🧪 Testes

Cenários de Teste Recomendados

  1. Enquete com votos simples

    • Criar enquete com 3 opções
    • 3 usuários diferentes votam em opções diferentes
    • Verificar se todos os votos aparecem corretamente
  2. Usuário muda de voto

    • Usuário vota na "Opção 1"
    • Usuário muda para "Opção 2"
    • Verificar se apenas "Opção 2" aparece como voto deste usuário
  3. Votos já descriptografados

    • Criar enquete e receber votos
    • Chamar o endpoint
    • Verificar se usa os votos já descriptografados
  4. Enquete sem votos

    • Criar enquete
    • Não receber votos
    • Verificar se retorna estrutura vazia corretamente

🔐 Segurança

  • ✅ Validação de entrada via JSONSchema7
  • ✅ Autenticação via API key (guard)
  • ✅ Validação de instância (multi-tenant)
  • ✅ Tratamento seguro de erros (não expõe informações sensíveis)

📈 Performance

  • Busca otimizada no banco de dados (filtro por messageType)
  • Processamento em memória para agregação
  • Uso de Set para remoção eficiente de duplicatas
  • Cache implícito ao usar votos já descriptografados

🔄 Compatibilidade

  • ✅ Compatível com PostgreSQL e MySQL
  • ✅ Compatível com todas as versões do Baileys que suportam decryptPollVote
  • ✅ Não quebra funcionalidades existentes

📚 Dependências

  • baileys:
    • Função decryptPollVote: Descriptografa votos de enquetes
    • Função jidNormalizedUser: Normaliza JIDs para comparação

✅ Checklist

  • Código segue os padrões do projeto (ESLint, Prettier)
  • Validação de entrada implementada (JSONSchema7)
  • Tratamento de erros implementado
  • Logs detalhados para debug
  • Documentação do endpoint atualizada
  • Compatível com multi-tenant (validação de instância)
  • Suporte a votos já descriptografados
  • Suporte a votos criptografados
  • Remoção de duplicatas (voto mais recente por usuário)

🚀 Próximos Passos (Opcional)

Possíveis melhorias futuras:

  1. Cache de Resultados

    • Cachear resultados descriptografados para evitar reprocessamento
  2. Paginação

    • Para enquetes com muitos votos, implementar paginação
  3. Estatísticas Adicionais

    • Percentual de votos por opção
    • Gráficos de distribuição
    • Histórico de mudanças de voto
  4. Webhook de Atualização

    • Notificar quando novos votos são recebidos

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:

  • Expose a POST /chat/getPollVote/:instanceName endpoint to retrieve decrypted poll results for a given poll message.
  • Introduce DecryptPollVoteDto and JSON schema validation to accept a poll creation message key and remoteJid as input for vote decryption.
  • Implement a Baileys-based service method to decrypt poll votes, normalize voters, and return a summarized poll result payload.

Enhancements:

  • Aggregate poll votes by option while considering only each user’s most recent vote and include per-option vote counts and voter lists in the response.
  • Improve logging around poll vote processing and decryption to aid debugging.

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.
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Dec 9, 2025

Reviewer's Guide

Adds 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 votes

sequenceDiagram
  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
Loading

Class diagram for new poll vote decryption endpoint

classDiagram
  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)
  }
Loading

File-Level Changes

Change Details Files
Expose a new chat endpoint that accepts a poll creation message key and remoteJid, validates input, and delegates to a controller method to decrypt and summarize poll votes.
  • Introduce DecryptPollVoteDto to describe the request payload with message.key.id and remoteJid.
  • Add decryptPollVoteSchema JSON schema for validating the request body.
  • Wire POST /chat/getPollVote/:instanceName route with guards, schema validation, and controller execution.
  • Implement ChatController.decryptPollVote to map the DTO into a proto-like IMessageKey and call the Baileys instance service.
src/api/dto/chat.dto.ts
src/validate/message.schema.ts
src/api/routes/chat.router.ts
src/api/controllers/chat.controller.ts
Implement BaileysStartupService.baileysDecryptPollVote to locate poll-related messages, normalize encryption keys and JIDs, decrypt or interpret votes, deduplicate by user and timestamp, and compute aggregated poll results.
  • Fetch the poll creation message and extract poll options and encryption secret, with error handling for missing data.
  • Normalize different poll encryption key formats (base64 string, Buffer-like objects, 44-byte WhatsApp-specific buffers).
  • Query pollUpdateMessage records for the current instance and filter only those tied to the given pollCreationMessageKey using jidNormalizedUser.
  • Build creator and voter JID candidate lists, normalize them, and attempt decryptPollVote across creator/voter combinations when encPayload is present.
  • Support already decrypted votes via selectedOptions, including both string names and hashed buffers mapped back to option names using sha256.
  • Maintain a per-user map of the most recent vote (by messageTimestamp) and then aggregate counts and voter lists per option, initializing all options with zero votes.
  • Return a structured payload with poll name, total unique voters, and per-option votes/voters, while logging verbose progress and warnings and wrapping failures in an InternalServerErrorException.
src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a 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 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.
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>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +5164 to +5173
const allPollUpdateMessages = await this.prismaRepository.message.findMany({
where: {
instanceId: this.instanceId,
messageType: 'pollUpdateMessage',
},
select: {
id: true,
key: true,
message: true,
messageTimestamp: true,
Copy link
Contributor

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:

  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.

if (selectedOptions.length > 0 && typeof selectedOptions[0] === 'string') {
// Já está descriptografado como nomes de opções
selectedOptionNames = selectedOptions;
successfulVoterJid = uniqueVoters[0];
Copy link
Contributor

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 {
  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.

Copy link

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!!!

Copy link
Contributor

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants