@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.
- package/README.md +1 -1
- package/WAProto/WAProto.proto +75 -1256
- package/WAProto/index.d.ts +19729 -36122
- package/WAProto/index.js +132193 -178538
- package/engine-requirements.js +15 -7
- package/lib/Defaults/index.js +14 -22
- package/lib/Signal/Group/group_cipher.js +5 -5
- package/lib/Signal/Group/keyhelper.js +4 -4
- package/lib/Signal/Group/sender-chain-key.js +2 -2
- package/lib/Signal/Group/sender-key-distribution-message.js +1 -1
- package/lib/Signal/Group/sender-key-message.js +3 -3
- package/lib/Signal/Group/sender-key-record.js +1 -1
- package/lib/Signal/Group/sender-message-key.js +5 -5
- package/lib/Signal/libsignal.js +28 -39
- package/lib/Signal/lid-mapping.js +8 -28
- package/lib/Socket/aigroups.js +5 -5
- package/lib/Socket/business.js +11 -3
- package/lib/Socket/chats.js +319 -668
- package/lib/Socket/communities.js +89 -85
- package/lib/Socket/graphql.js +228 -435
- package/lib/Socket/groups.js +211 -132
- package/lib/Socket/index.js +7 -9
- package/lib/Socket/interactive-handler.js +60 -117
- package/lib/Socket/interop.js +175 -56
- package/lib/Socket/luxu.js +387 -0
- package/lib/Socket/managed-account.js +48 -79
- package/lib/Socket/messages-recv.js +340 -898
- package/lib/Socket/messages-send.js +160 -462
- package/lib/Socket/mex.js +53 -40
- package/lib/Socket/newsletter.js +295 -291
- package/lib/Socket/privacy.js +265 -77
- package/lib/Socket/registration.js +157 -164
- package/lib/Socket/socket.js +199 -181
- package/lib/Socket/text-router.js +83 -0
- package/lib/Socket/username.js +227 -144
- package/lib/Store/keyed-db.js +108 -0
- package/lib/Store/make-cache-manager-store.js +2 -2
- package/lib/Store/make-in-memory-store.js +141 -25
- package/lib/Types/Auth.js +3 -3
- package/lib/Types/Label.js +1 -1
- package/lib/Types/LabelAssociation.js +1 -1
- package/lib/Types/Mex.js +112 -0
- package/lib/Types/Newsletter.js +65 -96
- package/lib/Types/State.js +4 -4
- package/lib/Types/index.js +1 -1
- package/lib/Utils/auth-utils.js +57 -52
- package/lib/Utils/browser-utils.js +1 -1
- package/lib/Utils/business.js +6 -2
- package/lib/Utils/chat-utils.js +38 -102
- package/lib/Utils/command-loader.d.ts +31 -0
- package/lib/Utils/command-loader.js +100 -0
- package/lib/Utils/companion-reg-client-utils.js +42 -0
- package/lib/Utils/crypto.js +84 -54
- package/lib/Utils/curve25519-js.js +275 -0
- package/lib/Utils/decode-wa-message.js +83 -316
- package/lib/Utils/event-buffer.js +61 -37
- package/lib/Utils/generics.js +103 -157
- package/lib/Utils/group-history.js +12 -3
- package/lib/Utils/history.js +15 -15
- package/lib/Utils/index.js +5 -1
- package/lib/Utils/link-preview.js +8 -3
- package/lib/Utils/lt-hash.js +35 -2
- package/lib/Utils/make-mutex.js +1 -1
- package/lib/Utils/message-retry-manager.js +92 -27
- package/lib/Utils/messages-media.js +130 -159
- package/lib/Utils/messages.js +566 -144
- package/lib/Utils/meta-ai-msmsg.js +202 -62
- package/lib/Utils/native-bridge.js +82 -0
- package/lib/Utils/noise-handler.js +21 -19
- package/lib/Utils/offline-node-processor.js +8 -3
- package/lib/Utils/pre-key-manager.js +23 -13
- package/lib/Utils/process-message.js +106 -197
- package/lib/Utils/reporting-utils.js +2 -2
- package/lib/Utils/session-pool.d.ts +16 -0
- package/lib/Utils/session-pool.js +94 -0
- package/lib/Utils/signal.js +63 -28
- package/lib/Utils/stanza-ack.js +12 -39
- package/lib/Utils/sticker.d.ts +17 -0
- package/lib/Utils/sticker.js +145 -0
- package/lib/Utils/sync-action-utils.js +7 -4
- package/lib/Utils/tc-token-utils.js +25 -20
- package/lib/Utils/use-multi-file-auth-state.js +14 -30
- package/lib/Utils/validate-connection.js +19 -59
- package/lib/Utils/view-once-cache.d.ts +9 -0
- package/lib/Utils/view-once-cache.js +79 -0
- package/lib/WABinary/decode.js +16 -50
- package/lib/WABinary/encode.js +224 -54
- package/lib/WABinary/generic-utils.js +97 -7
- package/lib/WABinary/jid-utils.js +14 -27
- package/lib/WAM/encode.js +7 -7
- package/lib/WAUSync/Protocols/USyncBusinessProtocol.js +1 -58
- package/lib/WAUSync/Protocols/USyncContactProtocol.js +1 -7
- package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +5 -13
- package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +4 -1
- package/lib/WAUSync/Protocols/USyncFeatureProtocol.js +7 -20
- package/lib/WAUSync/Protocols/USyncPictureProtocol.js +1 -1
- package/lib/WAUSync/Protocols/USyncSidelistProtocol.js +2 -2
- package/lib/WAUSync/Protocols/USyncStatusProtocol.js +2 -7
- package/lib/WAUSync/Protocols/USyncTextStatusProtocol.js +1 -1
- package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +1 -2
- package/lib/WAUSync/Protocols/index.js +0 -1
- package/lib/WAUSync/USyncQuery.js +6 -77
- package/lib/WAUSync/USyncUser.js +8 -16
- package/lib/WAUSync/index.js +0 -1
- package/lib/antiban.js +854 -608
- package/lib/index.js +1 -1
- 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
|
|
502
|
-
const
|
|
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
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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
|
-
|
|
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
|
|
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()
|
|
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
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
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
|
}
|