
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
224 lines
7.8 KiB
Python
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
|
|
) |