Last active
February 26, 2024 14:57
-
-
Save mnixry/364bb3fdf0694c13b83d80feb9abc720 to your computer and use it in GitHub Desktop.
Generate iCalendar file for BUPT class schedule.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
Generate iCalendar file for BUPT class schedule. | |
Usage: | |
python this_file.py <username> <password> | |
Note: | |
1. You need to install the following packages: | |
- httpx | |
- ics | |
- cryptography | |
2. Username and password should be as same as jwgl.bupt.edu.cn. | |
3. The output will be printed to the console. | |
Author: Mix | |
""" | |
import datetime | |
import json | |
from base64 import b64encode | |
from logging import getLogger | |
from typing import Any | |
from zoneinfo import ZoneInfo | |
import httpx | |
import ics | |
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes | |
from cryptography.hazmat.primitives.padding import PKCS7 | |
from ics.grammar.parse import ContentLine | |
logger = getLogger(__name__) | |
class BUPTClassCalendar: | |
def __init__(self, username: str, password: str, *, max_week: int = 20): | |
self.client = httpx.Client( | |
base_url="https://jwglweixin.bupt.edu.cn/", | |
) | |
self.username = username | |
self.password = password | |
self.max_week = max_week | |
@staticmethod | |
def _password_encrypt(password: str) -> str: | |
algorithm = algorithms.AES(b"qzkj1kjghd=876&*") | |
encryptor = Cipher( | |
algorithm, | |
modes.ECB(), | |
).encryptor() | |
padder = PKCS7(algorithm.block_size).padder() | |
encrypted_password = b64encode( | |
b64encode( | |
encryptor.update( | |
padder.update(json.dumps(password).encode()) + padder.finalize() | |
) | |
+ encryptor.finalize() | |
) | |
).decode() | |
return encrypted_password | |
def login(self): | |
encrypted_password = self._password_encrypt(self.password) | |
response = self.client.post( | |
"/bjyddx/login", | |
params={ | |
"userNo": self.username, | |
"pwd": encrypted_password, | |
"encode": 1, | |
"captchaData": "", | |
"codeVal": "", | |
}, | |
) | |
response.raise_for_status() | |
response_body: dict[str, Any] = response.json() | |
logger.debug("Login Response: %s", response_body) | |
if response_body["code"] != "1": | |
raise RuntimeError(response_body["Msg"]) | |
self.client.headers["Token"] = response_body["data"]["token"] | |
return | |
@staticmethod | |
def _course_time(time_str: str, timezone: str = "Asia/Shanghai"): | |
tz = ZoneInfo(timezone) | |
return datetime.time.fromisoformat(time_str + ":00").replace(tzinfo=tz) | |
def events(self): | |
calender = ics.Calendar() | |
course_dedup = set() | |
for current_week in range(1, self.max_week): | |
response = self.client.post( | |
"/bjyddx/student/curriculum", params={"week": current_week} | |
) | |
try: | |
data, *_ = response.json()["data"] | |
courses = data["courses"] | |
date = data["date"] | |
date_dict = {d["xqid"]: d["mxrq"] for d in date} | |
except Exception: | |
logger.exception("Error when getting week %s: ", current_week) | |
break | |
logger.debug("Week %s", current_week) | |
for course in courses: | |
course: dict[str, Any] | |
course_id = str(course["jx0408id"]) | |
if course_id in course_dedup: | |
continue | |
course_dedup.add(course_id) | |
event = ics.Event() | |
event.name = course["courseName"] | |
event.uid = course_id | |
logger.debug("Course: %s: %s", course_id, event.name) | |
date = datetime.date.fromisoformat(date_dict[course["weekDay"].replace('7','0')]) | |
start_time = self._course_time(course["startTime"]) | |
end_time = self._course_time(course["endTIme"]) | |
class_weeks = [ | |
int(w) for w in course["classWeekDetails"].split(",") if w | |
] | |
max_week = max(class_weeks) | |
until = datetime.datetime.combine( | |
date + datetime.timedelta(weeks=max_week - current_week), end_time | |
).strftime("%Y%m%dT%H%M%S") | |
event.begin = datetime.datetime.combine(date, start_time) | |
event.end = datetime.datetime.combine(date, end_time) | |
event.location = course["classroomName"] | |
description = "" | |
for name, value in course.items(): | |
if not isinstance(value, str): | |
continue | |
# camelCase to Normalized Name | |
normalized_name = ( | |
"".join([f" {c}" if c.isupper() else c for c in name]) | |
.strip() | |
.title() | |
) | |
description += f"{normalized_name}: {value}\n" | |
event.description = description | |
event.extra.append( | |
ContentLine( | |
name="RRULE", | |
value=f"FREQ=WEEKLY;INTERVAL=1;UNTIL={until}", | |
) | |
) | |
calender.events.add(event) | |
return calender | |
if __name__ == "__main__": | |
import sys | |
logger.setLevel("DEBUG") | |
try: | |
from pip._vendor.rich.logging import RichHandler | |
logger.addHandler(RichHandler()) | |
except ImportError: | |
pass | |
program, *args = sys.argv | |
if len(args) != 2: | |
print(f"Usage: {program} <username> <password>") | |
sys.exit(1) | |
username, password = args | |
calendar = BUPTClassCalendar(username, password) | |
calendar.login() | |
cal = calendar.events() | |
print("".join(cal.serialize_iter())) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment