@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/auth.ts
CHANGED
|
@@ -34,6 +34,7 @@ export async function getRoamingTicket(
|
|
|
34
34
|
} as ApiError;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
let page: string;
|
|
37
38
|
try {
|
|
38
39
|
const loginForm = await s.rawFetch(URLS.ID_LOGIN());
|
|
39
40
|
const body = $(await loginForm.text());
|
|
@@ -52,16 +53,27 @@ export async function getRoamingTicket(
|
|
|
52
53
|
body: formData,
|
|
53
54
|
});
|
|
54
55
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
if (!checkResponse.ok) {
|
|
57
|
+
throw { code: checkResponse.status, text: checkResponse.statusText };
|
|
58
|
+
}
|
|
59
|
+
page = await checkResponse.text();
|
|
59
60
|
} catch (err) {
|
|
60
61
|
throw {
|
|
61
62
|
reason: FailReason.ERROR_FETCH_FROM_ID,
|
|
62
63
|
extra: err,
|
|
63
64
|
} as ApiError;
|
|
64
65
|
}
|
|
66
|
+
|
|
67
|
+
// id.tsinghua.edu.cn answers with a link to the roaming URL (with the ticket), or with the form and an error
|
|
68
|
+
const body = $(page);
|
|
69
|
+
const redirectUrl = body('a').attr('href');
|
|
70
|
+
if (!redirectUrl) {
|
|
71
|
+
throw {
|
|
72
|
+
reason: FailReason.BAD_CREDENTIAL,
|
|
73
|
+
extra: body.root().text().replace(/\s+/g, ' ').trim().slice(0, 200),
|
|
74
|
+
} as ApiError;
|
|
75
|
+
}
|
|
76
|
+
return redirectUrl.split('=').slice(-1)[0];
|
|
65
77
|
}
|
|
66
78
|
|
|
67
79
|
/** login is necessary if you do not provide a `CredentialProvider` */
|
|
@@ -115,7 +127,8 @@ export async function login(
|
|
|
115
127
|
if (langMatches.length !== 0) s.lang = langMatches[0][1] === 'en' ? Language.EN : Language.ZH;
|
|
116
128
|
}
|
|
117
129
|
|
|
118
|
-
/** logout (to make everyone happy) */
|
|
130
|
+
/** logout (to make everyone happy); the CSRF token is cleared, so the next request logs in again */
|
|
119
131
|
export async function logout(s: Session): Promise<void> {
|
|
120
132
|
await s.rawFetch(URLS.LEARN_LOGOUT(), { method: 'POST' });
|
|
133
|
+
s.csrfToken = '';
|
|
121
134
|
}
|
package/src/calendar.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Course calendar from the registrar (academic.tsinghua.edu.cn).
|
|
2
2
|
|
|
3
3
|
import type { Session } from './request.ts';
|
|
4
|
-
import type { CalendarEvent, CalendarOptions, CalendarRange, CalendarResult } from './types.ts';
|
|
4
|
+
import type { ApiError, CalendarEvent, CalendarOptions, CalendarRange, CalendarResult } from './types.ts';
|
|
5
5
|
import { FailReason } from './types.ts';
|
|
6
6
|
import * as URLS from './urls.ts';
|
|
7
7
|
import { JSONP_EXTRACTOR_NAME, extractJSONPResult } from './utils.ts';
|
|
@@ -11,11 +11,16 @@ export const CALENDAR_WINDOW_DAYS = 28;
|
|
|
11
11
|
|
|
12
12
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
13
13
|
|
|
14
|
+
/** after this many windows in a row time out, `getCalendar` gives up on the rest (they go to `failedRanges`) */
|
|
15
|
+
const MAX_CONSECUTIVE_TIMEOUTS = 2;
|
|
16
|
+
|
|
17
|
+
const isTimeout = (err: unknown) => (err as ApiError | undefined)?.reason === FailReason.REQUEST_TIMEOUT;
|
|
18
|
+
|
|
14
19
|
const parseDate = (date: string): number => {
|
|
15
20
|
const match = /^(\d{4})(\d{2})(\d{2})$/.exec(date);
|
|
16
21
|
const time = match ? Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3])) : NaN;
|
|
17
22
|
if (Number.isNaN(time) || formatDate(time) !== date) {
|
|
18
|
-
throw
|
|
23
|
+
throw new RangeError(`invalid date ${date}, expected yyyymmdd`);
|
|
19
24
|
}
|
|
20
25
|
return time;
|
|
21
26
|
};
|
|
@@ -60,10 +65,12 @@ async function fetchWindow(s: Session, range: CalendarRange, graduate: boolean):
|
|
|
60
65
|
/**
|
|
61
66
|
* Get calendar items during the specified period (in yyyymmdd format, both inclusive).
|
|
62
67
|
*
|
|
63
|
-
* Ranges longer than `CALENDAR_WINDOW_DAYS` are fetched in consecutive windows (one at a time, each retried once
|
|
64
|
-
* merged in date order. Windows that still fail are listed in `failedRanges`, so callers can
|
|
65
|
-
* retry only those
|
|
66
|
-
*
|
|
68
|
+
* Ranges longer than `CALENDAR_WINDOW_DAYS` are fetched in consecutive windows (one at a time, each retried once
|
|
69
|
+
* unless it timed out) and merged in date order. Windows that still fail are listed in `failedRanges`, so callers can
|
|
70
|
+
* show partial data and retry only those; after two windows in a row time out, the rest are listed there without a
|
|
71
|
+
* request. If every window fails, this rejects with the last error instead (usually `FailReason.INVALID_RESPONSE`).
|
|
72
|
+
* The result might be empty if the period is too far away from now. Throws a `RangeError` for a date that is not a
|
|
73
|
+
* valid yyyymmdd.
|
|
67
74
|
*/
|
|
68
75
|
export async function getCalendar(
|
|
69
76
|
s: Session,
|
|
@@ -75,12 +82,10 @@ export async function getCalendar(
|
|
|
75
82
|
const ranges = splitCalendarRange(startDate, endDate);
|
|
76
83
|
if (ranges.length === 0) return { events: [], failedRanges: [] };
|
|
77
84
|
|
|
78
|
-
|
|
85
|
+
let ticket = await s.fetchTextWithToken(URLS.REGISTRAR_TICKET(), {
|
|
79
86
|
method: 'POST',
|
|
80
87
|
body: URLS.REGISTRAR_TICKET_FORM_DATA(),
|
|
81
88
|
});
|
|
82
|
-
|
|
83
|
-
let ticket = await ticketResponse.text();
|
|
84
89
|
ticket = ticket.substring(1, ticket.length - 1);
|
|
85
90
|
|
|
86
91
|
await s.fetch(URLS.REGISTRAR_AUTH(ticket));
|
|
@@ -88,12 +93,25 @@ export async function getCalendar(
|
|
|
88
93
|
const events: CalendarEvent[] = [];
|
|
89
94
|
const failedRanges: CalendarRange[] = [];
|
|
90
95
|
let lastError: unknown;
|
|
96
|
+
let timeouts = 0;
|
|
91
97
|
for (const range of ranges) {
|
|
98
|
+
// after this many timed-out windows in a row the registrar is taken as down, and the rest are not tried
|
|
99
|
+
if (timeouts >= MAX_CONSECUTIVE_TIMEOUTS) {
|
|
100
|
+
failedRanges.push(range);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
92
103
|
try {
|
|
93
|
-
|
|
104
|
+
// a timed-out window is not retried: the registrar is slow, not flaky, and each try takes `requestTimeout`
|
|
105
|
+
events.push(
|
|
106
|
+
...(await fetchWindow(s, range, graduate).catch((err: unknown) =>
|
|
107
|
+
isTimeout(err) ? Promise.reject(err) : fetchWindow(s, range, graduate),
|
|
108
|
+
)),
|
|
109
|
+
);
|
|
110
|
+
timeouts = 0;
|
|
94
111
|
} catch (e) {
|
|
95
112
|
lastError = e;
|
|
96
113
|
failedRanges.push(range);
|
|
114
|
+
timeouts = isTimeout(e) ? timeouts + 1 : 0;
|
|
97
115
|
}
|
|
98
116
|
}
|
|
99
117
|
|
package/src/contents.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// One kind of content for many courses at once.
|
|
2
2
|
|
|
3
3
|
import type { Session } from './request.ts';
|
|
4
|
-
import type { ContentTypeMap, CourseContent } from './types.ts';
|
|
4
|
+
import type { AllContentsOptions, AllContentsResult, ContentTypeMap, CourseContent } from './types.ts';
|
|
5
5
|
import { ContentType, CourseType, FailReason } from './types.ts';
|
|
6
6
|
import { getAnsweredQuestionList, getDiscussionList } from './discussions.ts';
|
|
7
7
|
import { getFileList } from './files.ts';
|
|
@@ -11,29 +11,42 @@ import { getQuestionnaireList } from './questionnaires.ts';
|
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Get certain type of content of all specified courses.
|
|
14
|
-
* It
|
|
14
|
+
* It wraps around the `getXXX` list functions; one course failing doesn't fail the others: its error is in `errors`.
|
|
15
15
|
*/
|
|
16
16
|
export async function getAllContents<T extends ContentType>(
|
|
17
17
|
s: Session,
|
|
18
18
|
courseIDs: string[],
|
|
19
19
|
type: T,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const fetchContentForCourse = (id: string) => {
|
|
20
|
+
options: AllContentsOptions<T> = {},
|
|
21
|
+
): Promise<AllContentsResult<T>> {
|
|
22
|
+
const courseType = options.courseType ?? CourseType.STUDENT;
|
|
23
|
+
const fetchContentForCourse = (id: string): Promise<ContentTypeMap[T][]> => {
|
|
24
24
|
switch (type) {
|
|
25
25
|
case ContentType.NOTIFICATION:
|
|
26
|
-
return getNotificationList(s, id, courseType
|
|
26
|
+
return getNotificationList(s, id, courseType, {
|
|
27
|
+
attachments: (options as AllContentsOptions<ContentType.NOTIFICATION>).details,
|
|
28
|
+
}) as Promise<ContentTypeMap[T][]>;
|
|
27
29
|
case ContentType.FILE:
|
|
28
30
|
return getFileList(s, id, courseType) as Promise<ContentTypeMap[T][]>;
|
|
29
31
|
case ContentType.HOMEWORK:
|
|
30
|
-
|
|
32
|
+
if (courseType === CourseType.TEACHER) {
|
|
33
|
+
// the teacher list has another shape (`HomeworkTA`); use `getHomeworkList(id, CourseType.TEACHER)`
|
|
34
|
+
return Promise.reject({
|
|
35
|
+
reason: FailReason.NOT_IMPLEMENTED,
|
|
36
|
+
extra: 'homework of a teacher course; use getHomeworkList(courseID, CourseType.TEACHER)',
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return getHomeworkList(s, id, CourseType.STUDENT, {
|
|
40
|
+
details: (options as AllContentsOptions<ContentType.HOMEWORK>).details,
|
|
41
|
+
}) as Promise<ContentTypeMap[T][]>;
|
|
31
42
|
case ContentType.DISCUSSION:
|
|
32
43
|
return getDiscussionList(s, id, courseType) as Promise<ContentTypeMap[T][]>;
|
|
33
44
|
case ContentType.QUESTION:
|
|
34
45
|
return getAnsweredQuestionList(s, id, courseType) as Promise<ContentTypeMap[T][]>;
|
|
35
46
|
case ContentType.QUESTIONNAIRE:
|
|
36
|
-
return getQuestionnaireList(s, id
|
|
47
|
+
return getQuestionnaireList(s, id, {
|
|
48
|
+
details: (options as AllContentsOptions<ContentType.QUESTIONNAIRE>).details,
|
|
49
|
+
}) as Promise<ContentTypeMap[T][]>;
|
|
37
50
|
default:
|
|
38
51
|
return Promise.reject({
|
|
39
52
|
reason: FailReason.NOT_IMPLEMENTED,
|
|
@@ -43,25 +56,17 @@ export async function getAllContents<T extends ContentType>(
|
|
|
43
56
|
};
|
|
44
57
|
|
|
45
58
|
const contents: CourseContent<T> = {};
|
|
59
|
+
const errors: AllContentsResult<T>['errors'] = {};
|
|
46
60
|
|
|
47
|
-
|
|
61
|
+
await Promise.all(
|
|
48
62
|
courseIDs.map(async (id) => {
|
|
49
|
-
|
|
63
|
+
try {
|
|
64
|
+
contents[id] = await fetchContentForCourse(id);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
errors[id] = err;
|
|
67
|
+
}
|
|
50
68
|
}),
|
|
51
69
|
);
|
|
52
70
|
|
|
53
|
-
|
|
54
|
-
for (const r of results) {
|
|
55
|
-
if (r.status === 'rejected') {
|
|
56
|
-
return Promise.reject({
|
|
57
|
-
reason: FailReason.INVALID_RESPONSE,
|
|
58
|
-
extra: {
|
|
59
|
-
reason: r.reason,
|
|
60
|
-
},
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return contents;
|
|
71
|
+
return { contents, errors };
|
|
67
72
|
}
|
package/src/courses.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// Semesters and courses.
|
|
2
2
|
|
|
3
3
|
import type { Session } from './request.ts';
|
|
4
|
-
import type { CourseInfo, SemesterInfo } from './types.ts';
|
|
4
|
+
import type { ApiError, CourseInfo, CourseListOptions, SemesterInfo } from './types.ts';
|
|
5
5
|
import { CourseType, FailReason } from './types.ts';
|
|
6
6
|
import * as URLS from './urls.ts';
|
|
7
7
|
import { decodeHTML, parseSemesterType } from './utils.ts';
|
|
8
8
|
|
|
9
9
|
export async function getSemesterIdList(s: Session): Promise<string[]> {
|
|
10
|
-
const json = await
|
|
10
|
+
const json = await s.fetchJsonWithToken(URLS.LEARN_SEMESTER_LIST());
|
|
11
11
|
if (!Array.isArray(json)) {
|
|
12
12
|
return Promise.reject({
|
|
13
13
|
reason: FailReason.INVALID_RESPONSE,
|
|
@@ -20,7 +20,7 @@ export async function getSemesterIdList(s: Session): Promise<string[]> {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export async function getCurrentSemester(s: Session): Promise<SemesterInfo> {
|
|
23
|
-
const json = await
|
|
23
|
+
const json = await s.fetchJsonWithToken(URLS.LEARN_CURRENT_SEMESTER());
|
|
24
24
|
if (json.message !== 'success') {
|
|
25
25
|
return Promise.reject({
|
|
26
26
|
reason: FailReason.INVALID_RESPONSE,
|
|
@@ -38,14 +38,15 @@ export async function getCurrentSemester(s: Session): Promise<SemesterInfo> {
|
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
/** get all courses in the specified semester */
|
|
41
|
+
/** get all courses in the specified semester; time and location only with `options.timeAndLocation` */
|
|
42
42
|
export async function getCourseList(
|
|
43
43
|
s: Session,
|
|
44
44
|
semesterID: string,
|
|
45
45
|
courseType: CourseType = CourseType.STUDENT,
|
|
46
46
|
lang = s.lang,
|
|
47
|
+
options: CourseListOptions = {},
|
|
47
48
|
): Promise<CourseInfo[]> {
|
|
48
|
-
const json = await
|
|
49
|
+
const json = await s.fetchJsonWithToken(URLS.LEARN_COURSE_LIST(semesterID, courseType, lang));
|
|
49
50
|
if (json.message !== 'success' || !Array.isArray(json.resultList)) {
|
|
50
51
|
return Promise.reject({
|
|
51
52
|
reason: FailReason.INVALID_RESPONSE,
|
|
@@ -56,11 +57,14 @@ export async function getCourseList(
|
|
|
56
57
|
|
|
57
58
|
return Promise.all(
|
|
58
59
|
result.map(async (c) => {
|
|
59
|
-
let timeAndLocation: string[]
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
let timeAndLocation: string[] | undefined;
|
|
61
|
+
if (options.timeAndLocation) {
|
|
62
|
+
try {
|
|
63
|
+
timeAndLocation = await getCourseTimeAndLocation(s, c.wlkcid);
|
|
64
|
+
} catch {
|
|
65
|
+
timeAndLocation = [];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
64
68
|
return {
|
|
65
69
|
id: c.wlkcid,
|
|
66
70
|
name: decodeHTML(c.zywkcm),
|
|
@@ -78,16 +82,24 @@ export async function getCourseList(
|
|
|
78
82
|
);
|
|
79
83
|
}
|
|
80
84
|
|
|
85
|
+
/** get the time and location of one course (1 request) */
|
|
86
|
+
export async function getCourseTimeAndLocation(s: Session, courseID: string): Promise<string[]> {
|
|
87
|
+
// see https://github.com/Harry-Chen/Learn-Helper/issues/145
|
|
88
|
+
const json = await s.fetchJsonWithToken(URLS.LEARN_COURSE_TIME_LOCATION(courseID));
|
|
89
|
+
if (!Array.isArray(json)) {
|
|
90
|
+
throw { reason: FailReason.INVALID_RESPONSE, extra: json } as ApiError;
|
|
91
|
+
}
|
|
92
|
+
return json as string[];
|
|
93
|
+
}
|
|
94
|
+
|
|
81
95
|
export async function sortCourses(s: Session, courseIDs: string[]): Promise<void> {
|
|
82
|
-
const json = await (
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
})
|
|
90
|
-
).json();
|
|
96
|
+
const json = await s.fetchJsonWithToken(URLS.LEARN_SORT_COURSES, {
|
|
97
|
+
method: 'POST',
|
|
98
|
+
body: JSON.stringify(courseIDs.map((id, index) => ({ wlkcid: id, xh: index + 1 }))),
|
|
99
|
+
headers: {
|
|
100
|
+
'Content-Type': 'application/json',
|
|
101
|
+
},
|
|
102
|
+
});
|
|
91
103
|
if (json.result !== 'success') {
|
|
92
104
|
return Promise.reject({
|
|
93
105
|
reason: FailReason.OPERATION_FAILED,
|
package/src/discussions.ts
CHANGED
|
@@ -15,7 +15,7 @@ export async function getDiscussionList(
|
|
|
15
15
|
courseID: string,
|
|
16
16
|
courseType: CourseType = CourseType.STUDENT,
|
|
17
17
|
): Promise<Discussion[]> {
|
|
18
|
-
const json = await
|
|
18
|
+
const json = await s.fetchJsonWithToken(URLS.LEARN_DISCUSSION_LIST(courseID, courseType));
|
|
19
19
|
if (json.result !== 'success') {
|
|
20
20
|
return Promise.reject({
|
|
21
21
|
reason: FailReason.INVALID_RESPONSE,
|
|
@@ -44,7 +44,7 @@ export async function getAnsweredQuestionList(
|
|
|
44
44
|
courseID: string,
|
|
45
45
|
courseType: CourseType = CourseType.STUDENT,
|
|
46
46
|
): Promise<Question[]> {
|
|
47
|
-
const json = await
|
|
47
|
+
const json = await s.fetchJsonWithToken(URLS.LEARN_QUESTION_LIST_ANSWERED(courseID, courseType));
|
|
48
48
|
if (json.result !== 'success') {
|
|
49
49
|
return Promise.reject({
|
|
50
50
|
reason: FailReason.INVALID_RESPONSE,
|
package/src/favorites.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { CONTENT_TYPE_MAP_REVERSE, decodeHTML } from './utils.ts';
|
|
|
11
11
|
* Add an item to favorites. (收藏)
|
|
12
12
|
*/
|
|
13
13
|
export async function addToFavorites(s: Session, type: ContentType, id: string): Promise<void> {
|
|
14
|
-
const json = await
|
|
14
|
+
const json = await s.fetchJsonWithToken(URLS.LEARN_FAVORITE_ADD(type, id));
|
|
15
15
|
if (json.result !== 'success' || !json.msg?.endsWith?.('成功')) {
|
|
16
16
|
return Promise.reject({
|
|
17
17
|
reason: FailReason.OPERATION_FAILED,
|
|
@@ -24,7 +24,7 @@ export async function addToFavorites(s: Session, type: ContentType, id: string):
|
|
|
24
24
|
* Remove an item from favorites. (取消收藏)
|
|
25
25
|
*/
|
|
26
26
|
export async function removeFromFavorites(s: Session, id: string): Promise<void> {
|
|
27
|
-
const json = await
|
|
27
|
+
const json = await s.fetchJsonWithToken(URLS.LEARN_FAVORITE_REMOVE(id));
|
|
28
28
|
if (json.result !== 'success' || !json.msg?.endsWith?.('成功')) {
|
|
29
29
|
return Promise.reject({
|
|
30
30
|
reason: FailReason.OPERATION_FAILED,
|
|
@@ -38,12 +38,10 @@ export async function removeFromFavorites(s: Session, id: string): Promise<void>
|
|
|
38
38
|
* If `courseID` or `type` is specified, only return favorites of that course or type.
|
|
39
39
|
*/
|
|
40
40
|
export async function getFavorites(s: Session, courseID?: string, type?: ContentType): Promise<FavoriteItem[]> {
|
|
41
|
-
const json = await (
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
})
|
|
46
|
-
).json();
|
|
41
|
+
const json = await s.fetchJsonWithToken(URLS.LEARN_FAVORITE_LIST(type), {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
body: URLS.LEARN_PAGE_LIST_FORM_DATA(courseID),
|
|
44
|
+
});
|
|
47
45
|
if (json.result !== 'success') {
|
|
48
46
|
return Promise.reject({
|
|
49
47
|
reason: FailReason.INVALID_RESPONSE,
|
|
@@ -78,12 +76,10 @@ export async function getFavorites(s: Session, courseID?: string, type?: Content
|
|
|
78
76
|
* Pin a favorite item. (置顶)
|
|
79
77
|
*/
|
|
80
78
|
export async function pinFavoriteItem(s: Session, id: string): Promise<void> {
|
|
81
|
-
const json = await (
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
})
|
|
86
|
-
).json();
|
|
79
|
+
const json = await s.fetchJsonWithToken(URLS.LEARN_FAVORITE_PIN, {
|
|
80
|
+
method: 'POST',
|
|
81
|
+
body: URLS.LEARN_FAVORITE_PIN_UNPIN_FORM_DATA(id),
|
|
82
|
+
});
|
|
87
83
|
if (json.result !== 'success') {
|
|
88
84
|
return Promise.reject({
|
|
89
85
|
reason: FailReason.OPERATION_FAILED,
|
|
@@ -96,12 +92,10 @@ export async function pinFavoriteItem(s: Session, id: string): Promise<void> {
|
|
|
96
92
|
* Unpin a favorite item. (取消置顶)
|
|
97
93
|
*/
|
|
98
94
|
export async function unpinFavoriteItem(s: Session, id: string): Promise<void> {
|
|
99
|
-
const json = await (
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
})
|
|
104
|
-
).json();
|
|
95
|
+
const json = await s.fetchJsonWithToken(URLS.LEARN_FAVORITE_UNPIN, {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
body: URLS.LEARN_FAVORITE_PIN_UNPIN_FORM_DATA(id),
|
|
98
|
+
});
|
|
105
99
|
if (json.result !== 'success') {
|
|
106
100
|
return Promise.reject({
|
|
107
101
|
reason: FailReason.OPERATION_FAILED,
|
|
@@ -115,12 +109,10 @@ export async function unpinFavoriteItem(s: Session, id: string): Promise<void> {
|
|
|
115
109
|
* Set an empty string to remove the comment.
|
|
116
110
|
*/
|
|
117
111
|
export async function setComment(s: Session, type: ContentType, id: string, content: string): Promise<void> {
|
|
118
|
-
const json = await (
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
})
|
|
123
|
-
).json();
|
|
112
|
+
const json = await s.fetchJsonWithToken(URLS.LEARN_COMMENT_SET, {
|
|
113
|
+
method: 'POST',
|
|
114
|
+
body: URLS.LEARN_COMMENT_SET_FORM_DATA(type, id, content),
|
|
115
|
+
});
|
|
124
116
|
if (json.result !== 'success' || !json.msg?.endsWith?.('成功')) {
|
|
125
117
|
return Promise.reject({
|
|
126
118
|
reason: FailReason.OPERATION_FAILED,
|
|
@@ -134,12 +126,10 @@ export async function setComment(s: Session, type: ContentType, id: string, cont
|
|
|
134
126
|
* If `courseID` or `type` is specified, only return favorites of that course or type.
|
|
135
127
|
*/
|
|
136
128
|
export async function getComments(s: Session, courseID?: string, type?: ContentType): Promise<CommentItem[]> {
|
|
137
|
-
const json = await (
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
})
|
|
142
|
-
).json();
|
|
129
|
+
const json = await s.fetchJsonWithToken(URLS.LEARN_COMMENT_LIST(type), {
|
|
130
|
+
method: 'POST',
|
|
131
|
+
body: URLS.LEARN_PAGE_LIST_FORM_DATA(courseID),
|
|
132
|
+
});
|
|
143
133
|
if (json.result !== 'success') {
|
|
144
134
|
return Promise.reject({
|
|
145
135
|
reason: FailReason.INVALID_RESPONSE,
|
package/src/files.ts
CHANGED
|
@@ -6,13 +6,13 @@ import { ContentType, CourseType, FailReason } from './types.ts';
|
|
|
6
6
|
import * as URLS from './urls.ts';
|
|
7
7
|
import { decodeHTML, formatFileSize } from './utils.ts';
|
|
8
8
|
|
|
9
|
-
/** Get all files (课程文件) of the specified course. */
|
|
9
|
+
/** Get all files (课程文件) of the specified course; `category` is left out if the categories can't be fetched. */
|
|
10
10
|
export async function getFileList(
|
|
11
11
|
s: Session,
|
|
12
12
|
courseID: string,
|
|
13
13
|
courseType: CourseType = CourseType.STUDENT,
|
|
14
14
|
): Promise<File[]> {
|
|
15
|
-
const json = await
|
|
15
|
+
const json = await s.fetchJsonWithToken(URLS.LEARN_FILE_LIST(courseID, courseType));
|
|
16
16
|
if (json.result !== 'success') {
|
|
17
17
|
return Promise.reject({
|
|
18
18
|
reason: FailReason.INVALID_RESPONSE,
|
|
@@ -29,7 +29,9 @@ export async function getFileList(
|
|
|
29
29
|
result = json.object;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
// categories only fill `category`; don't fail the whole list when they can't be fetched
|
|
33
|
+
const categoryList = await getFileCategoryList(s, courseID, courseType).catch(() => []);
|
|
34
|
+
const categories = new Map(categoryList.map((c) => [c.id, c]));
|
|
33
35
|
|
|
34
36
|
return result.map((f) => {
|
|
35
37
|
const title = decodeHTML(f.bt);
|
|
@@ -71,7 +73,7 @@ export async function getFileCategoryList(
|
|
|
71
73
|
courseID: string,
|
|
72
74
|
courseType: CourseType = CourseType.STUDENT,
|
|
73
75
|
): Promise<FileCategory[]> {
|
|
74
|
-
const json = await
|
|
76
|
+
const json = await s.fetchJsonWithToken(URLS.LEARN_FILE_CATEGORY_LIST(courseID, courseType));
|
|
75
77
|
if (json.result !== 'success') {
|
|
76
78
|
return Promise.reject({
|
|
77
79
|
reason: FailReason.INVALID_RESPONSE,
|
|
@@ -102,7 +104,7 @@ export async function getFileListByCategory(
|
|
|
102
104
|
courseType: CourseType = CourseType.STUDENT,
|
|
103
105
|
): Promise<File[]> {
|
|
104
106
|
if (courseType === CourseType.STUDENT) {
|
|
105
|
-
const json = await
|
|
107
|
+
const json = await s.fetchJsonWithToken(URLS.LEARN_FILE_LIST_BY_CATEGORY_STUDENT(courseID, categoryId));
|
|
106
108
|
if (json.result !== 'success') {
|
|
107
109
|
return Promise.reject({
|
|
108
110
|
reason: FailReason.INVALID_RESPONSE,
|
|
@@ -147,12 +149,10 @@ export async function getFileListByCategory(
|
|
|
147
149
|
} satisfies File;
|
|
148
150
|
});
|
|
149
151
|
} else {
|
|
150
|
-
const json = await (
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
})
|
|
155
|
-
).json();
|
|
152
|
+
const json = await s.fetchJsonWithToken(URLS.LEARN_FILE_LIST_BY_CATEGORY_TEACHER, {
|
|
153
|
+
method: 'POST',
|
|
154
|
+
body: URLS.LEARN_FILE_LIST_BY_CATEGORY_TEACHER_FORM_DATA(courseID, categoryId),
|
|
155
|
+
});
|
|
156
156
|
if (json.result !== 'success') {
|
|
157
157
|
return Promise.reject({
|
|
158
158
|
reason: FailReason.INVALID_RESPONSE,
|
|
@@ -160,7 +160,7 @@ export async function getFileListByCategory(
|
|
|
160
160
|
});
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
const result = (json.object
|
|
163
|
+
const result = (json.object?.aaData ?? []) as any[];
|
|
164
164
|
|
|
165
165
|
return result.map((f) => {
|
|
166
166
|
const title = decodeHTML(f.bt);
|