From 0254200dca80d16d6816ce817c16ab5f4f4c85fc Mon Sep 17 00:00:00 2001 From: shtlrs Date: Sat, 9 Dec 2023 16:33:25 +0100 Subject: allow timeout edits --- bot/exts/moderation/infraction/management.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/bot/exts/moderation/infraction/management.py b/bot/exts/moderation/infraction/management.py index 93959042b..9fd851245 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,9 @@ 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 infraction["type"] == "timeout": + await user.edit(reason=reason, timed_out_until=expiry) log_text += f""" Previous expiry: {time.until_expiration(infraction['expires_at'])} @@ -236,10 +243,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 -- cgit v1.2.3 From a57cbef73dc1cc1562d911a2e83264f4efd8ed7b Mon Sep 17 00:00:00 2001 From: shtlrs Date: Sat, 9 Dec 2023 16:37:38 +0100 Subject: do not edit discord timeout for users who left the server if a user isn't a member of the server, trying to edit their timeout will result in an error --- bot/exts/moderation/infraction/management.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/moderation/infraction/management.py b/bot/exts/moderation/infraction/management.py index 9fd851245..0a33c84ab 100644 --- a/bot/exts/moderation/infraction/management.py +++ b/bot/exts/moderation/infraction/management.py @@ -232,7 +232,7 @@ class ModManagement(commands.Cog): 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 infraction["type"] == "timeout": + if user and infraction["type"] == "timeout": await user.edit(reason=reason, timed_out_until=expiry) log_text += f""" -- cgit v1.2.3 From f494b237338a509b22a391be8653f39b4b4ab18b Mon Sep 17 00:00:00 2001 From: shtlrs Date: Fri, 29 Mar 2024 23:41:13 +0100 Subject: cap timeout duration upon edit --- bot/exts/moderation/infraction/_utils.py | 22 ++++++++++++++++++++++ bot/exts/moderation/infraction/infractions.py | 10 ++-------- bot/exts/moderation/infraction/management.py | 1 + 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/bot/exts/moderation/infraction/_utils.py b/bot/exts/moderation/infraction/_utils.py index c3dfb8310..d616e4009 100644 --- a/bot/exts/moderation/infraction/_utils.py +++ b/bot/exts/moderation/infraction/_utils.py @@ -1,5 +1,9 @@ + +import datetime + import arrow import discord +from dateutil.relativedelta import relativedelta from discord import Member from discord.ext.commands import Context from pydis_core.site_api import ResponseCodeError @@ -61,6 +65,8 @@ INFRACTION_DESCRIPTION_WARNING_TEMPLATE = ( ) +MAXIMUM_TIMEOUT_DAYS = datetime.timedelta(days=28) + async def post_user(ctx: Context, user: MemberOrUser) -> dict | None: """ Create a new user in the database. @@ -301,6 +307,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]: + """Caps 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. diff --git a/bot/exts/moderation/infraction/infractions.py b/bot/exts/moderation/infraction/infractions.py index cf8803487..3b2f2810a 100644 --- a/bot/exts/moderation/infraction/infractions.py +++ b/bot/exts/moderation/infraction/infractions.py @@ -230,10 +230,8 @@ 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: + capped, duration = _utils.cap_timeout_duration(duration) + if 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}") @@ -241,10 +239,6 @@ class Infractions(InfractionScheduler, commands.Cog): 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) await self.apply_timeout(ctx, user, reason, duration_or_expiry=duration) diff --git a/bot/exts/moderation/infraction/management.py b/bot/exts/moderation/infraction/management.py index 0a33c84ab..e5216cac9 100644 --- a/bot/exts/moderation/infraction/management.py +++ b/bot/exts/moderation/infraction/management.py @@ -233,6 +233,7 @@ class ModManagement(commands.Cog): 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": + _, duration = _utils.cap_timeout_duration(expiry) await user.edit(reason=reason, timed_out_until=expiry) log_text += f""" -- cgit v1.2.3 From 4072c8830c9d7f303d0d39b680e21997895ad9b7 Mon Sep 17 00:00:00 2001 From: shtlrs Date: Sat, 17 Feb 2024 10:56:10 +0100 Subject: apply timeouts on members who left the server If timeout was edited to a longer duration, members could evade being timeout upon rejoining, so we reapply them if that's the case --- bot/exts/moderation/infraction/infractions.py | 28 ++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/bot/exts/moderation/infraction/infractions.py b/bot/exts/moderation/infraction/infractions.py index 3b2f2810a..f2dbd858d 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 @@ -659,6 +659,32 @@ 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.""" -- cgit v1.2.3 From f319bf8356097608b136a42985f349a8288f07e4 Mon Sep 17 00:00:00 2001 From: shtlrs Date: Fri, 29 Mar 2024 23:43:29 +0100 Subject: notify mods about timeout cap upon edit --- bot/exts/moderation/infraction/_utils.py | 23 +++++++++++++++++++---- bot/exts/moderation/infraction/infractions.py | 18 ++---------------- bot/exts/moderation/infraction/management.py | 4 +++- 3 files changed, 24 insertions(+), 21 deletions(-) diff --git a/bot/exts/moderation/infraction/_utils.py b/bot/exts/moderation/infraction/_utils.py index d616e4009..f306ede02 100644 --- a/bot/exts/moderation/infraction/_utils.py +++ b/bot/exts/moderation/infraction/_utils.py @@ -5,17 +5,17 @@ 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__) @@ -66,6 +66,11 @@ 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: """ @@ -308,7 +313,7 @@ async def send_private_embed(user: MemberOrUser, embed: discord.Embed) -> bool: def cap_timeout_duration(duration: datetime.datetime | relativedelta) -> tuple[bool, datetime.datetime]: - """Caps the duration of a duration to Discord's limit.""" + """Cap the duration of a duration to Discord's limit.""" now = arrow.utcnow() capped = False if isinstance(duration, relativedelta): @@ -355,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 f2dbd858d..e435035f2 100644 --- a/bot/exts/moderation/infraction/infractions.py +++ b/bot/exts/moderation/infraction/infractions.py @@ -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): @@ -232,13 +225,7 @@ class Infractions(InfractionScheduler, commands.Cog): else: capped, duration = _utils.cap_timeout_duration(duration) if 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 self.bot.get_channel(Channels.mods).send( - f":warning: {ctx.author.mention} {cap_message_for_user}" - ) + await _utils.notify_timeout_cap(self.bot, ctx, user) await self.apply_timeout(ctx, user, reason, duration_or_expiry=duration) @@ -662,7 +649,6 @@ class Infractions(InfractionScheduler, commands.Cog): @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. diff --git a/bot/exts/moderation/infraction/management.py b/bot/exts/moderation/infraction/management.py index e5216cac9..ac228d8a2 100644 --- a/bot/exts/moderation/infraction/management.py +++ b/bot/exts/moderation/infraction/management.py @@ -233,7 +233,9 @@ class ModManagement(commands.Cog): 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": - _, duration = _utils.cap_timeout_duration(expiry) + 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""" -- cgit v1.2.3