@robertying/thu-learn-lib 4.1.0 → 5.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.
Files changed (72) hide show
  1. package/README.md +41 -7
  2. package/lib/auth.d.ts +1 -1
  3. package/lib/auth.d.ts.map +1 -1
  4. package/lib/auth.js +17 -5
  5. package/lib/auth.js.map +1 -1
  6. package/lib/calendar.d.ts +6 -4
  7. package/lib/calendar.d.ts.map +1 -1
  8. package/lib/calendar.js +21 -8
  9. package/lib/calendar.js.map +1 -1
  10. package/lib/contents.d.ts +4 -4
  11. package/lib/contents.d.ts.map +1 -1
  12. package/lib/contents.js +28 -20
  13. package/lib/contents.js.map +1 -1
  14. package/lib/courses.d.ts +5 -3
  15. package/lib/courses.d.ts.map +1 -1
  16. package/lib/courses.js +24 -12
  17. package/lib/courses.js.map +1 -1
  18. package/lib/discussions.js +2 -2
  19. package/lib/discussions.js.map +1 -1
  20. package/lib/favorites.d.ts.map +1 -1
  21. package/lib/favorites.js +12 -12
  22. package/lib/favorites.js.map +1 -1
  23. package/lib/files.d.ts +1 -1
  24. package/lib/files.d.ts.map +1 -1
  25. package/lib/files.js +10 -8
  26. package/lib/files.js.map +1 -1
  27. package/lib/homework.d.ts +11 -4
  28. package/lib/homework.d.ts.map +1 -1
  29. package/lib/homework.js +46 -43
  30. package/lib/homework.js.map +1 -1
  31. package/lib/index.d.ts +47 -13
  32. package/lib/index.d.ts.map +1 -1
  33. package/lib/index.js +63 -17
  34. package/lib/index.js.map +1 -1
  35. package/lib/notices.d.ts +11 -3
  36. package/lib/notices.d.ts.map +1 -1
  37. package/lib/notices.js +40 -36
  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 +20 -12
  42. package/lib/questionnaires.js.map +1 -1
  43. package/lib/request.d.ts +66 -10
  44. package/lib/request.d.ts.map +1 -1
  45. package/lib/request.js +234 -50
  46. package/lib/request.js.map +1 -1
  47. package/lib/types.d.ts +52 -3
  48. package/lib/types.d.ts.map +1 -1
  49. package/lib/types.js +1 -0
  50. package/lib/types.js.map +1 -1
  51. package/lib/user.js +1 -1
  52. package/lib/user.js.map +1 -1
  53. package/lib/utils.d.ts +3 -0
  54. package/lib/utils.d.ts.map +1 -1
  55. package/lib/utils.js +11 -3
  56. package/lib/utils.js.map +1 -1
  57. package/package.json +2 -2
  58. package/src/auth.ts +18 -5
  59. package/src/calendar.ts +28 -10
  60. package/src/contents.ts +30 -25
  61. package/src/courses.ts +31 -19
  62. package/src/discussions.ts +2 -2
  63. package/src/favorites.ts +22 -32
  64. package/src/files.ts +12 -12
  65. package/src/homework.ts +118 -123
  66. package/src/index.ts +94 -19
  67. package/src/notices.ts +58 -59
  68. package/src/questionnaires.ts +51 -37
  69. package/src/request.ts +260 -48
  70. package/src/types.ts +62 -2
  71. package/src/user.ts +1 -1
  72. package/src/utils.ts +14 -3
package/src/homework.ts CHANGED
@@ -9,6 +9,7 @@ import type {
9
9
  ApiError,
10
10
  ExcellentHomework,
11
11
  Homework,
12
+ HomeworkListOptions,
12
13
  HomeworkTA,
13
14
  IExcellentHomework,
14
15
  IHomework,
@@ -19,12 +20,18 @@ import type {
19
20
  } from './types.ts';
20
21
  import { ContentType, CourseType, FailReason } from './types.ts';
21
22
  import * as URLS from './urls.ts';
22
- import { GRADE_LEVEL_MAP, decodeHTML, trimAndDefine } from './utils.ts';
23
+ import { GRADE_LEVEL_MAP, decodeHTML, isSelected, trimAndDefine } from './utils.ts';
23
24
 
24
- /** Get all homeworks (课程作业) of the specified course. */
25
- export function getHomeworkList(s: Session, courseID: string): Promise<Homework[]>;
26
-
27
- export function getHomeworkList(s: Session, courseID: string, courseType: CourseType.STUDENT): Promise<Homework[]>;
25
+ /**
26
+ * Get all homeworks (课程作业) of the specified course: 3 status lists and the excellent homework list.
27
+ * Details (page + description) only for the homework `options.details` selects.
28
+ */
29
+ export function getHomeworkList(
30
+ s: Session,
31
+ courseID: string,
32
+ courseType?: CourseType.STUDENT,
33
+ options?: HomeworkListOptions,
34
+ ): Promise<Homework[]>;
28
35
 
29
36
  export function getHomeworkList(s: Session, courseID: string, courseType: CourseType.TEACHER): Promise<HomeworkTA[]>;
30
37
 
@@ -32,14 +39,13 @@ export async function getHomeworkList(
32
39
  s: Session,
33
40
  courseID: string,
34
41
  courseType: CourseType = CourseType.STUDENT,
42
+ options: HomeworkListOptions = {},
35
43
  ): Promise<Homework[] | HomeworkTA[]> {
36
44
  if (courseType === CourseType.TEACHER) {
37
- const json = await (
38
- await s.fetchWithToken(URLS.LEARN_HOMEWORK_LIST_TEACHER, {
39
- method: 'POST',
40
- body: URLS.LEARN_PAGE_LIST_FORM_DATA(courseID),
41
- })
42
- ).json();
45
+ const json = await s.fetchJsonWithToken(URLS.LEARN_HOMEWORK_LIST_TEACHER, {
46
+ method: 'POST',
47
+ body: URLS.LEARN_PAGE_LIST_FORM_DATA(courseID),
48
+ });
43
49
  if (json.result !== 'success') {
44
50
  return Promise.reject({
45
51
  reason: FailReason.INVALID_RESPONSE,
@@ -70,9 +76,19 @@ export async function getHomeworkList(
70
76
  }) satisfies HomeworkTA,
71
77
  );
72
78
  } else {
79
+ const [lists, excellentHomeworkListByHomework] = await Promise.all([
80
+ Promise.all(
81
+ URLS.LEARN_HOMEWORK_LIST_SOURCE.map((source) => getHomeworkListAtUrl(s, courseID, source.url, source.status)),
82
+ ),
83
+ // Don't block the whole process if excellent homework list cannot be fetched
84
+ getExcellentHomeworkListByHomework(s, courseID).catch((): { [id: string]: ExcellentHomework[] } => ({})),
85
+ ]);
73
86
  return Promise.all(
74
- URLS.LEARN_HOMEWORK_LIST_SOURCE.map((source) => getHomeworkListAtUrl(s, courseID, source.url, source.status)),
75
- ).then((r) => r.flat());
87
+ lists.flat().map(async (h) => {
88
+ const homework: Homework = { ...h, excellentHomeworkList: excellentHomeworkListByHomework[h.baseId] };
89
+ return isSelected(options.details, homework) ? { ...homework, ...(await getHomeworkDetail(s, h)) } : homework;
90
+ }),
91
+ );
76
92
  }
77
93
  }
78
94
 
@@ -81,13 +97,11 @@ async function getHomeworkListAtUrl(
81
97
  courseID: string,
82
98
  url: string,
83
99
  status: IHomeworkStatus,
84
- ): Promise<Homework[]> {
85
- const json = await (
86
- await s.fetchWithToken(url, {
87
- method: 'POST',
88
- body: URLS.LEARN_PAGE_LIST_FORM_DATA(courseID),
89
- })
90
- ).json();
100
+ ): Promise<IHomework[]> {
101
+ const json = await s.fetchJsonWithToken(url, {
102
+ method: 'POST',
103
+ body: URLS.LEARN_PAGE_LIST_FORM_DATA(courseID),
104
+ });
91
105
  if (json.result !== 'success') {
92
106
  return Promise.reject({
93
107
  reason: FailReason.INVALID_RESPONSE,
@@ -97,50 +111,31 @@ async function getHomeworkListAtUrl(
97
111
 
98
112
  const result = (json.object?.aaData ?? []) as any[];
99
113
 
100
- let excellentHomeworkListByHomework: { [id: string]: ExcellentHomework[] } = {};
101
- try {
102
- excellentHomeworkListByHomework = await getExcellentHomeworkListByHomework(s, courseID);
103
- } catch (e) {
104
- // Don't block the whole process if excellent homework list cannot be fetched
105
- }
106
-
107
- return Promise.all(
108
- result
109
- .map(
110
- (h) =>
111
- ({
112
- id: h.xszyid,
113
- studentHomeworkId: h.xszyid,
114
- baseId: h.zyid,
115
- title: decodeHTML(h.bt),
116
- url: URLS.LEARN_HOMEWORK_PAGE(h.wlkcid, h.xszyid),
117
- deadline: h.jzsj,
118
- lateSubmissionDeadline: h.bjjzsj ? h.bjjzsj : undefined,
119
- isLateSubmission: h.sfbj === YES,
120
- completionType: h.zywcfs,
121
- submissionType: h.zytjfs,
122
- submitUrl: URLS.LEARN_HOMEWORK_SUBMIT_PAGE(h.wlkcid, h.xszyid),
123
- submitTime: h.scsj === null ? undefined : h.scsj,
124
- grade: h.cj === null ? undefined : h.cj,
125
- gradeLevel: GRADE_LEVEL_MAP.get(h.cj),
126
- graderName: trimAndDefine(h.jsm),
127
- gradeContent: trimAndDefine(h.pynr),
128
- gradeTime: h.pysj === null ? undefined : h.pysj,
129
- isFavorite: h.sfsc === YES,
130
- favoriteTime: h.scsj === null || h.sfsc !== YES ? undefined : h.scsj,
131
- comment: h.bznr ?? undefined,
132
- excellentHomeworkList: excellentHomeworkListByHomework[h.zyid],
133
- ...status,
134
- }) satisfies IHomework,
135
- )
136
- .map(
137
- async (h) =>
138
- ({
139
- ...h,
140
- ...(await parseHomeworkAtUrl(s, h.url)),
141
- ...(await getHomeworkDetail(s, h.baseId)),
142
- }) satisfies Homework,
143
- ),
114
+ return result.map(
115
+ (h) =>
116
+ ({
117
+ id: h.xszyid,
118
+ studentHomeworkId: h.xszyid,
119
+ baseId: h.zyid,
120
+ title: decodeHTML(h.bt),
121
+ url: URLS.LEARN_HOMEWORK_PAGE(h.wlkcid, h.xszyid),
122
+ deadline: h.jzsj,
123
+ lateSubmissionDeadline: h.bjjzsj ? h.bjjzsj : undefined,
124
+ isLateSubmission: h.sfbj === YES,
125
+ completionType: h.zywcfs,
126
+ submissionType: h.zytjfs,
127
+ submitUrl: URLS.LEARN_HOMEWORK_SUBMIT_PAGE(h.wlkcid, h.xszyid),
128
+ submitTime: h.scsj === null ? undefined : h.scsj,
129
+ grade: h.cj === null ? undefined : h.cj,
130
+ gradeLevel: GRADE_LEVEL_MAP.get(h.cj),
131
+ graderName: trimAndDefine(h.jsm),
132
+ gradeContent: trimAndDefine(h.pynr),
133
+ gradeTime: h.pysj === null ? undefined : h.pysj,
134
+ isFavorite: h.sfsc === YES,
135
+ favoriteTime: h.scsj === null || h.sfsc !== YES ? undefined : h.scsj,
136
+ comment: h.bznr ?? undefined,
137
+ ...status,
138
+ }) satisfies IHomework,
144
139
  );
145
140
  }
146
141
 
@@ -148,12 +143,10 @@ async function getExcellentHomeworkListByHomework(
148
143
  s: Session,
149
144
  courseID: string,
150
145
  ): Promise<{ [id: string]: ExcellentHomework[] }> {
151
- const json = await (
152
- await s.fetchWithToken(URLS.LEARN_HOMEWORK_LIST_EXCELLENT, {
153
- method: 'POST',
154
- body: URLS.LEARN_PAGE_LIST_FORM_DATA(courseID),
155
- })
156
- ).json();
146
+ const json = await s.fetchJsonWithToken(URLS.LEARN_HOMEWORK_LIST_EXCELLENT, {
147
+ method: 'POST',
148
+ body: URLS.LEARN_PAGE_LIST_FORM_DATA(courseID),
149
+ });
157
150
  if (json.result !== 'success') {
158
151
  return Promise.reject({
159
152
  reason: FailReason.INVALID_RESPONSE,
@@ -163,45 +156,33 @@ async function getExcellentHomeworkListByHomework(
163
156
 
164
157
  const result = (json.object?.aaData ?? []) as any[];
165
158
 
166
- return (
167
- await Promise.all(
168
- result
169
- .map(
170
- (h) =>
171
- ({
172
- id: h.xszyid,
173
- baseId: h.zyid,
174
- title: decodeHTML(h.bt),
175
- url: URLS.LEARN_HOMEWORK_EXCELLENT_PAGE(h.wlkcid, h.xszyid),
176
- completionType: h.zywcfs,
177
- author: {
178
- id: h.cy?.split(' ')?.[0],
179
- name: h.cy?.split(' ')?.[1],
180
- anonymous: h.sfzm === YES,
181
- },
182
- }) satisfies IExcellentHomework,
183
- )
184
- .map(
185
- async (h) =>
186
- ({
187
- ...h,
188
- ...(await parseHomeworkAtUrl(s, h.url)),
189
- ...(await getHomeworkDetail(s, h.baseId)),
190
- }) satisfies ExcellentHomework,
191
- ),
159
+ return result
160
+ .map(
161
+ (h) =>
162
+ ({
163
+ id: h.xszyid,
164
+ baseId: h.zyid,
165
+ title: decodeHTML(h.bt),
166
+ url: URLS.LEARN_HOMEWORK_EXCELLENT_PAGE(h.wlkcid, h.xszyid),
167
+ completionType: h.zywcfs,
168
+ author: {
169
+ id: h.cy?.split(' ')?.[0],
170
+ name: h.cy?.split(' ')?.[1],
171
+ anonymous: h.sfzm === YES,
172
+ },
173
+ }) satisfies IExcellentHomework,
192
174
  )
193
- ).reduce<{ [id: string]: ExcellentHomework[] }>((acc, cur) => {
194
- if (!acc[cur.baseId]) {
195
- acc[cur.baseId] = [];
196
- }
197
- acc[cur.baseId].push(cur);
198
- return acc;
199
- }, {});
175
+ .reduce<{ [id: string]: ExcellentHomework[] }>((acc, cur) => {
176
+ if (!acc[cur.baseId]) {
177
+ acc[cur.baseId] = [];
178
+ }
179
+ acc[cur.baseId].push(cur);
180
+ return acc;
181
+ }, {});
200
182
  }
201
183
 
202
184
  async function parseHomeworkAtUrl(s: Session, url: string): Promise<IHomeworkDetail> {
203
- const response = await s.fetchWithToken(url);
204
- const result = $(await response.text());
185
+ const result = $(await s.fetchTextWithToken(url));
205
186
 
206
187
  const fileDivs = result('div.list.fujian.clearfix');
207
188
 
@@ -216,13 +197,26 @@ async function parseHomeworkAtUrl(s: Session, url: string): Promise<IHomeworkDet
216
197
  };
217
198
  }
218
199
 
219
- async function getHomeworkDetail(s: Session, baseId: string): Promise<IHomeworkDetail> {
220
- const json = await (
221
- await s.fetchWithToken(URLS.LEARN_HOMEWORK_DETAIL, {
222
- method: 'POST',
223
- body: URLS.LEARN_HOMEWORK_DETAIL_FORM_DATA(baseId),
224
- })
225
- ).json();
200
+ /**
201
+ * Get the detail of one homework or excellent homework: its page (answer, submitted content, attachments) and the
202
+ * description JSON (which wins for `description`). 2 requests.
203
+ */
204
+ export async function getHomeworkDetail(
205
+ s: Session,
206
+ homework: Pick<IHomework | IExcellentHomework, 'url' | 'baseId'>,
207
+ ): Promise<IHomeworkDetail> {
208
+ const [page, description] = await Promise.all([
209
+ parseHomeworkAtUrl(s, homework.url),
210
+ getHomeworkDescription(s, homework.baseId),
211
+ ]);
212
+ return { ...page, ...description };
213
+ }
214
+
215
+ async function getHomeworkDescription(s: Session, baseId: string): Promise<IHomeworkDetail> {
216
+ const json = await s.fetchJsonWithToken(URLS.LEARN_HOMEWORK_DETAIL, {
217
+ method: 'POST',
218
+ body: URLS.LEARN_HOMEWORK_DETAIL_FORM_DATA(baseId),
219
+ });
226
220
  if (json.result !== 'success') {
227
221
  throw {
228
222
  reason: FailReason.INVALID_RESPONSE,
@@ -236,18 +230,19 @@ async function getHomeworkDetail(s: Session, baseId: string): Promise<IHomeworkD
236
230
 
237
231
  function parseHomeworkFile(s: Session, fileDiv: Element): RemoteFile | undefined {
238
232
  const fileNode = $(fileDiv)('.ftitle').children('a')[0] ?? $(fileDiv)('.fl').children('a')[0];
239
- if (fileNode !== undefined) {
240
- const size = trimAndDefine($(fileDiv)('.fl > span[class^="color"]').first().text())!;
241
- const params = new URLSearchParams(fileNode.attribs.href.split('?').slice(-1)[0]);
233
+ const href = fileNode?.attribs.href;
234
+ if (fileNode !== undefined && href) {
235
+ const size = trimAndDefine($(fileDiv)('.fl > span[class^="color"]').first().text()) ?? '';
236
+ const params = new URLSearchParams(href.split('?').slice(-1)[0]);
242
237
  const attachmentId = params.get('fileId')!;
243
238
  // so dirty here...
244
- let downloadUrl = URLS.LEARN_PREFIX + fileNode.attribs.href;
239
+ let downloadUrl = URLS.LEARN_PREFIX + href;
245
240
  if (params.has('downloadUrl')) {
246
241
  downloadUrl = URLS.LEARN_PREFIX + params.get('downloadUrl')!;
247
242
  }
248
243
  return {
249
244
  id: attachmentId,
250
- name: (fileNode.children[0] as { data: string }).data,
245
+ name: (fileNode.children[0] as { data?: string } | undefined)?.data ?? '',
251
246
  downloadUrl,
252
247
  previewUrl: URLS.LEARN_FILE_PREVIEW(ContentType.HOMEWORK, attachmentId, CourseType.STUDENT, s.previewFirstPage),
253
248
  size,
@@ -264,12 +259,12 @@ export async function submitHomework(
264
259
  attachment?: IHomeworkSubmitAttachment,
265
260
  removeAttachment = false,
266
261
  ): Promise<void> {
267
- const json = await (
268
- await s.fetchWithToken(URLS.LEARN_HOMEWORK_SUBMIT(), {
269
- method: 'POST',
270
- body: URLS.LEARN_HOMEWORK_SUBMIT_FORM_DATA(id, content, attachment, removeAttachment),
271
- })
272
- ).json();
262
+ const json = await s.fetchJsonWithToken(URLS.LEARN_HOMEWORK_SUBMIT(), {
263
+ method: 'POST',
264
+ body: URLS.LEARN_HOMEWORK_SUBMIT_FORM_DATA(id, content, attachment, removeAttachment),
265
+ // the response only starts after the whole upload, which can take longer than `requestTimeout`
266
+ timeout: attachment ? 0 : undefined,
267
+ });
273
268
  if (json.result !== 'success') {
274
269
  return Promise.reject({
275
270
  reason: FailReason.OPERATION_FAILED,
package/src/index.ts CHANGED
@@ -10,8 +10,16 @@ import * as notices from './notices.ts';
10
10
  import * as questionnaires from './questionnaires.ts';
11
11
  import { Session } from './request.ts';
12
12
  import type {
13
+ AllContentsOptions,
14
+ AllContentsResult,
13
15
  CalendarOptions,
14
16
  CalendarResult,
17
+ CourseListOptions,
18
+ ExcellentHomework,
19
+ HomeworkListOptions,
20
+ IHomeworkDetail,
21
+ NotificationListOptions,
22
+ RemoteFile,
15
23
  CommentItem,
16
24
  CourseInfo,
17
25
  Discussion,
@@ -26,11 +34,14 @@ import type {
26
34
  Notification,
27
35
  Question,
28
36
  Questionnaire,
37
+ QuestionnaireDetail,
38
+ QuestionnaireListOptions,
29
39
  SemesterInfo,
30
40
  UserInfo,
31
41
  ContentType,
32
42
  } from './types.ts';
33
43
  import { CourseType } from './types.ts';
44
+ import * as URLS from './urls.ts';
34
45
  import * as user from './user.ts';
35
46
 
36
47
  export { addCSRFTokenToUrl } from './request.ts';
@@ -42,7 +53,7 @@ export class Learn2018Helper {
42
53
 
43
54
  /** you can provide a CookieJar and / or CredentialProvider in the configuration */
44
55
  constructor(config?: HelperConfig) {
45
- this.#session = new Session(config, () => this.login());
56
+ this.#session = new Session(config, () => auth.login(this.#session));
46
57
  }
47
58
 
48
59
  /** whether preview URLs open the first page only (`HelperConfig.generatePreviewUrlForFirstPage`) */
@@ -78,7 +89,11 @@ export class Learn2018Helper {
78
89
  return auth.getRoamingTicket(this.#session, username, password, fingerPrint, fingerGenPrint, fingerGenPrint3);
79
90
  }
80
91
 
81
- /** login is necessary if you do not provide a `CredentialProvider` */
92
+ /**
93
+ * Login is necessary if you do not provide a `CredentialProvider`.
94
+ * Without credentials it uses the provider, and concurrent calls share one SSO login. Logins never overlap: one with
95
+ * credentials waits for a login in progress, and requests that find the session expired meanwhile wait for it.
96
+ */
82
97
  public async login(
83
98
  username?: string,
84
99
  password?: string,
@@ -86,7 +101,19 @@ export class Learn2018Helper {
86
101
  fingerGenPrint?: string,
87
102
  fingerGenPrint3?: string,
88
103
  ): Promise<void> {
89
- return auth.login(this.#session, username, password, fingerPrint, fingerGenPrint, fingerGenPrint3);
104
+ if (!username || !password || !fingerPrint) return this.#session.relogin();
105
+ return this.#session.runLogin(() =>
106
+ auth.login(this.#session, username, password, fingerPrint, fingerGenPrint, fingerGenPrint3),
107
+ );
108
+ }
109
+
110
+ /**
111
+ * Make sure the session is usable without a full SSO login when possible: with a CSRF token (from an earlier login
112
+ * or `setCSRFToken`), make one cheap request and log in again only if the session has expired; without one, log in.
113
+ * Use it when resuming an app instead of `login()`.
114
+ */
115
+ public async ensureSession(): Promise<void> {
116
+ return this.#session.ensureSession(URLS.LEARN_CURRENT_SEMESTER());
90
117
  }
91
118
 
92
119
  /** logout (to make everyone happy) */
@@ -102,7 +129,8 @@ export class Learn2018Helper {
102
129
  /**
103
130
  * Get calendar items during the specified period (in yyyymmdd format, both inclusive).
104
131
  * Ranges longer than 28 days are fetched in windows and merged; windows that fail even after a retry are listed in
105
- * `failedRanges`. Rejects only when every window fails (usually with `FailReason.INVALID_RESPONSE`).
132
+ * `failedRanges`. Rejects only when every window fails (usually with `FailReason.INVALID_RESPONSE`), or with a
133
+ * `RangeError` for a date that is not a valid yyyymmdd.
106
134
  */
107
135
  public async getCalendar(startDate: string, endDate: string, options?: CalendarOptions): Promise<CalendarResult> {
108
136
  return calendar.getCalendar(this.#session, startDate, endDate, options);
@@ -116,34 +144,56 @@ export class Learn2018Helper {
116
144
  return courses.getCurrentSemester(this.#session);
117
145
  }
118
146
 
119
- /** get all courses in the specified semester */
147
+ /**
148
+ * Get all courses in the specified semester.
149
+ * `timeAndLocation` is only fetched (one request per course) with `options.timeAndLocation`.
150
+ */
120
151
  public async getCourseList(
121
152
  semesterID: string,
122
153
  courseType: CourseType = CourseType.STUDENT,
123
154
  lang: Language = this.#session.lang,
155
+ options?: CourseListOptions,
124
156
  ): Promise<CourseInfo[]> {
125
- return courses.getCourseList(this.#session, semesterID, courseType, lang);
157
+ return courses.getCourseList(this.#session, semesterID, courseType, lang, options);
158
+ }
159
+
160
+ /** Get the time and location of one course. */
161
+ public async getCourseTimeAndLocation(courseID: string): Promise<string[]> {
162
+ return courses.getCourseTimeAndLocation(this.#session, courseID);
126
163
  }
127
164
 
128
165
  /**
129
166
  * Get certain type of content of all specified courses.
130
- * It actually wraps around other `getXXX` functions. You can ignore the failure caused by certain courses.
167
+ * It wraps around the `getXXX` list functions; a course that fails is reported in `errors` instead of failing
168
+ * the rest. `options.details` is passed on as homework `details` or notification `attachments`.
131
169
  */
132
170
  public async getAllContents<T extends ContentType>(
133
171
  courseIDs: string[],
134
172
  type: T,
135
- courseType: CourseType = CourseType.STUDENT,
136
- allowFailure = false,
137
- ) {
138
- return contents.getAllContents(this.#session, courseIDs, type, courseType, allowFailure);
173
+ options?: AllContentsOptions<T>,
174
+ ): Promise<AllContentsResult<T>> {
175
+ return contents.getAllContents(this.#session, courseIDs, type, options);
139
176
  }
140
177
 
141
- /** Get all notifications (课程公告) of the specified course. */
178
+ /**
179
+ * Get all notifications (课程公告) of the specified course.
180
+ * Attachments are only fetched (one request per notice) for the notices `options.attachments` selects;
181
+ * use `getNotificationAttachment` for the others.
182
+ */
142
183
  public async getNotificationList(
143
184
  courseID: string,
144
185
  courseType: CourseType = CourseType.STUDENT,
186
+ options?: NotificationListOptions,
145
187
  ): Promise<Notification[]> {
146
- return notices.getNotificationList(this.#session, courseID, courseType);
188
+ return notices.getNotificationList(this.#session, courseID, courseType, options);
189
+ }
190
+
191
+ /** Get the attachment of a notice (one request), `undefined` if it has none. */
192
+ public async getNotificationAttachment(
193
+ notification: Pick<Notification, 'url' | 'attachmentName'>,
194
+ courseType: CourseType = CourseType.STUDENT,
195
+ ): Promise<RemoteFile | undefined> {
196
+ return notices.getNotificationAttachment(this.#session, notification, courseType);
147
197
  }
148
198
 
149
199
  /** Get all files (课程文件) of the specified course. */
@@ -171,17 +221,35 @@ export class Learn2018Helper {
171
221
  return files.getFileListByCategory(this.#session, courseID, categoryId, courseType);
172
222
  }
173
223
 
174
- /** Get all homeworks (课程作业) of the specified course. */
175
- public async getHomeworkList(courseID: string): Promise<Homework[]>;
176
- public async getHomeworkList(courseID: string, courseType: CourseType.STUDENT): Promise<Homework[]>;
224
+ /**
225
+ * Get all homeworks (课程作业) of the specified course, with the excellent homework list (without details).
226
+ * Details (page + description, two requests per homework) are only fetched for the homework `options.details`
227
+ * selects; use `getHomeworkDetail` for the others.
228
+ */
229
+ public async getHomeworkList(
230
+ courseID: string,
231
+ courseType?: CourseType.STUDENT,
232
+ options?: HomeworkListOptions,
233
+ ): Promise<Homework[]>;
177
234
  public async getHomeworkList(courseID: string, courseType: CourseType.TEACHER): Promise<HomeworkTA[]>;
178
235
  public async getHomeworkList(
179
236
  courseID: string,
180
237
  courseType: CourseType = CourseType.STUDENT,
238
+ options?: HomeworkListOptions,
181
239
  ): Promise<Homework[] | HomeworkTA[]> {
182
240
  return courseType === CourseType.TEACHER
183
241
  ? homework.getHomeworkList(this.#session, courseID, CourseType.TEACHER)
184
- : homework.getHomeworkList(this.#session, courseID, CourseType.STUDENT);
242
+ : homework.getHomeworkList(this.#session, courseID, CourseType.STUDENT, options);
243
+ }
244
+
245
+ /** Get the detail (page content, attachments and description) of one homework. */
246
+ public async getHomeworkDetail(item: Pick<Homework, 'url' | 'baseId'>): Promise<IHomeworkDetail> {
247
+ return homework.getHomeworkDetail(this.#session, item);
248
+ }
249
+
250
+ /** Get the detail (page content, attachments and description) of one excellent homework. */
251
+ public async getExcellentHomeworkDetail(item: Pick<ExcellentHomework, 'url' | 'baseId'>): Promise<IHomeworkDetail> {
252
+ return homework.getHomeworkDetail(this.#session, item);
185
253
  }
186
254
 
187
255
  /** Get all discussions (课程讨论) of the specified course. */
@@ -202,9 +270,16 @@ export class Learn2018Helper {
202
270
 
203
271
  /**
204
272
  * Get all questionnaires (课程问卷/QNR) of the specified course.
273
+ * Questions (`detail`, one request per questionnaire) for the questionnaires `options.details` selects, by default
274
+ * all; use `getQuestionnaireDetail` for the others.
205
275
  */
206
- public async getQuestionnaireList(courseID: string): Promise<Questionnaire[]> {
207
- return questionnaires.getQuestionnaireList(this.#session, courseID);
276
+ public async getQuestionnaireList(courseID: string, options?: QuestionnaireListOptions): Promise<Questionnaire[]> {
277
+ return questionnaires.getQuestionnaireList(this.#session, courseID, options);
278
+ }
279
+
280
+ /** Get the questions of one questionnaire (one request). */
281
+ public async getQuestionnaireDetail(courseID: string, questionnaireID: string): Promise<QuestionnaireDetail[]> {
282
+ return questionnaires.getQuestionnaireDetail(this.#session, courseID, questionnaireID);
208
283
  }
209
284
 
210
285
  /**