aboutsummaryrefslogtreecommitdiffstats
path: root/src/compat.js
blob: 600c60a45d9ca45b6cdeb3b78070ae31167ef032 (plain)
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;
    });
}