Step-by-Step Instructions
Step 1: Refine the Chat Consumer
- Update the
ChatConsumerinchat/consumers.pyto handle additional WebSocket functionality:
from channels.generic.websocket import AsyncWebsocketConsumer
from asgiref.sync import sync_to_async
from .models import ChatRoom, Message
from django.contrib.auth.models import User
import json
class ChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.room_name = self.scope['url_route']['kwargs']['room_name']
self.room_group_name = f'chat_{self.room_name}'
# Add user presence
self.user = self.scope["user"]
if not self.user.is_authenticated:
await self.close()
return
# Join room group
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)
# Accept connection
await self.accept()
# Notify group about user joining
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'user_join',
'username': self.user.username,
}
)
async def disconnect(self, close_code):
# Notify group about user leaving
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'user_leave',
'username': self.user.username,
}
)
# Leave room group
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)
async def receive(self, text_data):
try:
data = json.loads(text_data)
message = data.get('message', '').strip()
if message:
# Save message to database
await self.save_message(self.user.username, self.room_name, message)
# Send message to the group
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'chat_message',
'message': message,
'username': self.user.username,
}
)
except Exception as e:
# Handle invalid JSON or other issues
await self.send(text_data=json.dumps({
'error': 'Invalid message format or content.'
}))
async def chat_message(self, event):
# Broadcast a chat message to WebSocket clients
message = event['message']
username = event['username']
await self.send(text_data=json.dumps({
'message': message,
'username': username,
}))
async def user_join(self, event):
# Notify WebSocket clients about a new user joining
username = event['username']
await self.send(text_data=json.dumps({
'info': f'{username} has joined the chat.'
}))
async def user_leave(self, event):
# Notify WebSocket clients about a user leaving
username = event['username']
await self.send(text_data=json.dumps({
'info': f'{username} has left the chat.'
}))
@sync_to_async
def save_message(self, username, room_name, message):
room = ChatRoom.objects.get(name=room_name)
user = User.objects.get(username=username)
Message.objects.create(user=user, room=room, content=message)
Step 2: Enhance the Frontend for Presence Updates
- Update the
room.htmltemplate to handle notifications for user join/leave events:
<h2>Room: {{ room_name }}</h2>
<div id="chat-log"></div>
<input id="chat-message-input" type="text" size="100">
<button id="chat-message-submit">Send</button>
<script>
const roomName = "{{ room_name }}";
const chatSocket = new WebSocket(
'ws://' + window.location.host + '/ws/chat/' + roomName + '/'
);
chatSocket.onmessage = function(e) {
const data = JSON.parse(e.data);
if (data.message) {
document.querySelector('#chat-log').innerHTML += (
'<p><strong>' + data.username + ':</strong> ' + data.message + '</p>'
);
} else if (data.info) {
document.querySelector('#chat-log').innerHTML += (
'<p><em>' + data.info + '</em></p>'
);
} else if (data.error) {
console.error(data.error);
}
};
chatSocket.onclose = function(e) {
console.error('Chat socket closed unexpectedly');
};
document.querySelector('#chat-message-submit').onclick = function(e) {
const messageInputDom = document.querySelector('#chat-message-input');
const message = messageInputDom.value;
chatSocket.send(JSON.stringify({ 'message': message }));
messageInputDom.value = '';
};
</script>
Step 3: Retrieve Chat History
- Update the
ChatConsumerto load the last 50 messages when a user connects:
from channels.generic.websocket import AsyncWebsocketConsumer
from asgiref.sync import sync_to_async
from .models import ChatRoom, Message
from django.contrib.auth.models import User
import json
class ChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
"""Handle WebSocket connection."""
self.room_name = self.scope['url_route']['kwargs']['room_name']
self.room_group_name = f'chat_{self.room_name}'
# Authenticate user
self.user = self.scope["user"]
if not self.user.is_authenticated:
await self.close()
return
# Join room group
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)
# Accept connection
await self.accept()
# Send the last 50 messages to the client
messages = await self.get_last_messages(self.room_name)
for message in messages:
await self.send(text_data=json.dumps({
'message': message['content'],
'username': message['username'],
}))
# Notify group about user joining
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'user_join',
'username': self.user.username,
}
)
@sync_to_async
def get_last_messages(self, room_name):
"""Retrieve the last 50 messages for the chat room."""
try:
room = ChatRoom.objects.get(name=room_name)
messages = room.messages.order_by('-timestamp')[:50]
# Return a list of dictionaries to avoid ORM-related issues in async context
return [
{'content': message.content, 'username': message.user.username}
for message in messages
][::-1]
except ChatRoom.DoesNotExist:
return [] # Return an empty list if the room does not exist
async def disconnect(self, close_code):
"""Handle WebSocket disconnection."""
# Notify group about user leaving
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'user_leave',
'username': self.user.username,
}
)
# Leave room group
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)
async def receive(self, text_data):
"""Handle incoming messages from WebSocket."""
try:
data = json.loads(text_data)
message = data.get('message', '').strip()
if message:
# Save message to the database
await self.save_message(self.user.username, self.room_name, message)
# Send message to the group
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'chat_message',
'message': message,
'username': self.user.username,
}
)
except Exception:
# Handle invalid JSON or other issues
await self.send(text_data=json.dumps({
'error': 'Invalid message format or content.'
}))
async def chat_message(self, event):
"""Broadcast a chat message to WebSocket clients."""
message = event['message']
username = event['username']
await self.send(text_data=json.dumps({
'message': message,
'username': username,
}))
async def user_join(self, event):
"""Notify WebSocket clients about a new user joining."""
username = event['username']
await self.send(text_data=json.dumps({
'info': f'{username} has joined the chat.'
}))
async def user_leave(self, event):
"""Notify WebSocket clients about a user leaving."""
username = event['username']
await self.send(text_data=json.dumps({
'info': f'{username} has left the chat.'
}))
@sync_to_async
def save_message(self, username, room_name, message):
"""Save a chat message to the database."""
room = ChatRoom.objects.get(name=room_name)
user = User.objects.get(username=username)
Message.objects.create(user=user, room=room, content=message)
Step 4: Test WebSocket Communication
- Restart the server:
python manage.py runserver
Open multiple browser tabs or use different accounts to simulate multiple users joining a room.
Ensure:
- Messages are broadcasted in real-time.
- Join and leave notifications appear.
- Chat history loads for new users.