Skip to content

Revert "Feature/capr 52 dm module"#127

Merged
shamikkarkhanis merged 1 commit intodevelopfrom
revert-126-feature/capr-52-dm-module
Mar 17, 2026
Merged

Revert "Feature/capr 52 dm module"#127
shamikkarkhanis merged 1 commit intodevelopfrom
revert-126-feature/capr-52-dm-module

Conversation

@shamikkarkhanis
Copy link
Member

@shamikkarkhanis shamikkarkhanis commented Mar 17, 2026

Reverts #126

Summary by Sourcery

Remove the internal DM services feature and introduce a new guild settings management cog with in-memory configuration storage and commands for managing guild channels, roles, onboarding message, and settings summary.

New Features:

  • Add a guild settings cog that stores per-guild configuration in memory and exposes slash commands to manage channels, roles, onboarding welcome message, and view a summary of current settings.

Enhancements:

  • Define Pydantic schemas for guild settings forms and internal state to structure guild configuration data.
  • Tighten up existing guild role settings modal initialization for cleaner configuration handling.

Documentation:

  • Remove documentation describing the internal DM service and its usage patterns from the agents guide.

Chores:

  • Remove the DM service modules, policy helpers, and their associated tests from the codebase.

@linear
Copy link

linear bot commented Mar 17, 2026

Issue reopened: CAPR-52 dm module

@shamikkarkhanis shamikkarkhanis merged commit d520f96 into develop Mar 17, 2026
2 checks passed
@shamikkarkhanis shamikkarkhanis deleted the revert-126-feature/capr-52-dm-module branch March 17, 2026 20:16
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Mar 17, 2026

Reviewer's Guide

Reverts the internal DM service feature and related tooling while introducing a new guild settings cog with in-memory settings, slash commands for managing guild configuration, and basic Pydantic schemas for guild settings storage/forms.

Sequence diagram for guild channel settings slash command

sequenceDiagram
  actor Admin
  participant DiscordClient
  participant Bot
  participant GuildCog
  participant GuildSettingsStore as GuildSettings_store

  Admin->>DiscordClient: /guild channels reports=#reports announcements=#announcements
  DiscordClient->>Bot: InteractionCreate
  Bot->>GuildCog: guild_channels(interaction, reports, announcements, feedback)

  GuildCog->>GuildCog: _ensure_settings(guild_id)
  GuildCog->>GuildSettingsStore: get(guild_id)
  alt settings_missing
    GuildSettingsStore-->>GuildCog: None
    GuildCog->>GuildSettingsStore: create GuildSettings for guild_id
  else settings_exist
    GuildSettingsStore-->>GuildCog: GuildSettings
  end

  GuildCog->>GuildSettingsStore: update reports_channel, announcements_channel, feedback_channel
  GuildCog-->>Bot: send ephemeral "Channel settings saved"
  Bot-->>DiscordClient: InteractionResponse
  DiscordClient-->>Admin: Ephemeral confirmation message
Loading

Class diagram for new guild settings cog and schemas

classDiagram

class GuildCog {
  - bot: commands.Bot
  - log: logging.Logger
  - _store: dict[int, GuildSettings]
  + __init__(bot: commands.Bot)
  + _ensure_settings(guild_id: int) GuildSettings
  + guild_channels(interaction: discord.Interaction, reports: discord.TextChannel, announcements: discord.TextChannel, feedback: discord.TextChannel) void
  + guild_channels_clear(interaction: discord.Interaction, target: Literal) void
  + guild_roles(interaction: discord.Interaction, admin: discord.Role, member: discord.Role) void
  + guild_roles_clear(interaction: discord.Interaction, target: Literal) void
  + guild_onboarding(interaction: discord.Interaction, message: str) void
  + guild_summary(interaction: discord.Interaction) void
}

class GuildSettings {
  reports_channel: int | None
  announcements_channel: int | None
  feedback_channel: int | None
  admin_role: str | None
  member_roles: list[str]
  onboarding_welcome: str | None
}

class ChannelSettingsForm {
  reports: str
  announcements: str
  feedback: str
}

class RoleSettingsForm {
  admin: str
  member: str
}

class AnnouncementChannelForm {
  channel: str
}

class FeedbackChannelForm {
  channel: str
}

class WelcomeMessageForm {
  message: str
}

GuildCog --> GuildSettings : stores_per_guild
GuildCog ..> ChannelSettingsForm : related_form_model
GuildCog ..> RoleSettingsForm : related_form_model
GuildCog ..> AnnouncementChannelForm : related_form_model
GuildCog ..> FeedbackChannelForm : related_form_model
GuildCog ..> WelcomeMessageForm : related_form_model
Loading

File-Level Changes

Change Details Files
Remove internal DM service, policies, notify tooling, and their tests, rolling back the DM feature surface.
  • Delete internal DM service module and policy helper module used for controlled bulk/safe DMs.
  • Remove the notify tool cog that exposed a narrow operator-facing DM preview/send flow.
  • Drop the services package init re-exports and associated DM service tests, fully removing that feature area.
capy_discord/exts/tools/notify.py
capy_discord/services/__init__.py
capy_discord/services/dm.py
capy_discord/services/policies.py
tests/capy_discord/services/__init__.py
tests/capy_discord/services/test_dm.py
Introduce a new guild settings cog with slash commands backed by an in-memory settings store attached to the bot.
  • Implement a GuildCog that maintains per-guild settings in a bot-attached dict using a GuildSettings model.
  • Add slash-command group for managing channel IDs, roles, onboarding welcome text, and providing a human-readable summary.
  • Ensure commands are guild-only, respond ephemerally, validate interaction.guild, and use error_embed for DM misuse.
  • Provide a setup entrypoint that registers the GuildCog with the bot.
capy_discord/guild/guild.py
Add Pydantic schemas for guild settings and forms to support modal-based configuration and internal state.
  • Define form models for channel, role, announcement, feedback, and welcome message configuration with labeled fields.
  • Define a GuildSettings model capturing channels, roles, member roles, and onboarding welcome message as persisted state.
capy_discord/guild/_schemas.py
Tidy guild extension modal initialization and adjust AGENTS documentation structure after removing DM section.
  • Condense construction of initial role settings dict in the existing guild extension when launching the role settings modal.
  • Remove the Internal DM Service section from AGENTS documentation and renumber subsequent sections accordingly.
AGENTS.md
capy_discord/exts/guild/guild.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 - I've found 2 issues, and left some high level feedback:

  • There are now two guild-related modules (capy_discord/exts/guild/guild.py and capy_discord/guild/guild.py) that both deal with guild settings; consider consolidating these into a single cog or shared helper to avoid diverging logic and confusion over which one should be used.
  • The in-memory guild_settings_store attached to the bot will be lost on process restart and is shared across all shards/instances; if this is intended only as a temporary store, consider making that explicit in the design or wiring it through a persistence layer so settings survive restarts.
  • The repeated interaction.guild is None checks and error responses in each guild_* command could be factored into a small helper or decorator to keep the cog methods focused on their core behavior and reduce duplication.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- There are now two guild-related modules (`capy_discord/exts/guild/guild.py` and `capy_discord/guild/guild.py`) that both deal with guild settings; consider consolidating these into a single cog or shared helper to avoid diverging logic and confusion over which one should be used.
- The in-memory `guild_settings_store` attached to the bot will be lost on process restart and is shared across all shards/instances; if this is intended only as a temporary store, consider making that explicit in the design or wiring it through a persistence layer so settings survive restarts.
- The repeated `interaction.guild is None` checks and error responses in each `guild_*` command could be factored into a small helper or decorator to keep the cog methods focused on their core behavior and reduce duplication.

## Individual Comments

### Comment 1
<location path="capy_discord/guild/guild.py" line_range="38-47" />
<code_context>
+    @guild.command(name="channels", description="Set channel IDs in one line")
</code_context>
<issue_to_address>
**🚨 issue (security):** Guild settings commands are not permission-gated, allowing any member to mutate guild-wide configuration.

The `guild` subcommands (`channels*`, `roles*`, `onboarding`, `summary`) only use `@app_commands.guild_only()` and lack any permission/role checks, so any guild member can modify guild-wide settings. Please restrict these commands, e.g. with `@app_commands.default_permissions(manage_guild=True)` or a role-based check aligned with `admin_role`, while keeping read-only commands like `summary` more broadly accessible if desired.
</issue_to_address>

### Comment 2
<location path="capy_discord/guild/_schemas.py" line_range="23" />
<code_context>
+    member: str = Field(default="", title="Member Role", description="Role ID for general member access")
+
+
+class AnnouncementChannelForm(BaseModel):
+    """Form for setting the announcement channel."""
+
</code_context>
<issue_to_address>
**issue (complexity):** Consider consolidating the similar channel form models and aligning ID types between forms and `GuildSettings` to reduce redundancy and conversion overhead.

You can simplify this by (1) consolidating the very-similar form models and (2) normalizing ID types with `GuildSettings` to avoid future conversion glue.

### 1. Consolidate the single-field channel forms

`AnnouncementChannelForm` and `FeedbackChannelForm` are just single-field variants of the same concept. You can express them with a reusable model instead of two separate Pydantic classes:

```py
from pydantic import BaseModel, Field


class SingleChannelForm(BaseModel):
    """Generic form for configuring a single channel destination."""

    channel_id: int | None = Field(
        default=None,
        title="Channel",
        description="Channel ID",
    )

# Example specializations via helpers/factories (keeps behavior, reduces models):

def make_announcement_channel_form() -> SingleChannelForm:
    return SingleChannelForm.model_construct(
        channel_id=None,
        __pydantic_fields__={
            "channel_id": Field(
                default=None,
                title="Announcement Channel",
                description="Channel ID for global pings",
            ),
        },
    )

def make_feedback_channel_form() -> SingleChannelForm:
    return SingleChannelForm.model_construct(
        channel_id=None,
        __pydantic_fields__={
            "channel_id": Field(
                default=None,
                title="Feedback Channel",
                description="Channel ID for feedback routing",
            ),
        },
    )
```

This keeps the ability to have different titles/descriptions but avoids extra top-level models that look “different” but behave the same.

If you don’t need runtime factories, an even simpler variant is a single `SingleChannelForm` and you pass the label text from the UI layer instead of encoding it into the model.

### 2. Normalize ID types across forms and `GuildSettings`

Right now the forms use `str` for IDs while `GuildSettings` uses `int | None` for channels and `str` for roles. That guarantees extra parsing/branching later.

You can align all channel/role IDs to `int | None` in both the forms and `GuildSettings`:

```py
class ChannelSettingsForm(BaseModel):
    """Form for configuring guild channel destinations."""
    reports: int | None = Field(
        default=None,
        title="Reports Channel",
        description="Channel ID for bug reports",
    )
    announcements: int | None = Field(
        default=None,
        title="Announcements Channel",
        description="Channel ID for announcements",
    )
    feedback: int | None = Field(
        default=None,
        title="Feedback Channel",
        description="Channel ID for feedback routing",
    )


class RoleSettingsForm(BaseModel):
    """Form for configuring guild role scopes."""
    admin: int | None = Field(
        default=None,
        title="Admin Role",
        description="Role ID for administrator access",
    )
    member: int | None = Field(
        default=None,
        title="Member Role",
        description="Role ID for general member access",
    )


class GuildSettings(BaseModel):
    """Persisted guild settings (not a form — internal state)."""
    reports_channel: int | None = None
    announcements_channel: int | None = None
    feedback_channel: int | None = None
    admin_role: int | None = None
    member_roles: list[int] = Field(default_factory=list)
    onboarding_welcome: str | None = None
```

This removes the type mismatch between “form” and “internal state”, so future `GuildCog` code can assign directly:

```py
settings.reports_channel = form.reports
settings.admin_role = form.admin
```

instead of carrying conversion/parsing logic or mixed `str`/`int` handling in `guild_summary`.
</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 +38 to +47
@guild.command(name="channels", description="Set channel IDs in one line")
@app_commands.guild_only()
@app_commands.describe(
reports="Reports channel",
announcements="Announcements channel",
feedback="Feedback channel",
)
async def guild_channels(
self,
interaction: discord.Interaction,
Copy link
Contributor

Choose a reason for hiding this comment

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

🚨 issue (security): Guild settings commands are not permission-gated, allowing any member to mutate guild-wide configuration.

The guild subcommands (channels*, roles*, onboarding, summary) only use @app_commands.guild_only() and lack any permission/role checks, so any guild member can modify guild-wide settings. Please restrict these commands, e.g. with @app_commands.default_permissions(manage_guild=True) or a role-based check aligned with admin_role, while keeping read-only commands like summary more broadly accessible if desired.

member: str = Field(default="", title="Member Role", description="Role ID for general member access")


class AnnouncementChannelForm(BaseModel):
Copy link
Contributor

Choose a reason for hiding this comment

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

issue (complexity): Consider consolidating the similar channel form models and aligning ID types between forms and GuildSettings to reduce redundancy and conversion overhead.

You can simplify this by (1) consolidating the very-similar form models and (2) normalizing ID types with GuildSettings to avoid future conversion glue.

1. Consolidate the single-field channel forms

AnnouncementChannelForm and FeedbackChannelForm are just single-field variants of the same concept. You can express them with a reusable model instead of two separate Pydantic classes:

from pydantic import BaseModel, Field


class SingleChannelForm(BaseModel):
    """Generic form for configuring a single channel destination."""

    channel_id: int | None = Field(
        default=None,
        title="Channel",
        description="Channel ID",
    )

# Example specializations via helpers/factories (keeps behavior, reduces models):

def make_announcement_channel_form() -> SingleChannelForm:
    return SingleChannelForm.model_construct(
        channel_id=None,
        __pydantic_fields__={
            "channel_id": Field(
                default=None,
                title="Announcement Channel",
                description="Channel ID for global pings",
            ),
        },
    )

def make_feedback_channel_form() -> SingleChannelForm:
    return SingleChannelForm.model_construct(
        channel_id=None,
        __pydantic_fields__={
            "channel_id": Field(
                default=None,
                title="Feedback Channel",
                description="Channel ID for feedback routing",
            ),
        },
    )

This keeps the ability to have different titles/descriptions but avoids extra top-level models that look “different” but behave the same.

If you don’t need runtime factories, an even simpler variant is a single SingleChannelForm and you pass the label text from the UI layer instead of encoding it into the model.

2. Normalize ID types across forms and GuildSettings

Right now the forms use str for IDs while GuildSettings uses int | None for channels and str for roles. That guarantees extra parsing/branching later.

You can align all channel/role IDs to int | None in both the forms and GuildSettings:

class ChannelSettingsForm(BaseModel):
    """Form for configuring guild channel destinations."""
    reports: int | None = Field(
        default=None,
        title="Reports Channel",
        description="Channel ID for bug reports",
    )
    announcements: int | None = Field(
        default=None,
        title="Announcements Channel",
        description="Channel ID for announcements",
    )
    feedback: int | None = Field(
        default=None,
        title="Feedback Channel",
        description="Channel ID for feedback routing",
    )


class RoleSettingsForm(BaseModel):
    """Form for configuring guild role scopes."""
    admin: int | None = Field(
        default=None,
        title="Admin Role",
        description="Role ID for administrator access",
    )
    member: int | None = Field(
        default=None,
        title="Member Role",
        description="Role ID for general member access",
    )


class GuildSettings(BaseModel):
    """Persisted guild settings (not a form — internal state)."""
    reports_channel: int | None = None
    announcements_channel: int | None = None
    feedback_channel: int | None = None
    admin_role: int | None = None
    member_roles: list[int] = Field(default_factory=list)
    onboarding_welcome: str | None = None

This removes the type mismatch between “form” and “internal state”, so future GuildCog code can assign directly:

settings.reports_channel = form.reports
settings.admin_role = form.admin

instead of carrying conversion/parsing logic or mixed str/int handling in guild_summary.

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.

1 participant