@mo7yw4ng/openape 1.0.6 → 2.0.4
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/bin/openape +29 -0
- package/bin/openape.js +32 -0
- package/package.json +24 -28
- package/LICENSE +0 -21
- package/README.md +0 -138
- package/esm/_dnt.polyfills.d.ts +0 -101
- package/esm/_dnt.polyfills.d.ts.map +0 -1
- package/esm/_dnt.polyfills.js +0 -127
- package/esm/_dnt.shims.d.ts +0 -6
- package/esm/_dnt.shims.d.ts.map +0 -1
- package/esm/_dnt.shims.js +0 -61
- package/esm/deno.d.ts +0 -25
- package/esm/deno.d.ts.map +0 -1
- package/esm/deno.js +0 -23
- package/esm/package.json +0 -3
- package/esm/src/commands/announcements.d.ts +0 -3
- package/esm/src/commands/announcements.d.ts.map +0 -1
- package/esm/src/commands/announcements.js +0 -71
- package/esm/src/commands/assignments.d.ts +0 -3
- package/esm/src/commands/assignments.d.ts.map +0 -1
- package/esm/src/commands/assignments.js +0 -229
- package/esm/src/commands/auth.d.ts +0 -3
- package/esm/src/commands/auth.d.ts.map +0 -1
- package/esm/src/commands/auth.js +0 -290
- package/esm/src/commands/calendar.d.ts +0 -3
- package/esm/src/commands/calendar.d.ts.map +0 -1
- package/esm/src/commands/calendar.js +0 -127
- package/esm/src/commands/courses.d.ts +0 -3
- package/esm/src/commands/courses.d.ts.map +0 -1
- package/esm/src/commands/courses.js +0 -312
- package/esm/src/commands/forums.d.ts +0 -3
- package/esm/src/commands/forums.d.ts.map +0 -1
- package/esm/src/commands/forums.js +0 -190
- package/esm/src/commands/grades.d.ts +0 -3
- package/esm/src/commands/grades.d.ts.map +0 -1
- package/esm/src/commands/grades.js +0 -84
- package/esm/src/commands/materials.d.ts +0 -3
- package/esm/src/commands/materials.d.ts.map +0 -1
- package/esm/src/commands/materials.js +0 -402
- package/esm/src/commands/quizzes.d.ts +0 -3
- package/esm/src/commands/quizzes.d.ts.map +0 -1
- package/esm/src/commands/quizzes.js +0 -236
- package/esm/src/commands/skills.d.ts +0 -3
- package/esm/src/commands/skills.d.ts.map +0 -1
- package/esm/src/commands/skills.js +0 -106
- package/esm/src/commands/upload.d.ts +0 -3
- package/esm/src/commands/upload.d.ts.map +0 -1
- package/esm/src/commands/upload.js +0 -55
- package/esm/src/commands/videos.d.ts +0 -3
- package/esm/src/commands/videos.d.ts.map +0 -1
- package/esm/src/commands/videos.js +0 -266
- package/esm/src/index.d.ts +0 -28
- package/esm/src/index.d.ts.map +0 -1
- package/esm/src/index.js +0 -164
- package/esm/src/lib/auth.d.ts +0 -66
- package/esm/src/lib/auth.d.ts.map +0 -1
- package/esm/src/lib/auth.js +0 -286
- package/esm/src/lib/config.d.ts +0 -6
- package/esm/src/lib/config.d.ts.map +0 -1
- package/esm/src/lib/config.js +0 -36
- package/esm/src/lib/logger.d.ts +0 -3
- package/esm/src/lib/logger.d.ts.map +0 -1
- package/esm/src/lib/logger.js +0 -26
- package/esm/src/lib/moodle.d.ts +0 -447
- package/esm/src/lib/moodle.d.ts.map +0 -1
- package/esm/src/lib/moodle.js +0 -1353
- package/esm/src/lib/session.d.ts +0 -8
- package/esm/src/lib/session.d.ts.map +0 -1
- package/esm/src/lib/session.js +0 -42
- package/esm/src/lib/token.d.ts +0 -38
- package/esm/src/lib/token.d.ts.map +0 -1
- package/esm/src/lib/token.js +0 -178
- package/esm/src/lib/types.d.ts +0 -189
- package/esm/src/lib/types.d.ts.map +0 -1
- package/esm/src/lib/types.js +0 -2
- package/esm/src/lib/utils.d.ts +0 -52
- package/esm/src/lib/utils.d.ts.map +0 -1
- package/esm/src/lib/utils.js +0 -122
- package/script/_dnt.polyfills.d.ts +0 -101
- package/script/_dnt.polyfills.d.ts.map +0 -1
- package/script/_dnt.polyfills.js +0 -130
- package/script/_dnt.shims.d.ts +0 -6
- package/script/_dnt.shims.d.ts.map +0 -1
- package/script/_dnt.shims.js +0 -65
- package/script/deno.d.ts +0 -25
- package/script/deno.d.ts.map +0 -1
- package/script/deno.js +0 -25
- package/script/package.json +0 -3
- package/script/src/commands/announcements.d.ts +0 -3
- package/script/src/commands/announcements.d.ts.map +0 -1
- package/script/src/commands/announcements.js +0 -74
- package/script/src/commands/assignments.d.ts +0 -3
- package/script/src/commands/assignments.d.ts.map +0 -1
- package/script/src/commands/assignments.js +0 -268
- package/script/src/commands/auth.d.ts +0 -3
- package/script/src/commands/auth.d.ts.map +0 -1
- package/script/src/commands/auth.js +0 -296
- package/script/src/commands/calendar.d.ts +0 -3
- package/script/src/commands/calendar.d.ts.map +0 -1
- package/script/src/commands/calendar.js +0 -133
- package/script/src/commands/courses.d.ts +0 -3
- package/script/src/commands/courses.d.ts.map +0 -1
- package/script/src/commands/courses.js +0 -315
- package/script/src/commands/forums.d.ts +0 -3
- package/script/src/commands/forums.d.ts.map +0 -1
- package/script/src/commands/forums.js +0 -193
- package/script/src/commands/grades.d.ts +0 -3
- package/script/src/commands/grades.d.ts.map +0 -1
- package/script/src/commands/grades.js +0 -87
- package/script/src/commands/materials.d.ts +0 -3
- package/script/src/commands/materials.d.ts.map +0 -1
- package/script/src/commands/materials.js +0 -408
- package/script/src/commands/quizzes.d.ts +0 -3
- package/script/src/commands/quizzes.d.ts.map +0 -1
- package/script/src/commands/quizzes.js +0 -239
- package/script/src/commands/skills.d.ts +0 -3
- package/script/src/commands/skills.d.ts.map +0 -1
- package/script/src/commands/skills.js +0 -112
- package/script/src/commands/upload.d.ts +0 -3
- package/script/src/commands/upload.d.ts.map +0 -1
- package/script/src/commands/upload.js +0 -61
- package/script/src/commands/videos.d.ts +0 -3
- package/script/src/commands/videos.d.ts.map +0 -1
- package/script/src/commands/videos.js +0 -272
- package/script/src/index.d.ts +0 -28
- package/script/src/index.d.ts.map +0 -1
- package/script/src/index.js +0 -171
- package/script/src/lib/auth.d.ts +0 -66
- package/script/src/lib/auth.d.ts.map +0 -1
- package/script/src/lib/auth.js +0 -296
- package/script/src/lib/config.d.ts +0 -6
- package/script/src/lib/config.d.ts.map +0 -1
- package/script/src/lib/config.js +0 -42
- package/script/src/lib/logger.d.ts +0 -3
- package/script/src/lib/logger.d.ts.map +0 -1
- package/script/src/lib/logger.js +0 -29
- package/script/src/lib/moodle.d.ts +0 -447
- package/script/src/lib/moodle.d.ts.map +0 -1
- package/script/src/lib/moodle.js +0 -1425
- package/script/src/lib/session.d.ts +0 -8
- package/script/src/lib/session.d.ts.map +0 -1
- package/script/src/lib/session.js +0 -45
- package/script/src/lib/token.d.ts +0 -38
- package/script/src/lib/token.d.ts.map +0 -1
- package/script/src/lib/token.js +0 -189
- package/script/src/lib/types.d.ts +0 -189
- package/script/src/lib/types.d.ts.map +0 -1
- package/script/src/lib/types.js +0 -3
- package/script/src/lib/utils.d.ts +0 -52
- package/script/src/lib/utils.d.ts.map +0 -1
- package/script/src/lib/utils.js +0 -167
- package/skills/openape/SKILL.md +0 -115
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { getOutputFormat, formatTimestamp } from "../lib/utils.js";
|
|
2
|
-
import { getSiteInfoApi, getMessagesApi, getDiscussionPostsApi } from "../lib/moodle.js";
|
|
3
|
-
import { createApiContext } from "../lib/auth.js";
|
|
4
|
-
import { formatAndOutput } from "../index.js";
|
|
5
|
-
export function registerAnnouncementsCommand(program) {
|
|
6
|
-
const announcementsCmd = program.command("announcements");
|
|
7
|
-
announcementsCmd.description("Announcement operations");
|
|
8
|
-
announcementsCmd
|
|
9
|
-
.command("list-all")
|
|
10
|
-
.description("List all announcements across all courses")
|
|
11
|
-
.option("--level <type>", "Course level: in_progress (default) | all", "in_progress")
|
|
12
|
-
.option("--unread-only", "Show only unread announcements")
|
|
13
|
-
.option("--limit <n>", "Maximum number of announcements to show", "20")
|
|
14
|
-
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
15
|
-
.action(async (options, command) => {
|
|
16
|
-
const output = getOutputFormat(command);
|
|
17
|
-
const limit = parseInt(options.limit, 10);
|
|
18
|
-
const apiContext = await createApiContext(options, command);
|
|
19
|
-
if (!apiContext) {
|
|
20
|
-
process.exitCode = 1;
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
const siteInfo = await getSiteInfoApi(apiContext.session);
|
|
24
|
-
const messages = await getMessagesApi(apiContext.session, siteInfo.userid, {
|
|
25
|
-
limitnum: limit,
|
|
26
|
-
});
|
|
27
|
-
const allAnnouncements = messages.map(m => ({
|
|
28
|
-
course_id: 0,
|
|
29
|
-
course_name: "Notifications",
|
|
30
|
-
id: m.id,
|
|
31
|
-
subject: m.subject,
|
|
32
|
-
author: `User ${m.useridfrom}`,
|
|
33
|
-
author_id: m.useridfrom,
|
|
34
|
-
created_at: formatTimestamp(m.timecreated),
|
|
35
|
-
modified_at: formatTimestamp(m.timecreated),
|
|
36
|
-
unread: false,
|
|
37
|
-
}));
|
|
38
|
-
allAnnouncements.sort((a, b) => b.created_at > a.created_at ? 1 : -1);
|
|
39
|
-
const shown = allAnnouncements.slice(0, limit);
|
|
40
|
-
formatAndOutput(shown, output, apiContext.log, { status: "success", timestamp: new Date().toISOString(), total_announcements: allAnnouncements.length, shown: shown.length });
|
|
41
|
-
});
|
|
42
|
-
announcementsCmd
|
|
43
|
-
.command("read")
|
|
44
|
-
.description("Read a specific announcement (shows full content)")
|
|
45
|
-
.argument("<announcement-id>", "Discussion ID of the announcement")
|
|
46
|
-
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
47
|
-
.action(async (announcementId, options, command) => {
|
|
48
|
-
const output = getOutputFormat(command);
|
|
49
|
-
const apiContext = await createApiContext(options, command);
|
|
50
|
-
if (!apiContext) {
|
|
51
|
-
process.exitCode = 1;
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
const posts = await getDiscussionPostsApi(apiContext.session, parseInt(announcementId, 10));
|
|
55
|
-
if (posts.length === 0) {
|
|
56
|
-
apiContext.log.error(`Announcement not found: ${announcementId}`);
|
|
57
|
-
process.exitCode = 1;
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
const firstPost = posts[0];
|
|
61
|
-
formatAndOutput({
|
|
62
|
-
id: announcementId,
|
|
63
|
-
subject: firstPost.subject,
|
|
64
|
-
author: firstPost.author,
|
|
65
|
-
author_id: firstPost.authorId,
|
|
66
|
-
created_at: formatTimestamp(firstPost.created),
|
|
67
|
-
modified_at: formatTimestamp(firstPost.modified),
|
|
68
|
-
message: firstPost.message,
|
|
69
|
-
}, output, apiContext.log);
|
|
70
|
-
});
|
|
71
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"assignments.d.ts","sourceRoot":"","sources":["../../../src/src/commands/assignments.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAyPjE"}
|
|
@@ -1,229 +0,0 @@
|
|
|
1
|
-
import { getOutputFormat, formatFileSize, formatMoodleDate } from "../lib/utils.js";
|
|
2
|
-
import { getEnrolledCoursesApi, getAssignmentsByCoursesApi, getSubmissionStatusApi, saveSubmissionApi, uploadFileApi } from "../lib/moodle.js";
|
|
3
|
-
import { createApiContext } from "../lib/auth.js";
|
|
4
|
-
import { formatAndOutput } from "../index.js";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import fs from "node:fs/promises";
|
|
7
|
-
export function registerAssignmentsCommand(program) {
|
|
8
|
-
const assignmentsCmd = program.command("assignments");
|
|
9
|
-
assignmentsCmd.description("Assignment operations");
|
|
10
|
-
assignmentsCmd
|
|
11
|
-
.command("list")
|
|
12
|
-
.description("List assignments in a course")
|
|
13
|
-
.argument("<course-id>", "Course ID")
|
|
14
|
-
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
15
|
-
.action(async (courseId, options, command) => {
|
|
16
|
-
const output = getOutputFormat(command);
|
|
17
|
-
const apiContext = await createApiContext(options, command);
|
|
18
|
-
if (!apiContext) {
|
|
19
|
-
process.exitCode = 1;
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
const apiAssignments = await getAssignmentsByCoursesApi(apiContext.session, [parseInt(courseId, 10)]);
|
|
23
|
-
const assignments = apiAssignments.map(a => ({
|
|
24
|
-
id: a.id,
|
|
25
|
-
courseName: courseId,
|
|
26
|
-
name: a.name,
|
|
27
|
-
url: a.url,
|
|
28
|
-
cmid: a.cmid,
|
|
29
|
-
duedate: formatMoodleDate(a.duedate),
|
|
30
|
-
cutoffdate: formatMoodleDate(a.cutoffdate),
|
|
31
|
-
allowSubmissionsFromDate: formatMoodleDate(a.allowSubmissionsFromDate),
|
|
32
|
-
}));
|
|
33
|
-
apiContext.log.info(`\n找到 ${assignments.length} 個作業。`);
|
|
34
|
-
formatAndOutput(assignments, output, apiContext.log);
|
|
35
|
-
});
|
|
36
|
-
assignmentsCmd
|
|
37
|
-
.command("list-all")
|
|
38
|
-
.description("List all assignments across all courses")
|
|
39
|
-
.option("--level <type>", "Course level: in_progress (default) | all", "in_progress")
|
|
40
|
-
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
41
|
-
.action(async (options, command) => {
|
|
42
|
-
const output = getOutputFormat(command);
|
|
43
|
-
const apiContext = await createApiContext(options, command);
|
|
44
|
-
if (!apiContext) {
|
|
45
|
-
process.exitCode = 1;
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
const classification = options.level === "all" ? undefined : "inprogress";
|
|
49
|
-
const courses = await getEnrolledCoursesApi(apiContext.session, {
|
|
50
|
-
classification,
|
|
51
|
-
});
|
|
52
|
-
// Get assignments via WS API (no browser needed!)
|
|
53
|
-
const courseIds = courses.map(c => c.id);
|
|
54
|
-
const apiAssignments = await getAssignmentsByCoursesApi(apiContext.session, courseIds);
|
|
55
|
-
// Build a map of courseId -> course for quick lookup
|
|
56
|
-
const courseMap = new Map(courses.map(c => [c.id, c]));
|
|
57
|
-
const allAssignments = [];
|
|
58
|
-
for (const a of apiAssignments) {
|
|
59
|
-
const course = courseMap.get(a.courseId);
|
|
60
|
-
if (course) {
|
|
61
|
-
allAssignments.push({
|
|
62
|
-
id: a.id,
|
|
63
|
-
courseName: course.fullname,
|
|
64
|
-
name: a.name,
|
|
65
|
-
url: a.url,
|
|
66
|
-
cmid: a.cmid,
|
|
67
|
-
duedate: formatMoodleDate(a.duedate),
|
|
68
|
-
cutoffdate: formatMoodleDate(a.cutoffdate),
|
|
69
|
-
allowSubmissionsFromDate: formatMoodleDate(a.allowSubmissionsFromDate),
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
apiContext.log.info(`\n總計發現 ${allAssignments.length} 個作業。`);
|
|
74
|
-
formatAndOutput(allAssignments, output, apiContext.log);
|
|
75
|
-
});
|
|
76
|
-
// ── Submission Status ───────────────────────────────────────────────────────
|
|
77
|
-
assignmentsCmd
|
|
78
|
-
.command("status")
|
|
79
|
-
.description("Check assignment submission status")
|
|
80
|
-
.argument("<assignment-id>", "Assignment instance ID (from list-all)")
|
|
81
|
-
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
82
|
-
.action(async (assignmentId, options, command) => {
|
|
83
|
-
const output = getOutputFormat(command);
|
|
84
|
-
const apiContext = await createApiContext(options, command);
|
|
85
|
-
if (!apiContext) {
|
|
86
|
-
process.exitCode = 1;
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
const id = parseInt(assignmentId, 10);
|
|
90
|
-
apiContext.log.info("檢查繳交狀態...");
|
|
91
|
-
const status = await getSubmissionStatusApi(apiContext.session, id);
|
|
92
|
-
// Build status data object
|
|
93
|
-
const statusData = {
|
|
94
|
-
submitted: status.submitted,
|
|
95
|
-
submitted_text: status.submitted ? "已繳交" : "尚未繳交",
|
|
96
|
-
graded: status.graded,
|
|
97
|
-
graded_text: status.graded ? "已評分" : "尚未評分",
|
|
98
|
-
last_modified: status.lastModified ? new Date(status.lastModified * 1000).toISOString() : null,
|
|
99
|
-
last_modified_text: status.lastModified ? new Date(status.lastModified * 1000).toLocaleString("zh-TW") : null,
|
|
100
|
-
grader: status.grader,
|
|
101
|
-
grade: status.grade,
|
|
102
|
-
feedback: status.feedback,
|
|
103
|
-
files: status.extensions.map(f => ({
|
|
104
|
-
filename: f.filename,
|
|
105
|
-
filesize: f.filesize,
|
|
106
|
-
filesize_kb: formatFileSize(f.filesize),
|
|
107
|
-
})),
|
|
108
|
-
};
|
|
109
|
-
formatAndOutput(statusData, output, apiContext.log);
|
|
110
|
-
});
|
|
111
|
-
// ── Submit Assignment ────────────────────────────────────────────────────────
|
|
112
|
-
assignmentsCmd
|
|
113
|
-
.command("submit")
|
|
114
|
-
.description("Submit an assignment (online text or file)")
|
|
115
|
-
.argument("<assignment-id>", "Assignment instance ID (from list-all)")
|
|
116
|
-
.option("--text <content>", "Online text content to submit")
|
|
117
|
-
.option("--file-id <id>", "Draft file ID from file upload")
|
|
118
|
-
.option("--file <path>", "Upload and submit a file directly")
|
|
119
|
-
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
120
|
-
.action(async (assignmentId, options, command) => {
|
|
121
|
-
const output = getOutputFormat(command);
|
|
122
|
-
const apiContext = await createApiContext(options, command);
|
|
123
|
-
if (!apiContext) {
|
|
124
|
-
process.exitCode = 1;
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
const id = parseInt(assignmentId, 10);
|
|
128
|
-
// Check submission status first
|
|
129
|
-
const status = await getSubmissionStatusApi(apiContext.session, id);
|
|
130
|
-
let fileUploaded;
|
|
131
|
-
let cancelled = false;
|
|
132
|
-
if (status.submitted) {
|
|
133
|
-
const confirm = await promptConfirm("此作業已經繳交!確定要重新繳交嗎?(y/N): ");
|
|
134
|
-
if (!confirm) {
|
|
135
|
-
cancelled = true;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
if (cancelled) {
|
|
139
|
-
const cancelResult = {
|
|
140
|
-
success: false,
|
|
141
|
-
cancelled: true,
|
|
142
|
-
message: "Submission cancelled by user",
|
|
143
|
-
};
|
|
144
|
-
formatAndOutput(cancelResult, output, apiContext.log);
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
// Validate options
|
|
148
|
-
if (!options.text && !options.fileId && !options.file) {
|
|
149
|
-
const errorResult = {
|
|
150
|
-
success: false,
|
|
151
|
-
error: "請提供 --text、--file-id 或 --file 選項。",
|
|
152
|
-
};
|
|
153
|
-
formatAndOutput(errorResult, output, apiContext.log);
|
|
154
|
-
process.exitCode = 1;
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
let fileId = options.fileId ? parseInt(options.fileId, 10) : undefined;
|
|
158
|
-
// Upload file if --file option is provided
|
|
159
|
-
if (options.file) {
|
|
160
|
-
const resolvedPath = path.resolve(options.file);
|
|
161
|
-
let stats;
|
|
162
|
-
try {
|
|
163
|
-
stats = await fs.stat(resolvedPath);
|
|
164
|
-
}
|
|
165
|
-
catch {
|
|
166
|
-
const errorResult = {
|
|
167
|
-
success: false,
|
|
168
|
-
error: `檔案不存在: ${options.file}`,
|
|
169
|
-
};
|
|
170
|
-
formatAndOutput(errorResult, output, apiContext.log);
|
|
171
|
-
process.exitCode = 1;
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
const fileSizeKB = formatFileSize(stats.size);
|
|
175
|
-
const uploadResult = await uploadFileApi(apiContext.session, resolvedPath);
|
|
176
|
-
if (!uploadResult.success) {
|
|
177
|
-
const errorResult = {
|
|
178
|
-
success: false,
|
|
179
|
-
error: `檔案上傳失敗: ${uploadResult.error}`,
|
|
180
|
-
};
|
|
181
|
-
formatAndOutput(errorResult, output, apiContext.log);
|
|
182
|
-
process.exitCode = 1;
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
fileId = uploadResult.draftId;
|
|
186
|
-
fileUploaded = {
|
|
187
|
-
filename: path.basename(resolvedPath),
|
|
188
|
-
filesize: stats.size,
|
|
189
|
-
filesize_kb: formatFileSize(stats.size),
|
|
190
|
-
draft_id: fileId,
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
// Submit
|
|
194
|
-
const result = await saveSubmissionApi(apiContext.session, id, {
|
|
195
|
-
onlineText: options.text ? { text: options.text } : undefined,
|
|
196
|
-
fileId: fileId,
|
|
197
|
-
});
|
|
198
|
-
const submitResult = {
|
|
199
|
-
success: result.success,
|
|
200
|
-
assignment_id: id,
|
|
201
|
-
submitted: !!result.success,
|
|
202
|
-
online_text: !!options.text,
|
|
203
|
-
file_uploaded: fileUploaded,
|
|
204
|
-
file_id: fileId ?? null,
|
|
205
|
-
error: result.success ? undefined : result.error,
|
|
206
|
-
message: result.success ? "Assignment submitted successfully" : result.error,
|
|
207
|
-
};
|
|
208
|
-
formatAndOutput(submitResult, output, apiContext.log);
|
|
209
|
-
if (!result.success) {
|
|
210
|
-
process.exitCode = 1;
|
|
211
|
-
}
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
/**
|
|
215
|
-
* Prompt user for yes/no confirmation.
|
|
216
|
-
*/
|
|
217
|
-
async function promptConfirm(prompt) {
|
|
218
|
-
const readline = await import("node:readline");
|
|
219
|
-
const rl = readline.createInterface({
|
|
220
|
-
input: process.stdin,
|
|
221
|
-
output: process.stdout,
|
|
222
|
-
});
|
|
223
|
-
return new Promise((resolve) => {
|
|
224
|
-
rl.question(prompt, (answer) => {
|
|
225
|
-
rl.close();
|
|
226
|
-
resolve(/^y/i.test(answer));
|
|
227
|
-
});
|
|
228
|
-
});
|
|
229
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../../src/src/commands/auth.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAWpC,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAkStD"}
|
package/esm/src/commands/auth.js
DELETED
|
@@ -1,290 +0,0 @@
|
|
|
1
|
-
import { getBaseDir } from "../lib/utils.js";
|
|
2
|
-
import { chromium } from "playwright-core";
|
|
3
|
-
import { createLogger } from "../lib/logger.js";
|
|
4
|
-
import { findEdgePath } from "../lib/auth.js";
|
|
5
|
-
import { saveSesskey, acquireWsToken, saveWsToken, loadWsToken } from "../lib/token.js";
|
|
6
|
-
import { getSiteInfoApi } from "../lib/moodle.js";
|
|
7
|
-
import path from "node:path";
|
|
8
|
-
import fs from "node:fs";
|
|
9
|
-
export function registerCommand(program) {
|
|
10
|
-
program
|
|
11
|
-
.command("login")
|
|
12
|
-
.description("Login to iLearning manually and save session")
|
|
13
|
-
.action(async (options) => {
|
|
14
|
-
const log = createLogger(false);
|
|
15
|
-
// Determine session storage path
|
|
16
|
-
const baseDir = getBaseDir();
|
|
17
|
-
const sessionDir = path.resolve(baseDir, ".auth");
|
|
18
|
-
const sessionPath = path.resolve(sessionDir, "storage-state.json");
|
|
19
|
-
// Ensure session directory exists
|
|
20
|
-
if (!fs.existsSync(sessionDir)) {
|
|
21
|
-
fs.mkdirSync(sessionDir, { recursive: true });
|
|
22
|
-
}
|
|
23
|
-
const edgePath = findEdgePath();
|
|
24
|
-
const browser = await chromium.launch({
|
|
25
|
-
executablePath: edgePath,
|
|
26
|
-
headless: false,
|
|
27
|
-
slowMo: 0,
|
|
28
|
-
});
|
|
29
|
-
let context;
|
|
30
|
-
let page;
|
|
31
|
-
if (fs.existsSync(sessionPath)) {
|
|
32
|
-
log.info(`找到已有 session: ${sessionPath}`);
|
|
33
|
-
log.info("正在驗證 session...");
|
|
34
|
-
try {
|
|
35
|
-
context = await browser.newContext({ storageState: sessionPath });
|
|
36
|
-
page = await context.newPage();
|
|
37
|
-
await page.goto("https://ilearning.cycu.edu.tw/my/", {
|
|
38
|
-
waitUntil: "domcontentloaded",
|
|
39
|
-
timeout: 15000,
|
|
40
|
-
});
|
|
41
|
-
const url = page.url();
|
|
42
|
-
if (url.includes("login") || url.includes("microsoftonline")) {
|
|
43
|
-
log.warn("Session 已過期,請重新登入。");
|
|
44
|
-
await context.close();
|
|
45
|
-
context = await browser.newContext();
|
|
46
|
-
page = await context.newPage();
|
|
47
|
-
await page.goto("https://ilearning.cycu.edu.tw/login/index.php", {
|
|
48
|
-
waitUntil: "domcontentloaded",
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
else {
|
|
52
|
-
// Session is still valid, close browser and exit
|
|
53
|
-
try {
|
|
54
|
-
if (context)
|
|
55
|
-
await context.close().catch(() => { });
|
|
56
|
-
}
|
|
57
|
-
catch { }
|
|
58
|
-
try {
|
|
59
|
-
await browser.close().catch(() => { });
|
|
60
|
-
}
|
|
61
|
-
catch { }
|
|
62
|
-
// Wait a bit for browser to fully close
|
|
63
|
-
await new Promise(resolve => setTimeout(resolve, 500));
|
|
64
|
-
const result = {
|
|
65
|
-
status: "success",
|
|
66
|
-
message: "Session still valid",
|
|
67
|
-
session_path: sessionPath,
|
|
68
|
-
updated: false
|
|
69
|
-
};
|
|
70
|
-
console.log(JSON.stringify(result));
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
catch {
|
|
75
|
-
log.warn("無法恢復 session,請重新登入。");
|
|
76
|
-
// context might not have been initialized if the error occurred during newContext
|
|
77
|
-
if (context) {
|
|
78
|
-
await context.close();
|
|
79
|
-
}
|
|
80
|
-
context = await browser.newContext();
|
|
81
|
-
page = await context.newPage();
|
|
82
|
-
await page.goto("https://ilearning.cycu.edu.tw/login/index.php", {
|
|
83
|
-
waitUntil: "domcontentloaded",
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
else {
|
|
88
|
-
log.info("首次登入,請在瀏覽器中完成登入流程。");
|
|
89
|
-
context = await browser.newContext();
|
|
90
|
-
page = await context.newPage();
|
|
91
|
-
await page.goto("https://ilearning.cycu.edu.tw/login/index.php", {
|
|
92
|
-
waitUntil: "domcontentloaded",
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
log.info("\n請在瀏覽器中完成登入,登入成功後將自動儲存 session...\n");
|
|
96
|
-
try {
|
|
97
|
-
const startTime = Date.now();
|
|
98
|
-
const timeout = 300000;
|
|
99
|
-
let loggedIn = false;
|
|
100
|
-
while (Date.now() - startTime < timeout) {
|
|
101
|
-
await page.waitForTimeout(1000);
|
|
102
|
-
const currentUrl = page.url();
|
|
103
|
-
if (currentUrl.includes("ilearning.cycu.edu.tw") &&
|
|
104
|
-
!currentUrl.includes("login") &&
|
|
105
|
-
!currentUrl.includes("microsoftonline")) {
|
|
106
|
-
await page.waitForTimeout(2000);
|
|
107
|
-
const finalUrl = page.url();
|
|
108
|
-
if (finalUrl.includes("ilearning.cycu.edu.tw") &&
|
|
109
|
-
!finalUrl.includes("login") &&
|
|
110
|
-
!finalUrl.includes("microsoftonline")) {
|
|
111
|
-
loggedIn = true;
|
|
112
|
-
break;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
if (loggedIn) {
|
|
117
|
-
await context.storageState({ path: sessionPath });
|
|
118
|
-
// Extract and save sesskey for faster API calls
|
|
119
|
-
try {
|
|
120
|
-
// Navigate to a page with M.cfg first
|
|
121
|
-
await page.goto("https://ilearning.cycu.edu.tw/my/", { waitUntil: "domcontentloaded" });
|
|
122
|
-
// Use Function constructor to avoid dnt transforming globalThis
|
|
123
|
-
const sesskey = await page.evaluate(() => self.M?.cfg?.sesskey ?? null);
|
|
124
|
-
if (sesskey) {
|
|
125
|
-
saveSesskey(sessionPath, sesskey);
|
|
126
|
-
log.debug(`Saved sesskey: ${sesskey}`);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
catch {
|
|
130
|
-
// Ignore sesskey extraction errors
|
|
131
|
-
}
|
|
132
|
-
// Acquire WS token
|
|
133
|
-
let wsToken;
|
|
134
|
-
try {
|
|
135
|
-
wsToken = await acquireWsToken(page, { moodleBaseUrl: "https://ilearning.cycu.edu.tw" }, log);
|
|
136
|
-
saveWsToken(sessionPath, wsToken);
|
|
137
|
-
}
|
|
138
|
-
catch {
|
|
139
|
-
// WS token is optional, ignore errors
|
|
140
|
-
}
|
|
141
|
-
const stats = fs.statSync(sessionPath);
|
|
142
|
-
// Get user info via WS API
|
|
143
|
-
let user;
|
|
144
|
-
try {
|
|
145
|
-
if (wsToken) {
|
|
146
|
-
const siteInfo = await getSiteInfoApi({
|
|
147
|
-
wsToken,
|
|
148
|
-
moodleBaseUrl: "https://ilearning.cycu.edu.tw",
|
|
149
|
-
});
|
|
150
|
-
user = {
|
|
151
|
-
userid: siteInfo.userid,
|
|
152
|
-
username: siteInfo.username,
|
|
153
|
-
fullname: siteInfo.fullname,
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
catch {
|
|
158
|
-
// Ignore
|
|
159
|
-
}
|
|
160
|
-
const result = {
|
|
161
|
-
status: "success",
|
|
162
|
-
message: "Login successful",
|
|
163
|
-
session_path: sessionPath,
|
|
164
|
-
session_size: stats.size,
|
|
165
|
-
updated: true,
|
|
166
|
-
...(user ? { user } : {}),
|
|
167
|
-
};
|
|
168
|
-
console.log(JSON.stringify(result, null, 2));
|
|
169
|
-
}
|
|
170
|
-
else {
|
|
171
|
-
throw new Error("TimeoutError");
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
catch (err) {
|
|
175
|
-
const errorResult = {
|
|
176
|
-
status: "error",
|
|
177
|
-
error: err instanceof Error ? err.message : String(err),
|
|
178
|
-
session_path: sessionPath
|
|
179
|
-
};
|
|
180
|
-
console.log(JSON.stringify(errorResult));
|
|
181
|
-
}
|
|
182
|
-
finally {
|
|
183
|
-
// Safely close browser with error handling
|
|
184
|
-
try {
|
|
185
|
-
if (context)
|
|
186
|
-
await context.close().catch(() => { });
|
|
187
|
-
}
|
|
188
|
-
catch { }
|
|
189
|
-
try {
|
|
190
|
-
await browser.close().catch(() => { });
|
|
191
|
-
}
|
|
192
|
-
catch { }
|
|
193
|
-
// Wait for browser process to fully terminate
|
|
194
|
-
await new Promise(resolve => setTimeout(resolve, 500));
|
|
195
|
-
}
|
|
196
|
-
});
|
|
197
|
-
program
|
|
198
|
-
.command("status")
|
|
199
|
-
.description("Check session status")
|
|
200
|
-
.option("--session <path>", "Session file path", ".auth/storage-state.json")
|
|
201
|
-
.action(async (options) => {
|
|
202
|
-
const baseDir = getBaseDir();
|
|
203
|
-
const sessionPath = path.resolve(baseDir, options.session);
|
|
204
|
-
if (fs.existsSync(sessionPath)) {
|
|
205
|
-
const stats = fs.statSync(sessionPath);
|
|
206
|
-
// Try to read and validate the session
|
|
207
|
-
try {
|
|
208
|
-
const content = fs.readFileSync(sessionPath, "utf8");
|
|
209
|
-
const state = JSON.parse(content);
|
|
210
|
-
const cookies = state.cookies || [];
|
|
211
|
-
const moodleSession = cookies.find((c) => c.name === "MoodleSession");
|
|
212
|
-
const result = {
|
|
213
|
-
status: "success",
|
|
214
|
-
session_path: sessionPath,
|
|
215
|
-
exists: true,
|
|
216
|
-
modified: new Date(stats.mtime).toISOString(),
|
|
217
|
-
size: stats.size,
|
|
218
|
-
moodle_session: moodleSession ? {
|
|
219
|
-
exists: true,
|
|
220
|
-
expires: new Date(moodleSession.expires * 1000).toISOString()
|
|
221
|
-
} : {
|
|
222
|
-
exists: false
|
|
223
|
-
}
|
|
224
|
-
};
|
|
225
|
-
// Try to get user info from WS API
|
|
226
|
-
try {
|
|
227
|
-
const wsToken = loadWsToken(sessionPath);
|
|
228
|
-
if (wsToken) {
|
|
229
|
-
const session = {
|
|
230
|
-
wsToken,
|
|
231
|
-
moodleBaseUrl: "https://ilearning.cycu.edu.tw"
|
|
232
|
-
};
|
|
233
|
-
const siteInfo = await getSiteInfoApi(session);
|
|
234
|
-
result.user = {
|
|
235
|
-
userid: siteInfo.userid,
|
|
236
|
-
username: siteInfo.username,
|
|
237
|
-
fullname: siteInfo.fullname
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
catch {
|
|
242
|
-
// WS token might not be available or expired, skip user info
|
|
243
|
-
}
|
|
244
|
-
console.log(JSON.stringify(result, null, 2));
|
|
245
|
-
}
|
|
246
|
-
catch {
|
|
247
|
-
const result = {
|
|
248
|
-
status: "error",
|
|
249
|
-
error: "Session file is corrupted",
|
|
250
|
-
session_path: sessionPath
|
|
251
|
-
};
|
|
252
|
-
console.log(JSON.stringify(result, null, 2));
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
else {
|
|
256
|
-
const result = {
|
|
257
|
-
status: "error",
|
|
258
|
-
error: "Session not found",
|
|
259
|
-
session_path: sessionPath,
|
|
260
|
-
hint: "Run 'openape login' first"
|
|
261
|
-
};
|
|
262
|
-
console.log(JSON.stringify(result, null, 2));
|
|
263
|
-
}
|
|
264
|
-
});
|
|
265
|
-
program
|
|
266
|
-
.command("logout")
|
|
267
|
-
.description("Remove saved session")
|
|
268
|
-
.option("--session <path>", "Session file path", ".auth/storage-state.json")
|
|
269
|
-
.action(async (options) => {
|
|
270
|
-
const baseDir = getBaseDir();
|
|
271
|
-
const sessionPath = path.resolve(baseDir, options.session);
|
|
272
|
-
if (fs.existsSync(sessionPath)) {
|
|
273
|
-
fs.unlinkSync(sessionPath);
|
|
274
|
-
const result = {
|
|
275
|
-
status: "success",
|
|
276
|
-
message: "Session removed",
|
|
277
|
-
session_path: sessionPath
|
|
278
|
-
};
|
|
279
|
-
console.log(JSON.stringify(result, null, 2));
|
|
280
|
-
}
|
|
281
|
-
else {
|
|
282
|
-
const result = {
|
|
283
|
-
status: "error",
|
|
284
|
-
error: "Session not found",
|
|
285
|
-
session_path: sessionPath
|
|
286
|
-
};
|
|
287
|
-
console.log(JSON.stringify(result, null, 2));
|
|
288
|
-
}
|
|
289
|
-
});
|
|
290
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"calendar.d.ts","sourceRoot":"","sources":["../../../src/src/commands/calendar.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAOpC,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAqJ9D"}
|