Skip to content

tux.cogs.moderation.cases

Classes:

Name Description
MockUser

A mock user object for cases where we can't find the real user.

Cases

Classes

MockUser(user_id: int)

A mock user object for cases where we can't find the real user.

Source code in tux/cogs/moderation/cases.py
Python
def __init__(self, user_id: int) -> None:
    self.id = user_id
    self.name = "Unknown User"
    self.discriminator = "0000"
    self.avatar = None

Cases(bot: Tux)

Bases: ModerationCogBase

Methods:

Name Description
get_user_lock

Get or create a lock for operations on a specific user.

clean_user_locks

Remove locks for users that are not currently in use.

execute_user_action_with_lock

Execute an action on a user with a lock to prevent race conditions.

cases

Manage moderation cases in the server.

cases_view

View moderation cases in the server.

execute_mod_action

Execute a moderation action with case creation, DM sending, and additional actions.

cases_modify

Modify a moderation case in the server.

send_error_response

Send a standardized error response.

create_embed

Create an embed for moderation actions.

send_embed

Send an embed to the log channel.

send_dm

Send a DM to the target user.

check_conditions

Check if the conditions for the moderation action are met.

handle_case_response

Handle the response for a case.

is_pollbanned

Check if a user is poll banned.

is_snippetbanned

Check if a user is snippet banned.

is_jailed

Check if a user is jailed using the optimized latest case method.

Source code in tux/cogs/moderation/cases.py
Python
def __init__(self, bot: Tux) -> None:
    super().__init__(bot)
    self.cases.usage = generate_usage(self.cases)
    self.cases_view.usage = generate_usage(self.cases_view, CasesViewFlags)
    self.cases_modify.usage = generate_usage(
        self.cases_modify,
        CaseModifyFlags,
    )

Functions

get_user_lock(user_id: int) -> Lock async

Get or create a lock for operations on a specific user. If the number of stored locks exceeds the cleanup threshold, unused locks are removed.

Parameters:

Name Type Description Default
user_id int

The ID of the user to get a lock for.

required

Returns:

Type Description
Lock

The lock for the user.

Source code in tux/cogs/moderation/cases.py
Python
CASE_ACTION_MAP = {
    CaseType.BAN: "added",
    CaseType.KICK: "added",
    CaseType.TEMPBAN: "added",
    CaseType.TIMEOUT: "added",
    CaseType.WARN: "added",
    CaseType.JAIL: "added",
    CaseType.SNIPPETBAN: "added",
    CaseType.UNBAN: "removed",
    CaseType.UNTIMEOUT: "removed",
    CaseType.UNJAIL: "removed",
    CaseType.SNIPPETUNBAN: "removed",
}


# Define a protocol for user-like objects
class UserLike(Protocol):
    id: int
    name: str
    avatar: Any

    def __str__(self) -> str: ...
clean_user_locks() -> None async

Remove locks for users that are not currently in use. Iterates through the locks and removes any that are not currently locked.

Source code in tux/cogs/moderation/cases.py
Python
# Mock user object for when a user cannot be found
class MockUser:
    """A mock user object for cases where we can't find the real user."""

    def __init__(self, user_id: int) -> None:
        self.id = user_id
        self.name = "Unknown User"
        self.discriminator = "0000"
        self.avatar = None

    def __str__(self) -> str:
        return f"{self.name}#{self.discriminator}"


class Cases(ModerationCogBase):
    def __init__(self, bot: Tux) -> None:
        super().__init__(bot)
execute_user_action_with_lock(user_id: int, action_func: Callable[..., Coroutine[Any, Any, R]], *args: Any, **kwargs: Any) -> R async

Execute an action on a user with a lock to prevent race conditions.

Parameters:

Name Type Description Default
user_id int

The ID of the user to lock.

required
action_func Callable[..., Coroutine[Any, Any, R]]

The coroutine function to execute.

required
*args Any

Arguments to pass to the function.

()
**kwargs Any

Keyword arguments to pass to the function.

{}

Returns:

Type Description
R

The result of the action function.

Source code in tux/cogs/moderation/cases.py
Python
    self.cases_view.usage = generate_usage(self.cases_view, CasesViewFlags)
    self.cases_modify.usage = generate_usage(
        self.cases_modify,
        CaseModifyFlags,
    )

@commands.hybrid_group(
    name="cases",
    aliases=["case", "c"],
)
@commands.guild_only()
@checks.has_pl(2)
async def cases(self, ctx: commands.Context[Tux], case_number: str | None = None) -> None:
    """
    Manage moderation cases in the server.

    Parameters
    ----------
    case_number : str | None
        The case number to view.
    """

    if case_number is not None:
        await ctx.invoke(self.cases_view, number=case_number, flags=CasesViewFlags())

    elif ctx.subcommand_passed is None:
        await ctx.invoke(self.cases_view, number=None, flags=CasesViewFlags())

@cases.command(
    name="view",
cases(ctx: commands.Context[Tux], case_number: str | None = None) -> None async

Manage moderation cases in the server.

Parameters:

Name Type Description Default
case_number str | None

The case number to view.

None
Source code in tux/cogs/moderation/cases.py
Python
@commands.hybrid_group(
    name="cases",
    aliases=["case", "c"],
)
@commands.guild_only()
@checks.has_pl(2)
async def cases(self, ctx: commands.Context[Tux], case_number: str | None = None) -> None:
    """
    Manage moderation cases in the server.

    Parameters
    ----------
    case_number : str | None
        The case number to view.
    """

    if case_number is not None:
        await ctx.invoke(self.cases_view, number=case_number, flags=CasesViewFlags())

    elif ctx.subcommand_passed is None:
        await ctx.invoke(self.cases_view, number=None, flags=CasesViewFlags())
cases_view(ctx: commands.Context[Tux], number: str | None = None, *, flags: CasesViewFlags) -> None async

View moderation cases in the server.

Parameters:

Name Type Description Default
ctx Context[Tux]

The context in which the command is being invoked.

required
number Optional[str]

The case number to view.

None
flags CasesViewFlags

The flags for the command. (type, user, moderator)

required
Source code in tux/cogs/moderation/cases.py
Python
@cases.command(
    name="view",
    aliases=["v", "ls", "list"],
)
@commands.guild_only()
@checks.has_pl(2)
async def cases_view(
    self,
    ctx: commands.Context[Tux],
    number: str | None = None,
    *,
    flags: CasesViewFlags,
) -> None:
    """
    View moderation cases in the server.

    Parameters
    ----------
    ctx : commands.Context[Tux]
        The context in which the command is being invoked.
    number : Optional[str]
        The case number to view.
    flags : CasesViewFlags
        The flags for the command. (type, user, moderator)
    """
    assert ctx.guild

    if number is not None:
        await self._view_single_case(ctx, number)
    else:
        await self._view_cases_with_flags(ctx, flags)
_dummy_action() -> None async

Dummy coroutine for moderation actions that only create a case without performing Discord API actions. Used by commands like warn, pollban, snippetban etc. that only need case creation.

Source code in tux/cogs/moderation/cases.py
Python
)
@commands.guild_only()
@checks.has_pl(2)
async def cases_view(
    self,
    ctx: commands.Context[Tux],
execute_mod_action(ctx: commands.Context[Tux], case_type: CaseType, user: discord.Member | discord.User, reason: str, silent: bool, dm_action: str, actions: Sequence[tuple[Any, type[R]]] = (), duration: str | None = None, expires_at: datetime | None = None) -> None async

Execute a moderation action with case creation, DM sending, and additional actions.

Parameters:

Name Type Description Default
ctx Context[Tux]

The context of the command.

required
case_type CaseType

The type of case to create.

required
user Union[Member, User]

The target user of the moderation action.

required
reason str

The reason for the moderation action.

required
silent bool

Whether to send a DM to the user.

required
dm_action str

The action description for the DM.

required
actions Sequence[tuple[Any, type[R]]]

Additional actions to execute and their expected return types.

()
duration Optional[str]

The duration of the action, if applicable (for display/logging).

None
expires_at Optional[datetime]

The specific expiration time, if applicable.

None
Source code in tux/cogs/moderation/cases.py
Python
    *,
    flags: CasesViewFlags,
) -> None:
    """
    View moderation cases in the server.

    Parameters
    ----------
    ctx : commands.Context[Tux]
        The context in which the command is being invoked.
    number : Optional[str]
        The case number to view.
    flags : CasesViewFlags
        The flags for the command. (type, user, moderator)
    """
    assert ctx.guild

    if number is not None:
        await self._view_single_case(ctx, number)
    else:
        await self._view_cases_with_flags(ctx, flags)

@cases.command(
    name="modify",
    aliases=["m", "edit"],
)
@commands.guild_only()
@checks.has_pl(2)
async def cases_modify(
    self,
    ctx: commands.Context[Tux],
    number: str,
    *,
    flags: CaseModifyFlags,
) -> None:
    """
    Modify a moderation case in the server.

    Parameters
    ----------
    ctx : commands.Context[Tux]
        The context in which the command is being invoked.
    number : str
        The case number to modify.
    flags : CaseModifyFlags
        The flags for the command. (status, reason)
    """
    assert ctx.guild

    try:
        case_number = int(number)
    except ValueError:
        await ctx.send("Case number must be a valid integer.", ephemeral=True)
        return

    case = await self.db.case.get_case_by_number(ctx.guild.id, case_number)
    if not case:
        await ctx.send("Case not found.", ephemeral=True)
        return

    # Validate flags
    if flags.status is None and not flags.reason:
        await ctx.send("You must provide either a new status or reason.", ephemeral=True)
        return

    # Check if status is valid
    if flags.status is not None:
        try:
            flags.status = bool(flags.status)
            if flags.status == case.case_status:
                await ctx.send("Status is already set to that value.", ephemeral=True)
                return

        except ValueError:
            await ctx.send("Status must be a boolean value (true/false).", ephemeral=True)
            return

    # Check if reason is the same
    if flags.reason is not None and flags.reason == case.case_reason:
        await ctx.send("Reason is already set to that value.", ephemeral=True)
        return

    # If we get here, we have valid changes to make
    await self._update_case(ctx, case, flags)

async def _view_single_case(
    self,
    ctx: commands.Context[Tux],
    number: str,
) -> None:
    """
    View a single case by its number.

    Parameters
    ----------
    ctx : commands.Context[Tux]
        The context in which the command is being invoked.
    number : str
        The number of the case to view.
    """
    assert ctx.guild

    try:
        case_number = int(number)
    except ValueError:
        await self.send_error_response(ctx, "Case number must be a valid integer.")
cases_modify(ctx: commands.Context[Tux], number: str, *, flags: CaseModifyFlags) -> None async

Modify a moderation case in the server.

Parameters:

Name Type Description Default
ctx Context[Tux]

The context in which the command is being invoked.

required
number str

The case number to modify.

required
flags CaseModifyFlags

The flags for the command. (status, reason)

required
Source code in tux/cogs/moderation/cases.py
Python
@cases.command(
    name="modify",
    aliases=["m", "edit"],
)
@commands.guild_only()
@checks.has_pl(2)
async def cases_modify(
    self,
    ctx: commands.Context[Tux],
    number: str,
    *,
    flags: CaseModifyFlags,
) -> None:
    """
    Modify a moderation case in the server.

    Parameters
    ----------
    ctx : commands.Context[Tux]
        The context in which the command is being invoked.
    number : str
        The case number to modify.
    flags : CaseModifyFlags
        The flags for the command. (status, reason)
    """
    assert ctx.guild

    try:
        case_number = int(number)
    except ValueError:
        await ctx.send("Case number must be a valid integer.", ephemeral=True)
        return

    case = await self.db.case.get_case_by_number(ctx.guild.id, case_number)
    if not case:
        await ctx.send("Case not found.", ephemeral=True)
        return

    # Validate flags
    if flags.status is None and not flags.reason:
        await ctx.send("You must provide either a new status or reason.", ephemeral=True)
        return

    # Check if status is valid
    if flags.status is not None:
        try:
            flags.status = bool(flags.status)
            if flags.status == case.case_status:
                await ctx.send("Status is already set to that value.", ephemeral=True)
                return

        except ValueError:
            await ctx.send("Status must be a boolean value (true/false).", ephemeral=True)
            return

    # Check if reason is the same
    if flags.reason is not None and flags.reason == case.case_reason:
        await ctx.send("Reason is already set to that value.", ephemeral=True)
        return

    # If we get here, we have valid changes to make
    await self._update_case(ctx, case, flags)
_view_single_case(ctx: commands.Context[Tux], number: str) -> None async

View a single case by its number.

Parameters:

Name Type Description Default
ctx Context[Tux]

The context in which the command is being invoked.

required
number str

The number of the case to view.

required
Source code in tux/cogs/moderation/cases.py
Python
async def _view_single_case(
    self,
    ctx: commands.Context[Tux],
    number: str,
) -> None:
    """
    View a single case by its number.

    Parameters
    ----------
    ctx : commands.Context[Tux]
        The context in which the command is being invoked.
    number : str
        The number of the case to view.
    """
    assert ctx.guild

    try:
        case_number = int(number)
    except ValueError:
        await self.send_error_response(ctx, "Case number must be a valid integer.")
        return

    case = await self.db.case.get_case_by_number(ctx.guild.id, case_number)
    if not case:
        await self.send_error_response(ctx, "Case not found.")
        return

    user = await self._resolve_user(case.case_user_id)
    await self._handle_case_response(ctx, case, "viewed", case.case_reason, user)
_handle_dm_result(user: discord.Member | discord.User, dm_result: Any) -> bool

Handle the result of sending a DM.

Parameters:

Name Type Description Default
user Union[Member, User]

The user the DM was sent to.

required
dm_result Any

The result of the DM sending operation.

required

Returns:

Type Description
bool

Whether the DM was successfully sent.

Source code in tux/cogs/moderation/cases.py
Python
    case = await self.db.case.get_case_by_number(ctx.guild.id, case_number)
    if not case:
        await self.send_error_response(ctx, "Case not found.")
        return

    user = await self._resolve_user(case.case_user_id)
    await self._handle_case_response(ctx, case, "viewed", case.case_reason, user)

async def _view_cases_with_flags(
    self,
    ctx: commands.Context[Tux],
    flags: CasesViewFlags,
) -> None:
    """
    View cases with the provided flags.

    Parameters
    ----------
    ctx : commands.Context[Tux]
        The context in which the command is being invoked.
    flags : CasesViewFlags
_view_cases_with_flags(ctx: commands.Context[Tux], flags: CasesViewFlags) -> None async

View cases with the provided flags.

Parameters:

Name Type Description Default
ctx Context[Tux]

The context in which the command is being invoked.

required
flags CasesViewFlags

The flags for the command. (type, user, moderator)

required
Source code in tux/cogs/moderation/cases.py
Python
async def _view_cases_with_flags(
    self,
    ctx: commands.Context[Tux],
    flags: CasesViewFlags,
) -> None:
    """
    View cases with the provided flags.

    Parameters
    ----------
    ctx : commands.Context[Tux]
        The context in which the command is being invoked.
    flags : CasesViewFlags
        The flags for the command. (type, user, moderator)
    """
    assert ctx.guild

    options: CaseWhereInput = {}

    if flags.type:
        options["case_type"] = flags.type
    if flags.user:
        options["case_user_id"] = flags.user.id
    if flags.moderator:
        options["case_moderator_id"] = flags.moderator.id

    cases = await self.db.case.get_cases_by_options(ctx.guild.id, options)

    if not cases:
        await ctx.send("No cases found.", ephemeral=True)
        return

    total_cases = await self.db.case.get_all_cases(ctx.guild.id)

    await self._handle_case_list_response(ctx, cases, len(total_cases))
send_error_response(ctx: commands.Context[Tux], error_message: str, error_detail: Exception | None = None, ephemeral: bool = True) -> None async

Send a standardized error response.

Parameters:

Name Type Description Default
ctx Context[Tux]

The context of the command.

required
error_message str

The error message to display.

required
error_detail Optional[Exception]

The exception details, if available.

None
ephemeral bool

Whether the message should be ephemeral.

True
Source code in tux/cogs/moderation/cases.py
Python
    """
    assert ctx.guild

    options: CaseWhereInput = {}

    if flags.type:
        options["case_type"] = flags.type
    if flags.user:
        options["case_user_id"] = flags.user.id
    if flags.moderator:
        options["case_moderator_id"] = flags.moderator.id

    cases = await self.db.case.get_cases_by_options(ctx.guild.id, options)

    if not cases:
        await ctx.send("No cases found.", ephemeral=True)
        return

    total_cases = await self.db.case.get_all_cases(ctx.guild.id)

    await self._handle_case_list_response(ctx, cases, len(total_cases))

async def _update_case(
    self,
    ctx: commands.Context[Tux],
    case: Case,
    flags: CaseModifyFlags,
) -> None:
    """
    Update a case with the provided flags.

    Parameters
_update_case(ctx: commands.Context[Tux], case: Case, flags: CaseModifyFlags) -> None async

Update a case with the provided flags.

Parameters:

Name Type Description Default
ctx Context[Tux]

The context in which the command is being invoked.

required
case Case

The case to update.

required
flags CaseModifyFlags

The flags for the command. (status, reason)

required
Source code in tux/cogs/moderation/cases.py
Python
async def _update_case(
    self,
    ctx: commands.Context[Tux],
    case: Case,
    flags: CaseModifyFlags,
) -> None:
    """
    Update a case with the provided flags.

    Parameters
    ----------
    ctx : commands.Context[Tux]
        The context in which the command is being invoked.
    case : Case
        The case to update.
    flags : CaseModifyFlags
        The flags for the command. (status, reason)
    """
    assert ctx.guild
    assert case.case_number is not None

    updated_case = await self.db.case.update_case(
        ctx.guild.id,
        case.case_number,
        case_reason=flags.reason if flags.reason is not None else case.case_reason,
        case_status=flags.status if flags.status is not None else case.case_status,
    )

    if not updated_case:
        await self.send_error_response(ctx, "Failed to update case.")
        return

    user = await self._resolve_user(case.case_user_id)
    await self._handle_case_response(ctx, updated_case, "updated", updated_case.case_reason, user)
create_embed(ctx: commands.Context[Tux], title: str, fields: list[tuple[str, str, bool]], color: int, icon_url: str, timestamp: datetime | None = None, thumbnail_url: str | None = None) -> discord.Embed

Create an embed for moderation actions.

Parameters:

Name Type Description Default
ctx Context[Tux]

The context of the command.

required
title str

The title of the embed.

required
fields list[tuple[str, str, bool]]

The fields to add to the embed.

required
color int

The color of the embed.

required
icon_url str

The icon URL for the embed.

required
timestamp Optional[datetime]

The timestamp for the embed.

None
thumbnail_url Optional[str]

The thumbnail URL for the embed.

None

Returns:

Type Description
Embed

The embed for the moderation action.

Source code in tux/cogs/moderation/cases.py
Python
    ctx : commands.Context[Tux]
        The context in which the command is being invoked.
    case : Case
        The case to update.
    flags : CaseModifyFlags
        The flags for the command. (status, reason)
    """
    assert ctx.guild
    assert case.case_number is not None

    updated_case = await self.db.case.update_case(
        ctx.guild.id,
        case.case_number,
        case_reason=flags.reason if flags.reason is not None else case.case_reason,
        case_status=flags.status if flags.status is not None else case.case_status,
    )

    if not updated_case:
        await self.send_error_response(ctx, "Failed to update case.")
        return

    user = await self._resolve_user(case.case_user_id)
    await self._handle_case_response(ctx, updated_case, "updated", updated_case.case_reason, user)

async def _resolve_user(self, user_id: int) -> discord.User | MockUser:
    """
    Resolve a user ID to a User object or MockUser if not found.

    Parameters
    ----------
    user_id : int
        The ID of the user to resolve.

    Returns
    -------
    Union[discord.User, MockUser]
        The resolved user or a mock user if not found.
    """
    if user := self.bot.get_user(user_id):
        return user

    # If not in cache, try fetching
    try:
        return await self.bot.fetch_user(user_id)

    except discord.NotFound:
        logger.warning(f"Could not find user with ID {user_id}")
        return MockUser(user_id)
    except Exception as e:
        logger.exception(f"Error resolving user with ID {user_id}: {e}")
        return MockUser(user_id)

async def _resolve_moderator(self, moderator_id: int) -> discord.User | MockUser:
    """
    Resolve a moderator ID to a User object or MockUser if not found.
    We use a separate function to potentially add admin-specific
    resolution in the future.
_resolve_user(user_id: int) -> discord.User | MockUser async

Resolve a user ID to a User object or MockUser if not found.

Parameters:

Name Type Description Default
user_id int

The ID of the user to resolve.

required

Returns:

Type Description
Union[User, MockUser]

The resolved user or a mock user if not found.

Source code in tux/cogs/moderation/cases.py
Python
async def _resolve_user(self, user_id: int) -> discord.User | MockUser:
    """
    Resolve a user ID to a User object or MockUser if not found.

    Parameters
    ----------
    user_id : int
        The ID of the user to resolve.

    Returns
    -------
    Union[discord.User, MockUser]
        The resolved user or a mock user if not found.
    """
    if user := self.bot.get_user(user_id):
        return user

    # If not in cache, try fetching
    try:
        return await self.bot.fetch_user(user_id)

    except discord.NotFound:
        logger.warning(f"Could not find user with ID {user_id}")
        return MockUser(user_id)
    except Exception as e:
        logger.exception(f"Error resolving user with ID {user_id}: {e}")
        return MockUser(user_id)
_resolve_moderator(moderator_id: int) -> discord.User | MockUser async

Resolve a moderator ID to a User object or MockUser if not found. We use a separate function to potentially add admin-specific resolution in the future.

Parameters:

Name Type Description Default
moderator_id int

The ID of the moderator to resolve.

required

Returns:

Type Description
Union[User, MockUser]

The resolved moderator or a mock user if not found.

Source code in tux/cogs/moderation/cases.py
Python
async def _resolve_moderator(self, moderator_id: int) -> discord.User | MockUser:
    """
    Resolve a moderator ID to a User object or MockUser if not found.
    We use a separate function to potentially add admin-specific
    resolution in the future.

    Parameters
    ----------
    moderator_id : int
        The ID of the moderator to resolve.

    Returns
    -------
    Union[discord.User, MockUser]
        The resolved moderator or a mock user if not found.
    """
    return await self._resolve_user(moderator_id)
send_embed(ctx: commands.Context[Tux], embed: discord.Embed, log_type: str) -> None async

Send an embed to the log channel.

Parameters:

Name Type Description Default
ctx Context[Tux]

The context of the command.

required
embed Embed

The embed to send.

required
log_type str

The type of log to send the embed to.

required
Source code in tux/cogs/moderation/cases.py
Python
    Parameters
    ----------
    moderator_id : int
        The ID of the moderator to resolve.

    Returns
    -------
    Union[discord.User, MockUser]
        The resolved moderator or a mock user if not found.
    """
    return await self._resolve_user(moderator_id)

async def _handle_case_response(
    self,
    ctx: commands.Context[Tux],
    case: Case | None,
    action: str,
    reason: str,
    user: discord.User | MockUser,
) -> None:
    """
    Handle the response for a case.

    Parameters
    ----------
    ctx : commands.Context[Tux]
        The context in which the command is being invoked.
    case : Optional[Case]
_handle_case_response(ctx: commands.Context[Tux], case: Case | None, action: str, reason: str, user: discord.User | MockUser) -> None async

Handle the response for a case.

Parameters:

Name Type Description Default
ctx Context[Tux]

The context in which the command is being invoked.

required
case Optional[Case]

The case to handle the response for.

required
action str

The action being performed on the case.

required
reason str

The reason for the case.

required
user Union[User, MockUser]

The target of the case.

required
Source code in tux/cogs/moderation/cases.py
Python
async def _handle_case_response(
    self,
    ctx: commands.Context[Tux],
    case: Case | None,
    action: str,
    reason: str,
    user: discord.User | MockUser,
) -> None:
    """
    Handle the response for a case.

    Parameters
    ----------
    ctx : commands.Context[Tux]
        The context in which the command is being invoked.
    case : Optional[Case]
        The case to handle the response for.
    action : str
        The action being performed on the case.
    reason : str
        The reason for the case.
    user : Union[discord.User, MockUser]
        The target of the case.
    """
    if not case:
        embed = EmbedCreator.create_embed(
            embed_type=EmbedType.ERROR,
            title=f"Case {action}",
            description="Failed to find case.",
        )

        await ctx.send(embed=embed, ephemeral=True)
        return

    moderator = await self._resolve_moderator(case.case_moderator_id)
    fields = self._create_case_fields(moderator, user, reason)

    embed = 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"] if case.case_status else CONST.EMBED_ICONS["INACTIVE_CASE"],
    )

    # Safe avatar access that works with MockUser
    if hasattr(user, "avatar") and user.avatar:
        embed.set_thumbnail(url=user.avatar.url)

    await ctx.send(embed=embed, ephemeral=True)
send_dm(ctx: commands.Context[Tux], silent: bool, user: discord.Member | discord.User, reason: str, action: str) -> bool async

Send a DM to the target user.

Parameters:

Name Type Description Default
ctx Context[Tux]

The context of the command.

required
silent bool

Whether the command is silent.

required
user Union[Member, User]

The target of the moderation action.

required
reason str

The reason for the moderation action.

required
action str

The action being performed.

required

Returns:

Type Description
bool

Whether the DM was successfully sent.

Source code in tux/cogs/moderation/cases.py
Python
    action : str
        The action being performed on the case.
    reason : str
        The reason for the case.
    user : Union[discord.User, MockUser]
        The target of the case.
    """
    if not case:
        embed = EmbedCreator.create_embed(
            embed_type=EmbedType.ERROR,
            title=f"Case {action}",
            description="Failed to find case.",
        )

        await ctx.send(embed=embed, ephemeral=True)
        return

    moderator = await self._resolve_moderator(case.case_moderator_id)
    fields = self._create_case_fields(moderator, user, reason)

    embed = 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"] if case.case_status else CONST.EMBED_ICONS["INACTIVE_CASE"],
    )

    # Safe avatar access that works with MockUser
    if hasattr(user, "avatar") and user.avatar:
        embed.set_thumbnail(url=user.avatar.url)

    await ctx.send(embed=embed, ephemeral=True)

async def _handle_case_list_response(
    self,
    ctx: commands.Context[Tux],
    cases: list[Case],
    total_cases: int,
) -> None:
_handle_case_list_response(ctx: commands.Context[Tux], cases: list[Case], total_cases: int) -> None async

Handle the response for a case list.

Parameters:

Name Type Description Default
ctx Context[Tux]

The context in which the command is being invoked.

required
cases list[Case]

The cases to handle the response for.

required
total_cases int

The total number of cases.

required
Source code in tux/cogs/moderation/cases.py
Python
async def _handle_case_list_response(
    self,
    ctx: commands.Context[Tux],
    cases: list[Case],
    total_cases: int,
) -> None:
    """
    Handle the response for a case list.

    Parameters
    ----------
    ctx : commands.Context[Tux]
        The context in which the command is being invoked.
    cases : list[Case]
        The cases to handle the response for.
    total_cases : int
        The total number of cases.
    """
    if not cases:
        embed = EmbedCreator.create_embed(
            embed_type=EmbedType.ERROR,
            title="Cases",
            description="No cases found.",
        )
        await ctx.send(embed=embed, ephemeral=True)
        return

    menu = ViewMenu(
        ctx,
        menu_type=ViewMenu.TypeEmbed,
        all_can_click=True,
        delete_on_timeout=True,
    )

    # Paginate cases
    cases_per_page = 10

    for i in range(0, len(cases), cases_per_page):
        embed = self._create_case_list_embed(
            ctx,
            cases[i : i + cases_per_page],
            total_cases,
        )

        menu.add_page(embed)

    menu_buttons = [
        ViewButton(
            style=discord.ButtonStyle.secondary,
            custom_id=ViewButton.ID_GO_TO_FIRST_PAGE,
            emoji="⏮️",
        ),
        ViewButton(
            style=discord.ButtonStyle.secondary,
            custom_id=ViewButton.ID_PREVIOUS_PAGE,
            emoji="⏪",
        ),
        ViewButton(
            style=discord.ButtonStyle.secondary,
            custom_id=ViewButton.ID_NEXT_PAGE,
            emoji="⏩",
        ),
        ViewButton(
            style=discord.ButtonStyle.secondary,
            custom_id=ViewButton.ID_GO_TO_LAST_PAGE,
            emoji="⏭️",
        ),
    ]

    menu.add_buttons(menu_buttons)

    await menu.start()
check_conditions(ctx: commands.Context[Tux], user: discord.Member | discord.User, moderator: discord.Member | discord.User, action: str) -> bool async

Check if the conditions for the moderation action are met.

Parameters:

Name Type Description Default
ctx Context[Tux]

The context of the command.

required
user Union[Member, User]

The target of the moderation action.

required
moderator Union[Member, User]

The moderator of the moderation action.

required
action str

The action being performed.

required

Returns:

Type Description
bool

Whether the conditions are met.

Source code in tux/cogs/moderation/cases.py
Python
Handle the response for a case list.

Parameters
----------
ctx : commands.Context[Tux]
    The context in which the command is being invoked.
cases : list[Case]
    The cases to handle the response for.
total_cases : int
    The total number of cases.
"""
if not cases:
    embed = EmbedCreator.create_embed(
        embed_type=EmbedType.ERROR,
        title="Cases",
        description="No cases found.",
    )
    await ctx.send(embed=embed, ephemeral=True)
    return

menu = ViewMenu(
    ctx,
    menu_type=ViewMenu.TypeEmbed,
    all_can_click=True,
    delete_on_timeout=True,
)

# Paginate cases
cases_per_page = 10

for i in range(0, len(cases), cases_per_page):
    embed = self._create_case_list_embed(
        ctx,
        cases[i : i + cases_per_page],
        total_cases,
    )

    menu.add_page(embed)

menu_buttons = [
    ViewButton(
        style=discord.ButtonStyle.secondary,
        custom_id=ViewButton.ID_GO_TO_FIRST_PAGE,
        emoji="⏮️",
    ),
    ViewButton(
        style=discord.ButtonStyle.secondary,
        custom_id=ViewButton.ID_PREVIOUS_PAGE,
        emoji="⏪",
    ),
    ViewButton(
        style=discord.ButtonStyle.secondary,
        custom_id=ViewButton.ID_NEXT_PAGE,
handle_case_response(ctx: commands.Context[Tux], case_type: CaseType, case_number: int | None, reason: str, user: discord.Member | discord.User, dm_sent: bool, duration: str | None = None) -> None async

Handle the response for a case.

Parameters:

Name Type Description Default
ctx Context[Tux]

The context of the command.

required
case_type CaseType

The type of case.

required
case_number Optional[int]

The case number.

required
reason str

The reason for the case.

required
user Union[Member, User]

The target of the case.

required
dm_sent bool

Whether the DM was sent.

required
duration Optional[str]

The duration of the case.

None
Source code in tux/cogs/moderation/cases.py
Python
        ),
        ViewButton(
            style=discord.ButtonStyle.secondary,
            custom_id=ViewButton.ID_GO_TO_LAST_PAGE,
            emoji="⏭️",
        ),
    ]

    menu.add_buttons(menu_buttons)

    await menu.start()

@staticmethod
def _create_case_fields(
    moderator: discord.User | MockUser,
    user: discord.User | MockUser,
    reason: str,
) -> list[tuple[str, str, bool]]:
    """
    Create the fields for a case.

    Parameters
    ----------
    moderator : Union[discord.User, MockUser]
        The moderator of the case.
    user : Union[discord.User, MockUser]
        The user of the case.
    reason : str
        The reason for the case.

    Returns
    -------
    list[tuple[str, str, bool]]
        The fields for the case.
    """
    return [
        (
            "Moderator",
            f"**{moderator}**\n`{moderator.id if hasattr(moderator, 'id') else 'Unknown'}`",
            True,
        ),
        ("User", f"**{user}**\n`{user.id}`", True),
        ("Reason", f"> {reason}", False),
    ]

def _create_case_list_embed(
    self,
    ctx: commands.Context[Tux],
    cases: list[Case],
    total_cases: int,
) -> discord.Embed:
    """
_create_case_fields(moderator: discord.User | MockUser, user: discord.User | MockUser, reason: str) -> list[tuple[str, str, bool]] staticmethod

Create the fields for a case.

Parameters:

Name Type Description Default
moderator Union[User, MockUser]

The moderator of the case.

required
user Union[User, MockUser]

The user of the case.

required
reason str

The reason for the case.

required

Returns:

Type Description
list[tuple[str, str, bool]]

The fields for the case.

Source code in tux/cogs/moderation/cases.py
Python
@staticmethod
def _create_case_fields(
    moderator: discord.User | MockUser,
    user: discord.User | MockUser,
    reason: str,
) -> list[tuple[str, str, bool]]:
    """
    Create the fields for a case.

    Parameters
    ----------
    moderator : Union[discord.User, MockUser]
        The moderator of the case.
    user : Union[discord.User, MockUser]
        The user of the case.
    reason : str
        The reason for the case.

    Returns
    -------
    list[tuple[str, str, bool]]
        The fields for the case.
    """
    return [
        (
            "Moderator",
            f"**{moderator}**\n`{moderator.id if hasattr(moderator, 'id') else 'Unknown'}`",
            True,
        ),
        ("User", f"**{user}**\n`{user.id}`", True),
        ("Reason", f"> {reason}", False),
    ]
_create_case_list_embed(ctx: commands.Context[Tux], cases: list[Case], total_cases: int) -> discord.Embed

Create the embed for a case list.

Parameters:

Name Type Description Default
ctx Context[Tux]

The context in which the command is being invoked.

required
cases list[Case]

The cases to create the embed for.

required
total_cases int

The total number of cases.

required

Returns:

Type Description
Embed

The embed for the case list.

Source code in tux/cogs/moderation/cases.py
Python
def _create_case_list_embed(
    self,
    ctx: commands.Context[Tux],
    cases: list[Case],
    total_cases: int,
) -> discord.Embed:
    """
    Create the embed for a case list.

    Parameters
    ----------
    ctx : commands.Context[Tux]
        The context in which the command is being invoked.
    cases : list[Case]
        The cases to create the embed for.
    total_cases : int
        The total number of cases.

    Returns
    -------
    discord.Embed
        The embed for the case list.
    """
    assert ctx.guild
    assert ctx.guild.icon

    footer_text, footer_icon_url = EmbedCreator.get_footer(
        bot=self.bot,
        user_name=ctx.author.name,
        user_display_avatar=ctx.author.display_avatar.url,
    )

    embed = EmbedCreator.create_embed(
        title=f"Total Cases ({total_cases})",
        description="",
        embed_type=EmbedType.CASE,
        custom_author_text=ctx.guild.name,
        custom_author_icon_url=ctx.guild.icon.url,
        custom_footer_text=footer_text,
        custom_footer_icon_url=footer_icon_url,
    )

    # Header row for the list
    embed.description = "**Case**\u2003\u2003\u2002**Type**\u2003\u2002**Date**\n"

    # Add each case to the embed
    for case in cases:
        # Get emojis for this case
        status_emoji = self.bot.emoji_manager.get(
            "active_case" if case.case_status else "inactive_case",
        )
        type_emoji = self.bot.emoji_manager.get(
            CASE_TYPE_EMOJI_MAP.get(case.case_type, "tux_error"),
        )
        action_emoji = self.bot.emoji_manager.get(
            CASE_ACTION_MAP.get(case.case_type, "tux_error"),
        )

        # Format the case number
        case_number = f"{case.case_number:04}" if case.case_number is not None else "0000"

        # Format type and action
        case_type_and_action = f"{action_emoji}{type_emoji}"

        # Format date
        case_date = (
            discord.utils.format_dt(
                case.case_created_at,
                "R",
            )
            if case.case_created_at
            else f"{self.bot.emoji_manager.get('tux_error')}"
        )

        # Add the line to the embed
        embed.description += f"{status_emoji}`{case_number}`\u2003 {case_type_and_action} \u2003__{case_date}__\n"

    return embed
_format_case_title(case_type: CaseType, case_number: int | None, duration: str | None) -> str

Format a case title.

Parameters:

Name Type Description Default
case_type CaseType

The type of case.

required
case_number Optional[int]

The case number.

required
duration Optional[str]

The duration of the case.

required

Returns:

Type Description
str

The formatted case title.

Source code in tux/cogs/moderation/cases.py
Python
Parameters
----------
ctx : commands.Context[Tux]
    The context in which the command is being invoked.
cases : list[Case]
    The cases to create the embed for.
total_cases : int
    The total number of cases.

Returns
-------
discord.Embed
    The embed for the case list.
"""
assert ctx.guild
assert ctx.guild.icon

footer_text, footer_icon_url = EmbedCreator.get_footer(
    bot=self.bot,
    user_name=ctx.author.name,
    user_display_avatar=ctx.author.display_avatar.url,
is_pollbanned(guild_id: int, user_id: int) -> bool async

Check if a user is poll banned.

Parameters:

Name Type Description Default
guild_id int

The ID of the guild to check in.

required
user_id int

The ID of the user to check.

required

Returns:

Type Description
bool

True if the user is poll banned, False otherwise.

Source code in tux/cogs/moderation/cases.py
Python
embed = EmbedCreator.create_embed(
    title=f"Total Cases ({total_cases})",
    description="",
    embed_type=EmbedType.CASE,
    custom_author_text=ctx.guild.name,
    custom_author_icon_url=ctx.guild.icon.url,
    custom_footer_text=footer_text,
    custom_footer_icon_url=footer_icon_url,
)

# Header row for the list
embed.description = "**Case**\u2003\u2003\u2002**Type**\u2003\u2002**Date**\n"

# Add each case to the embed
for case in cases:
    # Get emojis for this case
    status_emoji = self.bot.emoji_manager.get(
        "active_case" if case.case_status else "inactive_case",
    )
    type_emoji = self.bot.emoji_manager.get(
        CASE_TYPE_EMOJI_MAP.get(case.case_type, "tux_error"),
    )
    action_emoji = self.bot.emoji_manager.get(
        CASE_ACTION_MAP.get(case.case_type, "tux_error"),
is_snippetbanned(guild_id: int, user_id: int) -> bool async

Check if a user is snippet banned.

Parameters:

Name Type Description Default
guild_id int

The ID of the guild to check in.

required
user_id int

The ID of the user to check.

required

Returns:

Type Description
bool

True if the user is snippet banned, False otherwise.

Source code in tux/cogs/moderation/cases.py
Python
            # Format the case number
            case_number = f"{case.case_number:04}" if case.case_number is not None else "0000"

            # Format type and action
            case_type_and_action = f"{action_emoji}{type_emoji}"

            # Format date
            case_date = (
                discord.utils.format_dt(
                    case.case_created_at,
                    "R",
                )
                if case.case_created_at
                else f"{self.bot.emoji_manager.get('tux_error')}"
            )

            # Add the line to the embed
            embed.description += f"{status_emoji}`{case_number}`\u2003 {case_type_and_action} \u2003__{case_date}__\n"

        return embed


async def setup(bot: Tux) -> None:
    await bot.add_cog(Cases(bot))
is_jailed(guild_id: int, user_id: int) -> bool async

Check if a user is jailed using the optimized latest case method.

Parameters:

Name Type Description Default
guild_id int

The ID of the guild to check in.

required
user_id int

The ID of the user to check.

required

Returns:

Type Description
bool

True if the user is jailed, False otherwise.

Functions