Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add flagging functionality for moderation #556

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
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 @@ -169,4 +169,6 @@ enum CaseType {
UNJAIL
SNIPPETUNBAN
UNTEMPBAN
FLAG
UNFLAG
}
7 changes: 5 additions & 2 deletions tux/cogs/moderation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ async def handle_case_response(
reason: str,
user: discord.Member | discord.User,
dm_sent: bool,
silent_action: bool,
duration: str | None = None,
):
moderator = ctx.author
Expand Down Expand Up @@ -246,7 +247,9 @@ async def handle_case_response(
if dm_sent:
embed.description = "A DM has been sent to the user."
else:
embed.description = "DMs are disabled for this user."
embed.description = "DMs are disabled for this user or the silent flag was used."

if not silent_action:
await self.send_embed(ctx, embed, log_type="mod")

await self.send_embed(ctx, embed, log_type="mod")
await ctx.send(embed=embed, delete_after=30, ephemeral=True)
10 changes: 9 additions & 1 deletion tux/cogs/moderation/ban.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,15 @@ async def ban(
guild_id=ctx.guild.id,
)

await self.handle_case_response(ctx, CaseType.BAN, case.case_number, flags.reason, member, dm_sent)
await self.handle_case_response(
ctx,
CaseType.BAN,
case.case_number,
flags.reason,
member,
dm_sent,
silent_action=False,
)


async def setup(bot: Tux) -> None:
Expand Down
4 changes: 4 additions & 0 deletions tux/cogs/moderation/cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
"jail": 1268115750392954880,
"snippetban": 1277174953950576681,
"snippetunban": 1277174953292337222,
"flag": 1275782294363312172,
"unflag": 1275782294363312172,
}


Expand Down Expand Up @@ -381,6 +383,8 @@ def _get_case_type_emoji(self, case_type: CaseType) -> discord.Emoji | None:
CaseType.UNJAIL: "jail",
CaseType.SNIPPETBAN: "snippetban",
CaseType.SNIPPETUNBAN: "snippetunban",
CaseType.FLAG: "flag",
CaseType.UNFLAG: "unflag",
}
emoji_name = emoji_map.get(case_type)
if emoji_name is not None:
Expand Down
101 changes: 101 additions & 0 deletions tux/cogs/moderation/flag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import discord
from discord.ext import commands

from prisma.enums import CaseType
from tux.bot import Tux
from tux.database.controllers.case import CaseController
from tux.utils import checks
from tux.utils.flags import FlagFlags, generate_usage

from . import ModerationCogBase


class Unflag(ModerationCogBase):
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 consolidating flag-related functionality into a single cog and moving shared methods to a utility module.

The current implementation introduces unnecessary complexity through code duplication and inconsistent naming. To simplify and improve maintainability:

  1. Combine the flag and unflag functionality into a single cog:
class FlagManagement(ModerationCogBase):
    def __init__(self, bot: Tux) -> None:
        super().__init__(bot)
        self.case_controller = CaseController()
        self.flag.usage = generate_usage(self.flag, FlagFlags)
        self.unflag.usage = generate_usage(self.unflag, FlagFlags)

    @commands.hybrid_command(name="flag", aliases=["fl"])
    @commands.guild_only()
    @checks.has_pl(2)
    async def flag(self, ctx: commands.Context[Tux], member: discord.Member, *, flags: FlagFlags) -> None:
        # ... (existing flag implementation)

    @commands.hybrid_command(name="unflag", aliases=["ufl"])
    @commands.guild_only()
    @checks.has_pl(2)
    async def unflag(self, ctx: commands.Context[Tux], member: discord.Member, *, flags: FlagFlags) -> None:
        # ... (unflag implementation)

    # ... (other methods)
  1. Move the is_flagged method to a shared utility module:
# In tux/utils/moderation.py
from tux.database.controllers.case import CaseController
from prisma.enums import CaseType

async def is_flagged(guild_id: int, user_id: int) -> bool:
    case_controller = CaseController()
    flag_cases = await case_controller.get_all_cases_by_type(guild_id, CaseType.FLAG)
    unflag_cases = await case_controller.get_all_cases_by_type(guild_id, CaseType.UNFLAG)

    flag_count = sum(case.case_user_id == user_id for case in flag_cases)
    unflag_count = sum(case.case_user_id == user_id for case in unflag_cases)

    return flag_count > unflag_count

Then, in the FlagManagement cog:

from tux.utils.moderation import is_flagged

class FlagManagement(ModerationCogBase):
    # ...

    async def flag(self, ctx: commands.Context[Tux], member: discord.Member, *, flags: FlagFlags) -> None:
        assert ctx.guild

        if await is_flagged(ctx.guild.id, member.id):
            await ctx.send("User is already flagged.", delete_after=30, ephemeral=True)
            return

        # ... (rest of the method)

    # ...

These changes will reduce code duplication, improve maintainability, and resolve the naming inconsistency while keeping all functionality intact.

def __init__(self, bot: Tux) -> None:
super().__init__(bot)
self.case_controller = CaseController()
self.flag.usage = generate_usage(self.flag, FlagFlags)

@commands.hybrid_command(
name="flag",
aliases=["fl"],
)
@commands.guild_only()
@checks.has_pl(2)
async def flag(
self,
ctx: commands.Context[Tux],
member: discord.Member,
*,
flags: FlagFlags,
) -> None:
"""
Flag a member from the server.

Parameters
----------
ctx : commands.Context[Tux]
The context in which the command is being invoked.
member : discord.Member
The member to flag.
flags : FlagFlags
The flags for the command. (reason: str, silent: bool)
"""

assert ctx.guild

if await self.is_flagged(ctx.guild.id, member.id):
await ctx.send("User is already flagged.", delete_after=30, ephemeral=True)
return

moderator = ctx.author

if not await self.check_conditions(ctx, member, moderator, "flag"):
return

case = await self.db.case.insert_case(
case_user_id=member.id,
case_moderator_id=ctx.author.id,
case_type=CaseType.FLAG,
case_reason=flags.reason,
guild_id=ctx.guild.id,
)

await self.handle_case_response(
ctx,
CaseType.FLAG,
case.case_number,
flags.reason,
member,
dm_sent=False,
silent_action=True,
)

async def is_flagged(self, guild_id: int, user_id: int) -> bool:
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 moving is_flagged method to a common location

The is_flagged method is duplicated in both flag.py and unflag.py. Consider moving this to a common location, such as a base class or utility module, to avoid code repetition and improve maintainability.

from tux.utils.moderation import is_flagged

class Flag(commands.Cog):
    # ... other methods ...

    @staticmethod
    async def is_flagged(guild_id: int, user_id: int) -> bool:
        return await is_flagged(guild_id, user_id)

"""
Check if a user is flagged.

Parameters
----------
guild_id : int
The ID of the guild to check in.
user_id : int
The ID of the user to check.

Returns
-------
bool
True if the user is flagged, False otherwise.
"""

flag_cases = await self.case_controller.get_all_cases_by_type(guild_id, CaseType.FLAG)
unflag_cases = await self.case_controller.get_all_cases_by_type(guild_id, CaseType.UNFLAG)

flag_count = sum(case.case_user_id == user_id for case in flag_cases)
unflag_count = sum(case.case_user_id == user_id for case in unflag_cases)

return flag_count > unflag_count


async def setup(bot: Tux) -> None:
await bot.add_cog(Unflag(bot))
10 changes: 9 additions & 1 deletion tux/cogs/moderation/jail.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,15 @@ async def jail( # noqa: PLR0911
return

dm_sent = await self.send_dm(ctx, flags.silent, member, flags.reason, "jailed")
await self.handle_case_response(ctx, CaseType.JAIL, case.case_number, flags.reason, member, dm_sent)
await self.handle_case_response(
ctx,
CaseType.JAIL,
case.case_number,
flags.reason,
member,
dm_sent,
silent_action=False,
)

def _get_manageable_roles(
self,
Expand Down
10 changes: 9 additions & 1 deletion tux/cogs/moderation/kick.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,15 @@ async def kick(
guild_id=ctx.guild.id,
)

await self.handle_case_response(ctx, CaseType.KICK, case.case_number, flags.reason, member, dm_sent)
await self.handle_case_response(
ctx,
CaseType.KICK,
case.case_number,
flags.reason,
member,
dm_sent,
silent_action=False,
)


async def setup(bot: Tux) -> None:
Expand Down
10 changes: 9 additions & 1 deletion tux/cogs/moderation/snippetban.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,15 @@ async def snippet_ban(
return

dm_sent = await self.send_dm(ctx, flags.silent, member, flags.reason, "snippet banned")
await self.handle_case_response(ctx, CaseType.SNIPPETBAN, case.case_number, flags.reason, member, dm_sent)
await self.handle_case_response(
ctx,
CaseType.SNIPPETBAN,
case.case_number,
flags.reason,
member,
dm_sent,
silent_action=False,
)

async def is_snippetbanned(self, guild_id: int, user_id: int) -> bool:
"""
Expand Down
10 changes: 9 additions & 1 deletion tux/cogs/moderation/snippetunban.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,15 @@ async def snippet_unban(
return

dm_sent = await self.send_dm(ctx, flags.silent, member, flags.reason, "snippet unbanned")
await self.handle_case_response(ctx, CaseType.SNIPPETUNBAN, case.case_number, flags.reason, member, dm_sent)
await self.handle_case_response(
ctx,
CaseType.SNIPPETUNBAN,
case.case_number,
flags.reason,
member,
dm_sent,
silent_action=False,
)

async def is_snippetbanned(self, guild_id: int, user_id: int) -> bool:
"""
Expand Down
10 changes: 9 additions & 1 deletion tux/cogs/moderation/tempban.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,15 @@ async def tempban(
case_tempban_expired=False,
)

await self.handle_case_response(ctx, CaseType.TEMPBAN, case.case_number, flags.reason, member, dm_sent)
await self.handle_case_response(
ctx,
CaseType.TEMPBAN,
case.case_number,
flags.reason,
member,
dm_sent,
silent_action=False,
)

@tasks.loop(hours=1)
async def tempban_check(self) -> None:
Expand Down
1 change: 1 addition & 0 deletions tux/cogs/moderation/timeout.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ async def timeout(
flags.reason,
member,
dm_sent,
False,
flags.duration,
)

Expand Down
10 changes: 9 additions & 1 deletion tux/cogs/moderation/unban.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,15 @@ async def unban(
case_reason=flags.reason,
)

await self.handle_case_response(ctx, CaseType.UNBAN, case.case_number, flags.reason, user, dm_sent=False)
await self.handle_case_response(
ctx,
CaseType.UNBAN,
case.case_number,
flags.reason,
user,
dm_sent=False,
silent_action=False,
)


async def setup(bot: Tux) -> None:
Expand Down
101 changes: 101 additions & 0 deletions tux/cogs/moderation/unflag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import discord
from discord.ext import commands

from prisma.enums import CaseType
from tux.bot import Tux
from tux.database.controllers.case import CaseController
from tux.utils import checks
from tux.utils.flags import UnFlagFlags, generate_usage

from . import ModerationCogBase


class Flag(ModerationCogBase):
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 consolidating flag-related functionality into a single cog and moving shared methods to a utility module.

The current implementation introduces unnecessary complexity through code duplication and separation of closely related functionality. Consider the following improvements:

  1. Combine flag and unflag functionality into a single cog:
class FlagManagement(ModerationCogBase):
    def __init__(self, bot: Tux) -> None:
        super().__init__(bot)
        self.case_controller = CaseController()
        self.flag.usage = generate_usage(self.flag, FlagFlags)
        self.unflag.usage = generate_usage(self.unflag, UnFlagFlags)

    @commands.hybrid_command(name="flag", aliases=["fl"])
    async def flag(self, ctx: commands.Context[Tux], member: discord.Member, *, flags: FlagFlags) -> None:
        # Implement flag logic here

    @commands.hybrid_command(name="unflag", aliases=["ufl"])
    async def unflag(self, ctx: commands.Context[Tux], member: discord.Member, *, flags: UnFlagFlags) -> None:
        # Implement unflag logic here (current implementation)

    # Other methods...
  1. Move the is_flagged method to a common utility module to avoid duplication:
# In tux/utils/moderation.py
async def is_flagged(case_controller: CaseController, guild_id: int, user_id: int) -> bool:
    flag_cases = await case_controller.get_all_cases_by_type(guild_id, CaseType.FLAG)
    unflag_cases = await case_controller.get_all_cases_by_type(guild_id, CaseType.UNFLAG)

    flag_count = sum(case.case_user_id == user_id for case in flag_cases)
    unflag_count = sum(case.case_user_id == user_id for case in unflag_cases)

    return flag_count > unflag_count

# In FlagManagement cog
from tux.utils.moderation import is_flagged

class FlagManagement(ModerationCogBase):
    # ...

    async def unflag(self, ctx: commands.Context[Tux], member: discord.Member, *, flags: UnFlagFlags) -> None:
        # ...
        if not await is_flagged(self.case_controller, ctx.guild.id, member.id):
            await ctx.send("User is not flagged.", delete_after=30, ephemeral=True)
            return
        # ...

    # Remove the is_flagged method from this class

These changes will reduce code duplication, improve maintainability, and provide a more logical structure for flag-related operations while maintaining all existing functionality.

def __init__(self, bot: Tux) -> None:
super().__init__(bot)
self.case_controller = CaseController()
self.unflag.usage = generate_usage(self.unflag, UnFlagFlags)

@commands.hybrid_command(
name="unflag",
aliases=["ufl"],
)
@commands.guild_only()
@checks.has_pl(2)
async def unflag(
self,
ctx: commands.Context[Tux],
member: discord.Member,
*,
flags: UnFlagFlags,
) -> None:
"""
Unflag a member from the server.

Parameters
----------
ctx : commands.Context[Tux]
The context in which the command is being invoked.
member : discord.Member
The member to unflag.
flags : UnFlagFlags
The flags for the command. (reason: str, silent: bool)
"""

assert ctx.guild

if not await self.is_flagged(ctx.guild.id, member.id):
await ctx.send("User is not flagged.", delete_after=30, ephemeral=True)
return

moderator = ctx.author

if not await self.check_conditions(ctx, member, moderator, "unflag"):
return

case = await self.db.case.insert_case(
case_user_id=member.id,
case_moderator_id=ctx.author.id,
case_type=CaseType.UNFLAG,
case_reason=flags.reason,
guild_id=ctx.guild.id,
)

await self.handle_case_response(
ctx,
CaseType.UNFLAG,
case.case_number,
flags.reason,
member,
dm_sent=False,
silent_action=True,
)

async def is_flagged(self, guild_id: int, user_id: int) -> bool:
"""
Check if a user is flagged.

Parameters
----------
guild_id : int
The ID of the guild to check in.
user_id : int
The ID of the user to check.

Returns
-------
bool
True if the user is flagged, False otherwise.
"""

flag_cases = await self.case_controller.get_all_cases_by_type(guild_id, CaseType.FLAG)
unflag_cases = await self.case_controller.get_all_cases_by_type(guild_id, CaseType.UNFLAG)

flag_count = sum(case.case_user_id == user_id for case in flag_cases)
unflag_count = sum(case.case_user_id == user_id for case in unflag_cases)

return flag_count > unflag_count


async def setup(bot: Tux) -> None:
await bot.add_cog(Flag(bot))
10 changes: 9 additions & 1 deletion tux/cogs/moderation/unjail.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,15 @@ async def unjail(

dm_sent = await self.send_dm(ctx, flags.silent, member, flags.reason, "unjailed")

await self.handle_case_response(ctx, CaseType.UNJAIL, unjail_case.case_number, flags.reason, member, dm_sent)
await self.handle_case_response(
ctx,
CaseType.UNJAIL,
unjail_case.case_number,
flags.reason,
member,
dm_sent,
silent_action=False,
)


async def setup(bot: Tux) -> None:
Expand Down
Loading