@nexustechpro/baileys 2.1.3 → 2.1.6

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.
@@ -36,9 +36,10 @@ import {
36
36
  makeNoiseHandler,
37
37
  promiseTimeout,
38
38
  signedKeyPair,
39
- xmppSignedPreKey
39
+ xmppSignedPreKey,
40
+ getPlatformId,
41
+ isAndroidBrowser
40
42
  } from '../Utils/index.js'
41
- import { getPlatformId, migrateIndexKey } from '../Utils/index.js'
42
43
  import {
43
44
  assertNodeErrorFree,
44
45
  binaryNodeToString,
@@ -117,6 +118,12 @@ export const makeSocket = (config) => {
117
118
  })
118
119
  }
119
120
 
121
+ if (browser[1].toLocaleLowerCase().includes('android')) {
122
+ logger.warn(
123
+ '⚠️ Using the Android browser is experimental and may lead to unexpected behavior. Use at your own risk.'
124
+ )
125
+ }
126
+
120
127
  const ev = makeEventBuffer(logger)
121
128
  const { creds } = authState
122
129
  const keys = addTransactionCapability(authState.keys, logger, transactionOpts)
@@ -629,8 +636,8 @@ export const makeSocket = (config) => {
629
636
  content: [
630
637
  { tag: 'link_code_pairing_wrapped_companion_ephemeral_pub', attrs: {}, content: await generatePairingKey() },
631
638
  { tag: 'companion_server_auth_key_pub', attrs: {}, content: authState.creds.noiseKey.public },
632
- { tag: 'companion_platform_id', attrs: {}, content: getCompanionPlatformId(browser) },
633
- { tag: 'companion_platform_display', attrs: {}, content: `${browser[1]} (${browser[0]})` },
639
+ { tag: 'companion_platform_id', attrs: {}, content: isAndroidBrowser(browser) ? getPlatformId('Chrome') : getCompanionPlatformId(browser) },
640
+ { tag: 'companion_platform_display', attrs: {}, content: isAndroidBrowser(browser) ? 'Chrome (Mac OS)' : `${browser[1]} (${browser[0]})` },
634
641
  { tag: 'link_code_pairing_nonce', attrs: {}, content: '0' }
635
642
  ]
636
643
  }]
@@ -747,7 +754,6 @@ export const makeSocket = (config) => {
747
754
  ev.emit('creds.update', { me: { ...authState.creds.me, lid: node.attrs.lid } })
748
755
  ev.emit('connection.update', { connection: 'open' })
749
756
  void sendUnifiedSession()
750
-
751
757
  if (node.attrs.lid && authState.creds.me?.id) {
752
758
  const myLID = node.attrs.lid
753
759
  process.nextTick(async () => {
@@ -755,23 +761,27 @@ export const makeSocket = (config) => {
755
761
  const myPN = authState.creds.me.id
756
762
  // Store own LID-PN mapping
757
763
  await signalRepository.lidMapping.storeLIDPNMappings([{ lid: myLID, pn: myPN }])
758
- // Build device-list using index-based batching to avoid unbounded key growth
764
+ // Write own device entry one key per user, no index blob
759
765
  const { user, device } = jidDecode(myPN)
760
- const currentBatch = await migrateIndexKey(authState.keys, 'device-list')
761
- currentBatch[user] = [device?.toString() || '0']
762
- const deviceKeys = Object.keys(currentBatch)
763
- if (deviceKeys.length > BATCH_SIZE) {
764
- deviceKeys.sort()
765
- deviceKeys.slice(0, deviceKeys.length - BATCH_SIZE).forEach((k) => delete currentBatch[k])
766
- }
767
- await authState.keys.set({ 'device-list': { index: currentBatch } })
766
+ await authState.keys.set({ 'device-list': { [user]: [device?.toString() || '0'] } })
768
767
  // Migrate own session from PN → LID
769
768
  await signalRepository.migrateSession(myPN, myLID)
770
769
  logger.info({ myPN, myLID }, 'Own LID session created successfully')
771
- // Batch-migrate any remaining PN sessions to LID
772
- if (signalRepository.migrateAllPNSessionsToLID) {
770
+
771
+ // Migrate any other known PN sessions to LID (replaces removed migrateAllPNSessionsToLID)
772
+ if (signalRepository.lidMapping.getLIDPNMappings) {
773
773
  try {
774
- const migrated = await signalRepository.migrateAllPNSessionsToLID()
774
+ const allMappings = await signalRepository.lidMapping.getLIDPNMappings()
775
+ const others = (allMappings || []).filter(m => m.pn !== myPN && m.lid && m.pn)
776
+ let migrated = 0
777
+ for (const { pn, lid } of others) {
778
+ try {
779
+ await signalRepository.migrateSession(pn, lid)
780
+ migrated++
781
+ } catch (sessErr) {
782
+ logger.warn({ error: sessErr, pn, lid }, 'Failed to migrate individual PN session to LID')
783
+ }
784
+ }
775
785
  if (migrated > 0) logger.info({ migrated }, 'Batch-migrated PN sessions to LID on connect')
776
786
  } catch (migErr) {
777
787
  logger.warn({ error: migErr }, 'Failed to batch-migrate PN sessions to LID')
@@ -783,7 +793,6 @@ export const makeSocket = (config) => {
783
793
  })
784
794
  }
785
795
  })
786
-
787
796
  // ─── Stream / connection error handlers ─────────────────────────────────────
788
797
 
789
798
  ws.on('CB:stream:error', (node) => {
@@ -18,11 +18,20 @@ export const Browsers = {
18
18
  macOS: browser => ['Mac OS', browser, '14.4.1'],
19
19
  baileys: browser => ['Baileys', browser, '6.5.0'],
20
20
  windows: browser => ['Windows', browser, '10.0.22631'],
21
+ android: browser => [browser, 'Android', ''],
21
22
  /** The appropriate browser based on your OS & release */
22
23
  appropriate: browser => [PLATFORM_MAP[platform()] || 'Ubuntu', browser, release()]
23
24
  };
24
25
  export const getPlatformId = (browser) => {
25
- const platformType = proto.DeviceProps.PlatformType[browser.toUpperCase()];
26
- return platformType ? platformType.toString() : '1'; //chrome
27
- };
26
+ const upper = browser.toUpperCase()
27
+ if (upper === 'ANDROID') {
28
+ return proto.DeviceProps.PlatformType['ANDROID_PHONE'].toString()
29
+ }
30
+ const platformType = proto.DeviceProps.PlatformType[upper]
31
+ return platformType ? platformType.toString() : '1' // chrome
32
+ }
33
+
34
+ export const isAndroidBrowser = (browser) => {
35
+ return browser[1]?.toUpperCase() === 'ANDROID'
36
+ }
28
37
  //# sourceMappingURL=browser-utils.js.map
@@ -1,22 +1,3 @@
1
1
  // ─── Key Store Helpers ─────────────────────────────────────────────────────────
2
2
  // Single source of truth for blob key names — change here, changes everywhere
3
- export const SESSION_INDEX_KEY = 'index'
4
- export const DEVICE_LIST_INDEX_KEY = 'index'
5
- export const TC_TOKEN_INDEX_KEY = 'index'
6
-
7
- // Migrates old _index blob to new index key in-place, returns the batch data
8
- export const migrateIndexKey = async (keys, type) => {
9
- const oldKeys = ['_index', '__index']
10
- const newKey = 'index'
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
- }
18
- }
19
-
20
- const newData = await keys.get(type, [newKey])
21
- return newData?.[newKey] || {}
22
- }
3
+ export const TC_TOKEN_INDEX_KEY = '__index'
@@ -235,35 +235,40 @@ export async function getAudioWaveform(buffer, logger) {
235
235
 
236
236
  // ─── FFMPEG CONVERTERS ────────────────────────────────────────────────────────
237
237
  const convertToOpusBuffer = async (buffer, logger) => {
238
+ if (buffer[0] === 0x4F && buffer[1] === 0x67 && buffer[2] === 0x67 && buffer[3] === 0x53) return buffer
238
239
  const ffmpegPath = await getFfmpegPath()
239
240
  const inputPath = join(tmpdir(), 'opus-in-' + generateMessageIDV2())
241
+ const outputPath = join(tmpdir(), 'opus-out-' + generateMessageIDV2() + '.ogg')
240
242
  await fs.writeFile(inputPath, buffer)
241
243
  try {
242
- return await new Promise((resolve, reject) => {
243
- const ff = spawn(ffmpegPath, ['-y', '-i', inputPath, '-c:a', 'libopus', '-b:a', '64k', '-vbr', 'on', '-compression_level', '10', '-frame_duration', '20', '-application', 'voip', '-f', 'ogg', 'pipe:1'], { stdio: ['ignore', 'pipe', 'pipe'] })
244
- const chunks = []
245
- ff.stdout.on('data', chunk => chunks.push(chunk))
246
- ff.stderr.on('data', () => { })
247
- ff.on('close', code => code === 0 ? resolve(Buffer.concat(chunks)) : reject(new Error(`FFmpeg Opus exited with code ${code}`)))
244
+ await new Promise((resolve, reject) => {
245
+ const ff = spawn(ffmpegPath, ['-y', '-i', inputPath, '-c:a', 'libopus', '-b:a', '128k', '-ar', '48000', '-ac', '1', '-vbr', 'on', '-compression_level', '10', '-frame_duration', '20', '-application', 'audio', '-map_metadata', '-1', outputPath], { stdio: ['ignore', 'ignore', 'pipe'] })
246
+ ff.on('close', code => code === 0 ? resolve() : reject(new Error(`FFmpeg Opus exited with code ${code}`)))
248
247
  ff.on('error', reject)
249
248
  })
249
+ return await fs.readFile(outputPath)
250
250
  } finally {
251
251
  try { await fs.unlink(inputPath) } catch { }
252
+ try { await fs.unlink(outputPath) } catch { }
252
253
  }
253
254
  }
254
255
 
255
256
  const convertToMp4Buffer = async (buffer, logger) => {
256
257
  const ffmpegPath = await getFfmpegPath()
257
- return new Promise((resolve, reject) => {
258
- const ff = spawn(ffmpegPath, ['-i', 'pipe:0', '-c:v', 'libx264', '-preset', 'veryfast', '-crf', '23', '-c:a', 'aac', '-b:a', '128k', '-movflags', 'faststart', '-f', 'mp4', 'pipe:1'], { stdio: ['pipe', 'pipe', 'pipe'] })
259
- const chunks = []
260
- ff.stdin.write(buffer)
261
- ff.stdin.end()
262
- ff.stdout.on('data', chunk => chunks.push(chunk))
263
- ff.stderr.on('data', () => { })
264
- ff.on('close', code => code === 0 ? resolve(Buffer.concat(chunks)) : reject(new Error(`FFmpeg MP4 exited with code ${code}`)))
265
- ff.on('error', reject)
266
- })
258
+ const inputPath = join(osTmpdir(), 'mp4-in-' + generateMessageIDV2())
259
+ const outputPath = join(osTmpdir(), 'mp4-out-' + generateMessageIDV2() + '.mp4')
260
+ await fs.writeFile(inputPath, buffer)
261
+ try {
262
+ await new Promise((resolve, reject) => {
263
+ const ff = spawn(ffmpegPath, ['-y', '-i', inputPath, '-c:v', 'libx264', '-preset', 'veryfast', '-crf', '23', '-pix_fmt', 'yuv420p', '-vf', 'scale=trunc(iw/2)*2:trunc(ih/2)*2', '-c:a', 'aac', '-b:a', '128k', '-movflags', '+faststart', '-map_metadata', '-1', outputPath], { stdio: ['ignore', 'ignore', 'pipe'] })
264
+ ff.on('close', code => code === 0 ? resolve() : reject(new Error(`FFmpeg MP4 exited with code ${code}`)))
265
+ ff.on('error', reject)
266
+ })
267
+ return await fs.readFile(outputPath)
268
+ } finally {
269
+ try { await fs.unlink(inputPath) } catch { }
270
+ try { await fs.unlink(outputPath) } catch { }
271
+ }
267
272
  }
268
273
 
269
274
  // ─── STREAM UTILS ─────────────────────────────────────────────────────────────
@@ -317,9 +317,11 @@ const handleButtonReply = (message) => {
317
317
 
318
318
  // ─── MAIN GENERATOR ───────────────────────────────────────────────────────────
319
319
  export const generateWAMessageContent = async (message, options = {}) => {
320
- const messageKeys = Object.keys(message)
320
+ const messageKeys = (message && typeof message === 'object') ? Object.keys(message) : []
321
321
  const isRawProtoMessage = messageKeys.some(k => k.endsWith('Message') && typeof message[k] === 'object' && !HIGH_LEVEL_KEYS.includes(k))
322
- const isWrapperMessage = ['viewOnceMessage', 'ephemeralMessage', 'viewOnceMessageV2', 'documentWithCaptionMessage'].some(k => k in message)
322
+ const isWrapperMessage = (message && typeof message === 'object')
323
+ ? ['viewOnceMessage', 'ephemeralMessage', 'viewOnceMessageV2', 'documentWithCaptionMessage'].some(k => k in message)
324
+ : false
323
325
  if ((isRawProtoMessage || isWrapperMessage) && messageKeys.length === 1) return WAProto.Message.create(message)
324
326
  if (!messageKeys.some(k => HIGH_LEVEL_KEYS.includes(k)) && isRawProtoMessage) return WAProto.Message.create(message)
325
327
 
@@ -1,17 +1,38 @@
1
1
  import { getBinaryNodeChild, getBinaryNodeChildren, isHostedLidUser, isHostedPnUser, isJidMetaAI, isLidUser, isPnUser, jidNormalizedUser } from '../WABinary/index.js';
2
- import { migrateIndexKey, TC_TOKEN_INDEX_KEY } from '../Utils/index.js';
3
-
2
+ import { TC_TOKEN_INDEX_KEY, unixTimestampSeconds } from '../Utils/index.js';
3
+ import { randomBytes } from 'crypto'
4
4
  // Same phone-number pattern as WABinary's isJidBot, applied against the user
5
5
  // part so the check is invariant to @c.us ↔ @s.whatsapp.net normalization.
6
6
  const BOT_PHONE_REGEX = /^1313555\d{4}$|^131655500\d{2}$/;
7
7
 
8
+ const generateTcToken = () => {
9
+ const bucket = Math.floor(unixTimestampSeconds() / 604800)
10
+ const bucketByte = (bucket - 2900) & 0xff
11
+ return Buffer.concat([
12
+ Buffer.from([0x04, 0x01, bucketByte]),
13
+ randomBytes(8)
14
+ ])
15
+ }
16
+
17
+ export const preSeedTcToken = async ({ authState, jid, getLIDForPN, logger }) => {
18
+ const tcTokenJid = await resolveTcTokenJid(jid, getLIDForPN)
19
+ const existing = await authState.keys.get('tctoken', [tcTokenJid])
20
+ if (existing[tcTokenJid]?.token?.length) return { token: existing[tcTokenJid].token, storageJid: tcTokenJid }
21
+ const token = generateTcToken()
22
+ const timestamp = String(unixTimestampSeconds())
23
+ const indexWrite = await buildMergedTcTokenIndexWrite(authState.keys, [tcTokenJid])
24
+ const existingEntry = existing[tcTokenJid] || {}
25
+ await authState.keys.set({ tctoken: { [tcTokenJid]: { ...existingEntry, token, timestamp }, ...indexWrite } })
26
+ logger?.debug({ jid, tcTokenJid }, 'pre-seeded locally generated tctoken')
27
+ return { token, storageJid: tcTokenJid }
28
+ }
8
29
  /**
9
30
  * Mirrors WA Web's `Wid.isRegularUser()` (user ∧ ¬PSA ∧ ¬Bot). Used to gate tctoken
10
31
  * storage against malformed notifications — WA Web filters server-side but we
11
32
  * defend here for parity with `WAWebSetTcTokenChatAction.handleIncomingTcToken`.
12
33
  * Works for both pre- and post-normalized JIDs (`@c.us` vs `@s.whatsapp.net`).
13
34
  */
14
- function isRegularUser(jid) {
35
+ export function isRegularUser(jid) {
15
36
  if (!jid) return false;
16
37
  const user = jid.split('@')[0] ?? '';
17
38
  if (user === '0') return false; // PSA
@@ -23,20 +44,33 @@ function isRegularUser(jid) {
23
44
  const TC_TOKEN_BUCKET_DURATION = 604800; // 7 days
24
45
  const TC_TOKEN_NUM_BUCKETS = 4; // ~28-day rolling window
25
46
 
26
- /** Read the persisted tctoken JID index and return its entries. */
27
47
  export async function readTcTokenIndex(keys) {
28
- const batch = await migrateIndexKey(keys, 'tctoken');
29
- return Object.keys(batch).filter(j => j && j !== TC_TOKEN_INDEX_KEY);
48
+ // Try new sentinel key first, fall back to old 'index' key
49
+ for (const key of [TC_TOKEN_INDEX_KEY, 'index']) {
50
+ const data = await keys.get('tctoken', [key])
51
+ const entry = data[key]
52
+ if (!entry?.token?.length) continue
53
+ try {
54
+ const parsed = JSON.parse(Buffer.from(entry.token).toString())
55
+ if (!Array.isArray(parsed)) continue
56
+ return parsed.filter(j => typeof j === 'string' && j.length > 0 && j !== TC_TOKEN_INDEX_KEY && j !== 'index')
57
+ } catch {
58
+ continue
59
+ }
60
+ }
61
+ return []
30
62
  }
31
63
 
32
- /** Build a SignalDataSet fragment that writes the merged index (persisted ∪ added) under the index key. */
33
64
  export async function buildMergedTcTokenIndexWrite(keys, addedJids) {
34
- const batch = await migrateIndexKey(keys, 'tctoken');
35
- const merged = { ...batch };
36
- for (const jid of addedJids) { if (jid && jid !== TC_TOKEN_INDEX_KEY) merged[jid] = merged[jid] || true; }
37
- return { [TC_TOKEN_INDEX_KEY]: merged };
65
+ const persisted = await readTcTokenIndex(keys)
66
+ const merged = new Set(persisted)
67
+ for (const jid of addedJids) {
68
+ if (jid && jid !== TC_TOKEN_INDEX_KEY && jid !== 'index') merged.add(jid)
69
+ }
70
+ return {
71
+ [TC_TOKEN_INDEX_KEY]: { token: Buffer.from(JSON.stringify([...merged])) }
72
+ }
38
73
  }
39
-
40
74
  // WA Web has separate sender/receiver AB props for these but they're identical today
41
75
  export function isTcTokenExpired(timestamp) {
42
76
  if (timestamp === null || timestamp === undefined) return true;
@@ -59,9 +93,10 @@ export function shouldSendNewTcToken(senderTimestamp) {
59
93
 
60
94
  /** Resolve JID to LID for tctoken storage (WA Web stores under LID) */
61
95
  export async function resolveTcTokenJid(jid, getLIDForPN) {
62
- if (isLidUser(jid)) return jid;
63
- const lid = await getLIDForPN(jid);
64
- return lid ?? jid;
96
+ if (isLidUser(jid)) return jid
97
+ const lid = await getLIDForPN(jid)
98
+ if (!lid) return null
99
+ return lid
65
100
  }
66
101
 
67
102
  /** Resolve target JID for issuing privacy token based on AB prop 14303 */