diff --git a/src/api/chats.controller.ts b/src/api/chats.controller.ts index 94f4a7bcf..312b00270 100644 --- a/src/api/chats.controller.ts +++ b/src/api/chats.controller.ts @@ -169,6 +169,24 @@ class ChatsController { return message; } + @Get(':chatId/messages/:messageId/reactions') + @SessionApiParam + @ApiOperation({ summary: 'Gets reactions for a message' }) + @ChatIdApiParam + async getMessageReactions( + @WorkingSessionParam session: WhatsappSession, + @Param('chatId') chatId: string, + @Param('messageId') messageId: string, + ) { + const message = await session.getChatMessage(chatId, messageId, { + downloadMedia: false, + }); + if (!message) { + throw new NotFoundException('Message not found'); + } + return message.reactions || []; + } + @Post(':chatId/messages/:messageId/pin') @SessionApiParam @ApiOperation({ summary: 'Pins a message in the chat' }) diff --git a/src/core/engines/gows/session.gows.core.ts b/src/core/engines/gows/session.gows.core.ts index 5894a4efa..8ec091ad3 100644 --- a/src/core/engines/gows/session.gows.core.ts +++ b/src/core/engines/gows/session.gows.core.ts @@ -122,6 +122,7 @@ import { MessageSource, WAMessage, WAMessageReaction, + WAReactionInfo, } from '@waha/structures/responses.dto'; import { CallData } from '@waha/structures/calls.dto'; import { @@ -539,38 +540,42 @@ export class WhatsappSessionGoWSCore extends WhatsappSession { msg?.Message?.protocolMessage?.key !== undefined ); }), - mergeMap(async (message): Promise => { - const afterMessage = await this.toWAMessage(message); - // Extract the revoked message ID from protocolMessage.key - const revokedMessageId = message.Message.protocolMessage.key?.ID; - return { - after: afterMessage, - before: null, - revokedMessageId: revokedMessageId, - _data: message, - }; - }), + mergeMap( + async (message): Promise => { + const afterMessage = await this.toWAMessage(message); + // Extract the revoked message ID from protocolMessage.key + const revokedMessageId = message.Message.protocolMessage.key?.ID; + return { + after: afterMessage, + before: null, + revokedMessageId: revokedMessageId, + _data: message, + }; + }, + ), ); this.events2.get(WAHAEvents.MESSAGE_REVOKED).switch(messagesRevoked$); // Handle edited messages const messagesEdited$ = messages$.pipe( filter((message) => IsEditedMessage(message.Message)), - mergeMap(async (message): Promise => { - const waMessage = await this.toWAMessage(message); - const content = normalizeMessageContent(message.Message); - // Extract the body from editedMessage using extractBody function - const body = extractBody(content.protocolMessage.editedMessage) || ''; - // Extract the original message ID from protocolMessage.key - // @ts-ignore - const editedMessageId = content.protocolMessage.key?.ID; - return { - ...waMessage, - body: body, - editedMessageId: editedMessageId, - _data: message, - }; - }), + mergeMap( + async (message): Promise => { + const waMessage = await this.toWAMessage(message); + const content = normalizeMessageContent(message.Message); + // Extract the body from editedMessage using extractBody function + const body = extractBody(content.protocolMessage.editedMessage) || ''; + // Extract the original message ID from protocolMessage.key + // @ts-ignore + const editedMessageId = content.protocolMessage.key?.ID; + return { + ...waMessage, + body: body, + editedMessageId: editedMessageId, + _data: message, + }; + }, + ), ); this.events2.get(WAHAEvents.MESSAGE_EDITED).switch(messagesEdited$); @@ -2239,10 +2244,20 @@ export class WhatsappSessionGoWSCore extends WhatsappSession { vCards: extractVCards(waproto), ackName: WAMessageAck[ack] || ACK_UNKNOWN, replyTo: replyTo, + reactions: this.extractReactions(message.Reactions), _data: message, }; } + protected extractReactions(reactions: any[]): WAReactionInfo[] { + if (!reactions || !Array.isArray(reactions)) return []; + return reactions.map((r) => ({ + reaction: r.Text || '', + senderId: toCusFormat(r.Sender), + timestamp: r.Timestamp ? new Date(r.Timestamp).getTime() / 1000 : 0, + })); + } + private toPollVotePayload(event: any): PollVotePayload { // Extract event creation message key from the message const creationKey = event.Message?.pollUpdateMessage.pollCreationMessageKey; diff --git a/src/core/engines/noweb/session.noweb.core.ts b/src/core/engines/noweb/session.noweb.core.ts index c23543cc5..50835da07 100644 --- a/src/core/engines/noweb/session.noweb.core.ts +++ b/src/core/engines/noweb/session.noweb.core.ts @@ -154,7 +154,7 @@ import { WAHAChatPresences, WAHAPresenceData, } from '@waha/structures/presence.dto'; -import { WAMessage, WAMessageReaction } from '@waha/structures/responses.dto'; +import { WAMessage, WAMessageReaction, WAReactionInfo } from '@waha/structures/responses.dto'; import { MeInfo } from '@waha/structures/sessions.dto'; import { BROADCAST_ID, @@ -2465,10 +2465,20 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { location: extractWALocation(waproto), vCards: extractVCards(waproto), replyTo: replyTo, + reactions: this.extractReactions(message.reactions), _data: message, }; } + protected extractReactions(reactions: any[]): WAReactionInfo[] { + if (!reactions || !Array.isArray(reactions)) return []; + return reactions.map((r) => ({ + reaction: r.text || '', + senderId: toCusFormat(r.key?.participant || r.key?.remoteJid), + timestamp: ensureNumber(r.senderTimestampMs) || 0, + })); + } + protected extractReplyTo(message): ReplyToMessage | null { const msgType = getContentType(message); const contextInfo = message[msgType]?.contextInfo; diff --git a/src/core/engines/webjs/session.webjs.core.ts b/src/core/engines/webjs/session.webjs.core.ts index 660123872..88d00d66a 100644 --- a/src/core/engines/webjs/session.webjs.core.ts +++ b/src/core/engines/webjs/session.webjs.core.ts @@ -118,6 +118,7 @@ import { WALocation, WAMessage, WAMessageReaction, + WAReactionInfo, } from '@waha/structures/responses.dto'; import { BrowserTraceQuery } from '@waha/structures/server.debug.dto'; import { MeInfo } from '@waha/structures/sessions.dto'; @@ -751,7 +752,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession { const message = this.recreateMessage(messageId); const options = { // It's fine to sent just ids instead of Contact object - mentions: request.mentions as unknown as string[], + mentions: (request.mentions as unknown) as string[], linkPreview: request.linkPreview, }; return message.edit(request.text, options); @@ -1692,19 +1693,23 @@ export class WhatsappSessionWebJSCore extends WhatsappSession { filter((evt: any) => this.jids.include(evt?.after?.id?.remote || evt?.before?.id?.remote), ), - map((event): WAMessageRevokedBody => { - const afterMessage = event.after ? this.toWAMessage(event.after) : null; - const beforeMessage = event.before - ? this.toWAMessage(event.before) - : null; - // Extract the revoked message ID from the protocolMessageKey.id field - const revokedMessageId = afterMessage?._data?.protocolMessageKey?.id; - return { - after: afterMessage, - before: beforeMessage, - revokedMessageId: revokedMessageId, - }; - }), + map( + (event): WAMessageRevokedBody => { + const afterMessage = event.after + ? this.toWAMessage(event.after) + : null; + const beforeMessage = event.before + ? this.toWAMessage(event.before) + : null; + // Extract the revoked message ID from the protocolMessageKey.id field + const revokedMessageId = afterMessage?._data?.protocolMessageKey?.id; + return { + after: afterMessage, + before: beforeMessage, + revokedMessageId: revokedMessageId, + }; + }, + ), ); this.events2.get(WAHAEvents.MESSAGE_REVOKED).switch(messagesRevoked$); @@ -1725,15 +1730,17 @@ export class WhatsappSessionWebJSCore extends WhatsappSession { ); const messagesEdit$ = messageEdit$.pipe( filter((event: any) => this.jids.include(event?.message?.id?.remote)), - map((event): WAMessageEditedBody => { - const message = this.toWAMessage(event.message); - return { - ...message, - body: event.newBody, - editedMessageId: message._data?.id?.id, - _data: event, - }; - }), + map( + (event): WAMessageEditedBody => { + const message = this.toWAMessage(event.message); + return { + ...message, + body: event.newBody, + editedMessageId: message._data?.id?.id, + _data: event, + }; + }, + ), ); this.events2.get(WAHAEvents.MESSAGE_EDITED).switch(messagesEdit$); @@ -1904,6 +1911,19 @@ export class WhatsappSessionWebJSCore extends WhatsappSession { ) { // Convert const wamessage = this.toWAMessage(message); + // Reactions - use getReactions() API if message has reactions + if (message.hasReaction) { + const reactionLists = await message.getReactions().catch((e) => { + this.logger.error( + { error: e, msg: message.id._serialized }, + 'Failed to get reactions', + ); + return null; + }); + if (reactionLists) { + wamessage.reactions = this.convertReactionLists(reactionLists); + } + } // Media if (downloadMedia) { const media = await this.downloadMediaSafe(message); @@ -1912,6 +1932,22 @@ export class WhatsappSessionWebJSCore extends WhatsappSession { return wamessage; } + protected convertReactionLists(reactionLists: any[]): WAReactionInfo[] { + if (!reactionLists || !Array.isArray(reactionLists)) return []; + const reactions: WAReactionInfo[] = []; + for (const reactionList of reactionLists) { + if (!reactionList.senders) continue; + for (const sender of reactionList.senders) { + reactions.push({ + reaction: sender.reaction || reactionList.aggregateEmoji || '', + senderId: toCusFormat(sender.senderId), + timestamp: sender.timestamp || 0, + }); + } + } + return reactions; + } + private toRejectedCallData(peerJid: string, id: string): CallData { const timestamp = Math.floor(Date.now() / 1000); return { @@ -2145,8 +2181,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession { } export class WEBJSEngineMediaProcessor - implements IMediaEngineProcessor -{ + implements IMediaEngineProcessor { hasMedia(message: Message): boolean { if (!message.hasMedia) { return false; diff --git a/src/structures/responses.dto.ts b/src/structures/responses.dto.ts index 34f5957a2..0d2cebe1c 100644 --- a/src/structures/responses.dto.ts +++ b/src/structures/responses.dto.ts @@ -125,6 +125,33 @@ export class WAMessage extends WAMessageBase { 'Message in a raw format that we get from WhatsApp. May be changed anytime, use it with caution! It depends a lot on the underlying backend.', }) _data?: any; + + @ApiProperty({ + description: 'Reactions to this message', + type: () => [WAReactionInfo], + required: false, + }) + reactions?: WAReactionInfo[]; +} + +export class WAReactionInfo { + @ApiProperty({ + description: 'Emoji reaction', + example: '👍', + }) + reaction: string; + + @ApiProperty({ + description: 'Who sent the reaction', + example: '1234567890@c.us', + }) + senderId: string; + + @ApiProperty({ + description: 'Unix timestamp when reaction was sent', + example: 1666943582, + }) + timestamp: number; } export class WAReaction {