# -*- coding = utf-8 -*- # @Time : 2025/2/13 下午4:53 # @File : routes.py # @Software : PyCharm # @Author : xingc # @Desc : import logging from typing import List from json.decoder import JSONDecodeError from fastapi import APIRouter, Query, Depends, WebSocket, WebSocketDisconnect, HTTPException, status from fastapi_limiter.depends import WebSocketRateLimiter from sqlalchemy import select, func, and_, exists, update from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload from core.base import schema as base_schema from core.db.crud import CRUD from core.db.engine import get_async_session from core.db.models import Message, Chat, User, ReadStatus, chat_participant from core.routers.auth.schema import LoginUser from core.routers.auth.services import get_current_user_form_http, get_current_user_form_ws from core.base.exceptions import WebsocketTooManyRequests from . import schema from .handlers import socket_manager from .services import websocket_callback, get_user_active_direct_chats, get_unread_messages_per_chat logger = logging.getLogger(__name__) chat_router = APIRouter(prefix='/chat', tags=['聊天模块']) @chat_router.get( '/all/', summary='获取所有聊天', response_model=base_schema.BaseResponse[ base_schema.Paginated[ List[schema.ChatUnreadOut] ] ] ) async def get_all_chats( login_user: LoginUser = Depends(get_current_user_form_http), db_session: AsyncSession = Depends(get_async_session), page: int = Query(default=1, gt=0), page_size: int = Query(default=15, gt=0, le=200), ): filter_conditions = ( exists().where( (chat_participant.c.chat_id == Chat.id) & (chat_participant.c.user_id == login_user.user_id) ) & Chat.is_deleted.is_(False) ) query = ( select(Chat) .where(filter_conditions).order_by(Chat.updated_at.desc()) ) total_query = select(func.count()).select_from(Chat).filter(filter_conditions) total_result = await db_session.execute(total_query) total = total_result.scalar() query = query.limit(page_size).offset((page - 1) * page_size) result = await db_session.execute(query) chats = result.scalars().all() unread_messages_chats_count = await get_unread_messages_per_chat(db_session, chats, login_user) return { 'data': { 'list': unread_messages_chats_count, 'total': total, 'page': page, 'page_size': page_size, } } @chat_router.get( '/{chat_guid}/messages/', summary='获取单个聊天的历史消息', response_model=base_schema.BaseResponse[ base_schema.Paginated[ List[schema.MessageOut] ] ] ) async def get_single_chat_messages( chat_guid: str, login_user: LoginUser = Depends(get_current_user_form_http), db_session: AsyncSession = Depends(get_async_session), page: int = Query(default=1, gt=0), page_size: int = Query(default=15, gt=0, le=200), last_read_message_id: int = Query(default=0, ge=0), ): db_crud = CRUD(db_session, Chat) chat: Chat | None = await db_crud.get(filters={'guid': chat_guid, 'is_deleted': False}) if not chat: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='您访问的聊天不存在') chat_user_ids = [user.user_id for user in await chat.awaitable_attrs.users] if login_user.user_id not in chat_user_ids: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='您无权访问此聊天') # 组装查询条件 conditions = and_( Message.chat_id == chat.id, Message.id > last_read_message_id ) # 记录总数查询 total_stmt = ( select(func.count()).where( conditions ) ) total = await db_session.execute(total_stmt) # 分页记录查询 stmt = ( select(Message) .where(conditions) .order_by(Message.created_at.desc()) .limit(page_size).offset((page - 1) * page_size) .options(selectinload(Message.user), selectinload(Message.chat)) ) records = await db_session.execute(stmt) results = [ { 'user': record[0].user.dict(), 'chat_guid': record[0].chat.guid, 'message_guid': record[0].guid, **record[0].dict(exclude={'guid'}) } for record in records ] return { 'data': { 'list': results, 'total': total.scalar(), 'page': page, 'page_size': page_size, }, } @chat_router.post('/create', summary='创建新聊天', response_model=base_schema.BaseResponse[schema.Chat]) async def create_new_chat( payload: schema.ChatCreateIn, login_user: LoginUser = Depends(get_current_user_form_http), db_session: AsyncSession = Depends(get_async_session), ): if payload.recipient_user_id == login_user.user_id: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='操作禁止') recipient_user: User | None = await CRUD(db_session, User).get(filters={'user_id': payload.recipient_user_id}) if recipient_user is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='收件人不存在') initiator_user: User = await CRUD(db_session, User).get(filters={'user_id': login_user.user_id}) # 查询是否已存在 对应且未关闭的聊天 query = ( select(Chat) .where( and_( exists().where( (chat_participant.c.chat_id == Chat.id) & (chat_participant.c.user_id == login_user.user_id) ), exists().where( (chat_participant.c.chat_id == Chat.id) & (chat_participant.c.user_id == recipient_user.user_id) ), Chat.is_deleted == False ) ).order_by(Chat.created_at.desc()) ) result = await db_session.execute(query) chat = result.scalar_one_or_none() if chat is None: try: chat = Chat() chat.users.append(initiator_user) chat.users.append(recipient_user) db_session.add(chat) await db_session.flush() initiator_read_status = ReadStatus(chat_id=chat.id, user_id=initiator_user.user_id, last_read_message_id=0) recipient_read_status = ReadStatus(chat_id=chat.id, user_id=recipient_user.user_id, last_read_message_id=0) db_session.add_all([initiator_read_status, recipient_read_status]) await db_session.commit() except Exception as exc_info: await db_session.rollback() raise exc_info return { 'data': { 'chat_guid': chat.guid, 'friend_user': recipient_user, } } @chat_router.delete('/{chat_guid}/', summary='删除聊天', response_model=base_schema.BaseResponse) async def delete_single_chat( chat_guid: str, login_user: LoginUser = Depends(get_current_user_form_http), db_session: AsyncSession = Depends(get_async_session), ): subquery = ( select(chat_participant.c.chat_id) .where(chat_participant.c.user_id == login_user.user_id) ).scalar_subquery() query = ( update(Chat) .where(and_( Chat.id.in_(subquery), Chat.guid == chat_guid )) .values(is_deleted=True) ) result = await db_session.execute(query) await db_session.commit() if result.rowcount == 0: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='聊天不存在') return {'message': '聊天已删除'} @chat_router.websocket('/ws') async def get_chat_websocket( websocket: WebSocket, login_user: LoginUser = Depends(get_current_user_form_ws), db_session: AsyncSession = Depends(get_async_session), ): await socket_manager.connect_socket(websocket=websocket) logger.info(f'Websocket connection is established: {login_user.user_id}') user_id = str(login_user.user_id) websocket.user_id = login_user.user_id await socket_manager.add_user_socket_connection(user_id, websocket) ratelimit = WebSocketRateLimiter(times=50, seconds=10, callback=websocket_callback) current_user = await CRUD(db_session, User).get(filters={'user_id': login_user.user_id}) if chats := await get_user_active_direct_chats(db_session, current_user=current_user): for chat_guid in chats: await socket_manager.add_user_to_chat(chat_guid, websocket) else: chats = dict() try: while True: try: incoming_message = await websocket.receive_json() logger.info(f'新消息:{incoming_message}') await ratelimit(websocket) try: message = schema.WSMessage(**incoming_message) except ValueError: await socket_manager.send_error('The message type or format you sent is invalid', websocket) continue handler = socket_manager.handlers.get(message.type) if not handler: logger.error(f'No handler [{message.type}] exists') await socket_manager.send_error(f'Type: {message.type} was not found', websocket) continue await handler( websocket=websocket, db_session=db_session, current_user=current_user, chats=chats, incoming_message=message, ) except (JSONDecodeError, AttributeError) as exc: logger.exception(f'Websocket error, detail: {exc}') await socket_manager.send_error('Wrong message format', websocket) except ValueError as exc: logger.exception(f'Websocket error, detail: {exc}') await socket_manager.send_error('Could not validate incoming message', websocket) except WebsocketTooManyRequests: logger.exception(f'User: {login_user} sent too many ws requests') await socket_manager.send_error('You have sent too many requests', websocket) except WebSocketDisconnect: logging.info(f'Websocket is disconnected: {login_user.user_id}')