From bc5f864dd4df6068db1a0c4a503db10c7c603565 Mon Sep 17 00:00:00 2001 From: Dolav Date: Wed, 15 Jul 2026 23:02:54 -0700 Subject: [PATCH 1/2] feat(client): add sendPresenceUpdate to Client to bypass database lookups --- index.d.ts | 15 +++++++++++---- src/Client.js | 16 ++++++++++++++++ tests/client.js | 10 ++++++++++ 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/index.d.ts b/index.d.ts index 8127770492b..f06a3cf1d03 100644 --- a/index.d.ts +++ b/index.d.ts @@ -212,10 +212,17 @@ declare namespace WAWebJS { ): Promise; /** Send a reaction to a specific messageId */ - sendReaction( - messageId: string, - reaction: string, - ): Promise; + sendReaction(messageId: string, reaction: string): Promise; + + /** + * Sends a presence state to a specific chat without fetching the chat from the database. + * @param {string} chatId The ID of the chat + * @param {string} state The presence state (e.g., 'typing', 'recording', 'paused') + */ + sendPresenceUpdate( + chatId: string, + state?: 'typing' | 'recording' | 'paused', + ): Promise; /** Sends a channel admin invitation to a user, allowing them to become an admin of the channel */ sendChannelAdminInvite( diff --git a/src/Client.js b/src/Client.js index a122a8f336b..7dbccbdb147 100644 --- a/src/Client.js +++ b/src/Client.js @@ -1645,6 +1645,22 @@ class Client extends EventEmitter { ); } + /** + * Sends a presence state to a specific chat without fetching the chat from the database. + * @param {string} chatId The ID of the chat + * @param {string} state The presence state (e.g., 'typing', 'recording', 'paused') + */ + async sendPresenceUpdate(chatId, state = 'typing') { + return this.pupPage.evaluate( + (chatId, state) => { + window.WWebJS.sendChatstate(state, chatId); + return true; + }, + chatId, + state, + ); + } + /** * @typedef {Object} SendChannelAdminInviteOptions * @property {?string} comment The comment to be added to an invitation diff --git a/tests/client.js b/tests/client.js index c0de50adb06..120018a3082 100644 --- a/tests/client.js +++ b/tests/client.js @@ -479,6 +479,16 @@ END:VCARD`; }); }); + describe('Presence Updates', function () { + it('can send a typing presence update via Client', async function () { + const result = await client.sendPresenceUpdate( + remoteId, + 'typing', + ); + expect(result).to.be.true; + }); + }); + describe('Search messages', function () { it('can search for messages', async function () { const m1 = await client.sendMessage( From 2695a82774b3a1a525b53e66b4fdbbe99f3c5708 Mon Sep 17 00:00:00 2001 From: Dolav Date: Wed, 15 Jul 2026 23:06:15 -0700 Subject: [PATCH 2/2] docs: regenerate documentation for sendPresenceUpdate --- docs/Base.html | 2 +- docs/BaseAuthStrategy.html | 2 +- docs/Broadcast.html | 2 +- docs/BusinessContact.html | 2 +- docs/Buttons.html | 2 +- docs/Call.html | 2 +- docs/Channel.html | 2 +- docs/Chat.html | 2 +- docs/Client.html | 160 +++-- docs/Client.js.html | 670 +++++++++++-------- docs/ClientInfo.html | 2 +- docs/Contact.html | 2 +- docs/GroupChat.html | 2 +- docs/GroupNotification.html | 2 +- docs/InterfaceController.html | 2 +- docs/Label.html | 2 +- docs/List.html | 2 +- docs/LocalAuth.html | 2 +- docs/LocalWebCache.html | 2 +- docs/Location.html | 2 +- docs/Message.html | 86 ++- docs/MessageMedia.html | 2 +- docs/NoAuth.html | 2 +- docs/Order.html | 2 +- docs/Poll.html | 2 +- docs/PollVote.html | 2 +- docs/PrivateChat.html | 2 +- docs/PrivateContact.html | 2 +- docs/Product.html | 2 +- docs/Reaction.html | 2 +- docs/RemoteAuth.html | 2 +- docs/RemoteWebCache.html | 2 +- docs/ScheduledEvent.html | 2 +- docs/Util.html | 2 +- docs/WebCache.html | 2 +- docs/authStrategies_BaseAuthStrategy.js.html | 2 +- docs/authStrategies_LocalAuth.js.html | 2 +- docs/authStrategies_NoAuth.js.html | 2 +- docs/authStrategies_RemoteAuth.js.html | 2 +- docs/global.html | 2 +- docs/index.html | 62 +- docs/structures_Base.js.html | 2 +- docs/structures_Broadcast.js.html | 2 +- docs/structures_BusinessContact.js.html | 2 +- docs/structures_Buttons.js.html | 2 +- docs/structures_Call.js.html | 2 +- docs/structures_Channel.js.html | 2 +- docs/structures_Chat.js.html | 2 +- docs/structures_ClientInfo.js.html | 2 +- docs/structures_Contact.js.html | 44 +- docs/structures_GroupChat.js.html | 2 +- docs/structures_GroupNotification.js.html | 2 +- docs/structures_Label.js.html | 2 +- docs/structures_List.js.html | 2 +- docs/structures_Location.js.html | 2 +- docs/structures_Message.js.html | 151 +++-- docs/structures_MessageMedia.js.html | 2 +- docs/structures_Order.js.html | 2 +- docs/structures_Payment.js.html | 2 +- docs/structures_Poll.js.html | 2 +- docs/structures_PollVote.js.html | 2 +- docs/structures_PrivateChat.js.html | 2 +- docs/structures_PrivateContact.js.html | 2 +- docs/structures_Product.js.html | 2 +- docs/structures_ProductMetadata.js.html | 2 +- docs/structures_Reaction.js.html | 2 +- docs/structures_ScheduledEvent.js.html | 2 +- docs/util_Constants.js.html | 2 +- docs/util_Injected_Utils.js.html | 59 +- docs/util_InterfaceController.js.html | 2 +- docs/util_Puppeteer.js.html | 2 +- docs/util_Util.js.html | 2 +- docs/webCache_LocalWebCache.js.html | 2 +- docs/webCache_RemoteWebCache.js.html | 2 +- docs/webCache_WebCache.js.html | 2 +- 75 files changed, 803 insertions(+), 565 deletions(-) diff --git a/docs/Base.html b/docs/Base.html index a32d5f5e9ea..1223ca5bbd8 100644 --- a/docs/Base.html +++ b/docs/Base.html @@ -50,7 +50,7 @@

new Base

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/BaseAuthStrategy.html b/docs/BaseAuthStrategy.html index 89cd05c6e1a..4ea772eceff 100644 --- a/docs/BaseAuthStrategy.html +++ b/docs/BaseAuthStrategy.html @@ -50,7 +50,7 @@

new BaseAuthStrategy

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/Broadcast.html b/docs/Broadcast.html index da86c0ebe52..b6928ae8551 100644 --- a/docs/Broadcast.html +++ b/docs/Broadcast.html @@ -152,7 +152,7 @@

getContact

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/BusinessContact.html b/docs/BusinessContact.html index 6237700cbd0..87cd147c1a9 100644 --- a/docs/BusinessContact.html +++ b/docs/BusinessContact.html @@ -338,7 +338,7 @@

unblock

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/Buttons.html b/docs/Buttons.html index 57a22954d79..6a2bc03ba34 100644 --- a/docs/Buttons.html +++ b/docs/Buttons.html @@ -234,7 +234,7 @@

Parameter

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/Call.html b/docs/Call.html index a17e61b7043..802d54cbbaf 100644 --- a/docs/Call.html +++ b/docs/Call.html @@ -168,7 +168,7 @@

reject<

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/Channel.html b/docs/Channel.html index ecc244fd35e..a9f50dfc5c1 100644 --- a/docs/Channel.html +++ b/docs/Channel.html @@ -900,7 +900,7 @@

unmute<

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/Chat.html b/docs/Chat.html index 0f92ef1d0b0..3faf97b929c 100644 --- a/docs/Chat.html +++ b/docs/Chat.html @@ -644,7 +644,7 @@

unpin

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/Client.html b/docs/Client.html index 9956aebb1a0..c91689f9cc7 100644 --- a/docs/Client.html +++ b/docs/Client.html @@ -23,7 +23,7 @@
-
class

Client

Source: Client.js:82

+
class

Client

Source: Client.js:81

Starting point for interacting with the WhatsApp Web API

@@ -285,6 +285,9 @@

Methods

sendPresenceUnavailable()
+
sendPresenceUpdate(chatId, state)
+
+
sendReaction(messageId, reaction)
@@ -371,9 +374,6 @@

Events

chat_removed
-
code
-
-
contact_changed
@@ -415,14 +415,14 @@

Events

message_ciphertext
-
message_ciphertext_failed
-
-
+
message_ciphertext_failed
+
+
message_create
@@ -5006,6 +5006,85 @@

Parameters

+ + + + +
+ +
async

sendPresenceUpdate(chatId, state)

Sends a presence state to a specific chat without fetching the chat from the database.

+ + +
+

Parameters

+ + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeOptionalDescription
+

chatId

+
+

string

+
+

 

+
+

The ID of the chat

+
+

state

+
+

string

+
+

 

+
+

The presence state (e.g., 'typing', 'recording', 'paused')

+

Defaults to typing.

+
+ +
+ + + +
+ + + + + + + + + + + + + + + + + + + @@ -6600,71 +6679,6 @@

Parameter

- - - -
- -

code

Emitted when a pairing code is received

- - -
-

Parameter

- - - - - - - - - - - - - - - - - - -
NameTypeOptionalDescription
-

code

-
-

string

-
-

 

-
-

Code

-
- -
- - - -
- - - - - - - - - - - - - - - - - - - - -
Returns
-

string 

Code that was just received

@@ -8390,7 +8404,7 @@

vote_update

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/Client.js.html b/docs/Client.js.html index 5a9a3d3dcec..4604c40b74f 100644 --- a/docs/Client.js.html +++ b/docs/Client.js.html @@ -42,7 +42,6 @@

Source: Client.js

WAState, MessageTypes, } = require('./util/Constants'); -const { ExposeAuthStore } = require('./util/Injected/AuthStore/AuthStore'); const { LoadUtils } = require('./util/Injected/Utils'); const ChatFactory = require('./factories/ChatFactory'); const ContactFactory = require('./factories/ContactFactory'); @@ -143,314 +142,355 @@

Source: Client.js

* Private function */ async inject() { - if ( - this.options.authTimeoutMs === undefined || - this.options.authTimeoutMs == 0 - ) { - this.options.authTimeoutMs = 30000; - } - let start = Date.now(); - let timeout = this.options.authTimeoutMs; - let res = false; - while (start > Date.now() - timeout) { - res = await this.pupPage.evaluate( - 'window.Debug?.VERSION != undefined', + // Cancel any previous inject still running + if (this._injectAbort) this._injectAbort.abort(); + const abort = new AbortController(); + this._injectAbort = abort; + + try { + const authTimeout = this.options.authTimeoutMs || 30000; + await this.pupPage + .waitForFunction('window.Debug?.VERSION != undefined', { + timeout: authTimeout, + signal: abort.signal, + }) + .catch((err) => { + if (abort.signal.aborted) throw err; + throw 'auth timeout'; + }); + if (abort.signal.aborted) return; + await this.setDeviceName( + this.options.deviceName, + this.options.browserName, ); - if (res) { - break; - } - await new Promise((r) => setTimeout(r, 200)); - } - if (!res) { - throw 'auth timeout'; - } - await this.setDeviceName( - this.options.deviceName, - this.options.browserName, - ); - const pairWithPhoneNumber = this.options.pairWithPhoneNumber; - const version = await this.getWWebVersion(); + const pairWithPhoneNumber = this.options.pairWithPhoneNumber; + const version = await this.getWWebVersion(); - await this.pupPage.evaluate(ExposeAuthStore); + const needAuthHandle = await this.pupPage.waitForFunction( + () => { + const state = + window.require?.('WAWebSocketModel')?.Socket?.state; + if ( + !state || + state === 'OPENING' || + state === 'UNLAUNCHED' || + state === 'PAIRING' + ) { + return false; + } + return { + need: state === 'UNPAIRED' || state === 'UNPAIRED_IDLE', + state, + }; + }, + { timeout: authTimeout }, + ); + const needAuthentication = await needAuthHandle.jsonValue(); - const needAuthentication = await this.pupPage.evaluate(async () => { - let state = window.require('WAWebSocketModel').Socket.state; + if (needAuthentication.need) { + const { failed, failureEventPayload, restart } = + await this.authStrategy.onAuthenticationNeeded(); - if ( - state === 'OPENING' || - state === 'UNLAUNCHED' || - state === 'PAIRING' - ) { - // wait till state changes - await new Promise((r) => { - window - .require('WAWebSocketModel') - .Socket.on( - 'change:state', - function waitTillInit(_AppState, state) { - if ( - state !== 'OPENING' &amp;&amp; - state !== 'UNLAUNCHED' &amp;&amp; - state !== 'PAIRING' - ) { - window - .require('WAWebSocketModel') - .Socket.off( - 'change:state', - waitTillInit, - ); - r(); - } - }, - ); - }); - } - state = window.require('WAWebSocketModel').Socket.state; - return state == 'UNPAIRED' || state == 'UNPAIRED_IDLE'; - }); + if (failed) { + /** + * Emitted when there has been an error while trying to restore an existing session + * @event Client#auth_failure + * @param {string} message + */ + this.emit( + Events.AUTHENTICATION_FAILURE, + failureEventPayload, + ); + await this.destroy(); + if (restart) { + // session restore failed so try again but without session to force new authentication + return this.initialize(); + } + return; + } - if (needAuthentication) { - const { failed, failureEventPayload, restart } = - await this.authStrategy.onAuthenticationNeeded(); + // Register qr/code events + if (pairWithPhoneNumber.phoneNumber) { + this.requestPairingCode( + pairWithPhoneNumber.phoneNumber, + pairWithPhoneNumber.showNotification, + pairWithPhoneNumber.intervalMs, + ); + } else { + let qrRetries = 0; - if (failed) { - /** - * Emitted when there has been an error while trying to restore an existing session - * @event Client#auth_failure - * @param {string} message - */ - this.emit(Events.AUTHENTICATION_FAILURE, failureEventPayload); - await this.destroy(); - if (restart) { - // session restore failed so try again but without session to force new authentication - return this.initialize(); - } - return; - } + this.on(Events.LOADING_SCREEN, () => { + qrRetries = 0; + }); - // Register qr/code events - if (pairWithPhoneNumber.phoneNumber) { - await exposeFunctionIfAbsent( - this.pupPage, - 'onCodeReceivedEvent', - async (code) => { - /** - * Emitted when a pairing code is received - * @event Client#code - * @param {string} code Code - * @returns {string} Code that was just received - */ - this.emit(Events.CODE_RECEIVED, code); - return code; - }, - ); - this.requestPairingCode( - pairWithPhoneNumber.phoneNumber, - pairWithPhoneNumber.showNotification, - pairWithPhoneNumber.intervalMs, - ); - } else { - let qrRetries = 0; - await exposeFunctionIfAbsent( - this.pupPage, - 'onQRChangedEvent', - async (qr) => { - /** - * Emitted when a QR code is received - * @event Client#qr - * @param {string} qr QR Code - */ - this.emit(Events.QR_RECEIVED, qr); - if (this.options.qrMaxRetries > 0) { - qrRetries++; - if (qrRetries > this.options.qrMaxRetries) { - this.emit( - Events.DISCONNECTED, - 'Max qrcode retries reached', - ); - await this.destroy(); + await exposeFunctionIfAbsent( + this.pupPage, + 'onQRChangedEvent', + async (qr) => { + /** + * Emitted when a QR code is received + * @event Client#qr + * @param {string} qr QR Code + */ + this.emit(Events.QR_RECEIVED, qr); + if (this.options.qrMaxRetries > 0) { + qrRetries++; + if (qrRetries > this.options.qrMaxRetries) { + this.emit( + Events.DISCONNECTED, + 'Max qrcode retries reached', + ); + await this.destroy(); + } } - } - }, - ); - - await this.pupPage.evaluate(async () => { - const registrationInfo = - await window.AuthStore.RegistrationUtils.waSignalStore.getRegistrationInfo(); - const noiseKeyPair = - await window.AuthStore.RegistrationUtils.waNoiseInfo.get(); - const staticKeyB64 = window.AuthStore.Base64Tools.encodeB64( - noiseKeyPair.staticKeyPair.pubKey, + }, ); - const identityKeyB64 = - window.AuthStore.Base64Tools.encodeB64( - registrationInfo.identityKeyPair.pubKey, - ); - const platform = - window.AuthStore.RegistrationUtils.DEVICE_PLATFORM; - const getQR = (ref) => - ref + - ',' + - staticKeyB64 + - ',' + - identityKeyB64 + - ',' + - window - .require('WAWebUserPrefsMultiDevice') - .getADVSecretKey() + - ',' + - platform; - window.onQRChangedEvent(getQR(window.AuthStore.Conn.ref)); // initial qr - window.AuthStore.Conn.on('change:ref', (_, ref) => { - window.onQRChangedEvent(getQR(ref)); - }); // future QR changes - }); - } - } - await exposeFunctionIfAbsent( - this.pupPage, - 'onAuthAppStateChangedEvent', - async (state) => { - if ( - state == 'UNPAIRED_IDLE' &amp;&amp; - !pairWithPhoneNumber.phoneNumber - ) { - // refresh qr code - window.require('WAWebCmd').Cmd.refreshQR(); - } - }, - ); + await this.pupPage.evaluate(async () => { + const registrationInfo = await window + .require('WAWebSignalStoreApi') + .waSignalStore.getRegistrationInfo(); + const noiseKeyPair = await window + .require('WAWebUserPrefsInfoStore') + .waNoiseInfo.get(); + const staticKeyB64 = window + .require('WABase64') + .encodeB64(noiseKeyPair.staticKeyPair.pubKey); + const identityKeyB64 = window + .require('WABase64') + .encodeB64(registrationInfo.identityKeyPair.pubKey); + const advSecretKey = await window + .require('WAWebUserPrefsMultiDevice') + .getADVSecretKey(); + const platform = window.require( + 'WAWebCompanionRegClientUtils', + ).DEVICE_PLATFORM; + const getQR = (ref) => + ref + + ',' + + staticKeyB64 + + ',' + + identityKeyB64 + + ',' + + advSecretKey + + ',' + + platform; + + const onRefChange = (_, ref) => { + if (ref == null) return; + window.onQRChangedEvent(getQR(ref)); + }; - await exposeFunctionIfAbsent( - this.pupPage, - 'onAppStateHasSyncedEvent', - async () => { - const authEventPayload = - await this.authStrategy.getAuthEventPayload(); - /** - * Emitted when authentication is successful - * @event Client#authenticated - */ - this.emit(Events.AUTHENTICATED, authEventPayload); + window.onQRChangedEvent( + getQR(window.require('WAWebConnModel').Conn.ref), + ); // initial qr + window + .require('WAWebConnModel') + .Conn.on('change:ref', onRefChange); // future QR changes - const injected = await this.pupPage.evaluate(async () => { - return typeof window.WWebJS !== 'undefined'; - }); + // Remove QR listener once authentication succeeds + window + .require('WAWebSocketModel') + .Socket.on('change:hasSynced', () => { + window + .require('WAWebConnModel') + .Conn.off('change:ref', onRefChange); + }); + }); + } + } - if (!injected) { + await exposeFunctionIfAbsent( + this.pupPage, + 'onAuthAppStateChangedEvent', + async (state) => { if ( - this.options.webVersionCache.type === 'local' &amp;&amp; - this.currentIndexHtml + state == 'UNPAIRED_IDLE' &amp;&amp; + !pairWithPhoneNumber.phoneNumber ) { - const { type: webCacheType, ...webCacheOptions } = - this.options.webVersionCache; - const webCache = WebCacheFactory.createWebCache( - webCacheType, - webCacheOptions, - ); - - await webCache.persist(this.currentIndexHtml, version); + // refresh qr code + await this.pupPage.evaluate(() => { + window.require('WAWebCmd').Cmd.refreshQR(); + }); } + }, + ); + + await exposeFunctionIfAbsent( + this.pupPage, + 'onAppStateHasSyncedEvent', + async () => { + const authEventPayload = + await this.authStrategy.getAuthEventPayload(); + /** + * Emitted when authentication is successful + * @event Client#authenticated + */ + this.emit(Events.AUTHENTICATED, authEventPayload); - // Load util functions (serializers, helper functions) - await this.pupPage.evaluate(LoadUtils); + const injected = await this.pupPage.evaluate(async () => { + return typeof window.WWebJS !== 'undefined'; + }); - let start = Date.now(); - let res = false; - while (start > Date.now() - 30000) { - // Check window.WWebJS Injection - res = await this.pupPage.evaluate( - 'window.WWebJS != undefined', - ); - if (res) { - break; + if (!injected) { + if ( + this.options.webVersionCache.type === 'local' &amp;&amp; + this.currentIndexHtml + ) { + const { type: webCacheType, ...webCacheOptions } = + this.options.webVersionCache; + const webCache = WebCacheFactory.createWebCache( + webCacheType, + webCacheOptions, + ); + + await webCache.persist( + this.currentIndexHtml, + version, + ); } - await new Promise((r) => setTimeout(r, 200)); - } - if (!res) { - throw 'ready timeout'; - } + // Load util functions (serializers, helper functions) + await this.pupPage.evaluate(LoadUtils); + + await this.pupPage + .waitForFunction( + 'typeof window.WWebJS !== "undefined"', + { timeout: 30000 }, + ) + .catch(() => { + throw 'ready timeout'; + }); + + /** + * Current connection information + * @type {ClientInfo} + */ + this.info = new ClientInfo( + this, + await this.pupPage.evaluate(() => { + return { + ...window + .require('WAWebConnModel') + .Conn.serialize(), + wid: + window + .require('WAWebUserPrefsMeUser') + .getMaybeMePnUser() || + window + .require('WAWebUserPrefsMeUser') + .getMaybeMeLidUser(), + }; + }), + ); + + this.interface = new InterfaceController(this); + + await this.attachEventListeners(); + } /** - * Current connection information - * @type {ClientInfo} + * Emitted when the client has initialized and is ready to receive messages. + * @event Client#ready */ - this.info = new ClientInfo( - this, - await this.pupPage.evaluate(() => { - return { - ...window - .require('WAWebConnModel') - .Conn.serialize(), - wid: - window - .require('WAWebUserPrefsMeUser') - .getMaybeMePnUser() || - window - .require('WAWebUserPrefsMeUser') - .getMaybeMeLidUser(), - }; - }), - ); - - this.interface = new InterfaceController(this); + this.emit(Events.READY); + this.authStrategy.afterAuthReady(); + }, + ); + let lastPercent = null; + await exposeFunctionIfAbsent( + this.pupPage, + 'onOfflineProgressUpdateEvent', + async (percent) => { + if (lastPercent !== percent) { + lastPercent = percent; + this.emit(Events.LOADING_SCREEN, percent, 'WhatsApp'); // Message is hardcoded as "WhatsApp" for now + } + }, + ); + await exposeFunctionIfAbsent( + this.pupPage, + 'onLogoutEvent', + async () => { + this.lastLoggedOut = true; + await this.pupPage + .waitForNavigation({ waitUntil: 'load', timeout: 5000 }) + .catch((_) => _); + }, + ); + await this.pupPage.evaluate(() => { + const Socket = window.require('WAWebSocketModel').Socket; + const Cmd = window.require('WAWebCmd').Cmd; + + const listeners = [ + [ + Socket, + 'change:state', + (_AppState, state) => { + window.onAuthAppStateChangedEvent(state); + }, + ], + [ + Socket, + 'change:hasSynced', + () => { + window.onAppStateHasSyncedEvent(); + }, + ], + [ + Cmd, + 'offline_progress_update_from_bridge', + () => { + window.onOfflineProgressUpdateEvent( + window + .require('WAWebOfflineHandler') + .OfflineMessageHandler.getOfflineDeliveryProgress(), + ); + }, + ], + [ + Cmd, + 'logout', + async () => { + await window.onLogoutEvent(); + }, + ], + [ + Cmd, + 'logout_from_bridge', + async () => { + await window.onLogoutEvent(); + }, + ], + ]; - await this.attachEventListeners(); + // Clean up old listeners to prevent accumulation on re-inject + if (window._wwjsListeners) { + for (const [obj, event, handler] of window._wwjsListeners) { + obj.off(event, handler); + } } - /** - * Emitted when the client has initialized and is ready to receive messages. - * @event Client#ready - */ - this.emit(Events.READY); - this.authStrategy.afterAuthReady(); - }, - ); - let lastPercent = null; - await exposeFunctionIfAbsent( - this.pupPage, - 'onOfflineProgressUpdateEvent', - async (percent) => { - if (lastPercent !== percent) { - lastPercent = percent; - this.emit(Events.LOADING_SCREEN, percent, 'WhatsApp'); // Message is hardcoded as "WhatsApp" for now + + for (const [obj, event, handler] of listeners) { + obj.on(event, handler); } - }, - ); - await exposeFunctionIfAbsent( - this.pupPage, - 'onLogoutEvent', - async () => { - this.lastLoggedOut = true; - await this.pupPage - .waitForNavigation({ waitUntil: 'load', timeout: 5000 }) - .catch((_) => _); - }, - ); - await this.pupPage.evaluate(() => { - window - .require('WAWebSocketModel') - .Socket.on('change:state', (_AppState, state) => { - window.onAuthAppStateChangedEvent(state); - }); - window - .require('WAWebSocketModel') - .Socket.on('change:hasSynced', () => { + window._wwjsListeners = listeners; + + // Atomic hasSynced check in the same synchronous block as listener registration. + // If hasSynced is already true, Backbone won't fire change:hasSynced (no transition). + // If hasSynced is false, the listener above will catch the future transition. + const storeInjected = typeof window.WWebJS !== 'undefined'; + if (Socket.hasSynced === true &amp;&amp; !storeInjected) { window.onAppStateHasSyncedEvent(); - }); - const Cmd = window.require('WAWebCmd').Cmd; - Cmd.on('offline_progress_update_from_bridge', () => { - window.onOfflineProgressUpdateEvent( - window.AuthStore.OfflineMessageHandler.getOfflineDeliveryProgress(), - ); - }); - Cmd.on('logout', async () => { - await window.onLogoutEvent(); - }); - Cmd.on('logout_from_bridge', async () => { - await window.onLogoutEvent(); + } }); - }); + } catch (err) { + if (abort.signal.aborted) return; // superseded by newer inject + throw err; + } finally { + if (this._injectAbort === abort) { + this._injectAbort = null; + } + } } /** @@ -520,16 +560,37 @@

Source: Client.js

referer: 'https://whatsapp.com/', }); + // Register framenavigated BEFORE inject so that if navigation + // interrupts inject, the handler triggers a fresh inject. + this._registerFramenavigatedHandler(); + await this.inject(); + } + + _registerFramenavigatedHandler() { + if (this._framenavigatedRegistered) return; + this._framenavigatedRegistered = true; this.pupPage.on('framenavigated', async (frame) => { - if (frame.url().includes('post_logout=1') || this.lastLoggedOut) { + if (frame.parentFrame() !== null) return; + + const isLogout = + frame.url().includes('post_logout=1') || this.lastLoggedOut; + + if (isLogout) { this.emit(Events.DISCONNECTED, 'LOGOUT'); await this.authStrategy.logout(); await this.authStrategy.beforeBrowserInitialized(); await this.authStrategy.afterBrowserInitialized(); this.lastLoggedOut = false; } + + const storeAvailable = await this.pupPage.evaluate(() => { + return typeof window.WWebJS !== 'undefined'; + }); + + if (!isLogout &amp;&amp; storeAvailable) return; + await this.inject(); }); } @@ -557,19 +618,15 @@

Source: Client.js

return await this.pupPage.evaluate( async (phoneNumber, showNotification, intervalMs) => { const getCode = async () => { - while (!window.AuthStore.PairingCodeLinkUtils) { - await new Promise((resolve) => - setTimeout(resolve, 250), - ); - } - window.AuthStore.PairingCodeLinkUtils.setPairingType( - 'ALT_DEVICE_LINKING', - ); - await window.AuthStore.PairingCodeLinkUtils.initializeAltDeviceLinking(); - return window.AuthStore.PairingCodeLinkUtils.startAltLinkingFlow( - phoneNumber, - showNotification, - ); + window + .require('WAWebAltDeviceLinkingApi') + .setPairingType('ALT_DEVICE_LINKING'); + await window + .require('WAWebAltDeviceLinkingApi') + .initializeAltDeviceLinking(); + return window + .require('WAWebAltDeviceLinkingApi') + .startAltLinkingFlow(phoneNumber, showNotification); }; if (window.codeInterval) { clearInterval(window.codeInterval); // remove existing interval @@ -1305,6 +1362,9 @@

Source: Client.js

* Closes the client */ async destroy() { + if (this._injectAbort) this._injectAbort.abort(); + this._framenavigatedRegistered = false; + const browser = this.pupBrowser; const isConnected = browser?.isConnected?.(); if (isConnected) { @@ -1615,6 +1675,22 @@

Source: Client.js

); } + /** + * Sends a presence state to a specific chat without fetching the chat from the database. + * @param {string} chatId The ID of the chat + * @param {string} state The presence state (e.g., 'typing', 'recording', 'paused') + */ + async sendPresenceUpdate(chatId, state = 'typing') { + return this.pupPage.evaluate( + (chatId, state) => { + window.WWebJS.sendChatstate(state, chatId); + return true; + }, + chatId, + state, + ); + } + /** * @typedef {Object} SendChannelAdminInviteOptions * @property {?string} comment The comment to be added to an invitation @@ -3472,7 +3548,7 @@

Source: Client.js

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/ClientInfo.html b/docs/ClientInfo.html index d73786e3e09..e86c10a1f0c 100644 --- a/docs/ClientInfo.html +++ b/docs/ClientInfo.html @@ -242,7 +242,7 @@

getBatteryStatus

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/Contact.html b/docs/Contact.html index 4ab6efc9cc6..4cdb9a8419b 100644 --- a/docs/Contact.html +++ b/docs/Contact.html @@ -305,7 +305,7 @@

unblock

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/GroupChat.html b/docs/GroupChat.html index b8dbcdbb4de..f0df284734c 100644 --- a/docs/GroupChat.html +++ b/docs/GroupChat.html @@ -1304,7 +1304,7 @@

unpin

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/GroupNotification.html b/docs/GroupNotification.html index 284035c7188..cf911669450 100644 --- a/docs/GroupNotification.html +++ b/docs/GroupNotification.html @@ -233,7 +233,7 @@

Parameters

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/InterfaceController.html b/docs/InterfaceController.html index 6f7f0b56060..92987d08667 100644 --- a/docs/InterfaceController.html +++ b/docs/InterfaceController.html @@ -382,7 +382,7 @@

Parameter

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/Label.html b/docs/Label.html index b958016a13b..669a7be690b 100644 --- a/docs/Label.html +++ b/docs/Label.html @@ -163,7 +163,7 @@

getChats

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/List.html b/docs/List.html index e7e704e4199..2e289a76d1e 100644 --- a/docs/List.html +++ b/docs/List.html @@ -256,7 +256,7 @@

Parameter

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/LocalAuth.html b/docs/LocalAuth.html index b63b2e4293b..f312b29f479 100644 --- a/docs/LocalAuth.html +++ b/docs/LocalAuth.html @@ -134,7 +134,7 @@

Parameters

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/LocalWebCache.html b/docs/LocalWebCache.html index 6ad77ef8324..101d0223ab9 100644 --- a/docs/LocalWebCache.html +++ b/docs/LocalWebCache.html @@ -120,7 +120,7 @@

Parameters

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/Location.html b/docs/Location.html index 094b96eed8d..10679b21099 100644 --- a/docs/Location.html +++ b/docs/Location.html @@ -173,7 +173,7 @@

url

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/Message.html b/docs/Message.html index 5cb3d3736a8..8e2b094d364 100644 --- a/docs/Message.html +++ b/docs/Message.html @@ -26,7 +26,7 @@
class

Message

-

Source: structures/Message.js:17

+

Source: structures/Message.js:18

Represents a Message on WhatsApp

@@ -158,6 +158,9 @@

Methods

downloadMedia()
+
downloadMediaStream([options])
+
+
edit(content[, options])
@@ -201,13 +204,13 @@

Methods

getReactions()
+
pin(duration)
+
+

-
pin(duration)
-
-
react(reaction)
@@ -465,12 +468,79 @@

Parameters

async
-

downloadMedia() → Promise containing MessageMedia

-

Downloads and returns the attatched message media

+

downloadMedia() → Promise containing (MessageMedia or undefined)

+

Downloads and returns the attached message media

+
+
Returns
+
+

Promise containing (MessageMedia or undefined) 

+
+
+
async
+

downloadMediaStream([options]) → Promise containing (MessageMediaStream or undefined)

+

Like downloadMedia(), but returns a Readable stream instead of loading the entire file into memory.

+
+

Parameters

+ + + + + + + + + + + + + + + + + +
NameTypeOptionalDescription
+

options

+
+

Object

+
+

Yes

+
+

Values in options have the following properties:

+ + + + + + + + + + + + + + + + + +
NameTypeOptionalDescription
+

chunkSize

+
+

number

+
+

Yes

+
+

Size in bytes of each chunk read from the browser (default 10MB)

+

Defaults to 10485760.

+
+
+
Returns
-

Promise containing MessageMedia 

+

Promise containing (MessageMediaStream or undefined)  +

undefined if media is unavailable

+

async
@@ -911,7 +981,7 @@

Parameter

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/MessageMedia.html b/docs/MessageMedia.html index f55b94dbe27..9a15a1f172e 100644 --- a/docs/MessageMedia.html +++ b/docs/MessageMedia.html @@ -364,7 +364,7 @@

Parameters

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/NoAuth.html b/docs/NoAuth.html index 527655f327d..e75e93406bd 100644 --- a/docs/NoAuth.html +++ b/docs/NoAuth.html @@ -51,7 +51,7 @@

new NoAuth

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/Order.html b/docs/Order.html index a3b867ccc06..a381eb139ac 100644 --- a/docs/Order.html +++ b/docs/Order.html @@ -102,7 +102,7 @@

total

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/Poll.html b/docs/Poll.html index c577797b4d4..6bf7f5e3ee3 100644 --- a/docs/Poll.html +++ b/docs/Poll.html @@ -148,7 +148,7 @@

pollOptions

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/PollVote.html b/docs/PollVote.html index 50e24bf46ab..ee10a95f48b 100644 --- a/docs/PollVote.html +++ b/docs/PollVote.html @@ -102,7 +102,7 @@

voter

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/PrivateChat.html b/docs/PrivateChat.html index ad71a1f18a1..45b1f45ed13 100644 --- a/docs/PrivateChat.html +++ b/docs/PrivateChat.html @@ -680,7 +680,7 @@

unpin

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/PrivateContact.html b/docs/PrivateContact.html index d40f1a9ab82..a28c15b894c 100644 --- a/docs/PrivateContact.html +++ b/docs/PrivateContact.html @@ -331,7 +331,7 @@

unblock

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/Product.html b/docs/Product.html index 18f6420dab4..64ea1c218ed 100644 --- a/docs/Product.html +++ b/docs/Product.html @@ -127,7 +127,7 @@

thumbnailUrl

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/Reaction.html b/docs/Reaction.html index 3cce2aa546f..8dd47fb5276 100644 --- a/docs/Reaction.html +++ b/docs/Reaction.html @@ -144,7 +144,7 @@

timestamp

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/RemoteAuth.html b/docs/RemoteAuth.html index e498fad619a..cbf7ff233c2 100644 --- a/docs/RemoteAuth.html +++ b/docs/RemoteAuth.html @@ -162,7 +162,7 @@

Parameters

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/RemoteWebCache.html b/docs/RemoteWebCache.html index 426531fca97..8e3c0ecaedd 100644 --- a/docs/RemoteWebCache.html +++ b/docs/RemoteWebCache.html @@ -120,7 +120,7 @@

Parameters

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/ScheduledEvent.html b/docs/ScheduledEvent.html index 845754db5f1..bf6ac0e274d 100644 --- a/docs/ScheduledEvent.html +++ b/docs/ScheduledEvent.html @@ -220,7 +220,7 @@

Parameters

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/Util.html b/docs/Util.html index fb1ba270269..c9f7273c6bf 100644 --- a/docs/Util.html +++ b/docs/Util.html @@ -243,7 +243,7 @@

Parameter

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/WebCache.html b/docs/WebCache.html index eda9e64ed9a..762a92269a0 100644 --- a/docs/WebCache.html +++ b/docs/WebCache.html @@ -50,7 +50,7 @@

new WebCache

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/authStrategies_BaseAuthStrategy.js.html b/docs/authStrategies_BaseAuthStrategy.js.html index 52b93506c64..478c40e66c6 100644 --- a/docs/authStrategies_BaseAuthStrategy.js.html +++ b/docs/authStrategies_BaseAuthStrategy.js.html @@ -68,7 +68,7 @@

Source: authStrategies/BaseAuthStrategy.js

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/authStrategies_LocalAuth.js.html b/docs/authStrategies_LocalAuth.js.html index 176a9711270..7e27ebb2e02 100644 --- a/docs/authStrategies_LocalAuth.js.html +++ b/docs/authStrategies_LocalAuth.js.html @@ -110,7 +110,7 @@

Source: authStrategies/LocalAuth.js

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/authStrategies_NoAuth.js.html b/docs/authStrategies_NoAuth.js.html index 07e73c66b30..2d48417927f 100644 --- a/docs/authStrategies_NoAuth.js.html +++ b/docs/authStrategies_NoAuth.js.html @@ -50,7 +50,7 @@

Source: authStrategies/NoAuth.js

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/authStrategies_RemoteAuth.js.html b/docs/authStrategies_RemoteAuth.js.html index 4d86617337e..9c83fe80dae 100644 --- a/docs/authStrategies_RemoteAuth.js.html +++ b/docs/authStrategies_RemoteAuth.js.html @@ -306,7 +306,7 @@

Source: authStrategies/RemoteAuth.js

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/global.html b/docs/global.html index 203cce73a7c..fe26a0ee8c2 100644 --- a/docs/global.html +++ b/docs/global.html @@ -4333,7 +4333,7 @@

Property

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/index.html b/docs/index.html index a00f047f883..e7dad37bdc9 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1177,11 +1177,6 @@

Client

-
- Client#event:code -
-
-
Client#event:contact_changed
@@ -1247,16 +1242,16 @@

Client

-

-
- -
-
Client#event:message_create
+
+
+ +
+
Client#event:message_edit
@@ -1447,16 +1442,16 @@

Client

-
-
- -
-
Client#isRegisteredUser(id)
+
+
+ +
+
Client#logout()
@@ -1547,6 +1542,11 @@

Client

+
+ Client#sendPresenceUpdate(chatId, state) +
+
+
Client#sendReaction(messageId, reaction)
@@ -2818,6 +2818,11 @@

Message

+
+ Message#downloadMediaStream([options]) +
+
+
Message#duration
@@ -2863,16 +2868,16 @@

Message

-
- Message#getGroupMentions() -
-
-
+
+ Message#getGroupMentions() +
+
+
Message#getInfo()
@@ -2963,16 +2968,16 @@

Message

-
- Message#links -
-
-
+
+ Message#links +
+
+
Message#location
@@ -4445,6 +4450,11 @@

window

+
+ window.WWebJS.resolveMediaBlob(msgId) +
+
+
@@ -4461,7 +4471,7 @@

window

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/structures_Base.js.html b/docs/structures_Base.js.html index 6086ac2ce3d..e7a433aecdf 100644 --- a/docs/structures_Base.js.html +++ b/docs/structures_Base.js.html @@ -63,7 +63,7 @@

Source: structures/Base.js

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/structures_Broadcast.js.html b/docs/structures_Broadcast.js.html index 8ac81b173cf..69c9a9e00ae 100644 --- a/docs/structures_Broadcast.js.html +++ b/docs/structures_Broadcast.js.html @@ -107,7 +107,7 @@

Source: structures/Broadcast.js

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/structures_BusinessContact.js.html b/docs/structures_BusinessContact.js.html index 6bbe16d83aa..0f6310abf29 100644 --- a/docs/structures_BusinessContact.js.html +++ b/docs/structures_BusinessContact.js.html @@ -59,7 +59,7 @@

Source: structures/BusinessContact.js

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/structures_Buttons.js.html b/docs/structures_Buttons.js.html index 2523107abc0..b37e108f7fa 100644 --- a/docs/structures_Buttons.js.html +++ b/docs/structures_Buttons.js.html @@ -125,7 +125,7 @@

Source: structures/Buttons.js

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/structures_Call.js.html b/docs/structures_Call.js.html index c03bacfb78b..145ab8618d4 100644 --- a/docs/structures_Call.js.html +++ b/docs/structures_Call.js.html @@ -119,7 +119,7 @@

Source: structures/Call.js

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/structures_Channel.js.html b/docs/structures_Channel.js.html index c5d7ae040bc..bc07e10623f 100644 --- a/docs/structures_Channel.js.html +++ b/docs/structures_Channel.js.html @@ -503,7 +503,7 @@

Source: structures/Channel.js

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/structures_Chat.js.html b/docs/structures_Chat.js.html index 2b305f440b3..821927228b5 100644 --- a/docs/structures_Chat.js.html +++ b/docs/structures_Chat.js.html @@ -390,7 +390,7 @@

Source: structures/Chat.js

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/structures_ClientInfo.js.html b/docs/structures_ClientInfo.js.html index 5eaf5d189aa..b936ed4264d 100644 --- a/docs/structures_ClientInfo.js.html +++ b/docs/structures_ClientInfo.js.html @@ -110,7 +110,7 @@

Source: structures/ClientInfo.js

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/structures_Contact.js.html b/docs/structures_Contact.js.html index f368a8b5380..578c04e41e1 100644 --- a/docs/structures_Contact.js.html +++ b/docs/structures_Contact.js.html @@ -189,15 +189,20 @@

Source: structures/Contact.js

const contact = await window .require('WAWebCollections') .Contact.find(contactId); - const resolved = window - .require('WAWebBlockContactUtils') - .getContactToBlockOnlyUseIfNoAssociatedChat( - contact, - 'ChatListBlock', - ); - await window - .require('WAWebBlockContactAction') - .blockContact({ contact: resolved }); + const lid = contact.id.isLid() + ? contact.id + : window + .require('WAWebApiContact') + .getAlternateUserWid(contact.id); + const ContactToBlock = { + id: lid, + isContactBlocked: false, + phoneNumber: null, + }; + await window.require('WAWebBlockContactAction').blockContact({ + contact: ContactToBlock, + blockEntryPoint: 'ChatListBlock', + }); }, this.id._serialized); this.isBlocked = true; @@ -212,18 +217,21 @@

Source: structures/Contact.js

if (this.isGroup) return false; await this.client.pupPage.evaluate(async (contactId) => { - const contact = await window + let contact = await window .require('WAWebCollections') .Contact.find(contactId); - const resolved = window - .require('WAWebBlockContactUtils') - .getContactToBlockOnlyUseIfNoAssociatedChat( - contact, - 'ChatListBlock', - ); + if (!contact.id.isLid()) { + const lid = window + .require('WAWebApiContact') + .getAlternateUserWid(contact.id); + + contact = await window + .require('WAWebCollections') + .Contact.find(lid._serialized); + } await window .require('WAWebBlockContactAction') - .unblockContact(resolved); + .unblockContact(contact, 'ChatListBlock'); }, this.id._serialized); this.isBlocked = false; @@ -275,7 +283,7 @@

Source: structures/Contact.js

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/structures_GroupChat.js.html b/docs/structures_GroupChat.js.html index ec3192562b7..5861e7245c5 100644 --- a/docs/structures_GroupChat.js.html +++ b/docs/structures_GroupChat.js.html @@ -719,7 +719,7 @@

Source: structures/GroupChat.js

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/structures_GroupNotification.js.html b/docs/structures_GroupNotification.js.html index 7f26107bbb7..ff56c8c8f8e 100644 --- a/docs/structures_GroupNotification.js.html +++ b/docs/structures_GroupNotification.js.html @@ -152,7 +152,7 @@

Source: structures/GroupNotification.js

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/structures_Label.js.html b/docs/structures_Label.js.html index 1f4d8ec75e6..598c2f9dc78 100644 --- a/docs/structures_Label.js.html +++ b/docs/structures_Label.js.html @@ -89,7 +89,7 @@

Source: structures/Label.js

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/structures_List.js.html b/docs/structures_List.js.html index c446399bf94..845a53eeadc 100644 --- a/docs/structures_List.js.html +++ b/docs/structures_List.js.html @@ -126,7 +126,7 @@

Source: structures/List.js

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/structures_Location.js.html b/docs/structures_Location.js.html index c63f9efc92e..4e54173829e 100644 --- a/docs/structures_Location.js.html +++ b/docs/structures_Location.js.html @@ -102,7 +102,7 @@

Source: structures/Location.js

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/structures_Message.js.html b/docs/structures_Message.js.html index a3314631d7f..4f508139ea1 100644 --- a/docs/structures_Message.js.html +++ b/docs/structures_Message.js.html @@ -31,6 +31,7 @@

Source: structures/Message.js

'use strict';
 
+const { Readable } = require('stream');
 const Base = require('./Base');
 const MessageMedia = require('./MessageMedia');
 const Location = require('./Location');
@@ -538,84 +539,25 @@ 

Source: structures/Message.js

} /** - * Downloads and returns the attatched message media - * @returns {Promise&lt;MessageMedia>} + * Downloads and returns the attached message media + * @returns {Promise&lt;MessageMedia|undefined>} */ async downloadMedia() { - if (!this.hasMedia) { - return undefined; - } + if (!this.hasMedia) return undefined; const result = await this.client.pupPage.evaluate(async (msgId) => { - const msg = - window.require('WAWebCollections').Msg.get(msgId) || - ( - await window - .require('WAWebCollections') - .Msg.getMessagesById([msgId]) - )?.messages?.[0]; - - // REUPLOADING mediaStage means the media is expired and the download button is spinning, cannot be downloaded now - if ( - !msg || - !msg.mediaData || - msg.mediaData.mediaStage === 'REUPLOADING' - ) { - return null; - } - if (msg.mediaData.mediaStage != 'RESOLVED') { - // try to resolve media - await msg.downloadMedia({ - downloadEvenIfExpensive: true, - rmrReason: 1, - }); - } - - if ( - msg.mediaData.mediaStage.includes('ERROR') || - msg.mediaData.mediaStage === 'FETCHING' - ) { - // media could not be downloaded - return undefined; - } - - try { - const mockQpl = { - addAnnotations: function () { - return this; - }, - addPoint: function () { - return this; - }, - }; - const decryptedMedia = await window - .require('WAWebDownloadManager') - .downloadManager.downloadAndMaybeDecrypt({ - directPath: msg.directPath, - encFilehash: msg.encFilehash, - filehash: msg.filehash, - mediaKey: msg.mediaKey, - mediaKeyTimestamp: msg.mediaKeyTimestamp, - type: msg.type, - signal: new AbortController().signal, - downloadQpl: mockQpl, - }); - - const data = - await window.WWebJS.arrayBufferToBase64Async( - decryptedMedia, - ); + const resolved = await window.WWebJS.resolveMediaBlob(msgId); + if (!resolved) return null; - return { - data, - mimetype: msg.mimetype, - filename: msg.filename, - filesize: msg.size, - }; - } catch (e) { - if (e.status &amp;&amp; e.status === 404) return undefined; - throw e; - } + const data = await window.WWebJS.arrayBufferToBase64Async( + await resolved.blob.arrayBuffer(), + ); + return { + data, + mimetype: resolved.mimetype, + filename: resolved.filename, + filesize: resolved.filesize, + }; }, this.id._serialized); if (!result) return undefined; @@ -627,6 +569,67 @@

Source: structures/Message.js

); } + /** + * Like downloadMedia(), but returns a Readable stream instead of loading the entire file into memory. + * @param {Object} [options] + * @param {number} [options.chunkSize=10485760] Size in bytes of each chunk read from the browser (default 10MB) + * @returns {Promise&lt;MessageMediaStream|undefined>} undefined if media is unavailable + */ + async downloadMediaStream({ chunkSize = 10 * 1024 * 1024 } = {}) { + if (!this.hasMedia) return undefined; + + const blobHandle = await this.client.pupPage.evaluateHandle( + async (msgId) => { + const result = await window.WWebJS.resolveMediaBlob(msgId); + return result?.blob ?? null; + }, + this.id._serialized, + ); + + let metadata; + try { + metadata = await blobHandle.evaluate((blob, msgId) => { + if (!blob) return null; + const msg = window.require('WAWebCollections').Msg.get(msgId); + return { + blobSize: blob.size, + mimetype: msg?.mimetype, + filename: msg?.filename, + filesize: msg?.size, + }; + }, this.id._serialized); + } catch (err) { + await blobHandle.dispose().catch(() => {}); + throw err; + } + if (!metadata) { + await blobHandle.dispose().catch(() => {}); + return undefined; + } + + const { blobSize, ...rest } = metadata; + + async function* readChunks() { + try { + for (let offset = 0; offset &lt; blobSize; offset += chunkSize) { + const base64 = await blobHandle.evaluate( + async (blob, s, e) => + window.WWebJS.arrayBufferToBase64Async( + await blob.slice(s, e).arrayBuffer(), + ), + offset, + offset + chunkSize, + ); + yield Buffer.from(base64, 'base64'); + } + } finally { + await blobHandle.dispose().catch(() => {}); + } + } + + return { stream: Readable.from(readChunks()), ...rest }; + } + /** * Deletes a message from the chat * @param {?boolean} everyone If true and the message is sent by the current user or the user is an admin, will delete it for everyone in the chat. @@ -1080,7 +1083,7 @@

Source: structures/Message.js

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/structures_MessageMedia.js.html b/docs/structures_MessageMedia.js.html index 6d600df1da6..d78175ada23 100644 --- a/docs/structures_MessageMedia.js.html +++ b/docs/structures_MessageMedia.js.html @@ -163,7 +163,7 @@

Source: structures/MessageMedia.js

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/structures_Order.js.html b/docs/structures_Order.js.html index a64fe699f9a..c6a3d9f839d 100644 --- a/docs/structures_Order.js.html +++ b/docs/structures_Order.js.html @@ -91,7 +91,7 @@

Source: structures/Order.js

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/structures_Payment.js.html b/docs/structures_Payment.js.html index dac7868f752..d68b422ead9 100644 --- a/docs/structures_Payment.js.html +++ b/docs/structures_Payment.js.html @@ -121,7 +121,7 @@

Source: structures/Payment.js

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/structures_Poll.js.html b/docs/structures_Poll.js.html index 88dcfda1e85..86b688d3115 100644 --- a/docs/structures_Poll.js.html +++ b/docs/structures_Poll.js.html @@ -83,7 +83,7 @@

Source: structures/Poll.js

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/structures_PollVote.js.html b/docs/structures_PollVote.js.html index d261e7b3b54..d1a4ed7bcc5 100644 --- a/docs/structures_PollVote.js.html +++ b/docs/structures_PollVote.js.html @@ -117,7 +117,7 @@

Source: structures/PollVote.js

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/structures_PrivateChat.js.html b/docs/structures_PrivateChat.js.html index 808a98fd7fb..a9861485bb1 100644 --- a/docs/structures_PrivateChat.js.html +++ b/docs/structures_PrivateChat.js.html @@ -50,7 +50,7 @@

Source: structures/PrivateChat.js

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/structures_PrivateContact.js.html b/docs/structures_PrivateContact.js.html index 6e9b557268f..50efafb1985 100644 --- a/docs/structures_PrivateContact.js.html +++ b/docs/structures_PrivateContact.js.html @@ -50,7 +50,7 @@

Source: structures/PrivateContact.js

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/structures_Product.js.html b/docs/structures_Product.js.html index edee2d050b7..38eee615d51 100644 --- a/docs/structures_Product.js.html +++ b/docs/structures_Product.js.html @@ -107,7 +107,7 @@

Source: structures/Product.js

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/structures_ProductMetadata.js.html b/docs/structures_ProductMetadata.js.html index c9e8c8d348f..ec6af3b9078 100644 --- a/docs/structures_ProductMetadata.js.html +++ b/docs/structures_ProductMetadata.js.html @@ -63,7 +63,7 @@

Source: structures/ProductMetadata.js

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/structures_Reaction.js.html b/docs/structures_Reaction.js.html index 5da180d714c..b7ec3a0ba20 100644 --- a/docs/structures_Reaction.js.html +++ b/docs/structures_Reaction.js.html @@ -106,7 +106,7 @@

Source: structures/Reaction.js

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/structures_ScheduledEvent.js.html b/docs/structures_ScheduledEvent.js.html index 7b7a9d4a2c5..fec1be807c9 100644 --- a/docs/structures_ScheduledEvent.js.html +++ b/docs/structures_ScheduledEvent.js.html @@ -122,7 +122,7 @@

Source: structures/ScheduledEvent.js

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/util_Constants.js.html b/docs/util_Constants.js.html index b31ac905cfe..4bcd73308ba 100644 --- a/docs/util_Constants.js.html +++ b/docs/util_Constants.js.html @@ -227,7 +227,7 @@

Source: util/Constants.js

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/util_Injected_Utils.js.html b/docs/util_Injected_Utils.js.html index 32fecaee04c..2a66d9aa308 100644 --- a/docs/util_Injected_Utils.js.html +++ b/docs/util_Injected_Utils.js.html @@ -1136,6 +1136,63 @@

Source: util/Injected/Utils.js

}); }; + /** + * Resolves the media blob and metadata for a message. + * Shared by downloadMedia and downloadMediaStream. + * @param {string} msgId + * @returns {Promise&lt;{blob: Blob, mimetype: string, filename: string, filesize: number}|null>} + */ + window.WWebJS.resolveMediaBlob = async (msgId) => { + const { Msg } = window.require('WAWebCollections'); + const msg = + Msg.get(msgId) || + (await Msg.getMessagesById([msgId]))?.messages?.[0]; + + if ( + !msg || + !msg.mediaData || + msg.mediaData.mediaStage === 'REUPLOADING' + ) { + return null; + } + + // Always call internal downloadMedia - never skip based on + // mediaStage, because cache eviction can leave stage=RESOLVED + // with empty InMemoryMediaBlobCache. + await msg.downloadMedia({ + downloadEvenIfExpensive: true, + rmrReason: 1, + isUserInitiated: true, + }); + + if ( + msg.mediaData.mediaStage.includes('ERROR') || + msg.mediaData.mediaStage === 'FETCHING' + ) { + return null; + } + + const cached = window + .require('WAWebMediaInMemoryBlobCache') + .InMemoryMediaBlobCache.get(msg.mediaObject?.filehash); + + let blob; + if (cached) { + blob = cached; + } else if (msg.mediaObject?.mediaBlob) { + blob = msg.mediaObject.mediaBlob.forceToBlob(); + } + + if (!blob) return null; + + return { + blob, + mimetype: msg.mimetype, + filename: msg.filename, + filesize: msg.size, + }; + }; + window.WWebJS.arrayBufferToBase64 = (arrayBuffer) => { let binary = ''; const bytes = new Uint8Array(arrayBuffer); @@ -1778,7 +1835,7 @@

Source: util/Injected/Utils.js

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/util_InterfaceController.js.html b/docs/util_InterfaceController.js.html index b4e800dc5a1..122a9adf6df 100644 --- a/docs/util_InterfaceController.js.html +++ b/docs/util_InterfaceController.js.html @@ -212,7 +212,7 @@

Source: util/InterfaceController.js

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/util_Puppeteer.js.html b/docs/util_Puppeteer.js.html index 33497a5b9c1..1f83838975f 100644 --- a/docs/util_Puppeteer.js.html +++ b/docs/util_Puppeteer.js.html @@ -62,7 +62,7 @@

Source: util/Puppeteer.js

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/util_Util.js.html b/docs/util_Util.js.html index e338ca98040..80714e7926c 100644 --- a/docs/util_Util.js.html +++ b/docs/util_Util.js.html @@ -238,7 +238,7 @@

Source: util/Util.js

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/webCache_LocalWebCache.js.html b/docs/webCache_LocalWebCache.js.html index 940dcb9c7eb..5bb855ea5ca 100644 --- a/docs/webCache_LocalWebCache.js.html +++ b/docs/webCache_LocalWebCache.js.html @@ -81,7 +81,7 @@

Source: webCache/LocalWebCache.js

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/webCache_RemoteWebCache.js.html b/docs/webCache_RemoteWebCache.js.html index 2db3d59f083..6a0b2f84d2f 100644 --- a/docs/webCache_RemoteWebCache.js.html +++ b/docs/webCache_RemoteWebCache.js.html @@ -85,7 +85,7 @@

Source: webCache/RemoteWebCache.js

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.

diff --git a/docs/webCache_WebCache.js.html b/docs/webCache_WebCache.js.html index 546ed71f7f1..a3636702581 100644 --- a/docs/webCache_WebCache.js.html +++ b/docs/webCache_WebCache.js.html @@ -56,7 +56,7 @@

Source: webCache/WebCache.js

- Generated by JSDoc 3.6.11 on April 24, 2026. + Generated by JSDoc 3.6.11 on July 16, 2026.