From 060aa8229478acb5e48758487734793ff41e3391 Mon Sep 17 00:00:00 2001 From: devlikepro Date: Wed, 28 May 2025 11:47:54 +0700 Subject: [PATCH] [core] WEBJS - Presence - API and Events fix #636 --- src/core/engines/webjs/WebjsClientCore.ts | 53 +++++++++++++ src/core/engines/webjs/ack.webjs.ts | 2 +- src/core/engines/webjs/presence.ts | 67 +++++++++++++++++ src/core/engines/webjs/session.webjs.core.ts | 79 ++++++++++++++++++-- src/core/engines/webjs/types.ts | 12 +++ yarn.lock | 4 +- 6 files changed, 208 insertions(+), 9 deletions(-) create mode 100644 src/core/engines/webjs/presence.ts create mode 100644 src/core/engines/webjs/types.ts diff --git a/src/core/engines/webjs/WebjsClientCore.ts b/src/core/engines/webjs/WebjsClientCore.ts index 8dda5c46..d9366a3e 100644 --- a/src/core/engines/webjs/WebjsClientCore.ts +++ b/src/core/engines/webjs/WebjsClientCore.ts @@ -1,8 +1,10 @@ +import { WebJSPresence } from '@waha/core/engines/webjs/types'; import { GetChatMessagesFilter } from '@waha/structures/chats.dto'; import { Label } from '@waha/structures/labels.dto'; import { LidToPhoneNumber } from '@waha/structures/lids.dto'; import { PaginationParams } from '@waha/structures/pagination.dto'; import { TextStatus } from '@waha/structures/status.dto'; +import { sleep } from '@waha/utils/promiseTimeout'; import { EventEmitter } from 'events'; import * as lodash from 'lodash'; import { Page } from 'puppeteer'; @@ -360,4 +362,55 @@ export class WebjsClientCore extends Client { }, phoneNumber)) as any; return lid; } + + /** + * Presences methods + */ + public async subscribePresence(chatId: string): Promise { + await this.pupPage.evaluate(async (chatId) => { + const d = require; + const WidFactory = d('WAWebWidFactory'); + + const wid = WidFactory.createWidFromWidLike(chatId); + const chat = d('WAWebChatCollection').ChatCollection.get(wid); + const tc = chat == null ? void 0 : chat.getTcToken(); + await d('WAWebContactPresenceBridge').subscribePresence(wid, tc); + }, chatId); + } + + private async getCurrentPresence(chatId: string): Promise { + const result = await this.pupPage.evaluate(async (chatId) => { + const d = require; + const WidFactory = d('WAWebWidFactory'); + const PresenceCollection = d( + 'WAWebPresenceCollection', + ).PresenceCollection; + const wid = WidFactory.createWidFromWidLike(chatId); + const presence = PresenceCollection.get(wid); + if (!presence) { + return []; + } + let chatstates = []; + if (chatId.endsWith('@c.us')) { + chatstates = [presence.chatstate]; + } else { + chatstates = presence.chatstates.getModelsArray(); + } + return chatstates.map((chatstate) => { + return { + participant: chatstate.id._serialized, + lastSeen: chatstate.t, + state: chatstate.type, + }; + }); + }, chatId); + return result; + } + + public async getPresence(chatId: string): Promise { + await this.sendPresenceAvailable(); + await this.subscribePresence(chatId); + await sleep(3_000); + return await this.getCurrentPresence(chatId); + } } diff --git a/src/core/engines/webjs/ack.webjs.ts b/src/core/engines/webjs/ack.webjs.ts index 7b0cfd53..2b2b84af 100644 --- a/src/core/engines/webjs/ack.webjs.ts +++ b/src/core/engines/webjs/ack.webjs.ts @@ -23,7 +23,7 @@ interface Me { lid?: string; } -function jid(field: any) { +export function jid(field: any) { if (!field) { return field; } diff --git a/src/core/engines/webjs/presence.ts b/src/core/engines/webjs/presence.ts new file mode 100644 index 00000000..01a96cbc --- /dev/null +++ b/src/core/engines/webjs/presence.ts @@ -0,0 +1,67 @@ +import { BinaryNode, WAPresence } from '@adiwajshing/baileys'; +import { toCusFormat } from '@waha/core/engines/noweb/session.noweb.core'; +import { WAHAPresenceStatus } from '@waha/structures/enums.dto'; +import { WAHAChatPresences } from '@waha/structures/presence.dto'; + +import { jid } from './ack.webjs'; + +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 d60c9425..09b765a6 100644 --- a/src/core/engines/webjs/session.webjs.core.ts +++ b/src/core/engines/webjs/session.webjs.core.ts @@ -19,6 +19,10 @@ import { ToGroupV2UpdateEvent, } from '@waha/core/engines/webjs/groups.webjs'; import { LocalAuth } from '@waha/core/engines/webjs/LocalAuth'; +import { + TagChatstateToPresence, + TagPresenceToPresence, +} from '@waha/core/engines/webjs/presence'; import { WebjsClientCore } from '@waha/core/engines/webjs/WebjsClientCore'; import { CallErrorEvent, @@ -98,10 +102,10 @@ import { LidToPhoneNumber } from '@waha/structures/lids.dto'; import { ReplyToMessage } from '@waha/structures/message.dto'; import { PaginationParams, SortOrder } from '@waha/structures/pagination.dto'; import { - MessageSource, - WAMessage, - WAMessageReaction, -} from '@waha/structures/responses.dto'; + WAHAChatPresences, + WAHAPresenceData, +} from '@waha/structures/presence.dto'; +import { WAMessage, WAMessageReaction } from '@waha/structures/responses.dto'; import { MeInfo } from '@waha/structures/sessions.dto'; import { StatusRequest, TextStatus } from '@waha/structures/status.dto'; import { @@ -114,7 +118,7 @@ import { sleep, waitUntil } from '@waha/utils/promiseTimeout'; import { SingleDelayedJobRunner } from '@waha/utils/SingleDelayedJobRunner'; import * as lodash from 'lodash'; import { ProtocolError } from 'puppeteer'; -import { filter, fromEvent, merge, mergeMap, Observable } from 'rxjs'; +import { filter, fromEvent, merge, mergeMap, Observable, share } from 'rxjs'; import { map } from 'rxjs/operators'; import { AuthStrategy, @@ -129,13 +133,14 @@ import { Label as WEBJSLabel, Location, Message, - MessageAck, MessageMedia, Reaction, WAState, } from 'whatsapp-web.js'; import { Message as MessageInstance } from 'whatsapp-web.js/src/structures'; +import { WAJSPresenceChatStateType, WebJSPresence } from './types'; + export interface WebJSConfig { webVersion?: string; cacheType: 'local' | 'none'; @@ -1284,6 +1289,52 @@ export class WhatsappSessionWebJSCore extends WhatsappSession { } } + public getPresences(): Promise { + throw new NotImplementedByEngineError(); + } + + public async getPresence(id: string): Promise { + const chatId = toCusFormat(id); + const presences = await this.whatsapp.getPresence(chatId); + return this.toWahaPresences(chatId, presences); + } + + public async subscribePresence(id: string): Promise { + const chatId = toCusFormat(id); + await this.whatsapp.subscribePresence(chatId); + } + + private toWahaPresences( + chatId: string, + data: WebJSPresence[], + ): WAHAChatPresences { + const presences: WAHAPresenceData[] = data.map((presence) => { + let status: WAHAPresenceStatus = WAHAPresenceStatus.OFFLINE; + switch (presence.state) { + case WAJSPresenceChatStateType.AVAILABLE: + status = WAHAPresenceStatus.ONLINE; + break; + case WAJSPresenceChatStateType.UNAVAILABLE: + status = WAHAPresenceStatus.OFFLINE; + break; + case WAJSPresenceChatStateType.TYPING: + status = WAHAPresenceStatus.TYPING; + break; + case WAJSPresenceChatStateType.RECORDING_AUDIO: + status = WAHAPresenceStatus.RECORDING; + } + return { + participant: presence.participant, + lastSeen: presence.lastSeen || null, + lastKnownPresence: status, + }; + }); + return { + id: toCusFormat(chatId), + presences: presences, + }; + } + /** * Status methods */ @@ -1413,6 +1464,22 @@ export class WhatsappSessionWebJSCore extends WhatsappSession { const stateChanged$ = fromEvent(this.whatsapp, Events.STATE_CHANGED); this.events2.get(WAHAEvents.STATE_CHANGE).switch(stateChanged$); + // + // Presence + // + const tagPresenceNode$ = fromEvent(this.whatsapp, Events.TAG_PRESENCE); + const presences$ = tagPresenceNode$.pipe( + map(TagPresenceToPresence), + filter(Boolean), + ); + const tagChatstateNode$ = fromEvent(this.whatsapp, 'tag:chatstate'); + const chatstatePresences$ = tagChatstateNode$.pipe( + map(TagChatstateToPresence), + filter(Boolean), + ); + const presenceUpdate$ = merge(presences$, chatstatePresences$); + this.events2.get(WAHAEvents.PRESENCE_UPDATE).switch(presenceUpdate$); + // // Groups // diff --git a/src/core/engines/webjs/types.ts b/src/core/engines/webjs/types.ts new file mode 100644 index 00000000..32da159f --- /dev/null +++ b/src/core/engines/webjs/types.ts @@ -0,0 +1,12 @@ +export enum WAJSPresenceChatStateType { + AVAILABLE = 'available', // User is online + UNAVAILABLE = 'unavailable', // User went offline (may include `t`) + TYPING = 'typing', // User is typing text + RECORDING_AUDIO = 'recording_audio', // User is recording a voice message +} + +export interface WebJSPresence { + participant: string; + lastSeen?: number; + state: WAJSPresenceChatStateType; +} diff --git a/yarn.lock b/yarn.lock index 7a1dc066..a2621507 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13488,7 +13488,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=ae0d467885ac832a4c5804f736f9da226148f3c0" + resolution: "whatsapp-web.js@https://github.com/devlikeapro/whatsapp-web.js.git#commit=26562719b4068713d588b7e8d51652a9f0e4f683" dependencies: "@pedroslopez/moduleraid": ^5.0.2 archiver: ^5.3.1 @@ -13506,7 +13506,7 @@ __metadata: optional: true unzipper: optional: true - checksum: 60ef6d7d5a19a7575d9eddd9e4a42049715619e6f6e92a6d8558909fbff1de9924b2d82eb8dcbea21598e256335a967be5376421917dd01f1f391bc71bb46387 + checksum: e1fe71ce6959542ec964b1d0e190071f0796c37459732c641b0b5be719383415b9f1008dab443713b20caf0562fecd3c005be5f64b3f992b56587122029666cd languageName: node linkType: hard