0

I am new to Djnago and new to WebSockets so I am a little unsure on the proper integration to use when working with messaging.

I have an API view to send a message to a user which adds that message to the database and also create a websocket message. I also have an API view to get the conversation between 2 users.

My logic is that when a user opens their conversation on the front-end, the get_conversation api view will run to get their previous texts and then on the front-end the WebSocket will be used to dynamically update their screen with new texts they are currently sending using the send_message API view.

Is my current logic/implementation the correct approach that is normally used or is there something that could be done differently?

This is the API view I am using to send a message:

@api_view(['POST'])
@permission_classes([IsAuthenticated])
def send_message(request, receiver_id):
    sender = request.user
    content = request.data.get('content')

    # Ensures there is content within the message
    if not content:
        return Response({"error": "Message content is required"}, status=status.HTTP_400_BAD_REQUEST)

    # Ensures the receiving user exists
    try:
        receiver = User.objects.get(id=receiver_id)
    except User.DoesNotExist:
        return Response({"error": "Receiver not found"}, status=status.HTTP_404_NOT_FOUND)

    # Ensures the user is not sending a message to themselves
    if sender == receiver:
        return Response({"error": "Cannot send message to yourself"}, status=status.HTTP_400_BAD_REQUEST)

    # Create a unique room group name using both sender and receiver IDs
    room_group_name = f"group_{min(sender.id, receiver.id)}_{max(sender.id, receiver.id)}"

    try:
        # Use an atomic transaction for creating the Message instance, and informing the WebSocket of the new message
        with transaction.atomic():
            # Create the message
            message = Message.objects.create(sender=sender, receiver=receiver, content=content, is_delivered=True)

            # Notify WebSocket group about the new message
            channel_layer = get_channel_layer()
            async_to_sync(channel_layer.group_send)(
                room_group_name,
                {
                    "type": "chat.message",
                    "content": content,
                    "unique_identifier": str(message.id)  # Use message's ID as unique_identifier
                }
            )
    except Exception as e:
        return Response({"error": "An error occurred while sending the message"},
                        status=status.HTTP_500_INTERNAL_SERVER_ERROR)

    serializer = MessageSerializer(message)

    return Response(serializer.data, status=status.HTTP_201_CREATED)

And this is the API view I am using to get the message conversation between 2 users:

@permission_classes([IsAuthenticated])
def get_conversation(request, user_id):
    # Obtain the user the requesting user has the conversation with
    try:
        user = User.objects.get(id=user_id)
    except User.DoesNotExist:
        return Response({"error": "User not found"}, status=status.HTTP_404_NOT_FOUND)

    # Set a default page size of 20 returned datasets per page
    default_page_size = 20
    # Utility function to get current page number and page size from the request's query parameters and calculate the pagination slicing indeces
    start_index, end_index, validation_response = get_pagination_indeces(request, default_page_size)
    if validation_response:
        return validation_response

    # Get all messages between the requesting user and the receiver
    messages = Message.objects.filter(
        Q(sender=request.user, receiver=user) | Q(sender=user, receiver=request.user))[start_index:end_index]

    # Get the most recent message in the conversation
    most_recent_message = messages.first()

    # Determine the status of the most recent message for the sender (if the receiver has seen their message or not)
    most_recent_sender_status = None  # if the requesting user is the receiver then they don't need a status
    if most_recent_message and most_recent_message.sender == request.user:
        most_recent_sender_status = {
            "id": most_recent_message.id,
            "is_read": most_recent_message.is_read
        }

    # Update unread messages sent by the `user` since the `requesting user` has viewed them after calling this API
    Message.objects.filter(sender=user, receiver=request.user, is_read=False).update(is_read=True)

    serializer = MessageSerializer(messages, many=True)

    return Response({
        "messages": serializer.data,
        "most_recent_sender_status": most_recent_sender_status,
    }, status=status.HTTP_200_OK)

And this is my WebSocket Consumer:

# WebSocket consumer to handle live messaging between users
class MessageConsumer(AsyncWebsocketConsumer):

    # Retrieve a user from the database by their authentication token.
    # wrapped with @database_sync_to_async to allow database access in an asynchronous context.
    @database_sync_to_async
    def get_user_by_token(self, token):
        try:
            return User.objects.get(auth_token=token)
        except User.DoesNotExist:
            return None

    # Retrieve a message from the database by its ID
    @database_sync_to_async
    def get_message(self, message_id):
        try:
            return Message.objects.get(id=message_id)
        except Message.DoesNotExist:
            return None

    # Initiates a WebSocket connection for live messaging.
    async def connect(self):
        # Get headers from the connection's scope
        headers = dict(self.scope["headers"])

        # If user is not authenticated, send an authentication required message and close the connection
        if b"authorization" not in headers:
            await self.accept()
            await self.send(text_data=json.dumps({
                "type": "authentication_required",
                "message": "Authentication is required to access notifications."
            }))
            await self.close()
            return

        # Extract the token from the Authorization header and get the sender user associated with the token
        token = headers[b"authorization"].decode("utf-8").split()[1]
        sender = await self.get_user_by_token(token)
        if sender is None:
            await self.close()
            return
        sender_id = int(sender.id)

        # Store the authenticated user's id to be used in the mark_message_as_read function
        self.auth_user = sender

        # Extract receiver ID from the WebSocket URL parameter
        receiver_id = int(self.scope['url_route']['kwargs']['receiver_id'])

        # Create a unique room group name using both sender and receiver IDs
        self.room_group_name = f"group_{min(sender_id, receiver_id)}_{max(sender_id, receiver_id)}"

        # Join the conversation group
        await self.channel_layer.group_add(self.room_group_name, self.channel_name)

        # Accept the WebSocket connection
        await self.accept()

    # Handles disconnection from the messaging session.
    async def disconnect(self, close_code):
        if hasattr(self, 'notification_group'):
            await self.channel_layer.group_discard(
                self.room_group_name,
                self.channel_name
            )

    # Marks a message in the Live WebSocket conversation as read if the user reading the message is the receiver
    async def mark_message_as_read(self, unique_identifier):
        # Find the message by its unique identifier and mark it as read
        try:
            message_id = int(unique_identifier)
            message = await self.get_message(message_id)

            # Check the receiver of the notification is the authenticated user and the message is not read
            if message.receiver == self.auth_user and not message.is_read:
                message.is_read = True
                message.save()
        except (ValueError, Message.DoesNotExist):
            pass

    # Receives incoming messages from the WebSocket connection and relays the messages to other users in the same chat group.
    async def receive(self, text_data):
        text_data_json = json.loads(text_data)
        content = text_data_json["content"]
        unique_identifier = text_data_json["unique_identifier"]

        # Update is_read status for the received message
        await self.mark_message_as_read(unique_identifier)

        # Send the received message to chat room group
        await self.channel_layer.group_send(
            self.room_group_name,
            {
                "type": "chat.message",
                "content": content,
                "unique_identifier": unique_identifier,
            }
        )

    # Relays messages to users in the chat group and sends the message to the original consumer (WebSocket connection).
    async def chat_message(self, event):
        content = event["content"]
        unique_identifier = event["unique_identifier"]

        await self.send(text_data=json.dumps({
            "type": "message",
            "content": content,
            "unique_identifier": unique_identifier,
        }))

    # Send a WebSocket message to the client indicating that a message should be removed
    async def remove_message(self, event):
        unique_identifier = event["unique_identifier"]

        await self.send(text_data=json.dumps({
            "type": "remove_message",
            "unique_identifier": unique_identifier,
        }))

1 Answers1

0

Your current use of WebSocket with Django API views for messaging is on the right track. You are using WebSocket for real-time updates and have a unique room group name for each conversation. You're handling message sending and receiving, and the WebSocket Consumer structure is suitable for a messaging system using WebSocket. However, your approach may need to be adjusted depending on the specific requirements of your application.