""" Conversations Router Handles audio conversations, transcriptions, and privacy settings. """ from fastapi import APIRouter, HTTPException, Request, Depends, Query, status from fastapi.responses import FileResponse, JSONResponse from typing import List, Optional from datetime import datetime import os from pathlib import Path from app.core.database import execute_query, execute_update from app.models.schemas import Conversation, ConversationUpdate from app.core.config import settings from app.core.contact_utils import get_contact_customer_ids router = APIRouter() @router.get("/conversations", response_model=List[Conversation]) async def get_conversations( request: Request, customer_id: Optional[int] = None, contact_id: Optional[int] = None, ticket_id: Optional[int] = None, only_mine: bool = False, include_deleted: bool = False ): """ List conversations with filtering. """ where_clauses = [] params = [] # Default: Exclude deleted if not include_deleted: where_clauses.append("deleted_at IS NULL") if contact_id: contact_customer_ids = get_contact_customer_ids(contact_id) if customer_id is not None: if customer_id not in contact_customer_ids: return [] contact_customer_ids = [customer_id] if not contact_customer_ids: return [] placeholders = ",".join(["%s"] * len(contact_customer_ids)) where_clauses.append(f"customer_id IN ({placeholders})") params.extend(contact_customer_ids) elif customer_id: where_clauses.append("customer_id = %s") params.append(customer_id) if ticket_id: where_clauses.append("ticket_id = %s") params.append(ticket_id) # Filtering Logic for Privacy # 1. Technical implementation of 'only_mine' depends on auth user context # Assuming we might have a user_id in session or request state (not fully clear from context, defaulting to param) # For this implementation, I'll assume 'only_mine' filters by the current user if available, or just ignored if no auth. # Note: Access Control logic should be here. # For now, we return public conversations OR private ones owned by user. # Since auth is light in this project, we implement basic logic. auth_user_id = None # data.get('user_id') # To be implemented with auth middleware # Taking a pragmatic approach: if is_private is true, we ideally shouldn't return it unless authorized. # For now, we return all, assuming the frontend filters or backend auth is added later. if only_mine and auth_user_id: where_clauses.append("user_id = %s") params.append(auth_user_id) where_sql = " AND ".join(where_clauses) if where_clauses else "TRUE" query = f""" SELECT * FROM conversations WHERE {where_sql} ORDER BY created_at DESC """ results = execute_query(query, tuple(params)) return results @router.get("/conversations/{conversation_id}/audio") async def get_conversation_audio(conversation_id: int): """ Stream the audio file for a conversation. """ query = "SELECT audio_file_path FROM conversations WHERE id = %s" results = execute_query(query, (conversation_id,)) if not results: raise HTTPException(status_code=404, detail="Conversation not found") # Security check: Check if deleted record = results[0] # (If using soft delete, check deleted_at if not admin) file_path_str = record['audio_file_path'] file_path = Path(file_path_str) # Validation if not file_path.exists(): # Fallback to absolute path check if stored relative abs_path = Path(os.getcwd()) / file_path if not abs_path.exists(): raise HTTPException(status_code=404, detail="Audio file not found on disk") file_path = abs_path return FileResponse(file_path, media_type="audio/mpeg", filename=file_path.name) @router.delete("/conversations/{conversation_id}") async def delete_conversation(conversation_id: int, hard_delete: bool = False): """ Delete a conversation. hard_delete=True removes file and record permanently (GDPR). hard_delete=False sets deleted_at (Recycle Bin). """ # 1. Fetch info query = "SELECT * FROM conversations WHERE id = %s" results = execute_query(query, (conversation_id,)) if not results: raise HTTPException(status_code=404, detail="Conversation not found") conv = results[0] if hard_delete: # HARD DELETE # 1. Delete file try: file_path = Path(conv['audio_file_path']) if not file_path.is_absolute(): file_path = Path(os.getcwd()) / file_path if file_path.exists(): os.remove(file_path) except Exception as e: # Log error but continue to cleanup DB? Or fail? # Better to fail safely or ensure DB matches reality print(f"Error deleting file: {e}") # 2. Delete DB Record execute_update("DELETE FROM conversations WHERE id = %s", (conversation_id,)) return {"status": "permanently_deleted"} else: # SOFT DELETE execute_update( "UPDATE conversations SET deleted_at = CURRENT_TIMESTAMP WHERE id = %s", (conversation_id,) ) return {"status": "moved_to_trash"} @router.patch("/conversations/{conversation_id}", response_model=Conversation) async def update_conversation(conversation_id: int, update: ConversationUpdate): """ Update conversation metadata (privacy, title, links). """ # Build update query dynamically fields = [] values = [] if update.title is not None: fields.append("title = %s") values.append(update.title) if update.is_private is not None: fields.append("is_private = %s") values.append(update.is_private) if update.ticket_id is not None: fields.append("ticket_id = %s") values.append(update.ticket_id) if update.customer_id is not None: fields.append("customer_id = %s") values.append(update.customer_id) if update.category is not None: fields.append("category = %s") values.append(update.category) if not fields: raise HTTPException(status_code=400, detail="No fields to update") fields.append("updated_at = CURRENT_TIMESTAMP") values.append(conversation_id) query = f"UPDATE conversations SET {', '.join(fields)} WHERE id = %s RETURNING *" # execute_query often returns list of dicts for SELECT/RETURNING results = execute_query(query, tuple(values)) if not results: raise HTTPException(status_code=404, detail="Conversation not found") return results[0]