@crysnovax/baileys 2.5.6 → 2.6.1
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 +11 -0
- package/README.md[old] +1612 -0
- package/WAProto/index.js +14789 -3578
- package/lib/Defaults/index.js +14 -7
- package/lib/Signal/libsignal.js +40 -18
- package/lib/Socket/business.js +2 -1
- package/lib/Socket/chats.js +266 -92
- package/lib/Socket/chats.js[past] +1213 -0
- package/lib/Socket/communities.js +2 -1
- package/lib/Socket/groups.js +24 -5
- package/lib/Socket/messages-recv.js +617 -322
- package/lib/Socket/messages-send.js +160 -61
- package/lib/Socket/newsletter.js +4 -4
- package/lib/Socket/socket.js +49 -20
- package/lib/Types/{Newsletter.js → Mex.js} +10 -4
- package/lib/Types/State.js +37 -1
- package/lib/Types/index.js +1 -1
- package/lib/Utils/auth-utils.js +13 -1
- package/lib/Utils/chat-utils.js +81 -21
- package/lib/Utils/decode-wa-message.js +35 -1
- package/lib/Utils/index.js +1 -0
- package/lib/Utils/messages-media.js +28 -16
- package/lib/Utils/messages-media.js[past] +842 -0
- package/lib/Utils/messages.js +325 -352
- package/lib/Utils/signal.js +43 -1
- package/lib/Utils/tc-token-utils.js +150 -5
- package/lib/Utils/use-sqlite-auth-state.js +108 -0
- package/lib/WABinary/constants.js +91 -0
- package/package.json +45 -10
package/lib/Socket/chats.js
CHANGED
|
@@ -1,24 +1,32 @@
|
|
|
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
|
+
import { getStream, toBuffer } from '../Utils/messages-media.js';
|
|
8
9
|
import { makeMutex } from '../Utils/make-mutex.js';
|
|
9
10
|
import processMessage from '../Utils/process-message.js';
|
|
10
11
|
import { buildTcTokenFromJid } from '../Utils/tc-token-utils.js';
|
|
11
|
-
import { getBinaryNodeChild, getBinaryNodeChildren, isLidUser, isPnUser, jidDecode, jidNormalizedUser, reduceBinaryNodeToDictionary, S_WHATSAPP_NET } from '../WABinary/index.js';
|
|
12
|
+
import { getBinaryNodeChild, getBinaryNodeChildren, isHostedLidUser, isHostedPnUser, isLidUser, isPnUser, jidDecode, jidNormalizedUser, reduceBinaryNodeToDictionary, S_WHATSAPP_NET } from '../WABinary/index.js';
|
|
12
13
|
import { USyncQuery, USyncUser } from '../WAUSync/index.js';
|
|
13
14
|
import { makeSocket } from './socket.js';
|
|
14
|
-
const MAX_SYNC_ATTEMPTS = 2;
|
|
15
|
-
// Lia@Note 08-02-26 --- I know it's not efficient for RSS ಥ‿ಥ
|
|
16
|
-
const USER_ID_CACHE = new Map();
|
|
17
15
|
export const makeChatsSocket = (config) => {
|
|
18
16
|
const { logger, markOnlineOnConnect, fireInitQueries, appStateMacVerification, shouldIgnoreJid, shouldSyncHistoryMessage, getMessage } = config;
|
|
19
17
|
const sock = makeSocket(config);
|
|
20
18
|
const { ev, ws, authState, generateMessageTag, sendNode, query, signalRepository, onUnexpectedError, sendUnifiedSession, registerSocketEndHandler } = sock;
|
|
19
|
+
const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping);
|
|
21
20
|
let privacySettings;
|
|
21
|
+
/** Server-assigned AB props for protocol behavior. */
|
|
22
|
+
const serverProps = {
|
|
23
|
+
/** AB prop 10518: gate tctoken on 1:1 messages. Default true (safe: avoids 463). */
|
|
24
|
+
privacyTokenOn1to1: true,
|
|
25
|
+
/** AB prop 9666: gate tctoken on profile picture IQs. WA Web default: true. */
|
|
26
|
+
profilePicPrivacyToken: true,
|
|
27
|
+
/** AB prop 14303: issue tctokens to LID instead of PN. WA Web default: false. */
|
|
28
|
+
lidTrustedTokenIssueToLid: false
|
|
29
|
+
};
|
|
22
30
|
let syncState = SyncState.Connecting;
|
|
23
31
|
/** this mutex ensures that messages are processed in order */
|
|
24
32
|
const messageMutex = makeMutex();
|
|
@@ -30,14 +38,20 @@ export const makeChatsSocket = (config) => {
|
|
|
30
38
|
const notificationMutex = makeMutex();
|
|
31
39
|
// Timeout for AwaitingInitialSync state
|
|
32
40
|
let awaitingSyncTimeout;
|
|
41
|
+
// In-memory history sync completion tracking (resets on reconnection)
|
|
42
|
+
const historySyncStatus = {
|
|
43
|
+
initialBootstrapComplete: false,
|
|
44
|
+
recentSyncComplete: false
|
|
45
|
+
};
|
|
46
|
+
let historySyncPausedTimeout;
|
|
47
|
+
// Collections blocked on missing app state sync keys (mirrors WA Web's "Blocked" state).
|
|
48
|
+
// When a key arrives via APP_STATE_SYNC_KEY_SHARE, these are re-synced.
|
|
49
|
+
const blockedCollections = new Set();
|
|
33
50
|
const placeholderResendCache = config.placeholderResendCache ||
|
|
34
51
|
new NodeCache({
|
|
35
52
|
stdTTL: DEFAULT_CACHE_TTLS.MSG_RETRY, // 1 hour
|
|
36
53
|
useClones: false
|
|
37
54
|
});
|
|
38
|
-
// if (!config.placeholderResendCache) {
|
|
39
|
-
// config.placeholderResendCache = placeholderResendCache;
|
|
40
|
-
// }
|
|
41
55
|
/** helper function to fetch the given app state sync key */
|
|
42
56
|
const getAppStateSyncKey = async (keyId) => {
|
|
43
57
|
const { [keyId]: key } = await authState.keys.get('app-state-sync-key', [keyId]);
|
|
@@ -176,38 +190,33 @@ export const makeChatsSocket = (config) => {
|
|
|
176
190
|
};
|
|
177
191
|
// Lia@Note 06-02-26 --- Quick helper only. This function exists solely for faster lookup with caching.
|
|
178
192
|
const findUserId = async (pnLid) => {
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (isPnUser(
|
|
185
|
-
userId.phoneNumber =
|
|
186
|
-
userId.lid = (await signalRepository.lidMapping.getLIDsForPNs([
|
|
187
|
-
if (!userId.lid) {
|
|
188
|
-
userId.lid = 'id-not-found';
|
|
189
|
-
return userId; // Lia@Note 06-02-26 --- Early return to skip caching for non-existent ID
|
|
190
|
-
}
|
|
193
|
+
const normalizedJid = jidNormalizedUser(pnLid);
|
|
194
|
+
const userId = {
|
|
195
|
+
lid: undefined,
|
|
196
|
+
phoneNumber: undefined
|
|
197
|
+
};
|
|
198
|
+
if (isPnUser(normalizedJid) || isHostedPnUser(normalizedJid)) {
|
|
199
|
+
userId.phoneNumber = normalizedJid;
|
|
200
|
+
userId.lid = jidNormalizedUser((await signalRepository.lidMapping.getLIDsForPNs([normalizedJid]))?.[0]?.lid);
|
|
191
201
|
}
|
|
192
|
-
else if (isLidUser(
|
|
193
|
-
userId.lid =
|
|
194
|
-
userId.phoneNumber = (await signalRepository.lidMapping.getPNsForLIDs([
|
|
195
|
-
if (!userId.phoneNumber) {
|
|
196
|
-
userId.phoneNumber = 'id-not-found';
|
|
197
|
-
return userId; // Lia@Note 06-02-26 --- Early return to skip caching for non-existent ID
|
|
198
|
-
}
|
|
202
|
+
else if (isLidUser(normalizedJid) || isHostedLidUser(normalizedJid)) {
|
|
203
|
+
userId.lid = normalizedJid;
|
|
204
|
+
userId.phoneNumber = jidNormalizedUser((await signalRepository.lidMapping.getPNsForLIDs([normalizedJid]))?.[0]?.pn);
|
|
199
205
|
}
|
|
200
206
|
else {
|
|
201
207
|
throw new Boom('Invalid id input to find user ids', { statusCode: 400 });
|
|
202
208
|
}
|
|
203
|
-
userId.phoneNumber = jidNormalizedUser(userId.phoneNumber);
|
|
204
|
-
userId.lid = jidNormalizedUser(userId.lid);
|
|
205
|
-
// Lia@Note 06-02-26 --- I know... it's dirty (╯︵╰,)
|
|
206
|
-
USER_ID_CACHE.set(userId.phoneNumber, userId);
|
|
207
|
-
USER_ID_CACHE.set(userId.lid, userId);
|
|
208
209
|
return userId;
|
|
209
210
|
};
|
|
210
|
-
/** update the profile picture for yourself or a group
|
|
211
|
+
/** update the profile picture for yourself or a group
|
|
212
|
+
*
|
|
213
|
+
* Standard (crop + resize to 720×720):
|
|
214
|
+
* sock.updateProfilePicture(jid, content)
|
|
215
|
+
* sock.updateProfilePicture(jid, content, { width: 640, height: 640 })
|
|
216
|
+
*
|
|
217
|
+
* HD (full-size, no crop, no resize):
|
|
218
|
+
* sock.updateProfilePicture(jid, content, { hd: true })
|
|
219
|
+
*/
|
|
211
220
|
const updateProfilePicture = async (jid, content, dimensions) => {
|
|
212
221
|
let targetJid;
|
|
213
222
|
if (!jid) {
|
|
@@ -219,7 +228,18 @@ export const makeChatsSocket = (config) => {
|
|
|
219
228
|
else {
|
|
220
229
|
targetJid = undefined;
|
|
221
230
|
}
|
|
222
|
-
|
|
231
|
+
let img;
|
|
232
|
+
if (dimensions?.hd) {
|
|
233
|
+
// crysnovax@HD-Profile --- bypass resize/crop, send raw buffer directly
|
|
234
|
+
if (Buffer.isBuffer(content)) {
|
|
235
|
+
img = content;
|
|
236
|
+
} else {
|
|
237
|
+
const { stream } = await getStream(content);
|
|
238
|
+
img = await toBuffer(stream);
|
|
239
|
+
}
|
|
240
|
+
} else {
|
|
241
|
+
({ img } = await generateProfilePicture(content, dimensions));
|
|
242
|
+
}
|
|
223
243
|
await query({
|
|
224
244
|
tag: 'iq',
|
|
225
245
|
attrs: {
|
|
@@ -293,6 +313,42 @@ export const makeChatsSocket = (config) => {
|
|
|
293
313
|
return getBinaryNodeChildren(listNode, 'item').map(n => n.attrs.jid);
|
|
294
314
|
};
|
|
295
315
|
const updateBlockStatus = async (jid, action) => {
|
|
316
|
+
const normalizedJid = jidNormalizedUser(jid);
|
|
317
|
+
let lid;
|
|
318
|
+
let pn_jid;
|
|
319
|
+
if (isLidUser(normalizedJid) || isHostedLidUser(normalizedJid)) {
|
|
320
|
+
lid = normalizedJid;
|
|
321
|
+
if (action === 'block') {
|
|
322
|
+
const pn = (await findUserId(normalizedJid)).phoneNumber;
|
|
323
|
+
if (!pn) {
|
|
324
|
+
throw new Boom(`Unable to resolve PN JID for LID: ${jid}`, { statusCode: 400 });
|
|
325
|
+
}
|
|
326
|
+
pn_jid = jidNormalizedUser(pn);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
else if (isPnUser(normalizedJid) || isHostedPnUser(normalizedJid)) {
|
|
330
|
+
const mapped = (await findUserId(normalizedJid)).lid;
|
|
331
|
+
if (!mapped) {
|
|
332
|
+
throw new Boom(`Unable to resolve LID for PN JID: ${jid}`, { statusCode: 400 });
|
|
333
|
+
}
|
|
334
|
+
lid = mapped;
|
|
335
|
+
if (action === 'block') {
|
|
336
|
+
pn_jid = jidNormalizedUser(normalizedJid);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
throw new Boom(`Invalid jid: ${jid}`, { statusCode: 400 });
|
|
341
|
+
}
|
|
342
|
+
const itemAttrs = {
|
|
343
|
+
action,
|
|
344
|
+
jid: lid
|
|
345
|
+
};
|
|
346
|
+
if (action === 'block') {
|
|
347
|
+
if (!pn_jid) {
|
|
348
|
+
throw new Boom(`pn_jid required for block: ${jid}`, { statusCode: 400 });
|
|
349
|
+
}
|
|
350
|
+
itemAttrs.pn_jid = pn_jid;
|
|
351
|
+
}
|
|
296
352
|
await query({
|
|
297
353
|
tag: 'iq',
|
|
298
354
|
attrs: {
|
|
@@ -303,10 +359,7 @@ export const makeChatsSocket = (config) => {
|
|
|
303
359
|
content: [
|
|
304
360
|
{
|
|
305
361
|
tag: 'item',
|
|
306
|
-
attrs:
|
|
307
|
-
action,
|
|
308
|
-
jid
|
|
309
|
-
}
|
|
362
|
+
attrs: itemAttrs
|
|
310
363
|
}
|
|
311
364
|
]
|
|
312
365
|
});
|
|
@@ -405,6 +458,9 @@ export const makeChatsSocket = (config) => {
|
|
|
405
458
|
const collectionsToHandle = new Set(collections);
|
|
406
459
|
// in case something goes wrong -- ensure we don't enter a loop that cannot be exited from
|
|
407
460
|
const attemptsMap = {};
|
|
461
|
+
// collections that failed and need a full snapshot on retry
|
|
462
|
+
// mirrors WA Web's ErrorFatal -> force snapshot behavior
|
|
463
|
+
const forceSnapshotCollections = new Set();
|
|
408
464
|
// keep executing till all collections are done
|
|
409
465
|
// sometimes a single patch request will not return all the patches (God knows why)
|
|
410
466
|
// so we fetch till they're all done (this is determined by the "has_more_patches" flag)
|
|
@@ -415,6 +471,7 @@ export const makeChatsSocket = (config) => {
|
|
|
415
471
|
const result = await authState.keys.get('app-state-sync-version', [name]);
|
|
416
472
|
let state = result[name];
|
|
417
473
|
if (state) {
|
|
474
|
+
state = ensureLTHashStateVersion(state);
|
|
418
475
|
if (typeof initialVersionMap[name] === 'undefined') {
|
|
419
476
|
initialVersionMap[name] = state.version;
|
|
420
477
|
}
|
|
@@ -423,14 +480,18 @@ export const makeChatsSocket = (config) => {
|
|
|
423
480
|
state = newLTHashState();
|
|
424
481
|
}
|
|
425
482
|
states[name] = state;
|
|
426
|
-
|
|
483
|
+
const shouldForceSnapshot = forceSnapshotCollections.has(name);
|
|
484
|
+
if (shouldForceSnapshot) {
|
|
485
|
+
forceSnapshotCollections.delete(name);
|
|
486
|
+
}
|
|
487
|
+
logger.info(`resyncing ${name} from v${state.version}${shouldForceSnapshot ? ' (forcing snapshot)' : ''}`);
|
|
427
488
|
nodes.push({
|
|
428
489
|
tag: 'collection',
|
|
429
490
|
attrs: {
|
|
430
491
|
name,
|
|
431
492
|
version: state.version.toString(),
|
|
432
|
-
// return snapshot if
|
|
433
|
-
return_snapshot: (!state.version).toString()
|
|
493
|
+
// return snapshot if syncing from scratch or forcing after a failed attempt
|
|
494
|
+
return_snapshot: (shouldForceSnapshot || !state.version).toString()
|
|
434
495
|
}
|
|
435
496
|
});
|
|
436
497
|
}
|
|
@@ -456,7 +517,7 @@ export const makeChatsSocket = (config) => {
|
|
|
456
517
|
const { patches, hasMorePatches, snapshot } = decoded[name];
|
|
457
518
|
try {
|
|
458
519
|
if (snapshot) {
|
|
459
|
-
const { state: newState, mutationMap } = await decodeSyncdSnapshot(name, snapshot, getCachedAppStateSyncKey, initialVersionMap[name], appStateMacVerification.snapshot);
|
|
520
|
+
const { state: newState, mutationMap } = await decodeSyncdSnapshot(name, snapshot, getCachedAppStateSyncKey, initialVersionMap[name], appStateMacVerification.snapshot, logger);
|
|
460
521
|
states[name] = newState;
|
|
461
522
|
Object.assign(globalMutationMap, mutationMap);
|
|
462
523
|
logger.info(`restored state of ${name} from snapshot to v${newState.version} with mutations`);
|
|
@@ -479,19 +540,37 @@ export const makeChatsSocket = (config) => {
|
|
|
479
540
|
}
|
|
480
541
|
}
|
|
481
542
|
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
543
|
attemptsMap[name] = (attemptsMap[name] || 0) + 1;
|
|
491
|
-
|
|
492
|
-
|
|
544
|
+
const logData = {
|
|
545
|
+
name,
|
|
546
|
+
attempt: attemptsMap[name],
|
|
547
|
+
version: states[name].version,
|
|
548
|
+
statusCode: error.output?.statusCode,
|
|
549
|
+
errorType: error.name,
|
|
550
|
+
error: error.stack
|
|
551
|
+
};
|
|
552
|
+
if (isMissingKeyError(error) && attemptsMap[name] >= MAX_SYNC_ATTEMPTS) {
|
|
553
|
+
// WA Web treats missing keys as "Blocked" — park the collection
|
|
554
|
+
// until the key arrives via APP_STATE_SYNC_KEY_SHARE.
|
|
555
|
+
logger.warn(logData, `${name} blocked on missing key from v${states[name].version}, parking after ${attemptsMap[name]} attempts`);
|
|
556
|
+
blockedCollections.add(name);
|
|
493
557
|
collectionsToHandle.delete(name);
|
|
494
558
|
}
|
|
559
|
+
else if (isMissingKeyError(error)) {
|
|
560
|
+
// Retry with a snapshot which may use a different key.
|
|
561
|
+
logger.info(logData, `${name} blocked on missing key from v${states[name].version}, retrying with snapshot`);
|
|
562
|
+
forceSnapshotCollections.add(name);
|
|
563
|
+
}
|
|
564
|
+
else if (isAppStateSyncIrrecoverable(error, attemptsMap[name])) {
|
|
565
|
+
logger.warn(logData, `failed to sync ${name} from v${states[name].version}, giving up`);
|
|
566
|
+
collectionsToHandle.delete(name);
|
|
567
|
+
}
|
|
568
|
+
else {
|
|
569
|
+
logger.info(logData, `failed to sync ${name} from v${states[name].version}, forcing snapshot retry`);
|
|
570
|
+
// force a full snapshot on retry to recover from
|
|
571
|
+
// corrupted local state (e.g. LTHash MAC mismatch)
|
|
572
|
+
forceSnapshotCollections.add(name);
|
|
573
|
+
}
|
|
495
574
|
}
|
|
496
575
|
}
|
|
497
576
|
}
|
|
@@ -506,25 +585,25 @@ export const makeChatsSocket = (config) => {
|
|
|
506
585
|
* type = "preview" for a low res picture
|
|
507
586
|
* type = "image for the high res picture"
|
|
508
587
|
*/
|
|
509
|
-
const profilePictureUrl = async (jid, type = 'image', timeoutMs) => {
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
}];
|
|
588
|
+
const profilePictureUrl = async (jid, type = 'image', timeoutMs = 5000, shouldIncludeTcToken = false) => {
|
|
589
|
+
const baseContent = [{ tag: 'picture', attrs: { type, query: 'url' } }];
|
|
590
|
+
// WA Web only includes tctoken for user JIDs (not groups/newsletters)
|
|
591
|
+
// and never for own profile pic (Chat model for self has no tcToken).
|
|
592
|
+
// Including tctoken for own JID causes the server to never respond.
|
|
593
|
+
const normalizedJid = jidNormalizedUser(jid);
|
|
594
|
+
const isUserJid = isPnUser(normalizedJid) || isLidUser(normalizedJid);
|
|
595
|
+
const me = authState.creds.me;
|
|
596
|
+
const isSelf = me && (normalizedJid === jidNormalizedUser(me.id) || (me.lid && normalizedJid === jidNormalizedUser(me.lid)));
|
|
597
|
+
let content = baseContent;
|
|
598
|
+
if (shouldIncludeTcToken && serverProps.profilePicPrivacyToken && isUserJid && !isSelf) {
|
|
599
|
+
content = await buildTcTokenFromJid({
|
|
600
|
+
authState,
|
|
601
|
+
jid: normalizedJid,
|
|
602
|
+
baseContent,
|
|
603
|
+
getLIDForPN
|
|
604
|
+
});
|
|
527
605
|
}
|
|
606
|
+
jid = jidNormalizedUser(jid);
|
|
528
607
|
const result = await query({
|
|
529
608
|
tag: 'iq',
|
|
530
609
|
attrs: {
|
|
@@ -533,16 +612,9 @@ export const makeChatsSocket = (config) => {
|
|
|
533
612
|
type: 'get',
|
|
534
613
|
xmlns: 'w:profile:picture'
|
|
535
614
|
},
|
|
536
|
-
content
|
|
615
|
+
content
|
|
537
616
|
}, timeoutMs);
|
|
538
617
|
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
618
|
return child?.attrs?.url;
|
|
547
619
|
};
|
|
548
620
|
const createCallLink = async (type, event, timeoutMs) => {
|
|
@@ -606,7 +678,12 @@ export const makeChatsSocket = (config) => {
|
|
|
606
678
|
* @param tcToken token for subscription, use if present
|
|
607
679
|
*/
|
|
608
680
|
const presenceSubscribe = async (toJid) => {
|
|
609
|
-
|
|
681
|
+
// Only include tctoken for user JIDs — groups/newsletters don't use tctokens
|
|
682
|
+
const normalizedToJid = jidNormalizedUser(toJid);
|
|
683
|
+
const isUserJid = isPnUser(normalizedToJid) || isLidUser(normalizedToJid);
|
|
684
|
+
const tcTokenContent = isUserJid
|
|
685
|
+
? await buildTcTokenFromJid({ authState, jid: normalizedToJid, getLIDForPN })
|
|
686
|
+
: undefined;
|
|
610
687
|
return sendNode({
|
|
611
688
|
tag: 'presence',
|
|
612
689
|
attrs: {
|
|
@@ -627,7 +704,8 @@ export const makeChatsSocket = (config) => {
|
|
|
627
704
|
if (tag === 'presence') {
|
|
628
705
|
presence = {
|
|
629
706
|
lastKnownPresence: attrs.type === 'unavailable' ? 'unavailable' : 'available',
|
|
630
|
-
lastSeen: attrs.last && attrs.last !== 'deny' ? +attrs.last : undefined
|
|
707
|
+
lastSeen: attrs.last && attrs.last !== 'deny' ? +attrs.last : undefined,
|
|
708
|
+
groupOnlineCount: attrs.count ? +attrs.count : undefined
|
|
631
709
|
};
|
|
632
710
|
}
|
|
633
711
|
else if (Array.isArray(content)) {
|
|
@@ -661,7 +739,7 @@ export const makeChatsSocket = (config) => {
|
|
|
661
739
|
logger.debug({ patch: patchCreate }, 'applying app patch');
|
|
662
740
|
await resyncAppState([name], false);
|
|
663
741
|
const { [name]: currentSyncVersion } = await authState.keys.get('app-state-sync-version', [name]);
|
|
664
|
-
initial = currentSyncVersion
|
|
742
|
+
initial = currentSyncVersion ? ensureLTHashStateVersion(currentSyncVersion) : newLTHashState();
|
|
665
743
|
encodeResult = await encodeSyncdPatch(patchCreate, myAppStateKeyId, initial, getAppStateSyncKey);
|
|
666
744
|
const { patch, state } = encodeResult;
|
|
667
745
|
const node = {
|
|
@@ -707,22 +785,21 @@ export const makeChatsSocket = (config) => {
|
|
|
707
785
|
}
|
|
708
786
|
}
|
|
709
787
|
};
|
|
710
|
-
/**
|
|
788
|
+
/** fetch AB props */
|
|
711
789
|
const fetchProps = async () => {
|
|
712
|
-
//TODO: implement both protocol 1 and protocol 2 prop fetching, specially for abKey for WM
|
|
713
790
|
const resultNode = await query({
|
|
714
791
|
tag: 'iq',
|
|
715
792
|
attrs: {
|
|
716
793
|
to: S_WHATSAPP_NET,
|
|
717
|
-
xmlns: '
|
|
794
|
+
xmlns: 'abt',
|
|
718
795
|
type: 'get'
|
|
719
796
|
},
|
|
720
797
|
content: [
|
|
721
798
|
{
|
|
722
799
|
tag: 'props',
|
|
723
800
|
attrs: {
|
|
724
|
-
protocol: '
|
|
725
|
-
hash: authState
|
|
801
|
+
protocol: '1',
|
|
802
|
+
...(authState?.creds?.lastPropHash ? { hash: authState.creds.lastPropHash } : {})
|
|
726
803
|
}
|
|
727
804
|
}
|
|
728
805
|
]
|
|
@@ -737,7 +814,20 @@ export const makeChatsSocket = (config) => {
|
|
|
737
814
|
}
|
|
738
815
|
props = reduceBinaryNodeToDictionary(propsNode, 'prop');
|
|
739
816
|
}
|
|
740
|
-
|
|
817
|
+
// Extract protocol-relevant AB props (only the ones we need)
|
|
818
|
+
const privacyTokenProp = props['10518'] ?? props['privacy_token_sending_on_all_1_on_1_messages'];
|
|
819
|
+
if (privacyTokenProp !== undefined) {
|
|
820
|
+
serverProps.privacyTokenOn1to1 = privacyTokenProp === 'true' || privacyTokenProp === '1';
|
|
821
|
+
}
|
|
822
|
+
const profilePicProp = props['9666'] ?? props['profile_scraping_privacy_token_in_photo_iq'];
|
|
823
|
+
if (profilePicProp !== undefined) {
|
|
824
|
+
serverProps.profilePicPrivacyToken = profilePicProp === 'true' || profilePicProp === '1';
|
|
825
|
+
}
|
|
826
|
+
const lidIssueProp = props['14303'] ?? props['lid_trusted_token_issue_to_lid'];
|
|
827
|
+
if (lidIssueProp !== undefined) {
|
|
828
|
+
serverProps.lidTrustedTokenIssueToLid = lidIssueProp === 'true' || lidIssueProp === '1';
|
|
829
|
+
}
|
|
830
|
+
logger.debug({ serverProps }, 'fetched props');
|
|
741
831
|
return props;
|
|
742
832
|
};
|
|
743
833
|
/**
|
|
@@ -877,6 +967,47 @@ export const makeChatsSocket = (config) => {
|
|
|
877
967
|
? shouldSyncHistoryMessage(historyMsg) &&
|
|
878
968
|
PROCESSABLE_HISTORY_TYPES.includes(historyMsg.syncType)
|
|
879
969
|
: false;
|
|
970
|
+
if (historyMsg && shouldProcessHistoryMsg) {
|
|
971
|
+
const syncType = historyMsg.syncType;
|
|
972
|
+
// INITIAL_BOOTSTRAP — fire immediately, no progress check (same as WA Web K function)
|
|
973
|
+
if (syncType === proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP &&
|
|
974
|
+
!historySyncStatus.initialBootstrapComplete) {
|
|
975
|
+
historySyncStatus.initialBootstrapComplete = true;
|
|
976
|
+
ev.emit('messaging-history.status', {
|
|
977
|
+
syncType,
|
|
978
|
+
status: 'complete',
|
|
979
|
+
explicit: true
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
// RECENT with progress === 100 — explicit completion
|
|
983
|
+
if (syncType === proto.HistorySync.HistorySyncType.RECENT &&
|
|
984
|
+
historyMsg.progress === 100 &&
|
|
985
|
+
!historySyncStatus.recentSyncComplete) {
|
|
986
|
+
historySyncStatus.recentSyncComplete = true;
|
|
987
|
+
clearTimeout(historySyncPausedTimeout);
|
|
988
|
+
historySyncPausedTimeout = undefined;
|
|
989
|
+
ev.emit('messaging-history.status', {
|
|
990
|
+
syncType,
|
|
991
|
+
status: 'complete',
|
|
992
|
+
explicit: true
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
// Reset 120s paused timeout on any RECENT chunk (like WA Web's handleChunkProgress)
|
|
996
|
+
if (syncType === proto.HistorySync.HistorySyncType.RECENT && !historySyncStatus.recentSyncComplete) {
|
|
997
|
+
clearTimeout(historySyncPausedTimeout);
|
|
998
|
+
historySyncPausedTimeout = setTimeout(() => {
|
|
999
|
+
if (!historySyncStatus.recentSyncComplete) {
|
|
1000
|
+
historySyncStatus.recentSyncComplete = true;
|
|
1001
|
+
ev.emit('messaging-history.status', {
|
|
1002
|
+
syncType: proto.HistorySync.HistorySyncType.RECENT,
|
|
1003
|
+
status: 'paused',
|
|
1004
|
+
explicit: false
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
historySyncPausedTimeout = undefined;
|
|
1008
|
+
}, HISTORY_SYNC_PAUSED_TIMEOUT_MS);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
880
1011
|
// State machine: decide on sync and flush
|
|
881
1012
|
if (historyMsg && syncState === SyncState.AwaitingInitialSync) {
|
|
882
1013
|
if (awaitingSyncTimeout) {
|
|
@@ -896,6 +1027,8 @@ export const makeChatsSocket = (config) => {
|
|
|
896
1027
|
}
|
|
897
1028
|
const doAppStateSync = async () => {
|
|
898
1029
|
if (syncState === SyncState.Syncing) {
|
|
1030
|
+
// All collections will be synced, so clear any blocked ones
|
|
1031
|
+
blockedCollections.clear();
|
|
899
1032
|
logger.info('Doing app state sync');
|
|
900
1033
|
await resyncAppState(ALL_WA_PATCH_NAMES, true);
|
|
901
1034
|
// Sync is complete, go online and flush everything
|
|
@@ -955,6 +1088,11 @@ export const makeChatsSocket = (config) => {
|
|
|
955
1088
|
}
|
|
956
1089
|
});
|
|
957
1090
|
ev.on('connection.update', ({ connection, receivedPendingNotifications }) => {
|
|
1091
|
+
if (connection === 'close') {
|
|
1092
|
+
blockedCollections.clear();
|
|
1093
|
+
clearTimeout(historySyncPausedTimeout);
|
|
1094
|
+
historySyncPausedTimeout = undefined;
|
|
1095
|
+
}
|
|
958
1096
|
if (connection === 'open') {
|
|
959
1097
|
if (fireInitQueries) {
|
|
960
1098
|
executeInitQueries().catch(error => onUnexpectedError(error, 'init queries'));
|
|
@@ -964,6 +1102,10 @@ export const makeChatsSocket = (config) => {
|
|
|
964
1102
|
if (!receivedPendingNotifications || syncState !== SyncState.Connecting) {
|
|
965
1103
|
return;
|
|
966
1104
|
}
|
|
1105
|
+
historySyncStatus.initialBootstrapComplete = false;
|
|
1106
|
+
historySyncStatus.recentSyncComplete = false;
|
|
1107
|
+
clearTimeout(historySyncPausedTimeout);
|
|
1108
|
+
historySyncPausedTimeout = undefined;
|
|
967
1109
|
syncState = SyncState.AwaitingInitialSync;
|
|
968
1110
|
logger.info('Connection is now AwaitingInitialSync, buffering events');
|
|
969
1111
|
ev.buffer();
|
|
@@ -976,19 +1118,49 @@ export const makeChatsSocket = (config) => {
|
|
|
976
1118
|
setTimeout(() => ev.flush(), 0);
|
|
977
1119
|
return;
|
|
978
1120
|
}
|
|
979
|
-
|
|
1121
|
+
// On reconnection (accountSyncCounter > 0), the server does not push
|
|
1122
|
+
// history sync notifications — the device already has its data.
|
|
1123
|
+
// Skip the 20s wait and go online immediately.
|
|
1124
|
+
if (authState.creds.accountSyncCounter > 0) {
|
|
1125
|
+
logger.info('Reconnection with existing sync data, skipping history sync wait. Transitioning to Online.');
|
|
1126
|
+
syncState = SyncState.Online;
|
|
1127
|
+
setTimeout(() => ev.flush(), 0);
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
logger.info('First connection, awaiting history sync notification with a 20s timeout.');
|
|
980
1131
|
if (awaitingSyncTimeout) {
|
|
981
1132
|
clearTimeout(awaitingSyncTimeout);
|
|
982
1133
|
}
|
|
983
1134
|
awaitingSyncTimeout = setTimeout(() => {
|
|
984
1135
|
if (syncState === SyncState.AwaitingInitialSync) {
|
|
985
|
-
// TODO: investigate
|
|
986
1136
|
logger.warn('Timeout in AwaitingInitialSync, forcing state to Online and flushing buffer');
|
|
987
1137
|
syncState = SyncState.Online;
|
|
988
1138
|
ev.flush();
|
|
1139
|
+
// Increment so subsequent reconnections skip the 20s wait.
|
|
1140
|
+
// Late-arriving history is still processed via processMessage
|
|
1141
|
+
// regardless of the state machine phase.
|
|
1142
|
+
const accountSyncCounter = (authState.creds.accountSyncCounter || 0) + 1;
|
|
1143
|
+
ev.emit('creds.update', { accountSyncCounter });
|
|
989
1144
|
}
|
|
990
1145
|
}, 20000);
|
|
991
1146
|
});
|
|
1147
|
+
// When an app state sync key arrives (myAppStateKeyId is set) and there are
|
|
1148
|
+
// collections blocked on a missing key, trigger a re-sync for just those collections.
|
|
1149
|
+
// This mirrors WA Web's Blocked → retry-on-key-arrival behavior.
|
|
1150
|
+
ev.on('creds.update', ({ myAppStateKeyId }) => {
|
|
1151
|
+
if (!myAppStateKeyId || blockedCollections.size === 0) {
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
// If we're in the middle of a full sync, doAppStateSync handles all collections
|
|
1155
|
+
if (syncState === SyncState.Syncing) {
|
|
1156
|
+
blockedCollections.clear();
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
const collections = [...blockedCollections];
|
|
1160
|
+
blockedCollections.clear();
|
|
1161
|
+
logger.info({ collections }, 'app state sync key arrived, re-syncing blocked collections');
|
|
1162
|
+
resyncAppState(collections, false).catch(error => onUnexpectedError(error, 'blocked collections resync'));
|
|
1163
|
+
});
|
|
992
1164
|
ev.on('lid-mapping.update', async ({ lid, pn }) => {
|
|
993
1165
|
try {
|
|
994
1166
|
await signalRepository.lidMapping.storeLIDPNMappings([{ lid, pn }]);
|
|
@@ -1010,6 +1182,8 @@ export const makeChatsSocket = (config) => {
|
|
|
1010
1182
|
});
|
|
1011
1183
|
return {
|
|
1012
1184
|
...sock,
|
|
1185
|
+
findUserId,
|
|
1186
|
+
serverProps,
|
|
1013
1187
|
createCallLink,
|
|
1014
1188
|
getBotListV2,
|
|
1015
1189
|
messageMutex,
|
|
@@ -1025,7 +1199,6 @@ export const makeChatsSocket = (config) => {
|
|
|
1025
1199
|
fetchBlocklist,
|
|
1026
1200
|
fetchStatus,
|
|
1027
1201
|
fetchDisappearingDuration,
|
|
1028
|
-
findUserId,
|
|
1029
1202
|
updateProfilePicture,
|
|
1030
1203
|
removeProfilePicture,
|
|
1031
1204
|
updateProfileStatus,
|
|
@@ -1047,6 +1220,7 @@ export const makeChatsSocket = (config) => {
|
|
|
1047
1220
|
cleanDirtyBits,
|
|
1048
1221
|
addOrEditContact,
|
|
1049
1222
|
removeContact,
|
|
1223
|
+
placeholderResendCache,
|
|
1050
1224
|
addLabel,
|
|
1051
1225
|
addChatLabel,
|
|
1052
1226
|
removeChatLabel,
|
|
@@ -1056,4 +1230,4 @@ export const makeChatsSocket = (config) => {
|
|
|
1056
1230
|
addOrEditQuickReply,
|
|
1057
1231
|
removeQuickReply
|
|
1058
1232
|
};
|
|
1059
|
-
};
|
|
1233
|
+
};
|