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:
parent
ec2d5cc93d
commit
cc08f4bfff
6 changed files with 1749 additions and 5 deletions
|
|
@ -3,6 +3,7 @@ use crate::config::{
|
|||
HeartbeatConfig, IMessageConfig, MatrixConfig, MemoryConfig, ObservabilityConfig,
|
||||
RuntimeConfig, SecretsConfig, SlackConfig, TelegramConfig, WebhookConfig,
|
||||
};
|
||||
use crate::config::schema::WhatsAppConfig;
|
||||
use anyhow::{Context, Result};
|
||||
use console::style;
|
||||
use dialoguer::{Confirm, Input, Select};
|
||||
|
|
@ -945,6 +946,7 @@ fn setup_channels() -> Result<ChannelsConfig> {
|
|||
webhook: None,
|
||||
imessage: None,
|
||||
matrix: None,
|
||||
whatsapp: None,
|
||||
};
|
||||
|
||||
loop {
|
||||
|
|
@ -989,6 +991,14 @@ fn setup_channels() -> Result<ChannelsConfig> {
|
|||
"— self-hosted chat"
|
||||
}
|
||||
),
|
||||
format!(
|
||||
"WhatsApp {}",
|
||||
if config.whatsapp.is_some() {
|
||||
"✅ connected"
|
||||
} else {
|
||||
"— Business Cloud API"
|
||||
}
|
||||
),
|
||||
format!(
|
||||
"Webhook {}",
|
||||
if config.webhook.is_some() {
|
||||
|
|
@ -1003,7 +1013,7 @@ fn setup_channels() -> Result<ChannelsConfig> {
|
|||
let choice = Select::new()
|
||||
.with_prompt(" Connect a channel (or Done to continue)")
|
||||
.items(&options)
|
||||
.default(6)
|
||||
.default(7)
|
||||
.interact()?;
|
||||
|
||||
match choice {
|
||||
|
|
@ -1425,6 +1435,91 @@ fn setup_channels() -> Result<ChannelsConfig> {
|
|||
});
|
||||
}
|
||||
5 => {
|
||||
// ── WhatsApp ──
|
||||
println!();
|
||||
println!(
|
||||
" {} {}",
|
||||
style("WhatsApp Setup").white().bold(),
|
||||
style("— Business Cloud API").dim()
|
||||
);
|
||||
print_bullet("1. Go to developers.facebook.com and create a WhatsApp app");
|
||||
print_bullet("2. Add the WhatsApp product and get your phone number ID");
|
||||
print_bullet("3. Generate a temporary access token (System User)");
|
||||
print_bullet("4. Configure webhook URL to: https://your-domain/whatsapp");
|
||||
println!();
|
||||
|
||||
let access_token: String = Input::new()
|
||||
.with_prompt(" Access token (from Meta Developers)")
|
||||
.interact_text()?;
|
||||
|
||||
if access_token.trim().is_empty() {
|
||||
println!(" {} Skipped", style("→").dim());
|
||||
continue;
|
||||
}
|
||||
|
||||
let phone_number_id: String = Input::new()
|
||||
.with_prompt(" Phone number ID (from WhatsApp app settings)")
|
||||
.interact_text()?;
|
||||
|
||||
if phone_number_id.trim().is_empty() {
|
||||
println!(" {} Skipped — phone number ID required", style("→").dim());
|
||||
continue;
|
||||
}
|
||||
|
||||
let verify_token: String = Input::new()
|
||||
.with_prompt(" Webhook verify token (create your own)")
|
||||
.default("zeroclaw-whatsapp-verify".into())
|
||||
.interact_text()?;
|
||||
|
||||
// Test connection
|
||||
print!(" {} Testing connection... ", style("⏳").dim());
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let url = format!(
|
||||
"https://graph.facebook.com/v18.0/{}",
|
||||
phone_number_id.trim()
|
||||
);
|
||||
match client
|
||||
.get(&url)
|
||||
.header("Authorization", format!("Bearer {}", access_token.trim()))
|
||||
.send()
|
||||
{
|
||||
Ok(resp) if resp.status().is_success() => {
|
||||
println!(
|
||||
"\r {} Connected to WhatsApp API ",
|
||||
style("✅").green().bold()
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
println!(
|
||||
"\r {} Connection failed — check access token and phone number ID",
|
||||
style("❌").red().bold()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let users_str: String = Input::new()
|
||||
.with_prompt(" Allowed phone numbers (comma-separated +1234567890, or * for all)")
|
||||
.default("*".into())
|
||||
.interact_text()?;
|
||||
|
||||
let allowed_numbers = if users_str.trim() == "*" {
|
||||
vec!["*".into()]
|
||||
} else {
|
||||
users_str
|
||||
.split(',')
|
||||
.map(|s| s.trim().to_string())
|
||||
.collect()
|
||||
};
|
||||
|
||||
config.whatsapp = Some(WhatsAppConfig {
|
||||
access_token: access_token.trim().to_string(),
|
||||
phone_number_id: phone_number_id.trim().to_string(),
|
||||
verify_token: verify_token.trim().to_string(),
|
||||
allowed_numbers,
|
||||
});
|
||||
}
|
||||
6 => {
|
||||
// ── Webhook ──
|
||||
println!();
|
||||
println!(
|
||||
|
|
@ -1479,6 +1574,9 @@ fn setup_channels() -> Result<ChannelsConfig> {
|
|||
if config.matrix.is_some() {
|
||||
active.push("Matrix");
|
||||
}
|
||||
if config.whatsapp.is_some() {
|
||||
active.push("WhatsApp");
|
||||
}
|
||||
if config.webhook.is_some() {
|
||||
active.push("Webhook");
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue