cw-generator/signalsrv/signalsrv.py

147 lines
4.4 KiB
Python

import asyncio
import json
import re
from websockets.asyncio.server import serve
__VERSION__ = "0.0.1"
class Client:
freqs = {}
freq_re = re.compile(r"^\d+\.\d{3}$")
def __init__(self, websocket):
self.websocket = websocket
self.curr_freq = None
async def handle(self):
print(f" >>> New client {self.client} connected")
exc = None
try:
await self._handle_client()
except Exception as e:
exc = e
finally:
# FIXME: basically handle disconnect / leave from room
print(f" <<< Client {self.client} disconnected: {exc}")
@property
def client(self):
ip, port, *_ = self.websocket.remote_address
if ':' in ip:
ip = f"[{ip}]"
return f"{ip}:{port}"
@property
def id(self):
return str(self.websocket.id)
async def _handle_client(self):
await self._send(type="hello", name="LobbySrv 3000", version=__VERSION__)
async for data in self.websocket:
print(f" <-- client {self.client} sent {repr(data)}")
try:
data = json.loads(data)
except json.JSONDecodeError:
self._send_error("Could not decode message, invalid json")
continue
if not isinstance(data, dict) or "cmd" not in data:
self._send_error("Invalid format in json")
continue
print(f"{self.client} wrote:", data)
match data["cmd"]:
case "quit":
break
case "create":
await self._create_room(data)
case "join":
await self._join_room(data)
case "list":
freqs = [{"freq": freq, "players": len(players)}
for freq, players in self.freqs.items()]
await self._send(type="freq-list", freqs=freqs)
case "disconnect":
pass
case "morse-state":
# FIXME: send to all other clients
pass
case _:
await self._send_error("Unknown command")
async def _create_room(self, data):
if self.curr_freq:
await self._send_error(f"Already on frequency {self.curr_freq}")
return
if "freq" not in data:
await self._send_error("No frequency in create message")
return
freq = data["freq"]
if not self.freq_re.match(freq):
await self._send_error("Invalid frequency")
return
if freq in self.freqs:
await self._send_error("Frequency already in use")
return
self.curr_freq = freq
self.freqs[freq] = [self]
await self._send(type="join", freq=self.curr_freq, others=[])
async def _join_room(self, data):
if self.curr_freq:
await self._send_error(f"Already on frequency {self.curr_freq}")
return
if "freq" not in data:
await self._send_error("No frequency in join message")
return
freq = data["freq"]
if freq not in self.freqs:
await self._send_error(f"Frequency {freq} not available")
return
self.freqs[freq].append(self)
# FIXME: do we need locking here?
await self._send(type="join", freq=self.curr_freq, players=[c.id for c in self._others(freq)])
for other in self._others(freq):
await self._send(type="player-joined", player=self.id)
def _others(self, freq):
return [c for c in self.freqs[freq] if c != self.websocket]
async def _send(self, **kwargs):
data = json.dumps(kwargs).encode()
print(f" --> sending out to {self.client}: {data}")
await self.websocket.send(json.dumps(kwargs).encode() + b"\n")
async def _send_error(self, msg: str):
await self._send(type="error", message=msg)
async def new_client(websocket):
try:
client = Client(websocket)
await client.handle()
finally:
pass
# async for message in websocket:
# await websocket.send(message)
async def main():
HOST, PORT = "localhost", 3784
async with serve(new_client, HOST, PORT) as server:
await server.serve_forever()
if __name__ == "__main__":
asyncio.run(main())