@alannxd/baileys 6.0.5 → 6.0.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.
Files changed (123) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +341 -286
  3. package/WAProto/WAProto.proto +1782 -359
  4. package/WAProto/index.d.ts +75133 -9893
  5. package/WAProto/index.js +205861 -60606
  6. package/lib/Socket/luxu.js +6 -117
  7. package/lib/Socket/messages-send.js +637 -493
  8. package/lib/Socket/newsletter.js +229 -156
  9. package/lib/Utils/browser-utils.js +26 -6
  10. package/lib/Utils/decode-wa-message.js +33 -0
  11. package/lib/Utils/generics.js +10 -0
  12. package/lib/Utils/index.js +1 -0
  13. package/lib/Utils/message-composer.js +273 -0
  14. package/lib/Utils/messages.js +361 -24
  15. package/lib/WABinary/generic-utils.js +8 -1
  16. package/lib/WABinary/jid-utils.js +2 -0
  17. package/lib/index.js +1 -2
  18. package/package.json +5 -4
  19. package/lib/Defaults/index.d.ts +0 -75
  20. package/lib/Signal/Group/ciphertext-message.d.ts +0 -10
  21. package/lib/Signal/Group/group-session-builder.d.ts +0 -15
  22. package/lib/Signal/Group/group_cipher.d.ts +0 -17
  23. package/lib/Signal/Group/index.d.ts +0 -12
  24. package/lib/Signal/Group/keyhelper.d.ts +0 -11
  25. package/lib/Signal/Group/sender-chain-key.d.ts +0 -14
  26. package/lib/Signal/Group/sender-key-distribution-message.d.ts +0 -17
  27. package/lib/Signal/Group/sender-key-message.d.ts +0 -19
  28. package/lib/Signal/Group/sender-key-name.d.ts +0 -18
  29. package/lib/Signal/Group/sender-key-record.d.ts +0 -31
  30. package/lib/Signal/Group/sender-key-state.d.ts +0 -39
  31. package/lib/Signal/Group/sender-message-key.d.ts +0 -12
  32. package/lib/Signal/libsignal.d.ts +0 -5
  33. package/lib/Signal/lid-mapping.d.ts +0 -23
  34. package/lib/Socket/Client/index.d.ts +0 -3
  35. package/lib/Socket/Client/types.d.ts +0 -16
  36. package/lib/Socket/Client/websocket.d.ts +0 -13
  37. package/lib/Socket/business.d.ts +0 -217
  38. package/lib/Socket/chats.d.ts +0 -124
  39. package/lib/Socket/communities.d.ts +0 -273
  40. package/lib/Socket/groups.d.ts +0 -162
  41. package/lib/Socket/index.d.ts +0 -260
  42. package/lib/Socket/luxu.d.ts +0 -22
  43. package/lib/Socket/messages-recv.d.ts +0 -213
  44. package/lib/Socket/messages-send.d.ts +0 -199
  45. package/lib/Socket/mex.d.ts +0 -3
  46. package/lib/Socket/newsletter.d.ts +0 -170
  47. package/lib/Socket/socket.d.ts +0 -59
  48. package/lib/Store/index.d.ts +0 -10
  49. package/lib/Store/keyed-db.d.ts +0 -22
  50. package/lib/Store/make-cache-manager-store.d.ts +0 -19
  51. package/lib/Store/make-in-memory-store.d.ts +0 -39
  52. package/lib/Store/make-ordered-dictionary.d.ts +0 -14
  53. package/lib/Store/object-repository.d.ts +0 -11
  54. package/lib/Types/Auth.d.ts +0 -117
  55. package/lib/Types/Bussines.d.ts +0 -25
  56. package/lib/Types/Call.d.ts +0 -15
  57. package/lib/Types/Chat.d.ts +0 -124
  58. package/lib/Types/Contact.d.ts +0 -26
  59. package/lib/Types/Events.d.ts +0 -256
  60. package/lib/Types/GroupMetadata.d.ts +0 -71
  61. package/lib/Types/Label.d.ts +0 -47
  62. package/lib/Types/LabelAssociation.d.ts +0 -30
  63. package/lib/Types/Message.d.ts +0 -320
  64. package/lib/Types/Mex.d.ts +0 -141
  65. package/lib/Types/Product.d.ts +0 -79
  66. package/lib/Types/Signal.d.ts +0 -87
  67. package/lib/Types/Socket.d.ts +0 -136
  68. package/lib/Types/State.d.ts +0 -97
  69. package/lib/Types/USync.d.ts +0 -26
  70. package/lib/Types/index.d.ts +0 -65
  71. package/lib/Utils/auth-utils.d.ts +0 -24
  72. package/lib/Utils/browser-utils.d.ts +0 -4
  73. package/lib/Utils/business.d.ts +0 -23
  74. package/lib/Utils/chat-utils.d.ts +0 -100
  75. package/lib/Utils/companion-reg-client-utils.d.ts +0 -17
  76. package/lib/Utils/crypto.d.ts +0 -37
  77. package/lib/Utils/decode-wa-message.d.ts +0 -66
  78. package/lib/Utils/event-buffer.d.ts +0 -36
  79. package/lib/Utils/generics.d.ts +0 -91
  80. package/lib/Utils/history.d.ts +0 -24
  81. package/lib/Utils/identity-change-handler.d.ts +0 -44
  82. package/lib/Utils/index.d.ts +0 -22
  83. package/lib/Utils/link-preview.d.ts +0 -21
  84. package/lib/Utils/logger.d.ts +0 -12
  85. package/lib/Utils/lt-hash.d.ts +0 -8
  86. package/lib/Utils/make-mutex.d.ts +0 -9
  87. package/lib/Utils/message-retry-manager.d.ts +0 -115
  88. package/lib/Utils/messages-media.d.ts +0 -133
  89. package/lib/Utils/messages.d.ts +0 -91
  90. package/lib/Utils/noise-handler.d.ts +0 -20
  91. package/lib/Utils/offline-node-processor.d.ts +0 -17
  92. package/lib/Utils/pre-key-manager.d.ts +0 -28
  93. package/lib/Utils/process-message.d.ts +0 -60
  94. package/lib/Utils/reporting-utils.d.ts +0 -11
  95. package/lib/Utils/signal.d.ts +0 -47
  96. package/lib/Utils/stanza-ack.d.ts +0 -11
  97. package/lib/Utils/sync-action-utils.d.ts +0 -19
  98. package/lib/Utils/tc-token-utils.d.ts +0 -37
  99. package/lib/Utils/use-multi-file-auth-state.d.ts +0 -13
  100. package/lib/Utils/validate-connection.d.ts +0 -11
  101. package/lib/WABinary/constants.d.ts +0 -28
  102. package/lib/WABinary/decode.d.ts +0 -7
  103. package/lib/WABinary/encode.d.ts +0 -3
  104. package/lib/WABinary/generic-utils.d.ts +0 -18
  105. package/lib/WABinary/index.d.ts +0 -6
  106. package/lib/WABinary/jid-utils.d.ts +0 -48
  107. package/lib/WABinary/types.d.ts +0 -19
  108. package/lib/WAM/BinaryInfo.d.ts +0 -9
  109. package/lib/WAM/constants.d.ts +0 -40
  110. package/lib/WAM/encode.d.ts +0 -3
  111. package/lib/WAM/index.d.ts +0 -4
  112. package/lib/WAUSync/Protocols/USyncContactProtocol.d.ts +0 -10
  113. package/lib/WAUSync/Protocols/USyncDeviceProtocol.d.ts +0 -23
  114. package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.d.ts +0 -13
  115. package/lib/WAUSync/Protocols/USyncStatusProtocol.d.ts +0 -13
  116. package/lib/WAUSync/Protocols/USyncUsernameProtocol.d.ts +0 -10
  117. package/lib/WAUSync/Protocols/UsyncBotProfileProtocol.d.ts +0 -26
  118. package/lib/WAUSync/Protocols/UsyncLIDProtocol.d.ts +0 -10
  119. package/lib/WAUSync/Protocols/index.d.ts +0 -6
  120. package/lib/WAUSync/USyncQuery.d.ts +0 -30
  121. package/lib/WAUSync/USyncUser.d.ts +0 -17
  122. package/lib/WAUSync/index.d.ts +0 -4
  123. package/lib/index.d.ts +0 -13
@@ -2,16 +2,17 @@ import NodeCache from '@cacheable/node-cache';
2
2
  import { Boom } from '@hapi/boom';
3
3
  import { proto } from '../../WAProto/index.js';
4
4
  import { DEFAULT_CACHE_TTLS, WA_DEFAULT_EPHEMERAL } from '../Defaults/index.js';
5
- import { aggregateMessageKeysNotFromMe, assertMediaContent, assertMeId, bindWaitForEvent, decryptMediaRetryData, DEF_MEDIA_HOST, encodeNewsletterMessage, encodeSignedDeviceIdentity, encodeWAMessage, encryptMediaRetryRequest, extractDeviceJids, generateMessageIDV2, generateParticipantHashV2, generateWAMessage, getStatusCodeForMediaRetry, getUrlFromDirectPath, getWAUploadToServer, MessageRetryManager, normalizeMessageContent, parseAndInjectE2ESessions, unixTimestampSeconds } from '../Utils/index.js';
5
+ import { aggregateMessageKeysNotFromMe, assertMediaContent, assertMeId, bindWaitForEvent, decryptMediaRetryData, DEF_MEDIA_HOST, encodeNewsletterMessage, encodeSignedDeviceIdentity, encodeWAMessage, encryptMediaRetryRequest, extractDeviceJids, generateMessageIDV2, generateParticipantHashV2, generateWAMessage, getStatusCodeForMediaRetry, getUrlFromDirectPath, getWAUploadToServer, MessageRetryManager, normalizeMessageContent, parseAndInjectE2ESessions, unixTimestampSeconds, setBotMessageSecret } from '../Utils/index.js';
6
6
  import { getUrlInfo } from '../Utils/link-preview.js';
7
7
  import { makeKeyedMutex, makeMutex } from '../Utils/make-mutex.js';
8
8
  import { getMessageReportingToken, shouldIncludeReportingToken } from '../Utils/reporting-utils.js';
9
9
  import { buildMergedTcTokenIndexWrite, isTcTokenExpired, resolveIssuanceJid, resolveTcTokenJid, shouldSendNewTcToken, storeTcTokensFromIqResult } from '../Utils/tc-token-utils.js';
10
- import { areJidsSameUser, getBinaryNodeChild, getBinaryNodeChildren, isHostedLidUser, isHostedPnUser, isJidBot, isJidGroup, isJidMetaAI, isLidUser, isPnUser, jidDecode, jidEncode, jidNormalizedUser, PSA_WID, S_WHATSAPP_NET, getAdditionalNode, getBinaryNodeFilter } from '../WABinary/index.js';
10
+ import { areJidsSameUser, getBinaryNodeChild, getBinaryNodeChildren, isHostedLidUser, isHostedPnUser, isJidBot, isJidGroup, isJidMetaAI, isLidUser, isPnUser, jidDecode, jidEncode, jidNormalizedUser, PSA_WID, S_WHATSAPP_NET, getAdditionalNode, getBinaryNodeFilter, getBinaryFilteredBizBot, isInteropUser } from '../WABinary/index.js';
11
11
  import { USyncQuery, USyncUser } from '../WAUSync/index.js';
12
12
  import { makeNewsletterSocket } from './newsletter.js';
13
13
  import imup from './luxu.js';
14
14
  import * as Utils_1 from '../Utils/index.js';
15
+ import { randomBytes } from 'crypto';
15
16
  export const makeMessagesSocket = (config) => {
16
17
  const { logger, linkPreviewImageThumbnailWidth, generateHighQualityLinkPreview, options: httpRequestOptions, patchMessageBeforeSending, cachedGroupMetadata, enableRecentMessageCache, maxMsgRetryCount, aiLabel } = config;
17
18
  const sock = makeNewsletterSocket(config);
@@ -427,494 +428,464 @@ export const makeMessagesSocket = (config) => {
427
428
  }
428
429
  return { nodes, shouldIncludeDeviceIdentity };
429
430
  };
430
- const relayMessage = async (jid, message, { messageId: msgId, participant, additionalAttributes, additionalNodes, useUserDevicesCache, useCachedGroupMetadata, statusJidList }) => {
431
- const meId = assertMeId(authState.creds);
432
- const meLid = authState.creds.me?.lid;
433
- const isRetryResend = Boolean(participant?.jid);
434
- let shouldIncludeDeviceIdentity = isRetryResend;
435
- const statusJid = 'status@broadcast';
436
- const { user, server } = jidDecode(jid);
437
- const isGroup = server === 'g.us';
438
- const isStatus = jid === statusJid;
439
- const isLid = server === 'lid';
440
- const isNewsletter = server === 'newsletter';
441
- const isGroupOrStatus = isGroup || isStatus;
442
- const finalJid = jid;
443
- msgId = msgId || generateMessageIDV2(meId);
444
- useUserDevicesCache = useUserDevicesCache !== false;
445
- useCachedGroupMetadata = useCachedGroupMetadata !== false && !isStatus;
446
- const participants = [];
447
- const destinationJid = !isStatus ? finalJid : statusJid;
448
- const binaryNodeContent = [];
449
- const devices = [];
450
- const buttonType = getButtonType(message);
451
- let didPushAdditional = false;
452
- let reportingMessage;
453
- const meMsg = {
454
- deviceSentMessage: {
455
- destinationJid,
456
- message
457
- },
458
- messageContextInfo: message.messageContextInfo
459
- };
460
- const extraAttrs = {};
461
- if (participant) {
462
- if (!isGroup && !isStatus) {
463
- additionalAttributes = { ...additionalAttributes, device_fanout: 'false' };
464
- }
465
- const { user, device } = jidDecode(participant.jid);
466
- devices.push({
467
- user,
468
- device,
469
- jid: participant.jid
470
- });
471
- }
472
- await authState.keys.transaction(async () => {
473
- const mediaType = getMediaType(message);
474
- if (mediaType) {
475
- extraAttrs['mediatype'] = mediaType;
476
- }
477
- if (isNewsletter) {
478
- const patched = patchMessageBeforeSending ? await patchMessageBeforeSending(message, []) : message;
479
- const bytes = encodeNewsletterMessage(patched);
480
- binaryNodeContent.push({
481
- tag: 'plaintext',
482
- attrs: {},
483
- content: bytes
484
- });
485
- const stanza = {
486
- tag: 'message',
487
- attrs: {
488
- to: jid,
489
- id: msgId,
490
- type: getMessageType(message),
491
- ...(additionalAttributes || {})
492
- },
493
- content: binaryNodeContent
494
- };
495
- logger.debug({ msgId }, `sending newsletter message to ${jid}`);
496
- await sendNode(stanza);
497
- return;
498
- }
499
- if (normalizeMessageContent(message)?.pinInChatMessage || normalizeMessageContent(message)?.reactionMessage) {
500
- extraAttrs['decrypt-fail'] = 'hide'; // todo: expand for reactions and other types
501
- }
502
- if (isGroupOrStatus && !isRetryResend) {
503
- const [groupData, senderKeyMap] = await Promise.all([
504
- (async () => {
505
- 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?
506
- if (groupData && Array.isArray(groupData?.participants)) {
507
- logger.trace({ jid, participants: groupData.participants.length }, 'using cached group metadata');
508
- }
509
- else if (!isStatus) {
510
- groupData = await groupMetadata(jid); // TODO: start storing group participant list + addr mode in Signal & stop relying on this
511
- }
512
- return groupData;
513
- })(),
514
- (async () => {
515
- if (!participant && !isStatus) {
516
- // what if sender memory is less accurate than the cached metadata
517
- // on participant change in group, we should do sender memory manipulation
518
- 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?
519
- return result[jid] || {};
520
- }
521
- return {};
522
- })()
523
- ]);
524
- const participantsList = groupData ? groupData.participants.map(p => p.id) : [];
525
- if (groupData?.ephemeralDuration && groupData.ephemeralDuration > 0) {
526
- additionalAttributes = {
527
- ...additionalAttributes,
528
- expiration: groupData.ephemeralDuration.toString()
529
- };
530
- }
531
- if (isStatus && statusJidList) {
532
- participantsList.push(...statusJidList);
533
- }
534
- const additionalDevices = await getUSyncDevices(participantsList, !!useUserDevicesCache, false);
535
- devices.push(...additionalDevices);
536
- if (isGroup) {
537
- additionalAttributes = {
538
- ...additionalAttributes,
539
- addressing_mode: groupData?.addressingMode || 'lid'
540
- };
541
- }
542
- const patched = await patchMessageBeforeSending(message);
543
- if (Array.isArray(patched)) {
544
- throw new Boom('Per-jid patching is not supported in groups');
545
- }
546
- const bytes = encodeWAMessage(patched);
547
- reportingMessage = patched;
548
- const groupAddressingMode = additionalAttributes?.['addressing_mode'] || groupData?.addressingMode || 'lid';
549
- const groupSenderIdentity = groupAddressingMode === 'lid' && meLid ? meLid : meId;
550
- const { ciphertext, senderKeyDistributionMessage } = await signalRepository.encryptGroupMessage({
551
- group: destinationJid,
552
- data: bytes,
553
- meId: groupSenderIdentity
554
- });
555
- const senderKeyRecipients = [];
556
- for (const device of devices) {
557
- const deviceJid = device.jid;
558
- const hasKey = !!senderKeyMap[deviceJid];
559
- if ((!hasKey || !!participant) &&
560
- !isHostedLidUser(deviceJid) &&
561
- !isHostedPnUser(deviceJid) &&
562
- device.device !== 99) {
563
- //todo: revamp all this logic
564
- // 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
565
- senderKeyRecipients.push(deviceJid);
566
- senderKeyMap[deviceJid] = true;
567
- }
568
- }
569
- if (senderKeyRecipients.length) {
570
- logger.debug({ senderKeyJids: senderKeyRecipients }, 'sending new sender key');
571
- const senderKeyMsg = {
572
- senderKeyDistributionMessage: {
573
- axolotlSenderKeyDistributionMessage: senderKeyDistributionMessage,
574
- groupId: destinationJid
575
- }
576
- };
577
- const senderKeySessionTargets = senderKeyRecipients;
578
- await assertSessions(senderKeySessionTargets);
579
- const result = await createParticipantNodes(senderKeyRecipients, senderKeyMsg, extraAttrs);
580
- shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || result.shouldIncludeDeviceIdentity;
581
- participants.push(...result.nodes);
582
- }
583
- binaryNodeContent.push({
584
- tag: 'enc',
585
- attrs: { v: '2', type: 'skmsg', ...extraAttrs },
586
- content: ciphertext
587
- });
588
- await authState.keys.set({ 'sender-key-memory': { [jid]: senderKeyMap } });
589
- }
590
- else {
591
- // ADDRESSING CONSISTENCY: Match own identity to conversation context
592
- // TODO: investigate if this is true
593
- let ownId = meId;
594
- if (isLid && meLid) {
595
- ownId = meLid;
596
- logger.debug({ to: jid, ownId }, 'Using LID identity for @lid conversation');
597
- }
598
- else {
599
- logger.debug({ to: jid, ownId }, 'Using PN identity for @s.whatsapp.net conversation');
600
- }
601
- const { user: ownUser } = jidDecode(ownId);
602
- if (!participant) {
603
- const patchedForReporting = await patchMessageBeforeSending(message, [jid]);
604
- reportingMessage = Array.isArray(patchedForReporting)
605
- ? patchedForReporting.find(item => item.recipientJid === jid) || patchedForReporting[0]
606
- : patchedForReporting;
607
- }
608
- if (!isRetryResend) {
609
- const targetUserServer = isLid ? 'lid' : 's.whatsapp.net';
610
- devices.push({
611
- user,
612
- device: 0,
613
- jid: jidEncode(user, targetUserServer, 0) // rajeh, todo: this entire logic is convoluted and weird.
614
- });
615
- if (user !== ownUser) {
616
- const ownUserServer = isLid ? 'lid' : 's.whatsapp.net';
617
- const ownUserForAddressing = isLid && meLid ? jidDecode(meLid).user : jidDecode(meId).user;
618
- devices.push({
619
- user: ownUserForAddressing,
620
- device: 0,
621
- jid: jidEncode(ownUserForAddressing, ownUserServer, 0)
622
- });
623
- }
624
- if (additionalAttributes?.['category'] !== 'peer') {
625
- // Clear placeholders and enumerate actual devices
626
- devices.length = 0;
627
- // Use conversation-appropriate sender identity
628
- const senderIdentity = isLid && meLid
629
- ? jidEncode(jidDecode(meLid)?.user, 'lid', undefined)
630
- : jidEncode(jidDecode(meId)?.user, 's.whatsapp.net', undefined);
631
- // Enumerate devices for sender and target with consistent addressing
632
- const sessionDevices = await getUSyncDevices([senderIdentity, jid], true, false);
633
- devices.push(...sessionDevices);
634
- logger.debug({
635
- deviceCount: devices.length,
636
- devices: devices.map(d => `${d.user}:${d.device}@${jidDecode(d.jid)?.server}`)
637
- }, 'Device enumeration complete with unified addressing');
638
- }
639
- }
640
- const allRecipients = [];
641
- const meRecipients = [];
642
- const otherRecipients = [];
643
- const { user: mePnUser } = jidDecode(meId);
644
- const { user: meLidUser } = meLid ? jidDecode(meLid) : { user: null };
645
- for (const { user, jid } of devices) {
646
- const isExactSenderDevice = jid === meId || (meLid && jid === meLid);
431
+ const relayMessage = async (
432
+ jid,
433
+ message,
434
+ {
435
+ messageId: msgId,
436
+ participant = false,
437
+ additionalAttributes,
438
+ additionalNodes,
439
+ useUserDevicesCache,
440
+ useCachedGroupMetadata,
441
+ statusJidList
442
+ }
443
+ ) => {
444
+ const meId = authState.creds.me.id
445
+ const meLid = authState.creds.me?.lid
446
+ const isRetryResend = Boolean(participant?.jid)
447
+ let shouldIncludeDeviceIdentity = isRetryResend
448
+ const statusJid = 'status@broadcast'
449
+ const { user, server } = jidDecode(jid)
450
+ const isGroup = server === 'g.us'
451
+ const isStatus = jid === statusJid
452
+ const isLid = server === 'lid'
453
+ const isNewsletter = server === 'newsletter'
454
+ const isInterop = isInteropUser(jid)
455
+ const isGroupOrStatus = isGroup || isStatus
456
+ const finalJid = jid
457
+ msgId = msgId || generateMessageIDV2(meId)
458
+ useUserDevicesCache = useUserDevicesCache!== false
459
+ useCachedGroupMetadata = useCachedGroupMetadata!== false &&!isStatus
460
+ const participants = []
461
+ const destinationJid =!isStatus? finalJid : statusJid
462
+ const binaryNodeContent = []
463
+ const devices = []
464
+ let reportingMessage
465
+ const meMsg = {
466
+ deviceSentMessage: { destinationJid, message },
467
+ messageContextInfo: message.messageContextInfo
468
+ }
469
+ const extraAttrs = {}
470
+ const regexGroupOld = /^(\d{1,15})-(\d+)@g\.us$/
471
+ const messages = normalizeMessageContent(message)
472
+ const buttonType = getButtonType(messages)
473
+ const pollMessage =
474
+ messages.pollCreationMessage || messages.pollCreationMessageV2 || messages.pollCreationMessageV3
475
+ await authState.keys.transaction(async () => {
476
+ const mediaType = getMediaType(message)
477
+ if (mediaType) extraAttrs.mediatype = mediaType
478
+ if (isNewsletter) {
479
+ const patched = patchMessageBeforeSending? await patchMessageBeforeSending(message, []) : message
480
+ const bytes = encodeNewsletterMessage(patched)
481
+ binaryNodeContent.push({ tag: 'plaintext', attrs: {}, content: bytes })
482
+ const stanza = {
483
+ tag: 'message',
484
+ attrs: {
485
+ to: jid,
486
+ id: msgId,
487
+ type: getMessageType(message),
488
+ ...(additionalAttributes || {})
489
+ },
490
+ content: binaryNodeContent
491
+ }
492
+ logger.debug({ msgId }, `sending newsletter message to ${jid}`)
493
+ await sendNode(stanza)
494
+ return
495
+ }
496
+ if (normalizeMessageContent(message)?.pinInChatMessage || normalizeMessageContent(message)?.reactionMessage) {
497
+ extraAttrs['decrypt-fail'] = 'hide'
498
+ }
499
+ if (isGroupOrStatus &&!isRetryResend) {
500
+ const [groupData, senderKeyMap] = await Promise.all([
501
+ (async () => {
502
+ let groupData = useCachedGroupMetadata && cachedGroupMetadata? await cachedGroupMetadata(jid) : undefined
503
+ if (groupData && Array.isArray(groupData?.participants)) {
504
+ logger.trace({ jid, participants: groupData.participants.length }, 'using cached group metadata')
505
+ } else if (!isStatus) {
506
+ groupData = await groupMetadata(jid)
507
+ }
508
+ return groupData
509
+ })(),
510
+ (async () => {
511
+ if (!participant &&!isStatus) {
512
+ const result = await authState.keys.get('sender-key-memory', [jid])
513
+ return result[jid] || {}
514
+ }
515
+ return {}
516
+ })()
517
+ ])
518
+ const participantsList = groupData? groupData.participants.map(p => p.id) : []
519
+ if (groupData?.ephemeralDuration && groupData.ephemeralDuration > 0) {
520
+ additionalAttributes = {...additionalAttributes, expiration: groupData.ephemeralDuration.toString() }
521
+ }
522
+ if (isStatus && statusJidList) participantsList.push(...statusJidList)
523
+ const additionalDevices = await getUSyncDevices(participantsList,!!useUserDevicesCache, false)
524
+ devices.push(...additionalDevices)
525
+ if (isGroup) {
526
+ additionalAttributes = {
527
+ ...additionalAttributes,
528
+ addressing_mode: groupData?.addressingMode || 'lid'
529
+ }
530
+ }
531
+ if (message?.groupStatusMessageV2 &&!message?.messageContextInfo?.messageSecret) {
532
+ message = {
533
+ ...message,
534
+ messageContextInfo: {
535
+ ...(message.messageContextInfo || {}),
536
+ messageSecret: randomBytes(32)
537
+ },
538
+ groupStatusMessageV2: {
539
+ ...message.groupStatusMessageV2,
540
+ message: {
541
+ ...(message.groupStatusMessageV2.message || {}),
542
+ messageContextInfo: {
543
+ ...(message.groupStatusMessageV2.message?.messageContextInfo || {}),
544
+ messageSecret: message.messageContextInfo?.messageSecret || randomBytes(32)
545
+ }
546
+ }
547
+ }
548
+ }
549
+ }
550
+ // list/buttons/template -> interactiveMessage
551
+ if (message.listMessage) {
552
+ const list = message.listMessage
553
+ message = {
554
+ interactiveMessage: {
555
+ nativeFlowMessage: {
556
+ buttons: [
557
+ {
558
+ name: 'single_select',
559
+ buttonParamsJson: JSON.stringify({
560
+ title: list.buttonText || 'Select',
561
+ sections: (list.sections || []).map(section => ({
562
+ title: section.title || '',
563
+ highlight_label: '',
564
+ rows: (section.rows || []).map(row => ({
565
+ header: '',
566
+ title: row.title || '',
567
+ description: row.description || '',
568
+ id: row.rowId || row.id || ''
569
+ }))
570
+ }))
571
+ })
572
+ }
573
+ ],
574
+ messageParamsJson: '',
575
+ messageVersion: 1
576
+ },
577
+ body: { text: list.description || '' },
578
+ footer: list.footerText? { text: list.footerText } : undefined,
579
+ header: list.title? { title: list.title, hasMediaAttachment: false, subtitle: '' } : undefined,
580
+ contextInfo: list.contextInfo
581
+ }
582
+ }
583
+ } else if (message.buttonsMessage) {
584
+ const bMsg = message.buttonsMessage
585
+ const buttons = (bMsg.buttons || []).map(btn => ({
586
+ name: 'quick_reply',
587
+ buttonParamsJson: JSON.stringify({
588
+ display_text: btn.buttonText?.displayText || btn.buttonText || '',
589
+ id: btn.buttonId || btn.buttonText?.displayText || ''
590
+ })
591
+ }))
592
+ message = {
593
+ interactiveMessage: {
594
+ nativeFlowMessage: { buttons, messageParamsJson: '', messageVersion: 1 },
595
+ body: { text: bMsg.contentText || bMsg.text || '' },
596
+ footer: bMsg.footerText? { text: bMsg.footerText } : undefined,
597
+ header: bMsg.text
598
+ ? { title: bMsg.text, hasMediaAttachment: false, subtitle: '' }
599
+ : bMsg.imageMessage || bMsg.videoMessage || bMsg.documentMessage
600
+ ? { hasMediaAttachment: true,...(bMsg.imageMessage? { imageMessage: bMsg.imageMessage } : {}),...(bMsg.videoMessage? { videoMessage: bMsg.videoMessage } : {}) }
601
+ : undefined,
602
+ contextInfo: bMsg.contextInfo
603
+ }
604
+ }
605
+ } else if (message.templateMessage) {
606
+ const tmpl = message.templateMessage.hydratedTemplate || message.templateMessage.fourRowTemplate
607
+ if (tmpl) {
608
+ const buttons = (tmpl.hydratedButtons || [])
609
+ .map(hBtn => {
610
+ if (hBtn.quickReplyButton) {
611
+ return { name: 'quick_reply', buttonParamsJson: JSON.stringify({ display_text: hBtn.quickReplyButton.displayText || '', id: hBtn.quickReplyButton.id || hBtn.quickReplyButton.displayText || '' }) }
612
+ } else if (hBtn.urlButton) {
613
+ return { name: 'cta_url', buttonParamsJson: JSON.stringify({ display_text: hBtn.urlButton.displayText || '', url: hBtn.urlButton.url || '', merchant_url: hBtn.urlButton.url || '' }) }
614
+ } else if (hBtn.callButton) {
615
+ return { name: 'cta_call', buttonParamsJson: JSON.stringify({ display_text: hBtn.callButton.displayText || '', phone_number: hBtn.callButton.phoneNumber || '' }) }
616
+ }
617
+ return null
618
+ })
619
+ .filter(Boolean)
620
+ message = {
621
+ interactiveMessage: {
622
+ nativeFlowMessage: { buttons, messageParamsJson: '', messageVersion: 1 },
623
+ body: { text: tmpl.hydratedContentText || tmpl.contentText || '' },
624
+ footer: tmpl.hydratedFooterText? { text: tmpl.hydratedFooterText } : undefined,
625
+ header: tmpl.hydratedTitleText
626
+ ? { title: tmpl.hydratedTitleText, hasMediaAttachment: false, subtitle: '' }
627
+ : tmpl.imageMessage || tmpl.videoMessage || tmpl.documentMessage
628
+ ? { hasMediaAttachment: true,...(tmpl.imageMessage? { imageMessage: tmpl.imageMessage } : {}),...(tmpl.videoMessage? { videoMessage: tmpl.videoMessage } : {}) }
629
+ : undefined,
630
+ contextInfo: tmpl.contextInfo
631
+ }
632
+ }
633
+ }
634
+ }
635
+
636
+ const patched = await patchMessageBeforeSending(message)
637
+ if (Array.isArray(patched)) throw new Boom('Per-jid patching is not supported in groups')
638
+ const bytes = encodeWAMessage(patched)
639
+ reportingMessage = patched
640
+ const groupAddressingMode = additionalAttributes?.['addressing_mode'] || groupData?.addressingMode || 'lid'
641
+ const groupSenderIdentity = groupAddressingMode === 'lid' && meLid? meLid : meId
642
+ const { ciphertext, senderKeyDistributionMessage } = await signalRepository.encryptGroupMessage({
643
+ group: destinationJid,
644
+ data: bytes,
645
+ meId: groupSenderIdentity
646
+ })
647
+ const senderKeyRecipients = []
648
+ for (const device of devices) {
649
+ const deviceJid = device.jid
650
+ const hasKey =!!senderKeyMap[deviceJid]
651
+ if (!hasKey ||!!participant &&!isHostedLidUser(deviceJid) &&!isHostedPnUser(deviceJid) && device.device!== 99) {
652
+ senderKeyRecipients.push(deviceJid)
653
+ senderKeyMap[deviceJid] = true
654
+ }
655
+ }
656
+ if (senderKeyRecipients.length) {
657
+ logger.debug({ senderKeyJids: senderKeyRecipients }, 'sending new sender key')
658
+ const senderKeyMsg = {
659
+ senderKeyDistributionMessage: {
660
+ axolotlSenderKeyDistributionMessage: senderKeyDistributionMessage,
661
+ groupId: destinationJid
662
+ }
663
+ }
664
+ await assertSessions(senderKeyRecipients)
665
+ const result = await createParticipantNodes(senderKeyRecipients, senderKeyMsg, extraAttrs)
666
+ shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || result.shouldIncludeDeviceIdentity
667
+ participants.push(...result.nodes)
668
+ }
669
+ binaryNodeContent.push({ tag: 'enc', attrs: { v: '2', type: 'skmsg',...extraAttrs }, content: ciphertext })
670
+ await authState.keys.set({ 'sender-key-memory': { [jid]: senderKeyMap } })
671
+ } else {
672
+ let ownId = meId
673
+ if (isLid && meLid) {
674
+ ownId = meLid
675
+ logger.debug({ to: jid, ownId }, 'Using LID identity for @lid conversation')
676
+ } else {
677
+ logger.debug({ to: jid, ownId }, 'Using PN identity for @s.whatsapp.net conversation')
678
+ }
679
+ const { user: ownUser } = jidDecode(ownId)
680
+ if (!participant) {
681
+ const patchedForReporting = await patchMessageBeforeSending(message, [jid])
682
+ reportingMessage = Array.isArray(patchedForReporting)
683
+ ? patchedForReporting.find(item => item.recipientJid === jid) || patchedForReporting[0]
684
+ : patchedForReporting
685
+ }
686
+ if (!isRetryResend) {
687
+ const targetUserServer = isLid? 'lid' : isInterop? 'interop' : 's.whatsapp.net'
688
+ devices.push({ user, device: 0, jid: jidEncode(user, targetUserServer, 0) })
689
+ if (user!== ownUser &&!isInterop) {
690
+ const ownUserServer = isLid? 'lid' : 's.whatsapp.net'
691
+ const ownUserForAddressing = isLid && meLid? jidDecode(meLid).user : jidDecode(meId).user
692
+ devices.push({ user: ownUserForAddressing, device: 0, jid: jidEncode(ownUserForAddressing, ownUserServer, 0) })
693
+ }
694
+ if (additionalAttributes?.['category']!== 'peer' &&!isInterop) {
695
+ devices.length = 0
696
+ const senderIdentity = isLid && meLid
697
+ ? jidEncode(jidDecode(meLid)?.user, 'lid', undefined)
698
+ : jidEncode(jidDecode(meId)?.user, 's.whatsapp.net', undefined)
699
+ const sessionDevices = await getUSyncDevices([senderIdentity, jid], true, false)
700
+ devices.push(...sessionDevices)
701
+ logger.debug({ deviceCount: devices.length, devices: devices.map(d => `${d.user}:${d.device}@${jidDecode(d.jid)?.server}`) }, 'Device enumeration complete with unified addressing')
702
+ }
703
+ }
704
+ const allRecipients = []
705
+ const meRecipients = []
706
+ const otherRecipients = []
707
+ const { user: mePnUser } = jidDecode(meId)
708
+ const { user: meLidUser } = meLid? jidDecode(meLid) : { user: null }
709
+ for (const { user, jid } of devices) {
710
+ /** participant method by Xzc || Tsm, who's delete the credits = love BBC */
711
+ const isExactSenderDevice = jid === meId || (meLid && jid === meLid)
647
712
  if (isExactSenderDevice) {
648
- logger.debug({ jid, meId, meLid }, 'Skipping exact sender device (whatsmeow pattern)');
649
- continue;
650
- }
651
- // Check if this is our device (could match either PN or LID user)
652
- const isMe = user === mePnUser || user === meLidUser;
653
- if (isMe) {
654
- meRecipients.push(jid);
713
+ logger.debug({ jid, meId, meLid }, 'Skipping exact sender device (whatsmeow pattern)')
714
+ continue
655
715
  }
656
- else {
657
- otherRecipients.push(jid);
658
- }
659
- allRecipients.push(jid);
660
- }
661
- await assertSessions(allRecipients);
662
- const [{ nodes: meNodes, shouldIncludeDeviceIdentity: s1 }, { nodes: otherNodes, shouldIncludeDeviceIdentity: s2 }] = await Promise.all([
663
- // For own devices: use DSM if available (1:1 chats only)
664
- createParticipantNodes(meRecipients, meMsg || message, extraAttrs),
665
- createParticipantNodes(otherRecipients, message, extraAttrs, meMsg)
666
- ]);
667
- participants.push(...meNodes);
668
- participants.push(...otherNodes);
669
- if (meRecipients.length > 0 || otherRecipients.length > 0) {
670
- extraAttrs['phash'] = generateParticipantHashV2([...meRecipients, ...otherRecipients]);
671
- }
672
- shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || s1 || s2;
673
- }
674
- if (isRetryResend) {
675
- const isParticipantLid = isLidUser(participant.jid);
676
- const isMe = areJidsSameUser(participant.jid, isParticipantLid ? meLid : meId);
677
- let messageToSend = message;
678
- if (isGroupOrStatus) {
679
- let groupSenderIdentity;
680
- if (meLid && (await signalRepository.hasSenderKey({ group: destinationJid, meId: meLid }))) {
681
- groupSenderIdentity = meLid;
682
- }
683
- else if (await signalRepository.hasSenderKey({ group: destinationJid, meId })) {
684
- groupSenderIdentity = meId;
685
- }
686
- if (groupSenderIdentity) {
687
- try {
688
- const skdm = await signalRepository.getSenderKeyDistributionMessage({
689
- group: destinationJid,
690
- meId: groupSenderIdentity
691
- });
692
- messageToSend = {
693
- ...message,
694
- senderKeyDistributionMessage: {
695
- groupId: destinationJid,
696
- axolotlSenderKeyDistributionMessage: skdm
697
- }
698
- };
699
- }
700
- catch (err) {
701
- logger.warn({ err, jid: destinationJid }, 'failed to build SKDM for retry, sending without it');
716
+ const isMe = user === mePnUser || user === meLidUser
717
+ let ptcp = false
718
+ if (participant) {
719
+ if (!isJidGroup(jid) && !isStatus) {
720
+ if (!(!isMe)) ptcp = true
721
+ } else {
722
+ ptcp = false
702
723
  }
703
724
  }
704
- }
705
- const encodedMessageToSend = isMe
706
- ? encodeWAMessage({
707
- deviceSentMessage: {
708
- destinationJid,
709
- message: messageToSend
725
+ if (!ptcp) {
726
+ if (isMe) {
727
+ meRecipients.push(jid)
728
+ } else {
729
+ otherRecipients.push(jid)
710
730
  }
711
- })
712
- : encodeWAMessage(messageToSend);
713
- const { type, ciphertext: encryptedContent } = await signalRepository.encryptMessage({
714
- data: encodedMessageToSend,
715
- jid: participant.jid
716
- });
717
- binaryNodeContent.push({
718
- tag: 'enc',
719
- attrs: {
720
- v: '2',
721
- type,
722
- count: participant.count.toString()
723
- },
724
- content: encryptedContent
725
- });
726
- }
727
- if (participants.length) {
728
- if (additionalAttributes?.['category'] === 'peer') {
729
- const peerNode = participants[0]?.content?.[0];
730
- if (peerNode) {
731
- binaryNodeContent.push(peerNode); // push only enc
731
+ allRecipients.push(jid)
732
732
  }
733
733
  }
734
- else {
735
- binaryNodeContent.push({
736
- tag: 'participants',
737
- attrs: {},
738
- content: participants
739
- });
740
- }
741
- }
742
- const stanza = {
743
- tag: 'message',
744
- attrs: {
745
- id: msgId,
746
- to: destinationJid,
747
- type: getMessageType(message),
748
- ...(additionalAttributes || {})
749
- },
750
- content: binaryNodeContent
751
- };
752
- // if the participant to send to is explicitly specified (generally retry recp)
753
- // ensure the message is only sent to that person
754
- // if a retry receipt is sent to everyone -- it'll fail decryption for everyone else who received the msg
755
- if (participant) {
756
- if (isJidGroup(destinationJid)) {
757
- stanza.attrs.to = destinationJid;
758
- stanza.attrs.participant = participant.jid;
759
- }
760
- else if (areJidsSameUser(participant.jid, meId)) {
761
- stanza.attrs.to = participant.jid;
762
- stanza.attrs.recipient = destinationJid;
763
- }
764
- else {
765
- stanza.attrs.to = participant.jid;
766
- }
767
- }
768
- else {
769
- stanza.attrs.to = destinationJid;
770
- }
771
- if (shouldIncludeDeviceIdentity) {
772
- ;
773
- stanza.content.push({
774
- tag: 'device-identity',
775
- attrs: {},
776
- content: encodeSignedDeviceIdentity(authState.creds.account, true)
777
- });
778
- logger.debug({ jid }, 'adding device identity');
779
- }
780
- if (!isNewsletter && buttonType && !isStatus) {
781
- const content = getAdditionalNode(buttonType)
782
- const filteredNode = getBinaryNodeFilter(additionalNodes)
734
+ await assertSessions(allRecipients)
735
+ const [
736
+ { nodes: meNodes, shouldIncludeDeviceIdentity: s1 },
737
+ { nodes: otherNodes, shouldIncludeDeviceIdentity: s2 }
738
+ ] = await Promise.all([
739
+ createParticipantNodes(meRecipients, meMsg || message, extraAttrs),
740
+ createParticipantNodes(otherRecipients, message, extraAttrs, meMsg)
741
+ ])
742
+ participants.push(...meNodes,...otherNodes)
743
+ if (meRecipients.length > 0 || otherRecipients.length > 0) {
744
+ extraAttrs.phash = generateParticipantHashV2([...meRecipients,...otherRecipients])
745
+ }
746
+ shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || s1 || s2
747
+ }
748
+ if (isRetryResend) {
749
+ const isParticipantLid = jidDecode(participant.jid).server === 'lid'
750
+ const isMe = areJidsSameUser(participant.jid, isParticipantLid? meLid : meId)
751
+ const encodedMessageToSend = isMe
752
+ ? encodeWAMessage({ deviceSentMessage: { destinationJid, message } })
753
+ : encodeWAMessage(message)
754
+ const { type, ciphertext: encryptedContent } = await signalRepository.encryptMessage({
755
+ data: encodedMessageToSend,
756
+ jid: participant.jid
757
+ })
758
+ binaryNodeContent.push({
759
+ tag: 'enc',
760
+ attrs: { v: '2', type, count: (participant.count?? 0).toString() },
761
+ content: encryptedContent
762
+ })
763
+ }
764
+ if (participants.length) {
765
+ if (additionalAttributes?.['category'] === 'peer') {
766
+ const peerNode = participants[0]?.content?.[0]
767
+ if (peerNode) binaryNodeContent.push(peerNode)
768
+ } else if (isInterop) {
769
+ const recipientNode = participants.find(p => isInteropUser(p?.attrs?.jid))
770
+ const encNode = (recipientNode?? participants[0])?.content?.[0]
771
+ if (encNode) binaryNodeContent.push(encNode)
772
+ } else {
773
+ binaryNodeContent.push({ tag: 'participants', attrs: {}, content: participants })
774
+ }
775
+ }
776
+ const stanza = {
777
+ tag: 'message',
778
+ attrs: { id: msgId, to: destinationJid, type: getMessageType(message),...(additionalAttributes || {}) },
779
+ content: binaryNodeContent
780
+ }
781
+ if (shouldIncludeDeviceIdentity) {
782
+ stanza.content.push({ tag: 'device-identity', attrs: {}, content: encodeSignedDeviceIdentity(authState.creds.account, true) })
783
+ logger.debug({ jid }, 'adding device identity')
784
+ }
783
785
 
784
- if (filteredNode) {
785
- didPushAdditional = true
786
- stanza.content.push(...additionalNodes)
787
- }
788
- else {
789
- stanza.content.push(...content)
790
- }
791
- logger.debug({ jid }, 'adding business node')
792
- }
793
- if (!isNewsletter && !isGroupOrStatus && aiLabel) {
794
- const botNode = {
795
- tag: 'bot',
796
- attrs: {
797
- biz_bot: '1'
798
- }
799
- }
800
- const filteredBizBot = getBinaryNodeFilter(additionalNodes ? additionalNodes : [])
801
- if (filteredBizBot) {
802
- stanza.content.push(...additionalNodes)
803
- didPushAdditional = true
804
- }
805
- else {
806
- stanza.content.push(botNode)
807
- }
808
- }
809
- if (!isNewsletter &&
810
- !isRetryResend &&
811
- reportingMessage?.messageContextInfo?.messageSecret &&
812
- shouldIncludeReportingToken(reportingMessage)) {
813
- try {
814
- const encoded = encodeWAMessage(reportingMessage);
815
- const reportingKey = {
816
- id: msgId,
817
- fromMe: true,
818
- remoteJid: destinationJid,
819
- participant: participant?.jid
820
- };
821
- const reportingNode = await getMessageReportingToken(encoded, reportingMessage, reportingKey);
822
- if (reportingNode) {
823
- ;
824
- stanza.content.push(reportingNode);
825
- logger.trace({ jid }, 'added reporting token to message');
826
- }
827
- }
828
- catch (error) {
829
- logger.warn({ jid, trace: error?.stack }, 'failed to attach reporting token');
830
- }
831
- }
832
- // WA Web never attaches tctoken to peer (AppStateSync) messages — server rejects with 479
833
- const isPeerMessage = additionalAttributes?.['category'] === 'peer';
834
- const is1on1Send = !isGroup && !isRetryResend && !isStatus && !isNewsletter && !isPeerMessage;
835
- // Resolve destination to LID for tctoken storage — matches Signal session key pattern
836
- const tcTokenJid = is1on1Send ? await resolveTcTokenJid(destinationJid, getLIDForPN) : destinationJid;
837
- const contactTcTokenData = is1on1Send ? await authState.keys.get('tctoken', [tcTokenJid]) : {};
838
- const existingTokenEntry = contactTcTokenData[tcTokenJid];
839
- let tcTokenBuffer = existingTokenEntry?.token;
840
- // Treat expired tokens the same as missing — clear from cache
841
- if (tcTokenBuffer?.length && isTcTokenExpired(existingTokenEntry?.timestamp)) {
842
- logger.debug({ jid: destinationJid, timestamp: existingTokenEntry?.timestamp }, 'tctoken expired, clearing');
843
- tcTokenBuffer = undefined;
844
- // Preserve senderTimestamp so the fire-and-forget issuance dedupe survives cleanup.
845
- const cleared = existingTokenEntry?.senderTimestamp !== undefined
846
- ? { token: Buffer.alloc(0), senderTimestamp: existingTokenEntry.senderTimestamp }
847
- : null;
848
- try {
849
- await authState.keys.set({ tctoken: { [tcTokenJid]: cleared } });
850
- }
851
- catch (err) {
852
- logger.debug({ jid: destinationJid, err: err?.message }, 'failed to persist tctoken expiry cleanup');
853
- }
854
- }
855
- if (tcTokenBuffer?.length && sock.serverProps.privacyTokenOn1to1) {
856
- ;
857
- stanza.content.push({
858
- tag: 'tctoken',
859
- attrs: {},
860
- content: tcTokenBuffer
861
- });
862
- }
863
- if (!didPushAdditional && additionalNodes && additionalNodes.length > 0) {
864
- ;
865
- stanza.content.push(...additionalNodes);
866
- }
867
- logger.debug({ msgId }, `sending message to ${participants.length} devices`);
868
- await sendNode(stanza);
869
- // Fire-and-forget: issue our token to the contact AFTER message send.
870
- // WA Web skips protocol messages and PSA/bot contacts (TcTokenChatAction: isRegularUser)
871
- const isProtocolMsg = !!normalizeMessageContent(message)?.protocolMessage;
872
- const isBotOrPSA = destinationJid === PSA_WID || isJidBot(destinationJid) || isJidMetaAI(destinationJid);
873
- if (is1on1Send &&
874
- !isProtocolMsg &&
875
- !isBotOrPSA &&
876
- shouldSendNewTcToken(existingTokenEntry?.senderTimestamp) &&
877
- !inFlightTcTokenIssuance.has(tcTokenJid)) {
878
- inFlightTcTokenIssuance.add(tcTokenJid);
879
- const issueTimestamp = unixTimestampSeconds();
880
- const getPNForLID = signalRepository.lidMapping.getPNForLID.bind(signalRepository.lidMapping);
881
- resolveIssuanceJid(destinationJid, sock.serverProps.lidTrustedTokenIssueToLid, getLIDForPN, getPNForLID)
882
- .then(issueJid => issuePrivacyTokens([issueJid], issueTimestamp))
883
- .then(async (result) => {
884
- await storeTcTokensFromIqResult({
885
- result,
886
- fallbackJid: tcTokenJid,
887
- keys: authState.keys,
888
- getLIDForPN
889
- });
890
- const currentData = await authState.keys.get('tctoken', [tcTokenJid]);
891
- const currentEntry = currentData[tcTokenJid];
892
- const indexWrite = await buildMergedTcTokenIndexWrite(authState.keys, [tcTokenJid]);
893
- await authState.keys.set({
894
- tctoken: {
895
- [tcTokenJid]: {
896
- token: Buffer.alloc(0),
897
- ...currentEntry,
898
- senderTimestamp: issueTimestamp
899
- },
900
- ...indexWrite
901
- }
902
- });
903
- })
904
- .catch(err => {
905
- logger.debug({ jid: destinationJid, err: err?.message }, 'fire-and-forget tctoken issuance failed');
906
- })
907
- .finally(() => {
908
- inFlightTcTokenIssuance.delete(tcTokenJid);
909
- });
910
- }
911
- // Add message to retry cache if enabled
912
- if (messageRetryManager && !participant) {
913
- messageRetryManager.addRecentMessage(destinationJid, msgId, message);
914
- }
915
- }, meId);
916
- return msgId;
917
- };
786
+ if (isGroup && regexGroupOld.test(jid) &&!message.reactionMessage) {
787
+ stanza.content.push({ tag: 'multicast', attrs: {} })
788
+ }
789
+ if (pollMessage || messages.eventMessage) {
790
+ stanza.content.push({
791
+ tag: 'meta',
792
+ attrs: messages.eventMessage
793
+ ? { event_type: 'creation' }
794
+ : isNewsletter
795
+ ? { polltype: 'creation', contenttype: pollMessage?.pollContentType === 2? 'image' : 'text' }
796
+ : { polltype: 'creation' }
797
+ })
798
+ }
799
+ if (!isNewsletter &&!isRetryResend && reportingMessage?.messageContextInfo?.messageSecret && shouldIncludeReportingToken(reportingMessage)) {
800
+ try {
801
+ const encoded = encodeWAMessage(reportingMessage)
802
+ const reportingKey = { id: msgId, fromMe: true, remoteJid: destinationJid, participant: participant?.jid }
803
+ const reportingNode = await getMessageReportingToken(encoded, reportingMessage, reportingKey)
804
+ if (reportingNode) {
805
+ stanza.content.push(reportingNode)
806
+ logger.trace({ jid }, 'added reporting token to message')
807
+ }
808
+ } catch (error) {
809
+ logger.warn({ jid, trace: error?.stack }, 'failed to attach reporting token')
810
+ }
811
+ }
812
+ let didPushAdditional = false
813
+ if (!isNewsletter && buttonType) {
814
+ const buttonsNode = getButtonArgs(messages)
815
+ const filteredButtons = getBinaryNodeFilter(additionalNodes? additionalNodes : [])
816
+ if (filteredButtons) {
817
+ stanza.content.push(...additionalNodes)
818
+ didPushAdditional = true
819
+ } else {
820
+ stanza.content.push(buttonsNode)
821
+ }
822
+ }
823
+ if (!aiLabel && isPnUser(destinationJid)) {
824
+ const alreadyHasBizBot = getBinaryFilteredBizBot(additionalNodes || []) || getBinaryFilteredBizBot(stanza.content)
825
+ if (!alreadyHasBizBot) stanza.content.push({ tag: 'bot', attrs: { biz_bot: '1' } })
826
+ } else if (aiLabel &&!isGroup &&!isStatus &&!isNewsletter) {
827
+ const existingBizBot = getBinaryFilteredBizBot(additionalNodes || [])
828
+ if (!existingBizBot) stanza.content.push({ tag: 'bot', attrs: { biz_bot: '1' } })
829
+ }
830
+ const isPeerMessage = additionalAttributes?.['category'] === 'peer'
831
+ const is1on1Send =!isGroup &&!isRetryResend &&!isStatus &&!isNewsletter &&!isPeerMessage
832
+ const tcTokenJid = is1on1Send? await resolveTcTokenJid(destinationJid, getLIDForPN) : destinationJid
833
+ const contactTcTokenData = is1on1Send? await authState.keys.get('tctoken', [tcTokenJid]) : {}
834
+ const existingTokenEntry = contactTcTokenData[tcTokenJid]
835
+ let tcTokenBuffer = existingTokenEntry?.token
836
+ if (tcTokenBuffer?.length && isTcTokenExpired(existingTokenEntry?.timestamp)) {
837
+ logger.debug({ jid: destinationJid, timestamp: existingTokenEntry?.timestamp }, 'tctoken expired, clearing')
838
+ tcTokenBuffer = undefined
839
+ const cleared = existingTokenEntry?.senderTimestamp!== undefined? { token: Buffer.alloc(0), senderTimestamp: existingTokenEntry.senderTimestamp } : null
840
+ try {
841
+ await authState.keys.set({ tctoken: { [tcTokenJid]: cleared } })
842
+ } catch (err) {
843
+ logger.debug({ jid: destinationJid, err: err?.message }, 'failed to persist tctoken expiry cleanup')
844
+ }
845
+ }
846
+ if (tcTokenBuffer?.length && sock.serverProps.privacyTokenOn1to1) {
847
+ stanza.content.push({ tag: 'tctoken', attrs: {}, content: tcTokenBuffer })
848
+ }
849
+ if (additionalNodes && additionalNodes.length > 0 &&!didPushAdditional) {
850
+ stanza.content.push(...additionalNodes)
851
+ }
852
+ logger.debug({ msgId }, `sending message to ${participants.length} devices`)
853
+ await sendNode(stanza)
854
+ if (message.messageContextInfo?.messageSecret) {
855
+ setBotMessageSecret(msgId, message.messageContextInfo.messageSecret, destinationJid)
856
+ }
857
+ const isProtocolMsg =!!normalizeMessageContent(message)?.protocolMessage
858
+ const isBotOrPSA = destinationJid === PSA_WID || isJidBot(destinationJid) || isJidMetaAI(destinationJid)
859
+ if (is1on1Send &&!isProtocolMsg &&!isBotOrPSA && shouldSendNewTcToken(existingTokenEntry?.senderTimestamp) &&!inFlightTcTokenIssuance.has(tcTokenJid)) {
860
+ inFlightTcTokenIssuance.add(tcTokenJid)
861
+ const issueTimestamp = unixTimestampSeconds()
862
+ const getPNForLID = signalRepository.lidMapping.getPNForLID.bind(signalRepository.lidMapping)
863
+ resolveIssuanceJid(destinationJid, sock.serverProps.lidTrustedTokenIssueToLid, getLIDForPN, getPNForLID)
864
+ .then(issueJid => issuePrivacyTokens([issueJid], issueTimestamp))
865
+ .then(async result => {
866
+ await storeTcTokensFromIqResult({ result, fallbackJid: tcTokenJid, keys: authState.keys, getLIDForPN })
867
+ const currentData = await authState.keys.get('tctoken', [tcTokenJid])
868
+ const currentEntry = currentData[tcTokenJid]
869
+ const indexWrite = await buildMergedTcTokenIndexWrite(authState.keys, [tcTokenJid])
870
+ await authState.keys.set({
871
+ tctoken: {
872
+ [tcTokenJid]: { token: Buffer.alloc(0),...currentEntry, senderTimestamp: issueTimestamp },
873
+ ...indexWrite
874
+ }
875
+ })
876
+ })
877
+ .catch(err => logger.debug({ jid: destinationJid, err: err?.message }, 'fire-and-forget tctoken issuance failed'))
878
+ .finally(() => inFlightTcTokenIssuance.delete(tcTokenJid))
879
+ }
880
+ if (messageRetryManager &&!participant) {
881
+ messageRetryManager.addRecentMessage(destinationJid, msgId, message)
882
+ }
883
+ if (isInterop &&!isRetryResend) {
884
+ await trustInteropContact(destinationJid).catch(err => logger.debug({ err, jid: destinationJid }, 'failed to trust interop contact'))
885
+ }
886
+ }, meId)
887
+ return msgId
888
+ }
918
889
  const getMessageType = (message) => {
919
890
  const normalizedMessage = normalizeMessageContent(message);
920
891
  if (!normalizedMessage)
@@ -924,8 +895,10 @@ export const makeMessagesSocket = (config) => {
924
895
  }
925
896
  if (normalizedMessage.pollCreationMessage ||
926
897
  normalizedMessage.pollCreationMessageV2 ||
927
- normalizedMessage.pollCreationMessageV3 ||
928
- normalizedMessage.pollUpdateMessage) {
898
+ normalizedMessage.pollCreationMessageV3 ||
899
+ normalizedMessage.pollCreationMessageV4 ||
900
+ normalizedMessage.pollCreationMessageV5 ||
901
+ normalizedMessage.pollUpdateMessage) {
929
902
  return 'poll';
930
903
  }
931
904
  if (normalizedMessage.eventMessage) {
@@ -1007,6 +980,9 @@ export const makeMessagesSocket = (config) => {
1007
980
  else if (message.interactiveMessage?.nativeFlowMessage?.buttons?.[0]?.name === 'payment_method') {
1008
981
  return 'payment_method'
1009
982
  }
983
+ else if (message.interactiveMessage?.nativeFlowMessage?.buttons?.[0]?.name === 'catalog_message') {
984
+ return 'catalog_message'
985
+ }
1010
986
  else if (message.interactiveMessage && message.interactiveMessage?.nativeFlowMessage) {
1011
987
  return 'interactive'
1012
988
  }
@@ -1014,6 +990,128 @@ export const makeMessagesSocket = (config) => {
1014
990
  return 'native_flow'
1015
991
  }
1016
992
  };
993
+ const getButtonArgs = (message) => {
994
+ const nativeFlow = message.interactiveMessage?.nativeFlowMessage
995
+ const firstButtonName = nativeFlow?.buttons?.[0]?.name
996
+ const nativeFlowSpecials = [
997
+ 'mpm',
998
+ 'cta_catalog',
999
+ 'send_location',
1000
+ 'call_permission_request',
1001
+ 'wa_payment_transaction_details',
1002
+ 'automated_greeting_message_view_catalog'
1003
+ ]
1004
+
1005
+ if (nativeFlow && (firstButtonName === 'review_and_pay' || firstButtonName === 'payment_info')) {
1006
+ return {
1007
+ tag: 'biz',
1008
+ attrs: {
1009
+ native_flow_name: firstButtonName === 'review_and_pay' ? 'order_details' : firstButtonName
1010
+ }
1011
+ }
1012
+ } else if (nativeFlow && nativeFlowSpecials.includes(firstButtonName)) {
1013
+ // Only works for WhatsApp Original, not WhatsApp Business
1014
+ return {
1015
+ tag: 'biz',
1016
+ attrs: {
1017
+ actual_actors: '2',
1018
+ host_storage: '2',
1019
+ privacy_mode_ts: Utils_1.unixTimestampSeconds().toString()
1020
+ },
1021
+ content: [
1022
+ {
1023
+ tag: 'interactive',
1024
+ attrs: {
1025
+ type: 'native_flow',
1026
+ v: '1'
1027
+ },
1028
+ content: [
1029
+ {
1030
+ tag: 'native_flow',
1031
+ attrs: {
1032
+ v: '2',
1033
+ name: firstButtonName
1034
+ }
1035
+ }
1036
+ ]
1037
+ },
1038
+ {
1039
+ tag: 'quality_control',
1040
+ attrs: {
1041
+ source_type: 'third_party'
1042
+ }
1043
+ }
1044
+ ]
1045
+ }
1046
+ } else if (nativeFlow || message.buttonsMessage) {
1047
+ // It works for whatsapp original and whatsapp business
1048
+ return {
1049
+ tag: 'biz',
1050
+ attrs: {
1051
+ actual_actors: '2',
1052
+ host_storage: '2',
1053
+ privacy_mode_ts: Utils_1.unixTimestampSeconds().toString()
1054
+ },
1055
+ content: [
1056
+ {
1057
+ tag: 'interactive',
1058
+ attrs: {
1059
+ type: 'native_flow',
1060
+ v: '1'
1061
+ },
1062
+ content: [
1063
+ {
1064
+ tag: 'native_flow',
1065
+ attrs: {
1066
+ v: '9',
1067
+ name: 'mixed'
1068
+ }
1069
+ }
1070
+ ]
1071
+ },
1072
+ {
1073
+ tag: 'quality_control',
1074
+ attrs: {
1075
+ source_type: 'third_party'
1076
+ }
1077
+ }
1078
+ ]
1079
+ }
1080
+ } else if (message.listMessage) {
1081
+ return {
1082
+ tag: 'biz',
1083
+ attrs: {
1084
+ actual_actors: '2',
1085
+ host_storage: '2',
1086
+ privacy_mode_ts: Utils_1.unixTimestampSeconds().toString()
1087
+ },
1088
+ content: [
1089
+ {
1090
+ tag: 'list',
1091
+ attrs: {
1092
+ v: '2',
1093
+ type: 'product_list'
1094
+ }
1095
+ },
1096
+ {
1097
+ tag: 'quality_control',
1098
+ attrs: {
1099
+ source_type: 'third_party'
1100
+ }
1101
+ }
1102
+ ]
1103
+ }
1104
+ } else {
1105
+ return {
1106
+ tag: 'biz',
1107
+ attrs: {
1108
+ actual_actors: '2',
1109
+ host_storage: '2',
1110
+ privacy_mode_ts: Utils_1.unixTimestampSeconds().toString()
1111
+ }
1112
+ }
1113
+ }
1114
+ }
1017
1115
  const issuePrivacyTokens = async (jids, timestamp) => {
1018
1116
  const t = (timestamp ?? unixTimestampSeconds()).toString();
1019
1117
  const result = await query({
@@ -1113,10 +1211,61 @@ export const makeMessagesSocket = (config) => {
1113
1211
  ev.emit('messages.update', [{ key: message.key, update: { message: message.message } }]);
1114
1212
  return message;
1115
1213
  },
1214
+ sendTable: async (jid, title, headers, rows, quoted, options = {}) => {
1215
+ const { message, messageId } = Utils_1.generateTableContent(title, headers, rows, quoted, options)
1216
+ await relayMessage(jid, message, { messageId })
1217
+ return { message, messageId }
1218
+ },
1219
+ sendList: async (jid, title, items, quoted, options = {}) => {
1220
+ const { message, messageId } = Utils_1.generateListContent(title, items, quoted, options)
1221
+ await relayMessage(jid, message, { messageId })
1222
+ return { message, messageId }
1223
+ },
1224
+ sendCodeBlock: async (jid, code, quoted, options = {}) => {
1225
+ const { message, messageId } = Utils_1.generateCodeBlockContent(code, quoted, options)
1226
+ await relayMessage(jid, message, { messageId })
1227
+ return { message, messageId }
1228
+ },
1229
+ sendLatex: async (jid, quoted, options) => {
1230
+ const { message, messageId } = Utils_1.generateLatexContent(quoted, options)
1231
+ await relayMessage(jid, message, { messageId })
1232
+ return { message, messageId }
1233
+ },
1234
+ sendLatexImage: async (jid, quoted, options, renderLatexToPng, uploadFn) => {
1235
+ const { message, messageId } = await Utils_1.generateLatexImageContent(
1236
+ quoted,
1237
+ options,
1238
+ uploadFn,
1239
+ renderLatexToPng
1240
+ )
1241
+ await relayMessage(jid, message, { messageId })
1242
+ return { message, messageId }
1243
+ },
1244
+ sendLatexInlineImage: async (jid, quoted, options, renderLatexToPng, uploadFn) => {
1245
+ const { message, messageId } = await Utils_1.generateLatexInlineImageContent(
1246
+ quoted,
1247
+ options,
1248
+ uploadFn,
1249
+ renderLatexToPng
1250
+ )
1251
+ await relayMessage(jid, message, { messageId })
1252
+ return { message, messageId }
1253
+ },
1254
+ captureUnifiedResponse: Utils_1.captureUnifiedResponse,
1255
+ sendUnifiedResponse: async (jid, quoted, captured) => {
1256
+ const { message, messageId } = Utils_1.generateUnifiedResponseContent(quoted, captured)
1257
+ await relayMessage(jid, message, { messageId })
1258
+ return { message, messageId }
1259
+ },
1260
+ sendRichMessage: async (jid, submessages, quoted, options = {}) => {
1261
+ const { message, messageId } = Utils_1.generateRichMessageContent(submessages, quoted, options)
1262
+ await relayMessage(jid, message, { messageId })
1263
+ return { message, messageId }
1264
+ },
1116
1265
  sendMessage: async (jid, content, options = {}) => {
1117
1266
  const userJid = authState.creds.me.id;
1118
1267
  const luki = new imup(Utils_1, waUploadToServer, relayMessage)
1119
- const { quoted } = options;
1268
+ const { quoted, participant = false } = options;
1120
1269
  const messageType = luki.detectType(content);
1121
1270
  if (typeof content === 'object' &&
1122
1271
  'disappearingMessagesInChat' in content &&
@@ -1144,13 +1293,7 @@ export const makeMessagesSocket = (config) => {
1144
1293
  return await relayMessage(jid, productMsg.message, {
1145
1294
  messageId: productMsg.key.id,
1146
1295
  });
1147
-
1148
- case 'INTERACTIVE':
1149
- const interactiveContent = await luki.handleInteractive(content, jid, quoted);
1150
- const interactiveMsg = await Utils_1.generateWAMessageFromContent(jid, interactiveContent, { quoted });
1151
- return await relayMessage(jid, interactiveMsg.message, {
1152
- messageId: interactiveMsg.key.id
1153
- });
1296
+
1154
1297
  case 'ALBUM':
1155
1298
  return await luki.handleAlbum(content, jid, quoted)
1156
1299
  case 'EVENT':
@@ -1230,7 +1373,8 @@ export const makeMessagesSocket = (config) => {
1230
1373
  useCachedGroupMetadata: options.useCachedGroupMetadata,
1231
1374
  additionalAttributes,
1232
1375
  statusJidList: options.statusJidList,
1233
- additionalNodes: aiLabel ? additionalNodes : options.additionalNodes
1376
+ additionalNodes: aiLabel ? additionalNodes : options.additionalNodes,
1377
+ participant
1234
1378
  });
1235
1379
  if (config.emitOwnEvents) {
1236
1380
  process.nextTick(async () => {