init with crummy buzzer & docker setup
so much boilerplate :O
This commit is contained in:
commit
f6bf544f54
23 changed files with 746 additions and 0 deletions
0
quiz/__init__.py
Normal file
0
quiz/__init__.py
Normal file
20
quiz/__main__.py
Normal file
20
quiz/__main__.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import asyncio
|
||||
import logging
|
||||
|
||||
from .quiz import server
|
||||
|
||||
|
||||
def main():
|
||||
logging.basicConfig(
|
||||
format="{asctime},{msecs:03.0f} [{name}:{process}] {levelname}: {message}",
|
||||
style="{",
|
||||
datefmt="%Y-%m-%d %H:%M:%a",
|
||||
level=logging.INFO,
|
||||
)
|
||||
|
||||
asyncio.get_event_loop().run_until_complete(server())
|
||||
asyncio.get_event_loop().run_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
140
quiz/quiz.py
Normal file
140
quiz/quiz.py
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
import asyncio
|
||||
import logging
|
||||
import unicodedata
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
from json import dumps, loads
|
||||
from secrets import token_hex
|
||||
from time import perf_counter_ns
|
||||
from typing import *
|
||||
|
||||
import websockets
|
||||
|
||||
Websocket = websockets.WebSocketServerProtocol
|
||||
|
||||
|
||||
@dataclass
|
||||
class Client:
|
||||
ws: Websocket
|
||||
path: str
|
||||
id: str = field(default_factory=lambda: token_hex(8))
|
||||
name: str = ""
|
||||
|
||||
def __str__(self):
|
||||
return f"{ascii(self.id)[1:-1]}:{ascii(self.name)[1:-1]}"
|
||||
|
||||
|
||||
sessions: dict[str, dict[str, Client]] = defaultdict(dict)
|
||||
|
||||
log = logging.getLogger("buzzer")
|
||||
|
||||
|
||||
async def send_time(client):
|
||||
await client.ws.send(dumps({"type": "time", "value": perf_counter_ns()}))
|
||||
|
||||
|
||||
async def send_buzz(target, client, time):
|
||||
await target.ws.send(dumps({"type": "buzz", "client": client.id, "time": time}))
|
||||
|
||||
|
||||
async def send_clients(client):
|
||||
clients = [
|
||||
{"name": c.name or "<noname>", "id": c.id}
|
||||
for c in sessions[client.path].values()
|
||||
]
|
||||
await client.ws.send(dumps({"type": "clients", "value": clients}))
|
||||
|
||||
|
||||
async def wait(coros, **kwds):
|
||||
"""Schedule and wait for the given coroutines to complete."""
|
||||
tasks = [asyncio.create_task(f) for f in coros]
|
||||
if not tasks:
|
||||
return
|
||||
return await asyncio.wait(tasks, **kwds)
|
||||
|
||||
|
||||
async def broadcast_client(client):
|
||||
msg = dumps(
|
||||
{
|
||||
"type": "client",
|
||||
"value": {"name": client.name or "<noname>", "id": client.id},
|
||||
}
|
||||
)
|
||||
await wait(c.ws.send(msg) for c in sessions[client.path].values())
|
||||
|
||||
|
||||
async def broadcast_clients(client):
|
||||
await wait(send_clients(c) for c in sessions[client.path].values())
|
||||
|
||||
|
||||
async def broadcast_buzz(client, time):
|
||||
await wait(send_buzz(c, client, time) for c in sessions[client.path].values())
|
||||
|
||||
|
||||
async def send_id(client):
|
||||
await client.ws.send(dumps({"type": "id", "value": client.id}))
|
||||
|
||||
|
||||
async def send_hello(client):
|
||||
await wait([send_time(client), send_id(client), send_clients(client)])
|
||||
|
||||
|
||||
async def send_heartbeat(client):
|
||||
await asyncio.sleep(5.0)
|
||||
await send_time(client)
|
||||
|
||||
|
||||
def printable(s: str) -> str:
|
||||
# See https://www.unicode.org/versions/Unicode13.0.0/ch04.pdf "Table 4-4."
|
||||
return "".join(c for c in s if not unicodedata.category(c).startswith("C"))
|
||||
|
||||
|
||||
async def handle_messages(client):
|
||||
async for message in client.ws:
|
||||
log.debug("[%s] got a message: %a", client, message)
|
||||
mdata = loads(message)
|
||||
if mdata["type"] == "buzz":
|
||||
time = mdata["value"]
|
||||
log.info("[%s] buzz: %a", client, time)
|
||||
# todo: check time against perf_counter_ns
|
||||
await broadcast_buzz(client, time)
|
||||
elif mdata["type"] == "name":
|
||||
name = printable(mdata["value"])
|
||||
log.info("[%s] new name: %a", client, name)
|
||||
client.name = name
|
||||
await broadcast_client(client)
|
||||
else:
|
||||
log.error("[%s] received borked message", client)
|
||||
|
||||
|
||||
async def juggle(client):
|
||||
while client.ws.open:
|
||||
done, pending = await wait(
|
||||
[send_heartbeat(client), handle_messages(client)],
|
||||
return_when=asyncio.FIRST_COMPLETED,
|
||||
)
|
||||
for task in pending:
|
||||
task.cancel()
|
||||
|
||||
|
||||
async def connected(ws: Websocket, path: str):
|
||||
if not path.startswith("/quiz/"):
|
||||
await ws.close()
|
||||
return
|
||||
client = Client(ws, path)
|
||||
log.info("[%s] new client on %a", client, path)
|
||||
sessions[path][client.id] = client
|
||||
try:
|
||||
await send_hello(client)
|
||||
await juggle(client)
|
||||
finally:
|
||||
log.info("[%s] client disconnected", client)
|
||||
del sessions[path][client.id]
|
||||
await broadcast_clients(client)
|
||||
# Clean up sessions map
|
||||
if not sessions[path]:
|
||||
del sessions[path]
|
||||
|
||||
|
||||
def server(host="0.0.0.0", port=8765):
|
||||
return websockets.serve(connected, host, port)
|
||||
Loading…
Add table
Add a link
Reference in a new issue