aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Amrou Bellalouna <[email protected]>2024-03-29 23:54:59 +0100
committerGravatar GitHub <[email protected]>2024-03-29 23:54:59 +0100
commit972fd18d8cf1e21c2a7175d6eaf7c39af22bfed3 (patch)
treef66c8bf121c2091d7fd8acfd8e375ef05bb2389b
parentBump sentry-sdk from 1.43.0 to 1.44.0 (#2986) (diff)
parentnotify mods about timeout cap upon edit (diff)
Merge pull request #2839 from python-discord/allow-mute-edits
Support editing of timeout durations.
-rw-r--r--bot/exts/moderation/infraction/_utils.py43
-rw-r--r--bot/exts/moderation/infraction/infractions.py54
-rw-r--r--bot/exts/moderation/infraction/management.py18
3 files changed, 82 insertions, 33 deletions
diff --git a/bot/exts/moderation/infraction/_utils.py b/bot/exts/moderation/infraction/_utils.py
index c3dfb8310..f306ede02 100644
--- a/bot/exts/moderation/infraction/_utils.py
+++ b/bot/exts/moderation/infraction/_utils.py
@@ -1,17 +1,21 @@
+
+import datetime
+
import arrow
import discord
+from dateutil.relativedelta import relativedelta
from discord import Member
-from discord.ext.commands import Context
+from discord.ext.commands import Bot, Context
from pydis_core.site_api import ResponseCodeError
import bot
-from bot.constants import Categories, Colours, Icons, MODERATION_ROLES, STAFF_PARTNERS_COMMUNITY_ROLES
+from bot.constants import Categories, Channels, Colours, Icons, MODERATION_ROLES, STAFF_PARTNERS_COMMUNITY_ROLES
from bot.converters import DurationOrExpiry, MemberOrUser
from bot.errors import InvalidInfractedUserError
from bot.exts.moderation.infraction._views import InfractionConfirmationView
from bot.log import get_logger
from bot.utils import time
-from bot.utils.channel import is_in_category
+from bot.utils.channel import is_in_category, is_mod_channel
from bot.utils.time import unpack_duration
log = get_logger(__name__)
@@ -61,6 +65,13 @@ INFRACTION_DESCRIPTION_WARNING_TEMPLATE = (
)
+MAXIMUM_TIMEOUT_DAYS = datetime.timedelta(days=28)
+TIMEOUT_CAP_MESSAGE = (
+ f"The timeout for {{0}} can't be longer than {MAXIMUM_TIMEOUT_DAYS.days} days."
+ " I'll pretend that's what you meant."
+)
+
+
async def post_user(ctx: Context, user: MemberOrUser) -> dict | None:
"""
Create a new user in the database.
@@ -301,6 +312,22 @@ async def send_private_embed(user: MemberOrUser, embed: discord.Embed) -> bool:
return False
+def cap_timeout_duration(duration: datetime.datetime | relativedelta) -> tuple[bool, datetime.datetime]:
+ """Cap the duration of a duration to Discord's limit."""
+ now = arrow.utcnow()
+ capped = False
+ if isinstance(duration, relativedelta):
+ duration += now
+
+ if duration > now + MAXIMUM_TIMEOUT_DAYS:
+ duration = now + MAXIMUM_TIMEOUT_DAYS - datetime.timedelta(minutes=1) # Duration cap is exclusive.
+ capped = True
+ elif duration > now + MAXIMUM_TIMEOUT_DAYS - datetime.timedelta(minutes=1):
+ # Duration cap is exclusive. This is to still allow specifying "28d".
+ duration -= datetime.timedelta(minutes=1)
+ return capped, duration
+
+
async def confirm_elevated_user_ban(ctx: Context, user: MemberOrUser) -> bool:
"""
If user has an elevated role, require confirmation before banning.
@@ -333,3 +360,13 @@ async def confirm_elevated_user_ban(ctx: Context, user: MemberOrUser) -> bool:
return False
return True
+
+
+async def notify_timeout_cap(bot: Bot, ctx: Context, user: discord.Member) -> None:
+ """Notify moderators about a timeout duration being capped."""
+ cap_message_for_user = TIMEOUT_CAP_MESSAGE.format(user.mention)
+ if is_mod_channel(ctx.channel):
+ await ctx.reply(f":warning: {cap_message_for_user}")
+ else:
+ await bot.get_channel(Channels.mods).send(
+ f":warning: {ctx.author.mention} {cap_message_for_user}")
diff --git a/bot/exts/moderation/infraction/infractions.py b/bot/exts/moderation/infraction/infractions.py
index cf8803487..e435035f2 100644
--- a/bot/exts/moderation/infraction/infractions.py
+++ b/bot/exts/moderation/infraction/infractions.py
@@ -1,6 +1,6 @@
import textwrap
import typing as t
-from datetime import timedelta
+from datetime import UTC, timedelta
import arrow
import discord
@@ -12,13 +12,12 @@ from pydis_core.utils.members import get_or_fetch_member
from bot import constants
from bot.bot import Bot
-from bot.constants import Channels, Event
+from bot.constants import Event
from bot.converters import Age, Duration, DurationOrExpiry, MemberOrUser, UnambiguousMemberOrUser
from bot.decorators import ensure_future_timestamp, respect_role_hierarchy
from bot.exts.moderation.infraction import _utils
from bot.exts.moderation.infraction._scheduler import InfractionScheduler
from bot.log import get_logger
-from bot.utils.channel import is_mod_channel
from bot.utils.messages import format_user
log = get_logger(__name__)
@@ -46,12 +45,6 @@ COMP_BAN_REASON = (
"this message to appeal your ban."
)
COMP_BAN_DURATION = timedelta(days=4)
-# Timeout
-MAXIMUM_TIMEOUT_DAYS = timedelta(days=28)
-TIMEOUT_CAP_MESSAGE = (
- f"The timeout for {{0}} can't be longer than {MAXIMUM_TIMEOUT_DAYS.days} days."
- " I'll pretend that's what you meant."
-)
class Infractions(InfractionScheduler, commands.Cog):
@@ -230,21 +223,9 @@ class Infractions(InfractionScheduler, commands.Cog):
if duration is None:
duration = await Duration().convert(ctx, "1h")
else:
- now = arrow.utcnow()
- if isinstance(duration, relativedelta):
- duration += now
- if duration > now + MAXIMUM_TIMEOUT_DAYS:
- cap_message_for_user = TIMEOUT_CAP_MESSAGE.format(user.mention)
- if is_mod_channel(ctx.channel):
- await ctx.reply(f":warning: {cap_message_for_user}")
- else:
- await self.bot.get_channel(Channels.mods).send(
- f":warning: {ctx.author.mention} {cap_message_for_user}"
- )
- duration = now + MAXIMUM_TIMEOUT_DAYS - timedelta(minutes=1) # Duration cap is exclusive.
- elif duration > now + MAXIMUM_TIMEOUT_DAYS - timedelta(minutes=1):
- # Duration cap is exclusive. This is to still allow specifying "28d".
- duration -= timedelta(minutes=1)
+ capped, duration = _utils.cap_timeout_duration(duration)
+ if capped:
+ await _utils.notify_timeout_cap(self.bot, ctx, user)
await self.apply_timeout(ctx, user, reason, duration_or_expiry=duration)
@@ -665,6 +646,31 @@ class Infractions(InfractionScheduler, commands.Cog):
await ctx.send(str(error.errors[0]))
error.handled = True
+ @commands.Cog.listener()
+ async def on_member_join(self, member: Member) -> None:
+ """
+ Apply active timeout infractions for returning members.
+
+ This is needed for users who might have had their infraction edited in our database but not in Discord itself.
+ """
+ active_timeouts = await self.bot.api_client.get(
+ endpoint="bot/infractions",
+ params={"active": "true", "type": "timeout", "user__id": member.id}
+ )
+
+ if active_timeouts:
+ timeout_infraction = active_timeouts[0]
+ expiry = arrow.get(timeout_infraction["expires_at"], tzinfo=UTC).datetime.replace(second=0, microsecond=0)
+
+ if member.is_timed_out() and expiry == member.timed_out_until.replace(second=0, microsecond=0):
+ return
+
+ reason = f"Applying active timeout for returning member: {timeout_infraction['id']}"
+
+ async def action() -> None:
+ await member.edit(timed_out_until=expiry, reason=reason)
+ await self.reapply_infraction(timeout_infraction, action)
+
async def setup(bot: Bot) -> None:
"""Load the Infractions cog."""
diff --git a/bot/exts/moderation/infraction/management.py b/bot/exts/moderation/infraction/management.py
index 93959042b..ac228d8a2 100644
--- a/bot/exts/moderation/infraction/management.py
+++ b/bot/exts/moderation/infraction/management.py
@@ -197,8 +197,8 @@ class ModManagement(commands.Cog):
# Update `last_applied` if expiry changes.
request_data["last_applied"] = origin.isoformat()
request_data["expires_at"] = expiry.isoformat()
- expiry = time.format_with_duration(expiry, origin)
- confirm_messages.append(f"set to expire on {expiry}")
+ formatted_expiry = time.format_with_duration(expiry, origin)
+ confirm_messages.append(f"set to expire on {formatted_expiry}")
else:
confirm_messages.append("expiry unchanged")
@@ -218,6 +218,10 @@ class ModManagement(commands.Cog):
json=request_data,
)
+ # Get information about the infraction's user
+ user_id = new_infraction["user"]
+ user = await get_or_fetch_member(ctx.guild, user_id)
+
# Re-schedule infraction if the expiration has been updated
if "expires_at" in request_data:
# A scheduled task should only exist if the old infraction wasn't permanent
@@ -227,6 +231,12 @@ class ModManagement(commands.Cog):
# If the infraction was not marked as permanent, schedule a new expiration task
if request_data["expires_at"]:
self.infractions_cog.schedule_expiration(new_infraction)
+ # Timeouts are handled by Discord itself, so we need to edit the expiry in Discord as well
+ if user and infraction["type"] == "timeout":
+ capped, duration = _utils.cap_timeout_duration(expiry)
+ if capped:
+ await _utils.notify_timeout_cap(self.bot, ctx, user)
+ await user.edit(reason=reason, timed_out_until=expiry)
log_text += f"""
Previous expiry: {time.until_expiration(infraction['expires_at'])}
@@ -236,10 +246,6 @@ class ModManagement(commands.Cog):
changes = " & ".join(confirm_messages)
await ctx.send(f":ok_hand: Updated infraction #{infraction_id}: {changes}")
- # Get information about the infraction's user
- user_id = new_infraction["user"]
- user = await get_or_fetch_member(ctx.guild, user_id)
-
if user:
user_text = messages.format_user(user)
thumbnail = user.display_avatar.url