whalibmob 5.5.31 → 5.5.32
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 +91 -7
- package/lib/Registration.js +115 -12
- package/lib/noise.js +2 -2
- package/package.json +1 -1
package/lib/Client.js
CHANGED
|
@@ -122,6 +122,7 @@ class WhalibmobClient extends EventEmitter {
|
|
|
122
122
|
this._connected = false;
|
|
123
123
|
this._reconnecting = false;
|
|
124
124
|
this._reconnectTry = 0;
|
|
125
|
+
this._hasConnectedOnce = false; // true after first successful open; used to detect reconnects
|
|
125
126
|
this._phoneNumber = null;
|
|
126
127
|
this._mediaConn = null;
|
|
127
128
|
this._pendingIqs = new Map(); // id → resolve fn
|
|
@@ -277,6 +278,32 @@ class WhalibmobClient extends EventEmitter {
|
|
|
277
278
|
|
|
278
279
|
this._requestMediaConnection();
|
|
279
280
|
this._uploadPreKeys();
|
|
281
|
+
|
|
282
|
+
// ── Reconnect: flush stale device cache + background re-usync ─────────────
|
|
283
|
+
// On first connect _hasConnectedOnce is false — nothing extra to do.
|
|
284
|
+
// On every reconnect (network drop / NAT reset / server restart) we flush the
|
|
285
|
+
// in-memory device cache because contacts may have linked or unlinked devices
|
|
286
|
+
// while we were offline. Disk files are kept for next restart warm-read;
|
|
287
|
+
// in-memory entries are cleared so the next send triggers a fresh usync IQ.
|
|
288
|
+
if (this._hasConnectedOnce && this._devMgr) {
|
|
289
|
+
const phonesInCache = Object.keys(this._devMgr._cacheSnapshot || {});
|
|
290
|
+
_whaDbg('[DBG] RECONNECT: flushing device cache (' + phonesInCache.length + ' entries) for fresh usync');
|
|
291
|
+
this._devMgr._dcFlush();
|
|
292
|
+
|
|
293
|
+
// Immediately re-usync own devices in the background (tablets / linked devices
|
|
294
|
+
// could have been added or unlinked while we were offline).
|
|
295
|
+
if (this._store && this._store.phoneNumber && this._signal) {
|
|
296
|
+
const ownPhone = this._store.phoneNumber;
|
|
297
|
+
setImmediate(() => {
|
|
298
|
+
if (!this._connected || !this._devMgr) return;
|
|
299
|
+
this._devMgr.ensureOwnDeviceSessions(ownPhone, this._signal, false)
|
|
300
|
+
.then(() => _whaDbg('[DBG] RECONNECT own-device usync done'))
|
|
301
|
+
.catch(e => _whaDbg('[DBG] RECONNECT own-device usync err: ' + e.message));
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
this._hasConnectedOnce = true;
|
|
306
|
+
|
|
280
307
|
this.emit('connected');
|
|
281
308
|
}
|
|
282
309
|
|
|
@@ -1009,6 +1036,50 @@ class WhalibmobClient extends EventEmitter {
|
|
|
1009
1036
|
this._handlePrivacyTokenNotification(node);
|
|
1010
1037
|
}
|
|
1011
1038
|
|
|
1039
|
+
if (type === 'devices') {
|
|
1040
|
+
// Server push: a contact's linked device list changed (they linked or
|
|
1041
|
+
// unlinked a tablet, desktop, etc.). Invalidate that phone's cache entry
|
|
1042
|
+
// so the next send triggers a fresh usync IQ and reaches all their devices.
|
|
1043
|
+
const fromJid = attrs.from || '';
|
|
1044
|
+
const fromPhone = fromJid.split('@')[0].split(':')[0];
|
|
1045
|
+
if (fromPhone && this._devMgr) {
|
|
1046
|
+
this._devMgr._dcDel([fromPhone]);
|
|
1047
|
+
_whaDbg('[DBG] NOTIF_DEVICES invalidated cache for ' + fromPhone);
|
|
1048
|
+
// Also process any inline <devices> list the server included
|
|
1049
|
+
const devicesNode = findChildDeep(node, 'devices');
|
|
1050
|
+
if (devicesNode) this._processDeviceUpdate(devicesNode);
|
|
1051
|
+
// Background re-usync so the cache is warm before the next send
|
|
1052
|
+
if (this._signal) {
|
|
1053
|
+
setImmediate(() => {
|
|
1054
|
+
if (!this._connected || !this._devMgr) return;
|
|
1055
|
+
this._devMgr.bulkEnsureSessions([fromPhone], this._signal, false)
|
|
1056
|
+
.then(() => _whaDbg('[DBG] NOTIF_DEVICES bg-usync done for ' + fromPhone))
|
|
1057
|
+
.catch(e => _whaDbg('[DBG] NOTIF_DEVICES bg-usync err: ' + e.message));
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
if (type === 'identity') {
|
|
1064
|
+
// Server push: a contact re-registered WhatsApp (new identity key / new phone).
|
|
1065
|
+
// Their old Signal sessions are no longer valid — clear them and the device
|
|
1066
|
+
// cache so the next send builds a fresh pkmsg session from scratch.
|
|
1067
|
+
const fromJid = attrs.from || '';
|
|
1068
|
+
const fromPhone = fromJid.split('@')[0].split(':')[0];
|
|
1069
|
+
if (fromPhone && this._devMgr) {
|
|
1070
|
+
this._devMgr._dcDel([fromPhone]);
|
|
1071
|
+
_whaDbg('[DBG] NOTIF_IDENTITY flushed device cache for re-registered ' + fromPhone);
|
|
1072
|
+
}
|
|
1073
|
+
if (fromPhone && this._signal && this._signal.store && this._signal.store._sessions) {
|
|
1074
|
+
const sessions = this._signal.store._sessions;
|
|
1075
|
+
let cleared = 0;
|
|
1076
|
+
for (const addr of Object.keys(sessions)) {
|
|
1077
|
+
if (addr.startsWith(fromPhone + '.')) { delete sessions[addr]; cleared++; }
|
|
1078
|
+
}
|
|
1079
|
+
_whaDbg('[DBG] NOTIF_IDENTITY cleared ' + cleared + ' Signal sessions for ' + fromPhone);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1012
1083
|
// Ack the notification
|
|
1013
1084
|
if (this._socket && this._connected) {
|
|
1014
1085
|
this._socket.sendNode(new BinaryNode('ack', {
|
|
@@ -1021,16 +1092,29 @@ class WhalibmobClient extends EventEmitter {
|
|
|
1021
1092
|
}
|
|
1022
1093
|
|
|
1023
1094
|
_processDeviceUpdate(devicesNode) {
|
|
1024
|
-
|
|
1095
|
+
// Called from _handleNotification(type='account_sync') and type='devices'.
|
|
1096
|
+
// Rebuilds the device cache for the affected phones from the server-provided list.
|
|
1097
|
+
// Previous bug: called Set.add() on cache entries that are number[] arrays → silently failed.
|
|
1098
|
+
if (!Array.isArray(devicesNode.content) || !this._devMgr) return;
|
|
1099
|
+
|
|
1100
|
+
// Group all device IDs per phone from the notification
|
|
1101
|
+
const phoneDevices = new Map(); // phone → number[]
|
|
1025
1102
|
for (const deviceNode of devicesNode.content) {
|
|
1026
1103
|
if (!deviceNode || deviceNode.description !== 'device') continue;
|
|
1027
1104
|
const jid = deviceNode.attrs && deviceNode.attrs.jid;
|
|
1028
|
-
if (jid
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1105
|
+
if (!jid) continue;
|
|
1106
|
+
const phone = String(jid).split('@')[0].split(':')[0];
|
|
1107
|
+
const device = parseInt((String(jid).split(':')[1] || '0').split('@')[0], 10);
|
|
1108
|
+
if (!phoneDevices.has(phone)) phoneDevices.set(phone, []);
|
|
1109
|
+
const ids = phoneDevices.get(phone);
|
|
1110
|
+
if (!ids.includes(device)) ids.push(device);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// Replace cache entries atomically per phone (server list is authoritative)
|
|
1114
|
+
for (const [phone, ids] of phoneDevices) {
|
|
1115
|
+
this._devMgr._dcDel([phone]); // evict stale entry
|
|
1116
|
+
for (const id of ids) this._devMgr._dcAdd(phone, id);
|
|
1117
|
+
_whaDbg('[DBG] DEV_UPDATE phone=' + phone + ' ids=[' + ids.join(',') + ']');
|
|
1034
1118
|
}
|
|
1035
1119
|
}
|
|
1036
1120
|
|
package/lib/Registration.js
CHANGED
|
@@ -11,9 +11,11 @@ const { getDeviceConfig } = require('./DeviceConfig');
|
|
|
11
11
|
// ---------- SOCKS5 / Tor support ----------
|
|
12
12
|
// Set TOR_PROXY=socks5://127.0.0.1:9050 (or any socks5 host) to route all
|
|
13
13
|
// WhatsApp registration traffic through Tor / a residential proxy.
|
|
14
|
-
|
|
14
|
+
let SOCKS_LIB;
|
|
15
|
+
try { SOCKS_LIB = require.resolve('socks'); }
|
|
16
|
+
catch (_) { SOCKS_LIB = '/home/runner/workspace/.config/npm/node_global/lib/node_modules/socks/build/index.js'; }
|
|
15
17
|
|
|
16
|
-
async function httpPostViaSocks(path, body, waVersion, proxyUrl) {
|
|
18
|
+
async function httpPostViaSocks(path, body, waVersion, proxyUrl, locale) {
|
|
17
19
|
const url = new URL(proxyUrl);
|
|
18
20
|
const pHost = url.hostname;
|
|
19
21
|
const pPort = parseInt(url.port) || 1080;
|
|
@@ -43,13 +45,17 @@ async function httpPostViaSocks(path, body, waVersion, proxyUrl) {
|
|
|
43
45
|
const userAgent = _dev.os === 'android'
|
|
44
46
|
? `WhatsApp/${waVersion} A`
|
|
45
47
|
: `WhatsApp/${waVersion} iOS/${_dev.osVersion} Device/${_dev.model}`;
|
|
48
|
+
const _acceptLang = locale || 'en-US;q=1';
|
|
46
49
|
const req = [
|
|
47
50
|
`POST /v2${path} HTTP/1.1`,
|
|
48
51
|
`Host: ${dHost}`,
|
|
49
52
|
`User-Agent: ${userAgent}`,
|
|
53
|
+
`Accept: */*`,
|
|
54
|
+
`Accept-Language: ${_acceptLang}`,
|
|
55
|
+
`Accept-Encoding: gzip, deflate, br`,
|
|
50
56
|
`Content-Type: application/x-www-form-urlencoded`,
|
|
51
57
|
`Content-Length: ${Buffer.byteLength(body)}`,
|
|
52
|
-
`Connection:
|
|
58
|
+
`Connection: keep-alive`,
|
|
53
59
|
'',
|
|
54
60
|
body
|
|
55
61
|
].join('\r\n');
|
|
@@ -216,6 +222,80 @@ const TWO_DIGIT_CCS = new Set([
|
|
|
216
222
|
'95','98'
|
|
217
223
|
]);
|
|
218
224
|
|
|
225
|
+
|
|
226
|
+
// ---------- Timezone offset per country code (UTC seconds) ----------
|
|
227
|
+
// Used to populate time_zone_offset in /code requests — real iPhones
|
|
228
|
+
// always send this field with their local offset.
|
|
229
|
+
const CC_TZ_OFFSET = {
|
|
230
|
+
'1': -18000, // US Eastern
|
|
231
|
+
'7': 10800, // Russia Moscow
|
|
232
|
+
'20': 7200, // Egypt
|
|
233
|
+
'27': 7200, // South Africa
|
|
234
|
+
'30': 7200, // Greece
|
|
235
|
+
'31': 3600, // Netherlands
|
|
236
|
+
'32': 3600, // Belgium
|
|
237
|
+
'33': 3600, // France
|
|
238
|
+
'34': 3600, // Spain
|
|
239
|
+
'36': 3600, // Hungary
|
|
240
|
+
'39': 3600, // Italy
|
|
241
|
+
'40': 7200, // Romania
|
|
242
|
+
'41': 3600, // Switzerland
|
|
243
|
+
'43': 3600, // Austria
|
|
244
|
+
'44': 0, // UK
|
|
245
|
+
'45': 3600, // Denmark
|
|
246
|
+
'46': 3600, // Sweden
|
|
247
|
+
'47': 3600, // Norway
|
|
248
|
+
'48': 3600, // Poland
|
|
249
|
+
'49': 3600, // Germany
|
|
250
|
+
'51': -18000, // Peru
|
|
251
|
+
'52': -21600, // Mexico
|
|
252
|
+
'54': -10800, // Argentina
|
|
253
|
+
'55': -10800, // Brazil
|
|
254
|
+
'56': -14400, // Chile
|
|
255
|
+
'57': -18000, // Colombia
|
|
256
|
+
'58': -14400, // Venezuela
|
|
257
|
+
'60': 28800, // Malaysia
|
|
258
|
+
'61': 36000, // Australia
|
|
259
|
+
'62': 25200, // Indonesia
|
|
260
|
+
'63': 28800, // Philippines
|
|
261
|
+
'64': 43200, // New Zealand
|
|
262
|
+
'65': 28800, // Singapore
|
|
263
|
+
'66': 25200, // Thailand
|
|
264
|
+
'81': 32400, // Japan
|
|
265
|
+
'82': 32400, // South Korea
|
|
266
|
+
'84': 25200, // Vietnam
|
|
267
|
+
'86': 28800, // China
|
|
268
|
+
'90': 10800, // Turkey
|
|
269
|
+
'91': 19800, // India (IST = UTC+5:30)
|
|
270
|
+
'92': 18000, // Pakistan
|
|
271
|
+
'93': 16200, // Afghanistan (UTC+4:30)
|
|
272
|
+
'94': 19800, // Sri Lanka
|
|
273
|
+
'95': 23400, // Myanmar (UTC+6:30)
|
|
274
|
+
'98': 12600, // Iran (UTC+3:30)
|
|
275
|
+
'212': 3600, // Morocco
|
|
276
|
+
'213': 3600, // Algeria
|
|
277
|
+
'216': 3600, // Tunisia
|
|
278
|
+
'218': 7200, // Libya
|
|
279
|
+
'234': 3600, // Nigeria
|
|
280
|
+
'254': 10800, // Kenya
|
|
281
|
+
'255': 10800, // Tanzania
|
|
282
|
+
'256': 10800, // Uganda
|
|
283
|
+
'351': 0, // Portugal
|
|
284
|
+
'353': 0, // Ireland
|
|
285
|
+
'358': 7200, // Finland
|
|
286
|
+
'380': 7200, // Ukraine
|
|
287
|
+
'420': 3600, // Czech Republic
|
|
288
|
+
'966': 10800, // Saudi Arabia
|
|
289
|
+
'971': 14400, // UAE
|
|
290
|
+
'972': 7200, // Israel
|
|
291
|
+
'880': 21600, // Bangladesh
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
function getTzOffset(cc) {
|
|
295
|
+
const v = CC_TZ_OFFSET[String(cc)];
|
|
296
|
+
return (v !== undefined) ? v : 0;
|
|
297
|
+
}
|
|
298
|
+
|
|
219
299
|
function parsePhone(phoneNumber) {
|
|
220
300
|
const str = String(phoneNumber).replace(/\D/g, '');
|
|
221
301
|
|
|
@@ -413,9 +493,9 @@ function encryptPayload(plaintext) {
|
|
|
413
493
|
|
|
414
494
|
// ---------- HTTP ----------
|
|
415
495
|
|
|
416
|
-
function httpPost(path, body, waVersion) {
|
|
496
|
+
function httpPost(path, body, waVersion, locale) {
|
|
417
497
|
const proxy = process.env.TOR_PROXY || process.env.SOCKS_PROXY || '';
|
|
418
|
-
if (proxy) return httpPostViaSocks(path, body, waVersion, proxy);
|
|
498
|
+
if (proxy) return httpPostViaSocks(path, body, waVersion, proxy, locale);
|
|
419
499
|
|
|
420
500
|
return new Promise((resolve, reject) => {
|
|
421
501
|
const _httpDev = getDeviceConfig();
|
|
@@ -423,6 +503,7 @@ function httpPost(path, body, waVersion) {
|
|
|
423
503
|
? `WhatsApp/${waVersion} A`
|
|
424
504
|
: `WhatsApp/${waVersion} iOS/${_httpDev.osVersion} Device/${_httpDev.model}`;
|
|
425
505
|
const bodyBuf = Buffer.from(body, 'utf8');
|
|
506
|
+
const acceptLang = locale || 'en-US;q=1';
|
|
426
507
|
const opts = {
|
|
427
508
|
hostname: 'v.whatsapp.net',
|
|
428
509
|
port: 443,
|
|
@@ -430,9 +511,13 @@ function httpPost(path, body, waVersion) {
|
|
|
430
511
|
method: 'POST',
|
|
431
512
|
timeout: 20000,
|
|
432
513
|
headers: {
|
|
433
|
-
'User-Agent':
|
|
434
|
-
'
|
|
435
|
-
'
|
|
514
|
+
'User-Agent': userAgent,
|
|
515
|
+
'Accept': '*/*',
|
|
516
|
+
'Accept-Language': acceptLang,
|
|
517
|
+
'Accept-Encoding': 'gzip, deflate, br',
|
|
518
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
519
|
+
'Content-Length': bodyBuf.length,
|
|
520
|
+
'Connection': 'keep-alive'
|
|
436
521
|
}
|
|
437
522
|
};
|
|
438
523
|
const req = https.request(opts, (res) => {
|
|
@@ -458,7 +543,12 @@ async function sendRequest(path, store, waVersion, useToken, extraPairs) {
|
|
|
458
543
|
const plaintext = buildPayload(store, waVersion, useToken, extraPairs);
|
|
459
544
|
const enc = encryptPayload(plaintext);
|
|
460
545
|
const body = 'ENC=' + enc;
|
|
461
|
-
|
|
546
|
+
const { cc: _srCc } = parsePhone(store.phoneNumber);
|
|
547
|
+
const _srMeta = getCountryMeta(_srCc);
|
|
548
|
+
const locale = _srMeta.lg === 'en'
|
|
549
|
+
? `en-${_srMeta.lc};q=1`
|
|
550
|
+
: `${_srMeta.lg}-${_srMeta.lc};q=1, en-${_srMeta.lc};q=0.9`;
|
|
551
|
+
return httpPost(path, body, waVersion, locale);
|
|
462
552
|
}
|
|
463
553
|
|
|
464
554
|
// ---------- Public API ----------
|
|
@@ -501,7 +591,10 @@ async function checkNumberStatus(phoneNumber) {
|
|
|
501
591
|
'sim_mcc', meta.mcc,
|
|
502
592
|
'sim_mnc', meta.mnc,
|
|
503
593
|
'reason', '',
|
|
504
|
-
'cellular_strength', '
|
|
594
|
+
'cellular_strength', '3',
|
|
595
|
+
'network_radio_type','1',
|
|
596
|
+
'mistyped', '0',
|
|
597
|
+
'time_zone_offset', String(getTzOffset(cc))
|
|
505
598
|
];
|
|
506
599
|
|
|
507
600
|
function hasBlock(r) {
|
|
@@ -608,13 +701,18 @@ async function requestSmsCode(store, method, opts) {
|
|
|
608
701
|
if (!emailAddr) throw new Error('requestSmsCode: email method requires opts.email address');
|
|
609
702
|
const { cc: _regCc } = parsePhone(store.phoneNumber);
|
|
610
703
|
const _regMeta = getCountryMeta(_regCc);
|
|
704
|
+
const _emailTz = String(getTzOffset(parsePhone(store.phoneNumber).cc));
|
|
611
705
|
const extra = [
|
|
612
706
|
'method', 'email',
|
|
613
707
|
'email', emailAddr,
|
|
614
708
|
'sim_mcc', _regMeta.mcc,
|
|
615
709
|
'sim_mnc', _regMeta.mnc,
|
|
616
710
|
'reason', '',
|
|
617
|
-
'cellular_strength', '
|
|
711
|
+
'cellular_strength', '3',
|
|
712
|
+
'network_radio_type','1',
|
|
713
|
+
'hasinrc', _device.os === 'ios' ? '1' : null,
|
|
714
|
+
'mistyped', '0',
|
|
715
|
+
'time_zone_offset', _emailTz
|
|
618
716
|
];
|
|
619
717
|
const result = await sendRequest('/code', store, waVersion, true, extra);
|
|
620
718
|
const status = result.status;
|
|
@@ -632,13 +730,18 @@ async function requestSmsCode(store, method, opts) {
|
|
|
632
730
|
const { cc: _regCc } = parsePhone(store.phoneNumber);
|
|
633
731
|
const _regMeta = getCountryMeta(_regCc);
|
|
634
732
|
|
|
733
|
+
const _regTz = String(getTzOffset(_regCc));
|
|
635
734
|
async function _tryMethod(m) {
|
|
636
735
|
const extra = [
|
|
637
736
|
'method', m,
|
|
638
737
|
'sim_mcc', _regMeta.mcc,
|
|
639
738
|
'sim_mnc', _regMeta.mnc,
|
|
640
739
|
'reason', '',
|
|
641
|
-
'cellular_strength', '
|
|
740
|
+
'cellular_strength', '3',
|
|
741
|
+
'network_radio_type','1',
|
|
742
|
+
'hasinrc', _device.os === 'ios' ? '1' : null,
|
|
743
|
+
'mistyped', '0',
|
|
744
|
+
'time_zone_offset', _regTz
|
|
642
745
|
];
|
|
643
746
|
|
|
644
747
|
// Retry loop:
|
package/lib/noise.js
CHANGED
|
@@ -310,8 +310,8 @@ class NoiseSocket extends EventEmitter {
|
|
|
310
310
|
username: BigInt(this.store.phoneNumber),
|
|
311
311
|
passive: false,
|
|
312
312
|
pushName: this.store.registered ? (this.store.name || null) : null,
|
|
313
|
-
shortConnect:
|
|
314
|
-
connectType: 1,
|
|
313
|
+
shortConnect: (this.store.connectAttemptCount || 0) > 0,
|
|
314
|
+
connectType: (this.store.connectAttemptCount || 0) > 0 ? 3 : 1,
|
|
315
315
|
connectReason: 1,
|
|
316
316
|
connectAttemptCount: (this.store.connectAttemptCount || 0),
|
|
317
317
|
device: 0,
|