Skip to content

Feature/capr 52 dm module#126

Merged
shamikkarkhanis merged 3 commits intodevelopfrom
feature/capr-52-dm-module
Mar 17, 2026
Merged

Feature/capr 52 dm module#126
shamikkarkhanis merged 3 commits intodevelopfrom
feature/capr-52-dm-module

Conversation

@shamikkarkhanis
Copy link
Member

@shamikkarkhanis shamikkarkhanis commented Mar 17, 2026

Summary by Sourcery

Introduce a safety-focused internal direct messaging service with explicit policies and a narrow self-test command surface, and update documentation to codify its usage model.

New Features:

  • Add an internal DM service with policy-based audience validation, drafting, previewing, and batch send tracking.
  • Expose a /notify command that lets users send themselves a test DM using the internal DM service.
  • Provide policy helper utilities for building explicit DM allowlists by users, roles, or both.

Enhancements:

  • Document internal DM service patterns and safety model in the AGENTS guide, including usage examples and standards.
  • Export the new DM and policy service modules from the service package for centralized access.
  • Minor formatting cleanup in the guild role settings modal initialization.

Tests:

  • Add unit tests covering DM policy defaults, @everyone protection, audience resolution and deduplication, recipient cap enforcement, and send failure tracking.
  • Add a tests package initializer for service-layer tests.

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Mar 17, 2026

Reviewer's Guide

Introduces a safety-focused internal direct messaging service with policy-based audience allowlists, a small test /notify cog, and documentation describing the DM safety model, along with unit tests and minor formatting cleanup.

Sequence diagram for /notify self-test DM flow

sequenceDiagram
    actor User
    participant DiscordClient
    participant Bot
    participant NotifyCog
    participant Policies as policies_module
    participant DmFacade as dm_module
    participant Messenger as DirectMessenger
    participant DiscordAPI

    User->>DiscordClient: invoke slash command /notify message
    DiscordClient->>Bot: interaction create
    Bot->>NotifyCog: notify(interaction, message)

    NotifyCog->>NotifyCog: validate guild not None
    NotifyCog->>Policies: allow_users(interaction.user.id, max_recipients=1)
    Policies-->>NotifyCog: Policy

    NotifyCog->>DmFacade: compose_to_user(guild, interaction.user.id, message, policy)
    DmFacade->>Messenger: compose_to_user(guild, user_id, content, policy)
    Messenger->>Messenger: _resolve_policy(policy)
    Messenger->>Messenger: _normalize_content(content)
    Messenger->>Messenger: _validate_requested_audience(user_ids, role_ids, policy, default_role_id)
    Messenger->>Messenger: _resolve_audience(guild, user_ids, role_ids)
    Messenger->>Messenger: _validate_send_policy(policy, preview)
    Messenger-->>DmFacade: Draft
    DmFacade-->>NotifyCog: Draft

    NotifyCog->>DmFacade: render_preview(Draft)
    DmFacade->>Messenger: render_preview(Draft)
    Messenger-->>DmFacade: str preview
    DmFacade-->>NotifyCog: str preview
    NotifyCog->>NotifyCog: log preview

    NotifyCog->>DmFacade: send(guild, Draft)
    DmFacade->>Messenger: send(guild, Draft)
    Messenger->>Messenger: _validate_send_policy(policy, preview)
    loop for each recipient
        Messenger->>DiscordAPI: create DM message
        alt success
            DiscordAPI-->>Messenger: message created
        else failure
            DiscordAPI-->>Messenger: error
            Messenger->>Messenger: record failed_id
        end
    end
    Messenger-->>DmFacade: SendResult
    DmFacade-->>NotifyCog: SendResult

    alt sent_count == 1
        NotifyCog->>DiscordClient: interaction.response.send_message("Sent you a DM.", ephemeral)
    else
        NotifyCog->>NotifyCog: raise UserFriendlyError
        NotifyCog->>DiscordClient: error response
    end

    DiscordClient-->>User: ephemeral confirmation or error
Loading

Class diagram for internal DM service and policies

classDiagram
    class Policy {
      +frozenset~int~ allowed_user_ids
      +frozenset~int~ allowed_role_ids
      +int max_recipients
      +__post_init__()
    }

    class MessagePayload {
      +str content
    }

    class AudiencePreview {
      +list~discord.Member~ recipients
      +list~int~ skipped_ids
      +tuple~int,...~ source_user_ids
      +tuple~int,...~ source_role_ids
      +int recipient_count
    }

    class Draft {
      +int guild_id
      +AudiencePreview preview
      +MessagePayload payload
      +Policy policy
      +datetime created_at
    }

    class SendResult {
      +int sent_count
      +list~int~ failed_ids
    }

    class DirectMessenger {
      -logging.Logger log
      +DirectMessenger()
      +compose(guild, content, user_ids, role_ids, policy) Draft
      +compose_to_user(guild, user_id, content, policy) Draft
      +compose_to_users(guild, user_ids, content, policy) Draft
      +compose_to_role(guild, role_id, content, policy) Draft
      +compose_to_roles(guild, role_ids, content, policy) Draft
      +send(guild, draft) SendResult
      +render_preview(draft) str
      -_compose(guild, content, user_ids, role_ids, policy) Draft
      -_resolve_policy(policy) Policy
      -_normalize_content(content) str
      -_validate_requested_audience(user_ids, role_ids, policy, default_role_id) void
      -_resolve_audience(guild, user_ids, role_ids) AudiencePreview
      -_validate_send_policy(policy, preview) void
      -_resolve_member(guild, user_id) discord.Member
    }

    class PoliciesModule {
      +Policy DENY_ALL
      +allow_users(user_ids, max_recipients) Policy
      +allow_roles(role_ids, max_recipients) Policy
      +allow_targets(user_ids, role_ids, max_recipients) Policy
    }

    class DmFacade {
      +compose(guild, content, user_ids, role_ids, policy) Draft
      +compose_to_user(guild, user_id, content, policy) Draft
      +compose_to_users(guild, user_ids, content, policy) Draft
      +compose_to_role(guild, role_id, content, policy) Draft
      +compose_to_roles(guild, role_ids, content, policy) Draft
      +send(guild, draft) SendResult
      +render_preview(draft) str
    }

    DirectMessenger --> Policy
    DirectMessenger --> MessagePayload
    DirectMessenger --> AudiencePreview
    DirectMessenger --> Draft
    DirectMessenger --> SendResult

    Draft --> AudiencePreview
    Draft --> MessagePayload
    Draft --> Policy

    PoliciesModule --> Policy
    DmFacade --> DirectMessenger
Loading

File-Level Changes

Change Details Files
Add an internal, policy-driven direct messaging service for composing and sending safe DM batches.
  • Implemented a DirectMessenger service with composition helpers for users and roles, using a Policy object to enforce allowlists and max recipient caps.
  • Normalized and validated DM content (non-empty, length-capped) and audience (no @everyone, deny-by-default, explicit policy checks).
  • Resolved target audiences into unique discord.Member recipients with deduplication, fallback fetching, and failure tracking during send.
  • Exposed module-level compose*/send/render_preview functions that proxy to a shared DirectMessenger instance.
capy_discord/services/dm.py
Introduce reusable policy helpers to construct DM allowlists and export them via the services package.
  • Defined DENY_ALL plus helper constructors for user-only, role-only, and combined user/role policies with configurable max_recipient caps.
  • Re-exported dm and policies modules from the services package for ergonomic imports.
capy_discord/services/policies.py
capy_discord/services/__init__.py
Add an operator-facing /notify command that sends a self-targeted test DM using the new service.
  • Implemented a Notify cog with a /notify slash command that DMs the invoking user only, using an allow_users policy with a single-recipient cap.
  • Logged DM previews, handled guild-only constraints, and raised a UserFriendlyError when the DM fails so the user gets actionable feedback.
capy_discord/exts/tools/notify.py
Document the internal DM service design and add tests for DM safety behavior and audience resolution.
  • Updated AGENTS.md to describe the internal DM service location, safety model, usage patterns, and guidance against broad /dm or bulk-message commands, and renumbered subsequent sections.
  • Added unit tests covering deny-by-default behavior, @everyone rejection, policy enforcement, deduplication and audience caps, and send failure tracking.
  • Performed a small style cleanup in the guild role settings modal initialization.
AGENTS.md
tests/capy_discord/services/test_dm.py
tests/capy_discord/services/__init__.py
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

@shamikkarkhanis shamikkarkhanis merged commit daeda0a into develop Mar 17, 2026
2 checks passed
@shamikkarkhanis shamikkarkhanis deleted the feature/capr-52-dm-module branch March 17, 2026 20:15
@shamikkarkhanis shamikkarkhanis restored the feature/capr-52-dm-module branch March 17, 2026 20:15
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 left some high level feedback:

  • In DirectMessenger._resolve_policy, consider returning the shared policies.DENY_ALL instance instead of constructing a new Policy() so there is a single canonical default and you can rely on identity checks or shared configuration in one place.
  • In AudiencePreview, skipped_ids currently mixes both user and role IDs; if you plan to inspect this for debugging or metrics, it may be clearer to track skipped user IDs and role IDs separately (or annotate their type) to make diagnostics more precise.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `DirectMessenger._resolve_policy`, consider returning the shared `policies.DENY_ALL` instance instead of constructing a new `Policy()` so there is a single canonical default and you can rely on identity checks or shared configuration in one place.
- In `AudiencePreview`, `skipped_ids` currently mixes both user and role IDs; if you plan to inspect this for debugging or metrics, it may be clearer to track skipped user IDs and role IDs separately (or annotate their type) to make diagnostics more precise.

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.

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