@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.
Files changed (84) hide show
  1. package/README.md +94 -227
  2. package/lib/auth.d.ts +15 -4
  3. package/lib/auth.d.ts.map +1 -1
  4. package/lib/auth.js +65 -55
  5. package/lib/auth.js.map +1 -1
  6. package/lib/calendar.d.ts +12 -5
  7. package/lib/calendar.d.ts.map +1 -1
  8. package/lib/calendar.js +118 -20
  9. package/lib/calendar.js.map +1 -1
  10. package/lib/contents.d.ts.map +1 -1
  11. package/lib/contents.js +10 -6
  12. package/lib/contents.js.map +1 -1
  13. package/lib/courses.d.ts.map +1 -1
  14. package/lib/courses.js +15 -29
  15. package/lib/courses.js.map +1 -1
  16. package/lib/discussions.d.ts.map +1 -1
  17. package/lib/discussions.js +8 -19
  18. package/lib/discussions.js.map +1 -1
  19. package/lib/favorites.d.ts +1 -0
  20. package/lib/favorites.d.ts.map +1 -1
  21. package/lib/favorites.js +25 -60
  22. package/lib/favorites.js.map +1 -1
  23. package/lib/files.d.ts +4 -4
  24. package/lib/files.d.ts.map +1 -1
  25. package/lib/files.js +112 -134
  26. package/lib/files.js.map +1 -1
  27. package/lib/homework.d.ts +35 -5
  28. package/lib/homework.d.ts.map +1 -1
  29. package/lib/homework.js +150 -92
  30. package/lib/homework.js.map +1 -1
  31. package/lib/index.d.ts +45 -20
  32. package/lib/index.d.ts.map +1 -1
  33. package/lib/index.js +52 -17
  34. package/lib/index.js.map +1 -1
  35. package/lib/notices.d.ts +4 -4
  36. package/lib/notices.d.ts.map +1 -1
  37. package/lib/notices.js +13 -33
  38. package/lib/notices.js.map +1 -1
  39. package/lib/questionnaires.d.ts +6 -3
  40. package/lib/questionnaires.d.ts.map +1 -1
  41. package/lib/questionnaires.js +26 -24
  42. package/lib/questionnaires.js.map +1 -1
  43. package/lib/request.d.ts +64 -20
  44. package/lib/request.d.ts.map +1 -1
  45. package/lib/request.js +174 -64
  46. package/lib/request.js.map +1 -1
  47. package/lib/timetable.d.ts +26 -0
  48. package/lib/timetable.d.ts.map +1 -0
  49. package/lib/timetable.js +110 -0
  50. package/lib/timetable.js.map +1 -0
  51. package/lib/types.d.ts +117 -76
  52. package/lib/types.d.ts.map +1 -1
  53. package/lib/types.js +3 -0
  54. package/lib/types.js.map +1 -1
  55. package/lib/urls.d.ts +10 -20
  56. package/lib/urls.d.ts.map +1 -1
  57. package/lib/urls.js +11 -56
  58. package/lib/urls.js.map +1 -1
  59. package/lib/user.d.ts +7 -2
  60. package/lib/user.d.ts.map +1 -1
  61. package/lib/user.js +20 -6
  62. package/lib/user.js.map +1 -1
  63. package/lib/utils.d.ts +42 -4
  64. package/lib/utils.d.ts.map +1 -1
  65. package/lib/utils.js +80 -29
  66. package/lib/utils.js.map +1 -1
  67. package/package.json +9 -8
  68. package/src/auth.ts +72 -70
  69. package/src/calendar.ts +136 -24
  70. package/src/contents.ts +13 -6
  71. package/src/courses.ts +21 -37
  72. package/src/discussions.ts +10 -21
  73. package/src/favorites.ts +44 -80
  74. package/src/files.ts +124 -142
  75. package/src/homework.ts +207 -135
  76. package/src/index.ts +79 -48
  77. package/src/notices.ts +29 -42
  78. package/src/questionnaires.ts +62 -45
  79. package/src/request.ts +192 -69
  80. package/src/timetable.ts +121 -0
  81. package/src/types.ts +123 -87
  82. package/src/urls.ts +13 -64
  83. package/src/user.ts +22 -7
  84. 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 IUserInfo {
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 type UserInfo = IUserInfo;
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 ICourseInfo {
91
+ export interface CourseInfo {
82
92
  id: string;
83
93
  name: string;
84
94
  chineseName: string;
85
95
  englishName: string;
86
- /** only with `getCourseList(..., { timeAndLocation: true })`; see `getCourseTimeAndLocation` */
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 type CourseInfo = ICourseInfo;
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
- export type RemoteFile = IRemoteFile;
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: string;
125
+ publishTime: ServerTime;
116
126
  publisher: string;
117
- expireTime?: string;
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 INotificationDetail {
134
+ export interface CourseNotificationDetail {
125
135
  attachment?: RemoteFile;
126
136
  }
127
137
 
128
- export type Notification = INotification & INotificationDetail;
138
+ export type CourseNotification = CourseNotificationBase & CourseNotificationDetail;
129
139
 
130
- interface IFileCategory {
140
+ export interface FileCategory {
131
141
  id: string;
132
142
  title: string;
133
- creationTime: Date;
143
+ creationTime: ServerTime;
134
144
  }
135
145
 
136
- export type FileCategory = IFileCategory;
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
- /** previously `id` */
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: string;
152
- publishTime: string;
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
- /** for compatibility */
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 type File = IFile;
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
- export interface IHomework extends IHomeworkStatus {
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: string;
217
- lateSubmissionDeadline?: string;
223
+ deadline: ServerTime;
224
+ lateSubmissionDeadline?: ServerTime;
218
225
  url: string;
219
226
  completionType: HomeworkCompletionType;
220
227
  submissionType: HomeworkSubmissionType;
221
228
  submitUrl: string;
222
- submitTime?: string;
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?: string;
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 IHomeworkDetail {
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 = IHomework & IHomeworkDetail;
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
- export interface IExcellentHomework {
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 = IExcellentHomework & IHomeworkDetail;
283
+ export type ExcellentHomework = ExcellentHomeworkBase & HomeworkDetail;
276
284
 
277
- export interface IHomeworkTA {
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: string;
284
- startTime: string;
285
- deadline: string;
286
- lateSubmissionDeadline?: string;
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
- export type HomeworkTA = IHomeworkTA;
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 IHomeworkSubmitAttachment {
320
+ export interface HomeworkSubmitAttachment {
298
321
  filename: string;
299
- content: Blob;
322
+ content: Blob | ReactNativeFile;
300
323
  }
301
324
 
302
- export interface IDiscussionBase {
325
+ export interface DiscussionBase {
303
326
  id: string;
304
327
  title: string;
305
328
  publisherName: string;
306
- publishTime: string;
329
+ publishTime: ServerTime;
307
330
  lastReplierName: string;
308
- lastReplyTime: string;
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 IDiscussion extends IDiscussionBase {
338
+ export interface Discussion extends DiscussionBase {
316
339
  url: string;
317
340
  boardId: string;
318
341
  }
319
342
 
320
- export type Discussion = IDiscussion;
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 IQuestionnaire {
374
+ export interface Questionnaire {
356
375
  id: string;
357
376
  type: QuestionnaireType;
358
377
  title: string;
359
- startTime: string;
360
- endTime: string;
361
- uploadTime: string;
378
+ startTime: ServerTime;
379
+ endTime: ServerTime;
380
+ uploadTime: ServerTime;
362
381
  uploaderId: string;
363
382
  uploaderName: string;
364
- submitTime?: string;
383
+ submitTime?: ServerTime;
365
384
  isFavorite: boolean;
366
385
  comment?: string;
367
386
  url: string;
368
- detail: QuestionnaireDetail[];
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<IHomework>;
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<INotification>;
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` or notification `attachments`, passed to each course's list */
425
+ /** homework or questionnaire `details`, or notification `attachments`, passed to each course's list */
400
426
  details?: T extends ContentType.HOMEWORK
401
- ? DetailSelector<IHomework>
427
+ ? DetailSelector<HomeworkBase>
402
428
  : T extends ContentType.NOTIFICATION
403
- ? DetailSelector<INotification>
404
- : never;
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]: Notification;
416
- [ContentType.FILE]: 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 ICourseContent<T extends ContentType> {
451
+ export interface CourseContent<T extends ContentType = ContentType> {
424
452
  [id: string]: ContentTypeMap[T][];
425
453
  }
426
454
 
427
- export type CourseContent<T extends ContentType = ContentType> = ICourseContent<T>;
428
-
429
- interface IFavoriteItem {
455
+ export interface FavoriteItem {
430
456
  id: string;
431
457
  type: ContentType;
432
458
  title: string;
433
- time: string;
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?: string;
466
+ pinnedTime?: ServerTime;
441
467
  comment?: string;
442
- addedTime: string;
468
+ addedTime: ServerTime;
443
469
  /** for reference */
444
470
  itemId: string;
445
471
  }
446
472
 
447
- export type FavoriteItem = IFavoriteItem;
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: string;
481
+ commentTime: ServerTime;
458
482
  /** for reference */
459
483
  itemId: string;
460
484
  }
461
485
 
462
- export type CommentItem = ICommentItem;
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
- /** windows that failed even after a retry; empty when the whole range was fetched */
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 { FormData } from 'node-fetch-native';
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 = () => 'https://id.tsinghua.edu.cn/do/off/ui/auth/login/form/bb5df85216504820be7bba2b0ae1535b/0';
14
+ export const ID_LOGIN = `${ID_PREFIX}/do/off/ui/auth/login/form/bb5df85216504820be7bba2b0ae1535b/0`;
13
15
 
14
- export const ID_LOGIN_CHECK = () => 'https://id.tsinghua.edu.cn/do/off/ui/auth/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 = () => `${LEARN_PREFIX}/f/j_spring_security_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
- export const LEARN_STUDENT_COURSE_LIST_PAGE = () => `${LEARN_PREFIX}/f/wlxt/index/course/student/`;
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 = () => `${LEARN_PREFIX}/b/wlxt/kc/v_wlkc_xs_xktjb_coassb/queryxnxq`;
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 = () => `${LEARN_PREFIX}/b/kc/zhjw_v_code_xnxq/getCurrentAndNextSemester`;
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 = () => `${LEARN_PREFIX}/b/wlxt/kczy/zy/student/tjzy`;
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 = () => `${LEARN_PREFIX}/b/wlxt/common/auth/gnt`;
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 { $ } from './request.ts';
5
- import type { UserInfo, Language } from './types.ts';
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
- /** get user's name and department */
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 (await s.fetchWithToken(URLS.LEARN_HOMEPAGE(courseType))).text();
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 department = dom('.fl.up-img-info p:nth-child(2) label').text().trim();
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
  }