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())