diff --git a/docs/site/content/en/docs/how-to/engines/index.md b/docs/site/content/en/docs/how-to/engines/index.md index 9b727814..ade7ca7a 100644 --- a/docs/site/content/en/docs/how-to/engines/index.md +++ b/docs/site/content/en/docs/how-to/engines/index.md @@ -86,7 +86,7 @@ Here, you will find a list of supported endpoints and webhooks per engine. If you don't specify `WHATSAPP_DEFAULT_ENGINE` environment variable - look at **WEBJS** engine, it's the engine WAHA runs by default. -### Engine +### Protocols | | WEBJS | NOWEB | VENOM | |--------------------------------------------------------------|:-----:|:-----:|:-----:| @@ -186,10 +186,13 @@ please [create an issue](https://github.com/devlikeapro/whatsapp-http-api/issues | `GET /api/version` | ➖ | ✔️ | ➖ | | `GET /health` ![](/images/versions/plus.png) | ✔️ | ✔️ | ✔️ | +### Webhooks + | **Webhooks** | WEBJS | NOWEB | VENOM | |-----------------------------------------------------|:-----:|:-----:|:-----:| | `message` | ✔️ | ✔️ | ✔️ | | `message` with files ![](/images/versions/plus.png) | ✔️ | ✔️ | ✔️ | +| `message.reaction` | ✔️ | ✔️ | ➖ | | `message.any` | ✔️ | ✔️ | ✔️ | | `message.ack` | ✔️ | ✔️ | ✔️ | | `message.revoked` | ✔️ | ➖ | ➖ | diff --git a/docs/site/content/en/docs/how-to/webhooks/index.md b/docs/site/content/en/docs/how-to/webhooks/index.md index fe7f8ee4..fda6f49d 100644 --- a/docs/site/content/en/docs/how-to/webhooks/index.md +++ b/docs/site/content/en/docs/how-to/webhooks/index.md @@ -226,6 +226,43 @@ Fired on all message creations, including your own. The payload is the same as f } ``` +### message.reaction +Receive events when a message is reacted to by a user (or **yourself** reacting to a message). +- `payload.reaction.text` - emoji that was used to react to the message. It'll be an empty string if the reaction was removed. +- `payload.reaction.messageId` - id of the message that was reacted to. + +```json +{ + "event": "message.reaction", + "session": "default", + "me": { + "id": "79222222222@c.us", + "pushName": "WAHA" + }, + "payload": { + "id": "false_79111111@c.us_11111111111111111111111111111111", + "from": "79111111@c.us", + "fromMe": false, + "participant": "79111111@c.us", + "to": "79111111@c.us", + "timestamp": 1710481111.853, + "reaction": { + "text": "🙏", + "messageId": "true_79111111@c.us_11111111111111111111111111111111" + } + }, + "engine": "WEBJS", + "environment": { + "version": "2024.3.3", + "engine": "WEBJS", + "tier": "PLUS", + "browser": "/usr/bin/google-chrome-stable" + } +} +``` + +{{< alert icon="👉" text="NOWEB engine note - reactions were sent in 'message' and 'message.any' events, not it's available only in 'message.reaction'!" />}} + ### message.ack Receive events when server or recipient gets the message, read or played it. diff --git a/docs/site/content/en/docs/overview/changelog.md b/docs/site/content/en/docs/overview/changelog.md index c0d34d2a..b5c0d7a3 100644 --- a/docs/site/content/en/docs/overview/changelog.md +++ b/docs/site/content/en/docs/overview/changelog.md @@ -19,6 +19,10 @@ If you wish to get and read Release Notes - read them on one of our platforms, * You even can **subscribe to get new updates** there! +## 2024.3 +- Add [message.reaction]({{< relref "/docs/how-to/webhooks#messagereaction" >}}) webhook in **WEBJS** and **NOWEB** engines ([#275](https://github.com/devlikeapro/whatsapp-http-api/issues/275)). + - 👉 **NOWEB** engine note - reactions were sent in `'message'` and `'message.any'` events, not it's available **only** in `'message.reaction'`! + ## 2024.2 - Add support for [MongoDB as storage for Session data]({{< relref "/docs/how-to/storages" >}}) - Support persistent file storage for media files - [now you can save media files between container restarts]({{< relref "/docs/how-to/storages#media" >}}) diff --git a/src/core/engines/noweb/session.noweb.core.ts b/src/core/engines/noweb/session.noweb.core.ts index ffc54584..b09ea051 100644 --- a/src/core/engines/noweb/session.noweb.core.ts +++ b/src/core/engines/noweb/session.noweb.core.ts @@ -55,7 +55,10 @@ import { WAHAChatPresences, WAHAPresenceData, } from '../../../structures/presence.dto'; -import { WAMessage } from '../../../structures/responses.dto'; +import { + WAMessage, + WAMessageReaction, +} from '../../../structures/responses.dto'; import { MeInfo } from '../../../structures/sessions.dto'; import { BROADCAST_ID, TextStatus } from '../../../structures/status.dto'; import { @@ -585,6 +588,12 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { this.handleIncomingMessages(messages, handler, false); }); return true; + case WAHAEvents.MESSAGE_REACTION: + this.sock.ev.on(BaileysEvents.MESSAGES_UPSERT, ({ messages }) => { + const reactions = this.processMessageReaction(messages); + reactions.map(handler); + }); + return true; case WAHAEvents.MESSAGE_ANY: this.sock.ev.on(BaileysEvents.MESSAGES_UPSERT, ({ messages }) => this.handleIncomingMessages(messages, handler, true), @@ -641,6 +650,8 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { // if there is no text or media message if (!message) return; if (!message.message) return; + // Ignore reactions, we have dedicated handler for that + if (message.message.reactionMessage) return; // Ignore poll votes, we have dedicated handler for that if (message.message.pollUpdateMessage) return; // Do not include my messages @@ -651,6 +662,34 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { } } + private processMessageReaction(messages: any[]): WAMessageReaction[] { + const reactions = []; + for (const message of messages) { + if (!message) return; + if (!message.message) return; + if (!message.message.reactionMessage) return; + + const id = buildMessageId(message.key); + const fromToParticipant = getFromToParticipant(message); + const reactionMessage = message.message.reactionMessage; + const messageId = buildMessageId(reactionMessage.key); + const reaction: WAMessageReaction = { + id: id, + timestamp: message.messageTimestamp, + from: toCusFormat(fromToParticipant.from), + fromMe: message.key.fromMe, + to: toCusFormat(fromToParticipant.to), + participant: toCusFormat(fromToParticipant.participant), + reaction: { + text: reactionMessage.text, + messageId: messageId, + }, + }; + reactions.push(reaction); + } + return reactions; + } + private processIncomingMessage(message) { return this.downloadMedia(message) .then(this.toWAMessage) diff --git a/src/core/engines/webjs/session.webjs.core.ts b/src/core/engines/webjs/session.webjs.core.ts index c7a0ed76..880d6ed1 100644 --- a/src/core/engines/webjs/session.webjs.core.ts +++ b/src/core/engines/webjs/session.webjs.core.ts @@ -8,6 +8,7 @@ import { GroupChat, Location, Message, + Reaction, } from 'whatsapp-web.js'; import { Message as MessageInstance } from 'whatsapp-web.js/src/structures'; @@ -39,7 +40,10 @@ import { ParticipantsRequest, SettingsSecurityChangeInfo, } from '../../../structures/groups.dto'; -import { WAMessage } from '../../../structures/responses.dto'; +import { + WAMessage, + WAMessageReaction, +} from '../../../structures/responses.dto'; import { MeInfo } from '../../../structures/sessions.dto'; import { WAMessageRevokedBody } from '../../../structures/webhooks.dto'; import { IEngineMediaProcessor } from '../../abc/media.abc'; @@ -520,6 +524,11 @@ export class WhatsappSessionWebJSCore extends WhatsappSession { }, ); return true; + case WAHAEvents.MESSAGE_REACTION: + this.whatsapp.on('message_reaction', (message) => + handler(this.processMessageReaction(message)), + ); + return true; case WAHAEvents.MESSAGE_ANY: this.whatsapp.on(Events.MESSAGE_CREATE, (message) => this.processIncomingMessage(message).then(handler), @@ -557,6 +566,21 @@ export class WhatsappSessionWebJSCore extends WhatsappSession { return await this.toWAMessage(message); } + private processMessageReaction(reaction: Reaction): WAMessageReaction { + return { + id: reaction.id._serialized, + from: reaction.senderId, + fromMe: reaction.id.fromMe, + participant: reaction.senderId, + to: reaction.id.remote, + timestamp: reaction.timestamp, + reaction: { + text: reaction.reaction, + messageId: reaction.msgId._serialized, + }, + }; + } + protected toWAMessage(message: Message): Promise { // @ts-ignore return Promise.resolve({ diff --git a/src/structures/enums.dto.ts b/src/structures/enums.dto.ts index cb116b03..b76eb3df 100644 --- a/src/structures/enums.dto.ts +++ b/src/structures/enums.dto.ts @@ -3,6 +3,7 @@ export const SECOND = 1000; export enum WAHAEvents { SESSION_STATUS = 'session.status', MESSAGE = 'message', + MESSAGE_REACTION = 'message.reaction', MESSAGE_ANY = 'message.any', MESSAGE_ACK = 'message.ack', MESSAGE_REVOKED = 'message.revoked', diff --git a/src/structures/responses.dto.ts b/src/structures/responses.dto.ts index eea48d48..9523bc08 100644 --- a/src/structures/responses.dto.ts +++ b/src/structures/responses.dto.ts @@ -30,7 +30,7 @@ export class WAMedia { filename?: string; } -export class WAMessage { +class WAMessageBase { @MessageIdProperty() id: string; @@ -65,7 +65,9 @@ export class WAMessage { description: 'For groups - participant who sent the message', }) participant: string; +} +export class WAMessage extends WAMessageBase { @ApiProperty({ description: 'Message content', }) @@ -121,3 +123,25 @@ export class WAMessage { }) _data?: any; } + +export class WAReaction { + @ApiProperty({ + description: + 'Reaction to the message. Either the reaction (emoji) or empty string to remove the reaction', + }) + text: string; + + @ApiProperty({ + description: 'Message ID for the message to react to', + example: 'false_11111111111@c.us_AAAAAAAAAAAAAAAAAAAA', + }) + messageId: string; +} + +export class WAMessageReaction extends WAMessageBase { + @ApiProperty({ + description: + 'Reaction to the message. Either the reaction (emoji) or empty string to remove the reaction', + }) + reaction: WAReaction; +} diff --git a/src/structures/webhooks.dto.ts b/src/structures/webhooks.dto.ts index 244f31a1..e11d5dec 100644 --- a/src/structures/webhooks.dto.ts +++ b/src/structures/webhooks.dto.ts @@ -10,7 +10,7 @@ import { import { WAHAEnvironment } from './environment.dto'; import { WAHAChatPresences } from './presence.dto'; import { ChatIdProperty, MessageIdProperty } from './properties.dto'; -import { WAMessage } from './responses.dto'; +import { WAMessage, WAMessageReaction } from './responses.dto'; import { MeInfo } from './sessions.dto'; export class WAMessageAckBody { @@ -134,6 +134,16 @@ class WAHAWebhookMessage extends WAHAWebhook { payload: WAMessage; } +class WAHAWebhookMessageReaction extends WAHAWebhook { + @ApiProperty({ + description: + 'The event is triggered when a user reacts or removes a reaction.', + }) + event = WAHAEvents.MESSAGE_REACTION; + + payload: WAMessageReaction; +} + class WAHAWebhookMessageAny extends WAHAWebhook { @ApiProperty({ description: 'Fired on all message creations, including your own.', @@ -223,6 +233,7 @@ class WAHAWebhookPollVoteFailed extends WAHAWebhook { const WAHA_WEBHOOKS = [ WAHAWebhookSessionStatus, WAHAWebhookMessage, + WAHAWebhookMessageReaction, WAHAWebhookMessageAny, WAHAWebhookMessageAck, WAHAWebhookMessageRevoked,