@wilm-ai/wilma-client 1.5.2 → 1.6.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 +66 -0
- package/dist/client.d.ts +67 -2
- package/dist/client.js +167 -128
- package/dist/external-fetch.d.ts +18 -0
- package/dist/external-fetch.js +263 -0
- package/dist/finnish-time.d.ts +19 -0
- package/dist/finnish-time.js +64 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -0
- package/dist/parsers/attendance.d.ts +4 -2
- package/dist/parsers/attendance.js +50 -13
- package/dist/parsers/dates.js +14 -17
- package/dist/parsers/exams.d.ts +8 -1
- package/dist/parsers/exams.js +37 -0
- package/dist/parsers/gradebook.d.ts +8 -0
- package/dist/parsers/gradebook.js +44 -0
- package/dist/parsers/html-text.d.ts +6 -0
- package/dist/parsers/html-text.js +42 -0
- package/dist/parsers/messages.d.ts +6 -2
- package/dist/parsers/messages.js +54 -8
- package/dist/parsers/news.js +61 -7
- package/dist/parsers/overview.d.ts +2 -1
- package/dist/parsers/overview.js +4 -3
- package/dist/parsers/printouts.d.ts +3 -0
- package/dist/parsers/printouts.js +20 -0
- package/dist/parsers/schedule.d.ts +6 -0
- package/dist/parsers/schedule.js +37 -0
- package/dist/parsers/students.d.ts +2 -5
- package/dist/parsers/students.js +22 -7
- package/dist/session.d.ts +58 -6
- package/dist/session.js +357 -77
- package/dist/timeouts.d.ts +20 -0
- package/dist/timeouts.js +82 -0
- package/dist/types.d.ts +56 -0
- package/package.json +21 -7
package/dist/timeouts.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Idle timeouts for requests and downloads. A request fails when the server
|
|
3
|
+
* stops answering for a while — before the response, or while the body is
|
|
4
|
+
* read — not after a fixed total, so a large attachment on a slow connection
|
|
5
|
+
* still finishes while a stalled one doesn't hang forever.
|
|
6
|
+
*/
|
|
7
|
+
import { Response } from "undici";
|
|
8
|
+
import { NetworkError, wrapNetworkError } from "./network-error.js";
|
|
9
|
+
/** An abort signal that fires after `ms` without progress. */
|
|
10
|
+
export class IdleTimer {
|
|
11
|
+
ms;
|
|
12
|
+
controller = new AbortController();
|
|
13
|
+
timer;
|
|
14
|
+
constructor(ms) {
|
|
15
|
+
this.ms = ms;
|
|
16
|
+
this.touch();
|
|
17
|
+
}
|
|
18
|
+
get signal() {
|
|
19
|
+
return this.controller.signal;
|
|
20
|
+
}
|
|
21
|
+
/** Progress: start the wait over. */
|
|
22
|
+
touch() {
|
|
23
|
+
clearTimeout(this.timer);
|
|
24
|
+
this.timer = setTimeout(() => this.controller.abort(new DOMException("The server stopped answering", "TimeoutError")), this.ms);
|
|
25
|
+
// A response nobody reads must not keep the process alive.
|
|
26
|
+
this.timer.unref?.();
|
|
27
|
+
}
|
|
28
|
+
stop() {
|
|
29
|
+
clearTimeout(this.timer);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** A timeout or broken connection as a NetworkError; anything else unchanged. */
|
|
33
|
+
export function asNetworkError(err, origin) {
|
|
34
|
+
if (err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError")) {
|
|
35
|
+
return new NetworkError(`${origin} didn't answer in time`, { code: "TIMEOUT", origin, cause: err });
|
|
36
|
+
}
|
|
37
|
+
return wrapNetworkError(err, origin);
|
|
38
|
+
}
|
|
39
|
+
// Statuses whose responses can't carry a body.
|
|
40
|
+
const NULL_BODY_STATUSES = new Set([101, 103, 204, 205, 304]);
|
|
41
|
+
/**
|
|
42
|
+
* The same response, with its body read under the idle timer: each chunk
|
|
43
|
+
* restarts the wait, and a stall or broken connection while reading surfaces
|
|
44
|
+
* as a NetworkError, like one before the response.
|
|
45
|
+
*/
|
|
46
|
+
export function watchBody(response, idle, origin) {
|
|
47
|
+
if (!response.body || NULL_BODY_STATUSES.has(response.status)) {
|
|
48
|
+
idle.stop();
|
|
49
|
+
return response;
|
|
50
|
+
}
|
|
51
|
+
const reader = response.body.getReader();
|
|
52
|
+
const body = new ReadableStream({
|
|
53
|
+
async pull(controller) {
|
|
54
|
+
try {
|
|
55
|
+
const { done, value } = await reader.read();
|
|
56
|
+
if (done) {
|
|
57
|
+
idle.stop();
|
|
58
|
+
controller.close();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
idle.touch();
|
|
62
|
+
controller.enqueue(value);
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
idle.stop();
|
|
66
|
+
controller.error(asNetworkError(err, origin));
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
cancel(reason) {
|
|
70
|
+
idle.stop();
|
|
71
|
+
return reader.cancel(reason);
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
const watched = new Response(body, {
|
|
75
|
+
status: response.status,
|
|
76
|
+
statusText: response.statusText,
|
|
77
|
+
headers: response.headers,
|
|
78
|
+
});
|
|
79
|
+
// Parsers resolve links against the final address.
|
|
80
|
+
Object.defineProperty(watched, "url", { value: response.url });
|
|
81
|
+
return watched;
|
|
82
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -9,9 +9,23 @@ export interface Message {
|
|
|
9
9
|
senderName?: string | null;
|
|
10
10
|
sendersJson?: Record<string, unknown> | null;
|
|
11
11
|
status?: number | null;
|
|
12
|
+
/** In lists: Wilma hasn't seen it opened yet. */
|
|
13
|
+
unread?: boolean;
|
|
14
|
+
/** In lists: how many replies the thread has (read them with the message). */
|
|
15
|
+
replyCount?: number;
|
|
12
16
|
content?: string | null;
|
|
17
|
+
/** Message detail: who it went to (Wilma may hide the list). */
|
|
18
|
+
recipients?: string[] | null;
|
|
19
|
+
/** Message detail: the thread's replies, oldest first. */
|
|
20
|
+
replies?: MessageReply[];
|
|
13
21
|
fetchedAt: Date;
|
|
14
22
|
}
|
|
23
|
+
export interface MessageReply {
|
|
24
|
+
id: number;
|
|
25
|
+
sentAt: Date;
|
|
26
|
+
senderName: string | null;
|
|
27
|
+
content: string;
|
|
28
|
+
}
|
|
15
29
|
export interface NewsItem {
|
|
16
30
|
wilmaId: number;
|
|
17
31
|
title: string;
|
|
@@ -20,6 +34,10 @@ export interface NewsItem {
|
|
|
20
34
|
published?: Date | null;
|
|
21
35
|
content?: string | null;
|
|
22
36
|
resources?: NewsResource[];
|
|
37
|
+
/** A pinned bulletin (Wilma: "Pysyvät tiedotteet"), kept on the page until removed. */
|
|
38
|
+
pinned?: boolean;
|
|
39
|
+
/** Listed among older bulletins (Wilma: "Vanhat tiedotteet"); its date is on the bulletin itself. */
|
|
40
|
+
archived?: boolean;
|
|
23
41
|
fetchedAt: Date;
|
|
24
42
|
}
|
|
25
43
|
export type NewsResourceAuthContext = "external" | "wilma";
|
|
@@ -39,6 +57,8 @@ export interface Exam {
|
|
|
39
57
|
description?: string | null;
|
|
40
58
|
teacher?: string | null;
|
|
41
59
|
notes?: string | null;
|
|
60
|
+
/** Start time ("08:30") when the school gives one; only the exam calendar has it. */
|
|
61
|
+
time?: string | null;
|
|
42
62
|
fetchedAt: Date;
|
|
43
63
|
}
|
|
44
64
|
export interface StudentInfo {
|
|
@@ -78,11 +98,15 @@ export interface ScheduleLesson {
|
|
|
78
98
|
teacher: string;
|
|
79
99
|
teacherCode: string;
|
|
80
100
|
groupId: number;
|
|
101
|
+
/** Room code(s), e.g. "407"; null when Wilma gives none. */
|
|
102
|
+
room?: string | null;
|
|
81
103
|
}
|
|
82
104
|
export interface UpcomingExam {
|
|
83
105
|
examId: number;
|
|
84
106
|
/** YYYY-MM-DD */
|
|
85
107
|
date: string;
|
|
108
|
+
/** Start time ("08:30") when the school gives one (from the exam calendar). */
|
|
109
|
+
time?: string | null;
|
|
86
110
|
name: string;
|
|
87
111
|
subject: string;
|
|
88
112
|
subjectCode: string;
|
|
@@ -133,4 +157,36 @@ export interface LessonNote {
|
|
|
133
157
|
typeLabel: string;
|
|
134
158
|
typeClass: string;
|
|
135
159
|
teacher: string;
|
|
160
|
+
/** The teacher's own words, when they wrote any (e.g. what was missing, or praise). */
|
|
161
|
+
note?: string | null;
|
|
162
|
+
}
|
|
163
|
+
/** How many lesson notes of each kind in a period (absences, lateness, feedback…). */
|
|
164
|
+
export interface LessonNoteSummary {
|
|
165
|
+
/** YYYY-MM-DD, or null for "the whole school year" as Wilma defines it. */
|
|
166
|
+
from: string | null;
|
|
167
|
+
to: string | null;
|
|
168
|
+
total: number;
|
|
169
|
+
byType: {
|
|
170
|
+
type: string;
|
|
171
|
+
count: number;
|
|
172
|
+
}[];
|
|
173
|
+
}
|
|
174
|
+
/** One row of the gradebook (Suoritukset): a subject, a syllabus or a course, with its sub-rows. */
|
|
175
|
+
export interface GradebookEntry {
|
|
176
|
+
name: string;
|
|
177
|
+
/** Course code such as "MA_81" (null for subject rows). */
|
|
178
|
+
code: string | null;
|
|
179
|
+
grade: string | null;
|
|
180
|
+
/** Scope as Wilma shows it (e.g. "2 vvt"), if any. */
|
|
181
|
+
credits: string | null;
|
|
182
|
+
/** YYYY-MM-DD when completed. */
|
|
183
|
+
date: string | null;
|
|
184
|
+
children: GradebookEntry[];
|
|
185
|
+
}
|
|
186
|
+
/** A printable document Wilma offers as a PDF (report cards, absence reports…). */
|
|
187
|
+
export interface Printout {
|
|
188
|
+
id: string;
|
|
189
|
+
title: string;
|
|
190
|
+
/** Path on this Wilma, e.g. "/!123/printouts/456.pdf". */
|
|
191
|
+
path: string;
|
|
136
192
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wilm-ai/wilma-client",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
|
+
"description": "Unofficial client for Wilma, the Finnish school information system: login (with two-step verification), schedule, homework, exams, messages and news.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/aikarjal/wilmai.git",
|
|
9
|
+
"directory": "packages/wilma-client"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://wilm.ai",
|
|
12
|
+
"bugs": "https://github.com/aikarjal/wilmai/issues",
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=20.18.1"
|
|
15
|
+
},
|
|
4
16
|
"type": "module",
|
|
5
17
|
"main": "dist/index.js",
|
|
6
18
|
"types": "dist/index.d.ts",
|
|
@@ -12,19 +24,21 @@
|
|
|
12
24
|
},
|
|
13
25
|
"files": [
|
|
14
26
|
"dist",
|
|
15
|
-
"tenant_list.json"
|
|
27
|
+
"tenant_list.json",
|
|
28
|
+
"LICENSE",
|
|
29
|
+
"README.md"
|
|
16
30
|
],
|
|
17
31
|
"scripts": {
|
|
18
32
|
"build": "tsc -p tsconfig.json",
|
|
19
|
-
"lint": "
|
|
20
|
-
"test": "pnpm run build && node test/parser-messages.mjs && node test/parser-news.mjs && node test/parser-schedule.mjs && node test/parser-students.mjs && node test/client-students.mjs && node test/client-schedule.mjs && node test/client-news-resources.mjs && node test/network-error.mjs",
|
|
21
|
-
"test:
|
|
22
|
-
"
|
|
33
|
+
"lint": "tsc -p tsconfig.json --noEmit",
|
|
34
|
+
"test": "pnpm run build && node test/login.mjs && node test/parser-messages.mjs && node test/parser-news.mjs && node test/parser-schedule.mjs && node test/parser-students.mjs && node test/client-students.mjs && node test/client-schedule.mjs && node test/client-news-resources.mjs && node test/network-error.mjs && node test/audit.mjs && node test/real-pages.mjs && node test/client-features.mjs",
|
|
35
|
+
"test:live": "tsc -p tsconfig.json && node test/live.spec.mjs",
|
|
36
|
+
"fixtures:anonymize": "node scripts/anonymize-fixture.mjs"
|
|
23
37
|
},
|
|
24
38
|
"dependencies": {
|
|
25
39
|
"cheerio": "^1.0.0-rc.12",
|
|
26
40
|
"tough-cookie": "^4.1.4",
|
|
27
|
-
"undici": "^6.
|
|
41
|
+
"undici": "^6.27.0"
|
|
28
42
|
},
|
|
29
43
|
"devDependencies": {
|
|
30
44
|
"typescript": "^5.6.3",
|