0

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):
...
Operator23
  • 29
  • 7

0 Answers0