@itsliaaa/baileys 0.1.32 → 0.2.0
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/README.md +328 -74
- package/WAProto/index.js +22 -18
- package/lib/Defaults/index.js +2 -0
- package/lib/Socket/chats.js +223 -57
- package/lib/Socket/groups.js +6 -0
- package/lib/Socket/messages-recv.js +230 -53
- package/lib/Socket/messages-send.js +78 -7
- package/lib/Utils/chat-utils.js +34 -7
- package/lib/Utils/decode-wa-message.js +14 -0
- package/lib/Utils/event-buffer.js +2 -0
- package/lib/Utils/generics.js +9 -0
- package/lib/Utils/history.js +11 -9
- package/lib/Utils/identity-change-handler.js +1 -0
- package/lib/Utils/messages-media.js +1 -1
- package/lib/Utils/messages.js +21 -6
- package/lib/Utils/process-message.js +53 -1
- package/lib/Utils/rich-message-utils.js +40 -35
- package/lib/Utils/sync-action-utils.js +1 -0
- package/lib/Utils/tc-token-utils.js +151 -5
- package/lib/Utils/use-single-file-auth-state.js +19 -26
- package/lib/WAUSync/Protocols/USyncContactProtocol.js +26 -3
- package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +22 -0
- package/lib/WAUSync/Protocols/index.js +2 -1
- package/lib/WAUSync/USyncQuery.js +5 -1
- package/lib/WAUSync/USyncUser.js +8 -0
- package/package.json +2 -2
|
@@ -5,17 +5,19 @@ 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 { WAMessageStatus, WAMessageStubType } from '../Types/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';
|
|
8
|
+
import { aesDecryptCTR, aesEncryptGCM, cleanMessage, Curve, decodeMediaRetryNode, decodeMessageNode, decryptMessageNode, delay, derivePairingCodeKey, encodeBigEndian, encodeSignedDeviceIdentity, extractAddressingContext, getCallStatusFromNode, getHistoryMsg, getNextPreKeys, getStatusFromReceiptType, handleIdentityChange, hkdf, MISSING_KEYS_ERROR_TEXT, SERVER_ERROR_CODES, 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 { makeOfflineNodeProcessor } from '../Utils/offline-node-processor.js';
|
|
11
11
|
import { buildAckStanza } from '../Utils/stanza-ack.js';
|
|
12
|
+
import { buildMergedTcTokenIndexWrite, isTcTokenExpired, readTcTokenIndex, resolveIssuanceJid, resolveTcTokenJid, storeTcTokensFromIqResult, TC_TOKEN_INDEX_KEY } from '../Utils/tc-token-utils.js';
|
|
12
13
|
import { areJidsSameUser, binaryNodeToString, getAllBinaryNodeChildren, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildren, getBinaryNodeChildString, isJidGroup, isJidNewsletter, isJidStatusBroadcast, isLidUser, isPnUser, jidDecode, jidNormalizedUser, S_WHATSAPP_NET } from '../WABinary/index.js';
|
|
13
14
|
import { extractGroupMetadata } from './groups.js';
|
|
14
15
|
import { makeMessagesSocket } from './messages-send.js';
|
|
15
16
|
export const makeMessagesRecvSocket = (config) => {
|
|
16
17
|
const { logger, retryRequestDelayMs, maxMsgRetryCount, getMessage, shouldIgnoreJid, enableAutoSessionRecreation } = config;
|
|
17
18
|
const sock = makeMessagesSocket(config);
|
|
18
|
-
const { ev, authState, ws, messageMutex, notificationMutex, receiptMutex, signalRepository, query, upsertMessage, resyncAppState, onUnexpectedError, assertSessions, sendNode, relayMessage, sendReceipt, uploadPreKeys, sendPeerDataOperationMessage, generateMessageTag, messageRetryManager, registerSocketEndHandler } = sock;
|
|
19
|
+
const { ev, authState, ws, messageMutex, notificationMutex, receiptMutex, signalRepository, query, upsertMessage, resyncAppState, onUnexpectedError, assertSessions, sendNode, relayMessage, sendReceipt, uploadPreKeys, sendPeerDataOperationMessage, generateMessageTag, messageRetryManager, issuePrivacyTokens, registerSocketEndHandler } = sock;
|
|
20
|
+
const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping);
|
|
19
21
|
/** this mutex ensures that each retryRequest will wait for the previous one to finish */
|
|
20
22
|
const retryMutex = makeMutex();
|
|
21
23
|
const devicesMutex = makeMutex();
|
|
@@ -473,6 +475,40 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
473
475
|
logger.info({ msgAttrs: node.attrs, retryCount }, 'sent retry receipt');
|
|
474
476
|
}, authState?.creds?.me?.id || 'sendRetryRequest');
|
|
475
477
|
};
|
|
478
|
+
/**
|
|
479
|
+
* Fire-and-forget tctoken re-issuance after a peer's device identity changed.
|
|
480
|
+
* Mirrors WAWebSendTcTokenWhenDeviceIdentityChange — runs in parallel with
|
|
481
|
+
* the session refresh (not after it).
|
|
482
|
+
*/
|
|
483
|
+
const reissueTcTokenAfterIdentityChange = (from) => {
|
|
484
|
+
void (async () => {
|
|
485
|
+
const normalizedJid = jidNormalizedUser(from);
|
|
486
|
+
const tcJid = await resolveTcTokenJid(normalizedJid, getLIDForPN);
|
|
487
|
+
const tcTokenData = await authState.keys.get('tctoken', [tcJid]);
|
|
488
|
+
const senderTs = tcTokenData?.[tcJid]?.senderTimestamp;
|
|
489
|
+
if (senderTs === null || senderTs === undefined || isTcTokenExpired(senderTs)) {
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
logger.debug({ jid: normalizedJid, senderTimestamp: senderTs }, 'identity changed, re-issuing tctoken');
|
|
493
|
+
const getPNForLID = signalRepository.lidMapping.getPNForLID.bind(signalRepository.lidMapping);
|
|
494
|
+
const issueJid = await resolveIssuanceJid(
|
|
495
|
+
normalizedJid,
|
|
496
|
+
sock.serverProps.lidTrustedTokenIssueToLid,
|
|
497
|
+
getLIDForPN,
|
|
498
|
+
getPNForLID
|
|
499
|
+
);
|
|
500
|
+
const result = await issuePrivacyTokens([issueJid], senderTs);
|
|
501
|
+
await storeTcTokensFromIqResult({
|
|
502
|
+
result,
|
|
503
|
+
fallbackJid: tcJid,
|
|
504
|
+
keys: authState.keys,
|
|
505
|
+
getLIDForPN,
|
|
506
|
+
onNewJidStored: trackTcTokenJid
|
|
507
|
+
});
|
|
508
|
+
})().catch(err => {
|
|
509
|
+
logger.debug({ jid: from, err: err?.message }, 'failed to re-issue tctoken after identity change');
|
|
510
|
+
});
|
|
511
|
+
};
|
|
476
512
|
const handleEncryptNotification = async (node) => {
|
|
477
513
|
const from = node.attrs.from;
|
|
478
514
|
if (from === S_WHATSAPP_NET) {
|
|
@@ -491,7 +527,8 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
491
527
|
validateSession: signalRepository.validateSession,
|
|
492
528
|
assertSessions,
|
|
493
529
|
debounceCache: identityAssertDebounce,
|
|
494
|
-
logger
|
|
530
|
+
logger,
|
|
531
|
+
onBeforeSessionRefresh: reissueTcTokenAfterIdentityChange
|
|
495
532
|
});
|
|
496
533
|
if (result.action === 'no_identity_node') {
|
|
497
534
|
logger.info({ node }, 'unknown encrypt notification');
|
|
@@ -502,6 +539,7 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
502
539
|
// TODO: Support PN/LID (Here is only LID now)
|
|
503
540
|
const actingParticipantLid = fullNode.attrs.participant;
|
|
504
541
|
const actingParticipantPn = fullNode.attrs.participant_pn;
|
|
542
|
+
const actingParticipantUsername = fullNode.attrs.participant_username;
|
|
505
543
|
const affectedParticipantLid = getBinaryNodeChild(child, 'participant')?.attrs?.jid || actingParticipantLid;
|
|
506
544
|
const affectedParticipantPn = getBinaryNodeChild(child, 'participant')?.attrs?.phone_number || actingParticipantPn;
|
|
507
545
|
switch (child?.tag) {
|
|
@@ -521,7 +559,8 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
521
559
|
{
|
|
522
560
|
...metadata,
|
|
523
561
|
author: actingParticipantLid,
|
|
524
|
-
authorPn: actingParticipantPn
|
|
562
|
+
authorPn: actingParticipantPn,
|
|
563
|
+
authorUsername: actingParticipantUsername
|
|
525
564
|
}
|
|
526
565
|
]);
|
|
527
566
|
break;
|
|
@@ -552,6 +591,7 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
552
591
|
id: attrs.jid,
|
|
553
592
|
phoneNumber: isLidUser(attrs.jid) && isPnUser(attrs.phone_number) ? attrs.phone_number : undefined,
|
|
554
593
|
lid: isPnUser(attrs.jid) && isLidUser(attrs.lid) ? attrs.lid : undefined,
|
|
594
|
+
username: attrs.participant_username || attrs.username || undefined,
|
|
555
595
|
admin: (attrs.type || null)
|
|
556
596
|
};
|
|
557
597
|
});
|
|
@@ -834,27 +874,70 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
834
874
|
return result;
|
|
835
875
|
}
|
|
836
876
|
};
|
|
877
|
+
/**
|
|
878
|
+
* In-memory cache of storage JIDs with stored tctokens, seeded from the persisted index.
|
|
879
|
+
* Used to coalesce writes during a session; pruning always re-reads the persisted index
|
|
880
|
+
* to cover writes made by other layers (e.g. history sync).
|
|
881
|
+
*/
|
|
882
|
+
const tcTokenKnownJids = new Set();
|
|
883
|
+
const tcTokenIndexLoaded = (async () => {
|
|
884
|
+
try {
|
|
885
|
+
const jids = await readTcTokenIndex(authState.keys);
|
|
886
|
+
for (const jid of jids) {
|
|
887
|
+
tcTokenKnownJids.add(jid);
|
|
888
|
+
}
|
|
889
|
+
logger.debug({ count: tcTokenKnownJids.size }, 'loaded tctoken index');
|
|
890
|
+
}
|
|
891
|
+
catch (err) {
|
|
892
|
+
logger.warn({ err: err?.message }, 'failed to load tctoken index');
|
|
893
|
+
}
|
|
894
|
+
})();
|
|
895
|
+
let tcTokenIndexTimer;
|
|
896
|
+
async function flushTcTokenIndex() {
|
|
897
|
+
if (tcTokenIndexTimer) {
|
|
898
|
+
clearTimeout(tcTokenIndexTimer);
|
|
899
|
+
tcTokenIndexTimer = undefined;
|
|
900
|
+
}
|
|
901
|
+
// Merge with whatever is already persisted so we don't clobber writes from other
|
|
902
|
+
// paths (history sync, concurrent sessions on the same store).
|
|
903
|
+
const write = await buildMergedTcTokenIndexWrite(authState.keys, tcTokenKnownJids);
|
|
904
|
+
return authState.keys.set({ tctoken: write });
|
|
905
|
+
};
|
|
906
|
+
function scheduleTcTokenIndexSave() {
|
|
907
|
+
if (tcTokenIndexTimer) {
|
|
908
|
+
clearTimeout(tcTokenIndexTimer);
|
|
909
|
+
}
|
|
910
|
+
tcTokenIndexTimer = setTimeout(() => {
|
|
911
|
+
tcTokenIndexTimer = undefined;
|
|
912
|
+
flushTcTokenIndex().catch(err => {
|
|
913
|
+
logger.warn({ err: err?.message }, 'failed to save tctoken index');
|
|
914
|
+
});
|
|
915
|
+
}, 5000);
|
|
916
|
+
};
|
|
917
|
+
function trackTcTokenJid(jid) {
|
|
918
|
+
if (jid && jid !== TC_TOKEN_INDEX_KEY && !tcTokenKnownJids.has(jid)) {
|
|
919
|
+
tcTokenKnownJids.add(jid);
|
|
920
|
+
scheduleTcTokenIndexSave();
|
|
921
|
+
}
|
|
922
|
+
}
|
|
837
923
|
const handlePrivacyTokenNotification = async (node) => {
|
|
838
924
|
const tokensNode = getBinaryNodeChild(node, 'tokens');
|
|
925
|
+
if (!tokensNode) return;
|
|
839
926
|
const from = jidNormalizedUser(node.attrs.from);
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
const
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
tctoken: { [from]: { token: content, timestamp } }
|
|
855
|
-
});
|
|
856
|
-
}
|
|
857
|
-
}
|
|
927
|
+
// WA Web uses: senderLid ?? toLid(from) for the storage key
|
|
928
|
+
// The sender_lid attribute provides the LID directly when available
|
|
929
|
+
const senderLid = node.attrs.sender_lid && isLidUser(jidNormalizedUser(node.attrs.sender_lid))
|
|
930
|
+
? jidNormalizedUser(node.attrs.sender_lid)
|
|
931
|
+
: undefined;
|
|
932
|
+
const fallbackJid = senderLid ?? (await resolveTcTokenJid(from, getLIDForPN));
|
|
933
|
+
logger.debug({ from, storageJid: fallbackJid }, 'processing privacy token notification');
|
|
934
|
+
await storeTcTokensFromIqResult({
|
|
935
|
+
result: node,
|
|
936
|
+
fallbackJid,
|
|
937
|
+
keys: authState.keys,
|
|
938
|
+
getLIDForPN,
|
|
939
|
+
onNewJidStored: trackTcTokenJid
|
|
940
|
+
});
|
|
858
941
|
};
|
|
859
942
|
async function decipherLinkPublicKey(data) {
|
|
860
943
|
const buffer = toRequiredBuffer(data);
|
|
@@ -973,11 +1056,6 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
973
1056
|
fromMe,
|
|
974
1057
|
participant: attrs.participant
|
|
975
1058
|
};
|
|
976
|
-
if (shouldIgnoreJid(remoteJid) && remoteJid !== S_WHATSAPP_NET) {
|
|
977
|
-
logger.debug({ remoteJid }, 'ignoring receipt from jid');
|
|
978
|
-
await sendMessageAck(node);
|
|
979
|
-
return;
|
|
980
|
-
}
|
|
981
1059
|
const ids = [attrs.id];
|
|
982
1060
|
if (Array.isArray(content)) {
|
|
983
1061
|
const items = getBinaryNodeChildren(content[0], 'item');
|
|
@@ -1042,11 +1120,6 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
1042
1120
|
};
|
|
1043
1121
|
const handleNotification = async (node) => {
|
|
1044
1122
|
const remoteJid = node.attrs.from;
|
|
1045
|
-
if (shouldIgnoreJid(remoteJid) && remoteJid !== S_WHATSAPP_NET) {
|
|
1046
|
-
logger.debug({ remoteJid, id: node.attrs.id }, 'ignored notification');
|
|
1047
|
-
await sendMessageAck(node);
|
|
1048
|
-
return;
|
|
1049
|
-
}
|
|
1050
1123
|
try {
|
|
1051
1124
|
await Promise.all([
|
|
1052
1125
|
notificationMutex.mutex(async () => {
|
|
@@ -1059,6 +1132,7 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
1059
1132
|
fromMe,
|
|
1060
1133
|
participant: node.attrs.participant,
|
|
1061
1134
|
participantAlt,
|
|
1135
|
+
username: attrs.participant_username || attrs.username || undefined,
|
|
1062
1136
|
addressingMode,
|
|
1063
1137
|
id: node.attrs.id,
|
|
1064
1138
|
...(msg.key || {})
|
|
@@ -1076,11 +1150,6 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
1076
1150
|
}
|
|
1077
1151
|
};
|
|
1078
1152
|
const handleMessage = async (node) => {
|
|
1079
|
-
if (shouldIgnoreJid(node.attrs.from) && node.attrs.from !== S_WHATSAPP_NET) {
|
|
1080
|
-
logger.debug({ key: node.attrs.key }, 'ignored message');
|
|
1081
|
-
await sendMessageAck(node, NACK_REASONS.UnhandledError);
|
|
1082
|
-
return;
|
|
1083
|
-
}
|
|
1084
1153
|
const encNode = getBinaryNodeChild(node, 'enc');
|
|
1085
1154
|
// TODO: temporary fix for crashes and issues resulting of failed msmsg decryption
|
|
1086
1155
|
if (encNode?.attrs.type === 'msmsg') {
|
|
@@ -1298,6 +1367,13 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
1298
1367
|
offline: !!attrs.offline,
|
|
1299
1368
|
status
|
|
1300
1369
|
};
|
|
1370
|
+
if (status === 'relaylatency') {
|
|
1371
|
+
const latencyValue = infoChild.attrs.latency || infoChild.attrs['latency_ms'] || infoChild.attrs['latency-ms'];
|
|
1372
|
+
const latencyMs = latencyValue ? Number(latencyValue) : undefined;
|
|
1373
|
+
if (Number.isFinite(latencyMs)) {
|
|
1374
|
+
call.latencyMs = latencyMs;
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1301
1377
|
if (status === 'offer') {
|
|
1302
1378
|
call.isVideo = !!getBinaryNodeChild(infoChild, 'video');
|
|
1303
1379
|
call.isGroup = infoChild.attrs.type === 'group' || !!infoChild.attrs['group-jid'];
|
|
@@ -1342,7 +1418,19 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
1342
1418
|
// error in acknowledgement,
|
|
1343
1419
|
// device could not display the message
|
|
1344
1420
|
if (attrs.error) {
|
|
1345
|
-
|
|
1421
|
+
if (attrs.error === SERVER_ERROR_CODES.MissingTcToken) {
|
|
1422
|
+
// 463 = account restricted + no tctoken for this contact.
|
|
1423
|
+
// WA Web prevents this client-side (disables compose bar).
|
|
1424
|
+
// No retry — retrying worsens the restriction by counting
|
|
1425
|
+
// as another "reach out" to an unknown contact.
|
|
1426
|
+
logger.warn({ msgId: attrs.id, from: attrs.from }, 'error 463: account restricted or missing tctoken for contact');
|
|
1427
|
+
}
|
|
1428
|
+
else if (attrs.error === SERVER_ERROR_CODES.SmaxInvalid) {
|
|
1429
|
+
logger.warn({ msgId: attrs.id, from: attrs.from }, 'smax-invalid (479): stanza rejected by server — likely stale device session or malformed addressing');
|
|
1430
|
+
}
|
|
1431
|
+
else {
|
|
1432
|
+
logger.warn({ attrs }, 'received error in ack');
|
|
1433
|
+
}
|
|
1346
1434
|
ev.emit('messages.update', [
|
|
1347
1435
|
{
|
|
1348
1436
|
key,
|
|
@@ -1352,19 +1440,6 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
1352
1440
|
}
|
|
1353
1441
|
}
|
|
1354
1442
|
]);
|
|
1355
|
-
// resend the message with device_fanout=false, use at your own risk
|
|
1356
|
-
// if (attrs.error === '475') {
|
|
1357
|
-
// const msg = await getMessage(key)
|
|
1358
|
-
// if (msg) {
|
|
1359
|
-
// await relayMessage(key.remoteJid!, msg, {
|
|
1360
|
-
// messageId: key.id!,
|
|
1361
|
-
// useUserDevicesCache: false,
|
|
1362
|
-
// additionalAttributes: {
|
|
1363
|
-
// device_fanout: 'false'
|
|
1364
|
-
// }
|
|
1365
|
-
// })
|
|
1366
|
-
// }
|
|
1367
|
-
// }
|
|
1368
1443
|
}
|
|
1369
1444
|
};
|
|
1370
1445
|
/// processes a node with the given function
|
|
@@ -1388,6 +1463,22 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
1388
1463
|
yieldToEventLoop: () => new Promise(resolve => setImmediate(resolve))
|
|
1389
1464
|
});
|
|
1390
1465
|
const processNode = async (type, node, identifier, exec) => {
|
|
1466
|
+
// Fast path: ack and drop ignored JIDs before entering the buffer/queue
|
|
1467
|
+
const from = node.attrs.from;
|
|
1468
|
+
let ignoreJid = from;
|
|
1469
|
+
if (type === 'receipt' && from) {
|
|
1470
|
+
const attrs = node.attrs;
|
|
1471
|
+
const isLid = attrs.from.includes('lid');
|
|
1472
|
+
const isNodeFromMe = areJidsSameUser(
|
|
1473
|
+
attrs.participant || attrs.from,
|
|
1474
|
+
isLid ? authState.creds.me?.lid : authState.creds.me?.id
|
|
1475
|
+
);
|
|
1476
|
+
ignoreJid = !isNodeFromMe || isJidGroup(attrs.from) ? attrs.from : attrs.recipient;
|
|
1477
|
+
}
|
|
1478
|
+
if (ignoreJid && ignoreJid !== S_WHATSAPP_NET && shouldIgnoreJid(ignoreJid)) {
|
|
1479
|
+
await sendMessageAck(node, type === 'message' ? NACK_REASONS.UnhandledError : undefined);
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1391
1482
|
const isOffline = !!node.attrs.offline;
|
|
1392
1483
|
if (isOffline) {
|
|
1393
1484
|
offlineNodeProcessor.enqueue(type, node);
|
|
@@ -1443,12 +1534,98 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
1443
1534
|
await upsertMessage(protoMsg, call.offline ? 'append' : 'notify');
|
|
1444
1535
|
}
|
|
1445
1536
|
});
|
|
1446
|
-
|
|
1537
|
+
/** timestamp of last tctoken prune run — throttles to once per 24h */
|
|
1538
|
+
let lastTcTokenPruneTs = 0;
|
|
1539
|
+
ev.on('connection.update', ({ isOnline, connection }) => {
|
|
1447
1540
|
if (typeof isOnline !== 'undefined') {
|
|
1448
1541
|
sendActiveReceipts = isOnline;
|
|
1449
1542
|
logger.trace(`sendActiveReceipts set to "${sendActiveReceipts}"`);
|
|
1450
1543
|
}
|
|
1451
|
-
|
|
1544
|
+
// Flush pending tctoken index save on disconnect to avoid writing after close
|
|
1545
|
+
if (connection === 'close' && tcTokenIndexTimer) {
|
|
1546
|
+
clearTimeout(tcTokenIndexTimer);
|
|
1547
|
+
tcTokenIndexTimer = undefined;
|
|
1548
|
+
// Best-effort flush — may fail if store is already closed
|
|
1549
|
+
try {
|
|
1550
|
+
void Promise.resolve(flushTcTokenIndex()).catch(() => {});
|
|
1551
|
+
}
|
|
1552
|
+
catch {
|
|
1553
|
+
/* ignore sync errors */
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
// Prune expired tctokens when coming online, at most once per 24 hours
|
|
1557
|
+
// Matches WA Web's CLEAN_TC_TOKENS task
|
|
1558
|
+
// Note: don't gate on tcTokenKnownJids.size — the index may still be loading
|
|
1559
|
+
if (isOnline) {
|
|
1560
|
+
const now = Date.now();
|
|
1561
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
1562
|
+
if (now - lastTcTokenPruneTs >= DAY_MS) {
|
|
1563
|
+
lastTcTokenPruneTs = now;
|
|
1564
|
+
void pruneExpiredTcTokens();
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
})
|
|
1568
|
+
async function pruneExpiredTcTokens() {
|
|
1569
|
+
try {
|
|
1570
|
+
await tcTokenIndexLoaded;
|
|
1571
|
+
// Union with the persisted index picks up JIDs added by other layers
|
|
1572
|
+
// (history sync) without needing inter-module wiring.
|
|
1573
|
+
const persisted = await readTcTokenIndex(authState.keys);
|
|
1574
|
+
const allJids = new Set(tcTokenKnownJids);
|
|
1575
|
+
for (const jid of persisted) {
|
|
1576
|
+
allJids.add(jid);
|
|
1577
|
+
}
|
|
1578
|
+
if (!allJids.size) return;
|
|
1579
|
+
const jids = [...allJids];
|
|
1580
|
+
const allTokens = await authState.keys.get('tctoken', jids);
|
|
1581
|
+
const writes = {};
|
|
1582
|
+
const survivors = new Set();
|
|
1583
|
+
let mutated = 0;
|
|
1584
|
+
for (const jid of jids) {
|
|
1585
|
+
const entry = allTokens[jid];
|
|
1586
|
+
if (!entry) {
|
|
1587
|
+
// Tracked but nothing in store — drop from index.
|
|
1588
|
+
mutated++;
|
|
1589
|
+
continue;
|
|
1590
|
+
}
|
|
1591
|
+
const hasPeerToken = !!entry.token?.length;
|
|
1592
|
+
const peerTokenExpired = hasPeerToken && isTcTokenExpired(entry.timestamp);
|
|
1593
|
+
const hasSenderTs = entry.senderTimestamp !== undefined;
|
|
1594
|
+
const senderTsExpired = hasSenderTs && isTcTokenExpired(entry.senderTimestamp);
|
|
1595
|
+
const keepPeerToken = hasPeerToken && !peerTokenExpired;
|
|
1596
|
+
const keepSenderTs = hasSenderTs && !senderTsExpired;
|
|
1597
|
+
if (!keepPeerToken && !keepSenderTs) {
|
|
1598
|
+
writes[jid] = null;
|
|
1599
|
+
mutated++;
|
|
1600
|
+
}
|
|
1601
|
+
else if (peerTokenExpired && keepSenderTs) {
|
|
1602
|
+
writes[jid] = { token: Buffer.alloc(0), senderTimestamp: entry.senderTimestamp };
|
|
1603
|
+
survivors.add(jid);
|
|
1604
|
+
mutated++;
|
|
1605
|
+
}
|
|
1606
|
+
else {
|
|
1607
|
+
survivors.add(jid);
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
if (mutated === 0) return;
|
|
1611
|
+
await authState.keys.set({
|
|
1612
|
+
tctoken: {
|
|
1613
|
+
...writes,
|
|
1614
|
+
[TC_TOKEN_INDEX_KEY]: {
|
|
1615
|
+
token: Buffer.from(JSON.stringify([...survivors]))
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
});
|
|
1619
|
+
tcTokenKnownJids.clear();
|
|
1620
|
+
for (const jid of survivors) {
|
|
1621
|
+
tcTokenKnownJids.add(jid);
|
|
1622
|
+
}
|
|
1623
|
+
logger.debug({ mutated, remaining: survivors.size }, 'pruned expired tctokens');
|
|
1624
|
+
}
|
|
1625
|
+
catch (err) {
|
|
1626
|
+
logger.warn({ err: err?.message }, 'failed to prune expired tctokens');
|
|
1627
|
+
}
|
|
1628
|
+
};
|
|
1452
1629
|
registerSocketEndHandler(() => {
|
|
1453
1630
|
if (!config.msgRetryCounterCache && msgRetryCache.close) {
|
|
1454
1631
|
msgRetryCache.close();
|
|
@@ -8,13 +8,21 @@ import { AssociationType } from '../Types/index.js';
|
|
|
8
8
|
import { getUrlInfo } from '../Utils/link-preview.js';
|
|
9
9
|
import { makeKeyedMutex } from '../Utils/make-mutex.js';
|
|
10
10
|
import { getMessageReportingToken, shouldIncludeReportingToken } from '../Utils/reporting-utils.js';
|
|
11
|
-
import {
|
|
11
|
+
import { buildMergedTcTokenIndexWrite, isTcTokenExpired, resolveIssuanceJid, resolveTcTokenJid, shouldSendNewTcToken, storeTcTokensFromIqResult } from '../Utils/tc-token-utils.js';
|
|
12
|
+
import { areJidsSameUser, getBinaryNodeChild, getBinaryNodeChildren, getBizBinaryNode, isHostedLidUser, isHostedPnUser, isJidBot, isJidGroup, isJidMetaAI, isJidNewsletter, isLidUser, isPnUser, jidDecode, jidEncode, jidNormalizedUser, PSA_WID, S_WHATSAPP_NET } from '../WABinary/index.js';
|
|
12
13
|
import { USyncQuery, USyncUser } from '../WAUSync/index.js';
|
|
13
14
|
import { makeNewsletterSocket } from './newsletter.js';
|
|
14
15
|
export const makeMessagesSocket = (config) => {
|
|
15
16
|
const { logger, linkPreviewImageThumbnailWidth, generateHighQualityLinkPreview, options: httpRequestOptions, patchMessageBeforeSending, cachedGroupMetadata, enableRecentMessageCache, maxMsgRetryCount } = config;
|
|
16
17
|
const sock = makeNewsletterSocket(config);
|
|
17
18
|
const { ev, authState, messageMutex, signalRepository, upsertMessage, query, fetchPrivacySettings, sendNode, groupMetadata, groupToggleEphemeral, registerSocketEndHandler } = sock;
|
|
19
|
+
const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping);
|
|
20
|
+
/**
|
|
21
|
+
* Set of tctoken storage JIDs with a fire-and-forget `issuePrivacyTokens` IQ in flight.
|
|
22
|
+
* Prevents duplicate IQs from rapid back-to-back sends before `senderTimestamp` persists.
|
|
23
|
+
* Entries are always removed in `.finally()`, so the set is bounded by concurrency.
|
|
24
|
+
*/
|
|
25
|
+
const inFlightTcTokenIssuance = new Set();
|
|
18
26
|
const userDevicesCache = config.userDevicesCache ??=
|
|
19
27
|
new NodeCache({
|
|
20
28
|
stdTTL: DEFAULT_CACHE_TTLS.USER_DEVICES, // 5 minutes
|
|
@@ -801,9 +809,30 @@ export const makeMessagesSocket = (config) => {
|
|
|
801
809
|
logger.warn({ jid, trace: error?.stack }, 'failed to attach reporting token');
|
|
802
810
|
}
|
|
803
811
|
}
|
|
804
|
-
|
|
805
|
-
const
|
|
806
|
-
|
|
812
|
+
// WA Web never attaches tctoken to peer (AppStateSync) messages — server rejects with 479
|
|
813
|
+
const isPeerMessage = additionalAttributes?.['category'] === 'peer';
|
|
814
|
+
const is1on1Send = !isGroup && !isRetryResend && !isStatus && !isNewsletter && !isPeerMessage;
|
|
815
|
+
// Resolve destination to LID for tctoken storage — matches Signal session key pattern
|
|
816
|
+
const tcTokenJid = is1on1Send ? await resolveTcTokenJid(destinationJid, getLIDForPN) : destinationJid;
|
|
817
|
+
const contactTcTokenData = is1on1Send ? await authState.keys.get('tctoken', [tcTokenJid]) : {};
|
|
818
|
+
const existingTokenEntry = contactTcTokenData[tcTokenJid];
|
|
819
|
+
let tcTokenBuffer = existingTokenEntry?.token;
|
|
820
|
+
// Treat expired tokens the same as missing — clear from cache
|
|
821
|
+
if (tcTokenBuffer?.length && isTcTokenExpired(existingTokenEntry?.timestamp)) {
|
|
822
|
+
logger.debug({ jid: destinationJid, timestamp: existingTokenEntry?.timestamp }, 'tctoken expired, clearing');
|
|
823
|
+
tcTokenBuffer = undefined;
|
|
824
|
+
// Preserve senderTimestamp so the fire-and-forget issuance dedupe survives cleanup.
|
|
825
|
+
const cleared = existingTokenEntry?.senderTimestamp !== undefined
|
|
826
|
+
? { token: Buffer.alloc(0), senderTimestamp: existingTokenEntry.senderTimestamp }
|
|
827
|
+
: null;
|
|
828
|
+
try {
|
|
829
|
+
await authState.keys.set({ tctoken: { [tcTokenJid]: cleared } });
|
|
830
|
+
}
|
|
831
|
+
catch (err) {
|
|
832
|
+
logger.debug({ jid: destinationJid, err: err?.message }, 'failed to persist tctoken expiry cleanup');
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
if (tcTokenBuffer?.length && sock.serverProps.privacyTokenOn1to1) {
|
|
807
836
|
;
|
|
808
837
|
stanza.content.push({
|
|
809
838
|
tag: 'tctoken',
|
|
@@ -825,6 +854,48 @@ export const makeMessagesSocket = (config) => {
|
|
|
825
854
|
}
|
|
826
855
|
logger.debug({ msgId }, `sending message to ${participants.length} devices`);
|
|
827
856
|
await sendNode(stanza);
|
|
857
|
+
// Fire-and-forget: issue our token to the contact AFTER message send.
|
|
858
|
+
// WA Web skips protocol messages and PSA/bot contacts (TcTokenChatAction: isRegularUser)
|
|
859
|
+
const isProtocolMsg = !!normalizeMessageContent(message)?.protocolMessage;
|
|
860
|
+
const isBotOrPSA = destinationJid === PSA_WID || isJidBot(destinationJid) || isJidMetaAI(destinationJid);
|
|
861
|
+
if (is1on1Send &&
|
|
862
|
+
!isProtocolMsg &&
|
|
863
|
+
!isBotOrPSA &&
|
|
864
|
+
shouldSendNewTcToken(existingTokenEntry?.senderTimestamp) &&
|
|
865
|
+
!inFlightTcTokenIssuance.has(tcTokenJid)) {
|
|
866
|
+
inFlightTcTokenIssuance.add(tcTokenJid);
|
|
867
|
+
const issueTimestamp = unixTimestampSeconds();
|
|
868
|
+
const getPNForLID = signalRepository.lidMapping.getPNForLID.bind(signalRepository.lidMapping);
|
|
869
|
+
resolveIssuanceJid(destinationJid, sock.serverProps.lidTrustedTokenIssueToLid, getLIDForPN, getPNForLID)
|
|
870
|
+
.then(issueJid => issuePrivacyTokens([issueJid], issueTimestamp))
|
|
871
|
+
.then(async result => {
|
|
872
|
+
await storeTcTokensFromIqResult({
|
|
873
|
+
result,
|
|
874
|
+
fallbackJid: tcTokenJid,
|
|
875
|
+
keys: authState.keys,
|
|
876
|
+
getLIDForPN
|
|
877
|
+
});
|
|
878
|
+
const currentData = await authState.keys.get('tctoken', [tcTokenJid]);
|
|
879
|
+
const currentEntry = currentData[tcTokenJid];
|
|
880
|
+
const indexWrite = await buildMergedTcTokenIndexWrite(authState.keys, [tcTokenJid]);
|
|
881
|
+
await authState.keys.set({
|
|
882
|
+
tctoken: {
|
|
883
|
+
[tcTokenJid]: {
|
|
884
|
+
token: Buffer.alloc(0),
|
|
885
|
+
...currentEntry,
|
|
886
|
+
senderTimestamp: issueTimestamp
|
|
887
|
+
},
|
|
888
|
+
...indexWrite
|
|
889
|
+
}
|
|
890
|
+
});
|
|
891
|
+
})
|
|
892
|
+
.catch(err => {
|
|
893
|
+
logger.debug({ jid: destinationJid, err: err?.message }, 'fire-and-forget tctoken issuance failed');
|
|
894
|
+
})
|
|
895
|
+
.finally(() => {
|
|
896
|
+
inFlightTcTokenIssuance.delete(tcTokenJid);
|
|
897
|
+
});
|
|
898
|
+
}
|
|
828
899
|
// Add message to retry cache if enabled
|
|
829
900
|
if (messageRetryManager && !participant) {
|
|
830
901
|
messageRetryManager.addRecentMessage(destinationJid, msgId, message);
|
|
@@ -915,8 +986,8 @@ export const makeMessagesSocket = (config) => {
|
|
|
915
986
|
}
|
|
916
987
|
return ''
|
|
917
988
|
};
|
|
918
|
-
const
|
|
919
|
-
const t = unixTimestampSeconds().toString();
|
|
989
|
+
const issuePrivacyTokens = async (jids, timestamp) => {
|
|
990
|
+
const t = (timestamp ?? unixTimestampSeconds()).toString();
|
|
920
991
|
const result = await query({
|
|
921
992
|
tag: 'iq',
|
|
922
993
|
attrs: {
|
|
@@ -957,7 +1028,7 @@ export const makeMessagesSocket = (config) => {
|
|
|
957
1028
|
});
|
|
958
1029
|
return {
|
|
959
1030
|
...sock,
|
|
960
|
-
|
|
1031
|
+
issuePrivacyTokens,
|
|
961
1032
|
assertSessions,
|
|
962
1033
|
relayMessage,
|
|
963
1034
|
sendReceipt,
|
package/lib/Utils/chat-utils.js
CHANGED
|
@@ -38,7 +38,7 @@ const to64BitNetworkOrder = (e) => {
|
|
|
38
38
|
buff.writeUint32BE(e, 4);
|
|
39
39
|
return buff;
|
|
40
40
|
};
|
|
41
|
-
const makeLtHashGenerator = ({ indexValueMap, hash }) => {
|
|
41
|
+
export const makeLtHashGenerator = ({ indexValueMap, hash }) => {
|
|
42
42
|
indexValueMap = { ...indexValueMap };
|
|
43
43
|
const addBuffs = [];
|
|
44
44
|
const subBuffs = [];
|
|
@@ -48,7 +48,10 @@ const makeLtHashGenerator = ({ indexValueMap, hash }) => {
|
|
|
48
48
|
const prevOp = indexValueMap[indexMacBase64];
|
|
49
49
|
if (operation === proto.SyncdMutation.SyncdOperation.REMOVE) {
|
|
50
50
|
if (!prevOp) {
|
|
51
|
-
|
|
51
|
+
// WA Web does not throw here — it logs a warning and skips the subtract.
|
|
52
|
+
// The missing REMOVE will cause an LTHash mismatch, which is handled
|
|
53
|
+
// by the MAC validation layer (snapshot recovery or retry).
|
|
54
|
+
return;
|
|
52
55
|
}
|
|
53
56
|
// remove from index value mac, since this mutation is erased
|
|
54
57
|
delete indexValueMap[indexMacBase64];
|
|
@@ -83,7 +86,7 @@ export const newLTHashState = () => ({ version: 0, hash: Buffer.alloc(128), inde
|
|
|
83
86
|
export const encodeSyncdPatch = async ({ type, index, syncAction, apiVersion, operation }, myAppStateKeyId, state, getAppStateSyncKey) => {
|
|
84
87
|
const key = !!myAppStateKeyId ? await getAppStateSyncKey(myAppStateKeyId) : undefined;
|
|
85
88
|
if (!key) {
|
|
86
|
-
throw new Boom(`myAppStateKey ("${myAppStateKeyId}") not present`, {
|
|
89
|
+
throw new Boom(`myAppStateKey ("${myAppStateKeyId}") not present`, { data: { isMissingKey: true } });
|
|
87
90
|
}
|
|
88
91
|
const encKeyId = Buffer.from(myAppStateKeyId, 'base64');
|
|
89
92
|
state = { ...state, indexValueMap: { ...state.indexValueMap } };
|
|
@@ -176,7 +179,7 @@ export const decodeSyncdMutations = async (msgMutations, initialState, getAppSta
|
|
|
176
179
|
if (!keyEnc) {
|
|
177
180
|
throw new Boom(`failed to find key "${base64Key}" to decode mutation`, {
|
|
178
181
|
statusCode: 404,
|
|
179
|
-
data: { msgMutations }
|
|
182
|
+
data: { isMissingKey: true, msgMutations }
|
|
180
183
|
});
|
|
181
184
|
}
|
|
182
185
|
const keys = mutationKeys(keyEnc.keyData);
|
|
@@ -184,12 +187,35 @@ export const decodeSyncdMutations = async (msgMutations, initialState, getAppSta
|
|
|
184
187
|
return keys;
|
|
185
188
|
}
|
|
186
189
|
};
|
|
190
|
+
export const ensureLTHashStateVersion = (state) => {
|
|
191
|
+
if (typeof state.version !== 'number' || isNaN(state.version)) {
|
|
192
|
+
state.version = 0;
|
|
193
|
+
}
|
|
194
|
+
return state;
|
|
195
|
+
};
|
|
196
|
+
export const MAX_SYNC_ATTEMPTS = 2;
|
|
197
|
+
/**
|
|
198
|
+
* Check if an error is a missing app state sync key.
|
|
199
|
+
* WA Web treats these as "Blocked" (waits for key arrival), not fatal.
|
|
200
|
+
* In Baileys we retry with a snapshot which may use a different key.
|
|
201
|
+
*/
|
|
202
|
+
export const isMissingKeyError = (error) => {
|
|
203
|
+
return error?.data?.isMissingKey === true;
|
|
204
|
+
};
|
|
205
|
+
/**
|
|
206
|
+
* Determines if an app state sync error is unrecoverable.
|
|
207
|
+
* TypeError indicates a WASM crash; otherwise we give up after MAX_SYNC_ATTEMPTS.
|
|
208
|
+
* Missing keys are NOT checked here — they are handled separately as "Blocked".
|
|
209
|
+
*/
|
|
210
|
+
export const isAppStateSyncIrrecoverable = (error, attempts) => {
|
|
211
|
+
return attempts >= MAX_SYNC_ATTEMPTS || error?.name === 'TypeError';
|
|
212
|
+
};
|
|
187
213
|
export const decodeSyncdPatch = async (msg, name, initialState, getAppStateSyncKey, onMutation, validateMacs) => {
|
|
188
214
|
if (validateMacs) {
|
|
189
215
|
const base64Key = Buffer.from(msg.keyId.id).toString('base64');
|
|
190
216
|
const mainKeyObj = await getAppStateSyncKey(base64Key);
|
|
191
217
|
if (!mainKeyObj) {
|
|
192
|
-
throw new Boom(`failed to find key "${base64Key}" to decode patch`, {
|
|
218
|
+
throw new Boom(`failed to find key "${base64Key}" to decode patch`, { data: { isMissingKey: true, msg } });
|
|
193
219
|
}
|
|
194
220
|
const mainKey = mutationKeys(mainKeyObj.keyData);
|
|
195
221
|
const mutationmacs = msg.mutations.map(mutation => mutation.record.value.blob.slice(-32));
|
|
@@ -267,7 +293,7 @@ export const decodeSyncdSnapshot = async (name, snapshot, getAppStateSyncKey, mi
|
|
|
267
293
|
const base64Key = Buffer.from(snapshot.keyId.id).toString('base64');
|
|
268
294
|
const keyEnc = await getAppStateSyncKey(base64Key);
|
|
269
295
|
if (!keyEnc) {
|
|
270
|
-
throw new Boom(`failed to find key "${base64Key}" to decode mutation
|
|
296
|
+
throw new Boom(`failed to find key "${base64Key}" to decode mutation`, { data: { isMissingKey: true } });
|
|
271
297
|
}
|
|
272
298
|
const result = mutationKeys(keyEnc.keyData);
|
|
273
299
|
const computedSnapshotMac = generateSnapshotMac(newState.hash, newState.version, name, result.snapshotMacKey);
|
|
@@ -309,7 +335,7 @@ export const decodePatches = async (name, syncds, initial, getAppStateSyncKey, o
|
|
|
309
335
|
const base64Key = Buffer.from(keyId.id).toString('base64');
|
|
310
336
|
const keyEnc = await getAppStateSyncKey(base64Key);
|
|
311
337
|
if (!keyEnc) {
|
|
312
|
-
throw new Boom(`failed to find key "${base64Key}" to decode mutation
|
|
338
|
+
throw new Boom(`failed to find key "${base64Key}" to decode mutation`, { data: { isMissingKey: true } });
|
|
313
339
|
}
|
|
314
340
|
const result = mutationKeys(keyEnc.keyData);
|
|
315
341
|
const computedSnapshotMac = generateSnapshotMac(newState.hash, newState.version, name, result.snapshotMacKey);
|
|
@@ -779,6 +805,7 @@ export const processSyncAction = (syncAction, ev, me, initialSyncOpts, logger) =
|
|
|
779
805
|
action.lidContactAction.firstName ||
|
|
780
806
|
action.lidContactAction.username ||
|
|
781
807
|
undefined,
|
|
808
|
+
username: action.lidContactAction.username || undefined,
|
|
782
809
|
lid: id,
|
|
783
810
|
phoneNumber: undefined
|
|
784
811
|
}
|