plogme 2.1.0 → 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 +111 -2
- package/lib/Socket/messages-send.js +52 -2
- package/lib/Utils/messages.js +156 -1
- package/lib/Utils/rich-message-utils.js +195 -83
- package/lib/index.d.ts +4 -0
- package/package.json +1 -1
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
|
-
###
|
|
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.
|
package/lib/Utils/messages.js
CHANGED
|
@@ -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
|
-
|
|
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,6 +47,14 @@ 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
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) || '';
|
|
@@ -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,
|
|
@@ -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
|
|
142
|
-
|
|
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
|
|
846
|
-
const
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
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: '
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
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
|
];
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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.1.
|
|
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",
|