Skip to content
Merged
Changes from 2 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
73 changes: 71 additions & 2 deletions bot/exts/moderation/infraction/_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@
import typing as t
from abc import abstractmethod
from collections.abc import Awaitable, Callable
from datetime import UTC, datetime, timedelta
from gettext import ngettext

import arrow
import dateutil.parser
import discord
from async_rediscache import RedisCache
from discord.ext.commands import Context
from pydis_core.site_api import ResponseCodeError
from pydis_core.utils import scheduling
from pydis_core.utils.channel import get_or_fetch_channel

from bot import constants
from bot.bot import Bot
Expand All @@ -24,18 +27,26 @@

log = get_logger(__name__)

AUTOMATED_TIDY_UP_HOURS = 8


class InfractionScheduler:
"""Handles the application, pardoning, and expiration of infractions."""

messages_to_tidy: RedisCache = RedisCache()
Comment thread
jb3 marked this conversation as resolved.

def __init__(self, bot: Bot, supported_infractions: t.Container[str]):
self.bot = bot
self.scheduler = scheduling.Scheduler(self.__class__.__name__)
self.tidy_up_scheduler = scheduling.Scheduler(
f"{self.__class__.__name__}TidyUp"
)
self.supported_infractions = supported_infractions

async def cog_unload(self) -> None:
"""Cancel scheduled tasks."""
self.scheduler.cancel_all()
self.tidy_up_scheduler.cancel_all()

@property
def mod_log(self) -> ModLog:
Expand Down Expand Up @@ -76,7 +87,47 @@ async def cog_load(self) -> None:

self.scheduler.schedule_at(next_reschedule_point, -1, self.cog_load())

log.trace("Done rescheduling")
log.trace("Done rescheduling expirations, scheduling tidy up tasks.")

for key, expire_at in await self.messages_to_tidy.items():
channel_id, message_id = map(int, key.split(":"))
expire_at = dateutil.parser.isoparse(expire_at)

log.trace(
"Scheduling tidy up for message %s in channel %s at %s",
message_id, channel_id, expire_at
)

self.tidy_up_scheduler.schedule_at(
expire_at,
message_id,
self._delete_infraction_message(channel_id, message_id)
)

async def _delete_infraction_message(
self,
channel_id: int,
message_id: int
) -> None:
"""
Delete a message in the given channel.

This is used to delete infraction messages after a certain period of time.
"""
try:
channel = await get_or_fetch_channel(self.bot, channel_id)
message = await channel.fetch_message(message_id)
await message.delete()
Comment thread
jb3 marked this conversation as resolved.
log.trace(f"Deleted infraction message {message_id} in channel {channel_id}.")
except discord.NotFound:
log.warning(f"Channel or message {message_id} not found in channel {channel_id}.")
except discord.Forbidden:
log.warning(f"Bot lacks permissions to delete message {message_id} in channel {channel_id}.")
Copy link
Copy Markdown
Member

@ChrisLovering ChrisLovering Jun 23, 2025

Choose a reason for hiding this comment

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

Unsure if this should always be a warning, since trying to delete messages in closed threads will also raise Forbidden iirc. No point in sending that specific case to sentry

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Ah I hadn't realised that manage messages does not override this. I'll drop it to an info log.

except discord.HTTPException:
log.exception(f"Issue during scheduled deletion of message {message_id} in channel {channel_id}.")
return # Keep the task in Redis on HTTP errors

await self.messages_to_tidy.delete(f"{channel_id}:{message_id}")

async def reapply_infraction(
self,
Expand Down Expand Up @@ -269,7 +320,25 @@ async def apply_infraction(
# Send a confirmation message to the invoking context.
log.trace(f"Sending infraction #{id_} confirmation message.")
mentions = discord.AllowedMentions(users=[user], roles=False)
await ctx.send(f"{dm_result}{confirm_msg}{infr_message}.", allowed_mentions=mentions)
sent_msg = await ctx.send(f"{dm_result}{confirm_msg}{infr_message}.", allowed_mentions=mentions)

if infraction["actor"] == self.bot.user.id:
expire_message_time = datetime.now(UTC) + timedelta(hours=AUTOMATED_TIDY_UP_HOURS)

log.trace(f"Scheduling message tidy for infraction #{id_} in {AUTOMATED_TIDY_UP_HOURS} hours.")

# Schedule the message to be deleted after a certain period of time.
self.tidy_up_scheduler.schedule_at(
expire_message_time,
sent_msg.id,
self._delete_infraction_message(ctx.channel.id, sent_msg.id)
)

# Persist to Redis to handle for bot restarts.
await self.messages_to_tidy.set(
f"{ctx.channel.id}:{sent_msg.id}",
expire_message_time.isoformat(),
)

if jump_url is None:
jump_url = "(Infraction issued in a ModMail channel.)"
Expand Down