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

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

View File

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

View File

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

View File

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

View File

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