mirror of
https://github.com/OHV-IT/collabrix.git
synced 2025-12-15 16:48:36 +01:00
512 lines
18 KiB
Python
512 lines
18 KiB
Python
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
|