I am trying to implement messaging and notification functionality like in FB through Django channels. I have gone through channels tutorial and also the multichat example by Andrewgodwin, I might be missing something but here is my scenario:
I have a user (i.e admin or any user) who can view all of the one-to-one chat rooms he has with other users. and choose and chat with any of them in a single-page app.
Messages are sent and broadcasted using a “chat” consumer, that is also responsible for saving the message to the database, and broadcasting via groups. “chat” consumer instance is only created when the user clicks/selects one of the chat rooms (that is when the WebSocket connection is opened).
I have chosen to create another consumer for notifications. (to be able to send notifications separately with no need to send the messages too, or sending the notifications only to recipients rather than broadcasting to a group). The notification connection is open once the page is loaded to receive any notifications, and no groups are used.
What I am facing trouble with is that I am trying to edit the status of my messages to be “read” once it’s about to be broadcasted, but only at the recipient side, i.e if the recipient has their chat WebSocket connected. for some reason this is not working, even though my console.log show my Websocket is not connected at the recipient side, self.scope[‘user’] shows the recipient user, (which if I understand correctly it shouldn’t be the case), thus my conditioning of changing the message status only if the connected client is not the author, is not working.
I would appreciate any advice or thoughts on this.
I’m not clear on a couple things you’ve written here.
Are you opening multiple websocket connections from an individual browser? Or are you routing the frames when they’re received to an appropriate handler?
Can you clarify what you mean by:
I’m not understanding what you’re trying to say here. (Maybe you could try to write this out as a linear set of things that are supposed to happen when a message is sent?)
I am yes, from a single page (I have two websocket, one for the chat messages, and one for the notification)
can you explain what you mean about routing frames to appropriate handlers? if you mean having each of the WebSockets URL points to a separate consumer, yes this is the case in my code.
so a clearer explanation:
What I am facing trouble with is that I am trying to edit the status of my messages to be “read” once it’s about to be broadcasted, but only at the recipient side, i.e if the recipient has their chat WebSocket connected.
User1 sends to the the admin/recipient a message,
the message is “received” by the consumer, saved in the database, and then group_sent to clients’ WebSockets.
What I wanted to do was, since by default all of my messages are unread, I wanted before self.send() is sent to the client, if the recipient is connected “i.e. the recipient is self.scope[‘user’] and it’s not the same as the message author” I wanted to mark the message read in the database.
however, now, the messages are marked read always, even if the recipient client is not connected to the messages websocket, (i.e. he hasn’t read the message yet), the self.scope[‘user’] prints the recipient.
I thought the consumer at each client-side is only called once the WS is connected from that client.
We are currently using a different approach. We have a single websocket url that all connections are made through. We then have a single listener that handles all events from the browser, and dispatches messages to the right handler based upon a code in the message frame sent.
For example, all JS modules would send a message frame looking something like this: { ‘module’: ‘reader’, ‘status’: ‘OK’, ‘data’: ‘Some data here’}. The listener checks the module and status keys, and then passes the data to the appropriate module - in this case, ‘reader’. This allows us to have multiple “logical channels” (our internal name for this) between JS modules and the Django backend, while only maintaining one websocket connection. (Our routing for this is dynamic - modules can be started or stopped, and the router checks for the current status of a module before dispatching the message to it. A message can be rejected if the module isn’t running.)
But, leaving that aside for the moment, I think we might need to see your code where you’re checking and updating the “read” flag for these messages. Without having seen anything, I’m inclined to believe that a comparison isn’t working the way you might be expecting it to.
In particular, and reading between the lines, self.scope[“user”] is the User object for the person currently connected to that socket. If you’re storing a username as the “message author”, you’ll want to make sure you’re comparing username to username, not username to User object id.
can you clarify what you by listener, handler here? do you mean the listener "recieve function in the consumer? and handler is the broadcasting function from the group? or do you mean the listener and handler in JavaScript context? also what do you mean by module here?
I didn’t post my code cz I was not sure of the forum’s protocol in terms of posting code, but here is my consumers below:
class ChatConsumer(WebsocketConsumer):
def connect(self):
if self.scope["user"].is_anonymous:
self.close()
else:
self.room_group_name = self.scope['url_route']['kwargs']['room_name']
print(self.channel_name)
# Join room group
async_to_sync(self.channel_layer.group_add)(
self.room_group_name,
self.channel_name
)
self.accept()
def disconnect(self, close_code):
# Leave room group
async_to_sync(self.channel_layer.group_discard)(
self.room_group_name,
self.channel_name
)
#receiving messages from WebSocket, message
def receive(self, text_data):
#convert from str containing json to python object
text_data_obj = json.loads(text_data)
message = text_data_obj['message']
# creating customer message
if not self.scope['user'].is_staff:
user_var = self.scope['user']
msg_obj = Message.objects.create(content=message, author=user_var, room=user_var.user_room)
# creating staff message
elif self.scope['user'].is_staff:
user_var = self.scope['user']
room_var = self.scope['url_route']['kwargs']['room_name']
msg_obj = Message.objects.create(content=message, author=user_var, room=Room.objects.get(name=room_var))
#distrubte the message back to other consumers
async_to_sync(self.channel_layer.group_send)(
self.room_group_name,
{
'type': 'chat_message',
'message': {'message': message,'author': msg_obj.author.username, 'timestamp': f'{msg_obj.created_at.strftime("%B %d, %Y, %I:%M %p")}', 'id': msg_obj.id},
}
)
# receive message from room group,
def chat_message(self, event):
message = event["message"]
# if we are at the targeted recipient client.
# mark the new message read.
if message["author"] != self.scope['user'].username:
print("are we marking everything as read?")
upd_msg = Message.objects.get(pk=message["id"])
upd_msg.status = 'r'
upd_msg.save()
# Send message to WebSocket (authors users)
self.send(text_data=json.dumps({'message': message}))
#
#
class NotificationConsumer(WebsocketConsumer):
def connect(self):
# Are they logged in?
if self.scope["user"].is_anonymous:
self.close()
else:
self.room_group_name = 'notifications'
print(self.channel_name)
# Join room group
async_to_sync(self.channel_layer.group_add)(
self.room_group_name,
self.channel_name
)
self.accept()
def disconnect(self, close_code):
async_to_sync(self.channel_layer.group_discard)(
self.room_group_name,
self.channel_name
)
#receiving messages from WebSocket, (room name)
def receive(self, text_data):
text_data_obj = json.loads(text_data)
# we have to send this notification from sender to our targeted other party.
async_to_sync(self.channel_layer.group_send)(
self.room_group_name,
{
'type': 'notification_message',
'notification': {'snt_msg_room': text_data_obj['snt_msg_room']},
}
)
# receive message from room group
def notification_message(self, event):
notification = event["notification"]
#get the last message notification
message = Message.objects.filter(room__name=notification['snt_msg_room']).order_by('-created_at')[:1]
if message:
# if we are not at the sender client.
if message[0].author.id != self.scope['user'].id:
# if it was the admin who sent the message, notify the room customer.
if self.scope['user'].id == message[0].room.user_agent.id:
self.send(text_data=json.dumps({'notification': True, 'room_targeted': notification['snt_msg_room']}))
# if it was the customer of the room who sent the message, notify the admin
elif self.scope['user'].is_staff:
self.send(text_data=json.dumps({'notification': True, 'room_targeted': notification['snt_msg_room']}))
No problem - I know many times when I’m referring to what we do, I have a tendency to use the “internal” names we apply to things as opposed to what they probably should be called - causing confusion when I’m trying to relate this to what other people are doing. (A lot of this is caused by what we called different things in a previous implementation - we changed the technology and implementation, but didn’t change our use of terminology - shame on us.)
So yes, what we call the “listener” is a JsonWebsocketConsumer. The “handler” is the receive method that receives the message frame sent by the browser. When a message is received from the browser, it examines the message, getting the module name. In this specific case, the “module” is actually an external process. We use the channel_layer as a simple “task queue” to pass messages back and forth between the consumer and these external modules, and those external modules are all individual instances of worker processes being run. (See Worker and Background tasks for more details here.)
As far as my understanding goes, you’re free to post whatever code you feel comfortable posting, provided it does not violate any IP-related laws and complies with the Django Code of Conduct.
So my first attempt at this would be to verify exactly what message[‘author’] and self.scope[‘user’].username are at this point. They’re not equal, so I’d want to see what Django thinks they are before this line.
At all, I really appreciate the work you all do and the help you are offering! it’s immensely appreciated
I will definitely read more about workers to understand fully the method you explained, it’s definitely cleaner and serves better the function I am seeking to implement.
I have printed out the values of message[“author”], self.scope[‘user’].username, and NOW, they are exactly the username I am expecting at each client-side, for some reason I can’t reproduce the problem, which since I haven’t changed my code it means something is odd…I’ll compare my git versions of the file and reply if I could detect anything.