540 lines
14 KiB
Go
540 lines
14 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"encoding/csv"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/rss2/backend/internal/db"
|
|
"github.com/rss2/backend/internal/models"
|
|
)
|
|
|
|
type FeedResponse struct {
|
|
ID int64 `json:"id"`
|
|
Nombre string `json:"nombre"`
|
|
Descripcion *string `json:"descripcion"`
|
|
URL string `json:"url"`
|
|
CategoriaID *int64 `json:"categoria_id"`
|
|
PaisID *int64 `json:"pais_id"`
|
|
Idioma *string `json:"idioma"`
|
|
Activo bool `json:"activo"`
|
|
Fallos *int64 `json:"fallos"`
|
|
LastError *string `json:"last_error"`
|
|
FuenteURLID *int64 `json:"fuente_url_id"`
|
|
}
|
|
|
|
func GetFeeds(c *gin.Context) {
|
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
|
perPage, _ := strconv.Atoi(c.DefaultQuery("per_page", "50"))
|
|
activo := c.Query("activo")
|
|
categoriaID := c.Query("categoria_id")
|
|
paisID := c.Query("pais_id")
|
|
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
if perPage < 1 || perPage > 100 {
|
|
perPage = 50
|
|
}
|
|
|
|
offset := (page - 1) * perPage
|
|
|
|
where := "1=1"
|
|
args := []interface{}{}
|
|
argNum := 1
|
|
|
|
if activo != "" {
|
|
where += fmt.Sprintf(" AND activo = $%d", argNum)
|
|
args = append(args, activo == "true")
|
|
argNum++
|
|
}
|
|
if categoriaID != "" {
|
|
where += fmt.Sprintf(" AND categoria_id = $%d", argNum)
|
|
args = append(args, categoriaID)
|
|
argNum++
|
|
}
|
|
if paisID != "" {
|
|
where += fmt.Sprintf(" AND pais_id = $%d", argNum)
|
|
args = append(args, paisID)
|
|
argNum++
|
|
}
|
|
|
|
var total int
|
|
countQuery := fmt.Sprintf("SELECT COUNT(*) FROM feeds WHERE %s", where)
|
|
err := db.GetPool().QueryRow(c.Request.Context(), countQuery, args...).Scan(&total)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: "Failed to count feeds", Message: err.Error()})
|
|
return
|
|
}
|
|
|
|
sqlQuery := fmt.Sprintf(`
|
|
SELECT f.id, f.nombre, f.descripcion, f.url,
|
|
f.categoria_id, f.pais_id, f.idioma, f.activo, f.fallos, f.last_error,
|
|
c.nombre AS categoria, p.nombre AS pais,
|
|
(SELECT COUNT(*) FROM noticias n WHERE n.fuente_nombre = f.nombre) as noticias_count
|
|
FROM feeds f
|
|
LEFT JOIN categorias c ON c.id = f.categoria_id
|
|
LEFT JOIN paises p ON p.id = f.pais_id
|
|
WHERE %s
|
|
ORDER BY p.nombre NULLS LAST, f.activo DESC, f.fallos ASC, c.nombre NULLS LAST, f.nombre
|
|
LIMIT $%d OFFSET $%d
|
|
`, where, argNum, argNum+1)
|
|
|
|
args = append(args, perPage, offset)
|
|
|
|
rows, err := db.GetPool().Query(c.Request.Context(), sqlQuery, args...)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: "Failed to fetch feeds", Message: err.Error()})
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
type FeedWithStats struct {
|
|
FeedResponse
|
|
Categoria *string `json:"categoria"`
|
|
Pais *string `json:"pais"`
|
|
NoticiasCount int64 `json:"noticias_count"`
|
|
}
|
|
|
|
var feeds []FeedWithStats
|
|
for rows.Next() {
|
|
var f FeedWithStats
|
|
err := rows.Scan(
|
|
&f.ID, &f.Nombre, &f.Descripcion, &f.URL,
|
|
&f.CategoriaID, &f.PaisID, &f.Idioma, &f.Activo, &f.Fallos, &f.LastError,
|
|
&f.Categoria, &f.Pais, &f.NoticiasCount,
|
|
)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
feeds = append(feeds, f)
|
|
}
|
|
|
|
totalPages := (total + perPage - 1) / perPage
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"feeds": feeds,
|
|
"total": total,
|
|
"page": page,
|
|
"per_page": perPage,
|
|
"total_pages": totalPages,
|
|
})
|
|
}
|
|
|
|
func GetFeedByID(c *gin.Context) {
|
|
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, models.ErrorResponse{Error: "Invalid feed ID"})
|
|
return
|
|
}
|
|
|
|
var f FeedResponse
|
|
err = db.GetPool().QueryRow(c.Request.Context(), `
|
|
SELECT id, nombre, descripcion, url, categoria_id, pais_id, idioma, activo, fallos
|
|
FROM feeds WHERE id = $1`, id).Scan(
|
|
&f.ID, &f.Nombre, &f.Descripcion, &f.URL,
|
|
&f.CategoriaID, &f.PaisID, &f.Idioma, &f.Activo, &f.Fallos,
|
|
)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, models.ErrorResponse{Error: "Feed not found"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, f)
|
|
}
|
|
|
|
type CreateFeedRequest struct {
|
|
Nombre string `json:"nombre" binding:"required"`
|
|
URL string `json:"url" binding:"required,url"`
|
|
Descripcion *string `json:"descripcion"`
|
|
CategoriaID *int64 `json:"categoria_id"`
|
|
PaisID *int64 `json:"pais_id"`
|
|
Idioma *string `json:"idioma"`
|
|
}
|
|
|
|
func CreateFeed(c *gin.Context) {
|
|
var req CreateFeedRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, models.ErrorResponse{Error: "Invalid request", Message: err.Error()})
|
|
return
|
|
}
|
|
|
|
var feedID int64
|
|
err := db.GetPool().QueryRow(c.Request.Context(), `
|
|
INSERT INTO feeds (nombre, descripcion, url, categoria_id, pais_id, idioma)
|
|
VALUES ($1, $2, $3, $4, $5, $6)
|
|
RETURNING id`,
|
|
req.Nombre, req.Descripcion, req.URL, req.CategoriaID, req.PaisID, req.Idioma,
|
|
).Scan(&feedID)
|
|
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: "Failed to create feed", Message: err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, gin.H{"id": feedID, "message": "Feed created successfully"})
|
|
}
|
|
|
|
type UpdateFeedRequest struct {
|
|
Nombre string `json:"nombre" binding:"required"`
|
|
URL string `json:"url" binding:"required,url"`
|
|
Descripcion *string `json:"descripcion"`
|
|
CategoriaID *int64 `json:"categoria_id"`
|
|
PaisID *int64 `json:"pais_id"`
|
|
Idioma *string `json:"idioma"`
|
|
Activo *bool `json:"activo"`
|
|
}
|
|
|
|
func UpdateFeed(c *gin.Context) {
|
|
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, models.ErrorResponse{Error: "Invalid feed ID"})
|
|
return
|
|
}
|
|
|
|
var req UpdateFeedRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, models.ErrorResponse{Error: "Invalid request", Message: err.Error()})
|
|
return
|
|
}
|
|
|
|
activeVal := true
|
|
if req.Activo != nil {
|
|
activeVal = *req.Activo
|
|
}
|
|
|
|
result, err := db.GetPool().Exec(c.Request.Context(), `
|
|
UPDATE feeds
|
|
SET nombre = $1, descripcion = $2, url = $3,
|
|
categoria_id = $4, pais_id = $5, idioma = $6, activo = $7
|
|
WHERE id = $8`,
|
|
req.Nombre, req.Descripcion, req.URL,
|
|
req.CategoriaID, req.PaisID, req.Idioma, activeVal, id,
|
|
)
|
|
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: "Failed to update feed", Message: err.Error()})
|
|
return
|
|
}
|
|
|
|
if result.RowsAffected() == 0 {
|
|
c.JSON(http.StatusNotFound, models.ErrorResponse{Error: "Feed not found"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, models.SuccessResponse{Message: "Feed updated successfully"})
|
|
}
|
|
|
|
func DeleteFeed(c *gin.Context) {
|
|
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, models.ErrorResponse{Error: "Invalid feed ID"})
|
|
return
|
|
}
|
|
|
|
result, err := db.GetPool().Exec(c.Request.Context(), "DELETE FROM feeds WHERE id = $1", id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: "Failed to delete feed", Message: err.Error()})
|
|
return
|
|
}
|
|
|
|
if result.RowsAffected() == 0 {
|
|
c.JSON(http.StatusNotFound, models.ErrorResponse{Error: "Feed not found"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, models.SuccessResponse{Message: "Feed deleted successfully"})
|
|
}
|
|
|
|
func ToggleFeedActive(c *gin.Context) {
|
|
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, models.ErrorResponse{Error: "Invalid feed ID"})
|
|
return
|
|
}
|
|
|
|
result, err := db.GetPool().Exec(c.Request.Context(), `
|
|
UPDATE feeds SET activo = NOT activo WHERE id = $1`, id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: "Failed to toggle feed", Message: err.Error()})
|
|
return
|
|
}
|
|
|
|
if result.RowsAffected() == 0 {
|
|
c.JSON(http.StatusNotFound, models.ErrorResponse{Error: "Feed not found"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, models.SuccessResponse{Message: "Feed toggled successfully"})
|
|
}
|
|
|
|
func ReactivateFeed(c *gin.Context) {
|
|
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, models.ErrorResponse{Error: "Invalid feed ID"})
|
|
return
|
|
}
|
|
|
|
result, err := db.GetPool().Exec(c.Request.Context(), `
|
|
UPDATE feeds SET activo = TRUE, fallos = 0 WHERE id = $1`, id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: "Failed to reactivate feed", Message: err.Error()})
|
|
return
|
|
}
|
|
|
|
if result.RowsAffected() == 0 {
|
|
c.JSON(http.StatusNotFound, models.ErrorResponse{Error: "Feed not found"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, models.SuccessResponse{Message: "Feed reactivated successfully"})
|
|
}
|
|
|
|
func ExportFeeds(c *gin.Context) {
|
|
activo := c.Query("activo")
|
|
categoriaID := c.Query("categoria_id")
|
|
paisID := c.Query("pais_id")
|
|
|
|
where := "1=1"
|
|
args := []interface{}{}
|
|
argNum := 1
|
|
|
|
if activo != "" {
|
|
where += fmt.Sprintf(" AND activo = $%d", argNum)
|
|
args = append(args, activo == "true")
|
|
argNum++
|
|
}
|
|
if categoriaID != "" {
|
|
where += fmt.Sprintf(" AND categoria_id = $%d", argNum)
|
|
args = append(args, categoriaID)
|
|
argNum++
|
|
}
|
|
if paisID != "" {
|
|
where += fmt.Sprintf(" AND pais_id = $%d", argNum)
|
|
args = append(args, paisID)
|
|
argNum++
|
|
}
|
|
|
|
query := fmt.Sprintf(`
|
|
SELECT f.id, f.nombre, f.descripcion, f.url,
|
|
f.categoria_id, c.nombre AS categoria,
|
|
f.pais_id, p.nombre AS pais,
|
|
f.idioma, f.activo, f.fallos
|
|
FROM feeds f
|
|
LEFT JOIN categorias c ON c.id = f.categoria_id
|
|
LEFT JOIN paises p ON p.id = f.pais_id
|
|
WHERE %s
|
|
ORDER BY f.id
|
|
`, where)
|
|
|
|
rows, err := db.GetPool().Query(c.Request.Context(), query, args...)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: "Failed to fetch feeds", Message: err.Error()})
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
c.Header("Content-Type", "text/csv")
|
|
c.Header("Content-Disposition", "attachment; filename=feeds_export.csv")
|
|
|
|
writer := csv.NewWriter(c.Writer)
|
|
defer writer.Flush()
|
|
|
|
writer.Write([]string{"id", "nombre", "descripcion", "url", "categoria_id", "categoria", "pais_id", "pais", "idioma", "activo", "fallos"})
|
|
|
|
for rows.Next() {
|
|
var id int64
|
|
var nombre, url string
|
|
var descripcion, idioma *string
|
|
var categoriaID, paisID, fallos *int64
|
|
var activo bool
|
|
var categoria, pais *string
|
|
|
|
err := rows.Scan(&id, &nombre, &descripcion, &url, &categoriaID, &categoria, &paisID, &pais, &idioma, &activo, &fallos)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
writer.Write([]string{
|
|
fmt.Sprintf("%d", id),
|
|
nombre,
|
|
stringOrEmpty(descripcion),
|
|
url,
|
|
int64ToString(categoriaID),
|
|
stringOrEmpty(categoria),
|
|
int64ToString(paisID),
|
|
stringOrEmpty(pais),
|
|
stringOrEmpty(idioma),
|
|
fmt.Sprintf("%t", activo),
|
|
int64ToString(fallos),
|
|
})
|
|
}
|
|
}
|
|
|
|
func ImportFeeds(c *gin.Context) {
|
|
file, err := c.FormFile("file")
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, models.ErrorResponse{Error: "No file provided"})
|
|
return
|
|
}
|
|
|
|
f, err := file.Open()
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: "Failed to open file", Message: err.Error()})
|
|
return
|
|
}
|
|
defer f.Close()
|
|
|
|
content, err := io.ReadAll(f)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: "Failed to read file", Message: err.Error()})
|
|
return
|
|
}
|
|
|
|
reader := csv.NewReader(strings.NewReader(string(content)))
|
|
_, err = reader.Read()
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, models.ErrorResponse{Error: "Invalid CSV format"})
|
|
return
|
|
}
|
|
|
|
imported := 0
|
|
skipped := 0
|
|
failed := 0
|
|
errors := []string{}
|
|
|
|
tx, err := db.GetPool().Begin(context.Background())
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: "Failed to start transaction", Message: err.Error()})
|
|
return
|
|
}
|
|
defer tx.Rollback(context.Background())
|
|
|
|
for {
|
|
record, err := reader.Read()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
failed++
|
|
continue
|
|
}
|
|
|
|
if len(record) < 4 {
|
|
skipped++
|
|
continue
|
|
}
|
|
|
|
nombre := strings.TrimSpace(record[1])
|
|
url := strings.TrimSpace(record[3])
|
|
|
|
if nombre == "" || url == "" {
|
|
skipped++
|
|
continue
|
|
}
|
|
|
|
var descripcion *string
|
|
if len(record) > 2 && strings.TrimSpace(record[2]) != "" {
|
|
descripcionStr := strings.TrimSpace(record[2])
|
|
descripcion = &descripcionStr
|
|
}
|
|
|
|
var categoriaID *int64
|
|
if len(record) > 4 && strings.TrimSpace(record[4]) != "" {
|
|
catID, err := strconv.ParseInt(strings.TrimSpace(record[4]), 10, 64)
|
|
if err == nil {
|
|
categoriaID = &catID
|
|
}
|
|
}
|
|
|
|
var paisID *int64
|
|
if len(record) > 6 && strings.TrimSpace(record[6]) != "" {
|
|
pID, err := strconv.ParseInt(strings.TrimSpace(record[6]), 10, 64)
|
|
if err == nil {
|
|
paisID = &pID
|
|
}
|
|
}
|
|
|
|
var idioma *string
|
|
if len(record) > 8 && strings.TrimSpace(record[8]) != "" {
|
|
lang := strings.TrimSpace(record[8])
|
|
if len(lang) > 2 {
|
|
lang = lang[:2]
|
|
}
|
|
idioma = &lang
|
|
}
|
|
|
|
activo := true
|
|
if len(record) > 9 && strings.TrimSpace(record[9]) != "" {
|
|
activo = strings.ToLower(strings.TrimSpace(record[9])) == "true"
|
|
}
|
|
|
|
var fallos int64
|
|
if len(record) > 10 && strings.TrimSpace(record[10]) != "" {
|
|
f, err := strconv.ParseInt(strings.TrimSpace(record[10]), 10, 64)
|
|
if err == nil {
|
|
fallos = f
|
|
}
|
|
}
|
|
|
|
var existingID int64
|
|
err = tx.QueryRow(context.Background(), "SELECT id FROM feeds WHERE url = $1", url).Scan(&existingID)
|
|
if err == nil {
|
|
_, err = tx.Exec(context.Background(), `
|
|
UPDATE feeds SET nombre=$1, descripcion=$2, categoria_id=$3, pais_id=$4, idioma=$5, activo=$6, fallos=$7
|
|
WHERE id=$8`,
|
|
nombre, descripcion, categoriaID, paisID, idioma, activo, fallos, existingID,
|
|
)
|
|
if err != nil {
|
|
failed++
|
|
errors = append(errors, fmt.Sprintf("Error updating %s: %v", url, err))
|
|
continue
|
|
}
|
|
} else {
|
|
_, err = tx.Exec(context.Background(), `
|
|
INSERT INTO feeds (nombre, descripcion, url, categoria_id, pais_id, idioma, activo, fallos)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
|
|
nombre, descripcion, url, categoriaID, paisID, idioma, activo, fallos,
|
|
)
|
|
if err != nil {
|
|
failed++
|
|
errors = append(errors, fmt.Sprintf("Error inserting %s: %v", url, err))
|
|
continue
|
|
}
|
|
}
|
|
|
|
imported++
|
|
}
|
|
|
|
if err := tx.Commit(context.Background()); err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: "Failed to commit transaction", Message: err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"imported": imported,
|
|
"skipped": skipped,
|
|
"failed": failed,
|
|
"errors": errors,
|
|
"message": fmt.Sprintf("Import completed. Imported: %d, Skipped: %d, Failed: %d", imported, skipped, failed),
|
|
})
|
|
}
|
|
|
|
func stringOrEmpty(s *string) string {
|
|
if s == nil {
|
|
return ""
|
|
}
|
|
return *s
|
|
}
|
|
|
|
func int64ToString(i *int64) string {
|
|
if i == nil {
|
|
return ""
|
|
}
|
|
return fmt.Sprintf("%d", *i)
|
|
}
|