diff --git a/docs/config.md b/docs/config.md index 1893f491..8aaff790 100644 --- a/docs/config.md +++ b/docs/config.md @@ -789,7 +789,7 @@ Note that the self-hosted server literally sends the message `New message` for e may be `Some other message`. This is so that if iOS cannot talk to the self-hosted server (in time, or at all), it'll show `New message` as a popup. -## Web Push notifications +## Web Push [Web Push](https://developer.mozilla.org/en-US/docs/Web/API/Push_API) ([RFC8030](https://datatracker.ietf.org/doc/html/rfc8030)) allows ntfy to receive push notifications, even when the ntfy web app (or even the browser, depending on the platform) is closed. When enabled, the user can enable **background notifications** for their topics in the wep app under Settings. Once enabled by the @@ -816,7 +816,8 @@ To configure VAPID keys, first generate them: ```sh $ ntfy webpush keys -Web Push keys generated. +Web Push keys generated. +... ``` Then copy the generated values into your `server.yml` or use the corresponding environment variables or command line arguments: @@ -828,8 +829,9 @@ web-push-subscriptions-file: /var/cache/ntfy/webpush.db web-push-email-address: sysadmin@example.com ``` -The `web-push-subscriptions-file` is used to store the push subscriptions. Subscriptions do not ever expire automatically, unless the push -gateway returns an error (e.g. 410 Gone when a user has unsubscribed). +The `web-push-subscriptions-file` is used to store the push subscriptions. Unused subscriptions will send out a warning after 7 days, +and will automatically expire after 9 days (not configurable). If the gateway returns an error (e.g. 410 Gone when a user has unsubscribed), +subscriptions are also removed automatically. The web app refreshes subscriptions on start and regularly on an interval, but this file should be persisted across restarts. If the subscription file is deleted or lost, any web apps that aren't open will not receive new web push notifications until you open then. @@ -1333,8 +1335,8 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`). | `stripe-secret-key` | `NTFY_STRIPE_SECRET_KEY` | *string* | - | Payments: Key used for the Stripe API communication, this enables payments | | `stripe-webhook-key` | `NTFY_STRIPE_WEBHOOK_KEY` | *string* | - | Payments: Key required to validate the authenticity of incoming webhooks from Stripe | | `billing-contact` | `NTFY_BILLING_CONTACT` | *email address* or *website* | - | Payments: Email or website displayed in Upgrade dialog as a billing contact | -| `web-push-public-key` | `NTFY_WEB_PUSH_PUBLIC_KEY` | *string* | - | Web Push: Public Key. Run `ntfy webpush generate-keys` to generate | -| `web-push-private-key` | `NTFY_WEB_PUSH_PRIVATE_KEY` | *string* | - | Web Push: Private Key. Run `ntfy webpush generate-keys` to generate | +| `web-push-public-key` | `NTFY_WEB_PUSH_PUBLIC_KEY` | *string* | - | Web Push: Public Key. Run `ntfy webpush keys` to generate | +| `web-push-private-key` | `NTFY_WEB_PUSH_PRIVATE_KEY` | *string* | - | Web Push: Private Key. Run `ntfy webpush keys` to generate | | `web-push-subscriptions-file` | `NTFY_WEB_PUSH_SUBSCRIPTIONS_FILE` | *string* | - | Web Push: Subscriptions file | | `web-push-email-address` | `NTFY_WEB_PUSH_EMAIL_ADDRESS` | *string* | - | Web Push: Sender email address | diff --git a/docs/subscribe/desktop.md b/docs/subscribe/desktop.md index fd9a9297..8d97571c 100644 --- a/docs/subscribe/desktop.md +++ b/docs/subscribe/desktop.md @@ -1,8 +1,7 @@ # Using the web app as an installed web app - While ntfy doesn't have a native desktop app, it is built as a [progressive web app](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps) (PWA) -and thus can be installed on both desktop and mobile. This gives it its own launcher (e.g. shortcut on Windows, app on -macOS, launcher shortcut on Linux) own window, push notifications, and an app badge with the unread notification count. +and thus can be installed on both desktop and mobile devices. This gives it its own launcher (e.g. shortcut on Windows, app on +macOS, launcher shortcut on Linux), own window, push notifications, and an app badge with the unread notification count. To install and register the web app in your operating system, click the "install app" icon in your browser (usually next to the address bar). To receive background notifications, **either the browser or the installed web app must be open**. diff --git a/server/server.yml b/server/server.yml index 37efb74a..48577cd2 100644 --- a/server/server.yml +++ b/server/server.yml @@ -146,8 +146,8 @@ # Web Push support (background notifications for browsers) # -# If enabled, allows ntfy to receive push notifications, even when the ntfy web app is closed. When enabled, the user -# can enable background notifications. Once enabled by the user, ntfy will forward published messages to the push +# If enabled, allows ntfy to receive push notifications, even when the ntfy web app is closed. When enabled, users +# can enable background notifications in the web app. Once enabled, ntfy will forward published messages to the push # endpoint, which will then forward it to the browser. # # You must configure all settings below to enable Web Push. diff --git a/server/server_account_test.go b/server/server_account_test.go index 119efb16..bf9b84db 100644 --- a/server/server_account_test.go +++ b/server/server_account_test.go @@ -431,6 +431,41 @@ func TestAccount_Delete_Not_Allowed(t *testing.T) { require.Equal(t, 40026, toHTTPError(t, rr.Body.String()).Code) } +func TestAccount_Delete_Success_WithWebPush(t *testing.T) { + conf := configureAuth(t, newTestConfigWithWebPush(t)) + conf.EnableSignup = true + s := newTestServer(t, conf) + + // Add account + rr := request(t, s, "POST", "/v1/account", `{"username":"phil", "password":"mypass"}`, nil) + require.Equal(t, 200, rr.Code) + + // Add web push subscription + rr = request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{"mytopic"}, testWebPushEndpoint), map[string]string{ + "Authorization": util.BasicAuth("phil", "mypass"), + }) + require.Equal(t, 200, rr.Code) + + u, err := s.userManager.User("phil") + require.Nil(t, err) + + subs, err := s.webPush.SubscriptionsForTopic("mytopic") + require.Nil(t, err) + require.Len(t, subs, 1) + require.Equal(t, u.ID, subs[0].UserID) + + // Delete account + rr = request(t, s, "DELETE", "/v1/account", `{"password":"mypass"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "mypass"), + }) + require.Equal(t, 200, rr.Code) + + // Make sure web push subscription was deleted + subs, err = s.webPush.SubscriptionsForTopic("mytopic") + require.Nil(t, err) + require.Len(t, subs, 0) +} + func TestAccount_Reservation_AddWithoutTierFails(t *testing.T) { conf := newTestConfigWithAuthFile(t) conf.EnableSignup = true diff --git a/server/server_webpush.go b/server/server_webpush.go index 209cb2d7..bb0f5408 100644 --- a/server/server_webpush.go +++ b/server/server_webpush.go @@ -120,7 +120,6 @@ func (s *Server) pruneAndNotifyWebPushSubscriptionsInternal() error { } payload, err := json.Marshal(newWebPushSubscriptionExpiringPayload()) if err != nil { - log.Tag(tagWebPush).Err(err).Warn("Unable to marshal expiring payload") return err } warningSent := make([]*webPushSubscription, 0) @@ -140,7 +139,14 @@ func (s *Server) pruneAndNotifyWebPushSubscriptionsInternal() error { func (s *Server) sendWebPushNotification(sub *webPushSubscription, message []byte, contexters ...log.Contexter) error { log.Tag(tagWebPush).With(sub).With(contexters...).Debug("Sending web push message") - resp, err := webpush.SendNotification(message, sub.ToSubscription(), &webpush.Options{ + payload := &webpush.Subscription{ + Endpoint: sub.Endpoint, + Keys: webpush.Keys{ + Auth: sub.Auth, + P256dh: sub.P256dh, + }, + } + resp, err := webpush.SendNotification(message, payload, &webpush.Options{ Subscriber: s.config.WebPushEmailAddress, VAPIDPublicKey: s.config.WebPushPublicKey, VAPIDPrivateKey: s.config.WebPushPrivateKey, diff --git a/server/types.go b/server/types.go index c2ac7bd4..d6a15cea 100644 --- a/server/types.go +++ b/server/types.go @@ -1,7 +1,6 @@ package server import ( - "github.com/SherClockHolmes/webpush-go" "net/http" "net/netip" "time" @@ -512,16 +511,6 @@ type webPushSubscription struct { UserID string } -func (w *webPushSubscription) ToSubscription() *webpush.Subscription { - return &webpush.Subscription{ - Endpoint: w.Endpoint, - Keys: webpush.Keys{ - Auth: w.Auth, - P256dh: w.P256dh, - }, - } -} - func (w *webPushSubscription) Context() log.Context { return map[string]any{ "web_push_subscription_id": w.ID, diff --git a/server/webpush_store.go b/server/webpush_store.go index 42a2ee67..ad3e8588 100644 --- a/server/webpush_store.go +++ b/server/webpush_store.go @@ -63,8 +63,12 @@ const ( WHERE st.topic = ? ORDER BY endpoint ` - selectWebPushSubscriptionsExpiringSoonQuery = `SELECT id, endpoint, key_auth, key_p256dh, user_id FROM subscription WHERE warned_at = 0 AND updated_at <= ?` - insertWebPushSubscriptionQuery = ` + selectWebPushSubscriptionsExpiringSoonQuery = ` + SELECT id, endpoint, key_auth, key_p256dh, user_id + FROM subscription + WHERE warned_at = 0 AND updated_at <= ? + ` + insertWebPushSubscriptionQuery = ` INSERT INTO subscription (id, endpoint, key_auth, key_p256dh, user_id, subscriber_ip, updated_at, warned_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT (endpoint)