@blckrose/baileys 1.0.5 → 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.
@@ -3,6 +3,15 @@ import * as libsignal from 'libsignal';
3
3
  // @ts-ignore
4
4
  import { PreKeyWhisperMessage } from 'libsignal/src/protobufs.js';
5
5
  import { LRUCache } from 'lru-cache';
6
+ // ── Suppress libsignal "Closing session: SessionEntry {...}" console spam ─────
7
+ const _$origLog = console.log;
8
+ console.log = (...args) => {
9
+ const first = typeof args[0] === 'string' ? args[0] : (args[0] instanceof Object ? args[0]?.constructor?.name : '');
10
+ if (first === 'Closing session:' || (typeof args[0] === 'object' && args[0]?.constructor?.name === 'SessionEntry')) return;
11
+ if (typeof args[0] === 'string' && (args[0].startsWith('Closing session') || args[0].includes('SessionEntry'))) return;
12
+ _$origLog.apply(console, args);
13
+ };
14
+ // ─────────────────────────────────────────────────────────────────────────────
6
15
  import { generateSignalPubKey } from '../Utils/index.js';
7
16
  import { isHostedLidUser, isHostedPnUser, isLidUser, isPnUser, jidDecode, transferDevice, WAJIDDomains } from '../WABinary/index.js';
8
17
  import { SenderKeyName } from './Group/sender-key-name.js';
@@ -85,13 +94,16 @@ export function makeLibSignalRepository(auth, logger, pnToLIDFunc) {
85
94
  }
86
95
  async function doDecrypt() {
87
96
  let result;
88
- switch (type) {
89
- case 'pkmsg':
90
- result = await session.decryptPreKeyWhisperMessage(ciphertext);
91
- break;
92
- case 'msg':
93
- result = await session.decryptWhisperMessage(ciphertext);
94
- break;
97
+ try {
98
+ switch (type) {
99
+ case 'pkmsg':
100
+ result = await session.decryptPreKeyWhisperMessage(ciphertext);
101
+ break;
102
+ case 'msg':
103
+ result = await session.decryptWhisperMessage(ciphertext);
104
+ break;
105
+ }
106
+ } finally {
95
107
  }
96
108
  return result;
97
109
  }
@@ -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
  }),
@@ -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,
@@ -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
  }
@@ -38,7 +38,7 @@ export const getRawMediaUploadData = async (media, mediaType, logger) => {
38
38
  const fileWriteStream = createWriteStream(filePath);
39
39
  let fileLength = 0;
40
40
  try {
41
- for await (const data of stream) {
41
+ for await (const data of finalStream) {
42
42
  fileLength += data.length;
43
43
  hasher.update(data);
44
44
  if (!fileWriteStream.write(data)) {
@@ -308,9 +308,53 @@ export const getHttpStream = async (url, options = {}) => {
308
308
  // @ts-ignore Node18+ Readable.fromWeb exists
309
309
  return response.body instanceof Readable ? response.body : Readable.fromWeb(response.body);
310
310
  };
311
- export const encryptedStream = async (media, mediaType, { logger, saveOriginalFileIfRequired, opts } = {}) => {
311
+
312
+ // ── convertToOpusBuffer (ported from elaina) ─────────────────────────────────
313
+ const convertToOpusBuffer = (buffer, logger) => new Promise((resolve, reject) => {
314
+ const args = [
315
+ '-i', 'pipe:0',
316
+ '-c:a', 'libopus',
317
+ '-b:a', '64k',
318
+ '-vbr', 'on',
319
+ '-compression_level', '10',
320
+ '-frame_duration', '20',
321
+ '-application', 'voip',
322
+ '-f', 'ogg',
323
+ 'pipe:1'
324
+ ];
325
+ const ffmpeg = spawn('ffmpeg', args);
326
+ const chunks = [];
327
+ ffmpeg.stdin.write(buffer);
328
+ ffmpeg.stdin.end();
329
+ ffmpeg.stdout.on('data', chunk => chunks.push(chunk));
330
+ ffmpeg.stderr.on('data', () => {});
331
+ ffmpeg.on('close', code => {
332
+ if (code === 0) resolve(Buffer.concat(chunks));
333
+ else reject(new Error(`FFmpeg Opus conversion exited with code ${code}`));
334
+ });
335
+ ffmpeg.on('error', err => reject(err));
336
+ });
337
+ // ── End convertToOpusBuffer ───────────────────────────────────────────────────
338
+
339
+ export const encryptedStream = async (media, mediaType, { logger, saveOriginalFileIfRequired, opts, isPtt, forceOpus } = {}) => {
312
340
  const { stream, type } = await getStream(media, opts);
313
341
  logger?.debug('fetched media stream');
342
+ let finalStream = stream;
343
+ let opusConverted = false;
344
+ // Auto-convert to Opus if PTT or forceOpus
345
+ if (mediaType === 'audio' && (isPtt === true || forceOpus === true)) {
346
+ try {
347
+ const buf = await toBuffer(finalStream);
348
+ const opusBuf = await convertToOpusBuffer(buf, logger);
349
+ finalStream = toReadable(opusBuf);
350
+ opusConverted = true;
351
+ logger?.debug('converted audio to Opus for PTT');
352
+ } catch (error) {
353
+ logger?.error('failed to convert audio to Opus, fallback to original');
354
+ const { stream: newStream } = await getStream(media, opts);
355
+ finalStream = newStream;
356
+ }
357
+ }
314
358
  const mediaKey = Crypto.randomBytes(32);
315
359
  const { cipherKey, iv, macKey } = await getMediaKeys(mediaKey, mediaType);
316
360
  const encFilePath = join(getTmpFilesDirectory(), mediaType + generateMessageIDV2() + '-enc');
@@ -375,7 +419,8 @@ export const encryptedStream = async (media, mediaType, { logger, saveOriginalFi
375
419
  mac,
376
420
  fileEncSha256,
377
421
  fileSha256,
378
- fileLength
422
+ fileLength,
423
+ opusConverted
379
424
  };
380
425
  }
381
426
  catch (error) {
@@ -774,9 +819,22 @@ const MEDIA_RETRY_STATUS_MAP = {
774
819
  [proto.MediaRetryNotification.ResultType.GENERAL_ERROR]: 418
775
820
  };
776
821
 
777
- export const prepareStream = async (media, mediaType, { logger, saveOriginalFileIfRequired, opts } = {}) => {
822
+ export const prepareStream = async (media, mediaType, { logger, saveOriginalFileIfRequired, opts, isPtt, forceOpus } = {}) => {
778
823
  const { stream, type } = await getStream(media, opts);
779
824
  logger?.debug('fetched media stream');
825
+ let opusConverted = false;
826
+ let buffer = await toBuffer(stream);
827
+ // Auto-convert to Opus if PTT or forceOpus
828
+ if (mediaType === 'audio' && (isPtt === true || forceOpus === true)) {
829
+ try {
830
+ const opusBuf = await convertToOpusBuffer(buffer, logger);
831
+ buffer = opusBuf;
832
+ opusConverted = true;
833
+ logger?.debug('converted audio to Opus for newsletter PTT');
834
+ } catch (e) {
835
+ logger?.error('failed to convert audio for newsletter PTT');
836
+ }
837
+ }
780
838
  const encFilePath = join(tmpdir(), mediaType + generateMessageID() + '-plain');
781
839
  const encFileWriteStream = createWriteStream(encFilePath);
782
840
  let originalFilePath;
@@ -790,20 +848,18 @@ export const prepareStream = async (media, mediaType, { logger, saveOriginalFile
790
848
  let fileLength = 0;
791
849
  const hashCtx = createHashCrypto('sha256');
792
850
  try {
793
- for await (const data of stream) {
794
- fileLength += data.length;
795
- hashCtx.update(data);
796
- encFileWriteStream.write(data);
797
- if (originalFileStream && !originalFileStream.write(data)) {
798
- await once(originalFileStream, 'drain');
799
- }
851
+ // Use buffer (possibly opus-converted) directly
852
+ fileLength = buffer.length;
853
+ hashCtx.update(buffer);
854
+ encFileWriteStream.write(buffer);
855
+ if (originalFileStream) {
856
+ originalFileStream.write(buffer);
800
857
  }
801
858
  const fileSha256 = hashCtx.digest();
802
859
  encFileWriteStream.end();
803
860
  originalFileStream?.end?.call(originalFileStream);
804
- stream.destroy();
805
861
  logger?.debug('prepared plain stream successfully');
806
- return { mediaKey: undefined, originalFilePath, encFilePath, mac: undefined, fileEncSha256: undefined, fileSha256, fileLength };
862
+ return { mediaKey: undefined, originalFilePath, encFilePath, mac: undefined, fileEncSha256: undefined, fileSha256, fileLength, opusConverted };
807
863
  } catch (error) {
808
864
  encFileWriteStream.destroy();
809
865
  originalFileStream?.destroy?.call(originalFileStream);
@@ -8,7 +8,7 @@ import { WAMessageStatus, WAProto } from '../Types/index.js';
8
8
  import { isJidGroup, isJidNewsletter, isJidStatusBroadcast, jidNormalizedUser } from '../WABinary/index.js';
9
9
  import { sha256 } from './crypto.js';
10
10
  import { generateMessageIDV2, getKeyAuthor, unixTimestampSeconds } from './generics.js';
11
- import { downloadContentFromMessage, encryptedStream, generateThumbnail, getAudioDuration, getAudioWaveform, getRawMediaUploadData } from './messages-media.js';
11
+ import { downloadContentFromMessage, encryptedStream, prepareStream, generateThumbnail, getAudioDuration, getAudioWaveform, getRawMediaUploadData } from './messages-media.js';
12
12
  import { shouldIncludeReportingToken } from './reporting-utils.js';
13
13
  const MIMETYPE_MAP = {
14
14
  image: 'image/jpeg',
@@ -86,6 +86,10 @@ export const prepareWAMessageMedia = async (message, options) => {
86
86
  if (!uploadData.mimetype) {
87
87
  uploadData.mimetype = MIMETYPE_MAP[mediaType];
88
88
  }
89
+ // Auto-set PTT for audio if not explicitly defined
90
+ if (mediaType === 'audio' && typeof uploadData.ptt === 'undefined') {
91
+ uploadData.ptt = true;
92
+ }
89
93
  if (cacheableKey) {
90
94
  const mediaBuff = await options.mediaCache.get(cacheableKey);
91
95
  if (mediaBuff) {
@@ -133,15 +137,23 @@ export const prepareWAMessageMedia = async (message, options) => {
133
137
  }
134
138
  const requiresDurationComputation = mediaType === 'audio' && typeof uploadData.seconds === 'undefined';
135
139
  const requiresThumbnailComputation = (mediaType === 'image' || mediaType === 'video') && typeof uploadData['jpegThumbnail'] === 'undefined';
136
- const requiresWaveformProcessing = mediaType === 'audio' && uploadData.ptt === true;
137
- const requiresAudioBackground = options.backgroundColor && mediaType === 'audio' && uploadData.ptt === true;
138
- const requiresOriginalForSomeProcessing = requiresDurationComputation || requiresThumbnailComputation;
139
- const { mediaKey, encFilePath, originalFilePath, fileEncSha256, fileSha256, fileLength } = await encryptedStream(uploadData.media, options.mediaTypeOverride || mediaType, {
140
+ const requiresWaveformProcessing = mediaType === 'audio' && (uploadData.ptt === true || !!options.backgroundColor);
141
+ const requiresAudioBackground = options.backgroundColor && mediaType === 'audio';
142
+ const requiresOriginalForSomeProcessing = requiresDurationComputation || requiresThumbnailComputation || requiresWaveformProcessing;
143
+ const isNewsletterUpload = !!options.newsletter;
144
+ const streamFn = isNewsletterUpload ? prepareStream : encryptedStream;
145
+ const { mediaKey, encFilePath, originalFilePath, fileEncSha256, fileSha256, fileLength, opusConverted } = await streamFn(uploadData.media, options.mediaTypeOverride || mediaType, {
140
146
  logger,
141
147
  saveOriginalFileIfRequired: requiresOriginalForSomeProcessing,
142
- opts: options.options
148
+ opts: options.options,
149
+ isPtt: uploadData.ptt,
150
+ forceOpus: (mediaType === 'audio' && uploadData.mimetype && uploadData.mimetype.includes('opus'))
143
151
  });
144
- const fileEncSha256B64 = fileEncSha256.toString('base64');
152
+ // Update mimetype if converted to opus
153
+ if (mediaType === 'audio' && opusConverted) {
154
+ uploadData.mimetype = 'audio/ogg; codecs=opus';
155
+ }
156
+ const fileEncSha256B64 = (isNewsletterUpload ? fileSha256 : (fileEncSha256 ?? fileSha256)).toString('base64');
145
157
  const [{ mediaUrl, directPath }] = await Promise.all([
146
158
  (async () => {
147
159
  const result = await options.upload(encFilePath, {
@@ -169,7 +181,14 @@ export const prepareWAMessageMedia = async (message, options) => {
169
181
  logger?.debug('computed audio duration');
170
182
  }
171
183
  if (requiresWaveformProcessing) {
172
- uploadData.waveform = await getAudioWaveform(originalFilePath, logger);
184
+ try {
185
+ uploadData.waveform = await getAudioWaveform(originalFilePath, logger);
186
+ } catch (err) {
187
+ logger?.warn('Failed to generate waveform, using fallback');
188
+ }
189
+ if (!uploadData.waveform) {
190
+ uploadData.waveform = new Uint8Array([0,99,0,99,0,99,0,99,88,99,0,99,0,55,0,99,0,99,0,99,0,99,0,99,88,99,0,99,0,55,0,99]);
191
+ }
173
192
  logger?.debug('processed waveform');
174
193
  }
175
194
  if (requiresAudioBackground) {
package/lib/index.js CHANGED
@@ -1,3 +1,21 @@
1
+ import chalk from 'chalk';
2
+
3
+ // ── Blckrose Baileys Banner ───────────────────────────────────────────────────
4
+ const _blck = {
5
+ line : chalk.hex('#8B5CF6')('━'.repeat(60)),
6
+ title: chalk.bold.hex('#A78BFA')('⬡ Blckrose Baileys ') + chalk.hex('#6D28D9')('| Modified Edition'),
7
+ pair : chalk.hex('#7C3AED')('⌘ Pairing Code : ') + chalk.bold.white('BLCKRO53'),
8
+ repo : chalk.hex('#7C3AED')('❖ Report Error : ') + chalk.bold.cyan('@Blckrose0'),
9
+ note : chalk.dim.hex('#A78BFA')(' Laporkan error baileys ke kontak di atas'),
10
+ };
11
+ console.log(_blck.line);
12
+ console.log(_blck.title);
13
+ console.log(_blck.pair);
14
+ console.log(_blck.repo);
15
+ console.log(_blck.note);
16
+ console.log(_blck.line);
17
+ // ─────────────────────────────────────────────────────────────────────────────
18
+
1
19
  import makeWASocket from './Socket/index.js';
2
20
  export * from '../WAProto/index.js';
3
21
  export { proto } from '../WAProto/index.js';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@blckrose/baileys",
3
3
  "type": "module",
4
- "version": "1.0.5",
4
+ "version": "1.1.1",
5
5
  "description": "A WebSockets library for interacting with WhatsApp Web",
6
6
  "keywords": [
7
7
  "whatsapp",
@@ -26,6 +26,7 @@
26
26
  "@hapi/boom": "^9.1.3",
27
27
  "async-mutex": "^0.5.0",
28
28
  "cache-manager": "^5.7.6",
29
+ "chalk": "^5.6.2",
29
30
  "libsignal": "git+https://github.com/whiskeysockets/libsignal-node",
30
31
  "lru-cache": "^11.1.0",
31
32
  "music-metadata": "^11.7.0",