@badzz88/baileys 8.5.3 → 8.5.4

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