@mo7yw4ng/openape 1.0.0 → 1.0.2
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/LICENSE +21 -21
- package/README.md +15 -5
- package/esm/_dnt.polyfills.d.ts +83 -6
- package/esm/_dnt.polyfills.d.ts.map +1 -0
- package/esm/_dnt.polyfills.js +127 -1
- package/esm/_dnt.shims.d.ts +1 -0
- package/esm/_dnt.shims.d.ts.map +1 -0
- package/esm/deno.d.ts +2 -0
- package/esm/deno.d.ts.map +1 -0
- package/esm/deno.js +2 -1
- package/esm/src/commands/announcements.d.ts +3 -0
- package/esm/src/commands/announcements.d.ts.map +1 -0
- package/esm/src/commands/announcements.js +135 -0
- package/esm/src/commands/auth.d.ts +3 -0
- package/esm/src/commands/auth.d.ts.map +1 -0
- package/esm/src/commands/auth.js +264 -0
- package/esm/src/commands/calendar.d.ts +3 -0
- package/esm/src/commands/calendar.d.ts.map +1 -0
- package/esm/src/commands/calendar.js +180 -0
- package/esm/src/commands/courses.d.ts +3 -0
- package/esm/src/commands/courses.d.ts.map +1 -0
- package/esm/src/commands/courses.js +348 -0
- package/esm/src/commands/forums.d.ts +3 -0
- package/esm/src/commands/forums.d.ts.map +1 -0
- package/esm/src/commands/forums.js +231 -0
- package/esm/src/commands/grades.d.ts +3 -0
- package/esm/src/commands/grades.d.ts.map +1 -0
- package/esm/src/commands/grades.js +121 -0
- package/esm/src/commands/materials.d.ts +3 -0
- package/esm/src/commands/materials.d.ts.map +1 -0
- package/esm/src/commands/materials.js +362 -0
- package/esm/src/commands/quizzes.d.ts +3 -0
- package/esm/src/commands/quizzes.d.ts.map +1 -0
- package/esm/src/commands/quizzes.js +160 -0
- package/esm/src/commands/skills.d.ts +3 -0
- package/esm/src/commands/skills.d.ts.map +1 -0
- package/esm/src/commands/skills.js +110 -0
- package/esm/src/commands/videos.d.ts +3 -0
- package/esm/src/commands/videos.d.ts.map +1 -0
- package/esm/src/commands/videos.js +302 -0
- package/esm/src/index.d.ts +27 -0
- package/esm/src/index.d.ts.map +1 -0
- package/esm/src/index.js +149 -0
- package/esm/src/lib/auth.d.ts +25 -0
- package/esm/src/lib/auth.d.ts.map +1 -0
- package/esm/src/lib/auth.js +194 -0
- package/esm/src/lib/config.d.ts +6 -0
- package/esm/src/lib/config.d.ts.map +1 -0
- package/esm/src/lib/config.js +36 -0
- package/esm/src/lib/logger.d.ts +3 -0
- package/esm/src/lib/logger.d.ts.map +1 -0
- package/esm/src/lib/logger.js +24 -0
- package/esm/src/lib/moodle.d.ts +205 -0
- package/esm/src/lib/moodle.d.ts.map +1 -0
- package/esm/src/lib/moodle.js +690 -0
- package/esm/src/lib/session.d.ts +8 -0
- package/esm/src/lib/session.d.ts.map +1 -0
- package/esm/src/lib/session.js +42 -0
- package/esm/src/lib/token.d.ts +38 -0
- package/esm/src/lib/token.d.ts.map +1 -0
- package/esm/src/lib/token.js +178 -0
- package/esm/src/lib/types.d.ts +271 -0
- package/esm/src/lib/types.d.ts.map +1 -0
- package/esm/src/lib/types.js +1 -0
- package/esm/src/lib/utils.d.ts +17 -0
- package/esm/src/lib/utils.d.ts.map +1 -0
- package/esm/src/lib/utils.js +51 -0
- package/package.json +7 -3
- package/script/_dnt.polyfills.d.ts +83 -6
- package/script/_dnt.polyfills.d.ts.map +1 -0
- package/script/_dnt.polyfills.js +128 -0
- package/script/_dnt.shims.d.ts +1 -0
- package/script/_dnt.shims.d.ts.map +1 -0
- package/script/deno.d.ts +2 -0
- package/script/deno.d.ts.map +1 -0
- package/script/deno.js +2 -1
- package/script/src/commands/announcements.d.ts +1 -0
- package/script/src/commands/announcements.d.ts.map +1 -0
- package/script/src/commands/announcements.js +75 -222
- package/script/src/commands/auth.d.ts +1 -0
- package/script/src/commands/auth.d.ts.map +1 -0
- package/script/src/commands/auth.js +49 -17
- package/script/src/commands/calendar.d.ts +1 -0
- package/script/src/commands/calendar.d.ts.map +1 -0
- package/script/src/commands/calendar.js +112 -301
- package/script/src/commands/courses.d.ts +1 -0
- package/script/src/commands/courses.d.ts.map +1 -0
- package/script/src/commands/courses.js +43 -173
- package/script/src/commands/forums.d.ts +1 -0
- package/script/src/commands/forums.d.ts.map +1 -0
- package/script/src/commands/forums.js +145 -311
- package/script/src/commands/grades.d.ts +1 -0
- package/script/src/commands/grades.d.ts.map +1 -0
- package/script/src/commands/grades.js +62 -194
- package/script/src/commands/materials.d.ts +1 -0
- package/script/src/commands/materials.d.ts.map +1 -0
- package/script/src/commands/materials.js +111 -166
- package/script/src/commands/quizzes.d.ts +1 -0
- package/script/src/commands/quizzes.d.ts.map +1 -0
- package/script/src/commands/quizzes.js +40 -102
- package/script/src/commands/skills.d.ts +1 -0
- package/script/src/commands/skills.d.ts.map +1 -0
- package/script/src/commands/skills.js +17 -18
- package/script/src/commands/videos.d.ts +1 -0
- package/script/src/commands/videos.d.ts.map +1 -0
- package/script/src/commands/videos.js +26 -52
- package/script/src/index.d.ts +1 -0
- package/script/src/index.d.ts.map +1 -0
- package/script/src/index.js +4 -4
- package/script/src/lib/auth.d.ts +1 -0
- package/script/src/lib/auth.d.ts.map +1 -0
- package/script/src/lib/auth.js +9 -10
- package/script/src/lib/config.d.ts +1 -0
- package/script/src/lib/config.d.ts.map +1 -0
- package/script/src/lib/config.js +6 -7
- package/script/src/lib/logger.d.ts +1 -0
- package/script/src/lib/logger.d.ts.map +1 -0
- package/script/src/lib/logger.js +1 -2
- package/script/src/lib/moodle.d.ts +25 -54
- package/script/src/lib/moodle.d.ts.map +1 -0
- package/script/src/lib/moodle.js +103 -324
- package/script/src/lib/session.d.ts +1 -0
- package/script/src/lib/session.d.ts.map +1 -0
- package/script/src/lib/session.js +3 -29
- package/script/src/lib/token.d.ts +16 -5
- package/script/src/lib/token.d.ts.map +1 -0
- package/script/src/lib/token.js +71 -36
- package/script/src/lib/types.d.ts +10 -0
- package/script/src/lib/types.d.ts.map +1 -0
- package/script/src/lib/utils.d.ts +12 -0
- package/script/src/lib/utils.d.ts.map +1 -0
- package/script/src/lib/utils.js +57 -11
- package/skills/openape/SKILL.md +331 -328
|
@@ -0,0 +1,264 @@
|
|
|
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 } from "../lib/token.js";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
export function registerAuthCommand(program) {
|
|
9
|
+
const authCmd = program.command("auth");
|
|
10
|
+
authCmd.description("Authentication commands");
|
|
11
|
+
// Register login directly on program (not under auth subcommand)
|
|
12
|
+
program
|
|
13
|
+
.command("login")
|
|
14
|
+
.description("Login to iLearning manually and save session")
|
|
15
|
+
.action(async (options) => {
|
|
16
|
+
const log = createLogger(false);
|
|
17
|
+
// Determine session storage path
|
|
18
|
+
const baseDir = getBaseDir();
|
|
19
|
+
const sessionDir = path.resolve(baseDir, ".auth");
|
|
20
|
+
const sessionPath = path.resolve(sessionDir, "storage-state.json");
|
|
21
|
+
// Ensure session directory exists
|
|
22
|
+
if (!fs.existsSync(sessionDir)) {
|
|
23
|
+
fs.mkdirSync(sessionDir, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
const edgePath = findEdgePath();
|
|
26
|
+
const browser = await chromium.launch({
|
|
27
|
+
executablePath: edgePath,
|
|
28
|
+
headless: false,
|
|
29
|
+
slowMo: 0,
|
|
30
|
+
});
|
|
31
|
+
let context;
|
|
32
|
+
let page;
|
|
33
|
+
if (fs.existsSync(sessionPath)) {
|
|
34
|
+
log.info(`找到已有 session: ${sessionPath}`);
|
|
35
|
+
log.info("正在驗證 session...");
|
|
36
|
+
try {
|
|
37
|
+
context = await browser.newContext({ storageState: sessionPath });
|
|
38
|
+
page = await context.newPage();
|
|
39
|
+
await page.goto("https://ilearning.cycu.edu.tw/my/", {
|
|
40
|
+
waitUntil: "domcontentloaded",
|
|
41
|
+
timeout: 15000,
|
|
42
|
+
});
|
|
43
|
+
const url = page.url();
|
|
44
|
+
if (url.includes("login") || url.includes("microsoftonline")) {
|
|
45
|
+
log.warn("Session 已過期,請重新登入。");
|
|
46
|
+
await context.close();
|
|
47
|
+
context = await browser.newContext();
|
|
48
|
+
page = await context.newPage();
|
|
49
|
+
await page.goto("https://ilearning.cycu.edu.tw/login/index.php", {
|
|
50
|
+
waitUntil: "domcontentloaded",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
// Session is still valid, close browser and exit
|
|
55
|
+
try {
|
|
56
|
+
if (context)
|
|
57
|
+
await context.close().catch(() => { });
|
|
58
|
+
}
|
|
59
|
+
catch { }
|
|
60
|
+
try {
|
|
61
|
+
await browser.close().catch(() => { });
|
|
62
|
+
}
|
|
63
|
+
catch { }
|
|
64
|
+
// Wait a bit for browser to fully close
|
|
65
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
66
|
+
const result = {
|
|
67
|
+
status: "success",
|
|
68
|
+
message: "Session still valid",
|
|
69
|
+
session_path: sessionPath,
|
|
70
|
+
updated: false
|
|
71
|
+
};
|
|
72
|
+
console.log(JSON.stringify(result));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
log.warn("無法恢復 session,請重新登入。");
|
|
78
|
+
// context might not have been initialized if the error occurred during newContext
|
|
79
|
+
if (context) {
|
|
80
|
+
await context.close();
|
|
81
|
+
}
|
|
82
|
+
context = await browser.newContext();
|
|
83
|
+
page = await context.newPage();
|
|
84
|
+
await page.goto("https://ilearning.cycu.edu.tw/login/index.php", {
|
|
85
|
+
waitUntil: "domcontentloaded",
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
log.info("首次登入,請在瀏覽器中完成登入流程。");
|
|
91
|
+
context = await browser.newContext();
|
|
92
|
+
page = await context.newPage();
|
|
93
|
+
await page.goto("https://ilearning.cycu.edu.tw/login/index.php", {
|
|
94
|
+
waitUntil: "domcontentloaded",
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
log.info("\n請在瀏覽器中完成登入,登入成功後將自動儲存 session...\n");
|
|
98
|
+
try {
|
|
99
|
+
const startTime = Date.now();
|
|
100
|
+
const timeout = 300000;
|
|
101
|
+
let loggedIn = false;
|
|
102
|
+
while (Date.now() - startTime < timeout) {
|
|
103
|
+
await page.waitForTimeout(1000);
|
|
104
|
+
const currentUrl = page.url();
|
|
105
|
+
if (currentUrl.includes("ilearning.cycu.edu.tw") &&
|
|
106
|
+
!currentUrl.includes("login") &&
|
|
107
|
+
!currentUrl.includes("microsoftonline")) {
|
|
108
|
+
await page.waitForTimeout(2000);
|
|
109
|
+
const finalUrl = page.url();
|
|
110
|
+
if (finalUrl.includes("ilearning.cycu.edu.tw") &&
|
|
111
|
+
!finalUrl.includes("login") &&
|
|
112
|
+
!finalUrl.includes("microsoftonline")) {
|
|
113
|
+
loggedIn = true;
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (loggedIn) {
|
|
119
|
+
await context.storageState({ path: sessionPath });
|
|
120
|
+
// Extract and save sesskey for faster API calls
|
|
121
|
+
try {
|
|
122
|
+
// Navigate to a page with M.cfg first
|
|
123
|
+
await page.goto("https://ilearning.cycu.edu.tw/my/", { waitUntil: "domcontentloaded" });
|
|
124
|
+
// Use Function constructor to avoid dnt transforming globalThis
|
|
125
|
+
const sesskey = await page.evaluate(() => self.M?.cfg?.sesskey ?? null);
|
|
126
|
+
if (sesskey) {
|
|
127
|
+
saveSesskey(sessionPath, sesskey);
|
|
128
|
+
log.debug(`Saved sesskey: ${sesskey}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// Ignore sesskey extraction errors
|
|
133
|
+
}
|
|
134
|
+
// Try to acquire WS token
|
|
135
|
+
try {
|
|
136
|
+
const config = {
|
|
137
|
+
username: "",
|
|
138
|
+
password: "",
|
|
139
|
+
courseUrl: "",
|
|
140
|
+
moodleBaseUrl: "https://ilearning.cycu.edu.tw",
|
|
141
|
+
headless: false,
|
|
142
|
+
slowMo: 0,
|
|
143
|
+
authStatePath: sessionPath,
|
|
144
|
+
ollamaBaseUrl: "",
|
|
145
|
+
};
|
|
146
|
+
const wsToken = await acquireWsToken(page, config, log);
|
|
147
|
+
saveWsToken(sessionPath, wsToken);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// WS token is optional, ignore errors
|
|
151
|
+
}
|
|
152
|
+
const stats = fs.statSync(sessionPath);
|
|
153
|
+
const result = {
|
|
154
|
+
status: "success",
|
|
155
|
+
message: "Login successful",
|
|
156
|
+
session_path: sessionPath,
|
|
157
|
+
session_size: stats.size,
|
|
158
|
+
updated: true
|
|
159
|
+
};
|
|
160
|
+
console.log(JSON.stringify(result, null, 2));
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
throw new Error("TimeoutError");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
const errorResult = {
|
|
168
|
+
status: "error",
|
|
169
|
+
error: err instanceof Error ? err.message : String(err),
|
|
170
|
+
session_path: sessionPath
|
|
171
|
+
};
|
|
172
|
+
console.log(JSON.stringify(errorResult));
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
// Safely close browser with error handling
|
|
176
|
+
try {
|
|
177
|
+
if (context)
|
|
178
|
+
await context.close().catch(() => { });
|
|
179
|
+
}
|
|
180
|
+
catch { }
|
|
181
|
+
try {
|
|
182
|
+
await browser.close().catch(() => { });
|
|
183
|
+
}
|
|
184
|
+
catch { }
|
|
185
|
+
// Wait for browser process to fully terminate
|
|
186
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
// auth subcommand group (for status and other auth-related commands)
|
|
190
|
+
authCmd
|
|
191
|
+
.command("status")
|
|
192
|
+
.description("Check session status")
|
|
193
|
+
.option("--session <path>", "Session file path", ".auth/storage-state.json")
|
|
194
|
+
.action(async (options) => {
|
|
195
|
+
const baseDir = getBaseDir();
|
|
196
|
+
const sessionPath = path.resolve(baseDir, options.session);
|
|
197
|
+
if (fs.existsSync(sessionPath)) {
|
|
198
|
+
const stats = fs.statSync(sessionPath);
|
|
199
|
+
// Try to read and validate the session
|
|
200
|
+
try {
|
|
201
|
+
const content = fs.readFileSync(sessionPath, "utf8");
|
|
202
|
+
const state = JSON.parse(content);
|
|
203
|
+
const cookies = state.cookies || [];
|
|
204
|
+
const moodleSession = cookies.find((c) => c.name === "MoodleSession");
|
|
205
|
+
const result = {
|
|
206
|
+
status: "success",
|
|
207
|
+
session_path: sessionPath,
|
|
208
|
+
exists: true,
|
|
209
|
+
modified: new Date(stats.mtime).toISOString(),
|
|
210
|
+
size: stats.size,
|
|
211
|
+
moodle_session: moodleSession ? {
|
|
212
|
+
exists: true,
|
|
213
|
+
expires: new Date(moodleSession.expires * 1000).toISOString()
|
|
214
|
+
} : {
|
|
215
|
+
exists: false
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
console.log(JSON.stringify(result, null, 2));
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
const result = {
|
|
222
|
+
status: "error",
|
|
223
|
+
error: "Session file is corrupted",
|
|
224
|
+
session_path: sessionPath
|
|
225
|
+
};
|
|
226
|
+
console.log(JSON.stringify(result, null, 2));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
const result = {
|
|
231
|
+
status: "error",
|
|
232
|
+
error: "Session not found",
|
|
233
|
+
session_path: sessionPath,
|
|
234
|
+
hint: "Run 'openape auth login' first"
|
|
235
|
+
};
|
|
236
|
+
console.log(JSON.stringify(result, null, 2));
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
authCmd
|
|
240
|
+
.command("logout")
|
|
241
|
+
.description("Remove saved session")
|
|
242
|
+
.option("--session <path>", "Session file path", ".auth/storage-state.json")
|
|
243
|
+
.action(async (options) => {
|
|
244
|
+
const baseDir = getBaseDir();
|
|
245
|
+
const sessionPath = path.resolve(baseDir, options.session);
|
|
246
|
+
if (fs.existsSync(sessionPath)) {
|
|
247
|
+
fs.unlinkSync(sessionPath);
|
|
248
|
+
const result = {
|
|
249
|
+
status: "success",
|
|
250
|
+
message: "Session removed",
|
|
251
|
+
session_path: sessionPath
|
|
252
|
+
};
|
|
253
|
+
console.log(JSON.stringify(result, null, 2));
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
const result = {
|
|
257
|
+
status: "error",
|
|
258
|
+
error: "Session not found",
|
|
259
|
+
session_path: sessionPath
|
|
260
|
+
};
|
|
261
|
+
console.log(JSON.stringify(result, null, 2));
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
}
|
|
@@ -0,0 +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;AAQpC,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAoM9D"}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { getBaseDir } from "../lib/utils.js";
|
|
2
|
+
import { getEnrolledCoursesApi, getCalendarEventsApi } 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 registerCalendarCommand(program) {
|
|
8
|
+
const calendarCmd = program.command("calendar");
|
|
9
|
+
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
|
+
calendarCmd
|
|
44
|
+
.command("events")
|
|
45
|
+
.description("List calendar events")
|
|
46
|
+
.option("--upcoming", "Show only upcoming events")
|
|
47
|
+
.option("--days <n>", "Number of days ahead to look", "30")
|
|
48
|
+
.option("--course <id>", "Filter by course ID")
|
|
49
|
+
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
50
|
+
.action(async (options, command) => {
|
|
51
|
+
const days = parseInt(options.days, 10);
|
|
52
|
+
const apiContext = await createApiContext(options, command);
|
|
53
|
+
if (!apiContext) {
|
|
54
|
+
process.exitCode = 1;
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const courses = await getEnrolledCoursesApi(apiContext.session);
|
|
58
|
+
// Calculate time range
|
|
59
|
+
const now = Math.floor(Date.now() / 1000);
|
|
60
|
+
const endTime = now + (days * 24 * 60 * 60);
|
|
61
|
+
let allEvents = [];
|
|
62
|
+
if (options.course) {
|
|
63
|
+
// Get events for specific course
|
|
64
|
+
const courseId = parseInt(options.course, 10);
|
|
65
|
+
const events = await getCalendarEventsApi(apiContext.session, {
|
|
66
|
+
startTime: now,
|
|
67
|
+
endTime: endTime,
|
|
68
|
+
});
|
|
69
|
+
allEvents = events.filter(e => e.courseid === courseId);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
// Get events for all courses
|
|
73
|
+
for (const course of courses) {
|
|
74
|
+
try {
|
|
75
|
+
const events = await getCalendarEventsApi(apiContext.session, {
|
|
76
|
+
courseId: course.id,
|
|
77
|
+
startTime: now,
|
|
78
|
+
endTime: endTime,
|
|
79
|
+
});
|
|
80
|
+
allEvents.push(...events);
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
apiContext.log.debug(`Failed to fetch calendar events for ${course.fullname}: ${err}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Sort by start time
|
|
88
|
+
allEvents.sort((a, b) => a.timestart - b.timestart);
|
|
89
|
+
// Filter upcoming only if requested
|
|
90
|
+
let filteredEvents = allEvents;
|
|
91
|
+
if (options.upcoming) {
|
|
92
|
+
filteredEvents = allEvents.filter(e => e.timestart > now);
|
|
93
|
+
}
|
|
94
|
+
const output = {
|
|
95
|
+
status: "success",
|
|
96
|
+
timestamp: new Date().toISOString(),
|
|
97
|
+
events: filteredEvents.map(e => ({
|
|
98
|
+
id: e.id,
|
|
99
|
+
name: e.name,
|
|
100
|
+
description: e.description,
|
|
101
|
+
course_id: e.courseid,
|
|
102
|
+
event_type: e.eventtype,
|
|
103
|
+
start_time: new Date(e.timestart).toISOString(),
|
|
104
|
+
end_time: e.timeduration ? new Date(e.timestart + e.timeduration / 1000).toISOString() : null,
|
|
105
|
+
location: e.location,
|
|
106
|
+
})),
|
|
107
|
+
summary: {
|
|
108
|
+
total_events: allEvents.length,
|
|
109
|
+
upcoming: allEvents.filter(e => e.timestart > now).length,
|
|
110
|
+
by_type: allEvents.reduce((acc, e) => {
|
|
111
|
+
acc[e.eventtype] = (acc[e.eventtype] || 0) + 1;
|
|
112
|
+
return acc;
|
|
113
|
+
}, {}),
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
console.log(JSON.stringify(output));
|
|
117
|
+
});
|
|
118
|
+
calendarCmd
|
|
119
|
+
.command("export")
|
|
120
|
+
.description("Export calendar events to file")
|
|
121
|
+
.option("--output <path>", "Output file path", "./calendar.json")
|
|
122
|
+
.option("--days <n>", "Number of days ahead to include", "30")
|
|
123
|
+
.action(async (options, command) => {
|
|
124
|
+
const apiContext = await createApiContext(options, command);
|
|
125
|
+
if (!apiContext) {
|
|
126
|
+
process.exitCode = 1;
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const courses = await getEnrolledCoursesApi(apiContext.session);
|
|
130
|
+
// Calculate time range
|
|
131
|
+
const now = Math.floor(Date.now() / 1000);
|
|
132
|
+
const days = parseInt(options.days, 10);
|
|
133
|
+
const endTime = now + (days * 24 * 60 * 60);
|
|
134
|
+
const allEvents = [];
|
|
135
|
+
for (const course of courses) {
|
|
136
|
+
try {
|
|
137
|
+
const events = await getCalendarEventsApi(apiContext.session, {
|
|
138
|
+
courseId: course.id,
|
|
139
|
+
startTime: now,
|
|
140
|
+
endTime: endTime,
|
|
141
|
+
});
|
|
142
|
+
allEvents.push(...events);
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
apiContext.log.debug(`Failed to fetch calendar events for ${course.fullname}: ${err}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// Sort by start time
|
|
149
|
+
allEvents.sort((a, b) => a.timestart - b.timestart);
|
|
150
|
+
// Export data
|
|
151
|
+
const exportData = {
|
|
152
|
+
exported_at: new Date().toISOString(),
|
|
153
|
+
time_range: {
|
|
154
|
+
start: new Date(now * 1000).toISOString(),
|
|
155
|
+
end: new Date(endTime * 1000).toISOString(),
|
|
156
|
+
days: days,
|
|
157
|
+
},
|
|
158
|
+
events: allEvents.map(e => ({
|
|
159
|
+
id: e.id,
|
|
160
|
+
name: e.name,
|
|
161
|
+
description: e.description,
|
|
162
|
+
course_id: e.courseid,
|
|
163
|
+
event_type: e.eventtype,
|
|
164
|
+
start_time: new Date(e.timestart).toISOString(),
|
|
165
|
+
end_time: e.timeduration ? new Date(e.timestart + e.timeduration / 1000).toISOString() : null,
|
|
166
|
+
location: e.location,
|
|
167
|
+
})),
|
|
168
|
+
summary: {
|
|
169
|
+
total_events: allEvents.length,
|
|
170
|
+
by_type: allEvents.reduce((acc, e) => {
|
|
171
|
+
acc[e.eventtype] = (acc[e.eventtype] || 0) + 1;
|
|
172
|
+
return acc;
|
|
173
|
+
}, {}),
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
// Write to file
|
|
177
|
+
fs.writeFileSync(options.output, JSON.stringify(exportData));
|
|
178
|
+
apiContext.log.success(`Exported ${allEvents.length} events to ${options.output}`);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
@@ -0,0 +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;AASpC,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA4Y7D"}
|