whalibmob 5.22.0 → 5.23.1

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
@@ -181,6 +181,7 @@ npm install -g whalibmob
181
181
  - [When 463 Means the Account Is Restricted](#when-463-means-the-account-is-restricted)
182
182
  - [What Is Automatic vs What You Need to Do](#what-is-automatic-vs-what-you-need-to-do)
183
183
  - [Receiving Media](#receiving-media)
184
+ - [When the File Is Gone from the CDN](#when-the-file-is-gone-from-the-cdn)
184
185
  - [Sending Messages](#sending-messages)
185
186
  - [Text Message](#text-message)
186
187
  - [Quote Message](#quote-message)
@@ -573,7 +574,7 @@ After running `wa connect <phone>`, every feature of the library is available as
573
574
  #### Send Text
574
575
 
575
576
  ```sh
576
- wa> /send 919634847671@s.whatsapp.net Hello, how are you?
577
+ wa> /send 919634847671 Hello, how are you?
577
578
  sent 3EB0ABCDEF123456
578
579
 
579
580
  # to a group
@@ -583,8 +584,8 @@ wa> /send 120363000000000000@g.us Hello everyone!
583
584
  #### Send Image
584
585
 
585
586
  ```sh
586
- wa> /image 919634847671@s.whatsapp.net ./photo.jpg
587
- wa> /image 919634847671@s.whatsapp.net ./photo.jpg Look at this!
587
+ wa> /image 919634847671 ./photo.jpg
588
+ wa> /image 919634847671 ./photo.jpg Look at this!
588
589
  ```
589
590
 
590
591
  The second argument is the file path. The optional third argument is the caption.
@@ -592,8 +593,8 @@ The second argument is the file path. The optional third argument is the caption
592
593
  #### Send Video
593
594
 
594
595
  ```sh
595
- wa> /video 919634847671@s.whatsapp.net ./clip.mp4
596
- wa> /video 919634847671@s.whatsapp.net ./clip.mp4 Watch this
596
+ wa> /video 919634847671 ./clip.mp4
597
+ wa> /video 919634847671 ./clip.mp4 Watch this
597
598
  ```
598
599
 
599
600
  #### Send Audio
@@ -601,7 +602,7 @@ wa> /video 919634847671@s.whatsapp.net ./clip.mp4 Watch this
601
602
  Sends the file as a regular audio attachment:
602
603
 
603
604
  ```sh
604
- wa> /audio 919634847671@s.whatsapp.net ./song.mp3
605
+ wa> /audio 919634847671 ./song.mp3
605
606
  ```
606
607
 
607
608
  #### Send Voice Note
@@ -609,14 +610,14 @@ wa> /audio 919634847671@s.whatsapp.net ./song.mp3
609
610
  Sends the file as a push-to-talk voice note with waveform:
610
611
 
611
612
  ```sh
612
- wa> /ptt 919634847671@s.whatsapp.net ./voice.ogg
613
+ wa> /ptt 919634847671 ./voice.ogg
613
614
  ```
614
615
 
615
616
  #### Send Document
616
617
 
617
618
  ```sh
618
- wa> /doc 919634847671@s.whatsapp.net ./report.pdf
619
- wa> /doc 919634847671@s.whatsapp.net ./report.pdf "Q1 Report.pdf"
619
+ wa> /doc 919634847671 ./report.pdf
620
+ wa> /doc 919634847671 ./report.pdf "Q1 Report.pdf"
620
621
  ```
621
622
 
622
623
  The optional third argument overrides the displayed filename.
@@ -626,7 +627,7 @@ The optional third argument overrides the displayed filename.
626
627
  The file must be in WebP format:
627
628
 
628
629
  ```sh
629
- wa> /sticker 919634847671@s.whatsapp.net ./sticker.webp
630
+ wa> /sticker 919634847671 ./sticker.webp
630
631
  ```
631
632
 
632
633
  #### Send Poll (CLI)
@@ -635,7 +636,7 @@ Separate the question from the options using `|`. At least two options are requi
635
636
 
636
637
  ```sh
637
638
  # single-choice poll (selectable=1)
638
- wa> /poll 919634847671@s.whatsapp.net Best language? | JavaScript | Python | Rust | selectable=1
639
+ wa> /poll 919634847671 Best language? | JavaScript | Python | Rust | selectable=1
639
640
 
640
641
  # unlimited-choice poll (default)
641
642
  wa> /poll 120363000000000000@g.us Pick your favourites | Red | Green | Blue
@@ -644,10 +645,10 @@ wa> /poll 120363000000000000@g.us Pick your favourites | Red | Green | Blue
644
645
  #### React to a Message
645
646
 
646
647
  ```sh
647
- wa> /react 919634847671@s.whatsapp.net 3EB0ABCDEF123456 👍
648
+ wa> /react 919634847671 3EB0ABCDEF123456 👍
648
649
 
649
650
  # remove a reaction — pass a space or empty string
650
- wa> /react 919634847671@s.whatsapp.net 3EB0ABCDEF123456 " "
651
+ wa> /react 919634847671 3EB0ABCDEF123456 " "
651
652
  ```
652
653
 
653
654
  The message ID is shown in the incoming message display as `id`.
@@ -658,17 +659,17 @@ The message ID is shown in the incoming message display as `id`.
658
659
  > Editing is only possible within 15 minutes of the original send.
659
660
 
660
661
  ```sh
661
- wa> /edit 919634847671@s.whatsapp.net 3EB0ABCDEF123456 Corrected text here
662
+ wa> /edit 919634847671 3EB0ABCDEF123456 Corrected text here
662
663
  ```
663
664
 
664
665
  #### Delete a Message
665
666
 
666
667
  ```sh
667
668
  # delete for yourself only
668
- wa> /delete 919634847671@s.whatsapp.net 3EB0ABCDEF123456
669
+ wa> /delete 919634847671 3EB0ABCDEF123456
669
670
 
670
671
  # delete for everyone (revoke)
671
- wa> /delete 919634847671@s.whatsapp.net 3EB0ABCDEF123456 all
672
+ wa> /delete 919634847671 3EB0ABCDEF123456 all
672
673
  ```
673
674
 
674
675
  #### Post a Status / Story
@@ -684,7 +685,7 @@ wa> /status Good morning everyone!
684
685
  Sends a message with the forwarded flag set:
685
686
 
686
687
  ```sh
687
- wa> /forward 919634847671@s.whatsapp.net This message was forwarded
688
+ wa> /forward 919634847671 This message was forwarded
688
689
  ```
689
690
 
690
691
  #### Reply to a Message (CLI)
@@ -693,10 +694,10 @@ Quote and reply to a specific message. You need the message ID (shown as `id:` i
693
694
 
694
695
  ```sh
695
696
  # DM — senderJid is the same as the chat JID
696
- wa> /reply 919634847671@s.whatsapp.net 3EB0XXXXXXXX 919634847671@s.whatsapp.net Got it, thanks!
697
+ wa> /reply 919634847671 3EB0XXXXXXXX 919634847671 Got it, thanks!
697
698
 
698
699
  # Group — senderJid is the member who sent the original message
699
- wa> /reply 120363000000000000@g.us 3EB0XXXXXXXX 919634847671@s.whatsapp.net Agreed!
700
+ wa> /reply 120363000000000000@g.us 3EB0XXXXXXXX 919634847671 Agreed!
700
701
  ```
701
702
 
702
703
  The message ID is printed when a message arrives:
@@ -710,13 +711,13 @@ Send a GPS location pin. Latitude and longitude are required; name and address (
710
711
 
711
712
  ```sh
712
713
  # lat/lon only
713
- wa> /location 919634847671@s.whatsapp.net 48.8566 2.3522
714
+ wa> /location 919634847671 48.8566 2.3522
714
715
 
715
716
  # with name
716
- wa> /location 919634847671@s.whatsapp.net 48.8566 2.3522 Eiffel Tower
717
+ wa> /location 919634847671 48.8566 2.3522 Eiffel Tower
717
718
 
718
719
  # with name and address (separate with |)
719
- wa> /location 919634847671@s.whatsapp.net 48.8566 2.3522 Eiffel Tower | Champ de Mars, Paris
720
+ wa> /location 919634847671 48.8566 2.3522 Eiffel Tower | Champ de Mars, Paris
720
721
 
721
722
  # to a group
722
723
  wa> /location 120363000000000000@g.us 51.5074 -0.1278 London
@@ -727,7 +728,7 @@ wa> /location 120363000000000000@g.us 51.5074 -0.1278 London
727
728
  Send a contact card. The vCard string must follow the vCard v3 format. Wrap it in quotes in the shell:
728
729
 
729
730
  ```sh
730
- wa> /vcard 919634847671@s.whatsapp.net "Alice Smith" "BEGIN:VCARD\nVERSION:3.0\nFN:Alice Smith\nTEL;TYPE=CELL:+919634847671\nEND:VCARD"
731
+ wa> /vcard 919634847671 "Alice Smith" "BEGIN:VCARD\nVERSION:3.0\nFN:Alice Smith\nTEL;TYPE=CELL:+919634847671\nEND:VCARD"
731
732
  ```
732
733
 
733
734
  For multi-line vCards it is easiest to store the string in a shell variable:
@@ -740,7 +741,7 @@ TEL;TYPE=CELL:+919634847671
740
741
  EMAIL:alice@example.com
741
742
  END:VCARD"
742
743
 
743
- wa> /vcard 919634847671@s.whatsapp.net "Alice Smith" "$VCARD"
744
+ wa> /vcard 919634847671 "Alice Smith" "$VCARD"
744
745
  ```
745
746
 
746
747
  ---
@@ -758,13 +759,13 @@ wa> /offline
758
759
 
759
760
  ```sh
760
761
  # show "typing…" in a chat
761
- wa> /typing 919634847671@s.whatsapp.net
762
+ wa> /typing 919634847671
762
763
 
763
764
  # show "recording audio…" in a chat
764
- wa> /recording 919634847671@s.whatsapp.net
765
+ wa> /recording 919634847671
765
766
 
766
767
  # stop the indicator
767
- wa> /stop 919634847671@s.whatsapp.net
768
+ wa> /stop 919634847671
768
769
  ```
769
770
 
770
771
  #### Subscribe to a Contact's Presence
@@ -772,7 +773,7 @@ wa> /stop 919634847671@s.whatsapp.net
772
773
  Subscribes to online/offline events for a contact. The shell will print presence updates as they arrive:
773
774
 
774
775
  ```sh
775
- wa> /subscribe 919634847671@s.whatsapp.net
776
+ wa> /subscribe 919634847671
776
777
  subscribed to 919634847671@s.whatsapp.net
777
778
 
778
779
  # when they come online:
@@ -852,7 +853,7 @@ wa> /whatsapp 919634847671 12345678901
852
853
  Returns the CDN URL for a contact's or group's profile picture:
853
854
 
854
855
  ```sh
855
- wa> /picture 919634847671@s.whatsapp.net
856
+ wa> /picture 919634847671
856
857
  https://mmg.whatsapp.net/v/...
857
858
 
858
859
  wa> /picture 120363000000000000@g.us
@@ -864,7 +865,7 @@ wa> /picture 120363000000000000@g.us
864
865
  Fetches the bio / about text for a contact:
865
866
 
866
867
  ```sh
867
- wa> /contact about 919634847671@s.whatsapp.net
868
+ wa> /contact about 919634847671
868
869
  Available 24/7
869
870
  ```
870
871
 
@@ -881,42 +882,42 @@ send the request a primary makes for itself; `/pin`, `/archive` and `/star`
881
882
  update this session only. The command says which happened:
882
883
 
883
884
  ```sh
884
- wa> /pin 919634847671@s.whatsapp.net
885
+ wa> /pin 919634847671
885
886
  pinned (this session only — no app state key)
886
887
  ```
887
888
 
888
889
  #### Mark Read / Unread
889
890
 
890
891
  ```sh
891
- wa> /read 919634847671@s.whatsapp.net
892
- wa> /unread 919634847671@s.whatsapp.net
892
+ wa> /read 919634847671
893
+ wa> /unread 919634847671
893
894
  ```
894
895
 
895
896
  #### Mute / Unmute
896
897
 
897
898
  ```sh
898
899
  # mute for 60 minutes
899
- wa> /mute 919634847671@s.whatsapp.net 60
900
+ wa> /mute 919634847671 60
900
901
 
901
902
  # mute indefinitely
902
- wa> /mute 919634847671@s.whatsapp.net
903
+ wa> /mute 919634847671
903
904
 
904
905
  # unmute
905
- wa> /unmute 919634847671@s.whatsapp.net
906
+ wa> /unmute 919634847671
906
907
  ```
907
908
 
908
909
  #### Pin / Unpin
909
910
 
910
911
  ```sh
911
- wa> /pin 919634847671@s.whatsapp.net
912
- wa> /unpin 919634847671@s.whatsapp.net
912
+ wa> /pin 919634847671
913
+ wa> /unpin 919634847671
913
914
  ```
914
915
 
915
916
  #### Archive / Unarchive
916
917
 
917
918
  ```sh
918
- wa> /archive 919634847671@s.whatsapp.net
919
- wa> /unarchive 919634847671@s.whatsapp.net
919
+ wa> /archive 919634847671
920
+ wa> /unarchive 919634847671
920
921
  ```
921
922
 
922
923
  #### Star / Unstar a Message (CLI)
@@ -925,8 +926,8 @@ Add `me` when the message is one you sent — it is part of how the star is file
925
926
  so leaving it off on your own message stars the wrong thing.
926
927
 
927
928
  ```sh
928
- wa> /star 919634847671@s.whatsapp.net 3EB0ABCDEF123456 me
929
- wa> /unstar 919634847671@s.whatsapp.net 3EB0ABCDEF123456
929
+ wa> /star 919634847671 3EB0ABCDEF123456 me
930
+ wa> /unstar 919634847671 3EB0ABCDEF123456
930
931
  ```
931
932
 
932
933
  <a id="cli-app-state"></a>
@@ -1051,13 +1052,13 @@ shell prints the change as it happens:
1051
1052
 
1052
1053
  ```sh
1053
1054
  # set 1-day timer on a DM
1054
- wa> /ephemeral 919634847671@s.whatsapp.net 86400
1055
+ wa> /ephemeral 919634847671 86400
1055
1056
 
1056
1057
  # set 1-week timer on a group
1057
1058
  wa> /ephemeral 120363000000000000@g.us 604800
1058
1059
 
1059
1060
  # turn off
1060
- wa> /ephemeral 919634847671@s.whatsapp.net 0
1061
+ wa> /ephemeral 919634847671 0
1061
1062
  ```
1062
1063
 
1063
1064
  #### Default Disappearing Timer
@@ -1078,10 +1079,10 @@ Accepts the same values as `/ephemeral`: 0, 86400, 604800, 7776000.
1078
1079
  #### Block / Unblock
1079
1080
 
1080
1081
  ```sh
1081
- wa> /block 919634847671@s.whatsapp.net
1082
+ wa> /block 919634847671
1082
1083
  blocked 919634847671@s.whatsapp.net
1083
1084
 
1084
- wa> /unblock 919634847671@s.whatsapp.net
1085
+ wa> /unblock 919634847671
1085
1086
  unblocked 919634847671@s.whatsapp.net
1086
1087
  ```
1087
1088
 
@@ -1101,7 +1102,7 @@ wa> /blocklist
1101
1102
  #### CLI Create a Group
1102
1103
 
1103
1104
  ```sh
1104
- wa> /group create MyGroup 919634847671@s.whatsapp.net 12345678901@s.whatsapp.net
1105
+ wa> /group create MyGroup 919634847671 12345678901
1105
1106
  creating group...
1106
1107
  created 120363000000000000@g.us
1107
1108
  subject MyGroup
@@ -1119,12 +1120,12 @@ left 120363000000000000@g.us
1119
1120
 
1120
1121
  ```sh
1121
1122
  # add participants
1122
- wa> /group add 120363000000000000@g.us 919634847671@s.whatsapp.net 12345678901@s.whatsapp.net
1123
+ wa> /group add 120363000000000000@g.us 919634847671 12345678901
1123
1124
  added 919634847671@s.whatsapp.net
1124
1125
  failed 12345678901@s.whatsapp.net — their privacy settings do not allow it (403) · can be invited instead
1125
1126
 
1126
1127
  # remove participants
1127
- wa> /group remove 120363000000000000@g.us 919634847671@s.whatsapp.net
1128
+ wa> /group remove 120363000000000000@g.us 919634847671
1128
1129
  removed 919634847671@s.whatsapp.net
1129
1130
  ```
1130
1131
 
@@ -1135,10 +1136,10 @@ reported on its own line, because the server decides each one separately.
1135
1136
 
1136
1137
  ```sh
1137
1138
  # promote to admin
1138
- wa> /group promote 120363000000000000@g.us 919634847671@s.whatsapp.net
1139
+ wa> /group promote 120363000000000000@g.us 919634847671
1139
1140
 
1140
1141
  # demote from admin
1141
- wa> /group demote 120363000000000000@g.us 919634847671@s.whatsapp.net
1142
+ wa> /group demote 120363000000000000@g.us 919634847671
1142
1143
  ```
1143
1144
 
1144
1145
  #### Change Group Name
@@ -1285,11 +1286,11 @@ wa> /group pending 120363000000000000@g.us
1285
1286
 
1286
1287
  ```sh
1287
1288
  # approve one or more pending members
1288
- wa> /group approve 120363000000000000@g.us 919634847671@s.whatsapp.net
1289
+ wa> /group approve 120363000000000000@g.us 919634847671
1289
1290
  approved 919634847671@s.whatsapp.net
1290
1291
 
1291
1292
  # reject one or more pending members
1292
- wa> /group reject 120363000000000000@g.us 919634847671@s.whatsapp.net
1293
+ wa> /group reject 120363000000000000@g.us 919634847671
1293
1294
  rejected 919634847671@s.whatsapp.net
1294
1295
  ```
1295
1296
 
@@ -1304,7 +1305,7 @@ For someone whose privacy settings do not let them be added to a group directly,
1304
1305
  `add-invite` adds whoever it can and sends the rest a personal invitation:
1305
1306
 
1306
1307
  ```sh
1307
- wa> /group add-invite 120363000000000000@g.us 919634847671@s.whatsapp.net 12345678901@s.whatsapp.net
1308
+ wa> /group add-invite 120363000000000000@g.us 919634847671 12345678901
1308
1309
  added 919634847671@s.whatsapp.net
1309
1310
  failed 12345678901@s.whatsapp.net — their privacy settings do not allow it (403) · can be invited instead · invitation sent
1310
1311
  ```
@@ -1320,17 +1321,17 @@ An invitation that arrives for you shows the command that accepts it:
1320
1321
 
1321
1322
  ```sh
1322
1323
  # look at the group without joining it
1323
- wa> /group preview-invite 120363000000000000@g.us 919634847671@s.whatsapp.net AbCdEfGh 1790000000
1324
+ wa> /group preview-invite 120363000000000000@g.us 919634847671 AbCdEfGh 1790000000
1324
1325
 
1325
1326
  # join
1326
- wa> /group accept-invite 120363000000000000@g.us 919634847671@s.whatsapp.net AbCdEfGh 1790000000
1327
+ wa> /group accept-invite 120363000000000000@g.us 919634847671 AbCdEfGh 1790000000
1327
1328
  joined 120363000000000000@g.us
1328
1329
 
1329
1330
  # send one by hand
1330
- wa> /group send-invite 120363000000000000@g.us 12345678901@s.whatsapp.net AbCdEfGh 1790000000
1331
+ wa> /group send-invite 120363000000000000@g.us 12345678901 AbCdEfGh 1790000000
1331
1332
 
1332
1333
  # take one back before it is used
1333
- wa> /group revoke-invite 120363000000000000@g.us 12345678901@s.whatsapp.net
1334
+ wa> /group revoke-invite 120363000000000000@g.us 12345678901
1334
1335
  ```
1335
1336
 
1336
1337
  #### Group Settings
@@ -1413,7 +1414,7 @@ wa> /newsletter post 120363000000000004@newsletter Breaking: WhatsApp adds polls
1413
1414
  Query the public business profile of any WhatsApp Business account:
1414
1415
 
1415
1416
  ```sh
1416
- wa> /biz 919634847671@s.whatsapp.net
1417
+ wa> /biz 919634847671
1417
1418
  ────────────────────────────────────────────────────────
1418
1419
  jid 919634847671@s.whatsapp.net
1419
1420
  category Software & IT Services
@@ -2162,7 +2163,7 @@ Everything after the scan is identical to the pairing-code path: `paired`, a str
2162
2163
  A few seconds after the code is accepted (or the QR is scanned) you will see `paired`, the server restarts the stream, and `connected` fires on the new connection. From that point on everything else in this document applies unchanged:
2163
2164
 
2164
2165
  ```js
2165
- await client.sendText('919876543210@s.whatsapp.net', 'sent from a linked device')
2166
+ await client.sendText('919876543210', 'sent from a linked device')
2166
2167
  ```
2167
2168
 
2168
2169
  ### Reconnecting a Linked Session
@@ -2409,6 +2410,7 @@ Every event from the SMS primary API fires here too — `message`, `receipt`, `p
2409
2410
  | `pair_device` | `{ refs }` | the QR path produced reference strings |
2410
2411
  | `history_sync` | `{ syncTypeName, chats, contacts, pushNames, merged }` | a chunk of history arrived |
2411
2412
  | `history_sync_error` | `{ err, notification }` | a chunk could not be fetched or decrypted |
2413
+ | `media_retry` | `{ messageId, chatJid, fromMe, participant, ciphertext, iv, error }` | the sender's phone answered a `requestMediaRetry()`. Read it with `decryptMediaRetry()` and the original media key; an `error` instead of a payload means the phone no longer has the file either |
2412
2414
  | `client_rejected` | `{ reason, location, message }` | the server refused the client itself, not the session — `405` means the announced version is not accepted. Fires whether the refusal arrives during the handshake or once the stream is open, and the client stops retrying either way. Distinct from `auth_failure`, and there is nothing to re-pair. |
2413
2415
  | `version_update` | `{ from, to, source }` | the announced version was refreshed from the platform's store before a handshake. Fires on reconnects as well as the first connect |
2414
2416
  | `apk_material_stale` | `{ materialVersion, liveVersion, hint }` | Android only: the Play Store listing has moved past the APK the cached token material came from. Nothing is broken — registration is what reads that material — but the next number registered from this install would go out under an older build |
@@ -3129,7 +3131,7 @@ async function connect() {
3129
3131
 
3130
3132
  client.on('connected', async () => {
3131
3133
  console.log('connected')
3132
- await client.sendText('919634847671@s.whatsapp.net', 'Hello!')
3134
+ await client.sendText('919634847671', 'Hello!')
3133
3135
  })
3134
3136
 
3135
3137
  client.on('disconnected', () => {
@@ -3663,12 +3665,56 @@ const bytes = await client.downloadMedia(d, { verify: true })
3663
3665
  with no media, no CDN location, an unsupported type, or a file that does not
3664
3666
  match the message all say so.
3665
3667
 
3668
+ ### When the file is gone from the CDN
3669
+
3670
+ Media is not carried inside the message — the message carries a URL, a hash and
3671
+ the key, and the bytes sit on WhatsApp's CDN for a limited time. A download that
3672
+ answers **404** or **410** has nothing left to fetch. This is most common for
3673
+ messages that arrive through history sync long after they were sent.
3674
+
3675
+ The sender's phone still has the original, and can be asked to upload it again:
3676
+
3677
+ ```js
3678
+ client.on('media_retry', (notification) => {
3679
+ const result = client.decryptMediaRetry(notification, mediaKey)
3680
+ if (result.ok) {
3681
+ // fresh location — download it the usual way
3682
+ console.log('re-uploaded at', result.directPath)
3683
+ } else {
3684
+ // the phone no longer has it either; nothing further to try
3685
+ }
3686
+ })
3687
+
3688
+ try {
3689
+ await client.downloadMedia(msg)
3690
+ } catch (err) {
3691
+ if (/404|410/.test(err.message)) {
3692
+ await client.requestMediaRetry({
3693
+ id: msg.key.id,
3694
+ chatJid: msg.key.remoteJid,
3695
+ fromMe: msg.key.fromMe,
3696
+ // groups only — the member who sent it
3697
+ participant: msg.key.participant
3698
+ }, mediaKey)
3699
+ }
3700
+ }
3701
+ ```
3702
+
3703
+ `requestMediaRetry` resolves as soon as the request is on the wire; the answer
3704
+ arrives later on the `media_retry` event, which is why the two halves are
3705
+ written separately. Keep the `mediaKey` — the reply is encrypted under a key
3706
+ derived from it, and without it the fresh location cannot be read.
3707
+
3708
+ That derivation is also what makes the request safe to send: it proves the
3709
+ asker was a recipient of the message rather than someone who merely knows a
3710
+ message id, so no phone can be made to re-upload a file on request.
3711
+
3666
3712
  ## Sending Messages
3667
3713
 
3668
3714
  ### Text Message
3669
3715
 
3670
3716
  ```js
3671
- await client.sendText('919634847671@s.whatsapp.net', 'Hello!')
3717
+ await client.sendText('919634847671', 'Hello!')
3672
3718
  ```
3673
3719
 
3674
3720
  ### Quote Message
@@ -3704,10 +3750,10 @@ await client.sendText(
3704
3750
 
3705
3751
  ```js
3706
3752
  // react to a message
3707
- await client.sendReaction('919634847671@s.whatsapp.net', 'MSGID123', '👍')
3753
+ await client.sendReaction('919634847671', 'MSGID123', '👍')
3708
3754
 
3709
3755
  // remove a reaction — pass empty string
3710
- await client.sendReaction('919634847671@s.whatsapp.net', 'MSGID123', '')
3756
+ await client.sendReaction('919634847671', 'MSGID123', '')
3711
3757
  ```
3712
3758
 
3713
3759
  ### Edit Message
@@ -3727,10 +3773,10 @@ await client.editMessage(
3727
3773
 
3728
3774
  ```js
3729
3775
  // delete for yourself only
3730
- await client.deleteMessage('MSGID123', '919634847671@s.whatsapp.net', true, false)
3776
+ await client.deleteMessage('MSGID123', '919634847671', true, false)
3731
3777
 
3732
3778
  // delete for everyone (revoke)
3733
- await client.deleteMessage('MSGID123', '919634847671@s.whatsapp.net', true, true)
3779
+ await client.deleteMessage('MSGID123', '919634847671', true, true)
3734
3780
  ```
3735
3781
 
3736
3782
  ### Forward Message
@@ -3740,12 +3786,12 @@ re-uploading. Pass a decoded message object from the `message` event to forward
3740
3786
 
3741
3787
  ```js
3742
3788
  // Forward text
3743
- await client.forwardMessage('919634847671@s.whatsapp.net', 'text to forward')
3789
+ await client.forwardMessage('919634847671', 'text to forward')
3744
3790
 
3745
3791
  // Forward any received message (full media, no re-upload)
3746
3792
  client.on('message', async (msg) => {
3747
3793
  if (msg.decoded && msg.decoded.type !== 'text') {
3748
- await client.forwardMessage('919634847671@s.whatsapp.net', msg)
3794
+ await client.forwardMessage('919634847671', msg)
3749
3795
  }
3750
3796
  })
3751
3797
  ```
@@ -3795,10 +3841,10 @@ Send a GPS location pin. `name` and `address` are optional labels shown below th
3795
3841
 
3796
3842
  ```js
3797
3843
  // minimal — lat/lon only
3798
- await client.sendLocation('919634847671@s.whatsapp.net', 48.8566, 2.3522)
3844
+ await client.sendLocation('919634847671', 48.8566, 2.3522)
3799
3845
 
3800
3846
  // with name and address
3801
- await client.sendLocation('919634847671@s.whatsapp.net', 48.8566, 2.3522, {
3847
+ await client.sendLocation('919634847671', 48.8566, 2.3522, {
3802
3848
  name: 'Eiffel Tower',
3803
3849
  address: 'Champ de Mars, 5 Av. Anatole France, Paris'
3804
3850
  })
@@ -3809,7 +3855,7 @@ await client.sendLocation('120363000000000000@g.us', 51.5074, -0.1278, {
3809
3855
  })
3810
3856
 
3811
3857
  // with a map image for the bubble — WhatsApp draws a blank card without one
3812
- await client.sendLocation('919634847671@s.whatsapp.net', 48.8566, 2.3522, {
3858
+ await client.sendLocation('919634847671', 48.8566, 2.3522, {
3813
3859
  name: 'Eiffel Tower',
3814
3860
  thumbnail: jpegBuffer
3815
3861
  })
@@ -3829,7 +3875,7 @@ const vcard = [
3829
3875
  'END:VCARD'
3830
3876
  ].join('\n')
3831
3877
 
3832
- await client.sendContact('919634847671@s.whatsapp.net', 'Alice Smith', vcard)
3878
+ await client.sendContact('919634847671', 'Alice Smith', vcard)
3833
3879
  ```
3834
3880
 
3835
3881
  ### Call Link
@@ -3848,7 +3894,7 @@ const audio = await client.createCallLink()
3848
3894
  await client.createCallLink('video', { startTime: 1800000000 })
3849
3895
 
3850
3896
  // create it and send it in one step
3851
- await client.sendCallLink('919634847671@s.whatsapp.net', 'video', {
3897
+ await client.sendCallLink('919634847671', 'video', {
3852
3898
  text: 'Join me here:'
3853
3899
  })
3854
3900
  ```
@@ -3869,10 +3915,10 @@ the preview.
3869
3915
 
3870
3916
  ```js
3871
3917
  // from file path
3872
- await client.sendImage('919634847671@s.whatsapp.net', './photo.jpg', { caption: 'Look at this' })
3918
+ await client.sendImage('919634847671', './photo.jpg', { caption: 'Look at this' })
3873
3919
 
3874
3920
  // from Buffer
3875
- await client.sendImage('919634847671@s.whatsapp.net', buffer, {
3921
+ await client.sendImage('919634847671', buffer, {
3876
3922
  caption: 'Photo',
3877
3923
  mimetype: 'image/jpeg'
3878
3924
  })
@@ -3881,26 +3927,26 @@ await client.sendImage('919634847671@s.whatsapp.net', buffer, {
3881
3927
  ### Video Message
3882
3928
 
3883
3929
  ```js
3884
- await client.sendVideo('919634847671@s.whatsapp.net', './clip.mp4', { caption: 'Watch this' })
3930
+ await client.sendVideo('919634847671', './clip.mp4', { caption: 'Watch this' })
3885
3931
  ```
3886
3932
 
3887
3933
  ### Audio Message
3888
3934
 
3889
3935
  ```js
3890
- await client.sendAudio('919634847671@s.whatsapp.net', './song.mp3')
3936
+ await client.sendAudio('919634847671', './song.mp3')
3891
3937
  ```
3892
3938
 
3893
3939
  ### Voice Note
3894
3940
 
3895
3941
  ```js
3896
3942
  // ptt: true renders the audio as a push-to-talk voice note with waveform
3897
- await client.sendAudio('919634847671@s.whatsapp.net', './voice.ogg', { ptt: true })
3943
+ await client.sendAudio('919634847671', './voice.ogg', { ptt: true })
3898
3944
  ```
3899
3945
 
3900
3946
  ### Document Message
3901
3947
 
3902
3948
  ```js
3903
- await client.sendDocument('919634847671@s.whatsapp.net', './report.pdf', {
3949
+ await client.sendDocument('919634847671', './report.pdf', {
3904
3950
  fileName: 'Q1 Report.pdf'
3905
3951
  })
3906
3952
  ```
@@ -3908,7 +3954,7 @@ await client.sendDocument('919634847671@s.whatsapp.net', './report.pdf', {
3908
3954
  ### Sticker Message
3909
3955
 
3910
3956
  ```js
3911
- await client.sendSticker('919634847671@s.whatsapp.net', './sticker.webp')
3957
+ await client.sendSticker('919634847671', './sticker.webp')
3912
3958
  ```
3913
3959
 
3914
3960
  ## Status / Stories
@@ -3931,7 +3977,7 @@ await client.sendStatus({ audio: './voice.ogg' })
3931
3977
  await client.sendStatus('Colorful', { backgroundArgb: 0xFF25D366, font: 3 })
3932
3978
 
3933
3979
  // post to an explicit list instead of the privacy-derived one
3934
- await client.sendStatus('Hi', { recipients: ['919634847671@s.whatsapp.net'] })
3980
+ await client.sendStatus('Hi', { recipients: ['919634847671'] })
3935
3981
  ```
3936
3982
 
3937
3983
  ### Status Privacy
@@ -3956,7 +4002,7 @@ pass `recipients` in that case.
3956
4002
 
3957
4003
  ```js
3958
4004
  // mark all messages in a chat as read (sends IQ to server)
3959
- await client.markChatRead('919634847671@s.whatsapp.net')
4005
+ await client.markChatRead('919634847671')
3960
4006
  ```
3961
4007
 
3962
4008
  ### Mark Voice Message Played
@@ -3965,7 +4011,7 @@ Send a `played` receipt for a received voice note (push-to-talk audio). This tel
3965
4011
 
3966
4012
  ```js
3967
4013
  // msgId: ID of the audio message, from: JID of the sender
3968
- client.markMessagePlayed('3EB0ABCDEF123456', '919634847671@s.whatsapp.net')
4014
+ client.markMessagePlayed('3EB0ABCDEF123456', '919634847671')
3969
4015
  ```
3970
4016
 
3971
4017
  ### Update Presence
@@ -3976,9 +4022,9 @@ client.setOnline(true)
3976
4022
  client.setOnline(false)
3977
4023
 
3978
4024
  // show typing or recording in a specific chat
3979
- client.setChatPresence('919634847671@s.whatsapp.net', 'composing') // typing
3980
- client.setChatPresence('919634847671@s.whatsapp.net', 'recording') // recording audio
3981
- client.setChatPresence('919634847671@s.whatsapp.net', 'paused') // stopped
4025
+ client.setChatPresence('919634847671', 'composing') // typing
4026
+ client.setChatPresence('919634847671', 'recording') // recording audio
4027
+ client.setChatPresence('919634847671', 'paused') // stopped
3982
4028
  ```
3983
4029
 
3984
4030
  ## Modifying Chats
@@ -3996,7 +4042,7 @@ They return a boolean: **whether the change reached app state**, and so whether
3996
4042
  other devices will see it.
3997
4043
 
3998
4044
  ```js
3999
- const synced = await client.pinChat('919634847671@s.whatsapp.net')
4045
+ const synced = await client.pinChat('919634847671')
4000
4046
  if (!synced) console.log('pinned here, but your phone will not know')
4001
4047
  ```
4002
4048
 
@@ -4023,16 +4069,16 @@ if (!synced) console.log('pinned here, but your phone will not know')
4023
4069
  ### Archive / Unarchive a Chat
4024
4070
 
4025
4071
  ```js
4026
- await client.archiveChat('919634847671@s.whatsapp.net')
4027
- await client.unarchiveChat('919634847671@s.whatsapp.net')
4072
+ await client.archiveChat('919634847671')
4073
+ await client.unarchiveChat('919634847671')
4028
4074
  ```
4029
4075
 
4030
4076
  ### Mute / Unmute a Chat
4031
4077
 
4032
4078
  ```js
4033
- await client.muteChat('919634847671@s.whatsapp.net', 8 * 60 * 60 * 1000) // 8 hours
4034
- await client.muteChat('919634847671@s.whatsapp.net', 0) // until unmuted
4035
- await client.unmuteChat('919634847671@s.whatsapp.net')
4079
+ await client.muteChat('919634847671', 8 * 60 * 60 * 1000) // 8 hours
4080
+ await client.muteChat('919634847671', 0) // until unmuted
4081
+ await client.unmuteChat('919634847671')
4036
4082
  ```
4037
4083
 
4038
4084
  ### Mark a Chat Read / Unread
@@ -4041,15 +4087,15 @@ This is the chat's own unread badge. To send read receipts (blue ticks) for
4041
4087
  particular messages, use `markRead()` instead.
4042
4088
 
4043
4089
  ```js
4044
- await client.markChatRead('919634847671@s.whatsapp.net')
4045
- await client.markChatUnread('919634847671@s.whatsapp.net')
4090
+ await client.markChatRead('919634847671')
4091
+ await client.markChatUnread('919634847671')
4046
4092
  ```
4047
4093
 
4048
4094
  ### Pin / Unpin a Chat
4049
4095
 
4050
4096
  ```js
4051
- await client.pinChat('919634847671@s.whatsapp.net')
4052
- await client.unpinChat('919634847671@s.whatsapp.net')
4097
+ await client.pinChat('919634847671')
4098
+ await client.unpinChat('919634847671')
4053
4099
  ```
4054
4100
 
4055
4101
  ### Star / Unstar a Message
@@ -4058,8 +4104,8 @@ The third argument says whether the message being starred is one you sent. It is
4058
4104
  part of how the star is filed, so getting it wrong stars a different message.
4059
4105
 
4060
4106
  ```js
4061
- await client.starMessage('MSGID123', '919634847671@s.whatsapp.net', true) // yours
4062
- await client.unstarMessage('MSGID123', '919634847671@s.whatsapp.net', false) // theirs
4107
+ await client.starMessage('MSGID123', '919634847671', true) // yours
4108
+ await client.unstarMessage('MSGID123', '919634847671', false) // theirs
4063
4109
  ```
4064
4110
 
4065
4111
  <a id="app-state-sync"></a>
@@ -4146,11 +4192,11 @@ client.on('app_state_mutation', ({ collection, index, action, removed }) => {
4146
4192
 
4147
4193
  ```js
4148
4194
  // set disappearing timer for a specific chat (DM or group)
4149
- await client.changeEphemeralTimer('919634847671@s.whatsapp.net', 86400)
4195
+ await client.changeEphemeralTimer('919634847671', 86400)
4150
4196
  await client.changeEphemeralTimer('120363000000000000@g.us', 604800)
4151
4197
 
4152
4198
  // remove disappearing messages
4153
- await client.changeEphemeralTimer('919634847671@s.whatsapp.net', 0)
4199
+ await client.changeEphemeralTimer('919634847671', 0)
4154
4200
  ```
4155
4201
 
4156
4202
  ## User Queries
@@ -4175,7 +4221,7 @@ const results = await client.hasWhatsapp(['919634847671', '12345678901'])
4175
4221
  ### Fetch Profile About
4176
4222
 
4177
4223
  ```js
4178
- const about = await client.queryAbout('919634847671@s.whatsapp.net')
4224
+ const about = await client.queryAbout('919634847671')
4179
4225
  console.log(about)
4180
4226
 
4181
4227
  // your own, asked the same way
@@ -4185,7 +4231,7 @@ console.log(await client.queryOwnAbout())
4185
4231
  ### Fetch Profile Picture
4186
4232
 
4187
4233
  ```js
4188
- const url = await client.queryPicture('919634847671@s.whatsapp.net')
4234
+ const url = await client.queryPicture('919634847671')
4189
4235
  // also works for groups
4190
4236
  const groupUrl = await client.queryPicture('120363000000000000@g.us')
4191
4237
  ```
@@ -4194,7 +4240,7 @@ const groupUrl = await client.queryPicture('120363000000000000@g.us')
4194
4240
 
4195
4241
  ```js
4196
4242
  // triggers 'presence' events when the contact comes online or goes offline
4197
- client.subscribeToPresence('919634847671@s.whatsapp.net')
4243
+ client.subscribeToPresence('919634847671')
4198
4244
 
4199
4245
  client.on('presence', ({ from, available }) => {
4200
4246
  console.log(from, available ? 'online' : 'offline')
@@ -4303,8 +4349,8 @@ await client.changeProfilePicture(buf, { size: 640, quality: 50 })
4303
4349
 
4304
4350
  ```js
4305
4351
  // both return the updated block list, and throw if the server refuses
4306
- const blocked = await client.blockContact('919634847671@s.whatsapp.net')
4307
- await client.unblockContact('919634847671@s.whatsapp.net')
4352
+ const blocked = await client.blockContact('919634847671')
4353
+ await client.unblockContact('919634847671')
4308
4354
 
4309
4355
  // the block list is addressed by LID; a phone number is resolved to one first,
4310
4356
  // looking it up if it is not already known
@@ -4426,9 +4472,9 @@ for (const r of results) {
4426
4472
  else console.log('failed', r.jid, r.status, r.needsInvite ? '(invite instead)' : '')
4427
4473
  }
4428
4474
 
4429
- await client.removeGroupParticipants(groupJid, ['919634847671@s.whatsapp.net'])
4430
- await client.promoteGroupParticipants(groupJid, ['919634847671@s.whatsapp.net'])
4431
- await client.demoteGroupParticipants(groupJid, ['919634847671@s.whatsapp.net'])
4475
+ await client.removeGroupParticipants(groupJid, ['919634847671'])
4476
+ await client.promoteGroupParticipants(groupJid, ['919634847671'])
4477
+ await client.demoteGroupParticipants(groupJid, ['919634847671'])
4432
4478
  ```
4433
4479
 
4434
4480
  Each result looks like this:
@@ -4709,7 +4755,7 @@ groupName, jpegThumbnail, caption, isCommunity }`.
4709
4755
  To withdraw an invitation you sent before it is used:
4710
4756
 
4711
4757
  ```js
4712
- await client.revokeGroupInviteForParticipant(groupJid, '919634847671@s.whatsapp.net')
4758
+ await client.revokeGroupInviteForParticipant(groupJid, '919634847671')
4713
4759
  ```
4714
4760
 
4715
4761
  An expired or already-spent invitation throws rather than resolving to nothing,
@@ -4801,7 +4847,7 @@ await client.sendNewsletterText('120363000000000004@newsletter', 'Breaking: What
4801
4847
  Query the public business profile of any WhatsApp Business account:
4802
4848
 
4803
4849
  ```js
4804
- const bp = await client.queryBusinessProfile('919634847671@s.whatsapp.net')
4850
+ const bp = await client.queryBusinessProfile('919634847671')
4805
4851
  if (bp) {
4806
4852
  console.log(bp.category) // e.g. "Software & IT Services"
4807
4853
  console.log(bp.email) // business email (if set)
package/lib/Client.js CHANGED
@@ -571,7 +571,10 @@ class WhalibmobClient extends EventEmitter {
571
571
  const device = this._store.device || getDeviceConfig();
572
572
  const business = !!device.business;
573
573
  const android = device.os === 'android';
574
- const source = android ? 'Play Store listing' : 'App Store listing';
574
+ // Named for where the answer comes from, since that is the first thing
575
+ // anyone reading a version line wants to know. Android is answered by
576
+ // Play's API now, not by the store page it used to scrape.
577
+ const source = android ? 'Play Store' : 'App Store listing';
575
578
 
576
579
  try {
577
580
  const { compareVersions, fetchAndroidVersionLive } = require('./Registration');
@@ -1420,6 +1423,71 @@ class WhalibmobClient extends EventEmitter {
1420
1423
  this.emit('close');
1421
1424
  }
1422
1425
 
1426
+ /**
1427
+ * Ask the sender's phone to upload a media file again.
1428
+ *
1429
+ * Media lives on the CDN for a while, not forever, and the message only ever
1430
+ * carried the URL and the key. A download that comes back 404 or 410 — most
1431
+ * often for something that arrived through history sync long after it was
1432
+ * sent — has nothing left to fetch, and until now that was where it ended:
1433
+ * the message looked fine and the file was simply gone.
1434
+ *
1435
+ * The sender still has the original. This asks for it back.
1436
+ *
1437
+ * The request is encrypted under a key derived from the message's own media
1438
+ * key, so it is proof that we were a recipient rather than merely someone who
1439
+ * knows a message id — no phone can be made to re-upload on request.
1440
+ *
1441
+ * Resolves once the request is on the wire. The answer arrives later as a
1442
+ * `media_retry` event; pass it and the same media key to
1443
+ * decryptMediaRetry() for the fresh path.
1444
+ *
1445
+ * client.on('media_retry', (n) => {
1446
+ * const r = client.decryptMediaRetry(n, mediaKey)
1447
+ * if (r.ok) // re-download from r.directPath
1448
+ * })
1449
+ *
1450
+ * @param {object} info { id, chatJid, fromMe, participant }
1451
+ * `participant` only for a group — the member who sent it.
1452
+ * @param {Buffer} mediaKey the media key off the original message
1453
+ * @returns {Promise<void>}
1454
+ */
1455
+ async requestMediaRetry(info, mediaKey) {
1456
+ if (!this._socket || !this._connected) {
1457
+ throw new Error('Not connected — cannot request a media retry');
1458
+ }
1459
+ if (!info || !info.id) throw new Error('requestMediaRetry: info.id is required');
1460
+ if (!info.chatJid) throw new Error('requestMediaRetry: info.chatJid is required');
1461
+ if (!mediaKey || !mediaKey.length) {
1462
+ throw new Error('requestMediaRetry: the original message\'s mediaKey is required');
1463
+ }
1464
+
1465
+ const { buildRetryReceiptNode } = require('./messages/MediaRetry');
1466
+ // Addressed to ourselves: the receipt is a report about a message we hold,
1467
+ // and the server routes it on to the sender.
1468
+ const node = buildRetryReceiptNode(info, mediaKey, this._ownDeviceJid(), BinaryNode);
1469
+ this._socket.sendNode(node);
1470
+ _whaDbg('[DBG] MEDIA_RETRY requested id=' + info.id + ' chat=' + info.chatJid +
1471
+ (info.participant ? ' participant=' + info.participant : ''));
1472
+ }
1473
+
1474
+ /**
1475
+ * Read the answer to requestMediaRetry().
1476
+ *
1477
+ * @param {object} notification a `media_retry` event payload
1478
+ * @param {Buffer} mediaKey the same key the request was made with
1479
+ * @returns {{ok: boolean, result: number|null, directPath: string|null}}
1480
+ * `ok` false means the phone answered but could not oblige — it no
1481
+ * longer has the file either, and there is nothing further to try.
1482
+ */
1483
+ decryptMediaRetry(notification, mediaKey) {
1484
+ const { decryptRetryNotification } = require('./messages/MediaRetry');
1485
+ if (notification && notification.error) {
1486
+ return { ok: false, result: null, directPath: null, error: notification.error };
1487
+ }
1488
+ return decryptRetryNotification(notification, mediaKey);
1489
+ }
1490
+
1423
1491
  /**
1424
1492
  * Ask the server whether this session's registration is still alive.
1425
1493
  *
@@ -3529,6 +3597,27 @@ class WhalibmobClient extends EventEmitter {
3529
3597
  this._checkAndReplenishPreKeys();
3530
3598
  }
3531
3599
 
3600
+ // The sender's phone answering a re-upload request — see
3601
+ // requestMediaRetry(). Surfaced raw as well as decrypted, since only the
3602
+ // caller holds the media key the payload is encrypted under.
3603
+ if (type === 'mediaretry') {
3604
+ try {
3605
+ const { parseRetryNotification } = require('./messages/MediaRetry');
3606
+ const parsed = parseRetryNotification(node, {
3607
+ findChild, getContent: getNodeContent
3608
+ });
3609
+ if (parsed) {
3610
+ _whaDbg('[DBG] MEDIA_RETRY notification id=' + parsed.messageId +
3611
+ (parsed.error ? ' error=' + parsed.error.code : ' payload=' +
3612
+ (parsed.ciphertext ? parsed.ciphertext.length + 'B' : 'none')));
3613
+ this.emit('media_retry', parsed);
3614
+ }
3615
+ } catch (err) {
3616
+ _whaDbg('[DBG] MEDIA_RETRY parse failed: ' + (err && err.message));
3617
+ }
3618
+ return;
3619
+ }
3620
+
3532
3621
  if (type === 'account_sync') {
3533
3622
  // Account sync notification — may contain device list updates
3534
3623
  const devicesNode = findChildDeep(node, 'devices');
@@ -655,8 +655,29 @@ async function fetchPlayVersion(business) {
655
655
  async function fetchAndroidVersionLive(business) {
656
656
  const fromPlay = await fetchPlayVersion(business);
657
657
  if (fromPlay) return fromPlay;
658
- _whaDbg('[DBG] PLAY_VERSION unavailable — falling back to the store page');
659
- return fetchAndroidVersion(business);
658
+
659
+ // Play did not answer. That happens on any host it will not serve — a
660
+ // datacenter IP is the usual one, since the dispenser handing out the
661
+ // anonymous account rate-limits them hard — so this path is what a bot on
662
+ // hosting actually runs, every time.
663
+ //
664
+ // The store page is tried next, but it cannot be trusted on its own: it has
665
+ // been observed answering 2.26.27.85 while Play was serving 2.26.31.77. Left
666
+ // as-is that is worse than useless, because a value older than the pinned
667
+ // version still counts as an answer and shadows it — the caller's "never go
668
+ // backwards" guard then reads it as "already current" and the session sits
669
+ // on whatever it had, with the newer pinned version never once applied.
670
+ //
671
+ // So the pinned version is a floor, not a last resort: anything the page
672
+ // says that is older than it is discarded in its favour.
673
+ const fromPage = await fetchAndroidVersion(business);
674
+ if (fromPage && compareVersions(fromPage, ANDROID_VERSION_FALLBACK) > 0) {
675
+ _whaDbg('[DBG] PLAY_VERSION unavailable — the store page says ' + fromPage);
676
+ return fromPage;
677
+ }
678
+ _whaDbg('[DBG] PLAY_VERSION unavailable, and the store page said ' +
679
+ (fromPage || 'nothing') + ' — using the pinned ' + ANDROID_VERSION_FALLBACK);
680
+ return ANDROID_VERSION_FALLBACK;
660
681
  }
661
682
 
662
683
  async function fetchIosVersion(business) {
@@ -0,0 +1,228 @@
1
+ 'use strict';
2
+
3
+ // Asking the sender's phone to upload a media file again.
4
+ //
5
+ // Media is not carried in the message. The message carries a URL, a SHA-256 of
6
+ // the encrypted bytes and the key to decrypt them, and the bytes themselves sit
7
+ // on WhatsApp's CDN with a limited life. A file that has aged out, or one whose
8
+ // message arrived through history sync long after the fact, answers the
9
+ // download with 404 or 410 — and at that point the media is gone as far as this
10
+ // client is concerned, however healthy the message looks.
11
+ //
12
+ // The sender's phone still has the original. This is how it is asked to put it
13
+ // back: a <receipt type="server-error"> naming the message, carrying a small
14
+ // encrypted payload that proves we are the intended recipient. The phone
15
+ // re-uploads, and the answer comes back as a <notification type="mediaretry">
16
+ // with a fresh URL — encrypted the same way, so only we can read it.
17
+ //
18
+ // The proof is what makes this safe. The receipt encrypts the message id under
19
+ // a key derived from the message's own media key, which only the sender and the
20
+ // recipients hold. Someone who merely knows a message id cannot produce a valid
21
+ // one, so this cannot be used to make a phone re-upload anything on request.
22
+ //
23
+ // receipt key = HKDF-SHA256(mediaKey, info = "WhatsApp Media Retry Notification", 32)
24
+ // ciphertext = AES-256-GCM(receipt key, iv = random 12B, ServerErrorReceipt,
25
+ // aad = message id)
26
+ //
27
+ // The notification comes back under the same key, with the same message id as
28
+ // the additional data.
29
+
30
+ const crypto = require('crypto');
31
+ const { hkdf } = require('@noble/hashes/hkdf');
32
+ const { sha256 } = require('@noble/hashes/sha256');
33
+
34
+ const RETRY_KEY_INFO = 'WhatsApp Media Retry Notification';
35
+ const IV_SIZE = 12;
36
+
37
+ // ─── protobuf helpers ───────────────────────────────────────────────────────
38
+ //
39
+ // Two tiny messages, hand-encoded rather than pulled through the schema — the
40
+ // whole of what travels here is one string out and a handful of fields back.
41
+
42
+ function pbString(field, str) {
43
+ const buf = Buffer.from(str, 'utf8');
44
+ return Buffer.concat([Buffer.from([(field << 3) | 2]), varint(buf.length), buf]);
45
+ }
46
+
47
+ function varint(n) {
48
+ const out = [];
49
+ do { let b = n & 0x7f; n >>>= 7; if (n) b |= 0x80; out.push(b); } while (n);
50
+ return Buffer.from(out);
51
+ }
52
+
53
+ function readVarint(buf, pos) {
54
+ let result = 0, shift = 0, byte;
55
+ do {
56
+ if (pos >= buf.length) throw new Error('truncated varint');
57
+ byte = buf[pos++];
58
+ result |= (byte & 0x7f) << shift;
59
+ shift += 7;
60
+ } while (byte & 0x80);
61
+ return [result, pos];
62
+ }
63
+
64
+ // Every field of the notification, by number. Unknown ones are kept rather than
65
+ // dropped so a schema that grows does not read as a corrupt message here.
66
+ function readFields(buf) {
67
+ const out = {};
68
+ let pos = 0;
69
+ while (pos < buf.length) {
70
+ let key; [key, pos] = readVarint(buf, pos);
71
+ const num = key >> 3, wire = key & 7;
72
+ if (wire === 2) {
73
+ let len; [len, pos] = readVarint(buf, pos);
74
+ out[num] = buf.slice(pos, pos + len);
75
+ pos += len;
76
+ } else if (wire === 0) {
77
+ let val; [val, pos] = readVarint(buf, pos);
78
+ out[num] = val;
79
+ } else if (wire === 5) { pos += 4; }
80
+ else if (wire === 1) { pos += 8; }
81
+ else break;
82
+ }
83
+ return out;
84
+ }
85
+
86
+ // ─── key derivation ─────────────────────────────────────────────────────────
87
+
88
+ /** The AES key both halves of the exchange are encrypted under. */
89
+ function mediaRetryKey(mediaKey) {
90
+ const ikm = Buffer.isBuffer(mediaKey) ? mediaKey : Buffer.from(mediaKey);
91
+ return Buffer.from(
92
+ hkdf(sha256, ikm, Buffer.alloc(0), Buffer.from(RETRY_KEY_INFO, 'utf8'), 32));
93
+ }
94
+
95
+ /**
96
+ * Encrypt the receipt that asks for a re-upload.
97
+ *
98
+ * @param {string} msgId the message whose media is missing
99
+ * @param {Buffer} mediaKey that message's media key
100
+ * @returns {{ciphertext: Buffer, iv: Buffer}}
101
+ */
102
+ function encryptRetryReceipt(msgId, mediaKey) {
103
+ // ServerErrorReceipt { stanzaId = 1 }
104
+ const plaintext = pbString(1, msgId);
105
+ const iv = crypto.randomBytes(IV_SIZE);
106
+ const cipher = crypto.createCipheriv('aes-256-gcm', mediaRetryKey(mediaKey), iv);
107
+ // The message id is the additional data, so a receipt cannot be lifted from
108
+ // one message and replayed against another.
109
+ cipher.setAAD(Buffer.from(msgId, 'utf8'));
110
+ const enc = Buffer.concat([cipher.update(plaintext), cipher.final()]);
111
+ return { ciphertext: Buffer.concat([enc, cipher.getAuthTag()]), iv };
112
+ }
113
+
114
+ /**
115
+ * Decrypt the notification that answers one.
116
+ *
117
+ * @param {object} notif { messageId, ciphertext, iv } off the wire
118
+ * @param {Buffer} mediaKey the same media key the receipt was sent with
119
+ * @returns {{result: number, directPath: string|null, url: string|null,
120
+ * handle: string|null, ciphertextSha256: Buffer|null}}
121
+ */
122
+ function decryptRetryNotification(notif, mediaKey) {
123
+ const { messageId, ciphertext, iv } = notif;
124
+ if (!ciphertext || !iv) throw new Error('media retry notification carried no payload');
125
+
126
+ const key = mediaRetryKey(mediaKey);
127
+ const tag = ciphertext.slice(ciphertext.length - 16);
128
+ const body = ciphertext.slice(0, ciphertext.length - 16);
129
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
130
+ decipher.setAuthTag(tag);
131
+ decipher.setAAD(Buffer.from(messageId, 'utf8'));
132
+ const plaintext = Buffer.concat([decipher.update(body), decipher.final()]);
133
+
134
+ // MediaRetryNotification { stanzaId = 1, directPath = 2, result = 3 }
135
+ const fields = readFields(plaintext);
136
+ const result = typeof fields[3] === 'number' ? fields[3] : null;
137
+
138
+ return {
139
+ result,
140
+ // 1 = SUCCESS in the enum; anything else means the phone could not oblige.
141
+ ok: result === 1,
142
+ directPath: fields[2] ? fields[2].toString('utf8') : null,
143
+ stanzaId: fields[1] ? fields[1].toString('utf8') : messageId
144
+ };
145
+ }
146
+
147
+ /**
148
+ * The <receipt> that asks the sender's phone for a re-upload.
149
+ *
150
+ * @param {object} info { id, chatJid, fromMe, participant }
151
+ * @param {Buffer} mediaKey
152
+ * @param {string} ownJid who the receipt is addressed to — ourselves
153
+ * @param {Function} BinaryNode
154
+ */
155
+ function buildRetryReceiptNode(info, mediaKey, ownJid, BinaryNode) {
156
+ const { ciphertext, iv } = encryptRetryReceipt(info.id, mediaKey);
157
+
158
+ const rmrAttrs = {
159
+ jid: info.chatJid,
160
+ from_me: String(!!info.fromMe)
161
+ };
162
+ // Only a group receipt names the member the message came from; on a DM the
163
+ // chat jid already says it, and sending it anyway is a shape no real client
164
+ // produces.
165
+ if (info.participant) rmrAttrs.participant = info.participant;
166
+
167
+ return new BinaryNode('receipt', {
168
+ id: info.id,
169
+ to: ownJid,
170
+ type: 'server-error'
171
+ }, [
172
+ new BinaryNode('encrypt', {}, [
173
+ new BinaryNode('enc_p', {}, ciphertext),
174
+ new BinaryNode('enc_iv', {}, iv)
175
+ ]),
176
+ new BinaryNode('rmr', rmrAttrs, null)
177
+ ]);
178
+ }
179
+
180
+ /**
181
+ * Read a <notification type="mediaretry"> into the pieces the caller needs.
182
+ *
183
+ * Returns null when the node is not one, so a caller can hand it every
184
+ * notification without checking first. An `error` in the result means the phone
185
+ * answered but declined — usually because it no longer has the file either.
186
+ */
187
+ function parseRetryNotification(node, helpers) {
188
+ const { findChild, getContent } = helpers;
189
+ const attrs = (node && node.attrs) || {};
190
+
191
+ const rmr = findChild(node, 'rmr');
192
+ if (!rmr) return null;
193
+
194
+ const out = {
195
+ messageId: attrs.id || null,
196
+ timestamp: attrs.t ? Number(attrs.t) : null,
197
+ chatJid: (rmr.attrs && rmr.attrs.jid) || null,
198
+ fromMe: !!(rmr.attrs && rmr.attrs.from_me === 'true'),
199
+ participant: (rmr.attrs && rmr.attrs.participant) || null,
200
+ error: null,
201
+ ciphertext: null,
202
+ iv: null
203
+ };
204
+
205
+ const errNode = findChild(node, 'error');
206
+ if (errNode) {
207
+ out.error = { code: (errNode.attrs && Number(errNode.attrs.code)) || 0 };
208
+ return out;
209
+ }
210
+
211
+ const encrypt = findChild(node, 'encrypt');
212
+ if (encrypt) {
213
+ const p = findChild(encrypt, 'enc_p');
214
+ const ivN = findChild(encrypt, 'enc_iv');
215
+ out.ciphertext = p ? getContent(p) : null;
216
+ out.iv = ivN ? getContent(ivN) : null;
217
+ }
218
+ return out;
219
+ }
220
+
221
+ module.exports = {
222
+ mediaRetryKey,
223
+ encryptRetryReceipt,
224
+ decryptRetryNotification,
225
+ buildRetryReceiptNode,
226
+ parseRetryNotification,
227
+ RETRY_KEY_INFO
228
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "whalibmob",
3
- "version": "5.22.0",
4
- "description": "WhatsApp library for interaction with WhatsApp Mobile API and web ",
3
+ "version": "5.23.1",
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",
7
7
  "bin": {
@@ -23,13 +23,29 @@
23
23
  },
24
24
  "keywords": [
25
25
  "whatsapp",
26
+ "whatsapp-api",
27
+ "whatsapp-bot",
28
+ "whatsapp-client",
29
+ "whatsapp-web",
30
+ "sms-registration",
31
+ "pairing-code",
32
+ "qr-code",
33
+ "companion",
34
+ "multi-device",
26
35
  "signal-protocol",
27
- "mobile",
28
- "client",
29
- "encryption",
30
36
  "e2e",
37
+ "e2ee",
38
+ "encryption",
39
+ "mobile",
40
+ "android",
31
41
  "ios",
32
- "multi-device"
42
+ "messaging",
43
+ "chatbot",
44
+ "automation",
45
+ "whatsapp-mobile",
46
+ "media",
47
+ "groups",
48
+ "channels"
33
49
  ],
34
50
  "repository": {
35
51
  "type": "git",