Llegar a lo que el driver no envuelve
Moodle tiene mucho más de lo que cubren las áreas del driver. Esta página muestra un programa que llama a un servicio AJAX de Moodle, lee una página por su estructura y rellena un formulario, todo con la sesión de la persona, y después cómo limitar lo que un programa (o un agente que lo maneje) puede llamar, para programadores.
Usa: Bajar de nivel, Categorías de datos, Cómo funciona, Errores.
El programa
import { createMoodle } from "@falkenslab/moodle-driver";
import { readTexts, setFields, submitForm } from "@falkenslab/moodle-driver/internals";
// Los campos de core_course_get_recent_courses que lee este programa; Moodle envía más.
interface RecentCourse {
id: number;
fullname: string;
viewurl: string;
}
await using moodle = createMoodle({ site: "https://campus.example.org/", profileDir: `${process.env.HOME}/.moodle-profile`, headless: true, downloadBrowser: true });
await moodle.logIn();
// 1. Un servicio AJAX: solo funcionan los que Moodle abre a AJAX ('ajax' => true en su db/services.php).
const recent = await moodle.callService<RecentCourse[]>("core_course_get_recent_courses", { limit: 5 });
for (const course of recent) console.log(`Reciente: ${course.fullname} (${course.id})`);
// 2. Una página leída por su estructura: el id del body nombra la página, las clases nombran las cosas (no el idioma del sitio).
const badges = await moodle.openSitePage("badges/mybadges.php");
try {
const bodyId = await badges.locator("body").getAttribute("id");
if (bodyId !== "page-badges-mybadges") throw new Error(`No es la página de insignias: ${bodyId}`);
// Estos selectores son los de la lista de insignias de Moodle: compruébalos con la versión de tu sitio.
const names = await readTexts(badges, ".badges .badge-name");
console.log(`Insignias: ${names.join(", ") || "ninguna todavía"}`);
} finally {
await badges.close(); // Las páginas de openSitePage y openForm son del programa, y le toca cerrarlas.
}
// 3. Un formulario: las preferencias de foro de la persona, por los nombres de los campos en el formulario de Moodle.
const form = await moodle.openForm("user/forum.php");
try {
await setFields(form, { maildigest: "1", autosubscribe: "0" }); // Resumen diario; sin suscripción automática.
await submitForm(form, "submitbutton"); // Lanza FormError con los mensajes de Moodle si lo rechaza.
console.log("Preferencias de foro guardadas.");
} finally {
await form.close();
}
Paso a paso
moodle.callService<T>(methodname, args)hace un POST alib/ajax/service.phpde Moodle con la sesión de la persona y susesskey, como hacen las propias páginas de Moodle.Tes lo que el programa espera de vuelta: el driver no lo comprueba, así que declara solo los campos que lees.moodle.openSitePage(path)abre una página del sitio en una pestaña nueva, como unPagede Playwright, y lanzaNotLoggedInErrorsi Moodle responde con su página de acceso. El programa la lee por su estructura: el id del body (page-badges-mybadges) dice qué página es, y las clases encuentran lo que hay en ella, así que el programa funciona sea cual sea el idioma del sitio.readTextsde/internalsda el texto de los elementos conservando párrafos y listas.moodle.openForm(path)abre una página con un formulario de Moodle y espera a sus editores y a sus dependencias (campos que habilitan otros).setFieldspone los campos por nombre, de uno en uno para que entre medias se ejecuten los manejadores de Moodle;submitFormpulsa el botón con ese nombre y lee los mensajes de validación de Moodle.- Cada página se cierra en un
finally: a diferencia de las operaciones del driver, estas dan una página al programa, y es del programa. await usingcierra el navegador al final, guardando la sesión.
Lo que imprime
Un ejemplo:
Reciente: Programación 1 (3)
Reciente: Bases de datos (7)
Insignias: Primera entrega, Habitual del foro
Preferencias de foro guardadas.
Restringir lo que se puede llamar
Un programa que deja que otro elija qué llamar (un asistente, un lenguaje de scripts, un plugin) puede permitir algunos tipos de datos y rechazar el resto. DATA_CATEGORIES da las categorías de datos de cada función, y MOODLE_METHODS los nombres de los métodos del objeto que son funciones del driver.
import { createMoodle, DATA_CATEGORIES, MOODLE_METHODS, MoodleError, type DataCategory, type Moodle } from "@falkenslab/moodle-driver";
// Este programa puede leer y construir contenido del curso y los datos propios de la persona, nunca los de otros.
const ALLOWED = new Set<DataCategory>(["none", "own", "course-content", "others-aggregate"]);
/** Por qué no se puede llamar a un método, o undefined cuando sí se puede. */
function refusal(method: string): string | undefined {
if (!MOODLE_METHODS.includes(method)) return `${method} no es uno de los métodos del driver`;
const categories = DATA_CATEGORIES[method];
if (!categories) return `${method} no tiene categorías de datos`; // Sin clasificar: se rechaza, no se adivina.
const denied = categories.filter((category) => !ALLOWED.has(category));
return denied.length > 0 ? `${method} toca ${denied.join(", ")}` : undefined;
}
/** Llama a un método del objeto por su nombre, solo si todas sus categorías están permitidas. */
export async function callAllowed(moodle: Moodle, method: string, args: unknown[]): Promise<unknown> {
const reason = refusal(method);
if (reason) throw new MoodleError(`No permitido: ${reason}.`, "invalid-input");
const fn = (moodle as unknown as Record<string, (...rest: unknown[]) => Promise<unknown>>)[method]!;
return fn.apply(moodle, args);
}
// Por ejemplo, con un nombre y unos argumentos que llegaron de otra parte:
await using moodle = createMoodle({ site: "https://campus.example.org/", profileDir: `${process.env.HOME}/.moodle-profile`, headless: true, downloadBrowser: true });
await moodle.logIn();
console.log(await callAllowed(moodle, "courseStructure", [3])); // Permitido: course-content, own.
await callAllowed(moodle, "assignmentSubmissions", [35]).catch((error: Error) => console.error(error.message)); // Rechazado: others-personal.
- Los miembros propios del objeto (
callService,tab,logIn…) no están enMOODLE_METHODS, así quecallAllowedlos rechaza; sus categorías están enDATA_CATEGORIEScomoMoodle.callServicey así sucesivamente, si quieres permitir alguno. callService,openSitePage,openForm,setFieldsy el resto de/internalssonany: llegan a cualquier cosa a la que se les apunte, así que una lista de permitidos sinanylos deja fuera.- Una función con varias categorías solo se permite cuando lo están todas:
gradeSubmission(others-personal,others-write) necesita las dos.
Variaciones
- Otro servicio:
core_calendar_get_calendar_upcoming_view,core_message_get_unread_conversations_count,core_course_get_enrolled_users_by_cmid… La documentación de la API de Moodle, o la pestaña de red del navegador en la página que lo usa, da el nombre y los argumentos. - Una página que la persona debe ver:
moodle.tab()da la pestaña de trabajo, que la persona ve cuando el navegador se muestra (setVisible(true)); déjala abierta, el driver la reutiliza. - Sin el objeto: cada primitiva es una función de
/internalsque recibe primero el contexto y el sitio,callService(await moodle.context(), moodle.site, ...). - Por actividad:
moduleCategories(module)dice qué toca leer una actividad de ese módulo (una página solo contiene contenido del curso; un foro, también mensajes de otros).
Cuando algo sale mal
MoodleServiceErrorconservicenotavailable: el servicio no está abierto a AJAX, aunque la app móvil pueda llamarlo. Coninvalidparameter: los argumentos no encajan con los del servicio;nopermissionsorequireloginerror: la persona no puede.NotLoggedInError: no hay sesión, desde cualquiera de ellos. Vuelve a iniciar sesión y reintenta; consulta Errores y sesiones caducadas.FormError: Moodle rechazó el formulario (sus mensajes por campo), un campo no está en él, o está desactivado hasta que otro lo habilite: pon primero el campo que lo habilita, en el mismosetFields.- Un selector que no encuentra nada: la página cambió con la versión o el tema de Moodle. Cuando puedas, lee estructura que Moodle mantiene (ids del body, atributos
data-, ids) antes que clases, ymoodleErrorMessage(page)te dice cuándo Moodle respondió en cambio con una página de error.
Datos que toca
Los servicios, las páginas y los formularios están en la categoría de datos any: este programa lee los cursos y las insignias de la propia persona y cambia sus propias preferencias, pero las mismas llamadas podrían llegar a cualquier cosa a la que llegue la persona. Por eso la lista de permitidos rechaza any salvo que un programa diga lo contrario.