1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
|
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())
|