Skip to content

Commit 9716f97

Browse files
Refactor membership queries (#605)
1 parent 8f2aaee commit 9716f97

File tree

5 files changed

+200
-131
lines changed

5 files changed

+200
-131
lines changed

cogs/make_member.py

Lines changed: 30 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,29 @@
44
import re
55
from typing import TYPE_CHECKING
66

7-
import aiohttp
8-
import bs4
97
import discord
10-
from bs4 import BeautifulSoup
118
from django.core.exceptions import ValidationError
129

1310
from config import settings
1411
from db.core.models import GroupMadeMember
1512
from exceptions import ApplicantRoleDoesNotExistError, GuestRoleDoesNotExistError
16-
from utils import GLOBAL_SSL_CONTEXT, CommandChecks, TeXBotBaseCog
13+
from utils import CommandChecks, TeXBotBaseCog
14+
from utils.msl import fetch_community_group_members_count, is_id_a_community_group_member
1715

1816
if TYPE_CHECKING:
19-
from collections.abc import Mapping, Sequence
17+
from collections.abc import Sequence
2018
from logging import Logger
2119
from typing import Final
2220

2321
from utils import TeXBotApplicationContext
2422

23+
2524
__all__: "Sequence[str]" = ("MakeMemberCommandCog", "MemberCountCommandCog")
2625

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

29+
2930
_GROUP_MEMBER_ID_ARGUMENT_DESCRIPTIVE_NAME: "Final[str]" = f"""{
3031
"Student"
3132
if (
@@ -49,21 +50,6 @@
4950
_GROUP_MEMBER_ID_ARGUMENT_DESCRIPTIVE_NAME.lower().replace(" ", "")
5051
)
5152

52-
REQUEST_HEADERS: "Final[Mapping[str, str]]" = {
53-
"Cache-Control": "no-cache",
54-
"Pragma": "no-cache",
55-
"Expires": "0",
56-
}
57-
58-
REQUEST_COOKIES: "Final[Mapping[str, str]]" = {
59-
".ASPXAUTH": settings["SU_PLATFORM_ACCESS_COOKIE"]
60-
}
61-
62-
BASE_MEMBERS_URL: "Final[str]" = (
63-
f"https://guildofstudents.com/organisation/memberlist/{settings['ORGANISATION_ID']}"
64-
)
65-
GROUPED_MEMBERS_URL: "Final[str]" = f"{BASE_MEMBERS_URL}/?sort=groups"
66-
6753

6854
class MakeMemberCommandCog(TeXBotBaseCog):
6955
"""Cog class that defines the "/make-member" command and its call-back method."""
@@ -101,10 +87,12 @@ class MakeMemberCommandCog(TeXBotBaseCog):
10187
required=True,
10288
max_length=7,
10389
min_length=7,
104-
parameter_name="group_member_id",
90+
parameter_name="raw_group_member_id",
10591
)
10692
@CommandChecks.check_interaction_user_in_main_guild
107-
async def make_member(self, ctx: "TeXBotApplicationContext", group_member_id: str) -> None: # type: ignore[misc]
93+
async def make_member( # type: ignore[misc]
94+
self, ctx: "TeXBotApplicationContext", raw_group_member_id: str
95+
) -> None:
10896
"""
10997
Definition & callback response of the "make_member" command.
11098
@@ -116,6 +104,20 @@ async def make_member(self, ctx: "TeXBotApplicationContext", group_member_id: st
116104
member_role: discord.Role = await self.bot.member_role
117105
interaction_member: discord.Member = await ctx.bot.get_main_guild_member(ctx.user)
118106

107+
INVALID_GROUP_MEMBER_ID_MESSAGE: Final[str] = (
108+
f"{raw_group_member_id!r} is not a valid {self.bot.group_member_id_type} ID."
109+
)
110+
111+
if not re.fullmatch(r"\A\d{7}\Z", raw_group_member_id):
112+
await self.command_send_error(ctx, message=(INVALID_GROUP_MEMBER_ID_MESSAGE))
113+
return
114+
115+
try:
116+
group_member_id: int = int(raw_group_member_id)
117+
except ValueError:
118+
await self.command_send_error(ctx, message=INVALID_GROUP_MEMBER_ID_MESSAGE)
119+
return
120+
119121
await ctx.defer(ephemeral=True)
120122
async with ctx.typing():
121123
if member_role in interaction_member.roles:
@@ -128,16 +130,6 @@ async def make_member(self, ctx: "TeXBotApplicationContext", group_member_id: st
128130
)
129131
return
130132

131-
if not re.fullmatch(r"\A\d{7}\Z", group_member_id):
132-
await self.command_send_error(
133-
ctx,
134-
message=(
135-
f"{group_member_id!r} is not a valid "
136-
f"{self.bot.group_member_id_type} ID."
137-
),
138-
)
139-
return
140-
141133
if await GroupMadeMember.objects.filter(
142134
hashed_group_member_id=GroupMadeMember.hash_group_member_id(
143135
group_member_id, self.bot.group_member_id_type
@@ -154,56 +146,7 @@ async def make_member(self, ctx: "TeXBotApplicationContext", group_member_id: st
154146
)
155147
return
156148

157-
guild_member_ids: set[str] = set()
158-
159-
async with (
160-
aiohttp.ClientSession(
161-
headers=REQUEST_HEADERS, cookies=REQUEST_COOKIES
162-
) as http_session,
163-
http_session.get(
164-
url=GROUPED_MEMBERS_URL, ssl=GLOBAL_SSL_CONTEXT
165-
) as http_response,
166-
):
167-
response_html: str = await http_response.text()
168-
169-
MEMBER_HTML_TABLE_IDS: Final[frozenset[str]] = frozenset(
170-
{
171-
"ctl00_Main_rptGroups_ctl05_gvMemberships",
172-
"ctl00_Main_rptGroups_ctl03_gvMemberships",
173-
"ctl00_ctl00_Main_AdminPageContent_rptGroups_ctl03_gvMemberships",
174-
"ctl00_ctl00_Main_AdminPageContent_rptGroups_ctl05_gvMemberships",
175-
}
176-
)
177-
table_id: str
178-
for table_id in MEMBER_HTML_TABLE_IDS:
179-
parsed_html: bs4.Tag | bs4.NavigableString | None = BeautifulSoup(
180-
response_html, "html.parser"
181-
).find("table", {"id": table_id})
182-
183-
if parsed_html is None or isinstance(parsed_html, bs4.NavigableString):
184-
continue
185-
186-
guild_member_ids.update(
187-
row.contents[2].text
188-
for row in parsed_html.find_all("tr", {"class": ["msl_row", "msl_altrow"]})
189-
)
190-
191-
guild_member_ids.discard("")
192-
guild_member_ids.discard("\n")
193-
guild_member_ids.discard(" ")
194-
195-
if not guild_member_ids:
196-
await self.command_send_error(
197-
ctx,
198-
error_code="E1041",
199-
logging_message=OSError(
200-
"The guild member IDs could not be retrieved from "
201-
"the MEMBERS_LIST_URL."
202-
),
203-
)
204-
return
205-
206-
if group_member_id not in guild_member_ids:
149+
if not await is_id_a_community_group_member(member_id=group_member_id):
207150
await self.command_send_error(
208151
ctx,
209152
message=(
@@ -222,7 +165,7 @@ async def make_member(self, ctx: "TeXBotApplicationContext", group_member_id: st
222165
)
223166

224167
try:
225-
await GroupMadeMember.objects.acreate(group_member_id=group_member_id) # type: ignore[misc]
168+
await GroupMadeMember.objects.acreate(group_member_id=raw_group_member_id) # type: ignore[misc]
226169
except ValidationError as create_group_made_member_error:
227170
error_is_already_exists: bool = (
228171
"hashed_group_member_id" in create_group_made_member_error.message_dict
@@ -276,53 +219,9 @@ async def member_count(self, ctx: "TeXBotApplicationContext") -> None: # type:
276219
await ctx.defer(ephemeral=False)
277220

278221
async with ctx.typing():
279-
async with (
280-
aiohttp.ClientSession(
281-
headers=REQUEST_HEADERS, cookies=REQUEST_COOKIES
282-
) as http_session,
283-
http_session.get(
284-
url=BASE_MEMBERS_URL, ssl=GLOBAL_SSL_CONTEXT
285-
) as http_response,
286-
):
287-
response_html: str = await http_response.text()
288-
289-
member_list_div: bs4.Tag | bs4.NavigableString | None = BeautifulSoup(
290-
response_html, "html.parser"
291-
).find("div", {"class": "memberlistcol"})
292-
293-
if member_list_div is None or isinstance(member_list_div, bs4.NavigableString):
294-
await self.command_send_error(
295-
ctx,
296-
error_code="E1041",
297-
logging_message=OSError(
298-
"The member count could not be retrieved from the MEMBERS_LIST_URL."
299-
),
300-
)
301-
return
302-
303-
if "showing 100 of" in member_list_div.text.lower():
304-
member_count: str = member_list_div.text.split(" ")[3]
305-
await ctx.followup.send(
306-
content=f"{self.bot.group_full_name} has {member_count} members! :tada:"
307-
)
308-
return
309-
310-
member_table: bs4.Tag | bs4.NavigableString | None = BeautifulSoup(
311-
response_html, "html.parser"
312-
).find("table", {"id": "ctl00_ctl00_Main_AdminPageContent_gvMembers"})
313-
314-
if member_table is None or isinstance(member_table, bs4.NavigableString):
315-
await self.command_send_error(
316-
ctx,
317-
error_code="E1041",
318-
logging_message=OSError(
319-
"The member count could not be retrieved from the MEMBERS_LIST_URL."
320-
),
321-
)
322-
return
323-
324222
await ctx.followup.send(
325-
content=f"{self.bot.group_full_name} has {
326-
len(member_table.find_all('tr', {'class': ['msl_row', 'msl_altrow']}))
327-
} members! :tada:"
223+
content=(
224+
f"{self.bot.group_full_name} has "
225+
f"{await fetch_community_group_members_count()} members! :tada:"
226+
)
328227
)

exceptions/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
MessagesJSONFileMissingKeyError,
2525
MessagesJSONFileValueError,
2626
)
27+
from .msl import MSLMembershipError
2728
from .strike import NoAuditLogsStrikeTrackingError, StrikeTrackingError
2829

2930
if TYPE_CHECKING:
@@ -44,6 +45,7 @@
4445
"InvalidActionDescriptionError",
4546
"InvalidActionTargetError",
4647
"InvalidMessagesJSONFileError",
48+
"MSLMembershipError",
4749
"MemberRoleDoesNotExistError",
4850
"MessagesJSONFileMissingKeyError",
4951
"MessagesJSONFileValueError",

exceptions/msl.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"""Custom exception classes raised when errors occur during use of MSL features."""
2+
3+
from typing import TYPE_CHECKING, override
4+
5+
from typed_classproperties import classproperty
6+
7+
from .base import BaseTeXBotError
8+
9+
if TYPE_CHECKING:
10+
from collections.abc import Sequence
11+
12+
13+
__all__: "Sequence[str]" = ("MSLMembershipError",)
14+
15+
16+
class MSLMembershipError(BaseTeXBotError, RuntimeError):
17+
"""
18+
Exception class to raise when any error occurs while checking MSL membership.
19+
20+
If this error occurs, it is likely that MSL features will not work correctly.
21+
"""
22+
23+
@classproperty
24+
@override
25+
def DEFAULT_MESSAGE(cls) -> str:
26+
return "An error occurred while trying to fetch membership data from MSL."

utils/msl/__init__.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"""MSL utility classes & functions provided for use across the whole of the project."""
2+
3+
from typing import TYPE_CHECKING
4+
5+
from .memberships import (
6+
fetch_community_group_members_count,
7+
fetch_community_group_members_list,
8+
is_id_a_community_group_member,
9+
)
10+
11+
if TYPE_CHECKING:
12+
from collections.abc import Sequence
13+
14+
__all__: "Sequence[str]" = (
15+
"fetch_community_group_members_count",
16+
"fetch_community_group_members_list",
17+
"is_id_a_community_group_member",
18+
)

0 commit comments

Comments
 (0)