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
+52
View File
@@ -0,0 +1,52 @@
package ntfy
import (
"net/url"
"strings"
"lazyntfy/internal/config"
)
// Group is a set of topics that share an origin and credentials and can
// therefore share one JSON stream.
type Group struct {
Origin string
Names []string
Auth config.Auth
}
// URL is the /json subscription endpoint for this group.
func (g Group) URL(since string) string {
parts := make([]string, len(g.Names))
for i, name := range g.Names {
parts[i] = url.PathEscape(name)
}
endpoint := g.Origin + "/" + strings.Join(parts, ",") + "/json"
if since == "" {
return endpoint
}
q := url.Values{}
q.Set("since", since)
return endpoint + "?" + q.Encode()
}
// PublishURL is the POST endpoint for one topic.
func PublishURL(origin, name string) string {
return origin + "/" + url.PathEscape(name)
}
// GroupSubs buckets subscriptions that can share a connection.
func GroupSubs(subs []config.Sub) []Group {
var out []Group
index := map[string]int{}
for _, sub := range subs {
key := sub.Origin + "\n" + sub.Auth.Token + "\n" + sub.Auth.User + "\n" + sub.Auth.Password
if i, ok := index[key]; ok {
out[i].Names = append(out[i].Names, sub.Name)
continue
}
index[key] = len(out)
out = append(out, Group{Origin: sub.Origin, Names: []string{sub.Name}, Auth: sub.Auth})
}
return out
}