houndify/
client.rs

1use crate::error::HoundifyError;
2use crate::query::{Query, RequestInfo, TextQuery, VoiceQuery};
3use crate::response::HoundServerResponse;
4use base64;
5use hmac::{Hmac, Mac};
6use reqwest::blocking::{Body, Client as HttpClient};
7use reqwest::header::HeaderMap;
8use sha2::Sha256;
9use std::time::SystemTime;
10use uuid::Uuid;
11
12pub type Result<T> = std::result::Result<T, HoundifyError>;
13
14/// Default Houndify API endpoint
15pub static DEFAULT_API_ENDPOINT: &str = "https://api.houndify.com/";
16
17fn get_current_timestamp() -> u64 {
18    SystemTime::now()
19        .duration_since(SystemTime::UNIX_EPOCH)
20        .unwrap()
21        .as_secs()
22}
23
24#[derive(Debug)]
25pub struct Client {
26    api_url: String,
27    client_id: String,
28    client_key: String,
29    http_client: HttpClient,
30    request_id_generator: fn() -> String,
31}
32
33impl Client {
34    pub fn new(
35        api_url: &str,
36        client_id: &str,
37        client_key: &str,
38        request_id_generator_option: Option<fn() -> String>,
39    ) -> Self {
40        let http_client = reqwest::blocking::Client::builder()
41            .http1_title_case_headers() // because houndify API headers are case-sensitive :(
42            .build()
43            .unwrap();
44
45        let request_id_generator = match request_id_generator_option {
46            Some(f) => f,
47            None => || Uuid::new_v4().to_string(),
48        };
49
50        Client {
51            api_url: api_url.to_string(),
52            client_id: client_id.to_string(),
53            client_key: client_key.to_string(),
54            http_client,
55            request_id_generator,
56        }
57    }
58
59    fn build_auth_headers(
60        &self,
61        user_id: &str,
62        request_id: &str,
63        timestamp: u64,
64    ) -> std::result::Result<HeaderMap, Box<dyn std::error::Error>> {
65        let decoded_client_key = base64::decode_config(&self.client_key, base64::URL_SAFE)?;
66        let mut mac: Hmac<Sha256> = Hmac::new_varkey(&decoded_client_key).unwrap();
67        let data = format!("{};{}{}", user_id, request_id, timestamp.to_string());
68        mac.input(data.as_bytes());
69        let hmac_result = mac.result();
70        let signature = base64::encode_config(&hmac_result.code(), base64::URL_SAFE);
71        let mut header_map = HeaderMap::new();
72        header_map.insert(
73            "Hound-Client-Authentication",
74            format!("{};{};{}", &self.client_id, &timestamp, &signature).parse()?,
75        );
76        header_map.insert(
77            "Hound-Request-Authentication",
78            format!("{};{}", &user_id, &request_id).parse()?,
79        );
80        Ok(header_map)
81    }
82
83    fn build_request_headers(
84        &self,
85        user_id: &str,
86        request_id: &str,
87        timestamp: u64,
88        request_info: &mut RequestInfo,
89    ) -> Result<HeaderMap> {
90        let mut headers = match self.build_auth_headers(user_id, &request_id, timestamp) {
91            Ok(h) => h,
92            Err(e) => return Err(HoundifyError::new(e.into())),
93        };
94
95        &request_info.timestamp(timestamp);
96        &request_info.client_id(&self.client_id);
97
98        let request_info_json = &request_info.clone().serialize()?;
99        let request_info_len = request_info_json.len();
100        headers.insert("Houndify-Request-Info", request_info_json.parse().unwrap());
101        headers.insert(
102            "Houndify-Request-Info-Length",
103            request_info_len.to_string().parse().unwrap(),
104        );
105
106        Ok(headers)
107    }
108
109    pub fn text_query(&self, mut query: TextQuery) -> Result<HoundServerResponse> {
110        let timestamp = get_current_timestamp();
111        let request_id = (&self.request_id_generator)();
112        let headers = self.build_request_headers(
113            &query.user_id,
114            &request_id,
115            timestamp,
116            &mut query.request_info,
117        )?;
118        let url = query.get_url(&self.api_url);
119        let req = self.http_client.get(&url).headers(headers);
120        println!("Request={:#?}", req);
121
122        match req.send() {
123            Ok(r) => self.parse_response(r),
124            Err(e) => {
125                println!("Error={:#?}", e);
126                Err(HoundifyError::new(e.into()))
127            }
128        }
129    }
130
131    pub fn voice_query(&self, mut query: VoiceQuery) -> Result<HoundServerResponse> {
132        let timestamp = get_current_timestamp();
133        let request_id = (&self.request_id_generator)();
134        let headers = self.build_request_headers(
135            &query.user_id,
136            &request_id,
137            timestamp,
138            &mut query.request_info,
139        )?;
140        let url = query.get_url(&self.api_url);
141        let req = self
142            .http_client
143            .post(&url)
144            .body(Body::new(query.audio_stream))
145            .headers(headers);
146        println!("Request={:#?}", req);
147        match req.send() {
148            Ok(r) => self.parse_response(r),
149            Err(e) => {
150                println!("Error={:#?}", e);
151                Err(HoundifyError::new(e.into()))
152            }
153        }
154    }
155
156    fn parse_response(&self, res: reqwest::blocking::Response) -> Result<HoundServerResponse> {
157        match res.text() {
158            Ok(res) => {
159                match serde_json::from_str(&res) {
160                    Ok(json) => Ok(json),
161                    Err(e) => Err(HoundifyError::new(e.into())),
162                }
163            },
164            Err(e) => Err(HoundifyError::new(e.into())),
165        }
166    }
167}
168
169#[cfg(test)]
170mod client_tests {
171    use super::*;
172
173    #[test]
174    fn test_generate_auth_values() {
175        let client_id = String::from("EqQpJDGt0YozIb8Az6xvvA==");
176        let client_key = String::from("jLTVjUOFBSetQtA3l-lGlb75rPVqKmH_JFgOVZjl4BdJqOq7PwUpub8ROcNnXUTssqd6M_7rC8Jn3_FjITouxQ==");
177        let api_base = String::from("https://api.houndify.com/");
178        let client = Client::new(&api_base, &client_id, &client_key, None);
179        let auth_headers = client
180            .build_auth_headers("test_user", "deadbeef", 1580278266)
181            .unwrap();
182        assert_eq!(
183            auth_headers.get("Hound-Client-Authentication").unwrap(),
184            "EqQpJDGt0YozIb8Az6xvvA==;1580278266;Ix3_MpLnyz1jGEV5g-mXxmbfgfZ85rD8-6S6yRTJEag="
185        );
186        assert_eq!(
187            auth_headers.get("Hound-Request-Authentication").unwrap(),
188            "test_user;deadbeef"
189        );
190    }
191}