7

Is it possible to access the list of channels added in a Group with django-channels?

def consumer(message):
    Group("group_name").add(message.reply_channel)
    channel_list = Group("group_name").???

EDIT:

What I try to achieve is to have access to sessions of all channel, like the list of connected users for instance.

Thus far I use a database based system that lists the connections. But if the server shuts down without executing my ws_disconnect consumers, these connections object will remain in database, and I don't want this.

EDIT 2:

To list the connected users, I found django-channels-presence. I'll test it.

vmonteco
  • 14,136
  • 15
  • 55
  • 86

5 Answers5

7

Yeah that's possible. And the easy hack is...

# Get channel_layer function
from channels.asgi import get_channel_layer

# passing group_channel takes channel name
channel_layer = get_channel_layer()
ch_group_list = channel_layer.group_channels('<your group name>')
Raja Simon
  • 10,126
  • 5
  • 43
  • 74
  • Nice! Is it possible to access some informations like the associated session/user too? :) – vmonteco Sep 12 '16 at 09:51
  • channels ships with `@channel_session_user_from_http` decorator and in consumers function you can get the user from `message.user` is that what you want – Raja Simon Sep 12 '16 at 11:10
  • 1
    I know that but doesn't it only applies on the current `message` consumer's parameter? what I got with the `get_channel_layer(),group_channels()` function was a dictionnary associating strings to floats : (`{'websocket.send!dLCNWvEM': 1473714104.796983}`). How can I access the defails of each channel? For instance to list the connected users? – vmonteco Sep 12 '16 at 21:26
  • @ShreeRangaRaju Mm let me check and get back to you on this... – Raja Simon Jun 22 '21 at 08:44
3

i found that channel_layer.group_channels('<your group name>'), mentioned above, did not work on channels 2. So , i decide to save information that i need in channel_layer, and it work.

version info

  • channels==2.1.5
  • aioredis==1.2.0
  • channels-redis==2.3.2

In my case, i need know the channels number in a group, because i start a celery beat in the background for keep pushing data to channels, but when the last channel in the group disconnect, i want to stop the celery beat.

I count the channels in a group by store it in channel_layer.

on connect

count = getattr(self.channel_layer, self.group_name, 0)
if not count:
    setattr(self.channel_layer, self.group_name, 1)
else:
    setattr(self.channel_layer, self.group_name, count + 1)

on disconnect

count = getattr(self.channel_layer, self.group_name, 0)
setattr(self.channel_layer, self.group_name, count - 1)
if count == 1:
    delattr(self.channel_layer, self.group_name)

    # stop my celery beat

dhq
  • 31
  • 2
  • 2
    This is dangerous. Async consumers may lead to race condition and there is no disconnect event/method you can rely on. – Yannic Hamann Jan 14 '20 at 14:06
3

You can optionally persist users connected in a room in a database. Create a room, add users on connect, and remove users on disconnect.

from django.db import models
from django.contrib.auth import get_user_model
from asgiref.sync import sync_to_async


# ./models
class Room(models.Model):
    room_name = models.CharField(max_length=150, unique=True)
    users = models.ManyToManyField(get_user_model(), related_name='rooms')

    @classmethod
    @sync_to_async
    def add(cls, room_name, user):
        room, created = cls.objects.get_or_create(room_name=room_name)
        room.users.add(user)
        return created # sockets => join or create



    @classmethod
    @sync_to_async
    def users_count(cls, room_name):
        rooms = cls.objects.filter(room_name=room_name)
        if rooms.exists():
            return rooms.first().users.count()
        return 0

    @classmethod
    @sync_to_async
    def remove_user(cls, user, room_name):
        room = cls.objects.filter(room_name=room_name)
        if room.exists():
            room.users.remove(user)


# ./consumer.py
class YourConsumer(AsyncWebsocketConsumer):

    async def connect(self):
        self.room_name = self.scope['url_route']['kwargs']['room_name']
        self.room_group_name = 'video_%s' % self.room_name
        self.group_users = {self.scope.get('user').id: 1}

        if self.scope.get('user').is_authenticated:
            room = await Room.add(self.room_name, self.scope.get('user'))

        await self.channel_layer.group_add(
            self.room_name,
            self.channel_name
        )

        await self.accept()

    async def disconnect(self, code):
        # Leave room group
        await self.channel_layer.group_discard(
            self.room_group_name,
            self.channel_name
        )
        if self.scope.get('user').is_authenticated:
            await Room.remove_user(self.room_name, self.scope.get('user'))

    async def websocket_receive(self, message):
        count = await Room.users_count(room_name=self.room_name)
        await self.send(json.dumps({
            'type': 'websocket.send',
            'text': 'pong',
            'group': self.room_group_name,
            'room': self.room_name,
            'connections': count
        }))

JeremyM4n
  • 750
  • 5
  • 10
p8ul
  • 2,212
  • 19
  • 19
0

I've tested django-channels-presence and I easily succeeded to list the connected users for a Group by creating a Room (that handle Group management/creation, channel adding/removing...) and that provides a get_users() method that permits me to achieve what I was searching for.

It also provides a way to clean added channels that aren't deleted if the server crashes (that wouldn't trigger the ws_disconnect consumer that is used to delete these channels from the group). It provides the prune_presence and prune_room tasks that cleans expired channels.

vmonteco
  • 14,136
  • 15
  • 55
  • 86
0

i create object in AuthMiddlewareStack


class TokenAuthMiddleware:
    handler: MessengerHandler = None

    def __init__(self, inner):
        self.inner = inner

    async def generate_object(self):
        if self.handler is None: # just for first time
            self.handler= MessengerHandler()
            await self.handler.import_key()
        return self.handler

    async def __call__(self, scope, send, rec):
        model = await self.generate_object()
        scope["handler"] = model
        return await self.inner(scope, send, rec)

in AsyncWebsocketConsumer class

@property
    def _get_handler(self) -> MessengerHandler:
        return self.scope["handler"]

connect

 self._get_handler.add_new_channel(self.channel_name, self.user_api.user_id)

disconnect

    async def disconnect(self, close_code):
        if self.user_api.is_active:
            self._get_handler.remove_channel(self.channel_name, self.user_api.user_id)
        pass

MessengerHandler

class MessengerHandler:

    channels: dict[int, ClientChannels] = {}


    def add_new_channel(self, channel: str, user_id: int):
        ....
    def remove_channel(self, channel: str, user_id: int):
       ....

    def get_user_channels(self, user_id: int, without_channel: str = None) -> list:
...


otherwise safe thread using Django-Redis package

handle channels

    @staticmethod
    def add_new_channel(channel: str, user_id: int, client_type: str, client_id: int):
        channel_name = f"u_{user_id}_{client_type}_{client_id}"
        cache.set(channel_name, channel, timeout=None)

    @staticmethod
    def remove_channel(channel_type: str, user_id: int, client_id: int):
        channel_name = f"u_{user_id}_{channel_type}_{client_id}"
        cache.delete(channel_name)

    @staticmethod
    def get_user_channels(user_id: int) -> list:
        channel_name = f"u_{user_id}_*"
        values = cache.get_many(cache.keys(channel_name))
        return [v for k, v in values.items()]
Mohsen Haydari
  • 550
  • 5
  • 20