diff --git a/src/core/abc/session.abc.ts b/src/core/abc/session.abc.ts index ce9661cd..96378e42 100644 --- a/src/core/abc/session.abc.ts +++ b/src/core/abc/session.abc.ts @@ -18,7 +18,7 @@ import { SendButtonsRequest } from '@waha/structures/chatting.buttons.dto'; import { BinaryFile, RemoteFile } from '@waha/structures/files.dto'; import { Label, LabelDTO, LabelID } from '@waha/structures/labels.dto'; import { PaginationParams } from '@waha/structures/pagination.dto'; -import { WAMessage } from '@waha/structures/responses.dto'; +import { MessageSource, WAMessage } from '@waha/structures/responses.dto'; import { DefaultMap } from '@waha/utils/DefaultMap'; import { generatePrefixedId } from '@waha/utils/ids'; import { LoggerBuilder } from '@waha/utils/logging'; @@ -26,7 +26,6 @@ import { complete } from '@waha/utils/reactive/complete'; import { SwitchObservable } from '@waha/utils/reactive/SwitchObservable'; import * as fs from 'fs'; import * as lodash from 'lodash'; -import { PinoLogger } from 'nestjs-pino'; import * as NodeCache from 'node-cache'; import { Logger } from 'pino'; import { @@ -73,7 +72,6 @@ import { CreateGroupRequest, GroupField, GroupsListFields, - GroupsPaginationParams, ParticipantsRequest, SettingsSecurityChangeInfo, } from '../../structures/groups.dto'; @@ -148,6 +146,12 @@ export abstract class WhatsappSession { stdTTL: 24 * 60 * 60, // 1 day }); + // Save sent messages ids in cache so we can determine if a message was sent + // via API or APP + private sentMessageIds: NodeCache = new NodeCache({ + stdTTL: 10 * 60, // 10 minutes + }); + public constructor({ name, printQR, @@ -885,6 +889,18 @@ export abstract class WhatsappSession { ); qrcode.generate(qr.raw, { small: true }); } + + protected saveSentMessageId(id: string) { + this.sentMessageIds.set(id, true); + } + + protected getMessageSource(id: string): MessageSource { + if (!id) { + return MessageSource.APP; + } + const api = this.sentMessageIds.has(id); + return api ? MessageSource.API : MessageSource.APP; + } } export function isNewsletter(jid: string) { @@ -916,3 +932,19 @@ export function parseChannelInviteLink(link: string): string { export function getPublicUrlFromDirectPath(directPath: string) { return `https://pps.whatsapp.net${directPath}`; } + +const deviceRegexp = /^.*:(\d+)@.*$/; + +/** + * Extracts the device ID from a JID string. + * + * @param jid - The JID string (e.g., "123123:12@c.us") + * @return The extracted device ID (e.g., "12") or null if the format is invalid. + */ +export function extractDeviceId(jid: string): string | null { + if (!jid) { + return null; + } + const match = jid.match(deviceRegexp); + return match ? match[1] : null; +} diff --git a/src/core/engines/gows/session.gows.core.ts b/src/core/engines/gows/session.gows.core.ts index 9edf66f8..e8cfbabd 100644 --- a/src/core/engines/gows/session.gows.core.ts +++ b/src/core/engines/gows/session.gows.core.ts @@ -8,19 +8,33 @@ import * as grpc from '@grpc/grpc-js'; import { connectivityState } from '@grpc/grpc-js'; import { UnprocessableEntityException } from '@nestjs/common'; import { + extractDeviceId, getChannelInviteLink, WhatsappSession, } from '@waha/core/abc/session.abc'; import { Jid } from '@waha/core/engines/const'; import { EventsFromObservable } from '@waha/core/engines/gows/EventsFromObservable'; import { GowsEventStreamObservable } from '@waha/core/engines/gows/GowsEventStreamObservable'; +import { + ToGroupV2JoinEvent, + ToGroupV2LeaveEvent, + ToGroupV2ParticipantsEvents, + ToGroupV2UpdateEvent, +} from '@waha/core/engines/gows/groups.gows'; import { messages } from '@waha/core/engines/gows/grpc/gows'; +import { + optional, + parseJson, + parseJsonList, + statusToAck, +} from '@waha/core/engines/gows/helpers'; import { GowsAuthFactoryCore } from '@waha/core/engines/gows/store/GowsAuthFactoryCore'; import { parseMessageIdSerialized, toCusFormat, toJID, } from '@waha/core/engines/noweb/session.noweb.core'; +import { extractMediaContent } from '@waha/core/engines/noweb/utils'; import { AvailableInPlusVersion, NotImplementedByEngineError, @@ -38,6 +52,13 @@ import { ListChannelsQuery, PreviewChannelMessages, } from '@waha/structures/channels.dto'; +import { + ChatSortField, + ChatSummary, + GetChatMessageQuery, + GetChatMessagesFilter, + GetChatMessagesQuery, +} from '@waha/structures/chats.dto'; import { ChatRequest, CheckNumberStatusQuery, @@ -53,6 +74,7 @@ import { SendSeenRequest, WANumberExistResult, } from '@waha/structures/chatting.dto'; +import { ContactQuery } from '@waha/structures/contacts.dto'; import { ACK_UNKNOWN, WAHAEvents, @@ -60,11 +82,24 @@ import { WAHASessionStatus, WAMessageAck, } from '@waha/structures/enums.dto'; +import { BinaryFile, RemoteFile } from '@waha/structures/files.dto'; +import { + CreateGroupRequest, + GroupSortField, + Participant, + ParticipantsRequest, + SettingsSecurityChangeInfo, +} from '@waha/structures/groups.dto'; +import { PaginationParams, SortOrder } from '@waha/structures/pagination.dto'; import { WAHAChatPresences, WAHAPresenceData, } from '@waha/structures/presence.dto'; -import { WAMessage, WAMessageReaction } from '@waha/structures/responses.dto'; +import { + MessageSource, + WAMessage, + WAMessageReaction, +} from '@waha/structures/responses.dto'; import { MeInfo, ProxyConfig } from '@waha/structures/sessions.dto'; import { BROADCAST_ID, @@ -73,6 +108,7 @@ import { TextStatus, } from '@waha/structures/status.dto'; import { EnginePayload, WAMessageAckBody } from '@waha/structures/webhooks.dto'; +import { PaginatorInMemory } from '@waha/utils/Paginator'; import { sleep, waitUntil } from '@waha/utils/promiseTimeout'; import { onlyEvent } from '@waha/utils/reactive/ops/onlyEvent'; import * as NodeCache from 'node-cache'; @@ -92,37 +128,6 @@ import { promisify } from 'util'; import * as gows from './types'; import MessageServiceClient = messages.MessageServiceClient; -import { - ToGroupV2JoinEvent, - ToGroupV2LeaveEvent, - ToGroupV2ParticipantsEvents, - ToGroupV2UpdateEvent, -} from '@waha/core/engines/gows/groups.gows'; -import { - optional, - parseJson, - parseJsonList, - statusToAck, -} from '@waha/core/engines/gows/helpers'; -import { extractMediaContent } from '@waha/core/engines/noweb/utils'; -import { - ChatSortField, - ChatSummary, - GetChatMessageQuery, - GetChatMessagesFilter, - GetChatMessagesQuery, -} from '@waha/structures/chats.dto'; -import { ContactQuery } from '@waha/structures/contacts.dto'; -import { BinaryFile, RemoteFile } from '@waha/structures/files.dto'; -import { - CreateGroupRequest, - GroupSortField, - Participant, - ParticipantsRequest, - SettingsSecurityChangeInfo, -} from '@waha/structures/groups.dto'; -import { PaginationParams, SortOrder } from '@waha/structures/pagination.dto'; -import { PaginatorInMemory } from '@waha/utils/Paginator'; enum WhatsMeowEvent { CONNECTED = 'gows.ConnectedEventData', @@ -256,6 +261,8 @@ export class WhatsappSessionGoWSCore extends WhatsappSession { id: toCusFormat(jidNormalizedUser(data.ID)), pushName: data.PushName, }; + // @ts-ignore + this.me.jid = data.ID; }); events.on(WhatsMeowEvent.DISCONNECTED, () => { @@ -1378,7 +1385,6 @@ export class WhatsappSessionGoWSCore extends WhatsappSession { const id = buildMessageId(message); const body = this.extractBody(message.Message); const replyTo = null; // TODO: this.extractReplyTo(message.message); - let ack; if (message.Status) { ack = statusToAck(message.Status); @@ -1386,12 +1392,14 @@ export class WhatsappSessionGoWSCore extends WhatsappSession { ack = message.Info.IsFromMe ? WAMessageAck.SERVER : WAMessageAck.DEVICE; } const mediaContent = extractMediaContent(message.Message); + const source = this.getSourceDeviceByMsg(message); return { id: id, timestamp: parseTimestampToSeconds(message.Info.Timestamp), from: toCusFormat(fromToParticipant.from), fromMe: message.Info.IsFromMe, + source: source, body: body, to: toCusFormat(fromToParticipant.to), participant: toCusFormat(fromToParticipant.participant), @@ -1408,6 +1416,17 @@ export class WhatsappSessionGoWSCore extends WhatsappSession { }; } + private getSourceDeviceByMsg(message): MessageSource { + if (!message.Info.IsFromMe) { + return MessageSource.APP; + } + // @ts-ignore + const myJid = this.me.jid; + const myDeviceId = extractDeviceId(myJid); + const sentDeviceId = extractDeviceId(message.Info.Sender); + return sentDeviceId === myDeviceId ? MessageSource.API : MessageSource.APP; + } + private extractBody(message) { if (!message) { return null; @@ -1514,11 +1533,13 @@ export class WhatsappSessionGoWSCore extends WhatsappSession { const fromToParticipant = getFromToParticipant(message); const reactionMessage = message.Message.reactionMessage; const messageId = this.buildMessageIdFromKey(reactionMessage.key); + const source = this.getSourceDeviceByMsg(message); const reaction: WAMessageReaction = { id: id, timestamp: parseTimestampToSeconds(message.Info.Timestamp), from: toCusFormat(fromToParticipant.from), fromMe: message.Info.IsFromMe, + source: source, to: toCusFormat(fromToParticipant.to), participant: toCusFormat(fromToParticipant.participant), reaction: { diff --git a/src/core/engines/noweb/session.noweb.core.ts b/src/core/engines/noweb/session.noweb.core.ts index abe6d052..f7f1dc06 100644 --- a/src/core/engines/noweb/session.noweb.core.ts +++ b/src/core/engines/noweb/session.noweb.core.ts @@ -5,6 +5,7 @@ import makeWASocket, { DisconnectReason, downloadMediaMessage, extractMessageContent, + generateMessageIDV2, getAggregateVotesInPollMessage, getContentType, getKeyAuthor, @@ -151,6 +152,7 @@ import { WAHAPresenceData, } from '../../../structures/presence.dto'; import { + MessageSource, WAMessage, WAMessageReaction, } from '../../../structures/responses.dto'; @@ -1765,11 +1767,13 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { const fromToParticipant = getFromToParticipant(message); const reactionMessage = message.message.reactionMessage; const messageId = buildMessageId(reactionMessage.key); + const source = this.getMessageSource(message.key.id); const reaction: WAMessageReaction = { id: id, timestamp: ensureNumber(message.messageTimestamp), from: toCusFormat(fromToParticipant.from), fromMe: message.key.fromMe, + source: source, to: toCusFormat(fromToParticipant.to), participant: toCusFormat(fromToParticipant.participant), reaction: { @@ -1828,11 +1832,13 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { const replyTo = this.extractReplyTo(message.message); const ack = message.ack || message.status - 1; const mediaContent = extractMediaContent(message.message); + const source = this.getMessageSource(message.key.id); return Promise.resolve({ id: id, timestamp: ensureNumber(message.messageTimestamp), from: toCusFormat(fromToParticipant.from), fromMe: message.key.fromMe, + source: source, body: body || null, to: toCusFormat(fromToParticipant.to), participant: toCusFormat(fromToParticipant.participant), @@ -2074,9 +2080,11 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { quoted = await this.store.loadMessage(jid, key.id); } const chat = await this.store.getChat(jid); + const messageId = this.generateMessageID(); return { quoted: quoted, ephemeralExpiration: chat?.ephemeralExpiration, + messageId: messageId, }; } @@ -2095,6 +2103,12 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { } return linkPreview; } + + protected generateMessageID() { + const id = generateMessageIDV2(this.sock.user?.id); + this.saveSentMessageId(id); + return id; + } } function hasPath(url: string) { diff --git a/src/core/engines/webjs/WebjsClientCore.ts b/src/core/engines/webjs/WebjsClientCore.ts index ce6d3bcd..768da344 100644 --- a/src/core/engines/webjs/WebjsClientCore.ts +++ b/src/core/engines/webjs/WebjsClientCore.ts @@ -2,9 +2,11 @@ import { GetChatMessagesFilter } from '@waha/structures/chats.dto'; import { Label } from '@waha/structures/labels.dto'; import { PaginationParams } from '@waha/structures/pagination.dto'; import { TextStatus } from '@waha/structures/status.dto'; +import { EventEmitter } from 'events'; import * as lodash from 'lodash'; import { Client, Events } from 'whatsapp-web.js'; import { Message } from 'whatsapp-web.js/src/structures'; +import { exposeFunctionIfAbsent } from 'whatsapp-web.js/src/util/Puppeter'; // eslint-disable-next-line @typescript-eslint/no-var-requires const { LoadWAHA } = require('./_WAHA.js'); @@ -19,10 +21,13 @@ const { LoadPaginator } = require('./_Paginator.js'); const ChatFactory = require('whatsapp-web.js/src/factories/ChatFactory'); export class WebjsClientCore extends Client { + public events = new EventEmitter(); + constructor(options) { super(options); // Wait until it's READY and inject more utils this.on(Events.READY, async () => { + await this.attachCustomEventListeners(); await this.injectWaha(); }); } @@ -33,6 +38,22 @@ export class WebjsClientCore extends Client { await this.pupPage.evaluate(LoadWAHA); } + async attachCustomEventListeners() { + await exposeFunctionIfAbsent( + this.pupPage, + 'onNewMessageId', + (messageId: string) => { + this.events.emit('message.id', { id: messageId }); + return; + }, + ); + } + + async destroy() { + this.events.removeAllListeners(); + await super.destroy(); + } + async setPushName(name: string) { await this.pupPage.evaluate(async (pushName) => { return await window['WAHA'].WAWebSetPushnameConnAction.setPushname( diff --git a/src/core/engines/webjs/session.webjs.core.ts b/src/core/engines/webjs/session.webjs.core.ts index 19045973..ba938507 100644 --- a/src/core/engines/webjs/session.webjs.core.ts +++ b/src/core/engines/webjs/session.webjs.core.ts @@ -73,7 +73,11 @@ import { import { Label, LabelDTO, LabelID } from '@waha/structures/labels.dto'; import { ReplyToMessage } from '@waha/structures/message.dto'; import { PaginationParams, SortOrder } from '@waha/structures/pagination.dto'; -import { WAMessage, WAMessageReaction } from '@waha/structures/responses.dto'; +import { + MessageSource, + WAMessage, + WAMessageReaction, +} from '@waha/structures/responses.dto'; import { MeInfo } from '@waha/structures/sessions.dto'; import { StatusRequest, TextStatus } from '@waha/structures/status.dto'; import { @@ -1154,6 +1158,11 @@ export class WhatsappSessionWebJSCore extends WhatsappSession { * END - Methods for API */ subscribeEngineEvents2() { + // Save sent message in cache + this.whatsapp.events.on('message.id', (data) => { + this.saveSentMessageId(data.id); + }); + // // All // @@ -1353,10 +1362,12 @@ export class WhatsappSessionWebJSCore extends WhatsappSession { } private processMessageReaction(reaction: Reaction): WAMessageReaction { + const source = this.getMessageSource(reaction.id.id); return { id: reaction.id._serialized, from: reaction.senderId, fromMe: reaction.id.fromMe, + source: source, participant: reaction.senderId, to: reaction.id.remote, timestamp: reaction.timestamp, @@ -1369,12 +1380,14 @@ export class WhatsappSessionWebJSCore extends WhatsappSession { protected toWAMessage(message: Message): WAMessage { const replyTo = this.extractReplyTo(message); + const source = this.getMessageSource(message.id.id); // @ts-ignore return { id: message.id._serialized, timestamp: message.timestamp, from: message.from, fromMe: message.fromMe, + source: source, to: message.to, body: message.body, // Media diff --git a/src/structures/responses.dto.ts b/src/structures/responses.dto.ts index 56299084..607c014a 100644 --- a/src/structures/responses.dto.ts +++ b/src/structures/responses.dto.ts @@ -11,6 +11,11 @@ export class WALocation { longitude: string; } +export enum MessageSource { + API = 'api', + APP = 'app', +} + class WAMessageBase { @MessageIdProperty() id: string; @@ -33,6 +38,13 @@ class WAMessageBase { }) fromMe: boolean; + @ApiProperty({ + description: + 'The device that sent the message - either API or APP. Available in events (webhooks/websockets) only and only "fromMe: true" messages.', + example: MessageSource.API, + }) + source: MessageSource; + @ChatIdProperty({ description: ` * ID for who this message is for. diff --git a/yarn.lock b/yarn.lock index 99678cc1..f8c1e918 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12931,7 +12931,7 @@ __metadata: "whatsapp-web.js@github:devlikeapro/whatsapp-web.js#fork-main-channels": version: 1.26.0 - resolution: "whatsapp-web.js@https://github.com/devlikeapro/whatsapp-web.js.git#commit=bb4525a80667d83122108a03d936e41cefd179a4" + resolution: "whatsapp-web.js@https://github.com/devlikeapro/whatsapp-web.js.git#commit=c73f3cfb4e1290103ee946ca7f56d5638f1a66bd" dependencies: "@pedroslopez/moduleraid": ^5.0.2 archiver: ^5.3.1 @@ -12949,7 +12949,7 @@ __metadata: optional: true unzipper: optional: true - checksum: a06d30c9f52a1ff0268e4e98e3c0d80112a5dc8a9445d0df460fac3cbe0168b6f1c5ad494518b1e9a98acb54da8c46a2f6644e9f41b1b2987771fac101747c55 + checksum: 831a4a2819041f63c2a51a98e1ad7dd9ff8474b1005b11314b613c6dadac545f3c780e38081b7d260d32afa9ffd333a25f9c6407b630a0660bd08dadbc8ead89 languageName: node linkType: hard