@wilm-ai/wilma-client 1.5.1 → 1.5.2
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/dist/client.js +24 -4
- package/dist/parsers/students.d.ts +10 -1
- package/dist/parsers/students.js +111 -9
- package/package.json +2 -2
package/dist/client.js
CHANGED
|
@@ -6,10 +6,10 @@ import { parseExamsHtml } from "./parsers/exams.js";
|
|
|
6
6
|
import { parseAttendanceHtml } from "./parsers/attendance.js";
|
|
7
7
|
import { parseOverview } from "./parsers/overview.js";
|
|
8
8
|
import { parseScheduleHtml } from "./parsers/schedule.js";
|
|
9
|
-
import { parseStudentsFromHome } from "./parsers/students.js";
|
|
9
|
+
import { parseStudentsFromAccountsRoles, parseStudentsFromHome } from "./parsers/students.js";
|
|
10
10
|
import { CookieJar } from "tough-cookie";
|
|
11
11
|
import { fetch, Headers } from "undici";
|
|
12
|
-
import { wrapNetworkError } from "./network-error.js";
|
|
12
|
+
import { NetworkError, wrapNetworkError } from "./network-error.js";
|
|
13
13
|
const EXTERNAL_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " +
|
|
14
14
|
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0 Safari/537.36";
|
|
15
15
|
export class WilmaClient {
|
|
@@ -37,7 +37,9 @@ export class WilmaClient {
|
|
|
37
37
|
return new WilmaClient(session);
|
|
38
38
|
}
|
|
39
39
|
static async listStudents(profile, onMfaRequired) {
|
|
40
|
-
const session = new WilmaSession(profile.baseUrl
|
|
40
|
+
const session = new WilmaSession(profile.baseUrl, {
|
|
41
|
+
debug: profile.debug ?? false,
|
|
42
|
+
});
|
|
41
43
|
try {
|
|
42
44
|
await session.login(profile.username, profile.password);
|
|
43
45
|
}
|
|
@@ -50,9 +52,27 @@ export class WilmaClient {
|
|
|
50
52
|
throw err;
|
|
51
53
|
}
|
|
52
54
|
}
|
|
55
|
+
try {
|
|
56
|
+
const accountsResp = await session.get("/api/v1/accounts/me/roles");
|
|
57
|
+
const accountsText = await accountsResp.text();
|
|
58
|
+
const fromAccounts = parseStudentsFromAccountsRoles(safeJson(accountsText));
|
|
59
|
+
if (fromAccounts.length > 0) {
|
|
60
|
+
return fromAccounts;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
if (err instanceof NetworkError) {
|
|
65
|
+
throw err;
|
|
66
|
+
}
|
|
67
|
+
// API missing or not JSON (e.g. 404 on older Wilma)
|
|
68
|
+
}
|
|
53
69
|
const resp = await session.get("/");
|
|
54
70
|
const html = await resp.text();
|
|
55
|
-
|
|
71
|
+
const fromHome = parseStudentsFromHome(html, resp.url);
|
|
72
|
+
if (fromHome.length > 0) {
|
|
73
|
+
return fromHome;
|
|
74
|
+
}
|
|
75
|
+
return [];
|
|
56
76
|
}
|
|
57
77
|
messages = {
|
|
58
78
|
list: async (folder = "inbox") => {
|
|
@@ -3,4 +3,13 @@ export interface StudentInfo {
|
|
|
3
3
|
name: string;
|
|
4
4
|
href: string;
|
|
5
5
|
}
|
|
6
|
-
export declare function
|
|
6
|
+
export declare function studentNumberFromHref(href: string): string | null;
|
|
7
|
+
export declare function parseStudentFromUrl(url: string): StudentInfo | null;
|
|
8
|
+
/**
|
|
9
|
+
* Parse GET /api/v1/accounts/me/roles.
|
|
10
|
+
* Accepts `{ payload: AccountRole[] }` or a raw array.
|
|
11
|
+
* Skips the account itself (`type === "passwd"` / type 7). Guardian (and other
|
|
12
|
+
* non-passwd) roles contribute a student from `slug` + `name`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseStudentsFromAccountsRoles(data: unknown): StudentInfo[];
|
|
15
|
+
export declare function parseStudentsFromHome(html: string, pageUrl?: string): StudentInfo[];
|
package/dist/parsers/students.js
CHANGED
|
@@ -12,16 +12,106 @@ const NAV_KEYWORDS = [
|
|
|
12
12
|
"printouts",
|
|
13
13
|
"news",
|
|
14
14
|
];
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
const STUDENT_HREF_RE = /\/!(\d+)(?:\/|$)/;
|
|
16
|
+
export function studentNumberFromHref(href) {
|
|
17
|
+
const match = STUDENT_HREF_RE.exec(href);
|
|
18
|
+
return match ? match[1] : null;
|
|
19
|
+
}
|
|
20
|
+
function isPasswdAccountRole(type) {
|
|
21
|
+
return type === "passwd" || type === 7 || type === "7";
|
|
22
|
+
}
|
|
23
|
+
function studentNumberFromSlug(slug) {
|
|
24
|
+
const trimmed = slug.trim();
|
|
25
|
+
if (!trimmed) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
if (/^\d+$/.test(trimmed)) {
|
|
29
|
+
return trimmed;
|
|
30
|
+
}
|
|
31
|
+
const withSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
32
|
+
return studentNumberFromHref(withSlash);
|
|
33
|
+
}
|
|
34
|
+
export function parseStudentFromUrl(url) {
|
|
35
|
+
const studentNumber = studentNumberFromHref(url);
|
|
36
|
+
if (!studentNumber) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
studentNumber,
|
|
41
|
+
name: studentNumber,
|
|
42
|
+
href: `/!${studentNumber}/`,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function roleType(rec) {
|
|
46
|
+
return rec.Type ?? rec.type ?? rec.RoleType ?? rec.roleType;
|
|
47
|
+
}
|
|
48
|
+
function roleName(rec, fallback) {
|
|
49
|
+
return String(rec.Name ?? rec.name ?? rec.Caption ?? rec.caption ?? fallback);
|
|
50
|
+
}
|
|
51
|
+
function collectStudent(students, studentNumber, name) {
|
|
52
|
+
if (!students.has(studentNumber)) {
|
|
53
|
+
students.set(studentNumber, {
|
|
54
|
+
studentNumber,
|
|
55
|
+
name,
|
|
56
|
+
href: `/!${studentNumber}/`,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Parse GET /api/v1/accounts/me/roles.
|
|
62
|
+
* Accepts `{ payload: AccountRole[] }` or a raw array.
|
|
63
|
+
* Skips the account itself (`type === "passwd"` / type 7). Guardian (and other
|
|
64
|
+
* non-passwd) roles contribute a student from `slug` + `name`.
|
|
65
|
+
*/
|
|
66
|
+
export function parseStudentsFromAccountsRoles(data) {
|
|
17
67
|
const students = new Map();
|
|
18
|
-
|
|
68
|
+
let roles = [];
|
|
69
|
+
if (Array.isArray(data)) {
|
|
70
|
+
roles = data;
|
|
71
|
+
}
|
|
72
|
+
else if (data && typeof data === "object") {
|
|
73
|
+
const rec = data;
|
|
74
|
+
const payload = rec.payload ?? rec.Payload;
|
|
75
|
+
if (Array.isArray(payload)) {
|
|
76
|
+
roles = payload;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
for (const item of roles) {
|
|
80
|
+
if (!item || typeof item !== "object") {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const rec = item;
|
|
84
|
+
if (isPasswdAccountRole(roleType(rec))) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const slug = String(rec.slug ?? rec.Slug ?? "");
|
|
88
|
+
const studentNumber = studentNumberFromSlug(slug);
|
|
89
|
+
if (!studentNumber) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const name = roleName(rec, studentNumber);
|
|
93
|
+
if (!name) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
collectStudent(students, studentNumber, name);
|
|
97
|
+
}
|
|
98
|
+
return [...students.values()];
|
|
99
|
+
}
|
|
100
|
+
export function parseStudentsFromHome(html, pageUrl) {
|
|
101
|
+
const $ = cheerio.load(html);
|
|
102
|
+
const named = new Map();
|
|
103
|
+
const fromHref = new Map();
|
|
104
|
+
$("a[href]").each((_, anchor) => {
|
|
19
105
|
const href = $(anchor).attr("href") ?? "";
|
|
20
|
-
const
|
|
21
|
-
if (!
|
|
106
|
+
const studentNumber = studentNumberFromHref(href);
|
|
107
|
+
if (!studentNumber) {
|
|
22
108
|
return;
|
|
23
109
|
}
|
|
24
|
-
|
|
110
|
+
fromHref.set(studentNumber, {
|
|
111
|
+
studentNumber,
|
|
112
|
+
name: studentNumber,
|
|
113
|
+
href,
|
|
114
|
+
});
|
|
25
115
|
const cloned = $(anchor).clone();
|
|
26
116
|
cloned.find("small, span.lem").remove();
|
|
27
117
|
const text = cloned.text().trim();
|
|
@@ -32,13 +122,25 @@ export function parseStudentsFromHome(html) {
|
|
|
32
122
|
if (NAV_KEYWORDS.some((kw) => lower.includes(kw))) {
|
|
33
123
|
return;
|
|
34
124
|
}
|
|
35
|
-
if (!
|
|
36
|
-
|
|
125
|
+
if (!named.has(studentNumber)) {
|
|
126
|
+
named.set(studentNumber, {
|
|
37
127
|
studentNumber,
|
|
38
128
|
name: text,
|
|
39
129
|
href,
|
|
40
130
|
});
|
|
41
131
|
}
|
|
42
132
|
});
|
|
43
|
-
|
|
133
|
+
if (named.size > 0) {
|
|
134
|
+
return [...named.values()];
|
|
135
|
+
}
|
|
136
|
+
if (fromHref.size > 0) {
|
|
137
|
+
return [...fromHref.values()];
|
|
138
|
+
}
|
|
139
|
+
if (pageUrl) {
|
|
140
|
+
const fromUrl = parseStudentFromUrl(pageUrl);
|
|
141
|
+
if (fromUrl) {
|
|
142
|
+
return [fromUrl];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return [];
|
|
44
146
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wilm-ai/wilma-client",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"scripts": {
|
|
18
18
|
"build": "tsc -p tsconfig.json",
|
|
19
19
|
"lint": "echo 'add lint'",
|
|
20
|
-
"test": "pnpm run build && node test/parser-messages.mjs && node test/parser-news.mjs && node test/parser-schedule.mjs && node test/client-schedule.mjs && node test/client-news-resources.mjs && node test/network-error.mjs",
|
|
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
21
|
"test:smoke": "tsc -p tsconfig.json && node test/smoke.mjs",
|
|
22
22
|
"test:live": "tsc -p tsconfig.json && node test/live.spec.mjs"
|
|
23
23
|
},
|