115 lines
3.2 KiB
Go
115 lines
3.2 KiB
Go
package main
|
|
|
|
import (
|
|
"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
|
|
token string
|
|
clickBase string
|
|
httpClient *http.Client
|
|
}
|
|
|
|
// newNtfyNotifierFromEnv builds a notifier from NTFY_URL / NTFY_TOPIC /
|
|
// 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
|
|
}
|
|
|
|
n := &ntfyNotifier{
|
|
url: strings.TrimRight(base, "/") + "/" + strings.TrimLeft(topic, "/"),
|
|
token: strings.TrimSpace(os.Getenv("NTFY_TOKEN")),
|
|
clickBase: strings.TrimRight(strings.TrimSpace(os.Getenv("PUBLIC_BASE_URL")), "/"),
|
|
httpClient: &http.Client{Timeout: 10 * time.Second},
|
|
}
|
|
|
|
auth := "none"
|
|
if n.token != "" {
|
|
auth = "access token"
|
|
}
|
|
log.Printf("ntfy notifications enabled: %s (auth: %s)", n.url, auth)
|
|
|
|
return n
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}()
|
|
}
|
|
|
|
// sendTest publishes a test notification and reports the outcome synchronously,
|
|
// so the admin UI can surface configuration problems.
|
|
func (n *ntfyNotifier) sendTest() error {
|
|
return n.send(UserReport{
|
|
Callsign: "TEST",
|
|
ReportType: "other",
|
|
Message: "Test notification from DMRmap admin.",
|
|
})
|
|
}
|
|
|
|
func (n *ntfyNotifier) send(rep UserReport) error {
|
|
body := reportTypeLabel(rep.ReportType)
|
|
if rep.Message != "" {
|
|
body += "\n\n" + rep.Message
|
|
}
|
|
if rep.RepeaterID > 0 {
|
|
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 != "" && rep.RepeaterID > 0 {
|
|
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 applies the ntfy access token. It is a bearer credential: ntfy does
|
|
// NOT accept it as the password half of a username:password pair (that yields
|
|
// HTTP 401), only as a bearer token or as basic auth with an empty username.
|
|
func (n *ntfyNotifier) setAuth(req *http.Request) {
|
|
if n.token != "" {
|
|
req.Header.Set("Authorization", "Bearer "+n.token)
|
|
}
|
|
}
|