use std::convert::TryInto;
use std::fmt::{self, Debug};
use crate::error::{Error, Result};
use common_multipart_rfc7578::client::{multipart, Error as MultipartError};
use futures::future::BoxFuture;
use futures::io::AsyncReadExt;
use isahc::http;
#[cfg(feature = "inspect-contents")]
use log::debug;
use mime::Mime;
use misskey_core::model::ApiResult;
use misskey_core::{Client, Request, UploadFileClient, UploadFileRequest};
use serde::Serialize;
use serde_json::value::{self, Value};
use url::Url;
pub mod builder;
use builder::HttpClientBuilder;
pub struct HttpClient {
url: Url,
token: Option<String>,
client: isahc::HttpClient,
}
impl Debug for HttpClient {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("HttpClient")
.field("url", &self.url)
.finish()
}
}
impl HttpClient {
pub fn new(url: Url) -> Result<Self> {
Ok(HttpClient {
url,
token: None,
client: isahc::HttpClient::new()?,
})
}
pub fn with_token(url: Url, token: impl Into<String>) -> Result<Self> {
Ok(HttpClient {
url,
token: Some(token.into()),
client: isahc::HttpClient::new()?,
})
}
pub fn builder<T>(url: T) -> HttpClientBuilder
where
T: TryInto<Url>,
T::Error: Into<Error>,
{
HttpClientBuilder::new(url)
}
fn set_api_key<R: Request>(
&self,
request: R,
) -> std::result::Result<impl Serialize, serde_json::Error> {
#[derive(Serialize)]
#[serde(untagged)]
enum ValueOrRequest<R> {
Value(Value),
Request(R),
}
if let Some(token) = &self.token {
let mut value = value::to_value(request)?;
let obj = value.as_object_mut().expect("Request must be an object");
if obj
.insert("i".to_string(), Value::String(token.to_owned()))
.is_some()
{
panic!("Request must not have 'i' key");
}
Ok(ValueOrRequest::Value(value))
} else {
Ok(ValueOrRequest::Request(request))
}
}
fn make_url<R: Request>(&self) -> Result<Url> {
let mut url = self.url.clone();
if let Ok(mut segments) = url.path_segments_mut() {
segments.pop_if_empty();
for segment in R::ENDPOINT.split('/') {
segments.push(segment);
}
} else {
return self.url.join(R::ENDPOINT).map_err(Into::into);
}
Ok(url)
}
}
impl Client for HttpClient {
type Error = Error;
fn request<R: Request>(&self, request: R) -> BoxFuture<Result<ApiResult<R::Response>>> {
let url = self.make_url::<R>();
let body = self
.set_api_key(request)
.and_then(|b| serde_json::to_vec(&b));
Box::pin(async move {
let url = url?;
let body = body?;
#[cfg(feature = "inspect-contents")]
debug!(
"sending request to {}: {}",
url,
String::from_utf8_lossy(&body)
);
use isahc::http::header::CONTENT_TYPE;
let response = self
.client
.send_async(
http::Request::post(url.to_string())
.header(CONTENT_TYPE, "application/json")
.body(body)
.unwrap(),
)
.await?;
response_to_result::<R>(response).await
})
}
}
impl UploadFileClient for HttpClient {
fn request_with_file<R, T>(
&self,
request: R,
type_: Mime,
file_name: String,
read: T,
) -> BoxFuture<Result<ApiResult<R::Response>>>
where
R: UploadFileRequest,
T: std::io::Read + Send + Sync + 'static,
{
let url = self.make_url::<R>();
let value = self.set_api_key(request).and_then(value::to_value);
Box::pin(async move {
let url = url?;
let value = value?;
#[cfg(feature = "inspect-contents")]
debug!(
"sending request to {} with {} content: {}",
url, type_, value
);
let mut form = multipart::Form::default();
form.add_reader_file_with_mime("file", read, file_name, type_);
let obj = value.as_object().expect("Request must be an object");
for (k, v) in obj {
let v = v
.as_str()
.expect("UploadFileRequest must be an object that all values are string");
form.add_text(k.to_owned(), v.to_owned());
}
let content_type = form.content_type();
use futures::stream::TryStreamExt;
let stream = multipart::Body::from(form).map_err(|e| match e {
MultipartError::HeaderWrite(e) => e,
MultipartError::BoundaryWrite(e) => e,
MultipartError::ContentRead(e) => e,
});
let body = isahc::Body::from_reader(async_dup::Mutex::new(stream.into_async_read()));
use isahc::http::header::CONTENT_TYPE;
let response = self
.client
.send_async(
http::Request::post(url.into_string())
.header(CONTENT_TYPE, content_type)
.body(body)
.unwrap(),
)
.await?;
response_to_result::<R>(response).await
})
}
}
async fn response_to_result<R: Request>(
response: http::Response<isahc::Body>,
) -> Result<ApiResult<R::Response>> {
let status = response.status();
let mut bytes = Vec::new();
response.into_body().read_to_end(&mut bytes).await?;
#[cfg(feature = "inspect-contents")]
debug!(
"got response ({}) from {}: {}",
status,
R::ENDPOINT,
String::from_utf8_lossy(&bytes)
);
let json_bytes = if bytes.is_empty() {
b"null".as_ref()
} else {
bytes.as_ref()
};
if status.is_success() {
Ok(ApiResult::Ok(serde_json::from_slice(json_bytes)?))
} else {
Ok(serde_json::from_slice(json_bytes)?)
}
}
#[cfg(test)]
mod tests {
use super::HttpClient;
use misskey_core::{Client, UploadFileClient};
use misskey_test::{self, env};
use uuid::Uuid;
fn test_client() -> HttpClient {
misskey_test::init_logger();
HttpClient::with_token(env::api_url(), env::token()).unwrap()
}
#[test]
fn test_send() {
fn assert_send<T: Send>() {}
assert_send::<HttpClient>();
}
#[test]
fn test_sync() {
fn assert_send<T: Sync>() {}
assert_send::<HttpClient>();
}
#[tokio::test]
async fn test_url_without_trailing_slash() {
let mut url = env::api_url().to_string();
assert_eq!(url.pop(), Some('/'));
let client = HttpClient::with_token(url.parse().unwrap(), env::token()).unwrap();
client
.request(
misskey_api::endpoint::notes::create::Request::builder()
.text("hi")
.build(),
)
.await
.unwrap()
.unwrap();
}
#[tokio::test]
async fn tokio_request() {
let client = test_client();
client
.request(
misskey_api::endpoint::notes::create::Request::builder()
.text("hi")
.build(),
)
.await
.unwrap()
.unwrap();
}
#[async_std::test]
async fn async_std_request() {
let client = test_client();
client
.request(
misskey_api::endpoint::notes::create::Request::builder()
.text("hi")
.build(),
)
.await
.unwrap()
.unwrap();
}
fn write_to_temp_file(data: impl AsRef<[u8]>) -> std::path::PathBuf {
let tmp_name = Uuid::new_v4().to_simple().to_string();
let path = std::env::temp_dir().join(tmp_name);
{
use std::{fs::File, io::Write};
let mut file = File::create(&path).unwrap();
file.write_all(data.as_ref()).unwrap();
file.sync_all().unwrap();
}
path
}
#[tokio::test]
async fn tokio_request_with_file() {
let client = test_client();
let path = write_to_temp_file("test");
let file = std::fs::File::open(path).unwrap();
client
.request_with_file(
misskey_api::endpoint::drive::files::create::Request::default(),
mime::TEXT_PLAIN,
"test.txt".to_string(),
file,
)
.await
.unwrap()
.unwrap();
}
#[async_std::test]
async fn async_std_request_with_file() {
let client = test_client();
let path = write_to_temp_file("test");
let file = std::fs::File::open(path).unwrap();
client
.request_with_file(
misskey_api::endpoint::drive::files::create::Request::default(),
mime::TEXT_PLAIN,
"test.txt".to_string(),
file,
)
.await
.unwrap()
.unwrap();
}
}