Skip to main content

voice_echo/twilio/
outbound.rs

1use crate::config::TwilioConfig;
2
3/// Twilio REST API client for initiating outbound calls.
4pub 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    /// Initiate an outbound call. Twilio will call `to`, and when answered,
24    /// POST to our /twilio/voice/outbound webhook which provides TwiML
25    /// to connect the media stream. The greeting is handled by the stream via TTS.
26    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(&params)
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}