diff --git a/server/message_cache.go b/server/message_cache.go index 4a48ac1a..8e787db3 100644 --- a/server/message_cache.go +++ b/server/message_cache.go @@ -23,6 +23,7 @@ const ( id INTEGER PRIMARY KEY AUTOINCREMENT, mid TEXT NOT NULL, time INT NOT NULL, + updated INT NOT NULL, topic TEXT NOT NULL, message TEXT NOT NULL, title TEXT NOT NULL, @@ -43,41 +44,47 @@ const ( COMMIT; ` 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) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ` + updateMessageQuery = `UPDATE messages SET updated = ?, message = ?, title = ?, priority = ?, tags = ?, click = ? WHERE topic = ? AND mid = ?` pruneMessagesQuery = `DELETE FROM messages WHERE time < ? AND published = 1` selectRowIDFromMessageID = `SELECT id FROM messages WHERE topic = ? AND mid = ?` 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 WHERE topic = ? AND time >= ? AND published = 1 ORDER BY time, id ` 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 WHERE topic = ? AND time >= ? ORDER BY time, id ` 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 WHERE topic = ? AND id > ? AND published = 1 ORDER BY time, id ` 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 WHERE topic = ? AND (id > ? OR published = 0) ORDER BY time, id ` 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 WHERE time <= ? AND published = 0 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 = ?` selectMessagesCountQuery = `SELECT COUNT(*) FROM messages` selectMessageCountForTopicQuery = `SELECT COUNT(*) FROM messages WHERE topic = ?` @@ -232,6 +239,7 @@ func (c *messageCache) AddMessage(m *message) error { insertMessageQuery, m.ID, m.Time, + m.Updated, m.Topic, m.Message, m.Title, @@ -250,6 +258,28 @@ func (c *messageCache) AddMessage(m *message) error { 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) { if since.IsNone() { return make([]*message, 0), nil @@ -393,16 +423,31 @@ func (c *messageCache) AttachmentsExpired() ([]string, error) { 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) { defer rows.Close() messages := make([]*message, 0) for rows.Next() { - var timestamp, attachmentSize, attachmentExpires int64 + var timestamp, updated, attachmentSize, attachmentExpires int64 var priority int var id, topic, msg, title, tagsStr, click, attachmentName, attachmentType, attachmentURL, attachmentOwner, encoding string err := rows.Scan( &id, ×tamp, + &updated, &topic, &msg, &title, @@ -438,6 +483,7 @@ func readMessages(rows *sql.Rows) ([]*message, error) { messages = append(messages, &message{ ID: id, Time: timestamp, + Updated: updated, Event: messageEvent, Topic: topic, Message: msg, diff --git a/server/server.go b/server/server.go index 0e81cb63..0347e266 100644 --- a/server/server.go +++ b/server/server.go @@ -55,9 +55,10 @@ type handleFunc func(http.ResponseWriter, *http.Request, *visitor) error var ( // If changed, don't forget to update Android App and auth_sqlite.go - 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! - externalTopicPathRegex = regexp.MustCompile(`^/[^/]+\.[^/]+/[-_A-Za-z0-9]{1,64}$`) // Extended topic path, for web-app, e.g. /example.com/mytopic + 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! + 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$`) 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$`) @@ -279,7 +280,7 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit return s.handleOptions(w, r) } 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) - } 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) } else if r.Method == http.MethodGet && publishPathRegex.MatchString(r.URL.Path) { 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 { - 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 { return err } @@ -398,11 +414,6 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito if err != nil { 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 { return err } @@ -430,8 +441,14 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito }() } if cache { - if err := s.messageCache.AddMessage(m); err != nil { - return err + if updated { + 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") @@ -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) { 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") m.Title = readParam(r, "x-title", "title", "t") m.Click = readParam(r, "x-click", "click") @@ -888,16 +909,20 @@ func (s *Server) handleOptions(w http.ResponseWriter, _ *http.Request) error { return nil } -func (s *Server) topicFromPath(path string) (*topic, error) { +func (s *Server) topicAndMessageIDFromPath(path string) (*topic, string, error) { parts := strings.Split(path, "/") - if len(parts) < 2 { - return nil, errHTTPBadRequestTopicInvalid + if len(parts) != 2 && len(parts) != 3 { + return nil, "", errHTTPBadRequestTopicInvalid } topics, err := s.topicsFromIDs(parts[1]) 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) { diff --git a/server/types.go b/server/types.go index 6594f050..d003df2c 100644 --- a/server/types.go +++ b/server/types.go @@ -30,7 +30,9 @@ type message struct { Attachment *attachment `json:"attachment,omitempty"` Title string `json:"title,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 { diff --git a/util/util.go b/util/util.go index e05736fc..90ba4e08 100644 --- a/util/util.go +++ b/util/util.go @@ -17,7 +17,7 @@ import ( ) const ( - randomStringCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + randomStringCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" // Update updateTopicPathRegex if changed ) var (