From 3b9d2fe6d19b81107622c9ee226f5b9bb2bd0cae Mon Sep 17 00:00:00 2001 From: dafibh Date: Fri, 17 Jul 2026 01:00:05 +0800 Subject: [PATCH 1/2] 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/2] 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') {