diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 777a9cc3..01a5f040 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -169,4 +169,6 @@ enum CaseType { UNJAIL SNIPPETUNBAN UNTEMPBAN + FLAG + UNFLAG } diff --git a/tux/cogs/moderation/__init__.py b/tux/cogs/moderation/__init__.py index c2e10800..4127f0fc 100644 --- a/tux/cogs/moderation/__init__.py +++ b/tux/cogs/moderation/__init__.py @@ -212,6 +212,7 @@ async def handle_case_response( reason: str, user: discord.Member | discord.User, dm_sent: bool, + silent_action: bool, duration: str | None = None, ): moderator = ctx.author @@ -246,7 +247,9 @@ async def handle_case_response( if dm_sent: embed.description = "A DM has been sent to the user." else: - embed.description = "DMs are disabled for this user." + embed.description = "DMs are disabled for this user or the silent flag was used." + + if not silent_action: + await self.send_embed(ctx, embed, log_type="mod") - await self.send_embed(ctx, embed, log_type="mod") await ctx.send(embed=embed, delete_after=30, ephemeral=True) diff --git a/tux/cogs/moderation/ban.py b/tux/cogs/moderation/ban.py index 7fefd79b..4195366f 100644 --- a/tux/cogs/moderation/ban.py +++ b/tux/cogs/moderation/ban.py @@ -69,7 +69,15 @@ async def ban( guild_id=ctx.guild.id, ) - await self.handle_case_response(ctx, CaseType.BAN, case.case_number, flags.reason, member, dm_sent) + await self.handle_case_response( + ctx, + CaseType.BAN, + case.case_number, + flags.reason, + member, + dm_sent, + silent_action=False, + ) async def setup(bot: Tux) -> None: diff --git a/tux/cogs/moderation/cases.py b/tux/cogs/moderation/cases.py index a80e0b7a..bf88f803 100644 --- a/tux/cogs/moderation/cases.py +++ b/tux/cogs/moderation/cases.py @@ -25,6 +25,8 @@ "jail": 1268115750392954880, "snippetban": 1277174953950576681, "snippetunban": 1277174953292337222, + "flag": 1275782294363312172, + "unflag": 1275782294363312172, } @@ -381,6 +383,8 @@ def _get_case_type_emoji(self, case_type: CaseType) -> discord.Emoji | None: CaseType.UNJAIL: "jail", CaseType.SNIPPETBAN: "snippetban", CaseType.SNIPPETUNBAN: "snippetunban", + CaseType.FLAG: "flag", + CaseType.UNFLAG: "unflag", } emoji_name = emoji_map.get(case_type) if emoji_name is not None: diff --git a/tux/cogs/moderation/flag.py b/tux/cogs/moderation/flag.py new file mode 100644 index 00000000..8fc4e289 --- /dev/null +++ b/tux/cogs/moderation/flag.py @@ -0,0 +1,101 @@ +import discord +from discord.ext import commands + +from prisma.enums import CaseType +from tux.bot import Tux +from tux.database.controllers.case import CaseController +from tux.utils import checks +from tux.utils.flags import FlagFlags, generate_usage + +from . import ModerationCogBase + + +class Unflag(ModerationCogBase): + def __init__(self, bot: Tux) -> None: + super().__init__(bot) + self.case_controller = CaseController() + self.flag.usage = generate_usage(self.flag, FlagFlags) + + @commands.hybrid_command( + name="flag", + aliases=["fl"], + ) + @commands.guild_only() + @checks.has_pl(2) + async def flag( + self, + ctx: commands.Context[Tux], + member: discord.Member, + *, + flags: FlagFlags, + ) -> None: + """ + Flag a member from the server. + + Parameters + ---------- + ctx : commands.Context[Tux] + The context in which the command is being invoked. + member : discord.Member + The member to flag. + flags : FlagFlags + The flags for the command. (reason: str, silent: bool) + """ + + assert ctx.guild + + if await self.is_flagged(ctx.guild.id, member.id): + await ctx.send("User is already flagged.", delete_after=30, ephemeral=True) + return + + moderator = ctx.author + + if not await self.check_conditions(ctx, member, moderator, "flag"): + return + + case = await self.db.case.insert_case( + case_user_id=member.id, + case_moderator_id=ctx.author.id, + case_type=CaseType.FLAG, + case_reason=flags.reason, + guild_id=ctx.guild.id, + ) + + await self.handle_case_response( + ctx, + CaseType.FLAG, + case.case_number, + flags.reason, + member, + dm_sent=False, + silent_action=True, + ) + + async def is_flagged(self, guild_id: int, user_id: int) -> bool: + """ + Check if a user is flagged. + + Parameters + ---------- + guild_id : int + The ID of the guild to check in. + user_id : int + The ID of the user to check. + + Returns + ------- + bool + True if the user is flagged, False otherwise. + """ + + flag_cases = await self.case_controller.get_all_cases_by_type(guild_id, CaseType.FLAG) + unflag_cases = await self.case_controller.get_all_cases_by_type(guild_id, CaseType.UNFLAG) + + flag_count = sum(case.case_user_id == user_id for case in flag_cases) + unflag_count = sum(case.case_user_id == user_id for case in unflag_cases) + + return flag_count > unflag_count + + +async def setup(bot: Tux) -> None: + await bot.add_cog(Unflag(bot)) diff --git a/tux/cogs/moderation/jail.py b/tux/cogs/moderation/jail.py index 5a51bcbd..d0d8a225 100644 --- a/tux/cogs/moderation/jail.py +++ b/tux/cogs/moderation/jail.py @@ -103,7 +103,15 @@ async def jail( # noqa: PLR0911 return dm_sent = await self.send_dm(ctx, flags.silent, member, flags.reason, "jailed") - await self.handle_case_response(ctx, CaseType.JAIL, case.case_number, flags.reason, member, dm_sent) + await self.handle_case_response( + ctx, + CaseType.JAIL, + case.case_number, + flags.reason, + member, + dm_sent, + silent_action=False, + ) def _get_manageable_roles( self, diff --git a/tux/cogs/moderation/kick.py b/tux/cogs/moderation/kick.py index 737bbeba..ae07f3d1 100644 --- a/tux/cogs/moderation/kick.py +++ b/tux/cogs/moderation/kick.py @@ -73,7 +73,15 @@ async def kick( guild_id=ctx.guild.id, ) - await self.handle_case_response(ctx, CaseType.KICK, case.case_number, flags.reason, member, dm_sent) + await self.handle_case_response( + ctx, + CaseType.KICK, + case.case_number, + flags.reason, + member, + dm_sent, + silent_action=False, + ) async def setup(bot: Tux) -> None: diff --git a/tux/cogs/moderation/snippetban.py b/tux/cogs/moderation/snippetban.py index 80cd3ef8..b3ec5221 100644 --- a/tux/cogs/moderation/snippetban.py +++ b/tux/cogs/moderation/snippetban.py @@ -64,7 +64,15 @@ async def snippet_ban( return dm_sent = await self.send_dm(ctx, flags.silent, member, flags.reason, "snippet banned") - await self.handle_case_response(ctx, CaseType.SNIPPETBAN, case.case_number, flags.reason, member, dm_sent) + await self.handle_case_response( + ctx, + CaseType.SNIPPETBAN, + case.case_number, + flags.reason, + member, + dm_sent, + silent_action=False, + ) async def is_snippetbanned(self, guild_id: int, user_id: int) -> bool: """ diff --git a/tux/cogs/moderation/snippetunban.py b/tux/cogs/moderation/snippetunban.py index d9fea55f..afe3b0ec 100644 --- a/tux/cogs/moderation/snippetunban.py +++ b/tux/cogs/moderation/snippetunban.py @@ -64,7 +64,15 @@ async def snippet_unban( return dm_sent = await self.send_dm(ctx, flags.silent, member, flags.reason, "snippet unbanned") - await self.handle_case_response(ctx, CaseType.SNIPPETUNBAN, case.case_number, flags.reason, member, dm_sent) + await self.handle_case_response( + ctx, + CaseType.SNIPPETUNBAN, + case.case_number, + flags.reason, + member, + dm_sent, + silent_action=False, + ) async def is_snippetbanned(self, guild_id: int, user_id: int) -> bool: """ diff --git a/tux/cogs/moderation/tempban.py b/tux/cogs/moderation/tempban.py index 3033c061..de50c18e 100644 --- a/tux/cogs/moderation/tempban.py +++ b/tux/cogs/moderation/tempban.py @@ -76,7 +76,15 @@ async def tempban( case_tempban_expired=False, ) - await self.handle_case_response(ctx, CaseType.TEMPBAN, case.case_number, flags.reason, member, dm_sent) + await self.handle_case_response( + ctx, + CaseType.TEMPBAN, + case.case_number, + flags.reason, + member, + dm_sent, + silent_action=False, + ) @tasks.loop(hours=1) async def tempban_check(self) -> None: diff --git a/tux/cogs/moderation/timeout.py b/tux/cogs/moderation/timeout.py index e908ada2..16fd92a2 100644 --- a/tux/cogs/moderation/timeout.py +++ b/tux/cogs/moderation/timeout.py @@ -86,6 +86,7 @@ async def timeout( flags.reason, member, dm_sent, + False, flags.duration, ) diff --git a/tux/cogs/moderation/unban.py b/tux/cogs/moderation/unban.py index b6a7b2de..4d5c1d95 100644 --- a/tux/cogs/moderation/unban.py +++ b/tux/cogs/moderation/unban.py @@ -74,7 +74,15 @@ async def unban( case_reason=flags.reason, ) - await self.handle_case_response(ctx, CaseType.UNBAN, case.case_number, flags.reason, user, dm_sent=False) + await self.handle_case_response( + ctx, + CaseType.UNBAN, + case.case_number, + flags.reason, + user, + dm_sent=False, + silent_action=False, + ) async def setup(bot: Tux) -> None: diff --git a/tux/cogs/moderation/unflag.py b/tux/cogs/moderation/unflag.py new file mode 100644 index 00000000..6a6d9498 --- /dev/null +++ b/tux/cogs/moderation/unflag.py @@ -0,0 +1,101 @@ +import discord +from discord.ext import commands + +from prisma.enums import CaseType +from tux.bot import Tux +from tux.database.controllers.case import CaseController +from tux.utils import checks +from tux.utils.flags import UnFlagFlags, generate_usage + +from . import ModerationCogBase + + +class Flag(ModerationCogBase): + def __init__(self, bot: Tux) -> None: + super().__init__(bot) + self.case_controller = CaseController() + self.unflag.usage = generate_usage(self.unflag, UnFlagFlags) + + @commands.hybrid_command( + name="unflag", + aliases=["ufl"], + ) + @commands.guild_only() + @checks.has_pl(2) + async def unflag( + self, + ctx: commands.Context[Tux], + member: discord.Member, + *, + flags: UnFlagFlags, + ) -> None: + """ + Unflag a member from the server. + + Parameters + ---------- + ctx : commands.Context[Tux] + The context in which the command is being invoked. + member : discord.Member + The member to unflag. + flags : UnFlagFlags + The flags for the command. (reason: str, silent: bool) + """ + + assert ctx.guild + + if not await self.is_flagged(ctx.guild.id, member.id): + await ctx.send("User is not flagged.", delete_after=30, ephemeral=True) + return + + moderator = ctx.author + + if not await self.check_conditions(ctx, member, moderator, "unflag"): + return + + case = await self.db.case.insert_case( + case_user_id=member.id, + case_moderator_id=ctx.author.id, + case_type=CaseType.UNFLAG, + case_reason=flags.reason, + guild_id=ctx.guild.id, + ) + + await self.handle_case_response( + ctx, + CaseType.UNFLAG, + case.case_number, + flags.reason, + member, + dm_sent=False, + silent_action=True, + ) + + async def is_flagged(self, guild_id: int, user_id: int) -> bool: + """ + Check if a user is flagged. + + Parameters + ---------- + guild_id : int + The ID of the guild to check in. + user_id : int + The ID of the user to check. + + Returns + ------- + bool + True if the user is flagged, False otherwise. + """ + + flag_cases = await self.case_controller.get_all_cases_by_type(guild_id, CaseType.FLAG) + unflag_cases = await self.case_controller.get_all_cases_by_type(guild_id, CaseType.UNFLAG) + + flag_count = sum(case.case_user_id == user_id for case in flag_cases) + unflag_count = sum(case.case_user_id == user_id for case in unflag_cases) + + return flag_count > unflag_count + + +async def setup(bot: Tux) -> None: + await bot.add_cog(Flag(bot)) diff --git a/tux/cogs/moderation/unjail.py b/tux/cogs/moderation/unjail.py index 2c3df2cb..a866f504 100644 --- a/tux/cogs/moderation/unjail.py +++ b/tux/cogs/moderation/unjail.py @@ -88,7 +88,15 @@ async def unjail( dm_sent = await self.send_dm(ctx, flags.silent, member, flags.reason, "unjailed") - await self.handle_case_response(ctx, CaseType.UNJAIL, unjail_case.case_number, flags.reason, member, dm_sent) + await self.handle_case_response( + ctx, + CaseType.UNJAIL, + unjail_case.case_number, + flags.reason, + member, + dm_sent, + silent_action=False, + ) async def setup(bot: Tux) -> None: diff --git a/tux/cogs/moderation/untimeout.py b/tux/cogs/moderation/untimeout.py index 27b3adbe..9b1da072 100644 --- a/tux/cogs/moderation/untimeout.py +++ b/tux/cogs/moderation/untimeout.py @@ -72,7 +72,15 @@ async def untimeout( ) dm_sent = await self.send_dm(ctx, flags.silent, member, flags.reason, "untimed out") - await self.handle_case_response(ctx, CaseType.UNTIMEOUT, case.case_number, flags.reason, member, dm_sent) + await self.handle_case_response( + ctx, + CaseType.UNTIMEOUT, + case.case_number, + flags.reason, + member, + dm_sent, + silent_action=False, + ) async def setup(bot: Tux) -> None: diff --git a/tux/cogs/moderation/warn.py b/tux/cogs/moderation/warn.py index d72ddb7b..2ae14265 100644 --- a/tux/cogs/moderation/warn.py +++ b/tux/cogs/moderation/warn.py @@ -56,7 +56,15 @@ async def warn( ) dm_sent = await self.send_dm(ctx, flags.silent, member, flags.reason, "warn") - await self.handle_case_response(ctx, CaseType.WARN, case.case_number, flags.reason, member, dm_sent) + await self.handle_case_response( + ctx, + CaseType.WARN, + case.case_number, + flags.reason, + member, + dm_sent, + silent_action=False, + ) async def setup(bot: Tux) -> None: diff --git a/tux/utils/flags.py b/tux/utils/flags.py index 176b6163..45f7162a 100644 --- a/tux/utils/flags.py +++ b/tux/utils/flags.py @@ -309,3 +309,21 @@ class SnippetUnbanFlags(commands.FlagConverter, case_insensitive=True, delimiter aliases=["s", "quiet"], default=False, ) + + +class FlagFlags(commands.FlagConverter, case_insensitive=True, delimiter=" ", prefix="-"): + reason: str = commands.flag( + name="reason", + description="Reason for the user flag.", + aliases=["r"], + default=MISSING, + ) + + +class UnFlagFlags(commands.FlagConverter, case_insensitive=True, delimiter=" ", prefix="-"): + reason: str = commands.flag( + name="reason", + description="Reason for the user unflag.", + aliases=["r"], + default=MISSING, + )