aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Robin <[email protected]>2024-03-26 08:58:38 -0500
committerGravatar GitHub <[email protected]>2024-03-26 13:58:38 +0000
commit3a5e38696d9e5699e65bed23c946ce4f754ea961 (patch)
treeed0383167117c3cf15660f95684da9c0a74fff5c
parentFix 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.py39
-rw-r--r--bot/exts/moderation/infraction/_views.py31
-rw-r--r--bot/exts/moderation/infraction/infractions.py13
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")