myfap 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
myfap/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """
2
+ MyFAP API CLI - Khởi tạo module
3
+ """
myfap/api.py ADDED
@@ -0,0 +1,226 @@
1
+ import requests
2
+ import html
3
+ from .auth import FAP_HOST, MAGIC_ID, create_checksum
4
+
5
+ def check_api_error(data):
6
+ if isinstance(data, dict):
7
+ code = str(data.get("code", ""))
8
+ if code == "201":
9
+ msg = data.get("message", "Lỗi API (Mã 201)")
10
+ raise Exception(f"FAP API báo lỗi: {msg}")
11
+ if "data" in data and not data.get("data"): # Báo lỗi nếu data rỗng hoặc null
12
+ raise Exception("Không có dữ liệu (Data rỗng) hoặc bạn chưa hoặc truyền sai flag --semester")
13
+ return data
14
+
15
+ def clean_json(data):
16
+ """Giải mã các ký tự HTML Entity (ô, á, ...) do server ASP.NET trả về"""
17
+ if isinstance(data, dict):
18
+ return {k: clean_json(v) for k, v in data.items()}
19
+ elif isinstance(data, list):
20
+ return [clean_json(v) for v in data]
21
+ elif isinstance(data, str):
22
+ return html.unescape(data)
23
+ return data
24
+
25
+
26
+ class MyFapEssential:
27
+ def __init__(self, auth=None):
28
+ self.auth = auth
29
+ self.session = requests.Session()
30
+ self.session.headers.update({"User-Agent": "okhttp/4.9.2"})
31
+
32
+ def get_campuses(self):
33
+ """Lấy danh sách các cơ sở (không cần authenKey)"""
34
+ url = f"{FAP_HOST}/GetAllActiveCampus"
35
+ r = self.session.get(url)
36
+ if r.status_code == 200:
37
+ return clean_json(check_api_error(r.json())).get('data', [])
38
+ return []
39
+
40
+ def get_semesters(self, campus: str = None):
41
+ """Lấy danh sách các kỳ học. Yêu cầu đối tượng auth hợp lệ."""
42
+ if not self.auth:
43
+ raise Exception("Lỗi: Cần đối tượng auth để lấy danh sách kỳ học.")
44
+
45
+ target_campus = campus or self.auth.campus
46
+ checksum = create_checksum(MAGIC_ID, target_campus)
47
+ url = f"{FAP_HOST}/GetSemester?campusCode={target_campus}&Authen={self.auth.authen_key}&checksum={checksum}"
48
+ r = self.session.get(url)
49
+ if r.status_code == 200:
50
+ semesters = check_api_error(r.json()).get('data', [])
51
+ semesters.sort(key=lambda x: x['termID'], reverse=True)
52
+ return semesters
53
+ raise Exception(f"Lỗi lấy danh sách kỳ học: {r.text}")
54
+
55
+ def get_week(self, date_str: str):
56
+ """Lấy số tuần tương ứng với ngày (YYYY-MM-DD)"""
57
+ url = f"{FAP_HOST}/GetWeekByDate?date={date_str}"
58
+ r = self.session.get(url)
59
+ if r.status_code == 200:
60
+ return int(check_api_error(r.json()).get("data", 0))
61
+ raise Exception(f"Lỗi lấy tuần: {r.text}")
62
+
63
+ def get_news(self, news_type: int = 1):
64
+ """Lấy 10 thông báo gần nhất (gửi toàn trường)"""
65
+ if not self.auth:
66
+ raise Exception("Lỗi: Không tìm thấy session xác thực (auth).")
67
+ # Identifier cho API này chính là giá trị của 'type'
68
+ checksum = create_checksum(str(news_type), self.auth.campus)
69
+ url = f"{FAP_HOST}/GetTop10News?campusCode={self.auth.campus}&Authen={self.auth.authen_key}&type={news_type}&checksum={checksum}"
70
+ r = self.session.get(url)
71
+ if r.status_code == 200:
72
+ return clean_json(check_api_error(r.json()))
73
+ raise Exception(f"Lỗi lấy thông báo: {r.text}")
74
+
75
+ class MyFapOther:
76
+ def __init__(self, auth):
77
+ self.auth = auth
78
+ self.session = requests.Session()
79
+ self.session.headers.update({"User-Agent": "okhttp/4.9.2"})
80
+
81
+ def get_required_survey(self):
82
+ """Lấy các survey chưa thực hiện"""
83
+ if not getattr(self.auth, 'email', None):
84
+ raise Exception("Lỗi: Không tìm thấy email trong session. Vui lòng chạy 'myfap login' lại để lấy thông tin.")
85
+ # Identifier cho API này là email (username)
86
+ checksum = create_checksum(self.auth.email, self.auth.campus)
87
+ url = f"https://survey.fpt.edu.vn/API/myFAP/GetRequiredSurvey?username={self.auth.email}&checksum={checksum}"
88
+ r = self.session.get(url)
89
+ if r.status_code == 200:
90
+ return clean_json(check_api_error(r.json()))
91
+ raise Exception(f"Lỗi lấy Survey: {r.text}")
92
+
93
+ def check_open_feedback(self):
94
+ """Kiểm tra Feedback"""
95
+ checksum = create_checksum(self.auth.mssv, self.auth.campus)
96
+ url = f"{FAP_HOST}/CheckOpenFeedBack?campusCode={self.auth.campus}&rollNumber={self.auth.mssv}&Authen={self.auth.authen_key}&checksum={checksum}"
97
+ r = self.session.get(url)
98
+ if r.status_code == 200:
99
+ return clean_json(check_api_error(r.json()))
100
+ raise Exception(f"Lỗi lấy Feedback: {r.text}")
101
+
102
+ def check_update_profile(self):
103
+ """Kiểm tra Update Profile"""
104
+ checksum = create_checksum(self.auth.mssv, self.auth.campus)
105
+ url = f"{FAP_HOST}/CheckUpdateProfile?campusCode={self.auth.campus}&rollNumber={self.auth.mssv}&Authen={self.auth.authen_key}&checksum={checksum}"
106
+ r = self.session.get(url)
107
+ if r.status_code == 200:
108
+ return clean_json(check_api_error(r.json()))
109
+ raise Exception(f"Lỗi lấy Update Profile: {r.text}")
110
+
111
+ def get_notification(self):
112
+ """Lấy thông báo qua MSSV"""
113
+ checksum = create_checksum(self.auth.mssv, self.auth.campus)
114
+ url = f"{FAP_HOST}/GetNotificationByRoll?campusCode={self.auth.campus}&rollNumber={self.auth.mssv}&Authen={self.auth.authen_key}&checksum={checksum}"
115
+ r = self.session.get(url)
116
+ if r.status_code == 200:
117
+ return clean_json(check_api_error(r.json()))
118
+ raise Exception(f"Lỗi lấy Notification: {r.text}")
119
+
120
+ def get_fee(self):
121
+ """Lấy danh sách học phí chưa thanh toán"""
122
+ checksum = create_checksum(self.auth.mssv, self.auth.campus)
123
+ url = f"{FAP_HOST}/GetFeeByRoll?campusCode={self.auth.campus}&rollNumber={self.auth.mssv}&Authen={self.auth.authen_key}&checksum={checksum}"
124
+ r = self.session.get(url)
125
+ if r.status_code == 200:
126
+ return clean_json(check_api_error(r.json()))
127
+ if r.status_code == 404:
128
+ return {"status": "Không có khoản nợ học phí nào cần thanh toán."}
129
+ raise Exception(f"Lỗi lấy Fee: {r.text}")
130
+
131
+ class MyFapClient:
132
+ def __init__(self, auth):
133
+ self.auth = auth
134
+ self.session = requests.Session()
135
+ self.session.headers.update({"User-Agent": "okhttp/4.9.2"})
136
+
137
+ def get_schedule(self, semester: str):
138
+ """Lấy lịch học theo kỳ"""
139
+ checksum = create_checksum(self.auth.mssv, self.auth.campus)
140
+ url = f"{FAP_HOST}/GetActivityStudent?campusCode={self.auth.campus}&rollNumber={self.auth.mssv}&Semester={semester}&Authen={self.auth.authen_key}&checksum={checksum}"
141
+ r = self.session.get(url)
142
+ if r.status_code == 200:
143
+ return clean_json(check_api_error(r.json()))
144
+ raise Exception(f"Lỗi lấy lịch học: {r.text}")
145
+
146
+ def get_marks(self, semester: str):
147
+ """Lấy bảng điểm theo kỳ"""
148
+ checksum = create_checksum(self.auth.mssv, self.auth.campus)
149
+ url = f"{FAP_HOST}/GetStudentMark?campusCode={self.auth.campus}&rollNumber={self.auth.mssv}&Semester={semester}&Authen={self.auth.authen_key}&checksum={checksum}"
150
+ r = self.session.get(url)
151
+ if r.status_code == 200:
152
+ return clean_json(check_api_error(r.json()))
153
+ raise Exception(f"Lỗi lấy bảng điểm: {r.text}")
154
+
155
+ def get_mark_by_course(self, course_id: str):
156
+ """Lấy điểm chi tiết của một môn học"""
157
+ checksum = create_checksum(self.auth.mssv, self.auth.campus)
158
+ url = f"{FAP_HOST}/GetMarkByCourse?campusCode={self.auth.campus}&CourseId={course_id}&rollNumber={self.auth.mssv}&Authen={self.auth.authen_key}&checksum={checksum}"
159
+ r = self.session.get(url)
160
+ if r.status_code == 200:
161
+ return clean_json(check_api_error(r.json()))
162
+ raise Exception(f"Lỗi lấy điểm môn học: {r.text}")
163
+
164
+ def get_exams(self, semester: str):
165
+ """Lấy lịch thi theo kỳ"""
166
+ checksum = create_checksum(self.auth.mssv, self.auth.campus)
167
+ url = f"{FAP_HOST}/GetScheduleExam?campusCode={self.auth.campus}&rollNumber={self.auth.mssv}&Authen={self.auth.authen_key}&checksum={checksum}&Semester={semester}"
168
+ r = self.session.get(url)
169
+ if r.status_code == 200:
170
+ return clean_json(check_api_error(r.json()))
171
+ raise Exception(f"Lỗi lấy lịch thi: {r.text}")
172
+
173
+ def get_schedule_by_week(self, week: int, semester: str, year: int):
174
+ """Lấy lịch học theo tuần"""
175
+ checksum = create_checksum(self.auth.mssv, self.auth.campus)
176
+ url = f"{FAP_HOST}/GetActivityStudentByWeek?campusCode={self.auth.campus}&week={week}&rollNumber={self.auth.mssv}&Semester={semester}&year={year}&Authen={self.auth.authen_key}&checksum={checksum}"
177
+ r = self.session.get(url)
178
+ if r.status_code == 200:
179
+ return clean_json(check_api_error(r.json()))
180
+ raise Exception(f"Lỗi lấy lịch học tuần: {r.text}")
181
+
182
+ def get_attendances(self, semester: str):
183
+ """Lấy danh sách trạng thái điểm danh tổng quát của kỳ"""
184
+ checksum = create_checksum(self.auth.mssv, self.auth.campus)
185
+ url = f"{FAP_HOST}/GetStudentAttendances?campusCode={self.auth.campus}&Semester={semester}&rollNumber={self.auth.mssv}&Authen={self.auth.authen_key}&checksum={checksum}"
186
+ r = self.session.get(url)
187
+ if r.status_code == 200:
188
+ return clean_json(check_api_error(r.json()))
189
+ raise Exception(f"Lỗi lấy trạng thái điểm danh: {r.text}")
190
+
191
+ def get_course_attendance(self, semester: str, subject_code: str, class_name: str):
192
+ """Lấy chi tiết điểm danh của một môn học"""
193
+ checksum = create_checksum(self.auth.mssv, self.auth.campus)
194
+ url = f"{FAP_HOST}/getCourseAttendance?campusCode={self.auth.campus}&rollNumber={self.auth.mssv}&Semester={semester}&SubjectCode={subject_code}&ClassName={class_name}&Authen={self.auth.authen_key}&checksum={checksum}"
195
+ r = self.session.get(url)
196
+ if r.status_code == 200:
197
+ return clean_json(check_api_error(r.json()))
198
+ raise Exception(f"Lỗi lấy chi tiết điểm danh môn học: {r.text}")
199
+
200
+ def get_applications(self):
201
+ """Lấy danh sách các đơn từ đã gửi cho trường"""
202
+ checksum = create_checksum(self.auth.mssv, self.auth.campus)
203
+ url = f"{FAP_HOST}/GetApplication?campusCode={self.auth.campus}&rollNumber={self.auth.mssv}&Authen={self.auth.authen_key}&checksum={checksum}"
204
+ r = self.session.get(url)
205
+ if r.status_code == 200:
206
+ return clean_json(check_api_error(r.json()))
207
+ raise Exception(f"Lỗi lấy danh sách đơn từ: {r.text}")
208
+
209
+ def get_student_info(self):
210
+ """Lấy thông tin sinh viên"""
211
+ checksum = create_checksum(self.auth.mssv, self.auth.campus)
212
+ url = f"{FAP_HOST}/GetStudentById?campusCode={self.auth.campus}&rollNumber={self.auth.mssv}&Authen={self.auth.authen_key}&checksum={checksum}"
213
+ r = self.session.get(url)
214
+ if r.status_code == 200:
215
+ return clean_json(check_api_error(r.json()))
216
+ raise Exception(f"Lỗi lấy thông tin sinh viên: {r.text}")
217
+
218
+ def get_campus_info(self):
219
+ """Lấy thông tin phòng Dịch vụ sinh viên"""
220
+ checksum = create_checksum(self.auth.mssv, self.auth.campus)
221
+ url = f"{FAP_HOST}/GetCampusInfo?campusCode={self.auth.campus}&rollNumber={self.auth.mssv}&Authen={self.auth.authen_key}&checksum={checksum}"
222
+ r = self.session.get(url)
223
+ if r.status_code == 200:
224
+ return clean_json(check_api_error(r.json()))
225
+ raise Exception(f"Lỗi lấy thông tin campus: {r.text}")
226
+
myfap/auth.py ADDED
@@ -0,0 +1,280 @@
1
+ import time
2
+ import json
3
+ import base64
4
+ import hashlib
5
+ import hmac
6
+ import requests
7
+ import pkce
8
+ from pathlib import Path
9
+ from datetime import datetime
10
+ import pytz
11
+ from playwright.sync_api import sync_playwright
12
+
13
+ # Import biến từ file ngoài
14
+ from .auth_var import FEID_HOST, FAP_HOST, CLIENT_ID, REDIRECT_URI, SECRET_KEY, MAGIC_ID
15
+ CONFIG_DIR = Path.home() / ".myfap-api-cli"
16
+ SESSION_FILE = CONFIG_DIR / "session.json"
17
+
18
+ def create_checksum(identifier: str, campus: str) -> str:
19
+ tz = pytz.timezone('Asia/Ho_Chi_Minh')
20
+ time_str = datetime.now(tz).strftime("%d/%m/%Y %H") + ":00"
21
+ raw = f"{identifier}MYFAP{campus}{time_str}"
22
+ sig = hmac.new(SECRET_KEY.encode(), raw.encode(), hashlib.sha1).digest()
23
+ return base64.b64encode(sig).decode()
24
+
25
+ class MyFapAuth:
26
+ def __init__(self, campus="APHL"):
27
+ self.campus = campus
28
+ self.code_verifier, self.code_challenge = pkce.generate_pkce_pair()
29
+ self.jwt_token = None
30
+ self.refresh_token = None
31
+ self.authen_key = None
32
+ self.mssv = None
33
+ self.email = None
34
+
35
+ # Thiết lập session với User-Agent mạo danh app mobile FPT
36
+ self.session = requests.Session()
37
+ self.session.headers.update({"User-Agent": "okhttp/4.9.2"})
38
+
39
+ def save_session(self):
40
+ data = {
41
+ "authen_key": self.authen_key,
42
+ "mssv": self.mssv,
43
+ "email": self.email,
44
+ "jwt_token": self.jwt_token,
45
+ "refresh_token": self.refresh_token,
46
+ "campus": self.campus
47
+ }
48
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
49
+ with open(SESSION_FILE, "w", encoding="utf-8") as f:
50
+ json.dump(data, f, indent=4)
51
+
52
+ def load_session(self):
53
+ if not SESSION_FILE.exists():
54
+ return False
55
+ try:
56
+ with open(SESSION_FILE, "r", encoding="utf-8") as f:
57
+ data = json.load(f)
58
+
59
+ self.authen_key = data.get("authen_key")
60
+ self.mssv = data.get("mssv")
61
+ self.email = data.get("email")
62
+ self.jwt_token = data.get("jwt_token")
63
+ self.refresh_token = data.get("refresh_token")
64
+ # Campus có thể ghi đè bằng flag CLI
65
+ # self.campus = data.get("campus", self.campus)
66
+
67
+ if not self.authen_key: return False
68
+
69
+ # Kiểm tra session còn sống không bằng cách gọi thử API
70
+ checksum = create_checksum(MAGIC_ID, self.campus)
71
+ url = f"{FAP_HOST}/GetSemester?campusCode={self.campus}&Authen={self.authen_key}&checksum={checksum}"
72
+ r = self.session.get(url, timeout=5)
73
+
74
+ if r.status_code == 200:
75
+ resp = r.json()
76
+ if str(resp.get("code", "")) == "201":
77
+ return self.refresh_session()
78
+ return True
79
+ return self.refresh_session()
80
+ except:
81
+ return self.refresh_session()
82
+
83
+
84
+ def refresh_session(self):
85
+ if not self.refresh_token: return False
86
+ try:
87
+ print("[*] Phiên đăng nhập hết hạn. Đang làm mới (Refresh Token)...")
88
+ r = self.session.post(f"{FEID_HOST}/connect/token", data={
89
+ "client_id": CLIENT_ID,
90
+ "refresh_token": self.refresh_token,
91
+ "grant_type": "refresh_token"
92
+ })
93
+ if r.status_code == 200:
94
+ resp = r.json()
95
+ self.jwt_token = resp.get('access_token', self.jwt_token)
96
+ if 'refresh_token' in resp:
97
+ self.refresh_token = resp['refresh_token']
98
+
99
+ # Handshake lại để lấy authen_key mới
100
+ checksum = create_checksum(MAGIC_ID, self.campus)
101
+ r_handshake = self.session.post(
102
+ f"{FAP_HOST}/AuthenticationByFeId?campusCode={self.campus}&checksum={checksum}",
103
+ json={"token": self.jwt_token},
104
+ headers={"Authorization": f"Bearer {self.jwt_token}", "Content-Type": "application/json"}
105
+ )
106
+ if r_handshake.status_code == 200:
107
+ resp_hs = r_handshake.json()
108
+ if 'data' in resp_hs and len(resp_hs['data']) > 0:
109
+ self.authen_key = resp_hs['data'][0]['authenKey']
110
+ self.save_session()
111
+ print("[*] Làm mới phiên đăng nhập thành công!")
112
+ return True
113
+ print("[!] Refresh Token thất bại. Vui lòng đăng nhập lại.")
114
+ return False
115
+ except Exception as e:
116
+ print(f"[!] Lỗi khi Refresh Token: {e}")
117
+ return False
118
+
119
+ def login(self):
120
+ print("[*] Đang khởi chạy Playwright...")
121
+ auth_url = (f"{FEID_HOST}/connect/authorize?client_id={CLIENT_ID}"
122
+ f"&redirect_uri={REDIRECT_URI}&response_type=code"
123
+ f"&scope=openid%20profile%20offline_access%20identity-service"
124
+ f"&code_challenge={self.code_challenge}&code_challenge_method=S256")
125
+ code = None
126
+
127
+ with sync_playwright() as p:
128
+ # Ưu tiên mở bằng Chrome có sẵn trên máy, fallback sang Edge
129
+ # Thêm cờ để bypass tính năng nhận diện bot của Google (Couldn't sign you in)
130
+ custom_args = ['--disable-blink-features=AutomationControlled']
131
+ try:
132
+ browser = p.chromium.launch(
133
+ channel="chrome",
134
+ headless=False,
135
+ args=custom_args,
136
+ ignore_default_args=["--enable-automation"]
137
+ )
138
+ except Exception:
139
+ try:
140
+ browser = p.chromium.launch(
141
+ channel="msedge",
142
+ headless=False,
143
+ args=custom_args,
144
+ ignore_default_args=["--enable-automation"]
145
+ )
146
+ except Exception:
147
+ # Nếu cả Chrome và Edge đều không có, dùng trình duyệt tải kèm playwright
148
+ browser = p.firefox.launch(headless=False)
149
+
150
+ context = browser.new_context(
151
+ user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36"
152
+ )
153
+ page = context.new_page()
154
+
155
+ def handle_response(response):
156
+ nonlocal code
157
+ if 300 <= response.status < 400:
158
+ location = response.headers.get("location", "")
159
+ if location and "io.identityserver.demo" in location and "code=" in location:
160
+ try:
161
+ import urllib.parse as urlparse
162
+ parsed = urlparse.urlparse(location)
163
+ code = urlparse.parse_qs(parsed.query)['code'][0]
164
+ except:
165
+ code = location.split("code=")[1].split("&")[0]
166
+
167
+ def handle_request(request):
168
+ nonlocal code
169
+ url = request.url
170
+ if "io.identityserver.demo" in url and "code=" in url:
171
+ try:
172
+ import urllib.parse as urlparse
173
+ parsed = urlparse.urlparse(url)
174
+ code = urlparse.parse_qs(parsed.query)['code'][0]
175
+ except:
176
+ code = url.split("code=")[1].split("&")[0]
177
+
178
+ page.on("response", handle_response)
179
+ page.on("request", handle_request)
180
+
181
+ print(">>> VUI LÒNG ĐĂNG NHẬP TRÊN CỬA SỔ TRÌNH DUYỆT <<<")
182
+ try:
183
+ page.goto(auth_url)
184
+ except Exception:
185
+ pass
186
+
187
+ start_time = time.time()
188
+ while not code and (time.time() - start_time) < 120:
189
+ try:
190
+ page.wait_for_timeout(500)
191
+ except:
192
+ break
193
+ browser.close()
194
+
195
+ if not code:
196
+ raise Exception("Lỗi: Không lấy được Code từ FEID (timeout hoặc trình duyệt bị đóng).")
197
+
198
+ print("[*] Lấy token từ Code...")
199
+ r = self.session.post(f"{FEID_HOST}/connect/token", data={
200
+ "client_id": CLIENT_ID, "code": code, "redirect_uri": REDIRECT_URI,
201
+ "code_verifier": self.code_verifier, "grant_type": "authorization_code"
202
+ })
203
+
204
+ if r.status_code != 200:
205
+ raise Exception(f"Lỗi lấy JWT Token: {r.text}")
206
+
207
+ self.jwt_token = r.json()['access_token']
208
+ self.refresh_token = r.json().get('refresh_token')
209
+
210
+ # Lấy MSSV và email từ Token
211
+ parts = self.jwt_token.split('.')
212
+ payload_str = parts[1] + "=" * ((4 - len(parts[1]) % 4) % 4)
213
+ payload = json.loads(base64.b64decode(payload_str))
214
+ self.email = payload.get('email', '')
215
+ try:
216
+ proj_str = payload.get('projectCampuses', '[]')
217
+ projects = json.loads(proj_str)
218
+ found = False
219
+ for p in projects:
220
+ if p.get('CampusCode') == self.campus:
221
+ self.mssv = p.get('RollNumber')
222
+ found = True
223
+ break
224
+ if not found and projects: self.mssv = projects[0].get('RollNumber')
225
+ print(f"[*] Xin chào sinh viên: {self.mssv}")
226
+ except:
227
+ self.mssv = input("[?] Không tìm thấy MSSV tự động. Nhập MSSV thủ công: ")
228
+
229
+ print("[*] Handshake với FAP API để lấy authenKey...")
230
+ checksum = create_checksum(MAGIC_ID, self.campus)
231
+ r = self.session.post(
232
+ f"{FAP_HOST}/AuthenticationByFeId?campusCode={self.campus}&checksum={checksum}",
233
+ json={"token": self.jwt_token},
234
+ headers={"Authorization": f"Bearer {self.jwt_token}", "Content-Type": "application/json"}
235
+ )
236
+ if r.status_code == 200:
237
+ resp = r.json()
238
+ if 'data' in resp and len(resp['data']) > 0:
239
+ self.authen_key = resp['data'][0]['authenKey']
240
+ print("[*] Handshake thành công!")
241
+ return self.authen_key, self.mssv
242
+ else:
243
+ raise Exception(f"Lỗi handshake: {resp}")
244
+ else:
245
+ raise Exception(f"API Error {r.status_code}: {r.text}")
246
+
247
+ if __name__ == "__main__":
248
+ print("--- BẮT ĐẦU TEST AUTH VÀ GỌI API ---")
249
+ campus = "APHL"
250
+ auth = MyFapAuth(campus=campus)
251
+
252
+ try:
253
+ authen_key, mssv = auth.login()
254
+
255
+ print("\n--- TEST LẤY DANH SÁCH KỲ HỌC (GetSemester) ---")
256
+ checksum = create_checksum(MAGIC_ID, campus)
257
+ url_sem = f"{FAP_HOST}/GetSemester?campusCode={campus}&Authen={authen_key}&checksum={checksum}"
258
+ r_sem = requests.get(url_sem)
259
+ semester = "Fall2025"
260
+ if r_sem.status_code == 200:
261
+ semesters = r_sem.json().get('data', [])
262
+ semesters.sort(key=lambda x: x['termID'], reverse=True)
263
+ if semesters:
264
+ semester = semesters[0]['semesterName']
265
+ print(f"[*] Kỳ học hiện tại: {semester}")
266
+
267
+ print("\n--- TEST API GetActivityStudent ---")
268
+ checksum_api = create_checksum(mssv, campus)
269
+ url_act = f"{FAP_HOST}/GetActivityStudent?campusCode={campus}&rollNumber={mssv}&Authen={authen_key}&checksum={checksum_api}&Semester={semester}"
270
+ r_act = requests.get(url_act)
271
+
272
+ if r_act.status_code == 200:
273
+ data = r_act.json()
274
+ print("[*] Gọi thành công! Dữ liệu mẫu (chỉ in vài dòng đầu):")
275
+ print(json.dumps(data, indent=2, ensure_ascii=False)[:600] + "\n...")
276
+ else:
277
+ print(f"[!] Lỗi gọi API: {r_act.text}")
278
+
279
+ except Exception as e:
280
+ print(f"[!] Lỗi: {e}")
myfap/auth_var.py ADDED
@@ -0,0 +1,16 @@
1
+ import base64
2
+
3
+ # encode sang Base64 để né search engine bot hoặc ai scrapper (méo bt có né dc ko)
4
+ _FEID_HOST_B64 = b'aHR0cHM6Ly9mZWlkLmZwdC5lZHUudm4='
5
+ _FAP_HOST_B64 = b'aHR0cHM6Ly9hcGkuZnB0LmVkdS52bi9mYXAvYXBpL015RkFQ'
6
+ _CLIENT_ID_B64 = b'ZmFwLW1vYmlsZS1mcm9udC1lbmQ='
7
+ _REDIRECT_URI_B64 = b'aW8uaWRlbnRpdHlzZXJ2ZXIuZGVtbzovb2F1dGhyZWRpcmVjdA=='
8
+ _SECRET_KEY_B64 = b'bjRBU3Nia2FXNmRkaElrRjBpcGlObXhJTW56aXgzbFJlNjJzMm1Ub2JLbmsyZW5BMmVvWlFNeWJGM2dlTGNONVV3MGxSM05YemJnZDltUUgwMHFzTndiQ0haVzBmT00wOHRBRmZjUzBBQXpQRnVjdGxKZU1WdXF4dUdOMmZOUlY='
9
+ _MAGIC_ID_B64 = b'VGxLaUEwMzQwcFk2SGtpbzRrYVRMRk12eEs3R0lPbHI2eHFWN21WQUk0YlJjaDdzZmpPT3g3Rm5JcFYxZHd2dmVIMGo1eHNSektsUkQ2c3FOT0F5MEc0OTJjbVFCNXhsSVFORmlYeVMyOHBYVlhUTjdFbXk3N3ZITmFzMmtMcEU='
10
+
11
+ FEID_HOST = base64.b64decode(_FEID_HOST_B64).decode()
12
+ FAP_HOST = base64.b64decode(_FAP_HOST_B64).decode()
13
+ CLIENT_ID = base64.b64decode(_CLIENT_ID_B64).decode()
14
+ REDIRECT_URI = base64.b64decode(_REDIRECT_URI_B64).decode()
15
+ SECRET_KEY = base64.b64decode(_SECRET_KEY_B64).decode()
16
+ MAGIC_ID = base64.b64decode(_MAGIC_ID_B64).decode()