0

我目前正在discord-ext-ipc用作 discord.py 的进程间通信 (IPC) 扩展,并将其与quart-discord. Quart 是 Fl​​ask 的 asyncio 重新实现,可以原生地提供 WebSockets 和其他异步的东西。我可以很容易地将 Web 前端用作仪表板来与公会中的不和谐用户进行交互。做一些有趣的事情,比如在仪表板上使用不和谐角色模型作为权限模型。

最近我偶然发现了 django,并迷上了这个强大的框架。作为一个学习项目,我想将当前的工作项目移植到 django 中,但我偶然发现了一些僵局。

首先是使用discord-ext-ipcand的工作示例quart-discord

僵尸软件

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)

主文件

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)

列表.html

<html>
<head>
<title></title>
</head>
<body>
    {% for guild_id in guild_ids %}
    <p>{{ guild_id }}</p>
    {% endfor %}
</body>
</html>

伸手去:http://localhost:5001/dashboard 给我这个机器人加入的公会ID列表。据我了解,调用会await ipc_client.request("get_guild_ids")发送一个 websocket 请求IPC Server <,并立即收到答案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

上面的bot.py仍在运行。在正常情况下,Django 是同步运行的,你不能想出一个async def函数。所以我们必须使用 import 切换到异步from asgiref.sync import async_to_sync, sync_to_async。我正在使用@login_required装饰器,根据this,我必须将@login_required装饰器包装到@sync_to_asyncand@async_to_sync中,否则会出现此错误:

ERROR: Your view return an HttpResponse object. It returned an unawaited coroutine instead. You may need to add an 'await' into your view

那么现在的问题是什么?这First request就像一个魅力,我得到了我所期待的结果。刷新网页后,我收到Second request. 第三次刷新后,错误又变了一次,见:Third request. 我的猜测是,Django 发出请求并以错误的方式关闭连接,并且在刷新后不会重新打开它。由于 StatReloader 重新启动 Django 服务器后,它再次工作,一次,直到再次重新启动,依此类推。

对于这种特殊情况,我是否必须使用渠道/消费者,还是有其他方法?如果 Channels 是“唯一的”解决方案,我将如何抛出 websocket 请求。我只是在 websocket 连接上失败了,bot.py所以我可以发送命令{'endpoint': 'get_guild_ids', 'data': {}, 'headers': {'Authorisation': 'SUPER-MEGA-SECRET-KEY'}}与之交互。

第一个请求

bot.py 响应

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 响应

{"list": [65465464465464, 9879879879879879]}

第二个请求

bot.py 响应

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 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']

第三次请求

bot.py 响应

No IPC request and response traceable

Django 响应

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']

视图.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})

网址.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'),
]

更新:

这里还有一些调试。首先,我们看到一个干净的请求,并返回了正确的回复。第一次刷新后,出现 RuntimeError,因为旧任务仍然等待结束(?)。

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):
...
4

0 回答 0