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