from fastapi import APIRouter, Depends, HTTPException, status, File, UploadFile, Form from sqlmodel import Session, select, or_, and_ from sqlalchemy.orm import joinedload from typing import List, Optional from app.database import get_session from app.models import DirectMessage, User, Snippet, DirectMessageAttachment from app.schemas import DirectMessageCreate, DirectMessageResponse, DirectMessageAttachmentResponse from app.auth import get_current_user from app.websocket import manager import os import uuid from pathlib import Path router = APIRouter(prefix="/direct-messages", tags=["Direct Messages"]) @router.post("/", response_model=DirectMessageResponse, status_code=status.HTTP_201_CREATED) async def create_direct_message( message_data: DirectMessageCreate, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Create a new direct message""" # Check if receiver exists receiver = session.get(User, message_data.receiver_id) if not receiver: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Receiver not found" ) # Can't send message to yourself if message_data.receiver_id == current_user.id: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Cannot send message to yourself" ) new_message = DirectMessage( content=message_data.content, sender_id=current_user.id, receiver_id=message_data.receiver_id, snippet_id=message_data.snippet_id, reply_to_id=message_data.reply_to_id ) session.add(new_message) session.commit() session.refresh(new_message) # Load snippet data if present snippet_data = None if new_message.snippet_id: snippet = session.get(Snippet, new_message.snippet_id) if snippet: snippet_owner = session.get(User, snippet.owner_id) snippet_data = { "id": snippet.id, "title": snippet.title, "content": snippet.content, "language": snippet.language, "tags": snippet.tags, "visibility": snippet.visibility, "department_id": snippet.department_id, "owner_id": snippet.owner_id, "owner_username": snippet_owner.username if snippet_owner else "Unknown", "created_at": snippet.created_at, "updated_at": snippet.updated_at } # Load reply_to data if present reply_to_data = None if new_message.reply_to_id: reply_to_msg = session.get(DirectMessage, new_message.reply_to_id) if reply_to_msg: reply_sender = session.get(User, reply_to_msg.sender_id) reply_to_data = { "id": reply_to_msg.id, "content": reply_to_msg.content, "sender_username": reply_sender.username if reply_sender else "Unknown", "sender_full_name": reply_sender.full_name if reply_sender else None } # Build attachment data attachments_data = [ { "id": att.id, "filename": att.filename, "original_filename": att.original_filename, "mime_type": att.mime_type, "file_size": att.file_size, "file_path": att.file_path, "direct_message_id": att.direct_message_id, "uploaded_at": att.uploaded_at, "upload_permission": att.upload_permission, "uploader_id": att.uploader_id, "is_editable": att.is_editable } for att in new_message.attachments ] # Build response using constructor response = DirectMessageResponse( id=new_message.id, content=new_message.content, sender_id=new_message.sender_id, receiver_id=new_message.receiver_id, snippet_id=new_message.snippet_id, created_at=new_message.created_at, is_read=new_message.is_read, sender_username=current_user.username, receiver_username=receiver.username, sender_full_name=current_user.full_name, sender_profile_picture=current_user.profile_picture, snippet=snippet_data, reply_to=reply_to_data, attachments=attachments_data ) # Broadcast via WebSocket to receiver (using negative user ID as "channel") response_data = { "id": new_message.id, "content": new_message.content, "sender_id": new_message.sender_id, "receiver_id": new_message.receiver_id, "sender_username": current_user.username, "receiver_username": receiver.username, "sender_full_name": current_user.full_name, "sender_profile_picture": current_user.profile_picture, "created_at": new_message.created_at.isoformat(), "is_read": new_message.is_read, "snippet_id": new_message.snippet_id, "snippet": snippet_data, "reply_to_id": new_message.reply_to_id, "reply_to": reply_to_data, "attachments": attachments_data } # Broadcast to both sender and receiver using their user IDs as "channel" await manager.broadcast_to_channel( {"type": "direct_message", "message": response_data}, -message_data.receiver_id # Negative to distinguish from channel IDs ) await manager.broadcast_to_channel( {"type": "direct_message", "message": response_data}, -current_user.id ) # Update user activity manager.update_activity(current_user.id) return response @router.delete("/{message_id}", status_code=status.HTTP_200_OK) async def delete_direct_message( message_id: int, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Delete a direct message (only sender can delete)""" message = session.get(DirectMessage, message_id) if not message: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Message not found" ) # Only sender can delete if message.sender_id != current_user.id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="You can only delete your own messages" ) # Delete associated attachments and files attachments = session.query(DirectMessageAttachment).filter( DirectMessageAttachment.direct_message_id == message_id ).all() for attachment in attachments: # Delete file from disk file_path = Path(attachment.file_path) if file_path.exists(): try: file_path.unlink() except Exception as e: print(f"Failed to delete file: {e}") session.delete(attachment) session.delete(message) session.commit() return {"success": True, "message": "Message deleted"} @router.get("/conversation/{user_id}", response_model=List[DirectMessageResponse]) def get_conversation( user_id: int, limit: int = 50, offset: int = 0, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Get direct messages between current user and another user""" # Check if other user exists other_user = session.get(User, user_id) if not other_user: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="User not found" ) # Get messages where current_user is sender and user_id is receiver OR vice versa statement = ( select(DirectMessage) .where( or_( and_(DirectMessage.sender_id == current_user.id, DirectMessage.receiver_id == user_id), and_(DirectMessage.sender_id == user_id, DirectMessage.receiver_id == current_user.id) ) ) .options(joinedload(DirectMessage.snippet)) .order_by(DirectMessage.created_at.desc()) .offset(offset) .limit(limit) ) messages = session.exec(statement).all() # Mark messages as read if they were sent to current user for msg in messages: if msg.receiver_id == current_user.id and not msg.is_read: msg.is_read = True session.add(msg) session.commit() # Build responses responses = [] for msg in messages: sender = session.get(User, msg.sender_id) receiver = session.get(User, msg.receiver_id) # Build reply_to data if it exists reply_to_data = None if msg.reply_to_id: reply_to_msg = session.get(DirectMessage, msg.reply_to_id) if reply_to_msg: reply_sender = session.get(User, reply_to_msg.sender_id) reply_to_data = { "id": reply_to_msg.id, "content": reply_to_msg.content, "sender_username": reply_sender.username if reply_sender else "Unknown", "sender_full_name": reply_sender.full_name if reply_sender else None } # Build attachment data attachments_data = [ { "id": att.id, "filename": att.filename, "original_filename": att.original_filename, "mime_type": att.mime_type, "file_size": att.file_size, "file_path": att.file_path, "direct_message_id": att.direct_message_id, "uploaded_at": att.uploaded_at, "upload_permission": att.upload_permission, "uploader_id": att.uploader_id, "is_editable": att.is_editable } for att in msg.attachments ] # Build snippet data snippet_data = None if msg.snippet: snippet_owner = session.get(User, msg.snippet.owner_id) snippet_data = { "id": msg.snippet.id, "title": msg.snippet.title, "content": msg.snippet.content, "language": msg.snippet.language, "tags": msg.snippet.tags, "visibility": msg.snippet.visibility, "department_id": msg.snippet.department_id, "owner_id": msg.snippet.owner_id, "owner_username": snippet_owner.username if snippet_owner else "Unknown", "created_at": msg.snippet.created_at, "updated_at": msg.snippet.updated_at } # Create response using dict constructor msg_response = DirectMessageResponse( id=msg.id, content=msg.content, sender_id=msg.sender_id, receiver_id=msg.receiver_id, snippet_id=msg.snippet_id, created_at=msg.created_at, is_read=msg.is_read, sender_username=sender.username if sender else "Unknown", receiver_username=receiver.username if receiver else "Unknown", sender_full_name=sender.full_name if sender else None, sender_profile_picture=sender.profile_picture if sender else None, snippet=snippet_data, reply_to=reply_to_data, attachments=attachments_data ) responses.append(msg_response) # Reverse to show oldest first return list(reversed(responses)) @router.get("/conversations", response_model=List[dict]) def get_conversations( session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Get list of users with whom current user has conversations""" # Get all direct messages involving current user statement = select(DirectMessage).where( or_( DirectMessage.sender_id == current_user.id, DirectMessage.receiver_id == current_user.id ) ) messages = session.exec(statement).all() # Extract unique user IDs user_ids = set() for msg in messages: if msg.sender_id != current_user.id: user_ids.add(msg.sender_id) if msg.receiver_id != current_user.id: user_ids.add(msg.receiver_id) # Get user details conversations = [] for user_id in user_ids: user = session.get(User, user_id) if user: # Get last message with this user last_msg_stmt = ( select(DirectMessage) .where( or_( and_(DirectMessage.sender_id == current_user.id, DirectMessage.receiver_id == user_id), and_(DirectMessage.sender_id == user_id, DirectMessage.receiver_id == current_user.id) ) ) .order_by(DirectMessage.created_at.desc()) .limit(1) ) last_msg = session.exec(last_msg_stmt).first() conversations.append({ "user_id": user.id, "username": user.username, "full_name": user.full_name, "email": user.email, "last_message": last_msg.content if last_msg else None, "last_message_at": last_msg.created_at.isoformat() if last_msg else None }) return conversations @router.post("/{user_id}/upload", status_code=status.HTTP_201_CREATED) async def upload_direct_message_file( user_id: int, file: UploadFile = File(...), content: str = Form(default=""), permission: str = Form(default="read"), reply_to_id: str = Form(default=""), session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """Upload a file with a direct message""" # Convert reply_to_id from string to int or None parsed_reply_to_id: Optional[int] = None if reply_to_id and reply_to_id.strip(): try: parsed_reply_to_id = int(reply_to_id) except ValueError: parsed_reply_to_id = None # Check if receiver exists receiver = session.get(User, user_id) if not receiver: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Receiver not found" ) # Can't send message to yourself if user_id == current_user.id: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Cannot send message to yourself" ) # Create uploads directory if it doesn't exist upload_dir = Path(__file__).parent.parent.parent / "uploads" upload_dir.mkdir(parents=True, exist_ok=True) # Generate unique filename file_ext = Path(file.filename).suffix unique_filename = f"{uuid.uuid4()}{file_ext}" file_path = upload_dir / unique_filename # Save file to disk file_content = await file.read() with open(file_path, 'wb') as f: f.write(file_content) # Create direct message new_message = DirectMessage( content=content or f"Shared file: {file.filename}", sender_id=current_user.id, receiver_id=user_id, reply_to_id=parsed_reply_to_id ) session.add(new_message) session.commit() session.refresh(new_message) # Create file attachment attachment = DirectMessageAttachment( filename=unique_filename, original_filename=file.filename, mime_type=file.content_type or "application/octet-stream", file_size=len(file_content), file_path=str(file_path), direct_message_id=new_message.id, uploader_id=current_user.id, upload_permission=permission ) session.add(attachment) session.commit() session.refresh(attachment) # Build response response = DirectMessageResponse.model_validate(new_message) response.sender_username = current_user.username response.receiver_username = receiver.username response.sender_full_name = current_user.full_name response.sender_profile_picture = current_user.profile_picture # Add attachment to response att_response = DirectMessageAttachmentResponse.model_validate(attachment) response.attachments = [att_response] # Load reply_to data if present reply_to_data = None if new_message.reply_to_id: reply_to_msg = session.get(DirectMessage, new_message.reply_to_id) if reply_to_msg: reply_sender = session.get(User, reply_to_msg.sender_id) reply_to_data = { "id": reply_to_msg.id, "content": reply_to_msg.content, "sender_username": reply_sender.username if reply_sender else "Unknown", "sender_full_name": reply_sender.full_name if reply_sender else None } # Broadcast via WebSocket response_data = { "id": new_message.id, "content": new_message.content, "sender_id": new_message.sender_id, "receiver_id": new_message.receiver_id, "sender_username": current_user.username, "receiver_username": receiver.username, "sender_full_name": current_user.full_name, "sender_profile_picture": current_user.profile_picture, "created_at": new_message.created_at.isoformat(), "is_read": new_message.is_read, "snippet_id": new_message.snippet_id, "snippet": None, "reply_to_id": new_message.reply_to_id, "reply_to": reply_to_data, "attachments": [{ "id": attachment.id, "filename": attachment.filename, "original_filename": attachment.original_filename, "mime_type": attachment.mime_type, "file_size": attachment.file_size, "uploaded_at": attachment.uploaded_at.isoformat(), "direct_message_id": attachment.direct_message_id, "upload_permission": attachment.upload_permission, "uploader_id": attachment.uploader_id, "is_editable": attachment.is_editable }] } # Broadcast to both sender and receiver await manager.broadcast_to_channel( {"type": "direct_message", "message": response_data}, -user_id ) await manager.broadcast_to_channel( {"type": "direct_message", "message": response_data}, -current_user.id ) # Update user activity manager.update_activity(current_user.id) return response