This commit is contained in:
binwiederhier
2026-01-06 14:22:55 -05:00
parent f51e99dc80
commit 2856793eff
6 changed files with 74 additions and 10 deletions

View File

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

View File

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

View File

@@ -31,7 +31,7 @@ type message struct {
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 {

View File

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

View File

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

View File

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