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,
Any ways to fix these issues? relevant github discussion is here.
You can do this by adding 2 handlers similar to fire-event
.
Then using Django Signals, send a websocket message to those handlers whenever a user becomes a box member or leaves the box