Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 67 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,74 @@ async def resource(self, interaction, action: str):
### Group Cogs
For complex features with multiple distinct sub-functions, use `commands.GroupCog`.

## 4. Error Handling
## 4. Internal DM Service

Direct messaging is an internal service, **not** a user-facing cog. Do not add `/dm`-style command surfaces for bulk messaging.

### Location
Use:

* `capy_discord.services.dm`
* `capy_discord.services.policies`

### Safety Model
* DM sends are **deny-all by default** via `policies.DENY_ALL`.
* Developers must opt into explicit allowlists with helpers like `policies.allow_users(...)`, `policies.allow_roles(...)`, or `policies.allow_targets(...)`.
* The service rejects `@everyone`, rejects targets outside the allowed policy, and enforces `max_recipients`.

### Usage Pattern
Developers should think in terms of:

1. The exact user or role to DM.
2. The predefined policy that permits that target.

Prefer explicit entrypoints over generic audience bags:

```python
from capy_discord.services import dm, policies

EVENT_POLICY = policies.allow_roles(EVENT_ROLE_ID, max_recipients=20)

draft = await dm.compose_to_role(
guild,
EVENT_ROLE_ID,
"Reminder: event starts at 7 PM.",
policy=EVENT_POLICY,
)
result = await dm.send(guild, draft)
```

For self-test or single-user flows, use `dm.compose_to_user(...)` with `policies.allow_users(...)`.

### Cog Usage
If you need an operator-facing entrypoint for DM functionality, keep it narrow and task-specific rather than exposing a generic DM surface.

Use a small cog command only for explicit, safe flows such as self-test notifications:

```python
from capy_discord.services import dm, policies

policy = policies.allow_users(interaction.user.id, max_recipients=1)

draft = await dm.compose_to_user(
guild,
interaction.user.id,
message,
policy=policy,
)
self.log.info("Notify preview\n%s", dm.render_preview(draft))

result = await dm.send(guild, draft)
```

This pattern is implemented in `capy_discord/exts/tools/notify.py`. Do not add broad `/dm` or bulk-message commands; use explicit commands tied to a specific feature or operational workflow.

## 5. Error Handling
We use a global `on_tree_error` handler in `bot.py`.
* Exceptions are logged with the specific module name.
* Do not wrap every command in `try/except` blocks unless handling specific business logic errors.

## 5. Logging
## 6. Logging
All logs follow a standardized format for consistency across the console and log files.

* **Format**: `[{asctime}] [{levelname:<8}] {name}: {message}`
Expand All @@ -113,12 +175,12 @@ self.log = logging.getLogger(__name__)
self.log.info("Starting feature X")
```

## 6. Time and Timezones
## 7. Time and Timezones
**Always use `zoneinfo.ZoneInfo`**.
* **Storage**: `UTC`.
* **Usage**: `datetime.now(ZoneInfo("UTC"))`.

## 7. Development Workflow
## 8. Development Workflow

### Linear & Branching
* **Issue Tracking**: Every task must have a Linear issue.
Expand Down Expand Up @@ -146,7 +208,7 @@ Format: `<type>(<scope>): <subject>`
2. **Reviewers**: Must include `Shamik` and `Jason`.
3. **Checks**: All CI checks (Lint, Test, Build) must pass.

## 8. Cog Standards
## 9. Cog Standards

### Initialization
All Cogs **MUST** accept the `bot` instance in `__init__`. The use of the global `capy_discord.instance` is **deprecated** and should not be used in new code.
Expand Down
5 changes: 4 additions & 1 deletion capy_discord/exts/guild/guild.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,10 @@ async def _open_channels(self, interaction: discord.Interaction, settings: Guild

async def _open_roles(self, interaction: discord.Interaction, settings: GuildSettings) -> None:
"""Launch the role settings modal pre-filled with current values."""
initial = {"admin": settings.admin_role, "member": settings.member_role}
initial = {
"admin": settings.admin_role,
"member": settings.member_role,
}
modal = ModelModal(
model_cls=RoleSettingsForm, callback=self._handle_roles, title="Role Settings", initial_data=initial
)
Expand Down
47 changes: 47 additions & 0 deletions capy_discord/exts/tools/notify.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""Safe notification command for testing the internal DM module."""

import logging

import discord
from discord import app_commands
from discord.ext import commands

from capy_discord.errors import UserFriendlyError
from capy_discord.services import dm, policies


class Notify(commands.Cog):
"""Cog for sending a self-targeted test DM."""

def __init__(self, bot: commands.Bot) -> None:
"""Initialize the Notify cog."""
self.bot = bot
self.log = logging.getLogger(__name__)
self.log.info("Notify cog initialized")

@app_commands.command(name="notify", description="Send yourself a test DM")
@app_commands.describe(message="Message content to send to your own DMs")
@app_commands.guild_only()
async def notify(self, interaction: discord.Interaction, message: str) -> None:
"""Send a test DM to the invoking user."""
guild = interaction.guild
if guild is None:
await interaction.response.send_message("This must be used in a server.", ephemeral=True)
return

policy = policies.allow_users(interaction.user.id, max_recipients=1)

draft = await dm.compose_to_user(guild, interaction.user.id, message, policy=policy)
self.log.info("Notify preview\n%s", dm.render_preview(draft))

result = await dm.send(guild, draft)
if result.sent_count != 1:
msg = "Failed to send test DM to the invoking user."
raise UserFriendlyError(msg, "I couldn't DM you. Check your Discord privacy settings and try again.")

await interaction.response.send_message("Sent you a DM.", ephemeral=True)


async def setup(bot: commands.Bot) -> None:
"""Set up the Notify cog."""
await bot.add_cog(Notify(bot))
Empty file removed capy_discord/guild/__init__.py
Empty file.
49 changes: 0 additions & 49 deletions capy_discord/guild/_schemas.py

This file was deleted.

Loading
Loading