I'm trying to use channels(v2.1.7) in django to send messages from server to client. When i execute the celery task below, my message is not being fetched in consumers.py(so not being sent to client) and surprisingly no error occures.
I'm able to send message from consumers to client directly. But i couldn't manage to send from outside of consumers using async_to_sync().
(I tried to use async_to_sync method in standard django views.py and i had same problem)
wololo/tasks.py
#app.task(name='wololo.tasks.upgrade_building')
def upgrade_building(user_id):
os.environ['DJANGO_SETTINGS_MODULE'] = 'DjangoFirebaseProject.settings'
from channels.layers import get_channel_layer
channel_layer = get_channel_layer()
print(channel_layer, "wololo")
async_to_sync(channel_layer.send)('chat', {
'type': 'hello.message',
'message': 'hadiInsss',
})
return True
wololo/consumers.py
from channels.generic.websocket import WebsocketConsumer
import json
from asgiref.sync import async_to_sync
class ChatConsumer(WebsocketConsumer):
def connect(self):
async_to_sync(self.channel_layer.group_add)("chat", self.channel_name)
self.accept()
def disconnect(self, close_code):
async_to_sync(self.channel_layer.group_discard)("chat", self.channel_name)
def hello_message(self, event):
print("U MUST SEE THAT MSG")
# Send a message down to the client
self.send(text_data=json.dumps(event['message']))
the result that i have in celery terminal
click to see celery terminal
Thanks in advance
It looks like you are using the channel_layer.send method, but I think you actually want to use channel_layer.group_send instead.
Related
I use Django Channels with channel_layers (RedisChannelLayer).
Using Channels I only need to get live messages from signals when post_save event happens.
I try to send a message from the signals.py module.
The fact that the first message is sending properly, I got it successfully in the js console,
but then disconnection from the socket happens with an Exception:
RuntimeError: Task got Future attached to a different loop.
It refers to ...redis/asyncio/connection.py:831
All my settings were done properly in accordance with the documentation.
My project also uses DRF, Celery(on Redis), Redis itself, Daphne server.
I only try to implement it with Debug=True mode, let alone production.
I have no idea what happens and how to solve it.
Here are snippets from my code:
#consumers.py
class LikeConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.room_group_name = "likes"
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)
await self.accept()
async def chat_message(self, event):
message = event['message']
await self.send(text_data=json.dumps({
'type': 'chat',
'message': message
}))
#signals.py
channel_layer = get_channel_layer()
#receiver(post_save, sender=Like)
def new_like(sender, instance, **kwargs):
async_to_sync(channel_layer.group_send)(
'likes',
{
"type": "chat_message",
"message": "1212341324, 213413252345"
}
)
//script.js
const likeSocket = new WebSocket(url);
likeSocket.onmessage = function(e) {
let data = JSON.parse(e.data);
console.log(data);
};
After a few days of investigating the issue, I have realized the Exception, RuntimeError, in that case, is just a Warning, not Error.
Because the server doesn't break and the Socket is going on further.
And that "Warning" is still in a Bug stage in Channels developers.
It looks not beauty in the server console, but it works properly.
I tried to try-except it, but can't figure it out - don't understand what code invokes the Error to try-except it.
I'm currently using discord-ext-ipc as an inter-process communication (IPC) extension for discord.py and linked this with quart-discord. Quart is an asyncio reimplementation of Flask and can natively serve WebSockets and other asynchronous stuff.
I can so easily use a Web-Frontend as a Dashboard to interact with the discord users on a guild. Doing fun stuff like, using the discord role model for a permission model at the dashboard.
Recently I stumbled over django and got hooked up with that powerful framework. As a learning project, I wanted to port the current working project into django but I stumbled over some impasses.
First the working example with discord-ext-ipc and quart-discord:
bot.py
import logging
import discord
from discord.ext import commands, ipc
intents = discord.Intents.default()
logging.basicConfig(level=logging.DEBUG)
TOKEN = DISCORD_TOKEN
class MyBot(commands.Bot):
def __init__(self,*args,**kwargs):
super().__init__(*args,**kwargs)
self.ipc = ipc.Server(self,secret_key = IPC_SECRET_KEY)
async def on_ready(self):
"""Called upon the READY event"""
print("Bot is ready.")
async def on_ipc_ready(self):
"""Called upon the IPC Server being ready"""
print("Ipc server is ready.")
async def on_ipc_error(self, endpoint, error):
"""Called upon an error being raised within an IPC route"""
print(endpoint, "raised", error)
my_bot = MyBot(command_prefix = ">", intents = intents, chunk_guilds_at_startup = False)
#my_bot.ipc.route()
async def get_guild_ids(data):
final = []
for guild in my_bot.guilds:
final.append(guild.id)
return final # returns the guild ids to the client
my_bot.ipc.start()
my_bot.run(TOKEN)
main.py
from discord.ext.commands.core import bot_has_permissions, bot_has_role
from quart import Quart, render_template, request, session, redirect, url_for
from quart_discord import DiscordOAuth2Session
from discord.ext import ipc
app = Quart(__name__)
ipc_client = ipc.Client(secret_key = IPC_SECRET_KEY)
app.config["SECRET_KEY"] = IPC_SECRET_KEY
app.config["DISCORD_CLIENT_ID"] = DISCORD_CLIENT_ID
app.config["DISCORD_CLIENT_SECRET"] = DISCORD_CLIENT_SECRET
app.config["DISCORD_REDIRECT_URI"] = DISCORD_REDIRECT_URI
discord = DiscordOAuth2Session(app)
#app.route("/dashboard")
async def dasboard():
guild_ids = await ipc_client.request("get_guild_ids")
return await render_template("list.html", guild_ids = guild_ids)
if __name__ == "__main__":
app.run(debug=True, port=5001)
list.html
<html>
<head>
<title></title>
</head>
<body>
{% for guild_id in guild_ids %}
<p>{{ guild_id }}</p>
{% endfor %}
</body>
</html>
Reaching out to: http://localhost:5001/dashboard gives me the list of guild ids the bot is joined.
As far as I understood, calling await ipc_client.request("get_guild_ids"), sends a websocket request IPC Server < with the answer received straight away IPC Server >:
DEBUG:discord.ext.ipc.server:IPC Server < {'endpoint': 'get_guild_ids', 'data': {}, 'headers': {'Authorization': 'SUPER-MEGA-SECRET-KEY'}}
DEBUG:discord.ext.ipc.server:IPC Server > [65465464465464, 9879879879879879]
Now looking into Django
The bot.py from above is still running.
Under normal conditions, Django is running synchronous and you cannot come up with a async def function. So we have to switch to asynchronous with an import from asgiref.sync import async_to_sync, sync_to_async.
I'm using a #login_required decorator and according to this, I have to wrap the #login_required decorator into #sync_to_async and #async_to_sync, otherwise I'm getting this error:
ERROR: Your view return an HttpResponse object. It returned an unawaited coroutine instead. You may need to add an 'await' into your view
So what is the problem now?
The First request is working like a charme and I'm getting the result what I'm expecting. After a refresh of the webpage, I'm getting the error shown in the Second request. After refreshing a third time, the error changed another time, see: Third request.
What I'm guessing is, Django makes the request and closes the connection in an faulty way and does not reopen it after a refresh. After restarting the Django Server due to StatReloader, it's working again, once, till another restart and so on.
Do I have to use Channels/Consumers for this particular case or is there another way? If Channels is the "one and only" solution, how would I throw out a websocket request.
I'm just failing on the websocket connection to bot.py so I can send the command {'endpoint': 'get_guild_ids', 'data': {}, 'headers': {'Authorisation': 'SUPER-MEGA-SECRET-KEY'}} to interact with it.
First request
bot.py response
INFO:discord.ext.ipc.server:Initiating Multicast Server.
DEBUG:discord.ext.ipc.server:Multicast Server < {'connect': True, 'headers': {'Authorization': 'SUPER-MEGA-SECRET-KEY'}}
DEBUG:discord.ext.ipc.server:Multicast Server > {'message': 'Connection success', 'port': 8765, 'code': 200}
INFO:discord.ext.ipc.server:Initiating IPC Server.
DEBUG:discord.ext.ipc.server:IPC Server < {'endpoint': 'get_guild_ids', 'data': {}, 'headers': {'Authorization': 'SUPER-MEGA-SECRET-KEY'}}
DEBUG:discord.ext.ipc.server:IPC Server > [65465464465464, 9879879879879879]
Django response
{"list": [65465464465464, 9879879879879879]}
Second request
bot.py response
DEBUG:discord.ext.ipc.server:IPC Server < {'endpoint': 'get_guild_ids', 'data': {}, 'headers': {'Authorization': 'SUPER-MEGA-SECRET-KEY'}}
DEBUG:discord.ext.ipc.server:IPC Server > [65465464465464, 9879879879879879]
Django response:
Django Error:
TypeError at /ipc/guild_count
the JSON object must be str, bytes or bytearray, not RuntimeError
Request Method: GET
Request URL: http://127.0.0.1:8000/ipc/guild_count
Django Version: 3.2.5
Exception Type: TypeError
Exception Value:
the JSON object must be str, bytes or bytearray, not RuntimeError
Exception Location: /usr/lib/python3.7/json/__init__.py, line 341, in loads
Python Executable: /home/user/python/discord/django/mysite/.venv/bin/python
Python Version: 3.7.3
Python Path:
['/home/user/python/discord/django/mysite/mysite-website',
'/usr/lib/python37.zip',
'/usr/lib/python3.7',
'/usr/lib/python3.7/lib-dynload',
'/home/user/python/discord/django/mysite/.venv/lib/python3.7/site-packages']
Third request
bot.py response
No IPC request and response traceable
Django response
Django Error:
ConnectionResetError at /ipc/guild_count
Cannot write to closing transport
Request Method: GET
Request URL: http://127.0.0.1:8000/ipc/guild_count
Django Version: 3.2.5
Exception Type: ConnectionResetError
Exception Value:
Cannot write to closing transport
Exception Location: /home/user/python/discord/django/mysite/.venv/lib/python3.7/site-packages/aiohttp/http_websocket.py, line 598, in _send_frame
Python Executable: /home/user/python/discord/django/mysite/.venv/bin/python
Python Version: 3.7.3
Python Path:
['/home/user/python/discord/django/mysite/mysite-website',
'/usr/lib/python37.zip',
'/usr/lib/python3.7',
'/usr/lib/python3.7/lib-dynload',
'/home/user/python/discord/django/mysite/.venv/lib/python3.7/site-packages']
views.py
from django.http import HttpRequest, JsonResponse
from django.contrib.auth import authenticate, login
from django.contrib.auth.decorators import login_required
from discord.ext import ipc
from asgiref.sync import async_to_sync, sync_to_async
ipc_client = ipc.Client(secret_key = IPC_SECRET_KEY)
#sync_to_async
#login_required(login_url='/oauth2/login')
#async_to_sync
async def get_guild_count(request):
guild_count = await ipc_client.request('get_guild_ids')
print(guild_count)
return JsonResponse({"list": guild_count})
urls.py
from django.urls import path
from . import views
urlpatterns = [
path('oauth2/login', views.discord_login, name='oauth2_login'),
path('ipc/guild_count', views.get_guild_count, name='get_guild_count'),
]
Update:
Here is some more debugging. At first we see a clean request with the correct reply coming back. After the first refresh, the RuntimeError occurs, due to the old task is still pending to be ended(?).
DEBUG:asyncio:Using selector: EpollSelector
DEBUG:asyncio:Using selector: EpollSelector
INFO:discord.ext.ipc.client:Requesting IPC Server for 'get_guild_ids' with {}
INFO:discord.ext.ipc.client:Initiating WebSocket connection.
INFO:discord.ext.ipc.client:Client connected to ws://localhost:8765
DEBUG:discord.ext.ipc.client:Client > {'endpoint': 'get_guild_ids', 'data': {}, 'headers': {'Authorization': 'SUPER-MEGA-SECRET-KEY'}}
DEBUG:discord.ext.ipc.client:Client < WSMessage(type=<WSMsgType.TEXT: 1>, data='[65465464465464, 9879879879879879]', extra='')
<class 'list'> [65465464465464, 9879879879879879]
"GET /ipc/guild_count HTTP/1.1" 200 49
DEBUG:asyncio:Using selector: EpollSelector
DEBUG:asyncio:Using selector: EpollSelector
INFO:discord.ext.ipc.client:Requesting IPC Server for 'get_guild_ids' with {}
DEBUG:discord.ext.ipc.client:Client > {'endpoint': 'get_guild_ids', 'data': {}, 'headers': {'Authorization': 'SUPER-MEGA-SECRET-KEY'}}
DEBUG:discord.ext.ipc.client:Client < WSMessage(type=<WSMsgType.ERROR: 258>, data=RuntimeError('Task <Task pending coro=<AsyncToSync.main_wrap() running at /home/aim/python/discord/django/ballern/.venv/lib/python3.7/site-packages/asgiref/sync.py:292> cb=[_run_until_complete_cb() at /usr/lib/python3.7/asyncio/base_events.py:158]> got Future attached to a different loop'), extra=None)
Internal Server Error: /ipc/guild_count
Traceback (most recent call last):
...
I tried to create a celery task that returns the roles from my discord guild.: (proof of concept)
...
from asgiref.sync import async_to_sync
from celery import shared_task, Task
from discord.http import HTTPClient
class ChatTask(Task):
_client = None
#property
def client(self) -> HTTPClient:
if self._client is None:
client = HTTPClient()
result = async_to_sync(
lambda: client.static_login(settings.BOT_TOKEN, bot=True)
)()
self._client = client
return self._client
#shared_task(base=ChatTask, bind=True)
def get_roles(self):
# Still testing!
client: HTTPClient = self.client
print(client) # Seems to be fine: `<discord.http.HTTPClient object at 0x7fcc6728dbb0>`
roles = async_to_sync(lambda: async_get_roles(client))() # ERROR!: `RuntimeError('Event loop is closed')`
print(roles)
return roles
However I keep getting RuntimeError('Event loop is closed').
Comment: I do know that celery v5 will support async/await. But I do not want to wait until the release at the end of the year. ;)
How can I fix this code to get rid of the error and actually get see the roles printed out to the console?
I'm currently trying to create a backend server to communicate with some clients with a websocket. The clients makes some request to the backend and the backend responds directly to the client through a consumer.
In addition, I've got an API that needs to send some requests to the client. It has to go through the opened socket of the consumer. I'm using Django Rest Framework for the API. So I've got 2 apps for now. One for the consumer and one for the API. I want to know if it's the correct way or not.
This is actually the code I'm thinking about right now:
# mybackendapp/consumers.py
class MyConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.client_id = self.scope['url_route']['kwargs']['client_id']
# This line I don't get it very well. It comes from:
# [channels doc: single channels][1]
# I don't know if I should create the Clients myself or if it's
# created automatically
Clients.objects.create(channel_name=self.channel_name,
self.client_id)
self.accept()
async def disconnect(self):
Clients.objects.filter(channel_name=self.channel_name).delete()
async def receive(self, text_data):
self.recv_data = json.loads(text_data)
if self.recv_data[0] == CLIENT_REQUEST:
self.handler = ClientRequestHandler(self.client_id,
self.recv_data)
await self.handler.run()
self.sent_data = self.handler.response
self.send(self.sent_data)
elif self.recv_data[0] == CLIENT_RESPONSE:
self.handler = ClientResponseHandler(self.client_id,
self.recv_data)
channel_layer = get_channel_layer()
# Here I'm not sure but I may have several API requests so
# several row with the same client_id.
# I welcome info on how to deal with that.
api_channel_name = self.another_handler.ext_channel_name
channel_layer.send(api_channel_name, {
"text_data": self.handler.response,
})
async def message_from_api(self, event):
self.api_channel_name = event['channel_name_answer']
# this line is for hiding the fact that I'm only manipulating data
# to send it to a correct format to the socket
self.another_handler = ExternalDataHandler(event['json_data'])
query_to_client = another_handler.get_formatted_query()
self.send(query_to_client)
In receive, this consumer handles differently the messages from the client depending if it's initiated by the client or the rest API. You can see that with CLIENT_REQUEST and CLIENT_RESPONSE constants.
Now from the API view:
# myapi/views.py
from channels.layers import get_channel_layer
def my_api_view(request, client_id):
channel_layer = get_channel_layer()
if request.method == 'POST':
ext_request_data_json = request.data
client_channel_name = Clients.objects.get(
client_id=client_id).channel_name
# I don't know what type is listen_channel_name. I assume it's str
listen_channel_name = async_to_sync(channels_layers.new_channel)()
async_to_sync(channel_layer.send)(
client_channel_name, {
'type': 'message.from.api',
'json_data': ext_request_data_json,
'channel_name_answer': listen_channel_name
}
)
received_msg = channel_layer.receive(listen_channel_name)
I believe that this code should work. I want to know if it's the correct way to do it.
See djangochannelsrestframework as a possible alternative solution.
Django Channels Rest Framework provides a DRF like interface for building channels-v3 websocket consumers.
I 'm trying to make use of the channels project (http://channels.readthedocs.org/en/latest/index.html) on django.
While on the docs there is a good tutorial for building a Group-based websocket application(chat), I couldn't find something related to a simple push mechanism that will be client specific (so no need to use Group)
Let's say I want to build a feed aggregator with various news providers and when a user visits the homepage and waits for all the feeds to get parsed, I want to send him informational messages about which one is being parsed by the server, while he waits.
What I got now is:
consumers.py
from channels import Group, Channel
from .views import sort_articles_by_date
from .soup import ProviderParser
from .models import Provider
# Connected to websocket.connect and websocket.keepalive
def ws_add(message):
Group("news_providers_loading").add(message.reply_channel)
def ws_message(message):
providers = Provider.objects.all()
articles = []
for provider in providers:
Group("news_providers_loading").send({'content': str(provider)})
parser = ProviderParser(provider)
articles.extend(parser.parse_articles())
sort_articles_by_date(articles)
# Connected to websocket.disconnect
def ws_disconnect(message):
Group("news_providers_loading").discard(message.reply_channel)
routing.py
channel_routing = {
"websocket.connect": "news_providers.consumers.ws_add",
"websocket.keepalive": "news_providers.consumers.ws_add",
"websocket.receive": "news_providers.consumers.ws_message",
"websocket.disconnect": "news_providers.consumers.ws_disconnect",
}
Though it works ok, I can't help it but feel that's a bit overkill(?)
Is there a way to just make use of the Channel constructor, instead of Group?
Thanks :)
Update:
channels version = 0.9
channels are 0.9 now so some changes are required for the client to receive the message from the server:
class Content:
def __init__(self, reply_channel):
self.reply_channel = reply_channel
def send(self, json):
self.reply_channel.send({
'reply_channel': str(self.reply_channel),
'text': dumps(json)
})
def ws_message(message):
content = Content(message.reply_channel)
content.send({'hello': 'world'})
routing.py stays the same...
channels version < 0.9
Bah, it was a bit tricky but found it.
You have to use the message's reply_channel property.
So this:
Group("news_providers_loading").send({'content': str(provider)})
turns into this:
Channel(message.reply_channel).send({'content': str(provider)})
What I got now is:
from channels import Channel
from .soup import ProviderParser, sort_articles_by_date
from .models import Provider
from django.template.loader import render_to_string
from json import dumps
class Content:
def __init__(self, reply_channel):
self.reply_channel = reply_channel
def send(self, json):
Channel(self.reply_channel).send({'content': dumps(json)})
def ws_message(message):
providers = Provider.objects.all()
content = Content(message.reply_channel)
content.send({'providers_length': len(providers)})
articles = []
for provider in providers:
content.send({'provider': str(provider)})
parser = ProviderParser(provider)
articles.extend(parser.parse_articles())
sort_articles_by_date(articles)
html = render_to_string('news_providers/article.html', {'articles': articles})
content.send({'html': html})
routing.py
channel_routing = {
"websocket.receive": "news_providers.consumers.ws_message",
}
Seems lighter, though you might want to keep connect, keepalive and disconnect methods (as simple foo methods) -not entirely sure about that-!
# connect, keepalive and disconnect
def ws_foo(message):
pass
routing.py
channel_routing = {
"websocket.connect": "news_providers.consumers.ws_foo",
"websocket.keepalive": "news_providers.consumers.ws_foo",
"websocket.receive": "news_providers.consumers.ws_message",
"websocket.disconnect": "news_providers.consumers.ws_foo",
}