Implementación inicial del sistema de automatización de redes sociales

- Estructura completa del proyecto con FastAPI
- Modelos de base de datos (productos, servicios, posts, calendario, interacciones)
- Publishers para X, Threads, Instagram, Facebook
- Generador de contenido con DeepSeek API
- Worker de Celery con tareas programadas
- Dashboard básico con templates HTML
- Docker Compose para despliegue
- Documentación completa

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-28 01:11:44 +00:00
commit 049d2133f9
53 changed files with 5876 additions and 0 deletions

201
app/api/routes/calendar.py Normal file
View File

@@ -0,0 +1,201 @@
"""
API Routes para gestión del Calendario de Contenido.
"""
from datetime import datetime, time
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from pydantic import BaseModel
from app.core.database import get_db
from app.models.content_calendar import ContentCalendar
router = APIRouter()
# ===========================================
# SCHEMAS
# ===========================================
class CalendarEntryCreate(BaseModel):
day_of_week: int # 0=Lunes, 6=Domingo
time: str # Formato "HH:MM"
content_type: str
platforms: List[str]
requires_approval: bool = False
category_filter: Optional[str] = None
priority: int = 0
description: Optional[str] = None
class Config:
from_attributes = True
class CalendarEntryUpdate(BaseModel):
day_of_week: Optional[int] = None
time: Optional[str] = None
content_type: Optional[str] = None
platforms: Optional[List[str]] = None
is_active: Optional[bool] = None
requires_approval: Optional[bool] = None
category_filter: Optional[str] = None
priority: Optional[int] = None
description: Optional[str] = None
class Config:
from_attributes = True
# ===========================================
# ENDPOINTS
# ===========================================
@router.get("/")
async def list_calendar_entries(
day_of_week: Optional[int] = Query(None),
content_type: Optional[str] = Query(None),
platform: Optional[str] = Query(None),
is_active: Optional[bool] = Query(True),
db: Session = Depends(get_db)
):
"""Listar entradas del calendario."""
query = db.query(ContentCalendar)
if day_of_week is not None:
query = query.filter(ContentCalendar.day_of_week == day_of_week)
if content_type:
query = query.filter(ContentCalendar.content_type == content_type)
if is_active is not None:
query = query.filter(ContentCalendar.is_active == is_active)
if platform:
query = query.filter(ContentCalendar.platforms.contains([platform]))
entries = query.order_by(
ContentCalendar.day_of_week,
ContentCalendar.time
).all()
return [e.to_dict() for e in entries]
@router.get("/week")
async def get_week_schedule(db: Session = Depends(get_db)):
"""Obtener el calendario semanal completo."""
entries = db.query(ContentCalendar).filter(
ContentCalendar.is_active == True
).order_by(
ContentCalendar.day_of_week,
ContentCalendar.time
).all()
# Organizar por día
week = {i: [] for i in range(7)}
for entry in entries:
week[entry.day_of_week].append(entry.to_dict())
days = ["Lunes", "Martes", "Miércoles", "Jueves", "Viernes", "Sábado", "Domingo"]
return {
days[i]: week[i] for i in range(7)
}
@router.get("/today")
async def get_today_schedule(db: Session = Depends(get_db)):
"""Obtener el calendario de hoy."""
today = datetime.now().weekday()
entries = db.query(ContentCalendar).filter(
ContentCalendar.day_of_week == today,
ContentCalendar.is_active == True
).order_by(ContentCalendar.time).all()
return [e.to_dict() for e in entries]
@router.get("/{entry_id}")
async def get_calendar_entry(entry_id: int, db: Session = Depends(get_db)):
"""Obtener una entrada del calendario por ID."""
entry = db.query(ContentCalendar).filter(ContentCalendar.id == entry_id).first()
if not entry:
raise HTTPException(status_code=404, detail="Entrada no encontrada")
return entry.to_dict()
@router.post("/")
async def create_calendar_entry(entry_data: CalendarEntryCreate, db: Session = Depends(get_db)):
"""Crear una nueva entrada en el calendario."""
# Parsear hora
hour, minute = map(int, entry_data.time.split(":"))
entry_time = time(hour=hour, minute=minute)
entry = ContentCalendar(
day_of_week=entry_data.day_of_week,
time=entry_time,
content_type=entry_data.content_type,
platforms=entry_data.platforms,
requires_approval=entry_data.requires_approval,
category_filter=entry_data.category_filter,
priority=entry_data.priority,
description=entry_data.description
)
db.add(entry)
db.commit()
db.refresh(entry)
return entry.to_dict()
@router.put("/{entry_id}")
async def update_calendar_entry(
entry_id: int,
entry_data: CalendarEntryUpdate,
db: Session = Depends(get_db)
):
"""Actualizar una entrada del calendario."""
entry = db.query(ContentCalendar).filter(ContentCalendar.id == entry_id).first()
if not entry:
raise HTTPException(status_code=404, detail="Entrada no encontrada")
update_data = entry_data.dict(exclude_unset=True)
# Parsear hora si se proporciona
if "time" in update_data and update_data["time"]:
hour, minute = map(int, update_data["time"].split(":"))
update_data["time"] = time(hour=hour, minute=minute)
for field, value in update_data.items():
setattr(entry, field, value)
entry.updated_at = datetime.utcnow()
db.commit()
db.refresh(entry)
return entry.to_dict()
@router.delete("/{entry_id}")
async def delete_calendar_entry(entry_id: int, db: Session = Depends(get_db)):
"""Eliminar una entrada del calendario."""
entry = db.query(ContentCalendar).filter(ContentCalendar.id == entry_id).first()
if not entry:
raise HTTPException(status_code=404, detail="Entrada no encontrada")
db.delete(entry)
db.commit()
return {"message": "Entrada eliminada", "entry_id": entry_id}
@router.post("/{entry_id}/toggle")
async def toggle_calendar_entry(entry_id: int, db: Session = Depends(get_db)):
"""Activar/desactivar una entrada del calendario."""
entry = db.query(ContentCalendar).filter(ContentCalendar.id == entry_id).first()
if not entry:
raise HTTPException(status_code=404, detail="Entrada no encontrada")
entry.is_active = not entry.is_active
db.commit()
return {
"message": f"Entrada {'activada' if entry.is_active else 'desactivada'}",
"is_active": entry.is_active
}