From ef7dffd2df0d0624cd3bff3002446ca94822c6bb Mon Sep 17 00:00:00 2001 From: JustJas Date: Wed, 3 Jun 2026 00:33:14 +0530 Subject: [PATCH] feat: add message ack handling and sent message webhooks - Update onMessageAck engine callback to accept full IncomingMessage instead of just message ID and ack - Implement onMessageAck handler in SessionService to log acks, update session activity, and dispatch webhook events - Add webhook dispatch for all outgoing messages in MessageService - Refine onMessage handler in SessionService to properly distinguish sent vs received messages - Add WebhookModule dependency to MessageModule - Clean up peer dependency metadata in package-lock.json --- package-lock.json | 51 ++------ .../adapters/whatsapp-web-js.adapter.ts | 13 +- .../interfaces/whatsapp-engine.interface.ts | 2 +- src/modules/message/message.module.ts | 3 +- src/modules/message/message.service.ts | 122 ++++++++++++++++++ src/modules/session/session.service.ts | 40 +++++- 6 files changed, 182 insertions(+), 49 deletions(-) diff --git a/package-lock.json b/package-lock.json index f3f6b17a..41d3f016 100644 --- a/package-lock.json +++ b/package-lock.json @@ -745,7 +745,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1251,7 +1250,6 @@ "resolved": "https://registry.npmjs.org/@bull-board/api/-/api-7.1.5.tgz", "integrity": "sha512-EW0sbTtGIysu9vipdVpPQeToPqOpPgVZTt+pn1Ut3gbSS/GLWbEgIfFtMmSQDUoSL9WH00RzjgUY5K+43nWh0A==", "license": "MIT", - "peer": true, "dependencies": { "redis-info": "^3.1.0" }, @@ -1290,7 +1288,6 @@ "resolved": "https://registry.npmjs.org/@bull-board/ui/-/ui-7.1.5.tgz", "integrity": "sha512-2IkatKwNRx/1M9/lAZIptcxS1FPNq6icpp2M46Upwd4olVxs/ujF9Kvs+Ff9ExtIO/OgYfwx7mG2IprGZ+nQCg==", "license": "MIT", - "peer": true, "dependencies": { "@bull-board/api": "7.1.5" } @@ -1580,7 +1577,6 @@ "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" @@ -1612,7 +1608,6 @@ "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", @@ -2880,7 +2875,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/bull-shared/-/bull-shared-11.0.4.tgz", "integrity": "sha512-VBJcDHSAzxQnpcDfA0kt9MTGUD1XZzfByV70su0W0eDCQ9aqIEBlzWRW21tv9FG9dIut22ysgDidshdjlnczLw==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "2.8.1" }, @@ -2955,7 +2949,6 @@ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -3126,7 +3119,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.21.tgz", "integrity": "sha512-YV1HYDGsm2rnR0vrLKidtrG6jYX5yqiIjeur1j8++dKGqhhsJ6cjMs0RfQRSTUH7IjgDemA59/znQ8nRrE0D9g==", "license": "MIT", - "peer": true, "dependencies": { "file-type": "21.3.4", "iterare": "1.2.1", @@ -3174,7 +3166,6 @@ "integrity": "sha512-fqo0BHgny3MOuAL8GSfG3ZUKFVVBaBQD/0iyibnwTONT5vPexjQxJzu+945iloVvBDmrnAaRWxC1gqCDEs/AXQ==", "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@nuxt/opencollective": "0.4.1", "fast-safe-stringify": "2.1.1", @@ -3235,7 +3226,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.21.tgz", "integrity": "sha512-lA3ViycOnz4Df3EstIKpuAVFhqxQixTnjAVk0M+LRyNBlGM6VSCaNJaAIrb9Pcry39T4hTHpNVbRqGLSvhL8gA==", "license": "MIT", - "peer": true, "dependencies": { "cors": "2.8.6", "express": "5.2.1", @@ -3257,7 +3247,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-11.1.21.tgz", "integrity": "sha512-Tq5JgaVS+auD3DXuRBy8UMU3mf69HJO8Ep+BuRS9GYMXGd/5sdMHqIQvXlXkGih9tQXdeeG9WoqURe/+IjPKng==", "license": "MIT", - "peer": true, "dependencies": { "socket.io": "4.8.3", "tslib": "2.8.1" @@ -3443,7 +3432,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-11.0.1.tgz", "integrity": "sha512-8rw/nKT0S+L+MkzgE9F2/mox7mAgsPlwfzmW9gsESN1lmQtIrVEfiiBwC2O8+guS1jBfQehJIdcdUj2OAp4VUQ==", "license": "MIT", - "peer": true, "peerDependencies": { "@nestjs/common": "^10.0.0 || ^11.0.0", "@nestjs/core": "^10.0.0 || ^11.0.0", @@ -4040,7 +4028,6 @@ "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -4160,7 +4147,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.0.tgz", "integrity": "sha512-AOQwYUNolgy3VosiRqXrACUXTN8nJUtPl7FJXMqZVyxiiCLhQuG3jXKvCS1ALr+Y2OmZhzzLVlYPEqJaiqkaJQ==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } @@ -4382,7 +4368,6 @@ "integrity": "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.59.3", "@typescript-eslint/types": "8.59.3", @@ -5096,7 +5081,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "devOptional": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5186,7 +5170,6 @@ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -5734,7 +5717,6 @@ "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", "license": "Apache-2.0", - "peer": true, "peerDependencies": { "bare-abort-controller": "*" }, @@ -6037,7 +6019,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -6128,7 +6109,6 @@ "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.76.10.tgz", "integrity": "sha512-LWve7SpQjYSpCP2GEsWmoyzTz2H37L8HRmSTu3YihYsTOr5kJxrfEX6aEV7m6eskEMWXSHZYTMZepX6qNaH6CQ==", "license": "MIT", - "peer": true, "dependencies": { "cron-parser": "4.9.0", "ioredis": "5.10.1", @@ -6455,15 +6435,13 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/class-validator": { "version": "0.15.1", "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.15.1.tgz", "integrity": "sha512-LqoS80HBBSCVhz/3KloUly0ovokxpdOLR++Al3J3+dHXWt9sTKlKd4eYtoxhxyUjoe5+UcIM+5k9MIxyBWnRTw==", "license": "MIT", - "peer": true, "dependencies": { "@types/validator": "^13.15.3", "libphonenumber-js": "^1.11.1", @@ -7183,8 +7161,7 @@ "version": "0.0.1581282", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz", "integrity": "sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==", - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/dezalgo": { "version": "1.0.4", @@ -7683,7 +7660,6 @@ "integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -7740,7 +7716,6 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -9202,7 +9177,6 @@ "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.1.tgz", "integrity": "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==", "license": "MIT", - "peer": true, "dependencies": { "@ioredis/commands": "1.5.1", "cluster-key-slot": "^1.1.0", @@ -9487,7 +9461,6 @@ "integrity": "sha512-Yi1jqNC/Oq0N4hBgNH/YvBpP1P57QqundgytzYqy3yqAa7NZPNjSoi4SGbRAXDMdBzNE6xBCi5U7RgfrvMEUVQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "30.4.2", "@jest/types": "30.4.1", @@ -11413,6 +11386,7 @@ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", "license": "MIT", + "peer": true, "engines": { "node": ">= 6" } @@ -11792,7 +11766,6 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.21.0.tgz", "integrity": "sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.13.0", "pg-pool": "^3.14.0", @@ -12085,7 +12058,6 @@ "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -12774,8 +12746,7 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/require-directory": { "version": "2.1.1", @@ -12925,7 +12896,6 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -13452,7 +13422,6 @@ "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", "hasInstallScript": true, "license": "BSD-3-Clause", - "peer": true, "dependencies": { "bindings": "^1.5.0", "node-addon-api": "^7.0.0", @@ -13975,7 +13944,6 @@ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -14329,7 +14297,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -14514,7 +14481,6 @@ "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.29.tgz", "integrity": "sha512-wwPEX/df4l72gCmOsrs0otJZYLGA9lLQkUZCkukbsymEycV4zXv2KM7wU7v2r8L01TaCgY9ApSSqHQWBOUhEoQ==", "license": "MIT", - "peer": true, "dependencies": { "@sqltools/formatter": "^1.2.5", "ansis": "^4.2.0", @@ -14734,7 +14700,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -15153,6 +15118,7 @@ "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ajv": "^8.0.0" }, @@ -15171,6 +15137,7 @@ "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -15184,6 +15151,7 @@ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -15198,6 +15166,7 @@ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "engines": { "node": ">=4.0" } @@ -15207,7 +15176,8 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/webpack/node_modules/schema-utils": { "version": "4.3.3", @@ -15215,6 +15185,7 @@ "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", diff --git a/src/engine/adapters/whatsapp-web-js.adapter.ts b/src/engine/adapters/whatsapp-web-js.adapter.ts index 727a3db3..e728566e 100644 --- a/src/engine/adapters/whatsapp-web-js.adapter.ts +++ b/src/engine/adapters/whatsapp-web-js.adapter.ts @@ -191,7 +191,18 @@ export class WhatsAppWebJsAdapter extends EventEmitter implements IWhatsAppEngin }); this.client.on('message_ack', (msg, ack) => { - this.callbacks.onMessageAck?.(msg.id._serialized, ack); + const incomingMessage: IncomingMessage = { + id: msg.id._serialized, + from: msg.from, + to: msg.to, + chatId: msg.from, + body: msg.body, + type: msg.type, + timestamp: msg.timestamp, + fromMe: msg.fromMe, + isGroup: msg.from.endsWith('@g.us'), + }; + this.callbacks.onMessageAck?.(incomingMessage, ack); }); this.client.on('disconnected', reason => { diff --git a/src/engine/interfaces/whatsapp-engine.interface.ts b/src/engine/interfaces/whatsapp-engine.interface.ts index f3e5de62..1fa48a47 100644 --- a/src/engine/interfaces/whatsapp-engine.interface.ts +++ b/src/engine/interfaces/whatsapp-engine.interface.ts @@ -198,7 +198,7 @@ export interface EngineEventCallbacks { onQRCode?: (qr: string) => void; onReady?: (phone: string, pushName: string) => void; onMessage?: (message: IncomingMessage) => void; - onMessageAck?: (messageId: string, ack: number) => void; + onMessageAck?: (message: IncomingMessage, ack: number) => void; onDisconnected?: (reason: string) => void; onStateChanged?: (state: EngineStatus) => void; } diff --git a/src/modules/message/message.module.ts b/src/modules/message/message.module.ts index 8051f3a3..01768db2 100644 --- a/src/modules/message/message.module.ts +++ b/src/modules/message/message.module.ts @@ -4,11 +4,12 @@ import { MessageService } from './message.service'; import { BulkMessageService } from './bulk-message.service'; import { MessageController } from './message.controller'; import { SessionModule } from '../session/session.module'; +import { WebhookModule } from '../webhook/webhook.module'; import { Message } from './entities/message.entity'; import { MessageBatch } from './entities/message-batch.entity'; @Module({ - imports: [TypeOrmModule.forFeature([Message, MessageBatch], 'data'), SessionModule], + imports: [TypeOrmModule.forFeature([Message, MessageBatch], 'data'), SessionModule, WebhookModule], controllers: [MessageController], providers: [MessageService, BulkMessageService], exports: [MessageService, BulkMessageService], diff --git a/src/modules/message/message.service.ts b/src/modules/message/message.service.ts index cd245bd5..2a955787 100644 --- a/src/modules/message/message.service.ts +++ b/src/modules/message/message.service.ts @@ -6,6 +6,7 @@ import { SendTextMessageDto, SendMediaMessageDto, MessageResponseDto } from './d import { MediaInput } from '../../engine/interfaces/whatsapp-engine.interface'; import { Message, MessageDirection, MessageStatus } from './entities/message.entity'; import { HookManager } from '../../core/hooks'; +import { WebhookService } from '../webhook/webhook.service'; export interface GetMessagesOptions { chatId?: string; @@ -20,6 +21,7 @@ export class MessageService { private readonly messageRepository: Repository, private readonly sessionService: SessionService, private readonly hookManager: HookManager, + private readonly webhookService: WebhookService, ) {} async sendText(sessionId: string, dto: SendTextMessageDto): Promise { @@ -62,6 +64,18 @@ export class MessageService { { sessionId, source: 'MessageService' }, ); + // Dispatch sent webhook + void this.webhookService.dispatch(sessionId, 'message.sent', { + id: result.id, + from: message.from, + to: message.to, + chatId: message.chatId, + body: message.body ?? '', + type: message.type, + timestamp: result.timestamp, + fromMe: true, + }); + return { messageId: result.id, timestamp: result.timestamp, @@ -102,6 +116,18 @@ export class MessageService { message.timestamp = result.timestamp; await this.messageRepository.save(message); + // Dispatch sent webhook + void this.webhookService.dispatch(sessionId, 'message.sent', { + id: result.id, + from: message.from, + to: message.to, + chatId: message.chatId, + body: message.body ?? '', + type: message.type, + timestamp: result.timestamp, + fromMe: true, + }); + return { messageId: result.id, timestamp: result.timestamp, @@ -133,6 +159,18 @@ export class MessageService { message.timestamp = result.timestamp; await this.messageRepository.save(message); + // Dispatch sent webhook + void this.webhookService.dispatch(sessionId, 'message.sent', { + id: result.id, + from: message.from, + to: message.to, + chatId: message.chatId, + body: message.body ?? '', + type: message.type, + timestamp: result.timestamp, + fromMe: true, + }); + return { messageId: result.id, timestamp: result.timestamp, @@ -163,6 +201,18 @@ export class MessageService { message.timestamp = result.timestamp; await this.messageRepository.save(message); + // Dispatch sent webhook + void this.webhookService.dispatch(sessionId, 'message.sent', { + id: result.id, + from: message.from, + to: message.to, + chatId: message.chatId, + body: message.body ?? '', + type: message.type, + timestamp: result.timestamp, + fromMe: true, + }); + return { messageId: result.id, timestamp: result.timestamp, @@ -194,6 +244,18 @@ export class MessageService { message.timestamp = result.timestamp; await this.messageRepository.save(message); + // Dispatch sent webhook + void this.webhookService.dispatch(sessionId, 'message.sent', { + id: result.id, + from: message.from, + to: message.to, + chatId: message.chatId, + body: message.body ?? '', + type: message.type, + timestamp: result.timestamp, + fromMe: true, + }); + return { messageId: result.id, timestamp: result.timestamp, @@ -258,6 +320,18 @@ export class MessageService { message.timestamp = result.timestamp; await this.messageRepository.save(message); + // Dispatch sent webhook + void this.webhookService.dispatch(sessionId, 'message.sent', { + id: result.id, + from: message.from, + to: message.to, + chatId: message.chatId, + body: message.body ?? '', + type: message.type, + timestamp: result.timestamp, + fromMe: true, + }); + return { messageId: result.id, timestamp: result.timestamp, @@ -294,6 +368,18 @@ export class MessageService { message.timestamp = result.timestamp; await this.messageRepository.save(message); + // Dispatch sent webhook + void this.webhookService.dispatch(sessionId, 'message.sent', { + id: result.id, + from: message.from, + to: message.to, + chatId: message.chatId, + body: message.body ?? '', + type: message.type, + timestamp: result.timestamp, + fromMe: true, + }); + return { messageId: result.id, timestamp: result.timestamp, @@ -324,6 +410,18 @@ export class MessageService { message.timestamp = result.timestamp; await this.messageRepository.save(message); + // Dispatch sent webhook + void this.webhookService.dispatch(sessionId, 'message.sent', { + id: result.id, + from: message.from, + to: message.to, + chatId: message.chatId, + body: message.body ?? '', + type: message.type, + timestamp: result.timestamp, + fromMe: true, + }); + return { messageId: result.id, timestamp: result.timestamp, @@ -357,6 +455,18 @@ export class MessageService { message.timestamp = result.timestamp; await this.messageRepository.save(message); + // Dispatch sent webhook + void this.webhookService.dispatch(sessionId, 'message.sent', { + id: result.id, + from: message.from, + to: message.to, + chatId: message.chatId, + body: message.body ?? '', + type: message.type, + timestamp: result.timestamp, + fromMe: true, + }); + return { messageId: result.id, timestamp: result.timestamp, @@ -390,6 +500,18 @@ export class MessageService { message.timestamp = result.timestamp; await this.messageRepository.save(message); + // Dispatch sent webhook + void this.webhookService.dispatch(sessionId, 'message.sent', { + id: result.id, + from: message.from, + to: message.to, + chatId: message.chatId, + body: message.body ?? '', + type: message.type, + timestamp: result.timestamp, + fromMe: true, + }); + return { messageId: result.id, timestamp: result.timestamp, diff --git a/src/modules/session/session.service.ts b/src/modules/session/session.service.ts index 671599db..22a82ef1 100644 --- a/src/modules/session/session.service.ts +++ b/src/modules/session/session.service.ts @@ -11,7 +11,7 @@ import { Repository, In, DataSource } from 'typeorm'; import { Session, SessionStatus } from './entities/session.entity'; import { CreateSessionDto } from './dto'; import { EngineFactory } from '../../engine/engine.factory'; -import { IWhatsAppEngine, EngineStatus } from '../../engine/interfaces/whatsapp-engine.interface'; +import { IWhatsAppEngine, EngineStatus, IncomingMessage } from '../../engine/interfaces/whatsapp-engine.interface'; import { createLogger } from '../../common/services/logger.service'; import { EventsGateway } from '../events/events.gateway'; import { WebhookService } from '../webhook/webhook.service'; @@ -283,20 +283,25 @@ export class SessionService implements OnModuleDestroy, OnModuleInit { }); }, onMessage: (message): void => { - this.logger.debug(`Message received from ${message.from}`, { + const isFromMe = (message as unknown as Record).fromMe === true; + const eventName = isFromMe ? 'message.sent' : 'message.received'; + const hookName = isFromMe ? 'message:sent' : 'message:received'; + + this.logger.debug(`Message ${isFromMe ? 'sent' : 'received'} from ${message.from}`, { sessionId: id, messageId: message.id, from: message.from, - action: 'message_received', + fromMe: isFromMe, + action: isFromMe ? 'message_sent' : 'message_received', }); // Update last active timestamp void this.sessionRepository.update(id, { lastActiveAt: new Date() }); // Convert IncomingMessage to plain object for dispatch const messageData = { ...message }; - // Execute hook for message received - plugins can modify or stop processing + // Execute hook for message event - plugins can modify or stop processing void this.hookManager - .execute('message:received', messageData, { + .execute(hookName, messageData, { sessionId: id, source: 'Engine', }) @@ -307,11 +312,34 @@ export class SessionService implements OnModuleDestroy, OnModuleInit { } // Dispatch to webhooks with potentially modified message - void this.webhookService.dispatch(id, 'message.received', finalMessage as Record); + void this.webhookService.dispatch(id, eventName, finalMessage as Record); // Emit real-time event to WebSocket clients this.eventsGateway.emitMessage(id, finalMessage as Record); }); }, + onMessageAck: (message: IncomingMessage, ack: number): void => { + this.logger.debug(`Message ACK: ${message.id} ack=${ack} fromMe=${message.fromMe}`, { + sessionId: id, + messageId: message.id, + ack, + fromMe: message.fromMe, + action: 'message_ack', + }); + + // Update last active timestamp + void this.sessionRepository.update(id, { lastActiveAt: new Date() }); + + const ackPayload: Record = { ...message, ack }; + + // Dispatch message.ack webhook + void this.webhookService.dispatch(id, 'message.ack', ackPayload); + + // For outgoing messages, also dispatch message.sent + // (covers phone-sent messages that don't fire the 'message' event) + if (message.fromMe) { + void this.webhookService.dispatch(id, 'message.sent', { ...message }); + } + }, onDisconnected: (reason: string): void => { this.logger.warn(`Session disconnected: ${reason}`, { sessionId: id,