@wilm-ai/wilma-client 1.7.1 → 1.9.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/dist/client.d.ts +5 -0
- package/dist/client.js +14 -0
- package/dist/parsers/forms.d.ts +6 -0
- package/dist/parsers/forms.js +6 -2
- package/dist/parsers/recipients.d.ts +6 -0
- package/dist/parsers/recipients.js +26 -0
- package/dist/write.d.ts +11 -1
- package/dist/write.js +22 -3
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -94,6 +94,11 @@ export declare class WilmaClient {
|
|
|
94
94
|
};
|
|
95
95
|
news: {
|
|
96
96
|
list: () => Promise<NewsItem[]>;
|
|
97
|
+
/**
|
|
98
|
+
* The bulletins, or null when this Wilma doesn't show them to this account: it answers /news with 403 (Helsinki's
|
|
99
|
+
* does, for a guardian) or 404 while the session is alive. Its other pages read as usual.
|
|
100
|
+
*/
|
|
101
|
+
listIfShown: () => Promise<NewsItem[] | null>;
|
|
97
102
|
get: (newsId: number) => Promise<NewsItem>;
|
|
98
103
|
fetchResource: (newsId: number, resourceId: string, options?: {
|
|
99
104
|
item?: NewsItem;
|
package/dist/client.js
CHANGED
|
@@ -241,6 +241,20 @@ export class WilmaClient {
|
|
|
241
241
|
}
|
|
242
242
|
return parseNewsListHtml(text);
|
|
243
243
|
},
|
|
244
|
+
/**
|
|
245
|
+
* The bulletins, or null when this Wilma doesn't show them to this account: it answers /news with 403 (Helsinki's
|
|
246
|
+
* does, for a guardian) or 404 while the session is alive. Its other pages read as usual.
|
|
247
|
+
*/
|
|
248
|
+
listIfShown: async () => {
|
|
249
|
+
try {
|
|
250
|
+
return await this.news.list();
|
|
251
|
+
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
if (err instanceof APIError && (err.status === 403 || err.status === 404))
|
|
254
|
+
return null;
|
|
255
|
+
throw err;
|
|
256
|
+
}
|
|
257
|
+
},
|
|
244
258
|
get: async (newsId) => {
|
|
245
259
|
const resp = await this.session.get(`/news/${newsId}`);
|
|
246
260
|
const contentType = resp.headers.get("content-type")?.toLowerCase() ?? "";
|
package/dist/parsers/forms.d.ts
CHANGED
|
@@ -23,6 +23,12 @@ export interface WilmaForm {
|
|
|
23
23
|
}[];
|
|
24
24
|
}[];
|
|
25
25
|
textareas: string[];
|
|
26
|
+
/** The form's checkboxes, ticked or not (`fields` has only the ticked ones, as a browser sends them). */
|
|
27
|
+
checkboxes?: {
|
|
28
|
+
name: string;
|
|
29
|
+
value: string;
|
|
30
|
+
checked: boolean;
|
|
31
|
+
}[];
|
|
26
32
|
}
|
|
27
33
|
export declare class FormOutsideWilmaError extends Error {
|
|
28
34
|
constructor();
|
package/dist/parsers/forms.js
CHANGED
|
@@ -18,6 +18,7 @@ export function readForm($, form, pageUrl) {
|
|
|
18
18
|
const buttons = [];
|
|
19
19
|
const selects = [];
|
|
20
20
|
const textareas = [];
|
|
21
|
+
const checkboxes = [];
|
|
21
22
|
form.find("input, select, textarea, button").each((_, el) => {
|
|
22
23
|
const control = $(el);
|
|
23
24
|
const name = control.attr("name");
|
|
@@ -50,14 +51,17 @@ export function readForm($, form, pageUrl) {
|
|
|
50
51
|
buttons.push({ name, value, label: clean(control.text()) || value });
|
|
51
52
|
}
|
|
52
53
|
else if (type === "checkbox" || type === "radio") {
|
|
53
|
-
|
|
54
|
+
const checked = control.attr("checked") !== undefined;
|
|
55
|
+
if (type === "checkbox")
|
|
56
|
+
checkboxes.push({ name, value: control.attr("value") ?? "on", checked });
|
|
57
|
+
if (checked)
|
|
54
58
|
fields.push([name, control.attr("value") ?? "on"]);
|
|
55
59
|
}
|
|
56
60
|
else if (type !== "file" && type !== "reset" && type !== "button") {
|
|
57
61
|
fields.push([name, value]);
|
|
58
62
|
}
|
|
59
63
|
});
|
|
60
|
-
return { action: wilmaPath(form.attr("action") ?? "", pageUrl), fields, buttons, selects, textareas };
|
|
64
|
+
return { action: wilmaPath(form.attr("action") ?? "", pageUrl), fields, buttons, selects, textareas, checkboxes };
|
|
61
65
|
}
|
|
62
66
|
/** The form key on a page (Wilma's anti-forgery token), preferring one outside the logout form. */
|
|
63
67
|
export function pageFormkey($) {
|
|
@@ -3,6 +3,12 @@ import type { MessageRecipient, StudentDetails } from "../types.js";
|
|
|
3
3
|
export declare function composePath(href: string | undefined, pageUrl: string): string | null;
|
|
4
4
|
/** What a compose link writes to, from its r_ parameters (a repeated or mixed one: several people). */
|
|
5
5
|
export declare function recipientKind(path: string): MessageRecipient["kind"];
|
|
6
|
+
/**
|
|
7
|
+
* One compose link to several recipients, made as Wilma's own "to all teachers" links are: every r_… parameter of
|
|
8
|
+
* theirs, each once, and for more than one person showrecipients=1&collatedreplies=1 (the recipients see who else
|
|
9
|
+
* got it and each other's replies, which the message form can still untick). The links must be one child's.
|
|
10
|
+
*/
|
|
11
|
+
export declare function composeToAll(paths: string[]): string;
|
|
6
12
|
/** The child's class and supervisors from their front page (`/!<n>/`); empty fields when the panel isn't there. */
|
|
7
13
|
export declare function parseStudentDetailsHtml(html: string, pageUrl: string): StudentDetails;
|
|
8
14
|
/**
|
|
@@ -51,6 +51,32 @@ export function recipientKind(path) {
|
|
|
51
51
|
return "other";
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* One compose link to several recipients, made as Wilma's own "to all teachers" links are: every r_… parameter of
|
|
56
|
+
* theirs, each once, and for more than one person showrecipients=1&collatedreplies=1 (the recipients see who else
|
|
57
|
+
* got it and each other's replies, which the message form can still untick). The links must be one child's.
|
|
58
|
+
*/
|
|
59
|
+
export function composeToAll(paths) {
|
|
60
|
+
if (!paths.length)
|
|
61
|
+
throw new TypeError("No recipients");
|
|
62
|
+
if (paths.length === 1)
|
|
63
|
+
return paths[0];
|
|
64
|
+
const urls = paths.map((p) => new URL(p, "https://wilma.invalid"));
|
|
65
|
+
if (urls.some((u) => u.pathname !== urls[0].pathname))
|
|
66
|
+
throw new TypeError("The recipients are another child's");
|
|
67
|
+
const params = [];
|
|
68
|
+
for (const u of urls) {
|
|
69
|
+
for (const [k, v] of u.searchParams)
|
|
70
|
+
if (k.startsWith("r_") && !params.some(([a, b]) => a === k && b === v))
|
|
71
|
+
params.push([k, v]);
|
|
72
|
+
}
|
|
73
|
+
const out = new URLSearchParams(params);
|
|
74
|
+
if (params.length > 1 || recipientKind(`?${out}`) === "group") {
|
|
75
|
+
out.set("showrecipients", "1");
|
|
76
|
+
out.set("collatedreplies", "1");
|
|
77
|
+
}
|
|
78
|
+
return `${urls[0].pathname}?${out}`;
|
|
79
|
+
}
|
|
54
80
|
/** The child's class and supervisors from their front page (`/!<n>/`); empty fields when the panel isn't there. */
|
|
55
81
|
export function parseStudentDetailsHtml(html, pageUrl) {
|
|
56
82
|
const $ = cheerio.load(html);
|
package/dist/write.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { WilmaClient } from "./client.js";
|
|
2
2
|
import { type WilmaForm } from "./parsers/forms.js";
|
|
3
|
+
export { composeToAll } from "./parsers/recipients.js";
|
|
3
4
|
import type { Invitation, MessageRecipient } from "./types.js";
|
|
4
5
|
export type { WilmaForm } from "./parsers/forms.js";
|
|
5
6
|
export { FormOutsideWilmaError } from "./parsers/forms.js";
|
|
@@ -98,14 +99,23 @@ export interface MessageForm {
|
|
|
98
99
|
readonly form: WilmaForm;
|
|
99
100
|
readonly subjectField: string;
|
|
100
101
|
readonly bodyField: string;
|
|
102
|
+
/**
|
|
103
|
+
* Whether the recipients see each other's replies (Wilma's CollatedReplies box, which makes the thread an open
|
|
104
|
+
* discussion), as Wilma's form has it; null when the form has no such box.
|
|
105
|
+
*/
|
|
106
|
+
readonly collatedReplies?: boolean | null;
|
|
101
107
|
}
|
|
102
108
|
export declare function prepareMessage(client: WilmaClient, to: {
|
|
103
109
|
teacherId: string | number;
|
|
104
110
|
} | Pick<MessageRecipient, "composePath">): Promise<MessageForm>;
|
|
105
|
-
/**
|
|
111
|
+
/**
|
|
112
|
+
* Send the message (once). `collatedReplies` ticks or clears Wilma's "recipients see each other's replies" box
|
|
113
|
+
* when the form has one; left out, the box stays as Wilma's form has it.
|
|
114
|
+
*/
|
|
106
115
|
export declare function sendMessage(client: WilmaClient, prepared: MessageForm, input: {
|
|
107
116
|
subject: string;
|
|
108
117
|
text: string;
|
|
118
|
+
collatedReplies?: boolean;
|
|
109
119
|
}): Promise<void>;
|
|
110
120
|
/**
|
|
111
121
|
* Sign up for an invitation's time slot, or cancel the sign-up. This is what
|
package/dist/write.js
CHANGED
|
@@ -26,6 +26,7 @@ import { sessionOf } from "./internal.js";
|
|
|
26
26
|
import { pageErrors, pageFormkey, pageNotices, readForm, setField, wilmaPath } from "./parsers/forms.js";
|
|
27
27
|
import { slotState } from "./parsers/invitations.js";
|
|
28
28
|
import { composePath } from "./parsers/recipients.js";
|
|
29
|
+
export { composeToAll } from "./parsers/recipients.js";
|
|
29
30
|
export { FormOutsideWilmaError } from "./parsers/forms.js";
|
|
30
31
|
/** Wilma doesn't offer this here right now (no answer form, absence reports closed…). Nothing was sent. */
|
|
31
32
|
export class NotOfferedError extends Error {
|
|
@@ -166,6 +167,7 @@ export async function sendReply(client, prepared, text) {
|
|
|
166
167
|
return;
|
|
167
168
|
throw new WriteUnconfirmedError("Wilma didn't show the answer as sent. Check the thread in Wilma before answering again.");
|
|
168
169
|
}
|
|
170
|
+
const COLLATED = "CollatedReplies";
|
|
169
171
|
export async function prepareMessage(client, to) {
|
|
170
172
|
const session = sessionFor(client);
|
|
171
173
|
let path;
|
|
@@ -185,9 +187,21 @@ export async function prepareMessage(client, to) {
|
|
|
185
187
|
const compose = await composeForm(session, path);
|
|
186
188
|
if (!compose.subjectField)
|
|
187
189
|
throw new NotOfferedError("Wilma's message form has no subject field.");
|
|
188
|
-
|
|
190
|
+
const collated = compose.form.checkboxes?.find((c) => c.name === COLLATED);
|
|
191
|
+
return {
|
|
192
|
+
kind: "message",
|
|
193
|
+
sessionMark: session.sessionMark,
|
|
194
|
+
recipients: compose.recipients,
|
|
195
|
+
form: compose.form,
|
|
196
|
+
subjectField: compose.subjectField,
|
|
197
|
+
bodyField: compose.bodyField,
|
|
198
|
+
collatedReplies: collated ? collated.checked : null,
|
|
199
|
+
};
|
|
189
200
|
}
|
|
190
|
-
/**
|
|
201
|
+
/**
|
|
202
|
+
* Send the message (once). `collatedReplies` ticks or clears Wilma's "recipients see each other's replies" box
|
|
203
|
+
* when the form has one; left out, the box stays as Wilma's form has it.
|
|
204
|
+
*/
|
|
191
205
|
export async function sendMessage(client, prepared, input) {
|
|
192
206
|
const session = sessionFor(client);
|
|
193
207
|
if (prepared.kind !== "message")
|
|
@@ -197,7 +211,12 @@ export async function sendMessage(client, prepared, input) {
|
|
|
197
211
|
if (!input.text.trim())
|
|
198
212
|
throw new TypeError("The message is empty");
|
|
199
213
|
const before = await sentState(client, null);
|
|
200
|
-
let fields =
|
|
214
|
+
let fields = prepared.form.fields;
|
|
215
|
+
const box = prepared.form.checkboxes?.find((c) => c.name === COLLATED);
|
|
216
|
+
if (box && input.collatedReplies !== undefined) {
|
|
217
|
+
fields = input.collatedReplies ? setField(fields, box.name, box.value) : fields.filter(([name]) => name !== box.name);
|
|
218
|
+
}
|
|
219
|
+
fields = setField(fields, prepared.subjectField, input.subject);
|
|
201
220
|
fields = withButton(setField(fields, prepared.bodyField, editorBody(prepared.form, input.text)), prepared.form, "addsavebtn");
|
|
202
221
|
sameSession(session, prepared);
|
|
203
222
|
const answer = await read(await session.submit(prepared.form.action, fields), "The message");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wilm-ai/wilma-client",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "Unofficial client for Wilma, the Finnish school information system: login (with two-step verification), schedule, homework, exams, messages and news.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|