-
Notifications
You must be signed in to change notification settings - Fork 1
/
erp.py
executable file
·445 lines (361 loc) · 13.4 KB
/
erp.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
#!/usr/bin/env python3
import os
from base64 import b64decode
from io import BytesIO
from json import dumps, loads
from re import DOTALL, search
from typing import Dict, List
import pytesseract as loki
import requests
from bs4 import BeautifulSoup
from decouple import config
from PIL import Image
from telegram import TG
api_key = config("TELEGRAM_API_KEY")
chat_id = config("TELEGRAM_CHAT_ID")
tg = TG(api_key)
def log(
user: str,
data: str,
captcha: str,
param: str,
captcha_image: Image,
login_response: str,
):
"""
A function to parse some data and send it to our log channel on Telegram
:param user: ERP User ID
:param data: HTML content of the erroneous page
:param captcha: The last tried captcha text
:param param: The name of the asp document that was being accessed
:param captcha_image: The base64encoded captcha image
:param login_response: The response to the initial login request
:return: Nothing, tbh
"""
if api_key is None or chat_id is None:
return
document = f"{user}.html"
with open(document, "w") as f:
f.write(data)
login_document = "login_response.html"
with open(login_document, "w") as f:
f.write(login_response)
image_file = f"{captcha}.png"
with open(image_file, "wb") as f:
f.write(b64decode(captcha_image))
tg.send_message(chat_id, f"<b>Poseidon</b>:\nUser {user}")
tg.send_image(chat_id, image_file, f"Captcha: {captcha}")
tg.send_document(chat_id, param, document)
tg.send_document(chat_id, "Login Response", login_document)
os.remove(document)
os.remove(image_file)
"""
Required for login to ERP
chkCheck is pretty simple, just checking the "I am not a robot checkbox"
__VIEWSTATE,__VIEWSTATEGENERATOR are for hidden values in form
"""
payload = {
"__VIEWSTATE": "/wEPDwULLTE5ODI5MDAxMzMPFgIeDkxPR0lOX0JBU0VEX09OZRYCAgEPZBYCAgMPZBYCZg9kFgYCDw9kFgICAQ8QZA8WAWYWARAFA1dQVQUDV1BVZxYBZmQCEw8PFgIeB0VuYWJsZWRnZGQCGQ9kFgICAQ8QZGQWAWZkGAEFHl9fQ29udHJvbHNSZXF1aXJlUG9zdEJhY2tLZXlfXxYBBQhjaGtDaGVja2gyHD2KOtO872SDtv2lNsGjExgEtGQ3xECszuQAb07J",
"__EVENTTARGET": "btnLogin",
"chkCheck": "on",
"__VIEWSTATEGENERATOR": "B8B84CAE",
}
# Just to easily get error messages, because these can be repeated
ERRORS = {
"e": "ERP is down!",
"w": "Wrong credentials!",
"c": "Captcha issue! Please refresh!",
"r": "Record not found!",
}
# List of valid titles
VALID_TITLES = {
"Self Attendance Report",
"MainLogin",
"Academic And Non Academic Fees Status",
}
def get_erp_data(
username: str, password: str, param: str, parent: str = "student"
) -> str:
"""
Parameters
----------
username - ERP ID
password - ERP Password
param - Page of ERP to be loaded
Returns
-------
Page's HTML content if successful, else corresponding error code
"""
# We give up after 10 tries
count = 0
while True:
# Use the same session so that login actually persists
with requests.session() as s:
captcha_text = ""
# Keep fetching and parsing a captcha until we receive some text
while len(captcha_text) != 6:
headers = {"Content-Type": "application/json; charset=utf-8"}
response = s.post(
"https://erp.mitwpu.edu.in/AdminLogin.aspx/funGenerateCaptcha",
headers=headers,
)
if response.status_code != 200:
return "e"
data = response.text
img = loads(data)["d"]
stream = BytesIO(b64decode(img))
# Use tesseract-ocr to read the captcha text
image = Image.open(stream)
captcha_text = loki.image_to_string(
image,
config="--psm 8 --oem 0 -c tessedit_char_whitelist=0123456789abcdef",
).strip()
# Set the payload for the actual login part
payload["txtUserId"] = username
payload["txtPassword"] = password
payload["txtCaptcha"] = captcha_text
# Headers for logging in and fetching the data
headers["Content-Type"] = "application/x-www-form-urlencoded; charset=utf-8"
# POST request to log in
response = s.post(
"https://erp.mitwpu.edu.in/AdminLogin.aspx",
headers=headers,
data=payload,
)
# Upon entering wrong credentials, ERP gives us a popup containing this text
if "USER Id/ Password Mismatch" in response.text:
return "w"
# Check for the new useless ratelimits of allowing us to login only every 30 minutes
if m := search(r"You are allowed to login after \d+ min\.", response.text):
error_key = f"{username}-ratelimit"
ERRORS[error_key] = m.group()
return error_key
# Increment count so we can break out after 10 tries and assume captcha reading failed
count += 1
# Retrieve the page content
data = s.get(
f"https://erp.mitwpu.edu.in/{os.path.join(parent, param)}.aspx"
).text
# Check the title of retrieved page
if title_search := search("(?<=<title>).+?(?=</title>)", data, DOTALL):
title = title_search.group().strip()
# Before a new trimester starts, ERP records are seemingly wiped
if "Record Not Found" in data:
return "r"
# For attendance and timetable, this is the page title
# However even wrong captcha page has the same title, so ensure no traces of AdminLogin.aspx
if title in VALID_TITLES and "AdminLogin.aspx" not in data:
return data
# A reference to AdminLogin.aspx means login failed. Since the credentials are correct, is it most likely
# captcha
if "AdminLogin.aspx" in data and count < 10:
continue
# At this point, all we can do is give up and assume that reading the captcha fail
# There is also the possibility of ERP forcing a password change or something similar, that needs to be
# accounted for
if count >= 10:
log(username, data, captcha_text, param, img, response.text)
return "c"
def attendance(username: str, password: str) -> str:
"""
Parameters
----------
username -> ERP ID
password -> ERP Password
Returns
-------
Either error code, or attendance page data
"""
return get_erp_data(username, password, "SelfAttendence")
def timetable(username: str, password: str) -> str:
"""
Parameters
----------
username -> ERP ID
password -> ERP Password
Returns
-------
Either error code, or timetable page data
"""
return get_erp_data(username, password, "StudentSelfTimeTable")
def fees(username: str, password: str) -> str:
"""
Parameters
----------
username -> ERP ID
password -> ERP Password
Returns
-------
Either error code, or payable fees page data
"""
return get_erp_data(username, password, "AcademicfeesAll", parent="student")
def miscellaneous(username: str, password: str) -> str:
"""
Parameters
----------
username -> ERP ID
password -> ERP Password
Returns
-------
Either error code, or miscellaneous data
"""
return get_erp_data(username, password, "MainNew")
def get_attendance(data: str) -> List:
"""
Parameters
----------
data -> Attendance HTML page to be parsed
Returns
-------
List of dicts containing attendance data
"""
soup = BeautifulSoup(data, features="html.parser")
tables = soup.findAll("table")
# ERP Attendance page contains 4 tables
if len(tables) != 4:
return ["Error"]
# Attendance data is in the second table
table = tables[1]
# Get all the table titles
titles = [h.text for h in table.find("thead").find("tr")]
# Get the table body
body = table.find("tbody")
ret: List = [Dict]
# Iterate over all the rows in the body to get the actual data
for row in body.findAll("tr"):
attendance_dict = {}
# Length 6 contains subject name and serial number as well
if len(row) == 6:
for element in range(len(row.findAll("td"))):
attendance_dict[titles[element]] = row.findAll("td")[
element
].text.strip()
# Length 4 means its the 2nd row for the subject, so need the previous subject name
elif len(row) == 4:
attendance_dict[titles[0]] = ret[-1][titles[0]]
attendance_dict[titles[1]] = ret[-1][titles[1]]
for element in range(len(row.findAll("td"))):
attendance_dict[titles[element + 2]] = row.findAll("td")[
element
].text.strip()
# Any other structure means its one of the totals which is easier to compute on our own than parse
else:
continue
ret.append(attendance_dict)
return ret
def attendance_json(username: str, password: str) -> str:
"""
Parameters
----------
username -> ERP ID
password -> ERP Password
Returns
-------
JSON body containing attendance data
"""
# Keep trying to get attendance until we succeed or hit one of our error messages
while True:
attendance_data = attendance(username, password)
if attendance_data in ERRORS.keys():
return dumps([{"response": ERRORS[attendance_data]}])
ret = get_attendance(attendance_data)
if ret == ["Error"]:
return dumps([{"response": "Error parsing attendance!"}])
break
# Convert the data BeautifulSoup gave us into a list of dicts
data: List[Dict] = list()
table = ret
for i in range(len(table)):
# Ignore serial number
if "SrNo" not in table[i].keys():
break
# Practical / Tutorial
elif i > 0 and str(table[i]["Subject"]) == str(table[i - 1]["Subject"]):
data[-1][str(table[i]["Subject Type"].lower()) + "_present"] = int(
table[i]["Present"]
)
data[-1][str(table[i]["Subject Type"].lower()) + "_total"] = int(
table[i]["Total Period"]
)
# Theory lecture
else:
data.append(
{
"subject": str(table[i]["Subject"]),
str(table[i]["Subject Type"].lower())
+ "_present": int(table[i]["Present"]),
str(table[i]["Subject Type"].lower())
+ "_total": int(table[i]["Total Period"]),
}
)
# Return the data after calling json.dumps() on it
return dumps(data)
def get_fees(data: str) -> list:
"""
Parameters
----------
data -> Fees HTML page to be parsed
Returns
-------
List of dicts containing fees data
"""
soup = BeautifulSoup(data, features="html.parser")
tables = soup.findAll("table")
# ERP Fees page contains 2 tables
if len(tables) != 2:
return ["Error"]
# Fees data is in the second table
table = tables[1]
# Get all the table titles
titles = []
demo = table.find("tr").findAll("td")
for l in list(demo):
titles.append(l.text)
ret = []
# Iterate over all the rows in the body to get the actual data(enumerate is used to get the index of for lop)
for idx, row in enumerate(table.findAll("tr")[1:]):
fees = {}
# Length 15 contains Fees type
if len(row) == 15:
# 7th,12th and 14th index of for loop contains the totals of respective fees types which we want to ignore
if idx == 7 or idx == 12 or idx == 14:
continue
for element in range(len(row.findAll("td"))):
fees[titles[element]] = row.findAll("td")[element].text.strip()
# Length 13 means its the 2nd row for the Fees type, so need the previous Fees type name
elif len(row) == 13:
# 15th index contains the grand total of all fees which we also ignore
if idx == 15:
continue
fees[titles[0]] = ret[-1][titles[0]]
for element in range(len(row.findAll("td"))):
fees[titles[element + 1]] = row.findAll("td")[element].text.strip()
else:
continue
# The Advance column in erp fees table gives a blank string when there is no advance, so replacing it with '0'
dct = {k: "0" if not v else v for k, v in fees.items()}
ret.append(dct)
return ret
def fees_json(username: str, password: str) -> str:
"""
Parameters
----------
username -> ERP ID
password -> ERP Password
Returns
-------
JSON body containing fees data
"""
# Keep trying to get fees until we succeed or hit one of our error messages
while True:
fees_data = fees(username, password)
if fees_data in ERRORS.keys():
return dumps([{"response": ERRORS[fees_data]}])
ret = get_fees(fees_data)
if ret == ["Error"]:
return dumps([{"response": "Error parsing attendance!"}])
break
# Convert the data BeautifulSoup gave us into a list of dicts
data = list(ret)
# Return the data after calling json.dumps() on it
return dumps(data)