Skip to content
Merged
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
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.info(f"Bot lacks permissions to delete message {message_id} in channel {channel_id}.")
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
Loading