@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.
- package/README.md +471 -10
- package/lib/Defaults/index.js +0 -4
- package/lib/Signal/libsignal.js +138 -249
- package/lib/Signal/lid-mapping.js +23 -4
- package/lib/Socket/chats.js +1 -0
- package/lib/Socket/messages-recv.js +28 -17
- package/lib/Socket/messages-send.js +43 -18
- package/lib/Socket/newsletter.js +22 -17
- package/lib/Socket/nexus-handler.js +222 -328
- package/lib/Socket/socket.js +27 -18
- package/lib/Utils/browser-utils.js +12 -3
- package/lib/Utils/key-store.js +1 -20
- package/lib/Utils/messages-media.js +21 -16
- package/lib/Utils/messages.js +4 -2
- package/lib/Utils/tc-token-utils.js +50 -15
- package/lib/Utils/use-multi-file-auth-state.js +211 -63
- package/lib/Utils/validate-connection.js +15 -3
- package/lib/WABinary/constants.js +14 -0
- package/lib/index.js +1 -1
- package/package.json +27 -12
package/lib/Socket/socket.js
CHANGED
|
@@ -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
|
-
//
|
|
764
|
+
// Write own device entry — one key per user, no index blob
|
|
759
765
|
const { user, device } = jidDecode(myPN)
|
|
760
|
-
|
|
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
|
-
|
|
772
|
-
|
|
770
|
+
|
|
771
|
+
// Migrate any other known PN sessions to LID (replaces removed migrateAllPNSessionsToLID)
|
|
772
|
+
if (signalRepository.lidMapping.getLIDPNMappings) {
|
|
773
773
|
try {
|
|
774
|
-
const
|
|
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
|
|
26
|
-
|
|
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
|
package/lib/Utils/key-store.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
243
|
-
const ff = spawn(ffmpegPath, ['-y', '-i', inputPath, '-c:a', 'libopus', '-b:a', '
|
|
244
|
-
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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 ─────────────────────────────────────────────────────────────
|
package/lib/Utils/messages.js
CHANGED
|
@@ -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 =
|
|
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 {
|
|
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
|
-
|
|
29
|
-
|
|
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
|
|
35
|
-
const merged =
|
|
36
|
-
for (const jid of addedJids) {
|
|
37
|
-
|
|
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
|
-
|
|
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 */
|