django channels fetch data from db and send over websocket - django

error:
File "/Users/soubhagyapradhan/Desktop/upwork/polyverse/polyverse_api/env/lib/python3.8/site-packages/django/db/models/sql/compiler.py", line 1173, in execute_sql
cursor = self.connection.cursor()
File "/Users/soubhagyapradhan/Desktop/upwork/polyverse/polyverse_api/env/lib/python3.8/site-packages/django/utils/asyncio.py", line 24, in inner
raise SynchronousOnlyOperation(message)
django.core.exceptions.SynchronousOnlyOperation: You cannot call this from an async context - use a thread or sync_to_async.
WebSocket DISCONNECT /ws/test/ [127.0.0.1:63954]
comsumer.py
import json
from channels.generic.websocket import AsyncWebsocketConsumer
from random import randint
from api.models import Asset
class WSConsumer(AsyncWebsocketConsumer):
async def connect(self):
await self.accept()
res = list(Asset.objects.filter(id=56).values('token_id'))
await self.send(json.dumps(res))
here is the code i am using for websocket. here
i am fetching data from database but getting above error
please take a look what will be the error ?

I think you need database_sync_async like this:
class WSConsumer(AsyncWebsocketConsumer):
# rest of the code
res = await database_sync_to_async(self.get_asset(56))()
# rest of the code
def get_asset(self, id):
return list(Asset.objects.filter(id=id).values('token_id'))
I believe it is because Django ORM is synchronous.

Related

Best practices for authenticating Django Channels

Django 4.1.4
djoser 2.1.0
channels 4.0.0
I have followed the documented recommendation for creating custom middleware to authenticate a user when using channels and I am successfully getting the user
and checking that the user is authenticated though I am sending the user ID in the querystring when connecting to the websocket to do this. The user is not automatically available in the websocket scope.
I am unsure if there are any potential security risks as the documentation mentions that their recommendation is insecure, I do check that the user.is_authenticated. So I believe I have secured it.
I do believe that using the token created by djoser would be better though I am not sure how to send headers with the websocket request unless I include the token in the querystring instead of the user's ID.
I am keen to hear what the best practices are.
I am passing the user ID to the websocket via querystring as follows at the frontend:
websocket.value = new WebSocket(`ws://127.0.0.1:8000/ws/marketwatch/? ${authStore.userId}`)
middleware.py
from channels.db import database_sync_to_async
from django.contrib.auth.models import AnonymousUser
from django.contrib.auth import get_user_model
from django.core.exceptions import ObjectDoesNotExist
#database_sync_to_async
def get_user(user_id):
User = get_user_model()
try:
user = User.objects.get(id=user_id)
except ObjectDoesNotExist:
return AnonymousUser()
else:
if user.is_authenticated:
return user
else:
return AnonymousUser()
class QueryAuthMiddleware:
def __init__(self, app):
self.app = app
async def __call__(self, scope, receive, send):
scope['user'] = await get_user(int(scope["query_string"].decode()))
return await self.app(scope, receive, send)
consumers.py
import os
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application
from channels.security.websocket import AllowedHostsOriginValidator
from api.middleware import QueryAuthMiddleware
from .routing import ws_urlpatterns
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'api.settings')
application = ProtocolTypeRouter({
'http':get_asgi_application(),
'websocket': AllowedHostsOriginValidator(
QueryAuthMiddleware(
URLRouter(ws_urlpatterns)
)
)
})
After doing some extensive research I decided not to pass the id or the token via the querystring as this poses a risk due to this data being stored in the server logs.
IMO the best option with the least amount of risk was passing the token as a message to the websocket after the connection was established and then verifying the token; closing the websocket if invalid.
This meant not requiring the middleware previously implemented. In this particular project no other messages would be received from the client so I don't need to do any checking on the key of the message received. This could be changed for chat apps and other apps that will receive further messages from the client.
from channels.generic.websocket import AsyncWebsocketConsumer
from channels.db import database_sync_to_async
import json
from rest_framework.authtoken.models import Token
class MarketWatchConsumer(AsyncWebsocketConsumer):
#database_sync_to_async
def verify_token(self, token_dict):
try:
token = Token.objects.get(key=token_dict['token'])
except Token.DoesNotExist:
return False
else:
if token.user.is_active:
return True
else:
return False
async def connect(self):
await self.channel_layer.group_add('group', self.channel_name)
await self.accept()
async def receive(self, text_data=None, bytes_data=None):
valid_token = await self.verify_token(json.loads(text_data))
if not valid_token:
await self.close()
async def disconnect(self, code):
await self.channel_layer.group_discard('group', self.channel_name)

Asynchrony in Python. How to receive requests in a Python Telegram bot and at the same time perform another function

I am coding telegram-bot, using Flask and asyncio. And I have a function for receiving and processing requests - get_messages(). And test function - sleep_func(), which will sleep for a while(20 seconds), but get_messages() will be working at this moment, when sleep_func() is sleeping. After 20 seconds sleep_func() must wake up, print some text, but doesn't finish the get_messages(). So, I want to implement asynchrony. I tried to do this, using asyncio.create_task, that worked, but only for one request(see at the comments in get_messages()). Now I tried another way, but it gives me this error:
ERROR:asyncio:Task exception was never retrieved
future: <Task finished name='Task-7' coro=<get_messages() done, defined at D:\Python_Labs\FilmMarketBot\fm_bot.py:31> exception=RuntimeError('Timeout context manager should be used inside a task')>
Traceback (most recent call last):
File "D:\Python_Labs\FilmMarketBot\fm_bot.py", line 37, in get_messages
await message(bot)
File "D:\Python_Labs\FilmMarketBot\bot_files\message_handling.py", line 14, in message
await commands(bot, chat_id)
File "D:\Python_Labs\FilmMarketBot\bot_files\messages\bot_commands\commands.py", line 11, in commands
await start(bot, chat_id)
File "D:\Python_Labs\FilmMarketBot\bot_files\messages\bot_commands\commands_methods.py", line 11, in start
await bot.send_message(chat_id, f"Hello, {first_name}!")
File "D:\Python_Labs\FM_Bot_test\venv\lib\site-packages\aiogram\bot\bot.py", line 339, in send_message
result = await self.request(api.Methods.SEND_MESSAGE, payload)
File "D:\Python_Labs\FM_Bot_test\venv\lib\site-packages\aiogram\bot\base.py", line 231, in request
return await api.make_request(await self.get_session(), self.server, self.__token, method, data, files,
File "D:\Python_Labs\FM_Bot_test\venv\lib\site-packages\aiogram\bot\api.py", line 139, in make_request
async with session.post(url, data=req, **kwargs) as response:
File "D:\Python_Labs\FM_Bot_test\venv\lib\site-packages\aiohttp\client.py", line 1138, in __aenter__
self._resp = await self._coro
File "D:\Python_Labs\FM_Bot_test\venv\lib\site-packages\aiohttp\client.py", line 466, in _request
with timer:
File "D:\Python_Labs\FM_Bot_test\venv\lib\site-packages\aiohttp\helpers.py", line 701, in __enter__
raise RuntimeError(
RuntimeError: Timeout context manager should be used inside a task
This is my fm_bot.py:
# бібліотека request для отримання запитів(повідомлень), які надходять на Flask-сервер від Телеграм-серверу і Stripe
from flask import Flask, request
import logging
from aiogram import Bot, Dispatcher
from threading import Thread
# from dotenv import load_dotenv
from time import sleep
import asyncio
import time
from bot_files.models import db, Users, Films, Purchases
from bot_files.message_handling import message, callback_query, object, other_messages
# from bot_files.other import technical_works
from bot_files.chat_member import delete_user
from bot_files.stripe import create_stripe_webhook
from bot_files.objects.stripe_requests_files.purchases import add_purchase
from bot_files.config import *
logging.basicConfig(level=logging.INFO)
bot = Bot(token=bot_token)
dp = Dispatcher(bot)
app = Flask(__name__)
app.config['DEBUG'] = True
app.config['SQLALCHEMY_DATABASE_URI'] = db_url
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = True
db.init_app(app)
async def get_messages():
print("Flag")
print(request.json)
message_type = list(request.json.keys())[1]
if message_type == 'message':
await message(bot)
elif message_type == 'callback_query':
await callback_query(bot, payment_token, stripe_token)
elif message_type == 'my_chat_member':
chat_id = request.json["my_chat_member"]["chat"]["id"]
if request.json["my_chat_member"]["new_chat_member"]["status"] == "kicked":
await delete_user(chat_id)
elif message_type == 'object':
await object(bot)
elif message_type == 'pre_checkout_query':
# Номер карти - 4242 4242 4242 4242
chat_id = request.json["pre_checkout_query"]["from"]["id"]
await add_purchase(bot, chat_id)
pre_checkout_query_id = request.json["pre_checkout_query"]["id"]
await bot.answer_pre_checkout_query(pre_checkout_query_id, True)
# else:
# await other_messages(bot)
return {"get_messages is ok": True}
async def sleep_func():
await asyncio.sleep(20)
print("Hello!")
#app.route('/' + bot_token, methods=['POST'])
async def asynchrony():
#start_time = time.time()
#get_messages_task = asyncio.create_task(get_messages())
#sleep_task1 = asyncio.create_task(sleep_func())
#sleep_task2 = asyncio.create_task(sleep_func())
#await asyncio.wait([get_messages_task, sleep_task1])
#print(time.time() - start_time)
get_messages_task = asyncio.create_task(get_messages())
await asyncio.wait([get_messages_task])
#await get_messages()
return {"asynchrony is ok": True}
#app.route('/')
async def webhook():
await bot.delete_webhook()
await bot.set_webhook(url=app_url)
# chat_id = None
await create_stripe_webhook(app_url, payment_token)
# await test_users()
# await test_films()
# await test_purchases()
# await test_discounts()
# while True:
# time_now = datetime.datetime.now()
# if datetime.date.today().isoweekday() == 4:
# await technical_works(bot)
sleep_task = asyncio.create_task(sleep_func())
await asyncio.wait([sleep_task])
return '!', 200
if __name__ == "__main__":
app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 5000)))
I will be very appreciative for you help!
My answer is not suitable for this code, which is Flask-app. But it is answer for this question. So you must use Aiogram for asynchrony receiving and processing messages. Aiogram has special decorator async_task for this: https://docs.aiogram.dev/en/latest/_modules/aiogram/dispatcher/dispatcher.html#Dispatcher.async_task.
If your bot at the same time must perform another function, for example, change data in database once a day, you can create button only for administrator that will start infinity cycle which will sleep for number of seconds in one day.

django channels WebsocketCommunicator TimeoutError

I am trying to run the following test:
tests.py
from rest_framework.test import APITestCase
from myapp.routing import application
from channels.testing import WebsocketCommunicator
from account.models import User
from rest_framework.authtoken.models import Token
class Tests(APITestCase):
def setUp(self):
self.user = User.objects.create(email='test#test.test',
password='a password')
self.token, created = Token.objects.get_or_create(user=self.user)
async def test_connect(self):
communicator = WebsocketCommunicator(application, f"/ws/user/{self.token}/")
connected, subprotocol = await communicator.connect()
self.assertTrue(connected)
await communicator.disconnect()
application is a boilerplate instance of channels.routing.ProtocolTypeRouter (like in here: https://channels.readthedocs.io/en/latest/topics/routing.html). Everything works fine in production. The test exits with the following error:
Traceback (most recent call last):
File "/home/projects/myapp/myapp-env/lib/python3.7/site-packages/asgiref/testing.py", line 74, in receive_output
return await self.output_queue.get()
File "/usr/lib/python3.7/asyncio/queues.py", line 159, in get
await getter
concurrent.futures._base.CancelledError
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/home/projects/myapp/myapp-env/lib/python3.7/site-packages/asgiref/sync.py", line 223, in __call__
return call_result.result()
File "/usr/lib/python3.7/concurrent/futures/_base.py", line 428, in result
return self.__get_result()
File "/usr/lib/python3.7/concurrent/futures/_base.py", line 384, in __get_result
raise self._exception
File "/home/projects/myapp/myapp-env/lib/python3.7/site-packages/asgiref/sync.py", line 292, in main_wrap
result = await self.awaitable(*args, **kwargs)
File "/home/projects/myapp/myapp-api/app/tests.py", line 35, in test_connect
connected, subprotocol = await communicator.connect()
File "/home/projects/myapp/myapp-env/lib/python3.7/site-packages/channels/testing/websocket.py", line 36, in connect
response = await self.receive_output(timeout)
File "/home/projects/myapp/myapp-env/lib/python3.7/site-packages/asgiref/testing.py", line 85, in receive_output
raise e
File "/home/projects/myapp/myapp-env/lib/python3.7/site-packages/asgiref/testing.py", line 74, in receive_output
return await self.output_queue.get()
File "/home/projects/myapp/myapp-env/lib/python3.7/site-packages/asgiref/timeout.py", line 66, in __aexit__
self._do_exit(exc_type)
File "/home/projects/myapp/myapp-env/lib/python3.7/site-packages/asgiref/timeout.py", line 103, in _do_exit
raise asyncio.TimeoutError
concurrent.futures._base.TimeoutError
----------------------------------------------------------------------
Ran 1 test in 1.026s
I have tried python versions 3.7.5, 3.8.0 and 3.9.9 using channels 3.0.4 with django 3.2.10 and channels-redis 3.3.1 ('BACKEND': 'channels_redis.core.RedisChannelLayer' in settings.py). The error persists. What am I doing wrong?
I had the same problem. APITestCase or TestCase dont allow transactions, you have to use SimpleTestCase from django test, and set databases to all. Just with that difference i think it will work.
Note that the transactions will be saved between test, and not rolled back after the test.
from django.test import SimpleTestCase
from myapp.routing import application
from channels.testing import WebsocketCommunicator
from account.models import User
from rest_framework.authtoken.models import Token
class Tests(SimpleTestCase):
databases = '__all__'
def setUp(self):
self.user = User.objects.create(email='test#test.test', password='a password')
self.token, created = Token.objects.get_or_create(user=self.user)
async def test_connect(self):
communicator = WebsocketCommunicator(application, f"/ws/user/{self.token}/")
connected, subprotocol = await communicator.connect()
self.assertTrue(connected)
await communicator.disconnect()
here are the information of SimpleTestCase
https://docs.djangoproject.com/en/4.0/topics/testing/tools/
I faced a similar issue and the solution is usually to mimic your production router for the tests too, i.e whatever middleware or additional component used in production should also be added when imstantiating your Communicator. for example in my asgi.py I have:
application = ProtocolTypeRouter(
{
"http": get_asgi_application(),
"websocket": AllowedHostsOriginValidator(
jwt_auth_middleware_stack(URLRouter(chat.routing.websocket_urlpatterns)),
),
}
)
My communicator is instantiated as follows:
communicator = WebsocketCommunicator(jwt_auth_middleware_stack(URLRouter(websocket_urlpatterns)),
f"/ws/chat/{chat.id}/?token={token}")
And my url is:
websocket_urlpatterns = [
path("ws/chat/<str:chat_id>/", consumers.AsyncChatConsumer.as_asgi())
]

Django channel not getting message

I am using django channel in my current project. From one of my django app, I am sending notification to the channel layer, so that websocket can broadcast the message. But problem is
consumer is not getting my message.
Utils in django app for sending notification to channel:
from asgiref.sync import AsyncToSync
from channels.layers import get_channel_layer
import json
def async_send(group_name, text):
channel_layer = get_channel_layer()
AsyncToSync(channel_layer.group_send)(
group_name,
{
'type': 'notify',
'text': json.dumps(text)
}
)
My consumer file is:
from channels.generic.websocket import AsyncWebsocketConsumer
class InformationConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.channel_layer.group_add(str(self.scope['user']), self.channel_name)
await self.accept()
async def notify(self, event):
await self.send(
{
"message": event['text'],
},
)
print(event['text'])
I am supposed to get the output of event['text'], but getting nothing :(
change from
self.channel_layer.group_add(str(self.scope['user']), self.channel_name)
to
await self.channel_layer.group_add(str(self.scope['user']), self.channel_name)

Custom Django Channels middleware not finishing processing before Websocket connects

I have an existing WSGI application which I'm adding Django Channels to to give websocket functionality. I created a consumer using WebsocketConsumer, added the custom middleware into the routing file, and implemented a basic version of pulling the token from the incoming connection request. I can successfully print the token that's in the database, so I know the correct information is passing.
I can connect to the socket, but it always comes back as being an anonymous user within the scope. It seems that the get_user_from_token function is not getting a chance to execute before the connect function executes, because all of the prints within the __call__ function of the TokenAuthMiddleware class are printed and none of the prints from the get_user_from_Token are printing. I tried switching the consumer to an async consumer, but that opened up a whole other set of problems that I couldn't figure out. I tried putting async in front of the __call__ and await in front of the function call, but that didn't work either. The current error I'm getting is:
Exception inside application: 'coroutine' object has no attribute '_wrapped'
File "C:\Users\PC\Envs\p3\lib\site-packages\channels\sessions.py", line 183, in __call__
return await self.inner(receive, self.send)
File "C:\Users\PC\Envs\p3\lib\site-packages\channels\middleware.py", line 40, in coroutine_
call
await self.resolve_scope(scope)
File "C:\Users\PC\Envs\p3\lib\site-packages\channels\auth.py", line 166, in resolve_scope
scope["user"]._wrapped = await get_user(scope)
'coroutine' object has no attribute '_wrapped'
How do I get my middleware to finish what it's doing before connect tries to test the user?
my_app/routing.py
from channels.routing import ProtocolTypeRouter, URLRouter
import api.channels.routing
from my_app.ws_token_auth import TokenAuthMiddlewareStack
application = ProtocolTypeRouter({
# (http->django views is added by default)
'websocket': TokenAuthMiddlewareStack(
URLRouter(
api.channels.routing.websocket_urlpatterns
)
),
})
api/channels/consumers.py
import json
from asgiref.sync import async_to_sync
from channels.db import database_sync_to_async
from channels.generic.websocket import WebsocketConsumer
class HeaderConsumer(WebsocketConsumer):
def connect(self):
if self.scope["user"].is_anonymous:
# Reject the connection
print('rejected')
self.close()
else:
self.accept()
self.user = self.scope['user']
self.message_threads = set()
def disconnect(self, code):
"""
Called when the WebSocket closes for any reason.
"""
# Leave all the rooms we are still in
for thread_id in list(self.message_threads):
try:
self.leave_thread(thread_id)
except ClientError:
pass
def receive(self, text_data):
text_data_json = json.loads(text_data)
message = text_data_json['message']
self.send(text_data=json.dumps({
'message': message + message
}))
my_app/ws_token_auth.py
from channels.auth import AuthMiddlewareStack
from channels.db import database_sync_to_async
from rest_framework.authtoken.models import Token
from django.contrib.auth.models import AnonymousUser
from django.db import close_old_connections
#database_sync_to_async
def close_connections():
close_old_connections()
#database_sync_to_async
def get_user_from_token(t):
try:
print("trying token" + t)
token = Token.objects.get(token=t).prefetch_related('user')
return token.user
except Token.DoesNotExist:
print("failed")
return AnonymousUser()
class TokenAuthMiddleware:
"""
Token authorization middleware for Django Channels 2
"""
def __init__(self, inner):
self.inner = inner
def __call__(self, scope):
close_connections()
print("hi")
headers = dict(scope['headers'])
if b'cookie' in headers:
pieces = headers[b'cookie'].decode().split("; ")
key_values = {i.split('=', 1)[0]: i.split('=', 1)[1] for i in pieces}
print("x")
if 'token' in key_values:
try:
scope['token'] = key_values['token']
print("y")
user = get_user_from_token(key_values['token'])
print("z")
except Token.DoesNotExist:
print("no token")
user = AnonymousUser()
else:
print("no token?")
else:
print("no cookie")
return self.inner(dict(scope, user=user))
TokenAuthMiddlewareStack = lambda inner: TokenAuthMiddleware(AuthMiddlewareStack(inner))
Change you class HeaderConsumer(WebsocketConsumer): with
class HeaderConsumer(AsyncWebsocketConsumer):
And also check if your websocket_urlpatterns:
websocket_urlpatterns = [
re_path(r'your path', consumers.HeaderConsumer.as_asgi()),
]