Skip to content

Architecture and Patterns

Shamik Karkhanis edited this page Jan 28, 2026 · 2 revisions

Architecture and Patterns

This document describes the architectural decisions and coding patterns used in capy-discord to ensure a consistent and scalable codebase.

1. Directory Structure & Conventions

We follow a flexible modular structure within capy_discord/exts/.

Cog Guidelines

  1. Naming: Cogs are named based on their primary function (e.g., ping, profile).
  2. Simple Cogs: Place single-file cogs directly in exts/ or a grouping subdirectory.
  3. Complex Cogs: Features requiring helpers or schemas get their own directory (e.g., exts/profile/).
    • The main file shares the directory's name (profile.py).
    • Non-cog helpers (schemas, views, etc.) must be prefixed with an underscore (e.g., _schemas.py) to prevent the extension loader from reading them.
  4. Grouping: Use directories like exts/tools/ to group similar simple cogs.
  5. Setup: All cogs must define a def setup(bot): function.
capy_discord/
├── exts/
│   ├── guild.py            # Simple Cog
│   ├── tools/              # Grouping for similar cogs
│   │   ├── ping.py
│   │   └── sync.py
│   ├── profile/            # Complex Cog (Directory)
│   │   ├── profile.py      # Main Cog File
│   │   └── _schemas.py     # Helper (ignored by loader)
│   └── __init__.py
├── ui/
│   ├── forms.py            # Model-driven UI (Pydantic forms)
│   ├── modal.py            # Base UI components
│   └── ...
└── bot.py

2. UI Patterns

We utilize high-level abstractions to eliminate the boilerplate associated with Discord UI interactions.

Forms & Data Collection (ui/forms.py)

Standard: Use ModelModal for all user data input.

This class handles all the code overhead automatically, so you should not subclass BaseModal manually for standard forms.

  • Auto-Generation: Converts Pydantic fields into Discord TextInputs.
  • Validation: Validates user input against the schema upon submission.
  • Retry Flow: Automatically handles validation errors by showing users a "Fix Errors" button that restores their previous input.

Example:

from pydantic import BaseModel, Field
from capy_discord.ui.forms import ModelModal

class UserProfile(BaseModel):
    name: str = Field(title="Display Name", max_length=20)
    bio: str = Field(title="Biography", max_length=200, default="")

async def edit_profile(self, interaction):
    # No manual modal class needed!
    modal = ModelModal(UserProfile, callback=self.save_profile, title="Edit Profile")
    await interaction.response.send_modal(modal)

Interactive Views (ui/views.py)

Standard: Inherit from BaseView for buttons, selects, and other interactive components.

Unlike forms, Views are highly custom, so you should subclass BaseView. It provides essential lifecycle safety that raw discord.ui.View lacks.

  • Timeout Handling: Automatically disables buttons/selects and marks the message as "[Timed Out]" when the view expires.
  • Error Safety: Catches exceptions in callbacks and sends a friendly ephemeral error message to the user, while logging the traceback for developers.
  • Message Tracking: Use view.reply(interaction, ...) to automatically link the view to its message for updates.

Example:

from capy_discord.ui.views import BaseView

class ConfirmAction(BaseView):
    @discord.ui.button(label="Confirm", style=discord.ButtonStyle.green)
    async def confirm(self, interaction: discord.Interaction, button: discord.ui.Button):
        await interaction.response.send_message("Confirmed!", ephemeral=True)
        self.stop()

Simple Inputs (ui/modal.py)

For simple one-off inputs where a Pydantic model feels like overkill, use CallbackModal to delegate logic directly to a function.

from capy_discord.ui.modal import CallbackModal

# Quick, functional modal without class definition overhead
modal = CallbackModal(callback=my_handler, title="Quick Input")

Base Classes (Internal)

  • BaseModal (ui/modal.py): The internal foundation for ModelModal and CallbackModal. You generally won't use this directly.

3. Command Patterns

Action Choices (CRUD)

For commands managing a single resource (like /profile), use a single top-level command with a choice parameter.

@app_commands.choices(action=[
    Choice(name="create", value="create"),
    Choice(name="show", value="show"),
    # ...
])

Group Cogs

For complex features with multiple sub-functions, use commands.GroupCog.

4. Time and Timezones

Always use zoneinfo.ZoneInfo for timezone-aware datetimes.

  • Internal/DB storage: Use UTC.
  • Method: datetime.now(ZoneInfo("UTC")).

5. Error Handling & Tracing

We use a global on_tree_error handler in bot.py to ensure that even unhandled exceptions in slash commands are logged with the correct module's name (e.g., capy_discord.exts.social.profile) instead of the generic Discord logger. This allows for better debugging without needing try/except blocks in every command.