799 lines
33 KiB
Python
799 lines
33 KiB
Python
"""
|
||
Views для системы чата.
|
||
"""
|
||
from rest_framework import viewsets, status
|
||
from rest_framework.decorators import action
|
||
from rest_framework.pagination import PageNumberPagination
|
||
from rest_framework.response import Response
|
||
from rest_framework.permissions import IsAuthenticated
|
||
from django.db.models import Q, Max
|
||
from django.utils import timezone
|
||
from .models import Chat, ChatParticipant, Message, MessageFile
|
||
from .serializers import (
|
||
ChatSerializer,
|
||
ChatDetailSerializer,
|
||
MessageSerializer,
|
||
MessageCreateSerializer,
|
||
ChatParticipantSerializer
|
||
)
|
||
from .permissions import IsChatParticipant
|
||
from .utils import (
|
||
save_file_to_preload,
|
||
move_file_from_preload_to_chat,
|
||
cleanup_preload_files
|
||
)
|
||
|
||
|
||
class ChatListPagination(PageNumberPagination):
|
||
page_size = 30
|
||
page_size_query_param = 'page_size'
|
||
max_page_size = 100
|
||
|
||
|
||
class ChatViewSet(viewsets.ModelViewSet):
|
||
"""ViewSet для чатов."""
|
||
|
||
queryset = Chat.objects.all()
|
||
serializer_class = ChatSerializer
|
||
permission_classes = [IsAuthenticated]
|
||
lookup_field = 'uuid'
|
||
pagination_class = ChatListPagination
|
||
|
||
def get_queryset(self):
|
||
"""Только чаты пользователя со связанными участниками."""
|
||
archived = self.request.query_params.get('archived', 'false').lower() == 'true'
|
||
user = self.request.user
|
||
|
||
queryset = Chat.objects.filter(
|
||
participants__user=user,
|
||
is_archived=archived
|
||
)
|
||
|
||
# Если не суперпользователь, фильтруем по связям
|
||
if not user.is_superuser:
|
||
from apps.users.models import User
|
||
contact_ids = set()
|
||
if user.role == 'mentor':
|
||
# Студенты ментора
|
||
contact_ids.update(User.objects.filter(role='client', client_profile__mentors=user).values_list('id', flat=True))
|
||
# Родители студентов ментора
|
||
contact_ids.update(User.objects.filter(role='parent', parent_profile__children__mentors=user).values_list('id', flat=True))
|
||
elif user.role == 'client':
|
||
# Менторы студента
|
||
contact_ids.update(User.objects.filter(role='mentor', clients__user=user).values_list('id', flat=True))
|
||
# Родители студента
|
||
contact_ids.update(User.objects.filter(role='parent', parent_profile__children__user=user).values_list('id', flat=True))
|
||
elif user.role == 'parent':
|
||
# Менторы детей родителя
|
||
contact_ids.update(User.objects.filter(role='mentor', clients__parents__user=user).values_list('id', flat=True))
|
||
# Дети родителя
|
||
contact_ids.update(User.objects.filter(role='client', client_profile__parents__user=user).values_list('id', flat=True))
|
||
|
||
# Фильтруем чаты: либо групповой, либо личный со связанным пользователем
|
||
# Для личных чатов проверяем, что второй участник (не текущий юзер) есть в списке контактов
|
||
queryset = queryset.filter(
|
||
Q(chat_type='group') |
|
||
Q(chat_type='direct', participants__user_id__in=contact_ids)
|
||
)
|
||
|
||
queryset = queryset.annotate(
|
||
last_msg_time=Max('messages__created_at')
|
||
).select_related('created_by')
|
||
|
||
# Для личных чатов нужно предзагрузить участников с их данными
|
||
from django.db.models import Prefetch
|
||
queryset = queryset.prefetch_related(
|
||
Prefetch(
|
||
'participants',
|
||
queryset=ChatParticipant.objects.select_related('user')
|
||
)
|
||
)
|
||
|
||
# Оптимизация: для списка prefetch только последнее сообщение вместо всех
|
||
if self.action == 'list':
|
||
from django.db.models import Prefetch
|
||
queryset = queryset.prefetch_related(
|
||
Prefetch(
|
||
'messages',
|
||
queryset=Message.objects.filter(is_deleted=False).select_related('sender').prefetch_related('files').order_by('-created_at')[:1],
|
||
to_attr='last_message_prefetch'
|
||
)
|
||
)
|
||
else:
|
||
# Для детального просмотра загружаем все сообщения
|
||
queryset = queryset.prefetch_related('messages')
|
||
|
||
queryset = queryset.order_by('-last_msg_time', '-created_at').distinct()
|
||
|
||
# Оптимизация: для списка используем only() для ограничения полей
|
||
if self.action == 'list':
|
||
queryset = queryset.only(
|
||
'id', 'uuid', 'chat_type', 'name', 'description', 'avatar',
|
||
'created_by_id', 'lesson_id', 'messages_count', 'last_message_at',
|
||
'is_archived', 'created_at'
|
||
)
|
||
|
||
return queryset
|
||
|
||
def get_serializer_class(self):
|
||
"""Выбор сериализатора."""
|
||
if self.action == 'retrieve':
|
||
return ChatDetailSerializer
|
||
return ChatSerializer
|
||
|
||
@action(detail=True, methods=['get'])
|
||
def messages(self, request, uuid=None):
|
||
"""
|
||
Получить сообщения чата.
|
||
|
||
GET /api/chat/chats/{uuid}/messages/
|
||
"""
|
||
chat = self.get_object()
|
||
messages = Message.objects.filter(
|
||
chat=chat,
|
||
is_deleted=False
|
||
).select_related('sender', 'reply_to', 'reply_to__sender').prefetch_related(
|
||
'files',
|
||
'reactions__user',
|
||
'reads' # Предзагружаем reads для get_is_read
|
||
)
|
||
|
||
# Сортируем по убыванию даты (последние сообщения сначала)
|
||
# НЕ используем only() так как нужны все поля для сериализации (sender, files, reactions)
|
||
# only() может вызвать дополнительные запросы при доступе к связанным полям
|
||
messages = messages.order_by('-created_at')
|
||
|
||
# Используем пагинацию для сообщений
|
||
from rest_framework.pagination import PageNumberPagination
|
||
|
||
paginator = PageNumberPagination()
|
||
paginator.page_size = 30 # Размер страницы по умолчанию
|
||
|
||
# Получаем параметры пагинации из запроса
|
||
page_size = request.query_params.get('page_size', '30')
|
||
try:
|
||
page_size = int(page_size)
|
||
if page_size > 0 and page_size <= 100: # Ограничиваем максимум 100
|
||
paginator.page_size = page_size
|
||
except (ValueError, TypeError):
|
||
pass
|
||
|
||
# Применяем пагинацию
|
||
page = paginator.paginate_queryset(messages, request)
|
||
if page is not None:
|
||
serializer = MessageSerializer(page, many=True, context={'request': request})
|
||
return paginator.get_paginated_response(serializer.data)
|
||
|
||
# Если пагинация не применилась (не должно быть), ограничиваем вручную
|
||
limited_messages = messages[:30]
|
||
serializer = MessageSerializer(limited_messages, many=True, context={'request': request})
|
||
return Response({
|
||
'count': messages.count(),
|
||
'next': None,
|
||
'previous': None,
|
||
'results': serializer.data
|
||
})
|
||
|
||
@action(detail=False, methods=['post'])
|
||
def create_direct(self, request):
|
||
"""
|
||
Создать личный чат с пользователем.
|
||
|
||
Проверяет наличие связи между пользователями.
|
||
"""
|
||
other_user_id = request.data.get('user_id')
|
||
|
||
if not other_user_id:
|
||
return Response({
|
||
'success': False,
|
||
'error': 'user_id обязателен'
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# Проверяем связь
|
||
from apps.users.models import User, Client, Parent
|
||
try:
|
||
other_user = User.objects.get(id=other_user_id)
|
||
except User.DoesNotExist:
|
||
return Response({
|
||
'success': False,
|
||
'error': 'Пользователь не найден'
|
||
}, status=status.HTTP_404_NOT_FOUND)
|
||
|
||
# Логика проверки связей
|
||
has_connection = False
|
||
user = request.user
|
||
|
||
if user.role == 'mentor':
|
||
# Ментор <-> Студент
|
||
if other_user.role == 'client' and Client.objects.filter(user=other_user, mentors=user).exists():
|
||
has_connection = True
|
||
# Ментор <-> Родитель студента
|
||
elif other_user.role == 'parent' and Parent.objects.filter(user=other_user, children__mentors=user).exists():
|
||
has_connection = True
|
||
|
||
elif user.role == 'client':
|
||
# Студент <-> Ментор
|
||
if other_user.role == 'mentor' and Client.objects.filter(user=user, mentors=other_user).exists():
|
||
has_connection = True
|
||
# Студент <-> Родитель
|
||
elif other_user.role == 'parent' and Parent.objects.filter(user=other_user, children__user=user).exists():
|
||
has_connection = True
|
||
|
||
elif user.role == 'parent':
|
||
# Родитель <-> Ментор
|
||
if other_user.role == 'mentor' and User.objects.filter(id=other_user.id, role='mentor', clients__parents__user=user).exists():
|
||
has_connection = True
|
||
# Родитель <-> Ребенок
|
||
elif other_user.role == 'client' and Client.objects.filter(user=other_user, parents__user=user).exists():
|
||
has_connection = True
|
||
|
||
if not has_connection and not user.is_superuser:
|
||
return Response({
|
||
'success': False,
|
||
'error': 'Вы можете создавать чаты только со связанными пользователями'
|
||
}, status=status.HTTP_403_FORBIDDEN)
|
||
|
||
# Проверяем существует ли уже чат
|
||
existing_chat = Chat.objects.filter(
|
||
chat_type='direct',
|
||
participants__user=request.user
|
||
).filter(
|
||
participants__user_id=other_user_id
|
||
).first()
|
||
|
||
if existing_chat:
|
||
serializer = ChatDetailSerializer(existing_chat)
|
||
return Response({
|
||
'success': True,
|
||
'data': serializer.data,
|
||
'message': 'Чат уже существует'
|
||
})
|
||
|
||
# Создаем новый чат
|
||
chat = Chat.objects.create(
|
||
chat_type='direct',
|
||
created_by=request.user
|
||
)
|
||
|
||
# Добавляем участников
|
||
ChatParticipant.objects.create(
|
||
chat=chat,
|
||
user=request.user,
|
||
role='admin'
|
||
)
|
||
|
||
ChatParticipant.objects.create(
|
||
chat=chat,
|
||
user=other_user,
|
||
role='member'
|
||
)
|
||
|
||
serializer = ChatDetailSerializer(chat)
|
||
return Response({
|
||
'success': True,
|
||
'data': serializer.data
|
||
}, status=status.HTTP_201_CREATED)
|
||
|
||
@action(detail=True, methods=['post'])
|
||
def mark_read(self, request, uuid=None):
|
||
"""
|
||
Отметить сообщения как прочитанные.
|
||
|
||
POST /api/chat/chats/{uuid}/mark_read/
|
||
Body: {
|
||
"message_uuids": ["uuid1", "uuid2", ...] # опционально, если не указано - все сообщения
|
||
}
|
||
"""
|
||
chat = self.get_object()
|
||
|
||
try:
|
||
participant = chat.participants.get(user=request.user)
|
||
except ChatParticipant.DoesNotExist:
|
||
return Response({
|
||
'success': False,
|
||
'error': 'Вы не участник этого чата'
|
||
}, status=status.HTTP_403_FORBIDDEN)
|
||
|
||
# Если указаны конкретные UUID сообщений
|
||
message_uuids = request.data.get('message_uuids', [])
|
||
if message_uuids:
|
||
from .models import MessageRead
|
||
messages = Message.objects.filter(
|
||
chat=chat,
|
||
uuid__in=message_uuids,
|
||
is_deleted=False
|
||
)
|
||
|
||
# Оптимизация: получаем существующие записи о прочтении одним запросом
|
||
existing_reads = set(
|
||
MessageRead.objects.filter(
|
||
message__in=messages,
|
||
user=request.user
|
||
).values_list('message_id', flat=True)
|
||
)
|
||
|
||
# Создаем только новые записи через bulk_create
|
||
messages_list = list(messages)
|
||
new_reads = [
|
||
MessageRead(message=message, user=request.user)
|
||
for message in messages_list
|
||
if message.id not in existing_reads
|
||
]
|
||
|
||
if new_reads:
|
||
MessageRead.objects.bulk_create(new_reads, ignore_conflicts=True)
|
||
|
||
# Собираем UUID новых прочитанных сообщений
|
||
read_message_uuids = [
|
||
str(message.uuid) for message in messages_list
|
||
if message.id not in existing_reads
|
||
]
|
||
|
||
# Отправляем уведомление через WebSocket о прочтении сообщений
|
||
if read_message_uuids:
|
||
from channels.layers import get_channel_layer
|
||
from asgiref.sync import async_to_sync
|
||
try:
|
||
channel_layer = get_channel_layer()
|
||
if channel_layer:
|
||
room_group_name = f'chat_{chat.uuid}'
|
||
async_to_sync(channel_layer.group_send)(
|
||
room_group_name,
|
||
{
|
||
'type': 'message_read',
|
||
'user_id': request.user.id,
|
||
'message_uuids': read_message_uuids
|
||
}
|
||
)
|
||
except Exception as e:
|
||
import logging
|
||
logger = logging.getLogger(__name__)
|
||
logger.error(f"Error sending message_read via WebSocket: {e}", exc_info=True)
|
||
|
||
# Уведомляем по WS об обновлении бейджей нижнего меню
|
||
try:
|
||
from apps.notifications.services import WebSocketNotificationService
|
||
WebSocketNotificationService.send_nav_badges_updated(request.user.id)
|
||
except Exception:
|
||
pass
|
||
|
||
# Пересчитываем непрочитанные
|
||
unread_count = Message.objects.filter(
|
||
chat=chat,
|
||
is_deleted=False
|
||
).exclude(
|
||
reads__user=request.user
|
||
).exclude(
|
||
sender=request.user
|
||
).count()
|
||
|
||
participant.unread_count = unread_count
|
||
participant.save(update_fields=['unread_count'])
|
||
else:
|
||
# Отмечаем все сообщения как прочитанные
|
||
from .models import MessageRead
|
||
messages = Message.objects.filter(
|
||
chat=chat,
|
||
is_deleted=False
|
||
).exclude(sender=request.user)
|
||
|
||
# Оптимизация: получаем существующие записи о прочтении одним запросом
|
||
messages_list = list(messages)
|
||
existing_reads = set(
|
||
MessageRead.objects.filter(
|
||
message__in=messages_list,
|
||
user=request.user
|
||
).values_list('message_id', flat=True)
|
||
)
|
||
|
||
# Создаем только новые записи через bulk_create
|
||
new_reads = [
|
||
MessageRead(message=message, user=request.user)
|
||
for message in messages_list
|
||
if message.id not in existing_reads
|
||
]
|
||
|
||
if new_reads:
|
||
MessageRead.objects.bulk_create(new_reads, ignore_conflicts=True)
|
||
|
||
# Обновляем счетчик и время последнего прочтения
|
||
participant.unread_count = 0
|
||
participant.last_read_at = timezone.now()
|
||
participant.save(update_fields=['unread_count', 'last_read_at'])
|
||
|
||
# Уведомляем по WS об обновлении бейджей нижнего меню
|
||
try:
|
||
from apps.notifications.services import WebSocketNotificationService
|
||
WebSocketNotificationService.send_nav_badges_updated(request.user.id)
|
||
except Exception:
|
||
pass
|
||
|
||
return Response({
|
||
'success': True,
|
||
'message': 'Сообщения отмечены как прочитанные'
|
||
})
|
||
|
||
@action(detail=False, methods=['get', 'post'])
|
||
def lesson_chat(self, request):
|
||
"""
|
||
Получить или создать личный чат между участниками урока.
|
||
|
||
Ищет существующий личный чат между ментором и клиентом урока.
|
||
Если чата нет, создает новый личный чат.
|
||
|
||
GET /api/chat/chats/lesson_chat/?lesson_id=123
|
||
POST /api/chat/chats/lesson_chat/
|
||
Body: {
|
||
"lesson_id": 123
|
||
}
|
||
"""
|
||
lesson_id = request.query_params.get('lesson_id') or request.data.get('lesson_id')
|
||
|
||
if not lesson_id:
|
||
return Response({
|
||
'success': False,
|
||
'error': 'lesson_id обязателен'
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
try:
|
||
from apps.schedule.models import Lesson
|
||
lesson = Lesson.objects.select_related('mentor', 'client', 'client__user').get(id=lesson_id)
|
||
except Lesson.DoesNotExist:
|
||
return Response({
|
||
'success': False,
|
||
'error': 'Урок не найден'
|
||
}, status=status.HTTP_404_NOT_FOUND)
|
||
|
||
# Определяем участников чата: ментор и клиент
|
||
mentor = lesson.mentor
|
||
client_user = lesson.client.user if lesson.client and lesson.client.user else None
|
||
|
||
if not mentor or not client_user:
|
||
return Response({
|
||
'success': False,
|
||
'error': 'У урока должны быть указаны ментор и клиент'
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# Ищем существующий личный чат между ментором и клиентом
|
||
existing_chat = Chat.objects.filter(
|
||
chat_type='direct',
|
||
participants__user=mentor
|
||
).filter(
|
||
participants__user=client_user
|
||
).distinct().first()
|
||
|
||
if existing_chat:
|
||
# Проверяем, является ли текущий пользователь участником
|
||
participant = existing_chat.participants.filter(user=request.user).first()
|
||
if not participant:
|
||
# Если текущий пользователь не участник, но это ментор или клиент урока - добавляем
|
||
if request.user == mentor or request.user == client_user:
|
||
ChatParticipant.objects.get_or_create(
|
||
chat=existing_chat,
|
||
user=request.user,
|
||
defaults={'role': 'member'}
|
||
)
|
||
|
||
serializer = ChatDetailSerializer(existing_chat, context={'request': request})
|
||
return Response({
|
||
'success': True,
|
||
'data': serializer.data
|
||
})
|
||
|
||
# Если чата нет, создаем новый личный чат между ментором и клиентом
|
||
# Используем ту же логику, что и в create_direct
|
||
chat = Chat.objects.create(
|
||
chat_type='direct',
|
||
created_by=request.user
|
||
)
|
||
|
||
# Добавляем участников
|
||
ChatParticipant.objects.create(
|
||
chat=chat,
|
||
user=mentor,
|
||
role='admin'
|
||
)
|
||
|
||
ChatParticipant.objects.create(
|
||
chat=chat,
|
||
user=client_user,
|
||
role='member'
|
||
)
|
||
|
||
# Если текущий пользователь не ментор и не клиент, добавляем его тоже
|
||
if request.user != mentor and request.user != client_user:
|
||
ChatParticipant.objects.create(
|
||
chat=chat,
|
||
user=request.user,
|
||
role='member'
|
||
)
|
||
|
||
serializer = ChatDetailSerializer(chat, context={'request': request})
|
||
return Response({
|
||
'success': True,
|
||
'data': serializer.data
|
||
}, status=status.HTTP_201_CREATED)
|
||
|
||
@action(detail=True, methods=['post'])
|
||
def mark_as_read(self, request, uuid=None):
|
||
"""
|
||
Отметить все сообщения как прочитанные (алиас для mark_read).
|
||
|
||
POST /api/chat/chats/{uuid}/mark_as_read/
|
||
"""
|
||
return self.mark_read(request, uuid)
|
||
|
||
@action(detail=True, methods=['post'])
|
||
def archive(self, request, uuid=None):
|
||
"""
|
||
Архивировать чат.
|
||
|
||
POST /api/chat/chats/{uuid}/archive/
|
||
"""
|
||
chat = self.get_object()
|
||
chat.is_archived = True
|
||
chat.save()
|
||
|
||
return Response({
|
||
'success': True,
|
||
'message': 'Чат архивирован'
|
||
})
|
||
|
||
@action(detail=True, methods=['post'])
|
||
def unarchive(self, request, uuid=None):
|
||
"""
|
||
Разархивировать чат.
|
||
|
||
POST /api/chat/chats/{uuid}/unarchive/
|
||
"""
|
||
chat = self.get_object()
|
||
chat.is_archived = False
|
||
chat.save()
|
||
|
||
return Response({
|
||
'success': True,
|
||
'message': 'Чат разархивирован'
|
||
})
|
||
|
||
@action(detail=True, methods=['post'])
|
||
def upload_files(self, request, uuid=None):
|
||
"""
|
||
Предзагрузка файлов в директорию preload_file_chat{id чата}.
|
||
|
||
POST /api/chat/chats/{uuid}/upload_files/
|
||
Body: FormData с файлами
|
||
"""
|
||
chat = self.get_object()
|
||
|
||
# Проверяем доступ
|
||
if not ChatParticipant.objects.filter(chat=chat, user=request.user).exists():
|
||
return Response({
|
||
'success': False,
|
||
'error': 'У вас нет доступа к этому чату'
|
||
}, status=status.HTTP_403_FORBIDDEN)
|
||
|
||
uploaded_files = []
|
||
errors = []
|
||
|
||
# Обрабатываем все переданные файлы
|
||
files = request.FILES.getlist('files')
|
||
if not files:
|
||
return Response({
|
||
'success': False,
|
||
'error': 'Файлы не переданы'
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
for file in files:
|
||
try:
|
||
import uuid as uuid_lib
|
||
import os
|
||
# Генерируем уникальное имя файла
|
||
ext = os.path.splitext(file.name)[1]
|
||
unique_filename = f"{uuid_lib.uuid4()}{ext}"
|
||
|
||
# Сохраняем в preload директорию
|
||
file_path = save_file_to_preload(chat.id, file, unique_filename)
|
||
|
||
uploaded_files.append({
|
||
'original_name': file.name,
|
||
'filename': unique_filename,
|
||
'file_path': file_path,
|
||
'size': file.size,
|
||
'content_type': file.content_type
|
||
})
|
||
except Exception as e:
|
||
errors.append({
|
||
'filename': file.name,
|
||
'error': str(e)
|
||
})
|
||
|
||
return Response({
|
||
'success': True,
|
||
'files': uploaded_files,
|
||
'errors': errors if errors else None
|
||
}, status=status.HTTP_201_CREATED)
|
||
|
||
|
||
class MessageViewSet(viewsets.ModelViewSet):
|
||
"""ViewSet для сообщений."""
|
||
|
||
queryset = Message.objects.all()
|
||
serializer_class = MessageSerializer
|
||
permission_classes = [IsAuthenticated, IsChatParticipant]
|
||
lookup_field = 'uuid'
|
||
|
||
def get_queryset(self):
|
||
"""Сообщения чата."""
|
||
chat_uuid = self.request.query_params.get('chat')
|
||
|
||
if chat_uuid:
|
||
queryset = Message.objects.filter(
|
||
chat__uuid=chat_uuid,
|
||
is_deleted=False
|
||
).select_related('sender', 'chat', 'reply_to', 'reply_to__sender').prefetch_related(
|
||
'files',
|
||
'reactions__user',
|
||
'reads' # Предзагружаем reads для get_is_read
|
||
)
|
||
|
||
# НЕ используем only() так как нужны все поля для сериализации (sender, files, reactions)
|
||
# only() может вызвать дополнительные запросы при доступе к связанным полям
|
||
|
||
return queryset.order_by('-created_at')
|
||
|
||
return Message.objects.none()
|
||
|
||
def get_serializer_class(self):
|
||
"""Выбор сериализатора."""
|
||
if self.action == 'create':
|
||
return MessageCreateSerializer
|
||
return MessageSerializer
|
||
|
||
def create(self, request, *args, **kwargs):
|
||
"""Создание сообщения."""
|
||
# Создаем копию данных
|
||
data = request.data.copy()
|
||
|
||
# Обрабатываем preloaded_files из FormData (QueryDict может вернуть список)
|
||
if 'preloaded_files' in data:
|
||
preloaded_files = data.get('preloaded_files')
|
||
# Если это список (из QueryDict), берем первый элемент (строку JSON)
|
||
if isinstance(preloaded_files, list) and len(preloaded_files) > 0:
|
||
data['preloaded_files'] = preloaded_files[0]
|
||
# Если это уже строка, оставляем как есть
|
||
elif not isinstance(preloaded_files, str):
|
||
data['preloaded_files'] = ''
|
||
|
||
serializer = self.get_serializer(data=data)
|
||
serializer.is_valid(raise_exception=True)
|
||
|
||
# Создаем сообщение (sender уже устанавливается в сериализаторе)
|
||
message = serializer.save()
|
||
|
||
# Отправляем уведомления
|
||
from apps.notifications.services import NotificationService
|
||
NotificationService.send_message_notification(message)
|
||
|
||
# Отправляем сообщение через WebSocket
|
||
from channels.layers import get_channel_layer
|
||
from asgiref.sync import async_to_sync
|
||
from .serializers import MessageSerializer
|
||
|
||
try:
|
||
channel_layer = get_channel_layer()
|
||
if channel_layer:
|
||
# Сериализуем сообщение для отправки через WebSocket
|
||
message_serializer = MessageSerializer(message, context={'request': request})
|
||
message_data = message_serializer.data
|
||
|
||
# Отправляем в группу чата
|
||
room_group_name = f'chat_{message.chat.uuid}'
|
||
async_to_sync(channel_layer.group_send)(
|
||
room_group_name,
|
||
{
|
||
'type': 'chat_message',
|
||
'message': message_data
|
||
}
|
||
)
|
||
# Уведомляем других участников чата об обновлении бейджей
|
||
from apps.notifications.services import WebSocketNotificationService
|
||
from .models import ChatParticipant
|
||
for p in ChatParticipant.objects.filter(chat=message.chat).exclude(user=request.user).select_related('user'):
|
||
WebSocketNotificationService.send_nav_badges_updated(p.user.id)
|
||
except Exception as e:
|
||
# Логируем ошибку, но не прерываем создание сообщения
|
||
import logging
|
||
logger = logging.getLogger(__name__)
|
||
logger.error(f"Error sending message via WebSocket: {e}", exc_info=True)
|
||
|
||
# Возвращаем полные данные сообщения
|
||
output_serializer = MessageSerializer(message, context={'request': request})
|
||
|
||
return Response({
|
||
'success': True,
|
||
'data': output_serializer.data
|
||
}, status=status.HTTP_201_CREATED)
|
||
|
||
def update(self, request, *args, **kwargs):
|
||
"""Редактирование сообщения."""
|
||
message = self.get_object()
|
||
|
||
# Проверяем что пользователь - отправитель
|
||
if message.sender != request.user:
|
||
return Response({
|
||
'success': False,
|
||
'error': 'Вы можете редактировать только свои сообщения'
|
||
}, status=status.HTTP_403_FORBIDDEN)
|
||
|
||
serializer = self.get_serializer(message, data=request.data, partial=True)
|
||
serializer.is_valid(raise_exception=True)
|
||
serializer.save()
|
||
|
||
# Отмечаем как отредактированное
|
||
message.mark_as_edited()
|
||
|
||
return Response({
|
||
'success': True,
|
||
'data': serializer.data
|
||
})
|
||
|
||
def destroy(self, request, *args, **kwargs):
|
||
"""Удаление сообщения."""
|
||
message = self.get_object()
|
||
|
||
# Проверяем что пользователь - отправитель
|
||
if message.sender != request.user:
|
||
return Response({
|
||
'success': False,
|
||
'error': 'Вы можете удалять только свои сообщения'
|
||
}, status=status.HTTP_403_FORBIDDEN)
|
||
|
||
# Мягкое удаление
|
||
message.soft_delete()
|
||
|
||
return Response({
|
||
'success': True,
|
||
'message': 'Сообщение удалено'
|
||
}, status=status.HTTP_204_NO_CONTENT)
|
||
|
||
@action(detail=True, methods=['post'])
|
||
def react(self, request, uuid=None):
|
||
"""
|
||
Добавить реакцию на сообщение.
|
||
|
||
POST /api/chat/messages/{uuid}/react/
|
||
Body: {
|
||
"emoji": "👍"
|
||
}
|
||
"""
|
||
message = self.get_object()
|
||
emoji = request.data.get('emoji')
|
||
|
||
if not emoji:
|
||
return Response({
|
||
'success': False,
|
||
'error': 'emoji обязателен'
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
from .models import MessageReaction
|
||
|
||
# Создаем или удаляем реакцию
|
||
reaction, created = MessageReaction.objects.get_or_create(
|
||
message=message,
|
||
user=request.user,
|
||
emoji=emoji
|
||
)
|
||
|
||
if not created:
|
||
# Если уже есть такая реакция, удаляем
|
||
reaction.delete()
|
||
return Response({
|
||
'success': True,
|
||
'message': 'Реакция удалена'
|
||
})
|
||
|
||
return Response({
|
||
'success': True,
|
||
'message': 'Реакция добавлена'
|
||
}, status=status.HTTP_201_CREATED)
|