Skip to content

Centralize AutoMod punishments and attribute report actions to the acting moderator - #292

Merged
celveren merged 4 commits into
masterfrom
copilot/centralize-automod-punishments
Oct 4, 2026
Merged

celveren merged 4 commits into
masterfrom
copilot/centralize-automod-punishments

Conversation

Copilot AI commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

AutoMod punishments were scattered as private _*_punishment methods on the cog, with separate timed variants. Message Reports reached into those privates, so every report action was recorded as the bot's even though a moderator confirmed it.

  • automod/punishments.py (new)
    • One Punishment subclass per type (Delete, Warn, Kick, Ban, Mute), kept in a registry.
    • Ban and Mute keep their variants as methods: permanent, timed, and for mute either the native timeout or the role plus timer.
    • Single entry point:
      await apply_punishment(bot, "MUTE", message, reason=..., moderator=interaction.user, duration=...)
    • moderator defaults to the bot, which is what AutoMod uses.
    • PunishmentContext carries the moderator, duration and tracked content, so the old special-casing of mute and ban durations is gone.
  • Moderator attribution
    • ctx.moderator is used for the infraction record, the timer's mod_id, the dispatched event and the audit-log reason. When the moderator isn't the bot, the audit reason is prefixed with them. Audit reasons are truncated to 512 characters.
    • LightningAutoModInfractionEvent.from_message takes an optional moderator. I kept this event type for reports because the modlog listener relies on .message and .tracked_content.
  • automod/cog.py
    • Removed the _*_punishment methods, the punishment dict, _time_ban_member, _time_mute_user, can_timeout and get_mute_role.
    • _handle_punishment now just builds the reason and calls apply_punishment.
  • reports/ui.py
    • confirm_button calls apply_punishment(..., moderator=interaction.user) and no longer needs the AutoMod cog loaded.
    • The DM verb and preposition come from the Punishment classes.

Behavior changes:

  • Timed mutes use the supplied reason instead of the hardcoded "Member triggered automod".
  • Report actions now show the confirming moderator as the actor.

Notes for review:

  • A timed mute through Discord's native timeout still doesn't log an infraction. That matches the previous behavior and I left it alone.
  • The warn-threshold kick/ban listener in handle_warn_thresholds still has its own code path.
  • No tests are included. aiohttp isn't installed in my sandbox, so I couldn't import the bot or run anything, and the changes are only compile-checked.

Copilot AI and others added 2 commits October 4, 2026 05:15
Co-authored-by: celveren <46062298+celveren@users.noreply.github.com>
Co-authored-by: celveren <46062298+celveren@users.noreply.github.com>
@sourcery-ai

sourcery-ai Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

The PR replaces cog-private AutoMod punishment handlers with a shared, registry-based punishment module and context, then routes both AutoMod and confirmed report actions through it so moderator identity is consistently recorded in infractions, timers, events, and audit logs.

Sequence diagram for moderator-attributed report punishment

sequenceDiagram
    actor Moderator
    participant ReportsUI
    participant Apply as apply_punishment
    participant Punishment as Punishment registry
    participant Context as PunishmentContext
    participant Discord
    participant Modlog

    Moderator->>ReportsUI: confirm_button
    ReportsUI->>Apply: apply_punishment(punishment, message, reason, moderator)
    Apply->>Punishment: select punishment type
    Apply->>Context: create context with moderator
    Punishment->>Discord: apply action with audit_reason
    Punishment->>Context: log(action)
    Context->>Modlog: add_infraction and dispatch event
    Modlog-->>ReportsUI: punishment result
Loading

File-Level Changes

Change Details Files
Centralize punishment execution behind registered punishment classes and a shared context.
  • Add Delete, Warn, Kick, Ban, and Mute implementations with shared dispatch and duration handling.
  • Move timer creation, moderation lookups, expiry calculation, infraction logging, and event dispatch into PunishmentContext.
  • Preserve timed ban and mute variants, including native timeout and mute-role fallback behavior.
lightning/cogs/automod/punishments.py
Refactor AutoMod to use the centralized punishment entry point.
  • Remove cog-local punishment methods, punishment registry, and timing/role helpers.
  • Pass rule-derived reason, duration, and tracked content to apply_punishment, retaining the bot as the default moderator.
lightning/cogs/automod/cog.py
Attribute report-confirmed actions to the moderator who approved them.
  • Invoke apply_punishment with interaction.user and handle unavailable cogs or failed permission/configuration checks.
  • Source report DM wording from the registered punishment metadata and remove the AutoMod cog dependency.
lightning/cogs/reports/ui.py
Propagate moderator identity through AutoMod infraction events and audit logging.
  • Allow from_message to accept an explicit moderator.
  • Use the moderator for infractions, dispatched events, timer mod_id values, and prefixed, 512-character-limited audit reasons.
lightning/events.py
lightning/cogs/automod/punishments.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

Comment thread lightning/cogs/automod/punishments.py Outdated

async def add_timer(self, event: str, **kwargs: Any) -> Any:
expiry = self.expiry
cog = self.bot.get_cog("Reminders")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Let's raise an error when Reminders isn't unavailable (i.e. returns a None type).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 88f022b/6f1b734. PunishmentContext.get_reminders() raises TimersUnavailable when the Reminders cog is missing. Ban checks it before banning, so a timed ban can't turn into a permanent one.

Comment thread lightning/cogs/automod/punishments.py Outdated
await self._permanent(ctx)

async def _get_role(self, ctx: PunishmentContext) -> Optional[discord.Role]:
cfg = await ctx.bot.get_cog("Moderation").get_mod_config(ctx.guild.id) # type: ignore

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Let's avoid a one liner here.

Something like

cog = ctx.bot.get_cog("Moderation")
if not cog:
    # Raise error here
cfg = await cog.get_mod_config(ctx.guild.id)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 88f022b/6f1b734. The one-liner is expanded and a new get_moderation() raises CogNotAvailable("Moderation") when the cog is missing.

Comment thread lightning/cogs/automod/punishments.py Outdated

role = await self._get_role(ctx)
if not role:
return

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What happens when it returns?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Before, it just did nothing, and the caller couldn't tell. Now apply() and apply_punishment() return a bool: False when nothing was done (no mute role, missing permissions, failed delete), True otherwise. Changed in 88f022b/6f1b734.

Comment thread lightning/cogs/reports/ui.py Outdated

await func(*args, reason=self.reason)
# The moderator pressing confirm is the one responsible for this action, not the bot.
await apply_punishment(interaction.client, punishment.type, self.message, reason=self.reason,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If it returns None, then the punishment wasn't completed, right? If so, let's not complete the rest of the interaction.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 88f022b/6f1b734. If apply_punishment returns False or raises a LightningError, confirm_button sends an ephemeral error and returns without calling complete().

Comment thread lightning/cogs/automod/punishments.py Outdated

async def _add_role(self, ctx: PunishmentContext, role: discord.Role) -> None:
await ctx.member.add_roles(role, reason=ctx.audit_reason)
await ctx.bot.get_cog("Moderation").add_punishment_role(ctx.guild.id, ctx.member.id, role.id) # type: ignore

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

See comment about one liners

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 88f022b/6f1b734. Both Moderation lookups now use the get_moderation() helper, with no one-liners left.

@celveren

celveren commented Oct 4, 2026

Copy link
Copy Markdown
Member

@copilot Please see the request changes and implement or comment as needed.

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
@celveren
celveren force-pushed the copilot/centralize-automod-punishments branch from 6f1b734 to 2784e3e Compare October 4, 2026 05:53
return None
if isinstance(self.duration, datetime.datetime):
return self.duration
return self.message.created_at + datetime.timedelta(seconds=self.duration)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot Where is this coming from again?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm assuming you mean message.created_at on this line. That base time comes from the old AutoMod code. _time_ban_member and _time_mute_user did message.created_at + timedelta(seconds=seconds) for rules that store the duration as seconds. Report actions pass a ready-made datetime (from ActionDashboard.duration), which is returned as-is. If you meant something else, tell me which part. If you'd rather count from "now" (discord.utils.utcnow()) than from the triggering message, I can change it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot Can you add a comment explaining that?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added the comment above that line in PunishmentContext.expiry in cd797b9.

Co-authored-by: celveren <46062298+celveren@users.noreply.github.com>

@celveren celveren left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm

@celveren
celveren marked this pull request as ready for review October 4, 2026 06:10
@celveren
celveren merged commit 5608240 into master Oct 4, 2026
4 checks passed
@celveren
celveren deleted the copilot/centralize-automod-punishments branch October 4, 2026 06:10

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="lightning/cogs/reports/ui.py" line_range="245-249" />
<code_context>
-            else:
-                v, loc = "punished", "in"
-            dm_message = construct_dm_message(self.message.author, v, loc, reason=self.reason,
+            dm_message = construct_dm_message(self.message.author, punishment.verb, punishment.preposition,
+                                              reason=self.reason,
                                               middle=f" due to a message you posted. ({self.message.jump_url})")
             await dm_user(self.message.author, dm_message)

-        await func(*args, reason=self.reason)
+        # The moderator pressing confirm is the one responsible for this action, not the bot.
</code_context>
<issue_to_address>
**issue (bug_risk):** The report DM is sent before `apply_punishment` confirms that the punishment succeeded. When the action fails because of missing permissions, a missing mute role, or an unavailable timer/moderation cog, the reported member receives a notification claiming they were punished even though no punishment was applied.

**Triggers:** When `self.notify` is true and the punishment returns `False` or raises `LightningError`.

**Suggested fix:** Apply the punishment first and send the DM only after `applied` is true, or explicitly report that notification delivery is conditional on successful punishment.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 1 finding to address first, and this centralizes ban, kick, mute, warning, and message-deletion execution and changes report actions to record the confirming moderator, so a defect could apply the wrong moderation action or create incorrect audit/timer records. Reverting stops future mistakes, but bans, role assignments, timeouts, and deleted messages that already occurred would require separate remediation.

Blocking findings: lightning/cogs/reports/ui.py:249


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread lightning/cogs/reports/ui.py Resolved
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.

2 participants