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}