whalibmob 5.14.11 → 5.14.13
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 +9 -8
- package/cli.js +78 -34
- package/lib/Client.js +124 -38
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -430,16 +430,17 @@ The CLI asks what to call it once, on a first run, and remembers the answer in
|
|
|
430
430
|
|
|
431
431
|
### CLI Pairing Code
|
|
432
432
|
|
|
433
|
-
If the number is already in use on a phone, or the verification SMS never arrives, link to the existing account instead.
|
|
433
|
+
If the number is already in use on a phone, or the verification SMS never arrives, link to the existing account instead.
|
|
434
|
+
|
|
435
|
+
`wa connect` works out which of the two a number is set up for by reading its session files, and says which one it chose:
|
|
434
436
|
|
|
435
437
|
```
|
|
436
|
-
|
|
437
|
-
1) sms register this number as its own device
|
|
438
|
-
2) pairing code link to an existing WhatsApp account (8-digit code)
|
|
439
|
-
sms or pairing code? [1/2] 2
|
|
438
|
+
connecting as companion (pairing code)...
|
|
440
439
|
```
|
|
441
440
|
|
|
442
|
-
|
|
441
|
+
A number can hold both — registered over SMS as its own device, and linked as a companion to another account. When it holds both, the one used most recently wins. A half-finished registration does not count as a session either way. When there is nothing to connect with, it says so and names the two commands that would create one, rather than picking a side.
|
|
442
|
+
|
|
443
|
+
Force either one:
|
|
443
444
|
|
|
444
445
|
```sh
|
|
445
446
|
wa connect 919634847671 --sms
|
|
@@ -1430,7 +1431,7 @@ the session rather than the environment.
|
|
|
1430
1431
|
|
|
1431
1432
|
```sh
|
|
1432
1433
|
# connect to a number (while already in the shell)
|
|
1433
|
-
#
|
|
1434
|
+
# picks sms or pairing from the session files on disk
|
|
1434
1435
|
wa> /connect 919634847671
|
|
1435
1436
|
|
|
1436
1437
|
# force one or the other
|
|
@@ -1563,7 +1564,7 @@ wa> /quit
|
|
|
1563
1564
|
| `/reg code <phone> [method] [--name "Name"]` | Request verification code, optionally naming the account |
|
|
1564
1565
|
| `/reg confirm <phone> <code> [--name "Name"]` | Complete registration |
|
|
1565
1566
|
| **Connection** | |
|
|
1566
|
-
| `/connect <phone> [sms\|pair]` | Connect to WhatsApp —
|
|
1567
|
+
| `/connect <phone> [sms\|pair]` | Connect to WhatsApp — picks the method from the session files when unset |
|
|
1567
1568
|
| `/pair <phone> [code]` | Link to an existing account by 8-digit pairing code |
|
|
1568
1569
|
| `/disconnect` | Disconnect current session |
|
|
1569
1570
|
| `/reconnect` | Force reconnection |
|
package/cli.js
CHANGED
|
@@ -900,41 +900,60 @@ function openShell(prompt) {
|
|
|
900
900
|
// Which way in: register this number over SMS as the account's own device, or
|
|
901
901
|
// link to an account that already exists the way WhatsApp Web does.
|
|
902
902
|
//
|
|
903
|
-
//
|
|
904
|
-
//
|
|
905
|
-
//
|
|
906
|
-
|
|
907
|
-
|
|
903
|
+
// Which kind of session a number has, read off the file rather than guessed.
|
|
904
|
+
//
|
|
905
|
+
// A number can hold both — registered over SMS as its own device, and linked
|
|
906
|
+
// as a companion to some other account — so "the file is there" was never
|
|
907
|
+
// enough to go on. It is also not enough on its own: a registration that was
|
|
908
|
+
// started and never finished leaves a store behind with `registered` still
|
|
909
|
+
// false, and connecting with it can only fail.
|
|
910
|
+
//
|
|
911
|
+
// Returns null when there is nothing usable, otherwise when the session was
|
|
912
|
+
// last written. That timestamp is the tie-breaker below.
|
|
913
|
+
function readSessionKind(file, extra) {
|
|
914
|
+
if (!fs.existsSync(file)) return null;
|
|
915
|
+
try {
|
|
916
|
+
const j = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
917
|
+
if (!j.registered) return null;
|
|
918
|
+
if (extra && !extra(j)) return null;
|
|
919
|
+
return { mtime: fs.statSync(file).mtimeMs };
|
|
920
|
+
} catch (_) { return null; }
|
|
908
921
|
}
|
|
909
922
|
|
|
910
|
-
function
|
|
911
|
-
|
|
912
|
-
if (!fs.existsSync(f)) return false;
|
|
913
|
-
try {
|
|
914
|
-
const j = JSON.parse(fs.readFileSync(f, 'utf8'));
|
|
915
|
-
return !!(j.registered && j.me && j.me.id);
|
|
916
|
-
} catch (_) { return false; }
|
|
923
|
+
function mobileSession(phone) {
|
|
924
|
+
return readSessionKind(storeFileFor(_sessDir, phone));
|
|
917
925
|
}
|
|
918
926
|
|
|
919
|
-
function
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
if (web && !mob) return Promise.resolve('pairing');
|
|
923
|
-
if (mob && !web) return Promise.resolve('sms');
|
|
924
|
-
if (!process.stdin.isTTY) return Promise.resolve(mob ? 'sms' : 'pairing');
|
|
927
|
+
function webSession(phone) {
|
|
928
|
+
return readSessionKind(webStoreFileFor(_sessDir, phone), j => !!(j.me && j.me.id));
|
|
929
|
+
}
|
|
925
930
|
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
931
|
+
function hasMobileSession(phone) { return !!mobileSession(phone); }
|
|
932
|
+
function hasWebSession(phone) { return !!webSession(phone); }
|
|
933
|
+
|
|
934
|
+
/**
|
|
935
|
+
* Work out how to connect a number, without asking.
|
|
936
|
+
*
|
|
937
|
+
* This used to put a question on the terminal, and the question was worse than
|
|
938
|
+
* useless: it opened a second reader on the same stdin while the shell's own
|
|
939
|
+
* was still running, so the two split the answer between them and the one that
|
|
940
|
+
* mattered usually got nothing. An unrecognised answer then fell through to
|
|
941
|
+
* "sms" — the more destructive of the two — and a companion session that was
|
|
942
|
+
* perfectly good was met with a login as a primary device, which the server
|
|
943
|
+
* answers with a 401 that reads exactly like a revoked session.
|
|
944
|
+
*
|
|
945
|
+
* There is nothing to ask. The session files say which kinds exist, and when
|
|
946
|
+
* both do, the one used most recently is the one being asked for.
|
|
947
|
+
*
|
|
948
|
+
* @returns {'sms'|'pairing'|null} null when the number has no usable session
|
|
949
|
+
*/
|
|
950
|
+
function resolveLoginMethod(phone) {
|
|
951
|
+
const mob = mobileSession(phone);
|
|
952
|
+
const web = webSession(phone);
|
|
953
|
+
if (mob && !web) return 'sms';
|
|
954
|
+
if (web && !mob) return 'pairing';
|
|
955
|
+
if (!mob && !web) return null;
|
|
956
|
+
return web.mtime >= mob.mtime ? 'pairing' : 'sms';
|
|
938
957
|
}
|
|
939
958
|
|
|
940
959
|
// Handlers for the two things a registration can stop and ask for.
|
|
@@ -945,8 +964,18 @@ function askLoginMethod(phone) {
|
|
|
945
964
|
function registrationPrompts() {
|
|
946
965
|
const prompt = (question) => new Promise((resolve) => {
|
|
947
966
|
if (!process.stdin.isTTY) return resolve(null);
|
|
967
|
+
// The shell's own reader has to stand down first. Two readline interfaces
|
|
968
|
+
// on one stdin both take the keypresses, so the answer is split between
|
|
969
|
+
// them: the shell treats it as a command and this one is left with
|
|
970
|
+
// nothing. Every other place that asks something mid-session already does
|
|
971
|
+
// this — the two that did not were where the answer went missing.
|
|
972
|
+
_rl && _rl.pause();
|
|
948
973
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
949
|
-
rl.question(question, (answer) => {
|
|
974
|
+
rl.question(question, (answer) => {
|
|
975
|
+
rl.close();
|
|
976
|
+
_rl && (_rl.resume(), _rl.prompt(true));
|
|
977
|
+
resolve(String(answer).trim());
|
|
978
|
+
});
|
|
950
979
|
});
|
|
951
980
|
|
|
952
981
|
return {
|
|
@@ -1229,8 +1258,17 @@ async function handleLine(line) {
|
|
|
1229
1258
|
const forced = (p[2] || '').toLowerCase();
|
|
1230
1259
|
const method = forced === 'pair' || forced === 'pairing' ? 'pairing'
|
|
1231
1260
|
: forced === 'sms' ? 'sms'
|
|
1232
|
-
:
|
|
1233
|
-
|
|
1261
|
+
: resolveLoginMethod(phn);
|
|
1262
|
+
// Nothing on disk to connect with. Say which of the two things to do
|
|
1263
|
+
// rather than picking one and letting the server explain it as a 401.
|
|
1264
|
+
if (!method) {
|
|
1265
|
+
fail('no session for +' + phn);
|
|
1266
|
+
out(' register it as its own device: /reg code ' + phn);
|
|
1267
|
+
out(' or link it to an account: /pair ' + phn);
|
|
1268
|
+
break;
|
|
1269
|
+
}
|
|
1270
|
+
out('connecting as ' + (method === 'pairing' ? 'companion (pairing code)'
|
|
1271
|
+
: 'primary device (sms)') + '...');
|
|
1234
1272
|
if (method === 'pairing') await doConnectWeb(phn);
|
|
1235
1273
|
else await doConnect(phn);
|
|
1236
1274
|
break;
|
|
@@ -2943,7 +2981,13 @@ async function main() {
|
|
|
2943
2981
|
const forced = String(flags.method || '').toLowerCase();
|
|
2944
2982
|
const method = flags.pair || forced === 'pair' || forced === 'pairing' ? 'pairing'
|
|
2945
2983
|
: flags.sms || forced === 'sms' ? 'sms'
|
|
2946
|
-
:
|
|
2984
|
+
: resolveLoginMethod(phone);
|
|
2985
|
+
if (!method) {
|
|
2986
|
+
fail('no session for +' + phone);
|
|
2987
|
+
out(' register it as its own device: wa registration --request-code ' + phone);
|
|
2988
|
+
out(' or link it to an account: wa pair ' + phone);
|
|
2989
|
+
process.exit(1);
|
|
2990
|
+
}
|
|
2947
2991
|
// Plain `wa>` until the connection actually opens. Naming the number in the
|
|
2948
2992
|
// prompt before that says "connected as this number" while the line above
|
|
2949
2993
|
// it says there is no session, which is the opposite of what happened —
|
package/lib/Client.js
CHANGED
|
@@ -6,6 +6,7 @@ const EventEmitter = require('events');
|
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const fs = require('fs');
|
|
8
8
|
const crypto = require('crypto');
|
|
9
|
+
const { Mutex } = require('async-mutex');
|
|
9
10
|
const { NoiseSocket } = require('./noise');
|
|
10
11
|
const { MessageSender, generateMessageId, makeJid, buildOrGetAdvIdentity } = require('./messages/MessageSender');
|
|
11
12
|
const { checkIfRegistered, checkNumberStatus, requestSmsCode, verifyCode, assertRegistrationKeys, fetchIosVersion, fetchWaVersion } = require('./Registration');
|
|
@@ -368,6 +369,33 @@ class WhalibmobClient extends EventEmitter {
|
|
|
368
369
|
this._numberFixAttempted = false;
|
|
369
370
|
this._pingTimer = null;
|
|
370
371
|
this._keepTimer = null;
|
|
372
|
+
// Both pre-key checks ask the server a question, wait for the answer, and
|
|
373
|
+
// top the account up if they do not like it. Three things start them —
|
|
374
|
+
// login, the half-hourly timer, and the server saying it is running low —
|
|
375
|
+
// and nothing stopped two from being in the air at once. Overlapping runs
|
|
376
|
+
// each saw the same shortage and each answered it, so the account
|
|
377
|
+
// generated and uploaded two batches for one gap. Serialised, the second
|
|
378
|
+
// run asks after the first has finished: the count it gets back is the one
|
|
379
|
+
// the first run just produced, so it stands down instead of repeating it.
|
|
380
|
+
// One mutex per client — two accounts in the same process are unrelated
|
|
381
|
+
// and must not wait on each other.
|
|
382
|
+
this._preKeyMutex = new Mutex();
|
|
383
|
+
// Incoming work runs one item at a time, in arrival order.
|
|
384
|
+
//
|
|
385
|
+
// Stanzas arrive faster than they are handled, and the handling is
|
|
386
|
+
// asynchronous, so without these each one raced the ones before it: two
|
|
387
|
+
// messages from the same contact could decrypt at once and advance the
|
|
388
|
+
// same ratchet from the same starting point, and two retry receipts could
|
|
389
|
+
// tear a session down while the resend for the previous one was still
|
|
390
|
+
// building it back up.
|
|
391
|
+
//
|
|
392
|
+
// Three separate locks, not one. A message whose decryption waits on
|
|
393
|
+
// something that only arrives as a notification would deadlock against a
|
|
394
|
+
// shared lock — the notification could never be processed to release it.
|
|
395
|
+
// Keeping them apart means each queue only ever waits on its own kind.
|
|
396
|
+
this._messageMutex = new Mutex();
|
|
397
|
+
this._receiptMutex = new Mutex();
|
|
398
|
+
this._notificationMutex = new Mutex();
|
|
371
399
|
this._connected = false;
|
|
372
400
|
this._reconnecting = false;
|
|
373
401
|
this._reconnectTry = 0;
|
|
@@ -2131,6 +2159,16 @@ class WhalibmobClient extends EventEmitter {
|
|
|
2131
2159
|
|
|
2132
2160
|
// ─── Message handling ─────────────────────────────────────────────────────
|
|
2133
2161
|
|
|
2162
|
+
// Put work on one of the incoming queues.
|
|
2163
|
+
//
|
|
2164
|
+
// Never rejects, and never awaited by the caller: the socket reader must keep
|
|
2165
|
+
// reading, and one stanza that fails must not take the ones queued behind it
|
|
2166
|
+
// with it. What the lock buys is order, not delivery of a result.
|
|
2167
|
+
_queueIncoming(mutex, label, fn) {
|
|
2168
|
+
return mutex.runExclusive(fn).catch(err =>
|
|
2169
|
+
_whaDbg('[DBG] ' + label + '_QUEUE_ERR ' + (err && err.message)));
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2134
2172
|
async _handleMessage(node) {
|
|
2135
2173
|
const attrs = node.attrs || {};
|
|
2136
2174
|
const fromRaw = attrs.from || null;
|
|
@@ -2203,22 +2241,29 @@ class WhalibmobClient extends EventEmitter {
|
|
|
2203
2241
|
}
|
|
2204
2242
|
}
|
|
2205
2243
|
|
|
2244
|
+
// The acknowledgement goes out first and stays outside the queue below.
|
|
2245
|
+
// It tells the server the stanza arrived, and it has to be prompt whatever
|
|
2246
|
+
// the decryption goes on to do — holding it behind a queue of other
|
|
2247
|
+
// people's messages is how a client stops looking like it is listening.
|
|
2206
2248
|
if (skmsgNode && isGroup) {
|
|
2207
2249
|
this._sendMessageAck(id, fromRaw, partRaw);
|
|
2208
2250
|
this._sendDeliveryReceipt(id, fromRaw, partRaw);
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2251
|
+
this._queueIncoming(this._messageMutex, 'MSG', async () => {
|
|
2252
|
+
// SKDM must be processed (awaited) before we attempt skmsg decryption,
|
|
2253
|
+
// because the first message from a sender always bundles both together.
|
|
2254
|
+
if (participantsNode) {
|
|
2255
|
+
await this._processSKDMDistribution(from, participant, participantsNode);
|
|
2256
|
+
}
|
|
2257
|
+
await this._decryptGroupMessage({ node, from, participant, fromRaw, partRaw, id, ts, skmsgNode });
|
|
2258
|
+
});
|
|
2215
2259
|
return;
|
|
2216
2260
|
}
|
|
2217
2261
|
|
|
2218
2262
|
if (dmEncNode && this._signal) {
|
|
2219
2263
|
this._sendMessageAck(id, fromRaw, partRaw);
|
|
2220
2264
|
this._sendDeliveryReceipt(id, fromRaw, partRaw);
|
|
2221
|
-
this.
|
|
2265
|
+
this._queueIncoming(this._messageMutex, 'MSG', () =>
|
|
2266
|
+
this._decryptDMMessage({ node, from, participant, fromRaw, partRaw, senderPn, id, ts, dmEncNode }));
|
|
2222
2267
|
return;
|
|
2223
2268
|
}
|
|
2224
2269
|
|
|
@@ -2335,7 +2380,10 @@ class WhalibmobClient extends EventEmitter {
|
|
|
2335
2380
|
(candidates.length > 1 ? ' alts=' + candidates.slice(1).join(',') : '') +
|
|
2336
2381
|
' cipherLen=' + cipherBuf.length);
|
|
2337
2382
|
|
|
2338
|
-
|
|
2383
|
+
// Returned, not discarded: _handleMessage runs this behind the message
|
|
2384
|
+
// queue, and the queue can only hold the next message back for as long as
|
|
2385
|
+
// it can see this one is still running.
|
|
2386
|
+
return this._decryptWithCandidates(candidates, encType, cipherBuf, id)
|
|
2339
2387
|
.then(async plaintext => {
|
|
2340
2388
|
this._resolveRetry(id);
|
|
2341
2389
|
const decoded = this._decodeMsg(plaintext);
|
|
@@ -2406,7 +2454,8 @@ class WhalibmobClient extends EventEmitter {
|
|
|
2406
2454
|
? skmsgNode.content
|
|
2407
2455
|
: Buffer.from(skmsgNode.content || '');
|
|
2408
2456
|
|
|
2409
|
-
|
|
2457
|
+
// Returned for the same reason as the direct-message path above.
|
|
2458
|
+
return this._signal.senderKeyDecrypt(from, participant, cipherBuf)
|
|
2410
2459
|
.then(plaintext => {
|
|
2411
2460
|
this._resolveRetry(id);
|
|
2412
2461
|
const decoded = this._decodeMsg(plaintext);
|
|
@@ -2555,36 +2604,51 @@ class WhalibmobClient extends EventEmitter {
|
|
|
2555
2604
|
|
|
2556
2605
|
_whaDbg('[DBG] RETRY_RECV msgId=' + msgId + ' from=' + fromStr +
|
|
2557
2606
|
' — clearing session + resending\n');
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
//
|
|
2561
|
-
//
|
|
2562
|
-
//
|
|
2563
|
-
//
|
|
2564
|
-
//
|
|
2565
|
-
//
|
|
2566
|
-
// the
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2607
|
+
|
|
2608
|
+
// Everything above this point stays synchronous on purpose. Every device
|
|
2609
|
+
// that could not read the message sends its own receipt, and the cache
|
|
2610
|
+
// entry taken above is what makes only the first of them resend — put an
|
|
2611
|
+
// await in front of it and they all pass the check together.
|
|
2612
|
+
//
|
|
2613
|
+
// From here on it is one at a time. Tearing a contact's sessions down and
|
|
2614
|
+
// building them back up is not something two receipts can be doing at once:
|
|
2615
|
+
// the second purge lands in the middle of the first resend and deletes the
|
|
2616
|
+
// session it had just created, so the resend goes out under a session the
|
|
2617
|
+
// recipient no longer has and draws another retry.
|
|
2618
|
+
return this._queueIncoming(this._receiptMutex, 'RETRY', async () => {
|
|
2619
|
+
this._purgeContactSessions(recipientPhone, 'RETRY');
|
|
2620
|
+
|
|
2621
|
+
// Re-send the message — will create fresh pre-key (pkmsg) sessions.
|
|
2622
|
+
// A group resend must go back through the group path: it needs a fresh
|
|
2623
|
+
// SenderKey distribution, which only _sendGroupMessage builds. Drop the
|
|
2624
|
+
// "already distributed" record first, or the resend would carry the same
|
|
2625
|
+
// bare skmsg the recipient just told us it could not read.
|
|
2626
|
+
// Carry the round count into the resend so its own retry receipts continue
|
|
2627
|
+
// the same chain instead of restarting it at zero.
|
|
2628
|
+
const opts = Object.assign({}, cached.options, { _retryDepth: depth });
|
|
2629
|
+
|
|
2630
|
+
if (cached.isGroup) {
|
|
2631
|
+
const resendId = generateMessageId();
|
|
2632
|
+
if (this._signal && this._signal.senderKeyStore) {
|
|
2633
|
+
this._signal.senderKeyStore.invalidateSKDM(cached.toJid);
|
|
2634
|
+
}
|
|
2635
|
+
_whaDbg('[DBG] RETRY_RESEND group round=' + depth + ' newId=' + resendId + '\n');
|
|
2636
|
+
// Awaited inside the queue, so the next receipt's purge cannot land
|
|
2637
|
+
// while this resend is still choosing and building sessions.
|
|
2638
|
+
await this._sender._sendGroupMessage(
|
|
2639
|
+
cached.toJid, resendId, cached.plaintext, cached.mediaType, opts
|
|
2640
|
+
)
|
|
2641
|
+
.then(r => _whaDbg('[DBG] RETRY_RESEND group ok id=' + (r && r.id) + '\n'))
|
|
2642
|
+
.catch(e => _whaDbg('[DBG] RETRY_RESEND_ERR: ' + e.message));
|
|
2643
|
+
return;
|
|
2573
2644
|
}
|
|
2574
|
-
|
|
2575
|
-
this._sender.
|
|
2576
|
-
cached.toJid,
|
|
2645
|
+
|
|
2646
|
+
await this._sender._sendDMMessage(
|
|
2647
|
+
cached.toJid, cached.msgId, cached.plaintext, cached.mediaType, opts
|
|
2577
2648
|
)
|
|
2578
|
-
.then(r => _whaDbg('[DBG] RETRY_RESEND
|
|
2649
|
+
.then(r => _whaDbg('[DBG] RETRY_RESEND ok id=' + r.id + '\n'))
|
|
2579
2650
|
.catch(e => _whaDbg('[DBG] RETRY_RESEND_ERR: ' + e.message));
|
|
2580
|
-
|
|
2581
|
-
}
|
|
2582
|
-
|
|
2583
|
-
this._sender._sendDMMessage(
|
|
2584
|
-
cached.toJid, cached.msgId, cached.plaintext, cached.mediaType, opts
|
|
2585
|
-
)
|
|
2586
|
-
.then(r => _whaDbg('[DBG] RETRY_RESEND ok id=' + r.id + '\n'))
|
|
2587
|
-
.catch(e => _whaDbg('[DBG] RETRY_RESEND_ERR: ' + e.message));
|
|
2651
|
+
});
|
|
2588
2652
|
}
|
|
2589
2653
|
|
|
2590
2654
|
_handleAck(node) {
|
|
@@ -3090,9 +3154,13 @@ class WhalibmobClient extends EventEmitter {
|
|
|
3090
3154
|
// cleared, so a LID with no known number simply gets looked up the next
|
|
3091
3155
|
// time something sends to it.
|
|
3092
3156
|
if (this._signal && fromPhone) {
|
|
3093
|
-
|
|
3157
|
+
// Queued rather than fired loose. A contact that changes devices
|
|
3158
|
+
// several times in a row — leaving and rejoining a group does it —
|
|
3159
|
+
// sends one of these per change, and two overlapping warm-ups race
|
|
3160
|
+
// each other's session building for the same contact.
|
|
3161
|
+
this._queueIncoming(this._notificationMutex, 'NOTIF', async () => {
|
|
3094
3162
|
if (!this._connected || !this._devMgr) return;
|
|
3095
|
-
this._devMgr.bulkEnsureSessions([fromPhone], this._signal, false)
|
|
3163
|
+
await this._devMgr.bulkEnsureSessions([fromPhone], this._signal, false)
|
|
3096
3164
|
.then(() => _whaDbg('[DBG] NOTIF_DEVICES bg-usync done for ' + fromPhone))
|
|
3097
3165
|
.catch(e => _whaDbg('[DBG] NOTIF_DEVICES bg-usync err: ' + e.message));
|
|
3098
3166
|
});
|
|
@@ -3490,7 +3558,18 @@ class WhalibmobClient extends EventEmitter {
|
|
|
3490
3558
|
// the signed pre-key it is serving — so nothing here depends on guessing how
|
|
3491
3559
|
// it hashes them. A mismatch means the bundle other people are handed is not
|
|
3492
3560
|
// the one this session can answer for, and no amount of waiting fixes that.
|
|
3561
|
+
//
|
|
3562
|
+
// Shares _preKeyMutex with _ensureServerPreKeys: both end in the same
|
|
3563
|
+
// replenish-and-upload, so protecting only one of them would leave the same
|
|
3564
|
+
// double upload reachable through the other.
|
|
3493
3565
|
async _verifyServerKeyBundle(reason) {
|
|
3566
|
+
if (!this._signal) return;
|
|
3567
|
+
return this._preKeyMutex.runExclusive(() => this._verifyServerKeyBundleLocked(reason));
|
|
3568
|
+
}
|
|
3569
|
+
|
|
3570
|
+
async _verifyServerKeyBundleLocked(reason) {
|
|
3571
|
+
// Re-checked inside the lock: the session can be released while a caller
|
|
3572
|
+
// is still waiting its turn.
|
|
3494
3573
|
if (!this._signal) return;
|
|
3495
3574
|
try {
|
|
3496
3575
|
const digest = await this._queryKeyDigest();
|
|
@@ -3542,7 +3621,14 @@ class WhalibmobClient extends EventEmitter {
|
|
|
3542
3621
|
// Top the server back up when it is running out. Never throws: a session that
|
|
3543
3622
|
// cannot ask is left exactly as it was rather than losing its connection over
|
|
3544
3623
|
// a bookkeeping query.
|
|
3624
|
+
//
|
|
3625
|
+
// Runs one at a time — see _preKeyMutex in the constructor.
|
|
3545
3626
|
async _ensureServerPreKeys(reason) {
|
|
3627
|
+
if (!this._signal) return;
|
|
3628
|
+
return this._preKeyMutex.runExclusive(() => this._ensureServerPreKeysLocked(reason));
|
|
3629
|
+
}
|
|
3630
|
+
|
|
3631
|
+
async _ensureServerPreKeysLocked(reason) {
|
|
3546
3632
|
if (!this._signal) return;
|
|
3547
3633
|
try {
|
|
3548
3634
|
const serverCount = await this._queryServerPreKeyCount();
|