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>
This commit is contained in:
@@ -83,6 +83,19 @@ async def dashboard_home(request: Request, db: Session = Depends(get_db)):
|
||||
})
|
||||
|
||||
|
||||
@router.get("/compose", response_class=HTMLResponse)
|
||||
async def dashboard_compose(request: Request, db: Session = Depends(get_db)):
|
||||
"""Página para crear nuevas publicaciones."""
|
||||
user = require_auth(request, db)
|
||||
if not user:
|
||||
return RedirectResponse(url="/login", status_code=302)
|
||||
|
||||
return templates.TemplateResponse("compose.html", {
|
||||
"request": request,
|
||||
"user": user.to_dict()
|
||||
})
|
||||
|
||||
|
||||
@router.get("/posts", response_class=HTMLResponse)
|
||||
async def dashboard_posts(request: Request, db: Session = Depends(get_db)):
|
||||
"""Página de gestión de posts."""
|
||||
|
||||
262
app/api/routes/publish.py
Normal file
262
app/api/routes/publish.py
Normal file
@@ -0,0 +1,262 @@
|
||||
"""
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user