@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
package/dist/cli/ui.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilidades de presentación para el CLI: color ANSI, tablas con caracteres de caja, barras de
|
|
3
|
+
* progreso y marcadores de estado en ASCII (sin emojis). Respeta NO_COLOR y salidas sin TTY.
|
|
4
|
+
*/
|
|
5
|
+
const useColor = !process.env.NO_COLOR && (process.stdout.isTTY || process.env.FORCE_COLOR === "1");
|
|
6
|
+
const wrap = (code) => (s) => (useColor ? `\x1b[${code}m${s}\x1b[0m` : s);
|
|
7
|
+
export const c = {
|
|
8
|
+
reset: wrap("0"),
|
|
9
|
+
bold: wrap("1"),
|
|
10
|
+
dim: wrap("2"),
|
|
11
|
+
red: wrap("31"),
|
|
12
|
+
green: wrap("32"),
|
|
13
|
+
yellow: wrap("33"),
|
|
14
|
+
blue: wrap("34"),
|
|
15
|
+
magenta: wrap("35"),
|
|
16
|
+
cyan: wrap("36"),
|
|
17
|
+
gray: wrap("90"),
|
|
18
|
+
boldCyan: wrap("1;36"),
|
|
19
|
+
boldRed: wrap("1;31"),
|
|
20
|
+
boldGreen: wrap("1;32"),
|
|
21
|
+
boldYellow: wrap("1;33"),
|
|
22
|
+
};
|
|
23
|
+
/** Longitud visible (sin códigos ANSI), para alinear columnas. */
|
|
24
|
+
function visibleLen(s) {
|
|
25
|
+
return s.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
26
|
+
}
|
|
27
|
+
function padEndVisible(s, width) {
|
|
28
|
+
const pad = width - visibleLen(s);
|
|
29
|
+
return pad > 0 ? s + " ".repeat(pad) : s;
|
|
30
|
+
}
|
|
31
|
+
/** Marcadores de estado ASCII (sin emojis). */
|
|
32
|
+
export const mark = {
|
|
33
|
+
ok: () => c.green("[OK]"),
|
|
34
|
+
warn: () => c.yellow("[!]"),
|
|
35
|
+
err: () => c.red("[x]"),
|
|
36
|
+
info: () => c.cyan("[i]"),
|
|
37
|
+
bullet: () => c.gray("•"),
|
|
38
|
+
arrow: () => c.cyan("›"),
|
|
39
|
+
pending: () => c.boldYellow("○"),
|
|
40
|
+
done: () => c.green("●"),
|
|
41
|
+
};
|
|
42
|
+
/** Título en caja de doble/simple línea, opcionalmente con subtítulo. */
|
|
43
|
+
export function banner(title, subtitle) {
|
|
44
|
+
const lines = [title, ...(subtitle ? [subtitle] : [])];
|
|
45
|
+
const width = Math.max(...lines.map((l) => l.length)) + 2;
|
|
46
|
+
const top = "┌" + "─".repeat(width) + "┐";
|
|
47
|
+
const bot = "└" + "─".repeat(width) + "┘";
|
|
48
|
+
const body = lines
|
|
49
|
+
.map((l, i) => {
|
|
50
|
+
const styled = i === 0 ? c.boldCyan(l) : c.dim(l);
|
|
51
|
+
return "│ " + styled + " ".repeat(width - 1 - l.length) + "│";
|
|
52
|
+
})
|
|
53
|
+
.join("\n");
|
|
54
|
+
return c.cyan(top) + "\n" + body + "\n" + c.cyan(bot);
|
|
55
|
+
}
|
|
56
|
+
/** Regla horizontal con etiqueta opcional. */
|
|
57
|
+
export function rule(label, width = 60) {
|
|
58
|
+
if (!label)
|
|
59
|
+
return c.gray("─".repeat(width));
|
|
60
|
+
const line = `── ${c.bold(label)} `;
|
|
61
|
+
const rest = width - visibleLen(line);
|
|
62
|
+
return c.gray("──") + " " + c.bold(label) + " " + c.gray("─".repeat(Math.max(rest, 0)));
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Tabla con bordes de caja. `rows` son celdas ya en texto plano; el color de columna se aplica
|
|
66
|
+
* al renderizar para no romper el cálculo de anchos.
|
|
67
|
+
*/
|
|
68
|
+
export function table(columns, rows) {
|
|
69
|
+
const widths = columns.map((col, i) => Math.max(visibleLen(col.header), ...rows.map((r) => visibleLen(r[i] ?? ""))));
|
|
70
|
+
const sep = (l, m, r) => c.gray(l + widths.map((w) => "─".repeat(w + 2)).join(m) + r);
|
|
71
|
+
const fmt = (cells, color = false) => c.gray("│") +
|
|
72
|
+
cells
|
|
73
|
+
.map((cell, i) => {
|
|
74
|
+
const col = columns[i];
|
|
75
|
+
const painted = color && col.color ? col.color(cell) : cell;
|
|
76
|
+
const padded = col.align === "right"
|
|
77
|
+
? " ".repeat(Math.max(widths[i] - visibleLen(cell), 0)) + painted
|
|
78
|
+
: padEndVisible(painted, widths[i]);
|
|
79
|
+
return " " + padded + " ";
|
|
80
|
+
})
|
|
81
|
+
.join(c.gray("│")) +
|
|
82
|
+
c.gray("│");
|
|
83
|
+
const head = fmt(columns.map((col) => c.bold(col.header)));
|
|
84
|
+
const body = rows.map((r) => fmt(r, true)).join("\n");
|
|
85
|
+
return [sep("┌", "┬", "┐"), head, sep("├", "┼", "┤"), body, sep("└", "┴", "┘")].join("\n");
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Barra de progreso en una sola línea (se reescribe con \r sobre stderr). Llama a `done()` al
|
|
89
|
+
* terminar para dejar la línea limpia.
|
|
90
|
+
*/
|
|
91
|
+
export function progressBar(total, label = "") {
|
|
92
|
+
const width = 28;
|
|
93
|
+
const stream = process.stderr;
|
|
94
|
+
const render = (current, note = "") => {
|
|
95
|
+
if (!stream.isTTY)
|
|
96
|
+
return;
|
|
97
|
+
const ratio = total > 0 ? Math.min(current / total, 1) : 0;
|
|
98
|
+
const filled = Math.round(ratio * width);
|
|
99
|
+
const bar = c.cyan("█".repeat(filled)) + c.gray("░".repeat(width - filled));
|
|
100
|
+
const pct = String(Math.round(ratio * 100)).padStart(3);
|
|
101
|
+
stream.write(`\r${label} ${bar} ${pct}% ${c.dim(`(${current}/${total})`)} ${c.dim(note)} `);
|
|
102
|
+
};
|
|
103
|
+
return {
|
|
104
|
+
update: render,
|
|
105
|
+
done: () => {
|
|
106
|
+
if (stream.isTTY)
|
|
107
|
+
stream.write("\r" + " ".repeat(width + 40) + "\r");
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=ui.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ui.js","sourceRoot":"","sources":["../../src/cli/ui.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { SessionExpiredError } from "./errors.js";
|
|
2
|
+
import { loginWithPlaywright } from "./login.js";
|
|
3
|
+
import { isValid, loadSession } from "./session.js";
|
|
4
|
+
/**
|
|
5
|
+
* Devuelve una sesión válida, renovándola según el modo:
|
|
6
|
+
* - "interactive" (por defecto): headless y, si falla, login visible (reusa el perfil
|
|
7
|
+
* persistente; SSO de Google normalmente vivo → sin reescribir credenciales).
|
|
8
|
+
* - "headless-only": sólo intento headless silencioso; si el SSO expiró, lanza
|
|
9
|
+
* SessionExpiredError (el llamador pide correr `dutic login`).
|
|
10
|
+
* - "none": nunca renueva.
|
|
11
|
+
*/
|
|
12
|
+
export async function ensureSession(opts = {}) {
|
|
13
|
+
const { mode = "interactive", login } = opts;
|
|
14
|
+
const existing = await loadSession();
|
|
15
|
+
if (isValid(existing))
|
|
16
|
+
return existing;
|
|
17
|
+
if (mode === "none")
|
|
18
|
+
throw new SessionExpiredError();
|
|
19
|
+
// Intento headless silencioso (funciona si el SSO de Google sigue vivo).
|
|
20
|
+
try {
|
|
21
|
+
return await loginWithPlaywright({ headless: true, timeoutMs: 45_000, ...login });
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
if (mode === "headless-only")
|
|
25
|
+
throw new SessionExpiredError();
|
|
26
|
+
// "interactive": abrir navegador visible para que el usuario complete el login.
|
|
27
|
+
return await loginWithPlaywright({ headless: false, ...login });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Envuelve una operación que usa la sesión. Si la operación falla por sesión expirada,
|
|
32
|
+
* renueva una vez y reintenta. Esto absorbe la caducidad del token a mitad de uso.
|
|
33
|
+
*/
|
|
34
|
+
export async function withSession(operation, opts = {}) {
|
|
35
|
+
let session = await ensureSession(opts);
|
|
36
|
+
try {
|
|
37
|
+
return await operation(session);
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
if (err instanceof SessionExpiredError) {
|
|
41
|
+
session = await ensureSession(opts);
|
|
42
|
+
return await operation(session);
|
|
43
|
+
}
|
|
44
|
+
throw err;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/core/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,mBAAmB,EAAqB,MAAM,YAAY,CAAC;AACpE,OAAO,EAAE,OAAO,EAAE,WAAW,EAAgB,MAAM,cAAc,CAAC;AAUlE;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,OAAkD,EAAE;IAEpD,MAAM,EAAE,IAAI,GAAG,aAAa,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC;IAC7C,MAAM,QAAQ,GAAG,MAAM,WAAW,EAAE,CAAC;IACrC,IAAI,OAAO,CAAC,QAAQ,CAAC;QAAE,OAAO,QAAQ,CAAC;IAEvC,IAAI,IAAI,KAAK,MAAM;QAAE,MAAM,IAAI,mBAAmB,EAAE,CAAC;IAErD,yEAAyE;IACzE,IAAI,CAAC;QACH,OAAO,MAAM,mBAAmB,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,KAAK,EAAE,CAAC,CAAC;IACpF,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,IAAI,KAAK,eAAe;YAAE,MAAM,IAAI,mBAAmB,EAAE,CAAC;QAC9D,gFAAgF;QAChF,OAAO,MAAM,mBAAmB,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,KAAK,EAAE,CAAC,CAAC;IAClE,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,SAA2C,EAC3C,OAAkD,EAAE;IAEpD,IAAI,OAAO,GAAG,MAAM,aAAa,CAAC,IAAI,CAAC,CAAC;IACxC,IAAI,CAAC;QACH,OAAO,MAAM,SAAS,CAAC,OAAO,CAAC,CAAC;IAClC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,GAAG,YAAY,mBAAmB,EAAE,CAAC;YACvC,OAAO,GAAG,MAAM,aAAa,CAAC,IAAI,CAAC,CAAC;YACpC,OAAO,MAAM,SAAS,CAAC,OAAO,CAAC,CAAC;QAClC,CAAC;QACD,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { chromium } from "playwright";
|
|
2
|
+
import { CHROME_USER_AGENT, HOST } from "./config.js";
|
|
3
|
+
/**
|
|
4
|
+
* Lanza un navegador headless para tareas que necesitan ejecutar el JS de Moodle (p. ej. listar
|
|
5
|
+
* archivos de una carpeta, cuyo árbol se puebla por JavaScript). Se lanza y cierra por llamada
|
|
6
|
+
* para no dejar el proceso vivo ni bloquear el perfil persistente del login.
|
|
7
|
+
*/
|
|
8
|
+
async function launchHeadless() {
|
|
9
|
+
const preferred = process.env.DUTIC_BROWSER_CHANNEL?.trim();
|
|
10
|
+
const channels = preferred
|
|
11
|
+
? [preferred === "chromium" ? undefined : preferred]
|
|
12
|
+
: ["chrome", "msedge", undefined];
|
|
13
|
+
let lastErr;
|
|
14
|
+
for (const channel of channels) {
|
|
15
|
+
try {
|
|
16
|
+
return await chromium.launch({ headless: true, ...(channel ? { channel } : {}) });
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
lastErr = err;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
throw new Error(`No se pudo lanzar un navegador headless: ${String(lastErr?.message ?? lastErr)}`);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Carga una URL del aula con la sesión inyectada, ejecuta su JS y devuelve todos los enlaces a
|
|
26
|
+
* pluginfile.php presentes en el DOM final. Best-effort: si el tema no expone enlaces (algunos
|
|
27
|
+
* temas renderizan el árbol de carpetas de forma que no deja anchors), devuelve lista vacía.
|
|
28
|
+
*/
|
|
29
|
+
export async function extractPluginfileLinks(session, url) {
|
|
30
|
+
const browser = await launchHeadless();
|
|
31
|
+
try {
|
|
32
|
+
const context = await browser.newContext({
|
|
33
|
+
userAgent: CHROME_USER_AGENT,
|
|
34
|
+
ignoreHTTPSErrors: true,
|
|
35
|
+
});
|
|
36
|
+
await context.addCookies([
|
|
37
|
+
{ name: "MoodleSession", value: session.moodleSession, domain: HOST, path: "/" },
|
|
38
|
+
]);
|
|
39
|
+
const page = await context.newPage();
|
|
40
|
+
await page
|
|
41
|
+
.goto(url, { waitUntil: "domcontentloaded", timeout: 20_000 })
|
|
42
|
+
.catch(() => { });
|
|
43
|
+
await page
|
|
44
|
+
.waitForSelector('a[href*="pluginfile.php"]', { timeout: 8_000 })
|
|
45
|
+
.catch(() => { });
|
|
46
|
+
const hrefs = await page.evaluate(() => {
|
|
47
|
+
const doc = globalThis.document;
|
|
48
|
+
return Array.from(doc.querySelectorAll('a[href*="pluginfile.php"]')).map((a) => a.href);
|
|
49
|
+
});
|
|
50
|
+
return [...new Set(hrefs)];
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
await browser.close().catch(() => { });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=browser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser.js","sourceRoot":"","sources":["../../src/core/browser.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAgB,MAAM,YAAY,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAGtD;;;;GAIG;AACH,KAAK,UAAU,cAAc;IAC3B,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,qBAAqB,EAAE,IAAI,EAAE,CAAC;IAC5D,MAAM,QAAQ,GAAG,SAAS;QACxB,CAAC,CAAC,CAAC,SAAS,KAAK,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;QACpD,CAAC,CAAC,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC;IACpC,IAAI,OAAgB,CAAC;IACrB,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,IAAI,CAAC;YACH,OAAO,MAAM,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;QACpF,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,GAAG,GAAG,CAAC;QAChB,CAAC;IACH,CAAC;IACD,MAAM,IAAI,KAAK,CACb,4CAA4C,MAAM,CAAE,OAAiB,EAAE,OAAO,IAAI,OAAO,CAAC,EAAE,CAC7F,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,OAAgB,EAChB,GAAW;IAEX,MAAM,OAAO,GAAG,MAAM,cAAc,EAAE,CAAC;IACvC,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC;YACvC,SAAS,EAAE,iBAAiB;YAC5B,iBAAiB,EAAE,IAAI;SACxB,CAAC,CAAC;QACH,MAAM,OAAO,CAAC,UAAU,CAAC;YACvB,EAAE,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,OAAO,CAAC,aAAa,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE;SACjF,CAAC,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC;QACrC,MAAM,IAAI;aACP,IAAI,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,kBAAkB,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;aAC7D,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QACnB,MAAM,IAAI;aACP,eAAe,CAAC,2BAA2B,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;aAChE,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QACnB,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE;YACrC,MAAM,GAAG,GAAI,UAA2C,CAAC,QAAQ,CAAC;YAClE,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,gBAAgB,CAAC,2BAA2B,CAAC,CAAC,CAAC,GAAG,CACtE,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,IAAc,CAC7B,CAAC;QACJ,CAAC,CAAC,CAAC;QACH,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC;IAC7B,CAAC;YAAS,CAAC;QACT,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IACxC,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Configuración central del cliente DUTIC.
|
|
5
|
+
*
|
|
6
|
+
* El aula virtual vive en https://aulavirtual.unsa.edu.pe/{SEMESTRE}/ donde SEMESTRE
|
|
7
|
+
* (p.ej. "2026A") cambia cada período académico. Se toma de la variable de entorno
|
|
8
|
+
* DUTIC_SEMESTER; si no existe, se usa un valor por defecto que también hay que
|
|
9
|
+
* actualizar cada ciclo. Aun así, el siteUrl real se auto-deriva tras el login a partir
|
|
10
|
+
* de la URL final del dashboard, por lo que el semestre efectivo se corrige solo.
|
|
11
|
+
*/
|
|
12
|
+
export const HOST = "aulavirtual.unsa.edu.pe";
|
|
13
|
+
/** UA de Chrome real: Google devuelve 403 a User-Agents genéricos de bot/WebView. */
|
|
14
|
+
export const CHROME_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
|
|
15
|
+
/** Semestre por defecto — sobreescribible con DUTIC_SEMESTER. Actualizar cada período. */
|
|
16
|
+
export const DEFAULT_SEMESTER = "2026A";
|
|
17
|
+
export function getSemester() {
|
|
18
|
+
return process.env.DUTIC_SEMESTER?.trim() || DEFAULT_SEMESTER;
|
|
19
|
+
}
|
|
20
|
+
/** URL base del sitio para el semestre configurado, ej. https://aulavirtual.unsa.edu.pe/2026A */
|
|
21
|
+
export function getSiteUrl(semester = getSemester()) {
|
|
22
|
+
return `https://${HOST}/${semester}`;
|
|
23
|
+
}
|
|
24
|
+
/** URL de login inicial (donde aparece el botón de Google OAuth). */
|
|
25
|
+
export function getLoginUrl(semester = getSemester()) {
|
|
26
|
+
return `${getSiteUrl(semester)}/login/index.php`;
|
|
27
|
+
}
|
|
28
|
+
// --- Rutas de estado local (fuera del repo, en el home del usuario) ---
|
|
29
|
+
export const DATA_DIR = process.env.DUTIC_DATA_DIR?.trim() || join(homedir(), ".dutic");
|
|
30
|
+
/** Perfil persistente del navegador Playwright: mantiene la sesión SSO de Google. */
|
|
31
|
+
export const BROWSER_PROFILE_DIR = join(DATA_DIR, "profile");
|
|
32
|
+
/** Sesión de Moodle serializada (cookie MoodleSession + sesskey + siteUrl). */
|
|
33
|
+
export const SESSION_FILE = join(DATA_DIR, "session.json");
|
|
34
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/core/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC;;;;;;;;GAQG;AAEH,MAAM,CAAC,MAAM,IAAI,GAAG,yBAAyB,CAAC;AAE9C,qFAAqF;AACrF,MAAM,CAAC,MAAM,iBAAiB,GAC5B,iHAAiH,CAAC;AAEpH,0FAA0F;AAC1F,MAAM,CAAC,MAAM,gBAAgB,GAAG,OAAO,CAAC;AAExC,MAAM,UAAU,WAAW;IACzB,OAAO,OAAO,CAAC,GAAG,CAAC,cAAc,EAAE,IAAI,EAAE,IAAI,gBAAgB,CAAC;AAChE,CAAC;AAED,iGAAiG;AACjG,MAAM,UAAU,UAAU,CAAC,QAAQ,GAAG,WAAW,EAAE;IACjD,OAAO,WAAW,IAAI,IAAI,QAAQ,EAAE,CAAC;AACvC,CAAC;AAED,qEAAqE;AACrE,MAAM,UAAU,WAAW,CAAC,QAAQ,GAAG,WAAW,EAAE;IAClD,OAAO,GAAG,UAAU,CAAC,QAAQ,CAAC,kBAAkB,CAAC;AACnD,CAAC;AAED,yEAAyE;AAEzE,MAAM,CAAC,MAAM,QAAQ,GACnB,OAAO,CAAC,GAAG,CAAC,cAAc,EAAE,IAAI,EAAE,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,CAAC,CAAC;AAElE,qFAAqF;AACrF,MAAM,CAAC,MAAM,mBAAmB,GAAG,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;AAE7D,+EAA+E;AAC/E,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,CAAC,QAAQ,EAAE,cAAc,CAAC,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Utilidades de fechas en español, compartidas por los scrapers de Moodle. */
|
|
2
|
+
const MONTHS_ES = {
|
|
3
|
+
enero: 0, febrero: 1, marzo: 2, abril: 3, mayo: 4, junio: 5,
|
|
4
|
+
julio: 6, agosto: 7, septiembre: 8, setiembre: 8, octubre: 9,
|
|
5
|
+
noviembre: 10, diciembre: 11,
|
|
6
|
+
};
|
|
7
|
+
/** Patrón de fecha larga en español: "19 de julio de 2026, 12:00" (hora opcional). */
|
|
8
|
+
export const SPANISH_DATE_RE = /(\d{1,2})\s+de\s+([a-záéíóúñ]+)(?:\s+de\s+(\d{4}))?(?:[,\s]+(\d{1,2}):(\d{2}))?/gi;
|
|
9
|
+
/** Convierte una fecha larga en español a epoch (segundos). null si no se puede. */
|
|
10
|
+
export function parseSpanishDate(text, defaultYear) {
|
|
11
|
+
const re = new RegExp(SPANISH_DATE_RE.source, "i");
|
|
12
|
+
const m = re.exec(text.toLowerCase());
|
|
13
|
+
if (!m)
|
|
14
|
+
return null;
|
|
15
|
+
const day = Number(m[1]);
|
|
16
|
+
const month = MONTHS_ES[m[2]];
|
|
17
|
+
if (month === undefined)
|
|
18
|
+
return null;
|
|
19
|
+
const year = m[3] ? Number(m[3]) : (defaultYear ?? new Date().getFullYear());
|
|
20
|
+
const hour = m[4] ? Number(m[4]) : 0;
|
|
21
|
+
const min = m[5] ? Number(m[5]) : 0;
|
|
22
|
+
const d = new Date(year, month, day, hour, min);
|
|
23
|
+
const t = d.getTime();
|
|
24
|
+
return Number.isNaN(t) ? null : Math.floor(t / 1000);
|
|
25
|
+
}
|
|
26
|
+
/** Encuentra todas las fechas en español dentro de un texto, con su epoch. */
|
|
27
|
+
export function findSpanishDates(text) {
|
|
28
|
+
const out = [];
|
|
29
|
+
const seen = new Set();
|
|
30
|
+
for (const m of text.matchAll(SPANISH_DATE_RE)) {
|
|
31
|
+
const raw = m[0].trim();
|
|
32
|
+
if (seen.has(raw))
|
|
33
|
+
continue;
|
|
34
|
+
seen.add(raw);
|
|
35
|
+
out.push({ text: raw, epoch: parseSpanishDate(raw) });
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
/** Diferencia en días entre dos epochs (segundos). */
|
|
40
|
+
export function daysBetween(a, b) {
|
|
41
|
+
return Math.round((a - b) / 86_400);
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=dates.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dates.js","sourceRoot":"","sources":["../../src/core/dates.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Errores tipados del dominio DUTIC, para distinguir causas de fallo. */
|
|
2
|
+
/** La sesión de Moodle caducó o no existe — hay que re-loguear. */
|
|
3
|
+
export class SessionExpiredError extends Error {
|
|
4
|
+
constructor(message = "La sesión de Moodle caducó o no existe. Ejecuta `dutic login`.") {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "SessionExpiredError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
/** Error de aplicación devuelto por Moodle (errorcode no relacionado con login). */
|
|
10
|
+
export class MoodleApiError extends Error {
|
|
11
|
+
moodleErrorCode;
|
|
12
|
+
constructor(message, moodleErrorCode = null) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.moodleErrorCode = moodleErrorCode;
|
|
15
|
+
this.name = "MoodleApiError";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** Fallo de red/HTTP tras agotar reintentos. */
|
|
19
|
+
export class NetworkError extends Error {
|
|
20
|
+
constructor(message) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = "NetworkError";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/core/errors.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAE1E,mEAAmE;AACnE,MAAM,OAAO,mBAAoB,SAAQ,KAAK;IAC5C,YAAY,OAAO,GAAG,gEAAgE;QACpF,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC;IACpC,CAAC;CACF;AAED,oFAAoF;AACpF,MAAM,OAAO,cAAe,SAAQ,KAAK;IAG5B;IAFX,YACE,OAAe,EACN,kBAAiC,IAAI;QAE9C,KAAK,CAAC,OAAO,CAAC,CAAC;QAFN,oBAAe,GAAf,eAAe,CAAsB;QAG9C,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;IAC/B,CAAC;CACF;AAED,gDAAgD;AAChD,MAAM,OAAO,YAAa,SAAQ,KAAK;IACrC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC;IAC7B,CAAC;CACF"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Agent } from "undici";
|
|
2
|
+
import { HOST } from "./config.js";
|
|
3
|
+
/**
|
|
4
|
+
* Dispatcher de undici que acepta el certificado de la CA privada de la UNSA.
|
|
5
|
+
* Sólo debe usarse para peticiones al host del aula virtual — no como dispatcher global.
|
|
6
|
+
*/
|
|
7
|
+
export const unsaAgent = new Agent({
|
|
8
|
+
connect: {
|
|
9
|
+
// La UNSA usa una CA no incluida en el trust store por defecto.
|
|
10
|
+
rejectUnauthorized: false,
|
|
11
|
+
},
|
|
12
|
+
headersTimeout: 30_000,
|
|
13
|
+
bodyTimeout: 60_000,
|
|
14
|
+
});
|
|
15
|
+
/** true si la URL apunta al host del aula virtual (para decidir si usar unsaAgent). */
|
|
16
|
+
export function isUnsaUrl(url) {
|
|
17
|
+
try {
|
|
18
|
+
return new URL(url).host === HOST;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* fetch contra el aula virtual con timeout duro (AbortController) y el dispatcher que acepta
|
|
26
|
+
* la CA privada de la UNSA. Sin este timeout, una petición colgada bloquea todo el barrido.
|
|
27
|
+
*/
|
|
28
|
+
export async function fetchUnsa(url, init = {}, timeoutMs = 25_000) {
|
|
29
|
+
const ac = new AbortController();
|
|
30
|
+
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
|
31
|
+
try {
|
|
32
|
+
return await fetch(url, {
|
|
33
|
+
...init,
|
|
34
|
+
signal: ac.signal,
|
|
35
|
+
dispatcher: unsaAgent,
|
|
36
|
+
redirect: init.redirect ?? "follow",
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
clearTimeout(timer);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=http.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../../src/core/http.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAC/B,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAEnC;;;GAGG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,IAAI,KAAK,CAAC;IACjC,OAAO,EAAE;QACP,gEAAgE;QAChE,kBAAkB,EAAE,KAAK;KAC1B;IACD,cAAc,EAAE,MAAM;IACtB,WAAW,EAAE,MAAM;CACpB,CAAC,CAAC;AAEH,uFAAuF;AACvF,MAAM,UAAU,SAAS,CAAC,GAAW;IACnC,IAAI,CAAC;QACH,OAAO,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC;IACpC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,GAAW,EACX,OAAoB,EAAE,EACtB,SAAS,GAAG,MAAM;IAElB,MAAM,EAAE,GAAG,IAAI,eAAe,EAAE,CAAC;IACjC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,KAAK,EAAE,EAAE,SAAS,CAAC,CAAC;IACtD,IAAI,CAAC;QACH,OAAO,MAAM,KAAK,CAAC,GAAG,EAAE;YACtB,GAAG,IAAI;YACP,MAAM,EAAE,EAAE,CAAC,MAAM;YACjB,UAAU,EAAE,SAAS;YACrB,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,QAAQ;SACpC,CAAC,CAAC;IACL,CAAC;YAAS,CAAC;QACT,YAAY,CAAC,KAAK,CAAC,CAAC;IACtB,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import { chromium, } from "playwright";
|
|
3
|
+
import { BROWSER_PROFILE_DIR, CHROME_USER_AGENT, DATA_DIR, getLoginUrl, HOST, } from "./config.js";
|
|
4
|
+
import { deriveSiteUrl, saveSession } from "./session.js";
|
|
5
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
6
|
+
/** true si la URL (string) apunta al dashboard /my del aula virtual. */
|
|
7
|
+
function isDashboardUrl(rawUrl) {
|
|
8
|
+
try {
|
|
9
|
+
const u = new URL(rawUrl);
|
|
10
|
+
return u.host === HOST && /\/my\/?$/.test(u.pathname);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** Normaliza un dominio de cookie (quita el punto inicial de dominios tipo ".host"). */
|
|
17
|
+
function cookieDomainMatchesHost(domain) {
|
|
18
|
+
return domain.replace(/^\./, "") === HOST;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Elige la cookie MoodleSession correcta. Moodle pone una cookie de sesión de invitado
|
|
22
|
+
* (path "/") ANTES del login y otra autenticada con scope "/{semestre}/" DESPUÉS. Se prefiere
|
|
23
|
+
* la de path más específico que sea prefijo de la ruta del dashboard.
|
|
24
|
+
*/
|
|
25
|
+
function pickMoodleCookie(cookies, dashboardPath) {
|
|
26
|
+
const candidates = cookies.filter((c) => c.name === "MoodleSession" &&
|
|
27
|
+
cookieDomainMatchesHost(c.domain) &&
|
|
28
|
+
c.value.length > 0 &&
|
|
29
|
+
dashboardPath.startsWith((c.path || "/").replace(/\/$/, "") || "/"));
|
|
30
|
+
candidates.sort((a, b) => (b.path?.length ?? 0) - (a.path?.length ?? 0));
|
|
31
|
+
if (candidates.length > 0)
|
|
32
|
+
return candidates[0];
|
|
33
|
+
// Fallback: cualquier MoodleSession del host, por si el path no encaja.
|
|
34
|
+
return cookies.find((c) => c.name === "MoodleSession" && cookieDomainMatchesHost(c.domain) && c.value.length > 0);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Sondea todas las páginas del contexto (principal y popups de Google) hasta que alguna
|
|
38
|
+
* aterriza de verdad en el dashboard /my — señal inequívoca de autenticación completada.
|
|
39
|
+
* Tolerante a redirecciones intermedias, popups y a que el usuario tarde en elegir su correo.
|
|
40
|
+
*/
|
|
41
|
+
async function waitForDashboardPage(context, deadline, onStatus) {
|
|
42
|
+
while (Date.now() < deadline) {
|
|
43
|
+
for (const pg of context.pages()) {
|
|
44
|
+
if (isDashboardUrl(pg.url()))
|
|
45
|
+
return pg;
|
|
46
|
+
}
|
|
47
|
+
// Aviso periódico de que seguimos esperando (cada ~20s).
|
|
48
|
+
const remaining = Math.round((deadline - Date.now()) / 1000);
|
|
49
|
+
if (remaining > 0 && remaining % 20 === 0) {
|
|
50
|
+
onStatus(`Esperando a que completes el login… (${remaining}s restantes)`);
|
|
51
|
+
}
|
|
52
|
+
await sleep(1000);
|
|
53
|
+
}
|
|
54
|
+
throw new Error("Tiempo agotado esperando el dashboard /my. ¿Completaste el login con tu correo UNSA? " +
|
|
55
|
+
"Vuelve a ejecutar `dutic login` y termina el flujo de Google en la ventana.");
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Extrae el sesskey desde la página cargada. Moodle 4.x inyecta window.M.cfg.sesskey de
|
|
59
|
+
* forma asíncrona vía RequireJS, así que se consultan varias fuentes en orden.
|
|
60
|
+
*/
|
|
61
|
+
const SESSKEY_SNIPPET = `(() => {
|
|
62
|
+
try { if (window.M && window.M.cfg && window.M.cfg.sesskey) return window.M.cfg.sesskey; } catch (_) {}
|
|
63
|
+
const el = document.querySelector('input[name="sesskey"]');
|
|
64
|
+
if (el && el.value) return el.value;
|
|
65
|
+
const a = document.querySelector('a[href*="logout.php?sesskey="]');
|
|
66
|
+
if (a) { const p = a.href.split('sesskey='); if (p.length > 1) return p[1].split('&')[0]; }
|
|
67
|
+
const node = document.querySelector('[data-sesskey]');
|
|
68
|
+
if (node) return node.getAttribute('data-sesskey');
|
|
69
|
+
return '';
|
|
70
|
+
})()`;
|
|
71
|
+
/**
|
|
72
|
+
* Lanza el contexto persistente. Por defecto usa el Google Chrome instalado del sistema
|
|
73
|
+
* (channel 'chrome'), evitando descargar el Chromium de Playwright (~184 MB). Si Chrome no
|
|
74
|
+
* está disponible, cae al Chromium empaquetado. Se puede forzar con DUTIC_BROWSER_CHANNEL
|
|
75
|
+
* ('chrome' | 'msedge' | 'chromium').
|
|
76
|
+
*/
|
|
77
|
+
async function launchContext(headless) {
|
|
78
|
+
const common = {
|
|
79
|
+
headless,
|
|
80
|
+
userAgent: CHROME_USER_AGENT,
|
|
81
|
+
// La UNSA usa una CA privada no confiada por defecto.
|
|
82
|
+
ignoreHTTPSErrors: true,
|
|
83
|
+
viewport: { width: 1280, height: 800 },
|
|
84
|
+
};
|
|
85
|
+
const preferred = process.env.DUTIC_BROWSER_CHANNEL?.trim();
|
|
86
|
+
const channels = preferred
|
|
87
|
+
? [preferred === "chromium" ? undefined : preferred]
|
|
88
|
+
: ["chrome", "msedge", undefined];
|
|
89
|
+
let lastErr;
|
|
90
|
+
for (const channel of channels) {
|
|
91
|
+
try {
|
|
92
|
+
return await chromium.launchPersistentContext(BROWSER_PROFILE_DIR, {
|
|
93
|
+
...common,
|
|
94
|
+
...(channel ? { channel } : {}),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
lastErr = err;
|
|
99
|
+
const msg = String(err?.message ?? "");
|
|
100
|
+
// Perfil bloqueado: otra instancia de dutic o un Chrome con este perfil ya está abierto.
|
|
101
|
+
if (/existing browser session|ProcessSingleton|SingletonLock|已经|has been closed/i.test(msg)) {
|
|
102
|
+
throw new Error(`El perfil del navegador (${BROWSER_PROFILE_DIR}) ya está en uso. ` +
|
|
103
|
+
`Cierra otras ventanas/instancias de DUTIC y reintenta.`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
throw new Error(`No se pudo lanzar un navegador (Chrome/Edge/Chromium). ` +
|
|
108
|
+
`Instala Google Chrome o ejecuta \`npx playwright install chromium\`. Detalle: ${String(lastErr?.message ?? lastErr)}`);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Lanza un navegador con perfil persistente, deja que el usuario/SSO complete el login de
|
|
112
|
+
* Google, y captura la cookie MoodleSession + el sesskey al llegar al dashboard /my.
|
|
113
|
+
*
|
|
114
|
+
* El perfil persistente en BROWSER_PROFILE_DIR mantiene viva la sesión de Google entre
|
|
115
|
+
* ejecuciones, de modo que las renovaciones futuras normalmente no requieren volver a
|
|
116
|
+
* escribir credenciales.
|
|
117
|
+
*/
|
|
118
|
+
export async function loginWithPlaywright(opts = {}) {
|
|
119
|
+
const { headless = false, timeoutMs = 300_000, onStatus = () => { } } = opts;
|
|
120
|
+
await mkdir(DATA_DIR, { recursive: true });
|
|
121
|
+
const loginUrl = getLoginUrl();
|
|
122
|
+
onStatus(`Abriendo navegador → ${loginUrl}`);
|
|
123
|
+
const context = await launchContext(headless);
|
|
124
|
+
const deadline = Date.now() + timeoutMs;
|
|
125
|
+
try {
|
|
126
|
+
const page = context.pages()[0] ?? (await context.newPage());
|
|
127
|
+
await page.goto(loginUrl, { waitUntil: "domcontentloaded", timeout: 60_000 });
|
|
128
|
+
onStatus(headless
|
|
129
|
+
? "Renovando sesión en segundo plano…"
|
|
130
|
+
: "En la ventana: pulsa «Ingresar con Correo UNSA» y elige tu cuenta de Google. " +
|
|
131
|
+
"No cierres la ventana; se cerrará sola al terminar.");
|
|
132
|
+
// Esperar (tolerante) a que CUALQUIER página aterrice de verdad en el dashboard /my.
|
|
133
|
+
// No cerramos ni extraemos nada hasta confirmar la autenticación real.
|
|
134
|
+
const dashPage = await waitForDashboardPage(context, deadline, onStatus);
|
|
135
|
+
onStatus("Login detectado. Capturando sesión…");
|
|
136
|
+
const dashboardUrl = dashPage.url();
|
|
137
|
+
const dashboardPath = new URL(dashboardUrl).pathname;
|
|
138
|
+
const siteUrl = deriveSiteUrl(dashboardUrl, new URL(dashboardUrl).origin);
|
|
139
|
+
// Asegurar que la página del dashboard está estable para leer el sesskey.
|
|
140
|
+
try {
|
|
141
|
+
await dashPage.waitForLoadState("domcontentloaded", { timeout: 15_000 });
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
/* ya cargada o navegando; los reintentos de abajo lo cubren */
|
|
145
|
+
}
|
|
146
|
+
// Extraer sesskey con reintentos (RequireJS lo inyecta async).
|
|
147
|
+
let sesskey = "";
|
|
148
|
+
for (const delay of [0, 800, 1600, 2500, 4000, 6000]) {
|
|
149
|
+
if (delay)
|
|
150
|
+
await sleep(delay);
|
|
151
|
+
try {
|
|
152
|
+
sesskey = String((await dashPage.evaluate(SESSKEY_SNIPPET)) ?? "").trim();
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
sesskey = "";
|
|
156
|
+
}
|
|
157
|
+
if (sesskey)
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
if (!sesskey) {
|
|
161
|
+
throw new Error("No se pudo extraer el sesskey del dashboard de Moodle.");
|
|
162
|
+
}
|
|
163
|
+
// Cookie MoodleSession: leer TODAS las cookies del contexto y elegir la autenticada
|
|
164
|
+
// (path "/{semestre}/"), evitando el filtro por path que dejaba fuera la cookie correcta.
|
|
165
|
+
const allCookies = await context.cookies();
|
|
166
|
+
const moodleCookie = pickMoodleCookie(allCookies, dashboardPath);
|
|
167
|
+
if (!moodleCookie?.value) {
|
|
168
|
+
throw new Error("No se encontró la cookie MoodleSession tras el login.");
|
|
169
|
+
}
|
|
170
|
+
const session = {
|
|
171
|
+
moodleSession: moodleCookie.value,
|
|
172
|
+
sesskey,
|
|
173
|
+
siteUrl,
|
|
174
|
+
capturedAt: Date.now(),
|
|
175
|
+
};
|
|
176
|
+
await saveSession(session);
|
|
177
|
+
onStatus(`Sesión capturada para ${siteUrl}`);
|
|
178
|
+
return session;
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
await context.close();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/** Comprueba si el host de trabajo es el esperado (defensa mínima). */
|
|
185
|
+
export function isExpectedHost(siteUrl) {
|
|
186
|
+
try {
|
|
187
|
+
return new URL(siteUrl).host === HOST;
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
//# sourceMappingURL=login.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/core/login.ts"],"names":[],"mappings":"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"}
|