@robertying/thu-learn-lib 5.0.0 → 6.0.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/README.md +94 -227
- package/lib/auth.d.ts +15 -4
- package/lib/auth.d.ts.map +1 -1
- package/lib/auth.js +65 -55
- package/lib/auth.js.map +1 -1
- package/lib/calendar.d.ts +12 -5
- package/lib/calendar.d.ts.map +1 -1
- package/lib/calendar.js +118 -20
- package/lib/calendar.js.map +1 -1
- package/lib/contents.d.ts.map +1 -1
- package/lib/contents.js +10 -6
- package/lib/contents.js.map +1 -1
- package/lib/courses.d.ts.map +1 -1
- package/lib/courses.js +15 -29
- package/lib/courses.js.map +1 -1
- package/lib/discussions.d.ts.map +1 -1
- package/lib/discussions.js +8 -19
- package/lib/discussions.js.map +1 -1
- package/lib/favorites.d.ts +1 -0
- package/lib/favorites.d.ts.map +1 -1
- package/lib/favorites.js +25 -60
- package/lib/favorites.js.map +1 -1
- package/lib/files.d.ts +4 -4
- package/lib/files.d.ts.map +1 -1
- package/lib/files.js +112 -134
- package/lib/files.js.map +1 -1
- package/lib/homework.d.ts +35 -5
- package/lib/homework.d.ts.map +1 -1
- package/lib/homework.js +150 -92
- package/lib/homework.js.map +1 -1
- package/lib/index.d.ts +45 -20
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +52 -17
- package/lib/index.js.map +1 -1
- package/lib/notices.d.ts +4 -4
- package/lib/notices.d.ts.map +1 -1
- package/lib/notices.js +13 -33
- package/lib/notices.js.map +1 -1
- package/lib/questionnaires.d.ts +6 -3
- package/lib/questionnaires.d.ts.map +1 -1
- package/lib/questionnaires.js +26 -24
- package/lib/questionnaires.js.map +1 -1
- package/lib/request.d.ts +64 -20
- package/lib/request.d.ts.map +1 -1
- package/lib/request.js +174 -64
- package/lib/request.js.map +1 -1
- package/lib/timetable.d.ts +26 -0
- package/lib/timetable.d.ts.map +1 -0
- package/lib/timetable.js +110 -0
- package/lib/timetable.js.map +1 -0
- package/lib/types.d.ts +117 -76
- package/lib/types.d.ts.map +1 -1
- package/lib/types.js +3 -0
- package/lib/types.js.map +1 -1
- package/lib/urls.d.ts +10 -20
- package/lib/urls.d.ts.map +1 -1
- package/lib/urls.js +11 -56
- package/lib/urls.js.map +1 -1
- package/lib/user.d.ts +7 -2
- package/lib/user.d.ts.map +1 -1
- package/lib/user.js +20 -6
- package/lib/user.js.map +1 -1
- package/lib/utils.d.ts +42 -4
- package/lib/utils.d.ts.map +1 -1
- package/lib/utils.js +80 -29
- package/lib/utils.js.map +1 -1
- package/package.json +9 -8
- package/src/auth.ts +72 -70
- package/src/calendar.ts +136 -24
- package/src/contents.ts +13 -6
- package/src/courses.ts +21 -37
- package/src/discussions.ts +10 -21
- package/src/favorites.ts +44 -80
- package/src/files.ts +124 -142
- package/src/homework.ts +207 -135
- package/src/index.ts +79 -48
- package/src/notices.ts +29 -42
- package/src/questionnaires.ts +62 -45
- package/src/request.ts +192 -69
- package/src/timetable.ts +121 -0
- package/src/types.ts +123 -87
- package/src/urls.ts +13 -64
- package/src/user.ts +22 -7
- package/src/utils.ts +97 -29
package/src/types.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { CookieJar } from 'tough-cookie';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* A time as an ISO 8601 string in Beijing time with its offset, e.g. `2026-11-07T18:28:00+08:00`: `new Date(t)` and
|
|
5
|
+
* `dayjs(t)` read it as the right moment in any time zone. (A string the server sent in another shape is passed on as
|
|
6
|
+
* it is.)
|
|
7
|
+
*/
|
|
8
|
+
export type ServerTime = string;
|
|
9
|
+
|
|
3
10
|
export type Fetch = typeof globalThis.fetch;
|
|
4
11
|
export type Credential = {
|
|
5
12
|
username?: string;
|
|
@@ -32,6 +39,9 @@ export enum FailReason {
|
|
|
32
39
|
OPERATION_FAILED = 'operation failed',
|
|
33
40
|
ERROR_SETTING_COOKIES = 'could not set cookies',
|
|
34
41
|
REQUEST_TIMEOUT = 'request timed out',
|
|
42
|
+
/** a request got no answer at all (no connection, DNS or TLS failure, connection reset); the error is in `extra` */
|
|
43
|
+
NETWORK_ERROR = 'network error',
|
|
44
|
+
ERROR_REGISTRAR_AUTH = 'could not sign in to the registrar (zhjw.cic.tsinghua.edu.cn)',
|
|
35
45
|
}
|
|
36
46
|
|
|
37
47
|
export interface ApiError {
|
|
@@ -55,14 +65,16 @@ export enum ContentType {
|
|
|
55
65
|
QUESTIONNAIRE = 'questionnaire',
|
|
56
66
|
}
|
|
57
67
|
|
|
58
|
-
interface
|
|
68
|
+
export interface UserInfo {
|
|
59
69
|
name: string;
|
|
70
|
+
/**
|
|
71
|
+
* The department (for undergraduates, their 书院) as the homepage shows it, in the website language.
|
|
72
|
+
* `''` when there is none, including in English for departments without an English name (the page says `none`).
|
|
73
|
+
*/
|
|
60
74
|
department: string;
|
|
61
75
|
}
|
|
62
76
|
|
|
63
|
-
export
|
|
64
|
-
|
|
65
|
-
interface ISemesterInfo {
|
|
77
|
+
export interface SemesterInfo {
|
|
66
78
|
id: string;
|
|
67
79
|
startDate: string;
|
|
68
80
|
endDate: string;
|
|
@@ -71,19 +83,20 @@ interface ISemesterInfo {
|
|
|
71
83
|
type: SemesterType;
|
|
72
84
|
}
|
|
73
85
|
|
|
74
|
-
export type SemesterInfo = ISemesterInfo;
|
|
75
|
-
|
|
76
86
|
export enum CourseType {
|
|
77
87
|
STUDENT = 'student',
|
|
78
88
|
TEACHER = 'teacher',
|
|
79
89
|
}
|
|
80
90
|
|
|
81
|
-
interface
|
|
91
|
+
export interface CourseInfo {
|
|
82
92
|
id: string;
|
|
83
93
|
name: string;
|
|
84
94
|
chineseName: string;
|
|
85
95
|
englishName: string;
|
|
86
|
-
/**
|
|
96
|
+
/**
|
|
97
|
+
* only with `getCourseList(..., { timeAndLocation: true })`, and left out for a course whose request failed;
|
|
98
|
+
* see `getCourseTimeAndLocation`. Read an entry with `parseCourseMeeting`.
|
|
99
|
+
*/
|
|
87
100
|
timeAndLocation?: string[];
|
|
88
101
|
url: string;
|
|
89
102
|
teacherName: string;
|
|
@@ -93,9 +106,7 @@ interface ICourseInfo {
|
|
|
93
106
|
courseType: CourseType;
|
|
94
107
|
}
|
|
95
108
|
|
|
96
|
-
export
|
|
97
|
-
|
|
98
|
-
interface IRemoteFile {
|
|
109
|
+
export interface RemoteFile {
|
|
99
110
|
id: string;
|
|
100
111
|
name: string;
|
|
101
112
|
downloadUrl: string;
|
|
@@ -103,42 +114,39 @@ interface IRemoteFile {
|
|
|
103
114
|
size: string;
|
|
104
115
|
}
|
|
105
116
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
export interface INotification {
|
|
117
|
+
/** a course notification as the list gives it, without its attachment */
|
|
118
|
+
export interface CourseNotificationBase {
|
|
109
119
|
id: string;
|
|
110
120
|
title: string;
|
|
111
121
|
content: string;
|
|
112
122
|
hasRead: boolean;
|
|
113
123
|
url: string;
|
|
114
124
|
markedImportant: boolean;
|
|
115
|
-
publishTime:
|
|
125
|
+
publishTime: ServerTime;
|
|
116
126
|
publisher: string;
|
|
117
|
-
expireTime?:
|
|
127
|
+
expireTime?: ServerTime;
|
|
118
128
|
isFavorite: boolean;
|
|
119
129
|
comment?: string;
|
|
120
130
|
/** name of the attachment, if the notice has one; fetch it with `getNotificationAttachment` */
|
|
121
131
|
attachmentName?: string;
|
|
122
132
|
}
|
|
123
133
|
|
|
124
|
-
export interface
|
|
134
|
+
export interface CourseNotificationDetail {
|
|
125
135
|
attachment?: RemoteFile;
|
|
126
136
|
}
|
|
127
137
|
|
|
128
|
-
export type
|
|
138
|
+
export type CourseNotification = CourseNotificationBase & CourseNotificationDetail;
|
|
129
139
|
|
|
130
|
-
interface
|
|
140
|
+
export interface FileCategory {
|
|
131
141
|
id: string;
|
|
132
142
|
title: string;
|
|
133
|
-
creationTime:
|
|
143
|
+
creationTime: ServerTime;
|
|
134
144
|
}
|
|
135
145
|
|
|
136
|
-
export
|
|
137
|
-
|
|
138
|
-
interface IFile {
|
|
139
|
-
/** previously `id2` */
|
|
146
|
+
export interface CourseFile {
|
|
147
|
+
/** the id of this file entry in the course (`kjxxid`) */
|
|
140
148
|
id: string;
|
|
141
|
-
/**
|
|
149
|
+
/** the id of the stored file (`wjid`), which the download and preview URLs use */
|
|
142
150
|
fileId: string;
|
|
143
151
|
/** note: will be unset when calling `getFileListByCategory` */
|
|
144
152
|
category?: FileCategory;
|
|
@@ -148,8 +156,8 @@ interface IFile {
|
|
|
148
156
|
size: string;
|
|
149
157
|
title: string;
|
|
150
158
|
description: string;
|
|
151
|
-
uploadTime:
|
|
152
|
-
publishTime:
|
|
159
|
+
uploadTime: ServerTime;
|
|
160
|
+
publishTime: ServerTime;
|
|
153
161
|
downloadUrl: string;
|
|
154
162
|
/** preview is not supported on all types of files, check before use */
|
|
155
163
|
previewUrl: string;
|
|
@@ -158,16 +166,14 @@ interface IFile {
|
|
|
158
166
|
visitCount: number;
|
|
159
167
|
downloadCount: number;
|
|
160
168
|
fileType: string;
|
|
161
|
-
/**
|
|
169
|
+
/** the same file as a `RemoteFile`, the shape attachments have */
|
|
162
170
|
remoteFile: RemoteFile;
|
|
163
171
|
/** could not get favorite or comment info when using `getFileList` or in TA mode */
|
|
164
172
|
isFavorite?: boolean;
|
|
165
173
|
comment?: string;
|
|
166
174
|
}
|
|
167
175
|
|
|
168
|
-
export
|
|
169
|
-
|
|
170
|
-
export interface IHomeworkStatus {
|
|
176
|
+
export interface HomeworkStatus {
|
|
171
177
|
submitted: boolean;
|
|
172
178
|
graded: boolean;
|
|
173
179
|
}
|
|
@@ -207,33 +213,34 @@ export enum HomeworkGradeLevel {
|
|
|
207
213
|
F = 'F',
|
|
208
214
|
}
|
|
209
215
|
|
|
210
|
-
|
|
216
|
+
/** a homework as the lists give it, without its detail */
|
|
217
|
+
export interface HomeworkBase extends HomeworkStatus {
|
|
211
218
|
id: string;
|
|
212
219
|
/** @deprecated use `id` */
|
|
213
220
|
studentHomeworkId: string;
|
|
214
221
|
baseId: string;
|
|
215
222
|
title: string;
|
|
216
|
-
deadline:
|
|
217
|
-
lateSubmissionDeadline?:
|
|
223
|
+
deadline: ServerTime;
|
|
224
|
+
lateSubmissionDeadline?: ServerTime;
|
|
218
225
|
url: string;
|
|
219
226
|
completionType: HomeworkCompletionType;
|
|
220
227
|
submissionType: HomeworkSubmissionType;
|
|
221
228
|
submitUrl: string;
|
|
222
|
-
submitTime?:
|
|
229
|
+
submitTime?: ServerTime;
|
|
223
230
|
isLateSubmission: boolean;
|
|
224
231
|
grade?: number;
|
|
225
232
|
/** some homework has levels but not grades, like A/B/.../F */
|
|
226
233
|
gradeLevel?: HomeworkGradeLevel;
|
|
227
|
-
gradeTime?:
|
|
234
|
+
gradeTime?: ServerTime;
|
|
228
235
|
graderName?: string;
|
|
229
236
|
gradeContent?: string;
|
|
230
237
|
isFavorite: boolean;
|
|
231
|
-
favoriteTime?: string;
|
|
232
238
|
comment?: string;
|
|
239
|
+
/** `[]` when there is none; `undefined` when the course's excellent homework list couldn't be fetched */
|
|
233
240
|
excellentHomeworkList?: ExcellentHomework[];
|
|
234
241
|
}
|
|
235
242
|
|
|
236
|
-
export interface
|
|
243
|
+
export interface HomeworkDetail {
|
|
237
244
|
description?: string;
|
|
238
245
|
/** attachment from teacher */
|
|
239
246
|
attachment?: RemoteFile;
|
|
@@ -247,7 +254,7 @@ export interface IHomeworkDetail {
|
|
|
247
254
|
gradeAttachment?: RemoteFile;
|
|
248
255
|
}
|
|
249
256
|
|
|
250
|
-
export type Homework =
|
|
257
|
+
export type Homework = HomeworkBase & HomeworkDetail;
|
|
251
258
|
|
|
252
259
|
export enum HomeworkCompletionType {
|
|
253
260
|
INDIVIDUAL = 1,
|
|
@@ -259,7 +266,8 @@ export enum HomeworkSubmissionType {
|
|
|
259
266
|
OFFLINE = 0,
|
|
260
267
|
}
|
|
261
268
|
|
|
262
|
-
|
|
269
|
+
/** an excellent homework as the list gives it, without its detail */
|
|
270
|
+
export interface ExcellentHomeworkBase {
|
|
263
271
|
id: string;
|
|
264
272
|
baseId: string;
|
|
265
273
|
title: string;
|
|
@@ -272,18 +280,18 @@ export interface IExcellentHomework {
|
|
|
272
280
|
};
|
|
273
281
|
}
|
|
274
282
|
|
|
275
|
-
export type ExcellentHomework =
|
|
283
|
+
export type ExcellentHomework = ExcellentHomeworkBase & HomeworkDetail;
|
|
276
284
|
|
|
277
|
-
export interface
|
|
285
|
+
export interface HomeworkTA {
|
|
278
286
|
id: string;
|
|
279
287
|
index: number;
|
|
280
288
|
title: string;
|
|
281
289
|
description: string;
|
|
282
290
|
publisherId: string;
|
|
283
|
-
publishTime:
|
|
284
|
-
startTime:
|
|
285
|
-
deadline:
|
|
286
|
-
lateSubmissionDeadline?:
|
|
291
|
+
publishTime: ServerTime;
|
|
292
|
+
startTime: ServerTime;
|
|
293
|
+
deadline: ServerTime;
|
|
294
|
+
lateSubmissionDeadline?: ServerTime;
|
|
287
295
|
url: string;
|
|
288
296
|
completionType: HomeworkCompletionType;
|
|
289
297
|
submissionType: HomeworkSubmissionType;
|
|
@@ -292,40 +300,51 @@ export interface IHomeworkTA {
|
|
|
292
300
|
unsubmittedCount: number;
|
|
293
301
|
}
|
|
294
302
|
|
|
295
|
-
|
|
303
|
+
/**
|
|
304
|
+
* A file to upload in React Native, whose `FormData` takes the file's `uri` (and MIME `type`) instead of a `Blob`.
|
|
305
|
+
*/
|
|
306
|
+
export interface ReactNativeFile {
|
|
307
|
+
uri: string;
|
|
308
|
+
/** MIME type; `application/octet-stream` if left out */
|
|
309
|
+
type?: string;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** The request `submitHomework` sends, from `getHomeworkSubmitRequest`, for sending with another HTTP client */
|
|
313
|
+
export interface HomeworkSubmitRequest {
|
|
314
|
+
/** the submit URL, with the CSRF token */
|
|
315
|
+
url: string;
|
|
316
|
+
/** POST it as multipart form data */
|
|
317
|
+
body: FormData;
|
|
318
|
+
}
|
|
296
319
|
|
|
297
|
-
export interface
|
|
320
|
+
export interface HomeworkSubmitAttachment {
|
|
298
321
|
filename: string;
|
|
299
|
-
content: Blob;
|
|
322
|
+
content: Blob | ReactNativeFile;
|
|
300
323
|
}
|
|
301
324
|
|
|
302
|
-
export interface
|
|
325
|
+
export interface DiscussionBase {
|
|
303
326
|
id: string;
|
|
304
327
|
title: string;
|
|
305
328
|
publisherName: string;
|
|
306
|
-
publishTime:
|
|
329
|
+
publishTime: ServerTime;
|
|
307
330
|
lastReplierName: string;
|
|
308
|
-
lastReplyTime:
|
|
331
|
+
lastReplyTime: ServerTime;
|
|
309
332
|
visitCount: number;
|
|
310
333
|
replyCount: number;
|
|
311
334
|
isFavorite: boolean;
|
|
312
335
|
comment?: string;
|
|
313
336
|
}
|
|
314
337
|
|
|
315
|
-
interface
|
|
338
|
+
export interface Discussion extends DiscussionBase {
|
|
316
339
|
url: string;
|
|
317
340
|
boardId: string;
|
|
318
341
|
}
|
|
319
342
|
|
|
320
|
-
export
|
|
321
|
-
|
|
322
|
-
interface IQuestion extends IDiscussionBase {
|
|
343
|
+
export interface Question extends DiscussionBase {
|
|
323
344
|
url: string;
|
|
324
345
|
question: string;
|
|
325
346
|
}
|
|
326
347
|
|
|
327
|
-
export type Question = IQuestion;
|
|
328
|
-
|
|
329
348
|
export enum QuestionnaireDetailType {
|
|
330
349
|
SINGLE = 'dnx',
|
|
331
350
|
MULTI = 'dox',
|
|
@@ -352,24 +371,23 @@ export enum QuestionnaireType {
|
|
|
352
371
|
SURVEY = 'wj',
|
|
353
372
|
}
|
|
354
373
|
|
|
355
|
-
export interface
|
|
374
|
+
export interface Questionnaire {
|
|
356
375
|
id: string;
|
|
357
376
|
type: QuestionnaireType;
|
|
358
377
|
title: string;
|
|
359
|
-
startTime:
|
|
360
|
-
endTime:
|
|
361
|
-
uploadTime:
|
|
378
|
+
startTime: ServerTime;
|
|
379
|
+
endTime: ServerTime;
|
|
380
|
+
uploadTime: ServerTime;
|
|
362
381
|
uploaderId: string;
|
|
363
382
|
uploaderName: string;
|
|
364
|
-
submitTime?:
|
|
383
|
+
submitTime?: ServerTime;
|
|
365
384
|
isFavorite: boolean;
|
|
366
385
|
comment?: string;
|
|
367
386
|
url: string;
|
|
368
|
-
|
|
387
|
+
/** the questions; only for the questionnaires `getQuestionnaireList`'s `details` selects (by default all) */
|
|
388
|
+
detail?: QuestionnaireDetail[];
|
|
369
389
|
}
|
|
370
390
|
|
|
371
|
-
export type Questionnaire = IQuestionnaire;
|
|
372
|
-
|
|
373
391
|
/** which items get their extra per-item requests: none (default), all, or those the predicate accepts */
|
|
374
392
|
export type DetailSelector<T> = 'none' | 'all' | ((item: T) => boolean);
|
|
375
393
|
|
|
@@ -383,7 +401,7 @@ export interface HomeworkListOptions {
|
|
|
383
401
|
* Which homework gets its detail (page + description JSON, 2 requests each); default `'none'`.
|
|
384
402
|
* The same as calling `getHomeworkDetail` for those items.
|
|
385
403
|
*/
|
|
386
|
-
details?: DetailSelector<
|
|
404
|
+
details?: DetailSelector<HomeworkBase>;
|
|
387
405
|
}
|
|
388
406
|
|
|
389
407
|
export interface NotificationListOptions {
|
|
@@ -391,17 +409,27 @@ export interface NotificationListOptions {
|
|
|
391
409
|
* Which notices with an attachment get it (1 request each); default `'none'`.
|
|
392
410
|
* The same as calling `getNotificationAttachment` for those items.
|
|
393
411
|
*/
|
|
394
|
-
attachments?: DetailSelector<
|
|
412
|
+
attachments?: DetailSelector<CourseNotificationBase>;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export interface QuestionnaireListOptions {
|
|
416
|
+
/**
|
|
417
|
+
* Which questionnaires get their questions (`detail`, 1 request each); default `'all'` (unlike the other lists, for
|
|
418
|
+
* compatibility). The same as calling `getQuestionnaireDetail` for those items.
|
|
419
|
+
*/
|
|
420
|
+
details?: DetailSelector<Questionnaire>;
|
|
395
421
|
}
|
|
396
422
|
|
|
397
423
|
export interface AllContentsOptions<T extends ContentType> {
|
|
398
424
|
courseType?: CourseType;
|
|
399
|
-
/** homework `details
|
|
425
|
+
/** homework or questionnaire `details`, or notification `attachments`, passed to each course's list */
|
|
400
426
|
details?: T extends ContentType.HOMEWORK
|
|
401
|
-
? DetailSelector<
|
|
427
|
+
? DetailSelector<HomeworkBase>
|
|
402
428
|
: T extends ContentType.NOTIFICATION
|
|
403
|
-
? DetailSelector<
|
|
404
|
-
:
|
|
429
|
+
? DetailSelector<CourseNotificationBase>
|
|
430
|
+
: T extends ContentType.QUESTIONNAIRE
|
|
431
|
+
? DetailSelector<Questionnaire>
|
|
432
|
+
: never;
|
|
405
433
|
}
|
|
406
434
|
|
|
407
435
|
export interface AllContentsResult<T extends ContentType> {
|
|
@@ -412,41 +440,37 @@ export interface AllContentsResult<T extends ContentType> {
|
|
|
412
440
|
}
|
|
413
441
|
|
|
414
442
|
export type ContentTypeMap = {
|
|
415
|
-
[ContentType.NOTIFICATION]:
|
|
416
|
-
[ContentType.FILE]:
|
|
443
|
+
[ContentType.NOTIFICATION]: CourseNotification;
|
|
444
|
+
[ContentType.FILE]: CourseFile;
|
|
417
445
|
[ContentType.HOMEWORK]: Homework;
|
|
418
446
|
[ContentType.DISCUSSION]: Discussion;
|
|
419
447
|
[ContentType.QUESTION]: Question;
|
|
420
448
|
[ContentType.QUESTIONNAIRE]: Questionnaire;
|
|
421
449
|
};
|
|
422
450
|
|
|
423
|
-
interface
|
|
451
|
+
export interface CourseContent<T extends ContentType = ContentType> {
|
|
424
452
|
[id: string]: ContentTypeMap[T][];
|
|
425
453
|
}
|
|
426
454
|
|
|
427
|
-
export
|
|
428
|
-
|
|
429
|
-
interface IFavoriteItem {
|
|
455
|
+
export interface FavoriteItem {
|
|
430
456
|
id: string;
|
|
431
457
|
type: ContentType;
|
|
432
458
|
title: string;
|
|
433
|
-
time:
|
|
459
|
+
time: ServerTime;
|
|
434
460
|
state: string;
|
|
435
461
|
/** extra message. For homework, this will be deadline (plus score if graded). It's too flexible and hard to parse so we leave it as is. */
|
|
436
462
|
extra?: string;
|
|
437
463
|
semesterId: string;
|
|
438
464
|
courseId: string;
|
|
439
465
|
pinned: boolean;
|
|
440
|
-
pinnedTime?:
|
|
466
|
+
pinnedTime?: ServerTime;
|
|
441
467
|
comment?: string;
|
|
442
|
-
addedTime:
|
|
468
|
+
addedTime: ServerTime;
|
|
443
469
|
/** for reference */
|
|
444
470
|
itemId: string;
|
|
445
471
|
}
|
|
446
472
|
|
|
447
|
-
export
|
|
448
|
-
|
|
449
|
-
interface ICommentItem {
|
|
473
|
+
export interface CommentItem {
|
|
450
474
|
id: string;
|
|
451
475
|
type: ContentType;
|
|
452
476
|
content: string;
|
|
@@ -454,12 +478,21 @@ interface ICommentItem {
|
|
|
454
478
|
title: string;
|
|
455
479
|
semesterId: string;
|
|
456
480
|
courseId: string;
|
|
457
|
-
commentTime:
|
|
481
|
+
commentTime: ServerTime;
|
|
458
482
|
/** for reference */
|
|
459
483
|
itemId: string;
|
|
460
484
|
}
|
|
461
485
|
|
|
462
|
-
|
|
486
|
+
/** one entry of a course's time and location, read by `parseCourseMeeting` */
|
|
487
|
+
export interface CourseMeeting {
|
|
488
|
+
/** 1 (Monday) to 7 (Sunday) */
|
|
489
|
+
weekday: number;
|
|
490
|
+
/** the class period (大节), a key of `CLASS_PERIOD_TIMES` */
|
|
491
|
+
period: number;
|
|
492
|
+
/** the teaching weeks it happens in, from 1; `null` for every week */
|
|
493
|
+
weeks: number[] | null;
|
|
494
|
+
location: string;
|
|
495
|
+
}
|
|
463
496
|
|
|
464
497
|
export interface CalendarEvent {
|
|
465
498
|
location: string;
|
|
@@ -484,7 +517,10 @@ export interface CalendarRange {
|
|
|
484
517
|
export interface CalendarResult {
|
|
485
518
|
/** events of every window that succeeded, in date order */
|
|
486
519
|
events: CalendarEvent[];
|
|
487
|
-
/**
|
|
520
|
+
/**
|
|
521
|
+
* windows that failed (after a retry, unless they timed out) or were skipped after repeated timeouts; empty when the
|
|
522
|
+
* whole range was fetched
|
|
523
|
+
*/
|
|
488
524
|
failedRanges: CalendarRange[];
|
|
489
525
|
}
|
|
490
526
|
|
package/src/urls.ts
CHANGED
|
@@ -1,36 +1,31 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { ContentType, IHomeworkSubmitAttachment, QuestionnaireType } from './types.ts';
|
|
1
|
+
import type { ContentType, QuestionnaireType } from './types.ts';
|
|
3
2
|
import { CourseType, Language } from './types.ts';
|
|
4
3
|
import { CONTENT_TYPE_MAP, getMkFromType } from './utils.ts';
|
|
5
4
|
|
|
5
|
+
// URLs without parameters are constants; URLs with parameters, and form bodies (a new `FormData` each time), are
|
|
6
|
+
// functions. Form bodies use the global `FormData`, which Node (22+) and React Native both have.
|
|
7
|
+
|
|
6
8
|
export const ID_PREFIX = 'https://id.tsinghua.edu.cn';
|
|
7
9
|
export const LEARN_PREFIX = 'https://learn.tsinghua.edu.cn';
|
|
8
10
|
export const REGISTRAR_PREFIX = 'https://zhjw.cic.tsinghua.edu.cn';
|
|
9
11
|
|
|
10
12
|
const MAX_SIZE = 200;
|
|
11
13
|
|
|
12
|
-
export const ID_LOGIN =
|
|
14
|
+
export const ID_LOGIN = `${ID_PREFIX}/do/off/ui/auth/login/form/bb5df85216504820be7bba2b0ae1535b/0`;
|
|
13
15
|
|
|
14
|
-
export const ID_LOGIN_CHECK =
|
|
16
|
+
export const ID_LOGIN_CHECK = `${ID_PREFIX}/do/off/ui/auth/login/check`;
|
|
15
17
|
|
|
16
18
|
export const LEARN_AUTH_ROAM = (ticket: string) =>
|
|
17
19
|
`${LEARN_PREFIX}/b/j_spring_security_thauth_roaming_entry?ticket=${ticket}`;
|
|
18
20
|
|
|
19
|
-
export const LEARN_LOGOUT =
|
|
20
|
-
|
|
21
|
-
export const LEARN_HOMEPAGE = (courseType: CourseType) => {
|
|
22
|
-
if (courseType === CourseType.STUDENT) {
|
|
23
|
-
return `${LEARN_PREFIX}/f/wlxt/index/course/student/`;
|
|
24
|
-
} else {
|
|
25
|
-
return `${LEARN_PREFIX}/f/wlxt/index/course/teacher/`;
|
|
26
|
-
}
|
|
27
|
-
};
|
|
21
|
+
export const LEARN_LOGOUT = `${LEARN_PREFIX}/f/j_spring_security_logout`;
|
|
28
22
|
|
|
29
|
-
|
|
23
|
+
/** the homepage (course list) of a student or a teacher; login reads the CSRF token from the student one */
|
|
24
|
+
export const LEARN_HOMEPAGE = (courseType: CourseType) => `${LEARN_PREFIX}/f/wlxt/index/course/${courseType}/`;
|
|
30
25
|
|
|
31
|
-
export const LEARN_SEMESTER_LIST =
|
|
26
|
+
export const LEARN_SEMESTER_LIST = `${LEARN_PREFIX}/b/wlxt/kc/v_wlkc_xs_xktjb_coassb/queryxnxq`;
|
|
32
27
|
|
|
33
|
-
export const LEARN_CURRENT_SEMESTER =
|
|
28
|
+
export const LEARN_CURRENT_SEMESTER = `${LEARN_PREFIX}/b/kc/zhjw_v_code_xnxq/getCurrentAndNextSemester`;
|
|
34
29
|
|
|
35
30
|
export const LEARN_COURSE_LIST = (semester: string, courseType: CourseType, lang: Language) =>
|
|
36
31
|
courseType === CourseType.STUDENT
|
|
@@ -86,37 +81,10 @@ export const LEARN_NOTIFICATION_DETAIL = (courseID: string, notificationID: stri
|
|
|
86
81
|
? `${LEARN_PREFIX}/f/wlxt/kcgg/wlkc_ggb/student/beforeViewXs?wlkcid=${courseID}&id=${notificationID}`
|
|
87
82
|
: `${LEARN_PREFIX}/f/wlxt/kcgg/wlkc_ggb/teacher/beforeViewJs?wlkcid=${courseID}&id=${notificationID}`;
|
|
88
83
|
|
|
89
|
-
export const LEARN_NOTIFICATION_EDIT = (courseType: CourseType): string =>
|
|
90
|
-
`${LEARN_PREFIX}/b/wlxt/kcgg/wlkc_ggb/${courseType}/editKcgg`;
|
|
91
|
-
|
|
92
84
|
export const LEARN_HOMEWORK_LIST_NEW = `${LEARN_PREFIX}/b/wlxt/kczy/zy/student/zyListWj`;
|
|
93
85
|
export const LEARN_HOMEWORK_LIST_SUBMITTED = `${LEARN_PREFIX}/b/wlxt/kczy/zy/student/zyListYjwg`;
|
|
94
86
|
export const LEARN_HOMEWORK_LIST_GRADED = `${LEARN_PREFIX}/b/wlxt/kczy/zy/student/zyListYpg`;
|
|
95
87
|
export const LEARN_HOMEWORK_LIST_EXCELLENT = `${LEARN_PREFIX}/b/wlxt/kczy/zy/student/yxzylist`;
|
|
96
|
-
export const LEARN_HOMEWORK_LIST_SOURCE = [
|
|
97
|
-
{
|
|
98
|
-
url: LEARN_HOMEWORK_LIST_NEW,
|
|
99
|
-
status: {
|
|
100
|
-
submitted: false,
|
|
101
|
-
graded: false,
|
|
102
|
-
},
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
url: LEARN_HOMEWORK_LIST_SUBMITTED,
|
|
106
|
-
status: {
|
|
107
|
-
submitted: true,
|
|
108
|
-
graded: false,
|
|
109
|
-
},
|
|
110
|
-
},
|
|
111
|
-
{
|
|
112
|
-
url: LEARN_HOMEWORK_LIST_GRADED,
|
|
113
|
-
status: {
|
|
114
|
-
submitted: true,
|
|
115
|
-
graded: true,
|
|
116
|
-
},
|
|
117
|
-
},
|
|
118
|
-
];
|
|
119
|
-
|
|
120
88
|
export const LEARN_HOMEWORK_PAGE = (courseID: string, id: string) =>
|
|
121
89
|
`${LEARN_PREFIX}/f/wlxt/kczy/zy/student/viewCj?wlkcid=${courseID}&xszyid=${id}`;
|
|
122
90
|
|
|
@@ -131,29 +99,10 @@ export const LEARN_HOMEWORK_DETAIL_FORM_DATA = (baseId: string) => {
|
|
|
131
99
|
return form;
|
|
132
100
|
};
|
|
133
101
|
|
|
134
|
-
export const LEARN_HOMEWORK_DOWNLOAD = (courseID: string, attachmentID: string) =>
|
|
135
|
-
`${LEARN_PREFIX}/b/wlxt/kczy/zy/student/downloadFile/${courseID}/${attachmentID}`;
|
|
136
|
-
|
|
137
102
|
export const LEARN_HOMEWORK_SUBMIT_PAGE = (courseID: string, id: string) =>
|
|
138
103
|
`${LEARN_PREFIX}/f/wlxt/kczy/zy/student/tijiao?wlkcid=${courseID}&xszyid=${id}`;
|
|
139
104
|
|
|
140
|
-
export const LEARN_HOMEWORK_SUBMIT =
|
|
141
|
-
|
|
142
|
-
export const LEARN_HOMEWORK_SUBMIT_FORM_DATA = (
|
|
143
|
-
id: string,
|
|
144
|
-
content = '',
|
|
145
|
-
attachment?: IHomeworkSubmitAttachment,
|
|
146
|
-
removeAttachment = false,
|
|
147
|
-
) => {
|
|
148
|
-
const form = new FormData();
|
|
149
|
-
form.append('xszyid', id);
|
|
150
|
-
form.append('zynr', content ?? '');
|
|
151
|
-
if (attachment) form.append('fileupload', attachment.content, attachment.filename);
|
|
152
|
-
else form.append('fileupload', 'undefined');
|
|
153
|
-
if (removeAttachment) form.append('isDeleted', '1');
|
|
154
|
-
else form.append('isDeleted', '0');
|
|
155
|
-
return form;
|
|
156
|
-
};
|
|
105
|
+
export const LEARN_HOMEWORK_SUBMIT = `${LEARN_PREFIX}/b/wlxt/kczy/zy/student/tjzy`;
|
|
157
106
|
|
|
158
107
|
export const LEARN_HOMEWORK_LIST_TEACHER = `${LEARN_PREFIX}/b/wlxt/kczy/zy/teacher/pageList`;
|
|
159
108
|
|
|
@@ -247,7 +196,7 @@ export const REGISTRAR_TICKET_FORM_DATA = () => {
|
|
|
247
196
|
return form;
|
|
248
197
|
};
|
|
249
198
|
|
|
250
|
-
export const REGISTRAR_TICKET =
|
|
199
|
+
export const REGISTRAR_TICKET = `${LEARN_PREFIX}/b/wlxt/common/auth/gnt`;
|
|
251
200
|
|
|
252
201
|
export const REGISTRAR_AUTH = (ticket: string) => `${REGISTRAR_PREFIX}/j_acegi_login.do?url=/&ticket=${ticket}`;
|
|
253
202
|
|
package/src/user.ts
CHANGED
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
// User info and website language.
|
|
2
2
|
|
|
3
3
|
import type { Session } from './request.ts';
|
|
4
|
-
import {
|
|
5
|
-
import type {
|
|
6
|
-
import { CourseType } from './types.ts';
|
|
4
|
+
import { discard } from './request.ts';
|
|
5
|
+
import type { Language, UserInfo } from './types.ts';
|
|
6
|
+
import { CourseType, FailReason } from './types.ts';
|
|
7
7
|
import * as URLS from './urls.ts';
|
|
8
|
+
import { $, apiError } from './utils.ts';
|
|
8
9
|
|
|
9
|
-
/**
|
|
10
|
+
/** what the English homepage shows for a department without an English name (as for the 书院) */
|
|
11
|
+
const NO_DEPARTMENT = 'none';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* get user's name and department (`''` if the page shows none)
|
|
15
|
+
*
|
|
16
|
+
* The homepage's profile card (`.fl.up-img-info`) has two `<p>`: the name and role labels (同学 / Student), then the
|
|
17
|
+
* department label (for undergraduates, their 书院). The name is also in the header's `a.user-log`.
|
|
18
|
+
*/
|
|
10
19
|
export async function getUserInfo(s: Session, courseType = CourseType.STUDENT): Promise<UserInfo> {
|
|
11
|
-
const content = await
|
|
20
|
+
const content = await s.fetchTextWithToken(URLS.LEARN_HOMEPAGE(courseType));
|
|
12
21
|
|
|
13
22
|
const dom = $(content);
|
|
14
23
|
const name = dom('a.user-log').text().trim();
|
|
15
|
-
const
|
|
24
|
+
const label = dom('.fl.up-img-info p:nth-child(2) label').text().trim();
|
|
25
|
+
const department = label.toLowerCase() === NO_DEPARTMENT ? '' : label;
|
|
16
26
|
|
|
17
27
|
return {
|
|
18
28
|
name,
|
|
@@ -21,8 +31,13 @@ export async function getUserInfo(s: Session, courseType = CourseType.STUDENT):
|
|
|
21
31
|
}
|
|
22
32
|
|
|
23
33
|
export async function setLanguage(s: Session, lang: Language): Promise<void> {
|
|
24
|
-
await s.fetchWithToken(URLS.LEARN_WEBSITE_LANGUAGE(lang), {
|
|
34
|
+
const response = await s.fetchWithToken(URLS.LEARN_WEBSITE_LANGUAGE(lang), {
|
|
25
35
|
method: 'POST',
|
|
26
36
|
});
|
|
37
|
+
// free the connection; only the status matters
|
|
38
|
+
discard(response);
|
|
39
|
+
if (!response.ok) {
|
|
40
|
+
throw apiError(FailReason.OPERATION_FAILED, { code: response.status, text: response.statusText });
|
|
41
|
+
}
|
|
27
42
|
s.lang = lang;
|
|
28
43
|
}
|