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
14pub 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() .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, ×tamp, &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}