@mo7yw4ng/openape 1.0.1 → 1.0.3
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 +8 -5
- package/esm/_dnt.polyfills.d.ts +83 -6
- package/esm/_dnt.polyfills.d.ts.map +1 -0
- package/esm/_dnt.polyfills.js +127 -1
- package/esm/_dnt.shims.d.ts +1 -0
- package/esm/_dnt.shims.d.ts.map +1 -0
- package/esm/deno.d.ts +2 -0
- package/esm/deno.d.ts.map +1 -0
- package/esm/deno.js +2 -1
- package/esm/src/commands/announcements.d.ts +3 -0
- package/esm/src/commands/announcements.d.ts.map +1 -0
- package/esm/src/commands/announcements.js +135 -0
- package/esm/src/commands/auth.d.ts +3 -0
- package/esm/src/commands/auth.d.ts.map +1 -0
- package/esm/src/commands/auth.js +260 -0
- package/esm/src/commands/calendar.d.ts +3 -0
- package/esm/src/commands/calendar.d.ts.map +1 -0
- package/esm/src/commands/calendar.js +180 -0
- package/esm/src/commands/courses.d.ts +3 -0
- package/esm/src/commands/courses.d.ts.map +1 -0
- package/esm/src/commands/courses.js +348 -0
- package/esm/src/commands/forums.d.ts +3 -0
- package/esm/src/commands/forums.d.ts.map +1 -0
- package/esm/src/commands/forums.js +226 -0
- package/esm/src/commands/grades.d.ts +3 -0
- package/esm/src/commands/grades.d.ts.map +1 -0
- package/esm/src/commands/grades.js +121 -0
- package/esm/src/commands/materials.d.ts +3 -0
- package/esm/src/commands/materials.d.ts.map +1 -0
- package/esm/src/commands/materials.js +522 -0
- package/esm/src/commands/quizzes.d.ts +3 -0
- package/esm/src/commands/quizzes.d.ts.map +1 -0
- package/esm/src/commands/quizzes.js +160 -0
- package/esm/src/commands/skills.d.ts +3 -0
- package/esm/src/commands/skills.d.ts.map +1 -0
- package/esm/src/commands/skills.js +110 -0
- package/esm/src/commands/videos.d.ts +3 -0
- package/esm/src/commands/videos.d.ts.map +1 -0
- package/esm/src/commands/videos.js +335 -0
- package/esm/src/index.d.ts +27 -0
- package/esm/src/index.d.ts.map +1 -0
- package/esm/src/index.js +149 -0
- package/esm/src/lib/auth.d.ts +25 -0
- package/esm/src/lib/auth.d.ts.map +1 -0
- package/esm/src/lib/auth.js +194 -0
- package/esm/src/lib/config.d.ts +6 -0
- package/esm/src/lib/config.d.ts.map +1 -0
- package/esm/src/lib/config.js +36 -0
- package/esm/src/lib/logger.d.ts +3 -0
- package/esm/src/lib/logger.d.ts.map +1 -0
- package/esm/src/lib/logger.js +24 -0
- package/esm/src/lib/moodle.d.ts +251 -0
- package/esm/src/lib/moodle.d.ts.map +1 -0
- package/esm/src/lib/moodle.js +833 -0
- package/esm/src/lib/session.d.ts +8 -0
- package/esm/src/lib/session.d.ts.map +1 -0
- package/esm/src/lib/session.js +42 -0
- package/esm/src/lib/token.d.ts +38 -0
- package/esm/src/lib/token.d.ts.map +1 -0
- package/esm/src/lib/token.js +178 -0
- package/esm/src/lib/types.d.ts +272 -0
- package/esm/src/lib/types.d.ts.map +1 -0
- package/esm/src/lib/types.js +1 -0
- package/esm/src/lib/utils.d.ts +37 -0
- package/esm/src/lib/utils.d.ts.map +1 -0
- package/esm/src/lib/utils.js +82 -0
- package/package.json +4 -4
- package/script/_dnt.polyfills.d.ts +83 -6
- package/script/_dnt.polyfills.d.ts.map +1 -0
- package/script/_dnt.polyfills.js +128 -0
- package/script/_dnt.shims.d.ts +1 -0
- package/script/_dnt.shims.d.ts.map +1 -0
- package/script/deno.d.ts +2 -0
- package/script/deno.d.ts.map +1 -0
- package/script/deno.js +2 -1
- package/script/src/commands/announcements.d.ts +1 -0
- package/script/src/commands/announcements.d.ts.map +1 -0
- package/script/src/commands/announcements.js +75 -222
- package/script/src/commands/auth.d.ts +2 -1
- package/script/src/commands/auth.d.ts.map +1 -0
- package/script/src/commands/auth.js +52 -24
- package/script/src/commands/calendar.d.ts +1 -0
- package/script/src/commands/calendar.d.ts.map +1 -0
- package/script/src/commands/calendar.js +112 -301
- package/script/src/commands/courses.d.ts +1 -0
- package/script/src/commands/courses.d.ts.map +1 -0
- package/script/src/commands/courses.js +43 -173
- package/script/src/commands/forums.d.ts +1 -0
- package/script/src/commands/forums.d.ts.map +1 -0
- package/script/src/commands/forums.js +145 -316
- package/script/src/commands/grades.d.ts +1 -0
- package/script/src/commands/grades.d.ts.map +1 -0
- package/script/src/commands/grades.js +62 -194
- package/script/src/commands/materials.d.ts +1 -0
- package/script/src/commands/materials.d.ts.map +1 -0
- package/script/src/commands/materials.js +283 -178
- package/script/src/commands/quizzes.d.ts +1 -0
- package/script/src/commands/quizzes.d.ts.map +1 -0
- package/script/src/commands/quizzes.js +40 -102
- package/script/src/commands/skills.d.ts +1 -0
- package/script/src/commands/skills.d.ts.map +1 -0
- package/script/src/commands/skills.js +17 -18
- package/script/src/commands/videos.d.ts +1 -0
- package/script/src/commands/videos.d.ts.map +1 -0
- package/script/src/commands/videos.js +127 -120
- package/script/src/index.d.ts +1 -0
- package/script/src/index.d.ts.map +1 -0
- package/script/src/index.js +5 -5
- package/script/src/lib/auth.d.ts +1 -0
- package/script/src/lib/auth.d.ts.map +1 -0
- package/script/src/lib/auth.js +9 -10
- package/script/src/lib/config.d.ts +1 -0
- package/script/src/lib/config.d.ts.map +1 -0
- package/script/src/lib/config.js +6 -7
- package/script/src/lib/logger.d.ts +1 -0
- package/script/src/lib/logger.d.ts.map +1 -0
- package/script/src/lib/logger.js +1 -2
- package/script/src/lib/moodle.d.ts +72 -55
- package/script/src/lib/moodle.d.ts.map +1 -0
- package/script/src/lib/moodle.js +275 -350
- package/script/src/lib/session.d.ts +1 -0
- package/script/src/lib/session.d.ts.map +1 -0
- package/script/src/lib/session.js +3 -29
- package/script/src/lib/token.d.ts +16 -5
- package/script/src/lib/token.d.ts.map +1 -0
- package/script/src/lib/token.js +71 -36
- package/script/src/lib/types.d.ts +11 -0
- package/script/src/lib/types.d.ts.map +1 -0
- package/script/src/lib/utils.d.ts +32 -0
- package/script/src/lib/utils.d.ts.map +1 -0
- package/script/src/lib/utils.js +93 -13
- package/skills/openape/SKILL.md +6 -26
package/script/src/lib/moodle.js
CHANGED
|
@@ -15,26 +15,49 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (
|
|
|
15
15
|
}) : function(o, v) {
|
|
16
16
|
o["default"] = v;
|
|
17
17
|
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || function (
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
};
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
25
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
-
exports.
|
|
36
|
+
exports.moodleApiCall = moodleApiCall;
|
|
37
|
+
exports.moodleAjax = moodleAjax;
|
|
38
|
+
exports.getEnrolledCoursesApi = getEnrolledCoursesApi;
|
|
39
|
+
exports.getEnrolledCourses = getEnrolledCourses;
|
|
40
|
+
exports.getCourseState = getCourseState;
|
|
41
|
+
exports.getSupervideosInCourse = getSupervideosInCourse;
|
|
42
|
+
exports.getForumsApi = getForumsApi;
|
|
43
|
+
exports.getForumDiscussionsApi = getForumDiscussionsApi;
|
|
44
|
+
exports.getDiscussionPostsApi = getDiscussionPostsApi;
|
|
45
|
+
exports.getResourcesInCourse = getResourcesInCourse;
|
|
46
|
+
exports.getCalendarEventsApi = getCalendarEventsApi;
|
|
47
|
+
exports.getCourseGradesApi = getCourseGradesApi;
|
|
48
|
+
exports.getVideoMetadata = getVideoMetadata;
|
|
49
|
+
exports.downloadVideo = downloadVideo;
|
|
50
|
+
exports.completeVideoApi = completeVideoApi;
|
|
51
|
+
exports.completeVideo = completeVideo;
|
|
52
|
+
exports.updateActivityCompletionStatusManually = updateActivityCompletionStatusManually;
|
|
53
|
+
exports.getSiteInfoApi = getSiteInfoApi;
|
|
54
|
+
exports.getIncompleteVideosApi = getIncompleteVideosApi;
|
|
55
|
+
exports.getSupervideosInCourseApi = getSupervideosInCourseApi;
|
|
56
|
+
exports.getQuizzesByCoursesApi = getQuizzesByCoursesApi;
|
|
57
|
+
exports.getResourcesByCoursesApi = getResourcesByCoursesApi;
|
|
58
|
+
exports.getMessagesApi = getMessagesApi;
|
|
27
59
|
const dntShim = __importStar(require("../../_dnt.shims.js"));
|
|
28
|
-
const
|
|
29
|
-
// ── HTML Parsing Helpers ──────────────────────────────────────────────────
|
|
30
|
-
/**
|
|
31
|
-
* Get the HTML content of a page and parse it.
|
|
32
|
-
*/
|
|
33
|
-
async function fetchAndParse(page, url) {
|
|
34
|
-
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30000 });
|
|
35
|
-
const content = await page.content();
|
|
36
|
-
return (0, node_html_parser_1.parse)(content);
|
|
37
|
-
}
|
|
60
|
+
const utils_js_1 = require("./utils.js");
|
|
38
61
|
// ── Core Moodle AJAX Wrapper ───────────────────────────────────────────
|
|
39
62
|
/**
|
|
40
63
|
* Moodle WS API functions that are known to work via /webservice/rest/server.php
|
|
@@ -47,6 +70,12 @@ const WS_API_FUNCTIONS = new Set([
|
|
|
47
70
|
"gradereport_user_get_grade_items",
|
|
48
71
|
"core_calendar_get_calendar_events",
|
|
49
72
|
"core_course_get_contents",
|
|
73
|
+
"core_course_get_course_module",
|
|
74
|
+
"core_completion_get_activities_completion_status",
|
|
75
|
+
"core_completion_update_activity_completion_status_manually",
|
|
76
|
+
"mod_supervideo_progress_save",
|
|
77
|
+
"mod_supervideo_progress_save_mobile",
|
|
78
|
+
"mod_supervideo_view_supervideo",
|
|
50
79
|
"mod_quiz_get_quizzes_by_courses",
|
|
51
80
|
"mod_resource_get_resources_by_courses",
|
|
52
81
|
"core_message_get_messages",
|
|
@@ -91,7 +120,6 @@ async function moodleApiCall(session, methodname, args) {
|
|
|
91
120
|
}
|
|
92
121
|
return result;
|
|
93
122
|
}
|
|
94
|
-
exports.moodleApiCall = moodleApiCall;
|
|
95
123
|
/**
|
|
96
124
|
* Send a Moodle AJAX request and return the result.
|
|
97
125
|
* Uses Web Service token if available AND the function is in WS_API_FUNCTIONS,
|
|
@@ -135,7 +163,6 @@ async function moodleAjax(page, session, methodname, args) {
|
|
|
135
163
|
return result[0].data;
|
|
136
164
|
}
|
|
137
165
|
}
|
|
138
|
-
exports.moodleAjax = moodleAjax;
|
|
139
166
|
// ── Course Operations ─────────────────────────────────────────────────────
|
|
140
167
|
/**
|
|
141
168
|
* Fetch enrolled courses via pure API (no browser required).
|
|
@@ -162,7 +189,7 @@ async function getEnrolledCoursesApi(session, options = {}) {
|
|
|
162
189
|
});
|
|
163
190
|
return (data?.courses ?? []).map((c) => ({
|
|
164
191
|
id: c.id,
|
|
165
|
-
fullname: c.fullname,
|
|
192
|
+
fullname: (0, utils_js_1.extractCourseName)(c.fullname),
|
|
166
193
|
shortname: c.shortname,
|
|
167
194
|
idnumber: c.idnumber,
|
|
168
195
|
category: c.category?.name,
|
|
@@ -171,7 +198,6 @@ async function getEnrolledCoursesApi(session, options = {}) {
|
|
|
171
198
|
enddate: c.enddate,
|
|
172
199
|
}));
|
|
173
200
|
}
|
|
174
|
-
exports.getEnrolledCoursesApi = getEnrolledCoursesApi;
|
|
175
201
|
/**
|
|
176
202
|
* Fetch all enrolled courses via Moodle AJAX API.
|
|
177
203
|
*/
|
|
@@ -196,7 +222,7 @@ async function getEnrolledCourses(page, session, log, options = {}) {
|
|
|
196
222
|
});
|
|
197
223
|
const courses = (data?.courses ?? []).map((c) => ({
|
|
198
224
|
id: c.id,
|
|
199
|
-
fullname: c.fullname,
|
|
225
|
+
fullname: (0, utils_js_1.extractCourseName)(c.fullname),
|
|
200
226
|
shortname: c.shortname,
|
|
201
227
|
idnumber: c.idnumber,
|
|
202
228
|
category: c.category?.name,
|
|
@@ -207,7 +233,6 @@ async function getEnrolledCourses(page, session, log, options = {}) {
|
|
|
207
233
|
log.debug(`Found ${courses.length} course${courses.length === 1 ? "" : "s"}.`);
|
|
208
234
|
return courses;
|
|
209
235
|
}
|
|
210
|
-
exports.getEnrolledCourses = getEnrolledCourses;
|
|
211
236
|
/**
|
|
212
237
|
* Get course state (modules) via core_courseformat_get_state.
|
|
213
238
|
*/
|
|
@@ -217,7 +242,6 @@ async function getCourseState(page, session, courseId) {
|
|
|
217
242
|
});
|
|
218
243
|
return typeof data === "string" ? JSON.parse(data) : data;
|
|
219
244
|
}
|
|
220
|
-
exports.getCourseState = getCourseState;
|
|
221
245
|
// ── Video Operations ──────────────────────────────────────────────────────
|
|
222
246
|
/**
|
|
223
247
|
* Get all SuperVideo modules in a course.
|
|
@@ -225,15 +249,17 @@ exports.getCourseState = getCourseState;
|
|
|
225
249
|
async function getSupervideosInCourse(page, session, courseId, log, options = {}) {
|
|
226
250
|
const state = await getCourseState(page, session, courseId);
|
|
227
251
|
const cms = state?.cm ?? [];
|
|
228
|
-
log.debug(` Course state returned ${cms.length} modules`);
|
|
229
|
-
// Debug: log first few modules
|
|
230
|
-
for (let i = 0; i < Math.min(3, cms.length); i++) {
|
|
231
|
-
log.debug(` Module ${i}: ${JSON.stringify(cms[i])}`);
|
|
232
|
-
}
|
|
233
252
|
const allSupervideos = cms.filter((cm) => cm.module === "supervideo" || cm.modname === "supervideo");
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
253
|
+
// Filter: Only include videos with completion tracking enabled (have completionstate field)
|
|
254
|
+
// and are not yet completed (completionstate != 1 or isoverallcomplete != true)
|
|
255
|
+
const incomplete = allSupervideos.filter((cm) => {
|
|
256
|
+
// Has completionstate field = completion tracking is enabled for this video
|
|
257
|
+
const hasCompletionTracking = "completionstate" in cm;
|
|
258
|
+
// Is not yet completed
|
|
259
|
+
const isIncomplete = cm.completionstate !== 1 && cm.isoverallcomplete !== true;
|
|
260
|
+
return hasCompletionTracking && isIncomplete;
|
|
261
|
+
});
|
|
262
|
+
log.debug(` SuperVideo: ${allSupervideos.length} total, ${incomplete.length} incomplete (with completion enabled)`);
|
|
237
263
|
// Return only incomplete if requested, otherwise return all
|
|
238
264
|
const videos = options.incompleteOnly ? incomplete : allSupervideos;
|
|
239
265
|
return videos.map((cm) => ({
|
|
@@ -243,81 +269,7 @@ async function getSupervideosInCourse(page, session, courseId, log, options = {}
|
|
|
243
269
|
isComplete: !!cm.isoverallcomplete,
|
|
244
270
|
}));
|
|
245
271
|
}
|
|
246
|
-
exports.getSupervideosInCourse = getSupervideosInCourse;
|
|
247
|
-
// ── Quiz Operations ───────────────────────────────────────────────────────
|
|
248
|
-
/**
|
|
249
|
-
* Get all Quiz modules in a course.
|
|
250
|
-
*/
|
|
251
|
-
async function getQuizzesInCourse(page, session, courseId, log) {
|
|
252
|
-
const state = await getCourseState(page, session, courseId);
|
|
253
|
-
const cms = state?.cm ?? [];
|
|
254
|
-
const allQuizzes = cms.filter((cm) => cm.module === "quiz");
|
|
255
|
-
const available = allQuizzes.filter((cm) => !("isoverallcomplete" in cm) || !cm.isoverallcomplete);
|
|
256
|
-
log.debug(` Quiz: ${allQuizzes.length} total, ${available.length} available`);
|
|
257
|
-
return available.map((cm) => ({
|
|
258
|
-
cmid: cm.cmid?.toString() ?? cm.id?.toString() ?? "",
|
|
259
|
-
name: cm.name,
|
|
260
|
-
url: cm.url,
|
|
261
|
-
isComplete: !!cm.isoverallcomplete,
|
|
262
|
-
timeOpen: cm.timeopen,
|
|
263
|
-
timeClose: cm.timeclose,
|
|
264
|
-
}));
|
|
265
|
-
}
|
|
266
|
-
exports.getQuizzesInCourse = getQuizzesInCourse;
|
|
267
272
|
// ── Forum Operations ──────────────────────────────────────────────────────
|
|
268
|
-
/**
|
|
269
|
-
* Get all forum modules in a course.
|
|
270
|
-
* If WS token is available, fetches forum IDs directly via WS API.
|
|
271
|
-
*/
|
|
272
|
-
async function getForumsInCourse(page, session, courseId, log) {
|
|
273
|
-
// First get basic forum info from course state
|
|
274
|
-
const state = await getCourseState(page, session, courseId);
|
|
275
|
-
const cms = state?.cm ?? [];
|
|
276
|
-
const forums = cms.filter((cm) => cm.module === "forum");
|
|
277
|
-
log.debug(` Found ${forums.length} forum${forums.length === 1 ? "" : "s"}.`);
|
|
278
|
-
const result = forums.map((cm) => ({
|
|
279
|
-
cmid: cm.cmid?.toString() ?? cm.id?.toString() ?? "",
|
|
280
|
-
forumId: 0,
|
|
281
|
-
name: cm.name,
|
|
282
|
-
url: cm.url,
|
|
283
|
-
courseId,
|
|
284
|
-
forumType: cm.modname,
|
|
285
|
-
}));
|
|
286
|
-
// If WS token is available, fetch forum IDs directly
|
|
287
|
-
if (session.wsToken && forums.length > 0) {
|
|
288
|
-
try {
|
|
289
|
-
const wsForums = await moodleAjax(page, session, "mod_forum_get_forums_by_courses", { courseids: [courseId] });
|
|
290
|
-
// Create maps for lookup by different fields
|
|
291
|
-
const byId = new Map(); // cmid -> forum id
|
|
292
|
-
const byName = new Map(); // name -> forum id
|
|
293
|
-
for (const wsForum of wsForums || []) {
|
|
294
|
-
if (wsForum.cmid) {
|
|
295
|
-
byId.set(wsForum.cmid, wsForum.id);
|
|
296
|
-
}
|
|
297
|
-
if (wsForum.name) {
|
|
298
|
-
byName.set(wsForum.name, wsForum.id);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
// Merge forum IDs into result
|
|
302
|
-
for (const forum of result) {
|
|
303
|
-
const cmid = parseInt(forum.cmid, 10);
|
|
304
|
-
if (byId.has(cmid)) {
|
|
305
|
-
forum.forumId = byId.get(cmid);
|
|
306
|
-
}
|
|
307
|
-
else if (byName.has(forum.name)) {
|
|
308
|
-
forum.forumId = byName.get(forum.name);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
const matchedCount = result.filter(f => f.forumId > 0).length;
|
|
312
|
-
log.debug(` WS API provided forum IDs for ${matchedCount}/${result.length} forums.`);
|
|
313
|
-
}
|
|
314
|
-
catch (e) {
|
|
315
|
-
log.debug(` WS API forum lookup failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
return result;
|
|
319
|
-
}
|
|
320
|
-
exports.getForumsInCourse = getForumsInCourse;
|
|
321
273
|
/**
|
|
322
274
|
* Get all forums via pure WS API (no browser required).
|
|
323
275
|
* Fast and lightweight - uses HTTP fetch directly.
|
|
@@ -329,144 +281,52 @@ async function getForumsApi(session, courseIds) {
|
|
|
329
281
|
cmid: f.cmid,
|
|
330
282
|
name: f.name,
|
|
331
283
|
courseid: f.course, // API returns 'course' not 'courseid'
|
|
284
|
+
timemodified: f.timemodified,
|
|
332
285
|
}));
|
|
333
286
|
}
|
|
334
|
-
exports.getForumsApi = getForumsApi;
|
|
335
|
-
/**
|
|
336
|
-
* Extract forum ID from forum page.
|
|
337
|
-
* First tries to find it in embedded page data, then falls back to
|
|
338
|
-
* extracting it from discussion posts API.
|
|
339
|
-
*/
|
|
340
|
-
async function getForumIdFromPage(page, cmid, session) {
|
|
341
|
-
try {
|
|
342
|
-
await page.goto(`https://ilearning.cycu.edu.tw/mod/forum/view.php?id=${cmid}`, { waitUntil: "domcontentloaded", timeout: 30000 });
|
|
343
|
-
// First try: extract from page HTML
|
|
344
|
-
const forumId = await page.evaluate(() => {
|
|
345
|
-
// Try multiple patterns to find the forum ID
|
|
346
|
-
const patterns = [
|
|
347
|
-
/"forumid":(\d+)/,
|
|
348
|
-
/"forumId":(\d+)/,
|
|
349
|
-
/forumid=(\d+)/,
|
|
350
|
-
/data-forum-id="(\d+)"/,
|
|
351
|
-
];
|
|
352
|
-
const html = document.body.innerHTML;
|
|
353
|
-
for (const pattern of patterns) {
|
|
354
|
-
const match = html.match(pattern);
|
|
355
|
-
if (match)
|
|
356
|
-
return parseInt(match[1], 10);
|
|
357
|
-
}
|
|
358
|
-
// Try to find it in a script tag with forum configuration
|
|
359
|
-
const scripts = Array.from(document.querySelectorAll('script'));
|
|
360
|
-
for (const script of scripts) {
|
|
361
|
-
const text = script.textContent || '';
|
|
362
|
-
const match = text.match(/"forumid":(\d+)/);
|
|
363
|
-
if (match)
|
|
364
|
-
return parseInt(match[1], 10);
|
|
365
|
-
}
|
|
366
|
-
// Try to find from discussion links - extract from API data embedded in page
|
|
367
|
-
const discussLinks = Array.from(document.querySelectorAll('a[href*="discuss.php"]'));
|
|
368
|
-
for (const link of discussLinks) {
|
|
369
|
-
const href = link.href;
|
|
370
|
-
// The discussion page might have forum info
|
|
371
|
-
const dMatch = href.match(/d=(\d+)/);
|
|
372
|
-
if (dMatch) {
|
|
373
|
-
// Try to find parent element with forum data
|
|
374
|
-
let parent = link.parentElement;
|
|
375
|
-
let depth = 0;
|
|
376
|
-
while (parent && depth < 10) {
|
|
377
|
-
const parentHtml = parent.innerHTML;
|
|
378
|
-
const fMatch = parentHtml.match(/"forum":(\d+)/);
|
|
379
|
-
if (fMatch)
|
|
380
|
-
return parseInt(fMatch[1], 10);
|
|
381
|
-
parent = parent.parentElement;
|
|
382
|
-
depth++;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
return null;
|
|
387
|
-
});
|
|
388
|
-
if (forumId)
|
|
389
|
-
return forumId;
|
|
390
|
-
// Fallback: if session is provided, try to get instance ID from discussion posts
|
|
391
|
-
if (session) {
|
|
392
|
-
// Get first discussion ID from page
|
|
393
|
-
const firstDiscussionId = await page.evaluate(() => {
|
|
394
|
-
const link = document.querySelector('a[href*="discuss.php"]');
|
|
395
|
-
if (!link)
|
|
396
|
-
return null;
|
|
397
|
-
const href = link.href;
|
|
398
|
-
const match = href.match(/d=(\d+)/);
|
|
399
|
-
return match ? parseInt(match[1], 10) : null;
|
|
400
|
-
});
|
|
401
|
-
if (firstDiscussionId) {
|
|
402
|
-
// Try to get posts and extract forum ID from response
|
|
403
|
-
const data = await moodleAjax(page, session, "mod_forum_get_forum_discussion_posts", {
|
|
404
|
-
discussionid: firstDiscussionId,
|
|
405
|
-
});
|
|
406
|
-
if (data?.posts && data.posts.length > 0) {
|
|
407
|
-
const firstPost = data.posts[0];
|
|
408
|
-
if (firstPost.forum) {
|
|
409
|
-
return firstPost.forum;
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
return null;
|
|
415
|
-
}
|
|
416
|
-
catch {
|
|
417
|
-
return null;
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
exports.getForumIdFromPage = getForumIdFromPage;
|
|
421
|
-
/**
|
|
422
|
-
* Get forums by course IDs via AJAX.
|
|
423
|
-
* Returns forum instance IDs directly from Moodle API.
|
|
424
|
-
* This is the cleanest way to get forum instance IDs.
|
|
425
|
-
*/
|
|
426
|
-
async function getForumsByCourseIds(page, session, courseIds) {
|
|
427
|
-
if (courseIds.length === 0)
|
|
428
|
-
return [];
|
|
429
|
-
try {
|
|
430
|
-
const data = await moodleAjax(page, session, "mod_forum_get_forums_by_courses", {
|
|
431
|
-
courseids: courseIds,
|
|
432
|
-
});
|
|
433
|
-
return data ?? [];
|
|
434
|
-
}
|
|
435
|
-
catch (e) {
|
|
436
|
-
// Re-throw with more context
|
|
437
|
-
throw new Error(`mod_forum_get_forums_by_courses failed: ${e?.message || e}`);
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
exports.getForumsByCourseIds = getForumsByCourseIds;
|
|
441
287
|
/**
|
|
442
|
-
* Get discussions in a forum via
|
|
443
|
-
*
|
|
288
|
+
* Get discussions in a forum via WS API (no browser required).
|
|
289
|
+
* Uses mod_forum_get_forum_discussions
|
|
444
290
|
*/
|
|
445
|
-
async function
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
|
|
291
|
+
async function getForumDiscussionsApi(session, forumId, options) {
|
|
292
|
+
const params = { forumid: forumId, sortorder: options?.sortorder ?? 2 };
|
|
293
|
+
if (options?.page !== undefined)
|
|
294
|
+
params.page = options.page;
|
|
295
|
+
if (options?.perpage !== undefined)
|
|
296
|
+
params.perpage = options.perpage;
|
|
297
|
+
if (options?.groupid !== undefined)
|
|
298
|
+
params.groupid = options.groupid;
|
|
299
|
+
const data = await moodleApiCall(session, "mod_forum_get_forum_discussions", params);
|
|
449
300
|
return (data?.discussions ?? []).map((d) => ({
|
|
450
|
-
id: d.
|
|
301
|
+
id: d.discussion,
|
|
451
302
|
forumId: d.forum,
|
|
452
303
|
name: d.name,
|
|
453
304
|
firstPostId: d.firstpost,
|
|
454
305
|
userId: d.userid,
|
|
306
|
+
userFullName: d.userfullname || "",
|
|
455
307
|
groupId: d.groupid,
|
|
456
308
|
timedue: d.timedue,
|
|
457
309
|
timeModified: d.timemodified,
|
|
310
|
+
timeStart: d.timestart,
|
|
311
|
+
timeEnd: d.timeend,
|
|
458
312
|
userModified: d.usermodified,
|
|
459
|
-
|
|
460
|
-
|
|
313
|
+
userModifiedFullName: d.usermodifiedfullname,
|
|
314
|
+
postCount: d.numreplies,
|
|
315
|
+
unread: (d.numunread ?? 0) > 0,
|
|
316
|
+
subject: (0, utils_js_1.stripHtmlTags)(d.subject ?? ""),
|
|
317
|
+
message: d.message,
|
|
318
|
+
pinned: d.pinned,
|
|
319
|
+
locked: d.locked,
|
|
320
|
+
starred: d.starred,
|
|
461
321
|
}));
|
|
462
322
|
}
|
|
463
|
-
exports.getForumDiscussions = getForumDiscussions;
|
|
464
323
|
/**
|
|
465
|
-
* Get posts in a discussion via
|
|
324
|
+
* Get posts in a discussion via WS API (no browser required).
|
|
325
|
+
* Uses mod_forum_get_forum_discussion_posts
|
|
466
326
|
*/
|
|
467
|
-
async function
|
|
327
|
+
async function getDiscussionPostsApi(session, discussionId) {
|
|
468
328
|
try {
|
|
469
|
-
const data = await
|
|
329
|
+
const data = await moodleApiCall(session, "mod_forum_get_discussion_posts", {
|
|
470
330
|
discussionid: discussionId,
|
|
471
331
|
});
|
|
472
332
|
if (!data?.posts || data.posts.length === 0) {
|
|
@@ -474,13 +334,13 @@ async function getDiscussionPosts(page, session, discussionId) {
|
|
|
474
334
|
}
|
|
475
335
|
return data.posts.map((p) => ({
|
|
476
336
|
id: p.id,
|
|
477
|
-
subject: p.subject || "",
|
|
478
|
-
author: p.author?.fullname ??
|
|
479
|
-
authorId: p.userid,
|
|
480
|
-
created: p.
|
|
481
|
-
modified: p.
|
|
482
|
-
message: p.message || "",
|
|
483
|
-
discussionId: p.
|
|
337
|
+
subject: (0, utils_js_1.stripHtmlTags)(p.subject || ""),
|
|
338
|
+
author: p.author?.fullname ?? "Unknown",
|
|
339
|
+
authorId: p.author?.id ?? p.userid,
|
|
340
|
+
created: p.timecreated,
|
|
341
|
+
modified: p.timemodified,
|
|
342
|
+
message: (0, utils_js_1.stripHtmlTags)(p.message || ""),
|
|
343
|
+
discussionId: p.discussionid,
|
|
484
344
|
unread: p.unread ?? false,
|
|
485
345
|
}));
|
|
486
346
|
}
|
|
@@ -490,7 +350,6 @@ async function getDiscussionPosts(page, session, discussionId) {
|
|
|
490
350
|
return [];
|
|
491
351
|
}
|
|
492
352
|
}
|
|
493
|
-
exports.getDiscussionPosts = getDiscussionPosts;
|
|
494
353
|
// ── Resource/Material Operations ──────────────────────────────────────────
|
|
495
354
|
/**
|
|
496
355
|
* Get all resource modules in a course.
|
|
@@ -511,79 +370,13 @@ async function getResourcesInCourse(page, session, courseId, log) {
|
|
|
511
370
|
modified: 0,
|
|
512
371
|
}));
|
|
513
372
|
}
|
|
514
|
-
|
|
515
|
-
// ── Grade Operations ──────────────────────────────────────────────────────
|
|
373
|
+
// ── Calendar Operations ─────────────────────────────────────────────────────
|
|
516
374
|
/**
|
|
517
|
-
* Get
|
|
518
|
-
*/
|
|
519
|
-
async function getCourseGrades(page, session, courseId) {
|
|
520
|
-
const data = await moodleAjax(page, session, "gradereport_user_get_grade_items", {
|
|
521
|
-
courseid: courseId,
|
|
522
|
-
});
|
|
523
|
-
const userGrades = data?.usergrades?.[0];
|
|
524
|
-
if (!userGrades) {
|
|
525
|
-
return { courseId, courseName: "", items: [] };
|
|
526
|
-
}
|
|
527
|
-
return {
|
|
528
|
-
courseId,
|
|
529
|
-
courseName: userGrades.coursefullname ?? "",
|
|
530
|
-
grade: userGrades.grade,
|
|
531
|
-
gradeFormatted: userGrades.gradeformatted,
|
|
532
|
-
rank: userGrades.rank,
|
|
533
|
-
totalUsers: userGrades.totalusers,
|
|
534
|
-
items: (userGrades.gradeitems ?? []).map((item) => ({
|
|
535
|
-
id: item.id,
|
|
536
|
-
name: item.itemname || item.itemmodule,
|
|
537
|
-
grade: item.grade,
|
|
538
|
-
gradeFormatted: item.gradeformatted,
|
|
539
|
-
range: item.graderangeformatted,
|
|
540
|
-
percentage: item.percentage,
|
|
541
|
-
weight: item.weight,
|
|
542
|
-
feedback: item.feedback,
|
|
543
|
-
graded: !!item.graded,
|
|
544
|
-
})),
|
|
545
|
-
};
|
|
546
|
-
}
|
|
547
|
-
exports.getCourseGrades = getCourseGrades;
|
|
548
|
-
/**
|
|
549
|
-
* Get course grades for the current user via pure WS API (no browser required).
|
|
375
|
+
* Get calendar events via pure WS API (no browser required).
|
|
550
376
|
* Fast and lightweight - uses HTTP fetch directly.
|
|
551
377
|
*/
|
|
552
|
-
async function
|
|
553
|
-
const data = await moodleApiCall(session, "
|
|
554
|
-
courseid: courseId,
|
|
555
|
-
});
|
|
556
|
-
const userGrades = data?.usergrades?.[0];
|
|
557
|
-
if (!userGrades) {
|
|
558
|
-
return { courseId, courseName: "", items: [] };
|
|
559
|
-
}
|
|
560
|
-
return {
|
|
561
|
-
courseId,
|
|
562
|
-
courseName: userGrades.coursefullname ?? "",
|
|
563
|
-
grade: userGrades.grade,
|
|
564
|
-
gradeFormatted: userGrades.gradeformatted,
|
|
565
|
-
rank: userGrades.rank,
|
|
566
|
-
totalUsers: userGrades.totalusers,
|
|
567
|
-
items: (userGrades.gradeitems ?? []).map((item) => ({
|
|
568
|
-
id: item.id,
|
|
569
|
-
name: item.itemname || item.itemmodule,
|
|
570
|
-
grade: item.grade,
|
|
571
|
-
gradeFormatted: item.gradeformatted,
|
|
572
|
-
range: item.graderangeformatted,
|
|
573
|
-
percentage: item.percentage,
|
|
574
|
-
weight: item.weight,
|
|
575
|
-
feedback: item.feedback,
|
|
576
|
-
graded: !!item.graded,
|
|
577
|
-
})),
|
|
578
|
-
};
|
|
579
|
-
}
|
|
580
|
-
exports.getCourseGradesApi = getCourseGradesApi;
|
|
581
|
-
// ── Calendar Operations ───────────────────────────────────────────────────
|
|
582
|
-
/**
|
|
583
|
-
* Get calendar events via AJAX.
|
|
584
|
-
*/
|
|
585
|
-
async function getCalendarEvents(page, session, options = {}) {
|
|
586
|
-
const data = await moodleAjax(page, session, "core_calendar_get_calendar_events", {
|
|
378
|
+
async function getCalendarEventsApi(session, options = {}) {
|
|
379
|
+
const data = await moodleApiCall(session, "core_calendar_get_calendar_events", {
|
|
587
380
|
...options,
|
|
588
381
|
});
|
|
589
382
|
return (data?.events ?? []).map((e) => ({
|
|
@@ -599,48 +392,50 @@ async function getCalendarEvents(page, session, options = {}) {
|
|
|
599
392
|
modulename: e.modulename,
|
|
600
393
|
instance: e.instance,
|
|
601
394
|
eventtype: e.eventtype,
|
|
602
|
-
timestart: e.timestart * 1000,
|
|
395
|
+
timestart: e.timestart * 1000, // Convert to milliseconds
|
|
603
396
|
timeduration: e.timeduration ? e.timeduration * 1000 : undefined,
|
|
604
397
|
timedue: e.timedue ? e.timedue * 1000 : undefined,
|
|
605
398
|
visible: e.visible,
|
|
606
399
|
location: e.location,
|
|
607
400
|
}));
|
|
608
401
|
}
|
|
609
|
-
|
|
402
|
+
// ── Grade Operations ──────────────────────────────────────────────────────
|
|
610
403
|
/**
|
|
611
|
-
* Get
|
|
404
|
+
* Get course grades for the current user via pure WS API (no browser required).
|
|
612
405
|
* Fast and lightweight - uses HTTP fetch directly.
|
|
613
406
|
*/
|
|
614
|
-
async function
|
|
615
|
-
const data = await moodleApiCall(session, "
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
visible: e.visible,
|
|
635
|
-
location: e.location,
|
|
636
|
-
}));
|
|
407
|
+
async function getCourseGradesApi(session, courseId) {
|
|
408
|
+
const data = await moodleApiCall(session, "gradereport_user_get_grade_items", { courseid: courseId });
|
|
409
|
+
// The API returns grade items for the course
|
|
410
|
+
const gradeItems = (data?.usergrades ?? []);
|
|
411
|
+
// Return a single CourseGrade object with items array
|
|
412
|
+
return {
|
|
413
|
+
courseId,
|
|
414
|
+
courseName: gradeItems[0]?.coursefullname ?? "",
|
|
415
|
+
grade: gradeItems[0]?.grade,
|
|
416
|
+
gradeFormatted: gradeItems[0]?.gradeformatted,
|
|
417
|
+
rank: gradeItems[0]?.rank,
|
|
418
|
+
totalUsers: gradeItems[0]?.totalusers,
|
|
419
|
+
items: gradeItems.map((g) => ({
|
|
420
|
+
id: g.id,
|
|
421
|
+
name: g.itemname || g.itemtype,
|
|
422
|
+
grade: g.grade,
|
|
423
|
+
gradeFormatted: g.gradeformatted,
|
|
424
|
+
range: g.grade ? `${g.grademin ?? 0}-${g.grademax ?? 100}` : undefined,
|
|
425
|
+
})),
|
|
426
|
+
};
|
|
637
427
|
}
|
|
638
|
-
exports.getCalendarEventsApi = getCalendarEventsApi;
|
|
639
428
|
// ── Video Metadata (from original course.ts) ───────────────────────────────
|
|
640
429
|
/**
|
|
641
430
|
* Visit a SuperVideo activity page and extract view_id + duration.
|
|
642
431
|
*/
|
|
432
|
+
/**
|
|
433
|
+
* Optimized video metadata extraction - minimal page load overhead.
|
|
434
|
+
* Blocks images, fonts, stylesheets to speed up viewId extraction.
|
|
435
|
+
*/
|
|
643
436
|
async function getVideoMetadata(page, activityUrl, log) {
|
|
437
|
+
// Block unnecessary resources for faster loading
|
|
438
|
+
await page.route("**/*.{png,jpg,jpeg,gif,webp,svg,ico,woff,woff2,ttf,css}", (route) => route.abort());
|
|
644
439
|
await page.goto(activityUrl, { waitUntil: "domcontentloaded", timeout: 20000 });
|
|
645
440
|
const name = await page.title();
|
|
646
441
|
const pageSource = await page.content();
|
|
@@ -754,7 +549,6 @@ async function getVideoMetadata(page, activityUrl, log) {
|
|
|
754
549
|
youtubeIds,
|
|
755
550
|
};
|
|
756
551
|
}
|
|
757
|
-
exports.getVideoMetadata = getVideoMetadata;
|
|
758
552
|
// ── Video Download ─────────────────────────────────────────────────────────────
|
|
759
553
|
/**
|
|
760
554
|
* Download a video from SuperVideo activity.
|
|
@@ -788,7 +582,7 @@ async function downloadVideo(page, metadata, outputPath, log) {
|
|
|
788
582
|
// Get array buffer and convert to Uint8Array
|
|
789
583
|
const arrayBuffer = await response.arrayBuffer();
|
|
790
584
|
const uint8Array = new Uint8Array(arrayBuffer);
|
|
791
|
-
// Write to file
|
|
585
|
+
// Write to file
|
|
792
586
|
await dntShim.Deno.writeFile(outputPath, uint8Array);
|
|
793
587
|
return { success: true, path: outputPath, type: "direct" };
|
|
794
588
|
}
|
|
@@ -821,24 +615,57 @@ async function downloadVideo(page, metadata, outputPath, log) {
|
|
|
821
615
|
error: "未找到影片來源",
|
|
822
616
|
};
|
|
823
617
|
}
|
|
824
|
-
exports.downloadVideo = downloadVideo;
|
|
825
618
|
// ── Progress Completion (from original progress.ts) ───────────────────────
|
|
826
619
|
/**
|
|
827
|
-
*
|
|
620
|
+
* Build duration map array for video progress tracking.
|
|
621
|
+
* Cached and scaled per duration to avoid repeated allocations.
|
|
828
622
|
*/
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
// Build duration map array (required by Moodle)
|
|
623
|
+
function buildDurationMap(duration) {
|
|
624
|
+
// Build the map array (0% to 100% in 1% increments)
|
|
832
625
|
const map = Array.from({ length: 100 }, (_, i) => ({
|
|
833
626
|
time: Math.round((duration * i) / 100),
|
|
834
627
|
percent: i,
|
|
835
628
|
}));
|
|
629
|
+
return JSON.stringify(map);
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Complete a video using WS API (mobile service only).
|
|
633
|
+
* Uses mod_supervideo_progress_save_mobile which is accessible via moodle_mobile_app service token.
|
|
634
|
+
*/
|
|
635
|
+
async function completeVideoApi(session, video) {
|
|
636
|
+
const { viewId, duration } = video;
|
|
637
|
+
try {
|
|
638
|
+
const result = await moodleApiCall(session, "mod_supervideo_progress_save_mobile", // Use mobile service specific function
|
|
639
|
+
{
|
|
640
|
+
view_id: viewId,
|
|
641
|
+
currenttime: duration,
|
|
642
|
+
duration: duration,
|
|
643
|
+
percent: 100,
|
|
644
|
+
mapa: buildDurationMap(duration),
|
|
645
|
+
});
|
|
646
|
+
// Debug: log the full result
|
|
647
|
+
console.debug(`completeVideoApi result:`, JSON.stringify(result));
|
|
648
|
+
const success = result?.[0]?.success === true || result?.success === true;
|
|
649
|
+
return { success, error: success ? undefined : `API returned success=false, result=${JSON.stringify(result)}`, result };
|
|
650
|
+
}
|
|
651
|
+
catch (err) {
|
|
652
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
653
|
+
console.debug(`completeVideoApi error: ${msg}`);
|
|
654
|
+
return { success: false, error: msg };
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Complete a video by forging progress AJAX call (legacy, requires browser).
|
|
659
|
+
* Note: This uses sesskey-based AJAX which works for mod_supervideo_progress_save.
|
|
660
|
+
*/
|
|
661
|
+
async function completeVideo(page, session, video, log) {
|
|
662
|
+
const { viewId, duration } = video;
|
|
836
663
|
const payload = {
|
|
837
664
|
view_id: viewId,
|
|
838
665
|
currenttime: duration,
|
|
839
666
|
duration: duration,
|
|
840
667
|
percent: 100,
|
|
841
|
-
mapa:
|
|
668
|
+
mapa: buildDurationMap(duration),
|
|
842
669
|
};
|
|
843
670
|
const url = `${session.moodleBaseUrl}/lib/ajax/service.php?sesskey=${session.sesskey}&info=mod_supervideo_progress_save`;
|
|
844
671
|
const ajaxPayload = [{ index: 0, methodname: "mod_supervideo_progress_save", args: payload }];
|
|
@@ -862,21 +689,95 @@ async function completeVideo(page, session, video, log) {
|
|
|
862
689
|
return false;
|
|
863
690
|
}
|
|
864
691
|
}
|
|
865
|
-
|
|
692
|
+
/**
|
|
693
|
+
* Update activity completion status manually via WS API.
|
|
694
|
+
* Used for marking resources as complete/incomplete.
|
|
695
|
+
*/
|
|
696
|
+
async function updateActivityCompletionStatusManually(session, cmid, completed) {
|
|
697
|
+
try {
|
|
698
|
+
const result = await moodleApiCall(session, "core_completion_update_activity_completion_status_manually", {
|
|
699
|
+
cmid: cmid,
|
|
700
|
+
completed: completed ? 1 : 0,
|
|
701
|
+
});
|
|
702
|
+
return result.status === true;
|
|
703
|
+
}
|
|
704
|
+
catch (e) {
|
|
705
|
+
console.debug(`Failed to update completion status for cmid ${cmid}: ${e}`);
|
|
706
|
+
return false;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
866
709
|
// ── Site Info (Get User ID) ───────────────────────────────────────────────────
|
|
710
|
+
/** Cache for site info to avoid redundant API calls */
|
|
711
|
+
let siteInfoCache = null;
|
|
712
|
+
let siteInfoCacheTime = 0;
|
|
713
|
+
const SITE_INFO_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
867
714
|
/**
|
|
868
715
|
* Get site info including current user ID via pure WS API.
|
|
716
|
+
* Results are cached for 5 minutes to avoid redundant calls.
|
|
869
717
|
*/
|
|
870
718
|
async function getSiteInfoApi(session) {
|
|
719
|
+
const now = Date.now();
|
|
720
|
+
if (siteInfoCache && (now - siteInfoCacheTime) < SITE_INFO_CACHE_TTL) {
|
|
721
|
+
return siteInfoCache;
|
|
722
|
+
}
|
|
871
723
|
const data = await moodleApiCall(session, "core_webservice_get_site_info", {});
|
|
872
|
-
|
|
724
|
+
siteInfoCache = {
|
|
873
725
|
userid: data.userid,
|
|
874
726
|
username: data.username,
|
|
875
727
|
fullname: data.fullname,
|
|
876
728
|
sitename: data.sitename,
|
|
877
729
|
};
|
|
730
|
+
siteInfoCacheTime = now;
|
|
731
|
+
return siteInfoCache;
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Get incomplete supervideos with completion tracking enabled via WS API.
|
|
735
|
+
* Uses core_completion_get_activities_completion_status to get only videos that:
|
|
736
|
+
* 1. Have completion tracking enabled (hascompletion: true)
|
|
737
|
+
* 2. Are not yet completed (isoverallcomplete: false or state !== 1)
|
|
738
|
+
*/
|
|
739
|
+
async function getIncompleteVideosApi(session, courseId) {
|
|
740
|
+
// Get user ID
|
|
741
|
+
const siteInfo = await getSiteInfoApi(session);
|
|
742
|
+
// Get completion status for all activities
|
|
743
|
+
const completionData = await moodleApiCall(session, "core_completion_get_activities_completion_status", { courseid: courseId, userid: siteInfo.userid });
|
|
744
|
+
if (!completionData?.statuses) {
|
|
745
|
+
return [];
|
|
746
|
+
}
|
|
747
|
+
// Get course contents to get URLs
|
|
748
|
+
const contentsData = await moodleApiCall(session, "core_course_get_contents", { courseid: courseId });
|
|
749
|
+
// Create a map of cmid to URL
|
|
750
|
+
const urlMap = new Map();
|
|
751
|
+
for (const section of contentsData || []) {
|
|
752
|
+
if (!section.modules)
|
|
753
|
+
continue;
|
|
754
|
+
for (const module of section.modules) {
|
|
755
|
+
if (module.id) {
|
|
756
|
+
urlMap.set(module.id, module.url);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
// Filter for incomplete supervideos with completion tracking enabled
|
|
761
|
+
const incompleteVideos = [];
|
|
762
|
+
for (const status of completionData.statuses) {
|
|
763
|
+
// Only supervideo modules
|
|
764
|
+
if (status.modname !== "supervideo")
|
|
765
|
+
continue;
|
|
766
|
+
// Must have completion enabled
|
|
767
|
+
if (!status.hascompletion)
|
|
768
|
+
continue;
|
|
769
|
+
// Must be incomplete
|
|
770
|
+
if (status.isoverallcomplete === true || status.state === 1)
|
|
771
|
+
continue;
|
|
772
|
+
const url = urlMap.get(status.cmid) || "";
|
|
773
|
+
incompleteVideos.push({
|
|
774
|
+
cmid: status.cmid,
|
|
775
|
+
name: status.name,
|
|
776
|
+
url,
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
return incompleteVideos;
|
|
878
780
|
}
|
|
879
|
-
exports.getSiteInfoApi = getSiteInfoApi;
|
|
880
781
|
// ── Videos via WS API ─────────────────────────────────────────────────────────
|
|
881
782
|
/**
|
|
882
783
|
* Get course contents and filter for SuperVideo modules via pure WS API.
|
|
@@ -896,14 +797,41 @@ async function getSupervideosInCourseApi(session, courseId) {
|
|
|
896
797
|
cmid: module.id.toString(),
|
|
897
798
|
name: module.name,
|
|
898
799
|
url: module.url,
|
|
899
|
-
|
|
800
|
+
instance: module.instance, // supervideo instance id (not cmid!)
|
|
801
|
+
isComplete: false, // Will be updated from completion API
|
|
900
802
|
});
|
|
901
803
|
}
|
|
902
804
|
}
|
|
903
805
|
}
|
|
806
|
+
// Get completion status using core_completion_get_activities_completion_status
|
|
807
|
+
try {
|
|
808
|
+
// First get user ID
|
|
809
|
+
const siteInfo = await getSiteInfoApi(session);
|
|
810
|
+
const completionData = await moodleApiCall(session, "core_completion_get_activities_completion_status", { courseid: courseId, userid: siteInfo.userid });
|
|
811
|
+
// Create a map of cmid to completion status
|
|
812
|
+
const completionMap = new Map();
|
|
813
|
+
if (completionData?.statuses) {
|
|
814
|
+
for (const status of completionData.statuses) {
|
|
815
|
+
// Only include modules that have completion enabled
|
|
816
|
+
if (status.hascompletion) {
|
|
817
|
+
completionMap.set(status.cmid, status.isoverallcomplete === true);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
// Update isComplete based on completion data
|
|
822
|
+
for (const video of videos) {
|
|
823
|
+
const cmid = parseInt(video.cmid, 10);
|
|
824
|
+
if (completionMap.has(cmid)) {
|
|
825
|
+
video.isComplete = completionMap.get(cmid) ?? false;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
catch (e) {
|
|
830
|
+
// If completion API fails, continue with isComplete=false
|
|
831
|
+
console.debug(`Failed to get completion status: ${e}`);
|
|
832
|
+
}
|
|
904
833
|
return videos;
|
|
905
834
|
}
|
|
906
|
-
exports.getSupervideosInCourseApi = getSupervideosInCourseApi;
|
|
907
835
|
/**
|
|
908
836
|
* Get quizzes in courses via pure WS API.
|
|
909
837
|
*/
|
|
@@ -915,13 +843,12 @@ async function getQuizzesByCoursesApi(session, courseIds) {
|
|
|
915
843
|
cmid: q.coursemodule.toString(),
|
|
916
844
|
name: q.name,
|
|
917
845
|
url: q.viewurl,
|
|
918
|
-
isComplete: false,
|
|
846
|
+
isComplete: false, // API doesn't provide completion status
|
|
919
847
|
timeOpen: q.timeopen,
|
|
920
848
|
timeClose: q.timeclose,
|
|
921
849
|
courseId: q.course,
|
|
922
850
|
}));
|
|
923
851
|
}
|
|
924
|
-
exports.getQuizzesByCoursesApi = getQuizzesByCoursesApi;
|
|
925
852
|
// ── Materials via WS API ──────────────────────────────────────────────────────
|
|
926
853
|
/**
|
|
927
854
|
* Get resources in courses via pure WS API.
|
|
@@ -938,14 +865,13 @@ async function getResourcesByCoursesApi(session, courseIds) {
|
|
|
938
865
|
name: r.name,
|
|
939
866
|
url: firstFile?.fileurl ?? "",
|
|
940
867
|
courseId: r.course,
|
|
941
|
-
modType: "resource",
|
|
868
|
+
modType: "resource", // This API only returns resources
|
|
942
869
|
mimetype: firstFile?.mimetype,
|
|
943
870
|
filesize: firstFile?.filesize,
|
|
944
871
|
modified: r.timemodified,
|
|
945
872
|
};
|
|
946
873
|
});
|
|
947
874
|
}
|
|
948
|
-
exports.getResourcesByCoursesApi = getResourcesByCoursesApi;
|
|
949
875
|
/**
|
|
950
876
|
* Get messages for the current user via pure WS API.
|
|
951
877
|
*/
|
|
@@ -963,4 +889,3 @@ async function getMessagesApi(session, userIdTo, options = {}) {
|
|
|
963
889
|
fullmessagehtml: m.fullmessagehtml,
|
|
964
890
|
}));
|
|
965
891
|
}
|
|
966
|
-
exports.getMessagesApi = getMessagesApi;
|