Skip to content

Commit

Permalink
Merge branch 'master' into dev
Browse files Browse the repository at this point in the history
Sxyntheon authored Jul 1, 2024
2 parents bdb949a + 569c4e0 commit 42b24a0
Showing 11 changed files with 365 additions and 29 deletions.
121 changes: 121 additions & 0 deletions backend/src/api/get_timetable_serviceworker.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
use actix_identity::Identity;
use actix_web::{web::{self}, Responder, Result};
use log::{debug, error};
use serde::{Deserialize, Serialize};
use surrealdb::sql::Thing;

use crate::{
api::response::Response, api_wrapper::{
untis_client::UntisClient, utils::{FormattedLesson, TimetableParameter}
}, models::{
model::{DBConnection, CRUD}, user_model::User
}, prelude::Error, utils::time::{format_for_untis, get_this_friday, get_this_monday}, GlobalUntisData
};

#[derive(Serialize)]
struct TimetableResponse {
lessons: Vec<FormattedLesson>,
}

#[derive(Deserialize)]
pub struct TimetableQuery {
from: Option<String>,
until: Option<String>,
class_name: Option<String>
}

#[derive(Deserialize)]
pub struct ServiceWorkerQuery {
jsessionid: Option<String>,
}

pub async fn get_timetable_serviceworker(
id: Option<Identity>, query: web::Query<TimetableQuery>, data: web::Json<ServiceWorkerQuery>, untis_data: web::Data<GlobalUntisData>,
db: web::Data<DBConnection>,
) -> Result<impl Responder> {
if id.is_none() {
return Ok(web::Json(Response::new_error(403, "Not logged in".to_string())));
}

let jsessionid = match data.jsessionid.clone() {
Some(session_cookie) => session_cookie,
None => return Ok(Response::new_error(403, "No JSESSIONID provided".to_string()).into()),
};

let pot_user: Option<User> = User::get_from_id(
db.clone(),
match id.unwrap().id() {
Ok(i) => {
let split = i.split_once(':');
if split.is_some() {
Thing::from(split.unwrap())
} else {
error!("ID in session_cookie is wrong???");
return Ok(Response::new_error(500, "There was an error trying to get your id".to_string()).into());
}
}
Err(e) => {
error!("Error getting Identity id\n{e}");
return Ok(Response::new_error(500, "There was an error trying to get your id".to_string()).into());
}
},
)
.await?;

let user = match pot_user {
Some(u) => u,
None => {
debug!("Deleted(?) User tried to log in with old session token");
return Ok(Response::new_error(404, "This account doesn't exist!".to_string()).into());
}
};

if !user.verified {
return Ok(Response::new_error(
403,
"Account not verified! Check your E-Mails for a verification link".to_string(),
)
.into());
}

let untis = match UntisClient::unsafe_init(
jsessionid,
user.person_id.try_into().expect("the database to not store numbers bigger than u16"),
5,
"the-schedule".into(),
untis_data.school.clone(),
untis_data.subdomain.clone(),
db,
)
.await
{
Ok(u) => u,
Err(e) => {
if let Error::Reqwest(_) = e {
return Ok(Response::new_error(400, "You done fucked up".into()).into());
} else if let Error::UntisError(body) = e {
return Ok(Response::new_error(500, "Untis done fucked up ".to_string() + &body).into());
}
else {
return Ok(Response::new_error(500, "Some mysterious guy done fucked up".into()).into());
}
}
};

let from = match query.from.clone() {
Some(from) => from,
None => format_for_untis(get_this_monday()),
};
let until = match query.until.clone() {
Some(until) => until,
None => format_for_untis(get_this_friday()),
};
let class_name: Option<String> = query.class_name.clone();
let timetable = match untis.clone().get_timetable(TimetableParameter::default(untis, from, until), class_name).await {
Ok(timetable) => timetable,
Err(err) => {
return Ok(Response::new_error(500, "Untis done fucked up ".to_string() + &err.to_string()).into());
}
};
Ok(Response::new_success(TimetableResponse { lessons: timetable }).into())
}
3 changes: 2 additions & 1 deletion backend/src/api/mod.rs
Original file line number Diff line number Diff line change
@@ -15,4 +15,5 @@ pub mod logout_all;
pub mod register;
pub mod resend_mail;
pub mod response;
pub mod verified;
pub mod verified;
pub mod get_timetable_serviceworker;
3 changes: 2 additions & 1 deletion backend/src/main.rs
Original file line number Diff line number Diff line change
@@ -26,7 +26,7 @@ use actix_web::{
cookie::{time::Duration, Key}, middleware::Logger, middleware::Compress, web::{self, Data}, App, HttpResponse, HttpServer
};
use api::{
change_email::change_email_get, change_password::change_password_post, change_untis_data::change_untis_data_post, check_session::check_session_get, delete::delete_post, forgot_password::forgot_password_post, gdpr_data_compliance::gdpr_data_compliance_get, get_lernbueros::get_lernbueros, get_timetable::get_timetable, get_free_rooms::get_free_rooms, link::{
change_email::change_email_get, get_free_rooms::get_free_rooms, get_timetable_serviceworker::get_timetable_serviceworker, change_password::change_password_post, change_untis_data::change_untis_data_post, check_session::check_session_get, delete::delete_post, forgot_password::forgot_password_post, gdpr_data_compliance::gdpr_data_compliance_get, get_lernbueros::get_lernbueros, get_timetable::get_timetable, link::{
check_uuid::check_uuid_get, email_change::email_change_post, email_reset::email_reset_post, password::reset_password_post, verify::verify_get
}, login::login_post, logout::logout_post, logout_all::logout_all_post, register::register_post, resend_mail::resend_mail_get, verified::verified_get
};
@@ -207,6 +207,7 @@ async fn main() -> io::Result<()> {
.service(web::resource("/delete").route(web::post().to(delete_post)))
.service(web::resource("/check_session").route(web::get().to(check_session_get)))
.service(web::resource("/get_timetable").route(web::get().to(get_timetable)))
.service(web::resource("/get_timetable_serviceworker").route(web::post().to(get_timetable_serviceworker)))
.service(web::resource("/get_lernbueros").route(web::get().to(get_lernbueros)))
.service(web::resource("/get_free_rooms").route(web::get().to(get_free_rooms)))
.service(web::resource("/change_email").route(web::get().to(change_email_get)))
4 changes: 2 additions & 2 deletions frontend/compileServiceWorker.ts
Original file line number Diff line number Diff line change
@@ -6,11 +6,11 @@ const compileServiceWorker = () => ({
name: "compile-typescript-service-worker",
async writeBundle(_options: any, _outputBundle: any) {
const inputOptions: InputOptions = {
input: "./src/pwa/serviceWorker.ts",
input: "./src/pwa/NotificationWorker.ts",
plugins: [typescript(), terser()]
};
const outputOptions: OutputOptions = {
file: "./.vercel/output/static/serviceWorker.js",
file: "./.vercel/output/static/notificationWorker.js",
format: "es",
compact: true
};
16 changes: 15 additions & 1 deletion frontend/src/api/theBackend.ts
Original file line number Diff line number Diff line change
@@ -93,7 +93,6 @@ export async function registerAccount(
}
export async function getTimetable(monday: string, friday: string, className?: string): Promise<TheScheduleObject[]> {
try {
console.log(className)
let body: { lessons: TheScheduleObject[] };
let searchQuery = `?from=${monday}&until=${friday}`;
if (className) {
@@ -113,6 +112,21 @@ export async function getTimetable(monday: string, friday: string, className?: s
return Promise.reject(error);
}
}
export async function getTimetableServiceWorker(monday: string, friday: string, JSessionId: string, className?: string): Promise<TheScheduleObject[]> {
try {
let body: { lessons: TheScheduleObject[] };
let searchQuery = `?from=${monday}&until=${friday}`;
if (className) {
searchQuery += `&class_name=${className}`;
}
body = await Request.Post<{ lessons: TheScheduleObject[] }>("get_timetable_serviceworker" + searchQuery, {
jsessionid: JSessionId
});
return body.lessons;
} catch (error) {
return Promise.reject(error);
}
}
export async function getLernbueros(monday: string, friday: string): Promise<TheScheduleObject[]> {
try {
let body: { lessons: TheScheduleObject[] };
44 changes: 44 additions & 0 deletions frontend/src/components/NotificationBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*@jsxImportSource preact */

import "@fontsource/inter";
import type { JSX } from "preact";
import { useEffect, useState } from "preact/hooks";
import "../styles/CookieBanner.scss";

export default function NotificationBanner(): JSX.Element | null {
const [bannerContent, setBannerContent] = useState<JSX.Element | null>(null);
const [showBanner, toggleBanner] = useState<boolean>(true);

useEffect(() => {
if (!document.cookie.match(/^(.*;)?\s*notification-consent\s*=\s*[^;]+(.*)?$/)) {
setBannerContent(
<div className="cookie-banner-container">
<p className="consent-message">
Wir können dir Benachrichtigungen über Entfälle senden.
<br />
Du kannst sie jederzeit in den Einstellungen ändern
</p>
<button className="consent-button" onClick={setConsentCookie}>
Benachrichtigungen erlauben
</button>
</div>
);
} else {
setBannerContent(null);
}
}, [showBanner]);
const setConsentCookie = () => {
Notification.requestPermission().then((permission) => {
if (permission === "granted") {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/notificationWorker.js", { scope: "/home" }).then((worker) => {
console.log(worker.scope);
});
}
}
});
document.cookie = `notification-consent=True; max-age=15552000;`;
toggleBanner(false);
};
return bannerContent;
}
2 changes: 2 additions & 0 deletions frontend/src/components/plan-components/Settings.tsx
Original file line number Diff line number Diff line change
@@ -13,6 +13,7 @@ import UserData from "./settings-components/UserData";
import { onSwipe } from "../../api/Touch";
import { accountIsVerified, resendVerifyEmail } from "../../api/theBackend";
import { getCommitHash } from "../../api/main";
import Notifications from "./settings-components/Notifications";

export default function Settings(): JSX.Element {
const [commitHash, setCommitHash] = useState("");
@@ -67,6 +68,7 @@ export default function Settings(): JSX.Element {
{MenuButton("Account löschen", <DeleteAccount />)}
{MenuButton("Abmelden", <Logout />)}
{MenuButton("Daten anfordern", <UserData />)}
{MenuButton("Benachrichtigungen", <Notifications />)}
</div>
);

Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/* @jsxImportSource preact */

import "../../../styles/SettingsElement.scss";
import type { JSX } from "preact";
import { useState } from "preact/hooks";
import { getLocalUntisCredentials } from "../../../api/untisAPI";

export default function Notifications(): JSX.Element {
const [errorMessage, setErrorMessage] = useState(<p></p>);
const unsubscribeNotification = async () => {
try {
const worker = await navigator.serviceWorker.getRegistration();
await worker?.unregister();
setErrorMessage(<p>Benachrichtigungen deaktiviert</p>);
} catch {}
};
const enableNotification = async () => {
await Notification.requestPermission();
if (Notification.permission === "granted") {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/notificationWorker.js", { scope: "/home" }).then((worker) => {
worker.active?.postMessage(getLocalUntisCredentials());
});
navigator.serviceWorker.ready.then((worker) => {
worker.active?.postMessage(getLocalUntisCredentials());
});
}
setErrorMessage(<p>Benachrichtigungen aktiviert</p>);
}
};
return (
<div class="page-content">
<div class="form-container">
<h2>Verwalte deine Benachrichtigungen</h2>
<button onClick={enableNotification}>Benachrichtigungen aktivieren</button>
<button onClick={unsubscribeNotification}>Benachrichtigungen deaktiveren</button>
<div class="error-message">{errorMessage}</div>
</div>
</div>
);
}
19 changes: 19 additions & 0 deletions frontend/src/layouts/PlanLayout.astro
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
---
import NotificationBanner from "../components/NotificationBanner";
import RootLayout from "./RootLayout.astro";
export interface Props {
title: string;
@@ -7,9 +8,27 @@ const { title } = Astro.props;
---

<RootLayout {title}>
<NotificationBanner client:load />
<slot />
</RootLayout>

<script>
import { getLocalUntisCredentials } from "../api/untisAPI";

if ("serviceWorker" in navigator) {
if(Notification.permission == "granted") {
navigator.serviceWorker.register("/notificationWorker.js", { scope: "/home" }).then((worker) => {
});
navigator.serviceWorker.ready.then((worker) => {
worker.active?.postMessage(getLocalUntisCredentials())
})
}
else {
console.log("permission denied")
}
}
</script>

<style>
body {
font-family: Inter;
41 changes: 17 additions & 24 deletions frontend/src/layouts/RootLayout.astro
Original file line number Diff line number Diff line change
@@ -198,29 +198,22 @@ const { title } = Astro.props;
<CookieBanner client:load />
<slot />
</body>
<script>
/*if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/serviceWorker.js", { scope: "/" }).then((worker) => {
console.log(worker.scope);
});
}*/
</script>
<style is:global>
:root {
--background-color: #24273a;
--text-color: #cad3f5;
--text-gradient: linear-gradient(
90deg,
rgba(198, 160, 246, 1) 0%,
rgba(237, 135, 150, 1) 50%,
rgba(238, 212, 159, 1) 100%
);
--highlight-blue: #5974e2;
--highlight-red: #e44040;
}
::selection {
background: #1a1d2f;
color: #b8c0e0;
}
<style is:global>
:root {
--background-color: #24273a;
--text-color: #cad3f5;
--text-gradient: linear-gradient(
90deg,
rgba(198, 160, 246, 1) 0%,
rgba(237, 135, 150, 1) 50%,
rgba(238, 212, 159, 1) 100%
);
--highlight-blue: #5974e2;
--highlight-red: #e44040;
}
::selection {
background: #1a1d2f;
color: #b8c0e0;
}
</style>
</html>
100 changes: 100 additions & 0 deletions frontend/src/pwa/NotificationWorker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { fetchJSessionId } from "../api/untisAPI";
import { getTimetableServiceWorker } from "../api/theBackend";
import type { TheScheduleObject } from "../api/main";

self.addEventListener("message", async (message) => {
requestTimetable(message.data);
});

async function requestTimetable(untisData: { username: string; password: string }, oldLessons?: TheScheduleObject[]) {
let today = new Date().toISOString().slice(0, 10).replace(/-/g, "");
const JSessionId = (await fetchJSessionId(untisData.username, untisData.password)).JSessionId;
let lessons = await getTimetableServiceWorker(today, today, JSessionId);
if (oldLessons) {
const changedLessons = compareLessons(oldLessons, lessons);
console.log(changedLessons);
handleChanges(changedLessons);
}
if (calculateTimeout() > 15 * 60 * 1000) {
lessons = [];
}
console.log(calculateTimeout());
setTimeout(() => requestTimetable(untisData, lessons), calculateTimeout());
}
function sendNotification(title: string, options: NotificationOptions) {
console.log(title, options);
if (Notification.permission === "granted") {
self.registration.showNotification(title, options);
} else {
console.log("denied");
}
}
function compareLessons(oldLessons: TheScheduleObject[], newLessons: TheScheduleObject[]): TheScheduleObject[] {
// Use .filter to find the lessons that have changed
const changedLessons = newLessons.filter((newLesson, index) => {
const oldLesson = oldLessons[index];
const oldSubstitution = oldLesson.substitution;
const newSubstitution = newLesson.substitution;
if (oldSubstitution && newSubstitution) {
// Check if the substitution's cancelled status or teacher has changed
console.log(oldSubstitution, newSubstitution);
return (
oldSubstitution.cancelled !== newSubstitution.cancelled ||
oldSubstitution.teacher !== newSubstitution.teacher ||
oldSubstitution.room !== newSubstitution.room
);
} else if (!oldSubstitution && newSubstitution) {
return true;
}
return false;
});
return changedLessons;
}
function calculateTimeout(): number {
const now = new Date();
const currentHour = now.getHours();
const startHour = 6;
const endHour = 14; // 2 PM is 14:00 in 24-hour format

if (currentHour >= startHour && currentHour <= endHour) {
// If the current time is between 6:00 AM and 2:00 PM, return 15 minutes in milliseconds
return 15 * 60 * 1000;
} else {
// Calculate the time remaining until the next 6:00 AM
let nextSixAM = new Date(now);
nextSixAM.setHours(startHour, 0, 0, 0);

if (now > nextSixAM) {
// If the current time is after 6:00 AM today, set the next 6:00 AM to tomorrow
nextSixAM.setDate(nextSixAM.getDate() + 1);
}

const timeUntilNextSixAM = nextSixAM.getTime() - now.getTime();
return timeUntilNextSixAM;
}
}
function handleChanges(changedLessons: TheScheduleObject[]) {
changedLessons.forEach((lesson) => {
if (lesson.substitution?.teacher == "---" || lesson.substitution?.cancelled) {
if (lesson.length == 2) {
sendNotification(`${lesson.start}. - ${lesson.start + 1}. Stunde entfällt`, {
body: `${lesson.subject_short} bei ${lesson.teacher}`
});
} else {
sendNotification(`${lesson.start}. Stunde entfällt`, {
body: `${lesson.subject_short} bei ${lesson.teacher}`
});
}
} else {
if (lesson.length == 2) {
sendNotification(`Änderungen in ${lesson.start}. - ${lesson.start + 1}. Stunde`, {
body: `${lesson.subject_short} bei ${lesson.teacher}`
});
} else {
sendNotification(`Änderungen in ${lesson.start}. Stunde`, {
body: `${lesson.subject_short} bei ${lesson.teacher}`
});
}
}
});
}

0 comments on commit 42b24a0

Please sign in to comment.