152 lines
3.8 KiB
Go
152 lines
3.8 KiB
Go
package ntfy
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"encoding/json"
|
||
|
|
"fmt"
|
||
|
|
"io"
|
||
|
|
"net/http"
|
||
|
|
"strings"
|
||
|
|
|
||
|
|
"lazyntfy/internal/config"
|
||
|
|
)
|
||
|
|
|
||
|
|
type accountBody struct {
|
||
|
|
SyncTopic string `json:"sync_topic"`
|
||
|
|
Subscriptions []struct {
|
||
|
|
BaseURL string `json:"base_url"`
|
||
|
|
Topic string `json:"topic"`
|
||
|
|
DisplayName *string `json:"display_name"`
|
||
|
|
} `json:"subscriptions"`
|
||
|
|
Reservations []struct {
|
||
|
|
Topic string `json:"topic"`
|
||
|
|
} `json:"reservations"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type grantBody struct {
|
||
|
|
Topic string `json:"topic"`
|
||
|
|
Permission string `json:"permission"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type userBody struct {
|
||
|
|
Grants []grantBody `json:"grants"`
|
||
|
|
}
|
||
|
|
|
||
|
|
// AccountTopics lists topics visible to this login: account subscriptions,
|
||
|
|
// reserved topics, and ACL grants when the token can read /v1/users.
|
||
|
|
// Wildcard patterns are skipped. Credentials are not included in errors.
|
||
|
|
func (c *Client) AccountTopics(ctx context.Context, host string, auth config.Auth) ([]config.Sub, error) {
|
||
|
|
host = strings.TrimRight(strings.TrimSpace(host), "/")
|
||
|
|
if host == "" || (auth.Token == "" && auth.User == "") {
|
||
|
|
return nil, nil
|
||
|
|
}
|
||
|
|
body, status, err := c.get(ctx, host+"/v1/account", auth)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
if status == http.StatusUnauthorized || status == http.StatusForbidden {
|
||
|
|
return nil, fmt.Errorf("HTTP %d: not authorized to list topics", status)
|
||
|
|
}
|
||
|
|
if status < 200 || status >= 300 {
|
||
|
|
return nil, fmt.Errorf("HTTP %d from /v1/account", status)
|
||
|
|
}
|
||
|
|
var account accountBody
|
||
|
|
if err := json.Unmarshal(body, &account); err != nil {
|
||
|
|
return nil, fmt.Errorf("account response: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
var subs []config.Sub
|
||
|
|
seen := map[string]bool{}
|
||
|
|
add := func(base, topic, display string) {
|
||
|
|
topic = strings.TrimSpace(topic)
|
||
|
|
if topic == "" || strings.Contains(topic, "*") || topic == account.SyncTopic {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
originHost := host
|
||
|
|
if strings.TrimSpace(base) != "" {
|
||
|
|
originHost = strings.TrimRight(base, "/")
|
||
|
|
}
|
||
|
|
origin, name, err := config.Resolve(topic, originHost)
|
||
|
|
if err != nil {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
key := origin + "\n" + name
|
||
|
|
if seen[key] {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
seen[key] = true
|
||
|
|
subs = append(subs, config.Sub{
|
||
|
|
Origin: origin,
|
||
|
|
Name: name,
|
||
|
|
Display: strings.TrimSpace(display),
|
||
|
|
Auth: auth,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, sub := range account.Subscriptions {
|
||
|
|
display := ""
|
||
|
|
if sub.DisplayName != nil {
|
||
|
|
display = *sub.DisplayName
|
||
|
|
}
|
||
|
|
add(sub.BaseURL, sub.Topic, display)
|
||
|
|
}
|
||
|
|
for _, res := range account.Reservations {
|
||
|
|
add("", res.Topic, "")
|
||
|
|
}
|
||
|
|
|
||
|
|
usersBody, usersStatus, err := c.get(ctx, host+"/v1/users", auth)
|
||
|
|
if err != nil || usersStatus < 200 || usersStatus >= 300 {
|
||
|
|
return subs, nil
|
||
|
|
}
|
||
|
|
for _, grant := range parseGrants(usersBody) {
|
||
|
|
perm := strings.ToLower(grant.Permission)
|
||
|
|
if perm == "deny-all" || perm == "deny" || perm == "none" {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
add("", grant.Topic, "")
|
||
|
|
}
|
||
|
|
return subs, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func parseGrants(body []byte) []grantBody {
|
||
|
|
var wrapped struct {
|
||
|
|
Users []userBody `json:"users"`
|
||
|
|
}
|
||
|
|
if err := json.Unmarshal(body, &wrapped); err == nil && wrapped.Users != nil {
|
||
|
|
return flattenGrants(wrapped.Users)
|
||
|
|
}
|
||
|
|
var list []userBody
|
||
|
|
if err := json.Unmarshal(body, &list); err != nil {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
return flattenGrants(list)
|
||
|
|
}
|
||
|
|
|
||
|
|
func flattenGrants(users []userBody) []grantBody {
|
||
|
|
var out []grantBody
|
||
|
|
for _, user := range users {
|
||
|
|
out = append(out, user.Grants...)
|
||
|
|
}
|
||
|
|
return out
|
||
|
|
}
|
||
|
|
|
||
|
|
func (c *Client) get(ctx context.Context, endpoint string, auth config.Auth) ([]byte, int, error) {
|
||
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
||
|
|
if err != nil {
|
||
|
|
return nil, 0, err
|
||
|
|
}
|
||
|
|
req.Header.Set("User-Agent", userAgent)
|
||
|
|
req.Header.Set("Accept", "application/json")
|
||
|
|
auth.Apply(req)
|
||
|
|
resp, err := c.http().Do(req)
|
||
|
|
if err != nil {
|
||
|
|
return nil, 0, err
|
||
|
|
}
|
||
|
|
defer resp.Body.Close()
|
||
|
|
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||
|
|
if err != nil {
|
||
|
|
return nil, resp.StatusCode, err
|
||
|
|
}
|
||
|
|
return body, resp.StatusCode, nil
|
||
|
|
}
|