gukgjzkgjhgjh
This commit is contained in:
@@ -1,25 +1,252 @@
|
||||
from django.shortcuts import render
|
||||
from asgiref.sync import async_to_sync
|
||||
from channels.layers import get_channel_layer
|
||||
from django.contrib.auth import get_user_model
|
||||
from rest_framework import status, viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import PermissionDenied, ValidationError
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_view
|
||||
|
||||
# Create your views here.
|
||||
from .models import Chat, Message, MessageFile
|
||||
from .permissions import CanDeleteMessage, CanManageChat, IsChatMember, IsMessageSenderOnly
|
||||
from .serializers import ChatMemberSerializer, ChatSerializer, MessageSendSerializer, MessageSerializer
|
||||
|
||||
|
||||
def get_users_chats(request):
|
||||
return None
|
||||
def _broadcast(chat_id, payload):
|
||||
"""Push a payload to the channel group for a chat from sync code."""
|
||||
channel_layer = get_channel_layer()
|
||||
async_to_sync(channel_layer.group_send)(f"chat_{chat_id}", payload)
|
||||
|
||||
def create_chat(request):
|
||||
return None
|
||||
|
||||
def invite_user_to_chat(request, chat_id: int, user_ids: list):
|
||||
return None
|
||||
# ---------------------------------------------------------------------------
|
||||
# Chat ViewSet
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def delete_chat(request, chat_id: int):
|
||||
return None
|
||||
@extend_schema_view(
|
||||
list=extend_schema(tags=["chat"], summary="List chats", description="Returns all chats the user is a member of. Superusers see all."),
|
||||
retrieve=extend_schema(tags=["chat"], summary="Retrieve a chat"),
|
||||
create=extend_schema(tags=["chat"], summary="Create a chat", description="Owner is set to the requesting user automatically."),
|
||||
update=extend_schema(tags=["chat"], summary="Replace a chat", description="Owner, moderator, or superuser only."),
|
||||
partial_update=extend_schema(tags=["chat"], summary="Update a chat", description="Owner, moderator, or superuser only."),
|
||||
destroy=extend_schema(tags=["chat"], summary="Delete a chat", description="Soft-deletes. Owner or superuser only."),
|
||||
)
|
||||
class ChatViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = ChatSerializer
|
||||
filterset_fields = ['chat_type', 'hub']
|
||||
search_fields = ['name']
|
||||
ordering_fields = ['created_at', 'name']
|
||||
ordering = ['-created_at']
|
||||
|
||||
def leave_chat(request, chat_id: int):
|
||||
return None
|
||||
def get_permissions(self):
|
||||
if self.action in ('update', 'partial_update', 'destroy',
|
||||
'add_member', 'remove_member', 'add_moderator', 'remove_moderator'):
|
||||
return [CanManageChat()]
|
||||
return [IsChatMember()]
|
||||
|
||||
def edit_chat(request, chat_object):
|
||||
return None
|
||||
def get_queryset(self):
|
||||
user = self.request.user
|
||||
if user.is_superuser:
|
||||
return Chat.objects.all()
|
||||
return Chat.objects.filter(members=user)
|
||||
|
||||
def get_chat_messages(request, chat_id: int, limit: int = 50, offset: int = 0):
|
||||
return None
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(owner=self.request.user)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Member management
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@extend_schema(
|
||||
tags=["chat"],
|
||||
summary="Add a member",
|
||||
description="Owner, moderator, or superuser only.",
|
||||
request=ChatMemberSerializer,
|
||||
responses={200: ChatSerializer},
|
||||
)
|
||||
@action(detail=True, methods=['post'], url_path='members/add')
|
||||
def add_member(self, request, pk=None):
|
||||
chat = self.get_object()
|
||||
ser = ChatMemberSerializer(data=request.data)
|
||||
ser.is_valid(raise_exception=True)
|
||||
User = get_user_model()
|
||||
try:
|
||||
user = User.objects.get(pk=ser.validated_data['user_id'])
|
||||
except User.DoesNotExist:
|
||||
return Response({'detail': 'User not found.'}, status=status.HTTP_404_NOT_FOUND)
|
||||
chat.members.add(user)
|
||||
return Response(ChatSerializer(chat, context={'request': request}).data)
|
||||
|
||||
@extend_schema(
|
||||
tags=["chat"],
|
||||
summary="Remove a member",
|
||||
description="Owner, moderator, or superuser only.",
|
||||
request=ChatMemberSerializer,
|
||||
responses={204: None},
|
||||
)
|
||||
@action(detail=True, methods=['post'], url_path='members/remove')
|
||||
def remove_member(self, request, pk=None):
|
||||
chat = self.get_object()
|
||||
ser = ChatMemberSerializer(data=request.data)
|
||||
ser.is_valid(raise_exception=True)
|
||||
User = get_user_model()
|
||||
try:
|
||||
user = User.objects.get(pk=ser.validated_data['user_id'])
|
||||
except User.DoesNotExist:
|
||||
return Response({'detail': 'User not found.'}, status=status.HTTP_404_NOT_FOUND)
|
||||
chat.members.remove(user)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@extend_schema(
|
||||
tags=["chat"],
|
||||
summary="Add a moderator",
|
||||
description="Owner or superuser only.",
|
||||
request=ChatMemberSerializer,
|
||||
responses={200: ChatSerializer},
|
||||
)
|
||||
@action(detail=True, methods=['post'], url_path='moderators/add')
|
||||
def add_moderator(self, request, pk=None):
|
||||
chat = self.get_object()
|
||||
if not (chat.owner == request.user or request.user.is_superuser):
|
||||
raise PermissionDenied('Only the chat owner or superuser can add moderators.')
|
||||
ser = ChatMemberSerializer(data=request.data)
|
||||
ser.is_valid(raise_exception=True)
|
||||
User = get_user_model()
|
||||
try:
|
||||
user = User.objects.get(pk=ser.validated_data['user_id'])
|
||||
except User.DoesNotExist:
|
||||
return Response({'detail': 'User not found.'}, status=status.HTTP_404_NOT_FOUND)
|
||||
chat.moderators.add(user)
|
||||
return Response(ChatSerializer(chat, context={'request': request}).data)
|
||||
|
||||
@extend_schema(
|
||||
tags=["chat"],
|
||||
summary="Remove a moderator",
|
||||
description="Owner or superuser only.",
|
||||
request=ChatMemberSerializer,
|
||||
responses={204: None},
|
||||
)
|
||||
@action(detail=True, methods=['post'], url_path='moderators/remove')
|
||||
def remove_moderator(self, request, pk=None):
|
||||
chat = self.get_object()
|
||||
if not (chat.owner == request.user or request.user.is_superuser):
|
||||
raise PermissionDenied('Only the chat owner or superuser can remove moderators.')
|
||||
ser = ChatMemberSerializer(data=request.data)
|
||||
ser.is_valid(raise_exception=True)
|
||||
User = get_user_model()
|
||||
try:
|
||||
user = User.objects.get(pk=ser.validated_data['user_id'])
|
||||
except User.DoesNotExist:
|
||||
return Response({'detail': 'User not found.'}, status=status.HTTP_404_NOT_FOUND)
|
||||
chat.moderators.remove(user)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Message ViewSet (read + edit + delete only — creation is via WebSocket)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@extend_schema_view(
|
||||
list=extend_schema(tags=["chat"], summary="List messages", description="Filter by `?chat=<id>`. Chat members only."),
|
||||
retrieve=extend_schema(tags=["chat"], summary="Retrieve a message"),
|
||||
partial_update=extend_schema(tags=["chat"], summary="Edit a message", description="Sender only. Broadcasts `edit.message` to the chat channel group in real time."),
|
||||
update=extend_schema(tags=["chat"], summary="Replace a message", description="Sender only."),
|
||||
destroy=extend_schema(tags=["chat"], summary="Delete a message", description="Sender, chat owner, moderator, or superuser. Broadcasts `delete.message` to the chat channel group in real time."),
|
||||
)
|
||||
class MessageViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = MessageSerializer
|
||||
filterset_fields = ['chat', 'sender', 'reply_to']
|
||||
search_fields = ['content']
|
||||
ordering_fields = ['created_at']
|
||||
ordering = ['created_at']
|
||||
# Standard create is disabled — use POST /messages/send which handles files + WS broadcast
|
||||
http_method_names = ['get', 'patch', 'put', 'delete', 'post', 'head', 'options']
|
||||
|
||||
def get_permissions(self):
|
||||
if self.action == 'destroy':
|
||||
return [CanDeleteMessage()]
|
||||
if self.action in ('update', 'partial_update'):
|
||||
return [IsMessageSenderOnly()]
|
||||
if self.action == 'send':
|
||||
return [IsAuthenticated()]
|
||||
# list, retrieve — any authenticated user (membership enforced by get_queryset)
|
||||
return [IsAuthenticated()]
|
||||
|
||||
def get_queryset(self):
|
||||
user = self.request.user
|
||||
qs = Message.objects.select_related('sender', 'chat').prefetch_related('media_files', 'reactions')
|
||||
if user.is_superuser:
|
||||
return qs
|
||||
# Only messages from chats the user is a member of
|
||||
return qs.filter(chat__members=user)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
message = serializer.instance
|
||||
new_content = serializer.validated_data.get('content', message.content)
|
||||
changed = message.edit_content(new_content)
|
||||
if changed:
|
||||
_broadcast(message.chat_id, {
|
||||
'type': 'edit.message',
|
||||
'message_id': message.id,
|
||||
'content': message.content,
|
||||
'is_edited': True,
|
||||
})
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
chat_id = instance.chat_id
|
||||
message_id = instance.id
|
||||
instance.delete()
|
||||
_broadcast(chat_id, {
|
||||
'type': 'delete.message',
|
||||
'message_id': message_id,
|
||||
})
|
||||
|
||||
@extend_schema(
|
||||
tags=["chat"],
|
||||
summary="Send a message",
|
||||
description=(
|
||||
"Creates a message with optional file attachments and broadcasts it to all "
|
||||
"connected WebSocket clients in the chat. Use this for all messages that include "
|
||||
"files, and optionally for text-only messages too. Chat members only."
|
||||
),
|
||||
request=MessageSendSerializer,
|
||||
responses={201: MessageSerializer},
|
||||
)
|
||||
@action(detail=False, methods=['post'], url_path='send')
|
||||
def send(self, request):
|
||||
ser = MessageSendSerializer(data=request.data)
|
||||
ser.is_valid(raise_exception=True)
|
||||
|
||||
chat = ser.validated_data['chat']
|
||||
if not request.user.is_superuser and not chat.members.filter(pk=request.user.pk).exists():
|
||||
raise PermissionDenied('You are not a member of this chat.')
|
||||
|
||||
message = Message.objects.create(
|
||||
chat=chat,
|
||||
sender=request.user,
|
||||
content=ser.validated_data.get('content', ''),
|
||||
reply_to=ser.validated_data.get('reply_to'),
|
||||
)
|
||||
|
||||
for f in request.FILES.getlist('files'):
|
||||
ct = getattr(f, 'content_type', '')
|
||||
if ct.startswith('image/'):
|
||||
mt = 'IMAGE'
|
||||
elif ct.startswith('video/'):
|
||||
mt = 'VIDEO'
|
||||
else:
|
||||
mt = 'FILE'
|
||||
MessageFile.objects.create(message=message, file=f, media_type=mt)
|
||||
|
||||
_broadcast(chat.id, {
|
||||
'type': 'chat.message',
|
||||
'message_id': message.id,
|
||||
'message': message.content,
|
||||
'sender': request.user.username,
|
||||
'has_files': message.media_files.exists(),
|
||||
})
|
||||
|
||||
return Response(
|
||||
MessageSerializer(message, context={'request': request}).data,
|
||||
status=status.HTTP_201_CREATED,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user