diff options
author | 2024-03-26 08:58:38 -0500 | |
---|---|---|
committer | 2024-03-26 13:58:38 +0000 | |
commit | 3a5e38696d9e5699e65bed23c946ce4f754ea961 (patch) | |
tree | ed0383167117c3cf15660f95684da9c0a74fff5c | |
parent | Fix type annotation in snekbox (diff) |
Ask for confirmation when banning members with elevated roles (#2316)
* Add safeguard when banning staff
* Update infractions.py
* Fix logical error where user would not get banned
* Update bot/exts/moderation/infraction/infractions.py
Co-authored-by: Preocts <[email protected]>
* Delete view if confirmed
* Implement suggestions
* Use instead of
* Don't call timeout manually
* Lint
* Switch button colors
* Remove message bind to view class
* Set message property of view to sent message
* Update poetry.lock
* Remove unnecessary View initializer
* Send message indicating if the infraction was cancelled or not
* Remove feedback on confirm
* Merge main into staff-ban-safeguard
* Make invocation async
* Make .stop() invocation sync again
* Clean up code
* Avoid truncating early and send message on timeout
* Should probably move this, too
* Fail safely
* Move view to a new file
* Break out confirmation into its own function
---------
Co-authored-by: Preocts <[email protected]>
Co-authored-by: Richard Si <[email protected]>
-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") |