mirror of
https://github.com/binwiederhier/ntfy.git
synced 2024-12-22 09:42:31 +01:00
Implement push subscription expiry
This commit is contained in:
parent
47ad024ec7
commit
0f0074cbab
16 changed files with 272 additions and 102 deletions
|
@ -99,6 +99,8 @@ var flagsServe = append(
|
|||
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-private-key", Aliases: []string{"web_push_private_key"}, EnvVars: []string{"NTFY_WEB_PUSH_PRIVATE_KEY"}, Usage: "private key used for web push notifications"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-subscriptions-file", Aliases: []string{"web_push_subscriptions_file"}, EnvVars: []string{"NTFY_WEB_PUSH_SUBSCRIPTIONS_FILE"}, Usage: "file used to store web push subscriptions"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-email-address", Aliases: []string{"web_push_email_address"}, EnvVars: []string{"NTFY_WEB_PUSH_EMAIL_ADDRESS"}, Usage: "e-mail address of sender, required to use browser push services"}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "web-push-expiry-warning-duration", Aliases: []string{"web_push_expiry_warning_duration"}, EnvVars: []string{"NTFY_WEB_PUSH_EXPIRY_WARNING_DURATION"}, Value: server.DefaultWebPushExpiryWarningDuration, Usage: "duration after last update to send a warning notification"}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "web-push-expiry-duration", Aliases: []string{"web_push_expiry_duration"}, EnvVars: []string{"NTFY_WEB_PUSH_EXPIRY_DURATION"}, Value: server.DefaultWebPushExpiryDuration, Usage: "duration after last update to expire subscription"}),
|
||||
)
|
||||
|
||||
var cmdServe = &cli.Command{
|
||||
|
@ -138,6 +140,8 @@ func execServe(c *cli.Context) error {
|
|||
webPushPrivateKey := c.String("web-push-private-key")
|
||||
webPushPublicKey := c.String("web-push-public-key")
|
||||
webPushSubscriptionsFile := c.String("web-push-subscriptions-file")
|
||||
webPushExpiryWarningDuration := c.Duration("web-push-expiry-warning-duration")
|
||||
webPushExpiryDuration := c.Duration("web-push-expiry-duration")
|
||||
webPushEmailAddress := c.String("web-push-email-address")
|
||||
cacheFile := c.String("cache-file")
|
||||
cacheDuration := c.Duration("cache-duration")
|
||||
|
@ -197,6 +201,8 @@ func execServe(c *cli.Context) error {
|
|||
return errors.New("if web push is enabled, web-push-private-key, web-push-public-key, web-push-subscriptions-file, web-push-email-address, and base-url should be set. run 'ntfy web-push generate-keys' to generate keys")
|
||||
} else if keepaliveInterval < 5*time.Second {
|
||||
return errors.New("keepalive interval cannot be lower than five seconds")
|
||||
} else if webPushEnabled && (webPushExpiryWarningDuration < 24*time.Hour || (webPushExpiryDuration-webPushExpiryWarningDuration < 24*time.Hour)) {
|
||||
return errors.New("web push expiry warning duration must be at least 1 day, expire duration must be at least 1 day later")
|
||||
} else if managerInterval < 5*time.Second {
|
||||
return errors.New("manager interval cannot be lower than five seconds")
|
||||
} else if cacheDuration > 0 && cacheDuration < managerInterval {
|
||||
|
@ -364,6 +370,8 @@ func execServe(c *cli.Context) error {
|
|||
conf.WebPushPublicKey = webPushPublicKey
|
||||
conf.WebPushSubscriptionsFile = webPushSubscriptionsFile
|
||||
conf.WebPushEmailAddress = webPushEmailAddress
|
||||
conf.WebPushExpiryDuration = webPushExpiryDuration
|
||||
conf.WebPushExpiryWarningDuration = webPushExpiryWarningDuration
|
||||
|
||||
// Set up hot-reloading of config
|
||||
go sigHandlerConfigReload(config)
|
||||
|
|
|
@ -827,6 +827,11 @@ web-push-email-address: sysadmin@example.com
|
|||
|
||||
# don't forget to set the required base-url for web push notifications
|
||||
base-url: https://ntfy.example.com
|
||||
|
||||
# you can also set custom expiry and warning durations
|
||||
# the minimum is 1 day for warning, and 1 day after warning for expiry
|
||||
web-push-expiry-warning-duration: 168h
|
||||
web-push-expiry-duration: 192h
|
||||
```
|
||||
|
||||
The `web-push-subscriptions-file` is used to store the push subscriptions. Subscriptions do not ever expire automatically, unless the push
|
||||
|
@ -1339,6 +1344,8 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`).
|
|||
| `web-push-private-key` | `NTFY_WEB_PUSH_PRIVATE_KEY` | *string* | - | Web Push: Private Key. Run `ntfy web-push generate-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 |
|
||||
| `web-push-expiry-warning-duration` | `NTFY_WEB_PUSH_EXPIRY_WARNING_DURATION` | *duration* | 1 week | Web Push: Time before expiry warning is sent (min 1 day) |
|
||||
| `web-push-expiry-duration` | `NTFY_WEB_PUSH_EXPIRY_DURATION` | *duration* | 1 week + 1 day | Web Push: Time before subscription is expired (min 1 day after warning) |
|
||||
|
||||
The format for a *duration* is: `<number>(smh)`, e.g. 30s, 20m or 1h.
|
||||
The format for a *size* is: `<number>(GMK)`, e.g. 1G, 200M or 4000k.
|
||||
|
@ -1436,5 +1443,8 @@ OPTIONS:
|
|||
--web-push-private-key value, --web_push_private_key value private key used for web push notifications [$NTFY_WEB_PUSH_PRIVATE_KEY]
|
||||
--web-push-subscriptions-file value, --web_push_subscriptions_file value file used to store web push subscriptions [$NTFY_WEB_PUSH_SUBSCRIPTIONS_FILE]
|
||||
--web-push-email-address value, --web_push_email_address value e-mail address of sender, required to use browser push services [$NTFY_WEB_PUSH_EMAIL_ADDRESS]
|
||||
--web-push-expiry-warning-duration value, --web_push_expiry_warning_duration value duration after last update to send a warning notification (default: 168h0m0s) [$NTFY_WEB_PUSH_EXPIRY_WARNING_DURATION]
|
||||
--web-push-expiry-duration value, --web_push_expiry_duration value duration after last update to expire subscription (default: 192h0m0s) [$NTFY_WEB_PUSH_EXPIRY_DURATION]
|
||||
--help, -h show help
|
||||
|
||||
```
|
||||
|
|
|
@ -2,15 +2,9 @@
|
|||
|
||||
You can use the Web UI to subscribe to topics as well. Simply type in the topic name and click the *Subscribe* button.
|
||||
|
||||
While subscribing, you have the option to enable desktop notifications, as well as background notifications. When you
|
||||
enable them for the first time, you will be prompted to allow notifications on your browser.
|
||||
While subscribing, you have the option to enable background notifications on supported browsers.
|
||||
|
||||
- **Sound only**
|
||||
|
||||
If you don't enable browser notifications, a sound will play when a new notification comes in, and the tab title
|
||||
will show the number of new notifications.
|
||||
|
||||
- **Browser Notifications**
|
||||
- If background notifications are off:
|
||||
|
||||
This requires an active ntfy tab to be open to receive notifications. These are typically instantaneous, and will
|
||||
appear as a system notification. If you don't see these, check that your browser is allowed to show notifications
|
||||
|
@ -19,7 +13,7 @@ enable them for the first time, you will be prompted to allow notifications on y
|
|||
If you don't want to enable background notifications, pinning the ntfy tab on your browser is a good solution to leave
|
||||
it running.
|
||||
|
||||
- **Background Notifications**
|
||||
- If background notifications are on:
|
||||
|
||||
This uses the [Web Push API](https://caniuse.com/push-api). You don't need an active ntfy tab open, but in some
|
||||
cases you may need to keep your browser open.
|
||||
|
@ -27,6 +21,9 @@ enable them for the first time, you will be prompted to allow notifications on y
|
|||
Background notifications are only supported on the same server hosting the web app. You cannot use another server,
|
||||
but can instead subscribe on the other server itself.
|
||||
|
||||
If the ntfy app is not opened for more than a week, background notifications will be paused. You can resume them
|
||||
by opening the app again, and will get a warning notification before they are paused.
|
||||
|
||||
| Browser | Platform | Browser Running | Browser Not Running | Restrictions |
|
||||
| ------- | -------- | --------------- | ------------------- | ------------------------------------------------------- |
|
||||
| Chrome | Desktop | ✅ | ❌ | |
|
||||
|
|
|
@ -23,6 +23,12 @@ const (
|
|||
DefaultStripePriceCacheDuration = 3 * time.Hour // Time to keep Stripe prices cached in memory before a refresh is needed
|
||||
)
|
||||
|
||||
// Defines default web push settings
|
||||
const (
|
||||
DefaultWebPushExpiryWarningDuration = 7 * 24 * time.Hour
|
||||
DefaultWebPushExpiryDuration = DefaultWebPushExpiryWarningDuration + 24*time.Hour
|
||||
)
|
||||
|
||||
// Defines all global and per-visitor limits
|
||||
// - message size limit: the max number of bytes for a message
|
||||
// - total topic limit: max number of topics overall
|
||||
|
@ -152,6 +158,8 @@ type Config struct {
|
|||
WebPushPublicKey string
|
||||
WebPushSubscriptionsFile string
|
||||
WebPushEmailAddress string
|
||||
WebPushExpiryDuration time.Duration
|
||||
WebPushExpiryWarningDuration time.Duration
|
||||
}
|
||||
|
||||
// NewConfig instantiates a default new server config
|
||||
|
@ -238,5 +246,7 @@ func NewConfig() *Config {
|
|||
WebPushPublicKey: "",
|
||||
WebPushSubscriptionsFile: "",
|
||||
WebPushEmailAddress: "",
|
||||
WebPushExpiryDuration: DefaultWebPushExpiryDuration,
|
||||
WebPushExpiryWarningDuration: DefaultWebPushExpiryWarningDuration,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ const (
|
|||
tagResetter = "resetter"
|
||||
tagWebsocket = "websocket"
|
||||
tagMatrix = "matrix"
|
||||
tagWebPush = "web_push"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
|
@ -47,6 +47,8 @@
|
|||
# web-push-private-key:
|
||||
# web-push-subscriptions-file:
|
||||
# web-push-email-address:
|
||||
# web-push-expiry-warning-duration: 168h
|
||||
# web-push-expiry-duration: 192h
|
||||
|
||||
# If "cache-file" is set, messages are cached in a local SQLite database instead of only in-memory.
|
||||
# This allows for service restarts without losing messages in support of the since= parameter.
|
||||
|
|
|
@ -15,6 +15,9 @@ func (s *Server) execManager() {
|
|||
s.pruneTokens()
|
||||
s.pruneAttachments()
|
||||
s.pruneMessages()
|
||||
if s.config.WebPushEnabled {
|
||||
s.expireOrNotifyOldSubscriptions()
|
||||
}
|
||||
|
||||
// Message count per topic
|
||||
var messagesCached int
|
||||
|
|
|
@ -43,6 +43,7 @@ func (s *Server) publishToWebPushEndpoints(v *visitor, m *message) {
|
|||
subscriptions, err := s.webPush.SubscriptionsForTopic(m.Topic)
|
||||
if err != nil {
|
||||
logvm(v, m).Err(err).Warn("Unable to publish web push messages")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -50,42 +51,61 @@ func (s *Server) publishToWebPushEndpoints(v *visitor, m *message) {
|
|||
go func(i int, sub webPushSubscription) {
|
||||
ctx := log.Context{"endpoint": sub.BrowserSubscription.Endpoint, "username": sub.UserID, "topic": m.Topic, "message_id": m.ID}
|
||||
|
||||
payload := &webPushPayload{
|
||||
SubscriptionID: fmt.Sprintf("%s/%s", s.config.BaseURL, m.Topic),
|
||||
Message: *m,
|
||||
}
|
||||
jsonPayload, err := json.Marshal(payload)
|
||||
|
||||
if err != nil {
|
||||
logvm(v, m).Err(err).Fields(ctx).Debug("Unable to publish web push message")
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := webpush.SendNotification(jsonPayload, &sub.BrowserSubscription, &webpush.Options{
|
||||
Subscriber: s.config.WebPushEmailAddress,
|
||||
VAPIDPublicKey: s.config.WebPushPublicKey,
|
||||
VAPIDPrivateKey: s.config.WebPushPrivateKey,
|
||||
// Deliverability on iOS isn't great with lower urgency values,
|
||||
// and thus we can't really map lower ntfy priorities to lower urgency values
|
||||
Urgency: webpush.UrgencyHigh,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logvm(v, m).Err(err).Fields(ctx).Debug("Unable to publish web push message")
|
||||
if err := s.webPush.RemoveByEndpoint(sub.BrowserSubscription.Endpoint); err != nil {
|
||||
logvm(v, m).Err(err).Fields(ctx).Warn("Unable to expire subscription")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// May want to handle at least 429 differently, but for now treat all errors the same
|
||||
if !(200 <= resp.StatusCode && resp.StatusCode <= 299) {
|
||||
logvm(v, m).Fields(ctx).Field("response", resp).Debug("Unable to publish web push message")
|
||||
if err := s.webPush.RemoveByEndpoint(sub.BrowserSubscription.Endpoint); err != nil {
|
||||
logvm(v, m).Err(err).Fields(ctx).Warn("Unable to expire subscription")
|
||||
}
|
||||
return
|
||||
}
|
||||
s.sendWebPushNotification(newWebPushPayload(fmt.Sprintf("%s/%s", s.config.BaseURL, m.Topic), *m), &sub, &ctx)
|
||||
}(i, xi)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) expireOrNotifyOldSubscriptions() {
|
||||
subscriptions, err := s.webPush.ExpireAndGetExpiringSubscriptions(s.config.WebPushExpiryWarningDuration, s.config.WebPushExpiryDuration)
|
||||
if err != nil {
|
||||
log.Tag(tagWebPush).Err(err).Warn("Unable to publish expiry imminent warning")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
for i, xi := range subscriptions {
|
||||
go func(i int, sub webPushSubscription) {
|
||||
ctx := log.Context{"endpoint": sub.BrowserSubscription.Endpoint}
|
||||
|
||||
s.sendWebPushNotification(newWebPushSubscriptionExpiringPayload(), &sub, &ctx)
|
||||
}(i, xi)
|
||||
}
|
||||
|
||||
log.Tag(tagWebPush).Debug("Expired old subscriptions and published %d expiry imminent warnings", len(subscriptions))
|
||||
}
|
||||
|
||||
func (s *Server) sendWebPushNotification(payload any, sub *webPushSubscription, ctx *log.Context) {
|
||||
jsonPayload, err := json.Marshal(payload)
|
||||
|
||||
if err != nil {
|
||||
log.Tag(tagWebPush).Err(err).Fields(*ctx).Debug("Unable to publish web push message")
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := webpush.SendNotification(jsonPayload, &sub.BrowserSubscription, &webpush.Options{
|
||||
Subscriber: s.config.WebPushEmailAddress,
|
||||
VAPIDPublicKey: s.config.WebPushPublicKey,
|
||||
VAPIDPrivateKey: s.config.WebPushPrivateKey,
|
||||
// Deliverability on iOS isn't great with lower urgency values,
|
||||
// and thus we can't really map lower ntfy priorities to lower urgency values
|
||||
Urgency: webpush.UrgencyHigh,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Tag(tagWebPush).Err(err).Fields(*ctx).Debug("Unable to publish web push message")
|
||||
if err := s.webPush.RemoveByEndpoint(sub.BrowserSubscription.Endpoint); err != nil {
|
||||
log.Tag(tagWebPush).Err(err).Fields(*ctx).Warn("Unable to expire subscription")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// May want to handle at least 429 differently, but for now treat all errors the same
|
||||
if !(200 <= resp.StatusCode && resp.StatusCode <= 299) {
|
||||
log.Tag(tagWebPush).Fields(*ctx).Field("response", resp).Debug("Unable to publish web push message")
|
||||
if err := s.webPush.RemoveByEndpoint(sub.BrowserSubscription.Endpoint); err != nil {
|
||||
log.Tag(tagWebPush).Err(err).Fields(*ctx).Warn("Unable to expire subscription")
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
|
@ -157,8 +157,42 @@ func TestServer_WebPush_PublishExpire(t *testing.T) {
|
|||
requireSubscriptionCount(t, s, "test-topic-abc", 0)
|
||||
}
|
||||
|
||||
func TestServer_WebPush_Expiry(t *testing.T) {
|
||||
s := newTestServer(t, newTestConfigWithWebPush(t))
|
||||
|
||||
var received atomic.Bool
|
||||
|
||||
upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, err := io.ReadAll(r.Body)
|
||||
require.Nil(t, err)
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte(``))
|
||||
received.Store(true)
|
||||
}))
|
||||
defer upstreamServer.Close()
|
||||
|
||||
addSubscription(t, s, "test-topic", upstreamServer.URL+"/push-receive")
|
||||
requireSubscriptionCount(t, s, "test-topic", 1)
|
||||
|
||||
_, err := s.webPush.db.Exec("UPDATE subscriptions SET updated_at = datetime('now', '-7 days')")
|
||||
require.Nil(t, err)
|
||||
|
||||
s.expireOrNotifyOldSubscriptions()
|
||||
requireSubscriptionCount(t, s, "test-topic", 1)
|
||||
|
||||
waitFor(t, func() bool {
|
||||
return received.Load()
|
||||
})
|
||||
|
||||
_, err = s.webPush.db.Exec("UPDATE subscriptions SET updated_at = datetime('now', '-8 days')")
|
||||
require.Nil(t, err)
|
||||
|
||||
s.expireOrNotifyOldSubscriptions()
|
||||
requireSubscriptionCount(t, s, "test-topic", 0)
|
||||
}
|
||||
|
||||
func payloadForTopics(t *testing.T, topics []string) string {
|
||||
topicsJson, err := json.Marshal(topics)
|
||||
topicsJSON, err := json.Marshal(topics)
|
||||
require.Nil(t, err)
|
||||
|
||||
return fmt.Sprintf(`{
|
||||
|
@ -170,7 +204,7 @@ func payloadForTopics(t *testing.T, topics []string) string {
|
|||
"auth": "auth-key"
|
||||
}
|
||||
}
|
||||
}`, topicsJson)
|
||||
}`, topicsJSON)
|
||||
}
|
||||
|
||||
func addSubscription(t *testing.T, s *Server, topic string, url string) {
|
||||
|
|
|
@ -468,10 +468,29 @@ type apiStripeSubscriptionDeletedEvent struct {
|
|||
}
|
||||
|
||||
type webPushPayload struct {
|
||||
Event string `json:"event"`
|
||||
SubscriptionID string `json:"subscription_id"`
|
||||
Message message `json:"message"`
|
||||
}
|
||||
|
||||
func newWebPushPayload(subscriptionID string, message message) webPushPayload {
|
||||
return webPushPayload{
|
||||
Event: "message",
|
||||
SubscriptionID: subscriptionID,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
type webPushControlMessagePayload struct {
|
||||
Event string `json:"event"`
|
||||
}
|
||||
|
||||
func newWebPushSubscriptionExpiringPayload() webPushControlMessagePayload {
|
||||
return webPushControlMessagePayload{
|
||||
Event: "subscription_expiring",
|
||||
}
|
||||
}
|
||||
|
||||
type webPushSubscription struct {
|
||||
BrowserSubscription webpush.Subscription
|
||||
UserID string
|
||||
|
|
|
@ -3,6 +3,7 @@ package server
|
|||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/SherClockHolmes/webpush-go"
|
||||
_ "github.com/mattn/go-sqlite3" // SQLite driver
|
||||
|
@ -18,7 +19,8 @@ const (
|
|||
endpoint TEXT NOT NULL,
|
||||
key_auth TEXT NOT NULL,
|
||||
key_p256dh TEXT NOT NULL,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
warning_sent BOOLEAN DEFAULT FALSE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_topic ON subscriptions (topic);
|
||||
CREATE INDEX IF NOT EXISTS idx_endpoint ON subscriptions (endpoint);
|
||||
|
@ -32,8 +34,12 @@ const (
|
|||
deleteWebPushSubscriptionByEndpointQuery = `DELETE FROM subscriptions WHERE endpoint = ?`
|
||||
deleteWebPushSubscriptionByUserIDQuery = `DELETE FROM subscriptions WHERE user_id = ?`
|
||||
deleteWebPushSubscriptionByTopicAndEndpointQuery = `DELETE FROM subscriptions WHERE topic = ? AND endpoint = ?`
|
||||
deleteWebPushSubscriptionsByAgeQuery = `DELETE FROM subscriptions WHERE warning_sent = 1 AND updated_at <= datetime('now', ?)`
|
||||
|
||||
selectWebPushSubscriptionsForTopicQuery = `SELECT endpoint, key_auth, key_p256dh, user_id FROM subscriptions WHERE topic = ?`
|
||||
selectWebPushSubscriptionsForTopicQuery = `SELECT endpoint, key_auth, key_p256dh, user_id FROM subscriptions WHERE topic = ?`
|
||||
selectWebPushSubscriptionsExpiringSoonQuery = `SELECT DISTINCT endpoint, key_auth, key_p256dh FROM subscriptions WHERE warning_sent = 0 AND updated_at <= datetime('now', ?)`
|
||||
|
||||
updateWarningSentQuery = `UPDATE subscriptions SET warning_sent = true WHERE warning_sent = 0 AND updated_at <= datetime('now', ?)`
|
||||
|
||||
selectWebPushSubscriptionsCountQuery = `SELECT COUNT(*) FROM subscriptions`
|
||||
)
|
||||
|
@ -72,7 +78,6 @@ func setupNewSubscriptionsDB(db *sql.DB) error {
|
|||
}
|
||||
|
||||
func (c *webPushStore) UpdateSubscriptions(topics []string, userID string, subscription webpush.Subscription) error {
|
||||
fmt.Printf("AAA")
|
||||
tx, err := c.db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -121,6 +126,49 @@ func (c *webPushStore) SubscriptionsForTopic(topic string) (subscriptions []webP
|
|||
return data, nil
|
||||
}
|
||||
|
||||
func (c *webPushStore) ExpireAndGetExpiringSubscriptions(warningDuration time.Duration, expiryDuration time.Duration) (subscriptions []webPushSubscription, err error) {
|
||||
tx, err := c.db.Begin()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
_, err = tx.Exec(deleteWebPushSubscriptionsByAgeQuery, fmt.Sprintf("-%.2f seconds", expiryDuration.Seconds()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rows, err := tx.Query(selectWebPushSubscriptionsExpiringSoonQuery, fmt.Sprintf("-%.2f seconds", warningDuration.Seconds()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var data []webPushSubscription
|
||||
for rows.Next() {
|
||||
i := webPushSubscription{}
|
||||
err = rows.Scan(&i.BrowserSubscription.Endpoint, &i.BrowserSubscription.Keys.Auth, &i.BrowserSubscription.Keys.P256dh)
|
||||
fmt.Printf("%v+", i)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data = append(data, i)
|
||||
}
|
||||
|
||||
// also set warning as sent
|
||||
_, err = tx.Exec(updateWarningSentQuery, fmt.Sprintf("-%.2f seconds", warningDuration.Seconds()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (c *webPushStore) RemoveByEndpoint(endpoint string) error {
|
||||
_, err := c.db.Exec(
|
||||
deleteWebPushSubscriptionByEndpointQuery,
|
||||
|
@ -136,6 +184,7 @@ func (c *webPushStore) RemoveByUserID(userID string) error {
|
|||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *webPushStore) Close() error {
|
||||
return c.db.Close()
|
||||
}
|
||||
|
|
112
web/public/sw.js
112
web/public/sw.js
|
@ -32,35 +32,50 @@ self.addEventListener("push", (event) => {
|
|||
const data = event.data.json();
|
||||
console.log("[ServiceWorker] Received Web Push Event", { event, data });
|
||||
|
||||
const { subscription_id: subscriptionId, message } = data;
|
||||
broadcastChannel.postMessage(message);
|
||||
|
||||
event.waitUntil(
|
||||
(async () => {
|
||||
const db = await getDbAsync();
|
||||
|
||||
await Promise.all([
|
||||
(async () => {
|
||||
await db.notifications.add({
|
||||
...message,
|
||||
subscriptionId,
|
||||
// New marker (used for bubble indicator); cannot be boolean; Dexie index limitation
|
||||
new: 1,
|
||||
});
|
||||
const badgeCount = await db.notifications.where({ new: 1 }).count();
|
||||
console.log("[ServiceWorker] Setting new app badge count", { badgeCount });
|
||||
self.navigator.setAppBadge?.(badgeCount);
|
||||
})(),
|
||||
db.subscriptions.update(subscriptionId, {
|
||||
last: message.id,
|
||||
}),
|
||||
self.registration.showNotification(formatTitleWithDefault(message, message.topic), {
|
||||
tag: subscriptionId,
|
||||
body: formatMessage(message),
|
||||
if (data.event === "subscription_expiring") {
|
||||
await self.registration.showNotification("Notifications will be paused", {
|
||||
body: "Open ntfy to continue receiving notifications",
|
||||
icon: "/static/images/ntfy.png",
|
||||
data,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
} else if (data.event === "message") {
|
||||
const { subscription_id: subscriptionId, message } = data;
|
||||
broadcastChannel.postMessage(message);
|
||||
|
||||
const db = await getDbAsync();
|
||||
|
||||
await Promise.all([
|
||||
(async () => {
|
||||
await db.notifications.add({
|
||||
...message,
|
||||
subscriptionId,
|
||||
// New marker (used for bubble indicator); cannot be boolean; Dexie index limitation
|
||||
new: 1,
|
||||
});
|
||||
const badgeCount = await db.notifications.where({ new: 1 }).count();
|
||||
console.log("[ServiceWorker] Setting new app badge count", { badgeCount });
|
||||
self.navigator.setAppBadge?.(badgeCount);
|
||||
})(),
|
||||
db.subscriptions.update(subscriptionId, {
|
||||
last: message.id,
|
||||
}),
|
||||
self.registration.showNotification(formatTitleWithDefault(message, message.topic), {
|
||||
tag: subscriptionId,
|
||||
body: formatMessage(message),
|
||||
icon: "/static/images/ntfy.png",
|
||||
data,
|
||||
}),
|
||||
]);
|
||||
} else {
|
||||
// We can't ignore the push, since permission can be revoked by the browser
|
||||
await self.registration.showNotification("Unknown notification received from server", {
|
||||
body: "You may need to update ntfy by opening the web app",
|
||||
icon: "/static/images/ntfy.png",
|
||||
data,
|
||||
});
|
||||
}
|
||||
})()
|
||||
);
|
||||
});
|
||||
|
@ -68,33 +83,38 @@ self.addEventListener("push", (event) => {
|
|||
self.addEventListener("notificationclick", (event) => {
|
||||
event.notification.close();
|
||||
|
||||
const { message } = event.notification.data;
|
||||
|
||||
if (message.click) {
|
||||
self.clients.openWindow(message.click);
|
||||
return;
|
||||
}
|
||||
|
||||
const rootUrl = new URL(self.location.origin);
|
||||
const topicUrl = new URL(message.topic, self.location.origin);
|
||||
|
||||
event.waitUntil(
|
||||
(async () => {
|
||||
const clients = await self.clients.matchAll({ type: "window" });
|
||||
|
||||
const topicClient = clients.find((client) => client.url === topicUrl.toString());
|
||||
if (topicClient) {
|
||||
topicClient.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
const rootUrl = new URL(self.location.origin);
|
||||
const rootClient = clients.find((client) => client.url === rootUrl.toString());
|
||||
if (rootClient) {
|
||||
rootClient.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
self.clients.openWindow(topicUrl);
|
||||
if (event.notification.data.event !== "message") {
|
||||
if (rootClient) {
|
||||
rootClient.focus();
|
||||
} else {
|
||||
self.clients.openWindow(rootUrl);
|
||||
}
|
||||
} else {
|
||||
const { message } = event.notification.data;
|
||||
|
||||
if (message.click) {
|
||||
self.clients.openWindow(message.click);
|
||||
return;
|
||||
}
|
||||
|
||||
const topicUrl = new URL(message.topic, self.location.origin);
|
||||
const topicClient = clients.find((client) => client.url === topicUrl.toString());
|
||||
|
||||
if (topicClient) {
|
||||
topicClient.focus();
|
||||
} else if (rootClient) {
|
||||
rootClient.focus();
|
||||
} else {
|
||||
self.clients.openWindow(topicUrl);
|
||||
}
|
||||
}
|
||||
})()
|
||||
);
|
||||
});
|
||||
|
|
|
@ -42,4 +42,5 @@ class Prefs {
|
|||
}
|
||||
}
|
||||
|
||||
export default new Prefs(getDb());
|
||||
const prefs = new Prefs(getDb());
|
||||
export default prefs;
|
||||
|
|
|
@ -57,7 +57,6 @@ import { IncorrectPasswordError, UnauthorizedError } from "../app/errors";
|
|||
import { ProChip } from "./SubscriptionPopup";
|
||||
import theme from "./theme";
|
||||
import session from "../app/Session";
|
||||
import subscriptionManager from "../app/SubscriptionManager";
|
||||
|
||||
const Account = () => {
|
||||
if (!session.exists()) {
|
||||
|
|
|
@ -58,9 +58,7 @@ const App = () => {
|
|||
const updateTitle = (newNotificationsCount) => {
|
||||
document.title = newNotificationsCount > 0 ? `(${newNotificationsCount}) ntfy` : "ntfy";
|
||||
|
||||
if ("setAppBadge" in window.navigator) {
|
||||
window.navigator.setAppBadge(newNotificationsCount);
|
||||
}
|
||||
window.navigator.setAppBadge?.(newNotificationsCount);
|
||||
};
|
||||
|
||||
const Layout = () => {
|
||||
|
|
|
@ -10,7 +10,6 @@ import session from "../app/Session";
|
|||
import accountApi from "../app/AccountApi";
|
||||
import { UnauthorizedError } from "../app/errors";
|
||||
import { webPushRefreshWorker, useWebPushUpdateWorker } from "../app/WebPushWorker";
|
||||
import notifier from "../app/Notifier";
|
||||
|
||||
/**
|
||||
* Wire connectionManager and subscriptionManager so that subscriptions are updated when the connection
|
||||
|
|
Loading…
Reference in a new issue