chuvsu-js 0.1.0 → 0.3.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.
@@ -1,10 +1,13 @@
1
1
  import type { PersonalData } from "./types.js";
2
2
  export declare class LkClient {
3
3
  private http;
4
+ private credentials;
4
5
  login(opts: {
5
6
  email: string;
6
7
  password: string;
7
8
  }): Promise<void>;
9
+ private isSessionExpired;
10
+ private authGet;
8
11
  getPersonalData(): Promise<PersonalData>;
9
12
  getGroupId(): Promise<number | null>;
10
13
  }
package/dist/lk/client.js CHANGED
@@ -6,14 +6,27 @@ const LOGIN_URL = `${BASE}/info/login.php`;
6
6
  const STUDENT_BASE = `${BASE}/student`;
7
7
  export class LkClient {
8
8
  http = new HttpClient();
9
+ credentials = null;
9
10
  async login(opts) {
10
11
  const res = await this.http.post(LOGIN_URL, { email: opts.email, password: opts.password, role: "1", enter: "" }, false);
11
12
  if (!(res.status === 302 && res.location?.includes("student"))) {
12
13
  throw new AuthError("LK login failed");
13
14
  }
15
+ this.credentials = opts;
16
+ }
17
+ isSessionExpired(body) {
18
+ return body.includes("login.php");
19
+ }
20
+ async authGet(url) {
21
+ const res = await this.http.get(url);
22
+ if (this.credentials && this.isSessionExpired(res.body)) {
23
+ await this.login(this.credentials);
24
+ return this.http.get(url);
25
+ }
26
+ return res;
14
27
  }
15
28
  async getPersonalData() {
16
- const { body } = await this.http.get(`${STUDENT_BASE}/personal_data.php`);
29
+ const { body } = await this.authGet(`${STUDENT_BASE}/personal_data.php`);
17
30
  const vals = extractScriptValues(body, "form_personal_data");
18
31
  return {
19
32
  lastName: vals.fam ?? "",
@@ -32,7 +45,7 @@ export class LkClient {
32
45
  };
33
46
  }
34
47
  async getGroupId() {
35
- const { body } = await this.http.get(`${STUDENT_BASE}/tt.php`);
48
+ const { body } = await this.authGet(`${STUDENT_BASE}/tt.php`);
36
49
  const match = body.match(/tt\.chuvsu\.ru\/index\/grouptt\/gr\/(\d+)/);
37
50
  return match ? parseInt(match[1]) : null;
38
51
  }
@@ -1,10 +1,11 @@
1
1
  import type { CacheEntry } from "../common/cache.js";
2
- import type { Period } from "../common/types.js";
2
+ import { Period } from "../common/types.js";
3
3
  import type { Faculty, Group, FullScheduleDay, ScheduleFilter, Lesson, ScheduleWeekDay, TtClientOptions, CacheConfig } from "./types.js";
4
4
  export declare class TtClient {
5
5
  private http;
6
6
  private educationType;
7
7
  private cache;
8
+ private loginMode;
8
9
  constructor(opts?: TtClientOptions);
9
10
  private get pertt();
10
11
  clearCache(category?: keyof CacheConfig): void;
@@ -15,6 +16,10 @@ export declare class TtClient {
15
16
  password: string;
16
17
  }): Promise<void>;
17
18
  loginAsGuest(): Promise<void>;
19
+ private isSessionExpired;
20
+ private relogin;
21
+ private authGet;
22
+ private authPost;
18
23
  getGroupSchedule(opts: {
19
24
  groupId: number;
20
25
  period?: Period;
@@ -30,7 +35,7 @@ export declare class TtClient {
30
35
  getScheduleForDate(opts: {
31
36
  groupId: number;
32
37
  date: Date;
33
- filter?: ScheduleFilter;
38
+ subgroup?: number;
34
39
  period?: Period;
35
40
  }): Promise<Lesson[]>;
36
41
  getScheduleForWeek(opts: {
@@ -41,11 +46,11 @@ export declare class TtClient {
41
46
  }): Promise<ScheduleWeekDay[]>;
42
47
  getCurrentLesson(opts: {
43
48
  groupId: number;
44
- filter?: ScheduleFilter;
49
+ subgroup?: number;
45
50
  }): Promise<Lesson | null>;
46
- getCurrentPeriod(opts: {
47
- groupId: number;
48
- }): Promise<Period | null>;
51
+ getCurrentPeriod(opts?: {
52
+ date?: Date;
53
+ }): Period;
49
54
  getFaculties(): Promise<Faculty[]>;
50
55
  getGroupsForFaculty(opts: {
51
56
  facultyId: number;
package/dist/tt/client.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { HttpClient } from "../common/http.js";
2
2
  import { Cache } from "../common/cache.js";
3
3
  import { AuthError } from "../common/types.js";
4
- import { parsePeriodFromPage, parseGroupButtons, parseFacultyButtons, parseTeacherButtons, parseFullSchedule, } from "./parse.js";
4
+ import { parseGroupButtons, parseFacultyButtons, parseTeacherButtons, parseFullSchedule, } from "./parse.js";
5
5
  import { filterSlots, getMonday, getWeekdayName, getWeekNumber, getSemesterStart, getSemesterWeeks, slotsToLessons, } from "./schedule.js";
6
6
  const BASE = "https://tt.chuvsu.ru";
7
7
  const AUTH_URL = `${BASE}/auth`;
@@ -9,6 +9,7 @@ export class TtClient {
9
9
  http = new HttpClient();
10
10
  educationType;
11
11
  cache;
12
+ loginMode = null;
12
13
  constructor(opts) {
13
14
  this.educationType = opts?.educationType ?? 1 /* EducationType.HigherEducation */;
14
15
  if (opts?.cache == null) {
@@ -19,7 +20,6 @@ export class TtClient {
19
20
  schedule: opts.cache,
20
21
  faculties: opts.cache,
21
22
  groups: opts.cache,
22
- currentPeriod: opts.cache,
23
23
  });
24
24
  }
25
25
  else {
@@ -52,12 +52,43 @@ export class TtClient {
52
52
  if (res.status !== 302) {
53
53
  throw new AuthError("TT login failed");
54
54
  }
55
+ this.loginMode = { type: "credentials", ...opts };
55
56
  }
56
57
  async loginAsGuest() {
57
58
  const res = await this.http.post(AUTH_URL, { guest: "Войти гостем", hfac: "0", pertt: this.pertt }, false);
58
59
  if (res.status !== 302) {
59
60
  throw new AuthError("TT guest login failed");
60
61
  }
62
+ this.loginMode = { type: "guest" };
63
+ }
64
+ isSessionExpired(body) {
65
+ return body.includes('name="wname"');
66
+ }
67
+ async relogin() {
68
+ if (!this.loginMode)
69
+ return;
70
+ if (this.loginMode.type === "credentials") {
71
+ await this.login(this.loginMode);
72
+ }
73
+ else {
74
+ await this.loginAsGuest();
75
+ }
76
+ }
77
+ async authGet(url) {
78
+ const res = await this.http.get(url);
79
+ if (this.loginMode && this.isSessionExpired(res.body)) {
80
+ await this.relogin();
81
+ return this.http.get(url);
82
+ }
83
+ return res;
84
+ }
85
+ async authPost(url, data) {
86
+ const res = await this.http.post(url, data);
87
+ if (this.loginMode && this.isSessionExpired(res.body)) {
88
+ await this.relogin();
89
+ return this.http.post(url, data);
90
+ }
91
+ return res;
61
92
  }
62
93
  // --- Schedule ---
63
94
  async getGroupSchedule(opts) {
@@ -68,14 +99,10 @@ export class TtClient {
68
99
  const url = `${BASE}/index/grouptt/gr/${opts.groupId}`;
69
100
  let body;
70
101
  if (opts.period !== undefined) {
71
- ({ body } = await this.http.post(url, { htype: String(opts.period) }));
102
+ ({ body } = await this.authPost(url, { htype: String(opts.period) }));
72
103
  }
73
104
  else {
74
- ({ body } = await this.http.get(url));
75
- }
76
- const period = parsePeriodFromPage(body);
77
- if (period !== null) {
78
- this.cache?.set("currentPeriod", String(opts.groupId), period);
105
+ ({ body } = await this.authGet(url));
79
106
  }
80
107
  const data = parseFullSchedule(body);
81
108
  this.cache?.set("schedule", cacheKey, data);
@@ -97,7 +124,9 @@ export class TtClient {
97
124
  const semesterStart = getSemesterStart({ period });
98
125
  const startMonday = getMonday(semesterStart);
99
126
  const date = new Date(startMonday);
100
- date.setDate(startMonday.getDate() + week * 7 + (weekday === 0 ? 6 : weekday - 1));
127
+ date.setDate(startMonday.getDate() +
128
+ (week - 1) * 7 +
129
+ (weekday === 0 ? 6 : weekday - 1));
101
130
  return date;
102
131
  }
103
132
  const now = new Date();
@@ -109,9 +138,7 @@ export class TtClient {
109
138
  return date;
110
139
  }
111
140
  async getScheduleForDay(opts) {
112
- const period = opts.period ??
113
- (await this.getCurrentPeriod({ groupId: opts.groupId })) ??
114
- 1;
141
+ const period = opts.period ?? this.getCurrentPeriod();
115
142
  const slots = await this.getFilteredSlots({
116
143
  groupId: opts.groupId,
117
144
  weekday: opts.weekday,
@@ -123,23 +150,21 @@ export class TtClient {
123
150
  }
124
151
  async getScheduleForDate(opts) {
125
152
  const weekday = opts.date.getDay();
126
- const period = opts.period ?? (await this.getCurrentPeriod({ groupId: opts.groupId }));
127
- const effectiveFilter = { ...opts.filter };
128
- if (period && !effectiveFilter.week) {
153
+ const period = opts.period ?? this.getCurrentPeriod({ date: opts.date });
154
+ const effectiveFilter = { subgroup: opts.subgroup };
155
+ if (effectiveFilter.week == null) {
129
156
  effectiveFilter.week = getWeekNumber({ period, date: opts.date });
130
157
  }
131
158
  const slots = await this.getFilteredSlots({
132
159
  groupId: opts.groupId,
133
160
  weekday,
134
161
  filter: effectiveFilter,
135
- period: period ?? undefined,
162
+ period,
136
163
  });
137
164
  return slotsToLessons(slots, opts.date);
138
165
  }
139
166
  async getScheduleForWeek(opts) {
140
- const period = opts.period ??
141
- (await this.getCurrentPeriod({ groupId: opts.groupId })) ??
142
- 1;
167
+ const period = opts.period ?? this.getCurrentPeriod();
143
168
  const week = opts.week ?? getWeekNumber({ period });
144
169
  const effectiveFilter = { ...opts.filter, week };
145
170
  const semesterWeeks = getSemesterWeeks({ period, weekCount: week });
@@ -166,7 +191,7 @@ export class TtClient {
166
191
  const lessons = await this.getScheduleForDate({
167
192
  groupId: opts.groupId,
168
193
  date: now,
169
- filter: opts.filter,
194
+ subgroup: opts.subgroup,
170
195
  });
171
196
  const timeMinutes = now.getHours() * 60 + now.getMinutes();
172
197
  for (const lesson of lessons) {
@@ -179,24 +204,28 @@ export class TtClient {
179
204
  return null;
180
205
  }
181
206
  // --- Period ---
182
- async getCurrentPeriod(opts) {
183
- const cacheKey = String(opts.groupId);
184
- const cached = this.cache?.get("currentPeriod", cacheKey);
185
- if (cached != null)
186
- return cached;
187
- const { body } = await this.http.get(`${BASE}/index/grouptt/gr/${opts.groupId}`);
188
- const period = parsePeriodFromPage(body);
189
- if (period !== null) {
190
- this.cache?.set("currentPeriod", cacheKey, period);
191
- }
192
- return period;
207
+ getCurrentPeriod(opts) {
208
+ const date = opts?.date ?? new Date();
209
+ const month = date.getMonth();
210
+ const day = date.getDate();
211
+ // Dec 25+ and Jan → Winter session (зимняя сессия)
212
+ if (month === 0 || (month === 11 && day >= 25))
213
+ return 2 /* Period.WinterSession */;
214
+ // Feb–May → Spring semester (весенний семестр)
215
+ if (month >= 1 && month <= 4)
216
+ return 3 /* Period.SpringSemester */;
217
+ // Jun–Aug → Summer session (летняя сессия)
218
+ if (month >= 5 && month <= 7)
219
+ return 4 /* Period.SummerSession */;
220
+ // Sep – Dec 24 → Fall semester (осенний семестр)
221
+ return 1 /* Period.FallSemester */;
193
222
  }
194
223
  // --- Search / Discovery ---
195
224
  async getFaculties() {
196
225
  const cached = this.cache?.get("faculties", "all");
197
226
  if (cached)
198
227
  return cached;
199
- const { body } = await this.http.get(`${BASE}/`);
228
+ const { body } = await this.authGet(`${BASE}/`);
200
229
  const data = parseFacultyButtons(body);
201
230
  this.cache?.set("faculties", "all", data);
202
231
  return data;
@@ -206,7 +235,7 @@ export class TtClient {
206
235
  const cached = this.cache?.get("groups", cacheKey);
207
236
  if (cached)
208
237
  return cached;
209
- const { body } = await this.http.post(`${BASE}/`, {
238
+ const { body } = await this.authPost(`${BASE}/`, {
210
239
  hfac: String(opts.facultyId),
211
240
  pertt: this.pertt,
212
241
  });
@@ -215,7 +244,7 @@ export class TtClient {
215
244
  return data;
216
245
  }
217
246
  async searchGroup(opts) {
218
- const { body } = await this.http.post(`${BASE}/`, {
247
+ const { body } = await this.authPost(`${BASE}/`, {
219
248
  grname: opts.name,
220
249
  findgr: "найти",
221
250
  hfac: "0",
@@ -224,7 +253,7 @@ export class TtClient {
224
253
  return parseGroupButtons(body);
225
254
  }
226
255
  async searchTeacher(opts) {
227
- const { body } = await this.http.post(`${BASE}/`, {
256
+ const { body } = await this.authPost(`${BASE}/`, {
228
257
  techname: opts.name,
229
258
  findtech: "найти",
230
259
  hfac: "0",
@@ -27,6 +27,5 @@ export declare function getSemesterWeeks(opts: {
27
27
  export declare function getWeekNumber(opts: {
28
28
  period: Period;
29
29
  date?: Date;
30
- year?: number;
31
30
  }): number;
32
31
  export declare function slotsToLessons(slots: FullScheduleSlot[], date: Date): Lesson[];
@@ -11,7 +11,7 @@ export function getWeekdayName(weekday) {
11
11
  return WEEKDAY_NAMES[weekday] ?? "";
12
12
  }
13
13
  export function filterSlots(slots, filter) {
14
- if (!filter?.subgroup && !filter?.week)
14
+ if (filter?.subgroup == null && filter?.week == null)
15
15
  return slots;
16
16
  return slots
17
17
  .map((slot) => ({
@@ -25,7 +25,7 @@ function filterEntries(entries, filter) {
25
25
  if (filter?.subgroup && e.subgroup && e.subgroup !== filter.subgroup) {
26
26
  return false;
27
27
  }
28
- if (filter?.week) {
28
+ if (filter?.week != null) {
29
29
  if (e.weeks.from > 0 &&
30
30
  (filter.week < e.weeks.from || filter.week > e.weeks.to)) {
31
31
  return false;
@@ -67,7 +67,6 @@ export interface CacheConfig {
67
67
  schedule?: number;
68
68
  faculties?: number;
69
69
  groups?: number;
70
- currentPeriod?: number;
71
70
  }
72
71
  export interface TtClientOptions {
73
72
  educationType?: EducationType;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chuvsu-js",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Node.js library for ChuvSU student portal (lk.chuvsu.ru) and schedule (tt.chuvsu.ru)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",