voice_echo/twilio/
outbound.rs1use crate::config::TwilioConfig;
2
3pub struct TwilioClient {
5 client: reqwest::Client,
6 account_sid: String,
7 auth_token: String,
8 from_number: String,
9 external_url: String,
10}
11
12impl TwilioClient {
13 pub fn new(twilio_config: &TwilioConfig, external_url: &str) -> Self {
14 Self {
15 client: reqwest::Client::new(),
16 account_sid: twilio_config.account_sid.clone(),
17 auth_token: twilio_config.auth_token.clone(),
18 from_number: twilio_config.phone_number.clone(),
19 external_url: external_url.to_string(),
20 }
21 }
22
23 pub async fn call(&self, to: &str) -> Result<String, OutboundError> {
27 let url = format!(
28 "https://api.twilio.com/2010-04-01/Accounts/{}/Calls.json",
29 self.account_sid
30 );
31
32 let webhook_url = format!("{}/twilio/voice/outbound", self.external_url);
33
34 let params = [
35 ("To", to),
36 ("From", &self.from_number),
37 ("Url", &webhook_url),
38 ];
39
40 let resp = self
41 .client
42 .post(&url)
43 .basic_auth(&self.account_sid, Some(&self.auth_token))
44 .form(¶ms)
45 .send()
46 .await
47 .map_err(|e| OutboundError::Request(e.to_string()))?;
48
49 if !resp.status().is_success() {
50 let status = resp.status();
51 let body = resp.text().await.unwrap_or_default();
52 return Err(OutboundError::Api(format!("{status}: {body}")));
53 }
54
55 let body: serde_json::Value = resp
56 .json()
57 .await
58 .map_err(|e| OutboundError::Request(e.to_string()))?;
59
60 let call_sid = body["sid"].as_str().unwrap_or("unknown").to_string();
61
62 tracing::info!(to, call_sid = %call_sid, "Outbound call initiated");
63 Ok(call_sid)
64 }
65}
66
67#[derive(Debug, thiserror::Error)]
68pub enum OutboundError {
69 #[error("HTTP request failed: {0}")]
70 Request(String),
71 #[error("Twilio API error: {0}")]
72 Api(String),
73}