Repository: python-discord/bot

Issue:
Don't unpin message if the message ID is None
Sentry Issue: [BOT-7G](https://sentry.io/organizations/python-discord/issues/1812744317/?referrer=github_integration)

If a message for the channel cannot be retrieved from the cache, don't try to unpin the message. It currently results in a warning.

https://github.com/python-discord/bot/blob/3649c2a03dc158a25f2c3be98db8691f903a1953/bot/cogs/help_channels.py#L554-L562

Also, sneak in a minor change to the warning log message to include the HTTP status as well as the code.


Discussion:

Resolving patch:
diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py
--- a/bot/cogs/help_channels.py
+++ b/bot/cogs/help_channels.py
@@ -548,21 +548,9 @@ async def move_to_dormant(self, channel: discord.TextChannel, caller: str) -> No
 
         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:
-            await self.bot.http.unpin_message(channel.id, msg_id)
-        except discord.HTTPException as e:
-            if e.code == 10008:
-                log.trace(f"Message {msg_id} don't exist, can't unpin.")
-            else:
-                log.warn(f"Got unexpected status {e.code} when unpinning message {msg_id}: {e.text}")
-        else:
-            log.trace(f"Unpinned message {msg_id}.")
-
         log.info(f"Moving #{channel} ({channel.id}) to the Dormant category.")
 
+        await self.help_channel_claimants.delete(channel.id)
         await self.move_to_bottom_position(
             channel=channel,
             category_id=constants.Categories.help_dormant,
@@ -585,6 +573,8 @@ async def move_to_dormant(self, channel: discord.TextChannel, caller: str) -> No
         embed = discord.Embed(description=DORMANT_MSG)
         await channel.send(embed=embed)
 
+        await self.unpin(channel)
+
         log.trace(f"Pushing #{channel} ({channel.id}) into the channel queue.")
         self.channel_queue.put_nowait(channel)
         self.report_stats()
@@ -702,15 +692,8 @@ async def on_message(self, message: discord.Message) -> None:
             log.info(f"Channel #{channel} was claimed by `{message.author.id}`.")
             await self.move_to_in_use(channel)
             await self.revoke_send_permissions(message.author)
-            # Pin message for better access and store this to cache
-            try:
-                await message.pin()
-            except discord.NotFound:
-                log.info(f"Pinning message {message.id} ({channel}) failed because message got deleted.")
-            except discord.HTTPException as e:
-                log.info(f"Pinning message {message.id} ({channel.id}) failed with code {e.code}", exc_info=e)
-            else:
-                await self.question_messages.set(channel.id, message.id)
+
+            await self.pin(message)
 
             # Add user with channel for dormant check.
             await self.help_channel_claimants.set(channel.id, message.author.id)
@@ -752,9 +735,22 @@ async def on_message_delete(self, msg: discord.Message) -> None:
         self.scheduler.schedule_later(delay, msg.channel.id, self.move_idle_channel(msg.channel))
 
     async def is_empty(self, channel: discord.TextChannel) -> bool:
-        """Return True if the most recent message in `channel` is the bot's `AVAILABLE_MSG`."""
-        msg = await self.get_last_message(channel)
-        return self.match_bot_embed(msg, AVAILABLE_MSG)
+        """Return True if there's an AVAILABLE_MSG and the messages leading up are bot messages."""
+        log.trace(f"Checking if #{channel} ({channel.id}) is empty.")
+
+        # A limit of 100 results in a single API call.
+        # If AVAILABLE_MSG isn't found within 100 messages, then assume the channel is not empty.
+        # Not gonna do an extensive search for it cause it's too expensive.
+        async for msg in channel.history(limit=100):
+            if not msg.author.bot:
+                log.trace(f"#{channel} ({channel.id}) has a non-bot message.")
+                return False
+
+            if self.match_bot_embed(msg, AVAILABLE_MSG):
+                log.trace(f"#{channel} ({channel.id}) has the available message embed.")
+                return True
+
+        return False
 
     async def check_cooldowns(self) -> None:
         """Remove expired cooldowns and re-schedule active ones."""
@@ -861,6 +857,47 @@ async def try_get_channel(self, channel_id: int) -> discord.abc.GuildChannel:
         log.trace(f"Channel #{channel} ({channel_id}) retrieved.")
         return channel
 
+    async def pin_wrapper(self, msg_id: int, channel: discord.TextChannel, *, pin: bool) -> bool:
+        """
+        Pin message `msg_id` in `channel` if `pin` is True or unpin if it's False.
+
+        Return True if successful and False otherwise.
+        """
+        channel_str = f"#{channel} ({channel.id})"
+        if pin:
+            func = self.bot.http.pin_message
+            verb = "pin"
+        else:
+            func = self.bot.http.unpin_message
+            verb = "unpin"
+
+        try:
+            await func(channel.id, msg_id)
+        except discord.HTTPException as e:
+            if e.code == 10008:
+                log.debug(f"Message {msg_id} in {channel_str} doesn't exist; can't {verb}.")
+            else:
+                log.exception(
+                    f"Error {verb}ning message {msg_id} in {channel_str}: {e.status} ({e.code})"
+                )
+            return False
+        else:
+            log.trace(f"{verb.capitalize()}ned message {msg_id} in {channel_str}.")
+            return True
+
+    async def pin(self, message: discord.Message) -> None:
+        """Pin an initial question `message` and store it in a cache."""
+        if await self.pin_wrapper(message.id, message.channel, pin=True):
+            await self.question_messages.set(message.channel.id, message.id)
+
+    async def unpin(self, channel: discord.TextChannel) -> None:
+        """Unpin the initial question message sent in `channel`."""
+        msg_id = await self.question_messages.pop(channel.id)
+        if msg_id is None:
+            log.debug(f"#{channel} ({channel.id}) doesn't have a message pinned.")
+        else:
+            await self.pin_wrapper(msg_id, channel, pin=False)
+
     async def wait_for_dormant_channel(self) -> discord.TextChannel:
         """Wait for a dormant channel to become available in the queue and return it."""
         log.trace("Waiting for a dormant channel.")


Test patch: