From 414d39c83592683bba5b5033cf29cd441e602cfe Mon Sep 17 00:00:00 2001 From: Julius Joska Date: Tue, 29 Sep 2026 18:30:11 +0200 Subject: [PATCH] fix(relay): let APNs hold wake-ups for devices that are offline The APNs wake-up was sent with apns-expiration 0, so a phone without connectivity at that moment (tunnel, flight) never received it and did not learn about the message until someone opened the app. APNs now keeps the wake-up for as long as the envelope stays in the relay queue. Priority stays at 5: APNs rejects priority 10 for background pushes. Immediate delivery on iOS needs a visible alert push handled by a Notification Service Extension, which is an app change. --- rust/sepot-relay/src/apns.rs | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/rust/sepot-relay/src/apns.rs b/rust/sepot-relay/src/apns.rs index 413363c..c48c0a0 100644 --- a/rust/sepot-relay/src/apns.rs +++ b/rust/sepot-relay/src/apns.rs @@ -18,7 +18,7 @@ use std::sync::Arc; -use chrono::Utc; +use chrono::{DateTime, Utc}; use jsonwebtoken::{Algorithm, EncodingKey, Header}; use serde::Serialize; use tokio::sync::Mutex; @@ -26,6 +26,15 @@ use tokio::sync::Mutex; const PROD_HOST: &str = "https://api.push.apple.com"; const SANDBOX_HOST: &str = "https://api.sandbox.push.apple.com"; +/// Do kdy má APNs budíček držet, když telefon zrovna není v síti (tunel, +/// letadlo). S `apns-expiration: 0` ho APNs zkusí jen jednou a zahodí — +/// telefon po návratu do sítě pak o zprávě neví, dokud appku někdo neotevře. +/// Déle než obálka ve frontě relaye nemá budíček smysl; APNs sám drží +/// nejvýš 30 dní. +fn wake_expiration(now: DateTime) -> i64 { + (now + chrono::Duration::days(crate::state::QUEUE_TTL_DAYS)).timestamp() +} + #[derive(Serialize)] struct Claims<'a> { iss: &'a str, @@ -108,6 +117,12 @@ impl ApnsSender { /// (nikdy token). `apns-push-type: background` + `content-available:1` /// = probuzení bez viditelné notifikace (tu si klient případně ukáže /// lokálně sám, bez obsahu). + /// + /// Priorita musí zůstat 5: u typu `background` APNs prioritu 10 odmítá + /// jako chybu a iOS takové budíčky doručuje podle stavu baterie. Okamžité + /// doručení umí jen viditelná notifikace (`alert` + `mutable-content`), + /// kterou appka v Notification Service Extension nahradí — to je změna + /// appky, ne jen relaye. pub async fn send_wake(&self, client: &reqwest::Client, device_token: &str) { let Some(jwt) = self.auth_token().await else { return; @@ -119,7 +134,7 @@ impl ApnsSender { .header("apns-topic", &self.topic) .header("apns-push-type", "background") .header("apns-priority", "5") - .header("apns-expiration", "0") + .header("apns-expiration", wake_expiration(Utc::now()).to_string()) .body(r#"{"aps":{"content-available":1}}"#) .send() .await; @@ -134,3 +149,18 @@ impl ApnsSender { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn budicek_drzi_apns_stejne_dlouho_jako_fronta() { + let ted = DateTime::parse_from_rfc3339("2026-09-29T18:00:00Z") + .unwrap() + .with_timezone(&Utc); + let limit = wake_expiration(ted) - ted.timestamp(); + assert_eq!(limit, crate::state::QUEUE_TTL_DAYS * 86_400); + assert!(limit <= 30 * 86_400, "APNs nedrží notifikace déle než 30 dní"); + } +}