WIP: Update messages

This commit is contained in:
Philipp Heckel 2022-03-23 16:39:22 -04:00
parent b409c89d3b
commit 8848829dfa
4 changed files with 100 additions and 27 deletions

View file

@ -23,6 +23,7 @@ const (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
mid TEXT NOT NULL, mid TEXT NOT NULL,
time INT NOT NULL, time INT NOT NULL,
updated INT NOT NULL,
topic TEXT NOT NULL, topic TEXT NOT NULL,
message TEXT NOT NULL, message TEXT NOT NULL,
title TEXT NOT NULL, title TEXT NOT NULL,
@ -43,41 +44,47 @@ const (
COMMIT; COMMIT;
` `
insertMessageQuery = ` insertMessageQuery = `
INSERT INTO messages (mid, time, topic, message, title, priority, tags, click, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding, published) INSERT INTO messages (mid, time, updated, topic, message, title, priority, tags, click, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding, published)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
` `
updateMessageQuery = `UPDATE messages SET updated = ?, message = ?, title = ?, priority = ?, tags = ?, click = ? WHERE topic = ? AND mid = ?`
pruneMessagesQuery = `DELETE FROM messages WHERE time < ? AND published = 1` pruneMessagesQuery = `DELETE FROM messages WHERE time < ? AND published = 1`
selectRowIDFromMessageID = `SELECT id FROM messages WHERE topic = ? AND mid = ?` selectRowIDFromMessageID = `SELECT id FROM messages WHERE topic = ? AND mid = ?`
selectMessagesSinceTimeQuery = ` selectMessagesSinceTimeQuery = `
SELECT mid, time, topic, message, title, priority, tags, click, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding SELECT mid, time, updated, topic, message, title, priority, tags, click, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding
FROM messages FROM messages
WHERE topic = ? AND time >= ? AND published = 1 WHERE topic = ? AND time >= ? AND published = 1
ORDER BY time, id ORDER BY time, id
` `
selectMessagesSinceTimeIncludeScheduledQuery = ` selectMessagesSinceTimeIncludeScheduledQuery = `
SELECT mid, time, topic, message, title, priority, tags, click, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding SELECT mid, time, updated, topic, message, title, priority, tags, click, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding
FROM messages FROM messages
WHERE topic = ? AND time >= ? WHERE topic = ? AND time >= ?
ORDER BY time, id ORDER BY time, id
` `
selectMessagesSinceIDQuery = ` selectMessagesSinceIDQuery = `
SELECT mid, time, topic, message, title, priority, tags, click, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding SELECT mid, time, updated, topic, message, title, priority, tags, click, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding
FROM messages FROM messages
WHERE topic = ? AND id > ? AND published = 1 WHERE topic = ? AND id > ? AND published = 1
ORDER BY time, id ORDER BY time, id
` `
selectMessagesSinceIDIncludeScheduledQuery = ` selectMessagesSinceIDIncludeScheduledQuery = `
SELECT mid, time, topic, message, title, priority, tags, click, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding SELECT mid, time, updated, topic, message, title, priority, tags, click, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding
FROM messages FROM messages
WHERE topic = ? AND (id > ? OR published = 0) WHERE topic = ? AND (id > ? OR published = 0)
ORDER BY time, id ORDER BY time, id
` `
selectMessagesDueQuery = ` selectMessagesDueQuery = `
SELECT mid, time, topic, message, title, priority, tags, click, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding SELECT mid, time, updated, topic, message, title, priority, tags, click, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding
FROM messages FROM messages
WHERE time <= ? AND published = 0 WHERE time <= ? AND published = 0
ORDER BY time, id ORDER BY time, id
` `
selectMessageByIDQuery = `
SELECT mid, time, updated, topic, message, title, priority, tags, click, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding
FROM messages
WHERE topic = ? AND mid = ?
`
updateMessagePublishedQuery = `UPDATE messages SET published = 1 WHERE mid = ?` updateMessagePublishedQuery = `UPDATE messages SET published = 1 WHERE mid = ?`
selectMessagesCountQuery = `SELECT COUNT(*) FROM messages` selectMessagesCountQuery = `SELECT COUNT(*) FROM messages`
selectMessageCountForTopicQuery = `SELECT COUNT(*) FROM messages WHERE topic = ?` selectMessageCountForTopicQuery = `SELECT COUNT(*) FROM messages WHERE topic = ?`
@ -232,6 +239,7 @@ func (c *messageCache) AddMessage(m *message) error {
insertMessageQuery, insertMessageQuery,
m.ID, m.ID,
m.Time, m.Time,
m.Updated,
m.Topic, m.Topic,
m.Message, m.Message,
m.Title, m.Title,
@ -250,6 +258,28 @@ func (c *messageCache) AddMessage(m *message) error {
return err return err
} }
func (c *messageCache) UpdateMessage(m *message) error {
if m.Event != messageEvent {
return errUnexpectedMessageType
}
if c.nop {
return nil
}
tags := strings.Join(m.Tags, ",")
_, err := c.db.Exec(
updateMessageQuery,
m.Updated,
m.Message,
m.Title,
m.Priority,
tags,
m.Click,
m.Topic,
m.ID,
)
return err
}
func (c *messageCache) Messages(topic string, since sinceMarker, scheduled bool) ([]*message, error) { func (c *messageCache) Messages(topic string, since sinceMarker, scheduled bool) ([]*message, error) {
if since.IsNone() { if since.IsNone() {
return make([]*message, 0), nil return make([]*message, 0), nil
@ -393,16 +423,31 @@ func (c *messageCache) AttachmentsExpired() ([]string, error) {
return ids, nil return ids, nil
} }
func (c *messageCache) Message(topic, id string) (*message, error) {
rows, err := c.db.Query(selectMessageByIDQuery, topic, id)
if err != nil {
return nil, err
}
messages, err := readMessages(rows)
if err != nil {
return nil, err
} else if len(messages) == 0 {
return nil, errors.New("not found")
}
return messages[0], nil
}
func readMessages(rows *sql.Rows) ([]*message, error) { func readMessages(rows *sql.Rows) ([]*message, error) {
defer rows.Close() defer rows.Close()
messages := make([]*message, 0) messages := make([]*message, 0)
for rows.Next() { for rows.Next() {
var timestamp, attachmentSize, attachmentExpires int64 var timestamp, updated, attachmentSize, attachmentExpires int64
var priority int var priority int
var id, topic, msg, title, tagsStr, click, attachmentName, attachmentType, attachmentURL, attachmentOwner, encoding string var id, topic, msg, title, tagsStr, click, attachmentName, attachmentType, attachmentURL, attachmentOwner, encoding string
err := rows.Scan( err := rows.Scan(
&id, &id,
&timestamp, &timestamp,
&updated,
&topic, &topic,
&msg, &msg,
&title, &title,
@ -438,6 +483,7 @@ func readMessages(rows *sql.Rows) ([]*message, error) {
messages = append(messages, &message{ messages = append(messages, &message{
ID: id, ID: id,
Time: timestamp, Time: timestamp,
Updated: updated,
Event: messageEvent, Event: messageEvent,
Topic: topic, Topic: topic,
Message: msg, Message: msg,

View file

@ -55,9 +55,10 @@ type handleFunc func(http.ResponseWriter, *http.Request, *visitor) error
var ( var (
// If changed, don't forget to update Android App and auth_sqlite.go // If changed, don't forget to update Android App and auth_sqlite.go
topicRegex = regexp.MustCompile(`^[-_A-Za-z0-9]{1,64}$`) // No /! topicRegex = regexp.MustCompile(`^[-_A-Za-z0-9]{1,64}$`) // No /!
topicPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}$`) // Regex must match JS & Android app! topicPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}$`) // Regex must match JS & Android app!
externalTopicPathRegex = regexp.MustCompile(`^/[^/]+\.[^/]+/[-_A-Za-z0-9]{1,64}$`) // Extended topic path, for web-app, e.g. /example.com/mytopic updateTopicPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}/[A-Za-z0-9]{12}$`) // ID length must match messageIDLength & util.randomStringCharset
externalTopicPathRegex = regexp.MustCompile(`^/[^/]+\.[^/]+/[-_A-Za-z0-9]{1,64}$`) // Extended topic path, for web-app, e.g. /example.com/mytopic
jsonPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/json$`) jsonPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/json$`)
ssePathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/sse$`) ssePathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/sse$`)
rawPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/raw$`) rawPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/raw$`)
@ -279,7 +280,7 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
return s.handleOptions(w, r) return s.handleOptions(w, r)
} else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && r.URL.Path == "/" { } else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && r.URL.Path == "/" {
return s.limitRequests(s.transformBodyJSON(s.authWrite(s.handlePublish)))(w, r, v) return s.limitRequests(s.transformBodyJSON(s.authWrite(s.handlePublish)))(w, r, v)
} else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && topicPathRegex.MatchString(r.URL.Path) { } else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && (topicPathRegex.MatchString(r.URL.Path) || updateTopicPathRegex.MatchString(r.URL.Path)) {
return s.limitRequests(s.authWrite(s.handlePublish))(w, r, v) return s.limitRequests(s.authWrite(s.handlePublish))(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.limitRequests(s.authWrite(s.handlePublish))(w, r, v) return s.limitRequests(s.authWrite(s.handlePublish))(w, r, v)
@ -390,7 +391,22 @@ func (s *Server) handleFile(w http.ResponseWriter, r *http.Request, v *visitor)
} }
func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visitor) error { func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visitor) error {
t, err := s.topicFromPath(r.URL.Path) t, messageID, err := s.topicAndMessageIDFromPath(r.URL.Path)
if err != nil {
return err
}
updated := messageID != ""
var m *message
if updated {
m, err = s.messageCache.Message(t.ID, messageID)
if err != nil {
return err //errors.New("message does not exist")
}
m.Updated = time.Now().Unix()
} else {
m = newDefaultMessage(t.ID, "")
}
cache, firebase, email, unifiedpush, err := s.parsePublishParams(r, v, m)
if err != nil { if err != nil {
return err return err
} }
@ -398,11 +414,6 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito
if err != nil { if err != nil {
return err return err
} }
m := newDefaultMessage(t.ID, "")
cache, firebase, email, unifiedpush, err := s.parsePublishParams(r, v, m)
if err != nil {
return err
}
if err := s.handlePublishBody(r, v, m, body, unifiedpush); err != nil { if err := s.handlePublishBody(r, v, m, body, unifiedpush); err != nil {
return err return err
} }
@ -430,8 +441,14 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito
}() }()
} }
if cache { if cache {
if err := s.messageCache.AddMessage(m); err != nil { if updated {
return err if err := s.messageCache.UpdateMessage(m); err != nil {
return err
}
} else {
if err := s.messageCache.AddMessage(m); err != nil {
return err
}
} }
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
@ -447,6 +464,10 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito
func (s *Server) parsePublishParams(r *http.Request, v *visitor, m *message) (cache bool, firebase bool, email string, unifiedpush bool, err error) { func (s *Server) parsePublishParams(r *http.Request, v *visitor, m *message) (cache bool, firebase bool, email string, unifiedpush bool, err error) {
cache = readBoolParam(r, true, "x-cache", "cache") cache = readBoolParam(r, true, "x-cache", "cache")
if !cache && m.Updated != 0 {
return false, false, "", false, errors.New("message updates must be cached")
}
// TODO more restrictions
firebase = readBoolParam(r, true, "x-firebase", "firebase") firebase = readBoolParam(r, true, "x-firebase", "firebase")
m.Title = readParam(r, "x-title", "title", "t") m.Title = readParam(r, "x-title", "title", "t")
m.Click = readParam(r, "x-click", "click") m.Click = readParam(r, "x-click", "click")
@ -888,16 +909,20 @@ func (s *Server) handleOptions(w http.ResponseWriter, _ *http.Request) error {
return nil return nil
} }
func (s *Server) topicFromPath(path string) (*topic, error) { func (s *Server) topicAndMessageIDFromPath(path string) (*topic, string, error) {
parts := strings.Split(path, "/") parts := strings.Split(path, "/")
if len(parts) < 2 { if len(parts) != 2 && len(parts) != 3 {
return nil, errHTTPBadRequestTopicInvalid return nil, "", errHTTPBadRequestTopicInvalid
} }
topics, err := s.topicsFromIDs(parts[1]) topics, err := s.topicsFromIDs(parts[1])
if err != nil { if err != nil {
return nil, err return nil, "", err
} }
return topics[0], nil messageID := ""
if len(parts) == 3 && len(parts[2]) == messageIDLength {
messageID = parts[2]
}
return topics[0], messageID, nil
} }
func (s *Server) topicsFromPath(path string) ([]*topic, string, error) { func (s *Server) topicsFromPath(path string) ([]*topic, string, error) {

View file

@ -30,7 +30,9 @@ type message struct {
Attachment *attachment `json:"attachment,omitempty"` Attachment *attachment `json:"attachment,omitempty"`
Title string `json:"title,omitempty"` Title string `json:"title,omitempty"`
Message string `json:"message,omitempty"` Message string `json:"message,omitempty"`
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
Updated int64 `json:"updated,omitempty"` // Set if updated, unix time in seconds
Deleted int64 `json:"deleted,omitempty"` // Set if deleted, unix time in seconds
} }
type attachment struct { type attachment struct {

View file

@ -17,7 +17,7 @@ import (
) )
const ( const (
randomStringCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" randomStringCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" // Update updateTopicPathRegex if changed
) )
var ( var (