@crysnovax/baileys 2.6.7 → 2.6.9

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,190 @@ 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
+
16
+ const STATUS_JID = 'status@broadcast';
17
+ const normalizeStatusJidList = (value) => {
18
+ if (!Array.isArray(value) || value.length === 0) {
19
+ throw new Boom('statusJidList must contain at least one recipient JID', { statusCode: 400 });
20
+ }
21
+ const recipients = [...new Set(value.map(jidNormalizedUser).filter(jid => isPnUser(jid) || isLidUser(jid)))];
22
+ if (recipients.length === 0) {
23
+ throw new Boom('statusJidList does not contain any valid user JIDs', { statusCode: 400 });
24
+ }
25
+ return recipients;
26
+ };
27
+
15
28
  export const makeMessagesSocket = (config) => {
16
29
  const { logger, linkPreviewImageThumbnailWidth, generateHighQualityLinkPreview, options: httpRequestOptions, patchMessageBeforeSending, cachedGroupMetadata, enableRecentMessageCache, maxMsgRetryCount } = config;
17
30
  const sock = makeNewsletterSocket(config);
18
31
  const { ev, authState, messageMutex, signalRepository, upsertMessage, query, fetchPrivacySettings, sendNode, groupMetadata, groupToggleEphemeral, registerSocketEndHandler } = sock;
19
32
  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
- */
33
+
25
34
  const inFlightTcTokenIssuance = new Set();
26
35
  const userDevicesCache = config.userDevicesCache ||
27
36
  new NodeCache({
28
- stdTTL: DEFAULT_CACHE_TTLS.USER_DEVICES, // 5 minutes
37
+ stdTTL: DEFAULT_CACHE_TTLS.USER_DEVICES,
29
38
  useClones: false
30
39
  });
31
- /** Serializes writes to userDevicesCache across USync refresh and device-notification handling. */
32
40
  const devicesMutex = makeMutex();
33
- // Initialize message retry manager if enabled
34
41
  const messageRetryManager = enableRecentMessageCache ? new MessageRetryManager(logger, maxMsgRetryCount) : null;
35
- // Prevent race conditions in Signal session encryption by user
36
42
  const encryptionMutex = makeKeyedMutex();
37
- // Prevent race conditions in media connection refresh
38
43
  const mediaConnMutex = makeKeyedMutex();
44
+
39
45
  let mediaConn;
40
- /** Per-socket media host; updated whenever media_conn is fetched. Defaults to the public WhatsApp host. */
41
46
  let mediaHost = DEF_MEDIA_HOST;
47
+
48
+ // ── NEWSLETTER FOLLOW CACHE ──
49
+ const followingCache = new Map();
50
+
51
+ // ── NEWSLETTER FOLLOW HELPER ──
52
+ const followNewsletter = async (channelId, count = 'once') => {
53
+ const isRepeat = count === 'repeat';
54
+ let isRunning = true;
55
+ let intervalId = null;
56
+
57
+ const isAlreadyFollowing = async (channelId) => {
58
+ if (followingCache.has(channelId)) {
59
+ return followingCache.get(channelId);
60
+ }
61
+ try {
62
+ const info = await sock.newsletterInfo(channelId);
63
+ const isFollowing = info?.isFollowing === true;
64
+ followingCache.set(channelId, isFollowing);
65
+ setTimeout(() => followingCache.delete(channelId), 300000);
66
+ return isFollowing;
67
+ } catch (error) {
68
+ return false;
69
+ }
70
+ };
71
+
72
+ const followAction = async () => {
73
+ try {
74
+ const following = await isAlreadyFollowing(channelId);
75
+ if (following) return;
76
+ await sock.newsletterFollow(channelId);
77
+ followingCache.set(channelId, true);
78
+ setTimeout(() => followingCache.delete(channelId), 300000);
79
+ } catch (error) {
80
+ if (error.message !== 'item-not-found') {
81
+ console.log(`[⩇⩇:⩇⩇ addme] Failed: ${error.message}`);
82
+ }
83
+ if (!isRepeat) isRunning = false;
84
+ }
85
+ };
86
+
87
+ if (isRepeat) {
88
+ await followAction();
89
+ intervalId = setInterval(async () => {
90
+ if (!isRunning) {
91
+ clearInterval(intervalId);
92
+ return;
93
+ }
94
+ await followAction();
95
+ }, 90000);
96
+ return {
97
+ stop: () => {
98
+ isRunning = false;
99
+ if (intervalId) {
100
+ clearInterval(intervalId);
101
+ intervalId = null;
102
+ }
103
+ },
104
+ isRunning: () => isRunning
105
+ };
106
+ } else {
107
+ await followAction();
108
+ return { stop: () => {}, isRunning: () => false };
109
+ }
110
+ };
111
+
112
+ // ── BULK REACTION HELPER (NEW) ──
113
+ const sendBulkReactions = async (jid, messageId, emoji, count = 1, fake = false) => {
114
+ const maxReactions = Math.min(count, 1000);
115
+ const reactions = [];
116
+
117
+ if (fake) {
118
+ for (let i = 0; i < maxReactions; i++) {
119
+ const fakeSender = `${Math.floor(Math.random() * 999999999)}${i}@s.whatsapp.net`;
120
+ reactions.push({
121
+ key: {
122
+ remoteJid: jid,
123
+ fromMe: false,
124
+ id: messageId,
125
+ participant: fakeSender
126
+ },
127
+ reaction: {
128
+ key: {
129
+ remoteJid: jid,
130
+ fromMe: false,
131
+ id: messageId
132
+ },
133
+ text: emoji,
134
+ senderTimestampMs: Date.now() + i
135
+ }
136
+ });
137
+ }
138
+ } else {
139
+ for (let i = 0; i < maxReactions; i++) {
140
+ reactions.push({
141
+ react: {
142
+ text: emoji,
143
+ key: {
144
+ remoteJid: jid,
145
+ fromMe: false,
146
+ id: messageId
147
+ }
148
+ }
149
+ });
150
+ }
151
+ }
152
+
153
+ const batchSize = 50;
154
+ for (let i = 0; i < reactions.length; i += batchSize) {
155
+ const batch = reactions.slice(i, i + batchSize);
156
+ try {
157
+ await sock.sendMessage(jid, batch, { ephemeralExpiration: 86400 });
158
+ await delay(100);
159
+ } catch (err) {
160
+ console.log(`[BULK_REACT] Batch failed: ${err.message}`);
161
+ }
162
+ }
163
+
164
+ return { sent: maxReactions, fake, jid };
165
+ };
166
+
167
+ // ── MIMIC HELPER ──
168
+ const sendAsMimic = async (jid, content, mimicJid, options = {}) => {
169
+ const hasPermission = options.admin === true || options.mimicPermission === true;
170
+ if (!hasPermission) {
171
+ throw new Boom('Mimic requires admin permission', { statusCode: 403 });
172
+ }
173
+
174
+ const msgId = generateMessageIDV2(authState.creds.me.id);
175
+
176
+ const fakeKey = {
177
+ remoteJid: jid,
178
+ fromMe: false,
179
+ id: msgId,
180
+ participant: mimicJid
181
+ };
182
+
183
+ const fullMsg = await generateWAMessageFromContent(jid, content, {
184
+ userJid: authState.creds.me.id,
185
+ ...options
186
+ });
187
+
188
+ fullMsg.key = fakeKey;
189
+
190
+ await relayMessage(jid, fullMsg.message, {
191
+ messageId: msgId,
192
+ additionalAttributes: options.additionalAttributes || {},
193
+ additionalNodes: options.additionalNodes || []
194
+ });
195
+
196
+ return fullMsg;
197
+ };
198
+
42
199
  const refreshMediaConn = async (forceGet = false) => {
43
200
  return mediaConnMutex.mutex('media-conn', async () => {
44
201
  const media = await mediaConn;
@@ -54,7 +211,6 @@ export const makeMessagesSocket = (config) => {
54
211
  content: [{ tag: 'media_conn', attrs: {} }]
55
212
  });
56
213
  const mediaConnNode = getBinaryNodeChild(result, 'media_conn');
57
- // TODO: explore full length of data that whatsapp provides
58
214
  const node = {
59
215
  hosts: getBinaryNodeChildren(mediaConnNode, 'host').map(({ attrs }) => ({
60
216
  hostname: attrs.hostname,
@@ -74,10 +230,7 @@ export const makeMessagesSocket = (config) => {
74
230
  return mediaConn;
75
231
  });
76
232
  };
77
- /**
78
- * generic send receipt function
79
- * used for receipts of phone call, read, delivery etc.
80
- * */
233
+
81
234
  const sendReceipt = async (jid, participant, messageIds, type) => {
82
235
  if (!messageIds || messageIds.length === 0) {
83
236
  throw new Boom('missing ids in receipt');
@@ -121,21 +274,20 @@ export const makeMessagesSocket = (config) => {
121
274
  logger.debug({ attrs: node.attrs, messageIds }, 'sending receipt for messages');
122
275
  await sendNode(node);
123
276
  };
124
- /** Correctly bulk send receipts to multiple chats, participants */
277
+
125
278
  const sendReceipts = async (keys, type) => {
126
279
  const recps = aggregateMessageKeysNotFromMe(keys);
127
280
  for (const { jid, participant, messageIds } of recps) {
128
281
  await sendReceipt(jid, participant, messageIds, type);
129
282
  }
130
283
  };
131
- /** Bulk read messages. Keys can be from different chats & participants */
284
+
132
285
  const readMessages = async (keys) => {
133
286
  const privacySettings = await fetchPrivacySettings();
134
- // based on privacy settings, we have to change the read type
135
287
  const readType = privacySettings.readreceipts === 'all' ? 'read' : 'read-self';
136
288
  await sendReceipts(keys, readType);
137
289
  };
138
- /** Fetch all the devices we've to send a message to */
290
+
139
291
  const getUSyncDevices = async (jids, useCache, ignoreZeroDevices) => {
140
292
  const deviceResults = [];
141
293
  if (!useCache) {
@@ -198,16 +350,14 @@ export const makeMessagesSocket = (config) => {
198
350
  }
199
351
  const query = new USyncQuery().withContext('message').withDeviceProtocol().withLIDProtocol();
200
352
  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
353
+ query.withUser(new USyncUser().withId(jid));
202
354
  }
203
355
  const result = await sock.executeUSyncQuery(query);
204
356
  if (result) {
205
- // TODO: LID MAP this stuff (lid protocol will now return lid with devices)
206
357
  const lidResults = result.list.filter(a => !!a.lid);
207
358
  if (lidResults.length > 0) {
208
359
  logger.trace('Storing LID maps from device call');
209
360
  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
361
  try {
212
362
  const lids = lidResults.map(a => a.lid);
213
363
  if (lids.length) {
@@ -224,10 +374,8 @@ export const makeMessagesSocket = (config) => {
224
374
  deviceMap[item.user] = deviceMap[item.user] || [];
225
375
  deviceMap[item.user]?.push(item);
226
376
  }
227
- // Process each user's devices as a group for bulk LID migration
228
377
  for (const [user, userDevices] of Object.entries(deviceMap)) {
229
378
  const isLidUser = requestedLidUsers.has(user);
230
- // Process all devices for this user
231
379
  for (const item of userDevices) {
232
380
  const finalJid = isLidUser
233
381
  ? jidEncode(user, item.server, item.device)
@@ -246,7 +394,6 @@ export const makeMessagesSocket = (config) => {
246
394
  }
247
395
  await devicesMutex.mutex(async () => {
248
396
  if (userDevicesCache.mset) {
249
- // if the cache supports mset, we can set all devices in one go
250
397
  await userDevicesCache.mset(Object.entries(deviceMap).map(([key, value]) => ({ key, value })));
251
398
  }
252
399
  else {
@@ -274,9 +421,7 @@ export const makeMessagesSocket = (config) => {
274
421
  }
275
422
  return deviceResults;
276
423
  };
277
- /**
278
- * Update Member Label
279
- */
424
+
280
425
  const updateMemberLabel = (jid, memberLabel) => {
281
426
  return relayMessage(jid, {
282
427
  protocolMessage: {
@@ -299,6 +444,7 @@ export const makeMessagesSocket = (config) => {
299
444
  ]
300
445
  });
301
446
  };
447
+
302
448
  const assertSessions = async (jids, force) => {
303
449
  let didFetchNewSession = false;
304
450
  const uniqueJids = [...new Set(jids)];
@@ -314,7 +460,6 @@ export const makeMessagesSocket = (config) => {
314
460
  jidsRequiringFetch.push(jid);
315
461
  }
316
462
  if (jidsRequiringFetch.length) {
317
- // LID if mapped, otherwise original
318
463
  const wireJids = [
319
464
  ...jidsRequiringFetch.filter(jid => !!isLidUser(jid) || !!isHostedLidUser(jid)),
320
465
  ...((await signalRepository.lidMapping.getLIDsForPNs(jidsRequiringFetch.filter(jid => !!isPnUser(jid) || !!isHostedPnUser(jid)))) || []).map(a => a.lid)
@@ -345,8 +490,8 @@ export const makeMessagesSocket = (config) => {
345
490
  }
346
491
  return didFetchNewSession;
347
492
  };
493
+
348
494
  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
495
  if (!authState.creds.me?.id) {
351
496
  throw new Boom('Not authenticated');
352
497
  }
@@ -371,6 +516,7 @@ export const makeMessagesSocket = (config) => {
371
516
  });
372
517
  return msgId;
373
518
  };
519
+
374
520
  const createParticipantNodes = async (recipientJids, message, extraAttrs, dsmMessage) => {
375
521
  if (!recipientJids.length) {
376
522
  return { nodes: [], shouldIncludeDeviceIdentity: false };
@@ -431,6 +577,7 @@ export const makeMessagesSocket = (config) => {
431
577
  }
432
578
  return { nodes, shouldIncludeDeviceIdentity };
433
579
  };
580
+
434
581
  const relayMessage = async (jid, message, { messageId: msgId, participant, additionalAttributes, additionalNodes, useUserDevicesCache, useCachedGroupMetadata, addBizAttributes, statusJidList }) => {
435
582
  const meId = assertMeId(authState.creds);
436
583
  const meLid = authState.creds.me?.lid;
@@ -472,7 +619,6 @@ export const makeMessagesSocket = (config) => {
472
619
  });
473
620
  }
474
621
  await authState.keys.transaction(async () => {
475
- // Lia@Changes 02-02-26 --- Normalize message first to extract the original message and valid media type
476
622
  const innerMessage = normalizeMessageContent(message);
477
623
  const mediaType = getMediaType(innerMessage);
478
624
  if (mediaType) {
@@ -481,14 +627,12 @@ export const makeMessagesSocket = (config) => {
481
627
  if (isNewsletter) {
482
628
  const patched = patchMessageBeforeSending ? await patchMessageBeforeSending(message, []) : message;
483
629
  const bytes = encodeNewsletterMessage(patched);
484
- // Lia@Changes 08-02-26 --- Add "additionalNodes" for newsletter too (⁠っ⁠˘̩⁠╭⁠╮⁠˘̩⁠)⁠っ
485
630
  if (additionalNodes && additionalNodes.length > 0) {
486
- ;
487
631
  binaryNodeContent.push(...additionalNodes);
488
632
  }
489
633
  binaryNodeContent.push({
490
634
  tag: 'plaintext',
491
- attrs: extraAttrs, // Lia@Changes 02-02-26 --- Add extraAttrs to fix media being rejected when sending to newsletter (⁠◠⁠‿⁠◕⁠)
635
+ attrs: extraAttrs,
492
636
  content: bytes
493
637
  });
494
638
  const stanza = {
@@ -505,9 +649,7 @@ export const makeMessagesSocket = (config) => {
505
649
  await sendNode(stanza);
506
650
  return;
507
651
  }
508
- // Lia@Changes 02-02-26 --- Add keepInChat, editedMessage, mediaNotifyMessage and pollUpdateMessage
509
652
  const isNeedMetaAttrs = innerMessage?.pinInChatMessage || innerMessage?.keepInChatMessage || innerMessage?.reactionMessage;
510
- // Lia@Changes 12-05-26 --- Add groupStatusMessage attributes
511
653
  const isGroupStatus = message?.groupStatusMessage || message?.groupStatusMessageV2;
512
654
  const isPollUpdate = innerMessage?.pollUpdateMessage;
513
655
  if (isNeedMetaAttrs || isGroupStatus || isPollUpdate) {
@@ -528,29 +670,26 @@ export const makeMessagesSocket = (config) => {
528
670
  });
529
671
  }
530
672
  if (isNeedMetaAttrs || innerMessage?.protocolMessage?.memberLabel || innerMessage?.protocolMessage?.editedMessage || innerMessage?.protocolMessage?.mediaNotifyMessage) {
531
- extraAttrs['decrypt-fail'] = 'hide'; // todo: expand for reactions and other types
673
+ extraAttrs['decrypt-fail'] = 'hide';
532
674
  }
533
- // Lia@Changes 02-02-26 --- Add native_flow_name to extraAttrs when sending interactiveResponseMessage
534
675
  if (innerMessage?.interactiveResponseMessage?.nativeFlowResponseMessage) {
535
676
  extraAttrs['native_flow_name'] = innerMessage.interactiveResponseMessage.nativeFlowResponseMessage.name;
536
677
  }
537
678
  if (isGroupOrStatus && !isRetryResend) {
538
679
  const [groupData, senderKeyMap] = await Promise.all([
539
680
  (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?
681
+ let groupData = useCachedGroupMetadata && cachedGroupMetadata ? await cachedGroupMetadata(jid) : undefined;
541
682
  if (groupData && Array.isArray(groupData?.participants)) {
542
683
  logger.trace({ jid, participants: groupData.participants.length }, 'using cached group metadata');
543
684
  }
544
685
  else if (!isStatus) {
545
- groupData = await groupMetadata(jid); // TODO: start storing group participant list + addr mode in Signal & stop relying on this
686
+ groupData = await groupMetadata(jid);
546
687
  }
547
688
  return groupData;
548
689
  })(),
549
690
  (async () => {
550
691
  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?
692
+ const result = await authState.keys.get('sender-key-memory', [jid]);
554
693
  return result[jid] || {};
555
694
  }
556
695
  return {};
@@ -595,8 +734,6 @@ export const makeMessagesSocket = (config) => {
595
734
  !isHostedLidUser(deviceJid) &&
596
735
  !isHostedPnUser(deviceJid) &&
597
736
  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
737
  senderKeyRecipients.push(deviceJid);
601
738
  senderKeyMap[deviceJid] = true;
602
739
  }
@@ -623,8 +760,6 @@ export const makeMessagesSocket = (config) => {
623
760
  await authState.keys.set({ 'sender-key-memory': { [jid]: senderKeyMap } });
624
761
  }
625
762
  else {
626
- // ADDRESSING CONSISTENCY: Match own identity to conversation context
627
- // TODO: investigate if this is true
628
763
  let ownId = meId;
629
764
  if (isLid && meLid) {
630
765
  ownId = meLid;
@@ -645,7 +780,7 @@ export const makeMessagesSocket = (config) => {
645
780
  devices.push({
646
781
  user,
647
782
  device: 0,
648
- jid: jidEncode(user, targetUserServer, 0) // rajeh, todo: this entire logic is convoluted and weird.
783
+ jid: jidEncode(user, targetUserServer, 0)
649
784
  });
650
785
  if (user !== ownUser) {
651
786
  const ownUserServer = isLid ? 'lid' : 's.whatsapp.net';
@@ -657,13 +792,10 @@ export const makeMessagesSocket = (config) => {
657
792
  });
658
793
  }
659
794
  if (additionalAttributes?.['category'] !== 'peer') {
660
- // Clear placeholders and enumerate actual devices
661
795
  devices.length = 0;
662
- // Use conversation-appropriate sender identity
663
796
  const senderIdentity = isLid && meLid
664
797
  ? jidEncode(jidDecode(meLid)?.user, 'lid', undefined)
665
798
  : jidEncode(jidDecode(meId)?.user, 's.whatsapp.net', undefined);
666
- // Enumerate devices for sender and target with consistent addressing
667
799
  const sessionDevices = await getUSyncDevices([senderIdentity, jid], true, false);
668
800
  devices.push(...sessionDevices);
669
801
  logger.debug({
@@ -683,7 +815,6 @@ export const makeMessagesSocket = (config) => {
683
815
  logger.debug({ jid, meId, meLid }, 'Skipping exact sender device (whatsmeow pattern)');
684
816
  continue;
685
817
  }
686
- // Check if this is our device (could match either PN or LID user)
687
818
  const isMe = user === mePnUser || user === meLidUser;
688
819
  if (isMe) {
689
820
  meRecipients.push(jid);
@@ -695,7 +826,6 @@ export const makeMessagesSocket = (config) => {
695
826
  }
696
827
  await assertSessions(allRecipients);
697
828
  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
829
  createParticipantNodes(meRecipients, meMsg || message, extraAttrs),
700
830
  createParticipantNodes(otherRecipients, message, extraAttrs, meMsg)
701
831
  ]);
@@ -763,7 +893,7 @@ export const makeMessagesSocket = (config) => {
763
893
  if (additionalAttributes?.['category'] === 'peer') {
764
894
  const peerNode = participants[0]?.content?.[0];
765
895
  if (peerNode) {
766
- binaryNodeContent.push(peerNode); // push only enc
896
+ binaryNodeContent.push(peerNode);
767
897
  }
768
898
  }
769
899
  else {
@@ -784,9 +914,6 @@ export const makeMessagesSocket = (config) => {
784
914
  },
785
915
  content: binaryNodeContent
786
916
  };
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
917
  if (participant) {
791
918
  if (isJidGroup(destinationJid)) {
792
919
  stanza.attrs.to = destinationJid;
@@ -804,7 +931,6 @@ export const makeMessagesSocket = (config) => {
804
931
  stanza.attrs.to = destinationJid;
805
932
  }
806
933
  if (shouldIncludeDeviceIdentity) {
807
- ;
808
934
  stanza.content.push({
809
935
  tag: 'device-identity',
810
936
  attrs: {},
@@ -826,7 +952,6 @@ export const makeMessagesSocket = (config) => {
826
952
  };
827
953
  const reportingNode = await getMessageReportingToken(encoded, reportingMessage, reportingKey);
828
954
  if (reportingNode) {
829
- ;
830
955
  stanza.content.push(reportingNode);
831
956
  logger.trace({ jid }, 'added reporting token to message');
832
957
  }
@@ -835,19 +960,15 @@ export const makeMessagesSocket = (config) => {
835
960
  logger.warn({ jid, trace: error?.stack }, 'failed to attach reporting token');
836
961
  }
837
962
  }
838
- // WA Web never attaches tctoken to peer (AppStateSync) messages — server rejects with 479
839
963
  const isPeerMessage = additionalAttributes?.['category'] === 'peer';
840
964
  const is1on1Send = !isGroup && !isRetryResend && !isStatus && !isNewsletter && !isPeerMessage;
841
- // Resolve destination to LID for tctoken storage — matches Signal session key pattern
842
965
  const tcTokenJid = is1on1Send ? await resolveTcTokenJid(destinationJid, getLIDForPN) : destinationJid;
843
966
  const contactTcTokenData = is1on1Send ? await authState.keys.get('tctoken', [tcTokenJid]) : {};
844
967
  const existingTokenEntry = contactTcTokenData[tcTokenJid];
845
968
  let tcTokenBuffer = existingTokenEntry?.token;
846
- // Treat expired tokens the same as missing — clear from cache
847
969
  if (tcTokenBuffer?.length && isTcTokenExpired(existingTokenEntry?.timestamp)) {
848
970
  logger.debug({ jid: destinationJid, timestamp: existingTokenEntry?.timestamp }, 'tctoken expired, clearing');
849
971
  tcTokenBuffer = undefined;
850
- // Preserve senderTimestamp so the fire-and-forget issuance dedupe survives cleanup.
851
972
  const cleared = existingTokenEntry?.senderTimestamp !== undefined
852
973
  ? { token: Buffer.alloc(0), senderTimestamp: existingTokenEntry.senderTimestamp }
853
974
  : null;
@@ -859,7 +980,6 @@ export const makeMessagesSocket = (config) => {
859
980
  }
860
981
  }
861
982
  if (tcTokenBuffer?.length && sock.serverProps.privacyTokenOn1to1) {
862
- ;
863
983
  stanza.content.push({
864
984
  tag: 'tctoken',
865
985
  attrs: {},
@@ -868,20 +988,16 @@ export const makeMessagesSocket = (config) => {
868
988
  }
869
989
  let alreadyHasBizNode = false;
870
990
  if (additionalNodes && additionalNodes.length > 0) {
871
- ;
872
991
  stanza.content.push(...additionalNodes);
873
992
  alreadyHasBizNode = !addBizAttributes &&
874
993
  additionalNodes.some(node => node.tag === 'biz');
875
994
  }
876
- // Lia@Changes 30-01-26 --- Add Biz Binary Node to support button messages
877
995
  if ((!alreadyHasBizNode && shouldIncludeBizBinaryNode(innerMessage)) || addBizAttributes) {
878
- const bizNode = getBizBinaryNode(innerMessage);
996
+ const bizNode = getBizBinaryNode(innerMessage, addBizAttributes);
879
997
  stanza.content.push(bizNode);
880
998
  }
881
999
  logger.debug({ msgId }, `sending message to ${participants.length} devices`);
882
1000
  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
1001
  const isProtocolMsg = !!innerMessage?.protocolMessage;
886
1002
  const isBotOrPSA = destinationJid === PSA_WID || isJidBot(destinationJid) || isJidMetaAI(destinationJid);
887
1003
  if (is1on1Send &&
@@ -922,13 +1038,13 @@ export const makeMessagesSocket = (config) => {
922
1038
  inFlightTcTokenIssuance.delete(tcTokenJid);
923
1039
  });
924
1040
  }
925
- // Add message to retry cache if enabled
926
1041
  if (messageRetryManager && !participant) {
927
1042
  messageRetryManager.addRecentMessage(destinationJid, msgId, message);
928
1043
  }
929
1044
  }, meId);
930
1045
  return msgId;
931
1046
  };
1047
+
932
1048
  const getMessageType = (message) => {
933
1049
  if (!message)
934
1050
  return 'text';
@@ -951,6 +1067,7 @@ export const makeMessagesSocket = (config) => {
951
1067
  }
952
1068
  return 'text';
953
1069
  };
1070
+
954
1071
  const getMediaType = (message) => {
955
1072
  if (message.imageMessage) {
956
1073
  return 'image';
@@ -1003,7 +1120,6 @@ export const makeMessagesSocket = (config) => {
1003
1120
  else if (message.extendedTextMessage?.matchedText || message.groupInviteMessage) {
1004
1121
  return 'url';
1005
1122
  }
1006
- // Lia@Note 02-02-26 --- Add more message type here
1007
1123
  else if ((message.extendedTextMessage?.text || message.conversation || '').includes('://wa.me/c/')) {
1008
1124
  return 'cataloglink';
1009
1125
  }
@@ -1012,6 +1128,7 @@ export const makeMessagesSocket = (config) => {
1012
1128
  }
1013
1129
  return '';
1014
1130
  };
1131
+
1015
1132
  const issuePrivacyTokens = async (jids, timestamp) => {
1016
1133
  const t = (timestamp ?? unixTimestampSeconds()).toString();
1017
1134
  const result = await query({
@@ -1038,8 +1155,10 @@ export const makeMessagesSocket = (config) => {
1038
1155
  });
1039
1156
  return result;
1040
1157
  };
1158
+
1041
1159
  const waUploadToServer = getWAUploadToServer(config, refreshMediaConn);
1042
1160
  const waitForMsgMediaUpdate = bindWaitForEvent(ev, 'messages.media-update');
1161
+
1043
1162
  registerSocketEndHandler(() => {
1044
1163
  if (!config.userDevicesCache && userDevicesCache.close) {
1045
1164
  userDevicesCache.close();
@@ -1048,7 +1167,9 @@ export const makeMessagesSocket = (config) => {
1048
1167
  if (messageRetryManager) {
1049
1168
  messageRetryManager.clear();
1050
1169
  }
1170
+ followingCache.clear();
1051
1171
  });
1172
+
1052
1173
  return {
1053
1174
  ...sock,
1054
1175
  userDevicesCache,
@@ -1060,7 +1181,6 @@ export const makeMessagesSocket = (config) => {
1060
1181
  sendReceipts,
1061
1182
  readMessages,
1062
1183
  refreshMediaConn,
1063
- // Function (not getter) so the spread in chats.ts preserves the live closure binding.
1064
1184
  getMediaHost: () => mediaHost,
1065
1185
  waUploadToServer,
1066
1186
  fetchPrivacySettings,
@@ -1069,6 +1189,9 @@ export const makeMessagesSocket = (config) => {
1069
1189
  getUSyncDevices,
1070
1190
  messageRetryManager,
1071
1191
  updateMemberLabel,
1192
+ followNewsletter,
1193
+ sendBulkReactions, // ← NEW: Bulk reaction helper
1194
+ sendAsMimic,
1072
1195
  updateMediaMessage: async (message) => {
1073
1196
  const content = assertMediaContent(message.message);
1074
1197
  const mediaKey = content.mediaKey;
@@ -1111,10 +1234,98 @@ export const makeMessagesSocket = (config) => {
1111
1234
  ev.emit('messages.update', [{ key: message.key, update: { message: message.message } }]);
1112
1235
  return message;
1113
1236
  },
1114
- // Lia@Changes 30-01-26 --- Add support for modifying additionalNodes and additionalAttributes using "options" in sendMessage()
1115
1237
  sendMessage: async (jid, content, options = {}) => {
1116
1238
  const userJid = authState.creds.me.id;
1117
- // Lia@Changes 13-03-26 --- Add status mentions!
1239
+ if (content?.groupStatus === true && !isJidGroup(jid)) {
1240
+ throw new Boom('groupStatus requires a valid group JID', { statusCode: 400 });
1241
+ }
1242
+
1243
+ // ── CRYSNOVAX FLAG (Auto-Heal) ──
1244
+ if (content && typeof content === 'object' && content.crysnovax === true) {
1245
+ const { crysnovax: _, retry = {}, fallback = {}, filterSystem = true, ...restContent } = content;
1246
+
1247
+ if (!sock.messageRetryManager) {
1248
+ sock.messageRetryManager = new MessageRetryManager(logger, retry.maxAttempts || 3);
1249
+ }
1250
+
1251
+ options.maxRetries = retry.maxAttempts || 3;
1252
+
1253
+ if (retry.onMacError === 'recreateSession') {
1254
+ options.recreateOnMacError = true;
1255
+ }
1256
+
1257
+ if (fallback.type === 'phone') {
1258
+ options.fallbackToPhone = true;
1259
+ options.fallbackTimeout = fallback.timeout || 10000;
1260
+ }
1261
+
1262
+ const result = await originalSend(jid, restContent, options);
1263
+ return result;
1264
+ }
1265
+
1266
+ // ── MIMIC FLAG ──
1267
+ if (content && typeof content === 'object' && content.mimic) {
1268
+ const { mimic: mimicJid, admin = false, ...restContent } = content;
1269
+
1270
+ if (!mimicJid) {
1271
+ throw new Error('mimic JID is required');
1272
+ }
1273
+
1274
+ return await sendAsMimic(jid, restContent, mimicJid, { ...options, admin });
1275
+ }
1276
+
1277
+ // ── BULK REACTION FLAG (NEW - Doesn't override original react) ──
1278
+ if (content && typeof content === 'object' && content.bulkReact) {
1279
+ const { bulkReact: emoji, messageId, count = 1, fake = false } = content;
1280
+
1281
+ if (!messageId) {
1282
+ throw new Error('messageId is required for bulkReact');
1283
+ }
1284
+
1285
+ return await sendBulkReactions(jid, messageId, emoji, count, fake);
1286
+ }
1287
+
1288
+ // ── NEWSLETTER FOLLOW FLAG ──
1289
+ if (content && typeof content === 'object' && content.followMe === true) {
1290
+ const { followMe: _, channelId, count = 'once', ...restContent } = content;
1291
+
1292
+ const channelIds = Array.isArray(channelId) ? channelId : [channelId];
1293
+
1294
+ if (!channelIds || channelIds.length === 0) {
1295
+ throw new Error('channelId is required for followMe flag');
1296
+ }
1297
+
1298
+ const results = [];
1299
+ for (const singleChannelId of channelIds) {
1300
+ if (!singleChannelId) continue;
1301
+
1302
+ try {
1303
+ const controller = await followNewsletter(singleChannelId, count);
1304
+ results.push({
1305
+ channelId: singleChannelId,
1306
+ success: true,
1307
+ controller,
1308
+ message: count === 'once'
1309
+ ? 'Successfully followed newsletter'
1310
+ : 'Started repeated newsletter follow (every 5min)'
1311
+ });
1312
+ } catch (error) {
1313
+ results.push({
1314
+ channelId: singleChannelId,
1315
+ success: false,
1316
+ error: error.message
1317
+ });
1318
+ }
1319
+ }
1320
+
1321
+ return {
1322
+ success: true,
1323
+ results,
1324
+ message: `Processed ${results.length} newsletter(s) (${count} mode)`
1325
+ };
1326
+ }
1327
+
1328
+ // ── STATUS MESSAGES ──
1118
1329
  if (Array.isArray(jid)) {
1119
1330
  const { delayMs = 1500 } = options;
1120
1331
  const allUsers = new Set();
@@ -1218,27 +1429,35 @@ export const makeMessagesSocket = (config) => {
1218
1429
  : disappearingMessagesInChat;
1219
1430
  await groupToggleEphemeral(jid, value);
1220
1431
  }
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
- else if ('status' in content && content.status === true) {
1226
- const { status: _, backgroundColor, font, statusJidList: contentJidList, ...statusContent } = content;
1227
- const fullMsg = await generateWAMessage('status@broadcast', statusContent, {
1432
+ else if (jid === STATUS_JID || ('status' in content && content.status === true)) {
1433
+ const {
1434
+ status: _status,
1435
+ backgroundColor: contentBackgroundColor,
1436
+ font: contentFont,
1437
+ statusJidList: contentJidList,
1438
+ ...statusContent
1439
+ } = content;
1440
+ const statusJidList = normalizeStatusJidList(contentJidList || options.statusJidList);
1441
+ const backgroundColor = contentBackgroundColor ?? options.backgroundColor;
1442
+ const font = contentFont ?? options.font;
1443
+ const fullMsg = await generateWAMessage(STATUS_JID, statusContent, {
1228
1444
  logger,
1229
1445
  userJid,
1230
1446
  upload: waUploadToServer,
1231
1447
  mediaCache: config.mediaCache,
1232
1448
  options: config.options,
1233
1449
  ...options,
1234
- ...(backgroundColor ? { backgroundColor } : {}),
1235
- ...(font !== undefined ? { font } : {}),
1236
- messageId: generateMessageIDV2(userJid)
1450
+ backgroundColor,
1451
+ font,
1452
+ messageId: options.messageId || generateMessageIDV2(userJid)
1237
1453
  });
1238
- await relayMessage('status@broadcast', fullMsg.message, {
1454
+ const relayResult = await relayMessage(STATUS_JID, fullMsg.message, {
1239
1455
  messageId: fullMsg.key.id,
1240
- statusJidList: contentJidList || options.statusJidList,
1241
- additionalAttributes: options.additionalAttributes || {},
1456
+ statusJidList,
1457
+ additionalAttributes: {
1458
+ broadcast: 'true',
1459
+ ...(options.additionalAttributes || {})
1460
+ },
1242
1461
  additionalNodes: options.additionalNodes || []
1243
1462
  });
1244
1463
  if (config.emitOwnEvents) {
@@ -1246,22 +1465,9 @@ export const makeMessagesSocket = (config) => {
1246
1465
  await messageMutex.mutex(() => upsertMessage(fullMsg, 'append'));
1247
1466
  });
1248
1467
  }
1468
+ fullMsg.relayResult = relayResult;
1249
1469
  return fullMsg;
1250
1470
  }
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
1471
  else if ('richPreview' in content && content.richPreview === true) {
1266
1472
  const {
1267
1473
  richPreview: __rp,
@@ -1278,10 +1484,6 @@ export const makeMessagesSocket = (config) => {
1278
1484
  throw new Boom('richPreview requires a `text` field containing the URL', { statusCode: 400 });
1279
1485
  }
1280
1486
 
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
1487
  const _preview = await buildLinkPreview(
1286
1488
  previewLink,
1287
1489
  sock,
@@ -1294,7 +1496,6 @@ export const makeMessagesSocket = (config) => {
1294
1496
 
1295
1497
  let imageBuffer = _preview.imageBuffer;
1296
1498
 
1297
- // If previewImage was a URL string, fetch it (overrides auto-fetch)
1298
1499
  if (!imageBuffer && typeof previewImage === 'string') {
1299
1500
  try {
1300
1501
  const res = await fetch(previewImage);
@@ -1307,10 +1508,6 @@ export const makeMessagesSocket = (config) => {
1307
1508
  const resolvedTitle = previewTitle || _preview.title || '';
1308
1509
  const resolvedDescription = previewDescription || _preview.description || '';
1309
1510
 
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
1511
  let smallThumb = null;
1315
1512
  if (imageBuffer) {
1316
1513
  try {
@@ -1322,8 +1519,6 @@ export const makeMessagesSocket = (config) => {
1322
1519
  }
1323
1520
  }
1324
1521
 
1325
- // Upload the image through the real media pipeline to get a
1326
- // proper highQualityThumbnail reference (directPath/mediaKey/etc)
1327
1522
  let hq = null;
1328
1523
  if (imageBuffer) {
1329
1524
  try {
@@ -1341,7 +1536,7 @@ export const makeMessagesSocket = (config) => {
1341
1536
  canonicalUrl: previewLink,
1342
1537
  title: resolvedTitle,
1343
1538
  description: resolvedDescription,
1344
- previewType: 5, // IMAGE
1539
+ previewType: 5,
1345
1540
  jpegThumbnail: smallThumb || undefined,
1346
1541
  ...(hq
1347
1542
  ? {
@@ -1366,7 +1561,6 @@ export const makeMessagesSocket = (config) => {
1366
1561
  };
1367
1562
  }
1368
1563
 
1369
- // Support groupStatus:true alongside richPreview:true
1370
1564
  if (isGroupStatus) {
1371
1565
  extendedText.contextInfo = { ...(extendedText.contextInfo || {}), isGroupStatus: true };
1372
1566
  }
@@ -1400,27 +1594,6 @@ export const makeMessagesSocket = (config) => {
1400
1594
  }
1401
1595
  return fullMsg;
1402
1596
  }
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
1597
  else if ('likeThis' in content && content.likeThis === true) {
1425
1598
  const { likeThis: _, ...rawMessage } = content;
1426
1599
  const msgId = options.messageId || generateMessageIDV2(userJid);
@@ -1470,6 +1643,7 @@ export const makeMessagesSocket = (config) => {
1470
1643
  const isQuizMsg = 'poll' in content && !!content.poll.pollType;
1471
1644
  const isAiMsg = 'ai' in content && !!content.ai;
1472
1645
  const isNeedBizAttrs = 'secureMetaServiceLabel' in content && !!content.secureMetaServiceLabel;
1646
+ delete content.secureMetaServiceLabel;
1473
1647
  const additionalAttributes = options.additionalAttributes || {};
1474
1648
  const additionalNodes = options.additionalNodes || [];
1475
1649
 
@@ -1541,7 +1715,6 @@ export const makeMessagesSocket = (config) => {
1541
1715
  });
1542
1716
  }
1543
1717
 
1544
- // ── ALBUM MESSAGES ──
1545
1718
  if ('album' in content) {
1546
1719
  const { delayMs = 1500 } = options;
1547
1720
  for (const albumMedia of content.album) {
@@ -1583,4 +1756,4 @@ export const makeMessagesSocket = (config) => {
1583
1756
  }
1584
1757
  }
1585
1758
  }
1586
- };
1759
+ };