# -*- 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, ChatParticipant 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, get_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( and_( ChatParticipant.chat_id == Chat.id, ChatParticipant.user_id == login_user.user_id, ChatParticipant.is_deleted == 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, 'next_page': (page * page_size) < total, '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(ge=0) ): chat: Chat | None = await get_chat(chat_guid, login_user, db_session, is_query_messages=True) if not chat: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='您访问的聊天不存在') read_status = await CRUD(db_session, ChatParticipant).get( filters={'chat_id': chat.id, 'user_id': login_user.user_id}) # 组装查询条件 conditions = and_( Message.chat_id == chat.id, Message.id > (read_status.last_del_message_id or 0), Message.id <= last_read_message_id, ) # 记录总数查询 total_stmt = ( select(func.count()).where( conditions ) ) total_result = await db_session.execute(total_stmt) total = total_result.scalar() # 分页记录查询 stmt = ( select(Message) .where(conditions) .options(selectinload(Message.user), selectinload(Message.chat)) .order_by(Message.id.desc()). limit(page_size).offset((page - 1) * page_size) ) 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, 'next_page': (page * page_size) < total, 'total': total, '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='操作禁止') db_crud = CRUD(db_session, User) recipient_user: User | None = await db_crud.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 db_crud.get(filters={'user_id': login_user.user_id}) # 查询是否已存在 对应且未关闭的聊天 query = ( select(Chat) .where( and_( exists().where( (ChatParticipant.chat_id == Chat.id) & (ChatParticipant.user_id == login_user.user_id) ), exists().where( (ChatParticipant.chat_id == Chat.id) & (ChatParticipant.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() await db_session.commit() except Exception as exc_info: await db_session.rollback() raise exc_info if chat.is_deleted is True: await db_crud.updates(model=Chat, data={'is_deleted': False}, filters={'id': chat.id}) await db_crud.updates(model=ChatParticipant, data={'is_deleted': False}, filters={'chat_id': chat.id}) 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(ChatParticipant.chat_id) .where(ChatParticipant.user_id == login_user.user_id) ).scalar_subquery() query = ( select(Chat) .where(and_( Chat.id.in_(subquery), Chat.guid == chat_guid )) .options(selectinload(Chat.chat_participants)) ) result = await db_session.execute(query) chat = result.scalar_one_or_none() if chat is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='聊天不存在') deleted_status = [ chat_participant.is_deleted for chat_participant in chat.chat_participants if chat_participant.user_id != login_user.user_id ] if all(deleted_status): await db_session.delete(chat) await db_session.commit() else: db_curd = CRUD(db_session, Chat) await db_curd.update( data={'is_deleted': True}, filters={'id': chat.id}, ) message_query = ( select(Message).where(Message.chat_id == chat.id).order_by(Message.id.desc()).limit(1) ) message_result = await db_session.execute(message_query) message: Message | None = message_result.scalar() condition = {'user_id': login_user.user_id, 'chat_id': chat.id} data = {'is_deleted': True} if message: data['last_del_message_id'] = message.id await db_curd.update( model=ChatParticipant, data=data, filters=condition, ) 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=30, 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, ) send_status_replay = schema.WSMessageOut( type='chat.send_status_reply', message_id=-1, message_guid=message.message_guid, message_type=message.message_type, chat_guid=message.chat_guid, ) await websocket.send_text(send_status_replay.model_dump_json(exclude_none=True)) 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}')