plogme 2.0.2 → 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, attachBotVerificationProofs, hasVerifiableProofs } 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';
@@ -1033,6 +1033,53 @@ export const makeMessagesSocket = (config) => {
1033
1033
  return msgId;
1034
1034
  };
1035
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
+
1036
1083
  const getMessageType = (message) => {
1037
1084
  if (!message)
1038
1085
  return 'text';
@@ -1391,11 +1438,7 @@ export const makeMessagesSocket = (config) => {
1391
1438
  quoted,
1392
1439
  ...options
1393
1440
  });
1394
- await relayMessage(jid, generated.message, {
1395
- messageId: generated.key.id,
1396
- useCachedGroupMetadata: options.useCachedGroupMetadata,
1397
- statusJidList: options.statusJidList
1398
- });
1441
+ await relayRichMessage(jid, generated, options);
1399
1442
  return { ...generated, messageId: generated.key.id };
1400
1443
  },
1401
1444
  /**
@@ -1408,9 +1451,15 @@ export const makeMessagesSocket = (config) => {
1408
1451
  responseId: options.responseId,
1409
1452
  forwarded: options.forwarded !== false
1410
1453
  });
1411
- const messageId = generateMessageIDV2(jid);
1412
- await relayMessage(jid, generated.message, { messageId, quoted });
1413
- 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 };
1414
1463
  },
1415
1464
  /**
1416
1465
  * Replace the generation primitive in an existing rich response.
@@ -1476,14 +1525,47 @@ export const makeMessagesSocket = (config) => {
1476
1525
  quoted,
1477
1526
  ...options
1478
1527
  });
1479
- await relayMessage(jid, fullMsg.message, {
1480
- messageId: fullMsg.key.id,
1481
- useCachedGroupMetadata: options.useCachedGroupMetadata,
1482
- 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,
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,
1483
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
+ }
1484
1566
  return fullMsg;
1485
1567
  },
1486
- captureAndResendUnifiedResponse: async (jid, metaAiMsg, quoted) => {
1568
+ captureAndResendUnifiedResponse: async (jid, metaAiMsg, quoted, options = {}) => {
1487
1569
  const captured = captureUnifiedResponse(metaAiMsg);
1488
1570
  if (!captured) {
1489
1571
  throw new Boom('captureAndResendUnifiedResponse: message contains no unifiedResponse data');
@@ -1495,11 +1577,7 @@ export const makeMessagesSocket = (config) => {
1495
1577
  messageId: generated.messageId || generateMessageIDV2(jid),
1496
1578
  quoted
1497
1579
  });
1498
- await relayMessage(jid, fullMsg.message, {
1499
- messageId: fullMsg.key.id,
1500
- useCachedGroupMetadata: options?.useCachedGroupMetadata,
1501
- statusJidList: options?.statusJidList
1502
- });
1580
+ await relayRichMessage(jid, fullMsg, options);
1503
1581
  return fullMsg;
1504
1582
  },
1505
1583
  updateMediaMessage: async (message) => {
@@ -2039,6 +2117,9 @@ export const makeMessagesSocket = (config) => {
2039
2117
  }
2040
2118
  // ── NORMAL MESSAGE HANDLING ──
2041
2119
  else {
2120
+ if ('album' in content) {
2121
+ return socket.sendAlbumMessage(jid, content.album, options);
2122
+ }
2042
2123
  const fullMsg = await generateWAMessage(jid, content, {
2043
2124
  logger,
2044
2125
  userJid,
@@ -2127,13 +2208,6 @@ export const makeMessagesSocket = (config) => {
2127
2208
  delete content.ai;
2128
2209
  }
2129
2210
 
2130
- if ('album' in content) {
2131
- if (!Array.isArray(content.album) || content.album.length < 2) {
2132
- throw new Boom('Album requires at least two image or video items', { statusCode: 400 });
2133
- }
2134
- fullMsg.message.messageContextInfo ||= {};
2135
- fullMsg.message.messageContextInfo.messageSecret ||= randomBytes(32);
2136
- }
2137
2211
  await relayMessage(jid, fullMsg.message, {
2138
2212
  messageId: fullMsg.key.id,
2139
2213
  useCachedGroupMetadata: options.useCachedGroupMetadata,
@@ -2150,55 +2224,6 @@ export const makeMessagesSocket = (config) => {
2150
2224
  });
2151
2225
  }
2152
2226
 
2153
- if ('album' in content) {
2154
- const { delayMs = 1500 } = options;
2155
- for (const [messageIndex, albumMedia] of content.album.entries()) {
2156
- // Give the album collection stanza time to register before
2157
- // its associated media children are relayed. This mirrors
2158
- // the ordering used by WhatsApp clients and avoids the
2159
- // children being treated as unrelated media messages.
2160
- await delay(delayMs);
2161
- const albumMsg = await generateWAMessage(jid, albumMedia, {
2162
- logger,
2163
- userJid,
2164
- upload: waUploadToServer,
2165
- mediaCache: config.mediaCache,
2166
- options: config.options,
2167
- ...options,
2168
- messageId: generateMessageIDV2(userJid)
2169
- });
2170
- if (!hasValidAlbumMedia(normalizeMessageContent(albumMsg.message))) {
2171
- throw new Boom('Invalid message type for album', { statusCode: 400 });
2172
- }
2173
- albumMsg.message.messageContextInfo ||= {};
2174
- // WhatsApp expects an encrypted context secret on each album
2175
- // child. Without it, clients may show the album as an
2176
- // unsupported message or fail to group the media together.
2177
- albumMsg.message.messageContextInfo.messageSecret ||= randomBytes(32);
2178
- albumMsg.message.messageContextInfo.messageAssociation = {
2179
- parentMessageKey: fullMsg.key,
2180
- // WhatsApp album child messages use association type 1.
2181
- // The generated MEDIA_ALBUM enum value (63) is a message
2182
- // category, not the child-association wire value.
2183
- associationType: 1,
2184
- messageIndex
2185
- };
2186
- await relayMessage(jid, albumMsg.message, {
2187
- messageId: albumMsg.key.id,
2188
- useCachedGroupMetadata: options.useCachedGroupMetadata,
2189
- addBizAttributes: isNeedBizAttrs,
2190
- statusJidList: options.statusJidList,
2191
- additionalAttributes,
2192
- additionalNodes
2193
- });
2194
- if (config.emitOwnEvents) {
2195
- process.nextTick(async () => {
2196
- await messageMutex.mutex(() => upsertMessage(albumMsg, 'append'));
2197
- });
2198
- }
2199
- }
2200
- }
2201
-
2202
2227
  return fullMsg;
2203
2228
  }
2204
2229
  }
@@ -2216,11 +2241,7 @@ export const makeMessagesSocket = (config) => {
2216
2241
  messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id),
2217
2242
  ...options,
2218
2243
  });
2219
- await relayMessage(jid, fullMsg.message, {
2220
- messageId: fullMsg.key.id,
2221
- useCachedGroupMetadata: options.useCachedGroupMetadata,
2222
- statusJidList: options.statusJidList,
2223
- });
2244
+ await relayRichMessage(jid, fullMsg, options);
2224
2245
  if (config.emitOwnEvents) {
2225
2246
  process.nextTick(async () => {
2226
2247
  await messageMutex.mutex(() => upsertMessage(fullMsg, 'append'));
@@ -2241,11 +2262,7 @@ export const makeMessagesSocket = (config) => {
2241
2262
  messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id),
2242
2263
  ...options,
2243
2264
  });
2244
- await relayMessage(jid, fullMsg.message, {
2245
- messageId: fullMsg.key.id,
2246
- useCachedGroupMetadata: options.useCachedGroupMetadata,
2247
- statusJidList: options.statusJidList,
2248
- });
2265
+ await relayRichMessage(jid, fullMsg, options);
2249
2266
  if (config.emitOwnEvents) {
2250
2267
  process.nextTick(async () => {
2251
2268
  await messageMutex.mutex(() => upsertMessage(fullMsg, 'append'));
@@ -2259,7 +2276,7 @@ export const makeMessagesSocket = (config) => {
2259
2276
  if (!content.copyText) throw new Boom('sendRichTextCopy requires copyText content', { statusCode: 400 });
2260
2277
  const wrapped = prepareRichQuoteMessage(content);
2261
2278
  const fullMsg = await generateWAMessageFromContent(jid, wrapped, { logger, userJid: authState.creds.me?.id, messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id), ...options });
2262
- await relayMessage(jid, fullMsg.message, { messageId: fullMsg.key.id, useCachedGroupMetadata: options.useCachedGroupMetadata, statusJidList: options.statusJidList });
2279
+ await relayRichMessage(jid, fullMsg, options);
2263
2280
  return fullMsg;
2264
2281
  };
2265
2282
  /** Sends Markdown text containing blockquotes in a forwarded GenAI rich response. */
@@ -2267,7 +2284,7 @@ export const makeMessagesSocket = (config) => {
2267
2284
  if (!jid) throw new Boom('sendMarkdownQuote requires a target JID', { statusCode: 400 });
2268
2285
  const wrapped = prepareRichQuoteMessage(content);
2269
2286
  const fullMsg = await generateWAMessageFromContent(jid, wrapped, { logger, userJid: authState.creds.me?.id, messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id), ...options });
2270
- await relayMessage(jid, fullMsg.message, { messageId: fullMsg.key.id, useCachedGroupMetadata: options.useCachedGroupMetadata, statusJidList: options.statusJidList });
2287
+ await relayRichMessage(jid, fullMsg, options);
2271
2288
  return fullMsg;
2272
2289
  };
2273
2290
  /** Sends Markdown blockquotes followed by a GenAI COPY_TO_CLIPBOARD action. */
@@ -2276,7 +2293,7 @@ export const makeMessagesSocket = (config) => {
2276
2293
  if (!content.copyText) throw new Boom('sendQuoteCopy requires copyText content', { statusCode: 400 });
2277
2294
  const wrapped = prepareRichQuoteMessage(content);
2278
2295
  const fullMsg = await generateWAMessageFromContent(jid, wrapped, { logger, userJid: authState.creds.me?.id, messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id), ...options });
2279
- await relayMessage(jid, fullMsg.message, { messageId: fullMsg.key.id, useCachedGroupMetadata: options.useCachedGroupMetadata, statusJidList: options.statusJidList });
2296
+ await relayRichMessage(jid, fullMsg, options);
2280
2297
  return fullMsg;
2281
2298
  };
2282
2299
  /**
@@ -2291,11 +2308,7 @@ export const makeMessagesSocket = (config) => {
2291
2308
  messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id),
2292
2309
  ...options,
2293
2310
  });
2294
- await relayMessage(jid, fullMsg.message, {
2295
- messageId: fullMsg.key.id,
2296
- useCachedGroupMetadata: options.useCachedGroupMetadata,
2297
- statusJidList: options.statusJidList,
2298
- });
2311
+ await relayRichMessage(jid, fullMsg, options);
2299
2312
  if (config.emitOwnEvents) {
2300
2313
  process.nextTick(async () => {
2301
2314
  await messageMutex.mutex(() => upsertMessage(fullMsg, 'append'));
@@ -2315,11 +2328,7 @@ export const makeMessagesSocket = (config) => {
2315
2328
  messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id),
2316
2329
  ...options,
2317
2330
  });
2318
- await relayMessage(jid, fullMsg.message, {
2319
- messageId: fullMsg.key.id,
2320
- useCachedGroupMetadata: options.useCachedGroupMetadata,
2321
- statusJidList: options.statusJidList,
2322
- });
2331
+ await relayRichMessage(jid, fullMsg, options);
2323
2332
  if (config.emitOwnEvents) {
2324
2333
  process.nextTick(async () => {
2325
2334
  await messageMutex.mutex(() => upsertMessage(fullMsg, 'append'));
@@ -2336,11 +2345,7 @@ export const makeMessagesSocket = (config) => {
2336
2345
  messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id),
2337
2346
  ...options,
2338
2347
  });
2339
- await relayMessage(jid, fullMsg.message, {
2340
- messageId: fullMsg.key.id,
2341
- useCachedGroupMetadata: options.useCachedGroupMetadata,
2342
- statusJidList: options.statusJidList,
2343
- });
2348
+ await relayRichMessage(jid, fullMsg, options);
2344
2349
  if (config.emitOwnEvents) {
2345
2350
  process.nextTick(async () => {
2346
2351
  await messageMutex.mutex(() => upsertMessage(fullMsg, 'append'));
@@ -2357,7 +2362,7 @@ export const makeMessagesSocket = (config) => {
2357
2362
  botForwardedMessage: { message: { richResponseMessage: { messageType: 1, submessages: [{ messageType: 2, messageText: text }], contextInfo: { isForwarded: true, forwardingScore: 1, forwardedAiBotMessageInfo: { botJid }, forwardOrigin: 4 } } } }
2358
2363
  };
2359
2364
  const fullMsg = await generateWAMessageFromContent(jid, wrapped, { logger, userJid: authState.creds.me?.id, messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id), ...options });
2360
- await relayMessage(jid, fullMsg.message, { messageId: fullMsg.key.id, useCachedGroupMetadata: options.useCachedGroupMetadata, statusJidList: options.statusJidList });
2365
+ await relayRichMessage(jid, fullMsg, options);
2361
2366
  return fullMsg;
2362
2367
  };
2363
2368
  /** Sends a rich response with experimental BotCommandMetadata. */
@@ -2366,7 +2371,7 @@ export const makeMessagesSocket = (config) => {
2366
2371
  const { text = 'Bot command test', commandName = '', commandDescription = '', commandPrompt = '', botJid = '867051314767696@bot' } = content;
2367
2372
  const wrapped = buildBotRichResponse({ text, botJid, botMetadata: { commandMetadata: { commandName, commandDescription, commandPrompt } } });
2368
2373
  const fullMsg = await generateWAMessageFromContent(jid, wrapped, { logger, userJid: authState.creds.me?.id, messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id), ...options });
2369
- await relayMessage(jid, fullMsg.message, { messageId: fullMsg.key.id, useCachedGroupMetadata: options.useCachedGroupMetadata, statusJidList: options.statusJidList });
2374
+ await relayRichMessage(jid, fullMsg, options);
2370
2375
  return fullMsg;
2371
2376
  };
2372
2377
  /** Sends a planning/execution-plan rich response with WAProto capabilities. */
@@ -2389,7 +2394,7 @@ export const makeMessagesSocket = (config) => {
2389
2394
  ]
2390
2395
  });
2391
2396
  const fullMsg = await generateWAMessageFromContent(jid, wrapped, { logger, userJid: authState.creds.me?.id, messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id), ...options });
2392
- await relayMessage(jid, fullMsg.message, { messageId: fullMsg.key.id, useCachedGroupMetadata: options.useCachedGroupMetadata, statusJidList: options.statusJidList });
2397
+ await relayRichMessage(jid, fullMsg, options);
2393
2398
  return fullMsg;
2394
2399
  };
2395
2400
  /** Sends a rich response with experimental BotResolvedToolCallMetadata. */
@@ -2408,7 +2413,7 @@ export const makeMessagesSocket = (config) => {
2408
2413
  }
2409
2414
  });
2410
2415
  const fullMsg = await generateWAMessageFromContent(jid, wrapped, { logger, userJid: authState.creds.me?.id, messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id), ...options });
2411
- await relayMessage(jid, fullMsg.message, { messageId: fullMsg.key.id, useCachedGroupMetadata: options.useCachedGroupMetadata, statusJidList: options.statusJidList });
2416
+ await relayRichMessage(jid, fullMsg, options);
2412
2417
  return fullMsg;
2413
2418
  };
2414
2419
  socket.sendStatus = async (content, options = {}) => {
@@ -2611,25 +2616,53 @@ export const makeMessagesSocket = (config) => {
2611
2616
 
2612
2617
 
2613
2618
  /**
2614
- * Sends an experimental WhatsApp album. The parent album header and each
2615
- * image/video child are relayed separately and associated by parent key.
2616
- * 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.
2617
2622
  */
2618
2623
  socket.sendAlbumMessage = async (jid, medias = [], options = {}) => {
2619
2624
  if (!jid) throw new Boom('sendAlbumMessage requires a target JID', { statusCode: 400 });
2620
2625
  if (!Array.isArray(medias) || medias.length < 2) {
2621
2626
  throw new Boom('sendAlbumMessage requires at least two media items', { statusCode: 400 });
2622
2627
  }
2623
- const album = medias.map((media, index) => {
2624
- const normalized = media?.type && media?.data
2625
- ? { [media.type]: media.data }
2626
- : { ...media };
2627
- if (index === 0 && options.caption && !normalized.caption) {
2628
- 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 });
2629
2632
  }
2630
- 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
2631
2658
  });
2632
- 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;
2633
2666
  };
2634
2667
 
2635
2668
 
@@ -2688,11 +2721,7 @@ export const makeMessagesSocket = (config) => {
2688
2721
  messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id),
2689
2722
  ...options
2690
2723
  });
2691
- await relayMessage(jid, fullMsg.message, {
2692
- messageId: fullMsg.key.id,
2693
- useCachedGroupMetadata: options.useCachedGroupMetadata,
2694
- statusJidList: options.statusJidList
2695
- });
2724
+ await relayRichMessage(jid, fullMsg, options);
2696
2725
  return fullMsg;
2697
2726
  };
2698
2727
 
@@ -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 = {
@@ -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) => {
@@ -649,6 +673,30 @@ export const prepareRichLinkMessage = (options = {}) => {
649
673
  return wrapToBotForwardedMessage(richResponseMessage, verificationMetadata);
650
674
  };
651
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);
698
+ };
699
+
652
700
  export const prepareRichResponseMessage = (content) => {
653
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;
654
702
  let submessages = [];
@@ -770,7 +818,10 @@ export const prepareRichResponseMessage = (content) => {
770
818
  reelSource: reel?.reelSource || reel?.reel_source || 'IG',
771
819
  isVerified: !!(reel?.isVerified ?? reel?.is_verified)
772
820
  } })) : [];
773
- const resolvedItems = items || reelItems;
821
+ const resolvedItems = [
822
+ ...(Array.isArray(items) ? items : []),
823
+ ...reelItems
824
+ ];
774
825
  if (resolvedItems?.length) {
775
826
  submessages.push({
776
827
  messageType: RichSubMessageType.CONTENT_ITEMS,
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.2",
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",