import asyncio import json import logging import re from websockets.asyncio.connection import broadcast from websockets.asyncio.server import serve __VERSION__ = "0.0.1" logging.basicConfig( level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s" ) LOG = logging.getLogger() 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): LOG.info(">>> New client %s connected", self.client) exc = None try: await self._handle_client() except Exception as e: exc = e finally: # FIXME: basically handle disconnect / leave from room LOG.info("<<< Client %s id %s disconnected: %s", self.client, self.id, exc) if self.curr_freq: await self._leave_room() @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: try: data = json.loads(data) except json.JSONDecodeError: self._send_error("Could not decode message, invalid json") LOG.error("client %s sent broken data %s", self.client, repr(data)) continue if not isinstance(data, dict) or "cmd" not in data: await self._send_error("Invalid format in json") LOG.error("client %s sent broken data (no cmd key in data) %s", self.client, repr(data)) continue LOG.info("client %s wrote: %s", self.client, data) match data["cmd"]: case "quit": break case "create": await self._create_room(data) case "join": await self._join_room(data) case "leave": await self._leave_room() 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": await self._handle_morse_state(data) 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: if data.get("join-if-present"): await self._join_room({"freq": freq}) else: 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, self_id=self.id, other_players=[]) 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.curr_freq = freq self.freqs[freq].append(self) # FIXME: do we need locking here? LOG.debug("FREQ %s %s %s", self.curr_freq, freq, self.freqs) await self._send(type="join", freq=self.curr_freq, self_id=self.id, other_players=[c.id for c in self._others(freq)]) await self._send_to_group(self._others(freq), type="player-joined", player=self.id) async def _handle_morse_state(self, data): if not self.curr_freq: await self._send_error("No frequency selected") return if "state" not in data or not isinstance(data["state"], bool): await self._send_error("No state key with type bool in data") return await self._send_to_group(self._others(self.curr_freq), type="morse-state", state=data["state"], from_player=self.id) async def _leave_room(self): if self.curr_freq: await self._send_to_group(self._others(self.curr_freq), type="player-left", player=self.id) try: self.freqs[self.curr_freq].remove(self) except ValueError: LOG.warning("Player %s was not in freq %s", self.id, self.curr_freq) if not self.freqs[self.curr_freq]: del self.freqs[self.curr_freq] self.curr_freq = None else: LOG.warning("Client %s is not on a frequency, sending a 'leave' nontheless", self.client) try: await self._send(type="leave") except Exception: pass def _others(self, freq): return [c for c in self.freqs[freq] if c.id != self.id] async def _send(self, ignore_exceptions=False, **kwargs): data = json.dumps(kwargs).encode() LOG.debug("--> sending out to %s: %s", self.client, data) try: await self.websocket.send(json.dumps(kwargs).encode() + b"\n") except Exception as e: LOG.error("Error sending data to %s: %s", self.client, e) if not ignore_exceptions: raise async def _send_to_group(self, group, **kwargs): LOG.info("broadcast() to %s clients: %s", len(group), kwargs) broadcast([c.websocket for c in group], 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 = "0.0.0.0", 3784 async with serve(new_client, HOST, PORT) as server: await server.serve_forever() if __name__ == "__main__": LOG.info("Starting server") asyncio.run(main())