18

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.

lukeaus
  • 11,465
  • 7
  • 50
  • 60

6 Answers6

23

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
        }))
user42488
  • 1,140
  • 14
  • 26
19

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.

Brown Bear
  • 19,655
  • 10
  • 58
  • 76
lukeaus
  • 11,465
  • 7
  • 50
  • 60
  • 2
    Is this the intended way of doing it, or just a workaround. – Shubham Feb 09 '17 at 17:19
  • 1
    I can't see any way of doing it outside of a consumer without a Group. You have to be within the consumer to get message.reply_chanel. Outside of the consumer you have to create a Group to access the user. Checkout the relevant docs here: https://channels.readthedocs.io/en/stable/concepts.html#channel-types. Also checkout both the Multichat and Livebloe examples provided here: https://github.com/andrewgodwin/channels-examples – lukeaus Feb 09 '17 at 20:16
  • was having the same question. creating a group for that is not really its intention, I guess we dont have another option – storm_buster Mar 27 '17 at 01:29
  • 3
    My biggest worry before implementing this solution is: do Groups delete themselves when they are empty? Or will each user create a group that will keep on existing even if they never come back? – Ryan Pergent Apr 15 '17 at 15:16
  • What if a malicious user adds using a console to a group of user and you are sending private data? – Kairat Kempirbaev May 18 '18 at 08:13
  • @KairatKempirbaev I don't understand your question. What console? Client side or server side? – lukeaus May 19 '18 at 09:06
  • Client side. You are not using authentication to add a user to a group, but you are readily sending data to that channel. – Kairat Kempirbaev May 20 '18 at 17:43
  • had this problem while trying to send message to a specific user outside the consumer.. not sure how hard would be to implement it but it would be awesome to "load" the consumer instance by using something like the "get_channel_layer" but for the consumer instance, using the channel_name.. it would be just a matter of saving the channel name in a db and load it outside the consumer from the db.. this might be hard.. otherwise, devs would have implemented it! – 37dev Aug 28 '19 at 23:37
12

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.

Karl Zillner
  • 581
  • 7
  • 16
  • This answer is very bad. What happens if you use two tabs? Hash will be different for each tab. – Sharpek Sep 20 '22 at 05:12
  • The answer isn't bad because of this. You just need to create a table to handle all connections. With two columns `connectionId`, `user` maybe a third column `channel` to save the page or the functionality the user is connected....you can make magic. – Karl Zillner Sep 23 '22 at 19:50
  • But you can also create a Group, like the other answer. It depends on your system. Sometimes I block the WebSocket of working in multiple tabs. Whatsapp Web blocks multiple tabs. Do you want, for example, to have two devices playing the same game in the same account? – Karl Zillner Sep 23 '22 at 19:55
5

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>

Raja Simon
  • 10,126
  • 5
  • 43
  • 74
2

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]
devsnd
  • 7,382
  • 3
  • 42
  • 50
0

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

Pankaj Sharma
  • 2,185
  • 2
  • 24
  • 50