@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.
- package/README.md +11 -8
- package/esm/deno.js +1 -1
- package/esm/src/commands/announcements.d.ts.map +1 -1
- package/esm/src/commands/announcements.js +22 -85
- package/esm/src/commands/assignments.d.ts.map +1 -1
- package/esm/src/commands/assignments.js +2 -3
- package/esm/src/commands/auth.d.ts.map +1 -1
- package/esm/src/commands/auth.js +24 -14
- package/esm/src/commands/calendar.d.ts.map +1 -1
- package/esm/src/commands/calendar.js +32 -84
- package/esm/src/commands/courses.d.ts.map +1 -1
- package/esm/src/commands/courses.js +2 -38
- package/esm/src/commands/forums.d.ts.map +1 -1
- package/esm/src/commands/forums.js +47 -175
- package/esm/src/commands/grades.d.ts.map +1 -1
- package/esm/src/commands/grades.js +10 -47
- package/esm/src/commands/materials.d.ts.map +1 -1
- package/esm/src/commands/materials.js +135 -223
- package/esm/src/commands/quizzes.d.ts.map +1 -1
- package/esm/src/commands/quizzes.js +32 -56
- package/esm/src/commands/skills.js +3 -3
- package/esm/src/commands/upload.d.ts.map +1 -1
- package/esm/src/commands/upload.js +2 -5
- package/esm/src/commands/videos.d.ts.map +1 -1
- package/esm/src/commands/videos.js +6 -76
- package/esm/src/index.d.ts +2 -1
- package/esm/src/index.d.ts.map +1 -1
- package/esm/src/index.js +5 -1
- package/esm/src/lib/auth.d.ts +21 -2
- package/esm/src/lib/auth.d.ts.map +1 -1
- package/esm/src/lib/auth.js +79 -20
- package/esm/src/lib/logger.d.ts +2 -2
- package/esm/src/lib/logger.d.ts.map +1 -1
- package/esm/src/lib/logger.js +6 -4
- package/esm/src/lib/moodle.d.ts +18 -0
- package/esm/src/lib/moodle.d.ts.map +1 -1
- package/esm/src/lib/moodle.js +54 -2
- package/esm/src/lib/utils.d.ts +3 -8
- package/esm/src/lib/utils.d.ts.map +1 -1
- package/esm/src/lib/utils.js +3 -10
- package/package.json +1 -1
- package/script/deno.js +1 -1
- package/script/src/commands/announcements.d.ts.map +1 -1
- package/script/src/commands/announcements.js +23 -89
- package/script/src/commands/assignments.d.ts.map +1 -1
- package/script/src/commands/assignments.js +2 -3
- package/script/src/commands/auth.d.ts.map +1 -1
- package/script/src/commands/auth.js +24 -14
- package/script/src/commands/calendar.d.ts.map +1 -1
- package/script/src/commands/calendar.js +33 -85
- package/script/src/commands/courses.d.ts.map +1 -1
- package/script/src/commands/courses.js +9 -48
- package/script/src/commands/forums.d.ts.map +1 -1
- package/script/src/commands/forums.js +50 -181
- package/script/src/commands/grades.d.ts.map +1 -1
- package/script/src/commands/grades.js +14 -54
- package/script/src/commands/materials.d.ts.map +1 -1
- package/script/src/commands/materials.js +132 -220
- package/script/src/commands/quizzes.d.ts.map +1 -1
- package/script/src/commands/quizzes.js +40 -67
- package/script/src/commands/skills.js +3 -3
- package/script/src/commands/upload.d.ts.map +1 -1
- package/script/src/commands/upload.js +2 -5
- package/script/src/commands/videos.d.ts.map +1 -1
- package/script/src/commands/videos.js +11 -81
- package/script/src/index.d.ts +2 -1
- package/script/src/index.d.ts.map +1 -1
- package/script/src/index.js +5 -1
- package/script/src/lib/auth.d.ts +21 -2
- package/script/src/lib/auth.d.ts.map +1 -1
- package/script/src/lib/auth.js +83 -56
- package/script/src/lib/logger.d.ts +2 -2
- package/script/src/lib/logger.d.ts.map +1 -1
- package/script/src/lib/logger.js +6 -4
- package/script/src/lib/moodle.d.ts +18 -0
- package/script/src/lib/moodle.d.ts.map +1 -1
- package/script/src/lib/moodle.js +56 -2
- package/script/src/lib/utils.d.ts +3 -8
- package/script/src/lib/utils.d.ts.map +1 -1
- package/script/src/lib/utils.js +3 -11
- package/skills/openape/SKILL.md +10 -8
package/script/src/lib/moodle.js
CHANGED
|
@@ -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}
|
|
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
|
-
*
|
|
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,
|
|
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"}
|
package/script/src/lib/utils.js
CHANGED
|
@@ -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}
|
|
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
|
-
*
|
|
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;
|
package/skills/openape/SKILL.md
CHANGED
|
@@ -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
|
|
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: `--
|
|
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
|
|
88
|
-
- `export` — Export calendar events to file. Flags: `--
|
|
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
|
-
|
|
103
|
+
Most data commands support `--output`: `json` (default), `csv`, `table`, `silent`
|
|
102
104
|
|
|
103
105
|
Global flags: `--verbose`, `--headed`, `--session <path>`
|
|
104
106
|
|