@mo7yw4ng/openape 1.0.6 → 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.
- package/bin/openape +29 -0
- package/bin/openape.js +29 -0
- package/package.json +22 -28
- package/LICENSE +0 -21
- package/README.md +0 -138
- package/esm/_dnt.polyfills.d.ts +0 -101
- package/esm/_dnt.polyfills.d.ts.map +0 -1
- package/esm/_dnt.polyfills.js +0 -127
- package/esm/_dnt.shims.d.ts +0 -6
- package/esm/_dnt.shims.d.ts.map +0 -1
- package/esm/_dnt.shims.js +0 -61
- package/esm/deno.d.ts +0 -25
- package/esm/deno.d.ts.map +0 -1
- package/esm/deno.js +0 -23
- package/esm/package.json +0 -3
- package/esm/src/commands/announcements.d.ts +0 -3
- package/esm/src/commands/announcements.d.ts.map +0 -1
- package/esm/src/commands/announcements.js +0 -71
- package/esm/src/commands/assignments.d.ts +0 -3
- package/esm/src/commands/assignments.d.ts.map +0 -1
- package/esm/src/commands/assignments.js +0 -229
- package/esm/src/commands/auth.d.ts +0 -3
- package/esm/src/commands/auth.d.ts.map +0 -1
- package/esm/src/commands/auth.js +0 -290
- package/esm/src/commands/calendar.d.ts +0 -3
- package/esm/src/commands/calendar.d.ts.map +0 -1
- package/esm/src/commands/calendar.js +0 -127
- package/esm/src/commands/courses.d.ts +0 -3
- package/esm/src/commands/courses.d.ts.map +0 -1
- package/esm/src/commands/courses.js +0 -312
- package/esm/src/commands/forums.d.ts +0 -3
- package/esm/src/commands/forums.d.ts.map +0 -1
- package/esm/src/commands/forums.js +0 -190
- package/esm/src/commands/grades.d.ts +0 -3
- package/esm/src/commands/grades.d.ts.map +0 -1
- package/esm/src/commands/grades.js +0 -84
- package/esm/src/commands/materials.d.ts +0 -3
- package/esm/src/commands/materials.d.ts.map +0 -1
- package/esm/src/commands/materials.js +0 -402
- package/esm/src/commands/quizzes.d.ts +0 -3
- package/esm/src/commands/quizzes.d.ts.map +0 -1
- package/esm/src/commands/quizzes.js +0 -236
- package/esm/src/commands/skills.d.ts +0 -3
- package/esm/src/commands/skills.d.ts.map +0 -1
- package/esm/src/commands/skills.js +0 -106
- package/esm/src/commands/upload.d.ts +0 -3
- package/esm/src/commands/upload.d.ts.map +0 -1
- package/esm/src/commands/upload.js +0 -55
- package/esm/src/commands/videos.d.ts +0 -3
- package/esm/src/commands/videos.d.ts.map +0 -1
- package/esm/src/commands/videos.js +0 -266
- package/esm/src/index.d.ts +0 -28
- package/esm/src/index.d.ts.map +0 -1
- package/esm/src/index.js +0 -164
- package/esm/src/lib/auth.d.ts +0 -66
- package/esm/src/lib/auth.d.ts.map +0 -1
- package/esm/src/lib/auth.js +0 -286
- package/esm/src/lib/config.d.ts +0 -6
- package/esm/src/lib/config.d.ts.map +0 -1
- package/esm/src/lib/config.js +0 -36
- package/esm/src/lib/logger.d.ts +0 -3
- package/esm/src/lib/logger.d.ts.map +0 -1
- package/esm/src/lib/logger.js +0 -26
- package/esm/src/lib/moodle.d.ts +0 -447
- package/esm/src/lib/moodle.d.ts.map +0 -1
- package/esm/src/lib/moodle.js +0 -1353
- package/esm/src/lib/session.d.ts +0 -8
- package/esm/src/lib/session.d.ts.map +0 -1
- package/esm/src/lib/session.js +0 -42
- package/esm/src/lib/token.d.ts +0 -38
- package/esm/src/lib/token.d.ts.map +0 -1
- package/esm/src/lib/token.js +0 -178
- package/esm/src/lib/types.d.ts +0 -189
- package/esm/src/lib/types.d.ts.map +0 -1
- package/esm/src/lib/types.js +0 -2
- package/esm/src/lib/utils.d.ts +0 -52
- package/esm/src/lib/utils.d.ts.map +0 -1
- package/esm/src/lib/utils.js +0 -122
- package/script/_dnt.polyfills.d.ts +0 -101
- package/script/_dnt.polyfills.d.ts.map +0 -1
- package/script/_dnt.polyfills.js +0 -130
- package/script/_dnt.shims.d.ts +0 -6
- package/script/_dnt.shims.d.ts.map +0 -1
- package/script/_dnt.shims.js +0 -65
- package/script/deno.d.ts +0 -25
- package/script/deno.d.ts.map +0 -1
- package/script/deno.js +0 -25
- package/script/package.json +0 -3
- package/script/src/commands/announcements.d.ts +0 -3
- package/script/src/commands/announcements.d.ts.map +0 -1
- package/script/src/commands/announcements.js +0 -74
- package/script/src/commands/assignments.d.ts +0 -3
- package/script/src/commands/assignments.d.ts.map +0 -1
- package/script/src/commands/assignments.js +0 -268
- package/script/src/commands/auth.d.ts +0 -3
- package/script/src/commands/auth.d.ts.map +0 -1
- package/script/src/commands/auth.js +0 -296
- package/script/src/commands/calendar.d.ts +0 -3
- package/script/src/commands/calendar.d.ts.map +0 -1
- package/script/src/commands/calendar.js +0 -133
- package/script/src/commands/courses.d.ts +0 -3
- package/script/src/commands/courses.d.ts.map +0 -1
- package/script/src/commands/courses.js +0 -315
- package/script/src/commands/forums.d.ts +0 -3
- package/script/src/commands/forums.d.ts.map +0 -1
- package/script/src/commands/forums.js +0 -193
- package/script/src/commands/grades.d.ts +0 -3
- package/script/src/commands/grades.d.ts.map +0 -1
- package/script/src/commands/grades.js +0 -87
- package/script/src/commands/materials.d.ts +0 -3
- package/script/src/commands/materials.d.ts.map +0 -1
- package/script/src/commands/materials.js +0 -408
- package/script/src/commands/quizzes.d.ts +0 -3
- package/script/src/commands/quizzes.d.ts.map +0 -1
- package/script/src/commands/quizzes.js +0 -239
- package/script/src/commands/skills.d.ts +0 -3
- package/script/src/commands/skills.d.ts.map +0 -1
- package/script/src/commands/skills.js +0 -112
- package/script/src/commands/upload.d.ts +0 -3
- package/script/src/commands/upload.d.ts.map +0 -1
- package/script/src/commands/upload.js +0 -61
- package/script/src/commands/videos.d.ts +0 -3
- package/script/src/commands/videos.d.ts.map +0 -1
- package/script/src/commands/videos.js +0 -272
- package/script/src/index.d.ts +0 -28
- package/script/src/index.d.ts.map +0 -1
- package/script/src/index.js +0 -171
- package/script/src/lib/auth.d.ts +0 -66
- package/script/src/lib/auth.d.ts.map +0 -1
- package/script/src/lib/auth.js +0 -296
- package/script/src/lib/config.d.ts +0 -6
- package/script/src/lib/config.d.ts.map +0 -1
- package/script/src/lib/config.js +0 -42
- package/script/src/lib/logger.d.ts +0 -3
- package/script/src/lib/logger.d.ts.map +0 -1
- package/script/src/lib/logger.js +0 -29
- package/script/src/lib/moodle.d.ts +0 -447
- package/script/src/lib/moodle.d.ts.map +0 -1
- package/script/src/lib/moodle.js +0 -1425
- package/script/src/lib/session.d.ts +0 -8
- package/script/src/lib/session.d.ts.map +0 -1
- package/script/src/lib/session.js +0 -45
- package/script/src/lib/token.d.ts +0 -38
- package/script/src/lib/token.d.ts.map +0 -1
- package/script/src/lib/token.js +0 -189
- package/script/src/lib/types.d.ts +0 -189
- package/script/src/lib/types.d.ts.map +0 -1
- package/script/src/lib/types.js +0 -3
- package/script/src/lib/utils.d.ts +0 -52
- package/script/src/lib/utils.d.ts.map +0 -1
- package/script/src/lib/utils.js +0 -167
- package/skills/openape/SKILL.md +0 -115
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import { getOutputFormat, formatTimestamp } from "../lib/utils.js";
|
|
2
|
-
import { getEnrolledCoursesApi, getCalendarEventsApi } from "../lib/moodle.js";
|
|
3
|
-
import { createApiContext } from "../lib/auth.js";
|
|
4
|
-
import { formatAndOutput } from "../index.js";
|
|
5
|
-
import fs from "node:fs";
|
|
6
|
-
export function registerCalendarCommand(program) {
|
|
7
|
-
const calendarCmd = program.command("calendar");
|
|
8
|
-
calendarCmd.description("Calendar operations");
|
|
9
|
-
calendarCmd
|
|
10
|
-
.command("events")
|
|
11
|
-
.description("List calendar events")
|
|
12
|
-
.option("--upcoming", "Show only upcoming events")
|
|
13
|
-
.option("--days <n>", "Number of days ahead to look", "30")
|
|
14
|
-
.option("--course <id>", "Filter by course ID")
|
|
15
|
-
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
16
|
-
.action(async (options, command) => {
|
|
17
|
-
const output = getOutputFormat(command);
|
|
18
|
-
const days = parseInt(options.days, 10);
|
|
19
|
-
const apiContext = await createApiContext(options, command);
|
|
20
|
-
if (!apiContext) {
|
|
21
|
-
process.exitCode = 1;
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
const courses = await getEnrolledCoursesApi(apiContext.session);
|
|
25
|
-
const now = Math.floor(Date.now() / 1000);
|
|
26
|
-
const endTime = now + (days * 24 * 60 * 60);
|
|
27
|
-
let allEvents = [];
|
|
28
|
-
if (options.course) {
|
|
29
|
-
const courseId = parseInt(options.course, 10);
|
|
30
|
-
const events = await getCalendarEventsApi(apiContext.session, {
|
|
31
|
-
startTime: now,
|
|
32
|
-
endTime: endTime,
|
|
33
|
-
});
|
|
34
|
-
allEvents = events.filter(e => e.courseid === courseId);
|
|
35
|
-
}
|
|
36
|
-
else {
|
|
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);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
allEvents.sort((a, b) => a.timestart - b.timestart);
|
|
48
|
-
let filteredEvents = allEvents;
|
|
49
|
-
if (options.upcoming) {
|
|
50
|
-
filteredEvents = allEvents.filter(e => e.timestart > now);
|
|
51
|
-
}
|
|
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, {
|
|
63
|
-
status: "success",
|
|
64
|
-
timestamp: new Date().toISOString(),
|
|
65
|
-
total_events: allEvents.length,
|
|
66
|
-
upcoming: allEvents.filter(e => e.timestart > now).length,
|
|
67
|
-
by_type: allEvents.reduce((acc, e) => {
|
|
68
|
-
acc[e.eventtype] = (acc[e.eventtype] || 0) + 1;
|
|
69
|
-
return acc;
|
|
70
|
-
}, {}),
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
calendarCmd
|
|
74
|
-
.command("export")
|
|
75
|
-
.description("Export calendar events to file")
|
|
76
|
-
.option("--output <path>", "Output file path", "./calendar.json")
|
|
77
|
-
.option("--days <n>", "Number of days ahead to include", "30")
|
|
78
|
-
.action(async (options, command) => {
|
|
79
|
-
const apiContext = await createApiContext(options, command);
|
|
80
|
-
if (!apiContext) {
|
|
81
|
-
process.exitCode = 1;
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
const courses = await getEnrolledCoursesApi(apiContext.session);
|
|
85
|
-
const now = Math.floor(Date.now() / 1000);
|
|
86
|
-
const days = parseInt(options.days, 10);
|
|
87
|
-
const endTime = now + (days * 24 * 60 * 60);
|
|
88
|
-
const allEvents = [];
|
|
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);
|
|
97
|
-
}
|
|
98
|
-
allEvents.sort((a, b) => a.timestart - b.timestart);
|
|
99
|
-
const exportData = {
|
|
100
|
-
exported_at: new Date().toISOString(),
|
|
101
|
-
time_range: {
|
|
102
|
-
start: new Date(now * 1000).toISOString(),
|
|
103
|
-
end: new Date(endTime * 1000).toISOString(),
|
|
104
|
-
days: days,
|
|
105
|
-
},
|
|
106
|
-
events: allEvents.map(e => ({
|
|
107
|
-
id: e.id,
|
|
108
|
-
name: e.name,
|
|
109
|
-
description: e.description,
|
|
110
|
-
course_id: e.courseid,
|
|
111
|
-
event_type: e.eventtype,
|
|
112
|
-
start_time: formatTimestamp(e.timestart),
|
|
113
|
-
end_time: e.timeduration ? formatTimestamp(e.timestart + Math.floor(e.timeduration / 1000)) : null,
|
|
114
|
-
location: e.location,
|
|
115
|
-
})),
|
|
116
|
-
summary: {
|
|
117
|
-
total_events: allEvents.length,
|
|
118
|
-
by_type: allEvents.reduce((acc, e) => {
|
|
119
|
-
acc[e.eventtype] = (acc[e.eventtype] || 0) + 1;
|
|
120
|
-
return acc;
|
|
121
|
-
}, {}),
|
|
122
|
-
},
|
|
123
|
-
};
|
|
124
|
-
fs.writeFileSync(options.output, JSON.stringify(exportData));
|
|
125
|
-
apiContext.log.success(`Exported ${allEvents.length} events to ${options.output}`);
|
|
126
|
-
});
|
|
127
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
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,312 +0,0 @@
|
|
|
1
|
-
import { getOutputFormat, formatTimestamp } from "../lib/utils.js";
|
|
2
|
-
import { getEnrolledCoursesApi } from "../lib/moodle.js";
|
|
3
|
-
import { createApiContext } from "../lib/auth.js";
|
|
4
|
-
import { formatAndOutput } from "../index.js";
|
|
5
|
-
export function registerCoursesCommand(program) {
|
|
6
|
-
const coursesCmd = program.command("courses");
|
|
7
|
-
coursesCmd.description("Course operations");
|
|
8
|
-
coursesCmd
|
|
9
|
-
.command("list")
|
|
10
|
-
.description("List enrolled courses")
|
|
11
|
-
.option("--incomplete-only", "Show only incomplete courses")
|
|
12
|
-
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
13
|
-
.option("--level <type>", "Course level: in_progress (default) | past | future | all", "in_progress")
|
|
14
|
-
.action(async (options, command) => {
|
|
15
|
-
const output = getOutputFormat(command);
|
|
16
|
-
const apiContext = await createApiContext(options, command);
|
|
17
|
-
if (!apiContext) {
|
|
18
|
-
process.exitCode = 1;
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
// Map level to classification
|
|
22
|
-
const classification = options.level === "all" ? undefined :
|
|
23
|
-
options.level === "past" ? "past" :
|
|
24
|
-
options.level === "future" ? "future" : "inprogress";
|
|
25
|
-
const courses = await getEnrolledCoursesApi(apiContext.session, {
|
|
26
|
-
classification,
|
|
27
|
-
});
|
|
28
|
-
let filteredCourses = courses;
|
|
29
|
-
if (options.incompleteOnly) {
|
|
30
|
-
filteredCourses = courses.filter(c => (c.progress ?? 0) < 100);
|
|
31
|
-
}
|
|
32
|
-
formatAndOutput(filteredCourses, output, apiContext.log);
|
|
33
|
-
});
|
|
34
|
-
coursesCmd
|
|
35
|
-
.command("info")
|
|
36
|
-
.description("Show detailed course information")
|
|
37
|
-
.argument("<course-id>", "Course ID")
|
|
38
|
-
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
39
|
-
.action(async (courseId, options, command) => {
|
|
40
|
-
const output = getOutputFormat(command);
|
|
41
|
-
const apiContext = await createApiContext(options, command);
|
|
42
|
-
if (!apiContext) {
|
|
43
|
-
process.exitCode = 1;
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
const courses = await getEnrolledCoursesApi(apiContext.session);
|
|
47
|
-
const course = courses.find(c => c.id === parseInt(courseId, 10));
|
|
48
|
-
if (!course) {
|
|
49
|
-
apiContext.log.error(`Course not found: ${courseId}`);
|
|
50
|
-
process.exitCode = 1;
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
formatAndOutput(course, output, apiContext.log);
|
|
54
|
-
});
|
|
55
|
-
coursesCmd
|
|
56
|
-
.command("progress")
|
|
57
|
-
.description("Show course progress")
|
|
58
|
-
.argument("<course-id>", "Course ID")
|
|
59
|
-
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
60
|
-
.action(async (courseId, options, command) => {
|
|
61
|
-
const output = getOutputFormat(command);
|
|
62
|
-
const apiContext = await createApiContext(options, command);
|
|
63
|
-
if (!apiContext) {
|
|
64
|
-
process.exitCode = 1;
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
const courses = await getEnrolledCoursesApi(apiContext.session);
|
|
68
|
-
const course = courses.find(c => c.id === parseInt(courseId, 10));
|
|
69
|
-
if (!course) {
|
|
70
|
-
apiContext.log.error(`Course not found: ${courseId}`);
|
|
71
|
-
process.exitCode = 1;
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
const progressData = {
|
|
75
|
-
courseId: course.id,
|
|
76
|
-
courseName: course.fullname,
|
|
77
|
-
progress: course.progress ?? 0,
|
|
78
|
-
startDate: course.startdate ? formatTimestamp(course.startdate) : null,
|
|
79
|
-
endDate: course.enddate ? formatTimestamp(course.enddate) : null,
|
|
80
|
-
};
|
|
81
|
-
formatAndOutput(progressData, output, apiContext.log);
|
|
82
|
-
});
|
|
83
|
-
// Helper function to fetch syllabus from CMAP using GWT-RPC API
|
|
84
|
-
async function fetchSyllabus(shortname) {
|
|
85
|
-
try {
|
|
86
|
-
const parts = shortname.split("_");
|
|
87
|
-
if (parts.length < 2) {
|
|
88
|
-
return { error: "Invalid course shortname format" };
|
|
89
|
-
}
|
|
90
|
-
const [yearTerm, opCode] = parts;
|
|
91
|
-
// Build GWT-RPC request body
|
|
92
|
-
// Format: 7|0|8|<base_url>|<permutation>|<service>|<method>|<param_types>|<params>...|1|2|3|4|3|5|5|5|6|7|8|
|
|
93
|
-
const gwtBody = `7|0|8|https://cmap.cycu.edu.tw:8443/Syllabus/syllabus/|339796D6E7B561A6465F5E9B5F4943FA|com.sanfong.syllabus.shared.SyllabusClientService|findClassTargetByYearAndOpCode|java.lang.String/2004016611|${yearTerm}|${opCode}|zh_TW|1|2|3|4|3|5|5|5|6|7|8|`;
|
|
94
|
-
const response = await fetch("https://cmap.cycu.edu.tw:8443/Syllabus/syllabus/syllabusClientService", {
|
|
95
|
-
method: "POST",
|
|
96
|
-
headers: {
|
|
97
|
-
"X-GWT-Permutation": "339796D6E7B561A6465F5E9B5F4943FA",
|
|
98
|
-
"Accept": "text/x-gwt-rpc, */*; q=0.01",
|
|
99
|
-
"Content-Type": "text/x-gwt-rpc; charset=UTF-8",
|
|
100
|
-
},
|
|
101
|
-
body: gwtBody,
|
|
102
|
-
});
|
|
103
|
-
if (!response.ok) {
|
|
104
|
-
return { error: `HTTP ${response.status}`, url: "https://cmap.cycu.edu.tw:8443/Syllabus/syllabus/syllabusClientService" };
|
|
105
|
-
}
|
|
106
|
-
const rawText = await response.text();
|
|
107
|
-
// GWT-RPC response format: //OK[...data...]
|
|
108
|
-
if (!rawText.startsWith("//OK")) {
|
|
109
|
-
return { error: "Invalid GWT-RPC response", rawResponse: rawText.slice(0, 200) };
|
|
110
|
-
}
|
|
111
|
-
// Extract the JSON array part from the GWT response
|
|
112
|
-
// Response format: //OK[data1,data2,...]
|
|
113
|
-
const content = rawText.slice(4); // Remove "//OK"
|
|
114
|
-
// Parse the GWT string table - GWT uses a special format where strings are escaped
|
|
115
|
-
// Format: ["string1","string2",...] or [123,"string2",...]
|
|
116
|
-
const stringTable = [];
|
|
117
|
-
// Simple parser for GWT string table
|
|
118
|
-
let current = "";
|
|
119
|
-
let inString = false;
|
|
120
|
-
let escaped = false;
|
|
121
|
-
for (let i = 0; i < content.length; i++) {
|
|
122
|
-
const char = content[i];
|
|
123
|
-
if (escaped) {
|
|
124
|
-
// Handle escape sequences
|
|
125
|
-
switch (char) {
|
|
126
|
-
case 'n':
|
|
127
|
-
current += '\n';
|
|
128
|
-
break;
|
|
129
|
-
case 'r':
|
|
130
|
-
current += '\r';
|
|
131
|
-
break;
|
|
132
|
-
case 't':
|
|
133
|
-
current += '\t';
|
|
134
|
-
break;
|
|
135
|
-
case '"':
|
|
136
|
-
current += '"';
|
|
137
|
-
break;
|
|
138
|
-
case '\\':
|
|
139
|
-
current += '\\';
|
|
140
|
-
break;
|
|
141
|
-
case '0':
|
|
142
|
-
current += '\0';
|
|
143
|
-
break;
|
|
144
|
-
default:
|
|
145
|
-
// Unknown escape, just append the char
|
|
146
|
-
current += char;
|
|
147
|
-
}
|
|
148
|
-
escaped = false;
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
if (char === "\\") {
|
|
152
|
-
escaped = true;
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
if (char === '"') {
|
|
156
|
-
inString = !inString;
|
|
157
|
-
if (!inString && current.length > 0) {
|
|
158
|
-
stringTable.push(current);
|
|
159
|
-
current = "";
|
|
160
|
-
}
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
if (inString) {
|
|
164
|
-
current += char;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
// Parse schedule from string table
|
|
168
|
-
// Strategy: Find week numbers (1-18), extract title (previous field) and date
|
|
169
|
-
const schedule = [];
|
|
170
|
-
const datePattern = /^\d{4}-\d{2}-\d{2}$/;
|
|
171
|
-
// Track processed indices to avoid duplicates
|
|
172
|
-
const processedIndices = new Set();
|
|
173
|
-
for (let i = 0; i < stringTable.length; i++) {
|
|
174
|
-
const s = stringTable[i];
|
|
175
|
-
// Look for week numbers (1-18)
|
|
176
|
-
if (/^[1-9]$|^1[0-8]$/.test(s) && !processedIndices.has(i)) {
|
|
177
|
-
const week = s;
|
|
178
|
-
let date = "";
|
|
179
|
-
let title = "";
|
|
180
|
-
// Previous field is the title
|
|
181
|
-
if (i - 1 >= 0 && !processedIndices.has(i - 1)) {
|
|
182
|
-
title = stringTable[i - 1];
|
|
183
|
-
}
|
|
184
|
-
// For week 1 & 2, find date before the week number
|
|
185
|
-
// For week 18, also look before (last week has no "next week")
|
|
186
|
-
// For other weeks (3-17), next field is next week's date
|
|
187
|
-
if (week === "1" || week === "2" || week === "18") {
|
|
188
|
-
// Look backwards for date pattern (search further back for week 18)
|
|
189
|
-
const maxLookback = week === "18" ? 15 : 6;
|
|
190
|
-
for (let j = i - 1; j >= Math.max(0, i - maxLookback); j--) {
|
|
191
|
-
if (datePattern.test(stringTable[j]) && !processedIndices.has(j)) {
|
|
192
|
-
date = stringTable[j];
|
|
193
|
-
processedIndices.add(j);
|
|
194
|
-
break;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
else {
|
|
199
|
-
// Week 3-17: look for next week number, then get date before it
|
|
200
|
-
for (let j = i + 1; j < Math.min(i + 10, stringTable.length); j++) {
|
|
201
|
-
if (/^[1-9]$|^1[0-8]$/.test(stringTable[j]) && !processedIndices.has(j)) {
|
|
202
|
-
// Found next week, look before it for date
|
|
203
|
-
for (let k = j - 1; k >= Math.max(0, j - 6); k--) {
|
|
204
|
-
if (datePattern.test(stringTable[k]) && !processedIndices.has(k)) {
|
|
205
|
-
date = stringTable[k];
|
|
206
|
-
processedIndices.add(k);
|
|
207
|
-
break;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
break;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
// Clean up title
|
|
215
|
-
title = title.trim()
|
|
216
|
-
.replace(/[\r\n]+/g, ' ')
|
|
217
|
-
.replace(/,+$/, '')
|
|
218
|
-
.trim()
|
|
219
|
-
.slice(0, 200);
|
|
220
|
-
// Only add if we have title (date is optional, will be inferred if missing)
|
|
221
|
-
if (title.length > 1) {
|
|
222
|
-
// If no date found, try to infer from the last added date
|
|
223
|
-
if (date.length === 0 && schedule.length > 0) {
|
|
224
|
-
const lastEntry = schedule[schedule.length - 1];
|
|
225
|
-
const lastDate = new Date(lastEntry.date);
|
|
226
|
-
const nextDate = new Date(lastDate.getTime() + 7 * 24 * 60 * 60 * 1000);
|
|
227
|
-
const year = nextDate.getFullYear();
|
|
228
|
-
const month = String(nextDate.getMonth() + 1).padStart(2, "0");
|
|
229
|
-
const day = String(nextDate.getDate()).padStart(2, "0");
|
|
230
|
-
date = `${year}-${month}-${day}`;
|
|
231
|
-
}
|
|
232
|
-
schedule.push({
|
|
233
|
-
week,
|
|
234
|
-
date,
|
|
235
|
-
title,
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
processedIndices.add(i);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
// Sort by date to maintain order
|
|
242
|
-
schedule.sort((a, b) => a.date.localeCompare(b.date));
|
|
243
|
-
// Extract course info from string table
|
|
244
|
-
const result = {
|
|
245
|
-
yearTerm,
|
|
246
|
-
opCode,
|
|
247
|
-
url: `https://cmap.cycu.edu.tw:8443/Syllabus/CoursePreview.html?yearTerm=${yearTerm}&opCode=${opCode}&locale=zh_TW`,
|
|
248
|
-
schedule,
|
|
249
|
-
};
|
|
250
|
-
// Try to find instructor (look for common patterns)
|
|
251
|
-
for (let i = 0; i < stringTable.length; i++) {
|
|
252
|
-
const s = stringTable[i];
|
|
253
|
-
if (s.includes("教授") || s.includes("老師") || s.includes("教師") || s.includes("Instructor")) {
|
|
254
|
-
result.instructor = s;
|
|
255
|
-
break;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
return result;
|
|
259
|
-
}
|
|
260
|
-
catch (e) {
|
|
261
|
-
return { error: e instanceof Error ? e.message : String(e) };
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
coursesCmd
|
|
265
|
-
.command("syllabus")
|
|
266
|
-
.description("Show course syllabus (from CMAP)")
|
|
267
|
-
.argument("<course-id>", "Course ID")
|
|
268
|
-
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
269
|
-
.action(async (courseId, options, command) => {
|
|
270
|
-
const output = getOutputFormat(command);
|
|
271
|
-
const apiContext = await createApiContext(options, command);
|
|
272
|
-
if (!apiContext) {
|
|
273
|
-
process.exitCode = 1;
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
try {
|
|
277
|
-
const courses = await getEnrolledCoursesApi(apiContext.session);
|
|
278
|
-
const course = courses.find(c => c.id === parseInt(courseId, 10));
|
|
279
|
-
if (!course) {
|
|
280
|
-
apiContext.log.error(`Course not found: ${courseId}`);
|
|
281
|
-
process.exitCode = 1;
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
// Fetch syllabus from CMAP
|
|
285
|
-
const syllabus = await fetchSyllabus(course.shortname);
|
|
286
|
-
if (!syllabus) {
|
|
287
|
-
apiContext.log.warn(`Syllabus not found for course: ${course.shortname}`);
|
|
288
|
-
// Return course info at least
|
|
289
|
-
formatAndOutput({
|
|
290
|
-
courseId: course.id,
|
|
291
|
-
shortname: course.shortname,
|
|
292
|
-
fullname: course.fullname,
|
|
293
|
-
note: "Syllabus not available from CMAP",
|
|
294
|
-
}, output, apiContext.log);
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
// Combine course info with syllabus
|
|
298
|
-
const result = {
|
|
299
|
-
courseId: course.id,
|
|
300
|
-
shortname: course.shortname,
|
|
301
|
-
fullname: course.fullname,
|
|
302
|
-
...syllabus,
|
|
303
|
-
};
|
|
304
|
-
formatAndOutput(result, output, apiContext.log);
|
|
305
|
-
}
|
|
306
|
-
catch (e) {
|
|
307
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
308
|
-
apiContext.log.error(`Error fetching syllabus: ${msg}`);
|
|
309
|
-
process.exitCode = 1;
|
|
310
|
-
}
|
|
311
|
-
});
|
|
312
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
import { stripHtmlTags, getOutputFormat, formatTimestamp } from "../lib/utils.js";
|
|
2
|
-
import { getEnrolledCoursesApi, getForumsApi, getForumDiscussionsApi, getDiscussionPostsApi, addForumDiscussionApi, addForumPostApi, deleteForumPostApi, resolveForumId } from "../lib/moodle.js";
|
|
3
|
-
import { createApiContext } from "../lib/auth.js";
|
|
4
|
-
import { formatAndOutput } from "../index.js";
|
|
5
|
-
export function registerForumsCommand(program) {
|
|
6
|
-
const forumsCmd = program.command("forums");
|
|
7
|
-
forumsCmd.description("Forum operations");
|
|
8
|
-
async function listForums(classification) {
|
|
9
|
-
const apiContext = await createApiContext({});
|
|
10
|
-
if (!apiContext) {
|
|
11
|
-
process.exitCode = 1;
|
|
12
|
-
return;
|
|
13
|
-
}
|
|
14
|
-
const courses = await getEnrolledCoursesApi(apiContext.session, {
|
|
15
|
-
classification,
|
|
16
|
-
});
|
|
17
|
-
const courseIds = courses.map(c => c.id);
|
|
18
|
-
const wsForums = await getForumsApi(apiContext.session, courseIds);
|
|
19
|
-
const courseMap = new Map(courses.map(c => [c.id, c]));
|
|
20
|
-
const allForums = [];
|
|
21
|
-
for (const wsForum of wsForums) {
|
|
22
|
-
const course = courseMap.get(wsForum.courseid);
|
|
23
|
-
if (course) {
|
|
24
|
-
allForums.push({
|
|
25
|
-
course_id: wsForum.courseid,
|
|
26
|
-
course_name: course.fullname,
|
|
27
|
-
intro: wsForum.intro,
|
|
28
|
-
cmid: wsForum.cmid.toString(),
|
|
29
|
-
forum_id: wsForum.id,
|
|
30
|
-
name: wsForum.name,
|
|
31
|
-
timemodified: wsForum.timemodified,
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
formatAndOutput(allForums, "json", apiContext.log, { status: "success", timestamp: new Date().toISOString(), total_courses: courses.length, total_forums: allForums.length });
|
|
36
|
-
}
|
|
37
|
-
forumsCmd
|
|
38
|
-
.command("list")
|
|
39
|
-
.description("List forums from in-progress courses")
|
|
40
|
-
.action(() => listForums("inprogress"));
|
|
41
|
-
forumsCmd
|
|
42
|
-
.command("list-all")
|
|
43
|
-
.description("List all forums across all courses")
|
|
44
|
-
.option("--level <type>", "Course level: in_progress (default) | all", "in_progress")
|
|
45
|
-
.action(async (options) => {
|
|
46
|
-
const classification = options.level === "all" ? undefined : "inprogress";
|
|
47
|
-
await listForums(classification);
|
|
48
|
-
});
|
|
49
|
-
forumsCmd
|
|
50
|
-
.command("discussions")
|
|
51
|
-
.description("List discussions in a forum (use forum ID)")
|
|
52
|
-
.argument("<forum-id>", "Forum ID")
|
|
53
|
-
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
54
|
-
.action(async (forumId, options, command) => {
|
|
55
|
-
const output = getOutputFormat(command);
|
|
56
|
-
const apiContext = await createApiContext(options, command);
|
|
57
|
-
if (!apiContext) {
|
|
58
|
-
process.exitCode = 1;
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
const resolved = await resolveForumId(apiContext.session, forumId);
|
|
62
|
-
if (!resolved) {
|
|
63
|
-
apiContext.log.error("Forum not found");
|
|
64
|
-
process.exitCode = 1;
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
const discussions = await getForumDiscussionsApi(apiContext.session, resolved.forumId);
|
|
68
|
-
const items = discussions.map(d => ({
|
|
69
|
-
id: d.id,
|
|
70
|
-
name: d.name,
|
|
71
|
-
user_id: d.userId,
|
|
72
|
-
time_modified: d.timeModified,
|
|
73
|
-
post_count: d.postCount,
|
|
74
|
-
unread: d.unread,
|
|
75
|
-
message: stripHtmlTags(d.message || ""),
|
|
76
|
-
}));
|
|
77
|
-
formatAndOutput(items, output, apiContext.log, { status: "success", timestamp: new Date().toISOString(), forum_id: resolved.forumId, forum_name: resolved.name ?? null, course_id: resolved.courseid ?? null, total_discussions: discussions.length });
|
|
78
|
-
});
|
|
79
|
-
forumsCmd
|
|
80
|
-
.command("posts")
|
|
81
|
-
.description("Show posts in a discussion")
|
|
82
|
-
.argument("<discussion-id>", "Discussion ID")
|
|
83
|
-
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
84
|
-
.action(async (discussionId, options, command) => {
|
|
85
|
-
const output = getOutputFormat(command);
|
|
86
|
-
const apiContext = await createApiContext(options, command);
|
|
87
|
-
if (!apiContext) {
|
|
88
|
-
process.exitCode = 1;
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
const posts = await getDiscussionPostsApi(apiContext.session, parseInt(discussionId, 10));
|
|
92
|
-
const items = posts.map(p => ({
|
|
93
|
-
id: p.id,
|
|
94
|
-
subject: p.subject,
|
|
95
|
-
author: p.author,
|
|
96
|
-
author_id: p.authorId,
|
|
97
|
-
created: formatTimestamp(p.created),
|
|
98
|
-
modified: formatTimestamp(p.modified),
|
|
99
|
-
message: p.message,
|
|
100
|
-
unread: p.unread,
|
|
101
|
-
}));
|
|
102
|
-
formatAndOutput(items, output, apiContext.log, { status: "success", timestamp: new Date().toISOString(), discussion_id: discussionId, total_posts: posts.length });
|
|
103
|
-
});
|
|
104
|
-
forumsCmd
|
|
105
|
-
.command("post")
|
|
106
|
-
.description("Post a new discussion to a forum")
|
|
107
|
-
.argument("<forum-id>", "Forum ID")
|
|
108
|
-
.argument("<subject>", "Discussion subject")
|
|
109
|
-
.argument("<message>", "Discussion message")
|
|
110
|
-
.option("--subscribe", "Subscribe to the discussion", false)
|
|
111
|
-
.option("--pin", "Pin the discussion", false)
|
|
112
|
-
.action(async (forumId, subject, message, options, command) => {
|
|
113
|
-
const apiContext = await createApiContext(options, command);
|
|
114
|
-
if (!apiContext) {
|
|
115
|
-
process.exitCode = 1;
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
const { log, session } = apiContext;
|
|
119
|
-
const resolved = await resolveForumId(session, forumId);
|
|
120
|
-
if (!resolved) {
|
|
121
|
-
log.error(`Forum not found: ${forumId}`);
|
|
122
|
-
process.exitCode = 1;
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
log.info(`Posting to forum: ${resolved.name ?? forumId}`);
|
|
126
|
-
const result = await addForumDiscussionApi(session, resolved.forumId, subject, message);
|
|
127
|
-
if (result.success) {
|
|
128
|
-
log.success(`✓ Discussion posted successfully!`);
|
|
129
|
-
log.info(` Discussion ID: ${result.discussionId}`);
|
|
130
|
-
}
|
|
131
|
-
else {
|
|
132
|
-
log.error(`✗ Failed to post discussion: ${result.error}`);
|
|
133
|
-
process.exitCode = 1;
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
forumsCmd
|
|
137
|
-
.command("reply")
|
|
138
|
-
.description("Reply to a discussion post")
|
|
139
|
-
.argument("<post-id>", "Parent post ID to reply to")
|
|
140
|
-
.argument("<subject>", "Reply subject")
|
|
141
|
-
.argument("<message>", "Reply message")
|
|
142
|
-
.option("--attachment-id <id>", "Draft file ID for attachment")
|
|
143
|
-
.option("--inline-attachment-id <id>", "Draft file ID for inline attachment")
|
|
144
|
-
.action(async (postId, subject, message, options, command) => {
|
|
145
|
-
const apiContext = await createApiContext(options, command);
|
|
146
|
-
if (!apiContext) {
|
|
147
|
-
process.exitCode = 1;
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
const { log, session } = apiContext;
|
|
151
|
-
log.info(`Replying to post: ${postId}`);
|
|
152
|
-
log.info(` Subject: ${subject}`);
|
|
153
|
-
log.info(` Message: ${message}`);
|
|
154
|
-
if (options.attachmentId) {
|
|
155
|
-
log.info(` Attachment ID: ${options.attachmentId}`);
|
|
156
|
-
}
|
|
157
|
-
const result = await addForumPostApi(session, parseInt(postId, 10), subject, message, {
|
|
158
|
-
attachmentId: options.attachmentId ? parseInt(options.attachmentId, 10) : undefined,
|
|
159
|
-
inlineAttachmentId: options.inlineAttachmentId ? parseInt(options.inlineAttachmentId, 10) : undefined,
|
|
160
|
-
});
|
|
161
|
-
if (result.success) {
|
|
162
|
-
log.success(`✓ Reply posted successfully!`);
|
|
163
|
-
log.info(` Post ID: ${result.postId}`);
|
|
164
|
-
}
|
|
165
|
-
else {
|
|
166
|
-
log.error(`✗ Failed to post reply: ${result.error}`);
|
|
167
|
-
process.exitCode = 1;
|
|
168
|
-
}
|
|
169
|
-
});
|
|
170
|
-
forumsCmd
|
|
171
|
-
.command("delete")
|
|
172
|
-
.description("Delete a forum post or discussion (by post ID)")
|
|
173
|
-
.argument("<post-id>", "Post ID to delete (deletes entire discussion if it's the first post)")
|
|
174
|
-
.action(async (postId, options, command) => {
|
|
175
|
-
const apiContext = await createApiContext(options, command);
|
|
176
|
-
if (!apiContext) {
|
|
177
|
-
process.exitCode = 1;
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
const { log, session } = apiContext;
|
|
181
|
-
const result = await deleteForumPostApi(session, parseInt(postId, 10));
|
|
182
|
-
if (result.success) {
|
|
183
|
-
log.success(`✓ Post ${postId} deleted successfully!`);
|
|
184
|
-
}
|
|
185
|
-
else {
|
|
186
|
-
log.error(`✗ Failed to delete post: ${result.error}`);
|
|
187
|
-
process.exitCode = 1;
|
|
188
|
-
}
|
|
189
|
-
});
|
|
190
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"grades.d.ts","sourceRoot":"","sources":["../../../src/src/commands/grades.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAgBpC,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA+F5D"}
|