@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.
Files changed (132) hide show
  1. package/README.md +8 -5
  2. package/esm/_dnt.polyfills.d.ts +83 -6
  3. package/esm/_dnt.polyfills.d.ts.map +1 -0
  4. package/esm/_dnt.polyfills.js +127 -1
  5. package/esm/_dnt.shims.d.ts +1 -0
  6. package/esm/_dnt.shims.d.ts.map +1 -0
  7. package/esm/deno.d.ts +2 -0
  8. package/esm/deno.d.ts.map +1 -0
  9. package/esm/deno.js +2 -1
  10. package/esm/src/commands/announcements.d.ts +3 -0
  11. package/esm/src/commands/announcements.d.ts.map +1 -0
  12. package/esm/src/commands/announcements.js +135 -0
  13. package/esm/src/commands/auth.d.ts +3 -0
  14. package/esm/src/commands/auth.d.ts.map +1 -0
  15. package/esm/src/commands/auth.js +260 -0
  16. package/esm/src/commands/calendar.d.ts +3 -0
  17. package/esm/src/commands/calendar.d.ts.map +1 -0
  18. package/esm/src/commands/calendar.js +180 -0
  19. package/esm/src/commands/courses.d.ts +3 -0
  20. package/esm/src/commands/courses.d.ts.map +1 -0
  21. package/esm/src/commands/courses.js +348 -0
  22. package/esm/src/commands/forums.d.ts +3 -0
  23. package/esm/src/commands/forums.d.ts.map +1 -0
  24. package/esm/src/commands/forums.js +226 -0
  25. package/esm/src/commands/grades.d.ts +3 -0
  26. package/esm/src/commands/grades.d.ts.map +1 -0
  27. package/esm/src/commands/grades.js +121 -0
  28. package/esm/src/commands/materials.d.ts +3 -0
  29. package/esm/src/commands/materials.d.ts.map +1 -0
  30. package/esm/src/commands/materials.js +522 -0
  31. package/esm/src/commands/quizzes.d.ts +3 -0
  32. package/esm/src/commands/quizzes.d.ts.map +1 -0
  33. package/esm/src/commands/quizzes.js +160 -0
  34. package/esm/src/commands/skills.d.ts +3 -0
  35. package/esm/src/commands/skills.d.ts.map +1 -0
  36. package/esm/src/commands/skills.js +110 -0
  37. package/esm/src/commands/videos.d.ts +3 -0
  38. package/esm/src/commands/videos.d.ts.map +1 -0
  39. package/esm/src/commands/videos.js +335 -0
  40. package/esm/src/index.d.ts +27 -0
  41. package/esm/src/index.d.ts.map +1 -0
  42. package/esm/src/index.js +149 -0
  43. package/esm/src/lib/auth.d.ts +25 -0
  44. package/esm/src/lib/auth.d.ts.map +1 -0
  45. package/esm/src/lib/auth.js +194 -0
  46. package/esm/src/lib/config.d.ts +6 -0
  47. package/esm/src/lib/config.d.ts.map +1 -0
  48. package/esm/src/lib/config.js +36 -0
  49. package/esm/src/lib/logger.d.ts +3 -0
  50. package/esm/src/lib/logger.d.ts.map +1 -0
  51. package/esm/src/lib/logger.js +24 -0
  52. package/esm/src/lib/moodle.d.ts +251 -0
  53. package/esm/src/lib/moodle.d.ts.map +1 -0
  54. package/esm/src/lib/moodle.js +833 -0
  55. package/esm/src/lib/session.d.ts +8 -0
  56. package/esm/src/lib/session.d.ts.map +1 -0
  57. package/esm/src/lib/session.js +42 -0
  58. package/esm/src/lib/token.d.ts +38 -0
  59. package/esm/src/lib/token.d.ts.map +1 -0
  60. package/esm/src/lib/token.js +178 -0
  61. package/esm/src/lib/types.d.ts +272 -0
  62. package/esm/src/lib/types.d.ts.map +1 -0
  63. package/esm/src/lib/types.js +1 -0
  64. package/esm/src/lib/utils.d.ts +37 -0
  65. package/esm/src/lib/utils.d.ts.map +1 -0
  66. package/esm/src/lib/utils.js +82 -0
  67. package/package.json +4 -4
  68. package/script/_dnt.polyfills.d.ts +83 -6
  69. package/script/_dnt.polyfills.d.ts.map +1 -0
  70. package/script/_dnt.polyfills.js +128 -0
  71. package/script/_dnt.shims.d.ts +1 -0
  72. package/script/_dnt.shims.d.ts.map +1 -0
  73. package/script/deno.d.ts +2 -0
  74. package/script/deno.d.ts.map +1 -0
  75. package/script/deno.js +2 -1
  76. package/script/src/commands/announcements.d.ts +1 -0
  77. package/script/src/commands/announcements.d.ts.map +1 -0
  78. package/script/src/commands/announcements.js +75 -222
  79. package/script/src/commands/auth.d.ts +2 -1
  80. package/script/src/commands/auth.d.ts.map +1 -0
  81. package/script/src/commands/auth.js +52 -24
  82. package/script/src/commands/calendar.d.ts +1 -0
  83. package/script/src/commands/calendar.d.ts.map +1 -0
  84. package/script/src/commands/calendar.js +112 -301
  85. package/script/src/commands/courses.d.ts +1 -0
  86. package/script/src/commands/courses.d.ts.map +1 -0
  87. package/script/src/commands/courses.js +43 -173
  88. package/script/src/commands/forums.d.ts +1 -0
  89. package/script/src/commands/forums.d.ts.map +1 -0
  90. package/script/src/commands/forums.js +145 -316
  91. package/script/src/commands/grades.d.ts +1 -0
  92. package/script/src/commands/grades.d.ts.map +1 -0
  93. package/script/src/commands/grades.js +62 -194
  94. package/script/src/commands/materials.d.ts +1 -0
  95. package/script/src/commands/materials.d.ts.map +1 -0
  96. package/script/src/commands/materials.js +283 -178
  97. package/script/src/commands/quizzes.d.ts +1 -0
  98. package/script/src/commands/quizzes.d.ts.map +1 -0
  99. package/script/src/commands/quizzes.js +40 -102
  100. package/script/src/commands/skills.d.ts +1 -0
  101. package/script/src/commands/skills.d.ts.map +1 -0
  102. package/script/src/commands/skills.js +17 -18
  103. package/script/src/commands/videos.d.ts +1 -0
  104. package/script/src/commands/videos.d.ts.map +1 -0
  105. package/script/src/commands/videos.js +127 -120
  106. package/script/src/index.d.ts +1 -0
  107. package/script/src/index.d.ts.map +1 -0
  108. package/script/src/index.js +5 -5
  109. package/script/src/lib/auth.d.ts +1 -0
  110. package/script/src/lib/auth.d.ts.map +1 -0
  111. package/script/src/lib/auth.js +9 -10
  112. package/script/src/lib/config.d.ts +1 -0
  113. package/script/src/lib/config.d.ts.map +1 -0
  114. package/script/src/lib/config.js +6 -7
  115. package/script/src/lib/logger.d.ts +1 -0
  116. package/script/src/lib/logger.d.ts.map +1 -0
  117. package/script/src/lib/logger.js +1 -2
  118. package/script/src/lib/moodle.d.ts +72 -55
  119. package/script/src/lib/moodle.d.ts.map +1 -0
  120. package/script/src/lib/moodle.js +275 -350
  121. package/script/src/lib/session.d.ts +1 -0
  122. package/script/src/lib/session.d.ts.map +1 -0
  123. package/script/src/lib/session.js +3 -29
  124. package/script/src/lib/token.d.ts +16 -5
  125. package/script/src/lib/token.d.ts.map +1 -0
  126. package/script/src/lib/token.js +71 -36
  127. package/script/src/lib/types.d.ts +11 -0
  128. package/script/src/lib/types.d.ts.map +1 -0
  129. package/script/src/lib/utils.d.ts +32 -0
  130. package/script/src/lib/utils.d.ts.map +1 -0
  131. package/script/src/lib/utils.js +93 -13
  132. package/skills/openape/SKILL.md +6 -26
@@ -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 (mod) {
19
- if (mod && mod.__esModule) return mod;
20
- var result = {};
21
- if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
- __setModuleDefault(result, mod);
23
- return result;
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.getMessagesApi = exports.getResourcesByCoursesApi = exports.getQuizzesByCoursesApi = exports.getSupervideosInCourseApi = exports.getSiteInfoApi = exports.completeVideo = exports.downloadVideo = exports.getVideoMetadata = exports.getCalendarEventsApi = exports.getCalendarEvents = exports.getCourseGradesApi = exports.getCourseGrades = exports.getResourcesInCourse = exports.getDiscussionPosts = exports.getForumDiscussions = exports.getForumsByCourseIds = exports.getForumIdFromPage = exports.getForumsApi = exports.getForumsInCourse = exports.getQuizzesInCourse = exports.getSupervideosInCourse = exports.getCourseState = exports.getEnrolledCourses = exports.getEnrolledCoursesApi = exports.moodleAjax = exports.moodleApiCall = void 0;
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 node_html_parser_1 = require("node-html-parser");
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
- log.debug(` Found ${allSupervideos.length} supervideo modules`);
235
- const incomplete = allSupervideos.filter((cm) => !("isoverallcomplete" in cm && cm.isoverallcomplete));
236
- log.debug(` SuperVideo: ${allSupervideos.length} total, ${incomplete.length} incomplete`);
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 AJAX.
443
- * Note: Requires forum instance ID, not cmid. Use getForumsByCourseIds() first.
288
+ * Get discussions in a forum via WS API (no browser required).
289
+ * Uses mod_forum_get_forum_discussions
444
290
  */
445
- async function getForumDiscussions(page, session, forumId) {
446
- const data = await moodleAjax(page, session, "mod_forum_get_forum_discussions", {
447
- forumid: forumId,
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.id,
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
- postCount: d.numdiscussion,
460
- unread: d.unread,
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 AJAX.
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 getDiscussionPosts(page, session, discussionId) {
327
+ async function getDiscussionPostsApi(session, discussionId) {
468
328
  try {
469
- const data = await moodleAjax(page, session, "mod_forum_get_forum_discussion_posts", {
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 ?? p.username ?? "Unknown",
479
- authorId: p.userid,
480
- created: p.created,
481
- modified: p.modified,
482
- message: p.message || "",
483
- discussionId: p.discussion,
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
- exports.getResourcesInCourse = getResourcesInCourse;
515
- // ── Grade Operations ──────────────────────────────────────────────────────
373
+ // ── Calendar Operations ─────────────────────────────────────────────────────
516
374
  /**
517
- * Get course grades for the current user via AJAX.
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 getCourseGradesApi(session, courseId) {
553
- const data = await moodleApiCall(session, "gradereport_user_get_grade_items", {
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
- exports.getCalendarEvents = getCalendarEvents;
402
+ // ── Grade Operations ──────────────────────────────────────────────────────
610
403
  /**
611
- * Get calendar events via pure WS API (no browser required).
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 getCalendarEventsApi(session, options = {}) {
615
- const data = await moodleApiCall(session, "core_calendar_get_calendar_events", {
616
- ...options,
617
- });
618
- return (data?.events ?? []).map((e) => ({
619
- id: e.id,
620
- name: e.name,
621
- description: e.description,
622
- format: e.format,
623
- courseid: e.courseid,
624
- categoryid: e.categoryid,
625
- groupid: e.groupid,
626
- userid: e.userid,
627
- moduleid: e.moduleid,
628
- modulename: e.modulename,
629
- instance: e.instance,
630
- eventtype: e.eventtype,
631
- timestart: e.timestart * 1000,
632
- timeduration: e.timeduration ? e.timeduration * 1000 : undefined,
633
- timedue: e.timedue ? e.timedue * 1000 : undefined,
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 using Deno
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
- * Complete a video by forging progress AJAX call.
620
+ * Build duration map array for video progress tracking.
621
+ * Cached and scaled per duration to avoid repeated allocations.
828
622
  */
829
- async function completeVideo(page, session, video, log) {
830
- const { viewId, duration } = video;
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: JSON.stringify(map),
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
- exports.completeVideo = completeVideo;
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
- return {
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
- isComplete: false, // API doesn't provide completion status
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;