From 2856793effecc7c2038e259c8c26b9348edced70 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Tue, 6 Jan 2026 14:22:55 -0500 Subject: [PATCH] Deleted --- server/message_cache.go | 7 +++-- server/server.go | 48 ++++++++++++++++++++++++++++++ server/types.go | 12 ++++---- web/public/sw.js | 6 ++++ web/src/app/SubscriptionManager.js | 4 ++- web/src/app/db.js | 7 +++++ 6 files changed, 74 insertions(+), 10 deletions(-) diff --git a/server/message_cache.go b/server/message_cache.go index bd4dddd4..589d06f8 100644 --- a/server/message_cache.go +++ b/server/message_cache.go @@ -75,7 +75,7 @@ const ( 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 = ` + 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 FROM messages WHERE mid = ? @@ -431,7 +431,7 @@ func (c *messageCache) addMessages(ms []*message) error { m.ContentType, m.Encoding, published, - 0, + m.Deleted, ) if err != nil { return err @@ -719,8 +719,9 @@ func readMessages(rows *sql.Rows) ([]*message, error) { func readMessage(rows *sql.Rows) (*message, error) { var timestamp, expires, attachmentSize, attachmentExpires int64 - var priority, deleted int + var priority int var id, sid, topic, msg, title, tagsStr, click, icon, actionsStr, attachmentName, attachmentType, attachmentURL, sender, user, contentType, encoding string + var deleted bool err := rows.Scan( &id, &sid, diff --git a/server/server.go b/server/server.go index 9c612ebd..67fe328d 100644 --- a/server/server.go +++ b/server/server.go @@ -547,6 +547,8 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit return s.transformMatrixJSON(s.limitRequestsWithTopic(s.authorizeTopicWrite(s.handlePublishMatrix)))(w, r, v) } else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && (topicPathRegex.MatchString(r.URL.Path) || updatePathRegex.MatchString(r.URL.Path)) { return s.limitRequestsWithTopic(s.authorizeTopicWrite(s.handlePublish))(w, r, v) + } else if r.Method == http.MethodDelete && updatePathRegex.MatchString(r.URL.Path) { + return s.limitRequestsWithTopic(s.authorizeTopicWrite(s.handleDelete))(w, r, v) } else if r.Method == http.MethodGet && publishPathRegex.MatchString(r.URL.Path) { return s.limitRequestsWithTopic(s.authorizeTopicWrite(s.handlePublish))(w, r, v) } else if r.Method == http.MethodGet && jsonPathRegex.MatchString(r.URL.Path) { @@ -902,6 +904,52 @@ func (s *Server) handlePublishMatrix(w http.ResponseWriter, r *http.Request, v * return writeMatrixSuccess(w) } +func (s *Server) handleDelete(w http.ResponseWriter, r *http.Request, v *visitor) error { + t, err := fromContext[*topic](r, contextTopic) + if err != nil { + return err + } + vrate, err := fromContext[*visitor](r, contextRateVisitor) + if err != nil { + return err + } + if !util.ContainsIP(s.config.VisitorRequestExemptPrefixes, v.ip) && !vrate.MessageAllowed() { + return errHTTPTooManyRequestsLimitMessages.With(t) + } + sid, e := s.sidFromPath(r.URL.Path) + if e != nil { + return e.With(t) + } + // Create a delete message: empty body, same SID, deleted flag set + m := newDefaultMessage(t.ID, "") + m.SID = sid + m.Deleted = true + m.Sender = v.IP() + m.User = v.MaybeUserID() + m.Expires = time.Unix(m.Time, 0).Add(v.Limits().MessageExpiryDuration).Unix() + // Publish to subscribers + if err := t.Publish(v, m); err != nil { + return err + } + // Send to Firebase for Android clients + if s.firebaseClient != nil { + go s.sendToFirebase(v, m) + } + // Send to web push endpoints + if s.config.WebPushPublicKey != "" { + go s.publishToWebPushEndpoints(v, m) + } + // Add to message cache + if err := s.messageCache.AddMessage(m); err != nil { + return err + } + logvrm(v, r, m).Tag(tagPublish).Debug("Deleted message with SID %s", sid) + s.mu.Lock() + s.messages++ + s.mu.Unlock() + return s.writeJSON(w, m.forJSON()) +} + func (s *Server) sendToFirebase(v *visitor, m *message) { logvm(v, m).Tag(tagFirebase).Debug("Publishing to Firebase") if err := s.firebaseClient.Send(v, m); err != nil { diff --git a/server/types.go b/server/types.go index 467e80f5..88110b0d 100644 --- a/server/types.go +++ b/server/types.go @@ -24,14 +24,14 @@ 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 + 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 Topic string `json:"topic"` Title string `json:"title,omitempty"` - Message string `json:"message,omitempty"` + Message string `json:"message"` // Allow empty message body Priority int `json:"priority,omitempty"` Tags []string `json:"tags,omitempty"` Click string `json:"click,omitempty"` @@ -40,10 +40,10 @@ type message struct { Attachment *attachment `json:"attachment,omitempty"` PollID string `json:"poll_id,omitempty"` ContentType string `json:"content_type,omitempty"` // text/plain by default (if empty), or text/markdown - Encoding string `json:"encoding,omitempty"` // empty for raw UTF-8, or "base64" for encoded bytes + Encoding string `json:"encoding,omitempty"` // Empty for raw UTF-8, or "base64" for encoded bytes + Deleted bool `json:"deleted,omitempty"` // True if message is marked as deleted Sender netip.Addr `json:"-"` // IP address of uploader, used for rate limiting User string `json:"-"` // UserID of the uploader, used to associated attachments - Deleted int `json:"deleted,omitempty"` } func (m *message) Context() log.Context { diff --git a/web/public/sw.js b/web/public/sw.js index e010e4d4..33e97da8 100644 --- a/web/public/sw.js +++ b/web/public/sw.js @@ -57,6 +57,12 @@ const handlePushMessage = async (data) => { broadcastChannel.postMessage(message); // To potentially play sound await addNotification({ subscriptionId, message }); + + // Don't show a notification for deleted messages + if (message.deleted) { + return; + } + await self.registration.showNotification( ...toNotificationParams({ subscriptionId, diff --git a/web/src/app/SubscriptionManager.js b/web/src/app/SubscriptionManager.js index ccce5ccc..7d917e2d 100644 --- a/web/src/app/SubscriptionManager.js +++ b/web/src/app/SubscriptionManager.js @@ -175,6 +175,7 @@ class SubscriptionManager { } // Collapse notification updates based on sids, keeping only the latest version + // Filters out notifications where the latest in the sequence is deleted groupNotificationsBySID(notifications) { const latestBySid = {}; notifications.forEach((notification) => { @@ -184,7 +185,8 @@ class SubscriptionManager { latestBySid[key] = notification; } }); - return Object.values(latestBySid); + // Filter out notifications where the latest is deleted + return Object.values(latestBySid).filter((n) => !n.deleted); } /** Adds notification, or returns false if it already exists */ diff --git a/web/src/app/db.js b/web/src/app/db.js index f11a5d0b..0391388d 100644 --- a/web/src/app/db.js +++ b/web/src/app/db.js @@ -18,6 +18,13 @@ const createDatabase = (username) => { prefs: "&key", }); + db.version(5).stores({ + subscriptions: "&id,baseUrl,[baseUrl+mutedUntil]", + notifications: "&id,sid,subscriptionId,time,new,deleted,[subscriptionId+new]", // added deleted index + users: "&baseUrl,username", + prefs: "&key", + }); + return db; };