chuvsu-js 4.0.1 → 4.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/dist/shared.d.ts +3 -3
- package/dist/shared.js +2 -2
- package/dist/tt/client.d.ts +11 -1
- package/dist/tt/client.js +51 -2
- package/dist/tt/parse/audience.js +2 -0
- package/dist/tt/parse/full-schedule.js +3 -0
- package/dist/tt/parse/index.d.ts +1 -0
- package/dist/tt/parse/index.js +1 -0
- package/dist/tt/parse/overlays.js +4 -1
- package/dist/tt/parse/teacher.js +3 -0
- package/dist/tt/parse/webinars.d.ts +2 -0
- package/dist/tt/parse/webinars.js +98 -0
- package/dist/tt/schedule.d.ts +1 -0
- package/dist/tt/schedule.js +19 -2
- package/dist/tt/types.d.ts +30 -0
- package/dist/tt/utils/holidays.js +6 -6
- package/dist/tt/utils/index.d.ts +1 -1
- package/dist/tt/utils/index.js +1 -1
- package/dist/tt/utils/lessons.d.ts +3 -1
- package/dist/tt/utils/lessons.js +50 -0
- package/package.json +1 -1
package/dist/shared.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export { Schedule } from "./tt/schedule.js";
|
|
2
|
-
export { parseGroupsString } from "./tt/parse/index.js";
|
|
3
|
-
export { getAdjacentSemester, getCompensatingWorkDays, getCurrentPeriod, getEffectiveHolidays, getHolidayTransfers, getLessonNumber, getSemesterStart, getSemesterWeeks, getTimeSlots, getWeekNumber, getWeekdayName, isHoliday, isSessionPeriod, RUSSIAN_HOLIDAYS, } from "./tt/utils/index.js";
|
|
2
|
+
export { parseGroupsString, parseWebinars } from "./tt/parse/index.js";
|
|
3
|
+
export { attachWebinarsToLessons, getAdjacentSemester, getCompensatingWorkDays, getCurrentPeriod, getEffectiveHolidays, getHolidayTransfers, getLessonNumber, getSemesterStart, getSemesterWeeks, getTimeSlots, getWeekNumber, getWeekdayName, isHoliday, isSessionPeriod, matchWebinarToLesson, RUSSIAN_HOLIDAYS, } from "./tt/utils/index.js";
|
|
4
4
|
export type { Holiday, HolidayTransfer } from "./tt/utils/index.js";
|
|
5
5
|
export { AuthError, EducationType, ParseError, Period, } from "./common/types.js";
|
|
6
6
|
export type { Teacher, Time, WeekRange } from "./common/types.js";
|
|
7
7
|
export type { BlobAdapter, BlobPutOptions, CacheAdapter, CacheEntry, } from "./common/cache.js";
|
|
8
|
-
export type { Audience, AudienceInfo, CacheConfig, Faculty, FullScheduleDay, FullScheduleSlot, Group, Lesson, LessonTime, LessonTimeSlot, ScheduleEntry, SemesterWeek, SubstituteForInfo, Substitution, TeacherInfo, TransferInfo, TtClientOptions, } from "./tt/types.js";
|
|
8
|
+
export type { Audience, AudienceInfo, CacheConfig, Faculty, FullScheduleDay, FullScheduleSlot, Group, Lesson, LessonTime, LessonTimeSlot, ScheduleEntry, SemesterWeek, SubstituteForInfo, Substitution, TeacherInfo, TransferInfo, TtClientOptions, Webinar, } from "./tt/types.js";
|
|
9
9
|
export type { LkCacheConfig, LkClientOptions, PersonalData } from "./lk/types.js";
|
package/dist/shared.js
CHANGED
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
// Both ./index.ts and ./browser.ts re-export everything from here and only
|
|
3
3
|
// add their platform-specific extras on top.
|
|
4
4
|
export { Schedule } from "./tt/schedule.js";
|
|
5
|
-
export { parseGroupsString } from "./tt/parse/index.js";
|
|
6
|
-
export { getAdjacentSemester, getCompensatingWorkDays, getCurrentPeriod, getEffectiveHolidays, getHolidayTransfers, getLessonNumber, getSemesterStart, getSemesterWeeks, getTimeSlots, getWeekNumber, getWeekdayName, isHoliday, isSessionPeriod, RUSSIAN_HOLIDAYS, } from "./tt/utils/index.js";
|
|
5
|
+
export { parseGroupsString, parseWebinars } from "./tt/parse/index.js";
|
|
6
|
+
export { attachWebinarsToLessons, getAdjacentSemester, getCompensatingWorkDays, getCurrentPeriod, getEffectiveHolidays, getHolidayTransfers, getLessonNumber, getSemesterStart, getSemesterWeeks, getTimeSlots, getWeekNumber, getWeekdayName, isHoliday, isSessionPeriod, matchWebinarToLesson, RUSSIAN_HOLIDAYS, } from "./tt/utils/index.js";
|
|
7
7
|
export { AuthError, ParseError, } from "./common/types.js";
|
package/dist/tt/client.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { CacheEntry } from "../common/cache.js";
|
|
2
2
|
import { Period } from "../common/types.js";
|
|
3
3
|
import { Schedule } from "./schedule.js";
|
|
4
|
-
import type { Audience, AudienceInfo, Faculty, Group, TeacherInfo, TtClientOptions, CacheConfig } from "./types.js";
|
|
4
|
+
import type { Audience, AudienceInfo, Faculty, Group, TeacherInfo, TtClientOptions, CacheConfig, Webinar } from "./types.js";
|
|
5
5
|
export declare class TtClient {
|
|
6
6
|
private http;
|
|
7
7
|
private educationType;
|
|
@@ -36,6 +36,16 @@ export declare class TtClient {
|
|
|
36
36
|
groupId: number;
|
|
37
37
|
period: Period;
|
|
38
38
|
}): Promise<Schedule>;
|
|
39
|
+
getWebinars(opts?: {
|
|
40
|
+
date?: Date;
|
|
41
|
+
facultyId?: number;
|
|
42
|
+
}): Promise<Webinar[]>;
|
|
43
|
+
getWebinarJoinUrl(opts: {
|
|
44
|
+
webinarId: string | number;
|
|
45
|
+
idType?: number;
|
|
46
|
+
email: string;
|
|
47
|
+
password: string;
|
|
48
|
+
}): Promise<string>;
|
|
39
49
|
getFaculties(): Promise<Faculty[]>;
|
|
40
50
|
getGroupsForFaculty(opts: {
|
|
41
51
|
facultyId: number;
|
package/dist/tt/client.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { HttpClient } from "../common/http.js";
|
|
2
2
|
import { HybridCache } from "../common/cache.js";
|
|
3
3
|
import { AuthError } from "../common/types.js";
|
|
4
|
-
import { parseAudienceButtons, parseAudienceFullSchedule, parseAudienceInfo, parseAudienceName, parseGroupButtons, parseFacultyButtons, parseTeacherButtons, parseFullSchedule, parseTeacherFullSchedule, parseTeacherInfo, } from "./parse/index.js";
|
|
4
|
+
import { parseAudienceButtons, parseAudienceFullSchedule, parseAudienceInfo, parseAudienceName, parseGroupButtons, parseFacultyButtons, parseTeacherButtons, parseFullSchedule, parseTeacherFullSchedule, parseTeacherInfo, parseWebinars, } from "./parse/index.js";
|
|
5
5
|
import { Schedule } from "./schedule.js";
|
|
6
6
|
const BASE = "https://tt.chuvsu.ru";
|
|
7
7
|
const AUTH_URL = `${BASE}/auth`;
|
|
@@ -23,8 +23,20 @@ function makeUniformCacheConfig(ttl) {
|
|
|
23
23
|
teacherPhotos: ttl,
|
|
24
24
|
audienceInfo: ttl,
|
|
25
25
|
audienceImages: ttl,
|
|
26
|
+
webinars: ttl,
|
|
26
27
|
};
|
|
27
28
|
}
|
|
29
|
+
function formatDate(date) {
|
|
30
|
+
const year = date.getFullYear();
|
|
31
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
32
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
33
|
+
return `${year}-${month}-${day}`;
|
|
34
|
+
}
|
|
35
|
+
function getAcademicYearKey(date = new Date()) {
|
|
36
|
+
const year = date.getFullYear();
|
|
37
|
+
const start = date.getMonth() >= 8 ? year : year - 1;
|
|
38
|
+
return `${start}-${start + 1}`;
|
|
39
|
+
}
|
|
28
40
|
export class TtClient {
|
|
29
41
|
http = new HttpClient();
|
|
30
42
|
educationType;
|
|
@@ -113,7 +125,7 @@ export class TtClient {
|
|
|
113
125
|
}
|
|
114
126
|
// --- Schedule ---
|
|
115
127
|
async fetchSchedule(groupId, period) {
|
|
116
|
-
const cacheKey = `${groupId}:${period}`;
|
|
128
|
+
const cacheKey = `${groupId}:${period}:${getAcademicYearKey()}`;
|
|
117
129
|
const cached = await this.cache?.get("schedule", cacheKey);
|
|
118
130
|
if (cached)
|
|
119
131
|
return cached;
|
|
@@ -147,6 +159,43 @@ export class TtClient {
|
|
|
147
159
|
schedules.set(opts.period, days);
|
|
148
160
|
return new Schedule(opts.groupId, schedules, opts.period, this.educationType);
|
|
149
161
|
}
|
|
162
|
+
async getWebinars(opts) {
|
|
163
|
+
const date = opts?.date ?? new Date();
|
|
164
|
+
const facultyId = opts?.facultyId ?? 0;
|
|
165
|
+
const cacheKey = `${formatDate(date)}:${facultyId}:${this.pertt}`;
|
|
166
|
+
const cached = await this.cache?.get("webinars", cacheKey);
|
|
167
|
+
if (cached)
|
|
168
|
+
return cached;
|
|
169
|
+
const { body } = await this.authPost(`${BASE}/webinar`, {
|
|
170
|
+
seldate: formatDate(date),
|
|
171
|
+
selfac: String(facultyId),
|
|
172
|
+
pertt: this.pertt,
|
|
173
|
+
});
|
|
174
|
+
const data = parseWebinars(body);
|
|
175
|
+
await this.cache?.set("webinars", cacheKey, data);
|
|
176
|
+
return data;
|
|
177
|
+
}
|
|
178
|
+
async getWebinarJoinUrl(opts) {
|
|
179
|
+
const { body } = await this.authPost(`${BASE}/webinar/getjoin`, {
|
|
180
|
+
idw: String(opts.webinarId),
|
|
181
|
+
idwt: String(opts.idType ?? 1),
|
|
182
|
+
name: opts.email,
|
|
183
|
+
pass: opts.password,
|
|
184
|
+
auto: "1",
|
|
185
|
+
});
|
|
186
|
+
let parsed;
|
|
187
|
+
try {
|
|
188
|
+
parsed = JSON.parse(body);
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
throw new AuthError("TT webinar join failed: invalid response");
|
|
192
|
+
}
|
|
193
|
+
const data = parsed;
|
|
194
|
+
if (data.mes !== "SUCCESS" || !data.url?.startsWith("http")) {
|
|
195
|
+
throw new AuthError(data.mes ?? "TT webinar join failed");
|
|
196
|
+
}
|
|
197
|
+
return data.url;
|
|
198
|
+
}
|
|
150
199
|
// --- Search / Discovery ---
|
|
151
200
|
async getFaculties() {
|
|
152
201
|
const cached = await this.cache?.get("faculties", "all");
|
|
@@ -3,6 +3,7 @@ import { parseSemesterScheduleWith } from "./full-schedule.js";
|
|
|
3
3
|
import { parseGroupsString } from "./groups.js";
|
|
4
4
|
import { parseSubstituteForDiv, parseSubstitutionDiv, parseTransferDiv, } from "./overlays.js";
|
|
5
5
|
import { LESSON_TYPE_GLOBAL_RE, LESSON_TYPE_RE, SUBGROUP_ANNOTATION_RE, SUBGROUP_RE, WEEKS_GLOBAL_RE, WEEKS_RE, } from "./patterns.js";
|
|
6
|
+
const DISTANCE_RE = /дистанционно|ДОТ/i;
|
|
6
7
|
export function parseAudienceInfo(html) {
|
|
7
8
|
const doc = parseHtml(html);
|
|
8
9
|
// Name: <span class="htext"><nobr>Аудитория <span style="color: blue;">NAME</span></nobr></span>
|
|
@@ -141,6 +142,7 @@ function parseAudienceSemesterEntry(el) {
|
|
|
141
142
|
groups: parseGroupsString(groupsLine),
|
|
142
143
|
subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
|
|
143
144
|
weekParity,
|
|
145
|
+
isDistance: DISTANCE_RE.test(cleanText),
|
|
144
146
|
substitutions: substitutions.length > 0 ? substitutions : undefined,
|
|
145
147
|
possibleChanges,
|
|
146
148
|
};
|
|
@@ -2,6 +2,7 @@ import { parseHtml, parseTeacher, parseTime, parseWeekParity, parseWeeks, text,
|
|
|
2
2
|
import { getLessonNumber } from "../utils/index.js";
|
|
3
3
|
import { parseSubstitutionDiv, parseTransferDiv, } from "./overlays.js";
|
|
4
4
|
import { FLEXIBLE_LESSON_TYPE_RE_I, LESSON_TYPE_RE, SUBGROUP_RE, WEEKS_RE, } from "./patterns.js";
|
|
5
|
+
const DISTANCE_RE = /дистанционно|ДОТ/i;
|
|
5
6
|
export function parseFullSchedule(html, educationType) {
|
|
6
7
|
const doc = parseHtml(html);
|
|
7
8
|
const edType = educationType ?? 1 /* EducationType.HigherEducation */;
|
|
@@ -110,6 +111,7 @@ function parseSemesterEntry(el) {
|
|
|
110
111
|
groups: [],
|
|
111
112
|
subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
|
|
112
113
|
weekParity,
|
|
114
|
+
isDistance: DISTANCE_RE.test(cleanText) || DISTANCE_RE.test(roomMatch?.[1] ?? ""),
|
|
113
115
|
substitutions: substitutions.length > 0 ? substitutions : undefined,
|
|
114
116
|
possibleChanges,
|
|
115
117
|
};
|
|
@@ -194,6 +196,7 @@ function parseSessionEntry(td) {
|
|
|
194
196
|
teacher: parseTeacher(teacherPart),
|
|
195
197
|
groups: [],
|
|
196
198
|
subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
|
|
199
|
+
isDistance: DISTANCE_RE.test(plainText) || DISTANCE_RE.test(room),
|
|
197
200
|
possibleChanges,
|
|
198
201
|
},
|
|
199
202
|
timeStart: parseTime(timeMatch[1]),
|
package/dist/tt/parse/index.d.ts
CHANGED
|
@@ -3,3 +3,4 @@ export { parseAudienceButtons, parseAudienceName, parseFacultyButtons, parseGrou
|
|
|
3
3
|
export { parseFullSchedule } from "./full-schedule.js";
|
|
4
4
|
export { parseAudienceFullSchedule, parseAudienceInfo, } from "./audience.js";
|
|
5
5
|
export { parseTeacherFullSchedule, parseTeacherInfo, } from "./teacher.js";
|
|
6
|
+
export { parseWebinars } from "./webinars.js";
|
package/dist/tt/parse/index.js
CHANGED
|
@@ -3,3 +3,4 @@ export { parseAudienceButtons, parseAudienceName, parseFacultyButtons, parseGrou
|
|
|
3
3
|
export { parseFullSchedule } from "./full-schedule.js";
|
|
4
4
|
export { parseAudienceFullSchedule, parseAudienceInfo, } from "./audience.js";
|
|
5
5
|
export { parseTeacherFullSchedule, parseTeacherInfo, } from "./teacher.js";
|
|
6
|
+
export { parseWebinars } from "./webinars.js";
|
|
@@ -2,6 +2,7 @@ import { parseTeacher, text } from "../../common/parse.js";
|
|
|
2
2
|
import { parseGroupsString } from "./groups.js";
|
|
3
3
|
import { LESSON_TYPE_PATTERN, LESSON_TYPE_RE, LESSON_TYPE_RE_I, SUBGROUP_RE, } from "./patterns.js";
|
|
4
4
|
const GROUP_CODE_RE = /[A-ZА-ЯЁ]{1,}(?:-[A-ZА-ЯЁa-zа-яё0-9]+)+/u;
|
|
5
|
+
const DISTANCE_RE = /дистанционно|ДОТ/i;
|
|
5
6
|
export function parseDate(dd, mm, yyyy) {
|
|
6
7
|
return new Date(parseInt(yyyy), parseInt(mm) - 1, parseInt(dd));
|
|
7
8
|
}
|
|
@@ -53,6 +54,7 @@ export function parseTransferDiv(div) {
|
|
|
53
54
|
teacher: parseTeacher(teacherPart),
|
|
54
55
|
groups: parseGroupsString(groupsPart),
|
|
55
56
|
subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
|
|
57
|
+
isDistance: DISTANCE_RE.test(divText) || DISTANCE_RE.test(roomMatch?.[1] ?? ""),
|
|
56
58
|
transfer,
|
|
57
59
|
},
|
|
58
60
|
};
|
|
@@ -72,7 +74,7 @@ export function parseSubstitutionDiv(div) {
|
|
|
72
74
|
const teacherMatch = divHtml.match(/Преподаватель:\s*<span[^>]*>([^<]+)<\/span>/);
|
|
73
75
|
if (teacherMatch)
|
|
74
76
|
teacher = parseTeacher(teacherMatch[1].trim());
|
|
75
|
-
return { date, room, teacher };
|
|
77
|
+
return { date, room, teacher, isDistance: DISTANCE_RE.test(room ?? divText) };
|
|
76
78
|
}
|
|
77
79
|
export function parseSubstituteForDiv(div) {
|
|
78
80
|
const divText = text(div);
|
|
@@ -111,6 +113,7 @@ export function parseSubstituteForDiv(div) {
|
|
|
111
113
|
teacher: { name: "" },
|
|
112
114
|
groups: parseGroupsString(groupsMatch?.[1]),
|
|
113
115
|
subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
|
|
116
|
+
isDistance: DISTANCE_RE.test(divText) || DISTANCE_RE.test(roomMatch?.[1] ?? ""),
|
|
114
117
|
substituteFor: { date, originalTeacher },
|
|
115
118
|
},
|
|
116
119
|
};
|
package/dist/tt/parse/teacher.js
CHANGED
|
@@ -4,6 +4,7 @@ import { parseSemesterScheduleWith } from "./full-schedule.js";
|
|
|
4
4
|
import { parseGroupsString } from "./groups.js";
|
|
5
5
|
import { parseSubstituteForDiv, parseSubstitutionDiv, parseTransferDiv, } from "./overlays.js";
|
|
6
6
|
import { FLEXIBLE_LESSON_TYPE_PATTERN, FLEXIBLE_LESSON_TYPE_RE_I, LESSON_TYPE_RE, SUBGROUP_RE, WEEKS_RE, } from "./patterns.js";
|
|
7
|
+
const DISTANCE_RE = /дистанционно|ДОТ/i;
|
|
7
8
|
export function parseTeacherFullSchedule(html, educationType) {
|
|
8
9
|
const doc = parseHtml(html);
|
|
9
10
|
const edType = educationType ?? 1 /* EducationType.HigherEducation */;
|
|
@@ -68,6 +69,7 @@ function parseTeacherSemesterEntry(el) {
|
|
|
68
69
|
groups: parseGroupsString(groupsMatch?.[1]),
|
|
69
70
|
subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
|
|
70
71
|
weekParity,
|
|
72
|
+
isDistance: DISTANCE_RE.test(cleanText) || DISTANCE_RE.test(roomMatch?.[1] ?? ""),
|
|
71
73
|
substitutions: substitutions.length > 0 ? substitutions : undefined,
|
|
72
74
|
possibleChanges,
|
|
73
75
|
};
|
|
@@ -140,6 +142,7 @@ function parseTeacherSessionEntry(td) {
|
|
|
140
142
|
teacher: { name: "" },
|
|
141
143
|
groups: parseGroupsString(groupsMatch?.[1]),
|
|
142
144
|
subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
|
|
145
|
+
isDistance: DISTANCE_RE.test(plainText) || DISTANCE_RE.test(room),
|
|
143
146
|
possibleChanges,
|
|
144
147
|
},
|
|
145
148
|
timeStart: parseTime(timeMatch[1]),
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { parseHtml, parseTeacher, parseTime, text, } from "../../common/parse.js";
|
|
2
|
+
import { parseGroupsString } from "./groups.js";
|
|
3
|
+
import { FLEXIBLE_LESSON_TYPE_RE_I, SUBGROUP_RE } from "./patterns.js";
|
|
4
|
+
const GROUP_CODE_RE = /[A-ZА-ЯЁ]{1,}(?:-[A-ZА-ЯЁa-zа-яё0-9]+)+(?:\s*ин)?/u;
|
|
5
|
+
function parseDateValue(value) {
|
|
6
|
+
const match = value?.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
7
|
+
if (!match)
|
|
8
|
+
return undefined;
|
|
9
|
+
return new Date(parseInt(match[1]), parseInt(match[2]) - 1, parseInt(match[3]));
|
|
10
|
+
}
|
|
11
|
+
function parseTimeRange(raw) {
|
|
12
|
+
const match = raw.match(/(\d{2}:\d{2})\s*-\s*(\d{2}:\d{2})/);
|
|
13
|
+
if (!match)
|
|
14
|
+
return null;
|
|
15
|
+
return { start: parseTime(match[1]), end: parseTime(match[2]) };
|
|
16
|
+
}
|
|
17
|
+
function splitLesson(raw) {
|
|
18
|
+
const typeMatches = [...raw.matchAll(new RegExp(FLEXIBLE_LESSON_TYPE_RE_I, "gi"))];
|
|
19
|
+
const typeMatch = typeMatches.at(-1);
|
|
20
|
+
if (!typeMatch || typeMatch.index == null) {
|
|
21
|
+
return {
|
|
22
|
+
subject: raw,
|
|
23
|
+
type: "",
|
|
24
|
+
teacherRaw: "",
|
|
25
|
+
groupsRaw: "",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const subject = raw.slice(0, typeMatch.index).trim();
|
|
29
|
+
const type = typeMatch[1].replace(/\.$/, "").toLowerCase();
|
|
30
|
+
const rest = raw.slice(typeMatch.index + typeMatch[0].length).trim();
|
|
31
|
+
const groupMatch = rest.match(GROUP_CODE_RE);
|
|
32
|
+
const teacherRaw = groupMatch?.index == null ? rest : rest.slice(0, groupMatch.index).trim();
|
|
33
|
+
const groupsRaw = groupMatch?.index == null ? "" : rest.slice(groupMatch.index).trim();
|
|
34
|
+
const subgroupMatch = raw.match(SUBGROUP_RE);
|
|
35
|
+
return {
|
|
36
|
+
subject,
|
|
37
|
+
type,
|
|
38
|
+
teacherRaw,
|
|
39
|
+
groupsRaw,
|
|
40
|
+
subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function parseWebinarRows(doc, tableSelector, scheduled, fallbackDate) {
|
|
44
|
+
const webinars = [];
|
|
45
|
+
for (const slotRow of doc.querySelectorAll(`${tableSelector} > tbody > tr`)) {
|
|
46
|
+
const slotCells = [...slotRow.children].filter((child) => child.tagName.toLowerCase() === "td");
|
|
47
|
+
const timeCell = slotCells[0];
|
|
48
|
+
const dataCell = slotCells[1];
|
|
49
|
+
if (!timeCell || !dataCell)
|
|
50
|
+
continue;
|
|
51
|
+
const timeText = text(timeCell);
|
|
52
|
+
const timeRange = parseTimeRange(timeText);
|
|
53
|
+
if (!timeRange)
|
|
54
|
+
continue;
|
|
55
|
+
const id = timeCell.querySelector("div")?.getAttribute("id") ?? "";
|
|
56
|
+
const idMatch = id.match(/trd(\d{4}-\d{2}-\d{2})t(\d+)/);
|
|
57
|
+
const date = parseDateValue(idMatch?.[1]) ?? fallbackDate;
|
|
58
|
+
const slotNumber = idMatch ? parseInt(idMatch[2]) : undefined;
|
|
59
|
+
for (const row of dataCell.querySelectorAll("table tr")) {
|
|
60
|
+
const cells = [...row.children].filter((child) => child.tagName.toLowerCase() === "td");
|
|
61
|
+
const lessonCell = cells[0];
|
|
62
|
+
const titleCell = cells[1];
|
|
63
|
+
if (!lessonCell || !titleCell)
|
|
64
|
+
continue;
|
|
65
|
+
const button = row.querySelector("button[onclick*=\"jointo\"]");
|
|
66
|
+
const onclick = button?.getAttribute("onclick") ?? "";
|
|
67
|
+
const joinMatch = onclick.match(/jointo(?:sub)?\('([^']+)',\s*(\d+)\)/);
|
|
68
|
+
const raw = text(lessonCell);
|
|
69
|
+
const parsed = splitLesson(raw);
|
|
70
|
+
webinars.push({
|
|
71
|
+
id: joinMatch?.[1] ?? "",
|
|
72
|
+
idType: joinMatch ? parseInt(joinMatch[2]) : 1,
|
|
73
|
+
scheduled,
|
|
74
|
+
date,
|
|
75
|
+
slotNumber,
|
|
76
|
+
timeStart: timeRange.start,
|
|
77
|
+
timeEnd: timeRange.end,
|
|
78
|
+
subject: parsed.subject,
|
|
79
|
+
type: parsed.type,
|
|
80
|
+
teacher: parseTeacher(parsed.teacherRaw),
|
|
81
|
+
groups: parseGroupsString(parsed.groupsRaw),
|
|
82
|
+
subgroup: parsed.subgroup,
|
|
83
|
+
title: text(titleCell),
|
|
84
|
+
raw,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return webinars;
|
|
89
|
+
}
|
|
90
|
+
export function parseWebinars(html) {
|
|
91
|
+
const doc = parseHtml(html);
|
|
92
|
+
const selectedDate = doc.querySelector('select[name="seldate"] option[selected]')?.getAttribute("value");
|
|
93
|
+
const fallbackDate = parseDateValue(selectedDate);
|
|
94
|
+
return [
|
|
95
|
+
...parseWebinarRows(doc, "#webstt", true, fallbackDate),
|
|
96
|
+
...parseWebinarRows(doc, "#websttext", false, fallbackDate),
|
|
97
|
+
];
|
|
98
|
+
}
|
package/dist/tt/schedule.d.ts
CHANGED
package/dist/tt/schedule.js
CHANGED
|
@@ -60,6 +60,15 @@ export class Schedule {
|
|
|
60
60
|
date.setHours(0, 0, 0, 0);
|
|
61
61
|
return date;
|
|
62
62
|
}
|
|
63
|
+
isInCurrentAcademicYear(date) {
|
|
64
|
+
const now = new Date();
|
|
65
|
+
const startYear = now.getMonth() >= 8
|
|
66
|
+
? now.getFullYear()
|
|
67
|
+
: now.getFullYear() - 1;
|
|
68
|
+
const start = new Date(startYear, 8, 1);
|
|
69
|
+
const end = new Date(startYear + 1, 7, 31, 23, 59, 59, 999);
|
|
70
|
+
return date >= start && date <= end;
|
|
71
|
+
}
|
|
63
72
|
// --- Public query methods ---
|
|
64
73
|
forDay(weekday, opts) {
|
|
65
74
|
const period = this.period;
|
|
@@ -70,7 +79,10 @@ export class Schedule {
|
|
|
70
79
|
const dayName = getWeekdayName(weekday);
|
|
71
80
|
for (const d of days) {
|
|
72
81
|
if (d.weekday.toLowerCase() === dayName.toLowerCase() && d.date) {
|
|
73
|
-
|
|
82
|
+
const slots = filterSlots(d.slots, { subgroup: opts?.subgroup });
|
|
83
|
+
lessons.push(...slotsToLessons(slots, d.date, {
|
|
84
|
+
isTeacherSchedule: this.isTeacherSchedule,
|
|
85
|
+
}));
|
|
74
86
|
}
|
|
75
87
|
}
|
|
76
88
|
return lessons.sort(sortLessons);
|
|
@@ -80,6 +92,8 @@ export class Schedule {
|
|
|
80
92
|
return slotsToLessons(slots, date, { isTeacherSchedule: this.isTeacherSchedule });
|
|
81
93
|
}
|
|
82
94
|
forDate(date, opts) {
|
|
95
|
+
if (!this.isInCurrentAcademicYear(date))
|
|
96
|
+
return [];
|
|
83
97
|
if (isHoliday(date, this.holidays, this.holidayTransfers))
|
|
84
98
|
return [];
|
|
85
99
|
const period = getCurrentPeriod({ date });
|
|
@@ -88,7 +102,10 @@ export class Schedule {
|
|
|
88
102
|
for (const [, d] of this.scheduleMap) {
|
|
89
103
|
for (const day of d) {
|
|
90
104
|
if (day.date && isSameDay(day.date, date)) {
|
|
91
|
-
|
|
105
|
+
const slots = filterSlots(day.slots, { subgroup: opts?.subgroup });
|
|
106
|
+
lessons.push(...slotsToLessons(slots, date, {
|
|
107
|
+
isTeacherSchedule: this.isTeacherSchedule,
|
|
108
|
+
}));
|
|
92
109
|
}
|
|
93
110
|
}
|
|
94
111
|
}
|
package/dist/tt/types.d.ts
CHANGED
|
@@ -42,6 +42,8 @@ export interface Substitution {
|
|
|
42
42
|
date: Date;
|
|
43
43
|
/** New room, if changed. */
|
|
44
44
|
room?: string;
|
|
45
|
+
/** Whether the substitution moves the lesson online. */
|
|
46
|
+
isDistance?: boolean;
|
|
45
47
|
/** New teacher, if changed. */
|
|
46
48
|
teacher?: Teacher;
|
|
47
49
|
}
|
|
@@ -78,6 +80,8 @@ export interface ScheduleEntry {
|
|
|
78
80
|
groups: string[];
|
|
79
81
|
subgroup?: number;
|
|
80
82
|
weekParity?: "even" | "odd";
|
|
83
|
+
/** True when the lesson is explicitly marked as дистанционно / ДОТ. */
|
|
84
|
+
isDistance?: boolean;
|
|
81
85
|
/** Date-specific substitutions (замена на). */
|
|
82
86
|
substitutions?: Substitution[];
|
|
83
87
|
/** If this entry is a transferred lesson (перенос). */
|
|
@@ -121,6 +125,10 @@ export interface Lesson {
|
|
|
121
125
|
weeks: WeekRange;
|
|
122
126
|
subgroup?: number;
|
|
123
127
|
weekParity?: "even" | "odd";
|
|
128
|
+
/** True when the lesson is explicitly marked as дистанционно / ДОТ. */
|
|
129
|
+
isDistance?: boolean;
|
|
130
|
+
/** Matched webinar metadata, if attached by `attachWebinarsToLessons`. */
|
|
131
|
+
webinar?: Webinar;
|
|
124
132
|
/** If a substitution was applied, the original room. */
|
|
125
133
|
originalRoom?: string;
|
|
126
134
|
/** If a substitution was applied, the original teacher. */
|
|
@@ -145,6 +153,27 @@ export interface SemesterWeek {
|
|
|
145
153
|
start: Date;
|
|
146
154
|
end: Date;
|
|
147
155
|
}
|
|
156
|
+
export interface Webinar {
|
|
157
|
+
/** Internal tt.chuvsu.ru webinar id used by `/webinar/getjoin`. */
|
|
158
|
+
id: string;
|
|
159
|
+
/** Webinar table type argument (`idwt`) used by `/webinar/getjoin`. */
|
|
160
|
+
idType: number;
|
|
161
|
+
/** True for "Вебинары по расписанию"; false for external webinars. */
|
|
162
|
+
scheduled: boolean;
|
|
163
|
+
date?: Date;
|
|
164
|
+
slotNumber?: number;
|
|
165
|
+
timeStart: Time;
|
|
166
|
+
timeEnd: Time;
|
|
167
|
+
subject: string;
|
|
168
|
+
type: string;
|
|
169
|
+
teacher: Teacher;
|
|
170
|
+
groups: string[];
|
|
171
|
+
subgroup?: number;
|
|
172
|
+
/** Free-form title/topic from the second table column. */
|
|
173
|
+
title: string;
|
|
174
|
+
/** Raw first-column text as rendered by tt.chuvsu.ru. */
|
|
175
|
+
raw: string;
|
|
176
|
+
}
|
|
148
177
|
export interface CacheConfig {
|
|
149
178
|
schedule?: number;
|
|
150
179
|
faculties?: number;
|
|
@@ -156,6 +185,7 @@ export interface CacheConfig {
|
|
|
156
185
|
teacherPhotos?: number;
|
|
157
186
|
audienceInfo?: number;
|
|
158
187
|
audienceImages?: number;
|
|
188
|
+
webinars?: number;
|
|
159
189
|
}
|
|
160
190
|
export interface TtClientOptions {
|
|
161
191
|
educationType?: EducationType;
|
|
@@ -92,18 +92,18 @@ export function getEffectiveHolidays(year, holidays = RUSSIAN_HOLIDAYS, transfer
|
|
|
92
92
|
const nonJanuaryOnWeekend = holidays
|
|
93
93
|
.filter((h) => !isJanuaryHoliday(h))
|
|
94
94
|
.map((h) => new Date(year, h.month - 1, h.day))
|
|
95
|
-
.filter((d) => d.getDay() === 0 || d.getDay() === 6)
|
|
95
|
+
.filter((d) => d.getDay() === 0 || (!sixDayWeek && d.getDay() === 6))
|
|
96
96
|
.sort((a, b) => a.getTime() - b.getTime());
|
|
97
97
|
for (const holiday of nonJanuaryOnWeekend) {
|
|
98
98
|
let candidate = new Date(holiday);
|
|
99
|
-
if (candidate.getDay() === 6) {
|
|
99
|
+
if (!sixDayWeek && candidate.getDay() === 6) {
|
|
100
100
|
candidate.setDate(candidate.getDate() + 2);
|
|
101
101
|
}
|
|
102
102
|
else {
|
|
103
103
|
candidate.setDate(candidate.getDate() + 1);
|
|
104
104
|
}
|
|
105
105
|
while (candidate.getDay() === 0 ||
|
|
106
|
-
candidate.getDay() === 6 ||
|
|
106
|
+
(!sixDayWeek && candidate.getDay() === 6) ||
|
|
107
107
|
isOriginalHoliday(candidate) ||
|
|
108
108
|
effectiveDays.some((ed) => isSameDay(ed, candidate))) {
|
|
109
109
|
candidate.setDate(candidate.getDate() + 1);
|
|
@@ -141,19 +141,19 @@ export function getHolidayTransfers(year, holidays = RUSSIAN_HOLIDAYS, transfers
|
|
|
141
141
|
const nonJanuaryOnWeekend = holidays
|
|
142
142
|
.filter((h) => !isJanuaryHoliday(h))
|
|
143
143
|
.map((h) => new Date(year, h.month - 1, h.day))
|
|
144
|
-
.filter((d) => d.getDay() === 0 || d.getDay() === 6)
|
|
144
|
+
.filter((d) => d.getDay() === 0 || (!sixDayWeek && d.getDay() === 6))
|
|
145
145
|
.sort((a, b) => a.getTime() - b.getTime());
|
|
146
146
|
const isOriginalHoliday = (d) => originalDates.some((od) => isSameDay(od, d));
|
|
147
147
|
for (const holiday of nonJanuaryOnWeekend) {
|
|
148
148
|
let candidate = new Date(holiday);
|
|
149
|
-
if (candidate.getDay() === 6) {
|
|
149
|
+
if (!sixDayWeek && candidate.getDay() === 6) {
|
|
150
150
|
candidate.setDate(candidate.getDate() + 2);
|
|
151
151
|
}
|
|
152
152
|
else {
|
|
153
153
|
candidate.setDate(candidate.getDate() + 1);
|
|
154
154
|
}
|
|
155
155
|
while (candidate.getDay() === 0 ||
|
|
156
|
-
candidate.getDay() === 6 ||
|
|
156
|
+
(!sixDayWeek && candidate.getDay() === 6) ||
|
|
157
157
|
isOriginalHoliday(candidate) ||
|
|
158
158
|
effectiveDays.some((ed) => isSameDay(ed, candidate))) {
|
|
159
159
|
candidate.setDate(candidate.getDate() + 1);
|
package/dist/tt/utils/index.d.ts
CHANGED
|
@@ -2,6 +2,6 @@ export { getMonday, getWeekdayName, isSameDay } from "./date.js";
|
|
|
2
2
|
export { getAdjacentSemester, getCurrentPeriod, isSessionPeriod, } from "./period.js";
|
|
3
3
|
export { getSemesterStart, getSemesterWeeks, getWeekNumber, } from "./semester.js";
|
|
4
4
|
export { getLessonNumber, getTimeSlots } from "./time-slots.js";
|
|
5
|
-
export { collectTransfers, filterSlots, slotsToLessons, sortLessons, suppressTransferredLessons, } from "./lessons.js";
|
|
5
|
+
export { attachWebinarsToLessons, collectTransfers, filterSlots, matchWebinarToLesson, slotsToLessons, sortLessons, suppressTransferredLessons, } from "./lessons.js";
|
|
6
6
|
export { getCompensatingWorkDays, getEffectiveHolidays, getHolidayTransfers, isHoliday, RUSSIAN_HOLIDAYS, } from "./holidays.js";
|
|
7
7
|
export type { Holiday, HolidayTransfer } from "./holidays.js";
|
package/dist/tt/utils/index.js
CHANGED
|
@@ -2,5 +2,5 @@ export { getMonday, getWeekdayName, isSameDay } from "./date.js";
|
|
|
2
2
|
export { getAdjacentSemester, getCurrentPeriod, isSessionPeriod, } from "./period.js";
|
|
3
3
|
export { getSemesterStart, getSemesterWeeks, getWeekNumber, } from "./semester.js";
|
|
4
4
|
export { getLessonNumber, getTimeSlots } from "./time-slots.js";
|
|
5
|
-
export { collectTransfers, filterSlots, slotsToLessons, sortLessons, suppressTransferredLessons, } from "./lessons.js";
|
|
5
|
+
export { attachWebinarsToLessons, collectTransfers, filterSlots, matchWebinarToLesson, slotsToLessons, sortLessons, suppressTransferredLessons, } from "./lessons.js";
|
|
6
6
|
export { getCompensatingWorkDays, getEffectiveHolidays, getHolidayTransfers, isHoliday, RUSSIAN_HOLIDAYS, } from "./holidays.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FullScheduleDay, FullScheduleSlot, Lesson, ScheduleEntry } from "../types.js";
|
|
1
|
+
import type { FullScheduleDay, FullScheduleSlot, Lesson, ScheduleEntry, Webinar } from "../types.js";
|
|
2
2
|
export declare function sortLessons(a: Lesson, b: Lesson): number;
|
|
3
3
|
export declare function filterSlots(slots: FullScheduleSlot[], opts?: {
|
|
4
4
|
subgroup?: number;
|
|
@@ -8,6 +8,8 @@ export declare function filterSlots(slots: FullScheduleSlot[], opts?: {
|
|
|
8
8
|
export declare function slotsToLessons(slots: FullScheduleSlot[], date: Date, opts?: {
|
|
9
9
|
isTeacherSchedule?: boolean;
|
|
10
10
|
}): Lesson[];
|
|
11
|
+
export declare function matchWebinarToLesson(lesson: Lesson, webinars: Webinar[]): Webinar | undefined;
|
|
12
|
+
export declare function attachWebinarsToLessons(lessons: Lesson[], webinars: Webinar[]): Lesson[];
|
|
11
13
|
/** All transfer entries from the given schedule days. */
|
|
12
14
|
export declare function collectTransfers(days: FullScheduleDay[]): ScheduleEntry[];
|
|
13
15
|
/** Remove lessons whose source date/slot match a transfer. */
|
package/dist/tt/utils/lessons.js
CHANGED
|
@@ -51,12 +51,16 @@ function makeLessonTime(date, time) {
|
|
|
51
51
|
d.setHours(time.hours, time.minutes, 0, 0);
|
|
52
52
|
return { date: d, hours: time.hours, minutes: time.minutes };
|
|
53
53
|
}
|
|
54
|
+
function isDistanceRoom(room) {
|
|
55
|
+
return /дистанционно|ДОТ/i.test(room ?? "");
|
|
56
|
+
}
|
|
54
57
|
export function slotsToLessons(slots, date, opts) {
|
|
55
58
|
const lessons = [];
|
|
56
59
|
for (const slot of slots) {
|
|
57
60
|
for (const entry of slot.entries) {
|
|
58
61
|
let room = entry.room;
|
|
59
62
|
let teacher = entry.teacher;
|
|
63
|
+
let isDistance = entry.isDistance || isDistanceRoom(room);
|
|
60
64
|
let originalRoom;
|
|
61
65
|
let originalTeacher;
|
|
62
66
|
// Apply date-specific substitutions
|
|
@@ -66,6 +70,7 @@ export function slotsToLessons(slots, date, opts) {
|
|
|
66
70
|
if (sub.room) {
|
|
67
71
|
originalRoom = room;
|
|
68
72
|
room = sub.room;
|
|
73
|
+
isDistance = sub.isDistance || isDistanceRoom(room);
|
|
69
74
|
}
|
|
70
75
|
if (sub.teacher) {
|
|
71
76
|
// On teacher schedules, a teacher substitution means another teacher
|
|
@@ -89,6 +94,7 @@ export function slotsToLessons(slots, date, opts) {
|
|
|
89
94
|
weeks: entry.weeks,
|
|
90
95
|
subgroup: entry.subgroup,
|
|
91
96
|
weekParity: entry.weekParity,
|
|
97
|
+
isDistance,
|
|
92
98
|
originalRoom,
|
|
93
99
|
originalTeacher,
|
|
94
100
|
transfer: entry.transfer,
|
|
@@ -99,6 +105,50 @@ export function slotsToLessons(slots, date, opts) {
|
|
|
99
105
|
}
|
|
100
106
|
return lessons;
|
|
101
107
|
}
|
|
108
|
+
function normalizeText(value) {
|
|
109
|
+
return value
|
|
110
|
+
.toLowerCase()
|
|
111
|
+
.replace(/ё/g, "е")
|
|
112
|
+
.replace(/\s+/g, " ")
|
|
113
|
+
.trim();
|
|
114
|
+
}
|
|
115
|
+
function sameTime(lesson, webinar) {
|
|
116
|
+
return (lesson.start.hours === webinar.timeStart.hours &&
|
|
117
|
+
lesson.start.minutes === webinar.timeStart.minutes &&
|
|
118
|
+
lesson.end.hours === webinar.timeEnd.hours &&
|
|
119
|
+
lesson.end.minutes === webinar.timeEnd.minutes);
|
|
120
|
+
}
|
|
121
|
+
export function matchWebinarToLesson(lesson, webinars) {
|
|
122
|
+
return webinars.find((webinar) => {
|
|
123
|
+
if (!webinar.scheduled)
|
|
124
|
+
return false;
|
|
125
|
+
if (webinar.date && !isSameDay(webinar.date, lesson.start.date))
|
|
126
|
+
return false;
|
|
127
|
+
if (webinar.slotNumber != null && webinar.slotNumber !== lesson.number) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
if (!sameTime(lesson, webinar))
|
|
131
|
+
return false;
|
|
132
|
+
if (normalizeText(webinar.subject) !== normalizeText(lesson.subject)) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
if (webinar.type && lesson.type && webinar.type !== lesson.type) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
if (webinar.subgroup != null &&
|
|
139
|
+
lesson.subgroup != null &&
|
|
140
|
+
webinar.subgroup !== lesson.subgroup) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
return true;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
export function attachWebinarsToLessons(lessons, webinars) {
|
|
147
|
+
return lessons.map((lesson) => {
|
|
148
|
+
const webinar = matchWebinarToLesson(lesson, webinars);
|
|
149
|
+
return webinar ? { ...lesson, webinar } : lesson;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
102
152
|
/** All transfer entries from the given schedule days. */
|
|
103
153
|
export function collectTransfers(days) {
|
|
104
154
|
const transfers = [];
|