"""Comment management router. Handles fetching, reviewing, and replying to comments on published 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 Comment, Post, ScheduledPost, SocialAccount, SystemSetting from ..schemas import CommentAction, CommentResponse from ..services.llm import get_llm_provider from ..services.social import get_publisher router = APIRouter( prefix="/api/comments", tags=["comments"], dependencies=[Depends(get_current_user)], ) @router.get("/", response_model=list[CommentResponse]) def list_comments( platform: str | None = Query(None), reply_status: str | None = Query(None), scheduled_post_id: int | None = Query(None), db: Session = Depends(get_db), ): """List comments with optional filters.""" query = db.query(Comment) if platform is not None: query = query.filter(Comment.platform == platform) if reply_status is not None: query = query.filter(Comment.reply_status == reply_status) if scheduled_post_id is not None: query = query.filter(Comment.scheduled_post_id == scheduled_post_id) return query.order_by(Comment.created_at.desc()).all() @router.get("/pending", response_model=list[CommentResponse]) def list_pending_comments(db: Session = Depends(get_db)): """Get only pending comments (reply_status='pending').""" return ( db.query(Comment) .filter(Comment.reply_status == "pending") .order_by(Comment.created_at.desc()) .all() ) @router.get("/{comment_id}", response_model=CommentResponse) def get_comment(comment_id: int, db: Session = Depends(get_db)): """Get a single comment by ID.""" comment = db.query(Comment).filter(Comment.id == comment_id).first() if not comment: raise HTTPException(status_code=404, detail="Comment not found") return comment @router.post("/{comment_id}/action", response_model=CommentResponse) def action_on_comment( comment_id: int, data: CommentAction, db: Session = Depends(get_db) ): """Take action on a comment: approve, edit, or ignore.""" comment = db.query(Comment).filter(Comment.id == comment_id).first() if not comment: raise HTTPException(status_code=404, detail="Comment not found") if data.action == "approve": comment.approved_reply = comment.ai_suggested_reply comment.reply_status = "approved" elif data.action == "edit": if not data.reply_text: raise HTTPException(status_code=400, detail="reply_text required for edit action") comment.approved_reply = data.reply_text comment.reply_status = "approved" elif data.action == "ignore": comment.reply_status = "ignored" else: raise HTTPException(status_code=400, detail=f"Unknown action '{data.action}'. Use: approve, edit, ignore") db.commit() db.refresh(comment) return comment @router.post("/{comment_id}/reply", response_model=CommentResponse) def reply_to_comment(comment_id: int, db: Session = Depends(get_db)): """Send the approved reply via the social platform API.""" comment = db.query(Comment).filter(Comment.id == comment_id).first() if not comment: raise HTTPException(status_code=404, detail="Comment not found") if not comment.approved_reply: raise HTTPException(status_code=400, detail="No approved reply to send. Use /action first.") if not comment.external_comment_id: raise HTTPException(status_code=400, detail="No external comment ID available for reply") # Find the social account for this platform via the scheduled post if not comment.scheduled_post_id: raise HTTPException(status_code=400, detail="Comment is not linked to a scheduled post") scheduled = ( db.query(ScheduledPost) .filter(ScheduledPost.id == comment.scheduled_post_id) .first() ) if not scheduled: raise HTTPException(status_code=404, detail="Associated scheduled post not found") post = db.query(Post).filter(Post.id == scheduled.post_id).first() if not post: raise HTTPException(status_code=404, detail="Associated post not found") account = ( db.query(SocialAccount) .filter( SocialAccount.character_id == post.character_id, SocialAccount.platform == comment.platform, SocialAccount.is_active == True, ) .first() ) if not account: raise HTTPException( status_code=400, detail=f"No active {comment.platform} account found for this character", ) # 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: publisher = get_publisher(account.platform, account.access_token, **kwargs) success = publisher.reply_to_comment(comment.external_comment_id, comment.approved_reply) if not success: raise RuntimeError("Platform returned failure for reply") comment.reply_status = "replied" comment.replied_at = datetime.utcnow() db.commit() db.refresh(comment) return comment except (RuntimeError, ValueError) as e: raise HTTPException(status_code=502, detail=f"Failed to send reply: {e}") @router.post("/fetch/{platform}") def fetch_comments(platform: str, db: Session = Depends(get_db)): """Fetch new comments from a platform for all published posts. Creates Comment records for any new comments not already in the database. Uses LLM to generate AI-suggested replies for each new comment. """ # Get all published scheduled posts for this platform published_posts = ( db.query(ScheduledPost) .filter( ScheduledPost.platform == platform, ScheduledPost.status == "published", ScheduledPost.external_post_id != None, ) .all() ) if not published_posts: return {"new_comments": 0, "message": f"No published posts found for {platform}"} # Get LLM settings for AI reply generation llm_provider_name = None llm_api_key = None llm_model = None for key in ("llm_provider", "llm_api_key", "llm_model"): setting = db.query(SystemSetting).filter(SystemSetting.key == key).first() if setting: if key == "llm_provider": llm_provider_name = setting.value elif key == "llm_api_key": llm_api_key = setting.value elif key == "llm_model": llm_model = setting.value llm = None if llm_provider_name and llm_api_key: try: llm = get_llm_provider(llm_provider_name, llm_api_key, llm_model) except ValueError: pass # LLM not available, skip AI replies new_comment_count = 0 for scheduled in published_posts: # Get the post to find the character post = db.query(Post).filter(Post.id == scheduled.post_id).first() if not post: continue # Find the social account account = ( db.query(SocialAccount) .filter( SocialAccount.character_id == post.character_id, SocialAccount.platform == platform, SocialAccount.is_active == True, ) .first() ) if not account or not account.access_token: continue # 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: publisher = get_publisher(account.platform, account.access_token, **kwargs) comments = publisher.get_comments(scheduled.external_post_id) except (RuntimeError, ValueError): continue # Skip this post if API call fails for ext_comment in comments: ext_id = ext_comment.get("id", "") if not ext_id: continue # Check if comment already exists existing = ( db.query(Comment) .filter(Comment.external_comment_id == ext_id) .first() ) if existing: continue # Generate AI suggested reply if LLM is available ai_reply = None if llm: try: system_prompt = ( f"You are managing social media comments for a content creator. " f"Write a friendly, on-brand reply to this comment. " f"Keep it concise (1-2 sentences). Be authentic and engaging." ) prompt = ( f"Comment by {ext_comment.get('author', 'someone')}: " f"\"{ext_comment.get('text', '')}\"\n\n" f"Write a reply:" ) ai_reply = llm.generate(prompt, system=system_prompt) except RuntimeError: pass # Skip AI reply if generation fails # Create comment record comment = Comment( scheduled_post_id=scheduled.id, platform=platform, external_comment_id=ext_id, author_name=ext_comment.get("author", "Unknown"), author_id=ext_comment.get("id", ""), content=ext_comment.get("text", ""), ai_suggested_reply=ai_reply, reply_status="pending", ) db.add(comment) new_comment_count += 1 db.commit() return {"new_comments": new_comment_count, "message": f"Fetched {new_comment_count} new comments from {platform}"}