@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/homework.ts
CHANGED
|
@@ -1,37 +1,54 @@
|
|
|
1
1
|
// Homework (课程作业), excellent homework and homework submission.
|
|
2
2
|
|
|
3
|
-
import type { Element } from 'cheerio';
|
|
4
3
|
import { Base64 } from 'js-base64';
|
|
5
4
|
|
|
6
5
|
import type { Session } from './request.ts';
|
|
7
|
-
import {
|
|
6
|
+
import { addCSRFTokenToUrl } from './request.ts';
|
|
8
7
|
import type {
|
|
9
|
-
ApiError,
|
|
10
8
|
ExcellentHomework,
|
|
9
|
+
ExcellentHomeworkBase,
|
|
11
10
|
Homework,
|
|
11
|
+
HomeworkBase,
|
|
12
|
+
HomeworkDetail,
|
|
12
13
|
HomeworkListOptions,
|
|
14
|
+
HomeworkStatus,
|
|
15
|
+
HomeworkSubmitAttachment,
|
|
16
|
+
HomeworkSubmitRequest,
|
|
13
17
|
HomeworkTA,
|
|
14
|
-
IExcellentHomework,
|
|
15
|
-
IHomework,
|
|
16
|
-
IHomeworkDetail,
|
|
17
|
-
IHomeworkStatus,
|
|
18
|
-
IHomeworkSubmitAttachment,
|
|
19
18
|
RemoteFile,
|
|
20
19
|
} from './types.ts';
|
|
21
20
|
import { ContentType, CourseType, FailReason } from './types.ts';
|
|
22
21
|
import * as URLS from './urls.ts';
|
|
23
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
$,
|
|
24
|
+
GRADE_LEVEL_MAP,
|
|
25
|
+
YES,
|
|
26
|
+
apiError,
|
|
27
|
+
decodeHTML,
|
|
28
|
+
expectSuccess,
|
|
29
|
+
isSelected,
|
|
30
|
+
looksLikeLoginPage,
|
|
31
|
+
toRequiredServerTime,
|
|
32
|
+
toServerTime,
|
|
33
|
+
trimAndDefine,
|
|
34
|
+
} from './utils.ts';
|
|
35
|
+
import type { Element } from './utils.ts';
|
|
36
|
+
|
|
37
|
+
/** the student homework lists, by submission status */
|
|
38
|
+
export const HOMEWORK_LIST_SOURCES: readonly { url: string; status: HomeworkStatus }[] = [
|
|
39
|
+
{ url: URLS.LEARN_HOMEWORK_LIST_NEW, status: { submitted: false, graded: false } },
|
|
40
|
+
{ url: URLS.LEARN_HOMEWORK_LIST_SUBMITTED, status: { submitted: true, graded: false } },
|
|
41
|
+
{ url: URLS.LEARN_HOMEWORK_LIST_GRADED, status: { submitted: true, graded: true } },
|
|
42
|
+
];
|
|
24
43
|
|
|
25
44
|
/**
|
|
26
45
|
* Get all homeworks (课程作业) of the specified course: 3 status lists and the excellent homework list.
|
|
27
46
|
* Details (page + description) only for the homework `options.details` selects.
|
|
28
47
|
*/
|
|
29
|
-
export function getHomeworkList(s: Session, courseID: string): Promise<Homework[]>;
|
|
30
|
-
|
|
31
48
|
export function getHomeworkList(
|
|
32
49
|
s: Session,
|
|
33
50
|
courseID: string,
|
|
34
|
-
courseType
|
|
51
|
+
courseType?: CourseType.STUDENT,
|
|
35
52
|
options?: HomeworkListOptions,
|
|
36
53
|
): Promise<Homework[]>;
|
|
37
54
|
|
|
@@ -43,77 +60,70 @@ export async function getHomeworkList(
|
|
|
43
60
|
courseType: CourseType = CourseType.STUDENT,
|
|
44
61
|
options: HomeworkListOptions = {},
|
|
45
62
|
): Promise<Homework[] | HomeworkTA[]> {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
body: URLS.LEARN_PAGE_LIST_FORM_DATA(courseID),
|
|
51
|
-
})
|
|
52
|
-
).json();
|
|
53
|
-
if (json.result !== 'success') {
|
|
54
|
-
return Promise.reject({
|
|
55
|
-
reason: FailReason.INVALID_RESPONSE,
|
|
56
|
-
extra: json,
|
|
57
|
-
});
|
|
58
|
-
}
|
|
63
|
+
return courseType === CourseType.TEACHER
|
|
64
|
+
? getTeacherHomeworkList(s, courseID)
|
|
65
|
+
: getStudentHomeworkList(s, courseID, options);
|
|
66
|
+
}
|
|
59
67
|
|
|
60
|
-
|
|
68
|
+
async function getTeacherHomeworkList(s: Session, courseID: string): Promise<HomeworkTA[]> {
|
|
69
|
+
const json = await s.fetchJsonWithToken(URLS.LEARN_HOMEWORK_LIST_TEACHER, {
|
|
70
|
+
method: 'POST',
|
|
71
|
+
body: URLS.LEARN_PAGE_LIST_FORM_DATA(courseID),
|
|
72
|
+
});
|
|
73
|
+
expectSuccess(json, FailReason.INVALID_RESPONSE);
|
|
61
74
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
75
|
+
const result = (json.object?.aaData ?? []) as any[];
|
|
76
|
+
|
|
77
|
+
return result.map(
|
|
78
|
+
(d) =>
|
|
79
|
+
({
|
|
80
|
+
id: d.zyid,
|
|
81
|
+
index: d.wz,
|
|
82
|
+
title: decodeHTML(d.bt),
|
|
83
|
+
description: decodeHTML(Base64.decode(d.nr ?? '')),
|
|
84
|
+
publisherId: d.fbr,
|
|
85
|
+
publishTime: toRequiredServerTime(d.fbsj),
|
|
86
|
+
startTime: toRequiredServerTime(d.kssj),
|
|
87
|
+
deadline: toRequiredServerTime(d.jzsj),
|
|
88
|
+
lateSubmissionDeadline: toServerTime(d.bjjzsj),
|
|
89
|
+
url: URLS.LEARN_HOMEWORK_DETAIL_TEACHER(courseID, d.zyid),
|
|
90
|
+
completionType: d.zywcfs,
|
|
91
|
+
submissionType: d.zytjfs,
|
|
92
|
+
gradedCount: d.ypys,
|
|
93
|
+
submittedCount: d.yjs,
|
|
94
|
+
unsubmittedCount: d.wjs,
|
|
95
|
+
}) satisfies HomeworkTA,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function getStudentHomeworkList(s: Session, courseID: string, options: HomeworkListOptions): Promise<Homework[]> {
|
|
100
|
+
const [lists, excellentHomeworkListByHomework] = await Promise.all([
|
|
101
|
+
Promise.all(HOMEWORK_LIST_SOURCES.map((source) => getHomeworkListAtUrl(s, courseID, source.url, source.status))),
|
|
102
|
+
// a failed excellent homework list doesn't fail the homework list: its items get `excellentHomeworkList: undefined`
|
|
103
|
+
getExcellentHomeworkListByHomework(s, courseID).catch(() => undefined),
|
|
104
|
+
]);
|
|
105
|
+
return Promise.all(
|
|
106
|
+
lists.flat().map(async (h) => {
|
|
107
|
+
const homework: Homework = {
|
|
108
|
+
...h,
|
|
109
|
+
excellentHomeworkList: excellentHomeworkListByHomework && (excellentHomeworkListByHomework[h.baseId] ?? []),
|
|
110
|
+
};
|
|
111
|
+
return isSelected(options.details, homework) ? { ...homework, ...(await getHomeworkDetail(s, h)) } : homework;
|
|
112
|
+
}),
|
|
113
|
+
);
|
|
97
114
|
}
|
|
98
115
|
|
|
99
116
|
async function getHomeworkListAtUrl(
|
|
100
117
|
s: Session,
|
|
101
118
|
courseID: string,
|
|
102
119
|
url: string,
|
|
103
|
-
status:
|
|
104
|
-
): Promise<
|
|
105
|
-
const json = await (
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
).json();
|
|
111
|
-
if (json.result !== 'success') {
|
|
112
|
-
return Promise.reject({
|
|
113
|
-
reason: FailReason.INVALID_RESPONSE,
|
|
114
|
-
extra: json,
|
|
115
|
-
});
|
|
116
|
-
}
|
|
120
|
+
status: HomeworkStatus,
|
|
121
|
+
): Promise<HomeworkBase[]> {
|
|
122
|
+
const json = await s.fetchJsonWithToken(url, {
|
|
123
|
+
method: 'POST',
|
|
124
|
+
body: URLS.LEARN_PAGE_LIST_FORM_DATA(courseID),
|
|
125
|
+
});
|
|
126
|
+
expectSuccess(json, FailReason.INVALID_RESPONSE);
|
|
117
127
|
|
|
118
128
|
const result = (json.object?.aaData ?? []) as any[];
|
|
119
129
|
|
|
@@ -125,23 +135,22 @@ async function getHomeworkListAtUrl(
|
|
|
125
135
|
baseId: h.zyid,
|
|
126
136
|
title: decodeHTML(h.bt),
|
|
127
137
|
url: URLS.LEARN_HOMEWORK_PAGE(h.wlkcid, h.xszyid),
|
|
128
|
-
deadline: h.jzsj,
|
|
129
|
-
lateSubmissionDeadline: h.bjjzsj
|
|
138
|
+
deadline: toRequiredServerTime(h.jzsj),
|
|
139
|
+
lateSubmissionDeadline: toServerTime(h.bjjzsj),
|
|
130
140
|
isLateSubmission: h.sfbj === YES,
|
|
131
141
|
completionType: h.zywcfs,
|
|
132
142
|
submissionType: h.zytjfs,
|
|
133
143
|
submitUrl: URLS.LEARN_HOMEWORK_SUBMIT_PAGE(h.wlkcid, h.xszyid),
|
|
134
|
-
submitTime: h.scsj
|
|
144
|
+
submitTime: toServerTime(h.scsj),
|
|
135
145
|
grade: h.cj === null ? undefined : h.cj,
|
|
136
146
|
gradeLevel: GRADE_LEVEL_MAP.get(h.cj),
|
|
137
147
|
graderName: trimAndDefine(h.jsm),
|
|
138
148
|
gradeContent: trimAndDefine(h.pynr),
|
|
139
|
-
gradeTime: h.pysj
|
|
149
|
+
gradeTime: toServerTime(h.pysj),
|
|
140
150
|
isFavorite: h.sfsc === YES,
|
|
141
|
-
favoriteTime: h.scsj === null || h.sfsc !== YES ? undefined : h.scsj,
|
|
142
151
|
comment: h.bznr ?? undefined,
|
|
143
152
|
...status,
|
|
144
|
-
}) satisfies
|
|
153
|
+
}) satisfies HomeworkBase,
|
|
145
154
|
);
|
|
146
155
|
}
|
|
147
156
|
|
|
@@ -149,18 +158,11 @@ async function getExcellentHomeworkListByHomework(
|
|
|
149
158
|
s: Session,
|
|
150
159
|
courseID: string,
|
|
151
160
|
): Promise<{ [id: string]: ExcellentHomework[] }> {
|
|
152
|
-
const json = await (
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
).json();
|
|
158
|
-
if (json.result !== 'success') {
|
|
159
|
-
return Promise.reject({
|
|
160
|
-
reason: FailReason.INVALID_RESPONSE,
|
|
161
|
-
extra: json,
|
|
162
|
-
});
|
|
163
|
-
}
|
|
161
|
+
const json = await s.fetchJsonWithToken(URLS.LEARN_HOMEWORK_LIST_EXCELLENT, {
|
|
162
|
+
method: 'POST',
|
|
163
|
+
body: URLS.LEARN_PAGE_LIST_FORM_DATA(courseID),
|
|
164
|
+
});
|
|
165
|
+
expectSuccess(json, FailReason.INVALID_RESPONSE);
|
|
164
166
|
|
|
165
167
|
const result = (json.object?.aaData ?? []) as any[];
|
|
166
168
|
|
|
@@ -178,7 +180,7 @@ async function getExcellentHomeworkListByHomework(
|
|
|
178
180
|
name: h.cy?.split(' ')?.[1],
|
|
179
181
|
anonymous: h.sfzm === YES,
|
|
180
182
|
},
|
|
181
|
-
}) satisfies
|
|
183
|
+
}) satisfies ExcellentHomeworkBase,
|
|
182
184
|
)
|
|
183
185
|
.reduce<{ [id: string]: ExcellentHomework[] }>((acc, cur) => {
|
|
184
186
|
if (!acc[cur.baseId]) {
|
|
@@ -189,9 +191,8 @@ async function getExcellentHomeworkListByHomework(
|
|
|
189
191
|
}, {});
|
|
190
192
|
}
|
|
191
193
|
|
|
192
|
-
async function parseHomeworkAtUrl(s: Session, url: string): Promise<
|
|
193
|
-
const
|
|
194
|
-
const result = $(await response.text());
|
|
194
|
+
async function parseHomeworkAtUrl(s: Session, url: string): Promise<HomeworkDetail> {
|
|
195
|
+
const result = $(await s.fetchTextWithToken(url));
|
|
195
196
|
|
|
196
197
|
const fileDivs = result('div.list.fujian.clearfix');
|
|
197
198
|
|
|
@@ -212,47 +213,41 @@ async function parseHomeworkAtUrl(s: Session, url: string): Promise<IHomeworkDet
|
|
|
212
213
|
*/
|
|
213
214
|
export async function getHomeworkDetail(
|
|
214
215
|
s: Session,
|
|
215
|
-
homework: Pick<
|
|
216
|
-
): Promise<
|
|
216
|
+
homework: Pick<HomeworkBase | ExcellentHomeworkBase, 'url' | 'baseId'>,
|
|
217
|
+
): Promise<HomeworkDetail> {
|
|
217
218
|
const [page, description] = await Promise.all([
|
|
218
219
|
parseHomeworkAtUrl(s, homework.url),
|
|
219
220
|
getHomeworkDescription(s, homework.baseId),
|
|
220
221
|
]);
|
|
221
|
-
|
|
222
|
+
// the description JSON wins, but an empty one falls back to the page
|
|
223
|
+
return { ...page, description: description.description ?? page.description };
|
|
222
224
|
}
|
|
223
225
|
|
|
224
|
-
async function getHomeworkDescription(s: Session, baseId: string): Promise<
|
|
225
|
-
const json = await (
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
).json();
|
|
231
|
-
if (json.result !== 'success') {
|
|
232
|
-
throw {
|
|
233
|
-
reason: FailReason.INVALID_RESPONSE,
|
|
234
|
-
extra: json,
|
|
235
|
-
} as ApiError;
|
|
236
|
-
}
|
|
226
|
+
async function getHomeworkDescription(s: Session, baseId: string): Promise<HomeworkDetail> {
|
|
227
|
+
const json = await s.fetchJsonWithToken(URLS.LEARN_HOMEWORK_DETAIL, {
|
|
228
|
+
method: 'POST',
|
|
229
|
+
body: URLS.LEARN_HOMEWORK_DETAIL_FORM_DATA(baseId),
|
|
230
|
+
});
|
|
231
|
+
expectSuccess(json, FailReason.INVALID_RESPONSE);
|
|
237
232
|
return {
|
|
238
233
|
description: trimAndDefine(json.msg),
|
|
239
234
|
};
|
|
240
235
|
}
|
|
241
236
|
|
|
242
|
-
|
|
237
|
+
/** the attachment in one `div.list.fujian` block; `undefined` if the block is missing or has no link */
|
|
238
|
+
function parseHomeworkFile(s: Session, fileDiv: Element | undefined): RemoteFile | undefined {
|
|
239
|
+
if (!fileDiv) return undefined;
|
|
243
240
|
const fileNode = $(fileDiv)('.ftitle').children('a')[0] ?? $(fileDiv)('.fl').children('a')[0];
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const
|
|
241
|
+
const href = fileNode?.attribs.href;
|
|
242
|
+
if (fileNode !== undefined && href) {
|
|
243
|
+
const size = trimAndDefine($(fileDiv)('.fl > span[class^="color"]').first().text()) ?? '';
|
|
244
|
+
const params = new URLSearchParams(href.split('?').slice(-1)[0]);
|
|
247
245
|
const attachmentId = params.get('fileId')!;
|
|
248
|
-
//
|
|
249
|
-
|
|
250
|
-
if (params.has('downloadUrl')) {
|
|
251
|
-
downloadUrl = URLS.LEARN_PREFIX + params.get('downloadUrl')!;
|
|
252
|
-
}
|
|
246
|
+
// a link to the file viewer carries the download path in its `downloadUrl` parameter
|
|
247
|
+
const downloadUrl = URLS.LEARN_PREFIX + (params.get('downloadUrl') ?? href);
|
|
253
248
|
return {
|
|
254
249
|
id: attachmentId,
|
|
255
|
-
name: (fileNode.children[0] as { data
|
|
250
|
+
name: (fileNode.children[0] as { data?: string } | undefined)?.data ?? '',
|
|
256
251
|
downloadUrl,
|
|
257
252
|
previewUrl: URLS.LEARN_FILE_PREVIEW(ContentType.HOMEWORK, attachmentId, CourseType.STUDENT, s.previewFirstPage),
|
|
258
253
|
size,
|
|
@@ -266,19 +261,96 @@ export async function submitHomework(
|
|
|
266
261
|
s: Session,
|
|
267
262
|
id: string,
|
|
268
263
|
content = '',
|
|
269
|
-
attachment?:
|
|
264
|
+
attachment?: HomeworkSubmitAttachment,
|
|
270
265
|
removeAttachment = false,
|
|
271
266
|
): Promise<void> {
|
|
272
|
-
const json = await (
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
)
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
267
|
+
const json = await s.fetchJsonWithToken(URLS.LEARN_HOMEWORK_SUBMIT, {
|
|
268
|
+
method: 'POST',
|
|
269
|
+
body: homeworkSubmitForm(id, content, attachment, removeAttachment),
|
|
270
|
+
// the response only starts after the whole upload, which can take longer than `requestTimeout`
|
|
271
|
+
timeout: attachment ? 0 : undefined,
|
|
272
|
+
});
|
|
273
|
+
checkHomeworkSubmitResponse(json);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* The request `submitHomework` would send, for sending it with another HTTP client (e.g. one that reports upload
|
|
278
|
+
* progress). Checks the session first (`ensureSession`: one request, and a login only if it has expired), as the
|
|
279
|
+
* request is sent without the helper's re-login and retry. Read the answer with `checkHomeworkSubmitResponse`.
|
|
280
|
+
*/
|
|
281
|
+
export async function getHomeworkSubmitRequest(
|
|
282
|
+
s: Session,
|
|
283
|
+
id: string,
|
|
284
|
+
content = '',
|
|
285
|
+
attachment?: HomeworkSubmitAttachment,
|
|
286
|
+
removeAttachment = false,
|
|
287
|
+
): Promise<HomeworkSubmitRequest> {
|
|
288
|
+
await s.ensureSession(URLS.LEARN_CURRENT_SEMESTER);
|
|
289
|
+
return {
|
|
290
|
+
url: addCSRFTokenToUrl(URLS.LEARN_HOMEWORK_SUBMIT, s.csrfToken),
|
|
291
|
+
body: homeworkSubmitForm(id, content, attachment, removeAttachment),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Check the server's answer to a homework submission: its parsed JSON, or the body as text. Rejects with
|
|
297
|
+
* `NOT_LOGGED_IN` for the login page of an expired session (which the server sends with status 200),
|
|
298
|
+
* `INVALID_RESPONSE` for any other text that isn't JSON, and `OPERATION_FAILED` (with the answer in `extra`) unless
|
|
299
|
+
* `result` is `'success'`.
|
|
300
|
+
*/
|
|
301
|
+
export function checkHomeworkSubmitResponse(body: unknown): void {
|
|
302
|
+
let json = body;
|
|
303
|
+
if (typeof body === 'string') {
|
|
304
|
+
try {
|
|
305
|
+
json = JSON.parse(body);
|
|
306
|
+
} catch {
|
|
307
|
+
throw apiError(
|
|
308
|
+
looksLikeLoginPage(body) ? FailReason.NOT_LOGGED_IN : FailReason.INVALID_RESPONSE,
|
|
309
|
+
body.slice(0, 200),
|
|
310
|
+
);
|
|
311
|
+
}
|
|
283
312
|
}
|
|
313
|
+
expectSuccess(json, FailReason.OPERATION_FAILED);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* The form the website's submit page sends (`fileUploadProgress` in `/res/js/common/myApp.js`): `fileupload` is the
|
|
318
|
+
* picked file, or the text "undefined" when none is picked (it appends `files[0]` as is); `zynr` is the whole current
|
|
319
|
+
* answer as HTML from its editor, sent every time (also when only the attachment changes); `isDeleted` is "1" when the
|
|
320
|
+
* existing attachment is deleted, and then the server ignores `zynr` (the submitted answer stays).
|
|
321
|
+
*/
|
|
322
|
+
export const homeworkSubmitForm = (
|
|
323
|
+
id: string,
|
|
324
|
+
content = '',
|
|
325
|
+
attachment?: HomeworkSubmitAttachment,
|
|
326
|
+
removeAttachment = false,
|
|
327
|
+
) => {
|
|
328
|
+
const form = new FormData();
|
|
329
|
+
form.append('xszyid', id);
|
|
330
|
+
form.append('zynr', content ?? '');
|
|
331
|
+
if (!attachment) form.append('fileupload', 'undefined');
|
|
332
|
+
else if (attachment.content instanceof Blob) form.append('fileupload', attachment.content, attachment.filename);
|
|
333
|
+
else {
|
|
334
|
+
// React Native's FormData reads a file from an object with its uri, name and type
|
|
335
|
+
const { uri, type } = attachment.content;
|
|
336
|
+
form.append('fileupload', { uri, name: attachment.filename, type: type || 'application/octet-stream' } as any);
|
|
337
|
+
}
|
|
338
|
+
if (removeAttachment) form.append('isDeleted', '1');
|
|
339
|
+
else form.append('isDeleted', '0');
|
|
340
|
+
return form;
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
const HTML_ESCAPES: Record<string, string> = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Plain text as the website's answer editor sends it: one `<p>` per line, with ` ` for an empty line, and `''`
|
|
347
|
+
* for text that is only whitespace. The server replaces the submitted answer with what is sent, so send its HTML
|
|
348
|
+
* as it is when the answer wasn't edited, to keep its formatting.
|
|
349
|
+
*/
|
|
350
|
+
export function homeworkContentHtml(text: string): string {
|
|
351
|
+
if (!text.trim()) return '';
|
|
352
|
+
return text
|
|
353
|
+
.split(/\r?\n/)
|
|
354
|
+
.map((line) => `<p>${line.trim() ? line.replace(/[&<>"']/g, (c) => HTML_ESCAPES[c]) : ' '}</p>`)
|
|
355
|
+
.join('');
|
|
284
356
|
}
|