From 3b9d2fe6d19b81107622c9ee226f5b9bb2bd0cae Mon Sep 17 00:00:00 2001 From: dafibh Date: Fri, 17 Jul 2026 01:00:05 +0800 Subject: [PATCH 1/3] fix: patch whatsapp-web.js for WA Web 2.3000.1043xxx _serialized rename WhatsApp Web build 2.3000.1043xxx renamed the internal WID/MsgKey property `_serialized` to `$1`, breaking whatsapp-web.js@1.34.7's injected layer: getChats, getChatById, fetchMessages, downloadMedia and message delete all threw a minified `r` error (HTTP 500). Backport the dual-compat shim from upstream PR wwebjs/whatsapp-web.js#201840 as a local patch applied at Docker build time. Adds widSerialized / normalizeSerialized helpers so node-side code keeps reading `id._serialized`. API response shape is unchanged (adds only a raw `$1` field alongside `_serialized`). The patch is version-guarded in the Dockerfile: it applies only to whatsapp-web.js 1.34.7 and is skipped for any other version, so bumping to an official fix will not break the build. See patches/README.md for revert steps. Co-Authored-By: Claude Opus 4.8 (1M context) --- Dockerfile | 12 +- patches/README.md | 46 ++++++++ patches/whatsapp-web.js+1.34.7.patch | 157 +++++++++++++++++++++++++++ 3 files changed, 214 insertions(+), 1 deletion(-) create mode 100644 patches/README.md create mode 100644 patches/whatsapp-web.js+1.34.7.patch diff --git a/Dockerfile b/Dockerfile index ef2d74a..b2cce98 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,6 +13,7 @@ FROM base AS deps ARG USE_EDGE=false COPY package*.json ./ +COPY patches ./patches RUN if [ "$USE_EDGE" = "true" ]; then \ apt-get update && apt-get install -y --no-install-recommends git ca-certificates && \ @@ -20,7 +21,16 @@ RUN if [ "$USE_EDGE" = "true" ]; then \ npm install --save-exact git+https://github.com/pedroslopez/whatsapp-web.js.git#main && \ apt-get purge -y git ca-certificates && apt-get autoremove -y && rm -rf /var/lib/apt/lists/*; \ else \ - npm ci --only=production --ignore-scripts; \ + apt-get update && apt-get install -y --no-install-recommends patch && \ + npm ci --only=production --ignore-scripts && \ + WWEBJS_VER="$(node -p 'require("/usr/src/app/node_modules/whatsapp-web.js/package.json").version')" && \ + if [ "$WWEBJS_VER" = "1.34.7" ]; then \ + echo "Applying WA 2.3000.1043xxx serialized-id compat patch to whatsapp-web.js@$WWEBJS_VER (see patches/README.md)" && \ + patch -p1 -d node_modules/whatsapp-web.js < patches/whatsapp-web.js+1.34.7.patch; \ + else \ + echo "whatsapp-web.js is $WWEBJS_VER, not 1.34.7 - skipping local patch (see patches/README.md)"; \ + fi && \ + apt-get purge -y patch && apt-get autoremove -y && rm -rf /var/lib/apt/lists/*; \ fi # Create the final stage diff --git a/patches/README.md b/patches/README.md new file mode 100644 index 0000000..e80e589 --- /dev/null +++ b/patches/README.md @@ -0,0 +1,46 @@ +# Local library patches + +Patches applied to `node_modules` at Docker build time. Applied by the `deps` +stage in the root `Dockerfile` (default build only; the `USE_EDGE=true` build +installs `whatsapp-web.js` from git `main` and is **not** patched). + +## `whatsapp-web.js+1.34.7.patch` + +**Why:** WhatsApp Web build `2.3000.1043xxx` renamed the internal WID / MsgKey +property `_serialized` to `$1`. `whatsapp-web.js@1.34.7` (latest published) still +reads `_serialized`, so every chat/message operation that goes through the +injected layer (`getChats`, `getChatById`, `fetchMessages`, `downloadMedia`, +message `delete`, etc.) throws a minified `r` error and returns HTTP 500. + +**What it does:** backports the dual-compat shim from upstream PR +[wwebjs/whatsapp-web.js#201840](https://github.com/wwebjs/whatsapp-web.js/pull/201840) +(unmerged as of 2026-07-16). Adds two helpers in `Injected/Utils.js`: +- `widSerialized(wid)` - reads `_serialized`, falls back to `$1`. +- `normalizeSerialized(obj)` - mirrors `$1` onto `_serialized` on objects + returned to Node so existing node-side code keeps reading `id._serialized`. + +Applied in `getMessageModel` / `getChatModel` and `Message` from/to/author. + +**API contract:** unchanged. Responses keep every original field +(`id._serialized`, `remote`, `id`, `fromMe`, `from`, `to`, ...). The only +difference is an additive raw `$1` field alongside `_serialized` (same value). + +**Version guard:** the Dockerfile applies this patch only when the installed +`whatsapp-web.js` is exactly `1.34.7`. Any other version is skipped with a log +line, so bumping the dependency will not break the build. + +## How to revert + +Preferred: `git revert` the commit that introduced this directory (removes the +patch file and the Dockerfile change together), then rebuild. + +When the upstream fix ships in a release, instead: +1. Bump `whatsapp-web.js` in `package.json` to the fixed version and update + `package-lock.json` (`npm install`). +2. Delete this `patches/` directory. +3. Revert the `deps`-stage patch block in the root `Dockerfile`. +4. Rebuild: `docker compose build api && docker compose up -d --force-recreate`. + +Note: even if step 2/3 are skipped, the version guard makes the patch a no-op +once the version is no longer `1.34.7` - so a plain version bump is safe on its +own, and the cleanup can follow later. diff --git a/patches/whatsapp-web.js+1.34.7.patch b/patches/whatsapp-web.js+1.34.7.patch new file mode 100644 index 0000000..a4813c9 --- /dev/null +++ b/patches/whatsapp-web.js+1.34.7.patch @@ -0,0 +1,157 @@ +--- a/src/util/Injected/Utils.js ++++ b/src/util/Injected/Utils.js +@@ -4,6 +4,51 @@ + window.WWebJS = {}; + + /** ++ * Dual-compat serialized id helper. ++ * Older WhatsApp Web builds expose WID/MsgKey as `_serialized`. ++ * Newer builds (e.g. 2.3000.1043xxx) expose the same value as `$1`. ++ * Prefer `_serialized` first so clients that have not updated keep working. ++ * @param {*} wid ++ * @returns {string|undefined} ++ */ ++ window.WWebJS.widSerialized = (wid) => { ++ if (!wid || typeof wid === 'string') return wid; ++ return ( ++ wid._serialized ?? ++ wid.$1 ?? ++ (typeof wid.toString === 'function' ? wid.toString() : undefined) ++ ); ++ }; ++ ++ /** ++ * Mirror `$1` onto `_serialized` in plain objects returned to Node so ++ * existing node-side code keeps reading `id._serialized`. ++ * Never overwrites an existing `_serialized` value. ++ * @param {*} obj ++ * @param {number} [depth=0] ++ * @returns {*} ++ */ ++ window.WWebJS.normalizeSerialized = (obj, depth = 0) => { ++ if (!obj || typeof obj !== 'object' || depth > 8) return obj; ++ if (Array.isArray(obj)) { ++ for (const item of obj) { ++ window.WWebJS.normalizeSerialized(item, depth + 1); ++ } ++ return obj; ++ } ++ if (obj.$1 !== undefined && obj._serialized === undefined) { ++ obj._serialized = obj.$1; ++ } ++ for (const key of Object.keys(obj)) { ++ const value = obj[key]; ++ if (value && typeof value === 'object') { ++ window.WWebJS.normalizeSerialized(value, depth + 1); ++ } ++ } ++ return obj; ++ }; ++ ++ /** + * Helper function that compares between two WWeb versions. Its purpose is to help the developer to choose the correct code implementation depending on the comparison value and the WWeb version. + * @param {string} lOperand The left operand for the WWeb version string to compare with + * @param {string} operator The comparison operator +@@ -830,13 +875,13 @@ + + if (typeof msg.id.remote === 'object') { + msg.id = Object.assign({}, msg.id, { +- remote: msg.id.remote._serialized, ++ remote: window.WWebJS.widSerialized(msg.id.remote), + }); + } + + delete msg.pendingAckUpdate; + +- return msg; ++ return window.WWebJS.normalizeSerialized(msg); + }; + + window.WWebJS.getChat = async (chatId, { getAsModel = true } = {}) => { +@@ -953,7 +998,7 @@ + model.isGroup = true; + const chatWid = window + .require('WAWebWidFactory') +- .createWid(chat.id._serialized); ++ .createWid(window.WWebJS.widSerialized(chat.id)); + const groupMetadata = + window.require('WAWebCollections').GroupMetadata || + window.require('WAWebCollections').WAWebGroupMetadataCollection; +@@ -981,28 +1026,32 @@ + + model.lastMessage = null; + if (model.msgs && model.msgs.length) { +- const lastMessage = chat.lastReceivedKey +- ? window +- .require('WAWebCollections') +- .Msg.get(chat.lastReceivedKey._serialized) || +- ( +- await window +- .require('WAWebCollections') +- .Msg.getMessagesById([ +- chat.lastReceivedKey._serialized, +- ]) +- )?.messages?.[0] +- : null; +- lastMessage && +- (model.lastMessage = +- window.WWebJS.getMessageModel(lastMessage)); ++ try { ++ const lastKeyId = window.WWebJS.widSerialized( ++ chat.lastReceivedKey, ++ ); ++ const lastMessage = lastKeyId ++ ? window.require('WAWebCollections').Msg.get(lastKeyId) || ++ ( ++ await window ++ .require('WAWebCollections') ++ .Msg.getMessagesById([lastKeyId]) ++ )?.messages?.[0] ++ : null; ++ lastMessage && ++ (model.lastMessage = ++ window.WWebJS.getMessageModel(lastMessage)); ++ } catch (_) { ++ // Key-format changes must not break the entire chat model ++ model.lastMessage = null; ++ } + } + + delete model.msgs; + delete model.msgUnsyncedButtonReplyMsgs; + delete model.unsyncedButtonReplies; + +- return model; ++ return window.WWebJS.normalizeSerialized(model); + }; + + window.WWebJS.getContactModel = (contact) => { +--- a/src/structures/Message.js ++++ b/src/structures/Message.js +@@ -74,7 +74,7 @@ + */ + this.from = + typeof data.from === 'object' && data.from !== null +- ? data.from._serialized ++ ? data.from._serialized ?? data.from.$1 + : data.from; + + /** +@@ -86,7 +86,7 @@ + */ + this.to = + typeof data.to === 'object' && data.to !== null +- ? data.to._serialized ++ ? data.to._serialized ?? data.to.$1 + : data.to; + + /** +@@ -95,7 +95,7 @@ + */ + this.author = + typeof data.author === 'object' && data.author !== null +- ? data.author._serialized ++ ? data.author._serialized ?? data.author.$1 + : data.author; + + /** From f6e2d6ab399d2afaa8937928261eac48a3407565 Mon Sep 17 00:00:00 2001 From: dafibh Date: Fri, 17 Jul 2026 01:12:46 +0800 Subject: [PATCH 2/3] fix: also normalize sendMessage return id ($1 rename) The send path returned `Msg.get(newMsgKey._serialized)`, which became `undefined` after WA renamed `_serialized` to `$1`, so `client.sendMessage` responded with a null message id. Use widSerialized(newMsgKey) so the sent message resolves and Client.sendMessage's getMessageModel call normalizes it. Verified on prod: send response now returns id._serialized / id.id, and deleting with that id straight from the send response succeeds. Co-Authored-By: Claude Opus 4.8 (1M context) --- patches/README.md | 4 +++- patches/whatsapp-web.js+1.34.7.patch | 9 +++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/patches/README.md b/patches/README.md index e80e589..52b17a4 100644 --- a/patches/README.md +++ b/patches/README.md @@ -19,7 +19,9 @@ message `delete`, etc.) throws a minified `r` error and returns HTTP 500. - `normalizeSerialized(obj)` - mirrors `$1` onto `_serialized` on objects returned to Node so existing node-side code keeps reading `id._serialized`. -Applied in `getMessageModel` / `getChatModel` and `Message` from/to/author. +Applied in `getMessageModel` / `getChatModel`, `Message` from/to/author, and the +`sendMessage` return (`Msg.get(newMsgKey._serialized)` returned `undefined` after +the rename, so the send response came back with a null message id). **API contract:** unchanged. Responses keep every original field (`id._serialized`, `remote`, `id`, `fromMe`, `from`, `to`, ...). The only diff --git a/patches/whatsapp-web.js+1.34.7.patch b/patches/whatsapp-web.js+1.34.7.patch index a4813c9..f5d42f2 100644 --- a/patches/whatsapp-web.js+1.34.7.patch +++ b/patches/whatsapp-web.js+1.34.7.patch @@ -52,6 +52,15 @@ * Helper function that compares between two WWeb versions. Its purpose is to help the developer to choose the correct code implementation depending on the comparison value and the WWeb version. * @param {string} lOperand The left operand for the WWeb version string to compare with * @param {string} operator The comparison operator +@@ -582,7 +627,7 @@ + + return window + .require('WAWebCollections') +- .Msg.get(newMsgKey._serialized); ++ .Msg.get(window.WWebJS.widSerialized(newMsgKey)); + }; + + window.WWebJS.editMessage = async (msg, content, options = {}) => { @@ -830,13 +875,13 @@ if (typeof msg.id.remote === 'object') { From b9bd45757b34a1a3ccb00ec66e03a1ce89fc696a Mon Sep 17 00:00:00 2001 From: dafibh Date: Fri, 18 Sep 2026 08:43:23 +0800 Subject: [PATCH 3/3] fix: patch whatsapp-web.js media send for WA Web 2.3000.1047xxx WhatsApp Web build 2.3000.1047xxx (seen 2026-09-17) gave the MediaData model its own private `__x_id`. Injected sendMessage spreads the media model into the outgoing message, so that key overwrote the MsgKey and every media send (image, video, audio, document) failed in Msg.initialize with "Data passed to getter must include an id property (it's how we memoize) but got undefined". Text sends were unaffected. Backport upstream PR wwebjs/whatsapp-web.js#201923 (unmerged): delete `message.__x_id` right after the message object is built. Added as a new hunk in the existing version-guarded 1.34.7 patch; other hunks unchanged apart from shifted line offsets. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01MkPU8ufB1bpefMxFdkaP1y --- patches/README.md | 10 ++++++++++ patches/whatsapp-web.js+1.34.7.patch | 20 ++++++++++++++++---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/patches/README.md b/patches/README.md index 52b17a4..17169e2 100644 --- a/patches/README.md +++ b/patches/README.md @@ -27,6 +27,16 @@ the rename, so the send response came back with a null message id). (`id._serialized`, `remote`, `id`, `fromMe`, `from`, `to`, ...). The only difference is an additive raw `$1` field alongside `_serialized` (same value). +**Also includes (media send fix):** WhatsApp Web build `2.3000.1047xxx` +(seen 2026-09-17) gave the `MediaData` model its own private `__x_id`. +`sendMessage` spreads the media model into the outgoing message, so that key +overwrote the Msg's id and every media send (image, video, audio, document) +failed with `Data passed to getter must include an id property (it's how we +memoize) but got undefined`. Text sends were unaffected. Backports upstream PR +[wwebjs/whatsapp-web.js#201923](https://github.com/wwebjs/whatsapp-web.js/pull/201923) +(unmerged as of 2026-09-18): `delete message.__x_id` right after the message +object is built. + **Version guard:** the Dockerfile applies this patch only when the installed `whatsapp-web.js` is exactly `1.34.7`. Any other version is skipped with a log line, so bumping the dependency will not break the build. diff --git a/patches/whatsapp-web.js+1.34.7.patch b/patches/whatsapp-web.js+1.34.7.patch index f5d42f2..77178b2 100644 --- a/patches/whatsapp-web.js+1.34.7.patch +++ b/patches/whatsapp-web.js+1.34.7.patch @@ -52,7 +52,19 @@ * Helper function that compares between two WWeb versions. Its purpose is to help the developer to choose the correct code implementation depending on the comparison value and the WWeb version. * @param {string} lOperand The left operand for the WWeb version string to compare with * @param {string} operator The comparison operator -@@ -582,7 +627,7 @@ +@@ -482,6 +527,11 @@ + ...botOptions, + ...extraOptions, + }; ++ ++ // MediaData is a model whose private __x_id field collides with Msg's ++ // internal id field when its enumerable properties are spread above, ++ // breaking getValidatedSender() during Msg initialization. ++ delete message.__x_id; + + // Bot's won't reply if canonicalUrl is set (linking) + if (botOptions) { +@@ -582,7 +632,7 @@ return window .require('WAWebCollections') @@ -61,7 +73,7 @@ }; window.WWebJS.editMessage = async (msg, content, options = {}) => { -@@ -830,13 +875,13 @@ +@@ -830,13 +880,13 @@ if (typeof msg.id.remote === 'object') { msg.id = Object.assign({}, msg.id, { @@ -77,7 +89,7 @@ }; window.WWebJS.getChat = async (chatId, { getAsModel = true } = {}) => { -@@ -953,7 +998,7 @@ +@@ -953,7 +1003,7 @@ model.isGroup = true; const chatWid = window .require('WAWebWidFactory') @@ -86,7 +98,7 @@ const groupMetadata = window.require('WAWebCollections').GroupMetadata || window.require('WAWebCollections').WAWebGroupMetadataCollection; -@@ -981,28 +1026,32 @@ +@@ -981,28 +1031,32 @@ model.lastMessage = null; if (model.msgs && model.msgs.length) {