@ryuu-reinzz/baileys 3.0.0-beta.2 → 3.0.0-beta.21
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/LICENSE +1 -1
- package/README.md +6 -0
- package/WAProto/fix-imports.js +70 -18
- package/WAProto/index.js +197 -160
- package/lib/Defaults/index.js +17 -4
- package/lib/Signal/libsignal.js +63 -2
- package/lib/Signal/lid-mapping.js +170 -70
- package/lib/Socket/Client/websocket.js +5 -1
- package/lib/Socket/business.js +11 -8
- package/lib/Socket/chats.js +55 -28
- package/lib/Socket/index.js +0 -6
- package/lib/Socket/messages-recv.js +152 -75
- package/lib/Socket/messages-send.js +230 -148
- package/lib/Socket/socket.js +69 -15
- package/lib/Utils/auth-utils.js +53 -20
- package/lib/Utils/chat-utils.js +100 -51
- package/lib/Utils/crypto.js +2 -26
- package/lib/Utils/event-buffer.js +33 -7
- package/lib/Utils/generics.js +4 -1
- package/lib/Utils/history.js +46 -5
- package/lib/Utils/identity-change-handler.js +49 -0
- package/lib/Utils/index.js +2 -0
- package/lib/Utils/lt-hash.js +2 -42
- package/lib/Utils/make-mutex.js +20 -27
- package/lib/Utils/message-retry-manager.js +58 -5
- package/lib/Utils/messages-media.js +151 -40
- package/lib/Utils/messages.js +43 -23
- package/lib/Utils/noise-handler.js +139 -85
- package/lib/Utils/process-message.js +57 -14
- package/lib/Utils/reporting-utils.js +258 -0
- package/lib/Utils/sync-action-utils.js +48 -0
- package/lib/Utils/tc-token-utils.js +18 -0
- package/lib/Utils/use-sqlite-auth-state.js +122 -0
- package/lib/WABinary/decode.js +24 -0
- package/lib/WABinary/encode.js +5 -1
- package/lib/WABinary/generic-utils.js +19 -8
- package/package.json +7 -2
package/lib/Socket/chats.js
CHANGED
|
@@ -7,6 +7,7 @@ import { SyncState } from '../Types/State.js';
|
|
|
7
7
|
import { chatModificationToAppPatch, decodePatches, decodeSyncdSnapshot, encodeSyncdPatch, extractSyncdPatches, generateProfilePicture, getHistoryMsg, newLTHashState, processSyncAction } from '../Utils/index.js';
|
|
8
8
|
import { makeMutex } from '../Utils/make-mutex.js';
|
|
9
9
|
import processMessage from '../Utils/process-message.js';
|
|
10
|
+
import { buildTcTokenFromJid } from '../Utils/tc-token-utils.js';
|
|
10
11
|
import { getBinaryNodeChild, getBinaryNodeChildren, jidDecode, jidNormalizedUser, reduceBinaryNodeToDictionary, S_WHATSAPP_NET } from '../WABinary/index.js';
|
|
11
12
|
import { USyncQuery, USyncUser } from '../WAUSync/index.js';
|
|
12
13
|
import { makeSocket } from './socket.js';
|
|
@@ -14,11 +15,17 @@ const MAX_SYNC_ATTEMPTS = 2;
|
|
|
14
15
|
export const makeChatsSocket = (config) => {
|
|
15
16
|
const { logger, markOnlineOnConnect, fireInitQueries, appStateMacVerification, shouldIgnoreJid, shouldSyncHistoryMessage, getMessage } = config;
|
|
16
17
|
const sock = makeSocket(config);
|
|
17
|
-
const { ev, ws, authState, generateMessageTag, sendNode, query, signalRepository, onUnexpectedError } = sock;
|
|
18
|
+
const { ev, ws, authState, generateMessageTag, sendNode, query, signalRepository, onUnexpectedError, sendUnifiedSession } = sock;
|
|
18
19
|
let privacySettings;
|
|
19
20
|
let syncState = SyncState.Connecting;
|
|
20
|
-
/** this mutex ensures that
|
|
21
|
-
const
|
|
21
|
+
/** this mutex ensures that messages are processed in order */
|
|
22
|
+
const messageMutex = makeMutex();
|
|
23
|
+
/** this mutex ensures that receipts are processed in order */
|
|
24
|
+
const receiptMutex = makeMutex();
|
|
25
|
+
/** this mutex ensures that app state patches are processed in order */
|
|
26
|
+
const appStatePatchMutex = makeMutex();
|
|
27
|
+
/** this mutex ensures that notifications are processed in order */
|
|
28
|
+
const notificationMutex = makeMutex();
|
|
22
29
|
// Timeout for AwaitingInitialSync state
|
|
23
30
|
let awaitingSyncTimeout;
|
|
24
31
|
const placeholderResendCache = config.placeholderResendCache ||
|
|
@@ -346,6 +353,15 @@ export const makeChatsSocket = (config) => {
|
|
|
346
353
|
};
|
|
347
354
|
};
|
|
348
355
|
const resyncAppState = ev.createBufferedFunction(async (collections, isInitialSync) => {
|
|
356
|
+
const appStateSyncKeyCache = new Map();
|
|
357
|
+
const getCachedAppStateSyncKey = async (keyId) => {
|
|
358
|
+
if (appStateSyncKeyCache.has(keyId)) {
|
|
359
|
+
return appStateSyncKeyCache.get(keyId) ?? undefined;
|
|
360
|
+
}
|
|
361
|
+
const key = await getAppStateSyncKey(keyId);
|
|
362
|
+
appStateSyncKeyCache.set(keyId, key ?? null);
|
|
363
|
+
return key;
|
|
364
|
+
};
|
|
349
365
|
// we use this to determine which events to fire
|
|
350
366
|
// otherwise when we resync from scratch -- all notifications will fire
|
|
351
367
|
const initialVersionMap = {};
|
|
@@ -405,7 +421,7 @@ export const makeChatsSocket = (config) => {
|
|
|
405
421
|
const { patches, hasMorePatches, snapshot } = decoded[name];
|
|
406
422
|
try {
|
|
407
423
|
if (snapshot) {
|
|
408
|
-
const { state: newState, mutationMap } = await decodeSyncdSnapshot(name, snapshot,
|
|
424
|
+
const { state: newState, mutationMap } = await decodeSyncdSnapshot(name, snapshot, getCachedAppStateSyncKey, initialVersionMap[name], appStateMacVerification.snapshot);
|
|
409
425
|
states[name] = newState;
|
|
410
426
|
Object.assign(globalMutationMap, mutationMap);
|
|
411
427
|
logger.info(`restored state of ${name} from snapshot to v${newState.version} with mutations`);
|
|
@@ -413,7 +429,7 @@ export const makeChatsSocket = (config) => {
|
|
|
413
429
|
}
|
|
414
430
|
// only process if there are syncd patches
|
|
415
431
|
if (patches.length) {
|
|
416
|
-
const { state: newState, mutationMap } = await decodePatches(name, patches, states[name],
|
|
432
|
+
const { state: newState, mutationMap } = await decodePatches(name, patches, states[name], getCachedAppStateSyncKey, config.options, initialVersionMap[name], logger, appStateMacVerification.patch);
|
|
417
433
|
await authState.keys.set({ 'app-state-sync-version': { [name]: newState } });
|
|
418
434
|
logger.info(`synced ${name} to v${newState.version}`);
|
|
419
435
|
initialVersionMap[name] = newState.version;
|
|
@@ -456,7 +472,8 @@ export const makeChatsSocket = (config) => {
|
|
|
456
472
|
* type = "image for the high res picture"
|
|
457
473
|
*/
|
|
458
474
|
const profilePictureUrl = async (jid, type = 'preview', timeoutMs) => {
|
|
459
|
-
|
|
475
|
+
const baseContent = [{ tag: 'picture', attrs: { type, query: 'url' } }];
|
|
476
|
+
const tcTokenContent = await buildTcTokenFromJid({ authState, jid, baseContent });
|
|
460
477
|
jid = jidNormalizedUser(jid);
|
|
461
478
|
const result = await query({
|
|
462
479
|
tag: 'iq',
|
|
@@ -466,7 +483,7 @@ export const makeChatsSocket = (config) => {
|
|
|
466
483
|
type: 'get',
|
|
467
484
|
xmlns: 'w:profile:picture'
|
|
468
485
|
},
|
|
469
|
-
content:
|
|
486
|
+
content: tcTokenContent
|
|
470
487
|
}, timeoutMs);
|
|
471
488
|
const child = getBinaryNodeChild(result, 'picture');
|
|
472
489
|
return child?.attrs?.url;
|
|
@@ -491,12 +508,16 @@ export const makeChatsSocket = (config) => {
|
|
|
491
508
|
};
|
|
492
509
|
const sendPresenceUpdate = async (type, toJid) => {
|
|
493
510
|
const me = authState.creds.me;
|
|
494
|
-
|
|
511
|
+
const isAvailableType = type === 'available';
|
|
512
|
+
if (isAvailableType || type === 'unavailable') {
|
|
495
513
|
if (!me.name) {
|
|
496
514
|
logger.warn('no name present, ignoring presence update request...');
|
|
497
515
|
return;
|
|
498
516
|
}
|
|
499
|
-
ev.emit('connection.update', { isOnline:
|
|
517
|
+
ev.emit('connection.update', { isOnline: isAvailableType });
|
|
518
|
+
if (isAvailableType) {
|
|
519
|
+
void sendUnifiedSession();
|
|
520
|
+
}
|
|
500
521
|
await sendNode({
|
|
501
522
|
tag: 'presence',
|
|
502
523
|
attrs: {
|
|
@@ -527,23 +548,18 @@ export const makeChatsSocket = (config) => {
|
|
|
527
548
|
* @param toJid the jid to subscribe to
|
|
528
549
|
* @param tcToken token for subscription, use if present
|
|
529
550
|
*/
|
|
530
|
-
const presenceSubscribe = (toJid
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
content: tcToken
|
|
543
|
-
}
|
|
544
|
-
]
|
|
545
|
-
: undefined
|
|
546
|
-
});
|
|
551
|
+
const presenceSubscribe = async (toJid) => {
|
|
552
|
+
const tcTokenContent = await buildTcTokenFromJid({ authState, jid: toJid });
|
|
553
|
+
return sendNode({
|
|
554
|
+
tag: 'presence',
|
|
555
|
+
attrs: {
|
|
556
|
+
to: toJid,
|
|
557
|
+
id: generateMessageTag(),
|
|
558
|
+
type: 'subscribe'
|
|
559
|
+
},
|
|
560
|
+
content: tcTokenContent
|
|
561
|
+
});
|
|
562
|
+
};
|
|
547
563
|
const handlePresenceUpdate = ({ tag, attrs, content }) => {
|
|
548
564
|
let presence;
|
|
549
565
|
const jid = attrs.from;
|
|
@@ -583,7 +599,7 @@ export const makeChatsSocket = (config) => {
|
|
|
583
599
|
}
|
|
584
600
|
let initial;
|
|
585
601
|
let encodeResult;
|
|
586
|
-
await
|
|
602
|
+
await appStatePatchMutex.mutex(async () => {
|
|
587
603
|
await authState.keys.transaction(async () => {
|
|
588
604
|
logger.debug({ patch: patchCreate }, 'applying app patch');
|
|
589
605
|
await resyncAppState([name], false);
|
|
@@ -916,11 +932,22 @@ export const makeChatsSocket = (config) => {
|
|
|
916
932
|
}
|
|
917
933
|
}, 20000);
|
|
918
934
|
});
|
|
935
|
+
ev.on('lid-mapping.update', async ({ lid, pn }) => {
|
|
936
|
+
try {
|
|
937
|
+
await signalRepository.lidMapping.storeLIDPNMappings([{ lid, pn }]);
|
|
938
|
+
}
|
|
939
|
+
catch (error) {
|
|
940
|
+
logger.warn({ lid, pn, error }, 'Failed to store LID-PN mapping');
|
|
941
|
+
}
|
|
942
|
+
});
|
|
919
943
|
return {
|
|
920
944
|
...sock,
|
|
921
945
|
createCallLink,
|
|
922
946
|
getBotListV2,
|
|
923
|
-
|
|
947
|
+
messageMutex,
|
|
948
|
+
receiptMutex,
|
|
949
|
+
appStatePatchMutex,
|
|
950
|
+
notificationMutex,
|
|
924
951
|
fetchPrivacySettings,
|
|
925
952
|
upsertMessage,
|
|
926
953
|
appPatch,
|
package/lib/Socket/index.js
CHANGED
|
@@ -6,12 +6,6 @@ const makeWASocket = (config) => {
|
|
|
6
6
|
...DEFAULT_CONNECTION_CONFIG,
|
|
7
7
|
...config
|
|
8
8
|
};
|
|
9
|
-
// If the user hasn't provided their own history sync function,
|
|
10
|
-
// let's create a default one that respects the syncFullHistory flag.
|
|
11
|
-
// TODO: Change
|
|
12
|
-
if (config.shouldSyncHistoryMessage === undefined) {
|
|
13
|
-
newConfig.shouldSyncHistoryMessage = () => !!newConfig.syncFullHistory;
|
|
14
|
-
}
|
|
15
9
|
return makeCommunitiesSocket(newConfig);
|
|
16
10
|
};
|
|
17
11
|
export default makeWASocket;
|
|
@@ -3,9 +3,9 @@ import { Boom } from '@hapi/boom';
|
|
|
3
3
|
import { randomBytes } from 'crypto';
|
|
4
4
|
import Long from 'long';
|
|
5
5
|
import { proto } from '../../WAProto/index.js';
|
|
6
|
-
import { DEFAULT_CACHE_TTLS, KEY_BUNDLE_TYPE, MIN_PREKEY_COUNT } from '../Defaults/index.js';
|
|
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 { WAMessageStatus, WAMessageStubType } from '../Types/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';
|
|
8
|
+
import { aesDecryptCTR, aesEncryptGCM, cleanMessage, Curve, decodeMediaRetryNode, decodeMessageNode, decryptMessageNode, delay, derivePairingCodeKey, encodeBigEndian, encodeSignedDeviceIdentity, extractAddressingContext, getCallStatusFromNode, getHistoryMsg, getNextPreKeys, getStatusFromReceiptType, handleIdentityChange, hkdf, MISSING_KEYS_ERROR_TEXT, NACK_REASONS, NO_MESSAGE_FOUND_ERROR_TEXT, toNumber, unixTimestampSeconds, xmppPreKey, xmppSignedPreKey } from '../Utils/index.js';
|
|
9
9
|
import { makeMutex } from '../Utils/make-mutex.js';
|
|
10
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';
|
|
@@ -13,7 +13,7 @@ import { makeMessagesSocket } from './messages-send.js';
|
|
|
13
13
|
export const makeMessagesRecvSocket = (config) => {
|
|
14
14
|
const { logger, retryRequestDelayMs, maxMsgRetryCount, getMessage, shouldIgnoreJid, enableAutoSessionRecreation } = config;
|
|
15
15
|
const sock = makeMessagesSocket(config);
|
|
16
|
-
const { ev, authState, ws,
|
|
16
|
+
const { ev, authState, ws, messageMutex, notificationMutex, receiptMutex, signalRepository, query, upsertMessage, resyncAppState, onUnexpectedError, assertSessions, sendNode, relayMessage, sendReceipt, uploadPreKeys, sendPeerDataOperationMessage, messageRetryManager } = sock;
|
|
17
17
|
/** this mutex ensures that each retryRequest will wait for the previous one to finish */
|
|
18
18
|
const retryMutex = makeMutex();
|
|
19
19
|
const msgRetryCache = config.msgRetryCounterCache ||
|
|
@@ -50,19 +50,21 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
50
50
|
};
|
|
51
51
|
return sendPeerDataOperationMessage(pdoMessage);
|
|
52
52
|
};
|
|
53
|
-
const requestPlaceholderResend = async (messageKey) => {
|
|
53
|
+
const requestPlaceholderResend = async (messageKey, msgData) => {
|
|
54
54
|
if (!authState.creds.me?.id) {
|
|
55
55
|
throw new Boom('Not authenticated');
|
|
56
56
|
}
|
|
57
|
-
if (placeholderResendCache.get(messageKey?.id)) {
|
|
57
|
+
if (await placeholderResendCache.get(messageKey?.id)) {
|
|
58
58
|
logger.debug({ messageKey }, 'already requested resend');
|
|
59
59
|
return;
|
|
60
60
|
}
|
|
61
61
|
else {
|
|
62
|
-
|
|
62
|
+
// Store original message data so PDO response handler can preserve
|
|
63
|
+
// metadata (LID details, timestamps, etc.) that the phone may omit
|
|
64
|
+
await placeholderResendCache.set(messageKey?.id, msgData || true);
|
|
63
65
|
}
|
|
64
|
-
await delay(
|
|
65
|
-
if (!placeholderResendCache.get(messageKey?.id)) {
|
|
66
|
+
await delay(2000);
|
|
67
|
+
if (!(await placeholderResendCache.get(messageKey?.id))) {
|
|
66
68
|
logger.debug({ messageKey }, 'message received while resend requested');
|
|
67
69
|
return 'RESOLVED';
|
|
68
70
|
}
|
|
@@ -75,11 +77,11 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
75
77
|
peerDataOperationRequestType: proto.Message.PeerDataOperationRequestType.PLACEHOLDER_MESSAGE_RESEND
|
|
76
78
|
};
|
|
77
79
|
setTimeout(async () => {
|
|
78
|
-
if (placeholderResendCache.get(messageKey?.id)) {
|
|
79
|
-
logger.debug({ messageKey }, 'PDO message without response after
|
|
80
|
+
if (await placeholderResendCache.get(messageKey?.id)) {
|
|
81
|
+
logger.debug({ messageKey }, 'PDO message without response after 8 seconds. Phone possibly offline');
|
|
80
82
|
await placeholderResendCache.del(messageKey?.id);
|
|
81
83
|
}
|
|
82
|
-
},
|
|
84
|
+
}, 8000);
|
|
83
85
|
return sendPeerDataOperationMessage(pdoMessage);
|
|
84
86
|
};
|
|
85
87
|
// Handles mex newsletter notifications
|
|
@@ -300,12 +302,12 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
300
302
|
// Check if we should recreate the session
|
|
301
303
|
let shouldRecreateSession = false;
|
|
302
304
|
let recreateReason = '';
|
|
303
|
-
if (enableAutoSessionRecreation && messageRetryManager) {
|
|
305
|
+
if (enableAutoSessionRecreation && messageRetryManager && retryCount > 1) {
|
|
304
306
|
try {
|
|
305
307
|
// Check if we have a session with this JID
|
|
306
308
|
const sessionId = signalRepository.jidToSignalProtocolAddress(fromJid);
|
|
307
309
|
const hasSession = await signalRepository.validateSession(fromJid);
|
|
308
|
-
const result = messageRetryManager.shouldRecreateSession(fromJid,
|
|
310
|
+
const result = messageRetryManager.shouldRecreateSession(fromJid, hasSession.exists);
|
|
309
311
|
shouldRecreateSession = result.recreate;
|
|
310
312
|
recreateReason = result.reason;
|
|
311
313
|
if (shouldRecreateSession) {
|
|
@@ -407,22 +409,15 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
407
409
|
}
|
|
408
410
|
}
|
|
409
411
|
else {
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
await assertSessions([from], true);
|
|
420
|
-
}
|
|
421
|
-
catch (error) {
|
|
422
|
-
logger.warn({ error, jid: from }, 'failed to assert sessions after identity change');
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
else {
|
|
412
|
+
const result = await handleIdentityChange(node, {
|
|
413
|
+
meId: authState.creds.me?.id,
|
|
414
|
+
meLid: authState.creds.me?.lid,
|
|
415
|
+
validateSession: signalRepository.validateSession,
|
|
416
|
+
assertSessions,
|
|
417
|
+
debounceCache: identityAssertDebounce,
|
|
418
|
+
logger
|
|
419
|
+
});
|
|
420
|
+
if (result.action === 'no_identity_node') {
|
|
426
421
|
logger.info({ node }, 'unknown encrypt notification');
|
|
427
422
|
}
|
|
428
423
|
}
|
|
@@ -592,6 +587,7 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
592
587
|
case 'picture':
|
|
593
588
|
const setPicture = getBinaryNodeChild(node, 'set');
|
|
594
589
|
const delPicture = getBinaryNodeChild(node, 'delete');
|
|
590
|
+
// TODO: WAJIDHASH stuff proper support inhouse
|
|
595
591
|
ev.emit('contacts.update', [
|
|
596
592
|
{
|
|
597
593
|
id: jidNormalizedUser(node?.attrs?.from) || (setPicture || delPicture)?.attrs?.hash || '',
|
|
@@ -644,7 +640,7 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
644
640
|
const companionSharedKey = Curve.sharedKey(authState.creds.pairingEphemeralKeyPair.private, codePairingPublicKey);
|
|
645
641
|
const random = randomBytes(32);
|
|
646
642
|
const linkCodeSalt = randomBytes(32);
|
|
647
|
-
const linkCodePairingExpanded =
|
|
643
|
+
const linkCodePairingExpanded = hkdf(companionSharedKey, 32, {
|
|
648
644
|
salt: linkCodeSalt,
|
|
649
645
|
info: 'link_code_pairing_key_bundle_encryption_key'
|
|
650
646
|
});
|
|
@@ -658,7 +654,7 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
658
654
|
const encryptedPayload = Buffer.concat([linkCodeSalt, encryptIv, encrypted]);
|
|
659
655
|
const identitySharedKey = Curve.sharedKey(authState.creds.signedIdentityKey.private, primaryIdentityPublicKey);
|
|
660
656
|
const identityPayload = Buffer.concat([companionSharedKey, identitySharedKey, random]);
|
|
661
|
-
authState.creds.advSecretKey = (
|
|
657
|
+
authState.creds.advSecretKey = Buffer.from(hkdf(identityPayload, 32, { info: 'adv_secret' })).toString('base64');
|
|
662
658
|
await query({
|
|
663
659
|
tag: 'iq',
|
|
664
660
|
attrs: {
|
|
@@ -789,11 +785,11 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
789
785
|
// Check if we should recreate session for this retry
|
|
790
786
|
let shouldRecreateSession = false;
|
|
791
787
|
let recreateReason = '';
|
|
792
|
-
if (enableAutoSessionRecreation && messageRetryManager) {
|
|
788
|
+
if (enableAutoSessionRecreation && messageRetryManager && retryCount > 1) {
|
|
793
789
|
try {
|
|
794
790
|
const sessionId = signalRepository.jidToSignalProtocolAddress(participant);
|
|
795
791
|
const hasSession = await signalRepository.validateSession(participant);
|
|
796
|
-
const result = messageRetryManager.shouldRecreateSession(participant,
|
|
792
|
+
const result = messageRetryManager.shouldRecreateSession(participant, hasSession.exists);
|
|
797
793
|
shouldRecreateSession = result.recreate;
|
|
798
794
|
recreateReason = result.reason;
|
|
799
795
|
if (shouldRecreateSession) {
|
|
@@ -856,7 +852,7 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
856
852
|
}
|
|
857
853
|
try {
|
|
858
854
|
await Promise.all([
|
|
859
|
-
|
|
855
|
+
receiptMutex.mutex(async () => {
|
|
860
856
|
const status = getStatusFromReceiptType(attrs.type);
|
|
861
857
|
if (typeof status !== 'undefined' &&
|
|
862
858
|
// basically, we only want to know when a message from us has been delivered to/read by the other person
|
|
@@ -877,7 +873,7 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
877
873
|
else {
|
|
878
874
|
ev.emit('messages.update', ids.map(id => ({
|
|
879
875
|
key: { ...key, id },
|
|
880
|
-
update: { status }
|
|
876
|
+
update: { status, messageTimestamp: toNumber(+(attrs.t ?? 0)) }
|
|
881
877
|
})));
|
|
882
878
|
}
|
|
883
879
|
}
|
|
@@ -920,7 +916,7 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
920
916
|
}
|
|
921
917
|
try {
|
|
922
918
|
await Promise.all([
|
|
923
|
-
|
|
919
|
+
notificationMutex.mutex(async () => {
|
|
924
920
|
const msg = await processNotification(node);
|
|
925
921
|
if (msg) {
|
|
926
922
|
const fromMe = areJidsSameUser(node.attrs.participant || remoteJid, authState.creds.me.id);
|
|
@@ -954,7 +950,7 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
954
950
|
}
|
|
955
951
|
const encNode = getBinaryNodeChild(node, 'enc');
|
|
956
952
|
// TODO: temporary fix for crashes and issues resulting of failed msmsg decryption
|
|
957
|
-
if (encNode
|
|
953
|
+
if (encNode?.attrs.type === 'msmsg') {
|
|
958
954
|
logger.debug({ key: node.attrs.key }, 'ignored msmsg');
|
|
959
955
|
await sendMessageAck(node, NACK_REASONS.MissingMessageSecret);
|
|
960
956
|
return;
|
|
@@ -984,58 +980,123 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
984
980
|
}, 'Added message to recent cache for retry receipts');
|
|
985
981
|
}
|
|
986
982
|
try {
|
|
987
|
-
await
|
|
983
|
+
await messageMutex.mutex(async () => {
|
|
988
984
|
await decrypt();
|
|
989
985
|
// message failed to decrypt
|
|
990
986
|
if (msg.messageStubType === proto.WebMessageInfo.StubType.CIPHERTEXT && msg.category !== 'peer') {
|
|
991
|
-
if (msg?.messageStubParameters?.[0] === MISSING_KEYS_ERROR_TEXT
|
|
992
|
-
|
|
993
|
-
return sendMessageAck(node);
|
|
987
|
+
if (msg?.messageStubParameters?.[0] === MISSING_KEYS_ERROR_TEXT) {
|
|
988
|
+
return sendMessageAck(node, NACK_REASONS.ParsingError);
|
|
994
989
|
}
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
990
|
+
if (msg.messageStubParameters?.[0] === NO_MESSAGE_FOUND_ERROR_TEXT) {
|
|
991
|
+
// Message arrived without encryption (e.g. CTWA ads messages).
|
|
992
|
+
// Check if this is eligible for placeholder resend (matching WA Web filters).
|
|
993
|
+
const unavailableNode = getBinaryNodeChild(node, 'unavailable');
|
|
994
|
+
const unavailableType = unavailableNode?.attrs?.type;
|
|
995
|
+
if (unavailableType === 'bot_unavailable_fanout' ||
|
|
996
|
+
unavailableType === 'hosted_unavailable_fanout' ||
|
|
997
|
+
unavailableType === 'view_once_unavailable_fanout') {
|
|
998
|
+
logger.debug({ msgId: msg.key.id, unavailableType }, 'skipping placeholder resend for excluded unavailable type');
|
|
999
|
+
return sendMessageAck(node);
|
|
1000
|
+
}
|
|
1001
|
+
const messageAge = unixTimestampSeconds() - toNumber(msg.messageTimestamp);
|
|
1002
|
+
if (messageAge > PLACEHOLDER_MAX_AGE_SECONDS) {
|
|
1003
|
+
logger.debug({ msgId: msg.key.id, messageAge }, 'skipping placeholder resend for old message');
|
|
1004
|
+
return sendMessageAck(node);
|
|
1005
|
+
}
|
|
1006
|
+
// Request the real content from the phone via placeholder resend PDO.
|
|
1007
|
+
// Upsert the CIPHERTEXT stub as a placeholder (like WA Web's processPlaceholderMsg),
|
|
1008
|
+
// and store the requestId in stubParameters[1] so users can correlate
|
|
1009
|
+
// with the incoming PDO response event.
|
|
1010
|
+
const cleanKey = {
|
|
1011
|
+
remoteJid: msg.key.remoteJid,
|
|
1012
|
+
fromMe: msg.key.fromMe,
|
|
1013
|
+
id: msg.key.id,
|
|
1014
|
+
participant: msg.key.participant
|
|
1015
|
+
};
|
|
1016
|
+
// Cache the original message metadata so the PDO response handler
|
|
1017
|
+
// can preserve key fields (LID details etc.) that the phone may omit
|
|
1018
|
+
const msgData = {
|
|
1019
|
+
key: msg.key,
|
|
1020
|
+
messageTimestamp: msg.messageTimestamp,
|
|
1021
|
+
pushName: msg.pushName,
|
|
1022
|
+
participant: msg.participant,
|
|
1023
|
+
verifiedBizName: msg.verifiedBizName
|
|
1024
|
+
};
|
|
1025
|
+
requestPlaceholderResend(cleanKey, msgData)
|
|
1026
|
+
.then(requestId => {
|
|
1027
|
+
if (requestId && requestId !== 'RESOLVED') {
|
|
1028
|
+
logger.debug({ msgId: msg.key.id, requestId }, 'requested placeholder resend for unavailable message');
|
|
1029
|
+
ev.emit('messages.update', [
|
|
1030
|
+
{
|
|
1031
|
+
key: msg.key,
|
|
1032
|
+
update: { messageStubParameters: [NO_MESSAGE_FOUND_ERROR_TEXT, requestId] }
|
|
1033
|
+
}
|
|
1034
|
+
]);
|
|
1017
1035
|
}
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1036
|
+
})
|
|
1037
|
+
.catch(err => {
|
|
1038
|
+
logger.warn({ err, msgId: msg.key.id }, 'failed to request placeholder resend for unavailable message');
|
|
1039
|
+
});
|
|
1040
|
+
await sendMessageAck(node);
|
|
1041
|
+
// Don't return — fall through to upsertMessage so the stub is emitted
|
|
1042
|
+
}
|
|
1043
|
+
else {
|
|
1044
|
+
// Skip retry for expired status messages (>24h old)
|
|
1045
|
+
if (isJidStatusBroadcast(msg.key.remoteJid)) {
|
|
1046
|
+
const messageAge = unixTimestampSeconds() - toNumber(msg.messageTimestamp);
|
|
1047
|
+
if (messageAge > STATUS_EXPIRY_SECONDS) {
|
|
1048
|
+
logger.debug({ msgId: msg.key.id, messageAge, remoteJid: msg.key.remoteJid }, 'skipping retry for expired status message');
|
|
1049
|
+
return sendMessageAck(node);
|
|
1022
1050
|
}
|
|
1023
1051
|
}
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1052
|
+
const errorMessage = msg?.messageStubParameters?.[0] || '';
|
|
1053
|
+
const isPreKeyError = errorMessage.includes('PreKey');
|
|
1054
|
+
logger.debug(`[handleMessage] Attempting retry request for failed decryption`);
|
|
1055
|
+
// Handle both pre-key and normal retries in single mutex
|
|
1056
|
+
await retryMutex.mutex(async () => {
|
|
1027
1057
|
try {
|
|
1058
|
+
if (!ws.isOpen) {
|
|
1059
|
+
logger.debug({ node }, 'Connection closed, skipping retry');
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
// Handle pre-key errors with upload and delay
|
|
1063
|
+
if (isPreKeyError) {
|
|
1064
|
+
logger.info({ error: errorMessage }, 'PreKey error detected, uploading and retrying');
|
|
1065
|
+
try {
|
|
1066
|
+
logger.debug('Uploading pre-keys for error recovery');
|
|
1067
|
+
await uploadPreKeys(5);
|
|
1068
|
+
logger.debug('Waiting for server to process new pre-keys');
|
|
1069
|
+
await delay(1000);
|
|
1070
|
+
}
|
|
1071
|
+
catch (uploadErr) {
|
|
1072
|
+
logger.error({ uploadErr }, 'Pre-key upload failed, proceeding with retry anyway');
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1028
1075
|
const encNode = getBinaryNodeChild(node, 'enc');
|
|
1029
1076
|
await sendRetryRequest(node, !encNode);
|
|
1077
|
+
if (retryRequestDelayMs) {
|
|
1078
|
+
await delay(retryRequestDelayMs);
|
|
1079
|
+
}
|
|
1030
1080
|
}
|
|
1031
|
-
catch (
|
|
1032
|
-
logger.error({
|
|
1081
|
+
catch (err) {
|
|
1082
|
+
logger.error({ err, isPreKeyError }, 'Failed to handle retry, attempting basic retry');
|
|
1083
|
+
// Still attempt retry even if pre-key upload failed
|
|
1084
|
+
try {
|
|
1085
|
+
const encNode = getBinaryNodeChild(node, 'enc');
|
|
1086
|
+
await sendRetryRequest(node, !encNode);
|
|
1087
|
+
}
|
|
1088
|
+
catch (retryErr) {
|
|
1089
|
+
logger.error({ retryErr }, 'Failed to send retry after error handling');
|
|
1090
|
+
}
|
|
1033
1091
|
}
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
}
|
|
1092
|
+
await sendMessageAck(node, NACK_REASONS.UnhandledError);
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1037
1095
|
}
|
|
1038
1096
|
else {
|
|
1097
|
+
if (messageRetryManager && msg.key.id) {
|
|
1098
|
+
messageRetryManager.cancelPendingPhoneRequest(msg.key.id);
|
|
1099
|
+
}
|
|
1039
1100
|
const isNewsletter = isJidNewsletter(msg.key.remoteJid);
|
|
1040
1101
|
if (!isNewsletter) {
|
|
1041
1102
|
// no type in the receipt => message delivered
|
|
@@ -1089,6 +1150,7 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
1089
1150
|
const call = {
|
|
1090
1151
|
chatId: attrs.from,
|
|
1091
1152
|
from,
|
|
1153
|
+
callerPn: infoChild.attrs['caller_pn'],
|
|
1092
1154
|
id: callId,
|
|
1093
1155
|
date: new Date(+attrs.t * 1000),
|
|
1094
1156
|
offline: !!attrs.offline,
|
|
@@ -1105,6 +1167,7 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
1105
1167
|
if (existingCall) {
|
|
1106
1168
|
call.isVideo = existingCall.isVideo;
|
|
1107
1169
|
call.isGroup = existingCall.isGroup;
|
|
1170
|
+
call.callerPn = call.callerPn || existingCall.callerPn;
|
|
1108
1171
|
}
|
|
1109
1172
|
// delete data once call has ended
|
|
1110
1173
|
if (status === 'reject' || status === 'accept' || status === 'timeout' || status === 'terminate') {
|
|
@@ -1166,6 +1229,10 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
1166
1229
|
return exec(node, false).catch(err => onUnexpectedError(err, identifier));
|
|
1167
1230
|
}
|
|
1168
1231
|
};
|
|
1232
|
+
/** Yields control to the event loop to prevent blocking */
|
|
1233
|
+
const yieldToEventLoop = () => {
|
|
1234
|
+
return new Promise(resolve => setImmediate(resolve));
|
|
1235
|
+
};
|
|
1169
1236
|
const makeOfflineNodeProcessor = () => {
|
|
1170
1237
|
const nodeProcessorMap = new Map([
|
|
1171
1238
|
['message', handleMessage],
|
|
@@ -1175,6 +1242,8 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
1175
1242
|
]);
|
|
1176
1243
|
const nodes = [];
|
|
1177
1244
|
let isProcessing = false;
|
|
1245
|
+
// Number of nodes to process before yielding to event loop
|
|
1246
|
+
const BATCH_SIZE = 10;
|
|
1178
1247
|
const enqueue = (type, node) => {
|
|
1179
1248
|
nodes.push({ type, node });
|
|
1180
1249
|
if (isProcessing) {
|
|
@@ -1182,6 +1251,7 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
1182
1251
|
}
|
|
1183
1252
|
isProcessing = true;
|
|
1184
1253
|
const promise = async () => {
|
|
1254
|
+
let processedInBatch = 0;
|
|
1185
1255
|
while (nodes.length && ws.isOpen) {
|
|
1186
1256
|
const { type, node } = nodes.shift();
|
|
1187
1257
|
const nodeProcessor = nodeProcessorMap.get(type);
|
|
@@ -1190,6 +1260,13 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
1190
1260
|
continue;
|
|
1191
1261
|
}
|
|
1192
1262
|
await nodeProcessor(node);
|
|
1263
|
+
processedInBatch++;
|
|
1264
|
+
// Yield to event loop after processing a batch
|
|
1265
|
+
// This prevents blocking the event loop for too long when there are many offline nodes
|
|
1266
|
+
if (processedInBatch >= BATCH_SIZE) {
|
|
1267
|
+
processedInBatch = 0;
|
|
1268
|
+
await yieldToEventLoop();
|
|
1269
|
+
}
|
|
1193
1270
|
}
|
|
1194
1271
|
isProcessing = false;
|
|
1195
1272
|
};
|