// Package apprise delivers notifications through the house Apprise API. // // Apprise is the shared notification gateway (AGENTS.md ยง8). The Email Agent // uses it only to deliver the daily summary by email (FR-10). The client speaks // the standard Apprise API server shape: // // POST {base}/notify/{key} JSON {"title","body","type"} // POST {base}/notify JSON {"urls","title","body","type"} (stateless) // // A key names a stored Apprise configuration. When no key is set the client // uses the stateless form with an explicit list of Apprise URLs. package apprise import ( "bytes" "context" "encoding/json" "fmt" "net/http" "strings" "time" ) // Client posts a notification to one Apprise API server. type Client struct { BaseURL string // Key names a stored Apprise configuration. Empty means stateless mode. Key string // URLs is the stateless destination list, used when Key is empty. URLs []string // Token is sent as a bearer token when the Apprise API is protected. Token string HTTP *http.Client } // New builds a client. baseURL is the Apprise API root, e.g. // http://192.168.20.35:8210. A zero timeout defaults to ten seconds. func New(baseURL, key string, urls []string, token string, timeout time.Duration) *Client { if timeout <= 0 { timeout = 10 * time.Second } return &Client{ BaseURL: strings.TrimRight(baseURL, "/"), Key: strings.TrimSpace(key), URLs: urls, Token: token, HTTP: &http.Client{Timeout: timeout}, } } // Configured reports whether a delivery can be attempted. func (c *Client) Configured() bool { if c == nil || strings.TrimSpace(c.BaseURL) == "" { return false } return c.Key != "" || len(c.URLs) > 0 } // notifyBody is the Apprise API request body. `type` selects the notification // class; the daily summary is informational. type notifyBody struct { URLs []string `json:"urls,omitempty"` Title string `json:"title,omitempty"` Body string `json:"body"` Type string `json:"type,omitempty"` } // Send delivers one notification titled title with the given text body. func (c *Client) Send(ctx context.Context, title, body string) error { if !c.Configured() { return fmt.Errorf("apprise is not configured") } payload := notifyBody{Title: title, Body: body, Type: "info"} url := c.BaseURL + "/notify" if c.Key != "" { url += "/" + c.Key } else { payload.URLs = c.URLs } raw, err := json.Marshal(payload) if err != nil { return fmt.Errorf("encode apprise request: %w", err) } req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(raw)) if err != nil { return fmt.Errorf("build apprise request: %w", err) } req.Header.Set("Content-Type", "application/json; charset=utf-8") if c.Token != "" { req.Header.Set("Authorization", "Bearer "+c.Token) } resp, err := c.HTTP.Do(req) if err != nil { return fmt.Errorf("post apprise: %w", err) } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { return fmt.Errorf("post apprise: status %d", resp.StatusCode) } return nil }