Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/api/chats.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' })
Expand Down
67 changes: 41 additions & 26 deletions src/core/engines/gows/session.gows.core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ import {
MessageSource,
WAMessage,
WAMessageReaction,
WAReactionInfo,
} from '@waha/structures/responses.dto';
import { CallData } from '@waha/structures/calls.dto';
import {
Expand Down Expand Up @@ -539,38 +540,42 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
msg?.Message?.protocolMessage?.key !== undefined
);
}),
mergeMap(async (message): Promise<WAMessageRevokedBody> => {
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<WAMessageRevokedBody> => {
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<WAMessageEditedBody> => {
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<WAMessageEditedBody> => {
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$);

Expand Down Expand Up @@ -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;
Expand Down
12 changes: 11 additions & 1 deletion src/core/engines/noweb/session.noweb.core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down
85 changes: 60 additions & 25 deletions src/core/engines/webjs/session.webjs.core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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$);

Expand All @@ -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$);

Expand Down Expand Up @@ -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);
Expand All @@ -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 {
Expand Down Expand Up @@ -2145,8 +2181,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
}

export class WEBJSEngineMediaProcessor
implements IMediaEngineProcessor<Message>
{
implements IMediaEngineProcessor<Message> {
hasMedia(message: Message): boolean {
if (!message.hasMedia) {
return false;
Expand Down
27 changes: 27 additions & 0 deletions src/structures/responses.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down