plogme 2.0.9 → 2.1.1

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
@@ -326,8 +326,24 @@ await sock.sendMessage(jid, {
326
326
  canAddOption: false
327
327
  }
328
328
  })
329
+
330
+ // Photo polls: each image is uploaded and sent as a MEDIA_POLL-associated child.
331
+ // Values without an image can be mixed in with image options.
332
+ await sock.sendMessage(channelJid, {
333
+ poll: {
334
+ name: 'Choose a cover',
335
+ values: [
336
+ { name: 'Purple', image: { url: './purple-cover.jpg' } },
337
+ { name: 'Blue', image: { url: './blue-cover.jpg' } },
338
+ 'Other'
339
+ ],
340
+ selectableCount: 1
341
+ }
342
+ })
329
343
  ```
330
344
 
345
+ Photo polls use WhatsApp's experimental image-poll format: a V3 poll with `pollContentType: IMAGE`, option hashes, and separately associated `pollCreationOptionImageMessage` children. Forwarding a received poll with a generic one-message forward does not automatically collect and resend those associated image children.
346
+
331
347
  ### Latest WAProto additions
332
348
 
333
349
  The WAProto generation is updated to WhatsApp Web revision
@@ -417,6 +433,13 @@ await sock.sendMessage(jid, {
417
433
  await sock.sendMessage(jid, {
418
434
  sticker: { url: './sticker.webp' }
419
435
  })
436
+
437
+ // AI and avatar-style protected sticker markers
438
+ await sock.sendMessage(jid, {
439
+ sticker: { url: './sticker.webp' },
440
+ meta: true, // native StickerMessage.isAiSticker
441
+ lock: true // native avatar-style marker; WhatsApp has no literal lock field
442
+ })
420
443
  ```
421
444
 
422
445
  Remote URLs, local file paths, and buffers are supported where the selected media helper accepts them.
@@ -1003,6 +1026,20 @@ const result = await sock.sendPlogmeMessage(jid, {
1003
1026
  verified: true
1004
1027
  }],
1005
1028
 
1029
+ // Optional raw HTML section, composed into this same unified response.
1030
+ html: '<section><h2>Deployment</h2><p>Ready</p></section>',
1031
+ trustedSources: ['crysnovax.com'],
1032
+
1033
+ // Optional tabbed HTML screen, using the same format as sendRichHtmlMessage.
1034
+ richHtml: {
1035
+ title: 'Service dashboard',
1036
+ url: 'https://crysnovax.com/dashboard',
1037
+ tabs: [
1038
+ { tab_header: 'Overview', html: '<h1>Overview</h1>' },
1039
+ { tab_header: 'Logs', html: '<h1>Logs</h1>' }
1040
+ ]
1041
+ },
1042
+
1006
1043
  // Optional source cards: [favicon URL, source URL, display text].
1007
1044
  sources: [[
1008
1045
  'https://example.com/favicon.jpg',
@@ -1029,6 +1066,8 @@ const result = await sock.sendPlogmeMessage(jid, {
1029
1066
  console.log('Sent message:', result.key.id)
1030
1067
  ```
1031
1068
 
1069
+ `sendPlogmeMessage` accepts both HTML formats: `html` uses the same `FOAHtmlPrimitiveDemoDONOTUSE` primitive as `sendHtmlMessage`, while `richHtml` uses the tabbed `GenAIaeacdsnwHtmlPrimitive` format from `sendRichHtmlMessage`. They can be included together in one response. Alternatively, pass the tabbed config as `html: { title, url, tabs, trustedSources }`.
1070
+ It also composes the compatible standalone AIRich helpers in the same response: `copyButton`/`copyToClipboard`, `richText`, `quote`, `richImage`, `richLinks`, `richMap`/`map`, `dynamicMedia`/`richDynamicMedia`, `socialEntity`, `socialProfile`, `richMenu`, `airichCommandMenu`, `gridImages`, `verticalAlbum`, `videoGrid`, `richGeneration`, `richLabel`, `suggestedPrompt`, `botSources`, and `botPttTranscript`. Use `sections` or `richSections` for newly discovered unified-response primitives, or `unifiedResponse: { sections, embedded_screens }` when passing an already serialized section set. A2UI/native-flow payloads remain separate because they use an `interactiveMessage` envelope rather than AIRich's `unifiedResponse` envelope; use `sendA2UIMessage` for those.
1032
1071
  Every field in the example is optional. A minimal response is valid as well:
1033
1072
 
1034
1073
  ```js
@@ -2155,7 +2194,12 @@ await sock.sendMessage(groupJid, {
2155
2194
  mentions: ['628123456789@s.whatsapp.net'],
2156
2195
  groupStatus: true
2157
2196
  })
2158
-
2197
+ // Compatibility test for the older groupStatusMessage envelope.
2198
+ await sock.sendMessage(groupJid, {
2199
+ text: 'Legacy group-status test',
2200
+ groupStatus: true,
2201
+ groupStatusVersion: 'legacy'
2202
+ })
2159
2203
  // Delete using the key returned by the original group-status send.
2160
2204
  // The group JID must match key.remoteJid when that field is present.
2161
2205
  await sock.deleteGroupStatus(groupJid, groupStatus.key)
@@ -2296,8 +2340,73 @@ await sock.sendMessage('status@broadcast', {
2296
2340
 
2297
2341
  Named, hex, and ARGB colors are accepted. Viewer JIDs are normalized and deduplicated.
2298
2342
 
2299
- ### Status Mentions
2343
+ ### Named custom audiences
2344
+
2345
+ A normal status can be restricted to a named status audience. This is different from a group status: it is still sent through `status@broadcast`, while the audience is configured through status privacy lists. The list name and emoji are rendered by the WhatsApp client when supported.
2346
+
2347
+ ```javascript
2348
+ const audienceMembers = [
2349
+ '15551234567@s.whatsapp.net',
2350
+ '15557654321@s.whatsapp.net'
2351
+ ]
2352
+
2353
+ await sock.setStatusPrivacy('whitelist', audienceMembers, [
2354
+ {
2355
+ id: 'cody-test-audience',
2356
+ listname: 'Cody AI Testers',
2357
+ emoji: '🧸',
2358
+ selected: true,
2359
+ members: audienceMembers
2360
+ }
2361
+ ])
2362
+
2363
+ await sock.sendStatus(
2364
+ { text: 'Only the selected custom audience can see this status.' },
2365
+ { statusJidList: audienceMembers }
2366
+ )
2367
+
2368
+ console.log(await sock.getStatusPrivacy())
2369
+ ```
2370
+
2371
+ Supported status privacy distribution types are `contacts`, `whitelist`, `blacklist`, and `null`. Named lists use the `customlist` status-privacy entry. A custom list does not turn the status into a group status, and the group JID itself must not be used as a status viewer; expand a group to its participant JIDs first.
2372
+
2373
+ ### Group status versus custom-audience status
2374
+
2375
+ These are separate WhatsApp features:
2376
+
2377
+ | Feature | Destination | Content flag/envelope | Result |
2378
+ | --- | --- | --- | --- |
2379
+ | Normal status | `status@broadcast` | `status: true` or `sendStatus()` | Personal status with selected viewers or privacy audience |
2380
+ | Custom-audience status | `status@broadcast` | Normal status plus status privacy/custom-list configuration | Native “custom audience” experience where supported by the client |
2381
+ | Group status | A group JID | `groupStatus: true` | Status attached to the group with “Added by …” and “Reply to group” UI |
2382
+ | Status mention | Array of user/group JIDs | `sendStatusMention()` or array destination | Status plus mention notification |
2383
+
2384
+ Plogme's default group-status wire envelope is `groupStatusMessageV2`. For compatibility testing, the older envelope can be selected explicitly with `groupStatusVersion: 'legacy'` (or `'v1'`). This selector applies only to group status and does not configure a custom audience.
2385
+
2386
+ For example, this tests the older group-status envelope in the supplied group without adding Cody or changing group membership:
2387
+
2388
+ ```javascript
2389
+ const testGroupJid = '120363406852054490@g.us'
2390
+
2391
+ await sock.sendMessage(testGroupJid, {
2392
+ text: 'Legacy group-status test',
2393
+ groupStatus: true,
2394
+ groupStatusVersion: 'legacy'
2395
+ })
2396
+ ```
2397
+
2398
+ The default V2 test is:
2399
+
2400
+ ```javascript
2401
+ await sock.sendMessage(testGroupJid, {
2402
+ text: 'V2 group-status test',
2403
+ groupStatus: true
2404
+ })
2405
+ ```
2406
+
2407
+ `groupStatusMessage` and `groupStatusMessageV2` are the two group-status protocol wrappers. They are not the same as `statusMentionMessage`, `groupStatusMentionMessage`, or the normal status/custom-audience path. `aiGroupAddBot()` is unrelated to all of these status APIs and is not required to send either group-status variant.
2300
2408
 
2409
+ ### Status Mentions
2301
2410
  Passing an array as the destination posts a status and sends mention notifications to those contacts or groups. This is intentionally separate from `statusJidList`, which controls viewers without mentioning them.
2302
2411
 
2303
2412
  ```javascript
@@ -3,7 +3,7 @@ import { Boom } from '@hapi/boom';
3
3
  import { randomBytes, randomUUID } 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, 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, generateWAMessageFromImagePoll, getStatusCodeForMediaRetry, makeWhatsAppFlowButton, getUrlFromDirectPath, getWAUploadToServer, MessageRetryManager, normalizeMessageContent, parseAndInjectE2ESessions, prepareWAMessageMedia, shouldIncludeBizBinaryNode, unixTimestampSeconds } from '../Utils/index.js';
7
7
  import { getUrlInfo } from '../Utils/link-preview.js';
8
8
  import { prepareRichMenuMessage, prepareA2UIMessage, prepareA2UICommandMenu, prepareAIRichCommandMenu, prepareSocialEntityMessage, prepareInstagramProfileMessage, prepareRichResponseMessage, prepareRichGenerationMessage, prepareCopyToClipboardMessage, prepareRichTextMessage, prepareRichQuoteMessage, prepareRichImageMessage, prepareRichLinkMessage, prepareSocialProfileMessage, prepareUnifiedResponseMessage, prepareRichMapMessage, prepareBotPromptSuggestionsMessage, prepareBotSourcesMessage, prepareRichDynamicMediaMessage, prepareBotPttTranscriptMessage, prepareGridImageMessage, prepareVerticalAlbumMessage, prepareVideoGridMessage, prepareHtmlMessage, prepareRichHtmlMessage, buildRichLabelMessage, attachBotVerificationProofs, hasVerifiableProofs } from '../Utils/rich-message-utils.js';
9
9
  import { generateSlotMachineHtml } from '../Utils/games/slot-machine.js';
@@ -1093,6 +1093,31 @@ export const makeMessagesSocket = (config) => {
1093
1093
  return fullMsg;
1094
1094
  };
1095
1095
 
1096
+ const sendImagePoll = async (jid, poll, sendOptions = {}) => {
1097
+ const userJid = authState.creds.me.id;
1098
+ const { additionalNodes: _callerNodes, ...relayBaseOptions } = sendOptions;
1099
+ return generateWAMessageFromImagePoll(jid, poll, async (message, relayOptions, fullMessage) => {
1100
+ await relayMessage(jid, message, {
1101
+ ...relayBaseOptions,
1102
+ useCachedGroupMetadata: sendOptions.useCachedGroupMetadata,
1103
+ statusJidList: sendOptions.statusJidList,
1104
+ statusPrivacy: sendOptions.statusPrivacy,
1105
+ ...relayOptions
1106
+ });
1107
+ if (config.emitOwnEvents && fullMessage) {
1108
+ process.nextTick(async () => {
1109
+ await messageMutex.mutex(() => upsertMessage(fullMessage, 'append'));
1110
+ });
1111
+ }
1112
+ }, {
1113
+ logger,
1114
+ userJid,
1115
+ upload: waUploadToServer,
1116
+ mediaCache: config.mediaCache,
1117
+ options: config.options,
1118
+ ...sendOptions
1119
+ });
1120
+ };
1096
1121
  const getMessageType = (message) => {
1097
1122
  if (!message)
1098
1123
  return 'text';
@@ -1576,7 +1601,7 @@ export const makeMessagesSocket = (config) => {
1576
1601
  * section present in `content`. Missing sections are intentionally
1577
1602
  * ignored by prepareRichResponseMessage instead of causing a partial
1578
1603
  * response to fail. This is the stable entry point for Plogme/CODY
1579
- * callers that may receive optional text, links, media, products,
1604
+ * callers that may receive optional text, HTML, links, media, products,
1580
1605
  * reels, tables, code, latex, or suggestion data.
1581
1606
  */
1582
1607
  sendPlogmeMessage: async (jid, content = {}, quoted, options = {}) => {
@@ -1807,6 +1832,10 @@ export const makeMessagesSocket = (config) => {
1807
1832
  },
1808
1833
  sendMessage: async (jid, content, options = {}) => {
1809
1834
  const userJid = authState.creds.me.id;
1835
+ const pollValues = content?.poll?.values;
1836
+ if (Array.isArray(pollValues) && pollValues.some(value => value && typeof value === 'object' && value.image)) {
1837
+ return sendImagePoll(jid, content.poll, options);
1838
+ }
1810
1839
  if (content?.groupStatus === true && !isJidGroup(jid)) {
1811
1840
  throw new Boom('groupStatus requires a valid group JID', { statusCode: 400 });
1812
1841
  }
@@ -3025,6 +3054,27 @@ export const makeMessagesSocket = (config) => {
3025
3054
  return fullMsg;
3026
3055
  };
3027
3056
 
3057
+ /**
3058
+ * Send a native group status. `content.private` adds WhatsApp's
3059
+ * statusAudienceMetadata without creating a group message or changing
3060
+ * group membership.
3061
+ */
3062
+ socket.sendGroupStatus = async (jid, content = {}, options = {}) => {
3063
+ if (!jid || !isJidGroup(jid)) {
3064
+ throw new Boom('sendGroupStatus requires a valid group JID', { statusCode: 400 });
3065
+ }
3066
+ if (!content || typeof content !== 'object' || Array.isArray(content)) {
3067
+ throw new Boom('sendGroupStatus content must be a message object', { statusCode: 400 });
3068
+ }
3069
+ const { private: privateAudience, ...statusContent } = content;
3070
+ const audience = privateAudience ?? options.private;
3071
+ return socket.sendMessage(
3072
+ jid,
3073
+ { ...statusContent, groupStatus: true, ...(audience !== undefined ? { private: audience } : {}) },
3074
+ options
3075
+ );
3076
+ };
3077
+
3028
3078
 
3029
3079
  /**
3030
3080
  * Sends an HTML message — renders raw HTML inside the chat bubble.
@@ -92,6 +92,21 @@ export const prepareWAMessageMedia = async (message, options) => {
92
92
  if (!uploadData.mimetype) {
93
93
  uploadData.mimetype = MIMETYPE_MAP[mediaType];
94
94
  }
95
+ if (mediaType === 'sticker') {
96
+ // @neoxr/wb names the native AI-sticker marker `meta`. WhatsApp's
97
+ // current StickerMessage schema exposes this as `isAiSticker`.
98
+ if (hasOptionalProperty(message, 'meta')) {
99
+ uploadData.isAiSticker = !!message.meta;
100
+ delete uploadData.meta;
101
+ }
102
+ // There is no protobuf field literally named `lock`. WhatsApp's
103
+ // compatible native marker for stickers that should not be saved is
104
+ // the avatar-sticker bit, which is what current clients inspect.
105
+ if (hasOptionalProperty(message, 'lock')) {
106
+ uploadData.isAvatar = !!message.lock;
107
+ delete uploadData.lock;
108
+ }
109
+ }
95
110
  if (cacheableKey) {
96
111
  const mediaBuff = await options.mediaCache.get(cacheableKey);
97
112
  if (mediaBuff) {
@@ -882,6 +897,33 @@ export const generateWAMessageContent = async (message, options) => {
882
897
  m.eventMessage.isScheduleCall = message.event.isScheduleCall ?? false;
883
898
  m.eventMessage.location = message.event.location;
884
899
  }
900
+ else if (hasNonNullishProperty(message, 'imagePoll')) {
901
+ const imagePoll = message.imagePoll;
902
+ if (!Array.isArray(imagePoll.options) || imagePoll.options.length < 2 || imagePoll.options.length > 12) {
903
+ throw new Boom('imagePoll.options must contain between 2 and 12 options', { statusCode: 400 });
904
+ }
905
+ const selectableOptionsCount = imagePoll.selectableCount ?? 1;
906
+ if (selectableOptionsCount < 0 || selectableOptionsCount > imagePoll.options.length) {
907
+ throw new Boom(`imagePoll.selectableCount should be >= 0 and <= ${imagePoll.options.length}`, {
908
+ statusCode: 400
909
+ });
910
+ }
911
+ m.pollCreationMessageV3 = {
912
+ name: imagePoll.name,
913
+ selectableOptionsCount,
914
+ options: imagePoll.options.map(option => ({
915
+ optionName: option.optionName,
916
+ optionHash: option.optionHash
917
+ })),
918
+ pollContentType: WAProto.Message.PollContentType.IMAGE,
919
+ endTime: imagePoll.endDate ? imagePoll.endDate.getTime() : undefined,
920
+ hideParticipantName: imagePoll.hideVoter ?? false,
921
+ allowAddOption: imagePoll.canAddOption ?? false
922
+ };
923
+ m.messageContextInfo = {
924
+ messageSecret: imagePoll.messageSecret || randomBytes(32)
925
+ };
926
+ }
885
927
  else if (hasNonNullishProperty(message, 'poll')) {
886
928
  (_a = message.poll).selectableCount || (_a.selectableCount = 0);
887
929
  (_b = message.poll).toAnnouncementGroup || (_b.toAnnouncementGroup = false);
@@ -1550,6 +1592,10 @@ export const generateWAMessageContent = async (message, options) => {
1550
1592
  if (hasOptionalProperty(message, 'groupStatus') && !!message.groupStatus) {
1551
1593
  const messageType = Object.keys(m)[0];
1552
1594
  const key = m[messageType];
1595
+ const privateAudience = message.private;
1596
+ if (privateAudience !== undefined && (privateAudience === null || typeof privateAudience !== 'object' || Array.isArray(privateAudience))) {
1597
+ throw new Boom('groupStatus private metadata must be an object', { statusCode: 400 });
1598
+ }
1553
1599
  if (key && 'contextInfo' in key && !!key.contextInfo) {
1554
1600
  key.contextInfo.isGroupStatus = message.groupStatus;
1555
1601
  }
@@ -1558,8 +1604,32 @@ export const generateWAMessageContent = async (message, options) => {
1558
1604
  isGroupStatus: message.groupStatus
1559
1605
  };
1560
1606
  }
1561
- m = { groupStatusMessageV2: { message: m } };
1607
+ if (privateAudience && (privateAudience.name != null || privateAudience.emoji != null)) {
1608
+ const name = privateAudience.name == null ? '' : String(privateAudience.name);
1609
+ const emoji = privateAudience.emoji == null ? '' : String(privateAudience.emoji);
1610
+ if (!name && !emoji) {
1611
+ throw new Boom('groupStatus private metadata requires a name or emoji', { statusCode: 400 });
1612
+ }
1613
+ const statusMessage = m[Object.keys(m)[0]];
1614
+ statusMessage.contextInfo = {
1615
+ ...(statusMessage.contextInfo || {}),
1616
+ statusAudienceMetadata: {
1617
+ audienceType: proto.ContextInfo.StatusAudienceMetadata.AudienceType.CLOSE_FRIENDS,
1618
+ ...(name ? { listName: name } : {}),
1619
+ ...(emoji ? { listEmoji: emoji } : {})
1620
+ }
1621
+ };
1622
+ }
1623
+ // V2 remains the default. The legacy envelope is available for
1624
+ // compatibility testing with clients that still handle the two
1625
+ // group-status fields differently.
1626
+ const groupStatusVersion = message.groupStatusVersion || options?.groupStatusVersion;
1627
+ m = groupStatusVersion === 'legacy' || groupStatusVersion === 'v1'
1628
+ ? { groupStatusMessage: { message: m } }
1629
+ : { groupStatusMessageV2: { message: m } };
1562
1630
  delete message.groupStatus;
1631
+ delete message.groupStatusVersion;
1632
+ delete message.private;
1563
1633
  }
1564
1634
  // Media spoiler support uses WhatsApp's FutureProofMessage spoiler envelope plus
1565
1635
  // contextInfo.isSpoiler. Text spoilers use ||...|| by default because native text
@@ -1790,6 +1860,91 @@ export const generateWAMessage = async (jid, content, options) => {
1790
1860
  }
1791
1861
  return generateWAMessageFromContent(jid, await generateWAMessageContent(content, options), options);
1792
1862
  };
1863
+ /**
1864
+ * Hashes an image-poll option as SHA-256(SHA-256(option name) + base64(file SHA-256)).
1865
+ * This follows the WhatsApp Web image-poll format; the format remains experimental.
1866
+ */
1867
+ export const hashImagePollOption = (optionName, fileSha256) => {
1868
+ const nameHash = sha256(Buffer.from(String(optionName), 'utf-8')).toString('hex');
1869
+ const mediaHash = fileSha256 ? Buffer.from(fileSha256).toString('base64') : '';
1870
+ return sha256(Buffer.from(nameHash + mediaHash, 'utf-8')).toString('hex');
1871
+ };
1872
+ /**
1873
+ * Sends a photo poll as one poll-creation message followed by associated image child messages.
1874
+ * `relay` receives `(message, relayOptions, webMessageInfo)` for each wire message.
1875
+ */
1876
+ export const generateWAMessageFromImagePoll = async (jid, content, relay, options = {}) => {
1877
+ if (typeof relay !== 'function') {
1878
+ throw new Boom('image poll requires a relay callback', { statusCode: 400 });
1879
+ }
1880
+ if (!Array.isArray(content?.values) || content.values.length < 2 || content.values.length > 12) {
1881
+ throw new Boom('image poll requires between 2 and 12 values', { statusCode: 400 });
1882
+ }
1883
+ if (content.values.some(value => value && typeof value === 'object' && value.image) && typeof options.upload !== 'function') {
1884
+ throw new Boom('image poll requires a configured media uploader', { statusCode: 400 });
1885
+ }
1886
+ const prepared = [];
1887
+ const pollOptions = [];
1888
+ for (const value of content.values) {
1889
+ const option = typeof value === 'string' ? { name: value } : value;
1890
+ const optionName = option?.name ?? option?.optionName;
1891
+ if (typeof optionName !== 'string' || !optionName.trim()) {
1892
+ throw new Boom('Each image poll value must have a non-empty name', { statusCode: 400 });
1893
+ }
1894
+ let media;
1895
+ if (option.image) {
1896
+ media = await prepareWAMessageMedia({ image: option.image }, { ...options, jid });
1897
+ }
1898
+ prepared.push({ media });
1899
+ pollOptions.push({
1900
+ optionName,
1901
+ optionHash: hashImagePollOption(optionName, media?.imageMessage?.fileSha256)
1902
+ });
1903
+ }
1904
+ if (!prepared.some(({ media }) => media)) {
1905
+ throw new Boom('At least one image poll option must include an image', { statusCode: 400 });
1906
+ }
1907
+ const pollMessageContent = await generateWAMessageContent({
1908
+ imagePoll: {
1909
+ name: content.name,
1910
+ selectableCount: content.selectableCount ?? 1,
1911
+ options: pollOptions,
1912
+ endDate: content.endDate,
1913
+ hideVoter: content.hideVoter,
1914
+ canAddOption: content.canAddOption,
1915
+ messageSecret: content.messageSecret
1916
+ }
1917
+ }, { ...options, jid });
1918
+ const poll = generateWAMessageFromContent(jid, pollMessageContent, { ...options, jid });
1919
+ const pollMeta = {
1920
+ tag: 'meta',
1921
+ attrs: {
1922
+ polltype: 'creation',
1923
+ contenttype: isJidNewsletter(jid) ? 'image' : undefined
1924
+ },
1925
+ content: undefined
1926
+ };
1927
+ await relay(poll.message, {
1928
+ messageId: poll.key.id,
1929
+ additionalNodes: [...(options.additionalNodes || []), pollMeta]
1930
+ }, poll);
1931
+ for (let index = 0; index < prepared.length; index++) {
1932
+ const media = prepared[index].media;
1933
+ if (!media) continue;
1934
+ const optionMessage = proto.Message.fromObject({
1935
+ pollCreationOptionImageMessage: { message: media },
1936
+ messageContextInfo: {
1937
+ messageAssociation: {
1938
+ associationType: proto.MessageAssociation.AssociationType.MEDIA_POLL,
1939
+ parentMessageKey: poll.key
1940
+ }
1941
+ }
1942
+ });
1943
+ const option = generateWAMessageFromContent(jid, optionMessage, { ...options, jid, messageId: undefined });
1944
+ await relay(option.message, { messageId: option.key.id }, option);
1945
+ }
1946
+ return poll;
1947
+ };
1793
1948
  /** Get the key to access the true type of content */
1794
1949
  export const getContentType = (content) => {
1795
1950
  if (content) {
@@ -47,8 +47,16 @@ export const tokenizeCode = (code, language = 'javascript') => {
47
47
  };
48
48
  const firstImageValue = (...values) => values.find(value => value !== undefined && value !== null && String(value).length > 0);
49
49
 
50
+ const inferImageMimeType = (url = '') => {
51
+ const path = String(url).split('?')[0].toLowerCase();
52
+ if (path.endsWith('.jpg') || path.endsWith('.jpeg')) return 'image/jpeg';
53
+ if (path.endsWith('.webp')) return 'image/webp';
54
+ if (path.endsWith('.gif')) return 'image/gif';
55
+ return 'image/png';
56
+ };
57
+
50
58
  /** Normalize album image variants without downgrading full-size URLs to previews. */
51
- const buildRichImagePrimitive = (image = {}, expiration = String(Date.now() + 30 * 24 * 60 * 60 * 1000), { fullOnly = false } = {}) => {
59
+ const buildRichImagePrimitive = (image = {}, expiration = String(Date.now() + 30 * 24 * 60 * 60 * 1000)) => {
52
60
  const previewUrl = firstImageValue(image.imagePreviewUrl, image.previewUrl, image.preview_url, image.sourceUrl, image.source_url, image.url) || '';
53
61
  const fullUrl = firstImageValue(image.imageHighResUrl, image.highResUrl, image.high_res_url, image.fullUrl, image.full_url, image.sourceUrl, image.source_url, image.url, previewUrl);
54
62
  const darkPreviewUrl = firstImageValue(image.darkModePreviewUrl, image.dark_mode_preview_url, previewUrl);
@@ -59,13 +67,7 @@ const buildRichImagePrimitive = (image = {}, expiration = String(Date.now() + 30
59
67
  const fullHeight = Number(image.fullHeight || image.full_height || image.highResHeight || image.high_res_height || image.height || height);
60
68
  const darkWidth = Number(image.darkWidth || image.dark_width || fullWidth);
61
69
  const darkHeight = Number(image.darkHeight || image.dark_height || fullHeight);
62
- const mimeType = image.mimeType || image.mime_type || (() => {
63
- const value = String(fullUrl || previewUrl).split('?')[0].toLowerCase();
64
- if (value.endsWith('.jpg') || value.endsWith('.jpeg')) return 'image/jpeg';
65
- if (value.endsWith('.webp')) return 'image/webp';
66
- if (value.endsWith('.gif')) return 'image/gif';
67
- return 'image/png';
68
- })();
70
+ const mimeType = image.mimeType || image.mime_type || inferImageMimeType(fullUrl || previewUrl);
69
71
  const media = (url, mediaWidth, mediaHeight) => ({
70
72
  url,
71
73
  url_fallback: url,
@@ -79,11 +81,11 @@ const buildRichImagePrimitive = (image = {}, expiration = String(Date.now() + 30
79
81
  asset_query_status: 'FETCHED',
80
82
  __typename: 'GenAIImagePrimitive'
81
83
  };
82
- if (!fullOnly) {
83
- primitive.preview_image = media(previewUrl, width, height);
84
- primitive.dark_mode_preview_image = media(darkPreviewUrl, darkWidth, darkHeight);
85
- primitive.dark_mode_full_image = media(darkFullUrl, darkWidth, darkHeight);
86
- }
84
+ // WhatsApp requires the preview slots to recognize an image primitive.
85
+ // Omitting them makes mixed image/video albums silently drop the image.
86
+ primitive.preview_image = media(previewUrl, width, height);
87
+ primitive.dark_mode_preview_image = media(darkPreviewUrl, darkWidth, darkHeight);
88
+ primitive.dark_mode_full_image = media(darkFullUrl, darkWidth, darkHeight);
87
89
  return primitive;
88
90
  };
89
91
  // Lia@Changes 09-04-26 --- Inject buffer into unifiedResponse.data to support proper rendering of rich messages (ex: tables and code blocks)
@@ -138,8 +140,24 @@ export const toUnified = (submessages, uuid) => ({
138
140
  }
139
141
  case RichSubMessageType.INLINE_IMAGE: {
140
142
  const image = submessage.imageMetadata || {};
141
- const media = mediaPrimitive({ url: image.imageUrl || image.url, mime_type: image.mimeType || image.mime_type, width: image.width, height: image.height });
142
- return media ? { view_model: { primitive: media, __typename: 'GenAISingleLayoutViewModel' } } : {};
143
+ const rawImage = image.imageUrl || image.url || image.image;
144
+ const imageData = typeof rawImage === 'string'
145
+ ? { ...image, url: rawImage }
146
+ : { ...image, ...(rawImage || {}) };
147
+ const primitive = buildRichImagePrimitive({
148
+ ...imageData,
149
+ mimeType: imageData.mimeType || imageData.mime_type,
150
+ });
151
+ return primitive.full_image.url
152
+ ? {
153
+ view_model: {
154
+ primitives: [{ ...primitive, display_mode: 'FULL_WIDTH', full_width: true }],
155
+ display_mode: 'FULL_WIDTH',
156
+ full_width: true,
157
+ __typename: 'GenAIVStackLayoutViewModel'
158
+ }
159
+ }
160
+ : {};
143
161
  }
144
162
  case RichSubMessageType.LATEX: {
145
163
  const metadata = submessage.latexMetadata || {};
@@ -842,23 +860,20 @@ export const prepareRichQuoteMessage = (options = {}) => {
842
860
  * @returns {Object} Wrapped botForwardedMessage ready for relayMessage
843
861
  */
844
862
  export const prepareRichImageMessage = (options = {}) => {
845
- const { url = '', mimeType = 'image/png', verificationMetadata } = options;
846
- const media = {
847
- __typename: 'GenAIMediaItem',
848
- mime_type: mimeType,
849
- url,
850
- url_fallback: url,
863
+ const { url = '', mimeType, verificationMetadata } = options;
864
+ const primitive = {
865
+ ...buildRichImagePrimitive({ url, mimeType }),
866
+ display_mode: 'FULL_WIDTH',
867
+ full_width: true,
851
868
  };
852
869
  const sections = [
853
870
  {
854
871
  __typename: 'GenAIUnifiedResponseSection',
855
872
  view_model: {
856
- __typename: 'GenAISingleLayoutViewModel',
857
- primitive: {
858
- __typename: 'GenAIImagePrimitive',
859
- preview_image: media,
860
- full_image: media,
861
- },
873
+ __typename: 'GenAIVStackLayoutViewModel',
874
+ primitives: [primitive],
875
+ display_mode: 'FULL_WIDTH',
876
+ full_width: true,
862
877
  },
863
878
  },
864
879
  ];
@@ -1009,7 +1024,7 @@ export const prepareVerticalAlbumMessage = ({ items = [], verificationMetadata }
1009
1024
  fullUrl,
1010
1025
  ...(fullWidth > 0 ? { width: fullWidth, fullWidth } : {}),
1011
1026
  ...(fullHeight > 0 ? { height: fullHeight, fullHeight } : {})
1012
- }, expiration, { fullOnly: true });
1027
+ }, expiration);
1013
1028
  return { ...primitive, display_mode: 'FULL_WIDTH', full_width: true };
1014
1029
  });
1015
1030
  const richResponseMessage = proto.AIRichResponseMessage.create({
@@ -1320,8 +1335,61 @@ export const prepareBotPttTranscriptMessage = ({ text = '', transcript = '', ver
1320
1335
  return envelope;
1321
1336
  };
1322
1337
 
1338
+ const buildFoaHtmlSection = (html, trustedSources = []) => ({
1339
+ __typename: 'GenAIUnifiedResponseSection',
1340
+ view_model: {
1341
+ __typename: 'GenAISingleLayoutViewModel',
1342
+ primitive: {
1343
+ __typename: 'FOAHtmlPrimitiveDemoDONOTUSE',
1344
+ trusted_sources: Array.isArray(trustedSources) ? trustedSources : [],
1345
+ payload: String(html ?? '').trim()
1346
+ }
1347
+ }
1348
+ });
1349
+
1350
+ const buildRichHtmlScreen = (options = {}) => {
1351
+ const {
1352
+ html = '',
1353
+ trustedSources = [],
1354
+ url = '',
1355
+ title = 'Preview',
1356
+ tabHeader = 'HTML',
1357
+ tabs
1358
+ } = options;
1359
+ const tabItems = Array.isArray(tabs) && tabs.length
1360
+ ? tabs
1361
+ : [{ id: 'tab_0', tab_header: tabHeader, html }];
1362
+ const embeddedTabs = tabItems.map((tab, index) => ({
1363
+ id: tab.id || `tab_${index}`,
1364
+ tab_header: tab.tab_header || tab.title || `HTML ${index + 1}`,
1365
+ sections: [{
1366
+ __typename: 'GenAIUnifiedResponseSection',
1367
+ view_model: {
1368
+ __typename: 'GenAISingleLayoutViewModel',
1369
+ primitive: {
1370
+ __typename: 'GenAIaeacdsnwHtmlPrimitive',
1371
+ payload: String(tab.html ?? tab.payload ?? html).trim(),
1372
+ url: tab.url || url,
1373
+ trusted_sources: tab.trustedSources || trustedSources
1374
+ }
1375
+ }
1376
+ }],
1377
+ step_entries: []
1378
+ }));
1379
+ return {
1380
+ title,
1381
+ embeddedScreen: {
1382
+ title,
1383
+ content: [{
1384
+ __typename: 'FOAIDNixelButtonSheets',
1385
+ tabs: embeddedTabs
1386
+ }]
1387
+ }
1388
+ };
1389
+ };
1390
+
1323
1391
  export const prepareRichResponseMessage = (content) => {
1324
- 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;
1392
+ const { alignment, code, contentText, disclaimerText, footerText, headerText, html, richHtml, trustedSources, imageText, inlineImage, inlineVideo, items, language, latex, links, noDonation, noHeading, posts, products, reels, sources: sourceInputs, suggested, richResponse, table, tapLinkUrl, title, verificationMetadata, sections: rawSections, richSections, copyButton, copyToClipboard, copyText, copyLabel, richText, quote, richImage, richLinks, richMap, map, dynamicMedia, richDynamicMedia, socialEntity, socialProfile, richMenu, airichCommandMenu, unifiedResponse, gridImages, verticalAlbum, videoGrid, richGeneration, richLabel, suggestedPrompt, botSources, botPttTranscript } = content;
1325
1393
  let submessages = [];
1326
1394
  const extraSections = [];
1327
1395
  if (Array.isArray(richResponse)) {
@@ -1456,19 +1524,25 @@ export const prepareRichResponseMessage = (content) => {
1456
1524
  });
1457
1525
  }
1458
1526
  if (inlineImage) {
1527
+ const image = typeof inlineImage === 'string' ? { url: inlineImage } : (inlineImage || {});
1459
1528
  submessages.push({
1460
1529
  messageType: RichSubMessageType.INLINE_IMAGE,
1461
1530
  imageMetadata: {
1462
- imageUrl: inlineImage,
1531
+ ...image,
1532
+ imageUrl: image.url || image.imageUrl || image.image || '',
1463
1533
  imageText,
1464
1534
  alignment,
1465
- tapLinkUrl
1535
+ tapLinkUrl,
1536
+ mimeType: image.mimeType || image.mime_type,
1537
+ width: image.width,
1538
+ height: image.height,
1539
+ fullWidth: image.fullWidth || image.full_width,
1540
+ fullHeight: image.fullHeight || image.full_height
1466
1541
  }
1467
1542
  });
1468
1543
  }
1469
1544
  if (inlineVideo) {
1470
1545
  const video = typeof inlineVideo === 'string' ? { url: inlineVideo } : inlineVideo;
1471
- submessages.push({ messageType: RichSubMessageType.TEXT, messageText: '[ CANNOT_LOAD_VIDEO - NIXEL ]' });
1472
1546
  extraSections.push({
1473
1547
  view_model: {
1474
1548
  primitive: buildImaginePrimitive({
@@ -1565,10 +1639,6 @@ export const prepareRichResponseMessage = (content) => {
1565
1639
  });
1566
1640
  }
1567
1641
  if (posts) {
1568
- submessages.push({
1569
- messageType: RichSubMessageType.TEXT,
1570
- messageText: '[ CANNOT_LOAD_POST - NIXEL ]'
1571
- });
1572
1642
  const postItems = (Array.isArray(posts) ? posts : [posts]).map((post) => ({
1573
1643
  title: post?.title || '',
1574
1644
  subtitle: post?.subtitle || '',
@@ -1593,10 +1663,6 @@ export const prepareRichResponseMessage = (content) => {
1593
1663
  if (postItems.length) extraSections.push({ view_model: { primitives: postItems, __typename: 'GenAIHScrollLayoutViewModel' } });
1594
1664
  }
1595
1665
  if (products) {
1596
- submessages.push({
1597
- messageType: RichSubMessageType.TEXT,
1598
- messageText: '[ CANNOT_LOAD_PRODUCT - NIXEL ]'
1599
- });
1600
1666
  const productItems = (Array.isArray(products) ? products : [products]).map((product) => ({
1601
1667
  title: product?.title || '',
1602
1668
  brand: product?.brand || '',
@@ -1640,11 +1706,72 @@ export const prepareRichResponseMessage = (content) => {
1640
1706
  });
1641
1707
  }
1642
1708
  }
1709
+ const htmlIsTabbedConfig = html && typeof html === 'object' && Array.isArray(html.tabs);
1710
+ const simpleHtmlPayload = htmlIsTabbedConfig ? '' : (typeof html === 'string' ? html : (html?.html ?? html?.payload ?? ''));
1711
+ const htmlPayload = simpleHtmlPayload;
1712
+ const htmlTrustedSources = Array.isArray(html?.trustedSources) ? html.trustedSources : trustedSources;
1713
+ if (String(htmlPayload).trim()) {
1714
+ extraSections.push(buildFoaHtmlSection(htmlPayload, htmlTrustedSources));
1715
+ }
1716
+ const richHtmlOptions = richHtml ?? (htmlIsTabbedConfig ? html : null);
1717
+ const richHtmlConfig = typeof richHtmlOptions === 'string'
1718
+ ? { html: richHtmlOptions, trustedSources }
1719
+ : (richHtmlOptions && typeof richHtmlOptions === 'object'
1720
+ ? { ...richHtmlOptions, trustedSources: richHtmlOptions.trustedSources ?? trustedSources }
1721
+ : null);
1722
+ const hasRichHtmlContent = !!(richHtmlConfig && (
1723
+ String(richHtmlConfig.html || '').trim() ||
1724
+ (Array.isArray(richHtmlConfig.tabs) && richHtmlConfig.tabs.some(tab => String(tab?.html ?? tab?.payload ?? '').trim()))
1725
+ ));
1726
+ let richHtmlScreen;
1727
+ if (hasRichHtmlContent) {
1728
+ richHtmlScreen = buildRichHtmlScreen(richHtmlConfig);
1729
+ submessages.unshift({
1730
+ messageType: RichSubMessageType.TEXT,
1731
+ messageText: `> ${richHtmlScreen.title}`
1732
+ });
1733
+ }
1643
1734
  const uuid = randomUUID();
1644
1735
  const unified = toUnified(submessages, uuid);
1645
1736
  unified.sections = [...unified.sections.filter(Boolean), ...extraSections];
1737
+ if (richHtmlScreen) unified.embedded_screens = [richHtmlScreen.embeddedScreen];
1738
+
1739
+ // Compose the remaining AIRich convenience formats as sections. These
1740
+ // aliases intentionally mirror the standalone send* helpers, allowing a
1741
+ // caller to mix them with text, HTML, tables, media, and citations in one
1742
+ // PLOGME message. Raw sections remain available as the forward-compatible
1743
+ // escape hatch for new WhatsApp primitives.
1744
+ const composed = { sections: unified.sections, embedded_screens: unified.embedded_screens, submessages, botMetadata: {} };
1745
+ const append = (prepared) => appendPreparedRichContent(composed, prepared);
1746
+ if (copyButton || copyToClipboard || copyText) append(prepareCopyToClipboardMessage(copyButton || copyToClipboard || { text: copyText, label: copyLabel }));
1747
+ if (richText) append(prepareRichTextMessage(typeof richText === 'string' ? { text: richText } : richText));
1748
+ if (quote) append(prepareRichQuoteMessage(typeof quote === 'string' ? { text: quote } : quote));
1749
+ if (richImage) append(prepareRichImageMessage(typeof richImage === 'string' ? { url: richImage } : richImage));
1750
+ if (richLinks) append(prepareRichLinkMessage(Array.isArray(richLinks) ? { links: richLinks } : richLinks));
1751
+ if (richMap || map) append(prepareRichMapMessage(richMap || map));
1752
+ if (dynamicMedia || richDynamicMedia) append(prepareRichDynamicMediaMessage(dynamicMedia || richDynamicMedia));
1753
+ if (socialEntity) append(prepareSocialEntityMessage(socialEntity));
1754
+ if (socialProfile) append(prepareSocialProfileMessage(socialProfile));
1755
+ if (richMenu) append(prepareRichMenuMessage(richMenu));
1756
+ if (airichCommandMenu) append(prepareAIRichCommandMenu(airichCommandMenu));
1757
+ if (gridImages) append(prepareGridImageMessage({ imageUrls: gridImages, verificationMetadata }));
1758
+ if (verticalAlbum) append(prepareVerticalAlbumMessage({ items: verticalAlbum, verificationMetadata }));
1759
+ if (videoGrid) append(prepareVideoGridMessage({ videos: videoGrid, verificationMetadata }));
1760
+ if (richGeneration) append(prepareRichGenerationMessage({ ...richGeneration, verificationMetadata }));
1761
+ if (richLabel) append(buildRichLabelMessage(richLabel));
1762
+ if (suggestedPrompt) append(prepareBotPromptSuggestionsMessage({ ...suggestedPrompt, verificationMetadata }));
1763
+ if (botSources) append(prepareBotSourcesMessage({ ...botSources, verificationMetadata }));
1764
+ if (botPttTranscript) append(prepareBotPttTranscriptMessage({ ...botPttTranscript, verificationMetadata }));
1765
+ if (unifiedResponse?.sections) {
1766
+ unified.sections.push(...unifiedResponse.sections);
1767
+ if (Array.isArray(unifiedResponse.embedded_screens)) unified.embedded_screens = [...(unified.embedded_screens || []), ...unifiedResponse.embedded_screens];
1768
+ }
1769
+ for (const section of [...(Array.isArray(rawSections) ? rawSections : []), ...(Array.isArray(richSections) ? richSections : [])]) {
1770
+ if (section && typeof section === 'object') unified.sections.push(section);
1771
+ }
1772
+ if (composed.embedded_screens?.length) unified.embedded_screens = composed.embedded_screens;
1646
1773
  const richResponseMessage = proto.AIRichResponseMessage.create({
1647
- submessages,
1774
+ submessages: composed.submessages,
1648
1775
  messageType: proto.AIRichResponseMessageType.AI_RICH_RESPONSE_TYPE_STANDARD,
1649
1776
  unifiedResponse: {
1650
1777
  data: Buffer.from(JSON.stringify(unified)) // Lia@Note 25-04-26 --- Expects "ArrayBufferLike"
@@ -1658,6 +1785,7 @@ export const prepareRichResponseMessage = (content) => {
1658
1785
  });
1659
1786
  const message = wrapToBotForwardedMessage(richResponseMessage, verificationMetadata);
1660
1787
  const botMetadata = message.messageContextInfo.botMetadata;
1788
+ Object.assign(botMetadata, composed.botMetadata);
1661
1789
  // Lia@Note 13-05-26 --- Add disclaimer text on richResponseMessage
1662
1790
  if (disclaimerText) {
1663
1791
  botMetadata.messageDisclaimerText = disclaimerText;
@@ -1706,19 +1834,7 @@ export const botMetadataCertificate = (length = 685) => {
1706
1834
  */
1707
1835
  export const prepareHtmlMessage = (options = {}) => {
1708
1836
  const { html = '', trustedSources = [], verificationMetadata } = options;
1709
- const sections = [
1710
- {
1711
- __typename: 'GenAIUnifiedResponseSection',
1712
- view_model: {
1713
- __typename: 'GenAISingleLayoutViewModel',
1714
- primitive: {
1715
- __typename: 'FOAHtmlPrimitiveDemoDONOTUSE',
1716
- trusted_sources: trustedSources,
1717
- payload: String(html).trim()
1718
- }
1719
- }
1720
- }
1721
- ];
1837
+ const sections = [buildFoaHtmlSection(html, trustedSources)];
1722
1838
  const unified = { response_id: randomUUID(), sections };
1723
1839
  const richResponseMessage = proto.AIRichResponseMessage.create({
1724
1840
  submessages: [],
@@ -1735,35 +1851,8 @@ export const prepareHtmlMessage = (options = {}) => {
1735
1851
  * aeacdsnw HTML primitive used by the attached game message.
1736
1852
  */
1737
1853
  export const prepareRichHtmlMessage = (options = {}) => {
1738
- const {
1739
- html = '',
1740
- trustedSources = [],
1741
- url = '',
1742
- title = 'Preview',
1743
- tabHeader = 'HTML',
1744
- tabs,
1745
- verificationMetadata
1746
- } = options;
1747
- const tabItems = Array.isArray(tabs) && tabs.length
1748
- ? tabs
1749
- : [{ id: 'tab_0', tab_header: tabHeader, html }];
1750
- const embeddedTabs = tabItems.map((tab, index) => ({
1751
- id: tab.id || `tab_${index}`,
1752
- tab_header: tab.tab_header || tab.title || `HTML ${index + 1}`,
1753
- sections: [{
1754
- __typename: 'GenAIUnifiedResponseSection',
1755
- view_model: {
1756
- __typename: 'GenAISingleLayoutViewModel',
1757
- primitive: {
1758
- __typename: 'GenAIaeacdsnwHtmlPrimitive',
1759
- payload: String(tab.html ?? tab.payload ?? html).trim(),
1760
- url: tab.url || url,
1761
- trusted_sources: tab.trustedSources || trustedSources
1762
- }
1763
- }
1764
- }],
1765
- step_entries: []
1766
- }));
1854
+ const { verificationMetadata } = options;
1855
+ const { title, embeddedScreen } = buildRichHtmlScreen(options);
1767
1856
  const unified = {
1768
1857
  response_id: randomUUID(),
1769
1858
  sections: [{
@@ -1772,13 +1861,7 @@ export const prepareRichHtmlMessage = (options = {}) => {
1772
1861
  __typename: 'GenAISingleLayoutViewModel'
1773
1862
  }
1774
1863
  }],
1775
- embedded_screens: [{
1776
- title,
1777
- content: [{
1778
- __typename: 'FOAIDNixelButtonSheets',
1779
- tabs: embeddedTabs
1780
- }]
1781
- }]
1864
+ embedded_screens: [embeddedScreen]
1782
1865
  };
1783
1866
  const richResponseMessage = proto.AIRichResponseMessage.create({
1784
1867
  messageType: proto.AIRichResponseMessageType.AI_RICH_RESPONSE_TYPE_STANDARD,
@@ -1789,6 +1872,35 @@ export const prepareRichHtmlMessage = (options = {}) => {
1789
1872
  return wrapToBotForwardedMessage(richResponseMessage, verificationMetadata);
1790
1873
  };
1791
1874
 
1875
+ const decodePreparedUnified = (prepared) => {
1876
+ const richResponse = prepared?.botForwardedMessage?.message?.richResponseMessage;
1877
+ const rawData = richResponse?.unifiedResponse?.data;
1878
+ if (!rawData) return null;
1879
+ try {
1880
+ const text = Buffer.isBuffer(rawData)
1881
+ ? rawData.toString()
1882
+ : (rawData instanceof Uint8Array ? Buffer.from(rawData).toString() : String(rawData));
1883
+ const decoded = Buffer.from(text, /^[A-Za-z0-9+/]+=*$/.test(text) ? 'base64' : 'utf8').toString();
1884
+ return JSON.parse(decoded);
1885
+ }
1886
+ catch {
1887
+ return null;
1888
+ }
1889
+ };
1890
+
1891
+ /** Merge any prepared AIRich helper into a single PLOGME response. */
1892
+ const appendPreparedRichContent = (target, prepared) => {
1893
+ const unified = decodePreparedUnified(prepared);
1894
+ if (unified?.sections?.length) target.sections.push(...unified.sections);
1895
+ if (Array.isArray(unified?.embedded_screens)) {
1896
+ target.embedded_screens = [...(target.embedded_screens || []), ...unified.embedded_screens];
1897
+ }
1898
+ const richResponse = prepared?.botForwardedMessage?.message?.richResponseMessage;
1899
+ if (Array.isArray(richResponse?.submessages)) target.submessages.push(...richResponse.submessages);
1900
+ const botMetadata = prepared?.messageContextInfo?.botMetadata;
1901
+ if (botMetadata) Object.assign(target.botMetadata, botMetadata);
1902
+ };
1903
+
1792
1904
  /**
1793
1905
  * Proofs are forwarded only when the caller supplies real, verifiable proofs.
1794
1906
  * The library never fabricates a certificate chain or signature: an unverifiable
package/lib/index.d.ts CHANGED
@@ -50,6 +50,8 @@ export interface StatusContent {
50
50
 
51
51
  export interface GroupStatusContent extends StatusContent {
52
52
  groupStatus: true;
53
+ /** Select the legacy groupStatusMessage envelope for compatibility testing. */
54
+ groupStatusVersion?: 'legacy' | 'v1' | 'v2';
53
55
  }
54
56
 
55
57
  /** One status distribution list entry as returned by `getStatusPrivacy`. */
@@ -466,6 +468,8 @@ export interface WASocket {
466
468
  sendBotToolResult(jid: string, content?: { text?: string; toolCallId?: string; resolutionData?: unknown; resolutionDataSerialized?: string; botJid?: string }, options?: Record<string, unknown>): Promise<unknown>;
467
469
  /** Send one tolerant AIRich response containing every supported section supplied in content. */
468
470
  sendPlogmeMessage(jid: string, content?: Record<string, unknown>, quoted?: unknown, options?: Record<string, unknown>): Promise<unknown>;
471
+ /** Send a native group status with optional private named-audience metadata. */
472
+ sendGroupStatus(jid: string, content: Record<string, unknown>, options?: { private?: { name?: string; emoji?: string }; [key: string]: unknown }): Promise<unknown>;
469
473
  /** Send a compact forwarded AIRich label bubble with optional colors and variant. */
470
474
  sendRichLabelMessage(jid: string, content?: { text?: string; variant?: string; backgroundColor?: string; textColor?: string; responseId?: string }, options?: Record<string, unknown>): Promise<Record<string, unknown>>;
471
475
  /** Send a prompt through the verified Meta AI/Hatch route and return its message ID. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plogme",
3
- "version": "2.0.9",
3
+ "version": "2.1.1",
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",