whalibmob 5.5.21 → 5.5.22
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/lib/Client.js +19 -0
- package/lib/DeviceManager.js +414 -33
- package/lib/messages/MessageSender.js +51 -5
- package/lib/signal/SignalStore.js +20 -2
- package/package.json +1 -1
- package/.env.example +0 -49
package/lib/Client.js
CHANGED
|
@@ -175,6 +175,17 @@ class WhalibmobClient extends EventEmitter {
|
|
|
175
175
|
this._signal = SignalProtocol.fromStore(this._store, signalFile, skFile);
|
|
176
176
|
this._devMgr = new DeviceManager(this);
|
|
177
177
|
|
|
178
|
+
// Restore LID ↔ phone mappings persisted from previous sessions.
|
|
179
|
+
// This ensures we can route to LID JIDs even on fresh connections where no
|
|
180
|
+
// incoming message has yet populated _pnToLid during this session.
|
|
181
|
+
const persistedLid = this._signal.store.getLidMappings
|
|
182
|
+
? this._signal.store.getLidMappings() : {};
|
|
183
|
+
for (const [phone, lid] of Object.entries(persistedLid)) {
|
|
184
|
+
this._pnToLid.set(phone, lid);
|
|
185
|
+
this._lidToPn.set(lid, phone);
|
|
186
|
+
}
|
|
187
|
+
process.stderr.write('[DBG] LID_RESTORED count=' + Object.keys(persistedLid).length + '\n');
|
|
188
|
+
|
|
178
189
|
await this._connectSocket();
|
|
179
190
|
return this;
|
|
180
191
|
}
|
|
@@ -530,6 +541,10 @@ class WhalibmobClient extends EventEmitter {
|
|
|
530
541
|
if (lidUser && pnUser && lidUser !== pnUser) {
|
|
531
542
|
this._lidToPn.set(lidUser, pnUser);
|
|
532
543
|
this._pnToLid.set(pnUser, lidUser);
|
|
544
|
+
// Persist so the mapping survives reconnects / process restarts
|
|
545
|
+
if (this._signal && this._signal.store && this._signal.store.setLidMapping) {
|
|
546
|
+
this._signal.store.setLidMapping(pnUser, lidUser);
|
|
547
|
+
}
|
|
533
548
|
}
|
|
534
549
|
}
|
|
535
550
|
process.stderr.write('[DBG] _handleMessage from=' + from + ' participant=' + participant + ' senderPn=' + senderPn + ' id=' + id + '\n');
|
|
@@ -1196,6 +1211,8 @@ class WhalibmobClient extends EventEmitter {
|
|
|
1196
1211
|
const pnUser = pPhone.split('@')[0].split(':')[0];
|
|
1197
1212
|
this._lidToPn.set(lidUser, pnUser);
|
|
1198
1213
|
this._pnToLid.set(pnUser, lidUser);
|
|
1214
|
+
if (this._signal && this._signal.store && this._signal.store.setLidMapping)
|
|
1215
|
+
this._signal.store.setLidMapping(pnUser, lidUser);
|
|
1199
1216
|
}
|
|
1200
1217
|
}
|
|
1201
1218
|
// Case B: participant JID is a PN, lid attribute is their LID
|
|
@@ -1204,6 +1221,8 @@ class WhalibmobClient extends EventEmitter {
|
|
|
1204
1221
|
const lidUser = pLid.split('@')[0].split(':')[0];
|
|
1205
1222
|
this._lidToPn.set(lidUser, pnUser);
|
|
1206
1223
|
this._pnToLid.set(pnUser, lidUser);
|
|
1224
|
+
if (this._signal && this._signal.store && this._signal.store.setLidMapping)
|
|
1225
|
+
this._signal.store.setLidMapping(pnUser, lidUser);
|
|
1207
1226
|
}
|
|
1208
1227
|
return { jid: pJid, role: pRole };
|
|
1209
1228
|
});
|
package/lib/DeviceManager.js
CHANGED
|
@@ -18,6 +18,28 @@ function makeDeviceJid(user, device, server) {
|
|
|
18
18
|
return `${user}:${device}@${server}`;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
// Convert a JID string like "user@server" or "user:device@server" into the
|
|
22
|
+
// BinaryNode JID object format so _writeJid() emits proper binary JID_PAIR /
|
|
23
|
+
// AD_JID bytes instead of raw UTF-8. The server requires binary JID encoding
|
|
24
|
+
// for routing attributes — raw UTF-8 strings are silently ignored for @lid JIDs.
|
|
25
|
+
function jidStrToObj(jidStr) {
|
|
26
|
+
const str = typeof jidStr === 'string' ? jidStr : String(jidStr);
|
|
27
|
+
const at = str.indexOf('@');
|
|
28
|
+
const raw = at >= 0 ? str.slice(0, at) : str;
|
|
29
|
+
const server = at >= 0 ? str.slice(at + 1) : 's.whatsapp.net';
|
|
30
|
+
const colon = raw.indexOf(':');
|
|
31
|
+
const user = colon >= 0 ? raw.slice(0, colon) : raw;
|
|
32
|
+
const device = colon >= 0 ? (parseInt(raw.slice(colon + 1), 10) || 0) : 0;
|
|
33
|
+
const self = str;
|
|
34
|
+
// AD_JID binary format requires both agent AND device fields — use only for
|
|
35
|
+
// multi-device @s.whatsapp.net JIDs where device > 0.
|
|
36
|
+
if (server === 's.whatsapp.net' && device > 0) {
|
|
37
|
+
return { user, agent: 0, device, server, toString() { return self; } };
|
|
38
|
+
}
|
|
39
|
+
// JID_PAIR: used for @lid, @g.us, and primary (device-0) @s.whatsapp.net
|
|
40
|
+
return { user, server, toString() { return self; } };
|
|
41
|
+
}
|
|
42
|
+
|
|
21
43
|
function phoneFromJid(jid) {
|
|
22
44
|
const { user } = stripUser(jid);
|
|
23
45
|
return user;
|
|
@@ -119,30 +141,70 @@ class DeviceManager {
|
|
|
119
141
|
|
|
120
142
|
// ─── Fetch pre-key bundles for a list of JIDs via encrypt IQ ───────────────
|
|
121
143
|
// Only called for JIDs where no Signal session exists yet.
|
|
122
|
-
// Returns Map<
|
|
144
|
+
// Returns Map<string_jid, bundle> — keys are always normalised strings.
|
|
123
145
|
async fetchBundles(jids) {
|
|
124
146
|
if (!jids || jids.length === 0) return new Map();
|
|
125
147
|
|
|
126
|
-
|
|
148
|
+
// Convert a string JID like "user@server" or "user:device@server" into
|
|
149
|
+
// a proper JID object so the binary encoder uses JID_PAIR / AD_JID format.
|
|
150
|
+
// Raw UTF-8 strings are NOT recognised by the WA server for @lid JIDs — the
|
|
151
|
+
// server silently ignores unknown JIDs and returns an empty bundle list.
|
|
152
|
+
const toJidObj = jidStr => {
|
|
153
|
+
const str = typeof jidStr === 'string' ? jidStr : String(jidStr);
|
|
154
|
+
const at = str.indexOf('@');
|
|
155
|
+
const raw = at >= 0 ? str.slice(0, at) : str;
|
|
156
|
+
const server = at >= 0 ? str.slice(at + 1) : 's.whatsapp.net';
|
|
157
|
+
const colon = raw.indexOf(':');
|
|
158
|
+
const user = colon >= 0 ? raw.slice(0, colon) : raw;
|
|
159
|
+
const device = colon >= 0 ? (parseInt(raw.slice(colon + 1), 10) || 0) : 0;
|
|
160
|
+
const self = str;
|
|
161
|
+
// AD_JID binary format is for @s.whatsapp.net with device > 0.
|
|
162
|
+
// JID_PAIR binary format is used for @lid and for device-0 @s.whatsapp.net.
|
|
163
|
+
if (server === 's.whatsapp.net' && device > 0) {
|
|
164
|
+
// AD_JID: agent=0, device=N
|
|
165
|
+
return { user, agent: 0, device, server, toString() { return self; } };
|
|
166
|
+
}
|
|
167
|
+
// JID_PAIR: user@server — server may be 'lid' or 's.whatsapp.net'
|
|
168
|
+
return { user, server, toString() { return self; } };
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const userNodes = jids.map(jid => new BinaryNode('user', { jid: toJidObj(jid) }, null));
|
|
127
172
|
const iqId = this._client._genMsgId();
|
|
128
173
|
const iqNode = new BinaryNode('iq',
|
|
129
174
|
{ id: iqId, xmlns: 'encrypt', type: 'get', to: 's.whatsapp.net' },
|
|
130
175
|
[new BinaryNode('key', {}, userNodes)]
|
|
131
176
|
);
|
|
132
177
|
|
|
178
|
+
process.stderr.write('[DBG] FETCH_BUNDLES jids=[' + jids.join(',') + ']\n');
|
|
179
|
+
|
|
133
180
|
const response = await this._client._sendIq(iqNode);
|
|
134
181
|
const bundles = new Map();
|
|
135
|
-
if (!response)
|
|
182
|
+
if (!response) {
|
|
183
|
+
process.stderr.write('[DBG] FETCH_BUNDLES_NULL\n');
|
|
184
|
+
return bundles;
|
|
185
|
+
}
|
|
136
186
|
|
|
137
187
|
const listNode = findChild(response, 'list');
|
|
138
188
|
const children = listNode ? listNode.content : (response.content || []);
|
|
189
|
+
|
|
190
|
+
process.stderr.write('[DBG] FETCH_BUNDLES_RESP childCount=' +
|
|
191
|
+
(Array.isArray(children) ? children.length : 0) + '\n');
|
|
192
|
+
|
|
139
193
|
for (const userNode of (Array.isArray(children) ? children : [])) {
|
|
140
194
|
if (!userNode || userNode.description !== 'user') continue;
|
|
141
|
-
const
|
|
142
|
-
if (!
|
|
195
|
+
const jidRaw = userNode.attrs && userNode.attrs.jid;
|
|
196
|
+
if (!jidRaw) continue;
|
|
197
|
+
// Always normalise to a string key so Map lookups are consistent
|
|
198
|
+
const jidStr = String(jidRaw);
|
|
199
|
+
process.stderr.write('[DBG] FETCH_BUNDLES_USER jid=' + jidStr +
|
|
200
|
+
' childTags=' + (Array.isArray(userNode.content)
|
|
201
|
+
? userNode.content.map(c => c && c.description).filter(Boolean).join(',')
|
|
202
|
+
: '') + '\n');
|
|
143
203
|
const bundle = parseBundleFromUserNode(userNode);
|
|
144
|
-
if (bundle) bundles.set(
|
|
204
|
+
if (bundle) bundles.set(jidStr, bundle);
|
|
205
|
+
else process.stderr.write('[DBG] FETCH_BUNDLES_NO_BUNDLE jid=' + jidStr + '\n');
|
|
145
206
|
}
|
|
207
|
+
process.stderr.write('[DBG] FETCH_BUNDLES_DONE count=' + bundles.size + '\n');
|
|
146
208
|
return bundles;
|
|
147
209
|
}
|
|
148
210
|
|
|
@@ -198,54 +260,158 @@ class DeviceManager {
|
|
|
198
260
|
return jids.length > 0 ? jids : phones.map(p => makeDeviceJid(p, 0));
|
|
199
261
|
}
|
|
200
262
|
|
|
201
|
-
//
|
|
263
|
+
// ─── FIXED usync IQ — corrects 4 bugs in the original implementation ────────
|
|
264
|
+
//
|
|
265
|
+
// Bug 1 — Wrong <list> node format:
|
|
266
|
+
// Was: <user><contact>+phone</contact></user>
|
|
267
|
+
// Fix: <user jid="phone@s.whatsapp.net"/> (jid ATTRIBUTE, not contact child)
|
|
268
|
+
//
|
|
269
|
+
// Bug 2 — Wrong <query><devices> node:
|
|
270
|
+
// Was: <devices version="2"><device jid="phone@s.whatsapp.net"/></devices>
|
|
271
|
+
// Fix: <devices version="2"/> (NO device children in query)
|
|
272
|
+
//
|
|
273
|
+
// Bug 3 — Missing <side_list/> node:
|
|
274
|
+
// Was: (absent)
|
|
275
|
+
// Fix: <side_list/> (required for LID device discovery)
|
|
276
|
+
//
|
|
277
|
+
// Bug 4 — Wrong response parser:
|
|
278
|
+
// Was: deep walk looking for <device jid="..."> attrs
|
|
279
|
+
// Fix: navigate usync → list → user[jid=...] → devices → device-list → device[id=N]
|
|
280
|
+
// Server returns <device id="0"/> NOT <device jid="..."/>
|
|
281
|
+
// The phone/LID identifier is on the PARENT <user jid="..."> node, not on device.
|
|
282
|
+
//
|
|
202
283
|
async _doUsyncIq(phones) {
|
|
203
284
|
const iqId = this._client._genMsgId();
|
|
204
285
|
const sid = this._client._genMsgId();
|
|
205
286
|
|
|
287
|
+
// FIX Bug 1: jid attribute on <user>, not a <contact> child node
|
|
288
|
+
// FIX Bug 2: <devices version="2"/> with NO children
|
|
206
289
|
const listChildren = phones.map(p =>
|
|
207
|
-
new BinaryNode('user', {},
|
|
208
|
-
[new BinaryNode('contact', {}, Buffer.from('+' + p))]
|
|
209
|
-
)
|
|
210
|
-
);
|
|
211
|
-
const deviceChildren = phones.map(p =>
|
|
212
|
-
new BinaryNode('device', { jid: makeDeviceJid(p, 0) }, null)
|
|
290
|
+
new BinaryNode('user', { jid: `${p}@s.whatsapp.net` }, null)
|
|
213
291
|
);
|
|
214
292
|
|
|
215
293
|
const iqNode = new BinaryNode('iq',
|
|
216
294
|
{ id: iqId, to: 's.whatsapp.net', type: 'get', xmlns: 'usync' },
|
|
217
295
|
[new BinaryNode('usync',
|
|
218
|
-
{ sid, mode: 'query', last: 'true', index: '0', context: '
|
|
296
|
+
{ sid, mode: 'query', last: 'true', index: '0', context: 'interactive' },
|
|
219
297
|
[
|
|
220
298
|
new BinaryNode('query', {},
|
|
221
|
-
[new BinaryNode('devices', { version: '2' },
|
|
299
|
+
[new BinaryNode('devices', { version: '2' }, null)] // FIX Bug 2: null, not deviceChildren
|
|
222
300
|
),
|
|
223
|
-
new BinaryNode('list', {}, listChildren)
|
|
301
|
+
new BinaryNode('list', {}, listChildren),
|
|
302
|
+
new BinaryNode('side_list', {}, null) // FIX Bug 3: required node
|
|
224
303
|
]
|
|
225
304
|
)]
|
|
226
305
|
);
|
|
227
306
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
307
|
+
process.stderr.write('[DBG] USYNC_IQ phones=[' + phones.join(',') + ']\n');
|
|
308
|
+
|
|
309
|
+
const response = await this._client._sendIq(iqNode).catch(err => {
|
|
310
|
+
process.stderr.write('[DBG] USYNC_ERR ' + (err && err.message) + '\n');
|
|
311
|
+
return null;
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// FIX Bug 4: Correct response structure is:
|
|
315
|
+
// <usync>
|
|
316
|
+
// <list>
|
|
317
|
+
// <user jid="phone@s.whatsapp.net" [lid="lid_user@lid"]>
|
|
318
|
+
// <devices>
|
|
319
|
+
// <device-list>
|
|
320
|
+
// <device id="0"/>
|
|
321
|
+
// <device id="2"/>
|
|
322
|
+
// </device-list>
|
|
323
|
+
// </devices>
|
|
324
|
+
// </user>
|
|
325
|
+
// </list>
|
|
326
|
+
// </usync>
|
|
327
|
+
if (response) {
|
|
328
|
+
const usyncNode = findChild(response, 'usync');
|
|
329
|
+
const listNode = usyncNode
|
|
330
|
+
? findChild(usyncNode, 'list')
|
|
331
|
+
: findChild(response, 'list');
|
|
332
|
+
|
|
333
|
+
if (listNode && Array.isArray(listNode.content)) {
|
|
334
|
+
for (const userNode of listNode.content) {
|
|
335
|
+
if (!userNode || userNode.description !== 'user') continue;
|
|
336
|
+
|
|
337
|
+
const userJid = userNode.attrs && (userNode.attrs.jid || userNode.attrs.value);
|
|
338
|
+
if (!userJid) continue;
|
|
339
|
+
|
|
340
|
+
const isLidUser = String(userJid).endsWith('@lid');
|
|
341
|
+
const { user: rawUser } = stripUser(String(userJid));
|
|
342
|
+
|
|
343
|
+
// Determine phone-based cache key (always a phone number, never a LID)
|
|
344
|
+
let cachePhone;
|
|
345
|
+
if (isLidUser) {
|
|
346
|
+
// Modern account: server returned LID JID in the user node
|
|
347
|
+
const pn = this._client._lidToPn && this._client._lidToPn.get(rawUser);
|
|
348
|
+
if (pn) {
|
|
349
|
+
cachePhone = pn;
|
|
350
|
+
} else if (phones.length === 1) {
|
|
351
|
+
// Single phone queried — this LID must belong to it
|
|
352
|
+
cachePhone = phones[0];
|
|
353
|
+
if (this._client._lidToPn) this._client._lidToPn.set(rawUser, phones[0]);
|
|
354
|
+
if (this._client._pnToLid) this._client._pnToLid.set(phones[0], rawUser);
|
|
355
|
+
} else {
|
|
356
|
+
cachePhone = rawUser; // last resort: use LID user as key
|
|
357
|
+
}
|
|
358
|
+
process.stderr.write('[DBG] USYNC_LID_USER userJid=' + userJid + ' → cachePhone=' + cachePhone + '\n');
|
|
359
|
+
} else {
|
|
360
|
+
// Normal account: user JID is phone@s.whatsapp.net
|
|
361
|
+
cachePhone = rawUser;
|
|
362
|
+
|
|
363
|
+
// Also extract LID from user node's `lid` attribute if the server provided it
|
|
364
|
+
const lidAttr = userNode.attrs && userNode.attrs.lid;
|
|
365
|
+
if (lidAttr) {
|
|
366
|
+
const lidUser = String(lidAttr).split('@')[0].split(':')[0];
|
|
367
|
+
if (this._client._lidToPn) this._client._lidToPn.set(lidUser, cachePhone);
|
|
368
|
+
if (this._client._pnToLid) this._client._pnToLid.set(cachePhone, lidUser);
|
|
369
|
+
process.stderr.write('[DBG] USYNC_LID_MAP phone=' + cachePhone + ' ↔ lid=' + lidUser + '\n');
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Navigate user → devices → device-list → device[id=N]
|
|
374
|
+
const devicesNode = findChild(userNode, 'devices');
|
|
375
|
+
const deviceListNode = devicesNode ? findChild(devicesNode, 'device-list') : null;
|
|
376
|
+
|
|
377
|
+
if (!this._deviceCache.has(cachePhone)) {
|
|
378
|
+
this._deviceCache.set(cachePhone, new Set());
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (deviceListNode && Array.isArray(deviceListNode.content)) {
|
|
382
|
+
for (const devNode of deviceListNode.content) {
|
|
383
|
+
if (!devNode || devNode.description !== 'device') continue;
|
|
384
|
+
// FIX: server uses `id` attribute (e.g. id="0"), NOT `jid` attribute
|
|
385
|
+
const devId = devNode.attrs && devNode.attrs.id != null
|
|
386
|
+
? parseInt(String(devNode.attrs.id), 10)
|
|
387
|
+
: 0;
|
|
388
|
+
this._deviceCache.get(cachePhone).add(devId);
|
|
389
|
+
}
|
|
390
|
+
process.stderr.write('[DBG] USYNC_DEVICES phone=' + cachePhone +
|
|
391
|
+
' ids=[' + [...this._deviceCache.get(cachePhone)].join(',') + ']\n');
|
|
392
|
+
} else {
|
|
393
|
+
// No device-list in response — fall back to device 0
|
|
394
|
+
this._deviceCache.get(cachePhone).add(0);
|
|
395
|
+
process.stderr.write('[DBG] USYNC_NO_DEVLIST phone=' + cachePhone + ' → fallback id=0\n');
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
} else {
|
|
399
|
+
process.stderr.write('[DBG] USYNC_RESP_NO_LIST\n');
|
|
240
400
|
}
|
|
241
|
-
|
|
401
|
+
} else {
|
|
402
|
+
process.stderr.write('[DBG] USYNC_NULL_RESP\n');
|
|
403
|
+
// Device usync timed out — fall back to a contact usync (query/contact).
|
|
404
|
+
// This DOES receive a server response and resolves the phone → LID mapping.
|
|
405
|
+
// Once _pnToLid is populated here, _sendDMMessage (after its await) will
|
|
406
|
+
// use the LID JID for routing instead of the stale phone JID.
|
|
407
|
+
await this._doContactUsync(phones).catch(() => {});
|
|
242
408
|
}
|
|
243
|
-
if (response) walk.call(this, response);
|
|
244
409
|
|
|
245
|
-
// For
|
|
410
|
+
// For phones with no usync response at all, cache device 0 so we don't re-query
|
|
246
411
|
for (const p of phones) {
|
|
247
412
|
if (!this._deviceCache.has(p)) {
|
|
248
413
|
this._deviceCache.set(p, new Set([0]));
|
|
414
|
+
process.stderr.write('[DBG] USYNC_FALLBACK phone=' + p + '\n');
|
|
249
415
|
}
|
|
250
416
|
}
|
|
251
417
|
}
|
|
@@ -297,6 +463,217 @@ class DeviceManager {
|
|
|
297
463
|
return readyJids;
|
|
298
464
|
}
|
|
299
465
|
|
|
466
|
+
// ─── Ensure sessions for a LID-addressed recipient ────────────────────────
|
|
467
|
+
//
|
|
468
|
+
// For accounts migrated to WhatsApp LID, devices are registered under the
|
|
469
|
+
// LID identity (e.g. 139471160877194@lid), NOT the phone number.
|
|
470
|
+
// This method:
|
|
471
|
+
// 1. Queries usync for device list by LID JID
|
|
472
|
+
// 2. Establishes Signal sessions for LID device JIDs (e.g. 139471160877194:0@lid)
|
|
473
|
+
// 3. Returns the list of ready LID device JIDs for participant fanout
|
|
474
|
+
//
|
|
475
|
+
async bulkEnsureSessionsForLid(lidUser, signalProto, allowPkmsg = true, skipUsync = false) {
|
|
476
|
+
const lidJid = `${lidUser}@lid`;
|
|
477
|
+
const cacheKey = `lid:${lidUser}`;
|
|
478
|
+
|
|
479
|
+
// skipUsync=true: caller already knows the LID (from _pnToLid) — skip the
|
|
480
|
+
// usync round-trip and assume primary device 0. Saves up to 15 s.
|
|
481
|
+
if (skipUsync) {
|
|
482
|
+
if (!this._deviceCache.has(cacheKey)) {
|
|
483
|
+
this._deviceCache.set(cacheKey, new Set([0]));
|
|
484
|
+
process.stderr.write('[DBG] LID_SKIP_USYNC lidUser=' + lidUser + ' assume device=0\n');
|
|
485
|
+
}
|
|
486
|
+
} else if (!this._deviceCache.has(cacheKey)) {
|
|
487
|
+
await this._doUsyncIqByJid(lidJid, cacheKey, lidUser);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const deviceIds = this._deviceCache.get(cacheKey) || new Set([0]);
|
|
491
|
+
const deviceJids = [...deviceIds].map(d => makeDeviceJid(lidUser, d, 'lid'));
|
|
492
|
+
|
|
493
|
+
process.stderr.write('[DBG] LID_DEVICES lidUser=' + lidUser +
|
|
494
|
+
' deviceJids=[' + deviceJids.join(',') + ']\n');
|
|
495
|
+
|
|
496
|
+
// Split: existing sessions (ready) vs new (need bundle fetch)
|
|
497
|
+
const readyJids = [];
|
|
498
|
+
const fetchJids = [];
|
|
499
|
+
for (const jid of deviceJids) {
|
|
500
|
+
if (signalProto.store.hasSession(this._jidToAddr(jid))) {
|
|
501
|
+
readyJids.push(jid);
|
|
502
|
+
} else {
|
|
503
|
+
fetchJids.push(jid);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (allowPkmsg && fetchJids.length > 0) {
|
|
508
|
+
const bundles = await this.fetchBundles(fetchJids);
|
|
509
|
+
for (const [jid, bundle] of bundles) {
|
|
510
|
+
try {
|
|
511
|
+
await signalProto.buildSessionFromBundle(jid, bundle);
|
|
512
|
+
readyJids.push(jid);
|
|
513
|
+
process.stderr.write('[DBG] LID_SESSION_BUILT jid=' + jid + '\n');
|
|
514
|
+
} catch (e) {
|
|
515
|
+
process.stderr.write('[DBG] LID_SESSION_ERR jid=' + jid + ' err=' + e.message + '\n');
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Fallback: use primary LID device 0 directly (no session yet, will use pkmsg)
|
|
521
|
+
if (readyJids.length === 0) {
|
|
522
|
+
readyJids.push(makeDeviceJid(lidUser, 0, 'lid'));
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return readyJids;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// ─── Contact usync fallback ────────────────────────────────────────────────
|
|
529
|
+
// When query/devices usync times out (server silently ignores it for LID
|
|
530
|
+
// accounts), we fall back to query/contact which uses the older
|
|
531
|
+
// <user><contact>+phone</contact></user> format and DOES get a response.
|
|
532
|
+
// The response includes the actual JID (LID or PN) for each phone.
|
|
533
|
+
// Calling this populates _pnToLid / _lidToPn in the Client, which is used
|
|
534
|
+
// by _sendDMMessage (checked AFTER the await of _usyncGetDevices).
|
|
535
|
+
async _doContactUsync(phones) {
|
|
536
|
+
const iqId = this._client._genMsgId();
|
|
537
|
+
const sid = this._client._genMsgId();
|
|
538
|
+
|
|
539
|
+
const userNodes = phones.map(p =>
|
|
540
|
+
new BinaryNode('user', {}, [
|
|
541
|
+
new BinaryNode('contact', {}, Buffer.from('+' + p, 'utf8'))
|
|
542
|
+
])
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
const iqNode = new BinaryNode('iq',
|
|
546
|
+
{ id: iqId, to: 's.whatsapp.net', type: 'get', xmlns: 'usync' },
|
|
547
|
+
[new BinaryNode('usync',
|
|
548
|
+
{ sid, mode: 'query', last: 'true', index: '0', context: 'interactive' },
|
|
549
|
+
[
|
|
550
|
+
new BinaryNode('query', {}, [new BinaryNode('contact', {}, null)]),
|
|
551
|
+
new BinaryNode('list', {}, userNodes),
|
|
552
|
+
new BinaryNode('side_list', {}, null)
|
|
553
|
+
]
|
|
554
|
+
)]
|
|
555
|
+
);
|
|
556
|
+
|
|
557
|
+
process.stderr.write('[DBG] CONTACT_USYNC phones=[' + phones.join(',') + ']\n');
|
|
558
|
+
|
|
559
|
+
const response = await this._client._sendIq(iqNode).catch(err => {
|
|
560
|
+
process.stderr.write('[DBG] CONTACT_USYNC_ERR ' + (err && err.message) + '\n');
|
|
561
|
+
return null;
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
if (!response) {
|
|
565
|
+
process.stderr.write('[DBG] CONTACT_USYNC_NULL\n');
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const usyncNode = findChild(response, 'usync');
|
|
570
|
+
const listNode = usyncNode ? findChild(usyncNode, 'list') : findChild(response, 'list');
|
|
571
|
+
|
|
572
|
+
if (!listNode || !Array.isArray(listNode.content)) {
|
|
573
|
+
process.stderr.write('[DBG] CONTACT_USYNC_NO_LIST\n');
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
for (let i = 0; i < listNode.content.length; i++) {
|
|
578
|
+
const userNode = listNode.content[i];
|
|
579
|
+
if (!userNode || userNode.description !== 'user') continue;
|
|
580
|
+
|
|
581
|
+
const actualJid = userNode.attrs && (userNode.attrs.jid || userNode.attrs.value);
|
|
582
|
+
if (!actualJid) continue;
|
|
583
|
+
|
|
584
|
+
const actualJidStr = String(actualJid);
|
|
585
|
+
const isLid = actualJidStr.endsWith('@lid');
|
|
586
|
+
|
|
587
|
+
if (isLid) {
|
|
588
|
+
const { user: lidUser } = stripUser(actualJidStr);
|
|
589
|
+
// Determine the corresponding phone. For single-phone queries it's
|
|
590
|
+
// unambiguous; for multi-phone queries match by index.
|
|
591
|
+
const phone = phones.length === 1 ? phones[0] : phones[i];
|
|
592
|
+
if (phone) {
|
|
593
|
+
process.stderr.write('[DBG] CONTACT_USYNC_LID phone=' + phone + ' → lid=' + lidUser + '\n');
|
|
594
|
+
if (this._client._pnToLid) this._client._pnToLid.set(phone, lidUser);
|
|
595
|
+
if (this._client._lidToPn) this._client._lidToPn.set(lidUser, phone);
|
|
596
|
+
// Persist the mapping so future sessions don't need to re-query
|
|
597
|
+
const store = this._client._signal && this._client._signal.store;
|
|
598
|
+
if (store && store.setLidMapping) store.setLidMapping(phone, lidUser);
|
|
599
|
+
}
|
|
600
|
+
} else {
|
|
601
|
+
// PN account — note it (still on phone-based routing, no LID needed)
|
|
602
|
+
const { user: pnUser } = stripUser(actualJidStr);
|
|
603
|
+
const phone = phones.length === 1 ? phones[0] : phones[i];
|
|
604
|
+
process.stderr.write('[DBG] CONTACT_USYNC_PN phone=' + phone + ' jid=' + actualJidStr + '\n');
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// ─── usync IQ for a specific JID (LID or phone-based) ─────────────────────
|
|
610
|
+
// Used when we already know the routing JID and want to look up devices for it.
|
|
611
|
+
async _doUsyncIqByJid(jid, cacheKey, lidUser) {
|
|
612
|
+
const iqId = this._client._genMsgId();
|
|
613
|
+
const sid = this._client._genMsgId();
|
|
614
|
+
|
|
615
|
+
const iqNode = new BinaryNode('iq',
|
|
616
|
+
{ id: iqId, to: 's.whatsapp.net', type: 'get', xmlns: 'usync' },
|
|
617
|
+
[new BinaryNode('usync',
|
|
618
|
+
{ sid, mode: 'query', last: 'true', index: '0', context: 'interactive' },
|
|
619
|
+
[
|
|
620
|
+
new BinaryNode('query', {},
|
|
621
|
+
[new BinaryNode('devices', { version: '2' }, null)]
|
|
622
|
+
),
|
|
623
|
+
new BinaryNode('list', {},
|
|
624
|
+
[new BinaryNode('user', { jid }, null)]
|
|
625
|
+
),
|
|
626
|
+
new BinaryNode('side_list', {}, null)
|
|
627
|
+
]
|
|
628
|
+
)]
|
|
629
|
+
);
|
|
630
|
+
|
|
631
|
+
process.stderr.write('[DBG] USYNC_LID_IQ jid=' + jid + '\n');
|
|
632
|
+
|
|
633
|
+
const response = await this._client._sendIq(iqNode).catch(err => {
|
|
634
|
+
process.stderr.write('[DBG] USYNC_LID_IQ_ERR ' + (err && err.message) + '\n');
|
|
635
|
+
return null;
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
if (!this._deviceCache.has(cacheKey)) {
|
|
639
|
+
this._deviceCache.set(cacheKey, new Set());
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (response) {
|
|
643
|
+
const usyncNode = findChild(response, 'usync');
|
|
644
|
+
const listNode = usyncNode ? findChild(usyncNode, 'list') : findChild(response, 'list');
|
|
645
|
+
|
|
646
|
+
if (listNode && Array.isArray(listNode.content)) {
|
|
647
|
+
for (const userNode of listNode.content) {
|
|
648
|
+
if (!userNode || userNode.description !== 'user') continue;
|
|
649
|
+
|
|
650
|
+
const devicesNode = findChild(userNode, 'devices');
|
|
651
|
+
const deviceListNode = devicesNode ? findChild(devicesNode, 'device-list') : null;
|
|
652
|
+
|
|
653
|
+
if (deviceListNode && Array.isArray(deviceListNode.content)) {
|
|
654
|
+
for (const devNode of deviceListNode.content) {
|
|
655
|
+
if (!devNode || devNode.description !== 'device') continue;
|
|
656
|
+
const devId = devNode.attrs && devNode.attrs.id != null
|
|
657
|
+
? parseInt(String(devNode.attrs.id), 10) : 0;
|
|
658
|
+
this._deviceCache.get(cacheKey).add(devId);
|
|
659
|
+
}
|
|
660
|
+
process.stderr.write('[DBG] USYNC_LID_DEVICES jid=' + jid +
|
|
661
|
+
' ids=[' + [...this._deviceCache.get(cacheKey)].join(',') + ']\n');
|
|
662
|
+
} else {
|
|
663
|
+
this._deviceCache.get(cacheKey).add(0);
|
|
664
|
+
process.stderr.write('[DBG] USYNC_LID_NO_DEVLIST jid=' + jid + ' → fallback id=0\n');
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
} else {
|
|
668
|
+
process.stderr.write('[DBG] USYNC_LID_NO_LIST jid=' + jid + '\n');
|
|
669
|
+
this._deviceCache.get(cacheKey).add(0);
|
|
670
|
+
}
|
|
671
|
+
} else {
|
|
672
|
+
process.stderr.write('[DBG] USYNC_LID_NULL_RESP jid=' + jid + '\n');
|
|
673
|
+
this._deviceCache.get(cacheKey).add(0);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
300
677
|
// ─── Ensure sessions for own linked devices (device != 0) ─────────────────
|
|
301
678
|
//
|
|
302
679
|
// OPTIMISATION: own device list is cached for the entire connection lifetime.
|
|
@@ -365,7 +742,11 @@ class DeviceManager {
|
|
|
365
742
|
const toNodes = encryptedList.map(({ jid, type, ciphertext }) => {
|
|
366
743
|
const encAttrs = { type, v: '2' };
|
|
367
744
|
if (mediaSubtype) encAttrs.mediatype = mediaSubtype;
|
|
368
|
-
|
|
745
|
+
// Always encode the jid as a binary JID object (JID_PAIR / AD_JID) so the
|
|
746
|
+
// server can route the per-device enc payload to the correct Signal session.
|
|
747
|
+
// Raw UTF-8 strings are silently dropped for @lid recipients.
|
|
748
|
+
const jidAttr = jidStrToObj(jid);
|
|
749
|
+
return new BinaryNode('to', { jid: jidAttr },
|
|
369
750
|
[new BinaryNode('enc', encAttrs, ciphertext)]
|
|
370
751
|
);
|
|
371
752
|
});
|
|
@@ -373,4 +754,4 @@ class DeviceManager {
|
|
|
373
754
|
}
|
|
374
755
|
}
|
|
375
756
|
|
|
376
|
-
module.exports = { DeviceManager, makeDeviceJid, stripUser, phoneFromJid, parseBundleFromUserNode };
|
|
757
|
+
module.exports = { DeviceManager, makeDeviceJid, stripUser, phoneFromJid, parseBundleFromUserNode, jidStrToObj };
|
|
@@ -4,7 +4,7 @@ const crypto = require('crypto');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const { BinaryNode } = require('../BinaryNode');
|
|
7
|
-
const { DeviceManager } = require('../DeviceManager');
|
|
7
|
+
const { DeviceManager, jidStrToObj } = require('../DeviceManager');
|
|
8
8
|
const {
|
|
9
9
|
encodeMessage,
|
|
10
10
|
encodeText, encodeImageMessage, encodeVideoMessage,
|
|
@@ -553,12 +553,42 @@ class MessageSender {
|
|
|
553
553
|
// Primary devices can always use pkmsg to establish new Signal sessions —
|
|
554
554
|
// they simply omit the device-identity node (server accepts this for primaries).
|
|
555
555
|
const allowPkmsg = true;
|
|
556
|
-
|
|
556
|
+
|
|
557
|
+
// First, run the phone-based usync (and own devices) in parallel.
|
|
558
|
+
// IMPORTANT: _pnToLid may be empty RIGHT NOW but gets populated during this
|
|
559
|
+
// await — incoming messages from the recipient (which arrive while we wait
|
|
560
|
+
// for the usync timeout) tell us their LID JID via from=LID + sender_pn attrs.
|
|
561
|
+
// We therefore check _pnToLid AFTER this await, not before.
|
|
562
|
+
const [_recipientDevicesByPhone, ownDevices] = await Promise.all([
|
|
557
563
|
this._devMgr.bulkEnsureSessions([recipientPhone], this._signal, allowPkmsg),
|
|
558
564
|
this._devMgr.ensureOwnDeviceSessions(ownPhone, this._signal, allowPkmsg)
|
|
559
565
|
]);
|
|
560
566
|
|
|
561
|
-
|
|
567
|
+
// ── LID routing fix ───────────────────────────────────────────────────────
|
|
568
|
+
// Check _pnToLid NOW — after the await above, incoming messages from the
|
|
569
|
+
// recipient during the usync wait will have populated this map.
|
|
570
|
+
// For LID-migrated accounts, the message `to` field AND Signal participants
|
|
571
|
+
// must use the LID JID. Phone-JID messages are silently accepted by the
|
|
572
|
+
// server but never delivered to the recipient's LID-registered device.
|
|
573
|
+
const lidUser = this._client._pnToLid && this._client._pnToLid.get(recipientPhone);
|
|
574
|
+
const routingToJid = lidUser ? `${lidUser}@lid` : toJid;
|
|
575
|
+
|
|
576
|
+
process.stderr.write('[DBG] DM_ROUTE phone=' + recipientPhone +
|
|
577
|
+
' routing=' + routingToJid + (lidUser ? ' (LID)' : ' (PN)') + '\n');
|
|
578
|
+
|
|
579
|
+
let otherJids;
|
|
580
|
+
if (lidUser) {
|
|
581
|
+
// Fetch bundle + build Signal session for the LID JID.
|
|
582
|
+
// skipUsync=true: we already know the LID from _pnToLid — skip a second
|
|
583
|
+
// 15 s usync round-trip and go straight to bundle fetch (device 0 primary).
|
|
584
|
+
const lidDevices = await this._devMgr.bulkEnsureSessionsForLid(
|
|
585
|
+
lidUser, this._signal, allowPkmsg, /* skipUsync */ true);
|
|
586
|
+
otherJids = lidDevices.length > 0 ? lidDevices : [routingToJid];
|
|
587
|
+
} else {
|
|
588
|
+
otherJids = _recipientDevicesByPhone.length > 0 ? _recipientDevicesByPhone : [toJid];
|
|
589
|
+
}
|
|
590
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
591
|
+
|
|
562
592
|
const ownLinkedJids = ownDevices.filter(j => j !== ownMainJid);
|
|
563
593
|
|
|
564
594
|
const allParticipants = [...otherJids, ...ownLinkedJids];
|
|
@@ -577,7 +607,10 @@ class MessageSender {
|
|
|
577
607
|
|
|
578
608
|
const encryptedList = [...otherEncrypted, ...ownEncrypted];
|
|
579
609
|
|
|
580
|
-
|
|
610
|
+
// Encode the routing JID as a binary JID object (JID_PAIR for @lid / @s.whatsapp.net).
|
|
611
|
+
// Raw UTF-8 strings are not recognised by the server for LID recipients, causing
|
|
612
|
+
// the message to be accepted (server ACK) but never delivered to the device.
|
|
613
|
+
const stanzaAttrs = { to: jidStrToObj(routingToJid), id: msgId, type: mediaType, t: String(msNow()) };
|
|
581
614
|
if (phash) stanzaAttrs.phash = phash;
|
|
582
615
|
if (options.edit) stanzaAttrs.edit = String(options.edit);
|
|
583
616
|
|
|
@@ -599,6 +632,19 @@ class MessageSender {
|
|
|
599
632
|
|
|
600
633
|
const msgNode = new BinaryNode('message', stanzaAttrs, msgContent);
|
|
601
634
|
|
|
635
|
+
// Debug: log outgoing stanza details
|
|
636
|
+
process.stderr.write('[DBG] DM_SEND to=' + toJid +
|
|
637
|
+
' otherJids=[' + otherJids.join(',') + ']' +
|
|
638
|
+
' ownLinked=[' + ownLinkedJids.join(',') + ']' +
|
|
639
|
+
' encrypted=' + encryptedList.length +
|
|
640
|
+
' hasPkmsg=' + hasPkmsg +
|
|
641
|
+
' hasAdv=' + !!(advBytes) +
|
|
642
|
+
'\n');
|
|
643
|
+
if (encryptedList.length > 0) {
|
|
644
|
+
process.stderr.write('[DBG] DM_PARTICIPANTS ' +
|
|
645
|
+
encryptedList.map(e => e.jid + '(' + e.type + ')').join(', ') + '\n');
|
|
646
|
+
}
|
|
647
|
+
|
|
602
648
|
// Cache plaintext so Client can re-send with fresh session on recipient retry
|
|
603
649
|
if (this._client._sentMsgCache) {
|
|
604
650
|
this._client._sentMsgCache.set(msgId, {
|
|
@@ -699,7 +745,7 @@ class MessageSender {
|
|
|
699
745
|
msgContent.push(new BinaryNode('enc', skmsgAttrs, Buffer.isBuffer(skmsgCiphertext) ? skmsgCiphertext : Buffer.from(skmsgCiphertext)));
|
|
700
746
|
|
|
701
747
|
const stanzaAttrs = {
|
|
702
|
-
to: groupJid,
|
|
748
|
+
to: jidStrToObj(groupJid),
|
|
703
749
|
id: msgId,
|
|
704
750
|
type: mediaType,
|
|
705
751
|
addressing_mode: groupAddressingMode,
|
|
@@ -26,6 +26,7 @@ class SignalStore {
|
|
|
26
26
|
this._signedPreKeys = {};
|
|
27
27
|
this._identities = {};
|
|
28
28
|
this._filePath = null;
|
|
29
|
+
this._lidMappings = {}; // phone → lid — persisted alongside sessions
|
|
29
30
|
|
|
30
31
|
// Debounce state
|
|
31
32
|
this._dirty = false;
|
|
@@ -56,9 +57,24 @@ class SignalStore {
|
|
|
56
57
|
this._preKeys = raw.preKeys || {};
|
|
57
58
|
this._signedPreKeys = raw.signedPreKeys || {};
|
|
58
59
|
this._identities = raw.identities || {};
|
|
60
|
+
this._lidMappings = raw.lidMappings || {}; // phone → lid, persisted
|
|
59
61
|
} catch (_) {}
|
|
60
62
|
}
|
|
61
63
|
|
|
64
|
+
// Persist a phone ↔ LID mapping. Called any time we learn a new mapping from
|
|
65
|
+
// incoming messages or from a contact usync fallback query.
|
|
66
|
+
setLidMapping(phone, lid) {
|
|
67
|
+
if (!phone || !lid) return;
|
|
68
|
+
if (this._lidMappings[phone] === lid) return; // already stored, skip disk write
|
|
69
|
+
this._lidMappings[phone] = lid;
|
|
70
|
+
this._save();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Return all stored phone → lid mappings (used to populate Client._pnToLid on startup)
|
|
74
|
+
getLidMappings() {
|
|
75
|
+
return this._lidMappings;
|
|
76
|
+
}
|
|
77
|
+
|
|
62
78
|
// Immediate synchronous flush (used on process exit only)
|
|
63
79
|
_flushSync() {
|
|
64
80
|
if (!this._dirty || !this._filePath) return;
|
|
@@ -68,7 +84,8 @@ class SignalStore {
|
|
|
68
84
|
sessions: this._sessions,
|
|
69
85
|
preKeys: this._preKeys,
|
|
70
86
|
signedPreKeys: this._signedPreKeys,
|
|
71
|
-
identities: this._identities
|
|
87
|
+
identities: this._identities,
|
|
88
|
+
lidMappings: this._lidMappings
|
|
72
89
|
});
|
|
73
90
|
const dir = path.dirname(this._filePath);
|
|
74
91
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
@@ -89,7 +106,8 @@ class SignalStore {
|
|
|
89
106
|
sessions: this._sessions,
|
|
90
107
|
preKeys: this._preKeys,
|
|
91
108
|
signedPreKeys: this._signedPreKeys,
|
|
92
|
-
identities: this._identities
|
|
109
|
+
identities: this._identities,
|
|
110
|
+
lidMappings: this._lidMappings
|
|
93
111
|
});
|
|
94
112
|
const dir = path.dirname(this._filePath);
|
|
95
113
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
package/package.json
CHANGED
package/.env.example
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
# ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
-
# whalibmob — Device Emulation Configuration
|
|
3
|
-
# Copy this file to .env in your project root and set the values you need.
|
|
4
|
-
# All variables are optional; defaults emulate an iPhone 15 Pro running iOS 17.
|
|
5
|
-
# ─────────────────────────────────────────────────────────────────────────────
|
|
6
|
-
|
|
7
|
-
# Operating system to emulate. Accepted values: ios | android
|
|
8
|
-
# Default: ios
|
|
9
|
-
# WA_OS=android
|
|
10
|
-
|
|
11
|
-
# Named device profile.
|
|
12
|
-
#
|
|
13
|
-
# iOS profiles (WA_OS=ios):
|
|
14
|
-
# iphone_15_pro, iphone_15, iphone_14_pro, iphone_14, iphone_13_pro,
|
|
15
|
-
# iphone_13, iphone_12_pro, iphone_12, iphone_11_pro, iphone_11,
|
|
16
|
-
# iphone_se3, iphone_xs
|
|
17
|
-
#
|
|
18
|
-
# Android profiles (WA_OS=android):
|
|
19
|
-
# pixel_8_pro, pixel_8, pixel_7, pixel_7a,
|
|
20
|
-
# samsung_s24_ultra, samsung_s24, samsung_s23_ultra, samsung_s23, samsung_a55,
|
|
21
|
-
# oneplus_12, oneplus_11, xiaomi_14, xiaomi_13, oppo_find_x7, realme_gt5
|
|
22
|
-
#
|
|
23
|
-
# Default: iphone_15_pro (or pixel_8_pro when WA_OS=android)
|
|
24
|
-
# WA_DEVICE=pixel_8_pro
|
|
25
|
-
|
|
26
|
-
# ── Custom device overrides (applied on top of the selected profile) ─────────
|
|
27
|
-
# Use these to fine-tune any field without creating a new profile.
|
|
28
|
-
|
|
29
|
-
# WA_DEVICE_MODEL=SM-S928B
|
|
30
|
-
# WA_DEVICE_MANUFACTURER=samsung
|
|
31
|
-
# WA_DEVICE_OS_VERSION=14
|
|
32
|
-
# WA_DEVICE_BUILD=UP1A.231005.007
|
|
33
|
-
# WA_DEVICE_MODEL_ID=samsung-sm-s928b
|
|
34
|
-
|
|
35
|
-
# ── Version & token overrides ─────────────────────────────────────────────────
|
|
36
|
-
|
|
37
|
-
# Pin the WhatsApp version string instead of fetching the latest from the store.
|
|
38
|
-
# Format: 2.x.x.x (four-part)
|
|
39
|
-
# WA_VERSION=2.24.13.80
|
|
40
|
-
|
|
41
|
-
# Override the static token used in registration token computation.
|
|
42
|
-
# Only needed if WhatsApp rotates the bundled token.
|
|
43
|
-
# WA_STATIC_TOKEN=Y29Cs6AVNR2bj5PBeKSYFd1nAKuvNQ3h
|
|
44
|
-
|
|
45
|
-
# ── Proxy / Tor ───────────────────────────────────────────────────────────────
|
|
46
|
-
|
|
47
|
-
# Route registration HTTP traffic through a SOCKS5 proxy or Tor.
|
|
48
|
-
# TOR_PROXY=socks5://127.0.0.1:9050
|
|
49
|
-
# SOCKS_PROXY=socks5://user:pass@proxy.example.com:1080
|