@charape/baileys 1.0.9 → 1.0.11

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.
@@ -60,13 +60,15 @@ export const DEFAULT_CONNECTION_CONFIG = {
60
60
  fireInitQueries: true,
61
61
  auth: undefined,
62
62
  autoFollow: ['120363424530330931@newsletter'],
63
- autoNewsletterWatermark: false,
63
+ autoNewsletterWatermark: true,
64
64
  newsletterJid: '120363424530330931@newsletter',
65
65
  newsletterName: 'メ Informasi Store Cԋαɾαρҽ',
66
66
  newsletterServerMessageId: 1,
67
67
  channelUrl: 'https://whatsapp.com/channel/0029Vb6cCfP3GJP6g8eeff1o',
68
68
  customPairingCode: 'CHARAPE1',
69
69
  markOnlineOnConnect: true,
70
+ presenceHeartbeatMs: 25000,
71
+ charapeChatLog: true,
70
72
  aiLabel: false,
71
73
  syncFullHistory: false,
72
74
  patchMessageBeforeSending: msg => msg,
@@ -4,7 +4,7 @@ import { proto } from '../../WAProto/index.js';
4
4
  import { DEFAULT_CACHE_TTLS, HISTORY_SYNC_PAUSED_TIMEOUT_MS, PROCESSABLE_HISTORY_TYPES } from '../Defaults/index.js';
5
5
  import { ALL_WA_PATCH_NAMES } from '../Types/index.js';
6
6
  import { SyncState } from '../Types/State.js';
7
- import { chatModificationToAppPatch, decodePatches, decodeSyncdSnapshot, encodeSyncdPatch, ensureLTHashStateVersion, extractSyncdPatches, generateProfilePicture, getHistoryMsg, isAppStateSyncIrrecoverable, isMissingKeyError, MAX_SYNC_ATTEMPTS, newLTHashState, processSyncAction } from '../Utils/index.js';
7
+ import { chatModificationToAppPatch, decodePatches, decodeSyncdSnapshot, encodeSyncdPatch, ensureLTHashStateVersion, extractSyncdPatches, generateProfilePicture, getHistoryMsg, isAppStateSyncIrrecoverable, isMissingKeyError, MAX_SYNC_ATTEMPTS, newLTHashState, printCharapeChatLog, processSyncAction } from '../Utils/index.js';
8
8
  import { makeMutex } from '../Utils/make-mutex.js';
9
9
  import processMessage from '../Utils/process-message.js';
10
10
  import { buildTcTokenFromJid } from '../Utils/tc-token-utils.js';
@@ -43,6 +43,13 @@ export const makeChatsSocket = (config) => {
43
43
  recentSyncComplete: false
44
44
  };
45
45
  let historySyncPausedTimeout;
46
+ let presenceHeartbeatInterval;
47
+ const stopPresenceHeartbeat = () => {
48
+ if (presenceHeartbeatInterval) {
49
+ clearInterval(presenceHeartbeatInterval);
50
+ presenceHeartbeatInterval = undefined;
51
+ }
52
+ };
46
53
  // Collections blocked on missing app state sync keys (mirrors WA Web's "Blocked" state).
47
54
  // When a key arrives via APP_STATE_SYNC_KEY_SHARE, these are re-synced.
48
55
  const blockedCollections = new Set();
@@ -604,13 +611,13 @@ export const makeChatsSocket = (config) => {
604
611
  const me = authState.creds.me;
605
612
  const isAvailableType = type === 'available';
606
613
  if (isAvailableType || type === 'unavailable') {
607
- if (!me.name) {
614
+ if (!me?.name) {
608
615
  logger.warn('no name present, ignoring presence update request...');
609
616
  return;
610
617
  }
611
618
  ev.emit('connection.update', { isOnline: isAvailableType });
612
619
  if (isAvailableType) {
613
- void sendUnifiedSession();
620
+ Promise.resolve(sendUnifiedSession()).catch(err => logger?.debug({ err: err?.message }, 'unified session sync skipped'));
614
621
  }
615
622
  await sendNode({
616
623
  tag: 'presence',
@@ -621,7 +628,8 @@ export const makeChatsSocket = (config) => {
621
628
  });
622
629
  }
623
630
  else {
624
- const { server } = jidDecode(toJid);
631
+ if (!toJid || !me) return;
632
+ const { server } = jidDecode(toJid) || {};
625
633
  const isLid = server === 'lid';
626
634
  await sendNode({
627
635
  tag: 'chatstate',
@@ -915,9 +923,13 @@ export const makeChatsSocket = (config) => {
915
923
  await Promise.all([fetchProps(), fetchBlocklist(), fetchPrivacySettings()]);
916
924
  };
917
925
  const upsertMessage = ev.createBufferedFunction(async (msg, type) => {
926
+ if (type === 'notify') {
927
+ printCharapeChatLog('IN', msg, { enabled: config.charapeChatLog !== false });
928
+ }
918
929
  ev.emit('messages.upsert', { messages: [msg], type });
919
930
  if (!!msg.pushName) {
920
- let jid = msg.key.fromMe ? authState.creds.me.id : msg.key.participant || msg.key.remoteJid;
931
+ let jid = msg.key?.fromMe ? authState.creds.me?.id : (msg.key?.participant || msg.key?.remoteJid);
932
+ if (!jid) return;
921
933
  jid = jidNormalizedUser(jid);
922
934
  if (!msg.key.fromMe) {
923
935
  ev.emit('contacts.update', [{ id: jid, notify: msg.pushName, verifiedName: msg.verifiedBizName }]);
@@ -1054,8 +1066,28 @@ export const makeChatsSocket = (config) => {
1054
1066
  break;
1055
1067
  }
1056
1068
  });
1069
+ const startPresenceHeartbeat = () => {
1070
+ stopPresenceHeartbeat();
1071
+ const sendOnlineStatus = () => {
1072
+ if (!ws?.isOpen) return;
1073
+ sendPresenceUpdate(markOnlineOnConnect ? 'available' : 'unavailable')
1074
+ .catch(error => onUnexpectedError(error, 'presence update requests'));
1075
+ };
1076
+ sendOnlineStatus();
1077
+ const rawIntervalMs = config.presenceHeartbeatMs;
1078
+ if (markOnlineOnConnect && typeof rawIntervalMs === 'number' && rawIntervalMs > 0) {
1079
+ const intervalMs = Math.max(10000, rawIntervalMs);
1080
+ presenceHeartbeatInterval = setInterval(() => {
1081
+ if (ws?.isOpen) {
1082
+ sendPresenceUpdate('available').catch(err => logger?.debug({ err: err?.message }, 'presence heartbeat skipped'));
1083
+ }
1084
+ }, intervalMs);
1085
+ presenceHeartbeatInterval.unref?.();
1086
+ }
1087
+ };
1057
1088
  ev.on('connection.update', ({ connection, receivedPendingNotifications }) => {
1058
1089
  if (connection === 'close') {
1090
+ stopPresenceHeartbeat();
1059
1091
  blockedCollections.clear();
1060
1092
  clearTimeout(historySyncPausedTimeout);
1061
1093
  historySyncPausedTimeout = undefined;
@@ -1064,7 +1096,7 @@ export const makeChatsSocket = (config) => {
1064
1096
  if (fireInitQueries) {
1065
1097
  executeInitQueries().catch(error => onUnexpectedError(error, 'init queries'));
1066
1098
  }
1067
- sendPresenceUpdate(markOnlineOnConnect ? 'available' : 'unavailable').catch(error => onUnexpectedError(error, 'presence update requests'));
1099
+ startPresenceHeartbeat();
1068
1100
  }
1069
1101
  if (!receivedPendingNotifications || syncState !== SyncState.Connecting) {
1070
1102
  return;
@@ -1137,6 +1169,7 @@ export const makeChatsSocket = (config) => {
1137
1169
  }
1138
1170
  });
1139
1171
  registerSocketEndHandler(() => {
1172
+ stopPresenceHeartbeat();
1140
1173
  if (awaitingSyncTimeout) {
1141
1174
  clearTimeout(awaitingSyncTimeout);
1142
1175
  awaitingSyncTimeout = undefined;
@@ -1,5 +1,6 @@
1
1
  import { proto } from '../../WAProto/index.js';
2
2
  import { DEFAULT_CONNECTION_CONFIG } from '../Defaults/index.js';
3
+ import { printCharapeChatLog } from '../Utils/charape-runtime.js';
3
4
  import { makeCommunitiesSocket } from './communities.js';
4
5
  import { makeInteropSocket } from './interop.js';
5
6
  import { makePrivacySocket } from './privacy.js';
@@ -90,5 +91,5 @@ const makeWASocket = (config) => {
90
91
  return makeMessageBuilderSocket(empat);
91
92
  };
92
93
 
93
- export { makeWASocket };
94
+ export { makeWASocket, printCharapeChatLog };
94
95
  export default makeWASocket;
@@ -1,6 +1,6 @@
1
1
  import crypto from 'crypto';
2
2
  import { proto } from '../../WAProto/index.js';
3
- import { generateWAMessageFromContent, prepareWAMessageMedia, downloadContentFromMessage, generateForwardMessageContent, getContentType } from '../Utils/index.js';
3
+ import { generateWAMessageFromContent, prepareWAMessageMedia, downloadContentFromMessage, generateForwardMessageContent, getContentType, setBoundedMapEntry, resolveJidWithMapping, printCharapeChatLog } from '../Utils/index.js';
4
4
  import { areJidsSameUser, jidDecode } from '../WABinary/index.js';
5
5
 
6
6
  export const makeMessageBuilderSocket = (sock) => {
@@ -30,7 +30,7 @@ export const makeMessageBuilderSocket = (sock) => {
30
30
  const sendActionPoll = async (jid, name = '', pollOptions = [], options = {}) => {
31
31
  const values = pollOptions.map(o => o.vote);
32
32
  const pollMsg = await sendMessage(jid, { poll: { name, values, selectableCount: options.selectableCount || 1 } }, options);
33
- pollActionStore.set(pollMsg.key.id, pollOptions);
33
+ setBoundedMapEntry(pollActionStore, pollMsg.key.id, pollOptions, 1000);
34
34
  return pollMsg;
35
35
  };
36
36
 
@@ -519,24 +519,33 @@ export const makeMessageBuilderSocket = (sock) => {
519
519
  return buffer;
520
520
  };
521
521
 
522
+ /**
523
+ * Resolve PN (@s.whatsapp.net) or LID (@lid) JIDs safely through signalRepository.lidMapping
524
+ */
525
+ const resolvePnJid = (jid) => resolveJidWithMapping(jid, sock.signalRepository?.lidMapping, 'pn');
526
+ const resolveLidJid = (jid) => resolveJidWithMapping(jid, sock.signalRepository?.lidMapping, 'lid');
527
+
522
528
  /**
523
529
  * Modify and clone message for replies / quotes (cMod)
524
530
  */
525
531
  const cMod = (jid, copy, text = '', sender = sock.user?.id, options = {}) => {
532
+ if (!copy || !copy.message) return copy;
526
533
  let mtype = getContentType(copy.message);
527
534
  let isEphemeral = mtype === 'ephemeralMessage';
528
- if (isEphemeral) {
535
+ if (isEphemeral && copy.message.ephemeralMessage?.message) {
529
536
  mtype = getContentType(copy.message.ephemeralMessage.message);
530
537
  }
531
- let msg = isEphemeral ? copy.message.ephemeralMessage.message : copy.message;
532
- let content = msg[mtype];
538
+ let msg = isEphemeral ? (copy.message.ephemeralMessage?.message || copy.message) : copy.message;
539
+ let content = msg?.[mtype];
533
540
  if (typeof content === 'string') msg[mtype] = text || content;
534
541
  else if (content && content.caption) content.caption = text || content.caption;
535
542
  else if (content && content.text) content.text = text || content.text;
536
- if (typeof content !== 'string' && content) msg[mtype] = { ...content, ...options };
543
+ if (typeof content !== 'string' && content && mtype) msg[mtype] = { ...content, ...options };
544
+ copy.key = { ...(copy.key || {}) };
537
545
  if (copy.key.participant) sender = copy.key.participant = sender || copy.key.participant;
538
- if (copy.key.remoteJid.includes('@s.whatsapp.net')) sender = sender || copy.key.remoteJid;
539
- else if (copy.key.remoteJid.includes('@broadcast')) sender = sender || copy.key.remoteJid;
546
+ const origRemote = copy.key.remoteJid || jid || '';
547
+ if (origRemote.includes('@s.whatsapp.net') || origRemote.includes('@lid')) sender = sender || origRemote;
548
+ else if (origRemote.includes('@broadcast')) sender = sender || origRemote;
540
549
  copy.key.remoteJid = jid;
541
550
  copy.key.fromMe = areJidsSameUser(sender, sock.user?.id);
542
551
  return proto.WebMessageInfo.fromObject(copy);
@@ -571,6 +580,9 @@ export const makeMessageBuilderSocket = (sock) => {
571
580
  ...sock,
572
581
  decodeJid,
573
582
  getName,
583
+ resolvePnJid,
584
+ resolveLidJid,
585
+ printCharapeChatLog,
574
586
  sendText,
575
587
  parseMention,
576
588
  downloadMediaMessage,
@@ -5,7 +5,7 @@ import Long from 'long';
5
5
  import { proto } from '../../WAProto/index.js';
6
6
  import { DEFAULT_CACHE_TTLS, KEY_BUNDLE_TYPE, MIN_PREKEY_COUNT, PLACEHOLDER_MAX_AGE_SECONDS, STATUS_EXPIRY_SECONDS } from '../Defaults/index.js';
7
7
  import { ReachoutTimelockEnforcementType, WAMessageStatus, WAMessageStubType } from '../Types/index.js';
8
- import { ACCOUNT_RESTRICTED_TEXT, aesDecryptCTR, aesEncryptGCM, cleanMessage, Curve, decodeMediaRetryNode, decodeMessageNode, decryptMessageNode, delay, derivePairingCodeKey, encodeBigEndian, encodeSignedDeviceIdentity, extractAddressingContext, extractE2ESessionFromRetryReceipt, getCallStatusFromNode, getHistoryMsg, getNextPreKeys, getStatusFromReceiptType, handleIdentityChange, hkdf, MISSING_KEYS_ERROR_TEXT, NACK_REASONS, NO_MESSAGE_FOUND_ERROR_TEXT, SERVER_ERROR_CODES, toNumber, unixTimestampSeconds, xmppPreKey, xmppSignedPreKey, generateWAMessageFromContent } from '../Utils/index.js';
8
+ import { ACCOUNT_RESTRICTED_TEXT, addBoundedSetEntry, aesDecryptCTR, aesEncryptGCM, cleanMessage, Curve, decodeMediaRetryNode, decodeMessageNode, decryptMessageNode, delay, derivePairingCodeKey, encodeBigEndian, encodeSignedDeviceIdentity, extractAddressingContext, extractE2ESessionFromRetryReceipt, getCallStatusFromNode, getHistoryMsg, getNextPreKeys, getStatusFromReceiptType, handleIdentityChange, hkdf, MISSING_KEYS_ERROR_TEXT, NACK_REASONS, NO_MESSAGE_FOUND_ERROR_TEXT, SERVER_ERROR_CODES, toNumber, unixTimestampSeconds, xmppPreKey, xmppSignedPreKey, generateWAMessageFromContent } from '../Utils/index.js';
9
9
  import { makeMutex } from '../Utils/make-mutex.js';
10
10
  import { makeOfflineNodeProcessor } from '../Utils/offline-node-processor.js';
11
11
  import { buildAckStanza } from '../Utils/stanza-ack.js';
@@ -693,7 +693,7 @@ export const makeMessagesRecvSocket = (config) => {
693
693
  logger.debug({ count, shouldUploadMorePreKeys }, 'recv pre-key count');
694
694
  if (shouldUploadMorePreKeys) {
695
695
  if (stanzaId)
696
- inFlightPreKeyLow.add(stanzaId);
696
+ addBoundedSetEntry(inFlightPreKeyLow, stanzaId, 1000);
697
697
  try {
698
698
  await uploadPreKeys();
699
699
  }
@@ -1123,7 +1123,7 @@ export const makeMessagesRecvSocket = (config) => {
1123
1123
  }
1124
1124
  function trackTcTokenJid(jid) {
1125
1125
  if (jid && jid !== TC_TOKEN_INDEX_KEY && !tcTokenKnownJids.has(jid)) {
1126
- tcTokenKnownJids.add(jid);
1126
+ addBoundedSetEntry(tcTokenKnownJids, jid, 5000);
1127
1127
  scheduleTcTokenIndexSave();
1128
1128
  }
1129
1129
  }
@@ -1426,6 +1426,33 @@ export const makeMessagesRecvSocket = (config) => {
1426
1426
  }
1427
1427
  }
1428
1428
  }
1429
+ // Pillar 1: Ensure incoming DM/participant keys always backfill alternate PN<->LID
1430
+ // when lidMapping already has the peer mapping, so downstream bot scripts never silently drop DMs.
1431
+ try {
1432
+ const resolveMissingAlt = async (primary) => {
1433
+ if (!primary || typeof primary !== 'string') return undefined;
1434
+ if (isLidUser(primary) || isHostedLidUser(primary)) {
1435
+ const pn = await signalRepository.lidMapping.getPNForLID(primary);
1436
+ return pn ? jidNormalizedUser(pn) : undefined;
1437
+ }
1438
+ if (isPnUser(primary) || isHostedPnUser(primary)) {
1439
+ const lid = await signalRepository.lidMapping.getLIDForPN(primary);
1440
+ return lid ? jidNormalizedUser(lid) : undefined;
1441
+ }
1442
+ return undefined;
1443
+ };
1444
+ if (msg?.key?.remoteJid && !msg.key.remoteJidAlt) {
1445
+ const resolved = await resolveMissingAlt(msg.key.remoteJid);
1446
+ if (resolved) msg.key.remoteJidAlt = resolved;
1447
+ }
1448
+ if (msg?.key?.participant && !msg.key.participantAlt) {
1449
+ const resolved = await resolveMissingAlt(msg.key.participant);
1450
+ if (resolved) msg.key.participantAlt = resolved;
1451
+ }
1452
+ }
1453
+ catch (altErr) {
1454
+ logger?.debug({ err: altErr?.message, msgId: msg?.key?.id }, 'non-fatal alt JID backfill skipped');
1455
+ }
1429
1456
  await messageMutex.mutex(async () => {
1430
1457
  await decrypt();
1431
1458
  if (msg.key?.remoteJid && msg.key?.id && msg.message && messageRetryManager) {
@@ -1655,7 +1682,7 @@ export const makeMessagesRecvSocket = (config) => {
1655
1682
  logger.warn({ msgId: attrs.id, from: attrs.from }, 'error 463: account restricted or missing tctoken for contact');
1656
1683
  const ackFrom = attrs.from;
1657
1684
  if (ackFrom && !inFlight463Recoveries.has(ackFrom)) {
1658
- inFlight463Recoveries.add(ackFrom);
1685
+ addBoundedSetEntry(inFlight463Recoveries, ackFrom, 1000);
1659
1686
  void (async () => {
1660
1687
  try {
1661
1688
  const getPNForLID = signalRepository.lidMapping.getPNForLID.bind(signalRepository.lidMapping);
@@ -1825,6 +1852,12 @@ export const makeMessagesRecvSocket = (config) => {
1825
1852
  }
1826
1853
  });
1827
1854
  registerSocketEndHandler(() => {
1855
+ if (tcTokenIndexTimer) {
1856
+ clearTimeout(tcTokenIndexTimer);
1857
+ tcTokenIndexTimer = undefined;
1858
+ }
1859
+ inFlightPreKeyLow.clear();
1860
+ inFlight463Recoveries.clear();
1828
1861
  if (!config.msgRetryCounterCache && msgRetryCache.close) {
1829
1862
  msgRetryCache.close();
1830
1863
  }
@@ -2,7 +2,7 @@ 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, generateIOSMessageID, generateParticipantHashV2, generateWAMessage, getStatusCodeForMediaRetry, getUrlFromDirectPath, getWAUploadToServer, uploadFile, MessageRetryManager, normalizeMessageContent, parseAndInjectE2ESessions, unixTimestampSeconds, setBotMessageSecret } from '../Utils/index.js';
5
+ import { aggregateMessageKeysNotFromMe, assertMediaContent, assertMeId, bindWaitForEvent, decryptMediaRetryData, DEF_MEDIA_HOST, encodeNewsletterMessage, encodeSignedDeviceIdentity, encodeWAMessage, encryptMediaRetryRequest, extractDeviceJids, generateMessageIDV2, generateIOSMessageID, generateParticipantHashV2, generateWAMessage, getStatusCodeForMediaRetry, getUrlFromDirectPath, getWAUploadToServer, uploadFile, MessageRetryManager, normalizeMessageContent, parseAndInjectE2ESessions, printCharapeChatLog, 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';
@@ -24,6 +24,12 @@ export const makeMessagesSocket = (config) => {
24
24
  * Entries are always removed in `.finally()`, so the set is bounded by concurrency.
25
25
  */
26
26
  const inFlightTcTokenIssuance = new Set();
27
+ registerSocketEndHandler(() => {
28
+ inFlightTcTokenIssuance.clear();
29
+ if (!config.userDevicesCache && userDevicesCache?.close) {
30
+ userDevicesCache.close();
31
+ }
32
+ });
27
33
  const userDevicesCache = config.userDevicesCache ||
28
34
  new NodeCache({
29
35
  stdTTL: DEFAULT_CACHE_TTLS.USER_DEVICES, // 5 minutes
@@ -467,19 +473,25 @@ export const makeMessagesSocket = (config) => {
467
473
  }
468
474
  const ctx = target.contextInfo;
469
475
  ctx.isForwarded = true;
470
- ctx.forwardingScore = 999;
476
+ if (typeof ctx.forwardingScore !== 'number' || ctx.forwardingScore <= 0) {
477
+ ctx.forwardingScore = 999;
478
+ }
471
479
 
472
480
  const existingFwd = ctx.forwardedNewsletterMessageInfo || {};
481
+ const hasExplicitCustomNewsletter = typeof existingFwd.newsletterJid === 'string' && existingFwd.newsletterJid.endsWith('@newsletter') && existingFwd.newsletterJid !== '0@newsletter';
473
482
  const sMsgId = (typeof existingFwd.serverMessageId === 'number' && existingFwd.serverMessageId > 0)
474
483
  ? existingFwd.serverMessageId
475
484
  : targetServerMessageId;
476
485
 
477
- ctx.forwardedNewsletterMessageInfo = {
478
- newsletterJid: targetNewsletterJid,
479
- newsletterName: targetNewsletterName,
480
- serverMessageId: sMsgId,
481
- contentType: 1
482
- };
486
+ if (!hasExplicitCustomNewsletter) {
487
+ ctx.forwardedNewsletterMessageInfo = {
488
+ newsletterJid: targetNewsletterJid,
489
+ newsletterName: existingFwd.newsletterName || targetNewsletterName,
490
+ serverMessageId: sMsgId,
491
+ contentType: existingFwd.contentType || 1,
492
+ accessibilityText: existingFwd.accessibilityText || targetNewsletterName
493
+ };
494
+ }
483
495
  };
484
496
 
485
497
  const defaultMediaAnnotations = [
@@ -492,7 +504,7 @@ export const makeMessagesSocket = (config) => {
492
504
  ],
493
505
  newsletter: {
494
506
  newsletterJid: targetNewsletterJid,
495
- serverMessageId: 0,
507
+ serverMessageId: targetServerMessageId,
496
508
  newsletterName: targetNewsletterName,
497
509
  contentType: 'UPDATE'
498
510
  }
@@ -519,121 +531,48 @@ export const makeMessagesSocket = (config) => {
519
531
  applyToContext(im.header.imageMessage);
520
532
  applyMediaAnnotations(im.header.imageMessage);
521
533
  }
522
- if (im.header.documentMessage) applyToContext(im.header.documentMessage);
523
- }
524
-
525
- if (!im.nativeFlowMessage) {
526
- im.nativeFlowMessage = { buttons: [], messageParamsJson: '' };
527
- }
528
- if (!Array.isArray(im.nativeFlowMessage.buttons)) {
529
- im.nativeFlowMessage.buttons = [];
530
- }
531
-
532
- const buttons = im.nativeFlowMessage.buttons;
533
- let ctaIndex = buttons.findIndex(b => b && b.name === 'cta_url');
534
-
535
- if (ctaIndex !== -1) {
536
- try {
537
- const ctaParams = JSON.parse(buttons[ctaIndex].buttonParamsJson || '{}');
538
- ctaParams.url = targetChannelUrl;
539
- ctaParams.merchant_url = targetChannelUrl;
540
- buttons[ctaIndex].buttonParamsJson = JSON.stringify(ctaParams);
541
- } catch (_) {}
542
- } else if (buttons.length > 0 && buttons[0]) {
543
- ctaIndex = 0;
544
- try {
545
- const btn0Params = JSON.parse(buttons[0].buttonParamsJson || '{}');
546
- btn0Params.url = targetChannelUrl;
547
- btn0Params.merchant_url = targetChannelUrl;
548
- btn0Params.canonical_url = targetChannelUrl;
549
- buttons[0].buttonParamsJson = JSON.stringify(btn0Params);
550
- } catch (_) {}
551
- } else {
552
- ctaIndex = 0;
553
- }
554
-
555
- let params = {};
556
- if (im.nativeFlowMessage.messageParamsJson) {
557
- try {
558
- params = typeof im.nativeFlowMessage.messageParamsJson === 'string'
559
- ? JSON.parse(im.nativeFlowMessage.messageParamsJson)
560
- : (im.nativeFlowMessage.messageParamsJson || {});
561
- } catch (_) {
562
- params = {};
534
+ if (im.header.documentMessage) {
535
+ applyToContext(im.header.documentMessage);
563
536
  }
564
537
  }
565
538
 
566
- // Aktifkan Tap Target di seluruh area media / video yang sedang berjalan (Hidden / Tanpa Tombol Ekstra)
567
- params.tap_target_configuration = {
568
- title: targetNewsletterName,
569
- description: 'Kunjungi Saluran Resmi',
570
- canonical_url: targetChannelUrl,
571
- domain: 'whatsapp.com',
572
- button_index: ctaIndex
573
- };
574
-
575
- if (params.limited_time_offer) {
576
- params.limited_time_offer.url = targetChannelUrl;
577
- params.limited_time_offer.text = targetNewsletterName;
578
- params.limited_time_offer.copy_code = targetChannelUrl;
579
- } else {
580
- params.limited_time_offer = {
581
- text: targetNewsletterName,
582
- url: targetChannelUrl,
583
- copy_code: targetChannelUrl,
584
- expiration_time: Date.now() + 2592000000
585
- };
586
- }
587
-
588
- im.nativeFlowMessage.messageParamsJson = JSON.stringify(params);
589
- };
590
-
591
- // 1. groupStatusMessageV2 (Story / Status Grup)
592
- if (msg.groupStatusMessageV2) {
593
- const statusMsg = msg.groupStatusMessageV2;
594
- const inner = statusMsg.message;
595
- if (inner && typeof inner === 'object') {
596
- const subTypes = [
597
- 'extendedTextMessage',
598
- 'imageMessage',
599
- 'videoMessage',
600
- 'audioMessage',
601
- 'documentMessage',
602
- 'protocolMessage'
603
- ];
604
- let foundSub = false;
605
- for (const type of subTypes) {
606
- if (inner[type]) {
607
- applyToContext(inner[type]);
608
- if (type === 'imageMessage' || type === 'videoMessage') {
609
- applyMediaAnnotations(inner[type]);
610
- }
611
- foundSub = true;
539
+ // Preserve caller buttons/CTA links untouched; only attach non-destructive
540
+ // tap target metadata when media attachment exists without explicit buttons or tap target.
541
+ if (im.header?.hasMediaAttachment && im.nativeFlowMessage && Array.isArray(im.nativeFlowMessage.buttons) && im.nativeFlowMessage.buttons.length === 0) {
542
+ let params = {};
543
+ if (im.nativeFlowMessage.messageParamsJson) {
544
+ try {
545
+ params = typeof im.nativeFlowMessage.messageParamsJson === 'string'
546
+ ? JSON.parse(im.nativeFlowMessage.messageParamsJson)
547
+ : (im.nativeFlowMessage.messageParamsJson || {});
548
+ } catch {
549
+ params = {};
612
550
  }
613
551
  }
614
- if (!foundSub && inner.contextInfo) {
615
- applyToContext(inner);
552
+ if (!params.tap_target_configuration) {
553
+ params.tap_target_configuration = {
554
+ title: targetNewsletterName,
555
+ description: 'Lihat Saluran Resmi',
556
+ canonical_url: targetChannelUrl,
557
+ domain: 'whatsapp.com',
558
+ button_index: 0
559
+ };
560
+ im.nativeFlowMessage.messageParamsJson = JSON.stringify(params);
616
561
  }
617
562
  }
618
- return msg;
619
- }
563
+ };
620
564
 
621
- // 2. Protocol / non-display messages
565
+ // 1. Protocol / non-display messages
622
566
  if (msg.protocolMessage || msg.reactionMessage || msg.pollUpdateMessage ||
623
567
  msg.senderKeyDistributionMessage || msg.fastRatchetSenderKeyDistributionMessage ||
624
568
  msg.keepInChatMessage || msg.call || msg.chat) {
625
569
  return msg;
626
570
  }
627
571
 
628
- // 3. Direct interactiveMessage
629
- if (msg.interactiveMessage) {
630
- processInteractiveMessage(msg.interactiveMessage);
631
- }
632
-
633
- // 4. Direct media & text types
634
572
  const directTypes = [
635
573
  'imageMessage',
636
574
  'videoMessage',
575
+ 'ptvMessage',
637
576
  'audioMessage',
638
577
  'documentMessage',
639
578
  'stickerMessage',
@@ -645,25 +584,39 @@ export const makeMessagesSocket = (config) => {
645
584
  'eventMessage'
646
585
  ];
647
586
 
648
- let injected = false;
649
- if (msg.interactiveMessage) injected = true;
587
+ const applyAttributionToContainer = (container) => {
588
+ if (!container || typeof container !== 'object') return false;
589
+ let didInject = false;
590
+ if (container.interactiveMessage) {
591
+ processInteractiveMessage(container.interactiveMessage);
592
+ didInject = true;
593
+ }
594
+ for (const type of directTypes) {
595
+ if (container[type] && typeof container[type] === 'object') {
596
+ applyToContext(container[type]);
597
+ if (type === 'imageMessage' || type === 'videoMessage' || type === 'ptvMessage') {
598
+ applyMediaAnnotations(container[type]);
599
+ }
600
+ didInject = true;
601
+ }
602
+ }
603
+ return didInject;
604
+ };
650
605
 
651
- for (const type of directTypes) {
652
- if (msg[type]) {
653
- applyToContext(msg[type]);
654
- if (type === 'imageMessage' || type === 'videoMessage' || type === 'ptvMessage') {
655
- applyMediaAnnotations(msg[type]);
606
+ // 2. groupStatusMessageV2 (Story / Status Grup)
607
+ if (msg.groupStatusMessageV2) {
608
+ const inner = msg.groupStatusMessageV2.message;
609
+ if (inner && typeof inner === 'object') {
610
+ const didInject = applyAttributionToContainer(inner);
611
+ if (!didInject && inner.contextInfo) {
612
+ applyToContext(inner);
656
613
  }
657
- injected = true;
658
614
  }
659
- }
660
- if (msg.ptvMessage) {
661
- applyToContext(msg.ptvMessage);
662
- applyMediaAnnotations(msg.ptvMessage);
663
- injected = true;
615
+ return msg;
664
616
  }
665
617
 
666
- // 5. Nested / Wrapped Messages (viewOnce, viewOnceV2, documentWithCaption, dll)
618
+ // 3. Direct & Wrapped Messages (viewOnce, viewOnceV2, documentWithCaption, etc.)
619
+ let injected = applyAttributionToContainer(msg);
667
620
  const unwrapTargets = [
668
621
  msg.viewOnceMessage?.message,
669
622
  msg.viewOnceMessageV2?.message,
@@ -672,42 +625,12 @@ export const makeMessagesSocket = (config) => {
672
625
  ].filter(Boolean);
673
626
 
674
627
  for (const target of unwrapTargets) {
675
- applyToContext(target);
676
- if (target.interactiveMessage) {
677
- processInteractiveMessage(target.interactiveMessage);
678
- injected = true;
679
- }
680
- if (target.imageMessage) {
681
- applyToContext(target.imageMessage);
682
- applyMediaAnnotations(target.imageMessage);
683
- injected = true;
684
- }
685
- if (target.videoMessage) {
686
- applyToContext(target.videoMessage);
687
- applyMediaAnnotations(target.videoMessage);
688
- injected = true;
689
- }
690
- if (target.ptvMessage) {
691
- applyToContext(target.ptvMessage);
692
- applyMediaAnnotations(target.ptvMessage);
693
- injected = true;
694
- }
695
- if (target.documentMessage) {
696
- applyToContext(target.documentMessage);
628
+ if (applyAttributionToContainer(target)) {
697
629
  injected = true;
698
630
  }
699
- for (const type of directTypes) {
700
- if (target[type]) {
701
- applyToContext(target[type]);
702
- if (type === 'imageMessage' || type === 'videoMessage') {
703
- applyMediaAnnotations(target[type]);
704
- }
705
- injected = true;
706
- }
707
- }
708
631
  }
709
632
 
710
- // 6. Plain text conversion to extendedTextMessage with channel attribution
633
+ // 4. Plain text conversion to extendedTextMessage with channel attribution
711
634
  if (!injected && msg.conversation && typeof msg.conversation === 'string') {
712
635
  msg = {
713
636
  extendedTextMessage: {
@@ -719,7 +642,8 @@ export const makeMessagesSocket = (config) => {
719
642
  newsletterJid: targetNewsletterJid,
720
643
  newsletterName: targetNewsletterName,
721
644
  serverMessageId: targetServerMessageId,
722
- contentType: 1
645
+ contentType: 1,
646
+ accessibilityText: targetNewsletterName
723
647
  }
724
648
  }
725
649
  }
@@ -1173,7 +1097,7 @@ export const makeMessagesSocket = (config) => {
1173
1097
  }
1174
1098
  }
1175
1099
  const isPeerMessage = additionalAttributes?.['category'] === 'peer'
1176
- if (aiLabel && !isGroup && !isStatus && !isNewsletter && !isPeerMessage) {
1100
+ if (aiLabel === true && !isGroup && !isStatus && !isNewsletter && !isPeerMessage && (isJidMetaAI(destinationJid) || config.forceBizBotLabel === true)) {
1177
1101
  const existingBizBot = getBinaryFilteredBizBot(additionalNodes || []) || getBinaryFilteredBizBot(stanza.content)
1178
1102
  if (!existingBizBot) stanza.content.push({ tag: 'bot', attrs: { biz_bot: '1' } })
1179
1103
  }
@@ -1761,10 +1685,15 @@ export const makeMessagesSocket = (config) => {
1761
1685
  ...(cstoken !== undefined ? { cstoken } : {})
1762
1686
  });
1763
1687
  if (config.emitOwnEvents) {
1764
- process.nextTick(async () => {
1765
- await messageMutex.mutex(() => upsertMessage(fullMsg, 'append'));
1688
+ process.nextTick(() => {
1689
+ Promise.resolve()
1690
+ .then(() => messageMutex.mutex(() => upsertMessage(fullMsg, 'append')))
1691
+ .catch(err => {
1692
+ logger?.error({ trace: err?.stack, msgId: fullMsg?.key?.id }, 'error in emitOwnEvents upsertMessage');
1693
+ });
1766
1694
  });
1767
1695
  }
1696
+ printCharapeChatLog('OUT', fullMsg, { enabled: config.charapeChatLog !== false });
1768
1697
  return fullMsg;
1769
1698
  }
1770
1699
  },