@badzz88/baileys 8.5.4 → 8.5.5

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.
Files changed (107) hide show
  1. package/README.md +1 -1
  2. package/WAProto/WAProto.proto +75 -1256
  3. package/WAProto/index.d.ts +19729 -36122
  4. package/WAProto/index.js +132193 -178538
  5. package/engine-requirements.js +15 -7
  6. package/lib/Defaults/index.js +14 -22
  7. package/lib/Signal/Group/group_cipher.js +5 -5
  8. package/lib/Signal/Group/keyhelper.js +4 -4
  9. package/lib/Signal/Group/sender-chain-key.js +2 -2
  10. package/lib/Signal/Group/sender-key-distribution-message.js +1 -1
  11. package/lib/Signal/Group/sender-key-message.js +3 -3
  12. package/lib/Signal/Group/sender-key-record.js +1 -1
  13. package/lib/Signal/Group/sender-message-key.js +5 -5
  14. package/lib/Signal/libsignal.js +28 -39
  15. package/lib/Signal/lid-mapping.js +8 -28
  16. package/lib/Socket/aigroups.js +5 -5
  17. package/lib/Socket/business.js +11 -3
  18. package/lib/Socket/chats.js +319 -668
  19. package/lib/Socket/communities.js +89 -85
  20. package/lib/Socket/graphql.js +228 -435
  21. package/lib/Socket/groups.js +211 -132
  22. package/lib/Socket/index.js +7 -9
  23. package/lib/Socket/interactive-handler.js +60 -117
  24. package/lib/Socket/interop.js +175 -56
  25. package/lib/Socket/luxu.js +387 -0
  26. package/lib/Socket/managed-account.js +48 -79
  27. package/lib/Socket/messages-recv.js +340 -898
  28. package/lib/Socket/messages-send.js +160 -462
  29. package/lib/Socket/mex.js +53 -40
  30. package/lib/Socket/newsletter.js +295 -291
  31. package/lib/Socket/privacy.js +265 -77
  32. package/lib/Socket/registration.js +157 -164
  33. package/lib/Socket/socket.js +199 -181
  34. package/lib/Socket/text-router.js +83 -0
  35. package/lib/Socket/username.js +227 -144
  36. package/lib/Store/keyed-db.js +108 -0
  37. package/lib/Store/make-cache-manager-store.js +2 -2
  38. package/lib/Store/make-in-memory-store.js +141 -25
  39. package/lib/Types/Auth.js +3 -3
  40. package/lib/Types/Label.js +1 -1
  41. package/lib/Types/LabelAssociation.js +1 -1
  42. package/lib/Types/Mex.js +112 -0
  43. package/lib/Types/Newsletter.js +65 -96
  44. package/lib/Types/State.js +4 -4
  45. package/lib/Types/index.js +1 -1
  46. package/lib/Utils/auth-utils.js +57 -52
  47. package/lib/Utils/browser-utils.js +1 -1
  48. package/lib/Utils/business.js +6 -2
  49. package/lib/Utils/chat-utils.js +38 -102
  50. package/lib/Utils/command-loader.d.ts +31 -0
  51. package/lib/Utils/command-loader.js +100 -0
  52. package/lib/Utils/companion-reg-client-utils.js +42 -0
  53. package/lib/Utils/crypto.js +84 -54
  54. package/lib/Utils/curve25519-js.js +275 -0
  55. package/lib/Utils/decode-wa-message.js +83 -316
  56. package/lib/Utils/event-buffer.js +61 -37
  57. package/lib/Utils/generics.js +103 -157
  58. package/lib/Utils/group-history.js +12 -3
  59. package/lib/Utils/history.js +15 -15
  60. package/lib/Utils/index.js +5 -1
  61. package/lib/Utils/link-preview.js +8 -3
  62. package/lib/Utils/lt-hash.js +35 -2
  63. package/lib/Utils/make-mutex.js +1 -1
  64. package/lib/Utils/message-retry-manager.js +92 -27
  65. package/lib/Utils/messages-media.js +130 -159
  66. package/lib/Utils/messages.js +566 -144
  67. package/lib/Utils/meta-ai-msmsg.js +202 -62
  68. package/lib/Utils/native-bridge.js +82 -0
  69. package/lib/Utils/noise-handler.js +21 -19
  70. package/lib/Utils/offline-node-processor.js +8 -3
  71. package/lib/Utils/pre-key-manager.js +23 -13
  72. package/lib/Utils/process-message.js +106 -197
  73. package/lib/Utils/reporting-utils.js +2 -2
  74. package/lib/Utils/session-pool.d.ts +16 -0
  75. package/lib/Utils/session-pool.js +94 -0
  76. package/lib/Utils/signal.js +63 -28
  77. package/lib/Utils/stanza-ack.js +12 -39
  78. package/lib/Utils/sticker.d.ts +17 -0
  79. package/lib/Utils/sticker.js +145 -0
  80. package/lib/Utils/sync-action-utils.js +7 -4
  81. package/lib/Utils/tc-token-utils.js +25 -20
  82. package/lib/Utils/use-multi-file-auth-state.js +14 -30
  83. package/lib/Utils/validate-connection.js +19 -59
  84. package/lib/Utils/view-once-cache.d.ts +9 -0
  85. package/lib/Utils/view-once-cache.js +79 -0
  86. package/lib/WABinary/decode.js +16 -50
  87. package/lib/WABinary/encode.js +224 -54
  88. package/lib/WABinary/generic-utils.js +97 -7
  89. package/lib/WABinary/jid-utils.js +14 -27
  90. package/lib/WAM/encode.js +7 -7
  91. package/lib/WAUSync/Protocols/USyncBusinessProtocol.js +1 -58
  92. package/lib/WAUSync/Protocols/USyncContactProtocol.js +1 -7
  93. package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +5 -13
  94. package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +4 -1
  95. package/lib/WAUSync/Protocols/USyncFeatureProtocol.js +7 -20
  96. package/lib/WAUSync/Protocols/USyncPictureProtocol.js +1 -1
  97. package/lib/WAUSync/Protocols/USyncSidelistProtocol.js +2 -2
  98. package/lib/WAUSync/Protocols/USyncStatusProtocol.js +2 -7
  99. package/lib/WAUSync/Protocols/USyncTextStatusProtocol.js +1 -1
  100. package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +1 -2
  101. package/lib/WAUSync/Protocols/index.js +0 -1
  102. package/lib/WAUSync/USyncQuery.js +6 -77
  103. package/lib/WAUSync/USyncUser.js +8 -16
  104. package/lib/WAUSync/index.js +0 -1
  105. package/lib/antiban.js +854 -608
  106. package/lib/index.js +1 -1
  107. package/package.json +20 -16
@@ -60,7 +60,6 @@ exports.getStatusCodeForMediaRetry =
60
60
  exports.downloadEncryptedContent =
61
61
  exports.downloadContentFromMessage =
62
62
  exports.getUrlFromDirectPath =
63
- exports.getMediaProp =
64
63
  exports.encryptedStream =
65
64
  exports.getHttpStream =
66
65
  exports.getStream =
@@ -94,7 +93,7 @@ const crypto_1 = require('./crypto')
94
93
  const generics_1 = require('./generics')
95
94
  const getTmpFilesDirectory = () => (0, os_1.tmpdir)()
96
95
  const getImageProcessingLibrary = async () => {
97
-
96
+ //@ts-ignore
98
97
  const [jimp, sharp] = await Promise.all([
99
98
  Promise.resolve()
100
99
  .then(() => __importStar(require('jimp')))
@@ -147,13 +146,13 @@ const getRawMediaUploadData = async (media, mediaType, logger) => {
147
146
  try {
148
147
  await fs_1.promises.unlink(filePath)
149
148
  } catch {
150
-
149
+ //
151
150
  }
152
151
  throw error
153
152
  }
154
153
  }
155
154
  exports.getRawMediaUploadData = getRawMediaUploadData
156
-
155
+ /** generates all the keys required to encrypt/decrypt & sign a media message */
157
156
  async function getMediaKeys(buffer, mediaType) {
158
157
  if (!buffer) {
159
158
  throw new boom_1.Boom('Cannot derive from empty media key')
@@ -161,7 +160,7 @@ async function getMediaKeys(buffer, mediaType) {
161
160
  if (typeof buffer === 'string') {
162
161
  buffer = Buffer.from(buffer.replace('data:;base64,', ''), 'base64')
163
162
  }
164
-
163
+ // expand using HKDF to 112 bytes, also pass in the relevant app info
165
164
  const expandedMediaKey = (0, crypto_1.hkdf)(buffer, 112, { info: (0, exports.hkdfInfoKey)(mediaType) })
166
165
  return {
167
166
  iv: expandedMediaKey.slice(0, 16),
@@ -169,7 +168,7 @@ async function getMediaKeys(buffer, mediaType) {
169
168
  macKey: expandedMediaKey.slice(48, 80)
170
169
  }
171
170
  }
172
-
171
+ /** Extracts video thumb using FFMPEG */
173
172
  const extractVideoThumb = async (path, destPath, time, size) =>
174
173
  new Promise((resolve, reject) => {
175
174
  const cmd = `ffmpeg -ss ${time} -i ${path} -y -vf scale=${size.width}:-1 -vframes 1 -f image2 ${destPath}`
@@ -182,8 +181,8 @@ const extractVideoThumb = async (path, destPath, time, size) =>
182
181
  })
183
182
  })
184
183
  const extractImageThumb = async (bufferOrFilePath, width = 32) => {
185
-
186
-
184
+ // TODO: Move entirely to sharp, removing jimp as it supports readable streams
185
+ // This will have positive speed and performance impacts as well as minimizing RAM usage.
187
186
  if (bufferOrFilePath instanceof stream_1.Readable) {
188
187
  bufferOrFilePath = await (0, exports.toBuffer)(bufferOrFilePath)
189
188
  }
@@ -226,9 +225,9 @@ const generateProfilePicture = async (mediaUpload, dimensions) => {
226
225
  if (Buffer.isBuffer(mediaUpload)) {
227
226
  buffer = mediaUpload
228
227
  } else {
229
-
228
+ // Use getStream to handle all WAMediaUpload types (Buffer, Stream, URL)
230
229
  const { stream } = await (0, exports.getStream)(mediaUpload)
231
-
230
+ // Convert the resulting stream to a buffer
232
231
  buffer = await (0, exports.toBuffer)(stream)
233
232
  }
234
233
  const lib = await getImageProcessingLibrary()
@@ -254,7 +253,7 @@ const generateProfilePicture = async (mediaUpload, dimensions) => {
254
253
  }
255
254
  }
256
255
  exports.generateProfilePicture = generateProfilePicture
257
-
256
+ /** gets the SHA256 of the given media message */
258
257
  const mediaMessageSHA256B64 = message => {
259
258
  const media = Object.values(message)[0]
260
259
  return media?.fileSha256 && Buffer.from(media.fileSha256).toString('base64')
@@ -275,10 +274,12 @@ async function getAudioDuration(buffer) {
275
274
  }
276
275
  return metadata.format.duration
277
276
  }
278
-
277
+ /**
278
+ referenced from and modifying https://github.com/wppconnect-team/wa-js/blob/main/src/chat/functions/prepareAudioWaveform.ts
279
+ */
279
280
  async function getAudioWaveform(buffer, logger) {
280
281
  try {
281
-
282
+ // @ts-ignore
282
283
  const { default: decoder } = await Promise.resolve().then(() => __importStar(require('audio-decode')))
283
284
  let audioData
284
285
  if (Buffer.isBuffer(buffer)) {
@@ -290,22 +291,22 @@ async function getAudioWaveform(buffer, logger) {
290
291
  audioData = await (0, exports.toBuffer)(buffer)
291
292
  }
292
293
  const audioBuffer = await decoder(audioData)
293
- const rawData = audioBuffer.getChannelData(0)
294
- const samples = 64
295
- const blockSize = Math.floor(rawData.length / samples)
294
+ const rawData = audioBuffer.getChannelData(0) // We only need to work with one channel of data
295
+ const samples = 64 // Number of samples we want to have in our final data set
296
+ const blockSize = Math.floor(rawData.length / samples) // the number of samples in each subdivision
296
297
  const filteredData = []
297
298
  for (let i = 0; i < samples; i++) {
298
- const blockStart = blockSize * i
299
+ const blockStart = blockSize * i // the location of the first sample in the block
299
300
  let sum = 0
300
301
  for (let j = 0; j < blockSize; j++) {
301
- sum = sum + Math.abs(rawData[blockStart + j])
302
+ sum = sum + Math.abs(rawData[blockStart + j]) // find the sum of all the samples in the block
302
303
  }
303
- filteredData.push(sum / blockSize)
304
+ filteredData.push(sum / blockSize) // divide the sum by the block size to get the average
304
305
  }
305
-
306
+ // This guarantees that the largest data point will be set to 1, and the rest of the data will scale proportionally.
306
307
  const multiplier = Math.pow(Math.max(...filteredData), -1)
307
308
  const normalizedData = filteredData.map(n => n * multiplier)
308
-
309
+ // Generate waveform like WhatsApp
309
310
  const waveform = new Uint8Array(normalizedData.map(n => Math.floor(100 * n)))
310
311
  return waveform
311
312
  } catch (e) {
@@ -346,7 +347,7 @@ const getStream = async (item, opts) => {
346
347
  return { stream: (0, fs_1.createReadStream)(item.url), type: 'file' }
347
348
  }
348
349
  exports.getStream = getStream
349
-
350
+ /** generates a thumbnail for a given media, if required */
350
351
  async function generateThumbnail(file, mediaType, options) {
351
352
  let thumbnail
352
353
  let originalImageDimensions
@@ -384,7 +385,7 @@ const getHttpStream = async (url, options = {}) => {
384
385
  if (!response.ok) {
385
386
  throw new boom_1.Boom(`Failed to fetch stream from ${url}`, { statusCode: response.status, data: { url } })
386
387
  }
387
-
388
+ // @ts-ignore Node18+ Readable.fromWeb exists
388
389
  return response.body instanceof stream_1.Readable ? response.body : stream_1.Readable.fromWeb(response.body)
389
390
  }
390
391
  exports.getHttpStream = getHttpStream
@@ -415,7 +416,7 @@ const encryptedStream = async (media, mediaType, { logger, saveOriginalFileIfReq
415
416
  const onChunk = async buff => {
416
417
  sha256Enc.update(buff)
417
418
  hmac.update(buff)
418
-
419
+ // Handle backpressure: if write returns false, wait for drain
419
420
  if (!encFileWriteStream.write(buff)) {
420
421
  await (0, events_1.once)(encFileWriteStream, 'drain')
421
422
  }
@@ -449,8 +450,8 @@ const encryptedStream = async (media, mediaType, { logger, saveOriginalFileIfReq
449
450
  encFileWriteStream.end()
450
451
  originalFileStream?.end?.()
451
452
  stream.destroy()
452
-
453
-
453
+ // Wait for write streams to fully flush to disk
454
+ // This helps reduce memory pressure by allowing OS to release buffers
454
455
  await encFinishPromise
455
456
  await originalFinishPromise
456
457
  logger?.debug('encrypted data successfully')
@@ -464,7 +465,7 @@ const encryptedStream = async (media, mediaType, { logger, saveOriginalFileIfReq
464
465
  fileLength
465
466
  }
466
467
  } catch (error) {
467
-
468
+ // destroy all streams with error
468
469
  encFileWriteStream.destroy()
469
470
  originalFileStream?.destroy?.()
470
471
  aes.destroy()
@@ -491,74 +492,25 @@ const toSmallestChunkSize = num => {
491
492
  }
492
493
  const getUrlFromDirectPath = directPath => `https://${DEF_HOST}${directPath}`
493
494
  exports.getUrlFromDirectPath = getUrlFromDirectPath
494
-
495
- const getMediaProp = (mediaAbProps, propName) => {
496
- if (!mediaAbProps || typeof mediaAbProps !== 'object') return false
497
- return !!mediaAbProps[propName]
498
- }
499
- exports.getMediaProp = getMediaProp
500
495
  const downloadContentFromMessage = async ({ mediaKey, directPath, url }, type, opts = {}) => {
501
- const directUrl = directPath ? (0, exports.getUrlFromDirectPath)(directPath) : undefined
502
- const fallbackUrl = url?.startsWith('https://') ? url : undefined
503
-
504
-
505
- let downloadUrl
506
- if (directUrl) {
507
- downloadUrl = directUrl
508
- } else if (fallbackUrl) {
509
- downloadUrl = fallbackUrl
510
- } else {
496
+ const isValidMediaUrl = url?.startsWith('https://mmg.whatsapp.net/')
497
+ const downloadUrl = isValidMediaUrl ? url : (0, exports.getUrlFromDirectPath)(directPath)
498
+ if (!downloadUrl) {
511
499
  throw new boom_1.Boom('No valid media URL or directPath present in message', { statusCode: 400 })
512
500
  }
513
501
  const keys = await getMediaKeys(mediaKey, type)
514
-
515
- const mediaAbProps = opts.mediaAbProps
516
- const pjpegHeaders = {}
517
- if (type === 'image' && getMediaProp(mediaAbProps, 'partial_pjpeg_enabled')) {
518
-
519
- pjpegHeaders['X-WhatsApp-PJPEG'] = '1'
520
- }
521
- if (type === 'image' && getMediaProp(mediaAbProps, 'multi_scan_pjpeg')) {
522
- pjpegHeaders['X-WhatsApp-Multi-Scan-PJPEG'] = '1'
523
- }
524
- const mergedOpts = Object.keys(pjpegHeaders).length
525
- ? { ...opts, options: { ...(opts.options || {}), headers: { ...(opts.options?.headers || {}), ...pjpegHeaders } } }
526
- : opts
527
- try {
528
- return await (0, exports.downloadEncryptedContent)(downloadUrl, keys, mergedOpts)
529
- } catch (err) {
530
-
531
- if (directUrl && fallbackUrl && fallbackUrl !== directUrl) {
532
- return (0, exports.downloadEncryptedContent)(fallbackUrl, keys, mergedOpts)
533
- }
534
- throw err
535
- }
502
+ return (0, exports.downloadEncryptedContent)(downloadUrl, keys, opts)
536
503
  }
537
504
  exports.downloadContentFromMessage = downloadContentFromMessage
538
-
539
- const downloadMediaChunk = (message, type, startByte, endByte, opts = {}) =>
540
- downloadContentFromMessage(message, type, { ...opts, startByte, endByte })
541
- exports.downloadMediaChunk = downloadMediaChunk
542
-
543
- async function* streamMediaChunks(message, type, opts = {}) {
544
- const chunkSize = opts.chunkSize || 2 * 1024 * 1024
545
- const fileLength = message.fileLength
546
- if (!fileLength || fileLength <= chunkSize) {
547
- yield downloadContentFromMessage(message, type, opts)
548
- return
549
- }
550
- for (let offset = 0; offset < fileLength; offset += chunkSize) {
551
- const endByte = Math.min(offset + chunkSize - 1, fileLength - 1)
552
- yield downloadContentFromMessage(message, type, { ...opts, startByte: offset, endByte })
553
- }
554
- }
555
- exports.streamMediaChunks = streamMediaChunks
556
-
505
+ /**
506
+ * Decrypts and downloads an AES256-CBC encrypted file given the keys.
507
+ * Assumes the SHA256 of the plaintext is appended to the end of the ciphertext
508
+ * */
557
509
  const downloadEncryptedContent = async (downloadUrl, { cipherKey, iv }, { startByte, endByte, options } = {}) => {
558
510
  let bytesFetched = 0
559
511
  let startChunk = 0
560
512
  let firstBlockIsIV = false
561
-
513
+ // if a start byte is specified -- then we need to fetch the previous chunk as that will form the IV
562
514
  if (startByte) {
563
515
  const chunk = toSmallestChunkSize(startByte || 0)
564
516
  if (chunk) {
@@ -579,7 +531,7 @@ const downloadEncryptedContent = async (downloadUrl, { cipherKey, iv }, { startB
579
531
  headers.Range += endChunk
580
532
  }
581
533
  }
582
-
534
+ // download the message
583
535
  const fetched = await (0, exports.getHttpStream)(downloadUrl, {
584
536
  ...(options || {}),
585
537
  headers
@@ -609,8 +561,8 @@ const downloadEncryptedContent = async (downloadUrl, { cipherKey, iv }, { startB
609
561
  data = data.slice(AES_CHUNK_SIZE)
610
562
  }
611
563
  aes = Crypto.createDecipheriv('aes-256-cbc', cipherKey, ivValue)
612
-
613
-
564
+ // if an end byte that is not EOF is specified
565
+ // stop auto padding (PKCS7) -- otherwise throws an error for decryption
614
566
  if (endByte) {
615
567
  aes.setAutoPadding(false)
616
568
  }
@@ -631,15 +583,7 @@ const downloadEncryptedContent = async (downloadUrl, { cipherKey, iv }, { startB
631
583
  }
632
584
  }
633
585
  })
634
-
635
-
636
-
637
- ;(0, stream_1.pipeline)(fetched, output, error => {
638
- if (error && !output.destroyed) {
639
- output.destroy(error)
640
- }
641
- })
642
- return output
586
+ return fetched.pipe(output, { end: true })
643
587
  }
644
588
  exports.downloadEncryptedContent = downloadEncryptedContent
645
589
  function extensionForMediaMessage(message) {
@@ -662,14 +606,6 @@ const isNodeRuntime = () => {
662
606
  typeof globalThis.Deno === 'undefined'
663
607
  )
664
608
  }
665
-
666
-
667
-
668
-
669
- const isFetchDispatcher = agent => {
670
- return !!agent && typeof agent.dispatch === 'function'
671
- }
672
- exports.isFetchDispatcher = isFetchDispatcher
673
609
  const uploadWithNodeHttp = async ({ url, filePath, headers, timeoutMs, agent }, redirectCount = 0) => {
674
610
  if (redirectCount > 5) {
675
611
  throw new Error('Too many redirects')
@@ -679,7 +615,7 @@ const uploadWithNodeHttp = async ({ url, filePath, headers, timeoutMs, agent },
679
615
  parsedUrl.protocol === 'https:'
680
616
  ? await Promise.resolve().then(() => __importStar(require('https')))
681
617
  : await Promise.resolve().then(() => __importStar(require('http')))
682
-
618
+ // Get file size for Content-Length header (required for Node.js streaming)
683
619
  const fileStats = await fs_1.promises.stat(filePath)
684
620
  const fileSize = fileStats.size
685
621
  return new Promise((resolve, reject) => {
@@ -693,13 +629,13 @@ const uploadWithNodeHttp = async ({ url, filePath, headers, timeoutMs, agent },
693
629
  ...headers,
694
630
  'Content-Length': fileSize
695
631
  },
696
- agent: isFetchDispatcher(agent) ? undefined : agent,
632
+ agent,
697
633
  timeout: timeoutMs
698
634
  },
699
635
  res => {
700
-
636
+ // Handle redirects (3xx)
701
637
  if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
702
- res.resume()
638
+ res.resume() // Consume response to free resources
703
639
  const newUrl = new url_1.URL(res.headers.location, url).toString()
704
640
  resolve(
705
641
  (0, exports.uploadWithNodeHttp)(
@@ -719,9 +655,13 @@ const uploadWithNodeHttp = async ({ url, filePath, headers, timeoutMs, agent },
719
655
  res.on('data', chunk => (body += chunk))
720
656
  res.on('end', () => {
721
657
  try {
722
- resolve(JSON.parse(body))
658
+ const parsed = JSON.parse(body)
659
+ if (parsed && typeof parsed === 'object') {
660
+ Object.defineProperty(parsed, '__statusCode', { value: res.statusCode, enumerable: false })
661
+ }
662
+ resolve(parsed)
723
663
  } catch {
724
- resolve(undefined)
664
+ resolve({ __statusCode: res.statusCode })
725
665
  }
726
666
  })
727
667
  }
@@ -741,11 +681,11 @@ const uploadWithNodeHttp = async ({ url, filePath, headers, timeoutMs, agent },
741
681
  }
742
682
  exports.uploadWithNodeHttp = uploadWithNodeHttp
743
683
  const uploadWithFetch = async ({ url, filePath, headers, timeoutMs, agent }) => {
744
-
684
+ // Convert Node.js Readable to Web ReadableStream
745
685
  const nodeStream = (0, fs_1.createReadStream)(filePath)
746
686
  const webStream = stream_1.Readable.toWeb(nodeStream)
747
687
  const response = await fetch(url, {
748
- dispatcher: isFetchDispatcher(agent) ? agent : undefined,
688
+ dispatcher: agent,
749
689
  method: 'POST',
750
690
  body: webStream,
751
691
  headers,
@@ -753,32 +693,49 @@ const uploadWithFetch = async ({ url, filePath, headers, timeoutMs, agent }) =>
753
693
  signal: timeoutMs ? AbortSignal.timeout(timeoutMs) : undefined
754
694
  })
755
695
  try {
756
- return await response.json()
696
+ const parsed = await response.json()
697
+ if (parsed && typeof parsed === 'object') {
698
+ Object.defineProperty(parsed, '__statusCode', { value: response.status, enumerable: false })
699
+ }
700
+ return parsed
757
701
  } catch {
758
- return undefined
759
- }
760
- }
761
-
702
+ return { __statusCode: response.status }
703
+ }
704
+ }
705
+ /**
706
+ * Uploads media to WhatsApp servers.
707
+ *
708
+ * ## Why we have two upload implementations:
709
+ *
710
+ * Node.js's native `fetch` (powered by undici) has a known bug where it buffers
711
+ * the entire request body in memory before sending, even when using streams.
712
+ * This causes memory issues with large files (e.g., 1GB file = 1GB+ memory usage).
713
+ * See: https://github.com/nodejs/undici/issues/4058
714
+ *
715
+ * Other runtimes (Bun, Deno, browsers) correctly stream the request body without
716
+ * buffering, so we can use the web-standard Fetch API there.
717
+ *
718
+ * ## Future considerations:
719
+ * Once the undici bug is fixed, we can simplify this to use only the Fetch API
720
+ * across all runtimes. Monitor the GitHub issue for updates.
721
+ */
762
722
  const uploadMedia = async (params, logger) => {
763
- if (isNodeRuntime() && !isFetchDispatcher(params.agent)) {
723
+ if (isNodeRuntime()) {
764
724
  logger?.debug('Using Node.js https module for upload (avoids undici buffering bug)')
765
725
  return (0, exports.uploadWithNodeHttp)(params)
766
726
  } else {
767
-
768
-
769
727
  logger?.debug('Using web-standard Fetch API for upload')
770
728
  return uploadWithFetch(params)
771
729
  }
772
730
  }
773
- exports.uploadMedia = uploadMedia
774
731
  const getWAUploadToServer = ({ customUploadHosts, fetchAgent, logger, options }, refreshMediaConn) => {
775
732
  return async (filePath, { mediaType, fileEncSha256B64, timeoutMs }) => {
776
-
733
+ // send a query JSON to obtain the url & auth token to upload our media
777
734
  let uploadInfo = await refreshMediaConn(false)
778
735
  let urls
779
736
  const hosts = [...customUploadHosts, ...uploadInfo.hosts]
780
737
  fileEncSha256B64 = (0, exports.encodeBase64EncodedStringForUpload)(fileEncSha256B64)
781
-
738
+ // Prepare common headers
782
739
  const customHeaders = (() => {
783
740
  const hdrs = options?.headers
784
741
  if (!hdrs) return {}
@@ -794,37 +751,52 @@ const getWAUploadToServer = ({ customUploadHosts, fetchAgent, logger, options },
794
751
  const auth = encodeURIComponent(uploadInfo.auth)
795
752
  const url = `https://${hostname}${Defaults_1.MEDIA_PATH_MAP[mediaType]}/${fileEncSha256B64}?auth=${auth}&token=${fileEncSha256B64}`
796
753
  let result
797
- try {
798
- result = await uploadMedia(
799
- {
800
- url,
801
- filePath,
802
- headers,
803
- timeoutMs,
804
- agent: fetchAgent
805
- },
806
- logger
807
- )
808
- if (result?.url || result?.direct_path) {
809
- urls = {
810
- mediaUrl: result.url,
811
- directPath: result.direct_path,
812
- meta_hmac: result.meta_hmac,
813
- fbid: result.fbid,
814
- ts: result.ts
754
+ const maxRateLimitRetries = 3
755
+ for (let attempt = 0; attempt <= maxRateLimitRetries; attempt++) {
756
+ try {
757
+ result = await uploadMedia(
758
+ {
759
+ url,
760
+ filePath,
761
+ headers,
762
+ timeoutMs,
763
+ agent: fetchAgent
764
+ },
765
+ logger
766
+ )
767
+ const isRateLimited = result?.__statusCode === 429 || result?.__statusCode === 503
768
+ if (isRateLimited && attempt < maxRateLimitRetries) {
769
+ const backoffMs = Math.min(500 * 2 ** attempt, 8000) + Math.floor(Math.random() * 250)
770
+ logger.warn(
771
+ { hostname, attempt, backoffMs, statusCode: result.__statusCode },
772
+ 'media upload rate-limited, backing off before retry'
773
+ )
774
+ await new Promise(resolve => setTimeout(resolve, backoffMs))
775
+ continue
815
776
  }
777
+ if (result?.url || result?.direct_path) {
778
+ urls = {
779
+ mediaUrl: result.url,
780
+ directPath: result.direct_path,
781
+ meta_hmac: result.meta_hmac,
782
+ fbid: result.fbid,
783
+ ts: result.ts
784
+ }
785
+ } else {
786
+ uploadInfo = await refreshMediaConn(true)
787
+ throw new Error(`upload failed, reason: ${JSON.stringify(result)}`)
788
+ }
789
+ break
790
+ } catch (error) {
791
+ const isLast = hostname === hosts[uploadInfo.hosts.length - 1]?.hostname
792
+ logger.warn(
793
+ { trace: error?.stack, uploadResult: result },
794
+ `Error in uploading to ${hostname} ${isLast ? '' : ', retrying...'}`
795
+ )
816
796
  break
817
- } else {
818
- uploadInfo = await refreshMediaConn(true)
819
- throw new Error(`upload failed, reason: ${JSON.stringify(result)}`)
820
797
  }
821
- } catch (error) {
822
- const isLast = hostname === hosts[uploadInfo.hosts.length - 1]?.hostname
823
- logger.warn(
824
- { trace: error?.stack, uploadResult: result },
825
- `Error in uploading to ${hostname} ${isLast ? '' : ', retrying...'}`
826
- )
827
798
  }
799
+ if (urls) break
828
800
  }
829
801
  if (!urls) {
830
802
  throw new boom_1.Boom('Media upload failed on all hosts', { statusCode: 500 })
@@ -834,12 +806,11 @@ const getWAUploadToServer = ({ customUploadHosts, fetchAgent, logger, options },
834
806
  }
835
807
  exports.getWAUploadToServer = getWAUploadToServer
836
808
  const getMediaRetryKey = mediaKey => {
837
- if (typeof mediaKey === 'string') {
838
- mediaKey = Buffer.from(mediaKey.replace('data:;base64,', ''), 'base64')
839
- }
840
809
  return (0, crypto_1.hkdf)(mediaKey, 32, { info: 'WhatsApp Media Retry Notification' })
841
810
  }
842
-
811
+ /**
812
+ * Generate a binary node that will request the phone to re-upload the media & return the newly uploaded URL
813
+ */
843
814
  const encryptMediaRetryRequest = (key, mediaKey, meId) => {
844
815
  const recp = { stanzaId: key.id }
845
816
  const recpBuffer = index_js_1.proto.ServerErrorReceipt.encode(recp).finish()
@@ -854,9 +825,9 @@ const encryptMediaRetryRequest = (key, mediaKey, meId) => {
854
825
  type: 'server-error'
855
826
  },
856
827
  content: [
857
-
858
-
859
-
828
+ // this encrypt node is actually pretty useless
829
+ // the media is returned even without this node
830
+ // keeping it here to maintain parity with WA Web
860
831
  {
861
832
  tag: 'encrypt',
862
833
  attrs: {},
@@ -870,7 +841,7 @@ const encryptMediaRetryRequest = (key, mediaKey, meId) => {
870
841
  attrs: {
871
842
  jid: key.remoteJid,
872
843
  from_me: (!!key.fromMe).toString(),
873
-
844
+ // @ts-ignore
874
845
  participant: key.participant || undefined
875
846
  }
876
847
  }