"""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}")