whalibmob 5.29.1 → 5.29.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 +69 -7
- package/index.d.ts +134 -3
- package/lib/Client.js +449 -96
- package/lib/DeviceManager.js +102 -36
- package/lib/MediaService.js +78 -13
- package/lib/messages/MessageSender.js +4 -4
- package/lib/proto/MessageProto.js +119 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2473,6 +2473,7 @@ Run it once, type the code into the phone, and it is linked. Run it again and it
|
|
|
2473
2473
|
| `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. |
|
|
2474
2474
|
| `client.requestPairingCode(phone?, customCode?)` | Ask for an 8-character code. Returns it immediately; the link completes later. Throws if the session is already linked. |
|
|
2475
2475
|
| `client.disconnect()` | Close the connection. The link survives — reconnect with `connectWeb()`. |
|
|
2476
|
+
| `client.logout()` | Unlink this device from the account, the way the Linked Devices screen does, then disconnect. The link does **not** survive — the next `connectWeb()` pairs afresh. Nothing on disk is deleted; remove the session directory yourself if you want the keys gone. |
|
|
2476
2477
|
|
|
2477
2478
|
`connectWeb(phone, opts)` options:
|
|
2478
2479
|
|
|
@@ -3284,7 +3285,7 @@ connect()
|
|
|
3284
3285
|
| `presence` | `{ from, available }` | Contact came online or went offline |
|
|
3285
3286
|
| `group_update` | `{ type, groupJid, actor, participants, subject, timestamp }` | Member added / removed / promoted / demoted, subject or settings changed |
|
|
3286
3287
|
| `notification` | node object | Group or contact update notification |
|
|
3287
|
-
| `call` | `{ from }` |
|
|
3288
|
+
| `call` | `{ from, id, status, tag, creator, creatorAlt, groupJid, platform, version, reason, ts, node }` | A call stanza. `id` is what `rejectCall()` needs, `creator` is who placed the call (it differs from `from` in a group), and `status` is the stage: `offer`, `offer_notice`, `ringing`, `accept`, `preaccept`, `transport`, `terminate`, `reject` or `unknown` |
|
|
3288
3289
|
| `chat_read` | `{ jid, read, remote?, synced? }` | Chat marked read (`read: true`) or unread (`read: false`) |
|
|
3289
3290
|
| `chat_muted` | `{ jid, muted, until, remote?, synced? }` | Chat muted or unmuted; `until` is epoch ms (−1 = indefinite) |
|
|
3290
3291
|
| `chat_pinned` | `{ jid, pinned, remote?, synced? }` | Chat pinned or unpinned |
|
|
@@ -3322,9 +3323,21 @@ The message object contains:
|
|
|
3322
3323
|
ts: number, // Unix timestamp (seconds)
|
|
3323
3324
|
node: object, // raw XML node — node.attrs.sender_pn holds the real phone JID
|
|
3324
3325
|
decoded: object, // structured payload — shape depends on message type (see below)
|
|
3326
|
+
fromMe: boolean, // you sent this from another of your own devices
|
|
3327
|
+
chat: string, // the conversation — equals from unless fromMe is true
|
|
3325
3328
|
}
|
|
3326
3329
|
```
|
|
3327
3330
|
|
|
3331
|
+
> [!NOTE]
|
|
3332
|
+
> When you send a message from your phone, WhatsApp echoes a copy of it to every
|
|
3333
|
+
> other device on the account, wrapped in a `DeviceSentMessage`. On that copy
|
|
3334
|
+
> `from` is **your own JID** — it names the sender, not the chat. Use `chat`,
|
|
3335
|
+
> which is the conversation whichever direction the message went, and `fromMe`
|
|
3336
|
+
> to tell the two apart. The envelope's own fields are on
|
|
3337
|
+
> `decoded.deviceSentMeta` (`destinationJid`, `phash`) if you need them raw.
|
|
3338
|
+
> These messages are not answered with a read receipt: nothing was read by
|
|
3339
|
+
> receiving your own message back.
|
|
3340
|
+
|
|
3328
3341
|
> [!NOTE]
|
|
3329
3342
|
> WhatsApp Multi-Device uses **LID JIDs** internally. The `from` field may be a LID like
|
|
3330
3343
|
> `112345678901234@s.whatsapp.net` rather than the real phone number. To get the actual
|
|
@@ -3766,13 +3779,36 @@ The whole message object is accepted as well as its `decoded` half, so
|
|
|
3766
3779
|
|
|
3767
3780
|
**Verifying the file**
|
|
3768
3781
|
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3782
|
+
Three things are checked, and all three by default — the message says what the
|
|
3783
|
+
file should be, so there is no reason to take the CDN's word for it:
|
|
3784
|
+
|
|
3785
|
+
| check | what it covers |
|
|
3786
|
+
|---|---|
|
|
3787
|
+
| `fileEncSha256` | the blob as it arrived, before any work is spent decrypting it |
|
|
3788
|
+
| the MAC | the ciphertext, under a key derived from `mediaKey` — this is the one that authenticates |
|
|
3789
|
+
| `fileSha256` | the decrypted file |
|
|
3773
3790
|
|
|
3774
3791
|
```js
|
|
3775
|
-
const bytes = await client.downloadMedia(d
|
|
3792
|
+
const bytes = await client.downloadMedia(d) // all three
|
|
3793
|
+
const raw = await client.downloadMedia(d, { verify: false }) // MAC only
|
|
3794
|
+
```
|
|
3795
|
+
|
|
3796
|
+
`verify: false` skips the two digests for a caller that wants the bytes whatever
|
|
3797
|
+
they are. The MAC is checked either way and is never optional.
|
|
3798
|
+
|
|
3799
|
+
**Link preview thumbnails**
|
|
3800
|
+
|
|
3801
|
+
A text message that quoted a link carries the preview the sender's client built.
|
|
3802
|
+
The small inline image is already on the message as `jpegThumbnail` and needs no
|
|
3803
|
+
download. The full-size one is a media file of its own — its own `directPath`,
|
|
3804
|
+
its own `mediaKey`, encrypted under a key name of its own — so `downloadMedia`
|
|
3805
|
+
cannot reach it:
|
|
3806
|
+
|
|
3807
|
+
```js
|
|
3808
|
+
if (d.thumbnail) {
|
|
3809
|
+
const preview = await client.downloadThumbnail(d)
|
|
3810
|
+
// d.title, d.description and d.matchedText are the rest of the preview
|
|
3811
|
+
}
|
|
3776
3812
|
```
|
|
3777
3813
|
|
|
3778
3814
|
**What happens underneath**
|
|
@@ -3979,10 +4015,36 @@ const { id, encKey } = await client.sendPoll(
|
|
|
3979
4015
|
['JavaScript', 'Python', 'Rust'],
|
|
3980
4016
|
1 // voters may pick 1 option (0 = unlimited)
|
|
3981
4017
|
)
|
|
3982
|
-
// encKey (32-byte Buffer) is
|
|
4018
|
+
// encKey (32-byte Buffer) is what reads the votes — keep it
|
|
3983
4019
|
// (also returned as `messageSecret`, which is the name the protocol uses)
|
|
3984
4020
|
```
|
|
3985
4021
|
|
|
4022
|
+
**Reading the votes**
|
|
4023
|
+
|
|
4024
|
+
A vote arrives as its own message and the ballot inside it is encrypted, so
|
|
4025
|
+
nothing about who chose what is visible in the stanza. `decryptPollVote()` opens
|
|
4026
|
+
it with the poll's `encKey`:
|
|
4027
|
+
|
|
4028
|
+
```js
|
|
4029
|
+
const options = ['JavaScript', 'Python', 'Rust']
|
|
4030
|
+
|
|
4031
|
+
client.on('message', (msg) => {
|
|
4032
|
+
if (msg.decoded?.type !== 'pollVote') return
|
|
4033
|
+
if (msg.decoded.pollKey.id !== id) return // a vote on some other poll
|
|
4034
|
+
|
|
4035
|
+
const { selected, votedAt } = client.decryptPollVote(msg, { encKey, options })
|
|
4036
|
+
console.log(msg.participant, 'voted for', selected) // [ 'Rust' ]
|
|
4037
|
+
})
|
|
4038
|
+
```
|
|
4039
|
+
|
|
4040
|
+
Pass `options` and the choices come back as text. Without them you get
|
|
4041
|
+
`selectedHashes` — the SHA-256 of each chosen option, which is how the ballot
|
|
4042
|
+
names them on the wire — and match them yourself.
|
|
4043
|
+
|
|
4044
|
+
The key is derived from the poll id, the poll's sender and the voter, so a vote
|
|
4045
|
+
on someone else's poll needs their `encKey`, which only arrives if they sent the
|
|
4046
|
+
poll to you: read it off `msg.decoded.encKey` on the poll message itself.
|
|
4047
|
+
|
|
3986
4048
|
### Quoted Reply
|
|
3987
4049
|
|
|
3988
4050
|
Send a text message that quotes (replies to) a specific earlier message. The recipient sees the original message highlighted above your reply.
|
package/index.d.ts
CHANGED
|
@@ -271,9 +271,56 @@ export interface DecodedBase {
|
|
|
271
271
|
ephemeral?: boolean;
|
|
272
272
|
/** The message is the new text of an edit. */
|
|
273
273
|
edited?: boolean;
|
|
274
|
+
/**
|
|
275
|
+
* On a text message that quoted a link, the downloadable preview image.
|
|
276
|
+
* Fetch it with `downloadThumbnail()`.
|
|
277
|
+
*/
|
|
278
|
+
thumbnail?: LinkThumbnail;
|
|
279
|
+
/**
|
|
280
|
+
* Present only on a message this account sent from one of its other devices.
|
|
281
|
+
* The stanza names us as the sender, so the chat it belongs to is written
|
|
282
|
+
* here and nowhere else.
|
|
283
|
+
*/
|
|
284
|
+
deviceSentMeta?: DeviceSentMeta;
|
|
274
285
|
[key: string]: any;
|
|
275
286
|
}
|
|
276
287
|
|
|
288
|
+
/** The downloadable preview image on a message that quoted a link. */
|
|
289
|
+
export interface LinkThumbnail {
|
|
290
|
+
directPath: string;
|
|
291
|
+
mediaKey: Buffer;
|
|
292
|
+
thumbnailSha256: Buffer | null;
|
|
293
|
+
thumbnailEncSha256: Buffer | null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export interface PollVoteOptions {
|
|
297
|
+
/** The poll's 32-byte message secret — what `sendPoll` returned. */
|
|
298
|
+
encKey: Buffer;
|
|
299
|
+
/** Who cast the vote. Defaults to the message's `participant`, then `from`. */
|
|
300
|
+
voter?: Jid;
|
|
301
|
+
/** Who sent the poll. Defaults to what the vote's message key names. */
|
|
302
|
+
pollSender?: Jid;
|
|
303
|
+
/** The poll's options, so the choices come back as text rather than hashes. */
|
|
304
|
+
options?: string[];
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export interface PollVoteResult {
|
|
308
|
+
/** The chosen options as text. Empty unless `options` was passed. */
|
|
309
|
+
selected: string[];
|
|
310
|
+
/** SHA-256 of each chosen option, always present. */
|
|
311
|
+
selectedHashes: Buffer[];
|
|
312
|
+
/** When the vote was cast, in ms, when the sender said. */
|
|
313
|
+
votedAt: number | null;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** What a `DeviceSentMessage` envelope said about the message inside it. */
|
|
317
|
+
export interface DeviceSentMeta {
|
|
318
|
+
/** The chat the message was originally addressed to. */
|
|
319
|
+
destinationJid?: Jid;
|
|
320
|
+
/** Hash of the participant list the sending device used, when it set one. */
|
|
321
|
+
phash?: string;
|
|
322
|
+
}
|
|
323
|
+
|
|
277
324
|
export interface DecodedText extends DecodedBase {
|
|
278
325
|
type: 'text';
|
|
279
326
|
text: string;
|
|
@@ -335,6 +382,39 @@ export type DecodedMessage =
|
|
|
335
382
|
| DecodedProtocol
|
|
336
383
|
| DecodedBase;
|
|
337
384
|
|
|
385
|
+
/** The stage a call is at, from the child tag inside the `<call>` stanza. */
|
|
386
|
+
export type CallStatus =
|
|
387
|
+
| 'offer' | 'offer_notice' | 'ringing' | 'accept'
|
|
388
|
+
| 'preaccept' | 'transport' | 'terminate' | 'reject' | 'unknown';
|
|
389
|
+
|
|
390
|
+
/** The payload of the `call` event. */
|
|
391
|
+
export interface CallEvent {
|
|
392
|
+
/** Who the stanza came from. Equals `creator` on a one-to-one call. */
|
|
393
|
+
from: Jid;
|
|
394
|
+
/** The call id — what `rejectCall` needs. `null` on a stanza that names none. */
|
|
395
|
+
id: string | null;
|
|
396
|
+
/** `'ringing'` is the `relaylatency` stanza, under the name it has always had. */
|
|
397
|
+
status: CallStatus;
|
|
398
|
+
/** The child tag verbatim, so a stage not in `CallStatus` is still readable. */
|
|
399
|
+
tag: string;
|
|
400
|
+
/** The account that placed the call. Differs from `from` in a group. */
|
|
401
|
+
creator: Jid;
|
|
402
|
+
/** The creator's other name — phone JID for a LID creator, and the reverse. */
|
|
403
|
+
creatorAlt: Jid | null;
|
|
404
|
+
/** The group the call is in, when it is a group call. */
|
|
405
|
+
groupJid: Jid | null;
|
|
406
|
+
/** The caller's platform, on the stanzas that carry it. */
|
|
407
|
+
platform: string | null;
|
|
408
|
+
/** The caller's client version, on the stanzas that carry it. */
|
|
409
|
+
version: string | null;
|
|
410
|
+
/** Why the call ended, on a `terminate`. */
|
|
411
|
+
reason: string | null;
|
|
412
|
+
/** Unix timestamp, seconds. */
|
|
413
|
+
ts: number;
|
|
414
|
+
/** Raw binary node. */
|
|
415
|
+
node: any;
|
|
416
|
+
}
|
|
417
|
+
|
|
338
418
|
/**
|
|
339
419
|
* The payload of the `message` event.
|
|
340
420
|
*
|
|
@@ -361,6 +441,17 @@ export interface IncomingMessage {
|
|
|
361
441
|
text?: string | null;
|
|
362
442
|
/** `true` on a group message. */
|
|
363
443
|
isGroup?: boolean;
|
|
444
|
+
/**
|
|
445
|
+
* `true` when this account sent the message from another of its devices and
|
|
446
|
+
* the server echoed it back here.
|
|
447
|
+
*/
|
|
448
|
+
fromMe?: boolean;
|
|
449
|
+
/**
|
|
450
|
+
* The conversation the message belongs to, whichever direction it went. Same
|
|
451
|
+
* as `from` for anything we received; for one of our own messages it is who
|
|
452
|
+
* we sent it to, which `from` cannot say.
|
|
453
|
+
*/
|
|
454
|
+
chat?: Jid;
|
|
364
455
|
}
|
|
365
456
|
|
|
366
457
|
// ────────────────────────────────────────────────────────────────────────────
|
|
@@ -695,7 +786,7 @@ export interface WhalibmobEvents {
|
|
|
695
786
|
message: (msg: IncomingMessage) => void;
|
|
696
787
|
receipt: (r: { type: string; id: string; from: Jid }) => void;
|
|
697
788
|
presence: (p: { from: Jid; available: boolean }) => void;
|
|
698
|
-
call: (c:
|
|
789
|
+
call: (c: CallEvent) => void;
|
|
699
790
|
notification: (node: any) => void;
|
|
700
791
|
decrypt_error: (e: { id: string; from: Jid; participant?: Jid; err: Error }) => void;
|
|
701
792
|
session_refresh: (e: { node: any }) => void;
|
|
@@ -798,6 +889,15 @@ export declare class WhalibmobClient extends EventEmitter {
|
|
|
798
889
|
/** Ask for an 8-character pairing code. Throws if the session is already linked. */
|
|
799
890
|
requestPairingCode(phoneNumber?: string, customCode?: string): Promise<string>;
|
|
800
891
|
disconnect(opts?: DisconnectOptions): void;
|
|
892
|
+
/**
|
|
893
|
+
* Unlink this device from the account, the way the Linked Devices screen on
|
|
894
|
+
* the phone does, then close the connection.
|
|
895
|
+
*
|
|
896
|
+
* Web only. Nothing on disk is deleted — the session it describes no longer
|
|
897
|
+
* exists, so the next `connectWeb()` pairs afresh. Resolves to whether the
|
|
898
|
+
* server accepted the removal; the connection drops either way.
|
|
899
|
+
*/
|
|
900
|
+
logout(): Promise<boolean>;
|
|
801
901
|
|
|
802
902
|
/** Ask the server whether this session still logs in, and under which number. */
|
|
803
903
|
checkSessionAlive(): Promise<SessionAliveProbe>;
|
|
@@ -824,8 +924,30 @@ export declare class WhalibmobClient extends EventEmitter {
|
|
|
824
924
|
createCallLink(type?: 'audio' | 'video', opts?: { startTime?: number }): Promise<CallLink>;
|
|
825
925
|
|
|
826
926
|
// ─── Receiving ───────────────────────────────────────────────────────────
|
|
827
|
-
/**
|
|
927
|
+
/**
|
|
928
|
+
* Download and decrypt a media message. Accepts `msg` or `msg.decoded`.
|
|
929
|
+
*
|
|
930
|
+
* Both digests the message carried are checked by default, on top of the MAC
|
|
931
|
+
* that always is. `verify: false` skips the digests only.
|
|
932
|
+
*/
|
|
828
933
|
downloadMedia(decoded: DecodedMessage | IncomingMessage, opts?: { verify?: boolean }): Promise<Buffer>;
|
|
934
|
+
/**
|
|
935
|
+
* Download the full-size preview image of a message that quoted a link.
|
|
936
|
+
*
|
|
937
|
+
* A link preview's thumbnail is a media file of its own, encrypted under its
|
|
938
|
+
* own key name, so `downloadMedia` cannot reach it. The small inline preview
|
|
939
|
+
* needs no download — it is already on the message as `jpegThumbnail`.
|
|
940
|
+
*/
|
|
941
|
+
downloadThumbnail(decoded: DecodedMessage | IncomingMessage, opts?: { verify?: boolean }): Promise<Buffer>;
|
|
942
|
+
/**
|
|
943
|
+
* Read a vote on a poll.
|
|
944
|
+
*
|
|
945
|
+
* The ballot is encrypted under a key derived from the poll's message secret:
|
|
946
|
+
* the `encKey` `sendPoll` returned, or `decoded.encKey` on a poll somebody
|
|
947
|
+
* else sent. Pass the poll's `options` to get the choices back as text —
|
|
948
|
+
* without them only the SHA-256 of each choice is available.
|
|
949
|
+
*/
|
|
950
|
+
decryptPollVote(vote: DecodedMessage | IncomingMessage, opts: PollVoteOptions): PollVoteResult;
|
|
829
951
|
/** Ask the sender's phone to re-upload a file the CDN no longer has. */
|
|
830
952
|
requestMediaRetry(info: { id: string; chatJid: Jid; fromMe: boolean; participant?: Jid }, mediaKey: Buffer): Promise<void>;
|
|
831
953
|
decryptMediaRetry(notification: MediaRetryNotification, mediaKey: Buffer): MediaRetryResult;
|
|
@@ -922,6 +1044,15 @@ export declare class WhalibmobClient extends EventEmitter {
|
|
|
922
1044
|
|
|
923
1045
|
/** Pull pins/archives/mutes/stars/contact names changed elsewhere. */
|
|
924
1046
|
syncAppState(names?: string[] | null, opts?: { snapshot?: boolean }): Promise<AppStateSyncResult>;
|
|
1047
|
+
/**
|
|
1048
|
+
* Acknowledge a dirty bit so the server stops announcing it.
|
|
1049
|
+
*
|
|
1050
|
+
* Done for you on every `<ib><dirty>` the server sends; this is here for a
|
|
1051
|
+
* bit you want to clear yourself. Pass the `timestamp` the announcement
|
|
1052
|
+
* carried — without it nothing in particular is acknowledged and the server
|
|
1053
|
+
* re-announces on the next connection.
|
|
1054
|
+
*/
|
|
1055
|
+
markNotDirty(type: string, timestamp?: number | string): boolean;
|
|
925
1056
|
/** Whether an app-state key is available — `false` on an SMS session with no companion. */
|
|
926
1057
|
canSyncAppState(): boolean;
|
|
927
1058
|
/**
|
|
@@ -936,7 +1067,7 @@ export declare class WhalibmobClient extends EventEmitter {
|
|
|
936
1067
|
|
|
937
1068
|
/**
|
|
938
1069
|
* Refuse one incoming call. Only reachable when the client was built with
|
|
939
|
-
* `autoRejectCalls: false`; the id and
|
|
1070
|
+
* `autoRejectCalls: false`; pass the `id` and `creator` off the `call` event.
|
|
940
1071
|
*/
|
|
941
1072
|
rejectCall(callId: string, from: Jid): boolean;
|
|
942
1073
|
|