Skip to content
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
3 changes: 3 additions & 0 deletions cogs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
CheckSUPlatformAuthorisationCommandCog,
CheckSUPlatformAuthorisationTaskCog,
)
from .colour_selector import MemberColourSelectorCommandCog
from .command_error import CommandErrorCog
from .committee_actions_tracking import (
CommitteeActionsTrackingContextCommandCog,
Expand Down Expand Up @@ -76,6 +77,7 @@
"MakeApplicantSlashCommandCog",
"MakeMemberCommandCog",
"ManualModerationCog",
"MemberColourSelectorCommandCog",
"MemberCountCommandCog",
"PingCommandCog",
"RemindMeCommandCog",
Expand Down Expand Up @@ -117,6 +119,7 @@ def setup(bot: "TeXBot") -> None:
MakeApplicantSlashCommandCog,
MakeMemberCommandCog,
ManualModerationCog,
MemberColourSelectorCommandCog,
MemberCountCommandCog,
PingCommandCog,
RemindMeCommandCog,
Expand Down
156 changes: 156 additions & 0 deletions cogs/colour_selector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
"""Contains cog classes for the colour selector command."""

import logging
from typing import TYPE_CHECKING

import discord

from exceptions import DiscordMemberNotInMainGuildError, GuildDoesNotExistError
from utils import CommandChecks, TeXBotBaseCog

if TYPE_CHECKING:
from collections.abc import Sequence
from collections.abc import Set as AbstractSet
from logging import Logger
from typing import Final

from utils import TeXBotApplicationContext, TeXBotAutocompleteContext


__all__: "Sequence[str]" = ("MemberColourSelectorCommandCog",)


logger: "Final[Logger]" = logging.getLogger("TeX-Bot")


COLOUR_ROLE_NAMES: "Final[AbstractSet[str]]" = { # TODO: Make this a config option in the future # noqa: FIX002
"og-green",
"pink",
"orange",
"purple",
"new-green",
"yellow",
"red",
}


class MemberColourSelectorCommandCog(TeXBotBaseCog):
"""Cog class for the colour selector command."""

@staticmethod
async def autocomplete_colour_roles(
ctx: "TeXBotAutocompleteContext",
) -> "AbstractSet[discord.OptionChoice] | AbstractSet[str]":
"""Autocomplete function for the colour roles option of the colour selector command."""
try:
main_guild: discord.Guild = ctx.bot.main_guild
except GuildDoesNotExistError:
return set()

return {
discord.OptionChoice(
name=role.name,
value=str(role.id),
)
for role in main_guild.roles
if role.name.lower() in COLOUR_ROLE_NAMES
}

@discord.slash_command(
name="member-colour-select",
description="Select a colour role for yourself.",
)
@discord.option(
name="colour-role",
description="The colour role you want to select.",
autocomplete=discord.utils.basic_autocomplete(autocomplete_colour_roles),
input_type=str,
required=True,
parameter_name="role_id_str",
)
@CommandChecks.check_interaction_user_in_main_guild
@CommandChecks.check_interaction_user_has_member_role
async def member_colour_select(
self, ctx: "TeXBotApplicationContext", role_id_str: str
) -> None:
"""Slash command for selecting a colour role for the user."""
# NOTE: Shortcut accessors are placed at the top of the function so that the exceptions they raise are displayed before any further errors may be sent
main_guild: discord.Guild = ctx.bot.main_guild
interaction_member: discord.Member | discord.User | None = ctx.interaction.user

await ctx.defer(ephemeral=True)

async with ctx.typing():
if not interaction_member:
await self.command_send_error(
ctx=ctx,
message="Interaction user was None for member-colour-select command run.",
)
return

try:
role_id_int = int(role_id_str)
except ValueError:
await ctx.respond(
"The role ID you provided is not a valid role ID. "
"Please use the autocomplete.",
ephemeral=True,
)
return

role_to_add: discord.Role | None = discord.utils.get(
main_guild.roles, id=role_id_int
)

if not role_to_add:
await ctx.respond(
"The role you selected does not exist. Please use the autocomplete.",
ephemeral=True,
)
return

if role_to_add.name.lower() not in COLOUR_ROLE_NAMES:
await ctx.respond(
f"{role_to_add.name} is not a valid colour role. "
"Please use the autocomplete."
)
return

if isinstance(interaction_member, discord.User):
try:
fetched_member: discord.Member = await self.bot.get_main_guild_member(
interaction_member
)
except DiscordMemberNotInMainGuildError:
await ctx.respond(
"You are not a member of the main guild. "
"Please join the main guild to use this command.",
ephemeral=True,
)
return

interaction_member = fetched_member

roles_to_remove: list[discord.Role] = [
role
for role in interaction_member.roles
if role.name.lower() in COLOUR_ROLE_NAMES
]

if role_to_add in roles_to_remove:
roles_to_remove.remove(role_to_add)

if roles_to_remove:
await interaction_member.remove_roles(
*roles_to_remove,
reason=f"{interaction_member} used TeX-Bot /member-colour-select.",
)

await interaction_member.add_roles(
role_to_add,
reason=f"{interaction_member} used TeX-Bot /member-colour-select.",
)

await ctx.respond(
f"Successfully gave you the {role_to_add.name} colour role!", ephemeral=True
)
6 changes: 6 additions & 0 deletions cogs/command_error.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ async def on_application_command_error(
"members can run this command."
)

elif CommandChecks.is_interaction_user_has_member_role_failure(error.checks[0]): # type: ignore[arg-type]
message = (
f"Only {await self.bot.get_mention_string(self.bot.member_role)} "
"members can run this command."
)

else:
logging_message = error

Expand Down
25 changes: 25 additions & 0 deletions utils/command_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,26 @@ async def _check(ctx: "TeXBotApplicationContext") -> bool:
)
)(func)

@staticmethod
def check_interaction_user_has_member_role[T: TeXBotBaseCog, **P](
func: "Callable[Concatenate[T, P], Awaitable[None]]",
) -> "Callable[Concatenate[T, P], Awaitable[None]]":
"""
Command check decorator to ensure the interaction user has the "Member" role.

If this check does not pass, the decorated command will not be executed.
Instead, an error message will be sent to the user.
"""

async def _check(ctx: "TeXBotApplicationContext") -> bool:
return await ctx.bot.check_user_has_member_role(ctx.user)

return commands.check_any(
commands.check(
_check # type: ignore[arg-type]
)
)(func)

@classmethod
def is_interaction_user_in_main_guild_failure(cls, check: "CheckFailure") -> bool:
"""Whether the check failed due to the user not being in your Discord guild."""
Expand All @@ -74,3 +94,8 @@ def is_interaction_user_in_main_guild_failure(cls, check: "CheckFailure") -> boo
def is_interaction_user_has_committee_role_failure(cls, check: "CheckFailure") -> bool:
"""Whether the check failed due to the user not having the committee role."""
return bool(check.__name__ == cls.check_interaction_user_has_committee_role.__name__) # type: ignore[attr-defined]

@classmethod
def is_interaction_user_has_member_role_failure(cls, check: "CheckFailure") -> bool:
"""Whether the check failed due to the user not having the Member role."""
return bool(check.__name__ == cls.check_interaction_user_has_member_role.__name__) # type: ignore[attr-defined]
4 changes: 4 additions & 0 deletions utils/tex_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,10 @@ async def check_user_has_committee_role(self, user: discord.Member | discord.Use
"""Util method to validate whether the given user has the "Committee" role."""
return await self.committee_role in (await self.get_main_guild_member(user)).roles

async def check_user_has_member_role(self, user: discord.Member | discord.User) -> bool:
"""Util method to validate whether the given user has the "Member" role."""
return await self.member_role in (await self.get_main_guild_member(user)).roles

def set_main_guild(self, main_guild: discord.Guild) -> None:
"""
Set the main_guild value that TeX-Bot will reference in the future.
Expand Down