plogme 2.0.4 → 2.0.6

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.
@@ -1863,6 +1863,9 @@ export const makeChatsSocket = (config) => {
1863
1863
  getUserDisclosures,
1864
1864
  acceptTosNotice,
1865
1865
  reportSpam,
1866
+ reportUser,
1867
+ reportMessage,
1868
+ reportAndBlockUser,
1866
1869
  toggleGroupHistory,
1867
1870
  getOptOutList,
1868
1871
  signPrivateCredential,
@@ -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, prepareA2UICommandMenu, prepareAIRichCommandMenu, prepareRichResponseMessage, prepareRichGenerationMessage, prepareCopyToClipboardMessage, prepareRichTextMessage, prepareRichQuoteMessage, prepareRichImageMessage, prepareRichLinkMessage, prepareGridImageMessage, prepareVerticalAlbumMessage, prepareVideoGridMessage, prepareHtmlMessage, prepareRichHtmlMessage, attachBotVerificationProofs, hasVerifiableProofs } from '../Utils/rich-message-utils.js';
8
+ import { prepareRichMenuMessage, prepareA2UIMessage, prepareA2UICommandMenu, prepareAIRichCommandMenu, prepareSocialEntityMessage, prepareRichResponseMessage, prepareRichGenerationMessage, prepareCopyToClipboardMessage, prepareRichTextMessage, prepareRichQuoteMessage, prepareRichImageMessage, prepareRichLinkMessage, prepareSocialProfileMessage, prepareUnifiedResponseMessage, 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';
@@ -1051,7 +1051,7 @@ export const makeMessagesSocket = (config) => {
1051
1051
  await relayMessage(jid, fullMsg.message, relayOptions);
1052
1052
 
1053
1053
  const richEnvelope = fullMsg.message?.botForwardedMessage?.message?.richResponseMessage;
1054
- if (options.renderRichResponse !== false && richEnvelope) {
1054
+ if (options.renderRichResponse !== false && (richEnvelope || options.forceRichEdit || fullMsg.message?.interactiveMessage?.bloksWidget)) {
1055
1055
  const editContent = proto.Message.fromObject({
1056
1056
  botForwardedMessage: {
1057
1057
  message: {
@@ -2370,6 +2370,76 @@ export const makeMessagesSocket = (config) => {
2370
2370
  }
2371
2371
  return fullMsg;
2372
2372
  };
2373
+ const sendSocialProfile = async (methodName, jid, content, options, prepare) => {
2374
+ if (!jid) throw new Boom(`${methodName} requires a target JID`, { statusCode: 400 });
2375
+ const wrapped = prepare(content);
2376
+ const fullMsg = await generateWAMessageFromContent(jid, wrapped, {
2377
+ logger,
2378
+ userJid: authState.creds.me?.id,
2379
+ messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id),
2380
+ ...options
2381
+ });
2382
+ await relayMessage(jid, fullMsg.message, {
2383
+ messageId: fullMsg.key.id,
2384
+ useCachedGroupMetadata: options.useCachedGroupMetadata,
2385
+ statusJidList: options.statusJidList
2386
+ });
2387
+ if (config.emitOwnEvents) {
2388
+ process.nextTick(async () => {
2389
+ await messageMutex.mutex(() => upsertMessage(fullMsg, 'append'));
2390
+ });
2391
+ }
2392
+ return fullMsg;
2393
+ };
2394
+ socket.sendSocialProfile = async (jid, content = {}, options = {}) => sendSocialProfile('sendSocialProfile', jid, content, options, prepareSocialProfileMessage);
2395
+ socket.sendInstagramProfile = async (jid, content = {}, options = {}) => sendSocialProfile(
2396
+ 'sendInstagramProfile',
2397
+ jid,
2398
+ { ...content, platform: 'instagram', entityType: content.entityType || 'IG_PROFILE', entityUrl: content.entityUrl || `https://www.instagram.com/${content.username || ''}` },
2399
+ options,
2400
+ prepareInstagramProfileMessage
2401
+ );
2402
+ socket.sendFacebookProfile = async (jid, content = {}, options = {}) => sendSocialProfile(
2403
+ 'sendFacebookProfile', jid,
2404
+ { ...content, platform: 'facebook', entityType: content.entityType || 'FB_PROFILE', entityUrl: content.entityUrl || `https://www.facebook.com/${content.username || ''}` },
2405
+ options, prepareSocialProfileMessage
2406
+ );
2407
+ socket.sendTikTokProfile = async (jid, content = {}, options = {}) => sendSocialProfile(
2408
+ 'sendTikTokProfile', jid,
2409
+ { ...content, platform: 'tiktok', entityType: content.entityType || 'TIKTOK_PROFILE', entityUrl: content.entityUrl || `https://www.tiktok.com/@${content.username || ''}` },
2410
+ options, prepareSocialProfileMessage
2411
+ );
2412
+ socket.sendYouTubeProfile = async (jid, content = {}, options = {}) => sendSocialProfile(
2413
+ 'sendYouTubeProfile', jid,
2414
+ { ...content, platform: 'youtube', entityType: content.entityType || 'YOUTUBE_PROFILE', entityUrl: content.entityUrl || `https://www.youtube.com/@${content.username || ''}` },
2415
+ options, prepareSocialProfileMessage
2416
+ );
2417
+ socket.sendXProfile = async (jid, content = {}, options = {}) => sendSocialProfile(
2418
+ 'sendXProfile', jid,
2419
+ { ...content, platform: 'x', entityType: content.entityType || 'X_PROFILE', entityUrl: content.entityUrl || `https://x.com/${content.username || ''}` },
2420
+ options, prepareSocialProfileMessage
2421
+ );
2422
+ socket.sendUnifiedResponse = async (jid, content = {}, options = {}) => {
2423
+ if (!jid) throw new Boom('sendUnifiedResponse requires a target JID', { statusCode: 400 });
2424
+ const wrapped = prepareUnifiedResponseMessage(content);
2425
+ const fullMsg = await generateWAMessageFromContent(jid, wrapped, {
2426
+ logger,
2427
+ userJid: authState.creds.me?.id,
2428
+ messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id),
2429
+ ...options
2430
+ });
2431
+ await relayMessage(jid, fullMsg.message, {
2432
+ messageId: fullMsg.key.id,
2433
+ useCachedGroupMetadata: options.useCachedGroupMetadata,
2434
+ statusJidList: options.statusJidList
2435
+ });
2436
+ if (config.emitOwnEvents) {
2437
+ process.nextTick(async () => {
2438
+ await messageMutex.mutex(() => upsertMessage(fullMsg, 'append'));
2439
+ });
2440
+ }
2441
+ return fullMsg;
2442
+ };
2373
2443
  socket.richMenu = async (jid, content = {}, options = {}) => {
2374
2444
  if (!jid) throw new Boom('richMenu requires a target JID', { statusCode: 400 });
2375
2445
  const wrapped = prepareRichMenuMessage(content);
@@ -2407,7 +2477,7 @@ export const makeMessagesSocket = (config) => {
2407
2477
  messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id || jid),
2408
2478
  ...options,
2409
2479
  });
2410
- await relayMessage(jid, fullMsg.message, { messageId: fullMsg.key.id, ...options.relayOptions });
2480
+ await relayRichMessage(jid, fullMsg, { ...options, forceRichEdit: true });
2411
2481
  if (config.emitOwnEvents) {
2412
2482
  process.nextTick(async () => {
2413
2483
  await messageMutex.mutex(() => upsertMessage(fullMsg, 'append'));
@@ -2415,6 +2485,27 @@ export const makeMessagesSocket = (config) => {
2415
2485
  }
2416
2486
  return fullMsg;
2417
2487
  };
2488
+ /** Send arbitrary A2UI/interactive payloads; sendA2UICommandMenu is a preset wrapper. */
2489
+ socket.sendA2UIMessage = async (jid, content = {}, options = {}) => {
2490
+ if (!jid) throw new Boom('sendA2UIMessage requires a target JID', { statusCode: 400 });
2491
+ const { image, ...payload } = content || {};
2492
+ let imageMessage;
2493
+ if (image) {
2494
+ const prepared = await prepareWAMessageMedia({ image }, { upload: waUploadToServer });
2495
+ imageMessage = prepared.imageMessage;
2496
+ }
2497
+ const wrapped = prepareA2UIMessage({ ...payload, imageMessage });
2498
+ const fullMsg = await generateWAMessageFromContent(jid, wrapped, {
2499
+ logger,
2500
+ userJid: authState.creds.me?.id,
2501
+ messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id || jid),
2502
+ ...options,
2503
+ });
2504
+ await relayRichMessage(jid, fullMsg, { ...options, forceRichEdit: true });
2505
+ if (config.emitOwnEvents) process.nextTick(async () => { await messageMutex.mutex(() => upsertMessage(fullMsg, 'append')); });
2506
+ return fullMsg;
2507
+ };
2508
+
2418
2509
  /** Sends the manually serialized AIRich command card shown in the screenshot. */
2419
2510
  socket.sendAIRichCommandMenu = async (jid, content = {}, options = {}) => {
2420
2511
  if (!jid) throw new Boom('sendAIRichCommandMenu requires a target JID', { statusCode: 400 });
@@ -2433,6 +2524,36 @@ export const makeMessagesSocket = (config) => {
2433
2524
  }
2434
2525
  return fullMsg;
2435
2526
  };
2527
+ /** Send a provider-neutral AIRich social/entity result card. */
2528
+ socket.sendSocialEntity = async (jid, content = {}, options = {}) => {
2529
+ if (!jid) throw new Boom('sendSocialEntity requires a target JID', { statusCode: 400 });
2530
+ const wrapped = prepareSocialEntityMessage(content);
2531
+ const fullMsg = await generateWAMessageFromContent(jid, wrapped, {
2532
+ logger,
2533
+ userJid: authState.creds.me?.id,
2534
+ messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id || jid),
2535
+ ...options,
2536
+ });
2537
+ await relayRichMessage(jid, fullMsg, options);
2538
+ if (config.emitOwnEvents) process.nextTick(async () => { await messageMutex.mutex(() => upsertMessage(fullMsg, 'append')); });
2539
+ return fullMsg;
2540
+ };
2541
+
2542
+ /** Send the primary Instagram profile result using the universal social/entity API. */
2543
+ socket.sendInstagramProfile = async (jid, content = {}, options = {}) => {
2544
+ if (!jid) throw new Boom('sendInstagramProfile requires a target JID', { statusCode: 400 });
2545
+ const wrapped = prepareInstagramProfileMessage(content);
2546
+ const fullMsg = await generateWAMessageFromContent(jid, wrapped, {
2547
+ logger,
2548
+ userJid: authState.creds.me?.id,
2549
+ messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id || jid),
2550
+ ...options,
2551
+ });
2552
+ await relayRichMessage(jid, fullMsg, options);
2553
+ if (config.emitOwnEvents) process.nextTick(async () => { await messageMutex.mutex(() => upsertMessage(fullMsg, 'append')); });
2554
+ return fullMsg;
2555
+ };
2556
+
2436
2557
  /** Sends a rich response with experimental BotInfrastructureDiagnostics metadata. */
2437
2558
  socket.sendBotDiagnostics = async (jid, content = {}, options = {}) => {
2438
2559
  if (!jid) throw new Boom('sendBotDiagnostics requires a target JID', { statusCode: 400 });
@@ -2484,7 +2605,7 @@ export const makeMessagesSocket = (config) => {
2484
2605
  const wrapped = buildBotRichResponse({
2485
2606
  text,
2486
2607
  botJid,
2487
- capabilities: [BotCapabilityType.AGENTIC_PLANNING],
2608
+ capabilities: [BotCapabilityType.AGENTIC_PLANNING, BotCapabilityType.QUERY_PLAN],
2488
2609
  botMetadata: {
2489
2610
  resolvedToolCallMetadata: {
2490
2611
  toolCallId,
@@ -2496,6 +2617,58 @@ export const makeMessagesSocket = (config) => {
2496
2617
  await relayRichMessage(jid, fullMsg, options);
2497
2618
  return fullMsg;
2498
2619
  };
2620
+ /**
2621
+ * Sends a real BotProgressIndicatorMetadata planning envelope. The plan is
2622
+ * application-controlled; this advertises the public schema capability but
2623
+ * does not claim to invoke Meta's private planning backend.
2624
+ */
2625
+ socket.sendBotPlan = async (jid, content = {}, options = {}) => {
2626
+ if (!jid) throw new Boom('sendBotPlan requires a target JID', { statusCode: 400 });
2627
+ const {
2628
+ text = 'Execution plan',
2629
+ title = 'Execution plan',
2630
+ steps = [],
2631
+ estimatedCompletionTime,
2632
+ botJid = '867051314767696@bot'
2633
+ } = content;
2634
+ if (!Array.isArray(steps) || steps.length === 0) {
2635
+ throw new Boom('sendBotPlan requires at least one step', { statusCode: 400 });
2636
+ }
2637
+ const planningStatus = proto.BotProgressIndicatorMetadata.BotPlanningStepMetadata.PlanningStepStatus;
2638
+ const normalizedSteps = steps.map((step, index) => ({
2639
+ statusTitle: String(step.title || step.name || `Step ${index + 1}`),
2640
+ statusBody: String(step.body || step.description || ''),
2641
+ status: step.status === 'executing' ? planningStatus.EXECUTING
2642
+ : step.status === 'finished' || step.status === 'completed' ? planningStatus.FINISHED
2643
+ : planningStatus.PLANNED,
2644
+ isReasoning: step.isReasoning === true,
2645
+ isEnhancedSearch: step.isEnhancedSearch === true,
2646
+ ...(Array.isArray(step.sections) ? { sections: step.sections } : {})
2647
+ }));
2648
+ const wrapped = {
2649
+ messageContextInfo: { botMetadata: {
2650
+ capabilityMetadata: {
2651
+ capabilities: [
2652
+ proto.BotCapabilityMetadata.BotCapabilityType.AGENTIC_PLANNING,
2653
+ proto.BotCapabilityMetadata.BotCapabilityType.QUERY_PLAN
2654
+ ]
2655
+ },
2656
+ progressIndicatorMetadata: {
2657
+ progressDescription: title,
2658
+ stepsMetadata: normalizedSteps,
2659
+ ...(estimatedCompletionTime ? { estimatedCompletionTime } : {})
2660
+ }
2661
+ } },
2662
+ botForwardedMessage: { message: { richResponseMessage: {
2663
+ messageType: 1,
2664
+ submessages: [{ messageType: 2, messageText: text }],
2665
+ contextInfo: { isForwarded: true, forwardingScore: 1, forwardedAiBotMessageInfo: { botJid }, forwardOrigin: 4 }
2666
+ } } }
2667
+ };
2668
+ const fullMsg = await generateWAMessageFromContent(jid, wrapped, { logger, userJid: authState.creds.me?.id, messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id), ...options });
2669
+ await relayMessage(jid, fullMsg.message, { messageId: fullMsg.key.id, useCachedGroupMetadata: options.useCachedGroupMetadata, statusJidList: options.statusJidList });
2670
+ return fullMsg;
2671
+ };
2499
2672
  socket.sendStatus = async (content, options = {}) => {
2500
2673
  const statusJidList = await resolveStatusRecipients(options.statusJidList || content?.statusJidList);
2501
2674
  return socket.sendMessage(STATUS_JID, {
@@ -2792,11 +2965,7 @@ export const makeMessagesSocket = (config) => {
2792
2965
  messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id),
2793
2966
  ...options
2794
2967
  });
2795
- await relayMessage(jid, fullMsg.message, {
2796
- messageId: fullMsg.key.id,
2797
- useCachedGroupMetadata: options.useCachedGroupMetadata,
2798
- statusJidList: options.statusJidList
2799
- });
2968
+ await relayRichMessage(jid, fullMsg, { ...options, forceRichEdit: true });
2800
2969
  return fullMsg;
2801
2970
  };
2802
2971
 
@@ -1592,7 +1592,16 @@ export const generateWAMessageContent = async (message, options) => {
1592
1592
  m = { lottieStickerMessage: { message: m } };
1593
1593
  }
1594
1594
  else if (hasOptionalProperty(message, 'viewOnce') && !!message.viewOnce) {
1595
- m = { viewOnceMessage: { message: m } };
1595
+ // Text view-once clients expect the flag on the inner
1596
+ // ExtendedTextMessage. Media continues to use the standard outer
1597
+ // viewOnceMessage envelope.
1598
+ if (m.extendedTextMessage) {
1599
+ m.extendedTextMessage.viewOnce = true;
1600
+ }
1601
+ else {
1602
+ m = { viewOnceMessage: { message: m } };
1603
+ }
1604
+ delete message.viewOnce;
1596
1605
  }
1597
1606
  // Lia@Changes 03-02-26 --- Add "viewOnceV2" boolean to wrap message into viewOnceMessageV2 like "viewOnce"
1598
1607
  else if (hasOptionalProperty(message, 'viewOnceV2') && !!message.viewOnceV2) {
@@ -5,6 +5,7 @@
5
5
  * If you use or copy this code, please credit my name or project. AND DO NOT CHANGE THIS NOTE
6
6
  */
7
7
  import { getRandomValues, randomUUID, randomBytes } from 'crypto';
8
+ import { Boom } from '@hapi/boom';
8
9
  import { BOT_RENDERING_CONFIG_METADATA, DONATE_URL, LEXER_REGEX } from '../Defaults/index.js';
9
10
  import { LANGUAGE_KEYWORDS } from '../WABinary/constants.js';
10
11
  import { CodeHighlightType, RichSubMessageType } from '../Types/RichType.js';
@@ -44,6 +45,38 @@ export const tokenizeCode = (code, language = 'javascript') => {
44
45
  }
45
46
  return blocks;
46
47
  };
48
+ const firstImageValue = (...values) => values.find(value => value !== undefined && value !== null && String(value).length > 0);
49
+
50
+ /** Normalize album image variants without downgrading full-size URLs to previews. */
51
+ const buildRichImagePrimitive = (image = {}, expiration = String(Date.now() + 30 * 24 * 60 * 60 * 1000)) => {
52
+ const previewUrl = firstImageValue(image.imagePreviewUrl, image.previewUrl, image.preview_url, image.sourceUrl, image.source_url, image.url) || '';
53
+ const fullUrl = firstImageValue(image.imageHighResUrl, image.highResUrl, image.high_res_url, image.fullUrl, image.full_url, image.sourceUrl, image.source_url, image.url, previewUrl);
54
+ const darkPreviewUrl = firstImageValue(image.darkModePreviewUrl, image.dark_mode_preview_url, previewUrl);
55
+ const darkFullUrl = firstImageValue(image.darkModeHighResUrl, image.darkModeHighResUrl, image.dark_mode_high_res_url, image.darkModePreviewUrl, image.dark_mode_preview_url, fullUrl);
56
+ const width = Number(image.previewWidth || image.preview_width || image.width || 600);
57
+ const height = Number(image.previewHeight || image.preview_height || image.height || 400);
58
+ const fullWidth = Number(image.fullWidth || image.full_width || image.highResWidth || image.high_res_width || image.width || width);
59
+ const fullHeight = Number(image.fullHeight || image.full_height || image.highResHeight || image.high_res_height || image.height || height);
60
+ const darkWidth = Number(image.darkWidth || image.dark_width || fullWidth);
61
+ const darkHeight = Number(image.darkHeight || image.dark_height || fullHeight);
62
+ const mimeType = image.mimeType || image.mime_type || 'image/png';
63
+ const media = (url, mediaWidth, mediaHeight) => ({
64
+ url,
65
+ url_fallback: url,
66
+ mime_type: mimeType,
67
+ expiration_timestamp_ms: expiration,
68
+ width: mediaWidth,
69
+ height: mediaHeight
70
+ });
71
+ return {
72
+ preview_image: media(previewUrl, width, height),
73
+ full_image: media(fullUrl, fullWidth, fullHeight),
74
+ dark_mode_preview_image: media(darkPreviewUrl, darkWidth, darkHeight),
75
+ dark_mode_full_image: media(darkFullUrl, darkWidth, darkHeight),
76
+ asset_query_status: 'FETCHED',
77
+ __typename: 'GenAIImagePrimitive'
78
+ };
79
+ };
47
80
  // Lia@Changes 09-04-26 --- Inject buffer into unifiedResponse.data to support proper rendering of rich messages (ex: tables and code blocks)
48
81
  export const toUnified = (submessages, uuid) => ({
49
82
  response_id: uuid || randomUUID(),
@@ -66,24 +99,7 @@ export const toUnified = (submessages, uuid) => ({
66
99
  const imageUrls = submessage.gridImageMetadata?.imageUrls || [];
67
100
  if (!imageUrls.length) return null;
68
101
  const expiration = String(Date.now() + 30 * 24 * 60 * 60 * 1000);
69
- const primitives = imageUrls.map(img => {
70
- const previewUrl = img.imagePreviewUrl || img.sourceUrl || '';
71
- const fullUrl = img.imageHighResUrl || img.imagePreviewUrl || img.sourceUrl || '';
72
- const width = img.width || 600;
73
- const height = img.height || 400;
74
- const darkPreviewUrl = img.darkModePreviewUrl || previewUrl;
75
- const darkFullUrl = img.darkModeHighResUrl || img.darkModePreviewUrl || fullUrl;
76
- const darkWidth = img.darkWidth || width;
77
- const darkHeight = img.darkHeight || height;
78
- return {
79
- preview_image: { url: previewUrl, mime_type: 'image/png', expiration_timestamp_ms: expiration, width, height },
80
- full_image: { url: fullUrl, mime_type: 'image/png', expiration_timestamp_ms: expiration, width, height },
81
- dark_mode_preview_image: { url: darkPreviewUrl, mime_type: 'image/png', expiration_timestamp_ms: expiration, width: darkWidth, height: darkHeight },
82
- dark_mode_full_image: { url: darkFullUrl, mime_type: 'image/png', expiration_timestamp_ms: expiration, width: darkWidth, height: darkHeight },
83
- asset_query_status: 'FETCHED',
84
- __typename: 'GenAIImagePrimitive'
85
- };
86
- });
102
+ const primitives = imageUrls.map(img => buildRichImagePrimitive(img, expiration));
87
103
  return { view_model: { primitives, __typename: 'GenAIGridLayoutViewModel' } };
88
104
  }
89
105
  case RichSubMessageType.CONTENT_ITEMS: {
@@ -376,6 +392,111 @@ const richMenuButton = (button, toast = '') => {
376
392
  * This is an experimental WhatsApp rich-response format, not a standard
377
393
  * buttonsMessage/nativeFlowMessage payload.
378
394
  */
395
+ /**
396
+ * Build a universal AIRich social/entity result card.
397
+ * The wire format is provider-neutral; Instagram is exposed as a dedicated
398
+ * convenience wrapper below because it is the primary supported profile view.
399
+ */
400
+ export const prepareSocialEntityMessage = ({
401
+ username = '',
402
+ platform = 'GENERIC',
403
+ title = 'CODY AI',
404
+ subtitle = '⚉',
405
+ imageUrl = '',
406
+ entityId,
407
+ entityUrl = '',
408
+ entityType,
409
+ fullName = 'NOT found',
410
+ isVerified = false,
411
+ resultText = 'See results',
412
+ botJid = '867051314767696@bot',
413
+ responseId
414
+ } = {}) => {
415
+ const normalizedPlatform = String(platform || 'GENERIC').toUpperCase();
416
+ const profileUrl = entityUrl || (username ? `https://www.${normalizedPlatform.toLowerCase()}.com/${encodeURIComponent(username)}` : '');
417
+ const resolvedType = entityType || (normalizedPlatform === 'INSTAGRAM' ? 'IG_PROFILE' : 'SOCIAL_PROFILE');
418
+ const displayName = username || fullName || 'Profile';
419
+ const sections = [
420
+ {
421
+ view_model: {
422
+ primitives: [{
423
+ title,
424
+ subtitle,
425
+ ...(imageUrl ? { image: { url: imageUrl, mime_type: 'image/png' } } : {}),
426
+ entity_id: String(entityId ?? username ?? ''),
427
+ entity_url: profileUrl,
428
+ entity_type: 'WEBSITE',
429
+ action_type: 'OPEN_URL',
430
+ is_verified: !!isVerified,
431
+ __typename: 'GenAICompactEntityPrimitive'
432
+ }],
433
+ __typename: 'GenAIActionRowLayoutViewModel'
434
+ }
435
+ },
436
+ {
437
+ view_model: {
438
+ primitives: [{ type: 'HORIZONTAL_LINE', __typename: 'GenAIDividerPrimitive' }],
439
+ __typename: 'GenAIVStackLayoutViewModel'
440
+ }
441
+ },
442
+ {
443
+ view_model: {
444
+ primitives: [
445
+ { __typename: 'GenAISpacerPrimitive' },
446
+ {
447
+ text: `# {{social_entity_1}}${resultText}\0{{/social_entity_1}} `,
448
+ inline_entities: [{
449
+ key: 'social_entity_1',
450
+ metadata: {
451
+ __typename: 'GenAISocialEntityItem',
452
+ entity_id: String(entityId ?? username ?? ''),
453
+ entity_name: displayName,
454
+ entity_full_name: fullName,
455
+ platform: String(platform || 'GENERIC').toUpperCase(),
456
+ entity_picture_url: imageUrl,
457
+ entity_url: profileUrl,
458
+ entity_type: resolvedType,
459
+ platform: normalizedPlatform,
460
+ is_verified: !!isVerified
461
+ }
462
+ }],
463
+ __typename: 'GenAIMarkdownTextUXPrimitive'
464
+ },
465
+ { __typename: 'GenAISpacerPrimitive' }
466
+ ],
467
+ __typename: 'GenAIActionRowLayoutViewModel'
468
+ }
469
+ }
470
+ ];
471
+ return {
472
+ botForwardedMessage: {
473
+ message: {
474
+ richResponseMessage: {
475
+ messageType: 1,
476
+ submessages: [],
477
+ unifiedResponse: {
478
+ response_id: responseId || randomUUID(),
479
+ data: Buffer.from(JSON.stringify({ sections })).toString('base64')
480
+ },
481
+ contextInfo: {
482
+ isForwarded: true,
483
+ forwardOrigin: 4,
484
+ forwardedAiBotMessageInfo: { botJid }
485
+ }
486
+ }
487
+ }
488
+ }
489
+ };
490
+ };
491
+
492
+ /** Dedicated Instagram profile card using the universal social-entity wire format. */
493
+ export const prepareInstagramProfileMessage = (content = {}) => prepareSocialProfileMessage({
494
+ ...content,
495
+ platform: 'INSTAGRAM',
496
+ entityType: 'IG_PROFILE',
497
+ entityUrl: content.entityUrl || (content.username ? `https://www.instagram.com/${encodeURIComponent(content.username)}` : undefined)
498
+ });
499
+
379
500
  export const prepareRichMenuMessage = (content = {}) => {
380
501
  const { header = {}, body = {}, footer = {}, verificationMetadata } = content || {};
381
502
  const sections = [];
@@ -500,6 +621,29 @@ export const prepareAIRichCommandMenu = ({ image, title = 'CODY AI', subtitle =
500
621
  return wrapToBotForwardedMessage(richResponseMessage);
501
622
  };
502
623
 
624
+ /**
625
+ * Build an arbitrary A2UI interactive message. The command-menu helper below
626
+ * is only a convenience preset; callers may provide any supported component,
627
+ * widget, or native-flow payload through this universal builder.
628
+ */
629
+ export const prepareA2UIMessage = ({ imageMessage, bloksWidget = {}, nativeFlowMessage, fallback = '', ...interactiveFields } = {}) => ({
630
+ interactiveMessage: {
631
+ ...interactiveFields,
632
+ ...(imageMessage ? { header: { imageMessage, hasMediaAttachment: true } } : {}),
633
+ bloksWidget: {
634
+ type: bloksWidget.type || 'im_a2ui',
635
+ data: typeof bloksWidget.data === 'string' ? bloksWidget.data : JSON.stringify(bloksWidget.data || {}),
636
+ ...(bloksWidget.fallback || fallback ? { fallback: String(bloksWidget.fallback || fallback) } : {})
637
+ },
638
+ ...(nativeFlowMessage ? {
639
+ nativeFlowMessage: {
640
+ ...nativeFlowMessage,
641
+ ...(nativeFlowMessage.buttons ? { buttons: nativeFlowMessage.buttons } : {})
642
+ }
643
+ } : {})
644
+ }
645
+ });
646
+
503
647
  /** Build the A2UI interactive command menu used by the full-size card layout. */
504
648
  export const prepareA2UICommandMenu = ({ imageMessage, title = 'CODY AI', rows = [], buttons = [], fallback = '' } = {}) => {
505
649
  if (!Array.isArray(rows)) throw new Error('A2UI command-menu rows must be an array');
@@ -790,26 +934,18 @@ export const prepareVerticalAlbumMessage = ({ items = [], verificationMetadata }
790
934
  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
935
  }
792
936
  const image = item?.image || item?.data || item;
793
- const sourceUrl = typeof image === 'string' ? image : (image?.sourceUrl || image?.url || '');
937
+ const sourceUrl = typeof image === 'string' ? image : firstImageValue(
938
+ image?.imageHighResUrl, image?.highResUrl, image?.fullUrl,
939
+ image?.imagePreviewUrl, image?.previewUrl, image?.sourceUrl, image?.url
940
+ ) || '';
794
941
  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
- };
942
+ const primitive = buildRichImagePrimitive({ ...image, width: image?.width || item?.width, height: image?.height || item?.height }, expiration);
943
+ return { ...primitive, display_mode: 'FULL_WIDTH', full_width: true };
808
944
  });
809
945
  const richResponseMessage = proto.AIRichResponseMessage.create({
810
946
  submessages: [],
811
947
  messageType: proto.AIRichResponseMessageType.AI_RICH_RESPONSE_TYPE_STANDARD,
812
- unifiedResponse: { data: Buffer.from(JSON.stringify({ response_id: randomUUID(), sections: [{ view_model: { primitives, __typename: 'GenAIVStackLayoutViewModel' } }] })) },
948
+ unifiedResponse: { data: Buffer.from(JSON.stringify({ response_id: randomUUID(), sections: [{ view_model: { primitives, display_mode: 'FULL_WIDTH', full_width: true, __typename: 'GenAIVStackLayoutViewModel' } }] })) },
813
949
  contextInfo: { isForwarded: true, forwardingScore: 1, forwardedAiBotMessageInfo: { botJid: '867051314767696@bot' }, forwardOrigin: 4 }
814
950
  });
815
951
  return wrapToBotForwardedMessage(richResponseMessage, verificationMetadata);
@@ -873,6 +1009,107 @@ export const prepareVideoGridMessage = ({ videos = [], verificationMetadata } =
873
1009
  return wrapToBotForwardedMessage(richResponseMessage, verificationMetadata);
874
1010
  };
875
1011
 
1012
+ /**
1013
+ * Builds a compact social-profile card used by WhatsApp's unified response
1014
+ * renderer. The summary is an inline social entity, not ordinary plain text.
1015
+ */
1016
+ export const prepareSocialProfileMessage = (options = {}) => {
1017
+ const {
1018
+ username,
1019
+ platform = 'instagram',
1020
+ title = 'CODY AI',
1021
+ subtitle = '⚉',
1022
+ imageUrl = '',
1023
+ entityId = username,
1024
+ entityUrl = `https://www.${platform}.com/${username || ''}`,
1025
+ entityType = platform === 'instagram' ? 'IG_PROFILE' : `${String(platform).toUpperCase()}_PROFILE`,
1026
+ fullName = 'NOT found',
1027
+ isVerified = false,
1028
+ resultText = 'See results'
1029
+ } = options;
1030
+ if (!username || typeof username !== 'string') {
1031
+ throw new Boom('sendSocialProfile requires a username', { statusCode: 400 });
1032
+ }
1033
+ const image = imageUrl ? { url: imageUrl, mime_type: 'image/png' } : undefined;
1034
+ const sections = [
1035
+ {
1036
+ view_model: {
1037
+ primitives: [{
1038
+ title,
1039
+ subtitle,
1040
+ ...(image ? { image } : {}),
1041
+ entity_id: String(entityId || username),
1042
+ entity_url: entityUrl,
1043
+ entity_type: 'WEBSITE',
1044
+ action_type: 'OPEN_URL',
1045
+ is_verified: Boolean(isVerified),
1046
+ __typename: 'GenAICompactEntityPrimitive'
1047
+ }],
1048
+ __typename: 'GenAIActionRowLayoutViewModel'
1049
+ }
1050
+ },
1051
+ {
1052
+ view_model: {
1053
+ primitives: [{ type: 'HORIZONTAL_LINE', __typename: 'GenAIDividerPrimitive' }],
1054
+ __typename: 'GenAIVStackLayoutViewModel'
1055
+ }
1056
+ },
1057
+ {
1058
+ view_model: {
1059
+ primitives: [
1060
+ { __typename: 'GenAISpacerPrimitive' },
1061
+ {
1062
+ text: `# {{social_entity_1}}${resultText}\0{{/social_entity_1}} `,
1063
+ inline_entities: [{
1064
+ key: 'social_entity_1',
1065
+ metadata: {
1066
+ __typename: 'GenAISocialEntityItem',
1067
+ entity_id: username,
1068
+ entity_name: username,
1069
+ entity_full_name: fullName,
1070
+ platform: String(platform || 'GENERIC').toUpperCase(),
1071
+ entity_picture_url: imageUrl,
1072
+ entity_url: entityUrl,
1073
+ entity_type: entityType,
1074
+ is_verified: Boolean(isVerified)
1075
+ }
1076
+ }],
1077
+ __typename: 'GenAIMarkdownTextUXPrimitive'
1078
+ },
1079
+ { __typename: 'GenAISpacerPrimitive' }
1080
+ ],
1081
+ __typename: 'GenAIActionRowLayoutViewModel'
1082
+ }
1083
+ }
1084
+ ];
1085
+ const richResponseMessage = proto.AIRichResponseMessage.create({
1086
+ submessages: [],
1087
+ messageType: proto.AIRichResponseMessageType.AI_RICH_RESPONSE_TYPE_STANDARD,
1088
+ unifiedResponse: { data: Buffer.from(JSON.stringify({ sections })) },
1089
+ contextInfo: { isForwarded: true, forwardOrigin: 4 }
1090
+ });
1091
+ return wrapToBotForwardedMessage(richResponseMessage);
1092
+ };
1093
+
1094
+
1095
+ /**
1096
+ * Builds a generic unified-response envelope without reducing its sections to
1097
+ * plain text. This is the extension point for newer WhatsApp primitives that
1098
+ * are present in WAProto but do not yet have a dedicated convenience helper.
1099
+ */
1100
+ export const prepareUnifiedResponseMessage = ({ sections = [], submessages = [], responseId = randomUUID(), contextInfo } = {}) => {
1101
+ if (!Array.isArray(sections) || sections.length === 0) {
1102
+ throw new Boom('sendUnifiedResponse requires at least one section', { statusCode: 400 });
1103
+ }
1104
+ const richResponseMessage = proto.AIRichResponseMessage.create({
1105
+ submessages,
1106
+ messageType: proto.AIRichResponseMessageType.AI_RICH_RESPONSE_TYPE_STANDARD,
1107
+ unifiedResponse: { data: Buffer.from(JSON.stringify({ response_id: responseId, sections })) },
1108
+ contextInfo: contextInfo || { isForwarded: true, forwardingScore: 1, forwardOrigin: 4 }
1109
+ });
1110
+ return wrapToBotForwardedMessage(richResponseMessage);
1111
+ };
1112
+
876
1113
  export const prepareRichResponseMessage = (content) => {
877
1114
  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;
878
1115
  let submessages = [];