@robertying/thu-learn-lib 0.0.0-stage → 4.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/LICENSE +21 -0
- package/README.md +324 -2
- package/lib/auth.d.ts +11 -0
- package/lib/auth.d.ts.map +1 -0
- package/lib/auth.js +99 -0
- package/lib/auth.js.map +1 -0
- package/lib/calendar.d.ts +16 -0
- package/lib/calendar.d.ts.map +1 -0
- package/lib/calendar.js +81 -0
- package/lib/calendar.js.map +1 -0
- package/lib/contents.d.ts +9 -0
- package/lib/contents.d.ts.map +1 -0
- package/lib/contents.js +52 -0
- package/lib/contents.js.map +1 -0
- package/lib/courses.d.ts +9 -0
- package/lib/courses.d.ts.map +1 -0
- package/lib/courses.js +82 -0
- package/lib/courses.js.map +1 -0
- package/lib/discussions.d.ts +11 -0
- package/lib/discussions.d.ts.map +1 -0
- package/lib/discussions.js +56 -0
- package/lib/discussions.js.map +1 -0
- package/lib/favorites.d.ts +35 -0
- package/lib/favorites.d.ts.map +1 -0
- package/lib/favorites.js +150 -0
- package/lib/favorites.js.map +1 -0
- package/lib/files.d.ts +13 -0
- package/lib/files.d.ts.map +1 -0
- package/lib/files.js +167 -0
- package/lib/files.js.map +1 -0
- package/lib/homework.d.ts +9 -0
- package/lib/homework.d.ts.map +1 -0
- package/lib/homework.js +193 -0
- package/lib/homework.js.map +1 -0
- package/lib/index.d.ts +105 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +181 -0
- package/lib/index.js.map +1 -0
- package/lib/notices.d.ts +6 -0
- package/lib/notices.d.ts.map +1 -0
- package/lib/notices.js +84 -0
- package/lib/notices.js.map +1 -0
- package/lib/questionnaires.d.ts +7 -0
- package/lib/questionnaires.d.ts.map +1 -0
- package/lib/questionnaires.js +62 -0
- package/lib/questionnaires.js.map +1 -0
- package/lib/request.d.ts +31 -0
- package/lib/request.d.ts.map +1 -0
- package/lib/request.js +90 -0
- package/lib/request.js.map +1 -0
- package/lib/types.d.ts +392 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +97 -0
- package/lib/types.js.map +1 -0
- package/lib/urls.d.ts +78 -0
- package/lib/urls.d.ts.map +1 -0
- package/lib/urls.js +160 -0
- package/lib/urls.js.map +1 -0
- package/lib/user.d.ts +7 -0
- package/lib/user.d.ts.map +1 -0
- package/lib/user.js +22 -0
- package/lib/user.js.map +1 -0
- package/lib/utils.d.ts +13 -0
- package/lib/utils.d.ts.map +1 -0
- package/lib/utils.js +115 -0
- package/lib/utils.js.map +1 -0
- package/package.json +67 -4
- package/src/auth.ts +121 -0
- package/src/calendar.ts +102 -0
- package/src/contents.ts +67 -0
- package/src/courses.ts +97 -0
- package/src/discussions.ts +80 -0
- package/src/favorites.ts +167 -0
- package/src/files.ts +197 -0
- package/src/homework.ts +279 -0
- package/src/index.ts +282 -0
- package/src/notices.ts +110 -0
- package/src/questionnaires.ts +76 -0
- package/src/request.ts +101 -0
- package/src/types.ts +445 -0
- package/src/urls.ts +257 -0
- package/src/user.ts +28 -0
- package/src/utils.ts +120 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2019-2022 Harry Chen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,325 @@
|
|
|
1
|
-
#
|
|
1
|
+
# thu-learn-lib
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/robertying/thu-learn-lib/actions/workflows/check.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/@robertying/thu-learn-lib)
|
|
5
|
+
|
|
6
|
+
This is a JavaScript library aimed to provide a program-friendly interface of [web Learning of Tsinghua University](https://learn.tsinghua.edu.cn).
|
|
7
|
+
|
|
8
|
+
This is [robertying](https://github.com/robertying)'s fork of [Harry-Chen/thu-learn-lib](https://github.com/Harry-Chen/thu-learn-lib), maintained for [learnX](https://github.com/robertying/learnX) and published as `@robertying/thu-learn-lib`.
|
|
9
|
+
|
|
10
|
+
This project is licensed under MIT License.
|
|
11
|
+
|
|
12
|
+
## Compatibility
|
|
13
|
+
|
|
14
|
+
The library is ESM-only and uses [node-fetch-native](https://github.com/unjs/node-fetch-native) with [fetch-cookie](https://github.com/valeriangalliat/fetch-cookie) for cookie and redirect support. It runs in Node and in React Native; you can also pass your own `fetch` in `HelperConfig`.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pnpm add @robertying/thu-learn-lib
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Development
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pnpm install
|
|
26
|
+
pnpm typecheck
|
|
27
|
+
pnpm lint
|
|
28
|
+
pnpm format
|
|
29
|
+
pnpm test
|
|
30
|
+
pnpm build # emits ESM + .d.ts to lib/
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The code is split by area under `src/`: `request.ts` (session, CSRF token, re-login), `auth.ts`, `courses.ts`, `notices.ts`, `files.ts`, `homework.ts`, `discussions.ts`, `questionnaires.ts`, `favorites.ts`, `calendar.ts`, `contents.ts` and `user.ts`. `index.ts` holds the public `Learn2018Helper` class, which passes its private session to those functions.
|
|
34
|
+
|
|
35
|
+
Linting uses type-aware [oxlint](https://oxc.rs/docs/guide/usage/linter) and formatting uses [oxfmt](https://oxc.rs/docs/guide/usage/formatter).
|
|
36
|
+
|
|
37
|
+
## Releasing
|
|
38
|
+
|
|
39
|
+
Bump `version` in `package.json`, merge, then push a matching tag (for example `git tag v4.1.0 && git push origin v4.1.0`). The Release workflow checks, builds, publishes to npm with provenance (npm trusted publishing, no token) and creates a GitHub release.
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
### Authentication related (important changes since v1.2.0)
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
import { Learn2018Helper } from '@robertying/thu-learn-lib';
|
|
47
|
+
|
|
48
|
+
// There are three ways of logging in:
|
|
49
|
+
|
|
50
|
+
// 1. provide a cookie jar with existing cookies (see `tough-cookie`)
|
|
51
|
+
const helper = new Learn2018Helper({ cookieJar });
|
|
52
|
+
// 2. provide nothing, and call login with your credentials
|
|
53
|
+
const helper = new Learn2018Helper();
|
|
54
|
+
// 3. provide a CredentialProvider function, which can be async
|
|
55
|
+
const helper = new Learn2018Helper({
|
|
56
|
+
provider: () => ({ username, password, fingerPrint }),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// `fingerPrint` (and optionally `fingerGenPrint`, `fingerGenPrint3`) is the device fingerprint that
|
|
60
|
+
// id.tsinghua.edu.cn asks for; login fails with `NO_CREDENTIAL` without it.
|
|
61
|
+
|
|
62
|
+
// With the first two methods the session may time out. With a CredentialProvider the library logs in
|
|
63
|
+
// again and retries once when a request finds the session expired, so we strongly recommend it.
|
|
64
|
+
|
|
65
|
+
// Without a CredentialProvider you must log in yourself. With one, the first request logs in.
|
|
66
|
+
try {
|
|
67
|
+
await helper.login(username, password, fingerPrint);
|
|
68
|
+
} catch (e) {
|
|
69
|
+
// e is an ApiError: check e.reason (a FailReason) and e.extra
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// To reuse cookies elsewhere (e.g. for file download), pass your own cookie jar in the config.
|
|
73
|
+
|
|
74
|
+
// Logout if you want, but the cookie jar will not be cleared.
|
|
75
|
+
await helper.logout();
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Content related
|
|
79
|
+
|
|
80
|
+
We currently support both student and teacher (or TA) version of web learning. To keep backwards compatibility, the **default behavior of all APIs is to use the student version**. The following APIs needs `CourseType.TEACHER` as the last (optional) parameter to access the teacher version. You **should use them when you are the teacher / TA** of one course, for you will get nothing from the student version in that case.
|
|
81
|
+
|
|
82
|
+
We do not maintain a global type in `Learn2018Helper` class, for there can be the situation that one can be a student and teacher of the same course simultaneously, on which by using different `CourseType` you can get different results.
|
|
83
|
+
|
|
84
|
+
- `getCourseList`
|
|
85
|
+
- `getNotificationList/getFileList/getHomeworkList/getDiscussionList/getAnsweredQuestionList`
|
|
86
|
+
- `getAllContents`
|
|
87
|
+
|
|
88
|
+
Note that currently fetching homework information from teacher version is **partially implemented**.
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
import { ContentType, ApiError } from '@robertying/thu-learn-lib';
|
|
92
|
+
|
|
93
|
+
// get ids of all semesters that current account has access to
|
|
94
|
+
const semesters = await helper.getSemesterIdList();
|
|
95
|
+
|
|
96
|
+
// get get semester info
|
|
97
|
+
const semester = await helper.getCurrentSemester();
|
|
98
|
+
|
|
99
|
+
// get courses of this semester
|
|
100
|
+
const courses = await helper.getCourseList(semester.id);
|
|
101
|
+
const course = courses[0];
|
|
102
|
+
|
|
103
|
+
// get detail information about the course
|
|
104
|
+
const discussions = await helper.getDiscussionList(course.id);
|
|
105
|
+
const notifications = await helper.getNotificationList(course.id);
|
|
106
|
+
const files = await helper.getFileList(course.id);
|
|
107
|
+
const homework = await helper.getHomeworkList(course.id);
|
|
108
|
+
const questions = await helper.getAnsweredQuestionList(course.id);
|
|
109
|
+
|
|
110
|
+
// get content from bunches of courses
|
|
111
|
+
// the return type will be { [id: string]: Content }
|
|
112
|
+
// where Content = Notification | File | Homework | Discussion | Question
|
|
113
|
+
const homeworks = await helper.getAllContents([1, 2, 3], ContentType.HOMEWORK);
|
|
114
|
+
|
|
115
|
+
// get course calendar in a period (yyyymmdd, inclusive)
|
|
116
|
+
// ranges over 28 days are fetched in windows; windows that fail even after a retry are in failedRanges
|
|
117
|
+
try {
|
|
118
|
+
const { events, failedRanges } = await helper.getCalendar('20191001', '20191201', { graduate: false });
|
|
119
|
+
} catch (e) {
|
|
120
|
+
// every window failed
|
|
121
|
+
const error = e as ApiError;
|
|
122
|
+
// check e.reason and e.extra for information
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Browsers block these cross-origin requests (CORS), so run the library in Node.js or React Native.
|
|
127
|
+
|
|
128
|
+
## Typing
|
|
129
|
+
|
|
130
|
+
See `src/types.ts` for type definitions. Note that `ApiError` represents the type that will be used in rejected Promises.
|
|
131
|
+
|
|
132
|
+
## Testing
|
|
133
|
+
|
|
134
|
+
`pnpm test` runs the offline tests. They give the helper a fake `fetch` (`test/helpers/fake-server.ts`) that serves hand-written responses shaped like Web Learning's, simulates SSO login and session expiry, and records every request so tests can also check how many requests an API makes.
|
|
135
|
+
|
|
136
|
+
`pnpm test:live` runs the tests against the real server. It needs your own credentials: copy `test/live/template.env` to `test/live/.env` and fill it in.
|
|
137
|
+
|
|
138
|
+
## Changelog
|
|
139
|
+
|
|
140
|
+
- v4.1.0
|
|
141
|
+
- Published as `@robertying/thu-learn-lib`
|
|
142
|
+
- Tooling: pnpm, TypeScript 7 (`tsc` ESM + `.d.ts` build), oxlint + oxfmt, Vitest 5, Node 26
|
|
143
|
+
- (_BREAKING CHANGE_) Drop the IIFE browser build and the browser-extension demo
|
|
144
|
+
- Code split into modules, with offline tests
|
|
145
|
+
- No API changes
|
|
146
|
+
- v4.0.0
|
|
147
|
+
- Upgrade cheerio to 1.0
|
|
148
|
+
- Add support for questionnaire (add `getQuestionnaireList` API and some public types)
|
|
149
|
+
- Add support for managing favorite items
|
|
150
|
+
- Add support for sorting courses (i.e. manage the order on web learning)
|
|
151
|
+
- Add support for managing comments on content (e.g. files, notifications)
|
|
152
|
+
- (_BREAKING CHANGE_) Deprecate usage of `studentHomeworkId`
|
|
153
|
+
- v3.2.1
|
|
154
|
+
- Upgrade to eslint v9
|
|
155
|
+
- Add `setCSRFToken` function to manually reuse previous (maybe valid) token and prevent unnecessary re-login (see [#49](https://github.com/Harry-Chen/thu-learn-lib/issues/49))
|
|
156
|
+
_Note:_ To use this feature in Node.js, `cookieJar` should also be reused, otherwise it will not work. (not the case in browser env)
|
|
157
|
+
- Fix `CourseContent` type: default value for type param `T` (breaking change introduced in [#53](https://github.com/Harry-Chen/thu-learn-lib/issues/53))
|
|
158
|
+
- _(Breaking...?)_ Rename `File.id2` added in previous version to actual `id` and previously used `id` to `fileId` (see [#60](https://github.com/Harry-Chen/thu-learn-lib/issues/60))
|
|
159
|
+
- Upgrade dependencies
|
|
160
|
+
- v3.2.0
|
|
161
|
+
- Support file categories (see [#57](https://github.com/Harry-Chen/thu-learn-lib/issues/57)):
|
|
162
|
+
- Add `FileCategory` type and optional `category` field in `File` type
|
|
163
|
+
- Now `getFileList` function automatically fetches all categories and saves them in each `File` object
|
|
164
|
+
- Add `getFileCategoryList` function to fetch file categories
|
|
165
|
+
- Add `getFileListByCategory` function to get file list by category
|
|
166
|
+
- Fix content of file download url and visit count in for `CourseType.TEACHER`
|
|
167
|
+
- v3.1.4
|
|
168
|
+
- Allow and check for undefined credential in login (see [#52](https://github.com/Harry-Chen/thu-learn-lib/issues/52))
|
|
169
|
+
- Discriminate `getAllContents` return type based on input content type (see [#53](https://github.com/Harry-Chen/thu-learn-lib/issues/53))
|
|
170
|
+
- Use `searchParams` to set csrf token & add csrf token to `getCalendar` (see [#54](https://github.com/Harry-Chen/thu-learn-lib/issues/54))
|
|
171
|
+
- Update bunches of dependencies
|
|
172
|
+
- v3.1.3
|
|
173
|
+
- Fix empty time and location of course (see [#145](https://github.com/Harry-Chen/Learn-Helper/issues/145))
|
|
174
|
+
- v3.1.2
|
|
175
|
+
- Fix empty course name parsing
|
|
176
|
+
- (Maybe BEARKING) `Homework.gradeLevel` changed to `HomeworkGradeLevel` (a string enum) for better i18n
|
|
177
|
+
- Fix get language
|
|
178
|
+
- v3.1.1
|
|
179
|
+
- Remove `fetch-cookie` dependency in browser build, reducing bundle size
|
|
180
|
+
- Add `getUserInfo` interface (#48, thanks to @robertying)
|
|
181
|
+
- v3.1.0
|
|
182
|
+
- **Refactor**:
|
|
183
|
+
- Build tool: `webpack` -> `tsup`
|
|
184
|
+
- Development tool: `webpack` -> `vite`
|
|
185
|
+
- Test tool: `jest` -> `vitest`
|
|
186
|
+
- Fetch:
|
|
187
|
+
- `cross-fetch` -> `node-fetch-native` (Node.js now has native `fetch`)
|
|
188
|
+
- `real-isomorphic-fetch` -> `fetch-cookie` (`fetch-cookie` now can handle redirects)
|
|
189
|
+
- `tough-cookie-no-native` -> `tough-cookie` (`tough-cookie` is now native)
|
|
190
|
+
- **(BREAKING)** Updated library output structure, now everything exported in index, cannot use `/lib` or `/lib/types` anymore
|
|
191
|
+
- Added:
|
|
192
|
+
- Submit homework
|
|
193
|
+
- Language switching and related behaviour
|
|
194
|
+
- Get TA's homework info (partial support)
|
|
195
|
+
- Fixed:
|
|
196
|
+
- **Web Learning API update**
|
|
197
|
+
- Decode course name
|
|
198
|
+
- Other:
|
|
199
|
+
- Move `fake-parse5` to [a seperate library](https://github.com/AsakuraMizu/fake-parse5)
|
|
200
|
+
- v3.0.4
|
|
201
|
+
- No feature changes, upgrade dependencies
|
|
202
|
+
- v3.0.3
|
|
203
|
+
- No feature changes, upgrade dependencies
|
|
204
|
+
- v3.0.2
|
|
205
|
+
- No feature changes, upgrade dependencies to mitigate security vulnerabilities
|
|
206
|
+
- v3.0.1
|
|
207
|
+
- Add config `generatePreviewUrlForFirstPage` to switch preview URL type (default to `true`)
|
|
208
|
+
- v3.0.0
|
|
209
|
+
- (BREAKING CHANGE) Redesign exported types, use `RemoteFile` to represent a file on Web Learning
|
|
210
|
+
- Add support for parsing attachment sizes & preview URLs in homework & notifications (Harry-Chen/Learn-Helper#109)
|
|
211
|
+
- v2.5.4
|
|
212
|
+
- Fix (sometimes) incorrect publish time of notifications (#36)
|
|
213
|
+
- v2.5.3
|
|
214
|
+
- Add `allowFailure` in `getAllContents` for convenience
|
|
215
|
+
- v2.5.2
|
|
216
|
+
- Allow retrieving CSRF token via `getCSRFToken` method
|
|
217
|
+
- Export `addCSRFTokenToUrl` function as API for convenience
|
|
218
|
+
- v2.5.1
|
|
219
|
+
- No feature change, add support for direct `import` in Node.js
|
|
220
|
+
- v2.5.0
|
|
221
|
+
- Add transparent support for CSRF token (recently deployed) in all Web Learning APIs
|
|
222
|
+
- v2.4.2
|
|
223
|
+
- No functionality change, bump some dependencies to mitigate security concerns
|
|
224
|
+
- v2.4.1
|
|
225
|
+
- Replace `parse5` with `htmlparser2` in Cheerio and remove it from bundled file (replaced with `src/fake-parse5`)
|
|
226
|
+
- Replace TSLint with ESLint
|
|
227
|
+
- v2.4.0
|
|
228
|
+
- Upgrade to TypeScript 4.1 & Webpack 5.15
|
|
229
|
+
- Add more null checking for disabled functionalities (see [xxr3376/Learn-Project#90](https://github.com/xxr3376/Learn-Project/issues/90))
|
|
230
|
+
- v2.3.2
|
|
231
|
+
- Fix a problem in `v2.3.1` (not published) and `v2.3.0` that build output fails to be uploaded to npm
|
|
232
|
+
- v2.3.0
|
|
233
|
+
- Refine error detecting & handling by using `ApiError` in usage of `Promise.reject` (might be a breaking change)
|
|
234
|
+
- v2.2.3
|
|
235
|
+
- Add workaround for some strange behaviors in teacher mode (thanks to @MashPlant)
|
|
236
|
+
- v2.2.2
|
|
237
|
+
- Return error when API return any non-success result
|
|
238
|
+
- v2.2.1
|
|
239
|
+
- Fix a missing parameter in `previewUrl`
|
|
240
|
+
- v2.2.0
|
|
241
|
+
- Use ECMAScript private fields in `Learn2018Helper` class to protect credentials
|
|
242
|
+
- Add `previewUrl` to `File`
|
|
243
|
+
- Upgrade to TypeScript 3.8.2
|
|
244
|
+
- v2.1.2
|
|
245
|
+
- Fix problem in invoking JSONP callback (because some engines might do JIT)
|
|
246
|
+
- v2.1.1
|
|
247
|
+
- Remove usage of `eval` in `getCalendar`
|
|
248
|
+
- v2.1.0
|
|
249
|
+
- Catch errors returned by calendar API and throw user-defined error
|
|
250
|
+
- Add documentation for all public APIs
|
|
251
|
+
- v2.0.0
|
|
252
|
+
- Use ES2018 in generated library
|
|
253
|
+
- Add `FailReason` to represent all strings that will be used as the reason of rejected Promises
|
|
254
|
+
- `login` and `logout` no longer return Promises
|
|
255
|
+
- v1.2.2
|
|
256
|
+
- Fix a function signature to keep compatibility
|
|
257
|
+
- v1.2.1
|
|
258
|
+
- Support TA version of many APIs (see above for usage)
|
|
259
|
+
- Fix some wrong URLs in fetched data
|
|
260
|
+
- v1.2.0
|
|
261
|
+
- Support getting course calendars from academic.tsinghua.edu.cn (thanks to robertying)
|
|
262
|
+
- Automatic retry logging in when fetching failed and `CredentialProvider` is provided (thanks to mayeths)
|
|
263
|
+
- Add unit tests using `jest` (thanks to mayeths)
|
|
264
|
+
- Filter out `null` values in `getSemesterIdList` API
|
|
265
|
+
- Switch to `https://learn.tsinghua.edu.cn/` from `learn2018` permanently
|
|
266
|
+
- v1.1.4
|
|
267
|
+
- Return empty array if any content module is disabled
|
|
268
|
+
- Add `getTACourseList` to get TA's course list (temporarily can not be used by other functions)
|
|
269
|
+
- v1.1.3
|
|
270
|
+
- Emergency fix of wrongly decoded base64 string, add `js-base64` back
|
|
271
|
+
- v1.1.2
|
|
272
|
+
- Switch to `Base64.js` instead of `js-base64`, which uses evil `eval`
|
|
273
|
+
- v1.1.1
|
|
274
|
+
- Decode HTML entities in the title of disscussions (the last one, I promise!)
|
|
275
|
+
- v1.1.0
|
|
276
|
+
- Fix an typo in grade level mapping
|
|
277
|
+
- Bump to a new minor version
|
|
278
|
+
- v1.0.16 (no v1.0.15 due to some publishing issues)
|
|
279
|
+
- Switch to `yarn`
|
|
280
|
+
- Add parsing of grade levels of homework (A+/A/.../F)
|
|
281
|
+
- v1.0.14
|
|
282
|
+
- Add prefix for `attachmentUrl` filed of Notification
|
|
283
|
+
- Deprecate all old versions
|
|
284
|
+
- v1.0.13
|
|
285
|
+
- Decode HTML entities whenever possible
|
|
286
|
+
- v1.0.12
|
|
287
|
+
- Add `url` for Course
|
|
288
|
+
- Fix `url` for Question (to display correct section name)
|
|
289
|
+
- v1.0.11
|
|
290
|
+
- Fix `url` error in Question
|
|
291
|
+
- v1.0.10
|
|
292
|
+
- Decode the HTML entities in the `description` field of homework
|
|
293
|
+
- v1.0.9
|
|
294
|
+
- Use `entities` to decode HTML entities
|
|
295
|
+
- v1.0.8
|
|
296
|
+
- Export type CourseContent
|
|
297
|
+
- v1.0.7
|
|
298
|
+
- No change made to code, update README
|
|
299
|
+
- v1.0.6
|
|
300
|
+
- Add API to fetching content for a list of courses
|
|
301
|
+
- v1.0.5
|
|
302
|
+
- Fix HTML entity replacement.
|
|
303
|
+
- v1.0.4
|
|
304
|
+
- No change made to code
|
|
305
|
+
- Remove unused build commands
|
|
306
|
+
- Fix multiple typos in README
|
|
307
|
+
- v1.0.3
|
|
308
|
+
- Add real logout API (thank @zhaofeng-shu33)
|
|
309
|
+
- v1.0.2
|
|
310
|
+
- Add API to get IDs of all semesters (thank @jiegec)
|
|
311
|
+
- v1.0.1
|
|
312
|
+
- Expose CookieJar in helper class
|
|
313
|
+
- Fix some HTML entity decoding problems
|
|
314
|
+
- **Rename of some APIs** (break compatibility before we have actual users)
|
|
315
|
+
- v1.0.0
|
|
316
|
+
- First release
|
|
317
|
+
- Support parsing of notification, homework, file, discussion and **answered** questions
|
|
318
|
+
|
|
319
|
+
## Projects using this library
|
|
320
|
+
|
|
321
|
+
- [Harry-Chen/Learn Project](https://github.com/Harry-Chen/Learn-Project)
|
|
322
|
+
- [jiegec/clone-learn-tsinghua](https://github.com/jiegec/clone-learn-tsinghua)
|
|
323
|
+
- [robertying/learnX](https://github.com/robertying/learnX) (customized fork)
|
|
324
|
+
- [Konano/thu-weblearn-tgbot](https://github.com/Konano/thu-weblearn-tgbot)
|
|
325
|
+
- [Starrah/THUCourseHelper](https://github.com/Starrah/THUCourseHelper)
|
package/lib/auth.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Session } from './request.ts';
|
|
2
|
+
/**
|
|
3
|
+
* If using alternative cookie management systems,
|
|
4
|
+
* be sure to clear id.tsinghua.edu.cn cookies before calling this function
|
|
5
|
+
*/
|
|
6
|
+
export declare function getRoamingTicket(s: Session, username: string, password: string, fingerPrint: string, fingerGenPrint?: string, fingerGenPrint3?: string): Promise<string>;
|
|
7
|
+
/** login is necessary if you do not provide a `CredentialProvider` */
|
|
8
|
+
export declare function login(s: Session, username?: string, password?: string, fingerPrint?: string, fingerGenPrint?: string, fingerGenPrint3?: string): Promise<void>;
|
|
9
|
+
/** logout (to make everyone happy) */
|
|
10
|
+
export declare function logout(s: Session): Promise<void>;
|
|
11
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAM5C;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,CAAC,EAAE,OAAO,EACV,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,cAAc,GAAE,MAAW,EAC3B,eAAe,GAAE,MAAW,mBA4C7B;AAED,sEAAsE;AACtE,wBAAsB,KAAK,CACzB,CAAC,EAAE,OAAO,EACV,QAAQ,CAAC,EAAE,MAAM,EACjB,QAAQ,CAAC,EAAE,MAAM,EACjB,WAAW,CAAC,EAAE,MAAM,EACpB,cAAc,CAAC,EAAE,MAAM,EACvB,eAAe,CAAC,EAAE,MAAM,GACvB,OAAO,CAAC,IAAI,CAAC,CAyCf;AAED,uCAAuC;AACvC,wBAAsB,MAAM,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAEtD"}
|
package/lib/auth.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Login to Web Learning through id.tsinghua.edu.cn (SSO) and logout.
|
|
2
|
+
import { sm2 } from 'sm-crypto';
|
|
3
|
+
import { $ } from './request.js';
|
|
4
|
+
import { FailReason, Language } from './types.js';
|
|
5
|
+
import * as URLS from './urls.js';
|
|
6
|
+
/**
|
|
7
|
+
* If using alternative cookie management systems,
|
|
8
|
+
* be sure to clear id.tsinghua.edu.cn cookies before calling this function
|
|
9
|
+
*/
|
|
10
|
+
export async function getRoamingTicket(s, username, password, fingerPrint, fingerGenPrint = '', fingerGenPrint3 = '') {
|
|
11
|
+
// Make sure we always start with the form page
|
|
12
|
+
// More code is needed to handle the case where the user is already logged in (i.e. cookies are set)
|
|
13
|
+
// It won't be a problem if it is always the same user,
|
|
14
|
+
// but if the current cookies are from a different user than the one trying to log in,
|
|
15
|
+
// it will cause problems.
|
|
16
|
+
try {
|
|
17
|
+
await s.cookieJar.setCookie('JSESSIONID=; path=/; HttpOnly', URLS.ID_PREFIX);
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
throw {
|
|
21
|
+
reason: FailReason.ERROR_SETTING_COOKIES,
|
|
22
|
+
extra: err,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const loginForm = await s.rawFetch(URLS.ID_LOGIN());
|
|
27
|
+
const body = $(await loginForm.text());
|
|
28
|
+
const sm2publicKey = body('#sm2publicKey').text().trim();
|
|
29
|
+
const formData = new FormData();
|
|
30
|
+
formData.append('i_user', username);
|
|
31
|
+
formData.append('i_pass', '04' + sm2.doEncrypt(password, sm2publicKey));
|
|
32
|
+
formData.append('singleLogin', 'on');
|
|
33
|
+
formData.append('fingerPrint', fingerPrint);
|
|
34
|
+
formData.append('fingerGenPrint', fingerGenPrint ?? '');
|
|
35
|
+
formData.append('fingerGenPrint3', fingerGenPrint3 ?? '');
|
|
36
|
+
formData.append('i_captcha', '');
|
|
37
|
+
const checkResponse = await s.rawFetch(URLS.ID_LOGIN_CHECK(), {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
body: formData,
|
|
40
|
+
});
|
|
41
|
+
const anchor = $(await checkResponse.text())('a');
|
|
42
|
+
const redirectUrl = anchor.attr('href');
|
|
43
|
+
const ticket = redirectUrl.split('=').slice(-1)[0];
|
|
44
|
+
return ticket;
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
throw {
|
|
48
|
+
reason: FailReason.ERROR_FETCH_FROM_ID,
|
|
49
|
+
extra: err,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** login is necessary if you do not provide a `CredentialProvider` */
|
|
54
|
+
export async function login(s, username, password, fingerPrint, fingerGenPrint, fingerGenPrint3) {
|
|
55
|
+
if (!username || !password || !fingerPrint) {
|
|
56
|
+
if (!s.provider)
|
|
57
|
+
return Promise.reject({
|
|
58
|
+
reason: FailReason.NO_CREDENTIAL,
|
|
59
|
+
});
|
|
60
|
+
const credential = await s.provider();
|
|
61
|
+
username = credential.username;
|
|
62
|
+
password = credential.password;
|
|
63
|
+
fingerPrint = credential.fingerPrint;
|
|
64
|
+
fingerGenPrint = credential.fingerGenPrint;
|
|
65
|
+
fingerGenPrint3 = credential.fingerGenPrint3;
|
|
66
|
+
if (!username || !password || !fingerPrint) {
|
|
67
|
+
return Promise.reject({
|
|
68
|
+
reason: FailReason.NO_CREDENTIAL,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// check response from id.tsinghua.edu.cn
|
|
73
|
+
const ticket = await getRoamingTicket(s, username, password, fingerPrint, fingerGenPrint, fingerGenPrint3);
|
|
74
|
+
const loginResponse = await s.rawFetch(URLS.LEARN_AUTH_ROAM(ticket));
|
|
75
|
+
if (!loginResponse.ok) {
|
|
76
|
+
return Promise.reject({
|
|
77
|
+
reason: FailReason.ERROR_ROAMING,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
const courseListPageSource = await (await s.rawFetch(URLS.LEARN_STUDENT_COURSE_LIST_PAGE())).text();
|
|
81
|
+
const tokenRegex = /^.*&_csrf=(\S*)".*$/gm;
|
|
82
|
+
const tokenMatches = [...courseListPageSource.matchAll(tokenRegex)];
|
|
83
|
+
if (tokenMatches.length === 0) {
|
|
84
|
+
return Promise.reject({
|
|
85
|
+
reason: FailReason.INVALID_RESPONSE,
|
|
86
|
+
extra: 'cannot fetch CSRF token from source',
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
s.csrfToken = tokenMatches[0][1];
|
|
90
|
+
const langRegex = /<script src="\/f\/wlxt\/common\/languagejs\?lang=(zh|en)"><\/script>/g;
|
|
91
|
+
const langMatches = [...courseListPageSource.matchAll(langRegex)];
|
|
92
|
+
if (langMatches.length !== 0)
|
|
93
|
+
s.lang = langMatches[0][1] === 'en' ? Language.EN : Language.ZH;
|
|
94
|
+
}
|
|
95
|
+
/** logout (to make everyone happy) */
|
|
96
|
+
export async function logout(s) {
|
|
97
|
+
await s.rawFetch(URLS.LEARN_LOGOUT(), { method: 'POST' });
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=auth.js.map
|
package/lib/auth.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Session } from './request.ts';
|
|
2
|
+
import type { CalendarOptions, CalendarRange, CalendarResult } from './types.ts';
|
|
3
|
+
/** The registrar often fails for ranges longer than about a month, so longer ranges are fetched in windows of this many days. */
|
|
4
|
+
export declare const CALENDAR_WINDOW_DAYS = 28;
|
|
5
|
+
/** Split an inclusive yyyymmdd range into consecutive inclusive windows of at most `days` days. */
|
|
6
|
+
export declare function splitCalendarRange(startDate: string, endDate: string, days?: number): CalendarRange[];
|
|
7
|
+
/**
|
|
8
|
+
* Get calendar items during the specified period (in yyyymmdd format, both inclusive).
|
|
9
|
+
*
|
|
10
|
+
* Ranges longer than `CALENDAR_WINDOW_DAYS` are fetched in consecutive windows (one at a time, each retried once) and
|
|
11
|
+
* merged in date order. Windows that still fail are listed in `failedRanges`, so callers can show partial data and
|
|
12
|
+
* retry only those. If every window fails, this rejects with the last error instead (usually
|
|
13
|
+
* `FailReason.INVALID_RESPONSE`). The result might be empty if the period is too far away from now.
|
|
14
|
+
*/
|
|
15
|
+
export declare function getCalendar(s: Session, startDate: string, endDate: string, options?: CalendarOptions): Promise<CalendarResult>;
|
|
16
|
+
//# sourceMappingURL=calendar.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"calendar.d.ts","sourceRoot":"","sources":["../src/calendar.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAiB,eAAe,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAKhG,iIAAiI;AACjI,eAAO,MAAM,oBAAoB,KAAK,CAAC;AAevC,mGAAmG;AACnG,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,SAAuB,GAAG,aAAa,EAAE,CASnH;AAyBD;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAC/B,CAAC,EAAE,OAAO,EACV,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,cAAc,CAAC,CA6BzB"}
|
package/lib/calendar.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Course calendar from the registrar (academic.tsinghua.edu.cn).
|
|
2
|
+
import { FailReason } from './types.js';
|
|
3
|
+
import * as URLS from './urls.js';
|
|
4
|
+
import { JSONP_EXTRACTOR_NAME, extractJSONPResult } from './utils.js';
|
|
5
|
+
/** The registrar often fails for ranges longer than about a month, so longer ranges are fetched in windows of this many days. */
|
|
6
|
+
export const CALENDAR_WINDOW_DAYS = 28;
|
|
7
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
8
|
+
const parseDate = (date) => {
|
|
9
|
+
const match = /^(\d{4})(\d{2})(\d{2})$/.exec(date);
|
|
10
|
+
const time = match ? Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3])) : NaN;
|
|
11
|
+
if (Number.isNaN(time) || formatDate(time) !== date) {
|
|
12
|
+
throw { reason: FailReason.INVALID_RESPONSE, extra: `invalid date ${date}, expected yyyymmdd` };
|
|
13
|
+
}
|
|
14
|
+
return time;
|
|
15
|
+
};
|
|
16
|
+
const formatDate = (time) => new Date(time).toISOString().slice(0, 10).replaceAll('-', '');
|
|
17
|
+
/** Split an inclusive yyyymmdd range into consecutive inclusive windows of at most `days` days. */
|
|
18
|
+
export function splitCalendarRange(startDate, endDate, days = CALENDAR_WINDOW_DAYS) {
|
|
19
|
+
const start = parseDate(startDate);
|
|
20
|
+
const end = parseDate(endDate);
|
|
21
|
+
const ranges = [];
|
|
22
|
+
for (let from = start; from <= end; from += days * DAY_MS) {
|
|
23
|
+
const to = Math.min(from + (days - 1) * DAY_MS, end);
|
|
24
|
+
ranges.push({ startDate: formatDate(from), endDate: formatDate(to) });
|
|
25
|
+
}
|
|
26
|
+
return ranges;
|
|
27
|
+
}
|
|
28
|
+
async function fetchWindow(s, range, graduate) {
|
|
29
|
+
const response = await s.fetchWithToken(URLS.REGISTRAR_CALENDAR(range.startDate, range.endDate, graduate, JSONP_EXTRACTOR_NAME));
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
return Promise.reject({
|
|
32
|
+
reason: FailReason.INVALID_RESPONSE,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
const result = extractJSONPResult(await response.text());
|
|
36
|
+
return result.map((i) => ({
|
|
37
|
+
location: i.dd,
|
|
38
|
+
status: i.fl,
|
|
39
|
+
startTime: i.kssj,
|
|
40
|
+
endTime: i.jssj,
|
|
41
|
+
date: i.nq,
|
|
42
|
+
courseName: i.nr,
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Get calendar items during the specified period (in yyyymmdd format, both inclusive).
|
|
47
|
+
*
|
|
48
|
+
* Ranges longer than `CALENDAR_WINDOW_DAYS` are fetched in consecutive windows (one at a time, each retried once) and
|
|
49
|
+
* merged in date order. Windows that still fail are listed in `failedRanges`, so callers can show partial data and
|
|
50
|
+
* retry only those. If every window fails, this rejects with the last error instead (usually
|
|
51
|
+
* `FailReason.INVALID_RESPONSE`). The result might be empty if the period is too far away from now.
|
|
52
|
+
*/
|
|
53
|
+
export async function getCalendar(s, startDate, endDate, options = {}) {
|
|
54
|
+
const graduate = options.graduate ?? false;
|
|
55
|
+
const ranges = splitCalendarRange(startDate, endDate);
|
|
56
|
+
if (ranges.length === 0)
|
|
57
|
+
return { events: [], failedRanges: [] };
|
|
58
|
+
const ticketResponse = await s.fetchWithToken(URLS.REGISTRAR_TICKET(), {
|
|
59
|
+
method: 'POST',
|
|
60
|
+
body: URLS.REGISTRAR_TICKET_FORM_DATA(),
|
|
61
|
+
});
|
|
62
|
+
let ticket = await ticketResponse.text();
|
|
63
|
+
ticket = ticket.substring(1, ticket.length - 1);
|
|
64
|
+
await s.fetch(URLS.REGISTRAR_AUTH(ticket));
|
|
65
|
+
const events = [];
|
|
66
|
+
const failedRanges = [];
|
|
67
|
+
let lastError;
|
|
68
|
+
for (const range of ranges) {
|
|
69
|
+
try {
|
|
70
|
+
events.push(...(await fetchWindow(s, range, graduate).catch(() => fetchWindow(s, range, graduate))));
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
lastError = e;
|
|
74
|
+
failedRanges.push(range);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (failedRanges.length === ranges.length)
|
|
78
|
+
throw lastError;
|
|
79
|
+
return { events, failedRanges };
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=calendar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"calendar.js","sourceRoot":"","sources":["../src/calendar.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Session } from './request.ts';
|
|
2
|
+
import type { CourseContent } from './types.ts';
|
|
3
|
+
import { ContentType, CourseType } from './types.ts';
|
|
4
|
+
/**
|
|
5
|
+
* Get certain type of content of all specified courses.
|
|
6
|
+
* It actually wraps around other `getXXX` functions. You can ignore the failure caused by certain courses.
|
|
7
|
+
*/
|
|
8
|
+
export declare function getAllContents<T extends ContentType>(s: Session, courseIDs: string[], type: T, courseType?: CourseType, allowFailure?: boolean): Promise<CourseContent<T>>;
|
|
9
|
+
//# sourceMappingURL=contents.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contents.d.ts","sourceRoot":"","sources":["../src/contents.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAkB,aAAa,EAAE,MAAM,YAAY,CAAC;AAChE,OAAO,EAAE,WAAW,EAAE,UAAU,EAAc,MAAM,YAAY,CAAC;AAOjE;;;GAGG;AACH,wBAAsB,cAAc,CAAC,CAAC,SAAS,WAAW,EACxD,CAAC,EAAE,OAAO,EACV,SAAS,EAAE,MAAM,EAAE,EACnB,IAAI,EAAE,CAAC,EACP,UAAU,GAAE,UAA+B,EAC3C,YAAY,UAAQ,6BA8CrB"}
|
package/lib/contents.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// One kind of content for many courses at once.
|
|
2
|
+
import { ContentType, CourseType, FailReason } from './types.js';
|
|
3
|
+
import { getAnsweredQuestionList, getDiscussionList } from './discussions.js';
|
|
4
|
+
import { getFileList } from './files.js';
|
|
5
|
+
import { getHomeworkList } from './homework.js';
|
|
6
|
+
import { getNotificationList } from './notices.js';
|
|
7
|
+
import { getQuestionnaireList } from './questionnaires.js';
|
|
8
|
+
/**
|
|
9
|
+
* Get certain type of content of all specified courses.
|
|
10
|
+
* It actually wraps around other `getXXX` functions. You can ignore the failure caused by certain courses.
|
|
11
|
+
*/
|
|
12
|
+
export async function getAllContents(s, courseIDs, type, courseType = CourseType.STUDENT, allowFailure = false) {
|
|
13
|
+
const fetchContentForCourse = (id) => {
|
|
14
|
+
switch (type) {
|
|
15
|
+
case ContentType.NOTIFICATION:
|
|
16
|
+
return getNotificationList(s, id, courseType);
|
|
17
|
+
case ContentType.FILE:
|
|
18
|
+
return getFileList(s, id, courseType);
|
|
19
|
+
case ContentType.HOMEWORK:
|
|
20
|
+
return getHomeworkList(s, id);
|
|
21
|
+
case ContentType.DISCUSSION:
|
|
22
|
+
return getDiscussionList(s, id, courseType);
|
|
23
|
+
case ContentType.QUESTION:
|
|
24
|
+
return getAnsweredQuestionList(s, id, courseType);
|
|
25
|
+
case ContentType.QUESTIONNAIRE:
|
|
26
|
+
return getQuestionnaireList(s, id);
|
|
27
|
+
default:
|
|
28
|
+
return Promise.reject({
|
|
29
|
+
reason: FailReason.NOT_IMPLEMENTED,
|
|
30
|
+
extra: 'Unknown content type',
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
const contents = {};
|
|
35
|
+
const results = await Promise.allSettled(courseIDs.map(async (id) => {
|
|
36
|
+
contents[id] = await fetchContentForCourse(id);
|
|
37
|
+
}));
|
|
38
|
+
if (!allowFailure) {
|
|
39
|
+
for (const r of results) {
|
|
40
|
+
if (r.status === 'rejected') {
|
|
41
|
+
return Promise.reject({
|
|
42
|
+
reason: FailReason.INVALID_RESPONSE,
|
|
43
|
+
extra: {
|
|
44
|
+
reason: r.reason,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return contents;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=contents.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contents.js","sourceRoot":"","sources":["../src/contents.ts"],"names":[],"mappings":"AAAA,gDAAgD;AAIhD,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACjE,OAAO,EAAE,uBAAuB,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC9E,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAE3D;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,CAAU,EACV,SAAmB,EACnB,IAAO,EACP,UAAU,GAAe,UAAU,CAAC,OAAO,EAC3C,YAAY,GAAG,KAAK;IAEpB,MAAM,qBAAqB,GAAG,CAAC,EAAU,EAAE,EAAE;QAC3C,QAAQ,IAAI,EAAE,CAAC;YACb,KAAK,WAAW,CAAC,YAAY;gBAC3B,OAAO,mBAAmB,CAAC,CAAC,EAAE,EAAE,EAAE,UAAU,CAAiC,CAAC;YAChF,KAAK,WAAW,CAAC,IAAI;gBACnB,OAAO,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,UAAU,CAAiC,CAAC;YACxE,KAAK,WAAW,CAAC,QAAQ;gBACvB,OAAO,eAAe,CAAC,CAAC,EAAE,EAAE,CAAiC,CAAC;YAChE,KAAK,WAAW,CAAC,UAAU;gBACzB,OAAO,iBAAiB,CAAC,CAAC,EAAE,EAAE,EAAE,UAAU,CAAiC,CAAC;YAC9E,KAAK,WAAW,CAAC,QAAQ;gBACvB,OAAO,uBAAuB,CAAC,CAAC,EAAE,EAAE,EAAE,UAAU,CAAiC,CAAC;YACpF,KAAK,WAAW,CAAC,aAAa;gBAC5B,OAAO,oBAAoB,CAAC,CAAC,EAAE,EAAE,CAAiC,CAAC;YACrE;gBACE,OAAO,OAAO,CAAC,MAAM,CAAC;oBACpB,MAAM,EAAE,UAAU,CAAC,eAAe;oBAClC,KAAK,EAAE,sBAAsB;iBAC9B,CAAC,CAAC;QACP,CAAC;IACH,CAAC,CAAC;IAEF,MAAM,QAAQ,GAAqB,EAAE,CAAC;IAEtC,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,CACtC,SAAS,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;QACzB,QAAQ,CAAC,EAAE,CAAC,GAAG,MAAM,qBAAqB,CAAC,EAAE,CAAC,CAAC;IACjD,CAAC,CAAC,CACH,CAAC;IAEF,IAAI,CAAC,YAAY,EAAE,CAAC;QAClB,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;YACxB,IAAI,CAAC,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;gBAC5B,OAAO,OAAO,CAAC,MAAM,CAAC;oBACpB,MAAM,EAAE,UAAU,CAAC,gBAAgB;oBACnC,KAAK,EAAE;wBACL,MAAM,EAAE,CAAC,CAAC,MAAM;qBACjB;iBACF,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|