whalibmob 5.5.93 → 5.6.3
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 +483 -2
- package/cli.js +169 -6
- package/index.js +30 -0
- package/lib/Client.js +553 -13
- 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/WebVersion.js +78 -0
- package/lib/constants.js +29 -0
- package/lib/messages/MessageSender.js +26 -7
- package/lib/noise.js +72 -7
- package/lib/signal/SignalProtocol.js +14 -5
- package/lib/webproto.js +275 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -36,7 +36,8 @@ Usdc ethereum network.
|
|
|
36
36
|
> This project is not affiliated, associated, authorized, endorsed by, or in any way officially connected with WhatsApp or any of its subsidiaries or affiliates. "WhatsApp" and related names are registered trademarks of their respective owners. Use at your own discretion.
|
|
37
37
|
|
|
38
38
|
- whalibmob does not require a browser, Selenium, or any other external runtime — it communicates directly with WhatsApp using a **TCP socket** and the **Noise Protocol** handshake.
|
|
39
|
-
- The library operates as a real **iOS mobile device**,
|
|
39
|
+
- The library operates as a real **iOS mobile device**, using the Mobile API endpoint, which behaves differently from the Web API.
|
|
40
|
+
- It **also speaks WhatsApp Web over a WebSocket**. When a number cannot receive an SMS, or is already in use on a phone, whalibmob can link itself to that existing account with an **8-character pairing code** and run as one of its linked devices — with full message history and the account's address book. See [Linking to an Existing Account](#linking-to-an-existing-account-pairing-code). The API is identical in both modes.
|
|
40
41
|
- Signal Protocol encryption is **fully inlined** in pure JavaScript — no native binaries, no node-gyp, runs anywhere Node.js runs.
|
|
41
42
|
|
|
42
43
|
## Install
|
|
@@ -57,6 +58,7 @@ npm install -g whalibmob
|
|
|
57
58
|
- [Install the CLI](#install-the-cli)
|
|
58
59
|
- [First-Time Setup: Register a Number](#first-time-setup-register-a-number)
|
|
59
60
|
- [Connect](#cli-connect)
|
|
61
|
+
- [Pairing Code](#cli-pairing-code)
|
|
60
62
|
- [Listen Mode](#listen-mode)
|
|
61
63
|
- [CLI — Interactive Shell Commands](#cli--interactive-shell-commands)
|
|
62
64
|
- [Messaging Commands](#messaging-commands)
|
|
@@ -128,6 +130,26 @@ npm install -g whalibmob
|
|
|
128
130
|
- [Register a New Number](#register-a-new-number)
|
|
129
131
|
- [Device Attestation with Frida (optional)](#device-attestation-with-frida-optional)
|
|
130
132
|
- [Connect](#connect)
|
|
133
|
+
- [Linking to an Existing Account (Pairing Code)](#linking-to-an-existing-account-pairing-code)
|
|
134
|
+
- [Requesting a Pairing Code](#requesting-a-pairing-code)
|
|
135
|
+
- [Reconnecting a Linked Session](#reconnecting-a-linked-session)
|
|
136
|
+
- [Choosing Your Own Code](#choosing-your-own-code)
|
|
137
|
+
- [Options](#options)
|
|
138
|
+
- [Events Specific to Linking](#events-specific-to-linking)
|
|
139
|
+
- [Getting the History and the Address Book](#getting-the-history-and-the-address-book)
|
|
140
|
+
- [Session Files](#session-files)
|
|
141
|
+
- [Media in Companion Mode](#media-in-companion-mode)
|
|
142
|
+
- [Device Identity](#device-identity)
|
|
143
|
+
- [How the Code Protects the Link](#how-the-code-protects-the-link)
|
|
144
|
+
- [Companion Mode — Node.js API](#companion-mode--nodejs-api)
|
|
145
|
+
- [Complete Working Example](#complete-working-example)
|
|
146
|
+
- [Methods](#methods)
|
|
147
|
+
- [Events](#events)
|
|
148
|
+
- [Reading What the Phone Sent](#reading-what-the-phone-sent)
|
|
149
|
+
- [Sending](#sending)
|
|
150
|
+
- [Handling Reconnects](#handling-reconnects)
|
|
151
|
+
- [Knowing Which Mode You Are In](#knowing-which-mode-you-are-in)
|
|
152
|
+
- [Two Sessions on One Number](#two-sessions-on-one-number)
|
|
131
153
|
- [Saving & Restoring Sessions](#saving--restoring-sessions)
|
|
132
154
|
- [Signal Store Utilities](#signal-store-utilities)
|
|
133
155
|
- [makeCacheableSignalKeyStore](#makecacheablesignalkeystore)
|
|
@@ -400,6 +422,390 @@ client.on('connected', () => {
|
|
|
400
422
|
await client.init('919634847671')
|
|
401
423
|
```
|
|
402
424
|
|
|
425
|
+
## Linking to an Existing Account (Pairing Code)
|
|
426
|
+
|
|
427
|
+
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.
|
|
428
|
+
|
|
429
|
+
You get an 8-character pairing code, the account owner types it into their phone, and from then on whalibmob is one of the account's linked devices. The whole library works the same afterwards — same client, same methods, same events.
|
|
430
|
+
|
|
431
|
+
> [!IMPORTANT]
|
|
432
|
+
> The two modes are independent. SMS registration is unchanged and still the default; nothing about it is affected by linking. A single number can even have both a registered session and a linked session — they are stored in separate files and never share state.
|
|
433
|
+
|
|
434
|
+
### Requesting a Pairing Code
|
|
435
|
+
|
|
436
|
+
Connect first, then ask for the code: the request travels over the encrypted channel, so the channel has to exist before there can be a code.
|
|
437
|
+
|
|
438
|
+
```js
|
|
439
|
+
const { WhalibmobClient } = require('whalibmob')
|
|
440
|
+
const path = require('path')
|
|
441
|
+
|
|
442
|
+
const client = new WhalibmobClient({
|
|
443
|
+
sessionDir: path.join(process.env.HOME, '.waSession')
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
client.on('paired', (p) => {
|
|
447
|
+
console.log('linked as', p.jid) // 919634847671:7@s.whatsapp.net
|
|
448
|
+
console.log('lid ', p.lid) // 112713111982325:7@lid
|
|
449
|
+
console.log('slot ', p.deviceIndex) // 7
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
client.on('connected', () => {
|
|
453
|
+
console.log('ready')
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
// open the WebSocket connection as a companion
|
|
457
|
+
await client.connectWeb('919634847671', { syncFullHistory: true })
|
|
458
|
+
|
|
459
|
+
// ask for the code — returns immediately, the link completes later
|
|
460
|
+
const code = await client.requestPairingCode('919634847671')
|
|
461
|
+
console.log('enter this on the phone:', code) // e.g. "K7M2QX4B"
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
On the phone that owns the number:
|
|
465
|
+
|
|
466
|
+
**WhatsApp → Settings → Linked Devices → Link a device → Link with phone number instead**, then type the code.
|
|
467
|
+
|
|
468
|
+
A few seconds after the code is accepted you will see `paired`, the server restarts the stream, and `connected` fires on the new connection. From that point on everything else in this document applies unchanged:
|
|
469
|
+
|
|
470
|
+
```js
|
|
471
|
+
await client.sendText('919876543210@s.whatsapp.net', 'sent from a linked device')
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
### Reconnecting a Linked Session
|
|
475
|
+
|
|
476
|
+
The link is persisted. On every later run, `connectWeb()` alone is enough — do **not** request a new code:
|
|
477
|
+
|
|
478
|
+
```js
|
|
479
|
+
const client = new WhalibmobClient({ sessionDir })
|
|
480
|
+
|
|
481
|
+
client.on('connected', () => console.log('reconnected'))
|
|
482
|
+
|
|
483
|
+
await client.connectWeb('919634847671')
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
`requestPairingCode()` throws if the session is already linked, so it is safe to guard on that.
|
|
487
|
+
|
|
488
|
+
### Choosing Your Own Code
|
|
489
|
+
|
|
490
|
+
If you would rather show the user a code you picked, pass it as the second argument. It must be exactly 8 characters:
|
|
491
|
+
|
|
492
|
+
```js
|
|
493
|
+
const code = await client.requestPairingCode('919634847671', 'MYCODE12')
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
### Options
|
|
497
|
+
|
|
498
|
+
```js
|
|
499
|
+
await client.connectWeb(phone, {
|
|
500
|
+
syncFullHistory: true, // ask the phone for full history (default true)
|
|
501
|
+
browser: ['Ubuntu', 'Chrome', '120.0.0.0'] // what the owner sees in Linked Devices
|
|
502
|
+
})
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
`browser` is `[os, client, version]`. The second element decides the device icon shown on the phone — `Chrome`, `Firefox`, `Safari`, `Edge`, `Opera`, `Desktop` are all recognised.
|
|
506
|
+
|
|
507
|
+
### Events Specific to Linking
|
|
508
|
+
|
|
509
|
+
| Event | Fires when |
|
|
510
|
+
|---|---|
|
|
511
|
+
| `pairing_code` | a code has been requested — `{ code, phoneNumber }` |
|
|
512
|
+
| `paired` | the owner accepted the code — `{ jid, lid, deviceIndex, platform }` |
|
|
513
|
+
| `restart_required` | the server is restarting the stream after pairing (normal; the reconnect is automatic) |
|
|
514
|
+
| `pair_device` | the QR path produced reference strings — `{ refs }` |
|
|
515
|
+
| `history_sync` | a chunk of history arrived from the phone |
|
|
516
|
+
|
|
517
|
+
### Getting the History and the Address Book
|
|
518
|
+
|
|
519
|
+
This is the part a registered number can never do. A device registered over SMS **is** the account's primary, and a primary has nobody to receive history from — it starts with an empty contact list and an empty chat list, and only learns about people who message it.
|
|
520
|
+
|
|
521
|
+
A linked device is different: the account's phone ships its chats, its contacts and its push names over as soon as the link is established. whalibmob decrypts and stores those automatically, and emits them as they arrive:
|
|
522
|
+
|
|
523
|
+
```js
|
|
524
|
+
client.on('history_sync', (r) => {
|
|
525
|
+
console.log(r.syncTypeName) // INITIAL_BOOTSTRAP, RECENT, FULL, PUSH_NAME
|
|
526
|
+
console.log('chats ', r.chats.length)
|
|
527
|
+
console.log('contacts', r.contacts.length)
|
|
528
|
+
|
|
529
|
+
for (const c of r.contacts.slice(0, 5)) {
|
|
530
|
+
console.log(c.jid, c.name || c.notify)
|
|
531
|
+
}
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
await client.connectWeb(phone, { syncFullHistory: true })
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
History arrives in chunks over the first minute or so after linking, largest first. Everything is merged into `<phone>.web.history.json` in your session directory, so it survives restarts and you can read it directly.
|
|
538
|
+
|
|
539
|
+
> [!NOTE]
|
|
540
|
+
> `syncFullHistory: false` asks only for recent messages, which links noticeably faster on accounts with years of history.
|
|
541
|
+
|
|
542
|
+
### Session Files
|
|
543
|
+
|
|
544
|
+
| File | Holds |
|
|
545
|
+
|---|---|
|
|
546
|
+
| `<phone>.web.json` | link state — keys, `advSecretKey`, the device slot you were given |
|
|
547
|
+
| `<phone>.web.signal.json` | Signal sessions and pre-keys for the linked device |
|
|
548
|
+
| `<phone>.web.sk.json` | group SenderKeys |
|
|
549
|
+
| `<phone>.web.tctoken.json` | privacy tokens |
|
|
550
|
+
| `<phone>.web.history.json` | synced chats, contacts, push names, LID↔PN mappings |
|
|
551
|
+
| `<phone>.web.messages.json` | flat map of message id → message metadata |
|
|
552
|
+
|
|
553
|
+
The `.web.` prefix keeps a linked session entirely separate from an SMS-registered one for the same number.
|
|
554
|
+
|
|
555
|
+
### Media in Companion Mode
|
|
556
|
+
|
|
557
|
+
Uploads and downloads go to the CDN endpoints the web client uses, not the mobile ones. This is handled for you — `sendImage`, `sendVideo`, `sendSticker` and the rest take the same arguments in both modes — but it is worth knowing why the distinction exists.
|
|
558
|
+
|
|
559
|
+
The web client has no endpoint per media type. A sticker is uploaded to the image endpoint and a GIF to the video one; what makes them a sticker or a GIF lives in the message itself, not in the URL. Uploading a sticker to `/mms/sticker` as a companion gets a 404. The CDN also expects a browser `Origin` on both the upload and the download, and a mobile WhatsApp user agent against a web-issued auth token is a mismatch it can reject.
|
|
560
|
+
|
|
561
|
+
| Media | Primary endpoint | Companion endpoint |
|
|
562
|
+
|---|---|---|
|
|
563
|
+
| image | `/mms/image` | `/mms/image` |
|
|
564
|
+
| video | `/mms/video` | `/mms/video` |
|
|
565
|
+
| audio | `/mms/audio` | `/mms/audio` |
|
|
566
|
+
| document | `/mms/document` | `/mms/document` |
|
|
567
|
+
| sticker | `/mms/sticker` | `/mms/image` |
|
|
568
|
+
| gif | `/mms/gif` | `/mms/video` |
|
|
569
|
+
| voice note | `/mms/ptt` | `/mms/audio` |
|
|
570
|
+
|
|
571
|
+
The same applies to history sync blobs and profile pictures.
|
|
572
|
+
|
|
573
|
+
### Device Identity
|
|
574
|
+
|
|
575
|
+
Every message a companion sends that opens a new Signal session carries a `device-identity` node: the signed record the account's primary device issued during pairing, including the account signature key. That is how the recipient's client knows a message from device 7 of an account genuinely belongs to that account rather than to someone who merely knows the number.
|
|
576
|
+
|
|
577
|
+
whalibmob attaches it automatically whenever a `pkmsg` is in the stanza, in direct messages and in groups alike. A device registered over SMS has no such record — nothing issued one to it — and correctly sends nothing.
|
|
578
|
+
|
|
579
|
+
### How the Code Protects the Link
|
|
580
|
+
|
|
581
|
+
The pairing code is a password, not an identifier. It is never sent to the server in the clear. Both sides run it through PBKDF2-SHA256 (131,072 iterations) to derive a key that wraps the ephemeral public keys they exchange, then combine two Diffie-Hellman results into `adv_secret` — the key every later proof of account membership is authenticated under.
|
|
582
|
+
|
|
583
|
+
whalibmob verifies three things before accepting a link, and refuses it outright if any fails: the HMAC over the signed device identity, the account signature over its own identity key, and the device slot it was issued. A wrong code cannot produce a working link, and a tampered response cannot either.
|
|
584
|
+
|
|
585
|
+
## Companion Mode — Node.js API
|
|
586
|
+
|
|
587
|
+
Everything below is the full surface for running whalibmob as a linked device. If you have used the SMS primary API, none of it will surprise you: the client is the same class, the methods take the same arguments, and the events carry the same shapes. Only the way in differs.
|
|
588
|
+
|
|
589
|
+
### Complete Working Example
|
|
590
|
+
|
|
591
|
+
A bot that links itself on first run, reconnects silently on every run after that, and replies to messages.
|
|
592
|
+
|
|
593
|
+
```js
|
|
594
|
+
const { WhalibmobClient } = require('whalibmob')
|
|
595
|
+
const path = require('path')
|
|
596
|
+
const fs = require('fs')
|
|
597
|
+
|
|
598
|
+
const PHONE = '919634847671' // no '+', no spaces
|
|
599
|
+
const SESS_DIR = path.join(process.env.HOME, '.waSession')
|
|
600
|
+
|
|
601
|
+
// A session is already linked when this file exists and carries a device JID.
|
|
602
|
+
function isLinked() {
|
|
603
|
+
const f = path.join(SESS_DIR, PHONE + '.web.json')
|
|
604
|
+
if (!fs.existsSync(f)) return false
|
|
605
|
+
try {
|
|
606
|
+
const j = JSON.parse(fs.readFileSync(f, 'utf8'))
|
|
607
|
+
return !!(j.registered && j.me && j.me.id)
|
|
608
|
+
} catch { return false }
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
async function start() {
|
|
612
|
+
const client = new WhalibmobClient({ sessionDir: SESS_DIR })
|
|
613
|
+
|
|
614
|
+
client.on('pairing_code', ({ code }) => {
|
|
615
|
+
console.log('\n pairing code:', code.slice(0, 4) + '-' + code.slice(4))
|
|
616
|
+
console.log(' phone → Settings → Linked Devices → Link a device')
|
|
617
|
+
console.log(' → Link with phone number instead\n')
|
|
618
|
+
})
|
|
619
|
+
|
|
620
|
+
client.on('paired', ({ jid, lid, deviceIndex, platform }) => {
|
|
621
|
+
console.log('linked as', jid, '· slot', deviceIndex, '· primary is', platform)
|
|
622
|
+
})
|
|
623
|
+
|
|
624
|
+
// The server restarts the stream right after pairing. This is normal and the
|
|
625
|
+
// reconnect is automatic — there is nothing to do but log it.
|
|
626
|
+
client.on('restart_required', () => console.log('restarting stream...'))
|
|
627
|
+
|
|
628
|
+
client.on('connected', () => console.log('connected'))
|
|
629
|
+
|
|
630
|
+
client.on('history_sync', (r) => {
|
|
631
|
+
console.log(`history ${r.syncTypeName}: ${r.chats.length} chats, ${r.contacts.length} contacts`)
|
|
632
|
+
})
|
|
633
|
+
|
|
634
|
+
client.on('message', async (msg) => {
|
|
635
|
+
const d = msg.decoded
|
|
636
|
+
if (!d) return
|
|
637
|
+
console.log(msg.from, d.type, d.text || '')
|
|
638
|
+
|
|
639
|
+
if (d.type === 'text' && d.text === 'ping') {
|
|
640
|
+
await client.sendText(msg.from, 'pong')
|
|
641
|
+
}
|
|
642
|
+
})
|
|
643
|
+
|
|
644
|
+
client.on('error', (e) => console.error('error:', e.message))
|
|
645
|
+
|
|
646
|
+
await client.connectWeb(PHONE, { syncFullHistory: true })
|
|
647
|
+
|
|
648
|
+
if (!isLinked()) {
|
|
649
|
+
await client.requestPairingCode(PHONE)
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
start()
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
Run it once, type the code into the phone, and it is linked. Run it again and it connects straight away.
|
|
657
|
+
|
|
658
|
+
### Methods
|
|
659
|
+
|
|
660
|
+
| Method | Description |
|
|
661
|
+
|---|---|
|
|
662
|
+
| `client.connectWeb(phone, opts?)` | Open the companion connection. Resolves as soon as the encrypted channel is up when unlinked, or after `<success>` when already linked. |
|
|
663
|
+
| `client.requestPairingCode(phone?, customCode?)` | Ask for an 8-character code. Returns it immediately; the link completes later. Throws if the session is already linked. |
|
|
664
|
+
| `client.disconnect()` | Close the connection. The link survives — reconnect with `connectWeb()`. |
|
|
665
|
+
|
|
666
|
+
`connectWeb(phone, opts)` options:
|
|
667
|
+
|
|
668
|
+
| Option | Default | Description |
|
|
669
|
+
|---|---|---|
|
|
670
|
+
| `syncFullHistory` | `true` | Ask the phone for the full archive. `false` requests recent messages only and links noticeably faster. |
|
|
671
|
+
| `browser` | `['Ubuntu', 'Chrome', '120.0.0.0']` | `[os, client, version]`. The second element picks the icon shown under Linked Devices: `Chrome`, `Firefox`, `Safari`, `Edge`, `Opera`, `Desktop`. |
|
|
672
|
+
| `version` | fetched live | Web client revision to announce, e.g. `[2, 3000, 1035194821]`. Set it to pin one. |
|
|
673
|
+
| `fetchVersion` | `true` | Set `false` to skip the live lookup and use the pinned fallback. |
|
|
674
|
+
|
|
675
|
+
#### The Announced Version
|
|
676
|
+
|
|
677
|
+
The web endpoint checks the client revision during the handshake and refuses an unrecognised one with `<failure reason="405">` — before any stanza is exchanged, and with nothing in the failure to say the version was the problem. The mobile endpoint is far more forgiving; this one is not.
|
|
678
|
+
|
|
679
|
+
whalibmob therefore reads the live revision from WhatsApp Web's own service worker on each `connectWeb()`, and falls back to a pinned value if that lookup fails. You should not have to think about it, but you can:
|
|
680
|
+
|
|
681
|
+
```js
|
|
682
|
+
const { fetchWaWebVersion } = require('whalibmob')
|
|
683
|
+
|
|
684
|
+
const { version, isLatest } = await fetchWaWebVersion()
|
|
685
|
+
console.log(version, isLatest) // [2, 3000, 1035194821] true
|
|
686
|
+
|
|
687
|
+
// pin it yourself
|
|
688
|
+
await client.connectWeb(phone, { version: [2, 3000, 1035194821] })
|
|
689
|
+
|
|
690
|
+
// or skip the lookup entirely
|
|
691
|
+
await client.connectWeb(phone, { fetchVersion: false })
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
If you ever see `405`, this is what it means. It is not a revoked session and there is nothing to re-pair — reconnect to pick up the current revision.
|
|
695
|
+
|
|
696
|
+
`requestPairingCode(phone, customCode)`:
|
|
697
|
+
|
|
698
|
+
- `phone` — optional; defaults to the number given to `connectWeb`.
|
|
699
|
+
- `customCode` — optional; must be **exactly 8 characters** or it throws. Use it to show a code you generated yourself.
|
|
700
|
+
|
|
701
|
+
### Events
|
|
702
|
+
|
|
703
|
+
Every event from the SMS primary API fires here too — `message`, `receipt`, `presence`, `group_update`, `call`, `blocklist`, `privacy_settings`, and the rest. These are the ones only companion mode produces:
|
|
704
|
+
|
|
705
|
+
| Event | Payload | Fires when |
|
|
706
|
+
|---|---|---|
|
|
707
|
+
| `pairing_code` | `{ code, phoneNumber }` | a code was requested |
|
|
708
|
+
| `paired` | `{ jid, lid, deviceIndex, platform }` | the owner accepted the code |
|
|
709
|
+
| `restart_required` | `{ reason }` | the server is restarting the stream after pairing — the reconnect is automatic |
|
|
710
|
+
| `pair_device` | `{ refs }` | the QR path produced reference strings |
|
|
711
|
+
| `history_sync` | `{ syncTypeName, chats, contacts, pushNames, merged }` | a chunk of history arrived |
|
|
712
|
+
| `history_sync_error` | `{ err, notification }` | a chunk could not be fetched or decrypted |
|
|
713
|
+
| `client_rejected` | `{ reason, location, message }` | the server refused the client itself, not the session — `405` means the announced version is not accepted. Distinct from `auth_failure`, and there is nothing to re-pair. |
|
|
714
|
+
|
|
715
|
+
### Reading What the Phone Sent
|
|
716
|
+
|
|
717
|
+
```js
|
|
718
|
+
client.on('history_sync', (r) => {
|
|
719
|
+
// r.syncTypeName — INITIAL_BOOTSTRAP | RECENT | FULL | PUSH_NAME | ...
|
|
720
|
+
for (const chat of r.chats) {
|
|
721
|
+
// { id, name, unreadCount, lastMsgTimestamp, messageCount }
|
|
722
|
+
console.log(chat.id, chat.name, chat.unreadCount, chat.messageCount)
|
|
723
|
+
}
|
|
724
|
+
for (const contact of r.contacts) {
|
|
725
|
+
// { id, name, username, pnJid, lidJid }
|
|
726
|
+
console.log(contact.id, contact.name, contact.pnJid, contact.lidJid)
|
|
727
|
+
}
|
|
728
|
+
for (const p of r.pushNames || []) {
|
|
729
|
+
console.log(p.id, p.pushname)
|
|
730
|
+
}
|
|
731
|
+
})
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
Chunks arrive over the first minute or so after linking, largest first. Everything is merged into `<phone>.web.history.json`, so you can also read it straight off disk once and skip the event:
|
|
735
|
+
|
|
736
|
+
```js
|
|
737
|
+
const hist = JSON.parse(
|
|
738
|
+
fs.readFileSync(path.join(SESS_DIR, PHONE + '.web.history.json'), 'utf8')
|
|
739
|
+
)
|
|
740
|
+
console.log(Object.keys(hist.chats || {}).length, 'chats on disk')
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
### Sending
|
|
744
|
+
|
|
745
|
+
Identical to primary mode — same methods, same arguments, same return shapes:
|
|
746
|
+
|
|
747
|
+
```js
|
|
748
|
+
await client.sendText(jid, 'hello')
|
|
749
|
+
await client.sendImage(jid, './photo.jpg', 'a caption')
|
|
750
|
+
await client.sendVideo(jid, './clip.mp4', 'watch this')
|
|
751
|
+
await client.sendAudio(jid, './voice.ogg', { ptt: true })
|
|
752
|
+
await client.sendDocument(jid, './report.pdf', 'report.pdf')
|
|
753
|
+
await client.sendSticker(jid, './sticker.webp')
|
|
754
|
+
await client.sendLocation(jid, 44.4268, 26.1025, 'Bucharest')
|
|
755
|
+
await client.sendContact(jid, 'Ana', vcard)
|
|
756
|
+
await client.sendPoll(jid, 'Lunch?', ['Pizza', 'Sushi'], 1)
|
|
757
|
+
await client.sendReaction(jid, msgId, '👍')
|
|
758
|
+
await client.sendStatus({ image: './photo.jpg', caption: 'hi' })
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
Full signatures for each of these are in [Sending Messages](#sending-messages) and [Media Messages](#media-messages); nothing about them changes in companion mode.
|
|
762
|
+
|
|
763
|
+
Groups, blocking, privacy settings and profile changes work the same way too.
|
|
764
|
+
|
|
765
|
+
> [!NOTE]
|
|
766
|
+
> Media uploads go to the endpoints the web client uses, which are not the same as the mobile ones for stickers, GIFs and voice notes. whalibmob switches automatically — see [Media in Companion Mode](#media-in-companion-mode).
|
|
767
|
+
|
|
768
|
+
### Handling Reconnects
|
|
769
|
+
|
|
770
|
+
The library reconnects on its own with backoff. What you should handle is the difference between a transient drop and a revoked link:
|
|
771
|
+
|
|
772
|
+
```js
|
|
773
|
+
client.on('disconnected', () => console.log('dropped'))
|
|
774
|
+
client.on('reconnecting', ({ delay }) => console.log('retry in', delay / 1000, 's'))
|
|
775
|
+
client.on('reconnected', () => console.log('back'))
|
|
776
|
+
|
|
777
|
+
// The owner removed this device under Linked Devices. The session is dead —
|
|
778
|
+
// delete it and pair again.
|
|
779
|
+
client.on('auth_failure', ({ reason }) => {
|
|
780
|
+
console.error('link revoked:', reason)
|
|
781
|
+
fs.rmSync(path.join(SESS_DIR, PHONE + '.web.json'), { force: true })
|
|
782
|
+
process.exit(1)
|
|
783
|
+
})
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
### Knowing Which Mode You Are In
|
|
787
|
+
|
|
788
|
+
```js
|
|
789
|
+
console.log(client._mode) // 'web' or 'mobile'
|
|
790
|
+
console.log(client.store.me.id) // 919634847671:7@s.whatsapp.net
|
|
791
|
+
console.log(client.store.me.lid) // 112713111982325:7@lid
|
|
792
|
+
console.log(client.store.deviceIndex) // 7 — which linked-device slot
|
|
793
|
+
console.log(client.store.platform) // 'android' — what the primary runs
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
`deviceIndex` is what makes a companion a companion. A primary is device 0 and its JID is the bare number; a companion occupies a numbered slot, and the account's own phone becomes a peer it encrypts to like any other device.
|
|
797
|
+
|
|
798
|
+
### Two Sessions on One Number
|
|
799
|
+
|
|
800
|
+
A number may be SMS-registered and separately linked as a companion. They never share state — separate files, separate Signal sessions, separate history. Which one you get is decided by the method you call:
|
|
801
|
+
|
|
802
|
+
```js
|
|
803
|
+
await client.init(PHONE) // mobile / primary, over TCP
|
|
804
|
+
await client.connectWeb(PHONE) // web / companion, over WebSocket
|
|
805
|
+
```
|
|
806
|
+
|
|
807
|
+
Use two `WhalibmobClient` instances if you want both at once.
|
|
808
|
+
|
|
403
809
|
## Saving & Restoring Sessions
|
|
404
810
|
|
|
405
811
|
Sessions are automatically persisted to disk as JSON files under the `sessionDir` you provide. The file is named `<phone>.json`. On the next `client.init()` call the session is restored and no re-registration is needed.
|
|
@@ -1906,6 +2312,69 @@ Use a custom session directory with `--session`:
|
|
|
1906
2312
|
wa connect 919634847671 --session /data/my-sessions
|
|
1907
2313
|
```
|
|
1908
2314
|
|
|
2315
|
+
### CLI Pairing Code
|
|
2316
|
+
|
|
2317
|
+
If the number is already in use on a phone, or the verification SMS never arrives, link to the existing account instead. When it is not obvious which way you mean, `wa connect` asks:
|
|
2318
|
+
|
|
2319
|
+
```
|
|
2320
|
+
how do you want to connect?
|
|
2321
|
+
1) sms register this number as its own device
|
|
2322
|
+
2) pairing code link to an existing WhatsApp account (8-digit code)
|
|
2323
|
+
sms or pairing code? [1/2] 2
|
|
2324
|
+
```
|
|
2325
|
+
|
|
2326
|
+
The question is skipped when only one kind of session exists on disk, and when stdin is not a terminal. Force either one:
|
|
2327
|
+
|
|
2328
|
+
```sh
|
|
2329
|
+
wa connect 919634847671 --sms
|
|
2330
|
+
wa connect 919634847671 --pair
|
|
2331
|
+
```
|
|
2332
|
+
|
|
2333
|
+
Or go straight to linking:
|
|
2334
|
+
|
|
2335
|
+
```sh
|
|
2336
|
+
wa pair 919634847671
|
|
2337
|
+
```
|
|
2338
|
+
|
|
2339
|
+
```
|
|
2340
|
+
linking +919634847671 to an existing WhatsApp account...
|
|
2341
|
+
────────────────────────────────────────────────────────
|
|
2342
|
+
pairing code K7M2-QX4B
|
|
2343
|
+
────────────────────────────────────────────────────────
|
|
2344
|
+
on the phone that owns +919634847671:
|
|
2345
|
+
WhatsApp → Settings → Linked Devices → Link a device
|
|
2346
|
+
→ Link with phone number instead → enter the code above
|
|
2347
|
+
|
|
2348
|
+
the code is valid for a few minutes; waiting...
|
|
2349
|
+
|
|
2350
|
+
linked as 919634847671:7@s.whatsapp.net (112713111982325:7@lid)
|
|
2351
|
+
device slot 7 · primary is android
|
|
2352
|
+
finishing handshake...
|
|
2353
|
+
connected as +919634847671 (web / companion)
|
|
2354
|
+
history INITIAL_BOOTSTRAP chats=214 contacts=486
|
|
2355
|
+
wa +919634847671>
|
|
2356
|
+
```
|
|
2357
|
+
|
|
2358
|
+
Once linked, plain `wa connect` reconnects without asking for a new code. To pick your own code, pass it as a second argument — exactly 8 characters:
|
|
2359
|
+
|
|
2360
|
+
```sh
|
|
2361
|
+
wa pair 919634847671 MYCODE12
|
|
2362
|
+
```
|
|
2363
|
+
|
|
2364
|
+
The debug prompt works the same in both modes. Answer `y` at startup, or pass `--debug`, to see every stanza of the pairing exchange:
|
|
2365
|
+
|
|
2366
|
+
```sh
|
|
2367
|
+
wa pair 919634847671 --debug
|
|
2368
|
+
```
|
|
2369
|
+
|
|
2370
|
+
From inside the shell:
|
|
2371
|
+
|
|
2372
|
+
```sh
|
|
2373
|
+
wa> /pair 919634847671
|
|
2374
|
+
wa> /connect 919634847671 pair
|
|
2375
|
+
wa> /connect 919634847671 sms
|
|
2376
|
+
```
|
|
2377
|
+
|
|
1909
2378
|
### Listen Mode
|
|
1910
2379
|
|
|
1911
2380
|
Connect and print all incoming events to the terminal. The process stays alive indefinitely until you press Ctrl+C:
|
|
@@ -2623,8 +3092,19 @@ now run: /connect 919634847671
|
|
|
2623
3092
|
|
|
2624
3093
|
```sh
|
|
2625
3094
|
# connect to a number (while already in the shell)
|
|
3095
|
+
# asks sms or pairing code when both are possible
|
|
2626
3096
|
wa> /connect 919634847671
|
|
2627
3097
|
|
|
3098
|
+
# force one or the other
|
|
3099
|
+
wa> /connect 919634847671 sms
|
|
3100
|
+
wa> /connect 919634847671 pair
|
|
3101
|
+
|
|
3102
|
+
# link to an existing account by 8-digit pairing code
|
|
3103
|
+
wa> /pair 919634847671
|
|
3104
|
+
|
|
3105
|
+
# with a code you chose yourself (exactly 8 characters)
|
|
3106
|
+
wa> /pair 919634847671 MYCODE12
|
|
3107
|
+
|
|
2628
3108
|
# disconnect
|
|
2629
3109
|
wa> /disconnect
|
|
2630
3110
|
|
|
@@ -2739,7 +3219,8 @@ wa> /quit
|
|
|
2739
3219
|
| `/reg code <phone> [method]` | Request verification code |
|
|
2740
3220
|
| `/reg confirm <phone> <code>` | Complete registration |
|
|
2741
3221
|
| **Connection** | |
|
|
2742
|
-
| `/connect <phone
|
|
3222
|
+
| `/connect <phone> [sms\|pair]` | Connect to WhatsApp — asks which method when unset |
|
|
3223
|
+
| `/pair <phone> [code]` | Link to an existing account by 8-digit pairing code |
|
|
2743
3224
|
| `/disconnect` | Disconnect current session |
|
|
2744
3225
|
| `/reconnect` | Force reconnection |
|
|
2745
3226
|
| `/session` | Show session info |
|