init with crummy buzzer & docker setup

so much boilerplate :O
This commit is contained in:
ducklet 2021-01-29 01:20:17 +01:00
commit f6bf544f54
23 changed files with 746 additions and 0 deletions

0
quiz/__init__.py Normal file
View file

20
quiz/__main__.py Normal file
View 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
View 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)