diff --git a/docs/releases.md b/docs/releases.md index 69222b82..7a1e9ede 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1378,6 +1378,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release **Features:** * Add username/password auth to email publishing ([#1164](https://github.com/binwiederhier/ntfy/pull/1164), thanks to [@bishtawi](https://github.com/bishtawi)) +* Add `latest` subscription param for grabbing just the most recent message (thanks to [@wunter8](https://github.com/wunter8)) **Bug fixes + maintenance:** diff --git a/docs/subscribe/api.md b/docs/subscribe/api.md index 3f1c0e81..2387c663 100644 --- a/docs/subscribe/api.md +++ b/docs/subscribe/api.md @@ -257,6 +257,14 @@ curl -s "ntfy.sh/mytopic/json?since=1645970742" curl -s "ntfy.sh/mytopic/json?since=nFS3knfcQ1xe" ``` +### Fetch latest message +If you only want the most recent message sent to a topic and do not have a message ID or timestamp to use with +`since=`, you can use `since=latest` to grab the most recent message from the cache for a particular topic. + +``` +curl -s "ntfy.sh/mytopic/json?poll=1&since=latest" +``` + ### Fetch scheduled messages Messages that are [scheduled to be delivered](../publish.md#scheduled-delivery) at a later date are not typically returned when subscribing via the API, which makes sense, because after all, the messages have technically not been diff --git a/server/message_cache.go b/server/message_cache.go index 4f677816..e314ace3 100644 --- a/server/message_cache.go +++ b/server/message_cache.go @@ -99,6 +99,13 @@ const ( WHERE topic = ? AND (id > ? OR published = 0) ORDER BY time, id ` + selectMessagesLatestQuery = ` + SELECT mid, 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 + FROM messages + WHERE topic = ? AND published = 1 + ORDER BY time DESC, id DESC + LIMIT 1 + ` selectMessagesDueQuery = ` SELECT mid, 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 FROM messages @@ -416,6 +423,8 @@ func (c *messageCache) addMessages(ms []*message) error { func (c *messageCache) Messages(topic string, since sinceMarker, scheduled bool) ([]*message, error) { if since.IsNone() { return make([]*message, 0), nil + } else if since.IsLatest() { + return c.messagesLatest(topic) } else if since.IsID() { return c.messagesSinceID(topic, since, scheduled) } @@ -462,6 +471,14 @@ func (c *messageCache) messagesSinceID(topic string, since sinceMarker, schedule return readMessages(rows) } +func (c *messageCache) messagesLatest(topic string) ([]*message, error) { + rows, err := c.db.Query(selectMessagesLatestQuery, topic) + if err != nil { + return nil, err + } + return readMessages(rows) +} + func (c *messageCache) MessagesDue() ([]*message, error) { rows, err := c.db.Query(selectMessagesDueQuery, time.Now().Unix()) if err != nil { diff --git a/server/message_cache_test.go b/server/message_cache_test.go index 589ecc42..778f28fe 100644 --- a/server/message_cache_test.go +++ b/server/message_cache_test.go @@ -66,6 +66,11 @@ func testCacheMessages(t *testing.T, c *messageCache) { require.Equal(t, 1, len(messages)) require.Equal(t, "my other message", messages[0].Message) + // mytopic: latest + messages, _ = c.Messages("mytopic", sinceLatestMessage, false) + require.Equal(t, 1, len(messages)) + require.Equal(t, "my other message", messages[0].Message) + // example: count counts, err = c.MessageCounts() require.Nil(t, err) diff --git a/server/server.go b/server/server.go index ee2da76a..9a7f9d43 100644 --- a/server/server.go +++ b/server/server.go @@ -1556,8 +1556,8 @@ func (s *Server) sendOldMessages(topics []*topic, since sinceMarker, scheduled b // parseSince returns a timestamp identifying the time span from which cached messages should be received. // -// Values in the "since=..." parameter can be either a unix timestamp or a duration (e.g. 12h), or -// "all" for all messages. +// Values in the "since=..." parameter can be either a unix timestamp or a duration (e.g. 12h), +// "all" for all messages, or "latest" for the most recent message for a topic func parseSince(r *http.Request, poll bool) (sinceMarker, error) { since := readParam(r, "x-since", "since", "si") @@ -1569,6 +1569,8 @@ func parseSince(r *http.Request, poll bool) (sinceMarker, error) { return sinceNoMessages, nil } else if since == "all" { return sinceAllMessages, nil + } else if since == "latest" { + return sinceLatestMessage, nil } else if since == "none" { return sinceNoMessages, nil } diff --git a/server/server_test.go b/server/server_test.go index 75379f8f..7fbbeb85 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -594,6 +594,11 @@ func TestServer_PublishAndPollSince(t *testing.T) { require.Equal(t, 1, len(messages)) require.Equal(t, "test 2", messages[0].Message) + response = request(t, s, "GET", "/mytopic/json?poll=1&since=latest", "", nil) + messages = toMessages(t, response.Body.String()) + require.Equal(t, 1, len(messages)) + require.Equal(t, "test 2", messages[0].Message) + response = request(t, s, "GET", "/mytopic/json?poll=1&since=INVALID", "", nil) require.Equal(t, 40008, toHTTPError(t, response.Body.String()).Code) } diff --git a/server/types.go b/server/types.go index fb08fb05..c6bdb4d1 100644 --- a/server/types.go +++ b/server/types.go @@ -169,8 +169,12 @@ func (t sinceMarker) IsNone() bool { return t == sinceNoMessages } +func (t sinceMarker) IsLatest() bool { + return t == sinceLatestMessage +} + func (t sinceMarker) IsID() bool { - return t.id != "" + return t.id != "" && t.id != "latest" } func (t sinceMarker) Time() time.Time { @@ -182,8 +186,9 @@ func (t sinceMarker) ID() string { } var ( - sinceAllMessages = sinceMarker{time.Unix(0, 0), ""} - sinceNoMessages = sinceMarker{time.Unix(1, 0), ""} + sinceAllMessages = sinceMarker{time.Unix(0, 0), ""} + sinceNoMessages = sinceMarker{time.Unix(1, 0), ""} + sinceLatestMessage = sinceMarker{time.Unix(0, 0), "latest"} ) type queryFilter struct {