plogme 2.0.2 → 2.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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, prepareA2UICommandMenu, prepareAIRichCommandMenu, prepareRichResponseMessage, prepareRichGenerationMessage, prepareCopyToClipboardMessage, prepareRichTextMessage, prepareRichQuoteMessage, prepareRichImageMessage, prepareRichLinkMessage, prepareGridImageMessage, prepareVerticalAlbumMessage, prepareVideoGridMessage, prepareHtmlMessage, prepareRichHtmlMessage, attachBotVerificationProofs, hasVerifiableProofs } from '../Utils/rich-message-utils.js';
9
9
  import { generateSlotMachineHtml } from '../Utils/games/slot-machine.js';
10
10
  import { generateMiniAppHtml } from '../Utils/games/miniapp.js';
11
11
  import { generateWebsitePreviewHtml, generateLinkPreviewHtml } from '../Utils/games/website-preview.js';
@@ -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';
@@ -1229,18 +1276,39 @@ export const makeMessagesSocket = (config) => {
1229
1276
  },
1230
1277
  sendTable: async (jid, title, headers, rows, quoted, options = {}) => {
1231
1278
  const { message, messageId } = generateTableContent(title, headers, rows, quoted, options);
1232
- await relayMessage(jid, message, { messageId });
1233
- return { message, messageId };
1279
+ const fullMsg = generateWAMessageFromContent(jid, message, {
1280
+ logger,
1281
+ userJid: authState.creds.me?.id,
1282
+ messageId: options.messageId || messageId || generateMessageIDV2(jid),
1283
+ quoted,
1284
+ ...options
1285
+ });
1286
+ await relayRichMessage(jid, fullMsg, options);
1287
+ return fullMsg;
1234
1288
  },
1235
1289
  sendList: async (jid, title, items, quoted, options = {}) => {
1236
1290
  const { message, messageId } = generateListContent(title, items, quoted, options);
1237
- await relayMessage(jid, message, { messageId });
1238
- return { message, messageId };
1291
+ const fullMsg = generateWAMessageFromContent(jid, message, {
1292
+ logger,
1293
+ userJid: authState.creds.me?.id,
1294
+ messageId: options.messageId || messageId || generateMessageIDV2(jid),
1295
+ quoted,
1296
+ ...options
1297
+ });
1298
+ await relayRichMessage(jid, fullMsg, options);
1299
+ return fullMsg;
1239
1300
  },
1240
1301
  sendCodeBlock: async (jid, code, quoted, options = {}) => {
1241
1302
  const { message, messageId } = generateCodeBlockContent(code, quoted, options);
1242
- await relayMessage(jid, message, { messageId });
1243
- return { message, messageId };
1303
+ const fullMsg = generateWAMessageFromContent(jid, message, {
1304
+ logger,
1305
+ userJid: authState.creds.me?.id,
1306
+ messageId: options.messageId || messageId || generateMessageIDV2(jid),
1307
+ quoted,
1308
+ ...options
1309
+ });
1310
+ await relayRichMessage(jid, fullMsg, options);
1311
+ return fullMsg;
1244
1312
  },
1245
1313
  sendCarousel: async (jid, cards, options = {}) => {
1246
1314
  if (!Array.isArray(cards) || cards.length === 0) {
@@ -1276,8 +1344,15 @@ export const makeMessagesSocket = (config) => {
1276
1344
  },
1277
1345
  sendLatex: async (jid, quoted, options = {}) => {
1278
1346
  const { message, messageId } = generateLatexContent(quoted, { expressions: [], ...options });
1279
- await relayMessage(jid, message, { messageId });
1280
- return { message, messageId };
1347
+ const fullMsg = generateWAMessageFromContent(jid, message, {
1348
+ logger,
1349
+ userJid: authState.creds.me?.id,
1350
+ messageId: options.messageId || messageId || generateMessageIDV2(jid),
1351
+ quoted,
1352
+ ...options
1353
+ });
1354
+ await relayRichMessage(jid, fullMsg, options);
1355
+ return fullMsg;
1281
1356
  },
1282
1357
  /**
1283
1358
  * Send LaTeX as rendered PNG image (like Meta AI does).
@@ -1305,8 +1380,14 @@ export const makeMessagesSocket = (config) => {
1305
1380
  headerText,
1306
1381
  footer
1307
1382
  });
1308
- await relayMessage(jid, message, { messageId });
1309
- return { message, messageId };
1383
+ const fullMsg = generateWAMessageFromContent(jid, message, {
1384
+ logger,
1385
+ userJid: authState.creds.me?.id,
1386
+ messageId: options.messageId || messageId || generateMessageIDV2(jid),
1387
+ ...options
1388
+ });
1389
+ await relayRichMessage(jid, fullMsg, options);
1390
+ return fullMsg;
1310
1391
  },
1311
1392
  sendMetaAI: async (a, b, c = {}) => {
1312
1393
  // baron signature: (jid, text, opts) — README signature: (text, opts)
@@ -1391,11 +1472,7 @@ export const makeMessagesSocket = (config) => {
1391
1472
  quoted,
1392
1473
  ...options
1393
1474
  });
1394
- await relayMessage(jid, generated.message, {
1395
- messageId: generated.key.id,
1396
- useCachedGroupMetadata: options.useCachedGroupMetadata,
1397
- statusJidList: options.statusJidList
1398
- });
1475
+ await relayRichMessage(jid, generated, options);
1399
1476
  return { ...generated, messageId: generated.key.id };
1400
1477
  },
1401
1478
  /**
@@ -1408,9 +1485,15 @@ export const makeMessagesSocket = (config) => {
1408
1485
  responseId: options.responseId,
1409
1486
  forwarded: options.forwarded !== false
1410
1487
  });
1411
- const messageId = generateMessageIDV2(jid);
1412
- await relayMessage(jid, generated.message, { messageId, quoted });
1413
- return { ...generated, messageId };
1488
+ const fullMsg = generateWAMessageFromContent(jid, generated.message, {
1489
+ logger,
1490
+ userJid: authState.creds.me?.id,
1491
+ messageId: options.messageId || generated.messageId || generateMessageIDV2(jid),
1492
+ quoted,
1493
+ ...options,
1494
+ });
1495
+ await relayRichMessage(jid, fullMsg, options);
1496
+ return { ...generated, messageId: fullMsg.key.id };
1414
1497
  },
1415
1498
  /**
1416
1499
  * Replace the generation primitive in an existing rich response.
@@ -1476,14 +1559,47 @@ export const makeMessagesSocket = (config) => {
1476
1559
  quoted,
1477
1560
  ...options
1478
1561
  });
1479
- await relayMessage(jid, fullMsg.message, {
1480
- messageId: fullMsg.key.id,
1481
- useCachedGroupMetadata: options.useCachedGroupMetadata,
1482
- statusJidList: options.statusJidList
1562
+ await relayRichMessage(jid, fullMsg, options);
1563
+ return fullMsg;
1564
+ },
1565
+ /**
1566
+ * Send one tolerant AIRich/unified response containing every supported
1567
+ * section present in `content`. Missing sections are intentionally
1568
+ * ignored by prepareRichResponseMessage instead of causing a partial
1569
+ * response to fail. This is the stable entry point for Plogme/CODY
1570
+ * callers that may receive optional text, links, media, products,
1571
+ * reels, tables, code, latex, or suggestion data.
1572
+ */
1573
+ sendPlogmeMessage: async (jid, content = {}, quoted, options = {}) => {
1574
+ if (!jid) throw new Boom('sendPlogmeMessage requires a target JID', { statusCode: 400 });
1575
+ if (!content || typeof content !== 'object' || Array.isArray(content)) {
1576
+ throw new Boom('sendPlogmeMessage content must be an object', { statusCode: 400 });
1577
+ }
1578
+
1579
+ const wrapped = prepareRichResponseMessage({
1580
+ ...content,
1581
+ // Avoid silently appending the library donation link to a
1582
+ // unified response unless the caller explicitly opts in.
1583
+ noDonation: options.noDonation ?? true,
1483
1584
  });
1585
+ const fullMsg = generateWAMessageFromContent(jid, wrapped, {
1586
+ logger,
1587
+ userJid: authState.creds.me?.id,
1588
+ messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id || jid),
1589
+ quoted,
1590
+ ...options,
1591
+ });
1592
+
1593
+ await relayRichMessage(jid, fullMsg, options);
1594
+
1595
+ if (config.emitOwnEvents) {
1596
+ process.nextTick(async () => {
1597
+ await messageMutex.mutex(() => upsertMessage(fullMsg, 'append'));
1598
+ });
1599
+ }
1484
1600
  return fullMsg;
1485
1601
  },
1486
- captureAndResendUnifiedResponse: async (jid, metaAiMsg, quoted) => {
1602
+ captureAndResendUnifiedResponse: async (jid, metaAiMsg, quoted, options = {}) => {
1487
1603
  const captured = captureUnifiedResponse(metaAiMsg);
1488
1604
  if (!captured) {
1489
1605
  throw new Boom('captureAndResendUnifiedResponse: message contains no unifiedResponse data');
@@ -1495,11 +1611,7 @@ export const makeMessagesSocket = (config) => {
1495
1611
  messageId: generated.messageId || generateMessageIDV2(jid),
1496
1612
  quoted
1497
1613
  });
1498
- await relayMessage(jid, fullMsg.message, {
1499
- messageId: fullMsg.key.id,
1500
- useCachedGroupMetadata: options?.useCachedGroupMetadata,
1501
- statusJidList: options?.statusJidList
1502
- });
1614
+ await relayRichMessage(jid, fullMsg, options);
1503
1615
  return fullMsg;
1504
1616
  },
1505
1617
  updateMediaMessage: async (message) => {
@@ -2039,6 +2151,9 @@ export const makeMessagesSocket = (config) => {
2039
2151
  }
2040
2152
  // ── NORMAL MESSAGE HANDLING ──
2041
2153
  else {
2154
+ if ('album' in content) {
2155
+ return socket.sendAlbumMessage(jid, content.album, options);
2156
+ }
2042
2157
  const fullMsg = await generateWAMessage(jid, content, {
2043
2158
  logger,
2044
2159
  userJid,
@@ -2127,13 +2242,6 @@ export const makeMessagesSocket = (config) => {
2127
2242
  delete content.ai;
2128
2243
  }
2129
2244
 
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
2245
  await relayMessage(jid, fullMsg.message, {
2138
2246
  messageId: fullMsg.key.id,
2139
2247
  useCachedGroupMetadata: options.useCachedGroupMetadata,
@@ -2150,55 +2258,6 @@ export const makeMessagesSocket = (config) => {
2150
2258
  });
2151
2259
  }
2152
2260
 
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
2261
  return fullMsg;
2203
2262
  }
2204
2263
  }
@@ -2216,11 +2275,7 @@ export const makeMessagesSocket = (config) => {
2216
2275
  messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id),
2217
2276
  ...options,
2218
2277
  });
2219
- await relayMessage(jid, fullMsg.message, {
2220
- messageId: fullMsg.key.id,
2221
- useCachedGroupMetadata: options.useCachedGroupMetadata,
2222
- statusJidList: options.statusJidList,
2223
- });
2278
+ await relayRichMessage(jid, fullMsg, options);
2224
2279
  if (config.emitOwnEvents) {
2225
2280
  process.nextTick(async () => {
2226
2281
  await messageMutex.mutex(() => upsertMessage(fullMsg, 'append'));
@@ -2241,11 +2296,7 @@ export const makeMessagesSocket = (config) => {
2241
2296
  messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id),
2242
2297
  ...options,
2243
2298
  });
2244
- await relayMessage(jid, fullMsg.message, {
2245
- messageId: fullMsg.key.id,
2246
- useCachedGroupMetadata: options.useCachedGroupMetadata,
2247
- statusJidList: options.statusJidList,
2248
- });
2299
+ await relayRichMessage(jid, fullMsg, options);
2249
2300
  if (config.emitOwnEvents) {
2250
2301
  process.nextTick(async () => {
2251
2302
  await messageMutex.mutex(() => upsertMessage(fullMsg, 'append'));
@@ -2259,7 +2310,7 @@ export const makeMessagesSocket = (config) => {
2259
2310
  if (!content.copyText) throw new Boom('sendRichTextCopy requires copyText content', { statusCode: 400 });
2260
2311
  const wrapped = prepareRichQuoteMessage(content);
2261
2312
  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 });
2313
+ await relayRichMessage(jid, fullMsg, options);
2263
2314
  return fullMsg;
2264
2315
  };
2265
2316
  /** Sends Markdown text containing blockquotes in a forwarded GenAI rich response. */
@@ -2267,7 +2318,7 @@ export const makeMessagesSocket = (config) => {
2267
2318
  if (!jid) throw new Boom('sendMarkdownQuote requires a target JID', { statusCode: 400 });
2268
2319
  const wrapped = prepareRichQuoteMessage(content);
2269
2320
  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 });
2321
+ await relayRichMessage(jid, fullMsg, options);
2271
2322
  return fullMsg;
2272
2323
  };
2273
2324
  /** Sends Markdown blockquotes followed by a GenAI COPY_TO_CLIPBOARD action. */
@@ -2276,7 +2327,7 @@ export const makeMessagesSocket = (config) => {
2276
2327
  if (!content.copyText) throw new Boom('sendQuoteCopy requires copyText content', { statusCode: 400 });
2277
2328
  const wrapped = prepareRichQuoteMessage(content);
2278
2329
  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 });
2330
+ await relayRichMessage(jid, fullMsg, options);
2280
2331
  return fullMsg;
2281
2332
  };
2282
2333
  /**
@@ -2291,11 +2342,7 @@ export const makeMessagesSocket = (config) => {
2291
2342
  messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id),
2292
2343
  ...options,
2293
2344
  });
2294
- await relayMessage(jid, fullMsg.message, {
2295
- messageId: fullMsg.key.id,
2296
- useCachedGroupMetadata: options.useCachedGroupMetadata,
2297
- statusJidList: options.statusJidList,
2298
- });
2345
+ await relayRichMessage(jid, fullMsg, options);
2299
2346
  if (config.emitOwnEvents) {
2300
2347
  process.nextTick(async () => {
2301
2348
  await messageMutex.mutex(() => upsertMessage(fullMsg, 'append'));
@@ -2315,11 +2362,7 @@ export const makeMessagesSocket = (config) => {
2315
2362
  messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id),
2316
2363
  ...options,
2317
2364
  });
2318
- await relayMessage(jid, fullMsg.message, {
2319
- messageId: fullMsg.key.id,
2320
- useCachedGroupMetadata: options.useCachedGroupMetadata,
2321
- statusJidList: options.statusJidList,
2322
- });
2365
+ await relayRichMessage(jid, fullMsg, options);
2323
2366
  if (config.emitOwnEvents) {
2324
2367
  process.nextTick(async () => {
2325
2368
  await messageMutex.mutex(() => upsertMessage(fullMsg, 'append'));
@@ -2336,11 +2379,53 @@ export const makeMessagesSocket = (config) => {
2336
2379
  messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id),
2337
2380
  ...options,
2338
2381
  });
2339
- await relayMessage(jid, fullMsg.message, {
2340
- messageId: fullMsg.key.id,
2341
- useCachedGroupMetadata: options.useCachedGroupMetadata,
2342
- statusJidList: options.statusJidList,
2382
+ await relayRichMessage(jid, fullMsg, options);
2383
+ if (config.emitOwnEvents) {
2384
+ process.nextTick(async () => {
2385
+ await messageMutex.mutex(() => upsertMessage(fullMsg, 'append'));
2386
+ });
2387
+ }
2388
+ return fullMsg;
2389
+ };
2390
+ /**
2391
+ * Sends the A2UI interactive card/menu format used by WhatsApp's large
2392
+ * command-card presentation. This is intentionally separate from richMenu
2393
+ * because it is an interactiveMessage, not an AIRich unifiedResponse.
2394
+ */
2395
+ socket.sendA2UICommandMenu = async (jid, content = {}, options = {}) => {
2396
+ if (!jid) throw new Boom('sendA2UICommandMenu requires a target JID', { statusCode: 400 });
2397
+ const { image, ...menuContent } = content || {};
2398
+ let imageMessage;
2399
+ if (image) {
2400
+ const prepared = await prepareWAMessageMedia({ image }, { upload: waUploadToServer });
2401
+ imageMessage = prepared.imageMessage;
2402
+ }
2403
+ const wrapped = prepareA2UICommandMenu({ ...menuContent, imageMessage });
2404
+ const fullMsg = await generateWAMessageFromContent(jid, wrapped, {
2405
+ logger,
2406
+ userJid: authState.creds.me?.id,
2407
+ messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id || jid),
2408
+ ...options,
2343
2409
  });
2410
+ await relayMessage(jid, fullMsg.message, { messageId: fullMsg.key.id, ...options.relayOptions });
2411
+ if (config.emitOwnEvents) {
2412
+ process.nextTick(async () => {
2413
+ await messageMutex.mutex(() => upsertMessage(fullMsg, 'append'));
2414
+ });
2415
+ }
2416
+ return fullMsg;
2417
+ };
2418
+ /** Sends the manually serialized AIRich command card shown in the screenshot. */
2419
+ socket.sendAIRichCommandMenu = async (jid, content = {}, options = {}) => {
2420
+ if (!jid) throw new Boom('sendAIRichCommandMenu requires a target JID', { statusCode: 400 });
2421
+ const wrapped = prepareAIRichCommandMenu(content);
2422
+ const fullMsg = await generateWAMessageFromContent(jid, wrapped, {
2423
+ logger,
2424
+ userJid: authState.creds.me?.id,
2425
+ messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id || jid),
2426
+ ...options,
2427
+ });
2428
+ await relayRichMessage(jid, fullMsg, options);
2344
2429
  if (config.emitOwnEvents) {
2345
2430
  process.nextTick(async () => {
2346
2431
  await messageMutex.mutex(() => upsertMessage(fullMsg, 'append'));
@@ -2357,7 +2442,7 @@ export const makeMessagesSocket = (config) => {
2357
2442
  botForwardedMessage: { message: { richResponseMessage: { messageType: 1, submessages: [{ messageType: 2, messageText: text }], contextInfo: { isForwarded: true, forwardingScore: 1, forwardedAiBotMessageInfo: { botJid }, forwardOrigin: 4 } } } }
2358
2443
  };
2359
2444
  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 });
2445
+ await relayRichMessage(jid, fullMsg, options);
2361
2446
  return fullMsg;
2362
2447
  };
2363
2448
  /** Sends a rich response with experimental BotCommandMetadata. */
@@ -2366,7 +2451,7 @@ export const makeMessagesSocket = (config) => {
2366
2451
  const { text = 'Bot command test', commandName = '', commandDescription = '', commandPrompt = '', botJid = '867051314767696@bot' } = content;
2367
2452
  const wrapped = buildBotRichResponse({ text, botJid, botMetadata: { commandMetadata: { commandName, commandDescription, commandPrompt } } });
2368
2453
  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 });
2454
+ await relayRichMessage(jid, fullMsg, options);
2370
2455
  return fullMsg;
2371
2456
  };
2372
2457
  /** Sends a planning/execution-plan rich response with WAProto capabilities. */
@@ -2389,7 +2474,7 @@ export const makeMessagesSocket = (config) => {
2389
2474
  ]
2390
2475
  });
2391
2476
  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 });
2477
+ await relayRichMessage(jid, fullMsg, options);
2393
2478
  return fullMsg;
2394
2479
  };
2395
2480
  /** Sends a rich response with experimental BotResolvedToolCallMetadata. */
@@ -2408,7 +2493,7 @@ export const makeMessagesSocket = (config) => {
2408
2493
  }
2409
2494
  });
2410
2495
  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 });
2496
+ await relayRichMessage(jid, fullMsg, options);
2412
2497
  return fullMsg;
2413
2498
  };
2414
2499
  socket.sendStatus = async (content, options = {}) => {
@@ -2611,25 +2696,73 @@ export const makeMessagesSocket = (config) => {
2611
2696
 
2612
2697
 
2613
2698
  /**
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: ... }].
2699
+ * Sends a full-width vertical AIRich album. Unlike the legacy
2700
+ * albumMessage collection or compact GRID_IMAGE layout, this is one
2701
+ * forwarded unified-response message with one large media item per row.
2617
2702
  */
2618
2703
  socket.sendAlbumMessage = async (jid, medias = [], options = {}) => {
2619
2704
  if (!jid) throw new Boom('sendAlbumMessage requires a target JID', { statusCode: 400 });
2620
2705
  if (!Array.isArray(medias) || medias.length < 2) {
2621
2706
  throw new Boom('sendAlbumMessage requires at least two media items', { statusCode: 400 });
2622
2707
  }
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;
2708
+ const albumItems = medias.map((media) => {
2709
+ const raw = media?.type && media?.data ? media.data : (media?.image ?? media);
2710
+ if (media?.video || media?.type === 'video') {
2711
+ const video = media?.video || raw;
2712
+ const sourceUrl = typeof video === 'string' ? video : (video?.url || video?.videoUrl || '');
2713
+ if (!sourceUrl) throw new Boom('Each video album item requires a video URL', { statusCode: 400 });
2714
+ return { type: 'video', video: { ...video, url: sourceUrl } };
2629
2715
  }
2630
- return normalized;
2716
+ return {
2717
+ type: 'image',
2718
+ image: raw
2719
+ };
2720
+ });
2721
+ const wrapped = prepareVerticalAlbumMessage({
2722
+ items: albumItems,
2723
+ verificationMetadata: options.verificationMetadata
2631
2724
  });
2632
- return socket.sendMessage(jid, { album }, options);
2725
+ const fullMsg = generateWAMessageFromContent(jid, wrapped, {
2726
+ logger,
2727
+ userJid: authState.creds.me?.id,
2728
+ messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id || jid),
2729
+ ...options
2730
+ });
2731
+ await relayRichMessage(jid, fullMsg, options);
2732
+ if (config.emitOwnEvents) {
2733
+ process.nextTick(async () => {
2734
+ await messageMutex.mutex(() => upsertMessage(fullMsg, 'append'));
2735
+ });
2736
+ }
2737
+ return fullMsg;
2738
+ };
2739
+
2740
+ /**
2741
+ * Sends a unified-response video grid. This uses GenAIVideoPrimitive items
2742
+ * inside GenAIGridLayoutViewModel; it is not the image-only GRID_IMAGE type.
2743
+ */
2744
+ socket.sendVideoGridMessage = async (jid, videos = [], options = {}) => {
2745
+ if (!jid) throw new Boom('sendVideoGridMessage requires a target JID', { statusCode: 400 });
2746
+ if (!Array.isArray(videos) || videos.length < 2) {
2747
+ throw new Boom('sendVideoGridMessage requires at least two videos', { statusCode: 400 });
2748
+ }
2749
+ const wrapped = prepareVideoGridMessage({
2750
+ videos,
2751
+ verificationMetadata: options.verificationMetadata
2752
+ });
2753
+ const fullMsg = generateWAMessageFromContent(jid, wrapped, {
2754
+ logger,
2755
+ userJid: authState.creds.me?.id,
2756
+ messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id || jid),
2757
+ ...options
2758
+ });
2759
+ await relayRichMessage(jid, fullMsg, options);
2760
+ if (config.emitOwnEvents) {
2761
+ process.nextTick(async () => {
2762
+ await messageMutex.mutex(() => upsertMessage(fullMsg, 'append'));
2763
+ });
2764
+ }
2765
+ return fullMsg;
2633
2766
  };
2634
2767
 
2635
2768
 
@@ -2688,11 +2821,7 @@ export const makeMessagesSocket = (config) => {
2688
2821
  messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id),
2689
2822
  ...options
2690
2823
  });
2691
- await relayMessage(jid, fullMsg.message, {
2692
- messageId: fullMsg.key.id,
2693
- useCachedGroupMetadata: options.useCachedGroupMetadata,
2694
- statusJidList: options.statusJidList
2695
- });
2824
+ await relayRichMessage(jid, fullMsg, options);
2696
2825
  return fullMsg;
2697
2826
  };
2698
2827
 
@@ -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 = {