Skip to content

feat(baileys): add sendPollVote and sendEventResponse endpoints - #2777

Open
timohissink wants to merge 2 commits into
evolution-foundation:developfrom
timohissink:feat/poll-vote-event-rsvp
Open

timohissink wants to merge 2 commits into
evolution-foundation:developfrom
timohissink:feat/poll-vote-event-rsvp

Conversation

@timohissink

@timohissink timohissink commented Nov 4, 2026 •

Copy link
Copy Markdown

📋 Description

This PR adds two Baileys endpoints that send real, encrypted responses to existing messages:

  • POST /message/sendPollVote/{instance} casts a vote on a poll (pollUpdateMessage). It is not an emoji reaction. WhatsApp clients count it in the poll results.
  • POST /message/sendEventResponse/{instance} sends an RSVP to an event message (encEventResponseMessage: going / maybe / not_going, plus an optional extraGuestCount).

Evolution can already create polls (sendPoll) and decrypt received votes (getPollVote, #2297), but it had no way to cast a vote or answer an event.

POST /message/sendPollVote/{instance}
{ "key": { "remoteJid": "[email protected]", "id": "3EB0...", "fromMe": false }, "options": ["Option A"], "jidMode": "auto" }

POST /message/sendEventResponse/{instance}
{ "key": { "remoteJid": "[email protected]", "id": "3EB0..." }, "response": "going", "extraGuestCount": 0 }

Implementation (follows the existing sendPoll / sendReaction pattern; 7 files, +325/−1):

File Change
src/api/dto/sendMessage.dto.ts SendPollVoteDto, SendEventResponseDto
src/validate/message.schema.ts pollVoteMessageSchema, eventResponseMessageSchema (JSONSchema7; max 12 options; jidMode enum)
src/api/controllers/sendMessage.controller.ts sendPollVote, sendEventResponse → waMonitor delegation
src/api/routes/sendMessage.router.ts POST sendPollVote, POST sendEventResponse
src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts pollVoteMessage, eventResponseMessage, plus private helpers getMessageSecretContext / encryptMessageSecretPayload
src/api/integrations/channel/meta/whatsapp.business.service.ts stubs throwing BadRequestException('Método não disponível na WhatsApp Business API')
src/api/integrations/channel/evolution/evolution.channel.service.ts stubs throwing BadRequestException('Método não disponível no Evolution Channel')

How it works

  • The creation message is looked up in Message, scoped by instanceId. The request's remoteJid must match the stored remoteJid or remoteJidAlt. The message type must be pollCreationMessage(V2/V3) or eventMessage. A 32-byte messageSecret must be present. Each failure returns a clear 400 or 404.
  • Poll options are validated against the stored options, and against selectableOptionsCount. Unknown options return the list of available ones.
  • Encryption mirrors Baileys decryptPollVote and whatsmeow encryptMsgSecret: HMAC-SHA256 key derivation over msgId + creatorJid + voterJid + "Poll Vote" | "Event Response", then AES-256-GCM with AAD msgId\0voterJid. It uses only Node crypto, with no new dependency.
  • jidMode (auto | lid | pn, default auto): in auto mode the LID domain is used when the chat or author uses LID addressing, and the own LID is used for fromMe messages, including the self-chat. lid and pn force one domain for edge cases.
  • In LID-addressed chats (including the self-chat) the pollCreationMessageKey.remoteJid and the relay target are the LID chat JID, matching what the primary device sends.
  • The vote is relayed with type="poll" and <meta polltype="vote" content_type="add_on">, the same markers a phone puts on its own votes (see Baileys PR #2870: without them the server acks but does not deliver the vote). The response returns voteMessageId and the JIDs used (crypto.creator, crypto.voter, crypto.keyRemoteJid).

No database migration (PostgreSQL and MySQL are unaffected). No new environment variables. No breaking change.

🔗 Related Issue

Closes #2776

🧪 Type of Change

  • 🐛 Bug fix (non-breaking change which fixes an issue)
  • ✨ New feature (non-breaking change which adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • 📚 Documentation update
  • 🔧 Refactoring (no functional changes)
  • ⚡ Performance improvement
  • 🧹 Code cleanup
  • 🔒 Security fix

🧪 Testing

✅ Verified on a device (2026-11-04). A vote sent through this endpoint (Evolution 2.3.7 + this patch + the Baileys decrypt-fail="hide" tweak below) is shown on the primary phone in a self-chat test poll. Earlier attempts without <meta polltype="vote" content_type="add_on"> / decrypt-fail="hide" were relayed but not shown. Other scenarios (listed below) have not been verified on a device yet.

Verified so far (Evolution 2.3.7 + this patch, self-chat test poll):

  • Votes cast on the phone for the same poll decrypt with Baileys decryptPollVote using creator = voter = own LID; the endpoint derives the same JIDs (auto).

  • Field-by-field match with the phone's own votes: chat JID @lid, pollCreationMessageKey { id, fromMe: true, remoteJid: <own LID> } without participant, metadata: {}, senderTimestampMs in ms, 12-byte encIv, no messageContextInfo on the vote.

  • Round trip: a payload built with the same derivation decrypts to the selected option hash with Baileys decryptPollVote.

  • Baileys debug log: the stanza is relayed with type="poll" + <meta polltype="vote"> to the primary device (sending message to 1 devices); no retry receipt.

  • Error paths: unknown option → 400 with available list; unknown message id → 404; nothing is sent in those cases.

  • Baileys does not set decrypt-fail="hide" on the enc node for poll updates/event responses and this is not configurable via relayMessage (only pinInChatMessage gets it). The test deployment adds it with a one-line patch to messages-send.js (as proposed upstream in Baileys PR #2870); this PR does not patch Baileys. Maintainers may prefer a patches/ entry or waiting for the upstream fix.

  • Manual testing completed (self-chat poll vote on a real device; other scenarios pending)

  • Functionality verified in development environment

  • No breaking changes introduced

  • Tested with different connection types (if applicable): Baileys only. Business API and Evolution return "Método não disponível".

Checks run locally on this branch (rebased on current develop):

  • npm ci ✅ · npm run db:generate ✅ · npm run lint:check ✅ (0 warnings) · prettier --check on changed files ✅ · tsc --noEmit ✅ · npm run build ✅ · commitlint ✅

Planned scenarios: group poll from another user (PN and LID), 1:1 poll, own poll in the self-chat, multi-select poll, unknown option, event Going / Maybe / Not going with guests, and a message without messageSecret.

📸 Screenshots (if applicable)

To be added after on-device verification.

✅ Checklist

  • My code follows the project's style guidelines
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have manually tested my changes thoroughly
  • I have verified the changes work with different scenarios
  • Any dependent changes have been merged and published

📝 Additional Notes

  • Documentation: the API reference lives at docs.evolutionfoundation.com.br (OpenAPI Evolution-API/message.yaml), outside this repo. I'm happy to send the two endpoints there if you point me to the docs repo.
  • Open to moving sendPollVote under /chat, next to getPollVote, if you prefer.
  • This PR was developed with AI assistance (Cursor). I reviewed, tested and take responsibility for the code.

Made-with: Cursor

Summary by Sourcery

Enable sending encrypted poll votes and event RSVPs through the messaging API.

New Features:

  • Add Baileys-backed endpoints for casting poll votes and responding to event messages with encrypted WhatsApp payloads.

Enhancements:

  • Validate referenced poll and event messages, response options, addressing modes, and message secrets before sending.
  • Provide explicit unsupported-operation responses for WhatsApp Business API and Evolution channels.

Allow voting on existing polls and responding to event invitations by encrypting the
vote/response with the stored messageSecret (inverse of Baileys decryptPollVote and
decryptEventResponse). Uses the LID domain for creator/voter when the chat uses LID
addressing or the message was sent by this account; jidMode can override (auto|lid|pn).
Unsupported on the Business API and Evolution channels.
@sourcery-ai

sourcery-ai Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Adds validated POST endpoints for casting encrypted poll votes and submitting encrypted event RSVPs, with Baileys-specific message-secret encryption, PN/LID JID resolution, protocol-compatible relay metadata, and explicit unsupported responses on other channel types.

Sequence diagram for encrypted poll vote submission

sequenceDiagram
    actor Client
    participant Router
    participant Controller
    participant Baileys
    participant Database
    participant WhatsApp

    Client->>Router: POST sendPollVote
    Router->>Router: pollVoteMessageSchema validation
    Router->>Controller: sendPollVote
    Controller->>Baileys: pollVoteMessage(data)
    Baileys->>Database: findFirst message by instanceId and key.id
    Database-->>Baileys: poll creation and messageSecret
    Baileys->>Baileys: getMessageSecretContext
    Baileys->>Baileys: encryptMessageSecretPayload
    Baileys->>WhatsApp: relayMessage with poll metadata
    WhatsApp-->>Baileys: voteMessageId
    Baileys-->>Controller: SENT response
    Controller-->>Client: 201 voteMessageId and crypto JIDs
Loading

Sequence diagram for encrypted event RSVP submission

sequenceDiagram
    actor Client
    participant Router
    participant Controller
    participant Baileys
    participant Database
    participant WhatsApp

    Client->>Router: POST sendEventResponse
    Router->>Router: eventResponseMessageSchema validation
    Router->>Controller: sendEventResponse
    Controller->>Baileys: eventResponseMessage(data)
    Baileys->>Database: findFirst event by instanceId and key.id
    Database-->>Baileys: event message and messageSecret
    Baileys->>Baileys: getMessageSecretContext
    Baileys->>Baileys: encryptMessageSecretPayload
    Baileys->>WhatsApp: relayMessage with event metadata
    WhatsApp-->>Baileys: responseMessageId
    Baileys-->>Controller: SENT response
    Controller-->>Client: 201 responseMessageId and crypto JIDs
Loading

File-Level Changes

Change Details Files
Expose new poll-vote and event-response HTTP endpoints with DTO validation and controller delegation.
  • Add request DTOs for poll votes and event RSVPs, including JID mode and guest count.
  • Add JSON Schema validation for message keys, poll options, response values, JID modes, and limits.
  • Register POST routes and delegate requests through the message controller to the active channel service.
src/api/dto/sendMessage.dto.ts
src/validate/message.schema.ts
src/api/controllers/sendMessage.controller.ts
src/api/routes/sendMessage.router.ts
Implement encrypted Baileys responses by resolving the original message context, deriving WhatsApp message-secret keys, and relaying protocol-compatible payloads.
  • Load and instance-scope the target message, validate JID and message type, require a 32-byte message secret, and resolve PN/LID identities.
  • Derive per-use HMAC keys and encrypt poll votes or event responses with AES-256-GCM and the required AAD.
  • Validate poll selections against stored options and selectable limits; encode event RSVP and guest-count data.
  • Relay poll votes and event responses with WhatsApp-compatible keys, attributes, metadata, and return message/crypto details.
src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts
Provide explicit unsupported-operation behavior for non-Baileys channel implementations.
  • Add methods that return BadRequestException responses for Evolution Channel.
  • Add methods that return BadRequestException responses for WhatsApp Business API.
src/api/integrations/channel/evolution/evolution.channel.service.ts
src/api/integrations/channel/meta/whatsapp.business.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

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts" line_range="3983" />
<code_context>
+    });
+
+    if (!stored) {
+      throw new NotFoundException('Message not found');
+    }
+
</code_context>
<issue_to_address>
**New errors use English**

When a request reaches one of the new error paths, the new exceptions and validation errors return English messages instead of Portuguese, so callers receive errors that do not follow the repository's user-facing language requirement.

Return Portuguese messages for the newly added user-facing errors.

Also at `src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts:3994`, `src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts:3999`, `src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts:4012`, `src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts:4091`, `src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts:4099`, `src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts:4142`, `src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts:4193`, `src/api/integrations/channel/meta/whatsapp.business.service.ts:1764`, `src/api/integrations/channel/meta/whatsapp.business.service.ts:1767`, `src/api/integrations/channel/evolution/evolution.channel.service.ts:785`, `src/api/integrations/channel/evolution/evolution.channel.service.ts:788`.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. If the message-secret key or JID derivation is wrong, the endpoint could relay a poll vote or event response that does not match the caller’s intended action. That response is sent outside the service and may remain visible after a revert; reverting only removes the endpoints.


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

This branch has not been deployed

No deployments
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