1
Fork 0
mirror of https://github.com/allthingslinux/tux.git synced 2024-10-02 16:43:12 +00:00

feat(emojis): add new snippetban and snippetunban emojis

feat(setup.py): add new Setup class for server setup
refactor(moderation): move handle_case_response method to ModerationCogBase class for code reusability
fix(moderation): replace async create_embed method with synchronous one
feat(moderation): add support for case handling in ban command
feat(cases.py): update snippetban and snippetunban emoji ids
refactor(cases.py): replace async create_embed method with synchronous one

refactor(jail.py, kick.py): remove redundant code and simplify jail and kick commands
feat(jail.py, kick.py): improve error handling and logging for jail and kick commands
style(jail.py, kick.py): improve code readability and maintainability by removing unnecessary imports and functions

refactor(snippetban.py, snippetunban.py, timeout.py): simplify case handling logic and error handling
feat(snippetban.py, snippetunban.py): add reason parameter to usage and make error messages ephemeral
fix(timeout.py): move send_dm call after successful timeout to ensure user is notified only when action is successful

refactor(unban.py, unjail.py): simplify unban and unjail commands by removing redundant code
feat(unjail.py): add error handling for missing jail role or jail channel
fix(unjail.py): ensure atomicity when removing jail role and adding previous roles to prevent partial role assignment

refactor(untimeout.py, warn.py): remove redundant code and simplify case response handling
fix(untimeout.py, warn.py): move send_dm call to after timeout/warn action to ensure it's sent after action is successful
This commit is contained in:
kzndotsh 2024-08-25 08:53:50 +00:00
parent 9e11b5fb67
commit 8e3d8cdc66
15 changed files with 242 additions and 666 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

85
tux/cogs/guild/setup.py Normal file
View file

@ -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))

View file

@ -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)

View file

@ -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:

View file

@ -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,

View file

@ -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))

View file

@ -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:

View file

@ -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] <silent>",
)
@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:
"""

View file

@ -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] <silent>",
)
@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:
"""

View file

@ -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:

View file

@ -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:

View file

@ -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:

View file

@ -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:

View file

@ -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: