I'm trying to develop a chat application that saves messages in the database via a WebSocket with Django Channels.
settings.py:
ASGI_APPLICATION = "app.asgi.application"
CHANNEL_LAYERS = {
'default': {
'BACKEND': "channels.layers.InMemoryChannelLayer",
},
}
asgi.py:
from api.routing import websocket_urlpatterns
application = ProtocolTypeRouter({
"http" : get_asgi_application(),
"websocket": JWTAuthMiddleware(
URLRouter(
websocket_urlpatterns
)
),
})
routing.py:
websocket_urlpatterns = [
path('ws/chat/<chat_uuid>/', consumers.ChatConsumer.as_asgi())
]
consumers.py
class ChatConsumer(AsyncJsonWebsocketConsumer):
#database_sync_to_async
def create_message(self, sender, message, chat):
if sender:
if content_validation(message):
chat = Chat.objects.filter(uuid=chat)
if chat.exists():
msg = Message.objects.create(sent_by=sender, content=message)
chat.first().messages.add(msg)
return msg
return None
#database_sync_to_async
def check_security(self, user, uuid):
# Chat has to exist and user has to be part of it
chat = Chat.objects.filter(Q(Q(user1=user) | Q(user2=user)) & Q(uuid=uuid))
if chat.exists():
return True
return False
async def connect(self):
self.chat_uuid = self.scope['url_route']['kwargs']['chat_uuid']
self.room_group_name = 'chat_%s' % self.chat_uuid
user = self.scope["user"]
if user:
check = await self.check_security(user, self.chat_uuid)
if check == True:
# Join room group
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)
await self.accept()
await self.disconnect(401)
async def disconnect(self, close_code):
# Leave room group
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)
# Receive message from WebSocket
async def receive(self, text_data):
text_data_json = json.loads(text_data)
message = text_data_json['message']
name = text_data_json['name']
print(text_data_json)
user = self.scope["user"]
# Is this smart? User not receiving a message for whatever reason won't trigger the create event
new_msg = await self.create_message(user, message, self.chat_uuid)
if new_msg != None:
# Send message to room group
print("Receiving")
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'chat_message',
'message': message,
'name': name
}
)
else:
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'content_guideline_violation',
'message': "",
'name': name
}
)
# Receive message from room group
async def chat_message(self, event):
message = event['message']
name = event['name']
print(event)
# Send message to WebSocket
print("Sending")
await self.send(text_data=json.dumps({
'message': message,
'name': name
}))
I won't post the JWTAuthMiddleware for now since everything worked fine after first implementing it.
The problem is that even though connect and receive are being executed just fine, it just won't execute chat_message anymore for whatever reason.
My Postman requests are looking like the following:
ws://127.0.0.1:8000/ws/chat/<chat_uuid>/?Bearer=<jwt_token>
Postman is able to connect to the websocket and it sends messages as well as saves them in a database but it doesn't show these messages for the other user in the chat.
Related
I'm trying to show the current users that are connected to same websocket (in my case chat room) but I have a little problem. I want to create variable that stores the users and send it trough websockets to my frontend, I've achieved that, but here is the problem.
Consumers.py - approach 1
class ChatRoomConsumer(AsyncWebsocketConsumer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.users: list = []
#database_sync_to_async
def create_msg(self, user_id=None, message=None, room=None):
if user_id is not None:
sender = User.objects.get(id=user_id)
msg = Message.objects.create(
author=sender, message=message, room_name=room)
msg.save()
return msg
else:
get_msgs = Message.objects.filter(room_name__in=[room])
serializer = MessageSerializer(get_msgs, many=True)
return serializer.data
async def connect(self):
self.room_name = self.scope['url_route']['kwargs']['room_name']
self.room_group_name = f'chat_{self.room_name}'
self.messages = await self.create_msg(room=self.room_name)
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)
await self.accept()
await self.send(text_data=json.dumps({
'db_messages': self.messages,
}))
async def disconnect(self, close_code):
print(close_code)
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)
async def receive(self, text_data):
text_data_json = json.loads(text_data)
type = text_data_json['type']
message = text_data_json['message']
username = text_data_json['username']
user_id = text_data_json['user_id']
self.user_id = text_data_json['user_id']
if type == 'chatroom_message':
self.msg = await self.create_msg(user_id, message, self.room_name)
await self.channel_layer.group_send(
self.room_group_name, {
'type': type,
'message': message,
'username': username,
'user_id': user_id
}
)
async def chatroom_message(self, event):
message = event['message']
username = event['username']
await self.send(text_data=json.dumps({
'message': message,
'username': username,
}))
# nefunkcni ukazatel momentalnich uzivatelu v mistnosti
async def get_user(self, event):
print('get_user called')
if event['message'] == 'disconnect':
print('remove', event['username'])
try:
self.users.remove(event['username'])
except ValueError:
print('user already removed')
else:
if event['username'] not in self.users:
self.users.append(event['username'])
print(self.users)
await self.send(text_data=json.dumps({
'users': self.users
}))
In this approach, it corretly shows the current logged in users in the view only from the 1st user that has entered the chat, other users dont see, that the user that has joined before them is there.
In this approach I define the variable users in the constructor.
Consumers.py - approach 2
from channels.generic.websocket import AsyncWebsocketConsumer
from channels.db import database_sync_to_async
from .models import Message
from .serializers import MessageSerializer
from django.contrib.auth.models import User
from django.db.models import Prefetch
from django.core.serializers.json import DjangoJSONEncoder
from asgiref.sync import sync_to_async, async_to_sync
import channels
import json
users: list = []
class ChatRoomConsumer(AsyncWebsocketConsumer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
#self.users: list = []
#database_sync_to_async
def create_msg(self, user_id=None, message=None, room=None):
if user_id is not None:
sender = User.objects.get(id=user_id)
msg = Message.objects.create(
author=sender, message=message, room_name=room)
msg.save()
return msg
else:
get_msgs = Message.objects.filter(room_name__in=[room])
serializer = MessageSerializer(get_msgs, many=True)
return serializer.data
async def connect(self):
self.room_name = self.scope['url_route']['kwargs']['room_name']
self.room_group_name = f'chat_{self.room_name}'
self.messages = await self.create_msg(room=self.room_name)
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)
await self.accept()
await self.send(text_data=json.dumps({
'db_messages': self.messages,
}))
async def disconnect(self, close_code):
print(close_code)
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)
async def receive(self, text_data):
text_data_json = json.loads(text_data)
type = text_data_json['type']
message = text_data_json['message']
username = text_data_json['username']
user_id = text_data_json['user_id']
self.user_id = text_data_json['user_id']
if type == 'chatroom_message':
self.msg = await self.create_msg(user_id, message, self.room_name)
await self.channel_layer.group_send(
self.room_group_name, {
'type': type,
'message': message,
'username': username,
'user_id': user_id
}
)
async def chatroom_message(self, event):
message = event['message']
username = event['username']
await self.send(text_data=json.dumps({
'message': message,
'username': username,
}))
# nefunkcni ukazatel momentalnich uzivatelu v mistnosti
async def get_user(self, event):
print('get_user called')
if event['message'] == 'disconnect':
print('remove', event['username'])
try:
users.remove(event['username'])
except ValueError:
print('user already removed')
else:
if event['username'] not in users:
users.append(event['username'])
print(users)
await self.send(text_data=json.dumps({
'users': users
}))
In this approach, the users show correctly, but not from the current room (it shows users from all the other rooms). It's kinda logical because the declaration of the variable is at the top level.
But my question is where should I declare it then? When it's in the constructor, it always overwrites the users that were in the room before the current user and when it's in the top level, it takes all the users from all the rooms.
In case you haven't found an answer yet, here's my 2 cents:
Consumers are singletons, meaning that there is one instance for every channel (websocket connection). So probably that is the problem of having users inside the consumer class.
An alternative would be to make user a dictionary, not an array. So you can have a key for every room, like so:
users = { "room_1": [], "room_2": [] }
I know there is no code in my answer, but I hope it serves as a guide for your solution!
I'm trying to create one to one chat but when I'm trying to get
self.scope['user']
it returns AnonymousUser
consumers.py
class ChatConsumer(SyncConsumer):
def websocket_connect(self,event):
#error
me = self.scope['user']
other_username = self.scope['url_route']['kwargs']['username']
other_user = User.objects.get(username=other_username)
self.thread_obj = Thread.objects.get_or_create_personal_thread(me,other_user)
self.room_name = f'{self.thread_obj.id}_service'
print(f"{self.channel_name} connected")
async_to_sync(self.channel_layer.group_add)(self.room_name,self.channel_name)
self.send({
'type':'websocket.accept'
})
def websocket_receive(self,event):
print(f"{self.channel_name} message received {event['text']}")
msg = json.dumps({
'text':event.get('text'),
'username':self.scope['user'].username
})
async_to_sync(self.channel_layer.group_send)(
self.room_name,
{
'type':'websocket.message',
'text':msg
}
)
def websocket_message(self,event):
print(f"{self.channel_name} message sent {event['text']}")
self.send({
'type':'websocket.send',
'text':event.get('text')
})
def websocket_disconnect(self,event):
print(f"{self.channel_name} disconnected")
async_to_sync(self.channel_layer.group_discard)(self.room_name,self.channel_name)
print(event)
routing.py
from channels.routing import ProtocolTypeRouter,URLRouter
from django.urls import path
from .consumers import ChatConsumer,EchoConsumer
from channels.auth import AuthMiddlewareStack
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings")
application = ProtocolTypeRouter({
"http": get_asgi_application(),
'websocket': AuthMiddlewareStack(
URLRouter([
path('ws/chat/',EchoConsumer()),
path('ws/chat/<str:username>/',ChatConsumer()),
])
)
})
I think the problem is here in auth.py:
#database_sync_to_async
def get_user(scope):
"""
Return the user model instance associated with the given scope.
If no user is retrieved, return an instance of `AnonymousUser`.
"""
# postpone model import to avoid ImproperlyConfigured error before Django
# setup is complete.
from django.contrib.auth.models import AnonymousUser
if "session" not in scope:
raise ValueError(
"Cannot find session in scope. You should wrap your consumer in "
"SessionMiddleware."
)
session = scope["session"]
user = None
try:
user_id = _get_user_session_key(session)
backend_path = session[BACKEND_SESSION_KEY]
except KeyError:
pass
else:
if backend_path in settings.AUTHENTICATION_BACKENDS:
backend = load_backend(backend_path)
user = backend.get_user(user_id)
# Verify the session
if hasattr(user, "get_session_auth_hash"):
session_hash = session.get(HASH_SESSION_KEY)
session_hash_verified = session_hash and constant_time_compare(
session_hash, user.get_session_auth_hash()
)
if not session_hash_verified:
session.flush()
user = None
return user or AnonymousUser()
because codes under else not running
I've made some changes to the code that I took from the documentation and the result is like this
class ChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
me = self.scope['user']
other_username = self.scope['url_route']['kwargs']['username']
other_user =await sync_to_async(User.objects.get)(username=other_username)
thread_obj =await sync_to_async(Thread.objects.get_or_create_personal_thread)(me,other_user)
self.room_name = f'{thread_obj.id}_service'
self.room_group_name = 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):
text_data_json = json.loads(text_data)
message = text_data_json['message']
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'chat_message',
'message': message
}
)
async def chat_message(self, event):
message = event['message']
await self.send(text_data=json.dumps({
'message': message
}))
I am working on a chat app with django channels, everthing is working but messages don't get send when room name has spaces or some other special carachters, ideally I'd like to be possible to the user to be free and name their room whatever they want and include spaces as well.
/*
consumers.py:
class ChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.room_name = self.scope['url_route']['kwargs']['room_name']
self.room_group_name = 'chat_%s' % self.room_name
# Join room
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)
await self.accept()
async def disconnect(self, close_code):
# Leave room
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)
# Receive message from web socket
async def receive(self, text_data):
data = json.loads(text_data)
message = data['message']
username = data['username']
room = data['room']
await self.save_message(username, room, message)
# Send message to room group
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'chat_message',
'message': message,
'username': username
}
)
async def chat_message(self, event):
message = event['message']
username = event['username']
# Send message to WebSocket
await self.send(text_data=json.dumps({
'message': message,
'username': username,
'timestamp': timezone.now().isoformat()
}))
#sync_to_async
def save_message(self, username, room, message):
if len(message) > 0:
Message.objects.create(username=username, room=room, content=message)
routing.py:
websocket_urlpatterns = [
path('ws/<str:room_name>/', consumers.ChatConsumer.as_asgi()),
]
urls.py:
urlpatterns = [
path('', views.index, name='index'),
path('<str:room_name>/', views.room, name='room'),
]
index.html:
<script>
document.querySelector('#room-name-input').focus();
document.querySelector("#room-name-input, #username-input").onkeyup = function(e) {
if (e.keyCode === 13) {
document.querySelector('#room-name-submit').click();
}
};
document.querySelector('#room-name-submit').onclick = function(e) {
var roomName = document.querySelector('#room-name-input').value;
var userName = document.querySelector('#username-input').value;
window.location.replace(roomName + '/?username=' + userName);
};
</script>
*/
I'm using django-channels to organize my websockets on backend.
Right now everything is working alright except of message sending to the front-end when info in db is changed. There is http endpoint to change model.
Here is my websocket consumer
import asyncio
from asgiref.sync import async_to_sync, sync_to_async
from channels.generic.websocket import AsyncJsonWebsocketConsumer
from rest_framework.response import Response
from rest_framework import status
from channels.db import database_sync_to_async
# models and serializers
from billing.models import Billing
from billing.serializers import BillingSerializer
from customers.models import Customer
from customers.serializers import CustomersSerializer
from shipping.models import Shipping
from shipping.serializers import ShippingSerializer
from .models import Order
from .serializers import OrdersSerializer
from .views import OrdersViewSet
from .exceptions import ClientError
from .utils import get_orders_or_error, get_orders_or_warning
class OrdersConsumer(AsyncJsonWebsocketConsumer):
async def connect(self):
orders = await get_orders_or_warning()
if 'user' in self.scope:
await self.close()
else:
await self.accept()
self.orders = set(orders)
# await self.create_order(content)
async def receive_json(self, content):
command = content.get('command', None)
orders = await get_orders_or_warning()
# print(list(self.orders))
# print(set(orders))
try:
if command == "join":
await self.join_room(JWT_Token=content['token'])
elif command == "leave":
await self.leave_room()
elif command == "send":
await self.send_room(content['message'])
except ClientError as e:
await self.send_json({"error": e.code})
async def disconnect(self, code):
for room_id in list(self.rooms):
try:
self.send_json({
'type': 'CLOSE',
'message': "Socket closed"
})
await self.leave_room(content['token'])
except ClientError:
pass
async def join_room(self, JWT_Token):
orders = await get_orders_or_warning()
serializer = OrdersSerializer(orders, many=True)
# print('serializer', serializer)
if self.orders != set(orders):
await self.send_json(
{
'type': 'orders',
'data': json.dumps(serializer.data),
},
)
await self.send_json(
{
'type': 'orders',
'data': json.dumps(serializer.data),
},
)
async def leave_room(self, JWT_Token):
await self.channel_layer.group_send(
orders.group_name,
{
'type': 'orders.leave',
'JWT_Token': JWT_Token
}
)
self.rooms.discard()
await self.channel_layer.group_discard(
orders.group_name,
self.channel_name
)
await self.send_json({
"leave": str(room_id),
})
async def send_room(self, message):
if room_id not in self.rooms:
raise ClientError("ROOM_ACCESS_DENIED")
orders = await get_orders_or_warning()
serializer = OrdersSerializer(orders, many=True)
await self.send_json(
{
'type': 'orders.give',
'data': json.dumps(serializer.data)
}
)
await self.send(text_data=json.dumps({
'message': message
}))
async def orders_leave(self, event):
await self.send_json(
{
'type': 'LEAVE',
'room': event["room_id"],
'username': event["username"]
}
)
Here is my routing file
application = ProtocolTypeRouter({
'websocket': (
URLRouter([
url('orders/', OrdersConsumer),
])
)
})
I want to get all data with front-end when some changes happened. Can this be done with current consumer? And if yes, how? I've looked too many info sources i think and now I'm kinda confused how i can actually do this.
I rly don't want to rewrite the structure, if it's possible. If you can explain how and why I need to write it the way you say I will be very grateful
You can use signals to monitor the change and send a message to the consumer using the approach defined in the docs on how to interact with the consumer from the outside
Hi i'm struggling with getting two things working simultaneously...
The channels2 chat room example was okay to get going, but i wanted to add a feature of knowing how many people where in the room. I did this by updating a model of the rooms.
Then I wanted to have a dashboard which would show the most popular current rooms, which again i wanted to update with using channels.
I used the django signals method and this method worked for updating the model whilst nobody was using the chat.
However, when seeing if the dashboard updated when somebody joined the chat there was an error.
2018-05-11 19:19:09,634 - ERROR - server - Exception inside application: You cannot use AsyncToSync in the same thread as an async event loop - just await the async function directly.
File "/dev/channels_sk/channels-master/channels/consumer.py", line 54, in __call__
await await_many_dispatch([receive, self.channel_receive], self.dispatch)
File "/dev/channels_sk/channels-master/channels/utils.py", line 50, in await_many_dispatch
await dispatch(result)
File "/dev/channels_sk/channels-master/channels/consumer.py", line 67, in dispatch
await handler(message)
File "/dev/channels_sk/channels-master/channels/generic/websocket.py", line 173, in websocket_connect
await self.connect()
File "/dev/channels_sk/tables/consumers.py", line 19, in connect
room.save()
File "/dev/channels_sk/.env/lib/python3.6/site-packages/django/db/models/base.py", line 729, in save
force_update=force_update, update_fields=update_fields)
File "/dev/channels_sk/.env/lib/python3.6/site-packages/django/db/models/base.py", line 769, in save_base
update_fields=update_fields, raw=raw, using=using,
File "/dev/channels_sk/.env/lib/python3.6/site-packages/django/dispatch/dispatcher.py", line 178, in send
for receiver in self._live_receivers(sender)
File "/dev/channels_sk/.env/lib/python3.6/site-packages/django/dispatch/dispatcher.py", line 178, in <listcomp>
for receiver in self._live_receivers(sender)
File "/dev/channels_sk/tables/signals.py", line 20, in room_save_handler
'update': instance.population,
File "/dev/channels_sk/.env/lib/python3.6/site-packages/asgiref/sync.py", line 34, in __call__
"You cannot use AsyncToSync in the same thread as an async event loop - "
You cannot use AsyncToSync in the same thread as an async event loop - just await the async function directly.
consumers.py
from channels.generic.websocket import AsyncWebsocketConsumer
import json
import time
from .models import Room
from django.db.models import F
class ChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.room_name = self.scope['url_route']['kwargs']['room_name']
self.room_group_name = 'chat_%s' % self.room_name
# create room or increment
room, created = Room.objects.get_or_create(title=self.room_name)
pop = room.population + 1
room.population = F('population') + 1
room.save()
# Join room group
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)
await self.accept()
# send new population to group
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'pop_message',
'population': pop,
}
)
async def disconnect(self, close_code):
room = Room.objects.get(title=self.room_name)
pop = room.population - 1
if room.population == 1:
if room.permanent == False:
room.delete()
else:
room.population = F('population') - 1
room.save()
# send new population to room group
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'pop_message',
'population': pop,
}
)
# Leave room group
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)
# Receive message from WebSocket
async def receive(self, text_data):
text_data_json = json.loads(text_data)
message = text_data_json['message']
# Send message to room group
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'chat_message',
'message': message,
)
# Receive message from room group
async def chat_message(self, event):
message = event['message']
# Send message to WebSocket
await self.send(text_data=json.dumps({
# 'type': 'chat_message',
'message': message,
}))
# change in room group population
async def pop_message(self, event):
content = event['type']
population = event['population']
# Send message to WebSocket
await self.send(text_data=json.dumps({
# 'type': 'pop_message',
'content': content,
'population': population,
}))
class RoomConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.group_name = 'chat_room_dash'
print("joined dash room")
# Join room group
await self.channel_layer.group_add(
self.group_name,
self.channel_name
)
await self.accept()
async def disconnect(self, close_code):
print("left dash room")
pass
async def send_message(self, text_data=None):
print(text_data)
labels = []
data = []
for room in Room.objects.all()[:3]:
labels.append(room.title)
data.append(room.population)
await self.send(text_data=json.dumps(
{
'labels': labels,
'data': data,
}))
signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync
from .models import Room
#receiver(post_save, sender=Room)
def room_save_handler(sender, instance, **kwargs):
channel_layer = get_channel_layer()
async_to_sync(channel_layer.group_send)(
'chat_room_dash',
{
'type': 'send_message',
'update': instance.population,
}
)
so far
In reading the error i have attempted the suggested solution by changing the the signals.py room_save_handler() to be async and await the message to the group.
This however didn't ever send the message when i updated the model manually or when a user went into the chat room.
My guest is that when the first consumer calls room.save() that the room_save_handler() is also called which means there is an async call within an async call.
Any help would be great!
I have the same problem and I have solved it in the following way:
import asyncio
from django.db.models.signals import post_save
from django.dispatch import receiver
from channels.layers import get_channel_layer
from .models import Room
#receiver(post_save, sender=Room)
def room_save_handler(sender, instance, **kwargs):
channel_layer = get_channel_layer()
loop = asyncio.get_event_loop()
coroutine = async_to_sync(channel_layer.group_send)(
'chat_room_dash',
{
'type': 'send_message',
'update': instance.population,
}
)
loop.run_until_complete(coroutine)