我目前正在discord-ext-ipc
用作 discord.py 的进程间通信 (IPC) 扩展,并将其与quart-discord
. Quart 是 Flask 的 asyncio 重新实现,可以原生地提供 WebSockets 和其他异步的东西。我可以很容易地将 Web 前端用作仪表板来与公会中的不和谐用户进行交互。做一些有趣的事情,比如在仪表板上使用不和谐角色模型作为权限模型。
最近我偶然发现了 django,并迷上了这个强大的框架。作为一个学习项目,我想将当前的工作项目移植到 django 中,但我偶然发现了一些僵局。
首先是使用discord-ext-ipc
and的工作示例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_async
and@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):
...