whalibmob 5.1.19 → 5.1.20

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/Client.js CHANGED
@@ -103,27 +103,6 @@ function toSignalKey(raw) {
103
103
  return Buffer.from(raw);
104
104
  }
105
105
 
106
- // ─── JID helpers ─────────────────────────────────────────────────────────────
107
-
108
- // Convert a BinaryNode JID value to its canonical string form.
109
- // BinaryNode's _readAdJid() returns an object { user, agent, device, server }
110
- // with a toString() that yields "user@s.whatsapp.net". For LID-mode group
111
- // participants those objects represent LID identifiers and must be stored as
112
- // "user@lid" so the rest of the code can detect them with endsWith('@lid').
113
- // Regular pair-JIDs ({ user, server, toString }) are returned verbatim.
114
- function _adJidToLid(jidVal) {
115
- if (!jidVal) return '';
116
- // AD JID objects (from _readAdJid) have an `agent` property — these are LIDs
117
- if (typeof jidVal === 'object' && 'agent' in jidVal) {
118
- return (jidVal.user || '') + '@lid';
119
- }
120
- // Pair JID objects (from _readJidPair) have user + server but no agent
121
- if (typeof jidVal === 'object' && 'server' in jidVal) {
122
- return jidVal.user ? `${jidVal.user}@${jidVal.server}` : `@${jidVal.server}`;
123
- }
124
- return String(jidVal);
125
- }
126
-
127
106
  // ─── WhalibmobClient ──────────────────────────────────────────────────────────
128
107
 
129
108
  class WhalibmobClient extends EventEmitter {
@@ -149,7 +128,6 @@ class WhalibmobClient extends EventEmitter {
149
128
  this._lidToPn = new Map(); // LID user → phone number
150
129
  this._pnToLid = new Map(); // phone number → LID user
151
130
  this._myLid = null; // own LID JID received from <success> node
152
- this._myLidAgent = 0; // agent field from own LID JID (usually 1)
153
131
  this._groupAddressingMode = new Map(); // groupJid → 'lid' | 'pn'
154
132
  this._retryPending = new Map(); // msgId → {node, retryCount}
155
133
  this._retryPreKeyIdx = 0; // rotating index for assigning unique prekeys to retries
@@ -306,10 +284,7 @@ class WhalibmobClient extends EventEmitter {
306
284
  }
307
285
  }
308
286
  if (attrs.platform) this._platform = attrs.platform;
309
- if (attrs.lid) {
310
- this._myLid = _adJidToLid(attrs.lid);
311
- this._myLidAgent = (typeof attrs.lid === 'object' && attrs.lid.agent) ? attrs.lid.agent : 0;
312
- }
287
+ if (attrs.lid) this._myLid = String(attrs.lid);
313
288
 
314
289
  const devIdNode = findChild(node, 'device-identity');
315
290
  if (devIdNode) {
@@ -822,9 +797,6 @@ class WhalibmobClient extends EventEmitter {
822
797
  const cls = attrs.class || '';
823
798
 
824
799
  if (cls === 'message') {
825
- if (attrs.error) {
826
- process.stderr.write('[DBG] ACK_ERR full node=' + JSON.stringify(node, (k,v) => Buffer.isBuffer(v) ? '<buf>' : v) + '\n');
827
- }
828
800
  const handler = this._pendingAcks.get(id);
829
801
  if (handler) {
830
802
  this._pendingAcks.delete(id);
@@ -1212,17 +1184,12 @@ class WhalibmobClient extends EventEmitter {
1212
1184
  const participants = children
1213
1185
  .filter(n => n && n.description === 'participant' && n.attrs && n.attrs.jid)
1214
1186
  .map(n => {
1215
- // AD JID objects (LID participants) have an `agent` property → convert to @lid form
1216
- const pJid = _adJidToLid(n.attrs.jid);
1187
+ const pJid = String(n.attrs.jid);
1217
1188
  const pRole = n.attrs.type || 'member';
1218
- // phone_number may also be a JID object → extract user part (PN without server)
1219
- const rawPhone = n.attrs.phone_number;
1220
- const pPhone = rawPhone
1221
- ? (typeof rawPhone === 'object' ? rawPhone.user : String(rawPhone).split('@')[0])
1222
- : null;
1223
- const pLid = n.attrs.lid ? _adJidToLid(n.attrs.lid) : null;
1189
+ const pPhone = n.attrs.phone_number ? String(n.attrs.phone_number) : null;
1190
+ const pLid = n.attrs.lid ? String(n.attrs.lid) : null;
1224
1191
  // Populate LID↔PN maps from participant attributes
1225
- // Case A: participant JID is a LID (ends with @lid), phone_number is their PN
1192
+ // Case A: participant JID is a LID, phone_number is their PN
1226
1193
  if (pJid.endsWith('@lid')) {
1227
1194
  const lidUser = pJid.split('@')[0].split(':')[0];
1228
1195
  if (pPhone) {
@@ -1330,18 +1297,12 @@ class WhalibmobClient extends EventEmitter {
1330
1297
  const response = await this._sendIq(node);
1331
1298
  if (!response) return [];
1332
1299
 
1333
- // Response structure: <iq><groups><group/><group/>...</groups></iq>
1334
- // Unwrap the <groups> container first, then collect <group> nodes
1335
- const iqChildren = Array.isArray(response.content) ? response.content : [];
1336
- let groupNodes = [];
1337
- const groupsWrapper = iqChildren.find(n => n && n.description === 'groups');
1338
- if (groupsWrapper && Array.isArray(groupsWrapper.content)) {
1339
- groupNodes = groupsWrapper.content.filter(n => n && n.description === 'group');
1340
- } else {
1341
- // Fallback: direct <group> children (older protocol variant)
1342
- groupNodes = iqChildren.filter(n => n && n.description === 'group');
1343
- }
1344
- const groups = groupNodes.map(n => this._parseGroupNode(n)).filter(Boolean);
1300
+ // Response content is a list of <group> nodes
1301
+ const children = Array.isArray(response.content) ? response.content : [];
1302
+ const groups = children
1303
+ .filter(n => n && n.description === 'group')
1304
+ .map(n => this._parseGroupNode(n))
1305
+ .filter(Boolean);
1345
1306
  return groups;
1346
1307
  }
1347
1308
 
@@ -5,24 +5,17 @@ const { BinaryNode } = require('./BinaryNode');
5
5
  // ─── JID helpers ─────────────────────────────────────────────────────────────
6
6
 
7
7
  function stripUser(jid) {
8
- const at = jid.indexOf('@');
9
- const full = at >= 0 ? jid.slice(0, at) : jid;
10
- const colon = full.indexOf(':');
11
- const base = colon >= 0 ? full.slice(0, colon) : full;
12
- const device = colon >= 0 ? parseInt(full.slice(colon + 1), 10) : 0;
13
- const under = base.lastIndexOf('_');
14
- if (under >= 0 && /^\d+$/.test(base.slice(under + 1))) {
15
- return { user: base.slice(0, under), agent: parseInt(base.slice(under + 1), 10), device };
16
- }
17
- return { user: base, agent: 0, device };
8
+ const at = jid.indexOf('@');
9
+ const user = at >= 0 ? jid.slice(0, at) : jid;
10
+ const colon = user.indexOf(':');
11
+ if (colon >= 0) return { user: user.slice(0, colon), device: parseInt(user.slice(colon + 1), 10) };
12
+ return { user, device: 0 };
18
13
  }
19
14
 
20
- function makeDeviceJid(user, device, server, agent) {
15
+ function makeDeviceJid(user, device, server) {
21
16
  server = server || 's.whatsapp.net';
22
- agent = agent || 0;
23
- const agentStr = agent !== 0 ? `_${agent}` : '';
24
- if (!device || device === 0) return `${user}${agentStr}@${server}`;
25
- return `${user}${agentStr}:${device}@${server}`;
17
+ if (!device || device === 0) return `${user}@${server}`;
18
+ return `${user}:${device}@${server}`;
26
19
  }
27
20
 
28
21
  function phoneFromJid(jid) {
@@ -130,22 +123,7 @@ class DeviceManager {
130
123
  async fetchBundles(jids) {
131
124
  if (!jids || jids.length === 0) return new Map();
132
125
 
133
- // Build proper JID objects for binary encoding. Passing a plain string produces a raw
134
- // bytes attribute, but the server requires a binary-encoded JID (JID_PAIR or AD_JID).
135
- // E.g. "112713111982325_1:86@s.whatsapp.net" → { user:"112713111982325", agent:1, device:86, server:"s.whatsapp.net" }
136
- const userNodes = jids.map(jid => {
137
- const atIdx = jid.indexOf('@');
138
- const server = atIdx >= 0 ? jid.slice(atIdx + 1) : 's.whatsapp.net';
139
- const userPart = atIdx >= 0 ? jid.slice(0, atIdx) : jid;
140
- const colonIdx = userPart.indexOf(':');
141
- const base = colonIdx >= 0 ? userPart.slice(0, colonIdx) : userPart;
142
- const device = colonIdx >= 0 ? parseInt(userPart.slice(colonIdx + 1), 10) : 0;
143
- const underIdx = base.lastIndexOf('_');
144
- const hasAgent = underIdx >= 0 && /^\d+$/.test(base.slice(underIdx + 1));
145
- const user = hasAgent ? base.slice(0, underIdx) : base;
146
- const agent = hasAgent ? parseInt(base.slice(underIdx + 1), 10) : 0;
147
- return new BinaryNode('user', { jid: { user, agent: agent || 0, device: device || 0, server } }, null);
148
- });
126
+ const userNodes = jids.map(jid => { const lidMatch = jid.match(/(\d{15,})@s\.whatsapp\.net/); return new BinaryNode('user', { jid }, null); });
149
127
  const iqId = this._client._genMsgId();
150
128
  const iqNode = new BinaryNode('iq',
151
129
  { id: iqId, xmlns: 'encrypt', type: 'get', to: 's.whatsapp.net' },
@@ -160,28 +138,10 @@ class DeviceManager {
160
138
  const children = listNode ? listNode.content : (response.content || []);
161
139
  for (const userNode of (Array.isArray(children) ? children : [])) {
162
140
  if (!userNode || userNode.description !== 'user') continue;
163
- const rawJid = userNode.attrs && userNode.attrs.jid;
164
- if (!rawJid) continue;
165
- // Convert the binary JID object to a canonical string "user_agent:device@server"
166
- let jidKey;
167
- if (typeof rawJid === 'string') {
168
- jidKey = rawJid;
169
- } else {
170
- const u = rawJid.user || '';
171
- const ag = rawJid.agent || 0;
172
- const dv = rawJid.device || 0;
173
- const sv = rawJid.server || 's.whatsapp.net';
174
- const agStr = ag !== 0 ? `_${ag}` : '';
175
- const dvStr = dv !== 0 ? `:${dv}` : '';
176
- jidKey = `${u}${agStr}${dvStr}@${sv}`;
177
- }
141
+ const jid = userNode.attrs && userNode.attrs.jid;
142
+ if (!jid) continue;
178
143
  const bundle = parseBundleFromUserNode(userNode);
179
- if (bundle) {
180
- process.stderr.write('[DBG] BUNDLE jid=' + jidKey + ' regId=' + bundle.registrationId + ' preKeyId=' + (bundle.preKey && bundle.preKey.keyId) + ' spkId=' + (bundle.signedPreKey && bundle.signedPreKey.keyId) + '\n');
181
- bundles.set(jidKey, bundle);
182
- } else {
183
- process.stderr.write('[DBG] BUNDLE_FAIL jid=' + jidKey + ' no bundle parsed\n');
184
- }
144
+ if (bundle) bundles.set(jid, bundle);
185
145
  }
186
146
  return bundles;
187
147
  }
@@ -239,17 +199,17 @@ class DeviceManager {
239
199
  }
240
200
 
241
201
  // Actual usync IQ — only called for phones NOT in _deviceCache
242
- // Uses the mobile API format (cobalt-compatible):
243
- // <list> uses <user jid="phone@s.whatsapp.net"/> (not <user><contact>+phone</contact></user>)
244
- // <query> uses empty <devices version="2"/> (no individual <device> children)
245
- // Includes <side_list/> sibling required by the mobile API
246
202
  async _doUsyncIq(phones) {
247
203
  const iqId = this._client._genMsgId();
248
204
  const sid = this._client._genMsgId();
249
205
 
250
- // Mobile-API format: <user jid="{user}@s.whatsapp.net"/> as JID pair object
251
206
  const listChildren = phones.map(p =>
252
- new BinaryNode('user', { jid: { user: String(p), server: 's.whatsapp.net' } }, null)
207
+ new BinaryNode('user', {},
208
+ [new BinaryNode('contact', {}, Buffer.from('+' + p))]
209
+ )
210
+ );
211
+ const deviceChildren = phones.map(p =>
212
+ new BinaryNode('device', { jid: makeDeviceJid(p, 0) }, null)
253
213
  );
254
214
 
255
215
  const iqNode = new BinaryNode('iq',
@@ -258,77 +218,34 @@ class DeviceManager {
258
218
  { sid, mode: 'query', last: 'true', index: '0', context: 'message' },
259
219
  [
260
220
  new BinaryNode('query', {},
261
- [new BinaryNode('devices', { version: '2' }, null)]
221
+ [new BinaryNode('devices', { version: '2' }, deviceChildren)]
262
222
  ),
263
- new BinaryNode('list', {}, listChildren),
264
- new BinaryNode('side_list', {}, null)
223
+ new BinaryNode('list', {}, listChildren)
265
224
  ]
266
225
  )]
267
226
  );
268
227
 
269
228
  const response = await this._client._sendIq(iqNode).catch(() => null);
270
- process.stderr.write('[DBG] _doUsyncIq phones=' + phones.join(',') + ' response=' + (response ? response.description : 'null') + '\n');
271
- if (!response) {
272
- // Timeout: cache device=0 as fallback
273
- for (const p of phones) {
274
- if (!this._deviceCache.has(p)) this._deviceCache.set(p, new Set([0]));
275
- }
276
- return;
277
- }
278
-
279
- // Cobalt response structure:
280
- // <iq><usync><list><user jid="..."><devices><device-list><device id="N"/></device-list></devices></user></list></usync></iq>
281
- this._parseUsyncResponse(response, phones);
282
- }
283
229
 
284
- // Parse a usync IQ response (works for both PN and LID usync)
285
- // Populates _deviceCache: phone/user Set<deviceId>
286
- _parseUsyncResponse(response, expectedUsers) {
287
- const self = this;
288
- function walkUser(userNode) {
289
- const rawJid = userNode.attrs && userNode.attrs.jid;
290
- if (!rawJid) return;
291
- process.stderr.write('[DBG] _parseUsyncResponse walkUser rawJid=' + JSON.stringify(rawJid) + '\n');
292
- const user = typeof rawJid === 'object' ? rawJid.user : String(rawJid).split('@')[0];
293
- if (!user) return;
294
-
295
- // Look for <devices><device-list><device id="N"/>
296
- const devicesNode = Array.isArray(userNode.content)
297
- ? userNode.content.find(n => n && n.description === 'devices') : null;
298
- if (!devicesNode || !Array.isArray(devicesNode.content)) {
299
- // No devices node: still cache device=0 so we don't query again
300
- if (!self._deviceCache.has(user)) self._deviceCache.set(user, new Set([0]));
301
- return;
302
- }
303
- const deviceListNode = devicesNode.content.find(n => n && n.description === 'device-list');
304
- const deviceNodes = (deviceListNode && Array.isArray(deviceListNode.content))
305
- ? deviceListNode.content.filter(n => n && n.description === 'device')
306
- : [];
307
-
308
- if (!self._deviceCache.has(user)) self._deviceCache.set(user, new Set());
309
- const devSet = self._deviceCache.get(user);
310
- if (deviceNodes.length === 0) {
311
- devSet.add(0);
312
- } else {
313
- for (const dn of deviceNodes) {
314
- const devId = parseInt((dn.attrs && dn.attrs.id) || '0', 10);
315
- devSet.add(isNaN(devId) ? 0 : devId);
316
- }
317
- }
318
- }
319
-
320
- // Walk: iq → usync → list → user
230
+ // Walk response and populate cache
231
+ const found = new Set();
321
232
  function walk(node) {
322
233
  if (!node) return;
323
- if (node.description === 'user') { walkUser(node); return; }
324
- if (Array.isArray(node.content)) node.content.forEach(walk);
234
+ if (node.description === 'device' && node.attrs && node.attrs.jid) {
235
+ const jid = node.attrs.jid;
236
+ const { user, device } = stripUser(jid);
237
+ if (!found.has(user)) found.add(user);
238
+ if (!this._deviceCache.has(user)) this._deviceCache.set(user, new Set());
239
+ this._deviceCache.get(user).add(device);
240
+ }
241
+ if (Array.isArray(node.content)) node.content.forEach(walk.bind(this));
325
242
  }
326
- walk(response);
243
+ if (response) walk.call(this, response);
327
244
 
328
- // Ensure any expected user not seen in response gets device=0 cached
329
- if (expectedUsers) {
330
- for (const u of expectedUsers) {
331
- if (!self._deviceCache.has(u)) self._deviceCache.set(u, new Set([0]));
245
+ // For any phone that returned no devices, cache device=0 so we don't query again
246
+ for (const p of phones) {
247
+ if (!this._deviceCache.has(p)) {
248
+ this._deviceCache.set(p, new Set([0]));
332
249
  }
333
250
  }
334
251
  }
@@ -433,166 +350,6 @@ class DeviceManager {
433
350
  return `${user}.${device}`;
434
351
  }
435
352
 
436
- // ─── usync device list query by full JID (LID or PN) ─────────────────────
437
- //
438
- // Used for LID-mode group members that have no LID→PN mapping in _lidToPn.
439
- // Sends <user jid="..."/> nodes directly instead of <user><contact>+phone</contact></user>,
440
- // which allows the server to resolve devices for LID JIDs natively.
441
- //
442
- async _usyncGetDevicesByJid(jids) {
443
- if (!jids || jids.length === 0) return [];
444
-
445
- const iqId = this._client._genMsgId();
446
- const sid = this._client._genMsgId();
447
-
448
- // Build proper JID objects for each input JID (e.g. "112713111982325@lid")
449
- const listChildren = jids.map(jidStr => {
450
- const at = jidStr.indexOf('@');
451
- const user = at >= 0 ? jidStr.slice(0, at) : jidStr;
452
- const server = at >= 0 ? jidStr.slice(at + 1) : 's.whatsapp.net';
453
- return new BinaryNode('user', { jid: { user, server } }, null);
454
- });
455
-
456
- const iqNode = new BinaryNode('iq',
457
- { id: iqId, to: 's.whatsapp.net', type: 'get', xmlns: 'usync' },
458
- [new BinaryNode('usync',
459
- { sid, mode: 'query', last: 'true', index: '0', context: 'message' },
460
- [
461
- new BinaryNode('query', {},
462
- [new BinaryNode('devices', { version: '2' }, null)]
463
- ),
464
- new BinaryNode('list', {}, listChildren),
465
- new BinaryNode('side_list', {}, null)
466
- ]
467
- )]
468
- );
469
-
470
- const response = await this._client._sendIq(iqNode).catch(() => null);
471
-
472
- if (!response) {
473
- process.stderr.write('[DBG] _usyncGetDevicesByJid TIMEOUT for jids=' + jids.join(',') + '\n');
474
- // Fallback: return main device for each input JID
475
- return jids.map(j => {
476
- const { user } = stripUser(j);
477
- const server = j.includes('@lid') ? 'lid' : 's.whatsapp.net';
478
- return `${user}@${server}`;
479
- });
480
- }
481
-
482
- // Parse response: collect (user, deviceId) pairs → build device JID strings
483
- // Response structure: <iq><usync><list><user jid="..."><devices><device-list><device id="N"/></device-list></devices></user></list></usync></iq>
484
- const foundJids = [];
485
- function walkUser(userNode) {
486
- const rawJid = userNode.attrs && userNode.attrs.jid;
487
- if (!rawJid) return;
488
- process.stderr.write('[DBG] walkUser rawJid=' + JSON.stringify(rawJid) + '\n');
489
- const user = typeof rawJid === 'object' ? rawJid.user : String(rawJid).split('@')[0];
490
- const server = typeof rawJid === 'object' ? rawJid.server : (String(rawJid).split('@')[1] || 's.whatsapp.net');
491
- const agent = typeof rawJid === 'object' ? (rawJid.agent || 0) : 0;
492
- const agentStr = agent !== 0 ? `_${agent}` : '';
493
- if (!user) return;
494
-
495
- const devicesNode = Array.isArray(userNode.content) ? userNode.content.find(n => n && n.description === 'devices') : null;
496
- const deviceListNode = devicesNode && Array.isArray(devicesNode.content) ? devicesNode.content.find(n => n && n.description === 'device-list') : null;
497
- const deviceNodes = deviceListNode && Array.isArray(deviceListNode.content) ? deviceListNode.content.filter(n => n && n.description === 'device') : [];
498
-
499
- if (deviceNodes.length === 0) {
500
- foundJids.push(`${user}${agentStr}@${server}`);
501
- } else {
502
- for (const dn of deviceNodes) {
503
- const devId = parseInt((dn.attrs && dn.attrs.id) || '0', 10);
504
- const dev = isNaN(devId) ? 0 : devId;
505
- foundJids.push(dev === 0 ? `${user}${agentStr}@${server}` : `${user}${agentStr}:${dev}@${server}`);
506
- }
507
- }
508
- }
509
- function walk(node) {
510
- if (!node) return;
511
- if (node.description === 'user') { walkUser(node); return; }
512
- if (Array.isArray(node.content)) node.content.forEach(walk);
513
- }
514
- walk(response);
515
-
516
- process.stderr.write('[DBG] _usyncGetDevicesByJid jids=' + jids.length + ' found=' + foundJids.length + ' ' + foundJids.join(',') + '\n');
517
-
518
- // Fallback: if server returned nothing, use the main device JID for each input
519
- if (foundJids.length === 0) {
520
- return jids.map(j => {
521
- const { user } = stripUser(j);
522
- const server = j.includes('@lid') ? 'lid' : 's.whatsapp.net';
523
- return `${user}@${server}`;
524
- });
525
- }
526
- return foundJids;
527
- }
528
-
529
- // ─── Ensure sessions for LID JID members (no PN mapping available) ─────────
530
- //
531
- // Called from _sendGroupMessage for any LID group member where _lidToPn has
532
- // no entry. Queries usync by JID instead of phone number so the server can
533
- // return the correct device list for LID-addressed participants.
534
- //
535
- async bulkEnsureSessionsByLidJids(lidJids, signalProto, allowPkmsg = true) {
536
- if (!lidJids || lidJids.length === 0) return [];
537
-
538
- const deviceJids = await this._usyncGetDevicesByJid(lidJids);
539
-
540
- // In LID mode, ALL devices (including device=0) may have pre-keys and need
541
- // Signal sessions. We try to fetch bundles for every device; if the server
542
- // returns 406 for device=0 (no pre-keys available) we simply skip that JID —
543
- // but we still include it in the result so it appears in phash/SKDM list.
544
- const readyJids = [];
545
- const fetchJids = [];
546
- const _sessKeys = Object.keys(signalProto.store._sessions || {});
547
- process.stderr.write('[DBG] bulkEnsureLid storedSessionKeys=' + JSON.stringify(_sessKeys) + '\n');
548
- for (const jid of deviceJids) {
549
- const addrKey = this._jidToAddr(jid);
550
- const hasSess = signalProto.store.hasSession(addrKey);
551
- process.stderr.write('[DBG] hasSession jid=' + jid + ' addrKey=' + addrKey + ' result=' + hasSess + '\n');
552
- if (hasSess) {
553
- readyJids.push(jid);
554
- } else {
555
- fetchJids.push(jid);
556
- }
557
- }
558
-
559
- process.stderr.write('[DBG] bulkEnsureLid deviceJids=' + JSON.stringify(deviceJids) + ' fetchJids=' + JSON.stringify(fetchJids) + '\n');
560
-
561
- if (allowPkmsg && fetchJids.length > 0) {
562
- const bundles = await this.fetchBundles(fetchJids);
563
- process.stderr.write('[DBG] bulkEnsureLid bundles.size=' + bundles.size + '\n');
564
- for (const [jid, bundle] of bundles) {
565
- try {
566
- await signalProto.buildSessionFromBundle(jid, bundle);
567
- readyJids.push(typeof jid === 'string' ? jid : String(jid));
568
- } catch (e) {
569
- process.stderr.write('[DBG] bulkEnsureSessionsByLidJids bundle err jid=' + jid + ' ' + e.message + '\n');
570
- }
571
- }
572
- // Any device we couldn't build a session for (e.g. 406, no pre-keys) still
573
- // needs to appear in the result for phash correctness.
574
- const sessionBuilt = new Set(readyJids);
575
- for (const jid of fetchJids) {
576
- if (!sessionBuilt.has(jid)) readyJids.push(jid);
577
- }
578
- } else {
579
- // All devices already have sessions; add any that were not in fetchJids
580
- for (const jid of deviceJids) {
581
- if (!readyJids.includes(jid)) readyJids.push(jid);
582
- }
583
- }
584
-
585
- const result = readyJids;
586
-
587
- // Fallback: if still no secondaries found, return ALL device JIDs so that at
588
- // least an SKDM attempt is made.
589
- if (result.length === 0) {
590
- return deviceJids;
591
- }
592
-
593
- return result;
594
- }
595
-
596
353
  // ─── Invalidate cached device list (used on phash mismatch / 421 retry) ───
597
354
  clearCache(phones) {
598
355
  if (!phones || phones.length === 0) {
@@ -608,23 +365,7 @@ class DeviceManager {
608
365
  const toNodes = encryptedList.map(({ jid, type, ciphertext }) => {
609
366
  const encAttrs = { type, v: '2' };
610
367
  if (mediaSubtype) encAttrs.mediatype = mediaSubtype;
611
- // Convert string JIDs to binary-encodable JID objects.
612
- // "user_agent:device@server" → { user, agent, device, server }
613
- let jidObj = jid;
614
- if (typeof jid === 'string') {
615
- const atIdx = jid.indexOf('@');
616
- const server = atIdx >= 0 ? jid.slice(atIdx + 1) : 's.whatsapp.net';
617
- const userPart = atIdx >= 0 ? jid.slice(0, atIdx) : jid;
618
- const colonIdx = userPart.indexOf(':');
619
- const base = colonIdx >= 0 ? userPart.slice(0, colonIdx) : userPart;
620
- const device = colonIdx >= 0 ? parseInt(userPart.slice(colonIdx + 1), 10) : 0;
621
- const underIdx = base.lastIndexOf('_');
622
- const hasAgent = underIdx >= 0 && /^\d+$/.test(base.slice(underIdx + 1));
623
- const user = hasAgent ? base.slice(0, underIdx) : base;
624
- const agent = hasAgent ? parseInt(base.slice(underIdx + 1), 10) : 0;
625
- jidObj = { user, agent: agent || 0, device: device || 0, server };
626
- }
627
- return new BinaryNode('to', { jid: jidObj },
368
+ return new BinaryNode('to', { jid },
628
369
  [new BinaryNode('enc', encAttrs, ciphertext)]
629
370
  );
630
371
  });
@@ -102,22 +102,37 @@ function _getCurve() {
102
102
  return _curve25519js;
103
103
  }
104
104
 
105
- // Return ADVSignedDeviceIdentity if we have one from the server (received at login).
106
- // For phone-based (non-WA-Web) registrations the server never sends device-identity
107
- // in the <success> node, so store.advIdentity stays null. In that case we return
108
- // null — the pkmsg stanza is sent WITHOUT a <device-identity> child, which is valid
109
- // for primary mobile devices and does NOT cause RETRY at the recipient.
110
- //
111
- // NOTE: Building a self-signed ADV from our own keys caused RETRY at recipient because
112
- // WhatsApp verifies the signature chain against the server-known account key.
105
+ // Build ADVSignedDeviceIdentity for our primary iOS device.
106
+ // Returns Buffer of encoded proto WITH accountSignatureKey included.
107
+ // Result is cached in store.advIdentity so we only build it once per registration.
113
108
  function _buildOrGetAdvIdentity(store) {
114
- if (!store.advIdentity) return null;
115
- const adv = Buffer.isBuffer(store.advIdentity)
116
- ? store.advIdentity
117
- : Buffer.from(store.advIdentity);
118
- if (adv.length === 0) return null;
119
- process.stderr.write('[DBG] ADV_PRESENT from server (' + adv.length + 'b)\n');
120
- return adv;
109
+ if (store.advIdentity && store.advIdentity.length > 0) return store.advIdentity;
110
+
111
+ try {
112
+ const curve = _getCurve();
113
+ const pubRaw = Buffer.from(store.identityKeyPair.public, 'base64');
114
+ const privRaw = Buffer.from(store.identityKeyPair.private, 'base64');
115
+ const pub32 = pubRaw.length === 33 ? pubRaw.slice(1) : pubRaw;
116
+ const priv32 = privRaw.length === 32 ? privRaw : privRaw.slice(0, 32);
117
+
118
+ const ts = Math.floor(Date.now() / 1000);
119
+ const regId = store.registrationId || 0;
120
+ const details = _encodeADVDetails(regId, ts, 0);
121
+
122
+ const acctMsg = Buffer.concat([WA_ADV_ACCOUNT_SIG_PREFIX, details, pub32]);
123
+ const acctSig = Buffer.from(curve.sign(priv32, acctMsg));
124
+
125
+ const devMsg = Buffer.concat([WA_ADV_DEVICE_SIG_PREFIX, details, pub32, pub32]);
126
+ const devSig = Buffer.from(curve.sign(priv32, devMsg));
127
+
128
+ const adv = _encodeADVSignedIdentity(details, pub32, acctSig, devSig);
129
+ store.advIdentity = adv;
130
+ process.stderr.write('[DBG] ADV_BUILT from primary iOS keys (' + adv.length + 'b)\n');
131
+ return adv;
132
+ } catch (e) {
133
+ process.stderr.write('[DBG] ADV_BUILD_ERR: ' + e.message + '\n');
134
+ return null;
135
+ }
121
136
  }
122
137
 
123
138
  function makeJid(input, server) {
@@ -606,10 +621,6 @@ class MessageSender {
606
621
  const ownPhone = String(this._store.phoneNumber);
607
622
  const ownJid = `${ownPhone}@s.whatsapp.net`;
608
623
  const mediaSubtype = options._mediaSubtype || null;
609
- // LID-based own user number (e.g. "90079053799508"), used for phash in LID-mode groups.
610
- // usync returns "@s.whatsapp.net" JIDs with the LID numeric ID as user when queried by
611
- // LID JID, so the phash sender entry must use this number, NOT the phone number.
612
- const myLidUser = this._client._myLid ? phoneFromJid(this._client._myLid) : null;
613
624
 
614
625
  // Auto-fetch group metadata if member cache is empty (first send to this group)
615
626
  let members = this._client._getGroupMembers(groupJid);
@@ -623,12 +634,7 @@ class MessageSender {
623
634
 
624
635
  // Determine addressing mode for this group (lid = modern groups, pn = legacy)
625
636
  const groupAddressingMode = this._client._groupAddressingMode.get(groupJid) || 'lid';
626
- // senderIdentity: For LID-mode groups the server tags our outbound message with
627
- // participant = our LID JID (e.g. "90079053799508@lid") because addressing_mode='lid'.
628
- // The recipient looks up the sender key by that participant JID, so we MUST store
629
- // the SKDM and encrypt under the SAME identity.
630
- // For legacy PN-mode groups the server uses the phone JID — fall back to ownJid.
631
- // This mirrors Baileys: groupSenderIdentity = groupAddressingMode==='lid' && meLid ? meLid : meId
637
+ // For LID-mode groups, sender identity is own LID JID; otherwise own PN JID
632
638
  const senderIdentity = (groupAddressingMode === 'lid' && this._client._myLid)
633
639
  ? this._client._myLid
634
640
  : ownJid;
@@ -636,70 +642,38 @@ class MessageSender {
636
642
  const rawSKDM = this._signal.buildSKDM(groupJid, senderIdentity);
637
643
  const skdmMsg = encodeSenderKeyDistributionMessage(groupJid, rawSKDM);
638
644
 
639
- // Separate members into two buckets:
640
- // 1. knownPhones — members whose phone number is known (from LID→PN map or direct PN JID)
641
- // 2. unknownLids — LID JIDs with no PN mapping yet (common on first send to a group)
642
- //
643
- // FIX: Previously, bucket-2 members were silently skipped (return null), which meant
644
- // SKDM (Sender Key Distribution Message) was never sent to them. Recipients would
645
- // receive the skmsg ciphertext but could not decrypt it — messages appeared "waiting".
646
- //
647
- // Cobalt solves this by querying usync directly with JID= nodes instead of phone numbers,
648
- // allowing the server to resolve devices for LID-addressed participants. We mirror that
649
- // approach via DeviceManager.bulkEnsureSessionsByLidJids().
650
- const knownPhones = [];
651
- const unknownLids = [];
652
-
653
- for (const jid of members) {
654
- const isLid = jid.endsWith('@lid');
655
- const raw = phoneFromJid(jid);
656
-
657
- if (isLid) {
658
- // Skip own LID identity
659
- if (raw === myLidUser) continue;
660
- // KEY FIX: For LID-mode groups the server only responds to LID-based usync queries
661
- // (i.e. <user jid="...@lid"/>), NOT to phone-number-based queries
662
- // (<contact>+phone</contact>). Always route ALL LID members through
663
- // bulkEnsureSessionsByLidJids() regardless of whether we know their PN.
664
- unknownLids.push(jid);
665
- } else if (raw !== ownPhone) {
666
- // Legacy PN-mode group — use conventional phone-number usync
667
- knownPhones.push(raw);
668
- }
669
- }
670
-
671
- const uniquePhones = [...new Set(knownPhones)];
672
- const uniqueLids = [...new Set(unknownLids)];
673
-
674
- process.stderr.write('[DBG] GROUP_SEND ' + groupJid + ' knownPhones=' + uniquePhones.length + ' unknownLids=' + uniqueLids.length + '\n');
645
+ const memberPhones = [...new Set(
646
+ members.map(jid => {
647
+ const isLid = jid.endsWith('@lid');
648
+ const raw = phoneFromJid(jid);
649
+ if (isLid) {
650
+ const pn = this._client._lidToPn && this._client._lidToPn.get(raw);
651
+ if (!pn) {
652
+ process.stderr.write('[DBG] GROUP_SEND skip LID member with no PN mapping: ' + jid + '\n');
653
+ return null;
654
+ }
655
+ return pn;
656
+ }
657
+ return raw;
658
+ }).filter(p => p !== null && p !== ownPhone)
659
+ )];
675
660
 
676
- const [memberDevicesPN, memberDevicesLid, ownDevices] = await Promise.all([
677
- uniquePhones.length > 0
678
- ? this._devMgr.bulkEnsureSessions(uniquePhones, this._signal)
679
- : Promise.resolve([]),
680
- uniqueLids.length > 0
681
- ? this._devMgr.bulkEnsureSessionsByLidJids(uniqueLids, this._signal)
661
+ const [memberDevices, ownDevices] = await Promise.all([
662
+ memberPhones.length > 0
663
+ ? this._devMgr.bulkEnsureSessions(memberPhones, this._signal)
682
664
  : Promise.resolve([]),
683
665
  this._devMgr.ensureOwnDeviceSessions(ownPhone, this._signal)
684
666
  ]);
685
667
 
686
- const allTargets = [...memberDevicesPN, ...memberDevicesLid, ...ownDevices];
687
-
688
- process.stderr.write('[DBG] GROUP senderIdentity=' + senderIdentity + ' groupAddressingMode=' + groupAddressingMode + ' myLid=' + this._client._myLid + '\n');
668
+ const allTargets = [...memberDevices, ...ownDevices];
689
669
 
690
- // phash: Baileys uses device JIDs of all RECIPIENTS (own secondary devices + others).
691
- // We don't have own secondary devices, so phash = memberDevices only.
692
- // We're currently sending phash=null and let server compute it (see below).
693
- const phashOwnJid = ownJid;
694
- // memberTargets = only other participants' devices (PN + LID), no own secondary devices
695
- const memberTargets = [...memberDevicesPN, ...memberDevicesLid];
696
- const phashTargets = [phashOwnJid, ...memberTargets.filter(j => j !== phashOwnJid)];
697
- process.stderr.write('[DBG] phashOwnJid=' + phashOwnJid + ' memberTargets=' + JSON.stringify(memberTargets) + ' ownDevices=' + JSON.stringify(ownDevices) + '\n');
670
+ const phashTargets = allTargets.includes(ownJid)
671
+ ? allTargets
672
+ : [ownJid, ...allTargets];
698
673
 
699
674
  const skStore = this._signal.senderKeyStore;
700
675
  const existingSkdmMap = skStore.getSKDMMap(groupJid);
701
676
  const skdmRecipients = allTargets.filter(jid => !existingSkdmMap[jid]);
702
- process.stderr.write('[DBG] allTargets=' + JSON.stringify(allTargets) + ' skdmRecipients=' + JSON.stringify(skdmRecipients) + ' existingSkdmKeys=' + JSON.stringify(Object.keys(existingSkdmMap)) + '\n');
703
677
 
704
678
  let skdmEncrypted = [];
705
679
  if (skdmRecipients.length > 0) {
@@ -708,13 +682,10 @@ class MessageSender {
708
682
  }
709
683
 
710
684
  const skmsgCiphertext = this._signal.senderKeyEncrypt(groupJid, senderIdentity, plaintext);
711
- const phashComputed = phashTargets.length > 0 ? computePhash(phashTargets) : null;
712
- const phash = null; // TEST: force no phash to see server response
713
- process.stderr.write('[DBG] phashTargets=' + JSON.stringify(phashTargets) + ' computed phash=' + phashComputed + ' (sending: ' + phash + ')\n');
685
+ const phash = phashTargets.length > 0 ? computePhash(phashTargets) : null;
714
686
 
715
687
  const skdmHasPkmsg = skdmEncrypted.some(e => e.type === 'pkmsg');
716
688
  const advBytes = skdmHasPkmsg ? _buildOrGetAdvIdentity(this._client._store) : null;
717
- process.stderr.write('[DBG] skdmHasPkmsg=' + skdmHasPkmsg + ' advBytes=' + (advBytes ? advBytes.length + 'b' : 'null') + '\n');
718
689
 
719
690
  const msgContent = [];
720
691
  if (advBytes) {
@@ -380,14 +380,12 @@ function decodeMessageContainer(buf) {
380
380
  if (dsm[2]) return decodeMessageContainer(dsm[2]);
381
381
  }
382
382
 
383
- // Field 2: senderKeyDistributionMessage (WAProto: Message.senderKeyDistributionMessage = 2)
383
+ // Field 35: senderKeyDistributionMessage used to set up sender keys.
384
384
  // In WhatsApp Multi-Device, SKDM is bundled WITH the first real message in a new
385
385
  // session (even for 1-on-1 DMs). We extract it and continue decoding the real message.
386
- // Also check field 15 (fastRatchetKeySenderKeyDistributionMessage) as a fallback.
387
386
  let skdmInfo = null;
388
- const skdmRaw = f[2] || f[15];
389
- if (skdmRaw) {
390
- const skdm = _decodeFields(skdmRaw);
387
+ if (f[35]) {
388
+ const skdm = _decodeFields(f[35]);
391
389
  // field 1 = groupId (string), field 2 = axolotlSenderKeyDistributionMessage (bytes)
392
390
  skdmInfo = {
393
391
  groupId: _str(skdm[1]),
@@ -405,10 +403,10 @@ function decodeMessageContainer(buf) {
405
403
  if (text) msgResult = { type: 'text', text };
406
404
  }
407
405
 
408
- // Field 6: extendedTextMessage (WAProto: Message.extendedTextMessage = 6) — field 1 = text
409
- if (!msgResult && f[6] && Buffer.isBuffer(f[6])) {
406
+ // Field 2: extendedTextMessage — field 1 = text
407
+ if (!msgResult && f[2] && Buffer.isBuffer(f[2])) {
410
408
  try {
411
- const ext = _decodeFields(f[6]);
409
+ const ext = _decodeFields(f[2]);
412
410
  const text = _str(ext[1]);
413
411
  if (text) msgResult = { type: 'text', text };
414
412
  } catch (_) {}
@@ -572,13 +570,11 @@ function decodeMessageContainer(buf) {
572
570
  }
573
571
 
574
572
  function encodeSenderKeyDistributionMessage(groupId, axolotlBytes) {
575
- // WAProto: Message.senderKeyDistributionMessage = field 2
576
- // Inner: SenderKeyDistributionMessage { groupId=1 (string), axolotlSKDM=2 (bytes) }
577
573
  const inner = Buffer.concat([
578
574
  field(1, WIRE_LEN, str(groupId)),
579
575
  field(2, WIRE_LEN, bytes(axolotlBytes))
580
576
  ]);
581
- return field(2, WIRE_LEN, inner);
577
+ return field(35, WIRE_LEN, inner);
582
578
  }
583
579
 
584
580
  module.exports = {
@@ -178,7 +178,6 @@ class SenderKeyStore {
178
178
  }
179
179
 
180
180
  markSKDMSent(groupId, jids) {
181
- process.stderr.write('[DBG] markSKDMSent groupId=' + groupId + ' jids=' + JSON.stringify(jids) + '\n' + new Error().stack.split('\n').slice(1,4).join('\n') + '\n');
182
181
  const map = this.getSKDMMap(groupId);
183
182
  for (const jid of jids) map[jid] = true;
184
183
  this.setSKDMMap(groupId, map);
@@ -170,8 +170,6 @@ class SignalProtocol {
170
170
  publicKey: bundle.preKey.publicKey
171
171
  } : undefined
172
172
  });
173
- const hasSess = this.store.hasSession(addr.toString ? addr.toString() : (addr.name + '.' + addr.deviceId));
174
- process.stderr.write('[DBG] SESSION_BUILT jid=' + jid + ' addr=' + addr.name + '.' + addr.deviceId + ' hasSess=' + hasSess + '\n');
175
173
  }
176
174
 
177
175
  async buildSessionsFromBundles(bundleMap) {
@@ -222,18 +220,11 @@ class SignalProtocol {
222
220
  // Returns [{jid, type, ciphertext}]
223
221
  async bulkEncryptForDevices(jids, plaintext) {
224
222
  const settled = await Promise.allSettled(
225
- jids.map(jid => this.encrypt(jid, plaintext).then(enc => {
226
- process.stderr.write('[DBG] SKDM_ENC jid=' + jid + ' type=' + enc.type + ' len=' + (enc.ciphertext ? enc.ciphertext.length : 0) + '\n');
227
- return { jid, type: enc.type, ciphertext: enc.ciphertext };
228
- }))
223
+ jids.map(jid => this.encrypt(jid, plaintext).then(enc => ({ jid, type: enc.type, ciphertext: enc.ciphertext })))
229
224
  );
230
- const results = settled
225
+ return settled
231
226
  .filter(r => r.status === 'fulfilled')
232
227
  .map(r => r.value);
233
- settled.filter(r => r.status === 'rejected').forEach((r, i) => {
234
- process.stderr.write('[DBG] SKDM_ENC_ERR jid=' + jids[i] + ' err=' + r.reason + '\n');
235
- });
236
- return results;
237
228
  }
238
229
 
239
230
  // ─── SenderKey group encrypt / decrypt ────────────────────────────────────
@@ -302,11 +293,6 @@ function jidToAddress(jid) {
302
293
  deviceId = parseInt(user.slice(colonIdx + 1), 10) || 0;
303
294
  user = user.slice(0, colonIdx);
304
295
  }
305
- // Strip agent suffix from user (e.g. "112713111982325_1" → "112713111982325")
306
- const underIdx = user.lastIndexOf('_');
307
- if (underIdx >= 0 && /^\d+$/.test(user.slice(underIdx + 1))) {
308
- user = user.slice(0, underIdx);
309
- }
310
296
  } else if (jid && jid.user) {
311
297
  user = jid.user;
312
298
  deviceId = jid.device || 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whalibmob",
3
- "version": "5.1.19",
3
+ "version": "5.1.20",
4
4
  "description": "WhatsApp library for interaction with WhatsApp Mobile API no web",
5
5
  "author": "Kunboruto20",
6
6
  "main": "index.js",
package/.env.example DELETED
@@ -1,44 +0,0 @@
1
- # ─────────────────────────────────────────────────────────────────────────────
2
- # whalibmob — iPhone Device Emulation Configuration
3
- # Copy this file to .env in your project root and set the values you need.
4
- # All variables are optional; the library picks a random iPhone by default.
5
- # ─────────────────────────────────────────────────────────────────────────────
6
-
7
- # Named iPhone profile to emulate.
8
- # If not set, the library picks a random iPhone from the list below.
9
- #
10
- # Available profiles:
11
- # iphone16promax, iphone16pro, iphone16plus, iphone16,
12
- # iphone15promax, iphone15pro, iphone15plus, iphone15,
13
- # iphone14promax, iphone14pro, iphone14plus, iphone14,
14
- # iphone13pro, iphone13, iphone12pro, iphone12,
15
- # iphone11pro, iphone11, iphonese3, iphonexs
16
- #
17
- # WA_DEVICE=iphone16pro
18
-
19
- # ── Custom iPhone overrides ──────────────────────────────────────────────────
20
- # Use these to emulate a specific iPhone model not in the list above.
21
- # All values are sent as-is in the WhatsApp registration and connection.
22
-
23
- # WA_DEVICE_MODEL=iPhone 16 Pro Max
24
- # WA_DEVICE_OS_VERSION=18.3.2
25
- # WA_DEVICE_BUILD=22D82
26
- # WA_DEVICE_MODEL_ID=iPhone17,2
27
-
28
- # ── Version & token overrides ────────────────────────────────────────────────
29
-
30
- # Pin the WhatsApp version string instead of fetching the latest from the
31
- # App Store (iTunes lookup). Format: 2.x.x.x (four-part).
32
- # WA_VERSION=2.26.10.74
33
-
34
- # Override the built-in iOS static secret used in the token MD5 formula:
35
- # token = MD5( WA_STATIC_TOKEN + MD5hex(version) + nationalNumber )
36
- # Only needed if WhatsApp rotates the built-in secret.
37
- # WA_STATIC_TOKEN=0a1mLfGUIBVrMKF1RdvLI5lkRBvof6vn0fD2QRSM
38
-
39
- # ── Proxy / Tor ──────────────────────────────────────────────────────────────
40
-
41
- # Route registration HTTP traffic through a SOCKS5 proxy or Tor.
42
- # Residential proxies recommended to avoid WhatsApp security blocks.
43
- # TOR_PROXY=socks5://127.0.0.1:9050
44
- # SOCKS_PROXY=socks5://user:pass@proxy.example.com:1080