mirror of
https://github.com/binwiederhier/ntfy.git
synced 2026-01-18 16:17:26 +01:00
Rename to sequence_id
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
×tamp,
|
||||
&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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user