UNPKG

@codebucket/whatsapp

Version:

A reusable WhatsApp Business API client with template and non-template support, webhook handling, pluggable storage, and text sanitization

205 lines (204 loc) 9.43 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.extractKeys = extractKeys; exports.normalizeVariables = normalizeVariables; exports.buildComponents = buildComponents; exports.createWhatsAppTemplatePayload = createWhatsAppTemplatePayload; const axios_1 = __importDefault(require("axios")); const types_1 = require("./types"); const utils_1 = require("./utils"); const templateCache = new Map(); async function fetchTemplate(businessAccountId, accessToken, templateName, language = 'en') { const key = `${businessAccountId}::${templateName}::${language}`; if (templateCache.has(key)) return templateCache.get(key); const resp = await axios_1.default.get(`https://graph.facebook.com/v22.0/${businessAccountId}/message_templates`, { params: { status: 'APPROVED', name: templateName, language }, headers: { Authorization: `Bearer ${accessToken}`, } }); const data = resp.data.data; if (!data.length) throw new Error(`Template '${templateName}' not found.`); const tpl = data[0]; const def = { name: tpl.name, language: tpl.language, components: tpl.components }; templateCache.set(key, def); return def; } function detectMode(keys) { if (!keys.length) return types_1.Mode.NUMERIC; return keys.every(k => /^\d+$/.test(k)) ? types_1.Mode.NUMERIC : types_1.Mode.NAMED; } function isStructured(vars) { return (!Array.isArray(vars) && (vars.header !== undefined || vars.body !== undefined || vars.buttons !== undefined)); } function extractKeys(components) { const bodyText = components.find(c => c.type === 'BODY' && c.text)?.text || ''; const regex = /\{\{(\w+)\}\}/g; const keys = []; let m; while ((m = regex.exec(bodyText)) !== null) { if (!keys.includes(m[1])) keys.push(m[1]); } return keys; } function normalizeVariables(vars, mode, keys) { const warnings = []; // 1) FLAT ARRAY INPUT if (Array.isArray(vars)) { // find how many body placeholders we expect const bodyCount = keys.length; // detect if first item is a media header (we'd need access to components for that // but simplest heuristic: if mode===NUMERIC and vars.length > bodyCount+? then assume header) const hasHeader = mode === types_1.Mode.NUMERIC && vars.length > bodyCount + // buttons? bodyCount; const headerSliceLen = hasHeader ? 1 : 0; // slice out the body segment const bodyRaw = vars.slice(headerSliceLen, headerSliceLen + bodyCount); const bodyClean = bodyRaw.map((raw, i) => { const vs = (0, utils_1.validateTemplateText)(raw); vs.forEach(w => warnings.push(`Placeholder #${i + 1}: ${w}`)); return (0, utils_1.cleanTemplateText)(raw); }); // rebuild the flat array: [ …headerRaw, …bodyClean, …buttonsRaw ] return { variables: [ ...vars.slice(0, headerSliceLen), // raw header ...bodyClean, // cleaned body ...vars.slice(headerSliceLen + bodyCount) // raw buttons ], warnings }; } // 2) STRUCTURED INPUT { header?, body?, buttons? } // – header is untouched // – buttons are untouched // – only clean/validate body[key] const struct = vars; const cleanBody = {}; keys.forEach(key => { const raw = struct.body?.[key] ?? ''; const vs = (0, utils_1.validateTemplateText)(raw); vs.forEach(w => warnings.push(`Variable "${key}": ${w}`)); cleanBody[key] = (0, utils_1.cleanTemplateText)(raw); }); return { variables: { header: struct.header, // raw, no cleaning body: cleanBody, // cleaned named placeholders buttons: struct.buttons // raw, no cleaning }, warnings }; } function buildComponents(components, variables, mode, keys) { // ── 1) Figure out your header value ─────────────────────────────────────────── const headerVal = isStructured(variables) && variables.header !== undefined ? variables.header : Array.isArray(variables) ? variables[0] : undefined; // ── 2) Positional flat array (for numeric & fallback) ──────────────────────── const positionalArr = Array.isArray(variables) ? variables : mode === types_1.Mode.NUMERIC ? Object.keys(variables) .filter(k => /^\d+$/.test(k)) .sort((a, b) => +a - +b) .map(k => variables[k] ?? '') : []; // ── 3) Body values ─────────────────────────────────────────────────────────── const bodyVals = isStructured(variables) && variables.body ? // structured named-body keys.map(k => variables.body[k] ?? '') : mode === types_1.Mode.NAMED ? // raw record→array keys.map(k => variables[k] ?? '') : // positional slice after header positionalArr.slice(headerVal != null ? 1 : 0, (headerVal != null ? 1 : 0) + keys.length); // ── 4) Button values ───────────────────────────────────────────────────────── const buttonVals = isStructured(variables) && Array.isArray(variables.buttons) ? variables.buttons : positionalArr.slice((headerVal != null ? 1 : 0) + bodyVals.length); // ── 5) Build each component ───────────────────────────────────────────────── return components.map(comp => { switch (comp.type) { case 'HEADER': // — Media header? — if (comp.format && comp.format !== 'TEXT' && headerVal) { const mtype = comp.format.toLowerCase(); const param = { type: mtype }; if (/^https?:\/\//.test(headerVal)) { param[mtype] = { link: headerVal }; } else { param[mtype] = { id: headerVal }; } return { type: 'header', parameters: [param] }; } // — Text header placeholders — if (comp.text?.includes('{{')) { if (mode === types_1.Mode.NAMED) { return { type: 'header', parameters: keys.map(name => ({ type: 'text', parameter_name: name, text: variables.body?.[name] ?? '' })) }; } return { type: 'header', parameters: (Array.isArray(variables) ? positionalArr : bodyVals) .map(t => ({ type: 'text', text: t })) }; } // — Static header — return { type: 'header' }; case 'BODY': if (mode === types_1.Mode.NAMED) { return { type: 'body', parameters: keys.map(name => ({ type: 'text', parameter_name: name, text: variables.body?.[name] ?? '' })) }; } return { type: 'body', parameters: bodyVals.map(t => ({ type: 'text', text: t })) }; case 'BUTTON': const idx = comp.index ?? 0; const text = buttonVals[idx] ?? ''; return { type: 'button', sub_type: comp.sub_type?.toLowerCase(), index: idx, parameters: [{ type: 'text', text }] }; default: return { type: comp.type.toLowerCase() }; } }); } /** * Creates a template payload and returns payload plus validation warnings. */ async function createWhatsAppTemplatePayload(opts) { const tpl = await fetchTemplate(opts.businessAccountId, opts.accessToken, opts.templateName, opts.language); const keys = extractKeys(tpl.components); const mode = detectMode(keys); const { variables, warnings } = normalizeVariables(opts.variables, mode, keys); const comps = buildComponents(tpl.components, variables, mode, keys); const payload = { recipient_type: 'individual', to: opts.to, type: 'template', template: { name: tpl.name, language: { code: tpl.language }, components: comps } }; return { payload, warnings }; }