diff options
| author | Elvis Claros Castro <elvis@claros.ar> | 2026-09-26 19:26:33 -0300 |
|---|---|---|
| committer | Elvis Claros Castro <elvis@claros.ar> | 2026-09-26 19:26:33 -0300 |
| commit | d80640d01eeb84907e36ff4af33add9081eda17c (patch) | |
| tree | 27698b34e06e28d111c2196700015d9c82f78274 /src/compat.js | |
| download | wa-cli-d80640d01eeb84907e36ff4af33add9081eda17c.tar.gz wa-cli-d80640d01eeb84907e36ff4af33add9081eda17c.zip | |
Diffstat (limited to 'src/compat.js')
| -rw-r--r-- | src/compat.js | 94 |
1 files changed, 94 insertions, 0 deletions
diff --git a/src/compat.js b/src/compat.js new file mode 100644 index 0000000..600c60a --- /dev/null +++ b/src/compat.js @@ -0,0 +1,94 @@ +// Patches for WhatsApp Web changes that whatsapp-web.js does not follow yet. +// They are applied from here (not by editing node_modules) so they survive `npm install`. +// Once upstream fixes them, each patch only needs to detect it is no longer needed. +// +// Re-injection on SPA navigations (fixed on main by wwebjs#201653, not released) +// whatsapp-web.js runs inject() on every `framenavigated`, which also fires for +// same-document navigations and iframes. Two concurrent inject() calls register the +// same bindings: exposeFunction throws "already exists", puppeteer swallows the error +// and `ready` is never emitted (the daemon stays in "authenticated"). +// +// MsgKey._serialized -> $1 (WA Web 2.3000.104x; wwebjs#201830, no release with the fix) +// Message IDs lost `_serialized` (now `$1`, same format). whatsapp-web.js reads it in +// the page (getChats, sendMessage, downloadMedia...) and in Node (Message.id). +// Symptoms: `r: r` in getChats(), sendMessage() returns undefined, undefined IDs. + +import { createRequire } from 'node:module'; + +const require = createRequire(import.meta.url); + +// Fills in `_serialized` on any MsgKey inside the data coming from the browser. +function fillSerialized(value, depth = 0) { + if (!value || typeof value !== 'object' || depth > 5) return; + if (Array.isArray(value)) { + for (const v of value) fillSerialized(v, depth + 1); + return; + } + if (value._serialized === undefined && typeof value.$1 === 'string') value._serialized = value.$1; + for (const v of Object.values(value)) if (v && typeof v === 'object') fillSerialized(v, depth + 1); +} + +// Inject once per document: the marker lives on `window`, so a real reload (new +// document) injects again and an SPA navigation does not. It is checked and set in a +// single evaluate so two simultaneous events cannot both get through. +export function patchClient(Client) { + const original = Client.prototype.inject; + if (original.waCompat) return; + Client.prototype.inject = async function (...args) { + let first = false; + for (let i = 0; i < 3; i++) { + try { + first = await this.pupPage.evaluate(() => { + if (window.__waCliInjected) return false; + window.__waCliInjected = true; + return true; + }); + break; + } catch { + // context in transition (reload in progress): the new document's + // framenavigated will call inject() again + await new Promise((r) => setTimeout(r, 500)); + } + } + if (first) return original.apply(this, args); + }; + Client.prototype.inject.waCompat = true; +} + +// Node side: every structure receives the page data in _patch(data). +export function patchStructures() { + const structures = require('whatsapp-web.js/src/structures/index.js'); + for (const Cls of new Set(Object.values(structures))) { + const proto = Cls?.prototype; + if (!proto || !Object.prototype.hasOwnProperty.call(proto, '_patch') || proto._patch.waCompat) continue; + const original = proto._patch; + proto._patch = function (data) { + fillSerialized(data); + return original.call(this, data); + }; + proto._patch.waCompat = true; + } +} + +// Page side: `_serialized` getter on MsgKey. It has to be reapplied on every WhatsApp Web +// load (whatsapp-web.js re-injects and emits `ready` again). +export async function patchPage(page) { + return page.evaluate(() => { + const applied = []; + const MsgKey = window.require('WAWebMsgKey'); + const proto = MsgKey?.prototype; + if (proto && !('_serialized' in proto)) { + Object.defineProperty(proto, '_serialized', { + configurable: true, + get() { + return this.$1; + }, + set(v) { + Object.defineProperty(this, '_serialized', { value: v, writable: true, configurable: true, enumerable: true }); + }, + }); + applied.push('MsgKey._serialized'); + } + return applied; + }); +} |