Automated Action 41bbd8c182 Build comprehensive real-time chat API with advanced features
Complete rewrite from task management to full-featured chat system:

Core Features:
- Real-time WebSocket messaging with connection management
- Direct messages and group chats with admin controls
- Message types: text, images, videos, audio, documents
- Message status tracking: sent, delivered, read receipts
- Typing indicators and user presence (online/offline)
- Message replies, editing, and deletion

Security & Encryption:
- End-to-end encryption with RSA + AES hybrid approach
- JWT authentication for API and WebSocket connections
- Secure file storage with access control
- Automatic RSA key pair generation per user

Media & File Sharing:
- Multi-format file upload (images, videos, audio, documents)
- Automatic thumbnail generation for images/videos
- File size validation and MIME type checking
- Secure download endpoints with permission checks

Notifications & Alerts:
- Real-time WebSocket notifications
- Push notifications via Firebase integration
- @username mention alerts with notification history
- Unread message and mention counting
- Custom notification types (message, mention, group invite)

Advanced Features:
- Group chat management with roles (member, admin, owner)
- User search and chat member management
- Message pagination and chat history
- Last seen timestamps and activity tracking
- Comprehensive API documentation with WebSocket events

Architecture:
- Clean layered architecture with services, models, schemas
- WebSocket connection manager for real-time features
- Modular notification system with multiple channels
- Comprehensive error handling and validation
- Production-ready with Docker support

Technologies: FastAPI, WebSocket, SQLAlchemy, SQLite, Cryptography, Firebase, Pillow
2025-06-21 16:49:25 +00:00

224 lines
7.8 KiB
Python

from typing import List
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session
from app.db.session import get_db
from app.core.deps import get_current_active_user
from app.models import User, Message as MessageModel, Media as MediaModel, ChatMember as ChatMemberModel
from app.schemas.message import MediaFile
from app.services.media_service import media_service
import io
router = APIRouter()
@router.post("/upload/{message_id}", response_model=List[MediaFile])
async def upload_media(
message_id: int,
files: List[UploadFile] = File(...),
current_user: User = Depends(get_current_active_user),
db: Session = Depends(get_db)
):
"""Upload media files for a message"""
# Verify message exists and user can access it
message = db.query(MessageModel).filter(MessageModel.id == message_id).first()
if not message:
raise HTTPException(status_code=404, detail="Message not found")
# Verify user is member of the chat
membership = db.query(ChatMemberModel).filter(
ChatMemberModel.chat_id == message.chat_id,
ChatMemberModel.user_id == current_user.id
).first()
if not membership:
raise HTTPException(status_code=403, detail="Access denied")
# Verify user owns the message
if message.sender_id != current_user.id:
raise HTTPException(status_code=403, detail="Can only upload media to your own messages")
media_files = []
for file in files:
try:
# Save file
filename, file_path, file_size, media_type, width, height, thumbnail_path = await media_service.save_file(
file, current_user.id
)
# Create media record
media = MediaModel(
message_id=message_id,
uploader_id=current_user.id,
filename=filename,
original_filename=file.filename,
file_path=file_path,
file_size=file_size,
mime_type=file.content_type or "application/octet-stream",
media_type=media_type,
width=width,
height=height,
thumbnail_path=thumbnail_path,
is_processed=True
)
db.add(media)
db.commit()
db.refresh(media)
# Create response
media_file = MediaFile(
id=media.id,
filename=media.filename,
original_filename=media.original_filename,
file_size=media.file_size,
mime_type=media.mime_type,
media_type=media.media_type.value,
width=media.width,
height=media.height,
duration=media.duration,
thumbnail_url=f"/api/media/{media.id}/thumbnail" if media.thumbnail_path else None
)
media_files.append(media_file)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to upload file: {str(e)}")
return media_files
@router.get("/{media_id}")
async def download_media(
media_id: int,
current_user: User = Depends(get_current_active_user),
db: Session = Depends(get_db)
):
"""Download media file"""
media = db.query(MediaModel).filter(MediaModel.id == media_id).first()
if not media:
raise HTTPException(status_code=404, detail="Media not found")
# Verify user can access this media
membership = db.query(ChatMemberModel).filter(
ChatMemberModel.chat_id == media.message.chat_id,
ChatMemberModel.user_id == current_user.id
).first()
if not membership:
raise HTTPException(status_code=403, detail="Access denied")
try:
content = await media_service.get_file_content(media.filename)
return StreamingResponse(
io.BytesIO(content),
media_type=media.mime_type,
headers={
"Content-Disposition": f"attachment; filename={media.original_filename}"
}
)
except FileNotFoundError:
raise HTTPException(status_code=404, detail="File not found")
@router.get("/{media_id}/thumbnail")
async def get_media_thumbnail(
media_id: int,
current_user: User = Depends(get_current_active_user),
db: Session = Depends(get_db)
):
"""Get media thumbnail"""
media = db.query(MediaModel).filter(MediaModel.id == media_id).first()
if not media:
raise HTTPException(status_code=404, detail="Media not found")
if not media.thumbnail_path:
raise HTTPException(status_code=404, detail="Thumbnail not available")
# Verify user can access this media
membership = db.query(ChatMemberModel).filter(
ChatMemberModel.chat_id == media.message.chat_id,
ChatMemberModel.user_id == current_user.id
).first()
if not membership:
raise HTTPException(status_code=403, detail="Access denied")
try:
content = await media_service.get_thumbnail_content(media.filename)
return StreamingResponse(
io.BytesIO(content),
media_type="image/jpeg"
)
except FileNotFoundError:
raise HTTPException(status_code=404, detail="Thumbnail not found")
@router.delete("/{media_id}")
async def delete_media(
media_id: int,
current_user: User = Depends(get_current_active_user),
db: Session = Depends(get_db)
):
"""Delete media file"""
media = db.query(MediaModel).filter(MediaModel.id == media_id).first()
if not media:
raise HTTPException(status_code=404, detail="Media not found")
# Verify user owns the media or is admin/owner of chat
can_delete = media.uploader_id == current_user.id
if not can_delete:
from app.models.chat_member import MemberRole
membership = db.query(ChatMemberModel).filter(
ChatMemberModel.chat_id == media.message.chat_id,
ChatMemberModel.user_id == current_user.id,
ChatMemberModel.role.in_([MemberRole.ADMIN, MemberRole.OWNER])
).first()
can_delete = membership is not None
if not can_delete:
raise HTTPException(status_code=403, detail="Insufficient permissions")
# Delete file from storage
thumbnail_filename = f"thumb_{media.filename}" if media.thumbnail_path else None
media_service.delete_file(media.filename, thumbnail_filename)
# Delete database record
db.delete(media)
db.commit()
return {"message": "Media deleted successfully"}
@router.get("/{media_id}/info", response_model=MediaFile)
async def get_media_info(
media_id: int,
current_user: User = Depends(get_current_active_user),
db: Session = Depends(get_db)
):
"""Get media file information"""
media = db.query(MediaModel).filter(MediaModel.id == media_id).first()
if not media:
raise HTTPException(status_code=404, detail="Media not found")
# Verify user can access this media
membership = db.query(ChatMemberModel).filter(
ChatMemberModel.chat_id == media.message.chat_id,
ChatMemberModel.user_id == current_user.id
).first()
if not membership:
raise HTTPException(status_code=403, detail="Access denied")
return MediaFile(
id=media.id,
filename=media.filename,
original_filename=media.original_filename,
file_size=media.file_size,
mime_type=media.mime_type,
media_type=media.media_type.value,
width=media.width,
height=media.height,
duration=media.duration,
thumbnail_url=f"/api/media/{media.id}/thumbnail" if media.thumbnail_path else None
)