Class WhatsAppChannel

java.lang.Object
com.tnsai.channels.whatsapp.WhatsAppChannel
All Implemented Interfaces:
ChannelAdapter

@Channel(id="whatsapp", displayName="WhatsApp") public class WhatsAppChannel extends Object implements ChannelAdapter
WhatsApp Business Cloud API channel adapter (TNS-352).

WhatsApp delivers inbound events via HTTP webhook only — there is no WebSocket, no long-poll alternative. This adapter therefore embeds a JDK HttpServer bound to the configured port + path to receive Meta's POSTs. Outbound goes via Graph API (POST /v.../{phone_id}/messages) on the standard OkHttp client like the other adapters.

Webhook lifecycle:

  1. Meta GETs <webhookPath>?hub.mode=subscribe&hub.verify_token=X&hub.challenge=Y at subscription time; we echo Y iff X equals the configured WhatsAppChannelConfig.verifyToken.
  2. For every inbound message Meta POSTs JSON to the same path, signing with HMAC-SHA256 of the request body under the app secret in the X-Hub-Signature-256: sha256=<hex> header. We verify the signature before parsing — unsigned or wrongly-signed requests get a 403 and never reach the agent.
  3. Parsed text messages become UnifiedMessages with conversationId = wa_id (the sender's phone-equivalent hash). Outbound responses POST .../messages with that same wa_id as to.

v1 scope is deliberately tight to keep the PR reviewable:

  • Inbound and outbound text only. Media (image, document, audio, location, sticker) are logged and dropped on the inbound side; the SPI doesn't model attachments the way Meta does, and the right answer is a media-download follow-up that fetches the URL via the Graph API media endpoint and converts to Attachment.ofData().
  • No template messages. WhatsApp's 24-hour customer-service window means agents can only reply to user-initiated messages — templated outreach requires preapproved message templates which are a separate API surface entirely. v1 assumes the agent is always replying to a recent inbound message.
  • No 24-hour-window enforcement. The Graph API will reject outbound text past the window with error.code = 131047; we surface that as a transient ChannelError but don't try to bridge to templates.
  • Loopback bind by default. Production deployments put a reverse proxy (Caddy / nginx) in front for TLS; the embedded server speaks plain HTTP on localhost.

Thread safety: send(com.tnsai.channels.api.UnifiedResponse) can be invoked from any agent thread concurrently with the webhook handler thread pool. Both routes share only immutable config and a single OkHttp client; no mutable adapter state to guard.