Bot Multi-Canal (WhatsApp, Messenger, Instagram)
Visão Geral
O bot de pedidos conversacional do PopinaFlow é agnóstico de canal. A máquina de estados central (estado: ConversationStateMachine) delega todo envio e recebimento a um adaptador de canal (IChannelAdapter), permitindo suporte a novos canais sem alterar a lógica de negócio.
Status por canal:
| Canal | Adaptador | Status de Produção |
|---|---|---|
| WhatsApp (Evolution API) | WhatsAppChannelAdapter | Produção |
| Facebook Messenger | MessengerChannelAdapter | Esqueleto — pendente Meta App Review |
| Instagram Direct | InstagramChannelAdapter | Esqueleto — pendente Meta App Review |
Localização
| Camada | Arquivo |
|---|---|
| Interface | backend/src/channels/channel-adapter.interface.ts |
| Adaptador WhatsApp | backend/src/channels/whatsapp.adapter.ts |
| Adaptador Messenger | backend/src/channels/messenger.adapter.ts |
| Adaptador Instagram | backend/src/channels/instagram.adapter.ts |
| Webhook Messenger | backend/src/channels/messenger-webhook.controller.ts |
| Webhook Instagram | backend/src/channels/instagram-webhook.controller.ts |
| Schema de config | backend/src/channels/schemas/channel-config.schema.ts |
| Módulo | backend/src/channels/channels.module.ts |
Interface IChannelAdapter
interface IChannelAdapter {
readonly channel: ChannelType; // 'whatsapp' | 'messenger' | 'instagram'
sendText(to: string, text: string, creds: ChannelCredentials): Promise<ChannelSendResult>;
sendMedia(to: string, mediaUrl: string, caption: string, creds: ChannelCredentials): Promise<ChannelSendResult>;
isConnected(creds: ChannelCredentials): Promise<boolean>;
}Princípio: adaptadores são stateless — recebem credenciais descriptografadas em cada chamada e não armazenam estado de sessão.
Tipo de mensagem normalizada (ChannelInboundMessage)
Independente do canal, o payload inbound é normalizado para:
| Campo | Tipo | Descrição |
|---|---|---|
externalUserId | string | ID nativo do canal (E.164 para WhatsApp, PSID para Messenger/IG) |
text | string | null | Texto da mensagem. null se somente mídia. |
isAudio | boolean | true para mensagens de voz (encaminhadas ao Whisper ASR) |
messageId | string | ID nativo da mensagem (dedup / auditoria) |
audioDurationSec | number? | Duração do áudio em segundos |
mediaRef | string? | Referência opaca à mídia (media ID da Meta, msgId do Evolution) |
Credenciais por Canal (ChannelCredentials)
Armazenadas criptografadas (AES-256-GCM) em documentos ChannelConfig por tenant:
| Campo | Messenger/IG | |
|---|---|---|
instanceId | Nome da instância Evolution | Meta phone number ID / Page ID |
accessTokenEncrypted | Token Evolution | Page Access Token |
webhookSecret | HMAC secret da instância | App Secret (para X-Hub-Signature-256) |
WhatsApp (Produção)
O WhatsAppChannelAdapter envolve o IWhatsAppProvider existente (Evolution API), que permanece em backend/src/whatsapp/providers/. Esta separação permite evolução independente:
IWhatsAppProvider→ notificações admin e fluxos de pedido legadosIChannelAdapter→ bot de pedidos multi-canal
Messenger e Instagram (Esqueleto — Pendente Meta App Review)
Os adaptadores estão implementados mas não enviam mensagens em produção ainda. As chamadas à Graph API estão comentadas aguardando aprovação do Meta.
O que já está implementado e funcional:
verifyHubSignature()— valida o headerX-Hub-Signature-256viaHMAC-SHA256(appSecret, rawBody)com comparação constant-time (timingSafeEqual)- Endpoint de verificação de webhook (
GET) para o desafio de verificação do Meta (hub.challenge) - Estrutura de credenciais e deserialização
API Graph (quando ativado):
POST https://graph.facebook.com/v18.0/me/messages
{
recipient: { id: PSID },
message: { text: "..." },
messaging_type: "RESPONSE",
access_token: PAGE_TOKEN
}Permissões Meta necessárias:
- Messenger:
pages_messaging - Instagram:
instagram_manage_messages
Verificação de Assinatura Meta
Tanto Messenger quanto Instagram usam o mesmo esquema HMAC-SHA256:
HMAC-SHA256(appSecret, rawRequestBody) → "sha256=<hex>"Verificado no header X-Hub-Signature-256. Importante: o body deve ser lido como buffer raw antes de qualquer JSON.parse — caso contrário a assinatura não bate.
Roteamento de Mensagens Inbound
POST /webhooks/channels/messenger (ou /instagram)
│
├── Verificar X-Hub-Signature-256
├── Normalizar payload → ChannelInboundMessage
└── ConversationStateMachine.handle(tenantId, message, 'messenger')
│
├── Identificar usuário / iniciar sessão de pedido
├── Chamar AssistantService (ferramentas de pedido)
└── IChannelAdapter.sendText() → resposta ao clienteHabilitando Messenger/Instagram (Checklist Pré-App Review)
- Criar App Meta em developers.facebook.com
- Solicitar permissão
pages_messaging(Messenger) ouinstagram_manage_messages(IG) - Configurar webhook URL:
https://seu-dominio.com/webhooks/channels/messenger - Adicionar
Page Access Tokennas credenciais do tenant via Admin → Integrações → Canais - Descomentar o bloco
// TODO: enable after Meta App Reviewnos adaptadores - Ativar o canal em
ChannelConfig.enabled = true