package api import ( "encoding/json" "fmt" "io" "net/http" "strings" "git.pengzhan.dev/haopengzhan/rsspipe/internal/models" "github.com/go-chi/chi/v5" ) // Category Handlers func (h *Handler) ListCategories(w http.ResponseWriter, r *http.Request) { cats, err := h.feedService.ListCategories(r.Context()) if err != nil { h.writeError(w, http.StatusInternalServerError, err.Error()) return } if cats == nil { cats = []models.Category{} } h.writeJSON(w, http.StatusOK, cats) } func (h *Handler) CreateCategory(w http.ResponseWriter, r *http.Request) { var body struct { Title string `json:"title"` ParentID *string `json:"parent_id,omitempty"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil || strings.TrimSpace(body.Title) == "" { h.writeError(w, http.StatusBadRequest, "valid category title is required") return } cat, err := h.feedService.CreateCategory(r.Context(), strings.TrimSpace(body.Title), body.ParentID) if err != nil { h.writeError(w, http.StatusInternalServerError, err.Error()) return } h.writeJSON(w, http.StatusCreated, cat) } func (h *Handler) DeleteCategory(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") if err := h.feedService.DeleteCategory(r.Context(), id); err != nil { h.writeError(w, http.StatusInternalServerError, err.Error()) return } h.writeJSON(w, http.StatusOK, map[string]string{"message": "category deleted"}) } // Feed Handlers func (h *Handler) ListFeeds(w http.ResponseWriter, r *http.Request) { feeds, err := h.feedService.ListFeeds(r.Context()) if err != nil { h.writeError(w, http.StatusInternalServerError, err.Error()) return } if feeds == nil { feeds = []models.Feed{} } h.writeJSON(w, http.StatusOK, feeds) } func (h *Handler) CreateFeed(w http.ResponseWriter, r *http.Request) { var req models.CreateFeedRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil || strings.TrimSpace(req.FeedURL) == "" { h.writeError(w, http.StatusBadRequest, "valid feed_url is required") return } feed, err := h.feedService.CreateFeed(r.Context(), req) if err != nil { h.writeError(w, http.StatusBadRequest, err.Error()) return } h.writeJSON(w, http.StatusCreated, feed) } func (h *Handler) UpdateFeed(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") var req models.UpdateFeedRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { h.writeError(w, http.StatusBadRequest, "invalid request body") return } feed, err := h.feedService.UpdateFeed(r.Context(), id, req) if err != nil { h.writeError(w, http.StatusInternalServerError, err.Error()) return } h.writeJSON(w, http.StatusOK, feed) } func (h *Handler) DeleteFeed(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") if err := h.feedService.DeleteFeed(r.Context(), id); err != nil { h.writeError(w, http.StatusInternalServerError, err.Error()) return } h.writeJSON(w, http.StatusOK, map[string]string{"message": "feed deleted"}) } func (h *Handler) RefreshFeed(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") f, err := h.feedService.RefreshFeed(r.Context(), id) if err != nil { h.writeError(w, http.StatusInternalServerError, err.Error()) return } h.writeJSON(w, http.StatusOK, f) } func (h *Handler) RefreshAllFeeds(w http.ResponseWriter, r *http.Request) { h.feedService.RefreshAllFeeds(r.Context()) h.writeJSON(w, http.StatusAccepted, map[string]string{"message": "refreshing all feeds in background"}) } func (h *Handler) GetStats(w http.ResponseWriter, r *http.Request) { stats, err := h.feedService.GetStats(r.Context()) if err != nil { h.writeError(w, http.StatusInternalServerError, err.Error()) return } h.writeJSON(w, http.StatusOK, stats) } // OPML Handlers func (h *Handler) ImportOPML(w http.ResponseWriter, r *http.Request) { if err := r.ParseMultipartForm(10 << 20); err != nil { h.writeError(w, http.StatusBadRequest, "failed to parse multipart form") return } file, _, err := r.FormFile("file") if err != nil { h.writeError(w, http.StatusBadRequest, "file field is required") return } defer func() { _ = file.Close() }() data, err := io.ReadAll(file) if err != nil { h.writeError(w, http.StatusBadRequest, "failed to read file") return } createdCount, totalParsed, err := h.feedService.ImportOPML(r.Context(), data) if err != nil { h.writeError(w, http.StatusBadRequest, err.Error()) return } h.writeJSON(w, http.StatusOK, map[string]interface{}{ "imported_count": createdCount, "total_parsed": totalParsed, }) } func (h *Handler) ExportOPML(w http.ResponseWriter, r *http.Request) { xmlData, err := h.feedService.ExportOPML(r.Context()) if err != nil { h.writeError(w, http.StatusInternalServerError, err.Error()) return } w.Header().Set("Content-Type", "application/xml; charset=utf-8") w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="rsspipe-subscriptions.opml"`)) w.WriteHeader(http.StatusOK) _, _ = w.Write(xmlData) }