init version

This commit is contained in:
JamesFlare1212
2025-05-09 09:04:25 -04:00
commit 5382659312
12 changed files with 1370 additions and 0 deletions

306
engage-api/get-activity.mjs Normal file
View File

@@ -0,0 +1,306 @@
// get-activity.mjs
import axios from 'axios';
import fs from 'fs/promises'; // Using fs.promises directly
import path from 'path';
import { fileURLToPath } from 'url';
// --- Replicating __dirname for ESM ---
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// --- Cookie Cache Configuration & In-Memory Cache ---
const COOKIE_FILE_PATH = path.resolve(__dirname, 'nkcs-engage.cookie.txt');
let _inMemoryCookie = null;
// --- Custom Error for Authentication ---
class AuthenticationError extends Error {
constructor(message = "Authentication failed, cookie may be invalid.", status) {
super(message);
this.name = "AuthenticationError";
this.status = status;
}
}
// --- Cookie Cache Helper Functions ---
async function loadCachedCookie() {
if (_inMemoryCookie) {
console.log("Using in-memory cached cookie.");
return _inMemoryCookie;
}
try {
const cookieFromFile = await fs.readFile(COOKIE_FILE_PATH, 'utf8');
if (cookieFromFile) {
_inMemoryCookie = cookieFromFile;
console.log("Loaded cookie from file cache.");
return _inMemoryCookie;
}
} catch (err) {
if (err.code === 'ENOENT') {
console.log("Cookie cache file not found. No cached cookie loaded.");
} else {
console.warn("Error loading cookie from file:", err.message);
}
}
return null;
}
async function saveCookieToCache(cookieString) {
if (!cookieString) {
console.warn("Attempted to save an empty or null cookie. Aborting save.");
return;
}
_inMemoryCookie = cookieString;
try {
await fs.writeFile(COOKIE_FILE_PATH, cookieString, 'utf8');
console.log("Cookie saved to file cache.");
} catch (err) {
console.error("Error saving cookie to file:", err.message);
}
}
async function clearCookieCache() {
_inMemoryCookie = null;
try {
await fs.unlink(COOKIE_FILE_PATH);
console.log("Cookie cache file deleted.");
} catch (err) {
if (err.code !== 'ENOENT') {
console.error("Error deleting cookie file:", err.message);
} else {
console.log("Cookie cache file did not exist, no need to delete.");
}
}
}
async function testCookieValidity(cookieString) {
if (!cookieString) return false;
console.log("Testing cookie validity...");
try {
const url = 'https://engage.nkcswx.cn/Services/ActivitiesService.asmx/GetActivityDetails';
const headers = {
'Content-Type': 'application/json; charset=UTF-8',
'Cookie': cookieString,
'User-Agent': 'Mozilla/5.0 (Node.js DSAS-CCA get-activity Module)',
};
const payload = { "activityID": "3350" };
await axios.post(url, payload, { headers, timeout: 10000 });
console.log("Cookie test successful (API responded 2xx). Cookie is valid.");
return true;
} catch (error) {
console.warn("Cookie validity test failed.");
if (error.response) {
console.warn(`Cookie test API response status: ${error.response.status}. Cookie is likely invalid or expired.`);
} else {
console.warn(`Cookie test failed due to network or other error: ${error.message}`);
}
return false;
}
}
// --- Core API Interaction Functions ---
async function getSessionId() {
const url = 'https://engage.nkcswx.cn/Login.aspx';
try {
const response = await axios.get(url, {
headers: { 'User-Agent': 'Mozilla/5.0 (Node.js DSAS-CCA get-activity Module)' }
});
const setCookieHeader = response.headers['set-cookie'];
if (setCookieHeader && setCookieHeader.length > 0) {
const sessionIdCookie = setCookieHeader.find(cookie => cookie.trim().startsWith('ASP.NET_SessionId='));
if (sessionIdCookie) {
console.log('Debugging - ASP.NET_SessionId created');
return sessionIdCookie.split(';')[0];
}
}
console.error("No ASP.NET_SessionId cookie found in Set-Cookie header.");
return null;
} catch (error) {
console.error(`Error in getSessionId: ${error.response ? `${error.response.status} - ${error.response.statusText}` : error.message}`);
throw error;
}
}
async function getMSAUTH(sessionId, userName, userPwd, templateFilePath) {
const url = 'https://engage.nkcswx.cn/Login.aspx';
try {
let templateData = await fs.readFile(templateFilePath, 'utf8');
const postData = templateData
.replace('{{USERNAME}}', userName)
.replace('{{PASSWORD}}', userPwd);
const headers = {
'Content-Type': 'application/x-www-form-urlencoded', 'Cookie': sessionId,
'User-Agent': 'Mozilla/5.0 (Node.js DSAS-CCA get-activity Module)',
'Referer': 'https://engage.nkcswx.cn/Login.aspx'
};
console.log('Debugging - Getting .ASPXFORMSAUTH');
const response = await axios.post(url, postData, {
headers, maxRedirects: 0,
validateStatus: (status) => status >= 200 && status < 400
});
const setCookieHeader = response.headers['set-cookie'];
let formsAuthCookieValue = null;
if (setCookieHeader && setCookieHeader.length > 0) {
const aspxAuthCookies = setCookieHeader.filter(cookie => cookie.trim().startsWith('.ASPXFORMSAUTH='));
if (aspxAuthCookies.length > 0) {
for (let i = aspxAuthCookies.length - 1; i >= 0; i--) {
const cookieCandidateParts = aspxAuthCookies[i].split(';');
const firstPart = cookieCandidateParts[0].trim();
if (firstPart.length > '.ASPXFORMSAUTH='.length && firstPart.substring('.ASPXFORMSAUTH='.length).length > 0) {
formsAuthCookieValue = firstPart; break;
}
}
}
}
if (formsAuthCookieValue) {
console.log('Debugging - .ASPXFORMSAUTH cookie obtained.');
return formsAuthCookieValue;
} else {
console.error("No valid .ASPXFORMSAUTH cookie found. Headers:", setCookieHeader || "none");
return null;
}
} catch (error) {
if (error.code === 'ENOENT') console.error(`Error: Template file '${templateFilePath}' not found.`);
else console.error(`Error in getMSAUTH: ${error.message}`);
throw error;
}
}
async function getCompleteCookies(userName, userPwd, templateFilePath) {
console.log('Debugging - Attempting to get complete cookie string (login process).');
const sessionId = await getSessionId();
if (!sessionId) throw new Error("Login failed: Could not obtain ASP.NET_SessionId.");
const msAuth = await getMSAUTH(sessionId, userName, userPwd, templateFilePath);
if (!msAuth) throw new Error("Login failed: Could not obtain .ASPXFORMSAUTH cookie.");
return `${sessionId}; ${msAuth}`;
}
async function getActivityDetailsRaw(activityId, cookies, maxRetries = 3, timeoutMilliseconds = 20000) {
const url = 'https://engage.nkcswx.cn/Services/ActivitiesService.asmx/GetActivityDetails';
const headers = {
'Content-Type': 'application/json; charset=UTF-8', 'Cookie': cookies,
'User-Agent': 'Mozilla/5.0 (Node.js DSAS-CCA get-activity Module)',
'X-Requested-With': 'XMLHttpRequest'
};
const payload = { "activityID": String(activityId) };
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await axios.post(url, payload, {
headers, timeout: timeoutMilliseconds, responseType: 'text'
});
const outerData = JSON.parse(response.data);
if (outerData && typeof outerData.d === 'string') {
const innerData = JSON.parse(outerData.d);
if (innerData.isError) {
console.warn(`API reported isError:true for activity ${activityId}.`);
return null;
}
return response.data;
} else {
console.error(`Unexpected API response structure for activity ${activityId}.`);
}
} catch (error) {
if (error.response && (error.response.status === 403 || error.response.status === 401)) {
console.warn(`Authentication error (${error.response.status}) while fetching activity ${activityId}. Cookie may be invalid.`);
throw new AuthenticationError(`Received ${error.response.status} for activity ${activityId}`, error.response.status);
}
console.error(`Attempt ${attempt + 1}/${maxRetries} for activity ${activityId} failed: ${error.message}`);
if (error.response) {
console.error(`Status: ${error.response.status}, Data (getActivityDetailsRaw): ${ String(error.response.data).slice(0,100)}...`);
}
if (attempt === maxRetries - 1) {
console.error(`All ${maxRetries} retries failed for activity ${activityId}.`);
throw error;
}
await new Promise(resolve => setTimeout(resolve, 1000 * (attempt + 1)));
}
}
return null;
}
/**
* Main exported function. Handles cookie caching, validation, re-authentication, and fetches activity details.
* @param {string} activityId - The ID of the activity to fetch.
* @param {string} userName - URL-encoded username.
* @param {string} userPwd - URL-encoded password.
* @param {string} [templateFileName="login_template.txt"] - Name of the login template file.
* @param {boolean} [forceLogin=false] - If true, bypasses cached cookie and forces a new login.
* @returns {Promise<object|null>} The parsed JSON object of activity details, or null on failure.
*/
export async function fetchActivityData(activityId, userName, userPwd, templateFileName = "login_template.txt", forceLogin = false) {
let currentCookie = forceLogin ? null : await loadCachedCookie();
if (forceLogin && currentCookie) {
await clearCookieCache();
currentCookie = null;
}
if (currentCookie) {
const isValid = await testCookieValidity(currentCookie);
if (!isValid) {
console.log("Cached cookie test failed or cookie expired. Clearing cache.");
await clearCookieCache();
currentCookie = null;
} else {
console.log("Using valid cached cookie.");
}
}
if (!currentCookie) {
console.log(forceLogin ? "Forcing new login." : "No valid cached cookie found or cache bypassed. Attempting login...");
try {
currentCookie = await getCompleteCookies(userName, userPwd, path.resolve(__dirname, templateFileName));
await saveCookieToCache(currentCookie);
} catch (loginError) {
console.error(`Login process failed: ${loginError.message}`);
return null;
}
}
if (!currentCookie) {
console.error("Critical: No cookie available after login attempt. Cannot fetch activity data.");
return null;
}
try {
const rawActivityDetailsString = await getActivityDetailsRaw(activityId, currentCookie);
if (rawActivityDetailsString) {
const parsedOuter = JSON.parse(rawActivityDetailsString);
return JSON.parse(parsedOuter.d);
}
console.warn(`No data returned from getActivityDetailsRaw for activity ${activityId}, but no authentication error was thrown.`);
return null;
} catch (error) {
if (error instanceof AuthenticationError) {
console.warn(`Initial fetch failed with AuthenticationError (Status: ${error.status}). Cookie was likely invalid. Attempting re-login and one retry.`);
await clearCookieCache();
try {
console.log("Attempting re-login due to authentication failure...");
currentCookie = await getCompleteCookies(userName, userPwd, path.resolve(__dirname, templateFileName));
await saveCookieToCache(currentCookie);
console.log("Re-login successful. Retrying request for activity details once...");
const rawActivityDetailsStringRetry = await getActivityDetailsRaw(activityId, currentCookie);
if (rawActivityDetailsStringRetry) {
const parsedOuterRetry = JSON.parse(rawActivityDetailsStringRetry);
return JSON.parse(parsedOuterRetry.d);
}
console.warn(`Still no details for activity ${activityId} after re-login and retry.`);
return null;
} catch (retryLoginOrFetchError) {
console.error(`Error during re-login or retry fetch for activity ${activityId}: ${retryLoginOrFetchError.message}`);
return null;
}
} else {
console.error(`Failed to fetch activity data for ${activityId} due to non-authentication error: ${error.message}`);
return null;
}
}
}
// Optionally export other functions if they are meant to be used externally
export { clearCookieCache, testCookieValidity };

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,151 @@
// struct-activity.mjs
let clubSchema = {
academicYear: null,
category: null,
description: null,
duration: {
endDate: null,
isRecurringWeekly: null,
startDate: null
},
grades: {
max: null,
min: null
},
id: null,
isPreSignup: null,
isStudentLed: null,
materials: [],
meeting: {
day: null,
endTime: null,
location: {
block: null,
room: null,
site: null
},
startTime: null
},
name: null,
photo: null,
poorWeatherPlan: null,
requirements: [],
schedule: null,
semesterCost: null,
staff: [],
staffForReports: [],
studentLeaders: []
}
async function applyFields(field, structuredActivityData) {
switch (true) {
case field.fID == "academicyear":
structuredActivityData.academicYear = field.fData;
break;
case field.fID == "schedule":
structuredActivityData.schedule = field.fData;
break;
case field.fID == "category":
structuredActivityData.category = field.fData;
break;
case field.fID == "activityname":
structuredActivityData.name = field.fData;
break;
case field.fID == "day":
structuredActivityData.meeting.day = field.fData;
break;
case field.fID == "start":
structuredActivityData.meeting.startTime = field.fData;
break;
case field.fID == "end":
structuredActivityData.meeting.endTime = field.fData;
break;
case field.fID == "site":
structuredActivityData.meeting.location.site = field.fData;
break;
case field.fID == "block":
structuredActivityData.meeting.location.block = field.fData;
break;
case field.fID == "room":
structuredActivityData.meeting.location.room = field.fData;
break;
case field.fID == "staff":
let staff = field.fData.split(", ");
structuredActivityData.staff = staff;
break;
case field.fID == "runsfrom":
structuredActivityData.duration.startDate = field.fData;
break;
case field.fID == "runsto":
structuredActivityData.duration.endDate = field.fData;
break;
case field.fData == "Recurring Weekly":
structuredActivityData.duration.isRecurringWeekly = true;
break;
default:
//console.log(`No matching case for field: fID=${field.fID}, fType=${field.fType}`);
break;
}
}
async function postProcess(structuredActivityData) {
structuredActivityData.description = structuredActivityData.description.replaceAll("<br/>","\n");
if (structuredActivityData.name.search("Student-led") != -1) {
structuredActivityData.isStudentLed = true;
} else {
structuredActivityData.isStudentLed = false;
}
const grades = structuredActivityData.schedule.match(/G(\d+)-(\d+)/);
structuredActivityData.grades.min = grades[1];
structuredActivityData.grades.max = grades[2];
}
export async function structActivityData(rawActivityData) {
let structuredActivityData = JSON.parse(JSON.stringify(clubSchema));
let rows = rawActivityData.newRows;
// Load club id - "rID": "3350:1:0:0"
structuredActivityData.id = rows[0].rID.split(":")[0];
for (const rowObject of rows) {
for (let i = 0; i < rowObject.fields.length; i++) {
const field = rowObject.fields[i];
// Optimize: no fData, just skip
if (field.fData == null && field.fData == "") { continue; }
// Process hard cases first
if (field.fData == "Description") {
structuredActivityData.description = rowObject.fields[i + 1].fData;
continue;
} else if (field.fData == "Name To Appear On Reports"){
let staffForReports = rowObject.fields[i + 1].fData.split(", ");
structuredActivityData.staffForReports = staffForReports;
} else if (field.fData == "Upload Photo") {
structuredActivityData.photo = rowObject.fields[i + 1].fData;
} else if (field.fData == "Poor Weather Plan") {
structuredActivityData.poorWeatherPlan = rowObject.fields[i + 1].fData;
} else if (field.fData == "Activity Runs From") {
if (rowObject.fields[i + 4].fData == "Recurring Weekly") {
structuredActivityData.duration.isRecurringWeekly = true;
} else {
structuredActivityData.duration.isRecurringWeekly = false;
}
} else if (field.fData == "Is Pre Sign-up") {
if (rowObject.fields[i + 1].fData == "") {
structuredActivityData.isPreSignup = false;
} else {
structuredActivityData.isPreSignup = true;
}
} else if (field.fData == "Semester Cost") {
if (rowObject.fields[i + 1].fData == "") {
structuredActivityData.semesterCost = null;
} else {
structuredActivityData.semesterCost = rowObject.fields[i + 1].fData
}
} else {
// Pass any other easy cases to helper function
applyFields(field, structuredActivityData);
}
}
}
postProcess(structuredActivityData);
return structuredActivityData
}

View File

@@ -0,0 +1,21 @@
// struct-staff.mjs
let staffs = new Map();
async function updateStaffMap(staffs,lParms) {
for (const staff of lParms) {
staffs.set(staff.key, staff.val)
}
}
export async function structStaffData(rawActivityData) {
let rows = rawActivityData.newRows;
for (const rowObject of rows) {
for (const field of rowObject.fields) {
if (field.fID == "staff") {
await updateStaffMap(staffs, field.lParms);
return staffs;
}
}
}
}