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 +349 -69
- package/lib/Socket/messages-send.js +74 -4
- package/lib/Utils/games/website-preview.js +2 -1
- package/lib/Utils/games/website-preview.js[bug] +293 -0
- package/lib/Utils/messages.js +14 -11
- package/lib/Utils/rich-message-utils.js +32 -1
- package/lib/index.d.ts +18 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# plogme
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
> **
|
|
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
|
-
|
|
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
|
|
28
|
+
const sock = makeWASocket({
|
|
37
29
|
auth: state,
|
|
38
30
|
logger: pino({ level: 'silent' })
|
|
39
31
|
})
|
|
40
32
|
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
46
|
+
Keep authentication files private. Never commit session credentials, private keys, or `.env` files.
|
|
61
47
|
|
|
62
|
-
|
|
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
|
-
|
|
53
|
+
const sock = makeWASocket({ auth: state })
|
|
54
|
+
sock.ev.on('creds.update', saveCreds)
|
|
71
55
|
|
|
72
|
-
|
|
73
|
-
const code = await socket.requestPairingCode('15551234567')
|
|
56
|
+
const code = await sock.requestPairingCode('15551234567')
|
|
74
57
|
console.log(code)
|
|
75
58
|
```
|
|
76
59
|
|
|
77
|
-
|
|
60
|
+
Pairing codes require the socket to be initialized and a valid phone number in international format.
|
|
78
61
|
|
|
79
|
-
##
|
|
62
|
+
## Ordinary text, mentions, reactions, and media
|
|
80
63
|
|
|
81
64
|
```js
|
|
82
|
-
await
|
|
65
|
+
await sock.sendMessage(jid, { text: 'Hello!' })
|
|
83
66
|
|
|
84
|
-
await
|
|
67
|
+
await sock.sendMessage(jid, {
|
|
85
68
|
text: 'Hello @15551234567',
|
|
86
69
|
mentions: ['15551234567@s.whatsapp.net']
|
|
87
70
|
})
|
|
88
71
|
|
|
89
|
-
await
|
|
90
|
-
react: { text: '
|
|
72
|
+
await sock.sendMessage(jid, {
|
|
73
|
+
react: { text: '👍', key: message.key }
|
|
91
74
|
})
|
|
92
75
|
```
|
|
93
76
|
|
|
94
|
-
|
|
77
|
+
Media examples:
|
|
95
78
|
|
|
96
79
|
```js
|
|
97
|
-
await
|
|
80
|
+
await sock.sendMessage(jid, {
|
|
98
81
|
image: { url: './image.jpg' },
|
|
99
82
|
caption: 'Image caption'
|
|
100
83
|
})
|
|
101
84
|
|
|
102
|
-
await
|
|
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
|
|
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
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
|
261
|
+
await sock.sendRichButtonGrid(jid, {
|
|
126
262
|
text: 'Choose an option',
|
|
127
|
-
footer: '
|
|
128
|
-
|
|
129
|
-
{
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
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
|
-
|
|
375
|
+
Use `backgroundColor` rather than `backgroundArgb` in the high-level API:
|
|
152
376
|
|
|
153
377
|
```js
|
|
154
|
-
await
|
|
378
|
+
await sock.sendStatus({
|
|
155
379
|
text: 'Service status: online',
|
|
156
|
-
|
|
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
|
|
442
|
+
await sock.sendMessage(jid, {
|
|
165
443
|
text: 'Disappearing message',
|
|
166
444
|
ephemeralExpiration: 86400
|
|
167
445
|
})
|
|
168
446
|
|
|
169
|
-
await
|
|
447
|
+
await sock.sendMessage(jid, {
|
|
170
448
|
text: 'Updated text',
|
|
171
449
|
edit: message.key
|
|
172
450
|
})
|
|
173
451
|
|
|
174
|
-
await
|
|
452
|
+
await sock.sendMessage(jid, {
|
|
175
453
|
delete: message.key
|
|
176
454
|
})
|
|
177
455
|
```
|
|
178
456
|
|
|
179
|
-
|
|
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
|
|
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
|
-
|
|
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('
|
|
469
|
+
console.error('Logged out; remove the old session and pair again.')
|
|
191
470
|
return
|
|
192
471
|
}
|
|
193
|
-
|
|
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
|
|
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
|
|
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,
|
|
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(/ /g, ' ')
|
|
212
|
+
.replace(/&/g, '&')
|
|
213
|
+
.replace(/</g, '<')
|
|
214
|
+
.replace(/>/g, '>')
|
|
215
|
+
.replace(/"/g, '"')
|
|
216
|
+
.replace(/'/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, '&')
|
|
248
|
+
.replace(/</g, '<')
|
|
249
|
+
.replace(/>/g, '>')
|
|
250
|
+
.replace(/"/g, '"')
|
|
251
|
+
.replace(/'/g, ''');
|
|
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;
|
package/lib/Utils/messages.js
CHANGED
|
@@ -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(
|
|
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
|
-
//
|
|
1406
|
-
//
|
|
1407
|
-
|
|
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
|
-
|
|
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.
|
|
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://
|
|
17
|
+
"url": "https://github.com/crysnovax/bailey"
|
|
18
18
|
},
|
|
19
19
|
"files": [
|
|
20
20
|
"lib/**/*",
|