feat: Add full WhatsApp Business Cloud API integration

- Add WhatsApp channel module with Cloud API v18.0 support
- Implement webhook-based message reception and API sending
- Add allowlist for phone numbers (E.164 format or wildcard)
- Add WhatsApp webhook endpoints to gateway (/whatsapp GET/POST)
- Add WhatsApp config schema with TOML support
- Wire WhatsApp into channel factory, CLI, and doctor commands
- Add WhatsApp to setup wizard with connection testing
- Add comprehensive test coverage (47 channel tests + 9 URL decoding tests)
- Update README with detailed WhatsApp setup instructions
- Support text messages only, skip media/status updates
- Normalize phone numbers with + prefix
- Handle webhook verification with Meta challenge-response

All 756 tests pass. Ready for production use.
This commit is contained in:
argenis de la rosa 2026-02-14 13:10:16 -05:00
parent ec2d5cc93d
commit cc08f4bfff
6 changed files with 1749 additions and 5 deletions

View file

@ -5,6 +5,7 @@ pub mod matrix;
pub mod slack;
pub mod telegram;
pub mod traits;
pub mod whatsapp;
pub use cli::CliChannel;
pub use discord::DiscordChannel;
@ -13,6 +14,7 @@ pub use matrix::MatrixChannel;
pub use slack::SlackChannel;
pub use telegram::TelegramChannel;
pub use traits::Channel;
pub use whatsapp::WhatsAppChannel;
use crate::config::Config;
use crate::memory::{self, Memory};
@ -236,6 +238,7 @@ pub fn handle_command(command: super::ChannelCommands, config: &Config) -> Resul
("Webhook", config.channels_config.webhook.is_some()),
("iMessage", config.channels_config.imessage.is_some()),
("Matrix", config.channels_config.matrix.is_some()),
("WhatsApp", config.channels_config.whatsapp.is_some()),
] {
println!(" {} {name}", if configured { "" } else { "" });
}
@ -330,6 +333,18 @@ pub async fn doctor_channels(config: Config) -> Result<()> {
));
}
if let Some(ref wa) = config.channels_config.whatsapp {
channels.push((
"WhatsApp",
Arc::new(WhatsAppChannel::new(
wa.access_token.clone(),
wa.phone_number_id.clone(),
wa.verify_token.clone(),
wa.allowed_numbers.clone(),
)),
));
}
if channels.is_empty() {
println!("No real-time channels configured. Run `zeroclaw onboard` first.");
return Ok(());
@ -481,6 +496,15 @@ pub async fn start_channels(config: Config) -> Result<()> {
)));
}
if let Some(ref wa) = config.channels_config.whatsapp {
channels.push(Arc::new(WhatsAppChannel::new(
wa.access_token.clone(),
wa.phone_number_id.clone(),
wa.verify_token.clone(),
wa.allowed_numbers.clone(),
)));
}
if channels.is_empty() {
println!("No channels configured. Run `zeroclaw onboard` to set up channels.");
return Ok(());