// 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; }); }