prompt: We are currently solving the following issue within our repository. Here is the issue text:
  --- BEGIN ISSUE ---
  Race condition in wait_for_deletion between a manual delete and reacting
  Sentry Issue: [BOT-86](https://sentry.io/organizations/python-discord/issues/1861537034/?referrer=github_integration)
  
  The message may be deleted before `wait_for_deletion` has a chance to add a reaction.
  
  ```
  NotFound: 404 Not Found (error code: 10008): Unknown Message
    File "bot/utils/messages.py", line 38, in wait_for_deletion
      await message.add_reaction(emoji)
    File "discord/message.py", line 953, in add_reaction
      await self._state.http.add_reaction(self.channel.id, self.id, emoji)
    File "discord/http.py", line 243, in request
      raise NotFound(r, data)
  
  Task exception was never retrieved
  future: <Task finished name='Task-333258' coro=<wait_for_deletion() done, defined at /bot/bot/utils/messages.py:19> exception=NotFound('404 Not Found (error code: 10008): Unknown Message')>
  ```
  
  --- END ISSUE ---
  
  Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
  --- BEGIN FILES ---
  Path: `bot/utils/messages.py`
  Content:
  ```
  1 import asyncio
  2 import contextlib
  3 import logging
  4 import random
  5 import re
  6 from io import BytesIO
  7 from typing import List, Optional, Sequence, Union
  8 
  9 import discord
  10 from discord.errors import HTTPException
  11 from discord.ext.commands import Context
  12 
  13 from bot.constants import Emojis, NEGATIVE_REPLIES
  14 
  15 log = logging.getLogger(__name__)
  16 
  17 
  18 async def wait_for_deletion(
  19     message: discord.Message,
  20     user_ids: Sequence[discord.abc.Snowflake],
  21     client: discord.Client,
  22     deletion_emojis: Sequence[str] = (Emojis.trashcan,),
  23     timeout: float = 60 * 5,
  24     attach_emojis: bool = True,
  25 ) -> None:
  26     """
  27     Wait for up to `timeout` seconds for a reaction by any of the specified `user_ids` to delete the message.
  28 
  29     An `attach_emojis` bool may be specified to determine whether to attach the given
  30     `deletion_emojis` to the message in the given `context`.
  31     """
  32     if message.guild is None:
  33         raise ValueError("Message must be sent on a guild")
  34 
  35     if attach_emojis:
  36         for emoji in deletion_emojis:
  37             await message.add_reaction(emoji)
  38 
  39     def check(reaction: discord.Reaction, user: discord.Member) -> bool:
  40         """Check that the deletion emoji is reacted by the appropriate user."""
  41         return (
  42             reaction.message.id == message.id
  43             and str(reaction.emoji) in deletion_emojis
  44             and user.id in user_ids
  45         )
  46 
  47     with contextlib.suppress(asyncio.TimeoutError):
  48         await client.wait_for('reaction_add', check=check, timeout=timeout)
  49         await message.delete()
  50 
  51 
  52 async def send_attachments(
  53     message: discord.Message,
  54     destination: Union[discord.TextChannel, discord.Webhook],
  55     link_large: bool = True
  56 ) -> List[str]:
  57     """
  58     Re-upload the message's attachments to the destination and return a list of their new URLs.
  59 
  60     Each attachment is sent as a separate message to more easily comply with the request/file size
  61     limit. If link_large is True, attachments which are too large are instead grouped into a single
  62     embed which links to them.
  63     """
  64     large = []
  65     urls = []
  66     for attachment in message.attachments:
  67         failure_msg = (
  68             f"Failed to re-upload attachment {attachment.filename} from message {message.id}"
  69         )
  70 
  71         try:
  72             # Allow 512 bytes of leeway for the rest of the request.
  73             # This should avoid most files that are too large,
  74             # but some may get through hence the try-catch.
  75             if attachment.size <= destination.guild.filesize_limit - 512:
  76                 with BytesIO() as file:
  77                     await attachment.save(file, use_cached=True)
  78                     attachment_file = discord.File(file, filename=attachment.filename)
  79 
  80                     if isinstance(destination, discord.TextChannel):
  81                         msg = await destination.send(file=attachment_file)
  82                         urls.append(msg.attachments[0].url)
  83                     else:
  84                         await destination.send(
  85                             file=attachment_file,
  86                             username=sub_clyde(message.author.display_name),
  87                             avatar_url=message.author.avatar_url
  88                         )
  89             elif link_large:
  90                 large.append(attachment)
  91             else:
  92                 log.info(f"{failure_msg} because it's too large.")
  93         except HTTPException as e:
  94             if link_large and e.status == 413:
  95                 large.append(attachment)
  96             else:
  97                 log.warning(f"{failure_msg} with status {e.status}.", exc_info=e)
  98 
  99     if link_large and large:
  100         desc = "\n".join(f"[{attachment.filename}]({attachment.url})" for attachment in large)
  101         embed = discord.Embed(description=desc)
  102         embed.set_footer(text="Attachments exceed upload size limit.")
  103 
  104         if isinstance(destination, discord.TextChannel):
  105             await destination.send(embed=embed)
  106         else:
  107             await destination.send(
  108                 embed=embed,
  109                 username=sub_clyde(message.author.display_name),
  110                 avatar_url=message.author.avatar_url
  111             )
  112 
  113     return urls
  114 
  115 
  116 def sub_clyde(username: Optional[str]) -> Optional[str]:
  117     """
  118     Replace "e"/"E" in any "clyde" in `username` with a Cyrillic "е"/"E" and return the new string.
  119 
  120     Discord disallows "clyde" anywhere in the username for webhooks. It will return a 400.
  121     Return None only if `username` is None.
  122     """
  123     def replace_e(match: re.Match) -> str:
  124         char = "е" if match[2] == "e" else "Е"
  125         return match[1] + char
  126 
  127     if username:
  128         return re.sub(r"(clyd)(e)", replace_e, username, flags=re.I)
  129     else:
  130         return username  # Empty string or None
  131 
  132 
  133 async def send_denial(ctx: Context, reason: str) -> None:
  134     """Send an embed denying the user with the given reason."""
  135     embed = discord.Embed()
  136     embed.colour = discord.Colour.red()
  137     embed.title = random.choice(NEGATIVE_REPLIES)
  138     embed.description = reason
  139 
  140     await ctx.send(embed=embed)
  141 
  142 
  143 def format_user(user: discord.abc.User) -> str:
  144     """Return a string for `user` which has their mention and ID."""
  145     return f"{user.mention} (`{user.id}`)"
  146
  ```
  --- END FILES ---
  
  Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.
  
  Here is an example:
  
  ```diff
  diff --git a/examples/server_async.py b/examples/server_async.py
  --- a/examples/server_async.py
  +++ b/examples/server_async.py
  @@ -313,4 +313,4 @@
   
   
   if __name__ == "__main__":
  -    asyncio.run(run_async_server("."), debug=True)
  +    asyncio.run(run_async_server(), debug=True)
  diff --git a/examples/server_sync.py b/examples/server_sync.py
  --- a/examples/server_sync.py
  +++ b/examples/server_sync.py
  @@ -313,5 +313,5 @@
   
   
   if __name__ == "__main__":
  -    server = run_sync_server(".")
  +    server = run_sync_server()
       server.shutdown()
  
  ```
  
golden_diff: diff --git a/bot/utils/messages.py b/bot/utils/messages.py
  --- a/bot/utils/messages.py
  +++ b/bot/utils/messages.py
  @@ -34,7 +34,11 @@
   
       if attach_emojis:
           for emoji in deletion_emojis:
  -            await message.add_reaction(emoji)
  +            try:
  +                await message.add_reaction(emoji)
  +            except discord.NotFound:
  +                log.trace(f"Aborting wait_for_deletion: message {message.id} deleted prematurely.")
  +                return
   
       def check(reaction: discord.Reaction, user: discord.Member) -> bool:
           """Check that the deletion emoji is reacted by the appropriate user."""
  
verification_info: {"golden_diff": "diff --git a/bot/utils/messages.py b/bot/utils/messages.py\n--- a/bot/utils/messages.py\n+++ b/bot/utils/messages.py\n@@ -34,7 +34,11 @@\n \n     if attach_emojis:\n         for emoji in deletion_emojis:\n-            await message.add_reaction(emoji)\n+            try:\n+                await message.add_reaction(emoji)\n+            except discord.NotFound:\n+                log.trace(f\"Aborting wait_for_deletion: message {message.id} deleted prematurely.\")\n+                return\n \n     def check(reaction: discord.Reaction, user: discord.Member) -> bool:\n         \"\"\"Check that the deletion emoji is reacted by the appropriate user.\"\"\"\n", "issue": "Race condition in wait_for_deletion between a manual delete and reacting\nSentry Issue: [BOT-86](https://sentry.io/organizations/python-discord/issues/1861537034/?referrer=github_integration)\r\n\r\nThe message may be deleted before `wait_for_deletion` has a chance to add a reaction.\r\n\r\n```\r\nNotFound: 404 Not Found (error code: 10008): Unknown Message\r\n  File \"bot/utils/messages.py\", line 38, in wait_for_deletion\r\n    await message.add_reaction(emoji)\r\n  File \"discord/message.py\", line 953, in add_reaction\r\n    await self._state.http.add_reaction(self.channel.id, self.id, emoji)\r\n  File \"discord/http.py\", line 243, in request\r\n    raise NotFound(r, data)\r\n\r\nTask exception was never retrieved\r\nfuture: <Task finished name='Task-333258' coro=<wait_for_deletion() done, defined at /bot/bot/utils/messages.py:19> exception=NotFound('404 Not Found (error code: 10008): Unknown Message')>\r\n```\n", "before_files": [{"content": "import asyncio\nimport contextlib\nimport logging\nimport random\nimport re\nfrom io import BytesIO\nfrom typing import List, Optional, Sequence, Union\n\nimport discord\nfrom discord.errors import HTTPException\nfrom discord.ext.commands import Context\n\nfrom bot.constants import Emojis, NEGATIVE_REPLIES\n\nlog = logging.getLogger(__name__)\n\n\nasync def wait_for_deletion(\n    message: discord.Message,\n    user_ids: Sequence[discord.abc.Snowflake],\n    client: discord.Client,\n    deletion_emojis: Sequence[str] = (Emojis.trashcan,),\n    timeout: float = 60 * 5,\n    attach_emojis: bool = True,\n) -> None:\n    \"\"\"\n    Wait for up to `timeout` seconds for a reaction by any of the specified `user_ids` to delete the message.\n\n    An `attach_emojis` bool may be specified to determine whether to attach the given\n    `deletion_emojis` to the message in the given `context`.\n    \"\"\"\n    if message.guild is None:\n        raise ValueError(\"Message must be sent on a guild\")\n\n    if attach_emojis:\n        for emoji in deletion_emojis:\n            await message.add_reaction(emoji)\n\n    def check(reaction: discord.Reaction, user: discord.Member) -> bool:\n        \"\"\"Check that the deletion emoji is reacted by the appropriate user.\"\"\"\n        return (\n            reaction.message.id == message.id\n            and str(reaction.emoji) in deletion_emojis\n            and user.id in user_ids\n        )\n\n    with contextlib.suppress(asyncio.TimeoutError):\n        await client.wait_for('reaction_add', check=check, timeout=timeout)\n        await message.delete()\n\n\nasync def send_attachments(\n    message: discord.Message,\n    destination: Union[discord.TextChannel, discord.Webhook],\n    link_large: bool = True\n) -> List[str]:\n    \"\"\"\n    Re-upload the message's attachments to the destination and return a list of their new URLs.\n\n    Each attachment is sent as a separate message to more easily comply with the request/file size\n    limit. If link_large is True, attachments which are too large are instead grouped into a single\n    embed which links to them.\n    \"\"\"\n    large = []\n    urls = []\n    for attachment in message.attachments:\n        failure_msg = (\n            f\"Failed to re-upload attachment {attachment.filename} from message {message.id}\"\n        )\n\n        try:\n            # Allow 512 bytes of leeway for the rest of the request.\n            # This should avoid most files that are too large,\n            # but some may get through hence the try-catch.\n            if attachment.size <= destination.guild.filesize_limit - 512:\n                with BytesIO() as file:\n                    await attachment.save(file, use_cached=True)\n                    attachment_file = discord.File(file, filename=attachment.filename)\n\n                    if isinstance(destination, discord.TextChannel):\n                        msg = await destination.send(file=attachment_file)\n                        urls.append(msg.attachments[0].url)\n                    else:\n                        await destination.send(\n                            file=attachment_file,\n                            username=sub_clyde(message.author.display_name),\n                            avatar_url=message.author.avatar_url\n                        )\n            elif link_large:\n                large.append(attachment)\n            else:\n                log.info(f\"{failure_msg} because it's too large.\")\n        except HTTPException as e:\n            if link_large and e.status == 413:\n                large.append(attachment)\n            else:\n                log.warning(f\"{failure_msg} with status {e.status}.\", exc_info=e)\n\n    if link_large and large:\n        desc = \"\\n\".join(f\"[{attachment.filename}]({attachment.url})\" for attachment in large)\n        embed = discord.Embed(description=desc)\n        embed.set_footer(text=\"Attachments exceed upload size limit.\")\n\n        if isinstance(destination, discord.TextChannel):\n            await destination.send(embed=embed)\n        else:\n            await destination.send(\n                embed=embed,\n                username=sub_clyde(message.author.display_name),\n                avatar_url=message.author.avatar_url\n            )\n\n    return urls\n\n\ndef sub_clyde(username: Optional[str]) -> Optional[str]:\n    \"\"\"\n    Replace \"e\"/\"E\" in any \"clyde\" in `username` with a Cyrillic \"\u0435\"/\"E\" and return the new string.\n\n    Discord disallows \"clyde\" anywhere in the username for webhooks. It will return a 400.\n    Return None only if `username` is None.\n    \"\"\"\n    def replace_e(match: re.Match) -> str:\n        char = \"\u0435\" if match[2] == \"e\" else \"\u0415\"\n        return match[1] + char\n\n    if username:\n        return re.sub(r\"(clyd)(e)\", replace_e, username, flags=re.I)\n    else:\n        return username  # Empty string or None\n\n\nasync def send_denial(ctx: Context, reason: str) -> None:\n    \"\"\"Send an embed denying the user with the given reason.\"\"\"\n    embed = discord.Embed()\n    embed.colour = discord.Colour.red()\n    embed.title = random.choice(NEGATIVE_REPLIES)\n    embed.description = reason\n\n    await ctx.send(embed=embed)\n\n\ndef format_user(user: discord.abc.User) -> str:\n    \"\"\"Return a string for `user` which has their mention and ID.\"\"\"\n    return f\"{user.mention} (`{user.id}`)\"\n", "path": "bot/utils/messages.py"}], "after_files": [{"content": "import asyncio\nimport contextlib\nimport logging\nimport random\nimport re\nfrom io import BytesIO\nfrom typing import List, Optional, Sequence, Union\n\nimport discord\nfrom discord.errors import HTTPException\nfrom discord.ext.commands import Context\n\nfrom bot.constants import Emojis, NEGATIVE_REPLIES\n\nlog = logging.getLogger(__name__)\n\n\nasync def wait_for_deletion(\n    message: discord.Message,\n    user_ids: Sequence[discord.abc.Snowflake],\n    client: discord.Client,\n    deletion_emojis: Sequence[str] = (Emojis.trashcan,),\n    timeout: float = 60 * 5,\n    attach_emojis: bool = True,\n) -> None:\n    \"\"\"\n    Wait for up to `timeout` seconds for a reaction by any of the specified `user_ids` to delete the message.\n\n    An `attach_emojis` bool may be specified to determine whether to attach the given\n    `deletion_emojis` to the message in the given `context`.\n    \"\"\"\n    if message.guild is None:\n        raise ValueError(\"Message must be sent on a guild\")\n\n    if attach_emojis:\n        for emoji in deletion_emojis:\n            try:\n                await message.add_reaction(emoji)\n            except discord.NotFound:\n                log.trace(f\"Aborting wait_for_deletion: message {message.id} deleted prematurely.\")\n                return\n\n    def check(reaction: discord.Reaction, user: discord.Member) -> bool:\n        \"\"\"Check that the deletion emoji is reacted by the appropriate user.\"\"\"\n        return (\n            reaction.message.id == message.id\n            and str(reaction.emoji) in deletion_emojis\n            and user.id in user_ids\n        )\n\n    with contextlib.suppress(asyncio.TimeoutError):\n        await client.wait_for('reaction_add', check=check, timeout=timeout)\n        await message.delete()\n\n\nasync def send_attachments(\n    message: discord.Message,\n    destination: Union[discord.TextChannel, discord.Webhook],\n    link_large: bool = True\n) -> List[str]:\n    \"\"\"\n    Re-upload the message's attachments to the destination and return a list of their new URLs.\n\n    Each attachment is sent as a separate message to more easily comply with the request/file size\n    limit. If link_large is True, attachments which are too large are instead grouped into a single\n    embed which links to them.\n    \"\"\"\n    large = []\n    urls = []\n    for attachment in message.attachments:\n        failure_msg = (\n            f\"Failed to re-upload attachment {attachment.filename} from message {message.id}\"\n        )\n\n        try:\n            # Allow 512 bytes of leeway for the rest of the request.\n            # This should avoid most files that are too large,\n            # but some may get through hence the try-catch.\n            if attachment.size <= destination.guild.filesize_limit - 512:\n                with BytesIO() as file:\n                    await attachment.save(file, use_cached=True)\n                    attachment_file = discord.File(file, filename=attachment.filename)\n\n                    if isinstance(destination, discord.TextChannel):\n                        msg = await destination.send(file=attachment_file)\n                        urls.append(msg.attachments[0].url)\n                    else:\n                        await destination.send(\n                            file=attachment_file,\n                            username=sub_clyde(message.author.display_name),\n                            avatar_url=message.author.avatar_url\n                        )\n            elif link_large:\n                large.append(attachment)\n            else:\n                log.info(f\"{failure_msg} because it's too large.\")\n        except HTTPException as e:\n            if link_large and e.status == 413:\n                large.append(attachment)\n            else:\n                log.warning(f\"{failure_msg} with status {e.status}.\", exc_info=e)\n\n    if link_large and large:\n        desc = \"\\n\".join(f\"[{attachment.filename}]({attachment.url})\" for attachment in large)\n        embed = discord.Embed(description=desc)\n        embed.set_footer(text=\"Attachments exceed upload size limit.\")\n\n        if isinstance(destination, discord.TextChannel):\n            await destination.send(embed=embed)\n        else:\n            await destination.send(\n                embed=embed,\n                username=sub_clyde(message.author.display_name),\n                avatar_url=message.author.avatar_url\n            )\n\n    return urls\n\n\ndef sub_clyde(username: Optional[str]) -> Optional[str]:\n    \"\"\"\n    Replace \"e\"/\"E\" in any \"clyde\" in `username` with a Cyrillic \"\u0435\"/\"E\" and return the new string.\n\n    Discord disallows \"clyde\" anywhere in the username for webhooks. It will return a 400.\n    Return None only if `username` is None.\n    \"\"\"\n    def replace_e(match: re.Match) -> str:\n        char = \"\u0435\" if match[2] == \"e\" else \"\u0415\"\n        return match[1] + char\n\n    if username:\n        return re.sub(r\"(clyd)(e)\", replace_e, username, flags=re.I)\n    else:\n        return username  # Empty string or None\n\n\nasync def send_denial(ctx: Context, reason: str) -> None:\n    \"\"\"Send an embed denying the user with the given reason.\"\"\"\n    embed = discord.Embed()\n    embed.colour = discord.Colour.red()\n    embed.title = random.choice(NEGATIVE_REPLIES)\n    embed.description = reason\n\n    await ctx.send(embed=embed)\n\n\ndef format_user(user: discord.abc.User) -> str:\n    \"\"\"Return a string for `user` which has their mention and ID.\"\"\"\n    return f\"{user.mention} (`{user.id}`)\"\n", "path": "bot/utils/messages.py"}]}