- ntfy client supports HTTP Basic auth (username/password) for servers with access control; [ntfy] config gains username/password. Scheduler + Test Ntfy use it instead of the old settings-only bearer token. - Settings page no longer has credential inputs (Apify/eBay/ntfy/Resend); they are managed in config.toml. Read-only status + Test buttons remain. - Sidebar gains a Sign out button; in forward-auth mode it routes through the Authentik outpost sign_out so the IdP session is cleared.
117 lines
2.9 KiB
Go
117 lines
2.9 KiB
Go
package ntfy
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type Client struct {
|
|
BaseURL string
|
|
Token string
|
|
// Username/Password take precedence over Token when set, using HTTP Basic
|
|
// auth (for ntfy servers that authenticate with user accounts rather than
|
|
// access tokens).
|
|
Username string
|
|
Password string
|
|
HTTP *http.Client
|
|
}
|
|
|
|
func New(baseURL string) *Client {
|
|
return &Client{
|
|
BaseURL: strings.TrimRight(baseURL, "/"),
|
|
HTTP: &http.Client{Timeout: 15 * time.Second},
|
|
}
|
|
}
|
|
|
|
// NewWithToken returns a ntfy Client with bearer-token auth set. Use this
|
|
// when the ntfy server requires token authentication.
|
|
func NewWithToken(baseURL, token string) *Client {
|
|
c := New(baseURL)
|
|
c.Token = strings.TrimSpace(token)
|
|
return c
|
|
}
|
|
|
|
// NewWithBasicAuth returns a ntfy Client that authenticates publishes with an
|
|
// ntfy user account (HTTP Basic auth). Use this for servers configured with
|
|
// auth-default-access: deny-all and per-user topic ACLs.
|
|
func NewWithBasicAuth(baseURL, username, password string) *Client {
|
|
c := New(baseURL)
|
|
c.Username = strings.TrimSpace(username)
|
|
c.Password = password
|
|
return c
|
|
}
|
|
|
|
type Notification struct {
|
|
Topic string
|
|
Title string
|
|
Message string
|
|
Priority string
|
|
Tags []string
|
|
Click string
|
|
}
|
|
|
|
// Send publishes to ntfy using the topic-path + header style
|
|
// (POST {base}/{topic} with metadata in HTTP headers and the message as the
|
|
// raw body). This is the most broadly compatible ntfy publish method —
|
|
// works on every ntfy version including self-hosted, and on any path layout
|
|
// the server is mounted under.
|
|
func (c *Client) Send(ctx context.Context, n Notification) error {
|
|
if c.BaseURL == "" {
|
|
return fmt.Errorf("ntfy base_url not configured")
|
|
}
|
|
if n.Topic == "" {
|
|
return fmt.Errorf("ntfy topic required")
|
|
}
|
|
url := c.BaseURL + "/" + strings.TrimLeft(n.Topic, "/")
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, strings.NewReader(n.Message))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Content-Type", "text/plain; charset=utf-8")
|
|
if c.Username != "" {
|
|
req.SetBasicAuth(c.Username, c.Password)
|
|
} else if c.Token != "" {
|
|
req.Header.Set("Authorization", "Bearer "+c.Token)
|
|
}
|
|
if n.Title != "" {
|
|
req.Header.Set("Title", n.Title)
|
|
}
|
|
if n.Priority != "" {
|
|
req.Header.Set("Priority", n.Priority)
|
|
}
|
|
if len(n.Tags) > 0 {
|
|
req.Header.Set("Tags", strings.Join(n.Tags, ","))
|
|
}
|
|
if n.Click != "" {
|
|
req.Header.Set("Click", n.Click)
|
|
}
|
|
authMode := "none"
|
|
switch {
|
|
case c.Username != "":
|
|
authMode = "basic"
|
|
case c.Token != "":
|
|
authMode = "bearer"
|
|
}
|
|
slog.Info("ntfy publish",
|
|
"url", url,
|
|
"topic", n.Topic,
|
|
"auth", authMode,
|
|
"user", c.Username,
|
|
)
|
|
resp, err := c.HTTP.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("ntfy POST: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode >= 300 {
|
|
b, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
|
|
return fmt.Errorf("ntfy returned %d: %s", resp.StatusCode, string(b))
|
|
}
|
|
return nil
|
|
}
|