Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 7 additions & 2 deletions tux/cogs/moderation/cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
"timeout": 1268115809083981886,
"warn": 1268115764498399264,
"jail": 1268115750392954880,
"snippetban": 1275782294363312172, # Placeholder
"snippetunban": 1275782294363312172, # Placeholder
}


Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down
117 changes: 117 additions & 0 deletions tux/cogs/moderation/snippetban.py
Original file line number Diff line number Diff line change
@@ -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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Refactor shared functionality into a base class

The handle_case_response method and overall command structure are very similar in both snippetban.py and snippetunban.py. Consider creating a base class or utility functions to reduce code duplication and improve maintainability.

class SnippetModBase:
    async def handle_case_response(
        self,
        ctx: commands.Context[commands.Bot],
        case: Case,
        action: str,
        reason: str,
        target: discord.Member
    ) -> None:
        # Implementation here

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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Move is_snippetbanned method to a common location to avoid duplication

This method is duplicated in both snippetban.py and snippetunban.py. Consider moving it to a shared utility module or base class to ensure consistency and reduce duplication.

from tux.utils.moderation import is_snippetbanned

class SnippetBan(commands.Cog):
    # ... existing code ...

    @commands.command()
    async def snippetban(self, ctx, user: discord.Member):
        if await is_snippetbanned(self.case_controller, ctx.guild.id, user.id):
            # ... rest of the method

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): Simplify constant sum() call (simplify-constant-sum)

Suggested change
ban_count = sum(1 for case in ban_cases if case.case_target_id == user_id)
ban_count = sum(bool(case.case_target_id == user_id)


ExplanationAs sum add the values it treats True as 1, and False as 0. We make use
of this fact to simplify the generator expression inside the sum call.

unban_count = sum(1 for case in unban_cases if case.case_target_id == user_id)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): Simplify constant sum() call (simplify-constant-sum)

Suggested change
unban_count = sum(1 for case in unban_cases if case.case_target_id == user_id)
unban_count = sum(bool(case.case_target_id == user_id)


ExplanationAs sum add the values it treats True as 1, and False as 0. We make use
of this fact to simplify the generator expression inside the sum call.


return ban_count > unban_count


async def setup(bot: commands.Bot) -> None:
await bot.add_cog(SnippetBan(bot))
102 changes: 102 additions & 0 deletions tux/cogs/moderation/snippetunban.py
Original file line number Diff line number Diff line change
@@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): Simplify constant sum() call (simplify-constant-sum)

Suggested change
ban_count = sum(1 for case in ban_cases if case.case_target_id == user_id)
ban_count = sum(bool(case.case_target_id == user_id)


ExplanationAs sum add the values it treats True as 1, and False as 0. We make use
of this fact to simplify the generator expression inside the sum call.

unban_count = sum(1 for case in unban_cases if case.case_target_id == user_id)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): Simplify constant sum() call (simplify-constant-sum)

Suggested change
unban_count = sum(1 for case in unban_cases if case.case_target_id == user_id)
unban_count = sum(bool(case.case_target_id == user_id)


ExplanationAs sum add the values it treats True as 1, and False as 0. We make use
of this fact to simplify the generator expression inside the sum call.


return ban_count > unban_count
17 changes: 16 additions & 1 deletion tux/cogs/utility/snippets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): Consider separating the user ban check into a utility function or service.

The new code introduces additional complexity due to the added dependencies on CaseController and CaseType, as well as the new is_snippetbanned method. This increases coupling and makes the code harder to maintain. To improve, consider separating the concern of checking if a user is banned into a utility function or service, which would keep the Snippets class focused on its primary responsibility. Additionally, if possible, simplify the logic for determining if a user is banned, perhaps by leveraging a method in CaseController that directly checks this status. This refactoring would enhance readability and maintainability.

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): Simplify constant sum() call (simplify-constant-sum)

Suggested change
ban_count = sum(1 for case in ban_cases if case.case_target_id == user_id)
ban_count = sum(bool(case.case_target_id == user_id)


ExplanationAs sum add the values it treats True as 1, and False as 0. We make use
of this fact to simplify the generator expression inside the sum call.

unban_count = sum(1 for case in unban_cases if case.case_target_id == user_id)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): Simplify constant sum() call (simplify-constant-sum)

Suggested change
unban_count = sum(1 for case in unban_cases if case.case_target_id == user_id)
unban_count = sum(bool(case.case_target_id == user_id)


ExplanationAs sum add the values it treats True as 1, and False as 0. We make use
of this fact to simplify the generator expression inside the sum call.


return ban_count > unban_count

@commands.command(
name="snippets",
Expand Down Expand Up @@ -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):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (performance): Consider optimizing the is_snippetbanned check

The current implementation of is_snippetbanned fetches all cases and then counts them. Consider optimizing this by using a more efficient database query to directly get the count or the latest status.

        snippet_ban_status = await self.get_snippet_ban_status(ctx.guild.id, ctx.author.id)
        if snippet_ban_status:
            await ctx.send("You are banned from using snippets.")
            return

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.")
Expand Down
30 changes: 30 additions & 0 deletions tux/utils/flags.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,33 @@ class WarnFlags(commands.FlagConverter, delimiter=" ", prefix="-"):
aliases=["s", "quiet"],
default=False,
)


class SnippetBanFlags(commands.FlagConverter, delimiter=" ", prefix="-"):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Consider creating a base class for flag converters to reduce duplication

The SnippetBanFlags, SnippetUnbanFlags, and WarnFlags classes share similar structure. Consider creating a base class to reduce code duplication and improve maintainability.

class BaseFlags(commands.FlagConverter, delimiter=" ", prefix="-"):
    pass

class SnippetBanFlags(BaseFlags):

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