How to limit number of clients in django channels' room - django

I wish to limit the number of users in a room to 2 because I am making a socket game
were two palyers can play a tic-tack-toe or a connect-4 game so I a trying to find some way of limiting only 2 players in one room.
Down below is my comsumers.py
from channels.generic.websocket import WebsocketConsumer
from asgiref.sync import async_to_sync
import json
class GameRoom(WebsocketConsumer):
def connect(self):
self.room_name = self.scope['url_route']['kwargs']['room_code']
self.room_group_name = 'room_%s' % self.room_name
print(self.room_group_name)
async_to_sync(self.channel_layer.group_add)(
self.room_group_name,
self.channel_name
)
self.accept()
I have removed some of the less necessary methods from this question

You have to create a room model in your project and save the connected users to it, then you can add a validation in your connect method, something like this:
def connect(self):
self.room_name = self.scope['url_route']['kwargs']['room_code']
self.room = Room.objects.get(code=self.room_name)
if self.room.connected_user >= 2:
return self.close()
else:
self.room.connected_user = self.room.connected_user + 1
self.room.save(update_fields=['connected_user'])
self.room_group_name = 'room_%s' % self.room_name
print(self.room_group_name)
async_to_sync(self.channel_layer.group_add)(
self.room_group_name,
self.channel_name
)
self.accept()
def disconnect(self):
...
self.room.connected_user = self.room.connected_user - 1
self.room.save(update_fields=['connected_user'])
Extra:
and of course, you can use the F() function, and make your performance better in connect and disconnect (useful when many users trying to connect in the same time)

Related

UUID breaking the ws connect

I've been using a websockets connection with django-channels for a chatroom app with the following routing:
re_path(r'ws/chat/(?P<room_name>\w+)/participant/(?P<user>\w+)/$', consumers.ChatConsumer.as_asgi())
So if the id of my chatroom is 1, to connect to WS I would use the following url with 2 being the id of the participant that wants to enter the room:
ws/chat/1/participant/1/
Now i changed the id of my room model to UUID so now to connect to a room I need to use the following url
ws/chat/84f48468-e966-46e9-a46c-67920026d669/participant/1/
where "84f48468-e966-46e9-a46c-67920026d669" is the id of my room now, but I'm getting the following error:
raise ValueError("No route found for path %r." % path)
ValueError: No route found for path 'ws/chat/84f48468-e966-46e9-a46c-
67920026d669/participant/1/'.
WebSocket DISCONNECT /ws/chat/84f48468-e966-46e9-a46c-67920026d669/participant/1/
[127.0.0.1:50532]
My consumers:
class ChatConsumer(WebsocketConsumer):
def connect(self):
self.room_name = self.scope['url_route']['kwargs']['room_name']
self.user = self.scope['url_route']['kwargs']['user']
self.room_group_name = 'chat_%s' % self.room_name
# Join room group
async_to_sync(self.channel_layer.group_add)(
self.room_group_name,
self.channel_name
)
self.accept()
def disconnect(self, close_code):
delete = delete_participant(self.room_name, self.user)
if delete == 'not_can_delete_user':
pass
else:
# Leave room group
channel_layer = get_channel_layer()
async_to_sync(channel_layer.group_send)(
f'chat_{self.room_name}',
{
'type': 'receive',
'message': "USER_DISCONNECT",
'body': {
'participant': delete,
'idParticipant': self.user
}
}
)
async_to_sync(self.channel_layer.group_discard)(
self.room_group_name,
self.channel_name
)
def receive(self, text_data=None, type='receive', **kwargs):
if isinstance(text_data, dict):
text_data_json = text_data
message = text_data_json['message']
body = text_data_json['body']
else:
text_data_json = json.loads(text_data)
message = text_data_json['message']
body = text_data_json['body']
self.send(text_data=json.dumps({
'message': message,
"body": body
}))
What is wrong?
try this regex:
re_path(r'ws/chat/(?P<room_name>[A-Za-z0-9_-]+)....
format the group id and remove the '-' pass it in the javascript and use groupId2 instead.
groupId2 = groupId.replaceAll("-","");
i think this is better than searching for a regex and the problem is that django uuid field is 32 bytes not 36 bytes as it would be with the '-'.
so reading the full log will show you that a value error was raised.

While testing my Websocket Consumer a model object is not created (Django Channels)

I'm new in Django Channels and I'm trying to build a simple chat app. But when I'm trying to test my async Websocket Consumer I run into the following exception: chat.models.RoomModel.DoesNotExist: RoomModel matching query does not exist.
It seems like the test room is not created.
test.py file is the following:
class ChatTest(TestCase):
#sync_to_async
def set_data(self):
room = RoomModel.objects.create_room('Room1', 'room_password_123')
room_slug = room.slug
user = User.objects.create_user(username='User1', password='user_password_123')
print(RoomModel.objects.all()) # querySet here contains the created room
return room_slug, user
async def test_my_consumer(self):
room_slug, user = await self.set_data()
application = URLRouter([
re_path(r'ws/chat/(?P<room_name>\w+)/$', ChatConsumer.as_asgi()),
])
communicator = WebsocketCommunicator(application, f'/ws/chat/{room_slug}/')
communicator.scope['user'] = user
connected, subprotocol = await communicator.connect()
self.assertTrue(connected)
await communicator.send_json_to({'message': 'hello'})
response = await communicator.receive_json_from()
self.assertEqual('hello', response)
await communicator.disconnect()
My consumer.py file is the following:
class ChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.room_name = self.scope['url_route']['kwargs']['room_name']
self.room_group_name = f'chat_{self.room_name}'
await self.channel_layer.group_add(self.room_group_name, self.channel_name)
await self.accept()
async def disconnect(self, close_code):
await self.channel_layer.group_discard(self.room_group_name, self.channel_name)
async def receive(self, text_data):
self.message = json.loads(text_data).get('message')
data = {'type': 'chat_message'}
data.update(await self.create_message())
await self.channel_layer.group_send(self.room_group_name, data)
async def chat_message(self, event):
await self.send(text_data=json.dumps({
'message': event['message'],
'user': event['user'],
'timestamp': event['timestamp']
}))
#database_sync_to_async
def create_message(self):
print(RoomModel.objects.all()) # qyerySet turns out to be empty here
room = RoomModel.objects.get(slug=self.room_name)
msg = room.messagemodel_set.create(text=self.message, user_name=self.scope['user'])
return {
'message': msg.text,
'user': self.scope['user'].username,
'timestamp': msg.timestamp.strftime("%d/%m/%Y, %H:%M")
}
I would be grateful for any help.
When you try to run room = RoomModel.objects.create_room('Room1', 'room_password_123') you are trying to commit a transaction.
And this cannot happen until the end of the test because TestCase wraps each test within a transaction. It waits until the end of the test to create this object.
And since you are awaiting set_data, the flow goes on to execute the rest of the test, i.e., it reaches the call to create_message where it tries to get the RoomModel object, which will be not present in the db yet since the transaction has not been committed.
I had a similar problem that I solved using this link https://github.com/django/channels/issues/1110. To summarize, you have to change TestCase to TransactionTestCase

How to use the same connection for two differents consummers in Django Channels?

I use the last version of django channels(V3) and i have two consummers.
This is my routing.py
application = ProtocolTypeRouter({
"websocket": AuthMiddlewareStack(
URLRouter([
url(r"^ws/user/(?P<user_id>\d+)/$", consumers.UserConsumer),
url(r"^ws/notification/(?P<room_name>\w+)/$", con.NotificationConsumer),
])
),
})
My first app.consummes.py
class UserConsumer(WebsocketConsumer):
user_number = 0
def connect(self):
self.room_name = self.scope['url_route']['kwargs']['user_id']
self.room_group_name = self.room_name
print("connected", self.room_group_name)
self.user_number+= 1
print("user_number", self.user_number)
# Join room group
async_to_sync(self.channel_layer.group_add)(
self.room_group_name,
self.channel_name
)
self.accept()
def disconnect(self, close_code):
# Leave room group
print("deconnected")
async_to_sync(self.channel_layer.group_discard)(
self.room_group_name,
self.channel_name
)
# Receive message from WebSocket
def receive(self, text_data):
text_data_json = json.loads(text_data)
proposal_identifiant = text_data_json['proposal_identifiant']
sender = text_data_json['sender']
messages = text_data_json['messages']
owner = text_data_json['owner']
conversator = text_data_json['conversator']
last_sender = text_data_json['last_sender']
type_ad = text_data_json['type_ad']
ad_id = text_data_json['ad_id']
price = text_data_json['price']
sending_by = text_data_json['sending_by']
price_is_changed = text_data_json['price_is_changed']
accepted = text_data_json['accepted']
from_send_message = text_data_json['from_send_message']
users_id = []
users_id.append(owner)
users_id.append(conversator)
winner_or_looser = text_data_json['winner_or_looser']
try:
try:
if proposal_identifiant and get_current_proposal(proposal_identifiant):
# we create message if proposal exist
if accepted == False:
update_proposal(proposal_identifiant, last_sender, price, price_is_changed, accepted)
create_new_message(proposal_identifiant, sender, messages)
else:
if from_send_message == True:
update_proposal(proposal_identifiant, last_sender, price, price_is_changed, accepted)
create_new_message(proposal_identifiant, sender, messages)
else:
try:
create_new_delivery(
owner,
conversator,
proposal_identifiant,
type_ad,
ad_id,
price,
accepted,
)
create_new_message(proposal_identifiant, sender, messages)
winner_or_looser = True
except IntegrityError:
print("error")
return self.send(text_data=json.dumps({
'error': "IntergyError"
}))
else:
# we create at first proposal and message
# print("new_proposal")
new_proposal = create_new_proposal(
owner,
conversator,
last_sender,
type_ad,
ad_id,
price
)
# print(new_proposal.id)
proposal_identifiant = new_proposal.id
# print(proposal_identifiant)
create_new_message(proposal_identifiant=new_proposal.id, sender=sender, messages=messages)
for id in users_id:
self.room_group_name = str(id)
# Send message to room group
# print(self.room_group_name)
async_to_sync(self.channel_layer.group_send)(
self.room_group_name,
{
'type': 'chat_message',
'proposal_identifiant': proposal_identifiant,
'sender': sender,
'messages': messages,
'statut': True,
'read_or_not': False,
'owner': owner,
'conversator': conversator,
'last_sender': last_sender,
'type_ad': type_ad,
'ad_id': ad_id,
'price': price,
'sending_by': sending_by,
'price_is_changed': price_is_changed,
'accepted': accepted,
'from_send_message': from_send_message,
'winner_or_looser':winner_or_looser,
}
)
except:
raise
except:
raise
# Receive message from room group
def chat_message(self, event):
proposal_identifiant = event['proposal_identifiant']
sender = event['sender']
messages = event['messages']
owner = event['owner']
conversator = event['conversator']
last_sender = event['last_sender']
type_ad = event['type_ad']
ad_id = event['ad_id']
price = event['price']
sending_by = event['sending_by']
price_is_changed = event['price_is_changed']
accepted = event['accepted']
from_send_message = event['from_send_message']
winner_or_looser = event['winner_or_looser']
# Send message to WebSocket
self.send(text_data=json.dumps({
'proposal_identifiant': proposal_identifiant,
'sender': sender,
'messages': messages,
'owner': owner,
'conversator': conversator,
'last_sender': last_sender,
'type_ad': type_ad,
'ad_id': ad_id,
'price': price,
'sending_by': sending_by,
'price_is_changed': price_is_changed,
'accepted': accepted,
'from_send_message': from_send_message,
'winner_or_looser': winner_or_looser,
}))
And my second app.consummer.py
class NotificationConsumer(WebsocketConsumer):
def connect(self):
self.room_name = self.scope['url_route']['kwargs']['room_name']
self.room_group_name = self.room_name
# Join room group
async_to_sync(self.channel_layer.group_add)(
self.room_group_name,
self.channel_name
)
self.accept()
def disconnect(self, close_code):
# Leave room group
print("deconnected")
async_to_sync(self.channel_layer.group_discard)(
self.room_group_name,
self.channel_name
)
# Receive message from WebSocket
def receive(self, text_data):
text_data_json = json.loads(text_data)
print("notif", text_data_json)
message = text_data_json['message']
from_user = text_data_json['from_user']
to_user = text_data_json['to_user']
users_id = []
users_id.append(from_user)
users_id.append(to_user)
# Send message to room group
for id in users_id:
self.room_group_name = str(id)
async_to_sync(self.channel_layer.group_send)(
self.room_group_name,
{
'type': 'chat_message',
'message': message,
'from_user': from_user,
'to_user': to_user,
}
)
# Receive message from room group
def chat_message(self, event):
message = event['message']
from_user = event['from_user']
to_user = event['to_user']
# Send message to WebSocket
self.send(text_data=json.dumps({
'message': message,
'from_user': from_user,
'to_user': to_user,
}))
How to use the same connection for differents consummers? because two differents connections doesn't works. I got error.
I tried many way but no success
You cannot connect the same socket connection to two different consumers
ie
Django-channels documentation says
Channels routers only work on the scope level, not on the level of individual events, which means you can only have one consumer for any given connection.
Routing is to work out what single consumer to give a connection, not how to spread events
from one connection across multiple consumers.
In your browser console write[javascript] the following code and hit enter
var socket = new WebSocket('ws://localhost:8000/ws/user/(?P<user_id>\d+)/$');
var socket2 = new WebSocket('ws://localhost:8000/ws/notification/(?P<room_name>\w+)/$');
and check if both of them connect to django-server; in my perspective both of them should connect since both the consumers are in different apps(ie my-first-app & my-second-app as you mentioned).
from my understanding what you want to do is connect a single socket which would handle user-related stuff on UserConsumer and notification on another NotificationConsumer.
For this you would have to create multiple event-handlers for(Notification alone in the same .consumers.py file) which would get trigger whenever a notification needs to be sent.
Or else you can use your existing implementation but variable "socket2" will handle notification.

django channels group_send not working suddenly

I have recently converted my existing django project to have a react front end. However , im facing the issue whereby the backend of my django-channels is not working. The issue im having is that the group_send method is not working in my case , as such the consumer does not receive the information generated by a signal in my models. Here is my code :
consumers.py
class NotificationConsumer (AsyncJsonWebsocketConsumer):
async def connect (self):
close_old_connections()
user = self.scope["user"]
if not user.is_anonymous:
await self.accept()
#connects the user to his/her websocket channel
group_name = "notifications_{}".format(str(user))
print(group_name)
await self.channel_layer.group_add(group_name, self.channel_name)
async def disconnect (self, code):
close_old_connections()
user = self.scope["user"]
if not user.is_anonymous:
#Notifications
notifications_group_name = "notifications_{}".format(str(user))
await self.channel_layer.group_discard(notifications_group_name, self.channel_name)
async def user_notification (self, event):
close_old_connections()
print('Notification recieved by consumer')
await self.send_json({
'event': 'notification',
'data': {
'event_type': 'notification',
'notification_pk': event['notification_pk'],
'link': event['link'],
'date_created': event['date_created'],
'object_preview': event['object_preview'],
}
})
print(event)
Models/signals
def ChannelNotification(sender, instance, created, **kwargs):
if created:
channel_layer = get_channel_layer()
print(channel_layer)
group_name = "notifications_{}".format(str(instance.target))
print('inside channel signal')
print(group_name)
async_to_sync(channel_layer.group_send)(
group_name, {
"type": "user_notification",
"notification_pk": instance.pk,
"link": instance.object_url,
"date_created": instance.time.strftime("%Y-%m-%d %H:%M:%S"),
"object_preview": instance.object_preview,
}
)
Whenever an notification object is created in the database , the signal will be sent to my ChannelNotification which receives this information and transmit it to the consumer via group send . It used to work perfectly but im not sure what happened after i converted my project to have react as a frontend.

Unable to get websockets tests to pass in Django Channels

Given a Django Channels consumer that looks like the following:
class NotificationConsumer(JsonWebsocketConsumer):
def connect(self):
user = self.scope["user"]
async_to_sync(self.channel_layer.group_add)("hello", "hello")
self.accept()
async_to_sync(self.channel_layer.group_send)(
"hello", {"type": "chat.message", "content": "hello"}
)
def receive_json(self, content, **kwargs):
print(content)
async_to_sync(self.channel_layer.group_send)(
"hello", {"type": "chat.message", "content": "hello"}
)
print("Here we are")
def chat_message(self, event):
self.send_json(content=event["content"])
def disconnect(self, close_code):
# Leave room group
async_to_sync(self.channel_layer.group_discard)("hello", "hello")
and a test that looks like the following:
#pytest.mark.asyncio
class TestWebsockets:
async def test_receives_data(self, settings):
communicator = WebsocketCommunicator(
application=application, path="/ws/notifications/"
)
connected, _ = await communicator.connect()
assert connected
await communicator.send_json_to({"type": "notify", "data": "who knows"})
response = await communicator.receive_json_from()
await communicator.disconnect()
I am always getting a TimeoutError when I run the test. What do I need to do differently?
If you'd like to see a full repo example, check out https://github.com/phildini/websockets-test
shouldn't async_to_sync(self.channel_layer.group_add)("hello", "hello") be async_to_sync(self.channel_layer.group_add)("hello", self.channel_name)?
In the first case you are adding "hello" to the group and the communicator.receive_json_from() in the test will fail as the group_send will not be received by the test client.
By refactoring the class as:
class NotificationConsumer(JsonWebsocketConsumer):
def connect(self):
user = self.scope["user"]
async_to_sync(self.channel_layer.group_add)("hello", self.channel_name)
self.accept()
async_to_sync(self.channel_layer.group_send)(
"hello", {"type": "chat.message", "content": "hello"}
)
def receive_json(self, content, **kwargs):
print(content)
async_to_sync(self.channel_layer.group_send)(
"hello", {"type": "chat.message", "content": "hello"}
)
print("Here we are")
def chat_message(self, event):
self.send_json(content=event["content"])
def disconnect(self, close_code):
# Leave room group
async_to_sync(self.channel_layer.group_discard)("hello", self.channel_name)
I can get the tests from the sample repo pass
For testing async channels code you are best of using purely functional async tests.
#pytest.mark.asyncio
async def test_receives_data(settings):
communicator = WebsocketCommunicator(
application=application, path="/ws/notifications/"
)
connected, _ = await communicator.connect()
assert connected
await communicator.send_json_to({"type": "notify", "data": "who knows"})
response = await communicator.receive_json_from()
await communicator.disconnect()
pytest will let you mix theses with class based regular Django tests.
Here you can find some examples for testing consumers.
https://github.com/hishnash/djangochannelsrestframework/tree/master/tests