@falkenslab/moodle-driver 0.0.0-stage → 0.3.1
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 +133 -2
- package/dist/areas/activities/build.d.ts +98 -0
- package/dist/areas/activities/build.js +246 -0
- package/dist/areas/activities/build.js.map +1 -0
- package/dist/areas/activities/content.d.ts +53 -0
- package/dist/areas/activities/content.js +113 -0
- package/dist/areas/activities/content.js.map +1 -0
- package/dist/areas/assignments/grader.d.ts +68 -0
- package/dist/areas/assignments/grader.js +169 -0
- package/dist/areas/assignments/grader.js.map +1 -0
- package/dist/areas/assignments/grading.d.ts +86 -0
- package/dist/areas/assignments/grading.js +162 -0
- package/dist/areas/assignments/grading.js.map +1 -0
- package/dist/areas/assignments/student.d.ts +57 -0
- package/dist/areas/assignments/student.js +115 -0
- package/dist/areas/assignments/student.js.map +1 -0
- package/dist/areas/calendar/calendar.d.ts +43 -0
- package/dist/areas/calendar/calendar.js +84 -0
- package/dist/areas/calendar/calendar.js.map +1 -0
- package/dist/areas/completion/completion.d.ts +50 -0
- package/dist/areas/completion/completion.js +112 -0
- package/dist/areas/completion/completion.js.map +1 -0
- package/dist/areas/courses/list.d.ts +59 -0
- package/dist/areas/courses/list.js +74 -0
- package/dist/areas/courses/list.js.map +1 -0
- package/dist/areas/courses/overview.d.ts +83 -0
- package/dist/areas/courses/overview.js +137 -0
- package/dist/areas/courses/overview.js.map +1 -0
- package/dist/areas/courses/state.d.ts +51 -0
- package/dist/areas/courses/state.js +45 -0
- package/dist/areas/courses/state.js.map +1 -0
- package/dist/areas/courses/structure.d.ts +89 -0
- package/dist/areas/courses/structure.js +104 -0
- package/dist/areas/courses/structure.js.map +1 -0
- package/dist/areas/forums/discussions.d.ts +57 -0
- package/dist/areas/forums/discussions.js +119 -0
- package/dist/areas/forums/discussions.js.map +1 -0
- package/dist/areas/forums/posts.d.ts +83 -0
- package/dist/areas/forums/posts.js +114 -0
- package/dist/areas/forums/posts.js.map +1 -0
- package/dist/areas/grades/gradebook.d.ts +104 -0
- package/dist/areas/grades/gradebook.js +250 -0
- package/dist/areas/grades/gradebook.js.map +1 -0
- package/dist/areas/grades/user-report.d.ts +26 -0
- package/dist/areas/grades/user-report.js +71 -0
- package/dist/areas/grades/user-report.js.map +1 -0
- package/dist/areas/h5p/h5p.d.ts +33 -0
- package/dist/areas/h5p/h5p.js +104 -0
- package/dist/areas/h5p/h5p.js.map +1 -0
- package/dist/areas/lessons/lessons.d.ts +60 -0
- package/dist/areas/lessons/lessons.js +145 -0
- package/dist/areas/lessons/lessons.js.map +1 -0
- package/dist/areas/messages/messages.d.ts +68 -0
- package/dist/areas/messages/messages.js +84 -0
- package/dist/areas/messages/messages.js.map +1 -0
- package/dist/areas/participants/participants.d.ts +57 -0
- package/dist/areas/participants/participants.js +135 -0
- package/dist/areas/participants/participants.js.map +1 -0
- package/dist/areas/questions/banks.d.ts +83 -0
- package/dist/areas/questions/banks.js +239 -0
- package/dist/areas/questions/banks.js.map +1 -0
- package/dist/areas/quizzes/attempts.d.ts +123 -0
- package/dist/areas/quizzes/attempts.js +300 -0
- package/dist/areas/quizzes/attempts.js.map +1 -0
- package/dist/areas/quizzes/results.d.ts +36 -0
- package/dist/areas/quizzes/results.js +93 -0
- package/dist/areas/quizzes/results.js.map +1 -0
- package/dist/areas/quizzes/structure.d.ts +42 -0
- package/dist/areas/quizzes/structure.js +100 -0
- package/dist/areas/quizzes/structure.js.map +1 -0
- package/dist/areas/site/info.d.ts +25 -0
- package/dist/areas/site/info.js +45 -0
- package/dist/areas/site/info.js.map +1 -0
- package/dist/areas/site/users.d.ts +56 -0
- package/dist/areas/site/users.js +89 -0
- package/dist/areas/site/users.js.map +1 -0
- package/dist/areas/surveys/choice.d.ts +38 -0
- package/dist/areas/surveys/choice.js +105 -0
- package/dist/areas/surveys/choice.js.map +1 -0
- package/dist/areas/surveys/surveys.d.ts +51 -0
- package/dist/areas/surveys/surveys.js +201 -0
- package/dist/areas/surveys/surveys.js.map +1 -0
- package/dist/browser/chromium.d.ts +15 -0
- package/dist/browser/chromium.js +53 -0
- package/dist/browser/chromium.js.map +1 -0
- package/dist/browser/cookies.d.ts +11 -0
- package/dist/browser/cookies.js +31 -0
- package/dist/browser/cookies.js.map +1 -0
- package/dist/browser/manager.d.ts +57 -0
- package/dist/browser/manager.js +177 -0
- package/dist/browser/manager.js.map +1 -0
- package/dist/browser.d.ts +53 -0
- package/dist/browser.js +167 -0
- package/dist/browser.js.map +1 -0
- package/dist/categories.d.ts +28 -0
- package/dist/categories.js +222 -0
- package/dist/categories.js.map +1 -0
- package/dist/chromiumInstaller.d.ts +15 -0
- package/dist/chromiumInstaller.js +52 -0
- package/dist/chromiumInstaller.js.map +1 -0
- package/dist/cli/args.d.ts +33 -0
- package/dist/cli/args.js +194 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/commands.generated.d.ts +4 -0
- package/dist/cli/commands.generated.js +2203 -0
- package/dist/cli/commands.generated.js.map +1 -0
- package/dist/cli/config.d.ts +46 -0
- package/dist/cli/config.js +107 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +19 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/output.d.ts +4 -0
- package/dist/cli/output.js +71 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/cli/run.d.ts +15 -0
- package/dist/cli/run.js +194 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/cli/spec.d.ts +42 -0
- package/dist/cli/spec.js +4 -0
- package/dist/cli/spec.js.map +1 -0
- package/dist/client.d.ts +217 -0
- package/dist/client.js +179 -0
- package/dist/client.js.map +1 -0
- package/dist/core/ajax.d.ts +25 -0
- package/dist/core/ajax.js +85 -0
- package/dist/core/ajax.js.map +1 -0
- package/dist/core/editor.d.ts +18 -0
- package/dist/core/editor.js +51 -0
- package/dist/core/editor.js.map +1 -0
- package/dist/core/errors.d.ts +12 -0
- package/dist/core/errors.js +10 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/files.d.ts +16 -0
- package/dist/core/files.js +58 -0
- package/dist/core/files.js.map +1 -0
- package/dist/core/forms.d.ts +45 -0
- package/dist/core/forms.js +200 -0
- package/dist/core/forms.js.map +1 -0
- package/dist/core/http.d.ts +18 -0
- package/dist/core/http.js +22 -0
- package/dist/core/http.js.map +1 -0
- package/dist/core/inPage.d.ts +8 -0
- package/dist/core/inPage.js +10 -0
- package/dist/core/inPage.js.map +1 -0
- package/dist/core/login.d.ts +40 -0
- package/dist/core/login.js +83 -0
- package/dist/core/login.js.map +1 -0
- package/dist/core/pace.d.ts +4 -0
- package/dist/core/pace.js +21 -0
- package/dist/core/pace.js.map +1 -0
- package/dist/core/session.d.ts +55 -0
- package/dist/core/session.js +157 -0
- package/dist/core/session.js.map +1 -0
- package/dist/core/site.d.ts +8 -0
- package/dist/core/site.js +36 -0
- package/dist/core/site.js.map +1 -0
- package/dist/core/text.d.ts +21 -0
- package/dist/core/text.js +113 -0
- package/dist/core/text.js.map +1 -0
- package/dist/home.d.ts +19 -0
- package/dist/home.js +54 -0
- package/dist/home.js.map +1 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -0
- package/dist/internals.d.ts +11 -0
- package/dist/internals.js +15 -0
- package/dist/internals.js.map +1 -0
- package/dist/moodle/activities.d.ts +94 -0
- package/dist/moodle/activities.js +240 -0
- package/dist/moodle/activities.js.map +1 -0
- package/dist/moodle/ajax.d.ts +22 -0
- package/dist/moodle/ajax.js +91 -0
- package/dist/moodle/ajax.js.map +1 -0
- package/dist/moodle/assign.d.ts +65 -0
- package/dist/moodle/assign.js +162 -0
- package/dist/moodle/assign.js.map +1 -0
- package/dist/moodle/calendar.d.ts +41 -0
- package/dist/moodle/calendar.js +80 -0
- package/dist/moodle/calendar.js.map +1 -0
- package/dist/moodle/choice.d.ts +33 -0
- package/dist/moodle/choice.js +100 -0
- package/dist/moodle/choice.js.map +1 -0
- package/dist/moodle/completion.d.ts +48 -0
- package/dist/moodle/completion.js +108 -0
- package/dist/moodle/completion.js.map +1 -0
- package/dist/moodle/content.d.ts +43 -0
- package/dist/moodle/content.js +105 -0
- package/dist/moodle/content.js.map +1 -0
- package/dist/moodle/course.d.ts +51 -0
- package/dist/moodle/course.js +45 -0
- package/dist/moodle/course.js.map +1 -0
- package/dist/moodle/courses.d.ts +57 -0
- package/dist/moodle/courses.js +70 -0
- package/dist/moodle/courses.js.map +1 -0
- package/dist/moodle/editor.d.ts +18 -0
- package/dist/moodle/editor.js +50 -0
- package/dist/moodle/editor.js.map +1 -0
- package/dist/moodle/files.d.ts +16 -0
- package/dist/moodle/files.js +57 -0
- package/dist/moodle/files.js.map +1 -0
- package/dist/moodle/forms.d.ts +44 -0
- package/dist/moodle/forms.js +199 -0
- package/dist/moodle/forms.js.map +1 -0
- package/dist/moodle/forum.d.ts +55 -0
- package/dist/moodle/forum.js +115 -0
- package/dist/moodle/forum.js.map +1 -0
- package/dist/moodle/forumPosts.d.ts +83 -0
- package/dist/moodle/forumPosts.js +113 -0
- package/dist/moodle/forumPosts.js.map +1 -0
- package/dist/moodle/gradebook.d.ts +102 -0
- package/dist/moodle/gradebook.js +246 -0
- package/dist/moodle/gradebook.js.map +1 -0
- package/dist/moodle/grades.d.ts +24 -0
- package/dist/moodle/grades.js +67 -0
- package/dist/moodle/grades.js.map +1 -0
- package/dist/moodle/grading.d.ts +92 -0
- package/dist/moodle/grading.js +161 -0
- package/dist/moodle/grading.js.map +1 -0
- package/dist/moodle/h5p.d.ts +31 -0
- package/dist/moodle/h5p.js +100 -0
- package/dist/moodle/h5p.js.map +1 -0
- package/dist/moodle/inPage.d.ts +8 -0
- package/dist/moodle/inPage.js +10 -0
- package/dist/moodle/inPage.js.map +1 -0
- package/dist/moodle/lesson.d.ts +57 -0
- package/dist/moodle/lesson.js +140 -0
- package/dist/moodle/lesson.js.map +1 -0
- package/dist/moodle/login.d.ts +37 -0
- package/dist/moodle/login.js +77 -0
- package/dist/moodle/login.js.map +1 -0
- package/dist/moodle/messages.d.ts +66 -0
- package/dist/moodle/messages.js +80 -0
- package/dist/moodle/messages.js.map +1 -0
- package/dist/moodle/overview.d.ts +83 -0
- package/dist/moodle/overview.js +137 -0
- package/dist/moodle/overview.js.map +1 -0
- package/dist/moodle/pace.d.ts +4 -0
- package/dist/moodle/pace.js +19 -0
- package/dist/moodle/pace.js.map +1 -0
- package/dist/moodle/participants.d.ts +55 -0
- package/dist/moodle/participants.js +131 -0
- package/dist/moodle/participants.js.map +1 -0
- package/dist/moodle/questionBank.d.ts +81 -0
- package/dist/moodle/questionBank.js +235 -0
- package/dist/moodle/questionBank.js.map +1 -0
- package/dist/moodle/quiz.d.ts +117 -0
- package/dist/moodle/quiz.js +291 -0
- package/dist/moodle/quiz.js.map +1 -0
- package/dist/moodle/quizResults.d.ts +36 -0
- package/dist/moodle/quizResults.js +93 -0
- package/dist/moodle/quizResults.js.map +1 -0
- package/dist/moodle/quizStructure.d.ts +42 -0
- package/dist/moodle/quizStructure.js +100 -0
- package/dist/moodle/quizStructure.js.map +1 -0
- package/dist/moodle/session.d.ts +39 -0
- package/dist/moodle/session.js +99 -0
- package/dist/moodle/session.js.map +1 -0
- package/dist/moodle/site.d.ts +8 -0
- package/dist/moodle/site.js +36 -0
- package/dist/moodle/site.js.map +1 -0
- package/dist/moodle/siteInfo.d.ts +25 -0
- package/dist/moodle/siteInfo.js +45 -0
- package/dist/moodle/siteInfo.js.map +1 -0
- package/dist/moodle/structure.d.ts +94 -0
- package/dist/moodle/structure.js +103 -0
- package/dist/moodle/structure.js.map +1 -0
- package/dist/moodle/submission.d.ts +48 -0
- package/dist/moodle/submission.js +109 -0
- package/dist/moodle/submission.js.map +1 -0
- package/dist/moodle/survey.d.ts +47 -0
- package/dist/moodle/survey.js +197 -0
- package/dist/moodle/survey.js.map +1 -0
- package/dist/moodle/text.d.ts +21 -0
- package/dist/moodle/text.js +113 -0
- package/dist/moodle/text.js.map +1 -0
- package/dist/moodle/user.d.ts +54 -0
- package/dist/moodle/user.js +78 -0
- package/dist/moodle/user.js.map +1 -0
- package/dist/options.d.ts +30 -0
- package/dist/options.js +35 -0
- package/dist/options.js.map +1 -0
- package/dist/sessionCookies.d.ts +11 -0
- package/dist/sessionCookies.js +31 -0
- package/dist/sessionCookies.js.map +1 -0
- package/dist/testing/fakeMoodle.d.ts +55 -0
- package/dist/testing/fakeMoodle.js +103 -0
- package/dist/testing/fakeMoodle.js.map +1 -0
- package/dist/testing/gradingPanel.d.ts +6 -0
- package/dist/testing/gradingPanel.js +22 -0
- package/dist/testing/gradingPanel.js.map +1 -0
- package/dist/testing/index.d.ts +5 -0
- package/dist/testing/index.js +18 -0
- package/dist/testing/index.js.map +1 -0
- package/package.json +77 -4
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { NotLoggedInError, openSitePage, pageIsLoggedIn } from "./session.js";
|
|
2
|
+
const POLL_MS = 1000;
|
|
3
|
+
export class LoginTimeoutError extends Error {
|
|
4
|
+
name = "LoginTimeoutError";
|
|
5
|
+
}
|
|
6
|
+
export class LoginWindowClosedError extends Error {
|
|
7
|
+
name = "LoginWindowClosedError";
|
|
8
|
+
constructor() {
|
|
9
|
+
super("The login window was closed before the person logged in.");
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/** The logged-in user, read from their own profile page (`user/profile.php`). */
|
|
13
|
+
export async function readOwnProfile(context, siteUrl) {
|
|
14
|
+
const page = await openSitePage(context, siteUrl, "user/profile.php");
|
|
15
|
+
try {
|
|
16
|
+
return await page.evaluate(() => {
|
|
17
|
+
const g = globalThis;
|
|
18
|
+
// The page heading is the user's full name, in any interface language.
|
|
19
|
+
const heading = g.document.querySelector("#page-header h1, .page-header-headings h1, h1");
|
|
20
|
+
return { userId: g.M.cfg.userId, fullName: heading?.textContent?.trim() ?? "" };
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
await page.close().catch(() => undefined);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Waits until `page` shows the site with a user logged in. The person may go through other domains
|
|
29
|
+
* (SSO) on the way; reads fail while a page navigates, and are retried.
|
|
30
|
+
*/
|
|
31
|
+
export async function waitForLogin(page, siteUrl, { timeoutMs, pollMs = POLL_MS, onWait }) {
|
|
32
|
+
const start = Date.now();
|
|
33
|
+
for (;;) {
|
|
34
|
+
if (page.isClosed())
|
|
35
|
+
throw new LoginWindowClosedError();
|
|
36
|
+
if (await pageIsLoggedIn(page, siteUrl).catch(() => false))
|
|
37
|
+
return;
|
|
38
|
+
const elapsed = Date.now() - start;
|
|
39
|
+
if (elapsed >= timeoutMs)
|
|
40
|
+
throw new LoginTimeoutError(`Nobody logged in within ${Math.round(timeoutMs / 1000)} seconds; the window was closed.`);
|
|
41
|
+
onWait?.(elapsed);
|
|
42
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Makes sure there's a session: the user at once if there is one; otherwise a browser window at the
|
|
47
|
+
* site's login page, waiting (calling `onWait`) until the person logs in there themselves. The session
|
|
48
|
+
* is saved (ADR-003), and the window closes again unless the browser is shown on demand.
|
|
49
|
+
*/
|
|
50
|
+
export async function logIn(browser, siteUrl, { timeoutMs, onWait }) {
|
|
51
|
+
try {
|
|
52
|
+
return { status: "already-logged-in", ...(await readOwnProfile(await browser.context(), siteUrl)) };
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (!(error instanceof NotLoggedInError))
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
const context = await browser.context({ headed: true });
|
|
59
|
+
const page = context.pages()[0] ?? (await context.newPage());
|
|
60
|
+
await page.goto(new URL("login/index.php", siteUrl).href, { waitUntil: "domcontentloaded" });
|
|
61
|
+
await page.bringToFront();
|
|
62
|
+
try {
|
|
63
|
+
await waitForLogin(page, siteUrl, { timeoutMs, onWait });
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (error instanceof LoginTimeoutError)
|
|
67
|
+
await browser.close();
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
const user = await readOwnProfile(context, siteUrl);
|
|
71
|
+
await browser.saveSession();
|
|
72
|
+
// Back to the chosen mode: the next call reopens the browser without a window.
|
|
73
|
+
if (!browser.visible())
|
|
74
|
+
await browser.close();
|
|
75
|
+
return { status: "logged-in", ...user };
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=login.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/moodle/login.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { BrowserContext } from "playwright-core";
|
|
2
|
+
/** Moodle didn't deliver a message. */
|
|
3
|
+
export declare class MessageError extends Error {
|
|
4
|
+
name: string;
|
|
5
|
+
}
|
|
6
|
+
export interface Notification {
|
|
7
|
+
id: number;
|
|
8
|
+
subject: string;
|
|
9
|
+
/** The notification's text, plain. */
|
|
10
|
+
text: string;
|
|
11
|
+
/** Who it's from: a user id, or a negative one for the site itself (noreply, support). */
|
|
12
|
+
fromUserId: number;
|
|
13
|
+
time?: string;
|
|
14
|
+
read: boolean;
|
|
15
|
+
/** Where it leads (the post, the assignment…). */
|
|
16
|
+
url?: string;
|
|
17
|
+
/** What sent it: "mod_forum", "mod_assign"… and its kind. */
|
|
18
|
+
component?: string;
|
|
19
|
+
eventType?: string;
|
|
20
|
+
}
|
|
21
|
+
/** The person's notifications, newest first, only the unread ones when asked. */
|
|
22
|
+
export declare function notifications(context: BrowserContext, siteUrl: string, { unreadOnly }?: {
|
|
23
|
+
unreadOnly?: boolean;
|
|
24
|
+
}): Promise<Notification[]>;
|
|
25
|
+
/** Marks all the person's notifications read, as the popover's "Mark all as read" does; doing it twice changes nothing. */
|
|
26
|
+
export declare function markNotificationsRead(context: BrowserContext, siteUrl: string): Promise<{
|
|
27
|
+
changed: string;
|
|
28
|
+
}>;
|
|
29
|
+
export interface ConversationSummary {
|
|
30
|
+
id: number;
|
|
31
|
+
type: "private" | "group" | "self";
|
|
32
|
+
/** A group conversation's name (its course group). */
|
|
33
|
+
name?: string;
|
|
34
|
+
members: {
|
|
35
|
+
userId: number;
|
|
36
|
+
fullName: string;
|
|
37
|
+
}[];
|
|
38
|
+
unread: number;
|
|
39
|
+
lastMessage?: {
|
|
40
|
+
fromUserId: number;
|
|
41
|
+
text: string;
|
|
42
|
+
time?: string;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** The person's conversations, latest first, with their last message as text. */
|
|
46
|
+
export declare function conversations(context: BrowserContext, siteUrl: string): Promise<ConversationSummary[]>;
|
|
47
|
+
/** A conversation's members and messages, oldest first, as text. */
|
|
48
|
+
export declare function conversation(context: BrowserContext, siteUrl: string, conversationId: number): Promise<{
|
|
49
|
+
id: number;
|
|
50
|
+
members: {
|
|
51
|
+
userId: number;
|
|
52
|
+
fullName: string;
|
|
53
|
+
}[];
|
|
54
|
+
messages: {
|
|
55
|
+
id: number;
|
|
56
|
+
fromUserId: number;
|
|
57
|
+
text: string;
|
|
58
|
+
time: string | undefined;
|
|
59
|
+
}[];
|
|
60
|
+
}>;
|
|
61
|
+
/** Sends a private message to a user, as the messaging drawer does; returns the message and its conversation. */
|
|
62
|
+
export declare function sendMessage(context: BrowserContext, siteUrl: string, userId: number, text: string): Promise<{
|
|
63
|
+
changed: string;
|
|
64
|
+
messageId: number;
|
|
65
|
+
conversationId?: number;
|
|
66
|
+
}>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { callService, readPageConfig } from "./ajax.js";
|
|
2
|
+
import { isoDate } from "./courses.js";
|
|
3
|
+
import { htmlToTexts } from "./forum.js";
|
|
4
|
+
import { decodeEntities } from "./text.js";
|
|
5
|
+
/** Moodle didn't deliver a message. */
|
|
6
|
+
export class MessageError extends Error {
|
|
7
|
+
name = "MessageError";
|
|
8
|
+
}
|
|
9
|
+
/** The person's notifications, newest first, only the unread ones when asked. */
|
|
10
|
+
export async function notifications(context, siteUrl, { unreadOnly = false } = {}) {
|
|
11
|
+
const { notifications: found } = await callService(context, siteUrl, "message_popup_get_popup_notifications", { useridto: 0, limit: 0, offset: 0 });
|
|
12
|
+
return found
|
|
13
|
+
.filter((n) => !unreadOnly || !n.read)
|
|
14
|
+
.map((n) => ({
|
|
15
|
+
id: n.id,
|
|
16
|
+
subject: decodeEntities(n.subject),
|
|
17
|
+
text: decodeEntities(n.fullmessage || n.smallmessage).trim(),
|
|
18
|
+
fromUserId: n.useridfrom,
|
|
19
|
+
time: isoDate(n.timecreated),
|
|
20
|
+
read: n.read,
|
|
21
|
+
...(n.contexturl ? { url: n.contexturl } : {}),
|
|
22
|
+
...(n.component ? { component: n.component } : {}),
|
|
23
|
+
...(n.eventtype ? { eventType: n.eventtype } : {}),
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
26
|
+
/** Marks all the person's notifications read, as the popover's "Mark all as read" does; doing it twice changes nothing. */
|
|
27
|
+
export async function markNotificationsRead(context, siteUrl) {
|
|
28
|
+
const { userId } = await readPageConfig(context, siteUrl);
|
|
29
|
+
await callService(context, siteUrl, "core_message_mark_all_notifications_as_read", { useridto: userId });
|
|
30
|
+
return { changed: "All notifications marked read." };
|
|
31
|
+
}
|
|
32
|
+
/** A conversation's types, by Moodle's MESSAGE_CONVERSATION_TYPE_* values. */
|
|
33
|
+
const CONVERSATION_TYPES = { 1: "private", 2: "group", 3: "self" };
|
|
34
|
+
/** The person's conversations, latest first, with their last message as text. */
|
|
35
|
+
export async function conversations(context, siteUrl) {
|
|
36
|
+
const { userId } = await readPageConfig(context, siteUrl);
|
|
37
|
+
const { conversations: found } = await callService(context, siteUrl, "core_message_get_conversations", { userid: userId, limitfrom: 0, limitnum: 0 });
|
|
38
|
+
const texts = await htmlToTexts(context, found.map((c) => c.messages[0]?.text ?? ""));
|
|
39
|
+
return found.map((c, i) => {
|
|
40
|
+
const last = c.messages[0];
|
|
41
|
+
return {
|
|
42
|
+
id: c.id,
|
|
43
|
+
type: CONVERSATION_TYPES[c.type] ?? "private",
|
|
44
|
+
...(c.name ? { name: decodeEntities(c.name) } : {}),
|
|
45
|
+
members: c.members.map((m) => ({ userId: m.id, fullName: m.fullname })),
|
|
46
|
+
unread: c.unreadcount ?? 0,
|
|
47
|
+
...(last ? { lastMessage: { fromUserId: last.useridfrom, text: texts[i] ?? "", time: isoDate(last.timecreated) } } : {}),
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/** A conversation's members and messages, oldest first, as text. */
|
|
52
|
+
export async function conversation(context, siteUrl, conversationId) {
|
|
53
|
+
const { userId } = await readPageConfig(context, siteUrl);
|
|
54
|
+
const found = await callService(context, siteUrl, "core_message_get_conversation_messages", {
|
|
55
|
+
currentuserid: userId,
|
|
56
|
+
convid: conversationId,
|
|
57
|
+
limitfrom: 0,
|
|
58
|
+
limitnum: 0,
|
|
59
|
+
newest: false,
|
|
60
|
+
});
|
|
61
|
+
const texts = await htmlToTexts(context, found.messages.map((m) => m.text));
|
|
62
|
+
return {
|
|
63
|
+
id: found.id,
|
|
64
|
+
members: found.members.map((m) => ({ userId: m.id, fullName: m.fullname })),
|
|
65
|
+
messages: found.messages.map((m, i) => ({ id: m.id, fromUserId: m.useridfrom, text: texts[i] ?? "", time: isoDate(m.timecreated) })),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** Sends a private message to a user, as the messaging drawer does; returns the message and its conversation. */
|
|
69
|
+
export async function sendMessage(context, siteUrl, userId, text) {
|
|
70
|
+
if (text.replace(/<[^>]*>/g, "").replace(/ /g, " ").trim() === "")
|
|
71
|
+
throw new MessageError("The message is empty: write something before sending it.");
|
|
72
|
+
const [sent] = await callService(context, siteUrl, "core_message_send_instant_messages", {
|
|
73
|
+
messages: [{ touserid: userId, text, textformat: 1 }],
|
|
74
|
+
});
|
|
75
|
+
// A refused message comes back with id -1 and Moodle's reason (blocked, not a contact…).
|
|
76
|
+
if (!sent || sent.msgid < 0)
|
|
77
|
+
throw new MessageError(`Moodle didn't send the message to user ${userId}: ${sent?.errormessage ?? "no reason given"}`);
|
|
78
|
+
return { changed: `Message ${sent.msgid} sent to user ${userId}.`, messageId: sent.msgid, ...(sent.conversationid ? { conversationId: sent.conversationid } : {}) };
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=messages.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"messages.js","sourceRoot":"","sources":["../../src/moodle/messages.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { BrowserContext } from "playwright-core";
|
|
2
|
+
/** One column of Moodle 5's activities overview, as `core_courseformat_get_overview_information` gives it. */
|
|
3
|
+
interface OverviewItem {
|
|
4
|
+
key: string;
|
|
5
|
+
name: string;
|
|
6
|
+
contenttype: string;
|
|
7
|
+
alertlabel?: string;
|
|
8
|
+
alertcount?: string | number;
|
|
9
|
+
contentjson?: string | null;
|
|
10
|
+
}
|
|
11
|
+
/** A column's value, whatever exporter Moodle used for it, as text plus what it carries besides. */
|
|
12
|
+
export interface OverviewField {
|
|
13
|
+
key: string;
|
|
14
|
+
label: string;
|
|
15
|
+
text: string;
|
|
16
|
+
date?: string;
|
|
17
|
+
url?: string;
|
|
18
|
+
details?: {
|
|
19
|
+
label: string;
|
|
20
|
+
value: string;
|
|
21
|
+
}[];
|
|
22
|
+
alert?: {
|
|
23
|
+
label: string;
|
|
24
|
+
count: number;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** Reads one column: dates (humandate), plain values, dialogs with their lines, links and their alerts. */
|
|
28
|
+
export declare function overviewField(item: OverviewItem): OverviewField;
|
|
29
|
+
/**
|
|
30
|
+
* Moodle 5's activities overview of a course for one type ("resource" for every resource type):
|
|
31
|
+
* per activity, the columns its overview page shows the person (due dates, submissions, what needs
|
|
32
|
+
* grading, their own grade…). Without a type, every type in the course's state: "resource" first,
|
|
33
|
+
* which brings pages, books, files, folders and links at once, then each type still missing.
|
|
34
|
+
*/
|
|
35
|
+
export declare function activitiesOverview(context: BrowserContext, siteUrl: string, courseId: number, type?: string): Promise<{
|
|
36
|
+
fields: OverviewField[];
|
|
37
|
+
available?: boolean | undefined;
|
|
38
|
+
visible: boolean;
|
|
39
|
+
url?: string | undefined;
|
|
40
|
+
id: number;
|
|
41
|
+
name: string;
|
|
42
|
+
type: string;
|
|
43
|
+
}[]>;
|
|
44
|
+
/**
|
|
45
|
+
* What the person has to do by a date (calendar action events: submit, attempt, grade…), from the
|
|
46
|
+
* last `pastDays` to `days` ahead, in one course or all. Teachers' grading counts come from the
|
|
47
|
+
* activities overview instead: Moodle gives them few action events.
|
|
48
|
+
*/
|
|
49
|
+
export declare function deadlines(context: BrowserContext, siteUrl: string, { courseId, days, pastDays }: {
|
|
50
|
+
courseId?: number;
|
|
51
|
+
days?: number;
|
|
52
|
+
pastDays?: number;
|
|
53
|
+
}): Promise<{
|
|
54
|
+
action?: {
|
|
55
|
+
url?: string | undefined;
|
|
56
|
+
count?: number | undefined;
|
|
57
|
+
name: string;
|
|
58
|
+
actionable: boolean;
|
|
59
|
+
} | undefined;
|
|
60
|
+
course?: {
|
|
61
|
+
shortname?: string | undefined;
|
|
62
|
+
id: number;
|
|
63
|
+
} | undefined;
|
|
64
|
+
event: string;
|
|
65
|
+
kind: string;
|
|
66
|
+
date: string;
|
|
67
|
+
overdue: boolean;
|
|
68
|
+
type?: string | undefined;
|
|
69
|
+
activity: string;
|
|
70
|
+
activityId?: number | undefined;
|
|
71
|
+
}[]>;
|
|
72
|
+
/** What changed in a course's activities since a date, with their names from the course's state. */
|
|
73
|
+
export declare function courseUpdates(context: BrowserContext, siteUrl: string, courseId: number, since: Date): Promise<{
|
|
74
|
+
changes: {
|
|
75
|
+
items?: number | undefined;
|
|
76
|
+
time?: string | undefined;
|
|
77
|
+
what: string;
|
|
78
|
+
}[];
|
|
79
|
+
name?: string | undefined;
|
|
80
|
+
type?: string | undefined;
|
|
81
|
+
id: number;
|
|
82
|
+
}[]>;
|
|
83
|
+
export {};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { callService } from "./ajax.js";
|
|
2
|
+
import { courseState } from "./course.js";
|
|
3
|
+
import { decodeEntities } from "./text.js";
|
|
4
|
+
const plain = (html) => decodeEntities(html.replace(/<[^>]+>/g, " ")).replace(/\s+/g, " ").trim();
|
|
5
|
+
/** Reads one column: dates (humandate), plain values, dialogs with their lines, links and their alerts. */
|
|
6
|
+
export function overviewField(item) {
|
|
7
|
+
let data = {};
|
|
8
|
+
try {
|
|
9
|
+
data = JSON.parse(item.contentjson ?? "null") ?? {};
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
// An exporter Moodle may add later: the label alone.
|
|
13
|
+
}
|
|
14
|
+
const type = item.contenttype.split("\\").pop() ?? "";
|
|
15
|
+
const field = { key: item.key, label: decodeEntities(item.name), text: "" };
|
|
16
|
+
if (type === "humandate" && typeof data.timestamp === "number") {
|
|
17
|
+
field.date = new Date(data.timestamp * 1000).toISOString();
|
|
18
|
+
field.text = [data.date, data.time].filter(Boolean).join(" ");
|
|
19
|
+
}
|
|
20
|
+
else if (type === "pix_icon") {
|
|
21
|
+
// A mark (answered, submitted…): what it means is in its alt text.
|
|
22
|
+
const extras = data.extras ?? [];
|
|
23
|
+
field.text = decodeEntities(extras.find((e) => e.name === "alt")?.value ?? extras.find((e) => e.name === "title")?.value ?? "");
|
|
24
|
+
}
|
|
25
|
+
else if (type === "overviewdialog") {
|
|
26
|
+
field.text = plain(String(data.buttoncontent ?? ""));
|
|
27
|
+
const items = data.items ?? [];
|
|
28
|
+
if (items.length > 0)
|
|
29
|
+
field.details = items.map((i) => ({ label: plain(i.label), value: plain(String(i.value)) }));
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
field.text = plain(String(data.content ?? data.value ?? ""));
|
|
33
|
+
if (typeof data.linkurl === "string")
|
|
34
|
+
field.url = data.linkurl;
|
|
35
|
+
}
|
|
36
|
+
if (item.alertlabel)
|
|
37
|
+
field.alert = { label: decodeEntities(item.alertlabel), count: Number(item.alertcount ?? 0) };
|
|
38
|
+
return field;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Moodle 5's activities overview of a course for one type ("resource" for every resource type):
|
|
42
|
+
* per activity, the columns its overview page shows the person (due dates, submissions, what needs
|
|
43
|
+
* grading, their own grade…). Without a type, every type in the course's state: "resource" first,
|
|
44
|
+
* which brings pages, books, files, folders and links at once, then each type still missing.
|
|
45
|
+
*/
|
|
46
|
+
export async function activitiesOverview(context, siteUrl, courseId, type) {
|
|
47
|
+
let types = type ? [type] : [];
|
|
48
|
+
let cmsByType = new Map();
|
|
49
|
+
if (!type) {
|
|
50
|
+
const state = await courseState(context, siteUrl, courseId);
|
|
51
|
+
cmsByType = new Map();
|
|
52
|
+
for (const cm of state.cm)
|
|
53
|
+
cmsByType.set(cm.module, [...(cmsByType.get(cm.module) ?? []), Number(cm.id)]);
|
|
54
|
+
types = ["resource", ...cmsByType.keys()];
|
|
55
|
+
}
|
|
56
|
+
const activities = [];
|
|
57
|
+
const seen = new Set();
|
|
58
|
+
for (const modname of types) {
|
|
59
|
+
// Every activity of this type came with an earlier answer ("resource").
|
|
60
|
+
if (!type && modname !== "resource" && (cmsByType.get(modname) ?? []).every((id) => seen.has(id)))
|
|
61
|
+
continue;
|
|
62
|
+
let answer;
|
|
63
|
+
try {
|
|
64
|
+
answer = await callService(context, siteUrl, "core_courseformat_get_overview_information", { courseid: courseId, modname });
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
// A type without an overview (labels, some plugins) is left out when listing them all.
|
|
68
|
+
if (type)
|
|
69
|
+
throw error;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
for (const activity of answer.activities) {
|
|
73
|
+
if (seen.has(activity.cmid))
|
|
74
|
+
continue;
|
|
75
|
+
seen.add(activity.cmid);
|
|
76
|
+
const name = activity.items.find((i) => i.key === "name");
|
|
77
|
+
const nameData = JSON.parse(name?.contentjson ?? "{}") ?? {};
|
|
78
|
+
activities.push({
|
|
79
|
+
id: activity.cmid,
|
|
80
|
+
name: decodeEntities(activity.name),
|
|
81
|
+
type: activity.modname,
|
|
82
|
+
...(activity.url ? { url: activity.url } : {}),
|
|
83
|
+
visible: !nameData.hidden,
|
|
84
|
+
...(nameData.available === false ? { available: false } : {}),
|
|
85
|
+
fields: activity.items.filter((i) => i.key !== "name").map(overviewField),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return activities;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* What the person has to do by a date (calendar action events: submit, attempt, grade…), from the
|
|
93
|
+
* last `pastDays` to `days` ahead, in one course or all. Teachers' grading counts come from the
|
|
94
|
+
* activities overview instead: Moodle gives them few action events.
|
|
95
|
+
*/
|
|
96
|
+
export async function deadlines(context, siteUrl, { courseId, days = 30, pastDays = 7 }) {
|
|
97
|
+
const now = Math.floor(Date.now() / 1000);
|
|
98
|
+
const window = { timesortfrom: now - pastDays * 86400, timesortto: now + days * 86400, limitnum: 50 };
|
|
99
|
+
const { events } = courseId
|
|
100
|
+
? await callService(context, siteUrl, "core_calendar_get_action_events_by_course", { courseid: courseId, ...window })
|
|
101
|
+
: await callService(context, siteUrl, "core_calendar_get_action_events_by_timesort", window);
|
|
102
|
+
return events.map((event) => {
|
|
103
|
+
const activityId = event.url ? Number(new URL(event.url).searchParams.get("id")) || undefined : undefined;
|
|
104
|
+
return {
|
|
105
|
+
...(activityId ? { activityId } : {}),
|
|
106
|
+
activity: decodeEntities(event.activityname ?? event.name),
|
|
107
|
+
...(event.modulename ? { type: event.modulename } : {}),
|
|
108
|
+
event: decodeEntities(event.name),
|
|
109
|
+
kind: event.eventtype,
|
|
110
|
+
date: new Date(event.timesort * 1000).toISOString(),
|
|
111
|
+
overdue: event.overdue === true,
|
|
112
|
+
...(event.course ? { course: { id: event.course.id, ...(event.course.shortname ? { shortname: decodeEntities(event.course.shortname) } : {}) } } : {}),
|
|
113
|
+
...(event.action ? { action: { name: decodeEntities(event.action.name), actionable: event.action.actionable !== false, ...(event.action.itemcount ? { count: event.action.itemcount } : {}), ...(event.action.url ? { url: event.action.url } : {}) } } : {}),
|
|
114
|
+
};
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/** What changed in a course's activities since a date, with their names from the course's state. */
|
|
118
|
+
export async function courseUpdates(context, siteUrl, courseId, since) {
|
|
119
|
+
// One after the other: the first call may have to open the kept service tab.
|
|
120
|
+
const { instances } = await callService(context, siteUrl, "core_course_get_updates_since", {
|
|
121
|
+
courseid: courseId,
|
|
122
|
+
since: Math.floor(since.getTime() / 1000),
|
|
123
|
+
});
|
|
124
|
+
const state = await courseState(context, siteUrl, courseId);
|
|
125
|
+
const byId = new Map(state.cm.map((cm) => [Number(cm.id), cm]));
|
|
126
|
+
return instances
|
|
127
|
+
.filter((instance) => instance.contextlevel === "module")
|
|
128
|
+
.map((instance) => {
|
|
129
|
+
const cm = byId.get(instance.id);
|
|
130
|
+
return {
|
|
131
|
+
id: instance.id,
|
|
132
|
+
...(cm ? { name: decodeEntities(cm.name), type: cm.module } : {}),
|
|
133
|
+
changes: instance.updates.map((u) => ({ what: u.name, ...(u.timeupdated ? { time: new Date(u.timeupdated * 1000).toISOString() } : {}), ...(u.itemids ? { items: u.itemids.length } : {}) })),
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=overview.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"overview.js","sourceRoot":"","sources":["../../src/moodle/overview.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** How long a person would take to read `text`, in seconds. */
|
|
2
|
+
export declare function readingSeconds(text: string): number;
|
|
3
|
+
/** Waits `seconds`, calling `onProgress` every few seconds so clients keep waiting. */
|
|
4
|
+
export declare function pause(seconds: number, onProgress?: (elapsedSeconds: number) => void): Promise<void>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Reading at a person's pace (ADR-005): about 240 words a minute, bounded per page.
|
|
2
|
+
const WORDS_PER_SECOND = 4;
|
|
3
|
+
const MIN_SECONDS = 3;
|
|
4
|
+
const MAX_SECONDS = 90;
|
|
5
|
+
const PROGRESS_EVERY_MS = 5000;
|
|
6
|
+
/** How long a person would take to read `text`, in seconds. */
|
|
7
|
+
export function readingSeconds(text) {
|
|
8
|
+
const words = text.split(/\s+/).filter(Boolean).length;
|
|
9
|
+
return Math.min(MAX_SECONDS, Math.max(MIN_SECONDS, Math.round(words / WORDS_PER_SECOND)));
|
|
10
|
+
}
|
|
11
|
+
/** Waits `seconds`, calling `onProgress` every few seconds so clients keep waiting. */
|
|
12
|
+
export async function pause(seconds, onProgress) {
|
|
13
|
+
const end = Date.now() + seconds * 1000;
|
|
14
|
+
while (Date.now() < end) {
|
|
15
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(PROGRESS_EVERY_MS, end - Date.now())));
|
|
16
|
+
onProgress?.(Math.round(seconds - (end - Date.now()) / 1000));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=pace.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pace.js","sourceRoot":"","sources":["../../src/moodle/pace.ts"],"names":[],"mappings":"AAAA,oFAAoF;AACpF,MAAM,gBAAgB,GAAG,CAAC,CAAC;AAC3B,MAAM,WAAW,GAAG,CAAC,CAAC;AACtB,MAAM,WAAW,GAAG,EAAE,CAAC;AACvB,MAAM,iBAAiB,GAAG,IAAI,CAAC;AAE/B,+DAA+D;AAC/D,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC;IACvD,OAAO,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,gBAAgB,CAAC,CAAC,CAAC,CAAC;AAC5F,CAAC;AAED,uFAAuF;AACvF,MAAM,CAAC,KAAK,UAAU,KAAK,CAAC,OAAe,EAAE,UAA6C;IACxF,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,GAAG,IAAI,CAAC;IACxC,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,GAAG,EAAE,CAAC;QACxB,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,iBAAiB,EAAE,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC;QACnG,UAAU,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;IAChE,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { BrowserContext, Page } from "playwright-core";
|
|
2
|
+
export interface Participant {
|
|
3
|
+
userId: number;
|
|
4
|
+
fullName: string;
|
|
5
|
+
roles: {
|
|
6
|
+
id: number;
|
|
7
|
+
name: string;
|
|
8
|
+
}[];
|
|
9
|
+
groups: {
|
|
10
|
+
id: number;
|
|
11
|
+
name: string;
|
|
12
|
+
}[];
|
|
13
|
+
}
|
|
14
|
+
export interface CourseGroup {
|
|
15
|
+
id: number;
|
|
16
|
+
name: string;
|
|
17
|
+
/** The members' user ids. */
|
|
18
|
+
members: number[];
|
|
19
|
+
}
|
|
20
|
+
/** The person can't see the course's participants. */
|
|
21
|
+
export declare class NoParticipantsError extends Error {
|
|
22
|
+
name: string;
|
|
23
|
+
}
|
|
24
|
+
/** The rows of a participants table fragment: user id and name, read by link and markup, in any language. */
|
|
25
|
+
export declare function readParticipantRows(page: Page): Promise<{
|
|
26
|
+
userId: number;
|
|
27
|
+
fullName: string;
|
|
28
|
+
}[]>;
|
|
29
|
+
/**
|
|
30
|
+
* The roles and groups the participants page filters by in a course (its filters' options), as the
|
|
31
|
+
* person may use them: every group, or only their own in a course with separate groups. "No roles"
|
|
32
|
+
* and "no group" left out.
|
|
33
|
+
*/
|
|
34
|
+
export declare function participantFilters(context: BrowserContext, siteUrl: string, courseId: number): Promise<{
|
|
35
|
+
roles: {
|
|
36
|
+
id: number;
|
|
37
|
+
name: string;
|
|
38
|
+
}[];
|
|
39
|
+
groups: {
|
|
40
|
+
id: number;
|
|
41
|
+
name: string;
|
|
42
|
+
}[];
|
|
43
|
+
}>;
|
|
44
|
+
/** A course's groups the person can see, with their members' user ids. */
|
|
45
|
+
export declare function groups(context: BrowserContext, siteUrl: string, courseId: number): Promise<CourseGroup[]>;
|
|
46
|
+
/**
|
|
47
|
+
* A course's participants with their roles and groups, as the participants page lists them: only
|
|
48
|
+
* those with a role (its id), in a group, or (`inactiveSince`) who haven't accessed the course since
|
|
49
|
+
* that date, when asked. Each role and group is a filter of its own, so ids come in any language.
|
|
50
|
+
*/
|
|
51
|
+
export declare function participants(context: BrowserContext, siteUrl: string, courseId: number, { role, group, inactiveSince }?: {
|
|
52
|
+
role?: number;
|
|
53
|
+
group?: number;
|
|
54
|
+
inactiveSince?: Date;
|
|
55
|
+
}): Promise<Participant[]>;
|