whalibmob 5.1.17 → 5.1.19
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 +50 -11
- package/lib/DeviceManager.js +295 -36
- package/lib/messages/MessageSender.js +83 -54
- package/lib/proto/MessageProto.js +11 -7
- package/lib/signal/SenderKey.js +1 -0
- package/lib/signal/SignalProtocol.js +16 -2
- package/package.json +1 -1
package/lib/Client.js
CHANGED
|
@@ -103,6 +103,27 @@ 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
|
+
|
|
106
127
|
// ─── WhalibmobClient ──────────────────────────────────────────────────────────
|
|
107
128
|
|
|
108
129
|
class WhalibmobClient extends EventEmitter {
|
|
@@ -128,6 +149,7 @@ class WhalibmobClient extends EventEmitter {
|
|
|
128
149
|
this._lidToPn = new Map(); // LID user → phone number
|
|
129
150
|
this._pnToLid = new Map(); // phone number → LID user
|
|
130
151
|
this._myLid = null; // own LID JID received from <success> node
|
|
152
|
+
this._myLidAgent = 0; // agent field from own LID JID (usually 1)
|
|
131
153
|
this._groupAddressingMode = new Map(); // groupJid → 'lid' | 'pn'
|
|
132
154
|
this._retryPending = new Map(); // msgId → {node, retryCount}
|
|
133
155
|
this._retryPreKeyIdx = 0; // rotating index for assigning unique prekeys to retries
|
|
@@ -284,7 +306,10 @@ class WhalibmobClient extends EventEmitter {
|
|
|
284
306
|
}
|
|
285
307
|
}
|
|
286
308
|
if (attrs.platform) this._platform = attrs.platform;
|
|
287
|
-
if (attrs.lid)
|
|
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
|
+
}
|
|
288
313
|
|
|
289
314
|
const devIdNode = findChild(node, 'device-identity');
|
|
290
315
|
if (devIdNode) {
|
|
@@ -797,6 +822,9 @@ class WhalibmobClient extends EventEmitter {
|
|
|
797
822
|
const cls = attrs.class || '';
|
|
798
823
|
|
|
799
824
|
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
|
+
}
|
|
800
828
|
const handler = this._pendingAcks.get(id);
|
|
801
829
|
if (handler) {
|
|
802
830
|
this._pendingAcks.delete(id);
|
|
@@ -1184,12 +1212,17 @@ class WhalibmobClient extends EventEmitter {
|
|
|
1184
1212
|
const participants = children
|
|
1185
1213
|
.filter(n => n && n.description === 'participant' && n.attrs && n.attrs.jid)
|
|
1186
1214
|
.map(n => {
|
|
1187
|
-
|
|
1215
|
+
// AD JID objects (LID participants) have an `agent` property → convert to @lid form
|
|
1216
|
+
const pJid = _adJidToLid(n.attrs.jid);
|
|
1188
1217
|
const pRole = n.attrs.type || 'member';
|
|
1189
|
-
|
|
1190
|
-
const
|
|
1218
|
+
// phone_number may also be a JID object → extract user part (PN without server)
|
|
1219
|
+
const rawPhone = n.attrs.phone_number;
|
|
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;
|
|
1191
1224
|
// Populate LID↔PN maps from participant attributes
|
|
1192
|
-
// Case A: participant JID is a LID, phone_number is their PN
|
|
1225
|
+
// Case A: participant JID is a LID (ends with @lid), phone_number is their PN
|
|
1193
1226
|
if (pJid.endsWith('@lid')) {
|
|
1194
1227
|
const lidUser = pJid.split('@')[0].split(':')[0];
|
|
1195
1228
|
if (pPhone) {
|
|
@@ -1297,12 +1330,18 @@ class WhalibmobClient extends EventEmitter {
|
|
|
1297
1330
|
const response = await this._sendIq(node);
|
|
1298
1331
|
if (!response) return [];
|
|
1299
1332
|
|
|
1300
|
-
// Response
|
|
1301
|
-
|
|
1302
|
-
const
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1333
|
+
// Response structure: <iq><groups><group/><group/>...</groups></iq>
|
|
1334
|
+
// Unwrap the <groups> container first, then collect <group> nodes
|
|
1335
|
+
const iqChildren = Array.isArray(response.content) ? response.content : [];
|
|
1336
|
+
let groupNodes = [];
|
|
1337
|
+
const groupsWrapper = iqChildren.find(n => n && n.description === 'groups');
|
|
1338
|
+
if (groupsWrapper && Array.isArray(groupsWrapper.content)) {
|
|
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);
|
|
1306
1345
|
return groups;
|
|
1307
1346
|
}
|
|
1308
1347
|
|
package/lib/DeviceManager.js
CHANGED
|
@@ -5,17 +5,24 @@ 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
|
-
|
|
8
|
+
const at = jid.indexOf('@');
|
|
9
|
+
const full = at >= 0 ? jid.slice(0, at) : jid;
|
|
10
|
+
const colon = full.indexOf(':');
|
|
11
|
+
const base = colon >= 0 ? full.slice(0, colon) : full;
|
|
12
|
+
const device = colon >= 0 ? parseInt(full.slice(colon + 1), 10) : 0;
|
|
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 };
|
|
13
18
|
}
|
|
14
19
|
|
|
15
|
-
function makeDeviceJid(user, device, server) {
|
|
20
|
+
function makeDeviceJid(user, device, server, agent) {
|
|
16
21
|
server = server || 's.whatsapp.net';
|
|
17
|
-
|
|
18
|
-
|
|
22
|
+
agent = agent || 0;
|
|
23
|
+
const agentStr = agent !== 0 ? `_${agent}` : '';
|
|
24
|
+
if (!device || device === 0) return `${user}${agentStr}@${server}`;
|
|
25
|
+
return `${user}${agentStr}:${device}@${server}`;
|
|
19
26
|
}
|
|
20
27
|
|
|
21
28
|
function phoneFromJid(jid) {
|
|
@@ -123,7 +130,22 @@ class DeviceManager {
|
|
|
123
130
|
async fetchBundles(jids) {
|
|
124
131
|
if (!jids || jids.length === 0) return new Map();
|
|
125
132
|
|
|
126
|
-
|
|
133
|
+
// Build proper JID objects for binary encoding. Passing a plain string produces a raw
|
|
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
|
+
});
|
|
127
149
|
const iqId = this._client._genMsgId();
|
|
128
150
|
const iqNode = new BinaryNode('iq',
|
|
129
151
|
{ id: iqId, xmlns: 'encrypt', type: 'get', to: 's.whatsapp.net' },
|
|
@@ -138,10 +160,28 @@ class DeviceManager {
|
|
|
138
160
|
const children = listNode ? listNode.content : (response.content || []);
|
|
139
161
|
for (const userNode of (Array.isArray(children) ? children : [])) {
|
|
140
162
|
if (!userNode || userNode.description !== 'user') continue;
|
|
141
|
-
const
|
|
142
|
-
if (!
|
|
163
|
+
const rawJid = userNode.attrs && userNode.attrs.jid;
|
|
164
|
+
if (!rawJid) continue;
|
|
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
|
+
}
|
|
143
178
|
const bundle = parseBundleFromUserNode(userNode);
|
|
144
|
-
if (bundle)
|
|
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
|
+
}
|
|
145
185
|
}
|
|
146
186
|
return bundles;
|
|
147
187
|
}
|
|
@@ -199,17 +239,17 @@ class DeviceManager {
|
|
|
199
239
|
}
|
|
200
240
|
|
|
201
241
|
// 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
|
|
202
246
|
async _doUsyncIq(phones) {
|
|
203
247
|
const iqId = this._client._genMsgId();
|
|
204
248
|
const sid = this._client._genMsgId();
|
|
205
249
|
|
|
250
|
+
// Mobile-API format: <user jid="{user}@s.whatsapp.net"/> as JID pair object
|
|
206
251
|
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)
|
|
252
|
+
new BinaryNode('user', { jid: { user: String(p), server: 's.whatsapp.net' } }, null)
|
|
213
253
|
);
|
|
214
254
|
|
|
215
255
|
const iqNode = new BinaryNode('iq',
|
|
@@ -218,34 +258,77 @@ class DeviceManager {
|
|
|
218
258
|
{ sid, mode: 'query', last: 'true', index: '0', context: 'message' },
|
|
219
259
|
[
|
|
220
260
|
new BinaryNode('query', {},
|
|
221
|
-
[new BinaryNode('devices', { version: '2' },
|
|
261
|
+
[new BinaryNode('devices', { version: '2' }, null)]
|
|
222
262
|
),
|
|
223
|
-
new BinaryNode('list',
|
|
263
|
+
new BinaryNode('list', {}, listChildren),
|
|
264
|
+
new BinaryNode('side_list', {}, null)
|
|
224
265
|
]
|
|
225
266
|
)]
|
|
226
267
|
);
|
|
227
268
|
|
|
228
269
|
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
|
+
}
|
|
229
283
|
|
|
230
|
-
|
|
231
|
-
|
|
284
|
+
// Parse a usync IQ response (works for both PN and LID usync)
|
|
285
|
+
// Populates _deviceCache: phone/user → Set<deviceId>
|
|
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
|
|
232
321
|
function walk(node) {
|
|
233
322
|
if (!node) return;
|
|
234
|
-
if (node.description === '
|
|
235
|
-
|
|
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));
|
|
323
|
+
if (node.description === 'user') { walkUser(node); return; }
|
|
324
|
+
if (Array.isArray(node.content)) node.content.forEach(walk);
|
|
242
325
|
}
|
|
243
|
-
|
|
326
|
+
walk(response);
|
|
244
327
|
|
|
245
|
-
//
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
328
|
+
// Ensure any expected user not seen in response gets device=0 cached
|
|
329
|
+
if (expectedUsers) {
|
|
330
|
+
for (const u of expectedUsers) {
|
|
331
|
+
if (!self._deviceCache.has(u)) self._deviceCache.set(u, new Set([0]));
|
|
249
332
|
}
|
|
250
333
|
}
|
|
251
334
|
}
|
|
@@ -350,6 +433,166 @@ class DeviceManager {
|
|
|
350
433
|
return `${user}.${device}`;
|
|
351
434
|
}
|
|
352
435
|
|
|
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
|
+
|
|
353
596
|
// ─── Invalidate cached device list (used on phash mismatch / 421 retry) ───
|
|
354
597
|
clearCache(phones) {
|
|
355
598
|
if (!phones || phones.length === 0) {
|
|
@@ -365,7 +608,23 @@ class DeviceManager {
|
|
|
365
608
|
const toNodes = encryptedList.map(({ jid, type, ciphertext }) => {
|
|
366
609
|
const encAttrs = { type, v: '2' };
|
|
367
610
|
if (mediaSubtype) encAttrs.mediatype = mediaSubtype;
|
|
368
|
-
|
|
611
|
+
// Convert string JIDs to binary-encodable JID objects.
|
|
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 },
|
|
369
628
|
[new BinaryNode('enc', encAttrs, ciphertext)]
|
|
370
629
|
);
|
|
371
630
|
});
|
|
@@ -102,37 +102,22 @@ function _getCurve() {
|
|
|
102
102
|
return _curve25519js;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
105
|
+
// Return ADVSignedDeviceIdentity if we have one from the server (received at login).
|
|
106
|
+
// For phone-based (non-WA-Web) registrations the server never sends device-identity
|
|
107
|
+
// in the <success> node, so store.advIdentity stays null. In that case we return
|
|
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.
|
|
108
113
|
function _buildOrGetAdvIdentity(store) {
|
|
109
|
-
if (store.advIdentity
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
}
|
|
114
|
+
if (!store.advIdentity) return null;
|
|
115
|
+
const adv = Buffer.isBuffer(store.advIdentity)
|
|
116
|
+
? store.advIdentity
|
|
117
|
+
: Buffer.from(store.advIdentity);
|
|
118
|
+
if (adv.length === 0) return null;
|
|
119
|
+
process.stderr.write('[DBG] ADV_PRESENT from server (' + adv.length + 'b)\n');
|
|
120
|
+
return adv;
|
|
136
121
|
}
|
|
137
122
|
|
|
138
123
|
function makeJid(input, server) {
|
|
@@ -621,6 +606,10 @@ class MessageSender {
|
|
|
621
606
|
const ownPhone = String(this._store.phoneNumber);
|
|
622
607
|
const ownJid = `${ownPhone}@s.whatsapp.net`;
|
|
623
608
|
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;
|
|
624
613
|
|
|
625
614
|
// Auto-fetch group metadata if member cache is empty (first send to this group)
|
|
626
615
|
let members = this._client._getGroupMembers(groupJid);
|
|
@@ -634,7 +623,12 @@ class MessageSender {
|
|
|
634
623
|
|
|
635
624
|
// Determine addressing mode for this group (lid = modern groups, pn = legacy)
|
|
636
625
|
const groupAddressingMode = this._client._groupAddressingMode.get(groupJid) || 'lid';
|
|
637
|
-
// For LID-mode groups
|
|
626
|
+
// senderIdentity: For LID-mode groups the server tags our outbound message with
|
|
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
|
|
638
632
|
const senderIdentity = (groupAddressingMode === 'lid' && this._client._myLid)
|
|
639
633
|
? this._client._myLid
|
|
640
634
|
: ownJid;
|
|
@@ -642,38 +636,70 @@ class MessageSender {
|
|
|
642
636
|
const rawSKDM = this._signal.buildSKDM(groupJid, senderIdentity);
|
|
643
637
|
const skdmMsg = encodeSenderKeyDistributionMessage(groupJid, rawSKDM);
|
|
644
638
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
)
|
|
639
|
+
// Separate members into two buckets:
|
|
640
|
+
// 1. knownPhones — members whose phone number is known (from LID→PN map or direct PN JID)
|
|
641
|
+
// 2. unknownLids — LID JIDs with no PN mapping yet (common on first send to a group)
|
|
642
|
+
//
|
|
643
|
+
// FIX: Previously, bucket-2 members were silently skipped (return null), which meant
|
|
644
|
+
// SKDM (Sender Key Distribution Message) was never sent to them. Recipients would
|
|
645
|
+
// receive the skmsg ciphertext but could not decrypt it — messages appeared "waiting".
|
|
646
|
+
//
|
|
647
|
+
// Cobalt solves this by querying usync directly with JID= nodes instead of phone numbers,
|
|
648
|
+
// allowing the server to resolve devices for LID-addressed participants. We mirror that
|
|
649
|
+
// approach via DeviceManager.bulkEnsureSessionsByLidJids().
|
|
650
|
+
const knownPhones = [];
|
|
651
|
+
const unknownLids = [];
|
|
652
|
+
|
|
653
|
+
for (const jid of members) {
|
|
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');
|
|
660
675
|
|
|
661
|
-
const [
|
|
662
|
-
|
|
663
|
-
? this._devMgr.bulkEnsureSessions(
|
|
676
|
+
const [memberDevicesPN, memberDevicesLid, ownDevices] = await Promise.all([
|
|
677
|
+
uniquePhones.length > 0
|
|
678
|
+
? this._devMgr.bulkEnsureSessions(uniquePhones, this._signal)
|
|
679
|
+
: Promise.resolve([]),
|
|
680
|
+
uniqueLids.length > 0
|
|
681
|
+
? this._devMgr.bulkEnsureSessionsByLidJids(uniqueLids, this._signal)
|
|
664
682
|
: Promise.resolve([]),
|
|
665
683
|
this._devMgr.ensureOwnDeviceSessions(ownPhone, this._signal)
|
|
666
684
|
]);
|
|
667
685
|
|
|
668
|
-
const allTargets = [...
|
|
686
|
+
const allTargets = [...memberDevicesPN, ...memberDevicesLid, ...ownDevices];
|
|
687
|
+
|
|
688
|
+
process.stderr.write('[DBG] GROUP senderIdentity=' + senderIdentity + ' groupAddressingMode=' + groupAddressingMode + ' myLid=' + this._client._myLid + '\n');
|
|
669
689
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
690
|
+
// phash: Baileys uses device JIDs of all RECIPIENTS (own secondary devices + others).
|
|
691
|
+
// We don't have own secondary devices, so phash = memberDevices only.
|
|
692
|
+
// We're currently sending phash=null and let server compute it (see below).
|
|
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');
|
|
673
698
|
|
|
674
699
|
const skStore = this._signal.senderKeyStore;
|
|
675
700
|
const existingSkdmMap = skStore.getSKDMMap(groupJid);
|
|
676
701
|
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');
|
|
677
703
|
|
|
678
704
|
let skdmEncrypted = [];
|
|
679
705
|
if (skdmRecipients.length > 0) {
|
|
@@ -682,10 +708,13 @@ class MessageSender {
|
|
|
682
708
|
}
|
|
683
709
|
|
|
684
710
|
const skmsgCiphertext = this._signal.senderKeyEncrypt(groupJid, senderIdentity, plaintext);
|
|
685
|
-
const
|
|
711
|
+
const phashComputed = phashTargets.length > 0 ? computePhash(phashTargets) : null;
|
|
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');
|
|
686
714
|
|
|
687
715
|
const skdmHasPkmsg = skdmEncrypted.some(e => e.type === 'pkmsg');
|
|
688
716
|
const advBytes = skdmHasPkmsg ? _buildOrGetAdvIdentity(this._client._store) : null;
|
|
717
|
+
process.stderr.write('[DBG] skdmHasPkmsg=' + skdmHasPkmsg + ' advBytes=' + (advBytes ? advBytes.length + 'b' : 'null') + '\n');
|
|
689
718
|
|
|
690
719
|
const msgContent = [];
|
|
691
720
|
if (advBytes) {
|
|
@@ -380,12 +380,14 @@ function decodeMessageContainer(buf) {
|
|
|
380
380
|
if (dsm[2]) return decodeMessageContainer(dsm[2]);
|
|
381
381
|
}
|
|
382
382
|
|
|
383
|
-
// Field
|
|
383
|
+
// Field 2: senderKeyDistributionMessage (WAProto: Message.senderKeyDistributionMessage = 2)
|
|
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.
|
|
386
387
|
let skdmInfo = null;
|
|
387
|
-
|
|
388
|
-
|
|
388
|
+
const skdmRaw = f[2] || f[15];
|
|
389
|
+
if (skdmRaw) {
|
|
390
|
+
const skdm = _decodeFields(skdmRaw);
|
|
389
391
|
// field 1 = groupId (string), field 2 = axolotlSenderKeyDistributionMessage (bytes)
|
|
390
392
|
skdmInfo = {
|
|
391
393
|
groupId: _str(skdm[1]),
|
|
@@ -403,10 +405,10 @@ function decodeMessageContainer(buf) {
|
|
|
403
405
|
if (text) msgResult = { type: 'text', text };
|
|
404
406
|
}
|
|
405
407
|
|
|
406
|
-
// Field
|
|
407
|
-
if (!msgResult && f[
|
|
408
|
+
// Field 6: extendedTextMessage (WAProto: Message.extendedTextMessage = 6) — field 1 = text
|
|
409
|
+
if (!msgResult && f[6] && Buffer.isBuffer(f[6])) {
|
|
408
410
|
try {
|
|
409
|
-
const ext = _decodeFields(f[
|
|
411
|
+
const ext = _decodeFields(f[6]);
|
|
410
412
|
const text = _str(ext[1]);
|
|
411
413
|
if (text) msgResult = { type: 'text', text };
|
|
412
414
|
} catch (_) {}
|
|
@@ -570,11 +572,13 @@ function decodeMessageContainer(buf) {
|
|
|
570
572
|
}
|
|
571
573
|
|
|
572
574
|
function encodeSenderKeyDistributionMessage(groupId, axolotlBytes) {
|
|
575
|
+
// WAProto: Message.senderKeyDistributionMessage = field 2
|
|
576
|
+
// Inner: SenderKeyDistributionMessage { groupId=1 (string), axolotlSKDM=2 (bytes) }
|
|
573
577
|
const inner = Buffer.concat([
|
|
574
578
|
field(1, WIRE_LEN, str(groupId)),
|
|
575
579
|
field(2, WIRE_LEN, bytes(axolotlBytes))
|
|
576
580
|
]);
|
|
577
|
-
return field(
|
|
581
|
+
return field(2, WIRE_LEN, inner);
|
|
578
582
|
}
|
|
579
583
|
|
|
580
584
|
module.exports = {
|
package/lib/signal/SenderKey.js
CHANGED
|
@@ -178,6 +178,7 @@ 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');
|
|
181
182
|
const map = this.getSKDMMap(groupId);
|
|
182
183
|
for (const jid of jids) map[jid] = true;
|
|
183
184
|
this.setSKDMMap(groupId, map);
|
|
@@ -170,6 +170,8 @@ 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');
|
|
173
175
|
}
|
|
174
176
|
|
|
175
177
|
async buildSessionsFromBundles(bundleMap) {
|
|
@@ -220,11 +222,18 @@ class SignalProtocol {
|
|
|
220
222
|
// Returns [{jid, type, ciphertext}]
|
|
221
223
|
async bulkEncryptForDevices(jids, plaintext) {
|
|
222
224
|
const settled = await Promise.allSettled(
|
|
223
|
-
jids.map(jid => this.encrypt(jid, plaintext).then(enc =>
|
|
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
|
+
}))
|
|
224
229
|
);
|
|
225
|
-
|
|
230
|
+
const results = settled
|
|
226
231
|
.filter(r => r.status === 'fulfilled')
|
|
227
232
|
.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;
|
|
228
237
|
}
|
|
229
238
|
|
|
230
239
|
// ─── SenderKey group encrypt / decrypt ────────────────────────────────────
|
|
@@ -293,6 +302,11 @@ function jidToAddress(jid) {
|
|
|
293
302
|
deviceId = parseInt(user.slice(colonIdx + 1), 10) || 0;
|
|
294
303
|
user = user.slice(0, colonIdx);
|
|
295
304
|
}
|
|
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
|
+
}
|
|
296
310
|
} else if (jid && jid.user) {
|
|
297
311
|
user = jid.user;
|
|
298
312
|
deviceId = jid.device || 0;
|