@mo7yw4ng/openape 1.0.5 → 2.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 (152) hide show
  1. package/bin/openape +29 -0
  2. package/bin/openape.js +29 -0
  3. package/package.json +22 -28
  4. package/LICENSE +0 -21
  5. package/README.md +0 -135
  6. package/esm/_dnt.polyfills.d.ts +0 -101
  7. package/esm/_dnt.polyfills.d.ts.map +0 -1
  8. package/esm/_dnt.polyfills.js +0 -127
  9. package/esm/_dnt.shims.d.ts +0 -6
  10. package/esm/_dnt.shims.d.ts.map +0 -1
  11. package/esm/_dnt.shims.js +0 -61
  12. package/esm/deno.d.ts +0 -25
  13. package/esm/deno.d.ts.map +0 -1
  14. package/esm/deno.js +0 -23
  15. package/esm/package.json +0 -3
  16. package/esm/src/commands/announcements.d.ts +0 -3
  17. package/esm/src/commands/announcements.d.ts.map +0 -1
  18. package/esm/src/commands/announcements.js +0 -134
  19. package/esm/src/commands/assignments.d.ts +0 -3
  20. package/esm/src/commands/assignments.d.ts.map +0 -1
  21. package/esm/src/commands/assignments.js +0 -230
  22. package/esm/src/commands/auth.d.ts +0 -3
  23. package/esm/src/commands/auth.d.ts.map +0 -1
  24. package/esm/src/commands/auth.js +0 -290
  25. package/esm/src/commands/calendar.d.ts +0 -3
  26. package/esm/src/commands/calendar.d.ts.map +0 -1
  27. package/esm/src/commands/calendar.js +0 -179
  28. package/esm/src/commands/courses.d.ts +0 -3
  29. package/esm/src/commands/courses.d.ts.map +0 -1
  30. package/esm/src/commands/courses.js +0 -348
  31. package/esm/src/commands/forums.d.ts +0 -3
  32. package/esm/src/commands/forums.d.ts.map +0 -1
  33. package/esm/src/commands/forums.js +0 -318
  34. package/esm/src/commands/grades.d.ts +0 -3
  35. package/esm/src/commands/grades.d.ts.map +0 -1
  36. package/esm/src/commands/grades.js +0 -121
  37. package/esm/src/commands/materials.d.ts +0 -3
  38. package/esm/src/commands/materials.d.ts.map +0 -1
  39. package/esm/src/commands/materials.js +0 -413
  40. package/esm/src/commands/quizzes.d.ts +0 -3
  41. package/esm/src/commands/quizzes.d.ts.map +0 -1
  42. package/esm/src/commands/quizzes.js +0 -271
  43. package/esm/src/commands/skills.d.ts +0 -3
  44. package/esm/src/commands/skills.d.ts.map +0 -1
  45. package/esm/src/commands/skills.js +0 -106
  46. package/esm/src/commands/upload.d.ts +0 -3
  47. package/esm/src/commands/upload.d.ts.map +0 -1
  48. package/esm/src/commands/upload.js +0 -58
  49. package/esm/src/commands/videos.d.ts +0 -3
  50. package/esm/src/commands/videos.d.ts.map +0 -1
  51. package/esm/src/commands/videos.js +0 -336
  52. package/esm/src/index.d.ts +0 -27
  53. package/esm/src/index.d.ts.map +0 -1
  54. package/esm/src/index.js +0 -160
  55. package/esm/src/lib/auth.d.ts +0 -47
  56. package/esm/src/lib/auth.d.ts.map +0 -1
  57. package/esm/src/lib/auth.js +0 -227
  58. package/esm/src/lib/config.d.ts +0 -6
  59. package/esm/src/lib/config.d.ts.map +0 -1
  60. package/esm/src/lib/config.js +0 -36
  61. package/esm/src/lib/logger.d.ts +0 -3
  62. package/esm/src/lib/logger.d.ts.map +0 -1
  63. package/esm/src/lib/logger.js +0 -27
  64. package/esm/src/lib/moodle.d.ts +0 -433
  65. package/esm/src/lib/moodle.d.ts.map +0 -1
  66. package/esm/src/lib/moodle.js +0 -1318
  67. package/esm/src/lib/session.d.ts +0 -8
  68. package/esm/src/lib/session.d.ts.map +0 -1
  69. package/esm/src/lib/session.js +0 -42
  70. package/esm/src/lib/token.d.ts +0 -38
  71. package/esm/src/lib/token.d.ts.map +0 -1
  72. package/esm/src/lib/token.js +0 -178
  73. package/esm/src/lib/types.d.ts +0 -189
  74. package/esm/src/lib/types.d.ts.map +0 -1
  75. package/esm/src/lib/types.js +0 -2
  76. package/esm/src/lib/utils.d.ts +0 -57
  77. package/esm/src/lib/utils.d.ts.map +0 -1
  78. package/esm/src/lib/utils.js +0 -129
  79. package/script/_dnt.polyfills.d.ts +0 -101
  80. package/script/_dnt.polyfills.d.ts.map +0 -1
  81. package/script/_dnt.polyfills.js +0 -130
  82. package/script/_dnt.shims.d.ts +0 -6
  83. package/script/_dnt.shims.d.ts.map +0 -1
  84. package/script/_dnt.shims.js +0 -65
  85. package/script/deno.d.ts +0 -25
  86. package/script/deno.d.ts.map +0 -1
  87. package/script/deno.js +0 -25
  88. package/script/package.json +0 -3
  89. package/script/src/commands/announcements.d.ts +0 -3
  90. package/script/src/commands/announcements.d.ts.map +0 -1
  91. package/script/src/commands/announcements.js +0 -140
  92. package/script/src/commands/assignments.d.ts +0 -3
  93. package/script/src/commands/assignments.d.ts.map +0 -1
  94. package/script/src/commands/assignments.js +0 -269
  95. package/script/src/commands/auth.d.ts +0 -3
  96. package/script/src/commands/auth.d.ts.map +0 -1
  97. package/script/src/commands/auth.js +0 -296
  98. package/script/src/commands/calendar.d.ts +0 -3
  99. package/script/src/commands/calendar.d.ts.map +0 -1
  100. package/script/src/commands/calendar.js +0 -185
  101. package/script/src/commands/courses.d.ts +0 -3
  102. package/script/src/commands/courses.d.ts.map +0 -1
  103. package/script/src/commands/courses.js +0 -354
  104. package/script/src/commands/forums.d.ts +0 -3
  105. package/script/src/commands/forums.d.ts.map +0 -1
  106. package/script/src/commands/forums.js +0 -324
  107. package/script/src/commands/grades.d.ts +0 -3
  108. package/script/src/commands/grades.d.ts.map +0 -1
  109. package/script/src/commands/grades.js +0 -127
  110. package/script/src/commands/materials.d.ts +0 -3
  111. package/script/src/commands/materials.d.ts.map +0 -1
  112. package/script/src/commands/materials.js +0 -419
  113. package/script/src/commands/quizzes.d.ts +0 -3
  114. package/script/src/commands/quizzes.d.ts.map +0 -1
  115. package/script/src/commands/quizzes.js +0 -277
  116. package/script/src/commands/skills.d.ts +0 -3
  117. package/script/src/commands/skills.d.ts.map +0 -1
  118. package/script/src/commands/skills.js +0 -112
  119. package/script/src/commands/upload.d.ts +0 -3
  120. package/script/src/commands/upload.d.ts.map +0 -1
  121. package/script/src/commands/upload.js +0 -64
  122. package/script/src/commands/videos.d.ts +0 -3
  123. package/script/src/commands/videos.d.ts.map +0 -1
  124. package/script/src/commands/videos.js +0 -342
  125. package/script/src/index.d.ts +0 -27
  126. package/script/src/index.d.ts.map +0 -1
  127. package/script/src/index.js +0 -167
  128. package/script/src/lib/auth.d.ts +0 -47
  129. package/script/src/lib/auth.d.ts.map +0 -1
  130. package/script/src/lib/auth.js +0 -269
  131. package/script/src/lib/config.d.ts +0 -6
  132. package/script/src/lib/config.d.ts.map +0 -1
  133. package/script/src/lib/config.js +0 -42
  134. package/script/src/lib/logger.d.ts +0 -3
  135. package/script/src/lib/logger.d.ts.map +0 -1
  136. package/script/src/lib/logger.js +0 -30
  137. package/script/src/lib/moodle.d.ts +0 -433
  138. package/script/src/lib/moodle.d.ts.map +0 -1
  139. package/script/src/lib/moodle.js +0 -1389
  140. package/script/src/lib/session.d.ts +0 -8
  141. package/script/src/lib/session.d.ts.map +0 -1
  142. package/script/src/lib/session.js +0 -45
  143. package/script/src/lib/token.d.ts +0 -38
  144. package/script/src/lib/token.d.ts.map +0 -1
  145. package/script/src/lib/token.js +0 -189
  146. package/script/src/lib/types.d.ts +0 -189
  147. package/script/src/lib/types.d.ts.map +0 -1
  148. package/script/src/lib/types.js +0 -3
  149. package/script/src/lib/utils.d.ts +0 -57
  150. package/script/src/lib/utils.d.ts.map +0 -1
  151. package/script/src/lib/utils.js +0 -175
  152. package/skills/openape/SKILL.md +0 -115
@@ -1,134 +0,0 @@
1
- import { getBaseDir, formatTimestamp } from "../lib/utils.js";
2
- import { getSiteInfoApi, getMessagesApi, getDiscussionPostsApi } from "../lib/moodle.js";
3
- import { createLogger } from "../lib/logger.js";
4
- import { loadWsToken } from "../lib/token.js";
5
- import path from "node:path";
6
- import fs from "node:fs";
7
- export function registerAnnouncementsCommand(program) {
8
- const announcementsCmd = program.command("announcements");
9
- announcementsCmd.description("Announcement operations");
10
- // Helper to get output format from global or local options
11
- function getOutputFormat(command) {
12
- const opts = command.optsWithGlobals();
13
- return opts.output || "json";
14
- }
15
- // Pure API context - no browser required (fast!)
16
- async function createApiContext(options, command) {
17
- const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
18
- const outputFormat = getOutputFormat(command || { optsWithGlobals: () => ({ output: "json" }) });
19
- const silent = outputFormat === "json" && !opts.verbose;
20
- const log = createLogger(opts.verbose, silent, outputFormat);
21
- const baseDir = getBaseDir();
22
- const sessionPath = path.resolve(baseDir, ".auth", "storage-state.json");
23
- // Check if session exists
24
- if (!fs.existsSync(sessionPath)) {
25
- console.error("未找到登入 session。請先執行 'openape login' 進行登入。");
26
- log.info(`Session 預期位置: ${sessionPath}`);
27
- return null;
28
- }
29
- // Try to load WS token
30
- const wsToken = loadWsToken(sessionPath);
31
- if (!wsToken) {
32
- console.error("未找到 WS token。請先執行 'openape login' 進行登入。");
33
- return null;
34
- }
35
- return {
36
- log,
37
- session: {
38
- wsToken,
39
- moodleBaseUrl: "https://ilearning.cycu.edu.tw",
40
- },
41
- };
42
- }
43
- announcementsCmd
44
- .command("list-all")
45
- .description("List all announcements across all courses")
46
- .option("--level <type>", "Course level: in_progress (default) | all", "in_progress")
47
- .option("--unread-only", "Show only unread announcements")
48
- .option("--limit <n>", "Maximum number of announcements to show", "20")
49
- .option("--output <format>", "Output format: json|csv|table|silent")
50
- .action(async (options, command) => {
51
- const limit = parseInt(options.limit, 10);
52
- const apiContext = await createApiContext(options, command);
53
- if (!apiContext) {
54
- process.exitCode = 1;
55
- return;
56
- }
57
- // Get site info to retrieve userid
58
- const siteInfo = await getSiteInfoApi(apiContext.session);
59
- // Get messages for the current user
60
- const messages = await getMessagesApi(apiContext.session, siteInfo.userid, {
61
- limitnum: limit,
62
- });
63
- // Convert messages to announcement format
64
- const allAnnouncements = messages.map(m => ({
65
- course_id: 0, // Messages don't have courseId
66
- course_name: "Notifications",
67
- id: m.id,
68
- subject: m.subject,
69
- author: `User ${m.useridfrom}`,
70
- authorId: m.useridfrom,
71
- createdAt: m.timecreated,
72
- modifiedAt: m.timecreated,
73
- unread: false, // Messages API doesn't provide unread status
74
- forumId: 0,
75
- }));
76
- // Sort by created date (newest first)
77
- allAnnouncements.sort((a, b) => b.createdAt - a.createdAt);
78
- // Apply limit
79
- let filteredAnnouncements = allAnnouncements.slice(0, limit);
80
- console.log(JSON.stringify({
81
- status: "success",
82
- timestamp: new Date().toISOString(),
83
- level: options.level,
84
- total_announcements: allAnnouncements.length,
85
- shown: filteredAnnouncements.length,
86
- }));
87
- for (const a of filteredAnnouncements) {
88
- console.log(JSON.stringify({
89
- course_id: a.course_id,
90
- course_name: a.course_name,
91
- id: a.id,
92
- subject: a.subject,
93
- author: a.author,
94
- author_id: a.authorId,
95
- created_at: formatTimestamp(a.createdAt),
96
- modified_at: formatTimestamp(a.modifiedAt),
97
- unread: a.unread,
98
- }));
99
- }
100
- });
101
- announcementsCmd
102
- .command("read")
103
- .description("Read a specific announcement (shows full content)")
104
- .argument("<announcement-id>", "Discussion ID of the announcement")
105
- .option("--output <format>", "Output format: json|csv|table|silent")
106
- .action(async (announcementId, options, command) => {
107
- const apiContext = await createApiContext(options, command);
108
- if (!apiContext) {
109
- process.exitCode = 1;
110
- return;
111
- }
112
- const posts = await getDiscussionPostsApi(apiContext.session, parseInt(announcementId, 10));
113
- if (posts.length === 0) {
114
- apiContext.log.error(`Announcement not found: ${announcementId}`);
115
- process.exitCode = 1;
116
- return;
117
- }
118
- const firstPost = posts[0];
119
- const output = {
120
- status: "success",
121
- timestamp: new Date().toISOString(),
122
- announcement: {
123
- id: announcementId,
124
- subject: firstPost.subject,
125
- author: firstPost.author,
126
- author_id: firstPost.authorId,
127
- created_at: formatTimestamp(firstPost.created),
128
- modified_at: formatTimestamp(firstPost.modified),
129
- message: firstPost.message,
130
- },
131
- };
132
- console.log(JSON.stringify(output));
133
- });
134
- }
@@ -1,3 +0,0 @@
1
- import { Command } from "commander";
2
- export declare function registerAssignmentsCommand(program: Command): void;
3
- //# sourceMappingURL=assignments.d.ts.map
@@ -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,CA0PjE"}
@@ -1,230 +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
- // Check if file exists
162
- try {
163
- await fs.access(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 stats = await fs.stat(resolvedPath);
175
- const fileSizeKB = formatFileSize(stats.size);
176
- const uploadResult = await uploadFileApi(apiContext.session, resolvedPath);
177
- if (!uploadResult.success) {
178
- const errorResult = {
179
- success: false,
180
- error: `檔案上傳失敗: ${uploadResult.error}`,
181
- };
182
- formatAndOutput(errorResult, output, apiContext.log);
183
- process.exitCode = 1;
184
- return;
185
- }
186
- fileId = uploadResult.draftId;
187
- fileUploaded = {
188
- filename: path.basename(resolvedPath),
189
- filesize: stats.size,
190
- filesize_kb: formatFileSize(stats.size),
191
- draft_id: fileId,
192
- };
193
- }
194
- // Submit
195
- const result = await saveSubmissionApi(apiContext.session, id, {
196
- onlineText: options.text ? { text: options.text } : undefined,
197
- fileId: fileId,
198
- });
199
- const submitResult = {
200
- success: result.success,
201
- assignment_id: id,
202
- submitted: !!result.success,
203
- online_text: !!options.text,
204
- file_uploaded: fileUploaded,
205
- file_id: fileId ?? null,
206
- error: result.success ? undefined : result.error,
207
- message: result.success ? "Assignment submitted successfully" : result.error,
208
- };
209
- formatAndOutput(submitResult, output, apiContext.log);
210
- if (!result.success) {
211
- process.exitCode = 1;
212
- }
213
- });
214
- }
215
- /**
216
- * Prompt user for yes/no confirmation.
217
- */
218
- async function promptConfirm(prompt) {
219
- const readline = await import("node:readline");
220
- const rl = readline.createInterface({
221
- input: process.stdin,
222
- output: process.stdout,
223
- });
224
- return new Promise((resolve) => {
225
- rl.question(prompt, (answer) => {
226
- rl.close();
227
- resolve(/^y/i.test(answer));
228
- });
229
- });
230
- }
@@ -1,3 +0,0 @@
1
- import { Command } from "commander";
2
- export declare function registerCommand(program: Command): void;
3
- //# sourceMappingURL=auth.d.ts.map
@@ -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"}
@@ -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,3 +0,0 @@
1
- import { Command } from "commander";
2
- export declare function registerCalendarCommand(program: Command): void;
3
- //# sourceMappingURL=calendar.d.ts.map
@@ -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;AAQpC,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAmM9D"}