@itsliaaa/baileys 0.3.0-rc.9 → 0.3.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 +0 -29
- package/lib/Defaults/index.d.ts +6 -7
- package/lib/Defaults/index.d.ts.map +1 -1
- package/lib/Defaults/index.js +7 -8
- package/lib/Defaults/index.js.map +1 -1
- package/lib/Signal/libsignal.d.ts +13 -0
- package/lib/Signal/libsignal.d.ts.map +1 -1
- package/lib/Signal/libsignal.js +45 -17
- package/lib/Signal/libsignal.js.map +1 -1
- package/lib/Signal/lid-mapping.d.ts +4 -0
- package/lib/Signal/lid-mapping.d.ts.map +1 -1
- package/lib/Signal/lid-mapping.js +6 -0
- package/lib/Signal/lid-mapping.js.map +1 -1
- package/lib/Socket/business.d.ts +9 -1
- package/lib/Socket/business.d.ts.map +1 -1
- package/lib/Socket/chats.d.ts +4 -1
- package/lib/Socket/chats.d.ts.map +1 -1
- package/lib/Socket/chats.js +14 -5
- package/lib/Socket/chats.js.map +1 -1
- package/lib/Socket/communities.d.ts +9 -1
- package/lib/Socket/communities.d.ts.map +1 -1
- package/lib/Socket/groups.d.ts +4 -1
- package/lib/Socket/groups.d.ts.map +1 -1
- package/lib/Socket/index.d.ts +9 -1
- package/lib/Socket/index.d.ts.map +1 -1
- package/lib/Socket/messages-recv.d.ts +9 -1
- package/lib/Socket/messages-recv.d.ts.map +1 -1
- package/lib/Socket/messages-recv.js +278 -124
- package/lib/Socket/messages-recv.js.map +1 -1
- package/lib/Socket/messages-send.d.ts +9 -1
- package/lib/Socket/messages-send.d.ts.map +1 -1
- package/lib/Socket/messages-send.js +68 -37
- package/lib/Socket/messages-send.js.map +1 -1
- package/lib/Socket/newsletter.d.ts +4 -1
- package/lib/Socket/newsletter.d.ts.map +1 -1
- package/lib/Socket/socket.d.ts +3 -1
- package/lib/Socket/socket.d.ts.map +1 -1
- package/lib/Socket/socket.js +24 -19
- package/lib/Socket/socket.js.map +1 -1
- package/lib/Utils/chat-utils.d.ts +1 -1
- package/lib/Utils/chat-utils.d.ts.map +1 -1
- package/lib/Utils/chat-utils.js +46 -12
- package/lib/Utils/chat-utils.js.map +1 -1
- package/lib/Utils/decode-wa-message.d.ts +1 -1
- package/lib/Utils/decode-wa-message.d.ts.map +1 -1
- package/lib/Utils/decode-wa-message.js +6 -2
- package/lib/Utils/decode-wa-message.js.map +1 -1
- package/lib/Utils/event-buffer.d.ts +1 -0
- package/lib/Utils/event-buffer.d.ts.map +1 -1
- package/lib/Utils/event-buffer.js +47 -1
- package/lib/Utils/event-buffer.js.map +1 -1
- package/lib/Utils/generics.d.ts.map +1 -1
- package/lib/Utils/generics.js +4 -4
- package/lib/Utils/generics.js.map +1 -1
- package/lib/Utils/history.d.ts +2 -0
- package/lib/Utils/history.d.ts.map +1 -1
- package/lib/Utils/history.js +1 -0
- package/lib/Utils/history.js.map +1 -1
- package/lib/Utils/message-retry-manager.d.ts +5 -0
- package/lib/Utils/message-retry-manager.d.ts.map +1 -1
- package/lib/Utils/message-retry-manager.js +40 -0
- package/lib/Utils/message-retry-manager.js.map +1 -1
- package/lib/Utils/messages-media.d.ts +2 -1
- package/lib/Utils/messages-media.d.ts.map +1 -1
- package/lib/Utils/messages-media.js +16 -4
- package/lib/Utils/messages-media.js.map +1 -1
- package/lib/Utils/messages.js +1 -1
- package/lib/Utils/messages.js.map +1 -1
- package/lib/Utils/signal.d.ts +13 -0
- package/lib/Utils/signal.d.ts.map +1 -1
- package/lib/Utils/signal.js +42 -0
- package/lib/Utils/signal.js.map +1 -1
- package/lib/Utils/validate-connection.d.ts.map +1 -1
- package/lib/Utils/validate-connection.js +3 -0
- package/lib/Utils/validate-connection.js.map +1 -1
- package/lib/WAUSync/USyncQuery.js +1 -1
- package/lib/WAUSync/USyncQuery.js.map +1 -1
- package/package.json +33 -4
|
@@ -5,12 +5,12 @@ import Long from 'long';
|
|
|
5
5
|
import { proto } from '../../WAProto/index.js';
|
|
6
6
|
import { DEFAULT_CACHE_TTLS, KEY_BUNDLE_TYPE, MIN_PREKEY_COUNT, PLACEHOLDER_MAX_AGE_SECONDS, STATUS_EXPIRY_SECONDS } from '../Defaults/index.js';
|
|
7
7
|
import { ReachoutTimelockEnforcementType, WAMessageStatus, WAMessageStubType } from '../Types/index.js';
|
|
8
|
-
import { ACCOUNT_RESTRICTED_TEXT, aesDecryptCTR, aesEncryptGCM, cleanMessage, Curve, decodeMediaRetryNode, decodeMessageNode, decryptMessageNode, delay, derivePairingCodeKey, encodeBigEndian, encodeSignedDeviceIdentity, extractAddressingContext, getCallStatusFromNode, getHistoryMsg, getNextPreKeys, getStatusFromReceiptType, handleIdentityChange, hkdf, MISSING_KEYS_ERROR_TEXT, NACK_REASONS, NO_MESSAGE_FOUND_ERROR_TEXT, SERVER_ERROR_CODES, toNumber, unixTimestampSeconds, xmppPreKey, xmppSignedPreKey } from '../Utils/index.js';
|
|
8
|
+
import { ACCOUNT_RESTRICTED_TEXT, aesDecryptCTR, aesEncryptGCM, cleanMessage, Curve, decodeMediaRetryNode, decodeMessageNode, decryptMessageNode, delay, derivePairingCodeKey, encodeBigEndian, encodeSignedDeviceIdentity, extractAddressingContext, extractE2ESessionFromRetryReceipt, getCallStatusFromNode, getHistoryMsg, getNextPreKeys, getStatusFromReceiptType, handleIdentityChange, hkdf, MISSING_KEYS_ERROR_TEXT, NACK_REASONS, NO_MESSAGE_FOUND_ERROR_TEXT, SERVER_ERROR_CODES, toNumber, unixTimestampSeconds, xmppPreKey, xmppSignedPreKey } from '../Utils/index.js';
|
|
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
12
|
import { buildMergedTcTokenIndexWrite, isTcTokenExpired, readTcTokenIndex, resolveIssuanceJid, resolveTcTokenJid, storeTcTokensFromIqResult, TC_TOKEN_INDEX_KEY } from '../Utils/tc-token-utils.js';
|
|
13
|
-
import { areJidsSameUser, binaryNodeToString, getAllBinaryNodeChildren, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildren, getBinaryNodeChildString, isJidGroup, isJidNewsletter, isJidStatusBroadcast, isLidUser, isPnUser, jidDecode, jidNormalizedUser, S_WHATSAPP_NET } from '../WABinary/index.js';
|
|
13
|
+
import { areJidsSameUser, binaryNodeToString, getAllBinaryNodeChildren, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildren, getBinaryNodeChildString, getBinaryNodeChildUInt, isJidGroup, isJidNewsletter, isJidStatusBroadcast, isLidUser, isPnUser, jidDecode, jidNormalizedUser, S_WHATSAPP_NET } from '../WABinary/index.js';
|
|
14
14
|
import { extractGroupMetadata } from './groups.js';
|
|
15
15
|
import { makeMessagesSocket } from './messages-send.js';
|
|
16
16
|
const ENFORCEMENT_TYPE_VALUES = new Set(Object.values(ReachoutTimelockEnforcementType));
|
|
@@ -20,7 +20,7 @@ function isValidEnforcementType(value) {
|
|
|
20
20
|
export const makeMessagesRecvSocket = (config) => {
|
|
21
21
|
const { logger, retryRequestDelayMs, maxMsgRetryCount, getMessage, shouldIgnoreJid, enableAutoSessionRecreation } = config;
|
|
22
22
|
const sock = makeMessagesSocket(config);
|
|
23
|
-
const { ev, authState, ws, messageMutex, notificationMutex, receiptMutex, signalRepository, query, upsertMessage, resyncAppState, onUnexpectedError, assertSessions, sendNode, relayMessage, sendReceipt, uploadPreKeys, sendPeerDataOperationMessage, messageRetryManager, issuePrivacyTokens, fetchAccountReachoutTimelock } = sock;
|
|
23
|
+
const { userDevicesCache, devicesMutex, ev, authState, ws, messageMutex, notificationMutex, receiptMutex, signalRepository, query, upsertMessage, resyncAppState, onUnexpectedError, assertSessions, sendNode, relayMessage, sendReceipt, uploadPreKeys, sendPeerDataOperationMessage, messageRetryManager, registerSocketEndHandler, issuePrivacyTokens, fetchAccountReachoutTimelock, placeholderResendCache } = sock;
|
|
24
24
|
const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping);
|
|
25
25
|
/** this mutex ensures that each retryRequest will wait for the previous one to finish */
|
|
26
26
|
const retryMutex = makeMutex();
|
|
@@ -34,11 +34,6 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
34
34
|
stdTTL: DEFAULT_CACHE_TTLS.CALL_OFFER, // 5 mins
|
|
35
35
|
useClones: false
|
|
36
36
|
});
|
|
37
|
-
const placeholderResendCache = config.placeholderResendCache ||
|
|
38
|
-
new NodeCache({
|
|
39
|
-
stdTTL: DEFAULT_CACHE_TTLS.MSG_RETRY, // 1 hour
|
|
40
|
-
useClones: false
|
|
41
|
-
});
|
|
42
37
|
// Debounce identity-change session refreshes per JID to avoid bursts
|
|
43
38
|
const identityAssertDebounce = new NodeCache({ stdTTL: 5, useClones: false });
|
|
44
39
|
let sendActiveReceipts = false;
|
|
@@ -277,83 +272,90 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
277
272
|
// Handles newsletter notifications
|
|
278
273
|
const handleNewsletterNotification = async (node) => {
|
|
279
274
|
const from = node.attrs.from;
|
|
280
|
-
const
|
|
275
|
+
const children = getAllBinaryNodeChildren(node);
|
|
281
276
|
const author = node.attrs.participant;
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
server_id: child.attrs.message_id,
|
|
288
|
-
reaction: {
|
|
289
|
-
code: getBinaryNodeChildString(child, 'reaction'),
|
|
290
|
-
count: 1
|
|
291
|
-
}
|
|
292
|
-
};
|
|
293
|
-
ev.emit('newsletter.reaction', reactionUpdate);
|
|
294
|
-
break;
|
|
295
|
-
case 'view':
|
|
296
|
-
const viewUpdate = {
|
|
297
|
-
id: from,
|
|
298
|
-
server_id: child.attrs.message_id,
|
|
299
|
-
count: parseInt(child.content?.toString() || '0', 10)
|
|
300
|
-
};
|
|
301
|
-
ev.emit('newsletter.view', viewUpdate);
|
|
302
|
-
break;
|
|
303
|
-
case 'participant':
|
|
304
|
-
const participantUpdate = {
|
|
305
|
-
id: from,
|
|
306
|
-
author,
|
|
307
|
-
user: child.attrs.jid,
|
|
308
|
-
action: child.attrs.action,
|
|
309
|
-
new_role: child.attrs.role
|
|
310
|
-
};
|
|
311
|
-
ev.emit('newsletter-participants.update', participantUpdate);
|
|
312
|
-
break;
|
|
313
|
-
case 'update':
|
|
314
|
-
const settingsNode = getBinaryNodeChild(child, 'settings');
|
|
315
|
-
if (settingsNode) {
|
|
316
|
-
const update = {};
|
|
317
|
-
const nameNode = getBinaryNodeChild(settingsNode, 'name');
|
|
318
|
-
if (nameNode?.content)
|
|
319
|
-
update.name = nameNode.content.toString();
|
|
320
|
-
const descriptionNode = getBinaryNodeChild(settingsNode, 'description');
|
|
321
|
-
if (descriptionNode?.content)
|
|
322
|
-
update.description = descriptionNode.content.toString();
|
|
323
|
-
ev.emit('newsletter-settings.update', {
|
|
277
|
+
for (const child of children) {
|
|
278
|
+
logger.debug({ from, child }, 'got newsletter notification');
|
|
279
|
+
switch (child.tag) {
|
|
280
|
+
case 'reaction': {
|
|
281
|
+
const reactionUpdate = {
|
|
324
282
|
id: from,
|
|
325
|
-
|
|
326
|
-
|
|
283
|
+
server_id: child.attrs.message_id,
|
|
284
|
+
reaction: {
|
|
285
|
+
code: getBinaryNodeChildString(child, 'reaction'),
|
|
286
|
+
count: 1
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
ev.emit('newsletter.reaction', reactionUpdate);
|
|
290
|
+
break;
|
|
327
291
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
292
|
+
case 'view': {
|
|
293
|
+
const viewUpdate = {
|
|
294
|
+
id: from,
|
|
295
|
+
server_id: child.attrs.message_id,
|
|
296
|
+
count: parseInt(child.content?.toString() || '0', 10)
|
|
297
|
+
};
|
|
298
|
+
ev.emit('newsletter.view', viewUpdate);
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
case 'participant': {
|
|
302
|
+
const participantUpdate = {
|
|
303
|
+
id: from,
|
|
304
|
+
author,
|
|
305
|
+
user: child.attrs.jid,
|
|
306
|
+
action: child.attrs.action,
|
|
307
|
+
new_role: child.attrs.role
|
|
308
|
+
};
|
|
309
|
+
ev.emit('newsletter-participants.update', participantUpdate);
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
case 'update': {
|
|
313
|
+
const settingsNode = getBinaryNodeChild(child, 'settings');
|
|
314
|
+
if (settingsNode) {
|
|
315
|
+
const update = {};
|
|
316
|
+
const nameNode = getBinaryNodeChild(settingsNode, 'name');
|
|
317
|
+
if (nameNode?.content)
|
|
318
|
+
update.name = nameNode.content.toString();
|
|
319
|
+
const descriptionNode = getBinaryNodeChild(settingsNode, 'description');
|
|
320
|
+
if (descriptionNode?.content)
|
|
321
|
+
update.description = descriptionNode.content.toString();
|
|
322
|
+
ev.emit('newsletter-settings.update', {
|
|
323
|
+
id: from,
|
|
324
|
+
update
|
|
325
|
+
});
|
|
348
326
|
}
|
|
349
|
-
|
|
350
|
-
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
case 'message': {
|
|
330
|
+
const plaintextNode = getBinaryNodeChild(child, 'plaintext');
|
|
331
|
+
if (plaintextNode?.content) {
|
|
332
|
+
try {
|
|
333
|
+
const contentBuf = typeof plaintextNode.content === 'string'
|
|
334
|
+
? Buffer.from(plaintextNode.content, 'binary')
|
|
335
|
+
: Buffer.from(plaintextNode.content);
|
|
336
|
+
const messageProto = proto.Message.decode(contentBuf).toJSON();
|
|
337
|
+
const fullMessage = proto.WebMessageInfo.fromObject({
|
|
338
|
+
key: {
|
|
339
|
+
remoteJid: from,
|
|
340
|
+
id: child.attrs.message_id || child.attrs.server_id,
|
|
341
|
+
fromMe: false // TODO: is this really true though
|
|
342
|
+
},
|
|
343
|
+
message: messageProto,
|
|
344
|
+
messageTimestamp: +child.attrs.t
|
|
345
|
+
}).toJSON();
|
|
346
|
+
await upsertMessage(fullMessage, 'append');
|
|
347
|
+
logger.debug('Processed plaintext newsletter message');
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
logger.error({ error }, 'Failed to decode plaintext newsletter message');
|
|
351
|
+
}
|
|
351
352
|
}
|
|
353
|
+
break;
|
|
352
354
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
355
|
+
default:
|
|
356
|
+
logger.warn({ node, child }, 'Unknown newsletter notification child');
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
357
359
|
}
|
|
358
360
|
};
|
|
359
361
|
const sendMessageAck = async (node, errorCode) => {
|
|
@@ -513,6 +515,8 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
513
515
|
logger.info({ msgAttrs: node.attrs, retryCount }, 'sent retry receipt');
|
|
514
516
|
}, authState?.creds?.me?.id || 'sendRetryRequest');
|
|
515
517
|
};
|
|
518
|
+
// Mirrors WAWeb/Handle/PreKeyLow.js: skip a re-issued notification with the same stanza id.
|
|
519
|
+
const inFlightPreKeyLow = new Set();
|
|
516
520
|
/**
|
|
517
521
|
* Fire-and-forget tctoken re-issuance after a peer's device identity changed.
|
|
518
522
|
* Mirrors WAWebSendTcTokenWhenDeviceIdentityChange — runs in parallel with
|
|
@@ -545,12 +549,24 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
545
549
|
const handleEncryptNotification = async (node) => {
|
|
546
550
|
const from = node.attrs.from;
|
|
547
551
|
if (from === S_WHATSAPP_NET) {
|
|
552
|
+
const stanzaId = node.attrs.id;
|
|
553
|
+
if (stanzaId && inFlightPreKeyLow.has(stanzaId)) {
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
548
556
|
const countChild = getBinaryNodeChild(node, 'count');
|
|
549
557
|
const count = +countChild.attrs.value;
|
|
550
558
|
const shouldUploadMorePreKeys = count < MIN_PREKEY_COUNT;
|
|
551
559
|
logger.debug({ count, shouldUploadMorePreKeys }, 'recv pre-key count');
|
|
552
560
|
if (shouldUploadMorePreKeys) {
|
|
553
|
-
|
|
561
|
+
if (stanzaId)
|
|
562
|
+
inFlightPreKeyLow.add(stanzaId);
|
|
563
|
+
try {
|
|
564
|
+
await uploadPreKeys();
|
|
565
|
+
}
|
|
566
|
+
finally {
|
|
567
|
+
if (stanzaId)
|
|
568
|
+
inFlightPreKeyLow.delete(stanzaId);
|
|
569
|
+
}
|
|
554
570
|
}
|
|
555
571
|
}
|
|
556
572
|
else {
|
|
@@ -694,6 +710,89 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
694
710
|
break;
|
|
695
711
|
}
|
|
696
712
|
};
|
|
713
|
+
const handleDevicesNotification = async (node) => {
|
|
714
|
+
const [child] = getAllBinaryNodeChildren(node);
|
|
715
|
+
const from = jidNormalizedUser(node.attrs.from);
|
|
716
|
+
if (!child) {
|
|
717
|
+
logger.debug({ from }, 'devices notification missing child, skipping');
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
const tag = child.tag;
|
|
721
|
+
const deviceHash = child.attrs.device_hash;
|
|
722
|
+
const devices = getBinaryNodeChildren(child, 'device');
|
|
723
|
+
if (areJidsSameUser(from, authState.creds.me.id) || areJidsSameUser(from, authState.creds.me.lid)) {
|
|
724
|
+
const deviceJids = devices.map(d => d.attrs.jid);
|
|
725
|
+
logger.info({ deviceJids }, 'got my own devices');
|
|
726
|
+
}
|
|
727
|
+
if (!devices.length) {
|
|
728
|
+
logger.debug({ from, tag }, 'no devices in notification, skipping');
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
const decoded = [];
|
|
732
|
+
for (const d of devices) {
|
|
733
|
+
const jid = d.attrs.jid;
|
|
734
|
+
if (!jid)
|
|
735
|
+
continue;
|
|
736
|
+
const parts = jidDecode(jid);
|
|
737
|
+
if (!parts) {
|
|
738
|
+
logger.debug({ jid }, 'failed to decode device jid, skipping');
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
decoded.push({ jid, user: parts.user, server: parts.server, device: parts.device });
|
|
742
|
+
}
|
|
743
|
+
if (!decoded.length)
|
|
744
|
+
return;
|
|
745
|
+
await devicesMutex.mutex(async () => {
|
|
746
|
+
const byUser = new Map();
|
|
747
|
+
for (const d of decoded) {
|
|
748
|
+
const list = byUser.get(d.user) || [];
|
|
749
|
+
list.push(d);
|
|
750
|
+
byUser.set(d.user, list);
|
|
751
|
+
}
|
|
752
|
+
for (const [user, entries] of byUser) {
|
|
753
|
+
if (tag === 'update') {
|
|
754
|
+
logger.debug({ user }, `${user}'s device list updated, dropping cached devices`);
|
|
755
|
+
await userDevicesCache?.del(user);
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
if (tag === 'remove') {
|
|
759
|
+
await signalRepository.deleteSession(entries.map(e => e.jid));
|
|
760
|
+
}
|
|
761
|
+
const existingCache = (await userDevicesCache?.get(user)) || [];
|
|
762
|
+
if (!existingCache.length) {
|
|
763
|
+
// No baseline yet; skip applying the delta so getUSyncDevices can
|
|
764
|
+
// later fetch the full device list. Caching just the notification
|
|
765
|
+
// entries would make a partial list look authoritative.
|
|
766
|
+
logger.debug({ user, tag }, 'device list not cached, deferring to USync refresh');
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
const affected = new Set(entries.map(e => e.device));
|
|
770
|
+
let updatedDevices;
|
|
771
|
+
switch (tag) {
|
|
772
|
+
case 'add':
|
|
773
|
+
logger.info({ deviceHash, count: entries.length }, 'devices added');
|
|
774
|
+
updatedDevices = [
|
|
775
|
+
...existingCache.filter(d => !affected.has(d.device)),
|
|
776
|
+
...entries.map(e => ({ user: e.user, server: e.server, device: e.device }))
|
|
777
|
+
];
|
|
778
|
+
break;
|
|
779
|
+
case 'remove':
|
|
780
|
+
logger.info({ deviceHash, count: entries.length }, 'devices removed');
|
|
781
|
+
updatedDevices = existingCache.filter(d => !affected.has(d.device));
|
|
782
|
+
break;
|
|
783
|
+
default:
|
|
784
|
+
logger.debug({ tag }, 'Unknown device list change tag');
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
if (updatedDevices.length === 0) {
|
|
788
|
+
await userDevicesCache?.del(user);
|
|
789
|
+
}
|
|
790
|
+
else {
|
|
791
|
+
await userDevicesCache?.set(user, updatedDevices);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
};
|
|
697
796
|
const processNotification = async (node) => {
|
|
698
797
|
const result = {};
|
|
699
798
|
const [child] = getAllBinaryNodeChildren(node);
|
|
@@ -718,13 +817,12 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
718
817
|
await handleEncryptNotification(node);
|
|
719
818
|
break;
|
|
720
819
|
case 'devices':
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
logger.
|
|
820
|
+
try {
|
|
821
|
+
await handleDevicesNotification(node);
|
|
822
|
+
}
|
|
823
|
+
catch (error) {
|
|
824
|
+
logger.error({ error, node }, 'failed to handle devices notification');
|
|
726
825
|
}
|
|
727
|
-
//TODO: drop a new event, add hashes
|
|
728
826
|
break;
|
|
729
827
|
case 'server_sync':
|
|
730
828
|
const update = getBinaryNodeChild(node, 'collection');
|
|
@@ -939,10 +1037,11 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
939
1037
|
const newValue = ((await msgRetryCache.get(key)) || 0) + 1;
|
|
940
1038
|
await msgRetryCache.set(key, newValue);
|
|
941
1039
|
};
|
|
942
|
-
const sendMessagesAgain = async (key, ids, retryNode) => {
|
|
1040
|
+
const sendMessagesAgain = async (key, ids, retryNode, receiptNode) => {
|
|
943
1041
|
const remoteJid = key.remoteJid;
|
|
944
1042
|
const participant = key.participant || remoteJid;
|
|
945
1043
|
const retryCount = +retryNode.attrs.count || 1;
|
|
1044
|
+
const msgId = ids[0];
|
|
946
1045
|
// Try to get messages from cache first, then fallback to getMessage
|
|
947
1046
|
const msgs = [];
|
|
948
1047
|
for (const id of ids) {
|
|
@@ -974,12 +1073,49 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
974
1073
|
// just re-send the message to everyone
|
|
975
1074
|
// prevents the first message decryption failure
|
|
976
1075
|
const sendToAll = !jidDecode(participant)?.device;
|
|
977
|
-
|
|
1076
|
+
const sessionId = signalRepository.jidToSignalProtocolAddress(participant);
|
|
1077
|
+
let injectedFromBundle = false;
|
|
1078
|
+
const bundle = extractE2ESessionFromRetryReceipt(receiptNode);
|
|
1079
|
+
if (bundle) {
|
|
1080
|
+
try {
|
|
1081
|
+
await signalRepository.injectE2ESession({ jid: participant, session: bundle });
|
|
1082
|
+
injectedFromBundle = true;
|
|
1083
|
+
logger.debug({ participant, retryCount }, 'injected session from retry receipt key bundle');
|
|
1084
|
+
}
|
|
1085
|
+
catch (error) {
|
|
1086
|
+
logger.warn({ error, participant }, 'failed to inject session from retry receipt');
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
if (!injectedFromBundle) {
|
|
1090
|
+
const receivedRegId = getBinaryNodeChildUInt(receiptNode, 'registration', 4);
|
|
1091
|
+
if (typeof receivedRegId === 'number' && Number.isInteger(receivedRegId)) {
|
|
1092
|
+
const info = await signalRepository.getSessionInfo(participant);
|
|
1093
|
+
if (info && info.registrationId !== 0 && info.registrationId !== receivedRegId) {
|
|
1094
|
+
logger.info({ participant, stored: info.registrationId, received: receivedRegId }, 'reg id mismatch on retry without bundle, deleting session');
|
|
1095
|
+
await authState.keys.set({ session: { [sessionId]: null } });
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
const BASE_KEY_CHECK_RETRY = 2;
|
|
1100
|
+
if (msgId && messageRetryManager) {
|
|
1101
|
+
const info = await signalRepository.getSessionInfo(participant);
|
|
1102
|
+
if (info) {
|
|
1103
|
+
if (retryCount === BASE_KEY_CHECK_RETRY) {
|
|
1104
|
+
messageRetryManager.saveBaseKey(sessionId, msgId, info.baseKey);
|
|
1105
|
+
}
|
|
1106
|
+
else if (retryCount > BASE_KEY_CHECK_RETRY) {
|
|
1107
|
+
if (messageRetryManager.hasSameBaseKey(sessionId, msgId, info.baseKey)) {
|
|
1108
|
+
logger.warn({ participant, retryCount }, 'base key collision on retry, forcing fresh session');
|
|
1109
|
+
await authState.keys.set({ session: { [sessionId]: null } });
|
|
1110
|
+
}
|
|
1111
|
+
messageRetryManager.deleteBaseKey(sessionId, msgId);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
978
1115
|
let shouldRecreateSession = false;
|
|
979
1116
|
let recreateReason = '';
|
|
980
|
-
if (enableAutoSessionRecreation && messageRetryManager && retryCount > 1) {
|
|
1117
|
+
if (enableAutoSessionRecreation && messageRetryManager && retryCount > 1 && !injectedFromBundle) {
|
|
981
1118
|
try {
|
|
982
|
-
const sessionId = signalRepository.jidToSignalProtocolAddress(participant);
|
|
983
1119
|
const hasSession = await signalRepository.validateSession(participant);
|
|
984
1120
|
const result = messageRetryManager.shouldRecreateSession(participant, hasSession.exists);
|
|
985
1121
|
shouldRecreateSession = result.recreate;
|
|
@@ -993,11 +1129,13 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
993
1129
|
logger.warn({ error, participant }, 'failed to check session recreation for outgoing retry');
|
|
994
1130
|
}
|
|
995
1131
|
}
|
|
996
|
-
|
|
1132
|
+
if (!injectedFromBundle) {
|
|
1133
|
+
await assertSessions([participant], true);
|
|
1134
|
+
}
|
|
997
1135
|
if (isJidGroup(remoteJid)) {
|
|
998
1136
|
await authState.keys.set({ 'sender-key-memory': { [remoteJid]: null } });
|
|
999
1137
|
}
|
|
1000
|
-
logger.debug({ participant, sendToAll, shouldRecreateSession, recreateReason }, '
|
|
1138
|
+
logger.debug({ participant, sendToAll, shouldRecreateSession, recreateReason, injectedFromBundle }, 'prepared session for retry resend');
|
|
1001
1139
|
for (const [i, msg] of msgs.entries()) {
|
|
1002
1140
|
if (!ids[i])
|
|
1003
1141
|
continue;
|
|
@@ -1073,7 +1211,7 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
1073
1211
|
try {
|
|
1074
1212
|
await updateSendMessageAgainCount(ids[0], key.participant);
|
|
1075
1213
|
logger.debug({ attrs, key }, 'recv retry request');
|
|
1076
|
-
await sendMessagesAgain(key, ids, retryNode);
|
|
1214
|
+
await sendMessagesAgain(key, ids, retryNode, node);
|
|
1077
1215
|
}
|
|
1078
1216
|
catch (error) {
|
|
1079
1217
|
logger.error({ key, ids, trace: error instanceof Error ? error.stack : 'Unknown error' }, 'error in sending message again');
|
|
@@ -1229,29 +1367,14 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
1229
1367
|
return sendMessageAck(node);
|
|
1230
1368
|
}
|
|
1231
1369
|
}
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
logger.debug(`[handleMessage] Attempting retry request for failed decryption`);
|
|
1235
|
-
// Handle both pre-key and normal retries in single mutex
|
|
1370
|
+
logger.debug('[handleMessage] Attempting retry request for failed decryption');
|
|
1371
|
+
// WAWeb only retry-receipts here; server emits PreKeyLow if prekeys run low.
|
|
1236
1372
|
await retryMutex.mutex(async () => {
|
|
1237
1373
|
try {
|
|
1238
1374
|
if (!ws.isOpen) {
|
|
1239
1375
|
logger.debug({ node }, 'Connection closed, skipping retry');
|
|
1240
1376
|
return;
|
|
1241
1377
|
}
|
|
1242
|
-
// Handle pre-key errors with upload and delay
|
|
1243
|
-
if (isPreKeyError) {
|
|
1244
|
-
logger.info({ error: errorMessage }, 'PreKey error detected, uploading and retrying');
|
|
1245
|
-
try {
|
|
1246
|
-
logger.debug('Uploading pre-keys for error recovery');
|
|
1247
|
-
await uploadPreKeys(5);
|
|
1248
|
-
logger.debug('Waiting for server to process new pre-keys');
|
|
1249
|
-
await delay(1000);
|
|
1250
|
-
}
|
|
1251
|
-
catch (uploadErr) {
|
|
1252
|
-
logger.error({ uploadErr }, 'Pre-key upload failed, proceeding with retry anyway');
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
1378
|
const encNode = getBinaryNodeChild(node, 'enc');
|
|
1256
1379
|
await sendRetryRequest(node, !encNode);
|
|
1257
1380
|
if (retryRequestDelayMs) {
|
|
@@ -1259,15 +1382,7 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
1259
1382
|
}
|
|
1260
1383
|
}
|
|
1261
1384
|
catch (err) {
|
|
1262
|
-
logger.error({ err
|
|
1263
|
-
// Still attempt retry even if pre-key upload failed
|
|
1264
|
-
try {
|
|
1265
|
-
const encNode = getBinaryNodeChild(node, 'enc');
|
|
1266
|
-
await sendRetryRequest(node, !encNode);
|
|
1267
|
-
}
|
|
1268
|
-
catch (retryErr) {
|
|
1269
|
-
logger.error({ retryErr }, 'Failed to send retry after error handling');
|
|
1270
|
-
}
|
|
1385
|
+
logger.error({ err }, 'Failed to send retry');
|
|
1271
1386
|
}
|
|
1272
1387
|
acked = true;
|
|
1273
1388
|
await sendMessageAck(node, NACK_REASONS.UnhandledError);
|
|
@@ -1395,12 +1510,39 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
1395
1510
|
// device could not display the message
|
|
1396
1511
|
if (attrs.error) {
|
|
1397
1512
|
const isReachoutTimelocked = attrs.error === String(NACK_REASONS.SenderReachoutTimelocked);
|
|
1398
|
-
if (attrs.error === SERVER_ERROR_CODES.
|
|
1399
|
-
// 463 =
|
|
1400
|
-
//
|
|
1401
|
-
//
|
|
1402
|
-
//
|
|
1513
|
+
if (attrs.error === SERVER_ERROR_CODES.MessageAccountRestriction) {
|
|
1514
|
+
// 463 = 1:1 message missing privacy token (tctoken). Usually means the
|
|
1515
|
+
// account is restricted: WhatsApp blocks starting new chats but preserves
|
|
1516
|
+
// existing ones, since established chats already carry a tctoken.
|
|
1517
|
+
// WA Web prevents this client-side (disables the compose bar).
|
|
1518
|
+
// No retry — retrying counts as another "reach out" and worsens the restriction.
|
|
1403
1519
|
logger.warn({ msgId: attrs.id, from: attrs.from }, 'error 463: account restricted or missing tctoken for contact');
|
|
1520
|
+
const ackFrom = attrs.from;
|
|
1521
|
+
if (ackFrom && !inFlight463Recoveries.has(ackFrom)) {
|
|
1522
|
+
inFlight463Recoveries.add(ackFrom);
|
|
1523
|
+
void (async () => {
|
|
1524
|
+
try {
|
|
1525
|
+
const getPNForLID = signalRepository.lidMapping.getPNForLID.bind(signalRepository.lidMapping);
|
|
1526
|
+
const tcStorageJid = await resolveTcTokenJid(ackFrom, getLIDForPN);
|
|
1527
|
+
const issueJid = await resolveIssuanceJid(ackFrom, sock.serverProps.lidTrustedTokenIssueToLid, getLIDForPN, getPNForLID);
|
|
1528
|
+
const result = await issuePrivacyTokens([issueJid], unixTimestampSeconds());
|
|
1529
|
+
await storeTcTokensFromIqResult({
|
|
1530
|
+
result,
|
|
1531
|
+
fallbackJid: tcStorageJid,
|
|
1532
|
+
keys: authState.keys,
|
|
1533
|
+
getLIDForPN,
|
|
1534
|
+
onNewJidStored: trackTcTokenJid
|
|
1535
|
+
});
|
|
1536
|
+
logger.debug({ from: ackFrom }, 'completed 463 token recovery issuance');
|
|
1537
|
+
}
|
|
1538
|
+
catch (err) {
|
|
1539
|
+
logger.debug({ from: ackFrom, err: err?.message }, 'failed 463 token recovery issuance');
|
|
1540
|
+
}
|
|
1541
|
+
finally {
|
|
1542
|
+
inFlight463Recoveries.delete(ackFrom);
|
|
1543
|
+
}
|
|
1544
|
+
})();
|
|
1545
|
+
}
|
|
1404
1546
|
}
|
|
1405
1547
|
else if (attrs.error === SERVER_ERROR_CODES.SmaxInvalid) {
|
|
1406
1548
|
logger.warn({ msgId: attrs.id, from: attrs.from }, 'smax-invalid (479): stanza rejected by server — likely stale device session or malformed addressing');
|
|
@@ -1515,6 +1657,8 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
1515
1657
|
});
|
|
1516
1658
|
/** timestamp of last tctoken prune run — throttles to once per 24h */
|
|
1517
1659
|
let lastTcTokenPruneTs = 0;
|
|
1660
|
+
/** dedupe in-flight 463 recovery token issuance by target JID */
|
|
1661
|
+
const inFlight463Recoveries = new Set();
|
|
1518
1662
|
ev.on('connection.update', ({ isOnline, connection }) => {
|
|
1519
1663
|
if (typeof isOnline !== 'undefined') {
|
|
1520
1664
|
sendActiveReceipts = isOnline;
|
|
@@ -1544,6 +1688,16 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
1544
1688
|
}
|
|
1545
1689
|
}
|
|
1546
1690
|
});
|
|
1691
|
+
registerSocketEndHandler(() => {
|
|
1692
|
+
if (!config.msgRetryCounterCache && msgRetryCache.close) {
|
|
1693
|
+
msgRetryCache.close();
|
|
1694
|
+
}
|
|
1695
|
+
if (!config.callOfferCache && callOfferCache.close) {
|
|
1696
|
+
callOfferCache.close();
|
|
1697
|
+
}
|
|
1698
|
+
identityAssertDebounce.close();
|
|
1699
|
+
sendActiveReceipts = false;
|
|
1700
|
+
});
|
|
1547
1701
|
async function pruneExpiredTcTokens() {
|
|
1548
1702
|
try {
|
|
1549
1703
|
await tcTokenIndexLoaded;
|