coda/internal/ntfy/client.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
}