- 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>
263 lines
6.8 KiB
Python
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
|
|
}
|