feat(ntfy): add the HTTP client, stream manager, and account discovery

Speak the ntfy JSON API directly: subscribe, publish, group topics that share a server and credentials, reconnect with backoff and a since cursor, and list account subscriptions, reservations, and ACL grants.
This commit is contained in:
ahp
2026-10-11 12:04:53 +03:30
parent 387b3c77f3
commit cd19d4c665
11 changed files with 1034 additions and 0 deletions
+72
View File
@@ -0,0 +1,72 @@
package ntfy
import (
"bytes"
"encoding/json"
"fmt"
"io"
)
// Event is one NDJSON object from GET /topic/json.
type Event struct {
ID string `json:"id"`
Time int64 `json:"time"`
Expires int64 `json:"expires,omitempty"`
Event string `json:"event"`
Topic string `json:"topic"`
Message string `json:"message"`
Title string `json:"title"`
Tags []string `json:"tags"`
Priority int `json:"priority"`
Click string `json:"click"`
SequenceID string `json:"sequence_id,omitempty"`
Attachment *Attachment `json:"attachment,omitempty"`
Actions json.RawMessage `json:"actions,omitempty"`
}
// Attachment describes a file linked from a message.
type Attachment struct {
Name string `json:"name"`
URL string `json:"url"`
Type string `json:"type,omitempty"`
Size int64 `json:"size,omitempty"`
Expires int64 `json:"expires,omitempty"`
}
// ParseLine decodes one NDJSON line. ok is false for blank lines.
func ParseLine(line []byte) (Event, bool, error) {
line = bytes.TrimSpace(line)
if len(line) == 0 {
return Event{}, false, nil
}
var ev Event
if err := json.Unmarshal(line, &ev); err != nil {
return Event{}, false, err
}
if ev.Event == "" {
return Event{}, false, fmt.Errorf("missing event")
}
return ev, true, nil
}
// ReadLines yields each decoded event until EOF or a read error.
// Malformed JSON is reported and skipped so one bad line does not kill the stream.
func ReadLines(r io.Reader, yield func(Event) error, onBad func(error)) error {
sc := newScanner(r)
for sc.Scan() {
ev, ok, err := ParseLine(sc.Bytes())
if err != nil {
if onBad != nil {
onBad(err)
}
continue
}
if !ok {
continue
}
if err := yield(ev); err != nil {
return err
}
}
return sc.Err()
}