1use std::convert::TryInto;
2use std::fmt::{self, Debug};
3
4use crate::error::{Error, Result};
5
6use common_multipart_rfc7578::client::{multipart, Error as MultipartError};
7use futures::future::BoxFuture;
8use futures::io::AsyncReadExt;
9use isahc::http;
10#[cfg(feature = "inspect-contents")]
11use log::debug;
12use mime::Mime;
13use misskey_core::model::ApiResult;
14use misskey_core::{Client, Request, UploadFileClient, UploadFileRequest};
15use serde::Serialize;
16use serde_json::value::{self, Value};
17use url::Url;
18
19pub mod builder;
20
21use builder::HttpClientBuilder;
22
23pub struct HttpClient {
28 url: Url,
29 token: Option<String>,
30 client: isahc::HttpClient,
31}
32
33impl Debug for HttpClient {
34 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
35 f.debug_struct("HttpClient")
36 .field("url", &self.url)
37 .finish()
38 }
39}
40
41impl HttpClient {
42 pub fn new(url: Url) -> Result<Self> {
44 Ok(HttpClient {
45 url,
46 token: None,
47 client: isahc::HttpClient::new()?,
48 })
49 }
50
51 pub fn with_token(url: Url, token: impl Into<String>) -> Result<Self> {
53 Ok(HttpClient {
54 url,
55 token: Some(token.into()),
56 client: isahc::HttpClient::new()?,
57 })
58 }
59
60 pub fn builder<T>(url: T) -> HttpClientBuilder
65 where
66 T: TryInto<Url>,
67 T::Error: Into<Error>,
68 {
69 HttpClientBuilder::new(url)
70 }
71
72 fn set_api_key<R: Request>(
73 &self,
74 request: R,
75 ) -> std::result::Result<impl Serialize, serde_json::Error> {
76 #[derive(Serialize)]
77 #[serde(untagged)]
78 enum ValueOrRequest<R> {
79 Value(Value),
80 Request(R),
81 }
82
83 if let Some(token) = &self.token {
84 let mut value = value::to_value(request)?;
85
86 let obj = value.as_object_mut().expect("Request must be an object");
87 if obj
88 .insert("i".to_string(), Value::String(token.to_owned()))
89 .is_some()
90 {
91 panic!("Request must not have 'i' key");
92 }
93
94 Ok(ValueOrRequest::Value(value))
95 } else {
96 Ok(ValueOrRequest::Request(request))
97 }
98 }
99
100 fn make_url<R: Request>(&self) -> Result<Url> {
101 let mut url = self.url.clone();
102 if let Ok(mut segments) = url.path_segments_mut() {
103 segments.pop_if_empty();
104 for segment in R::ENDPOINT.split('/') {
105 segments.push(segment);
106 }
107 } else {
108 return self.url.join(R::ENDPOINT).map_err(Into::into);
109 }
110 Ok(url)
111 }
112}
113
114impl Client for HttpClient {
115 type Error = Error;
116
117 fn request<R: Request>(&self, request: R) -> BoxFuture<Result<ApiResult<R::Response>>> {
118 let url = self.make_url::<R>();
119
120 let body = self
123 .set_api_key(request)
124 .and_then(|b| serde_json::to_vec(&b));
125
126 Box::pin(async move {
127 let url = url?;
128 let body = body?;
129
130 #[cfg(feature = "inspect-contents")]
131 debug!(
132 "sending request to {}: {}",
133 url,
134 String::from_utf8_lossy(&body)
135 );
136
137 use isahc::http::header::CONTENT_TYPE;
138 let response = self
139 .client
140 .send_async(
141 http::Request::post(url.to_string())
143 .header(CONTENT_TYPE, "application/json")
144 .body(body)
145 .unwrap(),
146 )
147 .await?;
148
149 response_to_result::<R>(response).await
150 })
151 }
152}
153
154impl UploadFileClient for HttpClient {
155 fn request_with_file<R, T>(
156 &self,
157 request: R,
158 type_: Mime,
159 file_name: String,
160 read: T,
161 ) -> BoxFuture<Result<ApiResult<R::Response>>>
162 where
163 R: UploadFileRequest,
164 T: std::io::Read + Send + Sync + 'static,
165 {
166 let url = self.make_url::<R>();
167
168 let value = self.set_api_key(request).and_then(value::to_value);
171
172 Box::pin(async move {
173 let url = url?;
174 let value = value?;
175
176 #[cfg(feature = "inspect-contents")]
177 debug!(
178 "sending request to {} with {} content: {}",
179 url, type_, value
180 );
181
182 let mut form = multipart::Form::default();
183
184 form.add_reader_file_with_mime("file", read, file_name, type_);
185
186 let obj = value.as_object().expect("Request must be an object");
187 for (k, v) in obj {
188 let v = v
189 .as_str()
190 .expect("UploadFileRequest must be an object that all values are string");
191 form.add_text(k.to_owned(), v.to_owned());
192 }
193
194 let content_type = form.content_type();
195
196 use futures::stream::TryStreamExt;
197 let stream = multipart::Body::from(form).map_err(|e| match e {
198 MultipartError::HeaderWrite(e) => e,
199 MultipartError::BoundaryWrite(e) => e,
200 MultipartError::ContentRead(e) => e,
201 });
202 let body = isahc::Body::from_reader(async_dup::Mutex::new(stream.into_async_read()));
203
204 use isahc::http::header::CONTENT_TYPE;
205 let response = self
206 .client
207 .send_async(
208 http::Request::post(url.into_string())
210 .header(CONTENT_TYPE, content_type)
211 .body(body)
212 .unwrap(),
213 )
214 .await?;
215
216 response_to_result::<R>(response).await
217 })
218 }
219}
220
221async fn response_to_result<R: Request>(
222 response: http::Response<isahc::Body>,
223) -> Result<ApiResult<R::Response>> {
224 let status = response.status();
225 let mut bytes = Vec::new();
226 response.into_body().read_to_end(&mut bytes).await?;
227
228 #[cfg(feature = "inspect-contents")]
229 debug!(
230 "got response ({}) from {}: {}",
231 status,
232 R::ENDPOINT,
233 String::from_utf8_lossy(&bytes)
234 );
235
236 let json_bytes = if bytes.is_empty() {
237 b"null".as_ref()
238 } else {
239 bytes.as_ref()
240 };
241
242 if status.is_success() {
243 Ok(ApiResult::Ok(serde_json::from_slice(json_bytes)?))
246 } else {
247 Ok(serde_json::from_slice(json_bytes)?)
248 }
249}
250
251#[cfg(test)]
252mod tests {
253 use super::HttpClient;
254
255 use misskey_core::{Client, UploadFileClient};
256 use misskey_test::{self, env};
257 use uuid::Uuid;
258
259 fn test_client() -> HttpClient {
260 misskey_test::init_logger();
261 HttpClient::with_token(env::api_url(), env::token()).unwrap()
262 }
263
264 #[test]
265 fn test_send() {
266 fn assert_send<T: Send>() {}
267 assert_send::<HttpClient>();
268 }
269
270 #[test]
271 fn test_sync() {
272 fn assert_send<T: Sync>() {}
273 assert_send::<HttpClient>();
274 }
275
276 #[tokio::test]
277 async fn test_url_without_trailing_slash() {
278 let mut url = env::api_url().to_string();
279 assert_eq!(url.pop(), Some('/'));
280 let client = HttpClient::with_token(url.parse().unwrap(), env::token()).unwrap();
281 client
282 .request(
283 misskey_api::endpoint::notes::create::Request::builder()
284 .text("hi")
285 .build(),
286 )
287 .await
288 .unwrap()
289 .unwrap();
290 }
291
292 #[tokio::test]
293 async fn tokio_request() {
294 let client = test_client();
295 client
296 .request(
297 misskey_api::endpoint::notes::create::Request::builder()
298 .text("hi")
299 .build(),
300 )
301 .await
302 .unwrap()
303 .unwrap();
304 }
305
306 #[async_std::test]
307 async fn async_std_request() {
308 let client = test_client();
309 client
310 .request(
311 misskey_api::endpoint::notes::create::Request::builder()
312 .text("hi")
313 .build(),
314 )
315 .await
316 .unwrap()
317 .unwrap();
318 }
319
320 fn write_to_temp_file(data: impl AsRef<[u8]>) -> std::path::PathBuf {
321 let tmp_name = Uuid::new_v4().to_simple().to_string();
322 let path = std::env::temp_dir().join(tmp_name);
323 {
324 use std::{fs::File, io::Write};
325 let mut file = File::create(&path).unwrap();
326 file.write_all(data.as_ref()).unwrap();
327 file.sync_all().unwrap();
328 }
329 path
330 }
331
332 #[tokio::test]
333 async fn tokio_request_with_file() {
334 let client = test_client();
335 let path = write_to_temp_file("test");
336 let file = std::fs::File::open(path).unwrap();
337
338 client
339 .request_with_file(
340 misskey_api::endpoint::drive::files::create::Request::default(),
341 mime::TEXT_PLAIN,
342 "test.txt".to_string(),
343 file,
344 )
345 .await
346 .unwrap()
347 .unwrap();
348 }
349
350 #[async_std::test]
351 async fn async_std_request_with_file() {
352 let client = test_client();
353 let path = write_to_temp_file("test");
354 let file = std::fs::File::open(path).unwrap();
355
356 client
357 .request_with_file(
358 misskey_api::endpoint::drive::files::create::Request::default(),
359 mime::TEXT_PLAIN,
360 "test.txt".to_string(),
361 file,
362 )
363 .await
364 .unwrap()
365 .unwrap();
366 }
367}