whalibmob 5.9.1 → 5.10.0
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 +118 -0
- package/cli.js +95 -0
- package/index.js +3 -0
- package/lib/Client.js +249 -97
- package/lib/DeviceManager.js +25 -26
- package/lib/MediaService.js +2 -2
- package/lib/MediaThumbnail.js +5 -7
- package/lib/ReachoutTimelock.js +133 -0
- package/lib/auth-utils.js +11 -13
- package/lib/messages/MessageSender.js +67 -72
- package/lib/messages/ReportingToken.js +4 -6
- package/lib/messages/TcTokenStore.js +9 -15
- package/lib/noise.js +3 -4
- package/lib/proto/MessageProto.js +4 -4
- package/lib/signal/SignalProtocol.js +4 -4
- package/lib/signal/SignalStore.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -98,6 +98,7 @@ npm install -g whalibmob
|
|
|
98
98
|
- [Archive / Unarchive](#archive--unarchive)
|
|
99
99
|
- [Star / Unstar a Message](#star--unstar-a-message-cli)
|
|
100
100
|
- [Sync App State (CLI)](#cli-app-state)
|
|
101
|
+
- [Account Restriction Countdown](#cli-restriction)
|
|
101
102
|
- [Disappearing Messages](#cli-disappearing-messages)
|
|
102
103
|
- [Default Disappearing Timer](#default-disappearing-timer)
|
|
103
104
|
- [Block / Unblock](#block--unblock)
|
|
@@ -169,6 +170,7 @@ npm install -g whalibmob
|
|
|
169
170
|
- [Persistent Files Written to Disk](#persistent-files-written-to-disk)
|
|
170
171
|
- [Reading the History Store](#reading-the-history-store)
|
|
171
172
|
- [tcToken — Error 463 Defense](#tctoken--error-463-defense)
|
|
173
|
+
- [When 463 Means the Account Is Restricted](#account-restriction)
|
|
172
174
|
- [What Is Automatic vs What You Need to Do](#what-is-automatic-vs-what-you-need-to-do)
|
|
173
175
|
- [Receiving Media](#receiving-media)
|
|
174
176
|
- [Sending Messages](#sending-messages)
|
|
@@ -1113,6 +1115,8 @@ connect()
|
|
|
1113
1115
|
| `app_state_mutation` | `{ collection, index, action, removed }` | An app-state change this library does not model |
|
|
1114
1116
|
| `app_state_key_missing` | `{ collection, keyId }` | App state cannot be read until your phone shares this key |
|
|
1115
1117
|
| `app_state_keys` | `{ keys }` | Your phone shared app-state sync keys; a sync starts automatically |
|
|
1118
|
+
| `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-restriction) |
|
|
1119
|
+
| `mex_notification` | `{ opName, data }` | A server push over `w:mex` this library does not model |
|
|
1116
1120
|
|
|
1117
1121
|
`remote: true` on a chat event means the change was made on your phone or another
|
|
1118
1122
|
linked device rather than by this session. Your own calls carry `synced` instead,
|
|
@@ -1390,6 +1394,61 @@ whalibmob implements the full lifecycle to prevent this:
|
|
|
1390
1394
|
|
|
1391
1395
|
Token storage uses the **LID JID** of the contact (e.g. `112345678901234@lid`) as the key — never the phone-number JID — matching WhatsApp's internal convention.
|
|
1392
1396
|
|
|
1397
|
+
<a id="account-restriction"></a>
|
|
1398
|
+
|
|
1399
|
+
### When 463 Means the Account Is Restricted
|
|
1400
|
+
|
|
1401
|
+
A `463` has two causes that look identical on the wire, and telling them apart matters because only one of them is worth retrying.
|
|
1402
|
+
|
|
1403
|
+
- **No privacy token for that one contact.** The library issues one and re-sends. This is the common case and it resolves itself.
|
|
1404
|
+
- **The account is restricted.** WhatsApp has decided you are starting too many chats with people who never reply, and refuses *all* new conversations until the restriction expires. Existing chats keep working, which is why the account otherwise looks healthy. No retry helps — and sending more counts as further reach-outs, which makes it longer.
|
|
1405
|
+
|
|
1406
|
+
The expiry is never pushed to a client that has not asked. `fetchReachoutTimelock()` is the only way to learn it:
|
|
1407
|
+
|
|
1408
|
+
```js
|
|
1409
|
+
const r = await client.fetchReachoutTimelock()
|
|
1410
|
+
|
|
1411
|
+
if (r.active) {
|
|
1412
|
+
console.log('restricted:', r.reason)
|
|
1413
|
+
console.log('ends at: ', r.endsAtDate.toISOString())
|
|
1414
|
+
console.log('remaining: ', r.remaining) // '04:59:20'
|
|
1415
|
+
} else {
|
|
1416
|
+
console.log('not restricted')
|
|
1417
|
+
}
|
|
1418
|
+
```
|
|
1419
|
+
|
|
1420
|
+
```js
|
|
1421
|
+
{
|
|
1422
|
+
active: true,
|
|
1423
|
+
remaining: '04:59:20', // HH:MM:SS, hours not wrapped at 24
|
|
1424
|
+
remainingMs: 17960000,
|
|
1425
|
+
endsAt: 1800018000000,
|
|
1426
|
+
endsAtDate: Date,
|
|
1427
|
+
enforcementType: 'BIZ_QUALITY',
|
|
1428
|
+
reason: 'too many people you messaged blocked or reported you',
|
|
1429
|
+
expiryUnknown: false, // true when the server withheld the end time
|
|
1430
|
+
checkedAt: 1800000040000
|
|
1431
|
+
}
|
|
1432
|
+
```
|
|
1433
|
+
|
|
1434
|
+
`getReachoutTimelock()` returns the same shape from what was last learned, without asking again — the countdown is recomputed on every call, so it is what a once-a-second display reads.
|
|
1435
|
+
|
|
1436
|
+
**You rarely need to call either.** A send refused with 463 triggers a check on its own (rate-limited to once a minute), and the server pushes an update both when a restriction starts and when it is lifted:
|
|
1437
|
+
|
|
1438
|
+
```js
|
|
1439
|
+
client.on('account_restriction', (r) => {
|
|
1440
|
+
if (r.active) console.log('restricted for', r.remaining, '—', r.reason)
|
|
1441
|
+
else console.log('restriction lifted')
|
|
1442
|
+
})
|
|
1443
|
+
```
|
|
1444
|
+
|
|
1445
|
+
`r.source` is `'notification'` when the server announced it and `'query'` when we asked.
|
|
1446
|
+
|
|
1447
|
+
> [!NOTE]
|
|
1448
|
+
> A first restriction is usually around five hours. Repeats get longer. Nothing
|
|
1449
|
+
> client-side shortens it; the only thing that helps is not sending more
|
|
1450
|
+
> unanswered first messages while it is on.
|
|
1451
|
+
|
|
1393
1452
|
### What Is Automatic vs What You Need to Do
|
|
1394
1453
|
|
|
1395
1454
|
**Everything in the "Automatic" column requires zero code from you.**
|
|
@@ -1412,6 +1471,8 @@ Token storage uses the **LID JID** of the contact (e.g. `112345678901234@lid`) a
|
|
|
1412
1471
|
| Handle incoming `privacy_token` notifications | ✅ | |
|
|
1413
1472
|
| Re-issue tcToken after peer identity change | ✅ | |
|
|
1414
1473
|
| Recover from error 463 with automatic retry | ✅ | |
|
|
1474
|
+
| Check for an account restriction after a 463 | ✅ | Rate-limited to once a minute |
|
|
1475
|
+
| Track restriction start / lift pushed by the server | ✅ | Raised as `account_restriction` |
|
|
1415
1476
|
| Populate in-memory LID↔PN maps | ✅ | |
|
|
1416
1477
|
| Listen to `history_sync` event | 🔵 Optional | Only if your app needs to react to history data |
|
|
1417
1478
|
| Read `<phone>.history.json` | 🔵 Optional | Only if your app needs chat/contact data at rest |
|
|
@@ -3150,6 +3211,60 @@ Changes that arrive on their own are printed as they land:
|
|
|
3150
3211
|
If your phone has not yet shared a sync key with this session, the command says
|
|
3151
3212
|
so — leave WhatsApp open on the phone for a moment and try again.
|
|
3152
3213
|
|
|
3214
|
+
<a id="cli-restriction"></a>
|
|
3215
|
+
|
|
3216
|
+
#### Account Restriction Countdown
|
|
3217
|
+
|
|
3218
|
+
When WhatsApp restricts an account, new chats are refused with error 463 until
|
|
3219
|
+
it expires — usually about five hours the first time. `/restriction` asks the
|
|
3220
|
+
server how long is left and then counts it down, once a second, in place:
|
|
3221
|
+
|
|
3222
|
+
```sh
|
|
3223
|
+
wa> /restriction
|
|
3224
|
+
checking account restriction...
|
|
3225
|
+
──────────────────────────────────────────────────
|
|
3226
|
+
status RESTRICTED
|
|
3227
|
+
reason too many people you messaged blocked or reported you
|
|
3228
|
+
type BIZ_QUALITY
|
|
3229
|
+
ends at 2026-07-28 19:41:12 UTC
|
|
3230
|
+
remaining 04:59:22
|
|
3231
|
+
──────────────────────────────────────────────────
|
|
3232
|
+
New chats with people you have never messaged are refused with
|
|
3233
|
+
error 463 until this expires. Existing conversations keep working,
|
|
3234
|
+
and sending more only makes the restriction longer.
|
|
3235
|
+
|
|
3236
|
+
press any key to stop watching
|
|
3237
|
+
restricted — 04:59:20 remaining
|
|
3238
|
+
```
|
|
3239
|
+
|
|
3240
|
+
The last line rewrites itself every second — `04:59:20`, `04:59:19`, … — and
|
|
3241
|
+
stops on its own the moment the restriction lifts. Any key ends the watch; the
|
|
3242
|
+
restriction is unaffected either way. `/limit` is the same command.
|
|
3243
|
+
|
|
3244
|
+
For the numbers without the countdown:
|
|
3245
|
+
|
|
3246
|
+
```sh
|
|
3247
|
+
wa> /restriction --once
|
|
3248
|
+
```
|
|
3249
|
+
|
|
3250
|
+
An account that is fine says so and returns immediately:
|
|
3251
|
+
|
|
3252
|
+
```sh
|
|
3253
|
+
wa> /restriction
|
|
3254
|
+
checking account restriction...
|
|
3255
|
+
──────────────────────────────────────────────────
|
|
3256
|
+
status not restricted — you can start new chats
|
|
3257
|
+
──────────────────────────────────────────────────
|
|
3258
|
+
```
|
|
3259
|
+
|
|
3260
|
+
You do not have to run it to find out. A refused send checks by itself, and the
|
|
3261
|
+
shell prints the change as it happens:
|
|
3262
|
+
|
|
3263
|
+
```sh
|
|
3264
|
+
ACCOUNT RESTRICTED — too many people you messaged blocked or reported you, 04:59:58 remaining
|
|
3265
|
+
run /restriction to watch the countdown
|
|
3266
|
+
```
|
|
3267
|
+
|
|
3153
3268
|
#### CLI Disappearing Messages
|
|
3154
3269
|
|
|
3155
3270
|
| Duration | Seconds |
|
|
@@ -3647,6 +3762,9 @@ wa> /quit
|
|
|
3647
3762
|
| `/unstar <jid> <msgId> [me]` | Unstar a message |
|
|
3648
3763
|
| `/appstate [collection...]` | Pull pins/archives/mutes/stars from your phone |
|
|
3649
3764
|
| `/appstate --snapshot` | Re-read all app state from scratch |
|
|
3765
|
+
| `/restriction` | Account restriction status with a live countdown |
|
|
3766
|
+
| `/restriction --once` | Restriction status without the countdown |
|
|
3767
|
+
| `/limit` | Alias for `/restriction` |
|
|
3650
3768
|
| `/ephemeral <jid> <seconds>` | Set disappearing messages timer for a chat |
|
|
3651
3769
|
| `/ephemeral-default <seconds>` | Set global default ephemeral timer for new chats |
|
|
3652
3770
|
| `/block <jid>` | Block a contact |
|
package/cli.js
CHANGED
|
@@ -303,6 +303,55 @@ function participantLine(p) {
|
|
|
303
303
|
// A chat setting either reached app state — and so every other device — or it
|
|
304
304
|
// did not, and stayed here. Saying which costs one word and saves the user
|
|
305
305
|
// wondering why their phone did not follow.
|
|
306
|
+
// A live HH:MM:SS countdown, redrawn in place once a second.
|
|
307
|
+
//
|
|
308
|
+
// The restriction's end is a fixed moment, so every tick recomputes from it
|
|
309
|
+
// rather than decrementing a counter — a tick that arrives late, or a laptop
|
|
310
|
+
// that slept, then shows the truth instead of drifting further behind.
|
|
311
|
+
//
|
|
312
|
+
// Returns a promise that settles when the time runs out or the user stops it.
|
|
313
|
+
function countdown(client, initial) {
|
|
314
|
+
return new Promise((resolve) => {
|
|
315
|
+
const tty = process.stdout.isTTY;
|
|
316
|
+
let done = false;
|
|
317
|
+
|
|
318
|
+
const finish = (why) => {
|
|
319
|
+
if (done) return;
|
|
320
|
+
done = true;
|
|
321
|
+
clearInterval(timer);
|
|
322
|
+
process.stdin.removeListener('data', onKey);
|
|
323
|
+
if (tty) process.stdout.write('\n');
|
|
324
|
+
resolve(why);
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const draw = () => {
|
|
328
|
+
const s = client.getReachoutTimelock();
|
|
329
|
+
if (!s.active) {
|
|
330
|
+
if (tty) process.stdout.write('\r\x1b[2K');
|
|
331
|
+
out(' the restriction has been lifted — you can start new chats again');
|
|
332
|
+
return finish('lifted');
|
|
333
|
+
}
|
|
334
|
+
const line = ' restricted — ' + s.remaining + ' remaining' +
|
|
335
|
+
(s.expiryUnknown ? ' (server did not give an end time; re-checking)' : '');
|
|
336
|
+
if (tty) process.stdout.write('\r\x1b[2K' + line);
|
|
337
|
+
else out(line);
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
// Any keypress stops the countdown; the restriction is unaffected either
|
|
341
|
+
// way, so there is nothing to confirm.
|
|
342
|
+
const onKey = () => finish('stopped');
|
|
343
|
+
if (tty && process.stdin.isTTY) {
|
|
344
|
+
process.stdin.resume();
|
|
345
|
+
process.stdin.once('data', onKey);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (tty) out(' press any key to stop watching');
|
|
349
|
+
void initial;
|
|
350
|
+
draw();
|
|
351
|
+
const timer = setInterval(draw, 1000);
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
306
355
|
function chatResult(label, synced) {
|
|
307
356
|
return label + (synced ? '' : ' (this session only — no app state key)');
|
|
308
357
|
}
|
|
@@ -427,6 +476,8 @@ const HELP = `
|
|
|
427
476
|
/unstar <jid> <msgId> [me] unstar a message
|
|
428
477
|
/appstate [collection...] pull pins/archives/mutes/stars from your phone
|
|
429
478
|
/appstate --snapshot re-read all of it from scratch
|
|
479
|
+
/restriction account restriction + live countdown
|
|
480
|
+
/restriction --once just the numbers, no countdown
|
|
430
481
|
/ephemeral <jid> <seconds> set disappearing timer for chat
|
|
431
482
|
/ephemeral-default <seconds> set default timer for ALL new chats
|
|
432
483
|
/block <jid> block contact
|
|
@@ -600,6 +651,20 @@ function attachEvents(client) {
|
|
|
600
651
|
_rl && (_rl.resume(), _rl.prompt(true));
|
|
601
652
|
});
|
|
602
653
|
|
|
654
|
+
// The server announces a restriction starting and being lifted, so a session
|
|
655
|
+
// that is just sitting there still finds out.
|
|
656
|
+
client.on('account_restriction', (r) => {
|
|
657
|
+
if (r.source !== 'notification') return;
|
|
658
|
+
_rl && _rl.pause();
|
|
659
|
+
if (r.active) {
|
|
660
|
+
out(' ACCOUNT RESTRICTED — ' + r.reason + ', ' + r.remaining + ' remaining');
|
|
661
|
+
out(' run /restriction to watch the countdown');
|
|
662
|
+
} else {
|
|
663
|
+
out(' account restriction lifted — you can start new chats again');
|
|
664
|
+
}
|
|
665
|
+
_rl && (_rl.resume(), _rl.prompt(true));
|
|
666
|
+
});
|
|
667
|
+
|
|
603
668
|
client.on('app_state_keys', (e) => {
|
|
604
669
|
_rl && _rl.pause();
|
|
605
670
|
out(' app state: received ' + e.keys.length + ' sync key(s) from your phone —' +
|
|
@@ -1412,6 +1477,36 @@ async function handleLine(line) {
|
|
|
1412
1477
|
out('marked read');
|
|
1413
1478
|
break;
|
|
1414
1479
|
|
|
1480
|
+
case '/restriction':
|
|
1481
|
+
case '/limit': {
|
|
1482
|
+
requireConn();
|
|
1483
|
+
const watch = !p.includes('--once');
|
|
1484
|
+
out('checking account restriction...');
|
|
1485
|
+
const st = await _client.fetchReachoutTimelock();
|
|
1486
|
+
hr();
|
|
1487
|
+
if (!st.active) {
|
|
1488
|
+
kv('status', 'not restricted — you can start new chats');
|
|
1489
|
+
hr();
|
|
1490
|
+
break;
|
|
1491
|
+
}
|
|
1492
|
+
kv('status', 'RESTRICTED');
|
|
1493
|
+
kv('reason', st.reason);
|
|
1494
|
+
kv('type', st.enforcementType);
|
|
1495
|
+
kv('ends at', st.endsAtDate ? st.endsAtDate.toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC') : 'unknown');
|
|
1496
|
+
kv('remaining', st.remaining);
|
|
1497
|
+
hr();
|
|
1498
|
+
out(' New chats with people you have never messaged are refused with');
|
|
1499
|
+
out(' error 463 until this expires. Existing conversations keep working,');
|
|
1500
|
+
out(' and sending more only makes the restriction longer.');
|
|
1501
|
+
out('');
|
|
1502
|
+
if (watch) {
|
|
1503
|
+
_rl && _rl.pause();
|
|
1504
|
+
await countdown(_client, st);
|
|
1505
|
+
_rl && _rl.resume();
|
|
1506
|
+
}
|
|
1507
|
+
break;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1415
1510
|
case '/appstate': {
|
|
1416
1511
|
requireConn();
|
|
1417
1512
|
const snapshot = p.includes('--snapshot');
|
package/index.js
CHANGED
|
@@ -13,6 +13,7 @@ const { encryptMedia, decryptMedia, uploadMedia, downloadMedia } = require('./li
|
|
|
13
13
|
const { MessageSender, makeJid, generateMessageId } = require('./lib/messages/MessageSender');
|
|
14
14
|
const { GroupParticipantResult, GroupJoinRequest } = require('./lib/GroupParticipant');
|
|
15
15
|
const { AppStateStore, COLLECTIONS: APP_STATE_COLLECTIONS } = require('./lib/appstate/AppStateStore');
|
|
16
|
+
const ReachoutTimelock = require('./lib/ReachoutTimelock');
|
|
16
17
|
const AppStateSync = require('./lib/appstate/AppStateSync');
|
|
17
18
|
const { PATCH_INTEGRITY: LT_HASH } = require('./lib/appstate/LTHash');
|
|
18
19
|
const {
|
|
@@ -110,6 +111,8 @@ module.exports = {
|
|
|
110
111
|
AppStateSync,
|
|
111
112
|
APP_STATE_COLLECTIONS,
|
|
112
113
|
LT_HASH,
|
|
114
|
+
// Account restriction
|
|
115
|
+
ReachoutTimelock,
|
|
113
116
|
// Auth utilities
|
|
114
117
|
makeCacheableSignalKeyStore,
|
|
115
118
|
addTransactionCapability,
|