moodle-cli 0.8.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -3
- package/SKILL.md +4 -3
- package/agents/openai.yaml +1 -1
- package/dist/moodle.js +2305 -844
- package/dist/worker/recovery.js +802 -385
- package/dist/worker/worker.js +802 -385
- package/package.json +2 -2
- package/references/command-reference.md +7 -2
package/dist/worker/worker.js
CHANGED
|
@@ -3582,7 +3582,7 @@ var require_parse = __commonJS({
|
|
|
3582
3582
|
var whitespace = /* @__PURE__ */ new Set([9, 10, 12, 13, 32]);
|
|
3583
3583
|
var ZERO = "0".charCodeAt(0);
|
|
3584
3584
|
var NINE = "9".charCodeAt(0);
|
|
3585
|
-
function
|
|
3585
|
+
function parse8(formula) {
|
|
3586
3586
|
formula = formula.trim().toLowerCase();
|
|
3587
3587
|
if (formula === "even") {
|
|
3588
3588
|
return [2, 0];
|
|
@@ -3634,7 +3634,7 @@ var require_parse = __commonJS({
|
|
|
3634
3634
|
}
|
|
3635
3635
|
}
|
|
3636
3636
|
}
|
|
3637
|
-
exports.parse =
|
|
3637
|
+
exports.parse = parse8;
|
|
3638
3638
|
}
|
|
3639
3639
|
});
|
|
3640
3640
|
|
|
@@ -5084,7 +5084,7 @@ var require_html = __commonJS({
|
|
|
5084
5084
|
}).join("");
|
|
5085
5085
|
}
|
|
5086
5086
|
set innerHTML(content) {
|
|
5087
|
-
const r =
|
|
5087
|
+
const r = parse8(content, this._parseOptions);
|
|
5088
5088
|
const nodes = r.childNodes.length ? r.childNodes : [new text_1.default(content, this)];
|
|
5089
5089
|
resetParent(nodes, this);
|
|
5090
5090
|
resetParent(this.childNodes, null);
|
|
@@ -5095,7 +5095,7 @@ var require_html = __commonJS({
|
|
|
5095
5095
|
content = [content];
|
|
5096
5096
|
} else if (typeof content == "string") {
|
|
5097
5097
|
options = Object.assign(Object.assign({}, this._parseOptions), options);
|
|
5098
|
-
const r =
|
|
5098
|
+
const r = parse8(content, options);
|
|
5099
5099
|
content = r.childNodes.length ? r.childNodes : [new text_1.default(r.innerHTML, this)];
|
|
5100
5100
|
}
|
|
5101
5101
|
resetParent(this.childNodes, null);
|
|
@@ -5109,7 +5109,7 @@ var require_html = __commonJS({
|
|
|
5109
5109
|
if (node2 instanceof node_1.default) {
|
|
5110
5110
|
return [node2];
|
|
5111
5111
|
} else if (typeof node2 == "string") {
|
|
5112
|
-
const r =
|
|
5112
|
+
const r = parse8(node2, this._parseOptions);
|
|
5113
5113
|
return r.childNodes.length ? r.childNodes : [new text_1.default(node2, this)];
|
|
5114
5114
|
}
|
|
5115
5115
|
return [];
|
|
@@ -5493,7 +5493,7 @@ var require_html = __commonJS({
|
|
|
5493
5493
|
if (arguments.length < 2) {
|
|
5494
5494
|
throw new Error("2 arguments required");
|
|
5495
5495
|
}
|
|
5496
|
-
const p =
|
|
5496
|
+
const p = parse8(html, this._parseOptions);
|
|
5497
5497
|
if (where === "afterend") {
|
|
5498
5498
|
this.after(...p.childNodes);
|
|
5499
5499
|
} else if (where === "afterbegin") {
|
|
@@ -5639,7 +5639,7 @@ var require_html = __commonJS({
|
|
|
5639
5639
|
}
|
|
5640
5640
|
/** Clone this Node */
|
|
5641
5641
|
clone() {
|
|
5642
|
-
return
|
|
5642
|
+
return parse8(this.toString(), this._parseOptions).firstChild;
|
|
5643
5643
|
}
|
|
5644
5644
|
};
|
|
5645
5645
|
exports.default = HTMLElement2;
|
|
@@ -5841,7 +5841,7 @@ var require_html = __commonJS({
|
|
|
5841
5841
|
return stack;
|
|
5842
5842
|
}
|
|
5843
5843
|
exports.base_parse = base_parse;
|
|
5844
|
-
function
|
|
5844
|
+
function parse8(data, options = {}) {
|
|
5845
5845
|
const stack = base_parse(data, options);
|
|
5846
5846
|
const [root] = stack;
|
|
5847
5847
|
while (stack.length > 1) {
|
|
@@ -5869,7 +5869,7 @@ var require_html = __commonJS({
|
|
|
5869
5869
|
}
|
|
5870
5870
|
return root;
|
|
5871
5871
|
}
|
|
5872
|
-
exports.parse =
|
|
5872
|
+
exports.parse = parse8;
|
|
5873
5873
|
function resolveInsertable(insertable) {
|
|
5874
5874
|
return insertable.map((val) => {
|
|
5875
5875
|
if (typeof val === "string") {
|
|
@@ -5937,18 +5937,18 @@ var require_dist = __commonJS({
|
|
|
5937
5937
|
var parse_1 = __importDefault(require_parse2());
|
|
5938
5938
|
var valid_1 = __importDefault(require_valid());
|
|
5939
5939
|
exports.valid = valid_1.default;
|
|
5940
|
-
function
|
|
5940
|
+
function parse8(data, options = {}) {
|
|
5941
5941
|
return (0, parse_1.default)(data, options);
|
|
5942
5942
|
}
|
|
5943
|
-
exports.default =
|
|
5944
|
-
exports.parse =
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
|
|
5948
|
-
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
|
|
5943
|
+
exports.default = parse8;
|
|
5944
|
+
exports.parse = parse8;
|
|
5945
|
+
parse8.parse = parse_1.default;
|
|
5946
|
+
parse8.HTMLElement = html_1.default;
|
|
5947
|
+
parse8.CommentNode = comment_1.default;
|
|
5948
|
+
parse8.valid = valid_1.default;
|
|
5949
|
+
parse8.Node = node_1.default;
|
|
5950
|
+
parse8.TextNode = text_1.default;
|
|
5951
|
+
parse8.NodeType = type_1.default;
|
|
5952
5952
|
}
|
|
5953
5953
|
});
|
|
5954
5954
|
|
|
@@ -6292,20 +6292,20 @@ function createOAuthRouter(options) {
|
|
|
6292
6292
|
async function handleAuthorizationCodeGrant(form) {
|
|
6293
6293
|
const code = form.get("code") ?? "";
|
|
6294
6294
|
const key = `${CODE_PREFIX}${await digestBearerToken(code)}`;
|
|
6295
|
-
const
|
|
6296
|
-
if (
|
|
6297
|
-
if (!
|
|
6295
|
+
const record3 = code ? await storage.get(key) : void 0;
|
|
6296
|
+
if (record3) await storage.delete(key);
|
|
6297
|
+
if (!record3 || record3.expiresAt <= now()) {
|
|
6298
6298
|
return oauthError(400, "invalid_grant", "The authorization code is invalid or expired.");
|
|
6299
6299
|
}
|
|
6300
|
-
if (form.get("client_id") !==
|
|
6300
|
+
if (form.get("client_id") !== record3.clientId) {
|
|
6301
6301
|
return oauthError(400, "invalid_grant", "The authorization code was issued to another client.");
|
|
6302
6302
|
}
|
|
6303
6303
|
const redirectUri = form.get("redirect_uri");
|
|
6304
|
-
if (redirectUri !== null && redirectUri !==
|
|
6304
|
+
if (redirectUri !== null && redirectUri !== record3.redirectUri) {
|
|
6305
6305
|
return oauthError(400, "invalid_grant", "The redirect URI does not match the authorization request.");
|
|
6306
6306
|
}
|
|
6307
6307
|
const verifier = form.get("code_verifier") ?? "";
|
|
6308
|
-
if (!/^[A-Za-z0-9._~-]{43,128}$/u.test(verifier) || await pkceChallenge(verifier) !==
|
|
6308
|
+
if (!/^[A-Za-z0-9._~-]{43,128}$/u.test(verifier) || await pkceChallenge(verifier) !== record3.codeChallenge) {
|
|
6309
6309
|
return oauthError(400, "invalid_grant", "The PKCE code verifier is invalid.");
|
|
6310
6310
|
}
|
|
6311
6311
|
const requestedResource = normalizeResource(form.get("resource"));
|
|
@@ -6313,9 +6313,9 @@ function createOAuthRouter(options) {
|
|
|
6313
6313
|
return oauthError(400, "invalid_target", "The requested resource is not hosted by this server.");
|
|
6314
6314
|
}
|
|
6315
6315
|
return issueTokens({
|
|
6316
|
-
clientId:
|
|
6316
|
+
clientId: record3.clientId,
|
|
6317
6317
|
resource: resourceUrl,
|
|
6318
|
-
scope:
|
|
6318
|
+
scope: record3.scope,
|
|
6319
6319
|
family: createSecret()
|
|
6320
6320
|
});
|
|
6321
6321
|
}
|
|
@@ -6325,33 +6325,33 @@ function createOAuthRouter(options) {
|
|
|
6325
6325
|
const requestedResource = normalizeResource(form.get("resource"));
|
|
6326
6326
|
if (requestedResource === void 0) return oauthError(400, "invalid_target", "The requested resource is not hosted by this server.");
|
|
6327
6327
|
const digest = await digestBearerToken(refreshToken);
|
|
6328
|
-
const
|
|
6329
|
-
if (!
|
|
6328
|
+
const record3 = await storage.get(`${REFRESH_PREFIX}${digest}`);
|
|
6329
|
+
if (!record3) {
|
|
6330
6330
|
const consumed = await storage.get(`${CONSUMED_REFRESH_PREFIX}${digest}`);
|
|
6331
6331
|
if (consumed) await revokeFamily(consumed.family);
|
|
6332
6332
|
return oauthError(400, "invalid_grant", "The refresh token is invalid or expired.");
|
|
6333
6333
|
}
|
|
6334
6334
|
await storage.delete(`${REFRESH_PREFIX}${digest}`);
|
|
6335
|
-
if (
|
|
6336
|
-
if (
|
|
6335
|
+
if (record3.resource !== resourceUrl) return oauthError(400, "invalid_grant", "The grant must be authorized again for this resource.");
|
|
6336
|
+
if (record3.expiresAt <= now()) return oauthError(400, "invalid_grant", "The refresh token is invalid or expired.");
|
|
6337
6337
|
const clientId = form.get("client_id");
|
|
6338
|
-
if (clientId !== null && clientId !==
|
|
6338
|
+
if (clientId !== null && clientId !== record3.clientId) {
|
|
6339
6339
|
return oauthError(400, "invalid_grant", "The refresh token was issued to another client.");
|
|
6340
6340
|
}
|
|
6341
6341
|
const requestedScope = form.get("scope");
|
|
6342
|
-
if (requestedScope && requestedScope.split(/\s+/u).some((scope) => scope && scope !==
|
|
6342
|
+
if (requestedScope && requestedScope.split(/\s+/u).some((scope) => scope && scope !== record3.scope)) {
|
|
6343
6343
|
return oauthError(400, "invalid_scope", "The requested scope exceeds the original grant.");
|
|
6344
6344
|
}
|
|
6345
6345
|
await storage.put(`${CONSUMED_REFRESH_PREFIX}${digest}`, {
|
|
6346
|
-
...
|
|
6346
|
+
...record3,
|
|
6347
6347
|
expiresAt: now() + CONSUMED_REFRESH_RETENTION_MS
|
|
6348
6348
|
});
|
|
6349
6349
|
await prune();
|
|
6350
6350
|
return issueTokens({
|
|
6351
|
-
clientId:
|
|
6352
|
-
resource:
|
|
6353
|
-
scope:
|
|
6354
|
-
family:
|
|
6351
|
+
clientId: record3.clientId,
|
|
6352
|
+
resource: record3.resource,
|
|
6353
|
+
scope: record3.scope,
|
|
6354
|
+
family: record3.family
|
|
6355
6355
|
});
|
|
6356
6356
|
}
|
|
6357
6357
|
async function handleRevoke(request) {
|
|
@@ -6363,8 +6363,8 @@ function createOAuthRouter(options) {
|
|
|
6363
6363
|
if (token) {
|
|
6364
6364
|
const digest = await digestBearerToken(token);
|
|
6365
6365
|
for (const prefix of [ACCESS_PREFIX, REFRESH_PREFIX]) {
|
|
6366
|
-
const
|
|
6367
|
-
if (
|
|
6366
|
+
const record3 = await storage.get(`${prefix}${digest}`);
|
|
6367
|
+
if (record3) await revokeFamily(record3.family);
|
|
6368
6368
|
}
|
|
6369
6369
|
}
|
|
6370
6370
|
return new Response(null, { status: 200, headers: { "cache-control": "no-store" } });
|
|
@@ -6424,8 +6424,8 @@ function createOAuthRouter(options) {
|
|
|
6424
6424
|
async revokeClients(clientId) {
|
|
6425
6425
|
await storage.delete(PAIRING_KEY);
|
|
6426
6426
|
for (const prefix of [CLIENT_PREFIX, CODE_PREFIX, ACCESS_PREFIX, REFRESH_PREFIX, CONSUMED_REFRESH_PREFIX]) {
|
|
6427
|
-
for (const [key,
|
|
6428
|
-
if (!clientId ||
|
|
6427
|
+
for (const [key, record3] of await storage.list({ prefix })) {
|
|
6428
|
+
if (!clientId || record3.clientId === clientId) await storage.delete(key);
|
|
6429
6429
|
}
|
|
6430
6430
|
}
|
|
6431
6431
|
},
|
|
@@ -6442,14 +6442,14 @@ function createOAuthRouter(options) {
|
|
|
6442
6442
|
async verifyAccessToken(token, resource) {
|
|
6443
6443
|
if (!token) return null;
|
|
6444
6444
|
const key = `${ACCESS_PREFIX}${await digestBearerToken(token)}`;
|
|
6445
|
-
const
|
|
6446
|
-
if (!
|
|
6447
|
-
if (
|
|
6445
|
+
const record3 = await storage.get(key);
|
|
6446
|
+
if (!record3) return null;
|
|
6447
|
+
if (record3.expiresAt <= now()) {
|
|
6448
6448
|
await storage.delete(key);
|
|
6449
6449
|
return null;
|
|
6450
6450
|
}
|
|
6451
|
-
if (
|
|
6452
|
-
return { clientId:
|
|
6451
|
+
if (record3.resource !== resourceUrl || resource !== resourceUrl) return null;
|
|
6452
|
+
return { clientId: record3.clientId, scope: record3.scope };
|
|
6453
6453
|
}
|
|
6454
6454
|
};
|
|
6455
6455
|
}
|
|
@@ -8529,18 +8529,18 @@ var validateAsync = async (schema2, value, _ctx) => {
|
|
|
8529
8529
|
return result.issues.length === 0;
|
|
8530
8530
|
};
|
|
8531
8531
|
var _encode = (_Err) => {
|
|
8532
|
-
const
|
|
8532
|
+
const parse8 = _parse(_Err);
|
|
8533
8533
|
const fn = (schema2, value, _ctx, _params) => {
|
|
8534
8534
|
const ctx = _ctx ? { ..._ctx, direction: "backward" } : { direction: "backward" };
|
|
8535
|
-
return
|
|
8535
|
+
return parse8(schema2, value, ctx, finalizeParams(fn, _params));
|
|
8536
8536
|
};
|
|
8537
8537
|
return fn;
|
|
8538
8538
|
};
|
|
8539
8539
|
var encode = /* @__PURE__ */ _encode($ZodRealError);
|
|
8540
8540
|
var _decode = (_Err) => {
|
|
8541
|
-
const
|
|
8541
|
+
const parse8 = _parse(_Err);
|
|
8542
8542
|
const fn = (schema2, value, _ctx, _params) => {
|
|
8543
|
-
return
|
|
8543
|
+
return parse8(schema2, value, _ctx, finalizeParams(fn, _params));
|
|
8544
8544
|
};
|
|
8545
8545
|
return fn;
|
|
8546
8546
|
};
|
|
@@ -8934,8 +8934,8 @@ var $ZodCheckMaxSize = /* @__PURE__ */ $constructor("$ZodCheckMaxSize", (inst, d
|
|
|
8934
8934
|
(_a3 = inst._zod.def).when ?? (_a3.when = _whenHasSize);
|
|
8935
8935
|
inst._zod.check = (payload) => {
|
|
8936
8936
|
const input3 = payload.value;
|
|
8937
|
-
const
|
|
8938
|
-
if (
|
|
8937
|
+
const size2 = input3.size;
|
|
8938
|
+
if (size2 <= def.maximum)
|
|
8939
8939
|
return;
|
|
8940
8940
|
payload.issues.push({
|
|
8941
8941
|
origin: getSizableOrigin(input3),
|
|
@@ -8954,8 +8954,8 @@ var $ZodCheckMinSize = /* @__PURE__ */ $constructor("$ZodCheckMinSize", (inst, d
|
|
|
8954
8954
|
(_a3 = inst._zod.def).when ?? (_a3.when = _whenHasSize);
|
|
8955
8955
|
inst._zod.check = (payload) => {
|
|
8956
8956
|
const input3 = payload.value;
|
|
8957
|
-
const
|
|
8958
|
-
if (
|
|
8957
|
+
const size2 = input3.size;
|
|
8958
|
+
if (size2 >= def.minimum)
|
|
8959
8959
|
return;
|
|
8960
8960
|
payload.issues.push({
|
|
8961
8961
|
origin: getSizableOrigin(input3),
|
|
@@ -8974,10 +8974,10 @@ var $ZodCheckSizeEquals = /* @__PURE__ */ $constructor("$ZodCheckSizeEquals", (i
|
|
|
8974
8974
|
(_a3 = inst._zod.def).when ?? (_a3.when = _whenHasSize);
|
|
8975
8975
|
inst._zod.check = (payload) => {
|
|
8976
8976
|
const input3 = payload.value;
|
|
8977
|
-
const
|
|
8978
|
-
if (
|
|
8977
|
+
const size2 = input3.size;
|
|
8978
|
+
if (size2 === def.size)
|
|
8979
8979
|
return;
|
|
8980
|
-
const tooBig =
|
|
8980
|
+
const tooBig = size2 > def.size;
|
|
8981
8981
|
payload.issues.push({
|
|
8982
8982
|
origin: getSizableOrigin(input3),
|
|
8983
8983
|
...tooBig ? { code: "too_big", maximum: def.size } : { code: "too_small", minimum: def.size },
|
|
@@ -21713,11 +21713,11 @@ function _minSize(minimum, params) {
|
|
|
21713
21713
|
});
|
|
21714
21714
|
}
|
|
21715
21715
|
// @__NO_SIDE_EFFECTS__
|
|
21716
|
-
function _size(
|
|
21716
|
+
function _size(size2, params) {
|
|
21717
21717
|
return new $ZodCheckSizeEquals({
|
|
21718
21718
|
check: "size_equals",
|
|
21719
21719
|
...normalizeParams(params),
|
|
21720
|
-
size
|
|
21720
|
+
size: size2
|
|
21721
21721
|
});
|
|
21722
21722
|
}
|
|
21723
21723
|
// @__NO_SIDE_EFFECTS__
|
|
@@ -23274,8 +23274,8 @@ function rewriteKeyNames(ctx) {
|
|
|
23274
23274
|
bySchema.set(entry.schema, entry);
|
|
23275
23275
|
}
|
|
23276
23276
|
const rewrites = /* @__PURE__ */ new Map();
|
|
23277
|
-
for (const
|
|
23278
|
-
const seen = ctx.seen.get(
|
|
23277
|
+
for (const record3 of pendingRecords.get(ctx) ?? []) {
|
|
23278
|
+
const seen = ctx.seen.get(record3);
|
|
23279
23279
|
const names = (seen?.def ?? seen?.schema)?.propertyNames;
|
|
23280
23280
|
if (!names || names === true || rewrites.has(names))
|
|
23281
23281
|
continue;
|
|
@@ -24992,8 +24992,8 @@ var ZodFile = /* @__PURE__ */ $constructor("ZodFile", (inst, def) => {
|
|
|
24992
24992
|
$ZodFile.init(inst, def);
|
|
24993
24993
|
ZodType.init(inst, def);
|
|
24994
24994
|
inst._zod.processJSONSchema = (ctx, json2, params) => fileProcessor(inst, ctx, json2, params);
|
|
24995
|
-
inst.min = (
|
|
24996
|
-
inst.max = (
|
|
24995
|
+
inst.min = (size2, params) => inst.check(_minSize(size2, params));
|
|
24996
|
+
inst.max = (size2, params) => inst.check(_maxSize(size2, params));
|
|
24997
24997
|
inst.mime = (types, params) => inst.check(_mime(Array.isArray(types) ? types : [types], params));
|
|
24998
24998
|
});
|
|
24999
24999
|
function file(params) {
|
|
@@ -26452,7 +26452,7 @@ function stringValue(value) {
|
|
|
26452
26452
|
}
|
|
26453
26453
|
|
|
26454
26454
|
// src/version.ts
|
|
26455
|
-
var VERSION = "0.
|
|
26455
|
+
var VERSION = "0.9.1";
|
|
26456
26456
|
|
|
26457
26457
|
// src/worker/http.ts
|
|
26458
26458
|
var HEALTH_PATH = "/healthz";
|
|
@@ -27004,7 +27004,12 @@ async function fetchWithSession(input3, init, moodleOrigin, cookie, fetchImpl =
|
|
|
27004
27004
|
headers.delete("authorization");
|
|
27005
27005
|
headers.delete("proxy-authorization");
|
|
27006
27006
|
}
|
|
27007
|
-
|
|
27007
|
+
let response;
|
|
27008
|
+
try {
|
|
27009
|
+
response = await fetchImpl(url2.toString(), { ...init, method, body, headers: Object.fromEntries(headers), signal, redirect: "manual" });
|
|
27010
|
+
} catch (error62) {
|
|
27011
|
+
throw requestFailure(error62, url2, deadline);
|
|
27012
|
+
}
|
|
27008
27013
|
if (!REDIRECT_STATUSES.has(response.status)) return response;
|
|
27009
27014
|
const location = response.headers.get("location");
|
|
27010
27015
|
if (!location) return response;
|
|
@@ -27025,6 +27030,25 @@ async function fetchWithSession(input3, init, moodleOrigin, cookie, fetchImpl =
|
|
|
27025
27030
|
url2 = next;
|
|
27026
27031
|
}
|
|
27027
27032
|
}
|
|
27033
|
+
var RequestFailed = class extends Error {
|
|
27034
|
+
host;
|
|
27035
|
+
timedOut;
|
|
27036
|
+
constructor(host, timedOut, cause) {
|
|
27037
|
+
super(timedOut ? `${host} did not respond within ${REQUEST_TIMEOUT_MS / 1e3}s.` : `Could not reach ${host}: ${cause}`);
|
|
27038
|
+
this.name = "RequestFailed";
|
|
27039
|
+
this.host = host;
|
|
27040
|
+
this.timedOut = timedOut;
|
|
27041
|
+
}
|
|
27042
|
+
};
|
|
27043
|
+
function requestFailure(error62, url2, deadline) {
|
|
27044
|
+
if (deadline.aborted) {
|
|
27045
|
+
return new RequestFailed(url2.host, true);
|
|
27046
|
+
}
|
|
27047
|
+
if (!(error62 instanceof Error)) {
|
|
27048
|
+
return error62;
|
|
27049
|
+
}
|
|
27050
|
+
return new RequestFailed(url2.host, false, error62.cause instanceof Error ? error62.cause.message : error62.message);
|
|
27051
|
+
}
|
|
27028
27052
|
|
|
27029
27053
|
// src/constants.ts
|
|
27030
27054
|
var PACKAGE_NAME = "moodle-cli";
|
|
@@ -27057,6 +27081,9 @@ var FUNC_GET_UNREAD_CONVERSATION_COUNTS = "core_message_get_unread_conversation_
|
|
|
27057
27081
|
var FUNC_GET_DISCUSSION_POSTS = "mod_forum_get_discussion_posts";
|
|
27058
27082
|
var DEFAULT_SESSION_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
27059
27083
|
|
|
27084
|
+
// src/moodle-assign-core.ts
|
|
27085
|
+
var import_node_html_parser3 = __toESM(require_dist(), 1);
|
|
27086
|
+
|
|
27060
27087
|
// src/html-utils.ts
|
|
27061
27088
|
var import_node_html_parser = __toESM(require_dist(), 1);
|
|
27062
27089
|
function htmlToStructuredContent(html, baseUrl) {
|
|
@@ -27126,262 +27153,6 @@ function decodeHtml(value) {
|
|
|
27126
27153
|
return value.replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'");
|
|
27127
27154
|
}
|
|
27128
27155
|
|
|
27129
|
-
// src/parsers.ts
|
|
27130
|
-
function schema(parser) {
|
|
27131
|
-
return { parse: parser };
|
|
27132
|
-
}
|
|
27133
|
-
var UserInfoSchema = schema(parseUserInfo);
|
|
27134
|
-
var CourseSchema = schema(parseCourse);
|
|
27135
|
-
var CoursesSchema = schema(parseCourses);
|
|
27136
|
-
var ActivitySchema = schema(parseActivity);
|
|
27137
|
-
var SectionSchema = schema(parseSection);
|
|
27138
|
-
var CourseContentsSchema = schema(parseCourseContents);
|
|
27139
|
-
var TodoItemSchema = schema(parseTodoItem);
|
|
27140
|
-
function parseUserInfo(value) {
|
|
27141
|
-
const data = asRecord(value);
|
|
27142
|
-
return {
|
|
27143
|
-
userid: numberValue(data.userid),
|
|
27144
|
-
username: stringValue2(data.username),
|
|
27145
|
-
fullname: stringValue2(data.fullname),
|
|
27146
|
-
sitename: stringValue2(data.sitename),
|
|
27147
|
-
siteurl: stringValue2(data.siteurl),
|
|
27148
|
-
lang: stringValue2(data.lang),
|
|
27149
|
-
...data.timezone ? { timezone: String(data.timezone) } : {}
|
|
27150
|
-
};
|
|
27151
|
-
}
|
|
27152
|
-
function parseCourse(value, nowSeconds = Math.floor(Date.now() / 1e3)) {
|
|
27153
|
-
const data = asRecord(value);
|
|
27154
|
-
const course = {
|
|
27155
|
-
id: numberValue(data.id),
|
|
27156
|
-
shortname: stringValue2(data.shortname),
|
|
27157
|
-
fullname: stringValue2(data.fullname),
|
|
27158
|
-
category: numberValue(data.category),
|
|
27159
|
-
visible: booleanValue(data.visible, true),
|
|
27160
|
-
startdate: numberValue(data.startdate)
|
|
27161
|
-
};
|
|
27162
|
-
const enddate = numberValue(data.enddate);
|
|
27163
|
-
if (enddate > 0) {
|
|
27164
|
-
course.enddate = enddate;
|
|
27165
|
-
}
|
|
27166
|
-
return course;
|
|
27167
|
-
}
|
|
27168
|
-
function parseCourses(value) {
|
|
27169
|
-
return asArray(value).map((item) => parseCourse(item));
|
|
27170
|
-
}
|
|
27171
|
-
function parseActivity(value) {
|
|
27172
|
-
const data = asRecord(value);
|
|
27173
|
-
return {
|
|
27174
|
-
id: numberValue(data.id),
|
|
27175
|
-
name: stringValue2(data.name),
|
|
27176
|
-
modname: stringValue2(data.modname),
|
|
27177
|
-
url: stringValue2(data.url),
|
|
27178
|
-
visible: booleanValue(data.visible, true),
|
|
27179
|
-
description: stringValue2(data.description),
|
|
27180
|
-
...data.completiondata && typeof data.completiondata === "object" ? { completion: numberValue(asRecord(data.completiondata).state) } : {},
|
|
27181
|
-
...Array.isArray(data.contents) ? { file_entries: data.contents.filter((f) => asRecord(f).fileurl).map((f) => ({ name: stringValue2(asRecord(f).filename), url: stringValue2(asRecord(f).fileurl), requires_authentication: true })) } : {}
|
|
27182
|
-
};
|
|
27183
|
-
}
|
|
27184
|
-
function parseSection(value) {
|
|
27185
|
-
const data = asRecord(value);
|
|
27186
|
-
return {
|
|
27187
|
-
id: numberValue(data.id),
|
|
27188
|
-
name: stringValue2(data.name),
|
|
27189
|
-
section: numberValue(data.section),
|
|
27190
|
-
visible: booleanValue(data.visible, true),
|
|
27191
|
-
summary: stringValue2(data.summary),
|
|
27192
|
-
...data.current !== void 0 ? { current: booleanValue(data.current) } : {},
|
|
27193
|
-
activities: asArray(data.modules).map((item) => parseActivity(item))
|
|
27194
|
-
};
|
|
27195
|
-
}
|
|
27196
|
-
function parseCourseContents(value) {
|
|
27197
|
-
return asArray(value).map((item) => parseSection(item));
|
|
27198
|
-
}
|
|
27199
|
-
function parseCourseFormatState(value, baseUrl) {
|
|
27200
|
-
const state = asRecord(parseJsonValue(value));
|
|
27201
|
-
const activities = /* @__PURE__ */ new Map();
|
|
27202
|
-
const activitiesBySection = /* @__PURE__ */ new Map();
|
|
27203
|
-
for (const item of asArray(state.cm)) {
|
|
27204
|
-
const data = asRecord(item);
|
|
27205
|
-
const id2 = numberValue(data.id);
|
|
27206
|
-
const sectionId = stringValue2(data.sectionid);
|
|
27207
|
-
const module = stringValue2(data.module) || stringValue2(data.plugin).replace(/^mod_/u, "") || stringValue2(data.modname).toLowerCase();
|
|
27208
|
-
const activity = {
|
|
27209
|
-
id: id2,
|
|
27210
|
-
name: htmlText(data.name, baseUrl),
|
|
27211
|
-
modname: module.toLowerCase(),
|
|
27212
|
-
url: stringValue2(data.url) ? resolveUrl(baseUrl, stringValue2(data.url)) : "",
|
|
27213
|
-
visible: booleanValue(data.visible, true) && booleanValue(data.uservisible, true) && !booleanValue(data.stealth),
|
|
27214
|
-
description: htmlText(data.content ?? data.description, baseUrl),
|
|
27215
|
-
...data.completionstate !== void 0 && data.completionstate !== null ? { completion: numberValue(data.completionstate) } : {}
|
|
27216
|
-
};
|
|
27217
|
-
activities.set(String(id2), activity);
|
|
27218
|
-
const sectionActivities = activitiesBySection.get(sectionId) ?? [];
|
|
27219
|
-
sectionActivities.push(activity);
|
|
27220
|
-
activitiesBySection.set(sectionId, sectionActivities);
|
|
27221
|
-
}
|
|
27222
|
-
return asArray(state.section).map((item) => {
|
|
27223
|
-
const data = asRecord(item);
|
|
27224
|
-
const id2 = numberValue(data.id);
|
|
27225
|
-
const hasActivityList = Array.isArray(data.cmlist);
|
|
27226
|
-
const listedActivities = asArray(data.cmlist).map((activityId) => activities.get(stringValue2(activityId))).filter((activity) => activity !== void 0);
|
|
27227
|
-
return {
|
|
27228
|
-
id: id2,
|
|
27229
|
-
name: htmlText(data.title || data.rawtitle, baseUrl),
|
|
27230
|
-
section: numberValue(data.section ?? data.number),
|
|
27231
|
-
visible: booleanValue(data.visible, true),
|
|
27232
|
-
summary: htmlText(data.summary, baseUrl),
|
|
27233
|
-
...data.current !== void 0 ? { current: booleanValue(data.current) } : {},
|
|
27234
|
-
activities: hasActivityList ? listedActivities : activitiesBySection.get(String(id2)) ?? []
|
|
27235
|
-
};
|
|
27236
|
-
});
|
|
27237
|
-
}
|
|
27238
|
-
function parseTodoItem(value) {
|
|
27239
|
-
const data = asRecord(value);
|
|
27240
|
-
const course = asRecord(data.course);
|
|
27241
|
-
const action = asRecord(data.action);
|
|
27242
|
-
const progress = course.progress;
|
|
27243
|
-
return {
|
|
27244
|
-
id: numberValue(data.id),
|
|
27245
|
-
name: stringValue2(data.name),
|
|
27246
|
-
activity_name: stringValue2(data.activityname),
|
|
27247
|
-
modname: stringValue2(data.modulename),
|
|
27248
|
-
course_id: numberValue(course.id),
|
|
27249
|
-
course_name: stringValue2(course.fullname),
|
|
27250
|
-
due_at: numberValue(data.timesort) || numberValue(data.timestart),
|
|
27251
|
-
overdue: booleanValue(data.overdue),
|
|
27252
|
-
actionable: booleanValue(action.actionable),
|
|
27253
|
-
action_name: stringValue2(action.name),
|
|
27254
|
-
action_url: stringValue2(action.url),
|
|
27255
|
-
url: stringValue2(data.url),
|
|
27256
|
-
event_type: stringValue2(data.eventtype),
|
|
27257
|
-
course_progress: typeof progress === "number" ? progress : void 0
|
|
27258
|
-
};
|
|
27259
|
-
}
|
|
27260
|
-
function parseTodoItems(value) {
|
|
27261
|
-
return asArray(value).map((item) => parseTodoItem(item));
|
|
27262
|
-
}
|
|
27263
|
-
function parseAlertNotification(value) {
|
|
27264
|
-
const data = asRecord(value);
|
|
27265
|
-
return {
|
|
27266
|
-
id: numberValue(data.id),
|
|
27267
|
-
subject: stringValue2(data.subject),
|
|
27268
|
-
short_subject: stringValue2(data.shortenedsubject),
|
|
27269
|
-
event_type: stringValue2(data.eventtype),
|
|
27270
|
-
component: stringValue2(data.component),
|
|
27271
|
-
created_at: numberValue(data.timecreated),
|
|
27272
|
-
created_pretty: stringValue2(data.timecreatedpretty),
|
|
27273
|
-
read: booleanValue(data.read),
|
|
27274
|
-
context_url: stringValue2(data.contexturl),
|
|
27275
|
-
context_name: stringValue2(data.contexturlname)
|
|
27276
|
-
};
|
|
27277
|
-
}
|
|
27278
|
-
function parseAlertSummary(notificationsData, countsData, unreadCountsData) {
|
|
27279
|
-
const notificationsRecord = asRecord(notificationsData);
|
|
27280
|
-
const counts2 = asRecord(countsData);
|
|
27281
|
-
const unreadCounts = asRecord(unreadCountsData);
|
|
27282
|
-
const types = asRecord(counts2.types);
|
|
27283
|
-
const unreadTypes = asRecord(unreadCounts.types);
|
|
27284
|
-
const notifications = asArray(notificationsRecord.notifications).map((item) => parseAlertNotification(item));
|
|
27285
|
-
return {
|
|
27286
|
-
notifications,
|
|
27287
|
-
notification_count: notifications.length,
|
|
27288
|
-
unread_notification_count: notifications.filter((notification) => !notification.read).length,
|
|
27289
|
-
starred_message_count: numberValue(counts2.favourites),
|
|
27290
|
-
direct_message_count: numberValue(types["1"]),
|
|
27291
|
-
group_message_count: numberValue(types["2"]),
|
|
27292
|
-
self_message_count: numberValue(types["3"]),
|
|
27293
|
-
unread_starred_message_count: numberValue(unreadCounts.favourites),
|
|
27294
|
-
unread_direct_message_count: numberValue(unreadTypes["1"]),
|
|
27295
|
-
unread_group_message_count: numberValue(unreadTypes["2"]),
|
|
27296
|
-
unread_self_message_count: numberValue(unreadTypes["3"])
|
|
27297
|
-
};
|
|
27298
|
-
}
|
|
27299
|
-
function parseForumPostAuthor(value) {
|
|
27300
|
-
const data = asRecord(value);
|
|
27301
|
-
const urls = asRecord(data.urls);
|
|
27302
|
-
return {
|
|
27303
|
-
id: numberValue(data.id),
|
|
27304
|
-
fullname: stringValue2(data.fullname),
|
|
27305
|
-
profile_url: stringValue2(urls.profile),
|
|
27306
|
-
profile_image_url: stringValue2(urls.profileimage)
|
|
27307
|
-
};
|
|
27308
|
-
}
|
|
27309
|
-
function parseForumPost(value, baseUrl = "") {
|
|
27310
|
-
const data = asRecord(value);
|
|
27311
|
-
const urls = asRecord(data.urls);
|
|
27312
|
-
const messageHtml = stringValue2(data.message);
|
|
27313
|
-
const structured = htmlToStructuredContent(messageHtml, stringValue2(urls.view || urls.discuss) || baseUrl);
|
|
27314
|
-
return {
|
|
27315
|
-
id: numberValue(data.id),
|
|
27316
|
-
discussion_id: numberValue(data.discussionid),
|
|
27317
|
-
subject: stringValue2(data.subject),
|
|
27318
|
-
message_html: messageHtml,
|
|
27319
|
-
message_text: structured.text,
|
|
27320
|
-
image_urls: structured.image_urls,
|
|
27321
|
-
links: structured.links,
|
|
27322
|
-
tables: structured.tables,
|
|
27323
|
-
author: parseForumPostAuthor(data.author),
|
|
27324
|
-
parent_id: numberValue(data.parentid),
|
|
27325
|
-
time_created: numberValue(data.timecreated),
|
|
27326
|
-
time_modified: numberValue(data.timemodified),
|
|
27327
|
-
created_pretty: "",
|
|
27328
|
-
unread: booleanValue(data.unread),
|
|
27329
|
-
is_deleted: booleanValue(data.isdeleted),
|
|
27330
|
-
is_private_reply: booleanValue(data.isprivatereply),
|
|
27331
|
-
url: stringValue2(urls.view || urls.viewisolated),
|
|
27332
|
-
reply_url: stringValue2(urls.reply)
|
|
27333
|
-
};
|
|
27334
|
-
}
|
|
27335
|
-
function parseForumDiscussion(value, discussionId, baseUrl = "") {
|
|
27336
|
-
const data = asRecord(value);
|
|
27337
|
-
const posts = asArray(data.posts).map((item) => parseForumPost(item, baseUrl));
|
|
27338
|
-
return {
|
|
27339
|
-
id: discussionId,
|
|
27340
|
-
subject: posts[0]?.subject ?? "",
|
|
27341
|
-
course_id: numberValue(data.courseid),
|
|
27342
|
-
forum_id: numberValue(data.forumid),
|
|
27343
|
-
group_id: numberValue(data.groupid),
|
|
27344
|
-
group_name: stringValue2(data.groupname),
|
|
27345
|
-
url: posts[0]?.url ? posts[0].url.split("#", 1)[0] : "",
|
|
27346
|
-
posts
|
|
27347
|
-
};
|
|
27348
|
-
}
|
|
27349
|
-
function asRecord(value) {
|
|
27350
|
-
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
27351
|
-
}
|
|
27352
|
-
function asArray(value) {
|
|
27353
|
-
return Array.isArray(value) ? value : [];
|
|
27354
|
-
}
|
|
27355
|
-
function stringValue2(value) {
|
|
27356
|
-
return typeof value === "string" ? value : value == null ? "" : String(value);
|
|
27357
|
-
}
|
|
27358
|
-
function numberValue(value) {
|
|
27359
|
-
if (typeof value === "number" && Number.isFinite(value)) {
|
|
27360
|
-
return value;
|
|
27361
|
-
}
|
|
27362
|
-
if (typeof value === "string" && value.trim() !== "" && Number.isFinite(Number(value))) {
|
|
27363
|
-
return Number(value);
|
|
27364
|
-
}
|
|
27365
|
-
return 0;
|
|
27366
|
-
}
|
|
27367
|
-
function booleanValue(value, defaultValue = false) {
|
|
27368
|
-
if (value === void 0 || value === null) {
|
|
27369
|
-
return defaultValue;
|
|
27370
|
-
}
|
|
27371
|
-
return Boolean(value);
|
|
27372
|
-
}
|
|
27373
|
-
function parseJsonValue(value) {
|
|
27374
|
-
if (typeof value !== "string") return value;
|
|
27375
|
-
try {
|
|
27376
|
-
return JSON.parse(value);
|
|
27377
|
-
} catch {
|
|
27378
|
-
return {};
|
|
27379
|
-
}
|
|
27380
|
-
}
|
|
27381
|
-
function htmlText(value, baseUrl) {
|
|
27382
|
-
return htmlToStructuredContent(stringValue2(value), baseUrl).text;
|
|
27383
|
-
}
|
|
27384
|
-
|
|
27385
27156
|
// src/scraper.ts
|
|
27386
27157
|
var import_node_html_parser2 = __toESM(require_dist(), 1);
|
|
27387
27158
|
function parseMoodleErrorHtml(html) {
|
|
@@ -27413,8 +27184,8 @@ function parseMoodleErrorHtml(html) {
|
|
|
27413
27184
|
function parsePageContext(html, baseUrl) {
|
|
27414
27185
|
const root = (0, import_node_html_parser2.parse)(html);
|
|
27415
27186
|
const config2 = parseMoodleConfig(html);
|
|
27416
|
-
const sesskey =
|
|
27417
|
-
const userid =
|
|
27187
|
+
const sesskey = stringValue2(config2.sesskey).trim();
|
|
27188
|
+
const userid = numberValue(config2.userId) || numberValue(root.querySelector("[data-user-id]")?.getAttribute("data-user-id"));
|
|
27418
27189
|
if (!sesskey || !userid) {
|
|
27419
27190
|
throw new Error("Session appears invalid: could not load authenticated Moodle context");
|
|
27420
27191
|
}
|
|
@@ -27426,8 +27197,8 @@ function parsePageContext(html, baseUrl) {
|
|
|
27426
27197
|
fullname: cleanNodeText(root.querySelector(".userfullname")),
|
|
27427
27198
|
sitename: extractSitename(root),
|
|
27428
27199
|
siteurl: baseUrl,
|
|
27429
|
-
...config2.timezone ? { timezone:
|
|
27430
|
-
lang:
|
|
27200
|
+
...config2.timezone ? { timezone: stringValue2(config2.timezone) } : {},
|
|
27201
|
+
lang: stringValue2(config2.language) || root.querySelector("html")?.getAttribute("lang") || ""
|
|
27431
27202
|
}
|
|
27432
27203
|
};
|
|
27433
27204
|
}
|
|
@@ -27967,7 +27738,7 @@ function numberQueryValue(href, key) {
|
|
|
27967
27738
|
return null;
|
|
27968
27739
|
}
|
|
27969
27740
|
}
|
|
27970
|
-
function
|
|
27741
|
+
function numberValue(value) {
|
|
27971
27742
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
27972
27743
|
return value;
|
|
27973
27744
|
}
|
|
@@ -27976,7 +27747,7 @@ function numberValue2(value) {
|
|
|
27976
27747
|
}
|
|
27977
27748
|
return 0;
|
|
27978
27749
|
}
|
|
27979
|
-
function
|
|
27750
|
+
function stringValue2(value) {
|
|
27980
27751
|
return typeof value === "string" ? value : value == null ? "" : String(value);
|
|
27981
27752
|
}
|
|
27982
27753
|
function fileEntry(name, url2, baseUrl) {
|
|
@@ -27990,6 +27761,619 @@ function unique(items) {
|
|
|
27990
27761
|
return [...new Set(items)];
|
|
27991
27762
|
}
|
|
27992
27763
|
|
|
27764
|
+
// src/moodle-assign-core.ts
|
|
27765
|
+
async function submitAssignmentFiles(deps, request) {
|
|
27766
|
+
const id2 = request.activityId;
|
|
27767
|
+
if (!Number.isSafeInteger(id2) || id2 <= 0) throw deps.usage("The assignment id must be a positive integer.");
|
|
27768
|
+
if (!request.files.length && !request.final) throw deps.usage("Give at least one file to upload, or use --final to submit the existing draft.");
|
|
27769
|
+
const seen = /* @__PURE__ */ new Set();
|
|
27770
|
+
for (const file3 of request.files) {
|
|
27771
|
+
if (!file3.name || /[\\/]/u.test(file3.name)) throw deps.usage(`'${file3.name}' is not a plain file name.`);
|
|
27772
|
+
if (seen.has(file3.name.toLowerCase())) throw deps.usage(`'${file3.name}' is given twice; Moodle keeps one file per name.`);
|
|
27773
|
+
seen.add(file3.name.toLowerCase());
|
|
27774
|
+
}
|
|
27775
|
+
const viewUrl = `${deps.baseUrl}${ASSIGN_VIEW_PATH}?id=${id2}`;
|
|
27776
|
+
request.onProgress?.("Reading the assignment");
|
|
27777
|
+
const before = parseReceiptPage(await pageText(deps, viewUrl), id2, deps.baseUrl);
|
|
27778
|
+
const form = parseSubmissionForm(await pageText(deps, `${viewUrl}&action=editsubmission`), deps);
|
|
27779
|
+
const draft = await listDraftFiles(deps, form);
|
|
27780
|
+
const removed = request.replace ? draft.map((file3) => file3.name) : [];
|
|
27781
|
+
const kept = request.replace ? [] : draft.filter((file3) => !seen.has(file3.name.toLowerCase()));
|
|
27782
|
+
checkLimits(deps, form, kept, request.files);
|
|
27783
|
+
let statement = form.statement;
|
|
27784
|
+
let confirm;
|
|
27785
|
+
if (request.final && !statement) {
|
|
27786
|
+
confirm = parseConfirmForm(await pageText(deps, `${viewUrl}&action=submit`), deps);
|
|
27787
|
+
statement = confirm.statement;
|
|
27788
|
+
}
|
|
27789
|
+
if (statement && !request.acceptStatement) {
|
|
27790
|
+
throw deps.usage(`Moodle requires you to accept this statement: "${statement}"`, "Re-run with --accept-statement once you agree.");
|
|
27791
|
+
}
|
|
27792
|
+
const limits = describeLimits(form);
|
|
27793
|
+
const uploads = request.files.map((file3) => ({ name: file3.name, bytes: file3.bytes.byteLength, ...file3.path ? { path: file3.path } : {} }));
|
|
27794
|
+
if (request.dryRun) {
|
|
27795
|
+
return {
|
|
27796
|
+
...before,
|
|
27797
|
+
action: "planned",
|
|
27798
|
+
files: draft.map((file3) => ({ name: file3.name, bytes: file3.bytes })),
|
|
27799
|
+
uploads,
|
|
27800
|
+
removed,
|
|
27801
|
+
limits,
|
|
27802
|
+
...statement ? { statement, statement_accepted: true } : {},
|
|
27803
|
+
checked_at: timestamp(deps)
|
|
27804
|
+
};
|
|
27805
|
+
}
|
|
27806
|
+
if (removed.length) {
|
|
27807
|
+
request.onProgress?.(`Removing ${removed.join(", ")}`);
|
|
27808
|
+
await deleteDraftFiles(deps, form, draft);
|
|
27809
|
+
}
|
|
27810
|
+
const storedNames = [];
|
|
27811
|
+
for (const [index, file3] of request.files.entries()) {
|
|
27812
|
+
request.onProgress?.(`Uploading ${file3.name} (${index + 1}/${request.files.length})`);
|
|
27813
|
+
storedNames.push(await uploadDraftFile(deps, form, file3));
|
|
27814
|
+
}
|
|
27815
|
+
if (storedNames.length || removed.length) {
|
|
27816
|
+
request.onProgress?.("Saving the submission");
|
|
27817
|
+
const savedHtml = await postForm(deps, form.action, [...form.fields, ...form.statement ? [["submissionstatement", "1"]] : [], ["submitbutton", "Save changes"]]);
|
|
27818
|
+
if (savedHtml !== null) throw deps.fail(`Moodle did not save the submission: ${noticesOf(savedHtml) || "it returned the edit form again without a reason"}`);
|
|
27819
|
+
}
|
|
27820
|
+
request.onProgress?.("Reading the receipt");
|
|
27821
|
+
let receipt = parseReceiptPage(await pageText(deps, viewUrl), id2, deps.baseUrl);
|
|
27822
|
+
const listed = new Set(receipt.files.map((file3) => file3.name.toLowerCase()));
|
|
27823
|
+
const missing = storedNames.filter((name) => !listed.has(name.toLowerCase()));
|
|
27824
|
+
if (missing.length) throw deps.fail(`Moodle saved the submission but its page does not list ${missing.join(", ")}; check the assignment in a browser before submitting.`);
|
|
27825
|
+
let action = "saved";
|
|
27826
|
+
if (isSubmitted(receipt.submission_status)) action = "submitted";
|
|
27827
|
+
else if (request.final) {
|
|
27828
|
+
request.onProgress?.("Submitting for grading");
|
|
27829
|
+
confirm ??= parseConfirmForm(await pageText(deps, `${viewUrl}&action=submit`), deps);
|
|
27830
|
+
const errorHtml = await postForm(deps, confirm.action, [...confirm.fields, ...confirm.statement ? [["submissionstatement", "1"]] : [], ["submitbutton", "Continue"]]);
|
|
27831
|
+
if (errorHtml !== null) throw deps.fail(`Moodle did not submit the assignment for grading: ${noticesOf(errorHtml) || "it returned the confirmation page again without a reason"}`);
|
|
27832
|
+
receipt = parseReceiptPage(await pageText(deps, viewUrl), id2, deps.baseUrl);
|
|
27833
|
+
if (!isSubmitted(receipt.submission_status)) throw deps.fail(`Moodle accepted the confirmation but still reports "${receipt.submission_status || "no status"}"; check the assignment in a browser.`);
|
|
27834
|
+
action = "submitted";
|
|
27835
|
+
}
|
|
27836
|
+
return {
|
|
27837
|
+
...receipt,
|
|
27838
|
+
action,
|
|
27839
|
+
uploads,
|
|
27840
|
+
removed,
|
|
27841
|
+
limits,
|
|
27842
|
+
...statement ? { statement, statement_accepted: true } : {},
|
|
27843
|
+
checked_at: timestamp(deps)
|
|
27844
|
+
};
|
|
27845
|
+
}
|
|
27846
|
+
function parseSubmissionForm(html, deps) {
|
|
27847
|
+
const root = (0, import_node_html_parser3.parse)(html);
|
|
27848
|
+
const form = formWithAction(root, "savesubmission");
|
|
27849
|
+
if (!form) {
|
|
27850
|
+
const notice = noticesOf(html);
|
|
27851
|
+
if (root.querySelectorAll("input[type=submit], button").some((button) => /begin assignment/iu.test(cleanText(button.getAttribute("value") ?? button.textContent)))) {
|
|
27852
|
+
throw deps.fail("This is a timed assignment; start it in a browser before uploading files.");
|
|
27853
|
+
}
|
|
27854
|
+
throw deps.fail(notice ? `Moodle is not accepting a submission: ${notice}` : "Moodle did not show a submission form for this assignment.");
|
|
27855
|
+
}
|
|
27856
|
+
const itemid = form.querySelector("input[name=files_filemanager]")?.getAttribute("value")?.trim() ?? "";
|
|
27857
|
+
if (!itemid) throw deps.fail("This assignment does not accept file uploads.");
|
|
27858
|
+
const options = filemanagerOptions(html, itemid);
|
|
27859
|
+
if (!options) throw deps.fail("Moodle did not describe the file upload area for this assignment.");
|
|
27860
|
+
const fields2 = formFields(form);
|
|
27861
|
+
const sesskey = fields2.find(([name]) => name === "sesskey")?.[1] ?? "";
|
|
27862
|
+
if (!sesskey) throw deps.fail("The submission form has no session key.");
|
|
27863
|
+
const picker = record2(options.filepicker);
|
|
27864
|
+
const repositories = (Array.isArray(picker.repositories) ? picker.repositories : Object.values(record2(picker.repositories))).map(record2);
|
|
27865
|
+
const upload = repositories.find((repo) => repo.type === "upload");
|
|
27866
|
+
if (!upload || upload.id === void 0) throw deps.fail("The site does not allow direct file uploads for this assignment.");
|
|
27867
|
+
const accepted = options.accepted_types;
|
|
27868
|
+
const acceptedTypes = Array.isArray(accepted) ? accepted.map(String).filter(Boolean) : accepted === void 0 || accepted === "*" ? "*" : [String(accepted)];
|
|
27869
|
+
return {
|
|
27870
|
+
action: resolveUrl(deps.baseUrl, form.getAttribute("action") || `${deps.baseUrl}${ASSIGN_VIEW_PATH}`),
|
|
27871
|
+
fields: fields2,
|
|
27872
|
+
sesskey,
|
|
27873
|
+
itemid,
|
|
27874
|
+
clientId: String(options.client_id ?? ""),
|
|
27875
|
+
contextId: String(record2(options.context).id ?? ""),
|
|
27876
|
+
repoId: String(upload.id),
|
|
27877
|
+
author: String(picker.author ?? ""),
|
|
27878
|
+
license: String(picker.defaultlicense ?? ""),
|
|
27879
|
+
maxBytes: integer2(options.maxbytes),
|
|
27880
|
+
areaMaxBytes: integer2(options.areamaxbytes),
|
|
27881
|
+
maxFiles: integer2(options.maxfiles),
|
|
27882
|
+
acceptedTypes: acceptedTypes.length === 1 && acceptedTypes[0] === "*" ? "*" : acceptedTypes,
|
|
27883
|
+
...statementOf(form)
|
|
27884
|
+
};
|
|
27885
|
+
}
|
|
27886
|
+
function parseConfirmForm(html, deps) {
|
|
27887
|
+
const root = (0, import_node_html_parser3.parse)(html);
|
|
27888
|
+
const form = formWithAction(root, "confirmsubmit");
|
|
27889
|
+
if (!form) {
|
|
27890
|
+
const notice = noticesOf(html);
|
|
27891
|
+
throw deps.fail(notice ? `Moodle is not accepting a submission for grading: ${notice}` : "Moodle did not show the submit-for-grading confirmation.");
|
|
27892
|
+
}
|
|
27893
|
+
return { action: resolveUrl(deps.baseUrl, form.getAttribute("action") || `${deps.baseUrl}${ASSIGN_VIEW_PATH}`), fields: formFields(form), ...statementOf(form) };
|
|
27894
|
+
}
|
|
27895
|
+
function parseReceiptPage(html, activityId, baseUrl) {
|
|
27896
|
+
const page2 = parseAssignmentHtml(html, activityId, baseUrl);
|
|
27897
|
+
const root = (0, import_node_html_parser3.parse)(html);
|
|
27898
|
+
const files = /* @__PURE__ */ new Map();
|
|
27899
|
+
const add = (link) => {
|
|
27900
|
+
const name = cleanText(link.textContent);
|
|
27901
|
+
const href = link.getAttribute("href") ?? "";
|
|
27902
|
+
if (name && !files.has(name.toLowerCase())) files.set(name.toLowerCase(), { name, ...href ? { url: resolveUrl(baseUrl, href) } : {} });
|
|
27903
|
+
};
|
|
27904
|
+
for (const link of root.querySelectorAll(".fileuploadsubmission a[href]")) add(link);
|
|
27905
|
+
for (const link of tableCell(root, "File submissions")?.querySelectorAll("a[href]") ?? []) add(link);
|
|
27906
|
+
return {
|
|
27907
|
+
id: activityId,
|
|
27908
|
+
name: page2.name,
|
|
27909
|
+
...page2.course_id ? { unit_id: page2.course_id } : {},
|
|
27910
|
+
url: page2.url,
|
|
27911
|
+
submission_status: page2.submission_status,
|
|
27912
|
+
grading_status: page2.grading_status,
|
|
27913
|
+
due: page2.due_pretty || cleanText(tableCell(root, "Due date")?.textContent),
|
|
27914
|
+
time_remaining: page2.time_remaining,
|
|
27915
|
+
last_modified: cleanText(tableCell(root, "Last modified")?.textContent),
|
|
27916
|
+
files: [...files.values()]
|
|
27917
|
+
};
|
|
27918
|
+
}
|
|
27919
|
+
function noticesOf(html) {
|
|
27920
|
+
const root = (0, import_node_html_parser3.parse)(html);
|
|
27921
|
+
const texts = [];
|
|
27922
|
+
for (const node2 of root.querySelectorAll(".alert, .invalid-feedback, .form-control-feedback, .error, [data-fieldtype] .text-danger")) {
|
|
27923
|
+
for (const junk of node2.querySelectorAll("button, .close")) junk.remove();
|
|
27924
|
+
const text = cleanText(node2.textContent);
|
|
27925
|
+
if (text && !texts.includes(text)) texts.push(text);
|
|
27926
|
+
}
|
|
27927
|
+
return texts.join(" ");
|
|
27928
|
+
}
|
|
27929
|
+
function formWithAction(root, action) {
|
|
27930
|
+
for (const form of root.querySelectorAll("form")) {
|
|
27931
|
+
if (form.querySelectorAll("input[name=action]").some((input3) => input3.getAttribute("value") === action)) return form;
|
|
27932
|
+
}
|
|
27933
|
+
return null;
|
|
27934
|
+
}
|
|
27935
|
+
function formFields(form) {
|
|
27936
|
+
const fields2 = [];
|
|
27937
|
+
for (const element of form.querySelectorAll("input, textarea, select")) {
|
|
27938
|
+
const name = element.getAttribute("name");
|
|
27939
|
+
if (!name) continue;
|
|
27940
|
+
const tag = element.tagName.toLowerCase();
|
|
27941
|
+
if (tag === "textarea") {
|
|
27942
|
+
fields2.push([name, element.textContent]);
|
|
27943
|
+
continue;
|
|
27944
|
+
}
|
|
27945
|
+
if (tag === "select") {
|
|
27946
|
+
const options = element.querySelectorAll("option");
|
|
27947
|
+
const chosen = options.find((option) => option.hasAttribute("selected")) ?? options[0];
|
|
27948
|
+
if (chosen) fields2.push([name, chosen.getAttribute("value") ?? cleanText(chosen.textContent)]);
|
|
27949
|
+
continue;
|
|
27950
|
+
}
|
|
27951
|
+
const type = (element.getAttribute("type") ?? "text").toLowerCase();
|
|
27952
|
+
if (["submit", "button", "image", "file", "reset"].includes(type)) continue;
|
|
27953
|
+
if ((type === "checkbox" || type === "radio") && !element.hasAttribute("checked")) continue;
|
|
27954
|
+
fields2.push([name, element.getAttribute("value") ?? (type === "checkbox" ? "on" : "")]);
|
|
27955
|
+
}
|
|
27956
|
+
return fields2;
|
|
27957
|
+
}
|
|
27958
|
+
function statementOf(form) {
|
|
27959
|
+
const box = form.querySelector("input[name=submissionstatement]");
|
|
27960
|
+
if (!box) return {};
|
|
27961
|
+
const id2 = box.getAttribute("id");
|
|
27962
|
+
const label = (id2 ? form.querySelector(`label[for="${id2}"]`) : null) ?? box.closest("label") ?? box.parentNode?.querySelector("label") ?? null;
|
|
27963
|
+
const text = cleanText(label?.textContent).replace(/\s*Required\s*$/u, "").trim();
|
|
27964
|
+
return { statement: text || "Submission statement" };
|
|
27965
|
+
}
|
|
27966
|
+
function filemanagerOptions(html, itemid) {
|
|
27967
|
+
const pattern = /M\.form_filemanager\.init\(\s*Y\s*,\s*/gu;
|
|
27968
|
+
let match;
|
|
27969
|
+
while (match = pattern.exec(html)) {
|
|
27970
|
+
const json2 = balancedObject(html, match.index + match[0].length);
|
|
27971
|
+
if (!json2) continue;
|
|
27972
|
+
try {
|
|
27973
|
+
const options = JSON.parse(json2);
|
|
27974
|
+
if (isRecord5(options) && String(options.itemid) === itemid) return options;
|
|
27975
|
+
} catch {
|
|
27976
|
+
}
|
|
27977
|
+
}
|
|
27978
|
+
return null;
|
|
27979
|
+
}
|
|
27980
|
+
function balancedObject(text, start) {
|
|
27981
|
+
if (text[start] !== "{") return null;
|
|
27982
|
+
let depth = 0;
|
|
27983
|
+
let quoted = false;
|
|
27984
|
+
for (let index = start; index < text.length; index += 1) {
|
|
27985
|
+
const char = text[index];
|
|
27986
|
+
if (quoted) {
|
|
27987
|
+
if (char === "\\") index += 1;
|
|
27988
|
+
else if (char === '"') quoted = false;
|
|
27989
|
+
} else if (char === '"') quoted = true;
|
|
27990
|
+
else if (char === "{") depth += 1;
|
|
27991
|
+
else if (char === "}") {
|
|
27992
|
+
depth -= 1;
|
|
27993
|
+
if (depth === 0) return text.slice(start, index + 1);
|
|
27994
|
+
}
|
|
27995
|
+
}
|
|
27996
|
+
return null;
|
|
27997
|
+
}
|
|
27998
|
+
function tableCell(root, label) {
|
|
27999
|
+
for (const row of root.querySelectorAll("tr")) {
|
|
28000
|
+
const cells = row.querySelectorAll("th, td");
|
|
28001
|
+
if (cells.length > 1 && cleanText(cells[0].textContent) === label) return cells[1];
|
|
28002
|
+
}
|
|
28003
|
+
return null;
|
|
28004
|
+
}
|
|
28005
|
+
async function pageText(deps, url2) {
|
|
28006
|
+
return (await deps.request(url2)).text();
|
|
28007
|
+
}
|
|
28008
|
+
async function postForm(deps, action, fields2) {
|
|
28009
|
+
const response = await deps.request(action, {
|
|
28010
|
+
method: "POST",
|
|
28011
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
28012
|
+
body: new URLSearchParams(fields2).toString()
|
|
28013
|
+
});
|
|
28014
|
+
const html = await response.text();
|
|
28015
|
+
return landedOnView(response.url) ? null : html;
|
|
28016
|
+
}
|
|
28017
|
+
function landedOnView(url2) {
|
|
28018
|
+
try {
|
|
28019
|
+
const parsed = new URL(url2);
|
|
28020
|
+
return parsed.pathname.endsWith(ASSIGN_VIEW_PATH) && (parsed.searchParams.get("action") ?? "view") === "view";
|
|
28021
|
+
} catch {
|
|
28022
|
+
return false;
|
|
28023
|
+
}
|
|
28024
|
+
}
|
|
28025
|
+
async function draftAjax(deps, form, action, params) {
|
|
28026
|
+
const body = new URLSearchParams({ sesskey: form.sesskey, client_id: form.clientId, itemid: form.itemid, ...params });
|
|
28027
|
+
const response = await deps.request(`${deps.baseUrl}/repository/draftfiles_ajax.php?action=${action}`, {
|
|
28028
|
+
method: "POST",
|
|
28029
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
28030
|
+
body: body.toString()
|
|
28031
|
+
}, { allowErrorStatus: true });
|
|
28032
|
+
return jsonOf(deps, response, `draft file ${action}`);
|
|
28033
|
+
}
|
|
28034
|
+
async function listDraftFiles(deps, form) {
|
|
28035
|
+
const data = record2(await draftAjax(deps, form, "list", { filepath: "/" }));
|
|
28036
|
+
const list2 = Array.isArray(data.list) ? data.list.map(record2) : [];
|
|
28037
|
+
return list2.filter((item) => item.type !== "folder").map((item) => ({ name: String(item.filename ?? item.fullname ?? ""), path: String(item.filepath ?? "/"), bytes: integer2(item.size) })).filter((item) => item.name);
|
|
28038
|
+
}
|
|
28039
|
+
async function deleteDraftFiles(deps, form, files) {
|
|
28040
|
+
const selected = JSON.stringify(files.map((file3) => ({ filename: file3.name, filepath: file3.path })));
|
|
28041
|
+
const result = await draftAjax(deps, form, "deleteselected", { selected });
|
|
28042
|
+
if (result === false) throw deps.fail("Moodle did not remove the existing submission files.");
|
|
28043
|
+
}
|
|
28044
|
+
async function uploadDraftFile(deps, form, file3) {
|
|
28045
|
+
const body = new FormData();
|
|
28046
|
+
body.set("sesskey", form.sesskey);
|
|
28047
|
+
body.set("client_id", form.clientId);
|
|
28048
|
+
body.set("repo_id", form.repoId);
|
|
28049
|
+
body.set("itemid", form.itemid);
|
|
28050
|
+
body.set("env", "filemanager");
|
|
28051
|
+
body.set("ctx_id", form.contextId);
|
|
28052
|
+
body.set("title", file3.name);
|
|
28053
|
+
body.set("author", form.author);
|
|
28054
|
+
body.set("license", form.license);
|
|
28055
|
+
body.set("savepath", "/");
|
|
28056
|
+
body.set("maxbytes", String(form.maxBytes));
|
|
28057
|
+
body.set("areamaxbytes", String(form.areaMaxBytes));
|
|
28058
|
+
for (const type of form.acceptedTypes === "*" ? ["*"] : form.acceptedTypes) body.append("accepted_types[]", type);
|
|
28059
|
+
body.set("overwrite", "1");
|
|
28060
|
+
body.set("repo_upload_file", new Blob([Uint8Array.from(file3.bytes)]), file3.name);
|
|
28061
|
+
const response = await deps.request(`${deps.baseUrl}/repository/repository_ajax.php?action=upload`, { method: "POST", body }, { allowErrorStatus: true });
|
|
28062
|
+
const data = record2(await jsonOf(deps, response, `upload of ${file3.name}`));
|
|
28063
|
+
if (typeof data.error === "string" && data.error) throw deps.fail(`Moodle refused ${file3.name}: ${data.error}`, typeof data.errorcode === "string" ? data.errorcode : void 0);
|
|
28064
|
+
if (data.event === "fileexists") throw deps.fail(`Moodle reports ${file3.name} already exists and did not overwrite it.`);
|
|
28065
|
+
const stored = typeof data.file === "string" && data.file ? data.file : file3.name;
|
|
28066
|
+
if (!data.url && !data.id && !data.file) throw deps.fail(`Moodle did not confirm the upload of ${file3.name}.`);
|
|
28067
|
+
return stored;
|
|
28068
|
+
}
|
|
28069
|
+
async function jsonOf(deps, response, step) {
|
|
28070
|
+
const text = await response.text();
|
|
28071
|
+
try {
|
|
28072
|
+
return JSON.parse(text);
|
|
28073
|
+
} catch {
|
|
28074
|
+
const notice = noticesOf(text);
|
|
28075
|
+
throw deps.fail(`Moodle did not answer the ${step} with JSON (HTTP ${response.status})${notice ? `: ${notice}` : ""}`);
|
|
28076
|
+
}
|
|
28077
|
+
}
|
|
28078
|
+
function checkLimits(deps, form, kept, files) {
|
|
28079
|
+
if (form.maxFiles > 0 && kept.length + files.length > form.maxFiles) {
|
|
28080
|
+
throw deps.usage(`This assignment allows ${form.maxFiles} file${form.maxFiles === 1 ? "" : "s"}; the submission would hold ${kept.length + files.length}.`, kept.length ? "Use --replace to drop the existing files first." : void 0);
|
|
28081
|
+
}
|
|
28082
|
+
for (const file3 of files) {
|
|
28083
|
+
if (form.maxBytes > 0 && file3.bytes.byteLength > form.maxBytes) throw deps.usage(`${file3.name} is ${size(file3.bytes.byteLength)}; the limit is ${size(form.maxBytes)}.`);
|
|
28084
|
+
if (form.acceptedTypes !== "*" && form.acceptedTypes.every((type) => type.startsWith(".")) && !form.acceptedTypes.some((type) => file3.name.toLowerCase().endsWith(type.toLowerCase()))) {
|
|
28085
|
+
throw deps.usage(`${file3.name} is not an accepted type; allowed: ${form.acceptedTypes.join(", ")}.`);
|
|
28086
|
+
}
|
|
28087
|
+
}
|
|
28088
|
+
const total = kept.reduce((sum, file3) => sum + file3.bytes, 0) + files.reduce((sum, file3) => sum + file3.bytes.byteLength, 0);
|
|
28089
|
+
if (form.areaMaxBytes > 0 && total > form.areaMaxBytes) throw deps.usage(`The submission would total ${size(total)}; the limit is ${size(form.areaMaxBytes)}.`, kept.length ? "Use --replace to drop the existing files first." : void 0);
|
|
28090
|
+
}
|
|
28091
|
+
function describeLimits(form) {
|
|
28092
|
+
return {
|
|
28093
|
+
...form.maxBytes > 0 ? { max_bytes: form.maxBytes } : {},
|
|
28094
|
+
...form.maxFiles > 0 ? { max_files: form.maxFiles } : {},
|
|
28095
|
+
...form.areaMaxBytes > 0 ? { area_max_bytes: form.areaMaxBytes } : {},
|
|
28096
|
+
...form.acceptedTypes !== "*" ? { accepted_types: form.acceptedTypes } : {}
|
|
28097
|
+
};
|
|
28098
|
+
}
|
|
28099
|
+
function isSubmitted(status) {
|
|
28100
|
+
return /\bsubmitted\b/iu.test(status) && !/\bnot submitted\b/iu.test(status);
|
|
28101
|
+
}
|
|
28102
|
+
function size(bytes) {
|
|
28103
|
+
if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
|
|
28104
|
+
if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} KiB`;
|
|
28105
|
+
return `${bytes} B`;
|
|
28106
|
+
}
|
|
28107
|
+
function timestamp(deps) {
|
|
28108
|
+
return (deps.now?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
28109
|
+
}
|
|
28110
|
+
function integer2(value) {
|
|
28111
|
+
const number4 = Number(value);
|
|
28112
|
+
return Number.isSafeInteger(number4) ? number4 : 0;
|
|
28113
|
+
}
|
|
28114
|
+
function isRecord5(value) {
|
|
28115
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
28116
|
+
}
|
|
28117
|
+
function record2(value) {
|
|
28118
|
+
return isRecord5(value) ? value : {};
|
|
28119
|
+
}
|
|
28120
|
+
|
|
28121
|
+
// src/parsers.ts
|
|
28122
|
+
function schema(parser) {
|
|
28123
|
+
return { parse: parser };
|
|
28124
|
+
}
|
|
28125
|
+
var UserInfoSchema = schema(parseUserInfo);
|
|
28126
|
+
var CourseSchema = schema(parseCourse);
|
|
28127
|
+
var CoursesSchema = schema(parseCourses);
|
|
28128
|
+
var ActivitySchema = schema(parseActivity);
|
|
28129
|
+
var SectionSchema = schema(parseSection);
|
|
28130
|
+
var CourseContentsSchema = schema(parseCourseContents);
|
|
28131
|
+
var TodoItemSchema = schema(parseTodoItem);
|
|
28132
|
+
function parseUserInfo(value) {
|
|
28133
|
+
const data = asRecord(value);
|
|
28134
|
+
return {
|
|
28135
|
+
userid: numberValue2(data.userid),
|
|
28136
|
+
username: stringValue3(data.username),
|
|
28137
|
+
fullname: stringValue3(data.fullname),
|
|
28138
|
+
sitename: stringValue3(data.sitename),
|
|
28139
|
+
siteurl: stringValue3(data.siteurl),
|
|
28140
|
+
lang: stringValue3(data.lang),
|
|
28141
|
+
...data.timezone ? { timezone: String(data.timezone) } : {}
|
|
28142
|
+
};
|
|
28143
|
+
}
|
|
28144
|
+
function parseCourse(value, nowSeconds = Math.floor(Date.now() / 1e3)) {
|
|
28145
|
+
const data = asRecord(value);
|
|
28146
|
+
const course = {
|
|
28147
|
+
id: numberValue2(data.id),
|
|
28148
|
+
shortname: stringValue3(data.shortname),
|
|
28149
|
+
fullname: stringValue3(data.fullname),
|
|
28150
|
+
category: numberValue2(data.category),
|
|
28151
|
+
visible: booleanValue(data.visible, true),
|
|
28152
|
+
startdate: numberValue2(data.startdate)
|
|
28153
|
+
};
|
|
28154
|
+
const enddate = numberValue2(data.enddate);
|
|
28155
|
+
if (enddate > 0) {
|
|
28156
|
+
course.enddate = enddate;
|
|
28157
|
+
}
|
|
28158
|
+
return course;
|
|
28159
|
+
}
|
|
28160
|
+
function parseCourses(value) {
|
|
28161
|
+
return asArray(value).map((item) => parseCourse(item));
|
|
28162
|
+
}
|
|
28163
|
+
function parseActivity(value) {
|
|
28164
|
+
const data = asRecord(value);
|
|
28165
|
+
return {
|
|
28166
|
+
id: numberValue2(data.id),
|
|
28167
|
+
name: stringValue3(data.name),
|
|
28168
|
+
modname: stringValue3(data.modname),
|
|
28169
|
+
url: stringValue3(data.url),
|
|
28170
|
+
visible: booleanValue(data.visible, true),
|
|
28171
|
+
description: stringValue3(data.description),
|
|
28172
|
+
...data.completiondata && typeof data.completiondata === "object" ? { completion: numberValue2(asRecord(data.completiondata).state) } : {},
|
|
28173
|
+
...Array.isArray(data.contents) ? { file_entries: data.contents.filter((f) => asRecord(f).fileurl).map((f) => ({ name: stringValue3(asRecord(f).filename), url: stringValue3(asRecord(f).fileurl), requires_authentication: true })) } : {}
|
|
28174
|
+
};
|
|
28175
|
+
}
|
|
28176
|
+
function parseSection(value) {
|
|
28177
|
+
const data = asRecord(value);
|
|
28178
|
+
return {
|
|
28179
|
+
id: numberValue2(data.id),
|
|
28180
|
+
name: stringValue3(data.name),
|
|
28181
|
+
section: numberValue2(data.section),
|
|
28182
|
+
visible: booleanValue(data.visible, true),
|
|
28183
|
+
summary: stringValue3(data.summary),
|
|
28184
|
+
...data.current !== void 0 ? { current: booleanValue(data.current) } : {},
|
|
28185
|
+
activities: asArray(data.modules).map((item) => parseActivity(item))
|
|
28186
|
+
};
|
|
28187
|
+
}
|
|
28188
|
+
function parseCourseContents(value) {
|
|
28189
|
+
return asArray(value).map((item) => parseSection(item));
|
|
28190
|
+
}
|
|
28191
|
+
function parseCourseFormatState(value, baseUrl) {
|
|
28192
|
+
const state = asRecord(parseJsonValue(value));
|
|
28193
|
+
const activities = /* @__PURE__ */ new Map();
|
|
28194
|
+
const activitiesBySection = /* @__PURE__ */ new Map();
|
|
28195
|
+
for (const item of asArray(state.cm)) {
|
|
28196
|
+
const data = asRecord(item);
|
|
28197
|
+
const id2 = numberValue2(data.id);
|
|
28198
|
+
const sectionId = stringValue3(data.sectionid);
|
|
28199
|
+
const module = stringValue3(data.module) || stringValue3(data.plugin).replace(/^mod_/u, "") || stringValue3(data.modname).toLowerCase();
|
|
28200
|
+
const activity = {
|
|
28201
|
+
id: id2,
|
|
28202
|
+
name: htmlText(data.name, baseUrl),
|
|
28203
|
+
modname: module.toLowerCase(),
|
|
28204
|
+
url: stringValue3(data.url) ? resolveUrl(baseUrl, stringValue3(data.url)) : "",
|
|
28205
|
+
visible: booleanValue(data.visible, true) && booleanValue(data.uservisible, true) && !booleanValue(data.stealth),
|
|
28206
|
+
description: htmlText(data.content ?? data.description, baseUrl),
|
|
28207
|
+
...data.completionstate !== void 0 && data.completionstate !== null ? { completion: numberValue2(data.completionstate) } : {}
|
|
28208
|
+
};
|
|
28209
|
+
activities.set(String(id2), activity);
|
|
28210
|
+
const sectionActivities = activitiesBySection.get(sectionId) ?? [];
|
|
28211
|
+
sectionActivities.push(activity);
|
|
28212
|
+
activitiesBySection.set(sectionId, sectionActivities);
|
|
28213
|
+
}
|
|
28214
|
+
return asArray(state.section).map((item) => {
|
|
28215
|
+
const data = asRecord(item);
|
|
28216
|
+
const id2 = numberValue2(data.id);
|
|
28217
|
+
const hasActivityList = Array.isArray(data.cmlist);
|
|
28218
|
+
const listedActivities = asArray(data.cmlist).map((activityId) => activities.get(stringValue3(activityId))).filter((activity) => activity !== void 0);
|
|
28219
|
+
return {
|
|
28220
|
+
id: id2,
|
|
28221
|
+
name: htmlText(data.title || data.rawtitle, baseUrl),
|
|
28222
|
+
section: numberValue2(data.section ?? data.number),
|
|
28223
|
+
visible: booleanValue(data.visible, true),
|
|
28224
|
+
summary: htmlText(data.summary, baseUrl),
|
|
28225
|
+
...data.current !== void 0 ? { current: booleanValue(data.current) } : {},
|
|
28226
|
+
activities: hasActivityList ? listedActivities : activitiesBySection.get(String(id2)) ?? []
|
|
28227
|
+
};
|
|
28228
|
+
});
|
|
28229
|
+
}
|
|
28230
|
+
function parseTodoItem(value) {
|
|
28231
|
+
const data = asRecord(value);
|
|
28232
|
+
const course = asRecord(data.course);
|
|
28233
|
+
const action = asRecord(data.action);
|
|
28234
|
+
const progress = course.progress;
|
|
28235
|
+
return {
|
|
28236
|
+
id: numberValue2(data.id),
|
|
28237
|
+
name: stringValue3(data.name),
|
|
28238
|
+
activity_name: stringValue3(data.activityname),
|
|
28239
|
+
modname: stringValue3(data.modulename),
|
|
28240
|
+
course_id: numberValue2(course.id),
|
|
28241
|
+
course_name: stringValue3(course.fullname),
|
|
28242
|
+
due_at: numberValue2(data.timesort) || numberValue2(data.timestart),
|
|
28243
|
+
overdue: booleanValue(data.overdue),
|
|
28244
|
+
actionable: booleanValue(action.actionable),
|
|
28245
|
+
action_name: stringValue3(action.name),
|
|
28246
|
+
action_url: stringValue3(action.url),
|
|
28247
|
+
url: stringValue3(data.url),
|
|
28248
|
+
event_type: stringValue3(data.eventtype),
|
|
28249
|
+
course_progress: typeof progress === "number" ? progress : void 0
|
|
28250
|
+
};
|
|
28251
|
+
}
|
|
28252
|
+
function parseTodoItems(value) {
|
|
28253
|
+
return asArray(value).map((item) => parseTodoItem(item));
|
|
28254
|
+
}
|
|
28255
|
+
function parseAlertNotification(value) {
|
|
28256
|
+
const data = asRecord(value);
|
|
28257
|
+
return {
|
|
28258
|
+
id: numberValue2(data.id),
|
|
28259
|
+
subject: stringValue3(data.subject),
|
|
28260
|
+
short_subject: stringValue3(data.shortenedsubject),
|
|
28261
|
+
event_type: stringValue3(data.eventtype),
|
|
28262
|
+
component: stringValue3(data.component),
|
|
28263
|
+
created_at: numberValue2(data.timecreated),
|
|
28264
|
+
created_pretty: stringValue3(data.timecreatedpretty),
|
|
28265
|
+
read: booleanValue(data.read),
|
|
28266
|
+
context_url: stringValue3(data.contexturl),
|
|
28267
|
+
context_name: stringValue3(data.contexturlname)
|
|
28268
|
+
};
|
|
28269
|
+
}
|
|
28270
|
+
function parseAlertSummary(notificationsData, countsData, unreadCountsData) {
|
|
28271
|
+
const notificationsRecord = asRecord(notificationsData);
|
|
28272
|
+
const counts2 = asRecord(countsData);
|
|
28273
|
+
const unreadCounts = asRecord(unreadCountsData);
|
|
28274
|
+
const types = asRecord(counts2.types);
|
|
28275
|
+
const unreadTypes = asRecord(unreadCounts.types);
|
|
28276
|
+
const notifications = asArray(notificationsRecord.notifications).map((item) => parseAlertNotification(item));
|
|
28277
|
+
return {
|
|
28278
|
+
notifications,
|
|
28279
|
+
notification_count: notifications.length,
|
|
28280
|
+
unread_notification_count: notifications.filter((notification) => !notification.read).length,
|
|
28281
|
+
starred_message_count: numberValue2(counts2.favourites),
|
|
28282
|
+
direct_message_count: numberValue2(types["1"]),
|
|
28283
|
+
group_message_count: numberValue2(types["2"]),
|
|
28284
|
+
self_message_count: numberValue2(types["3"]),
|
|
28285
|
+
unread_starred_message_count: numberValue2(unreadCounts.favourites),
|
|
28286
|
+
unread_direct_message_count: numberValue2(unreadTypes["1"]),
|
|
28287
|
+
unread_group_message_count: numberValue2(unreadTypes["2"]),
|
|
28288
|
+
unread_self_message_count: numberValue2(unreadTypes["3"])
|
|
28289
|
+
};
|
|
28290
|
+
}
|
|
28291
|
+
function parseForumPostAuthor(value) {
|
|
28292
|
+
const data = asRecord(value);
|
|
28293
|
+
const urls = asRecord(data.urls);
|
|
28294
|
+
return {
|
|
28295
|
+
id: numberValue2(data.id),
|
|
28296
|
+
fullname: stringValue3(data.fullname),
|
|
28297
|
+
profile_url: stringValue3(urls.profile),
|
|
28298
|
+
profile_image_url: stringValue3(urls.profileimage)
|
|
28299
|
+
};
|
|
28300
|
+
}
|
|
28301
|
+
function parseForumPost(value, baseUrl = "") {
|
|
28302
|
+
const data = asRecord(value);
|
|
28303
|
+
const urls = asRecord(data.urls);
|
|
28304
|
+
const messageHtml = stringValue3(data.message);
|
|
28305
|
+
const structured = htmlToStructuredContent(messageHtml, stringValue3(urls.view || urls.discuss) || baseUrl);
|
|
28306
|
+
return {
|
|
28307
|
+
id: numberValue2(data.id),
|
|
28308
|
+
discussion_id: numberValue2(data.discussionid),
|
|
28309
|
+
subject: stringValue3(data.subject),
|
|
28310
|
+
message_html: messageHtml,
|
|
28311
|
+
message_text: structured.text,
|
|
28312
|
+
image_urls: structured.image_urls,
|
|
28313
|
+
links: structured.links,
|
|
28314
|
+
tables: structured.tables,
|
|
28315
|
+
author: parseForumPostAuthor(data.author),
|
|
28316
|
+
parent_id: numberValue2(data.parentid),
|
|
28317
|
+
time_created: numberValue2(data.timecreated),
|
|
28318
|
+
time_modified: numberValue2(data.timemodified),
|
|
28319
|
+
created_pretty: "",
|
|
28320
|
+
unread: booleanValue(data.unread),
|
|
28321
|
+
is_deleted: booleanValue(data.isdeleted),
|
|
28322
|
+
is_private_reply: booleanValue(data.isprivatereply),
|
|
28323
|
+
url: stringValue3(urls.view || urls.viewisolated),
|
|
28324
|
+
reply_url: stringValue3(urls.reply)
|
|
28325
|
+
};
|
|
28326
|
+
}
|
|
28327
|
+
function parseForumDiscussion(value, discussionId, baseUrl = "") {
|
|
28328
|
+
const data = asRecord(value);
|
|
28329
|
+
const posts = asArray(data.posts).map((item) => parseForumPost(item, baseUrl));
|
|
28330
|
+
return {
|
|
28331
|
+
id: discussionId,
|
|
28332
|
+
subject: posts[0]?.subject ?? "",
|
|
28333
|
+
course_id: numberValue2(data.courseid),
|
|
28334
|
+
forum_id: numberValue2(data.forumid),
|
|
28335
|
+
group_id: numberValue2(data.groupid),
|
|
28336
|
+
group_name: stringValue3(data.groupname),
|
|
28337
|
+
url: posts[0]?.url ? posts[0].url.split("#", 1)[0] : "",
|
|
28338
|
+
posts
|
|
28339
|
+
};
|
|
28340
|
+
}
|
|
28341
|
+
function asRecord(value) {
|
|
28342
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
28343
|
+
}
|
|
28344
|
+
function asArray(value) {
|
|
28345
|
+
return Array.isArray(value) ? value : [];
|
|
28346
|
+
}
|
|
28347
|
+
function stringValue3(value) {
|
|
28348
|
+
return typeof value === "string" ? value : value == null ? "" : String(value);
|
|
28349
|
+
}
|
|
28350
|
+
function numberValue2(value) {
|
|
28351
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
28352
|
+
return value;
|
|
28353
|
+
}
|
|
28354
|
+
if (typeof value === "string" && value.trim() !== "" && Number.isFinite(Number(value))) {
|
|
28355
|
+
return Number(value);
|
|
28356
|
+
}
|
|
28357
|
+
return 0;
|
|
28358
|
+
}
|
|
28359
|
+
function booleanValue(value, defaultValue = false) {
|
|
28360
|
+
if (value === void 0 || value === null) {
|
|
28361
|
+
return defaultValue;
|
|
28362
|
+
}
|
|
28363
|
+
return Boolean(value);
|
|
28364
|
+
}
|
|
28365
|
+
function parseJsonValue(value) {
|
|
28366
|
+
if (typeof value !== "string") return value;
|
|
28367
|
+
try {
|
|
28368
|
+
return JSON.parse(value);
|
|
28369
|
+
} catch {
|
|
28370
|
+
return {};
|
|
28371
|
+
}
|
|
28372
|
+
}
|
|
28373
|
+
function htmlText(value, baseUrl) {
|
|
28374
|
+
return htmlToStructuredContent(stringValue3(value), baseUrl).text;
|
|
28375
|
+
}
|
|
28376
|
+
|
|
27993
28377
|
// src/moodle-forum-core.ts
|
|
27994
28378
|
var ForumModule = class {
|
|
27995
28379
|
baseUrl;
|
|
@@ -28124,13 +28508,13 @@ var ForumModule = class {
|
|
|
28124
28508
|
}
|
|
28125
28509
|
};
|
|
28126
28510
|
function shouldFallbackForumAjax(error62) {
|
|
28127
|
-
if (!
|
|
28511
|
+
if (!isRecord6(error62)) {
|
|
28128
28512
|
return false;
|
|
28129
28513
|
}
|
|
28130
28514
|
const code = typeof error62.moodleErrorCode === "string" ? error62.moodleErrorCode : "";
|
|
28131
28515
|
return code === "servicenotavailable" || code === "accessexception" || error62 instanceof Error && error62.message.includes("Web service is not available");
|
|
28132
28516
|
}
|
|
28133
|
-
function
|
|
28517
|
+
function isRecord6(value) {
|
|
28134
28518
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
28135
28519
|
}
|
|
28136
28520
|
|
|
@@ -28262,9 +28646,9 @@ async function searchForumContent(source, query, options = {}) {
|
|
|
28262
28646
|
hits.sort(sortBy === "recent" ? sortRecent : sortRelevant);
|
|
28263
28647
|
return hits.slice(0, options.limit ?? 20).map(([, hit]) => includePostText ? hit : { ...hit, snippet: "" });
|
|
28264
28648
|
}
|
|
28265
|
-
async function inParallel(items,
|
|
28649
|
+
async function inParallel(items, size2, run) {
|
|
28266
28650
|
const results = [];
|
|
28267
|
-
for (let index = 0; index < items.length; index +=
|
|
28651
|
+
for (let index = 0; index < items.length; index += size2) results.push(...await Promise.all(items.slice(index, index + size2).map(run)));
|
|
28268
28652
|
return results;
|
|
28269
28653
|
}
|
|
28270
28654
|
function normalizeQuery(value) {
|
|
@@ -28450,7 +28834,7 @@ var MoodleClientCore = class {
|
|
|
28450
28834
|
try {
|
|
28451
28835
|
if (this.unavailable.has(FUNC_GET_SITE_INFO) && this.userInfo?.fullname) return this.userInfo;
|
|
28452
28836
|
const data = await this.call(FUNC_GET_SITE_INFO);
|
|
28453
|
-
if (
|
|
28837
|
+
if (isRecord7(data) && "userid" in data) {
|
|
28454
28838
|
const info = parseUserInfo(data);
|
|
28455
28839
|
this.sesskey = typeof data.sesskey === "string" ? data.sesskey : this.sesskey;
|
|
28456
28840
|
this.userid = info.userid;
|
|
@@ -28549,9 +28933,9 @@ var MoodleClientCore = class {
|
|
|
28549
28933
|
let type = "";
|
|
28550
28934
|
try {
|
|
28551
28935
|
const data = await this.call(FUNC_GET_COURSE_MODULE, { cmid: id2 });
|
|
28552
|
-
const module =
|
|
28553
|
-
type =
|
|
28554
|
-
courseId =
|
|
28936
|
+
const module = isRecord7(data) && isRecord7(data.cm) ? data.cm : data;
|
|
28937
|
+
type = isRecord7(module) && typeof module.modname === "string" ? module.modname : "";
|
|
28938
|
+
courseId = isRecord7(module) && typeof module.course === "number" ? module.course : void 0;
|
|
28555
28939
|
} catch (error62) {
|
|
28556
28940
|
if (!this.errors.isApi(error62) || error62.moodleErrorCode !== "servicenotavailable") {
|
|
28557
28941
|
throw error62;
|
|
@@ -28595,14 +28979,14 @@ var MoodleClientCore = class {
|
|
|
28595
28979
|
} else {
|
|
28596
28980
|
data = await this.call(FUNC_GET_ACTION_EVENTS, { ...window, limittononsuspendedevents: true });
|
|
28597
28981
|
}
|
|
28598
|
-
const events =
|
|
28982
|
+
const events = isRecord7(data) && Array.isArray(data.events) ? data.events : [];
|
|
28599
28983
|
for (const item of parseTodoItems(events)) if (!seen.has(item.id)) {
|
|
28600
28984
|
seen.add(item.id);
|
|
28601
28985
|
items.push(item);
|
|
28602
28986
|
}
|
|
28603
28987
|
if (events.length < batchSize) break;
|
|
28604
28988
|
const last = events.at(-1);
|
|
28605
|
-
const next =
|
|
28989
|
+
const next = isRecord7(last) && typeof last.id === "number" ? last.id : void 0;
|
|
28606
28990
|
if (!next || next === aftereventid) throw this.errors.api("Moodle repeated a calendar page; refine the date window.");
|
|
28607
28991
|
aftereventid = next;
|
|
28608
28992
|
}
|
|
@@ -28769,8 +29153,18 @@ var MoodleClientCore = class {
|
|
|
28769
29153
|
async getFolder(id2) {
|
|
28770
29154
|
return parseFolderHtml(await this.get(FOLDER_VIEW_PATH, { id: id2 }), id2, this.baseUrl);
|
|
28771
29155
|
}
|
|
28772
|
-
async requestAbsolute(url2, init = {}) {
|
|
28773
|
-
return this.requestAbsoluteInternal(url2, init, true);
|
|
29156
|
+
async requestAbsolute(url2, init = {}, options = {}) {
|
|
29157
|
+
return this.requestAbsoluteInternal(url2, init, true, Boolean(options.allowErrorStatus));
|
|
29158
|
+
}
|
|
29159
|
+
/** Uploads files into an assignment and reads the receipt back from the site. */
|
|
29160
|
+
async submitAssignment(request) {
|
|
29161
|
+
await this.ensureSession();
|
|
29162
|
+
return submitAssignmentFiles({
|
|
29163
|
+
baseUrl: this.baseUrl,
|
|
29164
|
+
request: (url2, init, options) => this.requestAbsolute(url2, init, options),
|
|
29165
|
+
fail: (message, moodleErrorCode) => this.errors.api(message, moodleErrorCode),
|
|
29166
|
+
usage: (message, hint) => this.errors.usage ? this.errors.usage(message, hint) : new MoodleClientCoreError("usage", message, hint)
|
|
29167
|
+
}, request);
|
|
28774
29168
|
}
|
|
28775
29169
|
async getNewsForums(courseId) {
|
|
28776
29170
|
const units = courseId === void 0 ? await this.getCourses() : (await this.getCourses()).filter((c) => c.id === courseId);
|
|
@@ -28780,7 +29174,7 @@ var MoodleClientCore = class {
|
|
|
28780
29174
|
await this.ensureSession();
|
|
28781
29175
|
const data = await this.call("mod_forum_get_forums_by_courses", { courseids: units.map((c) => c.id) });
|
|
28782
29176
|
for (const f of Array.isArray(data) ? data : []) {
|
|
28783
|
-
if (!
|
|
29177
|
+
if (!isRecord7(f) || f.type !== "news" || typeof f.cmid !== "number") continue;
|
|
28784
29178
|
const c = units.find((c2) => c2.id === f.course);
|
|
28785
29179
|
forums.push({ id: f.cmid, name: String(f.name || ""), course_id: Number(f.course), course_name: c?.fullname || "", url: `${this.baseUrl}/mod/forum/view.php?id=${f.cmid}` });
|
|
28786
29180
|
}
|
|
@@ -28908,16 +29302,16 @@ var MoodleClientCore = class {
|
|
|
28908
29302
|
async getAbsolute(url2) {
|
|
28909
29303
|
return (await this.requestAbsolute(url2)).text();
|
|
28910
29304
|
}
|
|
28911
|
-
async requestAbsoluteInternal(url2, init, allowRetry) {
|
|
29305
|
+
async requestAbsoluteInternal(url2, init, allowRetry, allowErrorStatus = false) {
|
|
28912
29306
|
const response = await fetchWithSession(url2, init, this.baseUrl, this.cookie, this.fetchImpl);
|
|
28913
29307
|
if (response.url.includes("/login/")) {
|
|
28914
29308
|
if (this.onLoginRequired && allowRetry && !this.retryingLogin) {
|
|
28915
29309
|
await this.reauthenticate();
|
|
28916
|
-
return this.requestAbsoluteInternal(url2, init, false);
|
|
29310
|
+
return this.requestAbsoluteInternal(url2, init, false, allowErrorStatus);
|
|
28917
29311
|
}
|
|
28918
29312
|
throw this.errors.api("Session expired", "servicerequireslogin");
|
|
28919
29313
|
}
|
|
28920
|
-
if (!response.ok) {
|
|
29314
|
+
if (!response.ok && !allowErrorStatus) {
|
|
28921
29315
|
const context = `HTTP ${response.status} loading ${safeUrl(url2)}`;
|
|
28922
29316
|
const contentType2 = response.headers.get("content-type")?.toLowerCase() ?? "";
|
|
28923
29317
|
if (contentType2.includes("text/html") || contentType2.includes("application/xhtml+xml")) {
|
|
@@ -28935,7 +29329,7 @@ var MoodleClientCore = class {
|
|
|
28935
29329
|
let offset = 0;
|
|
28936
29330
|
while (true) {
|
|
28937
29331
|
const data = await this.call(FUNC_GET_COURSES_BY_TIMELINE, { classification: "all", limit: 100, offset });
|
|
28938
|
-
if (!
|
|
29332
|
+
if (!isRecord7(data) || !Array.isArray(data.courses) || !data.courses.length) {
|
|
28939
29333
|
break;
|
|
28940
29334
|
}
|
|
28941
29335
|
courses.push(...data.courses);
|
|
@@ -29098,19 +29492,19 @@ function placeholderUserInfo(baseUrl, userid) {
|
|
|
29098
29492
|
};
|
|
29099
29493
|
}
|
|
29100
29494
|
function parseTodoPayload(value) {
|
|
29101
|
-
return parseTodoItems(
|
|
29495
|
+
return parseTodoItems(isRecord7(value) && Array.isArray(value.events) ? value.events : []);
|
|
29102
29496
|
}
|
|
29103
29497
|
function errorMessage(value) {
|
|
29104
29498
|
return value instanceof Error ? value.message : "Unknown Moodle error";
|
|
29105
29499
|
}
|
|
29106
|
-
function chunks(values,
|
|
29500
|
+
function chunks(values, size2) {
|
|
29107
29501
|
const result = [];
|
|
29108
|
-
for (let index = 0; index < values.length; index +=
|
|
29109
|
-
result.push(values.slice(index, index +
|
|
29502
|
+
for (let index = 0; index < values.length; index += size2) {
|
|
29503
|
+
result.push(values.slice(index, index + size2));
|
|
29110
29504
|
}
|
|
29111
29505
|
return result;
|
|
29112
29506
|
}
|
|
29113
|
-
function
|
|
29507
|
+
function isRecord7(value) {
|
|
29114
29508
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
29115
29509
|
}
|
|
29116
29510
|
function isLoginErrorCode(code) {
|
|
@@ -29133,7 +29527,7 @@ function safeUrl(value) {
|
|
|
29133
29527
|
}
|
|
29134
29528
|
|
|
29135
29529
|
// src/mcp/gateway.ts
|
|
29136
|
-
var
|
|
29530
|
+
var import_node_html_parser4 = __toESM(require_dist(), 1);
|
|
29137
29531
|
var MAX_MCP_FILE_BYTES = 16 * 1024 * 1024;
|
|
29138
29532
|
var MoodleGatewayError = class extends Error {
|
|
29139
29533
|
code;
|
|
@@ -29143,13 +29537,14 @@ var MoodleGatewayError = class extends Error {
|
|
|
29143
29537
|
this.code = code;
|
|
29144
29538
|
}
|
|
29145
29539
|
};
|
|
29146
|
-
function createMoodleGateway(client) {
|
|
29540
|
+
function createMoodleGateway(client, hooks = {}) {
|
|
29147
29541
|
return {
|
|
29148
29542
|
getUser: () => client.getSiteInfo(),
|
|
29149
29543
|
listThreads: (id2) => client.getForumDiscussionRefs ? client.getForumDiscussionRefs(id2) : Promise.resolve([]),
|
|
29150
29544
|
listNewsForums: (id2) => client.getNewsForums ? client.getNewsForums(id2) : Promise.resolve([]),
|
|
29151
29545
|
getOverview: (input3) => client.getOverview(input3.todoLimit, input3.todoDays, input3.alertsLimit),
|
|
29152
29546
|
...client.getTodo ? { getDue: (days, courseId) => client.getTodo(Number.MAX_SAFE_INTEGER, days, courseId) } : {},
|
|
29547
|
+
...client.submitAssignmentFiles ? { submitAssignment: (input3) => client.submitAssignmentFiles({ ...input3, ...hooks.onSubmitProgress ? { onProgress: hooks.onSubmitProgress } : {} }) } : {},
|
|
29153
29548
|
listCourses: () => client.getCourses(),
|
|
29154
29549
|
async getCourse({ courseId }) {
|
|
29155
29550
|
const [courses, sections] = await Promise.all([
|
|
@@ -29273,7 +29668,7 @@ async function fileFromActivity(client, activityId) {
|
|
|
29273
29668
|
return { url: entry.url, name: entry.name };
|
|
29274
29669
|
}
|
|
29275
29670
|
function resourceLinks(html, baseUrl) {
|
|
29276
|
-
const root = (0,
|
|
29671
|
+
const root = (0, import_node_html_parser4.parse)(html);
|
|
29277
29672
|
const entries = root.querySelectorAll(".resourceworkaround a[href], .resourcecontent a[href], a.resourceworkaround[href]").map((link) => ({
|
|
29278
29673
|
name: link.textContent.trim(),
|
|
29279
29674
|
url: new URL(link.getAttribute("href") ?? "", baseUrl).toString()
|
|
@@ -29327,7 +29722,7 @@ function isHtml(response) {
|
|
|
29327
29722
|
return type.includes("text/html") || type.includes("application/xhtml+xml");
|
|
29328
29723
|
}
|
|
29329
29724
|
function looksLikeLoginPage(html) {
|
|
29330
|
-
const root = (0,
|
|
29725
|
+
const root = (0, import_node_html_parser4.parse)(html);
|
|
29331
29726
|
return root.querySelector('form[action*="/login/"], input[name="password"], #page-login-index') !== null || /<title>\s*(?:log in|login)/iu.test(html);
|
|
29332
29727
|
}
|
|
29333
29728
|
function contentType(response) {
|
|
@@ -29400,6 +29795,8 @@ var counts = external_exports.object(Object.fromEntries(["notification_count", "
|
|
|
29400
29795
|
var postSchema = external_exports.object({ id, parent_id: n, name: s, author: external_exports.object({ id, name: s }), time_created: id, created: s, message_text: s, links: external_exports.array(external_exports.object({ text: s, url: s })).optional() });
|
|
29401
29796
|
var gradeSchema = external_exports.object({ unit_id: id, code: s, graded: id, total: id, total_grade: s, total_range: s, total_percentage: s, items: external_exports.array(external_exports.object({ name: s, type: s, grade: s, range: s, percentage: s, weight: s, contribution: s, feedback: s, status: s, due: s, due_at: n })).optional() });
|
|
29402
29797
|
var searchSchema = external_exports.object({ unit_id: id, forum_id: id, discussion_id: id, name: s, post_id: id, snippet: s, time_created: n, created: s, url: s });
|
|
29798
|
+
var submissionFile = external_exports.object({ name: external_exports.string(), bytes: n, url: s });
|
|
29799
|
+
var receiptSchema = external_exports.object({ id, name: s, unit_id: n, url: s, action: external_exports.enum(["planned", "saved", "submitted"]), submission_status: s, grading_status: s, due: s, time_remaining: s, last_modified: s, statement: s, statement_accepted: external_exports.boolean().optional(), files: external_exports.array(submissionFile).optional(), uploads: external_exports.array(external_exports.object({ name: external_exports.string(), bytes: id, path: s })).optional(), removed: external_exports.array(external_exports.string()).optional(), limits: external_exports.object({ max_bytes: n, max_files: n, area_max_bytes: n, accepted_types: external_exports.array(external_exports.string()).optional() }).optional(), checked_at: external_exports.string() });
|
|
29403
29800
|
var input2 = (shape) => external_exports.object(shape).strict();
|
|
29404
29801
|
var list = (key, value) => external_exports.object({ [key]: external_exports.array(value).optional(), total: id });
|
|
29405
29802
|
var intentContracts = {
|
|
@@ -29413,6 +29810,7 @@ var intentContracts = {
|
|
|
29413
29810
|
news: { when: "announcements", command: "moodle news [UNIT]", what: "Latest announcement threads with first-post text.", instead: "search_forums for other discussions", refs: "optional unit code, name, id or URL", then: "thread with discussion id", cost: "up to 5 announcements by default", input: input2({ unit: ref.optional(), limit: limit.default(5) }), output: list("news", external_exports.object({ id, name: s, unit_id: id, unit_code: s, forum_id: id, post: postSchema.optional() })) },
|
|
29414
29811
|
thread: { when: "discussion posts", command: "moodle threads show ID", what: "A discussion and a page of compact posts, with attachment links.", instead: "news for announcements", refs: "discussion_id; offset and limit", then: "increase offset while posts_total exceeds returned", cost: "up to 20 posts by default", input: input2({ discussion_id: external_exports.number().int().positive(), limit, offset: external_exports.number().int().nonnegative().default(0) }), output: external_exports.object({ thread: external_exports.object({ id, name: s, unit_id: id, forum_id: id, url: s, posts: external_exports.array(postSchema).optional(), posts_total: id, offset: id }) }) },
|
|
29415
29812
|
search_forums: { when: "forum post text", command: 'moodle forums search "QUERY" --unit UNIT', what: "Matching posts with unit and forum name maps.", instead: "find for activity names", refs: "query, optional unit or courseId and forumId", then: "thread with discussion_id", cost: "bounded forum scan; total covers scanned scope", input: input2({ query: external_exports.string().trim().min(1), unit: ref.optional(), courseId: external_exports.number().int().positive().optional(), forumId: external_exports.number().int().positive().optional(), limit, includePostText: external_exports.boolean().default(true), titlesOnly: external_exports.boolean().default(false), unreadOnly: external_exports.boolean().default(false), sortBy: external_exports.enum(["relevance", "recent"]).default("relevance"), maxForums: limit.default(20), maxDiscussionsPerForum: limit.default(50) }), output: external_exports.object({ results: external_exports.array(searchSchema).optional(), total: id, forums: external_exports.record(external_exports.string(), external_exports.string()).optional(), units: external_exports.record(external_exports.string(), external_exports.string()).optional(), scope: external_exports.object({ max_forums: id, max_discussions_per_forum: id }) }) },
|
|
29813
|
+
submit: { when: "upload assignment files", command: 'moodle submit "UNIT TASK" FILE... [--final]', what: "Upload local files into an assignment; returns the receipt Moodle shows afterwards.", instead: "item for status only", refs: "assignment id, same-site URL or UNIT TASK phrase; local file paths", then: "dry_run (default) only plans; show the plan to the person, then rerun with dry_run false; final submits for grading and cannot be undone", cost: "writes to Moodle", input: input2({ ref, files: external_exports.array(external_exports.string().trim().min(1)).max(20).default([]), final: external_exports.boolean().default(false), replace: external_exports.boolean().default(false), accept_statement: external_exports.boolean().default(false), dry_run: external_exports.boolean().default(true) }), output: external_exports.object({ submission: receiptSchema }) },
|
|
29416
29814
|
file: { when: "download a file", command: 'moodle get "UNIT TASK" --to DIR', what: "One authenticated file as embedded content, at most 16 MiB.", instead: "item for file choices", refs: "resource id, same-site URL, or UNIT TASK phrase", then: "read the returned resource", cost: "binary content up to 16 MiB", input: input2({ ref }), output: external_exports.object({ file: external_exports.object({ name: external_exports.string(), mime_type: external_exports.string(), bytes: id, uri: external_exports.string() }) }) }
|
|
29417
29815
|
};
|
|
29418
29816
|
function intentDescription(name) {
|
|
@@ -29472,9 +29870,9 @@ function postRow(p, subject, tz = "UTC") {
|
|
|
29472
29870
|
}
|
|
29473
29871
|
|
|
29474
29872
|
// src/intents.ts
|
|
29475
|
-
async function inParallel2(items,
|
|
29873
|
+
async function inParallel2(items, size2, run) {
|
|
29476
29874
|
const results = [];
|
|
29477
|
-
for (let index = 0; index < items.length; index +=
|
|
29875
|
+
for (let index = 0; index < items.length; index += size2) results.push(...await Promise.all(items.slice(index, index + size2).map(run)));
|
|
29478
29876
|
return results;
|
|
29479
29877
|
}
|
|
29480
29878
|
function createIntentService(gateway, now = () => Date.now()) {
|
|
@@ -29665,6 +30063,12 @@ function createIntentService(gateway, now = () => Date.now()) {
|
|
|
29665
30063
|
result = { file: { name: file3.name, mime_type: file3.mimeType, bytes: file3.bytes, uri: file3.uri } };
|
|
29666
30064
|
break;
|
|
29667
30065
|
}
|
|
30066
|
+
case "submit": {
|
|
30067
|
+
if (!gateway.submitAssignment) throw new MoodleGatewayError("MOODLE_TOOL_UNAVAILABLE", "Submitting needs local files; run moodle submit or the local MCP server on the machine that holds them.");
|
|
30068
|
+
const activityId = await resolveItem(input3.ref);
|
|
30069
|
+
result = { submission: await gateway.submitAssignment({ activityId, files: input3.files, final: Boolean(input3.final), replace: Boolean(input3.replace), acceptStatement: Boolean(input3.accept_statement), dryRun: Boolean(input3.dry_run) }) };
|
|
30070
|
+
break;
|
|
30071
|
+
}
|
|
29668
30072
|
}
|
|
29669
30073
|
return intentContracts[name].output.parse(stripEmpty(result));
|
|
29670
30074
|
}
|
|
@@ -29683,13 +30087,23 @@ var READ_ONLY_ANNOTATIONS = {
|
|
|
29683
30087
|
idempotentHint: true,
|
|
29684
30088
|
openWorldHint: true
|
|
29685
30089
|
};
|
|
30090
|
+
var WRITE_ANNOTATIONS = {
|
|
30091
|
+
readOnlyHint: false,
|
|
30092
|
+
destructiveHint: true,
|
|
30093
|
+
idempotentHint: false,
|
|
30094
|
+
openWorldHint: true
|
|
30095
|
+
};
|
|
30096
|
+
var WRITE_TOOLS = /* @__PURE__ */ new Set(["submit"]);
|
|
29686
30097
|
var aliases = { get_overview: "home", list_courses: "units", get_course: "unit", get_activity: "item", get_grades: "grades", get_thread: "thread", get_file: "file" };
|
|
29687
30098
|
var TOOL_CATALOG = Object.entries(intentContracts).map(([name, contract]) => ({
|
|
29688
30099
|
name,
|
|
29689
30100
|
description: intentDescription(name),
|
|
29690
30101
|
inputSchema: compactSchema(external_exports.toJSONSchema(contract.input, { io: "input" })),
|
|
29691
|
-
annotations: READ_ONLY_ANNOTATIONS
|
|
30102
|
+
annotations: WRITE_TOOLS.has(name) ? WRITE_ANNOTATIONS : READ_ONLY_ANNOTATIONS
|
|
29692
30103
|
}));
|
|
30104
|
+
function toolsFor(gateway) {
|
|
30105
|
+
return gateway.submitAssignment ? TOOL_CATALOG : TOOL_CATALOG.filter((tool) => !WRITE_TOOLS.has(tool.name));
|
|
30106
|
+
}
|
|
29693
30107
|
var TOOL_OUTPUT_SCHEMAS = Object.fromEntries(Object.entries(intentContracts).map(([name, contract]) => [name, compactSchema(external_exports.toJSONSchema(contract.output))]));
|
|
29694
30108
|
function compactSchema(value) {
|
|
29695
30109
|
if (Array.isArray(value)) return value.map(compactSchema);
|
|
@@ -29727,7 +30141,7 @@ function createMoodleMcpServer(gateway, options = {}) {
|
|
|
29727
30141
|
protocolVersion,
|
|
29728
30142
|
capabilities: { tools: { listChanged: false } },
|
|
29729
30143
|
serverInfo,
|
|
29730
|
-
instructions: "Read-only access to the authenticated user's Moodle data."
|
|
30144
|
+
instructions: gateway.submitAssignment ? "Access to the authenticated user's Moodle data. Only submit writes; it defaults to a dry run." : "Read-only access to the authenticated user's Moodle data."
|
|
29731
30145
|
});
|
|
29732
30146
|
}
|
|
29733
30147
|
if (request.method === "ping") {
|
|
@@ -29735,7 +30149,7 @@ function createMoodleMcpServer(gateway, options = {}) {
|
|
|
29735
30149
|
}
|
|
29736
30150
|
if (request.method === "tools/list") {
|
|
29737
30151
|
return jsonRpcSuccess(id2, {
|
|
29738
|
-
tools:
|
|
30152
|
+
tools: toolsFor(gateway),
|
|
29739
30153
|
resultType: "complete",
|
|
29740
30154
|
_meta: RESULT_META
|
|
29741
30155
|
});
|
|
@@ -29791,8 +30205,9 @@ async function callTool(gateway, params) {
|
|
|
29791
30205
|
const requested = typeof params?.name === "string" ? params.name : "";
|
|
29792
30206
|
const name = Object.hasOwn(aliases, requested) ? aliases[requested] : requested;
|
|
29793
30207
|
if (!Object.hasOwn(intentContracts, name) && !["get_user", "list_activities", "list_forums"].includes(name)) throw new McpCallError("TOOL_NOT_FOUND", `Unknown Moodle tool: ${requested || "<missing>"}`);
|
|
30208
|
+
if (WRITE_TOOLS.has(name) && !gateway.submitAssignment) throw new McpCallError("TOOL_NOT_FOUND", `${requested} is only available on a local MCP server with access to the files.`);
|
|
29794
30209
|
const raw = params?.arguments ?? {};
|
|
29795
|
-
if (!
|
|
30210
|
+
if (!isRecord8(raw)) throw new McpCallError("INVALID_TOOL_ARGUMENTS", "Tool arguments must be an object.");
|
|
29796
30211
|
const args = { ...raw };
|
|
29797
30212
|
if (Object.hasOwn(aliases, requested)) {
|
|
29798
30213
|
const renames = { courseId: "unit", activityId: "ref", discussionId: "discussion_id", source: "ref", todoDays: "days", gradedOnly: "graded_only" };
|
|
@@ -29840,7 +30255,7 @@ async function callTool(gateway, params) {
|
|
|
29840
30255
|
const mapped2 = { type: "MOODLE_RESULT_INVALID", message: `Moodle returned ${name} data in an unexpected shape.`, hint: "Retry once; if it persists, run the same command locally with --verbose and report the tool name.", issues: error62.issues.slice(0, 5).map((issue2) => ({ path: issue2.path.join("."), message: issue2.message })) };
|
|
29841
30256
|
return { content: [{ type: "text", text: JSON.stringify({ error: mapped2 }) }], structuredContent: { error: mapped2 }, isError: true, resultType: "complete", _meta: RESULT_META };
|
|
29842
30257
|
}
|
|
29843
|
-
const mapped = error62 instanceof ReferenceError2 ? { type: error62.code, code: error62.code, message: error62.message, hint: error62.hint, candidates: error62.candidates } : mapMoodleError(error62);
|
|
30258
|
+
const mapped = error62 instanceof ReferenceError2 ? { type: error62.code, code: error62.code, message: error62.message, hint: error62.hint, candidates: error62.candidates } : mapMoodleError(error62, WRITE_TOOLS.has(name));
|
|
29844
30259
|
return { content: [{ type: "text", text: JSON.stringify({ error: mapped }) }], structuredContent: { error: mapped }, isError: true, resultType: "complete", _meta: RESULT_META };
|
|
29845
30260
|
}
|
|
29846
30261
|
}
|
|
@@ -29859,9 +30274,11 @@ function toolContent(name, payload, structuredContent) {
|
|
|
29859
30274
|
}
|
|
29860
30275
|
];
|
|
29861
30276
|
}
|
|
29862
|
-
function mapMoodleError(error62) {
|
|
29863
|
-
const
|
|
29864
|
-
const code = typeof
|
|
30277
|
+
function mapMoodleError(error62, verbatim = false) {
|
|
30278
|
+
const record3 = isRecord8(error62) ? error62 : {};
|
|
30279
|
+
const code = typeof record3.code === "string" ? record3.code : "";
|
|
30280
|
+
const own2 = verbatim && typeof record3.message === "string" && record3.message.trim() && code !== "auth" ? record3.message : void 0;
|
|
30281
|
+
const ownHint = verbatim && typeof record3.hint === "string" && record3.hint.trim() ? record3.hint : void 0;
|
|
29865
30282
|
const typeByCode = {
|
|
29866
30283
|
auth: "MOODLE_AUTH_REQUIRED",
|
|
29867
30284
|
not_found: "MOODLE_NOT_FOUND",
|
|
@@ -29870,8 +30287,8 @@ function mapMoodleError(error62) {
|
|
|
29870
30287
|
};
|
|
29871
30288
|
const type = code.startsWith("MOODLE_") ? code : typeByCode[code] ?? "MOODLE_UPSTREAM_ERROR";
|
|
29872
30289
|
const message = type === "MOODLE_AUTH_REQUIRED" ? "The Moodle session has expired. Sign in again." : type === "MOODLE_NOT_FOUND" || type === "MOODLE_COURSE_NOT_FOUND" ? "The requested Moodle item was not found." : type === "MOODLE_INVALID_REQUEST" ? "The Moodle request is invalid." : "Moodle could not complete the request.";
|
|
29873
|
-
const moodleCode = typeof
|
|
29874
|
-
return { type, message, hint: type === "MOODLE_AUTH_REQUIRED" ? "Run moodle mcp login for a remote server, or moodle auth login locally; then retry." : "Run moodle doctor, or refine the request using units and find.", ...type === "MOODLE_AUTH_REQUIRED" ? { recovery: { action: "moodle mcp login", where: "machine running moodle-cli", then: "retry this tool" } } : {}, ...moodleCode ? { moodleCode } : {} };
|
|
30290
|
+
const moodleCode = typeof record3.moodleErrorCode === "string" && /^[a-z][a-z0-9_]{0,63}$/u.test(record3.moodleErrorCode) ? record3.moodleErrorCode : void 0;
|
|
30291
|
+
return { type, message: own2 ?? message, hint: ownHint ?? (type === "MOODLE_AUTH_REQUIRED" ? "Run moodle mcp login for a remote server, or moodle auth login locally; then retry." : "Run moodle doctor, or refine the request using units and find."), ...type === "MOODLE_AUTH_REQUIRED" ? { recovery: { action: "moodle mcp login", where: "machine running moodle-cli", then: "retry this tool" } } : {}, ...moodleCode ? { moodleCode } : {} };
|
|
29875
30292
|
}
|
|
29876
30293
|
var McpCallError = class extends Error {
|
|
29877
30294
|
type;
|
|
@@ -29883,11 +30300,11 @@ var McpCallError = class extends Error {
|
|
|
29883
30300
|
this.details = details;
|
|
29884
30301
|
}
|
|
29885
30302
|
};
|
|
29886
|
-
function
|
|
30303
|
+
function isRecord8(value) {
|
|
29887
30304
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
29888
30305
|
}
|
|
29889
30306
|
function isMoodleFile(value) {
|
|
29890
|
-
return
|
|
30307
|
+
return isRecord8(value) && typeof value.name === "string" && typeof value.mimeType === "string" && typeof value.bytes === "number" && typeof value.uri === "string" && typeof value.blob === "string";
|
|
29891
30308
|
}
|
|
29892
30309
|
|
|
29893
30310
|
// src/worker/moodle-upstream.ts
|
|
@@ -29958,17 +30375,17 @@ var FetchMoodleSessionUpstream = class {
|
|
|
29958
30375
|
return { alive: false, remainingSeconds: null };
|
|
29959
30376
|
}
|
|
29960
30377
|
if (!Array.isArray(body) || body.length === 0) return { alive: null, remainingSeconds: null };
|
|
29961
|
-
const first2 =
|
|
30378
|
+
const first2 = isRecord9(body[0]) ? body[0] : void 0;
|
|
29962
30379
|
if (first2?.error) {
|
|
29963
|
-
const exception =
|
|
30380
|
+
const exception = isRecord9(first2.exception) ? first2.exception : void 0;
|
|
29964
30381
|
const errorCode = typeof exception?.errorcode === "string" ? exception.errorcode : "";
|
|
29965
30382
|
if (errorCode === "servicerequireslogin" || errorCode === "sitepolicynotagreed") {
|
|
29966
30383
|
return { alive: false, remainingSeconds: null };
|
|
29967
30384
|
}
|
|
29968
30385
|
return { alive: null, remainingSeconds: null };
|
|
29969
30386
|
}
|
|
29970
|
-
const last =
|
|
29971
|
-
const data =
|
|
30387
|
+
const last = isRecord9(body.at(-1)) ? body.at(-1) : void 0;
|
|
30388
|
+
const data = isRecord9(last?.data) ? last.data : void 0;
|
|
29972
30389
|
const remainingSeconds = typeof data?.timeremaining === "number" ? data.timeremaining : null;
|
|
29973
30390
|
const rotatedCookie = cookieFromSetCookie(response.headers.get("set-cookie"), session.cookieName);
|
|
29974
30391
|
return {
|
|
@@ -29990,7 +30407,7 @@ function cookieFromSetCookie(header, cookieName) {
|
|
|
29990
30407
|
const escaped = cookieName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
29991
30408
|
return header.match(new RegExp(`(?:^|,\\s*)${escaped}=([^;,\\s]+)`))?.[1];
|
|
29992
30409
|
}
|
|
29993
|
-
function
|
|
30410
|
+
function isRecord9(value) {
|
|
29994
30411
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
29995
30412
|
}
|
|
29996
30413
|
|
|
@@ -30212,16 +30629,16 @@ var SessionBroker = class {
|
|
|
30212
30629
|
});
|
|
30213
30630
|
}
|
|
30214
30631
|
async readSession(storage) {
|
|
30215
|
-
const
|
|
30216
|
-
if (!
|
|
30217
|
-
if (
|
|
30218
|
-
const decrypted = await decryptValue(
|
|
30632
|
+
const record3 = await storage.get(SESSION_STORAGE_KEY);
|
|
30633
|
+
if (!record3) return void 0;
|
|
30634
|
+
if (record3.version === 2 && typeof record3.encrypted_session === "string") {
|
|
30635
|
+
const decrypted = await decryptValue(record3.encrypted_session, await this.keyring());
|
|
30219
30636
|
const session = JSON.parse(decrypted.value);
|
|
30220
30637
|
if (typeof session.cookie_value !== "string" || typeof session.sesskey !== "string" || !Number.isSafeInteger(session.moodle_user_id) || session.moodle_user_id <= 0) throw new Error("Invalid encrypted session.");
|
|
30221
30638
|
return session;
|
|
30222
30639
|
}
|
|
30223
|
-
if (typeof
|
|
30224
|
-
const { encrypted_cookie, ...metadata } =
|
|
30640
|
+
if (typeof record3.encrypted_cookie !== "string") throw new Error("Invalid legacy session.");
|
|
30641
|
+
const { encrypted_cookie, ...metadata } = record3;
|
|
30225
30642
|
const cookie = await decryptValue(encrypted_cookie, await this.keyring());
|
|
30226
30643
|
return { ...metadata, cookie_value: cookie.value };
|
|
30227
30644
|
}
|
|
@@ -30233,10 +30650,10 @@ var SessionBroker = class {
|
|
|
30233
30650
|
}
|
|
30234
30651
|
async loadSession() {
|
|
30235
30652
|
return this.transaction(async (storage) => {
|
|
30236
|
-
const
|
|
30237
|
-
if (!
|
|
30653
|
+
const record3 = await storage.get(SESSION_STORAGE_KEY);
|
|
30654
|
+
if (!record3) return void 0;
|
|
30238
30655
|
const session = await this.readSession(storage);
|
|
30239
|
-
if (session && (
|
|
30656
|
+
if (session && (record3.version !== 2 || (await decryptValue(record3.encrypted_session, await this.keyring())).needsRotation)) {
|
|
30240
30657
|
await this.writeSession(storage, session);
|
|
30241
30658
|
}
|
|
30242
30659
|
return session;
|
|
@@ -30312,14 +30729,14 @@ function normalizeOrigin(value) {
|
|
|
30312
30729
|
}
|
|
30313
30730
|
}
|
|
30314
30731
|
function isSessionCandidate(value) {
|
|
30315
|
-
if (!
|
|
30732
|
+
if (!isRecord10(value)) return false;
|
|
30316
30733
|
return typeof value.moodleOrigin === "string" && typeof value.cookieName === "string" && /^MoodleSession[A-Za-z0-9_-]*$/.test(value.cookieName) && typeof value.cookieValue === "string" && value.cookieValue.length > 0 && value.cookieValue.length <= 4096 && !/[\u0000-\u001f\u007f;]/.test(value.cookieValue) && (value.expectedRevision === null || Number.isInteger(value.expectedRevision) && value.expectedRevision >= 0);
|
|
30317
30734
|
}
|
|
30318
|
-
function
|
|
30735
|
+
function isRecord10(value) {
|
|
30319
30736
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
30320
30737
|
}
|
|
30321
30738
|
function isMcpEnvelope(value) {
|
|
30322
|
-
if (!
|
|
30739
|
+
if (!isRecord10(value) || !("request" in value) || !isRecord10(value.context)) return false;
|
|
30323
30740
|
return (value.context.protocolVersion === void 0 || typeof value.context.protocolVersion === "string") && (value.context.method === void 0 || typeof value.context.method === "string") && (value.context.toolName === void 0 || typeof value.context.toolName === "string");
|
|
30324
30741
|
}
|
|
30325
30742
|
|