128 lines
2.7 KiB
Go
128 lines
2.7 KiB
Go
package ntfy
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/url"
|
|
"time"
|
|
|
|
"coda/internal/config"
|
|
"coda/internal/models"
|
|
)
|
|
|
|
const (
|
|
requestTimeout = 10 * time.Second
|
|
maxResponseBytes = 4096
|
|
)
|
|
|
|
type payload struct {
|
|
Topic string `json:"topic"`
|
|
Title string `json:"title"`
|
|
Message string `json:"message"`
|
|
Priority int `json:"priority"`
|
|
Tags []string `json:"tags"`
|
|
}
|
|
|
|
type Client struct {
|
|
cfg *config.Service
|
|
hc *http.Client
|
|
}
|
|
|
|
func New(cfg *config.Service) *Client {
|
|
return &Client{
|
|
cfg: cfg,
|
|
hc: &http.Client{Timeout: requestTimeout},
|
|
}
|
|
}
|
|
|
|
func (c *Client) Notify(ctx context.Context, job *models.Job) error {
|
|
cfg := c.cfg.Get()
|
|
|
|
if cfg.DryRun {
|
|
slog.Info("dry run: would send ntfy notification", "job_id", job.ID, "status", job.Status)
|
|
return nil
|
|
}
|
|
|
|
if !cfg.NtfyEnabled {
|
|
slog.Debug("ntfy disabled, skipping notification", "job_id", job.ID)
|
|
return nil
|
|
}
|
|
|
|
if cfg.NtfyURL == "" || cfg.NtfyTopic == "" {
|
|
slog.Debug("ntfy URL or topic not configured, skipping notification",
|
|
"job_id", job.ID,
|
|
"ntfy_url", cfg.NtfyURL,
|
|
"has_topic", cfg.NtfyTopic != "",
|
|
)
|
|
return nil
|
|
}
|
|
|
|
p := buildPayload(cfg.NtfyTopic, job)
|
|
|
|
body, err := json.Marshal(p)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal ntfy payload: %w", err)
|
|
}
|
|
|
|
endpoint, err := publishURL(cfg.NtfyURL)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body))
|
|
if err != nil {
|
|
return fmt.Errorf("create request: %w", err)
|
|
}
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
if cfg.NtfyToken != "" {
|
|
req.Header.Set("Authorization", "Bearer "+cfg.NtfyToken)
|
|
}
|
|
|
|
resp, err := c.hc.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("send notification: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
|
return nil
|
|
}
|
|
|
|
respBody, _ := io.ReadAll(io.LimitReader(resp.Body, maxResponseBytes))
|
|
return fmt.Errorf("ntfy returned %d: %s", resp.StatusCode, string(respBody))
|
|
}
|
|
|
|
func buildPayload(topic string, job *models.Job) payload {
|
|
if job.Status == models.StatusError {
|
|
return payload{
|
|
Topic: topic,
|
|
Title: fmt.Sprintf("Coda: %s - %s failed", job.Artist, job.Album),
|
|
Message: job.ErrorMsg,
|
|
Priority: 4,
|
|
Tags: []string{"x"},
|
|
}
|
|
}
|
|
return payload{
|
|
Topic: topic,
|
|
Title: fmt.Sprintf("Coda: %s - %s completed", job.Artist, job.Album),
|
|
Message: "Conversion completed successfully",
|
|
Priority: 3,
|
|
Tags: []string{"white_check_mark"},
|
|
}
|
|
}
|
|
|
|
func publishURL(baseURL string) (string, error) {
|
|
u, err := url.Parse(baseURL)
|
|
if err != nil {
|
|
return "", fmt.Errorf("parse ntfy URL: %w", err)
|
|
}
|
|
u.Path = "/"
|
|
return u.String(), nil
|
|
}
|