whalibmob 5.5.39 → 5.5.40
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 +262 -0
- package/index.js +26 -1
- package/lib/Client.js +280 -3
- package/lib/HistorySyncHandler.js +759 -0
- package/lib/proto/MessageProto.js +28 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -130,6 +130,13 @@ npm install -g whalibmob
|
|
|
130
130
|
- [Handling Events](#handling-events)
|
|
131
131
|
- [Example to Start](#example-to-start)
|
|
132
132
|
- [All Events](#all-events)
|
|
133
|
+
- [History Sync](#history-sync)
|
|
134
|
+
- [How It Works](#how-history-sync-works)
|
|
135
|
+
- [Listening to History Sync Events](#listening-to-history-sync-events)
|
|
136
|
+
- [Persistent Files Written to Disk](#persistent-files-written-to-disk)
|
|
137
|
+
- [Reading the History Store](#reading-the-history-store)
|
|
138
|
+
- [tcToken — Error 463 Defense](#tctoken--error-463-defense)
|
|
139
|
+
- [What Is Automatic vs What You Need to Do](#what-is-automatic-vs-what-you-need-to-do)
|
|
133
140
|
- [Receiving Media](#receiving-media)
|
|
134
141
|
- [Sending Messages](#sending-messages)
|
|
135
142
|
- [Text Message](#text-message)
|
|
@@ -550,6 +557,261 @@ The `decoded` object shape per message type:
|
|
|
550
557
|
{ type: 'protocol', subtype: string }
|
|
551
558
|
```
|
|
552
559
|
|
|
560
|
+
---
|
|
561
|
+
|
|
562
|
+
## History Sync
|
|
563
|
+
|
|
564
|
+
### How History Sync Works
|
|
565
|
+
|
|
566
|
+
When whalibmob connects, WhatsApp automatically sends the account's chat history to the client.
|
|
567
|
+
The library handles the entire pipeline **without any code from you** — you only need to listen
|
|
568
|
+
to the events if you want to use the data.
|
|
569
|
+
|
|
570
|
+
The full internal flow:
|
|
571
|
+
|
|
572
|
+
1. WhatsApp server sends an encrypted `ProtocolMessage` (type 6) containing a `HistorySyncNotification`.
|
|
573
|
+
2. The library decrypts it via Signal Protocol.
|
|
574
|
+
3. `HistorySyncHandler` downloads the encrypted blob from WhatsApp's CDN (`mmg.whatsapp.net`), or reads the inline payload if the server embedded it directly.
|
|
575
|
+
4. The blob is decrypted with **AES-256-CBC** using an HKDF key derived from `"WhatsApp History Keys"`.
|
|
576
|
+
5. The result is decompressed with **zlib** and decoded from **protobuf** (WAProto v2.3000.x — field numbers verified against the official proto definition).
|
|
577
|
+
6. Chats, contacts, push names, LID↔PN mappings, and **tcTokens** are merged into the disk store.
|
|
578
|
+
7. tcTokens from history are seeded into `TcTokenStore` in memory so the first outbound DM after reconnect already carries a valid `<tctoken>` node (prevents error 463 on cold start).
|
|
579
|
+
8. The `history_sync` event fires with a summary of what was received.
|
|
580
|
+
|
|
581
|
+
History arrives in **multiple chunks**. Each chunk fires one `history_sync` event. The first chunk (sync type `INITIAL_BOOTSTRAP`) is usually the largest and carries the most recent conversations.
|
|
582
|
+
|
|
583
|
+
### Listening to History Sync Events
|
|
584
|
+
|
|
585
|
+
```js
|
|
586
|
+
const { WhalibmobClient } = require('whalibmob')
|
|
587
|
+
const path = require('path')
|
|
588
|
+
|
|
589
|
+
const client = new WhalibmobClient({
|
|
590
|
+
sessionDir: path.join(process.env.HOME, '.waSession')
|
|
591
|
+
})
|
|
592
|
+
|
|
593
|
+
// history_sync fires once per history chunk — may fire multiple times on first connect
|
|
594
|
+
client.on('history_sync', result => {
|
|
595
|
+
console.log('History sync chunk received:')
|
|
596
|
+
console.log(' type :', result.syncTypeName) // e.g. 'INITIAL_BOOTSTRAP', 'RECENT', 'FULL'
|
|
597
|
+
console.log(' progress:', result.progress) // 0-100 server-reported
|
|
598
|
+
console.log(' chunk :', result.chunkOrder)
|
|
599
|
+
console.log(' chats :', result.chats.length)
|
|
600
|
+
console.log(' contacts:', result.contacts.length)
|
|
601
|
+
console.log(' pushNames:', (result.pushNames || []).length)
|
|
602
|
+
})
|
|
603
|
+
|
|
604
|
+
// history_sync_error fires if a chunk fails to download or decrypt
|
|
605
|
+
client.on('history_sync_error', ({ err, notification }) => {
|
|
606
|
+
console.error('History sync failed:', err.message)
|
|
607
|
+
console.error(' syncType:', notification.syncType)
|
|
608
|
+
})
|
|
609
|
+
|
|
610
|
+
await client.init('919634847671')
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
The `result` object shape emitted by `history_sync`:
|
|
614
|
+
|
|
615
|
+
```js
|
|
616
|
+
{
|
|
617
|
+
syncType: number, // HistorySyncType enum value
|
|
618
|
+
syncTypeName: string, // 'INITIAL_BOOTSTRAP' | 'RECENT' | 'FULL' | 'PUSH_NAME' | 'NON_BLOCKING_DATA' | 'ON_DEMAND'
|
|
619
|
+
progress: number, // 0-100, server-reported progress
|
|
620
|
+
chunkOrder: number, // chunk sequence number
|
|
621
|
+
chats: [ // one entry per conversation in this chunk
|
|
622
|
+
{
|
|
623
|
+
id: string, // chat JID e.g. '919634847671@s.whatsapp.net' or '120363...@g.us'
|
|
624
|
+
name: string, // display name (may be undefined for unknown contacts)
|
|
625
|
+
unreadCount: number,
|
|
626
|
+
lastMsgTimestamp: number, // Unix seconds
|
|
627
|
+
messageCount: number // number of messages in this chunk for this chat
|
|
628
|
+
}
|
|
629
|
+
],
|
|
630
|
+
contacts: [ // one entry per contact discovered in this chunk
|
|
631
|
+
{
|
|
632
|
+
id: string,
|
|
633
|
+
name: string,
|
|
634
|
+
username: string, // WhatsApp username if set
|
|
635
|
+
pnJid: string, // phone-number JID e.g. '919634847671@s.whatsapp.net'
|
|
636
|
+
lidJid: string // LID JID e.g. '112345678901234@lid'
|
|
637
|
+
}
|
|
638
|
+
],
|
|
639
|
+
pushNames: Array, // push-name entries { id, pushname }
|
|
640
|
+
lidPnMappings: Array, // LID<->PN mapping entries { lidJid, pnJid }
|
|
641
|
+
merged: object // raw merged history store (see Reading the History Store below)
|
|
642
|
+
}
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
Sync type values:
|
|
646
|
+
|
|
647
|
+
| `syncTypeName` | When it fires |
|
|
648
|
+
|---|---|
|
|
649
|
+
| `INITIAL_BOOTSTRAP` | First connect — most recent conversations |
|
|
650
|
+
| `RECENT` | Reconnect after a short offline period |
|
|
651
|
+
| `FULL` | Full historical sync (older messages) |
|
|
652
|
+
| `PUSH_NAME` | Contact name updates only |
|
|
653
|
+
| `NON_BLOCKING_DATA` | Background low-priority data |
|
|
654
|
+
| `ON_DEMAND` | Explicitly requested by the client |
|
|
655
|
+
|
|
656
|
+
### Persistent Files Written to Disk
|
|
657
|
+
|
|
658
|
+
The library automatically writes these files to `sessionDir` per account. You do not need to create or manage them.
|
|
659
|
+
|
|
660
|
+
| File | Contents |
|
|
661
|
+
|---|---|
|
|
662
|
+
| `<phone>.history.json` | Chats, contacts, push names, LID↔PN mappings, tcTokens |
|
|
663
|
+
| `<phone>.messages.json` | Flat map of `msgId → message metadata` |
|
|
664
|
+
| `<phone>.appStateKeys.json` | App-state sync keys (used for app-state patch decryption) |
|
|
665
|
+
| `<phone>.tctoken.json` | Trusted-contact token store (tcToken per contact JID) |
|
|
666
|
+
|
|
667
|
+
### Reading the History Store
|
|
668
|
+
|
|
669
|
+
After history sync completes you can read the on-disk files directly:
|
|
670
|
+
|
|
671
|
+
```js
|
|
672
|
+
const fs = require('fs')
|
|
673
|
+
const path = require('path')
|
|
674
|
+
|
|
675
|
+
const sessDir = path.join(process.env.HOME, '.waSession')
|
|
676
|
+
const phone = '919634847671'
|
|
677
|
+
|
|
678
|
+
// ── Read chats ────────────────────────────────────────────────────────────────
|
|
679
|
+
const histPath = path.join(sessDir, phone + '.history.json')
|
|
680
|
+
const hist = JSON.parse(fs.readFileSync(histPath, 'utf8'))
|
|
681
|
+
|
|
682
|
+
// List all chats sorted by last message time
|
|
683
|
+
const chats = Object.values(hist.chats)
|
|
684
|
+
.sort((a, b) => (b.lastMsgTimestamp || 0) - (a.lastMsgTimestamp || 0))
|
|
685
|
+
|
|
686
|
+
for (const chat of chats.slice(0, 10)) {
|
|
687
|
+
console.log(chat.id, '|', chat.name || '(unknown)', '|', chat.unreadCount, 'unread')
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// ── LID ↔ PN lookup ───────────────────────────────────────────────────────────
|
|
691
|
+
// Look up LID JID from phone number JID
|
|
692
|
+
const myLid = hist.pnLidMap['919634847671@s.whatsapp.net']
|
|
693
|
+
console.log('LID:', myLid) // e.g. '112345678901234@lid'
|
|
694
|
+
|
|
695
|
+
// Reverse: phone number JID from LID
|
|
696
|
+
const myPn = hist.lidPnMap[myLid]
|
|
697
|
+
console.log('PN:', myPn)
|
|
698
|
+
|
|
699
|
+
// ── Read message metadata ─────────────────────────────────────────────────────
|
|
700
|
+
const msgPath = path.join(sessDir, phone + '.messages.json')
|
|
701
|
+
const msgs = JSON.parse(fs.readFileSync(msgPath, 'utf8'))
|
|
702
|
+
const msgList = Object.values(msgs).sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0))
|
|
703
|
+
console.log('Total messages indexed:', msgList.length)
|
|
704
|
+
console.log('Latest:', msgList[0])
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
`hist.chats` schema per chat entry:
|
|
708
|
+
|
|
709
|
+
```js
|
|
710
|
+
{
|
|
711
|
+
id: string, // JID
|
|
712
|
+
name: string,
|
|
713
|
+
displayName: string,
|
|
714
|
+
unreadCount: number,
|
|
715
|
+
lastMsgTimestamp: number, // Unix seconds
|
|
716
|
+
messageCount: number, // total messages indexed from history
|
|
717
|
+
ephemeralExpiry: number, // disappearing messages timer in seconds, if set
|
|
718
|
+
archived: boolean,
|
|
719
|
+
pinned: number, // pin sort order (0 = not pinned)
|
|
720
|
+
tcToken: string, // base64 — trusted-contact token (used internally by the library)
|
|
721
|
+
tcTokenTimestamp: number, // Unix seconds — when the token was issued by the server
|
|
722
|
+
tcTokenSenderTimestamp: number // Unix seconds — sender-side issuance timestamp for 7-day bucket dedup
|
|
723
|
+
}
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
`msgs` schema per message entry:
|
|
727
|
+
|
|
728
|
+
```js
|
|
729
|
+
{
|
|
730
|
+
id: string, // WhatsApp message ID
|
|
731
|
+
chatId: string, // JID of the conversation
|
|
732
|
+
fromMe: boolean,
|
|
733
|
+
fromJid: string, // sender JID
|
|
734
|
+
timestamp: number, // Unix seconds
|
|
735
|
+
pushName: string, // display name of sender at send time
|
|
736
|
+
status: number // 0=error 1=pending 2=server 3=delivered 4=read 5=played
|
|
737
|
+
}
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
### tcToken — Error 463 Defense
|
|
741
|
+
|
|
742
|
+
> [!IMPORTANT]
|
|
743
|
+
> This section is informational. The entire tcToken lifecycle is **fully automatic**.
|
|
744
|
+
> You do not need to write any code for it.
|
|
745
|
+
|
|
746
|
+
WhatsApp counts every outbound DM sent **without** a `<tctoken>` node as an anonymous "reach-out" event. Once enough such events accumulate the server enforces a time-based **Reach-out Time-lock** and returns error `463` (`NackCallerReachoutTimelocked`), blocking all outbound messages and calls for a period.
|
|
747
|
+
|
|
748
|
+
whalibmob implements the full lifecycle to prevent this:
|
|
749
|
+
|
|
750
|
+
| Step | What the library does automatically |
|
|
751
|
+
|---|---|
|
|
752
|
+
| **History seed** | On every history sync chunk, `tcToken` bytes are extracted from each conversation in the protobuf and loaded into `TcTokenStore` in memory. The first send after reconnect already has a valid token ready — no 463 risk on cold start. |
|
|
753
|
+
| **Attach on send** | Before dispatching any DM, `MessageSender` looks up the token for the recipient JID, checks it has not expired (28-day rolling window), and pushes a `<tctoken>` child node into the message stanza. |
|
|
754
|
+
| **Proactive issuance** | After each successful DM send, the library fires a `<iq type='set' xmlns='privacy'>` requesting a fresh token for that JID from the server — once per 7-day bucket, deduplicated in-flight. |
|
|
755
|
+
| **Incoming notification** | When a contact starts a new conversation, WhatsApp pushes a `<notification type='privacy_token'>`. The library catches it in `_handlePrivacyTokenNotification` and stores the token immediately. |
|
|
756
|
+
| **Identity change re-issue** | When decrypting a `pkmsg` (new Signal session from peer), the library calls `_reissueTcTokenAfterIdentityChange` to re-issue the token for the new session. |
|
|
757
|
+
| **Error 463 recovery** | If a send fails with error 463, the library issues a fresh token, waits for the server response, and automatically retries the same message with the new token attached. |
|
|
758
|
+
| **Expiry** | Tokens use a 4-bucket rolling window (4 × 7 days = 28-day TTL). Expired tokens are cleared before the send and a fresh one is requested proactively. |
|
|
759
|
+
|
|
760
|
+
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.
|
|
761
|
+
|
|
762
|
+
### What Is Automatic vs What You Need to Do
|
|
763
|
+
|
|
764
|
+
**Everything in the "Automatic" column requires zero code from you.**
|
|
765
|
+
|
|
766
|
+
| Feature | Automatic | Notes |
|
|
767
|
+
|---|---|---|
|
|
768
|
+
| Download history blob from CDN | ✅ | |
|
|
769
|
+
| Decrypt history blob (AES-256-CBC + HKDF) | ✅ | |
|
|
770
|
+
| Decompress zlib | ✅ | |
|
|
771
|
+
| Decode protobuf (WAProto v2.3000.x) | ✅ | Field numbers verified against official proto |
|
|
772
|
+
| Persist chats / contacts / push names | ✅ | Written to `<phone>.history.json` |
|
|
773
|
+
| Persist message metadata | ✅ | Written to `<phone>.messages.json` |
|
|
774
|
+
| Persist app-state sync keys | ✅ | Written to `<phone>.appStateKeys.json` |
|
|
775
|
+
| Seed tcTokens into memory on connect | ✅ | Prevents error 463 on first send after reconnect |
|
|
776
|
+
| Attach tcToken to every outbound DM | ✅ | |
|
|
777
|
+
| Issue fresh tcTokens after each send | ✅ | Once per 7-day bucket per contact |
|
|
778
|
+
| Handle incoming `privacy_token` notifications | ✅ | |
|
|
779
|
+
| Re-issue tcToken after peer identity change | ✅ | |
|
|
780
|
+
| Recover from error 463 with automatic retry | ✅ | |
|
|
781
|
+
| Populate in-memory LID↔PN maps | ✅ | |
|
|
782
|
+
| Listen to `history_sync` event | 🔵 Optional | Only if your app needs to react to history data |
|
|
783
|
+
| Read `<phone>.history.json` | 🔵 Optional | Only if your app needs chat/contact data at rest |
|
|
784
|
+
| Read `<phone>.messages.json` | 🔵 Optional | Only if your app indexes messages |
|
|
785
|
+
|
|
786
|
+
**Minimum working integration — history sync, tcTokens, and error-463 defense all active with zero extra code:**
|
|
787
|
+
|
|
788
|
+
```js
|
|
789
|
+
const { WhalibmobClient } = require('whalibmob')
|
|
790
|
+
const path = require('path')
|
|
791
|
+
|
|
792
|
+
const client = new WhalibmobClient({
|
|
793
|
+
sessionDir: path.join(process.env.HOME, '.waSession')
|
|
794
|
+
})
|
|
795
|
+
|
|
796
|
+
// History sync, tcToken seeding, error-463 defense, and all disk persistence
|
|
797
|
+
// happen automatically. Add the listeners below only if your app needs the data.
|
|
798
|
+
|
|
799
|
+
client.on('history_sync', result => {
|
|
800
|
+
// Optional — fires once per chunk (multiple times on first connect)
|
|
801
|
+
console.log('[', result.syncTypeName, ']',
|
|
802
|
+
result.chats.length, 'chats,',
|
|
803
|
+
result.contacts.length, 'contacts')
|
|
804
|
+
})
|
|
805
|
+
|
|
806
|
+
client.on('history_sync_error', ({ err }) => {
|
|
807
|
+
// Optional — log failures (library continues working even if a chunk fails)
|
|
808
|
+
console.error('History chunk failed:', err.message)
|
|
809
|
+
})
|
|
810
|
+
|
|
811
|
+
await client.init('919634847671')
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
|
|
553
815
|
## Receiving Media
|
|
554
816
|
|
|
555
817
|
When a media message arrives, `msg.decoded` contains a CDN `url` and a `mediaKey`.
|
package/index.js
CHANGED
|
@@ -17,6 +17,19 @@ const {
|
|
|
17
17
|
assertMeId,
|
|
18
18
|
initAuthCreds
|
|
19
19
|
} = require('./lib/auth-utils');
|
|
20
|
+
const {
|
|
21
|
+
processHistorySyncNotification,
|
|
22
|
+
decodeHistorySyncNotification,
|
|
23
|
+
decodeHistorySync,
|
|
24
|
+
decodeAppStateSyncKeyShare,
|
|
25
|
+
saveAppStateSyncKeys,
|
|
26
|
+
loadAppStateSyncKeys,
|
|
27
|
+
mergeHistoryToStore,
|
|
28
|
+
decryptHistoryBlob,
|
|
29
|
+
deriveHistoryKeys,
|
|
30
|
+
HistorySyncType,
|
|
31
|
+
HistorySyncTypeName
|
|
32
|
+
} = require('./lib/HistorySyncHandler');
|
|
20
33
|
|
|
21
34
|
module.exports = {
|
|
22
35
|
WhalibmobClient,
|
|
@@ -59,5 +72,17 @@ module.exports = {
|
|
|
59
72
|
makeCacheableSignalKeyStore,
|
|
60
73
|
addTransactionCapability,
|
|
61
74
|
assertMeId,
|
|
62
|
-
initAuthCreds
|
|
75
|
+
initAuthCreds,
|
|
76
|
+
// History sync
|
|
77
|
+
processHistorySyncNotification,
|
|
78
|
+
decodeHistorySyncNotification,
|
|
79
|
+
decodeHistorySync,
|
|
80
|
+
decodeAppStateSyncKeyShare,
|
|
81
|
+
saveAppStateSyncKeys,
|
|
82
|
+
loadAppStateSyncKeys,
|
|
83
|
+
mergeHistoryToStore,
|
|
84
|
+
decryptHistoryBlob,
|
|
85
|
+
deriveHistoryKeys,
|
|
86
|
+
HistorySyncType,
|
|
87
|
+
HistorySyncTypeName
|
|
63
88
|
};
|
package/lib/Client.js
CHANGED
|
@@ -13,6 +13,13 @@ const { createNewStore, saveStore, loadStore, toSixParts, fromSixParts } = requi
|
|
|
13
13
|
const { BinaryNode } = require('./BinaryNode');
|
|
14
14
|
const { SignalProtocol } = require('./signal/SignalProtocol');
|
|
15
15
|
const { DeviceManager } = require('./DeviceManager');
|
|
16
|
+
const {
|
|
17
|
+
processHistorySyncNotification,
|
|
18
|
+
decodeHistorySyncNotification,
|
|
19
|
+
decodeAppStateSyncKeyShare,
|
|
20
|
+
saveAppStateSyncKeys,
|
|
21
|
+
HistorySyncType
|
|
22
|
+
} = require('./HistorySyncHandler');
|
|
16
23
|
|
|
17
24
|
const PING_INTERVAL_MS = 25000;
|
|
18
25
|
const KEEPALIVE_INTERVAL = 20000;
|
|
@@ -699,8 +706,22 @@ class WhalibmobClient extends EventEmitter {
|
|
|
699
706
|
return;
|
|
700
707
|
}
|
|
701
708
|
|
|
702
|
-
// Protocol messages
|
|
703
|
-
if (decoded.type === 'protocol') {
|
|
709
|
+
// Protocol messages — handle history sync and app-state key share before dropping
|
|
710
|
+
if (decoded.type === 'protocol') {
|
|
711
|
+
_whaDbg('[DBG] PROTO subtype=' + decoded.subtype + ' id=' + id);
|
|
712
|
+
|
|
713
|
+
// ── HISTORY_SYNC_NOTIFICATION ─────────────────────────────────────
|
|
714
|
+
if (decoded.subtype === 'history_sync' && decoded.historySyncNotification) {
|
|
715
|
+
this._handleHistorySync(decoded.historySyncNotification, id);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// ── APP_STATE_SYNC_KEY_SHARE ──────────────────────────────────────
|
|
719
|
+
if (decoded.subtype === 'app_state_sync_key_share' && decoded.appStateSyncKeyShare) {
|
|
720
|
+
this._handleAppStateSyncKeyShare(decoded.appStateSyncKeyShare);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
704
725
|
|
|
705
726
|
this.emit('message', { id, from, participant, ts, decoded, node });
|
|
706
727
|
this._sendReadReceipt(id, fromRaw, partRaw);
|
|
@@ -728,7 +749,15 @@ class WhalibmobClient extends EventEmitter {
|
|
|
728
749
|
const plaintext = this._signal.senderKeyDecrypt(from, participant, cipherBuf);
|
|
729
750
|
const decoded = this._decodeMsg(plaintext);
|
|
730
751
|
if (decoded.type === 'senderKeyDistribution') return;
|
|
731
|
-
if (decoded.type === 'protocol')
|
|
752
|
+
if (decoded.type === 'protocol') {
|
|
753
|
+
if (decoded.subtype === 'history_sync' && decoded.historySyncNotification) {
|
|
754
|
+
this._handleHistorySync(decoded.historySyncNotification, id);
|
|
755
|
+
}
|
|
756
|
+
if (decoded.subtype === 'app_state_sync_key_share' && decoded.appStateSyncKeyShare) {
|
|
757
|
+
this._handleAppStateSyncKeyShare(decoded.appStateSyncKeyShare);
|
|
758
|
+
}
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
732
761
|
this.emit('message', { id, from, participant, ts, decoded, isGroup: true, node });
|
|
733
762
|
this._sendReadReceipt(id, fromRaw, partRaw);
|
|
734
763
|
} catch (err) {
|
|
@@ -1020,6 +1049,14 @@ class WhalibmobClient extends EventEmitter {
|
|
|
1020
1049
|
this._handlePrivacyTokenNotification(node);
|
|
1021
1050
|
}
|
|
1022
1051
|
|
|
1052
|
+
if (type === 'md-msg-hist') {
|
|
1053
|
+
// Server signals that history sync data is available.
|
|
1054
|
+
// The actual encrypted notification may arrive as:
|
|
1055
|
+
// (a) an encrypted <message> containing HistorySyncNotification (primary path, handled in _decryptDMMessage)
|
|
1056
|
+
// (b) inline in this notification node via an <enc> child (handle here)
|
|
1057
|
+
this._handleMdMsgHistNotification(node);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1023
1060
|
if (type === 'devices') {
|
|
1024
1061
|
// Server push: a contact's linked device list changed (they linked or
|
|
1025
1062
|
// unlinked a tablet, desktop, etc.). Invalidate that phone's cache entry
|
|
@@ -1710,6 +1747,246 @@ class WhalibmobClient extends EventEmitter {
|
|
|
1710
1747
|
})();
|
|
1711
1748
|
}
|
|
1712
1749
|
|
|
1750
|
+
// ─── History sync ─────────────────────────────────────────────────────────
|
|
1751
|
+
|
|
1752
|
+
/**
|
|
1753
|
+
* _handleHistorySync — called when a decrypted DM protocol message contains
|
|
1754
|
+
* subtype='history_sync' with a HistorySyncNotification.
|
|
1755
|
+
*
|
|
1756
|
+
* Fires asynchronously (fire-and-forget with error emission).
|
|
1757
|
+
* Emits 'history_sync' on success, 'history_sync_error' on failure.
|
|
1758
|
+
*
|
|
1759
|
+
* @param {object} notification Decoded HistorySyncNotification
|
|
1760
|
+
* @param {string} msgId Source message ID (for logging)
|
|
1761
|
+
*/
|
|
1762
|
+
_handleHistorySync(notification, msgId) {
|
|
1763
|
+
if (!notification) return;
|
|
1764
|
+
const phone = this._store && this._store.phoneNumber;
|
|
1765
|
+
const sessionDir = this._sessionDir;
|
|
1766
|
+
|
|
1767
|
+
_whaDbg('[DBG] HIST_SYNC start syncType=' + notification.syncType +
|
|
1768
|
+
' progress=' + notification.progress +
|
|
1769
|
+
' chunk=' + notification.chunkOrder +
|
|
1770
|
+
' inline=' + !!(notification.initialHistBootstrapInlinePayload &&
|
|
1771
|
+
notification.initialHistBootstrapInlinePayload.length) +
|
|
1772
|
+
' msgId=' + msgId);
|
|
1773
|
+
|
|
1774
|
+
processHistorySyncNotification(notification, sessionDir, phone)
|
|
1775
|
+
.then(result => {
|
|
1776
|
+
_whaDbg('[DBG] HIST_SYNC done syncType=' + result.syncTypeName +
|
|
1777
|
+
' chats=' + result.chats.length +
|
|
1778
|
+
' contacts=' + result.contacts.length +
|
|
1779
|
+
' pushNames=' + (result.pushNames || []).length);
|
|
1780
|
+
|
|
1781
|
+
// Populate in-memory LID ↔ PN mappings from history sync data
|
|
1782
|
+
if (result.lidPnMappings && result.lidPnMappings.length) {
|
|
1783
|
+
for (const m of result.lidPnMappings) {
|
|
1784
|
+
if (m.lidJid && m.pnJid) {
|
|
1785
|
+
const lidUser = m.lidJid.split('@')[0].split(':')[0];
|
|
1786
|
+
const pnUser = m.pnJid.split('@')[0].split(':')[0];
|
|
1787
|
+
if (lidUser && pnUser) {
|
|
1788
|
+
this._lidToPn.set(lidUser, pnUser);
|
|
1789
|
+
this._pnToLid.set(pnUser, lidUser);
|
|
1790
|
+
if (this._signal && this._signal.store && this._signal.store.setLidMapping) {
|
|
1791
|
+
this._signal.store.setLidMapping(pnUser, lidUser);
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
// Seed TcTokenStore from history — populates trusted-contact tokens
|
|
1799
|
+
// for all existing conversations so the first send after reconnect
|
|
1800
|
+
// already carries a valid <tctoken> node (avoids error 463 on cold start).
|
|
1801
|
+
this._seedTcTokensFromHistory(result.merged);
|
|
1802
|
+
|
|
1803
|
+
this.emit('history_sync', result);
|
|
1804
|
+
})
|
|
1805
|
+
.catch(err => {
|
|
1806
|
+
_whaDbg('[DBG] HIST_SYNC_ERR ' + (err && err.message));
|
|
1807
|
+
this.emit('history_sync_error', { err, notification });
|
|
1808
|
+
});
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
/**
|
|
1812
|
+
* _seedTcTokensFromHistory — called after each history sync chunk.
|
|
1813
|
+
*
|
|
1814
|
+
* Reads tcToken/tcTokenTimestamp/tcTokenSenderTimestamp from the merged
|
|
1815
|
+
* history store and loads them into TcTokenStore so that the first outbound
|
|
1816
|
+
* DM after reconnect already has a valid <tctoken> node attached.
|
|
1817
|
+
*
|
|
1818
|
+
* Without this, TcTokenStore starts empty on every new connection and the
|
|
1819
|
+
* first send to every existing contact is treated as an anonymous reach-out
|
|
1820
|
+
* by the WhatsApp server, accumulating toward the error-463 timelock.
|
|
1821
|
+
*
|
|
1822
|
+
* Rules:
|
|
1823
|
+
* - Only seeds tokens that are not already in TcTokenStore (existing entry wins).
|
|
1824
|
+
* - Skips expired tokens (no point loading a token we'd immediately discard).
|
|
1825
|
+
* - Storage key follows TcTokenStore convention: normalized bare JID.
|
|
1826
|
+
* For LID chats (jid ends in @lid) we use the lid JID directly.
|
|
1827
|
+
* For PN chats we try to look up the LID first (Baileys always stores under LID).
|
|
1828
|
+
*
|
|
1829
|
+
* @param {object|null} merged - Return value of mergeHistoryToStore()
|
|
1830
|
+
*/
|
|
1831
|
+
_seedTcTokensFromHistory(merged) {
|
|
1832
|
+
if (!merged || !merged.chats) return;
|
|
1833
|
+
if (!this._tcTokenStore) return;
|
|
1834
|
+
|
|
1835
|
+
const { normalizeJidForTcToken, tcTokenExpired } = require('./messages/TcTokenStore');
|
|
1836
|
+
|
|
1837
|
+
let seeded = 0;
|
|
1838
|
+
for (const [jid, chat] of Object.entries(merged.chats)) {
|
|
1839
|
+
// Only chats that actually have a token stored from history
|
|
1840
|
+
if (!chat.tcToken) continue;
|
|
1841
|
+
|
|
1842
|
+
// Convert base64 string back to Buffer (mergeHistoryToStore stores as base64)
|
|
1843
|
+
const tokenBuf = Buffer.from(chat.tcToken, 'base64');
|
|
1844
|
+
if (!tokenBuf.length) continue;
|
|
1845
|
+
|
|
1846
|
+
const ts = chat.tcTokenTimestamp || 0;
|
|
1847
|
+
const senderTs = chat.tcTokenSenderTimestamp || 0;
|
|
1848
|
+
|
|
1849
|
+
// Skip already-expired tokens — no use loading them
|
|
1850
|
+
if (tcTokenExpired(ts)) {
|
|
1851
|
+
_whaDbg('[DBG] SEED_TCTOKEN skip expired jid=' + jid + ' ts=' + ts);
|
|
1852
|
+
continue;
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
// Resolve storage JID: Baileys always stores under LID.
|
|
1856
|
+
// If this chat is a @lid jid, use it directly.
|
|
1857
|
+
// If it's a PN jid, try to find its LID counterpart.
|
|
1858
|
+
let storageJid;
|
|
1859
|
+
if (jid.endsWith('@lid')) {
|
|
1860
|
+
storageJid = normalizeJidForTcToken(jid);
|
|
1861
|
+
} else {
|
|
1862
|
+
const pnUser = jid.split('@')[0].split(':')[0];
|
|
1863
|
+
const lidUser = this._pnToLid && this._pnToLid.get(pnUser);
|
|
1864
|
+
storageJid = lidUser
|
|
1865
|
+
? normalizeJidForTcToken(lidUser + '@lid')
|
|
1866
|
+
: normalizeJidForTcToken(jid);
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
// Don't overwrite a token that was already fetched live this session
|
|
1870
|
+
const existing = this._tcTokenStore.get(storageJid);
|
|
1871
|
+
if (existing && existing.token && existing.token.length &&
|
|
1872
|
+
!tcTokenExpired(existing.timestamp)) {
|
|
1873
|
+
_whaDbg('[DBG] SEED_TCTOKEN skip existing jid=' + storageJid);
|
|
1874
|
+
continue;
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
// Set token + senderTimestamp atomically via the two TcTokenStore setters
|
|
1878
|
+
this._tcTokenStore.setToken(storageJid, tokenBuf, ts);
|
|
1879
|
+
if (senderTs) {
|
|
1880
|
+
this._tcTokenStore.setSenderTimestamp(storageJid, senderTs);
|
|
1881
|
+
}
|
|
1882
|
+
seeded++;
|
|
1883
|
+
_whaDbg('[DBG] SEED_TCTOKEN seeded jid=' + storageJid +
|
|
1884
|
+
' ts=' + ts + ' senderTs=' + senderTs + ' len=' + tokenBuf.length);
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
if (seeded > 0) {
|
|
1888
|
+
_whaDbg('[DBG] SEED_TCTOKEN total seeded=' + seeded + ' from history');
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
/**
|
|
1893
|
+
* _handleAppStateSyncKeyShare — saves app-state sync keys received in a
|
|
1894
|
+
* ProtocolMessage (type=5 APP_STATE_SYNC_KEY_SHARE).
|
|
1895
|
+
* These keys are needed to decrypt app-state patches in _syncAppState.
|
|
1896
|
+
*
|
|
1897
|
+
* @param {object} keyShare Decoded AppStateSyncKeyShare { keys: [...] }
|
|
1898
|
+
*/
|
|
1899
|
+
_handleAppStateSyncKeyShare(keyShare) {
|
|
1900
|
+
if (!keyShare || !keyShare.keys || !keyShare.keys.length) return;
|
|
1901
|
+
const phone = this._store && this._store.phoneNumber;
|
|
1902
|
+
const sessionDir = this._sessionDir;
|
|
1903
|
+
|
|
1904
|
+
_whaDbg('[DBG] APP_STATE_KEY_SHARE keys=' + keyShare.keys.length);
|
|
1905
|
+
|
|
1906
|
+
try {
|
|
1907
|
+
saveAppStateSyncKeys(sessionDir, phone, keyShare);
|
|
1908
|
+
} catch (e) {
|
|
1909
|
+
_whaDbg('[DBG] APP_STATE_KEY_SHARE_SAVE_ERR ' + e.message);
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
this.emit('app_state_keys', { keys: keyShare.keys });
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
/**
|
|
1916
|
+
* _handleMdMsgHistNotification — called when server sends a
|
|
1917
|
+
* <notification type="md-msg-hist"> node.
|
|
1918
|
+
*
|
|
1919
|
+
* On mobile WhatsApp the primary history sync path is via encrypted DM
|
|
1920
|
+
* protocol messages (HistorySyncNotification inside a ProtocolMessage type=6).
|
|
1921
|
+
* This handler covers the secondary path where the server delivers the
|
|
1922
|
+
* notification inline inside the notification node itself with an <enc> child,
|
|
1923
|
+
* or simply signals availability so we emit the event.
|
|
1924
|
+
*
|
|
1925
|
+
* @param {object} node Full BinaryNode of the notification
|
|
1926
|
+
*/
|
|
1927
|
+
_handleMdMsgHistNotification(node) {
|
|
1928
|
+
if (!node) return;
|
|
1929
|
+
const attrs = node.attrs || {};
|
|
1930
|
+
|
|
1931
|
+
_whaDbg('[DBG] MD_MSG_HIST from=' + (attrs.from || '') + ' id=' + (attrs.id || ''));
|
|
1932
|
+
|
|
1933
|
+
// Check for inline HistorySyncNotification bytes in an <enc> child
|
|
1934
|
+
// (some server versions deliver it this way)
|
|
1935
|
+
const encNode = findChild(node, 'enc');
|
|
1936
|
+
if (encNode) {
|
|
1937
|
+
const encBuf = Buffer.isBuffer(encNode.content)
|
|
1938
|
+
? encNode.content
|
|
1939
|
+
: (encNode.content ? Buffer.from(encNode.content) : null);
|
|
1940
|
+
|
|
1941
|
+
if (encBuf && encBuf.length > 0) {
|
|
1942
|
+
// Try to decode directly as HistorySyncNotification (unencrypted path)
|
|
1943
|
+
try {
|
|
1944
|
+
const notification = decodeHistorySyncNotification(encBuf);
|
|
1945
|
+
if (notification && (notification.directPath || notification.initialHistBootstrapInlinePayload)) {
|
|
1946
|
+
_whaDbg('[DBG] MD_MSG_HIST inline notification directPath=' + notification.directPath);
|
|
1947
|
+
this._handleHistorySync(notification, attrs.id || 'md-msg-hist');
|
|
1948
|
+
return;
|
|
1949
|
+
}
|
|
1950
|
+
} catch (_) {}
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
// Emit raw event for the caller to inspect if needed
|
|
1955
|
+
this.emit('md_msg_hist', { node, attrs });
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
/**
|
|
1959
|
+
* getHistoryStore — load the persisted history store for this phone.
|
|
1960
|
+
* Returns the parsed {phone}.history.json content or null if not yet synced.
|
|
1961
|
+
*
|
|
1962
|
+
* @returns {object|null}
|
|
1963
|
+
*/
|
|
1964
|
+
getHistoryStore() {
|
|
1965
|
+
if (!this._store || !this._store.phoneNumber) return null;
|
|
1966
|
+
const fs = require('fs');
|
|
1967
|
+
const file = require('path').join(this._sessionDir, this._store.phoneNumber + '.history.json');
|
|
1968
|
+
try {
|
|
1969
|
+
if (!fs.existsSync(file)) return null;
|
|
1970
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
1971
|
+
} catch (_) { return null; }
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
/**
|
|
1975
|
+
* getMessagesStore — load the persisted messages for this phone.
|
|
1976
|
+
* Returns the parsed {phone}.messages.json content or null if not yet synced.
|
|
1977
|
+
*
|
|
1978
|
+
* @returns {object|null}
|
|
1979
|
+
*/
|
|
1980
|
+
getMessagesStore() {
|
|
1981
|
+
if (!this._store || !this._store.phoneNumber) return null;
|
|
1982
|
+
const fs = require('fs');
|
|
1983
|
+
const file = require('path').join(this._sessionDir, this._store.phoneNumber + '.messages.json');
|
|
1984
|
+
try {
|
|
1985
|
+
if (!fs.existsSync(file)) return null;
|
|
1986
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
1987
|
+
} catch (_) { return null; }
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1713
1990
|
// ─── Public API ───────────────────────────────────────────────────────────
|
|
1714
1991
|
|
|
1715
1992
|
setPresence(available) {
|