@lordmega/baileys 0.3.23 → 0.3.25

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.
Files changed (59) hide show
  1. package/README.md +35 -1434
  2. package/WAProto/index.d.ts +26 -0
  3. package/WAProto/index.js +203 -0
  4. package/lib/Defaults/index.d.ts +1 -0
  5. package/lib/Defaults/index.d.ts.map +1 -1
  6. package/lib/Defaults/index.js +1 -0
  7. package/lib/Defaults/index.js.map +1 -1
  8. package/lib/Signal/libsignal.js +4 -4
  9. package/lib/Socket/Client/websocket.js +10 -5
  10. package/lib/Socket/business.d.ts +1 -1
  11. package/lib/Socket/chats.d.ts +1 -1
  12. package/lib/Socket/chats.js +8 -6
  13. package/lib/Socket/chats.js.map +1 -1
  14. package/lib/Socket/communities.d.ts +1 -1
  15. package/lib/Socket/communities.js +3 -2
  16. package/lib/Socket/groups.d.ts +1 -1
  17. package/lib/Socket/groups.js +24 -8
  18. package/lib/Socket/index.d.ts +1 -1
  19. package/lib/Socket/messages-recv.d.ts +1 -1
  20. package/lib/Socket/messages-recv.js +92 -24
  21. package/lib/Socket/messages-send.d.ts +2 -1
  22. package/lib/Socket/messages-send.js +68 -8
  23. package/lib/Socket/messages-send.js.map +1 -1
  24. package/lib/Socket/newsletter.d.ts +1 -1
  25. package/lib/Socket/newsletter.js +28 -1
  26. package/lib/Socket/socket.js +41 -13
  27. package/lib/Store/make-in-memory-store.js +7 -2
  28. package/lib/Utils/browser-utils.d.ts +1 -0
  29. package/lib/Utils/browser-utils.js +1 -0
  30. package/lib/Utils/crypto.js +34 -1
  31. package/lib/Utils/decode-wa-message.d.ts.map +1 -1
  32. package/lib/Utils/decode-wa-message.js.map +1 -1
  33. package/lib/Utils/event-buffer.js +19 -2
  34. package/lib/Utils/generics.js +3 -6
  35. package/lib/Utils/index.js.map +1 -1
  36. package/lib/Utils/message-retry-manager.d.ts +1 -0
  37. package/lib/Utils/message-retry-manager.js +7 -0
  38. package/lib/Utils/messages-media.js +108 -41
  39. package/lib/Utils/messages.d.ts.map +1 -1
  40. package/lib/Utils/messages.js +49 -140
  41. package/lib/Utils/messages.js.map +1 -1
  42. package/lib/Utils/process-message.d.ts +1 -0
  43. package/lib/Utils/process-message.d.ts.map +1 -1
  44. package/lib/Utils/process-message.js +154 -78
  45. package/lib/Utils/process-message.js.map +1 -1
  46. package/lib/Utils/reporting-utils.d.ts +1 -0
  47. package/lib/Utils/reporting-utils.js +1 -1
  48. package/lib/Utils/rich-message-utils.d.ts.map +1 -1
  49. package/lib/Utils/rich-message-utils.js.map +1 -1
  50. package/lib/Utils/tc-token-utils.js +9 -11
  51. package/lib/Utils/use-multi-file-auth-state.js +10 -11
  52. package/lib/Utils/use-single-file-auth-state.js +8 -2
  53. package/lib/Utils/validate-connection.js +10 -4
  54. package/lib/WABinary/constants.d.ts.map +1 -1
  55. package/lib/WABinary/constants.js.map +1 -1
  56. package/lib/WAUSync/USyncQuery.js +24 -2
  57. package/package.json +10 -7
  58. package/lib/Utils/use-sqlite-auth-state.d.ts.map +0 -1
  59. package/lib/Utils/use-sqlite-auth-state.js.map +0 -1
@@ -436,7 +436,7 @@ export function makeCommunitiesSocket(config: any): {
436
436
  appPatch: (patchCreate: any) => Promise<void>;
437
437
  sendPresenceUpdate: (type: any, toJid: any) => Promise<void>;
438
438
  presenceSubscribe: (toJid: any) => Promise<void>;
439
- profilePictureUrl: (jid: any, type?: string, timeoutMs?: number, shouldIncludeTcToken?: boolean) => Promise<any>;
439
+ profilePictureUrl: (jid: any, type: string | undefined, timeoutMs: any) => Promise<any>;
440
440
  fetchBlocklist: () => Promise<any>;
441
441
  fetchStatus: (...jids: any[]) => Promise<any>;
442
442
  fetchDisappearingDuration: (...jids: any[]) => Promise<any>;
@@ -353,7 +353,7 @@ export const makeCommunitiesSocket = (config) => {
353
353
  remoteJid: inviteMessage.groupJid,
354
354
  id: generateMessageIDV2(sock.user?.id),
355
355
  fromMe: false,
356
- participant: key.remoteJid // TODO: investigate if this makes any sense at all
356
+ participant: key.remoteJid
357
357
  },
358
358
  messageStubType: WAMessageStubType.GROUP_PARTICIPANT_ADD,
359
359
  messageStubParameters: [JSON.stringify(authState.creds.me)],
@@ -419,8 +419,9 @@ export const extractCommunityMetadata = (result) => {
419
419
  memberAddMode,
420
420
  participants: getBinaryNodeChildren(community, 'participant').map(({ attrs }) => {
421
421
  return {
422
- // TODO: IMPLEMENT THE PN/LID FIELDS HERE!!
423
422
  id: attrs.jid,
423
+ lid: attrs.lid || undefined,
424
+ phoneNumber: attrs.phone_number || undefined,
424
425
  admin: (attrs.type || null)
425
426
  };
426
427
  }),
@@ -151,7 +151,7 @@ export function makeGroupsSocket(config: any): {
151
151
  appPatch: (patchCreate: any) => Promise<void>;
152
152
  sendPresenceUpdate: (type: any, toJid: any) => Promise<void>;
153
153
  presenceSubscribe: (toJid: any) => Promise<void>;
154
- profilePictureUrl: (jid: any, type?: string, timeoutMs?: number, shouldIncludeTcToken?: boolean) => Promise<any>;
154
+ profilePictureUrl: (jid: any, type: string | undefined, timeoutMs: any) => Promise<any>;
155
155
  fetchBlocklist: () => Promise<any>;
156
156
  fetchStatus: (...jids: any[]) => Promise<any>;
157
157
  fetchDisappearingDuration: (...jids: any[]) => Promise<any>;
@@ -6,7 +6,7 @@ import { getBinaryNodeChild, getBinaryNodeChildren, getBinaryNodeChildString, is
6
6
  import { makeChatsSocket } from './chats.js';
7
7
  export const makeGroupsSocket = (config) => {
8
8
  const sock = makeChatsSocket(config);
9
- const { authState, ev, query, upsertMessage } = sock;
9
+ const { authState, ev, query, upsertMessage, signalRepository } = sock;
10
10
  const groupQuery = async (jid, type, content) => query({
11
11
  tag: 'iq',
12
12
  attrs: {
@@ -16,9 +16,20 @@ export const makeGroupsSocket = (config) => {
16
16
  },
17
17
  content
18
18
  });
19
+ const storeGroupLIDMappings = async (participants) => {
20
+ if (!signalRepository?.lidMapping)
21
+ return;
22
+ const pairs = participants
23
+ .filter(p => p.lid && p.phoneNumber)
24
+ .map(p => ({ lid: p.lid, pn: p.phoneNumber }));
25
+ if (pairs.length)
26
+ await signalRepository.lidMapping.storeLIDPNMappings(pairs).catch(() => { });
27
+ };
19
28
  const groupMetadata = async (jid) => {
20
29
  const result = await groupQuery(jid, 'get', [{ tag: 'query', attrs: { request: 'interactive' } }]);
21
- return extractGroupMetadata(result);
30
+ const meta = extractGroupMetadata(result);
31
+ storeGroupLIDMappings(meta.participants).catch(() => { });
32
+ return meta;
22
33
  };
23
34
  const groupFetchAllParticipating = async () => {
24
35
  const result = await query({
@@ -44,12 +55,17 @@ export const makeGroupsSocket = (config) => {
44
55
  if (groupsChild) {
45
56
  const groups = getBinaryNodeChildren(groupsChild, 'group');
46
57
  for (const groupNode of groups) {
47
- const meta = extractGroupMetadata({
48
- tag: 'result',
49
- attrs: {},
50
- content: [groupNode]
51
- });
52
- data[meta.id] = meta;
58
+ try {
59
+ const meta = extractGroupMetadata({
60
+ tag: 'result',
61
+ attrs: {},
62
+ content: [groupNode]
63
+ });
64
+ data[meta.id] = meta;
65
+ }
66
+ catch (err) {
67
+ sock.logger.warn({ err, groupNode: groupNode?.attrs }, 'failed to parse group in groupFetchAllParticipating, skipping');
68
+ }
53
69
  }
54
70
  }
55
71
  // TODO: properly parse LID / PN DATA
@@ -426,7 +426,7 @@ declare function makeWASocket(config: any): {
426
426
  appPatch: (patchCreate: any) => Promise<void>;
427
427
  sendPresenceUpdate: (type: any, toJid: any) => Promise<void>;
428
428
  presenceSubscribe: (toJid: any) => Promise<void>;
429
- profilePictureUrl: (jid: any, type?: string, timeoutMs?: number, shouldIncludeTcToken?: boolean) => Promise<any>;
429
+ profilePictureUrl: (jid: any, type: string | undefined, timeoutMs: any) => Promise<any>;
430
430
  fetchBlocklist: () => Promise<any>;
431
431
  fetchStatus: (...jids: any[]) => Promise<any>;
432
432
  fetchDisappearingDuration: (...jids: any[]) => Promise<any>;
@@ -229,7 +229,7 @@ export function makeMessagesRecvSocket(config: any): {
229
229
  appPatch: (patchCreate: any) => Promise<void>;
230
230
  sendPresenceUpdate: (type: any, toJid: any) => Promise<void>;
231
231
  presenceSubscribe: (toJid: any) => Promise<void>;
232
- profilePictureUrl: (jid: any, type?: string, timeoutMs?: number, shouldIncludeTcToken?: boolean) => Promise<any>;
232
+ profilePictureUrl: (jid: any, type: string | undefined, timeoutMs: any) => Promise<any>;
233
233
  fetchBlocklist: () => Promise<any>;
234
234
  fetchStatus: (...jids: any[]) => Promise<any>;
235
235
  fetchDisappearingDuration: (...jids: any[]) => Promise<any>;
@@ -5,7 +5,7 @@ import Long from 'long';
5
5
  import { proto } from '../../WAProto/index.js';
6
6
  import { DEFAULT_CACHE_TTLS, KEY_BUNDLE_TYPE, MIN_PREKEY_COUNT, PLACEHOLDER_MAX_AGE_SECONDS, STATUS_EXPIRY_SECONDS } from '../Defaults/index.js';
7
7
  import { ReachoutTimelockEnforcementType, WAMessageStatus, WAMessageStubType } from '../Types/index.js';
8
- import { ACCOUNT_RESTRICTED_TEXT, aesDecryptCTR, aesEncryptGCM, cleanMessage, Curve, decodeMediaRetryNode, decodeMessageNode, decryptMessageNode, delay, derivePairingCodeKey, encodeBigEndian, encodeSignedDeviceIdentity, extractAddressingContext, extractE2ESessionFromRetryReceipt, getCallStatusFromNode, getHistoryMsg, getNextPreKeys, getStatusFromReceiptType, handleIdentityChange, hkdf, MISSING_KEYS_ERROR_TEXT, NACK_REASONS, NO_MESSAGE_FOUND_ERROR_TEXT, SERVER_ERROR_CODES, toNumber, unixTimestampSeconds, xmppPreKey, xmppSignedPreKey } from '../Utils/index.js';
8
+ import { ACCOUNT_RESTRICTED_TEXT, aesDecryptCTR, aesEncryptGCM, cleanMessage, Curve, decodeMediaRetryNode, decodeMessageNode, decryptMessageNode, decryptSecretEncryptedMessage, delay, derivePairingCodeKey, encodeBigEndian, encodeSignedDeviceIdentity, extractAddressingContext, extractE2ESessionFromRetryReceipt, getCallStatusFromNode, getHistoryMsg, getNextPreKeys, getStatusFromReceiptType, handleIdentityChange, hkdf, MISSING_KEYS_ERROR_TEXT, NACK_REASONS, NO_MESSAGE_FOUND_ERROR_TEXT, normalizeMessageContent, SERVER_ERROR_CODES, toNumber, unixTimestampSeconds, xmppPreKey, xmppSignedPreKey } from '../Utils/index.js';
9
9
  import { makeMutex } from '../Utils/make-mutex.js';
10
10
  import { makeOfflineNodeProcessor } from '../Utils/offline-node-processor.js';
11
11
  import { buildAckStanza } from '../Utils/stanza-ack.js';
@@ -384,7 +384,23 @@ export const makeMessagesRecvSocket = (config) => {
384
384
  };
385
385
  await query(stanza);
386
386
  };
387
- const sendRetryRequest = async (node, forceIncludeKeys = false) => {
387
+ const mapDecryptErrorToRetryReason = (errMsg) => {
388
+ if (!errMsg)
389
+ return '0';
390
+ const m = errMsg.toLowerCase();
391
+ if (m.includes('no session'))
392
+ return '1';
393
+ if (m.includes('invalid key id') || m.includes('unknown keyid'))
394
+ return '3';
395
+ if (m.includes('bad mac') || m.includes('badmac'))
396
+ return '7';
397
+ if (m.includes('invalid message') || m.includes('badmessage'))
398
+ return '4';
399
+ if (m.includes('invalid signature'))
400
+ return '5';
401
+ return '0';
402
+ };
403
+ const sendRetryRequest = async (node, forceIncludeKeys = false, decryptErrorMsg) => {
388
404
  const { fullMessage } = decodeMessageNode(node, authState.creds.me.id, authState.creds.me.lid || '');
389
405
  const { key: msgKey } = fullMessage;
390
406
  const msgId = msgKey.id;
@@ -476,8 +492,7 @@ export const makeMessagesRecvSocket = (config) => {
476
492
  id: node.attrs.id,
477
493
  t: node.attrs.t,
478
494
  v: '1',
479
- // ADD ERROR FIELD
480
- error: '0'
495
+ error: mapDecryptErrorToRetryReason(decryptErrorMsg)
481
496
  }
482
497
  },
483
498
  {
@@ -635,7 +650,6 @@ export const makeMessagesRecvSocket = (config) => {
635
650
  const stubType = `GROUP_PARTICIPANT_${child.tag.toUpperCase()}`;
636
651
  msg.messageStubType = WAMessageStubType[stubType];
637
652
  const participants = getBinaryNodeChildren(child, 'participant').map(({ attrs }) => {
638
- // TODO: Store LID MAPPINGS
639
653
  return {
640
654
  id: attrs.jid,
641
655
  phoneNumber: isLidUser(attrs.jid) && isPnUser(attrs.phone_number) ? attrs.phone_number : undefined,
@@ -644,6 +658,15 @@ export const makeMessagesRecvSocket = (config) => {
644
658
  admin: (attrs.type || null)
645
659
  };
646
660
  });
661
+ // store LID-PN mappings from participant notification
662
+ {
663
+ const pairs = participants
664
+ .filter(p => p.lid && p.phoneNumber)
665
+ .map(p => ({ lid: p.lid, pn: p.phoneNumber }));
666
+ if (pairs.length) {
667
+ signalRepository.lidMapping.storeLIDPNMappings(pairs).catch(err => logger.warn({ err }, 'failed to store group participant LID mappings'));
668
+ }
669
+ }
647
670
  if (participants.length === 1 &&
648
671
  // if recv. "remove" message and sender removed themselves
649
672
  // mark as left
@@ -701,7 +724,10 @@ export const makeMessagesRecvSocket = (config) => {
701
724
  break;
702
725
  case 'revoked_membership_requests':
703
726
  const isDenied = areJidsSameUser(affectedParticipantLid, actingParticipantLid);
704
- // TODO: LIDMAPPING SUPPORT
727
+ if (affectedParticipantLid && affectedParticipantPn) {
728
+ signalRepository.lidMapping.storeLIDPNMappings([{ lid: affectedParticipantLid, pn: affectedParticipantPn }])
729
+ .catch(err => logger.warn({ err }, 'failed to store membership request LID mapping'));
730
+ }
705
731
  msg.messageStubType = WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_REQUEST_NON_ADMIN_ADD;
706
732
  msg.messageStubParameters = [
707
733
  JSON.stringify({ lid: affectedParticipantLid, pn: affectedParticipantPn }),
@@ -828,19 +854,24 @@ export const makeMessagesRecvSocket = (config) => {
828
854
  const update = getBinaryNodeChild(node, 'collection');
829
855
  if (update) {
830
856
  const name = update.attrs.name;
831
- await resyncAppState([name], false);
857
+ // guard: resyncAppState failure must not block the notification mutex (#2453)
858
+ await resyncAppState([name], false).catch(err => logger.warn({ err, name }, 'server_sync resyncAppState failed, skipping'));
832
859
  }
833
860
  break;
834
861
  case 'picture':
835
862
  const setPicture = getBinaryNodeChild(node, 'set');
836
863
  const delPicture = getBinaryNodeChild(node, 'delete');
837
- // TODO: WAJIDHASH stuff proper support inhouse
838
- ev.emit('contacts.update', [
839
- {
840
- id: jidNormalizedUser(node?.attrs?.from) || (setPicture || delPicture)?.attrs?.hash || '',
841
- imgUrl: setPicture ? 'changed' : 'removed'
842
- }
843
- ]);
864
+ // use node.attrs.from as the contact JID; attrs.hash is a server-side
865
+ // obfuscated reference not suitable as a contact identifier
866
+ const picContactJid = jidNormalizedUser(node?.attrs?.from);
867
+ if (picContactJid) {
868
+ ev.emit('contacts.update', [
869
+ {
870
+ id: picContactJid,
871
+ imgUrl: setPicture ? 'changed' : 'removed'
872
+ }
873
+ ]);
874
+ }
844
875
  if (isJidGroup(from)) {
845
876
  const node = setPicture || delPicture;
846
877
  result.messageStubType = WAMessageStubType.GROUP_CHANGE_ICON;
@@ -1198,7 +1229,7 @@ export const makeMessagesRecvSocket = (config) => {
1198
1229
  else {
1199
1230
  ev.emit('messages.update', ids.map(id => ({
1200
1231
  key: { ...key, id },
1201
- update: { status, messageTimestamp: toNumber(+(attrs.t ?? 0)) }
1232
+ update: { status, ...(attrs.t ? { messageTimestamp: toNumber(+attrs.t) } : {}) }
1202
1233
  })));
1203
1234
  }
1204
1235
  }
@@ -1265,13 +1296,19 @@ export const makeMessagesRecvSocket = (config) => {
1265
1296
  };
1266
1297
  const handleMessage = async (node) => {
1267
1298
  const encNode = getBinaryNodeChild(node, 'enc');
1268
- // TODO: temporary fix for crashes and issues resulting of failed msmsg decryption
1299
+ // msmsg = multi-signal business message; requires MESSAGE_SECRET from phone
1300
+ // that companions don't have. NACK with MissingMessageSecret so the server
1301
+ // knows to request the key from the primary device.
1269
1302
  if (encNode?.attrs.type === 'msmsg') {
1270
- logger.debug({ key: node.attrs.key }, 'ignored msmsg');
1303
+ logger.debug({ key: node.attrs.key }, 'recv msmsg: requesting key from phone');
1271
1304
  await sendMessageAck(node, NACK_REASONS.MissingMessageSecret);
1272
1305
  return;
1273
1306
  }
1274
1307
  let acked = false;
1308
+ // Send provisional ACK before entering messageMutex so WA server doesn't
1309
+ // stop delivery when mutex is contended (slow DB, high load).
1310
+ // Matches WA Web behaviour: transport ACK is sent immediately on receipt.
1311
+ sendMessageAck(node).then(() => { acked = true; }).catch(err => logger.warn({ err }, 'provisional ack failed'));
1275
1312
  try {
1276
1313
  const { fullMessage: msg, category, author, decrypt } = decryptMessageNode(node, authState.creds.me.id, authState.creds.me.lid || '', signalRepository, logger);
1277
1314
  const alt = msg.key.participantAlt || msg.key.remoteJidAlt;
@@ -1376,7 +1413,8 @@ export const makeMessagesRecvSocket = (config) => {
1376
1413
  return;
1377
1414
  }
1378
1415
  const encNode = getBinaryNodeChild(node, 'enc');
1379
- await sendRetryRequest(node, !encNode);
1416
+ const decryptErrMsg = msg.messageStubParameters?.[0];
1417
+ await sendRetryRequest(node, !encNode, decryptErrMsg);
1380
1418
  if (retryRequestDelayMs) {
1381
1419
  await delay(retryRequestDelayMs);
1382
1420
  }
@@ -1405,9 +1443,15 @@ export const makeMessagesRecvSocket = (config) => {
1405
1443
  else if (msg.key.fromMe) {
1406
1444
  // message was sent by us from a different device
1407
1445
  type = 'sender';
1408
- // need to specially handle this case
1446
+ // for LID-addressed conversations, resolve participant to PN for receipt
1409
1447
  if (isLidUser(msg.key.remoteJid) || isLidUser(msg.key.remoteJidAlt)) {
1410
- participant = author; // TODO: investigate sending receipts to LIDs and not PNs
1448
+ if (author && isLidUser(author)) {
1449
+ const pn = await signalRepository.lidMapping.getPNForLID(author).catch(() => null);
1450
+ participant = pn || author;
1451
+ }
1452
+ else {
1453
+ participant = author;
1454
+ }
1411
1455
  }
1412
1456
  }
1413
1457
  else if (!sendActiveReceipts) {
@@ -1415,11 +1459,11 @@ export const makeMessagesRecvSocket = (config) => {
1415
1459
  }
1416
1460
  acked = true;
1417
1461
  await sendReceipt(msg.key.remoteJid, participant, [msg.key.id], type);
1418
- // send ack for history message
1462
+ // acknowledge receipt of history sync message to the sender
1419
1463
  const isAnyHistoryMsg = getHistoryMsg(msg.message);
1420
1464
  if (isAnyHistoryMsg) {
1421
1465
  const jid = jidNormalizedUser(msg.key.remoteJid);
1422
- await sendReceipt(jid, undefined, [msg.key.id], 'hist_sync'); // TODO: investigate
1466
+ await sendReceipt(jid, undefined, [msg.key.id], 'hist_sync');
1423
1467
  }
1424
1468
  }
1425
1469
  else {
@@ -1429,6 +1473,26 @@ export const makeMessagesRecvSocket = (config) => {
1429
1473
  }
1430
1474
  }
1431
1475
  cleanMessage(msg, authState.creds.me.id, authState.creds.me.lid);
1476
+ // Lia@Note --- decrypt secret encrypted message edits (#2554)
1477
+ const secretContent = normalizeMessageContent(msg.message);
1478
+ const secretEncryptedMessage = secretContent?.secretEncryptedMessage;
1479
+ if (secretEncryptedMessage) {
1480
+ const targetMessageKey = secretEncryptedMessage.targetMessageKey;
1481
+ const originalMessage = targetMessageKey?.id
1482
+ ? (messageRetryManager?.getRecentMessageById(targetMessageKey.id)?.message ??
1483
+ (await getMessage(targetMessageKey).catch(err => {
1484
+ logger.warn({ err, targetMessageKey }, 'failed to load original message for secret encrypted message');
1485
+ return undefined;
1486
+ })))
1487
+ : undefined;
1488
+ const messageSecret = normalizeMessageContent(originalMessage)?.messageContextInfo?.messageSecret;
1489
+ if (messageSecret?.length) {
1490
+ await decryptSecretEncryptedMessage(msg, messageSecret, authState.creds.me.id, authState.creds.me.lid, logger);
1491
+ }
1492
+ else {
1493
+ logger.warn({ secretEncType: secretEncryptedMessage.secretEncType, targetMessageKey }, 'missing original message secret for secret encrypted message');
1494
+ }
1495
+ }
1432
1496
  await upsertMessage(msg, node.attrs.offline ? 'append' : 'notify');
1433
1497
  });
1434
1498
  }
@@ -1585,7 +1649,7 @@ export const makeMessagesRecvSocket = (config) => {
1585
1649
  isWsOpen: () => ws.isOpen,
1586
1650
  onUnexpectedError,
1587
1651
  yieldToEventLoop: () => new Promise(resolve => setImmediate(resolve))
1588
- });
1652
+ }, config.offlineNodeBatchSize ?? 50);
1589
1653
  const processNode = async (type, node, identifier, exec) => {
1590
1654
  // Fast path: ack and drop ignored JIDs before entering the buffer/queue
1591
1655
  const from = node.attrs.from;
@@ -1596,7 +1660,11 @@ export const makeMessagesRecvSocket = (config) => {
1596
1660
  const isNodeFromMe = areJidsSameUser(attrs.participant || attrs.from, isLid ? authState.creds.me?.lid : authState.creds.me?.id);
1597
1661
  ignoreJid = !isNodeFromMe || isJidGroup(attrs.from) ? attrs.from : attrs.recipient;
1598
1662
  }
1599
- if (ignoreJid && ignoreJid !== S_WHATSAPP_NET && shouldIgnoreJid(ignoreJid)) {
1663
+ // never ignore pkmsg (pre-key messages) they establish Signal sessions;
1664
+ // and never ignore retry receipts — they request message re-encryption (#2509)
1665
+ const encType = getBinaryNodeChild(node, 'enc')?.attrs?.type;
1666
+ const isProtocolCritical = encType === 'pkmsg' || node.attrs.type === 'retry';
1667
+ if (!isProtocolCritical && ignoreJid && ignoreJid !== S_WHATSAPP_NET && shouldIgnoreJid(ignoreJid)) {
1600
1668
  await sendMessageAck(node, type === 'message' ? NACK_REASONS.UnhandledError : undefined);
1601
1669
  return;
1602
1670
  }
@@ -44,6 +44,7 @@ export function makeMessagesSocket(config: any): {
44
44
  messageRetryManager: MessageRetryManager | null;
45
45
  updateMemberLabel: (jid: any, memberLabel: any) => Promise<any>;
46
46
  updateMediaMessage: (message: any) => Promise<any>;
47
+ rvo: (message: any, jid?: any, options?: {}) => Promise<proto.WebMessageInfo>;
47
48
  sendMessage: (jid: any, content: any, options?: {}) => Promise<proto.WebMessageInfo | undefined>;
48
49
  executeWMexQuery: (variables: any, queryId: any, dataPath: any) => Promise<any>;
49
50
  newsletterCreate: (name: any, description: any) => Promise<{
@@ -224,7 +225,7 @@ export function makeMessagesSocket(config: any): {
224
225
  appPatch: (patchCreate: any) => Promise<void>;
225
226
  sendPresenceUpdate: (type: any, toJid: any) => Promise<void>;
226
227
  presenceSubscribe: (toJid: any) => Promise<void>;
227
- profilePictureUrl: (jid: any, type?: string, timeoutMs?: number, shouldIncludeTcToken?: boolean) => Promise<any>;
228
+ profilePictureUrl: (jid: any, type: string | undefined, timeoutMs: any) => Promise<any>;
228
229
  fetchBlocklist: () => Promise<any>;
229
230
  fetchStatus: (...jids: any[]) => Promise<any>;
230
231
  fetchDisappearingDuration: (...jids: any[]) => Promise<any>;
@@ -3,7 +3,7 @@ import { Boom } from '@hapi/boom';
3
3
  import { randomBytes } from 'crypto';
4
4
  import { proto } from '../../WAProto/index.js';
5
5
  import { BIZ_BOT_SUPPORT_PAYLOAD, DEFAULT_CACHE_TTLS, WA_DEFAULT_EPHEMERAL } from '../Defaults/index.js';
6
- import { aggregateMessageKeysNotFromMe, assertMediaContent, assertMeId, bindWaitForEvent, decryptMediaRetryData, DEF_MEDIA_HOST, delay, encodeNewsletterMessage, encodeSignedDeviceIdentity, encodeWAMessage, encryptMediaRetryRequest, extractDeviceJids, generateMessageIDV2, generateParticipantHashV2, generateWAMessage, generateWAMessageFromContent, getStatusCodeForMediaRetry, getUrlFromDirectPath, getWAUploadToServer, hasValidAlbumMedia, MessageRetryManager, normalizeMessageContent, parseAndInjectE2ESessions, shouldIncludeBizBinaryNode, unixTimestampSeconds } from '../Utils/index.js';
6
+ import { aggregateMessageKeysNotFromMe, assertMediaContent, assertMeId, bindWaitForEvent, decryptMediaRetryData, DEF_MEDIA_HOST, delay, encodeNewsletterMessage, encodeSignedDeviceIdentity, encodeWAMessage, encryptMediaRetryRequest, extractDeviceJids, generateMessageIDV2, generateParticipantHashV2, downloadMediaMessage, generateWAMessage, generateWAMessageFromContent, getContentType, getStatusCodeForMediaRetry, getUrlFromDirectPath, getWAUploadToServer, hasValidAlbumMedia, MessageRetryManager, normalizeMessageContent, parseAndInjectE2ESessions, shouldIncludeBizBinaryNode, unixTimestampSeconds } from '../Utils/index.js';
7
7
  import { AssociationType } from '../Types/index.js';
8
8
  import { getUrlInfo } from '../Utils/link-preview.js';
9
9
  import { makeKeyedMutex, makeMutex } from '../Utils/make-mutex.js';
@@ -41,7 +41,7 @@ export const makeMessagesSocket = (config) => {
41
41
  let mediaHost = DEF_MEDIA_HOST;
42
42
  const refreshMediaConn = async (forceGet = false) => {
43
43
  return mediaConnMutex.mutex('media-conn', async () => {
44
- const media = await mediaConn;
44
+ const media = await mediaConn?.catch(() => null);
45
45
  if (!media || forceGet || new Date().getTime() - media.fetchDate.getTime() > media.ttl * 1000) {
46
46
  mediaConn = (async () => {
47
47
  const result = await query({
@@ -54,7 +54,9 @@ export const makeMessagesSocket = (config) => {
54
54
  content: [{ tag: 'media_conn', attrs: {} }]
55
55
  });
56
56
  const mediaConnNode = getBinaryNodeChild(result, 'media_conn');
57
- // TODO: explore full length of data that whatsapp provides
57
+ if (!mediaConnNode) {
58
+ throw new Error('media_conn node missing from server response');
59
+ }
58
60
  const node = {
59
61
  hosts: getBinaryNodeChildren(mediaConnNode, 'host').map(({ attrs }) => ({
60
62
  hostname: attrs.hostname,
@@ -64,12 +66,17 @@ export const makeMessagesSocket = (config) => {
64
66
  ttl: +mediaConnNode.attrs.ttl,
65
67
  fetchDate: new Date()
66
68
  };
69
+ if (!node.hosts.length) {
70
+ throw new Error('media_conn returned no upload hosts');
71
+ }
67
72
  logger.debug('fetched media conn');
68
73
  if (node.hosts[0]) {
69
74
  mediaHost = node.hosts[0].hostname;
70
75
  }
71
76
  return node;
72
77
  })();
78
+ // reset on failure so next call retries fresh
79
+ mediaConn.catch(() => { mediaConn = null; });
73
80
  }
74
81
  return mediaConn;
75
82
  });
@@ -202,11 +209,13 @@ export const makeMessagesSocket = (config) => {
202
209
  }
203
210
  const result = await sock.executeUSyncQuery(query);
204
211
  if (result) {
212
+ // merge sideList (device-changed users) into main list for device enumeration
213
+ const allResults = [...result.list, ...(result.sideList || [])];
205
214
  // TODO: LID MAP this stuff (lid protocol will now return lid with devices)
206
- const lidResults = result.list.filter(a => !!a.lid);
215
+ const lidResults = allResults.filter(a => !!a.lid);
207
216
  if (lidResults.length > 0) {
208
217
  logger.trace('Storing LID maps from device call');
209
- await signalRepository.lidMapping.storeLIDPNMappings(lidResults.map(a => ({ lid: a.lid, pn: a.id })));
218
+ await signalRepository.lidMapping.storeLIDPNMappings(lidResults.map(a => ({ lid: a.lid, pn: a.id ?? a.pn })));
210
219
  // Force-refresh sessions for newly mapped LIDs to align identity addressing
211
220
  try {
212
221
  const lids = lidResults.map(a => a.lid);
@@ -218,7 +227,7 @@ export const makeMessagesSocket = (config) => {
218
227
  logger.warn({ e, count: lidResults.length }, 'failed to assert sessions for newly mapped LIDs');
219
228
  }
220
229
  }
221
- const extracted = extractDeviceJids(result?.list, authState.creds.me.id, authState.creds.me.lid, ignoreZeroDevices);
230
+ const extracted = extractDeviceJids(allResults, authState.creds.me.id, authState.creds.me.lid, ignoreZeroDevices);
222
231
  const deviceMap = {};
223
232
  for (const item of extracted) {
224
233
  deviceMap[item.user] = deviceMap[item.user] || [];
@@ -610,7 +619,9 @@ export const makeMessagesSocket = (config) => {
610
619
  }
611
620
  };
612
621
  const senderKeySessionTargets = senderKeyRecipients;
613
- await assertSessions(senderKeySessionTargets);
622
+ // catch per-device failures (e.g. not-acceptable for deregistered device)
623
+ // so one bad device doesn't abort the entire group send (#2521)
624
+ await assertSessions(senderKeySessionTargets).catch(err => logger.warn({ err }, 'assertSessions partial failure for group SKDM recipients, continuing'));
614
625
  const result = await createParticipantNodes(senderKeyRecipients, senderKeyMsg, extraAttrs);
615
626
  shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || result.shouldIncludeDeviceIdentity;
616
627
  participants.push(...result.nodes);
@@ -693,7 +704,8 @@ export const makeMessagesSocket = (config) => {
693
704
  }
694
705
  allRecipients.push(jid);
695
706
  }
696
- await assertSessions(allRecipients);
707
+ // catch partial session errors so one bad device doesn't abort the send (#2521)
708
+ await assertSessions(allRecipients).catch(err => logger.warn({ err }, 'assertSessions partial failure for 1:1 recipients, continuing'));
697
709
  const [{ nodes: meNodes, shouldIncludeDeviceIdentity: s1 }, { nodes: otherNodes, shouldIncludeDeviceIdentity: s2 }] = await Promise.all([
698
710
  // For own devices: use DSM if available (1:1 chats only)
699
711
  createParticipantNodes(meRecipients, meMsg || message, extraAttrs),
@@ -1111,6 +1123,54 @@ export const makeMessagesSocket = (config) => {
1111
1123
  ev.emit('messages.update', [{ key: message.key, update: { message: message.message } }]);
1112
1124
  return message;
1113
1125
  },
1126
+ // rvo --- reveal a view-once message: download the media, then resend it as normal media.
1127
+ // Downloads to a buffer and re-uploads (real media transfer), so it stays valid even after
1128
+ // the original view-once has been opened/expired. Supports image/video/audio/document/sticker.
1129
+ // message: received WAMessage (with .key + .message). jid: target (default = original remoteJid).
1130
+ rvo: async (message, jid, options = {}) => {
1131
+ const inner = normalizeMessageContent(message?.message);
1132
+ const contentType = inner && getContentType(inner);
1133
+ const media = contentType && inner[contentType];
1134
+ if (!media || typeof media !== 'object' || (!('url' in media) && !('directPath' in media))) {
1135
+ throw new Boom('rvo: not a media message', { statusCode: 400, data: { contentType } });
1136
+ }
1137
+ const targetJid = jid || message?.key?.remoteJid;
1138
+ if (!targetJid) {
1139
+ throw new Boom('rvo: no target jid', { statusCode: 400 });
1140
+ }
1141
+ // download original bytes
1142
+ const buffer = await downloadMediaMessage(message, 'buffer', {}, { logger });
1143
+ // map media type -> message content, preserving caption/mimetype/metadata
1144
+ const mediaType = contentType.replace('Message', '');
1145
+ const content = { [mediaType]: buffer };
1146
+ if (media.caption)
1147
+ content.caption = media.caption;
1148
+ if (media.mimetype)
1149
+ content.mimetype = media.mimetype;
1150
+ if (media.fileName)
1151
+ content.fileName = media.fileName;
1152
+ if (media.ptt != null)
1153
+ content.ptt = media.ptt;
1154
+ if (media.seconds != null)
1155
+ content.seconds = media.seconds;
1156
+ if (media.gifPlayback != null)
1157
+ content.gifPlayback = media.gifPlayback;
1158
+ const userJid = authState.creds.me.id;
1159
+ const fullMsg = await generateWAMessage(targetJid, content, {
1160
+ logger,
1161
+ userJid,
1162
+ upload: waUploadToServer,
1163
+ mediaCache: config.mediaCache,
1164
+ options: config.options,
1165
+ ...options,
1166
+ messageId: generateMessageIDV2(userJid)
1167
+ });
1168
+ await relayMessage(targetJid, fullMsg.message, { messageId: fullMsg.key.id, ...options });
1169
+ if (config.emitOwnEvents) {
1170
+ process.nextTick(() => messageMutex.mutex(() => upsertMessage(fullMsg, 'append')));
1171
+ }
1172
+ return fullMsg;
1173
+ },
1114
1174
  // Lia@Changes 30-01-26 --- Add support for modifying additionalNodes and additionalAttributes using "options" in sendMessage()
1115
1175
  sendMessage: async (jid, content, options = {}) => {
1116
1176
  const userJid = authState.creds.me.id;