@rennzsync/baileys 10.4.0 → 10.5.0

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.
@@ -0,0 +1,210 @@
1
+ /**
2
+ * a2ui.js — widget A2UI (Agent-to-UI) lewat interactiveMessage.bloksWidget.
3
+ *
4
+ * Builder deklaratif (Text, Image, Video, Button, Card, Column, Row, Divider,
5
+ * CheckBox, TextField, ChoicePicker) + listCard, lalu dikirim sebagai
6
+ * interactiveMessage dengan field bloksWidget (proto field 8) dan node <biz>
7
+ * native_flow.
8
+ *
9
+ * PERINGATAN (sama seperti rich-*.js): format ini internal WhatsApp dan tidak
10
+ * dijamin dirender di semua client — relayMessage bisa sukses tanpa error
11
+ * walau widget tidak tampil di sisi penerima.
12
+ *
13
+ * Pakai:
14
+ * const ui = new A2UI()
15
+ * const t = ui.text('Halo', { variant: 'h1' })
16
+ * ui.root([t])
17
+ * await sock.sendA2UI(jid, { a2ui: ui, bodyText: 'Widget' })
18
+ * // atau: sendA2UIWidget(sock, jid, { a2ui: ui })
19
+ */
20
+ import { randomBytes, randomUUID } from 'crypto';
21
+
22
+ const DEFAULT_CATALOG_ID = 'https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json';
23
+
24
+ export class A2UI {
25
+ constructor({ catalogId = DEFAULT_CATALOG_ID, version = 'v0.9' } = {}) {
26
+ this._version = version;
27
+ this._catalogId = catalogId;
28
+ this._components = new Map();
29
+ this._counter = 0;
30
+ this._rootChildren = [];
31
+ }
32
+ #nextId(prefix) {
33
+ return `${prefix}_${(this._counter++).toString(36)}`;
34
+ }
35
+ #reg(id, component, extra = {}) {
36
+ id ??= this.#nextId(component.toLowerCase());
37
+ if (this._components.has(id))
38
+ throw new Error(`Component id "${id}" already used`);
39
+ this._components.set(id, { id, component, ...extra });
40
+ return id;
41
+ }
42
+ text(text, { id, variant = 'body' } = {}) {
43
+ return this.#reg(id, 'Text', { text, variant });
44
+ }
45
+ image(url, { id, variant, fit = 'cover' } = {}) {
46
+ return this.#reg(id, 'Image', { url, ...(variant ? { variant } : {}), fit });
47
+ }
48
+ video(url, { id } = {}) {
49
+ return this.#reg(id, 'Video', { url });
50
+ }
51
+ checkbox(label, { id, value = false } = {}) {
52
+ return this.#reg(id, 'CheckBox', { label, value });
53
+ }
54
+ textField(label, { id, variant = 'text' } = {}) {
55
+ return this.#reg(id, 'TextField', { label, variant });
56
+ }
57
+ button(childId, { id, variant = 'primary', action } = {}) {
58
+ if (!childId)
59
+ throw new TypeError('button(childId) requires the id of a child component (e.g. from .text())');
60
+ return this.#reg(id, 'Button', { child: childId, variant, ...(action ? { action } : {}) });
61
+ }
62
+ card(childId, { id } = {}) {
63
+ return this.#reg(id, 'Card', { child: childId });
64
+ }
65
+ column(children = [], { id, justify, align } = {}) {
66
+ if (!children.length)
67
+ throw new TypeError('column(children) requires at least one child id');
68
+ return this.#reg(id, 'Column', { children, ...(justify ? { justify } : {}), ...(align ? { align } : {}) });
69
+ }
70
+ row(children = [], { id } = {}) {
71
+ if (!children.length)
72
+ throw new TypeError('row(children) requires at least one child id');
73
+ return this.#reg(id, 'Row', { children });
74
+ }
75
+ divider({ id } = {}) {
76
+ return this.#reg(id, 'Divider', {});
77
+ }
78
+ choicePicker(label, options, { id, variant = 'mutuallyExclusive', value, displayStyle = 'checkbox', filterable = false } = {}) {
79
+ if (!Array.isArray(options) || !options.length) {
80
+ throw new TypeError('choicePicker(label, options) requires a non-empty options array of {label, value}');
81
+ }
82
+ return this.#reg(id, 'ChoicePicker', {
83
+ label,
84
+ variant,
85
+ ...(value !== undefined ? { value } : {}),
86
+ options,
87
+ displayStyle,
88
+ filterable
89
+ });
90
+ }
91
+ root(children) {
92
+ if (!Array.isArray(children) || !children.length) {
93
+ throw new TypeError('root(children) requires a non-empty array of top-level component ids');
94
+ }
95
+ this._rootChildren = children;
96
+ return this;
97
+ }
98
+ listCard({ title, items, fallbackText, uuid = randomUUID() } = {}) {
99
+ if (!title)
100
+ throw new TypeError('listCard requires a title');
101
+ if (!Array.isArray(items) || !items.length) {
102
+ throw new TypeError('listCard requires a non-empty items array');
103
+ }
104
+ this._listCardPayload = {
105
+ uuid,
106
+ data: JSON.stringify({
107
+ type: 'list_card',
108
+ title,
109
+ fallback_text: fallbackText ?? '',
110
+ items: items.map(it => ({
111
+ asset_id: it.assetId ?? randomUUID().replace(/-/g, '').slice(0, 17),
112
+ asset_type: it.assetType ?? 'PRODUCT_ITEM',
113
+ title: it.title,
114
+ trailing_label: it.price ?? it.trailingLabel ?? '',
115
+ trailing_emphasis: it.emphasis ?? 'strong'
116
+ }))
117
+ }),
118
+ type: 'im_a2ui',
119
+ fallback: fallbackText ?? ''
120
+ };
121
+ return this;
122
+ }
123
+ build({ uuid = randomUUID(), surfaceId, type = 'im_a2ui', wrapped = true } = {}) {
124
+ if (this._listCardPayload)
125
+ return this._listCardPayload;
126
+ if (!this._rootChildren.length)
127
+ throw new Error('Call root([...ids]) before build()');
128
+ const root = { id: 'root', component: 'Column', children: this._rootChildren };
129
+ const components = [root, ...this._components.values()];
130
+ const data = wrapped
131
+ ? {
132
+ version: this._version,
133
+ createSurface: {
134
+ surfaceId: surfaceId ?? `starcore-widget=${uuid}`,
135
+ catalogId: this._catalogId,
136
+ components
137
+ }
138
+ }
139
+ : { components };
140
+ return {
141
+ uuid,
142
+ data: JSON.stringify(data),
143
+ type
144
+ };
145
+ }
146
+ }
147
+
148
+ /** Ambil jid user dari sock / objek user / string (sama seperti normalizeUserJid di rich-classic.js). */
149
+ const resolveUserJid = (client) => client?.user?.id || client?.authState?.creds?.me?.id;
150
+
151
+ /**
152
+ * Kirim widget A2UI.
153
+ * @param {object} client socket (butuh relayMessage; opsional user.id)
154
+ * @param {string} jid
155
+ * @param {object} options { a2ui, bodyText, footer, buttons, contextInfo, expiration, quoted, type, wrapped, singleScreen }
156
+ */
157
+ export async function sendA2UIWidget(client, jid, { a2ui, bodyText = '', footer = '', buttons = [], contextInfo = {}, expiration, quoted, type = 'im_a2ui', wrapped = true, singleScreen = false } = {}) {
158
+ if (!client || typeof client.relayMessage !== 'function')
159
+ throw new Error('Socket is required');
160
+ if (!(a2ui instanceof A2UI))
161
+ throw new TypeError('a2ui must be an A2UI instance');
162
+ const { generateWAMessageFromContent } = await import('./messages.js');
163
+ const nativeFlowMessage = buttons && buttons.length
164
+ ? {
165
+ buttons: buttons.map(b => ({
166
+ name: b.name ?? 'cta_url',
167
+ buttonParamsJson: typeof b.params === 'string' ? b.params : JSON.stringify(b.params ?? {})
168
+ })),
169
+ messageParamsJson: '{}',
170
+ messageVersion: 1
171
+ }
172
+ : { messageParamsJson: '' };
173
+ const ctx = expiration || Object.keys(contextInfo).length
174
+ ? { contextInfo: { ...(expiration ? { expiration } : {}), ...contextInfo } }
175
+ : {};
176
+ const interactiveMessage = singleScreen
177
+ ? {
178
+ nativeFlowMessage,
179
+ bloksWidget: a2ui.build({ type, wrapped }),
180
+ ...ctx
181
+ }
182
+ : {
183
+ ...(footer !== undefined ? { header: { hasMediaAttachment: false } } : {}),
184
+ ...(bodyText !== undefined ? { body: { text: bodyText } } : {}),
185
+ ...(footer !== undefined ? { footer: { text: footer } } : {}),
186
+ nativeFlowMessage,
187
+ bloksWidget: a2ui.build({ type, wrapped }),
188
+ ...ctx
189
+ };
190
+ const msg = generateWAMessageFromContent(jid, {
191
+ messageContextInfo: { messageSecret: randomBytes(32) },
192
+ interactiveMessage
193
+ }, { quoted, userJid: resolveUserJid(client) });
194
+ await client.relayMessage(msg.key.remoteJid, msg.message, {
195
+ messageId: msg.key.id,
196
+ additionalNodes: [
197
+ {
198
+ tag: 'biz',
199
+ attrs: { actual_actors: '2', host_storage: '2', privacy_mode_ts: String(Math.floor(Date.now() / 1e3)) },
200
+ content: [
201
+ { tag: 'interactive', attrs: { type: 'native_flow', v: '1' }, content: [{ tag: 'native_flow', attrs: { v: '9', name: 'mixed' } }] },
202
+ { tag: 'quality_control', attrs: { decision_id: randomUUID().replace(/-/g, ''), source_type: 'third_party' }, content: [{ tag: 'decision_source', attrs: { value: 'df' } }] }
203
+ ]
204
+ }
205
+ ]
206
+ });
207
+ return msg;
208
+ }
209
+
210
+ export default { A2UI, sendA2UIWidget };
@@ -23,5 +23,7 @@ export * from './media-processor.js';
23
23
  export * from './rich-webui.js';
24
24
  export * from './rich-classic.js';
25
25
  export * from './rich-carousel.js';
26
+ export * from './rich-menu.js';
27
+ export * from './a2ui.js';
26
28
  export * from './use-sqlite-auth-state.js';
27
29
  //# sourceMappingURL=index.js.map
@@ -563,6 +563,39 @@ export const generateWAMessageContent = async (message, options) => {
563
563
  }
564
564
  };
565
565
  }
566
+ // viewOnceV2 / viewOnceV2Extension: bungkus pesan (termasuk teks) ke
567
+ // viewOnceMessageV2 / viewOnceMessageV2Extension. Untuk teks, flag viewOnce
568
+ // di dalam extendedTextMessage ikut di-set (sama seperti plugin `vo`).
569
+ const wantV2Ext = hasOptionalProperty(message, 'viewOnceV2Extension') && !!message.viewOnceV2Extension;
570
+ const wantV2 = hasOptionalProperty(message, 'viewOnceV2') && !!message.viewOnceV2;
571
+ if (wantV2Ext || wantV2) {
572
+ const innerType = Object.keys(m)[0];
573
+ const inner = m[innerType];
574
+ if (inner && typeof inner === 'object') {
575
+ if (innerType === 'extendedTextMessage') {
576
+ inner.viewOnce = true;
577
+ }
578
+ if (innerType === 'extendedTextMessage' || 'contextInfo' in inner) {
579
+ inner.contextInfo = inner.contextInfo || {};
580
+ if (inner.contextInfo.forwardingScore === undefined) {
581
+ inner.contextInfo.forwardingScore = 1;
582
+ }
583
+ if (inner.contextInfo.isForwarded === undefined) {
584
+ inner.contextInfo.isForwarded = true;
585
+ }
586
+ }
587
+ }
588
+ const messageContextInfo = m.messageContextInfo;
589
+ if (messageContextInfo) {
590
+ delete m.messageContextInfo;
591
+ }
592
+ m = wantV2Ext
593
+ ? { viewOnceMessageV2Extension: { message: m } }
594
+ : { viewOnceMessageV2: { message: m } };
595
+ if (messageContextInfo) {
596
+ m.messageContextInfo = messageContextInfo;
597
+ }
598
+ }
566
599
  if (shouldIncludeReportingToken(m)) {
567
600
  m.messageContextInfo = m.messageContextInfo || {};
568
601
  if (!m.messageContextInfo.messageSecret) {
@@ -0,0 +1,211 @@
1
+ /**
2
+ * rich-menu.js — menu "rich response" (tombol + kartu geser + header gambar +
3
+ * footer open-URL) lewat botForwardedMessage > richResponseMessage >
4
+ * unifiedResponse, porting dari @vansnowi/baileys (`richMenu`).
5
+ *
6
+ * PERINGATAN (sama seperti rich-webui.js / rich-carousel.js):
7
+ * unifiedResponse adalah format GenAI internal WhatsApp — relayMessage bisa
8
+ * sukses tanpa error walau pesan tidak dirender di client penerima, dan nama
9
+ * typename-nya bisa berubah antar-versi WA. Tombol di sini adalah CTA widget
10
+ * dengan toast, BUKAN quick-reply yang mengirim balik pesan ke bot.
11
+ *
12
+ * Content yang dikembalikan siap dipakai dengan
13
+ * generateWAMessageFromContent(jid, content, { userJid }) lalu
14
+ * relayMessage(jid, msg.message, { messageId: msg.key.id }).
15
+ * Atau langsung: sock.richMenu(jid, content).
16
+ */
17
+ import { randomBytes } from 'crypto';
18
+
19
+ const IMAGE_TTL_MS = 24 * 60 * 60 * 1000;
20
+ /** ForwardOrigin.META_AI di proto */
21
+ const FORWARD_ORIGIN_META_AI = 4;
22
+
23
+ const toolCallId = () => randomBytes(8).toString('hex');
24
+
25
+ const section = (viewModel) => ({
26
+ __typename: 'GenAIUnifiedResponseSection',
27
+ view_model: viewModel
28
+ });
29
+
30
+ const single = (primitive) => section({
31
+ __typename: 'GenAISingleLayoutViewModel',
32
+ primitive
33
+ });
34
+
35
+ /** Gambar inline: dirender lewat inline entity LaTeX 1 titik (trik agar gambar muncul di dalam teks). */
36
+ const inlineImagePrimitive = (image, { width, height, padding }) => ({
37
+ __typename: 'GenAIMarkdownTextUXPrimitive',
38
+ text: '{{header}}.{{/header}}',
39
+ inline_entities: [
40
+ {
41
+ __typename: 'GenAITextInlineEntity',
42
+ key: 'header',
43
+ metadata: {
44
+ __typename: 'GenAILatexItem',
45
+ latex_expression: '.',
46
+ font_height: 24,
47
+ padding,
48
+ latex_image: {
49
+ __typename: 'GenAIMediaItem',
50
+ mime_type: image.mime_type || 'image/png',
51
+ url: image.url,
52
+ url_fallback: image.url,
53
+ width: image.width || width,
54
+ height: image.height || height,
55
+ expiration_timestamp_ms: Date.now() + IMAGE_TTL_MS
56
+ }
57
+ }
58
+ }
59
+ ]
60
+ });
61
+
62
+ const normalizeButton = (btn, fallbackToast) => {
63
+ const label = typeof btn === 'string' ? btn : btn?.label ?? btn?.text ?? '';
64
+ const toast = typeof btn === 'object' && btn?.toast !== undefined ? btn.toast : fallbackToast;
65
+ return {
66
+ label: String(label),
67
+ state: 'PENDING',
68
+ kind: 'OTHER',
69
+ tool_call_id: toolCallId(),
70
+ toast: {
71
+ label: toast || '',
72
+ __typename: 'GenAI3PExtWidgetToast'
73
+ },
74
+ __typename: 'GenAI3PExtWidgetCTA'
75
+ };
76
+ };
77
+
78
+ const widgetPrimitive = (title, buttons, toast) => ({
79
+ __typename: 'GenAI3PExtWidgetPrimitive',
80
+ header: {
81
+ __typename: 'GenAI3PExtWidgetStandardHeader',
82
+ title: title || ''
83
+ },
84
+ body: {
85
+ __typename: 'GenAI3PExtCalendarEventList',
86
+ ctas: buttons.map(btn => normalizeButton(btn, toast)),
87
+ sections: []
88
+ }
89
+ });
90
+
91
+ /**
92
+ * Bangun content pesan rich menu (belum dikirim).
93
+ *
94
+ * @param {object} content
95
+ * @param {object} [content.header] { title, image: { url, inline, mime_type, width, height }, disclaimer, disclaimerText }
96
+ * @param {object} [content.body] { title, buttons: string[], toast } atau
97
+ * { carousel: true | row: true, cards: [{ title, buttons, toast }] }
98
+ * @param {object} [content.footer] { text, url, image: { url, mime_type, width, height } } — CTA open-URL hanya dibuat jika `url` diisi
99
+ * @param {object} [content.contextInfo] override contextInfo richResponseMessage
100
+ * @returns {object} content object (kompatibel proto.Message)
101
+ */
102
+ export function buildRichMenuMessage(content = {}) {
103
+ const { header, body, footer } = content;
104
+ const sections = [];
105
+ let messageContextInfo;
106
+ if (header) {
107
+ const { disclaimer = false, disclaimerText = ' ', image, title = '' } = header;
108
+ if (disclaimer) {
109
+ messageContextInfo = { botMetadata: { messageDisclaimerText: disclaimerText } };
110
+ }
111
+ if (title) {
112
+ sections.push(single({ __typename: 'FOATextPrimitive', text: '# ' + title }));
113
+ }
114
+ if (image?.url) {
115
+ if (image.inline) {
116
+ sections.push(single(inlineImagePrimitive(image, { width: 500, height: 500, padding: 4 })));
117
+ }
118
+ else {
119
+ sections.push(single({
120
+ __typename: 'GenAIImagePrimitive',
121
+ preview_image: {
122
+ __typename: 'GenAIMediaItem',
123
+ mime_type: image.mime_type || 'image/png',
124
+ url: image.url
125
+ },
126
+ full_image: {
127
+ __typename: 'GenAIMediaItem',
128
+ mime_type: image.mime_type || 'image/png',
129
+ url: image.url
130
+ }
131
+ }));
132
+ }
133
+ }
134
+ }
135
+ if (body) {
136
+ const { cards, buttons, title = '', toast = '', carousel = false, row = false } = body;
137
+ if (carousel || row) {
138
+ if (cards?.length) {
139
+ sections.push(section({
140
+ __typename: carousel ? 'GenAIHScrollLayoutViewModel' : 'GenAIActionRowLayoutViewModel',
141
+ primitives: cards.map(card => widgetPrimitive(card?.title, card?.buttons || [], card?.toast))
142
+ }));
143
+ }
144
+ }
145
+ else if (buttons?.length) {
146
+ sections.push(single(widgetPrimitive(title, buttons, toast)));
147
+ }
148
+ }
149
+ if (footer) {
150
+ const { text = '', url = '', image } = footer;
151
+ const primitives = [];
152
+ if (url) {
153
+ primitives.push({
154
+ __typename: 'GenAIFooterActionPrimitive',
155
+ cta_text: text || 'Open',
156
+ cta_type: 'OPEN_URL',
157
+ cta_url: url
158
+ });
159
+ }
160
+ if (image?.url) {
161
+ primitives.push(inlineImagePrimitive(image, { width: 100, height: 100, padding: -5 }));
162
+ }
163
+ if (primitives.length) {
164
+ sections.push({
165
+ view_model: {
166
+ __typename: 'GenAIActionRowLayoutViewModel',
167
+ primitives
168
+ }
169
+ });
170
+ }
171
+ }
172
+ if (!sections.length) {
173
+ throw new TypeError('richMenu: header/body/footer kosong — isi minimal satu (title, image, buttons/cards, atau footer.url)');
174
+ }
175
+ return {
176
+ ...(messageContextInfo ? { messageContextInfo } : {}),
177
+ botForwardedMessage: {
178
+ message: {
179
+ richResponseMessage: {
180
+ unifiedResponse: {
181
+ data: Buffer.from(JSON.stringify({ sections })).toString('base64')
182
+ },
183
+ contextInfo: {
184
+ isForwarded: true,
185
+ forwardOrigin: FORWARD_ORIGIN_META_AI,
186
+ ...(content.contextInfo ?? {})
187
+ }
188
+ }
189
+ }
190
+ }
191
+ };
192
+ }
193
+
194
+ /**
195
+ * Bangun + kirim rich menu lewat socket.
196
+ * @param {object} sock Baileys socket (harus punya relayMessage)
197
+ * @param {string} jid JID tujuan
198
+ * @param {object} content lihat buildRichMenuMessage
199
+ * @param {object} [options] { messageId }
200
+ */
201
+ export async function sendRichMenu(sock, jid, content = {}, options = {}) {
202
+ if (!sock || typeof sock.relayMessage !== 'function') {
203
+ throw new TypeError('sendRichMenu: "sock" harus instance Baileys socket yang punya relayMessage()');
204
+ }
205
+ const message = buildRichMenuMessage(content);
206
+ const messageId = options.messageId || ('3EB0' + randomBytes(18).toString('hex').toUpperCase());
207
+ await sock.relayMessage(jid, message, { messageId });
208
+ return { messageId, message };
209
+ }
210
+
211
+ export default { buildRichMenuMessage, sendRichMenu };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rennzsync/baileys",
3
3
  "type": "module",
4
- "version": "10.4.0",
4
+ "version": "10.5.0",
5
5
  "description": "renz/baileys — WhatsApp Multi-Device library rebased @whiskeysockets/baileys 7.0.0-rc14, using the original libsignal (GPL-3.0) Signal Protocol engine. Maintained by RennZz-Dev. Focus: multimedia WhatsApp bots, low RAM footprint.",
6
6
  "license": "MIT",
7
7
  "author": "RennZz-Dev (https://github.com/renzxhub)",