import fastapi from fastapi.middleware.cors import CORSMiddleware import sqlite3 from typing import Annotated import datetime from pydantic import BaseModel from apscheduler.schedulers.background import BackgroundScheduler import secrets from enum import Enum from fastapi.responses import PlainTextResponse app = fastapi.FastAPI(title="Victoria Hall LaundryWeb", description="LaundryWeb Backend API", version="0.1") conn = sqlite3.connect("db.db", check_same_thread=False) cursor = conn.cursor() scheduler = BackgroundScheduler() scheduler.start() origins = [ "http://localhost", "http://localhost:998", "http://localhost:5173", "http://127.0.0.1", "http://127.0.0.1:998", "http://127.0.0.1:5173", "https://laundryweb.altafcreator.com" ] app.add_middleware( CORSMiddleware, allow_origins=origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) cursor.execute(""" CREATE TABLE IF NOT EXISTS timers ( timer_id INTEGER PRIMARY KEY, user_id VARCHAR(64) NOT NULL, start_time TEXT NOT NULL, duration INT NOT NULL, block INT NOT NULL, machine INT NOT NULL, status TEXT NOT NULL CHECK(status IN ('RUNNING', 'FINISHED')) ); """) # block is either 1 or 2, machine (1-4), odd is dryer, even is machine. class RequestData(BaseModel): duration: int block: int machine: int class BlockRequestData(BaseModel): block: int class FinishRequestData(BaseModel): id: int class Status(Enum): EMPTY = 0, FINISHED = 1, RUNNING = 2, OUTOFSERVICE = 3, machine_status = [[Status.EMPTY.name, Status.EMPTY.name, Status.EMPTY.name, Status.EMPTY.name], [Status.EMPTY.name, Status.EMPTY.name, Status.EMPTY.name, Status.EMPTY.name]] machine_times = [[None, None, None, None], [None, None, None, None]] machine_durations = [[None, None, None, None], [None, None, None, None]] # this method checks for any entry, and starts the previously-terminated schedules # useful if you're restarting the server def restart_terminated_schedules(): cursor.execute("SELECT * FROM timers;") out = cursor.fetchall() print("unfinished timers: " + str(len(out))) for row in out: print(row) end_date = datetime.datetime.fromisoformat(row[2]) + datetime.timedelta(minutes=row[3]) now = datetime.datetime.now() if now > end_date: print("unfinished timer was long gone") scheduler.add_job(final_timer_finished, 'date', run_date=(now + datetime.timedelta(seconds=1)), id=str(row[0]), args=[row[0]]) elif now + datetime.timedelta(minutes=5) > end_date: print("unfinished timer ends in less than five mins") scheduler.add_job(final_timer_finished, 'date', run_date=end_date, id=str(row[0]), args=[row[0]]) else: print("unfinished timer scheduler started") print(row[0]) scheduler.add_job(reminder_timer_finished, 'date', run_date=end_date, id=str(row[0]), args=[row[0]]) def reminder_timer_finished(timer_id): print("timer almost finished", timer_id) end_date = datetime.datetime.now() + datetime.timedelta(seconds=5) scheduler.add_job(final_timer_finished, 'date', run_date=end_date, id=timer_id, args=[timer_id]) cursor.execute(f"SELECT * FROM timers WHERE timer_id = '{timer_id}'") out = cursor.fetchall() for row in out: machine_status[row[4] - 1][row[5] - 1] = Status.FINISHED.name def final_timer_finished(timer_id): print("timer finished!1", timer_id) cursor.execute(f"SELECT * FROM timers WHERE timer_id = '{timer_id}'") out = cursor.fetchall() for row in out: machine_status[row[4] - 1][row[5] - 1] = Status.EMPTY.name machine_times[row[4] - 1][row[5] - 1] = None machine_durations[row[4] - 1][row[5] - 1] = None cursor.execute(f"DELETE FROM timers WHERE timer_id = {timer_id}") conn.commit() def create_session(response: fastapi.Response): cookie = secrets.token_hex(32) response.set_cookie(key="session_key", value=cookie) return cookie # ## beginning print("Hello, world!") restart_terminated_schedules() # ## api endpoints # --- starting new timer @app.post("/start", response_class=PlainTextResponse) async def start_new_timer(data: RequestData, response: fastapi.Response, session_key: Annotated[str | None, fastapi.Cookie()] = None): now = datetime.datetime.now() try: if not session_key: print("no session key, creating.") session_key = create_session(response) except Exception as exception: print("err @ key creation //", exception) response.status_code = fastapi.status.HTTP_500_INTERNAL_SERVER_ERROR return "something went wrong during session key creation" try: print("session key valid", session_key) cursor.execute(f""" INSERT INTO timers (user_id, start_time, duration, block, machine, status) VALUES ('{session_key}', '{now.isoformat()}', {data.duration * 30}, {data.block}, {data.machine}, 'RUNNING') """) conn.commit() cursor.execute("SELECT * FROM timers;") out = cursor.fetchall() for row in out: print(row) end_date = now + datetime.timedelta(minutes=(data.duration * 30) - 5) timer_id = str(out[len(out) - 1][0]) print("timer id", timer_id) except Exception as exception: print("err @ sql stuff //", exception) response.status_code = fastapi.status.HTTP_500_INTERNAL_SERVER_ERROR return "something went wrong during sql stuff" try: scheduler.add_job(reminder_timer_finished, 'date', run_date=end_date, id=timer_id, args=[timer_id]) except Exception as exception: print("err @ scheduler //", exception) response.status_code = fastapi.status.HTTP_500_INTERNAL_SERVER_ERROR return "something went wrong during scheduler stuff" try: machine_status[data.block - 1][data.machine - 1] = Status.RUNNING.name machine_times[data.block - 1][data.machine - 1] = now.isoformat() machine_durations[data.block - 1][data.machine - 1] = data.duration * 30 except Exception as exception: print("err @ machine_status //", exception) response.status_code = fastapi.status.HTTP_500_INTERNAL_SERVER_ERROR return "something went wrong during machine_status setting somehow" return "all good bro timer started" # --- check whether user has laundry or not @app.post("/check", response_class=PlainTextResponse) def check_status(response: fastapi.Response, session_key: Annotated[str | None, fastapi.Cookie()] = None): if not session_key: print("no session key, creating.") session_key = create_session(response) cursor.execute(f"SELECT * FROM timers WHERE user_id = '{session_key}'") out = cursor.fetchall() for row in out: print(row) if len(out) > 0: return "you got laundry" else: return "no got laundry" # --- fetch machine status for block @app.post("/status") def get_machine_status(data: BlockRequestData): block_idx = data.block - 1 return [machine_status[block_idx], machine_times[block_idx], machine_durations[block_idx]] # --- get laundr(y/ies) information of user @app.post("/laundry") def get_laundry_info(response: fastapi.Response, session_key: Annotated[str | None, fastapi.Cookie()] = None): if session_key: result = [] cursor.execute(f"SELECT * FROM timers WHERE user_id = '{session_key}'") out = cursor.fetchall() for row in out: curr_timer = { "duration": 0, "start_time": 0, "machine": 0, "status": "RUNNING", "id": -1, } curr_timer["duration"] = row[3] curr_timer["start_time"] = row[2] curr_timer["machine"] = row[5] curr_timer["status"] = row[6] curr_timer["id"] = row[0] result.append(curr_timer) return result else: response.status_code = fastapi.status.HTTP_401_UNAUTHORIZED return "you got no session key cookie how am i supposed to identify you" # --- finish one's laundry @app.post("/finish") def finish_laundry(data: FinishRequestData, response: fastapi.Response, session_key: Annotated[str | None, fastapi.Cookie()] = None): if session_key: cursor.execute(f"SELECT * FROM timers WHERE id = '{data.id}'") row = cursor.fetchall()[0] if session_key != row[1]: response.status_code = fastapi.status.HTTP_401_UNAUTHORIZED return "session key mismatch with timer id, dubious and hence unauthorised." else: response.status_code = fastapi.status.HTTP_401_UNAUTHORIZED return "you got no session key, cannot"