whalibmob 5.14.0 → 5.14.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 +44 -11
- package/lib/Client.js +61 -6
- package/lib/messages/MessageSender.js +8 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -86,8 +86,8 @@ npm install -g whalibmob
|
|
|
86
86
|
- [Pin / Unpin](#pin--unpin)
|
|
87
87
|
- [Archive / Unarchive](#archive--unarchive)
|
|
88
88
|
- [Star / Unstar a Message](#star--unstar-a-message-cli)
|
|
89
|
-
- [Sync App State (CLI)](#
|
|
90
|
-
- [Account Restriction Countdown](#
|
|
89
|
+
- [Sync App State (CLI)](#sync-app-state)
|
|
90
|
+
- [Account Restriction Countdown](#account-restriction-countdown)
|
|
91
91
|
- [Disappearing Messages](#cli-disappearing-messages)
|
|
92
92
|
- [Default Disappearing Timer](#default-disappearing-timer)
|
|
93
93
|
- [Block / Unblock](#block--unblock)
|
|
@@ -109,7 +109,7 @@ npm install -g whalibmob
|
|
|
109
109
|
- [List Group Participants](#list-group-participants)
|
|
110
110
|
- [Pending Join Requests](#pending-join-requests)
|
|
111
111
|
- [Approve / Reject Join Requests](#approve--reject-join-requests)
|
|
112
|
-
- [Personal Invitations (CLI)](#
|
|
112
|
+
- [Personal Invitations (CLI)](#personal-invitations)
|
|
113
113
|
- [Group Settings](#group-settings)
|
|
114
114
|
- [Community Commands](#community-commands-cli)
|
|
115
115
|
- [Newsletter / Channel Commands](#newsletter--channel-commands)
|
|
@@ -127,6 +127,7 @@ npm install -g whalibmob
|
|
|
127
127
|
- [Reading it out of an APK you already have](#reading-it-out-of-an-apk-you-already-have)
|
|
128
128
|
- [Device Attestation with Frida (optional)](#device-attestation-with-frida-optional)
|
|
129
129
|
- [Connect](#connect)
|
|
130
|
+
- [Client Options](#client-options)
|
|
130
131
|
- [Linking to an Existing Account (Pairing Code)](#linking-to-an-existing-account-pairing-code)
|
|
131
132
|
- [Requesting a Pairing Code](#requesting-a-pairing-code)
|
|
132
133
|
- [Reconnecting a Linked Session](#reconnecting-a-linked-session)
|
|
@@ -165,7 +166,7 @@ npm install -g whalibmob
|
|
|
165
166
|
- [Persistent Files Written to Disk](#persistent-files-written-to-disk)
|
|
166
167
|
- [Reading the History Store](#reading-the-history-store)
|
|
167
168
|
- [tcToken — Error 463 Defense](#tctoken--error-463-defense)
|
|
168
|
-
- [When 463 Means the Account Is Restricted](#account-
|
|
169
|
+
- [When 463 Means the Account Is Restricted](#when-463-means-the-account-is-restricted)
|
|
169
170
|
- [What Is Automatic vs What You Need to Do](#what-is-automatic-vs-what-you-need-to-do)
|
|
170
171
|
- [Receiving Media](#receiving-media)
|
|
171
172
|
- [Sending Messages](#sending-messages)
|
|
@@ -200,7 +201,7 @@ npm install -g whalibmob
|
|
|
200
201
|
- [Mark a Chat Read / Unread](#mark-a-chat-read--unread)
|
|
201
202
|
- [Pin / Unpin a Chat](#pin--unpin-a-chat)
|
|
202
203
|
- [Star / Unstar a Message](#star--unstar-a-message)
|
|
203
|
-
- [Reading Changes Made Elsewhere](#
|
|
204
|
+
- [Reading Changes Made Elsewhere](#reading-changes-made-elsewhere)
|
|
204
205
|
- [Disappearing Messages](#disappearing-messages)
|
|
205
206
|
- [User Queries](#user-queries)
|
|
206
207
|
- [Check If a Number Has WhatsApp](#check-if-a-number-has-whatsapp)
|
|
@@ -357,7 +358,7 @@ wa> /reg confirm 919634847671 123456
|
|
|
357
358
|
On success you will see:
|
|
358
359
|
|
|
359
360
|
```
|
|
360
|
-
registered session saved to /home/user/.waSession/919634847671.json
|
|
361
|
+
registered session saved to /home/user/.waSession/919634847671/919634847671.json
|
|
361
362
|
now run: /connect 919634847671
|
|
362
363
|
```
|
|
363
364
|
|
|
@@ -1379,7 +1380,7 @@ wa> /reg code 919634847671 wa_old
|
|
|
1379
1380
|
|
|
1380
1381
|
# confirm the code received
|
|
1381
1382
|
wa> /reg confirm 919634847671 123456
|
|
1382
|
-
registered session saved to /home/user/.waSession/919634847671.json
|
|
1383
|
+
registered session saved to /home/user/.waSession/919634847671/919634847671.json
|
|
1383
1384
|
now run: /connect 919634847671
|
|
1384
1385
|
```
|
|
1385
1386
|
|
|
@@ -1928,6 +1929,37 @@ client.on('connected', () => {
|
|
|
1928
1929
|
await client.init('919634847671')
|
|
1929
1930
|
```
|
|
1930
1931
|
|
|
1932
|
+
#### Client Options
|
|
1933
|
+
|
|
1934
|
+
Every option is optional; `sessionDir` is the only one most senders ever set.
|
|
1935
|
+
|
|
1936
|
+
| Option | Default | What it does |
|
|
1937
|
+
|---|---|---|
|
|
1938
|
+
| `sessionDir` | `~/.waSession` | The authentication folder. Each number gets its own subfolder inside it — see [Saving & Restoring Sessions](#saving--restoring-sessions). |
|
|
1939
|
+
| `autoFixNumber` | `true` | Re-file the session automatically when the server reports the account under a different number. Set `false` to be told instead of fixed — see [The Number WhatsApp Files Your Account Under](#the-number-whatsapp-files-your-account-under). |
|
|
1940
|
+
| `autoRead` | `true` | Send read receipts for incoming messages. `false` leaves them unread. |
|
|
1941
|
+
| `pino` | off | Debug logging. `true` turns it on at `debug` level; an object is passed to `pino` as-is. |
|
|
1942
|
+
| `sentCacheSize` | `2000` | How many sent messages keep their plaintext so a retry receipt naming them can be answered. |
|
|
1943
|
+
| `maxRetryResends` | `5` | How many times one message may be re-sent in answer to retry receipts before the client gives up. |
|
|
1944
|
+
|
|
1945
|
+
```js
|
|
1946
|
+
const client = new WhalibmobClient({
|
|
1947
|
+
sessionDir: path.join(process.env.HOME, '.waSession'),
|
|
1948
|
+
sentCacheSize: 10000,
|
|
1949
|
+
maxRetryResends: 8
|
|
1950
|
+
})
|
|
1951
|
+
```
|
|
1952
|
+
|
|
1953
|
+
**When to raise the last two.** A recipient's device that cannot decrypt a message asks for it again, and whalibmob answers by rebuilding the Signal session and re-sending. Answering requires the original text, which is why sent messages are held: a receipt naming a message no longer in the cache cannot be answered at all, and that message — already acked by the server — silently never arrives.
|
|
1954
|
+
|
|
1955
|
+
The defaults cover an ordinary sender. Raise `sentCacheSize` if you push messages faster than replies come back, which is easy to do when several recipients are being worked through at once: your own replies age out of the cache while the recipient is still asking for them. The symptom is this line in the debug log:
|
|
1956
|
+
|
|
1957
|
+
```
|
|
1958
|
+
[DBG] RETRY_RECV msgId=... — no cached plaintext, skipping resend
|
|
1959
|
+
```
|
|
1960
|
+
|
|
1961
|
+
If you see it, the cache is smaller than your in-flight window. An entry holds the encoded message, not the media it points at, so entries are small and raising the bound costs little memory.
|
|
1962
|
+
|
|
1931
1963
|
## Linking to an Existing Account (Pairing Code)
|
|
1932
1964
|
|
|
1933
1965
|
Registering a number over SMS makes whalibmob that number's **own device**. Sometimes that is not what you want — the number is already in use on a phone, or the verification SMS never arrives. For those cases whalibmob can instead connect over a **WebSocket** and link itself to an account that already exists, exactly the way the WhatsApp Web and desktop clients do.
|
|
@@ -2760,11 +2792,11 @@ connect()
|
|
|
2760
2792
|
| `chat_removed` | `{ jid, kind, remote }` | A chat was cleared or deleted on another device |
|
|
2761
2793
|
| `contact_update` | `{ jid, name, firstName, lid, username, removed, remote }` | A contact was renamed or removed elsewhere |
|
|
2762
2794
|
| `push_name_update` | `{ name, remote }` | Your own display name changed on another device |
|
|
2763
|
-
| `app_state_sync` | `{ collections, applied }` | An app-state sync finished; see [Reading Changes Made Elsewhere](#
|
|
2795
|
+
| `app_state_sync` | `{ collections, applied }` | An app-state sync finished; see [Reading Changes Made Elsewhere](#reading-changes-made-elsewhere) |
|
|
2764
2796
|
| `app_state_mutation` | `{ collection, index, action, removed }` | An app-state change this library does not model |
|
|
2765
2797
|
| `app_state_key_missing` | `{ collection, keyId }` | App state cannot be read until your phone shares this key |
|
|
2766
2798
|
| `app_state_keys` | `{ keys }` | Your phone shared app-state sync keys; a sync starts automatically |
|
|
2767
|
-
| `account_restriction` | `{ active, remaining, remainingMs, endsAtDate, enforcementType, reason, source }` | The account was restricted or the restriction was lifted — see [When 463 Means the Account Is Restricted](#account-
|
|
2799
|
+
| `account_restriction` | `{ active, remaining, remainingMs, endsAtDate, enforcementType, reason, source }` | The account was restricted or the restriction was lifted — see [When 463 Means the Account Is Restricted](#when-463-means-the-account-is-restricted) |
|
|
2768
2800
|
| `mex_notification` | `{ opName, data }` | A server push over `w:mex` this library does not model |
|
|
2769
2801
|
|
|
2770
2802
|
`remote: true` on a chat event means the change was made on your phone or another
|
|
@@ -4729,9 +4761,10 @@ writes.
|
|
|
4729
4761
|
From Node, the same thing:
|
|
4730
4762
|
|
|
4731
4763
|
```js
|
|
4732
|
-
const { refreshSessionVersion, currentVersionFor } = require('whalibmob')
|
|
4764
|
+
const { refreshSessionVersion, currentVersionFor, storeFileFor } = require('whalibmob')
|
|
4733
4765
|
|
|
4734
|
-
|
|
4766
|
+
const base = path.join(process.env.HOME, '.waSession')
|
|
4767
|
+
await refreshSessionVersion(storeFileFor(base, '5568936182750'))
|
|
4735
4768
|
await currentVersionFor({ os: 'android' }) // { version, source }
|
|
4736
4769
|
```
|
|
4737
4770
|
|
package/lib/Client.js
CHANGED
|
@@ -29,7 +29,19 @@ const {
|
|
|
29
29
|
// How many times a message may be re-sent in answer to retry receipts before
|
|
30
30
|
// the client stops. Each resend can itself draw retries, so this is what keeps
|
|
31
31
|
// an undecryptable message from turning into a flood.
|
|
32
|
-
|
|
32
|
+
//
|
|
33
|
+
// Matched to the cap on retries we ask for ourselves (5, in _sendRetryRequest):
|
|
34
|
+
// a device that keeps failing is worth the same number of rounds in either
|
|
35
|
+
// direction. Two rounds gave up while the recipient was still asking.
|
|
36
|
+
const MAX_RETRY_RESENDS = 5;
|
|
37
|
+
|
|
38
|
+
// How many sent messages keep their plaintext for answering a retry receipt.
|
|
39
|
+
// A receipt naming a message no longer held here cannot be answered at all —
|
|
40
|
+
// the message is acked by the server and then silently never arrives. The
|
|
41
|
+
// bound exists for memory, and an entry is small (the encoded Message, not the
|
|
42
|
+
// media it points at), so it is set well above a busy sender's in-flight window
|
|
43
|
+
// rather than at the smallest number that usually works.
|
|
44
|
+
const SENT_CACHE_SIZE = 2000;
|
|
33
45
|
const { NodeCache } = require('@cacheable/node-cache');
|
|
34
46
|
|
|
35
47
|
// Group metadata cache lifetime — matches DeviceManager's device cache.
|
|
@@ -369,6 +381,11 @@ class WhalibmobClient extends EventEmitter {
|
|
|
369
381
|
// the second sender's pkmsg arrives after the key was consumed and deleted.
|
|
370
382
|
this._retryAdvertisedPreKeys = new Set();
|
|
371
383
|
this._sentMsgCache = new Map(); // msgId → {plaintext, toJid, msgId, mediaType, options, recipientPhone}
|
|
384
|
+
// A sender that keeps more messages in flight than the cache holds outruns
|
|
385
|
+
// its own ability to answer retries, so both bounds are open to being
|
|
386
|
+
// raised for that kind of load.
|
|
387
|
+
this._sentCacheSize = Number(opts.sentCacheSize) > 0 ? Number(opts.sentCacheSize) : SENT_CACHE_SIZE;
|
|
388
|
+
this._maxRetryResends = Number(opts.maxRetryResends) > 0 ? Number(opts.maxRetryResends) : MAX_RETRY_RESENDS;
|
|
372
389
|
this._tcTokenStore = null; // TcTokenStore — loaded in init()
|
|
373
390
|
this._inFlightTcTokenIssuance = new Set(); // dedupe concurrent proactive issuePrivacyTokens per JID
|
|
374
391
|
this._inFlight463Recoveries = new Set(); // dedupe concurrent 463-triggered token issuances per JID (separate from proactive)
|
|
@@ -2415,7 +2432,7 @@ class WhalibmobClient extends EventEmitter {
|
|
|
2415
2432
|
// another send and floods the chat. Give up after a few rounds and leave
|
|
2416
2433
|
// the message undelivered rather than keep spamming.
|
|
2417
2434
|
const depth = (cached.retryDepth || 0) + 1;
|
|
2418
|
-
if (depth >
|
|
2435
|
+
if (depth > this._maxRetryResends) {
|
|
2419
2436
|
_whaDbg('[DBG] RETRY_RECV msgId=' + msgId + ' — giving up after ' +
|
|
2420
2437
|
cached.retryDepth + ' resends\n');
|
|
2421
2438
|
return;
|
|
@@ -2432,23 +2449,49 @@ class WhalibmobClient extends EventEmitter {
|
|
|
2432
2449
|
: fromStr;
|
|
2433
2450
|
const recipientPhone = targetJid.split('@')[0].split(':')[0].split('.')[0];
|
|
2434
2451
|
|
|
2435
|
-
|
|
2452
|
+
// The retry names the device under one address family, but a contact holds
|
|
2453
|
+
// a session under each: a Signal address is (user, device) with the server
|
|
2454
|
+
// stripped, and the LID user and the phone user are different strings, so
|
|
2455
|
+
// one session is not reachable under the other's name. Purging only the
|
|
2456
|
+
// family the receipt happened to arrive in left the other one in place —
|
|
2457
|
+
// the resend picked it straight back up and produced the same ciphertext
|
|
2458
|
+
// the device had just said it could not read, so the message stayed
|
|
2459
|
+
// undecryptable until something else rebuilt the session.
|
|
2460
|
+
//
|
|
2461
|
+
// This is what made a reply sent after a companion-device exchange stick on
|
|
2462
|
+
// "Waiting for this message" on the primary: the receipt came in one family,
|
|
2463
|
+
// the stale session sat in the other, and every resend re-used it.
|
|
2464
|
+
const recipientUsers = new Set([recipientPhone]);
|
|
2465
|
+
const mappedPn = this._lidToPn && this._lidToPn.get(recipientPhone);
|
|
2466
|
+
const mappedLid = this._pnToLid && this._pnToLid.get(recipientPhone);
|
|
2467
|
+
if (mappedPn) recipientUsers.add(mappedPn);
|
|
2468
|
+
if (mappedLid) recipientUsers.add(mappedLid);
|
|
2469
|
+
|
|
2470
|
+
_whaDbg('[DBG] RETRY_RECV msgId=' + msgId + ' from=' + fromStr +
|
|
2471
|
+
' users=[' + [...recipientUsers].join(',') + '] — clearing session + resending\n');
|
|
2436
2472
|
|
|
2437
2473
|
// Delete all Signal sessions for the recipient's devices so next send creates fresh pkmsg
|
|
2438
2474
|
const sigStore = this._signal && this._signal.store;
|
|
2439
2475
|
if (sigStore && sigStore._sessions) {
|
|
2440
2476
|
const sessions = sigStore._sessions;
|
|
2441
2477
|
Object.keys(sessions).forEach(addr => {
|
|
2442
|
-
|
|
2478
|
+
// Match on the user segment rather than a bare prefix: "4071.0" must
|
|
2479
|
+
// not be cleared by a retry for "407".
|
|
2480
|
+
const user = addr.slice(0, addr.lastIndexOf('.'));
|
|
2481
|
+
if (recipientUsers.has(user)) {
|
|
2443
2482
|
_whaDbg('[DBG] RETRY deleting session for ' + addr);
|
|
2444
2483
|
delete sessions[addr];
|
|
2445
2484
|
}
|
|
2446
2485
|
});
|
|
2447
2486
|
}
|
|
2448
2487
|
|
|
2449
|
-
// Clear device manager cache so we re-fetch devices on next send
|
|
2488
|
+
// Clear device manager cache so we re-fetch devices on next send. LID device
|
|
2489
|
+
// lists are filed under a "lid:" key, so clearing the bare user only ever
|
|
2490
|
+
// dropped the phone-side list.
|
|
2450
2491
|
if (this._devMgr) {
|
|
2451
|
-
|
|
2492
|
+
const cacheKeys = [];
|
|
2493
|
+
for (const u of recipientUsers) cacheKeys.push(u, 'lid:' + u);
|
|
2494
|
+
this._devMgr.clearCache(cacheKeys);
|
|
2452
2495
|
}
|
|
2453
2496
|
|
|
2454
2497
|
// Re-send the message — will create fresh pre-key (pkmsg) sessions.
|
|
@@ -2697,6 +2740,18 @@ class WhalibmobClient extends EventEmitter {
|
|
|
2697
2740
|
this._retryPending.delete(msgId);
|
|
2698
2741
|
}
|
|
2699
2742
|
|
|
2743
|
+
// Hold a sent message's plaintext so a retry receipt naming it can be
|
|
2744
|
+
// answered. Both send paths file their entry here so the bound lives in one
|
|
2745
|
+
// place instead of being repeated as a literal at each call site.
|
|
2746
|
+
_cacheSentMessage(entry) {
|
|
2747
|
+
if (!this._sentMsgCache || !entry || !entry.msgId) return;
|
|
2748
|
+
this._sentMsgCache.set(entry.msgId, entry);
|
|
2749
|
+
// Map preserves insertion order, so the first key is the oldest entry.
|
|
2750
|
+
while (this._sentMsgCache.size > this._sentCacheSize) {
|
|
2751
|
+
this._sentMsgCache.delete(this._sentMsgCache.keys().next().value);
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2700
2755
|
_sendRetryRequest(msgId, origNode) {
|
|
2701
2756
|
const existing = this._retryPending.get(msgId);
|
|
2702
2757
|
const count = existing ? existing.count + 1 : 1;
|
|
@@ -1091,17 +1091,10 @@ class MessageSender {
|
|
|
1091
1091
|
}
|
|
1092
1092
|
|
|
1093
1093
|
// Cache plaintext so Client can re-send with fresh session on recipient retry
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
});
|
|
1099
|
-
// Keep cache bounded — evict oldest entries beyond 200
|
|
1100
|
-
if (this._client._sentMsgCache.size > 200) {
|
|
1101
|
-
const firstKey = this._client._sentMsgCache.keys().next().value;
|
|
1102
|
-
this._client._sentMsgCache.delete(firstKey);
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1094
|
+
this._client._cacheSentMessage({
|
|
1095
|
+
plaintext, toJid, msgId, mediaType, options, recipientPhone,
|
|
1096
|
+
retryDepth: options._retryDepth || 0
|
|
1097
|
+
});
|
|
1105
1098
|
|
|
1106
1099
|
const dispatchResult = await this._dispatchAndAck(msgNode, msgId);
|
|
1107
1100
|
|
|
@@ -1436,16 +1429,10 @@ class MessageSender {
|
|
|
1436
1429
|
// does, and without this the client had nothing to re-send and dropped it
|
|
1437
1430
|
// with "no cached plaintext" — so the message was acked by the server and
|
|
1438
1431
|
// then silently never arrived.
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
});
|
|
1444
|
-
if (this._client._sentMsgCache.size > 200) {
|
|
1445
|
-
const firstKey = this._client._sentMsgCache.keys().next().value;
|
|
1446
|
-
this._client._sentMsgCache.delete(firstKey);
|
|
1447
|
-
}
|
|
1448
|
-
}
|
|
1432
|
+
this._client._cacheSentMessage({
|
|
1433
|
+
plaintext, toJid: groupJid, msgId, mediaType, options, isGroup: true,
|
|
1434
|
+
retryDepth: options._retryDepth || 0
|
|
1435
|
+
});
|
|
1449
1436
|
|
|
1450
1437
|
const msgNode = new BinaryNode('message', stanzaAttrs, msgContent);
|
|
1451
1438
|
const dispatchResult = await this._dispatchAndAck(msgNode, msgId);
|