-
Notifications
You must be signed in to change notification settings - Fork 12
Expand file tree
/
Copy pathcheck_su_platform_authorisation.py
More file actions
251 lines (201 loc) · 9.14 KB
/
check_su_platform_authorisation.py
File metadata and controls
251 lines (201 loc) · 9.14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
"""Contains cog classes for SU platform access cookie authorisation check interactions."""
import logging
from enum import Enum
from typing import TYPE_CHECKING, override
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,
)
from utils.msl import fetch_url_content_with_session
if TYPE_CHECKING:
from collections.abc import Iterable, Sequence
from collections.abc import Set as AbstractSet
from logging import Logger
from typing import Final
from utils import TeXBot, TeXBotApplicationContext
__all__: "Sequence[str]" = (
"CheckSUPlatformAuthorisationCommandCog",
"CheckSUPlatformAuthorisationTaskCog",
)
logger: "Final[Logger]" = logging.getLogger("TeX-Bot")
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):
"""Enum class defining the status of the SU Platform Access Cookie."""
INVALID = (
logging.WARNING,
(
"The SU platform access cookie is not associated with any MSL user, "
"meaning it is invalid or expired."
),
)
VALID = (
logging.WARNING,
(
"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 SU platform access 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 functionality for cookie authorisation checks."""
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 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.warning("Token is invalid or expired.")
return SUPlatformAccessCookieStatus.INVALID
organisation_admin_url: str = (
f"{SU_PLATFORM_ORGANISATION_URL}/{settings['ORGANISATION_ID']}"
)
response_html: str = await fetch_url_content_with_session(organisation_admin_url)
if "admin tools" in response_html.lower():
return SUPlatformAccessCookieStatus.AUTHORISED
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."
)
return SUPlatformAccessCookieStatus.INVALID
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 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."
)
return ()
if "Login" in str(page_title):
logger.warning(
"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"}
)
if profile_section_html is None:
logger.warning(
"Couldn't find the profile section of the user "
"when scraping the SU platform's website HTML."
)
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):
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"}
)
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 SU platform access 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(
"SU platform access cookie has admin authorisation to: %s as user %s",
organisations,
user_name.text,
)
return organisations
class CheckSUPlatformAuthorisationCommandCog(CheckSUPlatformAuthorisationBaseCog):
"""Cog class that defines the "/check-su-platform-authorisation" command."""
@discord.slash_command(
name="check-su-platform-authorisation",
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
async def check_su_platform_authorisation(self, ctx: "TeXBotApplicationContext") -> None:
"""
Definition of the "check_su_platform_authorisation" command.
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.
"""
await ctx.defer(ephemeral=True)
async with ctx.typing():
su_platform_access_cookie_organisations: AbstractSet[str] = set(
await self.get_su_platform_organisations()
)
await ctx.followup.send(
content=(
"No MSL organisations are available to the SU platform access cookie. "
"Please check the logs for errors."
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 su_platform_access_cookie_organisations
)
}"
)
),
ephemeral=True,
)
class CheckSUPlatformAuthorisationTaskCog(CheckSUPlatformAuthorisationBaseCog):
"""Cog class defining a repeated task for checking SU platform access cookie."""
@override
def __init__(self, bot: "TeXBot") -> None:
"""Start all task managers when this cog is initialised."""
if settings["AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING"]:
_ = self.su_platform_access_cookie_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.su_platform_access_cookie_check_task.cancel()
@tasks.loop(**settings["AUTO_SU_PLATFORM_ACCESS_COOKIE_CHECKING_INTERVAL"])
@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.
The task will check if the cookie is valid and if it is, it will retrieve the
MSL organisations the cookie has access to.
"""
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()
).value
logger.log(
level=su_platform_access_cookie_status[0],
msg=su_platform_access_cookie_status[1],
)
@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()