plogme 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # plogme
2
2
 
3
- A Node.js WhatsApp Web API client for building messaging integrations with authentication, media transfer, groups, interactive messages, status updates, and rich message composition.
3
+ `plogme` is a Node.js WhatsApp Web client based on the Baileys protocol implementation. It supports authentication, media, groups, interactive messages, status updates, albums, native flows, rich responses, and experimental Meta AI-style message envelopes.
4
4
 
5
- > **Package naming note:** npm scoped packages require a scope and package name. The valid package form is `plogme`; the bare string `@plogme` is a scope, not an installable package name by itself.
5
+ > **Experimental feature notice:** Rich responses, Meta AI-style metadata, HTML primitives, clipboard addon sections, carousels, and albums use WhatsApp Web protocol structures that may change without notice. Unsupported fields can be ignored, rejected, or rendered differently across WhatsApp versions.
6
6
 
7
7
  ## Installation
8
8
 
@@ -10,15 +10,7 @@ A Node.js WhatsApp Web API client for building messaging integrations with authe
10
10
  npm install plogme
11
11
  ```
12
12
 
13
- For a repository checkout:
14
-
15
- ```bash
16
- npm install github:crysnovax/baileys
17
- ```
18
-
19
- ## Requirements
20
-
21
- The package supports current Node.js LTS releases. Use Node.js 20 or newer for the latest runtime and dependency compatibility.
13
+ The current package version is **1.0.4**. Use Node.js 20 or newer.
22
14
 
23
15
  ## Quick start
24
16
 
@@ -33,164 +25,452 @@ import pino from 'pino'
33
25
 
34
26
  async function start() {
35
27
  const { state, saveCreds } = await useMultiFileAuthState('./session')
36
- const socket = makeWASocket({
28
+ const sock = makeWASocket({
37
29
  auth: state,
38
30
  logger: pino({ level: 'silent' })
39
31
  })
40
32
 
41
- socket.ev.on('creds.update', saveCreds)
42
- socket.ev.on('connection.update', ({ connection, lastDisconnect }) => {
33
+ sock.ev.on('creds.update', saveCreds)
34
+ sock.ev.on('connection.update', ({ connection, lastDisconnect }) => {
43
35
  if (connection === 'open') console.log('Connected')
44
36
  if (connection === 'close') {
45
37
  const status = new Boom(lastDisconnect?.error)?.output?.statusCode
46
38
  if (status !== DisconnectReason.loggedOut) start()
47
39
  }
48
40
  })
49
-
50
- socket.ev.on('messages.upsert', async ({ messages }) => {
51
- const message = messages[0]
52
- if (!message?.message) return
53
- await socket.sendMessage(message.key.remoteJid, { text: 'Hello from plogme' })
54
- })
55
41
  }
56
42
 
57
43
  start()
58
44
  ```
59
45
 
60
- ## Authentication
46
+ Keep authentication files private. Never commit session credentials, private keys, or `.env` files.
61
47
 
62
- The recommended authentication state is multi-file authentication:
48
+ ## Authentication and pairing
63
49
 
64
50
  ```js
65
- import { useMultiFileAuthState } from 'plogme'
66
-
67
51
  const { state, saveCreds } = await useMultiFileAuthState('./session')
68
- ```
69
52
 
70
- Pairing codes can be requested after the socket has initialized:
53
+ const sock = makeWASocket({ auth: state })
54
+ sock.ev.on('creds.update', saveCreds)
71
55
 
72
- ```js
73
- const code = await socket.requestPairingCode('15551234567')
56
+ const code = await sock.requestPairingCode('15551234567')
74
57
  console.log(code)
75
58
  ```
76
59
 
77
- Keep authentication files private. Do not commit session credentials, private keys, or environment files to a public repository.
60
+ Pairing codes require the socket to be initialized and a valid phone number in international format.
78
61
 
79
- ## Text, mentions, and reactions
62
+ ## Ordinary text, mentions, reactions, and media
80
63
 
81
64
  ```js
82
- await socket.sendMessage(jid, { text: 'Hello!' })
65
+ await sock.sendMessage(jid, { text: 'Hello!' })
83
66
 
84
- await socket.sendMessage(jid, {
67
+ await sock.sendMessage(jid, {
85
68
  text: 'Hello @15551234567',
86
69
  mentions: ['15551234567@s.whatsapp.net']
87
70
  })
88
71
 
89
- await socket.sendMessage(jid, {
90
- react: { text: '✅', key: message.key }
72
+ await sock.sendMessage(jid, {
73
+ react: { text: '👍', key: message.key }
91
74
  })
92
75
  ```
93
76
 
94
- ## Media
77
+ Media examples:
95
78
 
96
79
  ```js
97
- await socket.sendMessage(jid, {
80
+ await sock.sendMessage(jid, {
98
81
  image: { url: './image.jpg' },
99
82
  caption: 'Image caption'
100
83
  })
101
84
 
102
- await socket.sendMessage(jid, {
85
+ await sock.sendMessage(jid, {
103
86
  video: { url: './video.mp4' },
104
87
  caption: 'Video caption',
105
88
  gifPlayback: false
106
89
  })
107
90
 
108
- await socket.sendMessage(jid, {
91
+ await sock.sendMessage(jid, {
109
92
  document: { url: './report.pdf' },
110
93
  mimetype: 'application/pdf',
111
94
  fileName: 'report.pdf'
112
95
  })
113
96
 
114
- await socket.sendMessage(jid, {
97
+ await sock.sendMessage(jid, {
115
98
  audio: { url: './audio.ogg' },
116
99
  mimetype: 'audio/ogg; codecs=opus'
117
100
  })
118
101
  ```
119
102
 
120
- ## Interactive messages
103
+ ## Profile name and About description
104
+
105
+ WhatsApp exposes the personal profile description as the **About status**. In
106
+ `plogme`, use `updateProfileStatus`—not `updateProfileDescription`—and pass a
107
+ plain string:
108
+
109
+ ```js
110
+ await sock.updateProfileStatus('Building helpful bots')
111
+ await sock.updateProfileName('Cody AI')
112
+ ```
113
+
114
+ This updates the connected account's own profile. It does not update a contact
115
+ or group description.
116
+
117
+ ## Spoiler messages
118
+
119
+ For media, use `spoiler: true`. The default native mode serializes the
120
+ supported `contextInfo.isSpoiler` flag:
121
+
122
+ ```js
123
+ await sock.sendMessage(jid, {
124
+ image: { url: './classified.jpg' },
125
+ caption: 'Open only when ready',
126
+ spoiler: true
127
+ })
128
+ ```
129
+
130
+ For clients that do not support native media spoilers, explicitly use the
131
+ compatibility fallback. This sends a view-once envelope; it is **not** the
132
+ same as a native spoiler:
133
+
134
+ ```js
135
+ await sock.sendMessage(jid, {
136
+ video: { url: './reveal.mp4' },
137
+ spoiler: true,
138
+ mediaSpoilerMode: 'viewOnce'
139
+ })
140
+ ```
141
+
142
+ For text, the default fallback uses WhatsApp's rolling-out `||...||` spoiler
143
+ formatting. Native text metadata can be requested with
144
+ `spoilerMode: 'native'`. Rendering remains client-version dependent.
145
+
146
+ ```js
147
+ await sock.sendMessage(jid, {
148
+ text: 'The ending is revealed here',
149
+ spoiler: true
150
+ })
151
+ ```
152
+
153
+ ## Rich text and Markdown
154
+
155
+ `sendRichText` sends Markdown-capable text inside a forwarded GenAI rich-response envelope. Markdown rendering depends on the recipient's WhatsApp client.
156
+
157
+ ```js
158
+ await sock.sendRichText(jid, {
159
+ text: `**Heading**
160
+
161
+ Normal text
162
+
163
+ > This is a blockquote`
164
+ })
165
+ ```
166
+
167
+ A dedicated blockquote helper is also available:
168
+
169
+ ```js
170
+ await sock.sendMarkdownQuote(jid, {
171
+ text: `Here is the instruction:
172
+
173
+ > Use this exact logo image.`
174
+ })
175
+ ```
176
+
177
+ ## Copy-to-clipboard controls
178
+
179
+ There are two different clipboard-related features:
180
+
181
+ 1. `sendCopyButton` creates the inline GenAI `COPY_TO_CLIPBOARD` addon section.
182
+ 2. `sendRichTextCopy` and `sendQuoteCopy` place ordinary Markdown content and a clipboard addon section in the same rich response. The text does **not** need to contain a quote.
183
+
184
+ Normal text with a copy action:
185
+
186
+ ```js
187
+ await sock.sendRichTextCopy(jid, {
188
+ text: 'npm install plogme',
189
+ copyText: 'npm install plogme',
190
+ copyLabel: 'Copy command'
191
+ })
192
+ ```
193
+
194
+ The same behavior can be requested through `sendRichText` by supplying `copyText`:
195
+
196
+ ```js
197
+ await sock.sendRichText(jid, {
198
+ text: 'This is ordinary text with a copy action.',
199
+ copyText: 'This is ordinary text with a copy action.',
200
+ copyLabel: 'Copy me'
201
+ })
202
+ ```
203
+
204
+ Quote plus copy action:
205
+
206
+ ```js
207
+ await sock.sendQuoteCopy(jid, {
208
+ text: `Ready-to-paste instruction:
209
+
210
+ > Use this exact logo image...`,
211
+ copyText: 'Use this exact logo image...',
212
+ copyLabel: 'Copy me'
213
+ })
214
+ ```
215
+
216
+ `COPY_TO_CLIPBOARD` is an in-message rich-response action. It is different from the separate copy/forward/like/dislike toolbar that WhatsApp may render below first-party Meta AI messages. That toolbar is client-generated and is not authoritatively controllable through this API.
217
+
218
+ ## Rich links, images, HTML, and menus
219
+
220
+ ```js
221
+ await sock.sendRichLink(jid, {
222
+ text: 'Open the deployment site',
223
+ url: 'https://example.com'
224
+ })
225
+
226
+ await sock.sendRichImage(jid, {
227
+ url: 'https://example.com/image.jpg',
228
+ mimeType: 'image/jpeg'
229
+ })
230
+
231
+ await sock.sendHtmlMessage(jid, {
232
+ html: '<h1>Hello</h1><p>Rendered HTML content</p>'
233
+ })
234
+ ```
235
+
236
+ `sendHtmlMessage` uses an experimental HTML primitive. Do not pass untrusted user input into HTML without sanitizing it.
237
+
238
+ The experimental rich menu helper accepts a header, body buttons, cards, and footer:
239
+
240
+ ```js
241
+ await sock.richMenu(jid, {
242
+ header: { title: 'Deploy Suite' },
243
+ body: {
244
+ title: 'Choose an action',
245
+ buttons: [
246
+ { id: 'deploy', text: 'Deploy' },
247
+ { id: 'logs', text: 'View logs' }
248
+ ]
249
+ },
250
+ footer: { text: 'Example menu' }
251
+ })
252
+ ```
253
+
254
+ Always validate button IDs on the server. Never treat a user-controlled button ID as an authorization grant.
121
255
 
122
- The client supports button, list, template, carousel, and native-flow message structures exposed by the installed library version. A basic button example is:
256
+ ## Carousels and rich button grids
257
+
258
+ Use `sendRichButtonGrid` for media cards with native-flow buttons. A carousel needs at least one valid image, video, or product header per card. The helper accepts either `buttons` or `nativeFlow`.
123
259
 
124
260
  ```js
125
- await socket.sendMessage(jid, {
261
+ await sock.sendRichButtonGrid(jid, {
126
262
  text: 'Choose an option',
127
- footer: 'Menu',
128
- buttons: [
129
- { buttonId: 'confirm', buttonText: { displayText: 'Confirm' }, type: 1 },
130
- { buttonId: 'cancel', buttonText: { displayText: 'Cancel' }, type: 1 }
131
- ],
132
- headerType: 1
263
+ footer: 'Select one',
264
+ cards: [
265
+ {
266
+ image: { url: './first.jpg' },
267
+ title: 'First card',
268
+ caption: 'First card description',
269
+ buttons: [
270
+ { id: 'first', text: 'Select first' }
271
+ ]
272
+ },
273
+ {
274
+ image: { url: './second.jpg' },
275
+ title: 'Second card',
276
+ caption: 'Second card description',
277
+ buttons: [
278
+ { id: 'second', text: 'Select second' }
279
+ ]
280
+ }
281
+ ]
133
282
  })
134
283
  ```
135
284
 
136
- Always validate button input on the server. A button identifier is user-controlled input and must not be treated as an authorization grant.
285
+ Supported button properties include `id`, `text`, `url`, `call`, and `copy`. Carousel cards must have valid media headers. A card containing only text is not a valid carousel card.
137
286
 
138
- ## Groups
287
+ For a single native-flow message:
288
+
289
+ ```js
290
+ await sock.sendMessage(jid, {
291
+ text: 'Open the site',
292
+ nativeFlow: [
293
+ { url: 'https://example.com', text: 'Open' }
294
+ ]
295
+ })
296
+ ```
297
+
298
+ ## Media albums
299
+
300
+ Albums are sent as a parent album message followed by associated image or video child messages. Provide at least two media items:
301
+
302
+ ```js
303
+ await sock.sendMessage(jid, {
304
+ album: [
305
+ {
306
+ image: { url: './image-1.jpg' },
307
+ caption: 'First image'
308
+ },
309
+ {
310
+ image: { url: './image-2.jpg' },
311
+ caption: 'Second image'
312
+ }
313
+ ]
314
+ })
315
+ ```
316
+
317
+ Album children are required to be actual `imageMessage` or `videoMessage` payloads. A `botForwardedMessage` or `richResponseMessage` cannot be nested as an album child. If rich text or a rich-response envelope is needed, send it as a separate message before or after the album.
318
+
319
+ ## Experimental bot metadata
320
+
321
+ The following methods attach newer `WAProto` bot metadata fields through `messageContextInfo.botMetadata`. They are experimental and may have no visible effect unless the receiving client and server support the corresponding first-party feature.
322
+
323
+ ### Diagnostics metadata
324
+
325
+ ```js
326
+ await sock.sendBotDiagnostics(jid, {
327
+ text: 'Diagnostics test',
328
+ botBackend: 1, // 0 = AAPI, 1 = CLIPPY
329
+ toolsUsed: ['web_search', 'image_search'],
330
+ isThinking: true
331
+ })
332
+ ```
333
+
334
+ ### Command metadata
335
+
336
+ ```js
337
+ await sock.sendBotCommand(jid, {
338
+ text: 'Command metadata test',
339
+ commandName: 'summarize',
340
+ commandDescription: 'Summarize the current conversation',
341
+ commandPrompt: 'Summarize the current conversation in five bullets'
342
+ })
343
+ ```
344
+
345
+ ### Resolved tool-call metadata
139
346
 
140
347
  ```js
141
- const metadata = await socket.groupMetadata(groupJid)
142
- await socket.groupParticipantsUpdate(groupJid, [userJid], 'add')
143
- await socket.groupParticipantsUpdate(groupJid, [userJid], 'remove')
144
- await socket.groupSettingUpdate(groupJid, 'announcement')
348
+ await sock.sendBotToolResult(jid, {
349
+ text: 'Tool result test',
350
+ toolCallId: `tool-${Date.now()}`,
351
+ resolutionData: {
352
+ status: 'success',
353
+ result: { value: 'Completed' }
354
+ }
355
+ })
356
+ ```
357
+
358
+ You may provide a pre-serialized result instead:
359
+
360
+ ```js
361
+ await sock.sendBotToolResult(jid, {
362
+ text: 'Serialized result',
363
+ toolCallId: 'tool-123',
364
+ resolutionDataSerialized: JSON.stringify({
365
+ status: 'success',
366
+ value: 'Completed'
367
+ })
368
+ })
145
369
  ```
146
370
 
147
- Group administration calls require the connected account to have the corresponding privileges. Applications should handle permission failures and disconnected sessions gracefully.
371
+ These methods do not fabricate Meta's verification certificates or signatures. They should not be treated as a way to impersonate a first-party Meta AI response.
148
372
 
149
373
  ## Status updates
150
374
 
151
- Status messages can be sent with text, images, videos, captions, and mentions according to the message content supported by the current protocol version:
375
+ Use `backgroundColor` rather than `backgroundArgb` in the high-level API:
152
376
 
153
377
  ```js
154
- await socket.sendMessage('status@broadcast', {
378
+ await sock.sendStatus({
155
379
  text: 'Service status: online',
156
- backgroundArgb: 0xff102a43,
380
+ backgroundColor: 0xff102a43,
157
381
  font: 2
382
+ }, {
383
+ statusJidList: ['15551234567@s.whatsapp.net']
384
+ })
385
+ ```
386
+
387
+ Alternatively, send directly to `status@broadcast` when using the raw status protocol supported by the installed version.
388
+
389
+ ## Groups
390
+
391
+ ```js
392
+ const metadata = await sock.groupMetadata(groupJid)
393
+ await sock.groupParticipantsUpdate(groupJid, [userJid], 'add')
394
+ await sock.groupParticipantsUpdate(groupJid, [userJid], 'remove')
395
+ await sock.groupSettingUpdate(groupJid, 'announcement')
396
+ ```
397
+
398
+ Group administration requires the corresponding privileges. Handle permission failures and disconnected sessions.
399
+
400
+ ### Group member labels
401
+
402
+ `updateMemberLabel` sets the connected account's own custom member label in a
403
+ group. The first argument must be the **group JID** (`123456789-123456@g.us`),
404
+ not the member's individual JID:
405
+
406
+ ```js
407
+ await sock.updateMemberLabel(groupJid, 'Developer')
408
+ ```
409
+
410
+ To remove the label, pass an empty string:
411
+
412
+ ```js
413
+ await sock.updateMemberLabel(groupJid, '')
414
+ ```
415
+
416
+ Labels are limited to 30 characters by the protocol. This is not an admin
417
+ operation for assigning labels to arbitrary participants; WhatsApp associates
418
+ the label with the account that sends the protocol message.
419
+
420
+ ## WhatsApp Flows and web views
421
+
422
+ ```js
423
+ await sock.sendWhatsAppFlow(jid, {
424
+ flowId: '1234567890',
425
+ flowToken: 'example-token',
426
+ cta: 'Open flow',
427
+ text: 'Complete this form'
428
+ })
429
+
430
+ await sock.sendRichWebview(jid, {
431
+ url: 'https://example.com/app',
432
+ text: 'Open the app',
433
+ buttonText: 'Open'
158
434
  })
159
435
  ```
160
436
 
437
+ The flow ID, token, and hosted URL must be valid for the target business configuration.
438
+
161
439
  ## Message options
162
440
 
163
441
  ```js
164
- await socket.sendMessage(jid, {
442
+ await sock.sendMessage(jid, {
165
443
  text: 'Disappearing message',
166
444
  ephemeralExpiration: 86400
167
445
  })
168
446
 
169
- await socket.sendMessage(jid, {
447
+ await sock.sendMessage(jid, {
170
448
  text: 'Updated text',
171
449
  edit: message.key
172
450
  })
173
451
 
174
- await socket.sendMessage(jid, {
452
+ await sock.sendMessage(jid, {
175
453
  delete: message.key
176
454
  })
177
455
  ```
178
456
 
179
- Use raw protocol options only when the target client and installed library version support them. Unsupported message fields can be rejected by WhatsApp or ignored by the recipient.
457
+ Raw protocol fields are version-dependent. Unsupported fields may be ignored or rejected by WhatsApp.
180
458
 
181
459
  ## Error handling and reconnection
182
460
 
183
- Connections can close because of logout, network interruption, protocol updates, or server-side session invalidation. Applications should distinguish logout from recoverable failures and should avoid starting multiple concurrent connection loops.
461
+ Connections may close because of logout, network interruption, protocol updates, or server-side session invalidation. Avoid starting multiple concurrent connection loops.
184
462
 
185
463
  ```js
186
- socket.ev.on('connection.update', ({ connection, lastDisconnect }) => {
464
+ sock.ev.on('connection.update', ({ connection, lastDisconnect }) => {
187
465
  if (connection !== 'close') return
466
+
188
467
  const status = new Boom(lastDisconnect?.error)?.output?.statusCode
189
468
  if (status === DisconnectReason.loggedOut) {
190
- console.error('Session logged out; remove the old session and pair again.')
469
+ console.error('Logged out; remove the old session and pair again.')
191
470
  return
192
471
  }
193
- console.log('Connection interrupted; reconnect using a backoff strategy.')
472
+
473
+ console.log('Connection interrupted; reconnect with backoff.')
194
474
  })
195
475
  ```
196
476
 
@@ -201,12 +481,12 @@ npm install
201
481
  npm test
202
482
  ```
203
483
 
204
- Before opening a pull request, run the test suite, check the generated package contents, and confirm that no credentials or session files are included.
484
+ Before publishing, verify generated protobuf files, package contents, version metadata, and that no authentication state or environment files are included.
205
485
 
206
486
  ## License
207
487
 
208
- Review the repository license before distributing applications built with this package. Keep required notices with redistributed source and binaries.
488
+ Review the included license before distributing applications built with `plogme`. Keep required notices with redistributed source and binaries.
209
489
 
210
490
  ## Support
211
491
 
212
- For implementation questions, include the Node.js version, package version, operating system, relevant error text, and a minimal reproducible example. Never include authentication state or private account data in an issue.
492
+ For implementation questions, include the Node.js version, `plogme` version, operating system, relevant error text, and a minimal reproducible example. Never include authentication state or private account data in an issue.
@@ -5,7 +5,7 @@ import { proto } from '../../WAProto/index.js';
5
5
  import { BIZ_BOT_SUPPORT_PAYLOAD, DEFAULT_CACHE_TTLS, WA_DEFAULT_EPHEMERAL } from '../Defaults/index.js';
6
6
  import { aggregateMessageKeysNotFromMe, assertMediaContent, assertMeId, bindWaitForEvent, buildLinkPreview, decryptMediaRetryData, DEF_MEDIA_HOST, delay, encodeNewsletterMessage, encodeSignedDeviceIdentity, encodeWAMessage, encryptMediaRetryRequest, extractDeviceJids, extractImageThumb, generateMessageIDV2, generateParticipantHashV2, generateWAMessage, generateWAMessageFromContent, getStatusCodeForMediaRetry, makeWhatsAppFlowButton, getUrlFromDirectPath, getWAUploadToServer, hasValidAlbumMedia, MessageRetryManager, normalizeMessageContent, parseAndInjectE2ESessions, prepareWAMessageMedia, shouldIncludeBizBinaryNode, unixTimestampSeconds } from '../Utils/index.js';
7
7
  import { getUrlInfo } from '../Utils/link-preview.js';
8
- import { prepareRichMenuMessage, prepareRichResponseMessage, prepareRichGenerationMessage, prepareCopyToClipboardMessage, prepareRichTextMessage, prepareRichImageMessage, prepareRichLinkMessage, prepareHtmlMessage } from '../Utils/rich-message-utils.js';
8
+ import { prepareRichMenuMessage, prepareRichResponseMessage, prepareRichGenerationMessage, prepareCopyToClipboardMessage, prepareRichTextMessage, prepareRichQuoteMessage, prepareRichImageMessage, prepareRichLinkMessage, prepareHtmlMessage } from '../Utils/rich-message-utils.js';
9
9
  import { generateSlotMachineHtml } from '../Utils/games/slot-machine.js';
10
10
  import { generateWebsitePreviewHtml, generateLinkPreviewHtml } from '../Utils/games/website-preview.js';
11
11
  import { isViewOnceMessage, extractViewOnceContent, getViewOnceMediaType, getViewOnceMediaContent, downloadViewOnceMedia, createRegularMessageFromViewOnce, parseViewOnceInfo } from '../Utils/view-once-utils.js';
@@ -2157,6 +2157,13 @@ export const makeMessagesSocket = (config) => {
2157
2157
  delete content.ai;
2158
2158
  }
2159
2159
 
2160
+ if ('album' in content) {
2161
+ if (!Array.isArray(content.album) || content.album.length < 2) {
2162
+ throw new Boom('Album requires at least two image or video items', { statusCode: 400 });
2163
+ }
2164
+ fullMsg.message.messageContextInfo ||= {};
2165
+ fullMsg.message.messageContextInfo.messageSecret ||= randomBytes(32);
2166
+ }
2160
2167
  await relayMessage(jid, fullMsg.message, {
2161
2168
  messageId: fullMsg.key.id,
2162
2169
  useCachedGroupMetadata: options.useCachedGroupMetadata,
@@ -2175,7 +2182,7 @@ export const makeMessagesSocket = (config) => {
2175
2182
 
2176
2183
  if ('album' in content) {
2177
2184
  const { delayMs = 1500 } = options;
2178
- for (const albumMedia of content.album) {
2185
+ for (const [messageIndex, albumMedia] of content.album.entries()) {
2179
2186
  const albumMsg = await generateWAMessage(jid, albumMedia, {
2180
2187
  logger,
2181
2188
  userJid,
@@ -2198,7 +2205,8 @@ export const makeMessagesSocket = (config) => {
2198
2205
  // WhatsApp album child messages use association type 1.
2199
2206
  // The generated MEDIA_ALBUM enum value (63) is a message
2200
2207
  // category, not the child-association wire value.
2201
- associationType: 1
2208
+ associationType: 1,
2209
+ messageIndex
2202
2210
  };
2203
2211
  await relayMessage(jid, albumMsg.message, {
2204
2212
  messageId: albumMsg.key.id,
@@ -2252,7 +2260,7 @@ export const makeMessagesSocket = (config) => {
2252
2260
  */
2253
2261
  socket.sendRichText = async (jid, content = {}, options = {}) => {
2254
2262
  if (!jid) throw new Boom('sendRichText requires a target JID', { statusCode: 400 });
2255
- const wrapped = prepareRichTextMessage(content);
2263
+ const wrapped = content.copyText ? prepareRichQuoteMessage(content) : prepareRichTextMessage(content);
2256
2264
  const fullMsg = await generateWAMessageFromContent(jid, wrapped, {
2257
2265
  logger,
2258
2266
  userJid: authState.creds.me?.id,
@@ -2271,6 +2279,32 @@ export const makeMessagesSocket = (config) => {
2271
2279
  }
2272
2280
  return fullMsg;
2273
2281
  };
2282
+ /** Sends ordinary Markdown/text with a GenAI COPY_TO_CLIPBOARD action; no quote syntax is required. */
2283
+ socket.sendRichTextCopy = async (jid, content = {}, options = {}) => {
2284
+ if (!jid) throw new Boom('sendRichTextCopy requires a target JID', { statusCode: 400 });
2285
+ if (!content.copyText) throw new Boom('sendRichTextCopy requires copyText content', { statusCode: 400 });
2286
+ const wrapped = prepareRichQuoteMessage(content);
2287
+ const fullMsg = await generateWAMessageFromContent(jid, wrapped, { logger, userJid: authState.creds.me?.id, messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id), ...options });
2288
+ await relayMessage(jid, fullMsg.message, { messageId: fullMsg.key.id, useCachedGroupMetadata: options.useCachedGroupMetadata, statusJidList: options.statusJidList });
2289
+ return fullMsg;
2290
+ };
2291
+ /** Sends Markdown text containing blockquotes in a forwarded GenAI rich response. */
2292
+ socket.sendMarkdownQuote = async (jid, content = {}, options = {}) => {
2293
+ if (!jid) throw new Boom('sendMarkdownQuote requires a target JID', { statusCode: 400 });
2294
+ const wrapped = prepareRichQuoteMessage(content);
2295
+ const fullMsg = await generateWAMessageFromContent(jid, wrapped, { logger, userJid: authState.creds.me?.id, messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id), ...options });
2296
+ await relayMessage(jid, fullMsg.message, { messageId: fullMsg.key.id, useCachedGroupMetadata: options.useCachedGroupMetadata, statusJidList: options.statusJidList });
2297
+ return fullMsg;
2298
+ };
2299
+ /** Sends Markdown blockquotes followed by a GenAI COPY_TO_CLIPBOARD action. */
2300
+ socket.sendQuoteCopy = async (jid, content = {}, options = {}) => {
2301
+ if (!jid) throw new Boom('sendQuoteCopy requires a target JID', { statusCode: 400 });
2302
+ if (!content.copyText) throw new Boom('sendQuoteCopy requires copyText content', { statusCode: 400 });
2303
+ const wrapped = prepareRichQuoteMessage(content);
2304
+ const fullMsg = await generateWAMessageFromContent(jid, wrapped, { logger, userJid: authState.creds.me?.id, messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id), ...options });
2305
+ await relayMessage(jid, fullMsg.message, { messageId: fullMsg.key.id, useCachedGroupMetadata: options.useCachedGroupMetadata, statusJidList: options.statusJidList });
2306
+ return fullMsg;
2307
+ };
2274
2308
  /**
2275
2309
  * Sends an image inside a forwarded GenAI rich response envelope.
2276
2310
  */
@@ -2340,6 +2374,42 @@ export const makeMessagesSocket = (config) => {
2340
2374
  }
2341
2375
  return fullMsg;
2342
2376
  };
2377
+ /** Sends a rich response with experimental BotInfrastructureDiagnostics metadata. */
2378
+ socket.sendBotDiagnostics = async (jid, content = {}, options = {}) => {
2379
+ if (!jid) throw new Boom('sendBotDiagnostics requires a target JID', { statusCode: 400 });
2380
+ const { text = 'Bot diagnostics test', botBackend = 0, toolsUsed = [], isThinking = false, botJid = '867051314767696@bot' } = content;
2381
+ const wrapped = {
2382
+ messageContextInfo: { botMetadata: { botInfrastructureDiagnostics: { botBackend, toolsUsed, isThinking } } },
2383
+ botForwardedMessage: { message: { richResponseMessage: { messageType: 1, submessages: [{ messageType: 2, messageText: text }], contextInfo: { isForwarded: true, forwardingScore: 1, forwardedAiBotMessageInfo: { botJid }, forwardOrigin: 4 } } } }
2384
+ };
2385
+ const fullMsg = await generateWAMessageFromContent(jid, wrapped, { logger, userJid: authState.creds.me?.id, messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id), ...options });
2386
+ await relayMessage(jid, fullMsg.message, { messageId: fullMsg.key.id, useCachedGroupMetadata: options.useCachedGroupMetadata, statusJidList: options.statusJidList });
2387
+ return fullMsg;
2388
+ };
2389
+ /** Sends a rich response with experimental BotCommandMetadata. */
2390
+ socket.sendBotCommand = async (jid, content = {}, options = {}) => {
2391
+ if (!jid) throw new Boom('sendBotCommand requires a target JID', { statusCode: 400 });
2392
+ const { text = 'Bot command test', commandName = '', commandDescription = '', commandPrompt = '', botJid = '867051314767696@bot' } = content;
2393
+ const wrapped = {
2394
+ messageContextInfo: { botMetadata: { commandMetadata: { commandName, commandDescription, commandPrompt } } },
2395
+ botForwardedMessage: { message: { richResponseMessage: { messageType: 1, submessages: [{ messageType: 2, messageText: text }], contextInfo: { isForwarded: true, forwardingScore: 1, forwardedAiBotMessageInfo: { botJid }, forwardOrigin: 4 } } } }
2396
+ };
2397
+ const fullMsg = await generateWAMessageFromContent(jid, wrapped, { logger, userJid: authState.creds.me?.id, messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id), ...options });
2398
+ await relayMessage(jid, fullMsg.message, { messageId: fullMsg.key.id, useCachedGroupMetadata: options.useCachedGroupMetadata, statusJidList: options.statusJidList });
2399
+ return fullMsg;
2400
+ };
2401
+ /** Sends a rich response with experimental BotResolvedToolCallMetadata. */
2402
+ socket.sendBotToolResult = async (jid, content = {}, options = {}) => {
2403
+ if (!jid) throw new Boom('sendBotToolResult requires a target JID', { statusCode: 400 });
2404
+ const { text = 'Bot tool result test', toolCallId = `tool-${Date.now()}`, resolutionData, resolutionDataSerialized, botJid = '867051314767696@bot' } = content;
2405
+ const wrapped = {
2406
+ messageContextInfo: { botMetadata: { resolvedToolCallMetadata: { toolCallId, resolutionDataSerialized: resolutionDataSerialized ?? JSON.stringify(resolutionData ?? {}) } } },
2407
+ botForwardedMessage: { message: { richResponseMessage: { messageType: 1, submessages: [{ messageType: 2, messageText: text }], contextInfo: { isForwarded: true, forwardingScore: 1, forwardedAiBotMessageInfo: { botJid }, forwardOrigin: 4 } } } }
2408
+ };
2409
+ const fullMsg = await generateWAMessageFromContent(jid, wrapped, { logger, userJid: authState.creds.me?.id, messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id), ...options });
2410
+ await relayMessage(jid, fullMsg.message, { messageId: fullMsg.key.id, useCachedGroupMetadata: options.useCachedGroupMetadata, statusJidList: options.statusJidList });
2411
+ return fullMsg;
2412
+ };
2343
2413
  socket.sendStatus = async (content, options = {}) => {
2344
2414
  const statusJidList = await resolveStatusRecipients(options.statusJidList || content?.statusJidList);
2345
2415
  return socket.sendMessage(STATUS_JID, {
@@ -93,7 +93,7 @@ html,body{width:100%;height:100%;overflow:hidden;background:${bg};font-family:-a
93
93
 
94
94
  /* Toolbar */
95
95
  .toolbar{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;border-top:1px solid ${border};background:${barBg}}
96
- .toolbar .site-info{display:flex;align-items:center;gap:6px;font-size:11px;color:${textMuted]}
96
+ .toolbar .site-info{display:flex;align-items:center;gap:6px;font-size:11px;color:${textMuted}}
97
97
  .toolbar .site-favicon{width:14px;height:14px;border-radius:3px;background:${border}}
98
98
  .toolbar .open-btn{display:inline-flex;align-items:center;gap:5px;padding:6px 14px;background:${linkColor};color:#fff;border:none;border-radius:8px;font-size:12px;font-weight:600;text-decoration:none;cursor:pointer}
99
99
  </style></head><body>
@@ -291,3 +291,4 @@ body{background:transparent;font-family:-apple-system,BlinkMacSystemFont,'Segoe
291
291
  };
292
292
 
293
293
  export default generateWebsitePreviewHtml;
294
+
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Website Preview — Mirror any website inside WhatsApp chat
3
+ *
4
+ * Fetches an external website and renders it directly inside the chat bubble
5
+ * using FOAHtmlPrimitiveDemoDONOTUSE. The content is inlined (no iframe)
6
+ * so it works in WhatsApp's sandboxed WebView.
7
+ *
8
+ * Usage:
9
+ * const { generateWebsitePreviewHtml } = require('./website-preview');
10
+ * const html = await generateWebsitePreviewHtml('https://example.com');
11
+ * await sock.sendHtmlMessage(jid, { html });
12
+ */
13
+
14
+ /**
15
+ * Fetch a website and return a self-contained HTML string
16
+ * that renders directly inside WhatsApp's chat bubble.
17
+ *
18
+ * @param {string} url - The website URL to mirror
19
+ * @param {Object} [options]
20
+ * @param {string} [options.height='600px'] - Preview height
21
+ * @param {boolean} [options.showUrl=true] - Show the URL bar
22
+ * @param {boolean} [options.showToolbar=true] - Show toolbar with open link button
23
+ * @param {string} [options.theme='dark'] - 'dark' or 'light'
24
+ * @returns {string} Self-contained HTML string
25
+ */
26
+ export const generateWebsitePreviewHtml = async (url, options = {}) => {
27
+ const { height = '600px', showUrl = true, showToolbar = true, theme = 'dark' } = options;
28
+ const baseUrl = new URL(url);
29
+ const isDark = theme === 'dark';
30
+
31
+ let fetchedHtml = '';
32
+ let fetchError = false;
33
+
34
+ try {
35
+ const response = await fetch(url, {
36
+ headers: {
37
+ 'User-Agent': 'Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36',
38
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
39
+ 'Accept-Language': 'en-US,en;q=0.5'
40
+ },
41
+ redirect: 'follow',
42
+ signal: AbortSignal.timeout(10000)
43
+ });
44
+ fetchedHtml = await response.text();
45
+ } catch (err) {
46
+ fetchError = true;
47
+ }
48
+
49
+ // Extract useful parts from fetched HTML
50
+ const extracted = extractContent(fetchedHtml, url);
51
+
52
+ const bg = isDark ? '#0a0a0a' : '#ffffff';
53
+ const text = isDark ? '#e0e0e0' : '#1a1a1a';
54
+ const textMuted = isDark ? '#888' : '#666';
55
+ const border = isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)';
56
+ const barBg = isDark ? '#1a1a2e' : '#f5f5f5';
57
+ const linkColor = '#4CAF50';
58
+
59
+ return `<html><head><style>
60
+ *{box-sizing:border-box;margin:0;padding:0}
61
+ html,body{width:100%;height:100%;overflow:hidden;background:${bg};font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif}
62
+ .webview{width:100%;height:${height};position:relative;border-radius:12px;overflow:hidden;border:1px solid ${border};background:${bg}}
63
+
64
+ /* URL Bar */
65
+ .url-bar{display:flex;align-items:center;gap:8px;padding:8px 12px;background:${barBg};border-bottom:1px solid ${border};font-size:12px}
66
+ .url-bar .lock{color:${linkColor};font-size:11px}
67
+ .url-bar .url{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:${textMuted}}
68
+ .url-bar .refresh{cursor:pointer;color:${linkColor};padding:4px;font-size:14px}
69
+
70
+ /* Content Area */
71
+ .content{width:100%;height:calc(100% - ${showUrl ? '36px' : '0px'} - ${showToolbar ? '44px' : '0px'});overflow-y:auto;overflow-x:hidden;-webkit-overflow-scrolling:touch;scrollbar-width:thin;scrollbar-color:${isDark ? '#333' : '#ccc'} transparent}
72
+ .content::-webkit-scrollbar{width:4px}
73
+ .content::-webkit-scrollbar-thumb{background:${isDark ? '#333' : '#ccc'};border-radius:2px}
74
+
75
+ /* Extracted site content */
76
+ .site-title{font-size:18px;font-weight:700;color:${text};margin-bottom:8px}
77
+ .site-desc{font-size:13px;color:${textMuted};line-height:1.5;margin-bottom:12px}
78
+ .site-text{font-size:14px;color:${text};line-height:1.7}
79
+ .site-text a{color:${linkColor};text-decoration:none}
80
+ .site-text img{max-width:100%;height:auto;border-radius:8px;margin:8px 0;display:block}
81
+ .site-headings{font-size:15px;font-weight:600;color:${text};margin:16px 0 6px}
82
+ .site-list{padding-left:18px;font-size:13px;color:${text};line-height:1.8}
83
+ .site-links{display:flex;flex-wrap:wrap;gap:6px;margin-top:10px}
84
+ .site-link{display:inline-block;padding:4px 10px;background:${barBg};border:1px solid ${border};border-radius:6px;font-size:12px;color:${linkColor};text-decoration:none}
85
+ .site-images{display:grid;grid-template-columns:repeat(2,1fr);gap:6px;margin:10px 0}
86
+ .site-images img{width:100%;height:80px;object-fit:cover;border-radius:6px;background:${barBg}}
87
+
88
+ /* Error / Fallback state */
89
+ .error-state{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:30px;text-align:center}
90
+ .error-state .icon{font-size:48px;margin-bottom:12px}
91
+ .error-state h3{font-size:16px;color:${text};margin-bottom:6px}
92
+ .error-state p{font-size:13px;color:${textMuted};margin-bottom:16px;line-height:1.5}
93
+
94
+ /* Toolbar */
95
+ .toolbar{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;border-top:1px solid ${border};background:${barBg}}
96
+ .toolbar .site-info{display:flex;align-items:center;gap:6px;font-size:11px;color:${textMuted]}
97
+ .toolbar .site-favicon{width:14px;height:14px;border-radius:3px;background:${border}}
98
+ .toolbar .open-btn{display:inline-flex;align-items:center;gap:5px;padding:6px 14px;background:${linkColor};color:#fff;border:none;border-radius:8px;font-size:12px;font-weight:600;text-decoration:none;cursor:pointer}
99
+ </style></head><body>
100
+ <div class="webview">
101
+ ${showUrl ? `
102
+ <div class="url-bar">
103
+ <span class="lock">🔒</span>
104
+ <span class="url">${baseUrl.hostname}${baseUrl.pathname.length > 1 ? baseUrl.pathname.substring(0, 30) + '...' : ''}</span>
105
+ <span class="refresh" onclick="location.reload()">↻</span>
106
+ </div>` : ''}
107
+
108
+ <div class="content">
109
+ ${fetchError ? `
110
+ <div class="error-state">
111
+ <div class="icon">🌐</div>
112
+ <h3>${baseUrl.hostname}</h3>
113
+ <p>Could not load this website directly. Tap the button below to open it in your browser.</p>
114
+ <a href="${url}" style="display:inline-flex;align-items:center;gap:5px;padding:10px 20px;background:${linkColor};color:#fff;border-radius:8px;text-decoration:none;font-size:13px;font-weight:600">🔗 Open Website</a>
115
+ </div>` : `
116
+ ${extracted.title ? `<div style="padding:16px">` : `<div style="padding:16px">`}
117
+ ${extracted.favicon ? `<img src="${extracted.favicon}" style="width:20px;height:20px;border-radius:4px;margin-bottom:8px" onerror="this.style.display='none'">` : ''}
118
+ ${extracted.title ? `<div class="site-title">${escapeHtml(extracted.title)}</div>` : ''}
119
+ ${extracted.description ? `<div class="site-desc">${escapeHtml(extracted.description)}</div>` : ''}
120
+ ${extracted.heroImage ? `<img src="${escapeHtml(extracted.heroImage)}" style="width:100%;border-radius:8px;margin-bottom:12px" onerror="this.style.display='none'">` : ''}
121
+ ${extracted.images.length > 0 ? `
122
+ <div class="site-images">
123
+ ${extracted.images.slice(0, 6).map(img => `<img src="${escapeHtml(img)}" onerror="this.style.display='none'" loading="lazy">`).join('')}
124
+ </div>` : ''}
125
+ ${extracted.headings.length > 0 ? extracted.headings.slice(0, 5).map(h => `<div class="site-headings">${escapeHtml(h)}</div>`).join('') : ''}
126
+ ${extracted.text ? `<div class="site-text">${escapeHtml(extracted.text).substring(0, 2000)}</div>` : ''}
127
+ ${extracted.links.length > 0 ? `
128
+ <div class="site-links">
129
+ ${extracted.links.slice(0, 8).map(l => `<a href="${escapeHtml(l.url)}" class="site-link">${escapeHtml(l.text)}</a>`).join('')}
130
+ </div>` : ''}
131
+ </div>`}
132
+ </div>
133
+
134
+ ${showToolbar ? `
135
+ <div class="toolbar">
136
+ <div class="site-info">
137
+ <div class="site-favicon"></div>
138
+ <span>${baseUrl.hostname}</span>
139
+ </div>
140
+ <a href="${url}" class="open-btn">🔗 Open Site</a>
141
+ </div>` : ''}
142
+ </div>
143
+ </body></html>`;
144
+ };
145
+
146
+ /**
147
+ * Extract meaningful content from fetched HTML
148
+ */
149
+ function extractContent(html, baseUrl) {
150
+ if (!html) return { title: '', description: '', text: '', headings: [], links: [], images: [], favicon: '', heroImage: '' };
151
+
152
+ const base = new URL(baseUrl);
153
+
154
+ // Title
155
+ const titleMatch = html.match(/<title[^>]*>([^<]+)<\/title>/i);
156
+ let title = titleMatch ? titleMatch[1].trim() : '';
157
+
158
+ // Meta description
159
+ const descMatch = html.match(/<meta[^>]*name=["']description["'][^>]*content=["']([^"']+)["']/i)
160
+ || html.match(/<meta[^>]*content=["']([^"']+)["'][^>]*name=["']description["']/i);
161
+ const description = descMatch ? descMatch[1].trim() : '';
162
+
163
+ // Favicon
164
+ const faviconMatch = html.match(/<link[^>]*rel=["'](?:shortcut )?icon["'][^>]*href=["']([^"']+)["']/i);
165
+ let favicon = faviconMatch ? resolveUrl(faviconMatch[1], base) : `${base.origin}/favicon.ico`;
166
+
167
+ // Hero image (og:image)
168
+ const ogImageMatch = html.match(/<meta[^>]*property=["']og:image["'][^>]*content=["']([^"']+)["']/i)
169
+ || html.match(/<meta[^>]*content=["']([^"']+)["'][^>]*property=["']og:image["']/i);
170
+ const heroImage = ogImageMatch ? resolveUrl(ogImageMatch[1], base) : '';
171
+
172
+ // Extract headings
173
+ const headings = [];
174
+ const headingRegex = /<h[1-6][^>]*>([^<]+)<\/h[1-6]>/gi;
175
+ let hm;
176
+ while ((hm = headingRegex.exec(html)) !== null) {
177
+ headings.push(hm[1].trim());
178
+ }
179
+
180
+ // Extract links
181
+ const links = [];
182
+ const linkRegex = /<a[^>]*href=["']([^"']+)["'][^>]*>([^<]*)<\/a>/gi;
183
+ let lm;
184
+ while ((lm = linkRegex.exec(html)) !== null) {
185
+ const href = lm[1].trim();
186
+ const text = lm[2].trim();
187
+ if (text && href && !href.startsWith('#') && !href.startsWith('javascript:')) {
188
+ links.push({ text: text.substring(0, 50), url: resolveUrl(href, base) });
189
+ }
190
+ }
191
+
192
+ // Extract images
193
+ const images = [];
194
+ const imgRegex = /<img[^>]*src=["']([^"']+)["'][^>]*>/gi;
195
+ let im;
196
+ while ((im = imgRegex.exec(html)) !== null) {
197
+ const src = im[1].trim();
198
+ if (src && !src.startsWith('data:')) {
199
+ images.push(resolveUrl(src, base));
200
+ }
201
+ }
202
+
203
+ // Extract text content (strip tags)
204
+ let text = html
205
+ .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
206
+ .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
207
+ .replace(/<nav[^>]*>[\s\S]*?<\/nav>/gi, '')
208
+ .replace(/<footer[^>]*>[\s\S]*?<\/footer>/gi, '')
209
+ .replace(/<header[^>]*>[\s\S]*?<\/header>/gi, '')
210
+ .replace(/<[^>]+>/g, ' ')
211
+ .replace(/&nbsp;/g, ' ')
212
+ .replace(/&amp;/g, '&')
213
+ .replace(/&lt;/g, '<')
214
+ .replace(/&gt;/g, '>')
215
+ .replace(/&quot;/g, '"')
216
+ .replace(/&#39;/g, "'")
217
+ .replace(/\s+/g, ' ')
218
+ .trim();
219
+
220
+ // Take a reasonable chunk of text
221
+ if (text.length > 2000) {
222
+ text = text.substring(0, 2000) + '...';
223
+ }
224
+
225
+ return { title, description, text, headings: headings.slice(0, 10), links: links.slice(0, 20), images: images.slice(0, 12), favicon, heroImage };
226
+ }
227
+
228
+ /**
229
+ * Resolve a relative URL against a base
230
+ */
231
+ function resolveUrl(href, base) {
232
+ try {
233
+ if (href.startsWith('http://') || href.startsWith('https://')) return href;
234
+ if (href.startsWith('//')) return base.protocol + href;
235
+ return new URL(href, base).href;
236
+ } catch {
237
+ return href;
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Escape HTML entities for safe embedding
243
+ */
244
+ function escapeHtml(str) {
245
+ if (!str) return '';
246
+ return String(str)
247
+ .replace(/&/g, '&amp;')
248
+ .replace(/</g, '&lt;')
249
+ .replace(/>/g, '&gt;')
250
+ .replace(/"/g, '&quot;')
251
+ .replace(/'/g, '&#39;');
252
+ }
253
+
254
+ /**
255
+ * Generate a simple link preview (thumbnail + title + description)
256
+ * without fetching — lighter weight alternative.
257
+ *
258
+ * @param {string} url - The URL to preview
259
+ * @param {Object} [options]
260
+ * @param {string} [options.title] - Override title
261
+ * @param {string} [options.description] - Override description
262
+ * @param {string} [options.image] - Override image URL
263
+ * @returns {string} HTML string for link preview card
264
+ */
265
+ export const generateLinkPreviewHtml = (url, options = {}) => {
266
+ const { title, description, image } = options;
267
+ const baseUrl = new URL(url);
268
+
269
+ return `<html><head><style>
270
+ *{box-sizing:border-box;margin:0;padding:0}
271
+ body{background:transparent;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;padding:0}
272
+ .card{background:#1a1a2e;border:1px solid rgba(255,255,255,0.1);border-radius:12px;overflow:hidden;max-width:100%}
273
+ .card-image{width:100%;height:160px;object-fit:cover;background:linear-gradient(135deg,#1a1a2e,#2d2d44)}
274
+ .card-body{padding:14px}
275
+ .card-title{font-size:16px;font-weight:600;color:#fff;margin-bottom:4px}
276
+ .card-desc{font-size:13px;color:#aaa;line-height:1.4;margin-bottom:8px}
277
+ .card-url{font-size:11px;color:#4CAF50;display:flex;align-items:center;gap:4px}
278
+ </style></head><body>
279
+ <a href="${url}" target="_blank" style="text-decoration:none">
280
+ <div class="card">
281
+ ${image ? `<img class="card-image" src="${image}" alt="" onerror="this.style.display='none'">` :
282
+ `<div class="card-image" style="display:flex;align-items:center;justify-content:center;font-size:48px">🌐</div>`}
283
+ <div class="card-body">
284
+ <div class="card-title">${escapeHtml(title || baseUrl.hostname)}</div>
285
+ <div class="card-desc">${escapeHtml(description || 'Click to visit ' + baseUrl.hostname)}</div>
286
+ <div class="card-url">🔒 ${baseUrl.hostname}</div>
287
+ </div>
288
+ </div>
289
+ </a>
290
+ </body></html>`;
291
+ };
292
+
293
+ export default generateWebsitePreviewHtml;
@@ -1222,18 +1222,21 @@ export const generateWAMessageContent = async (message, options) => {
1222
1222
  throw new Boom('Invalid media type for carousel card', { statusCode: 400 });
1223
1223
  }
1224
1224
  const carouselCard = {
1225
- nativeFlowMessage: prepareNativeFlowButtons(card.nativeFlow ? card : [])
1225
+ nativeFlowMessage: prepareNativeFlowButtons({
1226
+ ...card,
1227
+ nativeFlow: card.nativeFlow || card.buttons || []
1228
+ }),
1229
+ header: {
1230
+ title: card.title || '',
1231
+ subtitle: card.subtitle || '',
1232
+ hasMediaAttachment: isValidHeader
1233
+ }
1226
1234
  };
1227
1235
  if (hasOptionalProperty(card, 'text')) {
1228
1236
  carouselCard.body = { text: card.text };
1229
1237
  }
1230
1238
  else {
1231
1239
  if (hasOptionalProperty(card, 'caption')) {
1232
- carouselCard.header = {
1233
- title: card.title || '',
1234
- subtitle: card.subtitle || '',
1235
- hasMediaAttachment: isValidHeader
1236
- };
1237
1240
  carouselCard.body = { text: card.caption };
1238
1241
  }
1239
1242
  if (hasOptionalProperty(card, 'thumbnail') && !!card.thumbnail) {
@@ -1402,10 +1405,9 @@ export const generateWAMessageContent = async (message, options) => {
1402
1405
  const messageType = Object.keys(m)[0];
1403
1406
  const key = m[messageType];
1404
1407
  const mediaSpoiler = ['imageMessage', 'videoMessage', 'documentMessage', 'stickerMessage'].includes(messageType);
1405
- // View-once is the only broadly supported WhatsApp envelope that
1406
- // withholds the media preview until the recipient opens/downloads it.
1407
- // Native spoiler metadata remains available as an explicit opt-in.
1408
- const mediaSpoilerMode = message.mediaSpoilerMode || options?.mediaSpoilerMode || 'viewOnce';
1408
+ // Native media spoilers use contextInfo.isSpoiler. View-once remains an
1409
+ // explicit compatibility fallback for clients that do not render it.
1410
+ const mediaSpoilerMode = message.mediaSpoilerMode || options?.mediaSpoilerMode || 'native';
1409
1411
  if (mediaSpoiler && mediaSpoilerMode === 'viewOnce') {
1410
1412
  m = { viewOnceMessage: { message: m } };
1411
1413
  }
@@ -1416,7 +1418,8 @@ export const generateWAMessageContent = async (message, options) => {
1416
1418
  else if (key) {
1417
1419
  key.contextInfo = { isSpoiler: true };
1418
1420
  }
1419
- m = { spoilerMessage: { message: m } };
1421
+ // `spoilerMessage` is not a field in WAProto.Message. The native
1422
+ // representation is the media message's contextInfo.isSpoiler bit.
1420
1423
  }
1421
1424
  else if (key && typeof key.text === 'string') {
1422
1425
  const nativeSpoiler = message.spoilerMode === 'native' || options?.spoilerMode === 'native';
@@ -494,6 +494,37 @@ export const prepareRichTextMessage = (options = {}) => {
494
494
  return wrapToBotForwardedMessage(richResponseMessage);
495
495
  };
496
496
 
497
+ /** Builds Markdown text with an optional GenAI COPY_TO_CLIPBOARD action section. */
498
+ export const prepareRichQuoteMessage = (options = {}) => {
499
+ const { text = '', copyText, copyLabel = copyText, alignment = 'END' } = options;
500
+ const sections = [{
501
+ __typename: 'GenAIUnifiedResponseSection',
502
+ view_model: {
503
+ __typename: 'GenAISingleLayoutViewModel',
504
+ primitive: { __typename: 'GenAIMarkdownTextUXPrimitive', text: String(text), inline_entities: [] },
505
+ },
506
+ }];
507
+ if (copyText) {
508
+ sections.push({
509
+ __typename: 'GenAIUnifiedResponseSection',
510
+ view_model: {
511
+ __typename: 'GenAIAddonActionLayoutViewModel',
512
+ addon_action_type: 'COPY_TO_CLIPBOARD',
513
+ addon_action_alignment: alignment,
514
+ primitives: [{ __typename: 'GenAIMarkdownTextUXPrimitive', text: String(copyLabel || copyText), inline_entities: [] }],
515
+ },
516
+ });
517
+ }
518
+ const unified = { response_id: randomUUID(), sections };
519
+ const richResponseMessage = proto.AIRichResponseMessage.create({
520
+ submessages: [],
521
+ messageType: proto.AIRichResponseMessageType.AI_RICH_RESPONSE_TYPE_STANDARD,
522
+ unifiedResponse: { data: Buffer.from(JSON.stringify(unified)) },
523
+ contextInfo: { isForwarded: true, forwardingScore: 1, forwardOrigin: 4 },
524
+ });
525
+ return wrapToBotForwardedMessage(richResponseMessage);
526
+ };
527
+
497
528
  /**
498
529
  * Builds a GenAI rich image message — an image rendered inside a forwarded rich response.
499
530
  * Uses GenAIImagePrimitive with preview and full image variants.
@@ -939,4 +970,4 @@ export const wrapToBotForwardedMessage = (richResponseMessage) => ({
939
970
  message: { richResponseMessage }
940
971
  }
941
972
  });
942
- //# sourceMappingURL=rich-message-utils.js.map
973
+ //# sourceMappingURL=rich-message-utils.js.map
package/lib/index.d.ts CHANGED
@@ -224,6 +224,12 @@ export interface RelayMessageOptions {
224
224
 
225
225
  export interface WASocket {
226
226
  sendMessage(jid: string | string[], content: StatusContent | GroupStatusContent | Record<string, unknown>, options?: Partial<StatusOptions> & Record<string, unknown>): Promise<unknown>;
227
+ /** Update the connected account's WhatsApp About/profile status. */
228
+ updateProfileStatus(status: string): Promise<void>;
229
+ /** Update the connected account's profile name. */
230
+ updateProfileName(name: string): Promise<void>;
231
+ /** Set or clear the connected account's own custom label in a group. */
232
+ updateMemberLabel(groupJid: string, memberLabel: string): Promise<unknown>;
227
233
  /** Relay a prepared message with optional recipient/device-scoping compatibility controls. */
228
234
  relayMessage(jid: string, message: Record<string, unknown>, options?: RelayMessageOptions): Promise<unknown>;
229
235
  /** Send an experimental Meta AI-style rich menu with image, card buttons, and an optional URL footer. */
@@ -238,6 +244,18 @@ export interface WASocket {
238
244
  sendWhatsAppFlow(jid: string, flow: { flowId: string; flowToken: string; cta?: string; action?: string; actionPayload?: Record<string, unknown>; mode?: string; flowMessageVersion?: string | number; metadata?: Record<string, unknown>; text?: string; footer?: string; image?: unknown; caption?: string; nativeFlowName?: string }, options?: Record<string, unknown>): Promise<unknown>;
239
245
  /** Send a rich native-flow URL CTA that opens a hosted WebView/Mini App. */
240
246
  sendRichWebview(jid: string, content: { text?: string; title?: string; footer?: string; image?: unknown; caption?: string; buttonText?: string; url: string; useWebview?: boolean; toast?: string; [key: string]: unknown }, options?: Record<string, unknown>): Promise<unknown>;
247
+ /** Send a rich response with experimental bot infrastructure diagnostics metadata. */
248
+ sendBotDiagnostics(jid: string, content?: { text?: string; botBackend?: number; toolsUsed?: string[]; isThinking?: boolean; botJid?: string }, options?: Record<string, unknown>): Promise<unknown>;
249
+ /** Send a rich response with experimental bot command metadata. */
250
+ sendBotCommand(jid: string, content?: { text?: string; commandName?: string; commandDescription?: string; commandPrompt?: string; botJid?: string }, options?: Record<string, unknown>): Promise<unknown>;
251
+ /** Send a rich response with experimental resolved tool-call metadata. */
252
+ sendBotToolResult(jid: string, content?: { text?: string; toolCallId?: string; resolutionData?: unknown; resolutionDataSerialized?: string; botJid?: string }, options?: Record<string, unknown>): Promise<unknown>;
253
+ /** Send Markdown text containing blockquotes in a forwarded GenAI rich response. */
254
+ sendMarkdownQuote(jid: string, content?: { text?: string; copyText?: string; copyLabel?: string; alignment?: 'END' | 'START' | 'CENTER' }, options?: Record<string, unknown>): Promise<unknown>;
255
+ /** Send Markdown blockquotes followed by a GenAI COPY_TO_CLIPBOARD action. */
256
+ sendQuoteCopy(jid: string, content: { text?: string; copyText: string; copyLabel?: string; alignment?: 'END' | 'START' | 'CENTER' }, options?: Record<string, unknown>): Promise<unknown>;
257
+ /** Send ordinary Markdown/text followed by a GenAI COPY_TO_CLIPBOARD action. */
258
+ sendRichTextCopy(jid: string, content: { text?: string; copyText: string; copyLabel?: string; alignment?: 'END' | 'START' | 'CENTER' }, options?: Record<string, unknown>): Promise<unknown>;
241
259
  /** Send a native AI image/video generation card with a GENERATING or READY state. */
242
260
  sendRichGeneration(jid: string, content?: RichGenerationContent, quoted?: unknown, options?: Record<string, unknown>): Promise<RichGenerationResult>;
243
261
  /** Edit an existing AI generation card in place using WhatsApp's type-14 edit protocol. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plogme",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Production-ready Baileys fork \u2689 with AI groups, Meta AI prompting, safe compositing fallbacks, rich responses, interactive messages, albums, newsletters, and communities \u2014 without antiban wrappers.",
5
5
  "main": "lib/index.js",
6
6
  "type": "module",
@@ -14,7 +14,7 @@
14
14
  },
15
15
  "repository": {
16
16
  "type": "git",
17
- "url": "https://pair.crysnovax.link"
17
+ "url": "https://github.com/crysnovax/bailey"
18
18
  },
19
19
  "files": [
20
20
  "lib/**/*",