diff options
-rw-r--r-- | bot/exts/moderation/infraction/_utils.py | 39 | ||||
-rw-r--r-- | bot/exts/moderation/infraction/_views.py | 31 | ||||
-rw-r--r-- | bot/exts/moderation/infraction/infractions.py | 13 |
3 files changed, 75 insertions, 8 deletions
diff --git a/bot/exts/moderation/infraction/_utils.py b/bot/exts/moderation/infraction/_utils.py index baeb971e4..c3dfb8310 100644 --- a/bot/exts/moderation/infraction/_utils.py +++ b/bot/exts/moderation/infraction/_utils.py @@ -1,13 +1,14 @@ - import arrow import discord +from discord import Member from discord.ext.commands import Context from pydis_core.site_api import ResponseCodeError import bot -from bot.constants import Categories, Colours, Icons +from bot.constants import Categories, 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 @@ -298,3 +299,37 @@ async def send_private_embed(user: MemberOrUser, embed: discord.Embed) -> bool: "The user either could not be retrieved or probably disabled their DMs." ) return False + + +async def confirm_elevated_user_ban(ctx: Context, user: MemberOrUser) -> bool: + """ + If user has an elevated role, require confirmation before banning. + + A member with the staff, partner, or community roles are considered elevated. + + Returns a boolean indicating whether the infraction should proceed. + """ + if not isinstance(user, Member) or not any(role.id in STAFF_PARTNERS_COMMUNITY_ROLES for role in user.roles): + return True + + confirmation_view = InfractionConfirmationView( + allowed_users=(ctx.author.id,), + allowed_roles=MODERATION_ROLES, + timeout=10, + ) + confirmation_view.message = await ctx.send( + f"{user.mention} has an elevated role. Are you sure you want to ban them?", + view=confirmation_view, + allowed_mentions=discord.AllowedMentions.none(), + ) + + timed_out = await confirmation_view.wait() + if timed_out: + log.trace(f"Attempted ban of user {user} by moderator {ctx.author} cancelled due to timeout.") + return False + + if confirmation_view.confirmed is False: + log.trace(f"Attempted ban of user {user} by moderator {ctx.author} cancelled due to manual cancel.") + return False + + return True diff --git a/bot/exts/moderation/infraction/_views.py b/bot/exts/moderation/infraction/_views.py new file mode 100644 index 000000000..6215b2b6e --- /dev/null +++ b/bot/exts/moderation/infraction/_views.py @@ -0,0 +1,31 @@ +from typing import Any + +import discord +from discord import ButtonStyle, Interaction +from discord.ui import Button +from pydis_core.utils import interactions + + +class InfractionConfirmationView(interactions.ViewWithUserAndRoleCheck): + """A confirmation view to be sent before issuing potentially suspect infractions.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.confirmed = False + + @discord.ui.button(label="Confirm", style=ButtonStyle.red) + async def confirm(self, interaction: Interaction, button: Button) -> None: + """Callback coroutine that is called when the "confirm" button is pressed.""" + self.confirmed = True + await interaction.response.defer() + self.stop() + + @discord.ui.button(label="Cancel", style=ButtonStyle.green) + async def cancel(self, interaction: Interaction, button: Button) -> None: + """Callback coroutine that is called when the "cancel" button is pressed.""" + await interaction.response.send_message("Cancelled infraction.") + self.stop() + + async def on_timeout(self) -> None: + await super().on_timeout() + await self.message.reply("Cancelled infraction due to timeout.") diff --git a/bot/exts/moderation/infraction/infractions.py b/bot/exts/moderation/infraction/infractions.py index 6af2571de..cf8803487 100644 --- a/bot/exts/moderation/infraction/infractions.py +++ b/bot/exts/moderation/infraction/infractions.py @@ -478,6 +478,9 @@ class Infractions(InfractionScheduler, commands.Cog): await ctx.send(":x: I can't ban users above or equal to me in the role hierarchy.") return None + if not await _utils.confirm_elevated_user_ban(ctx, user): + return None + # In the case of a permanent ban, we don't need get_active_infractions to tell us if one is active is_temporary = kwargs.get("duration_or_expiry") is not None active_infraction = await _utils.get_active_infraction(ctx, user, "ban", is_temporary) @@ -501,14 +504,12 @@ class Infractions(InfractionScheduler, commands.Cog): infraction["purge"] = "purge " if purge_days else "" - self.mod_log.ignore(Event.member_remove, user.id) - - if reason: - reason = textwrap.shorten(reason, width=512, placeholder="...") - async def action() -> None: - await ctx.guild.ban(user, reason=reason, delete_message_days=purge_days) + # Discord only supports ban reasons up to 512 characters in length. + discord_reason = textwrap.shorten(reason or "", width=512, placeholder="...") + await ctx.guild.ban(user, reason=discord_reason, delete_message_days=purge_days) + self.mod_log.ignore(Event.member_remove, user.id) await self.apply_infraction(ctx, infraction, user, action) bb_cog: BigBrother | None = self.bot.get_cog("Big Brother") |