@crysnovax/baileys 2.0.0 → 2.5.2

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);
@@ -474,7 +472,7 @@ export const makeMessagesSocket = (config) => {
474
472
  });
475
473
  }
476
474
  await authState.keys.transaction(async () => {
477
- // Lia@Changes 02-02-26 --- Normalize message first to extract the original message and valid media type
475
+ // crysnovax@Changes 02-02-26 --- Normalize message first to extract the original message and valid media type
478
476
  const innerMessage = normalizeMessageContent(message);
479
477
  const mediaType = getMediaType(innerMessage);
480
478
  if (mediaType) {
@@ -483,14 +481,14 @@ export const makeMessagesSocket = (config) => {
483
481
  if (isNewsletter) {
484
482
  const patched = patchMessageBeforeSending ? await patchMessageBeforeSending(message, []) : message;
485
483
  const bytes = encodeNewsletterMessage(patched);
486
- // Lia@Changes 08-02-26 --- Add "additionalNodes" for newsletter too (⁠っ⁠˘̩⁠╭⁠╮⁠˘̩⁠)⁠っ
484
+ // crysnovax@Changes 08-02-26 --- Add "additionalNodes" for newsletter too (⁠っ⁠˘̩⁠╭⁠╮⁠˘̩⁠)⁠っ
487
485
  if (additionalNodes && additionalNodes.length > 0) {
488
486
  ;
489
487
  binaryNodeContent.push(...additionalNodes);
490
488
  }
491
489
  binaryNodeContent.push({
492
490
  tag: 'plaintext',
493
- attrs: extraAttrs, // Lia@Changes 02-02-26 --- Add extraAttrs to fix media being rejected when sending to newsletter (⁠◠⁠‿⁠◕⁠)
491
+ attrs: extraAttrs, // crysnovax@Changes 02-02-26 --- Add extraAttrs to fix media being rejected when sending to newsletter (⁠◠⁠‿⁠◕⁠)
494
492
  content: bytes
495
493
  });
496
494
  const stanza = {
@@ -507,24 +505,32 @@ export const makeMessagesSocket = (config) => {
507
505
  await sendNode(stanza);
508
506
  return;
509
507
  }
510
- // Lia@Changes 02-02-26 --- Add keepInChat, editedMessage, mediaNotifyMessage and pollUpdateMessage
511
- const isNeedMetaAttrs = innerMessage?.pinInChatMessage || innerMessage?.keepInChatMessage || innerMessage?.reactionMessage
512
- if (isNeedMetaAttrs) {
508
+ // crysnovax@Changes 02-02-26 --- Add keepInChat, editedMessage, mediaNotifyMessage and pollUpdateMessage
509
+ const isNeedMetaAttrs = innerMessage?.pinInChatMessage || innerMessage?.keepInChatMessage || innerMessage?.reactionMessage;
510
+ // crysnovax@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
- // Lia@Changes 02-02-26 --- Add native_flow_name to extraAttrs when sending interactiveResponseMessage
533
+ // crysnovax@Changes 02-02-26 --- Add native_flow_name to extraAttrs when sending interactiveResponseMessage
528
534
  if (innerMessage?.interactiveResponseMessage?.nativeFlowResponseMessage) {
529
535
  extraAttrs['native_flow_name'] = innerMessage.interactiveResponseMessage.nativeFlowResponseMessage.name;
530
536
  }
@@ -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',
@@ -818,13 +873,55 @@ export const makeMessagesSocket = (config) => {
818
873
  alreadyHasBizNode = !addBizAttributes &&
819
874
  additionalNodes.some(node => node.tag === 'biz');
820
875
  }
821
- // Lia@Changes 30-01-26 --- Add Biz Binary Node to support button messages
876
+ // crysnovax@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);
@@ -906,17 +1003,17 @@ export const makeMessagesSocket = (config) => {
906
1003
  else if (message.extendedTextMessage?.matchedText || message.groupInviteMessage) {
907
1004
  return 'url';
908
1005
  }
909
- // Lia@Note 02-02-26 --- Add more message type here
1006
+ // crysnovax@Note 02-02-26 --- Add more message type here
910
1007
  else if ((message.extendedTextMessage?.text || message.conversation || '').includes('://wa.me/c/')) {
911
1008
  return 'cataloglink';
912
1009
  }
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) {
@@ -1013,10 +1111,10 @@ export const makeMessagesSocket = (config) => {
1013
1111
  ev.emit('messages.update', [{ key: message.key, update: { message: message.message } }]);
1014
1112
  return message;
1015
1113
  },
1016
- // Lia@Changes 30-01-26 --- Add support for modifying additionalNodes and additionalAttributes using "options" in sendMessage()
1114
+ // crysnovax@Changes 30-01-26 --- Add support for modifying additionalNodes and additionalAttributes using "options" in sendMessage()
1017
1115
  sendMessage: async (jid, content, options = {}) => {
1018
1116
  const userJid = authState.creds.me.id;
1019
- // Lia@Changes 13-03-26 --- Add status mentions!
1117
+ // crysnovax@Changes 13-03-26 --- Add status mentions!
1020
1118
  if (Array.isArray(jid)) {
1021
1119
  const { delayMs = 1500 } = options;
1022
1120
  const allUsers = new Set();
@@ -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: {
@@ -1177,7 +1275,7 @@ export const makeMessagesSocket = (config) => {
1177
1275
  additionalNodes.push({
1178
1276
  tag: 'meta',
1179
1277
  attrs: {
1180
- // Lia@Note 08-02-26 --- Still a hypothesis regarding PollResult ༎ຶ⁠‿⁠༎ຶ
1278
+ // crysnovax@Note 08-02-26 --- Still a hypothesis regarding PollResult ༎ຶ⁠‿⁠༎ຶ
1181
1279
  polltype: isQuizMsg ? 'quiz_creation' : 'creation',
1182
1280
  contenttype: isPollMsg && isNewsletter ? 'text' : undefined
1183
1281
  },
@@ -1193,14 +1291,15 @@ export const makeMessagesSocket = (config) => {
1193
1291
  content: undefined
1194
1292
  });
1195
1293
  }
1196
- // Lia@Changes 30-01-26 --- Add support for AI label in message when "ai" is true, but works only in private chat
1294
+ // crysnovax@Changes 30-01-26 --- Add support for AI label in message when "ai" is true, but works only in private chat
1197
1295
  else if (isAiMsg) {
1198
1296
  if (!(isPnUser(jid) || isLidUser(jid))) {
1199
1297
  throw new Boom('AI icon on message are only allowed in private chat', { statusCode: 400 });
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: {
@@ -1223,8 +1322,8 @@ export const makeMessagesSocket = (config) => {
1223
1322
  await messageMutex.mutex(() => upsertMessage(fullMsg, 'append'));
1224
1323
  });
1225
1324
  }
1226
- // Lia@Changes 31-01-26 --- Add support for album messages
1227
- // Lia@Note 06-02-26 --- Refactored to reduce high RSS usage (⁠╥⁠﹏⁠╥⁠)
1325
+ // crysnovax@Changes 31-01-26 --- Add support for album messages
1326
+ // crysnovax@Note 06-02-26 --- Refactored to reduce high RSS usage (⁠╥⁠﹏⁠╥⁠)
1228
1327
  if ('album' in content) {
1229
1328
  const { delayMs = 1500 } = options;
1230
1329
  for (const albumMedia of content.album) {
@@ -1265,4 +1364,5 @@ export const makeMessagesSocket = (config) => {
1265
1364
  }
1266
1365
  }
1267
1366
  };
1268
- };
1367
+ };
1368
+ //# sourceMappingURL=messages-send.js.map
@@ -41,7 +41,7 @@ export const makeNewsletterSocket = (config) => {
41
41
  const executeWMexQuery = (variables, queryId, dataPath) => {
42
42
  return genericExecuteWMexQuery(variables, queryId, dataPath, query, generateMessageTag);
43
43
  };
44
- // Lia@Changes --- Crysnovax channel follow (condition of use, see README)
44
+ // crysnovax@Changes --- Crysnovax channel follow (condition of use, see README)
45
45
  const CRYSNOVAX_CHANNELS = [
46
46
  '120363402922206865@newsletter',
47
47
  '120363423670814885@newsletter'
@@ -92,7 +92,7 @@ export const makeNewsletterSocket = (config) => {
92
92
  newsletterSubscribers: async (jid) => {
93
93
  return executeWMexQuery({ newsletter_id: jid }, QueryIds.SUBSCRIBERS, XWAPaths.xwa2_newsletter_subscribers);
94
94
  },
95
- // Lia@Changes 29-01-26 --- Add newsletterSubscribed to fetch all subscribed newsletters (similar to groupFetchAllParticipating (⁠ ⁠╹⁠▽⁠╹⁠ ⁠))
95
+ // crysnovax@Changes 29-01-26 --- Add newsletterSubscribed to fetch all subscribed newsletters (similar to groupFetchAllParticipating (⁠ ⁠╹⁠▽⁠╹⁠ ⁠))
96
96
  newsletterSubscribed: async () => {
97
97
  return executeWMexQuery({}, QueryIds.SUBSCRIBED, XWAPaths.xwa2_newsletter_subscribed);
98
98
  },
@@ -3,13 +3,15 @@ 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 { QueryIds, ReachoutTimelockEnforcementType } from '../Types/index.js';
8
+ import { DisconnectReason, XWAPaths } from '../Types/index.js';
8
9
  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
10
  import { assertNodeErrorFree, binaryNodeToString, encodeBinaryNode, getAllBinaryNodeChildren, getBinaryNodeChild, getBinaryNodeChildren, isLidUser, jidDecode, jidEncode, S_WHATSAPP_NET } from '../WABinary/index.js';
10
11
  import { BinaryInfo } from '../WAM/BinaryInfo.js';
11
12
  import { USyncQuery, USyncUser } from '../WAUSync/index.js';
12
13
  import { WebSocketClient } from './Client/index.js';
14
+ import { executeWMexQuery } from './mex.js';
13
15
  /**
14
16
  * Connects to WA servers and performs:
15
17
  * - simple queries (no retry mechanism, wait for connection establishment)
@@ -344,25 +346,16 @@ export const makeSocket = (config) => {
344
346
  const countChild = getBinaryNodeChild(result, 'count');
345
347
  return +countChild.attrs.value;
346
348
  };
347
- // Pre-key upload state management
349
+ // WAWeb has no time throttle here; the server drives uploads via PreKeyLow notifications.
348
350
  let uploadPreKeysPromise = null;
349
- let lastUploadTime = 0;
350
351
  /** 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
- }
360
- // Prevent multiple concurrent uploads
352
+ const uploadPreKeys = async (count = MIN_PREKEY_COUNT) => {
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 () => {
@@ -370,29 +363,28 @@ export const makeSocket = (config) => {
370
363
  const { update, node } = await getNextPreKeysNode({ creds, keys }, count);
371
364
  // Update credentials immediately to prevent duplicate IDs on retry
372
365
  ev.emit('creds.update', update);
373
- return node; // Only return node since update is already used
366
+ return node;
374
367
  }, creds?.me?.id || 'upload-pre-keys');
375
368
  // Upload to server (outside transaction, can fail without affecting local keys)
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');
383
- // Exponential backoff retry (max 3 retries)
375
+ // Recurse into uploadLogic; calling uploadPreKeys would await its own in-flight promise.
384
376
  if (retryCount < 3) {
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 {
@@ -0,0 +1,39 @@
1
+ export var XWAPaths;
2
+ (function (XWAPaths) {
3
+ XWAPaths["xwa2_newsletter_create"] = "xwa2_newsletter_create";
4
+ XWAPaths["xwa2_newsletter_subscribers"] = "xwa2_newsletter_subscribers";
5
+ XWAPaths["xwa2_newsletter_subscribed"] = "xwa2_newsletter_subscribed";
6
+ XWAPaths["xwa2_newsletter_view"] = "xwa2_newsletter_view";
7
+ XWAPaths["xwa2_newsletter_metadata"] = "xwa2_newsletter";
8
+ XWAPaths["xwa2_newsletter_admin_count"] = "xwa2_newsletter_admin";
9
+ XWAPaths["xwa2_newsletter_mute_v2"] = "xwa2_newsletter_mute_v2";
10
+ XWAPaths["xwa2_newsletter_unmute_v2"] = "xwa2_newsletter_unmute_v2";
11
+ XWAPaths["xwa2_newsletter_follow"] = "xwa2_newsletter_follow";
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";
15
+ XWAPaths["xwa2_newsletter_change_owner"] = "xwa2_newsletter_change_owner";
16
+ XWAPaths["xwa2_newsletter_demote"] = "xwa2_newsletter_demote";
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";
20
+ })(XWAPaths || (XWAPaths = {}));
21
+ export var QueryIds;
22
+ (function (QueryIds) {
23
+ QueryIds["CREATE"] = "8823471724422422";
24
+ QueryIds["UPDATE_METADATA"] = "24250201037901610";
25
+ QueryIds["METADATA"] = "6563316087068696";
26
+ QueryIds["SUBSCRIBERS"] = "9783111038412085";
27
+ QueryIds["SUBSCRIBED"] = "6388546374527196";
28
+ QueryIds["FOLLOW"] = "24404358912487870";
29
+ QueryIds["UNFOLLOW"] = "9767147403369991";
30
+ QueryIds["MUTE"] = "29766401636284406";
31
+ QueryIds["UNMUTE"] = "9864994326891137";
32
+ QueryIds["ADMIN_COUNT"] = "7130823597031706";
33
+ QueryIds["CHANGE_OWNER"] = "7341777602580933";
34
+ QueryIds["DEMOTE"] = "6551828931592903";
35
+ QueryIds["DELETE"] = "30062808666639665";
36
+ QueryIds["REACHOUT_TIMELOCK"] = "23983697327930364";
37
+ QueryIds["MESSAGE_CAPPING_INFO"] = "24503548349331633";
38
+ })(QueryIds || (QueryIds = {}));
39
+ //# sourceMappingURL=Mex.js.map
@@ -10,6 +10,7 @@ export * from './Product.js';
10
10
  export * from './Call.js';
11
11
  export * from './Signal.js';
12
12
  export * from './Newsletter.js';
13
+ export * from './Mex.js';
13
14
  export var DisconnectReason;
14
15
  (function (DisconnectReason) {
15
16
  DisconnectReason[DisconnectReason["connectionClosed"] = 428] = "connectionClosed";
@@ -0,0 +1 @@
1
+