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 +21 -0
- package/README.md +229 -0
- package/dist/common/cache.d.ts +14 -0
- package/dist/common/cache.js +48 -0
- package/dist/common/certs.d.ts +5 -0
- package/dist/common/certs.js +55 -0
- package/dist/common/http.d.ts +12 -0
- package/dist/common/http.js +54 -0
- package/dist/common/parse.d.ts +10 -0
- package/dist/common/parse.js +44 -0
- package/dist/common/types.d.ts +29 -0
- package/dist/common/types.js +12 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +4 -0
- package/dist/lk/client.d.ts +10 -0
- package/dist/lk/client.js +39 -0
- package/dist/lk/parse.d.ts +2 -0
- package/dist/lk/parse.js +10 -0
- package/dist/lk/types.d.ts +15 -0
- package/dist/lk/types.js +1 -0
- package/dist/tt/client.d.ts +62 -0
- package/dist/tt/client.js +235 -0
- package/dist/tt/parse.d.ts +10 -0
- package/dist/tt/parse.js +127 -0
- package/dist/tt/schedule.d.ts +32 -0
- package/dist/tt/schedule.js +124 -0
- package/dist/tt/types.d.ts +75 -0
- package/dist/tt/types.js +1 -0
- package/package.json +43 -0
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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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,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
|
+
}
|
package/dist/lk/parse.js
ADDED
|
@@ -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
|
+
}
|
package/dist/lk/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|