whalibmob 5.5.82 → 5.5.83

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.
@@ -1127,14 +1127,30 @@ class MessageSender {
1127
1127
  : this._devMgr.ensureOwnDeviceSessions(ownPhone, this._signal)
1128
1128
  ]);
1129
1129
 
1130
- // Dedupe: a device reached through more than one path must still appear
1131
- // exactly once in <participants>. The sending device is filtered out again
1132
- // here in case a member list named us under an address the walk above did
1133
- // not recognise.
1130
+ // Every device this group resolves to, our own primary included, deduped so
1131
+ // a device reached through more than one path is counted once. whatsmeow
1132
+ // hashes GetUserDevices(groupParticipants) in exactly this shape — the whole
1133
+ // enumeration, sending device and all — and drops hosted endpoints first,
1134
+ // since those are bot-side and never real participants.
1135
+ const isHostedJid = (jid) => {
1136
+ const s = String(jid);
1137
+ return s.endsWith('@hosted') || s.endsWith('@hosted.lid');
1138
+ };
1139
+ const enumeratedDevices = [...new Set(
1140
+ [...memberDevices, ...lidDevices, ...ownTargets, selfJid]
1141
+ )].filter(jid => !isHostedJid(jid));
1142
+
1143
+ // The participant hash the server checks the fan-out against. whatsmeow puts
1144
+ // it on group stanzas (sendGroup sets node.Attrs["phash"]) and nowhere else;
1145
+ // it is computed over the enumeration above, not over the encryption targets,
1146
+ // so the sending device counts here even though it is not a recipient.
1147
+ const phash = computePhash(enumeratedDevices);
1148
+
1149
+ // Targets to actually encrypt for: everything but the sending device. Both
1150
+ // Baileys and whatsmeow skip it explicitly at this point and not before.
1134
1151
  const selfAddrs = new Set([adString(`${ownPhone}@s.whatsapp.net`)]);
1135
1152
  if (myLidUser) selfAddrs.add(adString(`${myLidUser}@lid`));
1136
- const allTargets = [...new Set([...memberDevices, ...lidDevices, ...ownTargets])]
1137
- .filter(jid => !selfAddrs.has(adString(jid)));
1153
+ const allTargets = enumeratedDevices.filter(jid => !selfAddrs.has(adString(jid)));
1138
1154
 
1139
1155
  const skStore = this._signal.senderKeyStore;
1140
1156
  const existingSkdmMap = skStore.getSKDMMap(groupJid);
@@ -1192,16 +1208,16 @@ class MessageSender {
1192
1208
  if (mediaSubtype) skmsgAttrs.mediatype = mediaSubtype;
1193
1209
  msgContent.push(new BinaryNode('enc', skmsgAttrs, Buffer.isBuffer(skmsgCiphertext) ? skmsgCiphertext : Buffer.from(skmsgCiphertext)));
1194
1210
 
1195
- // No phash on group stanzas. Baileys computes a participant hash only on
1196
- // the 1:1 path; a group message carries id/to/type/addressing_mode and
1197
- // nothing else. Sending a hash the server did not ask for — over a
1198
- // participant set it derives itself from the group — is a way to have the
1199
- // stanza rejected outright.
1211
+ // phash belongs on a group stanza and only on a group stanza — whatsmeow's
1212
+ // sendGroup sets it, sendDM does not. It tells the server which device set we
1213
+ // fanned out to; the server compares it against its own and echoes its hash
1214
+ // back on the ack when they differ.
1200
1215
  const stanzaAttrs = {
1201
1216
  to: jidStrToObj(groupJid),
1202
1217
  id: msgId,
1203
1218
  type: mediaType,
1204
1219
  addressing_mode: groupAddressingMode,
1220
+ phash,
1205
1221
  t: String(msNow())
1206
1222
  };
1207
1223
  if (options.edit) stanzaAttrs.edit = String(options.edit);
@@ -1223,7 +1239,27 @@ class MessageSender {
1223
1239
  }
1224
1240
 
1225
1241
  const msgNode = new BinaryNode('message', stanzaAttrs, msgContent);
1226
- return this._dispatchAndAck(msgNode, msgId);
1242
+ const dispatchResult = await this._dispatchAndAck(msgNode, msgId);
1243
+
1244
+ // A hash on the ack that differs from ours means the server's participant
1245
+ // list for this group is not the one we just fanned out to, so some members
1246
+ // did not get the message. whatsmeow drops its group cache on exactly this
1247
+ // signal; the device lists behind the members are dropped too, so the next
1248
+ // send re-runs group metadata and usync rather than repeating the miss.
1249
+ if (dispatchResult && dispatchResult.phash && dispatchResult.phash !== phash) {
1250
+ _whaDbg('[DBG] GROUP_PHASH_MISMATCH ours=' + phash +
1251
+ ' server=' + dispatchResult.phash + ' group=' + groupJid +
1252
+ ' — invalidating group metadata and device caches');
1253
+ if (typeof this._client.invalidateGroupMetadata === 'function') {
1254
+ this._client.invalidateGroupMetadata(groupJid);
1255
+ }
1256
+ const staleKeys = [...new Set(enumeratedDevices.map(jid =>
1257
+ String(jid).endsWith('@lid') ? 'lid:' + phoneFromJid(jid) : phoneFromJid(jid)
1258
+ ))];
1259
+ if (staleKeys.length) this._devMgr.clearCache(staleKeys);
1260
+ }
1261
+
1262
+ return dispatchResult;
1227
1263
  }
1228
1264
 
1229
1265
  // ─── Dispatch and wait for ack ────────────────────────────────────────────
@@ -60,13 +60,19 @@ function encodeDouble(fieldNum, value) {
60
60
  return Buffer.concat([t, buf]);
61
61
  }
62
62
 
63
+ // Plain text goes in conversation (field 1). Text that carries a contextInfo —
64
+ // a quoted reply, a mention — has to be an ExtendedTextMessage instead, which is
65
+ // field 6 of Message: { text=1, contextInfo=17 }.
66
+ //
67
+ // The wrapper was being written to field 38, which is orderMessage, so replies
68
+ // and mentions arrived as an unreadable order rather than as text.
63
69
  function encodeText(text, contextInfo) {
64
70
  if (contextInfo) {
65
71
  const inner = Buffer.concat([
66
72
  field(1, WIRE_LEN, str(text)),
67
73
  field(17, WIRE_LEN, encodeContextInfo(contextInfo))
68
74
  ]);
69
- return field(38, WIRE_LEN, inner);
75
+ return field(6, WIRE_LEN, inner);
70
76
  }
71
77
  return field(1, WIRE_LEN, str(text));
72
78
  }
@@ -260,8 +266,12 @@ function encodeMessage(type, payload) {
260
266
  case 'sticker': return field(26, WIRE_LEN, payload);
261
267
  case 'reaction': return field(46, WIRE_LEN, payload);
262
268
  case 'protocol': return field(12, WIRE_LEN, payload);
263
- case 'extendedText': return field(2, WIRE_LEN, payload);
264
- case 'poll': return field(85, WIRE_LEN, payload);
269
+ // extendedTextMessage is field 6. It used to be written to field 2, which is
270
+ // senderKeyDistributionMessage — so an edited message went out claiming to be
271
+ // a SenderKey distribution, and field 2 was not free for the real one.
272
+ case 'extendedText': return field(6, WIRE_LEN, payload);
273
+ // pollCreationMessage is field 49; field 85 is eventCoverImage.
274
+ case 'poll': return field(49, WIRE_LEN, payload);
265
275
  case 'location': return field(5, WIRE_LEN, payload);
266
276
  case 'contact': return field(4, WIRE_LEN, payload);
267
277
  default: return payload;
@@ -380,12 +390,18 @@ function decodeMessageContainer(buf) {
380
390
  if (dsm[2]) return decodeMessageContainer(dsm[2]);
381
391
  }
382
392
 
383
- // Field 35: senderKeyDistributionMessage — used to set up sender keys.
393
+ // Field 2: senderKeyDistributionMessage — used to set up sender keys.
384
394
  // In WhatsApp Multi-Device, SKDM is bundled WITH the first real message in a new
385
395
  // session (even for 1-on-1 DMs). We extract it and continue decoding the real message.
396
+ //
397
+ // This read field 35, which is messageContextInfo. Two things followed: a real
398
+ // distribution from another member (always at field 2) was never seen, so their
399
+ // group messages stayed undecryptable; and the messageContextInfo that ordinary
400
+ // messages carry was fed to processSKDM as if it were a distribution, which
401
+ // writes junk states into the sender-key record.
386
402
  let skdmInfo = null;
387
- if (f[35]) {
388
- const skdm = _decodeFields(f[35]);
403
+ if (f[2]) {
404
+ const skdm = _decodeFields(f[2]);
389
405
  // field 1 = groupId (string), field 2 = axolotlSenderKeyDistributionMessage (bytes)
390
406
  skdmInfo = {
391
407
  groupId: _str(skdm[1]),
@@ -403,10 +419,10 @@ function decodeMessageContainer(buf) {
403
419
  if (text) msgResult = { type: 'text', text };
404
420
  }
405
421
 
406
- // Field 2: extendedTextMessage — field 1 = text
407
- if (!msgResult && f[2] && Buffer.isBuffer(f[2])) {
422
+ // Field 6: extendedTextMessage — field 1 = text
423
+ if (!msgResult && f[6] && Buffer.isBuffer(f[6])) {
408
424
  try {
409
- const ext = _decodeFields(f[2]);
425
+ const ext = _decodeFields(f[6]);
410
426
  const text = _str(ext[1]);
411
427
  if (text) msgResult = { type: 'text', text };
412
428
  } catch (_) {}
@@ -552,11 +568,11 @@ function decodeMessageContainer(buf) {
552
568
  } catch (_) { msgResult = { type: 'reaction', emoji: '' }; }
553
569
  }
554
570
 
555
- // Field 85: pollCreationMessage
571
+ // Field 49: pollCreationMessage
556
572
  // encKey=1, name=2 (question), options=3 (repeated PollOption{name=1}), selectableOptionsCount=4
557
- if (!msgResult && f[85] && Buffer.isBuffer(f[85])) {
573
+ if (!msgResult && f[49] && Buffer.isBuffer(f[49])) {
558
574
  try {
559
- const poll = _decodeFields(f[85]);
575
+ const poll = _decodeFields(f[49]);
560
576
  const question = _str(poll[2]);
561
577
  const rawOpts = poll[3];
562
578
  const options = [];
@@ -574,8 +590,8 @@ function decodeMessageContainer(buf) {
574
590
  } catch (_) { msgResult = { type: 'poll', question: '', options: [] }; }
575
591
  }
576
592
 
577
- // Field 89: pollUpdateMessage (a vote on an existing poll)
578
- if (!msgResult && f[89] && Buffer.isBuffer(f[89])) {
593
+ // Field 50: pollUpdateMessage (a vote on an existing poll)
594
+ if (!msgResult && f[50] && Buffer.isBuffer(f[50])) {
579
595
  msgResult = { type: 'pollVote' };
580
596
  }
581
597
 
@@ -595,12 +611,20 @@ function decodeMessageContainer(buf) {
595
611
  }
596
612
  }
597
613
 
614
+ // ─── SenderKeyDistributionMessage (field 2 of Message) ───────────────────────
615
+ // SenderKeyDistributionMessage { groupId=1, axolotlSenderKeyDistributionMessage=2 }
616
+ //
617
+ // This was written to field 35, which in Message is messageContextInfo — an
618
+ // entirely different type. Every group SenderKey we distributed was therefore
619
+ // labelled as context metadata, so no recipient ever saw a distribution, no
620
+ // recipient could open the sender-key session, and every skmsg we sent showed up
621
+ // as "Waiting for this message" on their side. It is field 2 in WAProto.
598
622
  function encodeSenderKeyDistributionMessage(groupId, axolotlBytes) {
599
623
  const inner = Buffer.concat([
600
624
  field(1, WIRE_LEN, str(groupId)),
601
625
  field(2, WIRE_LEN, bytes(axolotlBytes))
602
626
  ]);
603
- return field(35, WIRE_LEN, inner);
627
+ return field(2, WIRE_LEN, inner);
604
628
  }
605
629
 
606
630
  module.exports = {
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const { dbg: _whaDbg } = require('../logger');
4
+
3
5
  const crypto = require('crypto');
4
6
  const fs = require('fs');
5
7
  const path = require('path');
@@ -74,6 +76,16 @@ function deriveMessageKeys(messageKey) {
74
76
 
75
77
  const _SK_DEBOUNCE_MS = 300;
76
78
 
79
+ // Bumped whenever a change means the SenderKey distributions we already sent
80
+ // were never usable by their recipients. The stored "who already has our key"
81
+ // map is then dropped once on load, so the next send to each group distributes
82
+ // again instead of trusting a record of deliveries that never landed.
83
+ //
84
+ // v2: the distribution was written to Message field 35 (messageContextInfo)
85
+ // instead of field 2, so no recipient ever received a readable one.
86
+ const SKDM_DISTRIBUTION_FORMAT = 2;
87
+ const SKDM_FORMAT_KEY = '__skdmFormat__';
88
+
77
89
  class SenderKeyStore {
78
90
  constructor() {
79
91
  this._data = {};
@@ -96,6 +108,27 @@ class SenderKeyStore {
96
108
  this._data = {};
97
109
  }
98
110
  }
111
+ this._migrateSKDMDistribution();
112
+ }
113
+
114
+ // Drop the "already distributed" bookkeeping once when the format changes.
115
+ // The sender keys themselves are kept — only the record of who has them is
116
+ // reset, so the next send to a group re-distributes to every member.
117
+ _migrateSKDMDistribution() {
118
+ if (this._data[SKDM_FORMAT_KEY] === SKDM_DISTRIBUTION_FORMAT) return;
119
+ let dropped = 0;
120
+ for (const key of Object.keys(this._data)) {
121
+ if (key.startsWith('__skdm__')) {
122
+ delete this._data[key];
123
+ dropped++;
124
+ }
125
+ }
126
+ this._data[SKDM_FORMAT_KEY] = SKDM_DISTRIBUTION_FORMAT;
127
+ this._save();
128
+ if (dropped > 0) {
129
+ _whaDbg('[DBG] SKDM_FORMAT_MIGRATION dropped ' + dropped +
130
+ ' stale distribution maps — every group re-distributes on next send');
131
+ }
99
132
  }
100
133
 
101
134
  _flushSync() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whalibmob",
3
- "version": "5.5.82",
3
+ "version": "5.5.83",
4
4
  "description": "WhatsApp library for interaction with WhatsApp Mobile API no web",
5
5
  "author": "Kunboruto50",
6
6
  "main": "index.js",