@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/index.ts
CHANGED
|
@@ -11,38 +11,45 @@ import * as questionnaires from './questionnaires.ts';
|
|
|
11
11
|
import { Session } from './request.ts';
|
|
12
12
|
import type {
|
|
13
13
|
AllContentsOptions,
|
|
14
|
+
Credential,
|
|
14
15
|
AllContentsResult,
|
|
15
16
|
CalendarOptions,
|
|
16
17
|
CalendarResult,
|
|
17
|
-
CourseListOptions,
|
|
18
|
-
ExcellentHomework,
|
|
19
|
-
HomeworkListOptions,
|
|
20
|
-
IHomeworkDetail,
|
|
21
|
-
NotificationListOptions,
|
|
22
|
-
RemoteFile,
|
|
23
18
|
CommentItem,
|
|
19
|
+
ContentType,
|
|
20
|
+
CourseFile,
|
|
24
21
|
CourseInfo,
|
|
22
|
+
CourseListOptions,
|
|
23
|
+
CourseNotification,
|
|
25
24
|
Discussion,
|
|
25
|
+
ExcellentHomework,
|
|
26
26
|
FavoriteItem,
|
|
27
|
-
File,
|
|
28
27
|
FileCategory,
|
|
29
28
|
HelperConfig,
|
|
30
29
|
Homework,
|
|
30
|
+
HomeworkDetail,
|
|
31
|
+
HomeworkListOptions,
|
|
32
|
+
HomeworkSubmitAttachment,
|
|
33
|
+
HomeworkSubmitRequest,
|
|
31
34
|
HomeworkTA,
|
|
32
|
-
IHomeworkSubmitAttachment,
|
|
33
35
|
Language,
|
|
34
|
-
|
|
36
|
+
NotificationListOptions,
|
|
35
37
|
Question,
|
|
36
38
|
Questionnaire,
|
|
39
|
+
QuestionnaireDetail,
|
|
40
|
+
QuestionnaireListOptions,
|
|
41
|
+
RemoteFile,
|
|
37
42
|
SemesterInfo,
|
|
38
43
|
UserInfo,
|
|
39
|
-
ContentType,
|
|
40
44
|
} from './types.ts';
|
|
41
|
-
import { CourseType } from './types.ts';
|
|
45
|
+
import { CourseType, FailReason } from './types.ts';
|
|
46
|
+
import { apiError } from './utils.ts';
|
|
42
47
|
import * as URLS from './urls.ts';
|
|
43
48
|
import * as user from './user.ts';
|
|
44
49
|
|
|
45
|
-
export { addCSRFTokenToUrl } from './request.ts';
|
|
50
|
+
export { addCSRFTokenToUrl, isConnectionError } from './request.ts';
|
|
51
|
+
export { checkHomeworkSubmitResponse, homeworkContentHtml } from './homework.ts';
|
|
52
|
+
export { CLASS_PERIOD_TIMES, parseCourseMeeting, parseWeeks, semesterWeek } from './timetable.ts';
|
|
46
53
|
export * from './types.ts';
|
|
47
54
|
|
|
48
55
|
/** the main helper class */
|
|
@@ -51,7 +58,7 @@ export class Learn2018Helper {
|
|
|
51
58
|
|
|
52
59
|
/** you can provide a CookieJar and / or CredentialProvider in the configuration */
|
|
53
60
|
constructor(config?: HelperConfig) {
|
|
54
|
-
this.#session = new Session(config, () => auth.
|
|
61
|
+
this.#session = new Session(config, () => auth.loginWithProvider(this.#session));
|
|
55
62
|
}
|
|
56
63
|
|
|
57
64
|
/** whether preview URLs open the first page only (`HelperConfig.generatePreviewUrlForFirstPage`) */
|
|
@@ -74,32 +81,25 @@ export class Learn2018Helper {
|
|
|
74
81
|
}
|
|
75
82
|
|
|
76
83
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
84
|
+
* Sign in to id.tsinghua.edu.cn and return the Web Learning roaming ticket, without logging in to Web Learning.
|
|
85
|
+
* Rejects with `NO_CREDENTIAL` unless `credential` has a username, a password and a fingerprint. If using
|
|
86
|
+
* alternative cookie management systems, be sure to clear id.tsinghua.edu.cn cookies before calling this function.
|
|
79
87
|
*/
|
|
80
|
-
public async getRoamingTicket(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
fingerPrint: string,
|
|
84
|
-
fingerGenPrint: string = '',
|
|
85
|
-
fingerGenPrint3: string = '',
|
|
86
|
-
): Promise<string> {
|
|
87
|
-
return auth.getRoamingTicket(this.#session, username, password, fingerPrint, fingerGenPrint, fingerGenPrint3);
|
|
88
|
+
public async getRoamingTicket(credential: Credential): Promise<string> {
|
|
89
|
+
if (!auth.isComplete(credential)) throw apiError(FailReason.NO_CREDENTIAL);
|
|
90
|
+
return auth.getRoamingTicket(this.#session, credential);
|
|
88
91
|
}
|
|
89
92
|
|
|
90
93
|
/**
|
|
91
94
|
* Login is necessary if you do not provide a `CredentialProvider`.
|
|
92
|
-
* Without
|
|
95
|
+
* Without a credential it uses the provider, and concurrent calls share one SSO login; a credential without a
|
|
96
|
+
* username, a password and a fingerprint rejects with `NO_CREDENTIAL`. Logins never overlap: one with a credential
|
|
97
|
+
* waits for a login in progress, and requests that find the session expired meanwhile wait for it.
|
|
93
98
|
*/
|
|
94
|
-
public async login(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
fingerGenPrint?: string,
|
|
99
|
-
fingerGenPrint3?: string,
|
|
100
|
-
): Promise<void> {
|
|
101
|
-
if (!username || !password || !fingerPrint) return this.#session.relogin();
|
|
102
|
-
return auth.login(this.#session, username, password, fingerPrint, fingerGenPrint, fingerGenPrint3);
|
|
99
|
+
public async login(credential?: Credential): Promise<void> {
|
|
100
|
+
if (credential === undefined) return this.#session.relogin();
|
|
101
|
+
if (!auth.isComplete(credential)) throw apiError(FailReason.NO_CREDENTIAL);
|
|
102
|
+
return this.#session.runLogin(() => auth.login(this.#session, credential));
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
/**
|
|
@@ -108,7 +108,7 @@ export class Learn2018Helper {
|
|
|
108
108
|
* Use it when resuming an app instead of `login()`.
|
|
109
109
|
*/
|
|
110
110
|
public async ensureSession(): Promise<void> {
|
|
111
|
-
return this.#session.ensureSession(URLS.LEARN_CURRENT_SEMESTER
|
|
111
|
+
return this.#session.ensureSession(URLS.LEARN_CURRENT_SEMESTER);
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
/** logout (to make everyone happy) */
|
|
@@ -123,8 +123,11 @@ export class Learn2018Helper {
|
|
|
123
123
|
|
|
124
124
|
/**
|
|
125
125
|
* Get calendar items during the specified period (in yyyymmdd format, both inclusive).
|
|
126
|
-
* Ranges longer than 28 days are fetched in windows and merged; windows that fail
|
|
127
|
-
*
|
|
126
|
+
* Ranges longer than 28 days are fetched in windows and merged; windows that still fail are listed in `failedRanges`
|
|
127
|
+
* (a timed-out window is not retried). Rejects when every window fails (usually with `FailReason.INVALID_RESPONSE`),
|
|
128
|
+
* with `FailReason.ERROR_REGISTRAR_AUTH` when signing in to the registrar fails, or with a `RangeError` for a date
|
|
129
|
+
* that is not a valid yyyymmdd. With a `CredentialProvider`, a sign-in that id.tsinghua.edu.cn turns down (its session
|
|
130
|
+
* from the last login expired) logs in again and is tried once more.
|
|
128
131
|
*/
|
|
129
132
|
public async getCalendar(startDate: string, endDate: string, options?: CalendarOptions): Promise<CalendarResult> {
|
|
130
133
|
return calendar.getCalendar(this.#session, startDate, endDate, options);
|
|
@@ -140,7 +143,8 @@ export class Learn2018Helper {
|
|
|
140
143
|
|
|
141
144
|
/**
|
|
142
145
|
* Get all courses in the specified semester.
|
|
143
|
-
* `timeAndLocation` is only fetched (one request per course) with `options.timeAndLocation
|
|
146
|
+
* `timeAndLocation` is only fetched (one request per course) with `options.timeAndLocation`, and left out for a
|
|
147
|
+
* course whose request failed.
|
|
144
148
|
*/
|
|
145
149
|
public async getCourseList(
|
|
146
150
|
semesterID: string,
|
|
@@ -178,20 +182,20 @@ export class Learn2018Helper {
|
|
|
178
182
|
courseID: string,
|
|
179
183
|
courseType: CourseType = CourseType.STUDENT,
|
|
180
184
|
options?: NotificationListOptions,
|
|
181
|
-
): Promise<
|
|
185
|
+
): Promise<CourseNotification[]> {
|
|
182
186
|
return notices.getNotificationList(this.#session, courseID, courseType, options);
|
|
183
187
|
}
|
|
184
188
|
|
|
185
189
|
/** Get the attachment of a notice (one request), `undefined` if it has none. */
|
|
186
190
|
public async getNotificationAttachment(
|
|
187
|
-
notification: Pick<
|
|
191
|
+
notification: Pick<CourseNotification, 'url' | 'attachmentName'>,
|
|
188
192
|
courseType: CourseType = CourseType.STUDENT,
|
|
189
193
|
): Promise<RemoteFile | undefined> {
|
|
190
194
|
return notices.getNotificationAttachment(this.#session, notification, courseType);
|
|
191
195
|
}
|
|
192
196
|
|
|
193
197
|
/** Get all files (课程文件) of the specified course. */
|
|
194
|
-
public async getFileList(courseID: string, courseType: CourseType = CourseType.STUDENT): Promise<
|
|
198
|
+
public async getFileList(courseID: string, courseType: CourseType = CourseType.STUDENT): Promise<CourseFile[]> {
|
|
195
199
|
return files.getFileList(this.#session, courseID, courseType);
|
|
196
200
|
}
|
|
197
201
|
|
|
@@ -211,7 +215,7 @@ export class Learn2018Helper {
|
|
|
211
215
|
courseID: string,
|
|
212
216
|
categoryId: string,
|
|
213
217
|
courseType: CourseType = CourseType.STUDENT,
|
|
214
|
-
): Promise<
|
|
218
|
+
): Promise<CourseFile[]> {
|
|
215
219
|
return files.getFileListByCategory(this.#session, courseID, categoryId, courseType);
|
|
216
220
|
}
|
|
217
221
|
|
|
@@ -220,10 +224,9 @@ export class Learn2018Helper {
|
|
|
220
224
|
* Details (page + description, two requests per homework) are only fetched for the homework `options.details`
|
|
221
225
|
* selects; use `getHomeworkDetail` for the others.
|
|
222
226
|
*/
|
|
223
|
-
public async getHomeworkList(courseID: string): Promise<Homework[]>;
|
|
224
227
|
public async getHomeworkList(
|
|
225
228
|
courseID: string,
|
|
226
|
-
courseType
|
|
229
|
+
courseType?: CourseType.STUDENT,
|
|
227
230
|
options?: HomeworkListOptions,
|
|
228
231
|
): Promise<Homework[]>;
|
|
229
232
|
public async getHomeworkList(courseID: string, courseType: CourseType.TEACHER): Promise<HomeworkTA[]>;
|
|
@@ -238,12 +241,12 @@ export class Learn2018Helper {
|
|
|
238
241
|
}
|
|
239
242
|
|
|
240
243
|
/** Get the detail (page content, attachments and description) of one homework. */
|
|
241
|
-
public async getHomeworkDetail(item: Pick<Homework, 'url' | 'baseId'>): Promise<
|
|
244
|
+
public async getHomeworkDetail(item: Pick<Homework, 'url' | 'baseId'>): Promise<HomeworkDetail> {
|
|
242
245
|
return homework.getHomeworkDetail(this.#session, item);
|
|
243
246
|
}
|
|
244
247
|
|
|
245
248
|
/** Get the detail (page content, attachments and description) of one excellent homework. */
|
|
246
|
-
public async getExcellentHomeworkDetail(item: Pick<ExcellentHomework, 'url' | 'baseId'>): Promise<
|
|
249
|
+
public async getExcellentHomeworkDetail(item: Pick<ExcellentHomework, 'url' | 'baseId'>): Promise<HomeworkDetail> {
|
|
247
250
|
return homework.getHomeworkDetail(this.#session, item);
|
|
248
251
|
}
|
|
249
252
|
|
|
@@ -265,9 +268,16 @@ export class Learn2018Helper {
|
|
|
265
268
|
|
|
266
269
|
/**
|
|
267
270
|
* Get all questionnaires (课程问卷/QNR) of the specified course.
|
|
271
|
+
* Questions (`detail`, one request per questionnaire) for the questionnaires `options.details` selects, by default
|
|
272
|
+
* all; use `getQuestionnaireDetail` for the others.
|
|
268
273
|
*/
|
|
269
|
-
public async getQuestionnaireList(courseID: string): Promise<Questionnaire[]> {
|
|
270
|
-
return questionnaires.getQuestionnaireList(this.#session, courseID);
|
|
274
|
+
public async getQuestionnaireList(courseID: string, options?: QuestionnaireListOptions): Promise<Questionnaire[]> {
|
|
275
|
+
return questionnaires.getQuestionnaireList(this.#session, courseID, options);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Get the questions of one questionnaire (one request). */
|
|
279
|
+
public async getQuestionnaireDetail(courseID: string, questionnaireID: string): Promise<QuestionnaireDetail[]> {
|
|
280
|
+
return questionnaires.getQuestionnaireDetail(this.#session, courseID, questionnaireID);
|
|
271
281
|
}
|
|
272
282
|
|
|
273
283
|
/**
|
|
@@ -326,15 +336,36 @@ export class Learn2018Helper {
|
|
|
326
336
|
return courses.sortCourses(this.#session, courseIDs);
|
|
327
337
|
}
|
|
328
338
|
|
|
339
|
+
/**
|
|
340
|
+
* Submit homework `id` (the `studentHomeworkId`), the way the website's submit page does. `content` is the whole
|
|
341
|
+
* answer as HTML (the website sends its editor's HTML) and replaces the submitted one, so pass the current answer
|
|
342
|
+
* when only changing the attachment (`homeworkContentHtml` turns plain text into the editor's HTML). With no
|
|
343
|
+
* `attachment` the existing one is kept, unless `removeAttachment`: that only deletes the attachment, and `content` is
|
|
344
|
+
* ignored. In React Native, pass the attachment's `content` as `{ uri, type }`.
|
|
345
|
+
*/
|
|
329
346
|
public async submitHomework(
|
|
330
347
|
id: string,
|
|
331
348
|
content = '',
|
|
332
|
-
attachment?:
|
|
349
|
+
attachment?: HomeworkSubmitAttachment,
|
|
333
350
|
removeAttachment = false,
|
|
334
351
|
): Promise<void> {
|
|
335
352
|
return homework.submitHomework(this.#session, id, content, attachment, removeAttachment);
|
|
336
353
|
}
|
|
337
354
|
|
|
355
|
+
/**
|
|
356
|
+
* The request `submitHomework` would send, for sending with another HTTP client (e.g. to show upload progress, which
|
|
357
|
+
* `fetch` can't): POST `body` to `url`, then pass the answer (parsed JSON or text) to `checkHomeworkSubmitResponse`.
|
|
358
|
+
* It checks the session first (`ensureSession`), since that request doesn't log in again and retry.
|
|
359
|
+
*/
|
|
360
|
+
public async getHomeworkSubmitRequest(
|
|
361
|
+
id: string,
|
|
362
|
+
content = '',
|
|
363
|
+
attachment?: HomeworkSubmitAttachment,
|
|
364
|
+
removeAttachment = false,
|
|
365
|
+
): Promise<HomeworkSubmitRequest> {
|
|
366
|
+
return homework.getHomeworkSubmitRequest(this.#session, id, content, attachment, removeAttachment);
|
|
367
|
+
}
|
|
368
|
+
|
|
338
369
|
public async setLanguage(lang: Language): Promise<void> {
|
|
339
370
|
return user.setLanguage(this.#session, lang);
|
|
340
371
|
}
|
package/src/notices.ts
CHANGED
|
@@ -3,11 +3,19 @@
|
|
|
3
3
|
import { Base64 } from 'js-base64';
|
|
4
4
|
|
|
5
5
|
import type { Session } from './request.ts';
|
|
6
|
-
import {
|
|
7
|
-
import type { INotification, Notification, NotificationListOptions, RemoteFile } from './types.ts';
|
|
6
|
+
import type { CourseNotification, CourseNotificationBase, NotificationListOptions, RemoteFile } from './types.ts';
|
|
8
7
|
import { ContentType, CourseType, FailReason } from './types.ts';
|
|
9
8
|
import * as URLS from './urls.ts';
|
|
10
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
$,
|
|
11
|
+
YES,
|
|
12
|
+
decodeHTML,
|
|
13
|
+
expectSuccess,
|
|
14
|
+
isSelected,
|
|
15
|
+
toRequiredServerTime,
|
|
16
|
+
toServerTime,
|
|
17
|
+
trimAndDefine,
|
|
18
|
+
} from './utils.ts';
|
|
11
19
|
|
|
12
20
|
/**
|
|
13
21
|
* Get all notifications (课程公告) of the specified course: current and expired lists.
|
|
@@ -18,13 +26,13 @@ export async function getNotificationList(
|
|
|
18
26
|
courseID: string,
|
|
19
27
|
courseType: CourseType = CourseType.STUDENT,
|
|
20
28
|
options: NotificationListOptions = {},
|
|
21
|
-
): Promise<
|
|
29
|
+
): Promise<CourseNotification[]> {
|
|
22
30
|
const lists = await Promise.all([
|
|
23
31
|
getNotificationListKind(s, courseID, courseType, false),
|
|
24
32
|
getNotificationListKind(s, courseID, courseType, true),
|
|
25
33
|
]);
|
|
26
34
|
return Promise.all(
|
|
27
|
-
lists.flat().map(async (n): Promise<
|
|
35
|
+
lists.flat().map(async (n): Promise<CourseNotification> => {
|
|
28
36
|
if (!n.attachmentName || !isSelected(options.attachments, n)) return n;
|
|
29
37
|
return { ...n, attachment: await getNotificationAttachment(s, n, courseType) };
|
|
30
38
|
}),
|
|
@@ -36,19 +44,12 @@ async function getNotificationListKind(
|
|
|
36
44
|
courseID: string,
|
|
37
45
|
courseType: CourseType,
|
|
38
46
|
expired: boolean,
|
|
39
|
-
): Promise<
|
|
40
|
-
const json = await (
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
).json();
|
|
46
|
-
if (json.result !== 'success') {
|
|
47
|
-
return Promise.reject({
|
|
48
|
-
reason: FailReason.INVALID_RESPONSE,
|
|
49
|
-
extra: json,
|
|
50
|
-
});
|
|
51
|
-
}
|
|
47
|
+
): Promise<CourseNotificationBase[]> {
|
|
48
|
+
const json = await s.fetchJsonWithToken(URLS.LEARN_NOTIFICATION_LIST(courseType, expired), {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
body: URLS.LEARN_PAGE_LIST_FORM_DATA(courseID),
|
|
51
|
+
});
|
|
52
|
+
expectSuccess(json, FailReason.INVALID_RESPONSE);
|
|
52
53
|
|
|
53
54
|
const result = (json.object?.aaData ?? json.object?.resultsList ?? []) as any[];
|
|
54
55
|
|
|
@@ -62,44 +63,30 @@ async function getNotificationListKind(
|
|
|
62
63
|
publisher: n.fbrxm,
|
|
63
64
|
hasRead: n.sfyd === YES,
|
|
64
65
|
markedImportant: Number(n.sfqd) === 1, // n.sfqd could be string '1' (teacher mode) or number 1 (student mode)
|
|
65
|
-
publishTime: n.fbsj && typeof n.fbsj === 'string' ? n.fbsj : n.fbsjStr,
|
|
66
|
-
expireTime: n.jzsj
|
|
66
|
+
publishTime: toRequiredServerTime(n.fbsj && typeof n.fbsj === 'string' ? n.fbsj : n.fbsjStr),
|
|
67
|
+
expireTime: toServerTime(n.jzsj),
|
|
67
68
|
isFavorite: n.sfsc === YES,
|
|
68
69
|
comment: n.bznr ?? undefined,
|
|
69
70
|
attachmentName: typeof attachmentName === 'string' && attachmentName ? attachmentName : undefined,
|
|
70
|
-
} satisfies
|
|
71
|
+
} satisfies CourseNotificationBase;
|
|
71
72
|
});
|
|
72
73
|
}
|
|
73
74
|
|
|
74
75
|
/**
|
|
75
76
|
* Get the attachment of a notice from its detail page (1 request), or `undefined` without a request if
|
|
76
|
-
* `attachmentName` says it has none.
|
|
77
|
+
* `attachmentName` says it has none, or if the page has no attachment link.
|
|
77
78
|
*/
|
|
78
79
|
export async function getNotificationAttachment(
|
|
79
80
|
s: Session,
|
|
80
|
-
notification: Pick<
|
|
81
|
+
notification: Pick<CourseNotificationBase, 'url' | 'attachmentName'>,
|
|
81
82
|
courseType: CourseType = CourseType.STUDENT,
|
|
82
83
|
): Promise<RemoteFile | undefined> {
|
|
83
84
|
if (!notification.attachmentName) return undefined;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
// 'method': 'POST',
|
|
90
|
-
// 'body': postParams,
|
|
91
|
-
// })).json();
|
|
92
|
-
// const attachmentId = metadata.ggfjid as string;
|
|
93
|
-
/// parsed from HTML
|
|
94
|
-
const response = await s.fetchWithToken(notification.url);
|
|
95
|
-
const result = $(await response.text());
|
|
96
|
-
let path = '';
|
|
97
|
-
if (courseType === CourseType.STUDENT) {
|
|
98
|
-
path = result('.ml-10').attr('href')!;
|
|
99
|
-
} else {
|
|
100
|
-
path = result('#wjid').attr('href')!;
|
|
101
|
-
}
|
|
102
|
-
const size = trimAndDefine(result('div#attachment > div.fl > span[class^="color"]').first().text())!;
|
|
85
|
+
const result = $(await s.fetchTextWithToken(notification.url));
|
|
86
|
+
let path = (courseType === CourseType.STUDENT ? result('.ml-10') : result('#wjid')).attr('href');
|
|
87
|
+
// the page has no attachment link (removed since the list was fetched, or the page changed)
|
|
88
|
+
if (!path) return undefined;
|
|
89
|
+
const size = trimAndDefine(result('div#attachment > div.fl > span[class^="color"]').first().text()) ?? '';
|
|
103
90
|
const params = new URLSearchParams(path.split('?').slice(-1)[0]);
|
|
104
91
|
const attachmentId = params.get('wjid')!;
|
|
105
92
|
if (!path.startsWith(URLS.LEARN_PREFIX)) {
|
package/src/questionnaires.ts
CHANGED
|
@@ -1,63 +1,80 @@
|
|
|
1
1
|
// Questionnaires (课程问卷).
|
|
2
2
|
|
|
3
3
|
import type { Session } from './request.ts';
|
|
4
|
-
import {
|
|
5
|
-
import type { Questionnaire, QuestionnaireDetail } from './types.ts';
|
|
4
|
+
import type { Questionnaire, QuestionnaireDetail, QuestionnaireListOptions } from './types.ts';
|
|
6
5
|
import { FailReason, QuestionnaireType } from './types.ts';
|
|
7
6
|
import * as URLS from './urls.ts';
|
|
8
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
QNR_TYPE_MAP,
|
|
9
|
+
YES,
|
|
10
|
+
apiError,
|
|
11
|
+
decodeHTML,
|
|
12
|
+
expectSuccess,
|
|
13
|
+
isSelected,
|
|
14
|
+
toRequiredServerTime,
|
|
15
|
+
toServerTime,
|
|
16
|
+
} from './utils.ts';
|
|
9
17
|
|
|
10
18
|
/**
|
|
11
|
-
* Get all questionnaires (课程问卷/QNR) of the specified course.
|
|
19
|
+
* Get all questionnaires (课程问卷/QNR) of the specified course: ongoing and ended lists.
|
|
20
|
+
* Questions (`detail`, 1 request each) only for the questionnaires `options.details` selects (default `'all'`).
|
|
12
21
|
*/
|
|
13
|
-
export async function getQuestionnaireList(
|
|
14
|
-
|
|
22
|
+
export async function getQuestionnaireList(
|
|
23
|
+
s: Session,
|
|
24
|
+
courseID: string,
|
|
25
|
+
options: QuestionnaireListOptions = {},
|
|
26
|
+
): Promise<Questionnaire[]> {
|
|
27
|
+
const details = options.details ?? 'all';
|
|
28
|
+
const lists = await Promise.all([
|
|
15
29
|
getQuestionnaireListAtUrl(s, courseID, URLS.LEARN_QNR_LIST_ONGOING),
|
|
16
30
|
getQuestionnaireListAtUrl(s, courseID, URLS.LEARN_QNR_LIST_ENDED),
|
|
17
|
-
])
|
|
31
|
+
]);
|
|
32
|
+
return Promise.all(
|
|
33
|
+
lists
|
|
34
|
+
.flat()
|
|
35
|
+
.map(async (q) =>
|
|
36
|
+
isSelected(details, q) ? { ...q, detail: await getQuestionnaireDetail(s, courseID, q.id) } : q,
|
|
37
|
+
),
|
|
38
|
+
);
|
|
18
39
|
}
|
|
19
40
|
|
|
20
41
|
async function getQuestionnaireListAtUrl(s: Session, courseID: string, url: string): Promise<Questionnaire[]> {
|
|
21
|
-
const json = await (
|
|
22
|
-
|
|
23
|
-
).json();
|
|
24
|
-
if (json.result !== 'success') {
|
|
25
|
-
return Promise.reject({
|
|
26
|
-
reason: FailReason.INVALID_RESPONSE,
|
|
27
|
-
extra: json,
|
|
28
|
-
});
|
|
29
|
-
}
|
|
42
|
+
const json = await s.fetchJsonWithToken(url, { method: 'POST', body: URLS.LEARN_PAGE_LIST_FORM_DATA(courseID) });
|
|
43
|
+
expectSuccess(json, FailReason.INVALID_RESPONSE);
|
|
30
44
|
const result = (json.object?.aaData ?? []) as any[];
|
|
31
|
-
return
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
} satisfies Questionnaire;
|
|
49
|
-
}),
|
|
50
|
-
);
|
|
45
|
+
return result.map((e) => {
|
|
46
|
+
const type = QNR_TYPE_MAP.get(e.wjlx) ?? QuestionnaireType.SURVEY;
|
|
47
|
+
return {
|
|
48
|
+
id: e.wjid,
|
|
49
|
+
type,
|
|
50
|
+
title: decodeHTML(e.wjbt),
|
|
51
|
+
startTime: toRequiredServerTime(e.kssj),
|
|
52
|
+
endTime: toRequiredServerTime(e.jssj),
|
|
53
|
+
uploadTime: toRequiredServerTime(e.scsj),
|
|
54
|
+
uploaderId: e.scr,
|
|
55
|
+
uploaderName: e.scrxm,
|
|
56
|
+
submitTime: toServerTime(e.tjsj),
|
|
57
|
+
isFavorite: e.sfsc === YES,
|
|
58
|
+
comment: e.bznr ?? undefined,
|
|
59
|
+
url: URLS.LEARN_QNR_SUBMIT_PAGE(e.wlkcid, e.wjid, type),
|
|
60
|
+
} satisfies Questionnaire;
|
|
61
|
+
});
|
|
51
62
|
}
|
|
52
63
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
64
|
+
/** Get the questions of one questionnaire (1 request). */
|
|
65
|
+
export async function getQuestionnaireDetail(
|
|
66
|
+
s: Session,
|
|
67
|
+
courseID: string,
|
|
68
|
+
qnrID: string,
|
|
69
|
+
): Promise<QuestionnaireDetail[]> {
|
|
70
|
+
const json = await s.fetchJsonWithToken(URLS.LEARN_QNR_DETAIL, {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
body: URLS.LEARN_QNR_DETAIL_FORM(courseID, qnrID),
|
|
73
|
+
});
|
|
74
|
+
if (!Array.isArray(json)) {
|
|
75
|
+
throw apiError(FailReason.INVALID_RESPONSE, json);
|
|
76
|
+
}
|
|
77
|
+
return json.map(
|
|
61
78
|
(e) =>
|
|
62
79
|
({
|
|
63
80
|
id: e.wtid,
|
|
@@ -65,7 +82,7 @@ async function getQuestionnaireDetail(s: Session, courseID: string, qnrID: strin
|
|
|
65
82
|
type: e.type,
|
|
66
83
|
required: e.require === YES,
|
|
67
84
|
title: decodeHTML(e.wtbt),
|
|
68
|
-
score: e.wtfz ? Number(e.wtfz) : undefined, //
|
|
85
|
+
score: e.wtfz ? Number(e.wtfz) : undefined, // a number or a string of digits; none when missing or 0
|
|
69
86
|
options: (e.list as any[])?.map((o) => ({
|
|
70
87
|
id: o.xxid,
|
|
71
88
|
index: Number(o.xxbh),
|