@wilm-ai/wilma-client 1.8.0 → 1.10.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/README.md +1 -0
- package/dist/client.d.ts +11 -2
- package/dist/client.js +5 -0
- package/dist/core.d.ts +3 -3
- package/dist/core.js +1 -1
- 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/session.d.ts +26 -1
- package/dist/session.js +51 -1
- package/dist/write.d.ts +11 -1
- package/dist/write.js +22 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -50,6 +50,7 @@ Wilma allows one live session per account: a new login ends the previous one, an
|
|
|
50
50
|
- **Log in once** and use `client.forStudent(number)` for each child; they share the login.
|
|
51
51
|
- A session that Wilma ends (another login, or expiry) **logs in again by itself** on the next request — once, even when many requests notice at the same time, including the two-step verification step. A refused password stops further attempts, so a changed password can't lock the account.
|
|
52
52
|
- To continue a session in another process, save `client.exportSession()` (treat it like the password) and later call `WilmaClient.resume(profile, saved, onMfa)`. `client.onLogin(callback)` tells you when a new session should be saved.
|
|
53
|
+
- The password for `resume()` can be a function, e.g. one reading a keychain: `WilmaClient.resume({ baseUrl, username, password: () => readFromKeychain() }, saved)`. It's called only when Wilma has ended the session and a new login is needed, never while the saved one works, and once for many requests. If it fails, the request fails with `CredentialsUnavailableError` (with `reason` and `cause` from your error) and the next request tries again: unlike a password Wilma refuses, it never stops the client for good.
|
|
53
54
|
- `client.logout()` ends the session, as Wilma's own Log out does, so the parent can use Wilma in the browser. It never logs in: a session Wilma has already ended needs nothing, and it returns `false`. Afterwards the client and its `forStudent()` clients don't log in again by themselves. If Wilma can't be reached it throws and keeps the session, so it can be tried again.
|
|
54
55
|
|
|
55
56
|
### Any JavaScript runtime
|
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type SharedSession, type SessionEvent } from "./session.js";
|
|
1
|
+
import { type PasswordSource, type SharedSession, type SessionEvent } from "./session.js";
|
|
2
2
|
import type { Exam, GradebookEntry, Invitation, InvitationListing, MessageRecipient, StudentDetails, LessonNote, LessonNoteSummary, Message, MessageFolder, NewsItem, NewsResource, OverviewData, PastExam, Printout, StudentInfo, UpcomingExam, WilmaProfile } from "./types.js";
|
|
3
3
|
import type { WilmaTransport } from "./transport.js";
|
|
4
4
|
export type MfaCallback = (formkey: string) => Promise<string>;
|
|
@@ -19,6 +19,10 @@ export interface ClientOptions {
|
|
|
19
19
|
*/
|
|
20
20
|
sharedSession?: SharedSession;
|
|
21
21
|
}
|
|
22
|
+
/** A profile for WilmaClient.resume(): the password may be a function called only for a new login. */
|
|
23
|
+
export type ResumeProfile = Omit<WilmaProfile, "password"> & {
|
|
24
|
+
password: PasswordSource;
|
|
25
|
+
};
|
|
22
26
|
export declare class WilmaClient {
|
|
23
27
|
private session;
|
|
24
28
|
private constructor();
|
|
@@ -27,8 +31,13 @@ export declare class WilmaClient {
|
|
|
27
31
|
* A client on a session saved earlier with exportSession(), without logging
|
|
28
32
|
* in. If Wilma has ended that session, the first request logs in again by
|
|
29
33
|
* itself. Returns null when the saved state has no session.
|
|
34
|
+
*
|
|
35
|
+
* The password may be a function (e.g. reading a keychain): it is called
|
|
36
|
+
* only when Wilma needs a new login, never while the saved session works.
|
|
37
|
+
* If it fails, the request fails with CredentialsUnavailableError and the
|
|
38
|
+
* next request tries again.
|
|
30
39
|
*/
|
|
31
|
-
static resume(profile:
|
|
40
|
+
static resume(profile: ResumeProfile, state: string, onMfaRequired?: MfaCallback, options?: ClientOptions): WilmaClient | null;
|
|
32
41
|
/**
|
|
33
42
|
* A client on a saved session without any credentials: it never logs in.
|
|
34
43
|
* When Wilma ends the session, requests fail with SessionEndedError. For
|
package/dist/client.js
CHANGED
|
@@ -47,6 +47,11 @@ export class WilmaClient {
|
|
|
47
47
|
* A client on a session saved earlier with exportSession(), without logging
|
|
48
48
|
* in. If Wilma has ended that session, the first request logs in again by
|
|
49
49
|
* itself. Returns null when the saved state has no session.
|
|
50
|
+
*
|
|
51
|
+
* The password may be a function (e.g. reading a keychain): it is called
|
|
52
|
+
* only when Wilma needs a new login, never while the saved session works.
|
|
53
|
+
* If it fails, the request fails with CredentialsUnavailableError and the
|
|
54
|
+
* next request tries again.
|
|
50
55
|
*/
|
|
51
56
|
static resume(profile, state, onMfaRequired, options = {}) {
|
|
52
57
|
const session = new WilmaSession(profile.baseUrl, {
|
package/dist/core.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export type { Exam, ExamGrade, GradebookEntry, HomeworkItem, Invitation, InvitationListing, InvitationSlot, InvitationSlotState, MessageRecipient, StudentDetails, LessonNote, LessonNoteSummary, Message, MessageReply, MessageFolder, Municipality, NewsItem, NewsResource, NewsResourceAuthContext, OverviewData, PastExam, Printout, ScheduleLesson, TenantDiscoveryResponse, TenantInfo, UpcomingExam, WilmaProfile, StudentInfo, } from "./types.js";
|
|
2
2
|
export { WilmaClient } from "./client.js";
|
|
3
|
-
export type { MfaCallback, ClientOptions } from "./client.js";
|
|
4
|
-
export { WilmaSession, AuthenticationError, MfaRequiredError, APIError, SessionEndedError } from "./session.js";
|
|
5
|
-
export type { SessionOptions, RequestInit, SharedSession, SessionEvent } from "./session.js";
|
|
3
|
+
export type { MfaCallback, ClientOptions, ResumeProfile } from "./client.js";
|
|
4
|
+
export { WilmaSession, AuthenticationError, MfaRequiredError, APIError, SessionEndedError, CredentialsUnavailableError } from "./session.js";
|
|
5
|
+
export type { SessionOptions, RequestInit, SharedSession, SessionEvent, PasswordSource } from "./session.js";
|
|
6
6
|
export { setDefaultTransport } from "./transport.js";
|
|
7
7
|
export type { WilmaTransport, WilmaRequest, WilmaReply } from "./transport.js";
|
|
8
8
|
export { DOWNLOAD_TIMEOUT_MS, isHtmlResponse, setExternalFetcher } from "./downloads.js";
|
package/dist/core.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { WilmaClient } from "./client.js";
|
|
2
|
-
export { WilmaSession, AuthenticationError, MfaRequiredError, APIError, SessionEndedError } from "./session.js";
|
|
2
|
+
export { WilmaSession, AuthenticationError, MfaRequiredError, APIError, SessionEndedError, CredentialsUnavailableError } from "./session.js";
|
|
3
3
|
export { setDefaultTransport } from "./transport.js";
|
|
4
4
|
export { DOWNLOAD_TIMEOUT_MS, isHtmlResponse, setExternalFetcher } from "./downloads.js";
|
|
5
5
|
export { NetworkError, describeNetworkCode, extractCauseCode, wrapNetworkError } from "./network-error.js";
|
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/session.d.ts
CHANGED
|
@@ -56,6 +56,24 @@ export declare class APIError extends Error {
|
|
|
56
56
|
export declare class SessionEndedError extends APIError {
|
|
57
57
|
constructor(message?: string);
|
|
58
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* The password, or where to get it: a function called only when Wilma needs a
|
|
61
|
+
* new login (e.g. reading a keychain), not while a saved session works.
|
|
62
|
+
*/
|
|
63
|
+
export type PasswordSource = string | (() => Promise<string>);
|
|
64
|
+
/**
|
|
65
|
+
* Wilma needed a new login and the password couldn't be had (the function
|
|
66
|
+
* given as the password failed: a keychain locked, declined or missing the
|
|
67
|
+
* entry). Not a refusal by Wilma, so it never stops the client for good;
|
|
68
|
+
* the next request tries again. `reason` and `cause` come from the failure.
|
|
69
|
+
*/
|
|
70
|
+
export declare class CredentialsUnavailableError extends Error {
|
|
71
|
+
reason: string;
|
|
72
|
+
constructor(message: string, options?: {
|
|
73
|
+
reason?: string;
|
|
74
|
+
cause?: unknown;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
59
77
|
/**
|
|
60
78
|
* What happened to a client's session, for a host that shows it (e.g. a
|
|
61
79
|
* sign-in history): "login" (this client logged in), "adopted" (Wilma had
|
|
@@ -81,7 +99,7 @@ export declare class WilmaSession {
|
|
|
81
99
|
* fails with SessionEndedError. Returns false when the state holds no
|
|
82
100
|
* session to resume.
|
|
83
101
|
*/
|
|
84
|
-
resumeState(state: string, username?: string, password?:
|
|
102
|
+
resumeState(state: string, username?: string, password?: PasswordSource): boolean;
|
|
85
103
|
/** Called after every successful login (first or again). */
|
|
86
104
|
/** Share this login's session with the other WilmAI programs (see SharedSession). */
|
|
87
105
|
setShared(shared: SharedSession | undefined): void;
|
|
@@ -138,6 +156,13 @@ export declare class WilmaSession {
|
|
|
138
156
|
* waits for a fresh code.
|
|
139
157
|
*/
|
|
140
158
|
answerMfa(formkey: string, onMfa: (formkey: string) => Promise<string>): Promise<void>;
|
|
159
|
+
/**
|
|
160
|
+
* The password for a new login. A function given as the password is called
|
|
161
|
+
* here, only now; its answer replaces it (as a login does). If it fails, the
|
|
162
|
+
* session is "ended by Wilma" (never "logged out", never failed for good)
|
|
163
|
+
* and the error is a CredentialsUnavailableError.
|
|
164
|
+
*/
|
|
165
|
+
private resolvePassword;
|
|
141
166
|
/**
|
|
142
167
|
* A live session after Wilma ended this one, once for everyone sharing this
|
|
143
168
|
* login even if many requests notice at the same time: the newest session
|
package/dist/session.js
CHANGED
|
@@ -35,6 +35,20 @@ export class SessionEndedError extends APIError {
|
|
|
35
35
|
this.name = "SessionEndedError";
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Wilma needed a new login and the password couldn't be had (the function
|
|
40
|
+
* given as the password failed: a keychain locked, declined or missing the
|
|
41
|
+
* entry). Not a refusal by Wilma, so it never stops the client for good;
|
|
42
|
+
* the next request tries again. `reason` and `cause` come from the failure.
|
|
43
|
+
*/
|
|
44
|
+
export class CredentialsUnavailableError extends Error {
|
|
45
|
+
reason;
|
|
46
|
+
constructor(message, options = {}) {
|
|
47
|
+
super(message, options.cause === undefined ? undefined : { cause: options.cause });
|
|
48
|
+
this.name = "CredentialsUnavailableError";
|
|
49
|
+
this.reason = options.reason ?? "unavailable";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
38
52
|
/** How long Wilma may go silent (before answering, or mid-body) before a request fails. */
|
|
39
53
|
const REQUEST_TIMEOUT_MS = 30_000;
|
|
40
54
|
const MAX_REDIRECTS = 10;
|
|
@@ -268,6 +282,9 @@ export class WilmaSession {
|
|
|
268
282
|
const ended = holdsSession(auth.cookieJar, this.baseUrl) ? await this.endSession() : false;
|
|
269
283
|
auth.cookieJar.removeAllCookiesSync();
|
|
270
284
|
auth.loggedIn = false;
|
|
285
|
+
// Logged out on purpose: not "ended by Wilma", so the next request neither
|
|
286
|
+
// logs in again nor continues a session another program saved.
|
|
287
|
+
auth.endedByWilma = false;
|
|
271
288
|
auth.username = undefined;
|
|
272
289
|
auth.password = undefined;
|
|
273
290
|
return ended;
|
|
@@ -444,6 +461,36 @@ export class WilmaSession {
|
|
|
444
461
|
}
|
|
445
462
|
}
|
|
446
463
|
}
|
|
464
|
+
/**
|
|
465
|
+
* The password for a new login. A function given as the password is called
|
|
466
|
+
* here, only now; its answer replaces it (as a login does). If it fails, the
|
|
467
|
+
* session is "ended by Wilma" (never "logged out", never failed for good)
|
|
468
|
+
* and the error is a CredentialsUnavailableError.
|
|
469
|
+
*/
|
|
470
|
+
async resolvePassword() {
|
|
471
|
+
const auth = this.auth;
|
|
472
|
+
const source = auth.password;
|
|
473
|
+
if (typeof source !== "function")
|
|
474
|
+
return source ?? "";
|
|
475
|
+
try {
|
|
476
|
+
const password = await source();
|
|
477
|
+
if (typeof password !== "string" || !password)
|
|
478
|
+
throw new Error("No password was returned");
|
|
479
|
+
// logout() may have cleared it meanwhile: then it stays cleared.
|
|
480
|
+
if (auth.password === source)
|
|
481
|
+
auth.password = password;
|
|
482
|
+
return password;
|
|
483
|
+
}
|
|
484
|
+
catch (err) {
|
|
485
|
+
auth.loggedIn = false;
|
|
486
|
+
auth.endedByWilma = true;
|
|
487
|
+
auth.onEvent?.("ended");
|
|
488
|
+
if (err instanceof CredentialsUnavailableError)
|
|
489
|
+
throw err;
|
|
490
|
+
const reason = typeof err?.reason === "string" ? err.reason : undefined;
|
|
491
|
+
throw new CredentialsUnavailableError(err instanceof Error ? err.message : String(err), { reason, cause: err });
|
|
492
|
+
}
|
|
493
|
+
}
|
|
447
494
|
/**
|
|
448
495
|
* A live session after Wilma ended this one, once for everyone sharing this
|
|
449
496
|
* login even if many requests notice at the same time: the newest session
|
|
@@ -462,6 +509,9 @@ export class WilmaSession {
|
|
|
462
509
|
auth.onEvent?.("ended");
|
|
463
510
|
throw new SessionEndedError();
|
|
464
511
|
}
|
|
512
|
+
// Get the password before changing anything: if it can't be had, the
|
|
513
|
+
// session stays "ended by Wilma" and the next request tries again.
|
|
514
|
+
const password = await this.resolvePassword();
|
|
465
515
|
auth.loggedIn = false;
|
|
466
516
|
// Start from a clean jar: a leftover session cookie must not make a
|
|
467
517
|
// failed login look successful.
|
|
@@ -469,7 +519,7 @@ export class WilmaSession {
|
|
|
469
519
|
if (this.debug)
|
|
470
520
|
console.error("[wilmai] session cancelled or expired; logging in again");
|
|
471
521
|
try {
|
|
472
|
-
await this.login(auth.username,
|
|
522
|
+
await this.login(auth.username, password);
|
|
473
523
|
}
|
|
474
524
|
catch (err) {
|
|
475
525
|
if (err instanceof MfaRequiredError && auth.onMfa) {
|
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.10.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": {
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"scripts": {
|
|
40
40
|
"build": "tsc -p tsconfig.json",
|
|
41
41
|
"lint": "tsc -p tsconfig.json --noEmit",
|
|
42
|
-
"test": "pnpm run build && node test/login.mjs && node test/logout.mjs && node test/core.mjs && node test/write.mjs && node test/parser-attendance.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-attendance.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",
|
|
42
|
+
"test": "pnpm run build && node test/login.mjs && node test/logout.mjs && node test/password-source.mjs && node test/core.mjs && node test/write.mjs && node test/parser-attendance.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-attendance.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",
|
|
43
43
|
"test:live": "tsc -p tsconfig.json && node test/live.spec.mjs",
|
|
44
44
|
"fixtures:anonymize": "node scripts/anonymize-fixture.mjs"
|
|
45
45
|
},
|