binwiederhier 1 month ago
parent
commit
2856793eff
6 changed files with 74 additions and 10 deletions
  1. 4 3
      server/message_cache.go
  2. 48 0
      server/server.go
  3. 6 6
      server/types.go
  4. 6 0
      web/public/sw.js
  5. 3 1
      web/src/app/SubscriptionManager.js
  6. 7 0
      web/src/app/db.js

+ 4 - 3
server/message_cache.go

@@ -75,7 +75,7 @@ const (
 	deleteMessageQuery                = `DELETE FROM messages WHERE mid = ?`
 	deleteMessageQuery                = `DELETE FROM messages WHERE mid = ?`
 	updateMessagesForTopicExpiryQuery = `UPDATE messages SET expires = ? WHERE topic = ?`
 	updateMessagesForTopicExpiryQuery = `UPDATE messages SET expires = ? WHERE topic = ?`
 	selectRowIDFromMessageID          = `SELECT id FROM messages WHERE mid = ?` // Do not include topic, see #336 and TestServer_PollSinceID_MultipleTopics
 	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
 		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
 		FROM messages
 		WHERE mid = ?
 		WHERE mid = ?
@@ -431,7 +431,7 @@ func (c *messageCache) addMessages(ms []*message) error {
 			m.ContentType,
 			m.ContentType,
 			m.Encoding,
 			m.Encoding,
 			published,
 			published,
-			0,
+			m.Deleted,
 		)
 		)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
@@ -719,8 +719,9 @@ func readMessages(rows *sql.Rows) ([]*message, error) {
 
 
 func readMessage(rows *sql.Rows) (*message, error) {
 func readMessage(rows *sql.Rows) (*message, error) {
 	var timestamp, expires, attachmentSize, attachmentExpires int64
 	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 id, sid, topic, msg, title, tagsStr, click, icon, actionsStr, attachmentName, attachmentType, attachmentURL, sender, user, contentType, encoding string
+	var deleted bool
 	err := rows.Scan(
 	err := rows.Scan(
 		&id,
 		&id,
 		&sid,
 		&sid,

+ 48 - 0
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)
 		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)) {
 	} 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)
 		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) {
 	} else if r.Method == http.MethodGet && publishPathRegex.MatchString(r.URL.Path) {
 		return s.limitRequestsWithTopic(s.authorizeTopicWrite(s.handlePublish))(w, r, v)
 		return s.limitRequestsWithTopic(s.authorizeTopicWrite(s.handlePublish))(w, r, v)
 	} else if r.Method == http.MethodGet && jsonPathRegex.MatchString(r.URL.Path) {
 	} 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)
 	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) {
 func (s *Server) sendToFirebase(v *visitor, m *message) {
 	logvm(v, m).Tag(tagFirebase).Debug("Publishing to Firebase")
 	logvm(v, m).Tag(tagFirebase).Debug("Publishing to Firebase")
 	if err := s.firebaseClient.Send(v, m); err != nil {
 	if err := s.firebaseClient.Send(v, m); err != nil {

+ 6 - 6
server/types.go

@@ -24,14 +24,14 @@ const (
 
 
 // message represents a message published to a topic
 // message represents a message published to a topic
 type message struct {
 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)
 	Expires     int64       `json:"expires,omitempty"` // Unix time in seconds (not required for open/keepalive)
 	Event       string      `json:"event"`             // One of the above
 	Event       string      `json:"event"`             // One of the above
 	Topic       string      `json:"topic"`
 	Topic       string      `json:"topic"`
 	Title       string      `json:"title,omitempty"`
 	Title       string      `json:"title,omitempty"`
-	Message     string      `json:"message,omitempty"`
+	Message     string      `json:"message"` // Allow empty message body
 	Priority    int         `json:"priority,omitempty"`
 	Priority    int         `json:"priority,omitempty"`
 	Tags        []string    `json:"tags,omitempty"`
 	Tags        []string    `json:"tags,omitempty"`
 	Click       string      `json:"click,omitempty"`
 	Click       string      `json:"click,omitempty"`
@@ -40,10 +40,10 @@ type message struct {
 	Attachment  *attachment `json:"attachment,omitempty"`
 	Attachment  *attachment `json:"attachment,omitempty"`
 	PollID      string      `json:"poll_id,omitempty"`
 	PollID      string      `json:"poll_id,omitempty"`
 	ContentType string      `json:"content_type,omitempty"` // text/plain by default (if empty), or text/markdown
 	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
 	Sender      netip.Addr  `json:"-"`                      // IP address of uploader, used for rate limiting
 	User        string      `json:"-"`                      // UserID of the uploader, used to associated attachments
 	User        string      `json:"-"`                      // UserID of the uploader, used to associated attachments
-	Deleted     int         `json:"deleted,omitempty"`
 }
 }
 
 
 func (m *message) Context() log.Context {
 func (m *message) Context() log.Context {

+ 6 - 0
web/public/sw.js

@@ -57,6 +57,12 @@ const handlePushMessage = async (data) => {
   broadcastChannel.postMessage(message); // To potentially play sound
   broadcastChannel.postMessage(message); // To potentially play sound
 
 
   await addNotification({ subscriptionId, message });
   await addNotification({ subscriptionId, message });
+
+  // Don't show a notification for deleted messages
+  if (message.deleted) {
+    return;
+  }
+
   await self.registration.showNotification(
   await self.registration.showNotification(
     ...toNotificationParams({
     ...toNotificationParams({
       subscriptionId,
       subscriptionId,

+ 3 - 1
web/src/app/SubscriptionManager.js

@@ -175,6 +175,7 @@ class SubscriptionManager {
   }
   }
 
 
   // Collapse notification updates based on sids, keeping only the latest version
   // Collapse notification updates based on sids, keeping only the latest version
+  // Filters out notifications where the latest in the sequence is deleted
   groupNotificationsBySID(notifications) {
   groupNotificationsBySID(notifications) {
     const latestBySid = {};
     const latestBySid = {};
     notifications.forEach((notification) => {
     notifications.forEach((notification) => {
@@ -184,7 +185,8 @@ class SubscriptionManager {
         latestBySid[key] = notification;
         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 */
   /** Adds notification, or returns false if it already exists */

+ 7 - 0
web/src/app/db.js

@@ -18,6 +18,13 @@ const createDatabase = (username) => {
     prefs: "&key",
     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;
   return db;
 };
 };