gukgjzkgjhgjh

This commit is contained in:
2026-04-20 00:04:15 +02:00
parent 5280a87e8b
commit 659999f4fd
409 changed files with 19957 additions and 5176 deletions

View File

@@ -0,0 +1,68 @@
chat app diagram
┌─────────────────────────────────────────────────────────────────┐
│ CLIENT (browser) │
└────────────┬────────────────────────────┬───────────────────────┘
│ WebSocket │ HTTP REST
│ ws/chat/<id>/ │ /api/social/
▼ ▼
┌────────────────────────┐ ┌────────────────────────────────────┐
│ ChatConsumer │ │ REST Views │
│ (consumers.py) │ │ (views.py) │
│ │ │ │
│ connect() │ │ ChatViewSet │
│ ├─ auth check │ │ ├─ list / retrieve (GET) │
│ └─ membership check ──┼───┼──► IsChatMember │
│ │ │ ├─ create (POST) │
│ receive() │ │ ├─ update/partial (PATCH/PUT) │
│ ├─ new_chat_message │ │ │ └─ CanManageChat │
│ │ (text only) │ │ ├─ destroy (DELETE) │
│ │ └─► _create_message │ │ └─ CanManageChat │
│ │ (DB INSERT) │ │ └─ add/remove member & moderator │
│ │ │ │ │
│ ├─ new_reply_message │ │ MessageViewSet │
│ │ (text only) │ │ ├─ list / retrieve (GET) │
│ │ └─► _create_message │ │ └─ IsAuthenticated │
│ │ (DB INSERT) │ │ ├─ send (POST) │
│ │ │ │ │ ├─ IsAuthenticated │
│ └─ reaction │ │ │ ├─ DB INSERT Message │
│ └─► _toggle_reaction │ │ ├─ DB INSERT MessageFile(s) │
│ (DB UPDATE/ │ │ │ └─► group_send(chat.msg) ─────┼──► WebSocket push
│ DELETE) │ │ ├─ update/partial (PATCH/PUT) │
│ │ │ │ ├─ IsMessageSenderOnly │
│ typing / stop_typing │ │ │ ├─ message.edit_content() │
│ └─► group_send only │ │ │ └─► group_send(edit.msg) ─────┼──► WebSocket push
│ (no DB write) │ │ └─ destroy (DELETE) │
│ │ │ ├─ CanDeleteMessage │
│ │ │ └─► group_send(delete.msg) ───┼──► WebSocket push
└────────────┬───────────┘ └──────────────┬─────────────────────┘
│ │
│ both use channel_layer │
▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ Channel Layer (Redis) │
│ group: "chat_{id}" │
└─────────────────────────────────────────────────────────────────┘
▼ group_send dispatches to all connected consumers
┌─────────────────────────────────────────────────────────────────┐
│ ChatConsumer event handlers (push to each connected client) │
│ chat_message · reply_chat_message · edit_message │
│ delete_message · message_reaction · typing_status · stop_typing│
└─────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────┐ ┌──────────────────────────┐
│ Database writes summary │ │ When to use which path │
│ │ │ │
│ CREATE Message ← consumer │ │ WS ← text-only msg │
│ (text only) │ │ HTTP← msg with files │
│ CREATE Message ← view /send │ │ HTTP← edit message │
│ (any) │ │ HTTP← delete message │
│ CREATE MessageFile ← view/send │ │ WS ← reaction │
│ UPDATE Message ← view │ │ WS ← typing indicator │
│ DELETE Message ← view (soft) │ └──────────────────────────┘
│ CREATE MessageReaction ← cons │
│ UPDATE MessageReaction ← cons │
│ DELETE MessageReaction ← cons │
│ CREATE MessageHistory ← model │
│ (auto on edit_content) │
└──────────────────────────────────┘

View File

@@ -1,11 +1,185 @@
# chat/consumers.py
import json
from account.models import UserProfile
from channels.db import database_sync_to_async
from channels.generic.websocket import AsyncWebsocketConsumer
from asgiref.sync import sync_to_async, async_to_sync
from .models import Chat, Message
class ChatConsumer(AsyncWebsocketConsumer):
# -- CONNECT --
async def connect(self):
self.chat_id = self.scope["url_route"]["kwargs"]["chat_id"]
self.chat_name = f"chat_{self.chat_id}"
user = self.scope["user"]
if not user.is_authenticated:
await self.close(code=4401)
return
is_member = await _is_chat_member(self.chat_id, user)
if not is_member:
await self.close(code=4403)
return
await self.channel_layer.group_add(self.chat_name, self.channel_name)
await self.accept()
# -- DISCONNECT --
async def disconnect(self, close_code):
await self.channel_layer.group_discard(self.chat_name, self.channel_name)
# -- RECEIVE --
async def receive(self, text_data):
data = json.loads(text_data)
user = self.scope["user"]
msg_type = data.get("type")
if msg_type == "new_chat_message":
message = await _create_message(
chat_id=self.chat_id,
sender=user,
content=data["message"],
)
await self.channel_layer.group_send(self.chat_name, {
"type": "chat.message",
"message_id": message.id,
"message": message.content,
"sender": user.username,
})
elif msg_type == "new_reply_chat_message":
message = await _create_message(
chat_id=self.chat_id,
sender=user,
content=data["message"],
reply_to_id=data.get("reply_to_id"),
)
await self.channel_layer.group_send(self.chat_name, {
"type": "reply.chat.message",
"message_id": message.id,
"message": message.content,
"reply_to_id": data.get("reply_to_id"),
"sender": user.username,
})
elif msg_type == "reaction":
action, reaction = await _toggle_reaction(
message_id=data["message_id"],
user=user,
emoji=data["emoji"],
)
await self.channel_layer.group_send(self.chat_name, {
"type": "message.reaction",
"message_id": data["message_id"],
"emoji": data["emoji"],
"user": user.username,
"action": action, # 'added' | 'removed' | 'switched'
})
elif msg_type == "typing":
await self.channel_layer.group_send(self.chat_name, {
"type": "typing.status",
"user": user.username,
"is_typing": data.get("is_typing", True),
})
elif msg_type == "stop_typing":
await self.channel_layer.group_send(self.chat_name, {
"type": "stop.typing",
"user": user.username,
})
else:
await self.send(text_data=json.dumps({"error": "Unsupported message type."}))
# -- GROUP EVENT HANDLERS --
# These are called by the channel layer when another part of the system
# (consumer or view) calls group_send.
async def chat_message(self, event):
await self.send(text_data=json.dumps({
"type": "new_chat_message",
"message_id": event["message_id"],
"message": event["message"],
"sender": event["sender"],
}))
async def reply_chat_message(self, event):
await self.send(text_data=json.dumps({
"type": "new_reply_chat_message",
"message_id": event["message_id"],
"message": event["message"],
"reply_to_id": event["reply_to_id"],
"sender": event["sender"],
}))
async def edit_message(self, event):
await self.send(text_data=json.dumps({
"type": "edit_chat_message",
"message_id": event["message_id"],
"content": event["content"],
"is_edited": event.get("is_edited", True),
}))
async def delete_message(self, event):
await self.send(text_data=json.dumps({
"type": "delete_chat_message",
"message_id": event["message_id"],
}))
async def message_reaction(self, event):
await self.send(text_data=json.dumps({
"type": "reaction",
"message_id": event["message_id"],
"emoji": event["emoji"],
"user": event["user"],
"action": event["action"],
}))
async def typing_status(self, event):
await self.send(text_data=json.dumps({
"type": "typing",
"user": event["user"],
"is_typing": event["is_typing"],
}))
async def stop_typing(self, event):
await self.send(text_data=json.dumps({
"type": "stop_typing",
"user": event["user"],
}))
# ---------------------------------------------------------------------------
# DB helpers (run in thread pool via database_sync_to_async)
# ---------------------------------------------------------------------------
@database_sync_to_async
def _is_chat_member(chat_id, user):
return Chat.objects.filter(pk=chat_id, members=user).exists()
@database_sync_to_async
def _create_message(chat_id, sender, content, reply_to_id=None):
return Message.objects.create(
chat_id=chat_id,
sender=sender,
content=content,
reply_to_id=reply_to_id,
)
@database_sync_to_async
def _toggle_reaction(message_id, user, emoji):
message = Message.objects.get(pk=message_id)
return message.toggle_reaction(user, emoji)
class ChatConsumer(AsyncWebsocketConsumer):

View File

@@ -0,0 +1,98 @@
# Generated by Django 5.2.7 on 2026-04-19 21:51
import django.db.models.deletion
import vontor_cz.custom_fields
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('chat', '0001_initial'),
('hubs', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='chat',
name='banner',
field=vontor_cz.custom_fields.WebPImageField(blank=True, null=True, upload_to='chat_banners/'),
),
migrations.AddField(
model_name='chat',
name='chat_type',
field=models.CharField(choices=[('DM', 'Direct Message'), ('GROUP', 'Group')], default='GROUP', max_length=10),
),
migrations.AddField(
model_name='chat',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='chat',
name='hub',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='hubs.hub'),
),
migrations.AddField(
model_name='chat',
name='icon',
field=vontor_cz.custom_fields.WebPImageField(blank=True, null=True, upload_to='chat_icons/'),
),
migrations.AddField(
model_name='chat',
name='is_deleted',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='chat',
name='name',
field=models.CharField(blank=True, max_length=255),
),
migrations.AddField(
model_name='message',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='message',
name='is_deleted',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='messagefile',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='messagefile',
name='is_deleted',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='messagehistory',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='messagehistory',
name='is_deleted',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='messagereaction',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='messagereaction',
name='is_deleted',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='message',
name='sender',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sent_messages', to=settings.AUTH_USER_MODEL),
),
]

View File

@@ -43,7 +43,7 @@ class Chat(SoftDeleteModel):
)
hub = models.ForeignKey(
'pages.Hub',
'hubs.Hub',
on_delete=models.CASCADE,
null=True,
blank=True

View File

@@ -0,0 +1,65 @@
from rest_framework.permissions import IsAuthenticated, SAFE_METHODS
class IsChatMember(IsAuthenticated):
"""
View-level: must be authenticated (inherited).
Object-level: safe methods require chat membership; unsafe require membership too.
Used for reading messages and listing chat details.
"""
def has_object_permission(self, request, view, obj):
return request.user.is_superuser or obj.members.filter(pk=request.user.pk).exists()
class CanManageChat(IsAuthenticated):
"""
View-level: must be authenticated (inherited).
Object-level unsafe: chat owner, moderator, or superuser.
Used for editing/deleting the chat itself and managing members.
"""
def has_object_permission(self, request, view, obj):
if request.method in SAFE_METHODS:
return True
user = request.user
return (
user.is_superuser
or obj.owner == user
or obj.moderators.filter(pk=user.pk).exists()
)
class IsMessageSenderOnly(IsAuthenticated):
"""
View-level: must be authenticated (inherited).
Object-level unsafe: message sender only.
Used for editing messages.
"""
def has_object_permission(self, request, view, obj):
if request.method in SAFE_METHODS:
return True
return obj.sender == request.user
class CanDeleteMessage(IsAuthenticated):
"""
View-level: must be authenticated (inherited).
Object-level DELETE:
- Message sender
- Superuser
- Chat owner
- Chat moderator
"""
def has_object_permission(self, request, view, obj):
if request.method in SAFE_METHODS:
return True
user = request.user
if obj.sender == user or user.is_superuser:
return True
return (
obj.chat.owner == user
or obj.chat.moderators.filter(pk=user.pk).exists()
)

View File

@@ -0,0 +1,74 @@
from rest_framework import serializers
from .models import Chat, Message, MessageFile, MessageHistory, MessageReaction
class MessageFileSerializer(serializers.ModelSerializer):
class Meta:
model = MessageFile
fields = ['id', 'file', 'media_type', 'uploaded_at']
read_only_fields = ['uploaded_at']
class MessageReactionSerializer(serializers.ModelSerializer):
class Meta:
model = MessageReaction
fields = ['id', 'user', 'emoji', 'created_at']
read_only_fields = ['user', 'created_at']
class MessageHistorySerializer(serializers.ModelSerializer):
class Meta:
model = MessageHistory
fields = ['id', 'old_content', 'archived_at']
read_only_fields = ['archived_at']
class MessageSerializer(serializers.ModelSerializer):
media_files = MessageFileSerializer(many=True, read_only=True)
reactions = MessageReactionSerializer(many=True, read_only=True)
class Meta:
model = Message
fields = [
'id', 'chat', 'sender', 'reply_to',
'content', 'is_edited', 'edited_at',
'created_at', 'updated_at',
'media_files', 'reactions',
]
read_only_fields = ['sender', 'chat', 'reply_to', 'is_edited', 'edited_at', 'created_at', 'updated_at']
class MessageSendSerializer(serializers.Serializer):
"""Used for the HTTP send endpoint (text + optional files)."""
chat = serializers.PrimaryKeyRelatedField(queryset=Chat.objects.all())
content = serializers.CharField(required=False, allow_blank=True, default='')
reply_to = serializers.PrimaryKeyRelatedField(
queryset=Message.objects.all(), required=False, allow_null=True
)
# files come via request.FILES - declared here for schema documentation only
files = serializers.ListField(
child=serializers.FileField(allow_empty_file=False, use_url=False),
required=False,
default=list,
write_only=True,
)
def validate(self, attrs):
if not attrs.get('content') and not attrs.get('files'):
raise serializers.ValidationError('A message must have content or at least one file.')
return attrs
class ChatSerializer(serializers.ModelSerializer):
class Meta:
model = Chat
fields = [
'id', 'chat_type', 'owner', 'name',
'icon', 'banner', 'members', 'moderators',
'hub', 'created_at', 'updated_at',
]
read_only_fields = ['owner', 'created_at', 'updated_at']
class ChatMemberSerializer(serializers.Serializer):
user_id = serializers.IntegerField(help_text='PK of the user to add or remove.')

View File

@@ -0,0 +1,8 @@
from rest_framework.routers import DefaultRouter
from .views import ChatViewSet, MessageViewSet
router = DefaultRouter()
router.register('chats', ChatViewSet, basename='chat')
router.register('messages', MessageViewSet, basename='message')
urlpatterns = router.urls

View File

@@ -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,
)