Package com.tnsai.channels.whatsapp
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:
- Meta GETs
<webhookPath>?hub.mode=subscribe&hub.verify_token=X&hub.challenge=Yat subscription time; we echoYiffXequals the configuredWhatsAppChannelConfig.verifyToken. - 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. - Parsed text messages become
UnifiedMessages withconversationId = wa_id(the sender's phone-equivalent hash). Outbound responsesPOST .../messageswith that samewa_idasto.
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 transientChannelErrorbut 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.
-
Constructor Summary
ConstructorsConstructorDescriptionDefault constructor — bootstraps fromWhatsAppChannelConfig.fromEnvironment().WhatsAppChannel(WhatsAppChannelConfig config) Programmatic constructor — used by the factory. -
Method Summary
Modifier and TypeMethodDescriptionDeclares what this channel supports.Human-readable display name.id()Unique identifier for this channel (e.g.booleanWhether the adapter is currently connected and operational.voidsend(UnifiedResponse response) Send a response back through this channel.voidstart(ChannelContext context) Start listening for inbound messages.voidstop()Stop listening and release resources.
-
Constructor Details
-
WhatsAppChannel
public WhatsAppChannel()Default constructor — bootstraps fromWhatsAppChannelConfig.fromEnvironment(). -
WhatsAppChannel
Programmatic constructor — used by the factory.
-
-
Method Details
-
id
Description copied from interface:ChannelAdapterUnique identifier for this channel (e.g. "telegram", "discord", "slack").- Specified by:
idin interfaceChannelAdapter
-
displayName
Description copied from interface:ChannelAdapterHuman-readable display name.- Specified by:
displayNamein interfaceChannelAdapter
-
capabilities
Description copied from interface:ChannelAdapterDeclares what this channel supports.- Specified by:
capabilitiesin interfaceChannelAdapter
-
start
Description copied from interface:ChannelAdapterStart listening for inbound messages. The adapter should callChannelContext.onMessage(com.tnsai.channels.api.UnifiedMessage)when a message arrives.- Specified by:
startin interfaceChannelAdapter- Parameters:
context- callback interface for delivering events to the gateway
-
stop
public void stop()Description copied from interface:ChannelAdapterStop listening and release resources.- Specified by:
stopin interfaceChannelAdapter
-
isRunning
public boolean isRunning()Description copied from interface:ChannelAdapterWhether the adapter is currently connected and operational.- Specified by:
isRunningin interfaceChannelAdapter
-
send
Description copied from interface:ChannelAdapterSend a response back through this channel.- Specified by:
sendin interfaceChannelAdapter- Parameters:
response- the unified response to deliver
-