import { type MetaAccountParams } from './accounts.ts' import { graphItems, graphRequest, requireConfirmOrDryRun, type JsonObject } from './core.ts' import { boundedLimit } from './validation.ts' export type WhatsAppSendParams = MetaAccountParams & { phoneNumberId: string to: string body?: string previewUrl?: boolean templateName?: string templateLanguage?: string templateComponents?: unknown[] dryRun?: boolean confirm?: boolean } /** List Businesses the token can access. Use these ids to find WhatsApp accounts — never hard-code one. */ export async function listBusinesses(params: MetaAccountParams & { limit?: number; after?: string } = {}) { const data = await graphRequest({ ...params, path: '/me/businesses', query: { fields: 'id,name,verification_status', limit: boundedLimit(params.limit, 25, 100), after: params.after, }, }) return graphItems(data) } /** List WhatsApp Business accounts owned by a Business. */ export async function listWhatsAppAccounts( params: MetaAccountParams & { businessId: string; limit?: number; after?: string }, ) { const businessId = params.businessId?.trim() if (!businessId) throw new Error('listWhatsAppAccounts requires businessId from listBusinesses.') const data = await graphRequest({ ...params, path: '/' + encodeURIComponent(businessId) + '/owned_whatsapp_business_accounts', query: { fields: 'id,name,currency,timezone_id', limit: boundedLimit(params.limit, 25, 100), after: params.after, }, }) return graphItems(data) } /** List Cloud API phone numbers on a WhatsApp Business account. */ export async function listPhoneNumbers( params: MetaAccountParams & { wabaId: string; limit?: number; after?: string }, ) { const wabaId = params.wabaId?.trim() if (!wabaId) throw new Error('listPhoneNumbers requires wabaId from listWhatsAppAccounts.') const data = await graphRequest({ ...params, path: '/' + encodeURIComponent(wabaId) + '/phone_numbers', query: { fields: 'id,display_phone_number,verified_name,quality_rating,code_verification_status', limit: boundedLimit(params.limit, 25, 100), after: params.after, }, }) return graphItems(data) } /** Read one WhatsApp Cloud phone number. */ export async function getPhoneNumber(params: MetaAccountParams & { phoneNumberId: string }) { const phoneNumberId = params.phoneNumberId?.trim() if (!phoneNumberId) throw new Error('getPhoneNumber requires phoneNumberId from listPhoneNumbers.') return graphRequest({ ...params, path: '/' + encodeURIComponent(phoneNumberId), query: { fields: 'id,display_phone_number,verified_name,quality_rating,code_verification_status', }, }) } /** List message templates on a WhatsApp Business account. */ export async function listTemplates( params: MetaAccountParams & { wabaId: string; limit?: number; after?: string }, ) { const wabaId = params.wabaId?.trim() if (!wabaId) throw new Error('listTemplates requires wabaId.') const data = await graphRequest({ ...params, path: '/' + encodeURIComponent(wabaId) + '/message_templates', query: { fields: 'id,name,language,status,category', limit: boundedLimit(params.limit, 25, 100), after: params.after, }, }) return graphItems(data) } function sendPayload(params: WhatsAppSendParams): JsonObject { const to = params.to.replace(/\s+/g, '') if (!to) throw new Error('sendMessage requires to (E.164 digits, no spaces).') if (params.templateName?.trim()) { return { messaging_product: 'whatsapp', to, type: 'template', template: { name: params.templateName.trim(), language: { code: params.templateLanguage?.trim() || 'en_US' }, components: params.templateComponents ?? [], }, } } const body = params.body?.trim() if (!body) throw new Error('sendMessage requires body for a text message, or templateName for a template.') return { messaging_product: 'whatsapp', to, type: 'text', text: { body, preview_url: params.previewUrl === true }, } } /** * Preview or send a WhatsApp Cloud message. * Live send requires confirm: true. This helper never sends without that confirmation. */ export async function sendMessage(params: WhatsAppSendParams) { const phoneNumberId = params.phoneNumberId?.trim() if (!phoneNumberId) { throw new Error('sendMessage requires phoneNumberId from listPhoneNumbers. Do not invent a phone number id.') } const body = sendPayload(params) const path = '/' + encodeURIComponent(phoneNumberId) + '/messages' const mode = requireConfirmOrDryRun({ dryRun: params.dryRun, confirm: params.confirm, action: 'sendMessage', }) if (mode.dryRun) { return { dryRun: true as const, method: 'POST', path, body } } const result = await graphRequest({ ...params, method: 'POST', path, body, }) return { sent: true as const, to: body.to, result } } /** * WhatsApp Cloud API helpers. * @example * import { listBusinesses, sendMessage } from 'kody:@kody/meta/whatsapp' * const businesses = await listBusinesses() * const preview = await sendMessage({ phoneNumberId, to, body: 'Hi', dryRun: true }) */ export default function whatsapp() { return { listBusinesses, listWhatsAppAccounts, listPhoneNumbers, getPhoneNumber, listTemplates, sendMessage, } }