Skip to content

Conversation

@malcontent5
Copy link
Collaborator

@malcontent5 malcontent5 commented Jun 20, 2025

Description

Added config options for bookmarks in the settings.yml including custom emojis and added the ability for the user to delete bookmarks in their DMs

Guidelines

  • My code follows the style guidelines of this project (formatted with Ruff)

  • I have performed a self-review of my own code

  • I have commented my code, particularly in hard-to-understand areas

  • I have made corresponding changes to the documentation if needed

  • My changes generate no new warnings

  • I have tested this change

  • Any dependent changes have been merged and published in downstream modules

  • I have added all appropriate labels to this PR

  • I have followed all of these guidelines.

How Has This Been Tested? (if applicable)

Tested by reacting to messages both inside and outside of DMs both from tux and from the user all works correctly and it only removes Tux messages in DMs

Screenshots (if applicable)

image

Additional Information

closed my previous pull request because it was a mess. change to poll.py is ensuring it doesn't try to read reactions in dms
also sorry that some of the commits are messy and non-standard

Summary by Sourcery

Enable bookmark removal via emoji reactions and enhance bookmark reaction handling with configurable emojis, robust fetching, and error logging.

New Features:

  • Allow users to remove bookmarked messages by reacting with a removal emoji.
  • Expose add/remove bookmark emojis as configuration constants.

Bug Fixes:

  • Prevent the poll reaction handler from failing in direct messages by fetching channels when not cached.

Enhancements:

  • Centralize emoji validation and refactor bookmark embed creation to async.
  • Add fallback logic and error handling when fetching users, channels, and messages during reaction events.
  • Automatically add the removal emoji to bookmark DMs for quick deletion.

malcontent5 and others added 30 commits August 29, 2024 00:34
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Jun 20, 2025

Reviewer's Guide

This PR enhances bookmark functionality by introducing configurable add/remove emojis and central emoji validation, refactors the reaction handling workflow—splitting add/remove flows, making embed creation async and adding DM reaction options—bolsters user/channel fetch error handling, updates the poll cog to safely handle missing or DM channels, and adds a new standalone utility cog for bookmarks.

Sequence diagram for handling add/remove bookmark reactions

sequenceDiagram
    actor User
    participant Bot
    participant Channel
    participant Message
    User->>Bot: Reacts with add/remove bookmark emoji
    Bot->>Channel: Fetch channel
    Bot->>Message: Fetch message
    alt Add bookmark emoji
        Bot->>User: Send DM with embed
        Bot->>Message: Add remove bookmark reaction
    else Remove bookmark emoji (in DM)
        Bot->>Message: Delete message (if author is bot)
    end
Loading

Entity relationship diagram for bookmark emoji configuration

erDiagram
    CONSTANTS {
        string ADD_BOOKMARK
        string REMOVE_BOOKMARK
    }
    BOOKMARKS {
        string valid_add_emojis
        string valid_remove_emojis
        string valid_emojis
    }
    BOOKMARKS ||--|| CONSTANTS : uses
Loading

Class diagram for updated Bookmarks service and constants

classDiagram
    class Bookmarks {
        - bot: Tux
        - valid_add_emojis
        - valid_remove_emojis
        - valid_emojis
        + _is_valid_emoji(emoji, valid_list) bool
        + on_raw_reaction_add(payload)
        + _create_bookmark_embed(message) Embed
        + _delete_bookmark(message, user)
        + _send_bookmark(user, message, embed, emoji)
    }
    class Constants {
        + ADD_BOOKMARK
        + REMOVE_BOOKMARK
    }
    Bookmarks ..> Constants : uses
Loading

Class diagram for new utility Bookmarks cog

classDiagram
    class Bookmarks {
        - bot: commands.Bot
        + on_raw_reaction_add(payload)
    }
    class EmbedCreator {
        + create_info_embed(title, description)
    }
    Bookmarks ..> EmbedCreator : uses
Loading

File-Level Changes

Change Details Files
Introduce configurable emojis and central validation
  • Defined ADD_BOOKMARK and REMOVE_BOOKMARK constants
  • Added valid_add_emojis, valid_remove_emojis and combined valid_emojis in the Bookmarks cog
  • Implemented a private _is_valid_emoji helper method
tux/cogs/services/bookmarks.py
tux/utils/constants.py
Refactor bookmark reaction flows
  • Branch on add vs remove emojis in on_raw_reaction_add
  • Converted _create_bookmark_embed to async and consolidated send logic into _send_bookmark
  • Added reaction on DM-sent embeds for removal
  • Introduced async _delete_bookmark to delete bot messages
tux/cogs/services/bookmarks.py
Harden user/channel fetching with fallbacks
  • Fetch user via get_user or fetch_user with error logging
  • Fetch channel via get_channel or fetch_channel with exception handling
  • Log NotFound, Forbidden and HTTPException cases
tux/cogs/services/bookmarks.py
Update poll cog to skip unsupported channels
  • Replace get_channel-only logic with fetch_channel fallback
  • Cast fetched channels to TextChannel
Thread after verification
Add standalone bookmarks utility cog
  • Created new cogs/utility/bookmarks.py for bookmark handling
  • Implemented DM send with EmbedCreator and reaction removal
  • Added user notification when DMs are disabled
cogs/utility/bookmarks.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @cherryl1k - I've reviewed your changes - here's some feedback:

  • The new cogs/utility/bookmarks.py duplicates much of the logic in tux/cogs/services/bookmarks.py—consider consolidating into a single cog to avoid maintenance overhead and conflicting behavior.
  • The log message in the emoji validation else branch uses profanity—please replace it with a clear, professional error message.
  • You’re storing ADD_BOOKMARK and REMOVE_BOOKMARK as strings but treating them like collections in _is_valid_emoji; consider using lists or sets of emoji names for better clarity and future extensibility.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The new `cogs/utility/bookmarks.py` duplicates much of the logic in `tux/cogs/services/bookmarks.py`—consider consolidating into a single cog to avoid maintenance overhead and conflicting behavior.
- The log message in the emoji validation else branch uses profanity—please replace it with a clear, professional error message.
- You’re storing `ADD_BOOKMARK` and `REMOVE_BOOKMARK` as strings but treating them like collections in `_is_valid_emoji`; consider using lists or sets of emoji names for better clarity and future extensibility.

## Individual Comments

### Comment 1
<location> `tux/cogs/services/bookmarks.py:143` </location>
<code_context>
         try:
-            await user.send(embed=embed)
+            dm_message = await user.send(embed=embed)
+            await dm_message.add_reaction(CONST.REMOVE_BOOKMARK)

         except (discord.Forbidden, discord.HTTPException) as dm_error:
</code_context>

<issue_to_address>
Adding a reaction to the DM message may fail if the bot lacks permissions.

Handle potential exceptions from add_reaction with a try/except block to prevent unhandled errors if the bot lacks permissions or DMs are restricted.
</issue_to_address>

### Comment 2
<location> `tux/cogs/services/bookmarks.py:27` </location>
<code_context>
+        self.bot = bot
+
+    @commands.Cog.listener()
+    async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent) -> None:
+        if str(payload.emoji) == "🔖":
+            try:
</code_context>

<issue_to_address>
Consider refactoring on_raw_reaction_add to flatten control flow, centralize fetch logic, and make embed creation synchronous.

```markdown
You can flatten `on_raw_reaction_add` and remove unreachable branches by:

1. Dropping the initial `valid_emojis` check and final `else` (it’s never reached once you branch by add/remove).
2. Pulling user/channel get‐then‐fetch logic into small helpers.
3. Making `_create_bookmark_embed` a sync method again (nothing inside is async).

Example refactor:

```python
class Bookmarks(commands.Cog):
    def __init__(…):
        …
        self.add_emojis = set(CONST.ADD_BOOKMARK)
        self.remove_emojis = set(CONST.REMOVE_BOOKMARK)

    async def on_raw_reaction_add(self, payload):
        emoji = payload.emoji.name
        if emoji in self.add_emojis:
            user = await self._get_user(payload.user_id) or return
            channel = await self._get_channel(payload.channel_id) or return
            message = await self._safe_fetch(channel.fetch_message, payload.message_id) or return
            embed = self._create_bookmark_embed(message)
            await self._send_bookmark(user, message, embed)
        elif emoji in self.remove_emojis:
            user = await self._get_user(payload.user_id) or return
            channel = await self._get_channel(payload.channel_id) or return
            message = await self._safe_fetch(channel.fetch_message, payload.message_id) or return
            await self._delete_bookmark(message)
        # no final else needed

    async def _get_user(self, user_id):
        user = self.bot.get_user(user_id)
        if user: return user
        return await self._safe_fetch(self.bot.fetch_user, user_id)

    async def _get_channel(self, channel_id):
        channel = self.bot.get_channel(channel_id)
        if channel: return channel
        return await self._safe_fetch(self.bot.fetch_channel, channel_id)

    async def _safe_fetch(self, fn, *args):
        try:
            return await fn(*args)
        except discord.NotFound:
            logger.error(f"Not found: {fn.__name__}({args[0]})")
        except (discord.Forbidden, discord.HTTPException) as e:
            logger.error(f"Failed {fn.__name__}: {e}")

    def _create_bookmark_embed(self, message: discord.Message) -> discord.Embed:
        content = message.content
        if len(content) > CONST.EMBED_MAX_DESC_LENGTH:
            content = content[:CONST.EMBED_MAX_DESC_LENGTH - 3] + "..."
        embed = EmbedCreator.create_embed(
            bot=self.bot,
            embed_type=EmbedCreator.INFO,
            title="Message Bookmarked",
            description=f"> {content}",
        )
        embed.add_field("Author", message.author.name, inline=False)
        embed.add_field("Jump to Message", f"[Click Here]({message.jump_url})", inline=False)
        if message.attachments:
            urls = "\n".join(a.url for a in message.attachments)
            embed.add_field("Attachments", urls, inline=False)
        return embed
```

This removes deep nesting, centralizes fetch-and-log, drops the “how’d you get here?” branch, and keeps embed creation synchronous.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

try:
await user.send(embed=embed)
dm_message = await user.send(embed=embed)
await dm_message.add_reaction(CONST.REMOVE_BOOKMARK)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Adding a reaction to the DM message may fail if the bot lacks permissions.

Handle potential exceptions from add_reaction with a try/except block to prevent unhandled errors if the bot lacks permissions or DMs are restricted.

return False

@commands.Cog.listener()
async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent) -> None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): Consider refactoring on_raw_reaction_add to flatten control flow, centralize fetch logic, and make embed creation synchronous.

You can flatten `on_raw_reaction_add` and remove unreachable branches by:

1. Dropping the initial `valid_emojis` check and final `else` (it’s never reached once you branch by add/remove).
2. Pulling user/channel get‐then‐fetch logic into small helpers.
3. Making `_create_bookmark_embed` a sync method again (nothing inside is async).

Example refactor:

```python
class Bookmarks(commands.Cog):
    def __init__(…):
        …
        self.add_emojis = set(CONST.ADD_BOOKMARK)
        self.remove_emojis = set(CONST.REMOVE_BOOKMARK)

    async def on_raw_reaction_add(self, payload):
        emoji = payload.emoji.name
        if emoji in self.add_emojis:
            user = await self._get_user(payload.user_id) or return
            channel = await self._get_channel(payload.channel_id) or return
            message = await self._safe_fetch(channel.fetch_message, payload.message_id) or return
            embed = self._create_bookmark_embed(message)
            await self._send_bookmark(user, message, embed)
        elif emoji in self.remove_emojis:
            user = await self._get_user(payload.user_id) or return
            channel = await self._get_channel(payload.channel_id) or return
            message = await self._safe_fetch(channel.fetch_message, payload.message_id) or return
            await self._delete_bookmark(message)
        # no final else needed

    async def _get_user(self, user_id):
        user = self.bot.get_user(user_id)
        if user: return user
        return await self._safe_fetch(self.bot.fetch_user, user_id)

    async def _get_channel(self, channel_id):
        channel = self.bot.get_channel(channel_id)
        if channel: return channel
        return await self._safe_fetch(self.bot.fetch_channel, channel_id)

    async def _safe_fetch(self, fn, *args):
        try:
            return await fn(*args)
        except discord.NotFound:
            logger.error(f"Not found: {fn.__name__}({args[0]})")
        except (discord.Forbidden, discord.HTTPException) as e:
            logger.error(f"Failed {fn.__name__}: {e}")

    def _create_bookmark_embed(self, message: discord.Message) -> discord.Embed:
        content = message.content
        if len(content) > CONST.EMBED_MAX_DESC_LENGTH:
            content = content[:CONST.EMBED_MAX_DESC_LENGTH - 3] + "..."
        embed = EmbedCreator.create_embed(
            bot=self.bot,
            embed_type=EmbedCreator.INFO,
            title="Message Bookmarked",
            description=f"> {content}",
        )
        embed.add_field("Author", message.author.name, inline=False)
        embed.add_field("Jump to Message", f"[Click Here]({message.jump_url})", inline=False)
        if message.attachments:
            urls = "\n".join(a.url for a in message.attachments)
            embed.add_field("Attachments", urls, inline=False)
        return embed

This removes deep nesting, centralizes fetch-and-log, drops the “how’d you get here?” branch, and keeps embed creation synchronous.

self.bot = bot

@commands.Cog.listener()
async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent) -> None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (code-quality): Add guard clause (last-if-guard)

Comment on lines +22 to +24
if emoji.name in valid_list: # noqa: SIM103
return True
return False
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): We've found these issues:

Suggested change
if emoji.name in valid_list: # noqa: SIM103
return True
return False
return emoji.name in valid_list

@codecov
Copy link

codecov bot commented Jun 20, 2025

Codecov Report

Attention: Patch coverage is 4.16667% with 46 lines in your changes missing coverage. Please review.

Project coverage is 8.96%. Comparing base (5d36f15) to head (0a5e043).
Report is 2 commits behind head on main.

✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
tux/cogs/services/bookmarks.py 0.00% 36 Missing ⚠️
tux/cogs/utility/poll.py 0.00% 10 Missing ⚠️
Additional details and impacted files
@@          Coverage Diff          @@
##            main    #916   +/-   ##
=====================================
  Coverage   8.96%   8.96%           
=====================================
  Files        121     121           
  Lines      10223   10222    -1     
  Branches    1239    1239           
=====================================
  Hits         916     916           
+ Misses      9235    9234    -1     
  Partials      72      72           
Flag Coverage Δ *Carryforward flag
database 0.30% <ø> (ø) Carriedforward from 7fa2ee0
integration 5.95% <4.16%> (+<0.01%) ⬆️
unit 6.41% <4.16%> (+<0.01%) ⬆️

*This pull request uses carry forward flags. Click here to find out more.

Components Coverage Δ
Core Bot Infrastructure 16.45% <ø> (ø)
Database Layer 0.00% <ø> (ø)
Bot Commands & Features 0.00% <0.00%> (ø)
Event & Error Handling ∅ <ø> (∅)
Utilities & Helpers ∅ <ø> (∅)
User Interface Components 0.00% <ø> (ø)
CLI Interface ∅ <ø> (∅)
External Service Wrappers ∅ <ø> (∅)

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@malcontent5
Copy link
Collaborator Author

git is pissing me off please god make it stop

@malcontent5
Copy link
Collaborator Author

git made like 70 extra commits ill redo this AGAIn tmr

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants