Ir para o conteúdo principal

WhatsApp (Meta)

Visão Geral da Integração WhatsApp Business API

Descrição

O projeto OMN utiliza a API oficial do WhatsApp Business (Graph API v23.0) para comunicação, permitindo envio e recebimento de mensagens, gerenciamento de templates e processamento de webhooks.

1. Configuração e Inicialização

1.1 Constructor

Descrição: Inicializa a classe Whatsapp com cliente HTTP configurado para Graph API e injeta serviço S3 para gerenciamento de mídia.

Entrada:

constructor(@Inject(S3Service) private readonly s3Service: S3Service)

Implementação:

  constructor(@Inject(S3Service) private readonly s3Service: S3Service) {
    this.http = axios.create({
      baseURL: 'https://graph.facebook.com/v23.0/',
    })
  }

2. Operações de Mídia

2.1. Obtenção da midia

async getMediaLink(id: string, whatsappId: string, token: string): Promise<string> {
    const url = `${this.http.defaults.baseURL}${id}?phone_number_id=${whatsappId}`

Descrição: Obtém mídia do WhatsApp através do ID, faz download e upload para S3, retornando URL acessível.

Entrada:

{
  id: string,        // ID da mídia no WhatsApp
  whatsappId: string, // Phone Number ID
  token: string      // Access Token
}

Implementação:

// 1. GET /{id}?phone_number_id={whatsappId}
try{
  const response = await this.http.get(url, {
  headers: { Authorization: `Bearer ${token}` }
})

// 2. Download da mídia
const media = await this.http.get(metaUrl, {
  headers: { Authorization: `Bearer ${token}` },
  responseType: 'arraybuffer'
})

const mimetype = media.headers['content-type']
// 3. Upload para S3
return this.s3Service.upload(media.data, mimetype)
    } catch (error) {
      this.logger.log('Error:', error)
      addTags({
        'error.type': error.name,
        'error.msg': error.message,
        'error.stack': error.stack,
      })
      return url
    }

Retorno:

string // URL da mídia (S3 ou original)

2.2. Upload de Mídia

2.2.1 uploadMedia
async uploadMedia(mediaUri: string, whatsappId: string, token: string): Promise<{ id: string }>

Descrição: Faz upload de arquivo para WhatsApp, com conversão automática de áudio para formato compatível.

Entrada:

{
  mediaUri: string,   // URI do arquivo
  whatsappId: string, // Phone Number ID
  token: string       // Access Token
}

Implementação:

// 1. Extrai nome e extensão do arquivo da URI Obtem buffer e detecta tipo
const { fileName, extension } = Utils.getFileInfo(mediaUri)
let buffer = await Utils.getBuffer(mediaUri)
let mimeType = mime.lookup(extension)

const fileType = await fromBuffer(buffer) // detecta tipo real arquivo

// 2. Conversão de áudio se necessário
if (fileType?.mime.startsWith('audio')) {
  const { buffer: whatsappAudioBuffer, mimeType: whatsappAudioMimeType } =
    await AudioService.getInstance().convertAudioBufferToWhatsAppFormat(buffer)
  buffer = whatsappAudioBuffer
  mimeType = whatsappAudioMimeType
}

// 3. Upload via FormData
const form = new FormData()
form.append('file', blob, fileName)
form.append('messaging_product', 'whatsapp')

const response = await this.http.post(`${whatsappId}/media`, form, {
  headers: { Authorization: `Bearer ${token}` }
})

Retorno:

{
  id: string,   // ID da mídia no WhatsApp
}

3. Envio de Mensagens

3.1. Payloads

3.1.1. toWhatsappMessagePayload
toWhatsappMessagePayload(message: SendMessagePayload): SendMessageWhatsappPayload {

Descrição: Converte payload interno para formato da API do WhatsApp, processando texto, mídia e contexto.

Entrada:

{
  body?: string,           // Texto da mensagem
  to: string,             // Destinatário
  from:string,            // Rementente
  userName: string,
  type:string,
  mediaUri?: string,      // URI da mídia
  interactive?: object,   // Elementos interativos
  template?: object,      // Template
  quoted?:string,
  quotedMessage?: {       // Mensagem citada
    id: string,
    message?:string
  }
}

Implementação:

const { body, to, mediaUri, interactive, template, quotedMessage } = message

let payload: SendMessageWhatsappPayload = { 
  messaging_product: 'whatsapp', 
  to 
}

// Contexto para mensagem citada
if (quotedMessage?.id) {
  payload.context = { message_id: message.quotedMessage.id }
}

// Processamento da mensagem de texto
if (body) {
  payload = { ...payload, type: 'text', text: { body } }
}

// Proecssamento de Midia
if (mediaUri) {
      const { type, fileName } = Utils.getFileInfo(mediaUri)
      const supportedTypes = ['audio', 'document', 'image', 'sticker', 'video']
      if (!supportedTypes.includes(type)) {
        new BadRequestException('Media not supported')
      }
      payload = { ...payload, type, [type]: { link: mediaUri } }
      if (type === 'document') payload.document.filename = fileName
    }


// Processamento de template
  if (template) {
      payload = {
        ...payload,
        type: 'template',
        template: {
          name: 'hello_world',
          language: {
            code: 'en_US',
          },
        },
      }
    }
    return payload

Retorno:

{
  messaging_product: 'whatsapp',
  to: string,
  type?: string,
  text?: { body: string },
  context?: { message_id: string },
  audio?: { id?: string; link?: string },
  document?: { id?: string; link?: string; filename?: string },
  image?: { id?: string; link?: string },
  sticker?: { id?: string; link?: string },
  video?: { id?: string; link?: string }
}

3.2. Envio de Mensagem

3.2.1. sendMessage
async sendMessage(message: SendMessagePayload, whatsappId: string, token: string)

Descrição: Envia mensagem via WhatsApp Business API, com upload automático de mídia e fallback para link de texto.

Entrada:

{
  message: SendMessagePayload, // Payload da mensagem
  whatsappId: string,         // Phone Number ID
  token: string               // Access Token
}

Implementação:

// 1. Transformar payload
  const payload = this.toWhatsappMessagePayload(message)
  const { type } = payload
  const link = payload[type]?.link
  const textTypes = ['template', 'text', 'interactive']
  const isMedia = !textTypes.includes(type)

// 2. Processamento Midia chamando uploadMedia()
try{
if (isMedia && link) {
  let media = await this.uploadMedia(link, whatsappId, token)
  if (media) {
    payload[type] = media
  } else {
    // Fallback para link de texto
    delete payload[type]
    payload.type = 'text'
    payload.text = { 
      body: `Por favor, clique no link para realizar o download do arquivo: \n${link}` 
    }
  }
}

// 3. Enviar mensagem
const response = await this.http.post(`${whatsappId}/messages?access_token=${token}`, payload)

// 4. Delay para mídia
if (isMedia) await Utils.delay(parseInt(process.env.MEDIA_MESSAGE_DELAY_MS || '1000', 10))
return response
    } catch (error) {
      addTags({ whatsappPayload: JSON.stringify(payload) })
      this.logger.error('Sending whatsapp message error:', error)
      throw error
    }
  }

Retorno:

AxiosResponse // Response da API do WhatsApp

3.3. Envio de Template

3.3.1. sendTemplateMessage
async sendTemplateMessage(payload: StartConversationPayload, whatsappId: string, token: string, ): Promise<AxiosResponse> 

Descrição: Envia mensagem de template para iniciar conversação, com validação de variáveis e fallback para userName/companyName.

Entrada:

{
  to: string,                    // Destinatário
  template?: string,             // Nome do template
  templateVariables?: string[],  // Variáveis do template
  userName?: string,             // Nome do usuário (fallback)
  companyName?: string          // Nome da empresa (fallback)
}

Implementação:

// 1. Validação de variáveis
if (!payload?.templateVariables && !(payload.userName && payload.companyName))
  throw new BadRequestException("You must provide 'templateVariables' or both 'userName' and 'companyName'")

// 2. Preparar variáveis
if (!payload?.templateVariables) 
  payload.templateVariables = [payload.userName, payload.companyName]

// 3. Criar parâmetros
const parameters = []
payload.templateVariables.forEach((v) => parameters.push({ type: 'text', text: v }))

// 4. Montar payload do template
const templatePayload = {
  messaging_product: 'whatsapp',
  to: payload.to,
  type: 'template',
  template: {
    name: payload?.template || WhatsappDefaultTemplateNames.contato_cliente,
    language: { code: 'pt_BR' },
    components: [{
      type: 'body',
      parameters,
    }],
  },
}

// 5. Enviar
return this.http.post(`${whatsappId}/messages`, templatePayload, {
  headers: { Authorization: `Bearer ${token}` }
})

Retorno:

  AxiosResponse

4. Gerenciamento de Templates

4.1. Obtenção do template

4.1.1 getTemplates
public async getTemplates(wabaId: string, token: string)

Descrição: Lista todos os templates de mensagem aprovados na conta WhatsApp Business.

Entrada:

{
  wabaId: string, // WhatsApp Business Account ID
  token: string   // Access Token
}

Implementação:

try {
      const templates = (
        await this.http.get(`${wabaId}/message_templates`, {
          headers: {
            Authorization: `Bearer ${token}`,
          },
        })
      )?.data?.data
      return templates || []
    } catch (error) {
      this.logger.error('error in getTemplate', JSON.stringify(error), error?.response?.data)
      addTags({
        'error.type': error?.name,
        'error.msg': error?.message,
        'error.stack': error?.stack,
      })
      throw new Error('Cannot get templates in whatsapp')
    }
  }

Retorno:

Array<{
  name: string,
  components: Array<{
    type: string,
    format?: string,
    text?: string,
    example?: object
  }>,
  language: string,
  status: string,
  category: string,
  id: string
}>

4.2. Criação do template

4.2.1 createTemplate
async createTemplate(wabaId: string, token: string, template?: CreateWhatsappTempatePayload) 

Descrição: Cria novo template de mensagem no WhatsApp Business, com verificação de duplicatas e criação automática do template padrão.

Entrada:

{
  wabaId: string,                        // WABA ID
  token: string,                         // Access Token
  template?: CreateWhatsappTempatePayload // Template personalizado
}

Implementação:

// 1. Verificar template padrão se não fornecido
try {
if (!template) {
  const defaultTemplate = await this.getDefaultTemplate(wabaId, token)
  if (defaultTemplate.length !== 0) return
  template = this.generate360DefaultTemplate()
} else {
  // 2. Verificar duplicatas
  const userTemplates = await this.getTemplates(wabaId, token)
  if (userTemplates.find((t) => t.name === template.name))
    throw new ConflictException('Template name is already in use')
}

// 3. Criar template
await this.http.post(`${wabaId}/message_templates`, template, {
  headers: { Authorization: `Bearer ${token}` }
})
} catch (error) {
      this.logger.error('error in createTemplate', JSON.stringify(error), error?.response?.data)
      addTags({
        'error.type': error?.name,
        'error.msg': error?.message,
        'error.stack': error?.stack,
      })
      if (error instanceof ConflictException) throw error
      throw new CreateTemplateException('Whastapp Meta')
    }
  }

Retorno:

void // Sucesso ou lança exceção
  {
  "id": "1234567890123456",
  "status": "PENDING",
  "category": "UTILITY"
}

4.3. Template Padrão

4.3.1 generate360DefaultTemplate
private generate360DefaultTemplate(): CreateWhatsappTempatePayload

Descrição: Gera estrutura do template padrão "contato_cliente" com variáveis para nome do usuário e empresa.

Implementação:

return {
  name: WhatsappDefaultTemplateNames.contato_cliente,
  category: TemplateCategories.UTILITY,
  language: 'pt_BR',
  components: [{
    type: 'BODY',
    text: 'Olá {{1}} !\n\nFalo em nome da {{2}} e estou entrando em contato para continuarmos nossa conversa.\n\nAguardo o seu retorno.',
    example: {
      body_text: [['Bruno', 'Alterdata']],
    },
  }],
}

Retorno:

{
  name: string,
  category: 'UTILITY',
  language: 'pt_BR',
  components: Array<{
    type: 'BODY',
    text: string,
    example: {
      body_text: string[][]
    }
  }>
}

5. Processamento de WebHooks

5.1. Recebimento de Mensagens

5.1.1 whatsappPayloadToIncomingMessage

Descrição: Converte webhook do WhatsApp para formato interno, processando texto, mídia e mensagens citadas.

Entrada:

{
  payload: WhatsappMessageDto,  // Webhook do WhatsApp
  session: SessionDocument,     // Sessão ativa
  whatsapp: Whatsapp           // Instância do WhatsApp
}

Implementação:

// 1. Extrair informações da mensagem
const { message } = getMessagesInfo(payload)

// 2. Criar objeto base
const incomingMessage: IncomingMessageDto = {
  uid: message.id,
  sessionId: session._id,
  platformConfigId: session.platformConfigId,
  channelId: session.channelId,
  from: {
    uid: session.client.providerId,
    userName: session.client.name,
  },
  message: {
    body: message?.text?.body || '',
  },
  source: session.type,
  type: WebhookTypesEnum.MESSAGE,
}

// 3. Processar mensagem citada
if (message?.context?.id) {
  incomingMessage.message.quoted = { id: message?.context?.id }
}

// 4. Processar mídia
if (message.image || message.document || message.voice || message.audio || message.sticker || message.video) {
  const type = message?.type
  const { whatsappId, token } = session.channel.params as WhatsappConfigParams
  incomingMessage.message.media = {
    uri: await whatsapp.getMediaLink(message[type].id, whatsappId, token),
    contentType: message[type].mime_type,
    fileName: '',
  }
}

Retorno:

{
  uid: string,
  sessionId: ObjectId,
  platformConfigId: string,
  channelId: string,
  from: {
    uid: string,
    userName: string
  },
  message: {
    body: string,
    quoted?: { id: string },
    media?: {
      uri: string,
      contentType: string,
      fileName: string
    }
  },
  source: MessageServiceEnum,
  type: WebhookTypesEnum.MESSAGE
}
5.2. whatsappPayloadToStatus (Mapeamento de status)

Descrição: Converte webhook de status do WhatsApp para formato interno, mapeando estados de entrega.

Entrada:

{
  payload: WhatsappMessageDto, // Webhook do WhatsApp
  platformConfigId: string,    // ID da configuração
  channelId: string           // ID do canal
}

Implementação:

// 1. Extrair dados do status
const statusPayload = payload?.entry?.[0]?.changes?.[0]?.value?.statuses?.[0]
const metadata = payload?.entry?.[0]?.changes?.[0]?.value?.metadata

// 2. Mapear status
let messageStatus: WebhookStatusMessagesEnum
switch (statusPayload?.status) {
  case WhatsappMessageUpdateEnum.SENT:
    messageStatus = WebhookStatusMessagesEnum.SENDED
    break
  case WhatsappMessageUpdateEnum.DELIVERED:
    messageStatus = WebhookStatusMessagesEnum.DELIVERED
    break
  case WhatsappMessageUpdateEnum.READ:
    messageStatus = WebhookStatusMessagesEnum.READED
}

// 3. Criar objeto de status
const status: WebhookStatusDto = {
  platformConfigId,
  channelId,
  type: WebhookTypesEnum.MESSAGE_UPDATE,
  payload: {
    message: {
      from: metadata?.display_phone_number || null,
      to: statusPayload?.recipient_id || null,
      messageId: statusPayload?.id || null,
      status: messageStatus,
    }
  }
}

Retorno:

{
  platformConfigId: string,
  channelId: string,
  type: WebhookTypesEnum.MESSAGE_UPDATE,
  payload: {
    message: {
      from: string | null,
      to: string | null,
      messageId: string | null,
      status: WebhookStatusMessagesEnum
    }
  }
}

6. Configuração e Subscrição

6.1. Subscrição de App

6.1.1. subscribeApp
async subscribeApp(channel: PlatformChannelsConfig) 

Descrição: Subscreve aplicação aos webhooks da conta WhatsApp Business, verificando se já está subscrito.

Entrada:

{
  channel: PlatformChannelsConfig // Configuração do canal
}

Implementação:

//Extração de parâmetros
const wabaId = (channel.params as WhatsappConfigParams)?.wabaId
const token = (channel.params as WhatsappConfigParams)?.token
// 1. Obter apps subscritos
const subscribedApps = (
  await this.http.get(`${wabaId}/subscribed_apps`, {
    headers: { Authorization: `Bearer ${token}` }
  })
)?.data?.data

// 2. Verificar se app já está subscrito
const appId = process.env.WHATSAPP_META_API_APP_ID || '1274999130660446'
const whatsappApp = subscribedApps.find((app: any) => 
  app?.whatsapp_business_api_data?.id === appId
)

// 3. Subscrever se necessário
if (!whatsappApp)
  await this.http.post(`${wabaId}/subscribed_apps`, {}, {
    headers: { Authorization: `Bearer ${token}` }
  })

Retorno:

AxiosResponse | void // Response da subscrição ou void se já subscrito

7. DTOs e Estruturas de Dados

7.1. WhatsappMessageDto

Descrição: DTO principal para recebimento de webhooks do WhatsApp, contendo mensagens e atualizações de status.

{
  object: string,
  entry: Array<{
    id: string,
    time?: number,
    changes?: Array<{
      value: {
        messaging_product: string,
        metadata: {
          display_phone_number: string,
          phone_number_id: string
        },
        contacts?: Array<{
          profile: { name: string },
          wa_id: string
        }>,
        messages?: Array<{
          from: string,
          id: string,
          timestamp: string,
          type: string,
          text?: { body: string },
          image?: { mime_type: string, sha256: string, id: string },
          document?: { mime_type: string, sha256: string, id: string },
          voice?: { mime_type: string, sha256: string, id: string },
          audio?: { mime_type: string, sha256: string, id: string },
          sticker?: { mime_type: string, sha256: string, id: string },
          video?: { mime_type: string, sha256: string, id: string },
          context?: { from: string, id: string }
        }>,
        statuses?: Array<{
          id: string,
          status: 'sent' | 'delivered' | 'read',
          timestamp: string,
          recipient_id: string,
          conversation: {
            id: string,
            expiration_timestamp?: string,
            origin: { type: string }
          },
          pricing: {
            billable: boolean,
            pricing_model: string,
            category: string
          }
        }>
      },
      field: string
    }>
  }>
}

7.2. SendMessageWhatsappPayload

Descrição: Payload para envio de mensagens via API do WhatsApp, suportando texto, mídia e templates.

{
  messaging_product: 'whatsapp',
  to: string,
  type?: string,
  template?: {
    name: string,
    language: { code: string }
  },
  text?: { body: string },
  context?: { message_id: string },
  audio?: { id?: string; link?: string },
  document?: { id?: string; link?: string; filename?: string },
  image?: { id?: string; link?: string },
  sticker?: { id?: string; link?: string },
  video?: { id?: string; link?: string }
}

7.3. WhatsappConfigParams

Descrição: Parâmetros de configuração do canal WhatsApp, contendo credenciais e identificadores.

{
  token: string,      // Access Token
  whatsappId: string, // Phone Number ID
  wabaId: string     // WhatsApp Business Account ID
}

8. Enums e Constantes

8.1. WhatsappMessageUpdateEnum

Descrição: Estados de entrega de mensagens no WhatsApp.

enum WhatsappMessageUpdateEnum {
  SENT = 'sent',
  DELIVERED = 'delivered',
  READ = 'read'
}

8.2. WhatsappDefaultTemplateNames

Descrição: Nomes dos templates padrão utilizados no sistema.

enum WhatsappDefaultTemplateNames {
  contato_cliente = 'contato_cliente'
}

8.3. TemplateCategories

Descrição: Categorias de templates suportadas pelo WhatsApp Business.

enum TemplateCategories {
  UTILITY = 'UTILITY'
}