@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.
- package/README.md +41 -7
- package/lib/auth.d.ts +1 -1
- package/lib/auth.d.ts.map +1 -1
- package/lib/auth.js +17 -5
- package/lib/auth.js.map +1 -1
- package/lib/calendar.d.ts +6 -4
- package/lib/calendar.d.ts.map +1 -1
- package/lib/calendar.js +21 -8
- package/lib/calendar.js.map +1 -1
- package/lib/contents.d.ts +4 -4
- package/lib/contents.d.ts.map +1 -1
- package/lib/contents.js +28 -20
- package/lib/contents.js.map +1 -1
- package/lib/courses.d.ts +5 -3
- package/lib/courses.d.ts.map +1 -1
- package/lib/courses.js +24 -12
- package/lib/courses.js.map +1 -1
- package/lib/discussions.js +2 -2
- package/lib/discussions.js.map +1 -1
- package/lib/favorites.d.ts.map +1 -1
- package/lib/favorites.js +12 -12
- package/lib/favorites.js.map +1 -1
- package/lib/files.d.ts +1 -1
- package/lib/files.d.ts.map +1 -1
- package/lib/files.js +10 -8
- package/lib/files.js.map +1 -1
- package/lib/homework.d.ts +11 -4
- package/lib/homework.d.ts.map +1 -1
- package/lib/homework.js +46 -43
- package/lib/homework.js.map +1 -1
- package/lib/index.d.ts +47 -13
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +63 -17
- package/lib/index.js.map +1 -1
- package/lib/notices.d.ts +11 -3
- package/lib/notices.d.ts.map +1 -1
- package/lib/notices.js +40 -36
- 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 +20 -12
- package/lib/questionnaires.js.map +1 -1
- package/lib/request.d.ts +66 -10
- package/lib/request.d.ts.map +1 -1
- package/lib/request.js +234 -50
- package/lib/request.js.map +1 -1
- package/lib/types.d.ts +52 -3
- package/lib/types.d.ts.map +1 -1
- package/lib/types.js +1 -0
- package/lib/types.js.map +1 -1
- package/lib/user.js +1 -1
- package/lib/user.js.map +1 -1
- package/lib/utils.d.ts +3 -0
- package/lib/utils.d.ts.map +1 -1
- package/lib/utils.js +11 -3
- package/lib/utils.js.map +1 -1
- package/package.json +2 -2
- package/src/auth.ts +18 -5
- package/src/calendar.ts +28 -10
- package/src/contents.ts +30 -25
- package/src/courses.ts +31 -19
- package/src/discussions.ts +2 -2
- package/src/favorites.ts +22 -32
- package/src/files.ts +12 -12
- package/src/homework.ts +118 -123
- package/src/index.ts +94 -19
- package/src/notices.ts +58 -59
- package/src/questionnaires.ts +51 -37
- package/src/request.ts +260 -48
- package/src/types.ts +62 -2
- package/src/user.ts +1 -1
- 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
|
-
/**
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
75
|
-
|
|
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<
|
|
85
|
-
const json = await (
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
const
|
|
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 +
|
|
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
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
)
|
|
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, () =>
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
|
|
136
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
175
|
-
|
|
176
|
-
|
|
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
|
/**
|