Skip to content
Draft
Show file tree
Hide file tree
Changes from 72 commits
Commits
Show all changes
122 commits
Select commit Hold shift + click to select a range
35d0493
add MSL methods
MattyTheHacker Aug 18, 2024
247a011
actually do it this time
MattyTheHacker Aug 18, 2024
e4ad85b
add anyio
MattyTheHacker Aug 18, 2024
5365811
improve referencing
MattyTheHacker Aug 18, 2024
2b174c3
implement is_user_member
MattyTheHacker Aug 18, 2024
d758e20
make urls class variable
MattyTheHacker Aug 18, 2024
8192c85
refactor
MattyTheHacker Aug 18, 2024
7831001
yeet the classes
MattyTheHacker Aug 18, 2024
da09b7b
Merge branch 'main' into add-msl-methods
MattyTheHacker Aug 18, 2024
fca2bcb
refactor with config changes
MattyTheHacker Aug 18, 2024
81117f3
more
MattyTheHacker Aug 18, 2024
0eae636
refactor makemember
MattyTheHacker Aug 18, 2024
7de0d21
update comment
MattyTheHacker Aug 18, 2024
10d49ae
add await and remove old link
MattyTheHacker Aug 18, 2024
8f3256e
not working yet
MattyTheHacker Aug 19, 2024
fb8d6a0
refactor
MattyTheHacker Aug 22, 2024
87b19ec
yeet
MattyTheHacker Aug 22, 2024
60a4cb2
Merge branch 'main' into add-msl-methods
MattyTheHacker Aug 22, 2024
2c1122c
Merge branch 'main' into add-msl-methods
MattyTheHacker Aug 24, 2024
67872d7
refactor
MattyTheHacker Aug 24, 2024
6cd4016
build out activities
MattyTheHacker Aug 24, 2024
cf57587
Merge branch 'main' into add-msl-methods
MattyTheHacker Aug 27, 2024
7c7de5d
update deps
MattyTheHacker Aug 27, 2024
d346560
start finances
MattyTheHacker Aug 29, 2024
293d471
Merge branch 'main' into add-msl-methods
MattyTheHacker Aug 30, 2024
c18994e
add some stuff
MattyTheHacker Aug 30, 2024
1597ea6
not implemented
MattyTheHacker Aug 31, 2024
4415d36
formatting
MattyTheHacker Sep 1, 2024
71571d3
Merge branch 'main' into add-msl-methods
MattyTheHacker Sep 5, 2024
edce5da
update lock
MattyTheHacker Sep 5, 2024
0c35949
Merge branch 'main' into add-msl-methods
MattyTheHacker Sep 6, 2024
6002c63
Merge branch 'main' into add-msl-methods
MattyTheHacker Sep 11, 2024
7d3d89f
update deps
MattyTheHacker Sep 11, 2024
6ed18ff
minor finance work
MattyTheHacker Sep 12, 2024
9459613
update deps
MattyTheHacker Sep 12, 2024
798d191
Merge branch 'main' into add-msl-methods
MattyTheHacker Sep 17, 2024
2d798c4
update deps
MattyTheHacker Sep 17, 2024
4f5760e
Merge branch 'main' into add-msl-methods
MattyTheHacker Sep 25, 2024
c7cfd8d
Update dependencies
MattyTheHacker Sep 25, 2024
9cc67b9
add todo
MattyTheHacker Oct 2, 2024
aeaaf8a
Add caching to member list fetching
MattyTheHacker Oct 4, 2024
d39008d
Merge branch 'main' into add-msl-methods
MattyTheHacker Oct 7, 2024
7bb1861
update dependencies
MattyTheHacker Oct 7, 2024
900d911
not working yet
MattyTheHacker Oct 14, 2024
d949e17
Merge branch 'main' into add-msl-methods
MattyTheHacker Oct 14, 2024
c49cb42
update deps
MattyTheHacker Oct 14, 2024
af27c5c
Merge branch 'main' into add-msl-methods
MattyTheHacker Oct 16, 2024
9b656e3
update deps
MattyTheHacker Oct 16, 2024
3c8d312
ruff can suck my nuts
MattyTheHacker Oct 16, 2024
6e73fe2
Merge branch 'main' into add-msl-methods
MattyTheHacker Oct 19, 2024
921041e
update deps
MattyTheHacker Oct 19, 2024
97ca9a4
bump lock fle2
MattyTheHacker Dec 28, 2024
6f6fffa
fix ruff errors
MattyTheHacker Dec 28, 2024
ccccb10
Merge branch 'main' into add-msl-methods
MattyTheHacker Dec 28, 2024
9f89a88
Fix bits
MattyTheHacker Dec 28, 2024
8ffb008
update lock file
MattyTheHacker Jan 1, 2025
8545349
Merge branch 'main' into add-msl-methods
MattyTheHacker Jan 1, 2025
46b6b9f
fix
MattyTheHacker Jan 1, 2025
a760051
fix ruff errors
MattyTheHacker Jan 1, 2025
f0d0aac
Merge remote-tracking branch 'origin/main' into add-msl-methods
MattyTheHacker Mar 1, 2025
7dceeed
Change org id name to match main
MattyTheHacker Mar 1, 2025
0118730
Minor fixes
MattyTheHacker Mar 1, 2025
e251927
Merge branch 'main' into add-msl-methods
MattyTheHacker Mar 17, 2025
f2d0587
fix mypy
MattyTheHacker Mar 17, 2025
0e31b43
Add anyio back in
MattyTheHacker Mar 17, 2025
5d34aa5
run ruff format
MattyTheHacker Mar 17, 2025
6a56715
fix mypy
MattyTheHacker Mar 17, 2025
1b006da
Merge branch 'main' into add-msl-methods
MattyTheHacker Mar 18, 2025
75858c5
Merge branch 'main' into add-msl-methods
MattyTheHacker Apr 9, 2025
ea001c1
fix some stuff
MattyTheHacker Apr 9, 2025
88cd906
Fix bad handling
MattyTheHacker Apr 9, 2025
2fd3a1b
Fix toml error
MattyTheHacker Apr 9, 2025
24daeea
fix dependencies
MattyTheHacker Apr 9, 2025
8b8fb6f
fix lint
MattyTheHacker Apr 9, 2025
e3eae1d
fix syntax
MattyTheHacker Apr 9, 2025
e859d49
Merge branch 'main' into add-msl-methods
MattyTheHacker Apr 13, 2025
7c1ee0f
Add event stuff
MattyTheHacker May 2, 2025
e1bcd11
Merge branch 'main' into add-msl-methods
MattyTheHacker May 2, 2025
3e29e09
fix ruff errors
MattyTheHacker May 2, 2025
67d5e0e
fix it even more
MattyTheHacker May 2, 2025
5634c47
seriously getting very annoyed now
MattyTheHacker May 2, 2025
7403c0b
going to kms
MattyTheHacker May 2, 2025
fa92f5b
Merge branch 'main' into add-msl-methods
MattyTheHacker May 4, 2025
4ccfd95
Merge branch 'main' into add-msl-methods
MattyTheHacker May 5, 2025
d07d33c
Merge branch 'main' into add-msl-methods
MattyTheHacker May 9, 2025
1aa6570
Merge branch 'main' into add-msl-methods
MattyTheHacker May 12, 2025
cf958c1
Merge branch 'main' into add-msl-methods
MattyTheHacker May 14, 2025
ecb7c06
it works but it's awful
MattyTheHacker May 16, 2025
dd1bbd4
fix some stuff
MattyTheHacker May 16, 2025
5b3437c
begin work on activitiy fetching
MattyTheHacker May 16, 2025
2a09a25
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] May 16, 2025
afef663
Merge branch 'main' into add-msl-methods
MattyTheHacker May 20, 2025
ecc626b
fix error
MattyTheHacker May 20, 2025
c833f23
ruff reformat
MattyTheHacker May 20, 2025
12215b6
Merge branch 'main' into add-msl-methods
MattyTheHacker May 25, 2025
2083206
Merge branch 'main' into add-msl-methods
MattyTheHacker Jun 2, 2025
050b12a
Merge main into add-msl-methods
cssbhamdev Jun 12, 2025
dd30ea7
Merge main into add-msl-methods
cssbhamdev Jun 13, 2025
d22c9c5
Merge main into add-msl-methods
cssbhamdev Jun 13, 2025
e0944c2
Merge main into add-msl-methods
cssbhamdev Jun 14, 2025
1a6ff75
Allow committee-elect to update actions (and appear in auto-complete)…
Thatsmusic99 Jun 15, 2025
7239805
Merge main into add-msl-methods
cssbhamdev Jun 15, 2025
ccf6309
Merge main into add-msl-methods
cssbhamdev Jun 15, 2025
8e7dba8
Merge main into add-msl-methods
cssbhamdev Jun 15, 2025
19e4c3e
Merge main into add-msl-methods
cssbhamdev Jun 15, 2025
519c031
Merge main into add-msl-methods
cssbhamdev Jun 16, 2025
a9d83df
Merge main into add-msl-methods
cssbhamdev Jun 16, 2025
0cfeaeb
Merge main into add-msl-methods
cssbhamdev Jun 17, 2025
a815a0c
Merge main into add-msl-methods
cssbhamdev Jun 19, 2025
eb64bb8
Merge main into add-msl-methods
cssbhamdev Jun 19, 2025
6c096fe
Merge main into add-msl-methods
cssbhamdev Jun 22, 2025
9a4e079
Merge main into add-msl-methods
cssbhamdev Jun 24, 2025
9e25dcb
Merge main into add-msl-methods
cssbhamdev Jun 24, 2025
cd347d7
Merge main into add-msl-methods
cssbhamdev Jun 24, 2025
3eeabd9
Merge main into add-msl-methods
cssbhamdev Jun 25, 2025
4dcf6d4
Merge main into add-msl-methods
cssbhamdev Jun 30, 2025
331c82f
Merge main into add-msl-methods
automatic-pr-updater[bot] Jun 30, 2025
b58728e
Merge main into add-msl-methods
automatic-pr-updater[bot] Jul 2, 2025
2c3fa57
Merge main into add-msl-methods
automatic-pr-updater[bot] Jul 2, 2025
446095d
Merge main into add-msl-methods
automatic-pr-updater[bot] Jul 3, 2025
b5b63c7
Merge main into add-msl-methods
automatic-pr-updater[bot] Jul 3, 2025
9af9d12
Merge main into add-msl-methods
automatic-pr-updater[bot] Jul 3, 2025
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
167 changes: 26 additions & 141 deletions cogs/make_member.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@
import re
from typing import TYPE_CHECKING

import aiohttp
import bs4
import discord
from bs4 import BeautifulSoup
from django.core.exceptions import ValidationError

from config import settings
Expand All @@ -27,6 +24,8 @@

from utils import TeXBotApplicationContext

from utils.msl import get_membership_count, is_student_id_member

__all__: "Sequence[str]" = ("MakeMemberCommandCog", "MemberCountCommandCog")

logger: "Final[Logger]" = logging.getLogger("TeX-Bot")
Expand Down Expand Up @@ -118,7 +117,7 @@ class MakeMemberCommandCog(TeXBotBaseCog):
parameter_name="group_member_id",
)
@CommandChecks.check_interaction_user_in_main_guild
async def make_member(self, ctx: "TeXBotApplicationContext", group_member_id: str) -> None: # type: ignore[misc] # noqa: PLR0915
async def make_member(self, ctx: "TeXBotApplicationContext", group_member_id: str) -> None: # type: ignore[misc]
"""
Definition & callback response of the "make_member" command.

Expand Down Expand Up @@ -160,11 +159,9 @@ async def make_member(self, ctx: "TeXBotApplicationContext", group_member_id: st
)
).aexists()
if GROUP_MEMBER_ID_IS_ALREADY_USED:
# noinspection PyUnusedLocal
committee_mention: str = "committee"
with contextlib.suppress(CommitteeRoleDoesNotExistError):
committee_mention = (await self.bot.committee_role).mention

await ctx.followup.send(
content=(
":information_source: No changes made. This student ID has already "
Expand All @@ -175,92 +172,32 @@ async def make_member(self, ctx: "TeXBotApplicationContext", group_member_id: st
)
return

guild_member_ids: set[str] = set()

http_session: aiohttp.ClientSession = aiohttp.ClientSession(
headers=REQUEST_HEADERS,
cookies=REQUEST_COOKIES,
if not await is_student_id_member(student_id=group_member_id):
await self.command_send_error(
ctx=ctx,
message=(
f"You must be a member of {self.bot.group_full_name} "
"to use this command.\n"
f"The provided {_GROUP_MEMBER_ID_ARGUMENT_NAME} must match "
f"the {self.bot.group_member_id_type} ID "
f"that you purchased your {self.bot.group_short_name} membership with."
),
)
async with http_session, http_session.get(GROUPED_MEMBRS_URL) as http_response:
response_html: str = await http_response.text()

MEMBER_HTML_TABLE_IDS: Final[frozenset[str]] = frozenset(
{
"ctl00_Main_rptGroups_ctl05_gvMemberships",
"ctl00_Main_rptGroups_ctl03_gvMemberships",
"ctl00_ctl00_Main_AdminPageContent_rptGroups_ctl03_gvMemberships",
"ctl00_ctl00_Main_AdminPageContent_rptGroups_ctl05_gvMemberships",
},
)
table_id: str
for table_id in MEMBER_HTML_TABLE_IDS:
parsed_html: bs4.Tag | bs4.NavigableString | None = BeautifulSoup(
response_html,
"html.parser",
).find(
"table",
{"id": table_id},
)

if parsed_html is None or isinstance(parsed_html, bs4.NavigableString):
continue

guild_member_ids.update(
row.contents[2].text
for row in parsed_html.find_all(
"tr",
{"class": ["msl_row", "msl_altrow"]},
)
)

guild_member_ids.discard("")
guild_member_ids.discard("\n")
guild_member_ids.discard(" ")

if not guild_member_ids:
await self.command_send_error(
ctx,
error_code="E1041",
logging_message=OSError(
"The guild member IDs could not be retrieved from "
"the MEMBERS_LIST_URL.",
),
)
return

if group_member_id not in guild_member_ids:
await self.command_send_error(
ctx,
message=(
f"You must be a member of {self.bot.group_full_name} "
"to use this command.\n"
f"The provided {_GROUP_MEMBER_ID_ARGUMENT_NAME} must match "
f"the {self.bot.group_member_id_type} ID "
f"that you purchased your {self.bot.group_short_name} membership with."
),
try:
await GroupMadeMember.objects.acreate(group_member_id=group_member_id) # type: ignore[misc]
except ValidationError as create_group_made_member_error:
error_is_already_exists: bool = (
"hashed_group_member_id" in create_group_made_member_error.message_dict
and any(
"already exists" in error
for error in create_group_made_member_error.message_dict[
"hashed_group_member_id"
]
)
return

# NOTE: The "Member" role must be added to the user **before** the "Guest" role to ensure that the welcome message does not include the suggestion to purchase membership
await interaction_member.add_roles(
member_role,
reason='TeX Bot slash-command: "/makemember"',
)

try:
await GroupMadeMember.objects.acreate(group_member_id=group_member_id) # type: ignore[misc]
except ValidationError as create_group_made_member_error:
error_is_already_exists: bool = (
"hashed_group_member_id" in create_group_made_member_error.message_dict
and any(
"already exists" in error
for error in create_group_made_member_error.message_dict[
"hashed_group_member_id"
]
)
)
if not error_is_already_exists:
raise
if not error_is_already_exists:
raise create_group_made_member_error from create_group_made_member_error

await ctx.followup.send(content="Successfully made you a member!", ephemeral=True)

Expand Down Expand Up @@ -303,58 +240,6 @@ async def member_count(self, ctx: "TeXBotApplicationContext") -> None: # type:
await ctx.defer(ephemeral=False)

async with ctx.typing():
http_session: aiohttp.ClientSession = aiohttp.ClientSession(
headers=REQUEST_HEADERS,
cookies=REQUEST_COOKIES,
)
async with http_session, http_session.get(BASE_MEMBERS_URL) as http_response:
response_html: str = await http_response.text()

member_list_div: bs4.Tag | bs4.NavigableString | None = BeautifulSoup(
response_html,
"html.parser",
).find(
"div",
{"class": "memberlistcol"},
)

if member_list_div is None or isinstance(member_list_div, bs4.NavigableString):
await self.command_send_error(
ctx=ctx,
error_code="E1041",
logging_message=OSError(
"The member count could not be retrieved from the MEMBERS_LIST_URL.",
),
)
return

if "showing 100 of" in member_list_div.text.lower():
member_count: str = member_list_div.text.split(" ")[3]
await ctx.followup.send(
content=f"{GROUP_NAME} has {member_count} members! :tada:",
)
return

member_table: bs4.Tag | bs4.NavigableString | None = BeautifulSoup(
response_html,
"html.parser",
).find(
"table",
{"id": "ctl00_ctl00_Main_AdminPageContent_gvMembers"},
)

if member_table is None or isinstance(member_table, bs4.NavigableString):
await self.command_send_error(
ctx=ctx,
error_code="E1041",
logging_message=OSError(
"The member count could not be retrieved from the MEMBERS_LIST_URL."
),
)
return

await ctx.followup.send(
content=f"{GROUP_NAME} has {
len(member_table.find_all('tr', {'class': ['msl_row', 'msl_altrow']}))
} members! :tada:"
content=f"{GROUP_NAME} has {await get_membership_count()} members! :tada:",
)
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ test = ["pytest>=8.3"]
type-check = ["django-stubs[compatible-mypy]>=5.1", "mypy>=1.13", "types-beautifulsoup4>=4.12"]

[project] # TODO: Remove [project] table once https://github.com/astral-sh/uv/issues/8582 is completed
dependencies = ["anyio>=4.9.0"]
name = "TeX-Bot-Py-V2"
requires-python = ">=3.12,<3.13" # TODO: Allow Python 3.13 once py-cord makes a new release with support for it
version = "0.1.0"
Expand Down
33 changes: 33 additions & 0 deletions utils/msl/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""MSL utility classes & functions provided for use across the whole of the project."""

from typing import TYPE_CHECKING

from .events import create_event, get_all_guild_events
from .finances import (
fetch_financial_transactions,
fetch_transaction_from_id,
get_account_balance,
)
from .memberships import get_full_membership_list, get_membership_count, is_student_id_member
from .reports import (
get_product_customisations,
get_product_sales,
update_current_year_sales_report,
)

if TYPE_CHECKING:
from collections.abc import Sequence

__all__: "Sequence[str]" = (
"create_event",
"fetch_financial_transactions",
"fetch_transaction_from_id",
"get_account_balance",
"get_all_guild_events",
"get_full_membership_list",
"get_membership_count",
"get_product_customisations",
"get_product_sales",
"is_student_id_member",
"update_current_year_sales_report",
)
119 changes: 119 additions & 0 deletions utils/msl/activities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
"""Module for fetching activities from the guild website."""

import logging
from enum import Enum
from typing import TYPE_CHECKING, Final

import aiohttp
import bs4
from bs4 import BeautifulSoup

from .core import BASE_HEADERS, ORGANISATION_ID, get_msl_context

if TYPE_CHECKING:
from collections.abc import Sequence
from datetime import datetime
from logging import Logger

__all__: "Sequence[str]" = ()

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


ACTIVITIES_URL: Final[str] = (
f"https://www.guildofstudents.com/organisation/admin/activities/all/{ORGANISATION_ID}/"
)

ACTIVITIES_BUTTON_KEY: Final[str] = "ctl00$ctl00$Main$AdminPageContent$fsFilter$btnSubmit"
ACTIVITIES_TABLE_ID: Final[str] = "ctl00_ctl00_Main_AdminPageContent_gvResults"
ACTIVITIES_START_DATE_KEY: Final[str] = "ctl00$ctl00$Main$AdminPageContent$drDates$txtFromDate"
ACTIVITIES_END_DATE_KEY: Final[str] = "ctl00$ctl00$Main$AdminPageContent$drDates$txtToDate"


class ActivityStatus(Enum):
"""
Enum to define the possible activity status values.

Submitted - The activity has been submitted and is pending approval.
Approved - The activity has been approved and is scheduled.
Draft - The activity is a draft and is not yet submitted.
Cancelled - The activity has been cancelled.
Queried - The activity has been queried and is pending response.
"""

SUBMITTED = "Submitted"
APPROVED = "Approved"
DRAFT = "Draft"
CANCELLED = "Cancelled"
QUERIED = "Queried"


async def fetch_guild_activities(from_date: "datetime", to_date: "datetime") -> dict[str, str]:
"""Fetch all activities on the guild website."""
data_fields, cookies = await get_msl_context(url=ACTIVITIES_URL)

form_data: dict[str, str] = {
ACTIVITIES_START_DATE_KEY: from_date.strftime("%d/%m/%Y"),
ACTIVITIES_END_DATE_KEY: to_date.strftime("%d/%m/%Y"),
ACTIVITIES_BUTTON_KEY: "Apply",
"__EVENTTARGET": "",
"__EVENTARGUMENT": "",
"__VIEWSTATEENCRYPTED": "",
}

data_fields.update(form_data)

data_fields.pop("ctl00$ctl00$Main$AdminPageContent$fsFilter$btnCancel")

session_v2: aiohttp.ClientSession = aiohttp.ClientSession(
headers=BASE_HEADERS,
cookies=cookies,
)
async with (
session_v2,
session_v2.post(url=ACTIVITIES_URL, data=data_fields) as http_response,
):
if http_response.status != 200:
logger.debug("Returned a non 200 status code!!")
logger.debug(http_response)
return {}

response_html: str = await http_response.text()

activities_table_html: bs4.Tag | bs4.NavigableString | None = BeautifulSoup(
markup=response_html,
features="html.parser",
).find(
name="table",
attrs={"id": ACTIVITIES_TABLE_ID},
)

if activities_table_html is None or isinstance(activities_table_html, bs4.NavigableString):
logger.warning("Failed to find the activities table.")
logger.debug(response_html)
return {}

if "There are no activities" in str(activities_table_html):
logger.debug("No activities were found matching the date range.")
return {}

activities_list: list[bs4.Tag] = activities_table_html.find_all(name="tr")

activities_list.pop(0)

return {
activity.find(name="a").get("href").split("/")[7]: activity.find_all(name="td")[ # type: ignore[union-attr]
1
].text.strip()
for activity in activities_list
}


async def create_activity() -> int:
"""Create an activity on the guild website."""
raise NotImplementedError


async def fetch_activity(activity_id: int) -> dict[str, str]:
"""Fetch a specific activity from the guild website."""
raise NotImplementedError
Loading