whalibmob 5.5.64 → 5.5.66
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/BinaryNode.js +27 -3
- package/lib/Client.js +36 -8
- package/lib/messages/MessageSender.js +44 -11
- package/package.json +1 -1
package/lib/BinaryNode.js
CHANGED
|
@@ -127,9 +127,25 @@ function _writeBytes(buf, parts) {
|
|
|
127
127
|
parts.push(buf);
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
// AD_JID agent byte ↔ domain. 0 = phone JIDs, 1 = LIDs.
|
|
131
|
+
const _AGENT_SERVER = { 0: 's.whatsapp.net', 1: 'lid' };
|
|
132
|
+
function _serverForAgent(agent) {
|
|
133
|
+
return _AGENT_SERVER[agent] || 's.whatsapp.net';
|
|
134
|
+
}
|
|
135
|
+
function _agentForServer(server) {
|
|
136
|
+
return server === 'lid' ? 1 : 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
130
139
|
function _writeJid(jid, parts) {
|
|
131
|
-
|
|
132
|
-
|
|
140
|
+
// Derive the agent from the domain only when this JID is going out as AD_JID
|
|
141
|
+
// anyway (explicit agent, or a device suffix). A plain LID keeps its
|
|
142
|
+
// JID_PAIR + 'lid' server-token encoding, which is what the server expects
|
|
143
|
+
// and what the rest of the codebase already produces.
|
|
144
|
+
const agent = (jid.agent !== undefined && jid.agent !== null)
|
|
145
|
+
? jid.agent
|
|
146
|
+
: (jid.device ? _agentForServer(jid.server) : 0);
|
|
147
|
+
if (agent || jid.device) {
|
|
148
|
+
parts.push(Buffer.from([TAG.AD_JID, agent || 0, jid.device || 0]));
|
|
133
149
|
_writeString(jid.user, parts);
|
|
134
150
|
} else {
|
|
135
151
|
parts.push(Buffer.from([TAG.JID_PAIR]));
|
|
@@ -289,7 +305,15 @@ class NodeDecoder {
|
|
|
289
305
|
const device = this._readByte();
|
|
290
306
|
const userToken = this._readByte();
|
|
291
307
|
const user = this._readString(userToken);
|
|
292
|
-
|
|
308
|
+
// The agent byte selects the domain — it is not decoration. Hardcoding
|
|
309
|
+
// s.whatsapp.net turned every LID participant (agent 1) into a phone JID
|
|
310
|
+
// that does not exist, so device queries for it came back empty, no Signal
|
|
311
|
+
// session was ever built, and group sends were rejected with ack 479.
|
|
312
|
+
const server = _serverForAgent(agent);
|
|
313
|
+
return {
|
|
314
|
+
user, agent, device, server,
|
|
315
|
+
toString() { return `${user}@${server}`; }
|
|
316
|
+
};
|
|
293
317
|
}
|
|
294
318
|
}
|
|
295
319
|
|
package/lib/Client.js
CHANGED
|
@@ -148,8 +148,11 @@ class WhalibmobClient extends EventEmitter {
|
|
|
148
148
|
this._pnToLid = new Map(); // phone number → LID user
|
|
149
149
|
this._myLid = null; // own LID JID received from <success> node
|
|
150
150
|
this._groupAddressingMode = new Map(); // groupJid → 'lid' | 'pn'
|
|
151
|
-
this._retryPending = new Map(); // msgId → {node,
|
|
152
|
-
this._retryPreKeyIdx = 0; // rotating index
|
|
151
|
+
this._retryPending = new Map(); // msgId → {node, count, pkId}
|
|
152
|
+
this._retryPreKeyIdx = 0; // rotating index, fallback when every prekey is reserved
|
|
153
|
+
// keyIds handed out by unresolved retries — never advertise one twice, or
|
|
154
|
+
// the second sender's pkmsg arrives after the key was consumed and deleted.
|
|
155
|
+
this._retryAdvertisedPreKeys = new Set();
|
|
153
156
|
this._appStateVersions = {}; // collectionName → version (int)
|
|
154
157
|
this._sentMsgCache = new Map(); // msgId → {plaintext, toJid, msgId, mediaType, options, recipientPhone}
|
|
155
158
|
this._tcTokenStore = null; // TcTokenStore — loaded in init()
|
|
@@ -686,6 +689,7 @@ class WhalibmobClient extends EventEmitter {
|
|
|
686
689
|
|
|
687
690
|
this._signal.decrypt(sigJid, encType, cipherBuf)
|
|
688
691
|
.then(async plaintext => {
|
|
692
|
+
this._resolveRetry(id);
|
|
689
693
|
const decoded = this._decodeMsg(plaintext);
|
|
690
694
|
_whaDbg('[DBG] DM_DECODED id=' + id + ' type=' + decoded.type + ' ptLen=' + (plaintext ? plaintext.length : 0) + ' hasSKDM=' + !!(decoded.skdm || decoded.type === 'senderKeyDistribution'));
|
|
691
695
|
|
|
@@ -756,6 +760,7 @@ class WhalibmobClient extends EventEmitter {
|
|
|
756
760
|
|
|
757
761
|
this._signal.senderKeyDecrypt(from, participant, cipherBuf)
|
|
758
762
|
.then(plaintext => {
|
|
763
|
+
this._resolveRetry(id);
|
|
759
764
|
const decoded = this._decodeMsg(plaintext);
|
|
760
765
|
if (decoded.type === 'senderKeyDistribution') return;
|
|
761
766
|
if (decoded.type === 'protocol') {
|
|
@@ -959,6 +964,17 @@ class WhalibmobClient extends EventEmitter {
|
|
|
959
964
|
// - <registration> 4-byte reg-id
|
|
960
965
|
// - <keys> block (from retry #1): type + identity + key + skey + device-identity
|
|
961
966
|
|
|
967
|
+
// A message we asked to be re-sent finally decrypted (or we gave up on it).
|
|
968
|
+
// Release its reserved prekey so the pool does not drain over a long session.
|
|
969
|
+
_resolveRetry(msgId) {
|
|
970
|
+
const pending = this._retryPending.get(msgId);
|
|
971
|
+
if (!pending) return;
|
|
972
|
+
if (pending.pkId !== null && pending.pkId !== undefined) {
|
|
973
|
+
this._retryAdvertisedPreKeys.delete(pending.pkId);
|
|
974
|
+
}
|
|
975
|
+
this._retryPending.delete(msgId);
|
|
976
|
+
}
|
|
977
|
+
|
|
962
978
|
_sendRetryRequest(msgId, origNode) {
|
|
963
979
|
const existing = this._retryPending.get(msgId);
|
|
964
980
|
const count = existing ? existing.count + 1 : 1;
|
|
@@ -966,13 +982,25 @@ class WhalibmobClient extends EventEmitter {
|
|
|
966
982
|
|
|
967
983
|
if (!this._socket || !this._connected) return;
|
|
968
984
|
|
|
969
|
-
//
|
|
970
|
-
|
|
971
|
-
|
|
985
|
+
// Pick a prekey that is not already advertised by another unresolved retry.
|
|
986
|
+
// A prekey is deleted the moment a pkmsg using it is decrypted, so handing
|
|
987
|
+
// the same one to two senders means the second arrives after the key is
|
|
988
|
+
// gone and dies with "Invalid PreKey ID". Indexing modulo the key count
|
|
989
|
+
// could not prevent that: the list shrinks as keys are consumed, so the
|
|
990
|
+
// rotating index wraps back onto ids that are still in flight.
|
|
991
|
+
let pkId = existing ? existing.pkId : null;
|
|
992
|
+
if (pkId === null && this._signal) {
|
|
972
993
|
const allKeys = this._signal.getPreKeysForUpload(800);
|
|
973
|
-
|
|
994
|
+
const free = allKeys.find(k => !this._retryAdvertisedPreKeys.has(k.keyId));
|
|
995
|
+
// If every key is already spoken for, fall back to rotating rather than
|
|
996
|
+
// sending no bundle at all — a stale id beats no retry.
|
|
997
|
+
const chosen = free || allKeys[this._retryPreKeyIdx++ % Math.max(1, allKeys.length)];
|
|
998
|
+
if (chosen) {
|
|
999
|
+
pkId = chosen.keyId;
|
|
1000
|
+
this._retryAdvertisedPreKeys.add(pkId);
|
|
1001
|
+
}
|
|
974
1002
|
}
|
|
975
|
-
this._retryPending.set(msgId, { node: origNode, count,
|
|
1003
|
+
this._retryPending.set(msgId, { node: origNode, count, pkId });
|
|
976
1004
|
|
|
977
1005
|
// Use original message timestamp (not current time)
|
|
978
1006
|
const origAttrs = (origNode && origNode.attrs) || {};
|
|
@@ -991,7 +1019,7 @@ class WhalibmobClient extends EventEmitter {
|
|
|
991
1019
|
const spk = this._signal.getSignedPreKeyForUpload();
|
|
992
1020
|
const identKey = this._signal.getIdentityKey();
|
|
993
1021
|
const allPreKeys = this._signal.getPreKeysForUpload(800);
|
|
994
|
-
const pk = allPreKeys
|
|
1022
|
+
const pk = allPreKeys.find(k => k.keyId === pkId) || allPreKeys[0];
|
|
995
1023
|
|
|
996
1024
|
if (spk && identKey && pk) {
|
|
997
1025
|
const keysChildren = [
|
|
@@ -839,10 +839,22 @@ class MessageSender {
|
|
|
839
839
|
|
|
840
840
|
// Determine addressing mode for this group (lid = modern groups, pn = legacy)
|
|
841
841
|
const groupAddressingMode = this._client._groupAddressingMode.get(groupJid) || 'lid';
|
|
842
|
-
//
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
842
|
+
// A stanza addressed with addressing_mode='lid' must carry participants in
|
|
843
|
+
// LID space only. Mixing @s.whatsapp.net targets into it is exactly what
|
|
844
|
+
// DeviceManager warns about, and the server answers it with ack 479.
|
|
845
|
+
const useLid = groupAddressingMode === 'lid';
|
|
846
|
+
if (useLid && !this._client._myLid) {
|
|
847
|
+
throw new Error(
|
|
848
|
+
'Cannot send to ' + groupJid + ': group is LID-addressed but our own LID ' +
|
|
849
|
+
'is unknown (the server did not supply lid= on <success>). ' +
|
|
850
|
+
'Reconnect so the LID is re-issued.'
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
const myLidUser = useLid ? phoneFromJid(this._client._myLid) : null;
|
|
854
|
+
// For LID-mode groups, sender identity is own LID JID; otherwise own PN JID.
|
|
855
|
+
// Normalised to the bare user so it matches the participant targets below.
|
|
856
|
+
const selfJid = useLid ? `${myLidUser}@lid` : ownJid;
|
|
857
|
+
const senderIdentity = selfJid;
|
|
846
858
|
|
|
847
859
|
const rawSKDM = await this._signal.buildSKDM(groupJid, senderIdentity);
|
|
848
860
|
const skdmMsg = encodeSenderKeyDistributionMessage(groupJid, rawSKDM);
|
|
@@ -850,16 +862,31 @@ class MessageSender {
|
|
|
850
862
|
// Resolve member phones for session/device queries.
|
|
851
863
|
// For LID members without a PN mapping, keep the raw LID user part so
|
|
852
864
|
// bulkEnsureSessionsForLid can still acquire sessions for them.
|
|
865
|
+
// "40778226909:2@s.whatsapp.net" → "<ourLid>:2@lid" — keep the device
|
|
866
|
+
// suffix, swap identity and domain.
|
|
867
|
+
const ownJidToLid = (jid) => {
|
|
868
|
+
const base = String(jid).replace(/@(?:s\.whatsapp\.net|lid)$/, '');
|
|
869
|
+
const colon = base.indexOf(':');
|
|
870
|
+
return myLidUser + (colon >= 0 ? base.slice(colon) : '') + '@lid';
|
|
871
|
+
};
|
|
872
|
+
|
|
853
873
|
const lidMembersWithoutPn = [];
|
|
854
874
|
const memberPhones = [...new Set(
|
|
855
875
|
members.map(jid => {
|
|
856
876
|
const isLid = jid.endsWith('@lid');
|
|
857
877
|
const raw = phoneFromJid(jid);
|
|
858
878
|
if (isLid) {
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
//
|
|
862
|
-
|
|
879
|
+
// In a LID-addressed group, resolve LID members through the LID path
|
|
880
|
+
// even when we know their phone number — swapping in the PN would put
|
|
881
|
+
// an @s.whatsapp.net target into a @lid stanza.
|
|
882
|
+
if (!useLid) {
|
|
883
|
+
const pn = this._client._lidToPn && this._client._lidToPn.get(raw);
|
|
884
|
+
if (pn) return pn;
|
|
885
|
+
}
|
|
886
|
+
// Our own LID is a member too — ensureOwnDeviceSessions already
|
|
887
|
+
// covers those devices, so listing it here would encrypt the SKDM
|
|
888
|
+
// for ourselves twice and emit duplicate <to> entries.
|
|
889
|
+
if (raw !== myLidUser) lidMembersWithoutPn.push(jid);
|
|
863
890
|
return null;
|
|
864
891
|
}
|
|
865
892
|
return raw;
|
|
@@ -879,11 +906,17 @@ class MessageSender {
|
|
|
879
906
|
this._devMgr.ensureOwnDeviceSessions(ownPhone, this._signal)
|
|
880
907
|
]);
|
|
881
908
|
|
|
882
|
-
|
|
909
|
+
// Own devices come back as @s.whatsapp.net — re-express them in LID space
|
|
910
|
+
// so every target in a LID stanza shares one address family.
|
|
911
|
+
const ownTargets = useLid ? ownDevices.map(ownJidToLid) : ownDevices;
|
|
912
|
+
|
|
913
|
+
// Dedupe: a device reached through more than one path must still appear
|
|
914
|
+
// exactly once in <participants>.
|
|
915
|
+
const allTargets = [...new Set([...memberDevices, ...lidDevices, ...ownTargets])];
|
|
883
916
|
|
|
884
|
-
const phashTargets = allTargets.includes(
|
|
917
|
+
const phashTargets = allTargets.includes(selfJid)
|
|
885
918
|
? allTargets
|
|
886
|
-
: [
|
|
919
|
+
: [selfJid, ...allTargets];
|
|
887
920
|
|
|
888
921
|
const skStore = this._signal.senderKeyStore;
|
|
889
922
|
const existingSkdmMap = skStore.getSKDMMap(groupJid);
|