Make the topic feeds work with the ntfy apps

The JSON stream now carries the event field the apps look for, sends open and keepalive
events, and supports ?poll=1 and ?since= with a duration or a message id, which is how the
apps catch up. Adds a walkthrough for the phone app to the README.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
VeillaxandClaude Sonnet 5.5 committed 2026-10-27 04:22:53 -06:00
1 parent 7994ad492c
commit 955e01333d
4 files changed
+112 -10

No files matched your search

+50
View File
@@ -453,6 +453,56 @@ func TestAPIAuthSetupAndTopics(t *testing.T) {
t.Fatal("subscriber got nothing")
}
// The ntfy apps catch up with ?poll=1&since=<last id> and ignore lines without "event".
var ids []string
for _, body := range []string{"one", "two", "three"} {
r, _ := http.NewRequest("POST", ts.URL+"/phone", strings.NewReader(body))
r.Header.Set("Authorization", "Bearer "+key["token"])
resp, err := http.DefaultClient.Do(r)
if err != nil || resp.StatusCode != 200 {
t.Fatalf("publish %s: %v %v", body, err, resp)
}
var pm Message
json.NewDecoder(resp.Body).Decode(&pm)
resp.Body.Close()
if pm.Event != "message" {
t.Fatalf("publish response event: %q", pm.Event)
}
ids = append(ids, pm.ID)
}
pollLines := func(query string) []Message {
req, _ := http.NewRequest("GET", ts.URL+"/phone/json?poll=1&"+query, nil)
req.Header.Set("Authorization", "Bearer "+key["token"])
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
var got []Message
sc := bufio.NewScanner(resp.Body)
for sc.Scan() {
var m Message
json.Unmarshal(sc.Bytes(), &m)
if m.Event != "message" {
t.Fatalf("poll line without event: %s", sc.Text())
}
got = append(got, m)
}
return got
}
if got := pollLines("since=all"); len(got) != 3 {
t.Fatalf("poll since=all: %d messages", len(got))
}
if got := pollLines("since=" + ids[0]); len(got) != 2 || got[0].Message != "two" {
t.Fatalf("poll since=<id>: %+v", got)
}
if got := pollLines("since=" + ids[2]); len(got) != 0 {
t.Fatalf("poll since=<last id>: %+v", got)
}
if got := pollLines("since=10m"); len(got) != 3 {
t.Fatalf("poll since=10m: %d messages", len(got))
}
// Reserved topics are refused.
for _, reserved := range []string{"/api", "/status"} {
r, _ := http.NewRequest("POST", ts.URL+reserved, strings.NewReader("x"))