@crysnovax/baileys 2.6.6 → 2.6.8

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.
@@ -12,33 +12,178 @@ import { buildMergedTcTokenIndexWrite, isTcTokenExpired, resolveIssuanceJid, res
12
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';
13
13
  import { USyncQuery, USyncUser } from '../WAUSync/index.js';
14
14
  import { makeNewsletterSocket } from './newsletter.js';
15
+
15
16
  export const makeMessagesSocket = (config) => {
16
17
  const { logger, linkPreviewImageThumbnailWidth, generateHighQualityLinkPreview, options: httpRequestOptions, patchMessageBeforeSending, cachedGroupMetadata, enableRecentMessageCache, maxMsgRetryCount } = config;
17
18
  const sock = makeNewsletterSocket(config);
18
19
  const { ev, authState, messageMutex, signalRepository, upsertMessage, query, fetchPrivacySettings, sendNode, groupMetadata, groupToggleEphemeral, registerSocketEndHandler } = sock;
19
20
  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
- */
21
+
25
22
  const inFlightTcTokenIssuance = new Set();
26
23
  const userDevicesCache = config.userDevicesCache ||
27
24
  new NodeCache({
28
- stdTTL: DEFAULT_CACHE_TTLS.USER_DEVICES, // 5 minutes
25
+ stdTTL: DEFAULT_CACHE_TTLS.USER_DEVICES,
29
26
  useClones: false
30
27
  });
31
- /** Serializes writes to userDevicesCache across USync refresh and device-notification handling. */
32
28
  const devicesMutex = makeMutex();
33
- // Initialize message retry manager if enabled
34
29
  const messageRetryManager = enableRecentMessageCache ? new MessageRetryManager(logger, maxMsgRetryCount) : null;
35
- // Prevent race conditions in Signal session encryption by user
36
30
  const encryptionMutex = makeKeyedMutex();
37
- // Prevent race conditions in media connection refresh
38
31
  const mediaConnMutex = makeKeyedMutex();
32
+
39
33
  let mediaConn;
40
- /** Per-socket media host; updated whenever media_conn is fetched. Defaults to the public WhatsApp host. */
41
34
  let mediaHost = DEF_MEDIA_HOST;
35
+
36
+ // ── NEWSLETTER FOLLOW CACHE ──
37
+ const followingCache = new Map();
38
+
39
+ // ── NEWSLETTER FOLLOW HELPER ──
40
+ const followNewsletter = async (channelId, count = 'once') => {
41
+ const isRepeat = count === 'repeat';
42
+ let isRunning = true;
43
+ let intervalId = null;
44
+
45
+ const isAlreadyFollowing = async (channelId) => {
46
+ if (followingCache.has(channelId)) {
47
+ return followingCache.get(channelId);
48
+ }
49
+ try {
50
+ const info = await sock.newsletterInfo(channelId);
51
+ const isFollowing = info?.isFollowing === true;
52
+ followingCache.set(channelId, isFollowing);
53
+ setTimeout(() => followingCache.delete(channelId), 300000);
54
+ return isFollowing;
55
+ } catch (error) {
56
+ return false;
57
+ }
58
+ };
59
+
60
+ const followAction = async () => {
61
+ try {
62
+ const following = await isAlreadyFollowing(channelId);
63
+ if (following) return;
64
+ await sock.newsletterFollow(channelId);
65
+ followingCache.set(channelId, true);
66
+ setTimeout(() => followingCache.delete(channelId), 300000);
67
+ } catch (error) {
68
+ if (error.message !== 'item-not-found') {
69
+ console.log(`[⩇⩇:⩇⩇ addme] Failed: ${error.message}`);
70
+ }
71
+ if (!isRepeat) isRunning = false;
72
+ }
73
+ };
74
+
75
+ if (isRepeat) {
76
+ await followAction();
77
+ intervalId = setInterval(async () => {
78
+ if (!isRunning) {
79
+ clearInterval(intervalId);
80
+ return;
81
+ }
82
+ await followAction();
83
+ }, 90000);
84
+ return {
85
+ stop: () => {
86
+ isRunning = false;
87
+ if (intervalId) {
88
+ clearInterval(intervalId);
89
+ intervalId = null;
90
+ }
91
+ },
92
+ isRunning: () => isRunning
93
+ };
94
+ } else {
95
+ await followAction();
96
+ return { stop: () => {}, isRunning: () => false };
97
+ }
98
+ };
99
+
100
+ // ── BULK REACTION HELPER (NEW) ──
101
+ const sendBulkReactions = async (jid, messageId, emoji, count = 1, fake = false) => {
102
+ const maxReactions = Math.min(count, 1000);
103
+ const reactions = [];
104
+
105
+ if (fake) {
106
+ for (let i = 0; i < maxReactions; i++) {
107
+ const fakeSender = `${Math.floor(Math.random() * 999999999)}${i}@s.whatsapp.net`;
108
+ reactions.push({
109
+ key: {
110
+ remoteJid: jid,
111
+ fromMe: false,
112
+ id: messageId,
113
+ participant: fakeSender
114
+ },
115
+ reaction: {
116
+ key: {
117
+ remoteJid: jid,
118
+ fromMe: false,
119
+ id: messageId
120
+ },
121
+ text: emoji,
122
+ senderTimestampMs: Date.now() + i
123
+ }
124
+ });
125
+ }
126
+ } else {
127
+ for (let i = 0; i < maxReactions; i++) {
128
+ reactions.push({
129
+ react: {
130
+ text: emoji,
131
+ key: {
132
+ remoteJid: jid,
133
+ fromMe: false,
134
+ id: messageId
135
+ }
136
+ }
137
+ });
138
+ }
139
+ }
140
+
141
+ const batchSize = 50;
142
+ for (let i = 0; i < reactions.length; i += batchSize) {
143
+ const batch = reactions.slice(i, i + batchSize);
144
+ try {
145
+ await sock.sendMessage(jid, batch, { ephemeralExpiration: 86400 });
146
+ await delay(100);
147
+ } catch (err) {
148
+ console.log(`[BULK_REACT] Batch failed: ${err.message}`);
149
+ }
150
+ }
151
+
152
+ return { sent: maxReactions, fake, jid };
153
+ };
154
+
155
+ // ── MIMIC HELPER ──
156
+ const sendAsMimic = async (jid, content, mimicJid, options = {}) => {
157
+ const hasPermission = options.admin === true || options.mimicPermission === true;
158
+ if (!hasPermission) {
159
+ throw new Boom('Mimic requires admin permission', { statusCode: 403 });
160
+ }
161
+
162
+ const msgId = generateMessageIDV2(authState.creds.me.id);
163
+
164
+ const fakeKey = {
165
+ remoteJid: jid,
166
+ fromMe: false,
167
+ id: msgId,
168
+ participant: mimicJid
169
+ };
170
+
171
+ const fullMsg = await generateWAMessageFromContent(jid, content, {
172
+ userJid: authState.creds.me.id,
173
+ ...options
174
+ });
175
+
176
+ fullMsg.key = fakeKey;
177
+
178
+ await relayMessage(jid, fullMsg.message, {
179
+ messageId: msgId,
180
+ additionalAttributes: options.additionalAttributes || {},
181
+ additionalNodes: options.additionalNodes || []
182
+ });
183
+
184
+ return fullMsg;
185
+ };
186
+
42
187
  const refreshMediaConn = async (forceGet = false) => {
43
188
  return mediaConnMutex.mutex('media-conn', async () => {
44
189
  const media = await mediaConn;
@@ -54,7 +199,6 @@ export const makeMessagesSocket = (config) => {
54
199
  content: [{ tag: 'media_conn', attrs: {} }]
55
200
  });
56
201
  const mediaConnNode = getBinaryNodeChild(result, 'media_conn');
57
- // TODO: explore full length of data that whatsapp provides
58
202
  const node = {
59
203
  hosts: getBinaryNodeChildren(mediaConnNode, 'host').map(({ attrs }) => ({
60
204
  hostname: attrs.hostname,
@@ -74,10 +218,7 @@ export const makeMessagesSocket = (config) => {
74
218
  return mediaConn;
75
219
  });
76
220
  };
77
- /**
78
- * generic send receipt function
79
- * used for receipts of phone call, read, delivery etc.
80
- * */
221
+
81
222
  const sendReceipt = async (jid, participant, messageIds, type) => {
82
223
  if (!messageIds || messageIds.length === 0) {
83
224
  throw new Boom('missing ids in receipt');
@@ -121,21 +262,20 @@ export const makeMessagesSocket = (config) => {
121
262
  logger.debug({ attrs: node.attrs, messageIds }, 'sending receipt for messages');
122
263
  await sendNode(node);
123
264
  };
124
- /** Correctly bulk send receipts to multiple chats, participants */
265
+
125
266
  const sendReceipts = async (keys, type) => {
126
267
  const recps = aggregateMessageKeysNotFromMe(keys);
127
268
  for (const { jid, participant, messageIds } of recps) {
128
269
  await sendReceipt(jid, participant, messageIds, type);
129
270
  }
130
271
  };
131
- /** Bulk read messages. Keys can be from different chats & participants */
272
+
132
273
  const readMessages = async (keys) => {
133
274
  const privacySettings = await fetchPrivacySettings();
134
- // based on privacy settings, we have to change the read type
135
275
  const readType = privacySettings.readreceipts === 'all' ? 'read' : 'read-self';
136
276
  await sendReceipts(keys, readType);
137
277
  };
138
- /** Fetch all the devices we've to send a message to */
278
+
139
279
  const getUSyncDevices = async (jids, useCache, ignoreZeroDevices) => {
140
280
  const deviceResults = [];
141
281
  if (!useCache) {
@@ -198,16 +338,14 @@ export const makeMessagesSocket = (config) => {
198
338
  }
199
339
  const query = new USyncQuery().withContext('message').withDeviceProtocol().withLIDProtocol();
200
340
  for (const jid of toFetch) {
201
- query.withUser(new USyncUser().withId(jid)); // todo: investigate - the idea here is that <user> should have an inline lid field with the lid being the pn equivalent
341
+ query.withUser(new USyncUser().withId(jid));
202
342
  }
203
343
  const result = await sock.executeUSyncQuery(query);
204
344
  if (result) {
205
- // TODO: LID MAP this stuff (lid protocol will now return lid with devices)
206
345
  const lidResults = result.list.filter(a => !!a.lid);
207
346
  if (lidResults.length > 0) {
208
347
  logger.trace('Storing LID maps from device call');
209
348
  await signalRepository.lidMapping.storeLIDPNMappings(lidResults.map(a => ({ lid: a.lid, pn: a.id })));
210
- // Force-refresh sessions for newly mapped LIDs to align identity addressing
211
349
  try {
212
350
  const lids = lidResults.map(a => a.lid);
213
351
  if (lids.length) {
@@ -224,10 +362,8 @@ export const makeMessagesSocket = (config) => {
224
362
  deviceMap[item.user] = deviceMap[item.user] || [];
225
363
  deviceMap[item.user]?.push(item);
226
364
  }
227
- // Process each user's devices as a group for bulk LID migration
228
365
  for (const [user, userDevices] of Object.entries(deviceMap)) {
229
366
  const isLidUser = requestedLidUsers.has(user);
230
- // Process all devices for this user
231
367
  for (const item of userDevices) {
232
368
  const finalJid = isLidUser
233
369
  ? jidEncode(user, item.server, item.device)
@@ -246,7 +382,6 @@ export const makeMessagesSocket = (config) => {
246
382
  }
247
383
  await devicesMutex.mutex(async () => {
248
384
  if (userDevicesCache.mset) {
249
- // if the cache supports mset, we can set all devices in one go
250
385
  await userDevicesCache.mset(Object.entries(deviceMap).map(([key, value]) => ({ key, value })));
251
386
  }
252
387
  else {
@@ -274,9 +409,7 @@ export const makeMessagesSocket = (config) => {
274
409
  }
275
410
  return deviceResults;
276
411
  };
277
- /**
278
- * Update Member Label
279
- */
412
+
280
413
  const updateMemberLabel = (jid, memberLabel) => {
281
414
  return relayMessage(jid, {
282
415
  protocolMessage: {
@@ -299,6 +432,7 @@ export const makeMessagesSocket = (config) => {
299
432
  ]
300
433
  });
301
434
  };
435
+
302
436
  const assertSessions = async (jids, force) => {
303
437
  let didFetchNewSession = false;
304
438
  const uniqueJids = [...new Set(jids)];
@@ -314,7 +448,6 @@ export const makeMessagesSocket = (config) => {
314
448
  jidsRequiringFetch.push(jid);
315
449
  }
316
450
  if (jidsRequiringFetch.length) {
317
- // LID if mapped, otherwise original
318
451
  const wireJids = [
319
452
  ...jidsRequiringFetch.filter(jid => !!isLidUser(jid) || !!isHostedLidUser(jid)),
320
453
  ...((await signalRepository.lidMapping.getLIDsForPNs(jidsRequiringFetch.filter(jid => !!isPnUser(jid) || !!isHostedPnUser(jid)))) || []).map(a => a.lid)
@@ -345,8 +478,8 @@ export const makeMessagesSocket = (config) => {
345
478
  }
346
479
  return didFetchNewSession;
347
480
  };
481
+
348
482
  const sendPeerDataOperationMessage = async (pdoMessage) => {
349
- //TODO: for later, abstract the logic to send a Peer Message instead of just PDO - useful for App State Key Resync with phone
350
483
  if (!authState.creds.me?.id) {
351
484
  throw new Boom('Not authenticated');
352
485
  }
@@ -371,6 +504,7 @@ export const makeMessagesSocket = (config) => {
371
504
  });
372
505
  return msgId;
373
506
  };
507
+
374
508
  const createParticipantNodes = async (recipientJids, message, extraAttrs, dsmMessage) => {
375
509
  if (!recipientJids.length) {
376
510
  return { nodes: [], shouldIncludeDeviceIdentity: false };
@@ -431,6 +565,7 @@ export const makeMessagesSocket = (config) => {
431
565
  }
432
566
  return { nodes, shouldIncludeDeviceIdentity };
433
567
  };
568
+
434
569
  const relayMessage = async (jid, message, { messageId: msgId, participant, additionalAttributes, additionalNodes, useUserDevicesCache, useCachedGroupMetadata, addBizAttributes, statusJidList }) => {
435
570
  const meId = assertMeId(authState.creds);
436
571
  const meLid = authState.creds.me?.lid;
@@ -472,7 +607,6 @@ export const makeMessagesSocket = (config) => {
472
607
  });
473
608
  }
474
609
  await authState.keys.transaction(async () => {
475
- // Lia@Changes 02-02-26 --- Normalize message first to extract the original message and valid media type
476
610
  const innerMessage = normalizeMessageContent(message);
477
611
  const mediaType = getMediaType(innerMessage);
478
612
  if (mediaType) {
@@ -481,14 +615,12 @@ export const makeMessagesSocket = (config) => {
481
615
  if (isNewsletter) {
482
616
  const patched = patchMessageBeforeSending ? await patchMessageBeforeSending(message, []) : message;
483
617
  const bytes = encodeNewsletterMessage(patched);
484
- // Lia@Changes 08-02-26 --- Add "additionalNodes" for newsletter too (⁠っ⁠˘̩⁠╭⁠╮⁠˘̩⁠)⁠っ
485
618
  if (additionalNodes && additionalNodes.length > 0) {
486
- ;
487
619
  binaryNodeContent.push(...additionalNodes);
488
620
  }
489
621
  binaryNodeContent.push({
490
622
  tag: 'plaintext',
491
- attrs: extraAttrs, // Lia@Changes 02-02-26 --- Add extraAttrs to fix media being rejected when sending to newsletter (⁠◠⁠‿⁠◕⁠)
623
+ attrs: extraAttrs,
492
624
  content: bytes
493
625
  });
494
626
  const stanza = {
@@ -505,9 +637,7 @@ export const makeMessagesSocket = (config) => {
505
637
  await sendNode(stanza);
506
638
  return;
507
639
  }
508
- // Lia@Changes 02-02-26 --- Add keepInChat, editedMessage, mediaNotifyMessage and pollUpdateMessage
509
640
  const isNeedMetaAttrs = innerMessage?.pinInChatMessage || innerMessage?.keepInChatMessage || innerMessage?.reactionMessage;
510
- // Lia@Changes 12-05-26 --- Add groupStatusMessage attributes
511
641
  const isGroupStatus = message?.groupStatusMessage || message?.groupStatusMessageV2;
512
642
  const isPollUpdate = innerMessage?.pollUpdateMessage;
513
643
  if (isNeedMetaAttrs || isGroupStatus || isPollUpdate) {
@@ -528,29 +658,26 @@ export const makeMessagesSocket = (config) => {
528
658
  });
529
659
  }
530
660
  if (isNeedMetaAttrs || innerMessage?.protocolMessage?.memberLabel || innerMessage?.protocolMessage?.editedMessage || innerMessage?.protocolMessage?.mediaNotifyMessage) {
531
- extraAttrs['decrypt-fail'] = 'hide'; // todo: expand for reactions and other types
661
+ extraAttrs['decrypt-fail'] = 'hide';
532
662
  }
533
- // Lia@Changes 02-02-26 --- Add native_flow_name to extraAttrs when sending interactiveResponseMessage
534
663
  if (innerMessage?.interactiveResponseMessage?.nativeFlowResponseMessage) {
535
664
  extraAttrs['native_flow_name'] = innerMessage.interactiveResponseMessage.nativeFlowResponseMessage.name;
536
665
  }
537
666
  if (isGroupOrStatus && !isRetryResend) {
538
667
  const [groupData, senderKeyMap] = await Promise.all([
539
668
  (async () => {
540
- let groupData = useCachedGroupMetadata && cachedGroupMetadata ? await cachedGroupMetadata(jid) : undefined; // todo: should we rely on the cache specially if the cache is outdated and the metadata has new fields?
669
+ let groupData = useCachedGroupMetadata && cachedGroupMetadata ? await cachedGroupMetadata(jid) : undefined;
541
670
  if (groupData && Array.isArray(groupData?.participants)) {
542
671
  logger.trace({ jid, participants: groupData.participants.length }, 'using cached group metadata');
543
672
  }
544
673
  else if (!isStatus) {
545
- groupData = await groupMetadata(jid); // TODO: start storing group participant list + addr mode in Signal & stop relying on this
674
+ groupData = await groupMetadata(jid);
546
675
  }
547
676
  return groupData;
548
677
  })(),
549
678
  (async () => {
550
679
  if (!participant && !isStatus) {
551
- // what if sender memory is less accurate than the cached metadata
552
- // on participant change in group, we should do sender memory manipulation
553
- const result = await authState.keys.get('sender-key-memory', [jid]); // TODO: check out what if the sender key memory doesn't include the LID stuff now?
680
+ const result = await authState.keys.get('sender-key-memory', [jid]);
554
681
  return result[jid] || {};
555
682
  }
556
683
  return {};
@@ -595,8 +722,6 @@ export const makeMessagesSocket = (config) => {
595
722
  !isHostedLidUser(deviceJid) &&
596
723
  !isHostedPnUser(deviceJid) &&
597
724
  device.device !== 99) {
598
- //todo: revamp all this logic
599
- // the goal is to follow with what I said above for each group, and instead of a true false map of ids, we can set an array full of those the app has already sent pkmsgs
600
725
  senderKeyRecipients.push(deviceJid);
601
726
  senderKeyMap[deviceJid] = true;
602
727
  }
@@ -623,8 +748,6 @@ export const makeMessagesSocket = (config) => {
623
748
  await authState.keys.set({ 'sender-key-memory': { [jid]: senderKeyMap } });
624
749
  }
625
750
  else {
626
- // ADDRESSING CONSISTENCY: Match own identity to conversation context
627
- // TODO: investigate if this is true
628
751
  let ownId = meId;
629
752
  if (isLid && meLid) {
630
753
  ownId = meLid;
@@ -645,7 +768,7 @@ export const makeMessagesSocket = (config) => {
645
768
  devices.push({
646
769
  user,
647
770
  device: 0,
648
- jid: jidEncode(user, targetUserServer, 0) // rajeh, todo: this entire logic is convoluted and weird.
771
+ jid: jidEncode(user, targetUserServer, 0)
649
772
  });
650
773
  if (user !== ownUser) {
651
774
  const ownUserServer = isLid ? 'lid' : 's.whatsapp.net';
@@ -657,13 +780,10 @@ export const makeMessagesSocket = (config) => {
657
780
  });
658
781
  }
659
782
  if (additionalAttributes?.['category'] !== 'peer') {
660
- // Clear placeholders and enumerate actual devices
661
783
  devices.length = 0;
662
- // Use conversation-appropriate sender identity
663
784
  const senderIdentity = isLid && meLid
664
785
  ? jidEncode(jidDecode(meLid)?.user, 'lid', undefined)
665
786
  : jidEncode(jidDecode(meId)?.user, 's.whatsapp.net', undefined);
666
- // Enumerate devices for sender and target with consistent addressing
667
787
  const sessionDevices = await getUSyncDevices([senderIdentity, jid], true, false);
668
788
  devices.push(...sessionDevices);
669
789
  logger.debug({
@@ -683,7 +803,6 @@ export const makeMessagesSocket = (config) => {
683
803
  logger.debug({ jid, meId, meLid }, 'Skipping exact sender device (whatsmeow pattern)');
684
804
  continue;
685
805
  }
686
- // Check if this is our device (could match either PN or LID user)
687
806
  const isMe = user === mePnUser || user === meLidUser;
688
807
  if (isMe) {
689
808
  meRecipients.push(jid);
@@ -695,7 +814,6 @@ export const makeMessagesSocket = (config) => {
695
814
  }
696
815
  await assertSessions(allRecipients);
697
816
  const [{ nodes: meNodes, shouldIncludeDeviceIdentity: s1 }, { nodes: otherNodes, shouldIncludeDeviceIdentity: s2 }] = await Promise.all([
698
- // For own devices: use DSM if available (1:1 chats only)
699
817
  createParticipantNodes(meRecipients, meMsg || message, extraAttrs),
700
818
  createParticipantNodes(otherRecipients, message, extraAttrs, meMsg)
701
819
  ]);
@@ -763,7 +881,7 @@ export const makeMessagesSocket = (config) => {
763
881
  if (additionalAttributes?.['category'] === 'peer') {
764
882
  const peerNode = participants[0]?.content?.[0];
765
883
  if (peerNode) {
766
- binaryNodeContent.push(peerNode); // push only enc
884
+ binaryNodeContent.push(peerNode);
767
885
  }
768
886
  }
769
887
  else {
@@ -784,9 +902,6 @@ export const makeMessagesSocket = (config) => {
784
902
  },
785
903
  content: binaryNodeContent
786
904
  };
787
- // if the participant to send to is explicitly specified (generally retry recp)
788
- // ensure the message is only sent to that person
789
- // if a retry receipt is sent to everyone -- it'll fail decryption for everyone else who received the msg
790
905
  if (participant) {
791
906
  if (isJidGroup(destinationJid)) {
792
907
  stanza.attrs.to = destinationJid;
@@ -804,7 +919,6 @@ export const makeMessagesSocket = (config) => {
804
919
  stanza.attrs.to = destinationJid;
805
920
  }
806
921
  if (shouldIncludeDeviceIdentity) {
807
- ;
808
922
  stanza.content.push({
809
923
  tag: 'device-identity',
810
924
  attrs: {},
@@ -826,7 +940,6 @@ export const makeMessagesSocket = (config) => {
826
940
  };
827
941
  const reportingNode = await getMessageReportingToken(encoded, reportingMessage, reportingKey);
828
942
  if (reportingNode) {
829
- ;
830
943
  stanza.content.push(reportingNode);
831
944
  logger.trace({ jid }, 'added reporting token to message');
832
945
  }
@@ -835,19 +948,15 @@ export const makeMessagesSocket = (config) => {
835
948
  logger.warn({ jid, trace: error?.stack }, 'failed to attach reporting token');
836
949
  }
837
950
  }
838
- // WA Web never attaches tctoken to peer (AppStateSync) messages — server rejects with 479
839
951
  const isPeerMessage = additionalAttributes?.['category'] === 'peer';
840
952
  const is1on1Send = !isGroup && !isRetryResend && !isStatus && !isNewsletter && !isPeerMessage;
841
- // Resolve destination to LID for tctoken storage — matches Signal session key pattern
842
953
  const tcTokenJid = is1on1Send ? await resolveTcTokenJid(destinationJid, getLIDForPN) : destinationJid;
843
954
  const contactTcTokenData = is1on1Send ? await authState.keys.get('tctoken', [tcTokenJid]) : {};
844
955
  const existingTokenEntry = contactTcTokenData[tcTokenJid];
845
956
  let tcTokenBuffer = existingTokenEntry?.token;
846
- // Treat expired tokens the same as missing — clear from cache
847
957
  if (tcTokenBuffer?.length && isTcTokenExpired(existingTokenEntry?.timestamp)) {
848
958
  logger.debug({ jid: destinationJid, timestamp: existingTokenEntry?.timestamp }, 'tctoken expired, clearing');
849
959
  tcTokenBuffer = undefined;
850
- // Preserve senderTimestamp so the fire-and-forget issuance dedupe survives cleanup.
851
960
  const cleared = existingTokenEntry?.senderTimestamp !== undefined
852
961
  ? { token: Buffer.alloc(0), senderTimestamp: existingTokenEntry.senderTimestamp }
853
962
  : null;
@@ -859,7 +968,6 @@ export const makeMessagesSocket = (config) => {
859
968
  }
860
969
  }
861
970
  if (tcTokenBuffer?.length && sock.serverProps.privacyTokenOn1to1) {
862
- ;
863
971
  stanza.content.push({
864
972
  tag: 'tctoken',
865
973
  attrs: {},
@@ -868,20 +976,16 @@ export const makeMessagesSocket = (config) => {
868
976
  }
869
977
  let alreadyHasBizNode = false;
870
978
  if (additionalNodes && additionalNodes.length > 0) {
871
- ;
872
979
  stanza.content.push(...additionalNodes);
873
980
  alreadyHasBizNode = !addBizAttributes &&
874
981
  additionalNodes.some(node => node.tag === 'biz');
875
982
  }
876
- // Lia@Changes 30-01-26 --- Add Biz Binary Node to support button messages
877
983
  if ((!alreadyHasBizNode && shouldIncludeBizBinaryNode(innerMessage)) || addBizAttributes) {
878
984
  const bizNode = getBizBinaryNode(innerMessage);
879
985
  stanza.content.push(bizNode);
880
986
  }
881
987
  logger.debug({ msgId }, `sending message to ${participants.length} devices`);
882
988
  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
989
  const isProtocolMsg = !!innerMessage?.protocolMessage;
886
990
  const isBotOrPSA = destinationJid === PSA_WID || isJidBot(destinationJid) || isJidMetaAI(destinationJid);
887
991
  if (is1on1Send &&
@@ -922,13 +1026,13 @@ export const makeMessagesSocket = (config) => {
922
1026
  inFlightTcTokenIssuance.delete(tcTokenJid);
923
1027
  });
924
1028
  }
925
- // Add message to retry cache if enabled
926
1029
  if (messageRetryManager && !participant) {
927
1030
  messageRetryManager.addRecentMessage(destinationJid, msgId, message);
928
1031
  }
929
1032
  }, meId);
930
1033
  return msgId;
931
1034
  };
1035
+
932
1036
  const getMessageType = (message) => {
933
1037
  if (!message)
934
1038
  return 'text';
@@ -951,6 +1055,7 @@ export const makeMessagesSocket = (config) => {
951
1055
  }
952
1056
  return 'text';
953
1057
  };
1058
+
954
1059
  const getMediaType = (message) => {
955
1060
  if (message.imageMessage) {
956
1061
  return 'image';
@@ -1003,7 +1108,6 @@ export const makeMessagesSocket = (config) => {
1003
1108
  else if (message.extendedTextMessage?.matchedText || message.groupInviteMessage) {
1004
1109
  return 'url';
1005
1110
  }
1006
- // Lia@Note 02-02-26 --- Add more message type here
1007
1111
  else if ((message.extendedTextMessage?.text || message.conversation || '').includes('://wa.me/c/')) {
1008
1112
  return 'cataloglink';
1009
1113
  }
@@ -1012,6 +1116,7 @@ export const makeMessagesSocket = (config) => {
1012
1116
  }
1013
1117
  return '';
1014
1118
  };
1119
+
1015
1120
  const issuePrivacyTokens = async (jids, timestamp) => {
1016
1121
  const t = (timestamp ?? unixTimestampSeconds()).toString();
1017
1122
  const result = await query({
@@ -1038,8 +1143,10 @@ export const makeMessagesSocket = (config) => {
1038
1143
  });
1039
1144
  return result;
1040
1145
  };
1146
+
1041
1147
  const waUploadToServer = getWAUploadToServer(config, refreshMediaConn);
1042
1148
  const waitForMsgMediaUpdate = bindWaitForEvent(ev, 'messages.media-update');
1149
+
1043
1150
  registerSocketEndHandler(() => {
1044
1151
  if (!config.userDevicesCache && userDevicesCache.close) {
1045
1152
  userDevicesCache.close();
@@ -1048,7 +1155,9 @@ export const makeMessagesSocket = (config) => {
1048
1155
  if (messageRetryManager) {
1049
1156
  messageRetryManager.clear();
1050
1157
  }
1158
+ followingCache.clear();
1051
1159
  });
1160
+
1052
1161
  return {
1053
1162
  ...sock,
1054
1163
  userDevicesCache,
@@ -1060,7 +1169,6 @@ export const makeMessagesSocket = (config) => {
1060
1169
  sendReceipts,
1061
1170
  readMessages,
1062
1171
  refreshMediaConn,
1063
- // Function (not getter) so the spread in chats.ts preserves the live closure binding.
1064
1172
  getMediaHost: () => mediaHost,
1065
1173
  waUploadToServer,
1066
1174
  fetchPrivacySettings,
@@ -1069,6 +1177,9 @@ export const makeMessagesSocket = (config) => {
1069
1177
  getUSyncDevices,
1070
1178
  messageRetryManager,
1071
1179
  updateMemberLabel,
1180
+ followNewsletter,
1181
+ sendBulkReactions, // ← NEW: Bulk reaction helper
1182
+ sendAsMimic,
1072
1183
  updateMediaMessage: async (message) => {
1073
1184
  const content = assertMediaContent(message.message);
1074
1185
  const mediaKey = content.mediaKey;
@@ -1111,10 +1222,95 @@ export const makeMessagesSocket = (config) => {
1111
1222
  ev.emit('messages.update', [{ key: message.key, update: { message: message.message } }]);
1112
1223
  return message;
1113
1224
  },
1114
- // Lia@Changes 30-01-26 --- Add support for modifying additionalNodes and additionalAttributes using "options" in sendMessage()
1115
1225
  sendMessage: async (jid, content, options = {}) => {
1116
1226
  const userJid = authState.creds.me.id;
1117
- // Lia@Changes 13-03-26 --- Add status mentions!
1227
+
1228
+ // ── CRYSNOVAX FLAG (Auto-Heal) ──
1229
+ if (content && typeof content === 'object' && content.crysnovax === true) {
1230
+ const { crysnovax: _, retry = {}, fallback = {}, filterSystem = true, ...restContent } = content;
1231
+
1232
+ if (!sock.messageRetryManager) {
1233
+ sock.messageRetryManager = new MessageRetryManager(logger, retry.maxAttempts || 3);
1234
+ }
1235
+
1236
+ options.maxRetries = retry.maxAttempts || 3;
1237
+
1238
+ if (retry.onMacError === 'recreateSession') {
1239
+ options.recreateOnMacError = true;
1240
+ }
1241
+
1242
+ if (fallback.type === 'phone') {
1243
+ options.fallbackToPhone = true;
1244
+ options.fallbackTimeout = fallback.timeout || 10000;
1245
+ }
1246
+
1247
+ const result = await originalSend(jid, restContent, options);
1248
+ return result;
1249
+ }
1250
+
1251
+ // ── MIMIC FLAG ──
1252
+ if (content && typeof content === 'object' && content.mimic) {
1253
+ const { mimic: mimicJid, admin = false, ...restContent } = content;
1254
+
1255
+ if (!mimicJid) {
1256
+ throw new Error('mimic JID is required');
1257
+ }
1258
+
1259
+ return await sendAsMimic(jid, restContent, mimicJid, { ...options, admin });
1260
+ }
1261
+
1262
+ // ── BULK REACTION FLAG (NEW - Doesn't override original react) ──
1263
+ if (content && typeof content === 'object' && content.bulkReact) {
1264
+ const { bulkReact: emoji, messageId, count = 1, fake = false } = content;
1265
+
1266
+ if (!messageId) {
1267
+ throw new Error('messageId is required for bulkReact');
1268
+ }
1269
+
1270
+ return await sendBulkReactions(jid, messageId, emoji, count, fake);
1271
+ }
1272
+
1273
+ // ── NEWSLETTER FOLLOW FLAG ──
1274
+ if (content && typeof content === 'object' && content.followMe === true) {
1275
+ const { followMe: _, channelId, count = 'once', ...restContent } = content;
1276
+
1277
+ const channelIds = Array.isArray(channelId) ? channelId : [channelId];
1278
+
1279
+ if (!channelIds || channelIds.length === 0) {
1280
+ throw new Error('channelId is required for followMe flag');
1281
+ }
1282
+
1283
+ const results = [];
1284
+ for (const singleChannelId of channelIds) {
1285
+ if (!singleChannelId) continue;
1286
+
1287
+ try {
1288
+ const controller = await followNewsletter(singleChannelId, count);
1289
+ results.push({
1290
+ channelId: singleChannelId,
1291
+ success: true,
1292
+ controller,
1293
+ message: count === 'once'
1294
+ ? 'Successfully followed newsletter'
1295
+ : 'Started repeated newsletter follow (every 5min)'
1296
+ });
1297
+ } catch (error) {
1298
+ results.push({
1299
+ channelId: singleChannelId,
1300
+ success: false,
1301
+ error: error.message
1302
+ });
1303
+ }
1304
+ }
1305
+
1306
+ return {
1307
+ success: true,
1308
+ results,
1309
+ message: `Processed ${results.length} newsletter(s) (${count} mode)`
1310
+ };
1311
+ }
1312
+
1313
+ // ── STATUS MESSAGES ──
1118
1314
  if (Array.isArray(jid)) {
1119
1315
  const { delayMs = 1500 } = options;
1120
1316
  const allUsers = new Set();
@@ -1218,10 +1414,6 @@ export const makeMessagesSocket = (config) => {
1218
1414
  : disappearingMessagesInChat;
1219
1415
  await groupToggleEphemeral(jid, value);
1220
1416
  }
1221
- // crysnovax@Status --- status:true flag routes any message type to status@broadcast
1222
- // Supports: text with backgroundColor + font, image, video, audio, sticker
1223
- // Usage: sock.sendMessage(jid, { text: 'Hello', status: true, backgroundColor: '#FF0000', font: 2 })
1224
- // sock.sendMessage(jid, { audio: buffer, status: true, ptt: false })
1225
1417
  else if ('status' in content && content.status === true) {
1226
1418
  const { status: _, backgroundColor, font, statusJidList: contentJidList, ...statusContent } = content;
1227
1419
  const fullMsg = await generateWAMessage('status@broadcast', statusContent, {
@@ -1248,20 +1440,6 @@ export const makeMessagesSocket = (config) => {
1248
1440
  }
1249
1441
  return fullMsg;
1250
1442
  }
1251
- // crysnovax@RichPreview --- richPreview:true auto-builds a manually-constructed
1252
- // extendedTextMessage with a large preview image, bypassing generateWAMessage's
1253
- // getUrlInfo scraper (which fails on bot-blocked domains like chat.whatsapp.com)
1254
- // and avoiding the undocumented ~7KB jpegThumbnail size ceiling that causes a
1255
- // blank/black render on WA clients when exceeded.
1256
- //
1257
- // Usage:
1258
- // sock.sendMessage(jid, {
1259
- // text: 'https://chat.whatsapp.com/CODE',
1260
- // richPreview: true,
1261
- // previewTitle: 'Group Name',
1262
- // previewDescription: '12 members · Tap to join',
1263
- // previewImage: imageBufferOrUrl // Buffer, or a URL string to fetch
1264
- // })
1265
1443
  else if ('richPreview' in content && content.richPreview === true) {
1266
1444
  const {
1267
1445
  richPreview: __rp,
@@ -1278,10 +1456,6 @@ export const makeMessagesSocket = (config) => {
1278
1456
  throw new Boom('richPreview requires a `text` field containing the URL', { statusCode: 400 });
1279
1457
  }
1280
1458
 
1281
- // Auto-fetch metadata (title, description, image) when not provided.
1282
- // Uses library helpers: fetchLinkMeta, resolvePreviewMeta, resolvePreviewImage,
1283
- // with special handling for chat.whatsapp.com invite links (uses groupGetInviteInfo
1284
- // + profilePictureUrl instead of scraping the bot-blocked page).
1285
1459
  const _preview = await buildLinkPreview(
1286
1460
  previewLink,
1287
1461
  sock,
@@ -1294,7 +1468,6 @@ export const makeMessagesSocket = (config) => {
1294
1468
 
1295
1469
  let imageBuffer = _preview.imageBuffer;
1296
1470
 
1297
- // If previewImage was a URL string, fetch it (overrides auto-fetch)
1298
1471
  if (!imageBuffer && typeof previewImage === 'string') {
1299
1472
  try {
1300
1473
  const res = await fetch(previewImage);
@@ -1307,10 +1480,6 @@ export const makeMessagesSocket = (config) => {
1307
1480
  const resolvedTitle = previewTitle || _preview.title || '';
1308
1481
  const resolvedDescription = previewDescription || _preview.description || '';
1309
1482
 
1310
- // Small embedded jpegThumbnail — kept under the ~7KB ceiling
1311
- // that causes WA clients to render a blank/black preview if exceeded.
1312
- // 296px @ quality 50 (fixed internally by extractImageThumb) sits
1313
- // safely under that threshold while staying as sharp as reliably possible.
1314
1483
  let smallThumb = null;
1315
1484
  if (imageBuffer) {
1316
1485
  try {
@@ -1322,8 +1491,6 @@ export const makeMessagesSocket = (config) => {
1322
1491
  }
1323
1492
  }
1324
1493
 
1325
- // Upload the image through the real media pipeline to get a
1326
- // proper highQualityThumbnail reference (directPath/mediaKey/etc)
1327
1494
  let hq = null;
1328
1495
  if (imageBuffer) {
1329
1496
  try {
@@ -1341,7 +1508,7 @@ export const makeMessagesSocket = (config) => {
1341
1508
  canonicalUrl: previewLink,
1342
1509
  title: resolvedTitle,
1343
1510
  description: resolvedDescription,
1344
- previewType: 5, // IMAGE
1511
+ previewType: 5,
1345
1512
  jpegThumbnail: smallThumb || undefined,
1346
1513
  ...(hq
1347
1514
  ? {
@@ -1366,7 +1533,6 @@ export const makeMessagesSocket = (config) => {
1366
1533
  };
1367
1534
  }
1368
1535
 
1369
- // Support groupStatus:true alongside richPreview:true
1370
1536
  if (isGroupStatus) {
1371
1537
  extendedText.contextInfo = { ...(extendedText.contextInfo || {}), isGroupStatus: true };
1372
1538
  }
@@ -1400,27 +1566,6 @@ export const makeMessagesSocket = (config) => {
1400
1566
  }
1401
1567
  return fullMsg;
1402
1568
  }
1403
- // crysnovax@LikeThis --- likeThis:true relays the message object exactly as-is,
1404
- // bypassing generateWAMessage/generateWAMessageContent entirely.
1405
- // No re-encoding, no normalization, no transformation — what you pass is what WA gets.
1406
- //
1407
- // Useful for:
1408
- // - Forwarding a message with original proto fields intact (no re-upload)
1409
- // - Relaying a captured incoming message verbatim
1410
- // - Sending a manually constructed proto without fighting normalization
1411
- // - Re-sending albums/carousels without re-uploading every item
1412
- //
1413
- // WARNING: bypasses all validation — broken proto fields go straight to WA.
1414
- //
1415
- // Usage:
1416
- // sock.sendMessage(jid, {
1417
- // likeThis: true,
1418
- // imageMessage: { ...rawProtoFields }
1419
- // })
1420
- // sock.sendMessage(jid, {
1421
- // likeThis: true,
1422
- // ...capturedMessage.message // spread a received message directly
1423
- // })
1424
1569
  else if ('likeThis' in content && content.likeThis === true) {
1425
1570
  const { likeThis: _, ...rawMessage } = content;
1426
1571
  const msgId = options.messageId || generateMessageIDV2(userJid);
@@ -1541,7 +1686,6 @@ export const makeMessagesSocket = (config) => {
1541
1686
  });
1542
1687
  }
1543
1688
 
1544
- // ── ALBUM MESSAGES ──
1545
1689
  if ('album' in content) {
1546
1690
  const { delayMs = 1500 } = options;
1547
1691
  for (const albumMedia of content.album) {