from os import listdir from argparse import ArgumentParser import json import asyncio import websockets from websockets.exceptions import ConnectionClosedOK from sqlalchemy import Column, String, create_engine from sqlalchemy.orm import sessionmaker, declarative_base """ 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())