Files
unified-media-manager/internal/api/notifications.go
2026-04-24 10:45:19 -07:00

213 lines
6.7 KiB
Go

package api
import (
"context"
"encoding/json"
"log/slog"
"net/http"
"strconv"
"strings"
"time"
"github.com/TopherMayor/unified-media-manager/internal/service"
"github.com/labstack/echo/v4"
)
func listNotificationChannels(svc *service.NotificationService) echo.HandlerFunc {
return func(c echo.Context) error {
ctx, cancel := context.WithTimeout(c.Request().Context(), 10*time.Second)
defer cancel()
channels, err := svc.ListChannels(ctx)
if err != nil {
slog.Error("failed to list notification channels", "error", err)
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to list channels"})
}
if channels == nil {
channels = []service.NotificationChannel{}
}
return c.JSON(http.StatusOK, channels)
}
}
type createNotificationChannelRequest struct {
Name string `json:"name"`
Type string `json:"type"`
Config json.RawMessage `json:"config"`
Enabled *bool `json:"enabled,omitempty"`
EventTypes []string `json:"event_types"`
}
func createNotificationChannel(svc *service.NotificationService) echo.HandlerFunc {
return func(c echo.Context) error {
ctx, cancel := context.WithTimeout(c.Request().Context(), 5*time.Second)
defer cancel()
var req createNotificationChannelRequest
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request body"})
}
name := strings.TrimSpace(req.Name)
if len(name) < 3 || len(name) > 50 {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "name must be 3-50 characters"})
}
if req.Type != "webhook" && req.Type != "telegram" {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "type must be webhook or telegram"})
}
if req.Config == nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "config is required"})
}
id, err := svc.CreateChannel(ctx, name, req.Type, req.Config)
if err != nil {
slog.Error("failed to create notification channel", "error", err, "name", name, "type", req.Type)
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
}
if len(req.EventTypes) > 0 {
if subErr := svc.UpdateSubscriptions(ctx, id, req.EventTypes); subErr != nil {
slog.Error("failed to set subscriptions", "error", subErr, "channel_id", id)
}
}
return c.JSON(http.StatusCreated, map[string]int64{"id": id})
}
}
type updateNotificationChannelRequest struct {
Name *string `json:"name,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
Config json.RawMessage `json:"config,omitempty"`
EventTypes []string `json:"event_types,omitempty"`
}
func updateNotificationChannel(svc *service.NotificationService) echo.HandlerFunc {
return func(c echo.Context) error {
ctx, cancel := context.WithTimeout(c.Request().Context(), 5*time.Second)
defer cancel()
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid id"})
}
var req updateNotificationChannelRequest
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request body"})
}
if err := svc.UpdateChannel(ctx, id, req.Name, req.Enabled, req.Config); err != nil {
slog.Error("failed to update notification channel", "error", err, "id", id)
return c.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})
}
if req.EventTypes != nil {
if subErr := svc.UpdateSubscriptions(ctx, id, req.EventTypes); subErr != nil {
slog.Error("failed to update subscriptions", "error", subErr, "id", id)
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to update subscriptions"})
}
}
return c.JSON(http.StatusOK, map[string]string{"status": "updated"})
}
}
func deleteNotificationChannel(svc *service.NotificationService) echo.HandlerFunc {
return func(c echo.Context) error {
ctx, cancel := context.WithTimeout(c.Request().Context(), 5*time.Second)
defer cancel()
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid id"})
}
if err := svc.DeleteChannel(ctx, id); err != nil {
slog.Error("failed to delete notification channel", "error", err, "id", id)
return c.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, map[string]string{"status": "deleted"})
}
}
func testNotificationChannel(svc *service.NotificationService) echo.HandlerFunc {
return func(c echo.Context) error {
ctx, cancel := context.WithTimeout(c.Request().Context(), 15*time.Second)
defer cancel()
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid id"})
}
ch, err := svc.GetChannelWithConfig(ctx, id)
if err != nil {
return c.JSON(http.StatusNotFound, map[string]string{"error": "channel not found"})
}
var configMap map[string]interface{}
json.Unmarshal(ch.Config, &configMap)
var deliverErr error
switch ch.Type {
case "webhook":
webhookURL, _ := configMap["url"].(string)
deliverErr = svc.DeliverWebhook(ctx, webhookURL, map[string]interface{}{
"test": true,
"message": "Test notification from UMM",
})
case "telegram":
botToken, _ := configMap["bot_token"].(string)
chatID, _ := configMap["chat_id"].(string)
deliverErr = svc.DeliverTelegram(ctx, botToken, chatID, "🔔 Test notification from UMM")
}
if deliverErr != nil {
slog.Error("notification test failed", "channel", ch.Name, "type", ch.Type)
return c.JSON(http.StatusOK, map[string]interface{}{
"success": false,
"error": "delivery failed",
})
}
return c.JSON(http.StatusOK, map[string]interface{}{
"success": true,
})
}
}
func listNotificationQueue(svc *service.NotificationService) echo.HandlerFunc {
return func(c echo.Context) error {
ctx, cancel := context.WithTimeout(c.Request().Context(), 10*time.Second)
defer cancel()
status := c.QueryParam("status")
page, pageSize := service.ParsePagination(c.QueryParam("page"), c.QueryParam("page_size"))
entries, total, err := svc.ListQueue(ctx, status, page, pageSize)
if err != nil {
slog.Error("failed to list notification queue", "error", err)
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to list queue"})
}
if entries == nil {
entries = []service.QueueEntry{}
}
totalPages := (total + pageSize - 1) / pageSize
return c.JSON(http.StatusOK, paginatedResponse{
Data: entries,
Total: total,
Page: page,
PageSize: pageSize,
TotalPages: totalPages,
})
}
}