@lime1/esprit-ts 1.0.0

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.
package/dist/index.js ADDED
@@ -0,0 +1,659 @@
1
+ // src/index.ts
2
+ import axios from "axios";
3
+ import { wrapper } from "axios-cookiejar-support";
4
+ import { CookieJar } from "tough-cookie";
5
+ import * as cheerio2 from "cheerio";
6
+ import qs from "qs";
7
+
8
+ // src/utils.ts
9
+ import * as cheerio from "cheerio";
10
+ function extractASPNetFormData(html) {
11
+ const $ = cheerio.load(html);
12
+ return {
13
+ __VIEWSTATE: $("input#__VIEWSTATE").val() ?? "",
14
+ __VIEWSTATEGENERATOR: $("input#__VIEWSTATEGENERATOR").val() ?? "",
15
+ __EVENTVALIDATION: $("input#__EVENTVALIDATION").val() ?? ""
16
+ };
17
+ }
18
+ function findInputByPatterns($, patterns) {
19
+ for (const pattern of patterns) {
20
+ const lowerPattern = pattern.toLowerCase();
21
+ const byId = $(`input`).filter((_, el) => {
22
+ const id = $(el).attr("id")?.toLowerCase() ?? "";
23
+ return id.includes(lowerPattern);
24
+ }).first();
25
+ if (byId.length > 0) return byId;
26
+ const byName = $(`input`).filter((_, el) => {
27
+ const name = $(el).attr("name")?.toLowerCase() ?? "";
28
+ return name.includes(lowerPattern);
29
+ }).first();
30
+ if (byName.length > 0) return byName;
31
+ }
32
+ return null;
33
+ }
34
+ function getInputName($input, $) {
35
+ return $input.attr("name") ?? $input.attr("id") ?? "";
36
+ }
37
+ function parseEuropeanNumber(value) {
38
+ if (!value || value.trim() === "") return null;
39
+ const normalized = value.trim().replace(",", ".");
40
+ const parsed = parseFloat(normalized);
41
+ return isNaN(parsed) ? null : parsed;
42
+ }
43
+ function parseGrades(headers, rows) {
44
+ const grades = [];
45
+ const headerLower = headers.map((h) => h.toLowerCase());
46
+ const designationIdx = headerLower.findIndex((h) => h.includes("designation") || h.includes("module"));
47
+ const coefIdx = headerLower.findIndex((h) => h.includes("coef"));
48
+ const ccIdx = headerLower.findIndex((h) => h.includes("cc") || h.includes("note_cc"));
49
+ const tpIdx = headerLower.findIndex((h) => h.includes("tp") || h.includes("note_tp"));
50
+ const examIdx = headerLower.findIndex((h) => h.includes("exam") || h.includes("note_exam"));
51
+ for (const row of rows) {
52
+ if (row.length === 0) continue;
53
+ grades.push({
54
+ designation: designationIdx >= 0 ? row[designationIdx] ?? "" : row[0] ?? "",
55
+ coefficient: coefIdx >= 0 ? parseEuropeanNumber(row[coefIdx]) : null,
56
+ noteCC: ccIdx >= 0 ? parseEuropeanNumber(row[ccIdx]) : null,
57
+ noteTP: tpIdx >= 0 ? parseEuropeanNumber(row[tpIdx]) : null,
58
+ noteExam: examIdx >= 0 ? parseEuropeanNumber(row[examIdx]) : null
59
+ });
60
+ }
61
+ return grades;
62
+ }
63
+ function calculateModuleAverage(grade) {
64
+ const { noteExam, noteCC, noteTP } = grade;
65
+ if (noteExam === null) return null;
66
+ if (noteTP === null) {
67
+ if (noteCC === null) {
68
+ return noteExam;
69
+ } else {
70
+ return noteExam * 0.6 + noteCC * 0.4;
71
+ }
72
+ } else if (noteCC === null) {
73
+ return noteExam * 0.8 + noteTP * 0.2;
74
+ } else {
75
+ return noteExam * 0.5 + noteCC * 0.3 + noteTP * 0.2;
76
+ }
77
+ }
78
+ function calculateGradeSummary(grades) {
79
+ const gradesWithAverage = grades.map((grade) => ({
80
+ ...grade,
81
+ moyenne: calculateModuleAverage(grade)
82
+ }));
83
+ let totalWeightedSum = 0;
84
+ let totalCoefficient = 0;
85
+ for (const grade of gradesWithAverage) {
86
+ if (grade.moyenne !== null && grade.coefficient !== null) {
87
+ totalWeightedSum += grade.moyenne * grade.coefficient;
88
+ totalCoefficient += grade.coefficient;
89
+ }
90
+ }
91
+ const totalAverage = totalCoefficient > 0 ? totalWeightedSum / totalCoefficient : null;
92
+ return {
93
+ grades: gradesWithAverage,
94
+ totalAverage,
95
+ totalCoefficient
96
+ };
97
+ }
98
+ function parseTable($, table) {
99
+ const rowElements = table.find("tr");
100
+ const headers = [];
101
+ const rows = [];
102
+ rowElements.each((index, row) => {
103
+ const cells = [];
104
+ if (index === 0) {
105
+ $(row).find("th").each((_, cell) => {
106
+ headers.push($(cell).text().trim());
107
+ });
108
+ } else {
109
+ $(row).find("td").each((_, cell) => {
110
+ cells.push($(cell).text().trim());
111
+ });
112
+ if (cells.length > 0) {
113
+ rows.push(cells);
114
+ }
115
+ }
116
+ });
117
+ return { headers, rows };
118
+ }
119
+ function isLoginPage(url) {
120
+ const lower = url.toLowerCase();
121
+ return lower.includes("default.aspx") || lower.includes("login");
122
+ }
123
+ function isHomePage(url) {
124
+ return url.toLowerCase().includes("accueil.aspx");
125
+ }
126
+
127
+ // src/index.ts
128
+ var EspritClient = class {
129
+ client;
130
+ cookieJar;
131
+ debug;
132
+ // URL constants
133
+ LOGIN_URL = "https://esprit-tn.com/esponline/online/default.aspx";
134
+ HOME_URL = "https://esprit-tn.com/esponline/Etudiants/Accueil.aspx";
135
+ GRADES_URL = "https://esprit-tn.com/ESPOnline/Etudiants/Resultat2021.aspx";
136
+ ABSENCES_URL = "https://esprit-tn.com/ESPOnline/Etudiants/absenceetud.aspx";
137
+ CREDITS_URL = "https://esprit-tn.com/ESPOnline/Etudiants/Historique_Cr%C3%A9dit.aspx";
138
+ SCHEDULES_URL = "https://esprit-tn.com/ESPOnline/Etudiants/Emplois.aspx";
139
+ LOGOUT_URLS = [
140
+ "https://esprit-tn.com/esponline/Etudiants/Deconnexion.aspx",
141
+ "https://esprit-tn.com/esponline/online/Deconnexion.aspx"
142
+ ];
143
+ /**
144
+ * Create a new EspritClient instance.
145
+ *
146
+ * @param config - Optional configuration options
147
+ */
148
+ constructor(config = {}) {
149
+ this.debug = config.debug ?? false;
150
+ this.cookieJar = new CookieJar();
151
+ this.client = wrapper(axios.create({
152
+ jar: this.cookieJar,
153
+ withCredentials: true,
154
+ timeout: config.timeout ?? 3e4,
155
+ headers: {
156
+ "User-Agent": config.userAgent ?? "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
157
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
158
+ "Accept-Language": "en-US,en;q=0.5",
159
+ "Connection": "keep-alive"
160
+ },
161
+ maxRedirects: 5
162
+ }));
163
+ }
164
+ /**
165
+ * Log a debug message if debug mode is enabled.
166
+ */
167
+ log(message, ...args) {
168
+ if (this.debug) {
169
+ console.log(`[EspritClient] ${message}`, ...args);
170
+ }
171
+ }
172
+ /**
173
+ * Login to the ESPRIT student portal.
174
+ *
175
+ * This handles the multi-step ASP.NET login flow:
176
+ * 1. Load login page and extract ViewState
177
+ * 2. Submit student ID with checkbox
178
+ * 3. Submit password
179
+ *
180
+ * @param identifier - Student ID
181
+ * @param password - Student password
182
+ * @returns LoginResult with success status and cookies
183
+ */
184
+ async login(identifier, password) {
185
+ try {
186
+ this.log("Loading login page...");
187
+ const initialResponse = await this.client.get(this.LOGIN_URL);
188
+ if (initialResponse.status !== 200) {
189
+ return { success: false, cookies: [], message: "Failed to load login page" };
190
+ }
191
+ let $ = cheerio2.load(initialResponse.data);
192
+ let formData = extractASPNetFormData(initialResponse.data);
193
+ const idInput = findInputByPatterns($, ["textbox3", "textbox1"]);
194
+ const idFieldName = idInput ? getInputName(idInput, $) : "ctl00$ContentPlaceHolder1$TextBox3";
195
+ this.log("Found ID field:", idFieldName);
196
+ const checkbox = $('input[type="checkbox"]').first();
197
+ const checkboxName = checkbox.length > 0 ? getInputName(checkbox, $) : "";
198
+ this.log("Found checkbox:", checkboxName);
199
+ if (checkboxName) {
200
+ const checkboxFormData = {
201
+ ...formData,
202
+ [idFieldName]: identifier,
203
+ [checkboxName]: "on",
204
+ __EVENTTARGET: checkboxName,
205
+ __EVENTARGUMENT: ""
206
+ };
207
+ this.log("Checking checkbox...");
208
+ const checkboxResponse = await this.client.post(
209
+ this.LOGIN_URL,
210
+ qs.stringify(checkboxFormData),
211
+ { headers: { "Content-Type": "application/x-www-form-urlencoded" } }
212
+ );
213
+ if (checkboxResponse.status === 200) {
214
+ $ = cheerio2.load(checkboxResponse.data);
215
+ formData = extractASPNetFormData(checkboxResponse.data);
216
+ const continueButton = findInputByPatterns($, ["continuer", "continue"]);
217
+ if (continueButton) {
218
+ const continueButtonName = getInputName(continueButton, $);
219
+ this.log("Clicking continue button:", continueButtonName);
220
+ const continueFormData = {
221
+ ...formData,
222
+ [idFieldName]: identifier,
223
+ [checkboxName]: "on",
224
+ __EVENTTARGET: continueButtonName,
225
+ __EVENTARGUMENT: ""
226
+ };
227
+ const continueResponse = await this.client.post(
228
+ this.LOGIN_URL,
229
+ qs.stringify(continueFormData),
230
+ { headers: { "Content-Type": "application/x-www-form-urlencoded" } }
231
+ );
232
+ if (continueResponse.status === 200) {
233
+ $ = cheerio2.load(continueResponse.data);
234
+ formData = extractASPNetFormData(continueResponse.data);
235
+ }
236
+ }
237
+ }
238
+ }
239
+ const suivantButton = findInputByPatterns($, ["button3", "button1", "suivant", "next"]);
240
+ const suivantButtonName = suivantButton ? getInputName(suivantButton, $) : "ctl00$ContentPlaceHolder1$Button3";
241
+ this.log("Clicking Suivant button:", suivantButtonName);
242
+ const suivantFormData = {
243
+ ...formData,
244
+ [idFieldName]: identifier,
245
+ ...checkboxName ? { [checkboxName]: "on" } : {},
246
+ __EVENTTARGET: suivantButtonName,
247
+ __EVENTARGUMENT: ""
248
+ };
249
+ const suivantResponse = await this.client.post(
250
+ this.LOGIN_URL,
251
+ qs.stringify(suivantFormData),
252
+ { headers: { "Content-Type": "application/x-www-form-urlencoded" } }
253
+ );
254
+ if (suivantResponse.status !== 200) {
255
+ return { success: false, cookies: [], message: "Failed to submit ID" };
256
+ }
257
+ $ = cheerio2.load(suivantResponse.data);
258
+ const passwordField = findInputByPatterns($, ["textbox7"]);
259
+ const idFieldStillPresent = findInputByPatterns($, ["textbox3"]);
260
+ if (idFieldStillPresent && !passwordField) {
261
+ return { success: false, cookies: [], message: "Identifiant incorrect !" };
262
+ }
263
+ this.log("Now on password page...");
264
+ formData = extractASPNetFormData(suivantResponse.data);
265
+ const passwordFieldName = passwordField ? getInputName(passwordField, $) : "ctl00$ContentPlaceHolder1$TextBox7";
266
+ const connexionButton = findInputByPatterns($, ["buttonetudiant", "button2", "connexion", "connect"]);
267
+ const connexionButtonName = connexionButton ? getInputName(connexionButton, $) : "ctl00$ContentPlaceHolder1$ButtonEtudiant";
268
+ this.log("Submitting password with button:", connexionButtonName);
269
+ const passwordFormData = {
270
+ ...formData,
271
+ [passwordFieldName]: password,
272
+ __EVENTTARGET: connexionButtonName,
273
+ __EVENTARGUMENT: ""
274
+ };
275
+ const loginResponse = await this.client.post(
276
+ suivantResponse.request?.res?.responseUrl ?? this.LOGIN_URL,
277
+ qs.stringify(passwordFormData),
278
+ {
279
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
280
+ maxRedirects: 5
281
+ }
282
+ );
283
+ const finalUrl = loginResponse.request?.res?.responseUrl ?? "";
284
+ this.log("Final URL:", finalUrl);
285
+ $ = cheerio2.load(loginResponse.data);
286
+ const passwordStillPresent = findInputByPatterns($, ["textbox7"]);
287
+ const stillOnLoginPage = isLoginPage(finalUrl);
288
+ if (stillOnLoginPage && passwordStillPresent) {
289
+ return { success: false, cookies: [], message: "Mot de passe incorrect !" };
290
+ }
291
+ const successIndicators = [
292
+ "Vous pouvez consulter dans cet espace",
293
+ "Espace Etudiant",
294
+ "Accueil.aspx",
295
+ "Label2",
296
+ "Label3"
297
+ ];
298
+ const onHomePage = isHomePage(finalUrl);
299
+ const hasSuccessIndicator = successIndicators.some(
300
+ (indicator) => loginResponse.data.includes(indicator)
301
+ );
302
+ if (!stillOnLoginPage && (onHomePage || hasSuccessIndicator)) {
303
+ this.log("Login successful!");
304
+ const cookies = await this.getCookies();
305
+ return { success: true, cookies, message: "Login successful!" };
306
+ }
307
+ return { success: false, cookies: [], message: "Login failed - unknown error" };
308
+ } catch (error) {
309
+ const message = error instanceof Error ? error.message : "Unknown error";
310
+ this.log("Login error:", message);
311
+ return { success: false, cookies: [], message };
312
+ }
313
+ }
314
+ /**
315
+ * Logout from the ESPRIT portal.
316
+ *
317
+ * @returns True if logout was successful
318
+ */
319
+ async logout() {
320
+ try {
321
+ this.log("Attempting logout via postback...");
322
+ const homeResponse = await this.client.get(this.HOME_URL, { maxRedirects: 5 });
323
+ if (homeResponse.status === 200 && !isLoginPage(homeResponse.request?.res?.responseUrl ?? "")) {
324
+ const formData = extractASPNetFormData(homeResponse.data);
325
+ const logoutFormData = {
326
+ ...formData,
327
+ __EVENTTARGET: "ctl00$LinkButton1",
328
+ __EVENTARGUMENT: ""
329
+ };
330
+ await this.client.post(
331
+ this.HOME_URL,
332
+ qs.stringify(logoutFormData),
333
+ { headers: { "Content-Type": "application/x-www-form-urlencoded" } }
334
+ );
335
+ this.log("Logout successful via postback");
336
+ return true;
337
+ }
338
+ for (const logoutUrl of this.LOGOUT_URLS) {
339
+ try {
340
+ this.log("Trying logout URL:", logoutUrl);
341
+ await this.client.get(logoutUrl, { maxRedirects: 5 });
342
+ this.log("Logout successful via URL");
343
+ return true;
344
+ } catch {
345
+ continue;
346
+ }
347
+ }
348
+ this.log("Clearing cookies...");
349
+ this.cookieJar.removeAllCookiesSync();
350
+ return true;
351
+ } catch (error) {
352
+ this.log("Logout error:", error);
353
+ this.cookieJar.removeAllCookiesSync();
354
+ return true;
355
+ }
356
+ }
357
+ /**
358
+ * Get current session cookies.
359
+ *
360
+ * @returns Array of Cookie objects
361
+ */
362
+ async getCookies() {
363
+ const cookies = await this.cookieJar.getCookies(this.LOGIN_URL);
364
+ return cookies.map((cookie) => ({
365
+ name: cookie.key,
366
+ value: cookie.value,
367
+ domain: cookie.domain ?? "",
368
+ path: cookie.path ?? "/"
369
+ }));
370
+ }
371
+ /**
372
+ * Get student grades.
373
+ *
374
+ * @returns Array of Grade objects or null if page cannot be loaded
375
+ */
376
+ async getGrades() {
377
+ try {
378
+ const response = await this.client.get(this.GRADES_URL, { maxRedirects: 5 });
379
+ if (isLoginPage(response.request?.res?.responseUrl ?? "")) {
380
+ this.log("Session expired - redirected to login");
381
+ return null;
382
+ }
383
+ const $ = cheerio2.load(response.data);
384
+ const h1Tag = $("h1").filter(
385
+ (_, el) => $(el).text().includes("Notes Des Modules")
386
+ );
387
+ if (h1Tag.length === 0) {
388
+ this.log("Page does not contain expected content");
389
+ return null;
390
+ }
391
+ const table = $("#ContentPlaceHolder1_GridView1");
392
+ if (table.length === 0) {
393
+ this.log("Grades table not found");
394
+ return null;
395
+ }
396
+ const { headers, rows } = parseTable($, table);
397
+ return parseGrades(headers, rows);
398
+ } catch (error) {
399
+ this.log("Error getting grades:", error);
400
+ return null;
401
+ }
402
+ }
403
+ /**
404
+ * Get grades with calculated averages.
405
+ *
406
+ * @returns GradeSummary with grades and total average, or null if page cannot be loaded
407
+ */
408
+ async getGradesWithAverage() {
409
+ const grades = await this.getGrades();
410
+ if (!grades) return null;
411
+ return calculateGradeSummary(grades);
412
+ }
413
+ /**
414
+ * Get student absences.
415
+ *
416
+ * @returns Array of Absence records (as key-value objects) or null
417
+ */
418
+ async getAbsences() {
419
+ try {
420
+ const response = await this.client.get(this.ABSENCES_URL, { maxRedirects: 5 });
421
+ if (isLoginPage(response.request?.res?.responseUrl ?? "")) {
422
+ this.log("Session expired - redirected to login");
423
+ return null;
424
+ }
425
+ const $ = cheerio2.load(response.data);
426
+ const strongTag = $("strong").filter(
427
+ (_, el) => $(el).text().includes("Absence")
428
+ );
429
+ if (strongTag.length === 0) {
430
+ this.log("Page does not contain expected content");
431
+ return null;
432
+ }
433
+ const table = $("#ContentPlaceHolder1_GridView2");
434
+ if (table.length === 0) {
435
+ this.log("Absences table not found");
436
+ return null;
437
+ }
438
+ const { headers, rows } = parseTable($, table);
439
+ return rows.map((row) => {
440
+ const absence = {};
441
+ headers.forEach((header, index) => {
442
+ absence[header] = row[index] ?? "";
443
+ });
444
+ return absence;
445
+ });
446
+ } catch (error) {
447
+ this.log("Error getting absences:", error);
448
+ return null;
449
+ }
450
+ }
451
+ /**
452
+ * Get student credit history.
453
+ *
454
+ * @returns Array of Credit records (as key-value objects) or null
455
+ */
456
+ async getCredits() {
457
+ try {
458
+ const response = await this.client.get(this.CREDITS_URL, { maxRedirects: 5 });
459
+ if (isLoginPage(response.request?.res?.responseUrl ?? "")) {
460
+ this.log("Session expired - redirected to login");
461
+ return null;
462
+ }
463
+ const $ = cheerio2.load(response.data);
464
+ let table = $("#ContentPlaceHolder1_GridView1");
465
+ if (table.length === 0) {
466
+ $("table").each((_, el) => {
467
+ const firstRow = $(el).find("tr").first();
468
+ const headers2 = firstRow.find("th").map((_2, th) => $(th).text().trim()).get();
469
+ if (headers2.some((h) => h.includes("Ann\xE9e") || h.includes("enseignement"))) {
470
+ table = $(el);
471
+ return false;
472
+ }
473
+ });
474
+ }
475
+ if (table.length === 0) {
476
+ this.log("Credits table not found");
477
+ return null;
478
+ }
479
+ const { headers, rows } = parseTable($, table);
480
+ return rows.map((row) => {
481
+ const credit = {};
482
+ headers.forEach((header, index) => {
483
+ credit[header] = row[index] ?? "";
484
+ });
485
+ return credit;
486
+ });
487
+ } catch (error) {
488
+ this.log("Error getting credits:", error);
489
+ return null;
490
+ }
491
+ }
492
+ /**
493
+ * Get available schedules.
494
+ *
495
+ * @returns Array of Schedule objects or null
496
+ */
497
+ async getSchedules() {
498
+ try {
499
+ const response = await this.client.get(this.SCHEDULES_URL, { maxRedirects: 5 });
500
+ if (isLoginPage(response.request?.res?.responseUrl ?? "")) {
501
+ this.log("Session expired - redirected to login");
502
+ return null;
503
+ }
504
+ const $ = cheerio2.load(response.data);
505
+ const strongTag = $("strong").filter(
506
+ (_, el) => $(el).text().includes("Emploi du temps")
507
+ );
508
+ if (strongTag.length === 0) {
509
+ this.log("Page does not contain expected content");
510
+ return null;
511
+ }
512
+ const table = $("#ContentPlaceHolder1_GridView1");
513
+ if (table.length === 0) {
514
+ this.log("Schedules table not found");
515
+ return null;
516
+ }
517
+ const schedules = [];
518
+ table.find("tr").each((index, row) => {
519
+ if (index === 0) return;
520
+ const cells = $(row).find("td");
521
+ const rawData = [];
522
+ let name = "";
523
+ let link = null;
524
+ cells.each((i, cell) => {
525
+ const cellText = $(cell).text().trim();
526
+ const cellLink = $(cell).find("a");
527
+ if (i === 0) {
528
+ name = cellText;
529
+ }
530
+ if (cellLink.length > 0) {
531
+ const href = cellLink.attr("href") ?? "";
532
+ const match = href.match(/'([^']+)'/);
533
+ link = match ? match[1] : href;
534
+ }
535
+ rawData.push(cellText);
536
+ });
537
+ if (name) {
538
+ schedules.push({ name, link, rawData });
539
+ }
540
+ });
541
+ return schedules;
542
+ } catch (error) {
543
+ this.log("Error getting schedules:", error);
544
+ return null;
545
+ }
546
+ }
547
+ /**
548
+ * Get student name from the home page.
549
+ *
550
+ * @returns Student name or null
551
+ */
552
+ async getStudentName() {
553
+ try {
554
+ const response = await this.client.get(this.HOME_URL, { maxRedirects: 5 });
555
+ if (isLoginPage(response.request?.res?.responseUrl ?? "")) {
556
+ this.log("Session expired - redirected to login");
557
+ return null;
558
+ }
559
+ const $ = cheerio2.load(response.data);
560
+ const selectors = [
561
+ "#Label2",
562
+ "#ContentPlaceHolder1_Label2",
563
+ "span.h4.text-info"
564
+ ];
565
+ for (const selector of selectors) {
566
+ const element = $(selector);
567
+ if (element.length > 0) {
568
+ const text = element.text().trim();
569
+ if (text) return text;
570
+ }
571
+ }
572
+ this.log("Student name not found");
573
+ return null;
574
+ } catch (error) {
575
+ this.log("Error getting student name:", error);
576
+ return null;
577
+ }
578
+ }
579
+ /**
580
+ * Get student class from the home page.
581
+ *
582
+ * @returns Student class name or null
583
+ */
584
+ async getStudentClass() {
585
+ try {
586
+ const response = await this.client.get(this.HOME_URL, { maxRedirects: 5 });
587
+ if (isLoginPage(response.request?.res?.responseUrl ?? "")) {
588
+ this.log("Session expired - redirected to login");
589
+ return null;
590
+ }
591
+ const $ = cheerio2.load(response.data);
592
+ const selectors = [
593
+ "#Label3",
594
+ "#ContentPlaceHolder1_Label3"
595
+ ];
596
+ for (const selector of selectors) {
597
+ const element = $(selector);
598
+ if (element.length > 0) {
599
+ const text = element.text().trim();
600
+ if (text) return text;
601
+ }
602
+ }
603
+ this.log("Student class not found");
604
+ return null;
605
+ } catch (error) {
606
+ this.log("Error getting student class:", error);
607
+ return null;
608
+ }
609
+ }
610
+ /**
611
+ * Get both student name and class in one call.
612
+ *
613
+ * @returns StudentInfo object with name and className
614
+ */
615
+ async getStudentInfo() {
616
+ try {
617
+ const response = await this.client.get(this.HOME_URL, { maxRedirects: 5 });
618
+ if (isLoginPage(response.request?.res?.responseUrl ?? "")) {
619
+ this.log("Session expired - redirected to login");
620
+ return { name: null, className: null };
621
+ }
622
+ const $ = cheerio2.load(response.data);
623
+ let name = null;
624
+ for (const selector of ["#Label2", "#ContentPlaceHolder1_Label2"]) {
625
+ const element = $(selector);
626
+ if (element.length > 0) {
627
+ const text = element.text().trim();
628
+ if (text) {
629
+ name = text;
630
+ break;
631
+ }
632
+ }
633
+ }
634
+ let className = null;
635
+ for (const selector of ["#Label3", "#ContentPlaceHolder1_Label3"]) {
636
+ const element = $(selector);
637
+ if (element.length > 0) {
638
+ const text = element.text().trim();
639
+ if (text) {
640
+ className = text;
641
+ break;
642
+ }
643
+ }
644
+ }
645
+ return { name, className };
646
+ } catch (error) {
647
+ this.log("Error getting student info:", error);
648
+ return { name: null, className: null };
649
+ }
650
+ }
651
+ };
652
+ var index_default = EspritClient;
653
+ export {
654
+ EspritClient,
655
+ calculateGradeSummary,
656
+ calculateModuleAverage,
657
+ index_default as default
658
+ };
659
+ //# sourceMappingURL=index.js.map