@mo7yw4ng/openape 1.0.0

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 (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +97 -0
  3. package/esm/_dnt.polyfills.d.ts +24 -0
  4. package/esm/_dnt.polyfills.js +1 -0
  5. package/esm/_dnt.shims.d.ts +5 -0
  6. package/esm/_dnt.shims.js +61 -0
  7. package/esm/deno.d.ts +23 -0
  8. package/esm/deno.js +22 -0
  9. package/esm/package.json +3 -0
  10. package/package.json +34 -0
  11. package/script/_dnt.polyfills.d.ts +24 -0
  12. package/script/_dnt.polyfills.js +2 -0
  13. package/script/_dnt.shims.d.ts +5 -0
  14. package/script/_dnt.shims.js +65 -0
  15. package/script/deno.d.ts +23 -0
  16. package/script/deno.js +24 -0
  17. package/script/package.json +3 -0
  18. package/script/src/commands/announcements.d.ts +2 -0
  19. package/script/src/commands/announcements.js +288 -0
  20. package/script/src/commands/auth.d.ts +2 -0
  21. package/script/src/commands/auth.js +238 -0
  22. package/script/src/commands/calendar.d.ts +2 -0
  23. package/script/src/commands/calendar.js +375 -0
  24. package/script/src/commands/courses.d.ts +2 -0
  25. package/script/src/commands/courses.js +484 -0
  26. package/script/src/commands/forums.d.ts +2 -0
  27. package/script/src/commands/forums.js +403 -0
  28. package/script/src/commands/grades.d.ts +2 -0
  29. package/script/src/commands/grades.js +259 -0
  30. package/script/src/commands/materials.d.ts +2 -0
  31. package/script/src/commands/materials.js +423 -0
  32. package/script/src/commands/quizzes.d.ts +2 -0
  33. package/script/src/commands/quizzes.js +228 -0
  34. package/script/src/commands/skills.d.ts +2 -0
  35. package/script/src/commands/skills.js +117 -0
  36. package/script/src/commands/videos.d.ts +2 -0
  37. package/script/src/commands/videos.js +334 -0
  38. package/script/src/index.d.ts +26 -0
  39. package/script/src/index.js +156 -0
  40. package/script/src/lib/auth.d.ts +24 -0
  41. package/script/src/lib/auth.js +203 -0
  42. package/script/src/lib/config.d.ts +5 -0
  43. package/script/src/lib/config.js +43 -0
  44. package/script/src/lib/logger.d.ts +2 -0
  45. package/script/src/lib/logger.js +28 -0
  46. package/script/src/lib/moodle.d.ts +234 -0
  47. package/script/src/lib/moodle.js +966 -0
  48. package/script/src/lib/session.d.ts +7 -0
  49. package/script/src/lib/session.js +71 -0
  50. package/script/src/lib/token.d.ts +27 -0
  51. package/script/src/lib/token.js +154 -0
  52. package/script/src/lib/types.d.ts +261 -0
  53. package/script/src/lib/types.js +2 -0
  54. package/script/src/lib/utils.d.ts +5 -0
  55. package/script/src/lib/utils.js +43 -0
  56. package/skills/openape/SKILL.md +328 -0
@@ -0,0 +1,375 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.registerCalendarCommand = void 0;
7
+ const utils_js_1 = require("../lib/utils.js");
8
+ const moodle_js_1 = require("../lib/moodle.js");
9
+ const logger_js_1 = require("../lib/logger.js");
10
+ const auth_js_1 = require("../lib/auth.js");
11
+ const session_js_1 = require("../lib/session.js");
12
+ const auth_js_2 = require("../lib/auth.js");
13
+ const token_js_1 = require("../lib/token.js");
14
+ const path_1 = __importDefault(require("path"));
15
+ const fs_1 = __importDefault(require("fs"));
16
+ function registerCalendarCommand(program) {
17
+ const calendarCmd = program.command("calendar");
18
+ calendarCmd.description("Calendar operations");
19
+ // Helper to get output format from global or local options
20
+ function getOutputFormat(command) {
21
+ const opts = command.optsWithGlobals();
22
+ return opts.output || "json";
23
+ }
24
+ // Pure API context - no browser required (fast!)
25
+ async function createApiContext(options, command) {
26
+ const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
27
+ const outputFormat = getOutputFormat(command || { optsWithGlobals: () => ({ output: "json" }) });
28
+ const silent = outputFormat === "json" && !opts.verbose;
29
+ const log = (0, logger_js_1.createLogger)(opts.verbose, silent);
30
+ const baseDir = (0, utils_js_1.getBaseDir)();
31
+ const sessionPath = path_1.default.resolve(baseDir, ".auth", "storage-state.json");
32
+ // Check if session exists
33
+ if (!fs_1.default.existsSync(sessionPath)) {
34
+ return null;
35
+ }
36
+ // Try to load WS token
37
+ const wsToken = (0, token_js_1.loadWsToken)(sessionPath);
38
+ if (!wsToken) {
39
+ return null;
40
+ }
41
+ return {
42
+ log,
43
+ session: {
44
+ wsToken,
45
+ moodleBaseUrl: "https://ilearning.cycu.edu.tw",
46
+ },
47
+ };
48
+ }
49
+ // Helper function to create session context
50
+ async function createSessionContext(options, command) {
51
+ // Get global options if command is provided (for --verbose, --silent flags)
52
+ const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
53
+ // Auto-enable silent mode for JSON output (unless --verbose is also set)
54
+ const outputFormat = getOutputFormat(command || { optsWithGlobals: () => ({ output: "json" }) });
55
+ const silent = outputFormat === "json" && !opts.verbose;
56
+ const log = (0, logger_js_1.createLogger)(opts.verbose, silent);
57
+ // Determine session path
58
+ const baseDir = (0, utils_js_1.getBaseDir)();
59
+ const sessionPath = path_1.default.resolve(baseDir, ".auth", "storage-state.json");
60
+ // Check if session exists
61
+ if (!fs_1.default.existsSync(sessionPath)) {
62
+ log.error("未找到登入 session。請先執行 'openape auth login' 進行登入。");
63
+ log.info(`Session 預期位置: ${sessionPath}`);
64
+ return null;
65
+ }
66
+ // Create minimal config
67
+ const config = {
68
+ username: "",
69
+ password: "",
70
+ courseUrl: "",
71
+ moodleBaseUrl: "https://ilearning.cycu.edu.tw",
72
+ headless: !options.headed,
73
+ slowMo: 0,
74
+ authStatePath: sessionPath,
75
+ ollamaBaseUrl: "",
76
+ };
77
+ log.info("啟動瀏覽器...");
78
+ const { browser, context, page } = await (0, auth_js_1.launchAuthenticated)(config, log);
79
+ try {
80
+ const session = await (0, session_js_1.extractSessionInfo)(page, config, log);
81
+ return { log, page, session, browser, context };
82
+ }
83
+ catch (err) {
84
+ await context.close();
85
+ await browser.close();
86
+ throw err;
87
+ }
88
+ }
89
+ calendarCmd
90
+ .command("events")
91
+ .description("List calendar events")
92
+ .option("--upcoming", "Show only upcoming events")
93
+ .option("--days <n>", "Number of days ahead to look", "30")
94
+ .option("--course <id>", "Filter by course ID")
95
+ .option("--output <format>", "Output format: json|csv|table|silent")
96
+ .action(async (options, command) => {
97
+ const days = parseInt(options.days, 10);
98
+ // Try pure API mode (no browser, fast!)
99
+ const apiContext = await createApiContext(options, command);
100
+ if (apiContext) {
101
+ try {
102
+ const courses = await (0, moodle_js_1.getEnrolledCoursesApi)(apiContext.session);
103
+ // Calculate time range
104
+ const now = Math.floor(Date.now() / 1000);
105
+ const endTime = now + (days * 24 * 60 * 60);
106
+ let allEvents = [];
107
+ if (options.course) {
108
+ // Get events for specific course
109
+ const courseId = parseInt(options.course, 10);
110
+ const events = await (0, moodle_js_1.getCalendarEventsApi)(apiContext.session, {
111
+ startTime: now,
112
+ endTime: endTime,
113
+ });
114
+ allEvents = events.filter(e => e.courseid === courseId);
115
+ }
116
+ else {
117
+ // Get events for all courses
118
+ for (const course of courses) {
119
+ try {
120
+ const events = await (0, moodle_js_1.getCalendarEventsApi)(apiContext.session, {
121
+ courseId: course.id,
122
+ startTime: now,
123
+ endTime: endTime,
124
+ });
125
+ allEvents.push(...events);
126
+ }
127
+ catch (err) {
128
+ apiContext.log.debug(`Failed to fetch calendar events for ${course.fullname}: ${err}`);
129
+ }
130
+ }
131
+ }
132
+ // Sort by start time
133
+ allEvents.sort((a, b) => a.timestart - b.timestart);
134
+ // Filter upcoming only if requested
135
+ let filteredEvents = allEvents;
136
+ if (options.upcoming) {
137
+ filteredEvents = allEvents.filter(e => e.timestart > now);
138
+ }
139
+ const output = {
140
+ status: "success",
141
+ timestamp: new Date().toISOString(),
142
+ events: filteredEvents.map(e => ({
143
+ id: e.id,
144
+ name: e.name,
145
+ description: e.description,
146
+ course_id: e.courseid,
147
+ event_type: e.eventtype,
148
+ start_time: new Date(e.timestart).toISOString(),
149
+ end_time: e.timeduration ? new Date(e.timestart + e.timeduration / 1000).toISOString() : null,
150
+ location: e.location,
151
+ })),
152
+ summary: {
153
+ total_events: allEvents.length,
154
+ upcoming: allEvents.filter(e => e.timestart > now).length,
155
+ by_type: allEvents.reduce((acc, e) => {
156
+ acc[e.eventtype] = (acc[e.eventtype] || 0) + 1;
157
+ return acc;
158
+ }, {}),
159
+ },
160
+ };
161
+ console.log(JSON.stringify(output));
162
+ return;
163
+ }
164
+ catch (e) {
165
+ // API failed, fall through to browser mode
166
+ const msg = e instanceof Error ? e.message : String(e);
167
+ console.error(`// API mode failed: ${msg}, trying browser mode...`);
168
+ }
169
+ }
170
+ // Fallback to browser mode
171
+ const context = await createSessionContext(options, command);
172
+ if (!context) {
173
+ process.exitCode = 1;
174
+ return;
175
+ }
176
+ const { log, page, session, browser, context: browserContext } = context;
177
+ try {
178
+ const courses = await (0, moodle_js_1.getEnrolledCourses)(page, session, log);
179
+ // Calculate time range
180
+ const now = Math.floor(Date.now() / 1000);
181
+ const endTime = now + (days * 24 * 60 * 60);
182
+ let allEvents = [];
183
+ if (options.course) {
184
+ // Get events for specific course
185
+ const courseId = parseInt(options.course, 10);
186
+ const events = await (0, moodle_js_1.getCalendarEvents)(page, session, {
187
+ startTime: now,
188
+ endTime: endTime,
189
+ });
190
+ allEvents = events.filter(e => e.courseid === courseId);
191
+ }
192
+ else {
193
+ // Get events for all courses
194
+ for (const course of courses) {
195
+ try {
196
+ const events = await (0, moodle_js_1.getCalendarEvents)(page, session, {
197
+ courseId: course.id,
198
+ startTime: now,
199
+ endTime: endTime,
200
+ });
201
+ allEvents.push(...events);
202
+ }
203
+ catch (err) {
204
+ log.debug(`Failed to fetch calendar events for ${course.fullname}: ${err}`);
205
+ }
206
+ }
207
+ }
208
+ // Sort by start time
209
+ allEvents.sort((a, b) => a.timestart - b.timestart);
210
+ // Filter upcoming only if requested
211
+ let filteredEvents = allEvents;
212
+ if (options.upcoming) {
213
+ filteredEvents = allEvents.filter(e => e.timestart > now);
214
+ }
215
+ const output = {
216
+ status: "success",
217
+ timestamp: new Date().toISOString(),
218
+ events: filteredEvents.map(e => ({
219
+ id: e.id,
220
+ name: e.name,
221
+ description: e.description,
222
+ course_id: e.courseid,
223
+ event_type: e.eventtype,
224
+ start_time: new Date(e.timestart).toISOString(),
225
+ end_time: e.timeduration ? new Date(e.timestart + e.timeduration / 1000).toISOString() : null,
226
+ location: e.location,
227
+ })),
228
+ summary: {
229
+ total_events: allEvents.length,
230
+ upcoming: allEvents.filter(e => e.timestart > now).length,
231
+ by_type: allEvents.reduce((acc, e) => {
232
+ acc[e.eventtype] = (acc[e.eventtype] || 0) + 1;
233
+ return acc;
234
+ }, {}),
235
+ },
236
+ };
237
+ console.log(JSON.stringify(output));
238
+ }
239
+ finally {
240
+ await (0, auth_js_2.closeBrowserSafely)(browser, browserContext);
241
+ }
242
+ });
243
+ calendarCmd
244
+ .command("export")
245
+ .description("Export calendar events to file")
246
+ .option("--output <path>", "Output file path", "./calendar.json")
247
+ .option("--days <n>", "Number of days ahead to include", "30")
248
+ .action(async (options, command) => {
249
+ // Try pure API mode (no browser, fast!)
250
+ const apiContext = await createApiContext(options, command);
251
+ if (apiContext) {
252
+ try {
253
+ const courses = await (0, moodle_js_1.getEnrolledCoursesApi)(apiContext.session);
254
+ // Calculate time range
255
+ const now = Math.floor(Date.now() / 1000);
256
+ const days = parseInt(options.days, 10);
257
+ const endTime = now + (days * 24 * 60 * 60);
258
+ const allEvents = [];
259
+ for (const course of courses) {
260
+ try {
261
+ const events = await (0, moodle_js_1.getCalendarEventsApi)(apiContext.session, {
262
+ courseId: course.id,
263
+ startTime: now,
264
+ endTime: endTime,
265
+ });
266
+ allEvents.push(...events);
267
+ }
268
+ catch (err) {
269
+ apiContext.log.debug(`Failed to fetch calendar events for ${course.fullname}: ${err}`);
270
+ }
271
+ }
272
+ // Sort by start time
273
+ allEvents.sort((a, b) => a.timestart - b.timestart);
274
+ // Export data
275
+ const exportData = {
276
+ exported_at: new Date().toISOString(),
277
+ time_range: {
278
+ start: new Date(now * 1000).toISOString(),
279
+ end: new Date(endTime * 1000).toISOString(),
280
+ days: days,
281
+ },
282
+ events: allEvents.map(e => ({
283
+ id: e.id,
284
+ name: e.name,
285
+ description: e.description,
286
+ course_id: e.courseid,
287
+ event_type: e.eventtype,
288
+ start_time: new Date(e.timestart).toISOString(),
289
+ end_time: e.timeduration ? new Date(e.timestart + e.timeduration / 1000).toISOString() : null,
290
+ location: e.location,
291
+ })),
292
+ summary: {
293
+ total_events: allEvents.length,
294
+ by_type: allEvents.reduce((acc, e) => {
295
+ acc[e.eventtype] = (acc[e.eventtype] || 0) + 1;
296
+ return acc;
297
+ }, {}),
298
+ },
299
+ };
300
+ // Write to file
301
+ fs_1.default.writeFileSync(options.output, JSON.stringify(exportData));
302
+ apiContext.log.success(`Exported ${allEvents.length} events to ${options.output}`);
303
+ return;
304
+ }
305
+ catch (e) {
306
+ // API failed, fall through to browser mode
307
+ const msg = e instanceof Error ? e.message : String(e);
308
+ console.error(`// API mode failed: ${msg}, trying browser mode...`);
309
+ }
310
+ }
311
+ // Fallback to browser mode
312
+ const context = await createSessionContext(options, command);
313
+ if (!context) {
314
+ process.exitCode = 1;
315
+ return;
316
+ }
317
+ const { log, page, session, browser, context: browserContext } = context;
318
+ try {
319
+ const courses = await (0, moodle_js_1.getEnrolledCourses)(page, session, log);
320
+ // Calculate time range
321
+ const now = Math.floor(Date.now() / 1000);
322
+ const days = parseInt(options.days, 10);
323
+ const endTime = now + (days * 24 * 60 * 60);
324
+ const allEvents = [];
325
+ for (const course of courses) {
326
+ try {
327
+ const events = await (0, moodle_js_1.getCalendarEvents)(page, session, {
328
+ courseId: course.id,
329
+ startTime: now,
330
+ endTime: endTime,
331
+ });
332
+ allEvents.push(...events);
333
+ }
334
+ catch (err) {
335
+ log.debug(`Failed to fetch calendar events for ${course.fullname}: ${err}`);
336
+ }
337
+ }
338
+ // Sort by start time
339
+ allEvents.sort((a, b) => a.timestart - b.timestart);
340
+ // Export data
341
+ const exportData = {
342
+ exported_at: new Date().toISOString(),
343
+ time_range: {
344
+ start: new Date(now * 1000).toISOString(),
345
+ end: new Date(endTime * 1000).toISOString(),
346
+ days: days,
347
+ },
348
+ events: allEvents.map(e => ({
349
+ id: e.id,
350
+ name: e.name,
351
+ description: e.description,
352
+ course_id: e.courseid,
353
+ event_type: e.eventtype,
354
+ start_time: new Date(e.timestart).toISOString(),
355
+ end_time: e.timeduration ? new Date(e.timestart + e.timeduration / 1000).toISOString() : null,
356
+ location: e.location,
357
+ })),
358
+ summary: {
359
+ total_events: allEvents.length,
360
+ by_type: allEvents.reduce((acc, e) => {
361
+ acc[e.eventtype] = (acc[e.eventtype] || 0) + 1;
362
+ return acc;
363
+ }, {}),
364
+ },
365
+ };
366
+ // Write to file
367
+ fs_1.default.writeFileSync(options.output, JSON.stringify(exportData));
368
+ log.success(`Exported ${allEvents.length} events to ${options.output}`);
369
+ }
370
+ finally {
371
+ await (0, auth_js_2.closeBrowserSafely)(browser, browserContext);
372
+ }
373
+ });
374
+ }
375
+ exports.registerCalendarCommand = registerCalendarCommand;
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function registerCoursesCommand(program: Command): void;