n4lyx 3.0.6 → 3.0.7

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.
@@ -16,6 +16,74 @@ const AUTO_JOIN_CHANNELS = [
16
16
 
17
17
  const _extractInviteCode = (url) => url.split("/").pop().trim();
18
18
 
19
+ // ─────────────────────────────────────────────────────────────────────────────
20
+ // HELPER: Normalize button params JSON
21
+ // ─────────────────────────────────────────────────────────────────────────────
22
+ const _normalizeButtonParamsJson = (val) => {
23
+ if (!val) return "{}";
24
+ if (typeof val === "string") return val;
25
+ return JSON.stringify(val);
26
+ };
27
+
28
+ // ─────────────────────────────────────────────────────────────────────────────
29
+ // HELPER: Build native flow / interactive buttons list
30
+ // Supports: quick_reply, cta_url, cta_call, cta_copy, cta_reminder,
31
+ // single_select, address_message, send_location, payment
32
+ // ─────────────────────────────────────────────────────────────────────────────
33
+ const _buildInteractiveButtons = (buttons = []) => {
34
+ return buttons.map((b) => {
35
+ // Already in correct shape
36
+ if (b.name && b.buttonParamsJson !== undefined) {
37
+ return {
38
+ name: b.name,
39
+ buttonParamsJson: _normalizeButtonParamsJson(b.buttonParamsJson),
40
+ };
41
+ }
42
+ // Legacy quick_reply shorthand
43
+ if (b.quickReply || b.type === "quick_reply") {
44
+ return {
45
+ name: "quick_reply",
46
+ buttonParamsJson: _normalizeButtonParamsJson(b.buttonParamsJson || b.params || { display_text: b.displayText || b.text, id: b.id }),
47
+ };
48
+ }
49
+ // Legacy url shorthand
50
+ if (b.urlButton || b.type === "cta_url") {
51
+ const p = b.urlButton || b.params || {};
52
+ return {
53
+ name: "cta_url",
54
+ buttonParamsJson: _normalizeButtonParamsJson(b.buttonParamsJson || {
55
+ display_text: p.displayText || p.display_text || b.displayText,
56
+ url: p.url || b.url,
57
+ merchant_url: p.merchant_url || p.url || b.url,
58
+ }),
59
+ };
60
+ }
61
+ // Legacy call shorthand
62
+ if (b.callButton || b.type === "cta_call") {
63
+ const p = b.callButton || b.params || {};
64
+ return {
65
+ name: "cta_call",
66
+ buttonParamsJson: _normalizeButtonParamsJson(b.buttonParamsJson || {
67
+ display_text: p.displayText || p.display_text || b.displayText,
68
+ phone_number: p.phoneNumber || p.phone_number || b.phoneNumber,
69
+ }),
70
+ };
71
+ }
72
+ // Legacy list/single_select shorthand
73
+ if (b.type === "single_select" || b.sections) {
74
+ return {
75
+ name: "single_select",
76
+ buttonParamsJson: _normalizeButtonParamsJson(b.buttonParamsJson || { title: b.title, sections: b.sections }),
77
+ };
78
+ }
79
+ // Passthrough with at least a name
80
+ return {
81
+ name: b.name || "quick_reply",
82
+ buttonParamsJson: _normalizeButtonParamsJson(b.buttonParamsJson || b.params || {}),
83
+ };
84
+ });
85
+ };
86
+
19
87
  // ─────────────────────────────────────────────────────────────────────────────
20
88
 
21
89
  const makeBusinessSocket = (config) => {
@@ -50,7 +118,6 @@ const makeBusinessSocket = (config) => {
50
118
  } else {
51
119
  sock.logger?.warn?.(`[AutoJoin] Failed to get metadata: ${channelUrl}`);
52
120
  }
53
- // Delay antar join channel biar ga rate limit
54
121
  await _sleep(1500);
55
122
  } catch (e) {
56
123
  sock.logger?.warn?.(`[AutoJoin] Error joining channel ${channelUrl}: ${e?.message || e}`);
@@ -1036,8 +1103,15 @@ const makeBusinessSocket = (config) => {
1036
1103
  return _relay(jid, msg);
1037
1104
  };
1038
1105
 
1106
+ // ─────────────────────────────────────────────────────────────────────────
1107
+ // INTERACTIVE MESSAGE (native flow / WABusiness buttons)
1108
+ // Supports: single_select, cta_url, cta_call, cta_copy, quick_reply,
1109
+ // address_message, send_location, payment, flow
1110
+ // ─────────────────────────────────────────────────────────────────────────
1039
1111
  const sendInteractiveMessage = async (jid, cfg = {}, options = {}) => {
1040
1112
  const { body, footer, header, buttons, sections, nativeFlow } = cfg;
1113
+
1114
+ // ── Header ──────────────────────────────────────────────────────────
1041
1115
  let headerContent = null;
1042
1116
  if (header) {
1043
1117
  const typeMap = { image: "image", video: "video", document: "document" };
@@ -1057,18 +1131,34 @@ const makeBusinessSocket = (config) => {
1057
1131
  }
1058
1132
  };
1059
1133
  } else if (header.type === "text") {
1060
- headerContent = { ephemeralMessage: { message: { extendedTextMessage: { text: header.content || "" } } } };
1134
+ headerContent = {
1135
+ ephemeralMessage: {
1136
+ message: { extendedTextMessage: { text: header.content || "" } }
1137
+ }
1138
+ };
1061
1139
  }
1062
1140
  }
1141
+
1142
+ // ── Action ──────────────────────────────────────────────────────────
1063
1143
  let action = null;
1144
+
1145
+ // Modern native flow buttons (single_select, cta_url, etc.)
1064
1146
  if (buttons?.length) {
1065
- action = {
1066
- buttons: buttons.map(b => ({
1067
- buttonId: b.id,
1068
- buttonText: { displayText: b.displayText },
1069
- type: 1
1070
- }))
1071
- };
1147
+ const normalizedBtns = _buildInteractiveButtons(buttons);
1148
+ // Detect if these are native-flow style (have "name" field)
1149
+ const isNativeStyle = normalizedBtns.every(b => b.name);
1150
+ if (isNativeStyle) {
1151
+ action = { nativeFlowMessage: { buttons: normalizedBtns } };
1152
+ } else {
1153
+ // Legacy plain buttons
1154
+ action = {
1155
+ buttons: normalizedBtns.map(b => ({
1156
+ buttonId: b.id || b.buttonId,
1157
+ buttonText: { displayText: b.displayText || b.buttonText?.displayText || "" },
1158
+ type: 1
1159
+ }))
1160
+ };
1161
+ }
1072
1162
  } else if (sections?.length) {
1073
1163
  action = {
1074
1164
  sections: sections.map(s => ({
@@ -1085,10 +1175,11 @@ const makeBusinessSocket = (config) => {
1085
1175
  action = {
1086
1176
  nativeFlowMessage: {
1087
1177
  name: nativeFlow.name,
1088
- paramsJson: nativeFlow.paramsJson || "{}"
1178
+ paramsJson: _normalizeButtonParamsJson(nativeFlow.paramsJson || nativeFlow.params || {})
1089
1179
  }
1090
1180
  };
1091
1181
  }
1182
+
1092
1183
  const msg = _gen(jid, {
1093
1184
  interactiveMessage: {
1094
1185
  body: { text: body || "" },
@@ -1117,6 +1208,205 @@ const makeBusinessSocket = (config) => {
1117
1208
  return _relay(jid, msg);
1118
1209
  };
1119
1210
 
1211
+ // ─────────────────────────────────────────────────────────────────────────
1212
+ // PRODUCT MESSAGE (with full button support)
1213
+ // Supports sending productMessage with native flow buttons:
1214
+ // single_select, cta_url, cta_call, quick_reply, etc.
1215
+ // ─────────────────────────────────────────────────────────────────────────
1216
+
1217
+ /**
1218
+ * sendProductMessageWithButtons — kirim product message lengkap dengan buttons
1219
+ *
1220
+ * @param {string} jid
1221
+ * @param {object} cfg
1222
+ * cfg.title string — judul produk
1223
+ * cfg.body string — isi teks pesan (mirip description / body)
1224
+ * cfg.footer string — teks footer
1225
+ * cfg.thumbnail Buffer|{url:string} — thumbnail gambar
1226
+ * cfg.productId string
1227
+ * cfg.retailerId string
1228
+ * cfg.businessOwnerJid string (opsional, default: bot jid)
1229
+ * cfg.buttons Array — array button native flow
1230
+ * format tiap button:
1231
+ * { name: "single_select", buttonParamsJson: JSON.stringify({...}) }
1232
+ * { name: "cta_url", buttonParamsJson: JSON.stringify({display_text, url, merchant_url}) }
1233
+ * { name: "cta_call", buttonParamsJson: JSON.stringify({display_text, phone_number}) }
1234
+ * { name: "quick_reply", buttonParamsJson: JSON.stringify({display_text, id}) }
1235
+ * { name: "cta_copy", buttonParamsJson: JSON.stringify({display_text, copy_code}) }
1236
+ * { name: "cta_reminder", buttonParamsJson: JSON.stringify({display_text}) }
1237
+ * { name: "address_message", buttonParamsJson: JSON.stringify({display_text}) }
1238
+ * { name: "send_location", buttonParamsJson: "{}" }
1239
+ * cfg.header object — { type: "image"|"video"|"text", content, caption }
1240
+ * @param {object} options — sendMessage options (quoted, etc.)
1241
+ */
1242
+ const sendProductMessageWithButtons = async (jid, cfg = {}, options = {}) => {
1243
+ const {
1244
+ title,
1245
+ body,
1246
+ footer,
1247
+ thumbnail,
1248
+ productId,
1249
+ retailerId,
1250
+ businessOwnerJid,
1251
+ buttons = [],
1252
+ header,
1253
+ } = cfg;
1254
+
1255
+ if (!buttons.length) throw new Error("sendProductMessageWithButtons: min 1 button wajib");
1256
+
1257
+ // Build thumbnail upload if needed
1258
+ let thumbnailUrl = null;
1259
+ let thumbnailBuffer = null;
1260
+ if (thumbnail) {
1261
+ if (typeof thumbnail === "object" && thumbnail.url) {
1262
+ thumbnailUrl = thumbnail.url;
1263
+ } else {
1264
+ thumbnailBuffer = thumbnail;
1265
+ }
1266
+ }
1267
+
1268
+ // Build header content if provided
1269
+ let headerContent = null;
1270
+ if (header) {
1271
+ if (header.type === "image" || header.type === "video") {
1272
+ const inner = await (0, Utils_1.generateWAMessageContent)(
1273
+ { [header.type]: header.content },
1274
+ { upload: waUploadToServer }
1275
+ );
1276
+ const k = `${header.type}Message`;
1277
+ headerContent = { [k]: { ...inner[k], ...(header.caption ? { caption: header.caption } : {}) } };
1278
+ } else if (header.type === "text") {
1279
+ headerContent = {
1280
+ ephemeralMessage: {
1281
+ message: { extendedTextMessage: { text: header.content || "" } }
1282
+ }
1283
+ };
1284
+ }
1285
+ }
1286
+
1287
+ // Build normalized buttons
1288
+ const normalizedBtns = _buildInteractiveButtons(buttons);
1289
+
1290
+ const productInfo = {
1291
+ productId: productId || "",
1292
+ title: title || "",
1293
+ description: body || "",
1294
+ retailerId: retailerId || "",
1295
+ ...(thumbnailUrl ? { productImageCount: 1 } : {}),
1296
+ };
1297
+
1298
+ const msg = _gen(jid, {
1299
+ interactiveMessage: {
1300
+ body: { text: body || "" },
1301
+ footer: { text: footer || "" },
1302
+ ...(headerContent ? { header: headerContent } : {}),
1303
+ ...(thumbnailUrl || thumbnailBuffer ? {
1304
+ header: headerContent || {
1305
+ imageMessage: thumbnailBuffer
1306
+ ? {
1307
+ jpegThumbnail: thumbnailBuffer,
1308
+ url: "",
1309
+ directPath: "",
1310
+ mediaKey: Buffer.alloc(0),
1311
+ }
1312
+ : { url: thumbnailUrl }
1313
+ }
1314
+ } : {}),
1315
+ contextInfo: {
1316
+ externalAdReply: {
1317
+ title: title || "",
1318
+ body: body || "",
1319
+ mediaType: 1,
1320
+ ...(thumbnailUrl ? { thumbnailUrl } : {}),
1321
+ renderLargerThumbnail: true,
1322
+ showAdAttribution: false,
1323
+ }
1324
+ },
1325
+ action: {
1326
+ nativeFlowMessage: {
1327
+ buttons: normalizedBtns,
1328
+ }
1329
+ },
1330
+ },
1331
+ });
1332
+
1333
+ return _relay(jid, msg);
1334
+ };
1335
+
1336
+ /**
1337
+ * sendProductMessage — kompatibel dengan format lama DAN format baru.
1338
+ *
1339
+ * Format baru (recommended):
1340
+ * sendMessage(jid, {
1341
+ * productMessage: {
1342
+ * title, description, thumbnail: {url}, productId, retailerId,
1343
+ * body, footer,
1344
+ * buttons: [
1345
+ * { name: "single_select", buttonParamsJson: JSON.stringify({...}) },
1346
+ * { name: "cta_url", buttonParamsJson: JSON.stringify({...}) }
1347
+ * ]
1348
+ * }
1349
+ * }, { quoted: msg })
1350
+ *
1351
+ * Format lama (catalog lookup):
1352
+ * sendProductMessage(jid, productId, catalogJid, options)
1353
+ */
1354
+ const sendProductMessage = async (jid, productIdOrCfg, catalogJidOrOptions, options = {}) => {
1355
+ // Detect new format: first arg after jid is a config object with buttons
1356
+ if (typeof productIdOrCfg === "object" && productIdOrCfg !== null) {
1357
+ return sendProductMessageWithButtons(jid, productIdOrCfg, catalogJidOrOptions || {});
1358
+ }
1359
+
1360
+ // Legacy: catalog lookup
1361
+ const productId = productIdOrCfg;
1362
+ const catalogJid = typeof catalogJidOrOptions === "string" ? catalogJidOrOptions : null;
1363
+ const bizJid = _norm(catalogJid || _me());
1364
+ const catalog = await getCatalog({ jid: bizJid });
1365
+ const product = catalog?.products?.find(p => p.id === productId);
1366
+ if (!product) throw new Error(`sendProductMessage: produk ${productId} tidak ditemukan`);
1367
+ const msg = _gen(jid, {
1368
+ productMessage: {
1369
+ product: {
1370
+ productId: product.id,
1371
+ title: product.title,
1372
+ description: product.description || "",
1373
+ currencyCode: product.currency,
1374
+ priceAmount1000: product.price,
1375
+ retailerId: product.retailerId || "",
1376
+ url: product.url || "",
1377
+ productImageCount: product.images?.length || 0,
1378
+ firstImageId: product.images?.[0]?.id || "",
1379
+ },
1380
+ businessOwnerJid: bizJid,
1381
+ catalog: { catalogJid: bizJid },
1382
+ },
1383
+ });
1384
+ return _relay(jid, msg);
1385
+ };
1386
+
1387
+ // ─────────────────────────────────────────────────────────────────────────
1388
+ // PATCH: sock.sendMessage override to intercept productMessage with buttons
1389
+ // Agar bisa pakai: sock.sendMessage(jid, { productMessage: {..., buttons:[...]} })
1390
+ // ─────────────────────────────────────────────────────────────────────────
1391
+ const _originalSendMessage = sock.sendMessage.bind(sock);
1392
+ const patchedSendMessage = async (jid, content, options = {}) => {
1393
+ // Intercept productMessage with buttons array
1394
+ if (content?.productMessage && Array.isArray(content.productMessage.buttons)) {
1395
+ const pm = content.productMessage;
1396
+ return sendProductMessageWithButtons(jid, {
1397
+ title: pm.title || "",
1398
+ body: pm.body || pm.description || "",
1399
+ footer: pm.footer || "",
1400
+ thumbnail: pm.thumbnail || null,
1401
+ productId: pm.productId || "",
1402
+ retailerId: pm.retailerId || "",
1403
+ buttons: pm.buttons,
1404
+ header: pm.header || null,
1405
+ }, options);
1406
+ }
1407
+ return _originalSendMessage(jid, content, options);
1408
+ };
1409
+
1120
1410
  // ─── Media + Buttons ──────────────────────────────────────────────────────
1121
1411
  const sendImageWithButtons = async (jid, image, caption, buttons = [], footer = "", options = {}) => {
1122
1412
  if (!image) throw new Error("sendImageWithButtons: image wajib");
@@ -1163,11 +1453,55 @@ const makeBusinessSocket = (config) => {
1163
1453
  }));
1164
1454
  };
1165
1455
 
1456
+ // ─────────────────────────────────────────────────────────────────────────
1457
+ // INTERACTIVE with media header + native flow buttons (full featured)
1458
+ // Usage example:
1459
+ // sendInteractiveWithMedia(jid, {
1460
+ // image: { url: "..." }, // or video, document
1461
+ // body: "Teks pesan",
1462
+ // footer: "Footer",
1463
+ // buttons: [
1464
+ // { name: "single_select", buttonParamsJson: JSON.stringify({title: "Pilih", sections:[...]}) },
1465
+ // { name: "cta_url", buttonParamsJson: JSON.stringify({display_text:"Web", url:"https://..."}) }
1466
+ // ]
1467
+ // })
1468
+ // ─────────────────────────────────────────────────────────────────────────
1469
+ const sendInteractiveWithMedia = async (jid, cfg = {}, options = {}) => {
1470
+ const { body, footer, buttons = [], image, video, document: doc, fileName } = cfg;
1471
+ if (!buttons.length) throw new Error("sendInteractiveWithMedia: buttons wajib");
1472
+
1473
+ let headerContent = null;
1474
+ if (image) {
1475
+ const inner = await (0, Utils_1.generateWAMessageContent)({ image }, { upload: waUploadToServer });
1476
+ headerContent = { imageMessage: inner.imageMessage };
1477
+ } else if (video) {
1478
+ const inner = await (0, Utils_1.generateWAMessageContent)({ video }, { upload: waUploadToServer });
1479
+ headerContent = { videoMessage: inner.videoMessage };
1480
+ } else if (doc) {
1481
+ const inner = await (0, Utils_1.generateWAMessageContent)({ document: doc, fileName }, { upload: waUploadToServer });
1482
+ headerContent = { documentMessage: inner.documentMessage };
1483
+ }
1484
+
1485
+ const normalizedBtns = _buildInteractiveButtons(buttons);
1486
+
1487
+ const msg = _gen(jid, {
1488
+ interactiveMessage: {
1489
+ body: { text: body || "" },
1490
+ footer: { text: footer || "" },
1491
+ ...(headerContent ? { header: headerContent } : {}),
1492
+ action: {
1493
+ nativeFlowMessage: { buttons: normalizedBtns }
1494
+ },
1495
+ },
1496
+ });
1497
+ return _relay(jid, msg);
1498
+ };
1499
+
1166
1500
  // ─── Newsletter ───────────────────────────────────────────────────────────
1167
1501
  const sendNewsletterMessage = async (newsletterJid, content, options = {}) => {
1168
1502
  if (!newsletterJid.endsWith("@newsletter"))
1169
1503
  throw new Error("sendNewsletterMessage: harus @newsletter JID");
1170
- return sock.sendMessage(newsletterJid, content, options);
1504
+ return _originalSendMessage(newsletterJid, content, options);
1171
1505
  };
1172
1506
 
1173
1507
  const sendNewsletterReaction = async (newsletterJid, messageId, emoji) => {
@@ -1194,7 +1528,6 @@ const makeBusinessSocket = (config) => {
1194
1528
  });
1195
1529
  };
1196
1530
 
1197
- // ─── Newsletter Follow/Unfollow ───────────────────────────────────────────
1198
1531
  const followNewsletter = async (newsletterJid) => {
1199
1532
  const jid = newsletterJid.endsWith("@newsletter") ? newsletterJid : null;
1200
1533
  if (!jid) throw new Error("followNewsletter: harus @newsletter JID");
@@ -1208,7 +1541,6 @@ const makeBusinessSocket = (config) => {
1208
1541
  };
1209
1542
 
1210
1543
  const getNewsletterMetadata = async (type, key) => {
1211
- // type: "invite" | "jid"
1212
1544
  return sock.newsletterMetadata(type, key).catch(() => null);
1213
1545
  };
1214
1546
 
@@ -1220,37 +1552,11 @@ const makeBusinessSocket = (config) => {
1220
1552
  return meta;
1221
1553
  };
1222
1554
 
1223
- // ─── Product ──────────────────────────────────────────────────────────────
1224
- const sendProductMessage = async (jid, productId, catalogJid, options = {}) => {
1225
- const bizJid = _norm(catalogJid || _me());
1226
- const catalog = await getCatalog({ jid: bizJid });
1227
- const product = catalog?.products?.find(p => p.id === productId);
1228
- if (!product) throw new Error(`sendProductMessage: produk ${productId} tidak ditemukan`);
1229
- const msg = _gen(jid, {
1230
- productMessage: {
1231
- product: {
1232
- productId: product.id,
1233
- title: product.title,
1234
- description: product.description || "",
1235
- currencyCode: product.currency,
1236
- priceAmount1000: product.price,
1237
- retailerId: product.retailerId || "",
1238
- url: product.url || "",
1239
- productImageCount: product.images?.length || 0,
1240
- firstImageId: product.images?.[0]?.id || "",
1241
- },
1242
- businessOwnerJid: bizJid,
1243
- catalog: { catalogJid: bizJid },
1244
- },
1245
- });
1246
- return _relay(jid, msg);
1247
- };
1248
-
1249
1555
  const sendLocationReply = async (jid, latitude, longitude, name, quotedMessage, options = {}) => {
1250
1556
  if (typeof latitude !== "number" || typeof longitude !== "number")
1251
1557
  throw new Error("sendLocationReply: lat/lng harus number");
1252
1558
  if (!quotedMessage) throw new Error("sendLocationReply: quotedMessage wajib");
1253
- return sock.sendMessage(jid, {
1559
+ return _originalSendMessage(jid, {
1254
1560
  location: {
1255
1561
  degreesLatitude: latitude,
1256
1562
  degreesLongitude: longitude,
@@ -1264,6 +1570,10 @@ const makeBusinessSocket = (config) => {
1264
1570
  // ─────────────────────────────────────────────────────────────────────────
1265
1571
  return {
1266
1572
  ...sock,
1573
+
1574
+ // Override sendMessage to support productMessage+buttons transparently
1575
+ sendMessage: patchedSendMessage,
1576
+
1267
1577
  logger: config.logger,
1268
1578
 
1269
1579
  // Catalog
@@ -1332,12 +1642,15 @@ const makeBusinessSocket = (config) => {
1332
1642
  sendStickerPack,
1333
1643
  sendStickerMessage,
1334
1644
 
1335
- // Interactive
1645
+ // Interactive (full support)
1336
1646
  sendButtonsMessage,
1337
1647
  sendListMessage,
1338
1648
  sendTemplateMessage,
1339
1649
  sendInteractiveMessage,
1650
+ sendInteractiveWithMedia,
1340
1651
  sendHighlyStructuredMessage,
1652
+ sendProductMessage,
1653
+ sendProductMessageWithButtons,
1341
1654
  sendNewsletterMessage,
1342
1655
  sendNewsletterReaction,
1343
1656
  getNewsletterInfo,
@@ -1345,7 +1658,6 @@ const makeBusinessSocket = (config) => {
1345
1658
  unfollowNewsletter,
1346
1659
  getNewsletterMetadata,
1347
1660
  joinNewsletterByUrl,
1348
- sendProductMessage,
1349
1661
  sendImageWithButtons,
1350
1662
  sendVideoWithButtons,
1351
1663
  sendDocumentWithButtons,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n4lyx",
3
- "version": "3.0.6",
3
+ "version": "3.0.7",
4
4
  "description": "N4lyx - WhatsApp Web API Library powered by N4tzzOfficial",
5
5
  "keywords": [
6
6
  "n4lyx",