whalibmob 5.1.19 → 5.1.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.
- package/README.md +6 -0
- package/lib/Client.js +11 -50
- package/lib/DeviceManager.js +36 -295
- package/lib/messages/MessageSender.js +54 -83
- package/lib/proto/MessageProto.js +7 -11
- package/lib/signal/SenderKey.js +0 -1
- package/lib/signal/SignalProtocol.js +2 -16
- package/package.json +1 -1
- package/.env.example +0 -44
package/README.md
CHANGED
|
@@ -6,6 +6,12 @@
|
|
|
6
6
|
> [!CAUTION]
|
|
7
7
|
> Use a dedicated phone number with this library. Connecting with a number that is already active on a real device will cause WhatsApp to log that device out.
|
|
8
8
|
|
|
9
|
+
This product is free for everyone but if you want to support more whalibmob or others you can support me with some USD donations through Crypto at This product is free for everyone but if you want us to support more whalibmob or others you can support me with some USD donations through Crypto at 0x8AD64F47a715eC24DeF193FBb9aC64d4E857f0f3
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
9
15
|
> [!IMPORTANT]
|
|
10
16
|
> This project is not affiliated, associated, authorized, endorsed by, or in any way officially connected with WhatsApp or any of its subsidiaries or affiliates. "WhatsApp" and related names are registered trademarks of their respective owners. Use at your own discretion.
|
|
11
17
|
|
package/lib/Client.js
CHANGED
|
@@ -103,27 +103,6 @@ function toSignalKey(raw) {
|
|
|
103
103
|
return Buffer.from(raw);
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
// ─── JID helpers ─────────────────────────────────────────────────────────────
|
|
107
|
-
|
|
108
|
-
// Convert a BinaryNode JID value to its canonical string form.
|
|
109
|
-
// BinaryNode's _readAdJid() returns an object { user, agent, device, server }
|
|
110
|
-
// with a toString() that yields "user@s.whatsapp.net". For LID-mode group
|
|
111
|
-
// participants those objects represent LID identifiers and must be stored as
|
|
112
|
-
// "user@lid" so the rest of the code can detect them with endsWith('@lid').
|
|
113
|
-
// Regular pair-JIDs ({ user, server, toString }) are returned verbatim.
|
|
114
|
-
function _adJidToLid(jidVal) {
|
|
115
|
-
if (!jidVal) return '';
|
|
116
|
-
// AD JID objects (from _readAdJid) have an `agent` property — these are LIDs
|
|
117
|
-
if (typeof jidVal === 'object' && 'agent' in jidVal) {
|
|
118
|
-
return (jidVal.user || '') + '@lid';
|
|
119
|
-
}
|
|
120
|
-
// Pair JID objects (from _readJidPair) have user + server but no agent
|
|
121
|
-
if (typeof jidVal === 'object' && 'server' in jidVal) {
|
|
122
|
-
return jidVal.user ? `${jidVal.user}@${jidVal.server}` : `@${jidVal.server}`;
|
|
123
|
-
}
|
|
124
|
-
return String(jidVal);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
106
|
// ─── WhalibmobClient ──────────────────────────────────────────────────────────
|
|
128
107
|
|
|
129
108
|
class WhalibmobClient extends EventEmitter {
|
|
@@ -149,7 +128,6 @@ class WhalibmobClient extends EventEmitter {
|
|
|
149
128
|
this._lidToPn = new Map(); // LID user → phone number
|
|
150
129
|
this._pnToLid = new Map(); // phone number → LID user
|
|
151
130
|
this._myLid = null; // own LID JID received from <success> node
|
|
152
|
-
this._myLidAgent = 0; // agent field from own LID JID (usually 1)
|
|
153
131
|
this._groupAddressingMode = new Map(); // groupJid → 'lid' | 'pn'
|
|
154
132
|
this._retryPending = new Map(); // msgId → {node, retryCount}
|
|
155
133
|
this._retryPreKeyIdx = 0; // rotating index for assigning unique prekeys to retries
|
|
@@ -306,10 +284,7 @@ class WhalibmobClient extends EventEmitter {
|
|
|
306
284
|
}
|
|
307
285
|
}
|
|
308
286
|
if (attrs.platform) this._platform = attrs.platform;
|
|
309
|
-
if (attrs.lid)
|
|
310
|
-
this._myLid = _adJidToLid(attrs.lid);
|
|
311
|
-
this._myLidAgent = (typeof attrs.lid === 'object' && attrs.lid.agent) ? attrs.lid.agent : 0;
|
|
312
|
-
}
|
|
287
|
+
if (attrs.lid) this._myLid = String(attrs.lid);
|
|
313
288
|
|
|
314
289
|
const devIdNode = findChild(node, 'device-identity');
|
|
315
290
|
if (devIdNode) {
|
|
@@ -822,9 +797,6 @@ class WhalibmobClient extends EventEmitter {
|
|
|
822
797
|
const cls = attrs.class || '';
|
|
823
798
|
|
|
824
799
|
if (cls === 'message') {
|
|
825
|
-
if (attrs.error) {
|
|
826
|
-
process.stderr.write('[DBG] ACK_ERR full node=' + JSON.stringify(node, (k,v) => Buffer.isBuffer(v) ? '<buf>' : v) + '\n');
|
|
827
|
-
}
|
|
828
800
|
const handler = this._pendingAcks.get(id);
|
|
829
801
|
if (handler) {
|
|
830
802
|
this._pendingAcks.delete(id);
|
|
@@ -1212,17 +1184,12 @@ class WhalibmobClient extends EventEmitter {
|
|
|
1212
1184
|
const participants = children
|
|
1213
1185
|
.filter(n => n && n.description === 'participant' && n.attrs && n.attrs.jid)
|
|
1214
1186
|
.map(n => {
|
|
1215
|
-
|
|
1216
|
-
const pJid = _adJidToLid(n.attrs.jid);
|
|
1187
|
+
const pJid = String(n.attrs.jid);
|
|
1217
1188
|
const pRole = n.attrs.type || 'member';
|
|
1218
|
-
|
|
1219
|
-
const
|
|
1220
|
-
const pPhone = rawPhone
|
|
1221
|
-
? (typeof rawPhone === 'object' ? rawPhone.user : String(rawPhone).split('@')[0])
|
|
1222
|
-
: null;
|
|
1223
|
-
const pLid = n.attrs.lid ? _adJidToLid(n.attrs.lid) : null;
|
|
1189
|
+
const pPhone = n.attrs.phone_number ? String(n.attrs.phone_number) : null;
|
|
1190
|
+
const pLid = n.attrs.lid ? String(n.attrs.lid) : null;
|
|
1224
1191
|
// Populate LID↔PN maps from participant attributes
|
|
1225
|
-
// Case A: participant JID is a LID
|
|
1192
|
+
// Case A: participant JID is a LID, phone_number is their PN
|
|
1226
1193
|
if (pJid.endsWith('@lid')) {
|
|
1227
1194
|
const lidUser = pJid.split('@')[0].split(':')[0];
|
|
1228
1195
|
if (pPhone) {
|
|
@@ -1330,18 +1297,12 @@ class WhalibmobClient extends EventEmitter {
|
|
|
1330
1297
|
const response = await this._sendIq(node);
|
|
1331
1298
|
if (!response) return [];
|
|
1332
1299
|
|
|
1333
|
-
// Response
|
|
1334
|
-
|
|
1335
|
-
const
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
groupNodes = groupsWrapper.content.filter(n => n && n.description === 'group');
|
|
1340
|
-
} else {
|
|
1341
|
-
// Fallback: direct <group> children (older protocol variant)
|
|
1342
|
-
groupNodes = iqChildren.filter(n => n && n.description === 'group');
|
|
1343
|
-
}
|
|
1344
|
-
const groups = groupNodes.map(n => this._parseGroupNode(n)).filter(Boolean);
|
|
1300
|
+
// Response content is a list of <group> nodes
|
|
1301
|
+
const children = Array.isArray(response.content) ? response.content : [];
|
|
1302
|
+
const groups = children
|
|
1303
|
+
.filter(n => n && n.description === 'group')
|
|
1304
|
+
.map(n => this._parseGroupNode(n))
|
|
1305
|
+
.filter(Boolean);
|
|
1345
1306
|
return groups;
|
|
1346
1307
|
}
|
|
1347
1308
|
|
package/lib/DeviceManager.js
CHANGED
|
@@ -5,24 +5,17 @@ const { BinaryNode } = require('./BinaryNode');
|
|
|
5
5
|
// ─── JID helpers ─────────────────────────────────────────────────────────────
|
|
6
6
|
|
|
7
7
|
function stripUser(jid) {
|
|
8
|
-
const at
|
|
9
|
-
const
|
|
10
|
-
const colon
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const under = base.lastIndexOf('_');
|
|
14
|
-
if (under >= 0 && /^\d+$/.test(base.slice(under + 1))) {
|
|
15
|
-
return { user: base.slice(0, under), agent: parseInt(base.slice(under + 1), 10), device };
|
|
16
|
-
}
|
|
17
|
-
return { user: base, agent: 0, device };
|
|
8
|
+
const at = jid.indexOf('@');
|
|
9
|
+
const user = at >= 0 ? jid.slice(0, at) : jid;
|
|
10
|
+
const colon = user.indexOf(':');
|
|
11
|
+
if (colon >= 0) return { user: user.slice(0, colon), device: parseInt(user.slice(colon + 1), 10) };
|
|
12
|
+
return { user, device: 0 };
|
|
18
13
|
}
|
|
19
14
|
|
|
20
|
-
function makeDeviceJid(user, device, server
|
|
15
|
+
function makeDeviceJid(user, device, server) {
|
|
21
16
|
server = server || 's.whatsapp.net';
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (!device || device === 0) return `${user}${agentStr}@${server}`;
|
|
25
|
-
return `${user}${agentStr}:${device}@${server}`;
|
|
17
|
+
if (!device || device === 0) return `${user}@${server}`;
|
|
18
|
+
return `${user}:${device}@${server}`;
|
|
26
19
|
}
|
|
27
20
|
|
|
28
21
|
function phoneFromJid(jid) {
|
|
@@ -130,22 +123,7 @@ class DeviceManager {
|
|
|
130
123
|
async fetchBundles(jids) {
|
|
131
124
|
if (!jids || jids.length === 0) return new Map();
|
|
132
125
|
|
|
133
|
-
|
|
134
|
-
// bytes attribute, but the server requires a binary-encoded JID (JID_PAIR or AD_JID).
|
|
135
|
-
// E.g. "112713111982325_1:86@s.whatsapp.net" → { user:"112713111982325", agent:1, device:86, server:"s.whatsapp.net" }
|
|
136
|
-
const userNodes = jids.map(jid => {
|
|
137
|
-
const atIdx = jid.indexOf('@');
|
|
138
|
-
const server = atIdx >= 0 ? jid.slice(atIdx + 1) : 's.whatsapp.net';
|
|
139
|
-
const userPart = atIdx >= 0 ? jid.slice(0, atIdx) : jid;
|
|
140
|
-
const colonIdx = userPart.indexOf(':');
|
|
141
|
-
const base = colonIdx >= 0 ? userPart.slice(0, colonIdx) : userPart;
|
|
142
|
-
const device = colonIdx >= 0 ? parseInt(userPart.slice(colonIdx + 1), 10) : 0;
|
|
143
|
-
const underIdx = base.lastIndexOf('_');
|
|
144
|
-
const hasAgent = underIdx >= 0 && /^\d+$/.test(base.slice(underIdx + 1));
|
|
145
|
-
const user = hasAgent ? base.slice(0, underIdx) : base;
|
|
146
|
-
const agent = hasAgent ? parseInt(base.slice(underIdx + 1), 10) : 0;
|
|
147
|
-
return new BinaryNode('user', { jid: { user, agent: agent || 0, device: device || 0, server } }, null);
|
|
148
|
-
});
|
|
126
|
+
const userNodes = jids.map(jid => { const lidMatch = jid.match(/(\d{15,})@s\.whatsapp\.net/); return new BinaryNode('user', { jid }, null); });
|
|
149
127
|
const iqId = this._client._genMsgId();
|
|
150
128
|
const iqNode = new BinaryNode('iq',
|
|
151
129
|
{ id: iqId, xmlns: 'encrypt', type: 'get', to: 's.whatsapp.net' },
|
|
@@ -160,28 +138,10 @@ class DeviceManager {
|
|
|
160
138
|
const children = listNode ? listNode.content : (response.content || []);
|
|
161
139
|
for (const userNode of (Array.isArray(children) ? children : [])) {
|
|
162
140
|
if (!userNode || userNode.description !== 'user') continue;
|
|
163
|
-
const
|
|
164
|
-
if (!
|
|
165
|
-
// Convert the binary JID object to a canonical string "user_agent:device@server"
|
|
166
|
-
let jidKey;
|
|
167
|
-
if (typeof rawJid === 'string') {
|
|
168
|
-
jidKey = rawJid;
|
|
169
|
-
} else {
|
|
170
|
-
const u = rawJid.user || '';
|
|
171
|
-
const ag = rawJid.agent || 0;
|
|
172
|
-
const dv = rawJid.device || 0;
|
|
173
|
-
const sv = rawJid.server || 's.whatsapp.net';
|
|
174
|
-
const agStr = ag !== 0 ? `_${ag}` : '';
|
|
175
|
-
const dvStr = dv !== 0 ? `:${dv}` : '';
|
|
176
|
-
jidKey = `${u}${agStr}${dvStr}@${sv}`;
|
|
177
|
-
}
|
|
141
|
+
const jid = userNode.attrs && userNode.attrs.jid;
|
|
142
|
+
if (!jid) continue;
|
|
178
143
|
const bundle = parseBundleFromUserNode(userNode);
|
|
179
|
-
if (bundle)
|
|
180
|
-
process.stderr.write('[DBG] BUNDLE jid=' + jidKey + ' regId=' + bundle.registrationId + ' preKeyId=' + (bundle.preKey && bundle.preKey.keyId) + ' spkId=' + (bundle.signedPreKey && bundle.signedPreKey.keyId) + '\n');
|
|
181
|
-
bundles.set(jidKey, bundle);
|
|
182
|
-
} else {
|
|
183
|
-
process.stderr.write('[DBG] BUNDLE_FAIL jid=' + jidKey + ' no bundle parsed\n');
|
|
184
|
-
}
|
|
144
|
+
if (bundle) bundles.set(jid, bundle);
|
|
185
145
|
}
|
|
186
146
|
return bundles;
|
|
187
147
|
}
|
|
@@ -239,17 +199,17 @@ class DeviceManager {
|
|
|
239
199
|
}
|
|
240
200
|
|
|
241
201
|
// Actual usync IQ — only called for phones NOT in _deviceCache
|
|
242
|
-
// Uses the mobile API format (cobalt-compatible):
|
|
243
|
-
// <list> uses <user jid="phone@s.whatsapp.net"/> (not <user><contact>+phone</contact></user>)
|
|
244
|
-
// <query> uses empty <devices version="2"/> (no individual <device> children)
|
|
245
|
-
// Includes <side_list/> sibling required by the mobile API
|
|
246
202
|
async _doUsyncIq(phones) {
|
|
247
203
|
const iqId = this._client._genMsgId();
|
|
248
204
|
const sid = this._client._genMsgId();
|
|
249
205
|
|
|
250
|
-
// Mobile-API format: <user jid="{user}@s.whatsapp.net"/> as JID pair object
|
|
251
206
|
const listChildren = phones.map(p =>
|
|
252
|
-
new BinaryNode('user', {
|
|
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)
|
|
253
213
|
);
|
|
254
214
|
|
|
255
215
|
const iqNode = new BinaryNode('iq',
|
|
@@ -258,77 +218,34 @@ class DeviceManager {
|
|
|
258
218
|
{ sid, mode: 'query', last: 'true', index: '0', context: 'message' },
|
|
259
219
|
[
|
|
260
220
|
new BinaryNode('query', {},
|
|
261
|
-
[new BinaryNode('devices', { version: '2' },
|
|
221
|
+
[new BinaryNode('devices', { version: '2' }, deviceChildren)]
|
|
262
222
|
),
|
|
263
|
-
new BinaryNode('list',
|
|
264
|
-
new BinaryNode('side_list', {}, null)
|
|
223
|
+
new BinaryNode('list', {}, listChildren)
|
|
265
224
|
]
|
|
266
225
|
)]
|
|
267
226
|
);
|
|
268
227
|
|
|
269
228
|
const response = await this._client._sendIq(iqNode).catch(() => null);
|
|
270
|
-
process.stderr.write('[DBG] _doUsyncIq phones=' + phones.join(',') + ' response=' + (response ? response.description : 'null') + '\n');
|
|
271
|
-
if (!response) {
|
|
272
|
-
// Timeout: cache device=0 as fallback
|
|
273
|
-
for (const p of phones) {
|
|
274
|
-
if (!this._deviceCache.has(p)) this._deviceCache.set(p, new Set([0]));
|
|
275
|
-
}
|
|
276
|
-
return;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Cobalt response structure:
|
|
280
|
-
// <iq><usync><list><user jid="..."><devices><device-list><device id="N"/></device-list></devices></user></list></usync></iq>
|
|
281
|
-
this._parseUsyncResponse(response, phones);
|
|
282
|
-
}
|
|
283
229
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
_parseUsyncResponse(response, expectedUsers) {
|
|
287
|
-
const self = this;
|
|
288
|
-
function walkUser(userNode) {
|
|
289
|
-
const rawJid = userNode.attrs && userNode.attrs.jid;
|
|
290
|
-
if (!rawJid) return;
|
|
291
|
-
process.stderr.write('[DBG] _parseUsyncResponse walkUser rawJid=' + JSON.stringify(rawJid) + '\n');
|
|
292
|
-
const user = typeof rawJid === 'object' ? rawJid.user : String(rawJid).split('@')[0];
|
|
293
|
-
if (!user) return;
|
|
294
|
-
|
|
295
|
-
// Look for <devices><device-list><device id="N"/>
|
|
296
|
-
const devicesNode = Array.isArray(userNode.content)
|
|
297
|
-
? userNode.content.find(n => n && n.description === 'devices') : null;
|
|
298
|
-
if (!devicesNode || !Array.isArray(devicesNode.content)) {
|
|
299
|
-
// No devices node: still cache device=0 so we don't query again
|
|
300
|
-
if (!self._deviceCache.has(user)) self._deviceCache.set(user, new Set([0]));
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
const deviceListNode = devicesNode.content.find(n => n && n.description === 'device-list');
|
|
304
|
-
const deviceNodes = (deviceListNode && Array.isArray(deviceListNode.content))
|
|
305
|
-
? deviceListNode.content.filter(n => n && n.description === 'device')
|
|
306
|
-
: [];
|
|
307
|
-
|
|
308
|
-
if (!self._deviceCache.has(user)) self._deviceCache.set(user, new Set());
|
|
309
|
-
const devSet = self._deviceCache.get(user);
|
|
310
|
-
if (deviceNodes.length === 0) {
|
|
311
|
-
devSet.add(0);
|
|
312
|
-
} else {
|
|
313
|
-
for (const dn of deviceNodes) {
|
|
314
|
-
const devId = parseInt((dn.attrs && dn.attrs.id) || '0', 10);
|
|
315
|
-
devSet.add(isNaN(devId) ? 0 : devId);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// Walk: iq → usync → list → user
|
|
230
|
+
// Walk response and populate cache
|
|
231
|
+
const found = new Set();
|
|
321
232
|
function walk(node) {
|
|
322
233
|
if (!node) return;
|
|
323
|
-
if (node.description === '
|
|
324
|
-
|
|
234
|
+
if (node.description === 'device' && node.attrs && node.attrs.jid) {
|
|
235
|
+
const jid = node.attrs.jid;
|
|
236
|
+
const { user, device } = stripUser(jid);
|
|
237
|
+
if (!found.has(user)) found.add(user);
|
|
238
|
+
if (!this._deviceCache.has(user)) this._deviceCache.set(user, new Set());
|
|
239
|
+
this._deviceCache.get(user).add(device);
|
|
240
|
+
}
|
|
241
|
+
if (Array.isArray(node.content)) node.content.forEach(walk.bind(this));
|
|
325
242
|
}
|
|
326
|
-
walk(response);
|
|
243
|
+
if (response) walk.call(this, response);
|
|
327
244
|
|
|
328
|
-
//
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
245
|
+
// For any phone that returned no devices, cache device=0 so we don't query again
|
|
246
|
+
for (const p of phones) {
|
|
247
|
+
if (!this._deviceCache.has(p)) {
|
|
248
|
+
this._deviceCache.set(p, new Set([0]));
|
|
332
249
|
}
|
|
333
250
|
}
|
|
334
251
|
}
|
|
@@ -433,166 +350,6 @@ class DeviceManager {
|
|
|
433
350
|
return `${user}.${device}`;
|
|
434
351
|
}
|
|
435
352
|
|
|
436
|
-
// ─── usync device list query by full JID (LID or PN) ─────────────────────
|
|
437
|
-
//
|
|
438
|
-
// Used for LID-mode group members that have no LID→PN mapping in _lidToPn.
|
|
439
|
-
// Sends <user jid="..."/> nodes directly instead of <user><contact>+phone</contact></user>,
|
|
440
|
-
// which allows the server to resolve devices for LID JIDs natively.
|
|
441
|
-
//
|
|
442
|
-
async _usyncGetDevicesByJid(jids) {
|
|
443
|
-
if (!jids || jids.length === 0) return [];
|
|
444
|
-
|
|
445
|
-
const iqId = this._client._genMsgId();
|
|
446
|
-
const sid = this._client._genMsgId();
|
|
447
|
-
|
|
448
|
-
// Build proper JID objects for each input JID (e.g. "112713111982325@lid")
|
|
449
|
-
const listChildren = jids.map(jidStr => {
|
|
450
|
-
const at = jidStr.indexOf('@');
|
|
451
|
-
const user = at >= 0 ? jidStr.slice(0, at) : jidStr;
|
|
452
|
-
const server = at >= 0 ? jidStr.slice(at + 1) : 's.whatsapp.net';
|
|
453
|
-
return new BinaryNode('user', { jid: { user, server } }, null);
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
const iqNode = new BinaryNode('iq',
|
|
457
|
-
{ id: iqId, to: 's.whatsapp.net', type: 'get', xmlns: 'usync' },
|
|
458
|
-
[new BinaryNode('usync',
|
|
459
|
-
{ sid, mode: 'query', last: 'true', index: '0', context: 'message' },
|
|
460
|
-
[
|
|
461
|
-
new BinaryNode('query', {},
|
|
462
|
-
[new BinaryNode('devices', { version: '2' }, null)]
|
|
463
|
-
),
|
|
464
|
-
new BinaryNode('list', {}, listChildren),
|
|
465
|
-
new BinaryNode('side_list', {}, null)
|
|
466
|
-
]
|
|
467
|
-
)]
|
|
468
|
-
);
|
|
469
|
-
|
|
470
|
-
const response = await this._client._sendIq(iqNode).catch(() => null);
|
|
471
|
-
|
|
472
|
-
if (!response) {
|
|
473
|
-
process.stderr.write('[DBG] _usyncGetDevicesByJid TIMEOUT for jids=' + jids.join(',') + '\n');
|
|
474
|
-
// Fallback: return main device for each input JID
|
|
475
|
-
return jids.map(j => {
|
|
476
|
-
const { user } = stripUser(j);
|
|
477
|
-
const server = j.includes('@lid') ? 'lid' : 's.whatsapp.net';
|
|
478
|
-
return `${user}@${server}`;
|
|
479
|
-
});
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
// Parse response: collect (user, deviceId) pairs → build device JID strings
|
|
483
|
-
// Response structure: <iq><usync><list><user jid="..."><devices><device-list><device id="N"/></device-list></devices></user></list></usync></iq>
|
|
484
|
-
const foundJids = [];
|
|
485
|
-
function walkUser(userNode) {
|
|
486
|
-
const rawJid = userNode.attrs && userNode.attrs.jid;
|
|
487
|
-
if (!rawJid) return;
|
|
488
|
-
process.stderr.write('[DBG] walkUser rawJid=' + JSON.stringify(rawJid) + '\n');
|
|
489
|
-
const user = typeof rawJid === 'object' ? rawJid.user : String(rawJid).split('@')[0];
|
|
490
|
-
const server = typeof rawJid === 'object' ? rawJid.server : (String(rawJid).split('@')[1] || 's.whatsapp.net');
|
|
491
|
-
const agent = typeof rawJid === 'object' ? (rawJid.agent || 0) : 0;
|
|
492
|
-
const agentStr = agent !== 0 ? `_${agent}` : '';
|
|
493
|
-
if (!user) return;
|
|
494
|
-
|
|
495
|
-
const devicesNode = Array.isArray(userNode.content) ? userNode.content.find(n => n && n.description === 'devices') : null;
|
|
496
|
-
const deviceListNode = devicesNode && Array.isArray(devicesNode.content) ? devicesNode.content.find(n => n && n.description === 'device-list') : null;
|
|
497
|
-
const deviceNodes = deviceListNode && Array.isArray(deviceListNode.content) ? deviceListNode.content.filter(n => n && n.description === 'device') : [];
|
|
498
|
-
|
|
499
|
-
if (deviceNodes.length === 0) {
|
|
500
|
-
foundJids.push(`${user}${agentStr}@${server}`);
|
|
501
|
-
} else {
|
|
502
|
-
for (const dn of deviceNodes) {
|
|
503
|
-
const devId = parseInt((dn.attrs && dn.attrs.id) || '0', 10);
|
|
504
|
-
const dev = isNaN(devId) ? 0 : devId;
|
|
505
|
-
foundJids.push(dev === 0 ? `${user}${agentStr}@${server}` : `${user}${agentStr}:${dev}@${server}`);
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
function walk(node) {
|
|
510
|
-
if (!node) return;
|
|
511
|
-
if (node.description === 'user') { walkUser(node); return; }
|
|
512
|
-
if (Array.isArray(node.content)) node.content.forEach(walk);
|
|
513
|
-
}
|
|
514
|
-
walk(response);
|
|
515
|
-
|
|
516
|
-
process.stderr.write('[DBG] _usyncGetDevicesByJid jids=' + jids.length + ' found=' + foundJids.length + ' ' + foundJids.join(',') + '\n');
|
|
517
|
-
|
|
518
|
-
// Fallback: if server returned nothing, use the main device JID for each input
|
|
519
|
-
if (foundJids.length === 0) {
|
|
520
|
-
return jids.map(j => {
|
|
521
|
-
const { user } = stripUser(j);
|
|
522
|
-
const server = j.includes('@lid') ? 'lid' : 's.whatsapp.net';
|
|
523
|
-
return `${user}@${server}`;
|
|
524
|
-
});
|
|
525
|
-
}
|
|
526
|
-
return foundJids;
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
// ─── Ensure sessions for LID JID members (no PN mapping available) ─────────
|
|
530
|
-
//
|
|
531
|
-
// Called from _sendGroupMessage for any LID group member where _lidToPn has
|
|
532
|
-
// no entry. Queries usync by JID instead of phone number so the server can
|
|
533
|
-
// return the correct device list for LID-addressed participants.
|
|
534
|
-
//
|
|
535
|
-
async bulkEnsureSessionsByLidJids(lidJids, signalProto, allowPkmsg = true) {
|
|
536
|
-
if (!lidJids || lidJids.length === 0) return [];
|
|
537
|
-
|
|
538
|
-
const deviceJids = await this._usyncGetDevicesByJid(lidJids);
|
|
539
|
-
|
|
540
|
-
// In LID mode, ALL devices (including device=0) may have pre-keys and need
|
|
541
|
-
// Signal sessions. We try to fetch bundles for every device; if the server
|
|
542
|
-
// returns 406 for device=0 (no pre-keys available) we simply skip that JID —
|
|
543
|
-
// but we still include it in the result so it appears in phash/SKDM list.
|
|
544
|
-
const readyJids = [];
|
|
545
|
-
const fetchJids = [];
|
|
546
|
-
const _sessKeys = Object.keys(signalProto.store._sessions || {});
|
|
547
|
-
process.stderr.write('[DBG] bulkEnsureLid storedSessionKeys=' + JSON.stringify(_sessKeys) + '\n');
|
|
548
|
-
for (const jid of deviceJids) {
|
|
549
|
-
const addrKey = this._jidToAddr(jid);
|
|
550
|
-
const hasSess = signalProto.store.hasSession(addrKey);
|
|
551
|
-
process.stderr.write('[DBG] hasSession jid=' + jid + ' addrKey=' + addrKey + ' result=' + hasSess + '\n');
|
|
552
|
-
if (hasSess) {
|
|
553
|
-
readyJids.push(jid);
|
|
554
|
-
} else {
|
|
555
|
-
fetchJids.push(jid);
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
process.stderr.write('[DBG] bulkEnsureLid deviceJids=' + JSON.stringify(deviceJids) + ' fetchJids=' + JSON.stringify(fetchJids) + '\n');
|
|
560
|
-
|
|
561
|
-
if (allowPkmsg && fetchJids.length > 0) {
|
|
562
|
-
const bundles = await this.fetchBundles(fetchJids);
|
|
563
|
-
process.stderr.write('[DBG] bulkEnsureLid bundles.size=' + bundles.size + '\n');
|
|
564
|
-
for (const [jid, bundle] of bundles) {
|
|
565
|
-
try {
|
|
566
|
-
await signalProto.buildSessionFromBundle(jid, bundle);
|
|
567
|
-
readyJids.push(typeof jid === 'string' ? jid : String(jid));
|
|
568
|
-
} catch (e) {
|
|
569
|
-
process.stderr.write('[DBG] bulkEnsureSessionsByLidJids bundle err jid=' + jid + ' ' + e.message + '\n');
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
// Any device we couldn't build a session for (e.g. 406, no pre-keys) still
|
|
573
|
-
// needs to appear in the result for phash correctness.
|
|
574
|
-
const sessionBuilt = new Set(readyJids);
|
|
575
|
-
for (const jid of fetchJids) {
|
|
576
|
-
if (!sessionBuilt.has(jid)) readyJids.push(jid);
|
|
577
|
-
}
|
|
578
|
-
} else {
|
|
579
|
-
// All devices already have sessions; add any that were not in fetchJids
|
|
580
|
-
for (const jid of deviceJids) {
|
|
581
|
-
if (!readyJids.includes(jid)) readyJids.push(jid);
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
const result = readyJids;
|
|
586
|
-
|
|
587
|
-
// Fallback: if still no secondaries found, return ALL device JIDs so that at
|
|
588
|
-
// least an SKDM attempt is made.
|
|
589
|
-
if (result.length === 0) {
|
|
590
|
-
return deviceJids;
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
return result;
|
|
594
|
-
}
|
|
595
|
-
|
|
596
353
|
// ─── Invalidate cached device list (used on phash mismatch / 421 retry) ───
|
|
597
354
|
clearCache(phones) {
|
|
598
355
|
if (!phones || phones.length === 0) {
|
|
@@ -608,23 +365,7 @@ class DeviceManager {
|
|
|
608
365
|
const toNodes = encryptedList.map(({ jid, type, ciphertext }) => {
|
|
609
366
|
const encAttrs = { type, v: '2' };
|
|
610
367
|
if (mediaSubtype) encAttrs.mediatype = mediaSubtype;
|
|
611
|
-
|
|
612
|
-
// "user_agent:device@server" → { user, agent, device, server }
|
|
613
|
-
let jidObj = jid;
|
|
614
|
-
if (typeof jid === 'string') {
|
|
615
|
-
const atIdx = jid.indexOf('@');
|
|
616
|
-
const server = atIdx >= 0 ? jid.slice(atIdx + 1) : 's.whatsapp.net';
|
|
617
|
-
const userPart = atIdx >= 0 ? jid.slice(0, atIdx) : jid;
|
|
618
|
-
const colonIdx = userPart.indexOf(':');
|
|
619
|
-
const base = colonIdx >= 0 ? userPart.slice(0, colonIdx) : userPart;
|
|
620
|
-
const device = colonIdx >= 0 ? parseInt(userPart.slice(colonIdx + 1), 10) : 0;
|
|
621
|
-
const underIdx = base.lastIndexOf('_');
|
|
622
|
-
const hasAgent = underIdx >= 0 && /^\d+$/.test(base.slice(underIdx + 1));
|
|
623
|
-
const user = hasAgent ? base.slice(0, underIdx) : base;
|
|
624
|
-
const agent = hasAgent ? parseInt(base.slice(underIdx + 1), 10) : 0;
|
|
625
|
-
jidObj = { user, agent: agent || 0, device: device || 0, server };
|
|
626
|
-
}
|
|
627
|
-
return new BinaryNode('to', { jid: jidObj },
|
|
368
|
+
return new BinaryNode('to', { jid },
|
|
628
369
|
[new BinaryNode('enc', encAttrs, ciphertext)]
|
|
629
370
|
);
|
|
630
371
|
});
|
|
@@ -102,22 +102,37 @@ function _getCurve() {
|
|
|
102
102
|
return _curve25519js;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
// null — the pkmsg stanza is sent WITHOUT a <device-identity> child, which is valid
|
|
109
|
-
// for primary mobile devices and does NOT cause RETRY at the recipient.
|
|
110
|
-
//
|
|
111
|
-
// NOTE: Building a self-signed ADV from our own keys caused RETRY at recipient because
|
|
112
|
-
// WhatsApp verifies the signature chain against the server-known account key.
|
|
105
|
+
// Build ADVSignedDeviceIdentity for our primary iOS device.
|
|
106
|
+
// Returns Buffer of encoded proto WITH accountSignatureKey included.
|
|
107
|
+
// Result is cached in store.advIdentity so we only build it once per registration.
|
|
113
108
|
function _buildOrGetAdvIdentity(store) {
|
|
114
|
-
if (
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
109
|
+
if (store.advIdentity && store.advIdentity.length > 0) return store.advIdentity;
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const curve = _getCurve();
|
|
113
|
+
const pubRaw = Buffer.from(store.identityKeyPair.public, 'base64');
|
|
114
|
+
const privRaw = Buffer.from(store.identityKeyPair.private, 'base64');
|
|
115
|
+
const pub32 = pubRaw.length === 33 ? pubRaw.slice(1) : pubRaw;
|
|
116
|
+
const priv32 = privRaw.length === 32 ? privRaw : privRaw.slice(0, 32);
|
|
117
|
+
|
|
118
|
+
const ts = Math.floor(Date.now() / 1000);
|
|
119
|
+
const regId = store.registrationId || 0;
|
|
120
|
+
const details = _encodeADVDetails(regId, ts, 0);
|
|
121
|
+
|
|
122
|
+
const acctMsg = Buffer.concat([WA_ADV_ACCOUNT_SIG_PREFIX, details, pub32]);
|
|
123
|
+
const acctSig = Buffer.from(curve.sign(priv32, acctMsg));
|
|
124
|
+
|
|
125
|
+
const devMsg = Buffer.concat([WA_ADV_DEVICE_SIG_PREFIX, details, pub32, pub32]);
|
|
126
|
+
const devSig = Buffer.from(curve.sign(priv32, devMsg));
|
|
127
|
+
|
|
128
|
+
const adv = _encodeADVSignedIdentity(details, pub32, acctSig, devSig);
|
|
129
|
+
store.advIdentity = adv;
|
|
130
|
+
process.stderr.write('[DBG] ADV_BUILT from primary iOS keys (' + adv.length + 'b)\n');
|
|
131
|
+
return adv;
|
|
132
|
+
} catch (e) {
|
|
133
|
+
process.stderr.write('[DBG] ADV_BUILD_ERR: ' + e.message + '\n');
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
121
136
|
}
|
|
122
137
|
|
|
123
138
|
function makeJid(input, server) {
|
|
@@ -606,10 +621,6 @@ class MessageSender {
|
|
|
606
621
|
const ownPhone = String(this._store.phoneNumber);
|
|
607
622
|
const ownJid = `${ownPhone}@s.whatsapp.net`;
|
|
608
623
|
const mediaSubtype = options._mediaSubtype || null;
|
|
609
|
-
// LID-based own user number (e.g. "90079053799508"), used for phash in LID-mode groups.
|
|
610
|
-
// usync returns "@s.whatsapp.net" JIDs with the LID numeric ID as user when queried by
|
|
611
|
-
// LID JID, so the phash sender entry must use this number, NOT the phone number.
|
|
612
|
-
const myLidUser = this._client._myLid ? phoneFromJid(this._client._myLid) : null;
|
|
613
624
|
|
|
614
625
|
// Auto-fetch group metadata if member cache is empty (first send to this group)
|
|
615
626
|
let members = this._client._getGroupMembers(groupJid);
|
|
@@ -623,12 +634,7 @@ class MessageSender {
|
|
|
623
634
|
|
|
624
635
|
// Determine addressing mode for this group (lid = modern groups, pn = legacy)
|
|
625
636
|
const groupAddressingMode = this._client._groupAddressingMode.get(groupJid) || 'lid';
|
|
626
|
-
//
|
|
627
|
-
// participant = our LID JID (e.g. "90079053799508@lid") because addressing_mode='lid'.
|
|
628
|
-
// The recipient looks up the sender key by that participant JID, so we MUST store
|
|
629
|
-
// the SKDM and encrypt under the SAME identity.
|
|
630
|
-
// For legacy PN-mode groups the server uses the phone JID — fall back to ownJid.
|
|
631
|
-
// This mirrors Baileys: groupSenderIdentity = groupAddressingMode==='lid' && meLid ? meLid : meId
|
|
637
|
+
// For LID-mode groups, sender identity is own LID JID; otherwise own PN JID
|
|
632
638
|
const senderIdentity = (groupAddressingMode === 'lid' && this._client._myLid)
|
|
633
639
|
? this._client._myLid
|
|
634
640
|
: ownJid;
|
|
@@ -636,70 +642,38 @@ class MessageSender {
|
|
|
636
642
|
const rawSKDM = this._signal.buildSKDM(groupJid, senderIdentity);
|
|
637
643
|
const skdmMsg = encodeSenderKeyDistributionMessage(groupJid, rawSKDM);
|
|
638
644
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
const isLid = jid.endsWith('@lid');
|
|
655
|
-
const raw = phoneFromJid(jid);
|
|
656
|
-
|
|
657
|
-
if (isLid) {
|
|
658
|
-
// Skip own LID identity
|
|
659
|
-
if (raw === myLidUser) continue;
|
|
660
|
-
// KEY FIX: For LID-mode groups the server only responds to LID-based usync queries
|
|
661
|
-
// (i.e. <user jid="...@lid"/>), NOT to phone-number-based queries
|
|
662
|
-
// (<contact>+phone</contact>). Always route ALL LID members through
|
|
663
|
-
// bulkEnsureSessionsByLidJids() regardless of whether we know their PN.
|
|
664
|
-
unknownLids.push(jid);
|
|
665
|
-
} else if (raw !== ownPhone) {
|
|
666
|
-
// Legacy PN-mode group — use conventional phone-number usync
|
|
667
|
-
knownPhones.push(raw);
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
const uniquePhones = [...new Set(knownPhones)];
|
|
672
|
-
const uniqueLids = [...new Set(unknownLids)];
|
|
673
|
-
|
|
674
|
-
process.stderr.write('[DBG] GROUP_SEND ' + groupJid + ' knownPhones=' + uniquePhones.length + ' unknownLids=' + uniqueLids.length + '\n');
|
|
645
|
+
const memberPhones = [...new Set(
|
|
646
|
+
members.map(jid => {
|
|
647
|
+
const isLid = jid.endsWith('@lid');
|
|
648
|
+
const raw = phoneFromJid(jid);
|
|
649
|
+
if (isLid) {
|
|
650
|
+
const pn = this._client._lidToPn && this._client._lidToPn.get(raw);
|
|
651
|
+
if (!pn) {
|
|
652
|
+
process.stderr.write('[DBG] GROUP_SEND skip LID member with no PN mapping: ' + jid + '\n');
|
|
653
|
+
return null;
|
|
654
|
+
}
|
|
655
|
+
return pn;
|
|
656
|
+
}
|
|
657
|
+
return raw;
|
|
658
|
+
}).filter(p => p !== null && p !== ownPhone)
|
|
659
|
+
)];
|
|
675
660
|
|
|
676
|
-
const [
|
|
677
|
-
|
|
678
|
-
? this._devMgr.bulkEnsureSessions(
|
|
679
|
-
: Promise.resolve([]),
|
|
680
|
-
uniqueLids.length > 0
|
|
681
|
-
? this._devMgr.bulkEnsureSessionsByLidJids(uniqueLids, this._signal)
|
|
661
|
+
const [memberDevices, ownDevices] = await Promise.all([
|
|
662
|
+
memberPhones.length > 0
|
|
663
|
+
? this._devMgr.bulkEnsureSessions(memberPhones, this._signal)
|
|
682
664
|
: Promise.resolve([]),
|
|
683
665
|
this._devMgr.ensureOwnDeviceSessions(ownPhone, this._signal)
|
|
684
666
|
]);
|
|
685
667
|
|
|
686
|
-
const allTargets = [...
|
|
687
|
-
|
|
688
|
-
process.stderr.write('[DBG] GROUP senderIdentity=' + senderIdentity + ' groupAddressingMode=' + groupAddressingMode + ' myLid=' + this._client._myLid + '\n');
|
|
668
|
+
const allTargets = [...memberDevices, ...ownDevices];
|
|
689
669
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
const phashOwnJid = ownJid;
|
|
694
|
-
// memberTargets = only other participants' devices (PN + LID), no own secondary devices
|
|
695
|
-
const memberTargets = [...memberDevicesPN, ...memberDevicesLid];
|
|
696
|
-
const phashTargets = [phashOwnJid, ...memberTargets.filter(j => j !== phashOwnJid)];
|
|
697
|
-
process.stderr.write('[DBG] phashOwnJid=' + phashOwnJid + ' memberTargets=' + JSON.stringify(memberTargets) + ' ownDevices=' + JSON.stringify(ownDevices) + '\n');
|
|
670
|
+
const phashTargets = allTargets.includes(ownJid)
|
|
671
|
+
? allTargets
|
|
672
|
+
: [ownJid, ...allTargets];
|
|
698
673
|
|
|
699
674
|
const skStore = this._signal.senderKeyStore;
|
|
700
675
|
const existingSkdmMap = skStore.getSKDMMap(groupJid);
|
|
701
676
|
const skdmRecipients = allTargets.filter(jid => !existingSkdmMap[jid]);
|
|
702
|
-
process.stderr.write('[DBG] allTargets=' + JSON.stringify(allTargets) + ' skdmRecipients=' + JSON.stringify(skdmRecipients) + ' existingSkdmKeys=' + JSON.stringify(Object.keys(existingSkdmMap)) + '\n');
|
|
703
677
|
|
|
704
678
|
let skdmEncrypted = [];
|
|
705
679
|
if (skdmRecipients.length > 0) {
|
|
@@ -708,13 +682,10 @@ class MessageSender {
|
|
|
708
682
|
}
|
|
709
683
|
|
|
710
684
|
const skmsgCiphertext = this._signal.senderKeyEncrypt(groupJid, senderIdentity, plaintext);
|
|
711
|
-
const
|
|
712
|
-
const phash = null; // TEST: force no phash to see server response
|
|
713
|
-
process.stderr.write('[DBG] phashTargets=' + JSON.stringify(phashTargets) + ' computed phash=' + phashComputed + ' (sending: ' + phash + ')\n');
|
|
685
|
+
const phash = phashTargets.length > 0 ? computePhash(phashTargets) : null;
|
|
714
686
|
|
|
715
687
|
const skdmHasPkmsg = skdmEncrypted.some(e => e.type === 'pkmsg');
|
|
716
688
|
const advBytes = skdmHasPkmsg ? _buildOrGetAdvIdentity(this._client._store) : null;
|
|
717
|
-
process.stderr.write('[DBG] skdmHasPkmsg=' + skdmHasPkmsg + ' advBytes=' + (advBytes ? advBytes.length + 'b' : 'null') + '\n');
|
|
718
689
|
|
|
719
690
|
const msgContent = [];
|
|
720
691
|
if (advBytes) {
|
|
@@ -380,14 +380,12 @@ function decodeMessageContainer(buf) {
|
|
|
380
380
|
if (dsm[2]) return decodeMessageContainer(dsm[2]);
|
|
381
381
|
}
|
|
382
382
|
|
|
383
|
-
// Field
|
|
383
|
+
// Field 35: senderKeyDistributionMessage — used to set up sender keys.
|
|
384
384
|
// In WhatsApp Multi-Device, SKDM is bundled WITH the first real message in a new
|
|
385
385
|
// session (even for 1-on-1 DMs). We extract it and continue decoding the real message.
|
|
386
|
-
// Also check field 15 (fastRatchetKeySenderKeyDistributionMessage) as a fallback.
|
|
387
386
|
let skdmInfo = null;
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
const skdm = _decodeFields(skdmRaw);
|
|
387
|
+
if (f[35]) {
|
|
388
|
+
const skdm = _decodeFields(f[35]);
|
|
391
389
|
// field 1 = groupId (string), field 2 = axolotlSenderKeyDistributionMessage (bytes)
|
|
392
390
|
skdmInfo = {
|
|
393
391
|
groupId: _str(skdm[1]),
|
|
@@ -405,10 +403,10 @@ function decodeMessageContainer(buf) {
|
|
|
405
403
|
if (text) msgResult = { type: 'text', text };
|
|
406
404
|
}
|
|
407
405
|
|
|
408
|
-
// Field
|
|
409
|
-
if (!msgResult && f[
|
|
406
|
+
// Field 2: extendedTextMessage — field 1 = text
|
|
407
|
+
if (!msgResult && f[2] && Buffer.isBuffer(f[2])) {
|
|
410
408
|
try {
|
|
411
|
-
const ext = _decodeFields(f[
|
|
409
|
+
const ext = _decodeFields(f[2]);
|
|
412
410
|
const text = _str(ext[1]);
|
|
413
411
|
if (text) msgResult = { type: 'text', text };
|
|
414
412
|
} catch (_) {}
|
|
@@ -572,13 +570,11 @@ function decodeMessageContainer(buf) {
|
|
|
572
570
|
}
|
|
573
571
|
|
|
574
572
|
function encodeSenderKeyDistributionMessage(groupId, axolotlBytes) {
|
|
575
|
-
// WAProto: Message.senderKeyDistributionMessage = field 2
|
|
576
|
-
// Inner: SenderKeyDistributionMessage { groupId=1 (string), axolotlSKDM=2 (bytes) }
|
|
577
573
|
const inner = Buffer.concat([
|
|
578
574
|
field(1, WIRE_LEN, str(groupId)),
|
|
579
575
|
field(2, WIRE_LEN, bytes(axolotlBytes))
|
|
580
576
|
]);
|
|
581
|
-
return field(
|
|
577
|
+
return field(35, WIRE_LEN, inner);
|
|
582
578
|
}
|
|
583
579
|
|
|
584
580
|
module.exports = {
|
package/lib/signal/SenderKey.js
CHANGED
|
@@ -178,7 +178,6 @@ class SenderKeyStore {
|
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
markSKDMSent(groupId, jids) {
|
|
181
|
-
process.stderr.write('[DBG] markSKDMSent groupId=' + groupId + ' jids=' + JSON.stringify(jids) + '\n' + new Error().stack.split('\n').slice(1,4).join('\n') + '\n');
|
|
182
181
|
const map = this.getSKDMMap(groupId);
|
|
183
182
|
for (const jid of jids) map[jid] = true;
|
|
184
183
|
this.setSKDMMap(groupId, map);
|
|
@@ -170,8 +170,6 @@ class SignalProtocol {
|
|
|
170
170
|
publicKey: bundle.preKey.publicKey
|
|
171
171
|
} : undefined
|
|
172
172
|
});
|
|
173
|
-
const hasSess = this.store.hasSession(addr.toString ? addr.toString() : (addr.name + '.' + addr.deviceId));
|
|
174
|
-
process.stderr.write('[DBG] SESSION_BUILT jid=' + jid + ' addr=' + addr.name + '.' + addr.deviceId + ' hasSess=' + hasSess + '\n');
|
|
175
173
|
}
|
|
176
174
|
|
|
177
175
|
async buildSessionsFromBundles(bundleMap) {
|
|
@@ -222,18 +220,11 @@ class SignalProtocol {
|
|
|
222
220
|
// Returns [{jid, type, ciphertext}]
|
|
223
221
|
async bulkEncryptForDevices(jids, plaintext) {
|
|
224
222
|
const settled = await Promise.allSettled(
|
|
225
|
-
jids.map(jid => this.encrypt(jid, plaintext).then(enc => {
|
|
226
|
-
process.stderr.write('[DBG] SKDM_ENC jid=' + jid + ' type=' + enc.type + ' len=' + (enc.ciphertext ? enc.ciphertext.length : 0) + '\n');
|
|
227
|
-
return { jid, type: enc.type, ciphertext: enc.ciphertext };
|
|
228
|
-
}))
|
|
223
|
+
jids.map(jid => this.encrypt(jid, plaintext).then(enc => ({ jid, type: enc.type, ciphertext: enc.ciphertext })))
|
|
229
224
|
);
|
|
230
|
-
|
|
225
|
+
return settled
|
|
231
226
|
.filter(r => r.status === 'fulfilled')
|
|
232
227
|
.map(r => r.value);
|
|
233
|
-
settled.filter(r => r.status === 'rejected').forEach((r, i) => {
|
|
234
|
-
process.stderr.write('[DBG] SKDM_ENC_ERR jid=' + jids[i] + ' err=' + r.reason + '\n');
|
|
235
|
-
});
|
|
236
|
-
return results;
|
|
237
228
|
}
|
|
238
229
|
|
|
239
230
|
// ─── SenderKey group encrypt / decrypt ────────────────────────────────────
|
|
@@ -302,11 +293,6 @@ function jidToAddress(jid) {
|
|
|
302
293
|
deviceId = parseInt(user.slice(colonIdx + 1), 10) || 0;
|
|
303
294
|
user = user.slice(0, colonIdx);
|
|
304
295
|
}
|
|
305
|
-
// Strip agent suffix from user (e.g. "112713111982325_1" → "112713111982325")
|
|
306
|
-
const underIdx = user.lastIndexOf('_');
|
|
307
|
-
if (underIdx >= 0 && /^\d+$/.test(user.slice(underIdx + 1))) {
|
|
308
|
-
user = user.slice(0, underIdx);
|
|
309
|
-
}
|
|
310
296
|
} else if (jid && jid.user) {
|
|
311
297
|
user = jid.user;
|
|
312
298
|
deviceId = jid.device || 0;
|
package/package.json
CHANGED
package/.env.example
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
# ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
-
# whalibmob — iPhone Device Emulation Configuration
|
|
3
|
-
# Copy this file to .env in your project root and set the values you need.
|
|
4
|
-
# All variables are optional; the library picks a random iPhone by default.
|
|
5
|
-
# ─────────────────────────────────────────────────────────────────────────────
|
|
6
|
-
|
|
7
|
-
# Named iPhone profile to emulate.
|
|
8
|
-
# If not set, the library picks a random iPhone from the list below.
|
|
9
|
-
#
|
|
10
|
-
# Available profiles:
|
|
11
|
-
# iphone16promax, iphone16pro, iphone16plus, iphone16,
|
|
12
|
-
# iphone15promax, iphone15pro, iphone15plus, iphone15,
|
|
13
|
-
# iphone14promax, iphone14pro, iphone14plus, iphone14,
|
|
14
|
-
# iphone13pro, iphone13, iphone12pro, iphone12,
|
|
15
|
-
# iphone11pro, iphone11, iphonese3, iphonexs
|
|
16
|
-
#
|
|
17
|
-
# WA_DEVICE=iphone16pro
|
|
18
|
-
|
|
19
|
-
# ── Custom iPhone overrides ──────────────────────────────────────────────────
|
|
20
|
-
# Use these to emulate a specific iPhone model not in the list above.
|
|
21
|
-
# All values are sent as-is in the WhatsApp registration and connection.
|
|
22
|
-
|
|
23
|
-
# WA_DEVICE_MODEL=iPhone 16 Pro Max
|
|
24
|
-
# WA_DEVICE_OS_VERSION=18.3.2
|
|
25
|
-
# WA_DEVICE_BUILD=22D82
|
|
26
|
-
# WA_DEVICE_MODEL_ID=iPhone17,2
|
|
27
|
-
|
|
28
|
-
# ── Version & token overrides ────────────────────────────────────────────────
|
|
29
|
-
|
|
30
|
-
# Pin the WhatsApp version string instead of fetching the latest from the
|
|
31
|
-
# App Store (iTunes lookup). Format: 2.x.x.x (four-part).
|
|
32
|
-
# WA_VERSION=2.26.10.74
|
|
33
|
-
|
|
34
|
-
# Override the built-in iOS static secret used in the token MD5 formula:
|
|
35
|
-
# token = MD5( WA_STATIC_TOKEN + MD5hex(version) + nationalNumber )
|
|
36
|
-
# Only needed if WhatsApp rotates the built-in secret.
|
|
37
|
-
# WA_STATIC_TOKEN=0a1mLfGUIBVrMKF1RdvLI5lkRBvof6vn0fD2QRSM
|
|
38
|
-
|
|
39
|
-
# ── Proxy / Tor ──────────────────────────────────────────────────────────────
|
|
40
|
-
|
|
41
|
-
# Route registration HTTP traffic through a SOCKS5 proxy or Tor.
|
|
42
|
-
# Residential proxies recommended to avoid WhatsApp security blocks.
|
|
43
|
-
# TOR_PROXY=socks5://127.0.0.1:9050
|
|
44
|
-
# SOCKS_PROXY=socks5://user:pass@proxy.example.com:1080
|