@nexustechpro/baileys 2.1.0 → 2.1.3

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.
@@ -2,16 +2,21 @@
2
2
  // Single source of truth for blob key names — change here, changes everywhere
3
3
  export const SESSION_INDEX_KEY = 'index'
4
4
  export const DEVICE_LIST_INDEX_KEY = 'index'
5
+ export const TC_TOKEN_INDEX_KEY = 'index'
5
6
 
6
7
  // Migrates old _index blob to new index key in-place, returns the batch data
7
8
  export const migrateIndexKey = async (keys, type) => {
8
- const oldKey = '_index'
9
+ const oldKeys = ['_index', '__index']
9
10
  const newKey = 'index'
10
- const oldData = await keys.get(type, [oldKey])
11
- if (oldData?.[oldKey] !== null && oldData?.[oldKey] !== undefined && typeof oldData[oldKey] === 'object') { // only migrate if old blob actually exists and has data
12
- await keys.set({ [type]: { [newKey]: oldData[oldKey], [oldKey]: null } })
13
- return oldData[oldKey]
11
+
12
+ for (const oldKey of oldKeys) {
13
+ const oldData = await keys.get(type, [oldKey])
14
+ if (oldData?.[oldKey] !== null && oldData?.[oldKey] !== undefined && typeof oldData[oldKey] === 'object') {
15
+ await keys.set({ [type]: { [newKey]: oldData[oldKey], [oldKey]: null } })
16
+ return oldData[oldKey]
17
+ }
14
18
  }
19
+
15
20
  const newData = await keys.get(type, [newKey])
16
21
  return newData?.[newKey] || {}
17
22
  }
@@ -1,48 +1,7 @@
1
- import { hkdf } from './crypto.js';
1
+ import { LTHashAntiTampering } from 'whatsapp-rust-bridge';
2
2
  /**
3
3
  * LT Hash is a summation based hash algorithm that maintains the integrity of a piece of data
4
4
  * over a series of mutations. You can add/remove mutations and it'll return a hash equal to
5
5
  * if the same series of mutations was made sequentially.
6
6
  */
7
- const o = 128;
8
- class LTHash {
9
- constructor(e) {
10
- this.salt = e;
11
- }
12
- async add(e, t) {
13
- for (const item of t) {
14
- e = await this._addSingle(e, item);
15
- }
16
- return e;
17
- }
18
- async subtract(e, t) {
19
- for (const item of t) {
20
- e = await this._subtractSingle(e, item);
21
- }
22
- return e;
23
- }
24
- async subtractThenAdd(e, addList, subtractList) {
25
- const subtracted = await this.subtract(e, subtractList);
26
- return this.add(subtracted, addList);
27
- }
28
- async _addSingle(e, t) {
29
- const derived = new Uint8Array(hkdf(Buffer.from(t), o, { info: this.salt })).buffer;
30
- return this.performPointwiseWithOverflow(e, derived, (a, b) => a + b);
31
- }
32
- async _subtractSingle(e, t) {
33
- const derived = new Uint8Array(hkdf(Buffer.from(t), o, { info: this.salt })).buffer;
34
- return this.performPointwiseWithOverflow(e, derived, (a, b) => a - b);
35
- }
36
- performPointwiseWithOverflow(e, t, op) {
37
- const n = new DataView(e);
38
- const i = new DataView(t);
39
- const out = new ArrayBuffer(n.byteLength);
40
- const s = new DataView(out);
41
- for (let offset = 0; offset < n.byteLength; offset += 2) {
42
- s.setUint16(offset, op(n.getUint16(offset, true), i.getUint16(offset, true)), true);
43
- }
44
- return out;
45
- }
46
- }
47
- export const LT_HASH_ANTI_TAMPERING = new LTHash('WhatsApp Patch Integrity');
48
- //# sourceMappingURL=lt-hash.js.map
7
+ export const LT_HASH_ANTI_TAMPERING = new LTHashAntiTampering();
@@ -334,7 +334,10 @@ export const generateWAMessageContent = async (message, options = {}) => {
334
334
  } else if ('groupInvite' in message) {
335
335
  m = await handleGroupInvite(message, options)
336
336
  } else if ('stickerPack' in message) {
337
- return WAProto.Message.create({ stickerPackMessage: (await prepareStickerPackMessage(message.stickerPack, options)).stickerPackMessage })
337
+ const result = await prepareStickerPackMessage(message.stickerPack, options)
338
+ return result.isBatched
339
+ ? { stickerPackMessage: result.stickerPackMessage, isBatched: true, batchCount: result.batchCount }
340
+ : WAProto.Message.create({ stickerPackMessage: result.stickerPackMessage })
338
341
  } else if ('pin' in message) {
339
342
  const messageKey = typeof message.pin === 'boolean'
340
343
  ? (options.quoted?.key || (() => { throw new Boom('No quoted message key found for pin operation') })())
@@ -460,81 +463,99 @@ export const generateWAMessageContent = async (message, options = {}) => {
460
463
  // ─── STICKER PACK ─────────────────────────────────────────────────────────────
461
464
  export const prepareStickerPackMessage = async (stickerPack, options) => {
462
465
  const { stickers, cover, name, publisher, packId, description } = stickerPack
463
- if (!stickers?.length) throw new Boom('Sticker pack requires at least one sticker', { statusCode: 400 })
464
- if (stickers.length > 120) throw new Boom('Sticker pack exceeds maximum of 120 stickers', { statusCode: 400 })
466
+ const MAX_STICKER_SIZE = 1024 * 1024
467
+ const MAX_STICKERS_PER_PACK = 60
465
468
 
469
+ if (!stickers?.length) throw new Boom('Sticker pack must contain at least one sticker', { statusCode: 400 })
470
+
471
+ const stickerPackIdValue = packId || generateMessageIDV2()
466
472
  const lib = await getImageProcessingLibrary()
467
- const packId_ = packId || generateMessageIDV2()
473
+ const validStickers = []
474
+ const skippedStickers = []
468
475
 
469
476
  const isWebPBuffer = (buf) => buf.length >= 12 && buf[0] === 0x52 && buf[1] === 0x49 && buf[2] === 0x46 && buf[3] === 0x46 && buf[8] === 0x57 && buf[9] === 0x45 && buf[10] === 0x42 && buf[11] === 0x50
470
- const isAnimatedWebP = (buf) => {
471
- if (!isWebPBuffer(buf)) return false
472
- let offset = 12
473
- while (offset < buf.length - 8) {
474
- const fourCC = buf.toString('ascii', offset, offset + 4)
475
- const chunkSize = buf.readUInt32LE(offset + 4)
476
- if (fourCC === 'VP8X' && offset + 8 < buf.length && (buf[offset + 8] & 0x02)) return true
477
- if (fourCC === 'ANIM' || fourCC === 'ANMF') return true
478
- offset += 8 + chunkSize + (chunkSize % 2)
479
- }
480
- return false
481
- }
482
- const toWebp = async (buffer) => {
483
- if ('sharp' in lib && lib.sharp) return await lib.sharp.default(buffer).resize(512, 512, { fit: 'inside', withoutEnlargement: true }).webp({ quality: 75, effort: 6 }).toBuffer()
484
- if ('jimp' in lib && lib.jimp) return await lib.jimp.Jimp.read(buffer).then(img => img.getBuffer('image/webp'))
485
- throw new Boom('No image processing library available', { statusCode: 500 })
486
- }
487
477
 
488
- const validStickers = []
489
- await Promise.all(stickers.map(async (s) => {
478
+ const stickerPromises = stickers.map(async (s, i) => {
490
479
  try {
491
480
  const { stream } = await getStream(s.data || s.sticker)
492
481
  const buffer = await toBuffer(stream)
493
- if (!buffer?.length) return
494
- const animated = isAnimatedWebP(buffer)
495
- let webpBuffer = isWebPBuffer(buffer) ? buffer : await toWebp(buffer)
496
- if (webpBuffer.length > 1024 * 1024) {
497
- if ('sharp' in lib && lib.sharp) webpBuffer = await lib.sharp.default(webpBuffer).webp({ quality: 50 }).toBuffer()
498
- if (webpBuffer.length > 1024 * 1024) return
482
+ if (!buffer?.length) { skippedStickers.push({ index: i, reason: 'Empty buffer' }); return null }
483
+
484
+ const isWebP = isWebPBuffer(buffer)
485
+ let webpBuffer
486
+ try {
487
+ if (isWebP) webpBuffer = buffer
488
+ else if ('sharp' in lib && lib.sharp) webpBuffer = await lib.sharp.default(buffer).webp().toBuffer()
489
+ else if ('jimp' in lib && lib.jimp) webpBuffer = await lib.jimp.Jimp.read(buffer).then(img => img.getBuffer('image/webp'))
490
+ else { skippedStickers.push({ index: i, reason: 'No image processing library' }); return null }
491
+ } catch (convertError) {
492
+ if (!isWebP) { skippedStickers.push({ index: i, reason: `Conversion failed: ${convertError.message}` }); return null }
493
+ throw convertError
494
+ }
495
+
496
+ if (webpBuffer.length > MAX_STICKER_SIZE) {
497
+ options.logger?.warn?.(`Sticker at index ${i} exceeds 1MB, compressing...`)
498
+ try {
499
+ if ('sharp' in lib && lib.sharp) webpBuffer = await lib.sharp.default(buffer).webp({ quality: 70 }).toBuffer()
500
+ if (webpBuffer.length > MAX_STICKER_SIZE && 'sharp' in lib && lib.sharp) webpBuffer = await lib.sharp.default(buffer).webp({ quality: 50 }).toBuffer()
501
+ if (webpBuffer.length > MAX_STICKER_SIZE) { skippedStickers.push({ index: i, reason: 'Still exceeds 1MB after compression' }); return null }
502
+ options.logger?.info?.(`Sticker ${i} compressed successfully`)
503
+ } catch {
504
+ skippedStickers.push({ index: i, reason: 'Compression failed' })
505
+ return null
506
+ }
499
507
  }
508
+
500
509
  const hash = sha256(webpBuffer).toString('base64').replace(/\//g, '-').replace(/=/g, '')
501
- validStickers.push({ fileName: `${hash}.webp`, buffer: webpBuffer, mimetype: 'image/webp', isAnimated: s.isAnimated ?? animated, isLottie: s.isLottie || false, emojis: s.emojis || [], accessibilityLabel: s.accessibilityLabel || '' })
502
- } catch (e) { options.logger?.warn({ err: e }, 'failed processing sticker') }
503
- }))
510
+ return { fileName: `${hash}.webp`, webpBuffer, mimetype: 'image/webp', isAnimated: s.isAnimated || false, isLottie: s.isLottie || false, emojis: s.emojis || [], accessibilityLabel: s.accessibilityLabel || '' }
511
+ } catch (e) {
512
+ options.logger?.warn({ err: e }, 'failed processing sticker')
513
+ skippedStickers.push({ index: i, reason: e.message })
514
+ return null
515
+ }
516
+ })
504
517
 
518
+ validStickers.push(...(await Promise.all(stickerPromises)).filter(Boolean))
505
519
  if (!validStickers.length) throw new Boom('No valid stickers could be processed', { statusCode: 400 })
506
520
 
507
521
  const { stream: covStream } = await getStream(cover)
508
- const coverBuffer = await toWebp(await toBuffer(covStream))
509
-
510
- const processBatch = async (batch, batchIdx) => {
522
+ const coverBuffer = await toBuffer(covStream)
523
+ let coverWebpBuffer
524
+ if (isWebPBuffer(coverBuffer)) coverWebpBuffer = coverBuffer
525
+ else if ('sharp' in lib && lib.sharp) coverWebpBuffer = await lib.sharp.default(coverBuffer).webp().toBuffer()
526
+ else if ('jimp' in lib && lib.jimp) coverWebpBuffer = await lib.jimp.Jimp.read(coverBuffer).then(img => img.getBuffer('image/webp'))
527
+ else throw new Boom('No image processing library available for cover', { statusCode: 500 })
528
+
529
+ const processBatch = async (batch, batchIdx, totalBatches) => {
511
530
  const batchData = {}
512
- batch.forEach(s => { batchData[s.fileName] = [new Uint8Array(s.buffer), { level: 6 }] })
513
- const trayFile = `${packId_}_${batchIdx}.webp`
514
- batchData[trayFile] = [new Uint8Array(coverBuffer), { level: 6 }]
515
- const zipBuf = await new Promise((resolve, reject) => zip(batchData, { level: 6, memLevel: 9 }, (err, data) => err ? reject(err) : resolve(Buffer.from(data))))
516
- if (zipBuf.length > 10 * 1024 * 1024) throw new Boom(`Sticker pack batch ${batchIdx} too large`, { statusCode: 400 })
531
+ batch.forEach(s => { batchData[s.fileName] = [new Uint8Array(s.webpBuffer), { level: 0 }] })
532
+ const trayFile = totalBatches > 1 ? `${stickerPackIdValue}_batch${batchIdx}.webp` : `${stickerPackIdValue}.webp`
533
+ batchData[trayFile] = [new Uint8Array(coverWebpBuffer), { level: 0 }]
534
+
535
+ const zipBuf = await new Promise((resolve, reject) => zip(batchData, (err, data) => err ? reject(err) : resolve(Buffer.from(data))))
536
+
517
537
  const upload = await encryptedStream(zipBuf, 'sticker-pack', { logger: options.logger, opts: options.options })
518
- const uploadRes = await options.upload(upload.encFilePath || upload.encBuffer, { fileEncSha256B64: upload.fileEncSha256.toString('base64'), mediaType: 'sticker-pack', timeoutMs: options.mediaUploadTimeoutMs || 300000 })
519
- await upload.cleanup?.()
538
+ const uploadRes = await options.upload(upload.encFilePath || upload.encBuffer, { fileEncSha256B64: upload.fileEncSha256.toString('base64'), mediaType: 'sticker-pack', timeoutMs: options.mediaUploadTimeoutMs })
539
+ if (upload.encFilePath) await fs.unlink(upload.encFilePath).catch(() => { })
520
540
 
521
541
  let thumbRes = null
522
542
  try {
523
543
  let thumbBuf
524
- if ('sharp' in lib && lib.sharp) thumbBuf = await lib.sharp.default(coverBuffer).resize(252, 252, { fit: 'cover' }).jpeg({ quality: 80 }).toBuffer()
544
+ if ('sharp' in lib && lib.sharp) thumbBuf = await lib.sharp.default(coverBuffer).resize(252, 252).jpeg().toBuffer()
525
545
  else if ('jimp' in lib && lib.jimp) thumbBuf = await lib.jimp.Jimp.read(coverBuffer).then(img => img.resize({ w: 252, h: 252 }).getBuffer('image/jpeg'))
526
546
  if (thumbBuf?.length) {
527
547
  const thumbUpload = await encryptedStream(thumbBuf, 'thumbnail-sticker-pack', { logger: options.logger, opts: options.options, mediaKey: upload.mediaKey })
528
- thumbRes = await options.upload(thumbUpload.encFilePath || thumbUpload.encBuffer, { fileEncSha256B64: thumbUpload.fileEncSha256.toString('base64'), mediaType: 'thumbnail-sticker-pack', timeoutMs: options.mediaUploadTimeoutMs || 60000 })
529
- await thumbUpload.cleanup?.()
548
+ thumbRes = await options.upload(thumbUpload.encFilePath || thumbUpload.encBuffer, { fileEncSha256B64: thumbUpload.fileEncSha256.toString('base64'), mediaType: 'thumbnail-sticker-pack', timeoutMs: options.mediaUploadTimeoutMs })
549
+ if (thumbUpload.encFilePath) await fs.unlink(thumbUpload.encFilePath).catch(() => { })
530
550
  thumbRes._buf = thumbBuf; thumbRes._enc = thumbUpload
531
551
  }
532
552
  } catch (e) { options.logger?.warn({ err: e }, 'failed generating sticker pack thumbnail') }
533
553
 
534
554
  return {
535
- name: batchIdx > 0 ? `${name} (${batchIdx + 1})` : name, publisher, packDescription: description,
536
- stickerPackId: batchIdx > 0 ? `${packId_}_${batchIdx}` : packId_,
537
- stickerPackOrigin: proto.Message.StickerPackMessage.StickerPackOrigin.THIRD_PARTY,
555
+ name: totalBatches > 1 ? `${name} (${batchIdx + 1}/${totalBatches})` : name,
556
+ publisher, packDescription: description,
557
+ stickerPackId: totalBatches > 1 ? `${stickerPackIdValue}_${batchIdx}` : stickerPackIdValue,
558
+ stickerPackOrigin: proto.Message.StickerPackMessage.StickerPackOrigin.USER_CREATED,
538
559
  stickerPackSize: zipBuf.length,
539
560
  stickers: batch.map(s => ({ fileName: s.fileName, mimetype: s.mimetype, isAnimated: s.isAnimated, isLottie: s.isLottie, emojis: s.emojis, accessibilityLabel: s.accessibilityLabel })),
540
561
  fileSha256: upload.fileSha256, fileEncSha256: upload.fileEncSha256, mediaKey: upload.mediaKey,
@@ -543,13 +564,14 @@ export const prepareStickerPackMessage = async (stickerPack, options) => {
543
564
  }
544
565
  }
545
566
 
546
- if (validStickers.length > 60) {
567
+ if (validStickers.length > MAX_STICKERS_PER_PACK) {
547
568
  const batches = []
548
- for (let i = 0; i < validStickers.length; i += 60) batches.push(validStickers.slice(i, i + 60))
549
- const results = await Promise.all(batches.map((b, i) => processBatch(b, i)))
569
+ for (let i = 0; i < validStickers.length; i += MAX_STICKERS_PER_PACK) batches.push(validStickers.slice(i, i + MAX_STICKERS_PER_PACK))
570
+ const results = []
571
+ for (let i = 0; i < batches.length; i++) results.push(await processBatch(batches[i], i, batches.length))
550
572
  return { stickerPackMessage: results, isBatched: true, batchCount: batches.length }
551
573
  }
552
- return { stickerPackMessage: await processBatch(validStickers, 0), isBatched: false }
574
+ return { stickerPackMessage: await processBatch(validStickers, 0, 1), isBatched: false }
553
575
  }
554
576
 
555
577
  // ─── MESSAGE BUILDERS ─────────────────────────────────────────────────────────
@@ -562,18 +584,21 @@ export const generateWAMessageFromContent = (jid, message, options) => {
562
584
  if (quoted && !isJidNewsletter(jid)) {
563
585
  const participant = quoted.key.fromMe ? userJid : (quoted.participant || quoted.key.participant || quoted.key.remoteJid)
564
586
  const normalizedQuoted = normalizeMessageContent(quoted.message)
565
- const quotedType = getContentType(normalizedQuoted)
566
- const quotedMsg = proto.Message.fromObject({ [quotedType]: normalizedQuoted[quotedType] })
567
- const quotedContent = quotedMsg[quotedType]
568
- if (typeof quotedContent === 'object' && quotedContent && 'contextInfo' in quotedContent) delete quotedContent.contextInfo
569
- const contextInfo = (innerMessage[key]?.contextInfo) || {}
570
- contextInfo.participant = jidNormalizedUser(participant)
571
- contextInfo.stanzaId = quoted.key.id
572
- contextInfo.quotedMessage = quotedMsg
573
- if (jid !== quoted.key.remoteJid) contextInfo.remoteJid = quoted.key.remoteJid
574
- if (innerMessage[key]) innerMessage[key].contextInfo = contextInfo
587
+ if (!normalizedQuoted) {
588
+ // stub message with no content — skip quoted context
589
+ } else {
590
+ const quotedType = getContentType(normalizedQuoted)
591
+ const quotedMsg = proto.Message.fromObject({ [quotedType]: normalizedQuoted[quotedType] })
592
+ const quotedContent = quotedMsg[quotedType]
593
+ if (typeof quotedContent === 'object' && quotedContent && 'contextInfo' in quotedContent) delete quotedContent.contextInfo
594
+ const contextInfo = (innerMessage[key]?.contextInfo) || {}
595
+ contextInfo.participant = jidNormalizedUser(participant)
596
+ contextInfo.stanzaId = quoted.key.id
597
+ contextInfo.quotedMessage = quotedMsg
598
+ if (jid !== quoted.key.remoteJid) contextInfo.remoteJid = quoted.key.remoteJid
599
+ if (innerMessage[key]) innerMessage[key].contextInfo = contextInfo
600
+ }
575
601
  }
576
-
577
602
  if (options?.ephemeralExpiration && key !== 'protocolMessage' && key !== 'ephemeralMessage' && !isJidNewsletter(jid)) {
578
603
  innerMessage[key].contextInfo = { ...(innerMessage[key].contextInfo || {}), expiration: options.ephemeralExpiration || WA_DEFAULT_EPHEMERAL }
579
604
  }
@@ -680,7 +705,7 @@ export const downloadMediaMessage = async (message, type, options, ctx) => {
680
705
 
681
706
  export const assertMediaContent = (content) => {
682
707
  content = extractMessageContent(content)
683
- const mediaContent = content?.documentMessage || content?.imageMessage || content?.videoMessage || content?.audioMessage || content?.stickerMessage
708
+ const mediaContent = content?.documentMessage || content?.imageMessage || content?.videoMessage || content?.audioMessage || content?.stickerMessage || content?.stickerPackMessage
684
709
  if (!mediaContent) throw new Boom('given message is not a media message', { statusCode: 400, data: content })
685
710
  return mediaContent
686
711
  }