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 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 }` | Incoming call event |
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
- Pass `{ verify: true }` to check the download against the message's
3770
- `fileEncSha256` before decrypting it. The MAC already proves the plaintext was
3771
- not tampered with; this catches a truncated or substituted download earlier, and
3772
- names that failure separately from a decryption one.
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, { verify: true })
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 needed to decrypt incoming poll votes
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: { from: Jid; id: string | null; status: 'offer' | 'ringing' | 'terminate' | 'unknown'; node: any }) => void;
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
- /** Download and decrypt a media message. Accepts `msg` or `msg.decoded`. */
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 caller come off the `call` event.
1070
+ * `autoRejectCalls: false`; pass the `id` and `creator` off the `call` event.
940
1071
  */
941
1072
  rejectCall(callId: string, from: Jid): boolean;
942
1073