#![recursion_limit = "1024"]
extern crate failure;
extern crate flate2;
extern crate reqwest;
#[macro_use]
extern crate serde_derive;
extern crate serde;
extern crate serde_json;
use reqwest::header::{
qitem, Accept, AcceptEncoding, ContentEncoding, ContentType, Encoding, Headers,
};
use std::str::FromStr;
use failure::Error;
use flate2::write::GzEncoder;
use flate2::Compression;
use serde_json::value::Value;
use std::io::prelude::*;
#[derive(Debug, Serialize, Clone)]
pub struct PushToken(String);
impl FromStr for PushToken {
type Err = String;
fn from_str(s: &str) -> Result<PushToken, Self::Err> {
if (s.starts_with("ExponentPushToken[") || s.starts_with("ExpoPushToken["))
&& s.ends_with("]")
{
Ok(PushToken(s.to_string()))
} else {
Err(format!("A PushToken must be of the format `ExpoPushToken[xxx]` or `ExponentPushToken[xxx]`. Was given: {}", s))
}
}
}
#[derive(Debug, Deserialize)]
struct PushResponse<T>
where
T: std::fmt::Debug,
{
data: Vec<PushReceipt<T>>,
}
#[derive(Debug, Deserialize, PartialEq)]
pub struct PushReceipt<T>
where
T: std::fmt::Debug,
{
pub status: String,
pub message: Option<String>,
pub details: Option<T>,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub enum Priority {
#[serde(rename = "default")]
Default,
#[serde(rename = "normal")]
Normal,
#[serde(rename = "high")]
High,
}
impl Default for Priority {
fn default() -> Self {
Priority::Default
}
}
impl FromStr for Priority {
type Err = serde_json::Error;
fn from_str(s: &str) -> Result<Priority, Self::Err> {
serde_json::from_str(s)
}
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub enum Sound {
#[serde(rename = "default")]
Default,
}
impl Default for Sound {
fn default() -> Self {
Sound::Default
}
}
impl FromStr for Sound {
type Err = serde_json::Error;
fn from_str(s: &str) -> Result<Sound, Self::Err> {
serde_json::from_str(s)
}
}
#[derive(Serialize, Clone)]
pub struct PushMessage {
pub to: PushToken,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub body: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sound: Option<Sound>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ttl: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expiration: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub priority: Option<Priority>,
#[serde(skip_serializing_if = "Option::is_none")]
pub badge: Option<u32>,
}
impl PushMessage {
pub fn new(push_token: PushToken) -> PushMessage {
PushMessage {
to: push_token,
data: None,
title: None,
body: None,
sound: None,
ttl: None,
expiration: None,
priority: None,
badge: None,
}
}
pub fn data(mut self, data: Value) -> Self {
self.data = Some(data);
self
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn body(mut self, body: impl Into<String>) -> Self {
self.body = Some(body.into());
self
}
pub fn sound(mut self, sound: Sound) -> Self {
self.sound = Some(sound);
self
}
pub fn ttl(mut self, ttl: u32) -> Self {
self.ttl = Some(ttl);
self
}
pub fn expiration(mut self, expiration: u32) -> Self {
self.expiration = Some(expiration);
self
}
pub fn priority(mut self, priority: Priority) -> Self {
self.priority = Some(priority);
self
}
pub fn badge(mut self, badge: u32) -> Self {
self.badge = Some(badge);
self
}
}
pub enum GzipPolicy {
ZipGreaterThan1024Bytes,
Never,
Always,
}
impl Default for GzipPolicy {
fn default() -> Self {
GzipPolicy::ZipGreaterThan1024Bytes
}
}
pub struct PushNotifier {
pub url: String,
pub pushes_per_request: usize,
pub gzip_policy: GzipPolicy,
client: reqwest::Client,
}
impl PushNotifier {
pub fn new() -> PushNotifier {
PushNotifier {
url: "https://exp.host/--/api/v2/push/send".to_string(),
pushes_per_request: 100,
gzip_policy: GzipPolicy::default(),
client: reqwest::Client::new(),
}
}
pub fn url(mut self, url: impl Into<String>) -> Self {
self.url = url.into();
self
}
pub fn with_pushes_per_request(mut self, pushes_per_request: usize) -> Self {
self.pushes_per_request = pushes_per_request;
self
}
pub fn gzip_policy(mut self, gzip_policy: GzipPolicy) -> Self {
self.gzip_policy = gzip_policy;
self
}
pub fn send_push_notifications(
&self,
messages: &[PushMessage],
) -> Result<Vec<PushReceipt<Value>>, Error> {
let iter = messages.chunks(self.pushes_per_request);
let mut responses: Vec<PushReceipt<Value>> = Vec::new();
for chunk in iter {
let mut response = self.send_push_notifications_chunk(&self.url, &chunk)?;
responses.append(&mut response);
}
Ok(responses)
}
pub fn send_push_notification(
&self,
message: &PushMessage,
) -> Result<PushReceipt<Value>, Error> {
let mut result = self.send_push_notifications_chunk(&self.url, &[message.clone()])?;
Ok(result.pop().unwrap())
}
fn send_push_notifications_chunk(
&self,
url: &str,
messages: &[PushMessage],
) -> Result<Vec<PushReceipt<Value>>, Error> {
let body = serde_json::to_string(&messages).unwrap();
let should_compress = match self.gzip_policy {
GzipPolicy::Always => true,
GzipPolicy::Never => false,
GzipPolicy::ZipGreaterThan1024Bytes => {
if body.len() > 1024 {
true
} else {
false
}
}
};
let mut res = self.request_async(url, &body, should_compress)?;
let res = res.json::<PushResponse<Value>>()?;
Ok(res.data)
}
fn request_async(
&self,
url: &str,
body: &str,
should_compress: bool,
) -> Result<reqwest::Response, Error> {
let mut headers = Headers::new();
headers.set(Accept::json());
headers.set(AcceptEncoding(vec![
qitem(Encoding::Gzip),
qitem(Encoding::Deflate),
]));
headers.set(ContentType::json());
if should_compress {
headers.set(ContentEncoding(vec![Encoding::Gzip]));
let gzip_body = self.gzip_request(body)?;
self.construct_body(url, headers, gzip_body)
} else {
self.construct_body(url, headers, body.to_owned())
}
}
fn construct_body<T: Into<reqwest::Body>>(
&self,
url: &str,
headers: Headers,
body: T,
) -> Result<reqwest::Response, Error> {
let response = self
.client
.post(url)
.headers(headers)
.body(body)
.send()?
.error_for_status()?;
Ok(response)
}
fn gzip_request(&self, body: &str) -> Result<Vec<u8>, Error> {
let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
encoder.write(body.as_bytes())?;
let gzip = encoder.finish()?;
Ok(gzip)
}
}