open-vtop 1.0.5 → 1.0.6
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/README.md +3 -3
- package/dist/index.js +20 -1
- package/dist/session-manager.js +128 -125
- package/dist/views/Dashboard.js +1 -1
- package/dist/views/partials.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,9 +14,9 @@ An open-source VTOP client with automatic session management.
|
|
|
14
14
|
9) qol like automatic browser open, better logging, save usn password for faster logins, logout
|
|
15
15
|
## Features
|
|
16
16
|
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
17
|
+
- **Automatic Session Initialization**: VTOP session cookies and CSRF tokens are automatically established when the server starts
|
|
18
|
+
- **Background Processing**: Session setup happens in the background, no need to hit any endpoints first
|
|
19
|
+
- **Session Monitoring**: Check session status in real-time via the web interface
|
|
20
20
|
|
|
21
21
|
## Getting Started
|
|
22
22
|
|
package/dist/index.js
CHANGED
|
@@ -38,6 +38,26 @@ app.post("/api/login/form", async (c) => {
|
|
|
38
38
|
return c.html(_jsxs("div", { id: "error-message", class: "mt-4 p-3 bg-red-500/10 border border-red-500/50 rounded-md text-red-500 text-sm", children: ["Server error: ", String(error)] }));
|
|
39
39
|
}
|
|
40
40
|
});
|
|
41
|
+
// Course Details HTML Endpoint
|
|
42
|
+
app.get("/api/courses/html", async (c) => {
|
|
43
|
+
if (!sessionManager.isLoggedIn()) {
|
|
44
|
+
return c.html(_jsx("div", { class: "text-red-500", children: "Session expired." }));
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const courses = await sessionManager.fetchCourseDetails();
|
|
48
|
+
if (courses.length === 0) {
|
|
49
|
+
return c.html(_jsx("div", { class: "p-8 text-center bg-surface border border-border rounded-lg text-muted", children: "No course details found." }));
|
|
50
|
+
}
|
|
51
|
+
return c.html(_jsx("div", { class: "grid grid-cols-1 md:grid-cols-2 gap-4", children: courses.map((course, i) => (_jsxs("div", { class: "p-4 bg-surface border border-border rounded-lg flex flex-col justify-between", children: [_jsxs("div", { children: [_jsxs("div", { class: "flex justify-between items-start mb-2", children: [_jsx("span", { class: "text-xs font-bold text-muted uppercase tracking-wider", children: course.code }), _jsx("span", { class: "text-[0.65rem] px-1.5 py-0.5 rounded border border-border text-muted", children: course.type })] }), _jsx("h4", { class: "font-semibold text-sm mb-3 leading-snug", children: course.name })] }), _jsxs("div", { class: "flex items-end justify-between mt-2 pt-3 border-t border-border/50", children: [_jsxs("div", { class: "flex flex-col", children: [_jsx("span", { class: "text-[0.65rem] text-muted uppercase", children: "Attendance" }), _jsxs("span", { class: `text-lg font-bold ${course.attendanceColor === "danger"
|
|
52
|
+
? "text-red-500"
|
|
53
|
+
: course.attendanceColor === "warning"
|
|
54
|
+
? "text-yellow-500"
|
|
55
|
+
: "text-green-500"}`, children: [course.attendance, "%"] })] }), course.remarks && (_jsx("span", { class: `text-xs px-2 py-1 rounded bg-${course.attendanceColor === "danger" ? "red" : "green"}-500/10 text-${course.attendanceColor === "danger" ? "red" : "green"}-500`, children: course.remarks }))] })] }, i))) }));
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
return c.html(_jsxs("div", { class: "p-4 border border-red-500 rounded-md text-red-500", children: ["Failed to load courses: ", String(error)] }));
|
|
59
|
+
}
|
|
60
|
+
});
|
|
41
61
|
app.get("/api/assignments/html", async (c) => {
|
|
42
62
|
if (!sessionManager.isLoggedIn()) {
|
|
43
63
|
// If session expired, redirect/render login
|
|
@@ -48,7 +68,6 @@ app.get("/api/assignments/html", async (c) => {
|
|
|
48
68
|
if (assignments.length === 0) {
|
|
49
69
|
return c.html(_jsxs("div", { class: "p-12 text-center bg-surface border border-border rounded-lg", children: [_jsx("h3", { class: "text-lg font-medium mb-2", children: "No Upcoming Assignments" }), _jsx("p", { class: "text-muted", children: "You are all caught up!" })] }));
|
|
50
70
|
}
|
|
51
|
-
//ass
|
|
52
71
|
return c.html(_jsx("div", { class: "flex flex-col gap-3", children: assignments.map((ass, i) => (_jsxs("div", { class: "flex items-center justify-between p-4 bg-surface border border-border rounded-lg transition-colors hover:border-muted group", children: [_jsxs("div", { class: "flex-1 min-w-0 pr-4", children: [_jsxs("div", { class: "flex items-baseline gap-3 overflow-hidden whitespace-nowrap text-ellipsis", children: [_jsx("span", { class: "text-xs font-bold text-muted uppercase tracking-wider min-w-fit", children: ass.courseCode }), _jsx("span", { class: "font-semibold text-sm truncate", children: ass.assignmentTitle }), _jsxs("span", { class: "text-xs text-muted", children: ["\u2014 ", ass.courseName] })] }), _jsxs("div", { class: "flex gap-4 mt-1 text-xs text-muted", children: [_jsxs("span", { children: ["Due:", " ", _jsx("span", { class: "text-foreground", children: ass.dueDate || "N/A" })] }), _jsxs("span", { children: ["Max:", " ", _jsx("span", { class: "text-foreground", children: ass.maxMarks || "N/A" })] })] })] }), _jsx("span", { class: `text-xs px-2.5 py-1 rounded-full font-medium whitespace-nowrap ${ass.status?.toLowerCase().includes("pending")
|
|
53
72
|
? "bg-red-500/10 text-red-500 border border-red-500/20"
|
|
54
73
|
: "bg-blue-500/10 text-blue-500 border border-blue-500/20"}`, children: ass.status || "Pending" })] }, i))) }));
|
package/dist/session-manager.js
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Session Manager for VTOP
|
|
3
|
-
* Automatically initializes and maintains VTOP session cookies and CSRF tokens
|
|
4
|
-
*/
|
|
1
|
+
//seperations of concerns my arse
|
|
5
2
|
import { solve, extractDataUriParts, saveCaptchaImage, } from "./captcha-solver.js";
|
|
6
3
|
import * as path from "path";
|
|
7
4
|
const BASE = "https://vtop.vit.ac.in";
|
|
@@ -16,6 +13,7 @@ const VTOP_OPEN = `${BASE}/vtop/open`;
|
|
|
16
13
|
const CONTENT = `${BASE}/vtop/content`;
|
|
17
14
|
const ACADEMICS_CHECK = `${BASE}/vtop/academics/common/AcademicsDefaultCheck`;
|
|
18
15
|
const UPCOMING_ASSIGNMENTS = `${BASE}/vtop/get/upcoming/digital/assignments`;
|
|
16
|
+
const COURSE_DETAILS = `${BASE}/vtop/get/dashboard/current/semester/course/details`;
|
|
19
17
|
class VTOPSessionManager {
|
|
20
18
|
state = {
|
|
21
19
|
cookies: new Map(),
|
|
@@ -28,7 +26,6 @@ class VTOPSessionManager {
|
|
|
28
26
|
};
|
|
29
27
|
//cheerio at home
|
|
30
28
|
extractCsrf(html) {
|
|
31
|
-
// Look for _csrf token in various forms
|
|
32
29
|
const patterns = [
|
|
33
30
|
/name="_csrf"\s+value="([^"]+)"/,
|
|
34
31
|
/name='_csrf'\s+value='([^']+)'/,
|
|
@@ -58,16 +55,12 @@ class VTOPSessionManager {
|
|
|
58
55
|
.map(([name, value]) => `${name}=${value}`)
|
|
59
56
|
.join("; ");
|
|
60
57
|
}
|
|
61
|
-
/**
|
|
62
|
-
* Make a fetch request with cookie handling
|
|
63
|
-
*/
|
|
64
58
|
async fetchWithCookies(url, options = {}) {
|
|
65
59
|
const cookieHeader = this.getCookieHeader();
|
|
66
60
|
const headers = new Headers(options.headers);
|
|
67
61
|
if (cookieHeader) {
|
|
68
62
|
headers.set("Cookie", cookieHeader);
|
|
69
63
|
}
|
|
70
|
-
// Add common browser headers
|
|
71
64
|
headers.set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
|
|
72
65
|
headers.set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8");
|
|
73
66
|
headers.set("Accept-Language", "en-US,en;q=0.9");
|
|
@@ -79,16 +72,13 @@ class VTOPSessionManager {
|
|
|
79
72
|
headers,
|
|
80
73
|
redirect: "manual", // Handle redirects manually to capture cookies
|
|
81
74
|
});
|
|
82
|
-
// Store any cookies from the response
|
|
83
75
|
this.storeCookies(response);
|
|
84
|
-
// Follow redirects manually if needed
|
|
85
76
|
if (response.status >= 300 && response.status < 400) {
|
|
86
77
|
const location = response.headers.get("Location");
|
|
87
78
|
if (location) {
|
|
88
79
|
const redirectUrl = location.startsWith("http")
|
|
89
80
|
? location
|
|
90
81
|
: new URL(location, url).toString();
|
|
91
|
-
// Create new options without body for GET redirect
|
|
92
82
|
const redirectOptions = {
|
|
93
83
|
headers: options.headers,
|
|
94
84
|
redirect: options.redirect,
|
|
@@ -98,12 +88,9 @@ class VTOPSessionManager {
|
|
|
98
88
|
}
|
|
99
89
|
return response;
|
|
100
90
|
}
|
|
101
|
-
/**
|
|
102
|
-
* Initialize VTOP session
|
|
103
|
-
*/
|
|
104
91
|
async initialize() {
|
|
105
92
|
try {
|
|
106
|
-
console.log("
|
|
93
|
+
console.log("Initializing VTOP session...");
|
|
107
94
|
// Step 1: GET / to get SERVERID cookie
|
|
108
95
|
console.log("Step 1: GET / to get SERVERID...");
|
|
109
96
|
await this.fetchWithCookies(BASE);
|
|
@@ -119,16 +106,12 @@ class VTOPSessionManager {
|
|
|
119
106
|
console.log(" ✓ OK");
|
|
120
107
|
const csrf = this.extractCsrf(openPageHtml);
|
|
121
108
|
if (!csrf) {
|
|
122
|
-
console.warn("
|
|
109
|
+
console.warn("⚠Warning: _csrf not found on /vtop/openPage. Continuing anyway...");
|
|
123
110
|
}
|
|
124
111
|
else {
|
|
125
112
|
this.state.csrf = csrf;
|
|
126
113
|
console.log(` ✓ Found _csrf: ${csrf.substring(0, 20)}...`);
|
|
127
114
|
}
|
|
128
|
-
// Optional: Visit alternate open page (used as Referer in browser)
|
|
129
|
-
await this.fetchWithCookies(OPEN_PAGE_ALT).catch(() => {
|
|
130
|
-
// Ignore errors on this optional step
|
|
131
|
-
});
|
|
132
115
|
// Step 4: POST /vtop/prelogin/setup
|
|
133
116
|
console.log("Step 4: POST /vtop/prelogin/setup (flag=VTOP)...");
|
|
134
117
|
const formData = new URLSearchParams();
|
|
@@ -145,51 +128,35 @@ class VTOPSessionManager {
|
|
|
145
128
|
},
|
|
146
129
|
body: formData.toString(),
|
|
147
130
|
});
|
|
148
|
-
console.log(`
|
|
149
|
-
// Mark as initialized
|
|
131
|
+
console.log(` Prelogin status: ${preloginRes.status}`);
|
|
150
132
|
this.state.initialized = true;
|
|
151
133
|
this.state.lastInitialized = new Date();
|
|
152
|
-
console.log("
|
|
134
|
+
console.log("VTOP session initialized successfully!");
|
|
153
135
|
console.log(` Cookies stored: ${this.state.cookies.size}`);
|
|
154
136
|
console.log(` Cookie names: ${Array.from(this.state.cookies.keys()).join(", ")}`);
|
|
155
137
|
}
|
|
156
138
|
catch (error) {
|
|
157
|
-
console.error("
|
|
139
|
+
console.error("Failed to initialize VTOP session:", error);
|
|
158
140
|
throw error;
|
|
159
141
|
}
|
|
160
142
|
}
|
|
161
|
-
/**
|
|
162
|
-
* Get current session state
|
|
163
|
-
*/
|
|
164
143
|
getState() {
|
|
165
144
|
return {
|
|
166
145
|
...this.state,
|
|
167
146
|
cookies: new Map(this.state.cookies),
|
|
168
147
|
};
|
|
169
148
|
}
|
|
170
|
-
/**
|
|
171
|
-
* Get CSRF token
|
|
172
|
-
*/
|
|
173
149
|
getCsrf() {
|
|
174
150
|
return this.state.csrf;
|
|
175
151
|
}
|
|
176
|
-
/**
|
|
177
|
-
* Get cookies as a header string
|
|
178
|
-
*/
|
|
179
152
|
getCookies() {
|
|
180
153
|
return this.getCookieHeader();
|
|
181
154
|
}
|
|
182
|
-
/**
|
|
183
|
-
* Check if session is initialized
|
|
184
|
-
*/
|
|
185
155
|
isInitialized() {
|
|
186
156
|
return this.state.initialized;
|
|
187
157
|
}
|
|
188
|
-
/**
|
|
189
|
-
* Re-initialize session (useful for periodic refresh)
|
|
190
|
-
*/
|
|
191
158
|
async refresh() {
|
|
192
|
-
console.log("
|
|
159
|
+
console.log("Refreshing VTOP session...");
|
|
193
160
|
this.state.cookies.clear();
|
|
194
161
|
this.state.csrf = null;
|
|
195
162
|
this.state.initialized = false;
|
|
@@ -197,9 +164,6 @@ class VTOPSessionManager {
|
|
|
197
164
|
this.state.username = null;
|
|
198
165
|
await this.initialize();
|
|
199
166
|
}
|
|
200
|
-
/**
|
|
201
|
-
* Detect captcha type from login page HTML using regex (no cheerio)
|
|
202
|
-
*/
|
|
203
167
|
detectCaptcha(html) {
|
|
204
168
|
// reCAPTCHA signals
|
|
205
169
|
const recaptchaDom = html.includes('id="recaptcha"') ||
|
|
@@ -207,11 +171,7 @@ class VTOPSessionManager {
|
|
|
207
171
|
html.includes('class="g-recaptcha"');
|
|
208
172
|
const recaptchaJs = html.includes("var captchaType=2");
|
|
209
173
|
const isRecaptcha = recaptchaDom || recaptchaJs;
|
|
210
|
-
// Text CAPTCHA signals - check for captchaStr input
|
|
211
174
|
const hasCaptchaInput = html.includes('name="captchaStr"') || html.includes('id="captchaStr"');
|
|
212
|
-
// Look for data URI image in the HTML
|
|
213
|
-
// Support both single and double quotes
|
|
214
|
-
// VTOP sometimes sends "null" in base64, so we need to find the first valid one
|
|
215
175
|
const imgDataUriMatches = [
|
|
216
176
|
...html.matchAll(/src=["'](data:image\/[^"']+)["']/gi),
|
|
217
177
|
];
|
|
@@ -222,35 +182,27 @@ class VTOPSessionManager {
|
|
|
222
182
|
break;
|
|
223
183
|
}
|
|
224
184
|
}
|
|
225
|
-
// specific check for the warning if we found matches but none were valid
|
|
226
185
|
if (!imgDataUri && imgDataUriMatches.length > 0) {
|
|
227
|
-
console.warn("
|
|
186
|
+
console.warn("Detected invalid captcha image (base64 is null)");
|
|
228
187
|
}
|
|
229
188
|
const isTextCaptcha = hasCaptchaInput && imgDataUri !== null;
|
|
230
|
-
// Extract CSRF token
|
|
231
189
|
const csrf = this.extractCsrf(html);
|
|
232
190
|
return { isTextCaptcha, isRecaptcha, csrf, imgDataUri };
|
|
233
191
|
}
|
|
234
|
-
/**
|
|
235
|
-
* Login to VTOP with username, password, and registration number
|
|
236
|
-
* Polls the login page until a text CAPTCHA appears, solves it, and submits
|
|
237
|
-
*/
|
|
238
192
|
async login(username, password, regNo, maxAttempts = 10) {
|
|
239
193
|
if (!this.state.initialized) {
|
|
240
194
|
console.log("Session not initialized, initializing first...");
|
|
241
195
|
await this.initialize();
|
|
242
196
|
}
|
|
243
|
-
console.log("
|
|
197
|
+
console.log("Starting VTOP login process...");
|
|
244
198
|
console.log("Polling /vtop/login until text CAPTCHA appears...");
|
|
245
199
|
let attempt = 0;
|
|
246
200
|
while (attempt < maxAttempts) {
|
|
247
201
|
attempt++;
|
|
248
202
|
try {
|
|
249
|
-
// Fetch login page
|
|
250
203
|
const res = await this.fetchWithCookies(LOGIN_PAGE);
|
|
251
204
|
const body = await res.text();
|
|
252
205
|
const { isTextCaptcha, isRecaptcha, csrf, imgDataUri } = this.detectCaptcha(body);
|
|
253
|
-
// Get current cookies for logging
|
|
254
206
|
const curJsession = this.state.cookies.get("JSESSIONID") || "(?)";
|
|
255
207
|
const curServerID = this.state.cookies.get("SERVERID") || "(?)";
|
|
256
208
|
console.log(`Attempt ${attempt}: status ${res.status} | text-captcha=${isTextCaptcha ? "YES" : "no"} | recaptcha=${isRecaptcha ? "YES" : "no"} | JSESSIONID=${curJsession}`);
|
|
@@ -259,29 +211,25 @@ class VTOPSessionManager {
|
|
|
259
211
|
if (imgDataUri) {
|
|
260
212
|
const parts = extractDataUriParts(imgDataUri);
|
|
261
213
|
try {
|
|
262
|
-
// Trim whitespace just in case
|
|
263
214
|
const cleanDataUri = imgDataUri.trim();
|
|
264
215
|
if (cleanDataUri !== imgDataUri) {
|
|
265
|
-
console.log("
|
|
216
|
+
console.log("Trimmed whitespace from captcha data URI");
|
|
266
217
|
}
|
|
267
218
|
solvedCaptcha = await solve(cleanDataUri);
|
|
268
|
-
console.log("
|
|
269
|
-
// Save captcha image for debugging
|
|
219
|
+
console.log("Solved CAPTCHA:", solvedCaptcha);
|
|
270
220
|
if (parts?.base64) {
|
|
271
221
|
const out = path.resolve(process.cwd(), "captcha.jpg");
|
|
272
222
|
await saveCaptchaImage(parts.base64, out);
|
|
273
|
-
// console.log(`📷 Saved CAPTCHA image to ${out}`);
|
|
274
223
|
}
|
|
275
224
|
}
|
|
276
225
|
catch (e) {
|
|
277
|
-
console.warn("
|
|
226
|
+
console.warn("Failed to solve captcha:", e);
|
|
278
227
|
solvedCaptcha = "";
|
|
279
|
-
// Save the failed data URI for inspection
|
|
280
228
|
try {
|
|
281
229
|
const fs = await import("fs/promises");
|
|
282
230
|
const failPath = path.resolve(process.cwd(), "failed_captcha_data.txt");
|
|
283
231
|
await fs.writeFile(failPath, imgDataUri);
|
|
284
|
-
console.log(
|
|
232
|
+
console.log(`Saved failed captcha data URI to ${failPath}`);
|
|
285
233
|
}
|
|
286
234
|
catch (writeErr) {
|
|
287
235
|
console.error("Failed to save failed captcha data:", writeErr);
|
|
@@ -291,8 +239,7 @@ class VTOPSessionManager {
|
|
|
291
239
|
else {
|
|
292
240
|
console.log("Text CAPTCHA detected but no data URI image found.");
|
|
293
241
|
}
|
|
294
|
-
|
|
295
|
-
console.log("\n📤 Submitting POST /vtop/login ...");
|
|
242
|
+
console.log("\nSubmitting POST /vtop/login ...");
|
|
296
243
|
const loginForm = new URLSearchParams();
|
|
297
244
|
if (csrf)
|
|
298
245
|
loginForm.set("_csrf", csrf);
|
|
@@ -324,25 +271,20 @@ class VTOPSessionManager {
|
|
|
324
271
|
redirect: "manual",
|
|
325
272
|
});
|
|
326
273
|
console.log(` -> Login POST status: ${postRes.status}`);
|
|
327
|
-
// Store any new cookies
|
|
328
274
|
this.storeCookies(postRes);
|
|
329
275
|
const setCookie = postRes.headers.getSetCookie?.() || [];
|
|
330
276
|
if (setCookie.length > 0) {
|
|
331
277
|
console.log(" -> New cookies received");
|
|
332
278
|
}
|
|
333
|
-
// Check for redirect
|
|
334
279
|
const location = postRes.headers.get("Location");
|
|
335
280
|
if (location) {
|
|
336
281
|
console.log(` -> Redirect to: ${location}`);
|
|
337
|
-
// Follow redirect to complete the login flow
|
|
338
282
|
const redirectUrl = location.startsWith("http")
|
|
339
283
|
? location
|
|
340
284
|
: new URL(location, LOGIN_PAGE).toString();
|
|
341
285
|
await this.fetchWithCookies(redirectUrl);
|
|
342
286
|
}
|
|
343
|
-
|
|
344
|
-
// The actual success will be determined when we try to fetch data
|
|
345
|
-
console.log("🎉 Login POST submitted successfully!");
|
|
287
|
+
console.log("Login POST submitted successfully!");
|
|
346
288
|
this.state.loggedIn = true;
|
|
347
289
|
this.state.username = username;
|
|
348
290
|
this.state.regNo = regNo;
|
|
@@ -352,14 +294,11 @@ class VTOPSessionManager {
|
|
|
352
294
|
console.error("Login POST failed:", e);
|
|
353
295
|
}
|
|
354
296
|
}
|
|
355
|
-
// If reCAPTCHA, we cannot solve it automatically
|
|
356
297
|
if (isRecaptcha) {
|
|
357
|
-
console.log("
|
|
358
|
-
// Wait a bit and retry, hoping for text captcha
|
|
298
|
+
console.log("reCAPTCHA detected - cannot solve automatically");
|
|
359
299
|
await new Promise((r) => setTimeout(r, 2000));
|
|
360
300
|
continue;
|
|
361
301
|
}
|
|
362
|
-
// Wait before next poll
|
|
363
302
|
await new Promise((r) => setTimeout(r, 500));
|
|
364
303
|
}
|
|
365
304
|
catch (e) {
|
|
@@ -367,28 +306,18 @@ class VTOPSessionManager {
|
|
|
367
306
|
await new Promise((r) => setTimeout(r, 1000));
|
|
368
307
|
}
|
|
369
308
|
}
|
|
370
|
-
console.log(
|
|
309
|
+
console.log(`Login failed after ${maxAttempts} attempts`);
|
|
371
310
|
return false;
|
|
372
311
|
}
|
|
373
|
-
/**
|
|
374
|
-
* Check if logged in
|
|
375
|
-
*/
|
|
376
312
|
isLoggedIn() {
|
|
377
313
|
return this.state.loggedIn;
|
|
378
314
|
}
|
|
379
|
-
/**
|
|
380
|
-
* Get current username
|
|
381
|
-
*/
|
|
382
315
|
getUsername() {
|
|
383
316
|
return this.state.username;
|
|
384
317
|
}
|
|
385
|
-
/**
|
|
386
|
-
* Navigate through post-login pages to establish authenticated session
|
|
387
|
-
* This mimics what the browser does after successful login
|
|
388
|
-
*/
|
|
389
318
|
async navigatePostLogin() {
|
|
390
319
|
if (!this.state.loggedIn) {
|
|
391
|
-
console.error("
|
|
320
|
+
console.error(" Cannot navigate post-login pages: not logged in");
|
|
392
321
|
return false;
|
|
393
322
|
}
|
|
394
323
|
const cookies = this.getCookieHeader();
|
|
@@ -404,8 +333,7 @@ class VTOPSessionManager {
|
|
|
404
333
|
Cookie: cookies,
|
|
405
334
|
};
|
|
406
335
|
try {
|
|
407
|
-
|
|
408
|
-
console.log("📄 Navigating post-login pages...");
|
|
336
|
+
console.log("Navigating post-login pages...");
|
|
409
337
|
// 1. /vtop/init/page
|
|
410
338
|
const initRes = await this.fetchWithCookies(INIT_PAGE, { headers });
|
|
411
339
|
console.log(` -> /vtop/init/page: ${initRes.status}`);
|
|
@@ -422,24 +350,19 @@ class VTOPSessionManager {
|
|
|
422
350
|
const newCsrf = this.extractCsrf(contentHtml);
|
|
423
351
|
if (newCsrf) {
|
|
424
352
|
this.state.csrf = newCsrf;
|
|
425
|
-
console.log(`
|
|
353
|
+
console.log(`Updated CSRF token from content page`);
|
|
426
354
|
}
|
|
427
|
-
console.log("
|
|
355
|
+
console.log("Post-login navigation complete");
|
|
428
356
|
return true;
|
|
429
357
|
}
|
|
430
358
|
catch (e) {
|
|
431
|
-
console.error("
|
|
359
|
+
console.error("Post-login navigation failed:", e);
|
|
432
360
|
return false;
|
|
433
361
|
}
|
|
434
362
|
}
|
|
435
|
-
|
|
436
|
-
* Parse upcoming assignments from VTOP HTML response
|
|
437
|
-
* Uses regex instead of cheerio
|
|
438
|
-
*/
|
|
363
|
+
//slop
|
|
439
364
|
parseAssignmentsHtml(html) {
|
|
440
365
|
const assignments = [];
|
|
441
|
-
// VTOP returns assignments in a table or as JSON-like structure
|
|
442
|
-
// Try to parse as JSON first (some endpoints return JSON)
|
|
443
366
|
try {
|
|
444
367
|
const jsonMatch = html.match(/\[[\s\S]*?\]/);
|
|
445
368
|
if (jsonMatch) {
|
|
@@ -497,24 +420,68 @@ class VTOPSessionManager {
|
|
|
497
420
|
}
|
|
498
421
|
return assignments;
|
|
499
422
|
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
423
|
+
parseCourseDetailsHtml(html) {
|
|
424
|
+
const courses = [];
|
|
425
|
+
// Regex to match table rows
|
|
426
|
+
const rowRegex = /<tr[^>]*>([\s\S]*?)<\/tr>/g;
|
|
427
|
+
const matches = [...html.matchAll(rowRegex)];
|
|
428
|
+
// Skip header row
|
|
429
|
+
for (let i = 1; i < matches.length; i++) {
|
|
430
|
+
const rowContent = matches[i][1];
|
|
431
|
+
// Extract Code and Name: <span class="mx-2 text-dark fw-bold">BCSE204L</span>-<span class="mx-2 text-dark">Design and Analysis of Algorithms</span>
|
|
432
|
+
const codeMatch = rowContent.match(/<span[^>]*text-dark fw-bold[^>]*>([^<]+)<\/span>/);
|
|
433
|
+
const nameMatch = rowContent.match(/-[\s\n]*<span[^>]*text-dark[^>]*>([^<]+)<\/span>/);
|
|
434
|
+
// Extract Type: <td class="fst-italic text-primary fw-bold mx-1">TH</td>
|
|
435
|
+
const typeMatch = rowContent.match(/<td[^>]*fst-italic[^>]*>([^<]+)<\/td>/);
|
|
436
|
+
// Extract Attendance: <span class="text-danger fw-bold">65.0</span>
|
|
437
|
+
// Capture color class as well to determine status
|
|
438
|
+
const attendanceMatch = rowContent.match(/<span class="text-([a-z]+)[^>]*fw-bold">([\d.]+)<\/span>/);
|
|
439
|
+
// Extract Remarks: <span class="text-danger fw-bold">Critical - must improve</span>
|
|
440
|
+
// This usually comes after attendance in the last column
|
|
441
|
+
const remarksMatch = rowContent.match(/<span class="text-[^>]*>([^<]+)<\/span>[\s\n]*<\/td>[\s\n]*<\/tr>$/) ||
|
|
442
|
+
rowContent.match(/<td[^>]*text-nowrap text-start[^>]*>[\s\S]*?<span[^>]*>([^<]+)<\/span>/);
|
|
443
|
+
if (codeMatch && nameMatch) {
|
|
444
|
+
courses.push({
|
|
445
|
+
code: codeMatch[1].trim(),
|
|
446
|
+
name: nameMatch[1].trim(),
|
|
447
|
+
type: typeMatch ? typeMatch[1].trim() : "N/A",
|
|
448
|
+
attendance: attendanceMatch ? attendanceMatch[2].trim() : "N/A",
|
|
449
|
+
attendanceColor: attendanceMatch
|
|
450
|
+
? attendanceMatch[1].trim()
|
|
451
|
+
: "secondary",
|
|
452
|
+
remarks: remarksMatch ? remarksMatch[1].trim() : "",
|
|
453
|
+
});
|
|
454
|
+
}
|
|
507
455
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
456
|
+
return courses;
|
|
457
|
+
}
|
|
458
|
+
async performAcademicsCheck(headers) {
|
|
511
459
|
const now = new Date();
|
|
512
|
-
// First, do the academics default check
|
|
513
460
|
const accParams = new URLSearchParams();
|
|
514
461
|
accParams.set("authorizedID", this.state.regNo);
|
|
515
462
|
if (this.state.csrf)
|
|
516
463
|
accParams.set("_csrf", this.state.csrf);
|
|
517
464
|
accParams.set("x", now.toUTCString());
|
|
465
|
+
try {
|
|
466
|
+
console.log("Performing AcademicsDefaultCheck...");
|
|
467
|
+
const accRes = await fetch(ACADEMICS_CHECK, {
|
|
468
|
+
method: "POST",
|
|
469
|
+
headers,
|
|
470
|
+
body: accParams.toString(),
|
|
471
|
+
});
|
|
472
|
+
console.log(` -> AcademicsDefaultCheck: ${accRes.status}`);
|
|
473
|
+
}
|
|
474
|
+
catch (e) {
|
|
475
|
+
console.warn("AcademicsDefaultCheck failed:", e);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
async fetchCourseDetails() {
|
|
479
|
+
if (!this.state.loggedIn || !this.state.regNo) {
|
|
480
|
+
console.error("Cannot fetch course details: not logged in");
|
|
481
|
+
return [];
|
|
482
|
+
}
|
|
483
|
+
const cookies = this.getCookieHeader();
|
|
484
|
+
const now = new Date();
|
|
518
485
|
const apiHeaders = {
|
|
519
486
|
Accept: "*/*",
|
|
520
487
|
"Accept-Encoding": "gzip, deflate, br, zstd",
|
|
@@ -529,27 +496,65 @@ class VTOPSessionManager {
|
|
|
529
496
|
"Sec-Fetch-Site": "same-origin",
|
|
530
497
|
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
|
|
531
498
|
};
|
|
499
|
+
await this.performAcademicsCheck(apiHeaders);
|
|
500
|
+
const assParams = new URLSearchParams();
|
|
501
|
+
assParams.set("authorizedID", this.state.regNo);
|
|
502
|
+
if (this.state.csrf)
|
|
503
|
+
assParams.set("_csrf", this.state.csrf);
|
|
504
|
+
assParams.set("x", now.toUTCString());
|
|
532
505
|
try {
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
const accRes = await fetch(ACADEMICS_CHECK, {
|
|
506
|
+
console.log("Fetching course details...");
|
|
507
|
+
const res = await fetch(COURSE_DETAILS, {
|
|
536
508
|
method: "POST",
|
|
537
509
|
headers: apiHeaders,
|
|
538
|
-
body:
|
|
510
|
+
body: assParams,
|
|
539
511
|
});
|
|
540
|
-
|
|
512
|
+
const html = await res.text();
|
|
513
|
+
console.log("Course details HTML:", html);
|
|
514
|
+
const courses = this.parseCourseDetailsHtml(html);
|
|
515
|
+
console.log(`Parsed ${courses.length} courses`);
|
|
516
|
+
return courses;
|
|
541
517
|
}
|
|
542
|
-
catch (
|
|
543
|
-
console.
|
|
518
|
+
catch (error) {
|
|
519
|
+
console.error("Failed to fetch course details:", error);
|
|
520
|
+
return [];
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
async fetchUpcomingAssignments() {
|
|
524
|
+
if (!this.state.loggedIn || !this.state.regNo) {
|
|
525
|
+
console.error("Cannot fetch assignments: not logged in or no regNo");
|
|
526
|
+
return [];
|
|
544
527
|
}
|
|
545
|
-
|
|
528
|
+
await this.navigatePostLogin();
|
|
529
|
+
const cookies = this.getCookieHeader();
|
|
530
|
+
const now = new Date();
|
|
531
|
+
const accParams = new URLSearchParams();
|
|
532
|
+
accParams.set("authorizedID", this.state.regNo);
|
|
533
|
+
if (this.state.csrf)
|
|
534
|
+
accParams.set("_csrf", this.state.csrf);
|
|
535
|
+
accParams.set("x", now.toUTCString());
|
|
536
|
+
const apiHeaders = {
|
|
537
|
+
Accept: "*/*",
|
|
538
|
+
"Accept-Encoding": "gzip, deflate, br, zstd",
|
|
539
|
+
"Accept-Language": "en-US,en;q=0.7",
|
|
540
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
541
|
+
Cookie: cookies,
|
|
542
|
+
Origin: BASE,
|
|
543
|
+
Priority: "u=1, i",
|
|
544
|
+
Referer: CONTENT,
|
|
545
|
+
"Sec-Fetch-Dest": "empty",
|
|
546
|
+
"Sec-Fetch-Mode": "cors",
|
|
547
|
+
"Sec-Fetch-Site": "same-origin",
|
|
548
|
+
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
|
|
549
|
+
};
|
|
550
|
+
await this.performAcademicsCheck(apiHeaders);
|
|
546
551
|
const assParams = new URLSearchParams();
|
|
547
552
|
assParams.set("authorizedID", this.state.regNo);
|
|
548
553
|
if (this.state.csrf)
|
|
549
554
|
assParams.set("_csrf", this.state.csrf);
|
|
550
555
|
assParams.set("x", now.toUTCString());
|
|
551
556
|
try {
|
|
552
|
-
console.log("
|
|
557
|
+
console.log(" Fetching upcoming assignments...");
|
|
553
558
|
const assRes = await fetch(UPCOMING_ASSIGNMENTS, {
|
|
554
559
|
method: "POST",
|
|
555
560
|
headers: apiHeaders,
|
|
@@ -561,19 +566,17 @@ class VTOPSessionManager {
|
|
|
561
566
|
console.log(` -> Raw response:\n${assBody}`);
|
|
562
567
|
if (assBody) {
|
|
563
568
|
const assignments = this.parseAssignmentsHtml(assBody);
|
|
564
|
-
console.log(
|
|
569
|
+
console.log(` Parsed ${assignments.length} assignments`);
|
|
565
570
|
return assignments;
|
|
566
571
|
}
|
|
567
572
|
}
|
|
568
573
|
catch (e) {
|
|
569
|
-
console.error("
|
|
574
|
+
console.error(" Failed to fetch assignments:", e);
|
|
570
575
|
}
|
|
571
576
|
return [];
|
|
572
577
|
}
|
|
573
578
|
}
|
|
574
|
-
// Export singleton instance
|
|
575
579
|
export const sessionManager = new VTOPSessionManager();
|
|
576
|
-
// Auto-initialize on module load
|
|
577
580
|
sessionManager.initialize().catch((error) => {
|
|
578
581
|
console.error("Failed to auto-initialize session:", error);
|
|
579
582
|
});
|
package/dist/views/Dashboard.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
|
|
2
2
|
import { BaseLayout } from "./layouts/Base.js";
|
|
3
3
|
export const Dashboard = ({ username }) => {
|
|
4
|
-
return (_jsxs(BaseLayout, { title: "Dashboard - Open-VTOP", children: [_jsxs("div", { class: "flex justify-between items-center mb-8", children: [_jsxs("div", { children: [_jsx("h1", { class: "text-2xl font-bold tracking-tight", children: "Dashboard" }), _jsxs("p", { class: "text-muted text-sm", children: ["Welcome back, ", username] })] }), _jsx("div", {})] }), _jsx("div", { "hx-get": "/api/assignments/html", "hx-trigger": "load", "hx-target": "#assignments-container", "hx-swap": "innerHTML", class: "w-full", children: _jsx("div", { id: "assignments-container", class: "space-y-4", children: _jsxs("div", { id: "loading-state", class: "text-center py-16 text-muted", children: [_jsx("div", { class: "inline-block animate-spin rounded-full h-6 w-6 border-2 border-muted border-t-white mb-4" }), _jsx("p", { class: "text-sm", children: "Syncing assignments..." })] }) }) })] }));
|
|
4
|
+
return (_jsxs(BaseLayout, { title: "Dashboard - Open-VTOP", children: [_jsxs("div", { class: "flex justify-between items-center mb-8", children: [_jsxs("div", { children: [_jsx("h1", { class: "text-2xl font-bold tracking-tight", children: "Dashboard" }), _jsxs("p", { class: "text-muted text-sm", children: ["Welcome back, ", username] })] }), _jsx("div", {})] }), _jsxs("div", { class: "space-y-8", children: [_jsxs("section", { children: [_jsxs("div", { class: "flex items-center justify-between mb-4", children: [_jsx("h2", { class: "text-lg font-semibold", children: "Course Details" }), _jsx("span", { class: "text-xs text-muted bg-surface border border-border px-2 py-1 rounded", children: "Winter Semester 2025-26" })] }), _jsx("div", { "hx-get": "/api/courses/html", "hx-trigger": "htmx:afterOnLoad from:#assignments-loader", "hx-swap": "innerHTML", class: "w-full", children: _jsx("div", { class: "grid grid-cols-1 md:grid-cols-2 gap-4 animate-pulse", children: [1, 2, 3, 4].map((i) => (_jsx("div", { class: "h-32 bg-surface/50 border border-border rounded-lg" }, i))) }) })] }), _jsxs("section", { children: [_jsx("h2", { class: "text-lg font-semibold mb-4", children: "Assignments" }), _jsx("div", { id: "assignments-loader", "hx-get": "/api/assignments/html", "hx-trigger": "load", "hx-target": "#assignments-container", "hx-swap": "innerHTML", class: "w-full", children: _jsx("div", { id: "assignments-container", class: "space-y-4", children: _jsxs("div", { id: "loading-state", class: "text-center py-16 text-muted", children: [_jsx("div", { class: "inline-block animate-spin rounded-full h-6 w-6 border-2 border-muted border-t-white mb-4" }), _jsx("p", { class: "text-sm", children: "Syncing assignments..." })] }) }) })] })] })] }));
|
|
5
5
|
};
|
package/dist/views/partials.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx } from "hono/jsx/jsx-runtime";
|
|
2
2
|
export const SuccessMessage = ({ message }) => {
|
|
3
|
-
return
|
|
3
|
+
return _jsx("p", { class: "success", children: message });
|
|
4
4
|
};
|
package/package.json
CHANGED