#![warn(missing_docs)]
#![doc(html_root_url = "https://docs.rs/pb-async/0.1.0/")]
extern crate bytes;
extern crate failure;
extern crate futures;
extern crate http;
extern crate hyper;
extern crate hyper_tls;
extern crate mpart_async;
extern crate serde;
extern crate serde_json;
#[macro_use]
extern crate log;
#[macro_use]
extern crate failure_derive;
#[macro_use]
extern crate serde_derive;
mod errors;
pub use errors::{RequestError, StartupError};
use futures::{Future, Stream};
use http::header::HeaderValue;
static API_ROOT: &str = "https://api.pushbullet.com/v2/";
static TOKEN_HEADER: &str = "Access-Token";
type HyperClient = hyper::Client<hyper_tls::HttpsConnector<hyper::client::HttpConnector>>;
pub struct Client {
token: HeaderValue,
client: HyperClient,
}
impl Client {
pub fn new(token: &str) -> Result<Self, StartupError> {
let mut connector = hyper_tls::HttpsConnector::new(1).map_err(StartupError::Tls)?;
connector.force_https(true);
Ok(Client {
token: HeaderValue::from_str(token)
.map_err(|e| StartupError::InvalidToken(e, token.to_owned()))?,
client: hyper::Client::builder().keep_alive(true).build(connector),
})
}
pub fn with_client(token: &str, client: HyperClient) -> Result<Self, StartupError> {
Ok(Client {
token: HeaderValue::from_str(token)
.map_err(|e| StartupError::InvalidToken(e, token.to_owned()))?,
client: client,
})
}
pub fn get_user(&self) -> impl Future<Item = User, Error = RequestError> {
self.get("users/me").and_then(|(bytes, data)| {
serde_json::from_value(data).map_err(|error| RequestError::Json { error, bytes })
})
}
pub fn list_devices(&self) -> impl Future<Item = Vec<Device>, Error = RequestError> {
#[derive(Deserialize)]
struct Devices {
devices: Vec<Device>,
}
self.get("devices").and_then(|(bytes, data)| {
let d: Devices = serde_json::from_value(data).map_err(|error| RequestError::Json {
error,
bytes: bytes.clone(),
})?;
Ok(d.devices)
})
}
pub fn push(
&self,
target: PushTarget,
data: PushData,
) -> impl Future<Item = (), Error = RequestError> {
#[derive(Serialize)]
struct Push<'a> {
#[serde(flatten)]
data: PushData<'a>,
#[serde(flatten)]
target: PushTarget<'a>,
}
let post_data = serde_json::to_string(&Push { target, data }).unwrap();
self.post("pushes", post_data.into()).map(|_resp| ())
}
pub fn upload_request(
&self,
file_name: &str,
file_type: &str,
upload_data: hyper::Body,
) -> impl Future<Item = UploadRequestResponse, Error = RequestError> {
#[derive(Serialize)]
struct Upload<'a> {
file_name: &'a str,
file_type: &'a str,
}
let post_data = serde_json::to_string(&Upload {
file_name,
file_type,
}).unwrap();
let token_for_later_use = self.token.clone();
let client_for_later_use = self.client.clone();
self.post("upload-request", post_data.into())
.and_then(move |(bytes, data)| {
use http::header::*;
let RawUploadRequestResponse {
file_name,
file_type,
file_url,
upload_url,
} = serde_json::from_value(data)
.map_err(|error| RequestError::Json { error, bytes })?;
let mut mpart = mpart_async::MultipartRequest::default();
mpart.add_stream(
"file",
&*file_name,
&*file_type,
upload_data.map(|chunk| chunk.into_bytes()),
);
let request = hyper::Request::post(upload_url)
.header(TOKEN_HEADER, token_for_later_use)
.header(
CONTENT_TYPE,
&*format!("multipart/form-data; boundary={}", mpart.get_boundary()),
)
.body(hyper::Body::wrap_stream(mpart))?;
Ok((
request,
UploadRequestResponse {
file_name,
file_type,
file_url,
_priv: (),
},
))
})
.and_then(move |(request, last_response)| {
client_for_later_use
.request(request)
.and_then(|response| {
let (parts, body) = response.into_parts();
body.concat2().map(|body| (parts, body))
})
.from_err()
.and_then(|(parts, body)| {
let bytes = body.into_bytes();
if !parts.status.is_success() {
return Err(RequestError::Status {
status: parts.status,
bytes: bytes,
});
}
Ok(last_response)
})
})
}
fn get(
&self,
target: &'static str,
) -> impl Future<Item = (bytes::Bytes, serde_json::Value), Error = RequestError> {
self.request(target, hyper::Body::empty(), http::Method::GET, |b| b)
}
fn post(
&self,
target: &'static str,
body: hyper::Body,
) -> impl Future<Item = (bytes::Bytes, serde_json::Value), Error = RequestError> {
use hyper::body::Payload;
let length = body.content_length()
.expect("expected unconditional content length");
self.request(target, body, http::Method::POST, move |b| {
b.header(http::header::CONTENT_TYPE, "application/json")
.header(http::header::CONTENT_LENGTH, &*format!("{}", length))
})
}
fn request(
&self,
target: &'static str,
body: hyper::Body,
method: http::Method,
extra: impl FnOnce(&mut http::request::Builder) -> &mut http::request::Builder,
) -> impl Future<Item = (bytes::Bytes, serde_json::Value), Error = RequestError> {
let request = extra(
hyper::Request::builder()
.method(method)
.uri(format!("{}{}", API_ROOT, target))
.header(TOKEN_HEADER, self.token.clone()),
).body(body)
.expect("expected request to be well-formed");
debug!("sending request: {:?}", request);
self.client
.request(request)
.and_then(|response| {
let (parts, body) = response.into_parts();
body.concat2().map(|body| (parts, body))
})
.from_err()
.and_then(move |(parts, body)| {
let bytes = body.into_bytes();
let data: serde_json::Value =
serde_json::from_slice(&*bytes).map_err(|error| RequestError::Json {
error,
bytes: bytes.clone(),
})?;
debug!("received json: {:?} from {}", data, target);
if let Some(err_data) = data.as_object().and_then(|obj| obj.get("error")) {
#[derive(Deserialize)]
struct ErrorData {
code: String,
message: String,
}
if let Ok(ErrorData { code, message }) =
serde::Deserialize::deserialize(err_data)
{
return Err(RequestError::Server { code, message });
}
}
if !parts.status.is_success() {
return Err(RequestError::Status {
status: parts.status,
bytes: bytes,
});
}
Ok((bytes, data))
})
}
}
#[derive(Serialize, Copy, Clone, Debug)]
#[serde(untagged)]
pub enum PushTarget<'a> {
SelfUser {},
Device {
#[serde(rename = "device_iden")]
iden: &'a str,
},
User {
email: &'a str,
},
Channel {
#[serde(rename = "channel_tag")]
tag: &'a str,
},
Client {
#[serde(rename = "client_iden")]
iden: &'a str,
},
}
#[derive(Serialize, Copy, Clone, Debug)]
#[serde(tag = "type")]
#[serde(rename_all = "snake_case")]
pub enum PushData<'a> {
Note {
title: &'a str,
body: &'a str,
},
Link {
title: &'a str,
body: &'a str,
url: &'a str,
},
File {
body: &'a str,
file_name: &'a str,
file_type: &'a str,
file_url: &'a str,
},
}
#[derive(Clone, Debug, Deserialize)]
pub struct User {
pub created: f64,
pub email: String,
pub email_normalized: String,
pub iden: String,
pub image_url: Option<String>,
pub max_upload_size: f64,
pub modified: f64,
pub name: String,
#[serde(default)]
_priv: (),
}
#[derive(Clone, Debug, Deserialize)]
pub struct Device {
pub active: bool,
pub created: f64,
pub iden: String,
pub modified: f64,
pub nickname: Option<String>,
#[serde(default)]
_priv: (),
}
#[derive(Clone, Debug, Deserialize)]
struct RawUploadRequestResponse {
file_name: String,
file_type: String,
file_url: String,
upload_url: String,
}
#[derive(Clone, Debug)]
pub struct UploadRequestResponse {
pub file_name: String,
pub file_type: String,
pub file_url: String,
_priv: (),
}