Rename to sequence_id

This commit is contained in:
binwiederhier
2026-01-08 14:27:18 -05:00
parent fd8cd5ca91
commit 1ab7ca876c
13 changed files with 125 additions and 121 deletions

View File

@@ -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}

View File

@@ -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,

View File

@@ -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",

View File

@@ -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

View File

@@ -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) {

View File

@@ -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

View File

@@ -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
});

View File

@@ -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;
}
}

View File

@@ -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,
});
}

View File

@@ -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;

View File

@@ -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

View File

@@ -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;
};

View File

@@ -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) {