summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKit La Touche <kit@transneptune.net>2025-07-30 23:08:40 -0400
committerKit La Touche <kit@transneptune.net>2025-07-30 23:08:40 -0400
commited5e175a806f45469a6e5504ba0d3f5246997fad (patch)
tree0d4233c57596186b86d165640ca4721e7495567d
parentb63380b251d04dd92f06aa5bbc22a72ca3e4bf8e (diff)
Test receiving push events when backgrounded
And thus also displaying notifications.
-rw-r--r--src/push/app.rs40
-rw-r--r--src/push/handlers/echo.rs18
-rw-r--r--src/push/handlers/mod.rs1
-rw-r--r--src/push/handlers/unregister.rs15
-rw-r--r--src/push/repo.rs33
-rw-r--r--src/routes.rs6
-rw-r--r--ui/routes/+layout.svelte26
-rw-r--r--ui/service-worker.js10
8 files changed, 110 insertions, 39 deletions
diff --git a/src/push/app.rs b/src/push/app.rs
index 2d6e15c..ed8bf31 100644
--- a/src/push/app.rs
+++ b/src/push/app.rs
@@ -46,18 +46,42 @@ impl<'a> Push<'a> {
Ok(id)
}
+ pub async fn broadcast(
+ &self,
+ message: &str,
+ ) -> Result<(), EchoError> {
+ let mut tx = self.db.begin().await?;
+ let subscriptions = tx
+ .subscriptions()
+ .all()
+ .await?;
+
+ tx.commit().await?;
+
+ for subscription in subscriptions {
+ // We don't care if any of these error, for now.
+ // Eventually, we should remove rows that cause certain error conditions.
+ println!("Sending to {:#?}", subscription.info.endpoint);
+ self.send(&subscription.info, message).await.unwrap_or_else(|err| {
+ println!("Error with {:#?}: {}", subscription.info.endpoint, err);
+ })
+ }
+
+ Ok(())
+ }
+
pub async fn echo(
&self,
user: &User,
- subscription: &Id,
+ endpoint: &String,
message: &str,
) -> Result<(), EchoError> {
let mut tx = self.db.begin().await?;
let subscription = tx
.subscriptions()
- .by_id(subscription)
+ .by_endpoint(endpoint)
.await
- .not_found(|| EchoError::NotFound(subscription.clone()))?;
+ .not_found(|| EchoError::NotFound(endpoint.clone()))?;
if subscription.user != user.id {
return Err(EchoError::NotSubscriber(subscription.id, user.id.clone()));
}
@@ -89,13 +113,13 @@ impl<'a> Push<'a> {
Ok(())
}
- pub async fn unregister(&self, user: &User, subscription: &Id) -> Result<(), UnregisterError> {
+ pub async fn unregister(&self, user: &User, endpoint: &String) -> Result<(), UnregisterError> {
let mut tx = self.db.begin().await?;
let subscription = tx
.subscriptions()
- .by_id(subscription)
+ .by_endpoint(endpoint)
.await
- .not_found(|| UnregisterError::NotFound(subscription.clone()))?;
+ .not_found(|| UnregisterError::NotFound(endpoint.clone()))?;
if subscription.user != user.id {
return Err(UnregisterError::NotSubscriber(
subscription.id,
@@ -118,7 +142,7 @@ pub enum RegisterError {
#[derive(Debug, thiserror::Error)]
pub enum EchoError {
#[error("subscription {0} not found")]
- NotFound(Id),
+ NotFound(String),
#[error("user {1} is not the subscriber for subscription {0}")]
NotSubscriber(Id, user::Id),
#[error(transparent)]
@@ -130,7 +154,7 @@ pub enum EchoError {
#[derive(Debug, thiserror::Error)]
pub enum UnregisterError {
#[error("subscription {0} not found")]
- NotFound(Id),
+ NotFound(String),
#[error("user {1} is not the subscriber for subscription {0}")]
NotSubscriber(Id, user::Id),
#[error(transparent)]
diff --git a/src/push/handlers/echo.rs b/src/push/handlers/echo.rs
index 4b4de57..a6c7be2 100644
--- a/src/push/handlers/echo.rs
+++ b/src/push/handlers/echo.rs
@@ -1,10 +1,10 @@
use axum::extract::{Json, State};
-use crate::{app::App, push::Id, token::extract::Identity};
+use crate::{app::App, token::extract::Identity};
#[derive(serde::Deserialize)]
pub struct Request {
- subscription: Id,
+ endpoint: String,
msg: String,
}
@@ -13,8 +13,18 @@ pub async fn handler(
identity: Identity,
Json(request): Json<Request>,
) -> Result<(), crate::error::Internal> {
- let Request { subscription, msg } = request;
- app.push().echo(&identity.user, &subscription, &msg).await?;
+ let Request { endpoint, msg } = request;
+ app.push().echo(&identity.user, &endpoint, &msg).await?;
+
+ Ok(())
+}
+
+pub async fn broadcast(
+ State(app): State<App>,
+ Json(request): Json<Request>,
+) -> Result<(), crate::error::Internal> {
+ let Request { endpoint: _, msg } = request;
+ app.push().broadcast(&msg).await?;
Ok(())
}
diff --git a/src/push/handlers/mod.rs b/src/push/handlers/mod.rs
index 90edaa7..4b27ff5 100644
--- a/src/push/handlers/mod.rs
+++ b/src/push/handlers/mod.rs
@@ -7,6 +7,7 @@ mod register;
mod unregister;
pub use echo::handler as echo;
+pub use echo::broadcast as broadcast;
pub use register::handler as register;
pub use unregister::handler as unregister;
diff --git a/src/push/handlers/unregister.rs b/src/push/handlers/unregister.rs
index a00ee92..b35dbd5 100644
--- a/src/push/handlers/unregister.rs
+++ b/src/push/handlers/unregister.rs
@@ -1,16 +1,23 @@
use axum::{
- extract::{Path, State},
+ extract::{Json, State},
http::StatusCode,
};
-use crate::{app::App, error::Internal, push::Id, token::extract::Identity};
+use crate::{app::App, error::Internal, token::extract::Identity};
+
+
+#[derive(serde::Deserialize)]
+pub struct Request {
+ endpoint: String,
+}
pub async fn handler(
State(app): State<App>,
identity: Identity,
- Path(subscription): Path<Id>,
+ Json(request): Json<Request>,
) -> Result<StatusCode, Internal> {
- app.push().unregister(&identity.user, &subscription).await?;
+ let Request { endpoint } = request;
+ app.push().unregister(&identity.user, &endpoint).await?;
Ok(StatusCode::NO_CONTENT)
}
diff --git a/src/push/repo.rs b/src/push/repo.rs
index ddef706..56bbc3d 100644
--- a/src/push/repo.rs
+++ b/src/push/repo.rs
@@ -41,7 +41,30 @@ impl Subscriptions<'_> {
Ok(id)
}
- pub async fn by_id(&mut self, id: &Id) -> Result<Subscription, sqlx::Error> {
+ pub async fn all(&mut self) -> Result<Vec<Subscription>, sqlx::Error> {
+ let subscriptions = sqlx::query!(
+ r#"
+ select
+ id as "id: Id",
+ user as "user: user::Id",
+ endpoint,
+ key_p256dh,
+ key_auth
+ from subscription
+ "#
+ )
+ .map(|row| Subscription {
+ id: row.id,
+ user: row.user,
+ info: SubscriptionInfo::new(row.endpoint, row.key_p256dh, row.key_auth),
+ })
+ .fetch_all(&mut *self.0)
+ .await?;
+
+ Ok(subscriptions)
+ }
+
+ pub async fn by_endpoint(&mut self, endpoint: &String) -> Result<Subscription, sqlx::Error> {
let subscription = sqlx::query!(
r#"
select
@@ -51,9 +74,9 @@ impl Subscriptions<'_> {
key_p256dh,
key_auth
from subscription
- where id = $1
+ where endpoint = $1
"#,
- id,
+ endpoint,
)
.map(|row| Subscription {
id: row.id,
@@ -70,9 +93,9 @@ impl Subscriptions<'_> {
sqlx::query!(
r#"
delete from subscription
- where id = $1
+ where endpoint = $1
"#,
- subscription.id,
+ subscription.info.endpoint,
)
.execute(&mut *self.0)
.await?;
diff --git a/src/routes.rs b/src/routes.rs
index ce60835..3e2fc04 100644
--- a/src/routes.rs
+++ b/src/routes.rs
@@ -27,10 +27,8 @@ pub fn routes(app: &App) -> Router<App> {
.route("/api/setup", post(setup::handlers::setup))
.route("/api/vapid", get(push::handlers::vapid))
.route("/api/push", post(push::handlers::register))
- .route(
- "/api/push/:subscription",
- delete(push::handlers::unregister),
- )
+ .route("/api/push", delete(push::handlers::unregister))
+ .route("/api/broadcast", post(push::handlers::broadcast))
.route("/api/echo", post(push::handlers::echo));
// API routes that require the administrator to complete setup first.
diff --git a/ui/routes/+layout.svelte b/ui/routes/+layout.svelte
index 9089c7e..841d597 100644
--- a/ui/routes/+layout.svelte
+++ b/ui/routes/+layout.svelte
@@ -10,7 +10,7 @@
function doSubscribe() {
navigator.serviceWorker.ready
- .then(async(registration) => {
+ .then(async (registration) => {
const response = await fetch("/api/vapid");
// and if we fail to get it?
const vapidPublicKey = await response.text();
@@ -20,8 +20,13 @@
applicationServerKey: convertedVapidKey
});
}).then((subscription) => {
- subscriptionJson = JSON.parse(JSON.stringify(subscription));
- return fetch("/api/register", {
+ const subJson = subscription.toJSON();
+ subscriptionJson = {
+ endpoint: subJson.endpoint,
+ p256dh: subJson.keys.p256dh,
+ auth: subJson.keys.auth,
+ };
+ return fetch("/api/push", {
method: "post",
headers: { "Content-type": "application/json" },
body: JSON.stringify(subscriptionJson),
@@ -34,14 +39,15 @@
.then((registration) => {
return registration.pushManager.getSubscription();
}).then((subscription) => {
+ const { endpoint } = subscription.toJSON();
return subscription.unsubscribe()
- .then(function() {
- subscriptionJson = null;
- return fetch("/api/unregister", {
- method: "post",
+ .then(() => {
+ fetch("/api/push", {
+ method: "delete",
headers: { "Content-type": "application/json" },
- body: JSON.stringify({ subscription })
+ body: JSON.stringify({ endpoint }),
});
+ subscriptionJson = null;
});
});
}
@@ -59,8 +65,8 @@
method: "post",
headers: { "Content-type": "application/json" },
body: JSON.stringify({
- ...subscriptionJson,
- msg: "oople doople",
+ endpoint: subscriptionJson.endpoint,
+ msg: JSON.stringify({"msg": "oople doople"}),
})
});
}
diff --git a/ui/service-worker.js b/ui/service-worker.js
index 319b251..a88c5a2 100644
--- a/ui/service-worker.js
+++ b/ui/service-worker.js
@@ -54,10 +54,12 @@ self.addEventListener('fetch', (event) => {
});
self.addEventListener('push', (event) => {
- const payload = event.data?.text() ?? "no payload";
+ // If the data isn't json, this dies hard:
+ const payload = event.data?.json() ?? null;
event.waitUntil(
- self.registration.showNotification("ServiceWorker Cookbook", {
- body: payload,
- }),
+ // How do we control the action you get when you click the notification?
+ self.registration.showNotification(payload.title, {
+ body: payload.body
+ })
);
});