aboutsummaryrefslogtreecommitdiffstats
path: root/src/compat.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/compat.js')
-rw-r--r--src/compat.js94
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;
+ });
+}