From 84cd6c8b28b5297ddbc6e7d0573ac490a685aa6a Mon Sep 17 00:00:00 2001 From: rafavasconcelost Date: Sun, 26 Apr 2026 22:40:15 -0300 Subject: [PATCH] feat(chat): add markMessageAsPlayed endpoint (audio receipt) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds POST /chat/markMessageAsPlayed/{instance} for marking received audio messages as played (blue microphone in WhatsApp), mirroring the existing markMessageAsRead pattern. Baileys natively supports sock.sendReceipts(keys, 'played') but Evolution only exposed the 'read' type via /chat/markMessageAsRead. CRMs that play back voice notes received from contacts had no way to send the played ack — this endpoint fills the gap with the same DTO/schema shape (key shape: id, fromMe, remoteJid) under a 'playedMessages' array. Mirrors: - DTO: MarkMessageAsPlayedDto extends Key array (mirrors ReadMessageDto) - Schema: markMessageAsPlayedSchema (JSONSchema7, mirrors readMessageSchema) - Service: markMessageAsPlayed -> client.sendReceipts(keys, 'played') - Controller: markMessageAsPlayed -> waMonitor delegation - Router: POST routerPath('markMessageAsPlayed') Use case: agent CRMs (Chatwoot-like) that present audio messages with a play button and need to send the played receipt back to the contact when the agent plays the audio in the dashboard. --- src/api/controllers/chat.controller.ts | 5 +++++ src/api/dto/chat.dto.ts | 4 ++++ .../whatsapp/whatsapp.baileys.service.ts | 19 ++++++++++++++++ src/api/routes/chat.router.ts | 12 ++++++++++ src/validate/chat.schema.ts | 22 +++++++++++++++++++ 5 files changed, 62 insertions(+) diff --git a/src/api/controllers/chat.controller.ts b/src/api/controllers/chat.controller.ts index 22e90b9fa..b2d50df07 100644 --- a/src/api/controllers/chat.controller.ts +++ b/src/api/controllers/chat.controller.ts @@ -4,6 +4,7 @@ import { DeleteMessage, getBase64FromMediaMessageDto, MarkChatUnreadDto, + MarkMessageAsPlayedDto, NumberDto, PrivacySettingDto, ProfileNameDto, @@ -30,6 +31,10 @@ export class ChatController { return await this.waMonitor.waInstances[instanceName].markMessageAsRead(data); } + public async markMessageAsPlayed({ instanceName }: InstanceDto, data: MarkMessageAsPlayedDto) { + return await this.waMonitor.waInstances[instanceName].markMessageAsPlayed(data); + } + public async archiveChat({ instanceName }: InstanceDto, data: ArchiveChatDto) { return await this.waMonitor.waInstances[instanceName].archiveChat(data); } diff --git a/src/api/dto/chat.dto.ts b/src/api/dto/chat.dto.ts index b11f32b05..98a965366 100644 --- a/src/api/dto/chat.dto.ts +++ b/src/api/dto/chat.dto.ts @@ -70,6 +70,10 @@ export class ReadMessageDto { readMessages: Key[]; } +export class MarkMessageAsPlayedDto { + playedMessages: Key[]; +} + export class LastMessage { key: Key; messageTimestamp?: number; diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 60e857fcc..d1b450f5b 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -7,6 +7,7 @@ import { getBase64FromMediaMessageDto, LastMessage, MarkChatUnreadDto, + MarkMessageAsPlayedDto, NumberBusiness, OnWhatsAppDto, PrivacySettingDto, @@ -3688,6 +3689,24 @@ export class BaileysStartupService extends ChannelStartupService { } } + public async markMessageAsPlayed(data: MarkMessageAsPlayedDto) { + try { + const keys: proto.IMessageKey[] = []; + data.playedMessages.forEach((played) => { + if (isJidGroup(played.remoteJid) || isPnUser(played.remoteJid)) { + keys.push({ remoteJid: played.remoteJid, fromMe: played.fromMe, id: played.id }); + } + }); + // Baileys exposes sendReceipts(keys, type) where type='played' triggers the + // PLAYED ack (blue microphone). Used when an agent plays back an audio + // message received from a contact, mirroring the contact's view in WhatsApp. + await this.client.sendReceipts(keys, 'played'); + return { message: 'Played messages', played: 'success' }; + } catch (error) { + throw new InternalServerErrorException('Mark messages as played fail', error.toString()); + } + } + public async getLastMessage(number: string) { const where: any = { key: { remoteJid: number }, instanceId: this.instance.id }; diff --git a/src/api/routes/chat.router.ts b/src/api/routes/chat.router.ts index 158947ed2..90bd0d414 100644 --- a/src/api/routes/chat.router.ts +++ b/src/api/routes/chat.router.ts @@ -5,6 +5,7 @@ import { DeleteMessage, getBase64FromMediaMessageDto, MarkChatUnreadDto, + MarkMessageAsPlayedDto, NumberDto, PrivacySettingDto, ProfileNameDto, @@ -25,6 +26,7 @@ import { contactValidateSchema, deleteMessageSchema, markChatUnreadSchema, + markMessageAsPlayedSchema, messageUpSchema, messageValidateSchema, presenceSchema, @@ -70,6 +72,16 @@ export class ChatRouter extends RouterBroker { return res.status(HttpStatus.CREATED).json(response); }) + .post(this.routerPath('markMessageAsPlayed'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: markMessageAsPlayedSchema, + ClassRef: MarkMessageAsPlayedDto, + execute: (instance, data) => chatController.markMessageAsPlayed(instance, data), + }); + + return res.status(HttpStatus.CREATED).json(response); + }) .post(this.routerPath('archiveChat'), ...guards, async (req, res) => { const response = await this.dataValidate({ request: req, diff --git a/src/validate/chat.schema.ts b/src/validate/chat.schema.ts index 7dae44539..03ebb73f4 100644 --- a/src/validate/chat.schema.ts +++ b/src/validate/chat.schema.ts @@ -63,6 +63,28 @@ export const readMessageSchema: JSONSchema7 = { required: ['readMessages'], }; +export const markMessageAsPlayedSchema: JSONSchema7 = { + $id: v4(), + type: 'object', + properties: { + playedMessages: { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + properties: { + id: { type: 'string' }, + fromMe: { type: 'boolean', enum: [true, false] }, + remoteJid: { type: 'string' }, + }, + required: ['id', 'fromMe', 'remoteJid'], + ...isNotEmpty('id', 'remoteJid'), + }, + }, + }, + required: ['playedMessages'], +}; + export const archiveChatSchema: JSONSchema7 = { $id: v4(), type: 'object',