whalibmob 5.28.0 → 5.29.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 +204 -0
- package/index.d.ts +93 -1
- package/lib/Client.js +488 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -262,6 +262,10 @@ npm install -g whalibmob
|
|
|
262
262
|
- [Read Privacy Settings](#read-privacy-settings)
|
|
263
263
|
- [Update Privacy Settings](#update-privacy-settings)
|
|
264
264
|
- [Update Default Disappearing Mode](#update-default-disappearing-mode)
|
|
265
|
+
- [Account State](#account-state)
|
|
266
|
+
- [AB Props](#ab-props)
|
|
267
|
+
- [Contact Sync](#contact-sync)
|
|
268
|
+
- [Terms-of-Service Notices](#terms-of-service-notices)
|
|
265
269
|
- [Communities](#communities)
|
|
266
270
|
- [Create a Community](#create-a-community)
|
|
267
271
|
- [Deactivate / Delete a Community](#deactivate--delete-a-community)
|
|
@@ -4605,6 +4609,206 @@ await client.changeNewChatsEphemeralTimer(86400) // 1 day
|
|
|
4605
4609
|
await client.changeNewChatsEphemeralTimer(0) // off
|
|
4606
4610
|
```
|
|
4607
4611
|
|
|
4612
|
+
## Account State
|
|
4613
|
+
|
|
4614
|
+
Three things the app speaks on every account and whalibmob did not: the
|
|
4615
|
+
server's own feature flags, the address book, and the legal notices an account
|
|
4616
|
+
is asked to accept. Each is one IQ family; together they are part of what makes
|
|
4617
|
+
a session look like an app rather than a script.
|
|
4618
|
+
|
|
4619
|
+
### AB Props
|
|
4620
|
+
|
|
4621
|
+
Every WhatsApp client asks the server which experiments and configuration
|
|
4622
|
+
values apply to *this* account, then gates behaviour on the answer — how many
|
|
4623
|
+
bytes an identity hash is truncated to, whether the LID migration has started
|
|
4624
|
+
here, what sampling weight each telemetry event carries. A client that never
|
|
4625
|
+
asks runs on guessed defaults wherever the server has an opinion.
|
|
4626
|
+
|
|
4627
|
+
This is synced automatically on every connect. You do not have to call it.
|
|
4628
|
+
|
|
4629
|
+
```js
|
|
4630
|
+
const client = new WhalibmobClient({ sessionDir: './auth' })
|
|
4631
|
+
await client.connect()
|
|
4632
|
+
|
|
4633
|
+
// Already synced by the time 'connected' fires. Read a value:
|
|
4634
|
+
client.abProp(4405) // '16' — always a string on the wire
|
|
4635
|
+
client.abPropInt(4405, 8) // 16 — parsed, or 8 if unset/unparseable
|
|
4636
|
+
client.abPropBool(5010, false) // true — understands "1"/"0" and "true"/"false"
|
|
4637
|
+
```
|
|
4638
|
+
|
|
4639
|
+
Reading a prop before the first sync gives the fallback rather than throwing,
|
|
4640
|
+
so gating on a flag never needs a guard:
|
|
4641
|
+
|
|
4642
|
+
```js
|
|
4643
|
+
// Safe even on a client that has not connected yet.
|
|
4644
|
+
const hashLen = client.abPropInt(4405, 8)
|
|
4645
|
+
```
|
|
4646
|
+
|
|
4647
|
+
To re-sync by hand, or to look at the whole set:
|
|
4648
|
+
|
|
4649
|
+
```js
|
|
4650
|
+
const props = await client.queryAbProps({ force: true })
|
|
4651
|
+
|
|
4652
|
+
console.log(props.hash) // 'A1B2…' — rides on the next sync, asking for a delta
|
|
4653
|
+
console.log(props.refresh) // 86400 — seconds the server suggests waiting
|
|
4654
|
+
console.log(props.delta) // false — this reply was the whole table
|
|
4655
|
+
console.log(props.props) // { '4405': '16', '5010': 'true', … }
|
|
4656
|
+
console.log(props.sampling) // { '1200': 100, … } telemetry weights
|
|
4657
|
+
```
|
|
4658
|
+
|
|
4659
|
+
The hash is what turns the next request into a delta. A delta is **merged**
|
|
4660
|
+
onto what is already held rather than replacing it — otherwise every prop the
|
|
4661
|
+
server saw no reason to repeat would be dropped. The hash lives for the session
|
|
4662
|
+
only: a fresh process asks for the full set again, which is what a fresh
|
|
4663
|
+
install does.
|
|
4664
|
+
|
|
4665
|
+
Turn the automatic sync off if you do not want the extra IQ on a short session:
|
|
4666
|
+
|
|
4667
|
+
```js
|
|
4668
|
+
new WhalibmobClient({ sessionDir: './auth', syncAbPropsOnConnect: false })
|
|
4669
|
+
```
|
|
4670
|
+
|
|
4671
|
+
Being straight about the scope: nothing inside the library consumes these
|
|
4672
|
+
values yet. What you get today is the ability to see what the server thinks
|
|
4673
|
+
about an account — useful when one number behaves differently from another and
|
|
4674
|
+
you would otherwise be guessing — plus one more request a real client makes.
|
|
4675
|
+
|
|
4676
|
+
### Contact Sync
|
|
4677
|
+
|
|
4678
|
+
The address-book upload a phone does on first launch, and the delta syncs after
|
|
4679
|
+
it. It answers the question worth asking before a first message: **is this
|
|
4680
|
+
number on WhatsApp at all?**
|
|
4681
|
+
|
|
4682
|
+
```js
|
|
4683
|
+
const out = await client.syncContacts([
|
|
4684
|
+
'40712345678',
|
|
4685
|
+
'+55 11 91234-5678',
|
|
4686
|
+
'40799999999'
|
|
4687
|
+
])
|
|
4688
|
+
|
|
4689
|
+
out.registered // ['40712345678', '+55 11 91234-5678'] — on WhatsApp
|
|
4690
|
+
out.unregistered // ['40799999999'] — not on WhatsApp
|
|
4691
|
+
out.jids // { '40712345678': '40712345678@s.whatsapp.net', … }
|
|
4692
|
+
```
|
|
4693
|
+
|
|
4694
|
+
Numbers come back in the spelling you passed in, however they were written —
|
|
4695
|
+
spaces, dashes and a leading `+` are all normalised before sending and restored
|
|
4696
|
+
in the answer.
|
|
4697
|
+
|
|
4698
|
+
**A number the server did not answer about appears in neither list.** That is
|
|
4699
|
+
deliberate. Reporting a timeout as "not on WhatsApp" is the one wrong answer
|
|
4700
|
+
available here, because a caller acts on it — skipping the number, or deleting
|
|
4701
|
+
it. Silence is not a verdict:
|
|
4702
|
+
|
|
4703
|
+
```js
|
|
4704
|
+
const asked = phones.length
|
|
4705
|
+
const known = out.registered.length + out.unregistered.length
|
|
4706
|
+
if (known < asked) {
|
|
4707
|
+
console.log(`${asked - known} numbers went unanswered — try them again later`)
|
|
4708
|
+
}
|
|
4709
|
+
```
|
|
4710
|
+
|
|
4711
|
+
Large books are chunked automatically, and one unanswered chunk does not cost
|
|
4712
|
+
the others their answers:
|
|
4713
|
+
|
|
4714
|
+
```js
|
|
4715
|
+
await client.syncContacts(bigList, { chunkSize: 200 }) // default is 500
|
|
4716
|
+
```
|
|
4717
|
+
|
|
4718
|
+
The mode says what the sync claims about your address book. `full` is the
|
|
4719
|
+
whole book — what the app sends on first launch — and carries the
|
|
4720
|
+
`registration` context by default. `delta` is what changed since. `query` is a
|
|
4721
|
+
plain lookup that claims nothing:
|
|
4722
|
+
|
|
4723
|
+
```js
|
|
4724
|
+
await client.syncContacts(phones) // mode 'full', context 'registration'
|
|
4725
|
+
await client.syncContacts(phones, { mode: 'delta' }) // mode 'delta', context 'interactive'
|
|
4726
|
+
await client.syncContacts(phones, { mode: 'query' }) // a lookup, nothing claimed
|
|
4727
|
+
```
|
|
4728
|
+
|
|
4729
|
+
This is **not** called automatically on connect. Uploading an address book is
|
|
4730
|
+
your decision, not a side effect of connecting.
|
|
4731
|
+
|
|
4732
|
+
Where it earns its place is in front of a batch of first messages, together
|
|
4733
|
+
with the token path described under
|
|
4734
|
+
[tcToken](#tctoken--error-463-defense):
|
|
4735
|
+
|
|
4736
|
+
```js
|
|
4737
|
+
// 1. Which of these actually exist?
|
|
4738
|
+
const { registered } = await client.syncContacts(numbers)
|
|
4739
|
+
|
|
4740
|
+
// 2. Warm a trusted-contact token for each, so the first message carries one.
|
|
4741
|
+
for (const n of registered) {
|
|
4742
|
+
const jid = `${n.replace(/\D/g, '')}@s.whatsapp.net`
|
|
4743
|
+
await client.ensureTcTokenBeforeSend(jid, jid)
|
|
4744
|
+
await new Promise(r => setTimeout(r, 300))
|
|
4745
|
+
}
|
|
4746
|
+
|
|
4747
|
+
// 3. Send. No wasted sends to dead numbers, and no anonymous reach-outs.
|
|
4748
|
+
for (const n of registered) {
|
|
4749
|
+
await client.sendText(`${n.replace(/\D/g, '')}@s.whatsapp.net`, 'Hello')
|
|
4750
|
+
await new Promise(r => setTimeout(r, 1000))
|
|
4751
|
+
}
|
|
4752
|
+
```
|
|
4753
|
+
|
|
4754
|
+
### Terms-of-Service Notices
|
|
4755
|
+
|
|
4756
|
+
WhatsApp gates some surfaces on the account having accepted a given legal
|
|
4757
|
+
notice — a Terms update, a privacy-policy change, a regional disclosure. The
|
|
4758
|
+
app pulls the acceptance state, shows what is outstanding, and posts back what
|
|
4759
|
+
the user accepted.
|
|
4760
|
+
|
|
4761
|
+
```js
|
|
4762
|
+
const out = await client.queryTosNotices(['20230324', '20240101'])
|
|
4763
|
+
|
|
4764
|
+
out.refresh // 86400 — seconds the server suggests waiting before asking again
|
|
4765
|
+
out.notices // [ { id: '20230324', accepted: true },
|
|
4766
|
+
// { id: '20240101', accepted: false } ]
|
|
4767
|
+
|
|
4768
|
+
// Anything still outstanding:
|
|
4769
|
+
const pending = out.notices.filter(n => !n.accepted).map(n => n.id)
|
|
4770
|
+
if (pending.length) await client.acceptTosNotices(pending)
|
|
4771
|
+
```
|
|
4772
|
+
|
|
4773
|
+
Reading one back without asking again:
|
|
4774
|
+
|
|
4775
|
+
```js
|
|
4776
|
+
client.isTosAccepted('20230324') // true
|
|
4777
|
+
client.isTosAccepted('20240101') // false
|
|
4778
|
+
client.isTosAccepted('unknown-id') // null — this session has not asked
|
|
4779
|
+
```
|
|
4780
|
+
|
|
4781
|
+
`null` matters: it means *unknown*, not *not accepted*. Code deciding whether
|
|
4782
|
+
to accept something needs to be able to tell those apart.
|
|
4783
|
+
|
|
4784
|
+
Clearing an acceptance, so the server asks for it again:
|
|
4785
|
+
|
|
4786
|
+
```js
|
|
4787
|
+
await client.clearTosNotice('20230324')
|
|
4788
|
+
```
|
|
4789
|
+
|
|
4790
|
+
An acceptance made on another device arrives on its own, inside the same
|
|
4791
|
+
`account_sync` notification that carries device and privacy changes:
|
|
4792
|
+
|
|
4793
|
+
```js
|
|
4794
|
+
client.on('tos_notices', ({ notices }) => {
|
|
4795
|
+
for (const n of notices) {
|
|
4796
|
+
console.log(n.id, n.accepted ? 'accepted' : 'outstanding')
|
|
4797
|
+
}
|
|
4798
|
+
})
|
|
4799
|
+
```
|
|
4800
|
+
|
|
4801
|
+
A note on the wire, because it reads backwards from how it looks: the `state`
|
|
4802
|
+
attribute is present and `"false"` for a notice that has **not** been accepted,
|
|
4803
|
+
and **absent** for one that has. whalibmob reads it that way. If you parse these
|
|
4804
|
+
stanzas yourself, treating a missing attribute as "unknown" would report every
|
|
4805
|
+
accepted notice as outstanding.
|
|
4806
|
+
|
|
4807
|
+
Scope, honestly: no specific feature has been shown to be blocked by an
|
|
4808
|
+
unaccepted notice on a headless account. What this gives you is something to
|
|
4809
|
+
check rather than guess at — if a freshly registered number will not do
|
|
4810
|
+
something, you can now ask whether it has a notice pending.
|
|
4811
|
+
|
|
4608
4812
|
## Groups
|
|
4609
4813
|
|
|
4610
4814
|
### Create a Group
|
package/index.d.ts
CHANGED
|
@@ -131,6 +131,15 @@ export interface WhalibmobClientOptions {
|
|
|
131
131
|
* rest with `rejectCall()`.
|
|
132
132
|
*/
|
|
133
133
|
autoRejectCalls?: boolean;
|
|
134
|
+
/**
|
|
135
|
+
* Sync the account's AB props on every connect. Default `true`, because the
|
|
136
|
+
* app does it on every login and a client that never asks runs on guessed
|
|
137
|
+
* defaults wherever the server has an opinion.
|
|
138
|
+
*
|
|
139
|
+
* Turning it off saves one IQ per connect and costs nothing else: every
|
|
140
|
+
* reader of these has a fallback.
|
|
141
|
+
*/
|
|
142
|
+
syncAbPropsOnConnect?: boolean;
|
|
134
143
|
/** Refresh the announced build from the platform's store before every handshake. Default `true`. */
|
|
135
144
|
refreshVersion?: boolean;
|
|
136
145
|
/** `true` enables debug logging; an object is handed to `pino` as-is. */
|
|
@@ -478,6 +487,36 @@ export type PrivacyValue =
|
|
|
478
487
|
| 'on_standard'
|
|
479
488
|
| 'off';
|
|
480
489
|
|
|
490
|
+
export interface AbProps {
|
|
491
|
+
/** Hash of the returned set. Sent back on the next sync to ask for a delta. */
|
|
492
|
+
hash: string | null;
|
|
493
|
+
abKey: string | null;
|
|
494
|
+
/** Seconds the server suggests waiting before syncing again. */
|
|
495
|
+
refresh: number | null;
|
|
496
|
+
refreshId: string | null;
|
|
497
|
+
/** Whether this reply named only what changed, rather than the whole table. */
|
|
498
|
+
delta: boolean;
|
|
499
|
+
/** Experiment values, keyed by their numeric config code. */
|
|
500
|
+
props: { [configCode: string]: string };
|
|
501
|
+
/** Telemetry sampling weights, keyed by their numeric event code. */
|
|
502
|
+
sampling: { [eventCode: string]: number };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
export interface ContactSyncResult {
|
|
506
|
+
/** Numbers on WhatsApp, in the spelling they were passed in. */
|
|
507
|
+
registered: string[];
|
|
508
|
+
/** Numbers the server said are not on WhatsApp. */
|
|
509
|
+
unregistered: string[];
|
|
510
|
+
/** Each registered number mapped to the JID the server gave it. */
|
|
511
|
+
jids: { [phone: string]: string };
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
export interface TosNoticesResult {
|
|
515
|
+
/** Seconds the server suggests waiting before asking again. */
|
|
516
|
+
refresh: number | null;
|
|
517
|
+
notices: Array<{ id: string; accepted: boolean }>;
|
|
518
|
+
}
|
|
519
|
+
|
|
481
520
|
export interface PrivacySettings {
|
|
482
521
|
lastSeen: string | null;
|
|
483
522
|
profile: string | null;
|
|
@@ -820,6 +859,46 @@ export declare class WhalibmobClient extends EventEmitter {
|
|
|
820
859
|
|
|
821
860
|
// ─── Privacy ─────────────────────────────────────────────────────────────
|
|
822
861
|
queryPrivacySettings(opts?: { force?: boolean }): Promise<PrivacySettings>;
|
|
862
|
+
|
|
863
|
+
// ─── AB props ────────────────────────────────────────────────────────────
|
|
864
|
+
//
|
|
865
|
+
// The per-account feature flags the server hands out. Synced on connect
|
|
866
|
+
// unless `syncAbPropsOnConnect: false` was passed to the constructor, and
|
|
867
|
+
// held for the session — a fresh process asks for the full set again, which
|
|
868
|
+
// is what a fresh install does.
|
|
869
|
+
//
|
|
870
|
+
// Values are strings because that is what the wire carries; abPropInt and
|
|
871
|
+
// abPropBool are the two readings worth having, and both fall back rather
|
|
872
|
+
// than coerce.
|
|
873
|
+
queryAbProps(opts?: { force?: boolean; refreshId?: string }): Promise<AbProps>;
|
|
874
|
+
abProp(code: number | string, fallback?: string | null): string | null;
|
|
875
|
+
abPropInt(code: number | string, fallback?: number | null): number | null;
|
|
876
|
+
abPropBool(code: number | string, fallback?: boolean | null): boolean | null;
|
|
877
|
+
|
|
878
|
+
// ─── Contact sync ────────────────────────────────────────────────────────
|
|
879
|
+
//
|
|
880
|
+
// The address-book upload a phone does on first launch, and the delta syncs
|
|
881
|
+
// after it. Numbers the server did not answer about appear in neither list:
|
|
882
|
+
// silence is not a verdict, and reporting a timeout as "not on WhatsApp"
|
|
883
|
+
// is the one wrong answer available here.
|
|
884
|
+
syncContacts(
|
|
885
|
+
phones: string[],
|
|
886
|
+
opts?: {
|
|
887
|
+
mode?: 'full' | 'delta' | 'query';
|
|
888
|
+
context?: string;
|
|
889
|
+
chunkSize?: number;
|
|
890
|
+
}
|
|
891
|
+
): Promise<ContactSyncResult>;
|
|
892
|
+
|
|
893
|
+
// ─── Terms-of-Service notices ────────────────────────────────────────────
|
|
894
|
+
//
|
|
895
|
+
// `isTosAccepted` answers `null` for a notice this session has not asked
|
|
896
|
+
// about, which is not the same as "not accepted".
|
|
897
|
+
queryTosNotices(noticeIds: string[]): Promise<TosNoticesResult>;
|
|
898
|
+
acceptTosNotices(noticeIds: string[]): Promise<{ ok: true; raw: any }>;
|
|
899
|
+
clearTosNotice(noticeId: string): Promise<{ ok: true; raw: any }>;
|
|
900
|
+
isTosAccepted(noticeId: string): boolean | null;
|
|
901
|
+
|
|
823
902
|
changePrivacySetting(type: PrivacyType, value: PrivacyValue, excluded?: Jid[]): Promise<PrivacySettings>;
|
|
824
903
|
queryStatusPrivacy(): Promise<StatusPrivacyEntry[]>;
|
|
825
904
|
blockContact(jid: Jid): Promise<Jid[]>;
|
|
@@ -933,7 +1012,20 @@ export declare class WhalibmobClient extends EventEmitter {
|
|
|
933
1012
|
getLIDForPN(pn: Jid): Jid | null;
|
|
934
1013
|
|
|
935
1014
|
// ─── Lower level ─────────────────────────────────────────────────────────
|
|
936
|
-
|
|
1015
|
+
/**
|
|
1016
|
+
* Put a trusted-contact token on file *before* the next message is built, so
|
|
1017
|
+
* that message carries a `<tctoken>` like every later one.
|
|
1018
|
+
*
|
|
1019
|
+
* The token comes from the server, not from the contact: nothing here waits
|
|
1020
|
+
* on them replying, on having you in their address book, or on a conversation
|
|
1021
|
+
* existing. Called automatically by every send, so you rarely need it — reach
|
|
1022
|
+
* for it to warm a token ahead of time, before a burst of first messages.
|
|
1023
|
+
*
|
|
1024
|
+
* Returns whether a usable token is on file now. `false` is not a failure to
|
|
1025
|
+
* act on: the send goes ahead without one and the issuance continues in the
|
|
1026
|
+
* background for the next message.
|
|
1027
|
+
*/
|
|
1028
|
+
ensureTcTokenBeforeSend(tcJid: Jid, routingToJid: Jid): Promise<boolean>;
|
|
937
1029
|
sendPeerDataOperationMessage(pdo: any): Promise<any>;
|
|
938
1030
|
requestPlaceholderResend(messageKey: any, msgData?: any): Promise<any>;
|
|
939
1031
|
fetchMessageHistory(count: number, oldestMsgKey: any, oldestMsgTimestampMs: number): Promise<any>;
|
package/lib/Client.js
CHANGED
|
@@ -508,6 +508,18 @@ class WhalibmobClient extends EventEmitter {
|
|
|
508
508
|
// Cached privacy settings. Warmed after connect and refreshed whenever the
|
|
509
509
|
// server reports a change; markRead reads it.
|
|
510
510
|
this._privacySettings = null;
|
|
511
|
+
// The account's AB props, and the hash that turns the next sync into a
|
|
512
|
+
// delta. Both live for the session: a fresh process asks for the full set,
|
|
513
|
+
// which is what a fresh install does.
|
|
514
|
+
this._abProps = null;
|
|
515
|
+
this._abPropsHash = null;
|
|
516
|
+
// Whether to sync AB props on connect. On by default because the app does
|
|
517
|
+
// it on every login and a client that never does is running on guessed
|
|
518
|
+
// defaults, but it is one IQ a caller may not want on a short session.
|
|
519
|
+
this._syncAbPropsOnConnect = opts.syncAbPropsOnConnect !== false;
|
|
520
|
+
// noticeId → accepted. Absent means this session has not asked, which
|
|
521
|
+
// isTosAccepted reports as null rather than as "not accepted".
|
|
522
|
+
this._tosAccepted = new Map();
|
|
511
523
|
if (opts.pino !== undefined) _whaConfigLogger(opts.pino);
|
|
512
524
|
}
|
|
513
525
|
|
|
@@ -1702,6 +1714,16 @@ class WhalibmobClient extends EventEmitter {
|
|
|
1702
1714
|
this.queryPrivacySettings({ force: true }).catch(err =>
|
|
1703
1715
|
_whaDbg('[DBG] PRIVACY_FETCH_FAILED ' + (err && err.message)));
|
|
1704
1716
|
|
|
1717
|
+
// Sync the account's AB props, as the app does on every login. On a
|
|
1718
|
+
// reconnect the stored hash makes this a delta rather than the whole
|
|
1719
|
+
// table. A failure is logged and nothing else: every reader of these has a
|
|
1720
|
+
// default to fall back on, so a client that could not fetch them still
|
|
1721
|
+
// works — it just works on the defaults.
|
|
1722
|
+
if (this._syncAbPropsOnConnect) {
|
|
1723
|
+
this.queryAbProps({ force: true }).catch(err =>
|
|
1724
|
+
_whaDbg('[DBG] AB_PROPS_FETCH_FAILED ' + (err && err.message)));
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1705
1727
|
// ── Reconnect: flush stale device cache + background re-usync ─────────────
|
|
1706
1728
|
// On first connect _hasConnectedOnce is false — nothing extra to do.
|
|
1707
1729
|
// On every reconnect (network drop / NAT reset / server restart) we flush the
|
|
@@ -3821,6 +3843,11 @@ class WhalibmobClient extends EventEmitter {
|
|
|
3821
3843
|
// so a change made on the phone never reached the cache.
|
|
3822
3844
|
const privacyNode = findChildDeep(node, 'privacy');
|
|
3823
3845
|
if (privacyNode) this._handlePrivacySettingsNotification(privacyNode);
|
|
3846
|
+
// ...and Terms-of-Service acceptances. Accepting a notice on the phone
|
|
3847
|
+
// arrives here, and it is the only way this side learns about it without
|
|
3848
|
+
// asking.
|
|
3849
|
+
const tosNode = findChildDeep(node, 'tos');
|
|
3850
|
+
if (tosNode) this._handleTosNotification(tosNode);
|
|
3824
3851
|
}
|
|
3825
3852
|
|
|
3826
3853
|
if (type === 'w:gp2') {
|
|
@@ -6527,6 +6554,467 @@ class WhalibmobClient extends EventEmitter {
|
|
|
6527
6554
|
return this._sender.deleteMessage(origMsgId, chatJid, fromMe, forEveryone, opts);
|
|
6528
6555
|
}
|
|
6529
6556
|
|
|
6557
|
+
// ─── AB props — the server's own feature flags ────────────────────────────
|
|
6558
|
+
//
|
|
6559
|
+
// Every WhatsApp client asks the server which experiments and configuration
|
|
6560
|
+
// values apply to this account and then gates behaviour on the answer: how
|
|
6561
|
+
// many bytes an identity hash is truncated to, whether the LID migration has
|
|
6562
|
+
// started here, what sampling weight each telemetry event carries. A client
|
|
6563
|
+
// that never asks runs on guessed defaults wherever the server has an
|
|
6564
|
+
// opinion — and never asking is itself unlike the app, which asks on every
|
|
6565
|
+
// login.
|
|
6566
|
+
//
|
|
6567
|
+
// <iq xmlns="abt" to="s.whatsapp.net" type="get">
|
|
6568
|
+
// <props protocol="1" [hash="..."] [refresh_id="..."]/>
|
|
6569
|
+
// </iq>
|
|
6570
|
+
//
|
|
6571
|
+
// The reply carries a hash of the set it returned. Sending that hash back on
|
|
6572
|
+
// the next request asks for a delta rather than the whole table, which is
|
|
6573
|
+
// what the app does once it has synced. The hash is kept for the session
|
|
6574
|
+
// only — a fresh process asks for the full set, exactly as a fresh install
|
|
6575
|
+
// does — so nothing here has to be persisted or migrated.
|
|
6576
|
+
//
|
|
6577
|
+
// Values come back as strings because that is what the wire carries. Typing
|
|
6578
|
+
// them per prop would need a table this side does not have, and guessing
|
|
6579
|
+
// would be wrong more often than it is useful; `abPropInt` and `abPropBool`
|
|
6580
|
+
// are there for the two readings that are worth having.
|
|
6581
|
+
|
|
6582
|
+
/**
|
|
6583
|
+
* Sync this account's AB props.
|
|
6584
|
+
*
|
|
6585
|
+
* Served from the session cache unless `force` is set. A reply the server
|
|
6586
|
+
* marks `delta_update` is merged onto what is already held rather than
|
|
6587
|
+
* replacing it, which is the whole point of sending the hash.
|
|
6588
|
+
*
|
|
6589
|
+
* @param {object} [opts]
|
|
6590
|
+
* @param {boolean} [opts.force] re-ask even when a set is cached
|
|
6591
|
+
* @param {string} [opts.refreshId] the emergency-push branch. The server
|
|
6592
|
+
* announces a refresh id when a global update has to propagate at
|
|
6593
|
+
* once; sending it takes the place of the hash.
|
|
6594
|
+
* @returns {Promise<{hash: string|null, abKey: string|null,
|
|
6595
|
+
* refresh: number|null, refreshId: string|null,
|
|
6596
|
+
* delta: boolean, props: Object<string,string>,
|
|
6597
|
+
* sampling: Object<string,number>}>}
|
|
6598
|
+
*/
|
|
6599
|
+
async queryAbProps(opts) {
|
|
6600
|
+
opts = opts || {};
|
|
6601
|
+
if (this._abProps && !opts.force && opts.refreshId == null) return this._abProps;
|
|
6602
|
+
|
|
6603
|
+
const propsAttrs = { protocol: '1' };
|
|
6604
|
+
// One or the other, never both: a refresh id names a specific update to
|
|
6605
|
+
// fetch, and a hash asks for whatever has changed since it. Sending both
|
|
6606
|
+
// asks two different questions in one stanza.
|
|
6607
|
+
if (opts.refreshId != null) propsAttrs.refresh_id = String(opts.refreshId);
|
|
6608
|
+
else if (this._abPropsHash) propsAttrs.hash = this._abPropsHash;
|
|
6609
|
+
|
|
6610
|
+
const resp = await this._sendIq(new BinaryNode('iq', {
|
|
6611
|
+
id: this._genMsgId(),
|
|
6612
|
+
to: 's.whatsapp.net',
|
|
6613
|
+
type: 'get',
|
|
6614
|
+
xmlns: 'abt'
|
|
6615
|
+
}, [new BinaryNode('props', propsAttrs, null)]));
|
|
6616
|
+
|
|
6617
|
+
if (!resp) throw new Error('queryAbProps: no reply from server (IQ timed out)');
|
|
6618
|
+
if (resp.attrs && resp.attrs.type === 'error') {
|
|
6619
|
+
const errNode = findChild(resp, 'error');
|
|
6620
|
+
const code = errNode && errNode.attrs && errNode.attrs.code;
|
|
6621
|
+
throw new Error('queryAbProps: server rejected the request' +
|
|
6622
|
+
(code ? ' (' + code + ')' : ''));
|
|
6623
|
+
}
|
|
6624
|
+
|
|
6625
|
+
const parsed = this._parseAbProps(findChild(resp, 'props'));
|
|
6626
|
+
|
|
6627
|
+
// A delta names only what moved. Replacing the cache with it would drop
|
|
6628
|
+
// every prop the server saw no reason to repeat.
|
|
6629
|
+
if (parsed.delta && this._abProps) {
|
|
6630
|
+
parsed.props = Object.assign({}, this._abProps.props, parsed.props);
|
|
6631
|
+
parsed.sampling = Object.assign({}, this._abProps.sampling, parsed.sampling);
|
|
6632
|
+
}
|
|
6633
|
+
|
|
6634
|
+
if (parsed.hash) this._abPropsHash = parsed.hash;
|
|
6635
|
+
this._abProps = parsed;
|
|
6636
|
+
_whaDbg('[DBG] AB_PROPS ' + (parsed.delta ? 'delta' : 'full') +
|
|
6637
|
+
' props=' + Object.keys(parsed.props).length +
|
|
6638
|
+
' sampling=' + Object.keys(parsed.sampling).length +
|
|
6639
|
+
(parsed.hash ? ' hash=' + parsed.hash : ''));
|
|
6640
|
+
return parsed;
|
|
6641
|
+
}
|
|
6642
|
+
|
|
6643
|
+
// <props hash ab_key refresh refresh_id delta_update>
|
|
6644
|
+
// <prop config_code="..." config_value="..."/> an experiment value
|
|
6645
|
+
// <prop event_code="..." sampling_weight="..."/> a telemetry weight
|
|
6646
|
+
// </props>
|
|
6647
|
+
//
|
|
6648
|
+
// The two shapes share a tag and are told apart by which attributes they
|
|
6649
|
+
// carry, so each child is tried as an experiment first and as a sampling
|
|
6650
|
+
// entry second. A child that is neither is skipped rather than allowed to
|
|
6651
|
+
// fail the whole sync — an unknown shape is a prop this version has not
|
|
6652
|
+
// learned about, not a broken reply.
|
|
6653
|
+
_parseAbProps(propsNode) {
|
|
6654
|
+
const out = {
|
|
6655
|
+
hash: null,
|
|
6656
|
+
abKey: null,
|
|
6657
|
+
refresh: null,
|
|
6658
|
+
refreshId: null,
|
|
6659
|
+
delta: false,
|
|
6660
|
+
props: {},
|
|
6661
|
+
sampling: {}
|
|
6662
|
+
};
|
|
6663
|
+
if (!propsNode || !propsNode.attrs) return out;
|
|
6664
|
+
|
|
6665
|
+
const a = propsNode.attrs;
|
|
6666
|
+
out.hash = a.hash ? String(a.hash) : null;
|
|
6667
|
+
out.abKey = a.ab_key ? String(a.ab_key) : null;
|
|
6668
|
+
out.refreshId = a.refresh_id ? String(a.refresh_id) : null;
|
|
6669
|
+
|
|
6670
|
+
const refresh = Number(a.refresh);
|
|
6671
|
+
if (Number.isFinite(refresh) && refresh > 0) out.refresh = refresh;
|
|
6672
|
+
|
|
6673
|
+
// The server writes this as a string, and "false" is a true string.
|
|
6674
|
+
out.delta = a.delta_update === 'true' || a.delta_update === true || a.delta_update === '1';
|
|
6675
|
+
|
|
6676
|
+
const children = Array.isArray(propsNode.content) ? propsNode.content : [];
|
|
6677
|
+
for (const child of children) {
|
|
6678
|
+
if (!child || child.description !== 'prop' || !child.attrs) continue;
|
|
6679
|
+
const c = child.attrs;
|
|
6680
|
+
|
|
6681
|
+
if (c.config_code != null && c.config_value != null) {
|
|
6682
|
+
out.props[String(c.config_code)] = String(c.config_value);
|
|
6683
|
+
continue;
|
|
6684
|
+
}
|
|
6685
|
+
|
|
6686
|
+
if (c.event_code != null && c.sampling_weight != null) {
|
|
6687
|
+
const code = Number(c.event_code);
|
|
6688
|
+
const weight = Number(c.sampling_weight);
|
|
6689
|
+
// Bounds the app itself applies. A code below 1 names no event, and a
|
|
6690
|
+
// weight outside the range is not a weight — both mean this entry was
|
|
6691
|
+
// misread rather than that the server sent something exotic.
|
|
6692
|
+
if (Number.isFinite(code) && code >= 1 &&
|
|
6693
|
+
Number.isFinite(weight) && weight >= 0 && weight <= 1000000) {
|
|
6694
|
+
out.sampling[String(code)] = weight;
|
|
6695
|
+
}
|
|
6696
|
+
}
|
|
6697
|
+
}
|
|
6698
|
+
return out;
|
|
6699
|
+
}
|
|
6700
|
+
|
|
6701
|
+
/**
|
|
6702
|
+
* One AB prop as a string, or `fallback` when the server has not set it.
|
|
6703
|
+
*
|
|
6704
|
+
* Props are addressed by their numeric code, which is what the wire uses.
|
|
6705
|
+
* Reading one before `queryAbProps` has run returns the fallback rather than
|
|
6706
|
+
* throwing: a caller gating on a flag wants the default, not an exception.
|
|
6707
|
+
*
|
|
6708
|
+
* @param {number|string} code
|
|
6709
|
+
* @param {string|null} [fallback=null]
|
|
6710
|
+
* @returns {string|null}
|
|
6711
|
+
*/
|
|
6712
|
+
abProp(code, fallback) {
|
|
6713
|
+
const value = this._abProps && this._abProps.props[String(code)];
|
|
6714
|
+
return value === undefined || value === null ? (fallback === undefined ? null : fallback) : value;
|
|
6715
|
+
}
|
|
6716
|
+
|
|
6717
|
+
/** The same, read as an integer. Anything unparseable gives the fallback. */
|
|
6718
|
+
abPropInt(code, fallback) {
|
|
6719
|
+
const raw = this.abProp(code, null);
|
|
6720
|
+
const n = Number(raw);
|
|
6721
|
+
return raw === null || !Number.isFinite(n) ? (fallback === undefined ? null : fallback) : n;
|
|
6722
|
+
}
|
|
6723
|
+
|
|
6724
|
+
/**
|
|
6725
|
+
* The same, read as a flag.
|
|
6726
|
+
*
|
|
6727
|
+
* The wire spells these as "1"/"0" and as "true"/"false" depending on the
|
|
6728
|
+
* prop, so both are understood; anything else gives the fallback rather than
|
|
6729
|
+
* being coerced, since a prop carrying a version string is not a flag.
|
|
6730
|
+
*/
|
|
6731
|
+
abPropBool(code, fallback) {
|
|
6732
|
+
const raw = this.abProp(code, null);
|
|
6733
|
+
if (raw === '1' || raw === 'true') return true;
|
|
6734
|
+
if (raw === '0' || raw === 'false') return false;
|
|
6735
|
+
return fallback === undefined ? null : fallback;
|
|
6736
|
+
}
|
|
6737
|
+
|
|
6738
|
+
// ─── Contact sync — telling the server which numbers are in the book ──────
|
|
6739
|
+
//
|
|
6740
|
+
// The app uploads its address book on first launch and re-syncs deltas
|
|
6741
|
+
// afterwards, which is how it learns who is reachable and how the server
|
|
6742
|
+
// learns who this account knows. whalibmob has had the whole usync machinery
|
|
6743
|
+
// for device discovery all along and never used it for this, so an account
|
|
6744
|
+
// could send to a hundred strangers having never synced a single contact —
|
|
6745
|
+
// a shape nothing on a phone produces.
|
|
6746
|
+
//
|
|
6747
|
+
// This is the same <iq xmlns="usync"> the device path builds, with the
|
|
6748
|
+
// contact protocol on its own and the mode and context that name an address
|
|
6749
|
+
// book upload rather than a lookup.
|
|
6750
|
+
|
|
6751
|
+
/**
|
|
6752
|
+
* Sync contacts, and learn which of them are on WhatsApp.
|
|
6753
|
+
*
|
|
6754
|
+
* @param {string[]} phones E.164 numbers. A leading `+` is added when
|
|
6755
|
+
* missing, since that is the only form the contact protocol takes.
|
|
6756
|
+
* @param {object} [opts]
|
|
6757
|
+
* @param {'full'|'delta'|'query'} [opts.mode] `full` — the whole book, what
|
|
6758
|
+
* the app sends on first launch; `delta` — what changed since;
|
|
6759
|
+
* `query` — a lookup that claims nothing about the book. Defaults to
|
|
6760
|
+
* `full`.
|
|
6761
|
+
* @param {string} [opts.context] defaults to `registration` for a full
|
|
6762
|
+
* sync (the app's own first-launch context) and `interactive`
|
|
6763
|
+
* otherwise.
|
|
6764
|
+
* @param {number} [opts.chunkSize=500] numbers per IQ.
|
|
6765
|
+
* @returns {Promise<{registered: string[], unregistered: string[],
|
|
6766
|
+
* jids: Object<string,string>}>}
|
|
6767
|
+
* `registered` and `unregistered` hold the numbers as passed in;
|
|
6768
|
+
* `jids` maps each registered number to the JID the server gave it.
|
|
6769
|
+
*/
|
|
6770
|
+
async syncContacts(phones, opts) {
|
|
6771
|
+
if (!this._connected) throw new Error('Not connected');
|
|
6772
|
+
// _devMgr is torn down on close and rebuilt on init. Saying so beats the
|
|
6773
|
+
// null dereference a caller would otherwise get from a session that is
|
|
6774
|
+
// connected but has been through a teardown.
|
|
6775
|
+
if (!this._devMgr) throw new Error('syncContacts: no device manager — the session is not initialised');
|
|
6776
|
+
if (!Array.isArray(phones)) throw new Error('syncContacts: phones must be an array');
|
|
6777
|
+
|
|
6778
|
+
opts = opts || {};
|
|
6779
|
+
const mode = opts.mode || 'full';
|
|
6780
|
+
const context = opts.context || (mode === 'full' ? 'registration' : 'interactive');
|
|
6781
|
+
const chunkSize = Number(opts.chunkSize) > 0 ? Number(opts.chunkSize) : 500;
|
|
6782
|
+
|
|
6783
|
+
const { USyncQuery, USyncUser } = require('./WAUSync');
|
|
6784
|
+
|
|
6785
|
+
// Normalise once, and keep the caller's spelling to answer in. Duplicates
|
|
6786
|
+
// are dropped here rather than sent: the server counts what it is asked
|
|
6787
|
+
// about, and asking twice about one number in one book is not what a
|
|
6788
|
+
// phone does.
|
|
6789
|
+
const wanted = [];
|
|
6790
|
+
const seen = new Set();
|
|
6791
|
+
for (const raw of phones) {
|
|
6792
|
+
const digits = String(raw || '').replace(/[^\d]/g, '');
|
|
6793
|
+
if (!digits || seen.has(digits)) continue;
|
|
6794
|
+
seen.add(digits);
|
|
6795
|
+
wanted.push({ input: String(raw), digits });
|
|
6796
|
+
}
|
|
6797
|
+
|
|
6798
|
+
const out = { registered: [], unregistered: [], jids: {} };
|
|
6799
|
+
if (wanted.length === 0) return out;
|
|
6800
|
+
|
|
6801
|
+
for (let i = 0; i < wanted.length; i += chunkSize) {
|
|
6802
|
+
const chunk = wanted.slice(i, i + chunkSize);
|
|
6803
|
+
|
|
6804
|
+
const query = new USyncQuery()
|
|
6805
|
+
.withMode(mode)
|
|
6806
|
+
.withContext(context)
|
|
6807
|
+
.withContactProtocol();
|
|
6808
|
+
for (const c of chunk) query.withUser(new USyncUser().withPhone('+' + c.digits));
|
|
6809
|
+
|
|
6810
|
+
const parsed = await this._devMgr.executeUSyncQuery(query);
|
|
6811
|
+
|
|
6812
|
+
// A chunk the server did not answer leaves its numbers unclassified
|
|
6813
|
+
// rather than marked absent. Reporting "not on WhatsApp" for a timeout
|
|
6814
|
+
// is the one wrong answer here — a caller acts on it.
|
|
6815
|
+
if (!parsed || !Array.isArray(parsed.list)) {
|
|
6816
|
+
_whaDbg('[DBG] CONTACT_SYNC chunk ' + (i / chunkSize) + ' unanswered — ' +
|
|
6817
|
+
chunk.length + ' numbers left unclassified');
|
|
6818
|
+
continue;
|
|
6819
|
+
}
|
|
6820
|
+
|
|
6821
|
+
// The reply is keyed by JID, so match each entry back to the number it
|
|
6822
|
+
// came from by the JID's user part.
|
|
6823
|
+
const byUser = new Map();
|
|
6824
|
+
for (const entry of parsed.list) {
|
|
6825
|
+
if (!entry || !entry.id) continue;
|
|
6826
|
+
const user = String(entry.id).split('@')[0].split(':')[0];
|
|
6827
|
+
byUser.set(user, entry);
|
|
6828
|
+
}
|
|
6829
|
+
|
|
6830
|
+
for (const c of chunk) {
|
|
6831
|
+
const entry = byUser.get(c.digits);
|
|
6832
|
+
if (!entry) continue; // unclassified, as above
|
|
6833
|
+
if (entry.contact === true) {
|
|
6834
|
+
out.registered.push(c.input);
|
|
6835
|
+
out.jids[c.input] = String(entry.id);
|
|
6836
|
+
} else {
|
|
6837
|
+
out.unregistered.push(c.input);
|
|
6838
|
+
}
|
|
6839
|
+
}
|
|
6840
|
+
}
|
|
6841
|
+
|
|
6842
|
+
_whaDbg('[DBG] CONTACT_SYNC mode=' + mode + ' context=' + context +
|
|
6843
|
+
' asked=' + wanted.length + ' on=' + out.registered.length +
|
|
6844
|
+
' off=' + out.unregistered.length);
|
|
6845
|
+
return out;
|
|
6846
|
+
}
|
|
6847
|
+
|
|
6848
|
+
// ─── Terms-of-Service notices ─────────────────────────────────────────────
|
|
6849
|
+
//
|
|
6850
|
+
// WhatsApp gates some surfaces on the account having accepted a given legal
|
|
6851
|
+
// notice — a Terms update, a privacy-policy change, a regional disclosure.
|
|
6852
|
+
// The app pulls the acceptance state, shows the ones that are outstanding,
|
|
6853
|
+
// and posts back what the user accepted.
|
|
6854
|
+
//
|
|
6855
|
+
// read <iq xmlns="tos" type="get"><request><notice id="..."/></request></iq>
|
|
6856
|
+
// accept <iq xmlns="tos" type="set"><request type="session_update">
|
|
6857
|
+
// <notice id="..."/></request></iq>
|
|
6858
|
+
// clear <iq xmlns="tos" type="set"><delete id="..."/></iq>
|
|
6859
|
+
//
|
|
6860
|
+
// The reply's `state` attribute reads backwards from how it looks: it is
|
|
6861
|
+
// present and "false" for a notice that has NOT been accepted, and absent
|
|
6862
|
+
// for one that has. Treating a missing attribute as "unknown" would report
|
|
6863
|
+
// every accepted notice as outstanding.
|
|
6864
|
+
|
|
6865
|
+
/**
|
|
6866
|
+
* Read the acceptance state of one or more notices.
|
|
6867
|
+
*
|
|
6868
|
+
* @param {string[]} noticeIds
|
|
6869
|
+
* @returns {Promise<{refresh: number|null,
|
|
6870
|
+
* notices: Array<{id: string, accepted: boolean}>}>}
|
|
6871
|
+
* `refresh` is how many seconds the server suggests waiting before
|
|
6872
|
+
* asking again.
|
|
6873
|
+
*/
|
|
6874
|
+
async queryTosNotices(noticeIds) {
|
|
6875
|
+
if (!this._connected) throw new Error('Not connected');
|
|
6876
|
+
if (!Array.isArray(noticeIds)) throw new Error('queryTosNotices: noticeIds must be an array');
|
|
6877
|
+
|
|
6878
|
+
const ids = noticeIds.map(id => String(id)).filter(Boolean);
|
|
6879
|
+
const resp = await this._sendIq(new BinaryNode('iq', {
|
|
6880
|
+
id: this._genMsgId(),
|
|
6881
|
+
to: 's.whatsapp.net',
|
|
6882
|
+
type: 'get',
|
|
6883
|
+
xmlns: 'tos'
|
|
6884
|
+
}, [new BinaryNode('request', {},
|
|
6885
|
+
ids.map(id => new BinaryNode('notice', { id }, null)))]));
|
|
6886
|
+
|
|
6887
|
+
if (!resp) throw new Error('queryTosNotices: no reply from server (IQ timed out)');
|
|
6888
|
+
if (resp.attrs && resp.attrs.type === 'error') {
|
|
6889
|
+
const errNode = findChild(resp, 'error');
|
|
6890
|
+
const code = errNode && errNode.attrs && errNode.attrs.code;
|
|
6891
|
+
throw new Error('queryTosNotices: server rejected the request' +
|
|
6892
|
+
(code ? ' (' + code + ')' : ''));
|
|
6893
|
+
}
|
|
6894
|
+
|
|
6895
|
+
const parsed = this._parseTosNotices(findChild(resp, 'tos'));
|
|
6896
|
+
for (const n of parsed.notices) this._tosAccepted.set(n.id, n.accepted);
|
|
6897
|
+
_whaDbg('[DBG] TOS_QUERY asked=' + ids.length + ' answered=' + parsed.notices.length +
|
|
6898
|
+
' outstanding=' + parsed.notices.filter(n => !n.accepted).length);
|
|
6899
|
+
return parsed;
|
|
6900
|
+
}
|
|
6901
|
+
|
|
6902
|
+
// <tos refresh="86400"><notice id="..." [state="false"]/>...</tos>
|
|
6903
|
+
_parseTosNotices(tosNode) {
|
|
6904
|
+
const out = { refresh: null, notices: [] };
|
|
6905
|
+
if (!tosNode) return out;
|
|
6906
|
+
|
|
6907
|
+
const refresh = Number(tosNode.attrs && tosNode.attrs.refresh);
|
|
6908
|
+
if (Number.isFinite(refresh) && refresh > 0) out.refresh = refresh;
|
|
6909
|
+
|
|
6910
|
+
const children = Array.isArray(tosNode.content) ? tosNode.content : [];
|
|
6911
|
+
for (const child of children) {
|
|
6912
|
+
if (!child || child.description !== 'notice' || !child.attrs || !child.attrs.id) continue;
|
|
6913
|
+
const state = child.attrs.state;
|
|
6914
|
+
// Only an explicit "false" means not accepted; see the note above.
|
|
6915
|
+
out.notices.push({
|
|
6916
|
+
id: String(child.attrs.id),
|
|
6917
|
+
accepted: !(state === 'false' || state === false)
|
|
6918
|
+
});
|
|
6919
|
+
}
|
|
6920
|
+
return out;
|
|
6921
|
+
}
|
|
6922
|
+
|
|
6923
|
+
/**
|
|
6924
|
+
* Accept one or more notices.
|
|
6925
|
+
*
|
|
6926
|
+
* @param {string[]} noticeIds
|
|
6927
|
+
* @returns {Promise<{ok: true, raw: any}>}
|
|
6928
|
+
*/
|
|
6929
|
+
async acceptTosNotices(noticeIds) {
|
|
6930
|
+
if (!this._connected) throw new Error('Not connected');
|
|
6931
|
+
if (!Array.isArray(noticeIds) || noticeIds.length === 0) {
|
|
6932
|
+
throw new Error('acceptTosNotices: noticeIds must be a non-empty array');
|
|
6933
|
+
}
|
|
6934
|
+
|
|
6935
|
+
const ids = noticeIds.map(id => String(id)).filter(Boolean);
|
|
6936
|
+
const resp = await this._sendIq(new BinaryNode('iq', {
|
|
6937
|
+
id: this._genMsgId(),
|
|
6938
|
+
to: 's.whatsapp.net',
|
|
6939
|
+
type: 'set',
|
|
6940
|
+
xmlns: 'tos'
|
|
6941
|
+
}, [new BinaryNode('request', { type: 'session_update' },
|
|
6942
|
+
ids.map(id => new BinaryNode('notice', { id }, null)))]));
|
|
6943
|
+
|
|
6944
|
+
if (!resp) throw new Error('acceptTosNotices: no reply from server (IQ timed out)');
|
|
6945
|
+
if (resp.attrs && resp.attrs.type === 'error') {
|
|
6946
|
+
const errNode = findChild(resp, 'error');
|
|
6947
|
+
const code = errNode && errNode.attrs && errNode.attrs.code;
|
|
6948
|
+
throw new Error('acceptTosNotices: server rejected the request' +
|
|
6949
|
+
(code ? ' (' + code + ')' : ''));
|
|
6950
|
+
}
|
|
6951
|
+
|
|
6952
|
+
for (const id of ids) this._tosAccepted.set(id, true);
|
|
6953
|
+
_whaDbg('[DBG] TOS_ACCEPT ' + ids.join(','));
|
|
6954
|
+
return { ok: true, raw: resp };
|
|
6955
|
+
}
|
|
6956
|
+
|
|
6957
|
+
/**
|
|
6958
|
+
* Clear the accepted state of one notice, so the server asks for it again.
|
|
6959
|
+
*
|
|
6960
|
+
* @param {string} noticeId
|
|
6961
|
+
* @returns {Promise<{ok: true, raw: any}>}
|
|
6962
|
+
*/
|
|
6963
|
+
async clearTosNotice(noticeId) {
|
|
6964
|
+
if (!this._connected) throw new Error('Not connected');
|
|
6965
|
+
const id = String(noticeId || '');
|
|
6966
|
+
if (!id) throw new Error('clearTosNotice: noticeId is required');
|
|
6967
|
+
|
|
6968
|
+
const resp = await this._sendIq(new BinaryNode('iq', {
|
|
6969
|
+
id: this._genMsgId(),
|
|
6970
|
+
to: 's.whatsapp.net',
|
|
6971
|
+
type: 'set',
|
|
6972
|
+
xmlns: 'tos'
|
|
6973
|
+
}, [new BinaryNode('delete', { id }, null)]));
|
|
6974
|
+
|
|
6975
|
+
if (!resp) throw new Error('clearTosNotice: no reply from server (IQ timed out)');
|
|
6976
|
+
if (resp.attrs && resp.attrs.type === 'error') {
|
|
6977
|
+
const errNode = findChild(resp, 'error');
|
|
6978
|
+
const code = errNode && errNode.attrs && errNode.attrs.code;
|
|
6979
|
+
throw new Error('clearTosNotice: server rejected the request' +
|
|
6980
|
+
(code ? ' (' + code + ')' : ''));
|
|
6981
|
+
}
|
|
6982
|
+
|
|
6983
|
+
this._tosAccepted.set(id, false);
|
|
6984
|
+
_whaDbg('[DBG] TOS_CLEAR ' + id);
|
|
6985
|
+
return { ok: true, raw: resp };
|
|
6986
|
+
}
|
|
6987
|
+
|
|
6988
|
+
/**
|
|
6989
|
+
* Whether a notice is known to have been accepted.
|
|
6990
|
+
*
|
|
6991
|
+
* `null` means this session has not asked about it — which is not the same
|
|
6992
|
+
* as "not accepted", and a caller deciding whether to prompt needs to be
|
|
6993
|
+
* able to tell those apart.
|
|
6994
|
+
*
|
|
6995
|
+
* @param {string} noticeId
|
|
6996
|
+
* @returns {boolean|null}
|
|
6997
|
+
*/
|
|
6998
|
+
isTosAccepted(noticeId) {
|
|
6999
|
+
const known = this._tosAccepted.get(String(noticeId));
|
|
7000
|
+
return known === undefined ? null : known;
|
|
7001
|
+
}
|
|
7002
|
+
|
|
7003
|
+
// The server pushing a notice's state at us, rather than us asking:
|
|
7004
|
+
// <notification type="account_sync"><tos><notice id=".." state=".."/></tos>
|
|
7005
|
+
//
|
|
7006
|
+
// This is how an acceptance made on another device reaches this one. It only
|
|
7007
|
+
// ever adds to what is known — a push naming three notices says nothing
|
|
7008
|
+
// about a fourth.
|
|
7009
|
+
_handleTosNotification(tosNode) {
|
|
7010
|
+
const parsed = this._parseTosNotices(tosNode);
|
|
7011
|
+
if (parsed.notices.length === 0) return;
|
|
7012
|
+
for (const n of parsed.notices) this._tosAccepted.set(n.id, n.accepted);
|
|
7013
|
+
_whaDbg('[DBG] TOS_NOTIFICATION ' + parsed.notices
|
|
7014
|
+
.map(n => n.id + '=' + (n.accepted ? 'accepted' : 'outstanding')).join(' '));
|
|
7015
|
+
this.emit('tos_notices', { notices: parsed.notices });
|
|
7016
|
+
}
|
|
7017
|
+
|
|
6530
7018
|
// ─── Status / Stories ─────────────────────────────────────────────────────
|
|
6531
7019
|
|
|
6532
7020
|
// Who your Status posts go to.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whalibmob",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.29.0",
|
|
4
4
|
"description": "Node.js library for WhatsApp — register a number over SMS, or link as a companion by QR. Signal E2E encryption, media, groups, channels.",
|
|
5
5
|
"author": "Kunboruto20",
|
|
6
6
|
"main": "index.js",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"messaging",
|
|
50
50
|
"chatbot",
|
|
51
51
|
"automation",
|
|
52
|
-
"whatsapp-mobile",
|
|
52
|
+
"whatsapp-mobile",
|
|
53
53
|
"media",
|
|
54
54
|
"groups",
|
|
55
55
|
"channels"
|