Files
leopost-full/backend/app/routers/social.py
Michele 519a580679 Initial commit: Leopost Full — merge di Leopost, Post Generator e Autopilot OS
- Backend FastAPI con multi-LLM (Claude/OpenAI/Gemini)
- Publishing su Facebook, Instagram, YouTube, TikTok
- Calendario editoriale con awareness levels (PAS, AIDA, BAB...)
- Design system Editorial Fresh (Fraunces + DM Sans)
- Scheduler automatico, gestione commenti AI, affiliate links

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 17:23:16 +02:00

204 lines
7.1 KiB
Python

"""Social account management and publishing router.
Handles CRUD for social media accounts and manual publishing of scheduled posts.
"""
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from ..auth import get_current_user
from ..database import get_db
from ..models import Post, ScheduledPost, SocialAccount
from ..schemas import (
ScheduledPostResponse,
SocialAccountCreate,
SocialAccountResponse,
SocialAccountUpdate,
)
from ..services.social import get_publisher
router = APIRouter(
prefix="/api/social",
tags=["social"],
dependencies=[Depends(get_current_user)],
)
# === Social Accounts ===
@router.get("/accounts", response_model=list[SocialAccountResponse])
def list_social_accounts(
character_id: int | None = Query(None),
db: Session = Depends(get_db),
):
"""List all social accounts, optionally filtered by character."""
query = db.query(SocialAccount)
if character_id is not None:
query = query.filter(SocialAccount.character_id == character_id)
return query.order_by(SocialAccount.created_at.desc()).all()
@router.get("/accounts/{account_id}", response_model=SocialAccountResponse)
def get_social_account(account_id: int, db: Session = Depends(get_db)):
"""Get a single social account by ID."""
account = db.query(SocialAccount).filter(SocialAccount.id == account_id).first()
if not account:
raise HTTPException(status_code=404, detail="Social account not found")
return account
@router.post("/accounts", response_model=SocialAccountResponse, status_code=201)
def create_social_account(data: SocialAccountCreate, db: Session = Depends(get_db)):
"""Create/connect a new social account."""
account = SocialAccount(**data.model_dump())
db.add(account)
db.commit()
db.refresh(account)
return account
@router.put("/accounts/{account_id}", response_model=SocialAccountResponse)
def update_social_account(
account_id: int, data: SocialAccountUpdate, db: Session = Depends(get_db)
):
"""Update a social account."""
account = db.query(SocialAccount).filter(SocialAccount.id == account_id).first()
if not account:
raise HTTPException(status_code=404, detail="Social account not found")
update_data = data.model_dump(exclude_unset=True)
for key, value in update_data.items():
setattr(account, key, value)
db.commit()
db.refresh(account)
return account
@router.delete("/accounts/{account_id}", status_code=204)
def delete_social_account(account_id: int, db: Session = Depends(get_db)):
"""Delete a social account."""
account = db.query(SocialAccount).filter(SocialAccount.id == account_id).first()
if not account:
raise HTTPException(status_code=404, detail="Social account not found")
db.delete(account)
db.commit()
@router.post("/accounts/{account_id}/test")
def test_social_account(account_id: int, db: Session = Depends(get_db)):
"""Test connection to a social account by making a simple API call."""
account = db.query(SocialAccount).filter(SocialAccount.id == account_id).first()
if not account:
raise HTTPException(status_code=404, detail="Social account not found")
if not account.access_token:
raise HTTPException(status_code=400, detail="No access token configured for this account")
try:
# Build kwargs based on platform
kwargs: dict = {}
if account.platform == "facebook":
if not account.page_id:
raise HTTPException(status_code=400, detail="Facebook account requires page_id")
kwargs["page_id"] = account.page_id
elif account.platform == "instagram":
ig_user_id = account.account_id or (account.extra_data or {}).get("ig_user_id")
if not ig_user_id:
raise HTTPException(status_code=400, detail="Instagram account requires ig_user_id")
kwargs["ig_user_id"] = ig_user_id
# Try to instantiate the publisher (validates credentials format)
get_publisher(account.platform, account.access_token, **kwargs)
return {"status": "ok", "message": f"Connection to {account.platform} account is configured correctly"}
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except RuntimeError as e:
raise HTTPException(status_code=502, detail=f"Connection test failed: {e}")
# === Publishing ===
@router.post("/publish/{scheduled_post_id}", response_model=ScheduledPostResponse)
def publish_scheduled_post(scheduled_post_id: int, db: Session = Depends(get_db)):
"""Manually trigger publishing of a scheduled post."""
scheduled = (
db.query(ScheduledPost)
.filter(ScheduledPost.id == scheduled_post_id)
.first()
)
if not scheduled:
raise HTTPException(status_code=404, detail="Scheduled post not found")
# Get the post content
post = db.query(Post).filter(Post.id == scheduled.post_id).first()
if not post:
raise HTTPException(status_code=404, detail="Associated post not found")
# Find the social account for this platform and character
account = (
db.query(SocialAccount)
.filter(
SocialAccount.character_id == post.character_id,
SocialAccount.platform == scheduled.platform,
SocialAccount.is_active == True,
)
.first()
)
if not account:
raise HTTPException(
status_code=400,
detail=f"No active {scheduled.platform} account found for character {post.character_id}",
)
if not account.access_token:
raise HTTPException(status_code=400, detail="Social account has no access token configured")
# Build publisher kwargs
kwargs: dict = {}
if account.platform == "facebook":
kwargs["page_id"] = account.page_id
elif account.platform == "instagram":
kwargs["ig_user_id"] = account.account_id or (account.extra_data or {}).get("ig_user_id")
try:
scheduled.status = "publishing"
db.commit()
publisher = get_publisher(account.platform, account.access_token, **kwargs)
# Determine publish method based on content type
text = post.text_content or ""
if post.hashtags:
text = f"{text}\n\n{' '.join(post.hashtags)}"
if post.video_url:
external_id = publisher.publish_video(text, post.video_url)
elif post.image_url:
external_id = publisher.publish_image(text, post.image_url)
else:
external_id = publisher.publish_text(text)
# Update scheduled post
scheduled.status = "published"
scheduled.published_at = datetime.utcnow()
scheduled.external_post_id = external_id
# Update post status
post.status = "published"
post.updated_at = datetime.utcnow()
db.commit()
db.refresh(scheduled)
return scheduled
except (RuntimeError, ValueError) as e:
scheduled.status = "failed"
scheduled.error_message = str(e)
db.commit()
db.refresh(scheduled)
raise HTTPException(status_code=502, detail=f"Publishing failed: {e}")