소스 검색

Docs in server.yml, schemaVersion table, refactoring

binwiederhier 2 년 전
부모
커밋
9d38aeb863
7개의 변경된 파일74개의 추가작업 그리고 37개의 파일을 삭제
  1. 0 0
      cmd/webpush.go
  2. 0 0
      cmd/webpush_test.go
  3. 19 9
      server/server.yml
  4. 1 3
      server/server_manager.go
  5. 22 11
      server/server_web_push.go
  6. 8 5
      server/server_web_push_test.go
  7. 24 9
      server/webpush_store.go

+ 0 - 0
cmd/web_push.go → cmd/webpush.go


+ 0 - 0
cmd/web_push_test.go → cmd/webpush_test.go


+ 19 - 9
server/server.yml

@@ -38,15 +38,6 @@
 #
 # firebase-key-file: <filename>
 
-# Enable web push
-#
-# Run "ntfy webpush keys" to generate the keys
-#
-# web-push-public-key:
-# web-push-private-key:
-# web-push-subscriptions-file:
-# web-push-email-address:
-
 # 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.
 #
@@ -153,6 +144,25 @@
 # smtp-server-domain:
 # smtp-server-addr-prefix:
 
+# 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
+# endpoint, which will then forward it to the browser.
+#
+# You must configure all settings below to enable Web Push.
+# Run "ntfy webpush keys" to generate the keys.
+#
+# - web-push-public-key is the generated VAPID public key, e.g. AA1234BBCCddvveekaabcdfqwertyuiopasdfghjklzxcvbnm1234567890
+# - web-push-private-key is the generated VAPID private key, e.g. AA2BB1234567890abcdefzxcvbnm1234567890
+# - web-push-subscriptions-file is a database file to keep track of browser subscription endpoints, e.g. `/var/cache/ntfy/webpush.db`
+# - web-push-email-address is the admin email address send to the push provider, e.g. `sysadmin@example.com`
+#
+# web-push-public-key:
+# web-push-private-key:
+# web-push-subscriptions-file:
+# web-push-email-address:
+
 # If enabled, ntfy can perform voice calls via Twilio via the "X-Call" header.
 #
 # - twilio-account is the Twilio account SID, e.g. AC12345beefbeef67890beefbeef122586

+ 1 - 3
server/server_manager.go

@@ -15,9 +15,7 @@ func (s *Server) execManager() {
 	s.pruneTokens()
 	s.pruneAttachments()
 	s.pruneMessages()
-	if s.config.WebPushPublicKey != "" {
-		s.expireOrNotifyOldSubscriptions()
-	}
+	s.pruneOrNotifyWebPushSubscriptions()
 
 	// Message count per topic
 	var messagesCached int

+ 22 - 11
server/server_web_push.go

@@ -78,28 +78,39 @@ func (s *Server) publishToWebPushEndpoints(v *visitor, m *message) {
 // TODO this should return error
 // TODO rate limiting
 
-func (s *Server) expireOrNotifyOldSubscriptions() {
+func (s *Server) pruneOrNotifyWebPushSubscriptions() {
+	if s.config.WebPushPublicKey == "" {
+		return
+	}
+	go func() {
+		if err := s.pruneOrNotifyWebPushSubscriptionsInternal(); err != nil {
+			log.Tag(tagWebPush).Err(err).Warn("Unable to prune or notify web push subscriptions")
+		}
+	}()
+}
+
+func (s *Server) pruneOrNotifyWebPushSubscriptionsInternal() error {
 	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
+		return err
 	} else if len(subscriptions) == 0 {
-		return
+		return nil
 	}
 	payload, err := json.Marshal(newWebPushSubscriptionExpiringPayload())
 	if err != nil {
 		log.Tag(tagWebPush).Err(err).Warn("Unable to marshal expiring payload")
-		return
+		return err
 	}
-	go func() {
-		for _, subscription := range subscriptions {
-			ctx := log.Context{"endpoint": subscription.BrowserSubscription.Endpoint}
-			if err := s.sendWebPushNotification(payload, &subscription, &ctx); err != nil {
-				log.Tag(tagWebPush).Err(err).Fields(ctx).Warn("Unable to publish expiry imminent warning")
-			}
+	for _, subscription := range subscriptions {
+		ctx := log.Context{"endpoint": subscription.BrowserSubscription.Endpoint}
+		if err := s.sendWebPushNotification(payload, &subscription, &ctx); err != nil {
+			log.Tag(tagWebPush).Err(err).Fields(ctx).Warn("Unable to publish expiry imminent warning")
+			return err
 		}
-	}()
+	}
 	log.Tag(tagWebPush).Debug("Expiring old subscriptions and published %d expiry imminent warnings", len(subscriptions))
+	return nil
 }
 
 func (s *Server) sendWebPushNotification(message []byte, sub *webPushSubscription, ctx *log.Context) error {

+ 8 - 5
server/server_web_push_test.go

@@ -149,7 +149,7 @@ func TestServer_WebPush_Publish(t *testing.T) {
 	})
 }
 
-func TestServer_WebPush_PublishExpire(t *testing.T) {
+func TestServer_WebPush_Publish_RemoveOnError(t *testing.T) {
 	s := newTestServer(t, newTestConfigWithWebPush(t))
 
 	var received atomic.Bool
@@ -201,7 +201,7 @@ func TestServer_WebPush_Expiry(t *testing.T) {
 	_, err := s.webPush.db.Exec("UPDATE subscriptions SET updated_at = datetime('now', '-7 days')")
 	require.Nil(t, err)
 
-	s.expireOrNotifyOldSubscriptions()
+	s.pruneOrNotifyWebPushSubscriptions()
 	requireSubscriptionCount(t, s, "test-topic", 1)
 
 	waitFor(t, func() bool {
@@ -211,8 +211,12 @@ func TestServer_WebPush_Expiry(t *testing.T) {
 	_, 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)
+	s.pruneOrNotifyWebPushSubscriptions()
+	waitFor(t, func() bool {
+		subs, err := s.webPush.SubscriptionsForTopic("test-topic")
+		require.Nil(t, err)
+		return len(subs) == 0
+	})
 }
 
 func payloadForTopics(t *testing.T, topics []string, endpoint string) string {
@@ -246,6 +250,5 @@ func addSubscription(t *testing.T, s *Server, topic string, url string) {
 func requireSubscriptionCount(t *testing.T, s *Server, topic string, expectedLength int) {
 	subs, err := s.webPush.SubscriptionsForTopic("test-topic")
 	require.Nil(t, err)
-
 	require.Len(t, subs, expectedLength)
 }

+ 24 - 9
server/web_push.go → server/webpush_store.go

@@ -22,11 +22,16 @@ const (
 			updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
 			warning_sent BOOLEAN DEFAULT FALSE
 		);
+		CREATE TABLE IF NOT EXISTS schemaVersion (
+			id INT PRIMARY KEY,
+			version INT NOT NULL
+		);	
 		CREATE INDEX IF NOT EXISTS idx_topic ON subscriptions (topic);
 		CREATE INDEX IF NOT EXISTS idx_endpoint ON subscriptions (endpoint);
 		CREATE UNIQUE INDEX IF NOT EXISTS idx_topic_endpoint ON subscriptions (topic, endpoint);
 		COMMIT;
 	`
+
 	insertWebPushSubscriptionQuery = `
 		INSERT OR REPLACE INTO subscriptions (topic, user_id, endpoint, key_auth, key_p256dh)
 		VALUES (?, ?, ?, ?, ?)
@@ -39,8 +44,13 @@ const (
 	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`
+// Schema management queries
+const (
+	currentWebPushSchemaVersion     = 1
+	insertWebPushSchemaVersion      = `INSERT INTO schemaVersion VALUES (1, ?)`
+	selectWebPushSchemaVersionQuery = `SELECT version FROM schemaVersion WHERE id = 1`
 )
 
 type webPushStore struct {
@@ -52,7 +62,7 @@ func newWebPushStore(filename string) (*webPushStore, error) {
 	if err != nil {
 		return nil, err
 	}
-	if err := setupSubscriptionsDB(db); err != nil {
+	if err := setupWebPushDB(db); err != nil {
 		return nil, err
 	}
 	return &webPushStore{
@@ -60,33 +70,38 @@ func newWebPushStore(filename string) (*webPushStore, error) {
 	}, nil
 }
 
-func setupSubscriptionsDB(db *sql.DB) error {
-	// If 'subscriptions' table does not exist, this must be a new database
-	rows, err := db.Query(selectWebPushSubscriptionsCountQuery)
+func setupWebPushDB(db *sql.DB) error {
+	// If 'schemaVersion' table does not exist, this must be a new database
+	rows, err := db.Query(selectWebPushSchemaVersionQuery)
 	if err != nil {
-		return setupNewSubscriptionsDB(db)
+		return setupNewWebPushDB(db)
 	}
 	return rows.Close()
 }
 
-func setupNewSubscriptionsDB(db *sql.DB) error {
+func setupNewWebPushDB(db *sql.DB) error {
 	if _, err := db.Exec(createWebPushSubscriptionsTableQuery); err != nil {
 		return err
 	}
+	if _, err := db.Exec(insertWebPushSchemaVersion, currentWebPushSchemaVersion); err != nil {
+		return err
+	}
 	return nil
 }
 
+// UpdateSubscriptions updates the subscriptions for the given topics and user ID. It always first deletes all
+// existing entries for a given endpoint.
 func (c *webPushStore) UpdateSubscriptions(topics []string, userID string, subscription webpush.Subscription) error {
 	tx, err := c.db.Begin()
 	if err != nil {
 		return err
 	}
 	defer tx.Rollback()
-	if err = c.RemoveByEndpoint(subscription.Endpoint); err != nil {
+	if _, err := tx.Exec(deleteWebPushSubscriptionByEndpointQuery, subscription.Endpoint); err != nil {
 		return err
 	}
 	for _, topic := range topics {
-		if err := c.AddSubscription(topic, userID, subscription); err != nil {
+		if _, err = tx.Exec(insertWebPushSubscriptionQuery, topic, userID, subscription.Endpoint, subscription.Keys.Auth, subscription.Keys.P256dh); err != nil {
 			return err
 		}
 	}