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)
¶
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
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
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
# 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
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
@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
@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.
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
*,
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
@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
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
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
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
"""
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
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
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
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
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
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
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
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
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
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
),
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
@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
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
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
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
# 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))