@nexustechpro/baileys 2.0.6 → 2.1.2

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.
@@ -135,13 +135,13 @@ export function decodeMessageNode(stanza, meId, meLid) {
135
135
  const isMe = (jid) => areJidsSameUser(jid, meId)
136
136
  const isMeLid = (jid) => areJidsSameUser(jid, meLid)
137
137
  if (isPnUser(from) || isLidUser(from) || isHostedLidUser(from) || isHostedPnUser(from)) {
138
+ if (isMe(from) || isMeLid(from)) {
139
+ fromMe = true
140
+ }
138
141
  if (recipient && !isJidMetaAI(recipient)) {
139
- if (!isMe(from) && !isMeLid(from)) {
142
+ if (!fromMe) {
140
143
  throw new Boom("receipient present, but msg not from me", { data: stanza })
141
144
  }
142
- if (isMe(from) || isMeLid(from)) {
143
- fromMe = true
144
- }
145
145
  chatId = recipient
146
146
  } else {
147
147
  chatId = from
@@ -254,12 +254,13 @@ export const decryptMessageNode = (stanza, meId, meLid, repository, logger) => {
254
254
  })
255
255
  } catch (decryptErr) {
256
256
  const errMsg = decryptErr?.message || decryptErr?.toString() || ''
257
- if (errMsg.includes('invalid sender key session') || errMsg.includes('memory access out of bounds')) {
258
- logger?.debug?.({ group: sender, author: author, errMsg }, '[Signal] Stale/corrupt sender key session, deleting')
257
+ if (errMsg.includes('memory access out of bounds')) {
258
+ console.error('[Signal] Stale sender key — group:', sender, 'author:', author, 'err:', errMsg)
259
259
  try {
260
260
  await repository.deleteSenderKey(sender, author)
261
+ console.error('[Signal] Sender key deleted successfully')
261
262
  } catch (e) {
262
- logger?.warn?.({ err: e }, 'Failed to delete sender key')
263
+ console.error('[Signal] Failed to delete sender key:', e)
263
264
  }
264
265
  }
265
266
  throw decryptErr
@@ -267,24 +268,8 @@ export const decryptMessageNode = (stanza, meId, meLid, repository, logger) => {
267
268
  break
268
269
  case "pkmsg":
269
270
  case "msg":
270
- try {
271
- msgBuffer = await repository.decryptMessage({ jid: decryptionJid, type: e2eType, ciphertext: content })
272
- if (msgBuffer === null) {
273
- throw new Error("DuplicatedMessage")
274
- }
275
- } catch (decryptErr) {
276
- const errMsg = decryptErr?.message || decryptErr?.toString() || ''
277
- if ((errMsg.includes('InvalidMessage') || errMsg.includes('BadMac')) && e2eType === 'msg') {
278
- logger?.debug?.({ jid: decryptionJid, errMsg }, '[Signal] Stale session (InvalidMessage/BadMac), deleting')
279
- try {
280
- const toDelete = [decryptionJid]
281
- await repository.deleteSession(toDelete)
282
- } catch { }
283
- }
284
- const cleanError = new Error(errMsg || 'Decryption failed')
285
- cleanError.stack = decryptErr?.stack
286
- throw cleanError
287
- }
271
+ msgBuffer = await repository.decryptMessage({ jid: decryptionJid, type: e2eType, ciphertext: content })
272
+ if (msgBuffer === null) return // DuplicatedMessage libsignal already handled it silently
288
273
  break
289
274
  case "plaintext":
290
275
  msgBuffer = content
@@ -324,7 +309,7 @@ export const decryptMessageNode = (stanza, meId, meLid, repository, logger) => {
324
309
  } catch (err) {
325
310
  const errorMessage = err?.message || err?.toString() || ""
326
311
  const errStr = err?.message || (typeof err === "string" ? err : "") || ""
327
- const isExpectedDecryptErr = errStr.includes("InvalidPreKeyId") || errStr.includes("SessionNotFound") || errStr.includes("InvalidMessage") || errStr.includes("no sender key state") || errStr.includes("invalid sender key session") || errStr.includes("memory access out of bounds") || errStr.includes("old counter") || errStr.includes("DuplicatedMessage") || errStr.includes("BadMac")
312
+ const isExpectedDecryptErr = errStr.includes("InvalidPreKeyId") || errStr.includes("SessionNotFound") || errStr.includes("InvalidMessage") || errStr.includes("no sender key state") || errStr.includes("memory access out of bounds") || errStr.includes("old counter") || errStr.includes("DuplicatedMessage") || errStr.includes("BadMac")
328
313
  ; (isExpectedDecryptErr ? logger?.debug?.bind(logger) : logger?.error?.bind(logger))?.({ key: fullMessage.key, err, errorMessage, messageType: tag === "plaintext" ? "plaintext" : attrs.type, sender, author }, "failed to decrypt message")
329
314
  fullMessage.messageStubType = proto.WebMessageInfo.StubType.CIPHERTEXT
330
315
  fullMessage.messageStubParameters = [errorMessage]
@@ -8,7 +8,7 @@ export const migrateIndexKey = async (keys, type) => {
8
8
  const oldKey = '_index'
9
9
  const newKey = 'index'
10
10
  const oldData = await keys.get(type, [oldKey])
11
- if (oldData?.[oldKey] && typeof oldData[oldKey] === 'object') { // only migrate if old blob actually exists and has data
11
+ if (oldData?.[oldKey] !== null && oldData?.[oldKey] !== undefined && typeof oldData[oldKey] === 'object') { // only migrate if old blob actually exists and has data
12
12
  await keys.set({ [type]: { [newKey]: oldData[oldKey], [oldKey]: null } })
13
13
  return oldData[oldKey]
14
14
  }
@@ -1,108 +1,171 @@
1
- import { promises as dnsPromises } from 'dns'
2
1
  import { getLinkPreview } from 'link-preview-js'
2
+ import mql from '@microlink/mql'
3
+ import { lookup } from 'dns'
4
+ import { promisify } from 'util'
5
+ import { LRUCache } from 'lru-cache'
3
6
  import { prepareWAMessageMedia } from './messages.js'
4
7
  import { extractImageThumb, getHttpStream } from './messages-media.js'
5
8
 
6
- const THUMBNAIL_WIDTH_PX = 192
7
- const MAX_REDIRECTS = 5
8
- const PREVIEW_TIMEOUT = 5000
9
- // Concurrency pool — max simultaneous link preview fetches
9
+ const dnsLookup = promisify(lookup)
10
+ const THUMBNAIL_WIDTH = 192
11
+ const TIMEOUT = 4_000
10
12
  const MAX_CONCURRENT = 20
13
+ const MAX_INFLIGHT = 1000
14
+
15
+ const _previewCache = new LRUCache({ max: 500, ttl: 1000 * 60 * 10 })
16
+ const _negCache = new LRUCache({ max: 500, ttl: 1000 * 60 * 2 })
17
+ const _thumbCache = new LRUCache({ max: 200, ttl: 1000 * 60 * 30 })
18
+ const _inflight = new Map()
19
+
11
20
  let _active = 0
12
21
  const _queue = []
22
+
13
23
  const _drain = () => {
14
- if (_queue.length === 0 || _active >= MAX_CONCURRENT) return
24
+ if (!_queue.length || _active >= MAX_CONCURRENT) return
15
25
  _active++
16
26
  const { fn, resolve, reject } = _queue.shift()
17
27
  fn().then(resolve).catch(reject).finally(() => { _active--; _drain() })
18
28
  }
19
- const _enqueue = fn => new Promise((resolve, reject) => { _queue.push({ fn, resolve, reject }); _drain() })
20
29
 
21
- /** Fetches a remote image and compresses it to a JPEG thumbnail buffer */
30
+ const _enqueue = fn => new Promise((resolve, reject) => {
31
+ _queue.push({ fn, resolve, reject }); _drain()
32
+ })
33
+
34
+ const _resolveDNS = async url => {
35
+ try { return (await dnsLookup(new URL(url).hostname)).address }
36
+ catch { return new URL(url).hostname }
37
+ }
38
+
39
+ const _normalize = text => {
40
+ const t = text.trim()
41
+ return /^https?:\/\//i.test(t) ? t : `https://${t}`
42
+ }
43
+
44
+ const _previewType = (mediaType, image) => {
45
+ if (!mediaType) return image ? 5 : 0
46
+ const mt = mediaType.toLowerCase()
47
+ if (mt === 'video' || mt.startsWith('video.')) return 1
48
+ return mt === 'image' || image ? 5 : 0
49
+ }
50
+
22
51
  const _compressedThumb = async (url, opts) => {
23
52
  const stream = await getHttpStream(url, opts.fetchOpts)
24
- const result = await extractImageThumb(stream, opts.thumbnailWidth ?? THUMBNAIL_WIDTH_PX)
25
- return result.buffer
53
+ return (await extractImageThumb(stream, opts.thumbnailWidth ?? THUMBNAIL_WIDTH)).buffer
26
54
  }
27
55
 
28
- /** Resolves jpegThumbnail + highQualityThumbnail from an image URL, never throws */
29
56
  const _resolveThumbnail = async (image, opts) => {
30
57
  if (!image) return {}
58
+ const key = `thumb:${image}`
59
+ const hit = _thumbCache.get(key)
60
+ if (hit) return hit
61
+
62
+ let thumbs = {}
31
63
  if (opts.uploadImage) {
32
64
  try {
33
65
  const { imageMessage } = await prepareWAMessageMedia(
34
66
  { image: { url: image } },
35
67
  { upload: opts.uploadImage, mediaTypeOverride: 'thumbnail-link', options: opts.fetchOpts }
36
68
  )
37
- return {
38
- jpegThumbnail: imageMessage?.jpegThumbnail ? Buffer.from(imageMessage.jpegThumbnail) : undefined,
39
- highQualityThumbnail: imageMessage ?? undefined
40
- }
41
- } catch (err) {
42
- opts.logger?.warn({ err: err.message, url: image }, 'upload failed, falling back to compressed thumb')
69
+ const jpeg = imageMessage?.jpegThumbnail
70
+ ? Buffer.from(imageMessage.jpegThumbnail)
71
+ : await _compressedThumb(image, opts).catch(() => undefined)
72
+ thumbs = { jpegThumbnail: jpeg, highQualityThumbnail: imageMessage ?? undefined }
73
+ } catch {
74
+ try { thumbs = { jpegThumbnail: await _compressedThumb(image, opts) } } catch { }
43
75
  }
76
+ } else {
77
+ try { thumbs = { jpegThumbnail: await _compressedThumb(image, opts) } } catch { }
44
78
  }
45
- // Fallback: local compression (also used when no uploadImage provided)
79
+
80
+ if (thumbs.jpegThumbnail) _thumbCache.set(key, thumbs)
81
+ return thumbs
82
+ }
83
+
84
+ const _tryLinkPreview = async (url, opts) => {
46
85
  try {
47
- return { jpegThumbnail: await _compressedThumb(image, opts) }
86
+ const info = await getLinkPreview(url, {
87
+ timeout: opts.fetchOpts?.timeout ?? TIMEOUT,
88
+ followRedirects: 'follow',
89
+ resolveDNSHost: _resolveDNS,
90
+ })
91
+ if (info?.title) return info
48
92
  } catch (err) {
49
- opts.logger?.debug({ err: err.stack }, 'compressed thumb failed')
50
- return {}
93
+ opts.logger?.warn({ err: err?.message || err, url }, 'getLinkPreview failed')
51
94
  }
95
+ return undefined
52
96
  }
53
97
 
54
- /**
55
- * Extracts link preview info from a text string or URL.
56
- * Queued for concurrency control — never throws on preview failure.
57
- * @param {string} text - Raw text or URL to preview
58
- * @param {object} opts - Options: thumbnailWidth, fetchOpts, uploadImage, logger
59
- * @returns {Promise<object|undefined>} urlInfo or undefined if no valid URL/title found
60
- */
61
- export const getUrlInfo = (text, opts = {}) => _enqueue(async () => {
62
- const fetchOpts = opts.fetchOpts ?? { timeout: PREVIEW_TIMEOUT }
63
- const thumbnailWidth = opts.thumbnailWidth ?? THUMBNAIL_WIDTH_PX
64
- const resolvedOpts = { ...opts, fetchOpts, thumbnailWidth }
98
+ const _tryMicrolink = async (url, opts) => {
65
99
  try {
66
- let retries = 0
67
- const previewLink = (text.startsWith('https://') || text.startsWith('http://')) ? text : 'https://' + text
68
- const info = await getLinkPreview(previewLink, {
69
- ...fetchOpts,
70
- followRedirects: 'manual',
71
- handleRedirects: (baseURL, forwardedURL) => {
72
- if (retries >= MAX_REDIRECTS) return false
73
- const base = new URL(baseURL)
74
- const fwd = new URL(forwardedURL)
75
- const sameHost = fwd.hostname === base.hostname || fwd.hostname === 'www.' + base.hostname || 'www.' + fwd.hostname === base.hostname
76
- if (sameHost) { retries++; return true }
77
- return false
78
- },
79
- resolveDNSHost: async (url) => {
80
- const hostname = new URL(url).hostname
81
- try {
82
- const { address } = await dnsPromises.lookup(hostname)
83
- return address
84
- } catch {
85
- return hostname
86
- }
87
- },
88
- headers: fetchOpts.headers
89
- })
90
- if (!info || !('title' in info) || !info.title) return undefined
91
- const [image] = info.images ?? []
92
- const thumbs = await _resolveThumbnail(image, resolvedOpts)
100
+ const { data } = await mql(url)
101
+ if (!data?.title) return undefined
93
102
  return {
94
- 'canonical-url': info.url,
95
- 'matched-text': text,
96
- title: info.title,
97
- description: info.description,
98
- originalThumbnailUrl: image,
99
- ...thumbs
103
+ url: data.url ?? url,
104
+ title: data.title,
105
+ description: data.description ?? '',
106
+ images: [data.image?.url].filter(Boolean),
107
+ mediaType: 'website',
100
108
  }
101
109
  } catch (err) {
102
- // Suppress "no valid URL" and missing module errors; re-throw everything else
103
- if (!err.message?.includes('receive a valid') && err.code !== 'ERR_MODULE_NOT_FOUND' && err.code !== 'MODULE_NOT_FOUND') {
104
- throw err
105
- }
110
+ opts.logger?.warn({ err: err?.message || err, url }, 'microlink failed')
106
111
  }
107
- })
108
- //# sourceMappingURL=link-preview.js.map
112
+ return undefined
113
+ }
114
+
115
+ const _buildResult = async (info, text, opts) => {
116
+ const [image] = info.images ?? []
117
+ return {
118
+ 'canonical-url': info.url,
119
+ 'matched-text': text,
120
+ title: info.title,
121
+ description: info.description,
122
+ originalThumbnailUrl: image,
123
+ previewType: _previewType(info.mediaType, image),
124
+ ...await _resolveThumbnail(image, opts),
125
+ }
126
+ }
127
+
128
+ export const getUrlInfo = (text, opts = {}) => {
129
+ const url = _normalize(text)
130
+
131
+ if (_negCache.has(url)) return Promise.resolve(undefined)
132
+ if (_previewCache.has(url)) return Promise.resolve(_previewCache.get(url))
133
+ if (_inflight.has(url)) return _inflight.get(url)
134
+ if (_inflight.size >= MAX_INFLIGHT) return Promise.resolve(undefined)
135
+
136
+ const o = {
137
+ fetchOpts: { timeout: TIMEOUT, ...opts.fetchOpts },
138
+ thumbnailWidth: opts.thumbnailWidth ?? THUMBNAIL_WIDTH,
139
+ uploadImage: opts.uploadImage,
140
+ logger: opts.logger,
141
+ }
142
+
143
+ const promise = _enqueue(async () => {
144
+ try {
145
+ // Primary: link-preview-js
146
+ let info = await _tryLinkPreview(url, o)
147
+
148
+ // Fallback: microlink (if no result or no image)
149
+ if (!info?.title || !(info.images ?? []).length) {
150
+ o.logger?.debug({ url }, 'falling back to microlink')
151
+ info = await _tryMicrolink(url, o) ?? info
152
+ }
153
+
154
+ if (!info?.title) {
155
+ _negCache.set(url, true)
156
+ return undefined
157
+ }
158
+
159
+ const result = await _buildResult(info, text, o)
160
+ _previewCache.set(url, result)
161
+ return result
162
+ } catch (err) {
163
+ _negCache.set(url, true)
164
+ if (!err.message?.includes('receive a valid')) throw err
165
+ return undefined
166
+ }
167
+ }).finally(() => _inflight.delete(url))
168
+
169
+ _inflight.set(url, promise)
170
+ return promise
171
+ }
@@ -2,8 +2,8 @@ import { Boom } from '@hapi/boom'
2
2
  import { spawn } from 'child_process'
3
3
  import * as Crypto from 'crypto'
4
4
  import { once } from 'events'
5
- import { createReadStream, createWriteStream, promises as fs } from 'fs'
6
- import { tmpdir } from 'os'
5
+ import { createReadStream, createWriteStream, mkdirSync, promises as fs } from 'fs'
6
+ import { tmpdir as osTmpdir } from 'os'
7
7
  import { join } from 'path'
8
8
  import { Readable, Transform } from 'stream'
9
9
  import { URL } from 'url'
@@ -24,6 +24,22 @@ export const getImageProcessingLibrary = async () => {
24
24
  throw new Boom('No image processing library available')
25
25
  }
26
26
 
27
+ // ─── CUSTOM TMPDIR (real disk, fall back to tmpfs if cwd isnt writable (e.g, read-only Docker containers)) ─────────────────────────────────────
28
+ const BAILEYS_TMP_DIR = (() => {
29
+ const candidates = [
30
+ join(process.cwd(), '.b-tmp'),
31
+ join(osTmpdir(), 'b-tmp'),
32
+ ]
33
+ for (const dir of candidates) {
34
+ try {
35
+ mkdirSync(dir, { recursive: true, mode: 0o700 })
36
+ return dir
37
+ } catch { }
38
+ }
39
+ return osTmpdir()
40
+ })()
41
+ const tmpdir = () => BAILEYS_TMP_DIR
42
+
27
43
  // ─── FFMPEG ───────────────────────────────────────────────────────────────────
28
44
  let ffmpegPathResolved = null
29
45
  const getFfmpegPath = async () => {
@@ -42,7 +58,7 @@ export const hkdfInfoKey = (type) => `WhatsApp ${MEDIA_HKDF_KEY_MAPPING[type]} K
42
58
  export const getMediaKeys = async (buffer, mediaType) => {
43
59
  if (!buffer) throw new Boom('Cannot derive from empty media key')
44
60
  if (typeof buffer === 'string') buffer = Buffer.from(buffer.replace('data:;base64,', ''), 'base64')
45
- const expandedMediaKey = await hkdf(buffer, 112, { info: hkdfInfoKey(mediaType) })
61
+ const expandedMediaKey = hkdf(buffer, 112, { info: hkdfInfoKey(mediaType) })
46
62
  return {
47
63
  iv: expandedMediaKey.slice(0, 16),
48
64
  cipherKey: expandedMediaKey.slice(16, 48),
@@ -270,16 +286,18 @@ export const getStream = async (item, opts) => {
270
286
  if (Buffer.isBuffer(item)) return { stream: toReadable(item), type: 'buffer' }
271
287
  if (item?.stream?.pipe) return { stream: item.stream, type: 'readable' }
272
288
  if (item?.pipe) return { stream: item, type: 'readable' }
289
+ const isHttpUrl = (str) => { try { const u = new URL(str); return u.protocol === 'http:' || u.protocol === 'https:' } catch { return false } }
290
+
273
291
  if (item && typeof item === 'object' && 'url' in item) {
274
292
  const urlStr = item.url.toString()
275
293
  if (Buffer.isBuffer(item.url)) return { stream: toReadable(item.url), type: 'buffer' }
276
294
  if (urlStr.startsWith('data:')) return { stream: toReadable(Buffer.from(urlStr.split(',')[1], 'base64')), type: 'buffer' }
277
- if (urlStr.startsWith('http')) return { stream: await getHttpStream(item.url, opts), type: 'remote' }
295
+ if (isHttpUrl(urlStr)) return { stream: await getHttpStream(item.url, opts), type: 'remote' }
278
296
  return { stream: createReadStream(item.url), type: 'file' }
279
297
  }
280
298
  if (typeof item === 'string') {
281
299
  if (item.startsWith('data:')) return { stream: toReadable(Buffer.from(item.split(',')[1], 'base64')), type: 'buffer' }
282
- if (item.startsWith('http')) return { stream: await getHttpStream(item, opts), type: 'remote' }
300
+ if (isHttpUrl(item)) return { stream: await getHttpStream(item, opts), type: 'remote' }
283
301
  return { stream: createReadStream(item), type: 'file' }
284
302
  }
285
303
  throw new Boom(`Invalid input type for getStream: ${typeof item}`, { statusCode: 400 })
@@ -340,52 +358,100 @@ export const encryptedStream = async (media, mediaType, { logger, saveOriginalFi
340
358
  finalStream = (await getStream(media, opts)).stream
341
359
  }
342
360
  }
361
+
343
362
  const mediaKey = providedMediaKey || Crypto.randomBytes(32)
344
363
  const { cipherKey, iv, macKey } = await getMediaKeys(mediaKey, mediaType)
345
- const encFilePath = join(tmpdir(), mediaType + generateMessageIDV2() + '-enc')
346
- const encFileWriteStream = createWriteStream(encFilePath)
347
- let originalFileStream, originalFilePath
348
- if (saveOriginalFileIfRequired) {
349
- originalFilePath = join(tmpdir(), mediaType + generateMessageIDV2() + '-original')
350
- originalFileStream = createWriteStream(originalFilePath)
351
- }
352
- let fileLength = 0
364
+
353
365
  const aes = Crypto.createCipheriv('aes-256-cbc', cipherKey, iv)
354
366
  const hmac = Crypto.createHmac('sha256', macKey).update(iv)
355
367
  const sha256Plain = Crypto.createHash('sha256')
356
368
  const sha256Enc = Crypto.createHash('sha256')
369
+ const encChunks = []
370
+ const plainChunks = saveOriginalFileIfRequired ? [] : null
371
+ let fileLength = 0
372
+
357
373
  try {
358
374
  for await (const data of finalStream) {
359
375
  fileLength += data.length
360
- if (type === 'remote' && opts?.maxContentLength && fileLength > opts.maxContentLength) throw new Boom('content length exceeded', { data: { media, type } })
361
- if (originalFileStream && !originalFileStream.write(data)) await once(originalFileStream, 'drain')
376
+ if (type === 'remote' && opts?.maxContentLength && fileLength > opts.maxContentLength) {
377
+ throw new Boom('content length exceeded', { data: { media, type } })
378
+ }
379
+ if (plainChunks) plainChunks.push(data)
362
380
  sha256Plain.update(data)
363
381
  const encrypted = aes.update(data)
364
382
  sha256Enc.update(encrypted)
365
383
  hmac.update(encrypted)
366
- encFileWriteStream.write(encrypted)
384
+ encChunks.push(encrypted)
367
385
  }
386
+
368
387
  const finalData = aes.final()
369
388
  sha256Enc.update(finalData)
370
389
  hmac.update(finalData)
371
- encFileWriteStream.write(finalData)
390
+ encChunks.push(finalData)
391
+
372
392
  const mac = hmac.digest().slice(0, 10)
373
393
  sha256Enc.update(mac)
374
- encFileWriteStream.write(mac)
375
- encFileWriteStream.end()
376
- originalFileStream?.end?.()
394
+ encChunks.push(mac)
395
+
377
396
  finalStream.destroy()
378
- logger?.debug('encrypted data successfully')
379
- return { mediaKey, bodyPath: originalFilePath, encFilePath, mac, fileEncSha256: sha256Enc.digest(), fileSha256: sha256Plain.digest(), fileLength, opusConverted }
397
+ logger?.debug('encrypted data in memory')
398
+
399
+ const encBuffer = Buffer.concat(encChunks)
400
+ const fileEncSha256 = sha256Enc.digest()
401
+ const fileSha256 = sha256Plain.digest()
402
+
403
+ let originalFilePath = null
404
+ if (plainChunks) {
405
+ try {
406
+ originalFilePath = join(tmpdir(), mediaType + generateMessageIDV2() + '-original')
407
+ await fs.writeFile(originalFilePath, Buffer.concat(plainChunks))
408
+ logger?.debug('saved original file for processing')
409
+ } catch (err) {
410
+ logger?.warn({ err: err.message }, 'failed to save original file, bodyPath will be null')
411
+ originalFilePath = null
412
+ }
413
+ }
414
+ let encFilePath = null
415
+ let useMemory = false
416
+
417
+ try {
418
+ encFilePath = join(tmpdir(), mediaType + generateMessageIDV2() + '-enc')
419
+ await fs.writeFile(encFilePath, encBuffer)
420
+ logger?.debug('wrote enc file to disk')
421
+ } catch (err) {
422
+ logger?.warn({ err: err.message, code: err.code }, 'failed to write enc file to disk, falling back to memory upload')
423
+ if (encFilePath) {
424
+ try { await fs.unlink(encFilePath) } catch { }
425
+ encFilePath = null
426
+ }
427
+ useMemory = true
428
+ }
429
+
430
+ const cleanup = async () => {
431
+ if (encFilePath) try { await fs.unlink(encFilePath) } catch { }
432
+ if (originalFilePath) try { await fs.unlink(originalFilePath) } catch { }
433
+ }
434
+
435
+ return {
436
+ mediaKey,
437
+ bodyPath: originalFilePath,
438
+ encFilePath,
439
+ encBuffer: useMemory ? encBuffer : null,
440
+ mac,
441
+ fileEncSha256,
442
+ fileSha256,
443
+ fileLength,
444
+ opusConverted,
445
+ cleanup
446
+ }
380
447
  } catch (error) {
381
- encFileWriteStream.destroy()
382
- originalFileStream?.destroy?.()
383
448
  aes.destroy()
384
449
  hmac.destroy()
385
450
  sha256Plain.destroy()
386
451
  sha256Enc.destroy()
387
452
  finalStream.destroy()
388
- try { await fs.unlink(encFilePath); if (originalFilePath) await fs.unlink(originalFilePath) } catch (err) { logger?.error({ err }, 'failed deleting tmp files') }
453
+ if (encFilePath) try { await fs.unlink(encFilePath) } catch { }
454
+ if (originalFilePath) try { await fs.unlink(originalFilePath) } catch { }
389
455
  throw error
390
456
  }
391
457
  }
@@ -459,7 +525,12 @@ export const getWAUploadToServer = ({ customUploadHosts, fetchAgent, logger, opt
459
525
  const toUploadBody = async (input) => {
460
526
  if (!input) throw new Boom('Upload input is null or undefined', { statusCode: 400 })
461
527
  if (Buffer.isBuffer(input)) return input
462
- if (typeof input === 'string') return createReadStream(input)
528
+ if (typeof input === 'string') {
529
+ const stream = createReadStream(input)
530
+ stream.on('end', () => stream.destroy())
531
+ stream.on('error', () => stream.destroy())
532
+ return stream
533
+ }
463
534
  if (typeof ReadableStream !== 'undefined' && input instanceof ReadableStream) return Readable.fromWeb(input)
464
535
  if (typeof input.pipe === 'function' || typeof input[Symbol.asyncIterator] === 'function') return input
465
536
  throw new Boom(`Unsupported upload input type: ${Object.prototype.toString.call(input)}`, { statusCode: 400 })