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 CHANGED
@@ -160,15 +160,45 @@ function attachEvents(client) {
160
160
  _rl && _rl.pause();
161
161
  out('');
162
162
  hr();
163
- kv('time', ts());
164
- kv('from', msg.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', msg.id);
167
- if (msg.text) kv('text', msg.text);
168
- if (msg.plaintext) {
169
- const t = msg.plaintext.toString('utf8').replace(/[\x00-\x08\x0b-\x1f\x7f]/g, '');
170
- if (t.trim()) kv('content', t.substring(0, 400));
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.emit('message', { id, from, participant: from, ts, plaintext, node });
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.emit('message', { id, from, participant, ts, plaintext, isGroup: true, node });
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);
@@ -144,6 +144,11 @@ class SignalStore {
144
144
  this._save();
145
145
  }
146
146
 
147
+ async deleteSession(encodedAddress) {
148
+ delete this._sessions[encodedAddress];
149
+ this._save();
150
+ }
151
+
147
152
  hasSession(encodedAddress) {
148
153
  return !!this._sessions[encodedAddress];
149
154
  }
@@ -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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whalibmob",
3
- "version": "5.1.3",
3
+ "version": "5.1.5",
4
4
  "description": "WhatsApp library for interaction with WhatsApp Mobile API no web",
5
5
  "main": "index.js",
6
6
  "bin": {