reddy-api-srm 1.0.6 → 1.0.8

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.
@@ -2,8 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.validatePassword = validatePassword;
4
4
  async function validatePassword({ identifier, digest, password, }) {
5
+ let res;
5
6
  try {
6
- const res = await fetch(`https://academia.srmist.edu.in/accounts/p/40-10002227248/signin/v2/primary/${identifier}/password?digest=${digest}&cli_time=${Date.now()}&servicename=ZohoCreator&service_language=en&serviceurl=https%3A%2F%2Facademia.srmist.edu.in%2Fportal%2Facademia-academic-services%2FredirectFromLogin`, {
7
+ res = await fetch(`https://academia.srmist.edu.in/accounts/p/40-10002227248/signin/v2/primary/${identifier}/password?digest=${digest}&cli_time=${Date.now()}&servicename=ZohoCreator&service_language=en&serviceurl=https%3A%2F%2Facademia.srmist.edu.in%2Fportal%2Facademia-academic-services%2FredirectFromLogin`, {
7
8
  headers: {
8
9
  accept: "*/*",
9
10
  "accept-language": "en-US,en;q=0.9",
@@ -19,7 +20,24 @@ async function validatePassword({ identifier, digest, password, }) {
19
20
  body: `{"passwordauth":{"password":"${password}"}}`,
20
21
  method: "POST",
21
22
  });
22
- const response = (await res.json());
23
+ // ── Detect redirect to Zoho sessions-reminder page ───────────────────────
24
+ if (res.redirected && res.url && res.url.includes("sessions-reminder")) {
25
+ let flowId = null;
26
+ try { const u = new URL(res.url); flowId = u.searchParams.get("flowId") ?? u.searchParams.get("flow_id") ?? null; } catch (_) {}
27
+ return { data: { statusCode: 435, message: "Maximum concurrent sessions reached. Please terminate existing sessions to continue.", captcha: { required: false, digest: null }, isConcurrentLimit: true, flowId }, isAuthenticated: false };
28
+ }
29
+ // Read body as text first — safely handles non-JSON responses
30
+ const bodyText = await res.text();
31
+ let response;
32
+ try {
33
+ response = JSON.parse(bodyText);
34
+ } catch (_) {
35
+ const lower = bodyText.toLowerCase();
36
+ if (lower.includes("sessions-reminder") || (lower.includes("concurrent") && lower.includes("session"))) {
37
+ return { data: { statusCode: 435, message: "Maximum concurrent sessions reached. Please terminate existing sessions to continue.", captcha: { required: false, digest: null }, isConcurrentLimit: true, flowId: null }, isAuthenticated: false };
38
+ }
39
+ return { error: "Internal Server Error", errorReason: new Error("Non-JSON response from login endpoint") };
40
+ }
23
41
  if (response.status_code === 201) {
24
42
  // Use filterCookies to process set-cookie header
25
43
  const setCookieHeader = res.headers.get("set-cookie");
@@ -36,9 +54,6 @@ async function validatePassword({ identifier, digest, password, }) {
36
54
  return { data, isAuthenticated: true };
37
55
  }
38
56
  // ── Concurrent / device-limit detection ──────────────────────────────────
39
- // Zoho returns status_code 435 when the concurrent session limit is hit.
40
- // The response also embeds a flowId used to call the terminate endpoint.
41
- // Field names seen in Zoho responses: flowId, flow_id, concurrent_flow_id
42
57
  const msg = (response.localized_message ?? response.message ?? "").toLowerCase();
43
58
  const isConcurrentLimit =
44
59
  response.status_code === 435 ||
@@ -46,17 +61,16 @@ async function validatePassword({ identifier, digest, password, }) {
46
61
  msg.includes("maximum number") ||
47
62
  msg.includes("device limit") ||
48
63
  msg.includes("already logged") ||
64
+ msg.includes("sessions") ||
49
65
  !!response.flowId ||
50
66
  !!response.flow_id ||
51
67
  !!response.concurrent_flow_id;
52
- // Extract the flow identifier — try all known field names
53
68
  const flowId =
54
69
  response.flowId ??
55
70
  response.flow_id ??
56
71
  response.concurrent_flow_id ??
57
72
  response.login_flow_id ??
58
73
  null;
59
- // ─────────────────────────────────────────────────────────────────────────
60
74
  const captchaRequired = msg.includes("captcha");
61
75
  const data = {
62
76
  statusCode: response.status_code,
@@ -64,13 +78,15 @@ async function validatePassword({ identifier, digest, password, }) {
64
78
  captcha: captchaRequired
65
79
  ? { required: true, digest: response.cdigest }
66
80
  : { required: false, digest: null },
67
- // NEW: concurrent session fields
68
81
  isConcurrentLimit,
69
82
  flowId,
70
83
  };
71
84
  return { data, isAuthenticated: false };
72
85
  }
73
86
  catch (e) {
87
+ if (res && res.redirected && res.url && res.url.includes("sessions-reminder")) {
88
+ return { data: { statusCode: 435, message: "Maximum concurrent sessions reached.", captcha: { required: false, digest: null }, isConcurrentLimit: true, flowId: null }, isAuthenticated: false };
89
+ }
74
90
  return { error: "Internal Server Error", errorReason: e };
75
91
  }
76
92
  }
@@ -10,6 +10,7 @@ async function fetchCalendar(cookie) {
10
10
  const url = await (0, dynamicUrl_1.calendarDynamicUrl)();
11
11
  try {
12
12
  const response = await (0, axios_1.default)(url, {
13
+ responseType: "text",
13
14
  headers: {
14
15
  accept: "*/*",
15
16
  "accept-language": "en-US,en;q=0.9",
@@ -37,58 +37,128 @@ exports.parseCalendar = parseCalendar;
37
37
  const cheerio = __importStar(require("cheerio"));
38
38
  async function parseCalendar(response) {
39
39
  try {
40
- const $outer = cheerio.load(response);
41
- const zmlValue = $outer("div.zc-pb-embed-placeholder-content").attr("zmlvalue");
42
- if (!zmlValue) {
40
+ // ── Step 1: Normalise response to a string ─────────────────────────────
41
+ let rawResponse;
42
+ if (typeof response === "string") {
43
+ rawResponse = response;
44
+ } else if (response !== null && response !== undefined && typeof response === "object") {
45
+ // Axios may auto-parse JSON — try common field names that could hold HTML
46
+ const candidate = response.data ?? response.html ?? response.content ??
47
+ response.body ?? response.result ?? response.page ?? response.pageContent;
48
+ rawResponse = typeof candidate === "string" ? candidate : JSON.stringify(response);
49
+ } else {
50
+ rawResponse = String(response ?? "");
51
+ }
52
+ // ── Step 2: Extract the HTML content ───────────────────────────────────
53
+ let htmlContent = null;
54
+ // Method 1: Zoho Page Builder embed (zmlvalue attribute)
55
+ try {
56
+ const $outer = cheerio.load(rawResponse);
57
+ const zmlEl = $outer("div.zc-pb-embed-placeholder-content");
58
+ if (zmlEl.length > 0) {
59
+ const zmlValue = zmlEl.attr("zmlvalue");
60
+ if (zmlValue && zmlValue.length > 50) htmlContent = zmlValue;
61
+ }
62
+ } catch (_) { /* ignore */ }
63
+ // Method 2: pageSanitizer.sanitize('...') — Zoho XHR page/report response
64
+ if (!htmlContent) {
65
+ const match = rawResponse.match(/pageSanitizer\.sanitize\s*\(\s*['"](.+?)['"]\s*\)/s);
66
+ if (match && match[1]) {
67
+ htmlContent = match[1]
68
+ .replace(/\\x([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
69
+ .replace(/\\\\/g, "\\")
70
+ .replace(/\\'/g, "'")
71
+ .replace(/\\"/g, '"')
72
+ .replace(/\\n/g, "\n")
73
+ .replace(/\\t/g, "\t");
74
+ }
75
+ }
76
+ // Method 3: Response already contains table HTML
77
+ if (!htmlContent && rawResponse.includes("<table") && rawResponse.includes("<tr")) {
78
+ htmlContent = rawResponse;
79
+ }
80
+ // Method 4: JSON body with embedded HTML
81
+ if (!htmlContent && rawResponse.trim().startsWith("{")) {
82
+ try {
83
+ const parsed = JSON.parse(rawResponse);
84
+ const c = parsed.data ?? parsed.html ?? parsed.content ?? parsed.body ?? parsed.result;
85
+ if (typeof c === "string" && c.includes("<table")) htmlContent = c;
86
+ } catch (_) { /* not JSON */ }
87
+ }
88
+ if (!htmlContent) {
43
89
  return { error: "Failed to extract calendar details", status: 404 };
44
90
  }
45
- const $inner = cheerio.load(zmlValue);
46
- const $mainTable = $inner("table[bgcolor='#FAFCFE']");
47
- if ($mainTable.length === 0) {
48
- return { error: "Could not find the main calendar table.", status: 500 };
91
+ // ── Step 3: Find the calendar table ────────────────────────────────────
92
+ const $ = cheerio.load(htmlContent);
93
+ let $mainTable = $("table[bgcolor='#FAFCFE']");
94
+ if (!$mainTable.length) $mainTable = $("table[bgcolor='#fafcfe']");
95
+ if (!$mainTable.length) {
96
+ // Fall back to the table with the most data
97
+ let bestTable = null;
98
+ let bestScore = 0;
99
+ $("table").each((_, el) => {
100
+ const $el = $(el);
101
+ const score = $el.find("tr").length * ($el.find("tr").first().find("td, th").length || 1);
102
+ if (score > bestScore) { bestScore = score; bestTable = $el; }
103
+ });
104
+ if (bestTable && bestScore > 10) $mainTable = bestTable;
105
+ }
106
+ if (!$mainTable.length) {
107
+ return { error: "Could not find calendar table", status: 500 };
49
108
  }
50
- const $headerRow = $mainTable.find("tr").first();
51
- const $ths = $headerRow.find("th");
52
- const monthsData = [];
53
- for (let i = 0;; i++) {
54
- const monthNameThIndex = i * 5 + 2;
55
- if (monthNameThIndex >= $ths.length)
56
- break;
57
- const monthName = $ths.eq(monthNameThIndex).find("strong").text().trim();
58
- if (monthName) {
59
- monthsData.push({ month: monthName, days: [] });
60
- }
61
- else {
62
- break;
109
+ // ── Step 4: Locate the header row and month names ──────────────────────
110
+ const MONTH_NAMES = ["january","february","march","april","may","june",
111
+ "july","august","september","october","november","december"];
112
+ const allRows = $mainTable.find("tr").toArray();
113
+ let headerRowIndex = -1;
114
+ let monthNames = [];
115
+ for (let ri = 0; ri < Math.min(4, allRows.length); ri++) {
116
+ const cells = $(allRows[ri]).find("td, th").toArray();
117
+ const found = [];
118
+ for (const cell of cells) {
119
+ const text = ($(cell).find("strong").text().trim() || $(cell).text().trim());
120
+ if (text && MONTH_NAMES.includes(text.toLowerCase())) found.push(text);
63
121
  }
122
+ if (found.length > 0) { headerRowIndex = ri; monthNames = found; break; }
123
+ }
124
+ if (monthNames.length === 0) {
125
+ return { error: "Could not parse month headers from calendar", status: 500 };
126
+ }
127
+ // ── Step 5: Find first data row (starts with a number = date) ──────────
128
+ let firstDataRowIndex = headerRowIndex + 1;
129
+ while (firstDataRowIndex < allRows.length) {
130
+ const firstCell = $(allRows[firstDataRowIndex]).find("td, th").first().text().trim();
131
+ if (/^\d+$/.test(firstCell)) break;
132
+ firstDataRowIndex++;
64
133
  }
65
- const $dataRows = $mainTable.find("tr").slice(1).toArray();
66
- $dataRows.forEach((rowElement) => {
67
- const $tds = $inner(rowElement).find("td");
68
- monthsData.forEach((month, monthIndex) => {
69
- const offset = monthIndex * 5;
70
- if (offset + 3 >= $tds.length)
71
- return;
72
- const date = $tds.eq(offset).text().trim();
73
- if (!date)
74
- return;
75
- const day = $tds
76
- .eq(offset + 1)
77
- .text()
78
- .trim();
79
- const event = $tds
80
- .eq(offset + 2)
81
- .find("strong")
82
- .text()
83
- .trim();
84
- const dayOrder = $tds
85
- .eq(offset + 3)
86
- .text()
87
- .trim();
134
+ if (firstDataRowIndex >= allRows.length) {
135
+ return { error: "No data rows found in calendar table", status: 500 };
136
+ }
137
+ // ── Step 6: Calculate columns per month from the first data row ─────────
138
+ const dataRowCellCount = $(allRows[firstDataRowIndex]).find("td, th").length;
139
+ let colsPerMonth = monthNames.length > 0 ? Math.round(dataRowCellCount / monthNames.length) : 5;
140
+ if (colsPerMonth < 3) colsPerMonth = 5;
141
+ // ── Step 7: Parse all data rows ────────────────────────────────────────
142
+ const monthsData = monthNames.map(m => ({ month: m, days: [] }));
143
+ for (let ri = firstDataRowIndex; ri < allRows.length; ri++) {
144
+ const $cells = $(allRows[ri]).find("td, th");
145
+ monthsData.forEach((month, mi) => {
146
+ const offset = mi * colsPerMonth;
147
+ if (offset + 3 >= $cells.length) return;
148
+ const date = $cells.eq(offset).text().trim();
149
+ if (!date || !/^\d+$/.test(date)) return;
150
+ const day = $cells.eq(offset + 1).text().trim();
151
+ const event = ($cells.eq(offset + 2).find("strong").text().trim()
152
+ || $cells.eq(offset + 2).text().trim()).trim();
153
+ const dayOrder = $cells.eq(offset + 3).text().trim();
88
154
  month.days.push({ date, day, event, dayOrder });
89
155
  });
90
- });
91
- return { calendar: monthsData, status: 200 };
156
+ }
157
+ const populated = monthsData.filter(m => m.days.length > 0);
158
+ if (populated.length === 0) {
159
+ return { error: "Calendar table found but no data could be parsed", status: 500 };
160
+ }
161
+ return { calendar: populated, status: 200 };
92
162
  }
93
163
  catch (error) {
94
164
  console.error("Error parsing calendar:", error);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "reddy-api-srm",
3
3
  "description": "SRMIST KTR Academia portal",
4
- "version": "1.0.6",
4
+ "version": "1.0.8",
5
5
  "main": "dist/src/index.js",
6
6
  "types": "dist/src/index.d.ts",
7
7
  "exports": {
@@ -51,4 +51,4 @@
51
51
  "axios": "^1.11.0",
52
52
  "cheerio": "^1.1.2"
53
53
  }
54
- }
54
+ }