Add Unraid and Gotify notifications

This commit is contained in:
Mikei386
2026-06-14 13:50:02 +02:00
parent b7fe6ac86f
commit 1af23831f4
15 changed files with 232 additions and 44 deletions
+41 -16
View File
@@ -3,6 +3,7 @@ package notify
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
@@ -16,8 +17,10 @@ import (
type SecretGetter interface{ Get(string) (string, error) }
type Sender struct {
Secrets SecretGetter
Client *http.Client
Secrets SecretGetter
Client *http.Client
NotifyPath string
RunCommand func(context.Context, string, ...string) error
}
func (s *Sender) Send(ctx context.Context, target model.NotificationTarget, title, message, severity string) error {
@@ -27,25 +30,47 @@ func (s *Sender) Send(ctx context.Context, target model.NotificationTarget, titl
switch target.Type {
case "unraid":
importance := "normal"
if severity == "error" {
if severity == "failed" || severity == "error" {
importance = "alert"
} else if severity == "warning" {
importance = "warning"
}
return exec.CommandContext(ctx, "/usr/local/emhttp/webGui/scripts/notify", "-e", "Backupper", "-s", title, "-d", message, "-i", importance).Run()
case "ntfy":
endpoint := strings.TrimRight(target.URL, "/") + "/" + url.PathEscape(target.Topic)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewBufferString(message))
path := s.NotifyPath
if path == "" {
path = "/usr/local/emhttp/webGui/scripts/notify"
}
args := []string{"-e", "Backupper", "-s", title, "-d", message, "-i", importance, "-l", "/Tools/Backupper"}
if s.RunCommand != nil {
return s.RunCommand(ctx, path, args...)
}
return exec.CommandContext(ctx, path, args...).Run()
case "gotify":
token, err := s.Secrets.Get(target.TokenRef)
if err != nil {
return err
}
req.Header.Set("Title", title)
req.Header.Set("Tags", "floppy_disk")
if target.TokenRef != "" {
token, err := s.Secrets.Get(target.TokenRef)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+token)
endpoint, err := url.Parse(strings.TrimRight(target.URL, "/") + "/message")
if err != nil {
return err
}
query := endpoint.Query()
query.Set("token", token)
endpoint.RawQuery = query.Encode()
priority := 2
if severity == "warning" {
priority = 5
} else if severity == "failed" || severity == "error" {
priority = 8
}
payload, err := json.Marshal(map[string]any{"title": title, "message": message, "priority": priority})
if err != nil {
return err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), bytes.NewReader(payload))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
client := s.Client
if client == nil {
client = &http.Client{Timeout: 15 * time.Second}
@@ -56,7 +81,7 @@ func (s *Sender) Send(ctx context.Context, target model.NotificationTarget, titl
}
defer response.Body.Close()
if response.StatusCode < 200 || response.StatusCode >= 300 {
return fmt.Errorf("ntfy returned %s", response.Status)
return fmt.Errorf("Gotify returned %s", response.Status)
}
return nil
default:
+63
View File
@@ -0,0 +1,63 @@
package notify
import (
"context"
"encoding/json"
"io"
"net/http"
"reflect"
"strings"
"testing"
"github.com/backupper-unraid/backupper/internal/model"
)
type testSecrets map[string]string
func (s testSecrets) Get(id string) (string, error) { return s[id], nil }
type roundTripFunc func(*http.Request) (*http.Response, error)
func (f roundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) { return f(request) }
func TestSendGotify(t *testing.T) {
var body map[string]any
client := &http.Client{Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
if r.URL.Path != "/message" || r.URL.Query().Get("token") != "secret-token" {
t.Errorf("unexpected request URL %s", r.URL.String())
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
return nil, err
}
return &http.Response{StatusCode: http.StatusOK, Status: "200 OK", Body: io.NopCloser(strings.NewReader("{}")), Header: make(http.Header)}, nil
})}
sender := &Sender{Secrets: testSecrets{"gotify-token": "secret-token"}, Client: client}
target := model.NotificationTarget{Enabled: true, Type: "gotify", URL: "https://gotify.example.test", TokenRef: "gotify-token"}
if err := sender.Send(context.Background(), target, "Backup failed", "repository unavailable", "failed"); err != nil {
t.Fatal(err)
}
if body["title"] != "Backup failed" || body["message"] != "repository unavailable" || body["priority"] != float64(8) {
t.Fatalf("unexpected Gotify payload %#v", body)
}
}
func TestSendUnraid(t *testing.T) {
var command string
var args []string
sender := &Sender{RunCommand: func(_ context.Context, name string, values ...string) error {
command, args = name, values
return nil
}}
target := model.NotificationTarget{Enabled: true, Type: "unraid"}
if err := sender.Send(context.Background(), target, "Backup warning", "completed with warnings", "warning"); err != nil {
t.Fatal(err)
}
if command != "/usr/local/emhttp/webGui/scripts/notify" {
t.Fatalf("unexpected command %q", command)
}
want := []string{"-e", "Backupper", "-s", "Backup warning", "-d", "completed with warnings", "-i", "warning", "-l", "/Tools/Backupper"}
if !reflect.DeepEqual(args, want) {
t.Fatalf("arguments = %#v, want %#v", args, want)
}
}