Search code examples
pythondiscord.pypython-asyncioquart

Discord.py bot + Quart: Trying to connect to a voice channel always gives a "task got future attached to a different loop"


i've been trying to create a discord bot that can receive commands through a web interface. I'm using discord.py as a Discord API wrapper and Quart as a REST framework, because i need to handle asynchronous tasks and Flask doesn't support them.

Right now i have two files:

app.py

import discord

bot = discord.Client(intents=discord.Intents.all())

...

async def play_audio(audio_name, voiceChannel):
    vc = await voiceChannel.connect()
    print("test")
    vc.play(discord.FFmpegPCMAudio(source="audio\{}".format(audio_name), executable=FFMPEG_PATH))
    while vc.is_playing():
        time.sleep(.1)
    await vc.disconnect()

async def get_online_voice_members():
    guild = bot.get_guild(NMC_GUILD_ID)
    online_voice_users = {}
    for voiceChannel in guild.voice_channels:
        for user in voiceChannel.members:
            online_voice_users[user] = voiceChannel
    return online_voice_users

...

api.py

import asyncio
from quart import Quart
import app as discord

QUART_APP = Quart(__name__)

@QUART_APP.before_serving
async def before_serving():
    loop = asyncio.get_event_loop()
    await discord.bot.login("MY BOT TOKEN")
    loop.create_task(discord.bot.connect())

...

@QUART_APP.route("/online_list", methods=["GET"])
async def get_online_members():
    resp = {}
    members = await discord.get_online_voice_members()
    for user in members.keys():
        resp[user.id] = {"nick" : user.nick, "channel" : members[user].id}
    return resp

@QUART_APP.route("/goodnight", methods=["GET"])
async def send_goodnight():
    members = await discord.get_online_voice_members()
    for user in members.keys():
        if user.id == 12345:
            await discord.play_audio("goodnight.mp3", members[user])
            break
    return {"response":"OK"}

When i make a GET request on the endpoint /online_list everything works fine, but when i make a request on /goodnight, the code successfully runs until reaching the instruction await discord.play_audio("goodnight.mp3, members[user]), which receives the correct parameters, but it always raises the following exception:

Traceback (most recent call last):
  File "G:\Gunther\venv\lib\site-packages\quart\app.py", line 1814, in handle_request
    return await self.full_dispatch_request(request_context)
  File "G:\Gunther\venv\lib\site-packages\quart\app.py", line 1836, in full_dispatch_request
    result = await self.handle_user_exception(error)
  File "G:\Gunther\venv\lib\site-packages\quart\app.py", line 1076, in handle_user_exception
    raise error
  File "G:\Gunther\venv\lib\site-packages\quart\app.py", line 1834, in full_dispatch_request
    result = await self.dispatch_request(request_context)
  File "G:\Gunther\venv\lib\site-packages\quart\app.py", line 1882, in dispatch_request
    return await handler(**request_.view_args)
  File "G:/Dati HDD F/GitHub Projects/Gunther/api.py", line 59, in send_buonanotte
    await discord.play_audio("goodnight.mp3", members[user])
  File "G:\Gunther\app.py", line 55, in play_audio
    vc = await voiceChannel.connect()
  File "G:\Gunther\venv\lib\site-packages\discord\abc.py", line 1122, in connect
    await voice.connect(timeout=timeout, reconnect=reconnect)
  File "G:\Gunther\venv\lib\site-packages\discord\voice_client.py", line 352, in connect
    self.ws = await self.connect_websocket()
  File "G:\Gunther\venv\lib\site-packages\discord\voice_client.py", line 323, in connect_websocket
    await ws.poll_event()
  File "G:\Gunther\venv\lib\site-packages\discord\gateway.py", line 893, in poll_event
    await self.received_message(json.loads(msg.data))
  File "G:\Gunther\venv\lib\site-packages\discord\gateway.py", line 825, in received_message
    await self.initial_connection(data)
  File "G:\Gunther\venv\lib\site-packages\discord\gateway.py", line 849, in initial_connection
    recv = await self.loop.sock_recv(state.socket, 70)
  File "C:\Users\Kyles\AppData\Local\Programs\Python\Python38\lib\asyncio\proactor_events.py", line 693, in sock_recv
    return await self._proactor.recv(sock, n)
RuntimeError: Task <Task pending name='Task-27' coro=<ASGIHTTPConnection.handle_request() running at G:\Gunther\venv\lib\site-packages\quart\asgi.py:70> cb=[_wait.<locals>._on_completion() at C:\Users\Kyles\AppData\Local\Programs\Python\Python38\lib\asyncio\tasks.py:507]> got Future <_OverlappedFuture pending overlapped=<pending, 0x199c31f0ca0>> attached to a different loop

I guess i'm not understanding correctly how the asyncio library works, as it seems to me that no matter what i try, the line vc = await voiceChannel.connect() in app.py always ends up running on a different loop than the main one. Is there something i'm missing?


Solution

  • This is because you initiate the discord client on import (line 3 of app.py). Doing so means that it will use the event loop available at the time of import. However, Quart (and Hypercorn unless told not too) will close the existing loop and create a new one as it starts. It is for this reason that I recommend utilizing the startup functionality for initialization.

    To solve this I would wrap your discord commands in a class and initialize it in a startup function. Note that I like storing instances on the app itself (so they can be accessed via the current_app proxy), this is not necessary though. For example,

    app.py

    class DiscordClient:
        def __init__(self):
            self.bot = discord.Client(intents=discord.Intents.all())
            ...
    
        async def get_online_voice_members(self):
            guild = self.bot.get_guild(NMC_GUILD_ID)
            ...
    

    api.py

    @QUART_APP.before_serving
    async def before_serving():
        loop = asyncio.get_event_loop()
        QUART_APP.discord_client = DiscordClient()
        await QUART_APP.discord_client.bot.login("MY BOT TOKEN")
        loop.create_task(QUART_APP.discord_client.bot.connect())
    
    @QUART_APP.route("/online_list", methods=["GET"])
    async def get_online_members():
        resp = {}
        members = await QUART_APP.discord_client.get_online_voice_members()
        ...