@robertying/thu-learn-lib 5.1.0 → 6.0.1

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/README.md +81 -248
  2. package/lib/auth.d.ts +14 -3
  3. package/lib/auth.d.ts.map +1 -1
  4. package/lib/auth.js +56 -58
  5. package/lib/auth.js.map +1 -1
  6. package/lib/calendar.d.ts +7 -2
  7. package/lib/calendar.d.ts.map +1 -1
  8. package/lib/calendar.js +99 -14
  9. package/lib/calendar.js.map +1 -1
  10. package/lib/contents.d.ts.map +1 -1
  11. package/lib/contents.js +4 -9
  12. package/lib/contents.js.map +1 -1
  13. package/lib/courses.d.ts.map +1 -1
  14. package/lib/courses.js +11 -25
  15. package/lib/courses.js.map +1 -1
  16. package/lib/discussions.d.ts.map +1 -1
  17. package/lib/discussions.js +6 -17
  18. package/lib/discussions.js.map +1 -1
  19. package/lib/favorites.d.ts +1 -0
  20. package/lib/favorites.d.ts.map +1 -1
  21. package/lib/favorites.js +13 -48
  22. package/lib/favorites.js.map +1 -1
  23. package/lib/files.d.ts +3 -3
  24. package/lib/files.d.ts.map +1 -1
  25. package/lib/files.js +109 -133
  26. package/lib/files.js.map +1 -1
  27. package/lib/homework.d.ts +34 -3
  28. package/lib/homework.d.ts.map +1 -1
  29. package/lib/homework.js +135 -79
  30. package/lib/homework.js.map +1 -1
  31. package/lib/index.d.ts +39 -19
  32. package/lib/index.d.ts.map +1 -1
  33. package/lib/index.js +44 -17
  34. package/lib/index.js.map +1 -1
  35. package/lib/notices.d.ts +3 -3
  36. package/lib/notices.d.ts.map +1 -1
  37. package/lib/notices.js +4 -20
  38. package/lib/notices.js.map +1 -1
  39. package/lib/questionnaires.d.ts.map +1 -1
  40. package/lib/questionnaires.js +8 -14
  41. package/lib/questionnaires.js.map +1 -1
  42. package/lib/request.d.ts +27 -13
  43. package/lib/request.d.ts.map +1 -1
  44. package/lib/request.js +84 -51
  45. package/lib/request.js.map +1 -1
  46. package/lib/timetable.d.ts +26 -0
  47. package/lib/timetable.d.ts.map +1 -0
  48. package/lib/timetable.js +110 -0
  49. package/lib/timetable.js.map +1 -0
  50. package/lib/types.d.ts +108 -75
  51. package/lib/types.d.ts.map +1 -1
  52. package/lib/types.js +3 -0
  53. package/lib/types.js.map +1 -1
  54. package/lib/urls.d.ts +10 -20
  55. package/lib/urls.d.ts.map +1 -1
  56. package/lib/urls.js +11 -56
  57. package/lib/urls.js.map +1 -1
  58. package/lib/user.d.ts +7 -2
  59. package/lib/user.d.ts.map +1 -1
  60. package/lib/user.js +19 -5
  61. package/lib/user.js.map +1 -1
  62. package/lib/utils.d.ts +42 -4
  63. package/lib/utils.d.ts.map +1 -1
  64. package/lib/utils.js +78 -31
  65. package/lib/utils.js.map +1 -1
  66. package/package.json +8 -7
  67. package/src/auth.ts +62 -73
  68. package/src/calendar.ts +110 -16
  69. package/src/contents.ts +7 -9
  70. package/src/courses.ts +12 -26
  71. package/src/discussions.ts +8 -19
  72. package/src/favorites.ts +22 -48
  73. package/src/files.ts +121 -139
  74. package/src/homework.ts +184 -102
  75. package/src/index.ts +67 -48
  76. package/src/notices.ts +19 -26
  77. package/src/questionnaires.ts +18 -15
  78. package/src/request.ts +86 -62
  79. package/src/timetable.ts +121 -0
  80. package/src/types.ts +111 -86
  81. package/src/urls.ts +13 -64
  82. package/src/user.ts +21 -6
  83. package/src/utils.ts +96 -32
package/src/favorites.ts CHANGED
@@ -1,23 +1,25 @@
1
1
  // Favorites (收藏) and comments (备注).
2
2
 
3
3
  import type { Session } from './request.ts';
4
- import { YES } from './request.ts';
5
4
  import type { CommentItem, FavoriteItem } from './types.ts';
6
5
  import { ContentType, FailReason } from './types.ts';
7
6
  import * as URLS from './urls.ts';
8
- import { CONTENT_TYPE_MAP_REVERSE, decodeHTML } from './utils.ts';
7
+ import {
8
+ CONTENT_TYPE_MAP_REVERSE,
9
+ YES,
10
+ decodeHTML,
11
+ expectSuccess,
12
+ toRequiredServerTime,
13
+ toServerTime,
14
+ } from './utils.ts';
9
15
 
10
16
  /**
11
17
  * Add an item to favorites. (收藏)
18
+ * Success is `result: 'success'`; the message is in the website's language ("收藏成功", "Added successfully.").
12
19
  */
13
20
  export async function addToFavorites(s: Session, type: ContentType, id: string): Promise<void> {
14
21
  const json = await s.fetchJsonWithToken(URLS.LEARN_FAVORITE_ADD(type, id));
15
- if (json.result !== 'success' || !json.msg?.endsWith?.('成功')) {
16
- return Promise.reject({
17
- reason: FailReason.OPERATION_FAILED,
18
- extra: json,
19
- });
20
- }
22
+ expectSuccess(json, FailReason.OPERATION_FAILED);
21
23
  }
22
24
 
23
25
  /**
@@ -25,12 +27,7 @@ export async function addToFavorites(s: Session, type: ContentType, id: string):
25
27
  */
26
28
  export async function removeFromFavorites(s: Session, id: string): Promise<void> {
27
29
  const json = await s.fetchJsonWithToken(URLS.LEARN_FAVORITE_REMOVE(id));
28
- if (json.result !== 'success' || !json.msg?.endsWith?.('成功')) {
29
- return Promise.reject({
30
- reason: FailReason.OPERATION_FAILED,
31
- extra: json,
32
- });
33
- }
30
+ expectSuccess(json, FailReason.OPERATION_FAILED);
34
31
  }
35
32
 
36
33
  /**
@@ -42,12 +39,7 @@ export async function getFavorites(s: Session, courseID?: string, type?: Content
42
39
  method: 'POST',
43
40
  body: URLS.LEARN_PAGE_LIST_FORM_DATA(courseID),
44
41
  });
45
- if (json.result !== 'success') {
46
- return Promise.reject({
47
- reason: FailReason.INVALID_RESPONSE,
48
- extra: json,
49
- });
50
- }
42
+ expectSuccess(json, FailReason.INVALID_RESPONSE);
51
43
  const result = (json.object?.aaData ?? []) as any[];
52
44
  return result
53
45
  .map((e): FavoriteItem | undefined => {
@@ -57,15 +49,17 @@ export async function getFavorites(s: Session, courseID?: string, type?: Content
57
49
  id: e.ywid,
58
50
  type: itemType,
59
51
  title: decodeHTML(e.ywbt),
60
- time: itemType === ContentType.DISCUSSION || itemType === ContentType.QUESTION ? e.tlsj : e.ywsj,
52
+ time: toRequiredServerTime(
53
+ itemType === ContentType.DISCUSSION || itemType === ContentType.QUESTION ? e.tlsj : e.ywsj,
54
+ ),
61
55
  state: e.ywzt,
62
56
  extra: e.ywbz ?? undefined,
63
57
  semesterId: e.xnxq,
64
58
  courseId: e.wlkcid,
65
59
  pinned: e.sfzd === YES,
66
- pinnedTime: e.zdsj === null ? undefined : e.zdsj, // Note: this field is originally unix timestamp instead of string
60
+ pinnedTime: toServerTime(e.zdsj),
67
61
  comment: e.bznr ?? undefined,
68
- addedTime: e.scsj,
62
+ addedTime: toRequiredServerTime(e.scsj),
69
63
  itemId: e.id,
70
64
  } satisfies FavoriteItem;
71
65
  })
@@ -80,12 +74,7 @@ export async function pinFavoriteItem(s: Session, id: string): Promise<void> {
80
74
  method: 'POST',
81
75
  body: URLS.LEARN_FAVORITE_PIN_UNPIN_FORM_DATA(id),
82
76
  });
83
- if (json.result !== 'success') {
84
- return Promise.reject({
85
- reason: FailReason.OPERATION_FAILED,
86
- extra: json,
87
- });
88
- }
77
+ expectSuccess(json, FailReason.OPERATION_FAILED);
89
78
  }
90
79
 
91
80
  /**
@@ -96,12 +85,7 @@ export async function unpinFavoriteItem(s: Session, id: string): Promise<void> {
96
85
  method: 'POST',
97
86
  body: URLS.LEARN_FAVORITE_PIN_UNPIN_FORM_DATA(id),
98
87
  });
99
- if (json.result !== 'success') {
100
- return Promise.reject({
101
- reason: FailReason.OPERATION_FAILED,
102
- extra: json,
103
- });
104
- }
88
+ expectSuccess(json, FailReason.OPERATION_FAILED);
105
89
  }
106
90
 
107
91
  /**
@@ -113,12 +97,7 @@ export async function setComment(s: Session, type: ContentType, id: string, cont
113
97
  method: 'POST',
114
98
  body: URLS.LEARN_COMMENT_SET_FORM_DATA(type, id, content),
115
99
  });
116
- if (json.result !== 'success' || !json.msg?.endsWith?.('成功')) {
117
- return Promise.reject({
118
- reason: FailReason.OPERATION_FAILED,
119
- extra: json,
120
- });
121
- }
100
+ expectSuccess(json, FailReason.OPERATION_FAILED);
122
101
  }
123
102
 
124
103
  /**
@@ -130,12 +109,7 @@ export async function getComments(s: Session, courseID?: string, type?: ContentT
130
109
  method: 'POST',
131
110
  body: URLS.LEARN_PAGE_LIST_FORM_DATA(courseID),
132
111
  });
133
- if (json.result !== 'success') {
134
- return Promise.reject({
135
- reason: FailReason.INVALID_RESPONSE,
136
- extra: json,
137
- });
138
- }
112
+ expectSuccess(json, FailReason.INVALID_RESPONSE);
139
113
  const result = (json.object?.aaData ?? []) as any[];
140
114
  return result
141
115
  .map((e): CommentItem | undefined => {
@@ -149,7 +123,7 @@ export async function getComments(s: Session, courseID?: string, type?: ContentT
149
123
  title: decodeHTML(e.ywbt),
150
124
  semesterId: e.xnxq,
151
125
  courseId: e.wlkcid,
152
- commentTime: e.cjsj,
126
+ commentTime: toRequiredServerTime(e.cjsj),
153
127
  itemId: e.id,
154
128
  } satisfies CommentItem;
155
129
  })
package/src/files.ts CHANGED
@@ -1,24 +1,23 @@
1
1
  // Course files (课程文件) and file categories.
2
2
 
3
3
  import type { Session } from './request.ts';
4
- import type { File, FileCategory } from './types.ts';
4
+ import type { CourseFile, FileCategory } from './types.ts';
5
5
  import { ContentType, CourseType, FailReason } from './types.ts';
6
6
  import * as URLS from './urls.ts';
7
- import { decodeHTML, formatFileSize } from './utils.ts';
7
+ import { YES, decodeHTML, expectSuccess, formatFileSize, toRequiredServerTime } from './utils.ts';
8
8
 
9
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
- ): Promise<File[]> {
15
- const json = await s.fetchJsonWithToken(URLS.LEARN_FILE_LIST(courseID, courseType));
16
- if (json.result !== 'success') {
17
- return Promise.reject({
18
- reason: FailReason.INVALID_RESPONSE,
19
- extra: json,
20
- });
21
- }
14
+ ): Promise<CourseFile[]> {
15
+ // categories only fill `category`; don't fail the whole list when they can't be fetched
16
+ const [json, categoryList] = await Promise.all([
17
+ s.fetchJsonWithToken(URLS.LEARN_FILE_LIST(courseID, courseType)),
18
+ getFileCategoryList(s, courseID, courseType).catch((): FileCategory[] => []),
19
+ ]);
20
+ expectSuccess(json, FailReason.INVALID_RESPONSE);
22
21
 
23
22
  let result: any[] = [];
24
23
  if (Array.isArray(json.object?.resultsList)) {
@@ -29,42 +28,47 @@ export async function getFileList(
29
28
  result = json.object;
30
29
  }
31
30
 
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
31
  const categories = new Map(categoryList.map((c) => [c.id, c]));
32
+ return result.map((f) => ({ ...parseFile(s, f, courseType), category: categories.get(f.kjflid) }));
33
+ }
35
34
 
36
- return result.map((f) => {
37
- const title = decodeHTML(f.bt);
38
- const fileId = f.wjid;
39
- const uploadTime = f.scsj;
40
- const downloadUrl = URLS.LEARN_FILE_DOWNLOAD(fileId, courseType);
41
- const previewUrl = URLS.LEARN_FILE_PREVIEW(ContentType.FILE, fileId, courseType, s.previewFirstPage);
42
- return {
43
- id: f.kjxxid,
44
- fileId,
45
- category: categories.get(f.kjflid),
46
- title,
47
- description: decodeHTML(f.ms),
48
- rawSize: f.wjdx,
49
- size: f.fileSize,
50
- uploadTime,
51
- publishTime: uploadTime,
35
+ /** a 0 / 1 flag, which the server sends as a number, a string or (in older answers) a boolean */
36
+ const isSet = (flag: unknown): boolean => flag === true || Number(flag) === 1;
37
+
38
+ /** a count or size, which the server sends as a number or a string */
39
+ const toNumber = (value: unknown): number => Number(value ?? 0) || 0;
40
+
41
+ /** a file of the object-shaped lists (`getFileList`, and `getFileListByCategory` for a teacher) */
42
+ function parseFile(s: Session, f: any, courseType: CourseType): CourseFile {
43
+ const title = decodeHTML(f.bt);
44
+ const fileId = f.wjid;
45
+ const uploadTime = toRequiredServerTime(f.scsj);
46
+ const downloadUrl = URLS.LEARN_FILE_DOWNLOAD(fileId, courseType);
47
+ const previewUrl = URLS.LEARN_FILE_PREVIEW(ContentType.FILE, fileId, courseType, s.previewFirstPage);
48
+ return {
49
+ id: f.kjxxid,
50
+ fileId,
51
+ title,
52
+ description: decodeHTML(f.ms),
53
+ rawSize: toNumber(f.wjdx),
54
+ size: f.fileSize,
55
+ uploadTime,
56
+ publishTime: uploadTime,
57
+ downloadUrl,
58
+ previewUrl,
59
+ isNew: isSet(f.isNew), // 1 / 0 for a student
60
+ markedImportant: isSet(f.sfqd), // string '1' for a teacher, number 1 for a student
61
+ visitCount: toNumber(f.xsllcs ?? f.llcs),
62
+ downloadCount: toNumber(f.xzcs),
63
+ fileType: f.wjlx,
64
+ remoteFile: {
65
+ id: fileId,
66
+ name: title,
52
67
  downloadUrl,
53
68
  previewUrl,
54
- isNew: f.isNew ?? false,
55
- markedImportant: f.sfqd === 1,
56
- visitCount: f.xsllcs ?? f.llcs ?? 0,
57
- downloadCount: f.xzcs ?? 0,
58
- fileType: f.wjlx,
59
- remoteFile: {
60
- id: fileId,
61
- name: title,
62
- downloadUrl,
63
- previewUrl,
64
- size: f.fileSize,
65
- },
66
- } satisfies File;
67
- });
69
+ size: f.fileSize,
70
+ },
71
+ };
68
72
  }
69
73
 
70
74
  /** Get file categories of the specified course. */
@@ -74,12 +78,7 @@ export async function getFileCategoryList(
74
78
  courseType: CourseType = CourseType.STUDENT,
75
79
  ): Promise<FileCategory[]> {
76
80
  const json = await s.fetchJsonWithToken(URLS.LEARN_FILE_CATEGORY_LIST(courseID, courseType));
77
- if (json.result !== 'success') {
78
- return Promise.reject({
79
- reason: FailReason.INVALID_RESPONSE,
80
- extra: json,
81
- });
82
- }
81
+ expectSuccess(json, FailReason.INVALID_RESPONSE);
83
82
 
84
83
  const result = (json.object?.rows ?? []) as any[];
85
84
 
@@ -88,7 +87,7 @@ export async function getFileCategoryList(
88
87
  ({
89
88
  id: c.kjflid,
90
89
  title: decodeHTML(c.bt),
91
- creationTime: c.czsj,
90
+ creationTime: toRequiredServerTime(c.czsj),
92
91
  }) satisfies FileCategory,
93
92
  );
94
93
  }
@@ -102,96 +101,79 @@ export async function getFileListByCategory(
102
101
  courseID: string,
103
102
  categoryId: string,
104
103
  courseType: CourseType = CourseType.STUDENT,
105
- ): Promise<File[]> {
106
- if (courseType === CourseType.STUDENT) {
107
- const json = await s.fetchJsonWithToken(URLS.LEARN_FILE_LIST_BY_CATEGORY_STUDENT(courseID, categoryId));
108
- if (json.result !== 'success') {
109
- return Promise.reject({
110
- reason: FailReason.INVALID_RESPONSE,
111
- extra: json,
112
- });
113
- }
114
-
115
- const result = (json.object ?? []) as any[];
116
-
117
- return result.map((f) => {
118
- const fileId = f[7];
119
- const title = decodeHTML(f[1]);
120
- const rawSize = f[9];
121
- const size = formatFileSize(rawSize);
122
- const downloadUrl = URLS.LEARN_FILE_DOWNLOAD(fileId, courseType);
123
- const previewUrl = URLS.LEARN_FILE_PREVIEW(ContentType.FILE, fileId, courseType, s.previewFirstPage);
124
- return {
125
- id: f[0],
126
- fileId,
127
- title,
128
- description: decodeHTML(f[5]),
129
- rawSize,
130
- size,
131
- uploadTime: f[6],
132
- publishTime: f[10],
133
- downloadUrl,
134
- previewUrl,
135
- isNew: f[8] === 1,
136
- markedImportant: f[2] === 1,
137
- visitCount: 0,
138
- downloadCount: 0,
139
- fileType: f[13],
140
- remoteFile: {
141
- id: fileId,
142
- name: title,
143
- downloadUrl,
144
- previewUrl,
145
- size,
146
- },
147
- isFavorite: f[11],
148
- comment: f[14] ?? undefined,
149
- } satisfies File;
150
- });
151
- } else {
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
- 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,
104
+ ): Promise<CourseFile[]> {
105
+ return courseType === CourseType.STUDENT
106
+ ? getStudentFileListByCategory(s, courseID, categoryId)
107
+ : getTeacherFileListByCategory(s, courseID, categoryId);
108
+ }
109
+
110
+ /** the columns of a row of the student's file list of a category, which is an array instead of an object */
111
+ const COLUMN = {
112
+ ID: 0,
113
+ TITLE: 1,
114
+ MARKED_IMPORTANT: 2,
115
+ DESCRIPTION: 5,
116
+ UPLOAD_TIME: 6,
117
+ FILE_ID: 7,
118
+ IS_NEW: 8,
119
+ RAW_SIZE: 9,
120
+ PUBLISH_TIME: 10,
121
+ IS_FAVORITE: 11,
122
+ FILE_TYPE: 13,
123
+ COMMENT: 14,
124
+ } as const;
125
+
126
+ async function getStudentFileListByCategory(s: Session, courseID: string, categoryId: string): Promise<CourseFile[]> {
127
+ const json = await s.fetchJsonWithToken(URLS.LEARN_FILE_LIST_BY_CATEGORY_STUDENT(courseID, categoryId));
128
+ expectSuccess(json, FailReason.INVALID_RESPONSE);
129
+
130
+ const result = (json.object ?? []) as any[][];
131
+
132
+ return result.map((f) => {
133
+ const fileId = f[COLUMN.FILE_ID];
134
+ const title = decodeHTML(f[COLUMN.TITLE]);
135
+ const rawSize = toNumber(f[COLUMN.RAW_SIZE]);
136
+ const size = formatFileSize(rawSize);
137
+ const downloadUrl = URLS.LEARN_FILE_DOWNLOAD(fileId, CourseType.STUDENT);
138
+ const previewUrl = URLS.LEARN_FILE_PREVIEW(ContentType.FILE, fileId, CourseType.STUDENT, s.previewFirstPage);
139
+ return {
140
+ id: f[COLUMN.ID],
141
+ fileId,
142
+ title,
143
+ description: decodeHTML(f[COLUMN.DESCRIPTION]),
144
+ rawSize,
145
+ size,
146
+ uploadTime: toRequiredServerTime(f[COLUMN.UPLOAD_TIME]),
147
+ publishTime: toRequiredServerTime(f[COLUMN.PUBLISH_TIME]),
148
+ downloadUrl,
149
+ previewUrl,
150
+ isNew: isSet(f[COLUMN.IS_NEW]),
151
+ markedImportant: isSet(f[COLUMN.MARKED_IMPORTANT]),
152
+ // not in this list
153
+ visitCount: 0,
154
+ downloadCount: 0,
155
+ fileType: f[COLUMN.FILE_TYPE],
156
+ remoteFile: {
157
+ id: fileId,
158
+ name: title,
180
159
  downloadUrl,
181
160
  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
- }
161
+ size,
162
+ },
163
+ isFavorite: f[COLUMN.IS_FAVORITE] === YES, // '是' / '否' like the other lists
164
+ comment: f[COLUMN.COMMENT] ?? undefined,
165
+ } satisfies CourseFile;
166
+ });
167
+ }
168
+
169
+ async function getTeacherFileListByCategory(s: Session, courseID: string, categoryId: string): Promise<CourseFile[]> {
170
+ const json = await s.fetchJsonWithToken(URLS.LEARN_FILE_LIST_BY_CATEGORY_TEACHER, {
171
+ method: 'POST',
172
+ body: URLS.LEARN_FILE_LIST_BY_CATEGORY_TEACHER_FORM_DATA(courseID, categoryId),
173
+ });
174
+ expectSuccess(json, FailReason.INVALID_RESPONSE);
175
+
176
+ const result = (json.object?.aaData ?? []) as any[];
177
+
178
+ return result.map((f) => parseFile(s, f, CourseType.TEACHER));
197
179
  }