Skip to content

Commit 84cd6c8

Browse files
feat(chat): add markMessageAsPlayed endpoint (audio receipt)
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.
1 parent cd800f2 commit 84cd6c8

5 files changed

Lines changed: 62 additions & 0 deletions

File tree

src/api/controllers/chat.controller.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
DeleteMessage,
55
getBase64FromMediaMessageDto,
66
MarkChatUnreadDto,
7+
MarkMessageAsPlayedDto,
78
NumberDto,
89
PrivacySettingDto,
910
ProfileNameDto,
@@ -30,6 +31,10 @@ export class ChatController {
3031
return await this.waMonitor.waInstances[instanceName].markMessageAsRead(data);
3132
}
3233

34+
public async markMessageAsPlayed({ instanceName }: InstanceDto, data: MarkMessageAsPlayedDto) {
35+
return await this.waMonitor.waInstances[instanceName].markMessageAsPlayed(data);
36+
}
37+
3338
public async archiveChat({ instanceName }: InstanceDto, data: ArchiveChatDto) {
3439
return await this.waMonitor.waInstances[instanceName].archiveChat(data);
3540
}

src/api/dto/chat.dto.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ export class ReadMessageDto {
7070
readMessages: Key[];
7171
}
7272

73+
export class MarkMessageAsPlayedDto {
74+
playedMessages: Key[];
75+
}
76+
7377
export class LastMessage {
7478
key: Key;
7579
messageTimestamp?: number;

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
getBase64FromMediaMessageDto,
88
LastMessage,
99
MarkChatUnreadDto,
10+
MarkMessageAsPlayedDto,
1011
NumberBusiness,
1112
OnWhatsAppDto,
1213
PrivacySettingDto,
@@ -3688,6 +3689,24 @@ export class BaileysStartupService extends ChannelStartupService {
36883689
}
36893690
}
36903691

3692+
public async markMessageAsPlayed(data: MarkMessageAsPlayedDto) {
3693+
try {
3694+
const keys: proto.IMessageKey[] = [];
3695+
data.playedMessages.forEach((played) => {
3696+
if (isJidGroup(played.remoteJid) || isPnUser(played.remoteJid)) {
3697+
keys.push({ remoteJid: played.remoteJid, fromMe: played.fromMe, id: played.id });
3698+
}
3699+
});
3700+
// Baileys exposes sendReceipts(keys, type) where type='played' triggers the
3701+
// PLAYED ack (blue microphone). Used when an agent plays back an audio
3702+
// message received from a contact, mirroring the contact's view in WhatsApp.
3703+
await this.client.sendReceipts(keys, 'played');
3704+
return { message: 'Played messages', played: 'success' };
3705+
} catch (error) {
3706+
throw new InternalServerErrorException('Mark messages as played fail', error.toString());
3707+
}
3708+
}
3709+
36913710
public async getLastMessage(number: string) {
36923711
const where: any = { key: { remoteJid: number }, instanceId: this.instance.id };
36933712

src/api/routes/chat.router.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
DeleteMessage,
66
getBase64FromMediaMessageDto,
77
MarkChatUnreadDto,
8+
MarkMessageAsPlayedDto,
89
NumberDto,
910
PrivacySettingDto,
1011
ProfileNameDto,
@@ -25,6 +26,7 @@ import {
2526
contactValidateSchema,
2627
deleteMessageSchema,
2728
markChatUnreadSchema,
29+
markMessageAsPlayedSchema,
2830
messageUpSchema,
2931
messageValidateSchema,
3032
presenceSchema,
@@ -70,6 +72,16 @@ export class ChatRouter extends RouterBroker {
7072

7173
return res.status(HttpStatus.CREATED).json(response);
7274
})
75+
.post(this.routerPath('markMessageAsPlayed'), ...guards, async (req, res) => {
76+
const response = await this.dataValidate<MarkMessageAsPlayedDto>({
77+
request: req,
78+
schema: markMessageAsPlayedSchema,
79+
ClassRef: MarkMessageAsPlayedDto,
80+
execute: (instance, data) => chatController.markMessageAsPlayed(instance, data),
81+
});
82+
83+
return res.status(HttpStatus.CREATED).json(response);
84+
})
7385
.post(this.routerPath('archiveChat'), ...guards, async (req, res) => {
7486
const response = await this.dataValidate<ArchiveChatDto>({
7587
request: req,

src/validate/chat.schema.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,28 @@ export const readMessageSchema: JSONSchema7 = {
6363
required: ['readMessages'],
6464
};
6565

66+
export const markMessageAsPlayedSchema: JSONSchema7 = {
67+
$id: v4(),
68+
type: 'object',
69+
properties: {
70+
playedMessages: {
71+
type: 'array',
72+
minItems: 1,
73+
uniqueItems: true,
74+
items: {
75+
properties: {
76+
id: { type: 'string' },
77+
fromMe: { type: 'boolean', enum: [true, false] },
78+
remoteJid: { type: 'string' },
79+
},
80+
required: ['id', 'fromMe', 'remoteJid'],
81+
...isNotEmpty('id', 'remoteJid'),
82+
},
83+
},
84+
},
85+
required: ['playedMessages'],
86+
};
87+
6688
export const archiveChatSchema: JSONSchema7 = {
6789
$id: v4(),
6890
type: 'object',

0 commit comments

Comments
 (0)