@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
package/WAProto/index.js
CHANGED
|
@@ -12,14 +12,18 @@ function longToString(value, unsigned) {
|
|
|
12
12
|
if (typeof value === "number") {
|
|
13
13
|
return String(value);
|
|
14
14
|
}
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
// Fast path: convert Long {low, high} directly via native BigInt
|
|
16
|
+
// BigInt.toString() is a native C++ operation, much faster than Long's pure JS division loops
|
|
17
|
+
if (value && typeof value.low === "number" && typeof value.high === "number") {
|
|
18
|
+
const lo = BigInt(value.low >>> 0);
|
|
19
|
+
const hi = BigInt(value.high >>> 0);
|
|
20
|
+
const combined = (hi << 32n) | lo;
|
|
21
|
+
if (!unsigned && value.high < 0) {
|
|
22
|
+
return (combined - (1n << 64n)).toString();
|
|
23
|
+
}
|
|
24
|
+
return combined.toString();
|
|
17
25
|
}
|
|
18
|
-
|
|
19
|
-
const prepared = unsigned && normalized && typeof normalized.toUnsigned === "function"
|
|
20
|
-
? normalized.toUnsigned()
|
|
21
|
-
: normalized;
|
|
22
|
-
return prepared.toString();
|
|
26
|
+
return String(value);
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
function longToNumber(value, unsigned) {
|
|
@@ -27,19 +31,19 @@ function longToNumber(value, unsigned) {
|
|
|
27
31
|
return value;
|
|
28
32
|
}
|
|
29
33
|
if (typeof value === "string") {
|
|
30
|
-
const numeric = Number(value);
|
|
31
|
-
return numeric;
|
|
32
|
-
}
|
|
33
|
-
if (!$util.Long) {
|
|
34
34
|
return Number(value);
|
|
35
35
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
36
|
+
// Fast path: convert Long {low, high} directly via native BigInt
|
|
37
|
+
if (value && typeof value.low === "number" && typeof value.high === "number") {
|
|
38
|
+
const lo = BigInt(value.low >>> 0);
|
|
39
|
+
const hi = BigInt(value.high >>> 0);
|
|
40
|
+
const combined = (hi << 32n) | lo;
|
|
41
|
+
if (!unsigned && value.high < 0) {
|
|
42
|
+
return Number(combined - (1n << 64n));
|
|
43
|
+
}
|
|
44
|
+
return Number(combined);
|
|
45
|
+
}
|
|
46
|
+
return Number(value);
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
export const proto = $root.proto = (() => {
|
package/lib/Defaults/index.js
CHANGED
|
@@ -126,6 +126,8 @@ export const MEDIA_HKDF_KEY_MAPPING = {
|
|
|
126
126
|
'biz-cover-photo': 'Image'
|
|
127
127
|
};
|
|
128
128
|
export const MEDIA_KEYS = Object.keys(MEDIA_PATH_MAP);
|
|
129
|
+
/** 120s timeout for history sync stall detection, same as WA Web's handleChunkProgress / restartPausedTimer (g = 120) */
|
|
130
|
+
export const HISTORY_SYNC_PAUSED_TIMEOUT_MS = 120_000;
|
|
129
131
|
export const MIN_PREKEY_COUNT = 5;
|
|
130
132
|
export const INITIAL_PREKEY_COUNT = 812;
|
|
131
133
|
export const UPLOAD_TIMEOUT = 30000; // 30 seconds
|
package/lib/Socket/chats.js
CHANGED
|
@@ -1,24 +1,33 @@
|
|
|
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, isLidUser,
|
|
11
|
+
import { getBinaryNodeChild, getBinaryNodeChildren, isPnUser, isLidUser, isHostedLidUser, isHostedPnUser, 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
|
// Lia@Note 08-02-26 --- I know it's not efficient for RSS ಥ‿ಥ
|
|
16
15
|
const USER_ID_CACHE = new Map();
|
|
17
16
|
export const makeChatsSocket = (config) => {
|
|
18
17
|
const { logger, markOnlineOnConnect, fireInitQueries, appStateMacVerification, shouldIgnoreJid, shouldSyncHistoryMessage, getMessage } = config;
|
|
19
18
|
const sock = makeSocket(config);
|
|
20
19
|
const { ev, ws, authState, generateMessageTag, sendNode, query, signalRepository, onUnexpectedError, sendUnifiedSession, registerSocketEndHandler } = sock;
|
|
20
|
+
const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping);
|
|
21
21
|
let privacySettings;
|
|
22
|
+
/** Server-assigned AB props for protocol behavior. */
|
|
23
|
+
const serverProps = {
|
|
24
|
+
/** AB prop 10518: gate tctoken on 1:1 messages. Default true (safe: avoids 463). */
|
|
25
|
+
privacyTokenOn1to1: true,
|
|
26
|
+
/** AB prop 9666: gate tctoken on profile picture IQs. WA Web default: true. */
|
|
27
|
+
profilePicPrivacyToken: true,
|
|
28
|
+
/** AB prop 14303: issue tctokens to LID instead of PN. WA Web default: false. */
|
|
29
|
+
lidTrustedTokenIssueToLid: false
|
|
30
|
+
};
|
|
22
31
|
let syncState = SyncState.Connecting;
|
|
23
32
|
/** this mutex ensures that messages are processed in order */
|
|
24
33
|
const messageMutex = makeMutex();
|
|
@@ -30,6 +39,15 @@ export const makeChatsSocket = (config) => {
|
|
|
30
39
|
const notificationMutex = makeMutex();
|
|
31
40
|
// Timeout for AwaitingInitialSync state
|
|
32
41
|
let awaitingSyncTimeout;
|
|
42
|
+
// In-memory history sync completion tracking (resets on reconnection)
|
|
43
|
+
const historySyncStatus = {
|
|
44
|
+
initialBootstrapComplete: false,
|
|
45
|
+
recentSyncComplete: false
|
|
46
|
+
};
|
|
47
|
+
let historySyncPausedTimeout;
|
|
48
|
+
// Collections blocked on missing app state sync keys (mirrors WA Web's "Blocked" state).
|
|
49
|
+
// When a key arrives via APP_STATE_SYNC_KEY_SHARE, these are re-synced.
|
|
50
|
+
const blockedCollections = new Set();
|
|
33
51
|
const placeholderResendCache = config.placeholderResendCache ||
|
|
34
52
|
new NodeCache({
|
|
35
53
|
stdTTL: DEFAULT_CACHE_TTLS.MSG_RETRY, // 1 hour
|
|
@@ -293,6 +311,42 @@ export const makeChatsSocket = (config) => {
|
|
|
293
311
|
return getBinaryNodeChildren(listNode, 'item').map(n => n.attrs.jid);
|
|
294
312
|
};
|
|
295
313
|
const updateBlockStatus = async (jid, action) => {
|
|
314
|
+
const normalizedJid = jidNormalizedUser(jid);
|
|
315
|
+
let lid;
|
|
316
|
+
let pn_jid;
|
|
317
|
+
if (isLidUser(normalizedJid) || isHostedLidUser(normalizedJid)) {
|
|
318
|
+
lid = normalizedJid;
|
|
319
|
+
if (action === 'block') {
|
|
320
|
+
const pn = (await findUserId(normalizedJid)).phoneNumber;
|
|
321
|
+
if (pn.startsWith('id')) {
|
|
322
|
+
throw new Boom(`Unable to resolve PN JID for LID: ${jid}`, { statusCode: 400 });
|
|
323
|
+
}
|
|
324
|
+
pn_jid = jidNormalizedUser(pn);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
else if (isPnUser(normalizedJid) || isHostedPnUser(normalizedJid)) {
|
|
328
|
+
const mapped = (await findUserId(normalizedJid)).lid;
|
|
329
|
+
if (mapped.startsWith('id')) {
|
|
330
|
+
throw new Boom(`Unable to resolve LID for PN JID: ${jid}`, { statusCode: 400 });
|
|
331
|
+
}
|
|
332
|
+
lid = mapped;
|
|
333
|
+
if (action === 'block') {
|
|
334
|
+
pn_jid = jidNormalizedUser(normalizedJid);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
throw new Boom(`Invalid jid: ${jid}`, { statusCode: 400 });
|
|
339
|
+
}
|
|
340
|
+
const itemAttrs = {
|
|
341
|
+
action,
|
|
342
|
+
jid: lid
|
|
343
|
+
};
|
|
344
|
+
if (action === 'block') {
|
|
345
|
+
if (!pn_jid) {
|
|
346
|
+
throw new Boom(`pn_jid required for block: ${jid}`, { statusCode: 400 });
|
|
347
|
+
}
|
|
348
|
+
itemAttrs.pn_jid = pn_jid;
|
|
349
|
+
}
|
|
296
350
|
await query({
|
|
297
351
|
tag: 'iq',
|
|
298
352
|
attrs: {
|
|
@@ -303,10 +357,7 @@ export const makeChatsSocket = (config) => {
|
|
|
303
357
|
content: [
|
|
304
358
|
{
|
|
305
359
|
tag: 'item',
|
|
306
|
-
attrs:
|
|
307
|
-
action,
|
|
308
|
-
jid
|
|
309
|
-
}
|
|
360
|
+
attrs: itemAttrs
|
|
310
361
|
}
|
|
311
362
|
]
|
|
312
363
|
});
|
|
@@ -405,6 +456,9 @@ export const makeChatsSocket = (config) => {
|
|
|
405
456
|
const collectionsToHandle = new Set(collections);
|
|
406
457
|
// in case something goes wrong -- ensure we don't enter a loop that cannot be exited from
|
|
407
458
|
const attemptsMap = {};
|
|
459
|
+
// collections that failed and need a full snapshot on retry
|
|
460
|
+
// mirrors WA Web's ErrorFatal -> force snapshot behavior
|
|
461
|
+
const forceSnapshotCollections = new Set();
|
|
408
462
|
// keep executing till all collections are done
|
|
409
463
|
// sometimes a single patch request will not return all the patches (God knows why)
|
|
410
464
|
// so we fetch till they're all done (this is determined by the "has_more_patches" flag)
|
|
@@ -415,6 +469,7 @@ export const makeChatsSocket = (config) => {
|
|
|
415
469
|
const result = await authState.keys.get('app-state-sync-version', [name]);
|
|
416
470
|
let state = result[name];
|
|
417
471
|
if (state) {
|
|
472
|
+
state = ensureLTHashStateVersion(state);
|
|
418
473
|
if (typeof initialVersionMap[name] === 'undefined') {
|
|
419
474
|
initialVersionMap[name] = state.version;
|
|
420
475
|
}
|
|
@@ -423,14 +478,18 @@ export const makeChatsSocket = (config) => {
|
|
|
423
478
|
state = newLTHashState();
|
|
424
479
|
}
|
|
425
480
|
states[name] = state;
|
|
426
|
-
|
|
481
|
+
const shouldForceSnapshot = forceSnapshotCollections.has(name);
|
|
482
|
+
if (shouldForceSnapshot) {
|
|
483
|
+
forceSnapshotCollections.delete(name);
|
|
484
|
+
}
|
|
485
|
+
logger.info(`resyncing ${name} from v${state.version}${shouldForceSnapshot ? ' (forcing snapshot)' : ''}`);
|
|
427
486
|
nodes.push({
|
|
428
487
|
tag: 'collection',
|
|
429
488
|
attrs: {
|
|
430
489
|
name,
|
|
431
490
|
version: state.version.toString(),
|
|
432
|
-
// return snapshot if
|
|
433
|
-
return_snapshot: (!state.version).toString()
|
|
491
|
+
// return snapshot if syncing from scratch or forcing after a failed attempt
|
|
492
|
+
return_snapshot: (shouldForceSnapshot || !state.version).toString()
|
|
434
493
|
}
|
|
435
494
|
});
|
|
436
495
|
}
|
|
@@ -479,19 +538,37 @@ export const makeChatsSocket = (config) => {
|
|
|
479
538
|
}
|
|
480
539
|
}
|
|
481
540
|
catch (error) {
|
|
482
|
-
// if retry attempts overshoot
|
|
483
|
-
// or key not found
|
|
484
|
-
const isIrrecoverableError = attemptsMap[name] >= MAX_SYNC_ATTEMPTS ||
|
|
485
|
-
error.output?.statusCode === 404 ||
|
|
486
|
-
error.name === 'TypeError';
|
|
487
|
-
logger.info({ name, error: error.stack }, `failed to sync state from version${isIrrecoverableError ? '' : ', removing and trying from scratch'}`);
|
|
488
|
-
await authState.keys.set({ 'app-state-sync-version': { [name]: null } });
|
|
489
|
-
// increment number of retries
|
|
490
541
|
attemptsMap[name] = (attemptsMap[name] || 0) + 1;
|
|
491
|
-
|
|
492
|
-
|
|
542
|
+
const logData = {
|
|
543
|
+
name,
|
|
544
|
+
attempt: attemptsMap[name],
|
|
545
|
+
version: states[name].version,
|
|
546
|
+
statusCode: error.output?.statusCode,
|
|
547
|
+
errorType: error.name,
|
|
548
|
+
error: error.stack
|
|
549
|
+
};
|
|
550
|
+
if (isMissingKeyError(error) && attemptsMap[name] >= MAX_SYNC_ATTEMPTS) {
|
|
551
|
+
// WA Web treats missing keys as "Blocked" — park the collection
|
|
552
|
+
// until the key arrives via APP_STATE_SYNC_KEY_SHARE.
|
|
553
|
+
logger.warn(logData, `${name} blocked on missing key from v${states[name].version}, parking after ${attemptsMap[name]} attempts`);
|
|
554
|
+
blockedCollections.add(name);
|
|
555
|
+
collectionsToHandle.delete(name);
|
|
556
|
+
}
|
|
557
|
+
else if (isMissingKeyError(error)) {
|
|
558
|
+
// Retry with a snapshot which may use a different key.
|
|
559
|
+
logger.info(logData, `${name} blocked on missing key from v${states[name].version}, retrying with snapshot`);
|
|
560
|
+
forceSnapshotCollections.add(name);
|
|
561
|
+
}
|
|
562
|
+
else if (isAppStateSyncIrrecoverable(error, attemptsMap[name])) {
|
|
563
|
+
logger.warn(logData, `failed to sync ${name} from v${states[name].version}, giving up`);
|
|
493
564
|
collectionsToHandle.delete(name);
|
|
494
565
|
}
|
|
566
|
+
else {
|
|
567
|
+
logger.info(logData, `failed to sync ${name} from v${states[name].version}, forcing snapshot retry`);
|
|
568
|
+
// force a full snapshot on retry to recover from
|
|
569
|
+
// corrupted local state (e.g. LTHash MAC mismatch)
|
|
570
|
+
forceSnapshotCollections.add(name);
|
|
571
|
+
}
|
|
495
572
|
}
|
|
496
573
|
}
|
|
497
574
|
}
|
|
@@ -507,42 +584,34 @@ export const makeChatsSocket = (config) => {
|
|
|
507
584
|
* type = "image for the high res picture"
|
|
508
585
|
*/
|
|
509
586
|
const profilePictureUrl = async (jid, type = 'image', timeoutMs) => {
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
}];
|
|
587
|
+
const baseContent = [{ tag: 'picture', attrs: { type, query: 'url' } }];
|
|
588
|
+
// WA Web only includes tctoken for user JIDs (not groups/newsletters)
|
|
589
|
+
// and never for own profile pic (Chat model for self has no tcToken).
|
|
590
|
+
// Including tctoken for own JID causes the server to never respond.
|
|
591
|
+
const normalizedJid = jidNormalizedUser(jid);
|
|
592
|
+
const isUserJid = isPnUser(normalizedJid) || isLidUser(normalizedJid);
|
|
593
|
+
const me = authState.creds.me;
|
|
594
|
+
const isSelf = me && (normalizedJid === jidNormalizedUser(me.id) || (me.lid && normalizedJid === jidNormalizedUser(me.lid)));
|
|
595
|
+
let content = baseContent;
|
|
596
|
+
if (serverProps.profilePicPrivacyToken && isUserJid && !isSelf) {
|
|
597
|
+
content = await buildTcTokenFromJid({
|
|
598
|
+
authState,
|
|
599
|
+
jid: normalizedJid,
|
|
600
|
+
baseContent,
|
|
601
|
+
getLIDForPN
|
|
602
|
+
});
|
|
527
603
|
}
|
|
528
604
|
const result = await query({
|
|
529
605
|
tag: 'iq',
|
|
530
606
|
attrs: {
|
|
531
|
-
target:
|
|
607
|
+
target: normalizedJid,
|
|
532
608
|
to: S_WHATSAPP_NET,
|
|
533
609
|
type: 'get',
|
|
534
610
|
xmlns: 'w:profile:picture'
|
|
535
611
|
},
|
|
536
|
-
content
|
|
612
|
+
content
|
|
537
613
|
}, timeoutMs);
|
|
538
614
|
const child = getBinaryNodeChild(result, 'picture');
|
|
539
|
-
if (!child) {
|
|
540
|
-
throw new Boom('Picture node missing', { statusCode: 404 });
|
|
541
|
-
}
|
|
542
|
-
const status = child.attrs?.status;
|
|
543
|
-
if (status === '404' || status === '204') {
|
|
544
|
-
throw new Boom('Profile picture not set', { statusCode: 404 });
|
|
545
|
-
}
|
|
546
615
|
return child?.attrs?.url;
|
|
547
616
|
};
|
|
548
617
|
const createCallLink = async (type, event, timeoutMs) => {
|
|
@@ -606,7 +675,12 @@ export const makeChatsSocket = (config) => {
|
|
|
606
675
|
* @param tcToken token for subscription, use if present
|
|
607
676
|
*/
|
|
608
677
|
const presenceSubscribe = async (toJid) => {
|
|
609
|
-
|
|
678
|
+
// Only include tctoken for user JIDs — groups/newsletters don't use tctokens
|
|
679
|
+
const normalizedToJid = jidNormalizedUser(toJid);
|
|
680
|
+
const isUserJid = isPnUser(normalizedToJid) || isLidUser(normalizedToJid);
|
|
681
|
+
const tcTokenContent = isUserJid
|
|
682
|
+
? await buildTcTokenFromJid({ authState, jid: normalizedToJid, getLIDForPN })
|
|
683
|
+
: undefined;
|
|
610
684
|
return sendNode({
|
|
611
685
|
tag: 'presence',
|
|
612
686
|
attrs: {
|
|
@@ -661,7 +735,7 @@ export const makeChatsSocket = (config) => {
|
|
|
661
735
|
logger.debug({ patch: patchCreate }, 'applying app patch');
|
|
662
736
|
await resyncAppState([name], false);
|
|
663
737
|
const { [name]: currentSyncVersion } = await authState.keys.get('app-state-sync-version', [name]);
|
|
664
|
-
initial = currentSyncVersion
|
|
738
|
+
initial = currentSyncVersion ? ensureLTHashStateVersion(currentSyncVersion) : newLTHashState();
|
|
665
739
|
encodeResult = await encodeSyncdPatch(patchCreate, myAppStateKeyId, initial, getAppStateSyncKey);
|
|
666
740
|
const { patch, state } = encodeResult;
|
|
667
741
|
const node = {
|
|
@@ -707,22 +781,21 @@ export const makeChatsSocket = (config) => {
|
|
|
707
781
|
}
|
|
708
782
|
}
|
|
709
783
|
};
|
|
710
|
-
/**
|
|
784
|
+
/** fetch AB props */
|
|
711
785
|
const fetchProps = async () => {
|
|
712
|
-
//TODO: implement both protocol 1 and protocol 2 prop fetching, specially for abKey for WM
|
|
713
786
|
const resultNode = await query({
|
|
714
787
|
tag: 'iq',
|
|
715
788
|
attrs: {
|
|
716
789
|
to: S_WHATSAPP_NET,
|
|
717
|
-
xmlns: '
|
|
790
|
+
xmlns: 'abt',
|
|
718
791
|
type: 'get'
|
|
719
792
|
},
|
|
720
793
|
content: [
|
|
721
794
|
{
|
|
722
795
|
tag: 'props',
|
|
723
796
|
attrs: {
|
|
724
|
-
protocol: '
|
|
725
|
-
hash: authState
|
|
797
|
+
protocol: '1',
|
|
798
|
+
...(authState?.creds?.lastPropHash ? { hash: authState.creds.lastPropHash } : {})
|
|
726
799
|
}
|
|
727
800
|
}
|
|
728
801
|
]
|
|
@@ -737,7 +810,20 @@ export const makeChatsSocket = (config) => {
|
|
|
737
810
|
}
|
|
738
811
|
props = reduceBinaryNodeToDictionary(propsNode, 'prop');
|
|
739
812
|
}
|
|
740
|
-
|
|
813
|
+
// Extract protocol-relevant AB props (only the ones we need)
|
|
814
|
+
const privacyTokenProp = props['10518'] ?? props['privacy_token_sending_on_all_1_on_1_messages'];
|
|
815
|
+
if (privacyTokenProp !== undefined) {
|
|
816
|
+
serverProps.privacyTokenOn1to1 = privacyTokenProp === 'true' || privacyTokenProp === '1';
|
|
817
|
+
}
|
|
818
|
+
const profilePicProp = props['9666'] ?? props['profile_scraping_privacy_token_in_photo_iq'];
|
|
819
|
+
if (profilePicProp !== undefined) {
|
|
820
|
+
serverProps.profilePicPrivacyToken = profilePicProp === 'true' || profilePicProp === '1';
|
|
821
|
+
}
|
|
822
|
+
const lidIssueProp = props['14303'] ?? props['lid_trusted_token_issue_to_lid'];
|
|
823
|
+
if (lidIssueProp !== undefined) {
|
|
824
|
+
serverProps.lidTrustedTokenIssueToLid = lidIssueProp === 'true' || lidIssueProp === '1';
|
|
825
|
+
}
|
|
826
|
+
logger.debug({ serverProps }, 'fetched props');
|
|
741
827
|
return props;
|
|
742
828
|
};
|
|
743
829
|
/**
|
|
@@ -877,6 +963,44 @@ export const makeChatsSocket = (config) => {
|
|
|
877
963
|
? shouldSyncHistoryMessage(historyMsg) &&
|
|
878
964
|
PROCESSABLE_HISTORY_TYPES.includes(historyMsg.syncType)
|
|
879
965
|
: false;
|
|
966
|
+
if (historyMsg && shouldProcessHistoryMsg) {
|
|
967
|
+
const syncType = historyMsg.syncType;
|
|
968
|
+
// INITIAL_BOOTSTRAP — fire immediately, no progress check (same as WA Web K function)
|
|
969
|
+
if (syncType === proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP && !historySyncStatus.initialBootstrapComplete) {
|
|
970
|
+
historySyncStatus.initialBootstrapComplete = true;
|
|
971
|
+
ev.emit('messaging-history.status', {
|
|
972
|
+
syncType,
|
|
973
|
+
status: 'complete',
|
|
974
|
+
explicit: true
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
// RECENT with progress === 100 — explicit completion
|
|
978
|
+
if (syncType === proto.HistorySync.HistorySyncType.RECENT && historyMsg.progress === 100 && !historySyncStatus.recentSyncComplete) {
|
|
979
|
+
historySyncStatus.recentSyncComplete = true;
|
|
980
|
+
clearTimeout(historySyncPausedTimeout);
|
|
981
|
+
historySyncPausedTimeout = undefined;
|
|
982
|
+
ev.emit('messaging-history.status', {
|
|
983
|
+
syncType,
|
|
984
|
+
status: 'complete',
|
|
985
|
+
explicit: true
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
// Reset 120s paused timeout on any RECENT chunk (like WA Web's handleChunkProgress)
|
|
989
|
+
if (syncType === proto.HistorySync.HistorySyncType.RECENT && !historySyncStatus.recentSyncComplete) {
|
|
990
|
+
clearTimeout(historySyncPausedTimeout);
|
|
991
|
+
historySyncPausedTimeout = setTimeout(() => {
|
|
992
|
+
if (!historySyncStatus.recentSyncComplete) {
|
|
993
|
+
historySyncStatus.recentSyncComplete = true;
|
|
994
|
+
ev.emit('messaging-history.status', {
|
|
995
|
+
syncType: proto.HistorySync.HistorySyncType.RECENT,
|
|
996
|
+
status: 'paused',
|
|
997
|
+
explicit: false
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
historySyncPausedTimeout = undefined;
|
|
1001
|
+
}, HISTORY_SYNC_PAUSED_TIMEOUT_MS);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
880
1004
|
// State machine: decide on sync and flush
|
|
881
1005
|
if (historyMsg && syncState === SyncState.AwaitingInitialSync) {
|
|
882
1006
|
if (awaitingSyncTimeout) {
|
|
@@ -896,6 +1020,8 @@ export const makeChatsSocket = (config) => {
|
|
|
896
1020
|
}
|
|
897
1021
|
const doAppStateSync = async () => {
|
|
898
1022
|
if (syncState === SyncState.Syncing) {
|
|
1023
|
+
// All collections will be synced, so clear any blocked ones
|
|
1024
|
+
blockedCollections.clear();
|
|
899
1025
|
logger.info('Doing app state sync');
|
|
900
1026
|
await resyncAppState(ALL_WA_PATCH_NAMES, true);
|
|
901
1027
|
// Sync is complete, go online and flush everything
|
|
@@ -955,6 +1081,11 @@ export const makeChatsSocket = (config) => {
|
|
|
955
1081
|
}
|
|
956
1082
|
});
|
|
957
1083
|
ev.on('connection.update', ({ connection, receivedPendingNotifications }) => {
|
|
1084
|
+
if (connection === 'close') {
|
|
1085
|
+
blockedCollections.clear();
|
|
1086
|
+
clearTimeout(historySyncPausedTimeout);
|
|
1087
|
+
historySyncPausedTimeout = undefined;
|
|
1088
|
+
}
|
|
958
1089
|
if (connection === 'open') {
|
|
959
1090
|
if (fireInitQueries) {
|
|
960
1091
|
executeInitQueries().catch(error => onUnexpectedError(error, 'init queries'));
|
|
@@ -964,6 +1095,10 @@ export const makeChatsSocket = (config) => {
|
|
|
964
1095
|
if (!receivedPendingNotifications || syncState !== SyncState.Connecting) {
|
|
965
1096
|
return;
|
|
966
1097
|
}
|
|
1098
|
+
historySyncStatus.initialBootstrapComplete = false;
|
|
1099
|
+
historySyncStatus.recentSyncComplete = false;
|
|
1100
|
+
clearTimeout(historySyncPausedTimeout);
|
|
1101
|
+
historySyncPausedTimeout = undefined;
|
|
967
1102
|
syncState = SyncState.AwaitingInitialSync;
|
|
968
1103
|
logger.info('Connection is now AwaitingInitialSync, buffering events');
|
|
969
1104
|
ev.buffer();
|
|
@@ -976,19 +1111,49 @@ export const makeChatsSocket = (config) => {
|
|
|
976
1111
|
setTimeout(() => ev.flush(), 0);
|
|
977
1112
|
return;
|
|
978
1113
|
}
|
|
979
|
-
|
|
1114
|
+
// On reconnection (accountSyncCounter > 0), the server does not push
|
|
1115
|
+
// history sync notifications — the device already has its data.
|
|
1116
|
+
// Skip the 20s wait and go online immediately.
|
|
1117
|
+
if (authState.creds.accountSyncCounter > 0) {
|
|
1118
|
+
logger.info('Reconnection with existing sync data, skipping history sync wait. Transitioning to Online.');
|
|
1119
|
+
syncState = SyncState.Online;
|
|
1120
|
+
setTimeout(() => ev.flush(), 0);
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
logger.info('First connection, awaiting history sync notification with a 20s timeout.');
|
|
980
1124
|
if (awaitingSyncTimeout) {
|
|
981
1125
|
clearTimeout(awaitingSyncTimeout);
|
|
982
1126
|
}
|
|
983
1127
|
awaitingSyncTimeout = setTimeout(() => {
|
|
984
1128
|
if (syncState === SyncState.AwaitingInitialSync) {
|
|
985
|
-
// TODO: investigate
|
|
986
1129
|
logger.warn('Timeout in AwaitingInitialSync, forcing state to Online and flushing buffer');
|
|
987
1130
|
syncState = SyncState.Online;
|
|
988
1131
|
ev.flush();
|
|
1132
|
+
// Increment so subsequent reconnections skip the 20s wait.
|
|
1133
|
+
// Late-arriving history is still processed via processMessage
|
|
1134
|
+
// regardless of the state machine phase.
|
|
1135
|
+
const accountSyncCounter = (authState.creds.accountSyncCounter || 0) + 1;
|
|
1136
|
+
ev.emit('creds.update', { accountSyncCounter });
|
|
989
1137
|
}
|
|
990
1138
|
}, 20000);
|
|
991
1139
|
});
|
|
1140
|
+
// When an app state sync key arrives (myAppStateKeyId is set) and there are
|
|
1141
|
+
// collections blocked on a missing key, trigger a re-sync for just those collections.
|
|
1142
|
+
// This mirrors WA Web's Blocked → retry-on-key-arrival behavior.
|
|
1143
|
+
ev.on('creds.update', ({ myAppStateKeyId }) => {
|
|
1144
|
+
if (!myAppStateKeyId || blockedCollections.size === 0) {
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
// If we're in the middle of a full sync, doAppStateSync handles all collections
|
|
1148
|
+
if (syncState === SyncState.Syncing) {
|
|
1149
|
+
blockedCollections.clear();
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
const collections = [...blockedCollections];
|
|
1153
|
+
blockedCollections.clear();
|
|
1154
|
+
logger.info({ collections }, 'app state sync key arrived, re-syncing blocked collections');
|
|
1155
|
+
resyncAppState(collections, false).catch(error => onUnexpectedError(error, 'blocked collections resync'));
|
|
1156
|
+
});
|
|
992
1157
|
ev.on('lid-mapping.update', async ({ lid, pn }) => {
|
|
993
1158
|
try {
|
|
994
1159
|
await signalRepository.lidMapping.storeLIDPNMappings([{ lid, pn }]);
|
|
@@ -1010,6 +1175,7 @@ export const makeChatsSocket = (config) => {
|
|
|
1010
1175
|
});
|
|
1011
1176
|
return {
|
|
1012
1177
|
...sock,
|
|
1178
|
+
serverProps,
|
|
1013
1179
|
createCallLink,
|
|
1014
1180
|
getBotListV2,
|
|
1015
1181
|
messageMutex,
|
package/lib/Socket/groups.js
CHANGED
|
@@ -277,11 +277,13 @@ export const extractGroupMetadata = (result) => {
|
|
|
277
277
|
let descId;
|
|
278
278
|
let descOwner;
|
|
279
279
|
let descOwnerPn;
|
|
280
|
+
let descOwnerUsername;
|
|
280
281
|
let descTime;
|
|
281
282
|
if (descChild) {
|
|
282
283
|
desc = getBinaryNodeChildString(descChild, 'body');
|
|
283
284
|
descOwner = descChild.attrs.participant ? jidNormalizedUser(descChild.attrs.participant) : undefined;
|
|
284
285
|
descOwnerPn = descChild.attrs.participant_pn ? jidNormalizedUser(descChild.attrs.participant_pn) : undefined;
|
|
286
|
+
descOwnerUsername = descChild.attrs.participant_username || undefined;
|
|
285
287
|
descTime = +descChild.attrs.t;
|
|
286
288
|
descId = descChild.attrs.id;
|
|
287
289
|
}
|
|
@@ -295,16 +297,19 @@ export const extractGroupMetadata = (result) => {
|
|
|
295
297
|
subject: group.attrs.subject,
|
|
296
298
|
subjectOwner: group.attrs.s_o,
|
|
297
299
|
subjectOwnerPn: group.attrs.s_o_pn,
|
|
300
|
+
subjectOwnerUsername: group.attrs.s_o_username,
|
|
298
301
|
subjectTime: +group.attrs.s_t,
|
|
299
302
|
size: group.attrs.size ? +group.attrs.size : getBinaryNodeChildren(group, 'participant').length,
|
|
300
303
|
creation: +group.attrs.creation,
|
|
301
304
|
owner: group.attrs.creator ? jidNormalizedUser(group.attrs.creator) : undefined,
|
|
302
305
|
ownerPn: group.attrs.creator_pn ? jidNormalizedUser(group.attrs.creator_pn) : undefined,
|
|
306
|
+
ownerUsername: group.attrs.creator_username || undefined,
|
|
303
307
|
owner_country_code: group.attrs.creator_country_code,
|
|
304
308
|
desc,
|
|
305
309
|
descId,
|
|
306
310
|
descOwner,
|
|
307
311
|
descOwnerPn,
|
|
312
|
+
descOwnerUsername,
|
|
308
313
|
descTime,
|
|
309
314
|
linkedParent: getBinaryNodeChild(group, 'linked_parent')?.attrs.jid || undefined,
|
|
310
315
|
restrict: !!getBinaryNodeChild(group, 'locked'),
|
|
@@ -319,6 +324,7 @@ export const extractGroupMetadata = (result) => {
|
|
|
319
324
|
id: attrs.jid,
|
|
320
325
|
phoneNumber: isLidUser(attrs.jid) && isPnUser(attrs.phone_number) ? attrs.phone_number : undefined,
|
|
321
326
|
lid: isPnUser(attrs.jid) && isLidUser(attrs.lid) ? attrs.lid : undefined,
|
|
327
|
+
username: attrs.participant_username || attrs.username || undefined,
|
|
322
328
|
admin: (attrs.type || null)
|
|
323
329
|
};
|
|
324
330
|
}),
|