import Prisma from "@prisma/client"; import axios from "axios"; import { log, getLogPath } from "../logs.js"; import { getAuthtoken, getTimetables } from "./dsbmobile.js"; import { parseSubstitutionPlan } from "./untis.js"; import fs from "fs"; const prisma = new Prisma.PrismaClient(); const dsbFiles = ["Schüler_Monitor - subst_001", "Schüler Morgen - subst_001"]; export class Parser { dsbUser; dsbPassword; constructor(dsbUser, dsbPassword, interval) { this.dsbUser = dsbUser; this.dsbPassword = dsbPassword; setInterval(() => this.updatePlan(), interval); this.updatePlan(); } async updatePlan() { const startedAt = new Date(); try { const data = await this.fetchDSB(); if (!data) { throw "DSB request failed!"; } const plans = []; for (const entry of data) { const data = await this.fetchFile(entry.url); const parsed = parseSubstitutionPlan(data); plans.push(parsed); } const parseEvent = await prisma.parseEvent.create({ data: { logFile: getLogPath(), originalData: "", duration: new Date() - startedAt, succeeded: true, }, }); for (const plan of plans) { await this.insertSubstitutions(plan, parseEvent); } } catch (error) { await prisma.parseEvent.create({ data: { logFile: getLogPath(), originalData: error.toString(), duration: new Date() - startedAt, succeeded: false, }, }); log("Parser / Main", "Parse event failed: " + error); } } async fetchDSB() { try { const token = await getAuthtoken(this.dsbUser, this.dsbPassword); const response = await getTimetables(token); const timetables = response.filter((e) => dsbFiles.includes(e.title)); return timetables; } catch (error) { log("Parser / DSB Mobile", "Error getting data: " + error); } return false; } async fetchFile(url) { const result = await axios.request({ method: "GET", url: url, responseEncoding: "binary", }); return result.data; } async parsePlan(html) { return parseSubstitutionPlan(html); } async insertSubstitutions(parsedData, parseEvent) { const { updatedAt, date, changes } = parsedData; const classList = await prisma.class.findMany(); const knownSubstitutions = await prisma.substitution.findMany({ where: { date: new Date(new Date(date).setUTCHours(0, 0, 0, 0)), removed: false, }, }); for (const change of changes) { const classes = this.getSubstitutionClasses(classList, change.class); if (classes.length == 0) classes.push(change.class || "unknown"); // Workaround no currect match possible for subsitutions of this // type beacuse they don't have a class and a subject attribute if (change.type == "Sondereins." && !change.subject) { change.subject = change.notes; } const matchingSubstitutionId = knownSubstitutions.findIndex( (substitution) => { return substitution.date == new Date(date).setUTCHours(0, 0, 0, 0) && substitution.type == (change.type == "Entfall") ? "cancellation" : "change" && substitution.lesson == change.lesson && classes.sort().join(",") == substitution.class.sort().join(",") && substitution.changedSubject == change.subject; } ); const matchingSubstitution = knownSubstitutions[matchingSubstitutionId]; if (!matchingSubstitution) { const newSubstitution = await prisma.substitution.create({ data: { class: classes, date: new Date(date), type: change.type == "Entfall" ? "cancellation" : "change", lesson: parseInt(change.lesson), changedTeacher: change.changedTeacher, changedRoom: change.room || undefined, changedSubject: change.subject, notes: change.notes, removed: false, }, }); const substitutionChange = await prisma.substitutionChange.create({ data: { substitutionId: newSubstitution.id, type: "addition", changes: { class: classes, type: change.type == "Entfall" ? "cancellation" : "change", lesson: parseInt(change.lesson), date: new Date(date), change: { teacher: change.changedTeacher, room: change.room || undefined, subject: change.subject, }, }, parseEventId: parseEvent.id, }, }); log( "Insert / DB", `Created new substitution: S:${newSubstitution.id} C:${substitutionChange.id}` ); } else { const differences = this.findDifferences(matchingSubstitution, change); if (Object.keys(differences).length > 0) { const prismaOptions = { where: { id: matchingSubstitution.id, }, data: {}, }; if (differences.teacher) prismaOptions.data.changedTeacher = change.changedTeacher; if (differences.room) prismaOptions.data.changedRoom = change.room; if (differences.notes) prismaOptions.data.notes = change.notes; await prisma.substitution.update(prismaOptions); const substitutionChange = await prisma.substitutionChange.create({ data: { substitutionId: matchingSubstitution.id, type: "change", changes: differences, parseEventId: parseEvent.id, }, }); log( "Insert / DB", `Found changed substitution: S:${matchingSubstitution.id} C:${substitutionChange.id}` ); } else { log( "Insert / DB", `Substitution unchanged: S:${matchingSubstitution.id}` ); } knownSubstitutions.splice(matchingSubstitutionId, 1); } } for (const remainingSubstitution of knownSubstitutions) { await prisma.substitution.update({ where: { id: remainingSubstitution.id, }, data: { removed: true, }, }); const substitutionChange = await prisma.substitutionChange.create({ data: { substitutionId: remainingSubstitution.id, type: "deletion", changes: {}, parseEventId: parseEvent.id, }, }); log( "Insert / DB", `Deleted removed substitution: S:${remainingSubstitution.id} C:${substitutionChange.id}` ); } } getSubstitutionClasses(classList, classString) { const matchingClasses = classList.filter((element) => { const regex = new RegExp(element.regex); return (classString || "").toLowerCase().match(regex); }); return matchingClasses.map((e) => e.name); } findDifferences(currentSubstitution, newChange) { const differences = {}; if (newChange.changedTeacher != currentSubstitution.changedTeacher) { differences.teacher = { before: currentSubstitution.changedTeacher, after: newChange.changedTeacher, }; } if (newChange.room != currentSubstitution.changedRoom) { differences.room = { before: currentSubstitution.changedRoom, after: newChange.room, }; } if (newChange.notes != currentSubstitution.notes) { differences.notes = { before: currentSubstitution.notes, after: newChange.notes, }; } return differences; } }