diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 8ed537cb4..fb10a5a3a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -28,11 +28,13 @@ enum CaseType { HACKBAN TEMPBAN KICK + SNIPPETBAN TIMEOUT UNTIMEOUT WARN JAIL UNJAIL + SNIPPETUNBAN } // Docs: https://www.prisma.io/docs/orm/prisma-schema/data-model/models#defining-models diff --git a/tux/cogs/moderation/cases.py b/tux/cogs/moderation/cases.py index eba325c51..f48a2cc06 100644 --- a/tux/cogs/moderation/cases.py +++ b/tux/cogs/moderation/cases.py @@ -23,6 +23,8 @@ "timeout": 1268115809083981886, "warn": 1268115764498399264, "jail": 1268115750392954880, + "snippetban": 1275782294363312172, # Placeholder + "snippetunban": 1275782294363312172, # Placeholder } @@ -367,6 +369,8 @@ def _get_case_type_emoji(self, case_type: CaseType) -> discord.Emoji | None: CaseType.WARN: "warn", CaseType.JAIL: "jail", CaseType.UNJAIL: "jail", + CaseType.SNIPPETBAN: "snippetban", + CaseType.SNIPPETUNBAN: "snippetunban", } emoji_name = emoji_map.get(case_type) if emoji_name is not None: @@ -378,9 +382,10 @@ def _get_case_type_emoji(self, case_type: CaseType) -> discord.Emoji | None: def _get_case_action_emoji(self, case_type: CaseType) -> discord.Emoji | None: action = ( "added" - if case_type in [CaseType.BAN, CaseType.KICK, CaseType.TIMEOUT, CaseType.WARN, CaseType.JAIL] + if case_type + in [CaseType.BAN, CaseType.KICK, CaseType.TIMEOUT, CaseType.WARN, CaseType.JAIL, CaseType.SNIPPETBAN] else "removed" - if case_type in [CaseType.UNBAN, CaseType.UNTIMEOUT, CaseType.UNJAIL] + if case_type in [CaseType.UNBAN, CaseType.UNTIMEOUT, CaseType.UNJAIL, CaseType.SNIPPETUNBAN] else None ) if action is not None: diff --git a/tux/cogs/moderation/snippetban.py b/tux/cogs/moderation/snippetban.py new file mode 100644 index 000000000..bb5797736 --- /dev/null +++ b/tux/cogs/moderation/snippetban.py @@ -0,0 +1,117 @@ +import discord +from discord.ext import commands +from loguru import logger + +from prisma.enums import CaseType +from prisma.models import Case +from tux.database.controllers.case import CaseController +from tux.utils import checks +from tux.utils.constants import Constants as CONST +from tux.utils.flags import SnippetBanFlags + +from . import ModerationCogBase + + +class SnippetBan(ModerationCogBase): + def __init__(self, bot: commands.Bot) -> None: + super().__init__(bot) + self.case_controller = CaseController() + + @commands.hybrid_command( + name="snippetban", + aliases=["sb"], + usage="snippetban [target]", + ) + @commands.guild_only() + @checks.has_pl(3) + async def snippet_ban( + self, + ctx: commands.Context[commands.Bot], + target: discord.Member, + *, + flags: SnippetBanFlags, + ) -> None: + """ + Ban a user from creating snippets. + + Parameters + ---------- + ctx : commands.Context[commands.Bot] + The context object. + target : discord.Member + The member to snippet ban. + flags : SnippetBanFlags + The flags for the command. (reason: str, silent: bool) + """ + if ctx.guild is None: + logger.warning("Snippet ban command used outside of a guild context.") + return + + if await self.is_snippetbanned(ctx.guild.id, target.id): + await ctx.send("User is already snippet banned.", delete_after=30) + return + + case = await self.db.case.insert_case( + case_target_id=target.id, + case_moderator_id=ctx.author.id, + case_type=CaseType.SNIPPETBAN, + case_reason=flags.reason, + guild_id=ctx.guild.id, + ) + + await self.send_dm(ctx, flags.silent, target, flags.reason, "Snippet Banned") + await self.handle_case_response(ctx, case, "created", flags.reason, target) + + async def handle_case_response( + self, + ctx: commands.Context[commands.Bot], + case: Case | None, + action: str, + reason: str, + target: discord.Member | discord.User, + previous_reason: str | None = None, + ) -> None: + moderator = ctx.author + + fields = [ + ("Moderator", f"__{moderator}__\n`{moderator.id}`", True), + ("Target", f"__{target}__\n`{target.id}`", True), + ("Reason", f"> {reason}", False), + ] + + if previous_reason: + fields.append(("Previous Reason", f"> {previous_reason}", False)) + + if case is not None: + embed = await self.create_embed( + ctx, + title=f"Case #{case.case_number} ({case.case_type}) {action}", + fields=fields, + color=CONST.EMBED_COLORS["CASE"], + icon_url=CONST.EMBED_ICONS["ACTIVE_CASE"], + ) + embed.set_thumbnail(url=target.avatar) + else: + embed = await self.create_embed( + ctx, + title=f"Case {action} ({CaseType.SNIPPETBAN})", + fields=fields, + color=CONST.EMBED_COLORS["CASE"], + icon_url=CONST.EMBED_ICONS["ACTIVE_CASE"], + ) + + await self.send_embed(ctx, embed, log_type="mod") + await ctx.send(embed=embed, delete_after=30, ephemeral=True) + + async def is_snippetbanned(self, guild_id: int, user_id: int) -> bool: + ban_cases = await self.case_controller.get_all_cases_by_type(guild_id, CaseType.SNIPPETBAN) + unban_cases = await self.case_controller.get_all_cases_by_type(guild_id, CaseType.SNIPPETUNBAN) + + ban_count = sum(1 for case in ban_cases if case.case_target_id == user_id) + unban_count = sum(1 for case in unban_cases if case.case_target_id == user_id) + + return ban_count > unban_count + + +async def setup(bot: commands.Bot) -> None: + await bot.add_cog(SnippetBan(bot)) diff --git a/tux/cogs/moderation/snippetunban.py b/tux/cogs/moderation/snippetunban.py new file mode 100644 index 000000000..5c223fed0 --- /dev/null +++ b/tux/cogs/moderation/snippetunban.py @@ -0,0 +1,102 @@ +import discord +from discord.ext import commands +from loguru import logger + +from prisma.enums import CaseType +from prisma.models import Case +from tux.database.controllers.case import CaseController +from tux.utils import checks +from tux.utils.constants import Constants as CONST +from tux.utils.flags import SnippetUnbanFlags + +from . import ModerationCogBase + + +class SnippetUnban(ModerationCogBase): + def __init__(self, bot: commands.Bot) -> None: + super().__init__(bot) + self.case_controller = CaseController() + + @commands.hybrid_command( + name="snippetunban", + aliases=["sub"], + usage="snippetunban [target]", + ) + @commands.guild_only() + @checks.has_pl(3) + async def snippet_unban( + self, + ctx: commands.Context[commands.Bot], + target: discord.Member, + *, + flags: SnippetUnbanFlags, + ): + if ctx.guild is None: + logger.warning("Snippet ban command used outside of a guild context.") + return + + # Check if the user is already snippet banned + if not await self.is_snippetbanned(ctx.guild.id, target.id): + await ctx.send("User is not snippet banned.", delete_after=30) + return + + case = await self.db.case.insert_case( + case_target_id=target.id, + case_moderator_id=ctx.author.id, + case_type=CaseType.SNIPPETUNBAN, + case_reason=flags.reason, + guild_id=ctx.guild.id, + ) + + await self.send_dm(ctx, flags.silent, target, flags.reason, "Snippet Unbanned") + await self.handle_case_response(ctx, case, "created", flags.reason, target) + + async def handle_case_response( + self, + ctx: commands.Context[commands.Bot], + case: Case | None, + action: str, + reason: str, + target: discord.Member | discord.User, + previous_reason: str | None = None, + ) -> None: + moderator = ctx.author + + fields = [ + ("Moderator", f"__{moderator}__\n`{moderator.id}`", True), + ("Target", f"__{target}__\n`{target.id}`", True), + ("Reason", f"> {reason}", False), + ] + + if previous_reason: + fields.append(("Previous Reason", f"> {previous_reason}", False)) + + if case is not None: + embed = await self.create_embed( + ctx, + title=f"Case #{case.case_number} ({case.case_type}) {action}", + fields=fields, + color=CONST.EMBED_COLORS["CASE"], + icon_url=CONST.EMBED_ICONS["ACTIVE_CASE"], + ) + embed.set_thumbnail(url=target.avatar) + else: + embed = await self.create_embed( + ctx, + title=f"Case {action} ({CaseType.SNIPPETUNBAN})", + fields=fields, + color=CONST.EMBED_COLORS["CASE"], + icon_url=CONST.EMBED_ICONS["ACTIVE_CASE"], + ) + + await self.send_embed(ctx, embed, log_type="mod") + await ctx.send(embed=embed, delete_after=30, ephemeral=True) + + async def is_snippetbanned(self, guild_id: int, user_id: int) -> bool: + ban_cases = await self.case_controller.get_all_cases_by_type(guild_id, CaseType.SNIPPETBAN) + unban_cases = await self.case_controller.get_all_cases_by_type(guild_id, CaseType.SNIPPETUNBAN) + + ban_count = sum(1 for case in ban_cases if case.case_target_id == user_id) + unban_count = sum(1 for case in unban_cases if case.case_target_id == user_id) + + return ban_count > unban_count diff --git a/tux/cogs/utility/snippets.py b/tux/cogs/utility/snippets.py index 6972c6423..ebee50580 100644 --- a/tux/cogs/utility/snippets.py +++ b/tux/cogs/utility/snippets.py @@ -8,8 +8,9 @@ from loguru import logger from reactionmenu import ViewButton, ViewMenu +from prisma.enums import CaseType from prisma.models import Snippet -from tux.database.controllers import DatabaseController +from tux.database.controllers import CaseController, DatabaseController from tux.utils import checks from tux.utils.constants import Constants as CONST from tux.utils.embeds import EmbedCreator, create_embed_footer, create_error_embed @@ -20,6 +21,16 @@ def __init__(self, bot: commands.Bot) -> None: self.bot = bot self.db = DatabaseController().snippet self.config = DatabaseController().guild_config + self.case_controller = CaseController() + + async def is_snippetbanned(self, guild_id: int, user_id: int) -> bool: + ban_cases = await self.case_controller.get_all_cases_by_type(guild_id, CaseType.SNIPPETBAN) + unban_cases = await self.case_controller.get_all_cases_by_type(guild_id, CaseType.SNIPPETUNBAN) + + ban_count = sum(1 for case in ban_cases if case.case_target_id == user_id) + unban_count = sum(1 for case in unban_cases if case.case_target_id == user_id) + + return ban_count > unban_count @commands.command( name="snippets", @@ -359,6 +370,10 @@ async def create_snippet(self, ctx: commands.Context[commands.Bot], *, arg: str) await ctx.send("This command cannot be used in direct messages.") return + if await self.is_snippetbanned(ctx.guild.id, ctx.author.id): + await ctx.send("You are banned from using snippets.") + return + args = arg.split(" ") if len(args) < 2: embed = create_error_embed(error="Please provide a name and content for the snippet.") diff --git a/tux/utils/flags.py b/tux/utils/flags.py index 8dd4d5dd2..ae419ef5b 100644 --- a/tux/utils/flags.py +++ b/tux/utils/flags.py @@ -190,3 +190,33 @@ class WarnFlags(commands.FlagConverter, delimiter=" ", prefix="-"): aliases=["s", "quiet"], default=False, ) + + +class SnippetBanFlags(commands.FlagConverter, delimiter=" ", prefix="-"): + reason: str = commands.flag( + name="reason", + description="The reason for the snippet ban.", + aliases=["r"], + default=MISSING, + ) + silent: bool = commands.flag( + name="silent", + description="Do not send a DM to the target.", + aliases=["s", "quiet"], + default=False, + ) + + +class SnippetUnbanFlags(commands.FlagConverter, delimiter=" ", prefix="-"): + reason: str = commands.flag( + name="reason", + description="The reason for the snippet unban.", + aliases=["r"], + default=MISSING, + ) + silent: bool = commands.flag( + name="silent", + description="Do not send a DM to the target.", + aliases=["s", "quiet"], + default=False, + )