chuvsu-js 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Artemy Egorov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,229 @@
1
+ # chuvsu-js
2
+
3
+ Node.js библиотека для работы с порталами ЧувГУ:
4
+
5
+ - **tt.chuvsu.ru** — расписание занятий (факультеты, группы, преподаватели)
6
+ - **lk.chuvsu.ru** — личный кабинет студента (персональные данные)
7
+
8
+ Пока что очень сырая, много что можно оптимизировать.
9
+
10
+ ## Установка
11
+
12
+ ```bash
13
+ npm install chuvsu-js
14
+ ```
15
+
16
+ ## Быстрый старт
17
+
18
+ ### Расписание (TtClient)
19
+
20
+ ```ts
21
+ import { TtClient } from "chuvsu-js";
22
+
23
+ const tt = new TtClient();
24
+
25
+ // Войти гостем (без учётной записи)
26
+ await tt.loginAsGuest();
27
+
28
+ // Найти группу по названию
29
+ const groups = await tt.searchGroup({ name: "КТ-41-24" });
30
+ console.log(groups); // [{ id: 123, name: "КТ-41-24", specialty: "...", profile: "..." }]
31
+
32
+ // Получить расписание на сегодня
33
+ const lessons = await tt.getScheduleForDate({
34
+ groupId: groups[0].id,
35
+ date: new Date(),
36
+ });
37
+
38
+ for (const lesson of lessons) {
39
+ console.log(
40
+ `${lesson.start.hours}:${lesson.start.minutes} — ${lesson.subject} (${lesson.type})`,
41
+ );
42
+ }
43
+ ```
44
+
45
+ ### Личный кабинет (LkClient)
46
+
47
+ ```ts
48
+ import { LkClient } from "chuvsu-js";
49
+
50
+ const lk = new LkClient();
51
+ await lk.login({ email: "student@mail.ru", password: "password" });
52
+
53
+ const data = await lk.getPersonalData();
54
+ console.log(`${data.lastName} ${data.firstName}, группа ${data.group}`);
55
+
56
+ // Получить ID группы для использования с TtClient
57
+ const groupId = await lk.getGroupId();
58
+ ```
59
+
60
+ ## API
61
+
62
+ ### TtClient
63
+
64
+ Клиент для работы с расписанием (`tt.chuvsu.ru`).
65
+
66
+ #### Конструктор
67
+
68
+ ```ts
69
+ new TtClient(options?: TtClientOptions)
70
+ ```
71
+
72
+ | Опция | Тип | По умолчанию | Описание |
73
+ | --------------- | ----------------------- | ----------------- | -------------------------------------------------------------- |
74
+ | `educationType` | `EducationType` | `HigherEducation` | Тип образования: высшее (1) или СПО (2) |
75
+ | `cache` | `number \| CacheConfig` | — | TTL кеша в мс. Число задаёт единый TTL, объект — по категориям |
76
+
77
+ #### Авторизация
78
+
79
+ ```ts
80
+ // С учётной записью
81
+ await tt.login({ email: "...", password: "..." });
82
+
83
+ // Гостевой вход
84
+ await tt.loginAsGuest();
85
+ ```
86
+
87
+ #### Расписание
88
+
89
+ ```ts
90
+ // Полное расписание группы (все дни, все слоты)
91
+ const schedule = await tt.getGroupSchedule({ groupId, period? });
92
+
93
+ // Расписание на конкретную дату
94
+ const lessons = await tt.getScheduleForDate({ groupId, date, filter?, period? });
95
+
96
+ // Расписание на день недели (0 = воскресенье, 1 = понедельник, ...)
97
+ const lessons = await tt.getScheduleForDay({ groupId, weekday, filter?, period? });
98
+
99
+ // Расписание на неделю
100
+ const week = await tt.getScheduleForWeek({ groupId, week?, filter?, period? });
101
+
102
+ // Текущая пара
103
+ const lesson = await tt.getCurrentLesson({ groupId, filter? });
104
+ ```
105
+
106
+ **ScheduleFilter** — фильтрация по подгруппе и/или неделе:
107
+
108
+ ```ts
109
+ { subgroup?: number; week?: number }
110
+ ```
111
+
112
+ #### Поиск
113
+
114
+ ```ts
115
+ // Список факультетов
116
+ const faculties = await tt.getFaculties();
117
+
118
+ // Группы факультета
119
+ const groups = await tt.getGroupsForFaculty({ facultyId });
120
+
121
+ // Поиск группы по названию
122
+ const groups = await tt.searchGroup({ name: "ЗИ" });
123
+
124
+ // Поиск преподавателя
125
+ const teachers = await tt.searchTeacher({ name: "Иванов" });
126
+ ```
127
+
128
+ #### Кеш
129
+
130
+ ```ts
131
+ // Очистить весь кеш или по категории
132
+ tt.clearCache();
133
+ tt.clearCache("schedule");
134
+
135
+ // Экспорт/импорт (для сохранения между запусками)
136
+ const data = tt.exportCache();
137
+ tt.importCache(data);
138
+ ```
139
+
140
+ Категории кеша: `schedule`, `faculties`, `groups`, `currentPeriod`.
141
+
142
+ #### Утилиты семестра
143
+
144
+ ```ts
145
+ import {
146
+ getSemesterStart,
147
+ getSemesterWeeks,
148
+ getWeekNumber,
149
+ Period,
150
+ } from "chuvsu-js";
151
+
152
+ // Начало семестра
153
+ getSemesterStart({ period: Period.FallSemester, year: 2025 });
154
+
155
+ // Все недели семестра
156
+ getSemesterWeeks({ period: Period.SpringSemester });
157
+
158
+ // Номер текущей недели
159
+ getWeekNumber({ period: Period.SpringSemester });
160
+ ```
161
+
162
+ ### LkClient
163
+
164
+ Клиент для личного кабинета (`lk.chuvsu.ru`).
165
+
166
+ ```ts
167
+ await lk.login({ email, password });
168
+ const data = await lk.getPersonalData();
169
+ const groupId = await lk.getGroupId();
170
+ ```
171
+
172
+ **PersonalData** содержит: `lastName`, `firstName`, `patronymic`, `sex`, `birthday`, `recordBookNumber`, `faculty`, `specialty`, `profile`, `group`, `course`, `email`, `phone`.
173
+
174
+ ## Типы
175
+
176
+ ### Period
177
+
178
+ ```ts
179
+ enum Period {
180
+ FallSemester = 1, // Осенний семестр
181
+ WinterSession = 2, // Зимняя сессия
182
+ SpringSemester = 3, // Весенний семестр
183
+ SummerSession = 4, // Летняя сессия
184
+ }
185
+ ```
186
+
187
+ ### EducationType
188
+
189
+ ```ts
190
+ enum EducationType {
191
+ HigherEducation = 1, // Высшее образование
192
+ VocationalEducation = 2, // СПО
193
+ }
194
+ ```
195
+
196
+ ### Lesson
197
+
198
+ ```ts
199
+ interface Lesson {
200
+ number: number; // Номер пары
201
+ start: LessonTime; // Начало { date, hours, minutes }
202
+ end: LessonTime; // Конец { date, hours, minutes }
203
+ subject: string; // Предмет
204
+ type: string; // Тип (лекция, практика, лаб. работа)
205
+ room: string; // Аудитория
206
+ teacher: Teacher; // Преподаватель { name, position?, degree? }
207
+ weeks: WeekRange; // Диапазон недель { from, to }
208
+ subgroup?: number; // Подгруппа
209
+ weekParity?: "even" | "odd"; // Чётность недели
210
+ }
211
+ ```
212
+
213
+ ## Обработка ошибок
214
+
215
+ ```ts
216
+ import { AuthError, ParseError } from "chuvsu-js";
217
+
218
+ try {
219
+ await tt.login({ email: "...", password: "wrong" });
220
+ } catch (e) {
221
+ if (e instanceof AuthError) {
222
+ console.error("Неверные данные для входа");
223
+ }
224
+ }
225
+ ```
226
+
227
+ ## Лицензия
228
+
229
+ MIT
@@ -0,0 +1,14 @@
1
+ export interface CacheEntry {
2
+ data: unknown;
3
+ timestamp: number;
4
+ }
5
+ export declare class Cache {
6
+ private ttls;
7
+ private store;
8
+ constructor(ttls: Record<string, number | undefined>);
9
+ get(category: string, key: string): unknown | null;
10
+ set(category: string, key: string, data: unknown): void;
11
+ clear(category?: string): void;
12
+ export(): Record<string, CacheEntry>;
13
+ import(data: Record<string, CacheEntry>): void;
14
+ }
@@ -0,0 +1,48 @@
1
+ export class Cache {
2
+ ttls;
3
+ store = new Map();
4
+ constructor(ttls) {
5
+ this.ttls = ttls;
6
+ }
7
+ get(category, key) {
8
+ const ttl = this.ttls[category];
9
+ if (ttl == null)
10
+ return null;
11
+ const entry = this.store.get(`${category}:${key}`);
12
+ if (!entry)
13
+ return null;
14
+ if (ttl !== Infinity && Date.now() - entry.timestamp > ttl) {
15
+ this.store.delete(`${category}:${key}`);
16
+ return null;
17
+ }
18
+ return entry.data;
19
+ }
20
+ set(category, key, data) {
21
+ if (this.ttls[category] == null)
22
+ return;
23
+ this.store.set(`${category}:${key}`, {
24
+ data,
25
+ timestamp: Date.now(),
26
+ });
27
+ }
28
+ clear(category) {
29
+ if (!category) {
30
+ this.store.clear();
31
+ return;
32
+ }
33
+ const prefix = `${category}:`;
34
+ for (const key of this.store.keys()) {
35
+ if (key.startsWith(prefix)) {
36
+ this.store.delete(key);
37
+ }
38
+ }
39
+ }
40
+ export() {
41
+ return Object.fromEntries(this.store);
42
+ }
43
+ import(data) {
44
+ for (const [key, entry] of Object.entries(data)) {
45
+ this.store.set(key, entry);
46
+ }
47
+ }
48
+ }
@@ -0,0 +1,5 @@
1
+ /** GlobalSign GCC R3 DV TLS CA 2020 (intermediate) */
2
+ export declare const GLOBALSIGN_GCC_R3_DV_TLS_CA_2020 = "-----BEGIN CERTIFICATE-----\nMIIEsDCCA5igAwIBAgIQd70OB0LV2enQSdd00CpvmjANBgkqhkiG9w0BAQsFADBM\nMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEGA1UEChMKR2xv\nYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjAeFw0yMDA3MjgwMDAwMDBaFw0y\nOTAzMTgwMDAwMDBaMFMxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWdu\nIG52LXNhMSkwJwYDVQQDEyBHbG9iYWxTaWduIEdDQyBSMyBEViBUTFMgQ0EgMjAy\nMDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKxnlJV/de+OpwyvCXAJ\nIcxPCqkFPh1lttW2oljS3oUqPKq8qX6m7K0OVKaKG3GXi4CJ4fHVUgZYE6HRdjqj\nhhnuHY6EBCBegcUFgPG0scB12Wi8BHm9zKjWxo3Y2bwhO8Fvr8R42pW0eINc6OTb\nQXC0VWFCMVzpcqgz6X49KMZowAMFV6XqtItcG0cMS//9dOJs4oBlpuqX9INxMTGp\n6EASAF9cnlAGy/RXkVS9nOLCCa7pCYV+WgDKLTF+OK2Vxw3RUJ/p8009lQeUARv2\nUCcNNPCifYX1xIspvarkdjzLwzOdLahDdQbJON58zN4V+lMj0msg+c0KnywPIRp3\nBMkCAwEAAaOCAYUwggGBMA4GA1UdDwEB/wQEAwIBhjAdBgNVHSUEFjAUBggrBgEF\nBQcDAQYIKwYBBQUHAwIwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUDZjA\nc3+rvb3ZR0tJrQpKDKw+x3wwHwYDVR0jBBgwFoAUj/BLf6guRSSuTVD6Y5qL3uLd\nG7wwewYIKwYBBQUHAQEEbzBtMC4GCCsGAQUFBzABhiJodHRwOi8vb2NzcDIuZ2xv\nYmFsc2lnbi5jb20vcm9vdHIzMDsGCCsGAQUFBzAChi9odHRwOi8vc2VjdXJlLmds\nb2JhbHNpZ24uY29tL2NhY2VydC9yb290LXIzLmNydDA2BgNVHR8ELzAtMCugKaAn\nhiVodHRwOi8vY3JsLmdsb2JhbHNpZ24uY29tL3Jvb3QtcjMuY3JsMEcGA1UdIARA\nMD4wPAYEVR0gADA0MDIGCCsGAQUFBwIBFiZodHRwczovL3d3dy5nbG9iYWxzaWdu\nLmNvbS9yZXBvc2l0b3J5LzANBgkqhkiG9w0BAQsFAAOCAQEAy8j/c550ea86oCkf\nr2W+ptTCYe6iVzvo7H0V1vUEADJOWelTv07Obf+YkEatdN1Jg09ctgSNv2h+LMTk\nKRZdAXmsE3N5ve+z1Oa9kuiu7284LjeS09zHJQB4DJJJkvtIbjL/ylMK1fbMHhAW\ni0O194TWvH3XWZGXZ6ByxTUIv1+kAIql/Mt29PmKraTT5jrzcVzQ5A9jw16yysuR\nXRrLODlkS1hyBjsfyTNZrmL1h117IFgntBA5SQNVl9ckedq5r4RSAU85jV8XK5UL\nREjRZt2I6M9Po9QL7guFLu4sPFJpwR1sPJvubS2THeo7SxYoNDtdyBHs7euaGcMa\nD/fayQ==\n-----END CERTIFICATE-----";
3
+ /** GlobalSign Root CA - R3 */
4
+ export declare const GLOBALSIGN_ROOT_R3 = "-----BEGIN CERTIFICATE-----\nMIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G\nA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp\nZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4\nMTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG\nA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI\nhvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8\nRgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT\ngHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm\nKPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd\nQQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ\nXriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw\nDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o\nLkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU\nRUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp\njjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK\n6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX\nmcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs\nMx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH\nWD9f\n-----END CERTIFICATE-----";
5
+ export declare const CHUVSU_CA_CERTS: string[];
@@ -0,0 +1,55 @@
1
+ /** GlobalSign GCC R3 DV TLS CA 2020 (intermediate) */
2
+ export const GLOBALSIGN_GCC_R3_DV_TLS_CA_2020 = `-----BEGIN CERTIFICATE-----
3
+ MIIEsDCCA5igAwIBAgIQd70OB0LV2enQSdd00CpvmjANBgkqhkiG9w0BAQsFADBM
4
+ MSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEGA1UEChMKR2xv
5
+ YmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjAeFw0yMDA3MjgwMDAwMDBaFw0y
6
+ OTAzMTgwMDAwMDBaMFMxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWdu
7
+ IG52LXNhMSkwJwYDVQQDEyBHbG9iYWxTaWduIEdDQyBSMyBEViBUTFMgQ0EgMjAy
8
+ MDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKxnlJV/de+OpwyvCXAJ
9
+ IcxPCqkFPh1lttW2oljS3oUqPKq8qX6m7K0OVKaKG3GXi4CJ4fHVUgZYE6HRdjqj
10
+ hhnuHY6EBCBegcUFgPG0scB12Wi8BHm9zKjWxo3Y2bwhO8Fvr8R42pW0eINc6OTb
11
+ QXC0VWFCMVzpcqgz6X49KMZowAMFV6XqtItcG0cMS//9dOJs4oBlpuqX9INxMTGp
12
+ 6EASAF9cnlAGy/RXkVS9nOLCCa7pCYV+WgDKLTF+OK2Vxw3RUJ/p8009lQeUARv2
13
+ UCcNNPCifYX1xIspvarkdjzLwzOdLahDdQbJON58zN4V+lMj0msg+c0KnywPIRp3
14
+ BMkCAwEAAaOCAYUwggGBMA4GA1UdDwEB/wQEAwIBhjAdBgNVHSUEFjAUBggrBgEF
15
+ BQcDAQYIKwYBBQUHAwIwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUDZjA
16
+ c3+rvb3ZR0tJrQpKDKw+x3wwHwYDVR0jBBgwFoAUj/BLf6guRSSuTVD6Y5qL3uLd
17
+ G7wwewYIKwYBBQUHAQEEbzBtMC4GCCsGAQUFBzABhiJodHRwOi8vb2NzcDIuZ2xv
18
+ YmFsc2lnbi5jb20vcm9vdHIzMDsGCCsGAQUFBzAChi9odHRwOi8vc2VjdXJlLmds
19
+ b2JhbHNpZ24uY29tL2NhY2VydC9yb290LXIzLmNydDA2BgNVHR8ELzAtMCugKaAn
20
+ hiVodHRwOi8vY3JsLmdsb2JhbHNpZ24uY29tL3Jvb3QtcjMuY3JsMEcGA1UdIARA
21
+ MD4wPAYEVR0gADA0MDIGCCsGAQUFBwIBFiZodHRwczovL3d3dy5nbG9iYWxzaWdu
22
+ LmNvbS9yZXBvc2l0b3J5LzANBgkqhkiG9w0BAQsFAAOCAQEAy8j/c550ea86oCkf
23
+ r2W+ptTCYe6iVzvo7H0V1vUEADJOWelTv07Obf+YkEatdN1Jg09ctgSNv2h+LMTk
24
+ KRZdAXmsE3N5ve+z1Oa9kuiu7284LjeS09zHJQB4DJJJkvtIbjL/ylMK1fbMHhAW
25
+ i0O194TWvH3XWZGXZ6ByxTUIv1+kAIql/Mt29PmKraTT5jrzcVzQ5A9jw16yysuR
26
+ XRrLODlkS1hyBjsfyTNZrmL1h117IFgntBA5SQNVl9ckedq5r4RSAU85jV8XK5UL
27
+ REjRZt2I6M9Po9QL7guFLu4sPFJpwR1sPJvubS2THeo7SxYoNDtdyBHs7euaGcMa
28
+ D/fayQ==
29
+ -----END CERTIFICATE-----`;
30
+ /** GlobalSign Root CA - R3 */
31
+ export const GLOBALSIGN_ROOT_R3 = `-----BEGIN CERTIFICATE-----
32
+ MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G
33
+ A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp
34
+ Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4
35
+ MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG
36
+ A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI
37
+ hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8
38
+ RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT
39
+ gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm
40
+ KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd
41
+ QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ
42
+ XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw
43
+ DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o
44
+ LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU
45
+ RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp
46
+ jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK
47
+ 6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX
48
+ mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs
49
+ Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH
50
+ WD9f
51
+ -----END CERTIFICATE-----`;
52
+ export const CHUVSU_CA_CERTS = [
53
+ GLOBALSIGN_GCC_R3_DV_TLS_CA_2020,
54
+ GLOBALSIGN_ROOT_R3,
55
+ ];
@@ -0,0 +1,12 @@
1
+ export interface HttpResponse {
2
+ status: number;
3
+ body: string;
4
+ location?: string;
5
+ }
6
+ export declare class HttpClient {
7
+ private cookies;
8
+ private cookieHeader;
9
+ private saveCookies;
10
+ get(url: string, followRedirects?: boolean): Promise<HttpResponse>;
11
+ post(url: string, data: Record<string, string>, followRedirects?: boolean): Promise<HttpResponse>;
12
+ }
@@ -0,0 +1,54 @@
1
+ import { Agent, fetch } from "undici";
2
+ import { CHUVSU_CA_CERTS } from "./certs.js";
3
+ const agent = new Agent({
4
+ connect: { ca: CHUVSU_CA_CERTS },
5
+ });
6
+ export class HttpClient {
7
+ cookies = new Map();
8
+ cookieHeader() {
9
+ return [...this.cookies.entries()]
10
+ .map(([k, v]) => `${k}=${v}`)
11
+ .join("; ");
12
+ }
13
+ saveCookies(headers) {
14
+ const raw = headers.getSetCookie?.() ?? [];
15
+ for (const c of raw) {
16
+ const match = c.match(/^([^=]+)=([^;]*)/);
17
+ if (match)
18
+ this.cookies.set(match[1], match[2]);
19
+ }
20
+ }
21
+ async get(url, followRedirects = true) {
22
+ const res = await fetch(url, {
23
+ method: "GET",
24
+ headers: { Cookie: this.cookieHeader() },
25
+ redirect: followRedirects ? "follow" : "manual",
26
+ dispatcher: agent,
27
+ });
28
+ this.saveCookies(res.headers);
29
+ return {
30
+ status: res.status,
31
+ body: await res.text(),
32
+ location: res.headers.get("location") ?? undefined,
33
+ };
34
+ }
35
+ async post(url, data, followRedirects = true) {
36
+ const body = new URLSearchParams(data).toString();
37
+ const res = await fetch(url, {
38
+ method: "POST",
39
+ headers: {
40
+ "Content-Type": "application/x-www-form-urlencoded",
41
+ Cookie: this.cookieHeader(),
42
+ },
43
+ body,
44
+ redirect: followRedirects ? "follow" : "manual",
45
+ dispatcher: agent,
46
+ });
47
+ this.saveCookies(res.headers);
48
+ return {
49
+ status: res.status,
50
+ body: await res.text(),
51
+ location: res.headers.get("location") ?? undefined,
52
+ };
53
+ }
54
+ }
@@ -0,0 +1,10 @@
1
+ import type { Time, WeekRange, Teacher } from "./types.js";
2
+ export declare function parseHtml(html: string): Document;
3
+ export declare function text(el: Element | null): string;
4
+ /** Parse "HH:MM" into {hours, minutes} */
5
+ export declare function parseTime(s: string): Time;
6
+ /** Parse "2 нед." -> {from:2,to:2}, "6 - 8 нед." -> {from:6,to:8} */
7
+ export declare function parseWeeks(s: string): WeekRange;
8
+ /** Parse <sup>*</sup> / <sup>**</sup> markers: * = odd week, ** = even week */
9
+ export declare function parseWeekParity(html: string): "even" | "odd" | undefined;
10
+ export declare function parseTeacher(s: string): Teacher;
@@ -0,0 +1,44 @@
1
+ import { parseHTML } from "linkedom";
2
+ export function parseHtml(html) {
3
+ return parseHTML(html).document;
4
+ }
5
+ export function text(el) {
6
+ return el?.textContent?.trim() ?? "";
7
+ }
8
+ /** Parse "HH:MM" into {hours, minutes} */
9
+ export function parseTime(s) {
10
+ const [h, m] = s.split(":").map(Number);
11
+ return { hours: h ?? 0, minutes: m ?? 0 };
12
+ }
13
+ /** Parse "2 нед." -> {from:2,to:2}, "6 - 8 нед." -> {from:6,to:8} */
14
+ export function parseWeeks(s) {
15
+ const range = s.match(/(\d+)\s*-\s*(\d+)/);
16
+ if (range)
17
+ return { from: parseInt(range[1]), to: parseInt(range[2]) };
18
+ const single = s.match(/(\d+)/);
19
+ if (single)
20
+ return { from: parseInt(single[1]), to: parseInt(single[1]) };
21
+ return { from: 0, to: 0 };
22
+ }
23
+ /** Parse <sup>*</sup> / <sup>**</sup> markers: * = odd week, ** = even week */
24
+ export function parseWeekParity(html) {
25
+ const match = html.match(/<sup>\s*(\*{1,2})\s*<\/sup>/);
26
+ if (!match)
27
+ return undefined;
28
+ return match[1] === "**" ? "even" : "odd";
29
+ }
30
+ export function parseTeacher(s) {
31
+ const trimmed = s.trim();
32
+ if (!trimmed)
33
+ return { name: "" };
34
+ const posMatch = trimmed.match(/^(доц\.|проф\.|ст\.преп\.|ст\. преп\.|преп\.|асс\.)\s*/);
35
+ const afterPos = posMatch ? trimmed.slice(posMatch[0].length) : trimmed;
36
+ const degMatch = afterPos.match(/^([кд]\.[а-яё.-]+н\.)\s*/);
37
+ const name = degMatch ? afterPos.slice(degMatch[0].length).trim() : afterPos.trim();
38
+ const result = { name };
39
+ if (posMatch)
40
+ result.position = posMatch[1];
41
+ if (degMatch)
42
+ result.degree = degMatch[1];
43
+ return result;
44
+ }
@@ -0,0 +1,29 @@
1
+ export declare const enum Period {
2
+ FallSemester = 1,
3
+ WinterSession = 2,
4
+ SpringSemester = 3,
5
+ SummerSession = 4
6
+ }
7
+ export declare const enum EducationType {
8
+ HigherEducation = 1,
9
+ VocationalEducation = 2
10
+ }
11
+ export interface Time {
12
+ hours: number;
13
+ minutes: number;
14
+ }
15
+ export interface WeekRange {
16
+ from: number;
17
+ to: number;
18
+ }
19
+ export interface Teacher {
20
+ position?: string;
21
+ degree?: string;
22
+ name: string;
23
+ }
24
+ export declare class AuthError extends Error {
25
+ constructor(message: string);
26
+ }
27
+ export declare class ParseError extends Error {
28
+ constructor(message: string);
29
+ }
@@ -0,0 +1,12 @@
1
+ export class AuthError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = "AuthError";
5
+ }
6
+ }
7
+ export class ParseError extends Error {
8
+ constructor(message) {
9
+ super(message);
10
+ this.name = "ParseError";
11
+ }
12
+ }
@@ -0,0 +1,8 @@
1
+ export { LkClient } from "./lk/client.js";
2
+ export { TtClient } from "./tt/client.js";
3
+ export type { CacheEntry } from "./common/cache.js";
4
+ export { getSemesterStart, getSemesterWeeks, getWeekNumber, getWeekdayName, filterSlots, slotsToLessons, } from "./tt/schedule.js";
5
+ export { Period, EducationType, AuthError, ParseError } from "./common/types.js";
6
+ export type { Time, WeekRange, Teacher, } from "./common/types.js";
7
+ export type { PersonalData, } from "./lk/types.js";
8
+ export type { Faculty, Group, ScheduleEntry, FullScheduleSlot, FullScheduleDay, LessonTimeSlot, Lesson, LessonTime, ScheduleWeekDay, ScheduleFilter, SemesterWeek, TtClientOptions, CacheConfig, } from "./tt/types.js";
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { LkClient } from "./lk/client.js";
2
+ export { TtClient } from "./tt/client.js";
3
+ export { getSemesterStart, getSemesterWeeks, getWeekNumber, getWeekdayName, filterSlots, slotsToLessons, } from "./tt/schedule.js";
4
+ export { AuthError, ParseError } from "./common/types.js";
@@ -0,0 +1,10 @@
1
+ import type { PersonalData } from "./types.js";
2
+ export declare class LkClient {
3
+ private http;
4
+ login(opts: {
5
+ email: string;
6
+ password: string;
7
+ }): Promise<void>;
8
+ getPersonalData(): Promise<PersonalData>;
9
+ getGroupId(): Promise<number | null>;
10
+ }
@@ -0,0 +1,39 @@
1
+ import { HttpClient } from "../common/http.js";
2
+ import { AuthError } from "../common/types.js";
3
+ import { extractScriptValues } from "./parse.js";
4
+ const BASE = "https://lk.chuvsu.ru";
5
+ const LOGIN_URL = `${BASE}/info/login.php`;
6
+ const STUDENT_BASE = `${BASE}/student`;
7
+ export class LkClient {
8
+ http = new HttpClient();
9
+ async login(opts) {
10
+ const res = await this.http.post(LOGIN_URL, { email: opts.email, password: opts.password, role: "1", enter: "" }, false);
11
+ if (!(res.status === 302 && res.location?.includes("student"))) {
12
+ throw new AuthError("LK login failed");
13
+ }
14
+ }
15
+ async getPersonalData() {
16
+ const { body } = await this.http.get(`${STUDENT_BASE}/personal_data.php`);
17
+ const vals = extractScriptValues(body, "form_personal_data");
18
+ return {
19
+ lastName: vals.fam ?? "",
20
+ firstName: vals.nam ?? "",
21
+ patronymic: vals.oth ?? "",
22
+ sex: vals.sex ?? "",
23
+ birthday: vals.birthday ?? "",
24
+ recordBookNumber: vals.zachetka ?? "",
25
+ faculty: vals.faculty ?? "",
26
+ specialty: vals.spec ?? "",
27
+ profile: vals.profile ?? "",
28
+ group: vals.groupname ?? "",
29
+ course: vals.course ?? "",
30
+ email: vals.email ?? "",
31
+ phone: vals.phone ?? "",
32
+ };
33
+ }
34
+ async getGroupId() {
35
+ const { body } = await this.http.get(`${STUDENT_BASE}/tt.php`);
36
+ const match = body.match(/tt\.chuvsu\.ru\/index\/grouptt\/gr\/(\d+)/);
37
+ return match ? parseInt(match[1]) : null;
38
+ }
39
+ }
@@ -0,0 +1,2 @@
1
+ /** Extract values set via `document.formName.field.value='...'` in script tags */
2
+ export declare function extractScriptValues(html: string, formName: string): Record<string, string>;
@@ -0,0 +1,10 @@
1
+ /** Extract values set via `document.formName.field.value='...'` in script tags */
2
+ export function extractScriptValues(html, formName) {
3
+ const result = {};
4
+ const re = new RegExp(`document\\.${formName}\\.(\\w+)\\.value\\s*=\\s*'([^']*)'`, "g");
5
+ let m;
6
+ while ((m = re.exec(html))) {
7
+ result[m[1]] = m[2];
8
+ }
9
+ return result;
10
+ }
@@ -0,0 +1,15 @@
1
+ export interface PersonalData {
2
+ lastName: string;
3
+ firstName: string;
4
+ patronymic: string;
5
+ sex: string;
6
+ birthday: string;
7
+ recordBookNumber: string;
8
+ faculty: string;
9
+ specialty: string;
10
+ profile: string;
11
+ group: string;
12
+ course: string;
13
+ email: string;
14
+ phone: string;
15
+ }
@@ -0,0 +1 @@
1
+ export {};