story-kit: merge 320_story_whatsapp_business_api_integration_for_bot_commands

This commit is contained in:
Dave
2026-03-19 23:03:35 +00:00
parent cc0110e577
commit 351f770516
8 changed files with 721 additions and 90 deletions

View File

@@ -368,8 +368,11 @@ pub async fn run_bot(
// Create the transport abstraction based on the configured transport type.
let transport: Arc<dyn ChatTransport> = match config.transport.as_str() {
"whatsapp" => {
slog!("[matrix-bot] Using WhatsApp transport (stub)");
Arc::new(crate::whatsapp::WhatsAppTransport::new())
slog!("[matrix-bot] Using WhatsApp transport");
Arc::new(crate::whatsapp::WhatsAppTransport::new(
config.whatsapp_phone_number_id.clone().unwrap_or_default(),
config.whatsapp_access_token.clone().unwrap_or_default(),
))
}
_ => {
slog!("[matrix-bot] Using Matrix transport");
@@ -1393,7 +1396,7 @@ mod tests {
ambient_rooms: Arc::new(std::sync::Mutex::new(HashSet::new())),
agents: Arc::new(AgentPool::new_test(3000)),
htop_sessions: Arc::new(TokioMutex::new(HashMap::new())),
transport: Arc::new(crate::whatsapp::WhatsAppTransport::new()),
transport: Arc::new(crate::whatsapp::WhatsAppTransport::new("test-phone".to_string(), "test-token".to_string())),
};
// Clone must work (required by Matrix SDK event handler injection).
let _cloned = ctx.clone();

View File

@@ -66,6 +66,20 @@ pub struct BotConfig {
/// round-tripping.
#[serde(default = "default_transport")]
pub transport: String,
// ── WhatsApp Business API fields ─────────────────────────────────
// These are only required when `transport = "whatsapp"`.
/// WhatsApp Business phone number ID from the Meta dashboard.
#[serde(default)]
pub whatsapp_phone_number_id: Option<String>,
/// Long-lived access token for the WhatsApp Business API.
#[serde(default)]
pub whatsapp_access_token: Option<String>,
/// Verify token used in the webhook handshake (you choose this value
/// and configure it in the Meta webhook settings).
#[serde(default)]
pub whatsapp_verify_token: Option<String>,
}
fn default_transport() -> String {
@@ -97,7 +111,31 @@ impl BotConfig {
{
config.room_ids.push(single);
}
if config.room_ids.is_empty() {
if config.transport == "whatsapp" {
// Validate WhatsApp-specific fields.
if config.whatsapp_phone_number_id.as_ref().is_none_or(|s| s.is_empty()) {
eprintln!(
"[bot] bot.toml: transport=\"whatsapp\" requires \
whatsapp_phone_number_id"
);
return None;
}
if config.whatsapp_access_token.as_ref().is_none_or(|s| s.is_empty()) {
eprintln!(
"[bot] bot.toml: transport=\"whatsapp\" requires \
whatsapp_access_token"
);
return None;
}
if config.whatsapp_verify_token.as_ref().is_none_or(|s| s.is_empty()) {
eprintln!(
"[bot] bot.toml: transport=\"whatsapp\" requires \
whatsapp_verify_token"
);
return None;
}
} else if config.room_ids.is_empty() {
eprintln!(
"[matrix-bot] bot.toml has no room_ids configured — \
add `room_ids = [\"!roomid:example.com\"]` to bot.toml"
@@ -556,10 +594,88 @@ password = "secret"
room_ids = ["!abc:example.com"]
enabled = true
transport = "whatsapp"
whatsapp_phone_number_id = "123456"
whatsapp_access_token = "EAAtoken"
whatsapp_verify_token = "my-verify"
"#,
)
.unwrap();
let config = BotConfig::load(tmp.path()).unwrap();
assert_eq!(config.transport, "whatsapp");
assert_eq!(
config.whatsapp_phone_number_id.as_deref(),
Some("123456")
);
assert_eq!(
config.whatsapp_access_token.as_deref(),
Some("EAAtoken")
);
assert_eq!(
config.whatsapp_verify_token.as_deref(),
Some("my-verify")
);
}
#[test]
fn load_whatsapp_returns_none_when_missing_phone_number_id() {
let tmp = tempfile::tempdir().unwrap();
let sk = tmp.path().join(".story_kit");
fs::create_dir_all(&sk).unwrap();
fs::write(
sk.join("bot.toml"),
r#"
homeserver = "https://matrix.example.com"
username = "@bot:example.com"
password = "secret"
enabled = true
transport = "whatsapp"
whatsapp_access_token = "EAAtoken"
whatsapp_verify_token = "my-verify"
"#,
)
.unwrap();
assert!(BotConfig::load(tmp.path()).is_none());
}
#[test]
fn load_whatsapp_returns_none_when_missing_access_token() {
let tmp = tempfile::tempdir().unwrap();
let sk = tmp.path().join(".story_kit");
fs::create_dir_all(&sk).unwrap();
fs::write(
sk.join("bot.toml"),
r#"
homeserver = "https://matrix.example.com"
username = "@bot:example.com"
password = "secret"
enabled = true
transport = "whatsapp"
whatsapp_phone_number_id = "123456"
whatsapp_verify_token = "my-verify"
"#,
)
.unwrap();
assert!(BotConfig::load(tmp.path()).is_none());
}
#[test]
fn load_whatsapp_returns_none_when_missing_verify_token() {
let tmp = tempfile::tempdir().unwrap();
let sk = tmp.path().join(".story_kit");
fs::create_dir_all(&sk).unwrap();
fs::write(
sk.join("bot.toml"),
r#"
homeserver = "https://matrix.example.com"
username = "@bot:example.com"
password = "secret"
enabled = true
transport = "whatsapp"
whatsapp_phone_number_id = "123456"
whatsapp_access_token = "EAAtoken"
"#,
)
.unwrap();
assert!(BotConfig::load(tmp.path()).is_none());
}
}

View File

@@ -61,6 +61,12 @@ pub fn spawn_bot(
}
};
// WhatsApp transport is handled via HTTP webhooks, not the Matrix sync loop.
if config.transport == "whatsapp" {
crate::slog!("[bot] transport=whatsapp — skipping Matrix bot; webhooks handle WhatsApp");
return;
}
crate::slog!(
"[matrix-bot] Starting Matrix bot → homeserver={} rooms={:?}",
config.homeserver,