bmc_hub/app/conversations/backend/router.py

202 lines
6.9 KiB
Python
Raw Permalink Normal View History

"""
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]