@lime1/esprit-ts 1.0.0 → 1.1.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 CHANGED
@@ -136,6 +136,13 @@ var EspritClient = class {
136
136
  ABSENCES_URL = "https://esprit-tn.com/ESPOnline/Etudiants/absenceetud.aspx";
137
137
  CREDITS_URL = "https://esprit-tn.com/ESPOnline/Etudiants/Historique_Cr%C3%A9dit.aspx";
138
138
  SCHEDULES_URL = "https://esprit-tn.com/ESPOnline/Etudiants/Emplois.aspx";
139
+ // New grade endpoints
140
+ REGULAR_GRADES_URL = "https://esprit-tn.com/ESPOnline/Etudiants/Resultat2021.aspx";
141
+ PRINCIPAL_RESULT_URL = "https://esprit-tn.com/ESPOnline/Etudiants/ResultatPrincipale2021.aspx";
142
+ RATTRAPAGE_GRADES_URL = "https://esprit-tn.com/ESPOnline/Etudiants/noterat.aspx";
143
+ RATTRAPAGE_RESULT_URL = "https://esprit-tn.com/ESPOnline/Etudiants/ResultatRattrapage2021.aspx";
144
+ LANGUAGE_LEVELS_URL = "https://esprit-tn.com/ESPOnline/Etudiants/LANG2.aspx";
145
+ RANKING_URL = "https://esprit-tn.com/ESPOnline/Etudiants/ranking.aspx";
139
146
  LOGOUT_URLS = [
140
147
  "https://esprit-tn.com/esponline/Etudiants/Deconnexion.aspx",
141
148
  "https://esprit-tn.com/esponline/online/Deconnexion.aspx"
@@ -371,6 +378,7 @@ var EspritClient = class {
371
378
  /**
372
379
  * Get student grades.
373
380
  *
381
+ * @deprecated Use getRegularGrades() for more accurate naming. This method will be removed in a future version.
374
382
  * @returns Array of Grade objects or null if page cannot be loaded
375
383
  */
376
384
  async getGrades() {
@@ -648,6 +656,292 @@ var EspritClient = class {
648
656
  return { name: null, className: null };
649
657
  }
650
658
  }
659
+ /**
660
+ * Get regular grades (grades released incrementally during the semester - Session Principale).
661
+ *
662
+ * This replaces the old getGrades() method with more accurate naming.
663
+ * Fetches data from Resultat2021.aspx which displays module grades as they're released.
664
+ *
665
+ * @returns Array of string arrays where first row is headers, or null if unavailable
666
+ */
667
+ async getRegularGrades() {
668
+ try {
669
+ const response = await this.client.get(this.REGULAR_GRADES_URL, { maxRedirects: 5 });
670
+ if (isLoginPage(response.request?.res?.responseUrl ?? "")) {
671
+ this.log("Session expired - redirected to login");
672
+ return null;
673
+ }
674
+ if (response.data.includes("aucune note!")) {
675
+ this.log("No grades available yet");
676
+ return null;
677
+ }
678
+ const $ = cheerio2.load(response.data);
679
+ const h1Tag = $("h1").filter((_, el) => {
680
+ const text = $(el).text();
681
+ return text.includes("Notes Des Modules");
682
+ });
683
+ if (h1Tag.length === 0) {
684
+ this.log("Page does not contain expected content");
685
+ return null;
686
+ }
687
+ const table = $("#ContentPlaceHolder1_GridView1");
688
+ if (table.length === 0) {
689
+ this.log("Grades table not found");
690
+ return null;
691
+ }
692
+ const { headers, rows } = parseTable($, table);
693
+ return [headers, ...rows];
694
+ } catch (error) {
695
+ this.log("Error getting regular grades:", error);
696
+ return null;
697
+ }
698
+ }
699
+ /**
700
+ * Get the final verdict/decision at the end of the principal session.
701
+ *
702
+ * Fetches data from ResultatPrincipale2021.aspx showing the overall average and pass/fail decision.
703
+ *
704
+ * @returns PrincipalResult with moyenneGeneral and decision, or null if unavailable
705
+ */
706
+ async getPrincipalResult() {
707
+ try {
708
+ const response = await this.client.get(this.PRINCIPAL_RESULT_URL, { maxRedirects: 5 });
709
+ if (isLoginPage(response.request?.res?.responseUrl ?? "")) {
710
+ this.log("Session expired - redirected to login");
711
+ return null;
712
+ }
713
+ const $ = cheerio2.load(response.data);
714
+ const h1Tag = $("h1").filter((_, el) => {
715
+ const text = $(el).text();
716
+ return text.includes("Resultat Session Principale");
717
+ });
718
+ if (h1Tag.length === 0) {
719
+ this.log("Page does not contain expected content");
720
+ return null;
721
+ }
722
+ const table = $("#ContentPlaceHolder1_GridView3");
723
+ if (table.length === 0) {
724
+ this.log("Result table not found");
725
+ return null;
726
+ }
727
+ const { headers, rows } = parseTable($, table);
728
+ if (rows.length === 0) {
729
+ this.log("No result data found");
730
+ return null;
731
+ }
732
+ const headerLower = headers.map((h) => h.toLowerCase());
733
+ const moyenneIdx = headerLower.findIndex((h) => h.includes("moyenne"));
734
+ const decisionIdx = headerLower.findIndex((h) => h.includes("decision"));
735
+ return {
736
+ moyenneGeneral: moyenneIdx >= 0 ? rows[0][moyenneIdx] ?? null : null,
737
+ decision: decisionIdx >= 0 ? rows[0][decisionIdx] ?? null : null
738
+ };
739
+ } catch (error) {
740
+ this.log("Error getting principal result:", error);
741
+ return null;
742
+ }
743
+ }
744
+ /**
745
+ * Get rattrapage (retake) grades released incrementally during the retake session.
746
+ *
747
+ * Similar to regular grades but for the retake session, fetched from noterat.aspx.
748
+ *
749
+ * @returns Array of string arrays where first row is headers, or null if unavailable
750
+ */
751
+ async getRattrapageGrades() {
752
+ try {
753
+ const response = await this.client.get(this.RATTRAPAGE_GRADES_URL, { maxRedirects: 5 });
754
+ if (isLoginPage(response.request?.res?.responseUrl ?? "")) {
755
+ this.log("Session expired - redirected to login");
756
+ return null;
757
+ }
758
+ if (response.data.includes("aucune note!")) {
759
+ this.log("No rattrapage grades available yet");
760
+ return null;
761
+ }
762
+ const $ = cheerio2.load(response.data);
763
+ const h1Tag = $("h1").filter((_, el) => {
764
+ const text = $(el).text();
765
+ return text.includes("Notes Des Modules session Rattrapage");
766
+ });
767
+ if (h1Tag.length === 0) {
768
+ this.log("Page does not contain expected content");
769
+ return null;
770
+ }
771
+ const table = $("#ContentPlaceHolder1_GridView1");
772
+ if (table.length === 0) {
773
+ this.log("Rattrapage grades table not found");
774
+ return null;
775
+ }
776
+ const { headers, rows } = parseTable($, table);
777
+ return [headers, ...rows];
778
+ } catch (error) {
779
+ this.log("Error getting rattrapage grades:", error);
780
+ return null;
781
+ }
782
+ }
783
+ /**
784
+ * Get the final verdict/decision of the rattrapage (retake) session.
785
+ *
786
+ * Fetches data from ResultatRattrapage2021.aspx showing the overall average and pass/fail decision.
787
+ *
788
+ * @returns RattrapageResult with moyenneGeneral and decision, or null if unavailable
789
+ */
790
+ async getRattrapageResult() {
791
+ try {
792
+ const response = await this.client.get(this.RATTRAPAGE_RESULT_URL, { maxRedirects: 5 });
793
+ if (isLoginPage(response.request?.res?.responseUrl ?? "")) {
794
+ this.log("Session expired - redirected to login");
795
+ return null;
796
+ }
797
+ const $ = cheerio2.load(response.data);
798
+ const h1Tag = $("h1").filter((_, el) => {
799
+ const text = $(el).text();
800
+ return text.includes("Resultat");
801
+ });
802
+ const spanTag = $("#ContentPlaceHolder1_Label18").filter((_, el) => {
803
+ const text = $(el).text();
804
+ return text.includes("Resultat");
805
+ });
806
+ if (h1Tag.length === 0 && spanTag.length === 0) {
807
+ this.log("Page does not contain expected content");
808
+ return null;
809
+ }
810
+ const table = $("#ContentPlaceHolder1_GridView3");
811
+ if (table.length === 0) {
812
+ this.log("Rattrapage result table not found");
813
+ return null;
814
+ }
815
+ const { headers, rows } = parseTable($, table);
816
+ if (rows.length === 0) {
817
+ this.log("No rattrapage result data found");
818
+ return null;
819
+ }
820
+ const headerLower = headers.map((h) => h.toLowerCase());
821
+ const moyenneIdx = headerLower.findIndex((h) => h.includes("moyenne"));
822
+ const decisionIdx = headerLower.findIndex((h) => h.includes("decision"));
823
+ return {
824
+ moyenneGeneral: moyenneIdx >= 0 ? rows[0][moyenneIdx] ?? null : null,
825
+ decision: decisionIdx >= 0 ? rows[0][decisionIdx] ?? null : null
826
+ };
827
+ } catch (error) {
828
+ this.log("Error getting rattrapage result:", error);
829
+ return null;
830
+ }
831
+ }
832
+ /**
833
+ * Get language proficiency levels (French and English).
834
+ *
835
+ * Fetches data from LANG2.aspx showing language levels like 'B1', 'B2', etc.
836
+ *
837
+ * @returns LanguageLevel with francais and anglais levels, or null if unavailable
838
+ */
839
+ async getLanguageLevels() {
840
+ try {
841
+ const response = await this.client.get(this.LANGUAGE_LEVELS_URL, { maxRedirects: 5 });
842
+ if (isLoginPage(response.request?.res?.responseUrl ?? "")) {
843
+ this.log("Session expired - redirected to login");
844
+ return null;
845
+ }
846
+ const $ = cheerio2.load(response.data);
847
+ const h1Tag = $("h1").filter((_, el) => {
848
+ const text = $(el).text();
849
+ return text.includes("NIVEAU LANGUES");
850
+ });
851
+ if (h1Tag.length === 0) {
852
+ this.log("Page does not contain expected content");
853
+ return null;
854
+ }
855
+ const table = $("#ContentPlaceHolder1_GridView2");
856
+ if (table.length === 0) {
857
+ this.log("Language levels table not found");
858
+ return null;
859
+ }
860
+ const { headers, rows } = parseTable($, table);
861
+ if (rows.length === 0) {
862
+ this.log("No language level data found");
863
+ return null;
864
+ }
865
+ const headerLower = headers.map((h) => h.toLowerCase());
866
+ const francaisIdx = headerLower.findIndex((h) => h.includes("francais"));
867
+ const anglaisIdx = headerLower.findIndex((h) => h.includes("anglais"));
868
+ return {
869
+ francais: francaisIdx >= 0 ? rows[0][francaisIdx] ?? null : null,
870
+ anglais: anglaisIdx >= 0 ? rows[0][anglaisIdx] ?? null : null
871
+ };
872
+ } catch (error) {
873
+ this.log("Error getting language levels:", error);
874
+ return null;
875
+ }
876
+ }
877
+ /**
878
+ * Get historical academic ranking data across multiple years.
879
+ *
880
+ * Fetches data from ranking.aspx showing the student's rank, average, and class for each academic year.
881
+ * The table ID can vary, so multiple IDs are tried with a fallback to structure-based detection.
882
+ *
883
+ * @returns Array of RankingEntry objects, or null if unavailable
884
+ */
885
+ async getRanking() {
886
+ try {
887
+ const response = await this.client.get(this.RANKING_URL, { maxRedirects: 5 });
888
+ if (isLoginPage(response.request?.res?.responseUrl ?? "")) {
889
+ this.log("Session expired - redirected to login");
890
+ return null;
891
+ }
892
+ const $ = cheerio2.load(response.data);
893
+ let table = $();
894
+ const possibleIds = [
895
+ "ContentPlaceHolder1_GridView1_5",
896
+ "ContentPlaceHolder1_GridView1_4",
897
+ "ContentPlaceHolder1_GridView1_3",
898
+ "ContentPlaceHolder1_GridView1_2",
899
+ "ContentPlaceHolder1_GridView1"
900
+ ];
901
+ for (const id of possibleIds) {
902
+ table = $(`#${id}`);
903
+ if (table.length > 0) {
904
+ this.log("Found ranking table with ID:", id);
905
+ break;
906
+ }
907
+ }
908
+ if (table.length === 0) {
909
+ this.log("Trying fallback table detection");
910
+ $("table").each((_, el) => {
911
+ const firstRow = $(el).find("tr").first();
912
+ const headers2 = firstRow.find("th").map((_2, th) => $(th).text().trim().toLowerCase()).get();
913
+ if (headers2.some((h) => h.includes("a.u") || h.includes("rang"))) {
914
+ table = $(el);
915
+ this.log("Found ranking table by structure");
916
+ return false;
917
+ }
918
+ });
919
+ }
920
+ if (table.length === 0) {
921
+ this.log("Ranking table not found");
922
+ return null;
923
+ }
924
+ const { headers, rows } = parseTable($, table);
925
+ if (rows.length === 0) {
926
+ this.log("No ranking data found");
927
+ return null;
928
+ }
929
+ const headerLower = headers.map((h) => h.toLowerCase());
930
+ const auIdx = headerLower.findIndex((h) => h.includes("a.u") || h.includes("ann\xE9e"));
931
+ const classeIdx = headerLower.findIndex((h) => h.includes("classe"));
932
+ const moyenneIdx = headerLower.findIndex((h) => h.includes("moyenne"));
933
+ const rangIdx = headerLower.findIndex((h) => h.includes("rang"));
934
+ return rows.map((row) => ({
935
+ anneeUniversitaire: auIdx >= 0 ? row[auIdx] ?? null : null,
936
+ classe: classeIdx >= 0 ? row[classeIdx] ?? null : null,
937
+ moyenne: moyenneIdx >= 0 ? row[moyenneIdx] ?? null : null,
938
+ rang: rangIdx >= 0 ? row[rangIdx] ?? null : null
939
+ }));
940
+ } catch (error) {
941
+ this.log("Error getting ranking:", error);
942
+ return null;
943
+ }
944
+ }
651
945
  };
652
946
  var index_default = EspritClient;
653
947
  export {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/utils.ts"],"sourcesContent":["import axios, { AxiosInstance } from 'axios';\r\nimport { wrapper } from 'axios-cookiejar-support';\r\nimport { CookieJar } from 'tough-cookie';\r\nimport * as cheerio from 'cheerio';\r\nimport qs from 'qs';\r\n\r\nimport type {\r\n Cookie,\r\n LoginResult,\r\n EspritClientConfig,\r\n Grade,\r\n GradeSummary,\r\n Absence,\r\n Credit,\r\n Schedule,\r\n StudentInfo,\r\n} from './types';\r\n\r\nimport {\r\n extractASPNetFormData,\r\n findInputByPatterns,\r\n getInputName,\r\n parseTable,\r\n parseGrades,\r\n calculateGradeSummary,\r\n isLoginPage,\r\n isHomePage,\r\n} from './utils';\r\n\r\n// Re-export types for consumers\r\nexport * from './types';\r\nexport { calculateGradeSummary, calculateModuleAverage } from './utils';\r\n\r\n/**\r\n * ESPRIT Student Portal Client\r\n * \r\n * A TypeScript client for scraping student data from the ESPRIT university portal.\r\n * Handles ASP.NET session management, login flow, and data extraction.\r\n * \r\n * @example\r\n * ```typescript\r\n * import { EspritClient } from '@lime1/esprit-ts';\r\n * \r\n * const client = new EspritClient();\r\n * const result = await client.login('your-id', 'your-password');\r\n * \r\n * if (result.success) {\r\n * const grades = await client.getGrades();\r\n * console.log(grades);\r\n * }\r\n * ```\r\n */\r\nexport class EspritClient {\r\n private client: AxiosInstance;\r\n private cookieJar: CookieJar;\r\n private debug: boolean;\r\n\r\n // URL constants\r\n private readonly LOGIN_URL = 'https://esprit-tn.com/esponline/online/default.aspx';\r\n private readonly HOME_URL = 'https://esprit-tn.com/esponline/Etudiants/Accueil.aspx';\r\n private readonly GRADES_URL = 'https://esprit-tn.com/ESPOnline/Etudiants/Resultat2021.aspx';\r\n private readonly ABSENCES_URL = 'https://esprit-tn.com/ESPOnline/Etudiants/absenceetud.aspx';\r\n private readonly CREDITS_URL = 'https://esprit-tn.com/ESPOnline/Etudiants/Historique_Cr%C3%A9dit.aspx';\r\n private readonly SCHEDULES_URL = 'https://esprit-tn.com/ESPOnline/Etudiants/Emplois.aspx';\r\n private readonly LOGOUT_URLS = [\r\n 'https://esprit-tn.com/esponline/Etudiants/Deconnexion.aspx',\r\n 'https://esprit-tn.com/esponline/online/Deconnexion.aspx',\r\n ];\r\n\r\n /**\r\n * Create a new EspritClient instance.\r\n * \r\n * @param config - Optional configuration options\r\n */\r\n constructor(config: EspritClientConfig = {}) {\r\n this.debug = config.debug ?? false;\r\n this.cookieJar = new CookieJar();\r\n\r\n // Create axios instance with cookie jar support\r\n this.client = wrapper(axios.create({\r\n jar: this.cookieJar,\r\n withCredentials: true,\r\n timeout: config.timeout ?? 30000,\r\n headers: {\r\n 'User-Agent': config.userAgent ??\r\n 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',\r\n 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',\r\n 'Accept-Language': 'en-US,en;q=0.5',\r\n 'Connection': 'keep-alive',\r\n },\r\n maxRedirects: 5,\r\n }));\r\n }\r\n\r\n /**\r\n * Log a debug message if debug mode is enabled.\r\n */\r\n private log(message: string, ...args: unknown[]): void {\r\n if (this.debug) {\r\n console.log(`[EspritClient] ${message}`, ...args);\r\n }\r\n }\r\n\r\n /**\r\n * Login to the ESPRIT student portal.\r\n * \r\n * This handles the multi-step ASP.NET login flow:\r\n * 1. Load login page and extract ViewState\r\n * 2. Submit student ID with checkbox\r\n * 3. Submit password\r\n * \r\n * @param identifier - Student ID\r\n * @param password - Student password\r\n * @returns LoginResult with success status and cookies\r\n */\r\n async login(identifier: string, password: string): Promise<LoginResult> {\r\n try {\r\n // Step 1: Get the initial login page\r\n this.log('Loading login page...');\r\n const initialResponse = await this.client.get(this.LOGIN_URL);\r\n\r\n if (initialResponse.status !== 200) {\r\n return { success: false, cookies: [], message: 'Failed to load login page' };\r\n }\r\n\r\n let $ = cheerio.load(initialResponse.data);\r\n let formData = extractASPNetFormData(initialResponse.data);\r\n\r\n // Find the ID input field\r\n const idInput = findInputByPatterns($, ['textbox3', 'textbox1']);\r\n const idFieldName = idInput ? getInputName(idInput, $) : 'ctl00$ContentPlaceHolder1$TextBox3';\r\n this.log('Found ID field:', idFieldName);\r\n\r\n // Find the checkbox\r\n const checkbox = $('input[type=\"checkbox\"]').first();\r\n const checkboxName = checkbox.length > 0 ? getInputName(checkbox, $) : '';\r\n this.log('Found checkbox:', checkboxName);\r\n\r\n // Step 2: Check the checkbox to reveal continue button\r\n if (checkboxName) {\r\n const checkboxFormData = {\r\n ...formData,\r\n [idFieldName]: identifier,\r\n [checkboxName]: 'on',\r\n __EVENTTARGET: checkboxName,\r\n __EVENTARGUMENT: '',\r\n };\r\n\r\n this.log('Checking checkbox...');\r\n const checkboxResponse = await this.client.post(\r\n this.LOGIN_URL,\r\n qs.stringify(checkboxFormData),\r\n { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }\r\n );\r\n\r\n if (checkboxResponse.status === 200) {\r\n $ = cheerio.load(checkboxResponse.data);\r\n formData = extractASPNetFormData(checkboxResponse.data);\r\n\r\n // Look for continue button and click it if present\r\n const continueButton = findInputByPatterns($, ['continuer', 'continue']);\r\n if (continueButton) {\r\n const continueButtonName = getInputName(continueButton, $);\r\n this.log('Clicking continue button:', continueButtonName);\r\n\r\n const continueFormData = {\r\n ...formData,\r\n [idFieldName]: identifier,\r\n [checkboxName]: 'on',\r\n __EVENTTARGET: continueButtonName,\r\n __EVENTARGUMENT: '',\r\n };\r\n\r\n const continueResponse = await this.client.post(\r\n this.LOGIN_URL,\r\n qs.stringify(continueFormData),\r\n { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }\r\n );\r\n\r\n if (continueResponse.status === 200) {\r\n $ = cheerio.load(continueResponse.data);\r\n formData = extractASPNetFormData(continueResponse.data);\r\n }\r\n }\r\n }\r\n }\r\n\r\n // Step 3: Find and click the \"Suivant\" button to move to password page\r\n const suivantButton = findInputByPatterns($, ['button3', 'button1', 'suivant', 'next']);\r\n const suivantButtonName = suivantButton\r\n ? getInputName(suivantButton, $)\r\n : 'ctl00$ContentPlaceHolder1$Button3';\r\n\r\n this.log('Clicking Suivant button:', suivantButtonName);\r\n\r\n const suivantFormData = {\r\n ...formData,\r\n [idFieldName]: identifier,\r\n ...(checkboxName ? { [checkboxName]: 'on' } : {}),\r\n __EVENTTARGET: suivantButtonName,\r\n __EVENTARGUMENT: '',\r\n };\r\n\r\n const suivantResponse = await this.client.post(\r\n this.LOGIN_URL,\r\n qs.stringify(suivantFormData),\r\n { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }\r\n );\r\n\r\n if (suivantResponse.status !== 200) {\r\n return { success: false, cookies: [], message: 'Failed to submit ID' };\r\n }\r\n\r\n $ = cheerio.load(suivantResponse.data);\r\n\r\n // Check if we're still on the ID page (incorrect ID)\r\n const passwordField = findInputByPatterns($, ['textbox7']);\r\n const idFieldStillPresent = findInputByPatterns($, ['textbox3']);\r\n\r\n if (idFieldStillPresent && !passwordField) {\r\n return { success: false, cookies: [], message: 'Identifiant incorrect !' };\r\n }\r\n\r\n this.log('Now on password page...');\r\n\r\n // Step 4: Submit password\r\n formData = extractASPNetFormData(suivantResponse.data);\r\n\r\n const passwordFieldName = passwordField\r\n ? getInputName(passwordField, $)\r\n : 'ctl00$ContentPlaceHolder1$TextBox7';\r\n\r\n const connexionButton = findInputByPatterns($, ['buttonetudiant', 'button2', 'connexion', 'connect']);\r\n const connexionButtonName = connexionButton\r\n ? getInputName(connexionButton, $)\r\n : 'ctl00$ContentPlaceHolder1$ButtonEtudiant';\r\n\r\n this.log('Submitting password with button:', connexionButtonName);\r\n\r\n const passwordFormData = {\r\n ...formData,\r\n [passwordFieldName]: password,\r\n __EVENTTARGET: connexionButtonName,\r\n __EVENTARGUMENT: '',\r\n };\r\n\r\n const loginResponse = await this.client.post(\r\n suivantResponse.request?.res?.responseUrl ?? this.LOGIN_URL,\r\n qs.stringify(passwordFormData),\r\n {\r\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\r\n maxRedirects: 5,\r\n }\r\n );\r\n\r\n const finalUrl = loginResponse.request?.res?.responseUrl ?? '';\r\n this.log('Final URL:', finalUrl);\r\n\r\n // Check for incorrect password (still on login page)\r\n $ = cheerio.load(loginResponse.data);\r\n const passwordStillPresent = findInputByPatterns($, ['textbox7']);\r\n const stillOnLoginPage = isLoginPage(finalUrl);\r\n\r\n if (stillOnLoginPage && passwordStillPresent) {\r\n return { success: false, cookies: [], message: 'Mot de passe incorrect !' };\r\n }\r\n\r\n // Check for success indicators\r\n const successIndicators = [\r\n 'Vous pouvez consulter dans cet espace',\r\n 'Espace Etudiant',\r\n 'Accueil.aspx',\r\n 'Label2',\r\n 'Label3',\r\n ];\r\n\r\n const onHomePage = isHomePage(finalUrl);\r\n const hasSuccessIndicator = successIndicators.some(\r\n indicator => loginResponse.data.includes(indicator)\r\n );\r\n\r\n if (!stillOnLoginPage && (onHomePage || hasSuccessIndicator)) {\r\n this.log('Login successful!');\r\n\r\n // Extract cookies\r\n const cookies: Cookie[] = await this.getCookies();\r\n\r\n return { success: true, cookies, message: 'Login successful!' };\r\n }\r\n\r\n return { success: false, cookies: [], message: 'Login failed - unknown error' };\r\n\r\n } catch (error) {\r\n const message = error instanceof Error ? error.message : 'Unknown error';\r\n this.log('Login error:', message);\r\n return { success: false, cookies: [], message };\r\n }\r\n }\r\n\r\n /**\r\n * Logout from the ESPRIT portal.\r\n * \r\n * @returns True if logout was successful\r\n */\r\n async logout(): Promise<boolean> {\r\n try {\r\n // Method 1: Try ASP.NET postback mechanism\r\n this.log('Attempting logout via postback...');\r\n\r\n const homeResponse = await this.client.get(this.HOME_URL, { maxRedirects: 5 });\r\n\r\n if (homeResponse.status === 200 && !isLoginPage(homeResponse.request?.res?.responseUrl ?? '')) {\r\n const formData = extractASPNetFormData(homeResponse.data);\r\n\r\n const logoutFormData = {\r\n ...formData,\r\n __EVENTTARGET: 'ctl00$LinkButton1',\r\n __EVENTARGUMENT: '',\r\n };\r\n\r\n await this.client.post(\r\n this.HOME_URL,\r\n qs.stringify(logoutFormData),\r\n { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }\r\n );\r\n\r\n this.log('Logout successful via postback');\r\n return true;\r\n }\r\n\r\n // Method 2: Try direct logout URLs\r\n for (const logoutUrl of this.LOGOUT_URLS) {\r\n try {\r\n this.log('Trying logout URL:', logoutUrl);\r\n await this.client.get(logoutUrl, { maxRedirects: 5 });\r\n this.log('Logout successful via URL');\r\n return true;\r\n } catch {\r\n continue;\r\n }\r\n }\r\n\r\n // Method 3: Clear cookies as fallback\r\n this.log('Clearing cookies...');\r\n this.cookieJar.removeAllCookiesSync();\r\n return true;\r\n\r\n } catch (error) {\r\n this.log('Logout error:', error);\r\n this.cookieJar.removeAllCookiesSync();\r\n return true;\r\n }\r\n }\r\n\r\n /**\r\n * Get current session cookies.\r\n * \r\n * @returns Array of Cookie objects\r\n */\r\n async getCookies(): Promise<Cookie[]> {\r\n const cookies = await this.cookieJar.getCookies(this.LOGIN_URL);\r\n return cookies.map(cookie => ({\r\n name: cookie.key,\r\n value: cookie.value,\r\n domain: cookie.domain ?? '',\r\n path: cookie.path ?? '/',\r\n }));\r\n }\r\n\r\n /**\r\n * Get student grades.\r\n * \r\n * @returns Array of Grade objects or null if page cannot be loaded\r\n */\r\n async getGrades(): Promise<Grade[] | null> {\r\n try {\r\n const response = await this.client.get(this.GRADES_URL, { maxRedirects: 5 });\r\n\r\n if (isLoginPage(response.request?.res?.responseUrl ?? '')) {\r\n this.log('Session expired - redirected to login');\r\n return null;\r\n }\r\n\r\n const $ = cheerio.load(response.data);\r\n\r\n // Check for expected page content\r\n const h1Tag = $('h1').filter((_, el) =>\r\n $(el).text().includes('Notes Des Modules')\r\n );\r\n\r\n if (h1Tag.length === 0) {\r\n this.log('Page does not contain expected content');\r\n return null;\r\n }\r\n\r\n // Find the grades table\r\n const table = $('#ContentPlaceHolder1_GridView1');\r\n if (table.length === 0) {\r\n this.log('Grades table not found');\r\n return null;\r\n }\r\n\r\n const { headers, rows } = parseTable($, table);\r\n return parseGrades(headers, rows);\r\n\r\n } catch (error) {\r\n this.log('Error getting grades:', error);\r\n return null;\r\n }\r\n }\r\n\r\n /**\r\n * Get grades with calculated averages.\r\n * \r\n * @returns GradeSummary with grades and total average, or null if page cannot be loaded\r\n */\r\n async getGradesWithAverage(): Promise<GradeSummary | null> {\r\n const grades = await this.getGrades();\r\n if (!grades) return null;\r\n return calculateGradeSummary(grades);\r\n }\r\n\r\n /**\r\n * Get student absences.\r\n * \r\n * @returns Array of Absence records (as key-value objects) or null\r\n */\r\n async getAbsences(): Promise<Absence[] | null> {\r\n try {\r\n const response = await this.client.get(this.ABSENCES_URL, { maxRedirects: 5 });\r\n\r\n if (isLoginPage(response.request?.res?.responseUrl ?? '')) {\r\n this.log('Session expired - redirected to login');\r\n return null;\r\n }\r\n\r\n const $ = cheerio.load(response.data);\r\n\r\n // Check for expected page content\r\n const strongTag = $('strong').filter((_, el) =>\r\n $(el).text().includes('Absence')\r\n );\r\n\r\n if (strongTag.length === 0) {\r\n this.log('Page does not contain expected content');\r\n return null;\r\n }\r\n\r\n // Find the absences table\r\n const table = $('#ContentPlaceHolder1_GridView2');\r\n if (table.length === 0) {\r\n this.log('Absences table not found');\r\n return null;\r\n }\r\n\r\n const { headers, rows } = parseTable($, table);\r\n\r\n // Convert to objects with header keys\r\n return rows.map(row => {\r\n const absence: Absence = {};\r\n headers.forEach((header, index) => {\r\n absence[header] = row[index] ?? '';\r\n });\r\n return absence;\r\n });\r\n\r\n } catch (error) {\r\n this.log('Error getting absences:', error);\r\n return null;\r\n }\r\n }\r\n\r\n /**\r\n * Get student credit history.\r\n * \r\n * @returns Array of Credit records (as key-value objects) or null\r\n */\r\n async getCredits(): Promise<Credit[] | null> {\r\n try {\r\n const response = await this.client.get(this.CREDITS_URL, { maxRedirects: 5 });\r\n\r\n if (isLoginPage(response.request?.res?.responseUrl ?? '')) {\r\n this.log('Session expired - redirected to login');\r\n return null;\r\n }\r\n\r\n const $ = cheerio.load(response.data);\r\n\r\n // Find the credits table\r\n let table = $('#ContentPlaceHolder1_GridView1');\r\n\r\n if (table.length === 0) {\r\n // Try to find table by content\r\n $('table').each((_, el) => {\r\n const firstRow = $(el).find('tr').first();\r\n const headers = firstRow.find('th').map((_, th) => $(th).text().trim()).get();\r\n if (headers.some(h => h.includes('Année') || h.includes('enseignement'))) {\r\n table = $(el);\r\n return false; // break\r\n }\r\n });\r\n }\r\n\r\n if (table.length === 0) {\r\n this.log('Credits table not found');\r\n return null;\r\n }\r\n\r\n const { headers, rows } = parseTable($, table);\r\n\r\n // Convert to objects with header keys\r\n return rows.map(row => {\r\n const credit: Credit = {};\r\n headers.forEach((header, index) => {\r\n credit[header] = row[index] ?? '';\r\n });\r\n return credit;\r\n });\r\n\r\n } catch (error) {\r\n this.log('Error getting credits:', error);\r\n return null;\r\n }\r\n }\r\n\r\n /**\r\n * Get available schedules.\r\n * \r\n * @returns Array of Schedule objects or null\r\n */\r\n async getSchedules(): Promise<Schedule[] | null> {\r\n try {\r\n const response = await this.client.get(this.SCHEDULES_URL, { maxRedirects: 5 });\r\n\r\n if (isLoginPage(response.request?.res?.responseUrl ?? '')) {\r\n this.log('Session expired - redirected to login');\r\n return null;\r\n }\r\n\r\n const $ = cheerio.load(response.data);\r\n\r\n // Check for expected page content\r\n const strongTag = $('strong').filter((_, el) =>\r\n $(el).text().includes('Emploi du temps')\r\n );\r\n\r\n if (strongTag.length === 0) {\r\n this.log('Page does not contain expected content');\r\n return null;\r\n }\r\n\r\n // Find the schedules table\r\n const table = $('#ContentPlaceHolder1_GridView1');\r\n if (table.length === 0) {\r\n this.log('Schedules table not found');\r\n return null;\r\n }\r\n\r\n const schedules: Schedule[] = [];\r\n\r\n table.find('tr').each((index, row) => {\r\n if (index === 0) return; // Skip header\r\n\r\n const cells = $(row).find('td');\r\n const rawData: string[] = [];\r\n let name = '';\r\n let link: string | null = null;\r\n\r\n cells.each((i, cell) => {\r\n const cellText = $(cell).text().trim();\r\n const cellLink = $(cell).find('a');\r\n\r\n if (i === 0) {\r\n name = cellText;\r\n }\r\n\r\n if (cellLink.length > 0) {\r\n const href = cellLink.attr('href') ?? '';\r\n // Extract postback target from javascript call\r\n const match = href.match(/'([^']+)'/);\r\n link = match ? match[1] : href;\r\n }\r\n\r\n rawData.push(cellText);\r\n });\r\n\r\n if (name) {\r\n schedules.push({ name, link, rawData });\r\n }\r\n });\r\n\r\n return schedules;\r\n\r\n } catch (error) {\r\n this.log('Error getting schedules:', error);\r\n return null;\r\n }\r\n }\r\n\r\n /**\r\n * Get student name from the home page.\r\n * \r\n * @returns Student name or null\r\n */\r\n async getStudentName(): Promise<string | null> {\r\n try {\r\n const response = await this.client.get(this.HOME_URL, { maxRedirects: 5 });\r\n\r\n if (isLoginPage(response.request?.res?.responseUrl ?? '')) {\r\n this.log('Session expired - redirected to login');\r\n return null;\r\n }\r\n\r\n const $ = cheerio.load(response.data);\r\n\r\n // Try multiple selectors for the name label\r\n const selectors = [\r\n '#Label2',\r\n '#ContentPlaceHolder1_Label2',\r\n 'span.h4.text-info',\r\n ];\r\n\r\n for (const selector of selectors) {\r\n const element = $(selector);\r\n if (element.length > 0) {\r\n const text = element.text().trim();\r\n if (text) return text;\r\n }\r\n }\r\n\r\n this.log('Student name not found');\r\n return null;\r\n\r\n } catch (error) {\r\n this.log('Error getting student name:', error);\r\n return null;\r\n }\r\n }\r\n\r\n /**\r\n * Get student class from the home page.\r\n * \r\n * @returns Student class name or null\r\n */\r\n async getStudentClass(): Promise<string | null> {\r\n try {\r\n const response = await this.client.get(this.HOME_URL, { maxRedirects: 5 });\r\n\r\n if (isLoginPage(response.request?.res?.responseUrl ?? '')) {\r\n this.log('Session expired - redirected to login');\r\n return null;\r\n }\r\n\r\n const $ = cheerio.load(response.data);\r\n\r\n // Try multiple selectors for the class label\r\n const selectors = [\r\n '#Label3',\r\n '#ContentPlaceHolder1_Label3',\r\n ];\r\n\r\n for (const selector of selectors) {\r\n const element = $(selector);\r\n if (element.length > 0) {\r\n const text = element.text().trim();\r\n if (text) return text;\r\n }\r\n }\r\n\r\n this.log('Student class not found');\r\n return null;\r\n\r\n } catch (error) {\r\n this.log('Error getting student class:', error);\r\n return null;\r\n }\r\n }\r\n\r\n /**\r\n * Get both student name and class in one call.\r\n * \r\n * @returns StudentInfo object with name and className\r\n */\r\n async getStudentInfo(): Promise<StudentInfo> {\r\n try {\r\n const response = await this.client.get(this.HOME_URL, { maxRedirects: 5 });\r\n\r\n if (isLoginPage(response.request?.res?.responseUrl ?? '')) {\r\n this.log('Session expired - redirected to login');\r\n return { name: null, className: null };\r\n }\r\n\r\n const $ = cheerio.load(response.data);\r\n\r\n // Get name\r\n let name: string | null = null;\r\n for (const selector of ['#Label2', '#ContentPlaceHolder1_Label2']) {\r\n const element = $(selector);\r\n if (element.length > 0) {\r\n const text = element.text().trim();\r\n if (text) {\r\n name = text;\r\n break;\r\n }\r\n }\r\n }\r\n\r\n // Get class\r\n let className: string | null = null;\r\n for (const selector of ['#Label3', '#ContentPlaceHolder1_Label3']) {\r\n const element = $(selector);\r\n if (element.length > 0) {\r\n const text = element.text().trim();\r\n if (text) {\r\n className = text;\r\n break;\r\n }\r\n }\r\n }\r\n\r\n return { name, className };\r\n\r\n } catch (error) {\r\n this.log('Error getting student info:', error);\r\n return { name: null, className: null };\r\n }\r\n }\r\n}\r\n\r\n// Default export\r\nexport default EspritClient;\r\n","import * as cheerio from 'cheerio';\r\nimport type { CheerioAPI, Cheerio } from 'cheerio';\r\nimport type { Element, AnyNode } from 'domhandler';\r\nimport type { ASPNetFormData, Grade, GradeWithAverage, GradeSummary } from './types';\r\n\r\n/**\r\n * Extract ASP.NET hidden form fields from HTML content.\r\n * These fields are required for successful form submissions (postbacks).\r\n * \r\n * @param html - Raw HTML content of the page\r\n * @returns Object containing __VIEWSTATE, __VIEWSTATEGENERATOR, and __EVENTVALIDATION\r\n */\r\nexport function extractASPNetFormData(html: string): ASPNetFormData {\r\n const $ = cheerio.load(html);\r\n\r\n return {\r\n __VIEWSTATE: $('input#__VIEWSTATE').val() as string ?? '',\r\n __VIEWSTATEGENERATOR: $('input#__VIEWSTATEGENERATOR').val() as string ?? '',\r\n __EVENTVALIDATION: $('input#__EVENTVALIDATION').val() as string ?? '',\r\n };\r\n}\r\n\r\n/**\r\n * Find an input field by various ID patterns (case-insensitive).\r\n * ASP.NET often uses different naming conventions.\r\n * \r\n * @param $ - Cheerio instance\r\n * @param patterns - Array of ID patterns to search for (case-insensitive)\r\n * @returns The input element or null if not found\r\n */\r\nexport function findInputByPatterns(\r\n $: CheerioAPI,\r\n patterns: string[]\r\n): Cheerio<Element> | null {\r\n for (const pattern of patterns) {\r\n const lowerPattern = pattern.toLowerCase();\r\n\r\n // Try by ID\r\n const byId = $(`input`).filter((_, el) => {\r\n const id = $(el).attr('id')?.toLowerCase() ?? '';\r\n return id.includes(lowerPattern);\r\n }).first();\r\n\r\n if (byId.length > 0) return byId;\r\n\r\n // Try by name\r\n const byName = $(`input`).filter((_, el) => {\r\n const name = $(el).attr('name')?.toLowerCase() ?? '';\r\n return name.includes(lowerPattern);\r\n }).first();\r\n\r\n if (byName.length > 0) return byName;\r\n }\r\n\r\n return null;\r\n}\r\n\r\n/**\r\n * Get the name or id attribute of an input element.\r\n * Prefers 'name' attribute as it's used in form submissions.\r\n * \r\n * @param $input - Cheerio element for the input\r\n * @param $ - Cheerio instance\r\n * @returns The name or id attribute value\r\n */\r\nexport function getInputName(\r\n $input: Cheerio<Element>,\r\n $: CheerioAPI\r\n): string {\r\n return $input.attr('name') ?? $input.attr('id') ?? '';\r\n}\r\n\r\n/**\r\n * Parse a European-style number string (comma as decimal separator).\r\n * \r\n * @param value - String value to parse (e.g., \"14,5\")\r\n * @returns Parsed number or null if invalid/empty\r\n */\r\nexport function parseEuropeanNumber(value: string | undefined): number | null {\r\n if (!value || value.trim() === '') return null;\r\n\r\n // Replace comma with dot for parsing\r\n const normalized = value.trim().replace(',', '.');\r\n const parsed = parseFloat(normalized);\r\n\r\n return isNaN(parsed) ? null : parsed;\r\n}\r\n\r\n/**\r\n * Parse raw grade data from table rows into typed Grade objects.\r\n * \r\n * @param headers - Array of column headers\r\n * @param rows - Array of row data (each row is array of cell values)\r\n * @returns Array of Grade objects\r\n */\r\nexport function parseGrades(headers: string[], rows: string[][]): Grade[] {\r\n const grades: Grade[] = [];\r\n\r\n // Find column indices (case-insensitive matching)\r\n const headerLower = headers.map(h => h.toLowerCase());\r\n const designationIdx = headerLower.findIndex(h => h.includes('designation') || h.includes('module'));\r\n const coefIdx = headerLower.findIndex(h => h.includes('coef'));\r\n const ccIdx = headerLower.findIndex(h => h.includes('cc') || h.includes('note_cc'));\r\n const tpIdx = headerLower.findIndex(h => h.includes('tp') || h.includes('note_tp'));\r\n const examIdx = headerLower.findIndex(h => h.includes('exam') || h.includes('note_exam'));\r\n\r\n for (const row of rows) {\r\n if (row.length === 0) continue;\r\n\r\n grades.push({\r\n designation: designationIdx >= 0 ? row[designationIdx] ?? '' : row[0] ?? '',\r\n coefficient: coefIdx >= 0 ? parseEuropeanNumber(row[coefIdx]) : null,\r\n noteCC: ccIdx >= 0 ? parseEuropeanNumber(row[ccIdx]) : null,\r\n noteTP: tpIdx >= 0 ? parseEuropeanNumber(row[tpIdx]) : null,\r\n noteExam: examIdx >= 0 ? parseEuropeanNumber(row[examIdx]) : null,\r\n });\r\n }\r\n\r\n return grades;\r\n}\r\n\r\n/**\r\n * Calculate module average based on available grade components.\r\n * Uses the same formula as the Python version:\r\n * - Only exam: 100% exam\r\n * - Exam + CC: 60% exam + 40% CC\r\n * - Exam + TP: 80% exam + 20% TP\r\n * - All three: 50% exam + 30% CC + 20% TP\r\n * \r\n * @param grade - Grade object with available components\r\n * @returns Calculated average or null if exam grade is missing\r\n */\r\nexport function calculateModuleAverage(grade: Grade): number | null {\r\n const { noteExam, noteCC, noteTP } = grade;\r\n\r\n // Exam grade is required for calculation\r\n if (noteExam === null) return null;\r\n\r\n if (noteTP === null) {\r\n if (noteCC === null) {\r\n // Only exam grade available\r\n return noteExam;\r\n } else {\r\n // Exam + CC\r\n return noteExam * 0.6 + noteCC * 0.4;\r\n }\r\n } else if (noteCC === null) {\r\n // Exam + TP\r\n return noteExam * 0.8 + noteTP * 0.2;\r\n } else {\r\n // All three components\r\n return noteExam * 0.5 + noteCC * 0.3 + noteTP * 0.2;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate the weighted average for all grades.\r\n * \r\n * @param grades - Array of Grade objects\r\n * @returns GradeSummary with individual averages and total weighted average\r\n */\r\nexport function calculateGradeSummary(grades: Grade[]): GradeSummary {\r\n const gradesWithAverage: GradeWithAverage[] = grades.map(grade => ({\r\n ...grade,\r\n moyenne: calculateModuleAverage(grade),\r\n }));\r\n\r\n // Calculate weighted average (only for grades with valid moyenne and coefficient)\r\n let totalWeightedSum = 0;\r\n let totalCoefficient = 0;\r\n\r\n for (const grade of gradesWithAverage) {\r\n if (grade.moyenne !== null && grade.coefficient !== null) {\r\n totalWeightedSum += grade.moyenne * grade.coefficient;\r\n totalCoefficient += grade.coefficient;\r\n }\r\n }\r\n\r\n const totalAverage = totalCoefficient > 0\r\n ? totalWeightedSum / totalCoefficient\r\n : null;\r\n\r\n return {\r\n grades: gradesWithAverage,\r\n totalAverage,\r\n totalCoefficient,\r\n };\r\n}\r\n\r\n/**\r\n * Parse a table element into headers and rows.\r\n * \r\n * @param $ - Cheerio instance\r\n * @param table - Cheerio table element\r\n * @returns Object with headers and rows arrays\r\n */\r\nexport function parseTable(\r\n $: CheerioAPI,\r\n table: Cheerio<AnyNode>\r\n): { headers: string[]; rows: string[][] } {\r\n const rowElements = table.find('tr');\r\n const headers: string[] = [];\r\n const rows: string[][] = [];\r\n\r\n rowElements.each((index, row) => {\r\n const cells: string[] = [];\r\n\r\n if (index === 0) {\r\n // First row - extract headers from th elements\r\n $(row).find('th').each((_, cell) => {\r\n headers.push($(cell).text().trim());\r\n });\r\n } else {\r\n // Data rows - extract from td elements\r\n $(row).find('td').each((_, cell) => {\r\n cells.push($(cell).text().trim());\r\n });\r\n if (cells.length > 0) {\r\n rows.push(cells);\r\n }\r\n }\r\n });\r\n\r\n return { headers, rows };\r\n}\r\n\r\n/**\r\n * Check if a URL indicates the user has been redirected to the login page.\r\n * \r\n * @param url - Current page URL\r\n * @returns True if on login page\r\n */\r\nexport function isLoginPage(url: string): boolean {\r\n const lower = url.toLowerCase();\r\n return lower.includes('default.aspx') || lower.includes('login');\r\n}\r\n\r\n/**\r\n * Check if a URL indicates the user is on the home/dashboard page.\r\n * \r\n * @param url - Current page URL\r\n * @returns True if on home page\r\n */\r\nexport function isHomePage(url: string): boolean {\r\n return url.toLowerCase().includes('accueil.aspx');\r\n}\r\n"],"mappings":"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","names":["cheerio","headers","_"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/utils.ts"],"sourcesContent":["import axios, { AxiosInstance } from 'axios';\r\nimport { wrapper } from 'axios-cookiejar-support';\r\nimport { CookieJar } from 'tough-cookie';\r\nimport * as cheerio from 'cheerio';\r\nimport qs from 'qs';\r\n\r\nimport type {\r\n Cookie,\r\n LoginResult,\r\n EspritClientConfig,\r\n Grade,\r\n GradeSummary,\r\n Absence,\r\n Credit,\r\n Schedule,\r\n StudentInfo,\r\n RegularGrade,\r\n PrincipalResult,\r\n RattrapageGrade,\r\n RattrapageResult,\r\n LanguageLevel,\r\n RankingEntry,\r\n} from './types';\r\n\r\nimport {\r\n extractASPNetFormData,\r\n findInputByPatterns,\r\n getInputName,\r\n parseTable,\r\n parseGrades,\r\n calculateGradeSummary,\r\n isLoginPage,\r\n isHomePage,\r\n} from './utils';\r\n\r\n// Re-export types for consumers\r\nexport * from './types';\r\nexport { calculateGradeSummary, calculateModuleAverage } from './utils';\r\n\r\n/**\r\n * ESPRIT Student Portal Client\r\n * \r\n * A TypeScript client for scraping student data from the ESPRIT university portal.\r\n * Handles ASP.NET session management, login flow, and data extraction.\r\n * \r\n * @example\r\n * ```typescript\r\n * import { EspritClient } from '@lime1/esprit-ts';\r\n * \r\n * const client = new EspritClient();\r\n * const result = await client.login('your-id', 'your-password');\r\n * \r\n * if (result.success) {\r\n * const grades = await client.getGrades();\r\n * console.log(grades);\r\n * }\r\n * ```\r\n */\r\nexport class EspritClient {\r\n private client: AxiosInstance;\r\n private cookieJar: CookieJar;\r\n private debug: boolean;\r\n\r\n // URL constants\r\n private readonly LOGIN_URL = 'https://esprit-tn.com/esponline/online/default.aspx';\r\n private readonly HOME_URL = 'https://esprit-tn.com/esponline/Etudiants/Accueil.aspx';\r\n private readonly GRADES_URL = 'https://esprit-tn.com/ESPOnline/Etudiants/Resultat2021.aspx';\r\n private readonly ABSENCES_URL = 'https://esprit-tn.com/ESPOnline/Etudiants/absenceetud.aspx';\r\n private readonly CREDITS_URL = 'https://esprit-tn.com/ESPOnline/Etudiants/Historique_Cr%C3%A9dit.aspx';\r\n private readonly SCHEDULES_URL = 'https://esprit-tn.com/ESPOnline/Etudiants/Emplois.aspx';\r\n \r\n // New grade endpoints\r\n private readonly REGULAR_GRADES_URL = 'https://esprit-tn.com/ESPOnline/Etudiants/Resultat2021.aspx';\r\n private readonly PRINCIPAL_RESULT_URL = 'https://esprit-tn.com/ESPOnline/Etudiants/ResultatPrincipale2021.aspx';\r\n private readonly RATTRAPAGE_GRADES_URL = 'https://esprit-tn.com/ESPOnline/Etudiants/noterat.aspx';\r\n private readonly RATTRAPAGE_RESULT_URL = 'https://esprit-tn.com/ESPOnline/Etudiants/ResultatRattrapage2021.aspx';\r\n private readonly LANGUAGE_LEVELS_URL = 'https://esprit-tn.com/ESPOnline/Etudiants/LANG2.aspx';\r\n private readonly RANKING_URL = 'https://esprit-tn.com/ESPOnline/Etudiants/ranking.aspx';\r\n \r\n private readonly LOGOUT_URLS = [\r\n 'https://esprit-tn.com/esponline/Etudiants/Deconnexion.aspx',\r\n 'https://esprit-tn.com/esponline/online/Deconnexion.aspx',\r\n ];\r\n\r\n /**\r\n * Create a new EspritClient instance.\r\n * \r\n * @param config - Optional configuration options\r\n */\r\n constructor(config: EspritClientConfig = {}) {\r\n this.debug = config.debug ?? false;\r\n this.cookieJar = new CookieJar();\r\n\r\n // Create axios instance with cookie jar support\r\n this.client = wrapper(axios.create({\r\n jar: this.cookieJar,\r\n withCredentials: true,\r\n timeout: config.timeout ?? 30000,\r\n headers: {\r\n 'User-Agent': config.userAgent ??\r\n 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',\r\n 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',\r\n 'Accept-Language': 'en-US,en;q=0.5',\r\n 'Connection': 'keep-alive',\r\n },\r\n maxRedirects: 5,\r\n }));\r\n }\r\n\r\n /**\r\n * Log a debug message if debug mode is enabled.\r\n */\r\n private log(message: string, ...args: unknown[]): void {\r\n if (this.debug) {\r\n console.log(`[EspritClient] ${message}`, ...args);\r\n }\r\n }\r\n\r\n /**\r\n * Login to the ESPRIT student portal.\r\n * \r\n * This handles the multi-step ASP.NET login flow:\r\n * 1. Load login page and extract ViewState\r\n * 2. Submit student ID with checkbox\r\n * 3. Submit password\r\n * \r\n * @param identifier - Student ID\r\n * @param password - Student password\r\n * @returns LoginResult with success status and cookies\r\n */\r\n async login(identifier: string, password: string): Promise<LoginResult> {\r\n try {\r\n // Step 1: Get the initial login page\r\n this.log('Loading login page...');\r\n const initialResponse = await this.client.get(this.LOGIN_URL);\r\n\r\n if (initialResponse.status !== 200) {\r\n return { success: false, cookies: [], message: 'Failed to load login page' };\r\n }\r\n\r\n let $ = cheerio.load(initialResponse.data);\r\n let formData = extractASPNetFormData(initialResponse.data);\r\n\r\n // Find the ID input field\r\n const idInput = findInputByPatterns($, ['textbox3', 'textbox1']);\r\n const idFieldName = idInput ? getInputName(idInput, $) : 'ctl00$ContentPlaceHolder1$TextBox3';\r\n this.log('Found ID field:', idFieldName);\r\n\r\n // Find the checkbox\r\n const checkbox = $('input[type=\"checkbox\"]').first();\r\n const checkboxName = checkbox.length > 0 ? getInputName(checkbox, $) : '';\r\n this.log('Found checkbox:', checkboxName);\r\n\r\n // Step 2: Check the checkbox to reveal continue button\r\n if (checkboxName) {\r\n const checkboxFormData = {\r\n ...formData,\r\n [idFieldName]: identifier,\r\n [checkboxName]: 'on',\r\n __EVENTTARGET: checkboxName,\r\n __EVENTARGUMENT: '',\r\n };\r\n\r\n this.log('Checking checkbox...');\r\n const checkboxResponse = await this.client.post(\r\n this.LOGIN_URL,\r\n qs.stringify(checkboxFormData),\r\n { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }\r\n );\r\n\r\n if (checkboxResponse.status === 200) {\r\n $ = cheerio.load(checkboxResponse.data);\r\n formData = extractASPNetFormData(checkboxResponse.data);\r\n\r\n // Look for continue button and click it if present\r\n const continueButton = findInputByPatterns($, ['continuer', 'continue']);\r\n if (continueButton) {\r\n const continueButtonName = getInputName(continueButton, $);\r\n this.log('Clicking continue button:', continueButtonName);\r\n\r\n const continueFormData = {\r\n ...formData,\r\n [idFieldName]: identifier,\r\n [checkboxName]: 'on',\r\n __EVENTTARGET: continueButtonName,\r\n __EVENTARGUMENT: '',\r\n };\r\n\r\n const continueResponse = await this.client.post(\r\n this.LOGIN_URL,\r\n qs.stringify(continueFormData),\r\n { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }\r\n );\r\n\r\n if (continueResponse.status === 200) {\r\n $ = cheerio.load(continueResponse.data);\r\n formData = extractASPNetFormData(continueResponse.data);\r\n }\r\n }\r\n }\r\n }\r\n\r\n // Step 3: Find and click the \"Suivant\" button to move to password page\r\n const suivantButton = findInputByPatterns($, ['button3', 'button1', 'suivant', 'next']);\r\n const suivantButtonName = suivantButton\r\n ? getInputName(suivantButton, $)\r\n : 'ctl00$ContentPlaceHolder1$Button3';\r\n\r\n this.log('Clicking Suivant button:', suivantButtonName);\r\n\r\n const suivantFormData = {\r\n ...formData,\r\n [idFieldName]: identifier,\r\n ...(checkboxName ? { [checkboxName]: 'on' } : {}),\r\n __EVENTTARGET: suivantButtonName,\r\n __EVENTARGUMENT: '',\r\n };\r\n\r\n const suivantResponse = await this.client.post(\r\n this.LOGIN_URL,\r\n qs.stringify(suivantFormData),\r\n { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }\r\n );\r\n\r\n if (suivantResponse.status !== 200) {\r\n return { success: false, cookies: [], message: 'Failed to submit ID' };\r\n }\r\n\r\n $ = cheerio.load(suivantResponse.data);\r\n\r\n // Check if we're still on the ID page (incorrect ID)\r\n const passwordField = findInputByPatterns($, ['textbox7']);\r\n const idFieldStillPresent = findInputByPatterns($, ['textbox3']);\r\n\r\n if (idFieldStillPresent && !passwordField) {\r\n return { success: false, cookies: [], message: 'Identifiant incorrect !' };\r\n }\r\n\r\n this.log('Now on password page...');\r\n\r\n // Step 4: Submit password\r\n formData = extractASPNetFormData(suivantResponse.data);\r\n\r\n const passwordFieldName = passwordField\r\n ? getInputName(passwordField, $)\r\n : 'ctl00$ContentPlaceHolder1$TextBox7';\r\n\r\n const connexionButton = findInputByPatterns($, ['buttonetudiant', 'button2', 'connexion', 'connect']);\r\n const connexionButtonName = connexionButton\r\n ? getInputName(connexionButton, $)\r\n : 'ctl00$ContentPlaceHolder1$ButtonEtudiant';\r\n\r\n this.log('Submitting password with button:', connexionButtonName);\r\n\r\n const passwordFormData = {\r\n ...formData,\r\n [passwordFieldName]: password,\r\n __EVENTTARGET: connexionButtonName,\r\n __EVENTARGUMENT: '',\r\n };\r\n\r\n const loginResponse = await this.client.post(\r\n suivantResponse.request?.res?.responseUrl ?? this.LOGIN_URL,\r\n qs.stringify(passwordFormData),\r\n {\r\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\r\n maxRedirects: 5,\r\n }\r\n );\r\n\r\n const finalUrl = loginResponse.request?.res?.responseUrl ?? '';\r\n this.log('Final URL:', finalUrl);\r\n\r\n // Check for incorrect password (still on login page)\r\n $ = cheerio.load(loginResponse.data);\r\n const passwordStillPresent = findInputByPatterns($, ['textbox7']);\r\n const stillOnLoginPage = isLoginPage(finalUrl);\r\n\r\n if (stillOnLoginPage && passwordStillPresent) {\r\n return { success: false, cookies: [], message: 'Mot de passe incorrect !' };\r\n }\r\n\r\n // Check for success indicators\r\n const successIndicators = [\r\n 'Vous pouvez consulter dans cet espace',\r\n 'Espace Etudiant',\r\n 'Accueil.aspx',\r\n 'Label2',\r\n 'Label3',\r\n ];\r\n\r\n const onHomePage = isHomePage(finalUrl);\r\n const hasSuccessIndicator = successIndicators.some(\r\n indicator => loginResponse.data.includes(indicator)\r\n );\r\n\r\n if (!stillOnLoginPage && (onHomePage || hasSuccessIndicator)) {\r\n this.log('Login successful!');\r\n\r\n // Extract cookies\r\n const cookies: Cookie[] = await this.getCookies();\r\n\r\n return { success: true, cookies, message: 'Login successful!' };\r\n }\r\n\r\n return { success: false, cookies: [], message: 'Login failed - unknown error' };\r\n\r\n } catch (error) {\r\n const message = error instanceof Error ? error.message : 'Unknown error';\r\n this.log('Login error:', message);\r\n return { success: false, cookies: [], message };\r\n }\r\n }\r\n\r\n /**\r\n * Logout from the ESPRIT portal.\r\n * \r\n * @returns True if logout was successful\r\n */\r\n async logout(): Promise<boolean> {\r\n try {\r\n // Method 1: Try ASP.NET postback mechanism\r\n this.log('Attempting logout via postback...');\r\n\r\n const homeResponse = await this.client.get(this.HOME_URL, { maxRedirects: 5 });\r\n\r\n if (homeResponse.status === 200 && !isLoginPage(homeResponse.request?.res?.responseUrl ?? '')) {\r\n const formData = extractASPNetFormData(homeResponse.data);\r\n\r\n const logoutFormData = {\r\n ...formData,\r\n __EVENTTARGET: 'ctl00$LinkButton1',\r\n __EVENTARGUMENT: '',\r\n };\r\n\r\n await this.client.post(\r\n this.HOME_URL,\r\n qs.stringify(logoutFormData),\r\n { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }\r\n );\r\n\r\n this.log('Logout successful via postback');\r\n return true;\r\n }\r\n\r\n // Method 2: Try direct logout URLs\r\n for (const logoutUrl of this.LOGOUT_URLS) {\r\n try {\r\n this.log('Trying logout URL:', logoutUrl);\r\n await this.client.get(logoutUrl, { maxRedirects: 5 });\r\n this.log('Logout successful via URL');\r\n return true;\r\n } catch {\r\n continue;\r\n }\r\n }\r\n\r\n // Method 3: Clear cookies as fallback\r\n this.log('Clearing cookies...');\r\n this.cookieJar.removeAllCookiesSync();\r\n return true;\r\n\r\n } catch (error) {\r\n this.log('Logout error:', error);\r\n this.cookieJar.removeAllCookiesSync();\r\n return true;\r\n }\r\n }\r\n\r\n /**\r\n * Get current session cookies.\r\n * \r\n * @returns Array of Cookie objects\r\n */\r\n async getCookies(): Promise<Cookie[]> {\r\n const cookies = await this.cookieJar.getCookies(this.LOGIN_URL);\r\n return cookies.map(cookie => ({\r\n name: cookie.key,\r\n value: cookie.value,\r\n domain: cookie.domain ?? '',\r\n path: cookie.path ?? '/',\r\n }));\r\n }\r\n\r\n /**\r\n * Get student grades.\r\n * \r\n * @deprecated Use getRegularGrades() for more accurate naming. This method will be removed in a future version.\r\n * @returns Array of Grade objects or null if page cannot be loaded\r\n */\r\n async getGrades(): Promise<Grade[] | null> {\r\n try {\r\n const response = await this.client.get(this.GRADES_URL, { maxRedirects: 5 });\r\n\r\n if (isLoginPage(response.request?.res?.responseUrl ?? '')) {\r\n this.log('Session expired - redirected to login');\r\n return null;\r\n }\r\n\r\n const $ = cheerio.load(response.data);\r\n\r\n // Check for expected page content\r\n const h1Tag = $('h1').filter((_, el) =>\r\n $(el).text().includes('Notes Des Modules')\r\n );\r\n\r\n if (h1Tag.length === 0) {\r\n this.log('Page does not contain expected content');\r\n return null;\r\n }\r\n\r\n // Find the grades table\r\n const table = $('#ContentPlaceHolder1_GridView1');\r\n if (table.length === 0) {\r\n this.log('Grades table not found');\r\n return null;\r\n }\r\n\r\n const { headers, rows } = parseTable($, table);\r\n return parseGrades(headers, rows);\r\n\r\n } catch (error) {\r\n this.log('Error getting grades:', error);\r\n return null;\r\n }\r\n }\r\n\r\n /**\r\n * Get grades with calculated averages.\r\n * \r\n * @returns GradeSummary with grades and total average, or null if page cannot be loaded\r\n */\r\n async getGradesWithAverage(): Promise<GradeSummary | null> {\r\n const grades = await this.getGrades();\r\n if (!grades) return null;\r\n return calculateGradeSummary(grades);\r\n }\r\n\r\n /**\r\n * Get student absences.\r\n * \r\n * @returns Array of Absence records (as key-value objects) or null\r\n */\r\n async getAbsences(): Promise<Absence[] | null> {\r\n try {\r\n const response = await this.client.get(this.ABSENCES_URL, { maxRedirects: 5 });\r\n\r\n if (isLoginPage(response.request?.res?.responseUrl ?? '')) {\r\n this.log('Session expired - redirected to login');\r\n return null;\r\n }\r\n\r\n const $ = cheerio.load(response.data);\r\n\r\n // Check for expected page content\r\n const strongTag = $('strong').filter((_, el) =>\r\n $(el).text().includes('Absence')\r\n );\r\n\r\n if (strongTag.length === 0) {\r\n this.log('Page does not contain expected content');\r\n return null;\r\n }\r\n\r\n // Find the absences table\r\n const table = $('#ContentPlaceHolder1_GridView2');\r\n if (table.length === 0) {\r\n this.log('Absences table not found');\r\n return null;\r\n }\r\n\r\n const { headers, rows } = parseTable($, table);\r\n\r\n // Convert to objects with header keys\r\n return rows.map(row => {\r\n const absence: Absence = {};\r\n headers.forEach((header, index) => {\r\n absence[header] = row[index] ?? '';\r\n });\r\n return absence;\r\n });\r\n\r\n } catch (error) {\r\n this.log('Error getting absences:', error);\r\n return null;\r\n }\r\n }\r\n\r\n /**\r\n * Get student credit history.\r\n * \r\n * @returns Array of Credit records (as key-value objects) or null\r\n */\r\n async getCredits(): Promise<Credit[] | null> {\r\n try {\r\n const response = await this.client.get(this.CREDITS_URL, { maxRedirects: 5 });\r\n\r\n if (isLoginPage(response.request?.res?.responseUrl ?? '')) {\r\n this.log('Session expired - redirected to login');\r\n return null;\r\n }\r\n\r\n const $ = cheerio.load(response.data);\r\n\r\n // Find the credits table\r\n let table = $('#ContentPlaceHolder1_GridView1');\r\n\r\n if (table.length === 0) {\r\n // Try to find table by content\r\n $('table').each((_, el) => {\r\n const firstRow = $(el).find('tr').first();\r\n const headers = firstRow.find('th').map((_, th) => $(th).text().trim()).get();\r\n if (headers.some(h => h.includes('Année') || h.includes('enseignement'))) {\r\n table = $(el);\r\n return false; // break\r\n }\r\n });\r\n }\r\n\r\n if (table.length === 0) {\r\n this.log('Credits table not found');\r\n return null;\r\n }\r\n\r\n const { headers, rows } = parseTable($, table);\r\n\r\n // Convert to objects with header keys\r\n return rows.map(row => {\r\n const credit: Credit = {};\r\n headers.forEach((header, index) => {\r\n credit[header] = row[index] ?? '';\r\n });\r\n return credit;\r\n });\r\n\r\n } catch (error) {\r\n this.log('Error getting credits:', error);\r\n return null;\r\n }\r\n }\r\n\r\n /**\r\n * Get available schedules.\r\n * \r\n * @returns Array of Schedule objects or null\r\n */\r\n async getSchedules(): Promise<Schedule[] | null> {\r\n try {\r\n const response = await this.client.get(this.SCHEDULES_URL, { maxRedirects: 5 });\r\n\r\n if (isLoginPage(response.request?.res?.responseUrl ?? '')) {\r\n this.log('Session expired - redirected to login');\r\n return null;\r\n }\r\n\r\n const $ = cheerio.load(response.data);\r\n\r\n // Check for expected page content\r\n const strongTag = $('strong').filter((_, el) =>\r\n $(el).text().includes('Emploi du temps')\r\n );\r\n\r\n if (strongTag.length === 0) {\r\n this.log('Page does not contain expected content');\r\n return null;\r\n }\r\n\r\n // Find the schedules table\r\n const table = $('#ContentPlaceHolder1_GridView1');\r\n if (table.length === 0) {\r\n this.log('Schedules table not found');\r\n return null;\r\n }\r\n\r\n const schedules: Schedule[] = [];\r\n\r\n table.find('tr').each((index, row) => {\r\n if (index === 0) return; // Skip header\r\n\r\n const cells = $(row).find('td');\r\n const rawData: string[] = [];\r\n let name = '';\r\n let link: string | null = null;\r\n\r\n cells.each((i, cell) => {\r\n const cellText = $(cell).text().trim();\r\n const cellLink = $(cell).find('a');\r\n\r\n if (i === 0) {\r\n name = cellText;\r\n }\r\n\r\n if (cellLink.length > 0) {\r\n const href = cellLink.attr('href') ?? '';\r\n // Extract postback target from javascript call\r\n const match = href.match(/'([^']+)'/);\r\n link = match ? match[1] : href;\r\n }\r\n\r\n rawData.push(cellText);\r\n });\r\n\r\n if (name) {\r\n schedules.push({ name, link, rawData });\r\n }\r\n });\r\n\r\n return schedules;\r\n\r\n } catch (error) {\r\n this.log('Error getting schedules:', error);\r\n return null;\r\n }\r\n }\r\n\r\n /**\r\n * Get student name from the home page.\r\n * \r\n * @returns Student name or null\r\n */\r\n async getStudentName(): Promise<string | null> {\r\n try {\r\n const response = await this.client.get(this.HOME_URL, { maxRedirects: 5 });\r\n\r\n if (isLoginPage(response.request?.res?.responseUrl ?? '')) {\r\n this.log('Session expired - redirected to login');\r\n return null;\r\n }\r\n\r\n const $ = cheerio.load(response.data);\r\n\r\n // Try multiple selectors for the name label\r\n const selectors = [\r\n '#Label2',\r\n '#ContentPlaceHolder1_Label2',\r\n 'span.h4.text-info',\r\n ];\r\n\r\n for (const selector of selectors) {\r\n const element = $(selector);\r\n if (element.length > 0) {\r\n const text = element.text().trim();\r\n if (text) return text;\r\n }\r\n }\r\n\r\n this.log('Student name not found');\r\n return null;\r\n\r\n } catch (error) {\r\n this.log('Error getting student name:', error);\r\n return null;\r\n }\r\n }\r\n\r\n /**\r\n * Get student class from the home page.\r\n * \r\n * @returns Student class name or null\r\n */\r\n async getStudentClass(): Promise<string | null> {\r\n try {\r\n const response = await this.client.get(this.HOME_URL, { maxRedirects: 5 });\r\n\r\n if (isLoginPage(response.request?.res?.responseUrl ?? '')) {\r\n this.log('Session expired - redirected to login');\r\n return null;\r\n }\r\n\r\n const $ = cheerio.load(response.data);\r\n\r\n // Try multiple selectors for the class label\r\n const selectors = [\r\n '#Label3',\r\n '#ContentPlaceHolder1_Label3',\r\n ];\r\n\r\n for (const selector of selectors) {\r\n const element = $(selector);\r\n if (element.length > 0) {\r\n const text = element.text().trim();\r\n if (text) return text;\r\n }\r\n }\r\n\r\n this.log('Student class not found');\r\n return null;\r\n\r\n } catch (error) {\r\n this.log('Error getting student class:', error);\r\n return null;\r\n }\r\n }\r\n\r\n /**\r\n * Get both student name and class in one call.\r\n * \r\n * @returns StudentInfo object with name and className\r\n */\r\n async getStudentInfo(): Promise<StudentInfo> {\r\n try {\r\n const response = await this.client.get(this.HOME_URL, { maxRedirects: 5 });\r\n\r\n if (isLoginPage(response.request?.res?.responseUrl ?? '')) {\r\n this.log('Session expired - redirected to login');\r\n return { name: null, className: null };\r\n }\r\n\r\n const $ = cheerio.load(response.data);\r\n\r\n // Get name\r\n let name: string | null = null;\r\n for (const selector of ['#Label2', '#ContentPlaceHolder1_Label2']) {\r\n const element = $(selector);\r\n if (element.length > 0) {\r\n const text = element.text().trim();\r\n if (text) {\r\n name = text;\r\n break;\r\n }\r\n }\r\n }\r\n\r\n // Get class\r\n let className: string | null = null;\r\n for (const selector of ['#Label3', '#ContentPlaceHolder1_Label3']) {\r\n const element = $(selector);\r\n if (element.length > 0) {\r\n const text = element.text().trim();\r\n if (text) {\r\n className = text;\r\n break;\r\n }\r\n }\r\n }\r\n\r\n return { name, className };\r\n\r\n } catch (error) {\r\n this.log('Error getting student info:', error);\r\n return { name: null, className: null };\r\n }\r\n }\r\n\r\n /**\r\n * Get regular grades (grades released incrementally during the semester - Session Principale).\r\n * \r\n * This replaces the old getGrades() method with more accurate naming.\r\n * Fetches data from Resultat2021.aspx which displays module grades as they're released.\r\n * \r\n * @returns Array of string arrays where first row is headers, or null if unavailable\r\n */\r\n async getRegularGrades(): Promise<RegularGrade | null> {\r\n try {\r\n const response = await this.client.get(this.REGULAR_GRADES_URL, { maxRedirects: 5 });\r\n\r\n if (isLoginPage(response.request?.res?.responseUrl ?? '')) {\r\n this.log('Session expired - redirected to login');\r\n return null;\r\n }\r\n\r\n // Check for JavaScript alert indicating no grades available\r\n if (response.data.includes('aucune note!')) {\r\n this.log('No grades available yet');\r\n return null;\r\n }\r\n\r\n const $ = cheerio.load(response.data);\r\n\r\n // Check for expected page content (flexible text matching)\r\n const h1Tag = $('h1').filter((_, el) => {\r\n const text = $(el).text();\r\n return text.includes('Notes Des Modules');\r\n });\r\n\r\n if (h1Tag.length === 0) {\r\n this.log('Page does not contain expected content');\r\n return null;\r\n }\r\n\r\n // Find the grades table\r\n const table = $('#ContentPlaceHolder1_GridView1');\r\n if (table.length === 0) {\r\n this.log('Grades table not found');\r\n return null;\r\n }\r\n\r\n const { headers, rows } = parseTable($, table);\r\n return [headers, ...rows];\r\n\r\n } catch (error) {\r\n this.log('Error getting regular grades:', error);\r\n return null;\r\n }\r\n }\r\n\r\n /**\r\n * Get the final verdict/decision at the end of the principal session.\r\n * \r\n * Fetches data from ResultatPrincipale2021.aspx showing the overall average and pass/fail decision.\r\n * \r\n * @returns PrincipalResult with moyenneGeneral and decision, or null if unavailable\r\n */\r\n async getPrincipalResult(): Promise<PrincipalResult | null> {\r\n try {\r\n const response = await this.client.get(this.PRINCIPAL_RESULT_URL, { maxRedirects: 5 });\r\n\r\n if (isLoginPage(response.request?.res?.responseUrl ?? '')) {\r\n this.log('Session expired - redirected to login');\r\n return null;\r\n }\r\n\r\n const $ = cheerio.load(response.data);\r\n\r\n // Check for expected h1 tag (flexible matching due to whitespace)\r\n const h1Tag = $('h1').filter((_, el) => {\r\n const text = $(el).text();\r\n return text.includes('Resultat Session Principale');\r\n });\r\n\r\n if (h1Tag.length === 0) {\r\n this.log('Page does not contain expected content');\r\n return null;\r\n }\r\n\r\n // Find the result table\r\n const table = $('#ContentPlaceHolder1_GridView3');\r\n if (table.length === 0) {\r\n this.log('Result table not found');\r\n return null;\r\n }\r\n\r\n const { headers, rows } = parseTable($, table);\r\n \r\n if (rows.length === 0) {\r\n this.log('No result data found');\r\n return null;\r\n }\r\n\r\n // Find column indices\r\n const headerLower = headers.map(h => h.toLowerCase());\r\n const moyenneIdx = headerLower.findIndex(h => h.includes('moyenne'));\r\n const decisionIdx = headerLower.findIndex(h => h.includes('decision'));\r\n\r\n return {\r\n moyenneGeneral: moyenneIdx >= 0 ? rows[0][moyenneIdx] ?? null : null,\r\n decision: decisionIdx >= 0 ? rows[0][decisionIdx] ?? null : null,\r\n };\r\n\r\n } catch (error) {\r\n this.log('Error getting principal result:', error);\r\n return null;\r\n }\r\n }\r\n\r\n /**\r\n * Get rattrapage (retake) grades released incrementally during the retake session.\r\n * \r\n * Similar to regular grades but for the retake session, fetched from noterat.aspx.\r\n * \r\n * @returns Array of string arrays where first row is headers, or null if unavailable\r\n */\r\n async getRattrapageGrades(): Promise<RattrapageGrade | null> {\r\n try {\r\n const response = await this.client.get(this.RATTRAPAGE_GRADES_URL, { maxRedirects: 5 });\r\n\r\n if (isLoginPage(response.request?.res?.responseUrl ?? '')) {\r\n this.log('Session expired - redirected to login');\r\n return null;\r\n }\r\n\r\n // Check for JavaScript alert indicating no grades available\r\n if (response.data.includes('aucune note!')) {\r\n this.log('No rattrapage grades available yet');\r\n return null;\r\n }\r\n\r\n const $ = cheerio.load(response.data);\r\n\r\n // Check for expected h1 tag (flexible matching)\r\n const h1Tag = $('h1').filter((_, el) => {\r\n const text = $(el).text();\r\n return text.includes('Notes Des Modules session Rattrapage');\r\n });\r\n\r\n if (h1Tag.length === 0) {\r\n this.log('Page does not contain expected content');\r\n return null;\r\n }\r\n\r\n // Find the grades table\r\n const table = $('#ContentPlaceHolder1_GridView1');\r\n if (table.length === 0) {\r\n this.log('Rattrapage grades table not found');\r\n return null;\r\n }\r\n\r\n const { headers, rows } = parseTable($, table);\r\n return [headers, ...rows];\r\n\r\n } catch (error) {\r\n this.log('Error getting rattrapage grades:', error);\r\n return null;\r\n }\r\n }\r\n\r\n /**\r\n * Get the final verdict/decision of the rattrapage (retake) session.\r\n * \r\n * Fetches data from ResultatRattrapage2021.aspx showing the overall average and pass/fail decision.\r\n * \r\n * @returns RattrapageResult with moyenneGeneral and decision, or null if unavailable\r\n */\r\n async getRattrapageResult(): Promise<RattrapageResult | null> {\r\n try {\r\n const response = await this.client.get(this.RATTRAPAGE_RESULT_URL, { maxRedirects: 5 });\r\n\r\n if (isLoginPage(response.request?.res?.responseUrl ?? '')) {\r\n this.log('Session expired - redirected to login');\r\n return null;\r\n }\r\n\r\n const $ = cheerio.load(response.data);\r\n\r\n // Check for expected content - either h1 or span element\r\n const h1Tag = $('h1').filter((_, el) => {\r\n const text = $(el).text();\r\n return text.includes('Resultat');\r\n });\r\n\r\n const spanTag = $('#ContentPlaceHolder1_Label18').filter((_, el) => {\r\n const text = $(el).text();\r\n return text.includes('Resultat');\r\n });\r\n\r\n if (h1Tag.length === 0 && spanTag.length === 0) {\r\n this.log('Page does not contain expected content');\r\n return null;\r\n }\r\n\r\n // Find the result table\r\n const table = $('#ContentPlaceHolder1_GridView3');\r\n if (table.length === 0) {\r\n this.log('Rattrapage result table not found');\r\n return null;\r\n }\r\n\r\n const { headers, rows } = parseTable($, table);\r\n \r\n if (rows.length === 0) {\r\n this.log('No rattrapage result data found');\r\n return null;\r\n }\r\n\r\n // Find column indices\r\n const headerLower = headers.map(h => h.toLowerCase());\r\n const moyenneIdx = headerLower.findIndex(h => h.includes('moyenne'));\r\n const decisionIdx = headerLower.findIndex(h => h.includes('decision'));\r\n\r\n return {\r\n moyenneGeneral: moyenneIdx >= 0 ? rows[0][moyenneIdx] ?? null : null,\r\n decision: decisionIdx >= 0 ? rows[0][decisionIdx] ?? null : null,\r\n };\r\n\r\n } catch (error) {\r\n this.log('Error getting rattrapage result:', error);\r\n return null;\r\n }\r\n }\r\n\r\n /**\r\n * Get language proficiency levels (French and English).\r\n * \r\n * Fetches data from LANG2.aspx showing language levels like 'B1', 'B2', etc.\r\n * \r\n * @returns LanguageLevel with francais and anglais levels, or null if unavailable\r\n */\r\n async getLanguageLevels(): Promise<LanguageLevel | null> {\r\n try {\r\n const response = await this.client.get(this.LANGUAGE_LEVELS_URL, { maxRedirects: 5 });\r\n\r\n if (isLoginPage(response.request?.res?.responseUrl ?? '')) {\r\n this.log('Session expired - redirected to login');\r\n return null;\r\n }\r\n\r\n const $ = cheerio.load(response.data);\r\n\r\n // Check for expected h1 tag\r\n const h1Tag = $('h1').filter((_, el) => {\r\n const text = $(el).text();\r\n return text.includes('NIVEAU LANGUES');\r\n });\r\n\r\n if (h1Tag.length === 0) {\r\n this.log('Page does not contain expected content');\r\n return null;\r\n }\r\n\r\n // Find the language levels table\r\n const table = $('#ContentPlaceHolder1_GridView2');\r\n if (table.length === 0) {\r\n this.log('Language levels table not found');\r\n return null;\r\n }\r\n\r\n const { headers, rows } = parseTable($, table);\r\n \r\n if (rows.length === 0) {\r\n this.log('No language level data found');\r\n return null;\r\n }\r\n\r\n // Find column indices\r\n const headerLower = headers.map(h => h.toLowerCase());\r\n const francaisIdx = headerLower.findIndex(h => h.includes('francais'));\r\n const anglaisIdx = headerLower.findIndex(h => h.includes('anglais'));\r\n\r\n return {\r\n francais: francaisIdx >= 0 ? rows[0][francaisIdx] ?? null : null,\r\n anglais: anglaisIdx >= 0 ? rows[0][anglaisIdx] ?? null : null,\r\n };\r\n\r\n } catch (error) {\r\n this.log('Error getting language levels:', error);\r\n return null;\r\n }\r\n }\r\n\r\n /**\r\n * Get historical academic ranking data across multiple years.\r\n * \r\n * Fetches data from ranking.aspx showing the student's rank, average, and class for each academic year.\r\n * The table ID can vary, so multiple IDs are tried with a fallback to structure-based detection.\r\n * \r\n * @returns Array of RankingEntry objects, or null if unavailable\r\n */\r\n async getRanking(): Promise<RankingEntry[] | null> {\r\n try {\r\n const response = await this.client.get(this.RANKING_URL, { maxRedirects: 5 });\r\n\r\n if (isLoginPage(response.request?.res?.responseUrl ?? '')) {\r\n this.log('Session expired - redirected to login');\r\n return null;\r\n }\r\n\r\n const $ = cheerio.load(response.data);\r\n\r\n // Try multiple possible table IDs\r\n let table = $();\r\n const possibleIds = [\r\n 'ContentPlaceHolder1_GridView1_5',\r\n 'ContentPlaceHolder1_GridView1_4',\r\n 'ContentPlaceHolder1_GridView1_3',\r\n 'ContentPlaceHolder1_GridView1_2',\r\n 'ContentPlaceHolder1_GridView1',\r\n ];\r\n\r\n for (const id of possibleIds) {\r\n table = $(`#${id}`);\r\n if (table.length > 0) {\r\n this.log('Found ranking table with ID:', id);\r\n break;\r\n }\r\n }\r\n\r\n // Fallback: search all tables for one with 'A.U' or 'rang' in headers\r\n if (table.length === 0) {\r\n this.log('Trying fallback table detection');\r\n $('table').each((_, el) => {\r\n const firstRow = $(el).find('tr').first();\r\n const headers = firstRow.find('th').map((_, th) => $(th).text().trim().toLowerCase()).get();\r\n if (headers.some(h => h.includes('a.u') || h.includes('rang'))) {\r\n table = $(el);\r\n this.log('Found ranking table by structure');\r\n return false; // break\r\n }\r\n });\r\n }\r\n\r\n if (table.length === 0) {\r\n this.log('Ranking table not found');\r\n return null;\r\n }\r\n\r\n const { headers, rows } = parseTable($, table);\r\n \r\n if (rows.length === 0) {\r\n this.log('No ranking data found');\r\n return null;\r\n }\r\n\r\n // Find column indices\r\n const headerLower = headers.map(h => h.toLowerCase());\r\n const auIdx = headerLower.findIndex(h => h.includes('a.u') || h.includes('année'));\r\n const classeIdx = headerLower.findIndex(h => h.includes('classe'));\r\n const moyenneIdx = headerLower.findIndex(h => h.includes('moyenne'));\r\n const rangIdx = headerLower.findIndex(h => h.includes('rang'));\r\n\r\n return rows.map(row => ({\r\n anneeUniversitaire: auIdx >= 0 ? row[auIdx] ?? null : null,\r\n classe: classeIdx >= 0 ? row[classeIdx] ?? null : null,\r\n moyenne: moyenneIdx >= 0 ? row[moyenneIdx] ?? null : null,\r\n rang: rangIdx >= 0 ? row[rangIdx] ?? null : null,\r\n }));\r\n\r\n } catch (error) {\r\n this.log('Error getting ranking:', error);\r\n return null;\r\n }\r\n }\r\n}\r\n\r\n// Default export\r\nexport default EspritClient;\r\n","import * as cheerio from 'cheerio';\r\nimport type { CheerioAPI, Cheerio } from 'cheerio';\r\nimport type { Element, AnyNode } from 'domhandler';\r\nimport type { ASPNetFormData, Grade, GradeWithAverage, GradeSummary } from './types';\r\n\r\n/**\r\n * Extract ASP.NET hidden form fields from HTML content.\r\n * These fields are required for successful form submissions (postbacks).\r\n * \r\n * @param html - Raw HTML content of the page\r\n * @returns Object containing __VIEWSTATE, __VIEWSTATEGENERATOR, and __EVENTVALIDATION\r\n */\r\nexport function extractASPNetFormData(html: string): ASPNetFormData {\r\n const $ = cheerio.load(html);\r\n\r\n return {\r\n __VIEWSTATE: $('input#__VIEWSTATE').val() as string ?? '',\r\n __VIEWSTATEGENERATOR: $('input#__VIEWSTATEGENERATOR').val() as string ?? '',\r\n __EVENTVALIDATION: $('input#__EVENTVALIDATION').val() as string ?? '',\r\n };\r\n}\r\n\r\n/**\r\n * Find an input field by various ID patterns (case-insensitive).\r\n * ASP.NET often uses different naming conventions.\r\n * \r\n * @param $ - Cheerio instance\r\n * @param patterns - Array of ID patterns to search for (case-insensitive)\r\n * @returns The input element or null if not found\r\n */\r\nexport function findInputByPatterns(\r\n $: CheerioAPI,\r\n patterns: string[]\r\n): Cheerio<Element> | null {\r\n for (const pattern of patterns) {\r\n const lowerPattern = pattern.toLowerCase();\r\n\r\n // Try by ID\r\n const byId = $(`input`).filter((_, el) => {\r\n const id = $(el).attr('id')?.toLowerCase() ?? '';\r\n return id.includes(lowerPattern);\r\n }).first();\r\n\r\n if (byId.length > 0) return byId;\r\n\r\n // Try by name\r\n const byName = $(`input`).filter((_, el) => {\r\n const name = $(el).attr('name')?.toLowerCase() ?? '';\r\n return name.includes(lowerPattern);\r\n }).first();\r\n\r\n if (byName.length > 0) return byName;\r\n }\r\n\r\n return null;\r\n}\r\n\r\n/**\r\n * Get the name or id attribute of an input element.\r\n * Prefers 'name' attribute as it's used in form submissions.\r\n * \r\n * @param $input - Cheerio element for the input\r\n * @param $ - Cheerio instance\r\n * @returns The name or id attribute value\r\n */\r\nexport function getInputName(\r\n $input: Cheerio<Element>,\r\n $: CheerioAPI\r\n): string {\r\n return $input.attr('name') ?? $input.attr('id') ?? '';\r\n}\r\n\r\n/**\r\n * Parse a European-style number string (comma as decimal separator).\r\n * \r\n * @param value - String value to parse (e.g., \"14,5\")\r\n * @returns Parsed number or null if invalid/empty\r\n */\r\nexport function parseEuropeanNumber(value: string | undefined): number | null {\r\n if (!value || value.trim() === '') return null;\r\n\r\n // Replace comma with dot for parsing\r\n const normalized = value.trim().replace(',', '.');\r\n const parsed = parseFloat(normalized);\r\n\r\n return isNaN(parsed) ? null : parsed;\r\n}\r\n\r\n/**\r\n * Parse raw grade data from table rows into typed Grade objects.\r\n * \r\n * @param headers - Array of column headers\r\n * @param rows - Array of row data (each row is array of cell values)\r\n * @returns Array of Grade objects\r\n */\r\nexport function parseGrades(headers: string[], rows: string[][]): Grade[] {\r\n const grades: Grade[] = [];\r\n\r\n // Find column indices (case-insensitive matching)\r\n const headerLower = headers.map(h => h.toLowerCase());\r\n const designationIdx = headerLower.findIndex(h => h.includes('designation') || h.includes('module'));\r\n const coefIdx = headerLower.findIndex(h => h.includes('coef'));\r\n const ccIdx = headerLower.findIndex(h => h.includes('cc') || h.includes('note_cc'));\r\n const tpIdx = headerLower.findIndex(h => h.includes('tp') || h.includes('note_tp'));\r\n const examIdx = headerLower.findIndex(h => h.includes('exam') || h.includes('note_exam'));\r\n\r\n for (const row of rows) {\r\n if (row.length === 0) continue;\r\n\r\n grades.push({\r\n designation: designationIdx >= 0 ? row[designationIdx] ?? '' : row[0] ?? '',\r\n coefficient: coefIdx >= 0 ? parseEuropeanNumber(row[coefIdx]) : null,\r\n noteCC: ccIdx >= 0 ? parseEuropeanNumber(row[ccIdx]) : null,\r\n noteTP: tpIdx >= 0 ? parseEuropeanNumber(row[tpIdx]) : null,\r\n noteExam: examIdx >= 0 ? parseEuropeanNumber(row[examIdx]) : null,\r\n });\r\n }\r\n\r\n return grades;\r\n}\r\n\r\n/**\r\n * Calculate module average based on available grade components.\r\n * Uses the same formula as the Python version:\r\n * - Only exam: 100% exam\r\n * - Exam + CC: 60% exam + 40% CC\r\n * - Exam + TP: 80% exam + 20% TP\r\n * - All three: 50% exam + 30% CC + 20% TP\r\n * \r\n * @param grade - Grade object with available components\r\n * @returns Calculated average or null if exam grade is missing\r\n */\r\nexport function calculateModuleAverage(grade: Grade): number | null {\r\n const { noteExam, noteCC, noteTP } = grade;\r\n\r\n // Exam grade is required for calculation\r\n if (noteExam === null) return null;\r\n\r\n if (noteTP === null) {\r\n if (noteCC === null) {\r\n // Only exam grade available\r\n return noteExam;\r\n } else {\r\n // Exam + CC\r\n return noteExam * 0.6 + noteCC * 0.4;\r\n }\r\n } else if (noteCC === null) {\r\n // Exam + TP\r\n return noteExam * 0.8 + noteTP * 0.2;\r\n } else {\r\n // All three components\r\n return noteExam * 0.5 + noteCC * 0.3 + noteTP * 0.2;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate the weighted average for all grades.\r\n * \r\n * @param grades - Array of Grade objects\r\n * @returns GradeSummary with individual averages and total weighted average\r\n */\r\nexport function calculateGradeSummary(grades: Grade[]): GradeSummary {\r\n const gradesWithAverage: GradeWithAverage[] = grades.map(grade => ({\r\n ...grade,\r\n moyenne: calculateModuleAverage(grade),\r\n }));\r\n\r\n // Calculate weighted average (only for grades with valid moyenne and coefficient)\r\n let totalWeightedSum = 0;\r\n let totalCoefficient = 0;\r\n\r\n for (const grade of gradesWithAverage) {\r\n if (grade.moyenne !== null && grade.coefficient !== null) {\r\n totalWeightedSum += grade.moyenne * grade.coefficient;\r\n totalCoefficient += grade.coefficient;\r\n }\r\n }\r\n\r\n const totalAverage = totalCoefficient > 0\r\n ? totalWeightedSum / totalCoefficient\r\n : null;\r\n\r\n return {\r\n grades: gradesWithAverage,\r\n totalAverage,\r\n totalCoefficient,\r\n };\r\n}\r\n\r\n/**\r\n * Parse a table element into headers and rows.\r\n * \r\n * @param $ - Cheerio instance\r\n * @param table - Cheerio table element\r\n * @returns Object with headers and rows arrays\r\n */\r\nexport function parseTable(\r\n $: CheerioAPI,\r\n table: Cheerio<AnyNode>\r\n): { headers: string[]; rows: string[][] } {\r\n const rowElements = table.find('tr');\r\n const headers: string[] = [];\r\n const rows: string[][] = [];\r\n\r\n rowElements.each((index, row) => {\r\n const cells: string[] = [];\r\n\r\n if (index === 0) {\r\n // First row - extract headers from th elements\r\n $(row).find('th').each((_, cell) => {\r\n headers.push($(cell).text().trim());\r\n });\r\n } else {\r\n // Data rows - extract from td elements\r\n $(row).find('td').each((_, cell) => {\r\n cells.push($(cell).text().trim());\r\n });\r\n if (cells.length > 0) {\r\n rows.push(cells);\r\n }\r\n }\r\n });\r\n\r\n return { headers, rows };\r\n}\r\n\r\n/**\r\n * Check if a URL indicates the user has been redirected to the login page.\r\n * \r\n * @param url - Current page URL\r\n * @returns True if on login page\r\n */\r\nexport function isLoginPage(url: string): boolean {\r\n const lower = url.toLowerCase();\r\n return lower.includes('default.aspx') || lower.includes('login');\r\n}\r\n\r\n/**\r\n * Check if a URL indicates the user is on the home/dashboard page.\r\n * \r\n * @param url - Current page URL\r\n * @returns True if on home page\r\n */\r\nexport function isHomePage(url: string): boolean {\r\n return url.toLowerCase().includes('accueil.aspx');\r\n}\r\n"],"mappings":"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","names":["cheerio","headers","_"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lime1/esprit-ts",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "TypeScript client for ESPRIT student portal - scrape grades, absences, credits, and schedules",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",
@@ -64,4 +64,4 @@
64
64
  "engines": {
65
65
  "node": ">=18.0.0"
66
66
  }
67
- }
67
+ }