Skip to content

Instantly share code, notes, and snippets.

@mnixry
Last active February 26, 2024 14:57
Show Gist options
  • Save mnixry/364bb3fdf0694c13b83d80feb9abc720 to your computer and use it in GitHub Desktop.
Save mnixry/364bb3fdf0694c13b83d80feb9abc720 to your computer and use it in GitHub Desktop.
Generate iCalendar file for BUPT class schedule.
"""
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