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

391 lines
13 KiB
Python

from typing import List
from fastapi import APIRouter, Depends, HTTPException
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, Chat as ChatModel, ChatMember as ChatMemberModel, Message as MessageModel, ChatType, MemberRole
from app.schemas.chat import Chat, ChatCreate, DirectChatCreate, ChatUpdate, ChatList, ChatMember, ChatMemberCreate
from app.schemas.user import UserPublic
from app.websocket.connection_manager import connection_manager
router = APIRouter()
@router.get("/", response_model=List[ChatList])
def get_user_chats(
skip: int = 0,
limit: int = 50,
current_user: User = Depends(get_current_active_user),
db: Session = Depends(get_db)
):
"""Get all chats for the current user"""
# Get user's chat memberships
memberships = db.query(ChatMemberModel).filter(
ChatMemberModel.user_id == current_user.id
).offset(skip).limit(limit).all()
chats = []
for membership in memberships:
chat = membership.chat
# Get last message
last_message = db.query(MessageModel).filter(
MessageModel.chat_id == chat.id
).order_by(desc(MessageModel.created_at)).first()
# Calculate unread count
unread_count = 0
if membership.last_read_message_id:
unread_count = db.query(MessageModel).filter(
MessageModel.chat_id == chat.id,
MessageModel.id > membership.last_read_message_id,
MessageModel.sender_id != current_user.id
).count()
else:
unread_count = db.query(MessageModel).filter(
MessageModel.chat_id == chat.id,
MessageModel.sender_id != current_user.id
).count()
# For direct chats, get the other user's online status
is_online = False
chat_name = chat.name
if chat.chat_type == ChatType.DIRECT:
other_member = db.query(ChatMemberModel).filter(
ChatMemberModel.chat_id == chat.id,
ChatMemberModel.user_id != current_user.id
).first()
if other_member:
is_online = other_member.user.is_online
if not chat_name:
chat_name = other_member.user.username
chat_data = ChatList(
id=chat.id,
name=chat_name,
chat_type=chat.chat_type,
avatar_url=chat.avatar_url,
last_message={
"id": last_message.id,
"content": last_message.content[:100] if last_message.content else "",
"sender_username": last_message.sender.username,
"created_at": last_message.created_at.isoformat()
} if last_message else None,
last_activity=last_message.created_at if last_message else chat.updated_at,
unread_count=unread_count,
is_online=is_online
)
chats.append(chat_data)
# Sort by last activity
chats.sort(key=lambda x: x.last_activity or x.id, reverse=True)
return chats
@router.post("/direct", response_model=Chat)
def create_direct_chat(
chat_data: DirectChatCreate,
current_user: User = Depends(get_current_active_user),
db: Session = Depends(get_db)
):
"""Create or get existing direct chat"""
other_user = db.query(User).filter(User.id == chat_data.other_user_id).first()
if not other_user:
raise HTTPException(status_code=404, detail="User not found")
if other_user.id == current_user.id:
raise HTTPException(status_code=400, detail="Cannot create chat with yourself")
# Check if direct chat already exists
existing_chat = db.query(ChatModel).join(ChatMemberModel).filter(
ChatModel.chat_type == ChatType.DIRECT,
ChatMemberModel.user_id.in_([current_user.id, other_user.id])
).group_by(ChatModel.id).having(
db.func.count(ChatMemberModel.user_id) == 2
).first()
if existing_chat:
return _get_chat_with_details(existing_chat.id, current_user.id, db)
# Create new direct chat
chat = ChatModel(
chat_type=ChatType.DIRECT,
is_active=True
)
db.add(chat)
db.commit()
db.refresh(chat)
# Add both users as members
for user in [current_user, other_user]:
member = ChatMemberModel(
chat_id=chat.id,
user_id=user.id,
role=MemberRole.MEMBER
)
db.add(member)
db.commit()
# Add users to connection manager
connection_manager.add_user_to_chat(current_user.id, chat.id)
connection_manager.add_user_to_chat(other_user.id, chat.id)
return _get_chat_with_details(chat.id, current_user.id, db)
@router.post("/group", response_model=Chat)
def create_group_chat(
chat_data: ChatCreate,
current_user: User = Depends(get_current_active_user),
db: Session = Depends(get_db)
):
"""Create a new group chat"""
if not chat_data.name:
raise HTTPException(status_code=400, detail="Group name is required")
# Create group chat
chat = ChatModel(
name=chat_data.name,
description=chat_data.description,
chat_type=ChatType.GROUP,
avatar_url=chat_data.avatar_url,
is_active=True
)
db.add(chat)
db.commit()
db.refresh(chat)
# Add creator as owner
owner_member = ChatMemberModel(
chat_id=chat.id,
user_id=current_user.id,
role=MemberRole.OWNER
)
db.add(owner_member)
# Add other members
for member_id in chat_data.member_ids:
if member_id != current_user.id: # Don't add creator twice
user = db.query(User).filter(User.id == member_id).first()
if user:
member = ChatMemberModel(
chat_id=chat.id,
user_id=member_id,
role=MemberRole.MEMBER
)
db.add(member)
db.commit()
# Add users to connection manager
connection_manager.add_user_to_chat(current_user.id, chat.id)
for member_id in chat_data.member_ids:
connection_manager.add_user_to_chat(member_id, chat.id)
return _get_chat_with_details(chat.id, current_user.id, db)
@router.get("/{chat_id}", response_model=Chat)
def get_chat(
chat_id: int,
current_user: User = Depends(get_current_active_user),
db: Session = Depends(get_db)
):
"""Get chat details"""
# 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")
return _get_chat_with_details(chat_id, current_user.id, db)
@router.put("/{chat_id}", response_model=Chat)
def update_chat(
chat_id: int,
chat_update: ChatUpdate,
current_user: User = Depends(get_current_active_user),
db: Session = Depends(get_db)
):
"""Update chat details (admin/owner only)"""
# Verify user is admin/owner of chat
membership = db.query(ChatMemberModel).filter(
ChatMemberModel.chat_id == chat_id,
ChatMemberModel.user_id == current_user.id,
ChatMemberModel.role.in_([MemberRole.ADMIN, MemberRole.OWNER])
).first()
if not membership:
raise HTTPException(status_code=403, detail="Insufficient permissions")
chat = db.query(ChatModel).filter(ChatModel.id == chat_id).first()
if not chat:
raise HTTPException(status_code=404, detail="Chat not found")
update_data = chat_update.dict(exclude_unset=True)
for field, value in update_data.items():
setattr(chat, field, value)
db.commit()
db.refresh(chat)
return _get_chat_with_details(chat_id, current_user.id, db)
@router.post("/{chat_id}/members", response_model=ChatMember)
def add_chat_member(
chat_id: int,
member_data: ChatMemberCreate,
current_user: User = Depends(get_current_active_user),
db: Session = Depends(get_db)
):
"""Add member to chat (admin/owner only)"""
# Verify permissions
membership = db.query(ChatMemberModel).filter(
ChatMemberModel.chat_id == chat_id,
ChatMemberModel.user_id == current_user.id,
ChatMemberModel.role.in_([MemberRole.ADMIN, MemberRole.OWNER])
).first()
if not membership:
raise HTTPException(status_code=403, detail="Insufficient permissions")
# Check if user is already a member
existing_member = db.query(ChatMemberModel).filter(
ChatMemberModel.chat_id == chat_id,
ChatMemberModel.user_id == member_data.user_id
).first()
if existing_member:
raise HTTPException(status_code=400, detail="User is already a member")
# Verify user exists
user = db.query(User).filter(User.id == member_data.user_id).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
# Add member
new_member = ChatMemberModel(
chat_id=chat_id,
user_id=member_data.user_id,
role=member_data.role,
nickname=member_data.nickname
)
db.add(new_member)
db.commit()
db.refresh(new_member)
# Add to connection manager
connection_manager.add_user_to_chat(member_data.user_id, chat_id)
return ChatMember(
id=new_member.id,
chat_id=new_member.chat_id,
user_id=new_member.user_id,
role=new_member.role,
nickname=new_member.nickname,
is_muted=new_member.is_muted,
is_banned=new_member.is_banned,
joined_at=new_member.joined_at,
last_read_message_id=new_member.last_read_message_id,
user=UserPublic.from_orm(user)
)
@router.delete("/{chat_id}/members/{user_id}")
def remove_chat_member(
chat_id: int,
user_id: int,
current_user: User = Depends(get_current_active_user),
db: Session = Depends(get_db)
):
"""Remove member from chat"""
# Users can remove themselves, or admins/owners can remove others
if user_id != current_user.id:
membership = db.query(ChatMemberModel).filter(
ChatMemberModel.chat_id == chat_id,
ChatMemberModel.user_id == current_user.id,
ChatMemberModel.role.in_([MemberRole.ADMIN, MemberRole.OWNER])
).first()
if not membership:
raise HTTPException(status_code=403, detail="Insufficient permissions")
# Find and remove member
member = db.query(ChatMemberModel).filter(
ChatMemberModel.chat_id == chat_id,
ChatMemberModel.user_id == user_id
).first()
if not member:
raise HTTPException(status_code=404, detail="Member not found")
db.delete(member)
db.commit()
# Remove from connection manager
connection_manager.remove_user_from_chat(user_id, chat_id)
return {"message": "Member removed successfully"}
def _get_chat_with_details(chat_id: int, current_user_id: int, db: Session) -> Chat:
"""Helper function to get chat with all details"""
chat = db.query(ChatModel).filter(ChatModel.id == chat_id).first()
# Get members
members = db.query(ChatMemberModel).filter(ChatMemberModel.chat_id == chat_id).all()
chat_members = []
for member in members:
user_public = UserPublic.from_orm(member.user)
chat_member = ChatMember(
id=member.id,
chat_id=member.chat_id,
user_id=member.user_id,
role=member.role,
nickname=member.nickname,
is_muted=member.is_muted,
is_banned=member.is_banned,
joined_at=member.joined_at,
last_read_message_id=member.last_read_message_id,
user=user_public
)
chat_members.append(chat_member)
# Get last message
last_message = db.query(MessageModel).filter(
MessageModel.chat_id == chat_id
).order_by(desc(MessageModel.created_at)).first()
# Calculate unread count
current_member = next((m for m in members if m.user_id == current_user_id), None)
unread_count = 0
if current_member and current_member.last_read_message_id:
unread_count = db.query(MessageModel).filter(
MessageModel.chat_id == chat_id,
MessageModel.id > current_member.last_read_message_id,
MessageModel.sender_id != current_user_id
).count()
else:
unread_count = db.query(MessageModel).filter(
MessageModel.chat_id == chat_id,
MessageModel.sender_id != current_user_id
).count()
return Chat(
id=chat.id,
name=chat.name,
description=chat.description,
chat_type=chat.chat_type,
avatar_url=chat.avatar_url,
is_active=chat.is_active,
created_at=chat.created_at,
updated_at=chat.updated_at,
members=chat_members,
last_message={
"id": last_message.id,
"content": last_message.content,
"sender_username": last_message.sender.username,
"created_at": last_message.created_at.isoformat()
} if last_message else None,
unread_count=unread_count
)