@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/README.md
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
<h1 align="center">
|
|
2
2
|
<img src="assets/logo.svg" width="150" /><br/>
|
|
3
|
-
OpenApe CLI (Unofficial)
|
|
3
|
+
OpenApe CLI (Unofficial)<br/>
|
|
4
|
+
<a href="https://www.npmjs.com/package/@mo7yw4ng/openape"><img src="https://img.shields.io/npm/v/@mo7yw4ng/openape" alt="npm version" /></a>
|
|
5
|
+
<a href="https://jsr.io/@openape/openape"><img src="https://jsr.io/badges/@openape/openape" alt="JSR" /></a>
|
|
6
|
+
<a href="https://www.npmjs.com/package/@mo7yw4ng/openape"><img src="https://img.shields.io/npm/dm/@mo7yw4ng/openape" alt="npm downloads" /></a>
|
|
4
7
|
</h1>
|
|
5
8
|
|
|
6
9
|
中原大學 [i-Learning](https://ilearning.cycu.edu.tw/) (Moodle) 平台自動化命令列工具 (CLI),幫助你快速查詢課程、觀看影片、下載教材。
|
|
@@ -53,8 +56,8 @@ openape courses syllabus <id> # 顯示課程大綱
|
|
|
53
56
|
```bash
|
|
54
57
|
openape videos list <course-id> # 列出課程中的影片
|
|
55
58
|
openape videos complete <id> # 標記特定影片為已觀看
|
|
56
|
-
openape videos complete-all
|
|
57
|
-
openape videos download <id> # 下載影片
|
|
59
|
+
openape videos complete-all # 影片批次完成
|
|
60
|
+
openape videos download <id> # 下載影片 (支援 --output-dir)
|
|
58
61
|
```
|
|
59
62
|
|
|
60
63
|
### 測驗與教材 (Quizzes & Materials)
|
|
@@ -65,8 +68,8 @@ openape quizzes start <quiz-id> # 開始測驗
|
|
|
65
68
|
openape quizzes info <attempt-id> # 查看測驗題目
|
|
66
69
|
openape quizzes save <attempt-id> '<answers>' # 儲存測驗答案
|
|
67
70
|
openape materials list-all # 列出所有可下載教材
|
|
68
|
-
openape materials download <id> # 下載指定教材
|
|
69
|
-
openape materials download-all # 批次下載教材
|
|
71
|
+
openape materials download <id> # 下載指定教材 (支援 --output-dir)
|
|
72
|
+
openape materials download-all # 批次下載教材 (支援 --output-dir, --level)
|
|
70
73
|
openape materials complete <id> # 標記教材為已完成
|
|
71
74
|
openape materials complete-all # 批次標記教材為已完成
|
|
72
75
|
```
|
|
@@ -79,13 +82,13 @@ openape forums list # 列出進行中課程的討論區
|
|
|
79
82
|
openape forums list-all # 列出所有討論區
|
|
80
83
|
openape forums discussions <forum-id> # 列出討論區中的討論串
|
|
81
84
|
openape forums posts <discussion-id> # 列出討論串中的貼文
|
|
82
|
-
openape forums reply <post-id> <subject> <message> # 回覆貼文
|
|
85
|
+
openape forums reply <post-id> <subject> <message> # 回覆貼文 (支援 --attachment-id)
|
|
83
86
|
openape forums post <forum-id> <subject> <message> # 發起新討論
|
|
84
87
|
openape forums delete <post-id> # 刪除討論貼文
|
|
85
88
|
openape announcements list-all # 列出所有公告
|
|
86
89
|
openape announcements read <id> # 閱讀特定公告
|
|
87
|
-
openape calendar events # 查詢行事曆事件
|
|
88
|
-
openape calendar export # 匯出事件
|
|
90
|
+
openape calendar events # 查詢行事曆事件 (支援 --upcoming, --days, --course)
|
|
91
|
+
openape calendar export # 匯出事件 (支援 --output, --days)
|
|
89
92
|
```
|
|
90
93
|
|
|
91
94
|
### 作業與檔案上傳 (Assignments & Upload)
|
package/esm/deno.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"announcements.d.ts","sourceRoot":"","sources":["../../../src/src/commands/announcements.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"announcements.d.ts","sourceRoot":"","sources":["../../../src/src/commands/announcements.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAMpC,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAuFnE"}
|
|
@@ -1,45 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getOutputFormat, formatTimestamp } from "../lib/utils.js";
|
|
2
2
|
import { getSiteInfoApi, getMessagesApi, getDiscussionPostsApi } from "../lib/moodle.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import fs from "node:fs";
|
|
3
|
+
import { createApiContext } from "../lib/auth.js";
|
|
4
|
+
import { formatAndOutput } from "../index.js";
|
|
7
5
|
export function registerAnnouncementsCommand(program) {
|
|
8
6
|
const announcementsCmd = program.command("announcements");
|
|
9
7
|
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);
|
|
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
|
-
log.error("未找到登入 session。請先執行 'openape auth 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
|
-
log.error("未找到 WS token。請先執行 'openape auth login' 進行登入。");
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
return {
|
|
36
|
-
log,
|
|
37
|
-
session: {
|
|
38
|
-
wsToken,
|
|
39
|
-
moodleBaseUrl: "https://ilearning.cycu.edu.tw",
|
|
40
|
-
},
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
8
|
announcementsCmd
|
|
44
9
|
.command("list-all")
|
|
45
10
|
.description("List all announcements across all courses")
|
|
@@ -48,55 +13,31 @@ export function registerAnnouncementsCommand(program) {
|
|
|
48
13
|
.option("--limit <n>", "Maximum number of announcements to show", "20")
|
|
49
14
|
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
50
15
|
.action(async (options, command) => {
|
|
16
|
+
const output = getOutputFormat(command);
|
|
51
17
|
const limit = parseInt(options.limit, 10);
|
|
52
18
|
const apiContext = await createApiContext(options, command);
|
|
53
19
|
if (!apiContext) {
|
|
54
20
|
process.exitCode = 1;
|
|
55
21
|
return;
|
|
56
22
|
}
|
|
57
|
-
// Get site info to retrieve userid
|
|
58
23
|
const siteInfo = await getSiteInfoApi(apiContext.session);
|
|
59
|
-
// Get messages for the current user
|
|
60
24
|
const messages = await getMessagesApi(apiContext.session, siteInfo.userid, {
|
|
61
25
|
limitnum: limit,
|
|
62
26
|
});
|
|
63
|
-
// Convert messages to announcement format
|
|
64
27
|
const allAnnouncements = messages.map(m => ({
|
|
65
|
-
course_id: 0,
|
|
28
|
+
course_id: 0,
|
|
66
29
|
course_name: "Notifications",
|
|
67
30
|
id: m.id,
|
|
68
31
|
subject: m.subject,
|
|
69
32
|
author: `User ${m.useridfrom}`,
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
unread: false,
|
|
74
|
-
forumId: 0,
|
|
33
|
+
author_id: m.useridfrom,
|
|
34
|
+
created_at: formatTimestamp(m.timecreated),
|
|
35
|
+
modified_at: formatTimestamp(m.timecreated),
|
|
36
|
+
unread: false,
|
|
75
37
|
}));
|
|
76
|
-
|
|
77
|
-
allAnnouncements.
|
|
78
|
-
|
|
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
|
-
}
|
|
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 });
|
|
100
41
|
});
|
|
101
42
|
announcementsCmd
|
|
102
43
|
.command("read")
|
|
@@ -104,6 +45,7 @@ export function registerAnnouncementsCommand(program) {
|
|
|
104
45
|
.argument("<announcement-id>", "Discussion ID of the announcement")
|
|
105
46
|
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
106
47
|
.action(async (announcementId, options, command) => {
|
|
48
|
+
const output = getOutputFormat(command);
|
|
107
49
|
const apiContext = await createApiContext(options, command);
|
|
108
50
|
if (!apiContext) {
|
|
109
51
|
process.exitCode = 1;
|
|
@@ -116,19 +58,14 @@ export function registerAnnouncementsCommand(program) {
|
|
|
116
58
|
return;
|
|
117
59
|
}
|
|
118
60
|
const firstPost = posts[0];
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
modified_at: formatTimestamp(firstPost.modified),
|
|
129
|
-
message: firstPost.message,
|
|
130
|
-
},
|
|
131
|
-
};
|
|
132
|
-
console.log(JSON.stringify(output));
|
|
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);
|
|
133
70
|
});
|
|
134
71
|
}
|
|
@@ -1 +1 @@
|
|
|
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,
|
|
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"}
|
|
@@ -158,9 +158,9 @@ export function registerAssignmentsCommand(program) {
|
|
|
158
158
|
// Upload file if --file option is provided
|
|
159
159
|
if (options.file) {
|
|
160
160
|
const resolvedPath = path.resolve(options.file);
|
|
161
|
-
|
|
161
|
+
let stats;
|
|
162
162
|
try {
|
|
163
|
-
await fs.
|
|
163
|
+
stats = await fs.stat(resolvedPath);
|
|
164
164
|
}
|
|
165
165
|
catch {
|
|
166
166
|
const errorResult = {
|
|
@@ -171,7 +171,6 @@ export function registerAssignmentsCommand(program) {
|
|
|
171
171
|
process.exitCode = 1;
|
|
172
172
|
return;
|
|
173
173
|
}
|
|
174
|
-
const stats = await fs.stat(resolvedPath);
|
|
175
174
|
const fileSizeKB = formatFileSize(stats.size);
|
|
176
175
|
const uploadResult = await uploadFileApi(apiContext.session, resolvedPath);
|
|
177
176
|
if (!uploadResult.success) {
|
|
@@ -1 +1 @@
|
|
|
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,
|
|
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
CHANGED
|
@@ -129,31 +129,41 @@ export function registerCommand(program) {
|
|
|
129
129
|
catch {
|
|
130
130
|
// Ignore sesskey extraction errors
|
|
131
131
|
}
|
|
132
|
-
//
|
|
132
|
+
// Acquire WS token
|
|
133
|
+
let wsToken;
|
|
133
134
|
try {
|
|
134
|
-
|
|
135
|
-
username: "",
|
|
136
|
-
password: "",
|
|
137
|
-
courseUrl: "",
|
|
138
|
-
moodleBaseUrl: "https://ilearning.cycu.edu.tw",
|
|
139
|
-
headless: false,
|
|
140
|
-
slowMo: 0,
|
|
141
|
-
authStatePath: sessionPath,
|
|
142
|
-
ollamaBaseUrl: "",
|
|
143
|
-
};
|
|
144
|
-
const wsToken = await acquireWsToken(page, config, log);
|
|
135
|
+
wsToken = await acquireWsToken(page, { moodleBaseUrl: "https://ilearning.cycu.edu.tw" }, log);
|
|
145
136
|
saveWsToken(sessionPath, wsToken);
|
|
146
137
|
}
|
|
147
138
|
catch {
|
|
148
139
|
// WS token is optional, ignore errors
|
|
149
140
|
}
|
|
150
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
|
+
}
|
|
151
160
|
const result = {
|
|
152
161
|
status: "success",
|
|
153
162
|
message: "Login successful",
|
|
154
163
|
session_path: sessionPath,
|
|
155
164
|
session_size: stats.size,
|
|
156
|
-
updated: true
|
|
165
|
+
updated: true,
|
|
166
|
+
...(user ? { user } : {}),
|
|
157
167
|
};
|
|
158
168
|
console.log(JSON.stringify(result, null, 2));
|
|
159
169
|
}
|
|
@@ -247,7 +257,7 @@ export function registerCommand(program) {
|
|
|
247
257
|
status: "error",
|
|
248
258
|
error: "Session not found",
|
|
249
259
|
session_path: sessionPath,
|
|
250
|
-
hint: "Run 'openape
|
|
260
|
+
hint: "Run 'openape login' first"
|
|
251
261
|
};
|
|
252
262
|
console.log(JSON.stringify(result, null, 2));
|
|
253
263
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"calendar.d.ts","sourceRoot":"","sources":["../../../src/src/commands/calendar.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
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"}
|
|
@@ -1,45 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getOutputFormat, formatTimestamp } from "../lib/utils.js";
|
|
2
2
|
import { getEnrolledCoursesApi, getCalendarEventsApi } from "../lib/moodle.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import path from "node:path";
|
|
3
|
+
import { createApiContext } from "../lib/auth.js";
|
|
4
|
+
import { formatAndOutput } from "../index.js";
|
|
6
5
|
import fs from "node:fs";
|
|
7
6
|
export function registerCalendarCommand(program) {
|
|
8
7
|
const calendarCmd = program.command("calendar");
|
|
9
8
|
calendarCmd.description("Calendar 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);
|
|
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
|
-
log.error("未找到登入 session。請先執行 'openape auth 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
|
-
log.error("未找到 WS token。請先執行 'openape auth login' 進行登入。");
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
return {
|
|
36
|
-
log,
|
|
37
|
-
session: {
|
|
38
|
-
wsToken,
|
|
39
|
-
moodleBaseUrl: "https://ilearning.cycu.edu.tw",
|
|
40
|
-
},
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
9
|
calendarCmd
|
|
44
10
|
.command("events")
|
|
45
11
|
.description("List calendar events")
|
|
@@ -48,6 +14,7 @@ export function registerCalendarCommand(program) {
|
|
|
48
14
|
.option("--course <id>", "Filter by course ID")
|
|
49
15
|
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
50
16
|
.action(async (options, command) => {
|
|
17
|
+
const output = getOutputFormat(command);
|
|
51
18
|
const days = parseInt(options.days, 10);
|
|
52
19
|
const apiContext = await createApiContext(options, command);
|
|
53
20
|
if (!apiContext) {
|
|
@@ -55,12 +22,10 @@ export function registerCalendarCommand(program) {
|
|
|
55
22
|
return;
|
|
56
23
|
}
|
|
57
24
|
const courses = await getEnrolledCoursesApi(apiContext.session);
|
|
58
|
-
// Calculate time range
|
|
59
25
|
const now = Math.floor(Date.now() / 1000);
|
|
60
26
|
const endTime = now + (days * 24 * 60 * 60);
|
|
61
27
|
let allEvents = [];
|
|
62
28
|
if (options.course) {
|
|
63
|
-
// Get events for specific course
|
|
64
29
|
const courseId = parseInt(options.course, 10);
|
|
65
30
|
const events = await getCalendarEventsApi(apiContext.session, {
|
|
66
31
|
startTime: now,
|
|
@@ -69,29 +34,32 @@ export function registerCalendarCommand(program) {
|
|
|
69
34
|
allEvents = events.filter(e => e.courseid === courseId);
|
|
70
35
|
}
|
|
71
36
|
else {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
allEvents.push(...events);
|
|
81
|
-
}
|
|
82
|
-
catch (err) {
|
|
83
|
-
apiContext.log.debug(`Failed to fetch calendar events for ${course.fullname}: ${err}`);
|
|
84
|
-
}
|
|
37
|
+
const results = await Promise.allSettled(courses.map(course => getCalendarEventsApi(apiContext.session, {
|
|
38
|
+
courseId: course.id,
|
|
39
|
+
startTime: now,
|
|
40
|
+
endTime: endTime,
|
|
41
|
+
})));
|
|
42
|
+
for (const result of results) {
|
|
43
|
+
if (result.status === "fulfilled")
|
|
44
|
+
allEvents.push(...result.value);
|
|
85
45
|
}
|
|
86
46
|
}
|
|
87
|
-
// Sort by start time
|
|
88
47
|
allEvents.sort((a, b) => a.timestart - b.timestart);
|
|
89
|
-
// Filter upcoming only if requested
|
|
90
48
|
let filteredEvents = allEvents;
|
|
91
49
|
if (options.upcoming) {
|
|
92
50
|
filteredEvents = allEvents.filter(e => e.timestart > now);
|
|
93
51
|
}
|
|
94
|
-
|
|
52
|
+
const items = filteredEvents.map(e => ({
|
|
53
|
+
id: e.id,
|
|
54
|
+
name: e.name,
|
|
55
|
+
description: e.description,
|
|
56
|
+
course_id: e.courseid,
|
|
57
|
+
event_type: e.eventtype,
|
|
58
|
+
start_time: formatTimestamp(e.timestart),
|
|
59
|
+
end_time: e.timeduration ? formatTimestamp(e.timestart + Math.floor(e.timeduration / 1000)) : null,
|
|
60
|
+
location: e.location,
|
|
61
|
+
}));
|
|
62
|
+
formatAndOutput(items, output, apiContext.log, {
|
|
95
63
|
status: "success",
|
|
96
64
|
timestamp: new Date().toISOString(),
|
|
97
65
|
total_events: allEvents.length,
|
|
@@ -100,19 +68,7 @@ export function registerCalendarCommand(program) {
|
|
|
100
68
|
acc[e.eventtype] = (acc[e.eventtype] || 0) + 1;
|
|
101
69
|
return acc;
|
|
102
70
|
}, {}),
|
|
103
|
-
})
|
|
104
|
-
for (const e of filteredEvents) {
|
|
105
|
-
console.log(JSON.stringify({
|
|
106
|
-
id: e.id,
|
|
107
|
-
name: e.name,
|
|
108
|
-
description: e.description,
|
|
109
|
-
course_id: e.courseid,
|
|
110
|
-
event_type: e.eventtype,
|
|
111
|
-
start_time: formatTimestamp(e.timestart),
|
|
112
|
-
end_time: e.timeduration ? formatTimestamp(e.timestart + Math.floor(e.timeduration / 1000)) : null,
|
|
113
|
-
location: e.location,
|
|
114
|
-
}));
|
|
115
|
-
}
|
|
71
|
+
});
|
|
116
72
|
});
|
|
117
73
|
calendarCmd
|
|
118
74
|
.command("export")
|
|
@@ -126,27 +82,20 @@ export function registerCalendarCommand(program) {
|
|
|
126
82
|
return;
|
|
127
83
|
}
|
|
128
84
|
const courses = await getEnrolledCoursesApi(apiContext.session);
|
|
129
|
-
// Calculate time range
|
|
130
85
|
const now = Math.floor(Date.now() / 1000);
|
|
131
86
|
const days = parseInt(options.days, 10);
|
|
132
87
|
const endTime = now + (days * 24 * 60 * 60);
|
|
133
88
|
const allEvents = [];
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
allEvents.push(...
|
|
142
|
-
}
|
|
143
|
-
catch (err) {
|
|
144
|
-
apiContext.log.debug(`Failed to fetch calendar events for ${course.fullname}: ${err}`);
|
|
145
|
-
}
|
|
89
|
+
const results = await Promise.allSettled(courses.map(course => getCalendarEventsApi(apiContext.session, {
|
|
90
|
+
courseId: course.id,
|
|
91
|
+
startTime: now,
|
|
92
|
+
endTime: endTime,
|
|
93
|
+
})));
|
|
94
|
+
for (const result of results) {
|
|
95
|
+
if (result.status === "fulfilled")
|
|
96
|
+
allEvents.push(...result.value);
|
|
146
97
|
}
|
|
147
|
-
// Sort by start time
|
|
148
98
|
allEvents.sort((a, b) => a.timestart - b.timestart);
|
|
149
|
-
// Export data
|
|
150
99
|
const exportData = {
|
|
151
100
|
exported_at: new Date().toISOString(),
|
|
152
101
|
time_range: {
|
|
@@ -172,7 +121,6 @@ export function registerCalendarCommand(program) {
|
|
|
172
121
|
}, {}),
|
|
173
122
|
},
|
|
174
123
|
};
|
|
175
|
-
// Write to file
|
|
176
124
|
fs.writeFileSync(options.output, JSON.stringify(exportData));
|
|
177
125
|
apiContext.log.success(`Exported ${allEvents.length} events to ${options.output}`);
|
|
178
126
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"courses.d.ts","sourceRoot":"","sources":["../../../src/src/commands/courses.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"courses.d.ts","sourceRoot":"","sources":["../../../src/src/commands/courses.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAMpC,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAkW7D"}
|
|
@@ -1,46 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getOutputFormat, formatTimestamp } from "../lib/utils.js";
|
|
2
2
|
import { getEnrolledCoursesApi } from "../lib/moodle.js";
|
|
3
|
-
import {
|
|
4
|
-
import { loadWsToken } from "../lib/token.js";
|
|
3
|
+
import { createApiContext } from "../lib/auth.js";
|
|
5
4
|
import { formatAndOutput } from "../index.js";
|
|
6
|
-
import path from "node:path";
|
|
7
|
-
import fs from "node:fs";
|
|
8
5
|
export function registerCoursesCommand(program) {
|
|
9
6
|
const coursesCmd = program.command("courses");
|
|
10
7
|
coursesCmd.description("Course operations");
|
|
11
|
-
// Helper to get output format from global or local options
|
|
12
|
-
function getOutputFormat(command) {
|
|
13
|
-
const opts = command.optsWithGlobals();
|
|
14
|
-
return opts.output || "json";
|
|
15
|
-
}
|
|
16
|
-
// Pure API context - no browser required (fast!)
|
|
17
|
-
async function createApiContext(options, command) {
|
|
18
|
-
const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
|
|
19
|
-
const outputFormat = getOutputFormat(command || { optsWithGlobals: () => ({ output: "json" }) });
|
|
20
|
-
const silent = outputFormat === "json" && !opts.verbose;
|
|
21
|
-
const log = createLogger(opts.verbose, silent);
|
|
22
|
-
const baseDir = getBaseDir();
|
|
23
|
-
const sessionPath = path.resolve(baseDir, ".auth", "storage-state.json");
|
|
24
|
-
// Check if session exists
|
|
25
|
-
if (!fs.existsSync(sessionPath)) {
|
|
26
|
-
log.error("未找到登入 session。請先執行 'openape auth login' 進行登入。");
|
|
27
|
-
log.info(`Session 預期位置: ${sessionPath}`);
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
// Try to load WS token
|
|
31
|
-
const wsToken = loadWsToken(sessionPath);
|
|
32
|
-
if (!wsToken) {
|
|
33
|
-
log.error("未找到 WS token。請先執行 'openape auth login' 進行登入。");
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
return {
|
|
37
|
-
log,
|
|
38
|
-
session: {
|
|
39
|
-
wsToken,
|
|
40
|
-
moodleBaseUrl: "https://ilearning.cycu.edu.tw",
|
|
41
|
-
},
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
8
|
coursesCmd
|
|
45
9
|
.command("list")
|
|
46
10
|
.description("List enrolled courses")
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"forums.d.ts","sourceRoot":"","sources":["../../../src/src/commands/forums.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"forums.d.ts","sourceRoot":"","sources":["../../../src/src/commands/forums.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAiBpC,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA+O5D"}
|