MessageBuilderV4.7.js Nixel released MessageBuilderV4.7.js, an advanced WhatsApp interactive message builder for Baileys that supports buttons, carousels, native flows, and AI rich response payloads. The library includes a custom extractIE function for parsing hyperlinks, citations, and LaTeX expressions, and is available for personal or commercial use with restrictions on redistribution. | / | | Do not remove this watermark. | | | | NIXCODE - Advanced WhatsApp Interactive Message Builder | | Built for creating buttons, carousels, native flows, | | and AI rich response payloads using Baileys with | | fluent chaining, flexible payload customization, | | and scalable architecture for modern bot development. | | | | Runtime: | | - Baileys: @whiskeysockets/baileys latest | | | | Created by Nixel | | Contributors: ~ Ahmad tumbuh kembang | | | | WhatsApp: wa.me/6285188349341 | | Channel: https://whatsapp.com/channel/0029VbCV1ck8fewpdNb2TY2k | | | | Copyright c 2026 Nixel | | | | Permission is granted to use and modify this library | | for personal or commercial projects. | | | | Reuploading, reselling, relicensing, or redistributing | | this library as a standalone product is prohibited. | | | | Do not claim this project as your own original work. | | / | | | | 'use strict'; | | | | const VERSION = '4.7'; | | | | import { generateWAMessageFromContent, prepareWAMessageMedia, generateMessageIDV2 } from 'baileys'; | | import crypto from 'crypto'; | | import sharp from 'sharp'; | | import ffmpeg from 'fluent-ffmpeg'; | | import { PassThrough, Readable } from 'stream'; | | | | function extractIE text, { extract = true, hyperlink = true, citation = true, latex = true } = {} { | | if extract { | | return { | | text, | | ie: , | | inline entities: , | | }; | | } | | | | const createIE = type, ie = { | | if type == 'hyperlink' { | | return { | | key: ie.key, | | metadata: { | | display name: ie.text, | | is trusted: ie.is trusted, | | url: ie.url, | | typename: 'GenAIInlineLinkItem', | | }, | | }; | | } | | | | if type == 'citation' { | | return { | | key: ie.key, | | metadata: { | | reference id: ie.reference id, | | reference url: ie.url, | | reference title: ie.url, | | reference display name: ie.url, | | sources: , | | typename: 'GenAISearchCitationItem', | | }, | | }; | | } | | | | if type == 'latex' { | | return { | | key: ie.key, | | metadata: { | | latex expression: ie.text, | | latex image: { | | url: ie.url, | | width: Number ie.width || 100, | | height: Number ie.height || 100, | | }, | | font height: Number ie.font height || 83.333333333333, | | padding: Number ie.padding || 15, | | typename: 'GenAILatexItem', | | }, | | }; | | } | | }; | | | | let ie = ; | | let inline entities = ; | | let result = ''; | | let last = 0; | | let citation index = 1; | | let hyperlink index = 0; | | let latex index = 0; | | let stack = ; | | | | for let i = 0; i < text.length; i++ { | | if text i == ' ' && text i - 1 = '\\' { | | stack.push i ; | | } else if text i == ' ' && text i + 1 == ' ' || text i + 1 == '<' { | | let start = stack.pop ; | | | | if start == null continue; | | | | let open = text i + 1 ; | | let close = open == ' ' ? ' ' : ' '; | | let type = open == ' ' ? 'link' : 'latex'; | | let end = i + 2; | | let depth = 1; | | | | while end < text.length && depth { | | if text end == open && text end - 1 = '\\' depth++; | | else if text end == close && text end - 1 = '\\' depth--; | | end++; | | } | | | | if depth continue; | | | | let raw = text.slice start + 1, i .trim ; | | let url = text.slice i + 2, end - 1 .trim ; | | | | let key; | | let tag; | | let data; | | | | if type == 'latex' { | | if latex continue; | | | | let txt = '', width = null, height = null, font height = null, padding = null = raw.split '|' ; | | | | key = \u004E\u0049\u0058\u0045\u004C LATEX ${latex index++} ; | | tag = {{${key}}}${txt || 'image'}{{/${key}}} ; | | | | data = { | | type: 'latex', | | ie: { | | key, | | text: txt, | | url, | | width, | | height, | | font height, | | padding, | | }, | | }; | | } else if raw { | | if hyperlink continue; | | | | const trusted = url.startsWith ' ' ; | | | | if trusted { | | url = url.slice 1 ; | | } | | | | key = \u004E\u0049\u0058\u0045\u004C HYPERLINK ${hyperlink index++} ; | | tag = {{${key}}}${url}{{/${key}}} ; | | | | data = { | | type: 'hyperlink', | | ie: { | | key, | | text: raw, | | url, | | is trusted: trusted, | | }, | | }; | | } else { | | if citation continue; | | | | key = \u004E\u0049\u0058\u0045\u004C CITATION ${citation index - 1} ; | | tag = {{${key}}}${url}{{/${key}}} ; | | | | data = { | | type: 'citation', | | ie: { | | reference id: citation index++, | | key, | | text: '', | | url, | | }, | | }; | | } | | | | result += text.slice last, start + tag; | | last = end; | | | | ie.push data ; | | | | const entity = createIE data.type, data.ie ; | | | | if entity { | | inline entities.push entity ; | | } | | | | i = end - 1; | | } | | } | | | | result += text.slice last ; | | | | return { | | text: result, | | ie, | | inline entities, | | }; | | } | | | | async function waitAllPromises input { | | const isPromise = v = v && typeof v.then === 'function'; | | const isObject = v = v && typeof v === 'object'; | | | | const deep = async v = { | | if isPromise v return deep await v ; | | if Array.isArray v return Promise.all v.map deep ; | | if isObject v { | | const entries = await Promise.all Object.entries v .map async k, val = k, await deep val ; | | return Object.fromEntries entries ; | | } | | return v; | | }; | | | | return deep await input ; | | } | | | | class AIRichError extends Error { | | constructor message, code, meta = {} { | | super message ; | | this.name = 'AIRichError'; | | this.code = code; | | Object.assign this, meta ; | | } | | } | | | | class ItemNotFoundError extends AIRichError { | | constructor id, availableIds = { | | super Item id "${id}" not found${availableIds.length ? available: ${availableIds.join ', ' } : ' no items have an id yet '} , 'ITEM NOT FOUND', { id, availableIds } ; | | this.name = 'ItemNotFoundError'; | | } | | } | | | | class DuplicateIdError extends AIRichError { | | constructor id { | | super Item id "${id}" already exists , 'DUPLICATE ID', { id } ; | | this.name = 'DuplicateIdError'; | | } | | } | | | | class InvalidTargetError extends AIRichError { | | constructor message, meta = {} { | | super message, 'INVALID TARGET', meta ; | | this.name = 'InvalidTargetError'; | | } | | } | | | | class ContentValidationError extends AIRichError { | | constructor message, meta = {} { | | super message, 'CONTENT VALIDATION', meta ; | | this.name = 'ContentValidationError'; | | } | | } | | | | class Toolkit { | | constructor {} | | | | static extractIE text, { extract = true, hyperlink = true, citation = true, latex = true } = {} { | | return extractIE text, { extract, hyperlink, citation, latex } ; | | } | | | | static async resize buffer, x, y, fit = 'cover' { | | return await sharp buffer | | .resize x, y, { | | fit, | | position: 'center', | | background: { r: 0, g: 0, b: 0, alpha: 0 }, | | } | | .png | | .toBuffer ; | | } | | | | static async waitAllPromises input { | | return await waitAllPromises input ; | | } | | | | static async fetchBuffer url, options = {}, { silent = true } = {} { | | try { | | let response = await fetch url, options ; | | if response.ok throw Error HTTP ${response.status} ; | | return Buffer.from await response.arrayBuffer ; | | } catch error { | | if silent return Buffer.alloc 0 ; | | throw error; | | } | | } | | | | static async toUrl client, path, mediaType = 'document' { | | if path throw new Error 'Url or buffer needed' ; | | | | const media = await prepareWAMessageMedia | | { | | mediaType : Buffer.isBuffer path ? path : { url: path }, | | }, | | { | | upload: client.waUploadToServer, | | jid: '\u0040\u006e\u0065\u0077\u0073\u006c\u0065\u0074\u0074\u0065\u0072', | | } | | ; | | | | return Object.values media 0 ?.url; | | } | | | | static async resolveMedia client, media, mediaType = 'image', { resolveUrl = false, resolveWAUrl = false, result = 'url', resize = false, width = 300, height = 300 } = {} { | | const isUrl = str = /^https?:\/\/.+/i.test str ; | | | | const isWAUrl = str = /^https?:\/\/ ^/ \.whatsapp\.net\//i.test str ; | | | | if Array.isArray media { | | return Promise.all | | media.map item = | | Toolkit.resolveMedia client, item, mediaType, { | | resolveUrl, | | resolveWAUrl, | | result, | | resize, | | width, | | height, | | } | | | | ; | | } | | | | const originalIsBuffer = Buffer.isBuffer media ; | | | | if typeof media === 'string' && isUrl media { | | if isWAUrl media { | | if resolveWAUrl { | | media = await Toolkit.fetchBuffer media, {}, { silent: true } ; | | } else if resolveUrl { | | if result === 'url' return media; | | | | media = await Toolkit.fetchBuffer media, {}, { silent: true } ; | | } | | } else { | | if resolveUrl { | | if result === 'url' return media; | | | | media = await Toolkit.fetchBuffer media, {}, { silent: true } ; | | } else { | | media = await Toolkit.fetchBuffer media, {}, { silent: true } ; | | } | | } | | } | | | | if typeof media === 'string' && isUrl media { | | media = Buffer.from media, 'base64' ; | | } | | | | if Buffer.isBuffer media || media.length { | | return; | | } | | | | if resize && Buffer.isBuffer media { | | media = await Toolkit.resize media, width, height ; | | } | | | | if result === 'buffer' { | | return media; | | } | | | | if result === 'base64' { | | return media.toString 'base64' ; | | } | | | | if originalIsBuffer { | | return Toolkit.toUrl client, media, mediaType ; | | } | | | | return Toolkit.toUrl client, media, mediaType ; | | } | | | | static getMp4Duration buffer, { silent = true } = {} { | | try { | | if Buffer.isBuffer buffer || buffer.length < 8 { | | if silent return 0; | | throw new Error 'Invalid buffer' ; | | } | | | | let offset = 0; | | | | while offset < buffer.length - 8 { | | const size = buffer.readUInt32BE offset ; | | | | if size < 8 || offset + size buffer.length { | | if silent return 0; | | throw new Error 'Invalid atom size' ; | | } | | | | const type = buffer.toString 'ascii', offset + 4, offset + 8 ; | | | | if type === 'moov' { | | let moovOffset = offset + 8; | | const moovEnd = offset + size; | | | | while moovOffset < moovEnd - 8 { | | const childSize = buffer.readUInt32BE moovOffset ; | | | | if childSize < 8 || moovOffset + childSize moovEnd { | | if silent return 0; | | throw new Error 'Invalid child atom size' ; | | } | | | | const childType = buffer.toString 'ascii', moovOffset + 4, moovOffset + 8 ; | | | | if childType === 'mvhd' { | | const version = buffer.readUInt8 moovOffset + 8 ; | | | | if version === 0 { | | const timescale = buffer.readUInt32BE moovOffset + 20 ; | | const duration = buffer.readUInt32BE moovOffset + 24 ; | | | | if timescale { | | if silent return 0; | | throw new Error 'Invalid timescale' ; | | } | | | | return duration / timescale; | | } | | | | if version === 1 { | | const timescale = buffer.readUInt32BE moovOffset + 32 ; | | const duration = Number buffer.readBigUInt64BE moovOffset + 36 ; | | | | if timescale { | | if silent return 0; | | throw new Error 'Invalid timescale' ; | | } | | | | return duration / timescale; | | } | | } | | | | moovOffset += childSize; | | } | | } | | | | offset += size; | | } | | | | if silent return 0; | | | | throw new Error 'No mvhd found ' ; | | } catch err { | | if silent return 0; | | throw err; | | } | | } | | | | static getMp4Preview videoBuffer, { time, result = 'buffer', resize = true, width = 300, height = 300, silent = true } = {} { | | return new Promise resolve, reject = { | | const fail = err = { | | if silent { | | return resolve result === 'base64' ? '' : Buffer.alloc 0 ; | | } | | return reject err ; | | }; | | | | try { | | if Buffer.isBuffer videoBuffer || videoBuffer.length { | | return fail new Error 'videoBuffer tidak valid atau kosong' ; | | } | | | | const inputStream = new Readable { read {} } ; | | inputStream.push videoBuffer ; | | inputStream.push null ; | | | | const outputStream = new PassThrough ; | | const chunks = ; | | | | outputStream.on 'data', chunk = chunks.push chunk ; | | | | outputStream.on 'end', async = { | | try { | | let output = Buffer.concat chunks ; | | | | if output.length { | | return fail new Error 'Output kosong — cek format atau timestamp video' ; | | } | | | | if resize { | | output = await Toolkit.resize output, width, height ; | | } | | | | return resolve result === 'base64' ? output.toString 'base64' : output ; | | } catch err { | | return fail err ; | | } | | } ; | | | | outputStream.on 'error', fail ; | | | | time = time ?? Math.min Toolkit.getMp4Duration videoBuffer 0.2, 10 ; | | | | ffmpeg inputStream | | .outputOptions -ss ${time} , '-vframes 1', '-vcodec png', '-f image2pipe' | | .on 'error', err = fail new Error ffmpeg error: ${err.message} | | .pipe outputStream, { end: true } ; | | } catch err { | | return fail err ; | | } | | } ; | | } | | | | static stringifyEscaped obj { | | return JSON.stringify obj .replace / \u007f-\uffff /g, c = '\\u' + c.charCodeAt 0 .toString 16 .padStart 4, '0' ; | | } | | } | | | | class BaseBuilder { | | constructor { | | this. title = ''; | | this. subtitle = ''; | | this. body = ''; | | this. footer = ''; | | this. contextInfo = {}; | | this. extraPayload = {}; | | } | | | | setTitle title { | | if typeof title == 'string' { | | throw new TypeError 'Title must be a string' ; | | } | | this. title = title; | | return this; | | } | | | | setSubtitle subtitle { | | if typeof subtitle == 'string' { | | throw new TypeError 'Subtitle must be a string' ; | | } | | this. subtitle = subtitle; | | return this; | | } | | | | setBody body { | | if typeof body == 'string' { | | throw new TypeError 'Body must be a string' ; | | } | | this. body = body; | | return this; | | } | | | | setFooter footer { | | if typeof footer == 'string' { | | throw new TypeError 'Footer must be a string' ; | | } | | this. footer = footer; | | return this; | | } | | | | setContextInfo obj { | | if typeof obj == 'object' || obj === null || Array.isArray obj { | | throw new TypeError 'ContextInfo must be a plain object' ; | | } | | | | this. contextInfo = obj; | | return this; | | } | | | | addPayload obj { | | if typeof obj == 'object' || obj === null || Array.isArray obj { | | throw new TypeError 'Payload must be a plain object' ; | | } | | | | Object.assign this. extraPayload, obj ; | | | | return this; | | } | | } | | | | class Button extends BaseBuilder { | | client; | | | | constructor client { | | super ; | | if client { | | throw new Error 'Socket is required' ; | | } | | this. client = client; | | | | this. buttons = ; | | this. data; | | this. currentSelectionIndex = -1; | | this. currentSectionIndex = -1; | | this. params = {}; | | } | | | | loadFrom msg { | | if msg throw new Error 'interactiveMessage needed' ; | | if msg.interactiveMessage throw new Error 'interactiveMessage not found' ; | | | | const { interactiveMessage, ...extraPayload } = msg; | | const iM = interactiveMessage; | | const header = iM.header || {}; | | const nativeFlow = iM.nativeFlowMessage || {}; | | | | this. title = header.title || ''; | | this. subtitle = header.subtitle || ''; | | this. body = iM.body?.text || ''; | | this. footer = iM.footer?.text || ''; | | this. contextInfo = iM.contextInfo || {}; | | this. extraPayload = extraPayload; | | | | this. buttons = Array.isArray nativeFlow.buttons | | ? nativeFlow.buttons.map button = { | | ...button, | | buttonParamsJson: typeof button.buttonParamsJson === 'string' ? button.buttonParamsJson : JSON.stringify button.buttonParamsJson || {} , | | } | | : ; | | | | this. data = header.imageMessage | | ? { imageMessage: header.imageMessage } | | : header.videoMessage | | ? { videoMessage: header.videoMessage } | | : header.documentMessage | | ? { documentMessage: header.documentMessage } | | : header.productMessage | | ? { productMessage: header.productMessage } | | : undefined; | | | | this. params = {}; | | | | if typeof nativeFlow.messageParamsJson === 'string' { | | try { | | this. params = JSON.parse nativeFlow.messageParamsJson || '{}' ; | | } catch { | | this. params = {}; | | } | | } else if nativeFlow.messageParamsJson && typeof nativeFlow.messageParamsJson === 'object' { | | this. params = { ...nativeFlow.messageParamsJson }; | | } | | | | this. currentSelectionIndex = this. buttons.findLastIndex button = button.name === 'single select' ; | | | | this. currentSectionIndex = -1; | | | | if this. currentSelectionIndex == -1 { | | try { | | const button = this. buttons this. currentSelectionIndex ; | | const params = JSON.parse button.buttonParamsJson || '{}' ; | | | | if Array.isArray params.sections && params.sections.length { | | this. currentSectionIndex = params.sections.length - 1; | | } | | } catch { | | this. currentSelectionIndex = -1; | | this. currentSectionIndex = -1; | | } | | } | | | | return this; | | } | | | | setImage path, options = {} { | | if path throw new Error 'Url or buffer needed' ; | | Buffer.isBuffer path ? this. data = { image: path, ...options } : this. data = { image: { url: path }, ...options } ; | | return this; | | } | | | | setDocument path, options = {} { | | if path throw new Error 'Url or buffer needed' ; | | Buffer.isBuffer path ? this. data = { document: path, ...options } : this. data = { document: { url: path }, ...options } ; | | return this; | | } | | | | setMedia obj { | | if typeof obj == 'object' || obj === null || Array.isArray obj { | | throw new TypeError 'Media must be a plain object' ; | | } | | | | this. data = obj; | | return this; | | } | | | | clearButtons { | | this. buttons = ; | | return this; | | } | | | | setParams obj { | | this. params = obj; | | return this; | | } | | | | addButton name, params { | | this. buttons.push { | | name, | | buttonParamsJson: typeof params === 'string' ? params : JSON.stringify params , | | } ; | | | | return this; | | } | | | | makeRow header = '', title = '', description = '', id = '' { | | if this. currentSelectionIndex === -1 || this. currentSectionIndex === -1 { | | throw new Error 'You need to create a selection and a section first' ; | | } | | const buttonParams = JSON.parse this. buttons this. currentSelectionIndex .buttonParamsJson ; | | buttonParams.sections this. currentSectionIndex .rows.push { header, title, description, id } ; | | this. buttons this. currentSelectionIndex .buttonParamsJson = JSON.stringify buttonParams ; | | return this; | | } | | | | makeSection title = '', highlight label = '' { | | if this. currentSelectionIndex === -1 { | | throw new Error 'You need to create a selection first' ; | | } | | const buttonParams = JSON.parse this. buttons this. currentSelectionIndex .buttonParamsJson ; | | buttonParams.sections.push { title, highlight label, rows: } ; | | this. currentSectionIndex = buttonParams.sections.length - 1; | | this. buttons this. currentSelectionIndex .buttonParamsJson = JSON.stringify buttonParams ; | | return this; | | } | | | | addSelection title, options = {} { | | this. buttons.push { name: 'single select', buttonParamsJson: JSON.stringify { title, sections: , ...options } } ; | | this. currentSelectionIndex = this. buttons.length - 1; | | this. currentSectionIndex = -1; | | return this; | | } | | | | addReply display text = '', id = '', options = {} { | | this. buttons.push { | | name: 'quick reply', | | buttonParamsJson: JSON.stringify { | | display text, | | id, | | ...options, | | } , | | } ; | | return this; | | } | | | | addCall display text = '', id = '', options = {} { | | this. buttons.push { | | name: 'cta call', | | buttonParamsJson: JSON.stringify { | | display text, | | id, | | ...options, | | } , | | } ; | | return this; | | } | | | | addReminder display text = '', id = '', options = {} { | | this. buttons.push { | | name: 'cta reminder', | | buttonParamsJson: JSON.stringify { | | display text, | | id, | | ...options, | | } , | | } ; | | return this; | | } | | | | addCancelReminder display text = '', id = '', options = {} { | | this. buttons.push { | | name: 'cta cancel reminder', | | buttonParamsJson: JSON.stringify { | | display text, | | id, | | ...options, | | } , | | } ; | | return this; | | } | | | | addAddress display text = '', id = '', options = {} { | | this. buttons.push { | | name: 'address message', | | buttonParamsJson: JSON.stringify { | | display text, | | id, | | ...options, | | } , | | } ; | | return this; | | } | | | | addLocation options = {} { | | this. buttons.push { | | name: 'send location', | | buttonParamsJson: JSON.stringify options , | | } ; | | return this; | | } | | | | addUrl display text = '', url = '', webview interaction = false, options = {} { | | this. buttons.push { | | ...options, | | name: 'cta url', | | buttonParamsJson: JSON.stringify { | | display text, | | url, | | webview interaction, | | ...options, | | } , | | } ; | | return this; | | } | | | | addCopy display text = '', copy code = '', options = {} { | | this. buttons.push { | | name: 'cta copy', | | buttonParamsJson: JSON.stringify { | | display text, | | copy code, | | ...options, | | } , | | } ; | | return this; | | } | | | | static paramsList = { | | limited time offer: { | | text: 'string', | | url: 'string', | | copy code: 'string', | | expiration time: 'number', | | }, | | bottom sheet: { | | in thread buttons limit: 'number', | | divider indices: 'number' , | | list title: 'string', | | button title: 'string', | | }, | | tap target configuration: { | | title: 'string', | | description: 'string', | | canonical url: 'string', | | domain: 'string', | | buttonIndex: 'number', | | }, | | }; | | | | async toCard { | | return { | | body: { | | text: this. body, | | }, | | footer: { | | text: this. footer, | | }, | | header: { | | title: this. title, | | subtitle: this. subtitle, | | hasMediaAttachment: this. data, | | ... this. data | | ? await prepareWAMessageMedia this. data, { upload: this. client.waUploadToServer } .catch e = { | | if String e .includes 'Invalid media type' return this. data; | | throw e; | | } | | : {} , | | }, | | nativeFlowMessage: { | | messageParamsJson: JSON.stringify this. params , | | buttons: this. buttons, | | }, | | }; | | } | | | | async build jid, { messageId, ...options } = {} { | | const message = await this.toCard ; | | | | return generateWAMessageFromContent | | jid, | | { | | ...this. extraPayload, | | interactiveMessage: { | | ...message, | | contextInfo: this. contextInfo, | | }, | | }, | | { messageId: messageId || generateMessageIDV2 , ...options } | | ; | | } | | | | async send jid, { messageId, additionalNodes = , ...options } = {} { | | const msg = await this.build jid, { messageId, ...options } ; | | | | await this. client.relayMessage msg.key.remoteJid, msg.message, { | | messageId: msg.key.id, | | additionalNodes: | | { | | tag: 'biz', | | attrs: {}, | | content: | | { | | tag: 'interactive', | | attrs: { type: 'native flow', v: '1' }, | | content: { tag: 'native flow', attrs: { v: '9', name: 'mixed' } } , | | }, | | , | | }, | | ...additionalNodes, | | , | | ...options, | | } ; | | return msg; | | } | | } | | | | class ButtonV2 extends BaseBuilder { | | client; | | | | constructor client { | | super ; | | if client { | | throw new Error 'Socket is required' ; | | } | | | | this. client = client; | | this. image; | | this. data; | | this. buttons = ; | | } | | | | loadFrom msg { | | if msg throw new Error 'buttonsMessage needed' ; | | if msg.buttonsMessage throw new Error 'buttonsMessage not found' ; | | | | const { buttonsMessage, ...extraPayload } = msg; | | const bM = buttonsMessage; | | const location = bM.locationMessage || {}; | | | | this. title = location.name || ''; | | this. subtitle = location.address || ''; | | this. body = bM.contentText || ''; | | this. footer = bM.footerText || ''; | | this. contextInfo = bM.contextInfo || {}; | | this. extraPayload = extraPayload; | | | | this. buttons = Array.isArray bM.buttons | | ? bM.buttons.map button = { | | ...button, | | ... button.nativeFlowInfo | | ? { | | nativeFlowInfo: { | | ...button.nativeFlowInfo, | | paramsJson: typeof button.nativeFlowInfo.paramsJson === 'string' ? button.nativeFlowInfo.paramsJson : JSON.stringify button.nativeFlowInfo.paramsJson || {} , | | }, | | } | | : {} , | | } | | : ; | | | | this. image = location.jpegThumbnail || undefined; | | | | if this. image && bM.locationMessage { | | this. image = undefined; | | } | | | | if bM.locationMessage && bM.headerType === 6 { | | this. image = undefined; | | } | | | | this. data = Object.keys bM .reduce data, key = { | | if 'contentText', 'footerText', 'contextInfo', 'buttons', 'headerType', 'locationMessage', 'viewOnce' .includes key { | | data key = bM key ; | | } | | return data; | | }, {} ; | | | | if Object.keys this. data .length { | | this. data = undefined; | | } | | | | return this; | | } | | | | addButton displayText = '', buttonId = crypto.randomUUID { | | this. buttons.push { | | buttonId, | | buttonText: { displayText }, | | type: 1, | | } ; | | return this; | | } | | | | addRawButton obj { | | if typeof obj == 'object' || obj === null || Array.isArray obj { | | throw new TypeError 'Buttons must be a plain object' ; | | } | | | | this. buttons.push obj ; | | return this; | | } | | | | setRawThumbnail thumbnail { | | if thumbnail throw new Error 'Thumbnail needed' ; | | this. image = { base64: thumbnail, is raw: true }; | | return this; | | } | | | | setThumbnail path { | | if path throw new Error 'Url or buffer needed' ; | | this. image = path; | | return this; | | } | | | | setMedia obj { | | if typeof obj == 'object' || obj === null || Array.isArray obj { | | throw new TypeError 'Media must be a plain object' ; | | } | | | | this. data = obj; | | return this; | | } | | | | async build jid, { messageId, ...options } = {} { | | const thumbnail = this. image?.is raw | | ? this. image.base64 | | : this. image | | ? await Toolkit.resize Buffer.isBuffer this. image ? this. image : await Toolkit.fetchBuffer this. image, {}, { silent: true } , 300, 300 | | : null; | | const msg = generateWAMessageFromContent | | jid, | | { | | ...this. extraPayload, | | buttonsMessage: { | | contentText: this. body, | | footerText: this. footer, | | ... this. data | | ? this. data | | : { | | headerType: 6, | | locationMessage: { | | degreesLatitude: 0, | | degreesLongitude: 0, | | name: this. title, | | address: this. subtitle, | | jpegThumbnail: thumbnail, | | }, | | } , | | viewOnce: true, | | contextInfo: this. contextInfo, | | buttons: ...this. buttons , | | }, | | }, | | { messageId: messageId || generateMessageIDV2 , ...options } | | ; | | return msg; | | } | | | | async send jid, { messageId, additionalNodes = , ...options } = {} { | | if this. buttons.length < 1 throw new Error 'ButtonV2 requires at least one button' ; | | const msg = await this.build jid, { messageId, ...options } ; | | | | await this. client.relayMessage msg.key.remoteJid, msg.message, { | | messageId: msg.key.id, | | additionalNodes: | | { | | tag: 'biz', | | attrs: {}, | | content: | | { | | tag: 'interactive', | | attrs: { type: 'native flow', v: '1' }, | | content: { tag: 'native flow', attrs: { v: '9', name: 'mixed' } } , | | }, | | , | | }, | | ...additionalNodes, | | , | | ...options, | | } ; | | return msg; | | } | | } | | | | class Carousel extends BaseBuilder { | | client; | | | | constructor client { | | super ; | | if client { | | throw new Error 'Socket is required' ; | | } | | | | this. client = client; | | this. cards = ; | | } | | | | loadFrom msg { | | if msg throw new Error 'interactiveMessage needed' ; | | if msg.interactiveMessage throw new Error 'interactiveMessage not found' ; | | | | const { interactiveMessage, ...extraPayload } = msg; | | const iM = interactiveMessage; | | const carousel = iM.carouselMessage || {}; | | | | this. body = iM.body?.text || ''; | | this. footer = iM.footer?.text || ''; | | this. contextInfo = iM.contextInfo || {}; | | this. extraPayload = extraPayload; | | | | this. cards = Array.isArray carousel.cards | | ? carousel.cards.map card = { | | ...card, | | header: { | | ... card.header || {} , | | hasMediaAttachment: card.header?.hasMediaAttachment, | | ... card.header?.imageMessage ? { imageMessage: card.header.imageMessage } : {} , | | ... card.header?.videoMessage ? { videoMessage: card.header.videoMessage } : {} , | | }, | | body: { | | text: card.body?.text || '', | | }, | | footer: { | | text: card.footer?.text || '', | | }, | | nativeFlowMessage: { | | ... card.nativeFlowMessage || {} , | | buttons: Array.isArray card.nativeFlowMessage?.buttons | | ? card.nativeFlowMessage.buttons.map button = { | | ...button, | | buttonParamsJson: typeof button.buttonParamsJson === 'string' ? button.buttonParamsJson : JSON.stringify button.buttonParamsJson || {} , | | } | | : , | | messageParamsJson: | | typeof card.nativeFlowMessage?.messageParamsJson === 'string' ? card.nativeFlowMessage.messageParamsJson : JSON.stringify card.nativeFlowMessage?.messageParamsJson || {} , | | }, | | } | | : ; | | | | return this; | | } | | | | addCard card { | | const cards = Array.isArray card ? card : card ; | | const baseIndex = this. cards.length; | | | | for const index, c of cards.entries { | | if c?.header?.hasMediaAttachment { | | throw new Error Card ${baseIndex + index} must include an image or video in header ; | | } | | } | | | | this. cards.push ...cards ; | | return this; | | } | | | | build jid, { messageId, ...options } = {} { | | return generateWAMessageFromContent | | jid, | | { | | ...this. extraPayload, | | interactiveMessage: { | | header: { | | hasMediaAttachment: false, | | }, | | body: { text: this. body }, | | footer: { text: this. footer }, | | contextInfo: this. contextInfo, | | carouselMessage: { | | cards: this. cards, | | }, | | }, | | }, | | { messageId: messageId || generateMessageIDV2 , ...options } | | ; | | } | | | | async send jid, { messageId, additionalNodes = , ...options } = {} { | | const msg = this.build jid, { messageId, ...options } ; | | | | await this. client.relayMessage msg.key.remoteJid, msg.message, { | | messageId: msg.key.id, | | additionalNodes: | | { | | tag: 'biz', | | attrs: {}, | | content: | | { | | tag: 'interactive', | | attrs: { type: 'native flow', v: '1' }, | | content: { tag: 'native flow', attrs: { v: '9', name: 'mixed' } } , | | }, | | , | | }, | | ...additionalNodes, | | , | | ...options, | | } ; | | return msg; | | } | | } | | | | class AIRich extends BaseBuilder { | | client; | | | | constructor client, { dynamic = true, unsupportedTypeAlert = true } = {} { | | if client { | | throw new Error 'Socket is required' ; | | } | | | | super ; | | this. client = client; | | this. contextInfo = {}; | | this. nodes = ; | | this. idIndex = new Map ; | | this. unsupportedTypeAlert = unsupportedTypeAlert; | | this. dynamic = dynamic; | | this. responseId = crypto.randomUUID ; | | this. botResponseId = crypto.randomUUID ; | | this. lastMessageKey = null; | | } | | | | loadFrom msg { | | if msg throw new Error 'AI Rich message needed' ; | | | | const message = msg.message ?? msg; | | | | let richResponseMessage = message?.botForwardedMessage?.message?.richResponseMessage; | | | | if richResponseMessage { | | richResponseMessage = message?.botForwardedMessage?.richResponseMessage; | | } | | | | if richResponseMessage { | | richResponseMessage = message?.richResponseMessage; | | } | | | | if richResponseMessage { | | throw new Error 'richResponseMessage not found' ; | | } | | | | const messageContextInfo = message?.messageContextInfo ?? {}; | | const botMetadata = messageContextInfo?.botMetadata ?? {}; | | | | this. title = botMetadata?.messageDisclaimerText ?? ''; | | | | this. contextInfo = structuredClone richResponseMessage?.contextInfo ?? {} ; | | | | const loadedSubmessages = Array.isArray richResponseMessage?.submessages ? structuredClone richResponseMessage.submessages : ; | | | | let loadedSections = ; | | | | const unifiedData = richResponseMessage?.unifiedResponse?.data; | | | | if unifiedData { | | try { | | const decoded = Buffer.from unifiedData, 'base64' .toString 'utf8' ; | | const unifiedResponse = JSON.parse decoded ; | | | | if Array.isArray unifiedResponse?.sections { | | loadedSections = structuredClone unifiedResponse.sections ; | | } | | } catch {} | | } | | | | this. nodes = ; | | this. idIndex = new Map ; | | | | const maxLength = Math.max loadedSections.length, loadedSubmessages.length ; | | | | for let i = 0; i < maxLength; i++ { | | this. nodes.push { | | id: null, | | section: loadedSections i ?? null, | | submessage: loadedSubmessages i ?? null, | | } ; | | } | | | | this. extraPayload = {}; | | | | for const key, value of Object.entries message { | | if key == 'messageContextInfo' && key == 'botForwardedMessage' && key == 'richResponseMessage' { | | this. extraPayload key = structuredClone value ; | | } | | } | | | | return this; | | } | | | | setResponseId id { | | if typeof id == 'string' { | | throw new TypeError 'ID must be a string' ; | | } | | this. responseId = id; | | | | return this; | | } | | | | refreshResponseId { | | this. responseId = crypto.randomUUID ; | | | | return this; | | } | | | | setBotResponseId id { | | if typeof id == 'string' { | | throw new TypeError 'ID must be a string' ; | | } | | this. botResponseId = id; | | | | return this; | | } | | | | refreshBotResponseId { | | this. botResponseId = crypto.randomUUID ; | | | | return this; | | } | | | | createAlert type { | | if this. unsupportedTypeAlert { | | return { | | messageType: 2, | | messageText: UNSUPPORTED TYPE - ${type} , | | }; | | } | | | | return undefined; | | } | | | | addText text, { hyperlink = true, citation = true, latex = true, id, replace, insertAt } = {} { | | if typeof text == 'string' { | | throw new TypeError 'Text must be a string' ; | | } | | | | const { text: extractedText, inline entities } = extractIE text, { | | hyperlink, | | citation, | | latex, | | } ; | | | | const section = AIRich.newLayout 'Single', { | | text: extractedText, | | ... inline entities.length && { inline entities } , | | typename: 'GenAIMarkdownTextUXPrimitive', | | } ; | | | | const submessages = | | { | | messageType: 2, | | messageText: text, | | }, | | .filter Boolean ; | | | | return this. addContent section, submessages, { | | id, | | replace, | | insertAt, | | } ; | | } | | | | addFOAText text, { id, replace, insertAt } = {} { | | if typeof text == 'string' { | | throw new TypeError 'Text must be a string' ; | | } | | | | const section = AIRich.newLayout 'Single', { | | text, | | typename: 'FOATextPrimitive', | | } ; | | | | const submessages = | | { | | messageType: 2, | | messageText: text, | | }, | | ; | | | | return this. addContent section, submessages, { | | id, | | replace, | | insertAt, | | } ; | | } | | | | addCode language, code, { id, replace, insertAt } = {} { | | if typeof language == 'string' || typeof code == 'string' { | | throw new TypeError 'Language and code must be a string' ; | | } | | | | const meta = AIRich.tokenizer code, language ; | | | | const section = AIRich.newLayout 'Single', { | | language, | | code blocks: meta.unified codeBlock, | | typename: 'GenAICodeUXPrimitive', | | } ; | | | | const submessages = | | { | | messageType: 5, | | codeMetadata: { | | codeLanguage: language, | | codeBlocks: meta.codeBlock, | | }, | | }, | | ; | | | | return this. addContent section, submessages, { | | id, | | replace, | | insertAt, | | } ; | | } | | | | addTable table, { hyperlink = true, citation = true, latex = true, id, replace, insertAt } = {} { | | if Array.isArray table { | | throw new TypeError 'Table must be an array' ; | | } | | | | const meta = AIRich.toTableMetadata table, { | | hyperlink, | | citation, | | latex, | | } ; | | | | const section = AIRich.newLayout 'Single', { | | rows: meta.unified rows, | | typename: 'GenATableUXPrimitive', | | } ; | | | | const submessages = | | { | | messageType: 4, | | tableMetadata: { | | title: meta.title, | | rows: meta.rows, | | }, | | }, | | ; | | | | return this. addContent section, submessages, { | | id, | | replace, | | insertAt, | | } ; | | } | | | | addSource sources = , { id, replace, insertAt } = {} { | | if Array.isArray sources { | | throw new TypeError 'Sources must be an array of strings, arrays, or objects' ; | | } | | | | const isStringArray = sources.every item = typeof item === 'string' ; | | | | const isArrayFormat = sources.every item = Array.isArray item && item.every value = typeof value === 'string' ; | | | | const isObjectFormat = sources.every item = item && typeof item === 'object' && Array.isArray item ; | | | | if isStringArray && isArrayFormat && isObjectFormat { | | throw new TypeError 'Sources must be a string array, array of string arrays, or array of objects' ; | | } | | | | if isStringArray { | | sources = sources ; | | } | | | | const normalizedSources = sources.map source = { | | if Array.isArray source { | | const icon, url, title, subtitle = source; | | | | return { | | icon, | | url, | | title, | | subtitle, | | }; | | } | | | | return { | | icon: source.favicon ?? source.icon ?? '', | | url: source.url ?? '', | | title: source.title ?? '', | | subtitle: source.subtitle ?? '', | | }; | | } ; | | | | const source = normalizedSources.map { icon, url, title, subtitle } = { | | source type: 'THIRD PARTY', | | source display name: title, | | source subtitle: subtitle, | | source url: url, | | favicon: { | | url: Toolkit.resolveMedia this. client, icon, 'image' , | | mime type: 'image/jpeg', | | width: 16, | | height: 16, | | }, | | } ; | | | | const submessage = this.createAlert 'GenAISearchResultPrimitive' ; | | | | const section = AIRich.newLayout 'Single', { | | sources: source, | | typename: 'GenAISearchResultPrimitive', | | } ; | | | | return this. addContent section, submessage, { | | id, | | replace, | | insertAt, | | } ; | | } | | | | addReels reelsItems = , { id, replace, insertAt } = {} { | | if | | | | reelsItems && typeof reelsItems === 'object' && Array.isArray reelsItems || | | Array.isArray reelsItems && reelsItems.every item = item && typeof item === 'object' && Array.isArray item | | | | { | | throw new TypeError 'Reels items must be an object or an array of objects' ; | | } | | | | const items = Array.isArray reelsItems ? reelsItems : reelsItems ; | | | | const reels = items.map item = { | | ...item, | | avatar: Toolkit.resolveMedia this. client, item.profileIconUrl ?? item.profile url ?? item.profile ?? '', 'image' , | | thumbnail: Toolkit.resolveMedia this. client, item.thumbnailUrl ?? item.thumbnail ?? '', 'image' , | | } ; | | | | const section = AIRich.newLayout | | 'HScroll', | | reels.map item = { | | reels url: item.videoUrl ?? item.url ?? '', | | thumbnail url: item. thumbnail, | | creator: item.username ?? item.title ?? '', | | avatar url: item. avatar, | | reels title: item.reels title ?? item.title ?? '', | | likes count: item.likes count ?? item.like ?? 0, | | shares count: item.shares count ?? item.share ?? 0, | | view count: item.view count ?? item.view ?? 0, | | reel source: item.reel source ?? item.source ?? 'IG', | | is verified: item.is verified || item.verified , | | typename: 'GenAIReelPrimitive', | | } | | ; | | | | const submessages = | | { | | messageType: 9, | | contentItemsMetadata: { | | contentType: 1, | | itemsMetadata: reels.map item = { | | reelItem: { | | title: item.username ?? '', | | profileIconUrl: item. avatar, | | thumbnailUrl: item. thumbnail, | | videoUrl: item.videoUrl ?? item.url ?? '', | | }, | | } , | | }, | | }, | | ; | | | | return this. addContent section, submessages, { | | id, | | replace, | | insertAt, | | } ; | | } | | | | addImage imageUrl, { width, height, status = 'READY', update text, resolveUrl = false, id, replace, insertAt } = {} { | | if typeof imageUrl === 'string' || Buffer.isBuffer imageUrl || Array.isArray imageUrl && imageUrl.every v = typeof v === 'string' || Buffer.isBuffer v { | | throw new TypeError 'imageUrl must be string | buffer | array of string/buffer' ; | | } | | | | const list = Array.isArray imageUrl | | ? imageUrl.map v = { | | const url = Toolkit.resolveMedia this. client, v, 'image', { resolveUrl } ; | | | | return { | | imagePreviewUrl: url, | | imageHighResUrl: url, | | sourceUrl: url, | | }; | | } | | : = { | | const url = Toolkit.resolveMedia this. client, imageUrl, 'image', { resolveUrl } ; | | | | return | | { | | imagePreviewUrl: url, | | imageHighResUrl: url, | | sourceUrl: url, | | }, | | ; | | } ; | | | | const sections = list.map { imagePreviewUrl } = | | AIRich.newLayout 'Single', { | | media: { | | url: imagePreviewUrl, | | mime type: 'image/png', | | width, | | height, | | }, | | imagine type: 'IMAGE', | | status: { | | status, | | update text, | | }, | | typename: 'GenAIImaginePrimitive', | | } | | ; | | | | const submessage = { | | messageType: 1, | | gridImageMetadata: { | | gridImageUrl: { | | imagePreviewUrl: list 0 ?.imagePreviewUrl, | | }, | | imageUrls: list, | | }, | | }; | | | | if id && sections.length == 1 { | | throw new Error 'Cannot assign one id to multiple image sections' ; | | } | | | | return this. addContent sections, submessage, { | | id, | | replace, | | insertAt, | | } ; | | } | | | | addVideo videoUrl, { autoFill = true, status = 'READY', estimatedTime, id, replace, insertAt } = {} { | | const isObjectVideo = v = v && typeof v === 'object' && Array.isArray v && v.url; | | | | const isValidPrimitive = | | typeof videoUrl === 'string' || | | Buffer.isBuffer videoUrl || | | isObjectVideo videoUrl || | | Array.isArray videoUrl && videoUrl.every v = typeof v === 'string' || Buffer.isBuffer v || isObjectVideo v ; | | | | if isValidPrimitive { | | throw new TypeError 'videoUrl must be string | buffer | object | array' ; | | } | | | | const items = Array.isArray videoUrl ? videoUrl : videoUrl ; | | | | const alert = this.createAlert 'GenAIImaginePrimitive ANIMATE ' ; | | | | const sections = ; | | const submessages = ; | | | | for const item of items { | | const isObject = isObjectVideo item ; | | | | const url = isObject ? Toolkit.resolveMedia this. client, item.url ?? '', 'video' : Toolkit.resolveMedia this. client, item, 'video' ; | | | | const bufferPromise = autoFill ? Promise.resolve url .then u = Toolkit.fetchBuffer u : null; | | | | const file length = isObject && item.file length = null ? item.file length : autoFill ? bufferPromise.then b = b?.length ?? 0 : 0; | | | | const duration = | | isObject && item.duration = null | | ? item.duration | | : autoFill | | ? bufferPromise.then b = | | Toolkit.getMp4Duration b, { | | silent: true, | | } | | | | : 0; | | | | const thumbnail = | | isObject && item.thumbnail | | ? Toolkit.resolveMedia this. client, item.thumbnail, 'image', { | | result: 'base64', | | resize: true, | | width: 300, | | height: 300, | | } | | : autoFill | | ? bufferPromise?.then b = | | Toolkit.getMp4Preview b, { | | time: 0, | | result: 'base64', | | } | | | | : null; | | | | sections.push | | AIRich.newLayout 'Single', { | | media: { | | url, | | mime type: isObject ? item.mime type ?? 'video/mp4' : 'video/mp4', | | file length, | | duration, | | }, | | imagine type: 'ANIMATE', | | status: { | | status, | | estimated completion time: estimatedTime = null ? Math.floor Date.now + estimatedTime / 1000 : undefined, | | }, | | thumbnail: { | | raw media: thumbnail, | | }, | | typename: 'GenAIImaginePrimitive', | | } | | ; | | } | | | | if alert == undefined { | | submessages.push alert ; | | } | | | | if submessages.length 1 { | | throw new Error 'Video content can only have one submessage' ; | | } | | | | return this. addContent sections, submessages 0 , { | | id, | | replace, | | insertAt, | | } ; | | } | | | | addProduct data = {}, { id, replace, insertAt } = {} { | | if data && typeof data === 'object' && Array.isArray data || Array.isArray data && data.every item = item && typeof item === 'object' && Array.isArray item { | | throw new TypeError 'Product items must be an object or an array of objects' ; | | } | | | | const items = Array.isArray data ? data : data ; | | | | const product = items.map item = { | | title: item.title, | | brand: item.brand, | | price: item.price, | | sale price: item.sale price, | | product url: item.product url ?? item.url, | | image: { | | url: Toolkit.resolveMedia this. client, item.image url ?? item.image, 'image' , | | }, | | additional images: | | { | | url: Toolkit.resolveMedia this. client, item.icon url ?? item.icon, 'image' , | | }, | | , | | typename: 'GenAIProductItemCardPrimitive', | | } ; | | | | const section = AIRich.newLayout Array.isArray data ? 'HScroll' : 'Single', Array.isArray data ? product : product 0 ; | | | | const submessage = this.createAlert 'GenAIProductItemCardPrimitive' ; | | | | return this. addContent section, submessage, { | | id, | | replace, | | insertAt, | | } ; | | } | | | | addPost data = {}, { id, replace, insertAt } = {} { | | if data && typeof data === 'object' && Array.isArray data || Array.isArray data && data.every item = item && typeof item === 'object' && Array.isArray item { | | throw new TypeError 'Post items must be an object or an array of objects' ; | | } | | | | const posts = Array.isArray data ? data : data ; | | | | const primitives = posts.map p = { | | title: p.title ?? '', | | subtitle: p.subtitle ?? '', | | username: p.username ?? '', | | profile picture url: Toolkit.resolveMedia this. client, p.profile picture url ?? p.profile url ?? p.profile ?? '', 'image' , | | is verified: p.is verified || p.verified , | | thumbnail url: Toolkit.resolveMedia this. client, p.thumbnail url ?? p.thumbnail ?? '', 'image' , | | post caption: p.post caption ?? p.caption ?? '', | | likes count: p.likes count ?? p.like ?? 0, | | comments count: p.comments count ?? p.comment ?? 0, | | shares count: p.shares count ?? p.share ?? 0, | | post url: p.post url ?? p.url ?? '', | | post deeplink: p.post deeplink ?? p.deeplink ?? '', | | source app: p.source app || p.source || 'INSTAGRAM', | | footer label: p.footer label ?? p.footer ?? '', | | footer icon: Toolkit.resolveMedia this. client, p.footer icon ?? p.icon ?? '', 'image' , | | is carousel: posts.length 1, | | orientation: p.orientation ?? 'LANDSCAPE', | | post type: p.post type ?? 'VIDEO', | | typename: 'GenAIPostPrimitive', | | } ; | | | | const section = AIRich.newLayout 'HScroll', primitives ; | | | | const submessage = this.createAlert 'GenAIPostPrimitive' ; | | | | return this. addContent section, submessage, { | | id, | | replace, | | insertAt, | | } ; | | } | | | | addMetadata text, { id, replace, insertAt } = {} { | | if typeof text == 'string' { | | throw new TypeError 'Text must be a string' ; | | } | | | | const section = AIRich.newLayout 'Single', { | | text, | | typename: 'GenAIMetadataTextPrimitive', | | } ; | | | | const submessage = { | | messageType: 2, | | messageText: text, | | }; | | | | return this. addContent section, submessage, { | | id, | | replace, | | insertAt, | | } ; | | } | | | | addTip text, { id, replace, insertAt } = {} { | | if typeof text == 'string' { | | throw new TypeError 'Text must be a string' ; | | } | | | | const section = AIRich.newLayout 'Single', { | | text: 'ⓘ ' + text, | | typename: 'GenAIMetadataTextPrimitive', | | } ; | | | | const submessage = { | | messageType: 2, | | messageText: text, | | }; | | | | return this. addContent section, submessage, { | | id, | | replace, | | insertAt, | | } ; | | } | | | | addWidget data, { layout, id, replace, insertAt, ...options } = {} { | | if data && typeof data === 'object' && Array.isArray data || Array.isArray data && data.every item = item && typeof item === 'object' && Array.isArray item { | | throw new TypeError 'Widget must be an object or an array of objects' ; | | } | | | | const isArray = Array.isArray data ; | | | | const items = isArray ? data : data ; | | | | const widgets = items.map item = { | | typename: 'GenAI3PExtWidgetPrimitive', | | | | header: { | | typename: 'GenAI3PExtWidgetStandardHeader', | | title: item.title ?? '', | | ... item.header ?? {} , | | }, | | | | body: { | | typename: 'GenAI3PExtCalendarEventList', | | sections: item.sections ?? , | | | | ctas: item.actions ?? .map action = { | | typename: 'GenAI3PExtWidgetCTA', | | label: action.label ?? '', | | state: action.state ?? 'PENDING', | | kind: action.kind ?? 'OTHER', | | tool call id: action.tool call id ?? action.id ?? '', | | | | ... action.toast && { | | toast: { | | typename: 'GenAI3PExtWidgetToast', | | label: action.toast.label ?? action.label ?? '', | | }, | | } , | | } , | | | | ... item.body ?? {} , | | }, | | } ; | | | | const section = AIRich.newLayout layout ?? isArray ? 'HScroll' : 'Single' , isArray ? widgets : widgets 0 , options ; | | | | const submessage = this.createAlert 'GenAI3PExtWidgetStandardHeader' ; | | | | return this. addContent section, submessage, { | | id, | | replace, | | insertAt, | | } ; | | } | | | | addFooterAction data, { layout, id, replace, insertAt, ...options } = {} { | | if data && typeof data === 'object' && Array.isArray data || Array.isArray data && data.every item = item && typeof item === 'object' && Array.isArray item { | | throw new TypeError 'Footer action must be an object or an array of objects' ; | | } | | | | const isArray = Array.isArray data ; | | | | const items = isArray ? data : data ; | | | | const actions = items.map item = { | | typename: 'GenAIFooterActionPrimitive', | | | | cta text: item.text ?? item.cta text ?? '', | | | | cta type: item.type ?? item.cta type ?? 'OPEN URL', | | | | cta url: item.url ?? item.cta url ?? '', | | } ; | | | | const section = AIRich.newLayout layout ?? isArray ? 'HScroll' : 'Single' , isArray ? actions : actions 0 , options ; | | | | const submessage = this.createAlert 'GenAIFooterActionPrimitive' ; | | | | return this. addContent section, submessage, { | | id, | | replace, | | insertAt, | | } ; | | } | | | | addSuggest suggestion, { scroll = true, layout, id, replace, insertAt } = {} { | | if typeof suggestion === 'string' || Array.isArray suggestion && suggestion.every v = typeof v === 'string' { | | throw new TypeError 'Suggestion must be a string or array of strings' ; | | } | | | | const suggest = Array.isArray suggestion | | ? suggestion.map text = { | | prompt text: text, | | prompt type: 'SUGGESTED PROMPT', | | typename: 'GenAIFollowUpSuggestionPillPrimitive', | | } | | : | | { | | prompt text: suggestion, | | prompt type: 'SUGGESTED PROMPT', | | typename: 'GenAIFollowUpSuggestionPillPrimitive', | | }, | | ; | | | | const type = layout ?? suggest.length === 1 ? 'Single' : scroll ? 'HScroll' : 'ActionRow' ; | | | | const section = AIRich.newLayout type, type === 'Single' ? suggest 0 : suggest, { | | typename: 'GenAIUnifiedResponseSection', | | } ; | | | | const submessage = this.createAlert 'GenAIFollowUpSuggestionPillPrimitive' ; | | | | return this. addContent section, submessage, { | | id, | | replace, | | insertAt, | | } ; | | } | | | | async build | | jid, | | { bypassDownload = true, forwarded = true, notification = false, includesUnifiedResponse = true, includesSubmessages = true, quoted, quotedParticipant, messageId, ...options } = {} | | { | | const forward = forwarded | | ? { | | forwardingScore: 1, | | isForwarded: true, | | forwardedAiBotMessageInfo: { botJid: '867051314767696@bot' }, | | forwardOrigin: 4, | | } | | : {}; | | | | const notif = notification | | ? { | | sessionTransparencyMetadata: { | | disclaimerText: '~ Ahmad tumbuh kembang', | | hcaId: hca ${Date.now } , | | sessionTransparencyType: 1, | | }, | | } | | : {}; | | | | const qObj = quoted | | ? { | | stanzaId: quoted?.key?.id || quoted?.id, | | participant: quotedParticipant || quoted?.key?.participant || quoted?.participant || quoted?.key?.remoteJid, | | quotedType: 0, | | quotedMessage: typeof quoted === 'object' && quoted == null ? quoted.message ?? quoted : undefined, | | } | | : {}; | | | | const sections = this. footer | | ? | | ... await waitAllPromises this. sections , | | AIRich.newLayout 'Single', { | | text: this. footer, | | typename: 'GenAIMetadataTextPrimitive', | | } , | | | | : ... await waitAllPromises this. sections ; | | | | if this. dynamic { | | this.refreshResponseId ; | | this.refreshBotResponseId ; | | } | | | | return generateWAMessageFromContent | | jid, | | { | | messageContextInfo: { | | deviceListMetadata: {}, | | deviceListMetadataVersion: 2, | | botMetadata: { | | messageDisclaimerText: this. title, | | ...notif, | | verificationMetadata: AIRich.generateVerificationMetadata , | | botResponseId: this. botResponseId, | | }, | | }, | | ...this. extraPayload, | | botForwardedMessage: { | | message: { | | richResponseMessage: { | | messageType: 1, | | submessages: includesSubmessages ? await waitAllPromises this. submessages : , | | unifiedResponse: { | | data: includesUnifiedResponse ? Buffer.from Toolkit.stringifyEscaped { response id: this. responseId, sections } .toString 'base64' : '', | | }, | | contextInfo: { | | ...forward, | | ...qObj, | | ...this. contextInfo, | | }, | | }, | | }, | | }, | | }, | | { messageId: messageId || generateMessageIDV2 , ...options } | | ; | | } | | | | async buildEdit targetJid, targetId, { msg, messageId, ...options } = {} { | | if msg { | | msg = await this.build targetJid, options .message; | | } | | | | const editedMessage = msg; | | | | if editedMessage { | | throw new Error 'buildEdit: msg does not contain botForwardedMessage' ; | | } | | | | return generateWAMessageFromContent | | targetJid, | | { | | botForwardedMessage: { | | message: { | | protocolMessage: { | | key: { | | remoteJid: targetJid, | | fromMe: true, | | id: targetId, | | }, | | type: 14, | | editedMessage, | | }, | | }, | | }, | | }, | | { messageId: messageId || generateMessageIDV2 , ...options } | | ; | | } | | | | async sendEdit jid, id, { msg, messageId, additionalNodes = , ...options } = {} { | | jid = jid ?? this. lastMessageKey?.remoteJid; | | id = id ?? this. lastMessageKey?.id; | | | | if jid { | | throw new Error 'JID is required' ; | | } | | | | if id { | | throw new Error 'Message id is required' ; | | } | | | | const msgEdit = await this.buildEdit jid, id, { | | msg, | | messageId: messageId || generateMessageIDV2 , | | ...options, | | } ; | | | | await this. client.relayMessage jid, msgEdit.message, { | | messageId: msgEdit.key.id, | | additionalNodes, | | } ; | | | | return msgEdit; | | } | | | | async send jid, { bypassDownload = true, forwarded = true, notification = false, includesUnifiedResponse = true, includesSubmessages = true, messageId, additionalNodes = , ...options } = {} { | | const msg = await this.build jid, { | | forwarded, | | notification, | | includesUnifiedResponse, | | includesSubmessages, | | messageId, | | ...options, | | } ; | | | | await this. client.relayMessage msg.key.remoteJid, msg.message, { | | messageId: msg.key.id, | | additionalNodes, | | ...options, | | } ; | | | | if includesUnifiedResponse && bypassDownload { | | await this.sendEdit jid, msg.key.id, { | | msg: msg.message, | | } ; | | } | | | | this. lastMessageKey = msg.key; | | | | return msg; | | } | | | | static tokenizer code, lang = 'javascript' { | | const keywordsMap = { | | javascript: new Set | | 'break', | | 'case', | | 'catch', | | 'continue', | | 'debugger', | | 'delete', | | 'do', | | 'else', | | 'finally', | | 'for', | | 'function', | | 'if', | | 'in', | | 'instanceof', | | 'new', | | 'return', | | 'switch', | | 'this', | | 'throw', | | 'try', | | 'typeof', | | 'var', | | 'void', | | 'while', | | 'with', | | 'true', | | 'false', | | 'null', | | 'undefined', | | 'class', | | 'const', | | 'let', | | 'super', | | 'extends', | | 'export', | | 'import', | | 'yield', | | 'static', | | 'constructor', | | 'async', | | 'await', | | 'get', | | 'set', | | , | | | | typescript: new Set | | 'abstract', | | 'any', | | 'as', | | 'asserts', | | 'bigint', | | 'boolean', | | 'declare', | | 'enum', | | 'implements', | | 'infer', | | 'interface', | | 'is', | | 'keyof', | | 'module', | | 'namespace', | | 'never', | | 'readonly', | | 'require', | | 'number', | | 'object', | | 'override', | | 'private', | | 'protected', | | 'public', | | 'satisfies', | | 'string', | | 'symbol', | | 'type', | | 'unknown', | | 'using', | | 'from', | | 'break', | | 'case', | | 'catch', | | 'continue', | | 'do', | | 'else', | | 'finally', | | 'for', | | 'function', | | 'if', | | 'new', | | 'return', | | 'switch', | | 'this', | | 'throw', | | 'try', | | 'var', | | 'void', | | 'while', | | 'class', | | 'const', | | 'let', | | 'extends', | | 'import', | | 'export', | | 'async', | | 'await', | | , | | | | python: new Set | | 'False', | | 'None', | | 'True', | | 'and', | | 'as', | | 'assert', | | 'async', | | 'await', | | 'break', | | 'class', | | 'continue', | | 'def', | | 'del', | | 'elif', | | 'else', | | 'except', | | 'finally', | | 'for', | | 'from', | | 'global', | | 'if', | | 'import', | | 'in', | | 'is', | | 'lambda', | | 'nonlocal', | | 'not', | | 'or', | | 'pass', | | 'raise', | | 'return', | | 'try', | | 'while', | | 'with', | | 'yield', | | , | | | | java: new Set | | 'abstract', | | 'assert', | | 'boolean', | | 'break', | | 'byte', | | 'case', | | 'catch', | | 'char', | | 'class', | | 'const', | | 'continue', | | 'default', | | 'do', | | 'double', | | 'else', | | 'enum', | | 'extends', | | 'final', | | 'finally', | | 'float', | | 'for', | | 'goto', | | 'if', | | 'implements', | | 'import', | | 'instanceof', | | 'int', | | 'interface', | | 'long', | | 'native', | | 'new', | | 'package', | | 'private', | | 'protected', | | 'public', | | 'return', | | 'short', | | 'static', | | 'strictfp', | | 'super', | | 'switch', | | 'synchronized', | | 'this', | | 'throw', | | 'throws', | | 'transient', | | 'try', | | 'void', | | 'volatile', | | 'while', | | , | | | | golang: new Set | | 'break', | | 'case', | | 'chan', | | 'const', | | 'continue', | | 'default', | | 'defer', | | 'else', | | 'fallthrough', | | 'for', | | 'func', | | 'go', | | 'goto', | | 'if', | | 'import', | | 'interface', | | 'map', | | 'package', | | 'range', | | 'return', | | 'select', | | 'struct', | | 'switch', | | 'type', | | 'var', | | , | | | | c: new Set | | 'auto', | | 'break', | | 'case', | | 'char', | | 'const', | | 'continue', | | 'default', | | 'do', | | 'double', | | 'else', | | 'enum', | | 'extern', | | 'float', | | 'for', | | 'goto', | | 'if', | | 'int', | | 'long', | | 'register', | | 'return', | | 'short', | | 'signed', | | 'sizeof', | | 'static', | | 'struct', | | 'switch', | | 'typedef', | | 'union', | | 'unsigned', | | 'void', | | 'volatile', | | 'while', | | , | | | | cpp: new Set | | 'alignas', | | 'alignof', | | 'and', | | 'auto', | | 'bool', | | 'break', | | 'case', | | 'catch', | | 'class', | | 'const', | | 'constexpr', | | 'continue', | | 'delete', | | 'do', | | 'double', | | 'else', | | 'enum', | | 'explicit', | | 'export', | | 'extern', | | 'false', | | 'float', | | 'for', | | 'friend', | | 'if', | | 'inline', | | 'int', | | 'long', | | 'mutable', | | 'namespace', | | 'new', | | 'noexcept', | | 'nullptr', | | 'operator', | | 'private', | | 'protected', | | 'public', | | 'return', | | 'short', | | 'signed', | | 'sizeof', | | 'static', | | 'struct', | | 'switch', | | 'template', | | 'this', | | 'throw', | | 'true', | | 'try', | | 'typedef', | | 'typename', | | 'union', | | 'unsigned', | | 'using', | | 'virtual', | | 'void', | | 'while', | | , | | | | php: new Set | | 'abstract', | | 'and', | | 'array', | | 'as', | | 'break', | | 'callable', | | 'case', | | 'catch', | | 'class', | | 'clone', | | 'const', | | 'continue', | | 'declare', | | 'default', | | 'do', | | 'echo', | | 'else', | | 'elseif', | | 'empty', | | 'enddeclare', | | 'endfor', | | 'endforeach', | | 'endif', | | 'endswitch', | | 'endwhile', | | 'extends', | | 'final', | | 'finally', | | 'fn', | | 'for', | | 'foreach', | | 'function', | | 'global', | | 'goto', | | 'if', | | 'implements', | | 'include', | | 'include once', | | 'instanceof', | | 'interface', | | 'match', | | 'namespace', | | 'new', | | 'null', | | 'or', | | 'private', | | 'protected', | | 'public', | | 'require', | | 'require once', | | 'return', | | 'static', | | 'switch', | | 'throw', | | 'trait', | | 'try', | | 'use', | | 'var', | | 'while', | | 'yield', | | , | | | | rust: new Set | | 'as', | | 'break', | | 'const', | | 'continue', | | 'crate', | | 'else', | | 'enum', | | 'extern', | | 'false', | | 'fn', | | 'for', | | 'if', | | 'impl', | | 'in', | | 'let', | | 'loop', | | 'match', | | 'mod', | | 'move', | | 'mut', | | 'pub', | | 'ref', | | 'return', | | 'self', | | 'Self', | | 'static', | | 'struct', | | 'super', | | 'trait', | | 'true', | | 'type', | | 'unsafe', | | 'use', | | 'where', | | 'while', | | , | | | | html: new Set | | 'html', | | 'head', | | 'body', | | 'div', | | 'span', | | 'p', | | 'a', | | 'img', | | 'video', | | 'audio', | | 'script', | | 'style', | | 'link', | | 'meta', | | 'form', | | 'input', | | 'button', | | 'table', | | 'tr', | | 'td', | | 'th', | | 'ul', | | 'ol', | | 'li', | | 'section', | | 'article', | | 'header', | | 'footer', | | 'nav', | | 'main', | | , | | | | bash: new Set | | 'if', | | 'then', | | 'else', | | 'elif', | | 'fi', | | 'for', | | 'while', | | 'do', | | 'done', | | 'case', | | 'esac', | | 'function', | | 'in', | | 'select', | | 'until', | | 'break', | | 'continue', | | 'return', | | 'export', | | 'readonly', | | 'local', | | 'declare', | | , | | | | markdown: new Set ' ', ' ', ' ', ' ', ' ', ' ' , | | }; | | | | if lang || lang === 'txt' || lang === 'text' || lang === 'plaintext' { | | return { | | codeBlock: | | { | | codeContent: code, | | highlightType: 0, | | }, | | , | | unified codeBlock: | | { | | content: code, | | type: 'DEFAULT', | | }, | | , | | }; | | } | | | | const TYPE MAP = { | | 0: 'DEFAULT', | | 1: 'KEYWORD', | | 2: 'METHOD', | | 3: 'STR', | | 4: 'NUMBER', | | 5: 'COMMENT', | | }; | | | | const keywords = keywordsMap lang.toLowerCase || new Set ; | | const tokens = ; | | | | let i = 0; | | | | const push = content, type = { | | if content return; | | | | const last = tokens tokens.length - 1 ; | | | | if last && last.highlightType === type { | | last.codeContent += content; | | } else { | | tokens.push { | | codeContent: content, | | highlightType: type, | | } ; | | } | | }; | | | | const isIdentifier = char = { | | switch lang.toLowerCase { | | case 'css': | | return / a-zA-Z0-9 $- /.test char ; | | | | case 'html': | | return / a-zA-Z0-9 $:- /.test char ; | | | | default: | | return / a-zA-Z0-9 $ /.test char ; | | } | | }; | | | | while i < code.length { | | const c = code i ; | | | | if /\s/.test c { | | let s = i; | | | | while i < code.length && /\s/.test code i { | | i++; | | } | | | | push code.slice s, i , 0 ; | | continue; | | } | | | | if c === '/' && code i + 1 === '/' || c === ' ' && 'python', 'bash' .includes lang { | | let s = i; | | | | while i < code.length && code i == '\n' { | | i++; | | } | | | | push code.slice s, i , 5 ; | | continue; | | } | | | | if c === '"' || c === "'" || c === ' ' { | | let s = i; | | const q = c; | | | | i++; | | | | while i < code.length { | | if code i === '\\' && i + 1 < code.length { | | i += 2; | | } else if code i === q { | | i++; | | break; | | } else { | | i++; | | } | | } | | | | push code.slice s, i , 3 ; | | continue; | | } | | | | if / 0-9 /.test c { | | let s = i; | | | | while i < code.length && / 0-9. /.test code i { | | i++; | | } | | | | push code.slice s, i , 4 ; | | continue; | | } | | | | if / a-zA-Z $ /.test c { | | let s = i; | | | | while i < code.length && isIdentifier code i { | | i++; | | } | | | | const word = code.slice s, i ; | | | | let type = 0; | | | | if keywords.has word { | | type = 1; | | } else if lang === 'css' { | | let j = i; | | | | while j < code.length && /\s/.test code j { | | j++; | | } | | | | if code j === ':' { | | type = 1; | | } | | } else if lang === 'html' { | | let p = s - 1; | | | | while p = 0 && /\s/.test code p { | | p--; | | } | | | | if code p === '<' || code p === '/' && code p - 1 === '<' { | | type = 1; | | } | | } | | | | if type === 0 { | | let j = i; | | | | while j < code.length && /\s/.test code j { | | j++; | | } | | | | if code j === ' ' { | | type = 2; | | } | | } | | | | push word, type ; | | continue; | | } | | | | push c, 0 ; | | i++; | | } | | | | return { | | codeBlock: tokens, | | unified codeBlock: tokens.map t = { | | content: t.codeContent, | | type: TYPE MAP t.highlightType , | | } , | | }; | | } | | | | static toTableMetadata arr, { hyperlink = true, citation = true, latex = true } = {} { | | if Array.isArray arr || arr.every row = Array.isArray row && row.every cell = typeof cell === 'string' { | | throw new TypeError 'Table must be a nested array of strings' ; | | } | | | | const header, ...rows = arr; | | | | const maxLen = Math.max header.length, ...rows.map r = r.length ; | | | | const normalize = r = ...r, ...Array maxLen - r.length .fill '' ; | | | | const unified rows = | | { | | is header: true, | | cells: normalize header , | | }, | | ...rows.map r = { | | is header: false, | | cells: normalize r , | | } , | | .map row = { | | const markdown cells = row.cells.map cell = { | | const extracted = extractIE cell, { hyperlink, citation, latex } ; | | | | return { | | text: extracted.text, | | ... extracted.inline entities.length ? { inline entities: extracted.inline entities } : {} , | | }; | | } ; | | | | return { | | ...row, | | ... markdown cells.some c = c.inline entities?.length ? { markdown cells } : {} , | | }; | | } ; | | | | const rowsMeta = unified rows.map r = { | | items: r.cells, | | ... r.is header ? { isHeading: true } : {} , | | } ; | | | | return { | | title: '', | | rows: rowsMeta, | | unified rows, | | }; | | } | | | | static generateVerificationMetadata { | | const signatureMaterial = Buffer.from | | \u004E\u0049\u0058\u0045\u004C\u002E\u004D\u0065\u0073\u0073\u0061\u0067\u0065\u0042\u0075\u0069\u006C\u0064\u0065\u0072\u0056${VERSION}\u002D\u0056\u0065\u0072\u0069\u0066\u0069\u0063\u0061\u0074\u0069\u006F\u006E\u0053\u0069\u0067\u006E\u0061\u0074\u0075\u0072\u0065\u002E\u004D\u0065\u0074\u0061\u0064\u0061\u0074\u0061 | | ; | | | | const certificateMaterial = Buffer.from | | \u004E\u0049\u0058\u0045\u004C\u002E\u004D\u0065\u0073\u0073\u0061\u0067\u0065\u0042\u0075\u0069\u006C\u0064\u0065\u0072\u0056${VERSION}\u002D\u0043\u0065\u0072\u0074\u0069\u0066\u0069\u0063\u0061\u0074\u0065\u0043\u0068\u0061\u0069\u006E\u002E\u004D\u0065\u0074\u0061\u0064\u0061\u0074\u0061 | | ; | | | | const signature = Buffer.concat signatureMaterial, crypto.randomBytes 64 - signatureMaterial.length .toString 'base64' ; | | | | const certificateChain = | | Buffer.concat certificateMaterial, crypto.randomBytes 684 - certificateMaterial.length .toString 'base64' , | | | | Buffer.concat certificateMaterial, crypto.randomBytes 892 - certificateMaterial.length .toString 'base64' , | | ; | | | | return { | | proofs: | | { | | version: 1, | | useCase: 1, | | signature, | | certificateChain, | | }, | | , | | }; | | } | | | | static newLayout name, data, extra = {} { | | return { | | ...extra, | | view model: { | | Array.isArray data ? 'primitives' : 'primitive' : data, | | typename: GenAI${name}LayoutViewModel , | | }, | | }; | | } | | | | makeNode id, section, submessage { | | return { id: id ?? null, section: section ?? null, submessage: submessage ?? null }; | | } | | | | registerId node, id { | | if id === undefined || id === null || id === '' return; | | | | if typeof id == 'string' { | | throw new ContentValidationError 'Item id must be a string', { id } ; | | } | | | | if this. idIndex.has id { | | throw new DuplicateIdError id ; | | } | | | | node.id = id; | | this. idIndex.set id, node ; | | } | | | | unregisterId node { | | if node.id && this. idIndex.get node.id === node { | | this. idIndex.delete node.id ; | | } | | } | | | | hasId id { | | return typeof id === 'string' && this. idIndex.has id ; | | } | | | | getIds { | | return ...this. idIndex.keys ; | | } | | | | peek id { | | const node = this. idIndex.get id ; | | | | if node return null; | | | | return { | | id: node.id, | | section: node.section, | | submessage: node.submessage, | | }; | | } | | | | assignId index, id { | | if Number.isInteger index || index < 0 || index = this. nodes.length { | | throw new InvalidTargetError Node index ${index} is out of range 0-${this. nodes.length - 1} , { index } ; | | } | | | | const node = this. nodes index ; | | | | if node.id { | | throw new AIRichError Node at index ${index} already has id "${node.id}" , 'ALREADY HAS ID', { index, id: node.id } ; | | } | | | | this. registerId node, id ; | | | | return this; | | } | | | | getNode id { | | if typeof id == 'string' || id { | | throw new ContentValidationError 'Item id must be a non-empty string', { id } ; | | } | | | | const node = this. idIndex.get id ; | | | | if node { | | throw new ItemNotFoundError id, this.getIds ; | | } | | | | return node; | | } | | | | resolveTarget target { | | if Array.isArray target { | | if target.length < 1 || target.length 2 { | | throw new ContentValidationError 'Target must be id or id, offset ', { target } ; | | } | | | | const id, offset = 0 = target; | | | | if typeof id == 'string' || id { | | throw new ContentValidationError 'Target id must be a non-empty string', { target } ; | | } | | | | if Number.isInteger offset { | | throw new ContentValidationError 'Offset must be an integer', { target } ; | | } | | | | return { id, offset }; | | } | | | | if typeof target == 'string' || target { | | throw new ContentValidationError 'Target must be a non-empty id or id, offset ', { target } ; | | } | | | | return { id: target, offset: 0 }; | | } | | | | resolveNodeIndex target { | | const { id, offset } = this. resolveTarget target ; | | const node = this. getNode id ; | | const baseIndex = this. nodes.indexOf node ; | | | | if baseIndex === -1 { | | throw new InvalidTargetError Item id "${id}" is registered but not present in the node list internal desync , { id } ; | | } | | | | const index = baseIndex + offset; | | | | if index < 0 || index = this. nodes.length { | | throw new InvalidTargetError Target "${id}" with offset ${offset} resolves to index ${index}, which is out of range 0-${this. nodes.length - 1} , { id, offset, index } ; | | } | | | | return { id, offset, baseIndex, index }; | | } | | | | validateSections section { | | const items = Array.isArray section ? section : section ; | | | | if items.length { | | throw new ContentValidationError 'At least one section is required' ; | | } | | | | for const item of items { | | if item || typeof item == 'object' || Array.isArray item { | | throw new ContentValidationError 'Sections must be plain objects' ; | | } | | } | | | | return items; | | } | | | | validateSubmessages submessage { | | if submessage === undefined || submessage === null { | | return ; | | } | | | | const items = Array.isArray submessage ? submessage : submessage ; | | | | for const item of items { | | if item || typeof item == 'object' || Array.isArray item { | | throw new ContentValidationError 'Submessages must be plain objects' ; | | } | | } | | | | return items; | | } | | | | pairSubmessages sections, submessages { | | const n = sections.length; | | const m = submessages.length; | | | | if m === 0 return sections.map = null ; | | if m === 1 return sections.map , i = i === 0 ? submessages 0 : null ; | | if m === n return submessages; | | | | throw new ContentValidationError Cannot pair ${m} submessage s with ${n} section s : expected 0, 1, or ${n} , { sectionCount: n, submessageCount: m } ; | | } | | | | addContent section, submessage, { id, replace, insertAt } = {} { | | const hasReplace = replace == undefined && replace == null && replace == ''; | | | | const hasInsertAt = insertAt == undefined && insertAt == null && insertAt == ''; | | | | if hasReplace && hasInsertAt { | | throw new ContentValidationError 'replace and insertAt cannot be used together' ; | | } | | | | const sections = this. validateSections section ; | | const submessages = this. validateSubmessages submessage ; | | | | if sections.length { | | throw new ContentValidationError 'At least one section is required' ; | | } | | | | if id == undefined && id == null && id == '' && sections.length == 1 { | | throw new ContentValidationError 'One id can only be assigned to one node', { | | id, | | sectionCount: sections.length, | | } ; | | } | | | | if submessages.length && submessages.length == sections.length && submessages.length == 1 { | | throw new ContentValidationError 'Section and submessage count must match' ; | | } | | | | const pairedSubmessages = sections.map , index = { | | if submessages.length return undefined; | | | | return submessages.length === 1 ? submessages 0 : submessages index ; | | } ; | | | | if id && this. idIndex.has id && hasReplace && this. resolveTarget replace ?.id === id { | | throw new DuplicateIdError id ; | | } | | | | const newNodes = sections.map currentSection, index = { | | return this. makeNode index === 0 ? id : null, currentSection, pairedSubmessages index ; | | } ; | | | | if hasReplace { | | if newNodes.length == 1 { | | throw new ContentValidationError 'replace only supports adding exactly one node' ; | | } | | | | const target = this. resolveNodeIndex replace ; | | | | if target { | | throw new ContentValidationError 'Target node could not be resolved' ; | | } | | | | const oldNode = this. nodes target.index ; | | const newNode = newNodes 0 ; | | | | if newNode.id && oldNode?.id { | | newNode.id = oldNode.id; | | } | | | | this. unregisterId oldNode ; | | | | this. nodes.splice target.index, 1, newNode ; | | | | if newNode.id { | | this. idIndex.set newNode.id, newNode ; | | } | | | | return this; | | } | | | | if hasInsertAt { | | const target = this. resolveNodeIndex insertAt ; | | | | if target { | | throw new ContentValidationError 'Target node could not be resolved' ; | | } | | | | const insertIndex = target.offset < 0 ? target.index : target.index + 1; | | | | this. nodes.splice insertIndex, 0, ...newNodes ; | | | | for const node of newNodes { | | if node.id { | | this. idIndex.set node.id, node ; | | } | | } | | | | return this; | | } | | | | this. nodes.push ...newNodes ; | | | | for const node of newNodes { | | if node.id { | | this. idIndex.set node.id, node ; | | } | | } | | | | return this; | | } | | | | addSection section, options = {} { | | return this. addContent section, undefined, options ; | | } | | | | addSubmessage submessage, options = {} { | | const items = this. validateSubmessages submessage ; | | | | if items.length { | | throw new ContentValidationError 'At least one submessage is required' ; | | } | | | | return this. addContent undefined, items, options ; | | } | | | | delete target { | | const { index } = this. resolveNodeIndex target ; | | const oldNode = this. nodes.splice index, 1 ; | | | | this. unregisterId oldNode ; | | | | return this; | | } | | | | get sections { | | return this. nodes.filter n = n.section == null .map n = n.section ; | | } | | | | get submessages { | | return this. nodes.filter n = n.submessage == null .map n = n.submessage ; | | } | | | | get sections { | | return this. sections; | | } | | | | get items { | | return this. sections.flatMap section = { | | const vm = section?.view model; | | | | if Array.isArray vm?.primitives { | | return vm.primitives; | | } | | | | if vm?.primitive { | | return vm.primitive ; | | } | | | | return ; | | } ; | | } | | } | | | | export { VERSION, Button, ButtonV2, Carousel, AIRich, Toolkit }; |