From 7874c0e52a9401c96f78aae39f67ea23ba5082a7 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 27 Feb 2024 02:55:44 +0000 Subject: [PATCH] feat: add /notify endpoint Co-authored-by: bjoern --- src/main.rs | 6 ++--- src/notifier.rs | 5 ++-- src/server.rs | 72 +++++++++++++++++++++++++++++++++++++++++++++++++ src/state.rs | 14 +++++++++- 4 files changed, 90 insertions(+), 7 deletions(-) diff --git a/src/main.rs b/src/main.rs index 05de752..e6c062b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -37,16 +37,14 @@ async fn main() -> Result<()> { let opt = Opt::from_args(); let certificate = std::fs::File::open(&opt.certificate_file).context("invalid certificate")?; - let state = state::State::new(&opt.db, certificate, &opt.password)?; + let state = state::State::new(&opt.db, certificate, &opt.password, opt.topic.clone())?; let state2 = state.clone(); let host = opt.host.clone(); let port = opt.port; let server = async_std::task::spawn(async move { server::start(state2, host, port).await }); - let notif = async_std::task::spawn(async move { - notifier::start(state, opt.topic.as_deref(), opt.interval).await - }); + let notif = async_std::task::spawn(async move { notifier::start(state, opt.interval).await }); server.try_join(notif).await?; diff --git a/src/notifier.rs b/src/notifier.rs index 7af1dee..7a144c1 100644 --- a/src/notifier.rs +++ b/src/notifier.rs @@ -8,10 +8,11 @@ use log::*; use crate::state::State; -pub async fn start(state: State, topic: Option<&str>, interval: std::time::Duration) -> Result<()> { +pub async fn start(state: State, interval: std::time::Duration) -> Result<()> { let db = state.db(); let production_client = state.production_client(); let sandbox_client = state.sandbox_client(); + let topic = state.topic(); info!( "Waking up devices every {}", @@ -56,7 +57,7 @@ async fn wakeup( (production_client, device_token.as_str()) }; - // Sent silent notification. + // Send silent notification. // According to // to send a silent notification you need to set background notification flag `content-available` to 1 // and don't include `alert`, `badge` or `sound`. diff --git a/src/server.rs b/src/server.rs index d085433..7187fb2 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,3 +1,7 @@ +use a2::{ + DefaultNotificationBuilder, Error::ResponseError, NotificationBuilder, NotificationOptions, + Priority, +}; use anyhow::Result; use log::*; use serde::Deserialize; @@ -8,6 +12,7 @@ pub async fn start(state: State, server: String, port: u16) -> Result<()> { let mut app = tide::with_state(state); app.at("/").get(|_| async { Ok("Hello, world!") }); app.at("/register").post(register_device); + app.at("/notify").post(notify_device); info!("Listening on {server}:port"); app.listen((server, port)).await?; @@ -19,6 +24,7 @@ struct DeviceQuery { token: String, } +/// Registers a device for heartbeat notifications. async fn register_device(mut req: tide::Request) -> tide::Result { let query: DeviceQuery = req.body_json().await?; info!("register_device {}", query.token); @@ -29,3 +35,69 @@ async fn register_device(mut req: tide::Request) -> tide::Result) -> tide::Result { + let device_token = req.body_string().await?; + info!("Got direct notification for {device_token}."); + + let (client, device_token) = if let Some(sandbox_token) = device_token.strip_prefix("sandbox:") + { + (req.state().sandbox_client(), sandbox_token) + } else { + (req.state().production_client(), device_token.as_str()) + }; + + let db = req.state().db(); + let payload = DefaultNotificationBuilder::new() + .set_title("New messages") + .set_title_loc_key("new_messages") // Localization key for the title. + .set_body("You have new messages") + .set_loc_key("new_messages_body") // Localization key for the body. + .set_mutable_content() + .build( + device_token, + NotificationOptions { + // High priority (10). + // + apns_priority: Some(Priority::High), + apns_topic: req.state().topic(), + ..Default::default() + }, + ); + + match client.send(payload).await { + Ok(res) => { + match res.code { + 200 => { + info!("delivered notification for {}", device_token); + } + _ => { + warn!("unexpected status: {:?}", res); + } + } + + Ok(tide::Response::new(tide::StatusCode::Ok)) + } + Err(ResponseError(res)) => { + info!("Removing token {} due to error {:?}.", &device_token, res); + if res.code == 410 { + // 410 means that "The device token is no longer active for the topic." + // + // + // Unsubscribe invalid token from heartbeat notification if it is subscribed. + if let Err(err) = db.remove(device_token) { + error!("failed to remove {}: {:?}", &device_token, err); + } + // Return 410 Gone response so email server can remove the token. + Ok(tide::Response::new(tide::StatusCode::Gone)) + } else { + Ok(tide::Response::new(tide::StatusCode::InternalServerError)) + } + } + Err(err) => { + error!("failed to send notification: {}, {:?}", device_token, err); + Ok(tide::Response::new(tide::StatusCode::InternalServerError)) + } + } +} diff --git a/src/state.rs b/src/state.rs index 5868000..0a79615 100644 --- a/src/state.rs +++ b/src/state.rs @@ -18,10 +18,17 @@ pub struct InnerState { production_client: Client, sandbox_client: Client, + + topic: Option, } impl State { - pub fn new(db: &PathBuf, mut certificate: std::fs::File, password: &str) -> Result { + pub fn new( + db: &PathBuf, + mut certificate: std::fs::File, + password: &str, + topic: Option, + ) -> Result { let db = sled::open(db)?; let production_client = Client::certificate(&mut certificate, password, Endpoint::Production) @@ -37,6 +44,7 @@ impl State { db, production_client, sandbox_client, + topic, }), }) } @@ -52,4 +60,8 @@ impl State { pub fn sandbox_client(&self) -> &Client { &self.inner.sandbox_client } + + pub fn topic(&self) -> Option<&str> { + self.inner.topic.as_deref() + } }