twilio_agnostic/
lib.rs

1#[cfg(feature = "voice")]
2mod call;
3#[cfg(feature = "sms")]
4mod message;
5
6#[cfg(feature = "webhook")]
7pub mod twiml;
8#[cfg(feature = "webhook")]
9mod webhook;
10
11#[cfg(feature = "voice")]
12pub use call::{Call, OutboundCall};
13#[cfg(feature = "sms")]
14pub use message::{Message, OutboundMessage};
15
16use http::{
17    header::{HeaderValue, CONTENT_LENGTH, CONTENT_TYPE},
18    Method, StatusCode,
19};
20use isahc::{
21    auth::{Authentication, Credentials},
22    config::Configurable,
23    AsyncBody, AsyncReadResponseExt,
24};
25
26use std::{
27    collections::BTreeMap,
28    error::Error,
29    fmt::{self, Display, Formatter},
30};
31
32pub const GET: Method = Method::GET;
33pub const POST: Method = Method::POST;
34pub const PUT: Method = Method::PUT;
35
36pub struct Client {
37    account_id: String,
38    auth_token: String,
39}
40
41fn url_encode(params: &[(&str, &str)]) -> String {
42    params
43        .iter()
44        .map(|&t| {
45            let (k, v) = t;
46            format!("{}={}", k, v)
47        })
48        .fold("".to_string(), |mut acc, item| {
49            acc.push_str(&item);
50            acc.push_str("&");
51            acc.replace("+", "%2B")
52        })
53}
54
55#[derive(Debug)]
56pub enum TwilioError {
57    NetworkError(http::Error),
58    TransmissionError(isahc::error::Error),
59    HTTPError(StatusCode),
60    ParsingError,
61    AuthError,
62    BadRequest,
63}
64
65impl Display for TwilioError {
66    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
67        match *self {
68            TwilioError::NetworkError(ref e) => e.fmt(f),
69            TwilioError::TransmissionError(ref e) => e.fmt(f),
70            TwilioError::HTTPError(ref s) => write!(f, "Invalid HTTP status code: {}", s),
71            TwilioError::ParsingError => f.write_str("Parsing error"),
72            TwilioError::AuthError => f.write_str("Missing `X-Twilio-Signature` header in request"),
73            TwilioError::BadRequest => f.write_str("Bad request"),
74        }
75    }
76}
77
78impl Error for TwilioError {
79    fn source(&self) -> Option<&(dyn Error + 'static)> {
80        match *self {
81            TwilioError::NetworkError(ref e) => Some(e),
82            _ => None,
83        }
84    }
85}
86
87pub trait FromMap {
88    fn from_map(m: BTreeMap<String, String>) -> Result<Box<Self>, TwilioError>;
89}
90
91impl Client {
92    pub fn new(account_id: &str, auth_token: &str) -> Client {
93        Client {
94            account_id: account_id.to_string(),
95            auth_token: auth_token.to_string(),
96        }
97    }
98
99    async fn send_request<T>(
100        &self,
101        method: http::Method,
102        endpoint: &str,
103        params: &[(&str, &str)],
104    ) -> Result<T, TwilioError>
105    where
106        T: serde::de::DeserializeOwned + std::marker::Unpin,
107    {
108        let url = format!(
109            "https://api.twilio.com/2010-04-01/Accounts/{}/{}.json",
110            self.account_id, endpoint
111        );
112        let req = isahc::Request::builder()
113            .method(method)
114            .uri(&url)
115            .header(
116                CONTENT_TYPE,
117                HeaderValue::from_static("application/x-www-form-urlencoded"),
118            )
119            .authentication(Authentication::basic())
120            .credentials(Credentials::new(
121                self.account_id.clone(),
122                self.auth_token.clone(),
123            ))
124            .body(AsyncBody::from(url_encode(params)))
125            .map_err(|e| TwilioError::NetworkError(e))?;
126
127        let mut resp = isahc::send_async(req)
128            .await
129            .map_err(TwilioError::TransmissionError)?;
130
131        match resp.status() {
132            StatusCode::CREATED | StatusCode::OK => {
133                let value: T = resp.json().await.map_err(|_| TwilioError::ParsingError)?;
134                Ok(value)
135            }
136            other => return Err(TwilioError::HTTPError(other)),
137        }
138    }
139
140    #[cfg(feature = "webhook")]
141    pub async fn respond_to_webhook<T: FromMap, F>(
142        &self,
143        req: http::Request<&[u8]>,
144        mut logic: F,
145    ) -> http::Response<String>
146    where
147        F: FnMut(T) -> twiml::Twiml,
148    {
149        let o: T = match self.parse_request::<T>(req).await {
150            Ok(obj) => *obj,
151            Err(_) => {
152                let mut res = http::Response::new("Error.".to_string());
153                *res.status_mut() = StatusCode::BAD_REQUEST;
154                return res;
155            }
156        };
157
158        let t = logic(o);
159        let body = t.as_twiml();
160        let len = body.len() as u64;
161        let mut res = http::Response::new(body);
162        res.headers_mut().insert(
163            CONTENT_TYPE,
164            HeaderValue::from_static(mime::TEXT_XML.as_ref()),
165        );
166        res.headers_mut().insert(CONTENT_LENGTH, len.into());
167        res
168    }
169}