1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
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;
});
}
|