@ryuu-reinzz/baileys 3.0.0-beta.2 → 3.0.0-beta.21

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.
@@ -75,7 +75,7 @@ export async function getMediaKeys(buffer, mediaType) {
75
75
  buffer = Buffer.from(buffer.replace('data:;base64,', ''), 'base64');
76
76
  }
77
77
  // expand using HKDF to 112 bytes, also pass in the relevant app info
78
- const expandedMediaKey = await hkdf(buffer, 112, { info: hkdfInfoKey(mediaType) });
78
+ const expandedMediaKey = hkdf(buffer, 112, { info: hkdfInfoKey(mediaType) });
79
79
  return {
80
80
  iv: expandedMediaKey.slice(0, 16),
81
81
  cipherKey: expandedMediaKey.slice(16, 48),
@@ -217,7 +217,7 @@ export async function getAudioWaveform(buffer, logger) {
217
217
  const blockStart = blockSize * i; // the location of the first sample in the block
218
218
  let sum = 0;
219
219
  for (let j = 0; j < blockSize; j++) {
220
- sum = sum + Math.abs(rawData[blockStart + j]); // find the sum of all the samples in the block
220
+ sum = sum + Math.abs(rawData[blockStart + j] ?? 0); // find the sum of all the samples in the block
221
221
  }
222
222
  filteredData.push(sum / blockSize); // divide the sum by the block size to get the average
223
223
  }
@@ -324,10 +324,13 @@ export const encryptedStream = async (media, mediaType, { logger, saveOriginalFi
324
324
  const hmac = Crypto.createHmac('sha256', macKey).update(iv);
325
325
  const sha256Plain = Crypto.createHash('sha256');
326
326
  const sha256Enc = Crypto.createHash('sha256');
327
- const onChunk = (buff) => {
327
+ const onChunk = async (buff) => {
328
328
  sha256Enc.update(buff);
329
329
  hmac.update(buff);
330
- encFileWriteStream.write(buff);
330
+ // Handle backpressure: if write returns false, wait for drain
331
+ if (!encFileWriteStream.write(buff)) {
332
+ await once(encFileWriteStream, 'drain');
333
+ }
331
334
  };
332
335
  try {
333
336
  for await (const data of stream) {
@@ -345,17 +348,23 @@ export const encryptedStream = async (media, mediaType, { logger, saveOriginalFi
345
348
  }
346
349
  }
347
350
  sha256Plain.update(data);
348
- onChunk(aes.update(data));
351
+ await onChunk(aes.update(data));
349
352
  }
350
- onChunk(aes.final());
353
+ await onChunk(aes.final());
351
354
  const mac = hmac.digest().slice(0, 10);
352
355
  sha256Enc.update(mac);
353
356
  const fileSha256 = sha256Plain.digest();
354
357
  const fileEncSha256 = sha256Enc.digest();
355
358
  encFileWriteStream.write(mac);
359
+ const encFinishPromise = once(encFileWriteStream, 'finish');
360
+ const originalFinishPromise = originalFileStream ? once(originalFileStream, 'finish') : Promise.resolve();
356
361
  encFileWriteStream.end();
357
362
  originalFileStream?.end?.();
358
363
  stream.destroy();
364
+ // Wait for write streams to fully flush to disk
365
+ // This helps reduce memory pressure by allowing OS to release buffers
366
+ await encFinishPromise;
367
+ await originalFinishPromise;
359
368
  logger?.debug('encrypted data successfully');
360
369
  return {
361
370
  mediaKey,
@@ -506,6 +515,117 @@ export function extensionForMediaMessage(message) {
506
515
  }
507
516
  return extension;
508
517
  }
518
+ const isNodeRuntime = () => {
519
+ return (typeof process !== 'undefined' &&
520
+ process.versions?.node !== null &&
521
+ typeof process.versions.bun === 'undefined' &&
522
+ typeof globalThis.Deno === 'undefined');
523
+ };
524
+ export const uploadWithNodeHttp = async ({ url, filePath, headers, timeoutMs, agent }, redirectCount = 0) => {
525
+ if (redirectCount > 5) {
526
+ throw new Error('Too many redirects');
527
+ }
528
+ const parsedUrl = new URL(url);
529
+ const httpModule = parsedUrl.protocol === 'https:' ? await import('https') : await import('http');
530
+ // Get file size for Content-Length header (required for Node.js streaming)
531
+ const fileStats = await fs.stat(filePath);
532
+ const fileSize = fileStats.size;
533
+ return new Promise((resolve, reject) => {
534
+ const req = httpModule.request({
535
+ hostname: parsedUrl.hostname,
536
+ port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80),
537
+ path: parsedUrl.pathname + parsedUrl.search,
538
+ method: 'POST',
539
+ headers: {
540
+ ...headers,
541
+ 'Content-Length': fileSize
542
+ },
543
+ agent,
544
+ timeout: timeoutMs
545
+ }, res => {
546
+ // Handle redirects (3xx)
547
+ if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
548
+ res.resume(); // Consume response to free resources
549
+ const newUrl = new URL(res.headers.location, url).toString();
550
+ resolve(uploadWithNodeHttp({
551
+ url: newUrl,
552
+ filePath,
553
+ headers,
554
+ timeoutMs,
555
+ agent
556
+ }, redirectCount + 1));
557
+ return;
558
+ }
559
+ let body = '';
560
+ res.on('data', chunk => (body += chunk));
561
+ res.on('end', () => {
562
+ try {
563
+ resolve(JSON.parse(body));
564
+ }
565
+ catch {
566
+ resolve(undefined);
567
+ }
568
+ });
569
+ });
570
+ req.on('error', reject);
571
+ req.on('timeout', () => {
572
+ req.destroy();
573
+ reject(new Error('Upload timeout'));
574
+ });
575
+ const stream = createReadStream(filePath);
576
+ stream.pipe(req);
577
+ stream.on('error', err => {
578
+ req.destroy();
579
+ reject(err);
580
+ });
581
+ });
582
+ };
583
+ const uploadWithFetch = async ({ url, filePath, headers, timeoutMs, agent }) => {
584
+ // Convert Node.js Readable to Web ReadableStream
585
+ const nodeStream = createReadStream(filePath);
586
+ const webStream = Readable.toWeb(nodeStream);
587
+ const response = await fetch(url, {
588
+ dispatcher: agent,
589
+ method: 'POST',
590
+ body: webStream,
591
+ headers,
592
+ duplex: 'half',
593
+ signal: timeoutMs ? AbortSignal.timeout(timeoutMs) : undefined
594
+ });
595
+ try {
596
+ return (await response.json());
597
+ }
598
+ catch {
599
+ return undefined;
600
+ }
601
+ };
602
+ /**
603
+ * Uploads media to WhatsApp servers.
604
+ *
605
+ * ## Why we have two upload implementations:
606
+ *
607
+ * Node.js's native `fetch` (powered by undici) has a known bug where it buffers
608
+ * the entire request body in memory before sending, even when using streams.
609
+ * This causes memory issues with large files (e.g., 1GB file = 1GB+ memory usage).
610
+ * See: https://github.com/nodejs/undici/issues/4058
611
+ *
612
+ * Other runtimes (Bun, Deno, browsers) correctly stream the request body without
613
+ * buffering, so we can use the web-standard Fetch API there.
614
+ *
615
+ * ## Future considerations:
616
+ * Once the undici bug is fixed, we can simplify this to use only the Fetch API
617
+ * across all runtimes. Monitor the GitHub issue for updates.
618
+ */
619
+ const uploadMedia = async (params, logger) => {
620
+ if (isNodeRuntime()) {
621
+ logger?.debug('Using Node.js https module for upload (avoids undici buffering bug)');
622
+ return uploadWithNodeHttp(params);
623
+ }
624
+ else {
625
+ logger?.debug('Using web-standard Fetch API for upload');
626
+ return uploadWithFetch(params);
627
+ }
628
+ };
509
629
  export const getWAUploadToServer = ({ customUploadHosts, fetchAgent, logger, options }, refreshMediaConn) => {
510
630
  return async (filePath, { mediaType, fileEncSha256B64, timeoutMs }) => {
511
631
  // send a query JSON to obtain the url & auth token to upload our media
@@ -513,41 +633,32 @@ export const getWAUploadToServer = ({ customUploadHosts, fetchAgent, logger, opt
513
633
  let urls;
514
634
  const hosts = [...customUploadHosts, ...uploadInfo.hosts];
515
635
  fileEncSha256B64 = encodeBase64EncodedStringForUpload(fileEncSha256B64);
636
+ // Prepare common headers
637
+ const customHeaders = (() => {
638
+ const hdrs = options?.headers;
639
+ if (!hdrs)
640
+ return {};
641
+ return Array.isArray(hdrs) ? Object.fromEntries(hdrs) : hdrs;
642
+ })();
643
+ const headers = {
644
+ ...customHeaders,
645
+ 'Content-Type': 'application/octet-stream',
646
+ Origin: DEFAULT_ORIGIN
647
+ };
516
648
  for (const { hostname } of hosts) {
517
649
  logger.debug(`uploading to "${hostname}"`);
518
- const auth = encodeURIComponent(uploadInfo.auth); // the auth token
650
+ const auth = encodeURIComponent(uploadInfo.auth);
519
651
  const url = `https://${hostname}${MEDIA_PATH_MAP[mediaType]}/${fileEncSha256B64}?auth=${auth}&token=${fileEncSha256B64}`;
520
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
521
652
  let result;
522
653
  try {
523
- const stream = createReadStream(filePath);
524
- const response = await fetch(url, {
525
- dispatcher: fetchAgent,
526
- method: 'POST',
527
- body: stream,
528
- headers: {
529
- ...(() => {
530
- const hdrs = options?.headers;
531
- if (!hdrs)
532
- return {};
533
- return Array.isArray(hdrs) ? Object.fromEntries(hdrs) : hdrs;
534
- })(),
535
- 'Content-Type': 'application/octet-stream',
536
- Origin: DEFAULT_ORIGIN
537
- },
538
- duplex: 'half',
539
- // Note: custom agents/proxy require undici Agent; omitted here.
540
- signal: timeoutMs ? AbortSignal.timeout(timeoutMs) : undefined
541
- });
542
- let parsed = undefined;
543
- try {
544
- parsed = await response.json();
545
- }
546
- catch {
547
- parsed = undefined;
548
- }
549
- result = parsed;
550
- if (result?.url || result?.directPath) {
654
+ result = await uploadMedia({
655
+ url,
656
+ filePath,
657
+ headers,
658
+ timeoutMs,
659
+ agent: fetchAgent
660
+ }, logger);
661
+ if (result?.url || result?.direct_path) {
551
662
  urls = {
552
663
  mediaUrl: result.url,
553
664
  directPath: result.direct_path,
@@ -579,11 +690,11 @@ const getMediaRetryKey = (mediaKey) => {
579
690
  /**
580
691
  * Generate a binary node that will request the phone to re-upload the media & return the newly uploaded URL
581
692
  */
582
- export const encryptMediaRetryRequest = async (key, mediaKey, meId) => {
693
+ export const encryptMediaRetryRequest = (key, mediaKey, meId) => {
583
694
  const recp = { stanzaId: key.id };
584
695
  const recpBuffer = proto.ServerErrorReceipt.encode(recp).finish();
585
696
  const iv = Crypto.randomBytes(12);
586
- const retryKey = await getMediaRetryKey(mediaKey);
697
+ const retryKey = getMediaRetryKey(mediaKey);
587
698
  const ciphertext = aesEncryptGCM(recpBuffer, retryKey, iv, Buffer.from(key.id));
588
699
  const req = {
589
700
  tag: 'receipt',
@@ -648,8 +759,8 @@ export const decodeMediaRetryNode = (node) => {
648
759
  }
649
760
  return event;
650
761
  };
651
- export const decryptMediaRetryData = async ({ ciphertext, iv }, mediaKey, msgId) => {
652
- const retryKey = await getMediaRetryKey(mediaKey);
762
+ export const decryptMediaRetryData = ({ ciphertext, iv }, mediaKey, msgId) => {
763
+ const retryKey = getMediaRetryKey(mediaKey);
653
764
  const plaintext = aesDecryptGCM(ciphertext, retryKey, iv, Buffer.from(msgId));
654
765
  return proto.MediaRetryNotification.decode(plaintext);
655
766
  };
@@ -9,6 +9,7 @@ import { isJidGroup, isJidNewsletter, isJidStatusBroadcast, jidNormalizedUser }
9
9
  import { sha256 } from './crypto.js';
10
10
  import { generateMessageIDV2, getKeyAuthor, unixTimestampSeconds } from './generics.js';
11
11
  import { downloadContentFromMessage, encryptedStream, generateThumbnail, getAudioDuration, getAudioWaveform, getRawMediaUploadData } from './messages-media.js';
12
+ import { shouldIncludeReportingToken } from './reporting-utils.js';
12
13
  const MIMETYPE_MAP = {
13
14
  image: 'image/jpeg',
14
15
  video: 'video/mp4',
@@ -259,10 +260,20 @@ export const generateForwardMessageContent = (message, forceForward) => {
259
260
  }
260
261
  return content;
261
262
  };
263
+ export const hasNonNullishProperty = (message, key) => {
264
+ return (typeof message === 'object' &&
265
+ message !== null &&
266
+ key in message &&
267
+ message[key] !== null &&
268
+ message[key] !== undefined);
269
+ };
270
+ function hasOptionalProperty(obj, key) {
271
+ return typeof obj === 'object' && obj !== null && key in obj && obj[key] !== null;
272
+ }
262
273
  export const generateWAMessageContent = async (message, options) => {
263
274
  var _a, _b;
264
275
  let m = {};
265
- if ('text' in message) {
276
+ if (hasNonNullishProperty(message, 'text')) {
266
277
  const extContent = { text: message.text };
267
278
  let urlInfo = message.linkPreview;
268
279
  if (typeof urlInfo === 'undefined') {
@@ -293,7 +304,7 @@ export const generateWAMessageContent = async (message, options) => {
293
304
  }
294
305
  m.extendedTextMessage = extContent;
295
306
  }
296
- else if ('contacts' in message) {
307
+ else if (hasNonNullishProperty(message, 'contacts')) {
297
308
  const contactLen = message.contacts.contacts.length;
298
309
  if (!contactLen) {
299
310
  throw new Boom('require atleast 1 contact', { statusCode: 400 });
@@ -305,25 +316,25 @@ export const generateWAMessageContent = async (message, options) => {
305
316
  m.contactsArrayMessage = WAProto.Message.ContactsArrayMessage.create(message.contacts);
306
317
  }
307
318
  }
308
- else if ('location' in message) {
319
+ else if (hasNonNullishProperty(message, 'location')) {
309
320
  m.locationMessage = WAProto.Message.LocationMessage.create(message.location);
310
321
  }
311
- else if ('react' in message) {
322
+ else if (hasNonNullishProperty(message, 'react')) {
312
323
  if (!message.react.senderTimestampMs) {
313
324
  message.react.senderTimestampMs = Date.now();
314
325
  }
315
326
  m.reactionMessage = WAProto.Message.ReactionMessage.create(message.react);
316
327
  }
317
- else if ('delete' in message) {
328
+ else if (hasNonNullishProperty(message, 'delete')) {
318
329
  m.protocolMessage = {
319
330
  key: message.delete,
320
331
  type: WAProto.Message.ProtocolMessage.Type.REVOKE
321
332
  };
322
333
  }
323
- else if ('forward' in message) {
334
+ else if (hasNonNullishProperty(message, 'forward')) {
324
335
  m = generateForwardMessageContent(message.forward, message.force);
325
336
  }
326
- else if ('disappearingMessagesInChat' in message) {
337
+ else if (hasNonNullishProperty(message, 'disappearingMessagesInChat')) {
327
338
  const exp = typeof message.disappearingMessagesInChat === 'boolean'
328
339
  ? message.disappearingMessagesInChat
329
340
  ? WA_DEFAULT_EPHEMERAL
@@ -331,7 +342,7 @@ export const generateWAMessageContent = async (message, options) => {
331
342
  : message.disappearingMessagesInChat;
332
343
  m = prepareDisappearingMessageSettingContent(exp);
333
344
  }
334
- else if ('groupInvite' in message) {
345
+ else if (hasNonNullishProperty(message, 'groupInvite')) {
335
346
  m.groupInviteMessage = {};
336
347
  m.groupInviteMessage.inviteCode = message.groupInvite.inviteCode;
337
348
  m.groupInviteMessage.inviteExpiration = message.groupInvite.inviteExpiration;
@@ -351,7 +362,7 @@ export const generateWAMessageContent = async (message, options) => {
351
362
  }
352
363
  }
353
364
  }
354
- else if ('pin' in message) {
365
+ else if (hasNonNullishProperty(message, 'pin')) {
355
366
  m.pinInChatMessage = {};
356
367
  m.messageContextInfo = {};
357
368
  m.pinInChatMessage.key = message.pin;
@@ -359,7 +370,7 @@ export const generateWAMessageContent = async (message, options) => {
359
370
  m.pinInChatMessage.senderTimestampMs = Date.now();
360
371
  m.messageContextInfo.messageAddOnDurationInSecs = message.type === 1 ? message.time || 86400 : 0;
361
372
  }
362
- else if ('buttonReply' in message) {
373
+ else if (hasNonNullishProperty(message, 'buttonReply')) {
363
374
  switch (message.type) {
364
375
  case 'template':
365
376
  m.templateButtonReplyMessage = {
@@ -377,11 +388,11 @@ export const generateWAMessageContent = async (message, options) => {
377
388
  break;
378
389
  }
379
390
  }
380
- else if ('ptv' in message && message.ptv) {
391
+ else if (hasOptionalProperty(message, 'ptv') && message.ptv) {
381
392
  const { videoMessage } = await prepareWAMessageMedia({ video: message.video }, options);
382
393
  m.ptvMessage = videoMessage;
383
394
  }
384
- else if ('product' in message) {
395
+ else if (hasNonNullishProperty(message, 'product')) {
385
396
  const { imageMessage } = await prepareWAMessageMedia({ image: message.product.productImage }, options);
386
397
  m.productMessage = WAProto.Message.ProductMessage.create({
387
398
  ...message,
@@ -391,10 +402,10 @@ export const generateWAMessageContent = async (message, options) => {
391
402
  }
392
403
  });
393
404
  }
394
- else if ('listReply' in message) {
405
+ else if (hasNonNullishProperty(message, 'listReply')) {
395
406
  m.listResponseMessage = { ...message.listReply };
396
407
  }
397
- else if ('event' in message) {
408
+ else if (hasNonNullishProperty(message, 'event')) {
398
409
  m.eventMessage = {};
399
410
  const startTime = Math.floor(message.event.startDate.getTime() / 1000);
400
411
  if (message.event.call && options.getCallLink) {
@@ -414,7 +425,7 @@ export const generateWAMessageContent = async (message, options) => {
414
425
  m.eventMessage.isScheduleCall = message.event.isScheduleCall ?? false;
415
426
  m.eventMessage.location = message.event.location;
416
427
  }
417
- else if ('poll' in message) {
428
+ else if (hasNonNullishProperty(message, 'poll')) {
418
429
  (_a = message.poll).selectableCount || (_a.selectableCount = 0);
419
430
  (_b = message.poll).toAnnouncementGroup || (_b.toAnnouncementGroup = false);
420
431
  if (!Array.isArray(message.poll.values)) {
@@ -449,15 +460,15 @@ export const generateWAMessageContent = async (message, options) => {
449
460
  }
450
461
  }
451
462
  }
452
- else if ('sharePhoneNumber' in message) {
463
+ else if (hasNonNullishProperty(message, 'sharePhoneNumber')) {
453
464
  m.protocolMessage = {
454
465
  type: proto.Message.ProtocolMessage.Type.SHARE_PHONE_NUMBER
455
466
  };
456
467
  }
457
- else if ('requestPhoneNumber' in message) {
468
+ else if (hasNonNullishProperty(message, 'requestPhoneNumber')) {
458
469
  m.requestPhoneNumberMessage = {};
459
470
  }
460
- else if ('limitSharing' in message) {
471
+ else if (hasNonNullishProperty(message, 'limitSharing')) {
461
472
  m.protocolMessage = {
462
473
  type: proto.Message.ProtocolMessage.Type.LIMIT_SHARING,
463
474
  limitSharing: {
@@ -471,10 +482,10 @@ export const generateWAMessageContent = async (message, options) => {
471
482
  else {
472
483
  m = await prepareWAMessageMedia(message, options);
473
484
  }
474
- if ('viewOnce' in message && !!message.viewOnce) {
485
+ if (hasOptionalProperty(message, 'viewOnce') && !!message.viewOnce) {
475
486
  m = { viewOnceMessage: { message: m } };
476
487
  }
477
- if ('mentions' in message && message.mentions?.length) {
488
+ if (hasOptionalProperty(message, 'mentions') && message.mentions?.length) {
478
489
  const messageType = Object.keys(m)[0];
479
490
  const key = m[messageType];
480
491
  if ('contextInfo' in key && !!key.contextInfo) {
@@ -486,7 +497,7 @@ export const generateWAMessageContent = async (message, options) => {
486
497
  };
487
498
  }
488
499
  }
489
- if ('edit' in message) {
500
+ if (hasOptionalProperty(message, 'edit')) {
490
501
  m = {
491
502
  protocolMessage: {
492
503
  key: message.edit,
@@ -496,7 +507,7 @@ export const generateWAMessageContent = async (message, options) => {
496
507
  }
497
508
  };
498
509
  }
499
- if ('contextInfo' in message && !!message.contextInfo) {
510
+ if (hasOptionalProperty(message, 'contextInfo') && !!message.contextInfo) {
500
511
  const messageType = Object.keys(m)[0];
501
512
  const key = m[messageType];
502
513
  if ('contextInfo' in key && !!key.contextInfo) {
@@ -506,6 +517,12 @@ export const generateWAMessageContent = async (message, options) => {
506
517
  key.contextInfo = message.contextInfo;
507
518
  }
508
519
  }
520
+ if (shouldIncludeReportingToken(m)) {
521
+ m.messageContextInfo = m.messageContextInfo || {};
522
+ if (!m.messageContextInfo.messageSecret) {
523
+ m.messageContextInfo.messageSecret = randomBytes(32);
524
+ }
525
+ }
509
526
  return WAProto.Message.create(m);
510
527
  };
511
528
  export const generateWAMessageFromContent = (jid, message, options) => {
@@ -614,7 +631,10 @@ export const normalizeMessageContent = (content) => {
614
631
  message?.documentWithCaptionMessage ||
615
632
  message?.viewOnceMessageV2 ||
616
633
  message?.viewOnceMessageV2Extension ||
617
- message?.editedMessage);
634
+ message?.editedMessage ||
635
+ message?.associatedChildMessage ||
636
+ message?.groupStatusMessage ||
637
+ message?.groupStatusMessageV2);
618
638
  }
619
639
  };
620
640
  /**