moodle-cli 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -3
- package/SKILL.md +4 -3
- package/agents/openai.yaml +1 -1
- package/dist/moodle.js +1224 -635
- package/dist/worker/recovery.js +777 -384
- package/dist/worker/worker.js +777 -384
- package/package.json +2 -2
- package/references/command-reference.md +6 -1
package/dist/worker/recovery.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.0";
|
|
26456
26456
|
|
|
26457
26457
|
// src/worker/http.ts
|
|
26458
26458
|
var HEALTH_PATH = "/healthz";
|
|
@@ -27057,6 +27057,9 @@ var FUNC_GET_UNREAD_CONVERSATION_COUNTS = "core_message_get_unread_conversation_
|
|
|
27057
27057
|
var FUNC_GET_DISCUSSION_POSTS = "mod_forum_get_discussion_posts";
|
|
27058
27058
|
var DEFAULT_SESSION_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
27059
27059
|
|
|
27060
|
+
// src/moodle-assign-core.ts
|
|
27061
|
+
var import_node_html_parser3 = __toESM(require_dist(), 1);
|
|
27062
|
+
|
|
27060
27063
|
// src/html-utils.ts
|
|
27061
27064
|
var import_node_html_parser = __toESM(require_dist(), 1);
|
|
27062
27065
|
function htmlToStructuredContent(html, baseUrl) {
|
|
@@ -27126,262 +27129,6 @@ function decodeHtml(value) {
|
|
|
27126
27129
|
return value.replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'");
|
|
27127
27130
|
}
|
|
27128
27131
|
|
|
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
27132
|
// src/scraper.ts
|
|
27386
27133
|
var import_node_html_parser2 = __toESM(require_dist(), 1);
|
|
27387
27134
|
function parseMoodleErrorHtml(html) {
|
|
@@ -27413,8 +27160,8 @@ function parseMoodleErrorHtml(html) {
|
|
|
27413
27160
|
function parsePageContext(html, baseUrl) {
|
|
27414
27161
|
const root = (0, import_node_html_parser2.parse)(html);
|
|
27415
27162
|
const config2 = parseMoodleConfig(html);
|
|
27416
|
-
const sesskey =
|
|
27417
|
-
const userid =
|
|
27163
|
+
const sesskey = stringValue2(config2.sesskey).trim();
|
|
27164
|
+
const userid = numberValue(config2.userId) || numberValue(root.querySelector("[data-user-id]")?.getAttribute("data-user-id"));
|
|
27418
27165
|
if (!sesskey || !userid) {
|
|
27419
27166
|
throw new Error("Session appears invalid: could not load authenticated Moodle context");
|
|
27420
27167
|
}
|
|
@@ -27426,8 +27173,8 @@ function parsePageContext(html, baseUrl) {
|
|
|
27426
27173
|
fullname: cleanNodeText(root.querySelector(".userfullname")),
|
|
27427
27174
|
sitename: extractSitename(root),
|
|
27428
27175
|
siteurl: baseUrl,
|
|
27429
|
-
...config2.timezone ? { timezone:
|
|
27430
|
-
lang:
|
|
27176
|
+
...config2.timezone ? { timezone: stringValue2(config2.timezone) } : {},
|
|
27177
|
+
lang: stringValue2(config2.language) || root.querySelector("html")?.getAttribute("lang") || ""
|
|
27431
27178
|
}
|
|
27432
27179
|
};
|
|
27433
27180
|
}
|
|
@@ -27967,7 +27714,7 @@ function numberQueryValue(href, key) {
|
|
|
27967
27714
|
return null;
|
|
27968
27715
|
}
|
|
27969
27716
|
}
|
|
27970
|
-
function
|
|
27717
|
+
function numberValue(value) {
|
|
27971
27718
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
27972
27719
|
return value;
|
|
27973
27720
|
}
|
|
@@ -27976,7 +27723,7 @@ function numberValue2(value) {
|
|
|
27976
27723
|
}
|
|
27977
27724
|
return 0;
|
|
27978
27725
|
}
|
|
27979
|
-
function
|
|
27726
|
+
function stringValue2(value) {
|
|
27980
27727
|
return typeof value === "string" ? value : value == null ? "" : String(value);
|
|
27981
27728
|
}
|
|
27982
27729
|
function fileEntry(name, url2, baseUrl) {
|
|
@@ -27990,6 +27737,619 @@ function unique(items) {
|
|
|
27990
27737
|
return [...new Set(items)];
|
|
27991
27738
|
}
|
|
27992
27739
|
|
|
27740
|
+
// src/moodle-assign-core.ts
|
|
27741
|
+
async function submitAssignmentFiles(deps, request) {
|
|
27742
|
+
const id2 = request.activityId;
|
|
27743
|
+
if (!Number.isSafeInteger(id2) || id2 <= 0) throw deps.usage("The assignment id must be a positive integer.");
|
|
27744
|
+
if (!request.files.length && !request.final) throw deps.usage("Give at least one file to upload, or use --final to submit the existing draft.");
|
|
27745
|
+
const seen = /* @__PURE__ */ new Set();
|
|
27746
|
+
for (const file3 of request.files) {
|
|
27747
|
+
if (!file3.name || /[\\/]/u.test(file3.name)) throw deps.usage(`'${file3.name}' is not a plain file name.`);
|
|
27748
|
+
if (seen.has(file3.name.toLowerCase())) throw deps.usage(`'${file3.name}' is given twice; Moodle keeps one file per name.`);
|
|
27749
|
+
seen.add(file3.name.toLowerCase());
|
|
27750
|
+
}
|
|
27751
|
+
const viewUrl = `${deps.baseUrl}${ASSIGN_VIEW_PATH}?id=${id2}`;
|
|
27752
|
+
request.onProgress?.("Reading the assignment");
|
|
27753
|
+
const before = parseReceiptPage(await pageText(deps, viewUrl), id2, deps.baseUrl);
|
|
27754
|
+
const form = parseSubmissionForm(await pageText(deps, `${viewUrl}&action=editsubmission`), deps);
|
|
27755
|
+
const draft = await listDraftFiles(deps, form);
|
|
27756
|
+
const removed = request.replace ? draft.map((file3) => file3.name) : [];
|
|
27757
|
+
const kept = request.replace ? [] : draft.filter((file3) => !seen.has(file3.name.toLowerCase()));
|
|
27758
|
+
checkLimits(deps, form, kept, request.files);
|
|
27759
|
+
let statement = form.statement;
|
|
27760
|
+
let confirm;
|
|
27761
|
+
if (request.final && !statement) {
|
|
27762
|
+
confirm = parseConfirmForm(await pageText(deps, `${viewUrl}&action=submit`), deps);
|
|
27763
|
+
statement = confirm.statement;
|
|
27764
|
+
}
|
|
27765
|
+
if (statement && !request.acceptStatement) {
|
|
27766
|
+
throw deps.usage(`Moodle requires you to accept this statement: "${statement}"`, "Re-run with --accept-statement once you agree.");
|
|
27767
|
+
}
|
|
27768
|
+
const limits = describeLimits(form);
|
|
27769
|
+
const uploads = request.files.map((file3) => ({ name: file3.name, bytes: file3.bytes.byteLength, ...file3.path ? { path: file3.path } : {} }));
|
|
27770
|
+
if (request.dryRun) {
|
|
27771
|
+
return {
|
|
27772
|
+
...before,
|
|
27773
|
+
action: "planned",
|
|
27774
|
+
files: draft.map((file3) => ({ name: file3.name, bytes: file3.bytes })),
|
|
27775
|
+
uploads,
|
|
27776
|
+
removed,
|
|
27777
|
+
limits,
|
|
27778
|
+
...statement ? { statement, statement_accepted: true } : {},
|
|
27779
|
+
checked_at: timestamp(deps)
|
|
27780
|
+
};
|
|
27781
|
+
}
|
|
27782
|
+
if (removed.length) {
|
|
27783
|
+
request.onProgress?.(`Removing ${removed.join(", ")}`);
|
|
27784
|
+
await deleteDraftFiles(deps, form, draft);
|
|
27785
|
+
}
|
|
27786
|
+
const storedNames = [];
|
|
27787
|
+
for (const [index, file3] of request.files.entries()) {
|
|
27788
|
+
request.onProgress?.(`Uploading ${file3.name} (${index + 1}/${request.files.length})`);
|
|
27789
|
+
storedNames.push(await uploadDraftFile(deps, form, file3));
|
|
27790
|
+
}
|
|
27791
|
+
if (storedNames.length || removed.length) {
|
|
27792
|
+
request.onProgress?.("Saving the submission");
|
|
27793
|
+
const savedHtml = await postForm(deps, form.action, [...form.fields, ...form.statement ? [["submissionstatement", "1"]] : [], ["submitbutton", "Save changes"]]);
|
|
27794
|
+
if (savedHtml !== null) throw deps.fail(`Moodle did not save the submission: ${noticesOf(savedHtml) || "it returned the edit form again without a reason"}`);
|
|
27795
|
+
}
|
|
27796
|
+
request.onProgress?.("Reading the receipt");
|
|
27797
|
+
let receipt = parseReceiptPage(await pageText(deps, viewUrl), id2, deps.baseUrl);
|
|
27798
|
+
const listed = new Set(receipt.files.map((file3) => file3.name.toLowerCase()));
|
|
27799
|
+
const missing = storedNames.filter((name) => !listed.has(name.toLowerCase()));
|
|
27800
|
+
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.`);
|
|
27801
|
+
let action = "saved";
|
|
27802
|
+
if (isSubmitted(receipt.submission_status)) action = "submitted";
|
|
27803
|
+
else if (request.final) {
|
|
27804
|
+
request.onProgress?.("Submitting for grading");
|
|
27805
|
+
confirm ??= parseConfirmForm(await pageText(deps, `${viewUrl}&action=submit`), deps);
|
|
27806
|
+
const errorHtml = await postForm(deps, confirm.action, [...confirm.fields, ...confirm.statement ? [["submissionstatement", "1"]] : [], ["submitbutton", "Continue"]]);
|
|
27807
|
+
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"}`);
|
|
27808
|
+
receipt = parseReceiptPage(await pageText(deps, viewUrl), id2, deps.baseUrl);
|
|
27809
|
+
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.`);
|
|
27810
|
+
action = "submitted";
|
|
27811
|
+
}
|
|
27812
|
+
return {
|
|
27813
|
+
...receipt,
|
|
27814
|
+
action,
|
|
27815
|
+
uploads,
|
|
27816
|
+
removed,
|
|
27817
|
+
limits,
|
|
27818
|
+
...statement ? { statement, statement_accepted: true } : {},
|
|
27819
|
+
checked_at: timestamp(deps)
|
|
27820
|
+
};
|
|
27821
|
+
}
|
|
27822
|
+
function parseSubmissionForm(html, deps) {
|
|
27823
|
+
const root = (0, import_node_html_parser3.parse)(html);
|
|
27824
|
+
const form = formWithAction(root, "savesubmission");
|
|
27825
|
+
if (!form) {
|
|
27826
|
+
const notice = noticesOf(html);
|
|
27827
|
+
if (root.querySelectorAll("input[type=submit], button").some((button) => /begin assignment/iu.test(cleanText(button.getAttribute("value") ?? button.textContent)))) {
|
|
27828
|
+
throw deps.fail("This is a timed assignment; start it in a browser before uploading files.");
|
|
27829
|
+
}
|
|
27830
|
+
throw deps.fail(notice ? `Moodle is not accepting a submission: ${notice}` : "Moodle did not show a submission form for this assignment.");
|
|
27831
|
+
}
|
|
27832
|
+
const itemid = form.querySelector("input[name=files_filemanager]")?.getAttribute("value")?.trim() ?? "";
|
|
27833
|
+
if (!itemid) throw deps.fail("This assignment does not accept file uploads.");
|
|
27834
|
+
const options = filemanagerOptions(html, itemid);
|
|
27835
|
+
if (!options) throw deps.fail("Moodle did not describe the file upload area for this assignment.");
|
|
27836
|
+
const fields2 = formFields(form);
|
|
27837
|
+
const sesskey = fields2.find(([name]) => name === "sesskey")?.[1] ?? "";
|
|
27838
|
+
if (!sesskey) throw deps.fail("The submission form has no session key.");
|
|
27839
|
+
const picker = record2(options.filepicker);
|
|
27840
|
+
const repositories = (Array.isArray(picker.repositories) ? picker.repositories : Object.values(record2(picker.repositories))).map(record2);
|
|
27841
|
+
const upload = repositories.find((repo) => repo.type === "upload");
|
|
27842
|
+
if (!upload || upload.id === void 0) throw deps.fail("The site does not allow direct file uploads for this assignment.");
|
|
27843
|
+
const accepted = options.accepted_types;
|
|
27844
|
+
const acceptedTypes = Array.isArray(accepted) ? accepted.map(String).filter(Boolean) : accepted === void 0 || accepted === "*" ? "*" : [String(accepted)];
|
|
27845
|
+
return {
|
|
27846
|
+
action: resolveUrl(deps.baseUrl, form.getAttribute("action") || `${deps.baseUrl}${ASSIGN_VIEW_PATH}`),
|
|
27847
|
+
fields: fields2,
|
|
27848
|
+
sesskey,
|
|
27849
|
+
itemid,
|
|
27850
|
+
clientId: String(options.client_id ?? ""),
|
|
27851
|
+
contextId: String(record2(options.context).id ?? ""),
|
|
27852
|
+
repoId: String(upload.id),
|
|
27853
|
+
author: String(picker.author ?? ""),
|
|
27854
|
+
license: String(picker.defaultlicense ?? ""),
|
|
27855
|
+
maxBytes: integer2(options.maxbytes),
|
|
27856
|
+
areaMaxBytes: integer2(options.areamaxbytes),
|
|
27857
|
+
maxFiles: integer2(options.maxfiles),
|
|
27858
|
+
acceptedTypes: acceptedTypes.length === 1 && acceptedTypes[0] === "*" ? "*" : acceptedTypes,
|
|
27859
|
+
...statementOf(form)
|
|
27860
|
+
};
|
|
27861
|
+
}
|
|
27862
|
+
function parseConfirmForm(html, deps) {
|
|
27863
|
+
const root = (0, import_node_html_parser3.parse)(html);
|
|
27864
|
+
const form = formWithAction(root, "confirmsubmit");
|
|
27865
|
+
if (!form) {
|
|
27866
|
+
const notice = noticesOf(html);
|
|
27867
|
+
throw deps.fail(notice ? `Moodle is not accepting a submission for grading: ${notice}` : "Moodle did not show the submit-for-grading confirmation.");
|
|
27868
|
+
}
|
|
27869
|
+
return { action: resolveUrl(deps.baseUrl, form.getAttribute("action") || `${deps.baseUrl}${ASSIGN_VIEW_PATH}`), fields: formFields(form), ...statementOf(form) };
|
|
27870
|
+
}
|
|
27871
|
+
function parseReceiptPage(html, activityId, baseUrl) {
|
|
27872
|
+
const page2 = parseAssignmentHtml(html, activityId, baseUrl);
|
|
27873
|
+
const root = (0, import_node_html_parser3.parse)(html);
|
|
27874
|
+
const files = /* @__PURE__ */ new Map();
|
|
27875
|
+
const add = (link) => {
|
|
27876
|
+
const name = cleanText(link.textContent);
|
|
27877
|
+
const href = link.getAttribute("href") ?? "";
|
|
27878
|
+
if (name && !files.has(name.toLowerCase())) files.set(name.toLowerCase(), { name, ...href ? { url: resolveUrl(baseUrl, href) } : {} });
|
|
27879
|
+
};
|
|
27880
|
+
for (const link of root.querySelectorAll(".fileuploadsubmission a[href]")) add(link);
|
|
27881
|
+
for (const link of tableCell(root, "File submissions")?.querySelectorAll("a[href]") ?? []) add(link);
|
|
27882
|
+
return {
|
|
27883
|
+
id: activityId,
|
|
27884
|
+
name: page2.name,
|
|
27885
|
+
...page2.course_id ? { unit_id: page2.course_id } : {},
|
|
27886
|
+
url: page2.url,
|
|
27887
|
+
submission_status: page2.submission_status,
|
|
27888
|
+
grading_status: page2.grading_status,
|
|
27889
|
+
due: page2.due_pretty || cleanText(tableCell(root, "Due date")?.textContent),
|
|
27890
|
+
time_remaining: page2.time_remaining,
|
|
27891
|
+
last_modified: cleanText(tableCell(root, "Last modified")?.textContent),
|
|
27892
|
+
files: [...files.values()]
|
|
27893
|
+
};
|
|
27894
|
+
}
|
|
27895
|
+
function noticesOf(html) {
|
|
27896
|
+
const root = (0, import_node_html_parser3.parse)(html);
|
|
27897
|
+
const texts = [];
|
|
27898
|
+
for (const node2 of root.querySelectorAll(".alert, .invalid-feedback, .form-control-feedback, .error, [data-fieldtype] .text-danger")) {
|
|
27899
|
+
for (const junk of node2.querySelectorAll("button, .close")) junk.remove();
|
|
27900
|
+
const text = cleanText(node2.textContent);
|
|
27901
|
+
if (text && !texts.includes(text)) texts.push(text);
|
|
27902
|
+
}
|
|
27903
|
+
return texts.join(" ");
|
|
27904
|
+
}
|
|
27905
|
+
function formWithAction(root, action) {
|
|
27906
|
+
for (const form of root.querySelectorAll("form")) {
|
|
27907
|
+
if (form.querySelectorAll("input[name=action]").some((input3) => input3.getAttribute("value") === action)) return form;
|
|
27908
|
+
}
|
|
27909
|
+
return null;
|
|
27910
|
+
}
|
|
27911
|
+
function formFields(form) {
|
|
27912
|
+
const fields2 = [];
|
|
27913
|
+
for (const element of form.querySelectorAll("input, textarea, select")) {
|
|
27914
|
+
const name = element.getAttribute("name");
|
|
27915
|
+
if (!name) continue;
|
|
27916
|
+
const tag = element.tagName.toLowerCase();
|
|
27917
|
+
if (tag === "textarea") {
|
|
27918
|
+
fields2.push([name, element.textContent]);
|
|
27919
|
+
continue;
|
|
27920
|
+
}
|
|
27921
|
+
if (tag === "select") {
|
|
27922
|
+
const options = element.querySelectorAll("option");
|
|
27923
|
+
const chosen = options.find((option) => option.hasAttribute("selected")) ?? options[0];
|
|
27924
|
+
if (chosen) fields2.push([name, chosen.getAttribute("value") ?? cleanText(chosen.textContent)]);
|
|
27925
|
+
continue;
|
|
27926
|
+
}
|
|
27927
|
+
const type = (element.getAttribute("type") ?? "text").toLowerCase();
|
|
27928
|
+
if (["submit", "button", "image", "file", "reset"].includes(type)) continue;
|
|
27929
|
+
if ((type === "checkbox" || type === "radio") && !element.hasAttribute("checked")) continue;
|
|
27930
|
+
fields2.push([name, element.getAttribute("value") ?? (type === "checkbox" ? "on" : "")]);
|
|
27931
|
+
}
|
|
27932
|
+
return fields2;
|
|
27933
|
+
}
|
|
27934
|
+
function statementOf(form) {
|
|
27935
|
+
const box = form.querySelector("input[name=submissionstatement]");
|
|
27936
|
+
if (!box) return {};
|
|
27937
|
+
const id2 = box.getAttribute("id");
|
|
27938
|
+
const label = (id2 ? form.querySelector(`label[for="${id2}"]`) : null) ?? box.closest("label") ?? box.parentNode?.querySelector("label") ?? null;
|
|
27939
|
+
const text = cleanText(label?.textContent).replace(/\s*Required\s*$/u, "").trim();
|
|
27940
|
+
return { statement: text || "Submission statement" };
|
|
27941
|
+
}
|
|
27942
|
+
function filemanagerOptions(html, itemid) {
|
|
27943
|
+
const pattern = /M\.form_filemanager\.init\(\s*Y\s*,\s*/gu;
|
|
27944
|
+
let match;
|
|
27945
|
+
while (match = pattern.exec(html)) {
|
|
27946
|
+
const json2 = balancedObject(html, match.index + match[0].length);
|
|
27947
|
+
if (!json2) continue;
|
|
27948
|
+
try {
|
|
27949
|
+
const options = JSON.parse(json2);
|
|
27950
|
+
if (isRecord5(options) && String(options.itemid) === itemid) return options;
|
|
27951
|
+
} catch {
|
|
27952
|
+
}
|
|
27953
|
+
}
|
|
27954
|
+
return null;
|
|
27955
|
+
}
|
|
27956
|
+
function balancedObject(text, start) {
|
|
27957
|
+
if (text[start] !== "{") return null;
|
|
27958
|
+
let depth = 0;
|
|
27959
|
+
let quoted = false;
|
|
27960
|
+
for (let index = start; index < text.length; index += 1) {
|
|
27961
|
+
const char = text[index];
|
|
27962
|
+
if (quoted) {
|
|
27963
|
+
if (char === "\\") index += 1;
|
|
27964
|
+
else if (char === '"') quoted = false;
|
|
27965
|
+
} else if (char === '"') quoted = true;
|
|
27966
|
+
else if (char === "{") depth += 1;
|
|
27967
|
+
else if (char === "}") {
|
|
27968
|
+
depth -= 1;
|
|
27969
|
+
if (depth === 0) return text.slice(start, index + 1);
|
|
27970
|
+
}
|
|
27971
|
+
}
|
|
27972
|
+
return null;
|
|
27973
|
+
}
|
|
27974
|
+
function tableCell(root, label) {
|
|
27975
|
+
for (const row of root.querySelectorAll("tr")) {
|
|
27976
|
+
const cells = row.querySelectorAll("th, td");
|
|
27977
|
+
if (cells.length > 1 && cleanText(cells[0].textContent) === label) return cells[1];
|
|
27978
|
+
}
|
|
27979
|
+
return null;
|
|
27980
|
+
}
|
|
27981
|
+
async function pageText(deps, url2) {
|
|
27982
|
+
return (await deps.request(url2)).text();
|
|
27983
|
+
}
|
|
27984
|
+
async function postForm(deps, action, fields2) {
|
|
27985
|
+
const response = await deps.request(action, {
|
|
27986
|
+
method: "POST",
|
|
27987
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
27988
|
+
body: new URLSearchParams(fields2).toString()
|
|
27989
|
+
});
|
|
27990
|
+
const html = await response.text();
|
|
27991
|
+
return landedOnView(response.url) ? null : html;
|
|
27992
|
+
}
|
|
27993
|
+
function landedOnView(url2) {
|
|
27994
|
+
try {
|
|
27995
|
+
const parsed = new URL(url2);
|
|
27996
|
+
return parsed.pathname.endsWith(ASSIGN_VIEW_PATH) && (parsed.searchParams.get("action") ?? "view") === "view";
|
|
27997
|
+
} catch {
|
|
27998
|
+
return false;
|
|
27999
|
+
}
|
|
28000
|
+
}
|
|
28001
|
+
async function draftAjax(deps, form, action, params) {
|
|
28002
|
+
const body = new URLSearchParams({ sesskey: form.sesskey, client_id: form.clientId, itemid: form.itemid, ...params });
|
|
28003
|
+
const response = await deps.request(`${deps.baseUrl}/repository/draftfiles_ajax.php?action=${action}`, {
|
|
28004
|
+
method: "POST",
|
|
28005
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
28006
|
+
body: body.toString()
|
|
28007
|
+
}, { allowErrorStatus: true });
|
|
28008
|
+
return jsonOf(deps, response, `draft file ${action}`);
|
|
28009
|
+
}
|
|
28010
|
+
async function listDraftFiles(deps, form) {
|
|
28011
|
+
const data = record2(await draftAjax(deps, form, "list", { filepath: "/" }));
|
|
28012
|
+
const list2 = Array.isArray(data.list) ? data.list.map(record2) : [];
|
|
28013
|
+
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);
|
|
28014
|
+
}
|
|
28015
|
+
async function deleteDraftFiles(deps, form, files) {
|
|
28016
|
+
const selected = JSON.stringify(files.map((file3) => ({ filename: file3.name, filepath: file3.path })));
|
|
28017
|
+
const result = await draftAjax(deps, form, "deleteselected", { selected });
|
|
28018
|
+
if (result === false) throw deps.fail("Moodle did not remove the existing submission files.");
|
|
28019
|
+
}
|
|
28020
|
+
async function uploadDraftFile(deps, form, file3) {
|
|
28021
|
+
const body = new FormData();
|
|
28022
|
+
body.set("sesskey", form.sesskey);
|
|
28023
|
+
body.set("client_id", form.clientId);
|
|
28024
|
+
body.set("repo_id", form.repoId);
|
|
28025
|
+
body.set("itemid", form.itemid);
|
|
28026
|
+
body.set("env", "filemanager");
|
|
28027
|
+
body.set("ctx_id", form.contextId);
|
|
28028
|
+
body.set("title", file3.name);
|
|
28029
|
+
body.set("author", form.author);
|
|
28030
|
+
body.set("license", form.license);
|
|
28031
|
+
body.set("savepath", "/");
|
|
28032
|
+
body.set("maxbytes", String(form.maxBytes));
|
|
28033
|
+
body.set("areamaxbytes", String(form.areaMaxBytes));
|
|
28034
|
+
for (const type of form.acceptedTypes === "*" ? ["*"] : form.acceptedTypes) body.append("accepted_types[]", type);
|
|
28035
|
+
body.set("overwrite", "1");
|
|
28036
|
+
body.set("repo_upload_file", new Blob([Uint8Array.from(file3.bytes)]), file3.name);
|
|
28037
|
+
const response = await deps.request(`${deps.baseUrl}/repository/repository_ajax.php?action=upload`, { method: "POST", body }, { allowErrorStatus: true });
|
|
28038
|
+
const data = record2(await jsonOf(deps, response, `upload of ${file3.name}`));
|
|
28039
|
+
if (typeof data.error === "string" && data.error) throw deps.fail(`Moodle refused ${file3.name}: ${data.error}`, typeof data.errorcode === "string" ? data.errorcode : void 0);
|
|
28040
|
+
if (data.event === "fileexists") throw deps.fail(`Moodle reports ${file3.name} already exists and did not overwrite it.`);
|
|
28041
|
+
const stored = typeof data.file === "string" && data.file ? data.file : file3.name;
|
|
28042
|
+
if (!data.url && !data.id && !data.file) throw deps.fail(`Moodle did not confirm the upload of ${file3.name}.`);
|
|
28043
|
+
return stored;
|
|
28044
|
+
}
|
|
28045
|
+
async function jsonOf(deps, response, step) {
|
|
28046
|
+
const text = await response.text();
|
|
28047
|
+
try {
|
|
28048
|
+
return JSON.parse(text);
|
|
28049
|
+
} catch {
|
|
28050
|
+
const notice = noticesOf(text);
|
|
28051
|
+
throw deps.fail(`Moodle did not answer the ${step} with JSON (HTTP ${response.status})${notice ? `: ${notice}` : ""}`);
|
|
28052
|
+
}
|
|
28053
|
+
}
|
|
28054
|
+
function checkLimits(deps, form, kept, files) {
|
|
28055
|
+
if (form.maxFiles > 0 && kept.length + files.length > form.maxFiles) {
|
|
28056
|
+
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);
|
|
28057
|
+
}
|
|
28058
|
+
for (const file3 of files) {
|
|
28059
|
+
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)}.`);
|
|
28060
|
+
if (form.acceptedTypes !== "*" && form.acceptedTypes.every((type) => type.startsWith(".")) && !form.acceptedTypes.some((type) => file3.name.toLowerCase().endsWith(type.toLowerCase()))) {
|
|
28061
|
+
throw deps.usage(`${file3.name} is not an accepted type; allowed: ${form.acceptedTypes.join(", ")}.`);
|
|
28062
|
+
}
|
|
28063
|
+
}
|
|
28064
|
+
const total = kept.reduce((sum, file3) => sum + file3.bytes, 0) + files.reduce((sum, file3) => sum + file3.bytes.byteLength, 0);
|
|
28065
|
+
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);
|
|
28066
|
+
}
|
|
28067
|
+
function describeLimits(form) {
|
|
28068
|
+
return {
|
|
28069
|
+
...form.maxBytes > 0 ? { max_bytes: form.maxBytes } : {},
|
|
28070
|
+
...form.maxFiles > 0 ? { max_files: form.maxFiles } : {},
|
|
28071
|
+
...form.areaMaxBytes > 0 ? { area_max_bytes: form.areaMaxBytes } : {},
|
|
28072
|
+
...form.acceptedTypes !== "*" ? { accepted_types: form.acceptedTypes } : {}
|
|
28073
|
+
};
|
|
28074
|
+
}
|
|
28075
|
+
function isSubmitted(status) {
|
|
28076
|
+
return /\bsubmitted\b/iu.test(status) && !/\bnot submitted\b/iu.test(status);
|
|
28077
|
+
}
|
|
28078
|
+
function size(bytes) {
|
|
28079
|
+
if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
|
|
28080
|
+
if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} KiB`;
|
|
28081
|
+
return `${bytes} B`;
|
|
28082
|
+
}
|
|
28083
|
+
function timestamp(deps) {
|
|
28084
|
+
return (deps.now?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
28085
|
+
}
|
|
28086
|
+
function integer2(value) {
|
|
28087
|
+
const number4 = Number(value);
|
|
28088
|
+
return Number.isSafeInteger(number4) ? number4 : 0;
|
|
28089
|
+
}
|
|
28090
|
+
function isRecord5(value) {
|
|
28091
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
28092
|
+
}
|
|
28093
|
+
function record2(value) {
|
|
28094
|
+
return isRecord5(value) ? value : {};
|
|
28095
|
+
}
|
|
28096
|
+
|
|
28097
|
+
// src/parsers.ts
|
|
28098
|
+
function schema(parser) {
|
|
28099
|
+
return { parse: parser };
|
|
28100
|
+
}
|
|
28101
|
+
var UserInfoSchema = schema(parseUserInfo);
|
|
28102
|
+
var CourseSchema = schema(parseCourse);
|
|
28103
|
+
var CoursesSchema = schema(parseCourses);
|
|
28104
|
+
var ActivitySchema = schema(parseActivity);
|
|
28105
|
+
var SectionSchema = schema(parseSection);
|
|
28106
|
+
var CourseContentsSchema = schema(parseCourseContents);
|
|
28107
|
+
var TodoItemSchema = schema(parseTodoItem);
|
|
28108
|
+
function parseUserInfo(value) {
|
|
28109
|
+
const data = asRecord(value);
|
|
28110
|
+
return {
|
|
28111
|
+
userid: numberValue2(data.userid),
|
|
28112
|
+
username: stringValue3(data.username),
|
|
28113
|
+
fullname: stringValue3(data.fullname),
|
|
28114
|
+
sitename: stringValue3(data.sitename),
|
|
28115
|
+
siteurl: stringValue3(data.siteurl),
|
|
28116
|
+
lang: stringValue3(data.lang),
|
|
28117
|
+
...data.timezone ? { timezone: String(data.timezone) } : {}
|
|
28118
|
+
};
|
|
28119
|
+
}
|
|
28120
|
+
function parseCourse(value, nowSeconds = Math.floor(Date.now() / 1e3)) {
|
|
28121
|
+
const data = asRecord(value);
|
|
28122
|
+
const course = {
|
|
28123
|
+
id: numberValue2(data.id),
|
|
28124
|
+
shortname: stringValue3(data.shortname),
|
|
28125
|
+
fullname: stringValue3(data.fullname),
|
|
28126
|
+
category: numberValue2(data.category),
|
|
28127
|
+
visible: booleanValue(data.visible, true),
|
|
28128
|
+
startdate: numberValue2(data.startdate)
|
|
28129
|
+
};
|
|
28130
|
+
const enddate = numberValue2(data.enddate);
|
|
28131
|
+
if (enddate > 0) {
|
|
28132
|
+
course.enddate = enddate;
|
|
28133
|
+
}
|
|
28134
|
+
return course;
|
|
28135
|
+
}
|
|
28136
|
+
function parseCourses(value) {
|
|
28137
|
+
return asArray(value).map((item) => parseCourse(item));
|
|
28138
|
+
}
|
|
28139
|
+
function parseActivity(value) {
|
|
28140
|
+
const data = asRecord(value);
|
|
28141
|
+
return {
|
|
28142
|
+
id: numberValue2(data.id),
|
|
28143
|
+
name: stringValue3(data.name),
|
|
28144
|
+
modname: stringValue3(data.modname),
|
|
28145
|
+
url: stringValue3(data.url),
|
|
28146
|
+
visible: booleanValue(data.visible, true),
|
|
28147
|
+
description: stringValue3(data.description),
|
|
28148
|
+
...data.completiondata && typeof data.completiondata === "object" ? { completion: numberValue2(asRecord(data.completiondata).state) } : {},
|
|
28149
|
+
...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 })) } : {}
|
|
28150
|
+
};
|
|
28151
|
+
}
|
|
28152
|
+
function parseSection(value) {
|
|
28153
|
+
const data = asRecord(value);
|
|
28154
|
+
return {
|
|
28155
|
+
id: numberValue2(data.id),
|
|
28156
|
+
name: stringValue3(data.name),
|
|
28157
|
+
section: numberValue2(data.section),
|
|
28158
|
+
visible: booleanValue(data.visible, true),
|
|
28159
|
+
summary: stringValue3(data.summary),
|
|
28160
|
+
...data.current !== void 0 ? { current: booleanValue(data.current) } : {},
|
|
28161
|
+
activities: asArray(data.modules).map((item) => parseActivity(item))
|
|
28162
|
+
};
|
|
28163
|
+
}
|
|
28164
|
+
function parseCourseContents(value) {
|
|
28165
|
+
return asArray(value).map((item) => parseSection(item));
|
|
28166
|
+
}
|
|
28167
|
+
function parseCourseFormatState(value, baseUrl) {
|
|
28168
|
+
const state = asRecord(parseJsonValue(value));
|
|
28169
|
+
const activities = /* @__PURE__ */ new Map();
|
|
28170
|
+
const activitiesBySection = /* @__PURE__ */ new Map();
|
|
28171
|
+
for (const item of asArray(state.cm)) {
|
|
28172
|
+
const data = asRecord(item);
|
|
28173
|
+
const id2 = numberValue2(data.id);
|
|
28174
|
+
const sectionId = stringValue3(data.sectionid);
|
|
28175
|
+
const module = stringValue3(data.module) || stringValue3(data.plugin).replace(/^mod_/u, "") || stringValue3(data.modname).toLowerCase();
|
|
28176
|
+
const activity = {
|
|
28177
|
+
id: id2,
|
|
28178
|
+
name: htmlText(data.name, baseUrl),
|
|
28179
|
+
modname: module.toLowerCase(),
|
|
28180
|
+
url: stringValue3(data.url) ? resolveUrl(baseUrl, stringValue3(data.url)) : "",
|
|
28181
|
+
visible: booleanValue(data.visible, true) && booleanValue(data.uservisible, true) && !booleanValue(data.stealth),
|
|
28182
|
+
description: htmlText(data.content ?? data.description, baseUrl),
|
|
28183
|
+
...data.completionstate !== void 0 && data.completionstate !== null ? { completion: numberValue2(data.completionstate) } : {}
|
|
28184
|
+
};
|
|
28185
|
+
activities.set(String(id2), activity);
|
|
28186
|
+
const sectionActivities = activitiesBySection.get(sectionId) ?? [];
|
|
28187
|
+
sectionActivities.push(activity);
|
|
28188
|
+
activitiesBySection.set(sectionId, sectionActivities);
|
|
28189
|
+
}
|
|
28190
|
+
return asArray(state.section).map((item) => {
|
|
28191
|
+
const data = asRecord(item);
|
|
28192
|
+
const id2 = numberValue2(data.id);
|
|
28193
|
+
const hasActivityList = Array.isArray(data.cmlist);
|
|
28194
|
+
const listedActivities = asArray(data.cmlist).map((activityId) => activities.get(stringValue3(activityId))).filter((activity) => activity !== void 0);
|
|
28195
|
+
return {
|
|
28196
|
+
id: id2,
|
|
28197
|
+
name: htmlText(data.title || data.rawtitle, baseUrl),
|
|
28198
|
+
section: numberValue2(data.section ?? data.number),
|
|
28199
|
+
visible: booleanValue(data.visible, true),
|
|
28200
|
+
summary: htmlText(data.summary, baseUrl),
|
|
28201
|
+
...data.current !== void 0 ? { current: booleanValue(data.current) } : {},
|
|
28202
|
+
activities: hasActivityList ? listedActivities : activitiesBySection.get(String(id2)) ?? []
|
|
28203
|
+
};
|
|
28204
|
+
});
|
|
28205
|
+
}
|
|
28206
|
+
function parseTodoItem(value) {
|
|
28207
|
+
const data = asRecord(value);
|
|
28208
|
+
const course = asRecord(data.course);
|
|
28209
|
+
const action = asRecord(data.action);
|
|
28210
|
+
const progress = course.progress;
|
|
28211
|
+
return {
|
|
28212
|
+
id: numberValue2(data.id),
|
|
28213
|
+
name: stringValue3(data.name),
|
|
28214
|
+
activity_name: stringValue3(data.activityname),
|
|
28215
|
+
modname: stringValue3(data.modulename),
|
|
28216
|
+
course_id: numberValue2(course.id),
|
|
28217
|
+
course_name: stringValue3(course.fullname),
|
|
28218
|
+
due_at: numberValue2(data.timesort) || numberValue2(data.timestart),
|
|
28219
|
+
overdue: booleanValue(data.overdue),
|
|
28220
|
+
actionable: booleanValue(action.actionable),
|
|
28221
|
+
action_name: stringValue3(action.name),
|
|
28222
|
+
action_url: stringValue3(action.url),
|
|
28223
|
+
url: stringValue3(data.url),
|
|
28224
|
+
event_type: stringValue3(data.eventtype),
|
|
28225
|
+
course_progress: typeof progress === "number" ? progress : void 0
|
|
28226
|
+
};
|
|
28227
|
+
}
|
|
28228
|
+
function parseTodoItems(value) {
|
|
28229
|
+
return asArray(value).map((item) => parseTodoItem(item));
|
|
28230
|
+
}
|
|
28231
|
+
function parseAlertNotification(value) {
|
|
28232
|
+
const data = asRecord(value);
|
|
28233
|
+
return {
|
|
28234
|
+
id: numberValue2(data.id),
|
|
28235
|
+
subject: stringValue3(data.subject),
|
|
28236
|
+
short_subject: stringValue3(data.shortenedsubject),
|
|
28237
|
+
event_type: stringValue3(data.eventtype),
|
|
28238
|
+
component: stringValue3(data.component),
|
|
28239
|
+
created_at: numberValue2(data.timecreated),
|
|
28240
|
+
created_pretty: stringValue3(data.timecreatedpretty),
|
|
28241
|
+
read: booleanValue(data.read),
|
|
28242
|
+
context_url: stringValue3(data.contexturl),
|
|
28243
|
+
context_name: stringValue3(data.contexturlname)
|
|
28244
|
+
};
|
|
28245
|
+
}
|
|
28246
|
+
function parseAlertSummary(notificationsData, countsData, unreadCountsData) {
|
|
28247
|
+
const notificationsRecord = asRecord(notificationsData);
|
|
28248
|
+
const counts2 = asRecord(countsData);
|
|
28249
|
+
const unreadCounts = asRecord(unreadCountsData);
|
|
28250
|
+
const types = asRecord(counts2.types);
|
|
28251
|
+
const unreadTypes = asRecord(unreadCounts.types);
|
|
28252
|
+
const notifications = asArray(notificationsRecord.notifications).map((item) => parseAlertNotification(item));
|
|
28253
|
+
return {
|
|
28254
|
+
notifications,
|
|
28255
|
+
notification_count: notifications.length,
|
|
28256
|
+
unread_notification_count: notifications.filter((notification) => !notification.read).length,
|
|
28257
|
+
starred_message_count: numberValue2(counts2.favourites),
|
|
28258
|
+
direct_message_count: numberValue2(types["1"]),
|
|
28259
|
+
group_message_count: numberValue2(types["2"]),
|
|
28260
|
+
self_message_count: numberValue2(types["3"]),
|
|
28261
|
+
unread_starred_message_count: numberValue2(unreadCounts.favourites),
|
|
28262
|
+
unread_direct_message_count: numberValue2(unreadTypes["1"]),
|
|
28263
|
+
unread_group_message_count: numberValue2(unreadTypes["2"]),
|
|
28264
|
+
unread_self_message_count: numberValue2(unreadTypes["3"])
|
|
28265
|
+
};
|
|
28266
|
+
}
|
|
28267
|
+
function parseForumPostAuthor(value) {
|
|
28268
|
+
const data = asRecord(value);
|
|
28269
|
+
const urls = asRecord(data.urls);
|
|
28270
|
+
return {
|
|
28271
|
+
id: numberValue2(data.id),
|
|
28272
|
+
fullname: stringValue3(data.fullname),
|
|
28273
|
+
profile_url: stringValue3(urls.profile),
|
|
28274
|
+
profile_image_url: stringValue3(urls.profileimage)
|
|
28275
|
+
};
|
|
28276
|
+
}
|
|
28277
|
+
function parseForumPost(value, baseUrl = "") {
|
|
28278
|
+
const data = asRecord(value);
|
|
28279
|
+
const urls = asRecord(data.urls);
|
|
28280
|
+
const messageHtml = stringValue3(data.message);
|
|
28281
|
+
const structured = htmlToStructuredContent(messageHtml, stringValue3(urls.view || urls.discuss) || baseUrl);
|
|
28282
|
+
return {
|
|
28283
|
+
id: numberValue2(data.id),
|
|
28284
|
+
discussion_id: numberValue2(data.discussionid),
|
|
28285
|
+
subject: stringValue3(data.subject),
|
|
28286
|
+
message_html: messageHtml,
|
|
28287
|
+
message_text: structured.text,
|
|
28288
|
+
image_urls: structured.image_urls,
|
|
28289
|
+
links: structured.links,
|
|
28290
|
+
tables: structured.tables,
|
|
28291
|
+
author: parseForumPostAuthor(data.author),
|
|
28292
|
+
parent_id: numberValue2(data.parentid),
|
|
28293
|
+
time_created: numberValue2(data.timecreated),
|
|
28294
|
+
time_modified: numberValue2(data.timemodified),
|
|
28295
|
+
created_pretty: "",
|
|
28296
|
+
unread: booleanValue(data.unread),
|
|
28297
|
+
is_deleted: booleanValue(data.isdeleted),
|
|
28298
|
+
is_private_reply: booleanValue(data.isprivatereply),
|
|
28299
|
+
url: stringValue3(urls.view || urls.viewisolated),
|
|
28300
|
+
reply_url: stringValue3(urls.reply)
|
|
28301
|
+
};
|
|
28302
|
+
}
|
|
28303
|
+
function parseForumDiscussion(value, discussionId, baseUrl = "") {
|
|
28304
|
+
const data = asRecord(value);
|
|
28305
|
+
const posts = asArray(data.posts).map((item) => parseForumPost(item, baseUrl));
|
|
28306
|
+
return {
|
|
28307
|
+
id: discussionId,
|
|
28308
|
+
subject: posts[0]?.subject ?? "",
|
|
28309
|
+
course_id: numberValue2(data.courseid),
|
|
28310
|
+
forum_id: numberValue2(data.forumid),
|
|
28311
|
+
group_id: numberValue2(data.groupid),
|
|
28312
|
+
group_name: stringValue3(data.groupname),
|
|
28313
|
+
url: posts[0]?.url ? posts[0].url.split("#", 1)[0] : "",
|
|
28314
|
+
posts
|
|
28315
|
+
};
|
|
28316
|
+
}
|
|
28317
|
+
function asRecord(value) {
|
|
28318
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
28319
|
+
}
|
|
28320
|
+
function asArray(value) {
|
|
28321
|
+
return Array.isArray(value) ? value : [];
|
|
28322
|
+
}
|
|
28323
|
+
function stringValue3(value) {
|
|
28324
|
+
return typeof value === "string" ? value : value == null ? "" : String(value);
|
|
28325
|
+
}
|
|
28326
|
+
function numberValue2(value) {
|
|
28327
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
28328
|
+
return value;
|
|
28329
|
+
}
|
|
28330
|
+
if (typeof value === "string" && value.trim() !== "" && Number.isFinite(Number(value))) {
|
|
28331
|
+
return Number(value);
|
|
28332
|
+
}
|
|
28333
|
+
return 0;
|
|
28334
|
+
}
|
|
28335
|
+
function booleanValue(value, defaultValue = false) {
|
|
28336
|
+
if (value === void 0 || value === null) {
|
|
28337
|
+
return defaultValue;
|
|
28338
|
+
}
|
|
28339
|
+
return Boolean(value);
|
|
28340
|
+
}
|
|
28341
|
+
function parseJsonValue(value) {
|
|
28342
|
+
if (typeof value !== "string") return value;
|
|
28343
|
+
try {
|
|
28344
|
+
return JSON.parse(value);
|
|
28345
|
+
} catch {
|
|
28346
|
+
return {};
|
|
28347
|
+
}
|
|
28348
|
+
}
|
|
28349
|
+
function htmlText(value, baseUrl) {
|
|
28350
|
+
return htmlToStructuredContent(stringValue3(value), baseUrl).text;
|
|
28351
|
+
}
|
|
28352
|
+
|
|
27993
28353
|
// src/moodle-forum-core.ts
|
|
27994
28354
|
var ForumModule = class {
|
|
27995
28355
|
baseUrl;
|
|
@@ -28124,13 +28484,13 @@ var ForumModule = class {
|
|
|
28124
28484
|
}
|
|
28125
28485
|
};
|
|
28126
28486
|
function shouldFallbackForumAjax(error62) {
|
|
28127
|
-
if (!
|
|
28487
|
+
if (!isRecord6(error62)) {
|
|
28128
28488
|
return false;
|
|
28129
28489
|
}
|
|
28130
28490
|
const code = typeof error62.moodleErrorCode === "string" ? error62.moodleErrorCode : "";
|
|
28131
28491
|
return code === "servicenotavailable" || code === "accessexception" || error62 instanceof Error && error62.message.includes("Web service is not available");
|
|
28132
28492
|
}
|
|
28133
|
-
function
|
|
28493
|
+
function isRecord6(value) {
|
|
28134
28494
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
28135
28495
|
}
|
|
28136
28496
|
|
|
@@ -28262,9 +28622,9 @@ async function searchForumContent(source, query, options = {}) {
|
|
|
28262
28622
|
hits.sort(sortBy === "recent" ? sortRecent : sortRelevant);
|
|
28263
28623
|
return hits.slice(0, options.limit ?? 20).map(([, hit]) => includePostText ? hit : { ...hit, snippet: "" });
|
|
28264
28624
|
}
|
|
28265
|
-
async function inParallel(items,
|
|
28625
|
+
async function inParallel(items, size2, run) {
|
|
28266
28626
|
const results = [];
|
|
28267
|
-
for (let index = 0; index < items.length; index +=
|
|
28627
|
+
for (let index = 0; index < items.length; index += size2) results.push(...await Promise.all(items.slice(index, index + size2).map(run)));
|
|
28268
28628
|
return results;
|
|
28269
28629
|
}
|
|
28270
28630
|
function normalizeQuery(value) {
|
|
@@ -28450,7 +28810,7 @@ var MoodleClientCore = class {
|
|
|
28450
28810
|
try {
|
|
28451
28811
|
if (this.unavailable.has(FUNC_GET_SITE_INFO) && this.userInfo?.fullname) return this.userInfo;
|
|
28452
28812
|
const data = await this.call(FUNC_GET_SITE_INFO);
|
|
28453
|
-
if (
|
|
28813
|
+
if (isRecord7(data) && "userid" in data) {
|
|
28454
28814
|
const info = parseUserInfo(data);
|
|
28455
28815
|
this.sesskey = typeof data.sesskey === "string" ? data.sesskey : this.sesskey;
|
|
28456
28816
|
this.userid = info.userid;
|
|
@@ -28549,9 +28909,9 @@ var MoodleClientCore = class {
|
|
|
28549
28909
|
let type = "";
|
|
28550
28910
|
try {
|
|
28551
28911
|
const data = await this.call(FUNC_GET_COURSE_MODULE, { cmid: id2 });
|
|
28552
|
-
const module =
|
|
28553
|
-
type =
|
|
28554
|
-
courseId =
|
|
28912
|
+
const module = isRecord7(data) && isRecord7(data.cm) ? data.cm : data;
|
|
28913
|
+
type = isRecord7(module) && typeof module.modname === "string" ? module.modname : "";
|
|
28914
|
+
courseId = isRecord7(module) && typeof module.course === "number" ? module.course : void 0;
|
|
28555
28915
|
} catch (error62) {
|
|
28556
28916
|
if (!this.errors.isApi(error62) || error62.moodleErrorCode !== "servicenotavailable") {
|
|
28557
28917
|
throw error62;
|
|
@@ -28595,14 +28955,14 @@ var MoodleClientCore = class {
|
|
|
28595
28955
|
} else {
|
|
28596
28956
|
data = await this.call(FUNC_GET_ACTION_EVENTS, { ...window, limittononsuspendedevents: true });
|
|
28597
28957
|
}
|
|
28598
|
-
const events =
|
|
28958
|
+
const events = isRecord7(data) && Array.isArray(data.events) ? data.events : [];
|
|
28599
28959
|
for (const item of parseTodoItems(events)) if (!seen.has(item.id)) {
|
|
28600
28960
|
seen.add(item.id);
|
|
28601
28961
|
items.push(item);
|
|
28602
28962
|
}
|
|
28603
28963
|
if (events.length < batchSize) break;
|
|
28604
28964
|
const last = events.at(-1);
|
|
28605
|
-
const next =
|
|
28965
|
+
const next = isRecord7(last) && typeof last.id === "number" ? last.id : void 0;
|
|
28606
28966
|
if (!next || next === aftereventid) throw this.errors.api("Moodle repeated a calendar page; refine the date window.");
|
|
28607
28967
|
aftereventid = next;
|
|
28608
28968
|
}
|
|
@@ -28769,8 +29129,18 @@ var MoodleClientCore = class {
|
|
|
28769
29129
|
async getFolder(id2) {
|
|
28770
29130
|
return parseFolderHtml(await this.get(FOLDER_VIEW_PATH, { id: id2 }), id2, this.baseUrl);
|
|
28771
29131
|
}
|
|
28772
|
-
async requestAbsolute(url2, init = {}) {
|
|
28773
|
-
return this.requestAbsoluteInternal(url2, init, true);
|
|
29132
|
+
async requestAbsolute(url2, init = {}, options = {}) {
|
|
29133
|
+
return this.requestAbsoluteInternal(url2, init, true, Boolean(options.allowErrorStatus));
|
|
29134
|
+
}
|
|
29135
|
+
/** Uploads files into an assignment and reads the receipt back from the site. */
|
|
29136
|
+
async submitAssignment(request) {
|
|
29137
|
+
await this.ensureSession();
|
|
29138
|
+
return submitAssignmentFiles({
|
|
29139
|
+
baseUrl: this.baseUrl,
|
|
29140
|
+
request: (url2, init, options) => this.requestAbsolute(url2, init, options),
|
|
29141
|
+
fail: (message, moodleErrorCode) => this.errors.api(message, moodleErrorCode),
|
|
29142
|
+
usage: (message, hint) => this.errors.usage ? this.errors.usage(message, hint) : new MoodleClientCoreError("usage", message, hint)
|
|
29143
|
+
}, request);
|
|
28774
29144
|
}
|
|
28775
29145
|
async getNewsForums(courseId) {
|
|
28776
29146
|
const units = courseId === void 0 ? await this.getCourses() : (await this.getCourses()).filter((c) => c.id === courseId);
|
|
@@ -28780,7 +29150,7 @@ var MoodleClientCore = class {
|
|
|
28780
29150
|
await this.ensureSession();
|
|
28781
29151
|
const data = await this.call("mod_forum_get_forums_by_courses", { courseids: units.map((c) => c.id) });
|
|
28782
29152
|
for (const f of Array.isArray(data) ? data : []) {
|
|
28783
|
-
if (!
|
|
29153
|
+
if (!isRecord7(f) || f.type !== "news" || typeof f.cmid !== "number") continue;
|
|
28784
29154
|
const c = units.find((c2) => c2.id === f.course);
|
|
28785
29155
|
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
29156
|
}
|
|
@@ -28908,16 +29278,16 @@ var MoodleClientCore = class {
|
|
|
28908
29278
|
async getAbsolute(url2) {
|
|
28909
29279
|
return (await this.requestAbsolute(url2)).text();
|
|
28910
29280
|
}
|
|
28911
|
-
async requestAbsoluteInternal(url2, init, allowRetry) {
|
|
29281
|
+
async requestAbsoluteInternal(url2, init, allowRetry, allowErrorStatus = false) {
|
|
28912
29282
|
const response = await fetchWithSession(url2, init, this.baseUrl, this.cookie, this.fetchImpl);
|
|
28913
29283
|
if (response.url.includes("/login/")) {
|
|
28914
29284
|
if (this.onLoginRequired && allowRetry && !this.retryingLogin) {
|
|
28915
29285
|
await this.reauthenticate();
|
|
28916
|
-
return this.requestAbsoluteInternal(url2, init, false);
|
|
29286
|
+
return this.requestAbsoluteInternal(url2, init, false, allowErrorStatus);
|
|
28917
29287
|
}
|
|
28918
29288
|
throw this.errors.api("Session expired", "servicerequireslogin");
|
|
28919
29289
|
}
|
|
28920
|
-
if (!response.ok) {
|
|
29290
|
+
if (!response.ok && !allowErrorStatus) {
|
|
28921
29291
|
const context = `HTTP ${response.status} loading ${safeUrl(url2)}`;
|
|
28922
29292
|
const contentType2 = response.headers.get("content-type")?.toLowerCase() ?? "";
|
|
28923
29293
|
if (contentType2.includes("text/html") || contentType2.includes("application/xhtml+xml")) {
|
|
@@ -28935,7 +29305,7 @@ var MoodleClientCore = class {
|
|
|
28935
29305
|
let offset = 0;
|
|
28936
29306
|
while (true) {
|
|
28937
29307
|
const data = await this.call(FUNC_GET_COURSES_BY_TIMELINE, { classification: "all", limit: 100, offset });
|
|
28938
|
-
if (!
|
|
29308
|
+
if (!isRecord7(data) || !Array.isArray(data.courses) || !data.courses.length) {
|
|
28939
29309
|
break;
|
|
28940
29310
|
}
|
|
28941
29311
|
courses.push(...data.courses);
|
|
@@ -29098,19 +29468,19 @@ function placeholderUserInfo(baseUrl, userid) {
|
|
|
29098
29468
|
};
|
|
29099
29469
|
}
|
|
29100
29470
|
function parseTodoPayload(value) {
|
|
29101
|
-
return parseTodoItems(
|
|
29471
|
+
return parseTodoItems(isRecord7(value) && Array.isArray(value.events) ? value.events : []);
|
|
29102
29472
|
}
|
|
29103
29473
|
function errorMessage(value) {
|
|
29104
29474
|
return value instanceof Error ? value.message : "Unknown Moodle error";
|
|
29105
29475
|
}
|
|
29106
|
-
function chunks(values,
|
|
29476
|
+
function chunks(values, size2) {
|
|
29107
29477
|
const result = [];
|
|
29108
|
-
for (let index = 0; index < values.length; index +=
|
|
29109
|
-
result.push(values.slice(index, index +
|
|
29478
|
+
for (let index = 0; index < values.length; index += size2) {
|
|
29479
|
+
result.push(values.slice(index, index + size2));
|
|
29110
29480
|
}
|
|
29111
29481
|
return result;
|
|
29112
29482
|
}
|
|
29113
|
-
function
|
|
29483
|
+
function isRecord7(value) {
|
|
29114
29484
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
29115
29485
|
}
|
|
29116
29486
|
function isLoginErrorCode(code) {
|
|
@@ -29133,7 +29503,7 @@ function safeUrl(value) {
|
|
|
29133
29503
|
}
|
|
29134
29504
|
|
|
29135
29505
|
// src/mcp/gateway.ts
|
|
29136
|
-
var
|
|
29506
|
+
var import_node_html_parser4 = __toESM(require_dist(), 1);
|
|
29137
29507
|
var MAX_MCP_FILE_BYTES = 16 * 1024 * 1024;
|
|
29138
29508
|
var MoodleGatewayError = class extends Error {
|
|
29139
29509
|
code;
|
|
@@ -29143,13 +29513,14 @@ var MoodleGatewayError = class extends Error {
|
|
|
29143
29513
|
this.code = code;
|
|
29144
29514
|
}
|
|
29145
29515
|
};
|
|
29146
|
-
function createMoodleGateway(client) {
|
|
29516
|
+
function createMoodleGateway(client, hooks = {}) {
|
|
29147
29517
|
return {
|
|
29148
29518
|
getUser: () => client.getSiteInfo(),
|
|
29149
29519
|
listThreads: (id2) => client.getForumDiscussionRefs ? client.getForumDiscussionRefs(id2) : Promise.resolve([]),
|
|
29150
29520
|
listNewsForums: (id2) => client.getNewsForums ? client.getNewsForums(id2) : Promise.resolve([]),
|
|
29151
29521
|
getOverview: (input3) => client.getOverview(input3.todoLimit, input3.todoDays, input3.alertsLimit),
|
|
29152
29522
|
...client.getTodo ? { getDue: (days, courseId) => client.getTodo(Number.MAX_SAFE_INTEGER, days, courseId) } : {},
|
|
29523
|
+
...client.submitAssignmentFiles ? { submitAssignment: (input3) => client.submitAssignmentFiles({ ...input3, ...hooks.onSubmitProgress ? { onProgress: hooks.onSubmitProgress } : {} }) } : {},
|
|
29153
29524
|
listCourses: () => client.getCourses(),
|
|
29154
29525
|
async getCourse({ courseId }) {
|
|
29155
29526
|
const [courses, sections] = await Promise.all([
|
|
@@ -29273,7 +29644,7 @@ async function fileFromActivity(client, activityId) {
|
|
|
29273
29644
|
return { url: entry.url, name: entry.name };
|
|
29274
29645
|
}
|
|
29275
29646
|
function resourceLinks(html, baseUrl) {
|
|
29276
|
-
const root = (0,
|
|
29647
|
+
const root = (0, import_node_html_parser4.parse)(html);
|
|
29277
29648
|
const entries = root.querySelectorAll(".resourceworkaround a[href], .resourcecontent a[href], a.resourceworkaround[href]").map((link) => ({
|
|
29278
29649
|
name: link.textContent.trim(),
|
|
29279
29650
|
url: new URL(link.getAttribute("href") ?? "", baseUrl).toString()
|
|
@@ -29327,7 +29698,7 @@ function isHtml(response) {
|
|
|
29327
29698
|
return type.includes("text/html") || type.includes("application/xhtml+xml");
|
|
29328
29699
|
}
|
|
29329
29700
|
function looksLikeLoginPage(html) {
|
|
29330
|
-
const root = (0,
|
|
29701
|
+
const root = (0, import_node_html_parser4.parse)(html);
|
|
29331
29702
|
return root.querySelector('form[action*="/login/"], input[name="password"], #page-login-index') !== null || /<title>\s*(?:log in|login)/iu.test(html);
|
|
29332
29703
|
}
|
|
29333
29704
|
function contentType(response) {
|
|
@@ -29400,6 +29771,8 @@ var counts = external_exports.object(Object.fromEntries(["notification_count", "
|
|
|
29400
29771
|
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
29772
|
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
29773
|
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 });
|
|
29774
|
+
var submissionFile = external_exports.object({ name: external_exports.string(), bytes: n, url: s });
|
|
29775
|
+
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
29776
|
var input2 = (shape) => external_exports.object(shape).strict();
|
|
29404
29777
|
var list = (key, value) => external_exports.object({ [key]: external_exports.array(value).optional(), total: id });
|
|
29405
29778
|
var intentContracts = {
|
|
@@ -29413,6 +29786,7 @@ var intentContracts = {
|
|
|
29413
29786
|
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
29787
|
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
29788
|
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 }) }) },
|
|
29789
|
+
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
29790
|
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
29791
|
};
|
|
29418
29792
|
function intentDescription(name) {
|
|
@@ -29472,9 +29846,9 @@ function postRow(p, subject, tz = "UTC") {
|
|
|
29472
29846
|
}
|
|
29473
29847
|
|
|
29474
29848
|
// src/intents.ts
|
|
29475
|
-
async function inParallel2(items,
|
|
29849
|
+
async function inParallel2(items, size2, run) {
|
|
29476
29850
|
const results = [];
|
|
29477
|
-
for (let index = 0; index < items.length; index +=
|
|
29851
|
+
for (let index = 0; index < items.length; index += size2) results.push(...await Promise.all(items.slice(index, index + size2).map(run)));
|
|
29478
29852
|
return results;
|
|
29479
29853
|
}
|
|
29480
29854
|
function createIntentService(gateway, now = () => Date.now()) {
|
|
@@ -29665,6 +30039,12 @@ function createIntentService(gateway, now = () => Date.now()) {
|
|
|
29665
30039
|
result = { file: { name: file3.name, mime_type: file3.mimeType, bytes: file3.bytes, uri: file3.uri } };
|
|
29666
30040
|
break;
|
|
29667
30041
|
}
|
|
30042
|
+
case "submit": {
|
|
30043
|
+
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.");
|
|
30044
|
+
const activityId = await resolveItem(input3.ref);
|
|
30045
|
+
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) }) };
|
|
30046
|
+
break;
|
|
30047
|
+
}
|
|
29668
30048
|
}
|
|
29669
30049
|
return intentContracts[name].output.parse(stripEmpty(result));
|
|
29670
30050
|
}
|
|
@@ -29683,13 +30063,23 @@ var READ_ONLY_ANNOTATIONS = {
|
|
|
29683
30063
|
idempotentHint: true,
|
|
29684
30064
|
openWorldHint: true
|
|
29685
30065
|
};
|
|
30066
|
+
var WRITE_ANNOTATIONS = {
|
|
30067
|
+
readOnlyHint: false,
|
|
30068
|
+
destructiveHint: true,
|
|
30069
|
+
idempotentHint: false,
|
|
30070
|
+
openWorldHint: true
|
|
30071
|
+
};
|
|
30072
|
+
var WRITE_TOOLS = /* @__PURE__ */ new Set(["submit"]);
|
|
29686
30073
|
var aliases = { get_overview: "home", list_courses: "units", get_course: "unit", get_activity: "item", get_grades: "grades", get_thread: "thread", get_file: "file" };
|
|
29687
30074
|
var TOOL_CATALOG = Object.entries(intentContracts).map(([name, contract]) => ({
|
|
29688
30075
|
name,
|
|
29689
30076
|
description: intentDescription(name),
|
|
29690
30077
|
inputSchema: compactSchema(external_exports.toJSONSchema(contract.input, { io: "input" })),
|
|
29691
|
-
annotations: READ_ONLY_ANNOTATIONS
|
|
30078
|
+
annotations: WRITE_TOOLS.has(name) ? WRITE_ANNOTATIONS : READ_ONLY_ANNOTATIONS
|
|
29692
30079
|
}));
|
|
30080
|
+
function toolsFor(gateway) {
|
|
30081
|
+
return gateway.submitAssignment ? TOOL_CATALOG : TOOL_CATALOG.filter((tool) => !WRITE_TOOLS.has(tool.name));
|
|
30082
|
+
}
|
|
29693
30083
|
var TOOL_OUTPUT_SCHEMAS = Object.fromEntries(Object.entries(intentContracts).map(([name, contract]) => [name, compactSchema(external_exports.toJSONSchema(contract.output))]));
|
|
29694
30084
|
function compactSchema(value) {
|
|
29695
30085
|
if (Array.isArray(value)) return value.map(compactSchema);
|
|
@@ -29727,7 +30117,7 @@ function createMoodleMcpServer(gateway, options = {}) {
|
|
|
29727
30117
|
protocolVersion,
|
|
29728
30118
|
capabilities: { tools: { listChanged: false } },
|
|
29729
30119
|
serverInfo,
|
|
29730
|
-
instructions: "Read-only access to the authenticated user's Moodle data."
|
|
30120
|
+
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
30121
|
});
|
|
29732
30122
|
}
|
|
29733
30123
|
if (request.method === "ping") {
|
|
@@ -29735,7 +30125,7 @@ function createMoodleMcpServer(gateway, options = {}) {
|
|
|
29735
30125
|
}
|
|
29736
30126
|
if (request.method === "tools/list") {
|
|
29737
30127
|
return jsonRpcSuccess(id2, {
|
|
29738
|
-
tools:
|
|
30128
|
+
tools: toolsFor(gateway),
|
|
29739
30129
|
resultType: "complete",
|
|
29740
30130
|
_meta: RESULT_META
|
|
29741
30131
|
});
|
|
@@ -29791,8 +30181,9 @@ async function callTool(gateway, params) {
|
|
|
29791
30181
|
const requested = typeof params?.name === "string" ? params.name : "";
|
|
29792
30182
|
const name = Object.hasOwn(aliases, requested) ? aliases[requested] : requested;
|
|
29793
30183
|
if (!Object.hasOwn(intentContracts, name) && !["get_user", "list_activities", "list_forums"].includes(name)) throw new McpCallError("TOOL_NOT_FOUND", `Unknown Moodle tool: ${requested || "<missing>"}`);
|
|
30184
|
+
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
30185
|
const raw = params?.arguments ?? {};
|
|
29795
|
-
if (!
|
|
30186
|
+
if (!isRecord8(raw)) throw new McpCallError("INVALID_TOOL_ARGUMENTS", "Tool arguments must be an object.");
|
|
29796
30187
|
const args = { ...raw };
|
|
29797
30188
|
if (Object.hasOwn(aliases, requested)) {
|
|
29798
30189
|
const renames = { courseId: "unit", activityId: "ref", discussionId: "discussion_id", source: "ref", todoDays: "days", gradedOnly: "graded_only" };
|
|
@@ -29840,7 +30231,7 @@ async function callTool(gateway, params) {
|
|
|
29840
30231
|
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
30232
|
return { content: [{ type: "text", text: JSON.stringify({ error: mapped2 }) }], structuredContent: { error: mapped2 }, isError: true, resultType: "complete", _meta: RESULT_META };
|
|
29842
30233
|
}
|
|
29843
|
-
const mapped = error62 instanceof ReferenceError2 ? { type: error62.code, code: error62.code, message: error62.message, hint: error62.hint, candidates: error62.candidates } : mapMoodleError(error62);
|
|
30234
|
+
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
30235
|
return { content: [{ type: "text", text: JSON.stringify({ error: mapped }) }], structuredContent: { error: mapped }, isError: true, resultType: "complete", _meta: RESULT_META };
|
|
29845
30236
|
}
|
|
29846
30237
|
}
|
|
@@ -29859,9 +30250,11 @@ function toolContent(name, payload, structuredContent) {
|
|
|
29859
30250
|
}
|
|
29860
30251
|
];
|
|
29861
30252
|
}
|
|
29862
|
-
function mapMoodleError(error62) {
|
|
29863
|
-
const
|
|
29864
|
-
const code = typeof
|
|
30253
|
+
function mapMoodleError(error62, verbatim = false) {
|
|
30254
|
+
const record3 = isRecord8(error62) ? error62 : {};
|
|
30255
|
+
const code = typeof record3.code === "string" ? record3.code : "";
|
|
30256
|
+
const own2 = verbatim && typeof record3.message === "string" && record3.message.trim() && code !== "auth" ? record3.message : void 0;
|
|
30257
|
+
const ownHint = verbatim && typeof record3.hint === "string" && record3.hint.trim() ? record3.hint : void 0;
|
|
29865
30258
|
const typeByCode = {
|
|
29866
30259
|
auth: "MOODLE_AUTH_REQUIRED",
|
|
29867
30260
|
not_found: "MOODLE_NOT_FOUND",
|
|
@@ -29870,8 +30263,8 @@ function mapMoodleError(error62) {
|
|
|
29870
30263
|
};
|
|
29871
30264
|
const type = code.startsWith("MOODLE_") ? code : typeByCode[code] ?? "MOODLE_UPSTREAM_ERROR";
|
|
29872
30265
|
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 } : {} };
|
|
30266
|
+
const moodleCode = typeof record3.moodleErrorCode === "string" && /^[a-z][a-z0-9_]{0,63}$/u.test(record3.moodleErrorCode) ? record3.moodleErrorCode : void 0;
|
|
30267
|
+
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
30268
|
}
|
|
29876
30269
|
var McpCallError = class extends Error {
|
|
29877
30270
|
type;
|
|
@@ -29883,11 +30276,11 @@ var McpCallError = class extends Error {
|
|
|
29883
30276
|
this.details = details;
|
|
29884
30277
|
}
|
|
29885
30278
|
};
|
|
29886
|
-
function
|
|
30279
|
+
function isRecord8(value) {
|
|
29887
30280
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
29888
30281
|
}
|
|
29889
30282
|
function isMoodleFile(value) {
|
|
29890
|
-
return
|
|
30283
|
+
return isRecord8(value) && typeof value.name === "string" && typeof value.mimeType === "string" && typeof value.bytes === "number" && typeof value.uri === "string" && typeof value.blob === "string";
|
|
29891
30284
|
}
|
|
29892
30285
|
|
|
29893
30286
|
// src/worker/moodle-upstream.ts
|
|
@@ -29958,17 +30351,17 @@ var FetchMoodleSessionUpstream = class {
|
|
|
29958
30351
|
return { alive: false, remainingSeconds: null };
|
|
29959
30352
|
}
|
|
29960
30353
|
if (!Array.isArray(body) || body.length === 0) return { alive: null, remainingSeconds: null };
|
|
29961
|
-
const first2 =
|
|
30354
|
+
const first2 = isRecord9(body[0]) ? body[0] : void 0;
|
|
29962
30355
|
if (first2?.error) {
|
|
29963
|
-
const exception =
|
|
30356
|
+
const exception = isRecord9(first2.exception) ? first2.exception : void 0;
|
|
29964
30357
|
const errorCode = typeof exception?.errorcode === "string" ? exception.errorcode : "";
|
|
29965
30358
|
if (errorCode === "servicerequireslogin" || errorCode === "sitepolicynotagreed") {
|
|
29966
30359
|
return { alive: false, remainingSeconds: null };
|
|
29967
30360
|
}
|
|
29968
30361
|
return { alive: null, remainingSeconds: null };
|
|
29969
30362
|
}
|
|
29970
|
-
const last =
|
|
29971
|
-
const data =
|
|
30363
|
+
const last = isRecord9(body.at(-1)) ? body.at(-1) : void 0;
|
|
30364
|
+
const data = isRecord9(last?.data) ? last.data : void 0;
|
|
29972
30365
|
const remainingSeconds = typeof data?.timeremaining === "number" ? data.timeremaining : null;
|
|
29973
30366
|
const rotatedCookie = cookieFromSetCookie(response.headers.get("set-cookie"), session.cookieName);
|
|
29974
30367
|
return {
|
|
@@ -29990,7 +30383,7 @@ function cookieFromSetCookie(header, cookieName) {
|
|
|
29990
30383
|
const escaped = cookieName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
29991
30384
|
return header.match(new RegExp(`(?:^|,\\s*)${escaped}=([^;,\\s]+)`))?.[1];
|
|
29992
30385
|
}
|
|
29993
|
-
function
|
|
30386
|
+
function isRecord9(value) {
|
|
29994
30387
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
29995
30388
|
}
|
|
29996
30389
|
|
|
@@ -30212,16 +30605,16 @@ var SessionBroker = class {
|
|
|
30212
30605
|
});
|
|
30213
30606
|
}
|
|
30214
30607
|
async readSession(storage) {
|
|
30215
|
-
const
|
|
30216
|
-
if (!
|
|
30217
|
-
if (
|
|
30218
|
-
const decrypted = await decryptValue(
|
|
30608
|
+
const record3 = await storage.get(SESSION_STORAGE_KEY);
|
|
30609
|
+
if (!record3) return void 0;
|
|
30610
|
+
if (record3.version === 2 && typeof record3.encrypted_session === "string") {
|
|
30611
|
+
const decrypted = await decryptValue(record3.encrypted_session, await this.keyring());
|
|
30219
30612
|
const session = JSON.parse(decrypted.value);
|
|
30220
30613
|
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
30614
|
return session;
|
|
30222
30615
|
}
|
|
30223
|
-
if (typeof
|
|
30224
|
-
const { encrypted_cookie, ...metadata } =
|
|
30616
|
+
if (typeof record3.encrypted_cookie !== "string") throw new Error("Invalid legacy session.");
|
|
30617
|
+
const { encrypted_cookie, ...metadata } = record3;
|
|
30225
30618
|
const cookie = await decryptValue(encrypted_cookie, await this.keyring());
|
|
30226
30619
|
return { ...metadata, cookie_value: cookie.value };
|
|
30227
30620
|
}
|
|
@@ -30233,10 +30626,10 @@ var SessionBroker = class {
|
|
|
30233
30626
|
}
|
|
30234
30627
|
async loadSession() {
|
|
30235
30628
|
return this.transaction(async (storage) => {
|
|
30236
|
-
const
|
|
30237
|
-
if (!
|
|
30629
|
+
const record3 = await storage.get(SESSION_STORAGE_KEY);
|
|
30630
|
+
if (!record3) return void 0;
|
|
30238
30631
|
const session = await this.readSession(storage);
|
|
30239
|
-
if (session && (
|
|
30632
|
+
if (session && (record3.version !== 2 || (await decryptValue(record3.encrypted_session, await this.keyring())).needsRotation)) {
|
|
30240
30633
|
await this.writeSession(storage, session);
|
|
30241
30634
|
}
|
|
30242
30635
|
return session;
|
|
@@ -30312,14 +30705,14 @@ function normalizeOrigin(value) {
|
|
|
30312
30705
|
}
|
|
30313
30706
|
}
|
|
30314
30707
|
function isSessionCandidate(value) {
|
|
30315
|
-
if (!
|
|
30708
|
+
if (!isRecord10(value)) return false;
|
|
30316
30709
|
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
30710
|
}
|
|
30318
|
-
function
|
|
30711
|
+
function isRecord10(value) {
|
|
30319
30712
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
30320
30713
|
}
|
|
30321
30714
|
function isMcpEnvelope(value) {
|
|
30322
|
-
if (!
|
|
30715
|
+
if (!isRecord10(value) || !("request" in value) || !isRecord10(value.context)) return false;
|
|
30323
30716
|
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
30717
|
}
|
|
30325
30718
|
|