whalibmob 5.1.3 → 5.1.5
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/cli.js +37 -7
- package/lib/Client.js +40 -5
- package/lib/proto/MessageProto.js +154 -1
- package/lib/signal/SignalProtocol.js +5 -0
- package/lib/signal/SignalStore.js +5 -0
- package/lib/signal/libsignal/session_cipher.js +0 -4
- package/package.json +1 -1
package/cli.js
CHANGED
|
@@ -160,15 +160,45 @@ function attachEvents(client) {
|
|
|
160
160
|
_rl && _rl.pause();
|
|
161
161
|
out('');
|
|
162
162
|
hr();
|
|
163
|
-
kv('time',
|
|
164
|
-
kv('from',
|
|
163
|
+
kv('time', ts());
|
|
164
|
+
kv('from', msg.from);
|
|
165
165
|
if (msg.participant && msg.participant !== msg.from) kv('sender', msg.participant);
|
|
166
|
-
kv('id',
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
166
|
+
kv('id', msg.id);
|
|
167
|
+
|
|
168
|
+
const d = msg.decoded;
|
|
169
|
+
if (d) {
|
|
170
|
+
switch (d.type) {
|
|
171
|
+
case 'text':
|
|
172
|
+
kv('text', d.text);
|
|
173
|
+
break;
|
|
174
|
+
case 'image':
|
|
175
|
+
kv('type', 'image' + (d.caption ? ' caption: ' + d.caption : ''));
|
|
176
|
+
break;
|
|
177
|
+
case 'video':
|
|
178
|
+
kv('type', 'video' + (d.caption ? ' caption: ' + d.caption : ''));
|
|
179
|
+
break;
|
|
180
|
+
case 'audio':
|
|
181
|
+
kv('type', 'audio');
|
|
182
|
+
break;
|
|
183
|
+
case 'voice':
|
|
184
|
+
kv('type', 'voice note');
|
|
185
|
+
break;
|
|
186
|
+
case 'document':
|
|
187
|
+
kv('type', 'document file: ' + d.fileName);
|
|
188
|
+
break;
|
|
189
|
+
case 'sticker':
|
|
190
|
+
kv('type', 'sticker');
|
|
191
|
+
break;
|
|
192
|
+
case 'reaction':
|
|
193
|
+
kv('type', 'reaction emoji: ' + d.emoji);
|
|
194
|
+
break;
|
|
195
|
+
default:
|
|
196
|
+
if (msg.text) kv('text', msg.text);
|
|
197
|
+
}
|
|
198
|
+
} else if (msg.text) {
|
|
199
|
+
kv('text', msg.text);
|
|
171
200
|
}
|
|
201
|
+
|
|
172
202
|
out('');
|
|
173
203
|
_rl && (_rl.resume(), _rl.prompt(true));
|
|
174
204
|
});
|
package/lib/Client.js
CHANGED
|
@@ -445,10 +445,10 @@ class WhalibmobClient extends EventEmitter {
|
|
|
445
445
|
|
|
446
446
|
_handleMessage(node) {
|
|
447
447
|
const attrs = node.attrs || {};
|
|
448
|
-
const from = attrs.from || '';
|
|
449
|
-
const id = attrs.id || '';
|
|
448
|
+
const from = String(attrs.from || '');
|
|
449
|
+
const id = String(attrs.id || '');
|
|
450
450
|
const ts = parseInt(attrs.t || '0', 10);
|
|
451
|
-
const participant = attrs.participant || from;
|
|
451
|
+
const participant = String(attrs.participant || from);
|
|
452
452
|
|
|
453
453
|
// Group: sender is the group JID, participant is the actual sender
|
|
454
454
|
const isGroup = from.endsWith('@g.us');
|
|
@@ -492,11 +492,13 @@ class WhalibmobClient extends EventEmitter {
|
|
|
492
492
|
}
|
|
493
493
|
|
|
494
494
|
if (skmsgNode && isGroup) {
|
|
495
|
+
this._sendMessageAck(id, from, participant);
|
|
495
496
|
this._decryptGroupMessage({ node, from, participant, id, ts, skmsgNode });
|
|
496
497
|
return;
|
|
497
498
|
}
|
|
498
499
|
|
|
499
500
|
if (dmEncNode && this._signal) {
|
|
501
|
+
this._sendMessageAck(id, from, participant);
|
|
500
502
|
this._decryptDMMessage({ node, from, id, ts, dmEncNode });
|
|
501
503
|
return;
|
|
502
504
|
}
|
|
@@ -509,6 +511,7 @@ class WhalibmobClient extends EventEmitter {
|
|
|
509
511
|
: (bodyNode.content || null)
|
|
510
512
|
) : null;
|
|
511
513
|
|
|
514
|
+
this._sendMessageAck(id, from, participant);
|
|
512
515
|
this.emit('message', { id, from, participant, ts, text, node });
|
|
513
516
|
this._sendReadReceipt(id, from, participant);
|
|
514
517
|
}
|
|
@@ -521,11 +524,18 @@ class WhalibmobClient extends EventEmitter {
|
|
|
521
524
|
|
|
522
525
|
this._signal.decrypt(from, encType, cipherBuf)
|
|
523
526
|
.then(plaintext => {
|
|
524
|
-
this.
|
|
527
|
+
const decoded = this._decodeMsg(plaintext);
|
|
528
|
+
if (decoded.type === 'senderKeyDistribution') return;
|
|
529
|
+
if (decoded.type === 'protocol') return;
|
|
530
|
+
this.emit('message', { id, from, participant: from, ts, decoded, node });
|
|
525
531
|
this._sendReadReceipt(id, from, from);
|
|
526
532
|
})
|
|
527
533
|
.catch(err => {
|
|
528
534
|
this.emit('decrypt_error', { id, from, err });
|
|
535
|
+
// Delete the stale session so WhatsApp re-establishes it fresh via pkmsg on retry
|
|
536
|
+
if (this._signal && this._signal.deleteSession) {
|
|
537
|
+
this._signal.deleteSession(from).catch(() => {});
|
|
538
|
+
}
|
|
529
539
|
this._sendRetryRequest(id, from, node);
|
|
530
540
|
});
|
|
531
541
|
}
|
|
@@ -537,13 +547,25 @@ class WhalibmobClient extends EventEmitter {
|
|
|
537
547
|
|
|
538
548
|
try {
|
|
539
549
|
const plaintext = this._signal.senderKeyDecrypt(from, participant, cipherBuf);
|
|
540
|
-
this.
|
|
550
|
+
const decoded = this._decodeMsg(plaintext);
|
|
551
|
+
if (decoded.type === 'senderKeyDistribution') return;
|
|
552
|
+
if (decoded.type === 'protocol') return;
|
|
553
|
+
this.emit('message', { id, from, participant, ts, decoded, isGroup: true, node });
|
|
541
554
|
this._sendReadReceipt(id, from, participant);
|
|
542
555
|
} catch (err) {
|
|
543
556
|
this.emit('decrypt_error', { id, from, participant, err });
|
|
544
557
|
}
|
|
545
558
|
}
|
|
546
559
|
|
|
560
|
+
_decodeMsg(buf) {
|
|
561
|
+
try {
|
|
562
|
+
const { decodeMessageContainer } = require('./proto/MessageProto');
|
|
563
|
+
return decodeMessageContainer(buf);
|
|
564
|
+
} catch (_) {
|
|
565
|
+
return { type: 'unknown' };
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
547
569
|
// Process SKDM messages in participants node
|
|
548
570
|
_processSKDMDistribution(groupJid, senderJid, participantsNode) {
|
|
549
571
|
// For incoming group messages, the SKDM for us is in the participants node
|
|
@@ -603,6 +625,19 @@ class WhalibmobClient extends EventEmitter {
|
|
|
603
625
|
}
|
|
604
626
|
}
|
|
605
627
|
|
|
628
|
+
// Delivery ack — sent immediately on message receipt.
|
|
629
|
+
// Mirrors Cobalt's sendAck(messageNode) call.
|
|
630
|
+
// Signals the server that the message was delivered → gives sender 2 grey checkmarks.
|
|
631
|
+
_sendMessageAck(msgId, from, participant) {
|
|
632
|
+
if (!this._socket || !this._connected) return;
|
|
633
|
+
const attrs = { id: msgId, class: 'message', to: from };
|
|
634
|
+
if (participant && participant !== from) attrs.participant = participant;
|
|
635
|
+
this._socket.sendNode(new BinaryNode('ack', attrs, null));
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Read receipt — sent after decryption succeeds.
|
|
639
|
+
// Mirrors Cobalt's sendMessageReceipt(info, "read") call.
|
|
640
|
+
// Gives sender 2 blue checkmarks.
|
|
606
641
|
_sendReadReceipt(msgId, from, participant) {
|
|
607
642
|
if (!this._socket || !this._connected) return;
|
|
608
643
|
const node = new BinaryNode('receipt', {
|
|
@@ -234,6 +234,158 @@ function encodeExtendedText(text) {
|
|
|
234
234
|
return field(1, WIRE_LEN, str(text));
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
+
// ─── Protobuf decoder ─────────────────────────────────────────────────────────
|
|
238
|
+
// Decodes the raw bytes that come out of Signal decryption into a structured
|
|
239
|
+
// message object — mirrors what Cobalt does with MessageContainerSpec.decode()
|
|
240
|
+
// + .unbox().
|
|
241
|
+
|
|
242
|
+
function _readVarint(buf, offset) {
|
|
243
|
+
let result = 0, shift = 0;
|
|
244
|
+
while (offset < buf.length) {
|
|
245
|
+
const b = buf[offset++];
|
|
246
|
+
result += (b & 0x7f) * Math.pow(2, shift);
|
|
247
|
+
shift += 7;
|
|
248
|
+
if (!(b & 0x80)) break;
|
|
249
|
+
}
|
|
250
|
+
return { value: result, offset };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function _decodeFields(buf) {
|
|
254
|
+
const fields = {};
|
|
255
|
+
let offset = 0;
|
|
256
|
+
while (offset < buf.length) {
|
|
257
|
+
if (offset >= buf.length) break;
|
|
258
|
+
const tr = _readVarint(buf, offset);
|
|
259
|
+
offset = tr.offset;
|
|
260
|
+
const fieldNum = tr.value >> 3;
|
|
261
|
+
const wireType = tr.value & 0x7;
|
|
262
|
+
if (wireType === 0) {
|
|
263
|
+
const vr = _readVarint(buf, offset);
|
|
264
|
+
fields[fieldNum] = (fields[fieldNum] !== undefined)
|
|
265
|
+
? [].concat(fields[fieldNum], vr.value)
|
|
266
|
+
: vr.value;
|
|
267
|
+
offset = vr.offset;
|
|
268
|
+
} else if (wireType === 2) {
|
|
269
|
+
const lr = _readVarint(buf, offset);
|
|
270
|
+
offset = lr.offset;
|
|
271
|
+
const data = buf.slice(offset, offset + lr.value);
|
|
272
|
+
offset += lr.value;
|
|
273
|
+
if (fields[fieldNum] !== undefined) {
|
|
274
|
+
if (!Array.isArray(fields[fieldNum])) fields[fieldNum] = [fields[fieldNum]];
|
|
275
|
+
fields[fieldNum].push(data);
|
|
276
|
+
} else {
|
|
277
|
+
fields[fieldNum] = data;
|
|
278
|
+
}
|
|
279
|
+
} else if (wireType === 1) {
|
|
280
|
+
offset += 8;
|
|
281
|
+
} else if (wireType === 5) {
|
|
282
|
+
offset += 4;
|
|
283
|
+
} else {
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return fields;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function _str(buf) {
|
|
291
|
+
if (!buf) return '';
|
|
292
|
+
try { return buf.toString('utf8'); } catch (_) { return ''; }
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function decodeMessageContainer(buf) {
|
|
296
|
+
if (!buf || buf.length === 0) return { type: 'unknown' };
|
|
297
|
+
try {
|
|
298
|
+
const f = _decodeFields(buf);
|
|
299
|
+
|
|
300
|
+
// Field 31: deviceSentMessage — wraps the real message at field 2
|
|
301
|
+
// (sent to our own linked devices). Unwrap and re-decode.
|
|
302
|
+
if (f[31]) {
|
|
303
|
+
const dsm = _decodeFields(f[31]);
|
|
304
|
+
if (dsm[2]) return decodeMessageContainer(dsm[2]);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Field 35: senderKeyDistributionMessage — used to set up group keys
|
|
308
|
+
if (f[35]) {
|
|
309
|
+
const skdm = _decodeFields(f[35]);
|
|
310
|
+
return { type: 'senderKeyDistribution', groupId: _str(skdm[1]), raw: f[35] };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Field 1: conversation — plain text
|
|
314
|
+
if (f[1] && Buffer.isBuffer(f[1])) {
|
|
315
|
+
const text = _str(f[1]);
|
|
316
|
+
if (text) return { type: 'text', text };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Field 2: extendedTextMessage — field 1 = text, field 17 = contextInfo
|
|
320
|
+
if (f[2] && Buffer.isBuffer(f[2])) {
|
|
321
|
+
try {
|
|
322
|
+
const ext = _decodeFields(f[2]);
|
|
323
|
+
const text = _str(ext[1]);
|
|
324
|
+
if (text) return { type: 'text', text };
|
|
325
|
+
} catch (_) {}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Field 3: imageMessage — field 3 = caption
|
|
329
|
+
if (f[3] && Buffer.isBuffer(f[3])) {
|
|
330
|
+
try {
|
|
331
|
+
const img = _decodeFields(f[3]);
|
|
332
|
+
return { type: 'image', caption: _str(img[3]) };
|
|
333
|
+
} catch (_) { return { type: 'image', caption: '' }; }
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Field 7: documentMessage — field 8 = fileName, field 3 = title
|
|
337
|
+
if (f[7] && Buffer.isBuffer(f[7])) {
|
|
338
|
+
try {
|
|
339
|
+
const doc = _decodeFields(f[7]);
|
|
340
|
+
return { type: 'document', fileName: _str(doc[8]) || _str(doc[3]) || 'document' };
|
|
341
|
+
} catch (_) { return { type: 'document', fileName: 'document' }; }
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Field 8: audioMessage — field 6 = ptt (bool)
|
|
345
|
+
if (f[8] && Buffer.isBuffer(f[8])) {
|
|
346
|
+
try {
|
|
347
|
+
const aud = _decodeFields(f[8]);
|
|
348
|
+
return { type: aud[6] ? 'voice' : 'audio' };
|
|
349
|
+
} catch (_) { return { type: 'audio' }; }
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Field 9: videoMessage — field 7 = caption
|
|
353
|
+
if (f[9] && Buffer.isBuffer(f[9])) {
|
|
354
|
+
try {
|
|
355
|
+
const vid = _decodeFields(f[9]);
|
|
356
|
+
return { type: 'video', caption: _str(vid[7]) };
|
|
357
|
+
} catch (_) { return { type: 'video', caption: '' }; }
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Field 12: protocolMessage (delete, ephemeral setting, etc.)
|
|
361
|
+
if (f[12] && Buffer.isBuffer(f[12])) {
|
|
362
|
+
try {
|
|
363
|
+
const pm = _decodeFields(f[12]);
|
|
364
|
+
const pmType = pm[2];
|
|
365
|
+
const typeNames = { 0: 'revoke', 3: 'ephemeral', 10: 'app_state_sync' };
|
|
366
|
+
return { type: 'protocol', subtype: typeNames[pmType] || String(pmType) };
|
|
367
|
+
} catch (_) { return { type: 'protocol', subtype: 'unknown' }; }
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Field 26: stickerMessage
|
|
371
|
+
if (f[26] && Buffer.isBuffer(f[26])) {
|
|
372
|
+
return { type: 'sticker' };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Field 46: reactionMessage — field 1 = key, field 2 = emoji
|
|
376
|
+
if (f[46] && Buffer.isBuffer(f[46])) {
|
|
377
|
+
try {
|
|
378
|
+
const react = _decodeFields(f[46]);
|
|
379
|
+
return { type: 'reaction', emoji: _str(react[2]) };
|
|
380
|
+
} catch (_) { return { type: 'reaction', emoji: '' }; }
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return { type: 'unknown' };
|
|
384
|
+
} catch (_) {
|
|
385
|
+
return { type: 'unknown' };
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
237
389
|
module.exports = {
|
|
238
390
|
encodeText,
|
|
239
391
|
encodeImageMessage,
|
|
@@ -256,5 +408,6 @@ module.exports = {
|
|
|
256
408
|
WIRE_VARINT,
|
|
257
409
|
WIRE_LEN,
|
|
258
410
|
PROTOCOL_MSG_REVOKE,
|
|
259
|
-
PROTOCOL_MSG_EPHEMERAL
|
|
411
|
+
PROTOCOL_MSG_EPHEMERAL,
|
|
412
|
+
decodeMessageContainer
|
|
260
413
|
};
|
|
@@ -157,6 +157,11 @@ class SignalProtocol {
|
|
|
157
157
|
return this.store.hasSession(addr.toString());
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
async deleteSession(jid) {
|
|
161
|
+
const addr = jidToAddress(jid);
|
|
162
|
+
await this.store.deleteSession(addr.toString());
|
|
163
|
+
}
|
|
164
|
+
|
|
160
165
|
async buildSessionFromBundle(jid, bundle) {
|
|
161
166
|
const addr = jidToAddress(jid);
|
|
162
167
|
const builder = new SessionBuilder(this.store, addr);
|
|
@@ -154,10 +154,6 @@ class SessionCipher {
|
|
|
154
154
|
errs.push(e);
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
|
-
console.error("Failed to decrypt message with any known session...");
|
|
158
|
-
for (const e of errs) {
|
|
159
|
-
console.error("Session error:" + e, e.stack);
|
|
160
|
-
}
|
|
161
157
|
throw new errors.SessionError("No matching sessions found for message");
|
|
162
158
|
}
|
|
163
159
|
|