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

236 lines
7.9 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from sqlalchemy import desc
from app.db.session import get_db
from app.core.deps import get_current_active_user
from app.models import User, ChatMember as ChatMemberModel, Message as MessageModel
from app.schemas.message import Message, MessageUpdate, MessageList, MediaFile, MessageMention
router = APIRouter()
@router.get("/{chat_id}", response_model=MessageList)
def get_chat_messages(
chat_id: int,
page: int = Query(1, ge=1),
limit: int = Query(50, ge=1, le=100),
current_user: User = Depends(get_current_active_user),
db: Session = Depends(get_db)
):
"""Get messages for a chat with pagination"""
# Verify user is member of chat
membership = db.query(ChatMemberModel).filter(
ChatMemberModel.chat_id == chat_id,
ChatMemberModel.user_id == current_user.id
).first()
if not membership:
raise HTTPException(status_code=404, detail="Chat not found")
# Calculate offset
offset = (page - 1) * limit
# Get total count
total = db.query(MessageModel).filter(
MessageModel.chat_id == chat_id,
MessageModel.is_deleted.is_(False)
).count()
# Get messages
messages_query = db.query(MessageModel).filter(
MessageModel.chat_id == chat_id,
MessageModel.is_deleted.is_(False)
).order_by(desc(MessageModel.created_at)).offset(offset).limit(limit)
db_messages = messages_query.all()
# Convert to response format
messages = []
for msg in db_messages:
# Get media files
media_files = []
for media in msg.media_files:
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)
# Get mentions
mentions = []
for mention in msg.mentions:
mention_data = MessageMention(
id=mention.mentioned_user.id,
username=mention.mentioned_user.username,
full_name=mention.mentioned_user.full_name
)
mentions.append(mention_data)
# Get reply-to message if exists
reply_to = None
if msg.reply_to:
reply_to = Message(
id=msg.reply_to.id,
chat_id=msg.reply_to.chat_id,
sender_id=msg.reply_to.sender_id,
content=msg.reply_to.content,
content_type=msg.reply_to.content_type,
status=msg.reply_to.status,
is_edited=msg.reply_to.is_edited,
is_deleted=msg.reply_to.is_deleted,
edited_at=msg.reply_to.edited_at,
created_at=msg.reply_to.created_at,
sender_username=msg.reply_to.sender.username,
sender_avatar=msg.reply_to.sender.avatar_url,
media_files=[],
mentions=[],
reply_to=None
)
message = Message(
id=msg.id,
chat_id=msg.chat_id,
sender_id=msg.sender_id,
content=msg.content,
content_type=msg.content_type,
reply_to_id=msg.reply_to_id,
status=msg.status,
is_edited=msg.is_edited,
is_deleted=msg.is_deleted,
edited_at=msg.edited_at,
created_at=msg.created_at,
sender_username=msg.sender.username,
sender_avatar=msg.sender.avatar_url,
media_files=media_files,
mentions=mentions,
reply_to=reply_to
)
messages.append(message)
# Reverse to show oldest first
messages.reverse()
return MessageList(
messages=messages,
total=total,
page=page,
has_more=offset + limit < total
)
@router.put("/{message_id}", response_model=Message)
def update_message(
message_id: int,
message_update: MessageUpdate,
current_user: User = Depends(get_current_active_user),
db: Session = Depends(get_db)
):
"""Update/edit a message (sender only)"""
message = db.query(MessageModel).filter(MessageModel.id == message_id).first()
if not message:
raise HTTPException(status_code=404, detail="Message not found")
if message.sender_id != current_user.id:
raise HTTPException(status_code=403, detail="Can only edit your own messages")
if message.is_deleted:
raise HTTPException(status_code=400, detail="Cannot edit deleted message")
# Update message
update_data = message_update.dict(exclude_unset=True)
for field, value in update_data.items():
setattr(message, field, value)
message.is_edited = True
from datetime import datetime
message.edited_at = datetime.utcnow()
db.commit()
db.refresh(message)
# Return updated message (simplified response)
return Message(
id=message.id,
chat_id=message.chat_id,
sender_id=message.sender_id,
content=message.content,
content_type=message.content_type,
reply_to_id=message.reply_to_id,
status=message.status,
is_edited=message.is_edited,
is_deleted=message.is_deleted,
edited_at=message.edited_at,
created_at=message.created_at,
sender_username=message.sender.username,
sender_avatar=message.sender.avatar_url,
media_files=[],
mentions=[],
reply_to=None
)
@router.delete("/{message_id}")
def delete_message(
message_id: int,
current_user: User = Depends(get_current_active_user),
db: Session = Depends(get_db)
):
"""Delete a message (sender only or admin/owner)"""
message = db.query(MessageModel).filter(MessageModel.id == message_id).first()
if not message:
raise HTTPException(status_code=404, detail="Message not found")
# Check if user can delete (sender or admin/owner of chat)
can_delete = message.sender_id == current_user.id
if not can_delete:
# Check if user is admin/owner of the chat
from app.models.chat_member import MemberRole
membership = db.query(ChatMemberModel).filter(
ChatMemberModel.chat_id == 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")
# Soft delete
message.is_deleted = True
message.content = "[Deleted]"
db.commit()
return {"message": "Message deleted successfully"}
@router.post("/{message_id}/read")
def mark_message_read(
message_id: int,
current_user: User = Depends(get_current_active_user),
db: Session = Depends(get_db)
):
"""Mark message as read"""
message = db.query(MessageModel).filter(MessageModel.id == message_id).first()
if not message:
raise HTTPException(status_code=404, detail="Message not found")
# Update user's last read message
membership = db.query(ChatMemberModel).filter(
ChatMemberModel.chat_id == message.chat_id,
ChatMemberModel.user_id == current_user.id
).first()
if membership:
if not membership.last_read_message_id or membership.last_read_message_id < message.id:
membership.last_read_message_id = message.id
db.commit()
return {"message": "Message marked as read"}