Skip to content

Questão: Migrar WhatsApp de JavaScript para Go. #250

@matheusandre1

Description

@matheusandre1

A forma como está agora não me parece adequada; o whatsapp-js é frágil – depende do Puppeteer/Chromium headless e frequentemente apresenta problemas com as atualizações do WhatsApp Web (este problema não foi resolvido em um comentário específico em outra questão, o que deve estar causando problemas, já que o projeto ainda não foi atualizado para versões mais recentes da biblioteca).

Migração whatsapp/ → Go

Diagnóstico da Arquitetura Atual

O módulo whatsapp/ é um serviço Node.js de arquivo único (src/index.js, ~400 linhas) que funciona como adapter entre o WhatsApp e o timeless-api. A arquitetura atual apresenta problemas fundamentais de escalabilidade e manutenibilidade.

Problemas Que Eu identifiquei

Aspecto Problema
Runtime Node.js + Chromium headless (~500MB imagem Docker) — pesado, frágil, startup lento
Arquitetura Single-file (src/index.js) — sem separação de camadas, difícil testar e manter
Dual path Texto → SQS (async), Mídia → REST (sync) — inconsistente, duplica lógica de dispatch
Sessão wwebjs-auth/ em filesystem — impede escalabilidade horizontal (sticky session obrigatória)
Redis Declarado no docker-compose.yaml mas zero uso em código
Dependência crítica whatsapp-web.js automatiza WhatsApp Web via Puppeteer — não é API oficial, pode quebrar a qualquer atualização do WhatsApp
Tipagem JavaScript puro — contratos SQS/S3/REST sem validação em compile-time

Fluxo Atual

WhatsApp User
     │
     │ (WhatsApp message)
     ▼
┌──────────────────────────┐
│  whatsapp/ (Node.js)     │
│  whatsapp-web.js +       │
│  Chromium headless       │
└──────┬───────┬───────────┘
       │       │
       │       ├── [Audio] → OpenAI Whisper → POST /api/messages (REST)
       │       ├── [Image] → POST /api/messages/image (REST)
       │       └── [Text]  → SQS: incoming-messages.fifo
       │
       ▼
┌──────────────────────────┐
│  timeless-api (Quarkus)  │
│  AI processing + DB      │
└──────┬───────────────────┘
       │
       ▼
  SQS: messages-processed.fifo
       │
       ▼
┌──────────────────────────┐
│  whatsapp/ (sqs-consumer)│
│  → WhatsApp reply        │
└──────────────────────────┘

Dois caminhos de integração existem simultaneamente:

  1. Síncrono REST — mídia (imagem/áudio) chama timeless-api diretamente e espera resposta
  2. Assíncrono SQS — texto vai para fila, timeless-api consome, processa com IA, responde via outbox pattern ( isso está num pull request que está como rascunho ainda Use microprofile reactive-messaging for SQS and add pattern resilience for messaging #220

Por que Go

Benefícios Concretos

Métrica Node.js (atual) Go (proposto)
Tamanho da imagem Docker ~500MB (Node + Chromium) ~15-20MB (alpine + binário estático)
Consumo de RAM 200-500MB (Chromium) 20-50MB
Startup 10-30s (Chromium boot) <100ms
Instâncias por nó 1-2 10-20+
Concorrência Event loop (single-threaded) Goroutines (milhares leves)
Tipagem Dinâmica (JS) Estática (compile-time)
Deployment npm install + Chromium install Binário único
Dependência Chromium Sim — frágil, pesado Não — usa API oficial do Meta

Por que Go especificamente

  • Goroutines + channels — concorrência nativa para polling SQS, chamadas HTTP, upload S3 em paralelo, sem o overhead de threads do SO
  • Binário único compilado — sem runtime, sem node_modules, sem Chromium — deployment simplificado
  • Stdlib robustanet/http, encoding/json, crypto/*, os/signal cobrem a maior parte das necessidades
  • Cross-compileGOOS=linux GOARCH=amd64 go build gera binário para qualquer ambiente
  • Tipagem estática — contratos de mensagem SQS, responses da API, structs S3 validados antes do runtime
  • Ecossistema AWS maduro — SDK v2 oficial com tipagem forte, retry, middleware

Stack Go Recomendada

Responsabilidade Pacote Motivo
WhatsApp Integration net/http + WhatsApp Cloud API Elimina Puppeteer/Chromium — API oficial do Meta, estável e suportada
SQS Producer github.com/aws/aws-sdk-go-v2/service/sqs SDK oficial AWS v2, tipado, com retry e middleware
SQS Consumer github.com/aws/aws-sdk-go-v2/service/sqs + goroutine custom Long-polling com ReceiveMessage — simples com goroutines, sem lib extra
S3 Upload github.com/aws/aws-sdk-go-v2/service/s3 SDK oficial, consistente com SQS
Whisper (transcrição) github.com/sashabaranov/go-openai Client OpenAI maduro, suporta Whisper com CreateTranscription
HTTP Client (timeless-api) net/http + contexto com timeout Stdlib suficiente, sem framework
Config github.com/caarlos0/env Parsing de env vars para struct tipada com validação
Env local github.com/joho/godotenv Compatibilidade com .env existente
Logging log/slog (Go 1.21+) Structured logging nativo, zero dependência
Graceful shutdown os/signal + context.WithCancel Padrão idiomático Go
Testes testing (stdlib) + github.com/stretchr/testify Assertions legíveis, mocking via interfaces

Mudança Arquitetural Fundamental

De Puppeteer → WhatsApp Cloud API (Webhooks)

Hoje: Node.js abre browser headless, escaneia QR code, mantém sessão Chromium em filesystem. Impede escalabilidade horizontal.

Go: Meta envia POST (webhook) para cada mensagem recebida. O bot responde via POST para a Cloud API. Sem browser, sem sessão em filesystem, sem QR code.

┌─────────────────────────────────────────────────┐
│                  Meta Cloud                      │
│  WhatsApp Cloud API (webhook → POST no bot)     │
└────────────┬────────────────────┬───────────────┘
             │                    │
    [Webhook: mensagem]    [Send: resposta]
             │                    │
             ▼                    │
┌────────────────────┐           │
│  whatsapp-go       │           │
│  (Go binary)       │───────────┘
│                    │
│  ├─ /webhook       │  ← Meta envia mensagens aqui
│  ├─ SQS producer   │  → incoming-messages.fifo
│  ├─ SQS consumer   │  ← messages-processed.fifo
│  ├─ Whisper client │  → OpenAI API
│  ├─ S3 uploader    │  → AWS S3
│  └─ API client     │  → timeless-api (REST)
└────────────────────┘

Vantagens da Cloud API sobre Puppeteer

  • Stateless — N réplicas horizontalmente, sem sticky session
  • Sem Chromium — elimina a maior fonte de instabilidade e custo
  • Oficial — suportada pelo Meta, não quebra com atualizações do WhatsApp Web
  • Sem QR code — autenticação via token, não via sessão de browser ( isso aqui pode ser um exagero da minha parte, pode desconsiderar isso, podemos planejar outra forma
  • Webhook — push ao invés de polling no browser

Arquitetura Proposta em Go

whatsapp/
├── cmd/
│   └── bot/
│       └── main.go              # Entry point: config, wiring, graceful shutdown
├── internal/
│   ├── config/
│   │   └── config.go            # Struct tipada de env vars
│   ├── domain/
│   │   └── message.go           # Tipos: IncomingMessage, ProcessedMessage, etc.
│   ├── whatsapp/
│   │   ├── client.go            # WhatsApp Cloud API client (enviar mensagens)
│   │   └── webhook.go           # Handler para webhooks do Meta (recebimento)
│   ├── sqs/
│   │   ├── producer.go          # Envio para incoming-messages.fifo
│   │   └── consumer.go          # Long-poll de messages-processed.fifo
│   ├── s3/
│   │   └── uploader.go          # Upload de mídia
│   ├── transcription/
│   │   └── whisper.go           # OpenAI Whisper client
│   ├── api/
│   │   └── timeless.go          # HTTP client para timeless-api
│   └── handler/
│       ├── text.go              # Handler: texto → SQS
│       ├── audio.go             # Handler: áudio → Whisper → timeless-api REST
│       └── image.go             # Handler: imagem → timeless-api REST
├── pkg/
│   └── httpserver/
│       └── server.go            # HTTP server para webhooks + healthz
├── Dockerfile                   # Multi-stage: build → scratch/alpine, ~15MB
├── docker-compose.yaml
├── go.mod
├── go.sum
└── .env.example

Detalhamento dos Componentes

cmd/bot/main.go — Entry Point

func main() {
    cfg := config.Load()                          // Struct tipada de env vars
    ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
    defer stop()

    sqsClient := sqs.NewClient(cfg.AWS)           // SQS producer + consumer
    s3Client := s3.NewUploader(cfg.AWS)           // S3 uploader
    whisperClient := transcription.New(cfg.OpenAI)// Whisper
    apiClient := api.NewClient(cfg.TimelessAPI)   // timeless-api HTTP
    waClient := whatsapp.NewClient(cfg.WhatsApp)  // Cloud API send

    handler := handler.New(handler.Deps{
        SQS:         sqsClient,
        S3:          s3Client,
        Whisper:     whisperClient,
        API:         apiClient,
        WhatsApp:    waClient,
    })

    // Webhook receiver (Meta push)
    mux := http.NewServeMux()
    mux.HandleFunc("GET /webhook", webhook.Verify(cfg.WhatsApp.VerifyToken))
    mux.HandleFunc("POST /webhook", webhook.Handle(handler))
    mux.HandleFunc("GET /healthz", healthz)

    srv := &http.Server{Addr: ":" + cfg.Port, Handler: mux}

    // SQS consumer em goroutine separada
    go sqsClient.Consume(ctx, handler.OnProcessedMessage)

    // Graceful shutdown
    go func() {
        <-ctx.Done()
        shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
        defer cancel()
        srv.Shutdown(shutdownCtx)
    }()

    srv.ListenAndServe()
}

internal/domain/message.go — Contratos Tipados

type IncomingMessage struct {
    Sender      string `json:"sender"`
    Kind        string `json:"kind"`         // TEXT, IMAGE, AUDIO
    MessageID   string `json:"messageId"`
    MessageBody string `json:"messageBody"`
    MediaBase64 string `json:"mediaBase64,omitempty"`
    MimeType    string `json:"mimeType,omitempty"`
}

type ProcessedMessage struct {
    Kind     string                 `json:"kind"`  // GET_BALANCE, ADD_TRANSACTION
    Sender   string                 `json:"sender"`
    Metadata map[string]interface{} `json:"metadata"`
}

internal/sqs/consumer.go — Long-Polling com Goroutines

func (c *Consumer) Consume(ctx context.Context, handler func(ProcessedMessage)) {
    for {
        select {
        case <-ctx.Done():
            return
        default:
        }

        output, err := c.client.ReceiveMessage(ctx, &sqs.ReceiveMessageInput{
            QueueUrl:            c.queueURL,
            MaxNumberOfMessages: 10,
            WaitTimeSeconds:     20,  // Long-polling
        })
        if err != nil {
            slog.Error("sqs receive error", "err", err)
            continue
        }

        var wg sync.WaitGroup
        sem := make(chan struct{}, 10) // Concurrency limit

        for _, msg := range output.Messages {
            sem <- struct{}{}
            wg.Add(1)
            go func(m types.Message) {
                defer wg.Done()
                defer func() { <-sem }()

                var processed ProcessedMessage
                json.Unmarshal([]byte(*m.Body), &processed)
                handler(processed)

                c.client.DeleteMessage(ctx, &sqs.DeleteMessageInput{
                    QueueUrl:      c.queueURL,
                    ReceiptHandle: m.ReceiptHandle,
                })
            }(msg)
        }
        wg.Wait()
    }
}

internal/handler/text.go — Unificação do Fluxo

Com a Cloud API, todas as mensagens chegam via webhook. O handler decide o roteamento:

func (h *Handler) OnWebhookMessage(msg IncomingMessage) error {
    switch msg.Kind {
    case "TEXT":
        return h.routeText(msg)    // → SQS (async, mantém padrão atual)
    case "AUDIO":
        return h.routeAudio(msg)   // → Whisper → timeless-api REST
    case "IMAGE":
        return h.routeImage(msg)   // → timeless-api REST
    }
}

func (h *Handler) OnProcessedMessage(msg ProcessedMessage) error {
    return h.WhatsApp.SendText(msg.Sender, formatReply(msg))
}

Dockerfile Proposto

# Build
FROM golang:1.26-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /bot ./cmd/bot

# Runtime
FROM alpine:3.20
RUN apk --no-cache add ca-certificates
COPY --from=builder /bot /bot
EXPOSE 8080
ENTRYPOINT ["/bot"]

Resultado: Imagem ~15-20MB (vs ~500MB atual).


Estratégia de Migração (Gradual)

Fase 0 — Preparação (1-2 semanas)

  • Criar app no Meta Business Manager
  • Obter WHATSAPP_TOKEN, PHONE_NUMBER_ID, VERIFY_TOKEN
  • Configurar webhook URL com HTTPS (ngrok para dev local, ALB/CloudFront para prod)
  • Ajustar IAM/Terraform: adicionar permissões para Cloud API se necessário
  • Criar diretório whatsapp-go/ com a estrutura proposta
  • Implementar config.go, domain/message.go, main.go skeleton
  • Setup do go.mod com dependências

Fase 1 — Shadow Mode (2-3 semanas)

  • Implementar webhook receiver (/webhook GET verify + POST handler)
  • Implementar SQS producer (envio para incoming-messages.fifo)
  • Implementar SQS consumer (long-poll de messages-processed.fifo)
  • Implementar WhatsApp Cloud API sender (respostas ao usuário)
  • Implementar Whisper client (transcrição de áudio)
  • Implementar S3 uploader (upload de mídia)
  • Implementar HTTP client para timeless-api (POST /api/messages, POST /api/messages/image)
  • Não desligar o bot Node.js — ambos recebem mensagens, mas apenas o Node responde ao usuário
  • Logs comparativos para validar paridade entre Node e Go
  • Testes unitários para cada componente com mocks via interfaces

Fase 2 — Cutover (1 semana)

  • Desativar o bot Node.js (parar de receber mensagens)
  • Ativar respostas do bot Go
  • Monitorar: taxa de erro, latência, taxa de sucesso
  • Definir threshold de rollback (ex: erro > 5% → reativar Node.js)
  • Validar que todas as interações (texto, áudio, imagem) funcionam end-to-end

Fase 3 — Cleanup (1 semana)

  • Remover whatsapp/ (Node.js) do repositório
  • Remover Chromium buildpack
  • Remover Dockerfile antigo do Node
  • Atualizar Terraform (remover recursos órfãos do Puppeteer)
  • Remover Redis do docker-compose.yaml se ainda não usado
  • Atualizar CI/CD pipelines
  • Atualizar documentação

Escalabilidade — Como Go Resolve os Gargalos Atuais

Gargalo Atual Solução em Go
1 instância apenas (sessão Chromium em filesystem) Cloud API é stateless → N réplicas horizontalmente, sem sticky session
Chromium consome ~300-500MB RAM Binário Go ~20-50MB → mais instâncias por nó
sqs-consumer processa 1 msg por vez Goroutine pool com semaphore — N mensagens em paralelo
Startup lento (~10-30s para Chromium) Startup em <100ms — auto-scaling reativo
Sem health check estruturado /healthz endpoint — pronto para k8s readiness/liveness probe
Mídia síncrono bloqueia event loop go func() — I/O totalmente paralelo, sem bloqueio
Sem métricas expvar (stdlib) ou prometheus/client_golang — métricas de fila, latência, throughput

Escalabilidade Horizontal

Com a Cloud API, o serviço é stateless — qualquer instância pode processar qualquer mensagem. Isso permite:

  • Auto-scaling baseado em comprimento da fila SQS (CloudWatch alarm → scale out/in)
  • Zero downtime deploys — novas instâncias registram e começam a receber webhooks imediatamente
  • Multi-AZ — instâncias em zonas de disponibilidade diferentes sem restrição de sessão
  • Rolling updates — substituição gradual sem interrupção

Concorrência

// Processamento paralelo de mensagens com backpressure controlado
sem := make(chan struct{}, runtime.NumCPU()*2) // Limite baseado em CPU

for msg := range messages {
    sem <- struct{}{}
    go func(m Message) {
        defer func() { <-sem }()
        process(m)
    }(msg)
}

Pontos de Atenção

WhatsApp Cloud API

  • Rate limits — limites por número e por template. Importante para mensagens em massa
  • Webhook exige HTTPS + verificação do Meta — precisa de infra de TLS (ALB, CloudFront, ou ngrok para dev)
  • Templates de mensagem — mensagens proativas (fora da janela de 24h) exigem templates pré-aprovados pelo Meta
  • Custo — gratuita para conversas iniciadas pelo usuário (janela 24h), mas tem custo por conversa iniciada pelo business
  • Migração de sessões — usuários não precisam re-autenticar (Cloud API é independente de sessão do browser)

Compatibilidade com timeless-api

  • Os endpoints REST (POST /api/messages, POST /api/messages/image) permanecem inalterados — o bot Go é um drop-in replacement
  • Os contratos SQS (incoming-messages.fifo, messages-processed.fifo) permanecem os mesmos — basta serializar os mesmos JSONs
  • A IAM já está configurada no Terraform — apenas ajustar se necessário para Cloud API

Testabilidade

  • Cada componente (sqs, s3, whatsapp, transcription, api) deve ser definido como interface — permite mocking em testes
  • Testes de integração com LocalStack (já usado no projeto) para SQS e S3
  • Testes end-to-end com webhook mockado

Resumo: O que Deve Ser Usado

Item Escolha Justificativa
Linguagem Go 1.23+ Concorrência nativa, binário único, baixo consumo
WhatsApp Cloud API (webhooks) Oficial, stateless, sem Chromium
AWS SDK aws-sdk-go-v2 Tipado, retry, middleware oficial
OpenAI go-openai (sashabaranov) Maduro, suporta Whisper
HTTP net/http (stdlib) Suficiente, sem framework
Logging log/slog (stdlib) Structured logging nativo
Config caarlos0/env + godotenv Struct tipada, compatibilidade .env
Testes testing + testify Idiomático + assertions legíveis
Container alpine:3.20 + binário estático ~15-20MB imagem final
Métricas expvar ou prometheus/client_golang Observabilidade desde o início
CI/CD Reutilizar pipelines existentes Ajustar para go build e imagem Docker nova

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions