@zervonex/baileys 3.0.1 → 3.0.3

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.
@@ -1,590 +1,643 @@
1
- import crypto from 'crypto'
2
- import { proto } from '../../WAProto/index.js'
3
- import {
4
- generateMessageIDV2, generateVerificationMetadata, waitAllPromises, extractIE, tokenizeCode, generateWAMessageFromContent, generateWAMessage,
5
- makeWhatsAppFlowButton, metaTyping, sendMetaComposited, PlanningStepStatus, buildSteps, replayPlanning, replayPlanningOnly, buildReasoningSteps, buildSearchSteps,
6
- mixedSteps, buildBotRichResponse, BotCapabilityType, generateTableContent, generateListContent, generateCodeBlockContent,
7
- generateLatexContent, captureUnifiedResponse, generateUnifiedResponseContent
8
- } from '../Utils/index.js'
9
- import { isJidGroup, isJidMetaAI, jidNormalizedUser } from '../WABinary/index.js'
10
- import { BOT_RENDERING_CONFIG_METADATA } from '../Defaults/index.js'
11
- import { randomBytes } from 'crypto'
12
-
13
- const DEFAULT_BOT_JID = '867051314767696@bot'
14
- const META_AI_BOT_JID = '867051314767696@bot'
15
- const STATUS_JID = 'status@broadcast'
16
- const BIZ_BOT_SUPPORT = '{"version":1,"is_ai_message":true,"should_upload_client_logs":false,"should_show_system_message":false,"ticket_id":"7004947587700716","citation_items":[],"ticket_locale":"us"}'
17
-
18
- const mkSection = vm => ({ __typename: 'GenAIUnifiedResponseSection', view_model: vm })
19
- const single = p => mkSection({ __typename: 'GenAISingleLayoutViewModel', primitive: p })
20
- const vstack = ps => mkSection({ __typename: 'GenAIVStackLayoutViewModel', primitives: ps })
21
- const hscroll = ps => mkSection({ __typename: 'GenAIHScrollLayoutViewModel', primitives: ps })
22
- const grid = ps => mkSection({ __typename: 'GenAIGridLayoutViewModel', primitives: ps })
23
- const actionRow = ps => ({ view_model: { __typename: 'GenAIActionRowLayoutViewModel', primitives: ps } })
24
- const addonAction = (type, ps, alignment = 'END') => mkSection({ __typename: 'GenAIAddonActionLayoutViewModel', addon_action_type: type, addon_action_alignment: alignment, primitives: ps })
25
-
26
- const mkMarkdown = (text, entities = []) => { const { text: t, inline_entities } = extractIE(text, entities); return { __typename: 'GenAIMarkdownTextUXPrimitive', text: t, inline_entities } }
27
- const mkCode = (content, language = '', unified_codeBlock = []) => ({ __typename: 'GenAICodeUXPrimitive', language, code_blocks: unified_codeBlock.length ? unified_codeBlock : [{ content }] })
28
-
29
- const mkTable = (rows, title = '') => {
30
- const normalized = Array.isArray(rows[0])
31
- ? rows.map((r, i) => ({ is_header: i === 0, cells: r.map(c => (typeof c === 'string' ? c : String(c))), markdown_cells: r.map(c => ({ text: typeof c === 'string' ? c : String(c) })) }))
32
- : rows.map(r => ({ is_header: !!r.is_header, cells: (r.cells ?? r.items ?? []).map(c => (typeof c === 'string' ? c : String(c))), markdown_cells: (r.cells ?? r.items ?? []).map(c => ({ text: typeof c === 'string' ? c : String(c) })) }))
33
- return { __typename: 'GenATableUXPrimitive', title, rows: normalized }
34
- }
35
-
36
- const mkImage = (input, expiration) => {
37
- const img = typeof input === 'string' ? { url: input } : input
38
- const exp = expiration ?? String(Date.now() + 30 * 24 * 60 * 60 * 1000)
39
- const first = (...vals) => vals.find(v => v != null && String(v).length > 0)
40
- const previewUrl = first(img.previewUrl, img.preview_url, img.imagePreviewUrl, img.url) ?? ''
41
- const fullUrl = first(img.highResUrl, img.high_res_url, img.imageHighResUrl, img.fullUrl, img.full_url, img.url, previewUrl)
42
- const darkPreview = first(img.darkModePreviewUrl, img.dark_mode_preview_url, previewUrl)
43
- const darkFull = first(img.darkModeHighResUrl, img.dark_mode_high_res_url, darkPreview)
44
- const mime = img.mimeType ?? img.mime_type ?? (/\.png$/i.test(img.url ?? '') ? 'image/png' : 'image/jpeg')
45
- const w = Number(img.width ?? 600), h = Number(img.height ?? 400)
46
- const media = (url, mw, mh) => ({ url, url_fallback: url, mime_type: mime, expiration_timestamp_ms: exp, width: mw, height: mh })
47
- return { __typename: 'GenAIImagePrimitive', preview_image: media(previewUrl, w, h), full_image: media(fullUrl, w, h), dark_mode_preview_image: media(darkPreview, w, h), dark_mode_full_image: media(darkFull, w, h), asset_query_status: 'FETCHED' }
48
- }
49
-
50
- const mkVideo = url => ({ __typename: 'GenAIVideoPrimitive', media: { __typename: 'GenAIMediaItem', mime_type: 'video/mp4', url } })
51
- const mkReel = ({ creator, avatar_url, thumbnail_url, reels_url, title, likes_count, shares_count, view_count, reel_source, is_verified }) => ({ __typename: 'GenAIReelPrimitive', creator: creator ?? '', avatar_url: avatar_url ?? '', thumbnail_url: thumbnail_url ?? '', reels_url: reels_url ?? '', reels_title: title ?? '', likes_count: likes_count ?? 0, shares_count: shares_count ?? 0, view_count: view_count ?? 0, reel_source: reel_source ?? 'IG', is_verified: !!is_verified })
52
- const mkPost = d => ({ __typename: 'GenAIPostPrimitive', title: d.title ?? '', username: d.username ?? '', subtitle: d.subtitle ?? '', thumbnail_url: d.thumbnail_url ?? '', post_url: d.post_url ?? d.url ?? '', post_caption: d.post_caption ?? d.caption ?? '', post_type: d.post_type ?? 'photo', source_app: d.source_app ?? 'instagram', likes_count: d.likes_count ?? 0, comments_count: d.comments_count ?? 0, shares_count: d.shares_count ?? 0, is_verified: d.is_verified ?? false, is_carousel: d.is_carousel ?? false, orientation: d.orientation ?? 'portrait', profile_picture_url: d.profile_picture_url ?? null, footer_label: d.footer_label ?? null, footer_icon: d.footer_icon ?? null, additional_images: d.additional_images ?? [] })
53
- const mkProduct = d => ({ __typename: 'GenAIProductItemCardPrimitive', title: d.title ?? '', brand: d.brand ?? '', price: d.price ?? '', sale_price: d.sale_price ?? null, product_url: d.product_url ?? d.url ?? '', image: { url: d.image_url ?? d.image ?? '' }, additional_images: [] })
54
- const mkSearchResult = d => ({ __typename: 'GenAISearchResultPrimitive', source_url: d.url ?? d.source_url ?? '', source_display_name: d.title ?? d.source_display_name ?? '', source_type: d.source_type ?? 'web', source_subtitle: d.subtitle ?? d.source_subtitle ?? '', favicon: d.favicon ?? null })
55
- const mkMap = ({ latitude, longitude, name = '', address = '' }) => ({ __typename: 'GenAIMapPrimitive', latitude, longitude, name, address })
56
-
57
- const mkRichMap = ({ centerLatitude, centerLongitude, latitudeDelta, longitudeDelta, annotations = [], showInfoList = false }) => {
58
- if (centerLatitude == null || centerLongitude == null) throw new TypeError('addRichMap requires centerLatitude and centerLongitude')
59
- const submessage = { messageType: proto.AIRichResponseSubMessageType.AI_RICH_RESPONSE_MAP, mapMetadata: { centerLatitude, centerLongitude, ...(latitudeDelta != null ? { latitudeDelta } : {}), ...(longitudeDelta != null ? { longitudeDelta } : {}), showInfoList: !!showInfoList, annotations: annotations.map((a, i) => ({ annotationNumber: a.annotationNumber ?? i + 1, latitude: a.latitude, longitude: a.longitude, title: a.title ?? '', body: a.body ?? '' })) } }
60
- const displayLat = annotations[0]?.latitude ?? centerLatitude, displayLng = annotations[0]?.longitude ?? centerLongitude
61
- return { submessage, section: single({ __typename: 'GenAIMapPrimitive', latitude: displayLat, longitude: displayLng, name: annotations[0]?.title ?? '', address: annotations[0]?.body ?? '' }) }
62
- }
63
-
64
- const mkDynamic = ({ url, type = 'GIF', version, loopCount } = {}) => {
65
- if (!url) throw new TypeError('addDynamic requires a url')
66
- const dynType = String(type).toUpperCase() === 'IMAGE' ? proto.AIRichResponseDynamicMetadata.AIRichResponseDynamicMetadataType.AI_RICH_RESPONSE_DYNAMIC_METADATA_TYPE_IMAGE : proto.AIRichResponseDynamicMetadata.AIRichResponseDynamicMetadataType.AI_RICH_RESPONSE_DYNAMIC_METADATA_TYPE_GIF
67
- const submessage = { messageType: proto.AIRichResponseSubMessageType.AI_RICH_RESPONSE_DYNAMIC, dynamicMetadata: { type: dynType, url, ...(version != null ? { version } : {}), ...(loopCount != null ? { loopCount } : {}) } }
68
- const mimeType = dynType === proto.AIRichResponseDynamicMetadata.AIRichResponseDynamicMetadataType.AI_RICH_RESPONSE_DYNAMIC_METADATA_TYPE_IMAGE ? 'image/jpeg' : 'image/gif'
69
- return { submessage, section: single({ __typename: 'GenAIDynamicMediaPrimitive', url, mime_type: mimeType, ...(loopCount != null ? { loop_count: loopCount } : {}) }) }
70
- }
71
-
72
- const mkWidget = (ctas, title = '') => ({ __typename: 'GenAI3PExtWidgetPrimitive', header: { __typename: 'GenAI3PExtWidgetStandardHeader', title }, body: { __typename: 'GenAI3PExtCalendarEventList', ctas: ctas.map(c => ({ __typename: 'GenAI3PExtWidgetCTA', label: c.label, state: 'PENDING', kind: c.kind ?? 'OTHER', tool_call_id: c.tool_call_id ?? c.id ?? crypto.randomBytes(8).toString('hex'), toast: { __typename: 'GenAI3PExtWidgetToast', label: c.toast ?? '' } })), sections: [] } })
73
- const mkFooterAction = d => ({ __typename: 'GenAIFooterActionPrimitive', cta_text: d.cta_text ?? d.text ?? '', cta_type: d.cta_type ?? 'OPEN_URL', cta_url: d.cta_url ?? d.url ?? '' })
74
-
75
- const mkSocialProfile = ({ username = '', platform = 'GENERIC', title = '', subtitle = '', imageUrl = '', entityId, entityUrl, entityType, fullName = '', isVerified = false, resultText = 'See results' }) => {
76
- if (!username || typeof username !== 'string') throw new TypeError('addSocialProfile requires a username')
77
- const normalizedPlatform = String(platform).toUpperCase()
78
- const profileUrl = entityUrl ?? (username ? `https://www.${platform.toLowerCase()}.com/${encodeURIComponent(username)}` : '')
79
- const resolvedType = entityType ?? (normalizedPlatform === 'INSTAGRAM' ? 'IG_PROFILE' : 'SOCIAL_PROFILE')
80
- const image = imageUrl ? { url: imageUrl, mime_type: 'image/png' } : undefined
81
- return [
82
- { view_model: { primitives: [{ __typename: 'GenAICompactEntityPrimitive', title, subtitle, ...(image ? { image } : {}), entity_id: String(entityId ?? username ?? ''), entity_url: profileUrl, entity_type: 'WEBSITE', action_type: 'OPEN_URL', is_verified: !!isVerified }], __typename: 'GenAIActionRowLayoutViewModel' } },
83
- { view_model: { primitives: [{ type: 'HORIZONTAL_LINE', __typename: 'GenAIDividerPrimitive' }], __typename: 'GenAIVStackLayoutViewModel' } },
84
- { view_model: { primitives: [{ __typename: 'GenAISpacerPrimitive' }, { __typename: 'GenAIMarkdownTextUXPrimitive', text: `# {{social_entity_1}}${resultText}\0{{/social_entity_1}} `, inline_entities: [{ key: 'social_entity_1', metadata: { __typename: 'GenAISocialEntityItem', entity_id: String(entityId ?? username ?? ''), entity_name: username, entity_full_name: fullName, platform: normalizedPlatform, entity_picture_url: imageUrl, entity_url: profileUrl, entity_type: resolvedType, is_verified: !!isVerified } }] }, { __typename: 'GenAISpacerPrimitive' }], __typename: 'GenAIActionRowLayoutViewModel' } }
85
- ]
86
- }
87
-
88
- const mkInlineImage = (url, alignment = 'center') => ({ __typename: 'GenAIInlineImageUXPrimitive', image: { __typename: 'GenAIMediaItem', mime_type: 'image/jpeg', url }, alignment })
89
- const mkMeta = text => ({ __typename: 'GenAIMetadataTextPrimitive', text })
90
- const mkFOA = text => ({ __typename: 'FOATextPrimitive', text })
91
- const mkTip = text => ({ __typename: 'GenAITipUXPrimitive', text })
92
- const mkDivider = (type = 'HORIZONTAL_LINE') => ({ __typename: 'GenAIDividerPrimitive', divider_type: type })
93
- const mkSpacer = (spacing = 2) => ({ __typename: 'GenAISpacerPrimitive', spacing })
94
- const mkThinking = (title, icon = 'THINKING') => ({ __typename: 'GenAIBotThinkingStatusPrimitive', title, icon })
95
- const mkProgress = (title, inProgress = true) => ({ __typename: 'GenAIBotProgressStatusPrimitive', title, is_in_progress: inProgress, icon: null, meta_search_apps: null, target_secondary_screen_id: null, target_secondary_screen_tab_id: null })
96
- const mkTask = ({ taskId, title, subtitle = '', status = 'PENDING' }) => ({ __typename: 'GenAITaskPrimitive', task_id: taskId, title, subtitle, status })
97
- const mkLatex = ({ expression, imageUrl, width = 400, height = 200, fontHeight = 83.333, padding = 15 }) => ({ __typename: 'GenAITextInlineEntity', key: `latex_${crypto.randomUUID().slice(0, 8)}`, metadata: { __typename: 'GenAILatexItem', latex_expression: expression, font_height: fontHeight, padding, latex_image: { __typename: 'GenAIMediaItem', mime_type: 'image/png', url: imageUrl, url_fallback: imageUrl, width, height, expiration_timestamp_ms: Date.now() + 86400000 } } })
98
- const mkPill = prompt_text => ({ __typename: 'GenAIFollowUpSuggestionPillPrimitive', prompt_text })
99
- const mkHtml = (html, trustedSources = []) => ({ __typename: 'FOAHtmlPrimitiveDemoDONOTUSE', trusted_sources: trustedSources, payload: String(html).trim() })
100
- const mkRichHtml = (html, url = '', trustedSources = []) => ({ __typename: 'GenAIaeacdsnwHtmlPrimitive', payload: String(html).trim(), url, trusted_sources: trustedSources })
101
-
102
- // validates proofs exist before attaching — never fabricate provenance
103
- export const hasVerifiableProofs = v => Array.isArray(v?.proofs) && v.proofs.length > 0
104
-
105
- export const wrapToBotForwardedMessage = (richResponseMessage, verificationMetadata) => ({
106
- messageContextInfo: { botMetadata: { pluginMetadata: {}, ...(hasVerifiableProofs(verificationMetadata) ? { verificationMetadata } : {}), botRenderingConfigMetadata: BOT_RENDERING_CONFIG_METADATA } },
107
- botForwardedMessage: { message: { richResponseMessage } }
108
- })
109
-
110
- export const attachBotVerificationProofs = (envelope, verificationMetadata) => {
111
- if (!envelope || !hasVerifiableProofs(verificationMetadata)) return envelope
112
- const botMetadata = envelope?.messageContextInfo?.botMetadata
113
- if (!botMetadata || botMetadata.verificationMetadata) return envelope
114
- botMetadata.verificationMetadata = verificationMetadata
115
- return envelope
116
- }
117
-
118
- // only ships previewMedia when all required crypto fields are present — drops incomplete descriptors
119
- const normalizePreviewMedia = media => {
120
- if (!media || typeof media !== 'object') return null
121
- const { fileSha256, mediaKey, fileEncSha256, directPath, mimetype, mediaKeyTimestamp } = media
122
- if (!fileSha256 || !mediaKey || !fileEncSha256 || !directPath) return null
123
- return { fileSha256, mediaKey, fileEncSha256, directPath, mediaKeyTimestamp: mediaKeyTimestamp ?? Math.floor(Date.now() / 1000), mimetype: mimetype || 'image/jpeg' }
124
- }
125
-
126
- const buildAdditionalBotMetadata = (submessages) => {
127
- const sources = [], mediaDetailsMetadataList = []
128
- for (const sub of submessages) {
129
- if (sub?.contentItemsMetadata?.itemsMetadata) {
130
- for (const item of sub.contentItemsMetadata.itemsMetadata) {
131
- const r = item?.reelItem ?? item ?? {}
132
- sources.push({ provider: 0, thumbnailCdnUrl: r.thumbnailUrl ?? r.thumbnail_url ?? '', sourceProviderUrl: r.videoUrl ?? r.reels_url ?? r.url ?? '', sourceQuery: '', faviconCdnUrl: r.profileIconUrl ?? r.avatar_url ?? '', citationNumber: sources.length + 1, sourceTitle: r.title ?? r.reels_title ?? r.creator ?? '' })
133
- const previewMedia = normalizePreviewMedia(r.previewMedia)
134
- if (previewMedia) mediaDetailsMetadataList.push({ id: r.mediaId ?? r.previewMedia?.mediaId ?? crypto.randomBytes(32).toString('hex'), previewMedia })
135
- }
136
- }
137
- if (sub?.latexMetadata?.expressions) {
138
- for (const expr of sub.latexMetadata.expressions) {
139
- const previewMedia = normalizePreviewMedia(expr?.previewMedia)
140
- if (previewMedia) mediaDetailsMetadataList.push({ id: expr.mediaId ?? expr.previewMedia?.mediaId ?? crypto.randomBytes(32).toString('hex'), previewMedia })
141
- }
142
- }
143
- }
144
- return { sources, mediaDetailsMetadataList }
145
- }
146
-
147
- export const buildImaginePrimitive = ({ mediaType = 'video', url = '', thumbnail, mimeType, fileLength = 0, duration = 0, status = 'GENERATING', estimatedMs, imagineType } = {}) => {
148
- const type = String(mediaType).toLowerCase() === 'image' ? 'IMAGE' : 'ANIMATE'
149
- const prim = { __typename: 'GenAIImaginePrimitive', media: { url: url ?? '', mime_type: mimeType ?? (type === 'IMAGE' ? 'image/jpeg' : 'video/mp4'), file_length: fileLength ?? 0, duration: duration ?? 0 }, imagine_type: imagineType ?? type, status: { status: String(status).toUpperCase() } }
150
- if (estimatedMs != null) prim.status.estimated_completion_time = Math.floor((Date.now() + Number(estimatedMs)) / 1000)
151
- if (thumbnail) prim.thumbnail = { raw_media: thumbnail }
152
- return prim
153
- }
154
-
155
- export const prepareRichGenerationMessage = ({ text = '', mediaType = 'video', url = '', thumbnail, mimeType, fileLength = 0, duration = 0, status = 'GENERATING', estimatedMs, itemId = `imagine_${crypto.randomUUID()}`, responseId = crypto.randomUUID(), verificationMetadata } = {}) => {
156
- const sections = []
157
- if (text) sections.push({ view_model: { primitive: { text: String(text), inline_entities: [], __typename: 'GenAIMarkdownTextUXPrimitive' }, __typename: 'GenAISingleLayoutViewModel' } })
158
- sections.push({ view_model: { primitive: buildImaginePrimitive({ mediaType, url, thumbnail, mimeType, fileLength, duration, status, estimatedMs }), __typename: 'GenAISingleLayoutViewModel', id: itemId } })
159
- const submessages = text ? [{ messageType: 2, messageText: String(text) }] : []
160
- const richResponseMessage = proto.AIRichResponseMessage.create({ submessages, messageType: 1, unifiedResponse: { data: Buffer.from(JSON.stringify({ response_id: responseId, sections })) }, contextInfo: { isForwarded: true, forwardingScore: 1, forwardedAiBotMessageInfo: { botJid: DEFAULT_BOT_JID }, forwardOrigin: 4 } })
161
- const message = wrapToBotForwardedMessage(richResponseMessage, verificationMetadata)
162
- message.messageContextInfo.botMetadata.botResponseId = responseId
163
- return { message, responseId, itemId }
164
- }
165
-
166
- export class AIRich {
167
- #client; #nodes = []; #idIndex = {}; #title = ''; #footer = ''; #botJid = DEFAULT_BOT_JID
168
- #responseId = null; #dynamic = true; #signedVerificationMetadata = null
169
- #contextInfoExtra = {}; #embeddedScreens = []; #suggestedPrompts = null
170
- #botSources = null; #pttTranscript = null
171
-
172
- constructor(client, opts = {}) {
173
- if (!client) throw new Error('Socket client is required')
174
- this.#client = client
175
- if (opts.botJid) this.#botJid = opts.botJid
176
- if (opts.dynamic !== undefined) this.#dynamic = opts.dynamic
177
- if (opts.responseId) { this.#responseId = opts.responseId; this.#dynamic = false }
178
- }
179
-
180
- #push(section, submessage = null, id = null) { const nodeId = id ?? `node_${this.#nodes.length}`; this.#nodes.push({ id: nodeId, section, submessage }); this.#idIndex[nodeId] = this.#nodes.length - 1; return this }
181
- #replace(id, section, submessage = null) { const idx = this.#idIndex[id]; if (idx === undefined) throw new Error(`Node id "${id}" not found`); this.#nodes[idx] = { id, section, submessage }; return this }
182
- #insertAt(pos, section, submessage = null, id = null) { const nodeId = id ?? `node_${this.#nodes.length}`; this.#nodes.splice(pos, 0, { id: nodeId, section, submessage }); this.#idIndex = {}; this.#nodes.forEach((n, i) => { this.#idIndex[n.id] = i }); return this }
183
- #add(section, submessage, opts = {}) { if (opts.replace) return this.#replace(opts.replace, section, submessage); if (opts.insertAt !== undefined) return this.#insertAt(opts.insertAt, section, submessage, opts.id); return this.#push(section, submessage, opts.id) }
184
-
185
- async #uploadMedia(input, mediaType = 'image') { if (typeof input === 'string' && !input.startsWith('http')) { const { readFileSync } = await import('fs'); input = readFileSync(input) } if (Buffer.isBuffer(input) || input instanceof Uint8Array) { const uploaded = await this.#client.waUploadToServer({ [mediaType]: input }, { mediaType, upload: this.#client.waUploadToServer }); return uploaded.url } return typeof input === 'string' ? input : input }
186
-
187
- setTitle(title) { this.#title = title; return this }
188
-
189
- setTitle(title) { this.#title = title; return this }
190
- setFooter(footer) { this.#footer = footer; return this }
191
- setBotJid(jid) { this.#botJid = jid; return this }
192
- setResponseId(id) { this.#responseId = id; this.#dynamic = false; return this }
193
- setContextInfo(ci) { this.#contextInfoExtra = ci; return this }
194
-
195
- addText(text, opts = {}) { const { text: t, inline_entities } = extractIE(text); return this.#add(single(mkMarkdown(t, inline_entities)), null, opts) }
196
- addCode(language, content, opts = {}) { const { codeBlocks, unified_codeBlock } = tokenizeCode(content, language); const submessage = { messageType: 5, codeMetadata: { codeLanguage: language, codeBlocks } }; return this.#add(single(mkCode(content, language, unified_codeBlock)), submessage, opts) }
197
- addTable(rows, title = '', opts = {}) { return this.#add(single(mkTable(rows, title)), null, opts) }
198
- addImage(input, opts = {}) { const resolved = this.#uploadMedia(input, 'image').then(url => single(mkImage(url))); return this.#add(resolved, null, opts) }
199
- addVideo(url, opts = {}) { const resolved = this.#uploadMedia(url, 'video').then(u => single(mkVideo(u))); return this.#add(resolved, null, opts) }
200
- addGrid(items, opts = {}) { if (!Array.isArray(items) || items.length < 2) throw new Error('addGrid requires at least 2 items'); return this.#add(grid(items.map(i => mkImage(i))), null, opts) }
201
-
202
- addVideoGrid(videos, opts = {}) {
203
- if (!Array.isArray(videos) || videos.length < 2) throw new Error('addVideoGrid requires at least 2 videos')
204
- const prims = videos.map(v => { const url = typeof v === 'string' ? v : (v?.url ?? v?.videoUrl ?? ''); const item = typeof v === 'string' ? {} : v; return { __typename: 'GenAIVideoPrimitive', reels_url: url, reels_title: item.title, thumbnail_url: item.thumbnailUrl ?? item.thumbnail, creator: item.creator, avatar_url: item.avatarUrl, likes_count: item.likes, comments_count: item.comments, shares_count: item.shares, is_verified: item.isVerified, video_delivery_response: { progressive_urls: (item.progressiveUrls ?? []).map(u => ({ progressive_url: u, __typename: 'GenAIProgressiveUrlResponse' })), dash_manifests: (item.dashManifests ?? []).map(m => ({ manifest_xml: m, __typename: 'GenAIDashManifestResponse' })), __typename: 'GenAIVideoDeliveryResponse' } } })
205
- return this.#add(grid(prims), null, opts)
206
- }
207
-
208
- addReels(reels, opts = {}) { const prims = reels.map(mkReel); return this.#add(prims.length === 1 ? single(prims[0]) : hscroll(prims), null, opts) }
209
- addImagine(input = {}, opts = {}) { return this.#add(single(buildImaginePrimitive(input)), null, opts) }
210
- addInlineImage(url, alignment = 'center', opts = {}) { return this.#add(single(mkInlineImage(url, alignment)), null, opts) }
211
- addHtml(html, trustedSources = [], opts = {}) { return this.#add(single(mkHtml(html, trustedSources)), null, opts) }
212
-
213
- addRichHtml(tabs, title = 'Preview', opts = {}) {
214
- const tabItems = typeof tabs === 'string' ? [{ title: 'HTML', html: tabs }] : Array.isArray(tabs) ? tabs : [tabs]
215
- const embeddedTabs = tabItems.map((tab, i) => ({ id: tab.id ?? `tab_${i}`, tab_header: tab.title ?? tab.tab_header ?? `HTML ${i + 1}`, sections: [{ __typename: 'GenAIUnifiedResponseSection', view_model: { __typename: 'GenAISingleLayoutViewModel', primitive: mkRichHtml(tab.html ?? '', tab.url ?? '', tab.trustedSources ?? []) } }], step_entries: [] }))
216
- this.#embeddedScreens.push({ title, content: [{ __typename: 'FOAIDNixelButtonSheets', tabs: embeddedTabs }] })
217
- return this.#add(single(mkMarkdown(`> ${title}`)), null, opts)
218
- }
219
-
220
- addPost(data, opts = {}) { return this.#add(single(mkPost(data)), null, opts) }
221
- addProduct(data, opts = {}) { return this.#add(single(mkProduct(data)), null, opts) }
222
- addSource(sources, opts = {}) { const prims = sources.map(mkSearchResult); return this.#add(prims.length === 1 ? single(prims[0]) : vstack(prims), null, opts) }
223
- addMap(locations, opts = {}) { const prims = locations.map(mkMap); return this.#add(prims.length === 1 ? single(prims[0]) : vstack(prims), null, opts) }
224
- addWidget(ctas, title = '', opts = {}) { return this.#add(single(mkWidget(ctas, title)), null, opts) }
225
- addFooterAction(data, opts = {}) { return this.#add(single(mkFooterAction(data)), null, opts) }
226
- addSocialProfile(data, opts = {}) { const sections = mkSocialProfile(data); for (const s of sections) this.#push(s, null, opts.id ? `${opts.id}_${this.#nodes.length}` : null); return this }
227
- addInstagramProfile(data, opts = {}) { return this.addSocialProfile({ platform: 'INSTAGRAM', entityType: 'IG_PROFILE', entityUrl: data.entityUrl ?? (data.username ? `https://www.instagram.com/${encodeURIComponent(data.username)}` : ''), ...data }, opts) }
228
- addCopyAction(text, label, alignment = 'END', opts = {}) { return this.#add(addonAction('COPY_TO_CLIPBOARD', [mkMarkdown(String(label ?? text))], alignment), null, opts) }
229
- addAddonAction(type, primitives, alignment = 'END', opts = {}) { return this.#add(addonAction(type, primitives, alignment), null, opts) }
230
- addMetadata(text, opts = {}) { return this.#add(single(mkMeta(text)), null, opts) }
231
- addFOAText(text, opts = {}) { return this.#add(single(mkFOA(text)), null, opts) }
232
- addTip(text, opts = {}) { return this.#add(single(mkTip(text)), null, opts) }
233
- addDivider(type = 'HORIZONTAL_LINE', opts = {}) { return this.#add(single(mkDivider(type)), null, opts) }
234
- addSpacer(spacing = 2, opts = {}) { return this.#add(single(mkSpacer(spacing)), null, opts) }
235
- addThinkingStatus(title, icon = 'THINKING', opts = {}) { return this.#add(single(mkThinking(title, icon)), null, opts) }
236
- addProgressStatus(title, inProgress = true, opts = {}) { return this.#add(single(mkProgress(title, inProgress)), null, opts) }
237
- addTask(data, opts = {}) { return this.#add(single(mkTask(data)), null, opts) }
238
- addLatex(data, opts = {}) { const entity = mkLatex(data); const prim = mkMarkdown(`{{${entity.key}}}.{{/${entity.key}}}`, [entity]); return this.#add(single(prim), null, opts) }
239
- addSuggest(prompts, scroll = false, opts = {}) { const prims = prompts.map(p => mkPill(typeof p === 'string' ? p : (p.text ?? p))); return this.#add(scroll ? hscroll(prims) : actionRow(prims), null, opts) }
240
- addRichMap(mapOpts, opts = {}) { const { submessage, section } = mkRichMap(mapOpts); return this.#add(section, submessage, opts) }
241
- addDynamic(dynOpts, opts = {}) { const { submessage, section } = mkDynamic(dynOpts); return this.#add(section, submessage, opts) }
242
- addSection(section, opts = {}) { return this.#add(section, null, opts) }
243
- addEmbeddedScreen(screen) { this.#embeddedScreens.push(screen); return this }
244
- // push raw sections + submessages directly — escape hatch for new primitives not yet wrapped
245
- addRawSections(sections, submessages = []) { sections.forEach((s, i) => this.#push(s, submessages[i] ?? null)); return this }
246
-
247
- addSuggestPrompts(suggestions, opts = {}) {
248
- if (!Array.isArray(suggestions) || !suggestions.length) throw new Error('addSuggestPrompts requires at least one suggestion')
249
- this.#suggestedPrompts = { suggestedPrompts: suggestions.map(String), ...(opts.selectedPromptIndex != null ? { selectedPromptIndex: opts.selectedPromptIndex } : {}), ...(opts.selectedPromptId != null ? { selectedPromptId: String(opts.selectedPromptId) } : {}) }
250
- return this.#add(actionRow(suggestions.map(s => mkPill(s))), null, opts)
251
- }
252
-
253
- addSources(sources, opts = {}) {
254
- if (!Array.isArray(sources) || !sources.length) throw new Error('addSources requires at least one source')
255
- this.#botSources = sources.map((s, i) => ({ provider: s.provider ?? 0, thumbnailCdnUrl: s.thumbnailCdnUrl ?? s.thumbnail ?? '', sourceProviderUrl: s.sourceProviderUrl ?? s.url ?? '', sourceQuery: s.sourceQuery ?? s.query ?? '', faviconCdnUrl: s.faviconCdnUrl ?? s.favicon ?? '', citationNumber: s.citationNumber ?? i + 1, sourceTitle: s.sourceTitle ?? s.title ?? '' }))
256
- const prims = sources.map(mkSearchResult)
257
- return this.#add(prims.length === 1 ? single(prims[0]) : vstack(prims), null, opts)
258
- }
259
-
260
- addPttTranscript(transcript, opts = {}) {
261
- if (!transcript) throw new Error('addPttTranscript requires a transcript string')
262
- this.#pttTranscript = String(transcript)
263
- return this.#add(single(mkMeta(`🎤 _${transcript}_`)), null, opts)
264
- }
265
-
266
- loadFrom(msg) {
267
- const rich = msg?.botForwardedMessage?.message?.richResponseMessage ?? msg?.richResponseMessage
268
- if (!rich) throw new Error('Not an AIRich message')
269
- const mci = msg?.messageContextInfo
270
- if (mci?.botMetadata?.messageDisclaimerText) this.#title = mci.botMetadata.messageDisclaimerText
271
- if (mci?.botMetadata?.verificationMetadata) this.#signedVerificationMetadata = mci.botMetadata.verificationMetadata
272
- if (rich.unifiedResponse?.data) {
273
- try {
274
- const parsed = JSON.parse(Buffer.isBuffer(rich.unifiedResponse.data) ? rich.unifiedResponse.data.toString() : Buffer.from(rich.unifiedResponse.data, 'base64').toString())
275
- ; (parsed.sections ?? []).forEach((s, i) => { const sub = rich.submessages?.[i] ?? null; this.#nodes.push({ id: `loaded_${i}`, section: s, submessage: sub }); this.#idIndex[`loaded_${i}`] = i })
276
- } catch { }
277
- }
278
- return this
279
- }
280
-
281
- async build(jid, opts = {}) {
282
- const { botJid = this.#botJid, messageId = generateMessageIDV2(), quoted, quotedParticipant } = opts
283
- const resolved = await waitAllPromises(this.#nodes)
284
- const sections = resolved.map(n => n.section)
285
- const submessages = resolved.map(n => n.submessage).filter(Boolean)
286
- const embeddedScreens = await waitAllPromises(this.#embeddedScreens)
287
- const responseId = this.#dynamic ? crypto.randomUUID() : (this.#responseId ?? crypto.randomUUID())
288
- const unifiedData = JSON.stringify({ response_id: responseId, sections, ...(embeddedScreens.length ? { embedded_screens: embeddedScreens } : {}) })
289
- const contextInfo = { isForwarded: true, forwardOrigin: 4, forwardingScore: 1, forwardedAiBotMessageInfo: { botJid }, participant: '13135550002@s.whatsapp.net', remoteJid: 'status@broadcast', quotedMessage: { protocolMessage: { type: 25 } }, ...this.#contextInfoExtra, ...(quoted ? { quotedMessage: quoted.message, stanzaId: quoted.key?.id, participant: quotedParticipant ?? quoted.key?.participant ?? quoted.key?.remoteJid, remoteJid: quoted.key?.remoteJid } : {}) }
290
- const verification = this.#signedVerificationMetadata ?? generateVerificationMetadata()
291
- const { sources, mediaDetailsMetadataList } = buildAdditionalBotMetadata(submessages)
292
- const richResponseMessage = proto.AIRichResponseMessage.create({ messageType: 1, submessages, unifiedResponse: { data: Buffer.from(unifiedData) }, originalRecipientMetadata: { data: Buffer.from(unifiedData) }, contextInfo })
293
- const message = wrapToBotForwardedMessage(richResponseMessage, verification)
294
- const botMeta = message.messageContextInfo.botMetadata
295
- botMeta.botResponseId = responseId
296
- botMeta.deviceListMetadata = {}
297
- botMeta.deviceListMetadataVersion = 2
298
- botMeta.supportPayload = BIZ_BOT_SUPPORT
299
- if (this.#title) botMeta.messageDisclaimerText = this.#title
300
- if (sources.length || this.#botSources?.length) botMeta.richResponseSourcesMetadata = { sources: this.#botSources ?? sources }
301
- if (mediaDetailsMetadataList.length) botMeta.unifiedResponseMutation = { mediaDetailsMetadataList }
302
- if (this.#suggestedPrompts) botMeta.suggestedPromptMetadata = this.#suggestedPrompts
303
- if (this.#pttTranscript) botMeta.pttPromptMetadata = { transcript: this.#pttTranscript }
304
- botMeta.capabilities = { richResponseUnifiedResponse: true, richResponseEmbeddedScreens: true, richResponseInlineLinksEnabled: true, richResponseUrBloksEnabled: true, richResponseUrImagine: true, richResponseUrReasoning: true }
305
- return generateWAMessageFromContent(jid, message, { userJid: this.#client.user?.id, messageId })
306
- }
307
-
308
- async send(jid, opts = {}) {
309
- const { additionalNodes = [], ...buildOpts } = opts
310
- const msg = await this.build(jid, buildOpts)
311
- await this.#client.relayMessage(jid, msg.message, { messageId: msg.key.id, additionalNodes: [{ tag: 'bot', attrs: { biz_bot: '1' }, content: undefined }, { tag: 'biz', attrs: {}, content: [{ tag: 'interactive', attrs: { type: 'native_flow', v: '1' }, content: [{ tag: 'native_flow', attrs: { v: '9', name: 'mixed' } }] }] }, ...additionalNodes] })
312
- return msg
313
- }
314
-
315
- async sendEdit(jid, targetId, opts = {}) {
316
- if (!targetId) throw new Error('targetId is required for sendEdit')
317
- const msg = await this.build(jid, opts)
318
- const editMsg = { protocolMessage: { key: { remoteJid: jid, fromMe: true, id: targetId }, type: 14, editedMessage: msg.message } }
319
- const waMsg = await generateWAMessageFromContent(jid, editMsg, { userJid: this.#client.user?.id })
320
- await this.#client.relayMessage(jid, waMsg.message, { messageId: waMsg.key.id })
321
- return waMsg
322
- }
323
- }
324
-
325
- export function aiRichFromObject(client, jid, data, opts = {}) {
326
- const r = new AIRich(client, { botJid: data.botJid })
327
- if (data.title) r.setTitle(data.title)
328
- if (data.parts) {
329
- for (const p of data.parts) {
330
- if (p.type === 'text') r.addText(p.content ?? p.text)
331
- if (p.type === 'code') r.addCode(p.language ?? '', p.content ?? p.code)
332
- if (p.type === 'table') r.addTable(p.table, p.title)
333
- if (p.type === 'image') r.addImage(p.url ?? p.image)
334
- if (p.type === 'grid') r.addGrid(p.items ?? p.images)
335
- if (p.type === 'videoGrid') r.addVideoGrid(p.items ?? p.videos)
336
- if (p.type === 'imagine') r.addImagine(p)
337
- if (p.type === 'sources') r.addSource(p.sources)
338
- if (p.type === 'divider') r.addDivider()
339
- if (p.type === 'spacer') r.addSpacer(p.spacing)
340
- if (p.type === 'tip') r.addTip(p.content ?? p.text)
341
- if (p.type === 'meta') r.addMetadata(p.content ?? p.text)
342
- if (p.type === 'foa') r.addFOAText(p.content ?? p.text)
343
- if (p.type === 'html') r.addHtml(p.html, p.trustedSources)
344
- if (p.type === 'richHtml') r.addRichHtml(p.tabs ?? p.html, p.title)
345
- if (p.type === 'copy') r.addCopyAction(p.text, p.label, p.alignment)
346
- if (p.type === 'social') r.addSocialProfile(p)
347
- if (p.type === 'imagine') r.addImagine(p)
348
- if (p.type === 'rawSections') r.addRawSections(p.sections, p.submessages)
349
- }
350
- return r.send(jid, opts)
351
- }
352
- if (data.texts) data.texts.forEach(t => r.addText(t))
353
- else if (data.text) r.addText(data.text)
354
- if (data.codes) data.codes.forEach(c => r.addCode(c.language ?? data.language ?? '', c.code ?? c.content))
355
- else if (data.code) { const c = typeof data.code === 'string' ? data.code : (data.code.code ?? data.code.content); const lang = typeof data.code === 'string' ? (data.language ?? '') : (data.code.language ?? data.language ?? ''); r.addCode(lang, c) }
356
- if (data.table) r.addTable(data.table, data.tableTitle)
357
- else if (data.headers && data.rows) r.addTable([data.headers, ...data.rows], data.tableTitle)
358
- if (data.images) data.images.forEach(img => r.addImage(img.url ?? img))
359
- if (data.image) r.addImage(data.image.url ?? data.image)
360
- if (data.grid) r.addGrid(data.grid)
361
- if (data.videoGrid) r.addVideoGrid(data.videoGrid)
362
- if (data.reels) r.addReels(data.reels)
363
- if (data.imagine) r.addImagine(typeof data.imagine === 'object' ? data.imagine : { url: data.imagine })
364
- if (data.sources) r.addSource(data.sources)
365
- if (data.richMapSources) r.addSources(data.richMapSources)
366
- else if (data.botSources) r.addSources(data.botSources)
367
- if (data.richMap) r.addRichMap(data.richMap)
368
- if (data.dynamic) r.addDynamic(data.dynamic)
369
- if (data.suggestPrompts) r.addSuggestPrompts(Array.isArray(data.suggestPrompts) ? data.suggestPrompts : data.suggestPrompts.suggestions, data.suggestPrompts)
370
- if (data.pttTranscript) r.addPttTranscript(data.pttTranscript)
371
- if (data.instagramProfile) r.addInstagramProfile(data.instagramProfile)
372
- if (data.latex) { if (data.latexText) r.addText(data.latexText); (Array.isArray(data.latex) ? data.latex : [data.latex]).forEach(l => r.addLatex({ expression: l.expression ?? l.latexExpression ?? '', imageUrl: l.url ?? l.imageUrl ?? '', width: l.width ?? 400, height: l.height ?? 200 })) }
373
- if (data.html) r.addHtml(data.html, data.trustedSources)
374
- if (data.richHtml) r.addRichHtml(data.richHtml, data.richHtmlTitle)
375
- if (data.copyText) r.addCopyAction(data.copyText, data.copyLabel, data.copyAlignment)
376
- if (data.social) r.addSocialProfile(data.social)
377
- if (data.suggest) r.addSuggest(data.suggest)
378
- if (data.tip) r.addTip(data.tip)
379
- if (data.metadata ?? data.meta) r.addMetadata(data.metadata ?? data.meta)
380
- if (data.footer) r.addFOAText(data.footer)
381
- if (data.rawSections) r.addRawSections(data.rawSections, data.rawSubmessages)
382
- return r.send(jid, opts)
383
- }
384
-
385
- const stripDevice = jid => String(jid || '').replace(/:\d+$/, '')
386
-
387
- const collectMetaAIBotJids = async (sock, jid, botUser) => {
388
- const candidates = new Set([botUser])
389
- if (!isJidGroup(jid)) return candidates
390
- const botNumber = jidNormalizedUser(botUser).split('@')[0]
391
- try {
392
- const meta = await sock.groupMetadata(jid)
393
- for (const p of meta.participants || []) {
394
- const idNum = jidNormalizedUser(p.id).split('@')[0]
395
- const pnNum = p.phoneNumber ? jidNormalizedUser(p.phoneNumber).split('@')[0] : undefined
396
- if (pnNum === botNumber || idNum === botNumber || isJidMetaAI(p.id)) candidates.add(p.id)
397
- }
398
- } catch { }
399
- return candidates
400
- }
401
-
402
- const isMetaAIResponse = (msg, chatJid, promptId, botJids) => {
403
- if (!msg?.key || msg.key.fromMe || msg.key.remoteJid !== chatJid) return false
404
- const sender = stripDevice(msg.key.participant || msg.key.remoteJid)
405
- if (isJidMetaAI(sender)) return true
406
- for (const b of botJids) if (jidNormalizedUser(stripDevice(b)) === jidNormalizedUser(sender)) return true
407
- if (promptId) { const inner = msg.message ? Object.values(msg.message)[0] : undefined; const stanzaId = inner?.contextInfo?.stanzaId || msg.message?.contextInfo?.stanzaId; if (stanzaId === promptId) return true }
408
- return false
409
- }
410
-
411
- const q = quoted => quoted ? { quoted } : {}
412
-
413
- // ─── attachAIRich ─────────────────────────────────────────────────────────────
414
- // Attaches all AIRich, meta, bot, and relay methods to result, then returns the
415
- // fuzzy-proxy-wrapped result. This is the only export nexus-handler.js needs.
416
- export function attachAIRich(result, config) {
417
- const sock = result
418
-
419
- const relayRichMessage = async (jid, fullMsg, options = {}) => {
420
- const relayOpts = { messageId: fullMsg.key.id, useCachedGroupMetadata: options.useCachedGroupMetadata, statusJidList: options.statusJidList, ...(options.relayOptions || {}) }
421
- await sock.relayMessage(jid, fullMsg.message, relayOpts)
422
- const hasRich = !!fullMsg.message?.botForwardedMessage?.message?.richResponseMessage || !!fullMsg.message?.interactiveMessage?.bloksWidget
423
- if (options.renderRichResponse !== false && (hasRich || options.forceRichEdit)) {
424
- const editContent = proto.Message.fromObject({ botForwardedMessage: { message: { protocolMessage: { key: { remoteJid: jid, fromMe: true, id: fullMsg.key.id }, type: proto.Message.ProtocolMessage.Type.MESSAGE_EDIT, editedMessage: fullMsg.message } } } })
425
- const editMsg = generateWAMessageFromContent(jid, editContent, { userJid: sock.user?.id, messageId: generateMessageIDV2(sock.user?.id || jid) })
426
- await sock.relayMessage(jid, editMsg.message, { ...relayOpts, messageId: editMsg.key.id })
427
- }
428
- return fullMsg
429
- }
430
-
431
- result.relayRichMessage = relayRichMessage
432
- result.airich = opts => new AIRich(result, opts)
433
-
434
- result.captureAiRich = msg => {
435
- const rich = msg?.botForwardedMessage?.message?.richResponseMessage ?? msg?.richResponseMessage
436
- if (!rich?.unifiedResponse?.data) return null
437
- return { submessages: rich.submessages ?? [], sections: JSON.parse(Buffer.from(rich.unifiedResponse.data, 'base64').toString()), contextInfo: rich.contextInfo ?? {}, messageType: rich.messageType ?? 1 }
438
- }
439
-
440
- result.relayAiRich = (jid, captured, opts = {}) => {
441
- const r = new AIRich(result)
442
- if (captured.sections?.response_id) r.setResponseId(captured.sections.response_id)
443
- return r.loadFrom({ botForwardedMessage: { message: { richResponseMessage: { submessages: captured.submessages, unifiedResponse: { data: Buffer.from(JSON.stringify(captured.sections)) }, contextInfo: captured.contextInfo, messageType: captured.messageType } } } }).send(jid, opts)
444
- }
445
-
446
- result.sendRichFromObject = (jid, data, opts = {}) => aiRichFromObject(result, jid, data, opts)
447
- result.sendRichMessage = (jid, data, quoted, opts = {}) => result.sendMessage(jid, { aiRich: data }, { ...q(quoted), ...opts })
448
- result.sendCodeBlock = (jid, code, quoted, opts = {}) => result.sendMessage(jid, { aiRich: { code, language: opts.language ?? 'javascript', title: opts.title } }, q(quoted))
449
- result.sendCodeBlockV2 = (jid, code, quoted, opts = {}) => result.sendMessage(jid, { aiRich: { text: opts.text, code, language: opts.language ?? '', title: opts.title } }, q(quoted))
450
- result.sendTable = (jid, title, headers, rows, quoted, opts = {}) => result.sendMessage(jid, { aiRich: { title, table: [headers, ...rows] } }, q(quoted))
451
- result.sendTableV2 = (jid, tableArray, quoted, opts = {}) => result.sendMessage(jid, { aiRich: { text: opts.text, title: opts.headerText, table: tableArray } }, q(quoted))
452
- result.sendList = (jid, title, items, quoted, opts = {}) => result.sendMessage(jid, { aiRich: { title, table: [['Key', 'Value'], ...items] } }, q(quoted))
453
- result.sendLink = (jid, text, links, quoted, opts = {}) => result.sendMessage(jid, { aiRich: { title: opts.headerText, text, sources: links.map(url => ({ url, title: url, subtitle: new URL(url).hostname })) } }, q(quoted))
454
- result.sendLinkV2 = (jid, text, links, quoted, opts = {}) => result.sendMessage(jid, { aiRich: { title: opts.headerText, text, sources: links } }, q(quoted))
455
- result.sendLatex = (jid, expressions, quoted, opts = {}) => result.sendMessage(jid, { aiRich: { text: opts.text, title: opts.headerText, latex: expressions } }, q(quoted))
456
- result.sendRichTable = async (jid, title, headers, rows, quoted, opts = {}) => { const { message, messageId } = generateTableContent(title, headers, rows, quoted, opts); return relayRichMessage(jid, generateWAMessageFromContent(jid, message, { userJid: sock.user?.id, messageId: opts.messageId || messageId }), opts) }
457
- result.sendRichList = async (jid, title, items, quoted, opts = {}) => { const { message, messageId } = generateListContent(title, items, quoted, opts); return relayRichMessage(jid, generateWAMessageFromContent(jid, message, { userJid: sock.user?.id, messageId: opts.messageId || messageId }), opts) }
458
- result.sendRichCode = async (jid, code, quoted, opts = {}) => { const { message, messageId } = generateCodeBlockContent(code, quoted, opts); return relayRichMessage(jid, generateWAMessageFromContent(jid, message, { userJid: sock.user?.id, messageId: opts.messageId || messageId }), opts) }
459
- result.sendRichLatex = async (jid, quoted, opts = {}) => { const { message, messageId } = generateLatexContent(quoted, { expressions: [], ...opts }); return relayRichMessage(jid, generateWAMessageFromContent(jid, message, { userJid: sock.user?.id, messageId: opts.messageId || messageId }), opts) }
460
- result.sendRichLatexImage = async (jid, opts = {}) => {
461
- const { text, expressions = [], headerText, footer } = opts
462
- const rendered = expressions.map(expr => { const latex = expr.latexExpression || expr.expression || expr; const url = `https://latex.codecogs.com/png.latex?${encodeURIComponent(latex).replace(/'/g, '%27')}`; return { latexExpression: latex, url, width: expr.width || 400, height: expr.height || 100 } })
463
- const { message, messageId } = generateLatexContent(null, { text, expressions: rendered, headerText, footer })
464
- return relayRichMessage(jid, generateWAMessageFromContent(jid, message, { userJid: sock.user?.id, messageId: opts.messageId || messageId }), opts)
465
- }
466
- result.captureAndResendRichResponse = async (jid, metaAiMsg, quoted, opts = {}) => {
467
- const captured = captureUnifiedResponse(metaAiMsg)
468
- if (!captured) throw new Error('captureAndResendRichResponse: no unifiedResponse data in message')
469
- const { message, messageId } = generateUnifiedResponseContent(quoted, captured)
470
- return relayRichMessage(jid, generateWAMessageFromContent(jid, message, { userJid: sock.user?.id, messageId: opts.messageId || messageId }), opts)
471
- }
472
- result.sendBotPlanning = async (jid, content = {}, opts = {}) => {
473
- const { text = 'Execution plan', steps = [], queryPlan = true, botJid = META_AI_BOT_JID } = content
474
- const submessages = [{ messageType: proto.AIRichResponseSubMessageType.AI_RICH_RESPONSE_TEXT, messageText: text }, ...steps.map(s => ({ messageType: proto.AIRichResponseSubMessageType.AI_RICH_RESPONSE_TEXT, messageText: typeof s === 'string' ? s : `${s.title || s.name || 'Step'}${s.body ? `: ${s.body}` : ''}` }))]
475
- const wrapped = buildBotRichResponse({ botJid, submessages, capabilities: [BotCapabilityType.AGENTIC_PLANNING, ...(queryPlan ? [BotCapabilityType.QUERY_PLAN] : [])] })
476
- const full = await generateWAMessageFromContent(jid, wrapped, { userJid: sock.user?.id, messageId: opts.messageId || generateMessageIDV2(sock.user?.id) })
477
- return relayRichMessage(jid, full, opts)
478
- }
479
- result.sendBotPlan = async (jid, content = {}, opts = {}) => {
480
- const { text = 'Execution plan', title = 'Execution plan', steps = [], estimatedCompletionTime, botJid = META_AI_BOT_JID } = content
481
- if (!Array.isArray(steps) || steps.length === 0) throw new Error('sendBotPlan requires at least one step')
482
- const statusEnum = proto.BotProgressIndicatorMetadata.BotPlanningStepMetadata.PlanningStepStatus
483
- const normalizedSteps = steps.map((s, i) => ({ statusTitle: String(s.title || s.name || `Step ${i + 1}`), statusBody: String(s.body || s.description || ''), status: s.status === 'executing' ? statusEnum.EXECUTING : (s.status === 'finished' || s.status === 'completed') ? statusEnum.FINISHED : statusEnum.PLANNED, isReasoning: s.isReasoning === true, isEnhancedSearch: s.isEnhancedSearch === true }))
484
- const wrapped = { messageContextInfo: { botMetadata: { capabilityMetadata: { capabilities: [proto.BotCapabilityMetadata.BotCapabilityType.AGENTIC_PLANNING, proto.BotCapabilityMetadata.BotCapabilityType.QUERY_PLAN] }, progressIndicatorMetadata: { progressDescription: title, stepsMetadata: normalizedSteps, ...(estimatedCompletionTime ? { estimatedCompletionTime } : {}) } } }, botForwardedMessage: { message: { richResponseMessage: { messageType: 1, submessages: [{ messageType: 2, messageText: text }], contextInfo: { isForwarded: true, forwardingScore: 1, forwardedAiBotMessageInfo: { botJid }, forwardOrigin: 4 } } } } }
485
- const full = await generateWAMessageFromContent(jid, wrapped, { userJid: sock.user?.id, messageId: opts.messageId || generateMessageIDV2(sock.user?.id) })
486
- await sock.relayMessage(jid, full.message, { messageId: full.key.id })
487
- return full
488
- }
489
- result.replayPlanning = (jid, steps, finalContent, opts) => replayPlanning(result, jid, steps, finalContent, opts)
490
- result.replayPlanningOnly = (jid, steps, opts) => replayPlanningOnly(result, jid, steps, opts)
491
- result.metaTyping = (jid, opts) => metaTyping(result, jid, opts)
492
- result.sendMetaComposited = (jid, content, opts) => sendMetaComposited(result, jid, content, opts)
493
- result.buildReasoningSteps = buildReasoningSteps
494
- result.buildSearchSteps = buildSearchSteps
495
- result.mixedSteps = mixedSteps
496
- result.buildSteps = buildSteps
497
- result.PlanningStepStatus = PlanningStepStatus
498
- result.sendMetaAI = async (a, b, c = {}) => {
499
- let text, opts = c, yourJid
500
- if (typeof b === 'string') { text = b; yourJid = a } else { text = a; opts = b || {}; yourJid = opts.yourJid || '' }
501
- const jid = opts.jid || META_AI_BOT_JID
502
- const threadId = opts.threadId || generateMessageIDV2(yourJid)
503
- const now = Date.now()
504
- const senderKeyHash = opts.senderKeyHash || randomBytes(8).toString('base64')
505
- const message = {
506
- extendedTextMessage: proto.Message.ExtendedTextMessage.fromObject({ text, previewType: 'NONE', contextInfo: proto.ContextInfo.fromObject({ botMessageSharingInfo: { botEntryPointOrigin: 'FAVICON', forwardScore: 0 } }), inviteLinkGroupTypeV2: 'DEFAULT' }),
507
- messageContextInfo: proto.MessageContextInfo.fromObject({ deviceListMetadata: { senderKeyHash, senderTimestamp: opts.senderTimestamp || String(Math.floor(now / 1000)) }, deviceListMetadataVersion: 2, messageSecret: opts.messageSecret || randomBytes(32), botMetadata: { botModeSelectionMetadata: { overrideMode: [0] }, botThreadInfo: { serverInfo: { title: text.substring(0, 50) }, clientInfo: { type: 'DEFAULT' } }, botRenderingConfigMetadata: { bloksVersioningId: '1eb86e6f4117d052e6bab62fe758a2e2af43747b85c5c1a886c8262bac462ea4', pixelDensity: 2.625 }, ...(opts.conversationContext?.length ? { aiConversationContext: opts.conversationContext } : {}) }, threadId: [{ threadType: 'AI_THREAD', threadKey: { remoteJid: '0002@s.whatsapp.net', fromMe: true, id: threadId } }] })
508
- }
509
- const msgId = generateMessageIDV2(yourJid)
510
- await sock.relayMessage(jid, message, { messageId: msgId, ...(opts.quoted ? { quoted: opts.quoted } : {}) })
511
- return msgId
512
- }
513
- result.aiPrompt = async (jid, prompt, options = {}) => {
514
- const { timeout = 60_000, onPartial, botUser = META_AI_BOT_JID, mentions = [], ...sendOpts } = options
515
- if (!jid || typeof prompt !== 'string' || !prompt.trim()) throw new Error('aiPrompt requires a chat JID and a non-empty prompt')
516
- const botJids = await collectMetaAIBotJids(result, jid, botUser)
517
- const allMentions = [...new Set([...mentions, ...botJids])]
518
- return new Promise((resolve, reject) => {
519
- let promptId, settled = false
520
- const cleanup = () => { clearTimeout(timer); sock.ev.off('messages.upsert', onUpsert); if (onPartial) sock.ev.off('messages.update', onUpdate) }
521
- const settle = (fn, val) => { if (settled) return; settled = true; cleanup(); fn(val) }
522
- const timer = setTimeout(() => settle(reject, new Error(`aiPrompt timed out after ${timeout}ms in ${jid}`)), timeout)
523
- const onUpsert = ({ messages }) => { for (const msg of messages) if (isMetaAIResponse(msg, jid, promptId, botJids)) { settle(resolve, msg); return } }
524
- const onUpdate = ({ updates }) => { if (!onPartial) return; for (const u of updates) { if (!u.key || u.key.fromMe || u.key.remoteJid !== jid) continue; const sender = jidNormalizedUser(stripDevice(u.key.participant || u.key.remoteJid)); const isBot = isJidMetaAI(sender) || [...botJids].some(b => jidNormalizedUser(stripDevice(b)) === sender); if (isBot && u.message) onPartial(u.message, u.key) } }
525
- sock.ev.on('messages.upsert', onUpsert)
526
- if (onPartial) sock.ev.on('messages.update', onUpdate)
527
- ; (async () => {
528
- try {
529
- const content = { text: prompt, ...(isJidGroup(jid) ? { mentions: allMentions } : {}), ...sendOpts }
530
- const full = await generateWAMessage(jid, content, { userJid: sock.user?.id, messageId: sendOpts.messageId || generateMessageIDV2(sock.user?.id) })
531
- await sock.relayMessage(jid, full.message, { messageId: full.key.id })
532
- if (config.emitOwnEvents) process.nextTick(() => sock.ev.emit('messages.upsert', { messages: [full], type: 'append' }))
533
- promptId = full.key.id
534
- } catch (err) { settle(reject, err) }
535
- })()
536
- })
537
- }
538
- result.sendAsMimic = async (jid, content, mimicJid, options = {}) => {
539
- if (!options.admin && !options.mimicPermission) throw new Error('sendAsMimic requires admin: true')
540
- const msgId = generateMessageIDV2(sock.user?.id)
541
- const full = await generateWAMessageFromContent(jid, content, { userJid: sock.user?.id, ...options })
542
- full.key = { remoteJid: jid, fromMe: false, id: msgId, participant: mimicJid }
543
- await sock.relayMessage(jid, full.message, { messageId: msgId, ...(options.additionalAttributes ? { additionalAttributes: options.additionalAttributes } : {}) })
544
- return full
545
- }
546
- result.sendStatus = async (content, options = {}) => {
547
- const provided = options.statusJidList || content?.statusJidList || sock.statusJidList || []
548
- if (!provided.length) throw new Error('sendStatus requires statusJidList')
549
- return result.sendMessage(STATUS_JID, { ...content, status: true }, { ...options, broadcast: true, statusJidList: provided })
550
- }
551
- result.sendBotToolResult = async (jid, content = {}, opts = {}) => {
552
- const { text = 'Tool result', toolCallId = `tool-${Date.now()}`, resolutionData, resolutionDataSerialized, botJid = META_AI_BOT_JID } = content
553
- const wrapped = buildBotRichResponse({ text, botJid, capabilities: [BotCapabilityType.AGENTIC_PLANNING, BotCapabilityType.QUERY_PLAN], botMetadata: { resolvedToolCallMetadata: { toolCallId, resolutionDataSerialized: resolutionDataSerialized ?? JSON.stringify(resolutionData ?? {}) } } })
554
- const full = await generateWAMessageFromContent(jid, wrapped, { userJid: sock.user?.id, messageId: opts.messageId || generateMessageIDV2(sock.user?.id) })
555
- return relayRichMessage(jid, full, opts)
556
- }
557
- result.sendWhatsAppFlow = async (jid, flow, options = {}) => {
558
- const { text, footer, image, caption, ...flowOpts } = flow || {}
559
- const btn = makeWhatsAppFlowButton(flowOpts)
560
- const content = image ? { image, caption: caption || text || flowOpts.cta } : { text: text || flowOpts.cta || '' }
561
- return result.sendMessage(jid, { ...content, ...(footer ? { footer } : {}), nativeFlow: [btn] }, options)
562
- }
563
- result.sendRichMap = (jid, mapOpts, options = {}) => { const r = new AIRich(result); r.addRichMap(mapOpts); return r.send(jid, options) }
564
- result.sendDynamic = (jid, dynOpts, options = {}) => { const r = new AIRich(result); r.addDynamic(dynOpts); return r.send(jid, options) }
565
- result.sendBotPromptSuggestions = (jid, content = {}, options = {}) => { const { text, suggestions = [] } = content; const r = new AIRich(result); if (text) r.addText(text); r.addSuggestPrompts(suggestions, content); return r.send(jid, options) }
566
- result.sendBotSources = (jid, content = {}, options = {}) => { const { text, sources = [] } = content; const r = new AIRich(result); if (text) r.addText(text); r.addSources(sources); return r.send(jid, options) }
567
- result.sendBotPttTranscript = (jid, content = {}, options = {}) => { const { text, transcript } = content; const r = new AIRich(result); if (text) r.addText(text); r.addPttTranscript(transcript); return r.send(jid, options) }
568
- result.sendInstagramProfile = (jid, data, options = {}) => { const r = new AIRich(result); r.addInstagramProfile(data); return r.send(jid, options) }
569
- result.sendRichGeneration = async (jid, opts = {}) => { const { message, responseId, itemId } = prepareRichGenerationMessage(opts); const full = await generateWAMessageFromContent(jid, message, { userJid: sock.user?.id, messageId: opts.messageId || generateMessageIDV2(sock.user?.id) }); await sock.relayMessage(jid, full.message, { messageId: full.key.id }); return { msg: full, responseId, itemId } }
570
- result.sendUnifiedResponse = (jid, sections, submessages = [], options = {}) => { const r = new AIRich(result); r.addRawSections(sections, submessages); return r.send(jid, options) }
571
- result.sendRichButtonGrid = async (jid, grid, options = {}) => {
572
- const { text, footer, cards = [] } = grid || {}
573
- if (!Array.isArray(cards) || cards.length === 0) throw new Error('sendRichButtonGrid expects at least one card')
574
- return result.sendMessage(jid, { ...(text ? { text } : {}), ...(footer ? { footer } : {}), cards }, options)
575
- }
576
-
577
- const normalize = s => s.toLowerCase().replace(/[^a-z]/g, '')
578
- const knownKeys = Object.keys(result).filter(k => typeof result[k] === 'function')
579
- const normalizedMap = new Map(knownKeys.map(k => [normalize(k), k]))
580
-
581
- return new Proxy(result, {
582
- get(target, prop, receiver) {
583
- if (prop in target) return Reflect.get(target, prop, receiver)
584
- if (typeof prop !== 'string') return undefined
585
- const match = normalizedMap.get(normalize(prop))
586
- if (match) { target.logger?.warn?.(`[NexusHandler] Unknown method "${prop}" — did you mean "${match}"? Calling it.`); return target[match] }
587
- return undefined
588
- }
589
- })
1
+ import crypto from 'crypto'
2
+ import { proto } from '../../WAProto/index.js'
3
+ import {
4
+ generateMessageIDV2, generateVerificationMetadata, waitAllPromises, extractIE, tokenizeCode, generateWAMessageFromContent, generateWAMessage,
5
+ makeWhatsAppFlowButton, metaTyping, sendMetaComposited, PlanningStepStatus, buildSteps, replayPlanning, replayPlanningOnly, buildReasoningSteps, buildSearchSteps,
6
+ mixedSteps, buildBotRichResponse, BotCapabilityType, generateTableContent, generateListContent, generateCodeBlockContent,
7
+ generateLatexContent, captureUnifiedResponse, generateUnifiedResponseContent
8
+ } from '../Utils/index.js'
9
+ import { isJidGroup, isJidMetaAI, jidNormalizedUser } from '../WABinary/index.js'
10
+ import { BOT_RENDERING_CONFIG_METADATA } from '../Defaults/index.js'
11
+ import { randomBytes } from 'crypto'
12
+
13
+ const DEFAULT_BOT_JID = '867051314767696@bot'
14
+ const META_AI_BOT_JID = '867051314767696@bot'
15
+ const STATUS_JID = 'status@broadcast'
16
+ const BIZ_BOT_SUPPORT = '{"version":1,"is_ai_message":true,"should_upload_client_logs":false,"should_show_system_message":false,"ticket_id":"7004947587700716","citation_items":[],"ticket_locale":"us"}'
17
+
18
+ const mkSection = vm => ({ __typename: 'GenAIUnifiedResponseSection', view_model: vm })
19
+ const single = p => mkSection({ __typename: 'GenAISingleLayoutViewModel', primitive: p })
20
+ const vstack = ps => mkSection({ __typename: 'GenAIVStackLayoutViewModel', primitives: ps })
21
+ const hscroll = ps => mkSection({ __typename: 'GenAIHScrollLayoutViewModel', primitives: ps })
22
+ const grid = ps => mkSection({ __typename: 'GenAIGridLayoutViewModel', primitives: ps })
23
+ const actionRow = ps => ({ view_model: { __typename: 'GenAIActionRowLayoutViewModel', primitives: ps } })
24
+ const addonAction = (type, ps, alignment = 'END') => mkSection({ __typename: 'GenAIAddonActionLayoutViewModel', addon_action_type: type, addon_action_alignment: alignment, primitives: ps })
25
+
26
+ const mkMarkdown = (text, entities = []) => { const { text: t, inline_entities } = extractIE(text, entities); return { __typename: 'GenAIMarkdownTextUXPrimitive', text: t, inline_entities } }
27
+ const mkCode = (content, language = '', unified_codeBlock = []) => ({ __typename: 'GenAICodeUXPrimitive', language, code_blocks: unified_codeBlock.length ? unified_codeBlock : [{ content }] })
28
+
29
+ const MAX_TABLE_ROWS = 5000, MAX_TABLE_COLUMNS = 200, MAX_TABLE_CELLS = 100_000
30
+ const mkTable = (rows, title = '') => {
31
+ if (!Array.isArray(rows) || rows.length === 0) throw new TypeError('mkTable requires a non-empty rows array')
32
+ if (rows.length > MAX_TABLE_ROWS) throw new RangeError(`mkTable: too many rows (${rows.length} > ${MAX_TABLE_ROWS})`)
33
+ const firstRow = Array.isArray(rows[0]) ? rows[0] : (rows[0].cells ?? rows[0].items ?? [])
34
+ if (firstRow.length > MAX_TABLE_COLUMNS) throw new RangeError(`mkTable: too many columns (${firstRow.length} > ${MAX_TABLE_COLUMNS})`)
35
+ if (rows.length * firstRow.length > MAX_TABLE_CELLS) throw new RangeError(`mkTable: too many cells (${rows.length * firstRow.length} > ${MAX_TABLE_CELLS})`)
36
+ const normalized = Array.isArray(rows[0])
37
+ ? rows.map((r, i) => ({ is_header: i === 0, cells: r.map(c => (typeof c === 'string' ? c : String(c))), markdown_cells: r.map(c => ({ text: typeof c === 'string' ? c : String(c) })) }))
38
+ : rows.map(r => ({ is_header: !!r.is_header, cells: (r.cells ?? r.items ?? []).map(c => (typeof c === 'string' ? c : String(c))), markdown_cells: (r.cells ?? r.items ?? []).map(c => ({ text: typeof c === 'string' ? c : String(c) })) }))
39
+ return { __typename: 'GenATableUXPrimitive', title, rows: normalized }
40
+ }
41
+
42
+ const mkImage = (input, expiration) => {
43
+ const img = typeof input === 'string' ? { url: input } : input
44
+ const exp = expiration ?? String(Date.now() + 30 * 24 * 60 * 60 * 1000)
45
+ const first = (...vals) => vals.find(v => v != null && String(v).length > 0)
46
+ const previewUrl = first(img.previewUrl, img.preview_url, img.imagePreviewUrl, img.url) ?? ''
47
+ const fullUrl = first(img.highResUrl, img.high_res_url, img.imageHighResUrl, img.fullUrl, img.full_url, img.url, previewUrl)
48
+ const darkPreview = first(img.darkModePreviewUrl, img.dark_mode_preview_url, previewUrl)
49
+ const darkFull = first(img.darkModeHighResUrl, img.dark_mode_high_res_url, darkPreview)
50
+ const mime = img.mimeType ?? img.mime_type ?? (/\.png$/i.test(img.url ?? '') ? 'image/png' : 'image/jpeg')
51
+ const w = Number(img.width ?? 600), h = Number(img.height ?? 400)
52
+ const media = (url, mw, mh) => ({ url, url_fallback: url, mime_type: mime, expiration_timestamp_ms: exp, width: mw, height: mh })
53
+ return { __typename: 'GenAIImagePrimitive', preview_image: media(previewUrl, w, h), full_image: media(fullUrl, w, h), dark_mode_preview_image: media(darkPreview, w, h), dark_mode_full_image: media(darkFull, w, h), asset_query_status: 'FETCHED' }
54
+ }
55
+
56
+ const mkVideo = url => ({ __typename: 'GenAIVideoPrimitive', media: { __typename: 'GenAIMediaItem', mime_type: 'video/mp4', url } })
57
+ const mkReel = ({ creator, avatar_url, thumbnail_url, reels_url, title, likes_count, shares_count, view_count, reel_source, is_verified }) => ({ __typename: 'GenAIReelPrimitive', creator: creator ?? '', avatar_url: avatar_url ?? '', thumbnail_url: thumbnail_url ?? '', reels_url: reels_url ?? '', reels_title: title ?? '', likes_count: likes_count ?? 0, shares_count: shares_count ?? 0, view_count: view_count ?? 0, reel_source: reel_source ?? 'IG', is_verified: !!is_verified })
58
+ const mkPost = d => ({ __typename: 'GenAIPostPrimitive', title: d.title ?? '', username: d.username ?? '', subtitle: d.subtitle ?? '', thumbnail_url: d.thumbnail_url ?? '', post_url: d.post_url ?? d.url ?? '', post_caption: d.post_caption ?? d.caption ?? '', post_type: d.post_type ?? 'photo', source_app: d.source_app ?? 'instagram', likes_count: d.likes_count ?? 0, comments_count: d.comments_count ?? 0, shares_count: d.shares_count ?? 0, is_verified: d.is_verified ?? false, is_carousel: d.is_carousel ?? false, orientation: d.orientation ?? 'portrait', profile_picture_url: d.profile_picture_url ?? null, footer_label: d.footer_label ?? null, footer_icon: d.footer_icon ?? null, additional_images: d.additional_images ?? [] })
59
+ const mkProduct = d => ({ __typename: 'GenAIProductItemCardPrimitive', title: d.title ?? '', brand: d.brand ?? '', price: d.price ?? '', sale_price: d.sale_price ?? null, product_url: d.product_url ?? d.url ?? '', image: { url: d.image_url ?? d.image ?? '' }, additional_images: [] })
60
+ const mkSearchResult = d => ({ __typename: 'GenAISearchResultPrimitive', source_url: d.url ?? d.source_url ?? '', source_display_name: d.title ?? d.source_display_name ?? '', source_type: d.source_type ?? 'web', source_subtitle: d.subtitle ?? d.source_subtitle ?? '', favicon: d.favicon ?? null })
61
+ const mkMap = ({ latitude, longitude, name = '', address = '' }) => ({ __typename: 'GenAIMapPrimitive', latitude, longitude, name, address })
62
+
63
+ const mkRichMap = ({ centerLatitude, centerLongitude, latitudeDelta, longitudeDelta, annotations = [], showInfoList = false }) => {
64
+ if (centerLatitude == null || centerLongitude == null) throw new TypeError('addRichMap requires centerLatitude and centerLongitude')
65
+ const submessage = { messageType: proto.AIRichResponseSubMessageType.AI_RICH_RESPONSE_MAP, mapMetadata: { centerLatitude, centerLongitude, ...(latitudeDelta != null ? { latitudeDelta } : {}), ...(longitudeDelta != null ? { longitudeDelta } : {}), showInfoList: !!showInfoList, annotations: annotations.map((a, i) => ({ annotationNumber: a.annotationNumber ?? i + 1, latitude: a.latitude, longitude: a.longitude, title: a.title ?? '', body: a.body ?? '' })) } }
66
+ const displayLat = annotations[0]?.latitude ?? centerLatitude, displayLng = annotations[0]?.longitude ?? centerLongitude
67
+ return { submessage, section: single({ __typename: 'GenAIMapPrimitive', latitude: displayLat, longitude: displayLng, name: annotations[0]?.title ?? '', address: annotations[0]?.body ?? '' }) }
68
+ }
69
+
70
+ const mkRichMenu = ({ header = {}, body = {}, footer = {} }) => {
71
+ const sections = []
72
+ if (header?.title) sections.push(single({ __typename: 'FOATextPrimitive', text: '# ' + header.title }))
73
+ if (header?.image?.url) sections.push(single(header.image.inline ? mkInlineImage(header.image.url) : mkImage(header.image)))
74
+ if (header?.disclaimer) sections.push(single(mkMarkdown(header.disclaimerText || ' ')))
75
+ const cards = Array.isArray(body?.cards) ? body.cards : []
76
+ const buttons = Array.isArray(body?.buttons) ? body.buttons : []
77
+ if (cards.length) sections.push({ view_model: { primitives: cards.map(card => mkWidget(Array.isArray(card?.buttons) ? card.buttons : [], card?.title || '')), __typename: body.carousel ? 'GenAIHScrollLayoutViewModel' : 'GenAIActionRowLayoutViewModel' } })
78
+ else if (buttons.length) sections.push(single(mkWidget(buttons, body.title || '')))
79
+ if (footer?.text || footer?.url) sections.push({ view_model: { primitives: [mkFooterAction({ text: footer.text, url: footer.url })], __typename: 'GenAIActionRowLayoutViewModel' } })
80
+ return sections
81
+ }
82
+
83
+ const mkDynamic = ({ url, type = 'GIF', version, loopCount } = {}) => {
84
+ if (!url) throw new TypeError('addDynamic requires a url')
85
+ const dynType = String(type).toUpperCase() === 'IMAGE' ? proto.AIRichResponseDynamicMetadata.AIRichResponseDynamicMetadataType.AI_RICH_RESPONSE_DYNAMIC_METADATA_TYPE_IMAGE : proto.AIRichResponseDynamicMetadata.AIRichResponseDynamicMetadataType.AI_RICH_RESPONSE_DYNAMIC_METADATA_TYPE_GIF
86
+ const submessage = { messageType: proto.AIRichResponseSubMessageType.AI_RICH_RESPONSE_DYNAMIC, dynamicMetadata: { type: dynType, url, ...(version != null ? { version } : {}), ...(loopCount != null ? { loopCount } : {}) } }
87
+ const mimeType = dynType === proto.AIRichResponseDynamicMetadata.AIRichResponseDynamicMetadataType.AI_RICH_RESPONSE_DYNAMIC_METADATA_TYPE_IMAGE ? 'image/jpeg' : 'image/gif'
88
+ return { submessage, section: single({ __typename: 'GenAIDynamicMediaPrimitive', url, mime_type: mimeType, ...(loopCount != null ? { loop_count: loopCount } : {}) }) }
89
+ }
90
+
91
+ const mkWidget = (ctas, title = '') => ({ __typename: 'GenAI3PExtWidgetPrimitive', header: { __typename: 'GenAI3PExtWidgetStandardHeader', title }, body: { __typename: 'GenAI3PExtCalendarEventList', ctas: ctas.map(c => ({ __typename: 'GenAI3PExtWidgetCTA', label: c.label, state: 'PENDING', kind: c.kind ?? 'OTHER', tool_call_id: c.tool_call_id ?? c.id ?? crypto.randomBytes(8).toString('hex'), toast: { __typename: 'GenAI3PExtWidgetToast', label: c.toast ?? '' } })), sections: [] } })
92
+ const mkFooterAction = d => ({ __typename: 'GenAIFooterActionPrimitive', cta_text: d.cta_text ?? d.text ?? '', cta_type: d.cta_type ?? 'OPEN_URL', cta_url: d.cta_url ?? d.url ?? '' })
93
+
94
+ const mkSocialProfile = ({ username = '', platform = 'GENERIC', title = '', subtitle = '', imageUrl = '', entityId, entityUrl, entityType, fullName = '', isVerified = false, resultText = 'See results' }) => {
95
+ if (!username || typeof username !== 'string') throw new TypeError('addSocialProfile requires a username')
96
+ const normalizedPlatform = String(platform).toUpperCase()
97
+ const profileUrl = entityUrl ?? (username ? `https://www.${platform.toLowerCase()}.com/${encodeURIComponent(username)}` : '')
98
+ const resolvedType = entityType ?? (normalizedPlatform === 'INSTAGRAM' ? 'IG_PROFILE' : 'SOCIAL_PROFILE')
99
+ const image = imageUrl ? { url: imageUrl, mime_type: 'image/png' } : undefined
100
+ return [
101
+ { view_model: { primitives: [{ __typename: 'GenAICompactEntityPrimitive', title, subtitle, ...(image ? { image } : {}), entity_id: String(entityId ?? username ?? ''), entity_url: profileUrl, entity_type: 'WEBSITE', action_type: 'OPEN_URL', is_verified: !!isVerified }], __typename: 'GenAIActionRowLayoutViewModel' } },
102
+ { view_model: { primitives: [{ type: 'HORIZONTAL_LINE', __typename: 'GenAIDividerPrimitive' }], __typename: 'GenAIVStackLayoutViewModel' } },
103
+ { view_model: { primitives: [{ __typename: 'GenAISpacerPrimitive' }, { __typename: 'GenAIMarkdownTextUXPrimitive', text: `# {{social_entity_1}}${resultText}\0{{/social_entity_1}} `, inline_entities: [{ key: 'social_entity_1', metadata: { __typename: 'GenAISocialEntityItem', entity_id: String(entityId ?? username ?? ''), entity_name: username, entity_full_name: fullName, platform: normalizedPlatform, entity_picture_url: imageUrl, entity_url: profileUrl, entity_type: resolvedType, is_verified: !!isVerified } }] }, { __typename: 'GenAISpacerPrimitive' }], __typename: 'GenAIActionRowLayoutViewModel' } }
104
+ ]
105
+ }
106
+
107
+ const mkInlineImage = (url, alignment = 'center') => ({ __typename: 'GenAIInlineImageUXPrimitive', image: { __typename: 'GenAIMediaItem', mime_type: 'image/jpeg', url }, alignment })
108
+ const mkInlineLink = (links = []) => { let text = ''; const entities = []; links.forEach((link, i) => { const key = `INLINE_HYPERLINK_${i}`; const display = link.text || link.displayName || link.url || ''; text += `{{${key}}}${display}{{/${key}}}`; entities.push({ key, metadata: { display_name: display, is_trusted: link.isTrusted !== false, url: link.url || '', __typename: 'GenAIInlineLinkItem' } }) }); return { __typename: 'GenAIMarkdownTextUXPrimitive', text, inline_entities: entities } }
109
+ const mkMeta = text => ({ __typename: 'GenAIMetadataTextPrimitive', text })
110
+ const mkFOA = text => ({ __typename: 'FOATextPrimitive', text })
111
+ const mkTip = text => ({ __typename: 'GenAITipUXPrimitive', text })
112
+ const mkDivider = (type = 'HORIZONTAL_LINE') => ({ __typename: 'GenAIDividerPrimitive', divider_type: type })
113
+ const mkSpacer = (spacing = 2) => ({ __typename: 'GenAISpacerPrimitive', spacing })
114
+ const mkThinking = (title, icon = 'THINKING') => ({ __typename: 'GenAIBotThinkingStatusPrimitive', title, icon })
115
+ const mkProgress = (title, inProgress = true) => ({ __typename: 'GenAIBotProgressStatusPrimitive', title, is_in_progress: inProgress, icon: null, meta_search_apps: null, target_secondary_screen_id: null, target_secondary_screen_tab_id: null })
116
+ const mkTask = ({ taskId, title, subtitle = '', status = 'PENDING' }) => ({ __typename: 'GenAITaskPrimitive', task_id: taskId, title, subtitle, status })
117
+ const mkLatex = ({ expression, imageUrl, width = 400, height = 200, fontHeight = 83.333, padding = 15 }) => ({ __typename: 'GenAITextInlineEntity', key: `latex_${crypto.randomUUID().slice(0, 8)}`, metadata: { __typename: 'GenAILatexItem', latex_expression: expression, font_height: fontHeight, padding, latex_image: { __typename: 'GenAIMediaItem', mime_type: 'image/png', url: imageUrl, url_fallback: imageUrl, width, height, expiration_timestamp_ms: Date.now() + 86400000 } } })
118
+ const mkLabel = (text, variant = 'POSITIVE', backgroundColor = '#FF0000FF', textColor = '#000000FF') => ({ __typename: 'GenAILabelPrimitive', text: String(text), variant, background_color_override: backgroundColor, text_color_override: textColor })
119
+ const mkPill = prompt_text => ({ __typename: 'GenAIFollowUpSuggestionPillPrimitive', prompt_text })
120
+ const mkHtml = (html, trustedSources = []) => ({ __typename: 'FOAHtmlPrimitiveDemoDONOTUSE', trusted_sources: trustedSources, payload: String(html).trim() })
121
+ const mkRichHtml = (html, url = '', trustedSources = []) => ({ __typename: 'GenAIaeacdsnwHtmlPrimitive', payload: String(html).trim(), url, trusted_sources: trustedSources })
122
+
123
+ // validates proofs exist before attaching — never fabricate provenance
124
+ export const hasVerifiableProofs = v => Array.isArray(v?.proofs) && v.proofs.length > 0
125
+
126
+ export const wrapToBotForwardedMessage = (richResponseMessage, verificationMetadata) => ({
127
+ messageContextInfo: { botMetadata: { pluginMetadata: {}, ...(hasVerifiableProofs(verificationMetadata) ? { verificationMetadata } : {}), botRenderingConfigMetadata: BOT_RENDERING_CONFIG_METADATA } },
128
+ botForwardedMessage: { message: { richResponseMessage } }
129
+ })
130
+
131
+ export const attachBotVerificationProofs = (envelope, verificationMetadata) => {
132
+ if (!envelope || !hasVerifiableProofs(verificationMetadata)) return envelope
133
+ const botMetadata = envelope?.messageContextInfo?.botMetadata
134
+ if (!botMetadata || botMetadata.verificationMetadata) return envelope
135
+ botMetadata.verificationMetadata = verificationMetadata
136
+ return envelope
137
+ }
138
+
139
+ // only ships previewMedia when all required crypto fields are present — drops incomplete descriptors
140
+ const normalizePreviewMedia = media => {
141
+ if (!media || typeof media !== 'object') return null
142
+ const { fileSha256, mediaKey, fileEncSha256, directPath, mimetype, mediaKeyTimestamp } = media
143
+ if (!fileSha256 || !mediaKey || !fileEncSha256 || !directPath) return null
144
+ return { fileSha256, mediaKey, fileEncSha256, directPath, mediaKeyTimestamp: mediaKeyTimestamp ?? Math.floor(Date.now() / 1000), mimetype: mimetype || 'image/jpeg' }
145
+ }
146
+
147
+ const buildAdditionalBotMetadata = (submessages) => {
148
+ const sources = [], mediaDetailsMetadataList = []
149
+ for (const sub of submessages) {
150
+ if (sub?.contentItemsMetadata?.itemsMetadata) {
151
+ for (const item of sub.contentItemsMetadata.itemsMetadata) {
152
+ const r = item?.reelItem ?? item ?? {}
153
+ sources.push({ provider: 0, thumbnailCdnUrl: r.thumbnailUrl ?? r.thumbnail_url ?? '', sourceProviderUrl: r.videoUrl ?? r.reels_url ?? r.url ?? '', sourceQuery: '', faviconCdnUrl: r.profileIconUrl ?? r.avatar_url ?? '', citationNumber: sources.length + 1, sourceTitle: r.title ?? r.reels_title ?? r.creator ?? '' })
154
+ const previewMedia = normalizePreviewMedia(r.previewMedia)
155
+ if (previewMedia) mediaDetailsMetadataList.push({ id: r.mediaId ?? r.previewMedia?.mediaId ?? crypto.randomBytes(32).toString('hex'), previewMedia })
156
+ }
157
+ }
158
+ if (sub?.latexMetadata?.expressions) {
159
+ for (const expr of sub.latexMetadata.expressions) {
160
+ const previewMedia = normalizePreviewMedia(expr?.previewMedia)
161
+ if (previewMedia) mediaDetailsMetadataList.push({ id: expr.mediaId ?? expr.previewMedia?.mediaId ?? crypto.randomBytes(32).toString('hex'), previewMedia })
162
+ }
163
+ }
164
+ }
165
+ return { sources, mediaDetailsMetadataList }
166
+ }
167
+
168
+ export const buildImaginePrimitive = ({ mediaType = 'video', url = '', thumbnail, mimeType, fileLength = 0, duration = 0, status = 'GENERATING', estimatedMs, imagineType } = {}) => {
169
+ const type = String(mediaType).toLowerCase() === 'image' ? 'IMAGE' : 'ANIMATE'
170
+ const prim = { __typename: 'GenAIImaginePrimitive', media: { url: url ?? '', mime_type: mimeType ?? (type === 'IMAGE' ? 'image/jpeg' : 'video/mp4'), file_length: fileLength ?? 0, duration: duration ?? 0 }, imagine_type: imagineType ?? type, status: { status: String(status).toUpperCase() } }
171
+ if (estimatedMs != null) prim.status.estimated_completion_time = Math.floor((Date.now() + Number(estimatedMs)) / 1000)
172
+ if (thumbnail) prim.thumbnail = { raw_media: thumbnail }
173
+ return prim
174
+ }
175
+
176
+ export const prepareRichGenerationMessage = ({ text = '', mediaType = 'video', url = '', thumbnail, mimeType, fileLength = 0, duration = 0, status = 'GENERATING', estimatedMs, itemId = `imagine_${crypto.randomUUID()}`, responseId = crypto.randomUUID(), verificationMetadata } = {}) => {
177
+ const sections = []
178
+ if (text) sections.push({ view_model: { primitive: { text: String(text), inline_entities: [], __typename: 'GenAIMarkdownTextUXPrimitive' }, __typename: 'GenAISingleLayoutViewModel' } })
179
+ sections.push({ view_model: { primitive: buildImaginePrimitive({ mediaType, url, thumbnail, mimeType, fileLength, duration, status, estimatedMs }), __typename: 'GenAISingleLayoutViewModel', id: itemId } })
180
+ const submessages = text ? [{ messageType: 2, messageText: String(text) }] : []
181
+ const richResponseMessage = proto.AIRichResponseMessage.create({ submessages, messageType: 1, unifiedResponse: { data: Buffer.from(JSON.stringify({ response_id: responseId, sections })) }, contextInfo: { isForwarded: true, forwardingScore: 1, forwardedAiBotMessageInfo: { botJid: DEFAULT_BOT_JID }, forwardOrigin: 4 } })
182
+ const message = wrapToBotForwardedMessage(richResponseMessage, verificationMetadata)
183
+ message.messageContextInfo.botMetadata.botResponseId = responseId
184
+ return { message, responseId, itemId }
185
+ }
186
+
187
+ export class AIRich {
188
+ #client; #nodes = []; #idIndex = {}; #title = ''; #footer = ''; #botJid = DEFAULT_BOT_JID
189
+ #responseId = null; #dynamic = true; #signedVerificationMetadata = null
190
+ #contextInfoExtra = {}; #embeddedScreens = []; #suggestedPrompts = null
191
+ #botSources = null; #pttTranscript = null
192
+
193
+ constructor(client, opts = {}) {
194
+ if (!client) throw new Error('Socket client is required')
195
+ this.#client = client
196
+ if (opts.botJid) this.#botJid = opts.botJid
197
+ if (opts.dynamic !== undefined) this.#dynamic = opts.dynamic
198
+ if (opts.responseId) { this.#responseId = opts.responseId; this.#dynamic = false }
199
+ }
200
+
201
+ #push(section, submessage = null, id = null) { const nodeId = id ?? `node_${this.#nodes.length}`; this.#nodes.push({ id: nodeId, section, submessage }); this.#idIndex[nodeId] = this.#nodes.length - 1; return this }
202
+ #replace(id, section, submessage = null) { const idx = this.#idIndex[id]; if (idx === undefined) throw new Error(`Node id "${id}" not found`); this.#nodes[idx] = { id, section, submessage }; return this }
203
+ #insertAt(pos, section, submessage = null, id = null) { const nodeId = id ?? `node_${this.#nodes.length}`; this.#nodes.splice(pos, 0, { id: nodeId, section, submessage }); this.#idIndex = {}; this.#nodes.forEach((n, i) => { this.#idIndex[n.id] = i }); return this }
204
+ #add(section, submessage, opts = {}) { if (opts.replace) return this.#replace(opts.replace, section, submessage); if (opts.insertAt !== undefined) return this.#insertAt(opts.insertAt, section, submessage, opts.id); return this.#push(section, submessage, opts.id) }
205
+
206
+ async #uploadMedia(input, mediaType = 'image') { if (typeof input === 'string' && !input.startsWith('http')) { const { readFileSync } = await import('fs'); input = readFileSync(input) } if (Buffer.isBuffer(input) || input instanceof Uint8Array) { const uploaded = await this.#client.waUploadToServer({ [mediaType]: input }, { mediaType, upload: this.#client.waUploadToServer }); return uploaded.url } return typeof input === 'string' ? input : input }
207
+
208
+ setTitle(title) { this.#title = title; return this }
209
+ setFooter(footer) { this.#footer = footer; return this }
210
+ setBotJid(jid) { this.#botJid = jid; return this }
211
+ setResponseId(id) { this.#responseId = id; this.#dynamic = false; return this }
212
+ setContextInfo(ci) { this.#contextInfoExtra = ci; return this }
213
+
214
+ addText(text, opts = {}) { const { text: t, inline_entities } = extractIE(text); return this.#add(single(mkMarkdown(t, inline_entities)), null, opts) }
215
+ addCode(language, content, opts = {}) { const { codeBlocks, unified_codeBlock } = tokenizeCode(content, language); const submessage = { messageType: 5, codeMetadata: { codeLanguage: language, codeBlocks } }; return this.#add(single(mkCode(content, language, unified_codeBlock)), submessage, opts) }
216
+ addTable(rows, title = '', opts = {}) { return this.#add(single(mkTable(rows, title)), null, opts) }
217
+ addImage(input, opts = {}) { const resolved = this.#uploadMedia(input, 'image').then(url => single(mkImage(url))); return this.#add(resolved, null, opts) }
218
+ addVideo(url, opts = {}) { const resolved = this.#uploadMedia(url, 'video').then(u => single(mkVideo(u))); return this.#add(resolved, null, opts) }
219
+ addGrid(items, opts = {}) { if (!Array.isArray(items) || items.length < 2) throw new Error('addGrid requires at least 2 items'); return this.#add(grid(items.map(i => mkImage(i))), null, opts) }
220
+
221
+ addVideoGrid(videos, opts = {}) {
222
+ if (!Array.isArray(videos) || videos.length < 2) throw new Error('addVideoGrid requires at least 2 videos')
223
+ const prims = videos.map(v => { const url = typeof v === 'string' ? v : (v?.url ?? v?.videoUrl ?? ''); const item = typeof v === 'string' ? {} : v; return { __typename: 'GenAIVideoPrimitive', reels_url: url, reels_title: item.title, thumbnail_url: item.thumbnailUrl ?? item.thumbnail, creator: item.creator, avatar_url: item.avatarUrl, likes_count: item.likes, comments_count: item.comments, shares_count: item.shares, is_verified: item.isVerified, video_delivery_response: { progressive_urls: (item.progressiveUrls ?? []).map(u => ({ progressive_url: u, __typename: 'GenAIProgressiveUrlResponse' })), dash_manifests: (item.dashManifests ?? []).map(m => ({ manifest_xml: m, __typename: 'GenAIDashManifestResponse' })), __typename: 'GenAIVideoDeliveryResponse' } } })
224
+ return this.#add(grid(prims), null, opts)
225
+ }
226
+
227
+ addReels(reels, opts = {}) { const prims = reels.map(mkReel); return this.#add(prims.length === 1 ? single(prims[0]) : hscroll(prims), null, opts) }
228
+ addImagine(input = {}, opts = {}) { return this.#add(single(buildImaginePrimitive(input)), null, opts) }
229
+ addInlineImage(url, alignment = 'center', opts = {}) { return this.#add(single(mkInlineImage(url, alignment)), null, opts) }
230
+ addHtml(html, trustedSources = [], opts = {}) { return this.#add(single(mkHtml(html, trustedSources)), null, opts) }
231
+
232
+ addRichHtml(tabs, title = 'Preview', opts = {}) {
233
+ const tabItems = typeof tabs === 'string' ? [{ title: 'HTML', html: tabs }] : Array.isArray(tabs) ? tabs : [tabs]
234
+ const embeddedTabs = tabItems.map((tab, i) => ({ id: tab.id ?? `tab_${i}`, tab_header: tab.title ?? tab.tab_header ?? `HTML ${i + 1}`, sections: [{ __typename: 'GenAIUnifiedResponseSection', view_model: { __typename: 'GenAISingleLayoutViewModel', primitive: mkRichHtml(tab.html ?? '', tab.url ?? '', tab.trustedSources ?? []) } }], step_entries: [] }))
235
+ this.#embeddedScreens.push({ title, content: [{ __typename: 'FOAIDNixelButtonSheets', tabs: embeddedTabs }] })
236
+ return this.#add(single(mkMarkdown(`> ${title}`)), null, opts)
237
+ }
238
+
239
+ addPost(data, opts = {}) { return this.#add(single(mkPost(data)), null, opts) }
240
+ addProduct(data, opts = {}) { return this.#add(single(mkProduct(data)), null, opts) }
241
+ addSource(sources, opts = {}) { const prims = sources.map(mkSearchResult); return this.#add(prims.length === 1 ? single(prims[0]) : vstack(prims), null, opts) }
242
+ addMap(locations, opts = {}) { const prims = locations.map(mkMap); return this.#add(prims.length === 1 ? single(prims[0]) : vstack(prims), null, opts) }
243
+ addWidget(ctas, title = '', opts = {}) { return this.#add(single(mkWidget(ctas, title)), null, opts) }
244
+ addFooterAction(data, opts = {}) { return this.#add(single(mkFooterAction(data)), null, opts) }
245
+ addSocialProfile(data, opts = {}) { const sections = mkSocialProfile(data); for (const s of sections) this.#push(s, null, opts.id ? `${opts.id}_${this.#nodes.length}` : null); return this }
246
+ addInstagramProfile(data, opts = {}) { return this.addSocialProfile({ platform: 'INSTAGRAM', entityType: 'IG_PROFILE', entityUrl: data.entityUrl ?? (data.username ? `https://www.instagram.com/${encodeURIComponent(data.username)}` : ''), ...data }, opts) }
247
+ addCopyAction(text, label, alignment = 'END', opts = {}) { return this.#add(addonAction('COPY_TO_CLIPBOARD', [mkMarkdown(String(label ?? text))], alignment), null, opts) }
248
+ addAddonAction(type, primitives, alignment = 'END', opts = {}) { return this.#add(addonAction(type, primitives, alignment), null, opts) }
249
+ addMetadata(text, opts = {}) { return this.#add(single(mkMeta(text)), null, opts) }
250
+ addFOAText(text, opts = {}) { return this.#add(single(mkFOA(text)), null, opts) }
251
+ addTip(text, opts = {}) { return this.#add(single(mkTip(text)), null, opts) }
252
+ addDivider(type = 'HORIZONTAL_LINE', opts = {}) { return this.#add(single(mkDivider(type)), null, opts) }
253
+ addSpacer(spacing = 2, opts = {}) { return this.#add(single(mkSpacer(spacing)), null, opts) }
254
+ addThinkingStatus(title, icon = 'THINKING', opts = {}) { return this.#add(single(mkThinking(title, icon)), null, opts) }
255
+ addProgressStatus(title, inProgress = true, opts = {}) { return this.#add(single(mkProgress(title, inProgress)), null, opts) }
256
+ addTask(data, opts = {}) { return this.#add(single(mkTask(data)), null, opts) }
257
+ addLatex(data, opts = {}) { const entity = mkLatex(data); const prim = mkMarkdown(`{{${entity.key}}}.{{/${entity.key}}}`, [entity]); return this.#add(single(prim), null, opts) }
258
+ addSuggest(prompts, scroll = false, opts = {}) { const prims = prompts.map(p => mkPill(typeof p === 'string' ? p : (p.text ?? p))); return this.#add(scroll ? hscroll(prims) : actionRow(prims), null, opts) }
259
+ addRichMap(mapOpts, opts = {}) { const { submessage, section } = mkRichMap(mapOpts); return this.#add(section, submessage, opts) }
260
+ addRichMenu(menuOpts, opts = {}) { const sections = mkRichMenu(menuOpts); sections.forEach(s => this.#push(s, null, null)); return this }
261
+ addDynamic(dynOpts, opts = {}) { const { submessage, section } = mkDynamic(dynOpts); return this.#add(section, submessage, opts) }
262
+ addLabel(text, variant = 'POSITIVE', opts = {}) { return this.#add(single(mkLabel(text, opts.backgroundColor, opts.textColor)), null, opts) }
263
+ addInlineLinks(links, opts = {}) { return this.#add(single(mkInlineLink(links)), null, opts) }
264
+ addSection(section, opts = {}) { return this.#add(section, null, opts) }
265
+ addEmbeddedScreen(screen) { this.#embeddedScreens.push(screen); return this }
266
+ // push raw sections + submessages directly — escape hatch for new primitives not yet wrapped
267
+ addRawSections(sections, submessages = []) { sections.forEach((s, i) => this.#push(s, submessages[i] ?? null)); return this }
268
+
269
+ addSuggestPrompts(suggestions, opts = {}) {
270
+ if (!Array.isArray(suggestions) || !suggestions.length) throw new Error('addSuggestPrompts requires at least one suggestion')
271
+ this.#suggestedPrompts = { suggestedPrompts: suggestions.map(String), ...(opts.selectedPromptIndex != null ? { selectedPromptIndex: opts.selectedPromptIndex } : {}), ...(opts.selectedPromptId != null ? { selectedPromptId: String(opts.selectedPromptId) } : {}) }
272
+ return this.#add(actionRow(suggestions.map(s => mkPill(s))), null, opts)
273
+ }
274
+
275
+ addSources(sources, opts = {}) {
276
+ if (!Array.isArray(sources) || !sources.length) throw new Error('addSources requires at least one source')
277
+ this.#botSources = sources.map((s, i) => ({ provider: s.provider ?? 0, thumbnailCdnUrl: s.thumbnailCdnUrl ?? s.thumbnail ?? '', sourceProviderUrl: s.sourceProviderUrl ?? s.url ?? '', sourceQuery: s.sourceQuery ?? s.query ?? '', faviconCdnUrl: s.faviconCdnUrl ?? s.favicon ?? '', citationNumber: s.citationNumber ?? i + 1, sourceTitle: s.sourceTitle ?? s.title ?? '' }))
278
+ const prims = sources.map(mkSearchResult)
279
+ return this.#add(prims.length === 1 ? single(prims[0]) : vstack(prims), null, opts)
280
+ }
281
+
282
+ addVerticalAlbum(items, opts = {}) {
283
+ if (!Array.isArray(items) || items.length < 2) throw new Error('addVerticalAlbum requires at least 2 items')
284
+ const expiration = String(Date.now() + 30 * 24 * 60 * 60 * 1000)
285
+ const primitives = items.map(item => {
286
+ const kind = item?.type || (item?.video ? 'video' : 'image')
287
+ if (kind === 'video') {
288
+ const v = item.video || item
289
+ return buildImaginePrimitive({ mediaType: 'video', url: typeof v === 'string' ? v : (v?.url || ''), mimeType: v?.mimeType || 'video/mp4', fileLength: v?.fileLength || 0, duration: v?.duration || 0, status: 'READY' })
290
+ }
291
+ const img = typeof (item?.image || item) === 'string' ? { url: item?.image || item } : (item?.image || item || {})
292
+ const url = img.url || ''
293
+ if (!url) throw new Error('Each vertical album image item requires a URL')
294
+ return { __typename: 'GenAIImagePrimitive', preview_image: { url, url_fallback: url, mime_type: img.mimeType || 'image/jpeg', expiration_timestamp_ms: expiration, width: img.width || 600, height: img.height || 400 }, full_image: { url, url_fallback: url, mime_type: img.mimeType || 'image/jpeg', expiration_timestamp_ms: expiration, width: img.width || 600, height: img.height || 400 }, dark_mode_preview_image: { url, url_fallback: url, mime_type: img.mimeType || 'image/jpeg', expiration_timestamp_ms: expiration, width: img.width || 600, height: img.height || 400 }, dark_mode_full_image: { url, url_fallback: url, mime_type: img.mimeType || 'image/jpeg', expiration_timestamp_ms: expiration, width: img.width || 600, height: img.height || 400 }, asset_query_status: 'FETCHED', display_mode: 'FULL_WIDTH', full_width: true }
295
+ })
296
+ return this.#add({ view_model: { primitives, display_mode: 'FULL_WIDTH', full_width: true, __typename: 'GenAIVStackLayoutViewModel' } }, null, opts)
297
+ }
298
+
299
+ addPttTranscript(transcript, opts = {}) {
300
+ if (!transcript) throw new Error('addPttTranscript requires a transcript string')
301
+ this.#pttTranscript = String(transcript)
302
+ return this.#add(single(mkMeta(`🎤 _${transcript}_`)), null, opts)
303
+ }
304
+
305
+ loadFrom(msg) {
306
+ const rich = msg?.botForwardedMessage?.message?.richResponseMessage ?? msg?.richResponseMessage
307
+ if (!rich) throw new Error('Not an AIRich message')
308
+ const mci = msg?.messageContextInfo
309
+ if (mci?.botMetadata?.messageDisclaimerText) this.#title = mci.botMetadata.messageDisclaimerText
310
+ if (mci?.botMetadata?.verificationMetadata) this.#signedVerificationMetadata = mci.botMetadata.verificationMetadata
311
+ if (rich.unifiedResponse?.data) {
312
+ try {
313
+ const parsed = JSON.parse(Buffer.isBuffer(rich.unifiedResponse.data) ? rich.unifiedResponse.data.toString() : Buffer.from(rich.unifiedResponse.data, 'base64').toString())
314
+ ; (parsed.sections ?? []).forEach((s, i) => { const sub = rich.submessages?.[i] ?? null; this.#nodes.push({ id: `loaded_${i}`, section: s, submessage: sub }); this.#idIndex[`loaded_${i}`] = i })
315
+ } catch { }
316
+ }
317
+ return this
318
+ }
319
+
320
+ async build(jid, opts = {}) {
321
+ const { botJid = this.#botJid, messageId = generateMessageIDV2(), quoted, quotedParticipant } = opts
322
+ const resolved = await waitAllPromises(this.#nodes)
323
+ const sections = resolved.map(n => n.section)
324
+ const submessages = resolved.map(n => n.submessage).filter(Boolean)
325
+ const embeddedScreens = await waitAllPromises(this.#embeddedScreens)
326
+ const responseId = this.#dynamic ? crypto.randomUUID() : (this.#responseId ?? crypto.randomUUID())
327
+ const unifiedData = JSON.stringify({ response_id: responseId, sections, ...(embeddedScreens.length ? { embedded_screens: embeddedScreens } : {}) })
328
+ const contextInfo = { isForwarded: true, forwardOrigin: 4, forwardingScore: 1, forwardedAiBotMessageInfo: { botJid }, participant: '13135550002@s.whatsapp.net', remoteJid: 'status@broadcast', quotedMessage: { protocolMessage: { type: 25 } }, ...this.#contextInfoExtra, ...(quoted ? { quotedMessage: quoted.message, stanzaId: quoted.key?.id, participant: quotedParticipant ?? quoted.key?.participant ?? quoted.key?.remoteJid, remoteJid: quoted.key?.remoteJid } : {}) }
329
+ const verification = this.#signedVerificationMetadata ?? generateVerificationMetadata()
330
+ const { sources, mediaDetailsMetadataList } = buildAdditionalBotMetadata(submessages)
331
+ const richResponseMessage = proto.AIRichResponseMessage.create({ messageType: 1, submessages, unifiedResponse: { data: Buffer.from(unifiedData) }, originalRecipientMetadata: { data: Buffer.from(unifiedData) }, contextInfo })
332
+ const message = wrapToBotForwardedMessage(richResponseMessage, verification)
333
+ const botMeta = message.messageContextInfo.botMetadata
334
+ botMeta.botResponseId = responseId
335
+ botMeta.deviceListMetadata = {}
336
+ botMeta.deviceListMetadataVersion = 2
337
+ botMeta.supportPayload = BIZ_BOT_SUPPORT
338
+ if (this.#title) botMeta.messageDisclaimerText = this.#title
339
+ if (sources.length || this.#botSources?.length) botMeta.richResponseSourcesMetadata = { sources: this.#botSources ?? sources }
340
+ if (mediaDetailsMetadataList.length) botMeta.unifiedResponseMutation = { mediaDetailsMetadataList }
341
+ if (this.#suggestedPrompts) botMeta.suggestedPromptMetadata = this.#suggestedPrompts
342
+ if (this.#pttTranscript) botMeta.pttPromptMetadata = { transcript: this.#pttTranscript }
343
+ botMeta.capabilities = { richResponseUnifiedResponse: true, richResponseEmbeddedScreens: true, richResponseInlineLinksEnabled: true, richResponseUrBloksEnabled: true, richResponseUrImagine: true, richResponseUrReasoning: true }
344
+ return generateWAMessageFromContent(jid, message, { userJid: this.#client.user?.id, messageId })
345
+ }
346
+
347
+ async send(jid, opts = {}) {
348
+ const { additionalNodes = [], ...buildOpts } = opts
349
+ const msg = await this.build(jid, buildOpts)
350
+ const relayFn = this.#client.relayRichMessage ?? this.#client.relayMessage
351
+ await relayFn(jid, msg, {
352
+ ...opts,
353
+ additionalNodes: [
354
+ { tag: 'bot', attrs: { biz_bot: '1' }, content: undefined },
355
+ { tag: 'biz', attrs: {}, content: [{ tag: 'interactive', attrs: { type: 'native_flow', v: '1' }, content: [{ tag: 'native_flow', attrs: { v: '9', name: 'mixed' } }] }] },
356
+ ...(opts.additionalNodes ?? [])
357
+ ]
358
+ })
359
+ return msg
360
+ }
361
+
362
+ async sendEdit(jid, targetId, opts = {}) {
363
+ if (!targetId) throw new Error('targetId is required for sendEdit')
364
+ const msg = await this.build(jid, opts)
365
+ const editMsg = { protocolMessage: { key: { remoteJid: jid, fromMe: true, id: targetId }, type: 14, editedMessage: msg.message } }
366
+ const waMsg = await generateWAMessageFromContent(jid, editMsg, { userJid: this.#client.user?.id })
367
+ await this.#client.relayMessage(jid, waMsg.message, { messageId: waMsg.key.id })
368
+ return waMsg
369
+ }
370
+ }
371
+
372
+ export function aiRichFromObject(client, jid, data, opts = {}) {
373
+ const r = new AIRich(client, { botJid: data.botJid })
374
+ if (data.title) r.setTitle(data.title)
375
+ if (data.parts) {
376
+ for (const p of data.parts) {
377
+ if (p.type === 'text') r.addText(p.content ?? p.text)
378
+ if (p.type === 'code') r.addCode(p.language ?? '', p.content ?? p.code)
379
+ if (p.type === 'table') r.addTable(p.table, p.title)
380
+ if (p.type === 'image') r.addImage(p.url ?? p.image)
381
+ if (p.type === 'grid') r.addGrid(p.items ?? p.images)
382
+ if (p.type === 'videoGrid') r.addVideoGrid(p.items ?? p.videos)
383
+ if (p.type === 'imagine') r.addImagine(p)
384
+ if (p.type === 'sources') r.addSource(p.sources)
385
+ if (p.type === 'divider') r.addDivider()
386
+ if (p.type === 'spacer') r.addSpacer(p.spacing)
387
+ if (p.type === 'tip') r.addTip(p.content ?? p.text)
388
+ if (p.type === 'meta') r.addMetadata(p.content ?? p.text)
389
+ if (p.type === 'foa') r.addFOAText(p.content ?? p.text)
390
+ if (p.type === 'html') r.addHtml(p.html, p.trustedSources)
391
+ if (p.type === 'richHtml') r.addRichHtml(p.tabs ?? p.html, p.title)
392
+ if (p.type === 'copy') r.addCopyAction(p.text, p.label, p.alignment)
393
+ if (p.type === 'social') r.addSocialProfile(p)
394
+ if (p.type === 'imagine') r.addImagine(p)
395
+ if (p.type === 'rawSections') r.addRawSections(p.sections, p.submessages)
396
+ }
397
+ return r.send(jid, opts)
398
+ }
399
+ if (data.texts) data.texts.forEach(t => r.addText(t))
400
+ else if (data.text) r.addText(data.text)
401
+ if (data.codes) data.codes.forEach(c => r.addCode(c.language ?? data.language ?? '', c.code ?? c.content))
402
+ else if (data.code) { const c = typeof data.code === 'string' ? data.code : (data.code.code ?? data.code.content); const lang = typeof data.code === 'string' ? (data.language ?? '') : (data.code.language ?? data.language ?? ''); r.addCode(lang, c) }
403
+ if (data.table) r.addTable(data.table, data.tableTitle)
404
+ else if (data.headers && data.rows) r.addTable([data.headers, ...data.rows], data.tableTitle)
405
+ if (data.images) data.images.forEach(img => r.addImage(img.url ?? img))
406
+ if (data.image) r.addImage(data.image.url ?? data.image)
407
+ if (data.grid) r.addGrid(data.grid)
408
+ if (data.videoGrid) r.addVideoGrid(data.videoGrid)
409
+ if (data.reels) r.addReels(data.reels)
410
+ if (data.imagine) r.addImagine(typeof data.imagine === 'object' ? data.imagine : { url: data.imagine })
411
+ if (data.sources) r.addSource(data.sources)
412
+ if (data.richMapSources) r.addSources(data.richMapSources)
413
+ else if (data.botSources) r.addSources(data.botSources)
414
+ if (data.richMap) r.addRichMap(data.richMap)
415
+ if (data.dynamic) r.addDynamic(data.dynamic)
416
+ if (data.suggestPrompts) r.addSuggestPrompts(Array.isArray(data.suggestPrompts) ? data.suggestPrompts : data.suggestPrompts.suggestions, data.suggestPrompts)
417
+ if (data.pttTranscript) r.addPttTranscript(data.pttTranscript)
418
+ if (data.instagramProfile) r.addInstagramProfile(data.instagramProfile)
419
+ if (data.latex) { if (data.latexText) r.addText(data.latexText); (Array.isArray(data.latex) ? data.latex : [data.latex]).forEach(l => r.addLatex({ expression: l.expression ?? l.latexExpression ?? '', imageUrl: l.url ?? l.imageUrl ?? '', width: l.width ?? 400, height: l.height ?? 200 })) }
420
+ if (data.html) r.addHtml(data.html, data.trustedSources)
421
+ if (data.richHtml) r.addRichHtml(data.richHtml, data.richHtmlTitle)
422
+ if (data.copyText) r.addCopyAction(data.copyText, data.copyLabel, data.copyAlignment)
423
+ if (data.social) r.addSocialProfile(data.social)
424
+ if (data.suggest) r.addSuggest(data.suggest)
425
+ if (data.tip) r.addTip(data.tip)
426
+ if (data.metadata ?? data.meta) r.addMetadata(data.metadata ?? data.meta)
427
+ if (data.footer) r.addFOAText(data.footer)
428
+ if (data.rawSections) r.addRawSections(data.rawSections, data.rawSubmessages)
429
+ return r.send(jid, opts)
430
+ }
431
+
432
+ const stripDevice = jid => String(jid || '').replace(/:\d+$/, '')
433
+
434
+ const collectMetaAIBotJids = async (sock, jid, botUser) => {
435
+ const candidates = new Set([botUser])
436
+ if (!isJidGroup(jid)) return candidates
437
+ const botNumber = jidNormalizedUser(botUser).split('@')[0]
438
+ try {
439
+ const meta = await sock.groupMetadata(jid)
440
+ for (const p of meta.participants || []) {
441
+ const idNum = jidNormalizedUser(p.id).split('@')[0]
442
+ const pnNum = p.phoneNumber ? jidNormalizedUser(p.phoneNumber).split('@')[0] : undefined
443
+ if (pnNum === botNumber || idNum === botNumber || isJidMetaAI(p.id)) candidates.add(p.id)
444
+ }
445
+ } catch { }
446
+ return candidates
447
+ }
448
+
449
+ const isMetaAIResponse = (msg, chatJid, promptId, botJids) => {
450
+ if (!msg?.key || msg.key.fromMe || msg.key.remoteJid !== chatJid) return false
451
+ const sender = stripDevice(msg.key.participant || msg.key.remoteJid)
452
+ if (isJidMetaAI(sender)) return true
453
+ for (const b of botJids) if (jidNormalizedUser(stripDevice(b)) === jidNormalizedUser(sender)) return true
454
+ if (promptId) { const inner = msg.message ? Object.values(msg.message)[0] : undefined; const stanzaId = inner?.contextInfo?.stanzaId || msg.message?.contextInfo?.stanzaId; if (stanzaId === promptId) return true }
455
+ return false
456
+ }
457
+
458
+ const q = quoted => quoted ? { quoted } : {}
459
+
460
+ // ─── attachAIRich ─────────────────────────────────────────────────────────────
461
+ // Attaches all AIRich, meta, bot, and relay methods to result, then returns the
462
+ // fuzzy-proxy-wrapped result. This is the only export zervonex-handler.js needs.
463
+ export function attachAIRich(result, config) {
464
+ const sock = result
465
+
466
+ const relayRichMessage = async (jid, fullMsg, options = {}) => {
467
+ const relayOpts = { messageId: fullMsg.key.id, useCachedGroupMetadata: options.useCachedGroupMetadata, statusJidList: options.statusJidList, additionalNodes: options.additionalNodes, ...(options.relayOptions || {}) }
468
+ await sock.relayMessage(jid, fullMsg.message, relayOpts)
469
+ const hasRich = !!fullMsg.message?.botForwardedMessage?.message?.richResponseMessage || !!fullMsg.message?.interactiveMessage?.bloksWidget
470
+ const shouldEdit = options.bypassDownload !== false && (options.forceRichEdit || hasRich)
471
+ if (shouldEdit) {
472
+ try {
473
+ const editContent = proto.Message.fromObject({ botForwardedMessage: { message: { protocolMessage: { key: { remoteJid: jid, fromMe: true, id: fullMsg.key.id }, type: proto.Message.ProtocolMessage.Type.MESSAGE_EDIT, editedMessage: fullMsg.message } } } })
474
+ const editMsg = generateWAMessageFromContent(jid, editContent, { userJid: sock.user?.id, messageId: generateMessageIDV2(sock.user?.id || jid) })
475
+ await sock.relayMessage(jid, editMsg.message, { ...relayOpts, messageId: editMsg.key.id })
476
+ } catch { /* card already arrived, just behind download prompt */ }
477
+ }
478
+ return fullMsg
479
+ }
480
+
481
+ result.relayRichMessage = relayRichMessage
482
+ result.airich = opts => new AIRich(result, opts)
483
+
484
+ result.captureAiRich = msg => {
485
+ const rich = msg?.botForwardedMessage?.message?.richResponseMessage ?? msg?.richResponseMessage
486
+ if (!rich?.unifiedResponse?.data) return null
487
+ return { submessages: rich.submessages ?? [], sections: JSON.parse(Buffer.from(rich.unifiedResponse.data, 'base64').toString()), contextInfo: rich.contextInfo ?? {}, messageType: rich.messageType ?? 1 }
488
+ }
489
+
490
+ result.relayAiRich = (jid, captured, opts = {}) => {
491
+ const r = new AIRich(result)
492
+ if (captured.sections?.response_id) r.setResponseId(captured.sections.response_id)
493
+ return r.loadFrom({ botForwardedMessage: { message: { richResponseMessage: { submessages: captured.submessages, unifiedResponse: { data: Buffer.from(JSON.stringify(captured.sections)) }, contextInfo: captured.contextInfo, messageType: captured.messageType } } } }).send(jid, opts)
494
+ }
495
+
496
+ result.sendRichFromObject = (jid, data, opts = {}) => aiRichFromObject(result, jid, data, opts)
497
+ result.sendRichMessage = (jid, data, quoted, opts = {}) => result.sendMessage(jid, { aiRich: data }, { ...q(quoted), ...opts })
498
+ result.sendCodeBlock = (jid, code, quoted, opts = {}) => result.sendMessage(jid, { aiRich: { code, language: opts.language ?? 'javascript', title: opts.title } }, q(quoted))
499
+ result.sendCodeBlockV2 = (jid, code, quoted, opts = {}) => result.sendMessage(jid, { aiRich: { text: opts.text, code, language: opts.language ?? '', title: opts.title } }, q(quoted))
500
+ result.sendTable = (jid, title, headers, rows, quoted, opts = {}) => result.sendMessage(jid, { aiRich: { title, table: [headers, ...rows] } }, q(quoted))
501
+ result.sendTableV2 = (jid, tableArray, quoted, opts = {}) => result.sendMessage(jid, { aiRich: { text: opts.text, title: opts.headerText, table: tableArray } }, q(quoted))
502
+ result.sendList = (jid, title, items, quoted, opts = {}) => result.sendMessage(jid, { aiRich: { title, table: [['Key', 'Value'], ...items] } }, q(quoted))
503
+ result.sendLink = (jid, text, links, quoted, opts = {}) => result.sendMessage(jid, { aiRich: { title: opts.headerText, text, sources: links.map(url => ({ url, title: url, subtitle: new URL(url).hostname })) } }, q(quoted))
504
+ result.sendLinkV2 = (jid, text, links, quoted, opts = {}) => result.sendMessage(jid, { aiRich: { title: opts.headerText, text, sources: links } }, q(quoted))
505
+ result.sendLatex = (jid, expressions, quoted, opts = {}) => result.sendMessage(jid, { aiRich: { text: opts.text, title: opts.headerText, latex: expressions } }, q(quoted))
506
+ result.sendRichTable = async (jid, title, headers, rows, quoted, opts = {}) => { const { message, messageId } = generateTableContent(title, headers, rows, quoted, opts); return relayRichMessage(jid, generateWAMessageFromContent(jid, message, { userJid: sock.user?.id, messageId: opts.messageId || messageId }), opts) }
507
+ result.sendRichList = async (jid, title, items, quoted, opts = {}) => { const { message, messageId } = generateListContent(title, items, quoted, opts); return relayRichMessage(jid, generateWAMessageFromContent(jid, message, { userJid: sock.user?.id, messageId: opts.messageId || messageId }), opts) }
508
+ result.sendRichCode = async (jid, code, quoted, opts = {}) => { const { message, messageId } = generateCodeBlockContent(code, quoted, opts); return relayRichMessage(jid, generateWAMessageFromContent(jid, message, { userJid: sock.user?.id, messageId: opts.messageId || messageId }), opts) }
509
+ result.sendRichLatex = async (jid, quoted, opts = {}) => { const { message, messageId } = generateLatexContent(quoted, { expressions: [], ...opts }); return relayRichMessage(jid, generateWAMessageFromContent(jid, message, { userJid: sock.user?.id, messageId: opts.messageId || messageId }), opts) }
510
+ result.sendRichLabel = (jid, text, options = {}) => { const r = new AIRich(result); r.addLabel(text, options.variant, options); return r.send(jid, options) }
511
+ result.sendInlineLinks = (jid, links, options = {}) => { const r = new AIRich(result); r.addInlineLinks(links); return r.send(jid, options) }
512
+ result.sendVerticalAlbum = (jid, items, options = {}) => { const r = new AIRich(result); r.addVerticalAlbum(items); return r.send(jid, options) }
513
+ result.sendRichLatexImage = async (jid, opts = {}) => {
514
+ const { text, expressions = [], headerText, footer } = opts
515
+ const rendered = expressions.map(expr => { const latex = expr.latexExpression || expr.expression || expr; const url = `https://latex.codecogs.com/png.latex?${encodeURIComponent(latex).replace(/'/g, '%27')}`; return { latexExpression: latex, url, width: expr.width || 400, height: expr.height || 100 } })
516
+ const { message, messageId } = generateLatexContent(null, { text, expressions: rendered, headerText, footer })
517
+ return relayRichMessage(jid, generateWAMessageFromContent(jid, message, { userJid: sock.user?.id, messageId: opts.messageId || messageId }), opts)
518
+ }
519
+ result.captureAndResendRichResponse = async (jid, metaAiMsg, quoted, opts = {}) => {
520
+ const captured = captureUnifiedResponse(metaAiMsg)
521
+ if (!captured) throw new Error('captureAndResendRichResponse: no unifiedResponse data in message')
522
+ const { message, messageId } = generateUnifiedResponseContent(quoted, captured)
523
+ return relayRichMessage(jid, generateWAMessageFromContent(jid, message, { userJid: sock.user?.id, messageId: opts.messageId || messageId }), opts)
524
+ }
525
+ result.sendBotPlanning = async (jid, content = {}, opts = {}) => {
526
+ const { text = 'Execution plan', steps = [], queryPlan = true, botJid = META_AI_BOT_JID } = content
527
+ const submessages = [{ messageType: proto.AIRichResponseSubMessageType.AI_RICH_RESPONSE_TEXT, messageText: text }, ...steps.map(s => ({ messageType: proto.AIRichResponseSubMessageType.AI_RICH_RESPONSE_TEXT, messageText: typeof s === 'string' ? s : `${s.title || s.name || 'Step'}${s.body ? `: ${s.body}` : ''}` }))]
528
+ const wrapped = buildBotRichResponse({ botJid, submessages, capabilities: [BotCapabilityType.AGENTIC_PLANNING, ...(queryPlan ? [BotCapabilityType.QUERY_PLAN] : [])] })
529
+ const full = await generateWAMessageFromContent(jid, wrapped, { userJid: sock.user?.id, messageId: opts.messageId || generateMessageIDV2(sock.user?.id) })
530
+ return relayRichMessage(jid, full, opts)
531
+ }
532
+ result.sendBotPlan = async (jid, content = {}, opts = {}) => {
533
+ const { text = 'Execution plan', title = 'Execution plan', steps = [], estimatedCompletionTime, botJid = META_AI_BOT_JID } = content
534
+ if (!Array.isArray(steps) || steps.length === 0) throw new Error('sendBotPlan requires at least one step')
535
+ const statusEnum = proto.BotProgressIndicatorMetadata.BotPlanningStepMetadata.PlanningStepStatus
536
+ const normalizedSteps = steps.map((s, i) => ({ statusTitle: String(s.title || s.name || `Step ${i + 1}`), statusBody: String(s.body || s.description || ''), status: s.status === 'executing' ? statusEnum.EXECUTING : (s.status === 'finished' || s.status === 'completed') ? statusEnum.FINISHED : statusEnum.PLANNED, isReasoning: s.isReasoning === true, isEnhancedSearch: s.isEnhancedSearch === true }))
537
+ const wrapped = { messageContextInfo: { botMetadata: { capabilityMetadata: { capabilities: [proto.BotCapabilityMetadata.BotCapabilityType.AGENTIC_PLANNING, proto.BotCapabilityMetadata.BotCapabilityType.QUERY_PLAN] }, progressIndicatorMetadata: { progressDescription: title, stepsMetadata: normalizedSteps, ...(estimatedCompletionTime ? { estimatedCompletionTime } : {}) } } }, botForwardedMessage: { message: { richResponseMessage: { messageType: 1, submessages: [{ messageType: 2, messageText: text }], contextInfo: { isForwarded: true, forwardingScore: 1, forwardedAiBotMessageInfo: { botJid }, forwardOrigin: 4 } } } } }
538
+ const full = await generateWAMessageFromContent(jid, wrapped, { userJid: sock.user?.id, messageId: opts.messageId || generateMessageIDV2(sock.user?.id) })
539
+ await sock.relayMessage(jid, full.message, { messageId: full.key.id })
540
+ return full
541
+ }
542
+ result.replayPlanning = (jid, steps, finalContent, opts) => replayPlanning(result, jid, steps, finalContent, opts)
543
+ result.replayPlanningOnly = (jid, steps, opts) => replayPlanningOnly(result, jid, steps, opts)
544
+ result.metaTyping = (jid, opts) => metaTyping(result, jid, opts)
545
+ result.sendMetaComposited = (jid, content, opts) => sendMetaComposited(result, jid, content, opts)
546
+ result.buildReasoningSteps = buildReasoningSteps
547
+ result.buildSearchSteps = buildSearchSteps
548
+ result.mixedSteps = mixedSteps
549
+ result.buildSteps = buildSteps
550
+ result.PlanningStepStatus = PlanningStepStatus
551
+ result.sendMetaAI = async (a, b, c = {}) => {
552
+ let text, opts = c, yourJid
553
+ if (typeof b === 'string') { text = b; yourJid = a } else { text = a; opts = b || {}; yourJid = opts.yourJid || '' }
554
+ const jid = opts.jid || META_AI_BOT_JID
555
+ const threadId = opts.threadId || generateMessageIDV2(yourJid)
556
+ const now = Date.now()
557
+ const senderKeyHash = opts.senderKeyHash || randomBytes(8).toString('base64')
558
+ const message = {
559
+ extendedTextMessage: proto.Message.ExtendedTextMessage.fromObject({ text, previewType: 'NONE', contextInfo: proto.ContextInfo.fromObject({ botMessageSharingInfo: { botEntryPointOrigin: 'FAVICON', forwardScore: 0 } }), inviteLinkGroupTypeV2: 'DEFAULT' }),
560
+ messageContextInfo: proto.MessageContextInfo.fromObject({ deviceListMetadata: { senderKeyHash, senderTimestamp: opts.senderTimestamp || String(Math.floor(now / 1000)) }, deviceListMetadataVersion: 2, messageSecret: opts.messageSecret || randomBytes(32), botMetadata: { botModeSelectionMetadata: { overrideMode: [0] }, botThreadInfo: { serverInfo: { title: text.substring(0, 50) }, clientInfo: { type: 'DEFAULT' } }, botRenderingConfigMetadata: BOT_RENDERING_CONFIG_METADATA, ...(opts.conversationContext?.length ? { aiConversationContext: opts.conversationContext } : {}) }, threadId: [{ threadType: 'AI_THREAD', threadKey: { remoteJid: '0002@s.whatsapp.net', fromMe: true, id: threadId } }] })
561
+ }
562
+ const msgId = generateMessageIDV2(yourJid)
563
+ await sock.relayMessage(jid, message, { messageId: msgId, ...(opts.quoted ? { quoted: opts.quoted } : {}) })
564
+ return msgId
565
+ }
566
+ result.aiPrompt = async (jid, prompt, options = {}) => {
567
+ const { timeout = 60_000, onPartial, botUser = META_AI_BOT_JID, mentions = [], ...sendOpts } = options
568
+ if (!jid || typeof prompt !== 'string' || !prompt.trim()) throw new Error('aiPrompt requires a chat JID and a non-empty prompt')
569
+ const botJids = await collectMetaAIBotJids(result, jid, botUser)
570
+ const allMentions = [...new Set([...mentions, ...botJids])]
571
+ return new Promise((resolve, reject) => {
572
+ let promptId, settled = false
573
+ const cleanup = () => { clearTimeout(timer); sock.ev.off('messages.upsert', onUpsert); if (onPartial) sock.ev.off('messages.update', onUpdate) }
574
+ const settle = (fn, val) => { if (settled) return; settled = true; cleanup(); fn(val) }
575
+ const timer = setTimeout(() => settle(reject, new Error(`aiPrompt timed out after ${timeout}ms in ${jid}`)), timeout)
576
+ const onUpsert = ({ messages }) => { for (const msg of messages) if (isMetaAIResponse(msg, jid, promptId, botJids)) { settle(resolve, msg); return } }
577
+ const onUpdate = ({ updates }) => { if (!onPartial) return; for (const u of updates) { if (!u.key || u.key.fromMe || u.key.remoteJid !== jid) continue; const sender = jidNormalizedUser(stripDevice(u.key.participant || u.key.remoteJid)); const isBot = isJidMetaAI(sender) || [...botJids].some(b => jidNormalizedUser(stripDevice(b)) === sender); if (isBot && u.message) onPartial(u.message, u.key) } }
578
+ sock.ev.on('messages.upsert', onUpsert)
579
+ if (onPartial) sock.ev.on('messages.update', onUpdate)
580
+ ; (async () => {
581
+ try {
582
+ const content = { text: prompt, ...(isJidGroup(jid) ? { mentions: allMentions } : {}), ...sendOpts }
583
+ const full = await generateWAMessage(jid, content, { userJid: sock.user?.id, messageId: sendOpts.messageId || generateMessageIDV2(sock.user?.id) })
584
+ await sock.relayMessage(jid, full.message, { messageId: full.key.id })
585
+ if (config.emitOwnEvents) process.nextTick(() => sock.ev.emit('messages.upsert', { messages: [full], type: 'append' }))
586
+ promptId = full.key.id
587
+ } catch (err) { settle(reject, err) }
588
+ })()
589
+ })
590
+ }
591
+ result.sendAsMimic = async (jid, content, mimicJid, options = {}) => {
592
+ if (!options.admin && !options.mimicPermission) throw new Error('sendAsMimic requires admin: true')
593
+ const msgId = generateMessageIDV2(sock.user?.id)
594
+ const full = await generateWAMessageFromContent(jid, content, { userJid: sock.user?.id, ...options })
595
+ full.key = { remoteJid: jid, fromMe: false, id: msgId, participant: mimicJid }
596
+ await sock.relayMessage(jid, full.message, { messageId: msgId, ...(options.additionalAttributes ? { additionalAttributes: options.additionalAttributes } : {}) })
597
+ return full
598
+ }
599
+ result.sendStatus = async (content, options = {}) => {
600
+ const provided = options.statusJidList || content?.statusJidList || sock.statusJidList || []
601
+ if (!provided.length) throw new Error('sendStatus requires statusJidList')
602
+ return result.sendMessage(STATUS_JID, { ...content, status: true }, { ...options, broadcast: true, statusJidList: provided })
603
+ }
604
+ result.sendBotToolResult = async (jid, content = {}, opts = {}) => {
605
+ const { text = 'Tool result', toolCallId = `tool-${Date.now()}`, resolutionData, resolutionDataSerialized, botJid = META_AI_BOT_JID } = content
606
+ const wrapped = buildBotRichResponse({ text, botJid, capabilities: [BotCapabilityType.AGENTIC_PLANNING, BotCapabilityType.QUERY_PLAN], botMetadata: { resolvedToolCallMetadata: { toolCallId, resolutionDataSerialized: resolutionDataSerialized ?? JSON.stringify(resolutionData ?? {}) } } })
607
+ const full = await generateWAMessageFromContent(jid, wrapped, { userJid: sock.user?.id, messageId: opts.messageId || generateMessageIDV2(sock.user?.id) })
608
+ return relayRichMessage(jid, full, opts)
609
+ }
610
+ result.sendWhatsAppFlow = async (jid, flow, options = {}) => {
611
+ const { text, footer, image, caption, ...flowOpts } = flow || {}
612
+ const btn = makeWhatsAppFlowButton(flowOpts)
613
+ const content = image ? { image, caption: caption || text || flowOpts.cta } : { text: text || flowOpts.cta || '' }
614
+ return result.sendMessage(jid, { ...content, ...(footer ? { footer } : {}), nativeFlow: [btn] }, options)
615
+ }
616
+ result.sendRichMap = (jid, mapOpts, options = {}) => { const r = new AIRich(result); r.addRichMap(mapOpts); return r.send(jid, options) }
617
+ result.sendDynamic = (jid, dynOpts, options = {}) => { const r = new AIRich(result); r.addDynamic(dynOpts); return r.send(jid, options) }
618
+ result.sendBotPromptSuggestions = (jid, content = {}, options = {}) => { const { text, suggestions = [] } = content; const r = new AIRich(result); if (text) r.addText(text); r.addSuggestPrompts(suggestions, content); return r.send(jid, options) }
619
+ result.sendBotSources = (jid, content = {}, options = {}) => { const { text, sources = [] } = content; const r = new AIRich(result); if (text) r.addText(text); r.addSources(sources); return r.send(jid, options) }
620
+ result.sendBotPttTranscript = (jid, content = {}, options = {}) => { const { text, transcript } = content; const r = new AIRich(result); if (text) r.addText(text); r.addPttTranscript(transcript); return r.send(jid, options) }
621
+ result.sendInstagramProfile = (jid, data, options = {}) => { const r = new AIRich(result); r.addInstagramProfile(data); return r.send(jid, options) }
622
+ result.sendRichGeneration = async (jid, opts = {}) => { const { message, responseId, itemId } = prepareRichGenerationMessage(opts); const full = await generateWAMessageFromContent(jid, message, { userJid: sock.user?.id, messageId: opts.messageId || generateMessageIDV2(sock.user?.id) }); await sock.relayMessage(jid, full.message, { messageId: full.key.id }); return { msg: full, responseId, itemId } }
623
+ result.sendUnifiedResponse = (jid, sections, submessages = [], options = {}) => { const r = new AIRich(result); r.addRawSections(sections, submessages); return r.send(jid, options) }
624
+ result.sendRichButtonGrid = async (jid, grid, options = {}) => {
625
+ const { text, footer, cards = [] } = grid || {}
626
+ if (!Array.isArray(cards) || cards.length === 0) throw new Error('sendRichButtonGrid expects at least one card')
627
+ return result.sendMessage(jid, { ...(text ? { text } : {}), ...(footer ? { footer } : {}), cards }, options)
628
+ }
629
+
630
+ const normalize = s => s.toLowerCase().replace(/[^a-z]/g, '')
631
+ const knownKeys = Object.keys(result).filter(k => typeof result[k] === 'function')
632
+ const normalizedMap = new Map(knownKeys.map(k => [normalize(k), k]))
633
+
634
+ return new Proxy(result, {
635
+ get(target, prop, receiver) {
636
+ if (prop in target) return Reflect.get(target, prop, receiver)
637
+ if (typeof prop !== 'string') return undefined
638
+ const match = normalizedMap.get(normalize(prop))
639
+ if (match) { target.logger?.warn?.(`[ZervonexHandler] Unknown method "${prop}" — did you mean "${match}"? Calling it.`); return target[match] }
640
+ return undefined
641
+ }
642
+ })
590
643
  }