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

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

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

@@ -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 ID string `json:"id"` // Random message ID
SID string `json:"sid,omitempty"` // Message sequence ID for updating message contents (omitted if same as 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 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 {

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