@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,199 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { openSitePage } from "./session.js";
|
|
4
|
+
/** Moodle refused the form, or a field to set isn't in it. */
|
|
5
|
+
export class FormError extends Error {
|
|
6
|
+
fields;
|
|
7
|
+
name = "FormError";
|
|
8
|
+
constructor(message, fields) {
|
|
9
|
+
super(`${message} ${fields.map((f) => `${f.field}: ${f.message}`).join("; ")}`);
|
|
10
|
+
this.fields = fields;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** Opens a Moodle form page and waits for its editors to be ready. */
|
|
14
|
+
export async function openForm(context, siteUrl, pathAndQuery) {
|
|
15
|
+
const page = await openSitePage(context, siteUrl, pathAndQuery);
|
|
16
|
+
try {
|
|
17
|
+
if ((await page.locator("form.mform").count()) === 0)
|
|
18
|
+
throw new FormError("This page has no Moodle form.", [{ field: "page", message: pathAndQuery }]);
|
|
19
|
+
await page.waitForLoadState("load");
|
|
20
|
+
// TinyMCE starts after load; its content must be set through it, or the save takes the old one.
|
|
21
|
+
await page
|
|
22
|
+
.waitForFunction(() => {
|
|
23
|
+
const tiny = globalThis.tinymce;
|
|
24
|
+
return !tiny || tiny.get().every((editor) => editor.initialized);
|
|
25
|
+
}, undefined, { timeout: 15_000 })
|
|
26
|
+
.catch(() => undefined);
|
|
27
|
+
// Moodle enables and disables fields as others change (disabledIf) once its form dependencies
|
|
28
|
+
// start, after load: a field set before then stays disabled (confirmed empirically, 5.2).
|
|
29
|
+
const dependent = await page.evaluate(() => globalThis.document.documentElement.innerHTML.includes("M.form.initFormDependencies"));
|
|
30
|
+
if (dependent) {
|
|
31
|
+
await page
|
|
32
|
+
.waitForFunction(() => {
|
|
33
|
+
const g = globalThis;
|
|
34
|
+
const id = g.document.querySelector("form.mform")?.id ?? "";
|
|
35
|
+
return Boolean(g.M?.form?.dependencyManagers?.[id]);
|
|
36
|
+
}, undefined, { timeout: 15_000 })
|
|
37
|
+
.catch(() => undefined);
|
|
38
|
+
}
|
|
39
|
+
return page;
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
await page.close().catch(() => undefined);
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** A hidden field's value in the form (a draft area id, the course id…). */
|
|
47
|
+
export function formValue(page, name) {
|
|
48
|
+
return page.evaluate((name) => {
|
|
49
|
+
const field = globalThis.document.querySelector(`form.mform [name="${name}"]`);
|
|
50
|
+
return field?.value;
|
|
51
|
+
}, name);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Sets fields by their form name: text inputs and textareas (through TinyMCE when it edits them),
|
|
55
|
+
* selects (several values for a multiple one), checkboxes and radios, in the order given (a field can
|
|
56
|
+
* enable the next one). Throws `FormError` naming the fields the form doesn't have, and those it has
|
|
57
|
+
* disabled: Moodle wouldn't send them, and the change would be lost unnoticed.
|
|
58
|
+
*/
|
|
59
|
+
export async function setFields(page, values, formSelector = "form.mform") {
|
|
60
|
+
const missing = [];
|
|
61
|
+
const disabled = [];
|
|
62
|
+
// One field at a time: Moodle enables dependent fields in handlers that run after the change event
|
|
63
|
+
// (choosing whether to rescale enables a maximum grade, confirmed empirically, 5.2).
|
|
64
|
+
for (const [name, value] of Object.entries(values)) {
|
|
65
|
+
const outcome = await setField(page, name, value, formSelector);
|
|
66
|
+
if (outcome === "missing")
|
|
67
|
+
missing.push(name);
|
|
68
|
+
if (outcome === "disabled")
|
|
69
|
+
disabled.push(name);
|
|
70
|
+
}
|
|
71
|
+
if (missing.length > 0) {
|
|
72
|
+
throw new FormError("The form has no such field:", missing.map((field) => ({ field, message: "not in this form; check its name in Moodle's form" })));
|
|
73
|
+
}
|
|
74
|
+
if (disabled.length > 0) {
|
|
75
|
+
throw new FormError("The form has these fields disabled, so Moodle wouldn't save them:", disabled.map((field) => ({ field, message: "disabled; another field enables it (set that one first, in the same call)" })));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function setField(page, name, value, formSelector) {
|
|
79
|
+
return page.evaluate(({ name, value, formSelector }) => {
|
|
80
|
+
const g = globalThis;
|
|
81
|
+
const form = g.document.querySelector(formSelector);
|
|
82
|
+
const all = Array.from(form?.querySelectorAll(`[name="${name}"]`) ?? []);
|
|
83
|
+
// A Moodle checkbox comes with a hidden twin holding its unchecked value.
|
|
84
|
+
const fields = all.length > 1 ? all.filter((field) => field.type !== "hidden") : all;
|
|
85
|
+
if (fields.length === 0)
|
|
86
|
+
return "missing";
|
|
87
|
+
// Checked now, after the fields before it: their change events may have enabled it.
|
|
88
|
+
if (fields.every((field) => field.disabled))
|
|
89
|
+
return "disabled";
|
|
90
|
+
for (const field of fields) {
|
|
91
|
+
if (field.type === "checkbox")
|
|
92
|
+
field.checked = value === true || value === 1 || value === "1";
|
|
93
|
+
else if (field.type === "radio")
|
|
94
|
+
field.checked = field.value === String(value);
|
|
95
|
+
else if (field.multiple && field.options) {
|
|
96
|
+
const wanted = (Array.isArray(value) ? value : [value]).map(String);
|
|
97
|
+
for (const option of Array.from(field.options))
|
|
98
|
+
option.selected = wanted.includes(option.value);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
field.value = String(value);
|
|
102
|
+
const editor = field.id ? g.tinymce?.get(field.id) : null;
|
|
103
|
+
if (editor) {
|
|
104
|
+
editor.setContent(String(value));
|
|
105
|
+
editor.save();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
field.dispatchEvent(new g.Event("change", { bubbles: true }));
|
|
109
|
+
}
|
|
110
|
+
return "set";
|
|
111
|
+
}, { name, value, formSelector });
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Uploads local files to a form's draft area, as the file picker's "Upload a file" does: the
|
|
115
|
+
* upload repository's id comes from the page's file picker settings, the context from `M.cfg`.
|
|
116
|
+
*/
|
|
117
|
+
export async function uploadToDraft(page, draftItemId, filePaths) {
|
|
118
|
+
const files = await Promise.all(filePaths.map(async (filePath) => ({ name: path.basename(filePath), base64: (await readFile(filePath)).toString("base64") })));
|
|
119
|
+
const errors = await page.evaluate(async ({ draftItemId, files }) => {
|
|
120
|
+
const g = globalThis;
|
|
121
|
+
const repo = /"id":"(\d+)","name":"(?:[^"\\]|\\.)*","type":"upload"/.exec(g.document.documentElement.innerHTML)?.[1];
|
|
122
|
+
if (!repo)
|
|
123
|
+
return ["the site has no upload repository enabled"];
|
|
124
|
+
const failed = [];
|
|
125
|
+
for (const file of files) {
|
|
126
|
+
const body = new g.FormData();
|
|
127
|
+
body.append("repo_upload_file", new g.Blob([Uint8Array.from(g.atob(file.base64), (c) => c.charCodeAt(0))]), file.name);
|
|
128
|
+
for (const [key, value] of Object.entries({ sesskey: g.M.cfg.sesskey, repo_id: repo, itemid: draftItemId, ctx_id: String(g.M.cfg.contextid), savepath: "/", title: file.name, env: "filemanager", author: "" })) {
|
|
129
|
+
body.append(key, value);
|
|
130
|
+
}
|
|
131
|
+
const response = await g.fetch(`${g.M.cfg.wwwroot}/repository/repository_ajax.php?action=upload`, { method: "POST", body, credentials: "same-origin" });
|
|
132
|
+
const answer = await response.json().catch(() => ({ error: "the answer isn't JSON" }));
|
|
133
|
+
if (answer.error)
|
|
134
|
+
failed.push(`${file.name}: ${answer.error}`);
|
|
135
|
+
}
|
|
136
|
+
return failed;
|
|
137
|
+
}, { draftItemId, files });
|
|
138
|
+
if (errors.length > 0)
|
|
139
|
+
throw new FormError("Uploading failed:", errors.map((message) => ({ field: "files", message })));
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Submits the form with one of its buttons and waits for the next page. Throws `FormError` with
|
|
143
|
+
* Moodle's messages by field (`#id_error_<field>`) when the form comes back with errors.
|
|
144
|
+
*/
|
|
145
|
+
export async function submitForm(page, button) {
|
|
146
|
+
// The navigation's timeout, not the click's: saving can take Moodle long (regrading after a new
|
|
147
|
+
// maximum grade took over 30 seconds, confirmed empirically on the sandbox, 5.2).
|
|
148
|
+
await Promise.all([page.waitForNavigation({ waitUntil: "domcontentloaded" }), page.click(`form.mform [name="${button}"]`, { noWaitAfter: true })]);
|
|
149
|
+
const errors = await page.evaluate(() => {
|
|
150
|
+
const doc = globalThis.document;
|
|
151
|
+
return Array.from(doc.querySelectorAll('form.mform [id^="id_error_"]'))
|
|
152
|
+
.map((element) => ({ field: element.id.slice("id_error_".length), message: (element.textContent ?? "").replace(/\s+/g, " ").trim() }))
|
|
153
|
+
.filter((error) => error.message !== "");
|
|
154
|
+
});
|
|
155
|
+
if (errors.length > 0)
|
|
156
|
+
throw new FormError("Moodle refused the form:", errors);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* The values of a Moodle date selector's parts (`name[day]`, `[month]`, `[year]`, `[hour]`,
|
|
160
|
+
* `[minute]`) for `date`, in the person's time zone (`M.cfg.usertimezone`), which is how Moodle
|
|
161
|
+
* reads them. Its "enable" checkbox, when it has one, is the caller's to set first.
|
|
162
|
+
*/
|
|
163
|
+
export async function dateSelectorValues(page, name, date) {
|
|
164
|
+
const timeZone = await page.evaluate(() => globalThis.M?.cfg?.usertimezone ?? "UTC");
|
|
165
|
+
let parts;
|
|
166
|
+
try {
|
|
167
|
+
parts = new Intl.DateTimeFormat("en-GB", { timeZone, year: "numeric", month: "numeric", day: "numeric", hour: "numeric", minute: "numeric", hourCycle: "h23" }).formatToParts(date);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
throw new FormError(`The person's time zone in Moodle ("${timeZone}") isn't one the driver knows, so a date can't be set.`, [{ field: name, message: "unknown time zone" }]);
|
|
171
|
+
}
|
|
172
|
+
const part = (type) => Number(parts.find((p) => p.type === type)?.value);
|
|
173
|
+
return { [`${name}[day]`]: part("day"), [`${name}[month]`]: part("month"), [`${name}[year]`]: part("year"), [`${name}[hour]`]: part("hour"), [`${name}[minute]`]: part("minute") };
|
|
174
|
+
}
|
|
175
|
+
/** The date a Moodle date selector holds (`name[day]`… in the person's time zone), or undefined when it's off. */
|
|
176
|
+
export async function dateSelectorDate(page, name) {
|
|
177
|
+
const read = await page.evaluate((name) => {
|
|
178
|
+
const g = globalThis;
|
|
179
|
+
const enabled = g.document.querySelector(`form.mform [name="${name}[enabled]"][type="checkbox"]`);
|
|
180
|
+
if (enabled && !enabled.checked)
|
|
181
|
+
return undefined;
|
|
182
|
+
// Anonymous callbacks only: see the conventions on page.evaluate.
|
|
183
|
+
const [year, month, day, hour, minute] = ["year", "month", "day", "hour", "minute"].map((p) => Number(g.document.querySelector(`form.mform [name="${name}[${p}]"]`)?.value) || 0);
|
|
184
|
+
return { timeZone: g.M?.cfg?.usertimezone ?? "UTC", year: year, month: month, day: day, hour: hour, minute: minute };
|
|
185
|
+
}, name);
|
|
186
|
+
if (!read || !read.year)
|
|
187
|
+
return undefined;
|
|
188
|
+
// The wall time in that zone: start from it as if UTC, then take away the zone's offset at that moment (twice, across a DST change).
|
|
189
|
+
const wall = Date.UTC(read.year, read.month - 1, read.day, read.hour, read.minute);
|
|
190
|
+
let time = wall;
|
|
191
|
+
for (let i = 0; i < 2; i++) {
|
|
192
|
+
const parts = new Intl.DateTimeFormat("en-GB", { timeZone: read.timeZone, year: "numeric", month: "numeric", day: "numeric", hour: "numeric", minute: "numeric", hourCycle: "h23" }).formatToParts(new Date(time));
|
|
193
|
+
const part = (type) => Number(parts.find((p) => p.type === type)?.value);
|
|
194
|
+
const shown = Date.UTC(part("year"), part("month") - 1, part("day"), part("hour"), part("minute"));
|
|
195
|
+
time += wall - shown;
|
|
196
|
+
}
|
|
197
|
+
return new Date(time);
|
|
198
|
+
}
|
|
199
|
+
//# sourceMappingURL=forms.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"forms.js","sourceRoot":"","sources":["../../src/moodle/forms.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { BrowserContext, Page } from "playwright-core";
|
|
2
|
+
/** The id isn't a forum's course module, or the person can't see it. */
|
|
3
|
+
export declare class NotAForumError extends Error {
|
|
4
|
+
name: string;
|
|
5
|
+
}
|
|
6
|
+
/** A discussion as the forum page lists it. */
|
|
7
|
+
export interface DiscussionRow {
|
|
8
|
+
id: number;
|
|
9
|
+
subject: string;
|
|
10
|
+
author: string;
|
|
11
|
+
started?: number;
|
|
12
|
+
lastPost: {
|
|
13
|
+
author: string;
|
|
14
|
+
time?: number;
|
|
15
|
+
postId?: number;
|
|
16
|
+
};
|
|
17
|
+
replies: number;
|
|
18
|
+
locked: boolean;
|
|
19
|
+
pinned: boolean;
|
|
20
|
+
/** The forum's instance id, which the forum's services take. */
|
|
21
|
+
forumId?: number;
|
|
22
|
+
}
|
|
23
|
+
/** The discussions on a forum page, read by structure: data attributes, links, timestamps. */
|
|
24
|
+
export declare function readDiscussionRows(page: Page): Promise<DiscussionRow[]>;
|
|
25
|
+
/** Every discussion of a forum (its course module id), through all its pages. */
|
|
26
|
+
export declare function listDiscussions(context: BrowserContext, siteUrl: string, forumCmid: number): Promise<DiscussionRow[]>;
|
|
27
|
+
/** A post as `mod_forum_get_discussion_posts` returns it (Moodle 5.2), the fields read. */
|
|
28
|
+
export interface MoodlePost {
|
|
29
|
+
id: number;
|
|
30
|
+
subject: string;
|
|
31
|
+
message: string;
|
|
32
|
+
author: {
|
|
33
|
+
id: number;
|
|
34
|
+
fullname: string;
|
|
35
|
+
};
|
|
36
|
+
parentid: number | null;
|
|
37
|
+
timecreated: number;
|
|
38
|
+
timemodified?: number;
|
|
39
|
+
isprivatereply?: boolean;
|
|
40
|
+
unread?: boolean | null;
|
|
41
|
+
attachments?: {
|
|
42
|
+
filename: string;
|
|
43
|
+
url?: string;
|
|
44
|
+
fileurl?: string;
|
|
45
|
+
}[];
|
|
46
|
+
}
|
|
47
|
+
export declare function discussionPosts(context: BrowserContext, siteUrl: string, discussionId: number): Promise<{
|
|
48
|
+
posts: MoodlePost[];
|
|
49
|
+
forumid: number;
|
|
50
|
+
courseid: number;
|
|
51
|
+
}>;
|
|
52
|
+
/** Plain text of Moodle-cleaned HTML fragments, parsed in a blank tab. */
|
|
53
|
+
export declare function htmlToTexts(context: BrowserContext, fragments: string[]): Promise<string[]>;
|
|
54
|
+
/** A forum's instance id (what `post.php?forum=` takes), from its page. */
|
|
55
|
+
export declare function forumInstanceId(context: BrowserContext, siteUrl: string, forumCmid: number): Promise<number>;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { MoodleServiceError, callService } from "./ajax.js";
|
|
2
|
+
import { openSitePage } from "./session.js";
|
|
3
|
+
import { readTexts } from "./text.js";
|
|
4
|
+
/** The id isn't a forum's course module, or the person can't see it. */
|
|
5
|
+
export class NotAForumError extends Error {
|
|
6
|
+
name = "NotAForumError";
|
|
7
|
+
}
|
|
8
|
+
// The page size asked for; the forum pages until a page brings no new discussions.
|
|
9
|
+
const PAGE_SIZE = 100;
|
|
10
|
+
const MAX_PAGES = 50;
|
|
11
|
+
/** The discussions on a forum page, read by structure: data attributes, links, timestamps. */
|
|
12
|
+
export function readDiscussionRows(page) {
|
|
13
|
+
return page.evaluate(() => {
|
|
14
|
+
const doc = globalThis.document;
|
|
15
|
+
return Array.from(doc.querySelectorAll('tr[data-region="discussion-list-item"][data-discussionid]')).map((row) => {
|
|
16
|
+
const times = Array.from(row.querySelectorAll("time[data-timestamp]"));
|
|
17
|
+
// A person's cell is their name followed by the time: the name is the text before it.
|
|
18
|
+
// (An anonymous callback, not a named helper: see the conventions on page.evaluate.)
|
|
19
|
+
const [startedBy, lastBy] = [times[0], times[1]].map((time) => {
|
|
20
|
+
const all = (time?.closest("td")?.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
21
|
+
const when = (time?.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
22
|
+
return when && all.endsWith(when) ? all.slice(0, all.length - when.length).trim() : all;
|
|
23
|
+
});
|
|
24
|
+
const lastLink = times[1]?.closest("a");
|
|
25
|
+
const parent = lastLink ? new URL(lastLink.getAttribute("href") ?? "", "http://x/").searchParams.get("parent") : null;
|
|
26
|
+
const repliesCell = Array.from(row.querySelectorAll("td")).find((cell) => /^\d+$/.test((cell.textContent ?? "").trim()) && !cell.querySelector("a"));
|
|
27
|
+
const locked = row.querySelector('[data-region="locked-label"]');
|
|
28
|
+
return {
|
|
29
|
+
id: Number(row.getAttribute("data-discussionid")),
|
|
30
|
+
subject: (row.querySelector('a[href*="discuss.php?d="]')?.textContent ?? "").replace(/\s+/g, " ").trim(),
|
|
31
|
+
author: startedBy ?? "",
|
|
32
|
+
started: times[0] ? Number(times[0].getAttribute("data-timestamp")) : undefined,
|
|
33
|
+
lastPost: { author: lastBy ?? "", time: times[1] ? Number(times[1].getAttribute("data-timestamp")) : undefined, postId: parent ? Number(parent) : undefined },
|
|
34
|
+
replies: Number((repliesCell?.textContent ?? "0").trim()),
|
|
35
|
+
locked: locked !== null && !locked.hasAttribute("hidden"),
|
|
36
|
+
pinned: /\bpinned\b/.test(row.className),
|
|
37
|
+
forumId: Number(row.getAttribute("data-forumid")) || undefined,
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/** Every discussion of a forum (its course module id), through all its pages. */
|
|
43
|
+
export async function listDiscussions(context, siteUrl, forumCmid) {
|
|
44
|
+
const all = new Map();
|
|
45
|
+
for (let p = 0; p < MAX_PAGES; p++) {
|
|
46
|
+
const page = await openSitePage(context, siteUrl, `mod/forum/view.php?id=${forumCmid}&s=${PAGE_SIZE}&p=${p}`);
|
|
47
|
+
try {
|
|
48
|
+
// The body id names the page in any language.
|
|
49
|
+
if ((await page.locator("body#page-mod-forum-view").count()) === 0) {
|
|
50
|
+
throw new NotAForumError(`Activity ${forumCmid} isn't a forum you can view. Take the id of a forum (module "forum") from the course structure.`);
|
|
51
|
+
}
|
|
52
|
+
const rows = await readDiscussionRows(page);
|
|
53
|
+
const before = all.size;
|
|
54
|
+
for (const row of rows)
|
|
55
|
+
all.set(row.id, row);
|
|
56
|
+
if (rows.length < PAGE_SIZE || all.size === before)
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
await page.close().catch(() => undefined);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return Array.from(all.values());
|
|
64
|
+
}
|
|
65
|
+
export async function discussionPosts(context, siteUrl, discussionId) {
|
|
66
|
+
try {
|
|
67
|
+
return await callService(context, siteUrl, "mod_forum_get_discussion_posts", { discussionid: discussionId, sortby: "created", sortdirection: "ASC", includeinlineattachments: false });
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
// A missing discussion fails inside Moodle with a PHP error (confirmed empirically, 5.2).
|
|
71
|
+
if (error instanceof MoodleServiceError && error.errorcode === "generalexceptionmessage") {
|
|
72
|
+
throw new NotAForumError(`Discussion ${discussionId} doesn't exist or you can't see it. Take its id from the forum's discussions. (${error.message})`);
|
|
73
|
+
}
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/** Plain text of Moodle-cleaned HTML fragments, parsed in a blank tab. */
|
|
78
|
+
export async function htmlToTexts(context, fragments) {
|
|
79
|
+
if (fragments.length === 0)
|
|
80
|
+
return [];
|
|
81
|
+
const page = await context.newPage();
|
|
82
|
+
try {
|
|
83
|
+
await page.setContent(fragments.map((html) => `<div class="driver-fragment">${html}</div>`).join(""));
|
|
84
|
+
return await readTexts(page, "body > .driver-fragment");
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
await page.close().catch(() => undefined);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** A forum's instance id (what `post.php?forum=` takes), from its page. */
|
|
91
|
+
export async function forumInstanceId(context, siteUrl, forumCmid) {
|
|
92
|
+
const page = await openSitePage(context, siteUrl, `mod/forum/view.php?id=${forumCmid}`);
|
|
93
|
+
try {
|
|
94
|
+
if ((await page.locator("body#page-mod-forum-view").count()) === 0) {
|
|
95
|
+
throw new NotAForumError(`Activity ${forumCmid} isn't a forum you can view. Take the id of a forum (module "forum") from the course structure.`);
|
|
96
|
+
}
|
|
97
|
+
// The page's quick "new discussion" form carries it (also on an empty forum); discussion rows too.
|
|
98
|
+
const id = await page.evaluate(() => {
|
|
99
|
+
const doc = globalThis.document;
|
|
100
|
+
const fromForm = Number(doc.querySelector('form[action*="post.php"] [name="forum"]')?.value);
|
|
101
|
+
if (fromForm > 0)
|
|
102
|
+
return fromForm;
|
|
103
|
+
return Array.from(doc.querySelectorAll("[data-forumid]"))
|
|
104
|
+
.map((element) => Number(element.getAttribute("data-forumid")))
|
|
105
|
+
.find((value) => value > 0);
|
|
106
|
+
});
|
|
107
|
+
if (!id)
|
|
108
|
+
throw new NotAForumError(`Forum ${forumCmid}'s page doesn't say its id; you may not be allowed to post there.`);
|
|
109
|
+
return id;
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
await page.close().catch(() => undefined);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=forum.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"forum.js","sourceRoot":"","sources":["../../src/moodle/forum.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { BrowserContext } from "playwright-core";
|
|
2
|
+
import { type FieldValue } from "./forms.js";
|
|
3
|
+
export interface ForumDiscussion {
|
|
4
|
+
id: number;
|
|
5
|
+
subject: string;
|
|
6
|
+
author: string;
|
|
7
|
+
started?: string;
|
|
8
|
+
lastPost: {
|
|
9
|
+
author: string;
|
|
10
|
+
time?: string;
|
|
11
|
+
postId?: number;
|
|
12
|
+
};
|
|
13
|
+
replies: number;
|
|
14
|
+
locked: boolean;
|
|
15
|
+
pinned: boolean;
|
|
16
|
+
}
|
|
17
|
+
/** A forum's discussions, as its page lists them: who started each, replies, last post. */
|
|
18
|
+
export declare function forumDiscussions(context: BrowserContext, siteUrl: string, forumCmid: number): Promise<ForumDiscussion[]>;
|
|
19
|
+
export interface ForumPost {
|
|
20
|
+
id: number;
|
|
21
|
+
parentId?: number;
|
|
22
|
+
subject: string;
|
|
23
|
+
author: {
|
|
24
|
+
id: number;
|
|
25
|
+
fullName: string;
|
|
26
|
+
};
|
|
27
|
+
created?: string;
|
|
28
|
+
/** Only when edited more than a minute after posting. */
|
|
29
|
+
modified?: string;
|
|
30
|
+
/** The message as plain text, paragraphs and lists kept. */
|
|
31
|
+
text: string;
|
|
32
|
+
attachments: {
|
|
33
|
+
name: string;
|
|
34
|
+
url: string;
|
|
35
|
+
}[];
|
|
36
|
+
privateReply: boolean;
|
|
37
|
+
unread?: boolean;
|
|
38
|
+
}
|
|
39
|
+
/** A discussion's posts, oldest first, with their messages as text. */
|
|
40
|
+
export declare function discussion(context: BrowserContext, siteUrl: string, discussionId: number): Promise<{
|
|
41
|
+
discussion: {
|
|
42
|
+
id: number;
|
|
43
|
+
forumId: number;
|
|
44
|
+
courseId: number;
|
|
45
|
+
};
|
|
46
|
+
posts: ForumPost[];
|
|
47
|
+
}>;
|
|
48
|
+
export interface PostInput {
|
|
49
|
+
/** The message (HTML). */
|
|
50
|
+
message: string;
|
|
51
|
+
/** Absolute local paths to attach. */
|
|
52
|
+
files?: string[];
|
|
53
|
+
/** Subscribe to the discussion; Moodle's default when missing. */
|
|
54
|
+
subscribe?: boolean;
|
|
55
|
+
/** Other post form fields by name ({"pinned": true} for a teacher). */
|
|
56
|
+
fields?: Record<string, FieldValue>;
|
|
57
|
+
}
|
|
58
|
+
/** Replies to a post; returns the new post's id and its discussion's. */
|
|
59
|
+
export declare function replyToPost(context: BrowserContext, siteUrl: string, postId: number, input: PostInput & {
|
|
60
|
+
subject?: string;
|
|
61
|
+
}): Promise<{
|
|
62
|
+
changed: string;
|
|
63
|
+
postId: number | undefined;
|
|
64
|
+
discussionId: number | undefined;
|
|
65
|
+
}>;
|
|
66
|
+
/** Starts a discussion in a forum; returns its id. */
|
|
67
|
+
export declare function startDiscussion(context: BrowserContext, siteUrl: string, forumCmid: number, input: PostInput & {
|
|
68
|
+
subject: string;
|
|
69
|
+
}): Promise<{
|
|
70
|
+
changed: string;
|
|
71
|
+
discussionId: number | undefined;
|
|
72
|
+
}>;
|
|
73
|
+
/**
|
|
74
|
+
* Pins or unpins, locks or unlocks a discussion, as the discussion list's menu does; what's already
|
|
75
|
+
* so is left alone. Returns the discussion as the forum lists it afterwards.
|
|
76
|
+
*/
|
|
77
|
+
export declare function setDiscussionState(context: BrowserContext, siteUrl: string, forumCmid: number, discussionId: number, { pinned, locked }: {
|
|
78
|
+
pinned?: boolean;
|
|
79
|
+
locked?: boolean;
|
|
80
|
+
}): Promise<{
|
|
81
|
+
changed: string;
|
|
82
|
+
discussion?: ForumDiscussion;
|
|
83
|
+
}>;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { callService } from "./ajax.js";
|
|
2
|
+
import { isoDate } from "./courses.js";
|
|
3
|
+
import { checkLocalFiles } from "./files.js";
|
|
4
|
+
import { formValue, openForm, setFields, submitForm, uploadToDraft } from "./forms.js";
|
|
5
|
+
import { NotAForumError, discussionPosts, forumInstanceId, htmlToTexts, listDiscussions } from "./forum.js";
|
|
6
|
+
import { decodeEntities } from "./text.js";
|
|
7
|
+
/** A forum's discussions, as its page lists them: who started each, replies, last post. */
|
|
8
|
+
export async function forumDiscussions(context, siteUrl, forumCmid) {
|
|
9
|
+
const rows = await listDiscussions(context, siteUrl, forumCmid);
|
|
10
|
+
return rows.map((row) => ({
|
|
11
|
+
id: row.id,
|
|
12
|
+
subject: row.subject,
|
|
13
|
+
author: row.author,
|
|
14
|
+
started: isoDate(row.started),
|
|
15
|
+
lastPost: { author: row.lastPost.author, time: isoDate(row.lastPost.time), postId: row.lastPost.postId },
|
|
16
|
+
replies: row.replies,
|
|
17
|
+
locked: row.locked,
|
|
18
|
+
pinned: row.pinned,
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
/** A discussion's posts, oldest first, with their messages as text. */
|
|
22
|
+
export async function discussion(context, siteUrl, discussionId) {
|
|
23
|
+
const { posts, forumid, courseid } = await discussionPosts(context, siteUrl, discussionId);
|
|
24
|
+
const texts = await htmlToTexts(context, posts.map((post) => post.message));
|
|
25
|
+
return {
|
|
26
|
+
discussion: { id: discussionId, forumId: forumid, courseId: courseid },
|
|
27
|
+
posts: posts.map((post, i) => ({
|
|
28
|
+
id: post.id,
|
|
29
|
+
parentId: post.parentid ?? undefined,
|
|
30
|
+
subject: decodeEntities(post.subject),
|
|
31
|
+
author: { id: post.author.id, fullName: post.author.fullname },
|
|
32
|
+
created: isoDate(post.timecreated),
|
|
33
|
+
modified: post.timemodified && post.timemodified - post.timecreated > 60 ? isoDate(post.timemodified) : undefined,
|
|
34
|
+
text: texts[i] ?? "",
|
|
35
|
+
attachments: (post.attachments ?? []).map((file) => ({ name: file.filename, url: file.url ?? file.fileurl ?? "" })),
|
|
36
|
+
privateReply: post.isprivatereply ?? false,
|
|
37
|
+
unread: post.unread ?? undefined,
|
|
38
|
+
})),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** Fills and submits the post form at `formPath`; returns the URL Moodle lands on. */
|
|
42
|
+
async function post(context, siteUrl, formPath, input) {
|
|
43
|
+
// Moodle takes "<p></p>" as a message and posts it blank (confirmed empirically, 5.2).
|
|
44
|
+
const text = input.message.replace(/<[^>]*>/g, "").replace(/ /g, " ").trim();
|
|
45
|
+
if (text === "" && !/<(img|video|audio|iframe)\b/i.test(input.message))
|
|
46
|
+
throw new Error("The message is empty: write something before posting.");
|
|
47
|
+
if (input.files)
|
|
48
|
+
await checkLocalFiles(input.files);
|
|
49
|
+
const page = await openForm(context, siteUrl, formPath);
|
|
50
|
+
try {
|
|
51
|
+
if (input.files && input.files.length > 0) {
|
|
52
|
+
const draft = await formValue(page, "attachments");
|
|
53
|
+
if (!draft)
|
|
54
|
+
throw new Error("This forum takes no attachments.");
|
|
55
|
+
await uploadToDraft(page, draft, input.files);
|
|
56
|
+
}
|
|
57
|
+
const values = { "message[text]": input.message, ...input.fields };
|
|
58
|
+
if (input.subject !== undefined)
|
|
59
|
+
values.subject = input.subject;
|
|
60
|
+
if (input.subscribe !== undefined)
|
|
61
|
+
values.discussionsubscribe = input.subscribe;
|
|
62
|
+
await setFields(page, values);
|
|
63
|
+
await submitForm(page, "submitbutton");
|
|
64
|
+
return page.url();
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
await page.close().catch(() => undefined);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** Replies to a post; returns the new post's id and its discussion's. */
|
|
71
|
+
export async function replyToPost(context, siteUrl, postId, input) {
|
|
72
|
+
const landed = new URL(await post(context, siteUrl, `mod/forum/post.php?reply=${postId}`, input));
|
|
73
|
+
// Moodle lands on the discussion, at the new post: discuss.php?d=<discussion>#p<post>.
|
|
74
|
+
const newPost = /^#p(\d+)$/.exec(landed.hash)?.[1];
|
|
75
|
+
const discussionId = landed.searchParams.get("d");
|
|
76
|
+
return {
|
|
77
|
+
changed: `Replied to post ${postId}${newPost ? ` with post ${newPost}` : ""}.`,
|
|
78
|
+
postId: newPost ? Number(newPost) : undefined,
|
|
79
|
+
discussionId: discussionId ? Number(discussionId) : undefined,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/** Starts a discussion in a forum; returns its id. */
|
|
83
|
+
export async function startDiscussion(context, siteUrl, forumCmid, input) {
|
|
84
|
+
const instance = await forumInstanceId(context, siteUrl, forumCmid);
|
|
85
|
+
const before = new Set((await listDiscussions(context, siteUrl, forumCmid)).map((d) => d.id));
|
|
86
|
+
await post(context, siteUrl, `mod/forum/post.php?forum=${instance}`, input);
|
|
87
|
+
const created = (await listDiscussions(context, siteUrl, forumCmid)).find((d) => !before.has(d.id));
|
|
88
|
+
return { changed: `Started discussion ${created?.id ?? "?"} "${input.subject}" in forum ${forumCmid}.`, discussionId: created?.id };
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Pins or unpins, locks or unlocks a discussion, as the discussion list's menu does; what's already
|
|
92
|
+
* so is left alone. Returns the discussion as the forum lists it afterwards.
|
|
93
|
+
*/
|
|
94
|
+
export async function setDiscussionState(context, siteUrl, forumCmid, discussionId, { pinned, locked }) {
|
|
95
|
+
const find = async () => (await forumDiscussions(context, siteUrl, forumCmid)).find((d) => d.id === discussionId);
|
|
96
|
+
const row = (await listDiscussions(context, siteUrl, forumCmid)).find((d) => d.id === discussionId);
|
|
97
|
+
if (!row)
|
|
98
|
+
throw new NotAForumError(`Forum ${forumCmid} has no discussion ${discussionId}. Take its id from the forum's discussions.`);
|
|
99
|
+
const done = [];
|
|
100
|
+
if (pinned !== undefined && pinned !== row.pinned) {
|
|
101
|
+
await callService(context, siteUrl, "mod_forum_set_pin_state", { discussionid: discussionId, targetstate: pinned ? 1 : 0 });
|
|
102
|
+
done.push(pinned ? "pinned" : "unpinned");
|
|
103
|
+
}
|
|
104
|
+
if (locked !== undefined && locked !== row.locked) {
|
|
105
|
+
// Rows don't always carry the forum's id (confirmed empirically, 5.2): its page does.
|
|
106
|
+
const forumId = row.forumId ?? (await forumInstanceId(context, siteUrl, forumCmid));
|
|
107
|
+
// The service takes the current lock and switches it: 0 locks now, anything else unlocks.
|
|
108
|
+
await callService(context, siteUrl, "mod_forum_set_lock_state", { forumid: forumId, discussionid: discussionId, targetstate: locked ? 0 : 1 });
|
|
109
|
+
done.push(locked ? "locked" : "unlocked");
|
|
110
|
+
}
|
|
111
|
+
return { changed: done.length > 0 ? `Discussion ${discussionId} ${done.join(" and ")}.` : "Nothing to change.", discussion: await find() };
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=forumPosts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"forumPosts.js","sourceRoot":"","sources":["../../src/moodle/forumPosts.ts"],"names":[],"mappings":"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"}
|