@ryuu-reinzz/baileys 3.5.1 → 5.0.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 +30 -25
- package/WAProto/fix-imports.js +22 -18
- package/WAProto/index.js +22 -18
- package/lib/Defaults/index.js +10 -9
- package/lib/Signal/libsignal.js +46 -19
- package/lib/Signal/lid-mapping.js +6 -0
- package/lib/Socket/chats.js +241 -39
- package/lib/Socket/groups.js +20 -0
- package/lib/Socket/messages-recv.js +736 -314
- package/lib/Socket/messages-send.js +279 -129
- package/lib/Socket/newsletter.js +2 -2
- package/lib/Socket/socket.js +56 -25
- package/lib/Types/{Newsletter.js → Mex.js} +9 -3
- package/lib/Types/State.js +43 -0
- package/lib/Types/index.js +1 -1
- package/lib/Utils/auth-utils.js +12 -0
- package/lib/Utils/chat-utils.js +80 -20
- package/lib/Utils/companion-reg-client-utils.js +35 -0
- package/lib/Utils/decode-wa-message.js +34 -0
- package/lib/Utils/event-buffer.js +49 -1
- package/lib/Utils/generics.js +12 -3
- package/lib/Utils/history.js +12 -9
- package/lib/Utils/identity-change-handler.js +1 -0
- package/lib/Utils/index.js +3 -1
- package/lib/Utils/link-preview.js +2 -2
- package/lib/Utils/message-retry-manager.js +40 -0
- package/lib/Utils/messages-media.js +21 -7
- package/lib/Utils/messages.js +28 -5
- package/lib/Utils/offline-node-processor.js +40 -0
- package/lib/Utils/process-message.js +103 -1
- package/lib/Utils/signal.js +42 -0
- package/lib/Utils/stanza-ack.js +38 -0
- package/lib/Utils/sync-action-utils.js +1 -0
- package/lib/Utils/tc-token-utils.js +149 -4
- package/lib/Utils/validate-connection.js +3 -0
- package/lib/WAUSync/Protocols/USyncContactProtocol.js +26 -3
- package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +25 -0
- package/lib/WAUSync/Protocols/index.js +1 -0
- package/lib/WAUSync/USyncQuery.js +6 -2
- package/lib/WAUSync/USyncUser.js +8 -0
- package/package.json +39 -12
package/lib/Socket/chats.js
CHANGED
|
@@ -1,22 +1,31 @@
|
|
|
1
1
|
import NodeCache from '@cacheable/node-cache';
|
|
2
2
|
import { Boom } from '@hapi/boom';
|
|
3
3
|
import { proto } from '../../WAProto/index.js';
|
|
4
|
-
import { DEFAULT_CACHE_TTLS, PROCESSABLE_HISTORY_TYPES } from '../Defaults/index.js';
|
|
4
|
+
import { DEFAULT_CACHE_TTLS, HISTORY_SYNC_PAUSED_TIMEOUT_MS, PROCESSABLE_HISTORY_TYPES } from '../Defaults/index.js';
|
|
5
5
|
import { ALL_WA_PATCH_NAMES } from '../Types/index.js';
|
|
6
6
|
import { SyncState } from '../Types/State.js';
|
|
7
|
-
import { chatModificationToAppPatch, decodePatches, decodeSyncdSnapshot, encodeSyncdPatch, extractSyncdPatches, generateProfilePicture, getHistoryMsg, newLTHashState, processSyncAction } from '../Utils/index.js';
|
|
7
|
+
import { chatModificationToAppPatch, decodePatches, decodeSyncdSnapshot, encodeSyncdPatch, ensureLTHashStateVersion, extractSyncdPatches, generateProfilePicture, getHistoryMsg, isAppStateSyncIrrecoverable, isMissingKeyError, MAX_SYNC_ATTEMPTS, 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
10
|
import { buildTcTokenFromJid } from '../Utils/tc-token-utils.js';
|
|
11
|
-
import { getBinaryNodeChild, getBinaryNodeChildren, jidDecode, jidNormalizedUser, reduceBinaryNodeToDictionary, S_WHATSAPP_NET } from '../WABinary/index.js';
|
|
11
|
+
import { getBinaryNodeChild, getBinaryNodeChildren, isHostedLidUser, isHostedPnUser, isLidUser, isPnUser, jidDecode, jidNormalizedUser, reduceBinaryNodeToDictionary, S_WHATSAPP_NET } from '../WABinary/index.js';
|
|
12
12
|
import { USyncQuery, USyncUser } from '../WAUSync/index.js';
|
|
13
13
|
import { makeSocket } from './socket.js';
|
|
14
|
-
const MAX_SYNC_ATTEMPTS = 2;
|
|
15
14
|
export const makeChatsSocket = (config) => {
|
|
16
15
|
const { logger, markOnlineOnConnect, fireInitQueries, appStateMacVerification, shouldIgnoreJid, shouldSyncHistoryMessage, getMessage } = config;
|
|
17
16
|
const sock = makeSocket(config);
|
|
18
|
-
const { ev, ws, authState, generateMessageTag, sendNode, query, signalRepository, onUnexpectedError, sendUnifiedSession } = sock;
|
|
17
|
+
const { ev, ws, authState, generateMessageTag, sendNode, query, signalRepository, onUnexpectedError, sendUnifiedSession, registerSocketEndHandler } = sock;
|
|
18
|
+
const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping);
|
|
19
19
|
let privacySettings;
|
|
20
|
+
/** Server-assigned AB props for protocol behavior. */
|
|
21
|
+
const serverProps = {
|
|
22
|
+
/** AB prop 10518: gate tctoken on 1:1 messages. Default true (safe: avoids 463). */
|
|
23
|
+
privacyTokenOn1to1: true,
|
|
24
|
+
/** AB prop 9666: gate tctoken on profile picture IQs. WA Web default: true. */
|
|
25
|
+
profilePicPrivacyToken: true,
|
|
26
|
+
/** AB prop 14303: issue tctokens to LID instead of PN. WA Web default: false. */
|
|
27
|
+
lidTrustedTokenIssueToLid: false
|
|
28
|
+
};
|
|
20
29
|
let syncState = SyncState.Connecting;
|
|
21
30
|
/** this mutex ensures that messages are processed in order */
|
|
22
31
|
const messageMutex = makeMutex();
|
|
@@ -28,14 +37,20 @@ export const makeChatsSocket = (config) => {
|
|
|
28
37
|
const notificationMutex = makeMutex();
|
|
29
38
|
// Timeout for AwaitingInitialSync state
|
|
30
39
|
let awaitingSyncTimeout;
|
|
40
|
+
// In-memory history sync completion tracking (resets on reconnection)
|
|
41
|
+
const historySyncStatus = {
|
|
42
|
+
initialBootstrapComplete: false,
|
|
43
|
+
recentSyncComplete: false
|
|
44
|
+
};
|
|
45
|
+
let historySyncPausedTimeout;
|
|
46
|
+
// Collections blocked on missing app state sync keys (mirrors WA Web's "Blocked" state).
|
|
47
|
+
// When a key arrives via APP_STATE_SYNC_KEY_SHARE, these are re-synced.
|
|
48
|
+
const blockedCollections = new Set();
|
|
31
49
|
const placeholderResendCache = config.placeholderResendCache ||
|
|
32
50
|
new NodeCache({
|
|
33
51
|
stdTTL: DEFAULT_CACHE_TTLS.MSG_RETRY, // 1 hour
|
|
34
52
|
useClones: false
|
|
35
53
|
});
|
|
36
|
-
if (!config.placeholderResendCache) {
|
|
37
|
-
config.placeholderResendCache = placeholderResendCache;
|
|
38
|
-
}
|
|
39
54
|
/** helper function to fetch the given app state sync key */
|
|
40
55
|
const getAppStateSyncKey = async (keyId) => {
|
|
41
56
|
const { [keyId]: key } = await authState.keys.get('app-state-sync-key', [keyId]);
|
|
@@ -258,6 +273,42 @@ export const makeChatsSocket = (config) => {
|
|
|
258
273
|
return getBinaryNodeChildren(listNode, 'item').map(n => n.attrs.jid);
|
|
259
274
|
};
|
|
260
275
|
const updateBlockStatus = async (jid, action) => {
|
|
276
|
+
const normalizedJid = jidNormalizedUser(jid);
|
|
277
|
+
let lid;
|
|
278
|
+
let pn_jid;
|
|
279
|
+
if (isLidUser(normalizedJid) || isHostedLidUser(normalizedJid)) {
|
|
280
|
+
lid = normalizedJid;
|
|
281
|
+
if (action === 'block') {
|
|
282
|
+
const pn = await signalRepository.lidMapping.getPNForLID(normalizedJid);
|
|
283
|
+
if (!pn) {
|
|
284
|
+
throw new Boom(`Unable to resolve PN JID for LID: ${jid}`, { statusCode: 400 });
|
|
285
|
+
}
|
|
286
|
+
pn_jid = jidNormalizedUser(pn);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
else if (isPnUser(normalizedJid) || isHostedPnUser(normalizedJid)) {
|
|
290
|
+
const mapped = await signalRepository.lidMapping.getLIDForPN(normalizedJid);
|
|
291
|
+
if (!mapped) {
|
|
292
|
+
throw new Boom(`Unable to resolve LID for PN JID: ${jid}`, { statusCode: 400 });
|
|
293
|
+
}
|
|
294
|
+
lid = mapped;
|
|
295
|
+
if (action === 'block') {
|
|
296
|
+
pn_jid = jidNormalizedUser(normalizedJid);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
throw new Boom(`Invalid jid: ${jid}`, { statusCode: 400 });
|
|
301
|
+
}
|
|
302
|
+
const itemAttrs = {
|
|
303
|
+
action,
|
|
304
|
+
jid: lid
|
|
305
|
+
};
|
|
306
|
+
if (action === 'block') {
|
|
307
|
+
if (!pn_jid) {
|
|
308
|
+
throw new Boom(`pn_jid required for block: ${jid}`, { statusCode: 400 });
|
|
309
|
+
}
|
|
310
|
+
itemAttrs.pn_jid = pn_jid;
|
|
311
|
+
}
|
|
261
312
|
await query({
|
|
262
313
|
tag: 'iq',
|
|
263
314
|
attrs: {
|
|
@@ -268,10 +319,7 @@ export const makeChatsSocket = (config) => {
|
|
|
268
319
|
content: [
|
|
269
320
|
{
|
|
270
321
|
tag: 'item',
|
|
271
|
-
attrs:
|
|
272
|
-
action,
|
|
273
|
-
jid
|
|
274
|
-
}
|
|
322
|
+
attrs: itemAttrs
|
|
275
323
|
}
|
|
276
324
|
]
|
|
277
325
|
});
|
|
@@ -370,6 +418,9 @@ export const makeChatsSocket = (config) => {
|
|
|
370
418
|
const collectionsToHandle = new Set(collections);
|
|
371
419
|
// in case something goes wrong -- ensure we don't enter a loop that cannot be exited from
|
|
372
420
|
const attemptsMap = {};
|
|
421
|
+
// collections that failed and need a full snapshot on retry
|
|
422
|
+
// mirrors WA Web's ErrorFatal -> force snapshot behavior
|
|
423
|
+
const forceSnapshotCollections = new Set();
|
|
373
424
|
// keep executing till all collections are done
|
|
374
425
|
// sometimes a single patch request will not return all the patches (God knows why)
|
|
375
426
|
// so we fetch till they're all done (this is determined by the "has_more_patches" flag)
|
|
@@ -380,6 +431,7 @@ export const makeChatsSocket = (config) => {
|
|
|
380
431
|
const result = await authState.keys.get('app-state-sync-version', [name]);
|
|
381
432
|
let state = result[name];
|
|
382
433
|
if (state) {
|
|
434
|
+
state = ensureLTHashStateVersion(state);
|
|
383
435
|
if (typeof initialVersionMap[name] === 'undefined') {
|
|
384
436
|
initialVersionMap[name] = state.version;
|
|
385
437
|
}
|
|
@@ -388,14 +440,18 @@ export const makeChatsSocket = (config) => {
|
|
|
388
440
|
state = newLTHashState();
|
|
389
441
|
}
|
|
390
442
|
states[name] = state;
|
|
391
|
-
|
|
443
|
+
const shouldForceSnapshot = forceSnapshotCollections.has(name);
|
|
444
|
+
if (shouldForceSnapshot) {
|
|
445
|
+
forceSnapshotCollections.delete(name);
|
|
446
|
+
}
|
|
447
|
+
logger.info(`resyncing ${name} from v${state.version}${shouldForceSnapshot ? ' (forcing snapshot)' : ''}`);
|
|
392
448
|
nodes.push({
|
|
393
449
|
tag: 'collection',
|
|
394
450
|
attrs: {
|
|
395
451
|
name,
|
|
396
452
|
version: state.version.toString(),
|
|
397
|
-
// return snapshot if
|
|
398
|
-
return_snapshot: (!state.version).toString()
|
|
453
|
+
// return snapshot if syncing from scratch or forcing after a failed attempt
|
|
454
|
+
return_snapshot: (shouldForceSnapshot || !state.version).toString()
|
|
399
455
|
}
|
|
400
456
|
});
|
|
401
457
|
}
|
|
@@ -421,7 +477,7 @@ export const makeChatsSocket = (config) => {
|
|
|
421
477
|
const { patches, hasMorePatches, snapshot } = decoded[name];
|
|
422
478
|
try {
|
|
423
479
|
if (snapshot) {
|
|
424
|
-
const { state: newState, mutationMap } = await decodeSyncdSnapshot(name, snapshot, getCachedAppStateSyncKey, initialVersionMap[name], appStateMacVerification.snapshot);
|
|
480
|
+
const { state: newState, mutationMap } = await decodeSyncdSnapshot(name, snapshot, getCachedAppStateSyncKey, initialVersionMap[name], appStateMacVerification.snapshot, logger);
|
|
425
481
|
states[name] = newState;
|
|
426
482
|
Object.assign(globalMutationMap, mutationMap);
|
|
427
483
|
logger.info(`restored state of ${name} from snapshot to v${newState.version} with mutations`);
|
|
@@ -444,19 +500,37 @@ export const makeChatsSocket = (config) => {
|
|
|
444
500
|
}
|
|
445
501
|
}
|
|
446
502
|
catch (error) {
|
|
447
|
-
// if retry attempts overshoot
|
|
448
|
-
// or key not found
|
|
449
|
-
const isIrrecoverableError = attemptsMap[name] >= MAX_SYNC_ATTEMPTS ||
|
|
450
|
-
error.output?.statusCode === 404 ||
|
|
451
|
-
error.name === 'TypeError';
|
|
452
|
-
logger.info({ name, error: error.stack }, `failed to sync state from version${isIrrecoverableError ? '' : ', removing and trying from scratch'}`);
|
|
453
|
-
await authState.keys.set({ 'app-state-sync-version': { [name]: null } });
|
|
454
|
-
// increment number of retries
|
|
455
503
|
attemptsMap[name] = (attemptsMap[name] || 0) + 1;
|
|
456
|
-
|
|
457
|
-
|
|
504
|
+
const logData = {
|
|
505
|
+
name,
|
|
506
|
+
attempt: attemptsMap[name],
|
|
507
|
+
version: states[name].version,
|
|
508
|
+
statusCode: error.output?.statusCode,
|
|
509
|
+
errorType: error.name,
|
|
510
|
+
error: error.stack
|
|
511
|
+
};
|
|
512
|
+
if (isMissingKeyError(error) && attemptsMap[name] >= MAX_SYNC_ATTEMPTS) {
|
|
513
|
+
// WA Web treats missing keys as "Blocked" — park the collection
|
|
514
|
+
// until the key arrives via APP_STATE_SYNC_KEY_SHARE.
|
|
515
|
+
logger.warn(logData, `${name} blocked on missing key from v${states[name].version}, parking after ${attemptsMap[name]} attempts`);
|
|
516
|
+
blockedCollections.add(name);
|
|
458
517
|
collectionsToHandle.delete(name);
|
|
459
518
|
}
|
|
519
|
+
else if (isMissingKeyError(error)) {
|
|
520
|
+
// Retry with a snapshot which may use a different key.
|
|
521
|
+
logger.info(logData, `${name} blocked on missing key from v${states[name].version}, retrying with snapshot`);
|
|
522
|
+
forceSnapshotCollections.add(name);
|
|
523
|
+
}
|
|
524
|
+
else if (isAppStateSyncIrrecoverable(error, attemptsMap[name])) {
|
|
525
|
+
logger.warn(logData, `failed to sync ${name} from v${states[name].version}, giving up`);
|
|
526
|
+
collectionsToHandle.delete(name);
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
logger.info(logData, `failed to sync ${name} from v${states[name].version}, forcing snapshot retry`);
|
|
530
|
+
// force a full snapshot on retry to recover from
|
|
531
|
+
// corrupted local state (e.g. LTHash MAC mismatch)
|
|
532
|
+
forceSnapshotCollections.add(name);
|
|
533
|
+
}
|
|
460
534
|
}
|
|
461
535
|
}
|
|
462
536
|
}
|
|
@@ -473,7 +547,22 @@ export const makeChatsSocket = (config) => {
|
|
|
473
547
|
*/
|
|
474
548
|
const profilePictureUrl = async (jid, type = 'preview', timeoutMs) => {
|
|
475
549
|
const baseContent = [{ tag: 'picture', attrs: { type, query: 'url' } }];
|
|
476
|
-
|
|
550
|
+
// WA Web only includes tctoken for user JIDs (not groups/newsletters)
|
|
551
|
+
// and never for own profile pic (Chat model for self has no tcToken).
|
|
552
|
+
// Including tctoken for own JID causes the server to never respond.
|
|
553
|
+
const normalizedJid = jidNormalizedUser(jid);
|
|
554
|
+
const isUserJid = isPnUser(normalizedJid) || isLidUser(normalizedJid);
|
|
555
|
+
const me = authState.creds.me;
|
|
556
|
+
const isSelf = me && (normalizedJid === jidNormalizedUser(me.id) || (me.lid && normalizedJid === jidNormalizedUser(me.lid)));
|
|
557
|
+
let content = baseContent;
|
|
558
|
+
if (serverProps.profilePicPrivacyToken && isUserJid && !isSelf) {
|
|
559
|
+
content = await buildTcTokenFromJid({
|
|
560
|
+
authState,
|
|
561
|
+
jid: normalizedJid,
|
|
562
|
+
baseContent,
|
|
563
|
+
getLIDForPN
|
|
564
|
+
});
|
|
565
|
+
}
|
|
477
566
|
jid = jidNormalizedUser(jid);
|
|
478
567
|
const result = await query({
|
|
479
568
|
tag: 'iq',
|
|
@@ -483,7 +572,7 @@ export const makeChatsSocket = (config) => {
|
|
|
483
572
|
type: 'get',
|
|
484
573
|
xmlns: 'w:profile:picture'
|
|
485
574
|
},
|
|
486
|
-
content
|
|
575
|
+
content
|
|
487
576
|
}, timeoutMs);
|
|
488
577
|
const child = getBinaryNodeChild(result, 'picture');
|
|
489
578
|
return child?.attrs?.url;
|
|
@@ -549,7 +638,12 @@ export const makeChatsSocket = (config) => {
|
|
|
549
638
|
* @param tcToken token for subscription, use if present
|
|
550
639
|
*/
|
|
551
640
|
const presenceSubscribe = async (toJid) => {
|
|
552
|
-
|
|
641
|
+
// Only include tctoken for user JIDs — groups/newsletters don't use tctokens
|
|
642
|
+
const normalizedToJid = jidNormalizedUser(toJid);
|
|
643
|
+
const isUserJid = isPnUser(normalizedToJid) || isLidUser(normalizedToJid);
|
|
644
|
+
const tcTokenContent = isUserJid
|
|
645
|
+
? await buildTcTokenFromJid({ authState, jid: normalizedToJid, getLIDForPN })
|
|
646
|
+
: undefined;
|
|
553
647
|
return sendNode({
|
|
554
648
|
tag: 'presence',
|
|
555
649
|
attrs: {
|
|
@@ -570,7 +664,8 @@ export const makeChatsSocket = (config) => {
|
|
|
570
664
|
if (tag === 'presence') {
|
|
571
665
|
presence = {
|
|
572
666
|
lastKnownPresence: attrs.type === 'unavailable' ? 'unavailable' : 'available',
|
|
573
|
-
lastSeen: attrs.last && attrs.last !== 'deny' ? +attrs.last : undefined
|
|
667
|
+
lastSeen: attrs.last && attrs.last !== 'deny' ? +attrs.last : undefined,
|
|
668
|
+
groupOnlineCount: attrs.count ? +attrs.count : undefined
|
|
574
669
|
};
|
|
575
670
|
}
|
|
576
671
|
else if (Array.isArray(content)) {
|
|
@@ -604,7 +699,7 @@ export const makeChatsSocket = (config) => {
|
|
|
604
699
|
logger.debug({ patch: patchCreate }, 'applying app patch');
|
|
605
700
|
await resyncAppState([name], false);
|
|
606
701
|
const { [name]: currentSyncVersion } = await authState.keys.get('app-state-sync-version', [name]);
|
|
607
|
-
initial = currentSyncVersion
|
|
702
|
+
initial = currentSyncVersion ? ensureLTHashStateVersion(currentSyncVersion) : newLTHashState();
|
|
608
703
|
encodeResult = await encodeSyncdPatch(patchCreate, myAppStateKeyId, initial, getAppStateSyncKey);
|
|
609
704
|
const { patch, state } = encodeResult;
|
|
610
705
|
const node = {
|
|
@@ -650,22 +745,21 @@ export const makeChatsSocket = (config) => {
|
|
|
650
745
|
}
|
|
651
746
|
}
|
|
652
747
|
};
|
|
653
|
-
/**
|
|
748
|
+
/** fetch AB props */
|
|
654
749
|
const fetchProps = async () => {
|
|
655
|
-
//TODO: implement both protocol 1 and protocol 2 prop fetching, specially for abKey for WM
|
|
656
750
|
const resultNode = await query({
|
|
657
751
|
tag: 'iq',
|
|
658
752
|
attrs: {
|
|
659
753
|
to: S_WHATSAPP_NET,
|
|
660
|
-
xmlns: '
|
|
754
|
+
xmlns: 'abt',
|
|
661
755
|
type: 'get'
|
|
662
756
|
},
|
|
663
757
|
content: [
|
|
664
758
|
{
|
|
665
759
|
tag: 'props',
|
|
666
760
|
attrs: {
|
|
667
|
-
protocol: '
|
|
668
|
-
hash: authState
|
|
761
|
+
protocol: '1',
|
|
762
|
+
...(authState?.creds?.lastPropHash ? { hash: authState.creds.lastPropHash } : {})
|
|
669
763
|
}
|
|
670
764
|
}
|
|
671
765
|
]
|
|
@@ -680,7 +774,20 @@ export const makeChatsSocket = (config) => {
|
|
|
680
774
|
}
|
|
681
775
|
props = reduceBinaryNodeToDictionary(propsNode, 'prop');
|
|
682
776
|
}
|
|
683
|
-
|
|
777
|
+
// Extract protocol-relevant AB props (only the ones we need)
|
|
778
|
+
const privacyTokenProp = props['10518'] ?? props['privacy_token_sending_on_all_1_on_1_messages'];
|
|
779
|
+
if (privacyTokenProp !== undefined) {
|
|
780
|
+
serverProps.privacyTokenOn1to1 = privacyTokenProp === 'true' || privacyTokenProp === '1';
|
|
781
|
+
}
|
|
782
|
+
const profilePicProp = props['9666'] ?? props['profile_scraping_privacy_token_in_photo_iq'];
|
|
783
|
+
if (profilePicProp !== undefined) {
|
|
784
|
+
serverProps.profilePicPrivacyToken = profilePicProp === 'true' || profilePicProp === '1';
|
|
785
|
+
}
|
|
786
|
+
const lidIssueProp = props['14303'] ?? props['lid_trusted_token_issue_to_lid'];
|
|
787
|
+
if (lidIssueProp !== undefined) {
|
|
788
|
+
serverProps.lidTrustedTokenIssueToLid = lidIssueProp === 'true' || lidIssueProp === '1';
|
|
789
|
+
}
|
|
790
|
+
logger.debug({ serverProps }, 'fetched props');
|
|
684
791
|
return props;
|
|
685
792
|
};
|
|
686
793
|
/**
|
|
@@ -820,6 +927,47 @@ export const makeChatsSocket = (config) => {
|
|
|
820
927
|
? shouldSyncHistoryMessage(historyMsg) &&
|
|
821
928
|
PROCESSABLE_HISTORY_TYPES.includes(historyMsg.syncType)
|
|
822
929
|
: false;
|
|
930
|
+
if (historyMsg && shouldProcessHistoryMsg) {
|
|
931
|
+
const syncType = historyMsg.syncType;
|
|
932
|
+
// INITIAL_BOOTSTRAP — fire immediately, no progress check (same as WA Web K function)
|
|
933
|
+
if (syncType === proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP &&
|
|
934
|
+
!historySyncStatus.initialBootstrapComplete) {
|
|
935
|
+
historySyncStatus.initialBootstrapComplete = true;
|
|
936
|
+
ev.emit('messaging-history.status', {
|
|
937
|
+
syncType,
|
|
938
|
+
status: 'complete',
|
|
939
|
+
explicit: true
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
// RECENT with progress === 100 — explicit completion
|
|
943
|
+
if (syncType === proto.HistorySync.HistorySyncType.RECENT &&
|
|
944
|
+
historyMsg.progress === 100 &&
|
|
945
|
+
!historySyncStatus.recentSyncComplete) {
|
|
946
|
+
historySyncStatus.recentSyncComplete = true;
|
|
947
|
+
clearTimeout(historySyncPausedTimeout);
|
|
948
|
+
historySyncPausedTimeout = undefined;
|
|
949
|
+
ev.emit('messaging-history.status', {
|
|
950
|
+
syncType,
|
|
951
|
+
status: 'complete',
|
|
952
|
+
explicit: true
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
// Reset 120s paused timeout on any RECENT chunk (like WA Web's handleChunkProgress)
|
|
956
|
+
if (syncType === proto.HistorySync.HistorySyncType.RECENT && !historySyncStatus.recentSyncComplete) {
|
|
957
|
+
clearTimeout(historySyncPausedTimeout);
|
|
958
|
+
historySyncPausedTimeout = setTimeout(() => {
|
|
959
|
+
if (!historySyncStatus.recentSyncComplete) {
|
|
960
|
+
historySyncStatus.recentSyncComplete = true;
|
|
961
|
+
ev.emit('messaging-history.status', {
|
|
962
|
+
syncType: proto.HistorySync.HistorySyncType.RECENT,
|
|
963
|
+
status: 'paused',
|
|
964
|
+
explicit: false
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
historySyncPausedTimeout = undefined;
|
|
968
|
+
}, HISTORY_SYNC_PAUSED_TIMEOUT_MS);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
823
971
|
// State machine: decide on sync and flush
|
|
824
972
|
if (historyMsg && syncState === SyncState.AwaitingInitialSync) {
|
|
825
973
|
if (awaitingSyncTimeout) {
|
|
@@ -839,6 +987,8 @@ export const makeChatsSocket = (config) => {
|
|
|
839
987
|
}
|
|
840
988
|
const doAppStateSync = async () => {
|
|
841
989
|
if (syncState === SyncState.Syncing) {
|
|
990
|
+
// All collections will be synced, so clear any blocked ones
|
|
991
|
+
blockedCollections.clear();
|
|
842
992
|
logger.info('Doing app state sync');
|
|
843
993
|
await resyncAppState(ALL_WA_PATCH_NAMES, true);
|
|
844
994
|
// Sync is complete, go online and flush everything
|
|
@@ -898,6 +1048,11 @@ export const makeChatsSocket = (config) => {
|
|
|
898
1048
|
}
|
|
899
1049
|
});
|
|
900
1050
|
ev.on('connection.update', ({ connection, receivedPendingNotifications }) => {
|
|
1051
|
+
if (connection === 'close') {
|
|
1052
|
+
blockedCollections.clear();
|
|
1053
|
+
clearTimeout(historySyncPausedTimeout);
|
|
1054
|
+
historySyncPausedTimeout = undefined;
|
|
1055
|
+
}
|
|
901
1056
|
if (connection === 'open') {
|
|
902
1057
|
if (fireInitQueries) {
|
|
903
1058
|
executeInitQueries().catch(error => onUnexpectedError(error, 'init queries'));
|
|
@@ -907,6 +1062,10 @@ export const makeChatsSocket = (config) => {
|
|
|
907
1062
|
if (!receivedPendingNotifications || syncState !== SyncState.Connecting) {
|
|
908
1063
|
return;
|
|
909
1064
|
}
|
|
1065
|
+
historySyncStatus.initialBootstrapComplete = false;
|
|
1066
|
+
historySyncStatus.recentSyncComplete = false;
|
|
1067
|
+
clearTimeout(historySyncPausedTimeout);
|
|
1068
|
+
historySyncPausedTimeout = undefined;
|
|
910
1069
|
syncState = SyncState.AwaitingInitialSync;
|
|
911
1070
|
logger.info('Connection is now AwaitingInitialSync, buffering events');
|
|
912
1071
|
ev.buffer();
|
|
@@ -919,19 +1078,49 @@ export const makeChatsSocket = (config) => {
|
|
|
919
1078
|
setTimeout(() => ev.flush(), 0);
|
|
920
1079
|
return;
|
|
921
1080
|
}
|
|
922
|
-
|
|
1081
|
+
// On reconnection (accountSyncCounter > 0), the server does not push
|
|
1082
|
+
// history sync notifications — the device already has its data.
|
|
1083
|
+
// Skip the 20s wait and go online immediately.
|
|
1084
|
+
if (authState.creds.accountSyncCounter > 0) {
|
|
1085
|
+
logger.info('Reconnection with existing sync data, skipping history sync wait. Transitioning to Online.');
|
|
1086
|
+
syncState = SyncState.Online;
|
|
1087
|
+
setTimeout(() => ev.flush(), 0);
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
logger.info('First connection, awaiting history sync notification with a 20s timeout.');
|
|
923
1091
|
if (awaitingSyncTimeout) {
|
|
924
1092
|
clearTimeout(awaitingSyncTimeout);
|
|
925
1093
|
}
|
|
926
1094
|
awaitingSyncTimeout = setTimeout(() => {
|
|
927
1095
|
if (syncState === SyncState.AwaitingInitialSync) {
|
|
928
|
-
// TODO: investigate
|
|
929
1096
|
logger.warn('Timeout in AwaitingInitialSync, forcing state to Online and flushing buffer');
|
|
930
1097
|
syncState = SyncState.Online;
|
|
931
1098
|
ev.flush();
|
|
1099
|
+
// Increment so subsequent reconnections skip the 20s wait.
|
|
1100
|
+
// Late-arriving history is still processed via processMessage
|
|
1101
|
+
// regardless of the state machine phase.
|
|
1102
|
+
const accountSyncCounter = (authState.creds.accountSyncCounter || 0) + 1;
|
|
1103
|
+
ev.emit('creds.update', { accountSyncCounter });
|
|
932
1104
|
}
|
|
933
1105
|
}, 20000);
|
|
934
1106
|
});
|
|
1107
|
+
// When an app state sync key arrives (myAppStateKeyId is set) and there are
|
|
1108
|
+
// collections blocked on a missing key, trigger a re-sync for just those collections.
|
|
1109
|
+
// This mirrors WA Web's Blocked → retry-on-key-arrival behavior.
|
|
1110
|
+
ev.on('creds.update', ({ myAppStateKeyId }) => {
|
|
1111
|
+
if (!myAppStateKeyId || blockedCollections.size === 0) {
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
// If we're in the middle of a full sync, doAppStateSync handles all collections
|
|
1115
|
+
if (syncState === SyncState.Syncing) {
|
|
1116
|
+
blockedCollections.clear();
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
const collections = [...blockedCollections];
|
|
1120
|
+
blockedCollections.clear();
|
|
1121
|
+
logger.info({ collections }, 'app state sync key arrived, re-syncing blocked collections');
|
|
1122
|
+
resyncAppState(collections, false).catch(error => onUnexpectedError(error, 'blocked collections resync'));
|
|
1123
|
+
});
|
|
935
1124
|
ev.on('lid-mapping.update', async ({ lid, pn }) => {
|
|
936
1125
|
try {
|
|
937
1126
|
await signalRepository.lidMapping.storeLIDPNMappings([{ lid, pn }]);
|
|
@@ -940,8 +1129,20 @@ export const makeChatsSocket = (config) => {
|
|
|
940
1129
|
logger.warn({ lid, pn, error }, 'Failed to store LID-PN mapping');
|
|
941
1130
|
}
|
|
942
1131
|
});
|
|
1132
|
+
registerSocketEndHandler(() => {
|
|
1133
|
+
if (awaitingSyncTimeout) {
|
|
1134
|
+
clearTimeout(awaitingSyncTimeout);
|
|
1135
|
+
awaitingSyncTimeout = undefined;
|
|
1136
|
+
}
|
|
1137
|
+
if (!config.placeholderResendCache && placeholderResendCache.close) {
|
|
1138
|
+
placeholderResendCache.close();
|
|
1139
|
+
}
|
|
1140
|
+
syncState = SyncState.Connecting;
|
|
1141
|
+
privacySettings = undefined;
|
|
1142
|
+
});
|
|
943
1143
|
return {
|
|
944
1144
|
...sock,
|
|
1145
|
+
serverProps,
|
|
945
1146
|
createCallLink,
|
|
946
1147
|
getBotListV2,
|
|
947
1148
|
messageMutex,
|
|
@@ -978,6 +1179,7 @@ export const makeChatsSocket = (config) => {
|
|
|
978
1179
|
cleanDirtyBits,
|
|
979
1180
|
addOrEditContact,
|
|
980
1181
|
removeContact,
|
|
1182
|
+
placeholderResendCache,
|
|
981
1183
|
addLabel,
|
|
982
1184
|
addChatLabel,
|
|
983
1185
|
removeChatLabel,
|
package/lib/Socket/groups.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Boom } from '@hapi/boom';
|
|
1
2
|
import { proto } from '../../WAProto/index.js';
|
|
2
3
|
import { WAMessageAddressingMode, WAMessageStubType } from '../Types/index.js';
|
|
3
4
|
import { generateMessageIDV2, unixTimestampSeconds } from '../Utils/index.js';
|
|
@@ -270,16 +271,31 @@ export const makeGroupsSocket = (config) => {
|
|
|
270
271
|
};
|
|
271
272
|
export const extractGroupMetadata = (result) => {
|
|
272
273
|
const group = getBinaryNodeChild(result, 'group');
|
|
274
|
+
if (!group) {
|
|
275
|
+
// Mirror WAWeb: surface server/client errors with their code+text instead of crashing.
|
|
276
|
+
const errorNode = getBinaryNodeChild(result, 'error');
|
|
277
|
+
if (errorNode) {
|
|
278
|
+
const code = errorNode.attrs.code ? +errorNode.attrs.code : 500;
|
|
279
|
+
const text = errorNode.attrs.text || 'group metadata query failed';
|
|
280
|
+
throw new Boom(text, { statusCode: code, data: errorNode });
|
|
281
|
+
}
|
|
282
|
+
throw new Boom('Invalid group metadata response: missing <group> node', { data: result });
|
|
283
|
+
}
|
|
284
|
+
if (!group.attrs.id) {
|
|
285
|
+
throw new Boom('Invalid group metadata response: missing group id', { data: group });
|
|
286
|
+
}
|
|
273
287
|
const descChild = getBinaryNodeChild(group, 'description');
|
|
274
288
|
let desc;
|
|
275
289
|
let descId;
|
|
276
290
|
let descOwner;
|
|
277
291
|
let descOwnerPn;
|
|
292
|
+
let descOwnerUsername;
|
|
278
293
|
let descTime;
|
|
279
294
|
if (descChild) {
|
|
280
295
|
desc = getBinaryNodeChildString(descChild, 'body');
|
|
281
296
|
descOwner = descChild.attrs.participant ? jidNormalizedUser(descChild.attrs.participant) : undefined;
|
|
282
297
|
descOwnerPn = descChild.attrs.participant_pn ? jidNormalizedUser(descChild.attrs.participant_pn) : undefined;
|
|
298
|
+
descOwnerUsername = descChild.attrs.participant_username || undefined;
|
|
283
299
|
descTime = +descChild.attrs.t;
|
|
284
300
|
descId = descChild.attrs.id;
|
|
285
301
|
}
|
|
@@ -293,16 +309,19 @@ export const extractGroupMetadata = (result) => {
|
|
|
293
309
|
subject: group.attrs.subject,
|
|
294
310
|
subjectOwner: group.attrs.s_o,
|
|
295
311
|
subjectOwnerPn: group.attrs.s_o_pn,
|
|
312
|
+
subjectOwnerUsername: group.attrs.s_o_username,
|
|
296
313
|
subjectTime: +group.attrs.s_t,
|
|
297
314
|
size: group.attrs.size ? +group.attrs.size : getBinaryNodeChildren(group, 'participant').length,
|
|
298
315
|
creation: +group.attrs.creation,
|
|
299
316
|
owner: group.attrs.creator ? jidNormalizedUser(group.attrs.creator) : undefined,
|
|
300
317
|
ownerPn: group.attrs.creator_pn ? jidNormalizedUser(group.attrs.creator_pn) : undefined,
|
|
318
|
+
ownerUsername: group.attrs.creator_username || undefined,
|
|
301
319
|
owner_country_code: group.attrs.creator_country_code,
|
|
302
320
|
desc,
|
|
303
321
|
descId,
|
|
304
322
|
descOwner,
|
|
305
323
|
descOwnerPn,
|
|
324
|
+
descOwnerUsername,
|
|
306
325
|
descTime,
|
|
307
326
|
linkedParent: getBinaryNodeChild(group, 'linked_parent')?.attrs.jid || undefined,
|
|
308
327
|
restrict: !!getBinaryNodeChild(group, 'locked'),
|
|
@@ -317,6 +336,7 @@ export const extractGroupMetadata = (result) => {
|
|
|
317
336
|
id: attrs.jid,
|
|
318
337
|
phoneNumber: isLidUser(attrs.jid) && isPnUser(attrs.phone_number) ? attrs.phone_number : undefined,
|
|
319
338
|
lid: isPnUser(attrs.jid) && isLidUser(attrs.lid) ? attrs.lid : undefined,
|
|
339
|
+
username: attrs.participant_username || attrs.username || undefined,
|
|
320
340
|
admin: (attrs.type || null)
|
|
321
341
|
};
|
|
322
342
|
}),
|