From 000ea0d6ccf0fe32b323c5d6223489447e230432 Mon Sep 17 00:00:00 2001 From: MattyTheHacker <18513864+MattyTheHacker@users.noreply.github.com> Date: Sat, 3 May 2025 19:06:19 +0100 Subject: [PATCH 01/41] Add auto auth session setting setup --- config.py | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/config.py b/config.py index a401be2e6..27d5117fd 100644 --- a/config.py +++ b/config.py @@ -426,6 +426,53 @@ def _setup_members_list_auth_session_cookie(cls) -> None: raw_members_list_auth_session_cookie ) + @classmethod + def _setup_auto_auth_session_cookie_checking(cls) -> None: + raw_auto_auth_session_cookie_checking: str | bool = str( + os.getenv("AUTO_AUTH_SESSION_COOKIE_CHECKING", "False"), + ) + + if raw_auto_auth_session_cookie_checking in FALSE_VALUES: + cls._settings["AUTO_AUTH_SESSION_COOKIE_CHECKING"] = False + return + + if raw_auto_auth_session_cookie_checking in TRUE_VALUES: + raw_auto_auth_session_cookie_checking = "24h" + + raw_auto_auth_session_cookie_checking_delay: re.Match[str] | None = re.fullmatch( + r"\A(?:(?P(?:\d*\.)?\d+)s)?(?:(?P(?:\d*\.)?\d+)m)?(?:(?P(?:\d*\.)?\d+)h)?(?:(?P(?:\d*\.)?\d+)d)?(?:(?P(?:\d*\.)?\d+)w)?\Z", + str(raw_auto_auth_session_cookie_checking), + ) + + if not raw_auto_auth_session_cookie_checking_delay: + INVALID_SEND_INTRODUCTION_REMINDERS_DELAY_MESSAGE: Final[str] = ( + "SEND_INTRODUCTION_REMINDERS_DELAY must contain the delay " + "in any combination of seconds, minutes, hours, days or weeks." + ) + raise ImproperlyConfiguredError( + INVALID_SEND_INTRODUCTION_REMINDERS_DELAY_MESSAGE, + ) + + raw_timedelta_auto_auth_session_cookie_checking_delay: timedelta = timedelta( + **{ + key: float(value) + for key, value in ( + raw_auto_auth_session_cookie_checking_delay.groupdict().items() + ) + if value + }, + ) + + if raw_timedelta_auto_auth_session_cookie_checking_delay < timedelta(days=1): + logger.warning( + "Automatic checking of the MSL session cookie is below the " + "recommended minimum (24h) which could cause performance issues." + ) + + cls._settings["AUTO_AUTH_SESSION_COOKIE_CHECKING"] = ( + raw_timedelta_auto_auth_session_cookie_checking_delay + ) + @classmethod def _setup_send_introduction_reminders(cls) -> None: raw_send_introduction_reminders: str | bool = str( From 734a4ef20ff47fe8aadaa52792111ceb0b978107 Mon Sep 17 00:00:00 2001 From: MattyTheHacker <18513864+MattyTheHacker@users.noreply.github.com> Date: Sat, 3 May 2025 20:18:01 +0100 Subject: [PATCH 02/41] refactor and implement task --- cogs/get_token_authorisation.py | 154 ++++++++++++++++++++++++-------- 1 file changed, 116 insertions(+), 38 deletions(-) diff --git a/cogs/get_token_authorisation.py b/cogs/get_token_authorisation.py index 20e42785e..67e4d3799 100644 --- a/cogs/get_token_authorisation.py +++ b/cogs/get_token_authorisation.py @@ -1,22 +1,26 @@ """Contains cog classes for token authorisation check interactions.""" import logging -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, override import aiohttp import bs4 import discord from bs4 import BeautifulSoup +from discord.ext import tasks from config import settings from utils import CommandChecks, TeXBotBaseCog +from utils.error_capture_decorators import ( + capture_guild_does_not_exist_error, +) if TYPE_CHECKING: from collections.abc import Iterable, Mapping, Sequence from logging import Logger from typing import Final - from utils import TeXBotApplicationContext + from utils import TeXBot, TeXBotApplicationContext __all__: "Sequence[str]" = ("GetTokenAuthorisationCommandCog",) @@ -35,22 +39,39 @@ REQUEST_URL: "Final[str]" = "https://guildofstudents.com/profile" -class GetTokenAuthorisationCommandCog(TeXBotBaseCog): - """Cog class that defines the "/get_token_authorisation" command.""" +class TokenAuthorisationBaseCog(TeXBotBaseCog): + """Cog class that defines the base for token authorisation functions.""" - @discord.slash_command( # type: ignore[no-untyped-call, misc] - name="get-token-authorisation", - description="Checks the authorisations held by the token.", - ) - @CommandChecks.check_interaction_user_has_committee_role - @CommandChecks.check_interaction_user_in_main_guild - async def get_token_authorisation(self, ctx: "TeXBotApplicationContext") -> None: # type: ignore[misc] + async def is_token_valid(self) -> bool: """ - Definition of the "get_token_authorisation" command. + Definition of method to check if the authorisation token is valid. - The "get_token_authorisation" command will retrieve the profile for the token user. - The profile page will contain the user's name and a list of the MSL organisations - the user has administrative access to. + This is done by requesting the user profile page and + checking if the page title contains "Login". + """ + http_session: aiohttp.ClientSession = aiohttp.ClientSession( + headers=REQUEST_HEADERS, + cookies=REQUEST_COOKIES, + ) + + async with http_session, http_session.get(REQUEST_URL) as http_response: + response_html: str = await http_response.text() + + response_object: bs4.BeautifulSoup = BeautifulSoup( + response_html, + "html.parser", + ) + + page_title: bs4.Tag | bs4.NavigableString | None = response_object.find("title") + + return "Login" in str(page_title) + + async def get_token_groups(self, iterable: bool) -> str | Iterable[str]: # noqa: FBT001 + """ + Definition of method to get the groups the token has access to. + + This is done by requesting the user profile page and + scraping the HTML for the list of groups. """ http_session: aiohttp.ClientSession = aiohttp.ClientSession( headers=REQUEST_HEADERS, @@ -68,19 +89,15 @@ async def get_token_authorisation(self, ctx: "TeXBotApplicationContext") -> None page_title: bs4.Tag | bs4.NavigableString | None = response_object.find("title") if not page_title: - await self.command_send_error( - ctx=ctx, - message="Profile page returned no content when checking token authorisation!", + PROFILE_PAGE_INVALID: Final[str] = ( + "Profile page returned no content when checking token authorisation." ) - return + logger.warning(PROFILE_PAGE_INVALID) + return PROFILE_PAGE_INVALID if "Login" in str(page_title): - BAD_TOKEN_MESSAGE: Final[str] = ( - "Unable to fetch profile page because the token was not valid." # noqa: S105 - ) - logger.warning(BAD_TOKEN_MESSAGE) - await ctx.respond(content=BAD_TOKEN_MESSAGE) - return + logger.warning("Unable to fetch profile page because the token was not valid.") + return [] profile_section_html: bs4.Tag | bs4.NavigableString | None = response_object.find( "div", @@ -93,11 +110,7 @@ async def get_token_authorisation(self, ctx: "TeXBotApplicationContext") -> None "when scraping the website's HTML!", ) logger.debug("Retrieved HTML: %s", response_html) - await ctx.respond( - "Couldn't find the profile of the user! " - "This should never happen, please check the logs!", - ) - return + return "Something went wrong when fetching the profile page!" user_name: bs4.Tag | bs4.NavigableString | int | None = profile_section_html.find("h1") @@ -105,9 +118,9 @@ async def get_token_authorisation(self, ctx: "TeXBotApplicationContext") -> None NO_PROFILE_DEBUG_MESSAGE: Final[str] = ( "Found user profile but couldn't find their name!" ) - logger.debug(NO_PROFILE_DEBUG_MESSAGE) - await ctx.respond(NO_PROFILE_DEBUG_MESSAGE) - return + logger.warning(NO_PROFILE_DEBUG_MESSAGE) + logger.debug("Retrieved HTML: %s", response_html) + return "Something went wrong when fetching the profile page!" parsed_html: bs4.Tag | bs4.NavigableString | None = response_object.find( "ul", @@ -120,8 +133,7 @@ async def get_token_authorisation(self, ctx: "TeXBotApplicationContext") -> None "Please check you have used the correct token!" ) logger.warning(NO_ADMIN_TABLE_MESSAGE) - await ctx.respond(content=NO_ADMIN_TABLE_MESSAGE) - return + return NO_ADMIN_TABLE_MESSAGE organisations: Iterable[str] = [ list_item.get_text(strip=True) for list_item in parsed_html.find_all("li") @@ -133,10 +145,76 @@ async def get_token_authorisation(self, ctx: "TeXBotApplicationContext") -> None user_name.text, ) - await ctx.respond( + constructed_organisations: str = ( f"Admin token has access to the following MSL Organisations as " f"{user_name.text}:\n{ ', \n'.join(organisation for organisation in organisations) - }", - ephemeral=True, + }" ) + + return organisations if iterable else constructed_organisations + + +class GetTokenAuthorisationCommandCog(TokenAuthorisationBaseCog): + """Cog class that defines the "/get_token_authorisation" command.""" + + @discord.slash_command( # type: ignore[no-untyped-call, misc] + name="get-token-authorisation", + description="Checks the authorisations held by the token.", + ) + @CommandChecks.check_interaction_user_has_committee_role + @CommandChecks.check_interaction_user_in_main_guild + async def get_token_authorisation(self, ctx: "TeXBotApplicationContext") -> None: # type: ignore[misc] + """ + Definition of the "get_token_authorisation" command. + + The "get_token_authorisation" command will retrieve the profile for the token user. + The profile page will contain the user's name and a list of the MSL organisations + the user has administrative access to. + """ + await ctx.defer(ephemeral=True) + async with ctx.typing(): + await ctx.followup.send( + content=str(await self.get_token_groups(iterable=False)), + ephemeral=True, + ) + +class TokenAuthorisationCheckTaskCog(TokenAuthorisationBaseCog): + """Cog class that defines the background task for token authorisation checks.""" + + @override + def __init__(self, bot: "TeXBot") -> None: + """Start all task managers when this cog is initialised.""" + if settings["TOKEN_AUTHORISATION_CHECK_TASK"]: + _ = self.token_authorisation_check_task.start() + + super().__init__(bot) + + @override + def cog_unload(self) -> None: + """ + Unload-hook that ends all running tasks whenever the tasks cog is unloaded. + + This may be run dynamically or when the bot closes. + """ + self.token_authorisation_check_task.cancel() + + @tasks.loop(**settings["TOKEN_AUTHORISATION_CHECK_TASK_INTERVAL"]) + @capture_guild_does_not_exist_error + async def token_authorisation_check_task(self) -> None: + """ + Definition of the background task that checks the token authorisation. + + The task will check if the token is valid and if it is, it will retrieve the + groups the token has access to. + """ + logger.debug("Running token authorisation check task...") + + token_valid: bool = await self.is_token_valid() + + if not token_valid: + logger.warning("Token is not valid!") + + await self.bot.fetch_log_channel().send( + "Auth token has expired!" + ) From 2a70103d2c86ca63166963c2566e2a309345d389 Mon Sep 17 00:00:00 2001 From: MattyTheHacker <18513864+MattyTheHacker@users.noreply.github.com> Date: Sat, 3 May 2025 21:47:10 +0100 Subject: [PATCH 03/41] fix quotes --- cogs/get_token_authorisation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/get_token_authorisation.py b/cogs/get_token_authorisation.py index 67e4d3799..c83bdad33 100644 --- a/cogs/get_token_authorisation.py +++ b/cogs/get_token_authorisation.py @@ -66,7 +66,7 @@ async def is_token_valid(self) -> bool: return "Login" in str(page_title) - async def get_token_groups(self, iterable: bool) -> str | Iterable[str]: # noqa: FBT001 + async def get_token_groups(self, iterable: bool) -> str | "Iterable"[str]: # noqa: FBT001 """ Definition of method to get the groups the token has access to. From 79ed652856e86a0da12c19f2ffea794b5e06f071 Mon Sep 17 00:00:00 2001 From: MattyTheHacker <18513864+MattyTheHacker@users.noreply.github.com> Date: Sat, 3 May 2025 21:48:41 +0100 Subject: [PATCH 04/41] fix ruff --- cogs/get_token_authorisation.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/cogs/get_token_authorisation.py b/cogs/get_token_authorisation.py index c83bdad33..74cf4af65 100644 --- a/cogs/get_token_authorisation.py +++ b/cogs/get_token_authorisation.py @@ -147,9 +147,7 @@ async def get_token_groups(self, iterable: bool) -> str | "Iterable"[str]: # no constructed_organisations: str = ( f"Admin token has access to the following MSL Organisations as " - f"{user_name.text}:\n{ - ', \n'.join(organisation for organisation in organisations) - }" + f"{user_name.text}:\n{', \n'.join(organisation for organisation in organisations)}" ) return organisations if iterable else constructed_organisations @@ -179,6 +177,7 @@ async def get_token_authorisation(self, ctx: "TeXBotApplicationContext") -> None ephemeral=True, ) + class TokenAuthorisationCheckTaskCog(TokenAuthorisationBaseCog): """Cog class that defines the background task for token authorisation checks.""" @@ -215,6 +214,4 @@ async def token_authorisation_check_task(self) -> None: if not token_valid: logger.warning("Token is not valid!") - await self.bot.fetch_log_channel().send( - "Auth token has expired!" - ) + await self.bot.fetch_log_channel().send("Auth token has expired!") From e6b576227fcc4c94f315f7334bb8b03e0abb2178 Mon Sep 17 00:00:00 2001 From: MattyTheHacker <18513864+MattyTheHacker@users.noreply.github.com> Date: Sat, 3 May 2025 21:58:30 +0100 Subject: [PATCH 05/41] fix settings names --- cogs/get_token_authorisation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cogs/get_token_authorisation.py b/cogs/get_token_authorisation.py index 74cf4af65..4a6a720fe 100644 --- a/cogs/get_token_authorisation.py +++ b/cogs/get_token_authorisation.py @@ -184,7 +184,7 @@ class TokenAuthorisationCheckTaskCog(TokenAuthorisationBaseCog): @override def __init__(self, bot: "TeXBot") -> None: """Start all task managers when this cog is initialised.""" - if settings["TOKEN_AUTHORISATION_CHECK_TASK"]: + if settings["AUTO_AUTH_SESSION_COOKIE_CHECKING"]: _ = self.token_authorisation_check_task.start() super().__init__(bot) @@ -198,7 +198,7 @@ def cog_unload(self) -> None: """ self.token_authorisation_check_task.cancel() - @tasks.loop(**settings["TOKEN_AUTHORISATION_CHECK_TASK_INTERVAL"]) + @tasks.loop(**settings["AUTO_AUTH_SESSION_COOKIE_CHECKING"]) @capture_guild_does_not_exist_error async def token_authorisation_check_task(self) -> None: """ From 6230e26059dd4f58604b42541bf254255a88a1af Mon Sep 17 00:00:00 2001 From: MattyTheHacker <18513864+MattyTheHacker@users.noreply.github.com> Date: Sat, 3 May 2025 22:06:23 +0100 Subject: [PATCH 06/41] fix --- cogs/__init__.py | 7 ++++++- cogs/get_token_authorisation.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/cogs/__init__.py b/cogs/__init__.py index 068641872..21e9332fd 100644 --- a/cogs/__init__.py +++ b/cogs/__init__.py @@ -21,7 +21,10 @@ ) from .delete_all import DeleteAllCommandsCog from .edit_message import EditMessageCommandCog -from .get_token_authorisation import GetTokenAuthorisationCommandCog +from .get_token_authorisation import ( + GetTokenAuthorisationCommandCog, + TokenAuthorisationCheckTaskCog, +) from .induct import ( EnsureMembersInductedCommandCog, InductContextCommandsCog, @@ -78,6 +81,7 @@ "StatsCommandsCog", "StrikeCommandCog", "StrikeContextCommandsCog", + "TokenAuthorisationCheckTaskCog", "WriteRolesCommandCog", "setup", ) @@ -117,6 +121,7 @@ def setup(bot: "TeXBot") -> None: StatsCommandsCog, StrikeCommandCog, StrikeContextCommandsCog, + TokenAuthorisationCheckTaskCog, WriteRolesCommandCog, ) Cog: type[TeXBotBaseCog] diff --git a/cogs/get_token_authorisation.py b/cogs/get_token_authorisation.py index 4a6a720fe..437cd2844 100644 --- a/cogs/get_token_authorisation.py +++ b/cogs/get_token_authorisation.py @@ -22,7 +22,7 @@ from utils import TeXBot, TeXBotApplicationContext -__all__: "Sequence[str]" = ("GetTokenAuthorisationCommandCog",) +__all__: "Sequence[str]" = ("GetTokenAuthorisationCommandCog","TokenAuthorisationCheckTaskCog") logger: "Final[Logger]" = logging.getLogger("TeX-Bot") From 8d76c8bbc2db1f201ecb561a0b2c23b7fe1982ed Mon Sep 17 00:00:00 2001 From: MattyTheHacker <18513864+MattyTheHacker@users.noreply.github.com> Date: Sat, 3 May 2025 22:07:16 +0100 Subject: [PATCH 07/41] ruff --- cogs/get_token_authorisation.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cogs/get_token_authorisation.py b/cogs/get_token_authorisation.py index 437cd2844..e50a9d569 100644 --- a/cogs/get_token_authorisation.py +++ b/cogs/get_token_authorisation.py @@ -22,7 +22,10 @@ from utils import TeXBot, TeXBotApplicationContext -__all__: "Sequence[str]" = ("GetTokenAuthorisationCommandCog","TokenAuthorisationCheckTaskCog") +__all__: "Sequence[str]" = ( + "GetTokenAuthorisationCommandCog", + "TokenAuthorisationCheckTaskCog", +) logger: "Final[Logger]" = logging.getLogger("TeX-Bot") From 42e0f44a80f9aa6ea25882bfe383edf8e0fb8332 Mon Sep 17 00:00:00 2001 From: MattyTheHacker <18513864+MattyTheHacker@users.noreply.github.com> Date: Sat, 3 May 2025 22:19:02 +0100 Subject: [PATCH 08/41] actually call the method --- cogs/get_token_authorisation.py | 7 +++++-- config.py | 9 ++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/cogs/get_token_authorisation.py b/cogs/get_token_authorisation.py index e50a9d569..bc650fa25 100644 --- a/cogs/get_token_authorisation.py +++ b/cogs/get_token_authorisation.py @@ -99,8 +99,11 @@ async def get_token_groups(self, iterable: bool) -> str | "Iterable"[str]: # no return PROFILE_PAGE_INVALID if "Login" in str(page_title): - logger.warning("Unable to fetch profile page because the token was not valid.") - return [] + EXPIRED_AUTH_MESSAGE: Final[str] = ( + "Authentication redirected to login page. Token is invalid or expired." + ) + logger.warning(EXPIRED_AUTH_MESSAGE) + return EXPIRED_AUTH_MESSAGE profile_section_html: bs4.Tag | bs4.NavigableString | None = response_object.find( "div", diff --git a/config.py b/config.py index 27d5117fd..d787cf725 100644 --- a/config.py +++ b/config.py @@ -432,6 +432,8 @@ def _setup_auto_auth_session_cookie_checking(cls) -> None: os.getenv("AUTO_AUTH_SESSION_COOKIE_CHECKING", "False"), ) + logger.debug(raw_auto_auth_session_cookie_checking) + if raw_auto_auth_session_cookie_checking in FALSE_VALUES: cls._settings["AUTO_AUTH_SESSION_COOKIE_CHECKING"] = False return @@ -445,12 +447,12 @@ def _setup_auto_auth_session_cookie_checking(cls) -> None: ) if not raw_auto_auth_session_cookie_checking_delay: - INVALID_SEND_INTRODUCTION_REMINDERS_DELAY_MESSAGE: Final[str] = ( - "SEND_INTRODUCTION_REMINDERS_DELAY must contain the delay " + INVALID_AUTO_AUTH_CHECKING_DELAY_MESSAGE: Final[str] = ( + "AUTO_AUTH_SESSION_COOKIE_CHECKING must contain the delay " "in any combination of seconds, minutes, hours, days or weeks." ) raise ImproperlyConfiguredError( - INVALID_SEND_INTRODUCTION_REMINDERS_DELAY_MESSAGE, + INVALID_AUTO_AUTH_CHECKING_DELAY_MESSAGE, ) raw_timedelta_auto_auth_session_cookie_checking_delay: timedelta = timedelta( @@ -783,6 +785,7 @@ def _setup_env_variables(cls) -> None: cls._setup_roles_messages() cls._setup_organisation_id() cls._setup_members_list_auth_session_cookie() + cls._setup_auto_auth_session_cookie_checking() cls._setup_membership_perks_url() cls._setup_purchase_membership_url() cls._setup_send_introduction_reminders() From c0debd4e90b8c30665b549b801b4807e3e99c37f Mon Sep 17 00:00:00 2001 From: MattyTheHacker <18513864+MattyTheHacker@users.noreply.github.com> Date: Sat, 3 May 2025 22:27:25 +0100 Subject: [PATCH 09/41] fix again --- config.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/config.py b/config.py index d787cf725..5c326f35f 100644 --- a/config.py +++ b/config.py @@ -429,11 +429,9 @@ def _setup_members_list_auth_session_cookie(cls) -> None: @classmethod def _setup_auto_auth_session_cookie_checking(cls) -> None: raw_auto_auth_session_cookie_checking: str | bool = str( - os.getenv("AUTO_AUTH_SESSION_COOKIE_CHECKING", "False"), + os.getenv("AUTO_AUTH_SESSION_COOKIE_CHECKING", "false"), ) - logger.debug(raw_auto_auth_session_cookie_checking) - if raw_auto_auth_session_cookie_checking in FALSE_VALUES: cls._settings["AUTO_AUTH_SESSION_COOKIE_CHECKING"] = False return @@ -451,6 +449,7 @@ def _setup_auto_auth_session_cookie_checking(cls) -> None: "AUTO_AUTH_SESSION_COOKIE_CHECKING must contain the delay " "in any combination of seconds, minutes, hours, days or weeks." ) + logger.debug(raw_auto_auth_session_cookie_checking) raise ImproperlyConfiguredError( INVALID_AUTO_AUTH_CHECKING_DELAY_MESSAGE, ) From be9036b5dc85158cbf8be7c036124c345e11f556 Mon Sep 17 00:00:00 2001 From: MattyTheHacker <18513864+MattyTheHacker@users.noreply.github.com> Date: Sat, 3 May 2025 22:45:27 +0100 Subject: [PATCH 10/41] change --- config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.py b/config.py index 5c326f35f..ffbe5ee89 100644 --- a/config.py +++ b/config.py @@ -429,7 +429,7 @@ def _setup_members_list_auth_session_cookie(cls) -> None: @classmethod def _setup_auto_auth_session_cookie_checking(cls) -> None: raw_auto_auth_session_cookie_checking: str | bool = str( - os.getenv("AUTO_AUTH_SESSION_COOKIE_CHECKING", "false"), + os.getenv("AUTO_AUTH_SESSION_COOKIE_CHECKING", "False"), ) if raw_auto_auth_session_cookie_checking in FALSE_VALUES: From 90104691650658969029b37a67519a2d582f44ad Mon Sep 17 00:00:00 2001 From: MattyTheHacker <18513864+MattyTheHacker@users.noreply.github.com> Date: Sat, 3 May 2025 23:56:48 +0100 Subject: [PATCH 11/41] add logging --- config.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/config.py b/config.py index ffbe5ee89..54a9c29ba 100644 --- a/config.py +++ b/config.py @@ -432,7 +432,11 @@ def _setup_auto_auth_session_cookie_checking(cls) -> None: os.getenv("AUTO_AUTH_SESSION_COOKIE_CHECKING", "False"), ) - if raw_auto_auth_session_cookie_checking in FALSE_VALUES: + if ( + raw_auto_auth_session_cookie_checking in FALSE_VALUES + or raw_auto_auth_session_cookie_checking == "False" + ): + logger.debug("Setting AUTO_AUTH_SESSION_COOKIE_CHECKING to False.") cls._settings["AUTO_AUTH_SESSION_COOKIE_CHECKING"] = False return @@ -444,6 +448,8 @@ def _setup_auto_auth_session_cookie_checking(cls) -> None: str(raw_auto_auth_session_cookie_checking), ) + logger.debug(raw_auto_auth_session_cookie_checking_delay) + if not raw_auto_auth_session_cookie_checking_delay: INVALID_AUTO_AUTH_CHECKING_DELAY_MESSAGE: Final[str] = ( "AUTO_AUTH_SESSION_COOKIE_CHECKING must contain the delay " From 35eb61cb5c06c6c7d9ba5ea06cced3128cbd616c Mon Sep 17 00:00:00 2001 From: MattyTheHacker <18513864+MattyTheHacker@users.noreply.github.com> Date: Sun, 4 May 2025 00:36:09 +0100 Subject: [PATCH 12/41] suck my ass --- cogs/get_token_authorisation.py | 2 +- config.py | 40 ++++++++++++++++++++------------- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/cogs/get_token_authorisation.py b/cogs/get_token_authorisation.py index bc650fa25..717281768 100644 --- a/cogs/get_token_authorisation.py +++ b/cogs/get_token_authorisation.py @@ -204,7 +204,7 @@ def cog_unload(self) -> None: """ self.token_authorisation_check_task.cancel() - @tasks.loop(**settings["AUTO_AUTH_SESSION_COOKIE_CHECKING"]) + @tasks.loop(**settings["AUTO_AUTH_SESSION_COOKIE_CHECKING_INTERVAL"]) @capture_guild_does_not_exist_error async def token_authorisation_check_task(self) -> None: """ diff --git a/config.py b/config.py index 54a9c29ba..e7a10d64b 100644 --- a/config.py +++ b/config.py @@ -428,34 +428,42 @@ def _setup_members_list_auth_session_cookie(cls) -> None: @classmethod def _setup_auto_auth_session_cookie_checking(cls) -> None: - raw_auto_auth_session_cookie_checking: str | bool = str( - os.getenv("AUTO_AUTH_SESSION_COOKIE_CHECKING", "False"), + raw_auto_auth_session_cookie_checking: str = str( + os.getenv("AUTO_AUTH_SESSION_COOKIE_CHECKING_INTERVAL", "false"), ) - if ( - raw_auto_auth_session_cookie_checking in FALSE_VALUES - or raw_auto_auth_session_cookie_checking == "False" - ): - logger.debug("Setting AUTO_AUTH_SESSION_COOKIE_CHECKING to False.") - cls._settings["AUTO_AUTH_SESSION_COOKIE_CHECKING"] = False - return + if raw_auto_auth_session_cookie_checking.lower() not in TRUE_VALUES | FALSE_VALUES: + INVALID_AUTO_AUTH_CHECKING_MESSAGE: Final[str] = ( + "AUTO_AUTH_SESSION_COOKIE_CHECKING must be a boolean value." + ) + raise ImproperlyConfiguredError(INVALID_AUTO_AUTH_CHECKING_MESSAGE) + + cls._settings["AUTO_AUTH_SESSION_COOKIE_CHECKING"] = bool( + raw_auto_auth_session_cookie_checking in TRUE_VALUES + ) - if raw_auto_auth_session_cookie_checking in TRUE_VALUES: - raw_auto_auth_session_cookie_checking = "24h" + @classmethod + def _setup_auto_auth_session_cookie_checking_interval(cls) -> None: + if "AUTO_AUTH_SESSION_COOKIE_CHECKING" not in cls._settings: + INVALID_SETUP_ORDER_MESSAGE: Final[str] = ( + "Invalid setup order: AUTO_AUTH_SESSION_COOKIE_CHECKING must be set up " + "before AUTO_AUTH_SESSION_COOKIE_CHECKING_INTERVAL can be set up." + ) + raise RuntimeError(INVALID_SETUP_ORDER_MESSAGE) raw_auto_auth_session_cookie_checking_delay: re.Match[str] | None = re.fullmatch( r"\A(?:(?P(?:\d*\.)?\d+)s)?(?:(?P(?:\d*\.)?\d+)m)?(?:(?P(?:\d*\.)?\d+)h)?(?:(?P(?:\d*\.)?\d+)d)?(?:(?P(?:\d*\.)?\d+)w)?\Z", - str(raw_auto_auth_session_cookie_checking), + str(os.getenv("AUTO_AUTH_SESSION_COOKIE_CHECKING_INTERVAL", "24h")), ) logger.debug(raw_auto_auth_session_cookie_checking_delay) if not raw_auto_auth_session_cookie_checking_delay: INVALID_AUTO_AUTH_CHECKING_DELAY_MESSAGE: Final[str] = ( - "AUTO_AUTH_SESSION_COOKIE_CHECKING must contain the delay " + "AUTO_AUTH_SESSION_COOKIE_CHECKING_INTERVAL must contain the delay " "in any combination of seconds, minutes, hours, days or weeks." ) - logger.debug(raw_auto_auth_session_cookie_checking) + logger.debug(raw_auto_auth_session_cookie_checking_delay) raise ImproperlyConfiguredError( INVALID_AUTO_AUTH_CHECKING_DELAY_MESSAGE, ) @@ -476,7 +484,7 @@ def _setup_auto_auth_session_cookie_checking(cls) -> None: "recommended minimum (24h) which could cause performance issues." ) - cls._settings["AUTO_AUTH_SESSION_COOKIE_CHECKING"] = ( + cls._settings["AUTO_AUTH_SESSION_COOKIE_CHECKING_INTERVAL"] = ( raw_timedelta_auto_auth_session_cookie_checking_delay ) @@ -790,7 +798,7 @@ def _setup_env_variables(cls) -> None: cls._setup_roles_messages() cls._setup_organisation_id() cls._setup_members_list_auth_session_cookie() - cls._setup_auto_auth_session_cookie_checking() + cls._setup_auto_auth_session_cookie_checking_interval() cls._setup_membership_perks_url() cls._setup_purchase_membership_url() cls._setup_send_introduction_reminders() From 3f0178020cce9dc8fbaa80d5fadee31d6f435b08 Mon Sep 17 00:00:00 2001 From: MattyTheHacker <18513864+MattyTheHacker@users.noreply.github.com> Date: Sun, 4 May 2025 03:06:44 +0100 Subject: [PATCH 13/41] Implement fix --- cogs/get_token_authorisation.py | 8 +++--- config.py | 43 +++++++++++++++++---------------- 2 files changed, 25 insertions(+), 26 deletions(-) diff --git a/cogs/get_token_authorisation.py b/cogs/get_token_authorisation.py index 717281768..4a95390ed 100644 --- a/cogs/get_token_authorisation.py +++ b/cogs/get_token_authorisation.py @@ -67,9 +67,9 @@ async def is_token_valid(self) -> bool: page_title: bs4.Tag | bs4.NavigableString | None = response_object.find("title") - return "Login" in str(page_title) + return "Login" not in str(page_title) - async def get_token_groups(self, iterable: bool) -> str | "Iterable"[str]: # noqa: FBT001 + async def get_token_groups(self, iterable: bool) -> "str | Iterable[str]": # noqa: FBT001 """ Definition of method to get the groups the token has access to. @@ -218,6 +218,4 @@ async def token_authorisation_check_task(self) -> None: token_valid: bool = await self.is_token_valid() if not token_valid: - logger.warning("Token is not valid!") - - await self.bot.fetch_log_channel().send("Auth token has expired!") + logger.warning("Session cookie is invalid or expired!") diff --git a/config.py b/config.py index e7a10d64b..b048719c7 100644 --- a/config.py +++ b/config.py @@ -429,7 +429,7 @@ def _setup_members_list_auth_session_cookie(cls) -> None: @classmethod def _setup_auto_auth_session_cookie_checking(cls) -> None: raw_auto_auth_session_cookie_checking: str = str( - os.getenv("AUTO_AUTH_SESSION_COOKIE_CHECKING_INTERVAL", "false"), + os.getenv("AUTO_AUTH_SESSION_COOKIE_CHECKING", "false"), ) if raw_auto_auth_session_cookie_checking.lower() not in TRUE_VALUES | FALSE_VALUES: @@ -439,7 +439,7 @@ def _setup_auto_auth_session_cookie_checking(cls) -> None: raise ImproperlyConfiguredError(INVALID_AUTO_AUTH_CHECKING_MESSAGE) cls._settings["AUTO_AUTH_SESSION_COOKIE_CHECKING"] = bool( - raw_auto_auth_session_cookie_checking in TRUE_VALUES + raw_auto_auth_session_cookie_checking.lower() in TRUE_VALUES ) @classmethod @@ -451,41 +451,41 @@ def _setup_auto_auth_session_cookie_checking_interval(cls) -> None: ) raise RuntimeError(INVALID_SETUP_ORDER_MESSAGE) - raw_auto_auth_session_cookie_checking_delay: re.Match[str] | None = re.fullmatch( + if not cls._settings["AUTO_AUTH_SESSION_COOKIE_CHECKING"]: + cls._settings["AUTO_AUTH_SESSION_COOKIE_CHECKING_INTERVAL"] = { + "hours": 24, + } + return + + raw_auto_auth_session_cookie_checking_interval: re.Match[str] | None = re.fullmatch( r"\A(?:(?P(?:\d*\.)?\d+)s)?(?:(?P(?:\d*\.)?\d+)m)?(?:(?P(?:\d*\.)?\d+)h)?(?:(?P(?:\d*\.)?\d+)d)?(?:(?P(?:\d*\.)?\d+)w)?\Z", str(os.getenv("AUTO_AUTH_SESSION_COOKIE_CHECKING_INTERVAL", "24h")), ) - logger.debug(raw_auto_auth_session_cookie_checking_delay) - - if not raw_auto_auth_session_cookie_checking_delay: + if not raw_auto_auth_session_cookie_checking_interval: INVALID_AUTO_AUTH_CHECKING_DELAY_MESSAGE: Final[str] = ( "AUTO_AUTH_SESSION_COOKIE_CHECKING_INTERVAL must contain the delay " "in any combination of seconds, minutes, hours, days or weeks." ) - logger.debug(raw_auto_auth_session_cookie_checking_delay) + logger.debug(raw_auto_auth_session_cookie_checking_interval) raise ImproperlyConfiguredError( INVALID_AUTO_AUTH_CHECKING_DELAY_MESSAGE, ) - raw_timedelta_auto_auth_session_cookie_checking_delay: timedelta = timedelta( - **{ - key: float(value) - for key, value in ( - raw_auto_auth_session_cookie_checking_delay.groupdict().items() - ) - if value - }, - ) + raw_timedelta_auto_auth_session_cookie_checking_interval: Mapping[str, float] = { + "hours": 24, + } - if raw_timedelta_auto_auth_session_cookie_checking_delay < timedelta(days=1): - logger.warning( - "Automatic checking of the MSL session cookie is below the " - "recommended minimum (24h) which could cause performance issues." + raw_timedelta_auto_auth_session_cookie_checking_interval = { + key: float(value) + for key, value in ( + raw_auto_auth_session_cookie_checking_interval.groupdict().items() ) + if value + } cls._settings["AUTO_AUTH_SESSION_COOKIE_CHECKING_INTERVAL"] = ( - raw_timedelta_auto_auth_session_cookie_checking_delay + raw_timedelta_auto_auth_session_cookie_checking_interval ) @classmethod @@ -798,6 +798,7 @@ def _setup_env_variables(cls) -> None: cls._setup_roles_messages() cls._setup_organisation_id() cls._setup_members_list_auth_session_cookie() + cls._setup_auto_auth_session_cookie_checking() cls._setup_auto_auth_session_cookie_checking_interval() cls._setup_membership_perks_url() cls._setup_purchase_membership_url() From b9c9964bff7ce5f26eb2a1f43e7dec468e68c49c Mon Sep 17 00:00:00 2001 From: MattyTheHacker <18513864+MattyTheHacker@users.noreply.github.com> Date: Sun, 4 May 2025 13:05:44 +0100 Subject: [PATCH 14/41] Improve debug messages --- cogs/get_token_authorisation.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/cogs/get_token_authorisation.py b/cogs/get_token_authorisation.py index 4a95390ed..1936d0eec 100644 --- a/cogs/get_token_authorisation.py +++ b/cogs/get_token_authorisation.py @@ -111,22 +111,23 @@ async def get_token_groups(self, iterable: bool) -> "str | Iterable[str]": # no ) if profile_section_html is None: - logger.warning( + NO_PROFILE_WARNING_MESSAGE: Final[str] = ( "Couldn't find the profile section of the user" - "when scraping the website's HTML!", + "when scraping the website's HTML!" ) + logger.warning(NO_PROFILE_WARNING_MESSAGE) logger.debug("Retrieved HTML: %s", response_html) - return "Something went wrong when fetching the profile page!" + return NO_PROFILE_WARNING_MESSAGE user_name: bs4.Tag | bs4.NavigableString | int | None = profile_section_html.find("h1") if not isinstance(user_name, bs4.Tag): NO_PROFILE_DEBUG_MESSAGE: Final[str] = ( - "Found user profile but couldn't find their name!" + "Found user profile but couldn't find their name." ) logger.warning(NO_PROFILE_DEBUG_MESSAGE) logger.debug("Retrieved HTML: %s", response_html) - return "Something went wrong when fetching the profile page!" + return NO_PROFILE_DEBUG_MESSAGE parsed_html: bs4.Tag | bs4.NavigableString | None = response_object.find( "ul", From b8bfc9e1631478c7007651c82743eae53bbe9a12 Mon Sep 17 00:00:00 2001 From: MattyTheHacker <18513864+MattyTheHacker@users.noreply.github.com> Date: Sun, 4 May 2025 19:58:01 +0100 Subject: [PATCH 15/41] refactor to make it nicer --- cogs/get_token_authorisation.py | 86 ++++++++++++++++++++++----------- 1 file changed, 59 insertions(+), 27 deletions(-) diff --git a/cogs/get_token_authorisation.py b/cogs/get_token_authorisation.py index 1936d0eec..b7e82f3a7 100644 --- a/cogs/get_token_authorisation.py +++ b/cogs/get_token_authorisation.py @@ -1,6 +1,7 @@ """Contains cog classes for token authorisation check interactions.""" import logging +from enum import Enum from typing import TYPE_CHECKING, override import aiohttp @@ -39,35 +40,72 @@ ".ASPXAUTH": settings["MEMBERS_LIST_AUTH_SESSION_COOKIE"], } -REQUEST_URL: "Final[str]" = "https://guildofstudents.com/profile" +PROFILE_URL: "Final[str]" = "https://guildofstudents.com/profile" +ORGANISATION_URL: "Final[str]" = "https://www.guildofstudents.com/organisation/admin" class TokenAuthorisationBaseCog(TeXBotBaseCog): """Cog class that defines the base for token authorisation functions.""" - async def is_token_valid(self) -> bool: + class TokenStatus(Enum): """ - Definition of method to check if the authorisation token is valid. + Enum class that defines the status of the token. + + INVALID: The token does not have access to a user, meaning it is invalid or expired. + VALID: The token is a valid user, but not neccessarily to an organisation. + AUTHORISED: The token is a valid user and has access to an organisation. + """ + + INVALID = "invalid" + VALID = "valid" + AUTHORISED = "authorised" + + async def get_profile_page(self) -> bs4.BeautifulSoup: + """ + Definition of method to get the profile page. This is done by requesting the user profile page and - checking if the page title contains "Login". + scraping the HTML for the list of groups. """ http_session: aiohttp.ClientSession = aiohttp.ClientSession( headers=REQUEST_HEADERS, cookies=REQUEST_COOKIES, ) - async with http_session, http_session.get(REQUEST_URL) as http_response: + async with http_session, http_session.get(PROFILE_URL) as http_response: response_html: str = await http_response.text() - response_object: bs4.BeautifulSoup = BeautifulSoup( - response_html, - "html.parser", - ) + return BeautifulSoup(response_html, "html.parser") + + async def get_token_status(self) -> TokenStatus: + """ + Definition of method to get the status of the token. + This is done by checking if the token is valid and if it is, + checking if the token has access to the organisation. + """ + response_object: bs4.BeautifulSoup = await self.get_profile_page() page_title: bs4.Tag | bs4.NavigableString | None = response_object.find("title") + if not page_title or "Login" in str(page_title): + logger.debug("Token is invalid or expired.") + return self.TokenStatus.INVALID + + organisation_admin_url: str = f"{ORGANISATION_URL}/{settings['ORGANISATION_ID']}" + + async with ( + aiohttp.ClientSession( + headers=REQUEST_HEADERS, + cookies=REQUEST_COOKIES, + ) as http_session, + http_session.get(organisation_admin_url) as http_response, + ): + response_html: str = await http_response.text() - return "Login" not in str(page_title) + return ( + self.TokenStatus.AUTHORISED + if "Admin Tools" in response_html + else self.TokenStatus.VALID + ) async def get_token_groups(self, iterable: bool) -> "str | Iterable[str]": # noqa: FBT001 """ @@ -76,18 +114,7 @@ async def get_token_groups(self, iterable: bool) -> "str | Iterable[str]": # no This is done by requesting the user profile page and scraping the HTML for the list of groups. """ - http_session: aiohttp.ClientSession = aiohttp.ClientSession( - headers=REQUEST_HEADERS, - cookies=REQUEST_COOKIES, - ) - - async with http_session, http_session.get(REQUEST_URL) as http_response: - response_html: str = await http_response.text() - - response_object: bs4.BeautifulSoup = BeautifulSoup( - response_html, - "html.parser", - ) + response_object: bs4.BeautifulSoup = await self.get_profile_page() page_title: bs4.Tag | bs4.NavigableString | None = response_object.find("title") @@ -116,7 +143,7 @@ async def get_token_groups(self, iterable: bool) -> "str | Iterable[str]": # no "when scraping the website's HTML!" ) logger.warning(NO_PROFILE_WARNING_MESSAGE) - logger.debug("Retrieved HTML: %s", response_html) + logger.debug("Retrieved HTML: %s", response_object.text) return NO_PROFILE_WARNING_MESSAGE user_name: bs4.Tag | bs4.NavigableString | int | None = profile_section_html.find("h1") @@ -126,7 +153,7 @@ async def get_token_groups(self, iterable: bool) -> "str | Iterable[str]": # no "Found user profile but couldn't find their name." ) logger.warning(NO_PROFILE_DEBUG_MESSAGE) - logger.debug("Retrieved HTML: %s", response_html) + logger.debug("Retrieved HTML: %s", response_object.text) return NO_PROFILE_DEBUG_MESSAGE parsed_html: bs4.Tag | bs4.NavigableString | None = response_object.find( @@ -216,7 +243,12 @@ async def token_authorisation_check_task(self) -> None: """ logger.debug("Running token authorisation check task...") - token_valid: bool = await self.is_token_valid() + token_status: TokenAuthorisationBaseCog.TokenStatus = await self.get_token_status() + + if token_status == self.TokenStatus.INVALID: + logger.warning("Token is invalid or expired.") + return - if not token_valid: - logger.warning("Session cookie is invalid or expired!") + if token_status == self.TokenStatus.VALID: + logger.warning("Token is valid but does not have access to the organisation.") + return From 6f29beff94b3c92501a91413b25b62e92311a53e Mon Sep 17 00:00:00 2001 From: MattyTheHacker <18513864+MattyTheHacker@users.noreply.github.com> Date: Sun, 4 May 2025 20:52:58 +0100 Subject: [PATCH 16/41] minor refactor --- cogs/get_token_authorisation.py | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/cogs/get_token_authorisation.py b/cogs/get_token_authorisation.py index b7e82f3a7..5dfdb3703 100644 --- a/cogs/get_token_authorisation.py +++ b/cogs/get_token_authorisation.py @@ -60,6 +60,17 @@ class TokenStatus(Enum): VALID = "valid" AUTHORISED = "authorised" + async def fetch_with_session(self, url: str) -> str: + """Fetch a URL using a shared aiohttp session.""" + async with ( + aiohttp.ClientSession( + headers=REQUEST_HEADERS, + cookies=REQUEST_COOKIES, + ) as http_session, + http_session.get(url) as http_response, + ): + return await http_response.text() + async def get_profile_page(self) -> bs4.BeautifulSoup: """ Definition of method to get the profile page. @@ -67,14 +78,7 @@ async def get_profile_page(self) -> bs4.BeautifulSoup: This is done by requesting the user profile page and scraping the HTML for the list of groups. """ - http_session: aiohttp.ClientSession = aiohttp.ClientSession( - headers=REQUEST_HEADERS, - cookies=REQUEST_COOKIES, - ) - - async with http_session, http_session.get(PROFILE_URL) as http_response: - response_html: str = await http_response.text() - + response_html: str = await self.fetch_with_session(PROFILE_URL) return BeautifulSoup(response_html, "html.parser") async def get_token_status(self) -> TokenStatus: @@ -91,15 +95,7 @@ async def get_token_status(self) -> TokenStatus: return self.TokenStatus.INVALID organisation_admin_url: str = f"{ORGANISATION_URL}/{settings['ORGANISATION_ID']}" - - async with ( - aiohttp.ClientSession( - headers=REQUEST_HEADERS, - cookies=REQUEST_COOKIES, - ) as http_session, - http_session.get(organisation_admin_url) as http_response, - ): - response_html: str = await http_response.text() + response_html: str = await self.fetch_with_session(organisation_admin_url) return ( self.TokenStatus.AUTHORISED From af878eff8f5d128617a820b67bd334bcbf484d1a Mon Sep 17 00:00:00 2001 From: MattyTheHacker <18513864+MattyTheHacker@users.noreply.github.com> Date: Fri, 9 May 2025 09:50:33 +0100 Subject: [PATCH 17/41] Fix some stuff --- cogs/get_token_authorisation.py | 37 ++++++++++++++++----------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/cogs/get_token_authorisation.py b/cogs/get_token_authorisation.py index 5dfdb3703..76e11bb0a 100644 --- a/cogs/get_token_authorisation.py +++ b/cogs/get_token_authorisation.py @@ -7,7 +7,6 @@ import aiohttp import bs4 import discord -from bs4 import BeautifulSoup from discord.ext import tasks from config import settings @@ -52,7 +51,7 @@ class TokenStatus(Enum): Enum class that defines the status of the token. INVALID: The token does not have access to a user, meaning it is invalid or expired. - VALID: The token is a valid user, but not neccessarily to an organisation. + VALID: The token is a valid user, but not neccessarily admin to an organisation. AUTHORISED: The token is a valid user and has access to an organisation. """ @@ -71,16 +70,6 @@ async def fetch_with_session(self, url: str) -> str: ): return await http_response.text() - async def get_profile_page(self) -> bs4.BeautifulSoup: - """ - Definition of method to get the profile page. - - This is done by requesting the user profile page and - scraping the HTML for the list of groups. - """ - response_html: str = await self.fetch_with_session(PROFILE_URL) - return BeautifulSoup(response_html, "html.parser") - async def get_token_status(self) -> TokenStatus: """ Definition of method to get the status of the token. @@ -88,7 +77,9 @@ async def get_token_status(self) -> TokenStatus: This is done by checking if the token is valid and if it is, checking if the token has access to the organisation. """ - response_object: bs4.BeautifulSoup = await self.get_profile_page() + response_object: bs4.BeautifulSoup = bs4.BeautifulSoup( + await self.fetch_with_session(PROFILE_URL), "html.parser" + ) page_title: bs4.Tag | bs4.NavigableString | None = response_object.find("title") if not page_title or "Login" in str(page_title): logger.debug("Token is invalid or expired.") @@ -97,11 +88,17 @@ async def get_token_status(self) -> TokenStatus: organisation_admin_url: str = f"{ORGANISATION_URL}/{settings['ORGANISATION_ID']}" response_html: str = await self.fetch_with_session(organisation_admin_url) - return ( - self.TokenStatus.AUTHORISED - if "Admin Tools" in response_html - else self.TokenStatus.VALID - ) + if "admin tools" in response_html.lower(): + logger.debug(response_html) + return self.TokenStatus.AUTHORISED + + if "You do not have any permissions for this organisation" in response_html.lower(): + logger.debug(response_html) + return self.TokenStatus.VALID + + logger.warning("Unexpected response when checking token authorisation.") + logger.debug(response_html) + return self.TokenStatus.INVALID async def get_token_groups(self, iterable: bool) -> "str | Iterable[str]": # noqa: FBT001 """ @@ -110,7 +107,9 @@ async def get_token_groups(self, iterable: bool) -> "str | Iterable[str]": # no This is done by requesting the user profile page and scraping the HTML for the list of groups. """ - response_object: bs4.BeautifulSoup = await self.get_profile_page() + response_object: bs4.BeautifulSoup = bs4.BeautifulSoup( + await self.fetch_with_session(PROFILE_URL), "html.parser" + ) page_title: bs4.Tag | bs4.NavigableString | None = response_object.find("title") From f36602d8a1a051a54e1faab1f85653b147be6d98 Mon Sep 17 00:00:00 2001 From: MattyTheHacker <18513864+MattyTheHacker@users.noreply.github.com> Date: Fri, 9 May 2025 21:43:31 +0100 Subject: [PATCH 18/41] Refactor the method to only return the list --- cogs/get_token_authorisation.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/cogs/get_token_authorisation.py b/cogs/get_token_authorisation.py index 76e11bb0a..c5eb1c788 100644 --- a/cogs/get_token_authorisation.py +++ b/cogs/get_token_authorisation.py @@ -100,7 +100,7 @@ async def get_token_status(self) -> TokenStatus: logger.debug(response_html) return self.TokenStatus.INVALID - async def get_token_groups(self, iterable: bool) -> "str | Iterable[str]": # noqa: FBT001 + async def get_token_groups(self) -> "Iterable[str]": """ Definition of method to get the groups the token has access to. @@ -118,14 +118,14 @@ async def get_token_groups(self, iterable: bool) -> "str | Iterable[str]": # no "Profile page returned no content when checking token authorisation." ) logger.warning(PROFILE_PAGE_INVALID) - return PROFILE_PAGE_INVALID + return [] if "Login" in str(page_title): EXPIRED_AUTH_MESSAGE: Final[str] = ( "Authentication redirected to login page. Token is invalid or expired." ) logger.warning(EXPIRED_AUTH_MESSAGE) - return EXPIRED_AUTH_MESSAGE + return [] profile_section_html: bs4.Tag | bs4.NavigableString | None = response_object.find( "div", @@ -139,7 +139,7 @@ async def get_token_groups(self, iterable: bool) -> "str | Iterable[str]": # no ) logger.warning(NO_PROFILE_WARNING_MESSAGE) logger.debug("Retrieved HTML: %s", response_object.text) - return NO_PROFILE_WARNING_MESSAGE + return [] user_name: bs4.Tag | bs4.NavigableString | int | None = profile_section_html.find("h1") @@ -149,7 +149,7 @@ async def get_token_groups(self, iterable: bool) -> "str | Iterable[str]": # no ) logger.warning(NO_PROFILE_DEBUG_MESSAGE) logger.debug("Retrieved HTML: %s", response_object.text) - return NO_PROFILE_DEBUG_MESSAGE + return [] parsed_html: bs4.Tag | bs4.NavigableString | None = response_object.find( "ul", @@ -162,7 +162,7 @@ async def get_token_groups(self, iterable: bool) -> "str | Iterable[str]": # no "Please check you have used the correct token!" ) logger.warning(NO_ADMIN_TABLE_MESSAGE) - return NO_ADMIN_TABLE_MESSAGE + return [] organisations: Iterable[str] = [ list_item.get_text(strip=True) for list_item in parsed_html.find_all("li") @@ -174,12 +174,7 @@ async def get_token_groups(self, iterable: bool) -> "str | Iterable[str]": # no user_name.text, ) - constructed_organisations: str = ( - f"Admin token has access to the following MSL Organisations as " - f"{user_name.text}:\n{', \n'.join(organisation for organisation in organisations)}" - ) - - return organisations if iterable else constructed_organisations + return organisations class GetTokenAuthorisationCommandCog(TokenAuthorisationBaseCog): @@ -202,7 +197,14 @@ async def get_token_authorisation(self, ctx: "TeXBotApplicationContext") -> None await ctx.defer(ephemeral=True) async with ctx.typing(): await ctx.followup.send( - content=str(await self.get_token_groups(iterable=False)), + content=( + f"Admin token has access to the following MSL Organisations: " + f"\n{ + ', \n'.join( + organisation for organisation in await self.get_token_groups() + ) + }" + ), ephemeral=True, ) From 6bb88f1087cf06740408644d38bdc027c7f1f07a Mon Sep 17 00:00:00 2001 From: MattyTheHacker <18513864+MattyTheHacker@users.noreply.github.com> Date: Mon, 12 May 2025 10:08:52 +0100 Subject: [PATCH 19/41] Improve token status check --- cogs/get_token_authorisation.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/cogs/get_token_authorisation.py b/cogs/get_token_authorisation.py index c5eb1c788..8d06bf64c 100644 --- a/cogs/get_token_authorisation.py +++ b/cogs/get_token_authorisation.py @@ -242,10 +242,15 @@ async def token_authorisation_check_task(self) -> None: token_status: TokenAuthorisationBaseCog.TokenStatus = await self.get_token_status() - if token_status == self.TokenStatus.INVALID: - logger.warning("Token is invalid or expired.") - return - - if token_status == self.TokenStatus.VALID: - logger.warning("Token is valid but does not have access to the organisation.") - return + match token_status: + case self.TokenStatus.AUTHORISED: + logger.info("Token is valid and has access to the organisation.") + return + + case self.TokenStatus.VALID: + logger.warning("Token is valid but does not have access to the organisation.") + return + + case self.TokenStatus.INVALID: + logger.warning("Token is invalid or expired.") + return From 67bceb118cb1adaa1031408fa1d816feb6fe15d6 Mon Sep 17 00:00:00 2001 From: MattyTheHacker <18513864+MattyTheHacker@users.noreply.github.com> Date: Mon, 12 May 2025 10:17:06 +0100 Subject: [PATCH 20/41] fix debug messages --- cogs/get_token_authorisation.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/cogs/get_token_authorisation.py b/cogs/get_token_authorisation.py index 8d06bf64c..24f555dda 100644 --- a/cogs/get_token_authorisation.py +++ b/cogs/get_token_authorisation.py @@ -89,15 +89,12 @@ async def get_token_status(self) -> TokenStatus: response_html: str = await self.fetch_with_session(organisation_admin_url) if "admin tools" in response_html.lower(): - logger.debug(response_html) return self.TokenStatus.AUTHORISED if "You do not have any permissions for this organisation" in response_html.lower(): - logger.debug(response_html) return self.TokenStatus.VALID logger.warning("Unexpected response when checking token authorisation.") - logger.debug(response_html) return self.TokenStatus.INVALID async def get_token_groups(self) -> "Iterable[str]": From 3b93dc3fd4e8c5cd74d43ef402b3dcfcd243f4b0 Mon Sep 17 00:00:00 2001 From: MattyTheHacker <18513864+MattyTheHacker@users.noreply.github.com> Date: Wed, 21 May 2025 07:59:30 +0100 Subject: [PATCH 21/41] add missing check --- cogs/get_token_authorisation.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cogs/get_token_authorisation.py b/cogs/get_token_authorisation.py index 24f555dda..c7aa8f49b 100644 --- a/cogs/get_token_authorisation.py +++ b/cogs/get_token_authorisation.py @@ -251,3 +251,8 @@ async def token_authorisation_check_task(self) -> None: case self.TokenStatus.INVALID: logger.warning("Token is invalid or expired.") return + + @token_authorisation_check_task.before_loop + async def before_tasks(self) -> None: + """Pre-execution hook, preventing any tasks from executing before the bot is ready.""" + await self.bot.wait_until_ready() From 1a6ff7595f5786cee7ca7acea5d30d500764b0bb Mon Sep 17 00:00:00 2001 From: Holly <25277367+Thatsmusic99@users.noreply.github.com> Date: Sun, 15 Jun 2025 13:49:43 +0100 Subject: [PATCH 22/41] Allow committee-elect to update actions (and appear in auto-complete) (#508) Signed-off-by: Holly <25277367+Thatsmusic99@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Matty Widdop <18513864+MattyTheHacker@users.noreply.github.com> --- cogs/committee_actions_tracking.py | 52 ++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/cogs/committee_actions_tracking.py b/cogs/committee_actions_tracking.py index 35c1fee9e..ccea5bc98 100644 --- a/cogs/committee_actions_tracking.py +++ b/cogs/committee_actions_tracking.py @@ -1,5 +1,6 @@ """Contains cog classes for tracking committee-actions.""" +import contextlib import logging import random from enum import Enum @@ -11,6 +12,7 @@ from db.core.models import AssignedCommitteeAction, DiscordMember from exceptions import ( + CommitteeElectRoleDoesNotExistError, CommitteeRoleDoesNotExistError, InvalidActionDescriptionError, InvalidActionTargetError, @@ -129,11 +131,22 @@ async def autocomplete_get_committee_members( except CommitteeRoleDoesNotExistError: return set() + committee_elect_role: discord.Role | None = None + with contextlib.suppress(CommitteeElectRoleDoesNotExistError): + committee_elect_role = await ctx.bot.committee_elect_role + return { discord.OptionChoice( name=f"{member.display_name} ({member.global_name})", value=str(member.id) ) - for member in committee_role.members + for member in ( + set(committee_role.members) + | ( + set(committee_elect_role.members) + if committee_elect_role is not None + else set() + ) + ) if not member.bot } @@ -281,9 +294,7 @@ async def create( required=True, parameter_name="status", ) - @CommandChecks.check_interaction_user_has_committee_role - @CommandChecks.check_interaction_user_in_main_guild - async def update_status( + async def update_status( # NOTE: Committee role check is not present because non-committee can have actions, and need to be able to list their own actions. self, ctx: "TeXBotApplicationContext", action_id: str, status: str ) -> None: """ @@ -561,9 +572,7 @@ async def action_all_committee( default=None, parameter_name="status", ) - @CommandChecks.check_interaction_user_has_committee_role - @CommandChecks.check_interaction_user_in_main_guild - async def list_user_actions( + async def list_user_actions( # NOTE: Committee role check is not present because non-committee can have actions, and need to be able to list their own actions. self, ctx: "TeXBotApplicationContext", *, @@ -575,15 +584,32 @@ async def list_user_actions( Definition and callback of the "/list" command. Takes in a user and lists out their current actions. + If no user is specified, the user issuing the command will be used. + If a user has the committee role, they can list actions for other users. + If a user does not have the committee role, they can only list their own actions. """ - action_member: discord.Member | discord.User + action_member_id = action_member_id.strip() + + action_member: discord.Member | discord.User = ( + await self.bot.get_member_from_str_id(action_member_id) + if action_member_id + else ctx.user + ) - if action_member_id: - action_member = await self.bot.get_member_from_str_id( - action_member_id, + if action_member != ctx.user and not await self.bot.check_user_has_committee_role( + ctx.user + ): + await ctx.respond( + content="Committee role is required to list actions for other users.", + ephemeral=True, ) - else: - action_member = ctx.user + logger.debug( + "User: %s, tried to list actions for user: %s, " + "but did not have the committee role.", + ctx.user, + action_member, + ) + return user_actions: list[AssignedCommitteeAction] From 383fe393dc4bb630ef478ce2f31a5a661bc01c8c Mon Sep 17 00:00:00 2001 From: MattyTheHacker <18513864+MattyTheHacker@users.noreply.github.com> Date: Fri, 4 Jul 2025 08:51:03 +0100 Subject: [PATCH 23/41] Merge main --- cogs/get_token_authorisation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/get_token_authorisation.py b/cogs/get_token_authorisation.py index a535eed7d..c7aa8f49b 100644 --- a/cogs/get_token_authorisation.py +++ b/cogs/get_token_authorisation.py @@ -16,7 +16,7 @@ ) if TYPE_CHECKING: - from collections.abc import Collection, Mapping, Sequence + from collections.abc import Iterable, Mapping, Sequence from logging import Logger from typing import Final From a2473f00717f4168aafbd110d5c11e2be2c0effd Mon Sep 17 00:00:00 2001 From: MattyTheHacker <18513864+MattyTheHacker@users.noreply.github.com> Date: Fri, 4 Jul 2025 18:06:53 +0100 Subject: [PATCH 24/41] Change names --- cogs/__init__.py | 8 +- cogs/get_token_authorisation.py | 258 -------------------------------- 2 files changed, 4 insertions(+), 262 deletions(-) delete mode 100644 cogs/get_token_authorisation.py diff --git a/cogs/__init__.py b/cogs/__init__.py index 36f8394e1..c40039ccb 100644 --- a/cogs/__init__.py +++ b/cogs/__init__.py @@ -22,8 +22,8 @@ from .delete_all import DeleteAllCommandsCog from .edit_message import EditMessageCommandCog from .everest import EverestCommandCog -from .get_token_authorisation import ( - GetTokenAuthorisationCommandCog, +from .check_su_platform_authorisation import ( + CheckSUPlatformAuthorisationCommandCog, TokenAuthorisationCheckTaskCog, ) from .induct import ( @@ -65,7 +65,7 @@ "EditMessageCommandCog", "EnsureMembersInductedCommandCog", "EverestCommandCog", - "GetTokenAuthorisationCommandCog", + "CheckSUPlatformAuthorisationCommandCog", "InductContextCommandsCog", "InductSendMessageCog", "InductSlashCommandCog", @@ -107,7 +107,7 @@ def setup(bot: "TeXBot") -> None: EditMessageCommandCog, EnsureMembersInductedCommandCog, EverestCommandCog, - GetTokenAuthorisationCommandCog, + CheckSUPlatformAuthorisationCommandCog, InductContextCommandsCog, InductSendMessageCog, InductSlashCommandCog, diff --git a/cogs/get_token_authorisation.py b/cogs/get_token_authorisation.py deleted file mode 100644 index ff73abd87..000000000 --- a/cogs/get_token_authorisation.py +++ /dev/null @@ -1,258 +0,0 @@ -"""Contains cog classes for token authorisation check interactions.""" - -import logging -from enum import Enum -from typing import TYPE_CHECKING, override - -import aiohttp -import bs4 -import discord -from discord.ext import tasks - -from config import settings -from utils import CommandChecks, TeXBotBaseCog -from utils.error_capture_decorators import ( - capture_guild_does_not_exist_error, -) - -if TYPE_CHECKING: - from collections.abc import Iterable, Mapping, Sequence - from logging import Logger - from typing import Final - - from utils import TeXBot, TeXBotApplicationContext - -__all__: "Sequence[str]" = ( - "GetTokenAuthorisationCommandCog", - "TokenAuthorisationCheckTaskCog", -) - -logger: "Final[Logger]" = logging.getLogger("TeX-Bot") - -REQUEST_HEADERS: "Final[Mapping[str, str]]" = { - "Cache-Control": "no-cache", - "Pragma": "no-cache", - "Expires": "0", -} - -REQUEST_COOKIES: "Final[Mapping[str, str]]" = { - ".ASPXAUTH": settings["MEMBERS_LIST_AUTH_SESSION_COOKIE"] -} - -PROFILE_URL: "Final[str]" = "https://guildofstudents.com/profile" -ORGANISATION_URL: "Final[str]" = "https://www.guildofstudents.com/organisation/admin" - - -class TokenAuthorisationBaseCog(TeXBotBaseCog): - """Cog class that defines the base for token authorisation functions.""" - - class TokenStatus(Enum): - """ - Enum class that defines the status of the token. - - INVALID: The token does not have access to a user, meaning it is invalid or expired. - VALID: The token is a valid user, but not neccessarily admin to an organisation. - AUTHORISED: The token is a valid user and has access to an organisation. - """ - - INVALID = "invalid" - VALID = "valid" - AUTHORISED = "authorised" - - async def fetch_with_session(self, url: str) -> str: - """Fetch a URL using a shared aiohttp session.""" - async with ( - aiohttp.ClientSession( - headers=REQUEST_HEADERS, - cookies=REQUEST_COOKIES, - ) as http_session, - http_session.get(url) as http_response, - ): - return await http_response.text() - - async def get_token_status(self) -> TokenStatus: - """ - Definition of method to get the status of the token. - - This is done by checking if the token is valid and if it is, - checking if the token has access to the organisation. - """ - response_object: bs4.BeautifulSoup = bs4.BeautifulSoup( - await self.fetch_with_session(PROFILE_URL), "html.parser" - ) - page_title: bs4.Tag | bs4.NavigableString | None = response_object.find("title") - if not page_title or "Login" in str(page_title): - logger.debug("Token is invalid or expired.") - return self.TokenStatus.INVALID - - organisation_admin_url: str = f"{ORGANISATION_URL}/{settings['ORGANISATION_ID']}" - response_html: str = await self.fetch_with_session(organisation_admin_url) - - if "admin tools" in response_html.lower(): - return self.TokenStatus.AUTHORISED - - if "You do not have any permissions for this organisation" in response_html.lower(): - return self.TokenStatus.VALID - - logger.warning("Unexpected response when checking token authorisation.") - return self.TokenStatus.INVALID - - async def get_token_groups(self) -> "Iterable[str]": - """ - Definition of method to get the groups the token has access to. - - This is done by requesting the user profile page and - scraping the HTML for the list of groups. - """ - response_object: bs4.BeautifulSoup = bs4.BeautifulSoup( - await self.fetch_with_session(PROFILE_URL), "html.parser" - ) - - page_title: bs4.Tag | bs4.NavigableString | None = response_object.find("title") - - if not page_title: - PROFILE_PAGE_INVALID: Final[str] = ( - "Profile page returned no content when checking token authorisation." - ) - logger.warning(PROFILE_PAGE_INVALID) - return [] - - if "Login" in str(page_title): - EXPIRED_AUTH_MESSAGE: Final[str] = ( - "Authentication redirected to login page. Token is invalid or expired." - ) - logger.warning(EXPIRED_AUTH_MESSAGE) - return [] - - profile_section_html: bs4.Tag | bs4.NavigableString | None = response_object.find( - "div", - {"id": "profile_main"}, - ) - - if profile_section_html is None: - NO_PROFILE_WARNING_MESSAGE: Final[str] = ( - "Couldn't find the profile section of the user" - "when scraping the website's HTML!" - ) - logger.warning(NO_PROFILE_WARNING_MESSAGE) - logger.debug("Retrieved HTML: %s", response_object.text) - return [] - - user_name: bs4.Tag | bs4.NavigableString | int | None = profile_section_html.find("h1") - - if not isinstance(user_name, bs4.Tag): - NO_PROFILE_DEBUG_MESSAGE: Final[str] = ( - "Found user profile but couldn't find their name." - ) - logger.warning(NO_PROFILE_DEBUG_MESSAGE) - logger.debug("Retrieved HTML: %s", response_object.text) - return [] - - parsed_html: bs4.Tag | bs4.NavigableString | None = response_object.find( - "ul", - {"id": "ulOrgs"}, - ) - - if parsed_html is None or isinstance(parsed_html, bs4.NavigableString): - NO_ADMIN_TABLE_MESSAGE: Final[str] = ( - f"Failed to retrieve the admin table for user: {user_name.string}." - "Please check you have used the correct token!" - ) - logger.warning(NO_ADMIN_TABLE_MESSAGE) - return [] - - organisations: Iterable[str] = [ - list_item.get_text(strip=True) for list_item in parsed_html.find_all("li") - ] - - logger.debug( - "Admin Token has admin access to: %s as user %s", - organisations, - user_name.text, - ) - - return organisations - - -class GetTokenAuthorisationCommandCog(TokenAuthorisationBaseCog): - """Cog class that defines the "/get_token_authorisation" command.""" - - @discord.slash_command( # type: ignore[no-untyped-call, misc] - name="get-token-authorisation", - description="Checks the authorisations held by the token.", - ) - @CommandChecks.check_interaction_user_has_committee_role - @CommandChecks.check_interaction_user_in_main_guild - async def get_token_authorisation(self, ctx: "TeXBotApplicationContext") -> None: # type: ignore[misc] - """ - Definition of the "get_token_authorisation" command. - - The "get_token_authorisation" command will retrieve the profile for the token user. - The profile page will contain the user's name and a list of the MSL organisations - the user has administrative access to. - """ - await ctx.defer(ephemeral=True) - async with ctx.typing(): - await ctx.followup.send( - content=( - f"Admin token has access to the following MSL Organisations: " - f"\n{ - ', \n'.join( - organisation for organisation in await self.get_token_groups() - ) - }" - ), - ephemeral=True, - ) - - -class TokenAuthorisationCheckTaskCog(TokenAuthorisationBaseCog): - """Cog class that defines the background task for token authorisation checks.""" - - @override - def __init__(self, bot: "TeXBot") -> None: - """Start all task managers when this cog is initialised.""" - if settings["AUTO_AUTH_SESSION_COOKIE_CHECKING"]: - _ = self.token_authorisation_check_task.start() - - super().__init__(bot) - - @override - def cog_unload(self) -> None: - """ - Unload-hook that ends all running tasks whenever the tasks cog is unloaded. - - This may be run dynamically or when the bot closes. - """ - self.token_authorisation_check_task.cancel() - - @tasks.loop(**settings["AUTO_AUTH_SESSION_COOKIE_CHECKING_INTERVAL"]) - @capture_guild_does_not_exist_error - async def token_authorisation_check_task(self) -> None: - """ - Definition of the background task that checks the token authorisation. - - The task will check if the token is valid and if it is, it will retrieve the - groups the token has access to. - """ - logger.debug("Running token authorisation check task...") - - token_status: TokenAuthorisationBaseCog.TokenStatus = await self.get_token_status() - - match token_status: - case self.TokenStatus.AUTHORISED: - logger.info("Token is valid and has access to the organisation.") - return - - case self.TokenStatus.VALID: - logger.warning("Token is valid but does not have access to the organisation.") - return - - case self.TokenStatus.INVALID: - logger.warning("Token is invalid or expired.") - return - - @token_authorisation_check_task.before_loop - async def before_tasks(self) -> None: - """Pre-execution hook, preventing any tasks from executing before the bot is ready.""" - await self.bot.wait_until_ready() From 11d401ad17810394b4c98eb247c91c1628c8788e Mon Sep 17 00:00:00 2001 From: MattyTheHacker <18513864+MattyTheHacker@users.noreply.github.com> Date: Fri, 4 Jul 2025 18:07:17 +0100 Subject: [PATCH 25/41] add file --- cogs/check_su_platform_authorisation.py | 258 ++++++++++++++++++++++++ 1 file changed, 258 insertions(+) create mode 100644 cogs/check_su_platform_authorisation.py diff --git a/cogs/check_su_platform_authorisation.py b/cogs/check_su_platform_authorisation.py new file mode 100644 index 000000000..9cfc77ac9 --- /dev/null +++ b/cogs/check_su_platform_authorisation.py @@ -0,0 +1,258 @@ +"""Contains cog classes for SU platform access cookie authorisation check interactions.""" + +import logging +from enum import Enum +from typing import TYPE_CHECKING, override + +import aiohttp +import bs4 +import discord +from discord.ext import tasks + +from config import settings +from utils import CommandChecks, TeXBotBaseCog +from utils.error_capture_decorators import ( + capture_guild_does_not_exist_error, +) + +if TYPE_CHECKING: + from collections.abc import Iterable, Mapping, Sequence + from logging import Logger + from typing import Final + + from utils import TeXBot, TeXBotApplicationContext + +__all__: "Sequence[str]" = ( + "CheckSUPlatformAuthorisationCommandCog", + "TokenAuthorisationCheckTaskCog", +) + +logger: "Final[Logger]" = logging.getLogger("TeX-Bot") + +REQUEST_HEADERS: "Final[Mapping[str, str]]" = { + "Cache-Control": "no-cache", + "Pragma": "no-cache", + "Expires": "0", +} + +REQUEST_COOKIES: "Final[Mapping[str, str]]" = { + ".ASPXAUTH": settings["SU_PLATFORM_ACCESS_COOKIE"] +} + +PROFILE_URL: "Final[str]" = "https://guildofstudents.com/profile" +ORGANISATION_URL: "Final[str]" = "https://www.guildofstudents.com/organisation/admin" + + +class CheckSUPlatformAuthorisationBaseCog(TeXBotBaseCog): + """Cog class that defines the base for token authorisation functions.""" + + class TokenStatus(Enum): + """ + Enum class that defines the status of the token. + + INVALID: The token does not have access to a user, meaning it is invalid or expired. + VALID: The token is a valid user, but not neccessarily admin to an organisation. + AUTHORISED: The token is a valid user and has access to an organisation. + """ + + INVALID = "invalid" + VALID = "valid" + AUTHORISED = "authorised" + + async def fetch_with_session(self, url: str) -> str: + """Fetch a URL using a shared aiohttp session.""" + async with ( + aiohttp.ClientSession( + headers=REQUEST_HEADERS, + cookies=REQUEST_COOKIES, + ) as http_session, + http_session.get(url) as http_response, + ): + return await http_response.text() + + async def get_token_status(self) -> TokenStatus: + """ + Definition of method to get the status of the token. + + This is done by checking if the token is valid and if it is, + checking if the token has access to the organisation. + """ + response_object: bs4.BeautifulSoup = bs4.BeautifulSoup( + await self.fetch_with_session(PROFILE_URL), "html.parser" + ) + page_title: bs4.Tag | bs4.NavigableString | None = response_object.find("title") + if not page_title or "Login" in str(page_title): + logger.debug("Token is invalid or expired.") + return self.TokenStatus.INVALID + + organisation_admin_url: str = f"{ORGANISATION_URL}/{settings['ORGANISATION_ID']}" + response_html: str = await self.fetch_with_session(organisation_admin_url) + + if "admin tools" in response_html.lower(): + return self.TokenStatus.AUTHORISED + + if "You do not have any permissions for this organisation" in response_html.lower(): + return self.TokenStatus.VALID + + logger.warning("Unexpected response when checking token authorisation.") + return self.TokenStatus.INVALID + + async def get_token_groups(self) -> "Iterable[str]": + """ + Definition of method to get the groups the token has access to. + + This is done by requesting the user profile page and + scraping the HTML for the list of groups. + """ + response_object: bs4.BeautifulSoup = bs4.BeautifulSoup( + await self.fetch_with_session(PROFILE_URL), "html.parser" + ) + + page_title: bs4.Tag | bs4.NavigableString | None = response_object.find("title") + + if not page_title: + PROFILE_PAGE_INVALID: Final[str] = ( + "Profile page returned no content when checking token authorisation." + ) + logger.warning(PROFILE_PAGE_INVALID) + return [] + + if "Login" in str(page_title): + EXPIRED_AUTH_MESSAGE: Final[str] = ( + "Authentication redirected to login page. Token is invalid or expired." + ) + logger.warning(EXPIRED_AUTH_MESSAGE) + return [] + + profile_section_html: bs4.Tag | bs4.NavigableString | None = response_object.find( + "div", + {"id": "profile_main"}, + ) + + if profile_section_html is None: + NO_PROFILE_WARNING_MESSAGE: Final[str] = ( + "Couldn't find the profile section of the user" + "when scraping the website's HTML!" + ) + logger.warning(NO_PROFILE_WARNING_MESSAGE) + logger.debug("Retrieved HTML: %s", response_object.text) + return [] + + user_name: bs4.Tag | bs4.NavigableString | int | None = profile_section_html.find("h1") + + if not isinstance(user_name, bs4.Tag): + NO_PROFILE_DEBUG_MESSAGE: Final[str] = ( + "Found user profile but couldn't find their name." + ) + logger.warning(NO_PROFILE_DEBUG_MESSAGE) + logger.debug("Retrieved HTML: %s", response_object.text) + return [] + + parsed_html: bs4.Tag | bs4.NavigableString | None = response_object.find( + "ul", + {"id": "ulOrgs"}, + ) + + if parsed_html is None or isinstance(parsed_html, bs4.NavigableString): + NO_ADMIN_TABLE_MESSAGE: Final[str] = ( + f"Failed to retrieve the admin table for user: {user_name.string}." + "Please check you have used the correct token!" + ) + logger.warning(NO_ADMIN_TABLE_MESSAGE) + return [] + + organisations: Iterable[str] = [ + list_item.get_text(strip=True) for list_item in parsed_html.find_all("li") + ] + + logger.debug( + "Admin Token has admin access to: %s as user %s", + organisations, + user_name.text, + ) + + return organisations + + +class CheckSUPlatformAuthorisationCommandCog(CheckSUPlatformAuthorisationBaseCog): + """Cog class that defines the "/check-su-platform-authorisation-cookie" command.""" + + @discord.slash_command( # type: ignore[no-untyped-call, misc] + name="get-token-authorisation", + description="Checks the authorisations held by the token.", + ) + @CommandChecks.check_interaction_user_has_committee_role + @CommandChecks.check_interaction_user_in_main_guild + async def get_token_authorisation(self, ctx: "TeXBotApplicationContext") -> None: # type: ignore[misc] + """ + Definition of the "get_token_authorisation" command. + + The "get_token_authorisation" command will retrieve the profile for the token user. + The profile page will contain the user's name and a list of the MSL organisations + the user has administrative access to. + """ + await ctx.defer(ephemeral=True) + async with ctx.typing(): + await ctx.followup.send( + content=( + f"Admin token has access to the following MSL Organisations: " + f"\n{ + ', \n'.join( + organisation for organisation in await self.get_token_groups() + ) + }" + ), + ephemeral=True, + ) + + +class TokenAuthorisationCheckTaskCog(CheckSUPlatformAuthorisationBaseCog): + """Cog class that defines the background task for token authorisation checks.""" + + @override + def __init__(self, bot: "TeXBot") -> None: + """Start all task managers when this cog is initialised.""" + if settings["AUTO_AUTH_SESSION_COOKIE_CHECKING"]: + _ = self.token_authorisation_check_task.start() + + super().__init__(bot) + + @override + def cog_unload(self) -> None: + """ + Unload-hook that ends all running tasks whenever the tasks cog is unloaded. + + This may be run dynamically or when the bot closes. + """ + self.token_authorisation_check_task.cancel() + + @tasks.loop(**settings["AUTO_AUTH_SESSION_COOKIE_CHECKING_INTERVAL"]) + @capture_guild_does_not_exist_error + async def token_authorisation_check_task(self) -> None: + """ + Definition of the background task that checks the token authorisation. + + The task will check if the token is valid and if it is, it will retrieve the + groups the token has access to. + """ + logger.debug("Running token authorisation check task...") + + token_status: CheckSUPlatformAuthorisationBaseCog.TokenStatus = await self.get_token_status() + + match token_status: + case self.TokenStatus.AUTHORISED: + logger.info("Token is valid and has access to the organisation.") + return + + case self.TokenStatus.VALID: + logger.warning("Token is valid but does not have access to the organisation.") + return + + case self.TokenStatus.INVALID: + logger.warning("Token is invalid or expired.") + return + + @token_authorisation_check_task.before_loop + async def before_tasks(self) -> None: + """Pre-execution hook, preventing any tasks from executing before the bot is ready.""" + await self.bot.wait_until_ready() From 55a5e947ee8b1f07cfb37fba45c60c25d12554a4 Mon Sep 17 00:00:00 2001 From: MattyTheHacker <18513864+MattyTheHacker@users.noreply.github.com> Date: Fri, 4 Jul 2025 18:12:14 +0100 Subject: [PATCH 26/41] more changes --- cogs/check_su_platform_authorisation.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cogs/check_su_platform_authorisation.py b/cogs/check_su_platform_authorisation.py index 9cfc77ac9..491b378ad 100644 --- a/cogs/check_su_platform_authorisation.py +++ b/cogs/check_su_platform_authorisation.py @@ -183,11 +183,11 @@ class CheckSUPlatformAuthorisationCommandCog(CheckSUPlatformAuthorisationBaseCog ) @CommandChecks.check_interaction_user_has_committee_role @CommandChecks.check_interaction_user_in_main_guild - async def get_token_authorisation(self, ctx: "TeXBotApplicationContext") -> None: # type: ignore[misc] + async def check_su_platform_authorisation(self, ctx: "TeXBotApplicationContext") -> None: # type: ignore[misc] """ - Definition of the "get_token_authorisation" command. + Definition of the "check_su_platform_authorisation" command. - The "get_token_authorisation" command will retrieve the profile for the token user. + The "check_su_platform_authorisation" command will retrieve the profile for the user. The profile page will contain the user's name and a list of the MSL organisations the user has administrative access to. """ From f3d62f0541b3ee371f7feb053b6b4e914cc915cb Mon Sep 17 00:00:00 2001 From: MattyTheHacker <18513864+MattyTheHacker@users.noreply.github.com> Date: Fri, 4 Jul 2025 18:21:09 +0100 Subject: [PATCH 27/41] more --- cogs/check_su_platform_authorisation.py | 30 ++++++++++++++++++------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/cogs/check_su_platform_authorisation.py b/cogs/check_su_platform_authorisation.py index 491b378ad..ee44e163f 100644 --- a/cogs/check_su_platform_authorisation.py +++ b/cogs/check_su_platform_authorisation.py @@ -55,9 +55,21 @@ class TokenStatus(Enum): AUTHORISED: The token is a valid user and has access to an organisation. """ - INVALID = "invalid" - VALID = "valid" - AUTHORISED = "authorised" + INVALID = ( + logging.WARNING, + "The auth session cookie is not associated with any MSL user, " + "meaning it is invalid or expired.", + ) + VALID = ( + logging.WARNING, + "The auth session cookie is associated with a valid MSL user, " + "but is not an admin to any MSL organisations.", + ) + AUTHORISED = ( + logging.INFO, + "The auth session cookie is associated with a valid MSL user and " + "has access to at least one MSL organisation.", + ) async def fetch_with_session(self, url: str) -> str: """Fetch a URL using a shared aiohttp session.""" @@ -178,8 +190,8 @@ class CheckSUPlatformAuthorisationCommandCog(CheckSUPlatformAuthorisationBaseCog """Cog class that defines the "/check-su-platform-authorisation-cookie" command.""" @discord.slash_command( # type: ignore[no-untyped-call, misc] - name="get-token-authorisation", - description="Checks the authorisations held by the token.", + name="check-su-platform-authorisation", + description="Checks the authorisations held by the SU access token.", ) @CommandChecks.check_interaction_user_has_committee_role @CommandChecks.check_interaction_user_in_main_guild @@ -195,9 +207,9 @@ async def check_su_platform_authorisation(self, ctx: "TeXBotApplicationContext") async with ctx.typing(): await ctx.followup.send( content=( - f"Admin token has access to the following MSL Organisations: " + f"SU Platform Access Cookie has access to the following MSL Organisations:" f"\n{ - ', \n'.join( + ',\n'.join( organisation for organisation in await self.get_token_groups() ) }" @@ -237,7 +249,9 @@ async def token_authorisation_check_task(self) -> None: """ logger.debug("Running token authorisation check task...") - token_status: CheckSUPlatformAuthorisationBaseCog.TokenStatus = await self.get_token_status() + token_status: CheckSUPlatformAuthorisationBaseCog.TokenStatus = ( + await self.get_token_status() + ) match token_status: case self.TokenStatus.AUTHORISED: From 56749c990101f5cf5d730d54c2d9b90d6be44b1f Mon Sep 17 00:00:00 2001 From: MattyTheHacker <18513864+MattyTheHacker@users.noreply.github.com> Date: Fri, 4 Jul 2025 18:45:52 +0100 Subject: [PATCH 28/41] More changes --- cogs/check_su_platform_authorisation.py | 34 +++++++++++-------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/cogs/check_su_platform_authorisation.py b/cogs/check_su_platform_authorisation.py index ee44e163f..6448cbf09 100644 --- a/cogs/check_su_platform_authorisation.py +++ b/cogs/check_su_platform_authorisation.py @@ -71,8 +71,8 @@ class TokenStatus(Enum): "has access to at least one MSL organisation.", ) - async def fetch_with_session(self, url: str) -> str: - """Fetch a URL using a shared aiohttp session.""" + async def _fetch_url_content_with_session(self, url: str) -> str: + """Fetch the HTTP content at the given URL, using a shared aiohttp session.""" async with ( aiohttp.ClientSession( headers=REQUEST_HEADERS, @@ -90,7 +90,7 @@ async def get_token_status(self) -> TokenStatus: checking if the token has access to the organisation. """ response_object: bs4.BeautifulSoup = bs4.BeautifulSoup( - await self.fetch_with_session(PROFILE_URL), "html.parser" + await self._fetch_url_content_with_session(PROFILE_URL), "html.parser" ) page_title: bs4.Tag | bs4.NavigableString | None = response_object.find("title") if not page_title or "Login" in str(page_title): @@ -98,7 +98,7 @@ async def get_token_status(self) -> TokenStatus: return self.TokenStatus.INVALID organisation_admin_url: str = f"{ORGANISATION_URL}/{settings['ORGANISATION_ID']}" - response_html: str = await self.fetch_with_session(organisation_admin_url) + response_html: str = await self._fetch_url_content_with_session(organisation_admin_url) if "admin tools" in response_html.lower(): return self.TokenStatus.AUTHORISED @@ -117,7 +117,7 @@ async def get_token_groups(self) -> "Iterable[str]": scraping the HTML for the list of groups. """ response_object: bs4.BeautifulSoup = bs4.BeautifulSoup( - await self.fetch_with_session(PROFILE_URL), "html.parser" + await self._fetch_url_content_with_session(PROFILE_URL), "html.parser" ) page_title: bs4.Tag | bs4.NavigableString | None = response_object.find("title") @@ -127,14 +127,14 @@ async def get_token_groups(self) -> "Iterable[str]": "Profile page returned no content when checking token authorisation." ) logger.warning(PROFILE_PAGE_INVALID) - return [] + return () if "Login" in str(page_title): EXPIRED_AUTH_MESSAGE: Final[str] = ( "Authentication redirected to login page. Token is invalid or expired." ) logger.warning(EXPIRED_AUTH_MESSAGE) - return [] + return () profile_section_html: bs4.Tag | bs4.NavigableString | None = response_object.find( "div", @@ -142,23 +142,19 @@ async def get_token_groups(self) -> "Iterable[str]": ) if profile_section_html is None: - NO_PROFILE_WARNING_MESSAGE: Final[str] = ( - "Couldn't find the profile section of the user" - "when scraping the website's HTML!" + logger.warning( + "Couldn't find the profile section of the user " + "when scraping the website's HTML." ) - logger.warning(NO_PROFILE_WARNING_MESSAGE) logger.debug("Retrieved HTML: %s", response_object.text) - return [] + return () user_name: bs4.Tag | bs4.NavigableString | int | None = profile_section_html.find("h1") if not isinstance(user_name, bs4.Tag): - NO_PROFILE_DEBUG_MESSAGE: Final[str] = ( - "Found user profile but couldn't find their name." - ) - logger.warning(NO_PROFILE_DEBUG_MESSAGE) + logger.warning("Found user profile but couldn't find their name.") logger.debug("Retrieved HTML: %s", response_object.text) - return [] + return () parsed_html: bs4.Tag | bs4.NavigableString | None = response_object.find( "ul", @@ -171,7 +167,7 @@ async def get_token_groups(self) -> "Iterable[str]": "Please check you have used the correct token!" ) logger.warning(NO_ADMIN_TABLE_MESSAGE) - return [] + return () organisations: Iterable[str] = [ list_item.get_text(strip=True) for list_item in parsed_html.find_all("li") @@ -219,7 +215,7 @@ async def check_su_platform_authorisation(self, ctx: "TeXBotApplicationContext") class TokenAuthorisationCheckTaskCog(CheckSUPlatformAuthorisationBaseCog): - """Cog class that defines the background task for token authorisation checks.""" + """Cog class that defines a repeated background task for checking SU Platform Access Cookie.""" # noqa: E501, W505 @override def __init__(self, bot: "TeXBot") -> None: From d945acceb991f843693e85f5a40b216a4ef90dd2 Mon Sep 17 00:00:00 2001 From: MattyTheHacker <18513864+MattyTheHacker@users.noreply.github.com> Date: Fri, 4 Jul 2025 18:55:30 +0100 Subject: [PATCH 29/41] Fix --- cogs/__init__.py | 14 +++++++------- cogs/check_su_platform_authorisation.py | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/cogs/__init__.py b/cogs/__init__.py index c40039ccb..beca94749 100644 --- a/cogs/__init__.py +++ b/cogs/__init__.py @@ -14,6 +14,10 @@ CommitteeHandoverCommandCog, ) from .archive import ArchiveCommandCog +from .check_su_platform_authorisation import ( + CheckSUPlatformAuthorisationCommandCog, + CheckSUPlatformAuthorisationTaskCog, +) from .command_error import CommandErrorCog from .committee_actions_tracking import ( CommitteeActionsTrackingContextCommandsCog, @@ -22,10 +26,6 @@ from .delete_all import DeleteAllCommandsCog from .edit_message import EditMessageCommandCog from .everest import EverestCommandCog -from .check_su_platform_authorisation import ( - CheckSUPlatformAuthorisationCommandCog, - TokenAuthorisationCheckTaskCog, -) from .induct import ( EnsureMembersInductedCommandCog, InductContextCommandsCog, @@ -56,6 +56,8 @@ "AnnualRolesResetCommandCog", "AnnualYearChannelsIncrementCommandCog", "ArchiveCommandCog", + "CheckSUPlatformAuthorisationCommandCog", + "CheckSUPlatformAuthorisationTaskCog", "ClearRemindersBacklogTaskCog", "CommandErrorCog", "CommitteeActionsTrackingContextCommandsCog", @@ -65,7 +67,6 @@ "EditMessageCommandCog", "EnsureMembersInductedCommandCog", "EverestCommandCog", - "CheckSUPlatformAuthorisationCommandCog", "InductContextCommandsCog", "InductSendMessageCog", "InductSlashCommandCog", @@ -85,7 +86,6 @@ "StatsCommandsCog", "StrikeCommandCog", "StrikeContextCommandsCog", - "TokenAuthorisationCheckTaskCog", "WriteRolesCommandCog", "setup", ) @@ -127,7 +127,7 @@ def setup(bot: "TeXBot") -> None: StatsCommandsCog, StrikeCommandCog, StrikeContextCommandsCog, - TokenAuthorisationCheckTaskCog, + CheckSUPlatformAuthorisationTaskCog, WriteRolesCommandCog, ) Cog: type[TeXBotBaseCog] diff --git a/cogs/check_su_platform_authorisation.py b/cogs/check_su_platform_authorisation.py index 6448cbf09..ca3d34e42 100644 --- a/cogs/check_su_platform_authorisation.py +++ b/cogs/check_su_platform_authorisation.py @@ -24,7 +24,7 @@ __all__: "Sequence[str]" = ( "CheckSUPlatformAuthorisationCommandCog", - "TokenAuthorisationCheckTaskCog", + "CheckSUPlatformAuthorisationTaskCog", ) logger: "Final[Logger]" = logging.getLogger("TeX-Bot") @@ -214,7 +214,7 @@ async def check_su_platform_authorisation(self, ctx: "TeXBotApplicationContext") ) -class TokenAuthorisationCheckTaskCog(CheckSUPlatformAuthorisationBaseCog): +class CheckSUPlatformAuthorisationTaskCog(CheckSUPlatformAuthorisationBaseCog): """Cog class that defines a repeated background task for checking SU Platform Access Cookie.""" # noqa: E501, W505 @override From 7db711576c82fbf15bfae87d44699730518a1d51 Mon Sep 17 00:00:00 2001 From: MattyTheHacker <18513864+MattyTheHacker@users.noreply.github.com> Date: Fri, 4 Jul 2025 19:18:03 +0100 Subject: [PATCH 30/41] More renaming --- config.py | 63 +++++++++++++++++++++++++++---------------------------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/config.py b/config.py index 93cb41311..3a91a7e84 100644 --- a/config.py +++ b/config.py @@ -464,83 +464,82 @@ def _setup_organisation_id(cls) -> None: cls._settings["ORGANISATION_ID"] = raw_organisation_id @classmethod - def _setup_members_list_auth_session_cookie(cls) -> None: - raw_members_list_auth_session_cookie: str = os.getenv( - "MEMBERS_LIST_URL_SESSION_COOKIE", default="" + def _setup_su_platform_access_cookie(cls) -> None: + raw_su_platform_access_cookie: str = os.getenv( + "SU_PLATFORM_ACCESS_COOKIE", + default="", ).strip() - if not raw_members_list_auth_session_cookie or not re.fullmatch( - r"\A[A-Fa-f\d]{128,256}\Z", raw_members_list_auth_session_cookie + if not raw_su_platform_access_cookie or not re.fullmatch( + r"\A[A-Fa-f\d]{128,256}\Z", raw_su_platform_access_cookie ): - INVALID_MEMBERS_LIST_AUTH_SESSION_COOKIE_MESSAGE: Final[str] = ( - "MEMBERS_LIST_URL_SESSION_COOKIE must be a valid .ASPXAUTH cookie." + INVALID_SU_PLATFORM_ACCESS_COOKIE_MESSAGE: Final[str] = ( + "SU_PLATFORM_ACCESS_COOKIE must be a valid .ASPXAUTH cookie." ) - raise ImproperlyConfiguredError(INVALID_MEMBERS_LIST_AUTH_SESSION_COOKIE_MESSAGE) + raise ImproperlyConfiguredError(INVALID_SU_PLATFORM_ACCESS_COOKIE_MESSAGE) - cls._settings["MEMBERS_LIST_AUTH_SESSION_COOKIE"] = ( - raw_members_list_auth_session_cookie - ) + cls._settings["SU_PLATFORM_ACCESS_COOKIE"] = raw_su_platform_access_cookie @classmethod - def _setup_auto_auth_session_cookie_checking(cls) -> None: + def _setup_auto_su_platform_access_cookie_checking(cls) -> None: raw_auto_auth_session_cookie_checking: str = str( - os.getenv("AUTO_AUTH_SESSION_COOKIE_CHECKING", "false"), + os.getenv("AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING", "false"), ) if raw_auto_auth_session_cookie_checking.lower() not in TRUE_VALUES | FALSE_VALUES: INVALID_AUTO_AUTH_CHECKING_MESSAGE: Final[str] = ( - "AUTO_AUTH_SESSION_COOKIE_CHECKING must be a boolean value." + "AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING must be a boolean value." ) raise ImproperlyConfiguredError(INVALID_AUTO_AUTH_CHECKING_MESSAGE) - cls._settings["AUTO_AUTH_SESSION_COOKIE_CHECKING"] = bool( + cls._settings["AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING"] = bool( raw_auto_auth_session_cookie_checking.lower() in TRUE_VALUES ) @classmethod - def _setup_auto_auth_session_cookie_checking_interval(cls) -> None: - if "AUTO_AUTH_SESSION_COOKIE_CHECKING" not in cls._settings: + def _setup_auto_su_platform_access_cookie_checking_interval(cls) -> None: + if "AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING" not in cls._settings: INVALID_SETUP_ORDER_MESSAGE: Final[str] = ( - "Invalid setup order: AUTO_AUTH_SESSION_COOKIE_CHECKING must be set up " - "before AUTO_AUTH_SESSION_COOKIE_CHECKING_INTERVAL can be set up." + "Invalid setup order: AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING must be set up " + "before AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING can be set up." ) raise RuntimeError(INVALID_SETUP_ORDER_MESSAGE) - if not cls._settings["AUTO_AUTH_SESSION_COOKIE_CHECKING"]: - cls._settings["AUTO_AUTH_SESSION_COOKIE_CHECKING_INTERVAL"] = { + if not cls._settings["AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING"]: + cls._settings["AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING_INTERVAL"] = { "hours": 24, } return - raw_auto_auth_session_cookie_checking_interval: re.Match[str] | None = re.fullmatch( + raw_auto_su_platform_access_cookie_checking_interval: re.Match[str] | None = re.fullmatch( r"\A(?:(?P(?:\d*\.)?\d+)s)?(?:(?P(?:\d*\.)?\d+)m)?(?:(?P(?:\d*\.)?\d+)h)?(?:(?P(?:\d*\.)?\d+)d)?(?:(?P(?:\d*\.)?\d+)w)?\Z", - str(os.getenv("AUTO_AUTH_SESSION_COOKIE_CHECKING_INTERVAL", "24h")), + str(os.getenv("AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING_INTERVAL", "24h")), ) - if not raw_auto_auth_session_cookie_checking_interval: + if not raw_auto_su_platform_access_cookie_checking_interval: INVALID_AUTO_AUTH_CHECKING_DELAY_MESSAGE: Final[str] = ( - "AUTO_AUTH_SESSION_COOKIE_CHECKING_INTERVAL must contain the delay " + "AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING_INTERVAL must contain the delay " "in any combination of seconds, minutes, hours, days or weeks." ) - logger.debug(raw_auto_auth_session_cookie_checking_interval) + logger.debug(raw_auto_su_platform_access_cookie_checking_interval) raise ImproperlyConfiguredError( INVALID_AUTO_AUTH_CHECKING_DELAY_MESSAGE, ) - raw_timedelta_auto_auth_session_cookie_checking_interval: Mapping[str, float] = { + raw_timedelta_auto_su_platform_access_cookie_checking_interval: Mapping[str, float] = { "hours": 24, } - raw_timedelta_auto_auth_session_cookie_checking_interval = { + raw_timedelta_auto_su_platform_access_cookie_checking_interval = { key: float(value) for key, value in ( - raw_auto_auth_session_cookie_checking_interval.groupdict().items() + raw_auto_su_platform_access_cookie_checking_interval.groupdict().items() ) if value } - cls._settings["AUTO_AUTH_SESSION_COOKIE_CHECKING_INTERVAL"] = ( - raw_timedelta_auto_auth_session_cookie_checking_interval + cls._settings["AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING_INTERVAL"] = ( + raw_timedelta_auto_su_platform_access_cookie_checking_interval ) @classmethod @@ -900,7 +899,7 @@ def _setup_env_variables(cls) -> None: cls._setup_roles_messages() cls._setup_organisation_id() cls._setup_members_list_auth_session_cookie() - cls._setup_auto_auth_session_cookie_checking() + cls._setup_auto_su_platform_access_cookie_checking() cls._setup_auto_auth_session_cookie_checking_interval() cls._setup_membership_perks_url() cls._setup_purchase_membership_url() From ab350429e59ebc94d96d3ea862535c4605c1cf10 Mon Sep 17 00:00:00 2001 From: MattyTheHacker <18513864+MattyTheHacker@users.noreply.github.com> Date: Fri, 4 Jul 2025 19:20:58 +0100 Subject: [PATCH 31/41] Fix method call --- config.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config.py b/config.py index 3a91a7e84..2f7c1e72b 100644 --- a/config.py +++ b/config.py @@ -517,13 +517,13 @@ def _setup_auto_su_platform_access_cookie_checking_interval(cls) -> None: ) if not raw_auto_su_platform_access_cookie_checking_interval: - INVALID_AUTO_AUTH_CHECKING_DELAY_MESSAGE: Final[str] = ( + INVALID_AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING_DELAY_MESSAGE: Final[str] = ( "AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING_INTERVAL must contain the delay " "in any combination of seconds, minutes, hours, days or weeks." ) logger.debug(raw_auto_su_platform_access_cookie_checking_interval) raise ImproperlyConfiguredError( - INVALID_AUTO_AUTH_CHECKING_DELAY_MESSAGE, + INVALID_AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING_DELAY_MESSAGE, ) raw_timedelta_auto_su_platform_access_cookie_checking_interval: Mapping[str, float] = { @@ -898,9 +898,9 @@ def _setup_env_variables(cls) -> None: cls._setup_welcome_messages() cls._setup_roles_messages() cls._setup_organisation_id() - cls._setup_members_list_auth_session_cookie() + cls._setup_su_platform_access_cookie() cls._setup_auto_su_platform_access_cookie_checking() - cls._setup_auto_auth_session_cookie_checking_interval() + cls._setup_auto_su_platform_access_cookie_checking_interval() cls._setup_membership_perks_url() cls._setup_purchase_membership_url() cls._setup_custom_discord_invite_url() From bd93d4061f357aa47c26221dc1e13a37eb73dc14 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Fri, 4 Jul 2025 18:46:22 +0000 Subject: [PATCH 32/41] [pre-commit.ci lite] apply automatic fixes --- config.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/config.py b/config.py index 2f7c1e72b..6838232f9 100644 --- a/config.py +++ b/config.py @@ -511,9 +511,11 @@ def _setup_auto_su_platform_access_cookie_checking_interval(cls) -> None: } return - raw_auto_su_platform_access_cookie_checking_interval: re.Match[str] | None = re.fullmatch( - r"\A(?:(?P(?:\d*\.)?\d+)s)?(?:(?P(?:\d*\.)?\d+)m)?(?:(?P(?:\d*\.)?\d+)h)?(?:(?P(?:\d*\.)?\d+)d)?(?:(?P(?:\d*\.)?\d+)w)?\Z", - str(os.getenv("AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING_INTERVAL", "24h")), + raw_auto_su_platform_access_cookie_checking_interval: re.Match[str] | None = ( + re.fullmatch( + r"\A(?:(?P(?:\d*\.)?\d+)s)?(?:(?P(?:\d*\.)?\d+)m)?(?:(?P(?:\d*\.)?\d+)h)?(?:(?P(?:\d*\.)?\d+)d)?(?:(?P(?:\d*\.)?\d+)w)?\Z", + str(os.getenv("AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING_INTERVAL", "24h")), + ) ) if not raw_auto_su_platform_access_cookie_checking_interval: From 7b56cf72254d4234b7efd97f3f83a456fe9abfcf Mon Sep 17 00:00:00 2001 From: MattyTheHacker <18513864+MattyTheHacker@users.noreply.github.com> Date: Sat, 5 Jul 2025 18:48:36 +0100 Subject: [PATCH 33/41] Fix some stuff --- cogs/check_su_platform_authorisation.py | 119 ++++++++++-------------- 1 file changed, 48 insertions(+), 71 deletions(-) diff --git a/cogs/check_su_platform_authorisation.py b/cogs/check_su_platform_authorisation.py index ca3d34e42..3da907921 100644 --- a/cogs/check_su_platform_authorisation.py +++ b/cogs/check_su_platform_authorisation.py @@ -43,33 +43,28 @@ ORGANISATION_URL: "Final[str]" = "https://www.guildofstudents.com/organisation/admin" -class CheckSUPlatformAuthorisationBaseCog(TeXBotBaseCog): - """Cog class that defines the base for token authorisation functions.""" +class SUPlatformAccessCookieStatus(Enum): + """Enum class defining the status of the SU Platform Access Cookie.""" - class TokenStatus(Enum): - """ - Enum class that defines the status of the token. + INVALID = ( + logging.WARNING, + "The auth session cookie is not associated with any MSL user, " + "meaning it is invalid or expired.", + ) + VALID = ( + logging.WARNING, + "The auth session cookie is associated with a valid MSL user, " + "but is not an admin to any MSL organisations.", + ) + AUTHORISED = ( + logging.INFO, + "The auth session cookie is associated with a valid MSL user and " + "has access to at least one MSL organisation.", + ) - INVALID: The token does not have access to a user, meaning it is invalid or expired. - VALID: The token is a valid user, but not neccessarily admin to an organisation. - AUTHORISED: The token is a valid user and has access to an organisation. - """ - INVALID = ( - logging.WARNING, - "The auth session cookie is not associated with any MSL user, " - "meaning it is invalid or expired.", - ) - VALID = ( - logging.WARNING, - "The auth session cookie is associated with a valid MSL user, " - "but is not an admin to any MSL organisations.", - ) - AUTHORISED = ( - logging.INFO, - "The auth session cookie is associated with a valid MSL user and " - "has access to at least one MSL organisation.", - ) +class CheckSUPlatformAuthorisationBaseCog(TeXBotBaseCog): + """Cog class that defines the base for token authorisation functions.""" async def _fetch_url_content_with_session(self, url: str) -> str: """Fetch the HTTP content at the given URL, using a shared aiohttp session.""" @@ -82,40 +77,30 @@ async def _fetch_url_content_with_session(self, url: str) -> str: ): return await http_response.text() - async def get_token_status(self) -> TokenStatus: - """ - Definition of method to get the status of the token. - - This is done by checking if the token is valid and if it is, - checking if the token has access to the organisation. - """ + async def get_su_platform_access_cookie_status(self) -> SUPlatformAccessCookieStatus: + """Retrieve the current validity status of the members list auth session cookie.""" response_object: bs4.BeautifulSoup = bs4.BeautifulSoup( await self._fetch_url_content_with_session(PROFILE_URL), "html.parser" ) page_title: bs4.Tag | bs4.NavigableString | None = response_object.find("title") if not page_title or "Login" in str(page_title): logger.debug("Token is invalid or expired.") - return self.TokenStatus.INVALID + return SUPlatformAccessCookieStatus.INVALID organisation_admin_url: str = f"{ORGANISATION_URL}/{settings['ORGANISATION_ID']}" response_html: str = await self._fetch_url_content_with_session(organisation_admin_url) if "admin tools" in response_html.lower(): - return self.TokenStatus.AUTHORISED + return SUPlatformAccessCookieStatus.AUTHORISED if "You do not have any permissions for this organisation" in response_html.lower(): - return self.TokenStatus.VALID + return SUPlatformAccessCookieStatus.VALID logger.warning("Unexpected response when checking token authorisation.") - return self.TokenStatus.INVALID - - async def get_token_groups(self) -> "Iterable[str]": - """ - Definition of method to get the groups the token has access to. + return SUPlatformAccessCookieStatus.INVALID - This is done by requesting the user profile page and - scraping the HTML for the list of groups. - """ + async def get_su_platform_organisations(self) -> "Iterable[str]": + """Retrieve the set of MSL organisations the current members list auth session cookie has access to.""" # noqa: E501, W505 response_object: bs4.BeautifulSoup = bs4.BeautifulSoup( await self._fetch_url_content_with_session(PROFILE_URL), "html.parser" ) @@ -123,17 +108,15 @@ async def get_token_groups(self) -> "Iterable[str]": page_title: bs4.Tag | bs4.NavigableString | None = response_object.find("title") if not page_title: - PROFILE_PAGE_INVALID: Final[str] = ( + logger.warning( "Profile page returned no content when checking token authorisation." ) - logger.warning(PROFILE_PAGE_INVALID) return () if "Login" in str(page_title): - EXPIRED_AUTH_MESSAGE: Final[str] = ( + logger.warning( "Authentication redirected to login page. Token is invalid or expired." ) - logger.warning(EXPIRED_AUTH_MESSAGE) return () profile_section_html: bs4.Tag | bs4.NavigableString | None = response_object.find( @@ -200,13 +183,15 @@ async def check_su_platform_authorisation(self, ctx: "TeXBotApplicationContext") the user has administrative access to. """ await ctx.defer(ephemeral=True) + async with ctx.typing(): await ctx.followup.send( content=( f"SU Platform Access Cookie has access to the following MSL Organisations:" f"\n{ ',\n'.join( - organisation for organisation in await self.get_token_groups() + organisation + for organisation in (await self.get_su_platform_organisations()) ) }" ), @@ -221,7 +206,7 @@ class CheckSUPlatformAuthorisationTaskCog(CheckSUPlatformAuthorisationBaseCog): def __init__(self, bot: "TeXBot") -> None: """Start all task managers when this cog is initialised.""" if settings["AUTO_AUTH_SESSION_COOKIE_CHECKING"]: - _ = self.token_authorisation_check_task.start() + _ = self.su_platform_access_cookie_check_task.start() super().__init__(bot) @@ -232,37 +217,29 @@ def cog_unload(self) -> None: This may be run dynamically or when the bot closes. """ - self.token_authorisation_check_task.cancel() + self.su_platform_access_cookie_check_task.cancel() - @tasks.loop(**settings["AUTO_AUTH_SESSION_COOKIE_CHECKING_INTERVAL"]) + @tasks.loop(**settings["AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING_INTERVAL"]) @capture_guild_does_not_exist_error - async def token_authorisation_check_task(self) -> None: + async def su_platform_access_cookie_check_task(self) -> None: """ - Definition of the background task that checks the token authorisation. + Definition of the repeated background task that checks the SU Platform Access Cookie. - The task will check if the token is valid and if it is, it will retrieve the - groups the token has access to. + The task will check if the cookie is valid and if it is, it will retrieve the + MSL organisations the token has access to. """ - logger.debug("Running token authorisation check task...") - - token_status: CheckSUPlatformAuthorisationBaseCog.TokenStatus = ( - await self.get_token_status() - ) - - match token_status: - case self.TokenStatus.AUTHORISED: - logger.info("Token is valid and has access to the organisation.") - return + logger.debug("Running SU Platform Access Cookie check task...") - case self.TokenStatus.VALID: - logger.warning("Token is valid but does not have access to the organisation.") - return + su_platform_access_cookie_status: tuple[int, str] = ( + await self.get_su_platform_access_cookie_status() + ).value - case self.TokenStatus.INVALID: - logger.warning("Token is invalid or expired.") - return + logger.log( + level=su_platform_access_cookie_status[0], + msg=su_platform_access_cookie_status[1], + ) - @token_authorisation_check_task.before_loop + @su_platform_access_cookie_check_task.before_loop async def before_tasks(self) -> None: """Pre-execution hook, preventing any tasks from executing before the bot is ready.""" await self.bot.wait_until_ready() From 9a26cbf5f786a5468c1de8778781d21d2d5f9255 Mon Sep 17 00:00:00 2001 From: MattyTheHacker <18513864+MattyTheHacker@users.noreply.github.com> Date: Sat, 5 Jul 2025 18:50:13 +0100 Subject: [PATCH 34/41] minor fixes --- cogs/check_su_platform_authorisation.py | 2 +- config.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cogs/check_su_platform_authorisation.py b/cogs/check_su_platform_authorisation.py index 3da907921..2b5c75143 100644 --- a/cogs/check_su_platform_authorisation.py +++ b/cogs/check_su_platform_authorisation.py @@ -205,7 +205,7 @@ class CheckSUPlatformAuthorisationTaskCog(CheckSUPlatformAuthorisationBaseCog): @override def __init__(self, bot: "TeXBot") -> None: """Start all task managers when this cog is initialised.""" - if settings["AUTO_AUTH_SESSION_COOKIE_CHECKING"]: + if settings["AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING"]: _ = self.su_platform_access_cookie_check_task.start() super().__init__(bot) diff --git a/config.py b/config.py index 6838232f9..b426547e9 100644 --- a/config.py +++ b/config.py @@ -501,7 +501,7 @@ def _setup_auto_su_platform_access_cookie_checking_interval(cls) -> None: if "AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING" not in cls._settings: INVALID_SETUP_ORDER_MESSAGE: Final[str] = ( "Invalid setup order: AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING must be set up " - "before AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING can be set up." + "before AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING_INTERVAL can be set up." ) raise RuntimeError(INVALID_SETUP_ORDER_MESSAGE) From a0ece955430a7e09ddbda1378c742bd5ebd90b62 Mon Sep 17 00:00:00 2001 From: Matty Widdop <18513864+MattyTheHacker@users.noreply.github.com> Date: Sat, 5 Jul 2025 20:46:31 +0100 Subject: [PATCH 35/41] Apply suggestions from code review Co-authored-by: Matt Norton Signed-off-by: Matty Widdop <18513864+MattyTheHacker@users.noreply.github.com> --- cogs/check_su_platform_authorisation.py | 61 +++++++++++++------------ config.py | 44 +++++++++++------- 2 files changed, 60 insertions(+), 45 deletions(-) diff --git a/cogs/check_su_platform_authorisation.py b/cogs/check_su_platform_authorisation.py index 2b5c75143..36e035d1e 100644 --- a/cogs/check_su_platform_authorisation.py +++ b/cogs/check_su_platform_authorisation.py @@ -39,8 +39,8 @@ ".ASPXAUTH": settings["SU_PLATFORM_ACCESS_COOKIE"] } -PROFILE_URL: "Final[str]" = "https://guildofstudents.com/profile" -ORGANISATION_URL: "Final[str]" = "https://www.guildofstudents.com/organisation/admin" +SU_PLATFORM_PROFILE_URL: "Final[str]" = "https://guildofstudents.com/profile" +SU_PLATFORM_ORGANISATION_URL: "Final[str]" = "https://www.guildofstudents.com/organisation/admin" class SUPlatformAccessCookieStatus(Enum): @@ -48,30 +48,35 @@ class SUPlatformAccessCookieStatus(Enum): INVALID = ( logging.WARNING, - "The auth session cookie is not associated with any MSL user, " - "meaning it is invalid or expired.", + ( + "The auth session cookie is not associated with any MSL user, " + "meaning it is invalid or expired." + ), ) VALID = ( logging.WARNING, - "The auth session cookie is associated with a valid MSL user, " - "but is not an admin to any MSL organisations.", + ( + "The auth session cookie is associated with a valid MSL user, " + "but is not an admin to any MSL organisations." + ), ) AUTHORISED = ( logging.INFO, - "The auth session cookie is associated with a valid MSL user and " - "has access to at least one MSL organisation.", + ( + "The auth session cookie is associated with a valid MSL user and " + "has access to at least one MSL organisation." + ), ) class CheckSUPlatformAuthorisationBaseCog(TeXBotBaseCog): - """Cog class that defines the base for token authorisation functions.""" + """Cog class that defines the base functionality for cookie authorisation checks.""" async def _fetch_url_content_with_session(self, url: str) -> str: """Fetch the HTTP content at the given URL, using a shared aiohttp session.""" async with ( aiohttp.ClientSession( - headers=REQUEST_HEADERS, - cookies=REQUEST_COOKIES, + headers=REQUEST_HEADERS, cookies=REQUEST_COOKIES ) as http_session, http_session.get(url) as http_response, ): @@ -96,11 +101,11 @@ async def get_su_platform_access_cookie_status(self) -> SUPlatformAccessCookieSt if "You do not have any permissions for this organisation" in response_html.lower(): return SUPlatformAccessCookieStatus.VALID - logger.warning("Unexpected response when checking token authorisation.") + logger.warning("Unexpected response when checking SU platform access cookie authorisation.") return SUPlatformAccessCookieStatus.INVALID async def get_su_platform_organisations(self) -> "Iterable[str]": - """Retrieve the set of MSL organisations the current members list auth session cookie has access to.""" # noqa: E501, W505 + """Retrieve the set of MSL organisations the current SU platform session cookie has access to.""" # noqa: E501, W505 response_object: bs4.BeautifulSoup = bs4.BeautifulSoup( await self._fetch_url_content_with_session(PROFILE_URL), "html.parser" ) @@ -109,25 +114,24 @@ async def get_su_platform_organisations(self) -> "Iterable[str]": if not page_title: logger.warning( - "Profile page returned no content when checking token authorisation." + "Profile page returned no content when checking SU platform access cookie's authorisation." ) return () if "Login" in str(page_title): logger.warning( - "Authentication redirected to login page. Token is invalid or expired." + "Authentication redirected to login page. SU platform access cookie is invalid or expired." ) return () profile_section_html: bs4.Tag | bs4.NavigableString | None = response_object.find( - "div", - {"id": "profile_main"}, + "div", {"id": "profile_main"} ) if profile_section_html is None: logger.warning( "Couldn't find the profile section of the user " - "when scraping the website's HTML." + "when scraping the SU platform's website HTML." ) logger.debug("Retrieved HTML: %s", response_object.text) return () @@ -135,19 +139,18 @@ async def get_su_platform_organisations(self) -> "Iterable[str]": user_name: bs4.Tag | bs4.NavigableString | int | None = profile_section_html.find("h1") if not isinstance(user_name, bs4.Tag): - logger.warning("Found user profile but couldn't find their name.") + logger.warning("Found user profile on the SU platform but couldn't find their name.") logger.debug("Retrieved HTML: %s", response_object.text) return () parsed_html: bs4.Tag | bs4.NavigableString | None = response_object.find( - "ul", - {"id": "ulOrgs"}, + "ul", {"id": "ulOrgs"} ) if parsed_html is None or isinstance(parsed_html, bs4.NavigableString): NO_ADMIN_TABLE_MESSAGE: Final[str] = ( f"Failed to retrieve the admin table for user: {user_name.string}." - "Please check you have used the correct token!" + "Please check you have used the correct SU platform access token!" ) logger.warning(NO_ADMIN_TABLE_MESSAGE) return () @@ -157,7 +160,7 @@ async def get_su_platform_organisations(self) -> "Iterable[str]": ] logger.debug( - "Admin Token has admin access to: %s as user %s", + "SU platform access cookie has admin authorisation to: %s as user %s", organisations, user_name.text, ) @@ -166,11 +169,11 @@ async def get_su_platform_organisations(self) -> "Iterable[str]": class CheckSUPlatformAuthorisationCommandCog(CheckSUPlatformAuthorisationBaseCog): - """Cog class that defines the "/check-su-platform-authorisation-cookie" command.""" + """Cog class that defines the "/check-su-platform-authorisation" command.""" @discord.slash_command( # type: ignore[no-untyped-call, misc] name="check-su-platform-authorisation", - description="Checks the authorisations held by the SU access token.", + description="Checks the authorisation held by the SU platform access cookie.", ) @CommandChecks.check_interaction_user_has_committee_role @CommandChecks.check_interaction_user_in_main_guild @@ -200,7 +203,7 @@ async def check_su_platform_authorisation(self, ctx: "TeXBotApplicationContext") class CheckSUPlatformAuthorisationTaskCog(CheckSUPlatformAuthorisationBaseCog): - """Cog class that defines a repeated background task for checking SU Platform Access Cookie.""" # noqa: E501, W505 + """Cog class defining a repeated task for checking SU platform access cookie.""" @override def __init__(self, bot: "TeXBot") -> None: @@ -223,12 +226,12 @@ def cog_unload(self) -> None: @capture_guild_does_not_exist_error async def su_platform_access_cookie_check_task(self) -> None: """ - Definition of the repeated background task that checks the SU Platform Access Cookie. + Definition of the repeated background task that checks the SU platform access cookie. The task will check if the cookie is valid and if it is, it will retrieve the - MSL organisations the token has access to. + MSL organisations the cookie has access to. """ - logger.debug("Running SU Platform Access Cookie check task...") + logger.debug("Running SU platform access cookie check task...") su_platform_access_cookie_status: tuple[int, str] = ( await self.get_su_platform_access_cookie_status() diff --git a/config.py b/config.py index b426547e9..46c445ef3 100644 --- a/config.py +++ b/config.py @@ -482,18 +482,18 @@ def _setup_su_platform_access_cookie(cls) -> None: @classmethod def _setup_auto_su_platform_access_cookie_checking(cls) -> None: - raw_auto_auth_session_cookie_checking: str = str( - os.getenv("AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING", "false"), - ) + raw_auto_auth_session_cookie_checking: str = os.getenv( + "AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING", "False" + ).lower().strip() - if raw_auto_auth_session_cookie_checking.lower() not in TRUE_VALUES | FALSE_VALUES: + if raw_auto_auth_session_cookie_checking not in TRUE_VALUES | FALSE_VALUES: INVALID_AUTO_AUTH_CHECKING_MESSAGE: Final[str] = ( "AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING must be a boolean value." ) raise ImproperlyConfiguredError(INVALID_AUTO_AUTH_CHECKING_MESSAGE) - cls._settings["AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING"] = bool( - raw_auto_auth_session_cookie_checking.lower() in TRUE_VALUES + cls._settings["AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING"] = ( + raw_auto_auth_session_cookie_checking in TRUE_VALUES ) @classmethod @@ -507,39 +507,51 @@ def _setup_auto_su_platform_access_cookie_checking_interval(cls) -> None: if not cls._settings["AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING"]: cls._settings["AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING_INTERVAL"] = { - "hours": 24, + "hours": 24 } return raw_auto_su_platform_access_cookie_checking_interval: re.Match[str] | None = ( re.fullmatch( r"\A(?:(?P(?:\d*\.)?\d+)s)?(?:(?P(?:\d*\.)?\d+)m)?(?:(?P(?:\d*\.)?\d+)h)?(?:(?P(?:\d*\.)?\d+)d)?(?:(?P(?:\d*\.)?\d+)w)?\Z", - str(os.getenv("AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING_INTERVAL", "24h")), + os.getenv("AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING_INTERVAL", "24h") + .strip() + .lower() + .replace(" ", ""), ) ) if not raw_auto_su_platform_access_cookie_checking_interval: - INVALID_AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING_DELAY_MESSAGE: Final[str] = ( + INVALID_AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING_INTERVAL_MESSAGE: Final[str] = ( "AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING_INTERVAL must contain the delay " "in any combination of seconds, minutes, hours, days or weeks." ) logger.debug(raw_auto_su_platform_access_cookie_checking_interval) raise ImproperlyConfiguredError( - INVALID_AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING_DELAY_MESSAGE, + INVALID_AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING_INTERVAL_MESSAGE ) raw_timedelta_auto_su_platform_access_cookie_checking_interval: Mapping[str, float] = { - "hours": 24, - } - - raw_timedelta_auto_su_platform_access_cookie_checking_interval = { - key: float(value) + key: float(stripped_value) for key, value in ( raw_auto_su_platform_access_cookie_checking_interval.groupdict().items() ) - if value + if stripped_value := value.strip() } + if ( + timedelta( + **raw_timedelta_auto_su_platform_access_cookie_checking_interval + ).total_seconds() + <= 3 + ): + TOO_SMALL_AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING_INTERVAL_MESSAGE: Final[str] = ( + "AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING_INTERVAL must be longer than 3 seconds." + ) + raise ImproperlyConfiguredError( + TOO_SMALL_AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING_INTERVAL_MESSAGE, + ) + cls._settings["AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING_INTERVAL"] = ( raw_timedelta_auto_su_platform_access_cookie_checking_interval ) From 255eae721e14717442e24146d795d28cdad0aa6c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Sat, 5 Jul 2025 19:47:12 +0000 Subject: [PATCH 36/41] [pre-commit.ci lite] apply automatic fixes --- cogs/check_su_platform_authorisation.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/cogs/check_su_platform_authorisation.py b/cogs/check_su_platform_authorisation.py index 36e035d1e..55a0f4dc2 100644 --- a/cogs/check_su_platform_authorisation.py +++ b/cogs/check_su_platform_authorisation.py @@ -40,7 +40,9 @@ } SU_PLATFORM_PROFILE_URL: "Final[str]" = "https://guildofstudents.com/profile" -SU_PLATFORM_ORGANISATION_URL: "Final[str]" = "https://www.guildofstudents.com/organisation/admin" +SU_PLATFORM_ORGANISATION_URL: "Final[str]" = ( + "https://www.guildofstudents.com/organisation/admin" +) class SUPlatformAccessCookieStatus(Enum): @@ -101,7 +103,9 @@ async def get_su_platform_access_cookie_status(self) -> SUPlatformAccessCookieSt if "You do not have any permissions for this organisation" in response_html.lower(): return SUPlatformAccessCookieStatus.VALID - logger.warning("Unexpected response when checking SU platform access cookie authorisation.") + logger.warning( + "Unexpected response when checking SU platform access cookie authorisation." + ) return SUPlatformAccessCookieStatus.INVALID async def get_su_platform_organisations(self) -> "Iterable[str]": @@ -139,7 +143,9 @@ async def get_su_platform_organisations(self) -> "Iterable[str]": user_name: bs4.Tag | bs4.NavigableString | int | None = profile_section_html.find("h1") if not isinstance(user_name, bs4.Tag): - logger.warning("Found user profile on the SU platform but couldn't find their name.") + logger.warning( + "Found user profile on the SU platform but couldn't find their name." + ) logger.debug("Retrieved HTML: %s", response_object.text) return () From 699642c0b9f93f8f20ff1b7567296b81d5cbe163 Mon Sep 17 00:00:00 2001 From: MattyTheHacker <18513864+MattyTheHacker@users.noreply.github.com> Date: Sat, 5 Jul 2025 20:52:23 +0100 Subject: [PATCH 37/41] Fix shit --- cogs/check_su_platform_authorisation.py | 14 +++++---- config.py | 39 ++++++++++++------------- 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/cogs/check_su_platform_authorisation.py b/cogs/check_su_platform_authorisation.py index 55a0f4dc2..bbd40d243 100644 --- a/cogs/check_su_platform_authorisation.py +++ b/cogs/check_su_platform_authorisation.py @@ -87,14 +87,16 @@ async def _fetch_url_content_with_session(self, url: str) -> str: async def get_su_platform_access_cookie_status(self) -> SUPlatformAccessCookieStatus: """Retrieve the current validity status of the members list auth session cookie.""" response_object: bs4.BeautifulSoup = bs4.BeautifulSoup( - await self._fetch_url_content_with_session(PROFILE_URL), "html.parser" + await self._fetch_url_content_with_session(SU_PLATFORM_PROFILE_URL), "html.parser" ) page_title: bs4.Tag | bs4.NavigableString | None = response_object.find("title") if not page_title or "Login" in str(page_title): logger.debug("Token is invalid or expired.") return SUPlatformAccessCookieStatus.INVALID - organisation_admin_url: str = f"{ORGANISATION_URL}/{settings['ORGANISATION_ID']}" + organisation_admin_url: str = ( + f"{SU_PLATFORM_ORGANISATION_URL}/{settings['ORGANISATION_ID']}" + ) response_html: str = await self._fetch_url_content_with_session(organisation_admin_url) if "admin tools" in response_html.lower(): @@ -111,20 +113,22 @@ async def get_su_platform_access_cookie_status(self) -> SUPlatformAccessCookieSt async def get_su_platform_organisations(self) -> "Iterable[str]": """Retrieve the set of MSL organisations the current SU platform session cookie has access to.""" # noqa: E501, W505 response_object: bs4.BeautifulSoup = bs4.BeautifulSoup( - await self._fetch_url_content_with_session(PROFILE_URL), "html.parser" + await self._fetch_url_content_with_session(SU_PLATFORM_PROFILE_URL), "html.parser" ) page_title: bs4.Tag | bs4.NavigableString | None = response_object.find("title") if not page_title: logger.warning( - "Profile page returned no content when checking SU platform access cookie's authorisation." + "Profile page returned no content when checking " + "SU platform access cookie's authorisation." ) return () if "Login" in str(page_title): logger.warning( - "Authentication redirected to login page. SU platform access cookie is invalid or expired." + "Authentication redirected to login page. " + "SU platform access cookie is invalid or expired." ) return () diff --git a/config.py b/config.py index 46c445ef3..1a5e2ed31 100644 --- a/config.py +++ b/config.py @@ -482,9 +482,9 @@ def _setup_su_platform_access_cookie(cls) -> None: @classmethod def _setup_auto_su_platform_access_cookie_checking(cls) -> None: - raw_auto_auth_session_cookie_checking: str = os.getenv( - "AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING", "False" - ).lower().strip() + raw_auto_auth_session_cookie_checking: str = ( + os.getenv("AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING", "False").lower().strip() + ) if raw_auto_auth_session_cookie_checking not in TRUE_VALUES | FALSE_VALUES: INVALID_AUTO_AUTH_CHECKING_MESSAGE: Final[str] = ( @@ -506,9 +506,7 @@ def _setup_auto_su_platform_access_cookie_checking_interval(cls) -> None: raise RuntimeError(INVALID_SETUP_ORDER_MESSAGE) if not cls._settings["AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING"]: - cls._settings["AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING_INTERVAL"] = { - "hours": 24 - } + cls._settings["AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING_INTERVAL"] = {"hours": 24} return raw_auto_su_platform_access_cookie_checking_interval: re.Match[str] | None = ( @@ -532,25 +530,26 @@ def _setup_auto_su_platform_access_cookie_checking_interval(cls) -> None: ) raw_timedelta_auto_su_platform_access_cookie_checking_interval: Mapping[str, float] = { - key: float(stripped_value) + key: float(value) for key, value in ( raw_auto_su_platform_access_cookie_checking_interval.groupdict().items() ) - if stripped_value := value.strip() + if value == value.strip() } - if ( - timedelta( - **raw_timedelta_auto_su_platform_access_cookie_checking_interval - ).total_seconds() - <= 3 - ): - TOO_SMALL_AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING_INTERVAL_MESSAGE: Final[str] = ( - "AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING_INTERVAL must be longer than 3 seconds." - ) - raise ImproperlyConfiguredError( - TOO_SMALL_AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING_INTERVAL_MESSAGE, - ) + if ( + timedelta( + **raw_timedelta_auto_su_platform_access_cookie_checking_interval + ).total_seconds() + <= 3 + ): + TOO_SMALL_AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING_INTERVAL_MESSAGE: Final[str] = ( + "AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING_INTERVAL " + "must be greater than 3 seconds." + ) + raise ImproperlyConfiguredError( + TOO_SMALL_AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING_INTERVAL_MESSAGE, + ) cls._settings["AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING_INTERVAL"] = ( raw_timedelta_auto_su_platform_access_cookie_checking_interval From cb0a68a117ea1b5e4b5849e7b6674ef6f08c4e00 Mon Sep 17 00:00:00 2001 From: MattyTheHacker <18513864+MattyTheHacker@users.noreply.github.com> Date: Sat, 5 Jul 2025 21:20:08 +0100 Subject: [PATCH 38/41] Fixes --- cogs/check_su_platform_authorisation.py | 2 +- config.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cogs/check_su_platform_authorisation.py b/cogs/check_su_platform_authorisation.py index bbd40d243..91a00e7c4 100644 --- a/cogs/check_su_platform_authorisation.py +++ b/cogs/check_su_platform_authorisation.py @@ -159,7 +159,7 @@ async def get_su_platform_organisations(self) -> "Iterable[str]": if parsed_html is None or isinstance(parsed_html, bs4.NavigableString): NO_ADMIN_TABLE_MESSAGE: Final[str] = ( - f"Failed to retrieve the admin table for user: {user_name.string}." + f"Failed to retrieve the admin table for user: {user_name.string}. " "Please check you have used the correct SU platform access token!" ) logger.warning(NO_ADMIN_TABLE_MESSAGE) diff --git a/config.py b/config.py index 1a5e2ed31..89502dac6 100644 --- a/config.py +++ b/config.py @@ -530,11 +530,11 @@ def _setup_auto_su_platform_access_cookie_checking_interval(cls) -> None: ) raw_timedelta_auto_su_platform_access_cookie_checking_interval: Mapping[str, float] = { - key: float(value) + key: float(stripped_value) for key, value in ( raw_auto_su_platform_access_cookie_checking_interval.groupdict().items() ) - if value == value.strip() + if (stripped_value := value.strip()) } if ( From 95ae35f39e106a42b3f50d3a6d4f58d5bf6e73bf Mon Sep 17 00:00:00 2001 From: MattyTheHacker <18513864+MattyTheHacker@users.noreply.github.com> Date: Sat, 5 Jul 2025 21:33:06 +0100 Subject: [PATCH 39/41] Apply stuff --- cogs/check_su_platform_authorisation.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/cogs/check_su_platform_authorisation.py b/cogs/check_su_platform_authorisation.py index 91a00e7c4..ce45a1772 100644 --- a/cogs/check_su_platform_authorisation.py +++ b/cogs/check_su_platform_authorisation.py @@ -17,6 +17,7 @@ if TYPE_CHECKING: from collections.abc import Iterable, Mapping, Sequence + from collections.abc import Set as AbstractSet from logging import Logger from typing import Final @@ -198,15 +199,25 @@ async def check_su_platform_authorisation(self, ctx: "TeXBotApplicationContext") await ctx.defer(ephemeral=True) async with ctx.typing(): + members_list_auth_session_cookie_organisations: AbstractSet[str] = set( + await self.get_su_platform_organisations() + ) + await ctx.followup.send( content=( - f"SU Platform Access Cookie has access to the following MSL Organisations:" - f"\n{ - ',\n'.join( - organisation - for organisation in (await self.get_su_platform_organisations()) - ) - }" + "No MSL organisations are available to the SU platform access cookie. " + "Please check the logs for errors." + if not members_list_auth_session_cookie_organisations + else ( + f"SU Platform Access Cookie has access to the following " + "MSL Organisations:" + f"\n{ + ',\n'.join( + organisation + for organisation in members_list_auth_session_cookie_organisations + ) + }" + ) ), ephemeral=True, ) From bc661f2b4e26b7f0b72824d0afbf9181739981ae Mon Sep 17 00:00:00 2001 From: MattyTheHacker <18513864+MattyTheHacker@users.noreply.github.com> Date: Sat, 5 Jul 2025 21:39:26 +0100 Subject: [PATCH 40/41] variable --- cogs/check_su_platform_authorisation.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cogs/check_su_platform_authorisation.py b/cogs/check_su_platform_authorisation.py index ce45a1772..702233047 100644 --- a/cogs/check_su_platform_authorisation.py +++ b/cogs/check_su_platform_authorisation.py @@ -199,7 +199,7 @@ async def check_su_platform_authorisation(self, ctx: "TeXBotApplicationContext") await ctx.defer(ephemeral=True) async with ctx.typing(): - members_list_auth_session_cookie_organisations: AbstractSet[str] = set( + su_platform_access_cookie_organisations: AbstractSet[str] = set( await self.get_su_platform_organisations() ) @@ -207,14 +207,14 @@ async def check_su_platform_authorisation(self, ctx: "TeXBotApplicationContext") content=( "No MSL organisations are available to the SU platform access cookie. " "Please check the logs for errors." - if not members_list_auth_session_cookie_organisations + if not su_platform_access_cookie_organisations else ( f"SU Platform Access Cookie has access to the following " "MSL Organisations:" f"\n{ ',\n'.join( organisation - for organisation in members_list_auth_session_cookie_organisations + for organisation in su_platform_access_cookie_organisations ) }" ) From 742439a82251f1bef515d4fdb1d2ee8c63ae6b91 Mon Sep 17 00:00:00 2001 From: Matty Widdop <18513864+MattyTheHacker@users.noreply.github.com> Date: Sat, 5 Jul 2025 21:46:39 +0100 Subject: [PATCH 41/41] Apply suggestions from code review Co-authored-by: Matt Norton Signed-off-by: Matty Widdop <18513864+MattyTheHacker@users.noreply.github.com> --- cogs/check_su_platform_authorisation.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cogs/check_su_platform_authorisation.py b/cogs/check_su_platform_authorisation.py index 702233047..061d73ff6 100644 --- a/cogs/check_su_platform_authorisation.py +++ b/cogs/check_su_platform_authorisation.py @@ -52,21 +52,21 @@ class SUPlatformAccessCookieStatus(Enum): INVALID = ( logging.WARNING, ( - "The auth session cookie is not associated with any MSL user, " + "The SU platform access cookie is not associated with any MSL user, " "meaning it is invalid or expired." ), ) VALID = ( logging.WARNING, ( - "The auth session cookie is associated with a valid MSL user, " + "The SU platform access cookie is associated with a valid MSL user, " "but is not an admin to any MSL organisations." ), ) AUTHORISED = ( logging.INFO, ( - "The auth session cookie is associated with a valid MSL user and " + "The SU platform access cookie is associated with a valid MSL user and " "has access to at least one MSL organisation." ), ) @@ -86,7 +86,7 @@ async def _fetch_url_content_with_session(self, url: str) -> str: return await http_response.text() async def get_su_platform_access_cookie_status(self) -> SUPlatformAccessCookieStatus: - """Retrieve the current validity status of the members list auth session cookie.""" + """Retrieve the current validity status of the SU platform access cookie.""" response_object: bs4.BeautifulSoup = bs4.BeautifulSoup( await self._fetch_url_content_with_session(SU_PLATFORM_PROFILE_URL), "html.parser" ) @@ -112,7 +112,7 @@ async def get_su_platform_access_cookie_status(self) -> SUPlatformAccessCookieSt return SUPlatformAccessCookieStatus.INVALID async def get_su_platform_organisations(self) -> "Iterable[str]": - """Retrieve the set of MSL organisations the current SU platform session cookie has access to.""" # noqa: E501, W505 + """Retrieve the MSL organisations the current SU platform cookie has access to.""" response_object: bs4.BeautifulSoup = bs4.BeautifulSoup( await self._fetch_url_content_with_session(SU_PLATFORM_PROFILE_URL), "html.parser" )