diff --git a/src/core/engines/webjs/WebjsClientCore.ts b/src/core/engines/webjs/WebjsClientCore.ts index 5874839c..b4cb5577 100644 --- a/src/core/engines/webjs/WebjsClientCore.ts +++ b/src/core/engines/webjs/WebjsClientCore.ts @@ -1,4 +1,7 @@ -import { WebJSPresence } from '@waha/core/engines/webjs/types'; +import { + WebJSPresence, + WebJSPresenceUpdate, +} from '@waha/core/engines/webjs/types'; import { GetSerialized } from '@waha/core/utils/serialized'; import { GetChatMessagesFilter } from '@waha/structures/chats.dto'; import { Label } from '@waha/structures/labels.dto'; @@ -198,11 +201,111 @@ export class WebjsClientCore extends Client { return; }, ); + await exposeFunctionIfAbsent( + this.pupPage, + 'onPresenceUpdate', + (data: WebJSPresenceUpdate) => { + this.events.emit('presence.update', data); + return; + }, + ); + await this.attachPresenceEvents(); if (this.tags) { await this.attachTagsEvents(); } } + /** + * Presence lands in PresenceCollection on the main thread even when WhatsApp runs comms in a worker, + * so listen to the models instead of raw stanzas + */ + async attachPresenceEvents() { + await this.pupPage.evaluate(() => { + // @ts-ignore + if (window.presenceEventsOn) { + return; + } + // @ts-ignore + window.presenceEventsOn = true; + + const d = require; + const PresenceCollection = d( + 'WAWebPresenceCollection', + ).PresenceCollection; + const WAWebApiContact = d('WAWebApiContact'); + + // Prefer the phone number id when the LID mapping is known + const toId = (wid) => { + let id = wid; + if (wid.isLid()) { + id = WAWebApiContact.getPhoneNumber(wid) ?? wid; + } + // @ts-ignore + return window.WWebJS.GetSerialized(id); + }; + + const toPresences = (presence) => { + if (!presence.isGroup) { + return [ + { + participant: toId(presence.id), + lastSeen: presence.chatstate.t, + state: presence.chatstate.type, + }, + ]; + } + return presence.chatstates + .getModelsArray() + .filter((chatstate) => !!chatstate.type) + .map((chatstate) => { + return { + participant: toId(chatstate.id), + lastSeen: chatstate.t, + state: chatstate.type, + }; + }); + }; + + // type and t change in one set() - coalesce into a single event per presence + const pending = new Set(); + const flush = () => { + for (const presence of pending) { + // @ts-ignore + window.onPresenceUpdate({ + id: toId(presence.id), + presences: toPresences(presence), + }); + } + pending.clear(); + }; + + const findPresence = (chatstate) => { + const presence = PresenceCollection.get(chatstate.id); + if (presence && presence.chatstate === chatstate) { + return presence; + } + // Group chatstate id is the participant, not the group + return PresenceCollection.getModelsArray().find( + (model) => model.chatstate === chatstate, + ); + }; + + PresenceCollection.on( + 'change:chatstate.type change:chatstate.t', + (chatstate) => { + const presence = findPresence(chatstate); + if (!presence || !presence.hasData) { + return; + } + if (pending.size === 0) { + queueMicrotask(flush); + } + pending.add(presence); + }, + ); + }); + } + async attachTagsEvents() { await this.pupPage.evaluate(() => { // @ts-ignore @@ -210,7 +313,7 @@ export class WebjsClientCore extends Client { return; } - const tags = ['receipt', 'presence', 'chatstate']; + const tags = ['receipt']; const WAWap = window.require('WAWap'); // @ts-ignore window.decodeStanzaBack = WAWap.decodeStanza; diff --git a/src/core/engines/webjs/presence.ts b/src/core/engines/webjs/presence.ts deleted file mode 100644 index c2da2d5a..00000000 --- a/src/core/engines/webjs/presence.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type { BinaryNode, WAPresence } from '@adiwajshing/baileys'; -import { WAHAPresenceStatus } from '@waha/structures/enums.dto'; -import { WAHAChatPresences } from '@waha/structures/presence.dto'; - -import { jid } from './ack.webjs'; -import { toCusFormat } from '@waha/core/utils/jids'; - -export function TagPresenceToPresence(node: BinaryNode): WAHAChatPresences { - const { attrs } = node; - const id = jid(attrs.from); - const state = - attrs.type === 'unavailable' - ? WAHAPresenceStatus.OFFLINE - : WAHAPresenceStatus.ONLINE; - const lastSeen = attrs.last && attrs.last !== 'deny' ? +attrs.last : null; - return { - id: toCusFormat(id), - presences: [ - { - participant: toCusFormat(id), - lastKnownPresence: state, - lastSeen: lastSeen, - }, - ], - }; -} - -export function TagChatstateToPresence(node: BinaryNode): WAHAChatPresences { - const { attrs, content } = node; - const id = jid(attrs.from); - const participant = jid(attrs.participant) || jid(attrs.from); - - const firstChild = content[0] as BinaryNode; - - const type = firstChild.tag as WAPresence; - let status = WAHAPresenceStatus.OFFLINE; - - switch (type) { - case 'unavailable': - status = WAHAPresenceStatus.OFFLINE; - break; - case 'available': - status = WAHAPresenceStatus.ONLINE; - break; - case 'paused': - status = WAHAPresenceStatus.PAUSED; - break; - case 'composing': - status = WAHAPresenceStatus.TYPING; - break; - } - - if (firstChild.attrs?.media === 'audio') { - status = WAHAPresenceStatus.RECORDING; - } - - return { - id: toCusFormat(id), - presences: [ - { - participant: toCusFormat(participant), - lastKnownPresence: status, - lastSeen: null, - }, - ], - }; -} diff --git a/src/core/engines/webjs/session.webjs.core.ts b/src/core/engines/webjs/session.webjs.core.ts index df26f3af..f8942372 100644 --- a/src/core/engines/webjs/session.webjs.core.ts +++ b/src/core/engines/webjs/session.webjs.core.ts @@ -24,10 +24,6 @@ import { ToGroupV2ParticipantsEvent, ToGroupV2UpdateEvent, } from '@waha/core/engines/webjs/groups.webjs'; -import { - TagChatstateToPresence, - TagPresenceToPresence, -} from '@waha/core/engines/webjs/presence'; import { WebjsChannelMessage, WebjsClientCore, @@ -208,7 +204,11 @@ import { } from 'whatsapp-web.js/src/structures'; import { GetSerialized } from '@waha/core/utils/serialized'; -import { WAJSPresenceChatStateType, WebJSPresence } from './types'; +import { + WAJSPresenceChatStateType, + WebJSPresence, + WebJSPresenceUpdate, +} from './types'; import { WebJSAuthFactory } from './WebJSAuthFactory'; import { isJidCus, @@ -351,7 +351,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession { protected getWebjsTagsFlag() { // Emit 'tag:*' events only when explicitly enabled in session config. - // This flag is required for presence.update and message.ack events. + // This flag is required for message.ack events. // Disabled by default for performance and stability reasons. return !!this.sessionConfig?.webjs?.tagsEventsOn; } @@ -2504,19 +2504,15 @@ export class WhatsappSessionWebJSCore extends WhatsappSession { // // Presence // - const tagPresenceNode$ = fromEvent(this.whatsapp, Events.TAG_PRESENCE); - const presences$ = tagPresenceNode$.pipe( - map(TagPresenceToPresence), - filter(Boolean), - filter((presence: any) => this.jids.include(presence.id)), + const presenceUpdate$ = fromEvent( + this.whatsapp.events, + 'presence.update', + ).pipe( + map((data: WebJSPresenceUpdate) => + this.toWahaPresences(data.id, data.presences), + ), + filter((presence) => this.jids.include(presence.id)), ); - const tagChatstateNode$ = fromEvent(this.whatsapp, 'tag:chatstate'); - const chatstatePresences$ = tagChatstateNode$.pipe( - map(TagChatstateToPresence), - filter(Boolean), - filter((presence: any) => this.jids.include(presence.id)), - ); - const presenceUpdate$ = merge(presences$, chatstatePresences$); this.events2.get(WAHAEvents.PRESENCE_UPDATE).switch(presenceUpdate$); // diff --git a/src/core/engines/webjs/types.ts b/src/core/engines/webjs/types.ts index 32da159f..e15c5a68 100644 --- a/src/core/engines/webjs/types.ts +++ b/src/core/engines/webjs/types.ts @@ -10,3 +10,8 @@ export interface WebJSPresence { lastSeen?: number; state: WAJSPresenceChatStateType; } + +export interface WebJSPresenceUpdate { + id: string; + presences: WebJSPresence[]; +}