@emiran/omu-ubys 0.1.0 → 0.2.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.d.cts CHANGED
@@ -76,6 +76,37 @@ interface ClassDetailSurvey {
76
76
  url?: string;
77
77
  expired?: boolean;
78
78
  }
79
+ interface SurveyChoice {
80
+ id: string;
81
+ text: string;
82
+ }
83
+ interface SurveyQuestion {
84
+ id: string;
85
+ text: string;
86
+ required: boolean;
87
+ choices: SurveyChoice[];
88
+ }
89
+ interface SurveyPage {
90
+ id: string;
91
+ title: string;
92
+ description?: string;
93
+ questions: SurveyQuestion[];
94
+ choiceLabels: string[];
95
+ }
96
+ interface SurveyData {
97
+ url: string;
98
+ title: string;
99
+ description: string;
100
+ courseName?: string;
101
+ surveyId: string;
102
+ surveyUserId: string;
103
+ redirectUrl?: string;
104
+ pages: SurveyPage[];
105
+ }
106
+ interface SurveyCheckResult {
107
+ hasPendingSurvey: boolean;
108
+ surveys?: SurveyData[];
109
+ }
79
110
  interface ClassDetail {
80
111
  exams: Exam[];
81
112
  students: Student[];
@@ -217,6 +248,25 @@ declare class UBYSClient {
217
248
  getClassDetails(classId: string): Promise<ClassDetail>;
218
249
  getTranscript(): Promise<Semester[]>;
219
250
  getWeeklySchedule(): Promise<ScheduleItem[]>;
251
+ /**
252
+ * Check whether there are any mandatory portal-level surveys blocking access.
253
+ * Navigates to the student home page and detects PortalSurveyManagement redirects.
254
+ * If surveys are found they are fetched and returned.
255
+ */
256
+ getPendingSurveys(): Promise<SurveyCheckResult>;
257
+ /**
258
+ * Fetch and parse a single MES survey participant page.
259
+ *
260
+ * @param surveyUrl - Full URL (e.g. https://ubys.omu.edu.tr/MES/Application/Public/Participant?Id=…)
261
+ */
262
+ fetchSurvey(surveyUrl: string): Promise<SurveyData>;
263
+ /**
264
+ * Submit answers for a survey.
265
+ *
266
+ * @param surveyData - The SurveyData returned by `fetchSurvey()`
267
+ * @param answers - Map of questionId → choiceId
268
+ */
269
+ submitSurvey(surveyData: SurveyData, answers: Record<string, string>): Promise<void>;
220
270
  clearSession(): void;
221
271
  close(): Promise<void>;
222
272
  private ensureAuthenticated;
@@ -261,4 +311,4 @@ declare class ClientClosedError extends UBYSError {
261
311
  constructor(message?: string, options?: UBYSErrorOptions);
262
312
  }
263
313
 
264
- export { type Advisor, AuthenticationError, type CafeteriaMenu, type Chat, type ChatMessage, type ChatMessageReceipt, type ChatMessageSearchOptions, type ChatMessageSearchResult, type ChatRecipient, type ChatRecipientSearchOptions, type ChatRecipientSearchResult, type ChatSearchOptions, type ChatSearchResult, type ClassDetail, type ClassDetailSurvey, ClientClosedError, type Course, type Exam, HttpError, type Instructor, LoginError, type MenuItem, NetworkError, NotImplementedUBYSError, ParseError, type ScheduleItem, type Semester, SessionExpiredError, type Student, type StudentInfoPayload, type StudentProgram, UBYSClient, type UBYSClientOptions, UBYSError, type UserProfile };
314
+ export { type Advisor, AuthenticationError, type CafeteriaMenu, type Chat, type ChatMessage, type ChatMessageReceipt, type ChatMessageSearchOptions, type ChatMessageSearchResult, type ChatRecipient, type ChatRecipientSearchOptions, type ChatRecipientSearchResult, type ChatSearchOptions, type ChatSearchResult, type ClassDetail, type ClassDetailSurvey, ClientClosedError, type Course, type Exam, HttpError, type Instructor, LoginError, type MenuItem, NetworkError, NotImplementedUBYSError, ParseError, type ScheduleItem, type Semester, SessionExpiredError, type Student, type StudentInfoPayload, type StudentProgram, type SurveyCheckResult, type SurveyChoice, type SurveyData, type SurveyPage, type SurveyQuestion, UBYSClient, type UBYSClientOptions, UBYSError, type UserProfile };
package/dist/index.d.ts CHANGED
@@ -76,6 +76,37 @@ interface ClassDetailSurvey {
76
76
  url?: string;
77
77
  expired?: boolean;
78
78
  }
79
+ interface SurveyChoice {
80
+ id: string;
81
+ text: string;
82
+ }
83
+ interface SurveyQuestion {
84
+ id: string;
85
+ text: string;
86
+ required: boolean;
87
+ choices: SurveyChoice[];
88
+ }
89
+ interface SurveyPage {
90
+ id: string;
91
+ title: string;
92
+ description?: string;
93
+ questions: SurveyQuestion[];
94
+ choiceLabels: string[];
95
+ }
96
+ interface SurveyData {
97
+ url: string;
98
+ title: string;
99
+ description: string;
100
+ courseName?: string;
101
+ surveyId: string;
102
+ surveyUserId: string;
103
+ redirectUrl?: string;
104
+ pages: SurveyPage[];
105
+ }
106
+ interface SurveyCheckResult {
107
+ hasPendingSurvey: boolean;
108
+ surveys?: SurveyData[];
109
+ }
79
110
  interface ClassDetail {
80
111
  exams: Exam[];
81
112
  students: Student[];
@@ -217,6 +248,25 @@ declare class UBYSClient {
217
248
  getClassDetails(classId: string): Promise<ClassDetail>;
218
249
  getTranscript(): Promise<Semester[]>;
219
250
  getWeeklySchedule(): Promise<ScheduleItem[]>;
251
+ /**
252
+ * Check whether there are any mandatory portal-level surveys blocking access.
253
+ * Navigates to the student home page and detects PortalSurveyManagement redirects.
254
+ * If surveys are found they are fetched and returned.
255
+ */
256
+ getPendingSurveys(): Promise<SurveyCheckResult>;
257
+ /**
258
+ * Fetch and parse a single MES survey participant page.
259
+ *
260
+ * @param surveyUrl - Full URL (e.g. https://ubys.omu.edu.tr/MES/Application/Public/Participant?Id=…)
261
+ */
262
+ fetchSurvey(surveyUrl: string): Promise<SurveyData>;
263
+ /**
264
+ * Submit answers for a survey.
265
+ *
266
+ * @param surveyData - The SurveyData returned by `fetchSurvey()`
267
+ * @param answers - Map of questionId → choiceId
268
+ */
269
+ submitSurvey(surveyData: SurveyData, answers: Record<string, string>): Promise<void>;
220
270
  clearSession(): void;
221
271
  close(): Promise<void>;
222
272
  private ensureAuthenticated;
@@ -261,4 +311,4 @@ declare class ClientClosedError extends UBYSError {
261
311
  constructor(message?: string, options?: UBYSErrorOptions);
262
312
  }
263
313
 
264
- export { type Advisor, AuthenticationError, type CafeteriaMenu, type Chat, type ChatMessage, type ChatMessageReceipt, type ChatMessageSearchOptions, type ChatMessageSearchResult, type ChatRecipient, type ChatRecipientSearchOptions, type ChatRecipientSearchResult, type ChatSearchOptions, type ChatSearchResult, type ClassDetail, type ClassDetailSurvey, ClientClosedError, type Course, type Exam, HttpError, type Instructor, LoginError, type MenuItem, NetworkError, NotImplementedUBYSError, ParseError, type ScheduleItem, type Semester, SessionExpiredError, type Student, type StudentInfoPayload, type StudentProgram, UBYSClient, type UBYSClientOptions, UBYSError, type UserProfile };
314
+ export { type Advisor, AuthenticationError, type CafeteriaMenu, type Chat, type ChatMessage, type ChatMessageReceipt, type ChatMessageSearchOptions, type ChatMessageSearchResult, type ChatRecipient, type ChatRecipientSearchOptions, type ChatRecipientSearchResult, type ChatSearchOptions, type ChatSearchResult, type ClassDetail, type ClassDetailSurvey, ClientClosedError, type Course, type Exam, HttpError, type Instructor, LoginError, type MenuItem, NetworkError, NotImplementedUBYSError, ParseError, type ScheduleItem, type Semester, SessionExpiredError, type Student, type StudentInfoPayload, type StudentProgram, type SurveyCheckResult, type SurveyChoice, type SurveyData, type SurveyPage, type SurveyQuestion, UBYSClient, type UBYSClientOptions, UBYSError, type UserProfile };
package/dist/index.js CHANGED
@@ -1023,6 +1023,167 @@ function textOrUndefined3(value) {
1023
1023
  function normalizeText2(value) {
1024
1024
  return decodeHtmlEntities(value).replace(/\s+/g, " ").trim();
1025
1025
  }
1026
+ function parseSurveyPage(html, surveyUrl) {
1027
+ if (html.includes("/Account/Login")) {
1028
+ throw new SessionExpiredError();
1029
+ }
1030
+ const $ = load(html);
1031
+ const description = $(".panel-body p").first().text().trim();
1032
+ let surveyId = "";
1033
+ let surveyUserId = "";
1034
+ let redirectUrl = "";
1035
+ $("script").each((_, script) => {
1036
+ const content = $(script).html() ?? "";
1037
+ const base64Match = content.match(
1038
+ /var\s+_jsm\s*=\s*null;\s*\(function\(\)\s*\{\s*_jsm\s*=\s*JSON\.parse\(Base64\.decode\(["']([^"']+)["']\)/
1039
+ );
1040
+ if (base64Match) {
1041
+ try {
1042
+ const encoded = base64Match[1];
1043
+ if (!encoded) return;
1044
+ const decoded = Buffer.from(encoded, "base64").toString("utf-8");
1045
+ const jsmObj = JSON.parse(decoded);
1046
+ if (typeof jsmObj["SurveyId"] === "number" && !surveyId) {
1047
+ surveyId = String(jsmObj["SurveyId"]);
1048
+ }
1049
+ const survey = jsmObj["Survey"];
1050
+ if (typeof survey?.["Id"] === "number" && !surveyId) {
1051
+ surveyId = String(survey["Id"]);
1052
+ }
1053
+ const surveyUser = jsmObj["SurveyUser"];
1054
+ if (typeof surveyUser?.["Id"] === "number" && !surveyUserId) {
1055
+ surveyUserId = String(surveyUser["Id"]);
1056
+ }
1057
+ if (typeof jsmObj["RedirectPageUrl"] === "string" && !redirectUrl) {
1058
+ redirectUrl = jsmObj["RedirectPageUrl"];
1059
+ }
1060
+ } catch {
1061
+ }
1062
+ }
1063
+ if (!surveyId) {
1064
+ const m = content.match(/Survey\s*:\s*\{\s*Id\s*:\s*(\d+)/);
1065
+ if (m) surveyId = m[1] ?? "";
1066
+ }
1067
+ if (!surveyUserId) {
1068
+ const m = content.match(/SurveyUser\s*:\s*\{\s*Id\s*:\s*(\d+)/);
1069
+ if (m) surveyUserId = m[1] ?? "";
1070
+ }
1071
+ if (!redirectUrl) {
1072
+ const m = content.match(/RedirectPageUrl\s*:\s*["']([^"']+)["']/);
1073
+ if (m) redirectUrl = m[1] ?? "";
1074
+ }
1075
+ });
1076
+ if (!surveyId) {
1077
+ surveyId = $("#surveyParent").attr("data-survey-id") ?? "";
1078
+ }
1079
+ if (!surveyUserId) {
1080
+ const m = surveyUrl.match(/[?&]Id=(\d+)/i);
1081
+ if (m) surveyUserId = m[1] ?? "";
1082
+ }
1083
+ if (!redirectUrl) {
1084
+ try {
1085
+ const urlObj = new URL(surveyUrl);
1086
+ redirectUrl = urlObj.searchParams.get("RedirectPageUrl") ?? "";
1087
+ } catch {
1088
+ }
1089
+ }
1090
+ if (!surveyId && !surveyUserId) {
1091
+ throw new ParseError("Could not extract surveyId or surveyUserId from survey page.", {
1092
+ details: { surveyUrl }
1093
+ });
1094
+ }
1095
+ const pages = parseSurveyPages($);
1096
+ return {
1097
+ url: surveyUrl,
1098
+ title: "Ders De\u011Ferlendirme Anketi",
1099
+ description,
1100
+ surveyId,
1101
+ surveyUserId,
1102
+ ...redirectUrl ? { redirectUrl } : {},
1103
+ pages
1104
+ };
1105
+ }
1106
+ function parseSurveyPages($) {
1107
+ const pages = [];
1108
+ $(".survey-page").each((_, pageEl) => {
1109
+ const pageId = $(pageEl).attr("data-page-id") ?? "";
1110
+ if (!pageId) return;
1111
+ const pageTitleText = $(pageEl).text().trim();
1112
+ const pageBody = $(pageEl).closest(".panel").find(".panel-body");
1113
+ const questions = [];
1114
+ const choiceLabels = [];
1115
+ const table = pageBody.find("table");
1116
+ if (table.length > 0) {
1117
+ table.find("thead th").each((idx, th) => {
1118
+ const choiceText = $(th).text().trim();
1119
+ if (choiceText && idx > 0) {
1120
+ choiceLabels.push(choiceText);
1121
+ }
1122
+ });
1123
+ table.find("tbody tr[data-question-id]").each((_2, row) => {
1124
+ const questionId = $(row).attr("data-question-id") ?? "";
1125
+ const questionText = $(row).find("td").first().text().trim();
1126
+ const isRequired = $(row).hasClass("forRequired");
1127
+ const questionChoices = [];
1128
+ $(row).find('input[type="radio"]').each((idx, input) => {
1129
+ const choiceId = $(input).attr("data-choise-id") ?? "";
1130
+ if (choiceId) {
1131
+ questionChoices.push({
1132
+ id: choiceId,
1133
+ text: choiceLabels[idx] ?? `Option ${idx + 1}`
1134
+ });
1135
+ }
1136
+ });
1137
+ if (questionId && questionText) {
1138
+ questions.push({
1139
+ id: questionId,
1140
+ text: questionText,
1141
+ required: isRequired,
1142
+ choices: questionChoices
1143
+ });
1144
+ }
1145
+ });
1146
+ }
1147
+ pages.push({
1148
+ id: pageId,
1149
+ title: pageTitleText,
1150
+ questions,
1151
+ choiceLabels
1152
+ });
1153
+ });
1154
+ return pages;
1155
+ }
1156
+ function buildSurveySubmitBody(surveyData, answers) {
1157
+ const choises = Object.entries(answers).map(([questionId, choiceId]) => ({
1158
+ Id: 0,
1159
+ SelectedOrder: null,
1160
+ SurveyQuestionId: parseInt(questionId, 10),
1161
+ SurveyUserId: parseInt(surveyData.surveyUserId, 10),
1162
+ SurveyQuestionChoiseId: parseInt(choiceId, 10),
1163
+ Text: "",
1164
+ IsIrrevelant: false
1165
+ }));
1166
+ return {
1167
+ choises,
1168
+ RedirectPageUrl: surveyData.redirectUrl ?? "",
1169
+ surveyId: parseInt(surveyData.surveyId, 10)
1170
+ };
1171
+ }
1172
+ function extractSurveyCsrfToken(html) {
1173
+ const $ = load(html);
1174
+ const token = $('input[name="__RequestVerificationToken"]').val();
1175
+ return typeof token === "string" && token ? token : void 0;
1176
+ }
1177
+ function parseSurveyLinks(html, baseUrl) {
1178
+ const $ = load(html);
1179
+ const links = [];
1180
+ $('a[href*="MES/Application/Public/Participant"]').each((_, el) => {
1181
+ const href = $(el).attr("href");
1182
+ if (!href) return;
1183
+ links.push(href.startsWith("http") ? href : `${baseUrl}${href}`);
1184
+ });
1185
+ return links;
1186
+ }
1026
1187
 
1027
1188
  // src/client.ts
1028
1189
  var UBYSClient = class {
@@ -1279,6 +1440,89 @@ var UBYSClient = class {
1279
1440
  this.session.assertNotLoginRedirect(response);
1280
1441
  return parseWeeklySchedule(response.body);
1281
1442
  }
1443
+ /**
1444
+ * Check whether there are any mandatory portal-level surveys blocking access.
1445
+ * Navigates to the student home page and detects PortalSurveyManagement redirects.
1446
+ * If surveys are found they are fetched and returned.
1447
+ */
1448
+ async getPendingSurveys() {
1449
+ this.ensureAuthenticated();
1450
+ const homeResponse = await this.session.get("AIS/Student/Home/Index");
1451
+ this.session.assertOk(homeResponse);
1452
+ const responseUrl = homeResponse.url;
1453
+ const html = homeResponse.body;
1454
+ const redirectedToSurvey = responseUrl.includes("PortalSurveyManagement");
1455
+ const htmlMentionsSurvey = html.includes("Zorunlu olan anketi");
1456
+ if (!redirectedToSurvey && !htmlMentionsSurvey) {
1457
+ return { hasPendingSurvey: false };
1458
+ }
1459
+ if (redirectedToSurvey) {
1460
+ let requestedUrl = null;
1461
+ try {
1462
+ requestedUrl = new URL(responseUrl).searchParams.get("RequestedUrl");
1463
+ } catch {
1464
+ }
1465
+ if (requestedUrl) {
1466
+ const retryResponse = await this.session.get(requestedUrl);
1467
+ if (!retryResponse.url.includes("PortalSurveyManagement")) {
1468
+ return { hasPendingSurvey: false };
1469
+ }
1470
+ }
1471
+ const surveyLinks = parseSurveyLinks(html, this.session.baseUrl);
1472
+ if (surveyLinks.length > 0) {
1473
+ const surveys = [];
1474
+ for (const link of surveyLinks) {
1475
+ try {
1476
+ const survey = await this.fetchSurvey(link);
1477
+ surveys.push(survey);
1478
+ } catch {
1479
+ }
1480
+ }
1481
+ return { hasPendingSurvey: true, ...surveys.length > 0 ? { surveys } : {} };
1482
+ }
1483
+ }
1484
+ return { hasPendingSurvey: true };
1485
+ }
1486
+ /**
1487
+ * Fetch and parse a single MES survey participant page.
1488
+ *
1489
+ * @param surveyUrl - Full URL (e.g. https://ubys.omu.edu.tr/MES/Application/Public/Participant?Id=…)
1490
+ */
1491
+ async fetchSurvey(surveyUrl) {
1492
+ this.ensureAuthenticated();
1493
+ const response = await this.session.get(surveyUrl);
1494
+ this.session.assertOk(response);
1495
+ this.session.assertNotLoginRedirect(response);
1496
+ return parseSurveyPage(response.body, surveyUrl);
1497
+ }
1498
+ /**
1499
+ * Submit answers for a survey.
1500
+ *
1501
+ * @param surveyData - The SurveyData returned by `fetchSurvey()`
1502
+ * @param answers - Map of questionId → choiceId
1503
+ */
1504
+ async submitSurvey(surveyData, answers) {
1505
+ this.ensureAuthenticated();
1506
+ const pageResponse = await this.session.get(surveyData.url);
1507
+ this.session.assertOk(pageResponse);
1508
+ this.session.assertNotLoginRedirect(pageResponse);
1509
+ const csrfToken = extractSurveyCsrfToken(pageResponse.body);
1510
+ if (!csrfToken) {
1511
+ throw new Error("CSRF token not found on survey page.");
1512
+ }
1513
+ const body = buildSurveySubmitBody(surveyData, answers);
1514
+ const finishUrl = `${this.session.baseUrl}/MES/Application/Public/FinishSurvey`;
1515
+ const submitResponse = await this.session.post(finishUrl, {
1516
+ body: JSON.stringify(body),
1517
+ headers: {
1518
+ "content-type": "application/json; charset=UTF-8",
1519
+ "x-requested-with": "XMLHttpRequest",
1520
+ "x-requestverificationtoken": csrfToken,
1521
+ referer: surveyData.url
1522
+ }
1523
+ });
1524
+ this.session.assertOk(submitResponse);
1525
+ }
1282
1526
  clearSession() {
1283
1527
  this.ensureOpen();
1284
1528
  this.state = {