You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

302 lines
12 KiB

import logging
from django.conf import settings
from django.contrib.auth import get_user_model
from django.db.models import Q
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext_lazy as _
from rest_framework import status
from rest_framework.generics import GenericAPIView
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiResponse
from apps.chat.models.chat import ChatRoom, ChatMessage
from apps.chat.permissions import IsRoomParticipant
from apps.chat.services.centrifugo import generate_centrifugo_token, publish_to_centrifugo
from apps.chat.serializers import (
CentrifugoTokenResponseSerializer,
ChatMessageSerializer,
ChatMessageCreateSerializer,
ChatRoomListSerializer,
ChatRoomDetailSerializer,
ChatRoomCreateSerializer,
)
from apps.profiles.models.institution import Institution
from utils.pagination import StandardResultsSetPagination
logger = logging.getLogger(__name__)
User = get_user_model()
class CentrifugoTokenView(GenericAPIView):
permission_classes = [IsAuthenticated]
serializer_class = CentrifugoTokenResponseSerializer
@extend_schema(
summary="Obtain Centrifugo real-time WebSocket connection token",
description="Generates an HMAC-SHA256 connection JWT for client WebSocket authentication with Centrifugo v5.",
responses={200: CentrifugoTokenResponseSerializer},
tags=["Real-Time Messaging & Chat"],
)
def get(self, request, *args, **kwargs):
token = generate_centrifugo_token(request.user)
ws_url = getattr(settings, 'CENTRIFUGO_WS_URL', 'ws://localhost:8001/connection/websocket')
return Response({
'token': token,
'ws_url': ws_url,
'user_id': request.user.id,
'expires_in': 86400 * 7,
}, status=status.HTTP_200_OK)
class ChatRoomListCreateView(GenericAPIView):
permission_classes = [IsAuthenticated]
serializer_class = ChatRoomListSerializer
pagination_class = StandardResultsSetPagination
queryset = ChatRoom.objects.all()
def get_queryset(self):
if getattr(self, 'swagger_fake_view', False):
return ChatRoom.objects.none()
user = self.request.user
user_inst_ids = user.institution_memberships.values_list('institution_id', flat=True)
return ChatRoom.objects.filter(
Q(participant_users=user) |
Q(participant_institutions__id__in=user_inst_ids)
).distinct().prefetch_related('participant_users', 'participant_institutions', 'messages').order_by('-updated_at')
@extend_schema(
summary="List active conversations & direct chats",
description="Returns all active direct and group conversations for the authenticated user with unread counts.",
responses={200: ChatRoomListSerializer(many=True)},
tags=["Real-Time Messaging & Chat"],
)
def get(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True, context={'request': request})
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True, context={'request': request})
return Response(serializer.data, status=status.HTTP_200_OK)
@extend_schema(
summary="Start conversation with user or institution",
description="Creates a new direct chat or multi-institution group room. Reuses existing direct chat if already active.",
request=ChatRoomCreateSerializer,
responses={
201: ChatRoomDetailSerializer,
200: ChatRoomDetailSerializer,
400: OpenApiResponse(description="Validation error"),
},
tags=["Real-Time Messaging & Chat"],
)
def post(self, request, *args, **kwargs):
serializer = ChatRoomCreateSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
data = serializer.validated_data
room_type = data.get('room_type', ChatRoom.RoomType.DIRECT)
title = data.get('title', '')
target_user_id = data.get('target_user_id')
target_institution_id = data.get('target_institution_id')
user_ids = set(data.get('participant_user_ids', []))
inst_ids = set(data.get('participant_institution_ids', []))
# Always include the requesting user
user_ids.add(request.user.id)
if target_user_id:
user_ids.add(target_user_id)
if target_institution_id:
inst_ids.add(target_institution_id)
# Check if direct conversation already exists between the two entities
if room_type == ChatRoom.RoomType.DIRECT and target_user_id:
existing_room = ChatRoom.objects.filter(
room_type=ChatRoom.RoomType.DIRECT,
participant_users=request.user
).filter(participant_users__id=target_user_id).first()
if existing_room:
# If initial message provided, dispatch it
if data.get('initial_message'):
msg = ChatMessage.objects.create(
room=existing_room,
sender=request.user,
content=data['initial_message']
)
publish_to_centrifugo(
existing_room.centrifugo_channel,
{'type': 'new_message', 'message': ChatMessageSerializer(msg, context={'request': request}).data}
)
return Response(ChatRoomDetailSerializer(existing_room, context={'request': request}).data, status=status.HTTP_200_OK)
# Create new room
room = ChatRoom.objects.create(
room_type=room_type,
title=title,
created_by=request.user
)
if user_ids:
room.participant_users.set(User.objects.filter(id__in=user_ids))
if inst_ids:
room.participant_institutions.set(Institution.objects.filter(id__in=inst_ids))
# Send initial message if provided
if data.get('initial_message'):
msg = ChatMessage.objects.create(
room=room,
sender=request.user,
content=data['initial_message']
)
publish_to_centrifugo(
room.centrifugo_channel,
{'type': 'new_message', 'message': ChatMessageSerializer(msg, context={'request': request}).data}
)
return Response(ChatRoomDetailSerializer(room, context={'request': request}).data, status=status.HTTP_201_CREATED)
class ChatRoomDetailView(GenericAPIView):
permission_classes = [IsAuthenticated, IsRoomParticipant]
serializer_class = ChatRoomDetailSerializer
queryset = ChatRoom.objects.all()
def get_object(self):
room = get_object_or_404(ChatRoom, id=self.kwargs['pk'])
self.check_object_permissions(self.request, room)
return room
@extend_schema(
summary="Get chat room details & metadata",
description="Retrieves metadata, participants, and channel name for a conversation.",
responses={
200: ChatRoomDetailSerializer,
403: OpenApiResponse(description="Not a participant in this conversation"),
404: OpenApiResponse(description="Room not found"),
},
tags=["Real-Time Messaging & Chat"],
)
def get(self, request, pk, *args, **kwargs):
room = self.get_object()
serializer = ChatRoomDetailSerializer(room, context={'request': request})
return Response(serializer.data, status=status.HTTP_200_OK)
class ChatMessageListCreateView(GenericAPIView):
permission_classes = [IsAuthenticated, IsRoomParticipant]
serializer_class = ChatMessageSerializer
parser_classes = [MultiPartParser, FormParser, JSONParser]
pagination_class = StandardResultsSetPagination
queryset = ChatMessage.objects.all()
def get_room(self):
room = get_object_or_404(ChatRoom, id=self.kwargs['pk'])
self.check_object_permissions(self.request, room)
return room
@extend_schema(
summary="Get message history for chat room",
description="Returns paginated message history for a conversation and marks messages as read.",
responses={200: ChatMessageSerializer(many=True)},
tags=["Real-Time Messaging & Chat"],
)
def get(self, request, pk, *args, **kwargs):
room = self.get_room()
messages_qs = room.messages.all().select_related('sender', 'sender_institution').order_by('-created_at')
# Automatically mark retrieved messages as read for this user
unread_messages = room.messages.exclude(read_by=request.user).exclude(sender=request.user)
for unread_msg in unread_messages:
unread_msg.read_by.add(request.user)
page = self.paginate_queryset(messages_qs)
if page is not None:
serializer = ChatMessageSerializer(page, many=True, context={'request': request})
return self.get_paginated_response(serializer.data)
serializer = ChatMessageSerializer(messages_qs, many=True, context={'request': request})
return Response(serializer.data, status=status.HTTP_200_OK)
@extend_schema(
summary="Send message in conversation",
description="Persists message to database and broadcasts real-time WebSocket event via Centrifugo.",
request=ChatMessageCreateSerializer,
responses={
201: ChatMessageSerializer,
400: OpenApiResponse(description="Validation error"),
403: OpenApiResponse(description="Permission denied"),
},
tags=["Real-Time Messaging & Chat"],
)
def post(self, request, pk, *args, **kwargs):
room = self.get_room()
serializer = ChatMessageCreateSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
content = serializer.validated_data.get('content', '')
attachment = serializer.validated_data.get('attachment')
sender_inst_id = serializer.validated_data.get('sender_institution_id')
sender_institution = None
if sender_inst_id:
sender_institution = Institution.objects.filter(id=sender_inst_id).first()
message = ChatMessage.objects.create(
room=room,
sender=request.user,
sender_institution=sender_institution,
content=content,
attachment=attachment
)
# Broadcast via Centrifugo HTTP API
msg_data = ChatMessageSerializer(message, context={'request': request}).data
publish_to_centrifugo(
room.centrifugo_channel,
{
'type': 'new_message',
'room_id': room.id,
'message': msg_data
}
)
return Response(msg_data, status=status.HTTP_201_CREATED)
class ChatRoomMarkReadView(GenericAPIView):
permission_classes = [IsAuthenticated, IsRoomParticipant]
serializer_class = ChatMessageSerializer
queryset = ChatRoom.objects.all()
@extend_schema(
summary="Mark all conversation messages as read",
description="Updates read receipts for all messages in the room and emits read event.",
request=None,
responses={200: OpenApiResponse(description="Messages marked as read")},
tags=["Real-Time Messaging & Chat"],
)
def post(self, request, pk, *args, **kwargs):
room = get_object_or_404(ChatRoom, id=pk)
self.check_object_permissions(request, room)
unread_messages = room.messages.exclude(read_by=request.user)
for msg in unread_messages:
msg.read_by.add(request.user)
# Broadcast read receipt via Centrifugo
publish_to_centrifugo(
room.centrifugo_channel,
{
'type': 'messages_read',
'room_id': room.id,
'user_id': request.user.id
}
)
return Response({'status': 'read', 'message': _("All messages marked as read.")}, status=status.HTTP_200_OK)