diff options
| author | erdgeist <erdgeist@erdgeist.org> | 2024-12-22 21:53:57 +0100 |
|---|---|---|
| committer | erdgeist <erdgeist@erdgeist.org> | 2024-12-22 21:53:57 +0100 |
| commit | e3481a4a35091b32b6fbee80c1c9ba2b6d7b50d6 (patch) | |
| tree | 58f90b32cbd89599acfaab07377cc0447f1190c1 /fullnarp.py | |
Rework of halfnarp and fullnarp into a self contained repository. Still WIP
Diffstat (limited to 'fullnarp.py')
| -rw-r--r-- | fullnarp.py | 157 |
1 files changed, 157 insertions, 0 deletions
diff --git a/fullnarp.py b/fullnarp.py new file mode 100644 index 0000000..7c98785 --- /dev/null +++ b/fullnarp.py | |||
| @@ -0,0 +1,157 @@ | |||
| 1 | import asyncio | ||
| 2 | import json | ||
| 3 | import websockets | ||
| 4 | from os import listdir | ||
| 5 | from websockets.exceptions import ConnectionClosedOK | ||
| 6 | |||
| 7 | """ | ||
| 8 | This is best served by an nginx block that should look a bit like this: | ||
| 9 | |||
| 10 | location /fullnarp/ { | ||
| 11 | root /home/halfnarp; | ||
| 12 | index index.html index.htm; | ||
| 13 | } | ||
| 14 | |||
| 15 | location /fullnarp/ws/ { | ||
| 16 | proxy_pass http://127.0.0.1:5009; | ||
| 17 | proxy_http_version 1.1; | ||
| 18 | proxy_set_header Upgrade $http_upgrade; | ||
| 19 | proxy_set_header Connection 'upgrade'; | ||
| 20 | proxy_set_header Host $host; | ||
| 21 | proxy_set_header X-Real-IP $remote_addr; | ||
| 22 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; | ||
| 23 | proxy_set_header X-Forwarded-Proto $scheme; | ||
| 24 | |||
| 25 | # Set keepalive timeout | ||
| 26 | proxy_read_timeout 60; # Set a read timeout to prevent connection closures | ||
| 27 | proxy_send_timeout 60; # Set a send timeout to prevent connection closures | ||
| 28 | } | ||
| 29 | |||
| 30 | When importing talks from pretalx with halfnarp2.py -i, it creates a file in | ||
| 31 | var/talks_local_fullnarp which contains non-public lectures and privacy relevant | ||
| 32 | speaker availibilities. It should only be served behind some auth. | ||
| 33 | """ | ||
| 34 | |||
| 35 | # Shared state | ||
| 36 | current_version = {} | ||
| 37 | newest_version = 0 | ||
| 38 | current_version_lock = asyncio.Lock() # Lock for managing access to the global state | ||
| 39 | |||
| 40 | clients = {} # Key: websocket, Value: {'client_id': ..., 'last_version': ...} | ||
| 41 | |||
| 42 | |||
| 43 | async def notify_clients(): | ||
| 44 | """Notify all connected clients of the current state.""" | ||
| 45 | async with current_version_lock: | ||
| 46 | # Prepare a full state update message with the current version | ||
| 47 | message = {"current_version": newest_version, "data": current_version} | ||
| 48 | |||
| 49 | # Notify each client about their relevant updates | ||
| 50 | for client, info in clients.items(): | ||
| 51 | try: | ||
| 52 | # Send the state update | ||
| 53 | await client.send(json.dumps(message)) | ||
| 54 | # Update the client's last known version | ||
| 55 | info["last_version"] = newest_version | ||
| 56 | except ConnectionClosedOK: | ||
| 57 | # Handle disconnected clients gracefully | ||
| 58 | pass | ||
| 59 | |||
| 60 | |||
| 61 | async def handle_client(websocket): | ||
| 62 | """Handle incoming WebSocket connections.""" | ||
| 63 | |||
| 64 | # Initialize per-connection state | ||
| 65 | clients[websocket] = {"client_id": id(websocket), "last_version": 0} | ||
| 66 | |||
| 67 | try: | ||
| 68 | # Send the current global state to the newly connected client | ||
| 69 | async with current_version_lock: | ||
| 70 | global newest_version | ||
| 71 | await websocket.send( | ||
| 72 | json.dumps({"current_version": newest_version, "data": current_version}) | ||
| 73 | ) | ||
| 74 | clients[websocket][ | ||
| 75 | "last_version" | ||
| 76 | ] = newest_version # Update last known version | ||
| 77 | |||
| 78 | # Listen for updates from the client | ||
| 79 | async for message in websocket: | ||
| 80 | try: | ||
| 81 | # Parse incoming message | ||
| 82 | data = json.loads(message) | ||
| 83 | |||
| 84 | # Update global state with a lock to prevent race conditions | ||
| 85 | async with current_version_lock: | ||
| 86 | if "setevent" in data: | ||
| 87 | eventid = data["setevent"] | ||
| 88 | day = data["day"] | ||
| 89 | room = data["room"] | ||
| 90 | time = data["time"] | ||
| 91 | lastupdate = data["lastupdate"] | ||
| 92 | |||
| 93 | newest_version += 1 # Increment the version | ||
| 94 | print( | ||
| 95 | "Moving event: " | ||
| 96 | + eventid | ||
| 97 | + " to day " | ||
| 98 | + day | ||
| 99 | + " at " | ||
| 100 | + time | ||
| 101 | + " in room " | ||
| 102 | + room | ||
| 103 | + " newcurrentversion " | ||
| 104 | + str(newest_version) | ||
| 105 | ) | ||
| 106 | |||
| 107 | if not eventid in current_version or int( | ||
| 108 | current_version[eventid]["lastupdate"] | ||
| 109 | ) <= int(lastupdate): | ||
| 110 | current_version[eventid] = { | ||
| 111 | "day": day, | ||
| 112 | "room": room, | ||
| 113 | "time": time, | ||
| 114 | "lastupdate": int(newest_version), | ||
| 115 | } | ||
| 116 | with open( | ||
| 117 | "versions/fullnarp_" | ||
| 118 | + str(newest_version).zfill(5) | ||
| 119 | + ".json", | ||
| 120 | "w", | ||
| 121 | ) as outfile: | ||
| 122 | json.dump(current_version, outfile) | ||
| 123 | |||
| 124 | # Notify all clients about the updated global state | ||
| 125 | await notify_clients() | ||
| 126 | except json.JSONDecodeError: | ||
| 127 | await websocket.send(json.dumps({"error": "Invalid JSON"})) | ||
| 128 | except websockets.exceptions.ConnectionClosedError as e: | ||
| 129 | print(f"Client disconnected abruptly: {e}") | ||
| 130 | except ConnectionClosedOK: | ||
| 131 | pass | ||
| 132 | finally: | ||
| 133 | # Cleanup when the client disconnects | ||
| 134 | del clients[websocket] | ||
| 135 | |||
| 136 | |||
| 137 | async def main(): | ||
| 138 | newest_file = sorted(listdir("versions/"))[-1] | ||
| 139 | global newest_version | ||
| 140 | global current_version | ||
| 141 | |||
| 142 | if newest_file: | ||
| 143 | newest_version = int(newest_file.replace("fullnarp_", "").replace(".json", "")) | ||
| 144 | print("Resuming from version: " + str(newest_version)) | ||
| 145 | with open("versions/" + str(newest_file)) as data_file: | ||
| 146 | current_version = json.load(data_file) | ||
| 147 | else: | ||
| 148 | current_version = {} | ||
| 149 | newest_version = 0 | ||
| 150 | |||
| 151 | async with websockets.serve(handle_client, "localhost", 5009): | ||
| 152 | print("WebSocket server started on ws://localhost:5009") | ||
| 153 | await asyncio.Future() # Run forever | ||
| 154 | |||
| 155 | |||
| 156 | if __name__ == "__main__": | ||
| 157 | asyncio.run(main()) | ||
