diff --git a/CHANGELOG.md b/CHANGELOG.md index b94884a4b..d1124f3e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- **Native WhatsApp polls** via `POST /api/sessions/:sessionId/messages/send-poll`: question, 2–12 options and an optional `allowMultipleAnswers` flag (default single choice), implemented on both engines (whatsapp-web.js `Poll`, Baileys `poll` content with `selectableCount` 1/0). The message history stores the poll question as the body so the log stays readable. +- **Native WhatsApp polls** via `POST /api/sessions/:sessionId/messages/send-poll`: question, 2–12 options and an optional `allowMultipleAnswers` flag (default single choice), implemented on both engines (whatsapp-web.js `Poll`, Baileys `poll` content with `selectableCount` 1/0). The message history stores the poll question as the body so the log stays readable. Polls are a first-class `poll` message type end to end — both engines map incoming poll messages to it, so the websocket/webhook events, persisted rows, and dashboard all report `poll` consistently. Thanks @alejo117. ## [0.8.7] - 2026-07-03 diff --git a/dashboard/src/components/DashboardCharts.tsx b/dashboard/src/components/DashboardCharts.tsx index 2aac7462e..9a2568934 100644 --- a/dashboard/src/components/DashboardCharts.tsx +++ b/dashboard/src/components/DashboardCharts.tsx @@ -35,6 +35,7 @@ const TYPE_COLORS: Record = { video: '#14b8a6', sticker: '#ef4444', location: '#84cc16', + poll: '#6366f1', revoked: '#f43f5e', masked: '#8b5cf6', unknown: '#64748b', diff --git a/dashboard/src/services/api.ts b/dashboard/src/services/api.ts index c9b8f13b7..1d66cce7b 100644 --- a/dashboard/src/services/api.ts +++ b/dashboard/src/services/api.ts @@ -140,6 +140,7 @@ export const MESSAGE_TYPES = [ 'sticker', 'location', 'contact', + 'poll', 'call', 'revoked', 'masked', diff --git a/src/engine/adapters/baileys-message-mapper.spec.ts b/src/engine/adapters/baileys-message-mapper.spec.ts index 6374fbab9..b9382230a 100644 --- a/src/engine/adapters/baileys-message-mapper.spec.ts +++ b/src/engine/adapters/baileys-message-mapper.spec.ts @@ -29,7 +29,10 @@ describe('mapBaileysMessageType (baileys content-type -> neutral MessageType)', // its own `masked` type so it is distinguishable from a genuinely unparseable message (#574). ['placeholderMessage', false, 'masked'], [undefined, false, 'unknown'], - ['pollCreationMessage', false, 'unknown'], + // Native polls surface as their own `poll` type (WhatsApp bumps the content key across versions). + ['pollCreationMessage', false, 'poll'], + ['pollCreationMessageV2', false, 'poll'], + ['pollCreationMessageV3', false, 'poll'], // Regression trap: calls arrive via the `call` socket event, never as a message content type, // so any call-ish token must stay 'unknown' (no accidental mapping). ['callLogMessage', false, 'unknown'], diff --git a/src/engine/adapters/baileys-message-mapper.ts b/src/engine/adapters/baileys-message-mapper.ts index e0857b232..ba786193e 100644 --- a/src/engine/adapters/baileys-message-mapper.ts +++ b/src/engine/adapters/baileys-message-mapper.ts @@ -32,6 +32,11 @@ export function mapBaileysMessageType(contentType: string | undefined, isPtt = f case 'contactMessage': case 'contactsArrayMessage': return 'contact'; + case 'pollCreationMessage': + case 'pollCreationMessageV2': + case 'pollCreationMessageV3': + // Native polls; WhatsApp bumps the content key across versions, all map to the same neutral type. + return 'poll'; case 'interactiveMessage': case 'buttonsMessage': case 'templateMessage': diff --git a/src/engine/adapters/message-mapper.spec.ts b/src/engine/adapters/message-mapper.spec.ts index 9439c4bd7..9ab634ca7 100644 --- a/src/engine/adapters/message-mapper.spec.ts +++ b/src/engine/adapters/message-mapper.spec.ts @@ -124,6 +124,7 @@ describe('mapWwebjsMessageType (engine type-token -> neutral MessageType boundar ['multi_vcard', 'contact'], ['call_log', 'call'], ['revoked', 'revoked'], + ['poll_creation', 'poll'], ['e2e_notification', 'unknown'], // any unmapped wwebjs type ])('maps wwebjs type %s -> %s', (raw, expected) => { expect(mapWwebjsMessageType(raw)).toBe(expected); diff --git a/src/engine/adapters/message-mapper.ts b/src/engine/adapters/message-mapper.ts index e566a37f9..83e582db7 100644 --- a/src/engine/adapters/message-mapper.ts +++ b/src/engine/adapters/message-mapper.ts @@ -29,6 +29,8 @@ export function mapWwebjsMessageType(raw: string): MessageType { return 'contact'; case 'call_log': return 'call'; + case 'poll_creation': + return 'poll'; case 'revoked': return 'revoked'; default: diff --git a/src/engine/interfaces/whatsapp-engine.interface.ts b/src/engine/interfaces/whatsapp-engine.interface.ts index a2d4ee3e0..5dfc0c516 100644 --- a/src/engine/interfaces/whatsapp-engine.interface.ts +++ b/src/engine/interfaces/whatsapp-engine.interface.ts @@ -55,6 +55,7 @@ export type MessageType = | 'sticker' | 'location' | 'contact' + | 'poll' | 'call' | 'revoked' // A message WhatsApp deliberately withheld from linked/companion devices (e.g. high-security diff --git a/src/modules/message/message.service.spec.ts b/src/modules/message/message.service.spec.ts index cd612176f..d8074e75c 100644 --- a/src/modules/message/message.service.spec.ts +++ b/src/modules/message/message.service.spec.ts @@ -649,6 +649,10 @@ describe('MessageService', () => { options: ['Park', 'Beach'], allowMultipleAnswers: false, }); + // A poll has no plain-text body, so it is persisted as type 'poll' with the question as the body. + expect(repository.create).toHaveBeenCalledWith( + expect.objectContaining({ type: 'poll', body: '📊 Where should we meet?' }), + ); }); it('should pass allowMultipleAnswers through to the engine', async () => {