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:
1 parent
7994ad492c
commit
955e01333d
4 files changed
+112
-10
No files matched your search
+45
-9
@@ -518,11 +518,14 @@ func (s *Server) publishHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
bad(w, err.Error())
|
||||
return
|
||||
}
|
||||
m.Event = "message"
|
||||
writeJSON(w, 200, m)
|
||||
}
|
||||
|
||||
// subscribeHTTP streams a topic as server-sent events (/sse) or JSON lines (/json).
|
||||
// ?since=<unix seconds>|all replays stored messages first.
|
||||
// ?since= replays stored messages first: "all", unix seconds, a duration such as 10m, or the id
|
||||
// of the last message the client saw. ?poll=1 sends only that replay and then ends, which is
|
||||
// how the ntfy apps catch up.
|
||||
func (s *Server) subscribeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
topic := r.PathValue("topic")
|
||||
if !validTopic(topic) {
|
||||
@@ -543,7 +546,14 @@ func (s *Server) subscribeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("X-Accel-Buffering", "no")
|
||||
bw := bufio.NewWriter(w)
|
||||
event := func(kind string) {
|
||||
b, _ := json.Marshal(map[string]any{"id": newID(), "time": s.now().Unix(), "event": kind, "topic": topic})
|
||||
fmt.Fprintf(bw, "%s\n", b)
|
||||
bw.Flush()
|
||||
fl.Flush()
|
||||
}
|
||||
write := func(m Message) bool {
|
||||
m.Event = "message"
|
||||
b, _ := json.Marshal(m)
|
||||
var err error
|
||||
if sse {
|
||||
@@ -560,21 +570,22 @@ func (s *Server) subscribeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
defer unsub()
|
||||
seen := map[string]bool{}
|
||||
if q := r.URL.Query().Get("since"); q != "" {
|
||||
since := int64(0)
|
||||
if q != "all" {
|
||||
since, _ = strconv.ParseInt(q, 10, 64)
|
||||
}
|
||||
for _, m := range s.MessagesSince(topic, since, 500) {
|
||||
for _, m := range s.replaySince(topic, q) {
|
||||
seen[m.ID] = true
|
||||
if !write(m) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
if r.URL.Query().Get("poll") == "1" {
|
||||
return
|
||||
}
|
||||
if sse {
|
||||
fmt.Fprint(bw, ": connected\n\n")
|
||||
bw.Flush()
|
||||
fl.Flush()
|
||||
} else {
|
||||
event("open")
|
||||
}
|
||||
keepalive := time.NewTicker(30 * time.Second)
|
||||
defer keepalive.Stop()
|
||||
@@ -594,15 +605,40 @@ func (s *Server) subscribeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
case <-keepalive.C:
|
||||
if sse {
|
||||
fmt.Fprint(bw, ": keepalive\n\n")
|
||||
bw.Flush()
|
||||
fl.Flush()
|
||||
} else {
|
||||
fmt.Fprint(bw, "\n")
|
||||
event("keepalive")
|
||||
}
|
||||
bw.Flush()
|
||||
fl.Flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// replaySince returns the stored messages a subscriber should see first for a ?since= value.
|
||||
func (s *Server) replaySince(topic, q string) []Message {
|
||||
if q == "all" {
|
||||
return s.MessagesSince(topic, 0, 500)
|
||||
}
|
||||
if n, err := strconv.ParseInt(q, 10, 64); err == nil {
|
||||
return s.MessagesSince(topic, n, 500)
|
||||
}
|
||||
if d, err := time.ParseDuration(q); err == nil {
|
||||
return s.MessagesSince(topic, s.now().Add(-d).Unix(), 500)
|
||||
}
|
||||
// Otherwise it is the id of the last message the client has: send what came after it.
|
||||
var at int64
|
||||
if s.st.db.QueryRow(`SELECT time FROM messages WHERE topic=? AND id=?`, topic, q).Scan(&at) != nil {
|
||||
return nil
|
||||
}
|
||||
all := s.MessagesSince(topic, at, 500)
|
||||
for i, m := range all {
|
||||
if m.ID == q {
|
||||
return all[i+1:]
|
||||
}
|
||||
}
|
||||
return all
|
||||
}
|
||||
|
||||
// ---------- alerts in ----------
|
||||
|
||||
func (s *Server) inboundAlert(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
Reference in new issue
Block a user