Files
social-media-automation/app/api/routes/publish.py
Consultoría AS 3caf2a67fb feat(phase-2): Complete social media API integration
- Add ImageUploadService for public URL generation (Meta APIs)
- Create PublisherManager for unified multi-platform publishing
- Add /api/publish endpoints (single, multiple, thread, test)
- Add compose page in dashboard for creating posts
- Add connection test script (scripts/test_connections.py)
- Update navigation with compose link and logout

New endpoints:
- POST /api/publish/single - Publish to one platform
- POST /api/publish/multiple - Publish to multiple platforms
- POST /api/publish/thread - Publish thread (X/Threads)
- GET /api/publish/test - Test all API connections
- GET /api/publish/platforms - List available platforms

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 01:43:22 +00:00

263 lines
6.8 KiB
Python

"""
API endpoints para publicación en redes sociales.
"""
from typing import Optional, List
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from app.publishers.manager import publisher_manager, Platform, MultiPublishResult
router = APIRouter()
# ============================================================
# Schemas
# ============================================================
class PublishRequest(BaseModel):
"""Solicitud de publicación simple."""
platform: str
content: str
image_url: Optional[str] = None
class MultiPublishRequest(BaseModel):
"""Solicitud de publicación múltiple."""
platforms: List[str]
content: dict # {platform: content} o string único
image_url: Optional[str] = None
class ThreadPublishRequest(BaseModel):
"""Solicitud de publicación de hilo."""
platform: str
posts: List[str]
images: Optional[List[str]] = None
class PublishResponse(BaseModel):
"""Respuesta de publicación."""
success: bool
post_id: Optional[str] = None
url: Optional[str] = None
error: Optional[str] = None
class ConnectionTestResponse(BaseModel):
"""Respuesta de test de conexión."""
platform: str
configured: bool
connected: bool
error: Optional[str] = None
details: dict = {}
# ============================================================
# Endpoints
# ============================================================
@router.post("/single", response_model=PublishResponse)
async def publish_single(request: PublishRequest):
"""
Publicar contenido en una plataforma específica.
- **platform**: x, threads, facebook, instagram
- **content**: Texto del post
- **image_url**: URL o ruta de imagen (opcional)
"""
try:
platform = Platform(request.platform.lower())
except ValueError:
raise HTTPException(
status_code=400,
detail=f"Plataforma no válida: {request.platform}. "
f"Opciones: x, threads, facebook, instagram"
)
result = await publisher_manager.publish(
platform=platform,
content=request.content,
image_path=request.image_url
)
return PublishResponse(
success=result.success,
post_id=result.post_id,
url=result.url,
error=result.error_message
)
@router.post("/multiple")
async def publish_multiple(request: MultiPublishRequest):
"""
Publicar contenido en múltiples plataformas.
- **platforms**: Lista de plataformas ["x", "threads", "facebook"]
- **content**: Dict con contenido por plataforma o string único
- **image_url**: URL o ruta de imagen (opcional)
"""
platforms = []
for p in request.platforms:
try:
platforms.append(Platform(p.lower()))
except ValueError:
raise HTTPException(
status_code=400,
detail=f"Plataforma no válida: {p}"
)
result = await publisher_manager.publish_to_multiple(
platforms=platforms,
content=request.content,
image_path=request.image_url
)
return {
"success": result.success,
"successful_platforms": result.successful_platforms,
"failed_platforms": result.failed_platforms,
"results": {
p: {
"success": r.success,
"post_id": r.post_id,
"url": r.url,
"error": r.error_message
}
for p, r in result.results.items()
},
"errors": result.errors
}
@router.post("/thread", response_model=PublishResponse)
async def publish_thread(request: ThreadPublishRequest):
"""
Publicar un hilo de posts en una plataforma.
- **platform**: x o threads
- **posts**: Lista de textos para cada post del hilo
- **images**: Lista de URLs de imágenes (opcional)
"""
try:
platform = Platform(request.platform.lower())
except ValueError:
raise HTTPException(
status_code=400,
detail=f"Plataforma no válida: {request.platform}"
)
if platform not in [Platform.X, Platform.THREADS]:
raise HTTPException(
status_code=400,
detail="Los hilos solo están soportados en X y Threads"
)
result = await publisher_manager.publish_thread(
platform=platform,
posts=request.posts,
images=request.images
)
return PublishResponse(
success=result.success,
post_id=result.post_id,
url=result.url,
error=result.error_message
)
@router.get("/test/{platform}", response_model=ConnectionTestResponse)
async def test_connection(platform: str):
"""
Probar conexión con una plataforma específica.
Verifica que las credenciales estén configuradas y funcionando.
"""
try:
plat = Platform(platform.lower())
except ValueError:
raise HTTPException(
status_code=400,
detail=f"Plataforma no válida: {platform}"
)
result = await publisher_manager.test_connection(plat)
return ConnectionTestResponse(**result)
@router.get("/test")
async def test_all_connections():
"""
Probar conexión con todas las plataformas.
Devuelve el estado de cada plataforma configurada.
"""
results = await publisher_manager.test_all_connections()
return {
"platforms": results,
"summary": {
"total": len(results),
"configured": sum(1 for r in results.values() if r["configured"]),
"connected": sum(1 for r in results.values() if r["connected"])
}
}
@router.get("/platforms")
async def get_available_platforms():
"""
Obtener lista de plataformas disponibles.
Solo devuelve plataformas con credenciales configuradas.
"""
return {
"available": publisher_manager.get_available_platforms(),
"all": [p.value for p in Platform]
}
class PreviewRequest(BaseModel):
"""Solicitud de previsualización."""
content: str
platforms: List[str]
@router.post("/preview")
async def preview_content(request: PreviewRequest):
"""
Previsualizar cómo se verá el contenido en cada plataforma.
Valida longitud y muestra advertencias.
"""
previews = {}
char_limits = {
"x": 280,
"threads": 500,
"instagram": 2200,
"facebook": 63206
}
for p in request.platforms:
limit = char_limits.get(p.lower(), 500)
length = len(request.content)
previews[p] = {
"content": request.content[:limit],
"length": length,
"limit": limit,
"valid": length <= limit,
"truncated": length > limit,
"remaining": max(0, limit - length)
}
return {
"original_length": len(request.content),
"previews": previews
}