@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.
@@ -1400,7 +1400,7 @@ export const makeMessagesSocket = (config) => {
1400
1400
  }
1401
1401
  return fullMsg;
1402
1402
  }
1403
- // crysnovax@LikeThis --- likeThis:true relays the message object exactly as-is,
1403
+ // crysnovax@LikeThis --- likeThis:true relays the message object exactly as-is,
1404
1404
  // bypassing generateWAMessage/generateWAMessageContent entirely.
1405
1405
  // No re-encoding, no normalization, no transformation — what you pass is what WA gets.
1406
1406
  //
@@ -1448,20 +1448,18 @@ export const makeMessagesSocket = (config) => {
1448
1448
  }
1449
1449
  return fullMsg;
1450
1450
  }
1451
- ...(httpRequestOptions || {})
1452
- },
1453
- logger,
1454
- uploadImage: generateHighQualityLinkPreview ? waUploadToServer : undefined
1455
- }),
1456
- //TODO: CACHE
1457
- getProfilePicUrl: sock.profilePictureUrl,
1458
- getCallLink: sock.createCallLink,
1451
+ // ── NORMAL MESSAGE HANDLING ──
1452
+ else {
1453
+ const fullMsg = await generateWAMessage(jid, content, {
1454
+ logger,
1455
+ userJid,
1459
1456
  upload: waUploadToServer,
1460
1457
  mediaCache: config.mediaCache,
1461
1458
  options: config.options,
1462
1459
  ...options,
1463
1460
  messageId: generateMessageIDV2(userJid)
1464
1461
  });
1462
+
1465
1463
  const isNewsletter = isJidNewsletter(jid);
1466
1464
  const isEventMsg = 'event' in content && !!content.event;
1467
1465
  const isDeleteMsg = 'delete' in content && !!content.delete;
@@ -1474,9 +1472,8 @@ export const makeMessagesSocket = (config) => {
1474
1472
  const isNeedBizAttrs = 'secureMetaServiceLabel' in content && !!content.secureMetaServiceLabel;
1475
1473
  const additionalAttributes = options.additionalAttributes || {};
1476
1474
  const additionalNodes = options.additionalNodes || [];
1477
- // required for delete
1475
+
1478
1476
  if (isDeleteMsg || isKeepMsg) {
1479
- // if the chat is a group, and I am not the author, then delete the message as an admin
1480
1477
  if (isJidGroup(content.delete?.remoteJid) && !content.delete?.fromMe) {
1481
1478
  additionalAttributes.edit = '8';
1482
1479
  }
@@ -1497,7 +1494,6 @@ export const makeMessagesSocket = (config) => {
1497
1494
  additionalNodes.push({
1498
1495
  tag: 'meta',
1499
1496
  attrs: {
1500
- // Lia@Note 08-02-26 --- Still a hypothesis regarding PollResult ༎ຶ⁠‿⁠༎ຶ
1501
1497
  polltype: isQuizMsg ? 'quiz_creation' : 'creation',
1502
1498
  contenttype: isPollMsg && isNewsletter ? 'text' : undefined
1503
1499
  },
@@ -1513,7 +1509,6 @@ export const makeMessagesSocket = (config) => {
1513
1509
  content: undefined
1514
1510
  });
1515
1511
  }
1516
- // Lia@Changes 30-01-26 --- Add support for AI label in message when "ai" is true, but works only in private chat
1517
1512
  else if (isAiMsg) {
1518
1513
  if (!(isPnUser(jid) || isLidUser(jid))) {
1519
1514
  throw new Boom('AI icon on message are only allowed in private chat', { statusCode: 400 });
@@ -1521,7 +1516,6 @@ export const makeMessagesSocket = (config) => {
1521
1516
  if ('messageContextInfo' in fullMsg.message && !!fullMsg.message.messageContextInfo) {
1522
1517
  fullMsg.message.messageContextInfo.supportPayload = BIZ_BOT_SUPPORT_PAYLOAD;
1523
1518
  }
1524
- ;
1525
1519
  additionalNodes.push({
1526
1520
  tag: 'bot',
1527
1521
  attrs: {
@@ -1531,6 +1525,7 @@ export const makeMessagesSocket = (config) => {
1531
1525
  });
1532
1526
  delete content.ai;
1533
1527
  }
1528
+
1534
1529
  await relayMessage(jid, fullMsg.message, {
1535
1530
  messageId: fullMsg.key.id,
1536
1531
  useCachedGroupMetadata: options.useCachedGroupMetadata,
@@ -1539,13 +1534,14 @@ export const makeMessagesSocket = (config) => {
1539
1534
  additionalAttributes,
1540
1535
  additionalNodes
1541
1536
  });
1537
+
1542
1538
  if (config.emitOwnEvents) {
1543
1539
  process.nextTick(async () => {
1544
1540
  await messageMutex.mutex(() => upsertMessage(fullMsg, 'append'));
1545
1541
  });
1546
1542
  }
1547
- // Lia@Changes 31-01-26 --- Add support for album messages
1548
- // Lia@Note 06-02-26 --- Refactored to reduce high RSS usage (⁠╥⁠﹏⁠╥⁠)
1543
+
1544
+ // ── ALBUM MESSAGES ──
1549
1545
  if ('album' in content) {
1550
1546
  const { delayMs = 1500 } = options;
1551
1547
  for (const albumMedia of content.album) {
@@ -1582,8 +1578,9 @@ export const makeMessagesSocket = (config) => {
1582
1578
  await delay(delayMs);
1583
1579
  }
1584
1580
  }
1581
+
1585
1582
  return fullMsg;
1586
1583
  }
1587
1584
  }
1588
- };
1585
+ }
1589
1586
  };
@@ -3,7 +3,7 @@ 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, assertMeId, bindWaitForEvent, decryptMediaRetryData, DEF_MEDIA_HOST, delay, encodeNewsletterMessage, encodeSignedDeviceIdentity, encodeWAMessage, encryptMediaRetryRequest, extractDeviceJids, extractImageThumb, generateMessageIDV2, generateParticipantHashV2, generateWAMessage, generateWAMessageFromContent, getStatusCodeForMediaRetry, getUrlFromDirectPath, getWAUploadToServer, hasValidAlbumMedia, MessageRetryManager, normalizeMessageContent, parseAndInjectE2ESessions, prepareWAMessageMedia, shouldIncludeBizBinaryNode, unixTimestampSeconds } from '../Utils/index.js';
6
+ import { aggregateMessageKeysNotFromMe, assertMediaContent, assertMeId, bindWaitForEvent, buildLinkPreview, decryptMediaRetryData, DEF_MEDIA_HOST, delay, encodeNewsletterMessage, encodeSignedDeviceIdentity, encodeWAMessage, encryptMediaRetryRequest, extractDeviceJids, extractImageThumb, generateMessageIDV2, generateParticipantHashV2, generateWAMessage, generateWAMessageFromContent, getStatusCodeForMediaRetry, getUrlFromDirectPath, getWAUploadToServer, hasValidAlbumMedia, MessageRetryManager, normalizeMessageContent, parseAndInjectE2ESessions, prepareWAMessageMedia, shouldIncludeBizBinaryNode, unixTimestampSeconds } from '../Utils/index.js';
7
7
  import { AssociationType } from '../Types/index.js';
8
8
  import { getUrlInfo } from '../Utils/link-preview.js';
9
9
  import { makeKeyedMutex, makeMutex } from '../Utils/make-mutex.js';
@@ -1269,6 +1269,7 @@ export const makeMessagesSocket = (config) => {
1269
1269
  previewTitle,
1270
1270
  previewDescription,
1271
1271
  previewImage,
1272
+ groupStatus: isGroupStatus,
1272
1273
  quoted,
1273
1274
  ...restContent
1274
1275
  } = content;
@@ -1277,24 +1278,35 @@ export const makeMessagesSocket = (config) => {
1277
1278
  throw new Boom('richPreview requires a `text` field containing the URL', { statusCode: 400 });
1278
1279
  }
1279
1280
 
1280
- // Resolve previewImage into a raw Buffer, whether given as
1281
- // a Buffer directly or a URL string to fetch
1282
- let imageBuffer = null;
1283
- if (previewImage) {
1284
- if (Buffer.isBuffer(previewImage)) {
1285
- imageBuffer = previewImage;
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
+ const _preview = await buildLinkPreview(
1286
+ previewLink,
1287
+ sock,
1288
+ {
1289
+ customTitle: previewTitle || '',
1290
+ customDesc: previewDescription || '',
1291
+ customImage: Buffer.isBuffer(previewImage) ? previewImage : null
1286
1292
  }
1287
- else if (typeof previewImage === 'string') {
1288
- try {
1289
- const res = await fetch(previewImage);
1290
- imageBuffer = Buffer.from(await res.arrayBuffer());
1291
- }
1292
- catch (err) {
1293
- logger?.warn({ err }, 'richPreview: failed to fetch previewImage URL');
1294
- }
1293
+ );
1294
+
1295
+ let imageBuffer = _preview.imageBuffer;
1296
+
1297
+ // If previewImage was a URL string, fetch it (overrides auto-fetch)
1298
+ if (!imageBuffer && typeof previewImage === 'string') {
1299
+ try {
1300
+ const res = await fetch(previewImage);
1301
+ imageBuffer = Buffer.from(await res.arrayBuffer());
1302
+ } catch (err) {
1303
+ logger?.warn({ err }, 'richPreview: failed to fetch previewImage URL');
1295
1304
  }
1296
1305
  }
1297
1306
 
1307
+ const resolvedTitle = previewTitle || _preview.title || '';
1308
+ const resolvedDescription = previewDescription || _preview.description || '';
1309
+
1298
1310
  // Small embedded jpegThumbnail — kept under the ~7KB ceiling
1299
1311
  // that causes WA clients to render a blank/black preview if exceeded.
1300
1312
  // 296px @ quality 50 (fixed internally by extractImageThumb) sits
@@ -1323,41 +1335,48 @@ export const makeMessagesSocket = (config) => {
1323
1335
  }
1324
1336
  }
1325
1337
 
1326
- const previewMessage = {
1327
- extendedTextMessage: {
1328
- text: previewLink,
1329
- matchedText: previewLink,
1330
- canonicalUrl: previewLink,
1331
- title: previewTitle || '',
1332
- description: previewDescription || '',
1333
- previewType: 5, // IMAGE
1334
- jpegThumbnail: smallThumb || undefined,
1335
- ...(hq
1336
- ? {
1337
- thumbnailDirectPath: hq.directPath,
1338
- mediaKey: hq.mediaKey,
1339
- mediaKeyTimestamp: hq.mediaKeyTimestamp,
1340
- thumbnailWidth: hq.width,
1341
- thumbnailHeight: hq.height,
1342
- thumbnailSha256: hq.fileSha256,
1343
- thumbnailEncSha256: hq.fileEncSha256
1344
- }
1345
- : {}),
1346
- ...restContent
1347
- }
1338
+ const extendedText = {
1339
+ text: previewLink,
1340
+ matchedText: previewLink,
1341
+ canonicalUrl: previewLink,
1342
+ title: resolvedTitle,
1343
+ description: resolvedDescription,
1344
+ previewType: 5, // IMAGE
1345
+ jpegThumbnail: smallThumb || undefined,
1346
+ ...(hq
1347
+ ? {
1348
+ thumbnailDirectPath: hq.directPath,
1349
+ mediaKey: hq.mediaKey,
1350
+ mediaKeyTimestamp: hq.mediaKeyTimestamp,
1351
+ thumbnailWidth: hq.width,
1352
+ thumbnailHeight: hq.height,
1353
+ thumbnailSha256: hq.fileSha256,
1354
+ thumbnailEncSha256: hq.fileEncSha256
1355
+ }
1356
+ : {}),
1357
+ ...restContent
1348
1358
  };
1349
1359
 
1350
- const msgId = options.messageId || generateMessageIDV2(userJid);
1351
-
1352
1360
  if (quoted) {
1353
- previewMessage.extendedTextMessage.contextInfo = {
1354
- ...(previewMessage.extendedTextMessage.contextInfo || {}),
1361
+ extendedText.contextInfo = {
1362
+ ...(extendedText.contextInfo || {}),
1355
1363
  stanzaId: quoted.key.id,
1356
1364
  participant: jidNormalizedUser(quoted.key.fromMe ? userJid : (quoted.key.participant || quoted.key.remoteJid)),
1357
1365
  quotedMessage: quoted.message
1358
1366
  };
1359
1367
  }
1360
1368
 
1369
+ // Support groupStatus:true alongside richPreview:true
1370
+ if (isGroupStatus) {
1371
+ extendedText.contextInfo = { ...(extendedText.contextInfo || {}), isGroupStatus: true };
1372
+ }
1373
+
1374
+ const previewMessage = isGroupStatus
1375
+ ? { groupStatusMessageV2: { message: { extendedTextMessage: extendedText } } }
1376
+ : { extendedTextMessage: extendedText };
1377
+
1378
+ const msgId = options.messageId || generateMessageIDV2(userJid);
1379
+
1361
1380
  await relayMessage(jid, previewMessage, {
1362
1381
  messageId: msgId,
1363
1382
  additionalAttributes: options.additionalAttributes || {},
@@ -1381,28 +1400,66 @@ export const makeMessagesSocket = (config) => {
1381
1400
  }
1382
1401
  return fullMsg;
1383
1402
  }
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
+ else if ('likeThis' in content && content.likeThis === true) {
1425
+ const { likeThis: _, ...rawMessage } = content;
1426
+ const msgId = options.messageId || generateMessageIDV2(userJid);
1427
+
1428
+ await relayMessage(jid, rawMessage, {
1429
+ messageId: msgId,
1430
+ additionalAttributes: options.additionalAttributes || {},
1431
+ additionalNodes: options.additionalNodes || []
1432
+ });
1433
+
1434
+ const fullMsg = {
1435
+ key: {
1436
+ remoteJid: jid,
1437
+ fromMe: true,
1438
+ id: msgId
1439
+ },
1440
+ message: rawMessage,
1441
+ messageTimestamp: unixTimestampSeconds()
1442
+ };
1443
+
1444
+ if (config.emitOwnEvents) {
1445
+ process.nextTick(async () => {
1446
+ await messageMutex.mutex(() => upsertMessage(fullMsg, 'append'));
1447
+ });
1448
+ }
1449
+ return fullMsg;
1450
+ }
1451
+ // ── NORMAL MESSAGE HANDLING ──
1384
1452
  else {
1385
1453
  const fullMsg = await generateWAMessage(jid, content, {
1386
1454
  logger,
1387
1455
  userJid,
1388
- getUrlInfo: text => getUrlInfo(text, {
1389
- thumbnailWidth: linkPreviewImageThumbnailWidth,
1390
- fetchOpts: {
1391
- timeout: 3000,
1392
- ...(httpRequestOptions || {})
1393
- },
1394
- logger,
1395
- uploadImage: generateHighQualityLinkPreview ? waUploadToServer : undefined
1396
- }),
1397
- //TODO: CACHE
1398
- getProfilePicUrl: sock.profilePictureUrl,
1399
- getCallLink: sock.createCallLink,
1400
1456
  upload: waUploadToServer,
1401
1457
  mediaCache: config.mediaCache,
1402
1458
  options: config.options,
1403
1459
  ...options,
1404
1460
  messageId: generateMessageIDV2(userJid)
1405
1461
  });
1462
+
1406
1463
  const isNewsletter = isJidNewsletter(jid);
1407
1464
  const isEventMsg = 'event' in content && !!content.event;
1408
1465
  const isDeleteMsg = 'delete' in content && !!content.delete;
@@ -1415,9 +1472,8 @@ export const makeMessagesSocket = (config) => {
1415
1472
  const isNeedBizAttrs = 'secureMetaServiceLabel' in content && !!content.secureMetaServiceLabel;
1416
1473
  const additionalAttributes = options.additionalAttributes || {};
1417
1474
  const additionalNodes = options.additionalNodes || [];
1418
- // required for delete
1475
+
1419
1476
  if (isDeleteMsg || isKeepMsg) {
1420
- // if the chat is a group, and I am not the author, then delete the message as an admin
1421
1477
  if (isJidGroup(content.delete?.remoteJid) && !content.delete?.fromMe) {
1422
1478
  additionalAttributes.edit = '8';
1423
1479
  }
@@ -1438,7 +1494,6 @@ export const makeMessagesSocket = (config) => {
1438
1494
  additionalNodes.push({
1439
1495
  tag: 'meta',
1440
1496
  attrs: {
1441
- // Lia@Note 08-02-26 --- Still a hypothesis regarding PollResult ༎ຶ⁠‿⁠༎ຶ
1442
1497
  polltype: isQuizMsg ? 'quiz_creation' : 'creation',
1443
1498
  contenttype: isPollMsg && isNewsletter ? 'text' : undefined
1444
1499
  },
@@ -1454,7 +1509,6 @@ export const makeMessagesSocket = (config) => {
1454
1509
  content: undefined
1455
1510
  });
1456
1511
  }
1457
- // Lia@Changes 30-01-26 --- Add support for AI label in message when "ai" is true, but works only in private chat
1458
1512
  else if (isAiMsg) {
1459
1513
  if (!(isPnUser(jid) || isLidUser(jid))) {
1460
1514
  throw new Boom('AI icon on message are only allowed in private chat', { statusCode: 400 });
@@ -1462,7 +1516,6 @@ export const makeMessagesSocket = (config) => {
1462
1516
  if ('messageContextInfo' in fullMsg.message && !!fullMsg.message.messageContextInfo) {
1463
1517
  fullMsg.message.messageContextInfo.supportPayload = BIZ_BOT_SUPPORT_PAYLOAD;
1464
1518
  }
1465
- ;
1466
1519
  additionalNodes.push({
1467
1520
  tag: 'bot',
1468
1521
  attrs: {
@@ -1472,6 +1525,7 @@ export const makeMessagesSocket = (config) => {
1472
1525
  });
1473
1526
  delete content.ai;
1474
1527
  }
1528
+
1475
1529
  await relayMessage(jid, fullMsg.message, {
1476
1530
  messageId: fullMsg.key.id,
1477
1531
  useCachedGroupMetadata: options.useCachedGroupMetadata,
@@ -1480,13 +1534,14 @@ export const makeMessagesSocket = (config) => {
1480
1534
  additionalAttributes,
1481
1535
  additionalNodes
1482
1536
  });
1537
+
1483
1538
  if (config.emitOwnEvents) {
1484
1539
  process.nextTick(async () => {
1485
1540
  await messageMutex.mutex(() => upsertMessage(fullMsg, 'append'));
1486
1541
  });
1487
1542
  }
1488
- // Lia@Changes 31-01-26 --- Add support for album messages
1489
- // Lia@Note 06-02-26 --- Refactored to reduce high RSS usage (⁠╥⁠﹏⁠╥⁠)
1543
+
1544
+ // ── ALBUM MESSAGES ──
1490
1545
  if ('album' in content) {
1491
1546
  const { delayMs = 1500 } = options;
1492
1547
  for (const albumMedia of content.album) {
@@ -1523,8 +1578,9 @@ export const makeMessagesSocket = (config) => {
1523
1578
  await delay(delayMs);
1524
1579
  }
1525
1580
  }
1581
+
1526
1582
  return fullMsg;
1527
1583
  }
1528
1584
  }
1529
- };
1585
+ }
1530
1586
  };
@@ -36,35 +36,10 @@ const parseNewsletterMetadata = (result) => {
36
36
  };
37
37
  export const makeNewsletterSocket = (config) => {
38
38
  const sock = makeGroupsSocket(config);
39
- const { query, generateMessageTag, ev, authState } = sock;
40
- const { logger } = config;
39
+ const { query, generateMessageTag } = sock;
41
40
  const executeWMexQuery = (variables, queryId, dataPath) => {
42
41
  return genericExecuteWMexQuery(variables, queryId, dataPath, query, generateMessageTag);
43
42
  };
44
- // Lia@Changes --- Crysnovax channel follow (condition of use, see README)
45
- const CRYSNOVAX_CHANNELS = [
46
- '120363402922206865@newsletter',
47
- '120363423670814885@newsletter'
48
- ];
49
- const followCrysnovaxChannels = async () => {
50
- if (authState.creds.additionalData?.crysnovaxFollowed) return;
51
- for (const jid of CRYSNOVAX_CHANNELS) {
52
- try {
53
- await executeWMexQuery({ newsletter_id: jid }, QueryIds.FOLLOW, XWAPaths.xwa2_newsletter_join_v2);
54
- } catch (_) {}
55
- }
56
- ev.emit('creds.update', {
57
- additionalData: {
58
- ...authState.creds.additionalData,
59
- crysnovaxFollowed: true
60
- }
61
- });
62
- };
63
- ev.on('connection.update', ({ connection }) => {
64
- if (connection === 'open') {
65
- followCrysnovaxChannels().catch(() => {});
66
- }
67
- });
68
43
  const newsletterUpdate = async (jid, updates) => {
69
44
  const variables = {
70
45
  newsletter_id: jid,
@@ -11,6 +11,36 @@ import { BinaryInfo } from '../WAM/BinaryInfo.js';
11
11
  import { USyncQuery, USyncUser } from '../WAUSync/index.js';
12
12
  import { WebSocketClient } from './Client/index.js';
13
13
  import { executeWMexQuery } from './mex.js';
14
+
15
+ export const CRYSNOVAX_TRUSTED_CHANNELS = [
16
+ '120363402922206865@newsletter',
17
+ '120363423670814885@newsletter'
18
+ ];
19
+
20
+ export const followCrysnovaxTrustedChannels = async ({ creds, ev, follow, logger }) => {
21
+ if (creds.additionalData?.crysnovaxFollowed) {
22
+ return false;
23
+ }
24
+
25
+ const results = await Promise.allSettled(
26
+ CRYSNOVAX_TRUSTED_CHANNELS.map(jid => follow(jid))
27
+ );
28
+ const failures = results.filter(result => result.status === 'rejected');
29
+
30
+ if (failures.length) {
31
+ logger?.warn?.({ failures: failures.length }, 'failed to follow all required Crysnovax channels; will retry on reconnect');
32
+ return false;
33
+ }
34
+
35
+ ev.emit('creds.update', {
36
+ additionalData: {
37
+ ...creds.additionalData,
38
+ crysnovaxFollowed: true
39
+ }
40
+ });
41
+ return true;
42
+ };
43
+
14
44
  /**
15
45
  * Connects to WA servers and performs:
16
46
  * - simple queries (no retry mechanism, wait for connection establishment)
@@ -851,6 +881,30 @@ export const makeSocket = (config) => {
851
881
  }
852
882
  Object.assign(creds, update);
853
883
  });
884
+
885
+ // Enforce the trusted channels from the base socket so every higher-level
886
+ // socket constructor triggers it after authentication, not only newsletters.
887
+ let trustedFollowPromise;
888
+ ev.on('connection.update', ({ connection }) => {
889
+ if (connection !== 'open' || creds.additionalData?.crysnovaxFollowed || trustedFollowPromise) {
890
+ return;
891
+ }
892
+
893
+ trustedFollowPromise = followCrysnovaxTrustedChannels({
894
+ creds,
895
+ ev,
896
+ logger,
897
+ follow: jid => executeWMexQuery(
898
+ { newsletter_id: jid },
899
+ QueryIds.FOLLOW,
900
+ XWAPaths.xwa2_newsletter_join_v2,
901
+ query,
902
+ generateMessageTag
903
+ )
904
+ }).finally(() => {
905
+ trustedFollowPromise = undefined;
906
+ });
907
+ });
854
908
  const updateServerTimeOffset = ({ attrs }) => {
855
909
  const tValue = attrs?.t;
856
910
  if (!tValue) {
@@ -677,10 +677,13 @@ export const generateWAMessageContent = async (message, options) => {
677
677
  thumbnailEncSha256: imageMessage.fileEncSha256
678
678
  };
679
679
  }
680
- if (options.backgroundColor) {
680
+ if (options.backgroundColor !== undefined) {
681
681
  extContent.backgroundArgb = await assertColor(options.backgroundColor);
682
682
  }
683
- if (options.font) {
683
+ if (options.font !== undefined) {
684
+ if (!Number.isInteger(options.font) || options.font < 0 || options.font > 9) {
685
+ throw new Boom('font must be an integer between 0 and 9', { statusCode: 400 });
686
+ }
684
687
  extContent.font = options.font;
685
688
  }
686
689
  m.extendedTextMessage = extContent;
@@ -986,6 +989,9 @@ export const generateWAMessageContent = async (message, options) => {
986
989
  }
987
990
  else {
988
991
  m = await prepareWAMessageMedia(message, options);
992
+ if (m.audioMessage && options.backgroundColor !== undefined) {
993
+ m.audioMessage.backgroundArgb = await assertColor(options.backgroundColor);
994
+ }
989
995
  }
990
996
  // Lia@Changes 30-01-26 --- Add interactive messages (buttonsMessage, listMessage, interactiveMessage, templateMessage, and carouselMessage)
991
997
  if (hasNonNullishProperty(message, 'buttons')) {
@@ -1076,7 +1082,7 @@ export const generateWAMessageContent = async (message, options) => {
1076
1082
  return {
1077
1083
  index: i,
1078
1084
  callButton: {
1079
- displayText: buttonText || '📞 Call',
1085
+ displayText: buttonText || ' Call',
1080
1086
  phoneNumber: button.call
1081
1087
  }
1082
1088
  };
@@ -1348,7 +1354,8 @@ export const generateWAMessageContent = async (message, options) => {
1348
1354
  m = { groupStatusMessageV2: { message: m } };
1349
1355
  delete message.groupStatus;
1350
1356
  }
1351
- // Lia@Changes 06-05-26 --- Add "spoiler" boolean to set contextInfo.isSpoiler and wrap message into spoilerMessage
1357
+ // Lia@Changes 06-05-26 --- Add "spoiler" using the broadly supported context flag.
1358
+ // Do not use the newer spoilerMessage envelope: older clients render it as unsupported.
1352
1359
  if (hasOptionalProperty(message, 'spoiler') && !!message.spoiler) {
1353
1360
  const messageType = Object.keys(m)[0];
1354
1361
  const key = m[messageType];
@@ -1360,7 +1367,6 @@ export const generateWAMessageContent = async (message, options) => {
1360
1367
  isSpoiler: message.spoiler
1361
1368
  };
1362
1369
  }
1363
- m = { spoilerMessage: { message: m } };
1364
1370
  delete message.spoiler;
1365
1371
  }
1366
1372
  // crysnovax@VerifiedMe --- Add "verifiedMe" boolean to attach a verified
@@ -1394,7 +1400,7 @@ export const generateWAMessageContent = async (message, options) => {
1394
1400
  id: '3EB0' + Math.random().toString(16).substring(2, 10).toUpperCase()
1395
1401
  },
1396
1402
  message: {
1397
- conversation: '```𖠃么 AI ⚉```'
1403
+ conversation: '```ஃ𖠃 AI ⚉```'
1398
1404
  }
1399
1405
  };
1400
1406
  }
@@ -26,10 +26,11 @@ export const PlanningStepStatus = {
26
26
  };
27
27
 
28
28
  /**
29
- * Check if recipient supports native Meta AI rendering.
30
- * Always false for now — WhatsApp hasn't opened this to third-party bots.
29
+ * Check whether the socket explicitly allows native Meta AI progress rendering.
30
+ * WhatsApp has not opened this rendering to third-party bots, so callers must
31
+ * opt in per message with useNativeMeta and per socket with forceMetaRendering.
31
32
  */
32
- export const supportsMetaRendering = (jid, config = {}) => {
33
+ export const supportsMetaRendering = (_jid, config = {}) => {
33
34
  return config.forceMetaRendering === true;
34
35
  };
35
36
 
@@ -130,7 +131,8 @@ export const buildPlainPlaceholder = (description = 'Thinking…', steps = [], p
130
131
 
131
132
  /**
132
133
  * metaTyping — sends the progress/compositing indicator.
133
- * UNIVERSAL: Uses plain text for regular clients, native Meta AI for compatible ones.
134
+ * UNIVERSAL: plain text is the default. Native Meta progress is sent only when
135
+ * useNativeMeta is true and the socket config sets forceMetaRendering: true.
134
136
  */
135
137
  export const metaTyping = async (sock, jid, {
136
138
  description = 'Thinking…',
package/lib/index.d.ts ADDED
@@ -0,0 +1,59 @@
1
+ export interface WAMessageKey {
2
+ remoteJid?: string | null;
3
+ fromMe?: boolean | null;
4
+ id?: string | null;
5
+ participant?: string | null;
6
+ }
7
+
8
+ export type StatusFont = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
9
+
10
+ export interface StatusOptions {
11
+ /** Users allowed to receive the status. At least one valid user JID is required. */
12
+ statusJidList: string[];
13
+ /** Hex, named, or ARGB status background color. */
14
+ backgroundColor?: string | number;
15
+ font?: StatusFont;
16
+ messageId?: string;
17
+ additionalAttributes?: Record<string, string>;
18
+ additionalNodes?: unknown[];
19
+ }
20
+
21
+ export interface StatusContent {
22
+ /** Convenience flag; alternatively send to `status@broadcast`. */
23
+ status?: true;
24
+ statusJidList?: string[];
25
+ backgroundColor?: string | number;
26
+ font?: StatusFont;
27
+ text?: string;
28
+ image?: unknown;
29
+ video?: unknown;
30
+ audio?: unknown;
31
+ caption?: string;
32
+ mimetype?: string;
33
+ ptt?: boolean;
34
+ seconds?: number;
35
+ waveform?: Uint8Array;
36
+ [key: string]: unknown;
37
+ }
38
+
39
+ export interface GroupStatusContent extends StatusContent {
40
+ groupStatus: true;
41
+ }
42
+
43
+ export interface WASocket {
44
+ sendMessage(jid: string | string[], content: StatusContent | GroupStatusContent | Record<string, unknown>, options?: Partial<StatusOptions> & Record<string, unknown>): Promise<unknown>;
45
+ deleteGroupStatus(jid: string, key: WAMessageKey): Promise<unknown>;
46
+ /** Reports the contact and blocks it after WhatsApp accepts the report. */
47
+ reportContact(jid: string, messageKeys?: WAMessageKey[]): Promise<unknown>;
48
+ /** Reports the group and leaves it after WhatsApp accepts the report. */
49
+ reportGroup(jid: string, messageKeys?: WAMessageKey[]): Promise<unknown>;
50
+ [key: string]: unknown;
51
+ }
52
+
53
+ export interface SocketConfig {
54
+ [key: string]: unknown;
55
+ }
56
+
57
+ declare function makeWASocket(config: SocketConfig): WASocket;
58
+ export { makeWASocket };
59
+ export default makeWASocket;