Skip to content

Conversation

@Zingzy
Copy link
Collaborator

@Zingzy Zingzy commented Jun 5, 2025

Reducing code reusability and complexity and improving maintainability. Consolidate error handling and embed management across commands, enhancing user feedback and experience. Introduce centralized logging for image generation processes and streamline image request handling with the new ImageRequestBuilder

Description

Brief description of the changes made in this PR.

Type of Change

Please delete options that are not relevant:

  • Code refactoring (no functional changes)

Testing

Please describe the tests that you ran to verify your changes:

  • Tested locally with a test Discord server
  • All commands work as expected
  • Error scenarios and edge cases tested
  • Proper logging output verified
  • Code formatted with uvx ruff format
  • Code linted with uvx ruff check --fix

Checklist

  • My code follows the style guidelines of this project
  • 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
  • My changes generate no new warnings or errors
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published

Summary by Sourcery

Refactor all core image command cogs to adopt a new extensible architecture: introduce a shared BaseCommandCog for common logic, leverage BaseImageView and command-specific views for UI buttons, and unify image request handling with ImageRequestBuilder. Consolidate error handling and logging, eliminate duplication, and add comprehensive documentation for the new design.

New Features:

  • Introduce ImageRequestBuilder for fluent, centralized construction of image generation requests
  • Add BaseCommandCog to encapsulate common command logic, including error handling and logging
  • Create BaseImageView and specific view subclasses (ImagineView, CrossPollinateView, MultiPollinateView, BeemojiView) for UI components

Enhancements:

  • Refactor all image command cogs to extend BaseCommandCog and use the new view architecture, reducing duplication and complexity
  • Centralize error handling, embed creation, and logging within base classes to ensure consistent behavior
  • Streamline image generation workflows by replacing manual URL construction with the builder pattern

Documentation:

  • Add REFACTORING_GUIDE.md to describe the new architecture, migration steps, and overall refactoring benefits

Chores:

  • Remove obsolete utility functions and duplicated code in individual cogs
  • Update filesystem helper to exclude base_command_cog.py from dynamic loading

Zingzy and others added 3 commits June 4, 2025 18:18
…omplexity and improving maintainability. Consolidate error handling and embed management across commands, enhancing user feedback and experience. Introduce centralized logging for image generation processes and streamline image request handling with the new ImageRequestBuilder.
…e. Update file listing logic in fs.py to exclude base_command_cog.py from Python file filtering, enhancing functionality.
…ation URL for consistency. Modify cross_pollinate_cog.py to include validate_prompt import for enhanced prompt validation.
@sourcery-ai
Copy link

sourcery-ai bot commented Jun 5, 2025

Reviewer's Guide

This PR refactors the bot’s command cogs and utilities to a new, layered architecture by introducing a BaseCommandCog for shared logic, a unified Views system for UI components, and an ImageRequestBuilder for centralized URL construction, dramatically reducing duplication and improving maintainability.

Sequence Diagram: Refactored Command Execution Flow

sequenceDiagram
    actor User
    participant ImagineCmd as "/pollinate"
    participant ImagineCog as Imagine (Cog)
    participant BaseCog as BaseCommandCog
    participant ImgGenUtils as image_gen_utils
    participant ImgReqBuilder as ImageRequestBuilder
    participant DiscordAPI as Discord API
    participant ExternalImageAPI as Image API

    User->>DiscordAPI: Executes /pollinate command
    DiscordAPI->>ImagineCog: imagine_command(interaction, prompt, ...)
    ImagineCog->>BaseCog: self.log_generation_start(...)
    ImagineCog->>ImgGenUtils: generate_image(prompt, ...)
    activate ImgGenUtils
    ImgGenUtils->>ImgReqBuilder: builder = ImageRequestBuilder.for_standard_generation(prompt)
    ImgGenUtils->>ImgReqBuilder: builder.with_dimensions(...)
    ImgGenUtils->>ImgReqBuilder: builder.build_request_data()
    ImgReqBuilder-->>ImgGenUtils: request_data (incl. URL)
    ImgGenUtils->>ExternalImageAPI: GET request_data.url
    ExternalImageAPI-->>ImgGenUtils: image_data, metadata
    deactivate ImgGenUtils
    ImgGenUtils-->>ImagineCog: (dic, image_file)
    ImagineCog->>BaseCog: self.log_generation_complete(...)
    ImagineCog->>ImagineCog: embed = generate_pollinate_embed(...)
    ImagineCog->>ImagineCog: view = self.get_view_class()()
    ImagineCog->>BaseCog: self.send_response(interaction, embed, file=image_file, view=view)
    BaseCog->>DiscordAPI: Followup response with embed, image, view
    DiscordAPI-->>User: Displays image and buttons
Loading

Sequence Diagram: Refactored 'Edit' Button Interaction

sequenceDiagram
    actor User
    participant ImagineViewInst as ImagineView
    participant EditModal as EditImageModal
    participant ImgGenUtils as image_gen_utils
    participant ImgReqBuilder as ImageRequestBuilder
    participant ExternalImageAPI as Image API
    participant DiscordAPI as Discord API

    User->>DiscordAPI: Clicks "Edit" button
    DiscordAPI->>ImagineViewInst: edit(interaction)
    ImagineViewInst->>DiscordAPI: interaction.response.send_modal(EditModal(...))
    DiscordAPI-->>User: Shows EditImageModal
    User->>DiscordAPI: Submits EditImageModal (with edit_prompt)
    DiscordAPI->>EditModal: on_submit(interaction)
    activate EditModal
    EditModal->>ImgGenUtils: generate_cross_pollinate(prompt=edit_prompt.value, image_url=original_image_url, ...)
    activate ImgGenUtils
    ImgGenUtils->>ImgReqBuilder: builder = ImageRequestBuilder.for_cross_pollination(...)
    ImgGenUtils->>ImgReqBuilder: builder.build_request_data()
    ImgReqBuilder-->>ImgGenUtils: request_data (incl. URL)
    ImgGenUtils->>ExternalImageAPI: GET request_data.url
    ExternalImageAPI-->>ImgGenUtils: edited_image_data, metadata
    deactivate ImgGenUtils
    ImgGenUtils-->>EditModal: (dic, edited_image_file)
    EditModal->>EditModal: embed = generate_cross_pollinate_embed(...)
    EditModal->>DiscordAPI: interaction.followup.send(embed=embed, file=edited_image_file, view=CrossPollinateView())
    deactivate EditModal
    DiscordAPI-->>User: Displays edited image and new buttons
Loading

Class Diagram: Command Cog Hierarchy

classDiagram
    class BaseCommandCog {
        +bot: commands.Bot
        +command_name: str
        +command_config: dict
        +logger: Logger
        +__init__(bot, command_name_in_config)
        +cog_load()*
        +handle_command_error(interaction, error, **context)*
        +send_response(interaction, embed, file, ephemeral, view_instance)*
        +get_view_class() View
        #log_generation_start(model, dimensions, cached, action)
        #log_generation_complete(model, dimensions, generation_time, cached, action)
    }
    class Imagine {
        +__init__(bot)
        +get_view_class() ImagineView
        +imagine_command(interaction, prompt, ...)
    }
    class CrossPollinate {
        +__init__(bot)
        +get_view_class() CrossPollinateView
        +cross_pollinate_command(interaction, prompt, image, ...)
    }
    class Beemoji {
        +__init__(bot)
        +get_view_class() BeemojiView
        +beemoji_command(interaction, emoji1, emoji2, ...)
    }
    class MultiPollinate {
        +__init__(bot)
        +get_view_class() MultiPollinateView
        +multiimagine_command(interaction, prompt, ...)
    }
    class RandomImage {
        +__init__(bot)
        +get_view_class() CrossPollinateView
        +random_image_command(interaction, ...)
    }

    BaseCommandCog <|-- Imagine
    BaseCommandCog <|-- CrossPollinate
    BaseCommandCog <|-- Beemoji
    BaseCommandCog <|-- MultiPollinate
    BaseCommandCog <|-- RandomImage
Loading

Class Diagram: UI View Hierarchy

classDiagram
    class BaseImageView {
        +timeout: Optional[float]
        +__init__(timeout)
        +setup_common_buttons()* 
        +interaction_check(interaction)* bool
        +handle_delete_button(interaction)*
        +handle_bookmark_button(interaction)*
        +get_original_data(interaction)* dict
        +get_prompt_from_embed(embed) str
        #_get_bookmark_type() str
    }
    class ImagineView {
        +setup_buttons()* 
        +regenerate(interaction)*
        +edit(interaction)*
    }
    class CrossPollinateView {
        +setup_buttons()* 
        +edit(interaction)*
    }
    class MultiPollinateView {
        +image_count: int
        +setup_buttons()* 
        +handle_upscale_button(interaction, index)*
    }
    class BeemojiView {
        +emoji1_name: str
        +emoji2_name: str
        +generated_name: str
        +setup_buttons()* 
        +edit(interaction)*
        +add_to_server(interaction)*
        +reduce_image_size(image_data, width, height) bytes
    }
    class EditImageModal {
        +original_image_url: str
        +original_prompt: str
        +edit_prompt: TextInput
        +__init__(original_image_url, original_prompt)
        +on_submit(interaction)*
    }

    BaseImageView <|-- ImagineView
    BaseImageView <|-- CrossPollinateView
    BaseImageView <|-- MultiPollinateView
    BaseImageView <|-- BeemojiView

    ImagineView ..> EditImageModal : "uses"
    CrossPollinateView ..> EditImageModal : "uses"
    BeemojiView ..> EditImageModal : "uses for edit"
Loading

Class Diagram: Image Request Construction

classDiagram
    class ImageRequest {
        +prompt: Optional[str]
        +width: int
        +height: int
        +model: Optional[str]
        +safe: bool
        +cached: bool
        +nologo: bool
        +enhance: bool
        +private: bool
        +negative: Optional[str]
        +seed: Optional[str]
        +image_url: Optional[str]
        +to_dict() dict
    }
    class ImageRequestBuilder {
        -base_url: str
        -request: ImageRequest
        +__init__(base_url)
        +with_prompt(prompt) ImageRequestBuilder
        +with_dimensions(width, height) ImageRequestBuilder
        +with_model(model) ImageRequestBuilder
        +with_source_image(image_url) ImageRequestBuilder
        +build_url() str
        +build_request_data() dict
        +for_standard_generation(prompt, **kwargs)$ ImageRequestBuilder
        +for_cross_pollination(prompt, image_url, **kwargs)$ ImageRequestBuilder
        +for_random_generation(**kwargs)$ ImageRequestBuilder
    }
    class MultiImageRequestBuilder {
        -base_request: ImageRequest
        -models: list[str]
        +__init__(base_request)
        +with_base_prompt(prompt) MultiImageRequestBuilder
        +build_requests() list[ImageRequestBuilder]
    }

    ImageRequestBuilder "1" *-- "1" ImageRequest : aggregates
    MultiImageRequestBuilder "1" *-- "1" ImageRequest : aggregates
    MultiImageRequestBuilder ..> ImageRequestBuilder : creates
Loading

File-Level Changes

Change Details Files
Introduce BaseCommandCog for shared command logic
  • Add BaseCommandCog with standardized error handling, logging, and response methods
  • Refactor all command cogs to extend BaseCommandCog, removing duplicated cooldown/validation and embed logic
  • Replace in-cog error handlers with BaseCommandCog.handle_command_error
  • Adopt log_generation_start/complete and send_response utilities in cogs
cogs/base_command_cog.py
cogs/imagine_cog.py
cogs/cross_pollinate_cog.py
cogs/multi_pollinate_cog.py
cogs/random_cog.py
cogs/beemoji_cog.py
Migrate UI components into a unified Views architecture
  • Create BaseImageView for common delete/bookmark buttons
  • Extract and refactor command-specific buttons and modals into separate view modules
  • Register each view in its cog via get_view_class or add_view
  • Remove inline UI and modal classes from cogs
views/base_view.py
views/imagine_view.py
views/cross_pollinate_view.py
views/multi_pollinate_view.py
views/beemoji_view.py
views/__init__.py
Centralize request URL construction with ImageRequestBuilder
  • Add ImageRequestBuilder and MultiImageRequestBuilder using builder pattern
  • Refactor generate_image, generate_cross_pollinate, generate_random_image to use the builder
  • Remove manual URL concatenation and seed logic from utils and cogs
utils/image_request_builder.py
utils/image_gen_utils.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

@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 @Zingzy - I've reviewed your changes - here's some feedback:

  • BaseCommandCog.send_response ignores file and view when ephemeral=True, so ephemeral responses cannot include attachments or interactive buttons; consider including them for consistent UX.
  • You can DRY up your cog setup by using the provided setup_base_cog helper in each setup function instead of manually calling bot.add_cog and logging.
  • The manual string concatenation in ImageRequestBuilder.build_url can miss edge‐case escaping—consider using urllib.parse.urlencode to assemble query parameters more robustly.
Here's what I looked at during the review
  • 🟡 General issues: 6 issues found
  • 🟢 Security: all looks good
  • 🟢 Testing: all looks good
  • 🟡 Complexity: 3 issues found
  • 🟢 Documentation: all looks good

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.


return (request_data, image_file)

except aiohttp.ClientError as e:
Copy link

Choose a reason for hiding this comment

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

suggestion (bug_risk): APIError is raised with only the string message, which may lose status code context.

Consider passing the status code to APIError if available, to preserve error context for downstream handlers.

else:
await response.edit(embeds=embeds, attachments=files)
# For multi-pollinate, we need to edit the response with the grid
view = MultiPollinateView(image_count=len(embeds))
Copy link

Choose a reason for hiding this comment

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

issue: MultiPollinateView is instantiated with image_count, but get_view_class does not pass this parameter.

If get_view_class is used elsewhere without image_count, this could cause inconsistent behavior. Please standardize how MultiPollinateView is instantiated or clearly document the image_count requirement.

"""Handle button interactions."""
custom_id = interaction.data["custom_id"]

if custom_id.startswith("U"):
Copy link

Choose a reason for hiding this comment

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

issue (bug_risk): Index parsing only handles single-digit custom_ids

Parsing only custom_id[1] fails for multi-digit indices. Use int(custom_id[1:]) to handle all cases correctly.

)
return

# Get the original image URL from the message embed
Copy link

Choose a reason for hiding this comment

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

issue (bug_risk): Only checks the first embed for an image

Iterate through all embeds to locate the first one with an image, rather than assuming the first embed always contains it.


async def interaction_check(self, interaction: discord.Interaction) -> bool:
"""Handle button interactions."""
custom_id = interaction.data["custom_id"]
Copy link

Choose a reason for hiding this comment

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

issue (bug_risk): Direct access to interaction.data['custom_id'] may throw KeyError

Use interaction.data.get('custom_id') or check if 'custom_id' exists before accessing it to prevent KeyError.

return (request_data, image_file)

except aiohttp.ClientError as e:
raise APIError(f"Network error occurred: {str(e)}")
Copy link

Choose a reason for hiding this comment

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

suggestion (code-quality): Explicitly raise from a previous error (raise-from-previous-error)

Suggested change
raise APIError(f"Network error occurred: {str(e)}")
raise APIError(f"Network error occurred: {str(e)}") from e

return (request_data, image_file)

except aiohttp.ClientError as e:
raise APIError(f"Network error occurred: {str(e)}")
Copy link

Choose a reason for hiding this comment

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

suggestion (code-quality): Explicitly raise from a previous error (raise-from-previous-error)

Suggested change
raise APIError(f"Network error occurred: {str(e)}")
raise APIError(f"Network error occurred: {str(e)}") from e

"Authorization": f"Bearer {config.api.api_key}",
}

try:
Copy link

Choose a reason for hiding this comment

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

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

Comment on lines +121 to +124
# Add dimensions
params.append(f"width={self.request.width}")
params.append(f"height={self.request.height}")

Copy link

Choose a reason for hiding this comment

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

suggestion (code-quality): Merge consecutive list appends into a single extend (merge-list-appends-into-extend)

Suggested change
# Add dimensions
params.append(f"width={self.request.width}")
params.append(f"height={self.request.height}")
params.extend((f"width={self.request.width}", f"height={self.request.height}"))

Comment on lines +27 to +33
# Find the prompt field (different commands use different field names)
prompt = "Unknown prompt"
for field in interaction_data.get("fields", []):
if field["name"] in ["Prompt", "Cross-Pollinate Prompt 🐝"]:
prompt = field["value"][3:-3] # Remove ``` code block markers
break

Copy link

Choose a reason for hiding this comment

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

suggestion (code-quality): Use the built-in function next instead of a for-loop (use-next)

Suggested change
# Find the prompt field (different commands use different field names)
prompt = "Unknown prompt"
for field in interaction_data.get("fields", []):
if field["name"] in ["Prompt", "Cross-Pollinate Prompt 🐝"]:
prompt = field["value"][3:-3] # Remove ``` code block markers
break
prompt = next(
(
field["value"][3:-3]
for field in interaction_data.get("fields", [])
if field["name"] in ["Prompt", "Cross-Pollinate Prompt 🐝"]
),
"Unknown prompt",
)

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.

2 participants