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 +3 -0
- myfap/api.py +226 -0
- myfap/auth.py +280 -0
- myfap/auth_var.py +16 -0
- myfap/cli.py +535 -0
- myfap/parsers.py +116 -0
- myfap-0.1.0.dist-info/METADATA +214 -0
- myfap-0.1.0.dist-info/RECORD +12 -0
- myfap-0.1.0.dist-info/WHEEL +5 -0
- myfap-0.1.0.dist-info/entry_points.txt +2 -0
- myfap-0.1.0.dist-info/licenses/LICENSE +21 -0
- myfap-0.1.0.dist-info/top_level.txt +1 -0
myfap/__init__.py
ADDED
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()
|