summaryrefslogtreecommitdiff
path: root/fullnarp.py
diff options
context:
space:
mode:
authorerdgeist <erdgeist@erdgeist.org>2024-12-22 21:53:57 +0100
committererdgeist <erdgeist@erdgeist.org>2024-12-22 21:53:57 +0100
commite3481a4a35091b32b6fbee80c1c9ba2b6d7b50d6 (patch)
tree58f90b32cbd89599acfaab07377cc0447f1190c1 /fullnarp.py
Rework of halfnarp and fullnarp into a self contained repository. Still WIP
Diffstat (limited to 'fullnarp.py')
-rw-r--r--fullnarp.py157
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 @@
1import asyncio
2import json
3import websockets
4from os import listdir
5from websockets.exceptions import ConnectionClosedOK
6
7"""
8This 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
30When importing talks from pretalx with halfnarp2.py -i, it creates a file in
31var/talks_local_fullnarp which contains non-public lectures and privacy relevant
32speaker availibilities. It should only be served behind some auth.
33"""
34
35# Shared state
36current_version = {}
37newest_version = 0
38current_version_lock = asyncio.Lock() # Lock for managing access to the global state
39
40clients = {} # Key: websocket, Value: {'client_id': ..., 'last_version': ...}
41
42
43async 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
61async 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
137async 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
156if __name__ == "__main__":
157 asyncio.run(main())