diff --git a/server/.env.sample b/server/.env.sample deleted file mode 100644 index bd3bd1a..0000000 --- a/server/.env.sample +++ /dev/null @@ -1,17 +0,0 @@ -# For production, the database credentials should be changed -# here and in the docker-compose file for the postgres container, -# but they will work for testing or development -DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres?schema=public - -# These credentials are required for fetching substitution plan -# files from BOLLE. More information on how to get them can be found -# in the BOLLE parser (./server/parser/bolle.js) -# (If you are using a different file provider than bolle, you can -# remove these and add the ones required by your file provider) -BOLLE_URL= -BOLLE_USER= -BOLLE_KEY= - -# This password is required for logging into your timetable v2 -# instance. You can leave it empty to disable authentication. -AUTH_PASSWORD= \ No newline at end of file diff --git a/server/.eslintrc b/server/.eslintrc deleted file mode 100644 index 10d2238..0000000 --- a/server/.eslintrc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "env": { - "node": true - } -} diff --git a/server/.gitignore b/server/.gitignore deleted file mode 100644 index 37d7e73..0000000 --- a/server/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules -.env diff --git a/server/api/admin.js b/server/api/admin.js deleted file mode 100644 index 269a940..0000000 --- a/server/api/admin.js +++ /dev/null @@ -1,152 +0,0 @@ -import Prisma from "@prisma/client"; -const prisma = new Prisma.PrismaClient(); - -export function registerAdmin(app) { - app.get("/api/admin/timetable", listTimetables); - app.post("/api/admin/timetable", createTimetable); - app.put("/api/admin/timetable", editTimetable); - app.delete("/api/admin/timetable", deleteTimetable); - - app.get("/api/admin/key", listKeys); - app.post("/api/admin/key", createKey); - app.put("/api/admin/key", editKey); - app.delete("/api/admin/key", deleteKey); -} - -function sendMissingArguments(res) { - res.status(400).send({ - success: false, - error: "missing_arguments", - }); -} - -async function listTimetables(_, res) { - res.send( - await prisma.timetable.findMany({ - select: { - id: true, - title: true, - class: true, - source: true, - trusted: true, - }, - }), - ); -} - -async function createTimetable(req, res) { - let data = req.body; - if (!data.title || !data.data || !data.class) { - sendMissingArguments(res); - return; - } - const timetable = await prisma.timetable.create({ - data: req.body, - }); - res.status(201).send(timetable); -} - -async function editTimetable(req, res) { - let id = parseInt(req.query.id); - if (!id) { - sendMissingArguments(res); - return; - } - try { - const timetable = await prisma.timetable.update({ - where: { - id, - }, - data: req.body, - }); - res.status(201).send(timetable); - } catch (e) { - res.status(500).send(e); - } -} - -async function deleteTimetable(req, res) { - if (!req.query.id) { - sendMissingArguments(res); - return; - } - try { - await prisma.timetable.delete({ - where: { - id: parseInt(req.query.id), - }, - }); - res.status(200).send(); - } catch (e) { - res.status(500).send(e); - } -} - -async function listKeys(_, res) { - res.send(await prisma.key.findMany()); -} - -async function createKey(req, res) { - let data = req.body; - if (!data.key) { - sendMissingArguments(res); - return; - } - const existingKey = await prisma.key.findUnique({ - where: { - key: data.key, - }, - }); - if (existingKey) { - res.status(400).send({ - success: false, - error: "key_already_exists", - }); - return; - } - - const key = await prisma.key.create({ - data: { - key: data.key, - permissions: data.permissions || [], - validUntil: data.validUntil, - notes: data.notes, - }, - }); - res.status(201).send(key); -} - -async function editKey(req, res) { - if (!req.query.id) { - sendMissingArguments(res); - return; - } - try { - const timetable = await prisma.key.update({ - where: { - key: req.query.id, - }, - data: req.body, - }); - res.status(201).send(timetable); - } catch (e) { - res.status(500).send(e); - } -} - -async function deleteKey(req, res) { - if (!req.query.id) { - sendMissingArguments(res); - return; - } - try { - await prisma.key.delete({ - where: { - key: req.query.id, - }, - }); - res.status(200).send(); - } catch (e) { - res.status(500).send(e); - } -} diff --git a/server/api/auth.js b/server/api/auth.js deleted file mode 100644 index b0d72aa..0000000 --- a/server/api/auth.js +++ /dev/null @@ -1,158 +0,0 @@ -import Prisma from "@prisma/client"; -const prisma = new Prisma.PrismaClient(); - -import { log } from "../logs.js"; - -async function isLoggedIn(req) { - // If AUTH_PASSWORD env variable is not present don't require any login - if (!process.env.AUTH_PASSWORD) { - return true; - } - // If no session cookie is set and no token query - // parameter is provided the user can't be logged in - const token = req.query.token || req.cookies.session; - if (!token) { - return false; - } - // If there is a session cookie check it - const session = await prisma.session.findUnique({ - where: { - token, - }, - }); - // If no session is found (the session probably - // exired) the user is not logged in - if (!session) { - return false; - } - // If no checks failed the user is logged in - return session; -} - -async function renewSession(session) { - // Don't try to renew sessions if auth is disabled - if (session === true) return; - - await prisma.session.update({ - where: { - token: session.token, - }, - data: { - // 14 Days from now on - validUntil: new Date(Date.now() + 1000 * 60 * 60 * 24 * 14), - }, - }); -} - -async function login(req, res) { - // Check if the user is already logged in - let session = await isLoggedIn(req); - if (session) { - renewSession(session); - res.redirect("/"); - return; - } - // Check password - if (!req.body.password || req.body.password != process.env.AUTH_PASSWORD) { - res.redirect("/login"); - return; - } - // Create a new auth session - session = await prisma.session.create({ - data: { - // Expires after 14 days of inactivity - validUntil: new Date(Date.now() + 1000 * 60 * 60 * 24 * 14), - }, - }); - res.cookie("session", session.token, { - httpOnly: true, - // Expire "never" - expires: new Date(253402300000000), - }); - log("API / Auth", `New session: ${session.token}`); - res.redirect("/"); -} - -async function logout(req, res) { - const session = await isLoggedIn(req); - if (!session) { - res.sendStatus(401); - return; - } - await prisma.session.deleteMany({ - where: { - token: session.token, - }, - }); - log("API / Auth", `Removed session: ${session.token}`); - res.redirect("/"); -} - -async function checkLogin(req, res, next) { - // Allow requests to `/api/token` - if (req.path == "/token") { - next(); - return; - } - const session = await isLoggedIn(req); - if (!session) { - // send 401 Unauthorized so the - // app redirects to the login page - res.sendStatus(401); - return; - } - req.locals = { - session: session.token, - }; - renewSession(session); - next(); -} - -async function token(req, res) { - if (!req.query.password || req.query.password != process.env.AUTH_PASSWORD) { - res.status(401).send({ - success: false, - error: "wrong_auth", - message: "Wrong password", - }); - return; - } - // Create a new auth session - const session = await prisma.session.create({ - data: { - // API token expires after 1 hour - validUntil: new Date(Date.now() + 1000 * 60 * 60), - }, - }); - log("API / Auth", `New token: ${session.token}`); - - res.send({ - success: true, - token: session.token, - }); -} - -export default { - login, - logout, - checkLogin, - token, -}; - -// Clean up expired sessions every hour -setInterval( - async () => { - const sessions = await prisma.session.findMany(); - for (const session of sessions) { - if (session.validUntil < new Date()) { - log("API / Auth", `Removed expired session: ${session.token}`); - await prisma.session.delete({ - where: { - token: session.token, - }, - }); - } - } - }, - 1000 * 60 * 60, -); diff --git a/server/api/index.js b/server/api/index.js deleted file mode 100644 index cfb2e7c..0000000 --- a/server/api/index.js +++ /dev/null @@ -1,275 +0,0 @@ -import Prisma from "@prisma/client"; -const prisma = new Prisma.PrismaClient(); - -import { - applyKey, - hasPermission, - listPermissions, - revokeKey, -} from "./permission.js"; - -// Get info API endpoint (/api/info) -// Returns information about the requesting session -export async function getInfo(req, res) { - // If server has auth disabled - if (!req.locals.session) { - res.send({ - authenticated: true, - appliedKeys: [], - permissions: [], - }); - return; - } - - const session = await prisma.session.findUnique({ - where: { - token: req.locals.session, - }, - include: { - appliedKeys: { - select: { - key: true, - permissions: true, - validUntil: true, - }, - }, - }, - }); - - res.send({ - authenticated: true, - appliedKeys: session.appliedKeys, - permissions: await listPermissions(session.token), - }); -} - -// Put and Delete key API endpoints (/api/key) -// Applies or revokes a key from the requesting user's session -export async function putKey(req, res) { - if (await applyKey(req.locals.session, req.query.key)) { - res.status(200).send(); - } else { - res.status(400).send({ - success: false, - error: "invalid_key", - message: "This key does not exist", - }); - } -} - -export async function deleteKey(req, res) { - if (await revokeKey(req.locals.session, req.query.key)) { - res.status(200).send(); - } else { - res.status(400).send(); - } -} - -// Get timetable API endpoint (/api/timetable) -// Returns timetable data for requested class if available -export async function getTimetable(req, res) { - if (!req.query.class) { - res.status(400).send({ - success: false, - error: "missing_parameter", - message: "No class parameter provided", - }); - return; - } - const requestedClass = req.query.class.toLowerCase(); - const timetables = await prisma.timetable.findMany({ - where: { - class: requestedClass, - }, - orderBy: { - updatedAt: "desc", - }, - }); - const times = await prisma.time.findMany(); - res.send({ - timetables, - times, - }); -} - -// Edit timetable API endpoint (/api/timetable) -// Updates a remote timetable with the requested data -export async function putTimetable(req, res) { - const timetableId = parseInt(req.query.id); - const data = req.body.data; - if ( - !(await hasPermission(req.locals.session, "timetable.update", timetableId)) - ) { - res.status(401).send({ - success: false, - error: "missing_permission", - message: "You don't have permission to update this timetable!", - }); - return; - } - - await prisma.timetable.update({ - where: { - id: timetableId, - }, - data: { - data, - title: req.body.title, - }, - }); - res.status(201).send(); -} - -// Helper function for converting a date string -// (eg. "2022-06-02" or "1654128000000") to a -// unix timestamp -function convertToDate(dateQuery) { - var date; - if (dateQuery.match(/^[0-9]+$/) != null) date = parseInt(dateQuery); - else date = dateQuery; - date = new Date(date).setUTCHours(0, 0, 0, 0); - return new Date(date); -} - -// Get substitutions API endpoint (/api/substitutions) -// Returns all known substitutions for requested date / class -// If no class is supplied, all substitutions are returned -export async function getSubstitutions(req, res) { - const requestedClass = (req.query.class || "").toLowerCase(); - var from, to, date; - // Check if from or to date is set in request - if (req.query.from && req.query.to) { - from = convertToDate(req.query.from); - to = convertToDate(req.query.to); - } else if (req.query.date) { - date = convertToDate(req.query.date); - } - - const prismaOptions = { - where: { - removed: false, - }, - orderBy: { - lesson: "asc", - }, - }; - if (requestedClass) { - prismaOptions.where.class = { has: requestedClass }; - } - // Choose which date to use in database query - if (from && to) { - prismaOptions.where.date = { - gte: from, - lte: to, - }; - } else if (date) { - prismaOptions.where.date = date; - } else { - // Default to all substitutions for today and in the future - prismaOptions.where.date = { - gte: new Date(new Date().setUTCHours(0, 0, 0, 0)), - }; - } - - const rawSubstitutions = await prisma.substitution.findMany(prismaOptions); - const substitutions = rawSubstitutions.map((element) => { - const substitution = { - id: element.id, - class: element.class, - type: element.type, - rawType: element.rawType, - lesson: element.lesson, - date: new Date(element.date).getTime(), - notes: element.notes, - teacher: element.teacher, - change: {}, - }; - if (element.changedRoom) substitution.change.room = element.changedRoom; - if (element.changedTeacher) - substitution.change.teacher = element.changedTeacher; - if (element.changedSubject) - substitution.change.subject = element.changedSubject; - return substitution; - }); - res.send(substitutions); -} - -// Get history API endpoint (/api/history) -// Returns history of changes for all substituions in the date range -// for the requested class if supplied -export async function getHistory(req, res) { - const requestedClass = (req.query.class || "").toLowerCase(); - var from, to, date; - // Check if from or to date is set in request - if (req.query.from && req.query.to) { - from = convertToDate(req.query.from); - to = convertToDate(req.query.to); - } else if (req.query.date) { - date = convertToDate(req.query.date); - } - - const prismaOptions = { - where: { - substitution: {}, - }, - include: { - substitution: true, - }, - orderBy: { - createdAt: "desc", - }, - }; - if (requestedClass) { - prismaOptions.where.substitution.class = { has: requestedClass }; - } - // Choose which date to use in database query - if (from && to) { - prismaOptions.where.substitution.date = { - gte: from, - lte: to, - }; - } else if (date) { - prismaOptions.where.substitution.date = date; - } else { - // Default to history of all substitutions for today and in the future - prismaOptions.where.substitution.date = { - gte: new Date(new Date().setUTCHours(0, 0, 0, 0)), - }; - } - - const rawChanges = await prisma.substitutionChange.findMany(prismaOptions); - const changes = rawChanges.map((element) => { - return { - id: element.id, - type: element.type, - class: element.substitution.class, - substitutionId: element.substitutionId, - lesson: element.substitution.lesson, - updatedAt: new Date(element.createdAt).getTime(), - date: new Date(element.substitution.date).getTime(), - teacher: element.teacher, - change: element.changes, - notes: element.notes, - parseEventId: element.parseEventId, - }; - }); - res.send(changes); -} - -// Get classes API endpoints (/api/classes) -// Get all available classes where timetable and -// substitutions can be requested for -export async function getClasses(_req, res) { - const classes = await prisma.class.findMany({ - select: { - name: true, - regex: false, - }, - orderBy: { - name: "asc", - }, - }); - // Only return the name of the class - const classList = classes.map((element) => element.name); - res.send(classList); -} diff --git a/server/api/permission.js b/server/api/permission.js deleted file mode 100644 index b801f9b..0000000 --- a/server/api/permission.js +++ /dev/null @@ -1,110 +0,0 @@ -import Prisma from "@prisma/client"; -import { log } from "../logs.js"; -const prisma = new Prisma.PrismaClient(); - -export async function listPermissions(sessionToken) { - const session = await prisma.session.findUnique({ - where: { - token: sessionToken, - }, - include: { - appliedKeys: true, - }, - }); - if (!session) return []; - - const perms = []; - for (const key of session.appliedKeys) { - if (key.validUntil && new Date() > key.validUntil) continue; - for (const perm of key.permissions) { - perms.push(perm); - } - } - return perms; -} - -export async function hasPermission(sessionToken, permission, forValue) { - let hasPermission = false; - for (const perm of await listPermissions(sessionToken)) { - if (perm == permission) hasPermission = true; - else if (perm == permission + ":" + forValue) hasPermission = true; - } - return hasPermission; -} - -export async function checkAdmin(req, res, next) { - if (!(await hasPermission(req.locals.session, "admin"))) { - res.status(401).send({ - success: false, - error: "admin_only", - message: "You need to be admin to do this!", - }); - return; - } - next(); -} - -export async function applyKey(sessionToken, key) { - if (!key) return false; - const foundKey = await prisma.key.findUnique({ - where: { - key, - }, - }); - if (!foundKey) return false; - - await prisma.session.update({ - where: { - token: sessionToken, - }, - data: { - appliedKeys: { - connect: { - key: foundKey.key, - }, - }, - }, - }); - return true; -} - -export async function revokeKey(sessionToken, key) { - if (!key) return false; - - await prisma.session.update({ - where: { - token: sessionToken, - }, - data: { - appliedKeys: { - disconnect: { - key: key, - }, - }, - }, - }); - return true; -} - -// Clean up expired keys every hour -setInterval( - async () => { - const keys = await prisma.key.findMany(); - for (const key of keys) { - if (key.validUntil && key.validUntil < new Date()) { - log( - "API / Permissions", - `Removed expired key: ${key.key}; Permissions: ${key.permissions.join( - ", ", - )}`, - ); - await prisma.key.delete({ - where: { - key: key.key, - }, - }); - } - } - }, - 1000 * 60 * 60, -); diff --git a/server/createAdminKey.js b/server/createAdminKey.js deleted file mode 100644 index 24d7ab6..0000000 --- a/server/createAdminKey.js +++ /dev/null @@ -1,13 +0,0 @@ -import Prisma from "@prisma/client"; - -const prisma = new Prisma.PrismaClient(); -(async () => { - const key = await prisma.key.create({ - data: { - permissions: ["admin"], - notes: `Created at ${new Date().toLocaleString()} using the "createAdminKeys.js" script`, - }, - }); - console.log("Created admin key:"); - console.log(key.key); -})(); diff --git a/server/index.js b/server/index.js deleted file mode 100644 index 412ab44..0000000 --- a/server/index.js +++ /dev/null @@ -1,91 +0,0 @@ -import express from "express"; -import cors from "cors"; -import cookieParser from "cookie-parser"; - -// Import API endpoints -import { - getTimetable, - getSubstitutions, - getHistory, - getClasses, - putTimetable, - getInfo, - putKey, - deleteKey, -} from "./api/index.js"; -import auth from "./api/auth.js"; -import { Parser } from "./parser/index.js"; -import { BolleClient } from "./parser/bolle.js"; -import { parseSubstitutionPlan } from "./parser/untis.js"; -import { registerAdmin } from "./api/admin.js"; -import { checkAdmin } from "./api/permission.js"; - -// Check that credentials are supplied -if ( - !process.env.BOLLE_URL || - !process.env.BOLLE_USER || - !process.env.BOLLE_KEY -) { - console.error("Error: Bolle Auth environment variables missing!"); - process.exit(1); -} - -// Create the Webserver -const app = express(); -const port = process.env.PORT || 3000; -app.use(cors()); -app.use(cookieParser()); -app.use(express.urlencoded({ extended: true })); -app.use(express.json()); - -// Initialize the Parser and set it to update the -// substitution plan at the specified update interval -new Parser( - new BolleClient( - process.env.BOLLE_URL, - process.env.BOLLE_USER, - process.env.BOLLE_KEY, - ), - parseSubstitutionPlan, - process.env.UPDATE_INTERVAL || 1 * 60 * 1000, // Default to 1 minute -); - -// Create new Auth class to store sessions -app.post("/auth/login", auth.login); -app.get("/auth/logout", auth.logout); -// Check login for every API request -app.use("/api", auth.checkLogin); -// Provide check endpoint so the frontend -// can check if the user is logged in -app.get("/api/check", (_req, res) => { - res.sendStatus(200); -}); - -// Register API endpoints -app.get("/api/info", getInfo); -app.put("/api/key", putKey); -app.delete("/api/key", deleteKey); -app.get("/api/timetable", getTimetable); -app.put("/api/timetable", putTimetable); -app.get("/api/substitutions", getSubstitutions); -app.get("/api/history", getHistory); -app.get("/api/classes", getClasses); -app.post("/api/token", auth.token); - -// Register Admin endpoints -app.use("/api/admin", checkAdmin); -registerAdmin(app); - -// Respond with 400 for non-existent endpoints -app.get("/api/*", (_req, res) => { - res.sendStatus(400); -}); - -// Supply frontend files if any other url -// is requested to make vue router work -app.use("/", express.static("../dist")); -app.use("/*", express.static("../dist")); - -app.listen(port, () => { - console.log(`Server listening on http://localhost:${port}`); -}); diff --git a/server/jsconfig.json b/server/jsconfig.json deleted file mode 100644 index a1c4e6e..0000000 --- a/server/jsconfig.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "compilerOptions": { - "checkJs": false, - "resolveJsonModule": true, - "moduleResolution": "node", - "target": "es2020", - "module": "es2015" - }, - "exclude": [ - "dist", - "node_modules", - "build", - ".vscode", - "coverage", - ".npm", - ".yarn" - ], - "typeAcquisition": { - "enable": true, - "include": [ - "node" - ] - } -} \ No newline at end of file diff --git a/server/logs.js b/server/logs.js deleted file mode 100644 index 58d735a..0000000 --- a/server/logs.js +++ /dev/null @@ -1,17 +0,0 @@ -import fs from "fs"; - -export function log(type, text) { - if (!fs.existsSync("logs")) fs.mkdirSync("logs"); - const now = new Date().toISOString(); - - const logName = now.split("T")[0] + ".log"; - const timestamp = now.replace("T", " ").split(".")[0]; - const logLine = `<${timestamp}> [${type}]: ${text}`; - fs.appendFileSync("logs/" + logName, logLine + "\n"); - console.log(logLine); -} - -export function getLogPath() { - const logName = new Date().toISOString().split("T")[0] + ".log"; - return "logs/" + logName; -} diff --git a/server/package-lock.json b/server/package-lock.json deleted file mode 100644 index dac345c..0000000 --- a/server/package-lock.json +++ /dev/null @@ -1,952 +0,0 @@ -{ - "name": "timetable-server", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "timetable-server", - "version": "1.0.0", - "license": "GPL-3.0-or-later", - "dependencies": { - "@prisma/client": "^5.2.0", - "axios": "^1.4.0", - "cheerio": "^1.0.0-rc.11", - "cookie-parser": "^1.4.6", - "cors": "^2.8.5", - "express": "^4.18.2" - }, - "devDependencies": { - "prisma": "^5.2.0" - } - }, - "node_modules/@prisma/client": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.2.0.tgz", - "integrity": "sha512-AiTjJwR4J5Rh6Z/9ZKrBBLel3/5DzUNntMohOy7yObVnVoTNVFi2kvpLZlFuKO50d7yDspOtW6XBpiAd0BVXbQ==", - "hasInstallScript": true, - "dependencies": { - "@prisma/engines-version": "5.2.0-25.2804dc98259d2ea960602aca6b8e7fdc03c1758f" - }, - "engines": { - "node": ">=16.13" - }, - "peerDependencies": { - "prisma": "*" - }, - "peerDependenciesMeta": { - "prisma": { - "optional": true - } - } - }, - "node_modules/@prisma/engines": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.2.0.tgz", - "integrity": "sha512-dT7FOLUCdZmq+AunLqB1Iz+ZH/IIS1Fz2THmKZQ6aFONrQD/BQ5ecJ7g2wGS2OgyUFf4OaLam6/bxmgdOBDqig==", - "devOptional": true, - "hasInstallScript": true - }, - "node_modules/@prisma/engines-version": { - "version": "5.2.0-25.2804dc98259d2ea960602aca6b8e7fdc03c1758f", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.2.0-25.2804dc98259d2ea960602aca6b8e7fdc03c1758f.tgz", - "integrity": "sha512-jsnKT5JIDIE01lAeCj2ghY9IwxkedhKNvxQeoyLs6dr4ZXynetD0vTy7u6wMJt8vVPv8I5DPy/I4CFaoXAgbtg==" - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/axios": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", - "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", - "dependencies": { - "follow-redirects": "^1.15.0", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/cheerio": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", - "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", - "dependencies": { - "cheerio-select": "^2.1.0", - "dom-serializer": "^2.0.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "htmlparser2": "^8.0.1", - "parse5": "^7.0.0", - "parse5-htmlparser2-tree-adapter": "^7.0.0" - }, - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/cheeriojs/cheerio?sponsor=1" - } - }, - "node_modules/cheerio-select": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", - "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", - "dependencies": { - "boolbase": "^1.0.0", - "css-select": "^5.1.0", - "css-what": "^6.1.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-parser": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", - "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", - "dependencies": { - "cookie": "0.4.1", - "cookie-signature": "1.0.6" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" - }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/css-select": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", - "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ] - }, - "node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", - "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.1", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.5.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.11.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/express/node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "node_modules/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/htmlparser2": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", - "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "entities": "^4.4.0" - } - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/parse5": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", - "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", - "dependencies": { - "entities": "^4.4.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parse5-htmlparser2-tree-adapter": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", - "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", - "dependencies": { - "domhandler": "^5.0.2", - "parse5": "^7.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" - }, - "node_modules/prisma": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.2.0.tgz", - "integrity": "sha512-FfFlpjVCkZwrqxDnP4smlNYSH1so+CbfjgdpioFzGGqlQAEm6VHAYSzV7jJgC3ebtY9dNOhDMS2+4/1DDSM7bQ==", - "devOptional": true, - "hasInstallScript": true, - "dependencies": { - "@prisma/engines": "5.2.0" - }, - "bin": { - "prisma": "build/index.js" - }, - "engines": { - "node": ">=16.13" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, - "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", - "dependencies": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.18.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "engines": { - "node": ">= 0.8" - } - } - } -} diff --git a/server/package.json b/server/package.json deleted file mode 100644 index cd494a3..0000000 --- a/server/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "timetable-server", - "version": "1.0.0", - "description": "", - "main": "index.js", - "type": "module", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "author": "minie4", - "license": "GPL-3.0-or-later", - "dependencies": { - "@prisma/client": "^5.2.0", - "axios": "^1.4.0", - "cheerio": "^1.0.0-rc.11", - "cookie-parser": "^1.4.6", - "cors": "^2.8.5", - "express": "^4.18.2" - }, - "devDependencies": { - "prisma": "^5.2.0" - } -} diff --git a/server/parser/bolle.js b/server/parser/bolle.js deleted file mode 100644 index 70bccac..0000000 --- a/server/parser/bolle.js +++ /dev/null @@ -1,85 +0,0 @@ -import axios from "axios"; -import crypto from "node:crypto"; -import { log } from "../logs.js"; - -/* -This BOLLE (https://bolle-software.de/) parser tries to download -the latest substitution plan HTML files from the "Pinnwand". - -It uses the same API that the mobile app (v0.3.6) uses. -To get your login credentials you need to log into your BOLLE account -and register a new device in the settings (/einstellungen/geraete_personal). -Click on "Manuelle Logindaten Einblenden" and use "ID" as apiUser -and "Token" as apiKey. You need to make at least one request with -this API token before closing the registration window or else the -token will be invalidated immediately. -*/ - -// Files to download from the "Pinnwand" -const filenames = ["vp_heute", "vp_morgen"]; - -export class BolleClient { - constructor(bolleInstance, apiUser, apiKey) { - this.bolleInstance = bolleInstance; - this.apiUser = apiUser; - this.apiKey = apiKey; - } - - async getFiles() { - try { - let contents = []; - for (let file of filenames) { - contents.push(await this.getFile(file)); - } - return contents; - } catch (error) { - log("Parser / Bolle", "Error getting data: " + error); - return []; - } - } - - async getFile(filename) { - // Generate the BOLLE api payload - let payload = { - api_payload: JSON.stringify({ - method: "vertretungsplan_html", - payload: { content: filename }, - }), - }; - // Generate request headers - let headers = this.buildRequestHeaders(payload.api_payload); - // Send the POST request - let response = await axios.post(this.getRequestUrl(), payload, { - headers, - }); - // The server responds with a json object - // containing the base64 encoded html data - let base64 = response.data["html_base64"]; - // Decode the base64 data using the latin1 (ISO 8859-1) character set - return Buffer.from(base64, "base64").toString("latin1"); - } - getRequestUrl() { - // The API that the bolle mobile app uses is available at /app/basic - return `https://${this.bolleInstance}/app/basic`; - } - buildRequestHeaders(payload) { - // Bolle needs the sha1 hash of the payload - // to be set as the "b-hash" header - let hash = crypto.createHash("sha1"); - hash.update(payload); - - return { - Accept: "application/json", - // Set the auth headers - "X-Auth-User": this.apiUser, - "X-Auth-Token": this.apiKey, - "App-Version": "5", - // Set the hash - "B-Hash": hash.digest("hex"), - "Content-Type": "application/json", - Connection: "Keep-Alive", - "Accept-Encoding": "gzip", - "User-Agent": "okhttp/4.9.2", - }; - } -} diff --git a/server/parser/dsbmobile.js b/server/parser/dsbmobile.js deleted file mode 100644 index 13a8426..0000000 --- a/server/parser/dsbmobile.js +++ /dev/null @@ -1,71 +0,0 @@ -import axios from "axios"; -import { log } from "../logs.js"; - -const baseUrl = "https://mobileapi.dsbcontrol.de"; - -export async function getAuthtoken(username, password) { - const response = await axios.get( - `${baseUrl}/authid?user=${username}&password=${password}&bundleid&appversion&osversion&pushid`, - ); - if (response.data == "") throw "Wrong DSB username or password"; - return response.data; -} - -export async function getTimetables(authtoken) { - const response = await axios.get( - `${baseUrl}/dsbtimetables?authid=${authtoken}`, - ); - const timetables = response.data; - - const urls = []; - timetables.forEach((timetable) => { - const rawTimestamp = timetable.Date; - // Convert the timestamp to the correct - // format so new Date() accepts it - const date = rawTimestamp.split(" ")[0].split(".").reverse().join("-"); - const time = rawTimestamp.split(" ")[1]; - const timestamp = date + " " + time; - urls.push({ - title: timetable.Title, - url: timetable.Childs[0].Detail, - updatedAt: new Date(timestamp), - }); - }); - - return urls; -} - -// List of files that include timetable data -const dsbFiles = ["Schüler_Monitor - subst_001", "Schüler Morgen - subst_001"]; - -export class DSBClient { - constructor(dsbUser, dsbPassword) { - this.dsbUser = dsbUser; - this.dsbPassword = dsbPassword; - } - async getFiles() { - try { - // Get authtoken - const token = await getAuthtoken(this.dsbUser, this.dsbPassword); - // Fetch available files - const response = await getTimetables(token); - // Filter files that should be parsed - const timetables = response.filter((e) => dsbFiles.includes(e.title)); - // Fetch the contents - const files = []; - for (let timetable of timetables) { - const result = await axios.request({ - method: "GET", - url: timetable.url, - responseEncoding: "binary", - }); - files.push(result.data); - } - - return files; - } catch (error) { - log("Parser / DSB Mobile", "Error getting data: " + error); - return []; - } - } -} diff --git a/server/parser/filesystem.js b/server/parser/filesystem.js deleted file mode 100644 index c868667..0000000 --- a/server/parser/filesystem.js +++ /dev/null @@ -1,24 +0,0 @@ -import fs from "fs"; - -/* -This file provider allows the application to use a local folder containing -parsable (.html) files, instead of downloading them from the internet. - -This can be especially useful for development. -*/ - -export class FileSystemClient { - constructor(path) { - this.path = path; - } - - getFiles() { - const contents = []; - const files = fs.readdirSync(this.path); - for (const file of files) { - const data = fs.readFileSync(this.path + "/" + file).toString(); - contents.push(data); - } - return contents; - } -} diff --git a/server/parser/index.js b/server/parser/index.js deleted file mode 100644 index 8db1e6c..0000000 --- a/server/parser/index.js +++ /dev/null @@ -1,286 +0,0 @@ -import Prisma from "@prisma/client"; -import { log, getLogPath } from "../logs.js"; - -const prisma = new Prisma.PrismaClient(); - -export class Parser { - constructor(fileProvider, documentParser, interval) { - this.fileProvider = fileProvider; - this.documentParser = documentParser; - - // Schedule plan updates - setInterval(() => this.updatePlan(), interval); - // Do the first update instantly - this.updatePlan(); - } - async updatePlan() { - const startedAt = new Date(); - try { - // Request substitution plan files using the fileProvider - const files = await this.fileProvider.getFiles(); - const plans = []; - // Parse them using the provided parser - for (const file of files) { - // Parse the substitution plan - const parsed = this.documentParser(file); - plans.push(parsed); - } - // Create a new parse event - const parseEvent = await prisma.parseEvent.create({ - data: { - logFile: getLogPath(), - originalData: "", - duration: new Date() - startedAt, - succeeded: true, - }, - }); - // Group plans by date to prevent having - // multiple plan files with the same date - const dayPlans = []; - for (const plan of plans) { - const foundPlan = dayPlans.find((e) => e.date == plan.date); - if (!foundPlan) { - // Make sure to not insert duplicate substitutions within a file - const changes = structuredClone(plan.changes); - const cleanedChanges = []; - for (const change of changes) { - const changeExists = cleanedChanges.find( - (e) => JSON.stringify(e) == JSON.stringify(change), - ); - if (!changeExists) { - cleanedChanges.push(change); - } - // Use the new array of changes - plan.changes = cleanedChanges; - } - dayPlans.push(plan); - } else { - for (const change of plan.changes) { - // Make sure to not insert a substitution that already exists in the changes - const changeExists = foundPlan.changes.find( - (e) => JSON.stringify(e) == JSON.stringify(change), - ); - if (!changeExists) { - foundPlan.changes.push(change); - } - } - } - } - // Insert substitutions of all substitution plans - for (const plan of dayPlans) { - await this.insertSubstitutions(plan, parseEvent); - } - } catch (error) { - // If something went wrong, create a failed - // parse event with the error message - await prisma.parseEvent.create({ - data: { - logFile: getLogPath(), - originalData: error.toString(), - duration: new Date() - startedAt, - succeeded: false, - }, - }); - // Log the error - log("Parser / Main", "Parse event failed: " + error); - } - } - async insertSubstitutions(parsedData, parseEvent) { - const { 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, - }, - }); - - // Loop through every change of the substitution plan - for (const change of changes) { - // Find all classes the substitution belongs to - const classes = this.getSubstitutionClasses(classList, change.class); - // If the substitution does not belong to any classes known - // by the server, use the provied class string instead - if (classes.length == 0) classes.push(change.class || "unknown"); - - // Workaround: no correct match possible for subsitutions of this - // type beacuse they do not have a class and a subject attribute - if (change.type == "Sondereins." && !change.subject) { - change.subject = change.notes; - } - if (change.type == "Sondereins." && !change.teacher) { - change.teacher = change.changedTeacher; - change.changedTeacher = ""; - } - - // Check if a substitution exists in the database that - // it similar enough to the entry in the substitution - // plan to be considered the same substitution - // (Date, Type, Lesson, Classes and Subject need to be the same) - const matchingSubstitutionId = knownSubstitutions.findIndex( - (substitution) => { - return ( - substitution.date.getTime() == - new Date(date).setUTCHours(0, 0, 0, 0) && - substitution.rawType == change.type && - substitution.lesson == change.lesson && - classes.sort().join(",") == substitution.class.sort().join(",") && - substitution.changedSubject == change.subject && - substitution.teacher == (change.teacher || "") - ); - }, - ); - const matchingSubstitution = knownSubstitutions[matchingSubstitutionId]; - - if (!matchingSubstitution) { - // If the substitution is new, create it in the database - const newSubstitution = await prisma.substitution.create({ - data: { - class: classes, - date: new Date(date), - type: - change.type == "Entfall" || - change.type == "eigenverantwortliches Arbeiten" - ? "cancellation" - : "change", - rawType: change.type, - lesson: parseInt(change.lesson), - teacher: change.teacher || "", - changedTeacher: change.changedTeacher, - changedRoom: change.room || undefined, - changedSubject: change.subject, - notes: change.notes, - removed: false, - }, - }); - // Also create a change entry for it - const substitutionChange = await prisma.substitutionChange.create({ - data: { - substitutionId: newSubstitution.id, - type: "addition", - changes: { - class: classes, - type: change.type == "Entfall" ? "cancellation" : "change", - rawType: change.type, - lesson: parseInt(change.lesson), - date: new Date(date), - notes: change.notes, - teacher: change.teacher || "", - 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 { - // If the entry was updated, find the differences - const differences = this.findDifferences(matchingSubstitution, change); - if (Object.keys(differences).length > 0) { - // If differences were found, update the entry in the database - 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); - // And create a change event for it - 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}`, - ); - } - // Remove the substitution from the array to later know the - // entries that are not present in the substitution plan - knownSubstitutions.splice(matchingSubstitutionId, 1); - } - } - // Mark all entries as removed that were - // not found in the substitution plan - 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: { - class: remainingSubstitution.class, - type: remainingSubstitution.type, - rawType: remainingSubstitution.rawType, - lesson: remainingSubstitution.lesson, - date: remainingSubstitution.date.getTime(), - notes: remainingSubstitution.notes, - teacher: remainingSubstitution.teacher, - change: { - teacher: remainingSubstitution.changedTeacher, - room: remainingSubstitution.changedRoom, - subject: remainingSubstitution.changedSubject, - }, - }, - 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; - } -} diff --git a/server/parser/untis.js b/server/parser/untis.js deleted file mode 100644 index a779254..0000000 --- a/server/parser/untis.js +++ /dev/null @@ -1,110 +0,0 @@ -import * as cheerio from "cheerio"; -const titleTranslations = { - "Klasse(n)": "class", - Datum: "date", - Stunde: "lesson", - "(Lehrer)": "teacher", - Vertreter: "changedTeacher", - Fach: "subject", - Raum: "room", - Art: "type", - Text: "notes", -}; - -export function parseSubstitutionPlan(html) { - const infos = {}; - const $ = cheerio.load(html); - - const data = []; - const tables = $("table.mon_list"); - tables.each((tableIndex, tableElement) => { - // Extract the date, weekday and a/b week from the title - const title = $(tableElement) - .parent() - .siblings(".mon_title") - .text() - .split(" "); - const rawDate = title[0]; - const date = rawDate - .split(".") - .reverse() - .map((e) => e.padStart(2, 0)) - .join("-"); - - if (tableIndex == 0) { - infos.date = new Date(date).setUTCHours(0, 0, 0, 0); - infos.week = title[3]; - - // Get the export timestamp - const rawTimestamp = $(".mon_head") - .first() - .find("td") - .text() - .split("Stand: ")[1] - .replace(/[\s\n]*$/g, ""); - const exportDate = rawTimestamp - .split(" ")[0] - .split(".") - .reverse() - .join("-"); - const timestamp = exportDate + " " + rawTimestamp.split(" ")[1]; - infos.updatedAt = new Date(timestamp).getTime(); - } else { - // If there are multiple days in one file, - // ignore all except the first one - if (new Date(date).setUTCHours(0, 0, 0, 0) != infos.date) { - return; - } - } - - const titles = []; - const titleElements = $(tableElement).find("tr.list th"); - titleElements.each((index, titleElement) => { - const title = $(titleElement).text(); - titles[index] = titleTranslations[title]; - }); - - const subsitutionTable = $(tableElement).find("tr.list"); - // Loop through each table row - subsitutionTable.each((_rowcnt, row) => { - const rowData = {}; - - // Find the columns and ignore empty ones - const columns = $(row).find("td"); - if (columns.text() == "") return; - // Ignore columns that include "Keine Vertretungen" - // to have an empty array if there are no substitutions - if (columns.text().includes("Keine Vertretungen")) return; - - columns.each((columncnt, column) => { - const text = $(column).text(); - // Clean the text by removing new lines, tabs, ... - var cleantext = text.replace(/^\n\s*/g, "").replace(/\s*$/, ""); - if (cleantext == "" || cleantext == "---") cleantext = null; - - const columntitle = titles[columncnt]; - rowData[columntitle] = cleantext; - }); - - // Split change if it spans over multiple lessons - const rawLesson = rowData.lesson || "0"; - const fromToLessons = rawLesson.match(/\d+/g).map(Number); - const from = fromToLessons[0]; - const to = fromToLessons[1] || fromToLessons[0]; - - // Generate numbers from `from` to `to` - const lessons = Array(to - from + 1) - .fill() - .map((_e, i) => i + from); - - // Create new change for each lesson the change spans over - for (const lesson of lessons) { - rowData.lesson = lesson; - data.push({ ...rowData }); - } - }); - }); - - infos.changes = data; - return infos; -} diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma deleted file mode 100644 index 5560417..0000000 --- a/server/prisma/schema.prisma +++ /dev/null @@ -1,90 +0,0 @@ -generator client { - provider = "prisma-client-js" -} - -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model Timetable { - id Int @id @unique @default(autoincrement()) - title String @default("Default") - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - class String - validFrom DateTime @default(now()) - validUntil DateTime? - data Json - source String? - trusted Boolean @default(true) -} - -model Substitution { - id Int @id @unique @default(autoincrement()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - class String[] - date DateTime - type String - rawType String @default("unknown") - lesson Int - teacher String - changedTeacher String? - changedRoom String? - changedSubject String? - notes String? - removed Boolean @default(false) - - SubstitutionChange SubstitutionChange[] -} - -model SubstitutionChange { - id Int @id @unique @default(autoincrement()) - createdAt DateTime @default(now()) - substitution Substitution @relation(fields: [substitutionId], references: [id]) - substitutionId Int - type String - teacher String? - changes Json? - parseEvent ParseEvent @relation(fields: [parseEventId], references: [id]) - parseEventId Int -} - -model ParseEvent { - id Int @id @unique @default(autoincrement()) - createdAt DateTime @default(now()) - logFile String - originalData String - duration Int - succeeded Boolean - - SubstitutionChange SubstitutionChange[] -} - -model Class { - name String @id @unique - regex String -} - -model Time { - lesson Int @unique - start DateTime - end DateTime -} - -model Session { - token String @id @unique @default(uuid()) - createdAt DateTime @default(now()) - validUntil DateTime - appliedKeys Key[] -} - -model Key { - key String @id @unique @default(uuid()) - createdAt DateTime? @default(now()) - validUntil DateTime? - permissions String[] - notes String? - sessions Session[] -}