@robertying/thu-learn-lib 0.0.0-stage → 5.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.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +324 -2
  3. package/lib/auth.d.ts +11 -0
  4. package/lib/auth.d.ts.map +1 -0
  5. package/lib/auth.js +99 -0
  6. package/lib/auth.js.map +1 -0
  7. package/lib/calendar.d.ts +16 -0
  8. package/lib/calendar.d.ts.map +1 -0
  9. package/lib/calendar.js +81 -0
  10. package/lib/calendar.js.map +1 -0
  11. package/lib/contents.d.ts +9 -0
  12. package/lib/contents.d.ts.map +1 -0
  13. package/lib/contents.js +51 -0
  14. package/lib/contents.js.map +1 -0
  15. package/lib/courses.d.ts +11 -0
  16. package/lib/courses.d.ts.map +1 -0
  17. package/lib/courses.js +94 -0
  18. package/lib/courses.js.map +1 -0
  19. package/lib/discussions.d.ts +11 -0
  20. package/lib/discussions.d.ts.map +1 -0
  21. package/lib/discussions.js +56 -0
  22. package/lib/discussions.js.map +1 -0
  23. package/lib/favorites.d.ts +35 -0
  24. package/lib/favorites.d.ts.map +1 -0
  25. package/lib/favorites.js +150 -0
  26. package/lib/favorites.js.map +1 -0
  27. package/lib/files.d.ts +13 -0
  28. package/lib/files.d.ts.map +1 -0
  29. package/lib/files.js +167 -0
  30. package/lib/files.js.map +1 -0
  31. package/lib/homework.d.ts +17 -0
  32. package/lib/homework.d.ts.map +1 -0
  33. package/lib/homework.js +194 -0
  34. package/lib/homework.js.map +1 -0
  35. package/lib/index.d.ts +134 -0
  36. package/lib/index.d.ts.map +1 -0
  37. package/lib/index.js +219 -0
  38. package/lib/index.js.map +1 -0
  39. package/lib/notices.d.ts +14 -0
  40. package/lib/notices.d.ts.map +1 -0
  41. package/lib/notices.js +92 -0
  42. package/lib/notices.js.map +1 -0
  43. package/lib/questionnaires.d.ts +7 -0
  44. package/lib/questionnaires.d.ts.map +1 -0
  45. package/lib/questionnaires.js +62 -0
  46. package/lib/questionnaires.js.map +1 -0
  47. package/lib/request.d.ts +57 -0
  48. package/lib/request.d.ts.map +1 -0
  49. package/lib/request.js +197 -0
  50. package/lib/request.js.map +1 -0
  51. package/lib/types.d.ts +433 -0
  52. package/lib/types.d.ts.map +1 -0
  53. package/lib/types.js +98 -0
  54. package/lib/types.js.map +1 -0
  55. package/lib/urls.d.ts +78 -0
  56. package/lib/urls.d.ts.map +1 -0
  57. package/lib/urls.js +160 -0
  58. package/lib/urls.js.map +1 -0
  59. package/lib/user.d.ts +7 -0
  60. package/lib/user.d.ts.map +1 -0
  61. package/lib/user.js +22 -0
  62. package/lib/user.js.map +1 -0
  63. package/lib/utils.d.ts +16 -0
  64. package/lib/utils.d.ts.map +1 -0
  65. package/lib/utils.js +117 -0
  66. package/lib/utils.js.map +1 -0
  67. package/package.json +67 -4
  68. package/src/auth.ts +121 -0
  69. package/src/calendar.ts +102 -0
  70. package/src/contents.ts +63 -0
  71. package/src/courses.ts +111 -0
  72. package/src/discussions.ts +80 -0
  73. package/src/favorites.ts +167 -0
  74. package/src/files.ts +197 -0
  75. package/src/homework.ts +284 -0
  76. package/src/index.ts +345 -0
  77. package/src/notices.ts +115 -0
  78. package/src/questionnaires.ts +76 -0
  79. package/src/request.ts +214 -0
  80. package/src/types.ts +494 -0
  81. package/src/urls.ts +257 -0
  82. package/src/user.ts +28 -0
  83. package/src/utils.ts +125 -0
package/src/files.ts ADDED
@@ -0,0 +1,197 @@
1
+ // Course files (课程文件) and file categories.
2
+
3
+ import type { Session } from './request.ts';
4
+ import type { File, FileCategory } from './types.ts';
5
+ import { ContentType, CourseType, FailReason } from './types.ts';
6
+ import * as URLS from './urls.ts';
7
+ import { decodeHTML, formatFileSize } from './utils.ts';
8
+
9
+ /** Get all files (课程文件) of the specified course. */
10
+ export async function getFileList(
11
+ s: Session,
12
+ courseID: string,
13
+ courseType: CourseType = CourseType.STUDENT,
14
+ ): Promise<File[]> {
15
+ const json = await (await s.fetchWithToken(URLS.LEARN_FILE_LIST(courseID, courseType))).json();
16
+ if (json.result !== 'success') {
17
+ return Promise.reject({
18
+ reason: FailReason.INVALID_RESPONSE,
19
+ extra: json,
20
+ });
21
+ }
22
+
23
+ let result: any[] = [];
24
+ if (Array.isArray(json.object?.resultsList)) {
25
+ // teacher
26
+ result = json.object.resultsList;
27
+ } else if (Array.isArray(json.object)) {
28
+ // student
29
+ result = json.object;
30
+ }
31
+
32
+ const categories = new Map((await getFileCategoryList(s, courseID, courseType)).map((c) => [c.id, c]));
33
+
34
+ return result.map((f) => {
35
+ const title = decodeHTML(f.bt);
36
+ const fileId = f.wjid;
37
+ const uploadTime = f.scsj;
38
+ const downloadUrl = URLS.LEARN_FILE_DOWNLOAD(fileId, courseType);
39
+ const previewUrl = URLS.LEARN_FILE_PREVIEW(ContentType.FILE, fileId, courseType, s.previewFirstPage);
40
+ return {
41
+ id: f.kjxxid,
42
+ fileId,
43
+ category: categories.get(f.kjflid),
44
+ title,
45
+ description: decodeHTML(f.ms),
46
+ rawSize: f.wjdx,
47
+ size: f.fileSize,
48
+ uploadTime,
49
+ publishTime: uploadTime,
50
+ downloadUrl,
51
+ previewUrl,
52
+ isNew: f.isNew ?? false,
53
+ markedImportant: f.sfqd === 1,
54
+ visitCount: f.xsllcs ?? f.llcs ?? 0,
55
+ downloadCount: f.xzcs ?? 0,
56
+ fileType: f.wjlx,
57
+ remoteFile: {
58
+ id: fileId,
59
+ name: title,
60
+ downloadUrl,
61
+ previewUrl,
62
+ size: f.fileSize,
63
+ },
64
+ } satisfies File;
65
+ });
66
+ }
67
+
68
+ /** Get file categories of the specified course. */
69
+ export async function getFileCategoryList(
70
+ s: Session,
71
+ courseID: string,
72
+ courseType: CourseType = CourseType.STUDENT,
73
+ ): Promise<FileCategory[]> {
74
+ const json = await (await s.fetchWithToken(URLS.LEARN_FILE_CATEGORY_LIST(courseID, courseType))).json();
75
+ if (json.result !== 'success') {
76
+ return Promise.reject({
77
+ reason: FailReason.INVALID_RESPONSE,
78
+ extra: json,
79
+ });
80
+ }
81
+
82
+ const result = (json.object?.rows ?? []) as any[];
83
+
84
+ return result.map(
85
+ (c) =>
86
+ ({
87
+ id: c.kjflid,
88
+ title: decodeHTML(c.bt),
89
+ creationTime: c.czsj,
90
+ }) satisfies FileCategory,
91
+ );
92
+ }
93
+
94
+ /**
95
+ * Get all files of the specified category of the specified course.
96
+ * Note: this cannot get correct `visitCount` and `downloadCount` for student
97
+ */
98
+ export async function getFileListByCategory(
99
+ s: Session,
100
+ courseID: string,
101
+ categoryId: string,
102
+ courseType: CourseType = CourseType.STUDENT,
103
+ ): Promise<File[]> {
104
+ if (courseType === CourseType.STUDENT) {
105
+ const json = await (await s.fetchWithToken(URLS.LEARN_FILE_LIST_BY_CATEGORY_STUDENT(courseID, categoryId))).json();
106
+ if (json.result !== 'success') {
107
+ return Promise.reject({
108
+ reason: FailReason.INVALID_RESPONSE,
109
+ extra: json,
110
+ });
111
+ }
112
+
113
+ const result = (json.object ?? []) as any[];
114
+
115
+ return result.map((f) => {
116
+ const fileId = f[7];
117
+ const title = decodeHTML(f[1]);
118
+ const rawSize = f[9];
119
+ const size = formatFileSize(rawSize);
120
+ const downloadUrl = URLS.LEARN_FILE_DOWNLOAD(fileId, courseType);
121
+ const previewUrl = URLS.LEARN_FILE_PREVIEW(ContentType.FILE, fileId, courseType, s.previewFirstPage);
122
+ return {
123
+ id: f[0],
124
+ fileId,
125
+ title,
126
+ description: decodeHTML(f[5]),
127
+ rawSize,
128
+ size,
129
+ uploadTime: f[6],
130
+ publishTime: f[10],
131
+ downloadUrl,
132
+ previewUrl,
133
+ isNew: f[8] === 1,
134
+ markedImportant: f[2] === 1,
135
+ visitCount: 0,
136
+ downloadCount: 0,
137
+ fileType: f[13],
138
+ remoteFile: {
139
+ id: fileId,
140
+ name: title,
141
+ downloadUrl,
142
+ previewUrl,
143
+ size,
144
+ },
145
+ isFavorite: f[11],
146
+ comment: f[14] ?? undefined,
147
+ } satisfies File;
148
+ });
149
+ } else {
150
+ const json = await (
151
+ await s.fetchWithToken(URLS.LEARN_FILE_LIST_BY_CATEGORY_TEACHER, {
152
+ method: 'POST',
153
+ body: URLS.LEARN_FILE_LIST_BY_CATEGORY_TEACHER_FORM_DATA(courseID, categoryId),
154
+ })
155
+ ).json();
156
+ if (json.result !== 'success') {
157
+ return Promise.reject({
158
+ reason: FailReason.INVALID_RESPONSE,
159
+ extra: json,
160
+ });
161
+ }
162
+
163
+ const result = (json.object.aaData ?? []) as any[];
164
+
165
+ return result.map((f) => {
166
+ const title = decodeHTML(f.bt);
167
+ const fileId = f.wjid;
168
+ const uploadTime = f.scsj;
169
+ const downloadUrl = URLS.LEARN_FILE_DOWNLOAD(fileId, courseType);
170
+ const previewUrl = URLS.LEARN_FILE_PREVIEW(ContentType.FILE, fileId, courseType, s.previewFirstPage);
171
+ return {
172
+ id: f.kjxxid,
173
+ fileId,
174
+ title,
175
+ description: decodeHTML(f.ms),
176
+ rawSize: f.wjdx,
177
+ size: f.fileSize,
178
+ uploadTime,
179
+ publishTime: uploadTime,
180
+ downloadUrl,
181
+ previewUrl,
182
+ isNew: f.isNew ?? false,
183
+ markedImportant: f.sfqd === 1,
184
+ visitCount: f.xsllcs ?? f.llcs ?? 0,
185
+ downloadCount: f.xzcs ?? 0,
186
+ fileType: f.wjlx,
187
+ remoteFile: {
188
+ id: fileId,
189
+ name: title,
190
+ downloadUrl,
191
+ previewUrl,
192
+ size: f.fileSize,
193
+ },
194
+ } satisfies File;
195
+ });
196
+ }
197
+ }
@@ -0,0 +1,284 @@
1
+ // Homework (课程作业), excellent homework and homework submission.
2
+
3
+ import type { Element } from 'cheerio';
4
+ import { Base64 } from 'js-base64';
5
+
6
+ import type { Session } from './request.ts';
7
+ import { $, YES } from './request.ts';
8
+ import type {
9
+ ApiError,
10
+ ExcellentHomework,
11
+ Homework,
12
+ HomeworkListOptions,
13
+ HomeworkTA,
14
+ IExcellentHomework,
15
+ IHomework,
16
+ IHomeworkDetail,
17
+ IHomeworkStatus,
18
+ IHomeworkSubmitAttachment,
19
+ RemoteFile,
20
+ } from './types.ts';
21
+ import { ContentType, CourseType, FailReason } from './types.ts';
22
+ import * as URLS from './urls.ts';
23
+ import { GRADE_LEVEL_MAP, decodeHTML, isSelected, trimAndDefine } from './utils.ts';
24
+
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(s: Session, courseID: string): Promise<Homework[]>;
30
+
31
+ export function getHomeworkList(
32
+ s: Session,
33
+ courseID: string,
34
+ courseType: CourseType.STUDENT,
35
+ options?: HomeworkListOptions,
36
+ ): Promise<Homework[]>;
37
+
38
+ export function getHomeworkList(s: Session, courseID: string, courseType: CourseType.TEACHER): Promise<HomeworkTA[]>;
39
+
40
+ export async function getHomeworkList(
41
+ s: Session,
42
+ courseID: string,
43
+ courseType: CourseType = CourseType.STUDENT,
44
+ options: HomeworkListOptions = {},
45
+ ): Promise<Homework[] | HomeworkTA[]> {
46
+ if (courseType === CourseType.TEACHER) {
47
+ const json = await (
48
+ await s.fetchWithToken(URLS.LEARN_HOMEWORK_LIST_TEACHER, {
49
+ method: 'POST',
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
+ }
59
+
60
+ const result = (json.object?.aaData ?? []) as any[];
61
+
62
+ return result.map(
63
+ (d) =>
64
+ ({
65
+ id: d.zyid,
66
+ index: d.wz,
67
+ title: decodeHTML(d.bt),
68
+ description: decodeHTML(Base64.decode(d.nr)),
69
+ publisherId: d.fbr,
70
+ publishTime: d.fbsj,
71
+ startTime: d.kssj,
72
+ deadline: d.jzsj,
73
+ lateSubmissionDeadline: d.bjjzsj ?? undefined,
74
+ url: URLS.LEARN_HOMEWORK_DETAIL_TEACHER(courseID, d.zyid),
75
+ completionType: d.zywcfs,
76
+ submissionType: d.zytjfs,
77
+ gradedCount: d.ypys,
78
+ submittedCount: d.yjs,
79
+ unsubmittedCount: d.wjs,
80
+ }) satisfies HomeworkTA,
81
+ );
82
+ } else {
83
+ const [lists, excellentHomeworkListByHomework] = await Promise.all([
84
+ Promise.all(
85
+ URLS.LEARN_HOMEWORK_LIST_SOURCE.map((source) => getHomeworkListAtUrl(s, courseID, source.url, source.status)),
86
+ ),
87
+ // Don't block the whole process if excellent homework list cannot be fetched
88
+ getExcellentHomeworkListByHomework(s, courseID).catch((): { [id: string]: ExcellentHomework[] } => ({})),
89
+ ]);
90
+ return Promise.all(
91
+ lists.flat().map(async (h) => {
92
+ const homework: Homework = { ...h, excellentHomeworkList: excellentHomeworkListByHomework[h.baseId] };
93
+ return isSelected(options.details, homework) ? { ...homework, ...(await getHomeworkDetail(s, h)) } : homework;
94
+ }),
95
+ );
96
+ }
97
+ }
98
+
99
+ async function getHomeworkListAtUrl(
100
+ s: Session,
101
+ courseID: string,
102
+ url: string,
103
+ status: IHomeworkStatus,
104
+ ): Promise<IHomework[]> {
105
+ const json = await (
106
+ await s.fetchWithToken(url, {
107
+ method: 'POST',
108
+ body: URLS.LEARN_PAGE_LIST_FORM_DATA(courseID),
109
+ })
110
+ ).json();
111
+ if (json.result !== 'success') {
112
+ return Promise.reject({
113
+ reason: FailReason.INVALID_RESPONSE,
114
+ extra: json,
115
+ });
116
+ }
117
+
118
+ const result = (json.object?.aaData ?? []) as any[];
119
+
120
+ return result.map(
121
+ (h) =>
122
+ ({
123
+ id: h.xszyid,
124
+ studentHomeworkId: h.xszyid,
125
+ baseId: h.zyid,
126
+ title: decodeHTML(h.bt),
127
+ url: URLS.LEARN_HOMEWORK_PAGE(h.wlkcid, h.xszyid),
128
+ deadline: h.jzsj,
129
+ lateSubmissionDeadline: h.bjjzsj ? h.bjjzsj : undefined,
130
+ isLateSubmission: h.sfbj === YES,
131
+ completionType: h.zywcfs,
132
+ submissionType: h.zytjfs,
133
+ submitUrl: URLS.LEARN_HOMEWORK_SUBMIT_PAGE(h.wlkcid, h.xszyid),
134
+ submitTime: h.scsj === null ? undefined : h.scsj,
135
+ grade: h.cj === null ? undefined : h.cj,
136
+ gradeLevel: GRADE_LEVEL_MAP.get(h.cj),
137
+ graderName: trimAndDefine(h.jsm),
138
+ gradeContent: trimAndDefine(h.pynr),
139
+ gradeTime: h.pysj === null ? undefined : h.pysj,
140
+ isFavorite: h.sfsc === YES,
141
+ favoriteTime: h.scsj === null || h.sfsc !== YES ? undefined : h.scsj,
142
+ comment: h.bznr ?? undefined,
143
+ ...status,
144
+ }) satisfies IHomework,
145
+ );
146
+ }
147
+
148
+ async function getExcellentHomeworkListByHomework(
149
+ s: Session,
150
+ courseID: string,
151
+ ): Promise<{ [id: string]: ExcellentHomework[] }> {
152
+ const json = await (
153
+ await s.fetchWithToken(URLS.LEARN_HOMEWORK_LIST_EXCELLENT, {
154
+ method: 'POST',
155
+ body: URLS.LEARN_PAGE_LIST_FORM_DATA(courseID),
156
+ })
157
+ ).json();
158
+ if (json.result !== 'success') {
159
+ return Promise.reject({
160
+ reason: FailReason.INVALID_RESPONSE,
161
+ extra: json,
162
+ });
163
+ }
164
+
165
+ const result = (json.object?.aaData ?? []) as any[];
166
+
167
+ return result
168
+ .map(
169
+ (h) =>
170
+ ({
171
+ id: h.xszyid,
172
+ baseId: h.zyid,
173
+ title: decodeHTML(h.bt),
174
+ url: URLS.LEARN_HOMEWORK_EXCELLENT_PAGE(h.wlkcid, h.xszyid),
175
+ completionType: h.zywcfs,
176
+ author: {
177
+ id: h.cy?.split(' ')?.[0],
178
+ name: h.cy?.split(' ')?.[1],
179
+ anonymous: h.sfzm === YES,
180
+ },
181
+ }) satisfies IExcellentHomework,
182
+ )
183
+ .reduce<{ [id: string]: ExcellentHomework[] }>((acc, cur) => {
184
+ if (!acc[cur.baseId]) {
185
+ acc[cur.baseId] = [];
186
+ }
187
+ acc[cur.baseId].push(cur);
188
+ return acc;
189
+ }, {});
190
+ }
191
+
192
+ async function parseHomeworkAtUrl(s: Session, url: string): Promise<IHomeworkDetail> {
193
+ const response = await s.fetchWithToken(url);
194
+ const result = $(await response.text());
195
+
196
+ const fileDivs = result('div.list.fujian.clearfix');
197
+
198
+ return {
199
+ description: trimAndDefine(result('div.list.calendar.clearfix > div.fl.right > div.c55').slice(0, 1).html()),
200
+ answerContent: trimAndDefine(result('div.list.calendar.clearfix > div.fl.right > div.c55').slice(1, 2).html()),
201
+ submittedContent: trimAndDefine($(result('div.boxbox').slice(1, 2).toArray())('div.right').slice(2, 3).html()),
202
+ attachment: parseHomeworkFile(s, fileDivs[0]),
203
+ answerAttachment: parseHomeworkFile(s, fileDivs[1]),
204
+ submittedAttachment: parseHomeworkFile(s, fileDivs[2]),
205
+ gradeAttachment: parseHomeworkFile(s, fileDivs[3]),
206
+ };
207
+ }
208
+
209
+ /**
210
+ * Get the detail of one homework or excellent homework: its page (answer, submitted content, attachments) and the
211
+ * description JSON (which wins for `description`). 2 requests.
212
+ */
213
+ export async function getHomeworkDetail(
214
+ s: Session,
215
+ homework: Pick<IHomework | IExcellentHomework, 'url' | 'baseId'>,
216
+ ): Promise<IHomeworkDetail> {
217
+ const [page, description] = await Promise.all([
218
+ parseHomeworkAtUrl(s, homework.url),
219
+ getHomeworkDescription(s, homework.baseId),
220
+ ]);
221
+ return { ...page, ...description };
222
+ }
223
+
224
+ async function getHomeworkDescription(s: Session, baseId: string): Promise<IHomeworkDetail> {
225
+ const json = await (
226
+ await s.fetchWithToken(URLS.LEARN_HOMEWORK_DETAIL, {
227
+ method: 'POST',
228
+ body: URLS.LEARN_HOMEWORK_DETAIL_FORM_DATA(baseId),
229
+ })
230
+ ).json();
231
+ if (json.result !== 'success') {
232
+ throw {
233
+ reason: FailReason.INVALID_RESPONSE,
234
+ extra: json,
235
+ } as ApiError;
236
+ }
237
+ return {
238
+ description: trimAndDefine(json.msg),
239
+ };
240
+ }
241
+
242
+ function parseHomeworkFile(s: Session, fileDiv: Element): RemoteFile | undefined {
243
+ const fileNode = $(fileDiv)('.ftitle').children('a')[0] ?? $(fileDiv)('.fl').children('a')[0];
244
+ if (fileNode !== undefined) {
245
+ const size = trimAndDefine($(fileDiv)('.fl > span[class^="color"]').first().text())!;
246
+ const params = new URLSearchParams(fileNode.attribs.href.split('?').slice(-1)[0]);
247
+ const attachmentId = params.get('fileId')!;
248
+ // so dirty here...
249
+ let downloadUrl = URLS.LEARN_PREFIX + fileNode.attribs.href;
250
+ if (params.has('downloadUrl')) {
251
+ downloadUrl = URLS.LEARN_PREFIX + params.get('downloadUrl')!;
252
+ }
253
+ return {
254
+ id: attachmentId,
255
+ name: (fileNode.children[0] as { data: string }).data,
256
+ downloadUrl,
257
+ previewUrl: URLS.LEARN_FILE_PREVIEW(ContentType.HOMEWORK, attachmentId, CourseType.STUDENT, s.previewFirstPage),
258
+ size,
259
+ };
260
+ } else {
261
+ return undefined;
262
+ }
263
+ }
264
+
265
+ export async function submitHomework(
266
+ s: Session,
267
+ id: string,
268
+ content = '',
269
+ attachment?: IHomeworkSubmitAttachment,
270
+ removeAttachment = false,
271
+ ): Promise<void> {
272
+ const json = await (
273
+ await s.fetchWithToken(URLS.LEARN_HOMEWORK_SUBMIT(), {
274
+ method: 'POST',
275
+ body: URLS.LEARN_HOMEWORK_SUBMIT_FORM_DATA(id, content, attachment, removeAttachment),
276
+ })
277
+ ).json();
278
+ if (json.result !== 'success') {
279
+ return Promise.reject({
280
+ reason: FailReason.OPERATION_FAILED,
281
+ extra: json,
282
+ });
283
+ }
284
+ }