188 lines
4.4 KiB
TypeScript
188 lines
4.4 KiB
TypeScript
|
|
/**
|
||
|
|
* Meta WhatsApp Business Cloud API Provider
|
||
|
|
*
|
||
|
|
* Direct integration with Meta's Graph API for WhatsApp Business.
|
||
|
|
* Docs: https://developers.facebook.com/docs/whatsapp/cloud-api
|
||
|
|
*/
|
||
|
|
|
||
|
|
import type { WhatsAppProvider, WhatsAppResult } from './index'
|
||
|
|
|
||
|
|
const GRAPH_API_VERSION = 'v18.0'
|
||
|
|
const BASE_URL = `https://graph.facebook.com/${GRAPH_API_VERSION}`
|
||
|
|
|
||
|
|
export class MetaWhatsAppProvider implements WhatsAppProvider {
|
||
|
|
constructor(
|
||
|
|
private phoneNumberId: string,
|
||
|
|
private accessToken: string
|
||
|
|
) {}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Send a text message
|
||
|
|
*/
|
||
|
|
async sendText(to: string, body: string): Promise<WhatsAppResult> {
|
||
|
|
try {
|
||
|
|
const response = await fetch(
|
||
|
|
`${BASE_URL}/${this.phoneNumberId}/messages`,
|
||
|
|
{
|
||
|
|
method: 'POST',
|
||
|
|
headers: {
|
||
|
|
Authorization: `Bearer ${this.accessToken}`,
|
||
|
|
'Content-Type': 'application/json',
|
||
|
|
},
|
||
|
|
body: JSON.stringify({
|
||
|
|
messaging_product: 'whatsapp',
|
||
|
|
recipient_type: 'individual',
|
||
|
|
to: formatPhoneNumber(to),
|
||
|
|
type: 'text',
|
||
|
|
text: { body },
|
||
|
|
}),
|
||
|
|
}
|
||
|
|
)
|
||
|
|
|
||
|
|
const data = await response.json()
|
||
|
|
|
||
|
|
if (!response.ok) {
|
||
|
|
return {
|
||
|
|
success: false,
|
||
|
|
error: data.error?.message || `API error: ${response.status}`,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
success: true,
|
||
|
|
messageId: data.messages?.[0]?.id,
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
return {
|
||
|
|
success: false,
|
||
|
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Send a template message
|
||
|
|
*
|
||
|
|
* Templates must be pre-approved by Meta.
|
||
|
|
* Params are passed as components to the template.
|
||
|
|
*/
|
||
|
|
async sendTemplate(
|
||
|
|
to: string,
|
||
|
|
template: string,
|
||
|
|
params: Record<string, string>
|
||
|
|
): Promise<WhatsAppResult> {
|
||
|
|
try {
|
||
|
|
// Build template components from params
|
||
|
|
const components = buildTemplateComponents(params)
|
||
|
|
|
||
|
|
const response = await fetch(
|
||
|
|
`${BASE_URL}/${this.phoneNumberId}/messages`,
|
||
|
|
{
|
||
|
|
method: 'POST',
|
||
|
|
headers: {
|
||
|
|
Authorization: `Bearer ${this.accessToken}`,
|
||
|
|
'Content-Type': 'application/json',
|
||
|
|
},
|
||
|
|
body: JSON.stringify({
|
||
|
|
messaging_product: 'whatsapp',
|
||
|
|
recipient_type: 'individual',
|
||
|
|
to: formatPhoneNumber(to),
|
||
|
|
type: 'template',
|
||
|
|
template: {
|
||
|
|
name: template,
|
||
|
|
language: { code: 'en' },
|
||
|
|
components,
|
||
|
|
},
|
||
|
|
}),
|
||
|
|
}
|
||
|
|
)
|
||
|
|
|
||
|
|
const data = await response.json()
|
||
|
|
|
||
|
|
if (!response.ok) {
|
||
|
|
return {
|
||
|
|
success: false,
|
||
|
|
error: data.error?.message || `API error: ${response.status}`,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
success: true,
|
||
|
|
messageId: data.messages?.[0]?.id,
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
return {
|
||
|
|
success: false,
|
||
|
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Test the connection to Meta API
|
||
|
|
*/
|
||
|
|
async testConnection(): Promise<{ success: boolean; error?: string }> {
|
||
|
|
try {
|
||
|
|
// Try to get phone number info to verify credentials
|
||
|
|
const response = await fetch(
|
||
|
|
`${BASE_URL}/${this.phoneNumberId}`,
|
||
|
|
{
|
||
|
|
headers: {
|
||
|
|
Authorization: `Bearer ${this.accessToken}`,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
)
|
||
|
|
|
||
|
|
if (!response.ok) {
|
||
|
|
const data = await response.json()
|
||
|
|
return {
|
||
|
|
success: false,
|
||
|
|
error: data.error?.message || `API error: ${response.status}`,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return { success: true }
|
||
|
|
} catch (error) {
|
||
|
|
return {
|
||
|
|
success: false,
|
||
|
|
error: error instanceof Error ? error.message : 'Connection failed',
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Format phone number for WhatsApp API
|
||
|
|
* Removes + prefix and any non-digit characters
|
||
|
|
*/
|
||
|
|
function formatPhoneNumber(phone: string): string {
|
||
|
|
return phone.replace(/[^\d]/g, '')
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Build template components from params
|
||
|
|
* Converts { param1: "value1", param2: "value2" } to WhatsApp component format
|
||
|
|
*/
|
||
|
|
function buildTemplateComponents(
|
||
|
|
params: Record<string, string>
|
||
|
|
): Array<{
|
||
|
|
type: 'body'
|
||
|
|
parameters: Array<{ type: 'text'; text: string }>
|
||
|
|
}> {
|
||
|
|
const paramValues = Object.values(params)
|
||
|
|
|
||
|
|
if (paramValues.length === 0) {
|
||
|
|
return []
|
||
|
|
}
|
||
|
|
|
||
|
|
return [
|
||
|
|
{
|
||
|
|
type: 'body',
|
||
|
|
parameters: paramValues.map((value) => ({
|
||
|
|
type: 'text',
|
||
|
|
text: value,
|
||
|
|
})),
|
||
|
|
},
|
||
|
|
]
|
||
|
|
}
|