mirror of
				https://github.com/binwiederhier/ntfy.git
				synced 2025-10-31 13:02:24 +01:00 
			
		
		
		
	WIP: Update messages
This commit is contained in:
		
							parent
							
								
									b409c89d3b
								
							
						
					
					
						commit
						8848829dfa
					
				
					 4 changed files with 100 additions and 27 deletions
				
			
		|  | @ -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, | ||||
|  |  | |||
|  | @ -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) { | ||||
|  |  | |||
|  | @ -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 { | ||||
|  |  | |||
|  | @ -17,7 +17,7 @@ import ( | |||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	randomStringCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" | ||||
| 	randomStringCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" // Update updateTopicPathRegex if changed | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Philipp Heckel
						Philipp Heckel