whalibmob 5.5.90 → 5.6.2
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/README.md +463 -4
- package/cli.js +151 -5
- package/index.js +28 -0
- package/lib/Client.js +607 -62
- package/lib/CompanionPairing.js +247 -0
- package/lib/HistorySyncHandler.js +6 -1
- package/lib/MediaService.js +58 -12
- package/lib/PairingCode.js +198 -0
- package/lib/WebSocketStream.js +98 -0
- package/lib/WebStore.js +117 -0
- package/lib/constants.js +25 -0
- package/lib/messages/MessageSender.js +79 -19
- package/lib/messages/TcTokenStore.js +90 -3
- package/lib/noise.js +72 -7
- package/lib/signal/SignalProtocol.js +14 -5
- package/lib/webproto.js +273 -0
- package/package.json +3 -2
package/lib/Client.js
CHANGED
|
@@ -193,6 +193,11 @@ class WhalibmobClient extends EventEmitter {
|
|
|
193
193
|
this._signal = null;
|
|
194
194
|
this._devMgr = null;
|
|
195
195
|
this._sessionDir = opts.sessionDir || process.env.HOME + '/.waSession';
|
|
196
|
+
// 'mobile' — registered over SMS as the account's primary device (default,
|
|
197
|
+
// and what every existing caller gets). 'web' — linked to an existing
|
|
198
|
+
// account as a companion, via pairing code.
|
|
199
|
+
this._mode = 'mobile';
|
|
200
|
+
this._webSessionFile = null;
|
|
196
201
|
this._pingTimer = null;
|
|
197
202
|
this._keepTimer = null;
|
|
198
203
|
this._connected = false;
|
|
@@ -296,6 +301,8 @@ class WhalibmobClient extends EventEmitter {
|
|
|
296
301
|
}
|
|
297
302
|
|
|
298
303
|
async _connectSocket() {
|
|
304
|
+
if (this._mode === 'web') return this._connectWebSocket();
|
|
305
|
+
|
|
299
306
|
const socket = new NoiseSocket(this._store);
|
|
300
307
|
this._socket = socket;
|
|
301
308
|
|
|
@@ -313,6 +320,406 @@ class WhalibmobClient extends EventEmitter {
|
|
|
313
320
|
return socket;
|
|
314
321
|
}
|
|
315
322
|
|
|
323
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
324
|
+
// WHATSAPP WEB (companion) MODE
|
|
325
|
+
//
|
|
326
|
+
// Same library, same public API, different way in. Instead of registering a
|
|
327
|
+
// number over SMS as the account's primary device, we link to an account that
|
|
328
|
+
// already exists — the way the web client and the desktop apps do. Everything
|
|
329
|
+
// above the connection is unchanged: the Signal layer, the sender, the group
|
|
330
|
+
// and media code all read the same store fields.
|
|
331
|
+
//
|
|
332
|
+
// Two things do differ once connected, and both are handled below:
|
|
333
|
+
// · our own JID carries a device index, so we are no longer device 0 and
|
|
334
|
+
// the account's phone becomes just another device we encrypt to;
|
|
335
|
+
// · the primary ships history over, which a primary device never receives.
|
|
336
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Connect as a companion device.
|
|
340
|
+
*
|
|
341
|
+
* If the session has never been paired this resolves as soon as the encrypted
|
|
342
|
+
* channel is up, leaving the caller to call requestPairingCode(). If it has,
|
|
343
|
+
* it behaves like connect(): resolves once the server sends <success>.
|
|
344
|
+
*
|
|
345
|
+
* @param {string} phoneNumber account phone number, digits only
|
|
346
|
+
* @param {object} [opts] { syncFullHistory, browser }
|
|
347
|
+
*/
|
|
348
|
+
async connectWeb(phoneNumber, opts) {
|
|
349
|
+
return this.initWeb(phoneNumber, opts);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async initWeb(phoneNumber, opts) {
|
|
353
|
+
opts = opts || {};
|
|
354
|
+
phoneNumber = String(phoneNumber).replace(/\D/g, '');
|
|
355
|
+
this._phoneNumber = phoneNumber;
|
|
356
|
+
this._mode = 'web';
|
|
357
|
+
|
|
358
|
+
const { createNewWebStore, loadWebStore, saveWebStore, webSessionPath } =
|
|
359
|
+
require('./WebStore');
|
|
360
|
+
|
|
361
|
+
this._webSessionFile = webSessionPath(this._sessionDir, phoneNumber);
|
|
362
|
+
this._store = loadWebStore(this._webSessionFile) || createNewWebStore(phoneNumber);
|
|
363
|
+
|
|
364
|
+
if (opts.syncFullHistory !== undefined) this._store.syncFullHistory = !!opts.syncFullHistory;
|
|
365
|
+
if (opts.browser) this._store.browser = opts.browser;
|
|
366
|
+
saveWebStore(this._store, this._webSessionFile);
|
|
367
|
+
|
|
368
|
+
// Signal state lives in its own files so a web link and an SMS
|
|
369
|
+
// registration for the same number never share sessions or pre-keys.
|
|
370
|
+
const signalFile = path.join(this._sessionDir, `${phoneNumber}.web.signal.json`);
|
|
371
|
+
const skFile = path.join(this._sessionDir, `${phoneNumber}.web.sk.json`);
|
|
372
|
+
const tcTokenFile = path.join(this._sessionDir, `${phoneNumber}.web.tctoken.json`);
|
|
373
|
+
|
|
374
|
+
const { TcTokenStore } = require('./messages/TcTokenStore');
|
|
375
|
+
this._tcTokenStore = new TcTokenStore(tcTokenFile);
|
|
376
|
+
|
|
377
|
+
this._signal = SignalProtocol.fromStore(this._store, signalFile, skFile);
|
|
378
|
+
this._devMgr = new DeviceManager(this);
|
|
379
|
+
this._devMgr.attachSession(this._sessionDir, phoneNumber + '.web');
|
|
380
|
+
|
|
381
|
+
const persistedLid = this._signal.store.getLidMappings
|
|
382
|
+
? this._signal.store.getLidMappings() : {};
|
|
383
|
+
for (const [phone, lid] of Object.entries(persistedLid)) {
|
|
384
|
+
this._pnToLid.set(phone, lid);
|
|
385
|
+
this._lidToPn.set(lid, phone);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (this._store.me && this._store.me.lid) this._myLid = this._store.me.lid;
|
|
389
|
+
|
|
390
|
+
await this._connectWebSocket();
|
|
391
|
+
return this;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
_saveWebStore() {
|
|
395
|
+
if (this._mode !== 'web' || !this._webSessionFile) return;
|
|
396
|
+
const { saveWebStore } = require('./WebStore');
|
|
397
|
+
saveWebStore(this._store, this._webSessionFile);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async _connectWebSocket() {
|
|
401
|
+
const { encodeCompanionRegisterPayload, encodeCompanionLoginPayload } =
|
|
402
|
+
require('./webproto');
|
|
403
|
+
|
|
404
|
+
const store = this._store;
|
|
405
|
+
const registered = !!store.registered && !!(store.me && store.me.id);
|
|
406
|
+
|
|
407
|
+
const payloadOpts = {
|
|
408
|
+
version: store.webVersion,
|
|
409
|
+
browser: store.browser,
|
|
410
|
+
countryCode: 'US',
|
|
411
|
+
pushName: store.name && store.name !== 'User' ? store.name : undefined,
|
|
412
|
+
syncFullHistory: store.syncFullHistory !== false,
|
|
413
|
+
registrationId: store.registrationId,
|
|
414
|
+
identityKeyPair: store.identityKeyPair,
|
|
415
|
+
signedPreKey: store.signedPreKey
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
const socket = new NoiseSocket(store, {
|
|
419
|
+
transport: 'web',
|
|
420
|
+
// An unpaired companion never gets <success> on this connection — the
|
|
421
|
+
// server answers with pair-device and waits for a human to act.
|
|
422
|
+
expectSuccess: registered,
|
|
423
|
+
buildPayload: () => registered
|
|
424
|
+
? encodeCompanionLoginPayload(Object.assign({}, payloadOpts, {
|
|
425
|
+
username: store.phoneNumber,
|
|
426
|
+
device: store.deviceIndex || 0
|
|
427
|
+
}))
|
|
428
|
+
: encodeCompanionRegisterPayload(payloadOpts)
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
this._socket = socket;
|
|
432
|
+
this._sender = new MessageSender(this);
|
|
433
|
+
|
|
434
|
+
socket.on('open', (sn) => this._onOpen(sn));
|
|
435
|
+
socket.on('secured', () => {
|
|
436
|
+
_whaDbg('[DBG] WEB_CHANNEL_SECURED — awaiting pairing');
|
|
437
|
+
this.emit('web_ready_for_pairing');
|
|
438
|
+
});
|
|
439
|
+
socket.on('node', node => this._onNode(node));
|
|
440
|
+
socket.on('close', () => this._onClose());
|
|
441
|
+
socket.on('error', err => this.emit('error', err));
|
|
442
|
+
|
|
443
|
+
await socket.connect();
|
|
444
|
+
return socket;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Ask WhatsApp for an 8-character pairing code.
|
|
449
|
+
*
|
|
450
|
+
* The account owner enters it on their phone under
|
|
451
|
+
* Settings → Linked Devices → Link a device → Link with phone number instead.
|
|
452
|
+
* The code is a password, not an identifier: it never travels to the server
|
|
453
|
+
* in the clear, and both sides run it through PBKDF2 to wrap the ephemeral
|
|
454
|
+
* keys they exchange.
|
|
455
|
+
*
|
|
456
|
+
* Resolves with the code as soon as the request is on the wire. The link
|
|
457
|
+
* itself completes later — wait for the 'paired' or 'open' event.
|
|
458
|
+
*
|
|
459
|
+
* @param {string} [phoneNumber] defaults to the number initWeb was given
|
|
460
|
+
* @param {string} [customCode] exactly 8 characters, if you want to choose
|
|
461
|
+
* @returns {Promise<string>}
|
|
462
|
+
*/
|
|
463
|
+
async requestPairingCode(phoneNumber, customCode) {
|
|
464
|
+
if (this._mode !== 'web') {
|
|
465
|
+
throw new Error('requestPairingCode is only available in web mode — call connectWeb() first');
|
|
466
|
+
}
|
|
467
|
+
if (!this._socket || !this._socket.secured) {
|
|
468
|
+
throw new Error('Not connected — call connectWeb() before requesting a pairing code');
|
|
469
|
+
}
|
|
470
|
+
if (this._store.registered) {
|
|
471
|
+
throw new Error('This session is already linked — no pairing code needed');
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const { generatePairingCode, buildWrappedEphemeralPub, generateEphemeralKeyPair } =
|
|
475
|
+
require('./PairingCode');
|
|
476
|
+
const { platformTypeFor } = require('./webproto');
|
|
477
|
+
|
|
478
|
+
if (customCode != null && String(customCode).length !== 8) {
|
|
479
|
+
throw new Error('A custom pairing code must be exactly 8 characters');
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const phone = String(phoneNumber || this._phoneNumber || this._store.phoneNumber)
|
|
483
|
+
.replace(/\D/g, '');
|
|
484
|
+
if (!phone) throw new Error('requestPairingCode: a phone number is required');
|
|
485
|
+
|
|
486
|
+
const code = customCode ? String(customCode).toUpperCase() : generatePairingCode();
|
|
487
|
+
|
|
488
|
+
// A fresh ephemeral pair per attempt: a code that was requested and never
|
|
489
|
+
// used must not be able to complete a later exchange.
|
|
490
|
+
this._store.pairingEphemeralKeyPair = generateEphemeralKeyPair();
|
|
491
|
+
this._store.pairingCode = code;
|
|
492
|
+
this._store.phoneNumber = phone;
|
|
493
|
+
this._store.me = { id: phone + '@s.whatsapp.net', name: '~', lid: null };
|
|
494
|
+
this._saveWebStore();
|
|
495
|
+
|
|
496
|
+
const browser = this._store.browser || ['Ubuntu', 'Chrome', '120.0.0.0'];
|
|
497
|
+
const wrapped = await buildWrappedEphemeralPub(code, this._store.pairingEphemeralKeyPair.public);
|
|
498
|
+
|
|
499
|
+
// The bare 32-byte curve point, not the 33-byte Signal form.
|
|
500
|
+
//
|
|
501
|
+
// whalibmob stores every public key with the 0x05 type prefix, and the
|
|
502
|
+
// Noise handshake strips it on the way out. This node had no such strip, so
|
|
503
|
+
// the primary would have hashed 33 bytes where it expected 32 and the link
|
|
504
|
+
// would fail with nothing to point at.
|
|
505
|
+
const { stripKeyPrefix } = require('./PairingCode');
|
|
506
|
+
const noisePub = stripKeyPrefix(this._store.noiseKeyPair.public);
|
|
507
|
+
|
|
508
|
+
const node = new BinaryNode('iq', {
|
|
509
|
+
id: this._genMsgId(),
|
|
510
|
+
to: 's.whatsapp.net',
|
|
511
|
+
type: 'set',
|
|
512
|
+
xmlns: 'md'
|
|
513
|
+
}, [
|
|
514
|
+
new BinaryNode('link_code_companion_reg', {
|
|
515
|
+
jid: this._store.me.id,
|
|
516
|
+
stage: 'companion_hello',
|
|
517
|
+
should_show_push_notification: 'true'
|
|
518
|
+
}, [
|
|
519
|
+
new BinaryNode('link_code_pairing_wrapped_companion_ephemeral_pub', {}, wrapped),
|
|
520
|
+
new BinaryNode('companion_server_auth_key_pub', {}, noisePub),
|
|
521
|
+
new BinaryNode('companion_platform_id', {}, Buffer.from(
|
|
522
|
+
String(platformTypeFor(browser[1])), 'utf8')),
|
|
523
|
+
new BinaryNode('companion_platform_display', {}, Buffer.from(
|
|
524
|
+
`${browser[1]} (${browser[0]})`, 'utf8')),
|
|
525
|
+
new BinaryNode('link_code_pairing_nonce', {}, Buffer.from('0', 'utf8'))
|
|
526
|
+
])
|
|
527
|
+
]);
|
|
528
|
+
|
|
529
|
+
_whaDbg('[DBG] REQUEST_PAIRING_CODE phone=' + phone + ' code=' + code);
|
|
530
|
+
this._socket.sendNode(node);
|
|
531
|
+
this.emit('pairing_code', { code, phoneNumber: phone });
|
|
532
|
+
return code;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Stage 3 of the pairing exchange, driven by the server.
|
|
537
|
+
*
|
|
538
|
+
* Arrives once the account owner has typed the code into their phone. We
|
|
539
|
+
* unwrap the primary's ephemeral key with the key derived from that code, do
|
|
540
|
+
* the two Diffie-Hellmans, and store adv_secret — the key pair-success will
|
|
541
|
+
* be verified under.
|
|
542
|
+
*/
|
|
543
|
+
async _handleCompanionFinish(node) {
|
|
544
|
+
const { buildCompanionFinishBundle } = require('./PairingCode');
|
|
545
|
+
|
|
546
|
+
const reg = findChild(node, 'link_code_companion_reg');
|
|
547
|
+
if (!reg) return;
|
|
548
|
+
|
|
549
|
+
const childBuf = (desc) => {
|
|
550
|
+
const c = findChild(reg, desc);
|
|
551
|
+
if (!c) return null;
|
|
552
|
+
return Buffer.isBuffer(c.content) ? c.content
|
|
553
|
+
: ArrayBuffer.isView(c.content) ? Buffer.from(c.content)
|
|
554
|
+
: typeof c.content === 'string' ? Buffer.from(c.content, 'utf8')
|
|
555
|
+
: null;
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
const ref = childBuf('link_code_pairing_ref');
|
|
559
|
+
const primaryIdentityPub = childBuf('primary_identity_pub');
|
|
560
|
+
const wrappedEphemeral = childBuf('link_code_pairing_wrapped_primary_ephemeral_pub');
|
|
561
|
+
|
|
562
|
+
if (!ref || !primaryIdentityPub || !wrappedEphemeral) {
|
|
563
|
+
_whaDbg('[DBG] COMPANION_FINISH missing fields — aborting');
|
|
564
|
+
this.emit('error', new Error('Pairing failed: incomplete companion_finish from server'));
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
try {
|
|
569
|
+
const { advSecretKey, wrappedKeyBundle } = await buildCompanionFinishBundle({
|
|
570
|
+
pairingCode: this._store.pairingCode,
|
|
571
|
+
pairingEphemeralPrivate: this._store.pairingEphemeralKeyPair.private,
|
|
572
|
+
primaryEphemeralPubWrapped: wrappedEphemeral,
|
|
573
|
+
primaryIdentityPub,
|
|
574
|
+
identityKeyPair: this._store.identityKeyPair
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
this._store.advSecretKey = advSecretKey;
|
|
578
|
+
this._saveWebStore();
|
|
579
|
+
_whaDbg('[DBG] COMPANION_FINISH adv_secret derived');
|
|
580
|
+
|
|
581
|
+
// Baileys awaits this one rather than firing and forgetting, so a server
|
|
582
|
+
// rejection surfaces as an error here instead of as a silent stall
|
|
583
|
+
// waiting for a pair-success that is never coming.
|
|
584
|
+
const finishResult = await this._sendIq(new BinaryNode('iq', {
|
|
585
|
+
to: 's.whatsapp.net',
|
|
586
|
+
type: 'set',
|
|
587
|
+
id: this._genMsgId(),
|
|
588
|
+
xmlns: 'md'
|
|
589
|
+
}, [
|
|
590
|
+
new BinaryNode('link_code_companion_reg', {
|
|
591
|
+
jid: this._store.me.id,
|
|
592
|
+
stage: 'companion_finish'
|
|
593
|
+
}, [
|
|
594
|
+
new BinaryNode('link_code_pairing_wrapped_key_bundle', {}, wrappedKeyBundle),
|
|
595
|
+
new BinaryNode('companion_identity_public', {},
|
|
596
|
+
require('./PairingCode').stripKeyPrefix(this._store.identityKeyPair.public)),
|
|
597
|
+
new BinaryNode('link_code_pairing_ref', {}, ref)
|
|
598
|
+
])
|
|
599
|
+
]));
|
|
600
|
+
|
|
601
|
+
if (finishResult && finishResult.attrs && finishResult.attrs.type === 'error') {
|
|
602
|
+
const errNode = findChild(finishResult, 'error');
|
|
603
|
+
const code = errNode && errNode.attrs && errNode.attrs.code;
|
|
604
|
+
throw new Error('server rejected companion_finish' + (code ? ' (' + code + ')' : ''));
|
|
605
|
+
}
|
|
606
|
+
_whaDbg('[DBG] COMPANION_FINISH accepted — awaiting pair-success');
|
|
607
|
+
} catch (err) {
|
|
608
|
+
_whaDbg('[DBG] COMPANION_FINISH_ERR ' + (err && err.message));
|
|
609
|
+
this.emit('error', new Error('Pairing failed: ' + (err && err.message)));
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* <iq><pair-success> — the primary has authorised us.
|
|
615
|
+
*
|
|
616
|
+
* Verify what it sent, counter-sign it, reply, and persist. The server closes
|
|
617
|
+
* the connection right after with a 515; the reconnect logs in normally and
|
|
618
|
+
* that is when history starts arriving.
|
|
619
|
+
*/
|
|
620
|
+
_handlePairSuccess(node) {
|
|
621
|
+
const { configureSuccessfulPairing } = require('./CompanionPairing');
|
|
622
|
+
try {
|
|
623
|
+
const result = configureSuccessfulPairing(node, {
|
|
624
|
+
advSecretKey: this._store.advSecretKey,
|
|
625
|
+
identityKeyPair: this._store.identityKeyPair
|
|
626
|
+
}, BinaryNode);
|
|
627
|
+
|
|
628
|
+
this._store.me = result.me;
|
|
629
|
+
this._store.deviceIndex = result.deviceIndex;
|
|
630
|
+
this._store.platform = result.platform;
|
|
631
|
+
// The same field the mobile path fills from <success>: the signed device
|
|
632
|
+
// identity that rides on outgoing pkmsg stanzas.
|
|
633
|
+
this._store.advIdentity = result.accountEnc;
|
|
634
|
+
this._store.registered = true;
|
|
635
|
+
this._store.pairingCode = null;
|
|
636
|
+
if (result.me && result.me.name) this._store.name = result.me.name;
|
|
637
|
+
this._saveWebStore();
|
|
638
|
+
|
|
639
|
+
if (result.me && result.me.lid) {
|
|
640
|
+
this._myLid = result.me.lid;
|
|
641
|
+
const lidUser = String(result.me.lid).split('@')[0].split(':')[0];
|
|
642
|
+
const pnUser = String(result.me.id).split('@')[0].split(':')[0];
|
|
643
|
+
this._lidToPn.set(lidUser, pnUser);
|
|
644
|
+
this._pnToLid.set(pnUser, lidUser);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
this._socket.sendNode(result.reply);
|
|
648
|
+
this._sendUnifiedSession();
|
|
649
|
+
_whaDbg('[DBG] PAIR_SUCCESS jid=' + result.me.id + ' lid=' + result.me.lid +
|
|
650
|
+
' device=' + result.deviceIndex);
|
|
651
|
+
this.emit('paired', {
|
|
652
|
+
jid: result.me.id,
|
|
653
|
+
lid: result.me.lid,
|
|
654
|
+
deviceIndex: result.deviceIndex,
|
|
655
|
+
platform: result.platform
|
|
656
|
+
});
|
|
657
|
+
} catch (err) {
|
|
658
|
+
_whaDbg('[DBG] PAIR_SUCCESS_ERR ' + (err && err.message));
|
|
659
|
+
this.emit('error', err);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* <iq><pair-device> — the QR path.
|
|
665
|
+
*
|
|
666
|
+
* We do not display QR codes, but the node still has to be acknowledged or
|
|
667
|
+
* the server keeps re-sending refs. The refs are surfaced as an event so a
|
|
668
|
+
* caller that wants to render a QR can.
|
|
669
|
+
*/
|
|
670
|
+
_handlePairDevice(node) {
|
|
671
|
+
const id = node.attrs && node.attrs.id;
|
|
672
|
+
if (id) {
|
|
673
|
+
this._socket.sendNode(new BinaryNode('iq', {
|
|
674
|
+
to: 's.whatsapp.net', type: 'result', id
|
|
675
|
+
}, null));
|
|
676
|
+
}
|
|
677
|
+
const pairDevice = findChild(node, 'pair-device');
|
|
678
|
+
const refs = pairDevice && Array.isArray(pairDevice.content)
|
|
679
|
+
? pairDevice.content
|
|
680
|
+
.filter(c => c && c.description === 'ref')
|
|
681
|
+
.map(c => (Buffer.isBuffer(c.content) ? c.content.toString('utf8') : String(c.content)))
|
|
682
|
+
: [];
|
|
683
|
+
this.emit('pair_device', { refs });
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* The JID this device sends as.
|
|
688
|
+
*
|
|
689
|
+
* A primary is device 0 and its JID is the bare number. A companion occupies
|
|
690
|
+
* a numbered slot, and the account's phone is then a peer we encrypt to like
|
|
691
|
+
* any other linked device — which is exactly the difference the fan-out code
|
|
692
|
+
* needs to know about.
|
|
693
|
+
*/
|
|
694
|
+
_ownDeviceJid() {
|
|
695
|
+
const phone = String(this._store.phoneNumber);
|
|
696
|
+
const idx = (this._mode === 'web' && this._store.deviceIndex) || 0;
|
|
697
|
+
return idx > 0 ? `${phone}:${idx}@s.whatsapp.net` : `${phone}@s.whatsapp.net`;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// <ib><unified_session/> — the telemetry ping a web client emits after
|
|
701
|
+
// pairing and after each login. It carries no state we need; sending it keeps
|
|
702
|
+
// our traffic shaped like the client we are claiming to be. Failures are
|
|
703
|
+
// ignored, as they are in the reference.
|
|
704
|
+
_sendUnifiedSession() {
|
|
705
|
+
if (this._mode !== 'web' || !this._socket) return;
|
|
706
|
+
try {
|
|
707
|
+
const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
|
|
708
|
+
const id = ((Date.now() + 3 * 24 * 60 * 60 * 1000) % WEEK_MS).toString();
|
|
709
|
+
this._socket.sendNode(new BinaryNode('ib', {}, [
|
|
710
|
+
new BinaryNode('unified_session', { id }, null)
|
|
711
|
+
]));
|
|
712
|
+
} catch (_) {}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/** Same, in LID space. */
|
|
716
|
+
_ownDeviceLidJid() {
|
|
717
|
+
if (!this._myLid) return null;
|
|
718
|
+
const lidUser = String(this._myLid).split('@')[0].split(':')[0];
|
|
719
|
+
const idx = (this._mode === 'web' && this._store.deviceIndex) || 0;
|
|
720
|
+
return idx > 0 ? `${lidUser}:${idx}@lid` : `${lidUser}@lid`;
|
|
721
|
+
}
|
|
722
|
+
|
|
316
723
|
disconnect() {
|
|
317
724
|
this._reconnecting = false;
|
|
318
725
|
this._reconnectTry = 0;
|
|
@@ -372,6 +779,9 @@ class WhalibmobClient extends EventEmitter {
|
|
|
372
779
|
this._requestMediaConnection();
|
|
373
780
|
this._uploadPreKeys();
|
|
374
781
|
|
|
782
|
+
// Same telemetry ping the web client sends on every completed login.
|
|
783
|
+
this._sendUnifiedSession();
|
|
784
|
+
|
|
375
785
|
// Warm the privacy settings so read receipts respect them from the first
|
|
376
786
|
// message rather than from whenever something first asks for them.
|
|
377
787
|
this.queryPrivacySettings({ force: true }).catch(err =>
|
|
@@ -457,18 +867,34 @@ class WhalibmobClient extends EventEmitter {
|
|
|
457
867
|
if (bytes && bytes.length > 0) {
|
|
458
868
|
this._store.advIdentity = bytes;
|
|
459
869
|
// Persist to session file so it survives restart
|
|
460
|
-
|
|
461
|
-
this.
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
870
|
+
if (this._mode === 'web') {
|
|
871
|
+
this._saveWebStore();
|
|
872
|
+
} else {
|
|
873
|
+
const sessionFile = path.join(
|
|
874
|
+
this._sessionDir,
|
|
875
|
+
`${this._store.phoneNumber}.json`
|
|
876
|
+
);
|
|
877
|
+
try {
|
|
878
|
+
const { saveStore } = require('./Store');
|
|
879
|
+
saveStore(this._store, sessionFile);
|
|
880
|
+
} catch (_) {}
|
|
881
|
+
}
|
|
468
882
|
}
|
|
469
883
|
} else if (this._store.advIdentity) {
|
|
470
884
|
// Already have it persisted from a previous session — keep using it
|
|
471
885
|
}
|
|
886
|
+
|
|
887
|
+
// A companion learns its own LID from pair-success, not from <success>, so
|
|
888
|
+
// keep what pairing established rather than leaving it unset on reconnect.
|
|
889
|
+
if (this._mode === 'web') {
|
|
890
|
+
if (!this._myLid && this._store.me && this._store.me.lid) {
|
|
891
|
+
this._myLid = this._store.me.lid;
|
|
892
|
+
}
|
|
893
|
+
if (attrs.lid && this._store.me) {
|
|
894
|
+
this._store.me.lid = String(attrs.lid);
|
|
895
|
+
this._saveWebStore();
|
|
896
|
+
}
|
|
897
|
+
}
|
|
472
898
|
}
|
|
473
899
|
|
|
474
900
|
// ── Feature 3 (part A): Activate the connection ────────────────────────────
|
|
@@ -645,10 +1071,31 @@ class WhalibmobClient extends EventEmitter {
|
|
|
645
1071
|
else if (tag === 'ib') this._handleIb(node);
|
|
646
1072
|
else if (tag === 'success') this.emit('session_refresh', { node }); // late success (re-auth)
|
|
647
1073
|
else if (tag === 'failure') this._handleFailure(node);
|
|
648
|
-
else if (tag === 'stream:error') this.
|
|
1074
|
+
else if (tag === 'stream:error') this._handleStreamError(node);
|
|
649
1075
|
else this.emit('node', node);
|
|
650
1076
|
}
|
|
651
1077
|
|
|
1078
|
+
// A stream error ends the connection; the reconnect in _onClose picks it up.
|
|
1079
|
+
//
|
|
1080
|
+
// 515 is not a failure. The server sends it immediately after a companion
|
|
1081
|
+
// finishes pairing to say "you are linked now, start over as a normal
|
|
1082
|
+
// client" — the next connection carries the login payload instead of the
|
|
1083
|
+
// pairing bundle, and that is the one that receives history.
|
|
1084
|
+
_handleStreamError(node) {
|
|
1085
|
+
const attrs = (node && node.attrs) || {};
|
|
1086
|
+
const code = attrs.code ? String(attrs.code) : null;
|
|
1087
|
+
const reason = attrs.reason || code || 'unknown';
|
|
1088
|
+
|
|
1089
|
+
if (code === '515') {
|
|
1090
|
+
_whaDbg('[DBG] STREAM_ERROR 515 — restart required after pairing, reconnecting');
|
|
1091
|
+
this.emit('restart_required', { reason });
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
_whaDbg('[DBG] STREAM_ERROR code=' + code + ' reason=' + reason);
|
|
1096
|
+
this.emit('stream_error', { reason, code });
|
|
1097
|
+
}
|
|
1098
|
+
|
|
652
1099
|
// ─── IQ handling ──────────────────────────────────────────────────────────
|
|
653
1100
|
|
|
654
1101
|
_handleIq(node) {
|
|
@@ -656,6 +1103,18 @@ class WhalibmobClient extends EventEmitter {
|
|
|
656
1103
|
const type = node.attrs && node.attrs.type;
|
|
657
1104
|
const xmlns = node.attrs && node.attrs.xmlns;
|
|
658
1105
|
|
|
1106
|
+
// Companion pairing — these arrive unsolicited, so they are checked before
|
|
1107
|
+
// the pending-IQ table rather than after it.
|
|
1108
|
+
//
|
|
1109
|
+
// pair-device is matched on type=set; pair-success is matched on any type,
|
|
1110
|
+
// the way Baileys distinguishes them ('CB:iq,type:set,pair-device' against
|
|
1111
|
+
// a bare 'CB:iq,,pair-success'). Requiring type=set on pair-success would
|
|
1112
|
+
// drop the stanza that completes the link.
|
|
1113
|
+
if (this._mode === 'web') {
|
|
1114
|
+
if (type === 'set' && findChild(node, 'pair-device')) { this._handlePairDevice(node); return; }
|
|
1115
|
+
if (findChild(node, 'pair-success')) { this._handlePairSuccess(node); return; }
|
|
1116
|
+
}
|
|
1117
|
+
|
|
659
1118
|
// Resolve pending IQ
|
|
660
1119
|
if (id) {
|
|
661
1120
|
const handler = this._pendingIqs.get(id);
|
|
@@ -1099,6 +1558,69 @@ class WhalibmobClient extends EventEmitter {
|
|
|
1099
1558
|
return base;
|
|
1100
1559
|
}
|
|
1101
1560
|
|
|
1561
|
+
// The key a privacy token is stored and looked up under.
|
|
1562
|
+
//
|
|
1563
|
+
// Both references resolve it to the contact's LID and fall back to the plain
|
|
1564
|
+
// JID only when no LID is known — whatsmeow's resolveTCTokenStorageLID and
|
|
1565
|
+
// Baileys' resolveTcTokenJid are the same three lines:
|
|
1566
|
+
//
|
|
1567
|
+
// if (isLidUser(jid)) return jid
|
|
1568
|
+
// const lid = await getLIDForPN(jid)
|
|
1569
|
+
// return lid ?? jid
|
|
1570
|
+
//
|
|
1571
|
+
// whalibmob resolved it on the receiving side but not on the sending side,
|
|
1572
|
+
// which put the two halves out of step: a token a contact issued us arrived
|
|
1573
|
+
// on a privacy_token notification and was filed under their LID, while every
|
|
1574
|
+
// send looked it up under their phone JID and found nothing. The token was
|
|
1575
|
+
// therefore never attached to anything, and the server kept counting our
|
|
1576
|
+
// messages as anonymous reach-outs — which is what error 463 is.
|
|
1577
|
+
_resolveTcTokenJid(jid) {
|
|
1578
|
+
const { normalizeJidForTcToken } = require('./messages/TcTokenStore');
|
|
1579
|
+
const norm = normalizeJidForTcToken(jid);
|
|
1580
|
+
if (norm.endsWith('@lid')) return norm;
|
|
1581
|
+
const lidUser = this._pnToLid && this._pnToLid.get(norm.split('@')[0]);
|
|
1582
|
+
return lidUser ? lidUser + '@lid' : norm;
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
// The JID a token is *issued* to, which is not the JID it is *stored* under.
|
|
1586
|
+
//
|
|
1587
|
+
// Storage is always the LID. Issuance follows AB prop 14303
|
|
1588
|
+
// (lid_trusted_token_issue_to_lid), whose WA Web default is off — so the IQ
|
|
1589
|
+
// names the phone JID. Baileys implements exactly this in resolveIssuanceJid
|
|
1590
|
+
// and defaults the prop to false; whatsmeow issues to the LID instead. We
|
|
1591
|
+
// follow WA Web, and more importantly we now follow it in one place: the
|
|
1592
|
+
// send path, the 463 recovery and the post-identity-change re-issue used to
|
|
1593
|
+
// each pick a different address family for the same contact.
|
|
1594
|
+
_resolveTcTokenIssuanceJid(jid) {
|
|
1595
|
+
const { normalizeJidForTcToken } = require('./messages/TcTokenStore');
|
|
1596
|
+
const norm = normalizeJidForTcToken(jid);
|
|
1597
|
+
if (!norm.endsWith('@lid')) return norm;
|
|
1598
|
+
const pnUser = this._lidToPn && this._lidToPn.get(norm.split('@')[0]);
|
|
1599
|
+
return pnUser ? pnUser + '@s.whatsapp.net' : norm;
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
// The one way to read a token that is safe to put on the wire.
|
|
1603
|
+
//
|
|
1604
|
+
// whatsmeow funnels every attach site through ensureTCToken, which returns
|
|
1605
|
+
// nothing for an expired token. whalibmob had the expiry check on the message
|
|
1606
|
+
// path only; presence subscribe, group create, participant add and the
|
|
1607
|
+
// profile-picture query each tested `token.length` and nothing else, so once
|
|
1608
|
+
// a token aged past the 28-day window those four kept sending it. Same rule
|
|
1609
|
+
// everywhere now, and the dead token gets cleared on the way out.
|
|
1610
|
+
_tcTokenFor(jid) {
|
|
1611
|
+
if (!this._tcTokenStore) return null;
|
|
1612
|
+
const { tcTokenExpired } = require('./messages/TcTokenStore');
|
|
1613
|
+
const tcJid = this._resolveTcTokenJid(jid);
|
|
1614
|
+
const entry = this._tcTokenStore.get(tcJid);
|
|
1615
|
+
if (!entry || !entry.token || !entry.token.length) return null;
|
|
1616
|
+
if (tcTokenExpired(entry.timestamp)) {
|
|
1617
|
+
this._tcTokenStore.clearToken(tcJid);
|
|
1618
|
+
_whaDbg('[DBG] TCTOKEN_EXPIRED jid=' + tcJid + ' — cleared');
|
|
1619
|
+
return null;
|
|
1620
|
+
}
|
|
1621
|
+
return entry.token;
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1102
1624
|
// True when a JID names this account, in either address family.
|
|
1103
1625
|
_isOwnUser(jid) {
|
|
1104
1626
|
if (!jid) return false;
|
|
@@ -1344,6 +1866,11 @@ class WhalibmobClient extends EventEmitter {
|
|
|
1344
1866
|
this._handlePrivacyTokenNotification(node);
|
|
1345
1867
|
}
|
|
1346
1868
|
|
|
1869
|
+
if (type === 'link_code_companion_reg') {
|
|
1870
|
+
// The account owner just typed our pairing code into their phone.
|
|
1871
|
+
this._handleCompanionFinish(node);
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1347
1874
|
if (type === 'md-msg-hist') {
|
|
1348
1875
|
// Server signals that history sync data is available.
|
|
1349
1876
|
// The actual encrypted notification may arrive as:
|
|
@@ -1953,6 +2480,7 @@ class WhalibmobClient extends EventEmitter {
|
|
|
1953
2480
|
_storeTcTokenFromIqResult(result, fallbackJid, issueTs, opts) {
|
|
1954
2481
|
const saveSenderTs = !opts || opts.saveSenderTs !== false; // default true
|
|
1955
2482
|
if (!this._tcTokenStore) return;
|
|
2483
|
+
const { isRegularTcTokenUser } = require('./messages/TcTokenStore');
|
|
1956
2484
|
|
|
1957
2485
|
// ── Debug: dump result structure so we can trace server response ─────────
|
|
1958
2486
|
if (!result) {
|
|
@@ -2002,10 +2530,21 @@ class WhalibmobClient extends EventEmitter {
|
|
|
2002
2530
|
// → always prefer fallbackJid; only fall back to a.jid when fallbackJid is absent
|
|
2003
2531
|
const jid = fallbackJid || String(a.jid);
|
|
2004
2532
|
const t = a.t ? parseInt(String(a.t), 10) : issueTs;
|
|
2533
|
+
// Baileys drops anything that is not a regular user here — the guard
|
|
2534
|
+
// is against a malformed or spoofed notification filing a token under
|
|
2535
|
+
// the PSA account or a bot.
|
|
2536
|
+
if (!isRegularTcTokenUser(jid)) {
|
|
2537
|
+
_whaDbg('[DBG] STORE_TCTOKEN skipped non-user jid=' + jid);
|
|
2538
|
+
continue;
|
|
2539
|
+
}
|
|
2005
2540
|
if (bytes && bytes.length) {
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2541
|
+
// setToken refuses a timestamp-less or older-than-stored token.
|
|
2542
|
+
if (this._tcTokenStore.setToken(jid, bytes, t)) {
|
|
2543
|
+
tokenStoredCount++;
|
|
2544
|
+
_whaDbg('[DBG] TCTOKEN_STORED jid=' + jid + ' t=' + t + ' len=' + bytes.length);
|
|
2545
|
+
} else {
|
|
2546
|
+
_whaDbg('[DBG] STORE_TCTOKEN rejected (no/older timestamp) jid=' + jid + ' t=' + t);
|
|
2547
|
+
}
|
|
2009
2548
|
} else {
|
|
2010
2549
|
_whaDbg('[DBG] STORE_TCTOKEN token node has no bytes jid=' + jid);
|
|
2011
2550
|
}
|
|
@@ -2021,7 +2560,9 @@ class WhalibmobClient extends EventEmitter {
|
|
|
2021
2560
|
// ── we flood the server with privacy IQs. ─────
|
|
2022
2561
|
// ── For incoming privacy_token notifications saveSenderTs=false so ─────
|
|
2023
2562
|
// ── we don't overwrite our own send-dedupe timestamp with the peer's ─────
|
|
2024
|
-
|
|
2563
|
+
// Same user gate as the token itself — no dedupe row for the PSA account
|
|
2564
|
+
// or a bot, since we never issue to them in the first place.
|
|
2565
|
+
if (saveSenderTs && issueTs != null && isRegularTcTokenUser(fallbackJid)) {
|
|
2025
2566
|
try {
|
|
2026
2567
|
this._tcTokenStore.setSenderTimestamp(fallbackJid, issueTs);
|
|
2027
2568
|
_whaDbg('[DBG] SENDER_TS_SAVED jid=' + fallbackJid + ' ts=' + issueTs +
|
|
@@ -2081,11 +2622,9 @@ class WhalibmobClient extends EventEmitter {
|
|
|
2081
2622
|
// from = LID_USER@s.whatsapp.net — WA sends LID notifications this way.
|
|
2082
2623
|
// The user part is already the LID user; convert domain to @lid.
|
|
2083
2624
|
storageJid = fromUser + '@lid';
|
|
2084
|
-
} else if (this._pnToLid && this._pnToLid.has(fromUser)) {
|
|
2085
|
-
// from is a PN → convert to LID for storage (Baileys always stores under LID)
|
|
2086
|
-
storageJid = this._pnToLid.get(fromUser) + '@lid';
|
|
2087
2625
|
} else {
|
|
2088
|
-
|
|
2626
|
+
// Same resolver the send side uses, so both halves agree on the key.
|
|
2627
|
+
storageJid = this._resolveTcTokenJid(rawFrom);
|
|
2089
2628
|
}
|
|
2090
2629
|
|
|
2091
2630
|
_whaDbg('[DBG] PRIVACY_TOKEN_NOTIF storageJid=' + storageJid);
|
|
@@ -2112,11 +2651,7 @@ class WhalibmobClient extends EventEmitter {
|
|
|
2112
2651
|
|
|
2113
2652
|
void (async () => {
|
|
2114
2653
|
try {
|
|
2115
|
-
|
|
2116
|
-
const fromNorm = normalizeJidForTcToken(from);
|
|
2117
|
-
const fromUser = fromNorm.split('@')[0];
|
|
2118
|
-
const lidUser = this._pnToLid && this._pnToLid.get(fromUser);
|
|
2119
|
-
const tcJid = lidUser ? (lidUser + '@lid') : fromNorm;
|
|
2654
|
+
const tcJid = this._resolveTcTokenJid(from);
|
|
2120
2655
|
|
|
2121
2656
|
const entry = this._tcTokenStore.get(tcJid);
|
|
2122
2657
|
const senderTs = entry && entry.senderTimestamp;
|
|
@@ -2128,9 +2663,15 @@ class WhalibmobClient extends EventEmitter {
|
|
|
2128
2663
|
_whaDbg('[DBG] REISSUE_TC_TOKEN_AFTER_IDENTITY_CHANGE jid=' + tcJid +
|
|
2129
2664
|
' prevSenderTs=' + senderTs);
|
|
2130
2665
|
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
this
|
|
2666
|
+
// Baileys re-issues with the *previous* senderTimestamp, not a fresh
|
|
2667
|
+
// one, and stores the result without touching the dedupe state. That
|
|
2668
|
+
// matters: this is a repair of an existing grant after the peer's
|
|
2669
|
+
// device changed, not a new grant, so it must not move the 7-day
|
|
2670
|
+
// bucket that governs regular issuance. We were stamping it with
|
|
2671
|
+
// Date.now(), which silently skipped the next scheduled issuance.
|
|
2672
|
+
const issueJid = this._resolveTcTokenIssuanceJid(from);
|
|
2673
|
+
const result = await this._issuePrivacyTokens(issueJid, senderTs);
|
|
2674
|
+
this._storeTcTokenFromIqResult(result, tcJid, senderTs, { saveSenderTs: false });
|
|
2134
2675
|
} catch (e) {
|
|
2135
2676
|
_whaDbg('[DBG] REISSUE_TC_TOKEN_ERR from=' + from +
|
|
2136
2677
|
' err=' + (e && e.message));
|
|
@@ -2152,7 +2693,12 @@ class WhalibmobClient extends EventEmitter {
|
|
|
2152
2693
|
*/
|
|
2153
2694
|
_handleHistorySync(notification, msgId) {
|
|
2154
2695
|
if (!notification) return;
|
|
2155
|
-
|
|
2696
|
+
// Web-mode history lands in its own files. A number can be registered over
|
|
2697
|
+
// SMS and separately linked as a companion, and only the companion ever
|
|
2698
|
+
// receives history — merging the two would be merging different accounts'
|
|
2699
|
+
// views of the same number.
|
|
2700
|
+
const phone = (this._store && this._store.phoneNumber) +
|
|
2701
|
+
(this._mode === 'web' ? '.web' : '');
|
|
2156
2702
|
const sessionDir = this._sessionDir;
|
|
2157
2703
|
|
|
2158
2704
|
_whaDbg('[DBG] HIST_SYNC start syncType=' + notification.syncType +
|
|
@@ -2539,15 +3085,13 @@ class WhalibmobClient extends EventEmitter {
|
|
|
2539
3085
|
subscribeToPresence(jids) {
|
|
2540
3086
|
if (!this._socket || !this._connected) return;
|
|
2541
3087
|
const arr = Array.isArray(jids) ? jids : [jids];
|
|
2542
|
-
const {
|
|
3088
|
+
const { isRegularTcTokenUser } = require('./messages/TcTokenStore');
|
|
2543
3089
|
for (const raw of arr) {
|
|
2544
3090
|
const jid = makeJid(raw);
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
);
|
|
2548
|
-
const children = (
|
|
2549
|
-
? [new BinaryNode('tctoken', {}, entry.token)]
|
|
2550
|
-
: null;
|
|
3091
|
+
// Groups and newsletters have no privacy token; Baileys guards the same
|
|
3092
|
+
// way before building the node.
|
|
3093
|
+
const token = isRegularTcTokenUser(jid) ? this._tcTokenFor(jid) : null;
|
|
3094
|
+
const children = token ? [new BinaryNode('tctoken', {}, token)] : null;
|
|
2551
3095
|
this._socket.sendNode(new BinaryNode('presence', {
|
|
2552
3096
|
to: jidStrToObj(jid),
|
|
2553
3097
|
type: 'subscribe'
|
|
@@ -2682,12 +3226,8 @@ class WhalibmobClient extends EventEmitter {
|
|
|
2682
3226
|
iqAttrs = { id: this._genMsgId(), xmlns: 'w:g2', to: jidStrToObj(jidFull), type: 'get' };
|
|
2683
3227
|
content = [new BinaryNode('pictures', {}, [new BinaryNode('picture', attrs, null)])];
|
|
2684
3228
|
} else {
|
|
2685
|
-
const
|
|
2686
|
-
const
|
|
2687
|
-
this._tcTokenStore.get(normalizeJidForTcToken(jidFull));
|
|
2688
|
-
const picChildren = (entry && entry.token && entry.token.length)
|
|
2689
|
-
? [new BinaryNode('tctoken', {}, entry.token)]
|
|
2690
|
-
: null;
|
|
3229
|
+
const picToken = this._tcTokenFor(jidFull);
|
|
3230
|
+
const picChildren = picToken ? [new BinaryNode('tctoken', {}, picToken)] : null;
|
|
2691
3231
|
iqAttrs = {
|
|
2692
3232
|
id: this._genMsgId(),
|
|
2693
3233
|
xmlns: 'w:profile:picture',
|
|
@@ -2735,7 +3275,7 @@ class WhalibmobClient extends EventEmitter {
|
|
|
2735
3275
|
const info = await this.queryPictureInfo(jid, opts);
|
|
2736
3276
|
if (!info || !info.url) return null;
|
|
2737
3277
|
const { httpGetBuffer } = require('./MediaService');
|
|
2738
|
-
return httpGetBuffer(info.url);
|
|
3278
|
+
return httpGetBuffer(info.url, { web: this._mode === 'web' });
|
|
2739
3279
|
}
|
|
2740
3280
|
|
|
2741
3281
|
// Parse the <list> a blocklist query or update comes back with.
|
|
@@ -3007,26 +3547,39 @@ class WhalibmobClient extends EventEmitter {
|
|
|
3007
3547
|
}
|
|
3008
3548
|
|
|
3009
3549
|
const ownJid = String(this._store.phoneNumber) + '@s.whatsapp.net';
|
|
3010
|
-
let recipients;
|
|
3550
|
+
let recipients, source;
|
|
3011
3551
|
|
|
3012
3552
|
if (privacy.type === 'whitelist') {
|
|
3013
3553
|
recipients = privacy.list.slice();
|
|
3554
|
+
source = 'whitelist';
|
|
3014
3555
|
} else {
|
|
3015
|
-
const hist = this.getHistoryStore();
|
|
3016
|
-
const
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3556
|
+
const hist = this.getHistoryStore() || {};
|
|
3557
|
+
const isPerson = (jid) => !jid.endsWith('@g.us') && !jid.endsWith('@broadcast') &&
|
|
3558
|
+
!jid.endsWith('@newsletter') && !this._isOwnUser(jid);
|
|
3559
|
+
|
|
3560
|
+
// Contacts proper: entries the history sync gave a name to.
|
|
3561
|
+
const contacts = Object.keys(hist.contacts || {}).filter(jid => {
|
|
3020
3562
|
const c = hist.contacts[jid];
|
|
3021
|
-
return c && (c.name || c.displayName) &&
|
|
3022
|
-
!jid.endsWith('@broadcast');
|
|
3563
|
+
return c && (c.name || c.displayName) && isPerson(jid);
|
|
3023
3564
|
});
|
|
3565
|
+
|
|
3566
|
+
// A named contact list is what WhatsApp itself posts to. When history has
|
|
3567
|
+
// not brought one — a fresh session, or a sync that carried chats without
|
|
3568
|
+
// names — the people you actually have conversations with are the next
|
|
3569
|
+
// best thing, and far better than posting to nobody.
|
|
3570
|
+
let pool = contacts;
|
|
3571
|
+
source = 'contacts';
|
|
3572
|
+
if (pool.length === 0) {
|
|
3573
|
+
pool = Object.keys(hist.chats || {}).filter(isPerson);
|
|
3574
|
+
source = 'chats';
|
|
3575
|
+
}
|
|
3576
|
+
|
|
3024
3577
|
const blocked = new Set(privacy.type === 'blacklist' ? privacy.list : []);
|
|
3025
|
-
recipients =
|
|
3578
|
+
recipients = pool.filter(jid => !blocked.has(jid));
|
|
3026
3579
|
}
|
|
3027
3580
|
|
|
3028
3581
|
if (!recipients.some(j => this._isOwnUser(j))) recipients.push(ownJid);
|
|
3029
|
-
_whaDbg('[DBG] STATUS_RECIPIENTS type=' + privacy.type +
|
|
3582
|
+
_whaDbg('[DBG] STATUS_RECIPIENTS type=' + privacy.type + ' source=' + source +
|
|
3030
3583
|
' count=' + recipients.length);
|
|
3031
3584
|
return recipients;
|
|
3032
3585
|
}
|
|
@@ -3756,7 +4309,6 @@ class WhalibmobClient extends EventEmitter {
|
|
|
3756
4309
|
async createGroup(subject, participants, opts) {
|
|
3757
4310
|
opts = opts || {};
|
|
3758
4311
|
const id = this._genMsgId();
|
|
3759
|
-
const { normalizeJidForTcToken } = require('./messages/TcTokenStore');
|
|
3760
4312
|
// participant nodes MUST use attr `jid`, not `value`
|
|
3761
4313
|
const partNodes = (participants || []).map(p => {
|
|
3762
4314
|
const jid = makeJid(p);
|
|
@@ -3765,11 +4317,8 @@ class WhalibmobClient extends EventEmitter {
|
|
|
3765
4317
|
const pn = this._lidToPn.get(jid.split('@')[0].split(':')[0]);
|
|
3766
4318
|
if (pn) attrs.phone_number = jidStrToObj(pn + '@s.whatsapp.net');
|
|
3767
4319
|
}
|
|
3768
|
-
const
|
|
3769
|
-
|
|
3770
|
-
const children = (entry && entry.token && entry.token.length)
|
|
3771
|
-
? [new BinaryNode('privacy', {}, entry.token)]
|
|
3772
|
-
: null;
|
|
4320
|
+
const token = this._tcTokenFor(jid);
|
|
4321
|
+
const children = token ? [new BinaryNode('privacy', {}, token)] : null;
|
|
3773
4322
|
return new BinaryNode('participant', attrs, children);
|
|
3774
4323
|
});
|
|
3775
4324
|
|
|
@@ -3876,7 +4425,6 @@ class WhalibmobClient extends EventEmitter {
|
|
|
3876
4425
|
async _groupParticipantAction(groupJid, action, jids) {
|
|
3877
4426
|
const gJid = makeJid(groupJid);
|
|
3878
4427
|
const arr = Array.isArray(jids) ? jids : [jids];
|
|
3879
|
-
const { normalizeJidForTcToken } = require('./messages/TcTokenStore');
|
|
3880
4428
|
// Same shape whatsmeow's UpdateGroupParticipants builds: binary JIDs, and on
|
|
3881
4429
|
// an add, the phone number behind a LID plus our privacy token for them.
|
|
3882
4430
|
const participantNodes = arr.map(j => {
|
|
@@ -3888,11 +4436,8 @@ class WhalibmobClient extends EventEmitter {
|
|
|
3888
4436
|
const pn = this._lidToPn.get(jid.split('@')[0].split(':')[0]);
|
|
3889
4437
|
if (pn) attrs.phone_number = jidStrToObj(pn + '@s.whatsapp.net');
|
|
3890
4438
|
}
|
|
3891
|
-
const
|
|
3892
|
-
|
|
3893
|
-
if (entry && entry.token && entry.token.length) {
|
|
3894
|
-
children = [new BinaryNode('privacy', {}, entry.token)];
|
|
3895
|
-
}
|
|
4439
|
+
const token = this._tcTokenFor(jid);
|
|
4440
|
+
if (token) children = [new BinaryNode('privacy', {}, token)];
|
|
3896
4441
|
}
|
|
3897
4442
|
return new BinaryNode('participant', attrs, children);
|
|
3898
4443
|
});
|