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
|
|
@ -485,6 +485,7 @@ pub struct ChannelsConfig {
|
|||
pub webhook: Option<WebhookConfig>,
|
||||
pub imessage: Option<IMessageConfig>,
|
||||
pub matrix: Option<MatrixConfig>,
|
||||
pub whatsapp: Option<WhatsAppConfig>,
|
||||
}
|
||||
|
||||
impl Default for ChannelsConfig {
|
||||
|
|
@ -497,6 +498,7 @@ impl Default for ChannelsConfig {
|
|||
webhook: None,
|
||||
imessage: None,
|
||||
matrix: None,
|
||||
whatsapp: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -543,6 +545,19 @@ pub struct MatrixConfig {
|
|||
pub allowed_users: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WhatsAppConfig {
|
||||
/// Access token from Meta Business Suite
|
||||
pub access_token: String,
|
||||
/// Phone number ID from Meta Business API
|
||||
pub phone_number_id: String,
|
||||
/// Webhook verify token (you define this, Meta sends it back for verification)
|
||||
pub verify_token: String,
|
||||
/// Allowed phone numbers (E.164 format: +1234567890) or "*" for all
|
||||
#[serde(default)]
|
||||
pub allowed_numbers: Vec<String>,
|
||||
}
|
||||
|
||||
// ── Config impl ──────────────────────────────────────────────────
|
||||
|
||||
impl Default for Config {
|
||||
|
|
@ -717,6 +732,7 @@ mod tests {
|
|||
webhook: None,
|
||||
imessage: None,
|
||||
matrix: None,
|
||||
whatsapp: None,
|
||||
},
|
||||
memory: MemoryConfig::default(),
|
||||
tunnel: TunnelConfig::default(),
|
||||
|
|
@ -926,6 +942,7 @@ default_temperature = 0.7
|
|||
room_id: "!r:m".into(),
|
||||
allowed_users: vec!["@u:m".into()],
|
||||
}),
|
||||
whatsapp: None,
|
||||
};
|
||||
let toml_str = toml::to_string_pretty(&c).unwrap();
|
||||
let parsed: ChannelsConfig = toml::from_str(&toml_str).unwrap();
|
||||
|
|
@ -1010,6 +1027,89 @@ channel_id = "C123"
|
|||
assert_eq!(parsed.port, 8080);
|
||||
}
|
||||
|
||||
// ── WhatsApp config ──────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn whatsapp_config_serde() {
|
||||
let wc = WhatsAppConfig {
|
||||
access_token: "EAABx...".into(),
|
||||
phone_number_id: "123456789".into(),
|
||||
verify_token: "my-verify-token".into(),
|
||||
allowed_numbers: vec!["+1234567890".into(), "+9876543210".into()],
|
||||
};
|
||||
let json = serde_json::to_string(&wc).unwrap();
|
||||
let parsed: WhatsAppConfig = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(parsed.access_token, "EAABx...");
|
||||
assert_eq!(parsed.phone_number_id, "123456789");
|
||||
assert_eq!(parsed.verify_token, "my-verify-token");
|
||||
assert_eq!(parsed.allowed_numbers.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn whatsapp_config_toml_roundtrip() {
|
||||
let wc = WhatsAppConfig {
|
||||
access_token: "tok".into(),
|
||||
phone_number_id: "12345".into(),
|
||||
verify_token: "verify".into(),
|
||||
allowed_numbers: vec!["+1".into()],
|
||||
};
|
||||
let toml_str = toml::to_string(&wc).unwrap();
|
||||
let parsed: WhatsAppConfig = toml::from_str(&toml_str).unwrap();
|
||||
assert_eq!(parsed.phone_number_id, "12345");
|
||||
assert_eq!(parsed.allowed_numbers, vec!["+1"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn whatsapp_config_deserializes_without_allowed_numbers() {
|
||||
let json = r#"{"access_token":"tok","phone_number_id":"123","verify_token":"ver"}"#;
|
||||
let parsed: WhatsAppConfig = serde_json::from_str(json).unwrap();
|
||||
assert!(parsed.allowed_numbers.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn whatsapp_config_wildcard_allowed() {
|
||||
let wc = WhatsAppConfig {
|
||||
access_token: "tok".into(),
|
||||
phone_number_id: "123".into(),
|
||||
verify_token: "ver".into(),
|
||||
allowed_numbers: vec!["*".into()],
|
||||
};
|
||||
let toml_str = toml::to_string(&wc).unwrap();
|
||||
let parsed: WhatsAppConfig = toml::from_str(&toml_str).unwrap();
|
||||
assert_eq!(parsed.allowed_numbers, vec!["*"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn channels_config_with_whatsapp() {
|
||||
let c = ChannelsConfig {
|
||||
cli: true,
|
||||
telegram: None,
|
||||
discord: None,
|
||||
slack: None,
|
||||
webhook: None,
|
||||
imessage: None,
|
||||
matrix: None,
|
||||
whatsapp: Some(WhatsAppConfig {
|
||||
access_token: "tok".into(),
|
||||
phone_number_id: "123".into(),
|
||||
verify_token: "ver".into(),
|
||||
allowed_numbers: vec!["+1".into()],
|
||||
}),
|
||||
};
|
||||
let toml_str = toml::to_string_pretty(&c).unwrap();
|
||||
let parsed: ChannelsConfig = toml::from_str(&toml_str).unwrap();
|
||||
assert!(parsed.whatsapp.is_some());
|
||||
let wa = parsed.whatsapp.unwrap();
|
||||
assert_eq!(wa.phone_number_id, "123");
|
||||
assert_eq!(wa.allowed_numbers, vec!["+1"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn channels_config_default_has_no_whatsapp() {
|
||||
let c = ChannelsConfig::default();
|
||||
assert!(c.whatsapp.is_none());
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// SECURITY CHECKLIST TESTS — Gateway config
|
||||
// ══════════════════════════════════════════════════════════
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue