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()) | ||