From e3481a4a35091b32b6fbee80c1c9ba2b6d7b50d6 Mon Sep 17 00:00:00 2001 From: erdgeist Date: Sun, 22 Dec 2024 21:53:57 +0100 Subject: Rework of halfnarp and fullnarp into a self contained repository. Still WIP --- fullnarp.py | 157 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 fullnarp.py (limited to 'fullnarp.py') diff --git a/fullnarp.py b/fullnarp.py new file mode 100644 index 0000000..7c98785 --- /dev/null +++ b/fullnarp.py @@ -0,0 +1,157 @@ +import asyncio +import json +import websockets +from os import listdir +from websockets.exceptions import ConnectionClosedOK + +""" +This is best served by an nginx block that should look a bit like this: + + location /fullnarp/ { + root /home/halfnarp; + index index.html index.htm; + } + + location /fullnarp/ws/ { + proxy_pass http://127.0.0.1:5009; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # Set keepalive timeout + proxy_read_timeout 60; # Set a read timeout to prevent connection closures + proxy_send_timeout 60; # Set a send timeout to prevent connection closures + } + +When importing talks from pretalx with halfnarp2.py -i, it creates a file in +var/talks_local_fullnarp which contains non-public lectures and privacy relevant +speaker availibilities. It should only be served behind some auth. +""" + +# Shared state +current_version = {} +newest_version = 0 +current_version_lock = asyncio.Lock() # Lock for managing access to the global state + +clients = {} # Key: websocket, Value: {'client_id': ..., 'last_version': ...} + + +async def notify_clients(): + """Notify all connected clients of the current state.""" + async with current_version_lock: + # Prepare a full state update message with the current version + message = {"current_version": newest_version, "data": current_version} + + # Notify each client about their relevant updates + for client, info in clients.items(): + try: + # Send the state update + await client.send(json.dumps(message)) + # Update the client's last known version + info["last_version"] = newest_version + except ConnectionClosedOK: + # Handle disconnected clients gracefully + pass + + +async def handle_client(websocket): + """Handle incoming WebSocket connections.""" + + # Initialize per-connection state + clients[websocket] = {"client_id": id(websocket), "last_version": 0} + + try: + # Send the current global state to the newly connected client + async with current_version_lock: + global newest_version + await websocket.send( + json.dumps({"current_version": newest_version, "data": current_version}) + ) + clients[websocket][ + "last_version" + ] = newest_version # Update last known version + + # Listen for updates from the client + async for message in websocket: + try: + # Parse incoming message + data = json.loads(message) + + # Update global state with a lock to prevent race conditions + async with current_version_lock: + if "setevent" in data: + eventid = data["setevent"] + day = data["day"] + room = data["room"] + time = data["time"] + lastupdate = data["lastupdate"] + + newest_version += 1 # Increment the version + print( + "Moving event: " + + eventid + + " to day " + + day + + " at " + + time + + " in room " + + room + + " newcurrentversion " + + str(newest_version) + ) + + if not eventid in current_version or int( + current_version[eventid]["lastupdate"] + ) <= int(lastupdate): + current_version[eventid] = { + "day": day, + "room": room, + "time": time, + "lastupdate": int(newest_version), + } + with open( + "versions/fullnarp_" + + str(newest_version).zfill(5) + + ".json", + "w", + ) as outfile: + json.dump(current_version, outfile) + + # Notify all clients about the updated global state + await notify_clients() + except json.JSONDecodeError: + await websocket.send(json.dumps({"error": "Invalid JSON"})) + except websockets.exceptions.ConnectionClosedError as e: + print(f"Client disconnected abruptly: {e}") + except ConnectionClosedOK: + pass + finally: + # Cleanup when the client disconnects + del clients[websocket] + + +async def main(): + newest_file = sorted(listdir("versions/"))[-1] + global newest_version + global current_version + + if newest_file: + newest_version = int(newest_file.replace("fullnarp_", "").replace(".json", "")) + print("Resuming from version: " + str(newest_version)) + with open("versions/" + str(newest_file)) as data_file: + current_version = json.load(data_file) + else: + current_version = {} + newest_version = 0 + + async with websockets.serve(handle_client, "localhost", 5009): + print("WebSocket server started on ws://localhost:5009") + await asyncio.Future() # Run forever + + +if __name__ == "__main__": + asyncio.run(main()) -- cgit v1.2.3