import type { BlipClient } from '../client.ts' import type { Identity } from '../types/index.ts' import type { MessageTemplate, WhatsAppTemplateLanguage, WhatsappFlow } from '../types/whatsapp.ts' import { uri } from '../utils/uri.ts' import { getMessageTemplateVariables } from '../utils/whatsapp.ts' import { type ConsumeOptions, Namespace, type SendCommandOptions } from './namespace.ts' export class WhatsAppNamespace extends Namespace { constructor(blipClient: BlipClient, defaultOptions?: SendCommandOptions) { super(blipClient, 'wa.gw', defaultOptions) } public async phoneToIdentity(phoneNumber: string, opts?: ConsumeOptions): Promise { const fixedPhoneNumber = phoneNumber[0] !== '+' ? `+${phoneNumber}` : phoneNumber try { const result = await this.sendCommand< 'get', { alternativeAccount: Identity } >( { method: 'get', uri: uri`/accounts/${fixedPhoneNumber}`, }, opts, ) return result.alternativeAccount } catch { return `${phoneNumber.replace('+', '')}@wa.gw.msging.net` } } /** * This is a heavily cached endpoint on Meta, so it may take a while to reflect changes * @param filters.status - Filter by status of the message template * @param filters.name - Filter by name of the message template, supports partial match */ public async getMessageTemplates( filters?: Partial<{ status: MessageTemplate['status'] name: string }>, opts?: ConsumeOptions, ): Promise> { return await this.sendCommand( { method: 'get', uri: uri`/message-templates?${{ status: filters?.status?.toUpperCase(), templateName: filters?.name }}`, }, { collection: true, ...opts, }, ) } public async getMessageTemplate( templateName: string, language?: WhatsAppTemplateLanguage, opts?: ConsumeOptions, ): Promise<(MessageTemplate & { variables: ReturnType }) | undefined> { let templates = await this.getMessageTemplates({ name: templateName }, opts) if (language) { templates = templates.filter((template) => template.language === language) } const template = templates.find((template) => template.name === templateName) if (!template) { return } return { ...template, variables: getMessageTemplateVariables(template.components), } } public async createMessageTemplateAttachment( url: string, opts?: ConsumeOptions, ): Promise<{ uploadSessionId: string fileHandle: string fileSize: string fileSizeUploaded: string }> { return await this.sendCommand( { method: 'set', uri: uri`/message-templates-attachment`, type: 'application/vnd.lime.media-link+json', resource: { uri: url, }, }, opts, ) } public async createMessageTemplate( template: Pick, opts?: ConsumeOptions, ): Promise { return await this.sendCommand( { method: 'set', uri: uri`/message-templates`, type: 'application/json', resource: template, }, opts, ) } public async createMessageTemplateAndPoll( template: Pick, opts?: ConsumeOptions, ): Promise { await this.sendCommand( { method: 'set', uri: uri`/message-templates`, type: 'application/json', resource: template, }, opts, ) let tries = 0 while (tries++ < 5) { // this timer bypass the caching of message templates on Meta await new Promise((resolve) => setTimeout(resolve, 1000 * 60)) const created = await this.getMessageTemplate(template.name, template.language, opts) if (!created) { throw new Error(`Template ${template.name} was not created`) } if (created.status !== 'PENDING') { if (created.status === 'APPROVED') { return created } throw new Error(`Template ${template.name} was rejected: ${created.rejected_reason}`) } } throw new Error(`Template ${template.name} was created but might or not be approved yet`) } public async deleteMessageTemplate(templateName: string, opts?: ConsumeOptions): Promise { return await this.sendCommand( { method: 'delete', uri: uri`/message-templates/${templateName}`, }, opts, ) } public async createFlow( flow: { name: string; category: string; endpointUri?: string }, opts?: ConsumeOptions, ): Promise<{ id: string }> { return await this.sendCommand( { method: 'set', uri: uri`/whatsapp-flows`, type: 'application/json', resource: { name: flow.name, categories: [flow.category], endpoint_uri: flow.endpointUri, }, }, opts, ) } public async updateFlow( id: string, flow: { name?: string; endpointUri?: string }, opts?: ConsumeOptions, ): Promise { return await this.sendCommand( { method: 'set', uri: uri`/whatsapp-flows/${id}`, type: 'application/json', resource: { name: flow.name, endpoint_uri: flow.endpointUri, }, }, opts, ) } public async updateFlowJson(id: string, json: string, opts?: ConsumeOptions): Promise { return await this.sendCommand( { method: 'set', uri: uri`/whatsapp-flows/flow-json/${id}`, type: 'application/json', resource: JSON.parse(json), }, opts, ) } public async updateFlowsPublicKey(publicKey: string, opts?: ConsumeOptions): Promise { return await this.sendCommand( { method: 'set', uri: uri`/whatsapp-flows/public-key/upload`, type: 'application/json', resource: { business_public_key: publicKey, }, }, opts, ) } public async getFlows(opts?: ConsumeOptions): Promise> { return await this.sendCommand( { method: 'get', uri: uri`/whatsapp-flows`, }, { collection: true, ...opts, }, ) } public async getFlow(id: string, opts?: ConsumeOptions): Promise { return await this.sendCommand( { method: 'get', uri: uri`/whatsapp-flows/${id}`, }, opts, ) } public async publishFlow(id: string, opts?: ConsumeOptions): Promise { return await this.sendCommand( { method: 'get', uri: uri`/whatsapp-flows/publish/${id}`, }, opts, ) } public async deprecateFlow(id: string, opts?: ConsumeOptions): Promise { return await this.sendCommand( { method: 'get', uri: uri`/whatsapp-flows/deprecate/${id}`, }, opts, ) } public async getFlowAssets( id: string, opts?: ConsumeOptions, ): Promise> { return await this.sendCommand( { method: 'get', uri: uri`/whatsapp-flows/assets/${id}`, }, { collection: true, ...opts, }, ) } public async deleteFlow(id: string, opts?: ConsumeOptions): Promise { return await this.sendCommand( { method: 'delete', uri: uri`/whatsapp-flows/${id}`, }, opts, ) } public async isChannelActive(opts?: ConsumeOptions): Promise { const configurations = await this.blipClient.account.getConfigurations({ ...opts, ownerIdentity: this.identity, }) return 'IsChannelActive' in configurations && configurations.IsChannelActive === 'True' } public async getWabaDetails(opts?: ConsumeOptions): Promise<{ id: string name: string reviewStatus: 'APPROVED' | 'PENDING' | 'REJECTED' currency: string messageTemplateNamespace: string }> { return await this.sendCommand( { method: 'get', uri: uri`/wabas/details`, }, opts, ) } public async getPhoneNumberDetails(opts?: ConsumeOptions): Promise<{ id: string account_mode: 'SANDBOX' | 'LIVE' name_status: 'APPROVED' | 'DECLINED' | 'EXPIRED' | 'PENDING_REVIEW' | 'NONE' new_name_status: 'APPROVED' | 'DECLINED' | 'EXPIRED' | 'PENDING_REVIEW' | 'NONE' verified_name: string display_phone_number: string quality_rating: 'GREEN' | 'YELLOW' | 'RED' | 'UNKNOWN' quality_score: { score: 'GREEN' | 'YELLOW' | 'RED' | 'UNKNOWN' } }> { return await this.sendCommand( { method: 'get', uri: uri`/phone-number-details`, }, opts, ) } public async getPhoneNumber(opts?: ConsumeOptions): Promise { const configurations = await this.blipClient.account.getConfigurations({ ...opts, ownerIdentity: this.identity, }) if ('PhoneNumber' in configurations && 'CountryCode' in configurations) { return `+${configurations.CountryCode}${configurations.PhoneNumber}` } throw new Error('Phone number not found') } }