This repository has been archived by the owner on Dec 18, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.ts
204 lines (182 loc) · 6.11 KB
/
main.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
import {
dayjs,
Hono,
ical,
ICalCalendarMethod,
outdent,
StudentClient,
} from "./deps.ts";
import { checkAndAddRateLimit } from "./kv.ts";
function getFilename(url: string, dob: string) {
const requestedUrl = new URL(url);
let filename = requestedUrl.pathname.split("/").at(-1);
filename = filename === dob ? "calendar.ics" : filename;
return filename;
}
const dateOfBirthRegex = /^[0-9]{1,2}\-[0-9]{1,2}\-[0-9]{4}$/;
const app = new Hono();
app.get("/", async (c) => {
const url = new URL(c.req.url);
const currentUrl = `${url.protocol}//${url.hostname}${
url.port.length > 0 && url.port !== "80" && url.port !== "443"
? `:${url.port}`
: ""
}`;
const banner = await Deno.readTextFile("banner.txt");
return c.text(outdent`
${banner}
--- Endpoints ---
- Timetable2ICal: ${currentUrl}/v2/timetable/classchartsCode/dateOfBirth/calendar.ics
- Homework2ICal: ${currentUrl}/v2/homework/classchartsCode/dateOfBirth/calendar.ics
--- Notes ---
Make sure to replace the classchartsCode and dateOfBirth with your own details.
dateOfBirth should be in format: DD-MM-YYYY
Timetable2Ical returns lessons a week prior, and 32 days after the current date (${
dayjs().format(
"DD/MM/YYYY",
)
}).
Homework2ICal returns homework 32 days prior and a year in advance of the current date (${
dayjs().format(
"DD/MM/YYYY",
)
}).
These limits are due to having to request timetable days individually, whereas homework can be requested in a single request.
Feel free to modify the code and host your own instance to alter these limits.
--- Rate Limits ---
10 requests per endpoint, per hour. Limited by a hash of your ClassCharts code & date of birth.
--- Privacy ---
The only data which is collected is your classcharts code and date of birth (both hashed via Argon2) for the purpose of rate limiting.
If you are worried about privacy, it's super easy to host your own instance, see the source link below.
--- Source ---
The source code is avaliable to host yourself at: https://github.com/jamesatjaminit/classcharts-to-ical
And can easily be deployed to Deno Deploy without any configuration.
--- Contact ---
https://jamesatjaminit.dev
`);
});
app.options("*", (c) => {
return c.text("", 200, {
Allow: "OPTIONS, GET",
"Access-Control-Request-Method": "GET",
"Access-Control-Request-Headers": "Content-Type, Content-Disposition",
"Access-Control-Allow-Origin": "*",
});
});
app.get("/v2/timetable/:code/:dob/*", async (c) => {
const code = c.req.param("code");
const dob = c.req.param("dob");
if (dateOfBirthRegex.test(dob) === false) {
return c.text("Invalid date of birth. Should be in format DD-MM-YYYY", 400);
}
if (
(await checkAndAddRateLimit(
"timetable",
code + "/" + dob,
10,
60 * 60 * 1000,
)) === false
) {
return c.text("Rate limited. Check the home page for details.", {
status: 429,
});
}
const classchartsClient = new StudentClient(code, dob.replaceAll("-", "/"));
try {
await classchartsClient.login();
} catch {
return c.text("Failed to authenticate with ClassCharts", 400);
}
const calendar = ical({ name: "ClassCharts Timetable" });
calendar.method(ICalCalendarMethod.REQUEST);
const currentDay = dayjs().subtract(7, "day");
for (let i = 1; i <= 40; i++) {
try {
const timetableForDay = await classchartsClient.getLessons({
date: currentDay.add(i, "day").format("YYYY-MM-DD"),
});
for (const lesson of timetableForDay.data) {
calendar.createEvent({
start: dayjs(lesson.start_time).toDate(),
end: dayjs(lesson.end_time).toDate(),
summary: `${lesson.lesson_name} - ${lesson.room_name}`,
description: outdent`
Teacher Name: ${lesson.teacher_name}
Subject: ${lesson.subject_name}
Synced At: ${dayjs().toString()}
`,
});
}
} catch {
// Shushhhhh
}
}
const filename = getFilename(c.req.url, dob);
return c.text(calendar.toString(), 200, {
"Content-Type": "text/calendar; charset=utf-8",
"Content-Disposition": `attachment; filename="${filename}"`,
});
});
app.get("/v2/homework/:code/:dob/*", async (c) => {
const code = c.req.param("code");
const dob = c.req.param("dob");
if (dateOfBirthRegex.test(dob) === false) {
return c.text("Invalid date of birth. Should be in format DD-MM-YYYY", 400);
}
if (
(await checkAndAddRateLimit(
"homework",
code + "/" + dob,
10,
60 * 60 * 1000,
)) === false
) {
return c.text("Rate limited. Check the home page for details.", {
status: 429,
});
}
const classchartsClient = new StudentClient(code, dob.replaceAll("-", "/"));
try {
await classchartsClient.login();
} catch {
return c.text("Failed to authenticate with ClassCharts", 400);
}
const homeworks = (
await classchartsClient.getHomeworks({
from: dayjs().subtract(32, "day").format("YYYY-MM-DD"),
to: dayjs().add(366, "day").format("YYYY-MM-DD"),
displayDate: "due_date",
})
).data;
const calendar = ical({ name: "ClassCharts Homeworks" });
calendar.method(ICalCalendarMethod.REQUEST);
for (const homework of homeworks) {
let status = "TODO";
if (homework.status.state === "completed") {
status = "SUBMITTED";
} else if (homework.status.ticked === "yes") {
status = "TICKED";
}
calendar.createEvent({
start: dayjs(homework.due_date).toDate(),
summary: homework.title,
description: outdent`
Subject: ${homework.subject}
Teacher: ${homework.teacher}
Issue Date: ${dayjs(homework.issue_date).toString()}
Status: ${status}
Synced At: ${dayjs().toString()}
More Info: https://www.classcharts.com/mobile/student#${classchartsClient.studentId},homework,${homework.id}
Description:
${homework.description.replace(/<[^>]*>?/gm, "").trim()}
`,
allDay: true,
});
}
const filename = getFilename(c.req.url, dob);
return c.text(calendar.toString(), 200, {
"Content-Type": "text/calendar; charset=utf-8",
"Content-Disposition": `attachment; filename="${filename}"`,
});
});
Deno.serve(app.fetch);