package main import ( "encoding/base64" "fmt" "log" "net/http" "os" "strings" "time" ) // ntfyNotifier pushes user reports to an ntfy server. It is nil when the // required environment variables are not set, in which case notify is a no-op. type ntfyNotifier struct { url string // full topic URL, e.g. https://ntfy.example.com/dmrmap user string token string clickBase string httpClient *http.Client } // newNtfyNotifierFromEnv builds a notifier from NTFY_URL / NTFY_TOPIC / // NTFY_USER / NTFY_TOKEN. Returns nil when notifications are not configured. func newNtfyNotifierFromEnv() *ntfyNotifier { base := strings.TrimSpace(os.Getenv("NTFY_URL")) topic := strings.TrimSpace(os.Getenv("NTFY_TOPIC")) if base == "" || topic == "" { log.Println("ntfy notifications disabled (set NTFY_URL and NTFY_TOPIC to enable)") return nil } return &ntfyNotifier{ url: strings.TrimRight(base, "/") + "/" + strings.TrimLeft(topic, "/"), user: strings.TrimSpace(os.Getenv("NTFY_USER")), token: strings.TrimSpace(os.Getenv("NTFY_TOKEN")), clickBase: strings.TrimRight(strings.TrimSpace(os.Getenv("PUBLIC_BASE_URL")), "/"), httpClient: &http.Client{Timeout: 10 * time.Second}, } } // notifyReport sends a push notification for a new user report. It never // blocks the caller and never fails the request that triggered it. func (n *ntfyNotifier) notifyReport(rep UserReport) { if n == nil { return } go func() { if err := n.send(rep); err != nil { log.Printf("ntfy: failed to publish report %d: %v", rep.ID, err) } }() } func (n *ntfyNotifier) send(rep UserReport) error { body := reportTypeLabel(rep.ReportType) if rep.Message != "" { body += "\n\n" + rep.Message } body += fmt.Sprintf("\n\nRepeater ID: %d", rep.RepeaterID) req, err := http.NewRequest(http.MethodPost, n.url, strings.NewReader(body)) if err != nil { return err } title := fmt.Sprintf("Report: %s", rep.Callsign) if rep.Callsign == "" { title = fmt.Sprintf("Report: #%d", rep.RepeaterID) } req.Header.Set("Title", title) req.Header.Set("Tags", "warning,radio") req.Header.Set("Priority", "default") if n.clickBase != "" { req.Header.Set("Click", fmt.Sprintf("%s/admin/#%d", n.clickBase, rep.RepeaterID)) } n.setAuth(req) resp, err := n.httpClient.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode > 299 { return fmt.Errorf("ntfy returned HTTP %d", resp.StatusCode) } return nil } // setAuth uses basic auth when a username is configured (ntfy accepts an // access token as the password), and bearer auth for a token on its own. func (n *ntfyNotifier) setAuth(req *http.Request) { switch { case n.user != "" && n.token != "": creds := base64.StdEncoding.EncodeToString([]byte(n.user + ":" + n.token)) req.Header.Set("Authorization", "Basic "+creds) case n.token != "": req.Header.Set("Authorization", "Bearer "+n.token) } }