@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.
- package/LICENSE +21 -0
- package/README.md +223 -0
- package/dist/cli/format.js +69 -0
- package/dist/cli/format.js.map +1 -0
- package/dist/cli/index.js +426 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/setup.js +121 -0
- package/dist/cli/setup.js.map +1 -0
- package/dist/cli/ui.js +111 -0
- package/dist/cli/ui.js.map +1 -0
- package/dist/core/auth.js +47 -0
- package/dist/core/auth.js.map +1 -0
- package/dist/core/browser.js +56 -0
- package/dist/core/browser.js.map +1 -0
- package/dist/core/config.js +34 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/dates.js +43 -0
- package/dist/core/dates.js.map +1 -0
- package/dist/core/errors.js +25 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/http.js +43 -0
- package/dist/core/http.js.map +1 -0
- package/dist/core/login.js +193 -0
- package/dist/core/login.js.map +1 -0
- package/dist/core/models.js +79 -0
- package/dist/core/models.js.map +1 -0
- package/dist/core/moodleClient.js +94 -0
- package/dist/core/moodleClient.js.map +1 -0
- package/dist/core/session.js +64 -0
- package/dist/core/session.js.map +1 -0
- package/dist/domain/assign.js +112 -0
- package/dist/domain/assign.js.map +1 -0
- package/dist/domain/concurrency.js +17 -0
- package/dist/domain/concurrency.js.map +1 -0
- package/dist/domain/courses.js +70 -0
- package/dist/domain/courses.js.map +1 -0
- package/dist/domain/documents.js +209 -0
- package/dist/domain/documents.js.map +1 -0
- package/dist/domain/grades.js +99 -0
- package/dist/domain/grades.js.map +1 -0
- package/dist/domain/people.js +161 -0
- package/dist/domain/people.js.map +1 -0
- package/dist/domain/resources.js +242 -0
- package/dist/domain/resources.js.map +1 -0
- package/dist/domain/tasks.js +191 -0
- package/dist/domain/tasks.js.map +1 -0
- package/dist/mcp/server.js +261 -0
- package/dist/mcp/server.js.map +1 -0
- package/package.json +67 -0
- package/skills/dutic/SKILL.md +173 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { extractText, getDocumentProxy } from "unpdf";
|
|
4
|
+
import { fetchResourceBuffer, isFolderUrl, listCourseMaterials, listFolderFiles, } from "./resources.js";
|
|
5
|
+
import { mapLimit } from "./concurrency.js";
|
|
6
|
+
/**
|
|
7
|
+
* Limpia el texto crudo de un PDF y lo aproxima a Markdown legible: normaliza espacios, une
|
|
8
|
+
* líneas partidas por guion, separa párrafos y marca como encabezado (##) las líneas cortas
|
|
9
|
+
* en mayúsculas (títulos de sección típicos de sílabos/informes). No pretende ser perfecto:
|
|
10
|
+
* el objetivo es texto limpio y estructurado que un agente pueda leer y analizar barato.
|
|
11
|
+
*/
|
|
12
|
+
function textToMarkdown(text) {
|
|
13
|
+
const lines = text
|
|
14
|
+
.replace(/\r/g, "")
|
|
15
|
+
.split("\n")
|
|
16
|
+
.map((l) => l.replace(/[ \t]+/g, " ").trim());
|
|
17
|
+
const out = [];
|
|
18
|
+
for (let i = 0; i < lines.length; i++) {
|
|
19
|
+
const line = lines[i];
|
|
20
|
+
if (!line) {
|
|
21
|
+
if (out.length && out[out.length - 1] !== "")
|
|
22
|
+
out.push("");
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
// Une palabra cortada con guion al final de línea.
|
|
26
|
+
if (/[a-záéíóúñ]-$/i.test(line) && lines[i + 1]) {
|
|
27
|
+
lines[i + 1] = line.slice(0, -1) + lines[i + 1];
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
// Heurística de encabezado: línea corta, sin punto final, mayormente mayúsculas.
|
|
31
|
+
const isHeading = line.length <= 80 &&
|
|
32
|
+
!/[.:,;]$/.test(line) &&
|
|
33
|
+
line === line.toUpperCase() &&
|
|
34
|
+
/[A-ZÁÉÍÓÚÑ]/.test(line);
|
|
35
|
+
out.push(isHeading ? `## ${line}` : line);
|
|
36
|
+
}
|
|
37
|
+
return out.join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
38
|
+
}
|
|
39
|
+
/** Convierte un Buffer de PDF a Markdown. `maxChars` trunca el resultado (0 = sin límite). */
|
|
40
|
+
export async function pdfBufferToMarkdown(buffer, maxChars = 0) {
|
|
41
|
+
// verbosity 0 (ERRORS) silencia los warnings de fuentes de pdf.js ("TT: undefined function")
|
|
42
|
+
// que no afectan al texto extraído. Es seguro con conversiones concurrentes (sin tocar consola).
|
|
43
|
+
const pdf = await getDocumentProxy(new Uint8Array(buffer), { verbosity: 0 });
|
|
44
|
+
const { text, totalPages } = await extractText(pdf, { mergePages: true });
|
|
45
|
+
const rawText = Array.isArray(text) ? text.join("\n\n") : String(text);
|
|
46
|
+
const full = textToMarkdown(rawText);
|
|
47
|
+
const truncated = maxChars > 0 && full.length > maxChars;
|
|
48
|
+
return {
|
|
49
|
+
markdown: truncated ? full.slice(0, maxChars) + "\n\n…(truncado)" : full,
|
|
50
|
+
pages: totalPages,
|
|
51
|
+
totalChars: full.length,
|
|
52
|
+
truncated,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** Detecta si un buffer es un PDF por su cabecera mágica (%PDF). */
|
|
56
|
+
export function looksLikePdf(buffer, contentType) {
|
|
57
|
+
if (contentType && /pdf/i.test(contentType))
|
|
58
|
+
return true;
|
|
59
|
+
return buffer.length >= 5 && buffer.toString("latin1", 0, 5) === "%PDF-";
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Descarga un recurso del aula y devuelve su contenido como Markdown/texto listo para que el
|
|
63
|
+
* agente lo analice — sin gastar tokens en el binario. Convierte PDFs con `unpdf`, devuelve tal
|
|
64
|
+
* cual los recursos de texto, y para binarios no soportados (docx, xlsx, imágenes) informa el
|
|
65
|
+
* tipo y sugiere descargar. Reutiliza la resolución de pluginfile.php de resources.ts.
|
|
66
|
+
*/
|
|
67
|
+
export async function readResourceAsMarkdown(session, url, maxChars = 24_000) {
|
|
68
|
+
// Carpeta (mod/folder): puede tener varios archivos. Se listan (vía navegador headless) y se
|
|
69
|
+
// concatena el contenido convertido de cada uno, con un encabezado por archivo.
|
|
70
|
+
if (isFolderUrl(url)) {
|
|
71
|
+
const files = await listFolderFiles(session, url);
|
|
72
|
+
if (files.length === 0) {
|
|
73
|
+
return {
|
|
74
|
+
filename: "carpeta",
|
|
75
|
+
contentType: null,
|
|
76
|
+
kind: "binary",
|
|
77
|
+
markdown: null,
|
|
78
|
+
bytes: 0,
|
|
79
|
+
note: "La carpeta está vacía (sin archivos subidos).",
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const parts = [];
|
|
83
|
+
let used = 0;
|
|
84
|
+
let bytes = 0;
|
|
85
|
+
for (const f of files) {
|
|
86
|
+
if (used >= maxChars) {
|
|
87
|
+
parts.push(`\n\n…(quedan ${files.length} archivos sin mostrar; sube maxChars)`);
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
const sub = await readResourceAsMarkdown(session, f.url, maxChars - used);
|
|
92
|
+
bytes += sub.bytes;
|
|
93
|
+
if (sub.markdown) {
|
|
94
|
+
const block = `\n\n# ${f.filename}\n\n${sub.markdown}`;
|
|
95
|
+
parts.push(block);
|
|
96
|
+
used += block.length;
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
parts.push(`\n\n# ${f.filename}\n\n(${sub.note})`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
parts.push(`\n\n# ${f.filename}\n\n(error: ${e.message})`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
filename: `carpeta (${files.length} archivo(s))`,
|
|
108
|
+
contentType: "inode/directory",
|
|
109
|
+
kind: "text",
|
|
110
|
+
markdown: parts.join("").trim(),
|
|
111
|
+
totalChars: used,
|
|
112
|
+
truncated: used >= maxChars,
|
|
113
|
+
bytes,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const { buffer, contentType, filename } = await fetchResourceBuffer(session, url);
|
|
117
|
+
const bytes = buffer.byteLength;
|
|
118
|
+
if (looksLikePdf(buffer, contentType)) {
|
|
119
|
+
const doc = await pdfBufferToMarkdown(buffer, maxChars);
|
|
120
|
+
return {
|
|
121
|
+
filename,
|
|
122
|
+
contentType,
|
|
123
|
+
kind: "pdf",
|
|
124
|
+
markdown: doc.markdown,
|
|
125
|
+
pages: doc.pages,
|
|
126
|
+
totalChars: doc.totalChars,
|
|
127
|
+
truncated: doc.truncated,
|
|
128
|
+
bytes,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const isText = (contentType && /^text\/|json|xml|csv|markdown/i.test(contentType)) ||
|
|
132
|
+
/\.(txt|md|csv|json|xml|html?)$/i.test(filename);
|
|
133
|
+
if (isText) {
|
|
134
|
+
const text = buffer.toString("utf8");
|
|
135
|
+
const truncated = text.length > maxChars;
|
|
136
|
+
return {
|
|
137
|
+
filename,
|
|
138
|
+
contentType,
|
|
139
|
+
kind: "text",
|
|
140
|
+
markdown: truncated ? text.slice(0, maxChars) + "\n\n…(truncado)" : text,
|
|
141
|
+
totalChars: text.length,
|
|
142
|
+
truncated,
|
|
143
|
+
bytes,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
filename,
|
|
148
|
+
contentType,
|
|
149
|
+
kind: "binary",
|
|
150
|
+
markdown: null,
|
|
151
|
+
bytes,
|
|
152
|
+
note: `Tipo no convertible a texto (${contentType ?? "desconocido"}). ` +
|
|
153
|
+
`Usa dutic_download_file para guardarlo y procesarlo con otra herramienta.`,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
const sanitize = (name) => name.replace(/[<>:"/\\|?*]+/g, "_").slice(0, 120);
|
|
157
|
+
export async function studyCourseMaterials(session, courseId, destDir, opts = {}) {
|
|
158
|
+
const { concurrency = 5, section, onProgress } = opts;
|
|
159
|
+
const materials = await listCourseMaterials(session, courseId, { section });
|
|
160
|
+
let done = 0;
|
|
161
|
+
return mapLimit(materials, concurrency, async (m) => {
|
|
162
|
+
const sub = m.folder ? join(destDir, sanitize(m.folder)) : destDir;
|
|
163
|
+
try {
|
|
164
|
+
const { buffer, contentType } = await fetchResourceBuffer(session, m.url);
|
|
165
|
+
await mkdir(sub, { recursive: true });
|
|
166
|
+
if (looksLikePdf(buffer, contentType)) {
|
|
167
|
+
const doc = await pdfBufferToMarkdown(buffer, 0);
|
|
168
|
+
const out = join(sub, sanitize(m.filename).replace(/\.pdf$/i, "") + ".md");
|
|
169
|
+
await writeFile(out, `# ${m.filename}\n\n${doc.markdown}`, "utf8");
|
|
170
|
+
return {
|
|
171
|
+
filename: m.filename,
|
|
172
|
+
folder: m.folder,
|
|
173
|
+
savedTo: out,
|
|
174
|
+
kind: "markdown",
|
|
175
|
+
chars: doc.totalChars,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const out = join(sub, sanitize(m.filename));
|
|
179
|
+
await writeFile(out, buffer);
|
|
180
|
+
return { filename: m.filename, folder: m.folder, savedTo: out, kind: "file" };
|
|
181
|
+
}
|
|
182
|
+
catch (e) {
|
|
183
|
+
return {
|
|
184
|
+
filename: m.filename,
|
|
185
|
+
folder: m.folder,
|
|
186
|
+
savedTo: "",
|
|
187
|
+
kind: "error",
|
|
188
|
+
error: e.message,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
finally {
|
|
192
|
+
onProgress?.(++done, materials.length, m.filename);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
/** Convierte un PDF local a Markdown; opcionalmente lo guarda en outPath. */
|
|
197
|
+
export async function convertLocalPdfToMarkdown(inputPath, outPath, maxChars = 0) {
|
|
198
|
+
const buffer = await readFile(inputPath);
|
|
199
|
+
if (!looksLikePdf(buffer)) {
|
|
200
|
+
throw new Error(`No parece un PDF: ${inputPath}`);
|
|
201
|
+
}
|
|
202
|
+
const doc = await pdfBufferToMarkdown(buffer, maxChars);
|
|
203
|
+
if (outPath) {
|
|
204
|
+
await writeFile(outPath, doc.markdown, "utf8");
|
|
205
|
+
return { ...doc, savedTo: outPath };
|
|
206
|
+
}
|
|
207
|
+
return doc;
|
|
208
|
+
}
|
|
209
|
+
//# sourceMappingURL=documents.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"documents.js","sourceRoot":"","sources":["../../src/domain/documents.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,99 @@
|
|
|
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 { getEnrolledCourses } from "./courses.js";
|
|
6
|
+
import { mapLimit } from "./concurrency.js";
|
|
7
|
+
function clean(text) {
|
|
8
|
+
return text.replace(/\s+/g, " ").trim();
|
|
9
|
+
}
|
|
10
|
+
function textOrNull(v) {
|
|
11
|
+
const t = clean(v);
|
|
12
|
+
if (!t || t === "-" || /^\(\s*vac[ií]o\s*\)$/i.test(t))
|
|
13
|
+
return null;
|
|
14
|
+
return t;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Extrae la nota numérica de la celda, descartando el menú de acciones que Moodle añade
|
|
18
|
+
* ("… Acciones Análisis de calificaciones"). Devuelve el número tal cual ("9,64") o null.
|
|
19
|
+
*/
|
|
20
|
+
function parseGradeCell($, cell) {
|
|
21
|
+
const clone = cell.clone();
|
|
22
|
+
clone.find(".action-menu, .dropdown, .menubar, script, .accesshide").remove();
|
|
23
|
+
const raw = clean(clone.text());
|
|
24
|
+
const m = /-?\d+(?:[.,]\d+)?/.exec(raw);
|
|
25
|
+
if (m)
|
|
26
|
+
return m[0];
|
|
27
|
+
return textOrNull(raw);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Nombre limpio del ítem. La celda concatena el tipo (alt del icono) con el nombre, p. ej.
|
|
31
|
+
* "TareaTarea Individual 01". Se prefiere el texto del enlace; si no, se quita el prefijo tipo.
|
|
32
|
+
*/
|
|
33
|
+
function itemName($, cell, type) {
|
|
34
|
+
const link = clean(cell.find("a").first().text());
|
|
35
|
+
if (link)
|
|
36
|
+
return link;
|
|
37
|
+
let name = clean(cell.text());
|
|
38
|
+
if (type && name.startsWith(type))
|
|
39
|
+
name = name.slice(type.length).trim();
|
|
40
|
+
return name;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Descarga y parsea el reporte de notas del usuario de un curso
|
|
44
|
+
* (grade/report/user/index.php). Devuelve cada ítem calificable con su nota, rango y peso.
|
|
45
|
+
*/
|
|
46
|
+
export async function getCourseGrades(session, courseId, courseName = "") {
|
|
47
|
+
const url = `${session.siteUrl}/grade/report/user/index.php?id=${courseId}`;
|
|
48
|
+
const res = await fetchUnsa(url, {
|
|
49
|
+
headers: {
|
|
50
|
+
Cookie: `MoodleSession=${session.moodleSession}`,
|
|
51
|
+
"User-Agent": CHROME_USER_AGENT,
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
if (res.status === 302 || res.status === 303)
|
|
55
|
+
throw new SessionExpiredError();
|
|
56
|
+
const html = await res.text();
|
|
57
|
+
if (/login\/index\.php/.test(res.url))
|
|
58
|
+
throw new SessionExpiredError();
|
|
59
|
+
const $ = cheerio.load(html);
|
|
60
|
+
const items = [];
|
|
61
|
+
let total = null;
|
|
62
|
+
let totalPercentage = null;
|
|
63
|
+
$("table.user-grade tbody tr").each((_, tr) => {
|
|
64
|
+
const row = $(tr);
|
|
65
|
+
const nameCell = row.find(".column-itemname").first();
|
|
66
|
+
if (nameCell.length === 0)
|
|
67
|
+
return;
|
|
68
|
+
const type = clean(nameCell.find("img").first().attr("alt") ?? "") || null;
|
|
69
|
+
const name = itemName($, nameCell, type);
|
|
70
|
+
if (!name)
|
|
71
|
+
return;
|
|
72
|
+
const grade = parseGradeCell($, row.find(".column-grade").first());
|
|
73
|
+
const range = textOrNull(row.find(".column-range").first().text());
|
|
74
|
+
const percentage = textOrNull(row.find(".column-percentage").first().text());
|
|
75
|
+
const weight = textOrNull(row.find(".column-weight").first().text());
|
|
76
|
+
const isTotal = /total del curso/i.test(name);
|
|
77
|
+
if (isTotal) {
|
|
78
|
+
total = grade;
|
|
79
|
+
totalPercentage = percentage;
|
|
80
|
+
}
|
|
81
|
+
// Saltar la fila cabecera del curso (sin datos).
|
|
82
|
+
if (!isTotal && !grade && !range && !percentage && !weight)
|
|
83
|
+
return;
|
|
84
|
+
items.push({ name, type, grade, range, percentage, weight, isTotal });
|
|
85
|
+
});
|
|
86
|
+
return { courseId, courseName, items, total, totalPercentage };
|
|
87
|
+
}
|
|
88
|
+
/** Notas de todos los cursos matriculados (en paralelo, acotado). */
|
|
89
|
+
export async function getAllGrades(session, concurrency = 4) {
|
|
90
|
+
const courses = await getEnrolledCourses(session);
|
|
91
|
+
return mapLimit(courses, concurrency, (c) => getCourseGrades(session, c.id, c.fullname).catch(() => ({
|
|
92
|
+
courseId: c.id,
|
|
93
|
+
courseName: c.fullname,
|
|
94
|
+
items: [],
|
|
95
|
+
total: null,
|
|
96
|
+
totalPercentage: null,
|
|
97
|
+
})));
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=grades.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grades.js","sourceRoot":"","sources":["../../src/domain/grades.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,161 @@
|
|
|
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 { getCourseState, getEnrolledCourses } from "./courses.js";
|
|
6
|
+
import { getAssignDetail } from "./assign.js";
|
|
7
|
+
import { mapLimit } from "./concurrency.js";
|
|
8
|
+
function headers(session) {
|
|
9
|
+
return {
|
|
10
|
+
Cookie: `MoodleSession=${session.moodleSession}`,
|
|
11
|
+
"User-Agent": CHROME_USER_AGENT,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
async function getHtml(session, url) {
|
|
15
|
+
const res = await fetchUnsa(url, { headers: headers(session) }, 45_000);
|
|
16
|
+
if (res.status === 302 || res.status === 303)
|
|
17
|
+
throw new SessionExpiredError();
|
|
18
|
+
const html = await res.text();
|
|
19
|
+
if (/\/login\//.test(res.url) && /loginform/i.test(html))
|
|
20
|
+
throw new SessionExpiredError();
|
|
21
|
+
return html;
|
|
22
|
+
}
|
|
23
|
+
const clean = (s) => s.replace(/\s+/g, " ").trim();
|
|
24
|
+
const EMAIL_RE = /[\w.+-]+@[\w.-]+\.\w{2,}/;
|
|
25
|
+
/**
|
|
26
|
+
* Extrae el correo del perfil. Moodle lo ofusca percent-encoding los caracteres del mailto
|
|
27
|
+
* (anti-spam), p. ej. "%79%67...@%75ns%61.pe", así que hay que decodificarlo.
|
|
28
|
+
*/
|
|
29
|
+
function extractEmail($) {
|
|
30
|
+
const mailto = $('a[href^="mailto:"]').first().attr("href");
|
|
31
|
+
if (mailto) {
|
|
32
|
+
const raw = mailto.slice("mailto:".length);
|
|
33
|
+
let decoded = raw;
|
|
34
|
+
try {
|
|
35
|
+
decoded = decodeURIComponent(raw);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
/* si no es percent-encoding válido, se usa tal cual */
|
|
39
|
+
}
|
|
40
|
+
const m = EMAIL_RE.exec(decoded);
|
|
41
|
+
if (m)
|
|
42
|
+
return m[0];
|
|
43
|
+
}
|
|
44
|
+
const body = $(".userprofile, #region-main").text();
|
|
45
|
+
return EMAIL_RE.exec(body)?.[0] ?? null;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Lista los participantes visibles de un curso. Ojo: si el curso usa "grupos separados", Moodle
|
|
49
|
+
* sólo muestra a los del propio grupo del usuario (es lo mismo que ve en la web).
|
|
50
|
+
*/
|
|
51
|
+
export async function listCourseParticipants(session, courseId) {
|
|
52
|
+
const html = await getHtml(session, `${session.siteUrl}/user/index.php?id=${courseId}&perpage=200`);
|
|
53
|
+
const $ = cheerio.load(html);
|
|
54
|
+
const people = [];
|
|
55
|
+
$("table#participants tbody tr").each((_, tr) => {
|
|
56
|
+
const cells = $(tr).find("th,td");
|
|
57
|
+
// La primera celda es el checkbox; el nombre (con enlace al perfil) va en la segunda.
|
|
58
|
+
const nameCell = cells.length > 1 ? $(cells[1]) : $(cells[0]);
|
|
59
|
+
const link = nameCell.find('a[href*="user/view.php"]').first();
|
|
60
|
+
const href = link.attr("href") ?? "";
|
|
61
|
+
const userId = Number(/id=(\d+)/.exec(href)?.[1] ?? 0);
|
|
62
|
+
const name = clean(link.text()) || clean(nameCell.text());
|
|
63
|
+
if (!userId || !name)
|
|
64
|
+
return;
|
|
65
|
+
people.push({
|
|
66
|
+
userId,
|
|
67
|
+
name,
|
|
68
|
+
role: clean($(cells[2]).text()) || null,
|
|
69
|
+
group: clean($(cells[3]).text()) || null,
|
|
70
|
+
lastAccess: clean($(cells[4]).text()) || null,
|
|
71
|
+
courseId,
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
return people;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Perfil de una persona: correo institucional, zona horaria y cursos compartidos. Moodle oculta
|
|
78
|
+
* el correo en el listado pero lo muestra en el perfil cuando la política del sitio lo permite.
|
|
79
|
+
*/
|
|
80
|
+
export async function getPersonProfile(session, userId, courseId, fallbackName) {
|
|
81
|
+
const url = `${session.siteUrl}/user/view.php?id=${userId}${courseId ? `&course=${courseId}` : ""}`;
|
|
82
|
+
const html = await getHtml(session, url);
|
|
83
|
+
const $ = cheerio.load(html);
|
|
84
|
+
// En contexto de curso el <h1> puede ser el nombre del curso, así que se prefiere el nombre
|
|
85
|
+
// ya conocido del listado de participantes.
|
|
86
|
+
const heading = clean($(".page-header-headings h1").first().text());
|
|
87
|
+
const name = fallbackName || heading || `usuario ${userId}`;
|
|
88
|
+
const email = extractEmail($);
|
|
89
|
+
const profileText = $(".userprofile, #region-main").text();
|
|
90
|
+
// Cortar en la siguiente mayúscula para no arrastrar el texto pegado ("America/LimaDetalles").
|
|
91
|
+
const timezone = /(?:America|Europe|Asia)\/[A-Z][a-z_]+|UTC[+-]?\d*/.exec(profileText)?.[0] ?? null;
|
|
92
|
+
const courses = $('.userprofile a[href*="course/view.php"], a[href*="course/view.php"]')
|
|
93
|
+
.map((_, a) => clean($(a).text()))
|
|
94
|
+
.get()
|
|
95
|
+
.filter((t) => t.length > 3);
|
|
96
|
+
return { userId, name, email, timezone, courses: [...new Set(courses)] };
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Docentes de un curso. En este Moodle los profesores no aparecen en la lista de participantes
|
|
100
|
+
* del alumno, así que se combinan dos fuentes: los "contactos" que expone la API de cursos y el
|
|
101
|
+
* rol detectado en el listado (por si algún curso sí los muestra).
|
|
102
|
+
*/
|
|
103
|
+
export async function getCourseTeachers(session, courseId) {
|
|
104
|
+
const [courses, participants] = await Promise.all([
|
|
105
|
+
getEnrolledCourses(session).catch(() => []),
|
|
106
|
+
listCourseParticipants(session, courseId).catch(() => []),
|
|
107
|
+
]);
|
|
108
|
+
const fromApi = courses.find((c) => c.id === courseId)?.contacts ?? [];
|
|
109
|
+
const fromList = participants
|
|
110
|
+
.filter((p) => /docente|profesor|teacher/i.test(p.role ?? ""))
|
|
111
|
+
.map((p) => p.name);
|
|
112
|
+
// Tercera fuente (la que suele funcionar aquí): quién calificó las tareas del curso.
|
|
113
|
+
let fromGrading = [];
|
|
114
|
+
try {
|
|
115
|
+
const state = await getCourseState(session, courseId);
|
|
116
|
+
const assigns = state.modules.filter((m) => m.module === "assign" && m.url).slice(0, 12);
|
|
117
|
+
const details = await mapLimit(assigns, 5, (m) => getAssignDetail(session, m.url).catch(() => null));
|
|
118
|
+
fromGrading = details
|
|
119
|
+
.map((d) => d?.gradedBy)
|
|
120
|
+
.filter((n) => Boolean(n && n.length > 3));
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
/* si falla, nos quedamos con las otras fuentes */
|
|
124
|
+
}
|
|
125
|
+
return [...new Set([...fromApi, ...fromList, ...fromGrading])];
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Busca personas entre los participantes de todos tus cursos, por nombre o por correo. Si la
|
|
129
|
+
* consulta parece un correo (o `withEmail` es true) se abren los perfiles para poder comparar
|
|
130
|
+
* el correo, lo cual es más lento pero permite "buscar por correo".
|
|
131
|
+
*/
|
|
132
|
+
export async function findPeople(session, query, opts = {}) {
|
|
133
|
+
const { concurrency = 4 } = opts;
|
|
134
|
+
const looksLikeEmail = /@/.test(query);
|
|
135
|
+
const withEmail = opts.withEmail ?? looksLikeEmail;
|
|
136
|
+
const q = query.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "");
|
|
137
|
+
const courses = await getEnrolledCourses(session);
|
|
138
|
+
const perCourse = await mapLimit(courses, concurrency, async (c) => {
|
|
139
|
+
const list = await listCourseParticipants(session, c.id).catch(() => []);
|
|
140
|
+
return list.map((p) => ({ ...p, courseName: c.fullname }));
|
|
141
|
+
});
|
|
142
|
+
// Dedup por userId conservando el primer curso donde aparece.
|
|
143
|
+
const byUser = new Map();
|
|
144
|
+
for (const p of perCourse.flat())
|
|
145
|
+
if (!byUser.has(p.userId))
|
|
146
|
+
byUser.set(p.userId, p);
|
|
147
|
+
let candidates = [...byUser.values()];
|
|
148
|
+
if (!withEmail) {
|
|
149
|
+
return candidates.filter((p) => p.name.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").includes(q));
|
|
150
|
+
}
|
|
151
|
+
// Con correo: resolver perfiles (acotado) y filtrar por nombre o correo.
|
|
152
|
+
const byName = candidates.filter((p) => p.name.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").includes(q));
|
|
153
|
+
const pool = looksLikeEmail ? candidates : byName;
|
|
154
|
+
const enriched = await mapLimit(pool, concurrency, async (p) => {
|
|
155
|
+
const prof = await getPersonProfile(session, p.userId, p.courseId, p.name).catch(() => null);
|
|
156
|
+
return { ...p, email: prof?.email ?? null };
|
|
157
|
+
});
|
|
158
|
+
return enriched.filter((p) => p.name.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").includes(q) ||
|
|
159
|
+
(p.email ?? "").toLowerCase().includes(q));
|
|
160
|
+
}
|
|
161
|
+
//# sourceMappingURL=people.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"people.js","sourceRoot":"","sources":["../../src/domain/people.ts"],"names":[],"mappings":"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"}
|