Skip to content

feat:(bookmarks) added removing bookmarks #904

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 22 commits into from

Conversation

cherryl1k
Copy link
Collaborator

@cherryl1k cherryl1k commented Jun 19, 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.

Summary by Sourcery

Enable bookmark removal via emoji reaction, expose bookmark emojis as constants, improve reaction handling with fallback fetches and error logging, and fix the poll reaction handler to fetch uncached channels

New Features:

  • Allow users to remove bookmarked messages in DMs by reacting with the configured remove emoji
  • Expose add and remove bookmark emojis as configurable constants

Bug Fixes:

  • Fetch missing channels in the poll reaction handler to prevent failures on uncached or direct message channels

Enhancements:

  • Add fallback fetching and error logging for user and channel lookups in the bookmark reaction listener
  • Refactor bookmark reaction flow to validate emojis via a helper and separate add/remove logic

Copy link
Contributor

sourcery-ai bot commented Jun 19, 2025

Reviewer's Guide

This PR extends the bookmark feature to support removable emojis configurable via settings, implements deletion of bookmarked messages in DMs, and hardens raw reaction handlers (including the poll handler) with fallback user/channel fetching and error logging.

Sequence diagram for bookmark add and remove via emoji reaction

sequenceDiagram
    actor User
    participant Tux
    participant Discord
    participant Settings
    User->>Discord: Reacts with add or remove emoji
    Discord->>Tux: on_raw_reaction_add event
    Tux->>Settings: Get valid emojis
    alt Add emoji
        Tux->>Discord: Fetch message, user, channel
        Tux->>User: Send bookmark DM
        Tux->>User: Add remove emoji to DM
    else Remove emoji (in DM)
        Tux->>Discord: Fetch message, user, channel
        Tux->>Discord: Delete DM message
    end
Loading

Entity relationship diagram for bookmark emoji configuration

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

Updated class diagram for Bookmarks service with bookmark removal

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

File-Level Changes

Change Details Files
Enhance bookmark service to support add and remove emojis with removal flow
  • Expose ADD_BOOKMARK and REMOVE_BOOKMARK in constants for configuration
  • Initialize valid_add_emojis, valid_remove_emojis, and valid_emojis in the Cog
  • Distinguish add vs remove reactions and branch to _send_bookmark or _delete_bookmark
  • Make _create_bookmark_embed async and add _delete_bookmark method to delete bot messages
  • Append remove emoji to DMs so users can react to delete bookmarks
tux/cogs/services/bookmarks.py
tux/utils/constants.py
Harden raw reaction handlers with fallback fetching and error logging
  • Fetch users/channels when get_user or get_channel returns None
  • Catch and log NotFound, Forbidden, and HTTPException errors on fetch
  • Return early on fetch failure to prevent unhandled exceptions
tux/cogs/services/bookmarks.py
tux/cogs/utility/poll.py
Fix poll reaction handler to work in direct messages
  • Remove incompatible channel type check and always cast to TextChannel or Thread
  • Use fetch_channel fallback to handle uncached or DM channels
tux/cogs/utility/poll.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 and they look great!

Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments

### Comment 1
<location> `tux/cogs/services/bookmarks.py:45` </location>
<code_context>

-        if str(payload.emoji) != "🔖":
+        # Get the user who reacted to the message
+        user = self.bot.get_user(payload.user_id)
+        if user is None:
+            logger.error(f"User not found for ID: {payload.user_id}")
</code_context>

<issue_to_address>
get_user may return None for users not in cache.

Consider using await self.bot.fetch_user(payload.user_id) as a fallback to reliably retrieve the user if they are not in the cache.
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
        # Get the user who reacted to the message
        user = self.bot.get_user(payload.user_id)
        if user is None:
            logger.error(f"User not found for ID: {payload.user_id}")
            return
=======
        # Get the user who reacted to the message
        user = self.bot.get_user(payload.user_id)
        if user is None:
            try:
                user = await self.bot.fetch_user(payload.user_id)
            except discord.NotFound:
                logger.error(f"User not found for ID: {payload.user_id}")
                return
            except discord.HTTPException as fetch_exc:
                logger.error(f"HTTPException while fetching user {payload.user_id}: {fetch_exc}")
                return
>>>>>>> REPLACE

</suggested_fix>

### Comment 2
<location> `tux/cogs/services/bookmarks.py:41` </location>
<code_context>
         -------
         None
         """
+        if not self._is_valid_emoji(payload.emoji, self.valid_emojis):
+            return

</code_context>

<issue_to_address>
Consider replacing multiple emoji checks and if/elif branches with a single emoji-to-handler dictionary lookup for cleaner dispatch.

Here’s one way to collapse both of your `if/elif` emoji‐checks into a single lookup. You build one small dict mapping each emoji → a handler (and whether that handler needs you to build an embed), then in your listener you do one lookup and dispatch:

```python
class Bookmarks(commands.Cog):
    def __init__(self, bot: Tux) -> None:
        self.bot = bot

        # map emoji (ID or name) → (handler, needs_embed)
        self.emoji_actions: dict[int|str, tuple[Callable, bool]] = {
            **{e: (self._send_bookmark, True)  for e in CONFIG.ADD_BOOKMARK},
            **{e: (self._delete_bookmark, False) for e in CONFIG.REMOVE_BOOKMARK},
        }

    @commands.Cog.listener()
    async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent) -> None:
        # Fetch user/channel/message first…
        user = self.bot.get_user(payload.user_id)
        if not user:
            logger.error(f"User not found: {payload.user_id}")
            return

        channel = self.bot.get_channel(payload.channel_id)
        if not channel:
            logger.error(f"Channel not found: {payload.channel_id}")
            return
        channel = cast(discord.TextChannel|discord.Thread, channel)

        try:
            message = await channel.fetch_message(payload.message_id)
        except discord.NotFound:
            logger.error(f"Message not found: {payload.message_id}")
            return
        except (discord.Forbidden, discord.HTTPException) as e:
            logger.error(f"Failed fetch: {e}")
            return

        # single lookup
        key = payload.emoji.id or payload.emoji.name
        action = self.emoji_actions.get(key)
        if not action:
            return

        handler, needs_embed = action
        if needs_embed:
            embed = await self._create_bookmark_embed(message)
            await handler(user, message, embed, payload.emoji)
        else:
            await handler(message, user)
```

And then keep your two small handlers untouched:

```python
async def _delete_bookmark(self, message: discord.Message, user: discord.User) -> None:
    if message.author is not self.bot.user:
        return
    await message.delete()

@staticmethod
async def _send_bookmark(
    user: discord.User,
    message: discord.Message,
    embed: discord.Embed,
    emoji: discord.PartialEmoji
) -> None:
    …
```

This:

- Drops all repeated `_is_valid_emoji` checks
- Collapses the `if/elif` into one `dict.get`
- Keeps both add/remove routines in tiny focused methods.
</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.

@cherryl1k
Copy link
Collaborator Author

not sure why its getting giving an error for config,py with the tests

@electron271 electron271 self-requested a review June 19, 2025 17:57
@cherryl1k
Copy link
Collaborator Author

don't merge yet im trying to figure out what happened with git and why it was having a stroke

@cherryl1k
Copy link
Collaborator Author

it should be good now

@cherryl1k cherryl1k changed the title [Feature] Bookmark enhancments [Feature] Bookmark enhancements Jun 20, 2025
@anemoijereja-eden anemoijereja-eden self-assigned this Jun 20, 2025
@anemoijereja-eden
Copy link
Collaborator

Please use the conventional commit styling for commit messages, and use rebase to pull from main instead of creating a merge commit every time you update the branch.

@anemoijereja-eden
Copy link
Collaborator

definitely going to have to squash merge this to keep the git history looking somewhat reasonable

@anemoijereja-eden anemoijereja-eden marked this pull request as draft June 20, 2025 17:01
@cherryl1k
Copy link
Collaborator Author

this is hell

@cherryl1k
Copy link
Collaborator Author

im going to close this cause this is a mess and i should be using a feature branch for this

@cherryl1k cherryl1k closed this Jun 20, 2025
@cherryl1k cherryl1k changed the title [Feature] Bookmark enhancements feat:(bookmarks) added removing bookmarks Jun 21, 2025
@cherryl1k
Copy link
Collaborator Author

reopening this apologies about the messy commit history and the lack of a proper branch my other branch is completely wack

@cherryl1k cherryl1k reopened this Jun 21, 2025
@cherryl1k cherryl1k marked this pull request as ready for review June 21, 2025 18:15
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:

  • Consider using a list or set for valid_add_emojis/valid_remove_emojis instead of a single string so emoji lookups aren’t matching substrings of the emoji characters.
  • Extract the repeated fetch‐or‐get logic for users and channels into a shared helper to avoid duplicating error handling in both cogs.
  • Verify that _delete_bookmark deletes the DM bookmark message (sent by the bot) rather than the original source message to avoid accidentally removing content from the server.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- Consider using a list or set for valid_add_emojis/valid_remove_emojis instead of a single string so emoji lookups aren’t matching substrings of the emoji characters.
- Extract the repeated fetch‐or‐get logic for users and channels into a shared helper to avoid duplicating error handling in both cogs.
- Verify that `_delete_bookmark` deletes the DM bookmark message (sent by the bot) rather than the original source message to avoid accidentally removing content from the server.

## Individual Comments

### Comment 1
<location> `tux/cogs/services/bookmarks.py:21` </location>
<code_context>
+        self.valid_emojis = CONST.ADD_BOOKMARK + CONST.REMOVE_BOOKMARK
+
+    # The linter wants to change this but it breaks when it does that
+    def _is_valid_emoji(self, emoji: discord.PartialEmoji, valid_list: str) -> bool:
+        if emoji.name in valid_list:  # noqa: SIM103
+            return True
+        return False
</code_context>

<issue_to_address>
The use of 'in' with a string for emoji validation may lead to false positives.

Checking 'in' on a string can match substrings, not just exact emoji names. Use a list or tuple and check for equality to ensure only valid emojis are matched.
</issue_to_address>

### Comment 2
<location> `tux/cogs/services/bookmarks.py:16` </location>
<code_context>
     def __init__(self, bot: Tux) -> None:
         self.bot = bot

+        self.valid_add_emojis = CONST.ADD_BOOKMARK
+        self.valid_remove_emojis = CONST.REMOVE_BOOKMARK
+        self.valid_emojis = CONST.ADD_BOOKMARK + CONST.REMOVE_BOOKMARK
+
+    # The linter wants to change this but it breaks when it does that
</code_context>

<issue_to_address>
Storing valid emojis as concatenated strings may cause ambiguity.

Concatenating emojis into a string can lead to incorrect substring matches and iteration issues, particularly with multi-codepoint emojis. Consider using a list or tuple to store valid emojis for more reliable handling.

Suggested implementation:

```python
        self.valid_add_emojis = list(CONST.ADD_BOOKMARK)
        self.valid_remove_emojis = list(CONST.REMOVE_BOOKMARK)
        self.valid_emojis = self.valid_add_emojis + self.valid_remove_emojis

```

```python
    def _is_valid_emoji(self, emoji: discord.PartialEmoji, valid_list: list[str]) -> bool:
        return emoji.name in valid_list

```

If `CONST.ADD_BOOKMARK` and `CONST.REMOVE_BOOKMARK` are not already sequences (e.g., lists or tuples), but are strings of emojis, this change will treat each emoji as a single Unicode codepoint. If your emojis are multi-codepoint (e.g., emojis with skin tone modifiers), you may need to explicitly define them as lists of emoji strings in the constants file for full correctness.
</issue_to_address>

### Comment 3
<location> `tux/cogs/services/bookmarks.py:111` </location>
<code_context>
-
         return embed

+    async def _delete_bookmark(self, message: discord.Message, user: discord.User) -> None:
+        if message.author is not self.bot.user:
+            return
+        await message.delete()
</code_context>

<issue_to_address>
Using 'is not' for user comparison may not be reliable.

Compare user IDs instead of object instances to ensure correct user matching.
</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.

@kzndotsh
Copy link
Contributor

@sourcery-ai dismiss

@kzndotsh
Copy link
Contributor

@sourcery-ai review

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:

  • Deleting the original message in _delete_bookmark removes the source message rather than the DM bookmark—consider tracking and deleting the DM message object instead.
  • User/channel fetch and error-handling code is duplicated in both cogs—extract a shared helper to DRY this logic.
  • The _is_valid_emoji signature and linter workaround suggest its input type could be clearer; consider accepting an iterable of emoji names and simplifying to return emoji.name in valid_emojis.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- Deleting the original message in `_delete_bookmark` removes the source message rather than the DM bookmark—consider tracking and deleting the DM message object instead.
- User/channel fetch and error-handling code is duplicated in both cogs—extract a shared helper to DRY this logic.
- The `_is_valid_emoji` signature and linter workaround suggest its input type could be clearer; consider accepting an iterable of emoji names and simplifying to `return emoji.name in valid_emojis`.

## Individual Comments

### Comment 1
<location> `tux/cogs/services/bookmarks.py:111` </location>
<code_context>
-
         return embed

+    async def _delete_bookmark(self, message: discord.Message, user: discord.User) -> None:
+        if message.author is not self.bot.user:
+            return
+        await message.delete()
</code_context>

<issue_to_address>
Using 'is not' for author comparison may not be reliable.

Object identity isn't guaranteed; compare author IDs instead for accuracy.
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
        if message.author is not self.bot.user:
            return
=======
        if message.author.id != self.bot.user.id:
            return
>>>>>>> REPLACE

</suggested_fix>

### Comment 2
<location> `tux/cogs/services/bookmarks.py:139` </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 or document the potential exception if adding a reaction to a DM fails due to permission issues.
</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.

Comment on lines +112 to +113
if message.author is not self.bot.user:
return
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): Using 'is not' for author comparison may not be reliable.

Object identity isn't guaranteed; compare author IDs instead for accuracy.

Suggested change
if message.author is not self.bot.user:
return
if message.author.id != self.bot.user.id:
return

Comment on lines +139 to +140
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: Adding a reaction to the DM message may fail if the bot lacks permissions.

Handle or document the potential exception if adding a reaction to a DM fails due to permission issues.

@kzndotsh kzndotsh closed this Jun 21, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants