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:
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user