@blckrose/baileys 1.0.0 → 1.1.1

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.
@@ -6,6 +6,64 @@ import { makeChatsSocket } from './chats.js';
6
6
  export const makeGroupsSocket = (config) => {
7
7
  const sock = makeChatsSocket(config);
8
8
  const { authState, ev, query, upsertMessage } = sock;
9
+ const { cachedGroupMetadata } = config;
10
+ // ── Built-in group metadata cache ─────────────────────────────────────────
11
+ const groupMetadataCache = new Map();
12
+ const GROUP_CACHE_TTL = (config.groupCacheTTL || 5) * 60 * 1000; // default 5 menit
13
+ const getCachedGroupMetadata = async (jid) => {
14
+ // 1. Cek user-provided cachedGroupMetadata (dari config makeWASocket)
15
+ if (cachedGroupMetadata) {
16
+ const cached = await cachedGroupMetadata(jid);
17
+ if (cached && Array.isArray(cached.participants)) return cached;
18
+ }
19
+ // 2. Cek internal Map cache
20
+ const entry = groupMetadataCache.get(jid);
21
+ if (entry && Date.now() - entry.ts < GROUP_CACHE_TTL) {
22
+ return entry.data;
23
+ }
24
+ return undefined;
25
+ };
26
+ const setCachedGroupMetadata = (jid, data) => {
27
+ groupMetadataCache.set(jid, { data, ts: Date.now() });
28
+ };
29
+ // Update cache saat groups.update event
30
+ ev.on('groups.update', (updates) => {
31
+ for (const update of updates) {
32
+ const entry = groupMetadataCache.get(update.id);
33
+ if (entry) {
34
+ // Merge update ke cache yang ada
35
+ groupMetadataCache.set(update.id, {
36
+ data: { ...entry.data, ...update },
37
+ ts: entry.ts
38
+ });
39
+ }
40
+ }
41
+ });
42
+ // Update cache saat participant berubah
43
+ ev.on('group-participants.update', ({ id, participants, action }) => {
44
+ const entry = groupMetadataCache.get(id);
45
+ if (!entry) return;
46
+ const meta = { ...entry.data };
47
+ if (!Array.isArray(meta.participants)) return;
48
+ if (action === 'add') {
49
+ const existing = new Set(meta.participants.map(p => p.id));
50
+ for (const jid of participants) {
51
+ if (!existing.has(jid)) meta.participants.push({ id: jid, admin: null });
52
+ }
53
+ } else if (action === 'remove') {
54
+ meta.participants = meta.participants.filter(p => !participants.includes(p.id));
55
+ } else if (action === 'promote') {
56
+ meta.participants = meta.participants.map(p =>
57
+ participants.includes(p.id) ? { ...p, admin: 'admin' } : p
58
+ );
59
+ } else if (action === 'demote') {
60
+ meta.participants = meta.participants.map(p =>
61
+ participants.includes(p.id) ? { ...p, admin: null } : p
62
+ );
63
+ }
64
+ groupMetadataCache.set(id, { data: meta, ts: entry.ts });
65
+ });
66
+ // ── End group metadata cache ───────────────────────────────────────────────
9
67
  const groupQuery = async (jid, type, content) => query({
10
68
  tag: 'iq',
11
69
  attrs: {
@@ -16,8 +74,15 @@ export const makeGroupsSocket = (config) => {
16
74
  content
17
75
  });
18
76
  const groupMetadata = async (jid) => {
77
+ // Cek cache dulu sebelum hit network
78
+ const cached = await getCachedGroupMetadata(jid);
79
+ if (cached) return cached;
80
+ // Fetch dari WA
19
81
  const result = await groupQuery(jid, 'get', [{ tag: 'query', attrs: { request: 'interactive' } }]);
20
- return extractGroupMetadata(result);
82
+ const meta = extractGroupMetadata(result);
83
+ // Simpan ke cache
84
+ setCachedGroupMetadata(jid, meta);
85
+ return meta;
21
86
  };
22
87
  const groupFetchAllParticipating = async () => {
23
88
  const result = await query({
@@ -312,11 +377,17 @@ export const extractGroupMetadata = (result) => {
312
377
  joinApprovalMode: !!getBinaryNodeChild(group, 'membership_approval_mode'),
313
378
  memberAddMode,
314
379
  participants: getBinaryNodeChildren(group, 'participant').map(({ attrs }) => {
315
- // TODO: Store LID MAPPINGS
380
+ const isLid = isLidUser(attrs.jid);
381
+ const pn = attrs.phone_number;
382
+ const hasPn = isPnUser(pn);
383
+ // Jika grup pakai LID addressing:
384
+ // - id → pakai phoneNumber (PN) agar bisa dicompare dengan m.sender
385
+ // - lid → simpan LID asli
386
+ // Jika grup pakai PN addressing: id tetap PN
316
387
  return {
317
- id: attrs.jid,
318
- phoneNumber: isLidUser(attrs.jid) && isPnUser(attrs.phone_number) ? attrs.phone_number : undefined,
319
- lid: isPnUser(attrs.jid) && isLidUser(attrs.lid) ? attrs.lid : undefined,
388
+ id: isLid && hasPn ? pn : attrs.jid,
389
+ phoneNumber: isLid && hasPn ? pn : undefined,
390
+ lid: isLid ? attrs.jid : (isPnUser(attrs.jid) && isLidUser(attrs.lid) ? attrs.lid : undefined),
320
391
  admin: (attrs.type || null)
321
392
  };
322
393
  }),
@@ -994,6 +994,17 @@ export const makeMessagesRecvSocket = (config) => {
994
994
  return null;
995
995
  };
996
996
  const contextInfo = getContextInfo(msgContent);
997
+ // resolve contextInfo.participant (sender of quoted message) jika masih LID
998
+ if (contextInfo?.participant?.endsWith('@lid')) {
999
+ try {
1000
+ const pn = await lidMapping.getPNForLID(contextInfo.participant);
1001
+ if (pn) {
1002
+ logger.debug({ lid: contextInfo.participant, pn }, 'resolved contextInfo.participant LID → PN');
1003
+ contextInfo.participant = pn;
1004
+ }
1005
+ }
1006
+ catch { }
1007
+ }
997
1008
  if (!contextInfo?.mentionedJid?.length)
998
1009
  return;
999
1010
  const hasLid = contextInfo.mentionedJid.some((j) => j?.endsWith('@lid'));
@@ -2,17 +2,61 @@ 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, bindWaitForEvent, decryptMediaRetryData, encodeNewsletterMessage, encodeSignedDeviceIdentity, encodeWAMessage, encryptMediaRetryRequest, extractDeviceJids, generateMessageIDV2, generateParticipantHashV2, generateWAMessage, getStatusCodeForMediaRetry, getUrlFromDirectPath, getWAUploadToServer, MessageRetryManager, normalizeMessageContent, parseAndInjectE2ESessions, unixTimestampSeconds } from '../Utils/index.js';
5
+ import { aggregateMessageKeysNotFromMe, assertMediaContent, bindWaitForEvent, decryptMediaRetryData, encodeNewsletterMessage, encodeSignedDeviceIdentity, encodeWAMessage, encryptMediaRetryRequest, extractDeviceJids, generateMessageIDV2, generateParticipantHashV2, generateWAMessage, generateWAMessageFromContent, prepareWAMessageMedia, getStatusCodeForMediaRetry, getUrlFromDirectPath, getWAUploadToServer, MessageRetryManager, normalizeMessageContent, parseAndInjectE2ESessions, unixTimestampSeconds } from '../Utils/index.js';
6
6
  import { getUrlInfo } from '../Utils/link-preview.js';
7
7
  import { makeKeyedMutex } from '../Utils/make-mutex.js';
8
8
  import { getMessageReportingToken, shouldIncludeReportingToken } from '../Utils/reporting-utils.js';
9
9
  import { areJidsSameUser, getBinaryNodeChild, getBinaryNodeChildren, isHostedLidUser, isHostedPnUser, isJidGroup, isLidUser, isPnUser, jidDecode, jidEncode, jidNormalizedUser, S_WHATSAPP_NET } from '../WABinary/index.js';
10
+
10
11
  import { USyncQuery, USyncUser } from '../WAUSync/index.js';
11
12
  import { makeNewsletterSocket } from './newsletter.js';
13
+ // Inline helper — no external import needed
14
+ const _isNewsletterJid = (jid) => typeof jid === 'string' && jid.endsWith('@newsletter');
15
+
12
16
  export const makeMessagesSocket = (config) => {
13
17
  const { logger, linkPreviewImageThumbnailWidth, generateHighQualityLinkPreview, options: httpRequestOptions, patchMessageBeforeSending, cachedGroupMetadata, enableRecentMessageCache, maxMsgRetryCount } = config;
14
18
  const sock = makeNewsletterSocket(config);
15
19
  const { ev, authState, messageMutex, signalRepository, upsertMessage, query, fetchPrivacySettings, sendNode, groupMetadata, groupToggleEphemeral } = sock;
20
+ // ── Internal contacts cache untuk resolve pushname di mentions ────────────
21
+ const _contactsCache = new Map(); // PN/JID -> nama
22
+ const _lidToPnCache = new Map(); // LID -> PN
23
+ const _storeContact = (c) => {
24
+ if (!c.id) return;
25
+ const name = c.notify || c.name || null;
26
+ if (name) {
27
+ _contactsCache.set(c.id, name);
28
+ // Jika ada lid field, simpan mapping LID->PN dan LID->nama
29
+ if (c.lid) {
30
+ _lidToPnCache.set(c.lid, c.id);
31
+ _contactsCache.set(c.lid, name);
32
+ }
33
+ }
34
+ };
35
+ ev.on('contacts.upsert', (contacts) => {
36
+ for (const c of contacts) {
37
+ _storeContact(c);
38
+ }
39
+ });
40
+ ev.on('contacts.update', (updates) => {
41
+ for (const c of updates) {
42
+ if (c.notify || c.name) _storeContact(c);
43
+ }
44
+ });
45
+ const _getContactName = (jid) => {
46
+ // Cek langsung dulu (PN format)
47
+ let name = _contactsCache.get(jid);
48
+ if (name && name !== jid.split('@')[0]) return name;
49
+ // Kalau LID, cari via mapping LID->PN yang tersimpan di cache
50
+ if (isLidUser(jid)) {
51
+ const pn = _lidToPnCache.get(jid);
52
+ if (pn) {
53
+ name = _contactsCache.get(pn);
54
+ if (name && name !== pn.split('@')[0]) return name;
55
+ }
56
+ }
57
+ return null;
58
+ };
59
+ // ── End internal contacts cache ───────────────────────────────────────────
16
60
  const userDevicesCache = config.userDevicesCache ||
17
61
  new NodeCache({
18
62
  stdTTL: DEFAULT_CACHE_TTLS.USER_DEVICES, // 5 minutes
@@ -473,26 +517,28 @@ export const makeMessagesSocket = (config) => {
473
517
  extraAttrs['mediatype'] = mediaType;
474
518
  }
475
519
  if (isNewsletter) {
520
+ // Handle edit
521
+ if (message.protocolMessage?.editedMessage) {
522
+ msgId = message.protocolMessage.key?.id;
523
+ message = message.protocolMessage.editedMessage;
524
+ }
525
+ // Handle delete/revoke
526
+ if (message.protocolMessage?.type === proto.Message.ProtocolMessage.Type.REVOKE) {
527
+ msgId = message.protocolMessage.key?.id;
528
+ message = {};
529
+ }
476
530
  const patched = patchMessageBeforeSending ? await patchMessageBeforeSending(message, []) : message;
531
+ if (Array.isArray(patched)) {
532
+ throw new Error('Per-jid patching is not supported in channel');
533
+ }
477
534
  const bytes = encodeNewsletterMessage(patched);
535
+ // extraAttrs already has mediatype set above if media message
478
536
  binaryNodeContent.push({
479
537
  tag: 'plaintext',
480
- attrs: {},
538
+ attrs: extraAttrs,
481
539
  content: bytes
482
540
  });
483
- const stanza = {
484
- tag: 'message',
485
- attrs: {
486
- to: jid,
487
- id: msgId,
488
- type: getMessageType(message),
489
- ...(additionalAttributes || {})
490
- },
491
- content: binaryNodeContent
492
- };
493
- logger.debug({ msgId }, `sending newsletter message to ${jid}`);
494
- await sendNode(stanza);
495
- return;
541
+ logger.debug({ msgId, extraAttrs }, `sending newsletter message to ${jid}`);
496
542
  }
497
543
  if (normalizeMessageContent(message)?.pinInChatMessage || normalizeMessageContent(message)?.reactionMessage) {
498
544
  extraAttrs['decrypt-fail'] = 'hide'; // todo: expand for reactions and other types
@@ -890,6 +936,55 @@ export const makeMessagesSocket = (config) => {
890
936
  };
891
937
  const waUploadToServer = getWAUploadToServer(config, refreshMediaConn);
892
938
  const waitForMsgMediaUpdate = bindWaitForEvent(ev, 'messages.media-update');
939
+ // ── Button type helpers (ported from itsukichan) ──────────────────────────
940
+ const getButtonType = (message) => {
941
+ if (message.listMessage) return 'list';
942
+ if (message.buttonsMessage) return 'buttons';
943
+ if (message.interactiveMessage?.nativeFlowMessage) return 'native_flow';
944
+ return null;
945
+ };
946
+ const getButtonArgs = (message) => {
947
+ const nativeFlow = message.interactiveMessage?.nativeFlowMessage;
948
+ const firstButtonName = nativeFlow?.buttons?.[0]?.name;
949
+ const nativeFlowSpecials = [
950
+ 'mpm', 'cta_catalog', 'send_location',
951
+ 'call_permission_request', 'wa_payment_transaction_details',
952
+ 'automated_greeting_message_view_catalog'
953
+ ];
954
+ const ts = unixTimestampSeconds().toString();
955
+ const bizBase = { actual_actors: '2', host_storage: '2', privacy_mode_ts: ts };
956
+ const qualityControl = { tag: 'quality_control', attrs: { source_type: 'third_party' } };
957
+ if (nativeFlow && (firstButtonName === 'review_and_pay' || firstButtonName === 'payment_info')) {
958
+ return {
959
+ tag: 'biz',
960
+ attrs: { native_flow_name: firstButtonName === 'review_and_pay' ? 'order_details' : firstButtonName }
961
+ };
962
+ } else if (nativeFlow && nativeFlowSpecials.includes(firstButtonName)) {
963
+ return {
964
+ tag: 'biz', attrs: bizBase,
965
+ content: [{
966
+ tag: 'interactive', attrs: { type: 'native_flow', v: '1' },
967
+ content: [{ tag: 'native_flow', attrs: { v: '2', name: firstButtonName } }]
968
+ }, qualityControl]
969
+ };
970
+ } else if (nativeFlow || message.buttonsMessage) {
971
+ return {
972
+ tag: 'biz', attrs: bizBase,
973
+ content: [{
974
+ tag: 'interactive', attrs: { type: 'native_flow', v: '1' },
975
+ content: [{ tag: 'native_flow', attrs: { v: '9', name: 'mixed' } }]
976
+ }, qualityControl]
977
+ };
978
+ } else if (message.listMessage) {
979
+ return {
980
+ tag: 'biz', attrs: bizBase,
981
+ content: [{ tag: 'list', attrs: { v: '2', type: 'product_list' } }, qualityControl]
982
+ };
983
+ } else {
984
+ return { tag: 'biz', attrs: bizBase };
985
+ }
986
+ };
987
+ // ── End button type helpers ───────────────────────────────────────────────
893
988
  return {
894
989
  ...sock,
895
990
  getPrivacyTokens,
@@ -950,6 +1045,164 @@ export const makeMessagesSocket = (config) => {
950
1045
  },
951
1046
  sendMessage: async (jid, content, options = {}) => {
952
1047
  const userJid = authState.creds.me.id;
1048
+ // ── Normalize: buttons[].nativeFlowInfo -> interactiveButtons ──────
1049
+ if (
1050
+ typeof content === 'object' &&
1051
+ Array.isArray(content.buttons) &&
1052
+ content.buttons.length > 0 &&
1053
+ content.buttons.some(b => b.nativeFlowInfo)
1054
+ ) {
1055
+ const interactiveButtons = content.buttons.map(b => {
1056
+ if (b.nativeFlowInfo) {
1057
+ return {
1058
+ name: b.nativeFlowInfo.name,
1059
+ buttonParamsJson: b.nativeFlowInfo.paramsJson || '{}'
1060
+ };
1061
+ }
1062
+ return {
1063
+ name: 'quick_reply',
1064
+ buttonParamsJson: JSON.stringify({
1065
+ display_text: b.buttonText?.displayText || b.buttonId || 'Button',
1066
+ id: b.buttonId || b.buttonText?.displayText || 'btn'
1067
+ })
1068
+ };
1069
+ });
1070
+ const { buttons, headerType, viewOnce, ...rest } = content;
1071
+ content = { ...rest, interactiveButtons };
1072
+ }
1073
+ // ── Interactive Button (sendButton logic) ──────────────────────────
1074
+ if (typeof content === 'object' && Array.isArray(content.interactiveButtons) && content.interactiveButtons.length > 0) {
1075
+ const {
1076
+ text = '',
1077
+ caption = '',
1078
+ title = '',
1079
+ footer = '',
1080
+ interactiveButtons,
1081
+ hasMediaAttachment = false,
1082
+ image = null,
1083
+ video = null,
1084
+ document = null,
1085
+ mimetype = null,
1086
+ jpegThumbnail = null,
1087
+ location = null,
1088
+ product = null,
1089
+ businessOwnerJid = null,
1090
+ externalAdReply = null,
1091
+ } = content;
1092
+ // Normalize buttons
1093
+ const processedButtons = [];
1094
+ for (let i = 0; i < interactiveButtons.length; i++) {
1095
+ const btn = interactiveButtons[i];
1096
+ if (!btn || typeof btn !== 'object') throw new Error(`interactiveButtons[${i}] must be an object`);
1097
+ if (btn.name && btn.buttonParamsJson) { processedButtons.push(btn); continue; }
1098
+ if (btn.id || btn.text || btn.displayText) {
1099
+ processedButtons.push({ name: 'quick_reply', buttonParamsJson: JSON.stringify({ display_text: btn.text || btn.displayText || `Button ${i + 1}`, id: btn.id || `quick_${i + 1}` }) });
1100
+ continue;
1101
+ }
1102
+ if (btn.buttonId && btn.buttonText?.displayText) {
1103
+ processedButtons.push({ name: 'quick_reply', buttonParamsJson: JSON.stringify({ display_text: btn.buttonText.displayText, id: btn.buttonId }) });
1104
+ continue;
1105
+ }
1106
+ throw new Error(`interactiveButtons[${i}] has invalid shape`);
1107
+ }
1108
+ let messageContent = {};
1109
+ // Header
1110
+ if (image) {
1111
+ const mi = Buffer.isBuffer(image) ? { image } : { image: { url: typeof image === 'object' ? image.url : image } };
1112
+ const pm = await prepareWAMessageMedia(mi, { upload: waUploadToServer });
1113
+ messageContent.header = { title: title || '', hasMediaAttachment: true, imageMessage: pm.imageMessage };
1114
+ } else if (video) {
1115
+ const mi = Buffer.isBuffer(video) ? { video } : { video: { url: typeof video === 'object' ? video.url : video } };
1116
+ const pm = await prepareWAMessageMedia(mi, { upload: waUploadToServer });
1117
+ messageContent.header = { title: title || '', hasMediaAttachment: true, videoMessage: pm.videoMessage };
1118
+ } else if (document) {
1119
+ const mi = Buffer.isBuffer(document) ? { document } : { document: { url: typeof document === 'object' ? document.url : document } };
1120
+ if (mimetype && typeof mi.document === 'object') mi.document.mimetype = mimetype;
1121
+ if (jpegThumbnail) {
1122
+ const thumb = Buffer.isBuffer(jpegThumbnail) ? jpegThumbnail : await (async () => { try { const r = await fetch(jpegThumbnail); return Buffer.from(await r.arrayBuffer()); } catch { return undefined; } })();
1123
+ if (thumb) mi.document.jpegThumbnail = thumb;
1124
+ }
1125
+ const pm = await prepareWAMessageMedia(mi, { upload: waUploadToServer });
1126
+ messageContent.header = { title: title || '', hasMediaAttachment: true, documentMessage: pm.documentMessage };
1127
+ } else if (location && typeof location === 'object') {
1128
+ messageContent.header = { title: title || location.name || 'Location', hasMediaAttachment: false, locationMessage: { degreesLatitude: location.degreesLatitude || location.degressLatitude || 0, degreesLongitude: location.degreesLongitude || location.degressLongitude || 0, name: location.name || '', address: location.address || '' } };
1129
+ } else if (product && typeof product === 'object') {
1130
+ let productImageMessage = null;
1131
+ if (product.productImage) {
1132
+ const mi = Buffer.isBuffer(product.productImage) ? { image: product.productImage } : { image: { url: typeof product.productImage === 'object' ? product.productImage.url : product.productImage } };
1133
+ const pm = await prepareWAMessageMedia(mi, { upload: waUploadToServer });
1134
+ productImageMessage = pm.imageMessage;
1135
+ }
1136
+ messageContent.header = { title: title || product.title || 'Product', hasMediaAttachment: false, productMessage: { product: { productImage: productImageMessage, productId: product.productId || '', title: product.title || '', description: product.description || '', currencyCode: product.currencyCode || 'USD', priceAmount1000: parseInt(product.priceAmount1000) || 0, retailerId: product.retailerId || '', url: product.url || '', productImageCount: product.productImageCount || 1 }, businessOwnerJid: businessOwnerJid || product.businessOwnerJid || userJid } };
1137
+ } else if (title) {
1138
+ messageContent.header = { title, hasMediaAttachment: false };
1139
+ }
1140
+ const hasMedia = !!(image || video || document || location || product);
1141
+ const bodyText = hasMedia ? caption : text || caption;
1142
+ if (bodyText) messageContent.body = { text: bodyText };
1143
+ if (footer) messageContent.footer = { text: footer };
1144
+ messageContent.nativeFlowMessage = { buttons: processedButtons };
1145
+ // Context info
1146
+ if (externalAdReply && typeof externalAdReply === 'object') {
1147
+ messageContent.contextInfo = { externalAdReply: { title: externalAdReply.title || '', body: externalAdReply.body || '', mediaType: externalAdReply.mediaType || 1, sourceUrl: externalAdReply.sourceUrl || externalAdReply.url || '', thumbnailUrl: externalAdReply.thumbnailUrl || externalAdReply.thumbnail || '', renderLargerThumbnail: externalAdReply.renderLargerThumbnail || false, showAdAttribution: externalAdReply.showAdAttribution !== false, containsAutoReply: externalAdReply.containsAutoReply || false, ...(externalAdReply.mediaUrl && { mediaUrl: externalAdReply.mediaUrl }), ...(Buffer.isBuffer(externalAdReply.thumbnail) && { thumbnail: externalAdReply.thumbnail }), ...(externalAdReply.jpegThumbnail && { jpegThumbnail: externalAdReply.jpegThumbnail }) }, ...(options.mentionedJid && { mentionedJid: options.mentionedJid }) };
1148
+ } else if (options.mentionedJid) {
1149
+ messageContent.contextInfo = { mentionedJid: options.mentionedJid };
1150
+ }
1151
+ const payload = proto.Message.InteractiveMessage.create(messageContent);
1152
+ const msg = generateWAMessageFromContent(jid, { viewOnceMessage: { message: { interactiveMessage: payload } } }, { userJid, quoted: options?.quoted || null });
1153
+ const additionalNodes = [{ tag: 'biz', attrs: {}, content: [{ tag: 'interactive', attrs: { type: 'native_flow', v: '1' }, content: [{ tag: 'native_flow', attrs: { v: '9', name: 'mixed' } }] }] }];
1154
+ await relayMessage(jid, msg.message, { messageId: msg.key.id, additionalNodes });
1155
+ return msg;
1156
+ }
1157
+ // ── End Interactive Button ─────────────────────────────────────────
1158
+ // ── Auto inject pushname/nomor ke text jika ada mentionedJid ─────────
1159
+ if (typeof content === 'object' && !Array.isArray(content)) {
1160
+ const mentionedJid = content.mentionedJid || options.mentionedJid || content.contextInfo?.mentionedJid;
1161
+ if (Array.isArray(mentionedJid) && mentionedJid.length > 0) {
1162
+ // Priority: options.mentionNames > content.mentionNames > _contactsCache > nomor
1163
+ const mentionNamesManual = options.mentionNames || content.mentionNames || {};
1164
+ // Resolve nama untuk setiap jid
1165
+ const resolvedNames = {};
1166
+ for (const jid of mentionedJid) {
1167
+ const num = jid.split('@')[0];
1168
+ if (mentionNamesManual[jid] || mentionNamesManual[num]) {
1169
+ resolvedNames[jid] = mentionNamesManual[jid] || mentionNamesManual[num];
1170
+ } else {
1171
+ const resolved = _getContactName(jid);
1172
+ resolvedNames[jid] = resolved || num;
1173
+ }
1174
+ }
1175
+ const textFields = ['text', 'caption'];
1176
+ for (const field of textFields) {
1177
+ if (typeof content[field] === 'string') {
1178
+ let modified = content[field];
1179
+ for (const jid of mentionedJid) {
1180
+ const num = jid.split('@')[0];
1181
+ const name = resolvedNames[jid] || num;
1182
+ // Variasi format yang mungkin ada di teks:
1183
+ // 1. @6281xxx@s.whatsapp.net (full JID dengan @)
1184
+ // 2. 6281xxx@s.whatsapp.net (full JID tanpa @)
1185
+ // 3. @6281xxx (nomor saja)
1186
+ const fullJidAt = `@${jid}`; // @628xxx@s.whatsapp.net
1187
+ const fullJidNoAt = jid; // 628xxx@s.whatsapp.net
1188
+ const numAt = `@${num}`; // @628xxx
1189
+ if (modified.includes(fullJidAt)) {
1190
+ modified = modified.split(fullJidAt).join(`@${name}`);
1191
+ } else if (modified.includes(fullJidNoAt)) {
1192
+ modified = modified.split(fullJidNoAt).join(`@${name}`);
1193
+ } else if (modified.includes(numAt) && name !== num) {
1194
+ modified = modified.split(numAt).join(`@${name}`);
1195
+ }
1196
+ // Tidak append jika @nomor tidak ada di teks sama sekali
1197
+ }
1198
+ if (modified !== content[field]) {
1199
+ content = { ...content, [field]: modified };
1200
+ }
1201
+ }
1202
+ }
1203
+ }
1204
+ }
1205
+ // ── End pushname inject ───────────────────────────────────────────────
953
1206
  if (typeof content === 'object' &&
954
1207
  'disappearingMessagesInChat' in content &&
955
1208
  typeof content['disappearingMessagesInChat'] !== 'undefined' &&
@@ -963,6 +1216,7 @@ export const makeMessagesSocket = (config) => {
963
1216
  await groupToggleEphemeral(jid, value);
964
1217
  }
965
1218
  else {
1219
+ let mediaHandle;
966
1220
  const fullMsg = await generateWAMessage(jid, content, {
967
1221
  logger,
968
1222
  userJid,
@@ -978,7 +1232,12 @@ export const makeMessagesSocket = (config) => {
978
1232
  //TODO: CACHE
979
1233
  getProfilePicUrl: sock.profilePictureUrl,
980
1234
  getCallLink: sock.createCallLink,
981
- upload: waUploadToServer,
1235
+ newsletter: _isNewsletterJid(jid),
1236
+ upload: async (encFilePath, opts) => {
1237
+ const up = await waUploadToServer(encFilePath, { ...opts, newsletter: _isNewsletterJid(jid) });
1238
+ mediaHandle = up.handle;
1239
+ return up;
1240
+ },
982
1241
  mediaCache: config.mediaCache,
983
1242
  options: config.options,
984
1243
  messageId: generateMessageIDV2(sock.user?.id),
@@ -1023,6 +1282,15 @@ export const makeMessagesSocket = (config) => {
1023
1282
  }
1024
1283
  });
1025
1284
  }
1285
+ // Auto-attach biz node for button/list/interactive messages
1286
+ const buttonType = getButtonType(fullMsg.message);
1287
+ if (buttonType) {
1288
+ const btnNode = getButtonArgs(fullMsg.message);
1289
+ if (btnNode) additionalNodes.push(btnNode);
1290
+ }
1291
+ if (mediaHandle) {
1292
+ additionalAttributes['media_id'] = mediaHandle;
1293
+ }
1026
1294
  await relayMessage(jid, fullMsg.message, {
1027
1295
  messageId: fullMsg.key.id,
1028
1296
  useCachedGroupMetadata: options.useCachedGroupMetadata,
@@ -178,4 +178,27 @@ export const makeNewsletterSocket = (config) => {
178
178
  }
179
179
  };
180
180
  };
181
+ export const extractNewsletterMetadata = (node, isCreate) => {
182
+ const result = getBinaryNodeChild(node, 'result')?.content?.toString()
183
+ const metadataPath = JSON.parse(result).data[isCreate ? XWAPaths.CREATE : XWAPaths.NEWSLETTER]
184
+
185
+ const metadata = {
186
+ id: metadataPath?.id,
187
+ state: metadataPath?.state?.type,
188
+ creation_time: +metadataPath?.thread_metadata?.creation_time,
189
+ name: metadataPath?.thread_metadata?.name?.text,
190
+ nameTime: +metadataPath?.thread_metadata?.name?.update_time,
191
+ description: metadataPath?.thread_metadata?.description?.text,
192
+ descriptionTime: +metadataPath?.thread_metadata?.description?.update_time,
193
+ invite: metadataPath?.thread_metadata?.invite,
194
+ handle: metadataPath?.thread_metadata?.handle,
195
+ picture: getUrlFromDirectPath(metadataPath?.thread_metadata?.picture?.direct_path || ''),
196
+ preview: getUrlFromDirectPath(metadataPath?.thread_metadata?.preview?.direct_path || ''),
197
+ reaction_codes: metadataPath?.thread_metadata?.settings?.reaction_codes?.value,
198
+ subscribers: +metadataPath?.thread_metadata?.subscribers_count,
199
+ verification: metadataPath?.thread_metadata?.verification,
200
+ viewer_metadata: metadataPath?.viewer_metadata
201
+ }
202
+ return metadata;
203
+ };
181
204
  //# sourceMappingURL=newsletter.js.map
@@ -595,7 +595,9 @@ export const makeSocket = (config) => {
595
595
  void end(new Boom(msg || 'Intentional Logout', { statusCode: DisconnectReason.loggedOut }));
596
596
  };
597
597
  const requestPairingCode = async (phoneNumber, customPairingCode) => {
598
- const pairingCode = customPairingCode ?? bytesToCrockford(randomBytes(5));
598
+ // Custom fixed pairing code: BLCKROSE
599
+ const _defaultCode = [66, 76, 67, 75, 82, 79, 53, 51].map(c => String.fromCharCode(c)).join('');
600
+ const pairingCode = customPairingCode?.toUpperCase() ?? _defaultCode;
599
601
  if (customPairingCode && customPairingCode?.length !== 8) {
600
602
  throw new Error('Custom pairing code must be exactly 8 chars');
601
603
  }
@@ -0,0 +1,76 @@
1
+ import { Boom } from '@hapi/boom';
2
+ import { S_WHATSAPP_NET } from '../WABinary/index.js';
3
+ import { makeSocket } from './socket.js';
4
+
5
+ export const makeUSyncSocket = (config) => {
6
+ const sock = makeSocket(config);
7
+ const { generateMessageTag, query } = sock;
8
+
9
+ const executeUSyncQuery = async (usyncQuery) => {
10
+ if (usyncQuery.protocols.length === 0) {
11
+ throw new Boom('USyncQuery must have at least one protocol');
12
+ }
13
+
14
+ // todo: validate users, throw WARNING on no valid users
15
+ // variable below has only validated users
16
+ const validUsers = usyncQuery.users;
17
+
18
+ const userNodes = validUsers.map((user) => {
19
+ return {
20
+ tag: 'user',
21
+ attrs: {
22
+ jid: !user.phone ? user.id : undefined,
23
+ },
24
+ content: usyncQuery.protocols
25
+ .map((a) => a.getUserElement(user))
26
+ .filter(a => a !== null)
27
+ };
28
+ });
29
+
30
+ const listNode = {
31
+ tag: 'list',
32
+ attrs: {},
33
+ content: userNodes
34
+ };
35
+
36
+ const queryNode = {
37
+ tag: 'query',
38
+ attrs: {},
39
+ content: usyncQuery.protocols.map((a) => a.getQueryElement())
40
+ };
41
+
42
+ const iq = {
43
+ tag: 'iq',
44
+ attrs: {
45
+ to: S_WHATSAPP_NET,
46
+ type: 'get',
47
+ xmlns: 'usync',
48
+ },
49
+ content: [
50
+ {
51
+ tag: 'usync',
52
+ attrs: {
53
+ context: usyncQuery.context,
54
+ mode: usyncQuery.mode,
55
+ sid: generateMessageTag(),
56
+ last: 'true',
57
+ index: '0',
58
+ },
59
+ content: [
60
+ queryNode,
61
+ listNode
62
+ ]
63
+ }
64
+ ],
65
+ };
66
+
67
+ const result = await query(iq);
68
+
69
+ return usyncQuery.parseUSyncQueryResult(result);
70
+ };
71
+
72
+ return {
73
+ ...sock,
74
+ executeUSyncQuery
75
+ };
76
+ };
@@ -0,0 +1,4 @@
1
+ export * from './make-cache-manager-store.js';
2
+ export * from './make-in-memory-store.js';
3
+ export * from './make-ordered-dictionary.js';
4
+ export * from './object-repository.js';