@kelvdra/baileys 1.0.4 → 1.0.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.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/WAProto/index.js +65472 -137440
  3. package/lib/Defaults/index.d.ts +1 -1
  4. package/lib/Defaults/index.js +22 -3
  5. package/lib/Socket/chats.js +12 -13
  6. package/lib/Socket/groups.js +140 -7
  7. package/lib/Socket/hydra.js +44 -0
  8. package/lib/Socket/messages-recv.js +736 -324
  9. package/lib/Socket/messages-send.js +481 -110
  10. package/lib/Socket/mex.js +44 -6
  11. package/lib/Socket/newsletter.d.ts +16 -9
  12. package/lib/Socket/newsletter.js +259 -70
  13. package/lib/Types/Mex.d.ts +141 -0
  14. package/lib/Types/Mex.js +37 -0
  15. package/lib/Types/State.js +54 -1
  16. package/lib/Utils/auth-utils.js +12 -1
  17. package/lib/Utils/chat-utils.js +36 -2
  18. package/lib/Utils/companion-reg-client-utils.d.ts +17 -0
  19. package/lib/Utils/companion-reg-client-utils.js +35 -0
  20. package/lib/Utils/decode-wa-message.js +23 -4
  21. package/lib/Utils/generics.js +4 -1
  22. package/lib/Utils/identity-change-handler.d.ts +44 -0
  23. package/lib/Utils/identity-change-handler.js +50 -0
  24. package/lib/Utils/index.js +1 -1
  25. package/lib/Utils/message-retry-manager.js +25 -1
  26. package/lib/Utils/messages-media.js +162 -43
  27. package/lib/Utils/messages.d.ts +1 -1
  28. package/lib/Utils/messages.js +230 -9
  29. package/lib/Utils/offline-node-processor.d.ts +17 -0
  30. package/lib/Utils/offline-node-processor.js +40 -0
  31. package/lib/Utils/reporting-utils.d.ts +11 -0
  32. package/lib/Utils/reporting-utils.js +258 -0
  33. package/lib/Utils/signal.js +45 -1
  34. package/lib/Utils/stanza-ack.d.ts +11 -0
  35. package/lib/Utils/stanza-ack.js +38 -0
  36. package/lib/Utils/sync-action-utils.d.ts +19 -0
  37. package/lib/Utils/sync-action-utils.js +49 -0
  38. package/lib/Utils/tc-token-utils.d.ts +37 -0
  39. package/lib/Utils/tc-token-utils.js +163 -0
  40. package/lib/WAUSync/Protocols/USyncUsernameProtocol.d.ts +10 -0
  41. package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +25 -0
  42. package/package.json +3 -1
@@ -5,15 +5,16 @@ import Long from 'long';
5
5
  import { proto } from '../../WAProto/index.js';
6
6
  import { DEFAULT_CACHE_TTLS, KEY_BUNDLE_TYPE, MIN_PREKEY_COUNT } from '../Defaults/index.js';
7
7
  import { WAMessageStatus, WAMessageStubType } from '../Types/index.js';
8
- import { aesDecryptCTR, aesEncryptGCM, cleanMessage, Curve, decodeMediaRetryNode, decodeMessageNode, decryptMessageNode, delay, derivePairingCodeKey, encodeBigEndian, encodeSignedDeviceIdentity, getCallStatusFromNode, getHistoryMsg, getNextPreKeys, getStatusFromReceiptType, hkdf, MISSING_KEYS_ERROR_TEXT, NACK_REASONS, NO_MESSAGE_FOUND_ERROR_TEXT, unixTimestampSeconds, xmppPreKey, xmppSignedPreKey } from '../Utils/index.js';
8
+ import { aesDecryptCTR, aesEncryptGCM, cleanMessage, Curve, decodeMediaRetryNode, decodeMessageNode, decryptMessageNode, delay, derivePairingCodeKey, encodeBigEndian, encodeSignedDeviceIdentity, extractAddressingContext, getCallStatusFromNode, getHistoryMsg, getNextPreKeys, getStatusFromReceiptType, hkdf, MISSING_KEYS_ERROR_TEXT, NACK_REASONS, NO_MESSAGE_FOUND_ERROR_TEXT, unixTimestampSeconds, xmppPreKey, xmppSignedPreKey } from '../Utils/index.js';
9
9
  import { makeMutex } from '../Utils/make-mutex.js';
10
- import { areJidsSameUser, getAllBinaryNodeChildren, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildren, getBinaryNodeChildString, isJidGroup, isJidStatusBroadcast, isPnUser, isLidUser, jidDecode, jidNormalizedUser, S_WHATSAPP_NET } from '../WABinary/index.js';
10
+ import { areJidsSameUser, binaryNodeToString, getAllBinaryNodeChildren, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildren, getBinaryNodeChildString, isJidGroup, isJidNewsletter, isJidStatusBroadcast, isLidUser, isPnUser, jidDecode, jidNormalizedUser, S_WHATSAPP_NET } from '../WABinary/index.js';
11
11
  import { extractGroupMetadata } from './groups.js';
12
12
  import { makeMessagesSocket } from './messages-send.js';
13
+ import { USyncQuery, USyncUser } from '../WAUSync/index.js';
13
14
  export const makeMessagesRecvSocket = (config) => {
14
- const { logger, retryRequestDelayMs, maxMsgRetryCount, getMessage, shouldIgnoreJid } = config;
15
+ const { logger, retryRequestDelayMs, maxMsgRetryCount, getMessage, shouldIgnoreJid, enableAutoSessionRecreation } = config;
15
16
  const sock = makeMessagesSocket(config);
16
- const { ev, authState, ws, processingMutex, signalRepository, query, upsertMessage, resyncAppState, onUnexpectedError, assertSessions, sendNode, relayMessage, sendReceipt, uploadPreKeys, sendPeerDataOperationMessage } = sock;
17
+ const { ev, authState, ws, processingMutex, signalRepository, query, upsertMessage, resyncAppState, onUnexpectedError, assertSessions, sendNode, relayMessage, sendReceipt, uploadPreKeys, sendPeerDataOperationMessage, messageRetryManager } = sock;
17
18
  /** this mutex ensures that each retryRequest will wait for the previous one to finish */
18
19
  const retryMutex = makeMutex();
19
20
  const msgRetryCache = config.msgRetryCounterCache ||
@@ -31,7 +32,190 @@ export const makeMessagesRecvSocket = (config) => {
31
32
  stdTTL: DEFAULT_CACHE_TTLS.MSG_RETRY, // 1 hour
32
33
  useClones: false
33
34
  });
35
+ // Debounce identity-change session refreshes per JID to avoid bursts
36
+ const identityAssertDebounce = new NodeCache({ stdTTL: 5, useClones: false });
34
37
  let sendActiveReceipts = false;
38
+ const fetchMessageHistory = async (count, oldestMsgKey, oldestMsgTimestamp) => {
39
+ if (!authState.creds.me?.id) {
40
+ throw new Boom('Not authenticated');
41
+ }
42
+ const pdoMessage = {
43
+ historySyncOnDemandRequest: {
44
+ chatJid: oldestMsgKey.remoteJid,
45
+ oldestMsgFromMe: oldestMsgKey.fromMe,
46
+ oldestMsgId: oldestMsgKey.id,
47
+ oldestMsgTimestampMs: oldestMsgTimestamp,
48
+ onDemandMsgCount: count
49
+ },
50
+ peerDataOperationRequestType: proto.Message.PeerDataOperationRequestType.HISTORY_SYNC_ON_DEMAND
51
+ };
52
+ return sendPeerDataOperationMessage(pdoMessage);
53
+ };
54
+ const requestPlaceholderResend = async (messageKey) => {
55
+ if (!authState.creds.me?.id) {
56
+ throw new Boom('Not authenticated');
57
+ }
58
+ if (placeholderResendCache.get(messageKey?.id)) {
59
+ logger.debug({ messageKey }, 'already requested resend');
60
+ return;
61
+ }
62
+ else {
63
+ await placeholderResendCache.set(messageKey?.id, true);
64
+ }
65
+ await delay(5000);
66
+ if (!placeholderResendCache.get(messageKey?.id)) {
67
+ logger.debug({ messageKey }, 'message received while resend requested');
68
+ return 'RESOLVED';
69
+ }
70
+ const pdoMessage = {
71
+ placeholderMessageResendRequest: [
72
+ {
73
+ messageKey
74
+ }
75
+ ],
76
+ peerDataOperationRequestType: proto.Message.PeerDataOperationRequestType.PLACEHOLDER_MESSAGE_RESEND
77
+ };
78
+ setTimeout(async () => {
79
+ if (placeholderResendCache.get(messageKey?.id)) {
80
+ logger.debug({ messageKey }, 'PDO message without response after 15 seconds. Phone possibly offline');
81
+ await placeholderResendCache.del(messageKey?.id);
82
+ }
83
+ }, 15000);
84
+ return sendPeerDataOperationMessage(pdoMessage);
85
+ };
86
+ // Handles mex newsletter notifications
87
+ const handleMexNewsletterNotification = async (node) => {
88
+ const mexNode = getBinaryNodeChild(node, 'mex');
89
+ if (!mexNode?.content) {
90
+ logger.warn({ node }, 'Invalid mex newsletter notification');
91
+ return;
92
+ }
93
+ let data;
94
+ try {
95
+ data = JSON.parse(mexNode.content.toString());
96
+ }
97
+ catch (error) {
98
+ logger.error({ err: error, node }, 'Failed to parse mex newsletter notification');
99
+ return;
100
+ }
101
+ const operation = data?.operation;
102
+ const updates = data?.updates;
103
+ if (!updates || !operation) {
104
+ logger.warn({ data }, 'Invalid mex newsletter notification content');
105
+ return;
106
+ }
107
+ logger.info({ operation, updates }, 'got mex newsletter notification');
108
+ switch (operation) {
109
+ case 'NotificationNewsletterUpdate':
110
+ for (const update of updates) {
111
+ if (update.jid && update.settings && Object.keys(update.settings).length > 0) {
112
+ ev.emit('newsletter-settings.update', {
113
+ id: update.jid,
114
+ update: update.settings
115
+ });
116
+ }
117
+ }
118
+ break;
119
+ case 'NotificationNewsletterAdminPromote':
120
+ for (const update of updates) {
121
+ if (update.jid && update.user) {
122
+ ev.emit('newsletter-participants.update', {
123
+ id: update.jid,
124
+ author: node.attrs.from,
125
+ user: update.user,
126
+ new_role: 'ADMIN',
127
+ action: 'promote'
128
+ });
129
+ }
130
+ }
131
+ break;
132
+ default:
133
+ logger.info({ operation, data }, 'Unhandled mex newsletter notification');
134
+ break;
135
+ }
136
+ };
137
+ // Handles newsletter notifications
138
+ const handleNewsletterNotification = async (node) => {
139
+ const from = node.attrs.from;
140
+ const child = getAllBinaryNodeChildren(node)[0];
141
+ const author = node.attrs.participant;
142
+ logger.info({ from, child }, 'got newsletter notification');
143
+ switch (child.tag) {
144
+ case 'reaction':
145
+ const reactionUpdate = {
146
+ id: from,
147
+ server_id: child.attrs.message_id,
148
+ reaction: {
149
+ code: getBinaryNodeChildString(child, 'reaction'),
150
+ count: 1
151
+ }
152
+ };
153
+ ev.emit('newsletter.reaction', reactionUpdate);
154
+ break;
155
+ case 'view':
156
+ const viewUpdate = {
157
+ id: from,
158
+ server_id: child.attrs.message_id,
159
+ count: parseInt(child.content?.toString() || '0', 10)
160
+ };
161
+ ev.emit('newsletter.view', viewUpdate);
162
+ break;
163
+ case 'participant':
164
+ const participantUpdate = {
165
+ id: from,
166
+ author,
167
+ user: child.attrs.jid,
168
+ action: child.attrs.action,
169
+ new_role: child.attrs.role
170
+ };
171
+ ev.emit('newsletter-participants.update', participantUpdate);
172
+ break;
173
+ case 'update':
174
+ const settingsNode = getBinaryNodeChild(child, 'settings');
175
+ if (settingsNode) {
176
+ const update = {};
177
+ const nameNode = getBinaryNodeChild(settingsNode, 'name');
178
+ if (nameNode?.content)
179
+ update.name = nameNode.content.toString();
180
+ const descriptionNode = getBinaryNodeChild(settingsNode, 'description');
181
+ if (descriptionNode?.content)
182
+ update.description = descriptionNode.content.toString();
183
+ ev.emit('newsletter-settings.update', {
184
+ id: from,
185
+ update
186
+ });
187
+ }
188
+ break;
189
+ case 'message':
190
+ const plaintextNode = getBinaryNodeChild(child, 'plaintext');
191
+ if (plaintextNode?.content) {
192
+ try {
193
+ const contentBuf = typeof plaintextNode.content === 'string'
194
+ ? Buffer.from(plaintextNode.content, 'binary')
195
+ : Buffer.from(plaintextNode.content);
196
+ const messageProto = proto.Message.decode(contentBuf).toJSON();
197
+ const fullMessage = proto.WebMessageInfo.fromObject({
198
+ key: {
199
+ remoteJid: from,
200
+ id: child.attrs.message_id || child.attrs.server_id,
201
+ fromMe: false // TODO: is this really true though
202
+ },
203
+ message: messageProto,
204
+ messageTimestamp: +child.attrs.t
205
+ }).toJSON();
206
+ await upsertMessage(fullMessage, 'append');
207
+ logger.info('Processed plaintext newsletter message');
208
+ }
209
+ catch (error) {
210
+ logger.error({ error }, 'Failed to decode plaintext newsletter message');
211
+ }
212
+ }
213
+ break;
214
+ default:
215
+ logger.warn({ node }, 'Unknown newsletter notification');
216
+ break;
217
+ }
218
+ };
35
219
  const sendMessageAck = async ({ tag, attrs, content }, errorCode) => {
36
220
  const stanza = {
37
221
  tag: 'ack',
@@ -85,20 +269,76 @@ export const makeMessagesRecvSocket = (config) => {
85
269
  const { fullMessage } = decodeMessageNode(node, authState.creds.me.id, authState.creds.me.lid || '');
86
270
  const { key: msgKey } = fullMessage;
87
271
  const msgId = msgKey.id;
88
- const key = `${msgId}:${msgKey?.participant}`;
89
- let retryCount = msgRetryCache.get(key) || 0;
90
- if (retryCount >= maxMsgRetryCount) {
91
- logger.debug({ retryCount, msgId }, 'reached retry limit, clearing');
92
- msgRetryCache.del(key);
93
- return;
272
+ if (messageRetryManager) {
273
+ // Check if we've exceeded max retries using the new system
274
+ if (messageRetryManager.hasExceededMaxRetries(msgId)) {
275
+ logger.debug({ msgId }, 'reached retry limit with new retry manager, clearing');
276
+ messageRetryManager.markRetryFailed(msgId);
277
+ return;
278
+ }
279
+ // Increment retry count using new system
280
+ const retryCount = messageRetryManager.incrementRetryCount(msgId);
281
+ // Use the new retry count for the rest of the logic
282
+ const key = `${msgId}:${msgKey?.participant}`;
283
+ await msgRetryCache.set(key, retryCount);
284
+ }
285
+ else {
286
+ // Fallback to old system
287
+ const key = `${msgId}:${msgKey?.participant}`;
288
+ let retryCount = (await msgRetryCache.get(key)) || 0;
289
+ if (retryCount >= maxMsgRetryCount) {
290
+ logger.debug({ retryCount, msgId }, 'reached retry limit, clearing');
291
+ await msgRetryCache.del(key);
292
+ return;
293
+ }
294
+ retryCount += 1;
295
+ await msgRetryCache.set(key, retryCount);
94
296
  }
95
- retryCount += 1;
96
- msgRetryCache.set(key, retryCount);
297
+ const key = `${msgId}:${msgKey?.participant}`;
298
+ const retryCount = (await msgRetryCache.get(key)) || 1;
97
299
  const { account, signedPreKey, signedIdentityKey: identityKey } = authState.creds;
98
- if (retryCount === 1) {
99
- //request a resend via phone
100
- const msgId = await requestPlaceholderResend(msgKey);
101
- logger.debug(`sendRetryRequest: requested placeholder resend for message ${msgId}`);
300
+ const fromJid = node.attrs.from;
301
+ // Check if we should recreate the session
302
+ let shouldRecreateSession = false;
303
+ let recreateReason = '';
304
+ if (enableAutoSessionRecreation && messageRetryManager) {
305
+ try {
306
+ // Check if we have a session with this JID
307
+ const sessionId = signalRepository.jidToSignalProtocolAddress(fromJid);
308
+ const hasSession = await signalRepository.validateSession(fromJid);
309
+ const result = messageRetryManager.shouldRecreateSession(fromJid, retryCount, hasSession.exists);
310
+ shouldRecreateSession = result.recreate;
311
+ recreateReason = result.reason;
312
+ if (shouldRecreateSession) {
313
+ logger.debug({ fromJid, retryCount, reason: recreateReason }, 'recreating session for retry');
314
+ // Delete existing session to force recreation
315
+ await authState.keys.set({ session: { [sessionId]: null } });
316
+ forceIncludeKeys = true;
317
+ }
318
+ }
319
+ catch (error) {
320
+ logger.warn({ error, fromJid }, 'failed to check session recreation');
321
+ }
322
+ }
323
+ if (retryCount <= 2) {
324
+ // Use new retry manager for phone requests if available
325
+ if (messageRetryManager) {
326
+ // Schedule phone request with delay (like whatsmeow)
327
+ messageRetryManager.schedulePhoneRequest(msgId, async () => {
328
+ try {
329
+ const requestId = await requestPlaceholderResend(msgKey);
330
+ logger.debug(`sendRetryRequest: requested placeholder resend (${requestId}) for message ${msgId} (scheduled)`);
331
+ }
332
+ catch (error) {
333
+ logger.warn({ error, msgId }, 'failed to send scheduled phone request');
334
+ }
335
+ });
336
+ }
337
+ else {
338
+ // Fallback to immediate request
339
+ const msgId = await requestPlaceholderResend(msgKey);
340
+ logger.debug(`sendRetryRequest: requested placeholder resend for message ${msgId}`);
341
+ }
102
342
  }
103
343
  const deviceIdentity = encodeSignedDeviceIdentity(account, true);
104
344
  await authState.keys.transaction(async () => {
@@ -116,7 +356,9 @@ export const makeMessagesRecvSocket = (config) => {
116
356
  count: retryCount.toString(),
117
357
  id: node.attrs.id,
118
358
  t: node.attrs.t,
119
- v: '1'
359
+ v: '1',
360
+ // ADD ERROR FIELD
361
+ error: '0'
120
362
  }
121
363
  },
122
364
  {
@@ -132,7 +374,7 @@ export const makeMessagesRecvSocket = (config) => {
132
374
  if (node.attrs.participant) {
133
375
  receipt.attrs.participant = node.attrs.participant;
134
376
  }
135
- if (retryCount > 1 || forceIncludeKeys) {
377
+ if (retryCount > 1 || forceIncludeKeys || shouldRecreateSession) {
136
378
  const { update, preKeys } = await getNextPreKeys(authState, 1);
137
379
  const [keyId] = Object.keys(preKeys);
138
380
  const key = preKeys[+keyId];
@@ -152,7 +394,7 @@ export const makeMessagesRecvSocket = (config) => {
152
394
  }
153
395
  await sendNode(receipt);
154
396
  logger.info({ msgAttrs: node.attrs, retryCount }, 'sent retry receipt');
155
- });
397
+ }, authState?.creds?.me?.id || 'sendRetryRequest');
156
398
  };
157
399
  const handleEncryptNotification = async (node) => {
158
400
  const from = node.attrs.from;
@@ -169,22 +411,35 @@ export const makeMessagesRecvSocket = (config) => {
169
411
  const identityNode = getBinaryNodeChild(node, 'identity');
170
412
  if (identityNode) {
171
413
  logger.info({ jid: from }, 'identity changed');
172
- // not handling right now
173
- // signal will override new identity anyway
414
+ if (identityAssertDebounce.get(from)) {
415
+ logger.debug({ jid: from }, 'skipping identity assert (debounced)');
416
+ return;
417
+ }
418
+ identityAssertDebounce.set(from, true);
419
+ try {
420
+ await assertSessions([from], true);
421
+ }
422
+ catch (error) {
423
+ logger.warn({ error, jid: from }, 'failed to assert sessions after identity change');
424
+ }
174
425
  }
175
426
  else {
176
427
  logger.info({ node }, 'unknown encrypt notification');
177
428
  }
178
429
  }
179
430
  };
180
- const handleGroupNotification = (participant, child, msg) => {
181
- const participantJid = getBinaryNodeChild(child, 'participant')?.attrs?.jid || participant;
431
+ const handleGroupNotification = (fullNode, child, msg) => {
432
+ // TODO: Support PN/LID (Here is only LID now)
433
+ const actingParticipantLid = fullNode.attrs.participant;
434
+ const actingParticipantPn = fullNode.attrs.participant_pn;
435
+ const affectedParticipantLid = getBinaryNodeChild(child, 'participant')?.attrs?.jid || actingParticipantLid;
436
+ const affectedParticipantPn = getBinaryNodeChild(child, 'participant')?.attrs?.phone_number || actingParticipantPn;
182
437
  switch (child?.tag) {
183
438
  case 'create':
184
439
  const metadata = extractGroupMetadata(child);
185
440
  msg.messageStubType = WAMessageStubType.GROUP_CREATE;
186
441
  msg.messageStubParameters = [metadata.subject];
187
- msg.key = { participant: metadata.owner };
442
+ msg.key = { participant: metadata.owner, participantAlt: metadata.ownerPn };
188
443
  ev.emit('chats.upsert', [
189
444
  {
190
445
  id: metadata.id,
@@ -195,7 +450,8 @@ export const makeMessagesRecvSocket = (config) => {
195
450
  ev.emit('groups.upsert', [
196
451
  {
197
452
  ...metadata,
198
- author: participant
453
+ author: actingParticipantLid,
454
+ authorPn: actingParticipantPn
199
455
  }
200
456
  ]);
201
457
  break;
@@ -220,15 +476,24 @@ export const makeMessagesRecvSocket = (config) => {
220
476
  case 'leave':
221
477
  const stubType = `GROUP_PARTICIPANT_${child.tag.toUpperCase()}`;
222
478
  msg.messageStubType = WAMessageStubType[stubType];
223
- const participants = getBinaryNodeChildren(child, 'participant').map(p => p.attrs.jid);
479
+ const participants = getBinaryNodeChildren(child, 'participant').map(({ attrs }) => {
480
+ // TODO: Store LID MAPPINGS
481
+ return {
482
+ id: attrs.jid,
483
+ phoneNumber: isLidUser(attrs.jid) && isPnUser(attrs.phone_number) ? attrs.phone_number : undefined,
484
+ lid: isPnUser(attrs.jid) && isLidUser(attrs.lid) ? attrs.lid : undefined,
485
+ admin: (attrs.type || null)
486
+ };
487
+ });
224
488
  if (participants.length === 1 &&
225
489
  // if recv. "remove" message and sender removed themselves
226
490
  // mark as left
227
- areJidsSameUser(participants[0], participant) &&
491
+ (areJidsSameUser(participants[0].id, actingParticipantLid) ||
492
+ areJidsSameUser(participants[0].id, actingParticipantPn)) &&
228
493
  child.tag === 'remove') {
229
494
  msg.messageStubType = WAMessageStubType.GROUP_PARTICIPANT_LEAVE;
230
495
  }
231
- msg.messageStubParameters = participants;
496
+ msg.messageStubParameters = participants.map(a => JSON.stringify(a));
232
497
  break;
233
498
  case 'subject':
234
499
  msg.messageStubType = WAMessageStubType.GROUP_CHANGE_SUBJECT;
@@ -269,12 +534,20 @@ export const makeMessagesRecvSocket = (config) => {
269
534
  break;
270
535
  case 'created_membership_requests':
271
536
  msg.messageStubType = WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_REQUEST_NON_ADMIN_ADD;
272
- msg.messageStubParameters = [participantJid, 'created', child.attrs.request_method];
537
+ msg.messageStubParameters = [
538
+ JSON.stringify({ lid: affectedParticipantLid, pn: affectedParticipantPn }),
539
+ 'created',
540
+ child.attrs.request_method
541
+ ];
273
542
  break;
274
543
  case 'revoked_membership_requests':
275
- const isDenied = areJidsSameUser(participantJid, participant);
544
+ const isDenied = areJidsSameUser(affectedParticipantLid, actingParticipantLid);
545
+ // TODO: LIDMAPPING SUPPORT
276
546
  msg.messageStubType = WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_REQUEST_NON_ADMIN_ADD;
277
- msg.messageStubParameters = [participantJid, isDenied ? 'revoked' : 'rejected'];
547
+ msg.messageStubParameters = [
548
+ JSON.stringify({ lid: affectedParticipantLid, pn: affectedParticipantPn }),
549
+ isDenied ? 'revoked' : 'rejected'
550
+ ];
278
551
  break;
279
552
  }
280
553
  };
@@ -284,19 +557,6 @@ export const makeMessagesRecvSocket = (config) => {
284
557
  const nodeType = node.attrs.type;
285
558
  const from = jidNormalizedUser(node.attrs.from);
286
559
  switch (nodeType) {
287
- case 'privacy_token':
288
- const tokenList = getBinaryNodeChildren(child, 'token');
289
- for (const { attrs, content } of tokenList) {
290
- const jid = attrs.jid;
291
- ev.emit('chats.update', [
292
- {
293
- id: jid,
294
- tcToken: content
295
- }
296
- ]);
297
- logger.debug({ jid }, 'got privacy token update');
298
- }
299
- break;
300
560
  case 'newsletter':
301
561
  await handleNewsletterNotification(node);
302
562
  break;
@@ -304,7 +564,8 @@ export const makeMessagesRecvSocket = (config) => {
304
564
  await handleMexNewsletterNotification(node);
305
565
  break;
306
566
  case 'w:gp2':
307
- handleGroupNotification(node.attrs.participant, child, result);
567
+ // TODO: HANDLE PARTICIPANT_PN
568
+ handleGroupNotification(node, child, result);
308
569
  break;
309
570
  case 'mediaretry':
310
571
  const event = decodeMediaRetryNode(node);
@@ -315,10 +576,12 @@ export const makeMessagesRecvSocket = (config) => {
315
576
  break;
316
577
  case 'devices':
317
578
  const devices = getBinaryNodeChildren(child, 'device');
318
- if (areJidsSameUser(child.attrs.jid, authState.creds.me.id)) {
319
- const deviceJids = devices.map(d => d.attrs.jid);
320
- logger.info({ deviceJids }, 'got my own devices');
579
+ if (areJidsSameUser(child.attrs.jid, authState.creds.me.id) ||
580
+ areJidsSameUser(child.attrs.lid, authState.creds.me.lid)) {
581
+ const deviceData = devices.map(d => ({ id: d.attrs.jid, lid: d.attrs.lid }));
582
+ logger.info({ deviceData }, 'my own devices changed');
321
583
  }
584
+ //TODO: drop a new event, add hashes
322
585
  break;
323
586
  case 'server_sync':
324
587
  const update = getBinaryNodeChild(node, 'collection');
@@ -434,11 +697,37 @@ export const makeMessagesRecvSocket = (config) => {
434
697
  });
435
698
  authState.creds.registered = true;
436
699
  ev.emit('creds.update', authState.creds);
700
+ break;
701
+ case 'privacy_token':
702
+ await handlePrivacyTokenNotification(node);
703
+ break;
437
704
  }
438
705
  if (Object.keys(result).length) {
439
706
  return result;
440
707
  }
441
708
  };
709
+ const handlePrivacyTokenNotification = async (node) => {
710
+ const tokensNode = getBinaryNodeChild(node, 'tokens');
711
+ const from = jidNormalizedUser(node.attrs.from);
712
+ if (!tokensNode)
713
+ return;
714
+ const tokenNodes = getBinaryNodeChildren(tokensNode, 'token');
715
+ for (const tokenNode of tokenNodes) {
716
+ const { attrs, content } = tokenNode;
717
+ const type = attrs.type;
718
+ const timestamp = attrs.t;
719
+ if (type === 'trusted_contact' && content instanceof Buffer) {
720
+ logger.debug({
721
+ from,
722
+ timestamp,
723
+ tcToken: content
724
+ }, 'received trusted contact token');
725
+ await authState.keys.set({
726
+ tctoken: { [from]: { token: content, timestamp } }
727
+ });
728
+ }
729
+ }
730
+ };
442
731
  async function decipherLinkPublicKey(data) {
443
732
  const buffer = toRequiredBuffer(data);
444
733
  const salt = buffer.slice(0, 32);
@@ -453,33 +742,80 @@ export const makeMessagesRecvSocket = (config) => {
453
742
  }
454
743
  return data instanceof Buffer ? data : Buffer.from(data);
455
744
  }
456
- const willSendMessageAgain = (id, participant) => {
745
+ const willSendMessageAgain = async (id, participant) => {
457
746
  const key = `${id}:${participant}`;
458
- const retryCount = msgRetryCache.get(key) || 0;
747
+ const retryCount = (await msgRetryCache.get(key)) || 0;
459
748
  return retryCount < maxMsgRetryCount;
460
749
  };
461
- const updateSendMessageAgainCount = (id, participant) => {
750
+ const updateSendMessageAgainCount = async (id, participant) => {
462
751
  const key = `${id}:${participant}`;
463
- const newValue = (msgRetryCache.get(key) || 0) + 1;
464
- msgRetryCache.set(key, newValue);
752
+ const newValue = ((await msgRetryCache.get(key)) || 0) + 1;
753
+ await msgRetryCache.set(key, newValue);
465
754
  };
466
755
  const sendMessagesAgain = async (key, ids, retryNode) => {
467
- // todo: implement a cache to store the last 256 sent messages (copy whatsmeow)
468
- const msgs = await Promise.all(ids.map(id => getMessage({ ...key, id })));
469
756
  const remoteJid = key.remoteJid;
470
757
  const participant = key.participant || remoteJid;
758
+ const retryCount = +retryNode.attrs.count || 1;
759
+ // Try to get messages from cache first, then fallback to getMessage
760
+ const msgs = [];
761
+ for (const id of ids) {
762
+ let msg;
763
+ // Try to get from retry cache first if enabled
764
+ if (messageRetryManager) {
765
+ const cachedMsg = messageRetryManager.getRecentMessage(remoteJid, id);
766
+ if (cachedMsg) {
767
+ msg = cachedMsg.message;
768
+ logger.debug({ jid: remoteJid, id }, 'found message in retry cache');
769
+ // Mark retry as successful since we found the message
770
+ messageRetryManager.markRetrySuccess(id);
771
+ }
772
+ }
773
+ // Fallback to getMessage if not found in cache
774
+ if (!msg) {
775
+ msg = await getMessage({ ...key, id });
776
+ if (msg) {
777
+ logger.debug({ jid: remoteJid, id }, 'found message via getMessage');
778
+ // Also mark as successful if found via getMessage
779
+ if (messageRetryManager) {
780
+ messageRetryManager.markRetrySuccess(id);
781
+ }
782
+ }
783
+ }
784
+ msgs.push(msg);
785
+ }
471
786
  // if it's the primary jid sending the request
472
787
  // just re-send the message to everyone
473
788
  // prevents the first message decryption failure
474
789
  const sendToAll = !jidDecode(participant)?.device;
790
+ // Check if we should recreate session for this retry
791
+ let shouldRecreateSession = false;
792
+ let recreateReason = '';
793
+ if (enableAutoSessionRecreation && messageRetryManager) {
794
+ try {
795
+ const sessionId = signalRepository.jidToSignalProtocolAddress(participant);
796
+ const hasSession = await signalRepository.validateSession(participant);
797
+ const result = messageRetryManager.shouldRecreateSession(participant, retryCount, hasSession.exists);
798
+ shouldRecreateSession = result.recreate;
799
+ recreateReason = result.reason;
800
+ if (shouldRecreateSession) {
801
+ logger.debug({ participant, retryCount, reason: recreateReason }, 'recreating session for outgoing retry');
802
+ await authState.keys.set({ session: { [sessionId]: null } });
803
+ }
804
+ }
805
+ catch (error) {
806
+ logger.warn({ error, participant }, 'failed to check session recreation for outgoing retry');
807
+ }
808
+ }
475
809
  await assertSessions([participant], true);
476
810
  if (isJidGroup(remoteJid)) {
477
811
  await authState.keys.set({ 'sender-key-memory': { [remoteJid]: null } });
478
812
  }
479
- logger.debug({ participant, sendToAll }, 'forced new session for retry recp');
813
+ logger.debug({ participant, sendToAll, shouldRecreateSession, recreateReason }, 'forced new session for retry recp');
480
814
  for (const [i, msg] of msgs.entries()) {
481
- if (msg) {
482
- updateSendMessageAgainCount(ids[i], participant);
815
+ if (!ids[i])
816
+ continue;
817
+ if (msg && (await willSendMessageAgain(ids[i], participant))) {
818
+ await updateSendMessageAgainCount(ids[i], participant);
483
819
  const msgRelayOpts = { messageId: ids[i] };
484
820
  if (sendToAll) {
485
821
  msgRelayOpts.useUserDevicesCache = false;
@@ -509,7 +845,7 @@ export const makeMessagesRecvSocket = (config) => {
509
845
  fromMe,
510
846
  participant: attrs.participant
511
847
  };
512
- if (shouldIgnoreJid(remoteJid) && remoteJid !== '@s.whatsapp.net') {
848
+ if (shouldIgnoreJid(remoteJid) && remoteJid !== S_WHATSAPP_NET) {
513
849
  logger.debug({ remoteJid }, 'ignoring receipt from jid');
514
850
  await sendMessageAck(node);
515
851
  return;
@@ -550,14 +886,15 @@ export const makeMessagesRecvSocket = (config) => {
550
886
  // correctly set who is asking for the retry
551
887
  key.participant = key.participant || attrs.from;
552
888
  const retryNode = getBinaryNodeChild(node, 'retry');
553
- if (willSendMessageAgain(ids[0], key.participant)) {
889
+ if (ids[0] && key.participant && (await willSendMessageAgain(ids[0], key.participant))) {
554
890
  if (key.fromMe) {
555
891
  try {
892
+ await updateSendMessageAgainCount(ids[0], key.participant);
556
893
  logger.debug({ attrs, key }, 'recv retry request');
557
894
  await sendMessagesAgain(key, ids, retryNode);
558
895
  }
559
896
  catch (error) {
560
- logger.error({ key, ids, trace: error.stack }, 'error in sending message again');
897
+ logger.error({ key, ids, trace: error instanceof Error ? error.stack : 'Unknown error' }, 'error in sending message again');
561
898
  }
562
899
  }
563
900
  else {
@@ -577,7 +914,7 @@ export const makeMessagesRecvSocket = (config) => {
577
914
  };
578
915
  const handleNotification = async (node) => {
579
916
  const remoteJid = node.attrs.from;
580
- if (shouldIgnoreJid(remoteJid) && remoteJid !== '@s.whatsapp.net') {
917
+ if (shouldIgnoreJid(remoteJid) && remoteJid !== S_WHATSAPP_NET) {
581
918
  logger.debug({ remoteJid, id: node.attrs.id }, 'ignored notification');
582
919
  await sendMessageAck(node);
583
920
  return;
@@ -588,10 +925,13 @@ export const makeMessagesRecvSocket = (config) => {
588
925
  const msg = await processNotification(node);
589
926
  if (msg) {
590
927
  const fromMe = areJidsSameUser(node.attrs.participant || remoteJid, authState.creds.me.id);
928
+ const { senderAlt: participantAlt, addressingMode } = extractAddressingContext(node);
591
929
  msg.key = {
592
930
  remoteJid,
593
931
  fromMe,
594
932
  participant: node.attrs.participant,
933
+ participantAlt,
934
+ addressingMode,
595
935
  id: node.attrs.id,
596
936
  ...(msg.key || {})
597
937
  };
@@ -607,68 +947,319 @@ export const makeMessagesRecvSocket = (config) => {
607
947
  await sendMessageAck(node);
608
948
  }
609
949
  };
950
+
951
+ const resolveMentionedLIDs = async (msg, lidMapping) => {
952
+ // Important: do not rewrite envelope IDs like msg.key.participant/msg.key.remoteJid here.
953
+ // Group receipts, read status and message-info can become unstable when the same account
954
+ // is seen as both LID and PN/JID within the message key. Only resolve mention-related fields.
955
+ const msgContent = msg.message;
956
+ if (!msgContent) {
957
+ return;
958
+ }
959
+ const getContextInfo = (content) => {
960
+ if (!content || typeof content !== 'object') {
961
+ return null;
962
+ }
963
+ if (content.contextInfo) {
964
+ return content.contextInfo;
965
+ }
966
+ for (const val of Object.values(content)) {
967
+ const found = getContextInfo(val);
968
+ if (found) {
969
+ return found;
970
+ }
971
+ }
972
+ return null;
973
+ };
974
+ const getTextField = (content) => {
975
+ if (!content || typeof content !== 'object') {
976
+ return null;
977
+ }
978
+ for (const key of ['text', 'caption', 'conversation']) {
979
+ if (typeof content[key] === 'string') {
980
+ return { obj: content, key };
981
+ }
982
+ }
983
+ for (const val of Object.values(content)) {
984
+ const found = getTextField(val);
985
+ if (found) {
986
+ return found;
987
+ }
988
+ }
989
+ return null;
990
+ };
991
+ const getAllContextInfos = (content, results = []) => {
992
+ if (!content || typeof content !== 'object') {
993
+ return results;
994
+ }
995
+ if (content.contextInfo) {
996
+ results.push(content.contextInfo);
997
+ if (content.contextInfo.quotedMessage) {
998
+ getAllContextInfos(content.contextInfo.quotedMessage, results);
999
+ }
1000
+ }
1001
+ for (const val of Object.values(content)) {
1002
+ if (val && typeof val === 'object' && !results.includes(val)) {
1003
+ getAllContextInfos(val, results);
1004
+ }
1005
+ }
1006
+ return results;
1007
+ };
1008
+ const contextInfo = getContextInfo(msgContent);
1009
+ const allContextInfos = getAllContextInfos(msgContent);
1010
+ for (const ctx of allContextInfos) {
1011
+ if (ctx?.participant?.endsWith('@lid')) {
1012
+ try {
1013
+ const pn = await lidMapping.getPNForLID(ctx.participant);
1014
+ if (pn) {
1015
+ logger.debug({ lid: ctx.participant, pn }, 'resolved nested contextInfo.participant LID -> PN');
1016
+ ctx.participant = pn;
1017
+ }
1018
+ }
1019
+ catch { }
1020
+ }
1021
+ if (ctx !== contextInfo && ctx?.mentionedJid?.length) {
1022
+ const lids = ctx.mentionedJid.filter(j => j?.endsWith('@lid'));
1023
+ for (const lid of lids) {
1024
+ try {
1025
+ const pn = await lidMapping.getPNForLID(lid);
1026
+ if (pn) {
1027
+ ctx.mentionedJid = ctx.mentionedJid.map(j => j === lid ? pn : j);
1028
+ }
1029
+ }
1030
+ catch { }
1031
+ }
1032
+ }
1033
+ }
1034
+ if (contextInfo?.participant?.endsWith('@lid')) {
1035
+ try {
1036
+ const pn = await lidMapping.getPNForLID(contextInfo.participant);
1037
+ if (pn) {
1038
+ logger.debug({ lid: contextInfo.participant, pn }, 'resolved contextInfo.participant LID -> PN');
1039
+ contextInfo.participant = pn;
1040
+ }
1041
+ }
1042
+ catch { }
1043
+ }
1044
+ if (!contextInfo?.mentionedJid?.length) {
1045
+ return;
1046
+ }
1047
+ const textFieldEarly = getTextField(msgContent);
1048
+ if (textFieldEarly) {
1049
+ let earlyText = textFieldEarly.obj[textFieldEarly.key] || '';
1050
+ const lidNumPattern = /@(\d{13,20})/g;
1051
+ const lidNumMatches = [...earlyText.matchAll(lidNumPattern)];
1052
+ if (lidNumMatches.length > 0) {
1053
+ for (const resolvedJid of contextInfo.mentionedJid) {
1054
+ if (resolvedJid?.endsWith('@lid')) {
1055
+ continue;
1056
+ }
1057
+ const pnNum = resolvedJid.split('@')[0].split(':')[0];
1058
+ if (!pnNum) {
1059
+ continue;
1060
+ }
1061
+ for (const match of lidNumMatches) {
1062
+ const lidNum = match[1];
1063
+ if (earlyText.includes(`@${lidNum}`)) {
1064
+ earlyText = earlyText.split(`@${lidNum}`).join(`@${pnNum}`);
1065
+ logger.debug({ lidNum, pnNum }, 'replaced LID number in text with PN number');
1066
+ break;
1067
+ }
1068
+ }
1069
+ }
1070
+ textFieldEarly.obj[textFieldEarly.key] = earlyText;
1071
+ }
1072
+ }
1073
+ const hasLid = contextInfo.mentionedJid.some((j) => j?.endsWith('@lid'));
1074
+ if (!hasLid) {
1075
+ return;
1076
+ }
1077
+ const lidJids = contextInfo.mentionedJid.filter((j) => j?.endsWith('@lid'));
1078
+ const resolveMap = new Map();
1079
+ const stillUnresolved = [];
1080
+ for (const lidJid of lidJids) {
1081
+ try {
1082
+ const pn = await lidMapping.getPNForLID(lidJid);
1083
+ if (pn) {
1084
+ resolveMap.set(lidJid, pn);
1085
+ }
1086
+ else {
1087
+ stillUnresolved.push(lidJid);
1088
+ }
1089
+ }
1090
+ catch {
1091
+ stillUnresolved.push(lidJid);
1092
+ }
1093
+ }
1094
+ if (stillUnresolved.length > 0) {
1095
+ try {
1096
+ const usyncQ = new USyncQuery().withContactProtocol().withContext('background');
1097
+ for (const lidJid of stillUnresolved) {
1098
+ usyncQ.withUser(new USyncUser().withId(lidJid));
1099
+ }
1100
+ const result = await sock.executeUSyncQuery(usyncQ);
1101
+ if (result?.list) {
1102
+ const mappings = [];
1103
+ for (const item of result.list) {
1104
+ const itemNum = (item.id ?? '').split('@')[0].split(':')[0];
1105
+ const matchedLidJid = stillUnresolved.find(l => {
1106
+ if (l === item.id) {
1107
+ return true;
1108
+ }
1109
+ const lNum = l.split('@')[0].split(':')[0];
1110
+ return itemNum && lNum && itemNum === lNum;
1111
+ });
1112
+ if (matchedLidJid && item.id && !item.id.endsWith('@lid')) {
1113
+ resolveMap.set(matchedLidJid, item.id);
1114
+ mappings.push({ lid: matchedLidJid, pn: item.id });
1115
+ logger.debug({ lid: matchedLidJid, pn: item.id }, 'USync resolved LID -> PN');
1116
+ }
1117
+ }
1118
+ if (mappings.length > 0) {
1119
+ lidMapping.storeLIDPNMappings(mappings).catch(() => { });
1120
+ }
1121
+ }
1122
+ }
1123
+ catch (e) {
1124
+ logger.debug({ err: e }, 'USync LID resolve failed, using cache only');
1125
+ }
1126
+ }
1127
+ const textField = getTextField(msgContent);
1128
+ const stillUnresolvedAfterUSync = lidJids.filter(l => !resolveMap.has(l));
1129
+ if (stillUnresolvedAfterUSync.length > 0 && textField) {
1130
+ const rawText = textField.obj[textField.key] || '';
1131
+ const mentionMatches = [...rawText.matchAll(/@(\d{7,15})/g)].map(m => m[1]);
1132
+ if (mentionMatches.length > 0) {
1133
+ const lidOrder = contextInfo.mentionedJid
1134
+ .map((jid, idx) => ({ jid, idx }))
1135
+ .filter(({ jid }) => stillUnresolvedAfterUSync.includes(jid));
1136
+ for (let i = 0; i < lidOrder.length && i < mentionMatches.length; i++) {
1137
+ const lidJid = lidOrder[i].jid;
1138
+ const phoneNum = mentionMatches[i];
1139
+ const pnJid = `${phoneNum}@s.whatsapp.net`;
1140
+ resolveMap.set(lidJid, pnJid);
1141
+ lidMapping.storeLIDPNMappings([{ lid: lidJid, pn: pnJid }]).catch(() => { });
1142
+ logger.debug({ lid: lidJid, pn: pnJid }, 'text-extracted PN for LID -> PN');
1143
+ }
1144
+ }
1145
+ }
1146
+ contextInfo.mentionedJid = contextInfo.mentionedJid.map((jid) => {
1147
+ if (!jid?.endsWith('@lid')) {
1148
+ return jid;
1149
+ }
1150
+ const resolved = resolveMap.get(jid);
1151
+ if (resolved) {
1152
+ logger.debug({ lid: jid, pn: resolved }, 'resolved mentionedJid LID -> PN');
1153
+ return resolved;
1154
+ }
1155
+ return jid;
1156
+ });
1157
+ if (textField) {
1158
+ let text = textField.obj[textField.key];
1159
+ for (const [lidJid, pnJid] of resolveMap) {
1160
+ const lidNum = lidJid.split('@')[0].split(':')[0] ?? '';
1161
+ const pnNum = pnJid.replace('@s.whatsapp.net', '').split(':')[0] ?? '';
1162
+ if (lidNum && pnNum && text.includes(lidNum)) {
1163
+ text = text.split(lidNum).join(pnNum);
1164
+ }
1165
+ }
1166
+ textField.obj[textField.key] = text;
1167
+ }
1168
+ };
1169
+
610
1170
  const handleMessage = async (node) => {
611
- if (shouldIgnoreJid(node.attrs.from) && node.attrs.from !== '@s.whatsapp.net') {
1171
+ if (shouldIgnoreJid(node.attrs.from) && node.attrs.from !== S_WHATSAPP_NET) {
612
1172
  logger.debug({ key: node.attrs.key }, 'ignored message');
613
- await sendMessageAck(node);
1173
+ await sendMessageAck(node, NACK_REASONS.UnhandledError);
614
1174
  return;
615
1175
  }
616
1176
  const encNode = getBinaryNodeChild(node, 'enc');
617
1177
  // TODO: temporary fix for crashes and issues resulting of failed msmsg decryption
618
1178
  if (encNode && encNode.attrs.type === 'msmsg') {
619
1179
  logger.debug({ key: node.attrs.key }, 'ignored msmsg');
620
- await sendMessageAck(node);
1180
+ await sendMessageAck(node, NACK_REASONS.MissingMessageSecret);
621
1181
  return;
622
1182
  }
623
- let response;
624
- if (getBinaryNodeChild(node, 'unavailable') && !encNode) {
625
- await sendMessageAck(node);
626
- const { key } = decodeMessageNode(node, authState.creds.me.id, authState.creds.me.lid || '').fullMessage;
627
- response = await requestPlaceholderResend(key);
628
- if (response === 'RESOLVED') {
629
- return;
1183
+ const { fullMessage: msg, category, author, decrypt } = decryptMessageNode(node, authState.creds.me.id, authState.creds.me.lid || '', signalRepository, logger);
1184
+ const alt = msg.key.participantAlt || msg.key.remoteJidAlt;
1185
+ // store new mappings we didn't have before
1186
+ if (!!alt) {
1187
+ const altServer = jidDecode(alt)?.server;
1188
+ const primaryJid = msg.key.participant || msg.key.remoteJid;
1189
+ if (altServer === 'lid') {
1190
+ if (!(await signalRepository.lidMapping.getPNForLID(alt))) {
1191
+ await signalRepository.lidMapping.storeLIDPNMappings([{ lid: alt, pn: primaryJid }]);
1192
+ await signalRepository.migrateSession(primaryJid, alt);
1193
+ }
630
1194
  }
631
- logger.debug('received unavailable message, acked and requested resend from phone');
632
- }
633
- else {
634
- if (placeholderResendCache.get(node.attrs.id)) {
635
- placeholderResendCache.del(node.attrs.id);
1195
+ else {
1196
+ await signalRepository.lidMapping.storeLIDPNMappings([{ lid: primaryJid, pn: alt }]);
1197
+ await signalRepository.migrateSession(alt, primaryJid);
636
1198
  }
637
1199
  }
638
- const { fullMessage: msg, category, author, decrypt } = decryptMessageNode(node, authState.creds.me.id, authState.creds.me.lid || '', signalRepository, logger);
639
- if (response && msg?.messageStubParameters?.[0] === NO_MESSAGE_FOUND_ERROR_TEXT) {
640
- msg.messageStubParameters = [NO_MESSAGE_FOUND_ERROR_TEXT, response];
641
- }
642
- if (msg.message?.protocolMessage?.type === proto.Message.ProtocolMessage.Type.SHARE_PHONE_NUMBER &&
643
- node.attrs.sender_pn) {
644
- ev.emit('chats.phoneNumberShare', { lid: node.attrs.from, jid: node.attrs.sender_pn });
1200
+ if (msg.key?.remoteJid && msg.key?.id && messageRetryManager) {
1201
+ messageRetryManager.addRecentMessage(msg.key.remoteJid, msg.key.id, msg.message);
1202
+ logger.debug({
1203
+ jid: msg.key.remoteJid,
1204
+ id: msg.key.id
1205
+ }, 'Added message to recent cache for retry receipts');
645
1206
  }
646
1207
  try {
647
- await Promise.all([
648
- processingMutex.mutex(async () => {
649
- await decrypt();
650
- // message failed to decrypt
651
- if (msg.messageStubType === proto.WebMessageInfo.StubType.CIPHERTEXT) {
652
- if (msg?.messageStubParameters?.[0] === MISSING_KEYS_ERROR_TEXT) {
653
- return sendMessageAck(node, NACK_REASONS.ParsingError);
654
- }
655
- retryMutex.mutex(async () => {
656
- if (ws.isOpen) {
657
- if (getBinaryNodeChild(node, 'unavailable')) {
658
- return;
1208
+ await processingMutex.mutex(async () => {
1209
+ await decrypt();
1210
+ // message failed to decrypt
1211
+ if (msg.messageStubType === proto.WebMessageInfo.StubType.CIPHERTEXT && msg.category !== 'peer') {
1212
+ if (msg?.messageStubParameters?.[0] === MISSING_KEYS_ERROR_TEXT ||
1213
+ msg.messageStubParameters?.[0] === NO_MESSAGE_FOUND_ERROR_TEXT) {
1214
+ return sendMessageAck(node);
1215
+ }
1216
+ const errorMessage = msg?.messageStubParameters?.[0] || '';
1217
+ const isPreKeyError = errorMessage.includes('PreKey');
1218
+ logger.debug(`[handleMessage] Attempting retry request for failed decryption`);
1219
+ // Handle both pre-key and normal retries in single mutex
1220
+ await retryMutex.mutex(async () => {
1221
+ try {
1222
+ if (!ws.isOpen) {
1223
+ logger.debug({ node }, 'Connection closed, skipping retry');
1224
+ return;
1225
+ }
1226
+ // Handle pre-key errors with upload and delay
1227
+ if (isPreKeyError) {
1228
+ logger.info({ error: errorMessage }, 'PreKey error detected, uploading and retrying');
1229
+ try {
1230
+ logger.debug('Uploading pre-keys for error recovery');
1231
+ await uploadPreKeys(5);
1232
+ logger.debug('Waiting for server to process new pre-keys');
1233
+ await delay(1000);
659
1234
  }
1235
+ catch (uploadErr) {
1236
+ logger.error({ uploadErr }, 'Pre-key upload failed, proceeding with retry anyway');
1237
+ }
1238
+ }
1239
+ const encNode = getBinaryNodeChild(node, 'enc');
1240
+ await sendRetryRequest(node, !encNode);
1241
+ if (retryRequestDelayMs) {
1242
+ await delay(retryRequestDelayMs);
1243
+ }
1244
+ }
1245
+ catch (err) {
1246
+ logger.error({ err, isPreKeyError }, 'Failed to handle retry, attempting basic retry');
1247
+ // Still attempt retry even if pre-key upload failed
1248
+ try {
660
1249
  const encNode = getBinaryNodeChild(node, 'enc');
661
1250
  await sendRetryRequest(node, !encNode);
662
- if (retryRequestDelayMs) {
663
- await delay(retryRequestDelayMs);
664
- }
665
1251
  }
666
- else {
667
- logger.debug({ node }, 'connection closed, ignoring retry req');
1252
+ catch (retryErr) {
1253
+ logger.error({ retryErr }, 'Failed to send retry after error handling');
668
1254
  }
669
- });
670
- }
671
- else {
1255
+ }
1256
+ await sendMessageAck(node, NACK_REASONS.UnhandledError);
1257
+ });
1258
+ }
1259
+ else {
1260
+ await resolveMentionedLIDs(msg, signalRepository.lidMapping);
1261
+ const isNewsletter = isJidNewsletter(msg.key.remoteJid);
1262
+ if (!isNewsletter) {
672
1263
  // no type in the receipt => message delivered
673
1264
  let type = undefined;
674
1265
  let participant = msg.key.participant;
@@ -680,8 +1271,8 @@ export const makeMessagesRecvSocket = (config) => {
680
1271
  // message was sent by us from a different device
681
1272
  type = 'sender';
682
1273
  // need to specially handle this case
683
- if (isPnUser(msg.key.remoteJid)) {
684
- participant = author;
1274
+ if (isLidUser(msg.key.remoteJid) || isLidUser(msg.key.remoteJidAlt)) {
1275
+ participant = author; // TODO: investigate sending receipts to LIDs and not PNs
685
1276
  }
686
1277
  }
687
1278
  else if (!sendActiveReceipts) {
@@ -692,91 +1283,31 @@ export const makeMessagesRecvSocket = (config) => {
692
1283
  const isAnyHistoryMsg = getHistoryMsg(msg.message);
693
1284
  if (isAnyHistoryMsg) {
694
1285
  const jid = jidNormalizedUser(msg.key.remoteJid);
695
- await sendReceipt(jid, undefined, [msg.key.id], 'hist_sync');
1286
+ await sendReceipt(jid, undefined, [msg.key.id], 'hist_sync'); // TODO: investigate
696
1287
  }
697
1288
  }
698
- cleanMessage(msg, authState.creds.me.id);
699
- await upsertMessage(msg, node.attrs.offline ? 'append' : 'notify');
700
- })
701
- ]);
1289
+ else {
1290
+ await sendMessageAck(node);
1291
+ logger.debug({ key: msg.key }, 'processed newsletter message without receipts');
1292
+ }
1293
+ }
1294
+ cleanMessage(msg, authState.creds.me.id, authState.creds.me.lid);
1295
+ await upsertMessage(msg, node.attrs.offline ? 'append' : 'notify');
1296
+ });
702
1297
  }
703
1298
  catch (error) {
704
- logger.error({ error, node }, 'error in handling message');
705
- }
706
- };
707
- const fetchMessageHistory = async (count, oldestMsgKey, oldestMsgTimestamp) => {
708
- if (!authState.creds.me?.id) {
709
- throw new Boom('Not authenticated');
1299
+ logger.error({ error, node: binaryNodeToString(node) }, 'error in handling message');
710
1300
  }
711
- const pdoMessage = {
712
- historySyncOnDemandRequest: {
713
- chatJid: oldestMsgKey.remoteJid,
714
- oldestMsgFromMe: oldestMsgKey.fromMe,
715
- oldestMsgId: oldestMsgKey.id,
716
- oldestMsgTimestampMs: oldestMsgTimestamp,
717
- onDemandMsgCount: count
718
- },
719
- peerDataOperationRequestType: proto.Message.PeerDataOperationRequestType.HISTORY_SYNC_ON_DEMAND
720
- };
721
- return sendPeerDataOperationMessage(pdoMessage);
722
- };
723
- const requestPlaceholderResend = async (messageKey) => {
724
- if (!authState.creds.me?.id) {
725
- throw new Boom('Not authenticated');
726
- }
727
- if (placeholderResendCache.get(messageKey?.id)) {
728
- logger.debug({ messageKey }, 'already requested resend');
729
- return;
730
- }
731
- else {
732
- placeholderResendCache.set(messageKey?.id, true);
733
- }
734
- await delay(5000);
735
- if (!placeholderResendCache.get(messageKey?.id)) {
736
- logger.debug({ messageKey }, 'message received while resend requested');
737
- return 'RESOLVED';
738
- }
739
- const pdoMessage = {
740
- placeholderMessageResendRequest: [
741
- {
742
- messageKey
743
- }
744
- ],
745
- peerDataOperationRequestType: proto.Message.PeerDataOperationRequestType.PLACEHOLDER_MESSAGE_RESEND
746
- };
747
- setTimeout(() => {
748
- if (placeholderResendCache.get(messageKey?.id)) {
749
- logger.debug({ messageKey }, 'PDO message without response after 15 seconds. Phone possibly offline');
750
- placeholderResendCache.del(messageKey?.id);
751
- }
752
- }, 15000);
753
- return sendPeerDataOperationMessage(pdoMessage);
754
1301
  };
755
1302
  const handleCall = async (node) => {
756
- let status;
757
1303
  const { attrs } = node;
758
1304
  const [infoChild] = getAllBinaryNodeChildren(node);
1305
+ const status = getCallStatusFromNode(infoChild);
759
1306
  if (!infoChild) {
760
1307
  throw new Boom('Missing call info in call node');
761
1308
  }
762
1309
  const callId = infoChild.attrs['call-id'];
763
1310
  const from = infoChild.attrs.from || infoChild.attrs['call-creator'];
764
- status = getCallStatusFromNode(infoChild);
765
- if (isLidUser(from) && infoChild.tag === 'relaylatency') {
766
- const verify = callOfferCache.get(callId);
767
- if (!verify) {
768
- status = 'offer';
769
- const callLid = {
770
- chatId: attrs.from,
771
- from,
772
- id: callId,
773
- date: new Date(+attrs.t * 1000),
774
- offline: !!attrs.offline,
775
- status
776
- };
777
- callOfferCache.set(callId, callLid);
778
- }
779
- }
780
1311
  const call = {
781
1312
  chatId: attrs.from,
782
1313
  from,
@@ -789,9 +1320,9 @@ export const makeMessagesRecvSocket = (config) => {
789
1320
  call.isVideo = !!getBinaryNodeChild(infoChild, 'video');
790
1321
  call.isGroup = infoChild.attrs.type === 'group' || !!infoChild.attrs['group-jid'];
791
1322
  call.groupJid = infoChild.attrs['group-jid'];
792
- callOfferCache.set(call.id, call);
1323
+ await callOfferCache.set(call.id, call);
793
1324
  }
794
- const existingCall = callOfferCache.get(call.id);
1325
+ const existingCall = await callOfferCache.get(call.id);
795
1326
  // use existing call info to populate this event
796
1327
  if (existingCall) {
797
1328
  call.isVideo = existingCall.isVideo;
@@ -799,7 +1330,7 @@ export const makeMessagesRecvSocket = (config) => {
799
1330
  }
800
1331
  // delete data once call has ended
801
1332
  if (status === 'reject' || status === 'accept' || status === 'timeout' || status === 'terminate') {
802
- callOfferCache.del(call.id);
1333
+ await callOfferCache.del(call.id);
803
1334
  }
804
1335
  ev.emit('call', [call]);
805
1336
  await sendMessageAck(node);
@@ -832,6 +1363,19 @@ export const makeMessagesRecvSocket = (config) => {
832
1363
  }
833
1364
  }
834
1365
  ]);
1366
+ // resend the message with device_fanout=false, use at your own risk
1367
+ // if (attrs.error === '475') {
1368
+ // const msg = await getMessage(key)
1369
+ // if (msg) {
1370
+ // await relayMessage(key.remoteJid!, msg, {
1371
+ // messageId: key.id!,
1372
+ // useUserDevicesCache: false,
1373
+ // additionalAttributes: {
1374
+ // device_fanout: 'false'
1375
+ // }
1376
+ // })
1377
+ // }
1378
+ // }
835
1379
  }
836
1380
  };
837
1381
  /// processes a node with the given function
@@ -876,165 +1420,32 @@ export const makeMessagesRecvSocket = (config) => {
876
1420
  return { enqueue };
877
1421
  };
878
1422
  const offlineNodeProcessor = makeOfflineNodeProcessor();
879
- const processNode = (type, node, identifier, exec) => {
1423
+ const processNode = async (type, node, identifier, exec) => {
880
1424
  const isOffline = !!node.attrs.offline;
881
1425
  if (isOffline) {
882
1426
  offlineNodeProcessor.enqueue(type, node);
883
1427
  }
884
1428
  else {
885
- processNodeWithBuffer(node, identifier, exec);
1429
+ await processNodeWithBuffer(node, identifier, exec);
886
1430
  }
887
1431
  };
888
- // Handles newsletter notifications
889
- async function handleNewsletterNotification(node) {
890
- const from = node.attrs.from;
891
- const child = getAllBinaryNodeChildren(node)[0];
892
- const author = node.attrs.participant;
893
- logger.info({ from, child }, 'got newsletter notification');
894
- switch (child.tag) {
895
- case 'reaction':
896
- const reactionUpdate = {
897
- id: from,
898
- server_id: child.attrs.message_id,
899
- reaction: {
900
- code: getBinaryNodeChildString(child, 'reaction'),
901
- count: 1
902
- }
903
- };
904
- ev.emit('newsletter.reaction', reactionUpdate);
905
- break;
906
- case 'view':
907
- const viewUpdate = {
908
- id: from,
909
- server_id: child.attrs.message_id,
910
- count: parseInt(child.content?.toString() || '0', 10)
911
- };
912
- ev.emit('newsletter.view', viewUpdate);
913
- break;
914
- case 'participant':
915
- const participantUpdate = {
916
- id: from,
917
- author,
918
- user: child.attrs.jid,
919
- action: child.attrs.action,
920
- new_role: child.attrs.role
921
- };
922
- ev.emit('newsletter-participants.update', participantUpdate);
923
- break;
924
- case 'update':
925
- const settingsNode = getBinaryNodeChild(child, 'settings');
926
- if (settingsNode) {
927
- const update = {};
928
- const nameNode = getBinaryNodeChild(settingsNode, 'name');
929
- if (nameNode?.content)
930
- update.name = nameNode.content.toString();
931
- const descriptionNode = getBinaryNodeChild(settingsNode, 'description');
932
- if (descriptionNode?.content)
933
- update.description = descriptionNode.content.toString();
934
- ev.emit('newsletter-settings.update', {
935
- id: from,
936
- update
937
- });
938
- }
939
- break;
940
- case 'message':
941
- const plaintextNode = getBinaryNodeChild(child, 'plaintext');
942
- if (plaintextNode?.content) {
943
- try {
944
- const contentBuf = typeof plaintextNode.content === 'string'
945
- ? Buffer.from(plaintextNode.content, 'binary')
946
- : Buffer.from(plaintextNode.content);
947
- const messageProto = proto.Message.decode(contentBuf);
948
- const fullMessage = proto.WebMessageInfo.fromObject({
949
- key: {
950
- remoteJid: from,
951
- id: child.attrs.message_id || child.attrs.server_id,
952
- fromMe: false
953
- },
954
- message: messageProto,
955
- messageTimestamp: +child.attrs.t
956
- });
957
- await upsertMessage(fullMessage, 'append');
958
- logger.info('Processed plaintext newsletter message');
959
- }
960
- catch (error) {
961
- logger.error({ error }, 'Failed to decode plaintext newsletter message');
962
- }
963
- }
964
- break;
965
- default:
966
- logger.warn({ node }, 'Unknown newsletter notification');
967
- break;
968
- }
969
- }
970
- // Handles mex newsletter notifications
971
- async function handleMexNewsletterNotification(node) {
972
- const mexNode = getBinaryNodeChild(node, 'mex');
973
- if (!mexNode?.content) {
974
- logger.warn({ node }, 'Invalid mex newsletter notification');
975
- return;
976
- }
977
- let data;
978
- try {
979
- data = JSON.parse(mexNode.content.toString());
980
- }
981
- catch (error) {
982
- logger.error({ err: error, node }, 'Failed to parse mex newsletter notification');
983
- return;
984
- }
985
- const operation = data?.operation;
986
- const updates = data?.updates;
987
- if (!updates || !operation) {
988
- logger.warn({ data }, 'Invalid mex newsletter notification content');
989
- return;
990
- }
991
- logger.info({ operation, updates }, 'got mex newsletter notification');
992
- switch (operation) {
993
- case 'NotificationNewsletterUpdate':
994
- for (const update of updates) {
995
- if (update.jid && update.settings && Object.keys(update.settings).length > 0) {
996
- ev.emit('newsletter-settings.update', {
997
- id: update.jid,
998
- update: update.settings
999
- });
1000
- }
1001
- }
1002
- break;
1003
- case 'NotificationNewsletterAdminPromote':
1004
- for (const update of updates) {
1005
- if (update.jid && update.user) {
1006
- ev.emit('newsletter-participants.update', {
1007
- id: update.jid,
1008
- author: node.attrs.from,
1009
- user: update.user,
1010
- new_role: 'ADMIN',
1011
- action: 'promote'
1012
- });
1013
- }
1014
- }
1015
- break;
1016
- default:
1017
- logger.info({ operation, data }, 'Unhandled mex newsletter notification');
1018
- break;
1019
- }
1020
- }
1021
1432
  // recv a message
1022
- ws.on('CB:message', (node) => {
1023
- processNode('message', node, 'processing message', handleMessage);
1433
+ ws.on('CB:message', async (node) => {
1434
+ await processNode('message', node, 'processing message', handleMessage);
1024
1435
  });
1025
1436
  ws.on('CB:call', async (node) => {
1026
- processNode('call', node, 'handling call', handleCall);
1437
+ await processNode('call', node, 'handling call', handleCall);
1027
1438
  });
1028
- ws.on('CB:receipt', node => {
1029
- processNode('receipt', node, 'handling receipt', handleReceipt);
1439
+ ws.on('CB:receipt', async (node) => {
1440
+ await processNode('receipt', node, 'handling receipt', handleReceipt);
1030
1441
  });
1031
1442
  ws.on('CB:notification', async (node) => {
1032
- processNode('notification', node, 'handling notification', handleNotification);
1443
+ await processNode('notification', node, 'handling notification', handleNotification);
1033
1444
  });
1034
1445
  ws.on('CB:ack,class:message', (node) => {
1035
1446
  handleBadAck(node).catch(error => onUnexpectedError(error, 'handling bad ack'));
1036
1447
  });
1037
- ev.on('call', ([call]) => {
1448
+ ev.on('call', async ([call]) => {
1038
1449
  if (!call) {
1039
1450
  return;
1040
1451
  }
@@ -1062,7 +1473,7 @@ export const makeMessagesRecvSocket = (config) => {
1062
1473
  msg.message = { call: { callKey: Buffer.from(call.id) } };
1063
1474
  }
1064
1475
  const protoMsg = proto.WebMessageInfo.fromObject(msg);
1065
- upsertMessage(protoMsg, call.offline ? 'append' : 'notify');
1476
+ await upsertMessage(protoMsg, call.offline ? 'append' : 'notify');
1066
1477
  }
1067
1478
  });
1068
1479
  ev.on('connection.update', ({ isOnline }) => {
@@ -1077,7 +1488,8 @@ export const makeMessagesRecvSocket = (config) => {
1077
1488
  sendRetryRequest,
1078
1489
  rejectCall,
1079
1490
  fetchMessageHistory,
1080
- requestPlaceholderResend
1491
+ requestPlaceholderResend,
1492
+ messageRetryManager
1081
1493
  };
1082
1494
  };
1083
1495
  //# sourceMappingURL=messages-recv.js.map