anon-iclr-submission/benchname-bug-localization huggingface dataset:
Repository: python-discord/bot
Issue: Duckifying doesn't work for messages inside threads.

As the title says.

An example can be found on an internal staff thread [here](https://discord.com/channels/267624335836053506/902258820561137754/902259286527311913). For those that can't view the message, it has the required amount of duck reactions but the bot didn't "duckify" it.

The issue *may* be these lines ([L168-L170](https://github.com/python-discord/bot/blob/main/bot/exts/fun/duck_pond.py#L168-L170)) but can't be sure without further investigation:
```py
        channel = discord.utils.get(self.bot.get_all_channels(), id=payload.channel_id)
        if channel is None:
            return
```

Changed files:
bot/exts/fun/duck_pond.py

diff --git a/bot/exts/fun/duck_pond.py b/bot/exts/fun/duck_pond.py
index c5165634..8a41a311 100644
--- a/bot/exts/fun/duck_pond.py
+++ b/bot/exts/fun/duck_pond.py
@@ -2,7 +2,7 @@ import asyncio
 from typing import Union
 
 import discord
-from discord import Color, Embed, Message, RawReactionActionEvent, TextChannel, errors
+from discord import Color, Embed, Message, RawReactionActionEvent, errors
 from discord.ext.commands import Cog, Context, command
 
 from bot import constants
@@ -46,17 +46,6 @@ class DuckPond(Cog):
                     return True
         return False
 
-    @staticmethod
-    def is_helper_viewable(channel: TextChannel) -> bool:
-        """Check if helpers can view a specific channel."""
-        guild = channel.guild
-        helper_role = guild.get_role(constants.Roles.helpers)
-        # check channel overwrites for both the Helper role and @everyone and
-        # return True for channels that they have permissions to view.
-        helper_overwrites = channel.overwrites_for(helper_role)
-        default_overwrites = channel.overwrites_for(guild.default_role)
-        return default_overwrites.view_channel is None or helper_overwrites.view_channel is True
-
     async def has_green_checkmark(self, message: Message) -> bool:
         """Check if the message has a green checkmark reaction."""
         for reaction in message.reactions:
@@ -165,12 +154,15 @@ class DuckPond(Cog):
         if not self._payload_has_duckpond_emoji(payload.emoji):
             return
 
-        channel = discord.utils.get(self.bot.get_all_channels(), id=payload.channel_id)
+        await self.bot.wait_until_guild_available()
+        guild = self.bot.get_guild(payload.guild_id)
+        channel = guild.get_channel_or_thread(payload.channel_id)
         if channel is None:
             return
 
         # Was the message sent in a channel Helpers can see?
-        if not self.is_helper_viewable(channel):
+        helper_role = guild.get_role(constants.Roles.helpers)
+        if not channel.permissions_for(helper_role).view_channel:
             return
 
         try:
Repository: python-discord/bot
Issue: Auto-joining threads can run into a 400

Sentry Issue: [BOT-1RE](https://sentry.io/organizations/python-discord/issues/2848940096/?referrer=github_integration)

We should be adding `HTTPException` to the `contextlib.suppress` just above the `thread.join()` call.
```
HTTPException: 400 Bad Request (error code: 50083): Thread is archived
  File "discord/http.py", line 335, in request
    raise HTTPException(response, data)
  File "discord/threads.py", line 607, in join
    await self._state.http.join_thread(self.id)
  File "bot/exts/utils/bot.py", line 32, in on_thread_join
    await thread.join()
  File "discord/client.py", line 351, in _run_event
    await coro(*args, **kwargs)

Unhandled exception in on_thread_join.
```

Changed files:
bot/exts/utils/bot.py

diff --git a/bot/exts/utils/bot.py b/bot/exts/utils/bot.py
index 78869277..8f0094bc 100644
--- a/bot/exts/utils/bot.py
+++ b/bot/exts/utils/bot.py
@@ -1,7 +1,6 @@
-from contextlib import suppress
 from typing import Optional
 
-from discord import Embed, Forbidden, TextChannel, Thread
+from discord import Embed, TextChannel
 from discord.ext.commands import Cog, Context, command, group, has_any_role
 
 from bot.bot import Bot
@@ -17,20 +16,6 @@ class BotCog(Cog, name="Bot"):
     def __init__(self, bot: Bot):
         self.bot = bot
 
-    @Cog.listener()
-    async def on_thread_join(self, thread: Thread) -> None:
-        """
-        Try to join newly created threads.
-
-        Despite the event name being misleading, this is dispatched when new threads are created.
-        """
-        if thread.me:
-            # We have already joined this thread
-            return
-
-        with suppress(Forbidden):
-            await thread.join()
-
     @group(invoke_without_command=True, name="bot", hidden=True)
     async def botinfo_group(self, ctx: Context) -> None:
         """Bot informational commands."""
Repository: python-discord/bot
Issue: Disallow editing duration of expired infractions

Currently the command assumes that an infraction is still active if a duration is being edited. It tries to cancel the previous infraction but it will fail with a warning if the infraction already expired. Relevant code can be found here:
https://github.com/python-discord/bot/blob/582ddbb1ca8bab2cb883781911f5f35962330995/bot/cogs/moderation/management.py#L130-L142

Changed files:
bot/cogs/moderation/management.py

diff --git a/bot/cogs/moderation/management.py b/bot/cogs/moderation/management.py
index 35448f68..250a2424 100644
--- a/bot/cogs/moderation/management.py
+++ b/bot/cogs/moderation/management.py
@@ -100,7 +100,12 @@ class ModManagement(commands.Cog):
         confirm_messages = []
         log_text = ""
 
-        if isinstance(duration, str):
+        if duration is not None and not old_infraction['active']:
+            if reason is None:
+                await ctx.send(":x: Cannot edit the expiration of an expired infraction.")
+                return
+            confirm_messages.append("expiry unchanged (infraction already expired)")
+        elif isinstance(duration, str):
             request_data['expires_at'] = None
             confirm_messages.append("marked as permanent")
         elif duration is not None:
Repository: python-discord/bot
Issue: Duck pond may fail to fetch reaction message due to a missing channel

Sentry Issue: [BOT-8T](https://sentry.io/organizations/python-discord/issues/1892623782/?referrer=github_integration)

Possibly a cache issue. I believe that a channel object isn't really needed. There's likely a direct way to get the message via a method in `bot.http`. After all, the actual endpoint only needs a channel ID and a message ID.

```
AttributeError: 'NoneType' object has no attribute 'fetch_message'
  File "discord/client.py", line 312, in _run_event
    await coro(*args, **kwargs)
  File "bot/cogs/duck_pond.py", line 158, in on_raw_reaction_remove
    message = await channel.fetch_message(payload.message_id)

Unhandled exception in on_raw_reaction_remove.
```

Changed files:
bot/exts/fun/duck_pond.py

diff --git a/bot/exts/fun/duck_pond.py b/bot/exts/fun/duck_pond.py
index 6c2d22b9..82084ea8 100644
--- a/bot/exts/fun/duck_pond.py
+++ b/bot/exts/fun/duck_pond.py
@@ -145,6 +145,10 @@ class DuckPond(Cog):
         amount of ducks specified in the config under duck_pond/threshold, it will
         send the message off to the duck pond.
         """
+        # Ignore other guilds and DMs.
+        if payload.guild_id != constants.Guild.id:
+            return
+
         # Was this reaction issued in a blacklisted channel?
         if payload.channel_id in constants.DuckPond.channel_blacklist:
             return
@@ -154,6 +158,9 @@ class DuckPond(Cog):
             return
 
         channel = discord.utils.get(self.bot.get_all_channels(), id=payload.channel_id)
+        if channel is None:
+            return
+
         message = await channel.fetch_message(payload.message_id)
         member = discord.utils.get(message.guild.members, id=payload.user_id)
 
@@ -175,7 +182,13 @@ class DuckPond(Cog):
     @Cog.listener()
     async def on_raw_reaction_remove(self, payload: RawReactionActionEvent) -> None:
         """Ensure that people don't remove the green checkmark from duck ponded messages."""
+        # Ignore other guilds and DMs.
+        if payload.guild_id != constants.Guild.id:
+            return
+
         channel = discord.utils.get(self.bot.get_all_channels(), id=payload.channel_id)
+        if channel is None:
+            return
 
         # Prevent the green checkmark from being removed
         if payload.emoji.name == "✅":
Repository: python-discord/bot
Issue: API lag may lead to more than 2 available help channels

`on_message` uses a lock to ensure messages sent in available channels are processed sequentially. After it acquires the lock, it checks if the channel has already been moved out of available. If it has, then it aborts further processing. The problem is that this relies on Discord moving the channel in a reasonable amount of time. If it happens slowly, then the next message that acquires the lock will still think the channel can be claimed. 

This is because only the request to move the channel is awaited. It can be sure the request has been sent, but not that Discord has processed it and moved the channel. To do that, it'd have to wait for a guild channel update event. That's just one solution. Another idea I have is to check if the channel has a claimant associated with it, since that is already tracked for other purposes.

Changed files:
bot/cogs/help_channels.py

diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py
index 1be98047..975043df 100644
--- a/bot/cogs/help_channels.py
+++ b/bot/cogs/help_channels.py
@@ -694,7 +694,7 @@ class HelpChannels(commands.Cog):
         async with self.on_message_lock:
             log.trace(f"on_message lock acquired for {message.id}.")
 
-            if not self.is_in_category(channel, constants.Categories.help_available):
+            if await self.help_channel_claimants.contains(channel.id):
                 log.debug(
                     f"Message {message.id} will not make #{channel} ({channel.id}) in-use "
                     f"because another message in the channel already triggered that."
Repository: python-discord/bot
Issue: API lag may lead to more than 2 available help channels

`on_message` uses a lock to ensure messages sent in available channels are processed sequentially. After it acquires the lock, it checks if the channel has already been moved out of available. If it has, then it aborts further processing. The problem is that this relies on Discord moving the channel in a reasonable amount of time. If it happens slowly, then the next message that acquires the lock will still think the channel can be claimed. 

This is because only the request to move the channel is awaited. It can be sure the request has been sent, but not that Discord has processed it and moved the channel. To do that, it'd have to wait for a guild channel update event. That's just one solution. Another idea I have is to check if the channel has a claimant associated with it, since that is already tracked for other purposes.

Changed files:
bot/cogs/help_channels.py

diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py
index 1be98047..2400cd54 100644
--- a/bot/cogs/help_channels.py
+++ b/bot/cogs/help_channels.py
@@ -215,9 +215,6 @@ class HelpChannels(commands.Cog):
         log.trace("close command invoked; checking if the channel is in-use.")
         if ctx.channel.category == self.in_use_category:
             if await self.dormant_check(ctx):
-
-                # Remove the claimant and the cooldown role
-                await self.help_channel_claimants.delete(ctx.channel.id)
                 await self.remove_cooldown_role(ctx.author)
 
                 # Ignore missing task when cooldown has passed but the channel still isn't dormant.
@@ -551,6 +548,7 @@ class HelpChannels(commands.Cog):
 
         A caller argument is provided for metrics.
         """
+        await self.help_channel_claimants.delete(channel.id)
         msg_id = await self.question_messages.pop(channel.id)
 
         try:
Repository: python-discord/bot
Issue: Existing offensive messages sometimes posted again

Sentry Issue: [BOT-6B](https://sentry.io/organizations/python-discord/issues/1732235421/?referrer=github_integration)

My guess is this happens when an edited message re-triggers a filter. If this is indeed the case, then the error can simply be ignored.

```
ResponseCodeError: Status: 400 Response: {'id': ['offensive message with this id already exists.']}
(1 additional frame(s) were not displayed)
...
  File "bot/cogs/filtering.py", line 143, in on_message_edit
    await self._filter_message(after, delta)
  File "bot/cogs/filtering.py", line 304, in _filter_message
    await self.bot.api_client.post('bot/offensive-messages', json=data)
  File "bot/api.py", line 127, in post
    return await self.request("POST", endpoint, raise_for_status=raise_for_status, **kwargs)
  File "bot/api.py", line 114, in request
    await self.maybe_raise_for_status(resp, raise_for_status)
  File "bot/api.py", line 104, in maybe_raise_for_status
    raise ResponseCodeError(response=response, response_json=response_json)

Unhandled exception in on_message_edit.
```

Changed files:
bot/cogs/filtering.py

diff --git a/bot/cogs/filtering.py b/bot/cogs/filtering.py
index 93cc1c65..99b659bf 100644
--- a/bot/cogs/filtering.py
+++ b/bot/cogs/filtering.py
@@ -11,6 +11,7 @@ from discord import Colour, HTTPException, Member, Message, NotFound, TextChanne
 from discord.ext.commands import Cog
 from discord.utils import escape_markdown
 
+from bot.api import ResponseCodeError
 from bot.bot import Bot
 from bot.cogs.moderation import ModLog
 from bot.constants import (
@@ -301,9 +302,16 @@ class Filtering(Cog):
                                 'delete_date': delete_date
                             }
 
-                            await self.bot.api_client.post('bot/offensive-messages', json=data)
-                            self.schedule_msg_delete(data)
-                            log.trace(f"Offensive message {msg.id} will be deleted on {delete_date}")
+                            try:
+                                await self.bot.api_client.post('bot/offensive-messages', json=data)
+                            except ResponseCodeError as e:
+                                if e.status == 400 and "already exists" in e.response_json.get("id", [""])[0]:
+                                    log.debug(f"Offensive message {msg.id} already exists.")
+                                else:
+                                    log.error(f"Offensive message {msg.id} failed to post: {e}")
+                            else:
+                                self.schedule_msg_delete(data)
+                                log.trace(f"Offensive message {msg.id} will be deleted on {delete_date}")
 
                         if is_private:
                             channel_str = "via DM"
Repository: python-discord/bot
Issue: Leaked bot token still relayed to big-brother if user is watched

If a user pastes a bot token and the user is currently being watched (big-brother or nomination) then it will relay the message to the watch channel, which is not ideal. The same thing would apply for webhook URLs.
Ideally it would either send nothing, or possibly a message saying that it was removed, to the watch channels.


Changed files:
bot/cogs/watchchannels/watchchannel.py, bot/cogs/bot.py

diff --git a/bot/cogs/bot.py b/bot/cogs/bot.py
index 70ef407d..ddd1cef8 100644
--- a/bot/cogs/bot.py
+++ b/bot/cogs/bot.py
@@ -9,6 +9,7 @@ from discord.ext.commands import Cog, Context, command, group
 
 from bot.bot import Bot
 from bot.cogs.token_remover import TokenRemover
+from bot.cogs.webhook_remover import WEBHOOK_URL_RE
 from bot.constants import Categories, Channels, DEBUG_MODE, Guild, MODERATION_ROLES, Roles, URLs
 from bot.decorators import with_role
 from bot.utils.messages import wait_for_deletion
@@ -240,6 +241,7 @@ class BotCog(Cog, name="Bot"):
             and not msg.author.bot
             and len(msg.content.splitlines()) > 3
             and not TokenRemover.find_token_in_message(msg)
+            and not WEBHOOK_URL_RE.search(msg.content)
         )
 
         if parse_codeblock:  # no token in the msg
diff --git a/bot/cogs/watchchannels/watchchannel.py b/bot/cogs/watchchannels/watchchannel.py
index 04407735..a58b604c 100644
--- a/bot/cogs/watchchannels/watchchannel.py
+++ b/bot/cogs/watchchannels/watchchannel.py
@@ -15,6 +15,8 @@ from discord.ext.commands import Cog, Context
 from bot.api import ResponseCodeError
 from bot.bot import Bot
 from bot.cogs.moderation import ModLog
+from bot.cogs.token_remover import TokenRemover
+from bot.cogs.webhook_remover import WEBHOOK_URL_RE
 from bot.constants import BigBrother as BigBrotherConfig, Guild as GuildConfig, Icons
 from bot.pagination import LinePaginator
 from bot.utils import CogABCMeta, messages
@@ -226,14 +228,16 @@ class WatchChannel(metaclass=CogABCMeta):
 
             await self.send_header(msg)
 
-        cleaned_content = msg.clean_content
-
-        if cleaned_content:
+        if TokenRemover.find_token_in_message(msg) or WEBHOOK_URL_RE.search(msg.content):
+            cleaned_content = "Content is censored because it contains a bot or webhook token."
+        elif cleaned_content := msg.clean_content:
             # Put all non-media URLs in a code block to prevent embeds
             media_urls = {embed.url for embed in msg.embeds if embed.type in ("image", "video")}
             for url in URL_RE.findall(cleaned_content):
                 if url not in media_urls:
                     cleaned_content = cleaned_content.replace(url, f"`{url}`")
+
+        if cleaned_content:
             await self.webhook_send(
                 cleaned_content,
                 username=msg.author.display_name,
Repository: python-discord/bot
Issue: Exclude disabled commands from the help command

Sentry Issue: [BOT-89](https://sentry.io/organizations/python-discord/issues/1867358833/?referrer=github_integration)

```
DisabledCommand: raw command is disabled
  File "bot/cogs/help.py", line 105, in command_callback
    await super().command_callback(ctx, command=command)
  File "discord/ext/commands/help.py", line 858, in command_callback
    return await self.send_group_help(cmd)
  File "bot/cogs/help.py", line 243, in send_group_help
    embed = await self.command_formatting(group)
  File "bot/cogs/help.py", line 199, in command_formatting
    if not await command.can_run(self.context):
  File "discord/ext/commands/core.py", line 1047, in can_run
    raise DisabledCommand('{0.name} command is disabled'.format(self))

Task exception was never retrieved
future: <Task finished name='Task-37481353' coro=<CustomHelpCommand.command_callback() done, defined at /bot/bot/cogs/help.py:77> exception=DisabledCommand('raw command is disabled')>
```

Changed files:
bot/exts/info/help.py

diff --git a/bot/exts/info/help.py b/bot/exts/info/help.py
index 461ff82f..3a05b2c8 100644
--- a/bot/exts/info/help.py
+++ b/bot/exts/info/help.py
@@ -5,7 +5,7 @@ from contextlib import suppress
 from typing import List, Union
 
 from discord import Colour, Embed
-from discord.ext.commands import Bot, Cog, Command, Context, Group, HelpCommand
+from discord.ext.commands import Bot, Cog, Command, CommandError, Context, DisabledCommand, Group, HelpCommand
 from fuzzywuzzy import fuzz, process
 from fuzzywuzzy.utils import full_process
 
@@ -20,6 +20,8 @@ log = logging.getLogger(__name__)
 COMMANDS_PER_PAGE = 8
 PREFIX = constants.Bot.prefix
 
+NOT_ALLOWED_TO_RUN_MESSAGE = "***You cannot run this command.***\\n\\n"
+
 Category = namedtuple("Category", ["name", "description", "cogs"])
 
 
@@ -173,9 +175,16 @@ class CustomHelpCommand(HelpCommand):
         if aliases:
             command_details += f"**Can also use:** {aliases}\\n\\n"
 
-        # check if the user is allowed to run this command
-        if not await command.can_run(self.context):
-            command_details += "***You cannot run this command.***\\n\\n"
+        # when command is disabled, show message about it,
+        # when other CommandError or user is not allowed to run command,
+        # add this to help message.
+        try:
+            if not await command.can_run(self.context):
+                command_details += NOT_ALLOWED_TO_RUN_MESSAGE
+        except DisabledCommand:
+            command_details += "***This command is disabled.***\\n\\n"
+        except CommandError:
+            command_details += NOT_ALLOWED_TO_RUN_MESSAGE
 
         command_details += f"*{command.help or 'No details provided.'}*\\n"
         embed.description = command_details
Repository: python-discord/bot
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')>
```

Changed files:
bot/utils/messages.py

diff --git a/bot/utils/messages.py b/bot/utils/messages.py
index 9cc0d8a3..d0b2342b 100644
--- a/bot/utils/messages.py
+++ b/bot/utils/messages.py
@@ -34,7 +34,11 @@ async def wait_for_deletion(
 
     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."""
Repository: python-discord/bot
Issue: Null characters are not allowed in deleted message log

Sentry Issue: [BOT-8E](https://sentry.io/organizations/python-discord/issues/1877134544/?referrer=github_integration)

Not sure how a null character even got in there. Does it mean `\\0`?

```
ResponseCodeError: Status: 400 Response: {'deletedmessage_set': [{'content': ['Null characters are not allowed.']}]}
(1 additional frame(s) were not displayed)
...
  File "bot/cogs/moderation/modlog.py", line 577, in on_message_delete
    botlog_url = await self.upload_log(messages=[message], actor_id=message.author.id)
  File "bot/cogs/moderation/modlog.py", line 56, in upload_log
    response = await self.bot.api_client.post(
  File "bot/api.py", line 127, in post
    return await self.request("POST", endpoint, raise_for_status=raise_for_status, **kwargs)
  File "bot/api.py", line 114, in request
    await self.maybe_raise_for_status(resp, raise_for_status)
  File "bot/api.py", line 104, in maybe_raise_for_status
    raise ResponseCodeError(response=response, response_json=response_json)

Unhandled exception in on_message_delete.
```

Changed files:
bot/exts/moderation/modlog.py

diff --git a/bot/exts/moderation/modlog.py b/bot/exts/moderation/modlog.py
index 41ed46b6..b01de0ee 100644
--- a/bot/exts/moderation/modlog.py
+++ b/bot/exts/moderation/modlog.py
@@ -63,7 +63,7 @@ class ModLog(Cog, name="ModLog"):
                         'id': message.id,
                         'author': message.author.id,
                         'channel_id': message.channel.id,
-                        'content': message.content,
+                        'content': message.content.replace("\\0", ""),  # Null chars cause 400.
                         'embeds': [embed.to_dict() for embed in message.embeds],
                         'attachments': attachment,
                     }
Repository: python-discord/bot
Issue: ZeroDivisionError: division by zero

Sentry Issue: [BOT-KQ](https://sentry.io/organizations/python-discord/issues/2158550250/?referrer=github_integration)

```
ZeroDivisionError: division by zero
(3 additional frame(s) were not displayed)
...
  File "bot/exts/info/tags.py", line 223, in display_tag
    temp_founds = self._get_tag(tag_name)
  File "bot/exts/info/tags.py", line 109, in _get_tag
    return self._get_suggestions(tag_name)
  File "bot/exts/info/tags.py", line 87, in _get_suggestions
    scores: Dict[str, int] = {
  File "bot/exts/info/tags.py", line 88, in <dictcomp>
    tag_title: Tags._fuzzy_search(tag_name, tag['title'])
  File "bot/exts/info/tags.py", line 83, in _fuzzy_search
    return current / len(_search) * 100

Unhandled exception in on_command_error.
```

Changed files:
bot/exts/backend/error_handler.py

diff --git a/bot/exts/backend/error_handler.py b/bot/exts/backend/error_handler.py
index da264ce2..2f02fdee 100644
--- a/bot/exts/backend/error_handler.py
+++ b/bot/exts/backend/error_handler.py
@@ -159,12 +159,7 @@ class ErrorHandler(Cog):
                     return
 
         if not any(role.id in MODERATION_ROLES for role in ctx.author.roles):
-            tags_cog = self.bot.get_cog("Tags")
-            command_name = ctx.invoked_with
-            sent = await tags_cog.display_tag(ctx, command_name)
-
-            if not sent:
-                await self.send_command_suggestion(ctx, command_name)
+            await self.send_command_suggestion(ctx, ctx.invoked_with)
 
         # Return to not raise the exception
         return
Repository: python-discord/bot
Issue: Trying to run a command with a non-existing infraction ID result in a traceback

If a conversion is tried on a non-existing infraction, it should be gracefully handled instead of throwing a 404 exception. 

Sentry Issue: [BOT-M2](https://sentry.io/organizations/python-discord/issues/2170320668/?referrer=github_integration)

```
ResponseCodeError: Status: 404 Response: {'detail': 'Not found.'}
  File "bot/api.py", line 67, in maybe_raise_for_status
    raise ResponseCodeError(response=response, response_json=response_json)
  File "bot/api.py", line 75, in request
    await self.maybe_raise_for_status(resp, raise_for_status)
  File "bot/api.py", line 80, in get
    return await self.request("GET", endpoint, raise_for_status=raise_for_status, **kwargs)
  File "bot/converters.py", line 578, in convert
    return await ctx.bot.api_client.get(f"bot/infractions/{arg}")
  File "discord/ext/commands/core.py", line 451, in _actual_conversion
    ret = await instance.convert(ctx, argument)

ConversionError: (<class 'bot.converters.Infraction'>, ResponseCodeError())
  File "discord/ext/commands/core.py", line 464, in _actual_conversion
    raise ConversionError(converter, exc) from exc
  File "discord/ext/commands/core.py", line 505, in do_conversion
    return await self._actual_conversion(ctx, converter, argument, param)
  File "discord/ext/commands/core.py", line 552, in transform
    return await self.do_conversion(ctx, converter, argument, param)
  File "discord/ext/commands/core.py", line 697, in _parse_arguments
    transformed = await self.transform(ctx, param)
  File "discord/ext/commands/core.py", line 790, in prepare
    await self._parse_arguments(ctx)
...
(3 additional frame(s) were not displayed)

Error executing command invoked by [REDACTED]: !i e 26984Bye bye | [REDACTED] 
```

Changed files:
bot/exts/backend/error_handler.py

diff --git a/bot/exts/backend/error_handler.py b/bot/exts/backend/error_handler.py
index b8bb3757..ed7962b0 100644
--- a/bot/exts/backend/error_handler.py
+++ b/bot/exts/backend/error_handler.py
@@ -85,8 +85,14 @@ class ErrorHandler(Cog):
             else:
                 await self.handle_unexpected_error(ctx, e.original)
             return  # Exit early to avoid logging.
+        elif isinstance(e, errors.ConversionError):
+            if isinstance(e.original, ResponseCodeError):
+                await self.handle_api_error(ctx, e.original)
+            else:
+                await self.handle_unexpected_error(ctx, e.original)
+            return  # Exit early to avoid logging.
         elif not isinstance(e, errors.DisabledCommand):
-            # ConversionError, MaxConcurrencyReached, ExtensionError
+            # MaxConcurrencyReached, ExtensionError
             await self.handle_unexpected_error(ctx, e)
             return  # Exit early to avoid logging.
Repository: python-discord/bot
Issue: Logic in Source Command's Help Command Check

## Description
<!-- Describe the bug you've found -->
Its impossible to get the source for the HelpChannel cog.

## Steps to Reproduce
<!-- Detail the exact list of steps to be able to encounter the bug -->
`!src HelpChannel`
Due to the logic of the command, all that needs to be said is `!src help` followed by anything.

## Expected Behaviour
<!-- Outline what you expect to occur, include screenshots if relevant -->
Source link to the HelpChannel cog is shown.

## Actual Behaviour
<!-- Outline what occurs instead of the expected behavior, include screenshots if relevant -->
Help command source link is shown.

## Known Impacted Platforms
<!-- Replace [ ] with [x] to mark items -->

- [x] Web
- [x] Desktop
- [x] Android App
- [x] iOS App

## Possible Solutions
<!-- Detail any solutions you might have in mind to be able to resolve the bug -->
These lines are the probable cause.
https://github.com/python-discord/bot/blob/51af1369e0d9c2ad185f0c0920b599b7187ed077/bot/exts/info/source.py#L19-L20
After looking at sir-lancebot, it might even be possible to remove these two lines altogether.
## Would you like to implement a fix?

***Note: For high-priority or critical bugs, fixes may be implemented by staff.***
<!-- Replace [ ] with [x] with your choice -->

- [ ] I'd like to implement the bug fix
- [x] Anyone can implement the bug fix


Changed files:
bot/exts/info/source.py

diff --git a/bot/exts/info/source.py b/bot/exts/info/source.py
index 78a6f68a..ef07c77a 100644
--- a/bot/exts/info/source.py
+++ b/bot/exts/info/source.py
@@ -14,9 +14,10 @@ SourceType = Union[commands.HelpCommand, commands.Command, commands.Cog, str, co
 class SourceConverter(commands.Converter):
     """Convert an argument into a help command, tag, command, or cog."""
 
-    async def convert(self, ctx: commands.Context, argument: str) -> SourceType:
+    @staticmethod
+    async def convert(ctx: commands.Context, argument: str) -> SourceType:
         """Convert argument into source object."""
-        if argument.lower().startswith("help"):
+        if argument.lower() == "help":
             return ctx.bot.help_command
 
         cog = ctx.bot.get_cog(argument)
Repository: python-discord/bot
Issue: Forbidden: 403 Forbidden (error code: 50007): Cannot send messages to this user

Sentry Issue: [BOT-13C](https://sentry.io/organizations/python-discord/issues/2453738315/?referrer=github_integration)

```
Forbidden: 403 Forbidden (error code: 50007): Cannot send messages to this user
(2 additional frame(s) were not displayed)
...
  File "bot/utils/lock.py", line 108, in wrapper
    log.debug(
  File "bot/utils/lock.py", line 108, in wrapper
    log.debug(
  File "bot/exts/help_channels/_cog.py", line 583, in notify_session_participants
    await message.author.send(embed=embed)
  File "discord/abc.py", line 935, in send
    data = await state.http.send_message(channel.id, content, tts=tts, embed=embed,
  File "discord/http.py", line 241, in request
    raise Forbidden(r, data)

Unhandled exception in on_message.
```

This is caused when a user turns on help dms, but has dms closed.
We should catch this error and deal with it

Changed files:
bot/exts/help_channels/_cog.py

diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py
index 7fb72d7e..a9b84758 100644
--- a/bot/exts/help_channels/_cog.py
+++ b/bot/exts/help_channels/_cog.py
@@ -12,6 +12,7 @@ from discord.ext import commands
 
 from bot import constants
 from bot.bot import Bot
+from bot.constants import Channels, RedirectOutput
 from bot.exts.help_channels import _caches, _channel, _message, _name, _stats
 from bot.utils import channel as channel_utils, lock, scheduling
 
@@ -580,7 +581,22 @@ class HelpChannels(commands.Cog):
                 timestamp=message.created_at
             )
             embed.add_field(name="Conversation", value=f"[Jump to message]({message.jump_url})")
-            await message.author.send(embed=embed)
+
+            try:
+                await message.author.send(embed=embed)
+            except discord.Forbidden:
+                log.trace(
+                    f"Failed to send helpdm message to {message.author.id}. DMs Closed/Blocked. "
+                    "Removing user from helpdm."
+                )
+                bot_commands_channel = self.bot.get_channel(Channels.bot_commands)
+                await _caches.help_dm.delete(message.author.id)
+                await bot_commands_channel.send(
+                    f"{message.author.mention} {constants.Emojis.cross_mark} "
+                    "To receive updates on help channels you're active in, enable your DMs.",
+                    delete_after=RedirectOutput.delete_after
+                )
+                return
 
             await _caches.session_participants.set(
                 message.channel.id,
Repository: python-discord/bot
Issue: AttributeError: 'delete_after'

Sentry Issue: [BOT-147](https://sentry.io/organizations/python-discord/issues/2466022929/?referrer=github_integration)

```
Forbidden: 403 Forbidden (error code: 50007): Cannot send messages to this user
  File "bot/exts/help_channels/_cog.py", line 586, in notify_session_participants
    await message.author.send(embed=embed)
  File "discord/abc.py", line 935, in send
    data = await state.http.send_message(channel.id, content, tts=tts, embed=embed,
  File "discord/http.py", line 241, in request
    raise Forbidden(r, data)

KeyError: 'delete_after'
  File "bot/constants.py", line 183, in __getattr__
    return _CONFIG_YAML[cls.section][name]

AttributeError: 'delete_after'
(1 additional frame(s) were not displayed)
...
  File "bot/exts/help_channels/_cog.py", line 473, in on_message
    await self.notify_session_participants(message)
  File "bot/utils/lock.py", line 108, in wrapper
    except discord.Forbidden:
  File "bot/utils/lock.py", line 108, in wrapper
    except discord.Forbidden:
  File "bot/exts/help_channels/_cog.py", line 597, in notify_session_participants
    delete_after=RedirectOutput.delete_after
  File "bot/constants.py", line 191, in __getattr__
    raise AttributeError(repr(name)) from e

Unhandled exception in on_message.
```

Changed files:
bot/exts/help_channels/_cog.py

diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py
index a9b84758..35658d11 100644
--- a/bot/exts/help_channels/_cog.py
+++ b/bot/exts/help_channels/_cog.py
@@ -594,7 +594,7 @@ class HelpChannels(commands.Cog):
                 await bot_commands_channel.send(
                     f"{message.author.mention} {constants.Emojis.cross_mark} "
                     "To receive updates on help channels you're active in, enable your DMs.",
-                    delete_after=RedirectOutput.delete_after
+                    delete_after=RedirectOutput.delete_delay
                 )
                 return
Repository: python-discord/bot
Issue: AttributeError: 'moderation_team'

Sentry Issue: [BOT-1J0](https://sentry.io/organizations/python-discord/issues/2525032320/?referrer=github_integration)

```
KeyError: 'moderation_team'
  File "bot/constants.py", line 182, in __getattr__
    return _CONFIG_YAML[cls.section][cls.subsection][name]

AttributeError: 'moderation_team'
  File "discord/ext/commands/core.py", line 85, in wrapped
    ret = await coro(*args, **kwargs)
  File "bot/exts/info/information.py", line 189, in server_info
    member_counts = self.get_member_counts(ctx.guild)
  File "bot/exts/info/information.py", line 49, in get_member_counts
    constants.Roles.helpers, constants.Roles.moderation_team, constants.Roles.admins,
  File "bot/constants.py", line 191, in __getattr__
    raise AttributeError(repr(name)) from e

Error executing command invoked by Qwerty#0004: !server
```

Changed files:
bot/exts/info/information.py

diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py
index 089ce4eb..bb713eef 100644
--- a/bot/exts/info/information.py
+++ b/bot/exts/info/information.py
@@ -46,7 +46,7 @@ class Information(Cog):
         """Return the total number of members for certain roles in `guild`."""
         roles = (
             guild.get_role(role_id) for role_id in (
-                constants.Roles.helpers, constants.Roles.moderation_team, constants.Roles.admins,
+                constants.Roles.helpers, constants.Roles.mod_team, constants.Roles.admins,
                 constants.Roles.owners, constants.Roles.contributors,
             )
         )
Repository: python-discord/bot
Issue: TypeError raised when using infraction append incorrectly

!i a l 3d raises the following exception:

```py
TypeError: can only concatenate str (not "NoneType") to str
```

https://github.com/python-discord/bot/blob/e65afc4e83f822f06fa2d25a7eb8c233b1cce71a/bot/exts/moderation/infraction/management.py#L58 makes reason optional

https://github.com/python-discord/bot/blob/e65afc4e83f822f06fa2d25a7eb8c233b1cce71a/bot/exts/moderation/infraction/management.py#L86

assumes that the provided reason is not None. Should be a very straightforwards fix changing `if old_reason is not None` `and reason is not None`.

Changed files:
bot/exts/moderation/infraction/management.py

diff --git a/bot/exts/moderation/infraction/management.py b/bot/exts/moderation/infraction/management.py
index 4b0cb78a..3094159c 100644
--- a/bot/exts/moderation/infraction/management.py
+++ b/bot/exts/moderation/infraction/management.py
@@ -81,7 +81,7 @@ class ModManagement(commands.Cog):
         """
         old_reason = infraction["reason"]
 
-        if old_reason is not None:
+        if old_reason is not None and reason is not None:
             add_period = not old_reason.endswith((".", "!", "?"))
             reason = old_reason + (". " if add_period else " ") + reason
Repository: python-discord/bot
Issue: Dynamically insert help categories into DORMANT_MSG

As [suggested by Lemon](https://discord.com/channels/267624335836053506/429409067623251969/874919321283993631) in #community-meta, this will insert the correct category names into the DORMANT_MSG that's sent when a channel is moved to the dormant category.

Changed files:
bot/exts/help_channels/_message.py, bot/exts/help_channels/_cog.py

diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py
index 35658d11..afaf9b0b 100644
--- a/bot/exts/help_channels/_cog.py
+++ b/bot/exts/help_channels/_cog.py
@@ -387,7 +387,15 @@ class HelpChannels(commands.Cog):
         )
 
         log.trace(f"Sending dormant message for #{channel} ({channel.id}).")
-        embed = discord.Embed(description=_message.DORMANT_MSG)
+        dormant_category = await channel_utils.try_get_channel(constants.Categories.help_dormant)
+        available_category = await channel_utils.try_get_channel(constants.Categories.help_available)
+        embed = discord.Embed(
+            description=_message.DORMANT_MSG.format(
+                dormant=dormant_category.name,
+                available=available_category.name,
+                asking_guide=_message.ASKING_GUIDE_URL
+            )
+        )
         await channel.send(embed=embed)
 
         log.trace(f"Pushing #{channel} ({channel.id}) into the channel queue.")
diff --git a/bot/exts/help_channels/_message.py b/bot/exts/help_channels/_message.py
index befacd26..cf070be8 100644
--- a/bot/exts/help_channels/_message.py
+++ b/bot/exts/help_channels/_message.py
@@ -29,15 +29,15 @@ AVAILABLE_TITLE = "Available help channel"
 
 AVAILABLE_FOOTER = "Closes after a period of inactivity, or when you send !close."
 
-DORMANT_MSG = f"""
-This help channel has been marked as **dormant**, and has been moved into the **Help: Dormant** \\
+DORMANT_MSG = """
+This help channel has been marked as **dormant**, and has been moved into the **{dormant}** \\
 category at the bottom of the channel list. It is no longer possible to send messages in this \\
 channel until it becomes available again.
 
 If your question wasn't answered yet, you can claim a new help channel from the \\
-**Help: Available** category by simply asking your question again. Consider rephrasing the \\
+**{available}** category by simply asking your question again. Consider rephrasing the \\
 question to maximize your chance of getting a good answer. If you're not sure how, have a look \\
-through our guide for **[asking a good question]({ASKING_GUIDE_URL})**.
+through our guide for **[asking a good question]({asking_guide})**.
 """
Repository: python-discord/bot
Issue: `!user` command doesn't escape markdown in user's name

Pretty self explanatory. The title of the embed (the name) doesn't escape markdown so can end up being italic etc.

Changed files:
bot/exts/info/information.py

diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py
index a9ea403f..8bef6a8c 100644
--- a/bot/exts/info/information.py
+++ b/bot/exts/info/information.py
@@ -8,6 +8,7 @@ from typing import Any, DefaultDict, Mapping, Optional, Tuple, Union
 import rapidfuzz
 from discord import AllowedMentions, Colour, Embed, Guild, Message, Role
 from discord.ext.commands import BucketType, Cog, Context, Paginator, command, group, has_any_role
+from discord.utils import escape_markdown
 
 from bot import constants
 from bot.api import ResponseCodeError
@@ -244,6 +245,7 @@ class Information(Cog):
         name = str(user)
         if on_server and user.nick:
             name = f"{user.nick} ({name})"
+        name = escape_markdown(name)
 
         if user.public_flags.verified_bot:
             name += f" {constants.Emojis.verified_bot}"
Repository: python-discord/bot
Issue: Handle error for long poll titles in a user-friendly way

Sentry Issue: [BOT-7Q](https://sentry.io/organizations/python-discord/issues/1821866467/?referrer=github_integration)

The poll command puts the poll's question in the embed's title, which has a maximum character limit of 256. Instead of letting the exception for a 400 response status propagate, an error should be displayed to the user indicating the question given is too long. Automatic truncation is probably a bad idea in this context. The user should manually figure out a shorter phrasing.

Changed files:
bot/cogs/utils.py

diff --git a/bot/cogs/utils.py b/bot/cogs/utils.py
index 91c6cb36..d96abbd5 100644
--- a/bot/cogs/utils.py
+++ b/bot/cogs/utils.py
@@ -232,6 +232,8 @@ class Utils(Cog):
         A maximum of 20 options can be provided, as Discord supports a max of 20
         reactions on a single message.
         """
+        if len(title) > 256:
+            raise BadArgument("The title cannot be longer than 256 characters.")
         if len(options) < 2:
             raise BadArgument("Please provide at least 2 options.")
         if len(options) > 20:
Repository: python-discord/bot
Issue: Nickname Filter Also Catches Webhooks

The nickname filtering introduced by #964 also matches on webhooks, causing false positives. Posts from webhooks should be ignored.

Changed files:
bot/cogs/filtering.py

diff --git a/bot/cogs/filtering.py b/bot/cogs/filtering.py
index 4ec95ad7..93cc1c65 100644
--- a/bot/cogs/filtering.py
+++ b/bot/cogs/filtering.py
@@ -124,7 +124,10 @@ class Filtering(Cog):
     async def on_message(self, msg: Message) -> None:
         """Invoke message filter for new messages."""
         await self._filter_message(msg)
-        await self.check_bad_words_in_name(msg.author)
+
+        # Ignore webhook messages.
+        if msg.webhook_id is None:
+            await self.check_bad_words_in_name(msg.author)
 
     @Cog.listener()
     async def on_message_edit(self, before: Message, after: Message) -> None:
Repository: python-discord/bot
Issue: Check channel type before trying to access category

The fix is the same as https://github.com/python-discord/bot/commit/1509d3b1c39fac825d5e5a09fd0caf780db7c91c. I just overlooked applying it to `help_channels.py` too.

```
Ignoring exception in on_message
Traceback (most recent call last):
  File "/usr/local/lib/python3.8/site-packages/discord/client.py", line 312, in _run_event
    await coro(*args, **kwargs)
  File "/bot/bot/cogs/help_channels.py", line 516, in on_message
    if channel.category and channel.category.id != constants.Categories.help_available:
AttributeError: 'DMChannel' object has no attribute 'category'
```

Changed files:
bot/cogs/help_channels.py

diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py
index 692bb234..e73bbdae 100644
--- a/bot/cogs/help_channels.py
+++ b/bot/cogs/help_channels.py
@@ -420,6 +420,12 @@ class HelpChannels(Scheduler, commands.Cog):
         embed = message.embeds[0]
         return message.author == self.bot.user and embed.description.strip() == DORMANT_MSG.strip()
 
+    @staticmethod
+    def is_in_category(channel: discord.TextChannel, category_id: int) -> bool:
+        """Return True if `channel` is within a category with `category_id`."""
+        actual_category = getattr(channel, "category", None)
+        return actual_category is not None and actual_category.id == category_id
+
     async def move_idle_channel(self, channel: discord.TextChannel, has_task: bool = True) -> None:
         """
         Make the `channel` dormant if idle or schedule the move if still active.
@@ -581,7 +587,7 @@ class HelpChannels(Scheduler, commands.Cog):
             return  # Ignore messages sent by bots.
 
         channel = message.channel
-        if channel.category and channel.category.id != constants.Categories.help_available:
+        if not self.is_in_category(channel, constants.Categories.help_available):
             return  # Ignore messages outside the Available category.
 
         log.trace("Waiting for the cog to be ready before processing messages.")
@@ -591,7 +597,7 @@ class HelpChannels(Scheduler, commands.Cog):
         async with self.on_message_lock:
             log.trace(f"on_message lock acquired for {message.id}.")
 
-            if channel.category and channel.category.id != constants.Categories.help_available:
+            if not self.is_in_category(channel, constants.Categories.help_available):
                 log.debug(
                     f"Message {message.id} will not make #{channel} ({channel.id}) in-use "
                     f"because another message in the channel already triggered that."
Repository: python-discord/bot
Issue: Use appropriate log level for exceptions from event listeners

From @SebastiaanZ:

> Finally, `discord.py` currently "hides" errors/tracebacks that happen in event listeners as we only have a custom error handler for commands. This isn't too bad locally, since `d.py` **prints** those exceptions to stderr, but it obviously means they'll never show up in Sentry, as they are **not actually logged** with the appropriate level.

Changed files:
bot/bot.py

diff --git a/bot/bot.py b/bot/bot.py
index 6dd5ba89..49fac27e 100644
--- a/bot/bot.py
+++ b/bot/bot.py
@@ -7,6 +7,7 @@ from typing import Optional
 import aiohttp
 import discord
 from discord.ext import commands
+from sentry_sdk import push_scope
 
 from bot import DEBUG_MODE, api, constants
 from bot.async_stats import AsyncStatsClient
@@ -155,3 +156,14 @@ class Bot(commands.Bot):
         gateway event before giving up and thus not populating the cache for unavailable guilds.
         """
         await self._guild_available.wait()
+
+    async def on_error(self, event: str, *args, **kwargs) -> None:
+        """Log errors raised in event listeners rather than printing them to stderr."""
+        self.stats.incr(f"errors.event.{event}")
+
+        with push_scope() as scope:
+            scope.set_tag("event", event)
+            scope.set_extra("args", args)
+            scope.set_extra("kwargs", kwargs)
+
+            log.exception(f"Unhandled exception in {event}.")