馃 distributed transcription service
thistle.dunkirk.sh
1import { nanoid } from "nanoid";
2import db from "../db/schema";
3
4export interface Class {
5 id: string;
6 course_code: string;
7 name: string;
8 professor: string;
9 semester: string;
10 year: number;
11 archived: boolean;
12 created_at: number;
13}
14
15export interface MeetingTime {
16 id: string;
17 class_id: string;
18 label: string;
19 created_at: number;
20}
21
22export interface ClassMember {
23 class_id: string;
24 user_id: number;
25 enrolled_at: number;
26}
27
28/**
29 * Get all classes for a user (either enrolled or admin sees all)
30 */
31export function getClassesForUser(userId: number, isAdmin: boolean): Class[] {
32 if (isAdmin) {
33 return db
34 .query<Class, []>(
35 "SELECT * FROM classes ORDER BY year DESC, semester DESC, course_code ASC",
36 )
37 .all();
38 }
39
40 return db
41 .query<Class, [number]>(
42 `SELECT c.* FROM classes c
43 INNER JOIN class_members cm ON c.id = cm.class_id
44 WHERE cm.user_id = ?
45 ORDER BY c.year DESC, c.semester DESC, c.course_code ASC`,
46 )
47 .all(userId);
48}
49
50/**
51 * Get a single class by ID
52 */
53export function getClassById(classId: string): Class | null {
54 const result = db
55 .query<Class, [string]>("SELECT * FROM classes WHERE id = ?")
56 .get(classId);
57 return result ?? null;
58}
59
60/**
61 * Check if user is enrolled in a class
62 */
63export function isUserEnrolledInClass(
64 userId: number,
65 classId: string,
66): boolean {
67 const result = db
68 .query<{ count: number }, [string, number]>(
69 "SELECT COUNT(*) as count FROM class_members WHERE class_id = ? AND user_id = ?",
70 )
71 .get(classId, userId);
72 return (result?.count ?? 0) > 0;
73}
74
75/**
76 * Create a new class
77 */
78export function createClass(data: {
79 course_code: string;
80 name: string;
81 professor: string;
82 semester: string;
83 year: number;
84 meeting_times?: string[];
85}): Class {
86 const id = nanoid();
87 const now = Math.floor(Date.now() / 1000);
88
89 db.run(
90 "INSERT INTO classes (id, course_code, name, professor, semester, year, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
91 [
92 id,
93 data.course_code,
94 data.name,
95 data.professor,
96 data.semester,
97 data.year,
98 now,
99 ],
100 );
101
102 // Create meeting times if provided
103 if (data.meeting_times && data.meeting_times.length > 0) {
104 for (const label of data.meeting_times) {
105 createMeetingTime(id, label);
106 }
107 }
108
109 return {
110 id,
111 course_code: data.course_code,
112 name: data.name,
113 professor: data.professor,
114 semester: data.semester,
115 year: data.year,
116 archived: false,
117 created_at: now,
118 };
119}
120
121/**
122 * Archive or unarchive a class
123 */
124export function toggleClassArchive(classId: string, archived: boolean): void {
125 db.run("UPDATE classes SET archived = ? WHERE id = ?", [
126 archived ? 1 : 0,
127 classId,
128 ]);
129}
130
131/**
132 * Delete a class (cascades to members, meeting times, and transcriptions)
133 */
134export function deleteClass(classId: string): void {
135 db.run("DELETE FROM classes WHERE id = ?", [classId]);
136}
137
138/**
139 * Enroll a user in a class
140 */
141export function enrollUserInClass(userId: number, classId: string): void {
142 const now = Math.floor(Date.now() / 1000);
143 db.run(
144 "INSERT OR IGNORE INTO class_members (class_id, user_id, enrolled_at) VALUES (?, ?, ?)",
145 [classId, userId, now],
146 );
147}
148
149/**
150 * Remove a user from a class
151 */
152export function removeUserFromClass(userId: number, classId: string): void {
153 db.run("DELETE FROM class_members WHERE class_id = ? AND user_id = ?", [
154 classId,
155 userId,
156 ]);
157}
158
159/**
160 * Get all members of a class
161 */
162export function getClassMembers(classId: string) {
163 return db
164 .query<
165 {
166 user_id: number;
167 email: string;
168 name: string | null;
169 avatar: string;
170 enrolled_at: number;
171 },
172 [string]
173 >(
174 `SELECT cm.user_id, u.email, u.name, u.avatar, cm.enrolled_at
175 FROM class_members cm
176 INNER JOIN users u ON cm.user_id = u.id
177 WHERE cm.class_id = ?
178 ORDER BY cm.enrolled_at DESC`,
179 )
180 .all(classId);
181}
182
183/**
184 * Create a meeting time for a class
185 */
186export function createMeetingTime(classId: string, label: string): MeetingTime {
187 const id = nanoid();
188 const now = Math.floor(Date.now() / 1000);
189
190 db.run(
191 "INSERT INTO meeting_times (id, class_id, label, created_at) VALUES (?, ?, ?, ?)",
192 [id, classId, label, now],
193 );
194
195 return {
196 id,
197 class_id: classId,
198 label,
199 created_at: now,
200 };
201}
202
203/**
204 * Get all meeting times for a class
205 */
206export function getMeetingTimesForClass(classId: string): MeetingTime[] {
207 return db
208 .query<MeetingTime, [string]>(
209 "SELECT * FROM meeting_times WHERE class_id = ? ORDER BY created_at ASC",
210 )
211 .all(classId);
212}
213
214/**
215 * Update a meeting time label
216 */
217export function updateMeetingTime(meetingId: string, label: string): void {
218 db.run("UPDATE meeting_times SET label = ? WHERE id = ?", [label, meetingId]);
219}
220
221/**
222 * Delete a meeting time
223 */
224export function deleteMeetingTime(meetingId: string): void {
225 db.run("DELETE FROM meeting_times WHERE id = ?", [meetingId]);
226}
227
228/**
229 * Get transcriptions for a class
230 */
231export function getTranscriptionsForClass(classId: string) {
232 return db
233 .query<
234 {
235 id: string;
236 user_id: number;
237 meeting_time_id: string | null;
238 filename: string;
239 original_filename: string;
240 status: string;
241 progress: number;
242 error_message: string | null;
243 created_at: number;
244 updated_at: number;
245 },
246 [string]
247 >(
248 `SELECT id, user_id, meeting_time_id, filename, original_filename, status, progress, error_message, created_at, updated_at
249 FROM transcriptions
250 WHERE class_id = ?
251 ORDER BY created_at DESC`,
252 )
253 .all(classId);
254}
255
256/**
257 * Search for classes by course code
258 */
259export function searchClassesByCourseCode(courseCode: string): Class[] {
260 return db
261 .query<Class, [string]>(
262 `SELECT * FROM classes
263 WHERE UPPER(course_code) LIKE UPPER(?)
264 AND archived = 0
265 ORDER BY year DESC, semester DESC, professor ASC`,
266 )
267 .all(`%${courseCode}%`);
268}
269
270/**
271 * Join a class by class ID
272 */
273export function joinClass(
274 classId: string,
275 userId: number,
276): { success: boolean; error?: string } {
277 // Find class by ID
278 const cls = db
279 .query<Class, [string]>("SELECT * FROM classes WHERE id = ?")
280 .get(classId);
281
282 if (!cls) {
283 return { success: false, error: "Class not found" };
284 }
285
286 if (cls.archived) {
287 return { success: false, error: "This class is archived" };
288 }
289
290 // Check if already enrolled
291 const existing = db
292 .query<ClassMember, [string, number]>(
293 "SELECT * FROM class_members WHERE class_id = ? AND user_id = ?",
294 )
295 .get(cls.id, userId);
296
297 if (existing) {
298 return { success: false, error: "Already enrolled in this class" };
299 }
300
301 // Enroll user
302 db.query(
303 "INSERT INTO class_members (class_id, user_id, enrolled_at) VALUES (?, ?, ?)",
304 ).run(cls.id, userId, Math.floor(Date.now() / 1000));
305
306 return { success: true };
307}
308
309/**
310 * Waitlist entry interface
311 */
312export interface WaitlistEntry {
313 id: string;
314 user_id: number;
315 course_code: string;
316 course_name: string;
317 professor: string;
318 semester: string;
319 year: number;
320 additional_info: string | null;
321 meeting_times: string | null;
322 created_at: number;
323}
324
325/**
326 * Add a class to the waitlist
327 */
328export function addToWaitlist(
329 userId: number,
330 courseCode: string,
331 courseName: string,
332 professor: string,
333 semester: string,
334 year: number,
335 additionalInfo: string | null,
336 meetingTimes: string[] | null,
337): string {
338 const id = nanoid();
339 const meetingTimesJson = meetingTimes ? JSON.stringify(meetingTimes) : null;
340
341 db.query(
342 `INSERT INTO class_waitlist
343 (id, user_id, course_code, course_name, professor, semester, year, additional_info, meeting_times, created_at)
344 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
345 ).run(
346 id,
347 userId,
348 courseCode,
349 courseName,
350 professor,
351 semester,
352 year,
353 additionalInfo,
354 meetingTimesJson,
355 Math.floor(Date.now() / 1000),
356 );
357 return id;
358}
359
360/**
361 * Get all waitlist entries
362 */
363export function getAllWaitlistEntries(): WaitlistEntry[] {
364 return db
365 .query<WaitlistEntry, []>(
366 "SELECT * FROM class_waitlist ORDER BY created_at DESC",
367 )
368 .all();
369}
370
371/**
372 * Delete a waitlist entry
373 */
374export function deleteWaitlistEntry(id: string): void {
375 db.query("DELETE FROM class_waitlist WHERE id = ?").run(id);
376}