Browse Source

Rename to sequence_id

binwiederhier 2 months ago
parent
commit
1ab7ca876c

+ 1 - 1
server/errors.go

@@ -125,7 +125,7 @@ var (
 	errHTTPBadRequestInvalidUsername                 = &errHTTP{40046, http.StatusBadRequest, "invalid request: invalid username", "", nil}
 	errHTTPBadRequestTemplateFileNotFound            = &errHTTP{40047, http.StatusBadRequest, "invalid request: template file not found", "https://ntfy.sh/docs/publish/#message-templating", nil}
 	errHTTPBadRequestTemplateFileInvalid             = &errHTTP{40048, http.StatusBadRequest, "invalid request: template file invalid", "https://ntfy.sh/docs/publish/#message-templating", nil}
-	errHTTPBadRequestSIDInvalid                      = &errHTTP{40049, http.StatusBadRequest, "invalid request: SID invalid", "https://ntfy.sh/docs/publish/#TODO", nil}
+	errHTTPBadRequestSIDInvalid                      = &errHTTP{40049, http.StatusBadRequest, "invalid request: sequence ID invalid", "https://ntfy.sh/docs/publish/#TODO", nil}
 	errHTTPNotFound                                  = &errHTTP{40401, http.StatusNotFound, "page not found", "", nil}
 	errHTTPUnauthorized                              = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication", nil}
 	errHTTPForbidden                                 = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication", nil}

+ 19 - 19
server/message_cache.go

@@ -29,7 +29,7 @@ const (
 		CREATE TABLE IF NOT EXISTS messages (
 			id INTEGER PRIMARY KEY AUTOINCREMENT,
 			mid TEXT NOT NULL,
-			sid TEXT NOT NULL,
+			sequence_id TEXT NOT NULL,
 			time INT NOT NULL,
 			expires INT NOT NULL,
 			topic TEXT NOT NULL,
@@ -54,7 +54,7 @@ const (
 			deleted INT NOT NULL
 		);
 		CREATE INDEX IF NOT EXISTS idx_mid ON messages (mid);
-		CREATE INDEX IF NOT EXISTS idx_sid ON messages (sid);
+		CREATE INDEX IF NOT EXISTS idx_sequence_id ON messages (sequence_id);
 		CREATE INDEX IF NOT EXISTS idx_time ON messages (time);
 		CREATE INDEX IF NOT EXISTS idx_topic ON messages (topic);
 		CREATE INDEX IF NOT EXISTS idx_expires ON messages (expires);
@@ -69,50 +69,50 @@ const (
 		COMMIT;
 	`
 	insertMessageQuery = `
-		INSERT INTO messages (mid, sid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_deleted, sender, user, content_type, encoding, published, deleted)
+		INSERT INTO messages (mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_deleted, sender, user, content_type, encoding, published, deleted)
 		VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
 	`
 	deleteMessageQuery                = `DELETE FROM messages WHERE mid = ?`
 	updateMessagesForTopicExpiryQuery = `UPDATE messages SET expires = ? WHERE topic = ?`
 	selectRowIDFromMessageID          = `SELECT id FROM messages WHERE mid = ?` // Do not include topic, see #336 and TestServer_PollSinceID_MultipleTopics
 	selectMessagesByIDQuery           = `
-		SELECT mid, sid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted
+		SELECT mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted
 		FROM messages
 		WHERE mid = ?
 	`
 	selectMessagesSinceTimeQuery = `
-		SELECT mid, sid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted
+		SELECT mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted
 		FROM messages
 		WHERE topic = ? AND time >= ? AND published = 1
 		ORDER BY time, id
 	`
 	selectMessagesSinceTimeIncludeScheduledQuery = `
-		SELECT mid, sid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted
+		SELECT mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted
 		FROM messages
 		WHERE topic = ? AND time >= ?
 		ORDER BY time, id
 	`
 	selectMessagesSinceIDQuery = `
-		SELECT mid, sid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted
+		SELECT mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted
 		FROM messages
 		WHERE topic = ? AND id > ? AND published = 1
 		ORDER BY time, id
 	`
 	selectMessagesSinceIDIncludeScheduledQuery = `
-		SELECT mid, sid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted
+		SELECT mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted
 		FROM messages
 		WHERE topic = ? AND (id > ? OR published = 0)
 		ORDER BY time, id
 	`
 	selectMessagesLatestQuery = `
-		SELECT mid, sid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted
+		SELECT mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted
 		FROM messages
 		WHERE topic = ? AND published = 1
 		ORDER BY time DESC, id DESC
 		LIMIT 1
 	`
 	selectMessagesDueQuery = `
-		SELECT mid, sid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted
+		SELECT mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted
 		FROM messages
 		WHERE time <= ? AND published = 0
 		ORDER BY time, id
@@ -267,9 +267,9 @@ const (
 
 	//13 -> 14
 	migrate13To14AlterMessagesTableQuery = `
-	  ALTER TABLE messages ADD COLUMN sid TEXT NOT NULL DEFAULT('');
+		ALTER TABLE messages ADD COLUMN sequence_id TEXT NOT NULL DEFAULT('');
 		ALTER TABLE messages ADD COLUMN deleted INT NOT NULL DEFAULT('0');
-		CREATE INDEX IF NOT EXISTS idx_sid ON messages (sid);
+		CREATE INDEX IF NOT EXISTS idx_sequence_id ON messages (sequence_id);
 	`
 )
 
@@ -409,7 +409,7 @@ func (c *messageCache) addMessages(ms []*message) error {
 		}
 		_, err := stmt.Exec(
 			m.ID,
-			m.SID,
+			m.SequenceID,
 			m.Time,
 			m.Expires,
 			m.Topic,
@@ -720,11 +720,11 @@ func readMessages(rows *sql.Rows) ([]*message, error) {
 func readMessage(rows *sql.Rows) (*message, error) {
 	var timestamp, expires, attachmentSize, attachmentExpires int64
 	var priority int
-	var id, sid, topic, msg, title, tagsStr, click, icon, actionsStr, attachmentName, attachmentType, attachmentURL, sender, user, contentType, encoding string
+	var id, sequenceID, topic, msg, title, tagsStr, click, icon, actionsStr, attachmentName, attachmentType, attachmentURL, sender, user, contentType, encoding string
 	var deleted bool
 	err := rows.Scan(
 		&id,
-		&sid,
+		&sequenceID,
 		&timestamp,
 		&expires,
 		&topic,
@@ -773,13 +773,13 @@ func readMessage(rows *sql.Rows) (*message, error) {
 			URL:     attachmentURL,
 		}
 	}
-	// Clear SID if it equals ID (we do not want the SID in the message output)
-	if sid == id {
-		sid = ""
+	// Clear SequenceID if it equals ID (we do not want the SequenceID in the message output)
+	if sequenceID == id {
+		sequenceID = ""
 	}
 	return &message{
 		ID:          id,
-		SID:         sid,
+		SequenceID:  sequenceID,
 		Time:        timestamp,
 		Expires:     expires,
 		Event:       messageEvent,

+ 7 - 7
server/message_cache_test.go

@@ -319,7 +319,7 @@ func testCacheAttachments(t *testing.T, c *messageCache) {
 	expires1 := time.Now().Add(-4 * time.Hour).Unix() // Expired
 	m := newDefaultMessage("mytopic", "flower for you")
 	m.ID = "m1"
-	m.SID = "m1"
+	m.SequenceID = "m1"
 	m.Sender = netip.MustParseAddr("1.2.3.4")
 	m.Attachment = &attachment{
 		Name:    "flower.jpg",
@@ -333,7 +333,7 @@ func testCacheAttachments(t *testing.T, c *messageCache) {
 	expires2 := time.Now().Add(2 * time.Hour).Unix() // Future
 	m = newDefaultMessage("mytopic", "sending you a car")
 	m.ID = "m2"
-	m.SID = "m2"
+	m.SequenceID = "m2"
 	m.Sender = netip.MustParseAddr("1.2.3.4")
 	m.Attachment = &attachment{
 		Name:    "car.jpg",
@@ -347,7 +347,7 @@ func testCacheAttachments(t *testing.T, c *messageCache) {
 	expires3 := time.Now().Add(1 * time.Hour).Unix() // Future
 	m = newDefaultMessage("another-topic", "sending you another car")
 	m.ID = "m3"
-	m.SID = "m3"
+	m.SequenceID = "m3"
 	m.User = "u_BAsbaAa"
 	m.Sender = netip.MustParseAddr("5.6.7.8")
 	m.Attachment = &attachment{
@@ -403,13 +403,13 @@ func TestMemCache_Attachments_Expired(t *testing.T) {
 func testCacheAttachmentsExpired(t *testing.T, c *messageCache) {
 	m := newDefaultMessage("mytopic", "flower for you")
 	m.ID = "m1"
-	m.SID = "m1"
+	m.SequenceID = "m1"
 	m.Expires = time.Now().Add(time.Hour).Unix()
 	require.Nil(t, c.AddMessage(m))
 
 	m = newDefaultMessage("mytopic", "message with attachment")
 	m.ID = "m2"
-	m.SID = "m2"
+	m.SequenceID = "m2"
 	m.Expires = time.Now().Add(2 * time.Hour).Unix()
 	m.Attachment = &attachment{
 		Name:    "car.jpg",
@@ -422,7 +422,7 @@ func testCacheAttachmentsExpired(t *testing.T, c *messageCache) {
 
 	m = newDefaultMessage("mytopic", "message with external attachment")
 	m.ID = "m3"
-	m.SID = "m3"
+	m.SequenceID = "m3"
 	m.Expires = time.Now().Add(2 * time.Hour).Unix()
 	m.Attachment = &attachment{
 		Name:    "car.jpg",
@@ -434,7 +434,7 @@ func testCacheAttachmentsExpired(t *testing.T, c *messageCache) {
 
 	m = newDefaultMessage("mytopic2", "message with expired attachment")
 	m.ID = "m4"
-	m.SID = "m4"
+	m.SequenceID = "m4"
 	m.Expires = time.Now().Add(2 * time.Hour).Unix()
 	m.Attachment = &attachment{
 		Name:    "expired-car.jpg",

+ 13 - 13
server/server.go

@@ -917,13 +917,13 @@ func (s *Server) handleDelete(w http.ResponseWriter, r *http.Request, v *visitor
 	if !util.ContainsIP(s.config.VisitorRequestExemptPrefixes, v.ip) && !vrate.MessageAllowed() {
 		return errHTTPTooManyRequestsLimitMessages.With(t)
 	}
-	sid, e := s.sidFromPath(r.URL.Path)
+	sequenceID, e := s.sequenceIDFromPath(r.URL.Path)
 	if e != nil {
 		return e.With(t)
 	}
-	// Create a delete message: empty body, same SID, deleted flag set
+	// Create a delete message: empty body, same SequenceID, deleted flag set
 	m := newDefaultMessage(t.ID, deletedMessageBody)
-	m.SID = sid
+	m.SequenceID = sequenceID
 	m.Deleted = true
 	m.Sender = v.IP()
 	m.User = v.MaybeUserID()
@@ -944,7 +944,7 @@ func (s *Server) handleDelete(w http.ResponseWriter, r *http.Request, v *visitor
 	if err := s.messageCache.AddMessage(m); err != nil {
 		return err
 	}
-	logvrm(v, r, m).Tag(tagPublish).Debug("Deleted message with SID %s", sid)
+	logvrm(v, r, m).Tag(tagPublish).Debug("Deleted message with sequence ID %s", sequenceID)
 	s.mu.Lock()
 	s.messages++
 	s.mu.Unlock()
@@ -1009,21 +1009,21 @@ func (s *Server) forwardPollRequest(v *visitor, m *message) {
 
 func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, firebase bool, email, call string, template templateMode, unifiedpush bool, err *errHTTP) {
 	if r.Method != http.MethodGet && updatePathRegex.MatchString(r.URL.Path) {
-		pathSID, err := s.sidFromPath(r.URL.Path)
+		pathSequenceID, err := s.sequenceIDFromPath(r.URL.Path)
 		if err != nil {
 			return false, false, "", "", "", false, err
 		}
-		m.SID = pathSID
+		m.SequenceID = pathSequenceID
 	} else {
-		sid := readParam(r, "x-sequence-id", "sequence-id", "sid")
-		if sid != "" {
-			if sidRegex.MatchString(sid) {
-				m.SID = sid
+		sequenceID := readParam(r, "x-sequence-id", "sequence-id", "sid")
+		if sequenceID != "" {
+			if sidRegex.MatchString(sequenceID) {
+				m.SequenceID = sequenceID
 			} else {
 				return false, false, "", "", "", false, errHTTPBadRequestSIDInvalid
 			}
 		} else {
-			m.SID = m.ID
+			m.SequenceID = m.ID
 		}
 	}
 	cache = readBoolParam(r, true, "x-cache", "cache")
@@ -1764,8 +1764,8 @@ func (s *Server) topicsFromPath(path string) ([]*topic, string, error) {
 	return topics, parts[1], nil
 }
 
-// sidFromPath returns the SID from a POST path like /mytopic/sidHere
-func (s *Server) sidFromPath(path string) (string, *errHTTP) {
+// sequenceIDFromPath returns the sequence ID from a POST path like /mytopic/sequenceIdHere
+func (s *Server) sequenceIDFromPath(path string) (string, *errHTTP) {
 	parts := strings.Split(path, "/")
 	if len(parts) != 3 {
 		return "", errHTTPBadRequestSIDInvalid

+ 5 - 5
server/server_test.go

@@ -684,7 +684,7 @@ func TestServer_PublishWithSIDInPath(t *testing.T) {
 	response := request(t, s, "POST", "/mytopic/sid", "message", nil)
 	msg := toMessage(t, response.Body.String())
 	require.NotEmpty(t, msg.ID)
-	require.Equal(t, "sid", msg.SID)
+	require.Equal(t, "sid", msg.SequenceID)
 }
 
 func TestServer_PublishWithSIDInHeader(t *testing.T) {
@@ -695,7 +695,7 @@ func TestServer_PublishWithSIDInHeader(t *testing.T) {
 	})
 	msg := toMessage(t, response.Body.String())
 	require.NotEmpty(t, msg.ID)
-	require.Equal(t, "sid", msg.SID)
+	require.Equal(t, "sid", msg.SequenceID)
 }
 
 func TestServer_PublishWithSIDInPathAndHeader(t *testing.T) {
@@ -706,7 +706,7 @@ func TestServer_PublishWithSIDInPathAndHeader(t *testing.T) {
 	})
 	msg := toMessage(t, response.Body.String())
 	require.NotEmpty(t, msg.ID)
-	require.Equal(t, "sid1", msg.SID) // SID in path has priority over SID in header
+	require.Equal(t, "sid1", msg.SequenceID) // Sequence ID in path has priority over header
 }
 
 func TestServer_PublishWithSIDInQuery(t *testing.T) {
@@ -715,7 +715,7 @@ func TestServer_PublishWithSIDInQuery(t *testing.T) {
 	response := request(t, s, "PUT", "/mytopic?sid=sid1", "message", nil)
 	msg := toMessage(t, response.Body.String())
 	require.NotEmpty(t, msg.ID)
-	require.Equal(t, "sid1", msg.SID)
+	require.Equal(t, "sid1", msg.SequenceID)
 }
 
 func TestServer_PublishWithSIDViaGet(t *testing.T) {
@@ -724,7 +724,7 @@ func TestServer_PublishWithSIDViaGet(t *testing.T) {
 	response := request(t, s, "GET", "/mytopic/publish?sid=sid1", "message", nil)
 	msg := toMessage(t, response.Body.String())
 	require.NotEmpty(t, msg.ID)
-	require.Equal(t, "sid1", msg.SID)
+	require.Equal(t, "sid1", msg.SequenceID)
 }
 
 func TestServer_PublishWithInvalidSIDInPath(t *testing.T) {

+ 28 - 28
server/types.go

@@ -24,11 +24,11 @@ const (
 
 // message represents a message published to a topic
 type message struct {
-	ID          string      `json:"id"`                // Random message ID
-	SID         string      `json:"sid,omitempty"`     // Message sequence ID for updating message contents (omitted if same as ID)
-	Time        int64       `json:"time"`              // Unix time in seconds
-	Expires     int64       `json:"expires,omitempty"` // Unix time in seconds (not required for open/keepalive)
-	Event       string      `json:"event"`             // One of the above
+	ID          string      `json:"id"`                    // Random message ID
+	SequenceID  string      `json:"sequence_id,omitempty"` // Message sequence ID for updating message contents (omitted if same as ID)
+	Time        int64       `json:"time"`                  // Unix time in seconds
+	Expires     int64       `json:"expires,omitempty"`     // Unix time in seconds (not required for open/keepalive)
+	Event       string      `json:"event"`                 // One of the above
 	Topic       string      `json:"topic"`
 	Title       string      `json:"title,omitempty"`
 	Message     string      `json:"message"` // Allow empty message body
@@ -48,12 +48,12 @@ type message struct {
 
 func (m *message) Context() log.Context {
 	fields := map[string]any{
-		"topic":             m.Topic,
-		"message_id":        m.ID,
-		"message_sid":       m.SID,
-		"message_time":      m.Time,
-		"message_event":     m.Event,
-		"message_body_size": len(m.Message),
+		"topic":               m.Topic,
+		"message_id":          m.ID,
+		"message_sequence_id": m.SequenceID,
+		"message_time":        m.Time,
+		"message_event":       m.Event,
+		"message_body_size":   len(m.Message),
 	}
 	if m.Sender.IsValid() {
 		fields["message_sender"] = m.Sender.String()
@@ -94,23 +94,23 @@ func newAction() *action {
 
 // publishMessage is used as input when publishing as JSON
 type publishMessage struct {
-	Topic    string   `json:"topic"`
-	SID      string   `json:"sid"`
-	Title    string   `json:"title"`
-	Message  string   `json:"message"`
-	Priority int      `json:"priority"`
-	Tags     []string `json:"tags"`
-	Click    string   `json:"click"`
-	Icon     string   `json:"icon"`
-	Actions  []action `json:"actions"`
-	Attach   string   `json:"attach"`
-	Markdown bool     `json:"markdown"`
-	Filename string   `json:"filename"`
-	Email    string   `json:"email"`
-	Call     string   `json:"call"`
-	Cache    string   `json:"cache"`    // use string as it defaults to true (or use &bool instead)
-	Firebase string   `json:"firebase"` // use string as it defaults to true (or use &bool instead)
-	Delay    string   `json:"delay"`
+	Topic      string   `json:"topic"`
+	SequenceID string   `json:"sequence_id"`
+	Title      string   `json:"title"`
+	Message    string   `json:"message"`
+	Priority   int      `json:"priority"`
+	Tags       []string `json:"tags"`
+	Click      string   `json:"click"`
+	Icon       string   `json:"icon"`
+	Actions    []action `json:"actions"`
+	Attach     string   `json:"attach"`
+	Markdown   bool     `json:"markdown"`
+	Filename   string   `json:"filename"`
+	Email      string   `json:"email"`
+	Call       string   `json:"call"`
+	Cache      string   `json:"cache"`    // use string as it defaults to true (or use &bool instead)
+	Firebase   string   `json:"firebase"` // use string as it defaults to true (or use &bool instead)
+	Delay      string   `json:"delay"`
 }
 
 // messageEncoder is a function that knows how to encode a message

+ 6 - 5
web/public/sw.js

@@ -8,7 +8,7 @@ import { dbAsync } from "../src/app/db";
 
 import { toNotificationParams, icon, badge } from "../src/app/notificationUtils";
 import initI18n from "../src/app/i18n";
-import { messageWithSID } from "../src/app/utils";
+import { messageWithSequenceId } from "../src/app/utils";
 
 /**
  * General docs for service workers and PWAs:
@@ -27,14 +27,15 @@ const addNotification = async ({ subscriptionId, message }) => {
 
   // Note: SubscriptionManager duplicates this logic, so if you change it here, change it there too
 
-  // Delete existing notification with same SID (if any)
-  if (message.sid) {
-    await db.notifications.where({ subscriptionId, sid: message.sid }).delete();
+  // Delete existing notification with same sequence ID (if any)
+  const sequenceId = message.sequence_id || message.id;
+  if (sequenceId) {
+    await db.notifications.where({ subscriptionId, sequenceId }).delete();
   }
 
   // Add notification to database
   await db.notifications.add({
-    ...messageWithSID(message),
+    ...messageWithSequenceId(message),
     subscriptionId,
     new: 1, // New marker (used for bubble indicator); cannot be boolean; Dexie index limitation
   });

+ 19 - 16
web/src/app/Poller.js

@@ -47,22 +47,25 @@ class Poller {
     // Filter out notifications older than the prune threshold
     const deleteAfterSeconds = await prefs.deleteAfter();
     const pruneThresholdTimestamp = deleteAfterSeconds > 0 ? Math.round(Date.now() / 1000) - deleteAfterSeconds : 0;
-    const recentNotifications = pruneThresholdTimestamp > 0 ? notifications.filter((n) => n.time >= pruneThresholdTimestamp) : notifications;
+    const recentNotifications =
+      pruneThresholdTimestamp > 0 ? notifications.filter((n) => n.time >= pruneThresholdTimestamp) : notifications;
 
     // Find the latest notification for each sequence ID
-    const latestBySid = this.latestNotificationsBySid(recentNotifications);
+    const latestBySequenceId = this.latestNotificationsBySequenceId(recentNotifications);
 
     // Delete all existing notifications for which the latest notification is marked as deleted
-    const deletedSids = Object.entries(latestBySid)
+    const deletedSequenceIds = Object.entries(latestBySequenceId)
       .filter(([, notification]) => notification.deleted)
-      .map(([sid]) => sid);
-    if (deletedSids.length > 0) {
-      console.log(`[Poller] Deleting notifications with deleted sequence IDs for ${subscription.id}`, deletedSids);
-      await Promise.all(deletedSids.map((sid) => subscriptionManager.deleteNotificationBySid(subscription.id, sid)));
+      .map(([sequenceId]) => sequenceId);
+    if (deletedSequenceIds.length > 0) {
+      console.log(`[Poller] Deleting notifications with deleted sequence IDs for ${subscription.id}`, deletedSequenceIds);
+      await Promise.all(
+        deletedSequenceIds.map((sequenceId) => subscriptionManager.deleteNotificationBySequenceId(subscription.id, sequenceId))
+      );
     }
 
     // Add only the latest notification for each non-deleted sequence
-    const notificationsToAdd = Object.values(latestBySid).filter((n) => !n.deleted);
+    const notificationsToAdd = Object.values(latestBySequenceId).filter((n) => !n.deleted);
     if (notificationsToAdd.length > 0) {
       console.log(`[Poller] Adding ${notificationsToAdd.length} notification(s) for ${subscription.id}`);
       await subscriptionManager.addNotifications(subscription.id, notificationsToAdd);
@@ -82,18 +85,18 @@ class Poller {
   }
 
   /**
-   * Groups notifications by sid and returns only the latest (highest time) for each sequence.
-   * Returns an object mapping sid -> latest notification.
+   * Groups notifications by sequenceId and returns only the latest (highest time) for each sequence.
+   * Returns an object mapping sequenceId -> latest notification.
    */
-  latestNotificationsBySid(notifications) {
-    const latestBySid = {};
+  latestNotificationsBySequenceId(notifications) {
+    const latestBySequenceId = {};
     notifications.forEach((notification) => {
-      const sid = notification.sid || notification.id;
-      if (!(sid in latestBySid) || notification.time >= latestBySid[sid].time) {
-        latestBySid[sid] = notification;
+      const sequenceId = notification.sequence_id || notification.id;
+      if (!(sequenceId in latestBySequenceId) || notification.time >= latestBySequenceId[sequenceId].time) {
+        latestBySequenceId[sequenceId] = notification;
       }
     });
-    return latestBySid;
+    return latestBySequenceId;
   }
 }
 

+ 16 - 16
web/src/app/SubscriptionManager.js

@@ -2,7 +2,7 @@ import api from "./Api";
 import notifier from "./Notifier";
 import prefs from "./Prefs";
 import db from "./db";
-import { messageWithSID, topicUrl } from "./utils";
+import { messageWithSequenceId, topicUrl } from "./utils";
 
 class SubscriptionManager {
   constructor(dbImpl) {
@@ -15,7 +15,7 @@ class SubscriptionManager {
     return Promise.all(
       subscriptions.map(async (s) => ({
         ...s,
-        new: await this.db.notifications.where({ subscriptionId: s.id, new: 1 }).count()
+        new: await this.db.notifications.where({ subscriptionId: s.id, new: 1 }).count(),
       }))
     );
   }
@@ -83,7 +83,7 @@ class SubscriptionManager {
       baseUrl,
       topic,
       mutedUntil: 0,
-      last: null
+      last: null,
     };
 
     await this.db.subscriptions.put(subscription);
@@ -101,7 +101,7 @@ class SubscriptionManager {
 
         const local = await this.add(remote.base_url, remote.topic, {
           displayName: remote.display_name, // May be undefined
-          reservation // May be null!
+          reservation, // May be null!
         });
 
         return local.id;
@@ -183,15 +183,15 @@ class SubscriptionManager {
 
       // Add notification to database
       await this.db.notifications.add({
-        ...messageWithSID(notification),
+        ...messageWithSequenceId(notification),
         subscriptionId,
-        new: 1 // New marker (used for bubble indicator); cannot be boolean; Dexie index limitation
+        new: 1, // New marker (used for bubble indicator); cannot be boolean; Dexie index limitation
       });
 
       // FIXME consider put() for double tab
       // Update subscription last message id (for ?since=... queries)
       await this.db.subscriptions.update(subscriptionId, {
-        last: notification.id
+        last: notification.id,
       });
     } catch (e) {
       console.error(`[SubscriptionManager] Error adding notification`, e);
@@ -202,12 +202,12 @@ class SubscriptionManager {
   /** Adds/replaces notifications, will not throw if they exist */
   async addNotifications(subscriptionId, notifications) {
     const notificationsWithSubscriptionId = notifications.map((notification) => {
-      return { ...messageWithSID(notification), subscriptionId };
+      return { ...messageWithSequenceId(notification), subscriptionId };
     });
     const lastNotificationId = notifications.at(-1).id;
     await this.db.notifications.bulkPut(notificationsWithSubscriptionId);
     await this.db.subscriptions.update(subscriptionId, {
-      last: lastNotificationId
+      last: lastNotificationId,
     });
   }
 
@@ -228,8 +228,8 @@ class SubscriptionManager {
     await this.db.notifications.delete(notificationId);
   }
 
-  async deleteNotificationBySid(subscriptionId, sid) {
-    await this.db.notifications.where({ subscriptionId, sid }).delete();
+  async deleteNotificationBySequenceId(subscriptionId, sequenceId) {
+    await this.db.notifications.where({ subscriptionId, sequenceId }).delete();
   }
 
   async deleteNotifications(subscriptionId) {
@@ -240,8 +240,8 @@ class SubscriptionManager {
     await this.db.notifications.where({ id: notificationId }).modify({ new: 0 });
   }
 
-  async markNotificationReadBySid(subscriptionId, sid) {
-    await this.db.notifications.where({ subscriptionId, sid }).modify({ new: 0 });
+  async markNotificationReadBySequenceId(subscriptionId, sequenceId) {
+    await this.db.notifications.where({ subscriptionId, sequenceId }).modify({ new: 0 });
   }
 
   async markNotificationsRead(subscriptionId) {
@@ -250,19 +250,19 @@ class SubscriptionManager {
 
   async setMutedUntil(subscriptionId, mutedUntil) {
     await this.db.subscriptions.update(subscriptionId, {
-      mutedUntil
+      mutedUntil,
     });
   }
 
   async setDisplayName(subscriptionId, displayName) {
     await this.db.subscriptions.update(subscriptionId, {
-      displayName
+      displayName,
     });
   }
 
   async setReservation(subscriptionId, reservation) {
     await this.db.subscriptions.update(subscriptionId, {
-      reservation
+      reservation,
     });
   }
 

+ 3 - 4
web/src/app/db.js

@@ -11,12 +11,11 @@ const createDatabase = (username) => {
   const dbName = username ? `ntfy-${username}` : "ntfy"; // IndexedDB database is based on the logged-in user
   const db = new Dexie(dbName);
 
-  db.version(6).stores({
-    // FIXME Should be 3
+  db.version(3).stores({
     subscriptions: "&id,baseUrl,[baseUrl+mutedUntil]",
-    notifications: "&id,sid,subscriptionId,time,new,deleted,[subscriptionId+new],[subscriptionId+sid]",
+    notifications: "&id,sequenceId,subscriptionId,time,new,deleted,[subscriptionId+new],[subscriptionId+sequenceId]",
     users: "&baseUrl,username",
-    prefs: "&key",
+    prefs: "&key"
   });
 
   return db;

+ 1 - 1
web/src/app/notificationUtils.js

@@ -62,7 +62,7 @@ export const toNotificationParams = ({ subscriptionId, message, defaultTitle, to
       icon,
       image,
       timestamp: message.time * 1000,
-      tag: message.sid || message.id, // Update notification if there is a sequence ID
+      tag: message.sequence_id || message.id, // Update notification if there is a sequence ID
       renotify: true,
       silent: false,
       // This is used by the notification onclick event

+ 3 - 3
web/src/app/utils.js

@@ -103,9 +103,9 @@ export const maybeActionErrors = (notification) => {
   return actionErrors;
 };
 
-export const messageWithSID = (message) => {
-  if (!message.sid) {
-    message.sid = message.id;
+export const messageWithSequenceId = (message) => {
+  if (!message.sequenceId) {
+    message.sequenceId = message.sequence_id || message.id;
   }
   return message;
 };

+ 4 - 3
web/src/components/hooks.js

@@ -53,9 +53,10 @@ export const useConnectionListeners = (account, subscriptions, users, webPushTop
         // Note: This logic is duplicated in the Android app in SubscriberService::onNotificationReceived()
         //       and FirebaseService::handleMessage().
 
-        // Delete existing notification with same sid, if any
-        if (notification.sid) {
-          await subscriptionManager.deleteNotificationBySid(subscriptionId, notification.sid);
+        // Delete existing notification with same sequenceId, if any
+        const sequenceId = notification.sequence_id || notification.id;
+        if (sequenceId) {
+          await subscriptionManager.deleteNotificationBySequenceId(subscriptionId, sequenceId);
         }
         // Add notification to database
         if (!notification.deleted) {