diff --git a/assets/emojis/snippetban.png b/assets/emojis/snippetban.png new file mode 100644 index 0000000..39f0b12 Binary files /dev/null and b/assets/emojis/snippetban.png differ diff --git a/assets/emojis/snippetunban.png b/assets/emojis/snippetunban.png new file mode 100644 index 0000000..62134b6 Binary files /dev/null and b/assets/emojis/snippetunban.png differ diff --git a/tux/cogs/guild/setup.py b/tux/cogs/guild/setup.py new file mode 100644 index 0000000..9507fb4 --- /dev/null +++ b/tux/cogs/guild/setup.py @@ -0,0 +1,85 @@ +import discord +from discord import app_commands +from discord.ext import commands + +from tux.database.controllers import DatabaseController +from tux.utils import checks + + +class Setup(commands.Cog): + def __init__(self, bot: commands.Bot) -> None: + self.bot = bot + self.db = DatabaseController() + self.config = DatabaseController().guild_config + + setup = app_commands.Group(name="setup", description="Set up Tux for your server.") + + @setup.command(name="jail") + @commands.guild_only() + @checks.ac_has_pl(7) + async def setup_jail(self, interaction: discord.Interaction) -> None: + """ + Set up the jail role channel permissions for the server. + + Parameters + ---------- + interaction : discord.Interaction + The discord interaction object. + """ + + if interaction.guild is None: + return + + jail_role_id = await self.config.get_guild_config_field_value(interaction.guild.id, "jail_role_id") + if not jail_role_id: + await interaction.response.send_message("No jail role has been set up for this server.", ephemeral=True) + return + + jail_role = interaction.guild.get_role(jail_role_id) + if not jail_role: + await interaction.response.send_message("The jail role has been deleted.", ephemeral=True) + return + + jail_channel_id = await self.config.get_guild_config_field_value(interaction.guild.id, "jail_channel_id") + if not jail_channel_id: + await interaction.response.send_message("No jail channel has been set up for this server.", ephemeral=True) + return + + await interaction.response.defer(ephemeral=True) + + await self._set_permissions_for_channels(interaction, jail_role, jail_channel_id) + + await interaction.edit_original_response( + content="Permissions have been set up for the jail role.", + ) + + async def _set_permissions_for_channels( + self, + interaction: discord.Interaction, + jail_role: discord.Role, + jail_channel_id: int, + ) -> None: + if interaction.guild is None: + return + + for channel in interaction.guild.channels: + if not isinstance(channel, discord.TextChannel | discord.VoiceChannel | discord.ForumChannel): + continue + + if ( + jail_role in channel.overwrites + and channel.overwrites[jail_role].send_messages is False + and channel.overwrites[jail_role].read_messages is False + and channel.id != jail_channel_id + ): + continue + + await channel.set_permissions(jail_role, send_messages=False, read_messages=False) + if channel.id == jail_channel_id: + await channel.set_permissions(jail_role, send_messages=True, read_messages=True) + + await interaction.edit_original_response(content=f"Setting up permissions for {channel.name}.") + + +async def setup(bot: commands.Bot) -> None: + await bot.add_cog(Setup(bot)) diff --git a/tux/cogs/moderation/__init__.py b/tux/cogs/moderation/__init__.py index dff5908..e19932c 100644 --- a/tux/cogs/moderation/__init__.py +++ b/tux/cogs/moderation/__init__.py @@ -4,7 +4,9 @@ import discord from discord.ext import commands from loguru import logger +from prisma.enums import CaseType from tux.database.controllers import DatabaseController +from tux.utils.constants import Constants as CONST from tux.utils.embeds import create_embed_footer, create_error_embed @@ -14,7 +16,7 @@ class ModerationCogBase(commands.Cog): self.db = DatabaseController() self.config = DatabaseController().guild_config - async def create_embed( + def create_embed( self, ctx: commands.Context[commands.Bot], title: str, @@ -22,6 +24,7 @@ class ModerationCogBase(commands.Cog): color: int, icon_url: str, timestamp: datetime | None = None, + thumbnail_url: str | None = None, ) -> discord.Embed: """ Create an embed for moderation actions. @@ -49,6 +52,7 @@ class ModerationCogBase(commands.Cog): embed = discord.Embed(color=color, timestamp=timestamp or ctx.message.created_at) embed.set_author(name=title, icon_url=icon_url) + embed.set_thumbnail(url=thumbnail_url) footer_text, footer_icon_url = create_embed_footer(ctx) embed.set_footer(text=footer_text, icon_url=footer_icon_url) @@ -166,3 +170,42 @@ class ModerationCogBase(commands.Cog): return False return True + + async def handle_case_response( + self, + ctx: commands.Context[commands.Bot], + case_type: CaseType, + case_id: int | None, + reason: str, + target: discord.Member | discord.User, + duration: str | 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 case_id is not None: + embed = self.create_embed( + ctx, + title=f"Case #{case_id} ({duration} {case_type})" if duration else f"Case #{case_id} ({case_type})", + fields=fields, + color=CONST.EMBED_COLORS["CASE"], + icon_url=CONST.EMBED_ICONS["ACTIVE_CASE"], + ) + embed.set_thumbnail(url=target.avatar) + + else: + embed = self.create_embed( + ctx, + title=f"Case #0 ({duration} {case_type})" if duration else f"Case #0 ({case_type})", + 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) diff --git a/tux/cogs/moderation/ban.py b/tux/cogs/moderation/ban.py index 747bba9..f1d0c5e 100644 --- a/tux/cogs/moderation/ban.py +++ b/tux/cogs/moderation/ban.py @@ -3,9 +3,7 @@ from discord.ext import commands from loguru import logger from prisma.enums import CaseType -from prisma.models import Case from tux.utils import checks -from tux.utils.constants import Constants as CONST from tux.utils.flags import BanFlags from . import ModerationCogBase @@ -48,6 +46,7 @@ class Ban(ModerationCogBase): discord.HTTPException If an error occurs while banning the user. """ + if ctx.guild is None: logger.warning("Ban command used outside of a guild context.") return @@ -74,48 +73,7 @@ class Ban(ModerationCogBase): guild_id=ctx.guild.id, ) - 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.BAN})", - 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) + await self.handle_case_response(ctx, CaseType.BAN, case.case_id, flags.reason, target) async def setup(bot: commands.Bot) -> None: diff --git a/tux/cogs/moderation/cases.py b/tux/cogs/moderation/cases.py index a53e89a..2f513a5 100644 --- a/tux/cogs/moderation/cases.py +++ b/tux/cogs/moderation/cases.py @@ -23,8 +23,8 @@ emojis: dict[str, int] = { "timeout": 1268115809083981886, "warn": 1268115764498399264, "jail": 1268115750392954880, - "snippetban": 1275782294363312172, # Placeholder - "snippetunban": 1275782294363312172, # Placeholder + "snippetban": 1277174953950576681, + "snippetunban": 1277174953292337222, } @@ -269,7 +269,7 @@ class Cases(ModerationCogBase): fields = self._create_case_fields(moderator, target, reason) - embed = await self.create_embed( + embed = self.create_embed( ctx, title=f"Case #{case.case_number} ({case.case_type}) {action}", fields=fields, diff --git a/tux/cogs/moderation/jail.py b/tux/cogs/moderation/jail.py index 6a4222a..ee48982 100644 --- a/tux/cogs/moderation/jail.py +++ b/tux/cogs/moderation/jail.py @@ -1,12 +1,9 @@ import discord -from discord import app_commands from discord.ext import commands from loguru import logger from prisma.enums import CaseType -from prisma.models import Case from tux.utils import checks -from tux.utils.constants import Constants as CONST from tux.utils.flags import JailFlags from . import ModerationCogBase @@ -16,74 +13,6 @@ class Jail(ModerationCogBase): def __init__(self, bot: commands.Bot) -> None: super().__init__(bot) - @app_commands.command( - name="setup_jail", - ) - @commands.guild_only() - @checks.ac_has_pl(7) - async def setup_jail(self, interaction: discord.Interaction) -> None: - """ - Set up the jail role channel permissions for the server. - - Parameters - ---------- - interaction : discord.Interaction - The discord interaction object. - """ - - if interaction.guild is None: - return - - jail_role_id = await self.config.get_guild_config_field_value(interaction.guild.id, "jail_role_id") - if not jail_role_id: - await interaction.response.send_message("No jail role has been set up for this server.", ephemeral=True) - return - - jail_role = interaction.guild.get_role(jail_role_id) - if not jail_role: - await interaction.response.send_message("The jail role has been deleted.", ephemeral=True) - return - - jail_channel_id = await self.config.get_guild_config_field_value(interaction.guild.id, "jail_channel_id") - if not jail_channel_id: - await interaction.response.send_message("No jail channel has been set up for this server.", ephemeral=True) - return - - await interaction.response.defer(ephemeral=True) - - await self._set_permissions_for_channels(interaction, jail_role, jail_channel_id) - - await interaction.edit_original_response( - content="Permissions have been set up for the jail role.", - ) - - async def _set_permissions_for_channels( - self, - interaction: discord.Interaction, - jail_role: discord.Role, - jail_channel_id: int, - ) -> None: - if interaction.guild is None: - return - - for channel in interaction.guild.channels: - if not isinstance(channel, discord.TextChannel | discord.VoiceChannel | discord.ForumChannel): - continue - - if ( - jail_role in channel.overwrites - and channel.overwrites[jail_role].send_messages is False - and channel.overwrites[jail_role].read_messages is False - and channel.id != jail_channel_id - ): - continue - - await channel.set_permissions(jail_role, send_messages=False, read_messages=False) - if channel.id == jail_channel_id: - await channel.set_permissions(jail_role, send_messages=True, read_messages=True) - - await interaction.edit_original_response(content=f"Setting up permissions for {channel.name}.") - @commands.hybrid_command( name="jail", aliases=["j"], @@ -91,7 +20,7 @@ class Jail(ModerationCogBase): ) @commands.guild_only() @checks.has_pl(2) - async def jail( + async def jail( # noqa: PLR0911 self, ctx: commands.Context[commands.Bot], target: discord.Member, @@ -137,38 +66,55 @@ class Jail(ModerationCogBase): case_target_roles = [role.id for role in target_roles] - await self._jail_user(ctx, target, flags, jail_role, target_roles) - - case = await self._insert_jail_case(ctx, target, flags.reason, case_target_roles) - - await self.handle_case_response(ctx, case, "created", flags.reason, target) - - async def _insert_jail_case( - self, - ctx: commands.Context[commands.Bot], - target: discord.Member, - reason: str, - case_target_roles: list[int] | None = None, - ) -> Case | None: - if not ctx.guild: - logger.warning("Jail command used outside of a guild context.") - return None - try: - return await self.db.case.insert_case( + case = await self.db.case.insert_case( case_target_id=target.id, case_moderator_id=ctx.author.id, case_type=CaseType.JAIL, - case_reason=reason, + case_reason=flags.reason, guild_id=ctx.guild.id, case_target_roles=case_target_roles, ) - except Exception as e: - logger.error(f"Failed to insert jail case for {target}. {e}") - await ctx.send(f"Failed to insert jail case for {target}. {e}", delete_after=30, ephemeral=True) - return None - def _get_manageable_roles(self, target: discord.Member, jail_role: discord.Role) -> list[discord.Role]: + except Exception as e: + logger.error(f"Failed to jail {target}. {e}") + await ctx.send(f"Failed to jail {target}. {e}", delete_after=30, ephemeral=True) + return + + try: + if target_roles: + await target.remove_roles(*target_roles, reason=flags.reason, atomic=False) + await target.add_roles(jail_role, reason=flags.reason) + + except (discord.Forbidden, discord.HTTPException) as e: + logger.error(f"Failed to jail {target}. {e}") + await ctx.send(f"Failed to jail {target}. {e}", delete_after=30, ephemeral=True) + return + + await self.send_dm(ctx, flags.silent, target, flags.reason, "jailed") + await self.handle_case_response(ctx, CaseType.JAIL, case.case_id, flags.reason, target) + + def _get_manageable_roles( + self, + target: discord.Member, + jail_role: discord.Role, + ) -> list[discord.Role]: + """ + Get the roles that can be managed by the bot. + + Parameters + ---------- + target : discord.Member + The member to jail. + jail_role : discord.Role + The jail role. + + Returns + ------- + list[discord.Role] + The roles that can be managed by the bot. + """ + return [ role for role in target.roles @@ -182,70 +128,6 @@ class Jail(ModerationCogBase): and role.is_assignable() ] - async def _jail_user( - self, - ctx: commands.Context[commands.Bot], - target: discord.Member, - flags: JailFlags, - jail_role: discord.Role, - target_roles: list[discord.Role], - ) -> None: - try: - await self.send_dm(ctx, flags.silent, target, flags.reason, "jailed") - - if target_roles: - await target.remove_roles(*target_roles, reason=flags.reason, atomic=False) - await target.add_roles(jail_role, reason=flags.reason) - - except (discord.Forbidden, discord.HTTPException) as e: - logger.error(f"Failed to jail {target}. {e}") - await ctx.send(f"Failed to jail {target}. {e}", delete_after=30, ephemeral=True) - return - - 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: - fields = [ - ("Moderator", f"__{ctx.author}__\n`{ctx.author.id}`", True), - ("Target", f"__{target}__\n`{target.id}`", True), - ("Reason", f"> {reason}", False), - ] - - if previous_reason: - fields.append(("Previous Reason", f"> {previous_reason}", False)) - - embed = await self._create_case_embed(ctx, case, action, fields, target) - - await self.send_embed(ctx, embed, log_type="mod") - await ctx.send(embed=embed, delete_after=30, ephemeral=True) - - async def _create_case_embed( - self, - ctx: commands.Context[commands.Bot], - case: Case | None, - action: str, - fields: list[tuple[str, str, bool]], - target: discord.Member | discord.User, - ) -> discord.Embed: - title = f"Case #{case.case_number} ({case.case_type}) {action}" if case else f"Case {action} ({CaseType.JAIL})" - - embed = await self.create_embed( - ctx, - title=title, - fields=fields, - color=CONST.EMBED_COLORS["CASE"], - icon_url=CONST.EMBED_ICONS["ACTIVE_CASE"], - ) - - embed.set_thumbnail(url=target.avatar) - return embed - async def setup(bot: commands.Bot) -> None: await bot.add_cog(Jail(bot)) diff --git a/tux/cogs/moderation/kick.py b/tux/cogs/moderation/kick.py index 697a51f..935b0b4 100644 --- a/tux/cogs/moderation/kick.py +++ b/tux/cogs/moderation/kick.py @@ -3,9 +3,7 @@ from discord.ext import commands from loguru import logger from prisma.enums import CaseType -from prisma.models import Case from tux.utils import checks -from tux.utils.constants import Constants as CONST from tux.utils.flags import KickFlags from . import ModerationCogBase @@ -75,48 +73,7 @@ class Kick(ModerationCogBase): guild_id=ctx.guild.id, ) - 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} {action} ({case.case_type})", - 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.KICK})", - 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) + await self.handle_case_response(ctx, CaseType.KICK, case.case_id, flags.reason, target) async def setup(bot: commands.Bot) -> None: diff --git a/tux/cogs/moderation/snippetban.py b/tux/cogs/moderation/snippetban.py index 2555f4f..98f6c17 100644 --- a/tux/cogs/moderation/snippetban.py +++ b/tux/cogs/moderation/snippetban.py @@ -3,10 +3,8 @@ 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 @@ -20,7 +18,7 @@ class SnippetBan(ModerationCogBase): @commands.hybrid_command( name="snippetban", aliases=["sb"], - usage="snippetban [target]", + usage="snippetban [target] [reason] ", ) @commands.guild_only() @checks.has_pl(3) @@ -49,60 +47,25 @@ class SnippetBan(ModerationCogBase): return if await self.is_snippetbanned(ctx.guild.id, target.id): - await ctx.send("User is already snippet banned.", delete_after=30) + await ctx.send("User is already snippet banned.", delete_after=30, ephemeral=True) 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"], + try: + 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_embed(ctx, embed, log_type="mod") - await ctx.send(embed=embed, delete_after=30, ephemeral=True) + except Exception as e: + logger.error(f"Failed to ban {target}. {e}") + await ctx.send(f"Failed to ban {target}. {e}", delete_after=30) + return + + await self.send_dm(ctx, flags.silent, target, flags.reason, "snippet banned") + await self.handle_case_response(ctx, CaseType.SNIPPETBAN, case.case_id, flags.reason, target) 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 d44ee50..eac5dd5 100644 --- a/tux/cogs/moderation/snippetunban.py +++ b/tux/cogs/moderation/snippetunban.py @@ -3,10 +3,8 @@ 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 @@ -20,7 +18,7 @@ class SnippetUnban(ModerationCogBase): @commands.hybrid_command( name="snippetunban", aliases=["sub"], - usage="snippetunban [target]", + usage="snippetunban [target] [reason] ", ) @commands.guild_only() @checks.has_pl(3) @@ -48,62 +46,26 @@ class SnippetUnban(ModerationCogBase): 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) + await ctx.send("User is not snippet banned.", delete_after=30, ephemeral=True) 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"], + try: + 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_embed(ctx, embed, log_type="mod") - await ctx.send(embed=embed, delete_after=30, ephemeral=True) + except Exception as e: + logger.error(f"Failed to snippet unban {target}. {e}") + await ctx.send(f"Failed to snippet unban {target}. {e}", delete_after=30, ephemeral=True) + return + + await self.send_dm(ctx, flags.silent, target, flags.reason, "snippet unbanned") + await self.handle_case_response(ctx, CaseType.SNIPPETUNBAN, case.case_id, flags.reason, target) async def is_snippetbanned(self, guild_id: int, user_id: int) -> bool: """ diff --git a/tux/cogs/moderation/timeout.py b/tux/cogs/moderation/timeout.py index fd4620f..c96d67f 100644 --- a/tux/cogs/moderation/timeout.py +++ b/tux/cogs/moderation/timeout.py @@ -6,9 +6,7 @@ from discord.ext import commands from loguru import logger from prisma.enums import CaseType -from prisma.models import Case from tux.utils import checks -from tux.utils.constants import Constants as CONST from tux.utils.flags import TimeoutFlags from . import ModerationCogBase @@ -106,7 +104,6 @@ class Timeout(ModerationCogBase): duration = parse_time_string(flags.duration) try: - await self.send_dm(ctx, flags.silent, target, flags.reason, f"timed out for {flags.duration}") await target.timeout(duration, reason=flags.reason) except discord.DiscordException as e: @@ -122,49 +119,8 @@ class Timeout(ModerationCogBase): guild_id=ctx.guild.id, ) - await self.handle_case_response(ctx, flags, case, "created", flags.reason, target) - - async def handle_case_response( - self, - ctx: commands.Context[commands.Bot], - flags: TimeoutFlags, - 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} {action} ({flags.duration} {case.case_type})", - 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 #0 {action} ({flags.duration} {CaseType.TIMEOUT})", - 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) + await self.send_dm(ctx, flags.silent, target, flags.reason, f"timed out for {flags.duration}") + await self.handle_case_response(ctx, CaseType.TIMEOUT, case.case_id, flags.reason, target, flags.duration) async def setup(bot: commands.Bot) -> None: diff --git a/tux/cogs/moderation/unban.py b/tux/cogs/moderation/unban.py index 0eadebb..52ccf94 100644 --- a/tux/cogs/moderation/unban.py +++ b/tux/cogs/moderation/unban.py @@ -3,9 +3,7 @@ from discord.ext import commands from loguru import logger from prisma.enums import CaseType -from prisma.models import Case from tux.utils import checks -from tux.utils.constants import Constants as CONST from tux.utils.flags import UnbanFlags from . import ModerationCogBase @@ -35,8 +33,6 @@ class Unban(ModerationCogBase): ---------- ctx : commands.Context[commands.Bot] The context object for the command. - target : discord.Member - The member to unban. flags : UnbanFlags The flags for the command (username_or_id: str, reason: str). @@ -76,48 +72,7 @@ class Unban(ModerationCogBase): case_reason=flags.reason, ) - await self.handle_case_response(ctx, case, "created", flags.reason, user) - - 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.UNBAN})", - 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) + await self.handle_case_response(ctx, CaseType.UNBAN, case.case_id, flags.reason, user) async def setup(bot: commands.Bot) -> None: diff --git a/tux/cogs/moderation/unjail.py b/tux/cogs/moderation/unjail.py index ff4b191..5c10b5c 100644 --- a/tux/cogs/moderation/unjail.py +++ b/tux/cogs/moderation/unjail.py @@ -3,9 +3,7 @@ from discord.ext import commands from loguru import logger from prisma.enums import CaseType -from prisma.models import Case from tux.utils import checks -from tux.utils.constants import Constants as CONST from tux.utils.flags import UnjailFlags from . import ModerationCogBase @@ -22,7 +20,7 @@ class Unjail(ModerationCogBase): ) @commands.guild_only() @checks.has_pl(2) - async def unjail( + async def unjail( # noqa: PLR0911 self, ctx: commands.Context[commands.Bot], target: discord.Member, @@ -51,151 +49,55 @@ class Unjail(ModerationCogBase): if not await self.check_conditions(ctx, target, moderator, "unjail"): return - jail_role = await self._get_jail_role(ctx) - if not jail_role: + jail_role_id = await self.config.get_jail_role_id(ctx.guild.id) + jail_role = ctx.guild.get_role(jail_role_id) if jail_role_id else None + + jail_channel_id = await self.config.get_jail_channel_id(ctx.guild.id) + + if not all([jail_role_id, jail_role, jail_channel_id]): + error_msgs = { + not jail_role_id: "No jail role has been set up for this server.", + not jail_role: "The jail role has been deleted.", + not jail_channel_id: "No jail channel has been set up for this server.", + } + + for condition, msg in error_msgs.items(): + if condition: + await ctx.send(msg, delete_after=30, ephemeral=True) return if jail_role not in target.roles: await ctx.send("The member is not jailed.", delete_after=30, ephemeral=True) return - if not await self._check_jail_channel(ctx): - return - case = await self.db.case.get_last_jail_case_by_target_id(ctx.guild.id, target.id) if not case: await ctx.send("No jail case found for this member.", delete_after=30, ephemeral=True) return - await self._unjail_user(ctx, target, jail_role, case, flags.reason) - - unjail_case = await self._insert_unjail_case(ctx, target, flags.reason) - - await self.handle_case_response(ctx, unjail_case, "created", flags.reason, target) - - async def _get_jail_role(self, ctx: commands.Context[commands.Bot]) -> discord.Role | None: - if ctx.guild is None: - logger.warning("Unjail command used outside of a guild context.") - return None - - jail_role_id = await self.config.get_jail_role_id(ctx.guild.id) - if not jail_role_id: - await ctx.send("No jail role has been set up for this server.", delete_after=30, ephemeral=True) - return None - - jail_role = ctx.guild.get_role(jail_role_id) - if not jail_role: - await ctx.send("The jail role has been deleted.", delete_after=30, ephemeral=True) - return None - - return jail_role - - async def _check_jail_channel(self, ctx: commands.Context[commands.Bot]) -> bool: - if ctx.guild is None: - logger.warning("Unjail command used outside of a guild context.") - return False - - jail_channel_id = await self.config.get_jail_channel_id(ctx.guild.id) - if not jail_channel_id: - await ctx.send("No jail channel has been set up for this server.", delete_after=30, ephemeral=True) - return False - - return True - - async def _unjail_user( - self, - ctx: commands.Context[commands.Bot], - target: discord.Member, - jail_role: discord.Role, - case: Case, - reason: str, - ) -> None: - if ctx.guild is None: - logger.warning("Unjail command used outside of a guild context.") - return - try: - await target.remove_roles(jail_role, reason=reason) - previous_roles = [await commands.RoleConverter().convert(ctx, str(role)) for role in case.case_target_roles] - if previous_roles: - await target.add_roles(*previous_roles, reason=reason, atomic=False) + await target.remove_roles(jail_role, reason=flags.reason, atomic=True) + await target.add_roles(*previous_roles, reason=flags.reason, atomic=True) else: await ctx.send("No previous roles found for the member.", delete_after=30, ephemeral=True) + return except (discord.Forbidden, discord.HTTPException) as e: logger.error(f"Failed to unjail member {target}. {e}") await ctx.send(f"Failed to unjail member {target}. {e}", delete_after=30, ephemeral=True) + return - async def _insert_unjail_case( - self, - ctx: commands.Context[commands.Bot], - target: discord.Member, - reason: str, - ) -> Case | None: - if not ctx.guild: - logger.warning("Unjail command used outside of a guild context.") - return None - - try: - return await self.db.case.insert_case( - guild_id=ctx.guild.id, - case_target_id=target.id, - case_moderator_id=ctx.author.id, - case_type=CaseType.UNJAIL, - case_reason=reason, - ) - - except Exception as e: - logger.error(f"Failed to insert unjail case for {target}. {e}") - await ctx.send(f"Failed to insert unjail case for {target}. {e}", delete_after=30, ephemeral=True) - return None - - 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: - fields = [ - ("Moderator", f"__{ctx.author}__\n`{ctx.author.id}`", True), - ("Target", f"__{target}__\n`{target.id}`", True), - ("Reason", f"> {reason}", False), - ] - - if previous_reason: - fields.append(("Previous Reason", f"> {previous_reason}", False)) - - embed = await self._create_case_embed(ctx, case, action, fields, target) - await self.send_embed(ctx, embed, log_type="mod") - await ctx.send(embed=embed, delete_after=30, ephemeral=True) - - async def _create_case_embed( - self, - ctx: commands.Context[commands.Bot], - case: Case | None, - action: str, - fields: list[tuple[str, str, bool]], - target: discord.Member | discord.User, - ) -> discord.Embed: - title = ( - f"Case #{case.case_number} ({case.case_type}) {action}" if case else f"Case {action} ({CaseType.UNJAIL})" + unjail_case = await self.db.case.insert_case( + guild_id=ctx.guild.id, + case_target_id=target.id, + case_moderator_id=ctx.author.id, + case_type=CaseType.UNJAIL, + case_reason=flags.reason, ) - embed = await self.create_embed( - ctx, - title=title, - fields=fields, - color=CONST.EMBED_COLORS["CASE"], - icon_url=CONST.EMBED_ICONS["ACTIVE_CASE"], - ) - - embed.set_thumbnail(url=target.avatar) - return embed + await self.handle_case_response(ctx, CaseType.UNJAIL, unjail_case.case_id, flags.reason, target) async def setup(bot: commands.Bot) -> None: diff --git a/tux/cogs/moderation/untimeout.py b/tux/cogs/moderation/untimeout.py index 60587cf..9972528 100644 --- a/tux/cogs/moderation/untimeout.py +++ b/tux/cogs/moderation/untimeout.py @@ -3,9 +3,7 @@ from discord.ext import commands from loguru import logger from prisma.enums import CaseType -from prisma.models import Case from tux.utils import checks -from tux.utils.constants import Constants as CONST from tux.utils.flags import UntimeoutFlags from . import ModerationCogBase @@ -59,7 +57,6 @@ class Untimeout(ModerationCogBase): await ctx.send(f"{target} is not currently timed out.", delete_after=30, ephemeral=True) try: - await self.send_dm(ctx, flags.silent, target, flags.reason, "untimed out") await target.timeout(None, reason=flags.reason) except discord.DiscordException as e: await ctx.send(f"Failed to untimeout {target}. {e}", delete_after=30, ephemeral=True) @@ -74,49 +71,8 @@ class Untimeout(ModerationCogBase): guild_id=ctx.guild.id, ) - await self.handle_case_response(ctx, flags, case, "created", flags.reason, target) - - async def handle_case_response( - self, - ctx: commands.Context[commands.Bot], - flags: UntimeoutFlags, - 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} {action} ({case.case_type})", - 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 #0 {action} ({CaseType.UNTIMEOUT})", - 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) + await self.send_dm(ctx, flags.silent, target, flags.reason, "untimed out") + await self.handle_case_response(ctx, CaseType.UNTIMEOUT, case.case_id, flags.reason, target) async def setup(bot: commands.Bot) -> None: diff --git a/tux/cogs/moderation/warn.py b/tux/cogs/moderation/warn.py index 975d86c..537a179 100644 --- a/tux/cogs/moderation/warn.py +++ b/tux/cogs/moderation/warn.py @@ -3,9 +3,7 @@ from discord.ext import commands from loguru import logger from prisma.enums import CaseType -from prisma.models import Case from tux.utils import checks -from tux.utils.constants import Constants as CONST from tux.utils.flags import WarnFlags from . import ModerationCogBase @@ -61,48 +59,7 @@ class Warn(ModerationCogBase): guild_id=ctx.guild.id, ) - 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.WARN})", - 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) + await self.handle_case_response(ctx, CaseType.WARN, case.case_id, flags.reason, target) async def setup(bot: commands.Bot) -> None: