plogme 2.0.1 → 2.0.3

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,29 +291,36 @@ await sock.sendMessage(jid, {
291
291
 
292
292
  ### Albums
293
293
 
294
- Use at least two image or video items. Albums are sent as a parent message followed by separately encrypted media children.
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.
295
295
 
296
296
  ```js
297
297
  await sock.sendAlbumMessage(jid, [
298
- { type: 'image', data: { url: './one.jpg' } },
299
- { type: 'image', data: { url: './two.jpg' } }
300
- ], {
301
- caption: 'Trip photos'
302
- })
298
+ { type: 'image', data: { url: 'https://example.com/one.jpg', width: 1200, height: 800 } },
299
+ {
300
+ image: {
301
+ sourceUrl: 'https://example.com/two-preview.jpg',
302
+ imageHighResUrl: 'https://example.com/two.jpg',
303
+ darkModePreviewUrl: 'https://example.com/two-dark-preview.jpg',
304
+ darkModeHighResUrl: 'https://example.com/two-dark.jpg',
305
+ width: 1200,
306
+ height: 800
307
+ }
308
+ }
309
+ ])
303
310
  ```
304
311
 
305
- The general `sendMessage` album form is also supported:
312
+ The general `sendMessage` album form delegates to the same GRID_IMAGE implementation:
306
313
 
307
314
  ```js
308
315
  await sock.sendMessage(jid, {
309
316
  album: [
310
- { image: { url: './one.jpg' }, caption: 'First' },
311
- { video: { url: './clip.mp4' }, caption: 'Second' }
317
+ { image: { url: 'https://example.com/one.jpg' } },
318
+ { image: { url: 'https://example.com/two.jpg' } }
312
319
  ]
313
320
  })
314
321
  ```
315
322
 
316
- Rich-response and bot-forwarded messages cannot be nested as album children. Send them as separate messages.
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.
317
324
 
318
325
  ## Interactive messages
319
326
 
@@ -475,6 +482,79 @@ await sock.sendRichAIResponse(jid, {
475
482
 
476
483
  `sendRichAIResponse` also accepts `list`, `codeBlock`, and `latex` content objects. For a pre-existing Meta AI response, `captureAndResendUnifiedResponse(jid, message, quoted)` captures and resends its unified response.
477
484
 
485
+ ### One unified Plogme response
486
+
487
+ Use `sendPlogmeMessage` when a response may contain several optional sections. It combines every section that is present into one AIRich `unifiedResponse`; omitted sections are ignored, so callers do not need to construct a different payload for every result shape.
488
+
489
+ ```js
490
+ const result = await sock.sendPlogmeMessage(jid, {
491
+ headerText: 'CODY AI result',
492
+ contentText: 'The deployment completed successfully.',
493
+
494
+ // Optional highlighted code section.
495
+ code: 'const status = "online"',
496
+ language: 'javascript',
497
+
498
+ // Optional table section. The first row is treated as the heading by default.
499
+ title: 'Service status',
500
+ table: [
501
+ ['Service', 'State'],
502
+ ['API', 'Online'],
503
+ ['Worker', 'Online']
504
+ ],
505
+ noHeading: false,
506
+
507
+ // Optional carousel/content items or reels.
508
+ items: [
509
+ { title: 'API documentation', url: 'https://example.com/docs' }
510
+ ],
511
+ reels: [
512
+ {
513
+ title: 'Release notes',
514
+ url: 'https://example.com/release.mp4',
515
+ thumbnail_url: 'https://example.com/release.jpg'
516
+ }
517
+ ],
518
+
519
+ // Optional inline media and LaTeX sections.
520
+ inlineImage: 'https://example.com/status.jpg',
521
+ imageText: 'Deployment status',
522
+ alignment: 2,
523
+ latex: [
524
+ { latexExpression: 'E = mc^2', url: 'https://example.com/equation.png' }
525
+ ],
526
+
527
+ // Optional sources/links, posts, products, or suggested prompts.
528
+ links: [
529
+ {
530
+ text: 'Open the deployment logs',
531
+ url: 'https://example.com/logs',
532
+ title: 'Deployment logs',
533
+ displayName: 'Logs'
534
+ }
535
+ ],
536
+ suggested: true,
537
+ footerText: 'Generated by CODY AI'
538
+ }, undefined, {
539
+ // The compatibility relay is enabled by default. It sends the rich
540
+ // envelope normally, then uses WhatsApp's type-14 edit protocol so
541
+ // compatible clients render it immediately instead of showing Download.
542
+ renderRichResponse: true
543
+ })
544
+
545
+ console.log('Sent message:', result.key.id)
546
+ ```
547
+
548
+ Every field in the example is optional. A minimal response is valid as well:
549
+
550
+ ```js
551
+ await sock.sendPlogmeMessage(jid, {
552
+ contentText: 'Only the text section is available right now.'
553
+ })
554
+ ```
555
+
556
+ The compatibility edit can be disabled for a specific send with `{ renderRichResponse: false }`. This sends the generated rich envelope once without the follow-up edit.
557
+
478
558
  ## Rich menus, reels, and grids
479
559
 
480
560
  ### Original Gen4 rich menu
@@ -3,9 +3,9 @@ import { Boom } from '@hapi/boom';
3
3
  import { randomBytes } from 'crypto';
4
4
  import { proto } from '../../WAProto/index.js';
5
5
  import { BIZ_BOT_SUPPORT_PAYLOAD, DEFAULT_CACHE_TTLS, WA_DEFAULT_EPHEMERAL } from '../Defaults/index.js';
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, hasValidAlbumMedia, MessageRetryManager, normalizeMessageContent, parseAndInjectE2ESessions, prepareWAMessageMedia, shouldIncludeBizBinaryNode, unixTimestampSeconds } from '../Utils/index.js';
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, prepareHtmlMessage, prepareRichHtmlMessage } from '../Utils/rich-message-utils.js';
8
+ import { prepareRichMenuMessage, prepareRichResponseMessage, prepareRichGenerationMessage, prepareCopyToClipboardMessage, prepareRichTextMessage, prepareRichQuoteMessage, prepareRichImageMessage, prepareRichLinkMessage, prepareGridImageMessage, 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';
@@ -555,7 +555,18 @@ export const makeMessagesSocket = (config) => {
555
555
  };
556
556
  }
557
557
  const applyForwarded = m => {
558
+ // Auto-inject trusted verification proofs configured once on the socket
559
+ // (config.metaAiRendering.verificationMetadata). Per-message proofs win:
560
+ // an envelope that already carries verificationMetadata is left untouched.
561
+ // The second target covers edited rich envelopes (protocolMessage.editedMessage,
562
+ // used by updateRichGeneration) so an in-place rich update keeps its proofs too.
563
+ const edited = m?.botForwardedMessage?.message?.protocolMessage?.editedMessage;
558
564
  const rich = m?.botForwardedMessage?.message?.richResponseMessage || m?.richResponseMessage;
565
+ if (!rich && !edited) return m;
566
+ if (hasVerifiableProofs(config?.metaAiRendering?.verificationMetadata)) {
567
+ attachBotVerificationProofs(m, config.metaAiRendering.verificationMetadata);
568
+ attachBotVerificationProofs(edited, config.metaAiRendering.verificationMetadata);
569
+ }
559
570
  if (!rich) return m;
560
571
  rich.contextInfo = { ...rich.contextInfo, isForwarded: true, forwardOrigin: 4 };
561
572
  return m.botForwardedMessage ? m : { botForwardedMessage: { message: m } };
@@ -1022,6 +1033,53 @@ export const makeMessagesSocket = (config) => {
1022
1033
  return msgId;
1023
1034
  };
1024
1035
 
1036
+ /**
1037
+ * Relay a rich envelope and then replay it as a type-14 edit. Some
1038
+ * WhatsApp clients otherwise show a Download prompt before rendering the
1039
+ * AIRich unifiedResponse. The edit is only applied to bot-forwarded rich
1040
+ * envelopes, so ordinary messages are never duplicated accidentally.
1041
+ * Set renderRichResponse: false for callers that explicitly do not want
1042
+ * the compatibility replay.
1043
+ */
1044
+ const relayRichMessage = async (jid, fullMsg, options = {}) => {
1045
+ const relayOptions = {
1046
+ messageId: fullMsg.key.id,
1047
+ useCachedGroupMetadata: options.useCachedGroupMetadata,
1048
+ statusJidList: options.statusJidList,
1049
+ ...options.relayOptions,
1050
+ };
1051
+ await relayMessage(jid, fullMsg.message, relayOptions);
1052
+
1053
+ const richEnvelope = fullMsg.message?.botForwardedMessage?.message?.richResponseMessage;
1054
+ if (options.renderRichResponse !== false && richEnvelope) {
1055
+ const editContent = proto.Message.fromObject({
1056
+ botForwardedMessage: {
1057
+ message: {
1058
+ protocolMessage: {
1059
+ key: {
1060
+ remoteJid: jid,
1061
+ fromMe: true,
1062
+ id: fullMsg.key.id,
1063
+ },
1064
+ type: proto.Message.ProtocolMessage.Type.MESSAGE_EDIT,
1065
+ editedMessage: fullMsg.message,
1066
+ },
1067
+ },
1068
+ },
1069
+ });
1070
+ const editMessage = generateWAMessageFromContent(jid, editContent, {
1071
+ logger,
1072
+ userJid: authState.creds.me?.id,
1073
+ messageId: generateMessageIDV2(authState.creds.me?.id || jid),
1074
+ });
1075
+ await relayMessage(jid, editMessage.message, {
1076
+ ...relayOptions,
1077
+ messageId: editMessage.key.id,
1078
+ });
1079
+ }
1080
+ return fullMsg;
1081
+ };
1082
+
1025
1083
  const getMessageType = (message) => {
1026
1084
  if (!message)
1027
1085
  return 'text';
@@ -1380,11 +1438,7 @@ export const makeMessagesSocket = (config) => {
1380
1438
  quoted,
1381
1439
  ...options
1382
1440
  });
1383
- await relayMessage(jid, generated.message, {
1384
- messageId: generated.key.id,
1385
- useCachedGroupMetadata: options.useCachedGroupMetadata,
1386
- statusJidList: options.statusJidList
1387
- });
1441
+ await relayRichMessage(jid, generated, options);
1388
1442
  return { ...generated, messageId: generated.key.id };
1389
1443
  },
1390
1444
  /**
@@ -1397,9 +1451,15 @@ export const makeMessagesSocket = (config) => {
1397
1451
  responseId: options.responseId,
1398
1452
  forwarded: options.forwarded !== false
1399
1453
  });
1400
- const messageId = generateMessageIDV2(jid);
1401
- await relayMessage(jid, generated.message, { messageId, quoted });
1402
- return { ...generated, messageId };
1454
+ const fullMsg = generateWAMessageFromContent(jid, generated.message, {
1455
+ logger,
1456
+ userJid: authState.creds.me?.id,
1457
+ messageId: options.messageId || generated.messageId || generateMessageIDV2(jid),
1458
+ quoted,
1459
+ ...options,
1460
+ });
1461
+ await relayRichMessage(jid, fullMsg, options);
1462
+ return { ...generated, messageId: fullMsg.key.id };
1403
1463
  },
1404
1464
  /**
1405
1465
  * Replace the generation primitive in an existing rich response.
@@ -1465,14 +1525,47 @@ export const makeMessagesSocket = (config) => {
1465
1525
  quoted,
1466
1526
  ...options
1467
1527
  });
1468
- await relayMessage(jid, fullMsg.message, {
1469
- messageId: fullMsg.key.id,
1470
- useCachedGroupMetadata: options.useCachedGroupMetadata,
1471
- statusJidList: options.statusJidList
1528
+ await relayRichMessage(jid, fullMsg, options);
1529
+ return fullMsg;
1530
+ },
1531
+ /**
1532
+ * Send one tolerant AIRich/unified response containing every supported
1533
+ * section present in `content`. Missing sections are intentionally
1534
+ * ignored by prepareRichResponseMessage instead of causing a partial
1535
+ * response to fail. This is the stable entry point for Plogme/CODY
1536
+ * callers that may receive optional text, links, media, products,
1537
+ * reels, tables, code, latex, or suggestion data.
1538
+ */
1539
+ sendPlogmeMessage: async (jid, content = {}, quoted, options = {}) => {
1540
+ if (!jid) throw new Boom('sendPlogmeMessage requires a target JID', { statusCode: 400 });
1541
+ if (!content || typeof content !== 'object' || Array.isArray(content)) {
1542
+ throw new Boom('sendPlogmeMessage content must be an object', { statusCode: 400 });
1543
+ }
1544
+
1545
+ const wrapped = prepareRichResponseMessage({
1546
+ ...content,
1547
+ // Avoid silently appending the library donation link to a
1548
+ // unified response unless the caller explicitly opts in.
1549
+ noDonation: options.noDonation ?? true,
1472
1550
  });
1551
+ const fullMsg = generateWAMessageFromContent(jid, wrapped, {
1552
+ logger,
1553
+ userJid: authState.creds.me?.id,
1554
+ messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id || jid),
1555
+ quoted,
1556
+ ...options,
1557
+ });
1558
+
1559
+ await relayRichMessage(jid, fullMsg, options);
1560
+
1561
+ if (config.emitOwnEvents) {
1562
+ process.nextTick(async () => {
1563
+ await messageMutex.mutex(() => upsertMessage(fullMsg, 'append'));
1564
+ });
1565
+ }
1473
1566
  return fullMsg;
1474
1567
  },
1475
- captureAndResendUnifiedResponse: async (jid, metaAiMsg, quoted) => {
1568
+ captureAndResendUnifiedResponse: async (jid, metaAiMsg, quoted, options = {}) => {
1476
1569
  const captured = captureUnifiedResponse(metaAiMsg);
1477
1570
  if (!captured) {
1478
1571
  throw new Boom('captureAndResendUnifiedResponse: message contains no unifiedResponse data');
@@ -1484,11 +1577,7 @@ export const makeMessagesSocket = (config) => {
1484
1577
  messageId: generated.messageId || generateMessageIDV2(jid),
1485
1578
  quoted
1486
1579
  });
1487
- await relayMessage(jid, fullMsg.message, {
1488
- messageId: fullMsg.key.id,
1489
- useCachedGroupMetadata: options?.useCachedGroupMetadata,
1490
- statusJidList: options?.statusJidList
1491
- });
1580
+ await relayRichMessage(jid, fullMsg, options);
1492
1581
  return fullMsg;
1493
1582
  },
1494
1583
  updateMediaMessage: async (message) => {
@@ -2028,6 +2117,9 @@ export const makeMessagesSocket = (config) => {
2028
2117
  }
2029
2118
  // ── NORMAL MESSAGE HANDLING ──
2030
2119
  else {
2120
+ if ('album' in content) {
2121
+ return socket.sendAlbumMessage(jid, content.album, options);
2122
+ }
2031
2123
  const fullMsg = await generateWAMessage(jid, content, {
2032
2124
  logger,
2033
2125
  userJid,
@@ -2116,13 +2208,6 @@ export const makeMessagesSocket = (config) => {
2116
2208
  delete content.ai;
2117
2209
  }
2118
2210
 
2119
- if ('album' in content) {
2120
- if (!Array.isArray(content.album) || content.album.length < 2) {
2121
- throw new Boom('Album requires at least two image or video items', { statusCode: 400 });
2122
- }
2123
- fullMsg.message.messageContextInfo ||= {};
2124
- fullMsg.message.messageContextInfo.messageSecret ||= randomBytes(32);
2125
- }
2126
2211
  await relayMessage(jid, fullMsg.message, {
2127
2212
  messageId: fullMsg.key.id,
2128
2213
  useCachedGroupMetadata: options.useCachedGroupMetadata,
@@ -2139,55 +2224,6 @@ export const makeMessagesSocket = (config) => {
2139
2224
  });
2140
2225
  }
2141
2226
 
2142
- if ('album' in content) {
2143
- const { delayMs = 1500 } = options;
2144
- for (const [messageIndex, albumMedia] of content.album.entries()) {
2145
- // Give the album collection stanza time to register before
2146
- // its associated media children are relayed. This mirrors
2147
- // the ordering used by WhatsApp clients and avoids the
2148
- // children being treated as unrelated media messages.
2149
- await delay(delayMs);
2150
- const albumMsg = await generateWAMessage(jid, albumMedia, {
2151
- logger,
2152
- userJid,
2153
- upload: waUploadToServer,
2154
- mediaCache: config.mediaCache,
2155
- options: config.options,
2156
- ...options,
2157
- messageId: generateMessageIDV2(userJid)
2158
- });
2159
- if (!hasValidAlbumMedia(normalizeMessageContent(albumMsg.message))) {
2160
- throw new Boom('Invalid message type for album', { statusCode: 400 });
2161
- }
2162
- albumMsg.message.messageContextInfo ||= {};
2163
- // WhatsApp expects an encrypted context secret on each album
2164
- // child. Without it, clients may show the album as an
2165
- // unsupported message or fail to group the media together.
2166
- albumMsg.message.messageContextInfo.messageSecret ||= randomBytes(32);
2167
- albumMsg.message.messageContextInfo.messageAssociation = {
2168
- parentMessageKey: fullMsg.key,
2169
- // WhatsApp album child messages use association type 1.
2170
- // The generated MEDIA_ALBUM enum value (63) is a message
2171
- // category, not the child-association wire value.
2172
- associationType: 1,
2173
- messageIndex
2174
- };
2175
- await relayMessage(jid, albumMsg.message, {
2176
- messageId: albumMsg.key.id,
2177
- useCachedGroupMetadata: options.useCachedGroupMetadata,
2178
- addBizAttributes: isNeedBizAttrs,
2179
- statusJidList: options.statusJidList,
2180
- additionalAttributes,
2181
- additionalNodes
2182
- });
2183
- if (config.emitOwnEvents) {
2184
- process.nextTick(async () => {
2185
- await messageMutex.mutex(() => upsertMessage(albumMsg, 'append'));
2186
- });
2187
- }
2188
- }
2189
- }
2190
-
2191
2227
  return fullMsg;
2192
2228
  }
2193
2229
  }
@@ -2205,11 +2241,7 @@ export const makeMessagesSocket = (config) => {
2205
2241
  messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id),
2206
2242
  ...options,
2207
2243
  });
2208
- await relayMessage(jid, fullMsg.message, {
2209
- messageId: fullMsg.key.id,
2210
- useCachedGroupMetadata: options.useCachedGroupMetadata,
2211
- statusJidList: options.statusJidList,
2212
- });
2244
+ await relayRichMessage(jid, fullMsg, options);
2213
2245
  if (config.emitOwnEvents) {
2214
2246
  process.nextTick(async () => {
2215
2247
  await messageMutex.mutex(() => upsertMessage(fullMsg, 'append'));
@@ -2230,11 +2262,7 @@ export const makeMessagesSocket = (config) => {
2230
2262
  messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id),
2231
2263
  ...options,
2232
2264
  });
2233
- await relayMessage(jid, fullMsg.message, {
2234
- messageId: fullMsg.key.id,
2235
- useCachedGroupMetadata: options.useCachedGroupMetadata,
2236
- statusJidList: options.statusJidList,
2237
- });
2265
+ await relayRichMessage(jid, fullMsg, options);
2238
2266
  if (config.emitOwnEvents) {
2239
2267
  process.nextTick(async () => {
2240
2268
  await messageMutex.mutex(() => upsertMessage(fullMsg, 'append'));
@@ -2248,7 +2276,7 @@ export const makeMessagesSocket = (config) => {
2248
2276
  if (!content.copyText) throw new Boom('sendRichTextCopy requires copyText content', { statusCode: 400 });
2249
2277
  const wrapped = prepareRichQuoteMessage(content);
2250
2278
  const fullMsg = await generateWAMessageFromContent(jid, wrapped, { logger, userJid: authState.creds.me?.id, messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id), ...options });
2251
- await relayMessage(jid, fullMsg.message, { messageId: fullMsg.key.id, useCachedGroupMetadata: options.useCachedGroupMetadata, statusJidList: options.statusJidList });
2279
+ await relayRichMessage(jid, fullMsg, options);
2252
2280
  return fullMsg;
2253
2281
  };
2254
2282
  /** Sends Markdown text containing blockquotes in a forwarded GenAI rich response. */
@@ -2256,7 +2284,7 @@ export const makeMessagesSocket = (config) => {
2256
2284
  if (!jid) throw new Boom('sendMarkdownQuote requires a target JID', { statusCode: 400 });
2257
2285
  const wrapped = prepareRichQuoteMessage(content);
2258
2286
  const fullMsg = await generateWAMessageFromContent(jid, wrapped, { logger, userJid: authState.creds.me?.id, messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id), ...options });
2259
- await relayMessage(jid, fullMsg.message, { messageId: fullMsg.key.id, useCachedGroupMetadata: options.useCachedGroupMetadata, statusJidList: options.statusJidList });
2287
+ await relayRichMessage(jid, fullMsg, options);
2260
2288
  return fullMsg;
2261
2289
  };
2262
2290
  /** Sends Markdown blockquotes followed by a GenAI COPY_TO_CLIPBOARD action. */
@@ -2265,7 +2293,7 @@ export const makeMessagesSocket = (config) => {
2265
2293
  if (!content.copyText) throw new Boom('sendQuoteCopy requires copyText content', { statusCode: 400 });
2266
2294
  const wrapped = prepareRichQuoteMessage(content);
2267
2295
  const fullMsg = await generateWAMessageFromContent(jid, wrapped, { logger, userJid: authState.creds.me?.id, messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id), ...options });
2268
- await relayMessage(jid, fullMsg.message, { messageId: fullMsg.key.id, useCachedGroupMetadata: options.useCachedGroupMetadata, statusJidList: options.statusJidList });
2296
+ await relayRichMessage(jid, fullMsg, options);
2269
2297
  return fullMsg;
2270
2298
  };
2271
2299
  /**
@@ -2280,11 +2308,7 @@ export const makeMessagesSocket = (config) => {
2280
2308
  messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id),
2281
2309
  ...options,
2282
2310
  });
2283
- await relayMessage(jid, fullMsg.message, {
2284
- messageId: fullMsg.key.id,
2285
- useCachedGroupMetadata: options.useCachedGroupMetadata,
2286
- statusJidList: options.statusJidList,
2287
- });
2311
+ await relayRichMessage(jid, fullMsg, options);
2288
2312
  if (config.emitOwnEvents) {
2289
2313
  process.nextTick(async () => {
2290
2314
  await messageMutex.mutex(() => upsertMessage(fullMsg, 'append'));
@@ -2304,11 +2328,7 @@ export const makeMessagesSocket = (config) => {
2304
2328
  messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id),
2305
2329
  ...options,
2306
2330
  });
2307
- await relayMessage(jid, fullMsg.message, {
2308
- messageId: fullMsg.key.id,
2309
- useCachedGroupMetadata: options.useCachedGroupMetadata,
2310
- statusJidList: options.statusJidList,
2311
- });
2331
+ await relayRichMessage(jid, fullMsg, options);
2312
2332
  if (config.emitOwnEvents) {
2313
2333
  process.nextTick(async () => {
2314
2334
  await messageMutex.mutex(() => upsertMessage(fullMsg, 'append'));
@@ -2325,11 +2345,7 @@ export const makeMessagesSocket = (config) => {
2325
2345
  messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id),
2326
2346
  ...options,
2327
2347
  });
2328
- await relayMessage(jid, fullMsg.message, {
2329
- messageId: fullMsg.key.id,
2330
- useCachedGroupMetadata: options.useCachedGroupMetadata,
2331
- statusJidList: options.statusJidList,
2332
- });
2348
+ await relayRichMessage(jid, fullMsg, options);
2333
2349
  if (config.emitOwnEvents) {
2334
2350
  process.nextTick(async () => {
2335
2351
  await messageMutex.mutex(() => upsertMessage(fullMsg, 'append'));
@@ -2346,7 +2362,7 @@ export const makeMessagesSocket = (config) => {
2346
2362
  botForwardedMessage: { message: { richResponseMessage: { messageType: 1, submessages: [{ messageType: 2, messageText: text }], contextInfo: { isForwarded: true, forwardingScore: 1, forwardedAiBotMessageInfo: { botJid }, forwardOrigin: 4 } } } }
2347
2363
  };
2348
2364
  const fullMsg = await generateWAMessageFromContent(jid, wrapped, { logger, userJid: authState.creds.me?.id, messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id), ...options });
2349
- await relayMessage(jid, fullMsg.message, { messageId: fullMsg.key.id, useCachedGroupMetadata: options.useCachedGroupMetadata, statusJidList: options.statusJidList });
2365
+ await relayRichMessage(jid, fullMsg, options);
2350
2366
  return fullMsg;
2351
2367
  };
2352
2368
  /** Sends a rich response with experimental BotCommandMetadata. */
@@ -2355,7 +2371,7 @@ export const makeMessagesSocket = (config) => {
2355
2371
  const { text = 'Bot command test', commandName = '', commandDescription = '', commandPrompt = '', botJid = '867051314767696@bot' } = content;
2356
2372
  const wrapped = buildBotRichResponse({ text, botJid, botMetadata: { commandMetadata: { commandName, commandDescription, commandPrompt } } });
2357
2373
  const fullMsg = await generateWAMessageFromContent(jid, wrapped, { logger, userJid: authState.creds.me?.id, messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id), ...options });
2358
- await relayMessage(jid, fullMsg.message, { messageId: fullMsg.key.id, useCachedGroupMetadata: options.useCachedGroupMetadata, statusJidList: options.statusJidList });
2374
+ await relayRichMessage(jid, fullMsg, options);
2359
2375
  return fullMsg;
2360
2376
  };
2361
2377
  /** Sends a planning/execution-plan rich response with WAProto capabilities. */
@@ -2378,7 +2394,7 @@ export const makeMessagesSocket = (config) => {
2378
2394
  ]
2379
2395
  });
2380
2396
  const fullMsg = await generateWAMessageFromContent(jid, wrapped, { logger, userJid: authState.creds.me?.id, messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id), ...options });
2381
- await relayMessage(jid, fullMsg.message, { messageId: fullMsg.key.id, useCachedGroupMetadata: options.useCachedGroupMetadata, statusJidList: options.statusJidList });
2397
+ await relayRichMessage(jid, fullMsg, options);
2382
2398
  return fullMsg;
2383
2399
  };
2384
2400
  /** Sends a rich response with experimental BotResolvedToolCallMetadata. */
@@ -2397,7 +2413,7 @@ export const makeMessagesSocket = (config) => {
2397
2413
  }
2398
2414
  });
2399
2415
  const fullMsg = await generateWAMessageFromContent(jid, wrapped, { logger, userJid: authState.creds.me?.id, messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id), ...options });
2400
- await relayMessage(jid, fullMsg.message, { messageId: fullMsg.key.id, useCachedGroupMetadata: options.useCachedGroupMetadata, statusJidList: options.statusJidList });
2416
+ await relayRichMessage(jid, fullMsg, options);
2401
2417
  return fullMsg;
2402
2418
  };
2403
2419
  socket.sendStatus = async (content, options = {}) => {
@@ -2600,25 +2616,53 @@ export const makeMessagesSocket = (config) => {
2600
2616
 
2601
2617
 
2602
2618
  /**
2603
- * Sends an experimental WhatsApp album. The parent album header and each
2604
- * image/video child are relayed separately and associated by parent key.
2605
- * Accepts [{ image: ... }, { video: ... }] or [{ type: 'image', data: ... }].
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.
2606
2622
  */
2607
2623
  socket.sendAlbumMessage = async (jid, medias = [], options = {}) => {
2608
2624
  if (!jid) throw new Boom('sendAlbumMessage requires a target JID', { statusCode: 400 });
2609
2625
  if (!Array.isArray(medias) || medias.length < 2) {
2610
2626
  throw new Boom('sendAlbumMessage requires at least two media items', { statusCode: 400 });
2611
2627
  }
2612
- const album = medias.map((media, index) => {
2613
- const normalized = media?.type && media?.data
2614
- ? { [media.type]: media.data }
2615
- : { ...media };
2616
- if (index === 0 && options.caption && !normalized.caption) {
2617
- normalized.caption = options.caption;
2628
+ const imageUrls = medias.map((media) => {
2629
+ const raw = media?.type && media?.data ? media.data : (media?.image ?? media);
2630
+ if (media?.video || media?.type === 'video') {
2631
+ throw new Boom('GRID_IMAGE albums support images only', { statusCode: 400 });
2618
2632
  }
2619
- return normalized;
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 });
2636
+ }
2637
+ 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
2647
+ };
2648
+ });
2649
+ const wrapped = prepareGridImageMessage({
2650
+ imageUrls,
2651
+ verificationMetadata: options.verificationMetadata
2652
+ });
2653
+ const fullMsg = generateWAMessageFromContent(jid, wrapped, {
2654
+ logger,
2655
+ userJid: authState.creds.me?.id,
2656
+ messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id || jid),
2657
+ ...options
2620
2658
  });
2621
- return socket.sendMessage(jid, { album }, options);
2659
+ await relayRichMessage(jid, fullMsg, options);
2660
+ if (config.emitOwnEvents) {
2661
+ process.nextTick(async () => {
2662
+ await messageMutex.mutex(() => upsertMessage(fullMsg, 'append'));
2663
+ });
2664
+ }
2665
+ return fullMsg;
2622
2666
  };
2623
2667
 
2624
2668
 
@@ -2677,11 +2721,7 @@ export const makeMessagesSocket = (config) => {
2677
2721
  messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id),
2678
2722
  ...options
2679
2723
  });
2680
- await relayMessage(jid, fullMsg.message, {
2681
- messageId: fullMsg.key.id,
2682
- useCachedGroupMetadata: options.useCachedGroupMetadata,
2683
- statusJidList: options.statusJidList
2684
- });
2724
+ await relayRichMessage(jid, fullMsg, options);
2685
2725
  return fullMsg;
2686
2726
  };
2687
2727
 
@@ -24,7 +24,6 @@ import {
24
24
  buildPlainPlaceholder,
25
25
  PlanningStepStatus
26
26
  } from './meta-compositing.js';
27
- import { botMetadataSignature, botMetadataCertificate } from './rich-message-utils.js';
28
27
  import { BOT_RENDERING_CONFIG_METADATA } from '../Defaults/index.js';
29
28
  import { delay } from './generics.js';
30
29
 
@@ -1061,30 +1061,8 @@ export const generateWAMessageContent = async (message, options) => {
1061
1061
  };
1062
1062
  delete m.orderMessage.orderText;
1063
1063
  }
1064
- // Lia@Changes 31-01-26 --- Add support for album messages
1065
1064
  else if (hasNonNullishProperty(message, 'album')) {
1066
- if (!Array.isArray(message.album)) {
1067
- throw new Boom('Invalid album type. Expected an array.', { statusCode: 400 });
1068
- }
1069
- let videoCount = 0;
1070
- for (let i = 0; i < message.album.length; i++) {
1071
- if (message.album[i].video)
1072
- videoCount++;
1073
- }
1074
- ;
1075
- let imageCount = 0;
1076
- for (let i = 0; i < message.album.length; i++) {
1077
- if (message.album[i].image)
1078
- imageCount++;
1079
- }
1080
- ;
1081
- if ((videoCount + imageCount) < 2) {
1082
- throw new Boom('Minimum provide 2 media to upload album message', { statusCode: 400 });
1083
- }
1084
- m.albumMessage = {
1085
- expectedImageCount: imageCount,
1086
- expectedVideoCount: videoCount
1087
- };
1065
+ throw new Boom('Legacy albumMessage is removed; use socket.sendAlbumMessage() for an AIRich GRID_IMAGE album', { statusCode: 400 });
1088
1066
  }
1089
1067
  else if (hasNonNullishProperty(message, 'sharePhoneNumber')) {
1090
1068
  m.protocolMessage = {
@@ -5,7 +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 { DONATE_URL, LEXER_REGEX } from '../Defaults/index.js';
8
+ import { BOT_RENDERING_CONFIG_METADATA, DONATE_URL, LEXER_REGEX } from '../Defaults/index.js';
9
9
  import { LANGUAGE_KEYWORDS } from '../WABinary/constants.js';
10
10
  import { CodeHighlightType, RichSubMessageType } from '../Types/RichType.js';
11
11
  import { proto } from '../../WAProto/index.js';
@@ -62,6 +62,30 @@ export const toUnified = (submessages, uuid) => ({
62
62
  }
63
63
  };
64
64
  }
65
+ case RichSubMessageType.GRID_IMAGE: {
66
+ const imageUrls = submessage.gridImageMetadata?.imageUrls || [];
67
+ if (!imageUrls.length) return null;
68
+ 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
+ });
87
+ return { view_model: { primitives, __typename: 'GenAIGridLayoutViewModel' } };
88
+ }
65
89
  case RichSubMessageType.CONTENT_ITEMS: {
66
90
  const items = submessage.contentItemsMetadata?.itemsMetadata || [];
67
91
  const primitives = items.map((item) => {
@@ -196,7 +220,8 @@ export const prepareRichGenerationMessage = ({
196
220
  estimatedMs,
197
221
  itemId = `imagine_${randomUUID()}`,
198
222
  responseId = randomUUID(),
199
- forwarded = true
223
+ forwarded = true,
224
+ verificationMetadata
200
225
  } = {}) => {
201
226
  const sections = [];
202
227
  if (text) {
@@ -210,7 +235,7 @@ export const prepareRichGenerationMessage = ({
210
235
  unifiedResponse: { data: Buffer.from(JSON.stringify({ response_id: responseId, sections })) },
211
236
  contextInfo: forwarded ? { isForwarded: true, forwardingScore: 1, forwardedAiBotMessageInfo: { botJid: '867051314767696@bot' }, forwardOrigin: 4 } : {}
212
237
  });
213
- const message = wrapToBotForwardedMessage(richResponseMessage);
238
+ const message = wrapToBotForwardedMessage(richResponseMessage, verificationMetadata);
214
239
  message.messageContextInfo = message.messageContextInfo || {};
215
240
  message.messageContextInfo.botMetadata = {
216
241
  ...(message.messageContextInfo.botMetadata || {}),
@@ -220,56 +245,72 @@ export const prepareRichGenerationMessage = ({
220
245
  };
221
246
 
222
247
  // Lia@Note 17-04-26 --- WIP
248
+ /**
249
+ * A preview-media descriptor is only meaningful when the client can locate and
250
+ * decrypt the media. A descriptor without a key, hash and direct path describes
251
+ * media that cannot be fetched, so it is dropped rather than shipped
252
+ * present-but-empty (which is what previously produced `''` crypto fields and a
253
+ * hard-coded `image/jpeg` mimetype on every reel).
254
+ *
255
+ * Callers that upload real media supply it as `previewMedia` on the reel item or
256
+ * LaTeX expression: `{ fileSha256, mediaKey, fileEncSha256, directPath, mimetype?, mediaKeyTimestamp?, mediaId? }`.
257
+ */
258
+ const normalizePreviewMedia = (media) => {
259
+ if (!media || typeof media !== 'object') return null;
260
+ const { fileSha256, mediaKey, fileEncSha256, directPath, mimetype, mediaKeyTimestamp } = media;
261
+ if (!fileSha256 || !mediaKey || !fileEncSha256 || !directPath) return null;
262
+ return {
263
+ fileSha256,
264
+ mediaKey,
265
+ fileEncSha256,
266
+ directPath,
267
+ mediaKeyTimestamp: mediaKeyTimestamp ?? unixTimestampSeconds(),
268
+ mimetype: mimetype || 'image/jpeg'
269
+ };
270
+ };
271
+
223
272
  export const buildAdditionalBotMetadataContext = (submessages) => {
224
273
  const sources = [];
225
274
  const mediaDetailsMetadataList = [];
226
- for (let i = 0; i < submessages.length; i++) {
227
- const submessage = submessages[i];
275
+ for (const submessage of submessages) {
228
276
  switch (submessage.messageType) {
229
277
  case RichSubMessageType.CONTENT_ITEMS: {
230
278
  const itemsMetadata = submessage.contentItemsMetadata?.itemsMetadata || [];
231
- for (let n = 0; n < itemsMetadata.length; n++) {
232
- const reelItem = itemsMetadata[n]?.reelItem || itemsMetadata[n] || {};
279
+ for (const item of itemsMetadata) {
280
+ const reelItem = item?.reelItem || item || {};
233
281
  sources.push({
234
282
  provider: 0,
235
283
  thumbnailCdnUrl: reelItem.thumbnailUrl || reelItem.thumbnail_url || '',
236
284
  sourceProviderUrl: reelItem.videoUrl || reelItem.reels_url || reelItem.url || '',
237
285
  sourceQuery: '',
238
286
  faviconCdnUrl: reelItem.profileIconUrl || reelItem.avatar_url || '',
239
- citationNumber: i + 1,
287
+ // 1-based source ordinal, not the submessage index.
288
+ citationNumber: sources.length + 1,
240
289
  sourceTitle: reelItem.title || reelItem.reels_title || reelItem.creator || ''
241
290
  });
242
- mediaDetailsMetadataList.push({
243
- id: randomBytes(32).toString('hex'),
244
- previewMedia: {
245
- fileSha256: '',
246
- mediaKey: '',
247
- fileEncSha256: '',
248
- directPath: '',
249
- mediaKeyTimestamp: unixTimestampSeconds(),
250
- mimetype: 'image/jpeg'
251
- }
252
- });
291
+ const previewMedia = normalizePreviewMedia(reelItem.previewMedia);
292
+ if (previewMedia) {
293
+ mediaDetailsMetadataList.push({
294
+ id: reelItem.mediaId || reelItem.previewMedia?.mediaId || randomBytes(32).toString('hex'),
295
+ previewMedia
296
+ });
297
+ }
253
298
  }
254
299
  break;
255
300
  }
256
- case RichSubMessageType.LATEX:
257
- const expressions = submessage.latexMetadata.expressions;
258
- for (let n = 0; n < expressions.length; n++) {
259
- const expression = expressions[n];
260
- mediaDetailsMetadataList.push({
261
- id: randomBytes(32).toString('hex'),
262
- previewMedia: {
263
- fileSha256: '',
264
- mediaKey: '',
265
- fileEncSha256: '',
266
- directPath: '',
267
- mediaKeyTimestamp: unixTimestampSeconds(),
268
- mimetype: 'image/jpeg'
269
- }
270
- });
301
+ case RichSubMessageType.LATEX: {
302
+ const expressions = submessage.latexMetadata?.expressions || [];
303
+ for (const expression of expressions) {
304
+ const previewMedia = normalizePreviewMedia(expression?.previewMedia);
305
+ if (previewMedia) {
306
+ mediaDetailsMetadataList.push({
307
+ id: expression.mediaId || expression.previewMedia?.mediaId || randomBytes(32).toString('hex'),
308
+ previewMedia
309
+ });
310
+ }
271
311
  }
272
312
  break;
313
+ }
273
314
  }
274
315
  }
275
316
  return { sources, mediaDetailsMetadataList };
@@ -336,7 +377,7 @@ const richMenuButton = (button, toast = '') => {
336
377
  * buttonsMessage/nativeFlowMessage payload.
337
378
  */
338
379
  export const prepareRichMenuMessage = (content = {}) => {
339
- const { header = {}, body = {}, footer = {} } = content || {};
380
+ const { header = {}, body = {}, footer = {}, verificationMetadata } = content || {};
340
381
  const sections = [];
341
382
  const title = header?.title || '';
342
383
  if (title) {
@@ -418,7 +459,7 @@ export const prepareRichMenuMessage = (content = {}) => {
418
459
  unifiedResponse: { data: Buffer.from(JSON.stringify(unified)) },
419
460
  contextInfo: { isForwarded: true, forwardingScore: 1, forwardOrigin: 4 },
420
461
  });
421
- return wrapToBotForwardedMessage(richResponseMessage);
462
+ return wrapToBotForwardedMessage(richResponseMessage, verificationMetadata);
422
463
  };
423
464
 
424
465
  /**
@@ -432,7 +473,7 @@ export const prepareRichMenuMessage = (content = {}) => {
432
473
  * @returns {Object} Wrapped botForwardedMessage ready for relayMessage
433
474
  */
434
475
  export const prepareCopyToClipboardMessage = (options = {}) => {
435
- const { text = '', label, alignment = 'END' } = options;
476
+ const { text = '', label, alignment = 'END', verificationMetadata } = options;
436
477
  const displayText = label || text;
437
478
  const sections = [
438
479
  {
@@ -458,7 +499,7 @@ export const prepareCopyToClipboardMessage = (options = {}) => {
458
499
  unifiedResponse: { data: Buffer.from(JSON.stringify(unified)) },
459
500
  contextInfo: { isForwarded: true, forwardingScore: 1, forwardOrigin: 4 },
460
501
  });
461
- return wrapToBotForwardedMessage(richResponseMessage);
502
+ return wrapToBotForwardedMessage(richResponseMessage, verificationMetadata);
462
503
  };
463
504
 
464
505
  /**
@@ -470,7 +511,7 @@ export const prepareCopyToClipboardMessage = (options = {}) => {
470
511
  * @returns {Object} Wrapped botForwardedMessage ready for relayMessage
471
512
  */
472
513
  export const prepareRichTextMessage = (options = {}) => {
473
- const { text = '' } = options;
514
+ const { text = '', verificationMetadata } = options;
474
515
  const sections = [
475
516
  {
476
517
  __typename: 'GenAIUnifiedResponseSection',
@@ -491,12 +532,12 @@ export const prepareRichTextMessage = (options = {}) => {
491
532
  unifiedResponse: { data: Buffer.from(JSON.stringify(unified)) },
492
533
  contextInfo: { isForwarded: true, forwardingScore: 1, forwardOrigin: 4 },
493
534
  });
494
- return wrapToBotForwardedMessage(richResponseMessage);
535
+ return wrapToBotForwardedMessage(richResponseMessage, verificationMetadata);
495
536
  };
496
537
 
497
538
  /** Builds Markdown text with an optional GenAI COPY_TO_CLIPBOARD action section. */
498
539
  export const prepareRichQuoteMessage = (options = {}) => {
499
- const { text = '', copyText, copyLabel = copyText, alignment = 'END' } = options;
540
+ const { text = '', copyText, copyLabel = copyText, alignment = 'END', verificationMetadata } = options;
500
541
  const sections = [{
501
542
  __typename: 'GenAIUnifiedResponseSection',
502
543
  view_model: {
@@ -522,7 +563,7 @@ export const prepareRichQuoteMessage = (options = {}) => {
522
563
  unifiedResponse: { data: Buffer.from(JSON.stringify(unified)) },
523
564
  contextInfo: { isForwarded: true, forwardingScore: 1, forwardOrigin: 4 },
524
565
  });
525
- return wrapToBotForwardedMessage(richResponseMessage);
566
+ return wrapToBotForwardedMessage(richResponseMessage, verificationMetadata);
526
567
  };
527
568
 
528
569
  /**
@@ -535,7 +576,7 @@ export const prepareRichQuoteMessage = (options = {}) => {
535
576
  * @returns {Object} Wrapped botForwardedMessage ready for relayMessage
536
577
  */
537
578
  export const prepareRichImageMessage = (options = {}) => {
538
- const { url = '', mimeType = 'image/png' } = options;
579
+ const { url = '', mimeType = 'image/png', verificationMetadata } = options;
539
580
  const media = {
540
581
  __typename: 'GenAIMediaItem',
541
582
  mime_type: mimeType,
@@ -562,7 +603,7 @@ export const prepareRichImageMessage = (options = {}) => {
562
603
  unifiedResponse: { data: Buffer.from(JSON.stringify(unified)) },
563
604
  contextInfo: { isForwarded: true, forwardingScore: 1, forwardOrigin: 4 },
564
605
  });
565
- return wrapToBotForwardedMessage(richResponseMessage);
606
+ return wrapToBotForwardedMessage(richResponseMessage, verificationMetadata);
566
607
  };
567
608
 
568
609
 
@@ -575,7 +616,7 @@ export const prepareRichImageMessage = (options = {}) => {
575
616
  * @returns {Object} Wrapped botForwardedMessage ready for relayMessage
576
617
  */
577
618
  export const prepareRichLinkMessage = (options = {}) => {
578
- const { links = [], text: fallbackText } = options;
619
+ const { links = [], text: fallbackText, verificationMetadata } = options;
579
620
  const sections = [];
580
621
  const allInlineEntities = [];
581
622
  let combinedText = '';
@@ -629,11 +670,35 @@ export const prepareRichLinkMessage = (options = {}) => {
629
670
  unifiedResponse: { data: Buffer.from(JSON.stringify(unified)) },
630
671
  contextInfo: { isForwarded: true, forwardingScore: 1, forwardOrigin: 4 },
631
672
  });
632
- return wrapToBotForwardedMessage(richResponseMessage);
673
+ return wrapToBotForwardedMessage(richResponseMessage, verificationMetadata);
674
+ };
675
+
676
+ /** Build an AIRich GRID_IMAGE album instead of the legacy albumMessage collection. */
677
+ export const prepareGridImageMessage = ({ imageUrls = [], verificationMetadata } = {}) => {
678
+ if (!Array.isArray(imageUrls) || imageUrls.length < 2) {
679
+ throw new Error('GRID_IMAGE album requires at least two images');
680
+ }
681
+ const submessages = [{
682
+ messageType: RichSubMessageType.GRID_IMAGE,
683
+ gridImageMetadata: { imageUrls }
684
+ }];
685
+ const unified = toUnified(submessages, randomUUID());
686
+ const richResponseMessage = proto.AIRichResponseMessage.create({
687
+ submessages,
688
+ messageType: proto.AIRichResponseMessageType.AI_RICH_RESPONSE_TYPE_STANDARD,
689
+ unifiedResponse: { data: Buffer.from(JSON.stringify(unified)) },
690
+ contextInfo: {
691
+ isForwarded: true,
692
+ forwardingScore: 1,
693
+ forwardedAiBotMessageInfo: { botJid: '867051314767696@bot' },
694
+ forwardOrigin: 4
695
+ }
696
+ });
697
+ return wrapToBotForwardedMessage(richResponseMessage, verificationMetadata);
633
698
  };
634
699
 
635
700
  export const prepareRichResponseMessage = (content) => {
636
- 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 } = content;
701
+ 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;
637
702
  let submessages = [];
638
703
  if (Array.isArray(richResponse)) {
639
704
  submessages = richResponse.map((submessage) => {
@@ -753,7 +818,10 @@ export const prepareRichResponseMessage = (content) => {
753
818
  reelSource: reel?.reelSource || reel?.reel_source || 'IG',
754
819
  isVerified: !!(reel?.isVerified ?? reel?.is_verified)
755
820
  } })) : [];
756
- const resolvedItems = items || reelItems;
821
+ const resolvedItems = [
822
+ ...(Array.isArray(items) ? items : []),
823
+ ...reelItems
824
+ ];
757
825
  if (resolvedItems?.length) {
758
826
  submessages.push({
759
827
  messageType: RichSubMessageType.CONTENT_ITEMS,
@@ -879,7 +947,7 @@ export const prepareRichResponseMessage = (content) => {
879
947
  forwardOrigin: 4
880
948
  }
881
949
  });
882
- const message = wrapToBotForwardedMessage(richResponseMessage);
950
+ const message = wrapToBotForwardedMessage(richResponseMessage, verificationMetadata);
883
951
  const botMetadata = message.messageContextInfo.botMetadata;
884
952
  // Lia@Note 13-05-26 --- Add disclaimer text on richResponseMessage
885
953
  if (disclaimerText) {
@@ -898,6 +966,11 @@ export const prepareRichResponseMessage = (content) => {
898
966
  return message;
899
967
  };
900
968
  // Lia@Note 17-04-26 --- signature and certificateChain for proofs[] field
969
+ /**
970
+ * @deprecated Generates random bytes that verify against nothing. Kept only for
971
+ * experimental tooling; the library no longer attaches generated proofs to
972
+ * messages. Pass real proofs through the `verificationMetadata` option instead.
973
+ */
901
974
  export const botMetadataSignature = () => {
902
975
  const signature = new Uint8Array(64);
903
976
  getRandomValues(signature);
@@ -923,7 +996,7 @@ export const botMetadataCertificate = (length = 685) => {
923
996
  * @returns {Object} Wrapped botForwardedMessage ready for relayMessage
924
997
  */
925
998
  export const prepareHtmlMessage = (options = {}) => {
926
- const { html = '', trustedSources = [] } = options;
999
+ const { html = '', trustedSources = [], verificationMetadata } = options;
927
1000
  const sections = [
928
1001
  {
929
1002
  __typename: 'GenAIUnifiedResponseSection',
@@ -944,7 +1017,7 @@ export const prepareHtmlMessage = (options = {}) => {
944
1017
  unifiedResponse: { data: Buffer.from(JSON.stringify(unified)) },
945
1018
  contextInfo: { isForwarded: true, forwardingScore: 1, forwardOrigin: 4 },
946
1019
  });
947
- return wrapToBotForwardedMessage(richResponseMessage);
1020
+ return wrapToBotForwardedMessage(richResponseMessage, verificationMetadata);
948
1021
  };
949
1022
 
950
1023
  /**
@@ -959,7 +1032,8 @@ export const prepareRichHtmlMessage = (options = {}) => {
959
1032
  url = '',
960
1033
  title = 'Preview',
961
1034
  tabHeader = 'HTML',
962
- tabs
1035
+ tabs,
1036
+ verificationMetadata
963
1037
  } = options;
964
1038
  const tabItems = Array.isArray(tabs) && tabs.length
965
1039
  ? tabs
@@ -1003,26 +1077,55 @@ export const prepareRichHtmlMessage = (options = {}) => {
1003
1077
  unifiedResponse: { data: Buffer.from(JSON.stringify(unified)) },
1004
1078
  contextInfo: { isForwarded: true, forwardingScore: 1, forwardOrigin: 4 }
1005
1079
  });
1006
- return wrapToBotForwardedMessage(richResponseMessage);
1080
+ return wrapToBotForwardedMessage(richResponseMessage, verificationMetadata);
1081
+ };
1082
+
1083
+ /**
1084
+ * Proofs are forwarded only when the caller supplies real, verifiable proofs.
1085
+ * The library never fabricates a certificate chain or signature: an unverifiable
1086
+ * proof is worse than none, because it asserts provenance the caller cannot back.
1087
+ */
1088
+ export const hasVerifiableProofs = (verificationMetadata) =>
1089
+ Array.isArray(verificationMetadata?.proofs) && verificationMetadata.proofs.length > 0;
1090
+
1091
+ /**
1092
+ * Attach trusted verification proofs to an already-built rich envelope.
1093
+ *
1094
+ * Pure, null-safe, and idempotent: returns the envelope untouched when no
1095
+ * verifiable proofs are supplied or when the envelope carries no botMetadata.
1096
+ * Used by the send pipeline (`relayMessage`) to auto-inject proofs configured
1097
+ * once on the socket as `config.metaAiRendering.verificationMetadata`, so every
1098
+ * rich message (menu, reels, rich text/image/link, HTML) is covered without each
1099
+ * send method needing to thread the config through.
1100
+ */
1101
+ export const attachBotVerificationProofs = (envelope, verificationMetadata) => {
1102
+ if (!envelope || !hasVerifiableProofs(verificationMetadata))
1103
+ return envelope;
1104
+ const botMetadata = envelope?.messageContextInfo?.botMetadata;
1105
+ if (!botMetadata || botMetadata.verificationMetadata)
1106
+ return envelope;
1107
+ botMetadata.verificationMetadata = verificationMetadata;
1108
+ return envelope;
1007
1109
  };
1008
1110
 
1009
- export const wrapToBotForwardedMessage = (richResponseMessage) => ({
1111
+ /**
1112
+ * Wrap a rich response in the bot-forwarded envelope.
1113
+ *
1114
+ * @param {Object} richResponseMessage - proto.AIRichResponseMessage
1115
+ * @param {Object} [verificationMetadata] - Trusted proofs supplied by the caller
1116
+ * (typically `config.metaAiRendering.verificationMetadata`). Omitted when absent
1117
+ * or empty, so no fabricated provenance is ever sent.
1118
+ */
1119
+ export const wrapToBotForwardedMessage = (richResponseMessage, verificationMetadata) => ({
1010
1120
  messageContextInfo: {
1011
1121
  botMetadata: {
1012
- // Lia@Note 09-04-26 --- TODO: Fill verificationMetadata field
1013
- verificationMetadata: {
1014
- proofs: [
1015
- {
1016
- certificateChain: [
1017
- botMetadataCertificate(),
1018
- botMetadataCertificate(892)
1019
- ],
1020
- version: 1,
1021
- useCase: 1,
1022
- signature: botMetadataSignature()
1023
- }
1024
- ]
1025
- }
1122
+ pluginMetadata: {},
1123
+ ...(hasVerifiableProofs(verificationMetadata) ? { verificationMetadata } : {}),
1124
+ // Lia@Fix --- The Bloks rendering config is required for the client to resolve
1125
+ // and verify rich media. Without it, rich messages attach media the client
1126
+ // treats as unverified: "WhatsApp can't verify the security of this media",
1127
+ // tapping Download crashes the app, and reel previews stop rendering.
1128
+ botRenderingConfigMetadata: BOT_RENDERING_CONFIG_METADATA
1026
1129
  }
1027
1130
  },
1028
1131
  botForwardedMessage: {
package/lib/index.d.ts CHANGED
@@ -224,7 +224,17 @@ export interface RelayMessageOptions {
224
224
 
225
225
  export interface AlbumMediaInput {
226
226
  image?: unknown;
227
- video?: unknown;
227
+ type?: 'image';
228
+ data?: unknown;
229
+ sourceUrl?: string;
230
+ imagePreviewUrl?: string;
231
+ imageHighResUrl?: string;
232
+ darkModePreviewUrl?: string;
233
+ darkModeHighResUrl?: string;
234
+ width?: number;
235
+ height?: number;
236
+ darkWidth?: number;
237
+ darkHeight?: number;
228
238
  caption?: string;
229
239
  [key: string]: unknown;
230
240
  }
@@ -238,7 +248,7 @@ export interface AnimatedWallpaperInput {
238
248
 
239
249
  export interface WASocket {
240
250
  sendMessage(jid: string | string[], content: StatusContent | GroupStatusContent | Record<string, unknown>, options?: Partial<StatusOptions> & Record<string, unknown>): Promise<unknown>;
241
- /** Send at least two image/video items as an experimental WhatsApp album. */
251
+ /** Send at least two image URL items as an AIRich GRID_IMAGE album. */
242
252
  sendAlbumMessage(jid: string, medias: AlbumMediaInput[], options?: Record<string, unknown>): Promise<unknown>;
243
253
  /** Update the connected account's WhatsApp About/profile status. */
244
254
  updateProfileStatus(status: string): Promise<void>;
@@ -270,6 +280,8 @@ export interface WASocket {
270
280
  sendBotPlanning(jid: string, content?: { text?: string; steps?: Array<string | { title?: string; name?: string; body?: string }>; queryPlan?: boolean; botJid?: string }, options?: Record<string, unknown>): Promise<unknown>;
271
281
  /** Send a rich response with experimental resolved tool-call metadata. */
272
282
  sendBotToolResult(jid: string, content?: { text?: string; toolCallId?: string; resolutionData?: unknown; resolutionDataSerialized?: string; botJid?: string }, options?: Record<string, unknown>): Promise<unknown>;
283
+ /** Send one tolerant AIRich response containing every supported section supplied in content. */
284
+ sendPlogmeMessage(jid: string, content?: Record<string, unknown>, quoted?: unknown, options?: Record<string, unknown>): Promise<unknown>;
273
285
  /** Resolve a WhatsApp Channel invite code or invite URL to channel metadata. */
274
286
  newsletterGetInviteInfo(inviteCodeOrUrl: string): Promise<Record<string, unknown> | null>;
275
287
  /** React to a Channel post; pass an empty string, null, or undefined to remove the reaction. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plogme",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
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",