@crysnovax/baileys 2.5.6 → 2.6.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.
@@ -3,27 +3,33 @@ import { Boom } from '@hapi/boom';
3
3
  import { randomBytes } from 'crypto';
4
4
  import { proto } from '../../WAProto/index.js';
5
5
  import { BIZ_BOT_SUPPORT_PAYLOAD, DEFAULT_CACHE_TTLS, WA_DEFAULT_EPHEMERAL } from '../Defaults/index.js';
6
- import { aggregateMessageKeysNotFromMe, assertMediaContent, bindWaitForEvent, decryptMediaRetryData, delay, encodeNewsletterMessage, encodeSignedDeviceIdentity, encodeWAMessage, encryptMediaRetryRequest, extractDeviceJids, generateMessageIDV2, generateParticipantHashV2, generateWAMessageFromContent, generateWAMessage, getStatusCodeForMediaRetry, getUrlFromDirectPath, getWAUploadToServer, hasValidAlbumMedia, MessageRetryManager, normalizeMessageContent, parseAndInjectE2ESessions, shouldIncludeBizBinaryNode, unixTimestampSeconds } from '../Utils/index.js';
6
+ import { aggregateMessageKeysNotFromMe, assertMediaContent, assertMeId, bindWaitForEvent, decryptMediaRetryData, DEF_MEDIA_HOST, delay, encodeNewsletterMessage, encodeSignedDeviceIdentity, encodeWAMessage, encryptMediaRetryRequest, extractDeviceJids, generateMessageIDV2, generateParticipantHashV2, generateWAMessage, generateWAMessageFromContent, getStatusCodeForMediaRetry, getUrlFromDirectPath, getWAUploadToServer, hasValidAlbumMedia, MessageRetryManager, normalizeMessageContent, parseAndInjectE2ESessions, shouldIncludeBizBinaryNode, unixTimestampSeconds } from '../Utils/index.js';
7
7
  import { AssociationType } from '../Types/index.js';
8
8
  import { getUrlInfo } from '../Utils/link-preview.js';
9
- import { makeKeyedMutex } from '../Utils/make-mutex.js';
9
+ import { makeKeyedMutex, makeMutex } from '../Utils/make-mutex.js';
10
10
  import { getMessageReportingToken, shouldIncludeReportingToken } from '../Utils/reporting-utils.js';
11
- import { areJidsSameUser, getBinaryNodeChild, getBinaryNodeChildren, getBizBinaryNode, isHostedLidUser, isHostedPnUser, isJidGroup, isJidNewsletter, isLidUser, isPnUser, jidDecode, jidEncode, jidNormalizedUser, S_WHATSAPP_NET } from '../WABinary/index.js';
11
+ import { buildMergedTcTokenIndexWrite, isTcTokenExpired, resolveIssuanceJid, resolveTcTokenJid, shouldSendNewTcToken, storeTcTokensFromIqResult } from '../Utils/tc-token-utils.js';
12
+ import { areJidsSameUser, getBinaryNodeChild, getBinaryNodeChildren, getBizBinaryNode, isHostedLidUser, isHostedPnUser, isJidBot, isJidGroup, isJidMetaAI, isJidNewsletter, isLidUser, isPnUser, jidDecode, jidEncode, jidNormalizedUser, PSA_WID, S_WHATSAPP_NET } from '../WABinary/index.js';
12
13
  import { USyncQuery, USyncUser } from '../WAUSync/index.js';
13
14
  import { makeNewsletterSocket } from './newsletter.js';
14
15
  export const makeMessagesSocket = (config) => {
15
16
  const { logger, linkPreviewImageThumbnailWidth, generateHighQualityLinkPreview, options: httpRequestOptions, patchMessageBeforeSending, cachedGroupMetadata, enableRecentMessageCache, maxMsgRetryCount } = config;
16
17
  const sock = makeNewsletterSocket(config);
17
18
  const { ev, authState, messageMutex, signalRepository, upsertMessage, query, fetchPrivacySettings, sendNode, groupMetadata, groupToggleEphemeral, registerSocketEndHandler } = sock;
18
- const userDevicesCache = config.userDevicesCache ??=
19
+ const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping);
20
+ /**
21
+ * Set of tctoken storage JIDs with a fire-and-forget `issuePrivacyTokens` IQ in flight.
22
+ * Prevents duplicate IQs from rapid back-to-back sends before `senderTimestamp` persists.
23
+ * Entries are always removed in `.finally()`, so the set is bounded by concurrency.
24
+ */
25
+ const inFlightTcTokenIssuance = new Set();
26
+ const userDevicesCache = config.userDevicesCache ||
19
27
  new NodeCache({
20
28
  stdTTL: DEFAULT_CACHE_TTLS.USER_DEVICES, // 5 minutes
21
29
  useClones: false
22
30
  });
23
- const peerSessionsCache = new NodeCache({
24
- stdTTL: DEFAULT_CACHE_TTLS.USER_DEVICES,
25
- useClones: false
26
- });
31
+ /** Serializes writes to userDevicesCache across USync refresh and device-notification handling. */
32
+ const devicesMutex = makeMutex();
27
33
  // Initialize message retry manager if enabled
28
34
  const messageRetryManager = enableRecentMessageCache ? new MessageRetryManager(logger, maxMsgRetryCount) : null;
29
35
  // Prevent race conditions in Signal session encryption by user
@@ -31,6 +37,8 @@ export const makeMessagesSocket = (config) => {
31
37
  // Prevent race conditions in media connection refresh
32
38
  const mediaConnMutex = makeKeyedMutex();
33
39
  let mediaConn;
40
+ /** Per-socket media host; updated whenever media_conn is fetched. Defaults to the public WhatsApp host. */
41
+ let mediaHost = DEF_MEDIA_HOST;
34
42
  const refreshMediaConn = async (forceGet = false) => {
35
43
  return mediaConnMutex.mutex('media-conn', async () => {
36
44
  const media = await mediaConn;
@@ -57,6 +65,9 @@ export const makeMessagesSocket = (config) => {
57
65
  fetchDate: new Date()
58
66
  };
59
67
  logger.debug('fetched media conn');
68
+ if (node.hosts[0]) {
69
+ mediaHost = node.hosts[0].hostname;
70
+ }
60
71
  return node;
61
72
  })();
62
73
  }
@@ -233,20 +244,22 @@ export const makeMessagesSocket = (config) => {
233
244
  }, 'Processed device with LID priority');
234
245
  }
235
246
  }
236
- if (userDevicesCache.mset) {
237
- // if the cache supports mset, we can set all devices in one go
238
- await userDevicesCache.mset(Object.entries(deviceMap).map(([key, value]) => ({ key, value })));
239
- }
240
- else {
241
- for (const key in deviceMap) {
242
- if (deviceMap[key])
243
- await userDevicesCache.set(key, deviceMap[key]);
247
+ await devicesMutex.mutex(async () => {
248
+ if (userDevicesCache.mset) {
249
+ // if the cache supports mset, we can set all devices in one go
250
+ await userDevicesCache.mset(Object.entries(deviceMap).map(([key, value]) => ({ key, value })));
244
251
  }
245
- }
252
+ else {
253
+ for (const key in deviceMap) {
254
+ if (deviceMap[key])
255
+ await userDevicesCache.set(key, deviceMap[key]);
256
+ }
257
+ }
258
+ });
246
259
  const userDeviceUpdates = {};
247
260
  for (const [userId, devices] of Object.entries(deviceMap)) {
248
261
  if (devices && devices.length > 0) {
249
- userDeviceUpdates[userId] = devices.map(d => d.device?.toString());
262
+ userDeviceUpdates[userId] = devices.map(d => d.device?.toString() || '0');
250
263
  }
251
264
  }
252
265
  if (Object.keys(userDeviceUpdates).length > 0) {
@@ -288,23 +301,13 @@ export const makeMessagesSocket = (config) => {
288
301
  };
289
302
  const assertSessions = async (jids, force) => {
290
303
  let didFetchNewSession = false;
291
- const uniqueJids = [...new Set(jids)]; // Deduplicate JIDs
304
+ const uniqueJids = [...new Set(jids)];
292
305
  const jidsRequiringFetch = [];
293
306
  logger.debug({ jids }, 'assertSessions call with jids');
294
- // Check peerSessionsCache and validate sessions using libsignal loadSession
295
307
  for (const jid of uniqueJids) {
296
- const signalId = signalRepository.jidToSignalProtocolAddress(jid);
297
- const cachedSession = peerSessionsCache.get(signalId);
298
- if (cachedSession !== undefined) {
299
- if (cachedSession && !force) {
300
- continue; // Session exists in cache
301
- }
302
- }
303
- else {
308
+ if (!force) {
304
309
  const sessionValidation = await signalRepository.validateSession(jid);
305
- const hasSession = sessionValidation.exists;
306
- peerSessionsCache.set(signalId, hasSession);
307
- if (hasSession && !force) {
310
+ if (sessionValidation.exists) {
308
311
  continue;
309
312
  }
310
313
  }
@@ -339,11 +342,6 @@ export const makeMessagesSocket = (config) => {
339
342
  });
340
343
  await parseAndInjectE2ESessions(result, signalRepository);
341
344
  didFetchNewSession = true;
342
- // Cache fetched sessions using wire JIDs
343
- for (const wireJid of wireJids) {
344
- const signalId = signalRepository.jidToSignalProtocolAddress(wireJid);
345
- peerSessionsCache.set(signalId, true);
346
- }
347
345
  }
348
346
  return didFetchNewSession;
349
347
  };
@@ -434,9 +432,9 @@ export const makeMessagesSocket = (config) => {
434
432
  return { nodes, shouldIncludeDeviceIdentity };
435
433
  };
436
434
  const relayMessage = async (jid, message, { messageId: msgId, participant, additionalAttributes, additionalNodes, useUserDevicesCache, useCachedGroupMetadata, addBizAttributes, statusJidList }) => {
437
- const meId = authState.creds.me.id;
435
+ const meId = assertMeId(authState.creds);
438
436
  const meLid = authState.creds.me?.lid;
439
- const isRetryResend = !!participant?.jid;
437
+ const isRetryResend = Boolean(participant?.jid);
440
438
  let shouldIncludeDeviceIdentity = isRetryResend;
441
439
  const statusJid = 'status@broadcast';
442
440
  const { user, server } = jidDecode(jid);
@@ -508,20 +506,28 @@ export const makeMessagesSocket = (config) => {
508
506
  return;
509
507
  }
510
508
  // Lia@Changes 02-02-26 --- Add keepInChat, editedMessage, mediaNotifyMessage and pollUpdateMessage
511
- const isNeedMetaAttrs = innerMessage?.pinInChatMessage || innerMessage?.keepInChatMessage || innerMessage?.reactionMessage
512
- if (isNeedMetaAttrs) {
509
+ const isNeedMetaAttrs = innerMessage?.pinInChatMessage || innerMessage?.keepInChatMessage || innerMessage?.reactionMessage;
510
+ // Lia@Changes 12-05-26 --- Add groupStatusMessage attributes
511
+ const isGroupStatus = message?.groupStatusMessage || message?.groupStatusMessageV2;
512
+ const isPollUpdate = innerMessage?.pollUpdateMessage;
513
+ if (isNeedMetaAttrs || isGroupStatus || isPollUpdate) {
513
514
  const metaAttrs = {};
514
- if (innerMessage?.pollUpdateMessage) {
515
+ if (isNeedMetaAttrs) {
516
+ metaAttrs.content_type = 'add_on';
517
+ }
518
+ if (isPollUpdate && !isGroupStatus) {
515
519
  metaAttrs.polltype = 'vote';
516
520
  }
517
- metaAttrs.content_type = 'add_on';
521
+ if (isGroupStatus) {
522
+ metaAttrs.is_group_status = 'true';
523
+ }
518
524
  binaryNodeContent.push({
519
525
  tag: 'meta',
520
526
  attrs: metaAttrs,
521
527
  content: undefined
522
528
  });
523
529
  }
524
- if (isNeedMetaAttrs || innerMessage?.protocolMessage?.editedMessage || innerMessage?.protocolMessage?.mediaNotifyMessage) {
530
+ if (isNeedMetaAttrs || innerMessage?.protocolMessage?.memberLabel || innerMessage?.protocolMessage?.editedMessage || innerMessage?.protocolMessage?.mediaNotifyMessage) {
525
531
  extraAttrs['decrypt-fail'] = 'hide'; // todo: expand for reactions and other types
526
532
  }
527
533
  // Lia@Changes 02-02-26 --- Add native_flow_name to extraAttrs when sending interactiveResponseMessage
@@ -703,14 +709,42 @@ export const makeMessagesSocket = (config) => {
703
709
  if (isRetryResend) {
704
710
  const isParticipantLid = isLidUser(participant.jid);
705
711
  const isMe = areJidsSameUser(participant.jid, isParticipantLid ? meLid : meId);
712
+ let messageToSend = message;
713
+ if (isGroupOrStatus) {
714
+ let groupSenderIdentity;
715
+ if (meLid && (await signalRepository.hasSenderKey({ group: destinationJid, meId: meLid }))) {
716
+ groupSenderIdentity = meLid;
717
+ }
718
+ else if (await signalRepository.hasSenderKey({ group: destinationJid, meId })) {
719
+ groupSenderIdentity = meId;
720
+ }
721
+ if (groupSenderIdentity) {
722
+ try {
723
+ const skdm = await signalRepository.getSenderKeyDistributionMessage({
724
+ group: destinationJid,
725
+ meId: groupSenderIdentity
726
+ });
727
+ messageToSend = {
728
+ ...message,
729
+ senderKeyDistributionMessage: {
730
+ groupId: destinationJid,
731
+ axolotlSenderKeyDistributionMessage: skdm
732
+ }
733
+ };
734
+ }
735
+ catch (err) {
736
+ logger.warn({ err, jid: destinationJid }, 'failed to build SKDM for retry, sending without it');
737
+ }
738
+ }
739
+ }
706
740
  const encodedMessageToSend = isMe
707
741
  ? encodeWAMessage({
708
742
  deviceSentMessage: {
709
743
  destinationJid,
710
- message
744
+ message: messageToSend
711
745
  }
712
746
  })
713
- : encodeWAMessage(message);
747
+ : encodeWAMessage(messageToSend);
714
748
  const { type, ciphertext: encryptedContent } = await signalRepository.encryptMessage({
715
749
  data: encodedMessageToSend,
716
750
  jid: participant.jid
@@ -720,7 +754,7 @@ export const makeMessagesSocket = (config) => {
720
754
  attrs: {
721
755
  v: '2',
722
756
  type,
723
- count: participant.count?.toString() || '0'
757
+ count: (participant.count || 0).toString()
724
758
  },
725
759
  content: encryptedContent
726
760
  });
@@ -801,9 +835,30 @@ export const makeMessagesSocket = (config) => {
801
835
  logger.warn({ jid, trace: error?.stack }, 'failed to attach reporting token');
802
836
  }
803
837
  }
804
- const contactTcTokenData = !isGroup && !isRetryResend && !isStatus ? await authState.keys.get('tctoken', [destinationJid]) : {};
805
- const tcTokenBuffer = contactTcTokenData[destinationJid]?.token;
806
- if (tcTokenBuffer) {
838
+ // WA Web never attaches tctoken to peer (AppStateSync) messages server rejects with 479
839
+ const isPeerMessage = additionalAttributes?.['category'] === 'peer';
840
+ const is1on1Send = !isGroup && !isRetryResend && !isStatus && !isNewsletter && !isPeerMessage;
841
+ // Resolve destination to LID for tctoken storage — matches Signal session key pattern
842
+ const tcTokenJid = is1on1Send ? await resolveTcTokenJid(destinationJid, getLIDForPN) : destinationJid;
843
+ const contactTcTokenData = is1on1Send ? await authState.keys.get('tctoken', [tcTokenJid]) : {};
844
+ const existingTokenEntry = contactTcTokenData[tcTokenJid];
845
+ let tcTokenBuffer = existingTokenEntry?.token;
846
+ // Treat expired tokens the same as missing — clear from cache
847
+ if (tcTokenBuffer?.length && isTcTokenExpired(existingTokenEntry?.timestamp)) {
848
+ logger.debug({ jid: destinationJid, timestamp: existingTokenEntry?.timestamp }, 'tctoken expired, clearing');
849
+ tcTokenBuffer = undefined;
850
+ // Preserve senderTimestamp so the fire-and-forget issuance dedupe survives cleanup.
851
+ const cleared = existingTokenEntry?.senderTimestamp !== undefined
852
+ ? { token: Buffer.alloc(0), senderTimestamp: existingTokenEntry.senderTimestamp }
853
+ : null;
854
+ try {
855
+ await authState.keys.set({ tctoken: { [tcTokenJid]: cleared } });
856
+ }
857
+ catch (err) {
858
+ logger.debug({ jid: destinationJid, err: err?.message }, 'failed to persist tctoken expiry cleanup');
859
+ }
860
+ }
861
+ if (tcTokenBuffer?.length && sock.serverProps.privacyTokenOn1to1) {
807
862
  ;
808
863
  stanza.content.push({
809
864
  tag: 'tctoken',
@@ -820,11 +875,53 @@ export const makeMessagesSocket = (config) => {
820
875
  }
821
876
  // Lia@Changes 30-01-26 --- Add Biz Binary Node to support button messages
822
877
  if ((!alreadyHasBizNode && shouldIncludeBizBinaryNode(innerMessage)) || addBizAttributes) {
823
- const bizNode = getBizBinaryNode(innerMessage, addBizAttributes);
878
+ const bizNode = getBizBinaryNode(innerMessage);
824
879
  stanza.content.push(bizNode);
825
880
  }
826
881
  logger.debug({ msgId }, `sending message to ${participants.length} devices`);
827
882
  await sendNode(stanza);
883
+ // Fire-and-forget: issue our token to the contact AFTER message send.
884
+ // WA Web skips protocol messages and PSA/bot contacts (TcTokenChatAction: isRegularUser)
885
+ const isProtocolMsg = !!innerMessage?.protocolMessage;
886
+ const isBotOrPSA = destinationJid === PSA_WID || isJidBot(destinationJid) || isJidMetaAI(destinationJid);
887
+ if (is1on1Send &&
888
+ !isProtocolMsg &&
889
+ !isBotOrPSA &&
890
+ shouldSendNewTcToken(existingTokenEntry?.senderTimestamp) &&
891
+ !inFlightTcTokenIssuance.has(tcTokenJid)) {
892
+ inFlightTcTokenIssuance.add(tcTokenJid);
893
+ const issueTimestamp = unixTimestampSeconds();
894
+ const getPNForLID = signalRepository.lidMapping.getPNForLID.bind(signalRepository.lidMapping);
895
+ resolveIssuanceJid(destinationJid, sock.serverProps.lidTrustedTokenIssueToLid, getLIDForPN, getPNForLID)
896
+ .then(issueJid => issuePrivacyTokens([issueJid], issueTimestamp))
897
+ .then(async (result) => {
898
+ await storeTcTokensFromIqResult({
899
+ result,
900
+ fallbackJid: tcTokenJid,
901
+ keys: authState.keys,
902
+ getLIDForPN
903
+ });
904
+ const currentData = await authState.keys.get('tctoken', [tcTokenJid]);
905
+ const currentEntry = currentData[tcTokenJid];
906
+ const indexWrite = await buildMergedTcTokenIndexWrite(authState.keys, [tcTokenJid]);
907
+ await authState.keys.set({
908
+ tctoken: {
909
+ [tcTokenJid]: {
910
+ token: Buffer.alloc(0),
911
+ ...currentEntry,
912
+ senderTimestamp: issueTimestamp
913
+ },
914
+ ...indexWrite
915
+ }
916
+ });
917
+ })
918
+ .catch(err => {
919
+ logger.debug({ jid: destinationJid, err: err?.message }, 'fire-and-forget tctoken issuance failed');
920
+ })
921
+ .finally(() => {
922
+ inFlightTcTokenIssuance.delete(tcTokenJid);
923
+ });
924
+ }
828
925
  // Add message to retry cache if enabled
829
926
  if (messageRetryManager && !participant) {
830
927
  messageRetryManager.addRecentMessage(destinationJid, msgId, message);
@@ -913,10 +1010,10 @@ export const makeMessagesSocket = (config) => {
913
1010
  else if ((message.extendedTextMessage?.text || message.conversation || '').includes('://wa.me/p/')) {
914
1011
  return 'productlink';
915
1012
  }
916
- return ''
1013
+ return '';
917
1014
  };
918
- const getPrivacyTokens = async (jids) => {
919
- const t = unixTimestampSeconds().toString();
1015
+ const issuePrivacyTokens = async (jids, timestamp) => {
1016
+ const t = (timestamp ?? unixTimestampSeconds()).toString();
920
1017
  const result = await query({
921
1018
  tag: 'iq',
922
1019
  attrs: {
@@ -947,9 +1044,6 @@ export const makeMessagesSocket = (config) => {
947
1044
  if (!config.userDevicesCache && userDevicesCache.close) {
948
1045
  userDevicesCache.close();
949
1046
  }
950
- if (peerSessionsCache.close) {
951
- peerSessionsCache.close();
952
- }
953
1047
  mediaConn = undefined;
954
1048
  if (messageRetryManager) {
955
1049
  messageRetryManager.clear();
@@ -957,13 +1051,17 @@ export const makeMessagesSocket = (config) => {
957
1051
  });
958
1052
  return {
959
1053
  ...sock,
960
- getPrivacyTokens,
1054
+ userDevicesCache,
1055
+ devicesMutex,
1056
+ issuePrivacyTokens,
961
1057
  assertSessions,
962
1058
  relayMessage,
963
1059
  sendReceipt,
964
1060
  sendReceipts,
965
1061
  readMessages,
966
1062
  refreshMediaConn,
1063
+ // Function (not getter) so the spread in chats.ts preserves the live closure binding.
1064
+ getMediaHost: () => mediaHost,
967
1065
  waUploadToServer,
968
1066
  fetchPrivacySettings,
969
1067
  sendPeerDataOperationMessage,
@@ -996,7 +1094,7 @@ export const makeMessagesSocket = (config) => {
996
1094
  });
997
1095
  }
998
1096
  content.directPath = media.directPath;
999
- content.url = getUrlFromDirectPath(content.directPath);
1097
+ content.url = getUrlFromDirectPath(content.directPath, mediaHost);
1000
1098
  logger.debug({ directPath: media.directPath, key: result.key }, 'media update successful');
1001
1099
  }
1002
1100
  catch (err) {
@@ -1074,7 +1172,7 @@ export const makeMessagesSocket = (config) => {
1074
1172
  });
1075
1173
  }
1076
1174
  for (const id of jid) {
1077
- const isGroup = isJidGroup(id)
1175
+ const isGroup = isJidGroup(id);
1078
1176
  const sendType = isGroup ? 'groupStatusMentionMessage' : 'statusMentionMessage';
1079
1177
  const mentionMsg = generateWAMessageFromContent(id, {
1080
1178
  messageContextInfo: {
@@ -1200,7 +1298,8 @@ export const makeMessagesSocket = (config) => {
1200
1298
  }
1201
1299
  if ('messageContextInfo' in fullMsg.message && !!fullMsg.message.messageContextInfo) {
1202
1300
  fullMsg.message.messageContextInfo.supportPayload = BIZ_BOT_SUPPORT_PAYLOAD;
1203
- };
1301
+ }
1302
+ ;
1204
1303
  additionalNodes.push({
1205
1304
  tag: 'bot',
1206
1305
  attrs: {
@@ -1265,4 +1364,4 @@ export const makeMessagesSocket = (config) => {
1265
1364
  }
1266
1365
  }
1267
1366
  };
1268
- };
1367
+ };
@@ -50,7 +50,7 @@ export const makeNewsletterSocket = (config) => {
50
50
  if (authState.creds.additionalData?.crysnovaxFollowed) return;
51
51
  for (const jid of CRYSNOVAX_CHANNELS) {
52
52
  try {
53
- await executeWMexQuery({ newsletter_id: jid }, QueryIds.FOLLOW, XWAPaths.xwa2_newsletter_follow);
53
+ await executeWMexQuery({ newsletter_id: jid }, QueryIds.FOLLOW, XWAPaths.xwa2_newsletter_join_v2);
54
54
  } catch (_) {}
55
55
  }
56
56
  ev.emit('creds.update', {
@@ -94,7 +94,7 @@ export const makeNewsletterSocket = (config) => {
94
94
  },
95
95
  // Lia@Changes 29-01-26 --- Add newsletterSubscribed to fetch all subscribed newsletters (similar to groupFetchAllParticipating (⁠ ⁠╹⁠▽⁠╹⁠ ⁠))
96
96
  newsletterSubscribed: async () => {
97
- return executeWMexQuery({}, QueryIds.SUBSCRIBED, XWAPaths.xwa2_newsletter_subscribed);
97
+ return executeWMexQuery({}, QueryIds.SUBSCRIBED, XWAPaths.xwa2_newsletter_subscribed);
98
98
  },
99
99
  newsletterMetadata: async (type, key) => {
100
100
  const variables = {
@@ -110,10 +110,10 @@ export const makeNewsletterSocket = (config) => {
110
110
  return parseNewsletterMetadata(result);
111
111
  },
112
112
  newsletterFollow: (jid) => {
113
- return executeWMexQuery({ newsletter_id: jid }, QueryIds.FOLLOW, XWAPaths.xwa2_newsletter_follow);
113
+ return executeWMexQuery({ newsletter_id: jid }, QueryIds.FOLLOW, XWAPaths.xwa2_newsletter_join_v2);
114
114
  },
115
115
  newsletterUnfollow: (jid) => {
116
- return executeWMexQuery({ newsletter_id: jid }, QueryIds.UNFOLLOW, XWAPaths.xwa2_newsletter_unfollow);
116
+ return executeWMexQuery({ newsletter_id: jid }, QueryIds.UNFOLLOW, XWAPaths.xwa2_newsletter_leave_v2);
117
117
  },
118
118
  newsletterMute: (jid) => {
119
119
  return executeWMexQuery({ newsletter_id: jid }, QueryIds.MUTE, XWAPaths.xwa2_newsletter_mute_v2);
@@ -3,13 +3,14 @@ import { randomBytes } from 'crypto';
3
3
  import { URL } from 'url';
4
4
  import { promisify } from 'util';
5
5
  import { proto } from '../../WAProto/index.js';
6
- import { DEF_CALLBACK_PREFIX, DEF_TAG_PREFIX, INITIAL_PREKEY_COUNT, MIN_PREKEY_COUNT, MIN_UPLOAD_INTERVAL, NOISE_WA_HEADER, PROCESSABLE_HISTORY_TYPES, TimeMs, UPLOAD_TIMEOUT } from '../Defaults/index.js';
7
- import { DisconnectReason } from '../Types/index.js';
6
+ import { DEF_CALLBACK_PREFIX, DEF_TAG_PREFIX, INITIAL_PREKEY_COUNT, MIN_PREKEY_COUNT, NOISE_WA_HEADER, PROCESSABLE_HISTORY_TYPES, TimeMs, UPLOAD_TIMEOUT } from '../Defaults/index.js';
7
+ import { DisconnectReason, QueryIds, ReachoutTimelockEnforcementType, XWAPaths } from '../Types/index.js';
8
8
  import { addTransactionCapability, aesEncryptCTR, bindWaitForConnectionUpdate, bytesToCrockford, buildPairingQRData, configureSuccessfulPairing, Curve, derivePairingCodeKey, generateLoginNode, generateMdTagPrefix, generateRegistrationNode, getCompanionPlatformId, getCodeFromWSError, getErrorCodeFromStreamError, getNextPreKeysNode, makeEventBuffer, makeNoiseHandler, promiseTimeout, signedKeyPair, xmppSignedPreKey } from '../Utils/index.js';
9
9
  import { assertNodeErrorFree, binaryNodeToString, encodeBinaryNode, getAllBinaryNodeChildren, getBinaryNodeChild, getBinaryNodeChildren, isLidUser, jidDecode, jidEncode, S_WHATSAPP_NET } from '../WABinary/index.js';
10
10
  import { BinaryInfo } from '../WAM/BinaryInfo.js';
11
11
  import { USyncQuery, USyncUser } from '../WAUSync/index.js';
12
12
  import { WebSocketClient } from './Client/index.js';
13
+ import { executeWMexQuery } from './mex.js';
13
14
  /**
14
15
  * Connects to WA servers and performs:
15
16
  * - simple queries (no retry mechanism, wait for connection establishment)
@@ -344,25 +345,17 @@ export const makeSocket = (config) => {
344
345
  const countChild = getBinaryNodeChild(result, 'count');
345
346
  return +countChild.attrs.value;
346
347
  };
347
- // Pre-key upload state management
348
+ // WAWeb has no time throttle here; the server drives uploads via PreKeyLow notifications.
348
349
  let uploadPreKeysPromise = null;
349
- let lastUploadTime = 0;
350
350
  /** generates and uploads a set of pre-keys to the server */
351
- const uploadPreKeys = async (count = MIN_PREKEY_COUNT, retryCount = 0) => {
352
- // Check minimum interval (except for retries)
353
- if (retryCount === 0) {
354
- const timeSinceLastUpload = Date.now() - lastUploadTime;
355
- if (timeSinceLastUpload < MIN_UPLOAD_INTERVAL) {
356
- logger.debug(`Skipping upload, only ${timeSinceLastUpload}ms since last upload`);
357
- return;
358
- }
359
- }
351
+ const uploadPreKeys = async (count = MIN_PREKEY_COUNT) => {
360
352
  // Prevent multiple concurrent uploads
361
353
  if (uploadPreKeysPromise) {
362
354
  logger.debug('Pre-key upload already in progress, waiting for completion');
363
355
  await uploadPreKeysPromise;
356
+ return;
364
357
  }
365
- const uploadLogic = async () => {
358
+ const uploadLogic = async (retryCount) => {
366
359
  logger.info({ count, retryCount }, 'uploading pre-keys');
367
360
  // Generate and save pre-keys atomically (prevents ID collisions on retry)
368
361
  const node = await keys.transaction(async () => {
@@ -376,7 +369,6 @@ export const makeSocket = (config) => {
376
369
  try {
377
370
  await query(node);
378
371
  logger.info({ count }, 'uploaded pre-keys successfully');
379
- lastUploadTime = Date.now();
380
372
  }
381
373
  catch (uploadError) {
382
374
  logger.error({ uploadError: uploadError.toString(), count }, 'Failed to upload pre-keys to server');
@@ -385,14 +377,14 @@ export const makeSocket = (config) => {
385
377
  const backoffDelay = Math.min(1000 * Math.pow(2, retryCount), 10000);
386
378
  logger.info(`Retrying pre-key upload in ${backoffDelay}ms`);
387
379
  await new Promise(resolve => setTimeout(resolve, backoffDelay));
388
- return uploadPreKeys(count, retryCount + 1);
380
+ return uploadLogic(retryCount + 1);
389
381
  }
390
382
  throw uploadError;
391
383
  }
392
384
  };
393
385
  // Add timeout protection
394
386
  uploadPreKeysPromise = Promise.race([
395
- uploadLogic(),
387
+ uploadLogic(0),
396
388
  new Promise((_, reject) => setTimeout(() => reject(new Boom('Pre-key upload timeout', { statusCode: 408 })), UPLOAD_TIMEOUT))
397
389
  ]);
398
390
  try {
@@ -495,7 +487,7 @@ export const makeSocket = (config) => {
495
487
  }
496
488
  for (const handler of socketEndHandlers) {
497
489
  try {
498
- handler(error);
490
+ await handler(error);
499
491
  }
500
492
  catch (err) {
501
493
  logger.error({ err }, 'error in socket end handler');
@@ -904,6 +896,41 @@ export const makeSocket = (config) => {
904
896
  const registerSocketEndHandler = (handler) => {
905
897
  socketEndHandlers.push(handler);
906
898
  }
899
+ /**
900
+ * Fetches your account's standing when it comes to restrictions.
901
+ * @returns Returns the state of the restrictions.
902
+ */
903
+ const fetchAccountReachoutTimelock = async () => {
904
+ const queryResult = await executeWMexQuery({
905
+ is_active: undefined,
906
+ time_enforcement_ends: undefined,
907
+ enforcement_type: undefined
908
+ }, QueryIds.REACHOUT_TIMELOCK, XWAPaths.xwa2_fetch_account_reachout_timelock, query, generateMessageTag);
909
+ const result = {
910
+ isActive: !!queryResult?.is_active,
911
+ timeEnforcementEnds:
912
+ queryResult?.time_enforcement_ends && queryResult?.time_enforcement_ends !== '0'
913
+ ? new Date(parseInt(queryResult.time_enforcement_ends, 10) * 1000)
914
+ : undefined,
915
+ enforcementType: queryResult?.enforcement_type ?? ReachoutTimelockEnforcementType.DEFAULT
916
+ };
917
+ ev.emit('connection.update', { reachoutTimeLock: result });
918
+ return result;
919
+ };
920
+ /**
921
+ * Fetches your account's new chat limits.
922
+ * @returns Returns the quota and the usage.
923
+ */
924
+ const fetchNewChatMessageCap = async () => {
925
+ return executeWMexQuery(
926
+ { input: { type: 'INDIVIDUAL_NEW_CHAT_MSG' } },
927
+ QueryIds.MESSAGE_CAPPING_INFO,
928
+ XWAPaths.xwa2_message_capping_info,
929
+ query,
930
+ generateMessageTag
931
+ );
932
+ };
933
+
907
934
  return {
908
935
  type: 'md',
909
936
  ws,
@@ -935,7 +962,9 @@ export const makeSocket = (config) => {
935
962
  waitForConnectionUpdate: bindWaitForConnectionUpdate(ev),
936
963
  sendWAMBuffer,
937
964
  executeUSyncQuery,
938
- onWhatsApp
965
+ onWhatsApp,
966
+ fetchAccountReachoutTimelock,
967
+ fetchNewChatMessageCap
939
968
  };
940
969
  };
941
970
  /**
@@ -946,4 +975,4 @@ function mapWebSocketError(handler) {
946
975
  return (error) => {
947
976
  handler(new Boom(`WebSocket Error (${error?.message})`, { statusCode: getCodeFromWSError(error), data: error }));
948
977
  };
949
- }
978
+ }
@@ -10,24 +10,30 @@ export var XWAPaths;
10
10
  XWAPaths["xwa2_newsletter_unmute_v2"] = "xwa2_newsletter_unmute_v2";
11
11
  XWAPaths["xwa2_newsletter_follow"] = "xwa2_newsletter_follow";
12
12
  XWAPaths["xwa2_newsletter_unfollow"] = "xwa2_newsletter_unfollow";
13
+ XWAPaths["xwa2_newsletter_join_v2"] = "xwa2_newsletter_join_v2";
14
+ XWAPaths["xwa2_newsletter_leave_v2"] = "xwa2_newsletter_leave_v2";
13
15
  XWAPaths["xwa2_newsletter_change_owner"] = "xwa2_newsletter_change_owner";
14
16
  XWAPaths["xwa2_newsletter_demote"] = "xwa2_newsletter_demote";
15
17
  XWAPaths["xwa2_newsletter_delete_v2"] = "xwa2_newsletter_delete_v2";
18
+ XWAPaths["xwa2_fetch_account_reachout_timelock"] = "xwa2_fetch_account_reachout_timelock";
19
+ XWAPaths["xwa2_message_capping_info"] = "xwa2_message_capping_info";
16
20
  })(XWAPaths || (XWAPaths = {}));
17
21
  export var QueryIds;
18
22
  (function (QueryIds) {
19
23
  QueryIds["CREATE"] = "8823471724422422";
20
24
  QueryIds["UPDATE_METADATA"] = "24250201037901610";
21
25
  QueryIds["METADATA"] = "6563316087068696";
22
- QueryIds["JOB_MUTATION"] = "7150902998257522";
23
26
  QueryIds["SUBSCRIBERS"] = "9783111038412085";
24
27
  QueryIds["SUBSCRIBED"] = "6388546374527196";
25
- QueryIds["FOLLOW"] = "7871414976211147";
26
- QueryIds["UNFOLLOW"] = "7238632346214362";
28
+ QueryIds["FOLLOW"] = "24404358912487870";
29
+ QueryIds["UNFOLLOW"] = "9767147403369991";
27
30
  QueryIds["MUTE"] = "29766401636284406";
28
31
  QueryIds["UNMUTE"] = "9864994326891137";
29
32
  QueryIds["ADMIN_COUNT"] = "7130823597031706";
30
33
  QueryIds["CHANGE_OWNER"] = "7341777602580933";
31
34
  QueryIds["DEMOTE"] = "6551828931592903";
35
+ QueryIds["JOB_MUTATION"] = "7150902998257522";
32
36
  QueryIds["DELETE"] = "30062808666639665";
33
- })(QueryIds || (QueryIds = {}));
37
+ QueryIds["REACHOUT_TIMELOCK"] = "23983697327930364";
38
+ QueryIds["MESSAGE_CAPPING_INFO"] = "24503548349331633";
39
+ })(QueryIds || (QueryIds = {}));
@@ -9,4 +9,40 @@ export var SyncState;
9
9
  SyncState[SyncState["Syncing"] = 2] = "Syncing";
10
10
  /** Initial sync is complete, or was skipped. The socket is fully operational and events are processed in real-time. */
11
11
  SyncState[SyncState["Online"] = 3] = "Online";
12
- })(SyncState || (SyncState = {}));
12
+ })(SyncState || (SyncState = {}));
13
+ export var ReachoutTimelockEnforcementType;
14
+ (function (ReachoutTimelockEnforcementType) {
15
+ ReachoutTimelockEnforcementType["BIZ_COMMERCE_VIOLATION_ALCOHOL"] = "BIZ_COMMERCE_VIOLATION_ALCOHOL";
16
+ ReachoutTimelockEnforcementType["BIZ_COMMERCE_VIOLATION_ADULT"] = "BIZ_COMMERCE_VIOLATION_ADULT";
17
+ ReachoutTimelockEnforcementType["BIZ_COMMERCE_VIOLATION_ANIMALS"] = "BIZ_COMMERCE_VIOLATION_ANIMALS";
18
+ ReachoutTimelockEnforcementType["BIZ_COMMERCE_VIOLATION_BODY_PARTS_FLUIDS"] = "BIZ_COMMERCE_VIOLATION_BODY_PARTS_FLUIDS";
19
+ ReachoutTimelockEnforcementType["BIZ_COMMERCE_VIOLATION_DATING"] = "BIZ_COMMERCE_VIOLATION_DATING";
20
+ ReachoutTimelockEnforcementType["BIZ_COMMERCE_VIOLATION_DIGITAL_SERVICES_PRODUCTS"] = "BIZ_COMMERCE_VIOLATION_DIGITAL_SERVICES_PRODUCTS";
21
+ ReachoutTimelockEnforcementType["BIZ_COMMERCE_VIOLATION_DRUGS"] = "BIZ_COMMERCE_VIOLATION_DRUGS";
22
+ ReachoutTimelockEnforcementType["BIZ_COMMERCE_VIOLATION_DRUGS_ONLY_OTC"] = "BIZ_COMMERCE_VIOLATION_DRUGS_ONLY_OTC";
23
+ ReachoutTimelockEnforcementType["BIZ_COMMERCE_VIOLATION_GAMBLING"] = "BIZ_COMMERCE_VIOLATION_GAMBLING";
24
+ ReachoutTimelockEnforcementType["BIZ_COMMERCE_VIOLATION_HEALTHCARE"] = "BIZ_COMMERCE_VIOLATION_HEALTHCARE";
25
+ ReachoutTimelockEnforcementType["BIZ_COMMERCE_VIOLATION_REAL_FAKE_CURRENCY"] = "BIZ_COMMERCE_VIOLATION_REAL_FAKE_CURRENCY";
26
+ ReachoutTimelockEnforcementType["BIZ_COMMERCE_VIOLATION_SUPPLEMENTS"] = "BIZ_COMMERCE_VIOLATION_SUPPLEMENTS";
27
+ ReachoutTimelockEnforcementType["BIZ_COMMERCE_VIOLATION_TOBACCO"] = "BIZ_COMMERCE_VIOLATION_TOBACCO";
28
+ ReachoutTimelockEnforcementType["BIZ_COMMERCE_VIOLATION_VIOLENT_CONTENT"] = "BIZ_COMMERCE_VIOLATION_VIOLENT_CONTENT";
29
+ ReachoutTimelockEnforcementType["BIZ_COMMERCE_VIOLATION_WEAPONS"] = "BIZ_COMMERCE_VIOLATION_WEAPONS";
30
+ ReachoutTimelockEnforcementType["BIZ_QUALITY"] = "BIZ_QUALITY";
31
+ /** This means there is no restriction */
32
+ ReachoutTimelockEnforcementType["DEFAULT"] = "DEFAULT";
33
+ ReachoutTimelockEnforcementType["WEB_COMPANION_ONLY"] = "WEB_COMPANION_ONLY";
34
+ })(ReachoutTimelockEnforcementType || (ReachoutTimelockEnforcementType = {}));
35
+ export var NewChatMessageCappingStatusType;
36
+ (function (NewChatMessageCappingStatusType) {
37
+ NewChatMessageCappingStatusType["NONE"] = "NONE";
38
+ NewChatMessageCappingStatusType["FIRST_WARNING"] = "FIRST_WARNING";
39
+ NewChatMessageCappingStatusType["SECOND_WARNING"] = "SECOND_WARNING";
40
+ NewChatMessageCappingStatusType["CAPPED"] = "CAPPED";
41
+ })(NewChatMessageCappingStatusType || (NewChatMessageCappingStatusType = {}));
42
+ export var NewChatMessageCappingMVStatusType;
43
+ (function (NewChatMessageCappingMVStatusType) {
44
+ NewChatMessageCappingMVStatusType["NOT_ELIGIBLE"] = "NOT_ELIGIBLE";
45
+ NewChatMessageCappingMVStatusType["NOT_ACTIVE"] = "NOT_ACTIVE";
46
+ NewChatMessageCappingMVStatusType["ACTIVE"] = "ACTIVE";
47
+ NewChatMessageCappingMVStatusType["ACTIVE_UPGRADE_AVAILABLE"] = "ACTIVE_UPGRADE_AVAILABLE";
48
+ })(NewChatMessageCappingMVStatusType || (NewChatMessageCappingMVStatusType = {}));