How Can I Get Django Global Notifications Via Channels To Work?
I have spent the last month or so on and off trying to get the django channels notification to work. I can't get notifications to work but the chat works as expected. Here is my code...
Consumers.py
class ChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.is_group_chat = 'group_name' in self.scope['url_route']['kwargs']
if self.is_group_chat:
group_name = self.scope['url_route']['kwargs'].get('group_name')
self.group_name = group_name.replace('_', ' ') # Replace underscores with spaces
try:
self.group_chat = await database_sync_to_async(GroupChat.objects.get)(name=self.group_name)
except GroupChat.DoesNotExist:
await self.close()
return
self.chat_group = f'group_{self.group_name.replace(" ", "_")}'
else:
self.recipient_username = self.scope['url_route']['kwargs'].get('recipient_username')
self.sender = self.scope['user']
self.conversation_group = self.get_conversation_group_name(
self.sender.username,
self.recipient_username
)
# Convert recipient_username to a User object and assign it to self.recipient
try:
self.recipient = await database_sync_to_async(get_user_model().objects.get)(username=self.recipient_username)
except get_user_model().DoesNotExist:
await self.close()
return
# Join the group
if self.is_group_chat:
await self.channel_layer.group_add(
self.chat_group,
self.channel_name
)
else:
await self.channel_layer.group_add(
self.conversation_group,
self.channel_name
)
await self.accept()
def get_conversation_group_name(self, user1, user2):
return f'chat_{sorted([user1, user2])[0]}_{sorted([user1, user2])[1]}'
async def disconnect(self, close_code):
if self.is_group_chat:
await self.channel_layer.group_discard(
self.chat_group,
self.channel_name
)
else:
await self.channel_layer.group_discard(
self.conversation_group,
self.channel_name
)
async def receive(self, text_data):
text_data_json = json.loads(text_data)
timestamp = timezone.now().isoformat()
sender = self.scope['user']
sender_full_name = await self.get_user_full_name(sender) # Fetch full name
message = text_data_json.get('message', '')
attachment = text_data_json.get('attachment', None)
message_type = text_data_json.get('type')
# Ensure sender full name is calculated
sender_full_name = await self.get_user_full_name(sender)
if message_type == 'delete_message':
message_id = text_data_json['message_id']
await self.delete_message(message_id)
elif self.is_group_chat:
# Save the group message
recipients = self.group_chat.members.all() # Get all members of the group chat
# Initialize the message object with default values
chat_message = {
'sender': sender.username,
'sender_pk': sender.pk,
'sender_full_name': sender_full_name,
'timestamp': timezone.now().isoformat(), # Adjust timestamp if necessary
'message': message, # The message text
'attachment': None # Default to None
}
# If an attachment is present, process it
if attachment:
file_data = attachment.get('file_data')
file_name = attachment.get('file_name')
file_type = attachment.get('file_type')
# Save the file and get its URL
file_url = await self.save_file(file_data, file_name, is_group_message=True)
# Update the message with the attachment URL and other details
chat_message['attachment'] = {
'file_url': file_url, # The URL for downloading the file
'file_name': file_name,
'file_type': file_type,
}
# Save message with file info
saved_message = await self.save_group_message(sender, "", recipients=recipients, file_url=file_url)
else:
# Save message without file
saved_message = await self.save_group_message(sender, message, recipients=recipients)
# Send the message to all group members (only once)
await self.channel_layer.group_send(
self.chat_group,
{
'type': 'chat_message',
'message': chat_message['message'],
'sender': chat_message['sender'],
'sender_pk': chat_message['sender_pk'],
'sender_full_name': chat_message['sender_full_name'],
'timestamp': chat_message['timestamp'],
'message_id': str(saved_message.id), # Convert UUID to string
'is_sender': True, # This indicates it’s from the sender
'attachment': chat_message['attachment'] # Include attachment if present
}
)
# Create GroupMessageRead entries for each recipient (except the sender)
for recipient in recipients:
if recipient != sender:
await self.create_group_message_read(saved_message, recipient)
# Check if both message and attachment are present
elif message_type == 'message' or message_type == 'attachment' or (message and attachment):
# Prepare the message object
chat_message = {
'sender': sender.username,
'sender_pk': sender.pk,
'sender_full_name': sender_full_name,
'timestamp': timezone.now().isoformat(), # Adjust timestamp if necessary
'message': message, # Message text, could be empty if only attachment
'attachment': None # Default to None
}
# If an attachment is present, process it
if attachment:
file_data = attachment.get('file_data')
file_name = attachment.get('file_name')
file_type = attachment.get('file_type')
# Save the file and get its URL
file_url = await self.save_file(file_data, file_name)
# Update the message with file URL
chat_message['attachment'] = {
'file_url': file_url, # The URL for downloading the file
'file_name': file_name,
'file_type': file_type,
}
# Save message with file info
saved_message = await self.save_direct_message(sender, "", file_url=file_url)
else:
# Save message without file
saved_message = await self.save_direct_message(sender, message)
# Broadcast the message (with or without attachment)
await self.channel_layer.group_send(
self.conversation_group,
{
'type': 'chat_message',
'message': chat_message['message'],
'sender': chat_message['sender'],
'sender_pk': chat_message['sender_pk'],
'timestamp': saved_message.timestamp.isoformat(),
'message_id': str(saved_message.id),
'sender_full_name': chat_message['sender_full_name'],
'attachment': chat_message['attachment'] # Include attachment if present
}
)
async def chat_message(self, event):
# Check if there's an attachment and include it in the event data
attachment = event.get('attachment', None)
# Send the message along with attachment data to WebSocket
await self.send(text_data=json.dumps({
'message': event['message'],
'sender': event['sender'],
'sender_pk': event['sender_pk'],
'sender_full_name': event['sender_full_name'],
'timestamp': event['timestamp'],
'message_id': event['message_id'],
'is_sender': event.get('is_sender', False),
'attachment': attachment # Include the attachment if there is one
}))
async def chat_attachment(self, event):
# Send the file attachment details to WebSocket
await self.send(text_data=json.dumps({
'attachment': {
'file_url': event['file_url'],
'file_name': event['file_name'],
'file_type': event['file_type'],
}
}))
async def save_attachment(self, file_data, file_name, file_type):
# Save the file (assuming base64 encoding or binary data)
file_content = ContentFile(file_data)
file_path = default_storage.save(f'attachments/{file_name}', file_content)
# Return the file URL
file_url = default_storage.url(file_path)
return file_url
async def send_file_attachment(self, file_url, file_name, file_type):
# Send file attachment information to group
if self.is_group_chat:
await self.channel_layer.group_send(
self.chat_group,
{
'type': 'chat_attachment',
'file_url': file_url,
'file_name': file_name,
'file_type': file_type
}
)
else:
await self.channel_layer.group_send(
self.conversation_group,
{
'type': 'chat_attachment',
'file_url': file_url,
'file_name': file_name,
'file_type': file_type
}
)
@database_sync_to_async
def get_recipients(self):
# Fetch all members of the group chat
return self.group_chat.members.all()
@database_sync_to_async
def create_group_message_read(self, group_message, recipient):
GroupMessageRead.objects.get_or_create(
group_message=group_message,
recipient=recipient,
read=False # Initially, the message is unread for this user
)
@database_sync_to_async
def save_direct_message(self, sender, message, file_url=None):
recipient = get_user_model().objects.get(username=self.recipient_username)
# If there's a file, save it in the `attachment` field
if file_url:
return DirectMessage.objects.create(
sender=sender,
recipient=recipient,
message=message,
attachment=file_url, # Attach the file URL
read=False
)
else:
return DirectMessage.objects.create(
sender=sender,
recipient=recipient,
message=message,
read=False
)
@database_sync_to_async
def save_group_message(self, sender, message, recipients, file_url=None):
# Save the group message to the database
group_message = GroupMessage.objects.create(
sender=sender,
group=self.group_chat,
message=message,
timestamp=timezone.now().isoformat(),
attachment=file_url, # Pass the attachment URL directly
)
# Create GroupMessageRead entries for all recipients (except the sender)
for recipient in recipients:
if recipient != sender:
GroupMessageRead.objects.create(
group_message=group_message,
recipient=recipient,
read=False # Mark as unread initially
)
return group_message
# Save file method updated to handle different directories for group vs. direct messages
@database_sync_to_async
def save_file(self, file_data, file_name, is_group_message=False):
# Decode the base64 file data
file_content = ContentFile(base64.b64decode(file_data)) # Decode to file content
# Determine the directory based on message type
if is_group_message:
file_path = default_storage.save(f'attachments/group_chat/{file_name}', file_content) # Group messages go to group_chat folder
else:
file_path = default_storage.save(f'attachments/chat/{file_name}', file_content) # Direct messages go to chat folder
# Get the URL of the uploaded file
file_url = default_storage.url(file_path)
# Ensure the URL doesn't have /media/ prefix
if file_url.startswith('/media/'):
file_url = file_url[7:] # Remove "/media" part
return file_url
@database_sync_to_async
def update_read_status(self, message, recipient):
# Update the read status to True for the recipient
try:
group_message_read = GroupMessageRead.objects.get(group_message=message, recipient=recipient)
group_message_read.read = True
group_message_read.save()
except GroupMessageRead.DoesNotExist:
pass # If the record doesn't exist, no action is needed
async def delete_message(self, message_id):
try:
# Check if it's a group chat or direct message
if self.is_group_chat:
message = await self.get_group_message(message_id)
if message:
# Send the deletion notification to all group members
await self.channel_layer.group_send(
self.chat_group,
{
'type': 'message_deleted',
'message_id': message_id,
}
)
else:
print(f"Group message with ID {message_id} not found or does not belong to user.")
else:
message = await self.get_message(message_id)
if message:
# Send the deletion notification to both sender and recipient
await self.channel_layer.group_send(
self.conversation_group,
{
'type': 'message_deleted',
'message_id': message_id,
}
)
else:
print(f"Direct message with ID {message_id} not found or does not belong to user.")
except Exception as e:
print(f"Error deleting message: {e}")
# The deletion event handler
async def message_deleted(self, event):
# When a message deletion is broadcast, delete it on the WebSocket side
await self.send(text_data=json.dumps({
'type': 'message_deleted',
'message_id': event['message_id'], # Pass the message ID to frontend
}))
@database_sync_to_async
def get_message(self, message_id):
try:
# Fetch the direct message and ensure it's the correct user (sender)
message = DirectMessage.objects.get(id=message_id, sender=self.scope['user'])
message.delete() # Perform the delete
return message
except DirectMessage.DoesNotExist:
return None
@database_sync_to_async
def get_group_message(self, message_id):
try:
# Fetch the group message and ensure it belongs to the current user (sender)
message = GroupMessage.objects.get(id=message_id, sender=self.scope['user'])
message.delete() # Perform the delete operation
return message
except GroupMessage.DoesNotExist:
print(f"Group message with ID {message_id} not found for user {self.scope['user']}")
return None
@database_sync_to_async
def get_user_full_name(self, user):
return f"{user.first_name} {user.last_name}"
# async def receive(self, text_data):
# pass
async def send_notification(self, event):
notification = event["notification"]
print("Fragrances")
await self.send(text_data=json.dumps({
"message": notification,
}))
class GlobalNotificationConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.user = self.scope['user']
self.group_name = f"chat_notifications_{self.user.id}"
await self.channel_layer.group_add(self.group_name, self.channel_name)
await self.accept()
async def disconnect(self, close_code):
await self.channel_layer.group_discard(self.group_name, self.channel_name)
async def receive(self, text_data):
# Handle the incoming data
text_data_json = json.loads(text_data)
notification_type = text_data_json.get('type')
if notification_type == 'global_notification':
sender = text_data_json.get('sender')
message = text_data_json.get('message')
# Send the notification to the intended receiver only
receiver_username = text_data_json.get('receiver')
# Sanitize receiver's username as well
sanitized_receiver = sanitize_group_name(receiver_username)
# Log the sanitized receiver's group name for debugging
print(f"Sending notification to group: global_notifications_{sanitized_receiver}") # Debugging output
# Send the notification to the receiver's WebSocket group
await self.channel_layer.group_send(
f"global_notifications_{sanitized_receiver}",
{
'type': 'send_notification',
'sender': sender,
'message': message
}
)
# This method handles the 'notification' type message
async def notification(self, event):
# Extract notification data
notification = event['chat_message']
# Send the notification to the WebSocket
await self.send(text_data=json.dumps({
'notification': notification
}))
My base file...
var wsScheme = window.location.protocol == "https:" ? "wss" : "ws";
var wsPath = wsScheme + "://" + window.location.host + "/ws/notifications/";
var socket = new WebSocket(wsPath);
socket.onopen = function() {
console.log('WebSocket connection established.');
};
socket.onmessage = function(event) {
console.log('Message received from WebSocket: ', event.data); // Log the message
var data = JSON.parse(event.data);
// If there's a message, display it
if (data.message && data.sender) {
alert("New notification from " + data.sender + ": " + data.message); // Display an alert
} else {
console.log('No message data in received WebSocket message.');
}
};
socket.onclose = function() {
console.log('WebSocket connection closed.');
};
My URLs.py
websocket_urlpatterns = [
re_path(r'LevelSet/Chat/ws/group/(?P<group_name>[^/]+)/$', consumers.ChatConsumer.as_asgi()), # Match group names with spaces
re_path(r'LevelSet/Chat/ws/direct/(?P<recipient_username>[^/]+)/$', consumers.ChatConsumer.as_asgi()), # Direct message route
re_path(r'LevelSet/Chat/ws/notifications/$', consumers.GlobalNotificationConsumer.as_asgi()),
]
My ASGI file...
application = ProtocolTypeRouter(
{
'http': get_asgi_application(),
'websocket': AllowedHostsOriginValidator(
AuthMiddlewareStack(URLRouter(Chat.routing.websocket_urlpatterns))
)
}
)
And in my view...as an example...I try to execute this...
def send_test_notification(user):
channel_layer = get_channel_layer()
# Create a test notification
notification = "This is a test message sent to the WebSocket."
print("driver")
# Send message to the user's WebSocket group
async_to_sync(channel_layer.group_send)(
f"chat_notifications_{user.id}",
{
'type': 'chat_message',
'notification': notification
}
)
return HttpResponse("Test notification sent to WebSocket.") I see the driver print in my console....but the chat_message notification is not popping up or showing on my page...Thanks in advance for any thoughts...