plogme 2.0.3 → 2.0.4

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.
package/README.md CHANGED
@@ -291,7 +291,7 @@ await sock.sendMessage(jid, {
291
291
 
292
292
  ### Albums
293
293
 
294
- Albums now use the AIRich `GRID_IMAGE` unified-response layout. Provide at least two image URLs; the client receives one forwarded rich message containing preview, full-resolution, and dark-mode image primitives. The old `albumMessage` parent/child collection is no longer generated.
294
+ Albums now use an AIRich vertical unified-response layout. Provide at least two image or video items; the client receives one forwarded rich message with one large media item per row, matching WhatsApp's full-width stacked album presentation. The old `albumMessage` parent/child collection and compact thumbnail grid are no longer generated by `sendAlbumMessage()`.
295
295
 
296
296
  ```js
297
297
  await sock.sendAlbumMessage(jid, [
@@ -305,11 +305,20 @@ await sock.sendAlbumMessage(jid, [
305
305
  width: 1200,
306
306
  height: 800
307
307
  }
308
+ },
309
+ {
310
+ type: 'video',
311
+ data: {
312
+ url: 'https://example.com/three.mp4',
313
+ mimeType: 'video/mp4',
314
+ duration: 12,
315
+ thumbnailUrl: 'https://example.com/three.jpg'
316
+ }
308
317
  }
309
318
  ])
310
319
  ```
311
320
 
312
- The general `sendMessage` album form delegates to the same GRID_IMAGE implementation:
321
+ The general `sendMessage` album form delegates to the same full-width vertical album implementation:
313
322
 
314
323
  ```js
315
324
  await sock.sendMessage(jid, {
@@ -320,7 +329,30 @@ await sock.sendMessage(jid, {
320
329
  })
321
330
  ```
322
331
 
323
- GRID_IMAGE albums support images only. Video items and local `Buffer`/file inputs are rejected because this layout carries fetchable image URLs rather than uploaded album children.
332
+ `sendAlbumMessage()` accepts fetchable image and video URLs. Local `Buffer`/file inputs are not uploaded by this rich album API. The lower-level `prepareGridImageMessage()` helper remains available when a compact `GRID_IMAGE` layout is specifically required; `sendAlbumMessage()` itself uses the larger `GenAIVStackLayoutViewModel` presentation.
333
+
334
+ ### Video grids
335
+
336
+ Video grids use the supported `GenAIVideoPrimitive` inside a unified-response `GenAIGridLayoutViewModel`. They are exposed separately from `sendAlbumMessage()` because WhatsApp's protobuf does not define a `GRID_VIDEO` submessage type. Provide at least two publicly fetchable video URLs:
337
+
338
+ ```js
339
+ await sock.sendVideoGridMessage(jid, [
340
+ {
341
+ url: 'https://example.com/first.mp4',
342
+ title: 'First clip',
343
+ thumbnailUrl: 'https://example.com/first.jpg',
344
+ progressiveUrls: ['https://cdn.example.com/first-720p.mp4']
345
+ },
346
+ {
347
+ url: 'https://example.com/second.mp4',
348
+ title: 'Second clip',
349
+ thumbnailUrl: 'https://example.com/second.jpg',
350
+ progressiveUrls: ['https://cdn.example.com/second-720p.mp4']
351
+ }
352
+ ])
353
+ ```
354
+
355
+ The video-grid API is intentionally URL-based; it does not upload local buffers or files. `sendVideoGridMessage()` also uses the rich-message edit relay, so supported clients receive the unified response without an initial Download-only placeholder.
324
356
 
325
357
  ## Interactive messages
326
358
 
@@ -524,7 +556,7 @@ const result = await sock.sendPlogmeMessage(jid, {
524
556
  { latexExpression: 'E = mc^2', url: 'https://example.com/equation.png' }
525
557
  ],
526
558
 
527
- // Optional sources/links, posts, products, or suggested prompts.
559
+ // Optional citation links.
528
560
  links: [
529
561
  {
530
562
  text: 'Open the deployment logs',
@@ -533,6 +565,43 @@ const result = await sock.sendPlogmeMessage(jid, {
533
565
  displayName: 'Logs'
534
566
  }
535
567
  ],
568
+
569
+ // Optional AIRich product card (object = Single layout; array = HScroll).
570
+ products: {
571
+ title: 'CODY AI hosting',
572
+ brand: 'Plogme',
573
+ price: '$10',
574
+ sale_price: '$7',
575
+ url: 'https://example.com/product',
576
+ image: 'https://example.com/product.jpg'
577
+ },
578
+
579
+ // Optional social-post cards (always HScroll).
580
+ posts: [{
581
+ title: 'Release announcement',
582
+ username: 'codyai',
583
+ profile: 'https://example.com/profile.jpg',
584
+ thumbnail: 'https://example.com/post.jpg',
585
+ caption: 'Deployment completed.',
586
+ url: 'https://example.com/post',
587
+ source: 'INSTAGRAM',
588
+ verified: true
589
+ }],
590
+
591
+ // Optional source cards: [favicon URL, source URL, display text].
592
+ sources: [[
593
+ 'https://example.com/favicon.jpg',
594
+ 'https://example.com/docs',
595
+ 'Documentation'
596
+ ]],
597
+
598
+ // Optional playable video card. Metadata may be supplied manually.
599
+ inlineVideo: {
600
+ url: 'https://example.com/clip.mp4',
601
+ mimeType: 'video/mp4',
602
+ duration: 12,
603
+ fileLength: 123456
604
+ },
536
605
  suggested: true,
537
606
  footerText: 'Generated by CODY AI'
538
607
  }, undefined, {
@@ -580,6 +649,53 @@ await sock.richMenu(jid, {
580
649
  })
581
650
  ```
582
651
 
652
+ ### Large A2UI command card
653
+
654
+ Use `sendA2UICommandMenu` for the full-size card layout with an uploaded image header, a command/function table, URL buttons, and a native-flow command selector. This is an `interactiveMessage`/A2UI payload, not an AIRich `unifiedResponse`, so it is intentionally separate from `sendPlogmeMessage` and `richMenu`.
655
+
656
+ ```js
657
+ await sock.sendA2UICommandMenu(jid, {
658
+ image: 'https://example.com/cover.jpg',
659
+ title: 'CODY AI',
660
+ rows: [
661
+ ['.meid', 'Check ID'],
662
+ ['.fromid', 'Check Source'],
663
+ ['.ping', 'Check Speed']
664
+ ],
665
+ buttons: [
666
+ { text: '1st-Channel', url: 'https://example.com/first' },
667
+ { text: '2nd-Channel', url: 'https://example.com/second' }
668
+ ]
669
+ })
670
+ ```
671
+
672
+ The image URL is uploaded as the interactive header media. Rows are also available through the native-flow `single_select` menu, and button URLs are represented as A2UI `openUrl` actions.
673
+
674
+ ### Screenshot-style raw AIRich command card
675
+
676
+ Use `sendAIRichCommandMenu` when you want the manually serialized AIRich variant: an image primitive, widget CTAs, Markdown text with optional LaTeX inline metadata, a command table, and footer URL actions. Unlike A2UI, this path is an AIRich unified response and therefore uses the normal relay plus type-14 immediate-render edit.
677
+
678
+ ```js
679
+ await sock.sendAIRichCommandMenu(jid, {
680
+ image: 'https://example.com/cover.jpg',
681
+ title: 'CODY AI',
682
+ subtitle: 'Command List',
683
+ code: 'CODY AI',
684
+ rows: [
685
+ ['.meid', 'Check ID'],
686
+ ['.fromid', 'Check Source'],
687
+ ['.ping', 'Check Speed']
688
+ ],
689
+ latexImage: 'https://example.com/logo.png',
690
+ latexExpression: 'CODY',
691
+ buttons: [
692
+ { text: '1st-Channel', url: 'https://example.com/first' },
693
+ { text: '2nd-Channel', url: 'https://example.com/second' }
694
+ ],
695
+ footer: 'Developed by - CODY AI'
696
+ })
697
+ ```
698
+
583
699
  Cards and carousel layout are supported:
584
700
 
585
701
  ```js
@@ -5,7 +5,7 @@ import { proto } from '../../WAProto/index.js';
5
5
  import { BIZ_BOT_SUPPORT_PAYLOAD, DEFAULT_CACHE_TTLS, WA_DEFAULT_EPHEMERAL } from '../Defaults/index.js';
6
6
  import { aggregateMessageKeysNotFromMe, assertMediaContent, assertMeId, bindWaitForEvent, buildLinkPreview, decryptMediaRetryData, DEF_MEDIA_HOST, delay, encodeNewsletterMessage, encodeSignedDeviceIdentity, encodeWAMessage, encryptMediaRetryRequest, extractDeviceJids, extractImageThumb, generateMessageIDV2, generateParticipantHashV2, generateWAMessage, generateWAMessageFromContent, getStatusCodeForMediaRetry, makeWhatsAppFlowButton, getUrlFromDirectPath, getWAUploadToServer, MessageRetryManager, normalizeMessageContent, parseAndInjectE2ESessions, prepareWAMessageMedia, shouldIncludeBizBinaryNode, unixTimestampSeconds } from '../Utils/index.js';
7
7
  import { getUrlInfo } from '../Utils/link-preview.js';
8
- import { prepareRichMenuMessage, prepareRichResponseMessage, prepareRichGenerationMessage, prepareCopyToClipboardMessage, prepareRichTextMessage, prepareRichQuoteMessage, prepareRichImageMessage, prepareRichLinkMessage, prepareGridImageMessage, prepareHtmlMessage, prepareRichHtmlMessage, attachBotVerificationProofs, hasVerifiableProofs } from '../Utils/rich-message-utils.js';
8
+ import { prepareRichMenuMessage, prepareA2UICommandMenu, prepareAIRichCommandMenu, prepareRichResponseMessage, prepareRichGenerationMessage, prepareCopyToClipboardMessage, prepareRichTextMessage, prepareRichQuoteMessage, prepareRichImageMessage, prepareRichLinkMessage, prepareGridImageMessage, prepareVerticalAlbumMessage, prepareVideoGridMessage, prepareHtmlMessage, prepareRichHtmlMessage, attachBotVerificationProofs, hasVerifiableProofs } from '../Utils/rich-message-utils.js';
9
9
  import { generateSlotMachineHtml } from '../Utils/games/slot-machine.js';
10
10
  import { generateMiniAppHtml } from '../Utils/games/miniapp.js';
11
11
  import { generateWebsitePreviewHtml, generateLinkPreviewHtml } from '../Utils/games/website-preview.js';
@@ -1276,18 +1276,39 @@ export const makeMessagesSocket = (config) => {
1276
1276
  },
1277
1277
  sendTable: async (jid, title, headers, rows, quoted, options = {}) => {
1278
1278
  const { message, messageId } = generateTableContent(title, headers, rows, quoted, options);
1279
- await relayMessage(jid, message, { messageId });
1280
- return { message, messageId };
1279
+ const fullMsg = generateWAMessageFromContent(jid, message, {
1280
+ logger,
1281
+ userJid: authState.creds.me?.id,
1282
+ messageId: options.messageId || messageId || generateMessageIDV2(jid),
1283
+ quoted,
1284
+ ...options
1285
+ });
1286
+ await relayRichMessage(jid, fullMsg, options);
1287
+ return fullMsg;
1281
1288
  },
1282
1289
  sendList: async (jid, title, items, quoted, options = {}) => {
1283
1290
  const { message, messageId } = generateListContent(title, items, quoted, options);
1284
- await relayMessage(jid, message, { messageId });
1285
- return { message, messageId };
1291
+ const fullMsg = generateWAMessageFromContent(jid, message, {
1292
+ logger,
1293
+ userJid: authState.creds.me?.id,
1294
+ messageId: options.messageId || messageId || generateMessageIDV2(jid),
1295
+ quoted,
1296
+ ...options
1297
+ });
1298
+ await relayRichMessage(jid, fullMsg, options);
1299
+ return fullMsg;
1286
1300
  },
1287
1301
  sendCodeBlock: async (jid, code, quoted, options = {}) => {
1288
1302
  const { message, messageId } = generateCodeBlockContent(code, quoted, options);
1289
- await relayMessage(jid, message, { messageId });
1290
- return { message, messageId };
1303
+ const fullMsg = generateWAMessageFromContent(jid, message, {
1304
+ logger,
1305
+ userJid: authState.creds.me?.id,
1306
+ messageId: options.messageId || messageId || generateMessageIDV2(jid),
1307
+ quoted,
1308
+ ...options
1309
+ });
1310
+ await relayRichMessage(jid, fullMsg, options);
1311
+ return fullMsg;
1291
1312
  },
1292
1313
  sendCarousel: async (jid, cards, options = {}) => {
1293
1314
  if (!Array.isArray(cards) || cards.length === 0) {
@@ -1323,8 +1344,15 @@ export const makeMessagesSocket = (config) => {
1323
1344
  },
1324
1345
  sendLatex: async (jid, quoted, options = {}) => {
1325
1346
  const { message, messageId } = generateLatexContent(quoted, { expressions: [], ...options });
1326
- await relayMessage(jid, message, { messageId });
1327
- return { message, messageId };
1347
+ const fullMsg = generateWAMessageFromContent(jid, message, {
1348
+ logger,
1349
+ userJid: authState.creds.me?.id,
1350
+ messageId: options.messageId || messageId || generateMessageIDV2(jid),
1351
+ quoted,
1352
+ ...options
1353
+ });
1354
+ await relayRichMessage(jid, fullMsg, options);
1355
+ return fullMsg;
1328
1356
  },
1329
1357
  /**
1330
1358
  * Send LaTeX as rendered PNG image (like Meta AI does).
@@ -1352,8 +1380,14 @@ export const makeMessagesSocket = (config) => {
1352
1380
  headerText,
1353
1381
  footer
1354
1382
  });
1355
- await relayMessage(jid, message, { messageId });
1356
- return { message, messageId };
1383
+ const fullMsg = generateWAMessageFromContent(jid, message, {
1384
+ logger,
1385
+ userJid: authState.creds.me?.id,
1386
+ messageId: options.messageId || messageId || generateMessageIDV2(jid),
1387
+ ...options
1388
+ });
1389
+ await relayRichMessage(jid, fullMsg, options);
1390
+ return fullMsg;
1357
1391
  },
1358
1392
  sendMetaAI: async (a, b, c = {}) => {
1359
1393
  // baron signature: (jid, text, opts) — README signature: (text, opts)
@@ -2353,6 +2387,52 @@ export const makeMessagesSocket = (config) => {
2353
2387
  }
2354
2388
  return fullMsg;
2355
2389
  };
2390
+ /**
2391
+ * Sends the A2UI interactive card/menu format used by WhatsApp's large
2392
+ * command-card presentation. This is intentionally separate from richMenu
2393
+ * because it is an interactiveMessage, not an AIRich unifiedResponse.
2394
+ */
2395
+ socket.sendA2UICommandMenu = async (jid, content = {}, options = {}) => {
2396
+ if (!jid) throw new Boom('sendA2UICommandMenu requires a target JID', { statusCode: 400 });
2397
+ const { image, ...menuContent } = content || {};
2398
+ let imageMessage;
2399
+ if (image) {
2400
+ const prepared = await prepareWAMessageMedia({ image }, { upload: waUploadToServer });
2401
+ imageMessage = prepared.imageMessage;
2402
+ }
2403
+ const wrapped = prepareA2UICommandMenu({ ...menuContent, imageMessage });
2404
+ const fullMsg = await generateWAMessageFromContent(jid, wrapped, {
2405
+ logger,
2406
+ userJid: authState.creds.me?.id,
2407
+ messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id || jid),
2408
+ ...options,
2409
+ });
2410
+ await relayMessage(jid, fullMsg.message, { messageId: fullMsg.key.id, ...options.relayOptions });
2411
+ if (config.emitOwnEvents) {
2412
+ process.nextTick(async () => {
2413
+ await messageMutex.mutex(() => upsertMessage(fullMsg, 'append'));
2414
+ });
2415
+ }
2416
+ return fullMsg;
2417
+ };
2418
+ /** Sends the manually serialized AIRich command card shown in the screenshot. */
2419
+ socket.sendAIRichCommandMenu = async (jid, content = {}, options = {}) => {
2420
+ if (!jid) throw new Boom('sendAIRichCommandMenu requires a target JID', { statusCode: 400 });
2421
+ const wrapped = prepareAIRichCommandMenu(content);
2422
+ const fullMsg = await generateWAMessageFromContent(jid, wrapped, {
2423
+ logger,
2424
+ userJid: authState.creds.me?.id,
2425
+ messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id || jid),
2426
+ ...options,
2427
+ });
2428
+ await relayRichMessage(jid, fullMsg, options);
2429
+ if (config.emitOwnEvents) {
2430
+ process.nextTick(async () => {
2431
+ await messageMutex.mutex(() => upsertMessage(fullMsg, 'append'));
2432
+ });
2433
+ }
2434
+ return fullMsg;
2435
+ };
2356
2436
  /** Sends a rich response with experimental BotInfrastructureDiagnostics metadata. */
2357
2437
  socket.sendBotDiagnostics = async (jid, content = {}, options = {}) => {
2358
2438
  if (!jid) throw new Boom('sendBotDiagnostics requires a target JID', { statusCode: 400 });
@@ -2616,38 +2696,58 @@ export const makeMessagesSocket = (config) => {
2616
2696
 
2617
2697
 
2618
2698
  /**
2619
- * Sends an AIRich GRID_IMAGE album. Unlike the legacy albumMessage
2620
- * collection, this is one forwarded unified-response message containing
2621
- * the requested image primitives.
2699
+ * Sends a full-width vertical AIRich album. Unlike the legacy
2700
+ * albumMessage collection or compact GRID_IMAGE layout, this is one
2701
+ * forwarded unified-response message with one large media item per row.
2622
2702
  */
2623
2703
  socket.sendAlbumMessage = async (jid, medias = [], options = {}) => {
2624
2704
  if (!jid) throw new Boom('sendAlbumMessage requires a target JID', { statusCode: 400 });
2625
2705
  if (!Array.isArray(medias) || medias.length < 2) {
2626
2706
  throw new Boom('sendAlbumMessage requires at least two media items', { statusCode: 400 });
2627
2707
  }
2628
- const imageUrls = medias.map((media) => {
2708
+ const albumItems = medias.map((media) => {
2629
2709
  const raw = media?.type && media?.data ? media.data : (media?.image ?? media);
2630
2710
  if (media?.video || media?.type === 'video') {
2631
- throw new Boom('GRID_IMAGE albums support images only', { statusCode: 400 });
2632
- }
2633
- const sourceUrl = typeof raw === 'string' ? raw : (raw?.sourceUrl || raw?.url || '');
2634
- if (!sourceUrl) {
2635
- throw new Boom('Each GRID_IMAGE album item requires an image URL', { statusCode: 400 });
2711
+ const video = media?.video || raw;
2712
+ const sourceUrl = typeof video === 'string' ? video : (video?.url || video?.videoUrl || '');
2713
+ if (!sourceUrl) throw new Boom('Each video album item requires a video URL', { statusCode: 400 });
2714
+ return { type: 'video', video: { ...video, url: sourceUrl } };
2636
2715
  }
2637
2716
  return {
2638
- sourceUrl,
2639
- imagePreviewUrl: raw?.imagePreviewUrl || raw?.previewUrl || sourceUrl,
2640
- imageHighResUrl: raw?.imageHighResUrl || raw?.highResUrl || raw?.url || sourceUrl,
2641
- darkModePreviewUrl: raw?.darkModePreviewUrl,
2642
- darkModeHighResUrl: raw?.darkModeHighResUrl,
2643
- width: raw?.width || media?.width || 600,
2644
- height: raw?.height || media?.height || 400,
2645
- darkWidth: raw?.darkWidth || media?.darkWidth,
2646
- darkHeight: raw?.darkHeight || media?.darkHeight
2717
+ type: 'image',
2718
+ image: raw
2647
2719
  };
2648
2720
  });
2649
- const wrapped = prepareGridImageMessage({
2650
- imageUrls,
2721
+ const wrapped = prepareVerticalAlbumMessage({
2722
+ items: albumItems,
2723
+ verificationMetadata: options.verificationMetadata
2724
+ });
2725
+ const fullMsg = generateWAMessageFromContent(jid, wrapped, {
2726
+ logger,
2727
+ userJid: authState.creds.me?.id,
2728
+ messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id || jid),
2729
+ ...options
2730
+ });
2731
+ await relayRichMessage(jid, fullMsg, options);
2732
+ if (config.emitOwnEvents) {
2733
+ process.nextTick(async () => {
2734
+ await messageMutex.mutex(() => upsertMessage(fullMsg, 'append'));
2735
+ });
2736
+ }
2737
+ return fullMsg;
2738
+ };
2739
+
2740
+ /**
2741
+ * Sends a unified-response video grid. This uses GenAIVideoPrimitive items
2742
+ * inside GenAIGridLayoutViewModel; it is not the image-only GRID_IMAGE type.
2743
+ */
2744
+ socket.sendVideoGridMessage = async (jid, videos = [], options = {}) => {
2745
+ if (!jid) throw new Boom('sendVideoGridMessage requires a target JID', { statusCode: 400 });
2746
+ if (!Array.isArray(videos) || videos.length < 2) {
2747
+ throw new Boom('sendVideoGridMessage requires at least two videos', { statusCode: 400 });
2748
+ }
2749
+ const wrapped = prepareVideoGridMessage({
2750
+ videos,
2651
2751
  verificationMetadata: options.verificationMetadata
2652
2752
  });
2653
2753
  const fullMsg = generateWAMessageFromContent(jid, wrapped, {
@@ -472,6 +472,78 @@ export const prepareRichMenuMessage = (content = {}) => {
472
472
  * @param {'END'|'START'|'CENTER'} [options.alignment='END'] - Button alignment
473
473
  * @returns {Object} Wrapped botForwardedMessage ready for relayMessage
474
474
  */
475
+ /** Build the manually serialized AIRich command menu shown in the screenshot. */
476
+ export const prepareAIRichCommandMenu = ({ image, title = 'CODY AI', subtitle = '', code = '', rows = [], buttons = [], footer = 'Developed by - CODY AI', latexImage, latexExpression = '' } = {}) => {
477
+ if (!image) throw new Error('AIRich command menu requires an image URL');
478
+ if (!Array.isArray(rows)) throw new Error('AIRich command-menu rows must be an array');
479
+ if (!Array.isArray(buttons)) throw new Error('AIRich command-menu buttons must be an array');
480
+ const safeRows = rows.map((row, index) => ({ command: String(row?.[0] ?? row?.command ?? `command_${index + 1}`), description: String(row?.[1] ?? row?.description ?? '') }));
481
+ const table = ['Command | Function', '--- | ---', ...safeRows.map((row) => `${row.command} | ${row.description}`)].join('\n');
482
+ const inlineEntities = latexImage ? [{ key: 'NGA', metadata: { latex_expression: String(latexExpression), latex_image: { url: String(latexImage), width: 400, height: 400 }, font_height: 83.333, padding: 15, __typename: 'GenAILatexItem' } }] : [];
483
+ const text = [String(title), subtitle && String(subtitle), code && `Code: ${String(code)}`, '## Command List', table, footer && String(footer)].filter(Boolean).join('\n');
484
+ const imagePrimitive = { __typename: 'GenAIImagePrimitive', preview_image: { __typename: 'GenAIMediaItem', mime_type: 'image/png', url: String(image) }, full_image: { __typename: 'GenAIMediaItem', mime_type: 'image/png', url: String(image) } };
485
+ const ctas = buttons.map((button, index) => ({ label: String(button?.text || button?.label || `Button ${index + 1}`), state: 'PENDING', kind: 'OTHER', tool_call_id: String(button?.id || `airich_menu_${index + 1}`), toast: { label: String(button?.toast || ''), __typename: 'GenAIExtWidgetToast' }, __typename: 'GenAI3PExtWidgetCTA' }));
486
+ const footerPrimitives = buttons.map((button, index) => ({ cta_text: String(button?.text || button?.label || `Button ${index + 1}`), cta_type: 'OPEN_URL', cta_url: String(button?.url || ''), __typename: 'GenAIFooterActionPrimitive' }));
487
+ footerPrimitives.push({ cta_text: '☷ OPEN MENU', cta_type: 'OPEN_URL', cta_url: String(buttons[0]?.url || ''), __typename: 'GenAIFooterActionPrimitive' });
488
+ const sections = [
489
+ { __typename: 'GenAIUnifiedResponseSection', view_model: { __typename: 'GenAISingleLayoutViewModel', primitive: imagePrimitive } },
490
+ ...(ctas.length ? [{ __typename: 'GenAIUnifiedResponseSection', view_model: { primitives: [{ __typename: 'GenAI3PExtWidgetPrimitive', header: { __typename: 'GenAI3PExtWidgetStandardHeader', title: 'Command List' }, body: { __typename: 'GenAIExtCalendarEventList', ctas, sections: [] } }], __typename: 'GenAIActionRowLayoutViewModel' } }] : []),
491
+ { __typename: 'GenAIUnifiedResponseSection', view_model: { primitive: { text: `{{NGA}}${text}{{/NGA}}`, inline_entities: inlineEntities, __typename: 'GenAIMarkdownTextUXPrimitive' }, __typename: 'GenAISingleLayoutViewModel' } },
492
+ { __typename: 'GenAIUnifiedResponseSection', view_model: { primitives: footerPrimitives, __typename: 'GenAIActionRowLayoutViewModel' } }
493
+ ];
494
+ const richResponseMessage = proto.AIRichResponseMessage.create({
495
+ submessages: [{ messageType: RichSubMessageType.TEXT, messageText: text }],
496
+ messageType: proto.AIRichResponseMessageType.AI_RICH_RESPONSE_TYPE_STANDARD,
497
+ unifiedResponse: { data: Buffer.from(JSON.stringify({ response_id: randomUUID(), sections })) },
498
+ contextInfo: { isForwarded: true, forwardingScore: 1, forwardedAiBotMessageInfo: { botJid: '867051314767696@bot' }, forwardOrigin: 4 }
499
+ });
500
+ return wrapToBotForwardedMessage(richResponseMessage);
501
+ };
502
+
503
+ /** Build the A2UI interactive command menu used by the full-size card layout. */
504
+ export const prepareA2UICommandMenu = ({ imageMessage, title = 'CODY AI', rows = [], buttons = [], fallback = '' } = {}) => {
505
+ if (!Array.isArray(rows)) throw new Error('A2UI command-menu rows must be an array');
506
+ if (!Array.isArray(buttons)) throw new Error('A2UI command-menu buttons must be an array');
507
+ const safeRows = rows.map((row, index) => ({
508
+ title: String(row?.[0] ?? row?.title ?? `Command ${index + 1}`),
509
+ description: String(row?.[1] ?? row?.description ?? ''),
510
+ id: String(row?.[2] ?? row?.id ?? row?.[0] ?? `command_${index + 1}`)
511
+ }));
512
+ const safeButtons = buttons.map((button, index) => ({ id: `a2ui_button_${index + 1}`, label: String(button?.text || button?.label || `Button ${index + 1}`), url: String(button?.url || '') }));
513
+ const componentIds = ['title', 'divider', 'tableCard', 'footer', ...(safeButtons.length ? ['buttonRow'] : [])];
514
+ const components = [
515
+ { id: 'root', component: 'Column', align: 'center', children: componentIds },
516
+ { id: 'title', component: 'Text', text: String(title), variant: 'h2' },
517
+ { id: 'divider', component: 'Divider' },
518
+ { id: 'tableCard', component: 'Card', child: 'tableColumn' },
519
+ { id: 'tableColumn', component: 'Column', children: ['headerRow', ...safeRows.flatMap((_, index) => [`divider_${index}`, `row_${index}`])] },
520
+ { id: 'headerRow', component: 'Row', children: ['headerCommand', 'headerFunction'] },
521
+ { id: 'headerCommand', component: 'Text', text: 'Command', variant: 'h5' },
522
+ { id: 'headerFunction', component: 'Text', text: 'Function', variant: 'h5' },
523
+ ...safeRows.flatMap((row, index) => [
524
+ { id: `divider_${index}`, component: 'Divider' },
525
+ { id: `row_${index}`, component: 'Row', children: [`command_${index}`, `description_${index}`] },
526
+ { id: `command_${index}`, component: 'Text', text: row.title },
527
+ { id: `description_${index}`, component: 'Text', text: row.description }
528
+ ]),
529
+ { id: 'footer', component: 'Text', text: 'Developed by - CODY AI', variant: 'caption' }
530
+ ];
531
+ if (safeButtons.length) {
532
+ components.push({ id: 'buttonRow', component: 'Row', justify: 'spaceEvenly', children: safeButtons.flatMap((button, index) => [`buttonLabel_${index}`, `button_${index}`]) });
533
+ safeButtons.forEach((button, index) => {
534
+ components.push({ id: `buttonLabel_${index}`, component: 'Text', text: button.label });
535
+ components.push({ id: `button_${index}`, component: 'Button', child: `buttonLabel_${index}`, variant: 'primary', action: { call: 'openUrl', args: { url: button.url } } });
536
+ });
537
+ }
538
+ return {
539
+ interactiveMessage: {
540
+ ...(imageMessage ? { header: { imageMessage, hasMediaAttachment: true } } : {}),
541
+ bloksWidget: { type: 'im_a2ui', data: JSON.stringify({ version: 'v0.9', createSurface: { surfaceId: 'plogme-a2ui-command-menu', catalogId: 'https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json', components } }), fallback: String(fallback || title) },
542
+ nativeFlowMessage: { messageParamsJson: JSON.stringify({ limited_time_offer: { text: String(title), url: safeButtons[0]?.url || '', copy_code: String(title), expiration_time: Date.now() * 1000 } }), buttons: [{ name: 'single_select', buttonParamsJson: JSON.stringify({ title: String(title), sections: [{ title: 'Available Commands', rows: safeRows }] }) }] }
543
+ }
544
+ };
545
+ };
546
+
475
547
  export const prepareCopyToClipboardMessage = (options = {}) => {
476
548
  const { text = '', label, alignment = 'END', verificationMetadata } = options;
477
549
  const displayText = label || text;
@@ -697,9 +769,114 @@ export const prepareGridImageMessage = ({ imageUrls = [], verificationMetadata }
697
769
  return wrapToBotForwardedMessage(richResponseMessage, verificationMetadata);
698
770
  };
699
771
 
772
+ /**
773
+ * Build a video grid using the client-supported GenAIVideoPrimitive.
774
+ *
775
+ * WhatsApp exposes no AIRich GRID_VIDEO submessage type. Video grids therefore
776
+ * use the generic unified-response Grid layout with one GenAIVideoPrimitive per
777
+ * item. This is deliberately separate from prepareGridImageMessage so callers
778
+ * do not accidentally label video media as GenAIImagePrimitive.
779
+ */
780
+ /** Build a full-width vertical album with one large media item per row. */
781
+ export const prepareVerticalAlbumMessage = ({ items = [], verificationMetadata } = {}) => {
782
+ if (!Array.isArray(items) || items.length < 2) throw new Error('Vertical album requires at least two media items');
783
+ const expiration = String(Date.now() + 30 * 24 * 60 * 60 * 1000);
784
+ const primitives = items.map((item) => {
785
+ const kind = item?.type || (item?.video ? 'video' : 'image');
786
+ if (kind === 'video') {
787
+ const video = item.video || item.data || item;
788
+ const url = typeof video === 'string' ? video : (video?.url || video?.videoUrl || '');
789
+ if (!url) throw new Error('Each video album item requires a video URL');
790
+ return buildImaginePrimitive({ mediaType: 'video', url, thumbnail: video?.thumbnail || video?.thumbnailUrl, mimeType: video?.mimeType || video?.mime_type || 'video/mp4', fileLength: video?.fileLength || video?.file_length || 0, duration: video?.duration || 0, status: 'READY' });
791
+ }
792
+ const image = item?.image || item?.data || item;
793
+ const sourceUrl = typeof image === 'string' ? image : (image?.sourceUrl || image?.url || '');
794
+ if (!sourceUrl) throw new Error('Each image album item requires an image URL');
795
+ const previewUrl = image?.imagePreviewUrl || image?.previewUrl || sourceUrl;
796
+ const fullUrl = image?.imageHighResUrl || image?.highResUrl || image?.url || sourceUrl;
797
+ const width = image?.width || item?.width || 600;
798
+ const height = image?.height || item?.height || 400;
799
+ const mimeType = image?.mimeType || image?.mime_type || 'image/png';
800
+ return {
801
+ preview_image: { url: previewUrl, mime_type: mimeType, expiration_timestamp_ms: expiration, width, height },
802
+ full_image: { url: fullUrl, mime_type: mimeType, expiration_timestamp_ms: expiration, width, height },
803
+ dark_mode_preview_image: { url: image?.darkModePreviewUrl || previewUrl, mime_type: mimeType, expiration_timestamp_ms: expiration, width, height },
804
+ dark_mode_full_image: { url: image?.darkModeHighResUrl || image?.darkModePreviewUrl || fullUrl, mime_type: mimeType, expiration_timestamp_ms: expiration, width, height },
805
+ asset_query_status: 'FETCHED',
806
+ __typename: 'GenAIImagePrimitive'
807
+ };
808
+ });
809
+ const richResponseMessage = proto.AIRichResponseMessage.create({
810
+ submessages: [],
811
+ messageType: proto.AIRichResponseMessageType.AI_RICH_RESPONSE_TYPE_STANDARD,
812
+ unifiedResponse: { data: Buffer.from(JSON.stringify({ response_id: randomUUID(), sections: [{ view_model: { primitives, __typename: 'GenAIVStackLayoutViewModel' } }] })) },
813
+ contextInfo: { isForwarded: true, forwardingScore: 1, forwardedAiBotMessageInfo: { botJid: '867051314767696@bot' }, forwardOrigin: 4 }
814
+ });
815
+ return wrapToBotForwardedMessage(richResponseMessage, verificationMetadata);
816
+ };
817
+
818
+ export const prepareVideoGridMessage = ({ videos = [], verificationMetadata } = {}) => {
819
+ if (!Array.isArray(videos) || videos.length < 2) {
820
+ throw new Error('Video grid requires at least two videos');
821
+ }
822
+ const primitives = videos.map((video) => {
823
+ const url = typeof video === 'string' ? video : (video?.url || video?.videoUrl || video?.reelsUrl || '');
824
+ if (!url) throw new Error('Each video-grid item requires a video URL');
825
+ const item = typeof video === 'string' ? {} : video;
826
+ return {
827
+ post_id: item.postId == null ? undefined : String(item.postId),
828
+ reels_url: url,
829
+ reels_title: item.title,
830
+ thumbnail_url: item.thumbnailUrl || item.thumbnail,
831
+ creator: item.creator,
832
+ avatar_url: item.avatarUrl,
833
+ content_hash: item.contentHash,
834
+ likes_count: item.likes,
835
+ comments_count: item.comments,
836
+ shares_count: item.shares,
837
+ is_verified: item.isVerified,
838
+ source_app: item.sourceApp,
839
+ video_delivery_response: {
840
+ progressive_urls: (item.progressiveUrls || []).map((value) => ({
841
+ progressive_url: value,
842
+ __typename: 'GenAIProgressiveUrlResponse'
843
+ })),
844
+ dash_manifests: (item.dashManifests || []).map((value) => ({
845
+ manifest_xml: value,
846
+ __typename: 'GenAIDashManifestResponse'
847
+ })),
848
+ __typename: 'GenAIVideoDeliveryResponse'
849
+ },
850
+ __typename: 'GenAIVideoPrimitive'
851
+ };
852
+ });
853
+ const unified = {
854
+ response_id: randomUUID(),
855
+ sections: [{
856
+ view_model: {
857
+ primitives,
858
+ __typename: 'GenAIGridLayoutViewModel'
859
+ }
860
+ }]
861
+ };
862
+ const richResponseMessage = proto.AIRichResponseMessage.create({
863
+ submessages: [],
864
+ messageType: proto.AIRichResponseMessageType.AI_RICH_RESPONSE_TYPE_STANDARD,
865
+ unifiedResponse: { data: Buffer.from(JSON.stringify(unified)) },
866
+ contextInfo: {
867
+ isForwarded: true,
868
+ forwardingScore: 1,
869
+ forwardedAiBotMessageInfo: { botJid: '867051314767696@bot' },
870
+ forwardOrigin: 4
871
+ }
872
+ });
873
+ return wrapToBotForwardedMessage(richResponseMessage, verificationMetadata);
874
+ };
875
+
700
876
  export const prepareRichResponseMessage = (content) => {
701
877
  const { alignment, code, contentText, disclaimerText, footerText, headerText, imageText, inlineImage, inlineVideo, items, language, latex, links, noDonation, noHeading, posts, products, reels, sources: sourceInputs, suggested, richResponse, table, tapLinkUrl, title, verificationMetadata } = content;
702
878
  let submessages = [];
879
+ const extraSections = [];
703
880
  if (Array.isArray(richResponse)) {
704
881
  submessages = richResponse.map((submessage) => {
705
882
  if (submessage.text) {
@@ -843,9 +1020,21 @@ export const prepareRichResponseMessage = (content) => {
843
1020
  });
844
1021
  }
845
1022
  if (inlineVideo) {
846
- submessages.push({
847
- messageType: RichSubMessageType.TEXT,
848
- messageText: 'INLINE_VIDEO'
1023
+ const video = typeof inlineVideo === 'string' ? { url: inlineVideo } : inlineVideo;
1024
+ submessages.push({ messageType: RichSubMessageType.TEXT, messageText: '[ CANNOT_LOAD_VIDEO - NIXEL ]' });
1025
+ extraSections.push({
1026
+ view_model: {
1027
+ primitive: buildImaginePrimitive({
1028
+ mediaType: 'video',
1029
+ url: video?.url || video?.videoUrl || '',
1030
+ mimeType: video?.mimeType || video?.mime_type || 'video/mp4',
1031
+ fileLength: video?.fileLength || video?.file_length || 0,
1032
+ duration: video?.duration || 0,
1033
+ thumbnail: video?.thumbnail,
1034
+ status: video?.status || 'READY'
1035
+ }),
1036
+ __typename: 'GenAISingleLayoutViewModel'
1037
+ }
849
1038
  });
850
1039
  }
851
1040
  if (latex) {
@@ -863,7 +1052,40 @@ export const prepareRichResponseMessage = (content) => {
863
1052
  title: 'PAIR CRYSN⚉VA AI ⩇⩇:⩇⩇',
864
1053
  displayName: 'pair bot'
865
1054
  };
866
- const sourceLinks = (sourceInputs || links || []).map((linkField) => {
1055
+ const rawSources = Array.isArray(sourceInputs) ? sourceInputs : [];
1056
+ // Existing `sources: [{ url, displayName, subtitle }]` callers use
1057
+ // citation entities. The serialized AIRich addSource format is the
1058
+ // distinct tuple form: `[[faviconUrl, sourceUrl, displayText], ...]`.
1059
+ const sourceTuples = rawSources.length > 0 && rawSources.every((source) => Array.isArray(source));
1060
+ if (sourceTuples) {
1061
+ const sourceCards = rawSources.map((source) => {
1062
+ const item = Array.isArray(source)
1063
+ ? { icon: source[0], url: source[1], text: source[2] }
1064
+ : source;
1065
+ const sourceUrl = item.url || item.sourceUrl || item.source_url || '';
1066
+ return {
1067
+ source_type: 'THIRD_PARTY',
1068
+ source_display_name: item.text || item.displayName || item.sourceDisplayName || '',
1069
+ source_subtitle: item.subtitle || 'AI',
1070
+ source_url: sourceUrl,
1071
+ favicon: {
1072
+ url: item.icon || item.iconUrl || item.favicon || '',
1073
+ mime_type: item.mimeType || 'image/jpeg',
1074
+ width: item.width || 16,
1075
+ height: item.height || 16
1076
+ }
1077
+ };
1078
+ }).filter((source) => source.source_url || source.source_display_name);
1079
+ if (sourceCards.length) {
1080
+ extraSections.push({
1081
+ view_model: {
1082
+ primitive: { sources: sourceCards, __typename: 'GenAISearchResultPrimitive' },
1083
+ __typename: 'GenAISingleLayoutViewModel'
1084
+ }
1085
+ });
1086
+ }
1087
+ }
1088
+ const sourceLinks = (sourceTuples ? [] : (sourceInputs || links || [])).map((linkField) => {
867
1089
  if (typeof linkField === 'string') return { text: '', url: linkField, displayName: linkField };
868
1090
  return { text: linkField?.text || linkField?.displayName || linkField?.title || '', ...linkField };
869
1091
  });
@@ -898,14 +1120,53 @@ export const prepareRichResponseMessage = (content) => {
898
1120
  if (posts) {
899
1121
  submessages.push({
900
1122
  messageType: RichSubMessageType.TEXT,
901
- messageText: 'POSTS'
1123
+ messageText: '[ CANNOT_LOAD_POST - NIXEL ]'
902
1124
  });
1125
+ const postItems = (Array.isArray(posts) ? posts : [posts]).map((post) => ({
1126
+ title: post?.title || '',
1127
+ subtitle: post?.subtitle || '',
1128
+ username: post?.username || post?.user || '',
1129
+ profile_picture_url: post?.profile_picture_url || post?.profile || post?.profile_url || '',
1130
+ is_verified: !!(post?.is_verified ?? post?.verified),
1131
+ thumbnail_url: post?.thumbnail_url || post?.thumbnail || '',
1132
+ post_caption: post?.post_caption || post?.caption || '',
1133
+ likes_count: post?.likes_count ?? post?.like ?? 0,
1134
+ comments_count: post?.comments_count ?? post?.comment ?? 0,
1135
+ shares_count: post?.shares_count ?? post?.share ?? 0,
1136
+ post_url: post?.post_url || post?.url || '',
1137
+ post_deeplink: post?.post_deeplink || post?.deeplink || '',
1138
+ source_app: post?.source_app || post?.source || '',
1139
+ footer_label: post?.footer_label || post?.footer || '',
1140
+ footer_icon: post?.footer_icon || post?.icon || '',
1141
+ is_carousel: post?.is_carousel ?? true,
1142
+ orientation: post?.orientation || 'LANDSCAPE',
1143
+ post_type: post?.post_type || 'VIDEO',
1144
+ __typename: 'GenAIPostPrimitive'
1145
+ }));
1146
+ if (postItems.length) extraSections.push({ view_model: { primitives: postItems, __typename: 'GenAIHScrollLayoutViewModel' } });
903
1147
  }
904
1148
  if (products) {
905
1149
  submessages.push({
906
1150
  messageType: RichSubMessageType.TEXT,
907
- messageText: 'PRODUCTS'
1151
+ messageText: '[ CANNOT_LOAD_PRODUCT - NIXEL ]'
908
1152
  });
1153
+ const productItems = (Array.isArray(products) ? products : [products]).map((product) => ({
1154
+ title: product?.title || '',
1155
+ brand: product?.brand || '',
1156
+ price: product?.price || '',
1157
+ sale_price: product?.sale_price || product?.salePrice || '',
1158
+ product_url: product?.product_url || product?.url || '',
1159
+ image: { url: product?.image?.url || product?.image || '' },
1160
+ additional_images: [{ url: product?.icon_url || product?.icon || '' }],
1161
+ __typename: 'GenAIProductItemCardPrimitive'
1162
+ }));
1163
+ if (productItems.length) {
1164
+ extraSections.push({
1165
+ view_model: Array.isArray(products)
1166
+ ? { primitives: productItems, __typename: 'GenAIHScrollLayoutViewModel' }
1167
+ : { primitive: productItems[0], __typename: 'GenAISingleLayoutViewModel' }
1168
+ });
1169
+ }
909
1170
  }
910
1171
  if (suggested) {
911
1172
  submessages.push({
@@ -934,6 +1195,7 @@ export const prepareRichResponseMessage = (content) => {
934
1195
  }
935
1196
  const uuid = randomUUID();
936
1197
  const unified = toUnified(submessages, uuid);
1198
+ unified.sections = [...unified.sections.filter(Boolean), ...extraSections];
937
1199
  const richResponseMessage = proto.AIRichResponseMessage.create({
938
1200
  submessages,
939
1201
  messageType: proto.AIRichResponseMessageType.AI_RICH_RESPONSE_TYPE_STANDARD,
package/lib/index.d.ts CHANGED
@@ -224,7 +224,8 @@ export interface RelayMessageOptions {
224
224
 
225
225
  export interface AlbumMediaInput {
226
226
  image?: unknown;
227
- type?: 'image';
227
+ video?: unknown;
228
+ type?: 'image' | 'video';
228
229
  data?: unknown;
229
230
  sourceUrl?: string;
230
231
  imagePreviewUrl?: string;
@@ -246,10 +247,50 @@ export interface AnimatedWallpaperInput {
246
247
  opacity?: number;
247
248
  }
248
249
 
250
+ export interface A2UICommandMenuContent {
251
+ image?: unknown;
252
+ title?: string;
253
+ rows?: Array<[string, string, string?] | { title?: string; description?: string; id?: string }>;
254
+ buttons?: Array<{ text?: string; label?: string; url?: string }>;
255
+ fallback?: string;
256
+ }
257
+
258
+ export interface AIRichCommandMenuContent {
259
+ image: string;
260
+ title?: string;
261
+ subtitle?: string;
262
+ code?: string;
263
+ rows?: Array<[string, string] | { command?: string; description?: string }>;
264
+ latexImage?: string;
265
+ latexExpression?: string;
266
+ buttons?: Array<{ id?: string; text?: string; label?: string; url?: string; toast?: string }>;
267
+ footer?: string;
268
+ }
269
+
249
270
  export interface WASocket {
250
271
  sendMessage(jid: string | string[], content: StatusContent | GroupStatusContent | Record<string, unknown>, options?: Partial<StatusOptions> & Record<string, unknown>): Promise<unknown>;
251
- /** Send at least two image URL items as an AIRich GRID_IMAGE album. */
272
+ /** Send at least two image/video URL items as a full-width AIRich vertical album. */
252
273
  sendAlbumMessage(jid: string, medias: AlbumMediaInput[], options?: Record<string, unknown>): Promise<unknown>;
274
+ /** Send an AIRich unified-response grid of GenAIVideoPrimitive items. Requires at least two video URLs. */
275
+ sendVideoGridMessage(jid: string, videos: Array<string | {
276
+ url?: string;
277
+ videoUrl?: string;
278
+ reelsUrl?: string;
279
+ postId?: string | number;
280
+ title?: string;
281
+ thumbnailUrl?: string;
282
+ thumbnail?: string;
283
+ creator?: string;
284
+ avatarUrl?: string;
285
+ contentHash?: string;
286
+ likes?: number;
287
+ comments?: number;
288
+ shares?: number;
289
+ isVerified?: boolean;
290
+ sourceApp?: string;
291
+ progressiveUrls?: string[];
292
+ dashManifests?: string[];
293
+ }>, options?: Record<string, unknown>): Promise<unknown>;
253
294
  /** Update the connected account's WhatsApp About/profile status. */
254
295
  updateProfileStatus(status: string): Promise<void>;
255
296
  /** Update the connected account's profile name. */
@@ -260,6 +301,10 @@ export interface WASocket {
260
301
  relayMessage(jid: string, message: Record<string, unknown>, options?: RelayMessageOptions): Promise<unknown>;
261
302
  /** Send an experimental Meta AI-style rich menu with image, card buttons, and an optional URL footer. */
262
303
  richMenu(jid: string, content?: RichMenuContent, options?: Record<string, unknown>): Promise<unknown>;
304
+ /** Send the large A2UI card/menu with an image header, command table, URL buttons, and native-flow selector. */
305
+ sendA2UICommandMenu(jid: string, content?: A2UICommandMenuContent, options?: Record<string, unknown>): Promise<unknown>;
306
+ /** Send the manually serialized AIRich command card with image, Markdown, widget CTAs, and footer actions. */
307
+ sendAIRichCommandMenu(jid: string, content: AIRichCommandMenuContent, options?: Record<string, unknown>): Promise<unknown>;
263
308
  /** Send raw HTML through the experimental WhatsApp HTML primitive. */
264
309
  sendHtmlMessage(jid: string, content: { html: string; trustedSources?: string[] }, options?: Record<string, unknown>): Promise<unknown>;
265
310
  /** Post a personal status using explicit viewers or a configured contact source/provider. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plogme",
3
- "version": "2.0.3",
3
+ "version": "2.0.4",
4
4
  "description": "Production-ready Baileys fork ⚉ with AI groups, Meta AI prompting, safe compositing fallbacks, rich responses, interactive messages, albums, newsletters, and communities — without antiban wrappers.",
5
5
  "main": "lib/index.js",
6
6
  "type": "module",