Skip to content

Conversation

@oriondesign2015
Copy link
Contributor

@oriondesign2015 oriondesign2015 commented Dec 12, 2025

Fix: Validação e Truncamento de Strings para Evitar Erros de Tamanho de Coluna

📋 Resumo

Este PR adiciona validação e truncamento de strings antes de salvar dados no banco de dados, prevenindo erros relacionados ao tamanho máximo de colunas do Prisma. A mudança garante que valores de string sejam automaticamente truncados para respeitar os limites definidos no schema do banco de dados.

🎯 Tipo de Mudança

  • 🐛 Bug fix (correção que não quebra funcionalidade existente)
  • ✨ Nova feature
  • 💥 Breaking change
  • 📚 Documentação

🔍 Arquivos Modificados

1. src/api/services/channel.service.ts

  • Adicionada função helper truncate() para truncar strings
  • Aplicado truncamento nos campos msgCall e wavoipToken (limite de 100 caracteres)

2. src/api/services/monitor.service.ts

  • Adicionada função helper truncate() para truncar strings
  • Aplicado truncamento em múltiplos campos:
    • instanceName: 255 caracteres (com validação de campo obrigatório)
    • ownerJid: 100 caracteres
    • profileName: 100 caracteres
    • profilePicUrl: 500 caracteres
    • number: 100 caracteres
    • integration: 100 caracteres
    • token: 255 caracteres
    • clientName: 100 caracteres
    • businessId: 100 caracteres
  • Adicionada validação para garantir que instanceName não seja vazio
  • Corrigido uso de await desnecessário em get<Database>()
  • Melhorado tratamento de erros propagando exceções

🔧 Detalhes Técnicos

Função Helper truncate()

const truncate = (str: string | null | undefined, maxLength: number): string | null => {
  if (!str) return null;
  return str.length > maxLength ? str.substring(0, maxLength) : str;
};

Esta função:

  • Retorna null se o valor for null ou undefined
  • Trunca strings que excedem o maxLength especificado
  • Retorna a string original se estiver dentro do limite

Validação de instanceName

const instanceName = truncate(data.instanceName, 255);
if (!instanceName || instanceName.trim().length === 0) {
  throw new Error('instanceName is required and cannot be empty');
}

Adicionada validação para garantir que o campo obrigatório instanceName não seja vazio após o truncamento.

✅ Benefícios

  1. Prevenção de Erros: Evita erros de violação de tamanho de coluna no banco de dados
  2. Robustez: Garante que dados longos sejam automaticamente ajustados
  3. Validação: Adiciona validação para campos obrigatórios
  4. Manutenibilidade: Código mais seguro e previsível

🧪 Testes

Cenários Testados

  • Strings dentro do limite são preservadas
  • Strings acima do limite são truncadas
  • Valores null e undefined são tratados corretamente
  • instanceName vazio lança exceção apropriada
  • Operações de criação e atualização funcionam corretamente

📝 Notas Adicionais

  • Os limites de caracteres foram definidos com base nas restrições do schema Prisma
  • A função truncate() é definida localmente em cada método para manter o escopo limitado
  • O tratamento de erros foi melhorado para propagar exceções adequadamente

✅ Checklist de Revisão

  • Código segue os padrões do projeto
  • Mudanças foram testadas localmente e en produção
  • Não há breaking changes
  • Documentação atualizada (se necessário)
  • Commits seguem o padrão Conventional Commits

Summary by Sourcery

Add support for decrypting and aggregating WhatsApp poll votes while enforcing string length limits to prevent database column size errors.

New Features:

  • Expose a new chat API endpoint and DTO to request decryption and aggregation of poll votes for a given message.
  • Introduce a Baileys service method to decrypt poll votes, correlate them with their poll, and return aggregated results by option.

Bug Fixes:

  • Apply centralized string truncation and validation when saving instance and settings data to avoid Prisma/database column size violations.
  • Ensure instance creation fails fast by validating that instanceName is non-empty and propagating errors instead of silently logging them.

Enhancements:

  • Simplify configuration access by removing unnecessary async usage when reading database clientName.
  • Add schema validation for decrypt poll vote requests to ensure required fields are present before processing.

oriondesign2015 and others added 6 commits December 9, 2025 16:56
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 12, 2025

Reviewer's Guide

Implements poll vote decryption/exposure via a new chat route and Baileys service method, adds JSON schema/DTO/controller wiring, and introduces defensive string truncation and required-field validation before persisting instances/settings to Prisma to avoid column-length errors.

Sequence diagram for the new decrypt poll vote chat route

sequenceDiagram
  actor Client
  participant ChatRouter
  participant ChatController
  participant WAMonitoringService
  participant BaileysStartupService
  participant PrismaRepository

  Client->>ChatRouter: POST /chat/getPollVote
  ChatRouter->>ChatRouter: dataValidate with decryptPollVoteSchema
  ChatRouter->>ChatController: decryptPollVote(instanceDto, decryptPollVoteDto)
  ChatController->>WAMonitoringService: waInstances[instanceName]
  WAMonitoringService-->>ChatController: BaileysStartupService instance
  ChatController->>BaileysStartupService: baileysDecryptPollVote(pollCreationMessageKey)

  BaileysStartupService->>BaileysStartupService: getMessage(pollCreationMessageKey, true)
  BaileysStartupService-->>BaileysStartupService: pollCreationMessage
  BaileysStartupService->>BaileysStartupService: getMessage(pollCreationMessageKey)
  BaileysStartupService-->>BaileysStartupService: pollMessageSecret and pollEncKey

  BaileysStartupService->>PrismaRepository: message.findMany(filter pollUpdateMessage)
  PrismaRepository-->>BaileysStartupService: pollUpdateMessages

  loop For each pollUpdateMessage
    BaileysStartupService->>BaileysStartupService: derive creatorCandidates and voterCandidates
    alt selectedOptions already present
      BaileysStartupService->>BaileysStartupService: map to option names
    else encPayload present
      BaileysStartupService->>BaileysStartupService: decryptPollVote with creator and voter combinations
      BaileysStartupService->>BaileysStartupService: map hashes to option names
    end
    BaileysStartupService->>BaileysStartupService: keep most recent vote per voter
  end

  BaileysStartupService->>BaileysStartupService: aggregate results per option
  BaileysStartupService-->>ChatController: poll summary (name, totalVotes, results)
  ChatController-->>ChatRouter: poll summary
  ChatRouter-->>Client: 200 OK with poll summary

  rect rgb(255,230,230)
    ChatRouter->>ChatRouter: on validation or internal error
    ChatRouter-->>Client: error response
  end
Loading

Updated class diagram for chat, monitoring and Baileys services

classDiagram
  class ChatRouter {
    -chatController ChatController
    -waMonitor WAMonitoringService
    +registerRoutes()
  }

  class ChatController {
    -waMonitor WAMonitoringService
    +decryptPollVote(instanceDto, decryptPollVoteDto) Promise
    +blockUser(instanceDto, blockUserDto) Promise
  }

  class DecryptPollVoteDto {
    +message object
    +remoteJid string
  }

  class WAMonitoringService {
    +waInstances Record
    +saveInstance(data) Promise
    +loadInstancesFromDatabasePostgres() Promise
  }

  class ChannelStartupService {
    -prismaRepository PrismaRepository
    -instanceId string
    +setSettings(settingsDto) Promise
  }

  class BaileysStartupService {
    -logger Logger
    -instance WABaileysInstance
    -client BaileysClient
    -instanceId string
    -prismaRepository PrismaRepository
    +baileysDecryptPollVote(pollCreationMessageKey) Promise
  }

  class PrismaRepository {
    +instance InstanceModel
    +message MessageModel
  }

  class InstanceModel {
    +create(data) Promise
    +findMany(filter) Promise
  }

  class MessageModel {
    +findMany(filter) Promise
  }

  class SettingsDto {
    +rejectCall boolean
    +msgCall string
    +groupsIgnore boolean
    +alwaysOnline boolean
    +readMessages boolean
    +readStatus boolean
    +syncFullHistory boolean
    +wavoipToken string
  }

  ChatRouter --> ChatController
  ChatRouter --> WAMonitoringService
  ChatController --> WAMonitoringService
  WAMonitoringService --> BaileysStartupService
  ChannelStartupService <|-- BaileysStartupService
  ChannelStartupService --> PrismaRepository
  BaileysStartupService --> PrismaRepository
  DecryptPollVoteDto <.. ChatController
  DecryptPollVoteDto <.. ChatRouter
  SettingsDto <.. ChannelStartupService
Loading

Flow diagram for truncation and validation when saving an instance and settings

flowchart TD
  subgraph SaveInstanceFlow
    A_start[Receive instance data] --> B_truncateName[Truncate instanceName to 255]
    B_truncateName --> C_checkEmpty{instanceName is null or empty after trim}
    C_checkEmpty -- Yes --> D_throwError[Throw instanceName required error]
    C_checkEmpty -- No --> E_truncateOwnerJid[Truncate ownerJid to 100]
    E_truncateOwnerJid --> F_truncateProfileName[Truncate profileName to 100]
    F_truncateProfileName --> G_truncateProfilePicUrl[Truncate profilePicUrl to 500]
    G_truncateProfilePicUrl --> H_truncateNumber[Truncate number to 100]
    H_truncateNumber --> I_truncateIntegration[Truncate integration to 100]
    I_truncateIntegration --> J_truncateToken[Truncate hash to token 255]
    J_truncateToken --> K_truncateClientName[Truncate clientName to 100]
    K_truncateClientName --> L_truncateBusinessId[Truncate businessId to 100]
    L_truncateBusinessId --> M_persistInstance[Create instance via Prisma]
    M_persistInstance --> N_success[Instance saved]
    M_persistInstance --> O_error[On error propagate exception]
  end

  subgraph SetSettingsFlow
    P_start[Receive settings data] --> Q_truncateMsgCall[Truncate msgCall to 100]
    Q_truncateMsgCall --> R_truncateWavoipToken[Truncate wavoipToken to 100]
    R_truncateWavoipToken --> S_upsertSettings[Upsert settings via Prisma]
    S_upsertSettings --> T_done[Settings saved]
  end
Loading

File-Level Changes

Change Details Files
Add end-to-end support for decrypting poll votes via a new chat API endpoint wired into the Baileys WhatsApp service.
  • Implement baileysDecryptPollVote to resolve poll metadata, normalize encryption key formats, gather related pollUpdateMessage records, decrypt votes using multiple creator/voter JID combinations, and aggregate the latest vote per user into per-option tallies.
  • Introduce decryptPollVoteSchema JSON schema, a DecryptPollVoteDto, and a ChatController.decryptPollVote method that builds a pollCreationMessageKey and delegates to the Baileys service.
  • Register a new POST /getPollVote route that validates input with decryptPollVoteSchema and returns the decrypted poll result payload.
src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts
src/validate/message.schema.ts
src/api/routes/chat.router.ts
src/api/dto/chat.dto.ts
src/api/controllers/chat.controller.ts
Add local string truncation helpers and required-field validation to prevent Prisma column length violations when saving instances and settings.
  • In WAMonitoringService.saveInstance, replace an unnecessary awaited config read, introduce a local truncate helper, enforce non-empty instanceName after truncation, and truncate ownerJid, profileName, profilePicUrl, number, integration, token, clientName, and businessId to column-safe lengths before create.
  • Ensure loadInstancesFromDatabasePostgres uses a non-await config read for clientName while preserving behavior.
  • In ChannelStartupService.setSettings, add a local truncate helper and truncate msgCall and wavoipToken to 100 characters for both update and create paths.
src/api/services/monitor.service.ts
src/api/services/channel.service.ts

Possibly linked issues


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 - here's some feedback:

  • The truncate helper is duplicated in multiple services; consider extracting it to a shared utility (with centralized max-length constants) so behavior and limits stay consistent across the codebase.
  • In saveInstance, truncating fields like integration and clientName (which look like identifiers/enums) may silently change their values; it might be safer to validate and reject overlong values or adjust the schema rather than truncating these particular fields.
  • The baileysDecryptPollVote method currently loads all pollUpdateMessage records for an instance and filters them in memory, which may not scale well; consider narrowing the Prisma query by pollCreationMessageKey (or other indexed criteria) to avoid scanning the entire table.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `truncate` helper is duplicated in multiple services; consider extracting it to a shared utility (with centralized max-length constants) so behavior and limits stay consistent across the codebase.
- In `saveInstance`, truncating fields like `integration` and `clientName` (which look like identifiers/enums) may silently change their values; it might be safer to validate and reject overlong values or adjust the schema rather than truncating these particular fields.
- The `baileysDecryptPollVote` method currently loads all `pollUpdateMessage` records for an instance and filters them in memory, which may not scale well; consider narrowing the Prisma query by pollCreationMessageKey (or other indexed criteria) to avoid scanning the entire table.

## Individual Comments

### Comment 1
<location> `src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts:5217-5226` </location>
<code_context>
+        const voterCandidates = [
</code_context>

<issue_to_address>
**issue (bug_risk):** Avoid defaulting the voter JID to the first candidate when using pre-decrypted `selectedOptions`, to prevent misattributing votes.

When `pollVote.selectedOptions` is already an array of strings, `successfulVoterJid` is taken from `uniqueVoters[0]`, which may be `this.instance.wuid` or `this.client.user?.lid` rather than the actual voting participant. This can misattribute votes to the wrong JID. Instead, resolve the voter from the message’s sender-identifying fields (e.g. `key.participant` / `key.remoteJid`) or another explicit voter field, rather than defaulting to the first candidate.
</issue_to_address>

### Comment 2
<location> `src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts:5164` </location>
<code_context>
+      }
+
+      // Buscar todas as mensagens de atualização de votos
+      const allPollUpdateMessages = await this.prismaRepository.message.findMany({
+        where: {
+          instanceId: this.instanceId,
</code_context>

<issue_to_address>
**suggestion (performance):** Reduce the volume of messages fetched from the database when resolving poll updates to improve scalability.

This currently fetches all `pollUpdateMessage` records for the instance and only then filters them in memory by `pollCreationMessageKey`, which can be very expensive on busy instances. If possible, push this filtering into the query (e.g., a where-clause on `pollCreationMessageKey.id` via JSON path, or constraints on `messageTimestamp`) so the database limits the result set before it reaches the application.

Suggested implementation:

```typescript
      // Buscar mensagens de atualização de votos apenas relacionadas a esta enquete,
      // filtrando no banco para reduzir o volume de dados trafegados e processados em memória
      const allPollUpdateMessages = await this.prismaRepository.message.findMany({
        where: {
          instanceId: this.instanceId,
          messageType: 'pollUpdateMessage',
          // Quando disponível, restringe a busca às mensagens que apontam para a mesma pollCreationMessageKey
          ...(pollCreationMessageKey?.id && {
            message: {
              path: ['pollCreationMessageKey', 'id'],
              equals: pollCreationMessageKey.id,
            },
          }),
        },
        select: {
          id: true,
          key: true,
          message: true,
          messageTimestamp: true,
        },
      });

```

To make this work correctly, you need to ensure:

1. The `message` column in your Prisma schema is a `Json` field (or otherwise supports JSON path filters) and that Prisma is configured for JSON path querying (e.g., PostgreSQL with `Json`/`Jsonb`).
2. `pollCreationMessageKey` (with an `.id` property) is available in this scope and represents the creation message key for the poll you are resolving.
3. If your JSON structure or Prisma JSON filtering API differs (e.g., you use a different key name or nested structure), adjust `path: ['pollCreationMessageKey', 'id']` accordingly to match your actual schema.
4. If you also have reliable temporal information (e.g., the creation message timestamp), you can further constrain the query with a `messageTimestamp` range to reduce results even more, for example:
   ```ts
   messageTimestamp: {
     gte: pollCreationMessageTimestamp,
   }
   ```
   Add this inside the `where` object if such a variable exists and is appropriate.
</issue_to_address>

### Comment 3
<location> `src/api/services/monitor.service.ts:273-275` </location>
<code_context>
+          results,
+        },
+      };
+    } catch (error) {
+      this.logger.error(`Error decrypting poll votes: ${error}`);
+      throw new InternalServerErrorException('Error decrypting poll votes', error.toString());
</code_context>

<issue_to_address>
**🚨 suggestion (security):** Re-throwing the raw error may expose internal details; consider wrapping or normalizing before propagating.

Here, `error.toString()` is passed into `InternalServerErrorException`, which may still expose low-level details (e.g., driver/infra info) if this message reaches clients. Consider omitting the raw error from the thrown exception and relying on logging for diagnostics, while returning only a generic, domain-appropriate message to callers.
</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.

}

// Buscar todas as mensagens de atualização de votos
const allPollUpdateMessages = await this.prismaRepository.message.findMany({
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): Reduce the volume of messages fetched from the database when resolving poll updates to improve scalability.

This currently fetches all pollUpdateMessage records for the instance and only then filters them in memory by pollCreationMessageKey, which can be very expensive on busy instances. If possible, push this filtering into the query (e.g., a where-clause on pollCreationMessageKey.id via JSON path, or constraints on messageTimestamp) so the database limits the result set before it reaches the application.

Suggested implementation:

      // Buscar mensagens de atualização de votos apenas relacionadas a esta enquete,
      // filtrando no banco para reduzir o volume de dados trafegados e processados em memória
      const allPollUpdateMessages = await this.prismaRepository.message.findMany({
        where: {
          instanceId: this.instanceId,
          messageType: 'pollUpdateMessage',
          // Quando disponível, restringe a busca às mensagens que apontam para a mesma pollCreationMessageKey
          ...(pollCreationMessageKey?.id && {
            message: {
              path: ['pollCreationMessageKey', 'id'],
              equals: pollCreationMessageKey.id,
            },
          }),
        },
        select: {
          id: true,
          key: true,
          message: true,
          messageTimestamp: true,
        },
      });

To make this work correctly, you need to ensure:

  1. The message column in your Prisma schema is a Json field (or otherwise supports JSON path filters) and that Prisma is configured for JSON path querying (e.g., PostgreSQL with Json/Jsonb).
  2. pollCreationMessageKey (with an .id property) is available in this scope and represents the creation message key for the poll you are resolving.
  3. If your JSON structure or Prisma JSON filtering API differs (e.g., you use a different key name or nested structure), adjust path: ['pollCreationMessageKey', 'id'] accordingly to match your actual schema.
  4. If you also have reliable temporal information (e.g., the creation message timestamp), you can further constrain the query with a messageTimestamp range to reduce results even more, for example:
    messageTimestamp: {
      gte: pollCreationMessageTimestamp,
    }
    Add this inside the where object if such a variable exists and is appropriate.

Comment on lines 273 to +275
} catch (error) {
this.logger.error(error);
throw error; // Propagate error to prevent creating settings if instance creation fails
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 suggestion (security): Re-throwing the raw error may expose internal details; consider wrapping or normalizing before propagating.

Here, error.toString() is passed into InternalServerErrorException, which may still expose low-level details (e.g., driver/infra info) if this message reaches clients. Consider omitting the raw error from the thrown exception and relying on logging for diagnostics, while returning only a generic, domain-appropriate message to callers.

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.

1 participant