@mo7yw4ng/openape 1.0.4 → 1.0.6

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 (81) hide show
  1. package/README.md +11 -8
  2. package/esm/deno.js +1 -1
  3. package/esm/src/commands/announcements.d.ts.map +1 -1
  4. package/esm/src/commands/announcements.js +22 -85
  5. package/esm/src/commands/assignments.d.ts.map +1 -1
  6. package/esm/src/commands/assignments.js +2 -3
  7. package/esm/src/commands/auth.d.ts.map +1 -1
  8. package/esm/src/commands/auth.js +24 -14
  9. package/esm/src/commands/calendar.d.ts.map +1 -1
  10. package/esm/src/commands/calendar.js +32 -84
  11. package/esm/src/commands/courses.d.ts.map +1 -1
  12. package/esm/src/commands/courses.js +2 -38
  13. package/esm/src/commands/forums.d.ts.map +1 -1
  14. package/esm/src/commands/forums.js +47 -175
  15. package/esm/src/commands/grades.d.ts.map +1 -1
  16. package/esm/src/commands/grades.js +10 -47
  17. package/esm/src/commands/materials.d.ts.map +1 -1
  18. package/esm/src/commands/materials.js +135 -223
  19. package/esm/src/commands/quizzes.d.ts.map +1 -1
  20. package/esm/src/commands/quizzes.js +32 -56
  21. package/esm/src/commands/skills.js +3 -3
  22. package/esm/src/commands/upload.d.ts.map +1 -1
  23. package/esm/src/commands/upload.js +2 -5
  24. package/esm/src/commands/videos.d.ts.map +1 -1
  25. package/esm/src/commands/videos.js +6 -76
  26. package/esm/src/index.d.ts +2 -1
  27. package/esm/src/index.d.ts.map +1 -1
  28. package/esm/src/index.js +5 -1
  29. package/esm/src/lib/auth.d.ts +21 -2
  30. package/esm/src/lib/auth.d.ts.map +1 -1
  31. package/esm/src/lib/auth.js +79 -20
  32. package/esm/src/lib/logger.d.ts +2 -2
  33. package/esm/src/lib/logger.d.ts.map +1 -1
  34. package/esm/src/lib/logger.js +6 -4
  35. package/esm/src/lib/moodle.d.ts +18 -0
  36. package/esm/src/lib/moodle.d.ts.map +1 -1
  37. package/esm/src/lib/moodle.js +54 -2
  38. package/esm/src/lib/utils.d.ts +3 -8
  39. package/esm/src/lib/utils.d.ts.map +1 -1
  40. package/esm/src/lib/utils.js +3 -10
  41. package/package.json +1 -1
  42. package/script/deno.js +1 -1
  43. package/script/src/commands/announcements.d.ts.map +1 -1
  44. package/script/src/commands/announcements.js +23 -89
  45. package/script/src/commands/assignments.d.ts.map +1 -1
  46. package/script/src/commands/assignments.js +2 -3
  47. package/script/src/commands/auth.d.ts.map +1 -1
  48. package/script/src/commands/auth.js +24 -14
  49. package/script/src/commands/calendar.d.ts.map +1 -1
  50. package/script/src/commands/calendar.js +33 -85
  51. package/script/src/commands/courses.d.ts.map +1 -1
  52. package/script/src/commands/courses.js +9 -48
  53. package/script/src/commands/forums.d.ts.map +1 -1
  54. package/script/src/commands/forums.js +50 -181
  55. package/script/src/commands/grades.d.ts.map +1 -1
  56. package/script/src/commands/grades.js +14 -54
  57. package/script/src/commands/materials.d.ts.map +1 -1
  58. package/script/src/commands/materials.js +132 -220
  59. package/script/src/commands/quizzes.d.ts.map +1 -1
  60. package/script/src/commands/quizzes.js +40 -67
  61. package/script/src/commands/skills.js +3 -3
  62. package/script/src/commands/upload.d.ts.map +1 -1
  63. package/script/src/commands/upload.js +2 -5
  64. package/script/src/commands/videos.d.ts.map +1 -1
  65. package/script/src/commands/videos.js +11 -81
  66. package/script/src/index.d.ts +2 -1
  67. package/script/src/index.d.ts.map +1 -1
  68. package/script/src/index.js +5 -1
  69. package/script/src/lib/auth.d.ts +21 -2
  70. package/script/src/lib/auth.d.ts.map +1 -1
  71. package/script/src/lib/auth.js +83 -56
  72. package/script/src/lib/logger.d.ts +2 -2
  73. package/script/src/lib/logger.d.ts.map +1 -1
  74. package/script/src/lib/logger.js +6 -4
  75. package/script/src/lib/moodle.d.ts +18 -0
  76. package/script/src/lib/moodle.d.ts.map +1 -1
  77. package/script/src/lib/moodle.js +56 -2
  78. package/script/src/lib/utils.d.ts +3 -8
  79. package/script/src/lib/utils.d.ts.map +1 -1
  80. package/script/src/lib/utils.js +3 -11
  81. package/skills/openape/SKILL.md +10 -8
@@ -40,6 +40,7 @@ exports.getEnrolledCourses = getEnrolledCourses;
40
40
  exports.getCourseState = getCourseState;
41
41
  exports.getSupervideosInCourse = getSupervideosInCourse;
42
42
  exports.getForumsApi = getForumsApi;
43
+ exports.resolveForumId = resolveForumId;
43
44
  exports.getForumDiscussionsApi = getForumDiscussionsApi;
44
45
  exports.getDiscussionPostsApi = getDiscussionPostsApi;
45
46
  exports.deleteForumPostApi = deleteForumPostApi;
@@ -58,6 +59,7 @@ exports.getIncompleteVideosApi = getIncompleteVideosApi;
58
59
  exports.getSupervideosInCourseApi = getSupervideosInCourseApi;
59
60
  exports.getQuizzesByCoursesApi = getQuizzesByCoursesApi;
60
61
  exports.startQuizAttemptApi = startQuizAttemptApi;
62
+ exports.getAllQuizAttemptDataApi = getAllQuizAttemptDataApi;
61
63
  exports.getQuizAttemptDataApi = getQuizAttemptDataApi;
62
64
  exports.processQuizAttemptApi = processQuizAttemptApi;
63
65
  exports.getResourcesByCoursesApi = getResourcesByCoursesApi;
@@ -319,6 +321,41 @@ async function getForumsApi(session, courseIds) {
319
321
  timemodified: f.timemodified,
320
322
  }));
321
323
  }
324
+ /**
325
+ * Resolve a forum ID (cmid or instance ID) to a forum instance ID.
326
+ * Tries cmid resolution first (via core_course_get_course_module) to get name/course info.
327
+ * Falls back to treating the ID as a raw forum instance ID.
328
+ */
329
+ async function resolveForumId(session, id) {
330
+ const numId = parseInt(id, 10);
331
+ // Try cmid resolution first (gets name + course info)
332
+ try {
333
+ const cm = await moodleApiCall(session, "core_course_get_course_module", { cmid: numId });
334
+ if (cm?.cm && cm.cm.modname === "forum") {
335
+ return {
336
+ forumId: cm.cm.instance,
337
+ cmid: numId,
338
+ name: cm.cm.name,
339
+ courseid: cm.cm.course,
340
+ };
341
+ }
342
+ }
343
+ catch {
344
+ // Not a valid cmid, try as forum instance ID
345
+ }
346
+ // Fall back: treat as forum instance ID directly
347
+ try {
348
+ const data = await moodleApiCall(session, "mod_forum_get_forum_discussions", { forumid: numId, limit: 1 });
349
+ // If we get discussions back (even empty), the forum exists
350
+ if (data) {
351
+ return { forumId: numId };
352
+ }
353
+ }
354
+ catch {
355
+ // Invalid forum instance ID
356
+ }
357
+ return null;
358
+ }
322
359
  /**
323
360
  * Get discussions in a forum via WS API (no browser required).
324
361
  * Uses mod_forum_get_forum_discussions
@@ -410,7 +447,6 @@ async function addForumDiscussionApi(session, forumId, subject, message) {
410
447
  forumid: forumId,
411
448
  subject,
412
449
  message: message.replace(/\n/g, "<br>"),
413
- messageformat: 1,
414
450
  });
415
451
  if (data?.discussionid) {
416
452
  return { success: true, discussionId: data.discussionid };
@@ -1043,6 +1079,24 @@ async function startQuizAttemptApi(session, quizId, options = {}) {
1043
1079
  /**
1044
1080
  * Get quiz attempt data including questions via pure WS API.
1045
1081
  */
1082
+ async function getAllQuizAttemptDataApi(session, attemptId) {
1083
+ const firstPage = await getQuizAttemptDataApi(session, attemptId, 0);
1084
+ // Moodle re-indexes question keys per page (always starts at 0),
1085
+ // so we must re-key by actual slot number to avoid overwrites.
1086
+ const allQuestions = {};
1087
+ for (const q of Object.values(firstPage.questions)) {
1088
+ allQuestions[q.slot] = q;
1089
+ }
1090
+ let nextPage = firstPage.nextpage;
1091
+ while (nextPage !== undefined && nextPage !== null && nextPage !== -1) {
1092
+ const pageData = await getQuizAttemptDataApi(session, attemptId, nextPage);
1093
+ for (const q of Object.values(pageData.questions)) {
1094
+ allQuestions[q.slot] = q;
1095
+ }
1096
+ nextPage = pageData.nextpage;
1097
+ }
1098
+ return { ...firstPage, questions: allQuestions, nextpage: undefined };
1099
+ }
1046
1100
  async function getQuizAttemptDataApi(session, attemptId, page = 0) {
1047
1101
  const data = await moodleApiCall(session, "mod_quiz_get_attempt_data", { attemptid: attemptId, page });
1048
1102
  if (!data?.attempt || !data?.questions) {
@@ -1288,7 +1342,7 @@ async function uploadFileApi(session, filePath, options) {
1288
1342
  // Prepare multipart form data
1289
1343
  const formData = new FormData();
1290
1344
  formData.append("token", session.wsToken);
1291
- formData.append("file", new Blob([fileContent]), fileName);
1345
+ formData.append("file", new Blob([new Uint8Array(fileContent)]), fileName);
1292
1346
  formData.append("filepath", options?.filepath || "/");
1293
1347
  formData.append("itemid", String(draftItemId)); // Use our generated draft ID
1294
1348
  formData.append("contextid", String(userContextId)); // Use calculated user context
@@ -12,7 +12,7 @@ export declare function stripHtmlTags(html: string): string;
12
12
  /**
13
13
  * Extract clean course name from Moodle fullname.
14
14
  * Removes mlang tags, course codes, and instructor info.
15
- * Example: "{mlang zh-tw}1142爵士樂賞析(遠距)-楊曊恩..." -> "爵士樂賞析"
15
+ * Example: "{mlang zh-tw}1142Jazz Analysis(Distance)-Instructor..." -> "Jazz Analysis"
16
16
  */
17
17
  export declare function extractCourseName(fullname: string): string;
18
18
  /**
@@ -24,11 +24,6 @@ export declare function getOutputFormat(command: {
24
24
  output?: OutputFormat;
25
25
  };
26
26
  }): OutputFormat;
27
- /**
28
- * Determine if logs should be silenced based on output format and verbosity.
29
- * JSON output without verbose flag silences logs.
30
- */
31
- export declare function shouldSilenceLogs(outputFormat: OutputFormat, verbose?: boolean): boolean;
32
27
  /**
33
28
  * Sanitize filename by removing invalid characters and limiting length.
34
29
  * Replaces invalid characters with underscores and limits to maxLength.
@@ -47,11 +42,11 @@ export declare function formatFileSize(bytes: number, decimals?: number): string
47
42
  */
48
43
  export declare function formatMoodleDate(timestamp?: number): string;
49
44
  /**
50
- * 統一時間戳記轉換 (預設:本地時間字串)
45
+ * Unified timestamp conversion (default: local time string)
51
46
  */
52
47
  export declare function formatTimestamp(timestamp: number | undefined | null, format?: "iso" | "local" | "relative"): string;
53
48
  /**
54
- * 相對時間格式 (e.g., "2 hours ago")
49
+ * Relative time format (e.g., "2 hours ago")
55
50
  */
56
51
  export declare function formatRelativeTime(timestamp: number): string;
57
52
  //# sourceMappingURL=utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/src/lib/utils.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C;;;GAGG;AACH,wBAAgB,UAAU,IAAI,MAAM,CAcnC;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAelD;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAO1D;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE;IAAE,eAAe,IAAI;QAAE,MAAM,CAAC,EAAE,YAAY,CAAA;KAAE,CAAA;CAAE,GAAG,YAAY,CAGvG;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,YAAY,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,OAAO,CAExF;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,GAAE,MAAY,GAAG,MAAM,CAK9E;AAED;;GAEG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAGvC;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAU,GAAG,MAAM,CAE1E;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAG3D;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,EAAE,MAAM,GAAE,KAAK,GAAG,OAAO,GAAG,UAAoB,GAAG,MAAM,CAQ5H;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAM5D"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/src/lib/utils.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C;;;GAGG;AACH,wBAAgB,UAAU,IAAI,MAAM,CAcnC;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAelD;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAO1D;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE;IAAE,eAAe,IAAI;QAAE,MAAM,CAAC,EAAE,YAAY,CAAA;KAAE,CAAA;CAAE,GAAG,YAAY,CAGvG;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,GAAE,MAAY,GAAG,MAAM,CAK9E;AAED;;GAEG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAGvC;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAU,GAAG,MAAM,CAE1E;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAG3D;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,EAAE,MAAM,GAAE,KAAK,GAAG,OAAO,GAAG,UAAoB,GAAG,MAAM,CAQ5H;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAM5D"}
@@ -37,7 +37,6 @@ exports.getBaseDir = getBaseDir;
37
37
  exports.stripHtmlTags = stripHtmlTags;
38
38
  exports.extractCourseName = extractCourseName;
39
39
  exports.getOutputFormat = getOutputFormat;
40
- exports.shouldSilenceLogs = shouldSilenceLogs;
41
40
  exports.sanitizeFilename = sanitizeFilename;
42
41
  exports.getSessionPath = getSessionPath;
43
42
  exports.formatFileSize = formatFileSize;
@@ -90,7 +89,7 @@ function stripHtmlTags(html) {
90
89
  /**
91
90
  * Extract clean course name from Moodle fullname.
92
91
  * Removes mlang tags, course codes, and instructor info.
93
- * Example: "{mlang zh-tw}1142爵士樂賞析(遠距)-楊曊恩..." -> "爵士樂賞析"
92
+ * Example: "{mlang zh-tw}1142Jazz Analysis(Distance)-Instructor..." -> "Jazz Analysis"
94
93
  */
95
94
  function extractCourseName(fullname) {
96
95
  if (!fullname)
@@ -109,13 +108,6 @@ function getOutputFormat(command) {
109
108
  const opts = command.optsWithGlobals();
110
109
  return opts.output || "json";
111
110
  }
112
- /**
113
- * Determine if logs should be silenced based on output format and verbosity.
114
- * JSON output without verbose flag silences logs.
115
- */
116
- function shouldSilenceLogs(outputFormat, verbose) {
117
- return outputFormat === "json" && !verbose;
118
- }
119
111
  /**
120
112
  * Sanitize filename by removing invalid characters and limiting length.
121
113
  * Replaces invalid characters with underscores and limits to maxLength.
@@ -148,7 +140,7 @@ function formatMoodleDate(timestamp) {
148
140
  return new Date(timestamp * 1000).toLocaleString("zh-TW");
149
141
  }
150
142
  /**
151
- * 統一時間戳記轉換 (預設:本地時間字串)
143
+ * Unified timestamp conversion (default: local time string)
152
144
  */
153
145
  function formatTimestamp(timestamp, format = "local") {
154
146
  if (!timestamp || timestamp === 0)
@@ -161,7 +153,7 @@ function formatTimestamp(timestamp, format = "local") {
161
153
  return date.toLocaleString("zh-TW");
162
154
  }
163
155
  /**
164
- * 相對時間格式 (e.g., "2 hours ago")
156
+ * Relative time format (e.g., "2 hours ago")
165
157
  */
166
158
  function formatRelativeTime(timestamp) {
167
159
  const seconds = Math.floor(Date.now() / 1000) - timestamp;
@@ -41,11 +41,13 @@ openape <command> [subcommand] [args] [flags]
41
41
 
42
42
  ### quizzes — Quiz operations
43
43
 
44
- - `list <course-id>` — List incomplete quizzes in a course
44
+ - `list <course-id>` — List incomplete quizzes in a course. Flags: `--all`
45
45
  - `list-all` — List all incomplete quizzes across courses. Flags: `--level in_progress|all`
46
46
  - `start <quiz-id>` — Start a new quiz attempt
47
47
  - `info <attempt-id>` — Get quiz attempt data and questions. Flags: `--page <number>`
48
- - `save <attempt-id> '<answers-json>'` — Save answers for a quiz attempt. JSON format: `[{"slot":1,"answer":"0"}]`. Multichoice: number, multichoices: `"0,2"`, shortanswer: text
48
+ - `save <attempt-id> '<answers-json>'` — Save answers for a quiz attempt. Flags: `--submit`. JSON format: `[{"slot":1,"answer":"0"}]`. Multichoice: number, multichoices: `"0,2"`, shortanswer: text
49
+
50
+ > **NEVER SUBMIT WITHOUT USER'S PERMISSION**, you have to make sure answer is saved before submitting.
49
51
 
50
52
  ### materials — Material/resource operations
51
53
 
@@ -69,12 +71,12 @@ openape <command> [subcommand] [args] [flags]
69
71
 
70
72
  ### forums — Forum operations
71
73
 
72
- - `list` — List forums from in-progress courses. Flags: `--level in_progress|all`
73
- - `list-all` — List all forums across all courses
74
+ - `list` — List forums from in-progress courses
75
+ - `list-all` — List all forums across all courses. Flags: `--level in_progress|all`
74
76
  - `discussions <forum-id>` — List discussions in a forum
75
77
  - `posts <discussion-id>` — Show posts in a discussion
76
78
  - `post <forum-id> <subject> <message>` — Post a new discussion. Flags: `--subscribe`, `--pin`
77
- - `reply <post-id> <subject> <message>` — Reply to a discussion post. Flags: `--parent-id <id>`
79
+ - `reply <post-id> <subject> <message>` — Reply to a discussion post. Flags: `--attachment-id <id>`, `--inline-attachment-id <id>`
78
80
  - `delete <post-id>` — Delete a forum post or discussion
79
81
 
80
82
  ### announcements — Announcement operations
@@ -84,8 +86,8 @@ openape <command> [subcommand] [args] [flags]
84
86
 
85
87
  ### calendar — Calendar operations
86
88
 
87
- - `events` — List calendar events. Flags: `--course-id <id>`, `--events-after <date>`, `--events-before <date>`
88
- - `export` — Export calendar events to file. Flags: `--format json|ics`, `--output <file>`
89
+ - `events` — List calendar events. Flags: `--course <id>`, `--upcoming`, `--days <n>`
90
+ - `export` — Export calendar events to file. Flags: `--output <path>`, `--days <n>`
89
91
 
90
92
  ### upload — File upload
91
93
 
@@ -98,7 +100,7 @@ openape <command> [subcommand] [args] [flags]
98
100
 
99
101
  ## Output Formats
100
102
 
101
- All commands support `--output`: `json` (default), `csv`, `table`, `silent`
103
+ Most data commands support `--output`: `json` (default), `csv`, `table`, `silent`
102
104
 
103
105
  Global flags: `--verbose`, `--headed`, `--session <path>`
104
106