Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 6 additions & 25 deletions cogs/check_su_platform_authorisation.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,19 @@
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 GLOBAL_SSL_CONTEXT, CommandChecks, TeXBotBaseCog
from utils import CommandChecks, TeXBotBaseCog
from utils.error_capture_decorators import (
capture_guild_does_not_exist_error,
)
from utils.msl import fetch_url_content_with_session

if TYPE_CHECKING:
from collections.abc import Iterable, Mapping, Sequence
from collections.abc import Iterable, Sequence
from collections.abc import Set as AbstractSet
from logging import Logger
from typing import Final
Expand All @@ -31,15 +31,6 @@

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]]" = {
".AspNet.SharedCookie": settings["SU_PLATFORM_ACCESS_COOKIE"]
}

SU_PLATFORM_PROFILE_URL: "Final[str]" = "https://guildofstudents.com/profile"
SU_PLATFORM_ORGANISATION_URL: "Final[str]" = (
Expand Down Expand Up @@ -76,20 +67,10 @@ class SUPlatformAccessCookieStatus(Enum):
class CheckSUPlatformAuthorisationBaseCog(TeXBotBaseCog):
"""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
) as http_session,
http_session.get(url=url, ssl=GLOBAL_SSL_CONTEXT) as http_response,
):
return await http_response.text()

async def get_su_platform_access_cookie_status(self) -> SUPlatformAccessCookieStatus:
"""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"
await 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):
Expand All @@ -99,7 +80,7 @@ async def get_su_platform_access_cookie_status(self) -> SUPlatformAccessCookieSt
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)
response_html: str = await fetch_url_content_with_session(organisation_admin_url)

if "admin tools" in response_html.lower():
return SUPlatformAccessCookieStatus.AUTHORISED
Expand All @@ -115,7 +96,7 @@ async def get_su_platform_access_cookie_status(self) -> SUPlatformAccessCookieSt
async def get_su_platform_organisations(self) -> "Iterable[str]":
"""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"
await fetch_url_content_with_session(SU_PLATFORM_PROFILE_URL), "html.parser"
)

page_title: bs4.Tag | bs4.NavigableString | None = response_object.find("title")
Expand Down
2 changes: 2 additions & 0 deletions utils/msl/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from .memberships import (
fetch_community_group_members_count,
fetch_community_group_members_list,
fetch_url_content_with_session,
is_id_a_community_group_member,
)

Expand All @@ -14,5 +15,6 @@
__all__: "Sequence[str]" = (
"fetch_community_group_members_count",
"fetch_community_group_members_list",
"fetch_url_content_with_session",
"is_id_a_community_group_member",
)
44 changes: 30 additions & 14 deletions utils/msl/memberships.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,51 +13,67 @@
from utils import GLOBAL_SSL_CONTEXT

if TYPE_CHECKING:
from collections.abc import Mapping, Sequence
from collections.abc import Mapping, MutableMapping, Sequence
from http.cookies import Morsel
from logging import Logger
from typing import Final


__all__: "Sequence[str]" = (
"fetch_community_group_members_count",
"fetch_community_group_members_list",
"fetch_url_content_with_session",
"is_id_a_community_group_member",
)


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

SU_PLATFORM_ACCESS_COOKIE: "Final[str]" = settings["SU_PLATFORM_ACCESS_COOKIE"]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

settings["SU_PLATFORM_ACCESS_COOKIE"] can be used directly, rather than assigning to a variable first.


BASE_SU_PLATFORM_WEB_HEADERS: "Final[Mapping[str, str]]" = {
"Cache-Control": "no-cache",
"Pragma": "no-cache",
"Expires": "0",
}

BASE_SU_PLATFORM_WEB_COOKIES: "Final[Mapping[str, str]]" = {
".AspNet.SharedCookie": settings["SU_PLATFORM_ACCESS_COOKIE"],
BASE_SU_PLATFORM_WEB_COOKIES: "Final[MutableMapping[str, str]]" = {
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type annotation Final[MutableMapping[str, str]] is contradictory. Final indicates the variable binding cannot be reassigned, but MutableMapping indicates the contents are mutable. While Python will allow this at runtime, it violates the semantic intent of Final and may confuse type checkers. Consider either:

  1. Using MutableMapping[str, str] without Final (recommended, since the dictionary contents are mutated)
  2. Using a different pattern that doesn't mix immutability guarantees with mutable collections
Suggested change
BASE_SU_PLATFORM_WEB_COOKIES: "Final[MutableMapping[str, str]]" = {
BASE_SU_PLATFORM_WEB_COOKIES: "MutableMapping[str, str]" = {

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I neither know nor care if this matters, the code runs fine - Matt up to you if you want this fixed

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the correct annotation here is Mapping[str, str]. Then when you need to update the cookie, assign a new dict to BASE_SU_PLATFORM_WEB_COOKIES rather than just updating the key.

".AspNet.SharedCookie": SU_PLATFORM_ACCESS_COOKIE,
}

MEMBERS_LIST_URL: "Final[str]" = f"https://guildofstudents.com/organisation/memberlist/{settings['ORGANISATION_ID']}/?sort=groups"

_membership_list_cache: set[int] = set()


async def fetch_community_group_members_list() -> set[int]:
"""
Make a web request to fetch your community group's full membership list.
Returns a set of IDs.
"""
async def fetch_url_content_with_session(url: str) -> str:
"""Fetch the HTTP content at the given URL, using a shared aiohttp session."""
async with (
aiohttp.ClientSession(
headers=BASE_SU_PLATFORM_WEB_HEADERS, cookies=BASE_SU_PLATFORM_WEB_COOKIES
) as http_session,
http_session.get(url=MEMBERS_LIST_URL, ssl=GLOBAL_SSL_CONTEXT) as http_response,
http_session.get(url=url, ssl=GLOBAL_SSL_CONTEXT) as http_response,
):
response_html: str = await http_response.text()
returned_asp_cookie: Morsel | None = http_response.cookies.get(".AspNet.SharedCookie") # type: ignore[type-arg]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the need to use # type: ignore[type-arg] here?

if (
returned_asp_cookie
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
returned_asp_cookie
returned_asp_cookie is not None

and returned_asp_cookie.value
!= BASE_SU_PLATFORM_WEB_COOKIES[".AspNet.SharedCookie"]
Comment on lines +60 to +61
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
and returned_asp_cookie.value
!= BASE_SU_PLATFORM_WEB_COOKIES[".AspNet.SharedCookie"]
and (
returned_asp_cookie.value
!= BASE_SU_PLATFORM_WEB_COOKIES[".AspNet.SharedCookie"]
)

):
logger.info("SU platform access cookie was updated by the server; updating local.")
BASE_SU_PLATFORM_WEB_COOKIES[".AspNet.SharedCookie"] = returned_asp_cookie.value
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
BASE_SU_PLATFORM_WEB_COOKIES[".AspNet.SharedCookie"] = returned_asp_cookie.value
BASE_SU_PLATFORM_WEB_COOKIES = {".AspNet.SharedCookie": returned_asp_cookie.value}

return await http_response.text()


parsed_html: BeautifulSoup = BeautifulSoup(markup=response_html, features="html.parser")
async def fetch_community_group_members_list() -> set[int]:
"""
Make a web request to fetch your community group's full membership list.
Returns a set of IDs.
"""
parsed_html: BeautifulSoup = BeautifulSoup(
markup=await fetch_url_content_with_session(MEMBERS_LIST_URL), features="html.parser"
)

member_ids: set[int] = set()

Expand All @@ -72,7 +88,7 @@ async def fetch_community_group_members_list() -> set[int]:

if filtered_table is None:
logger.warning("Membership table with ID %s could not be found.", table_id)
logger.debug(response_html)
logger.debug(parsed_html)
continue

if isinstance(filtered_table, bs4.NavigableString):
Expand All @@ -97,7 +113,7 @@ async def fetch_community_group_members_list() -> set[int]:
if not member_ids: # NOTE: this should never be possible, because to fetch the page you need to have admin access, which requires being a member.
NO_MEMBERS_MESSAGE: Final[str] = "No members were found in either membership table."
logger.warning(NO_MEMBERS_MESSAGE)
logger.debug(response_html)
logger.debug(parsed_html)
raise MSLMembershipError(message=NO_MEMBERS_MESSAGE)

_membership_list_cache.clear()
Expand Down
Loading