| /** | |
- 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. | |
- | |
- Created by Nixel | |
- wa.me/6282139672290 | |
- | |
- 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. |
|
*/ |
|
|
|
const VERSION = '4.3'; |
|
|
|
import { generateWAMessageFromContent, prepareWAMessageMedia } from 'baileys'; |
|
import crypto from 'crypto'; |
|
import sharp from 'sharp'; |
|
|
|
function extractHyperlink(text) { |
|
let hyperlink = [], |
|
stack = [], |
|
result = '', |
|
last = 0, |
|
index = 1, |
|
entity = 0; |
|
for (let i = 0; i < text.length; i++) { |
|
if (text[i] == '[' && text[i - 1] != '\') { |
|
stack.push(i); |
|
} else if (text[i] == ']' && text[i + 1] == '(') { |
|
let start = stack.pop(); |
|
if (start == null) continue; |
|
let end = i + 2, |
|
depth = 1; |
|
while (end < text.length && depth) { |
|
if (text[end] == '(' && text[end - 1] != '\') depth++; |
|
else if (text[end] == ')' && text[end - 1] != '\') depth--; |
|
end++; |
|
} |
|
if (depth) continue; |
|
let txt = text.slice(start + 1, i).trim(), |
|
url = text.slice(i + 2, end - 1), |
|
reference_id = txt ? 0 : index++, |
|
key =
IE_${entity++}, | | tag ={{${key}}}${txt || 'Nixel'}{{/${key}}}; | | result += text.slice(last, start) + tag; | | last = end; | | hyperlink.push({ | | reference_id, | | key, | | text: txt, | | url, | | }); | | i = end - 1; | | } | | } | | result += text.slice(last); | | return { | | text: result, | | hyperlink, | | }; | | } | | | | async function fetchBuffer(url, options = {}, config = {}) { | | try { | | let response = await fetch(url, options); | | if (!response.ok) throw Error(HTTP ${response.status}); | | return Buffer.from(await response.arrayBuffer()); | | } catch (error) { | | if (config.silent) return Buffer.alloc(0); | | throw error; | | } | | } | | | | class BaseBuilder { | | constructor() { | | this._title = ''; | | this._subtitle = ''; | | this._body = ''; | | this._footer = ''; | | this._contextInfo = {}; | | this._extraPayload = {}; | | } | | | | setTitle(title) { | | this._title = title; | | return this; | | } | | | | setSubtitle(subtitle) { | | this._subtitle = subtitle; | | return this; | | } | | | | setBody(body) { | | this._body = body; | | return this; | | } | | | | setFooter(footer) { | | 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 = {}; | | } | | | | setVideo(path, options = {}) { | | if (!path) throw new Error('Url or buffer needed'); | | Buffer.isBuffer(path) ? (this._data = { video: path, ...options }) : (this._data = { video: { url: path }, ...options }); | | 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 = crypto.randomUUID()) { | | 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; | | } | | | | makeSections(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) { | | this._buttons.push({ name: 'single_select', buttonParamsJson: JSON.stringify({ title, sections: [] }) }); | | this._currentSelectionIndex = this._buttons.length - 1; | | this._currentSectionIndex = -1; | | return this; | | } | | | | addReply(display_text = '', id = crypto.randomUUID()) { | | this._buttons.push({ name: 'quick_reply', buttonParamsJson: JSON.stringify({ display_text, id }) }); | | return this; | | } | | | | addCall(display_text = '', id = crypto.randomUUID()) { | | this._buttons.push({ | | name: 'cta_call', | | buttonParamsJson: JSON.stringify({ | | display_text, | | id, | | }), | | }); | | return this; | | } | | | | addReminder(display_text = '', id = crypto.randomUUID()) { | | this._buttons.push({ | | name: 'cta_reminder', | | buttonParamsJson: JSON.stringify({ | | display_text, | | id, | | }), | | }); | | return this; | | } | | | | addCancelReminder(display_text = '', id = crypto.randomUUID()) { | | this._buttons.push({ | | name: 'cta_cancel_reminder', | | buttonParamsJson: JSON.stringify({ | | display_text, | | id, | | }), | | }); | | return this; | | } | | | | addAddress(display_text = '', id = crypto.randomUUID()) { | | this._buttons.push({ | | name: 'address_message', | | buttonParamsJson: JSON.stringify({ | | display_text, | | id, | | }), | | }); | | return this; | | } | | | | addLocation() { | | this._buttons.push({ | | name: 'send_location', | | buttonParamsJson: '', | | }); | | return this; | | } | | | | addUrl(display_text = '', url = '', webview_interaction = false) { | | this._buttons.push({ | | name: 'cta_url', | | buttonParamsJson: JSON.stringify({ | | display_text, | | url, | | webview_interaction, | | }), | | }); | | return this; | | } | | | | addCopy(display_text = '', copy_code = '', id = crypto.randomUUID()) { | | this._buttons.push({ | | name: 'cta_copy', | | buttonParamsJson: JSON.stringify({ | | display_text, | | copy_code, | | id, | | }), | | }); | | 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 }) : {}), | | }, | | nativeFlowMessage: { | | messageParamsJson: JSON.stringify(this._params), | | buttons: this._buttons, | | }, | | }; | | } | | | | async build(jid, { ...options } = {}) { | | const message = await this.toCard(); | | | | return generateWAMessageFromContent( | | jid, | | { | | ...this._extraPayload, | | interactiveMessage: { | | ...message, | | contextInfo: this._contextInfo, | | }, | | }, | | { ...options } | | ); | | } | | | | async send(jid, { ...options } = {}) { | | const msg = await this.build(jid, 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' } }], | | }, | | ], | | }, | | ], | | ...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 = []; | | } | | | | 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; | | } | | | | 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, { ...options } = {}) { | | let _thumbnail = this._image | | ? await sharp(Buffer.isBuffer(this._image) ? this._image : await fetchBuffer(this._image, {}, { silent: true })) | | .resize(300, 300) | | .png() | | .toBuffer() | | : null; | | const msg = generateWAMessageFromContent( | | jid, | | { | | ...this._extraPayload, | | buttonsMessage: { | | contentText: this._body, | | footerText: this._footer, | | ...(this._data | | ? this._data | | : { | | headerType: 6, | | locationMessage: { | | jpegThumbnail: _thumbnail, | | }, | | }), | | viewOnce: true, | | contextInfo: this._contextInfo, | | buttons: [...this._buttons], | | }, | | }, | | { ...options } | | ); | | return msg; | | } | | | | async send(jid, { ...options } = {}) { | | const msg = await this.build(jid, 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' } }], | | }, | | ], | | }, | | ], | | ...options, | | }); | | return msg; | | } | | } | | | | class Carousel extends BaseBuilder { | | #client; | | constructor(client) { | | super(); | | if (!client) { | | throw new Error('Socket is required'); | | } | | | | this.#client = client; | | this._cards = []; | | } | | | | addCard(card) { | | if (Array.isArray(card)) { | | this._cards.push(...card); | | } else { | | this._cards.push(card); | | } | | | | return this; | | } | | | | build(jid, { ...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, | | }, | | }, | | }, | | { ...options } | | ); | | } | | | | async send(jid, { ...options } = {}) { | | const msg = this.build(jid, 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' } }], | | }, | | ], | | }, | | ], | | ...options, | | }); | | return msg; | | } | | } | | | | class AIRich { | | #client; | | constructor(client) { | | if (!client) { | | throw new Error('Socket is required'); | | } | | | | this.#client = client; | | this._submessages = []; | | this._sections = []; | | this._richResponseSources = []; | | } | | | | addText(text, { hyperlink = true } = {}) { | | const extractedHyperlink = hyperlink | | ? extractHyperlink(text) | | : { | | text, | | hyperlink: [], | | }; | | | | this._submessages.push({ | | messageType: 2, | | messageText: hyperlink ? extractedHyperlink.text : text, | | }); | | | | if (extractedHyperlink.hyperlink.length && hyperlink) { | | this._sections.push({ | | view_model: { | | primitive: { | | text: extractedHyperlink.text, | | inline_entities: extractedHyperlink.hyperlink.map(({ reference_id, key, text, url }) => ({ | | key, | | metadata: text?.trim() | | ? { | | display_name: text, | | is_trusted: true, | | url, | | __typename: 'GenAIInlineLinkItem', | | } | | : { | | reference_id, | | reference_url: url, | | reference_title: url, | | reference_display_name: url, | | sources: [], | | __typename: 'GenAISearchCitationItem', | | }, | | })), | | __typename: 'GenAIMarkdownTextUXPrimitive', | | }, | | __typename: 'GenAISingleLayoutViewModel', | | }, | | }); | | } else { | | this._sections.push({ | | view_model: { | | primitive: { | | text, | | __typename: 'GenAIMarkdownTextUXPrimitive', | | }, | | | | __typename: 'GenAISingleLayoutViewModel', | | }, | | }); | | } | | | | return this; | | } | | | | addCode(language, code) { | | const meta = AIRich.tokenizer(code, language); | | | | this._submessages.push({ | | messageType: 5, | | codeMetadata: { | | codeLanguage: language, | | codeBlocks: meta.codeBlock, | | }, | | }); | | | | this._sections.push({ | | view_model: { | | primitive: { | | language, | | code_blocks: meta.unified_codeBlock, | | __typename: 'GenAICodeUXPrimitive', | | }, | | __typename: 'GenAISingleLayoutViewModel', | | }, | | }); | | | | return this; | | } | | | | addTable(table) { | | const meta = AIRich.toTableMetadata(table); | | | | this._submessages.push({ | | messageType: 4, | | tableMetadata: { | | title: meta.title, | | rows: meta.rows, | | }, | | }); | | | | this._sections.push({ | | view_model: { | | primitive: { | | rows: meta.unified_rows, | | __typename: 'GenATableUXPrimitive', | | }, | | __typename: 'GenAISingleLayoutViewModel', | | }, | | }); | | | | return this; | | } | | | | addSource(sources = []) { | | const source = sources.map(([profile_url, url, text]) => ({ | | source_type: 'THIRD_PARTY', | | source_display_name: text, | | source_subtitle: 'AI', | | source_url: url, | | favicon: { | | url: profile_url, | | mime_type: 'image/jpeg', | | width: 16, | | height: 16, | | }, | | })); | | | | this._sections.push({ | | view_model: { | | primitive: { | | sources: source, | | __typename: 'GenAISearchResultPrimitive', | | }, | | __typename: 'GenAISingleLayoutViewModel', | | }, | | }); | | | | return this; | | } | | | | addReels(reelsItems = []) { | | this._submessages.push({ | | messageType: 9, | | contentItemsMetadata: { | | contentType: 1, | | itemsMetadata: reelsItems.map((item) => ({ | | reelItem: { | | title: item.title, | | profileIconUrl: item.profileIconUrl, | | thumbnailUrl: item.thumbnailUrl, | | videoUrl: item.videoUrl, | | }, | | })), | | }, | | }); | | | | reelsItems.forEach((item, idx) => { | | this._richResponseSources.push({ | | provider: 'UNKNOWN', | | thumbnailCDNURL: item.thumbnailUrl, | | sourceProviderURL: item.videoUrl, | | sourceQuery: '', | | faviconCDNURL: item.profileIconUrl, | | citationNumber: idx + 1, | | sourceTitle: item.title, | | }); | | }); | | | | this._sections.push({ | | view_model: { | | primitives: reelsItems.map((item) => ({ | | reels_url: item.videoUrl, | | thumbnail_url: item.thumbnailUrl, | | creator: item.title, | | avatar_url: item.profileIconUrl, | | reels_title: item.reels_title, | | likes_count: 0, | | shares_count: 0, | | view_count: 0, | | reel_source: 'IG', | | is_verified: item.is_verified, | | __typename: 'GenAIReelPrimitive', | | })), | | __typename: 'GenAIHScrollLayoutViewModel', | | }, | | }); | | | | return this; | | } | | | | addImage(imageUrl) { | | const imageUrls = Array.isArray(imageUrl) | | ? imageUrl.map((url) => ({ | | imagePreviewUrl: url, | | imageHighResUrl: url, | | sourceUrl: 'https://google.com', | | })) | | : [ | | { | | imagePreviewUrl: imageUrl, | | imageHighResUrl: imageUrl, | | sourceUrl: 'https://google.com', | | }, | | ]; | | | | this._submessages.push({ | | messageType: 1, | | gridImageMetadata: { | | gridImageUrl: { | | imagePreviewUrl: Array.isArray(imageUrl) ? imageUrl[0] : imageUrl, | | }, | | imageUrls, | | }, | | }); | | | | imageUrls.forEach(({ imagePreviewUrl }) => { | | this._sections.push({ | | view_model: { | | primitive: { | | media: { | | url: imagePreviewUrl, | | mime_type: 'image/jpeg', | | }, | | imagine_type: 3, | | status: { | | status: 'READY', | | }, | | __typename: 'GenAIImaginePrimitive', | | }, | | __typename: 'GenAISingleLayoutViewModel', | | }, | | }); | | }); | | | | return this; | | } | | | | build({ forwarded = true, includesUnifiedResponse = true, ...options } = {}) { | | const contextInfo = forwarded | | ? { | | forwardingScore: 1, | | isForwarded: true, | | forwardedAiBotMessageInfo: { botJid: '0@bot' }, | | forwardOrigin: 4, | | } | | : {}; | | | | return { | | messageContextInfo: { | | deviceListMetadata: {}, | | deviceListMetadataVersion: 2, | | botMetadata: { | | pluginMetadata: {}, | | richResponseSourcesMetadata: { sources: this._richResponseSources }, | | }, | | }, | | botForwardedMessage: { | | message: { | | richResponseMessage: { | | messageType: 1, | | submessages: this.submessages, | | unifiedResponse: { | | data: includesUnifiedResponse ? Buffer.from(JSON.stringify({ response_id: crypto.randomUUID(), sections: this.sections })).toString('base64') : '', | | }, | | contextInfo, | | }, | | }, | | }, | | }; | | } | | | | async send(jid, { forwarded, includesUnifiedResponse, ...options } = {}) { | | const msg = this.build({ forwarded, includesUnifiedResponse, ...options }); | | | | return await this.#client.relayMessage(jid, msg, { ...options }); | | } | | | | 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', | | ]), | | }; | | | | const TYPE_MAP = { | | 0: 'DEFAULT', | | 1: 'KEYWORD', | | 2: 'METHOD', | | 3: 'STR', | | 4: 'NUMBER', | | 5: 'COMMENT', | | }; | | | | const keywords = keywordsMap[lang] || 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 }); | | }; | | | | 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] === '/') { | | let s = i; | | i += 2; | | 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 && /[a-zA-Z0-9$]/.test(code[i])) i++; | | const word = code.slice(s, i); | | | | let type = 0; | | if (keywords.has(word)) type = 1; | | else { | | 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) { | | if (!Array.isArray(arr) || arr.length < 2) throw new Error('Format tabel ngawur'); | | | | 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), | | })), | | ]; | | | | const rowsMeta = unified_rows.map((r) => ({ | | items: r.cells, | | ...(r.is_header ? { isHeading: true } : {}), | | })); | | | | return { | | title: '', | | rows: rowsMeta, | | unified_rows, | | }; | | } | | } | | | | export { VERSION, Button, ButtonV2, Carousel, AIRich }; |
source & further reading
gist.github.com — original article
things I am working on
install.md — Weekly LinkedIn analytics agent installer
Anthropic.txt