Skip to content

Commit

Permalink
feat: add /notify endpoint
Browse files Browse the repository at this point in the history
Co-authored-by: bjoern <r10s@b44t.com>
  • Loading branch information
link2xt and r10s committed Mar 2, 2024
1 parent ab33fb7 commit 7874c0e
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 7 deletions.
6 changes: 2 additions & 4 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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?;

Expand Down
5 changes: 3 additions & 2 deletions src/notifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}",
Expand Down Expand Up @@ -56,7 +57,7 @@ async fn wakeup(
(production_client, device_token.as_str())
};

// Sent silent notification.
// Send silent notification.
// According to <https://developer.apple.com/documentation/usernotifications/generating-a-remote-notification>
// to send a silent notification you need to set background notification flag `content-available` to 1
// and don't include `alert`, `badge` or `sound`.
Expand Down
72 changes: 72 additions & 0 deletions src/server.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
use a2::{
DefaultNotificationBuilder, Error::ResponseError, NotificationBuilder, NotificationOptions,
Priority,
};
use anyhow::Result;
use log::*;
use serde::Deserialize;
Expand All @@ -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?;
Expand All @@ -19,6 +24,7 @@ struct DeviceQuery {
token: String,
}

/// Registers a device for heartbeat notifications.
async fn register_device(mut req: tide::Request<State>) -> tide::Result<tide::Response> {
let query: DeviceQuery = req.body_json().await?;
info!("register_device {}", query.token);
Expand All @@ -29,3 +35,69 @@ async fn register_device(mut req: tide::Request<State>) -> tide::Result<tide::Re

Ok(tide::Response::new(tide::StatusCode::Ok))
}

/// Notifies a single device with a visible notification.
async fn notify_device(mut req: tide::Request<State>) -> tide::Result<tide::Response> {
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).
// <https://developer.apple.com/documentation/usernotifications/sending-notification-requests-to-apns>
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."
// <https://developer.apple.com/documentation/usernotifications/handling-notification-responses-from-apns>
//
// 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))
}
}
}
14 changes: 13 additions & 1 deletion src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,17 @@ pub struct InnerState {
production_client: Client,

sandbox_client: Client,

topic: Option<String>,
}

impl State {
pub fn new(db: &PathBuf, mut certificate: std::fs::File, password: &str) -> Result<Self> {
pub fn new(
db: &PathBuf,
mut certificate: std::fs::File,
password: &str,
topic: Option<String>,
) -> Result<Self> {
let db = sled::open(db)?;
let production_client =
Client::certificate(&mut certificate, password, Endpoint::Production)
Expand All @@ -37,6 +44,7 @@ impl State {
db,
production_client,
sandbox_client,
topic,
}),
})
}
Expand All @@ -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()
}
}

0 comments on commit 7874c0e

Please sign in to comment.