Files
gochat/backend/internal/handler/webhook/whatsapp_webhook.go
T
rogee aeddedf2a3 Reorganize repo: backend/, deploy/, docs/ layout + AGENTS.md
Restructure the monorepo into clear top-level directories:
- backend/: Go module root (cmd, internal, pkg, configs, migrations,
  docs/swagger, scripts, tests, go.mod, Makefile, .air.toml)
- deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd
- docs/: project documentation + reports/ (moved from repo root)
- AGENTS.md: new AI coding-agent guide at repo root

Update all references to the new layout:
- Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root)
- docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile,
  env_file ../../.env, volume mounts ../../backend:/app
- deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile
- CI: working-directory: backend for go commands, file deploy/docker/Dockerfile,
  coverage path backend/coverage.out, health_check backend/scripts/
- backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../
- README: architecture tree, quickstart, config paths updated

Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh,
gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to
preserve history. Build, vet, SQLite tests, and docker compose config verified.
2026-07-07 14:44:12 +08:00

127 lines
4.9 KiB
Go

// Package webhook provides Gin-compatible HTTP handler adapters for external
// channel webhook endpoints. Each adapter wraps the corresponding channel
// sub-package's WebhookHandler and exposes methods that can be bound directly
// to Gin router groups.
package webhook
import (
"context"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"github.com/gochat/gochat/internal/channel"
"github.com/gochat/gochat/internal/channel/whatsapp"
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/worker"
)
// WhatsAppWebhookHandler is a Gin adapter that wraps the WhatsApp
// sub-package's WebhookHandler. It translates Gin context calls into the
// underlying handler methods so that WhatsApp webhook routes can be registered
// on a Gin engine with minimal glue code.
//
// Chatwoot-style routes:
//
// GET /webhooks/whatsapp/:phone_number → HandleWhatsAppVerification
// POST /webhooks/whatsapp/:phone_number → HandleWhatsAppWebhook
//
// WhatsApp Cloud API webhook verification (GET):
//
// The Meta platform sends a GET request with query parameters:
// - hub.mode = "subscribe"
// - hub.verify_token = the token configured in the Meta dashboard
// - hub.challenge = a string the endpoint must echo back verbatim
//
// The handler validates hub.verify_token against the stored token and
// returns hub.challenge as the response body with HTTP 200, or returns
// HTTP 403 on mismatch.
//
// WhatsApp Cloud API webhook events (POST):
//
// Meta delivers event payloads as JSON with structure:
// {
// "object": "whatsapp_business_account",
// "entry": [ { "changes": [ ... ] } ]
// }
//
// The handler parses the payload, dispatches incoming messages and status
// updates, and must respond with HTTP 200 OK within 10 seconds to avoid
// Meta retrying delivery.
type WhatsAppWebhookHandler struct {
provider *whatsapp.WhatsAppProvider
waWebhook *whatsapp.WebhookHandler
persister *IncomingPersister
}
// NewWhatsAppWebhookHandler creates a Gin adapter wrapping the WhatsApp
// sub-package's WebhookHandler. The provider is stored for future use (e.g.
// health checks or direct API calls) while waWebhook is the core handler that
// processes verification and event requests.
func NewWhatsAppWebhookHandler(provider *whatsapp.WhatsAppProvider, waWebhook *whatsapp.WebhookHandler, db *gorm.DB, dispatcher ...*channel.Dispatcher) *WhatsAppWebhookHandler {
h := &WhatsAppWebhookHandler{
provider: provider,
waWebhook: waWebhook,
persister: NewIncomingPersister(db, dispatcher...),
}
if waWebhook != nil && h.persister != nil {
waWebhook.SetIncomingPersister(whatsAppPersisterAdapter{persister: h.persister})
}
return h
}
func (h *WhatsAppWebhookHandler) WithWorkerPool(wp *worker.WorkerPool) *WhatsAppWebhookHandler {
if h != nil && h.persister != nil {
h.persister.SetWorkerPool(wp)
}
return h
}
func (h *WhatsAppWebhookHandler) WithSearchIndexer(indexer IncomingSearchIndexer) *WhatsAppWebhookHandler {
if h != nil && h.persister != nil {
h.persister.SetSearchIndexer(indexer)
}
return h
}
type whatsAppPersisterAdapter struct {
persister *IncomingPersister
}
func (a whatsAppPersisterAdapter) PersistIncoming(ctx context.Context, inbox *model.Inbox, msg *channel.IncomingMessage) (interface{}, error) {
return a.persister.PersistIncoming(ctx, inbox, msg)
}
func (a whatsAppPersisterAdapter) UpdateMessageStatus(ctx context.Context, inbox *model.Inbox, sourceID string, status model.MessageStatus, occurredAt *time.Time) error {
return a.persister.UpdateMessageStatus(ctx, inbox, sourceID, status, occurredAt)
}
func (a whatsAppPersisterAdapter) UpdateMessageStatusWithError(ctx context.Context, inbox *model.Inbox, sourceID string, status model.MessageStatus, occurredAt *time.Time, externalError string) error {
return a.persister.UpdateMessageStatusWithError(ctx, inbox, sourceID, status, occurredAt, externalError)
}
// HandleWhatsAppVerification handles GET requests for WhatsApp Cloud API
// webhook verification. Meta sends this request during initial webhook setup
// and periodic re-verification. It delegates to the underlying
// waWebhook.HandleVerification method which validates hub.verify_token and
// echoes hub.challenge on success.
//
// Expected Gin route: GET /webhooks/whatsapp/:phone_number
func (h *WhatsAppWebhookHandler) HandleWhatsAppVerification(c *gin.Context) {
h.waWebhook.HandleVerification(c)
}
// HandleWhatsAppWebhook handles POST requests carrying WhatsApp Cloud API
// event payloads (messages, status updates, template events, etc.). It
// delegates to the underlying waWebhook.HandleWebhookEvent method which
// parses the JSON body and dispatches events to the appropriate processor.
//
// The endpoint must respond with HTTP 200 OK within 10 seconds; Meta retries
// delivery on timeout or non-200 responses.
//
// Expected Gin route: POST /webhooks/whatsapp/:phone_number
func (h *WhatsAppWebhookHandler) HandleWhatsAppWebhook(c *gin.Context) {
h.waWebhook.HandleWebhookEvent(c)
}