"""Чат — WebSocket + REST для сообщений""" from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, update import uuid from ...core.database import async_session_factory, Chat, ChatMessage, ContentType, ChatStatus from ...utils.auth import get_current_user router = APIRouter() class SendMessageRequest(BaseModel): chat_id: uuid.UUID content_type: str = "text" # text | image | file | voice content: str | None = None media_url: str | None = None reply_to_id: uuid.UUID | None = None @router.post("/messages") async def send_message(req: SendMessageRequest, session: AsyncSession = Depends(async_session_factory), user=Depends(get_current_user)): """Отправить сообщение в чат.""" chat = await session.get(Chat, req.chat_id) if not chat or chat.status != ChatStatus.ACTIVE: raise HTTPException(400, "Чат неактивен") message = ChatMessage( chat_id=req.chat_id, sender_id=user.id, content_type=ContentType(req.content_type), content=req.content, media_url=req.media_url, reply_to_id=req.reply_to_id, ) session.add(message) from datetime import datetime chat.last_message_at = datetime.utcnow() await session.commit() return {"id": str(message.id), "status": "sent"} @router.get("/messages/{chat_id}") async def get_messages(chat_id: uuid.UUID, limit: int = 50, before: uuid.UUID | None = Query(None), session: AsyncSession = Depends(async_session_factory)): """Получить историю сообщений чата.""" query = select(ChatMessage).where(ChatMessage.chat_id == chat_id) if before: query = query.where(ChatMessage.id < before) query = query.order_by(ChatMessage.created_at.desc()).limit(limit) result = await session.execute(query) messages = [m.mapped() for m in result.scalars().all()] return list(reversed(messages)) @router.get("/chats") async def get_user_chats(user=Depends(get_current_user), session: AsyncSession = Depends(async_session_factory)): """Получить все чаты пользователя.""" query = select(Chat).where( (Chat.client_id == user.id) | (Chat.master_id == user.id), Chat.status.in_([ChatStatus.ACTIVE, ChatStatus.COMPLETED]) ).order_by(Chat.last_message_at.desc()) result = await session.execute(query) return [c.mapped() for c in result.scalars().all()] @router.post("/chats/{chat_id}/mark-read") async def mark_as_read(chat_id: uuid.UUID, user=Depends(get_current_user), session: AsyncSession = Depends(async_session_factory)): """Отметить сообщения как прочитанные.""" await session.execute( update(ChatMessage) .where((ChatMessage.chat_id == chat_id) & (ChatMessage.sender_id != user.id) & (ChatMessage.read_at.is_(None))) .values(read_at=datetime.utcnow()) ) await session.commit() @router.post("/chats/{chat_id}/archive") async def archive_chat(chat_id: uuid.UUID, user=Depends(get_current_user), session: AsyncSession = Depends(async_session_factory)): """Заархивировать чат.""" chat = await session.get(Chat, chat_id) if not chat or (chat.client_id != user.id and chat.master_id != user.id): raise HTTPException(403) chat.status = ChatStatus.ARCHIVED await session.commit() @router.delete("/messages/{message_id}") async def delete_message(message_id: uuid.UUID, user=Depends(get_current_user), session: AsyncSession = Depends(async_session_factory)): """Удалить сообщение (только отправитель в течение 24ч).""" message = await session.get(ChatMessage, message_id) if not message or message.sender_id != user.id: raise HTTPException(403) await session.delete(message) await session.commit() @router.post("/chats/{chat_id}/search") async def search_messages(chat_id: uuid.UUID, query: str = Query(...), limit: int = 20, session: AsyncSession = Depends(async_session_factory)): """Поиск по сообщениям чата.""" result = await session.execute( select(ChatMessage) .where((ChatMessage.chat_id == chat_id) & (ChatMessage.content.ilike(f"%{query}%"))) .order_by(ChatMessage.created_at.desc()) .limit(limit) ) return [m.mapped() for m in result.scalars().all()]