Search code examples
djangodjango-channels

django-channels add user to multiple groups on connect


So I have my models like this.

class Box(models.Model):
    objects = models.Manager()
    name = models.CharField(max_length=100)
    owner = models.ForeignKey('users.User', on_delete=models.CASCADE)

    REQUIRED_FIELDS = [name, owner, icon]

class User(AbstractBaseUser):

    objects = BaseUserManager()
    email = models.EmailField(unique=True)
    username = models.CharField(max_length=32, validators=[MinLengthValidator(2)], unique=True)
    avatar = models.ImageField(upload_to='avatars/', default='avatars/default.jpg')

    REQUIRED_FIELDS = [username, email]

class Member(models.Model):

    objects = models.Manager()
    box = models.ForeignKey('uploads.Box', on_delete=models.CASCADE, editable=False)
    user = models.ForeignKey('users.User', on_delete=models.CASCADE, editable=False)
    roles = models.ManyToManyField('roles.Role', through='roles.MemberRole')
    invite = models.ForeignKey('users.Invite', null=True, blank=True, on_delete=models.CASCADE)

    REQUIRED_FIELDS = [box, user]

I have a websockets framework with routing like this.

websocket_urlpatterns = [
    path('gateway/', GatewayEventsConsumer.as_asgi()),
]
class GatewayEventsConsumer(AsyncWebsocketConsumer):
    """
    An ASGI consumer for gateway event sending. Any authenticated
    user can connect to this consumer. Users receive personalized events
    based on the permissions they have.
    """
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    async def connect(self):
        user = self.scope['user']
        if user.is_anonymous:
            # close the connection if the
            # user isn't authenticated yet
            await self.close()

        for member in user.member_set.all():
            # put user into groups according to the boxes
            # they are a part of. Additional groups would be
            # added mid-way if the user joins or creates
            # a box during the lifetime of this connection
            await self.channel_layer.group_add(member.box.id, self.channel_name)
        await self.channel_layer.group_add(self.scope['user'].id, self.channel_name)
        await self.accept()

    async def disconnect(self, close_code):
        for member in self.scope['user'].member_set.all():
            # remove user from groups according to the boxes they
            # are a part of. Additional groups would be
            # removed mid-way if the user leaves or gets kicked
            # out of a box during the lifetime of this connection
            await self.channel_layer.group_discard(member.box.id, self.channel_name)
        await self.channel_layer.group_discard(self.scope['user'].id, self.channel_name)

    async def fire_event(self, event: dict):
        formatted = {
            'data': event['data'],
            'event': event['event'],
        }
        box = event.get('box', None)
        channel = event.get('overwrite_channel', None)
        listener_perms = event.get('listener_permissions', [])
        if not listener_perms or not box:
            # box would be none if the event was user-specific
            # don't need to check permissions. Fan-out event
            # directly before checking member-permissions
            return await self.send(text_data=json.dumps(formatted))
        member = self.scope['user'].member_set.get(box=box)
        if listener_perms in member.get_permissions(channel):
            # check for permissions directly without any extra context
            # validation. Because data-binding is outbound permission
            # checking is not complex, unlike rest-framework checking
            await self.send(text_data=json.dumps(formatted))

this is how I send ws messages. (using django signals)

@receiver(post_delete, sender=Upload)
def on_upload_delete(instance=None, **kwargs) -> None:
    async_to_sync(channel_layer.group_send)(
        instance.box.id,
        {
            'type': 'fire_event',
            'event': 'UPLOAD_DELETE',
            'listener_permissions': ['READ_UPLOADS'],
            'overwrite_channel': instance.channel,
            'box': instance.box,
            'data': PartialUploadSerializer(instance).data
        }
    )

The api needs to send box-specific events, so I have different groups for boxes. Users which connect to these groups will receive the events they need.

So, when the user connects to the "gateway", I add the user to all the boxes they are a part of, (plus a private group to send user-specific information)

On disconnect, I remove them from the same. However, I am facing issues here. An example,

  • when an user joins a box during the scope of the connection, they would not receive the events that are being sent for that particular box.
  • when an user leaves a box during the scope of the connection, they would still receive events from that particular box.

Any ways to fix these issues? relevant github discussion is here.


Solution

  • You can do this by adding 2 handlers similar to fire-event.

    1. The first one adds a user to a group
    2. The second one deletes a user from a group.

    Then using Django Signals, send a websocket message to those handlers whenever a user becomes a box member or leaves the box