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