Rapptz / discord.py

An API wrapper for Discord written in Python.
http://discordpy.rtfd.org/en/latest
MIT License
14.84k stars 3.76k forks source link

TextChannel.edit takes minutes to return and asyncio raises Exception after "Pre-emptively rate limiting" #9073

Closed ca10s closed 1 year ago

ca10s commented 1 year ago

Summary

Function returns only after 10 minutes and timeouts interaction after calling edit() on a TextChannel twice in a short period of time

Reproduction Steps

The Code snippet below creates a simple command to change the name of a channel.

Minimal Reproducible Code

import logging
import sys

import discord
from discord import app_commands, Interaction, TextChannel
from discord.ui import Modal, TextInput, View

LOG = logging.getLogger(__name__)

class ShowCaseModal(Modal):

    ch_name: TextInput[View] = TextInput(label="new channel name", required=True)

    def __init__(self) -> None:
        super().__init__(title="change channel name")

    async def on_submit(self, interaction: Interaction) -> None:
        if interaction.channel is not None and isinstance(interaction.channel, TextChannel):
            LOG.debug("before edit")
            await interaction.channel.edit(name=self.ch_name.value)
            LOG.debug("after edit")
        await interaction.response.defer()

    async def on_error(self, interaction: Interaction, error: Exception) -> None:
        LOG.error("Error!\n %s", error)
        await interaction.response.defer()

@app_commands.command(description="channel edit test")
async def showcase(interaction: Interaction) -> None:
    LOG.info("send showcase modal")
    await interaction.response.send_modal(ShowCaseModal())

if __name__ == "__main__":
    logging.basicConfig(
        format="%(asctime)s %(levelname)s    %(name)s - %(message)s", level=logging.DEBUG, stream=sys.stdout
    )

    DISCORD_TOKEN = "TOKEN"
    DISCORD_GUILD = discord.Object(id=<guildid>)

    class MyClient(discord.Client):
        def __init__(self) -> None:
            intents = discord.Intents.default()
            super().__init__(intents=intents)
            self.tree = app_commands.CommandTree(self)

        async def on_ready(self) -> None:
            """Handle on_ready event."""
            if self.user is not None:
                LOG.info("Logged in as %s (ID: %d)", self.user, self.user.id)
            else:
                LOG.warn("Ready but not logged in")

        async def setup_hook(self) -> None:
            await self.tree.sync(guild=DISCORD_GUILD)

    client = MyClient()

    client.tree.add_command(showcase, guild=DISCORD_GUILD)

    client.run(DISCORD_TOKEN, log_level=logging.DEBUG)

Expected Results

The channel gets renamed again and the execution of on_submit continues right away

Actual Results

The channel gets renamed but the Modal does time out and the log only shows "after edit" only after 10 minutes.

Intents

discord.Intents.default()

System Information

Checklist

Additional Context

As far as I know the rate limit for editing channel names is 2 times within 10 minutes so the second edit shouldn't hit the rate limit. Based on that I read through all the debug output and found the following lines for the second edit:

2022-11-16 19:38:53,592 DEBUG    root - before edit
2022-11-16 19:38:53 DEBUG    discord.gateway For Shard ID None: WebSocket Event: {'t': 'CHANNEL_UPDATE' ......
......
2022-11-16 19:38:53 DEBUG    discord.http PATCH https://discord.com/api/v10/channels/<id> with {"name":"new-2"} has returned 200
2022-11-16 19:38:53,849 DEBUG    discord.http - PATCH https://discord.com/api/v10/channels/<id> with {"name":"new-2"} has returned 200
2022-11-16 19:38:53 DEBUG    discord.http A rate limit bucket (<id>) has been exhausted. Pre-emptively rate limiting...
2022-11-16 19:38:53,854 DEBUG    discord.http - A rate limit bucket (<id>) has been exhausted. Pre-emptively rate limiting...

and 10 minutes later:

2022-11-16 19:48:44,379 DEBUG    root - after edit
...
2022-11-16 19:48:44,783 ERROR    asyncio - Task exception was never retrieved
future: <Task finished name='discord-ui-modal-dispatch-036ffdac94a9835cf4c17ccf826f3cc9' coro=<Modal._scheduled_task() done, defined at /path/to/my/venv/lib/python3.8/site-packages/discord/ui/modal.py:177> exception=NotFound('404 Not Found (error code: 10062): Unknown interaction')>
Traceback (most recent call last):
  File "/path/to/my/venv/lib/python3.8/site-packages/discord/ui/modal.py", line 186, in _scheduled_task
    await self.on_submit(interaction)
  File "/media/programming/DiscordTerminBot/dndbot/interactions/events/showcase.py", line 26, in on_submit
    await interaction.response.defer()
  File "/path/to/my/venv/lib/python3.8/site-packages/discord/interactions.py", line 636, in defer
    await adapter.create_interaction_response(
  File "/path/to/my/venv/lib/python3.8/site-packages/discord/webhook/async_.py", line 218, in request
    raise NotFound(response, data)
discord.errors.NotFound: 404 Not Found (error code: 10062): Unknown interaction

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/path/to/my/venv/lib/python3.8/site-packages/discord/ui/modal.py", line 188, in _scheduled_task
    return await self.on_error(interaction, e)
  File "/media/programming/DiscordTerminBot/dndbot/interactions/events/showcase.py", line 30, in on_error
    await interaction.response.defer()
  File "/path/to/my/venv/lib/python3.8/site-packages/discord/interactions.py", line 636, in defer
    await adapter.create_interaction_response(
  File "/path/to/my/venv/lib/python3.8/site-packages/discord/webhook/async_.py", line 218, in request
    raise NotFound(response, data)
discord.errors.NotFound: 404 Not Found (error code: 10062): Unknown interaction

Having the pre-emptive rate limit renaming the channel but timing out the interaction afterwards somewhat breaks the workflow in my app.

Another question that came to my mind: Is there a reason why both the pre-emptive and "hard" rate limiting don't raise an exception but wait for the rate limit time to pass? Raising an exception right away would make it possible to do some internal cleanup in my app and/or send a message to the user like "try again later"

Rapptz commented 1 year ago

Having the pre-emptive rate limit renaming the channel but timing out the interaction afterwards somewhat breaks the workflow in my app.

Channel name and topic edits have a 2/10 minutes rate limit so it's waiting 10 minutes. The interaction token is only valid for 15 minutes so it expires. Discord does not like apps that rename channels constantly, so what you're doing is frowned down upon.

Is there a reason why both the pre-emptive and "hard" rate limiting don't raise an exception but wait for the rate limit time to pass?

Most rate limits are not long. Someone sending a message would be pretty annoyed if they ended up having an exception raised when the proper way to deal with it is just waiting for the rate limit to be over. In fact that's the way to go for a vast majority of rate limits, well over 99% of them.

Raising an exception right away would make it possible to do some internal cleanup in my app and/or send a message to the user like "try again later"

If you want to toggle the maximum timeout before raising discord.RateLimited (new in v2.0) then you can set max_ratelimit_timeout in Client.__init__ to some value you think is too long. For example 5 minutes or something.