Sending a message to a single user using django-channels - django

I have been trying out django-channels including reading the docs and playing around with the examples.
I want to be able to send a message to a single user that is triggered by saving a new instance to a database.
My use case is creating a new notification (via a celery task) and once the notification has saved, sending this notification to a single user.
This sounds like it is possible (from the django-channels docs)
...the crucial part is that you can run code (and so send on
channels) in response to any event - and that includes ones you
create. You can trigger on model saves, on other incoming messages, or
from code paths inside views and forms.
However reading the docs further and playing around with the django-channels examples, I can't see how I can do this. The databinding and liveblog examples demonstrate sending to a group, but I can't see how to just send to a single user.

Little update since Groups work differently with channels 2 than they did with channels 1. There is no Group class anymore, as mentioned here.
The new groups API is documented here. See also here.
What works for me is:
# Required for channel communication
from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync
def send_channel_message(group_name, message):
channel_layer = get_channel_layer()
async_to_sync(channel_layer.group_send)(
'{}'.format(group_name),
{
'type': 'channel_message',
'message': message
}
)
Do not forget to define a method to handle the message type in the Consumer!
# Receive message from the group
def channel_message(self, event):
message = event['message']
# Send message to WebSocket
self.send(text_data=json.dumps({
'message': message
}))

Expanding on #Flip's answer of creating a group for that particular user.
In your python function in your ws_connect function you can add that user into a a group just for them:
consumers.py
from channels.auth import channel_session_user_from_http
from channels import Group
#channel_session_user_from_http
def ws_connect(message):
if user.is_authenticated:
Group("user-{}".format(user.id)).add(message.reply_channel)
To send that user a message from your python code:
my view.py
import json
from channels import Group
def foo(user):
if user.is_authenticated:
Group("user-{}".format(user.id)).send({
"text": json.dumps({
"foo": 'bar'
})
})
If they are connected they will receive the message. If the user is not connected to a websocket it will fail silently.
You will need to also ensure that you only connect one user to each user's Group, otherwise multiple users could receive a message that you intended for only a specific user.
Have a look at django channels examples, particularly multichat for how to implement routing, creating the websocket connection on the client side and setting up django_channels.
Make sure you also have a look at the django channels docs.

In Channels 2, you can save self.channel_name in a db on connect method that is a specific hash for each user. Documentation here
from asgiref.sync import async_to_sync
from channels.generic.websocket import AsyncJsonWebsocketConsumer
import json
class Consumer(AsyncJsonWebsocketConsumer):
async def connect(self):
self.room_group_name = 'room'
if self.scope["user"].is_anonymous:
# Reject the connection
await self.close()
else:
# Accept the connection
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)
await self.accept()
print( self.channel_name )
Last line returns something like specific.WxuYsxLK!owndoeYTkLBw
This specific hash you can save in user's table.

The best approach is to create the Group for that particular user. When ws_connect you can add that user into Group("%s" % <user>).add(message.reply_channel)
Note: My websocket url is ws://127.0.0.1:8000/<user>

Just to extend #luke_aus's answer, if you are working with ResourceBindings, you can also make it so, that only users "owning" an object retrieve updates for these:
Just like #luke_aus answer we register the user to it's own group where we can publish actions (update, create) etc that should only be visible to that user:
from channels.auth import channel_session_user_from_http,
from channels import Group
#channel_session_user_from_http
def ws_connect(message):
Group("user-%s" % message.user).add(message.reply_channel)
Now we can change the corresponding binding so that it only publishes changes if the bound object belongs to that user, assuming a model like this:
class SomeUserOwnedObject(models.Model):
owner = models.ForeignKey(User)
Now we can bind this model to our user group and all actions (update, create, etc) will only be published to this one user:
class SomeUserOwnedObjectBinding(ResourceBinding):
# your binding might look like this:
model = SomeUserOwnedObject
stream = 'someuserownedobject'
serializer_class = SomeUserOwnedObjectSerializer
queryset = SomeUserOwnedObject.objects.all()
# here's the magic to only publish to this user's group
#classmethod
def group_names(cls, instance, action):
# note that this will also override all other model bindings
# like `someuserownedobject-update` `someuserownedobject-create` etc
return ['user-%s' % instance.owner.pk]

Although it's late but I have a direct solution for channels 2 i.e using send instead of group_send
send(self, channel, message)
| Send a message onto a (general or specific) channel.
use it as -
await self.channel_layer.send(
self.channel_name,
{
'type':'bad_request',
'user':user.username,
'message':'Insufficient Amount to Play',
'status':'400'
}
)
handel it -
await self.send(text_data=json.dumps({
'type':event['type'],
'message': event['message'],
'user': event['user'],
'status': event['status']
}))
Thanks

Related

Is it necessary to make the channel room name unique in django channels? I have a function which works fine but have some concerns

Hey guys I have made a small feed system which uses websockets and opens at the moment someone visits the website and login, all the new feeds will be send live and is updated realtime to those people who are subscribed to a particular user. I am using django-channels for this and have a non-unique room_name as of now, so that it is accessible to every user who is logged in and visits the website. but is it a good practice to do have a non unique room_name for such a system? Does it affect the performance or becomes obsolete if large number of users visits the website at the same time?
OR
Should I create a new table with the current user and a manyTomanyField which contains the subscribed users? if that's the case how do I add all the users to a channel group?
This is the code I have which is working fine now,
async def connect(self):
print('connected')
user = self.scope["user"]
self.user = user
room_name = f'sub-feeds'
self.room_name = room_name
await self.accept()
for now, I am checking a condition which returns True if the user is subscribed to me else it returns False.
async def external_feed(self, event): # sending from outside the consumer
user = self.user
oth_user = event['user']
condition = await self.get_subs_status(user, oth_user)
if condition == True:
await self.send_json({
'newFeed': event['feeds'],
})
I am actually concerned whether this breaks if the number of users increases a lot and using a separate room with subscribed users (new db table) will resolve the issue.
Please ask if you need more info. Help is much appreciated.
Thanks
If I was implementing this, I would use the username(which is assumed to be unique) as the room name. The implementation would be something like this,
from collections import defaultdict
class LiveFeedConsumer(WebsocketConsumer):
# map which uses the subscribed user's username as a key and stores the set of all users' username's who subscribed to their feeds as value.
# Ex - users 1, 2, 3, 4 are available
# __subscriptions[1] = {2, 3} -> Means 2 and 3 have subscribed to 1's feeds
__subscriptions = defaultdict(set)
def connect(self):
# authenticate
user_to_which_to_subscribe_to = get_user_somehow()
self.scope["session"]["room_name"] = user_to_which_to_subscribe_to.username
# Accept connection
self.__subscriptions[user_to_which_to_subscribe_to.username].add(self.scope["user"].username) # To extract users and their subscriptions
def disconnect(self, message):
# Disconnection logic
self.__subscriptions[ self.scope["session"]["room_name"] ].remove(self.scope["user"].username)
def get_subs_status(self, user, other_user):
return user.username in self.__subscriptions[other_user.username]
def external_feed(self, event): # sending from outside the consumer
user = self.user
oth_user = event['user']
condition = self.get_subs_status(user, oth_user)
if condition is True:
self.send_json({
'newFeed': event['feeds'],
})
# publish to pub/sub mechanism to support multiple processes/nodes
You can add the async/await parts of the code. You get the logic.
This way you'll have separate rooms for each user's live feed. Any user can subscribe to multiple other users. Furthermore, you can add a pub/sub listener to scale this horizontally, adding multiple nodes/processes.

Adding multiple groups with django channels and websocket

I try to development a notificaiton system. I would like the system administrator to be able to send notifications to different groups. For this, I create only one websocket connection, and when socket is connected(during login) I want to add the user to the groups it belongs to. I wrote the code as below but I'm not sure it's correct. I am already getting this error: "AttributeError: 'WebSocketProtocol' object has no attribute 'handshake_deferred'"
class MyConsumer(AsyncWebsocketConsumer):
async def connect(self):
if self.scope["user"].is_anonymous:
await self.close()
else:
groups = await sync_to_async(list)(self.scope["user"].channel_groups.all())
for group in groups:
await self.channel_layer.group_add(group.name,self.channel_name)
await self.accept()
print("###CONNECTED")
Can you help me? Am i on the right way? If so how do i fix this error?

Remove a specific user from a Django Channels group?

I have a couple of Django Channels groups that I use to send various messages to the client with.
Can I remove a specific user from one of these groups, using only the ID of the user that I want to remove?
Another potential option would be to force disconnect the user's connection using just their ID.
First, you have to save the user's channel_name to their model
and we assume that you had the group_name of channels too
then you can use group_discard for deleting user from group like this:
group_name = 'name_of_channels_group'
user = User.objects.get(id=id)
channel_name = user.channel_name
async_to_sync(self.channel_layer.group_discard)(group_name, channel_name)
https://channels.readthedocs.io/en/stable/topics/channel_layers.html?highlight=group_send#groups
I have thought couple a days about this problem and have an idea how this can be implemented, but can't test it currently. You should try to change your receive() method like this:
async def receive(self, text_data=None, bytes_data=None):
text_data_json = json.loads(text_data)
message = text_data_json['message']
users_to_kick = text_data_json['kick']
# you should inspect scope['user'] cuz I am not sure in what place
# user's id is placed, but there is 'user' object.
if self.scope['user']['id'] in list(map(int, users_to_kick)):
await self.close()
else:
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'some_method',
'message': message
}
)
You have to have Authorisation system enabled, you can't kick Anonymous users. And you have to send from Front-End a list of users which you want to kick.
You need to prevent connect based on scope['user'] in consumer connect method like this:
class MyConsumer(AsyncJsonWebsocketConsumer):
async def connect(self):
id_cannot_connect = 1
if self.scope['user'] == id_cannot_connect:
await self.close()
else:
await self.accept()
If you want to make lists of users allowed to connect to specific groups you'll need to store groups and group users in your database and use it the same way in connect method like above.
edit: You can discard user's channel from the group with group_discard in receive_json where you still have access to self.scope['user'] to filter needed users.
I solved this in the end by storing each channel name (self.channel_name) on channel connection, and removing them on disconnect. These are then tied to a Django user object.
Now, if I want to remove a user from a group, I can just loop over all stored channel names tied to a user object, and run group_discard.

Django Testing - check messages for a view that redirects

I have been writing tests for one of my django applications and have been looking to get around this problem for quite some time now. I have a view that sends messages using django.contrib.messages for different cases. The view looks something like the following.
from django.contrib import messages
from django.shortcuts import redirect
import custom_messages
def some_view(request):
""" This is a sample view for testing purposes.
"""
some_condition = models.SomeModel.objects.get_or_none(
condition=some_condition)
if some_condition:
messages.success(request, custom_message.SUCCESS)
else:
messages.error(request, custom_message.ERROR)
redirect(some_other_view)
Now, while testing this view client.get's response does not contain the context dictionary that contains the messages as this view uses a redirect. For views that render templates we can get access to the messages list using messages = response.context.get('messages'). How can we get access messages for a view that redirects?
Use the follow=True option in the client.get() call, and the client will follow the redirect. You can then test that the message is in the context of the view you redirected to.
def test_some_view(self):
# use follow=True to follow redirect
response = self.client.get('/some-url/', follow=True)
# don't really need to check status code because assertRedirects will check it
self.assertEqual(response.status_code, 200)
self.assertRedirects(response, '/some-other-url/')
# get message from context and check that expected text is there
message = list(response.context.get('messages'))[0]
self.assertEqual(message.tags, "success")
self.assertTrue("success text" in message.message)
You can use get_messages() with response.wsgi_request like this (tested in Django 1.10):
from django.contrib.messages import get_messages
...
def test_view(self):
response = self.client.get('/some-url/') # you don't need follow=True
self.assertRedirects(response, '/some-other-url/')
# each element is an instance of django.contrib.messages.storage.base.Message
all_messages = [msg for msg in get_messages(response.wsgi_request)]
# here's how you test the first message
self.assertEqual(all_messages[0].tags, "success")
self.assertEqual(all_messages[0].message, "you have done well")
If your views are redirecting and you use follow=true in your request to the test client the above doesn't work. I ended up writing a helper function to get the first (and in my case, only) message sent with the response.
#classmethod
def getmessage(cls, response):
"""Helper method to return message from response """
for c in response.context:
message = [m for m in c.get('messages')][0]
if message:
return message
You include this within your test class and use it like this:
message = self.getmessage(response)
Where response is what you get back from a get or post to a Client.
This is a little fragile but hopefully it saves someone else some time.
I had the same problem when using a 3rd party app.
If you want to get the messages from a view that returns an HttpResponseRedict (from which you can't access the context) from within another view, you can use get_messages(request)
from django.contrib.messages import get_messages
storage = get_messages(request)
for message in storage:
do_something_with_the_message(message)
This clears the message storage though, so if you want to access the messages from a template later on, add:
storage.used = False
Alternative method mocking messages (doesn't need to follow redirect):
from mock import ANY, patch
from django.contrib import messages
#patch('myapp.views.messages.add_message')
def test_some_view(self, mock_add_message):
r = self.client.get('/some-url/')
mock_add_message.assert_called_once_with(ANY, messages.ERROR, 'Expected message.') # or assert_called_with, assert_has_calls...

Chat application: Using Django with sockjs-tornado and redis

I am trying to write chat application, here are some desing thoughts:
Django doing main website serving.
Sockjs-tornado serves chat and between them I would like to setup redis. When sockjs-tornado receives message in chat, it processes it and sends to other connected clients and also puts it in redis, so Django can save this message in persistent database. I know I should use pubsub functionallity of redis. I know how to setup it in tornado (brukva library), but how can I subscribe to redis' channel in django? So I can receive message in django and save it to database? Do you have any ideas?
i'm not know how sockjs use, but this example illustrate how to save in django model
#in tornado
import brukva
import tornado.web
import tornado.websocket
c = brukva.Client()
c.connect()
class MessagesHandler(tornado.websoket.WebsocketHandler):
def open(self):
#....
def handle_request(self, response):
pass
def on_message(self, message):
#....
c.publish(self.channel, json.dumps({
"sender": self.sender_name,
"text": message,
}))
http_client = tornado.httpclient.AsyncHTTPClient()
request = tornado.httpclient.HTTPRequest(
'/to/django_project/url/chat_save_base/',
method="POST",
body=urllib.urlencode({
"message": message.encode("utf-8"),
"sender": self.sender.name,
})
http_client.fetch(request, self.handle_request)
#in django_url
url(r'/to/django_project/url/chat_save_base/','app.my_view')
#my_view
from django.views.decorators.csrf import csrf_exempt
from messages.models import Message
#csrf_exempt
def my_view(request):
message_text = request.POST.get("message")
sender = User.objects.get(id=request.POST.get("sender_id"))
message = Message()
message.text = message_text
message.sender_id = sender_id
message.save()
source for additional info:
http://habrahabr.ru/post/160123/