-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture and Patterns
This document describes the architectural decisions and coding patterns used in capy-discord to ensure a consistent and scalable codebase.
We follow a flexible modular structure within capy_discord/exts/.
-
Naming: Cogs are named based on their primary function (e.g.,
ping,profile). -
Simple Cogs: Place single-file cogs directly in
exts/or a grouping subdirectory. -
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.
- The main file shares the directory's name (
-
Grouping: Use directories like
exts/tools/to group similar simple cogs. -
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
We utilize high-level abstractions to eliminate the boilerplate associated with Discord UI interactions.
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)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()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")-
BaseModal(ui/modal.py): The internal foundation forModelModalandCallbackModal. You generally won't use this directly.
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"),
# ...
])For complex features with multiple sub-functions, use commands.GroupCog.
Always use zoneinfo.ZoneInfo for timezone-aware datetimes.
-
Internal/DB storage: Use
UTC. -
Method:
datetime.now(ZoneInfo("UTC")).
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.