@josetra/dutic-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +223 -0
  3. package/dist/cli/format.js +69 -0
  4. package/dist/cli/format.js.map +1 -0
  5. package/dist/cli/index.js +426 -0
  6. package/dist/cli/index.js.map +1 -0
  7. package/dist/cli/setup.js +121 -0
  8. package/dist/cli/setup.js.map +1 -0
  9. package/dist/cli/ui.js +111 -0
  10. package/dist/cli/ui.js.map +1 -0
  11. package/dist/core/auth.js +47 -0
  12. package/dist/core/auth.js.map +1 -0
  13. package/dist/core/browser.js +56 -0
  14. package/dist/core/browser.js.map +1 -0
  15. package/dist/core/config.js +34 -0
  16. package/dist/core/config.js.map +1 -0
  17. package/dist/core/dates.js +43 -0
  18. package/dist/core/dates.js.map +1 -0
  19. package/dist/core/errors.js +25 -0
  20. package/dist/core/errors.js.map +1 -0
  21. package/dist/core/http.js +43 -0
  22. package/dist/core/http.js.map +1 -0
  23. package/dist/core/login.js +193 -0
  24. package/dist/core/login.js.map +1 -0
  25. package/dist/core/models.js +79 -0
  26. package/dist/core/models.js.map +1 -0
  27. package/dist/core/moodleClient.js +94 -0
  28. package/dist/core/moodleClient.js.map +1 -0
  29. package/dist/core/session.js +64 -0
  30. package/dist/core/session.js.map +1 -0
  31. package/dist/domain/assign.js +112 -0
  32. package/dist/domain/assign.js.map +1 -0
  33. package/dist/domain/concurrency.js +17 -0
  34. package/dist/domain/concurrency.js.map +1 -0
  35. package/dist/domain/courses.js +70 -0
  36. package/dist/domain/courses.js.map +1 -0
  37. package/dist/domain/documents.js +209 -0
  38. package/dist/domain/documents.js.map +1 -0
  39. package/dist/domain/grades.js +99 -0
  40. package/dist/domain/grades.js.map +1 -0
  41. package/dist/domain/people.js +161 -0
  42. package/dist/domain/people.js.map +1 -0
  43. package/dist/domain/resources.js +242 -0
  44. package/dist/domain/resources.js.map +1 -0
  45. package/dist/domain/tasks.js +191 -0
  46. package/dist/domain/tasks.js.map +1 -0
  47. package/dist/mcp/server.js +261 -0
  48. package/dist/mcp/server.js.map +1 -0
  49. package/package.json +67 -0
  50. package/skills/dutic/SKILL.md +173 -0
@@ -0,0 +1,79 @@
1
+ import { z } from "zod";
2
+ /** Un curso en el que el usuario está matriculado. */
3
+ export const CourseSchema = z.object({
4
+ id: z.number(),
5
+ fullname: z.string(),
6
+ shortname: z.string(),
7
+ /** Nombres de contacto/profesor que Moodle asocia al curso (si los expone). */
8
+ contacts: z.array(z.string()).default([]),
9
+ });
10
+ /** Estado de entrega de una tarea, cuando se puede determinar. */
11
+ export const SubmissionStatusSchema = z.enum([
12
+ "submitted",
13
+ "not-submitted",
14
+ "graded",
15
+ "unknown",
16
+ ]);
17
+ /**
18
+ * Una tarea/assignment. `source` indica de dónde salió:
19
+ * - "calendar": apareció como evento de calendario (lo que ve la app Flutter).
20
+ * - "course-scan": se encontró barriendo el contenido del curso.
21
+ * `hidden` es true cuando NO estaba en el calendario — el caso que causa entregas perdidas.
22
+ */
23
+ export const TaskSchema = z.object({
24
+ id: z.number(),
25
+ name: z.string(),
26
+ courseId: z.number(),
27
+ courseName: z.string(),
28
+ /** Fecha de entrega en epoch (segundos). null si no tiene fecha definida. */
29
+ dueDate: z.number().nullable(),
30
+ url: z.string().nullable(),
31
+ description: z.string().nullable().default(null),
32
+ source: z.enum(["calendar", "course-scan"]),
33
+ hidden: z.boolean(),
34
+ submission: SubmissionStatusSchema.default("unknown"),
35
+ /** Nota, si la tarea ya fue calificada (texto tal cual lo muestra Moodle, ej. "16,00 / 20,00"). */
36
+ grade: z.string().nullable().default(null),
37
+ /** Texto de "Tiempo restante" de Moodle, cuando aplica. */
38
+ timeRemaining: z.string().nullable().default(null),
39
+ /** Archivos adjuntos a la consigna (guías, rúbricas) — legibles con read_resource. */
40
+ attachments: z
41
+ .array(z.object({ filename: z.string(), url: z.string() }))
42
+ .default([]),
43
+ /**
44
+ * true si la consigna menciona una fecha distinta (>1 día) a la fecha oficial de cierre.
45
+ * Señal de alerta: el profesor escribió otra fecha en el texto.
46
+ */
47
+ dateConflict: z.boolean().default(false),
48
+ /** Fechas encontradas dentro del texto de la consigna. */
49
+ datesInDescription: z
50
+ .array(z.object({ text: z.string(), epoch: z.number().nullable() }))
51
+ .default([]),
52
+ /** Course module id (cmid) — identificador único del módulo en el curso. */
53
+ cmid: z.number().nullable().default(null),
54
+ });
55
+ /** Un módulo dentro de una sección de curso (assign, quiz, resource, url, forum, ...). */
56
+ export const CourseModuleSchema = z.object({
57
+ cmid: z.number(),
58
+ name: z.string(),
59
+ modname: z.string(),
60
+ url: z.string().nullable(),
61
+ visible: z.boolean().default(true),
62
+ instance: z.number().nullable().default(null),
63
+ });
64
+ export const CourseSectionSchema = z.object({
65
+ id: z.number(),
66
+ name: z.string(),
67
+ modules: z.array(CourseModuleSchema),
68
+ });
69
+ /** Un archivo descargable (recurso, adjunto de carpeta, etc.). */
70
+ export const ResourceFileSchema = z.object({
71
+ filename: z.string(),
72
+ /** URL de vista del módulo (mod/resource/view.php...) o directamente pluginfile.php. */
73
+ fileurl: z.string(),
74
+ moduleName: z.string(),
75
+ modname: z.string(),
76
+ mimetype: z.string().nullable().default(null),
77
+ filesize: z.number().nullable().default(null),
78
+ });
79
+ //# sourceMappingURL=models.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"models.js","sourceRoot":"","sources":["../../src/core/models.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,94 @@
1
+ import { CHROME_USER_AGENT } from "./config.js";
2
+ import { MoodleApiError, NetworkError, SessionExpiredError } from "./errors.js";
3
+ import { unsaAgent } from "./http.js";
4
+ /** Backoff entre reintentos para errores de red transitorios. */
5
+ const RETRY_DELAYS_MS = [800, 1600];
6
+ const REQUEST_TIMEOUT_MS = 20_000;
7
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
8
+ /**
9
+ * Ejecuta una o varias llamadas contra el endpoint AJAX interno de Moodle
10
+ * ({siteUrl}/lib/ajax/service.php?sesskey=...). Reproduce exactamente la petición que hace
11
+ * el navegador: cuerpo batch JSON, cookie MoodleSession, Origin y User-Agent de Chrome.
12
+ *
13
+ * Devuelve el array `data` de cada llamada, en el mismo orden. Lanza:
14
+ * - SessionExpiredError si Moodle responde requireloginerror/servicerequireslogin.
15
+ * - MoodleApiError para otros errores de aplicación (definitivos, sin reintento).
16
+ * - NetworkError si se agotan los reintentos por fallos de red.
17
+ */
18
+ export async function postAjaxBatch(session, calls) {
19
+ const siteUri = new URL(session.siteUrl);
20
+ const url = `${session.siteUrl}/lib/ajax/service.php?sesskey=${encodeURIComponent(session.sesskey)}`;
21
+ const headers = {
22
+ "Content-Type": "application/json",
23
+ Cookie: `MoodleSession=${session.moodleSession}`,
24
+ Origin: `${siteUri.protocol}//${siteUri.host}`,
25
+ "User-Agent": CHROME_USER_AGENT,
26
+ };
27
+ const body = JSON.stringify(calls.map((c, index) => ({ index, methodname: c.methodname, args: c.args })));
28
+ let lastNetworkError = null;
29
+ for (let attempt = 0; attempt <= RETRY_DELAYS_MS.length; attempt++) {
30
+ if (attempt > 0)
31
+ await sleep(RETRY_DELAYS_MS[attempt - 1]);
32
+ const ac = new AbortController();
33
+ const timer = setTimeout(() => ac.abort(), REQUEST_TIMEOUT_MS);
34
+ try {
35
+ const res = await fetch(url, {
36
+ method: "POST",
37
+ headers,
38
+ body,
39
+ signal: ac.signal,
40
+ dispatcher: unsaAgent,
41
+ });
42
+ if (res.status !== 200) {
43
+ // 3xx (redirect a login) o 4xx suelen indicar sesión muerta.
44
+ if (res.status === 303 || res.status === 302 || res.status === 401) {
45
+ throw new SessionExpiredError();
46
+ }
47
+ throw new MoodleApiError(`HTTP ${res.status}`);
48
+ }
49
+ const text = await res.text();
50
+ let decoded;
51
+ try {
52
+ decoded = JSON.parse(text);
53
+ }
54
+ catch {
55
+ // Moodle devuelve HTML de login cuando la sesión murió.
56
+ if (/login\/index\.php|loginform/i.test(text))
57
+ throw new SessionExpiredError();
58
+ throw new MoodleApiError("Respuesta no es JSON válido");
59
+ }
60
+ if (!Array.isArray(decoded)) {
61
+ throw new MoodleApiError("Respuesta inesperada del servidor AJAX");
62
+ }
63
+ return decoded.map((entry) => {
64
+ const item = entry;
65
+ if (item.error) {
66
+ const code = item.exception?.errorcode ?? "";
67
+ const msg = item.exception?.message ?? "Error desconocido de Moodle";
68
+ if (code === "requireloginerror" || code === "servicerequireslogin") {
69
+ throw new SessionExpiredError();
70
+ }
71
+ throw new MoodleApiError(msg, code);
72
+ }
73
+ return item.data;
74
+ });
75
+ }
76
+ catch (err) {
77
+ if (err instanceof SessionExpiredError || err instanceof MoodleApiError) {
78
+ throw err; // definitivo: no reintentar
79
+ }
80
+ // AbortError / TypeError de red / undici errors → transitorio.
81
+ lastNetworkError = err;
82
+ }
83
+ finally {
84
+ clearTimeout(timer);
85
+ }
86
+ }
87
+ throw new NetworkError(`Fallo de red tras reintentos: ${String(lastNetworkError?.message ?? lastNetworkError)}`);
88
+ }
89
+ /** Atajo para una sola llamada; devuelve directamente su `data`. */
90
+ export async function postAjax(session, methodname, args) {
91
+ const [data] = await postAjaxBatch(session, [{ methodname, args }]);
92
+ return data;
93
+ }
94
+ //# sourceMappingURL=moodleClient.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"moodleClient.js","sourceRoot":"","sources":["../../src/core/moodleClient.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,64 @@
1
+ import { mkdir, readFile, writeFile, chmod } from "node:fs/promises";
2
+ import { z } from "zod";
3
+ import { DATA_DIR, SESSION_FILE } from "./config.js";
4
+ /**
5
+ * Sesión de Moodle capturada tras el login. Los dos artefactos que importan son:
6
+ * - moodleSession: valor de la cookie MoodleSession (autenticación).
7
+ * - sesskey: token CSRF que Moodle exige en cada llamada AJAX.
8
+ * siteUrl se auto-deriva del dashboard (incluye el semestre real).
9
+ */
10
+ export const SessionSchema = z.object({
11
+ moodleSession: z.string().min(1),
12
+ sesskey: z.string().min(1),
13
+ siteUrl: z.string().url(),
14
+ /** epoch ms del momento de captura. */
15
+ capturedAt: z.number(),
16
+ });
17
+ /**
18
+ * TTL sólo informativo (para `status` y para decidir un refresco proactivo). NO se usa como
19
+ * puerta dura: las sesiones de Moodle de la UNSA duran más de lo que sugiere este valor, así
20
+ * que la autoridad real es el servidor — si responde requireloginerror, renovamos. Gatear por
21
+ * tiempo provocaba re-logins prematuros con la sesión aún viva.
22
+ */
23
+ const SESSION_TTL_MS = 10 * 60 * 60 * 1000;
24
+ export function isExpired(session) {
25
+ return Date.now() - session.capturedAt >= SESSION_TTL_MS;
26
+ }
27
+ /**
28
+ * Una sesión es "usable" si tiene cookie y sesskey. No se descarta por antigüedad: se intenta
29
+ * usar y, si el servidor la rechaza, el cliente renueva. Así se reutiliza al máximo la sesión
30
+ * viva y se evita abrir el navegador sin necesidad.
31
+ */
32
+ export function isValid(session) {
33
+ return (session !== null &&
34
+ session.moodleSession.length > 0 &&
35
+ session.sesskey.length > 0);
36
+ }
37
+ export async function saveSession(session) {
38
+ await mkdir(DATA_DIR, { recursive: true });
39
+ await writeFile(SESSION_FILE, JSON.stringify(session, null, 2), "utf8");
40
+ // Permisos restrictivos (best-effort; en Windows es no-op práctico).
41
+ try {
42
+ await chmod(SESSION_FILE, 0o600);
43
+ }
44
+ catch {
45
+ /* ignorar en plataformas sin permisos POSIX */
46
+ }
47
+ }
48
+ export async function loadSession() {
49
+ try {
50
+ const raw = await readFile(SESSION_FILE, "utf8");
51
+ return SessionSchema.parse(JSON.parse(raw));
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ }
57
+ /** Deriva el siteUrl (con semestre) recortando la URL del dashboard en "/my". */
58
+ export function deriveSiteUrl(dashboardUrl, fallbackOrigin) {
59
+ const idx = dashboardUrl.indexOf("/my");
60
+ if (idx > 0)
61
+ return dashboardUrl.slice(0, idx);
62
+ return fallbackOrigin;
63
+ }
64
+ //# sourceMappingURL=session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/core/session.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACrE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAErD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,CAAC,MAAM,CAAC;IACpC,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAChC,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1B,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;IACzB,uCAAuC;IACvC,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE;CACvB,CAAC,CAAC;AAGH;;;;;GAKG;AACH,MAAM,cAAc,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAE3C,MAAM,UAAU,SAAS,CAAC,OAAgB;IACxC,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,UAAU,IAAI,cAAc,CAAC;AAC3D,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,OAAO,CAAC,OAAuB;IAC7C,OAAO,CACL,OAAO,KAAK,IAAI;QAChB,OAAO,CAAC,aAAa,CAAC,MAAM,GAAG,CAAC;QAChC,OAAO,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAC3B,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,OAAgB;IAChD,MAAM,KAAK,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3C,MAAM,SAAS,CAAC,YAAY,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;IACxE,qEAAqE;IACrE,IAAI,CAAC;QACH,MAAM,KAAK,CAAC,YAAY,EAAE,KAAK,CAAC,CAAC;IACnC,CAAC;IAAC,MAAM,CAAC;QACP,+CAA+C;IACjD,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW;IAC/B,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;QACjD,OAAO,aAAa,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;IAC9C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,aAAa,CAAC,YAAoB,EAAE,cAAsB;IACxE,MAAM,GAAG,GAAG,YAAY,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IACxC,IAAI,GAAG,GAAG,CAAC;QAAE,OAAO,YAAY,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAC/C,OAAO,cAAc,CAAC;AACxB,CAAC"}
@@ -0,0 +1,112 @@
1
+ import * as cheerio from "cheerio";
2
+ import { CHROME_USER_AGENT } from "../core/config.js";
3
+ import { SessionExpiredError } from "../core/errors.js";
4
+ import { fetchUnsa } from "../core/http.js";
5
+ import { daysBetween, findSpanishDates, parseSpanishDate } from "../core/dates.js";
6
+ function classifySubmission(estadoEntrega, estadoCalif) {
7
+ const e = estadoEntrega.toLowerCase();
8
+ const g = estadoCalif.toLowerCase();
9
+ if (/^calificado|(?<!sin )calificado/.test(g) && !/sin calificar/.test(g)) {
10
+ return "graded";
11
+ }
12
+ if (/no se han realizado|no entregado|sin intento|nada entregado|todav[ií]a no/.test(e)) {
13
+ return "not-submitted";
14
+ }
15
+ if (/enviado|entregado|para calificar/.test(e))
16
+ return "submitted";
17
+ return "unknown";
18
+ }
19
+ /**
20
+ * Descarga y parsea la página mod/assign/view.php de una tarea para extraer el estado de
21
+ * entrega, la nota y (si aparece) la fecha de entrega. Es el complemento necesario a
22
+ * core_courseformat_get_state, porque los eventos de calendario "de acción" sólo aparecen
23
+ * cuando la tarea está pendiente y futura — no sirven para tareas ya entregadas o vencidas.
24
+ */
25
+ export async function getAssignDetail(session, url) {
26
+ const res = await fetchUnsa(url, {
27
+ headers: {
28
+ Cookie: `MoodleSession=${session.moodleSession}`,
29
+ "User-Agent": CHROME_USER_AGENT,
30
+ },
31
+ });
32
+ if (res.status === 302 || res.status === 303)
33
+ throw new SessionExpiredError();
34
+ const html = await res.text();
35
+ if (/\/login\//.test(res.url) && /loginform|login\/index\.php/i.test(html)) {
36
+ throw new SessionExpiredError();
37
+ }
38
+ const $ = cheerio.load(html);
39
+ const kv = new Map();
40
+ $("table tr").each((_, tr) => {
41
+ const cells = $(tr).find("th,td");
42
+ if (cells.length >= 2) {
43
+ const k = $(cells[0]).text().trim().replace(/\s+/g, " ").toLowerCase();
44
+ const v = $(cells[1]).text().trim().replace(/\s+/g, " ");
45
+ if (k && v && k.length < 60 && !kv.has(k))
46
+ kv.set(k, v);
47
+ }
48
+ });
49
+ const estadoEntrega = kv.get("estado de la entrega") ?? "";
50
+ const estadoCalif = kv.get("estado de la calificación") ?? "";
51
+ const submission = classifySubmission(estadoEntrega, estadoCalif);
52
+ const grade = kv.get("calificación") ?? null;
53
+ const timeRemaining = kv.get("tiempo restante") ?? null;
54
+ const gradedBy = kv.get("calificado por") ?? null;
55
+ // Consigna/instrucciones de la tarea. Se excluye el árbol de archivos adjuntos, porque Moodle
56
+ // renderiza ahí la FECHA DE SUBIDA del adjunto y se confundiría con una fecha de la consigna.
57
+ const descEl = $(".activity-description").first();
58
+ const descClone = descEl.clone();
59
+ descClone
60
+ .find('[id^="assign_files_tree"], .fileuploadsubmission, .fileuploadsubmissiontime')
61
+ .remove();
62
+ const description = descClone.length
63
+ ? descClone.text().replace(/\s+/g, " ").trim() || null
64
+ : null;
65
+ // Adjuntos de la consigna (guías, rúbricas): el agente puede leerlos con read_resource.
66
+ const attachments = [];
67
+ const seenUrls = new Set();
68
+ descEl.find('a[href*="pluginfile.php"]').each((_, a) => {
69
+ const href = $(a).attr("href");
70
+ if (!href || seenUrls.has(href))
71
+ return;
72
+ seenUrls.add(href);
73
+ const filename = $(a).text().trim() ||
74
+ decodeURIComponent(href.split("/").pop()?.split("?")[0] ?? "archivo");
75
+ attachments.push({ filename, url: href });
76
+ });
77
+ // Fechas oficiales que Moodle muestra en la cabecera de la actividad.
78
+ let openDate = null;
79
+ let closeDate = null;
80
+ $(".activity-dates div, [data-region='activity-dates'] div").each((_, e) => {
81
+ const t = $(e).text().replace(/\s+/g, " ").trim();
82
+ if (/^apertura/i.test(t))
83
+ openDate = parseSpanishDate(t);
84
+ else if (/^cierre|^vencimiento/i.test(t))
85
+ closeDate = parseSpanishDate(t);
86
+ });
87
+ // Fecha de entrega: prioriza el cierre oficial; si no, una fila explícita de la tabla.
88
+ let dueDate = closeDate;
89
+ if (dueDate == null) {
90
+ const fechaRow = kv.get("fecha de entrega");
91
+ if (fechaRow)
92
+ dueDate = parseSpanishDate(fechaRow);
93
+ }
94
+ // Fechas escritas dentro de la consigna y detección de discrepancia con la oficial.
95
+ const datesInDescription = description ? findSpanishDates(description) : [];
96
+ const dateConflict = closeDate != null &&
97
+ datesInDescription.some((d) => d.epoch != null && Math.abs(daysBetween(d.epoch, closeDate)) > 1);
98
+ return {
99
+ submission,
100
+ grade,
101
+ dueDate,
102
+ timeRemaining,
103
+ description,
104
+ attachments,
105
+ openDate,
106
+ closeDate,
107
+ datesInDescription,
108
+ dateConflict,
109
+ gradedBy,
110
+ };
111
+ }
112
+ //# sourceMappingURL=assign.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assign.js","sourceRoot":"","sources":["../../src/domain/assign.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ /** Ejecuta `worker` sobre cada item con concurrencia acotada. Preserva el orden de entrada. */
2
+ export async function mapLimit(items, limit, worker) {
3
+ const results = new Array(items.length);
4
+ let next = 0;
5
+ async function run() {
6
+ while (true) {
7
+ const i = next++;
8
+ if (i >= items.length)
9
+ return;
10
+ results[i] = await worker(items[i], i);
11
+ }
12
+ }
13
+ const workers = Array.from({ length: Math.min(limit, items.length) }, run);
14
+ await Promise.all(workers);
15
+ return results;
16
+ }
17
+ //# sourceMappingURL=concurrency.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"concurrency.js","sourceRoot":"","sources":["../../src/domain/concurrency.ts"],"names":[],"mappings":"AAAA,+FAA+F;AAC/F,MAAM,CAAC,KAAK,UAAU,QAAQ,CAC5B,KAAU,EACV,KAAa,EACb,MAA8C;IAE9C,MAAM,OAAO,GAAG,IAAI,KAAK,CAAI,KAAK,CAAC,MAAM,CAAC,CAAC;IAC3C,IAAI,IAAI,GAAG,CAAC,CAAC;IAEb,KAAK,UAAU,GAAG;QAChB,OAAO,IAAI,EAAE,CAAC;YACZ,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC;YACjB,IAAI,CAAC,IAAI,KAAK,CAAC,MAAM;gBAAE,OAAO;YAC9B,OAAO,CAAC,CAAC,CAAC,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACzC,CAAC;IACH,CAAC;IAED,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;IAC3E,MAAM,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAC3B,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -0,0 +1,70 @@
1
+ import { postAjax } from "../core/moodleClient.js";
2
+ /** Lista los cursos en los que el usuario está matriculado. */
3
+ export async function getEnrolledCourses(session) {
4
+ const data = (await postAjax(session, "core_course_get_enrolled_courses_by_timeline_classification", { offset: 0, limit: 0, classification: "all", sort: "fullname" }));
5
+ const courses = data?.courses ?? [];
6
+ return courses.map((raw) => {
7
+ const c = raw;
8
+ return {
9
+ id: c.id,
10
+ fullname: c.fullname,
11
+ shortname: c.shortname,
12
+ contacts: (c.contacts ?? [])
13
+ .map((x) => x.fullname ?? "")
14
+ .filter((s) => s.length > 0),
15
+ };
16
+ });
17
+ }
18
+ /**
19
+ * Obtiene el estado completo del curso vía core_courseformat_get_state — la API que usa la
20
+ * propia página de curso de Moodle 4.x. En este Moodle (UNSA) está habilitada por AJAX,
21
+ * mientras que core_course_get_contents está BLOQUEADA. Devuelve TODOS los módulos del curso
22
+ * (incluidos assigns sin evento de calendario), que es lo que permite descubrir tareas ocultas.
23
+ *
24
+ * Nota: esta función devuelve su `data` como STRING JSON, no como objeto — hay que parsearlo.
25
+ */
26
+ export async function getCourseState(session, courseId) {
27
+ const raw = await postAjax(session, "core_courseformat_get_state", {
28
+ courseid: courseId,
29
+ });
30
+ const parsed = (typeof raw === "string" ? JSON.parse(raw) : raw);
31
+ const sections = (parsed.section ?? []).map((s) => ({
32
+ id: Number(s.id),
33
+ number: Number(s.number ?? s.section ?? 0),
34
+ title: s.title ?? "",
35
+ visible: s.visible !== false,
36
+ }));
37
+ const modules = (parsed.cm ?? []).map((m) => ({
38
+ cmid: Number(m.id),
39
+ name: m.name,
40
+ module: m.module,
41
+ url: m.url ?? null,
42
+ visible: m.visible !== false,
43
+ uservisible: m.uservisible !== false,
44
+ stealth: Boolean(m.stealth),
45
+ sectionId: Number(m.sectionid),
46
+ completionState: m.completionstate ?? null,
47
+ }));
48
+ return { courseId, sections, modules };
49
+ }
50
+ /**
51
+ * Secciones y módulos de un curso, construidas a partir de core_courseformat_get_state.
52
+ * Reemplaza al bloqueado core_course_get_contents.
53
+ */
54
+ export async function getCourseContents(session, courseId) {
55
+ const state = await getCourseState(session, courseId);
56
+ return state.sections.map((s) => {
57
+ const modules = state.modules
58
+ .filter((m) => m.sectionId === s.id)
59
+ .map((m) => ({
60
+ cmid: m.cmid,
61
+ name: m.name,
62
+ modname: m.module,
63
+ url: m.url,
64
+ visible: m.visible,
65
+ instance: null,
66
+ }));
67
+ return { id: s.id, name: s.title, modules };
68
+ });
69
+ }
70
+ //# sourceMappingURL=courses.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"courses.js","sourceRoot":"","sources":["../../src/domain/courses.ts"],"names":[],"mappings":"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"}