| /** | |
- 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. | |
- | |
- Reup, 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 }; |
source & further reading
gist.github.com — original article
/cos: a chief of staff skill for T3 Code (Claude Code + Codex)
GPT_Handover_Prompt.md
JITI kernel, OpenAI generations, and recovery ASCII diagrams