def _handle_choose_winner(self, content: dict): try: round_id = RoundID(UUID(hex=content["round"])) except (KeyError, ValueError): raise InvalidRequest("invalid round") try: winner_id = WhiteCardID(UUID(hex=content["winner"])) except (KeyError, ValueError): raise InvalidRequest("invalid winner") self.user.game.choose_winner(round_id, winner_id)
def _handle_chat(self, content: dict): # TODO rate limiting, spam blocking, blacklist try: text = content["text"] if not (isinstance(text, str) and config.chat.is_valid_message(text)): # TODO graceful handling for disallowed text raise InvalidRequest("invalid text") except KeyError: raise InvalidRequest("invalid text") self.user.game.send_event({ "type": "chat_message", "player": self.user.player.to_event_json(), "text": text, })
def _handle_join_game(self, content: dict): try: game_code = GameCode(content["code"]) if not isinstance(game_code, str): raise InvalidRequest("invalid code") except KeyError: raise InvalidRequest("invalid code") try: game = self.server.games.find_by("code", game_code) except KeyError: raise GameError("game_not_found", "game not found") if game.options.password: password = content.get("password", "") if not password: raise GameError("password_required", "a password is required to join the game") if game.options.password.upper() != password.upper(): raise GameError("password_incorrect", "incorrect password") game.add_player(self.user)
def _handle_authenticate(self, content: dict): if self.user: raise GameError("already_authenticated", "already authenticated") if "id" in content and "token" in content: try: user_id = UUID(hex=content["id"]) except (KeyError, ValueError): raise InvalidRequest("invalid id") try: user = self.server.users.find_by("id", user_id) except KeyError: raise GameError("user_not_found", "user not found") if user.token != content["token"]: raise GameError("invalid_token", "invalid token") self.user = user self.user.reconnected(self) elif "name" in content: name = content["name"] if not isinstance( name, str) or not config.users.username.is_valid_name(name): raise InvalidRequest("invalid name") if self.server.users.exists("name", name.lower()): raise GameError("name_in_use", "name already in use") user = User(name, self.server, self) self.server.add_user(user) self.user = user else: raise InvalidRequest("missing id/token or name") LOGGER.info("%s authenticated as %s", self.remote_addr, self.user) result = { "id": str(user.id), "token": user.token, "name": user.name, "in_game": user.game is not None } if user.game: user.game.send_updates(full_resync=True, to=user.player) return result
def _handle_play_white(self, content: dict): cards: List[Tuple[WhiteCardID, Optional[str]]] = [] try: round_id = RoundID(UUID(hex=content["round"])) except (KeyError, ValueError): raise InvalidRequest("invalid round") try: input_cards = content["cards"] for input_card in input_cards: if not isinstance(input_card, dict): raise InvalidRequest("invalid cards") slot_id = WhiteCardID(UUID(hex=input_card["id"])) text = input_card.get("text") if text is not None: if not (isinstance(text, str) and config.game.blank_cards.is_valid_text(text)): # TODO graceful handling for disallowed text raise InvalidRequest("invalid cards") text = text.strip() cards.append((slot_id, text)) except (KeyError, ValueError): raise InvalidRequest("invalid cards") self.user.game.play_white_cards(round_id, self.user.player, cards)
def _handle_kick_player(self, content: dict): try: user_id = UserID(UUID(hex=content["user"])) except (KeyError, ValueError): raise InvalidRequest("invalid user") if user_id == self.user.id: raise InvalidGameState("self_kick", "can't kick yourself") try: player = self.user.game.players.find_by("id", user_id) except KeyError: raise InvalidGameState("player_not_in_game", "the player is not in the game") self.user.game.remove_player(player, LeaveReason.host_kick)
def _handle_game_options(self, content: dict): changes = {} for field in fields(GameOptions): if field.name in content: if self.user.game.game_running and field.name not in GameOptions.updateable_ingame: raise InvalidGameState( "option_locked", f"{field.name} can't be changed while the game is ongoing" ) value = content[field.name] if field.name == "card_packs": try: value = tuple( self.server.card_packs.find_by( "id", CardPackID(UUID(uuid))) for uuid in value) except (TypeError, ValueError, KeyError): raise InvalidRequest("invalid card_packs list") changes[field.name] = value try: new_options = replace(self.user.game.options, **changes) self.user.game.update_options(new_options) except ConfigError as ex: raise GameError("invalid_options", str(ex)) from None
def _handle_request(self, action: ApiAction, call_id: Union[str, int, float], content: dict): # noinspection PyBroadException try: if not self.user and action != ApiAction.authenticate: raise GameError("not_authenticated", "must authenticate first") try: handler = self.handlers[action] except KeyError: raise InvalidRequest("invalid action") call_result = handler(self, content) or {} return {"call_id": call_id, "error": None, **call_result} except GameError as ex: # force a full resync if that will likely be useful if isinstance(ex, InvalidGameState) and self.user and self.user.game: LOGGER.error("%s hit an error likely caused by desync", self.user, exc_info=True) self.user.game.send_updates(full_resync=True, to=self.user.player) return { "call_id": call_id, "error": ex.code, "description": ex.description } except Exception: LOGGER.error("Internal uncaught exception in WebSocket handler.", exc_info=True) return { "call_id": call_id, "error": "internal_error", "description": "internal error" }
async def receive_json_from_client(self, message: Union[str, bytes]): """Called by subclasses when they receive a JSON message from the client.""" if not isinstance(message, str): raise InvalidRequest("only text JSON messages allowed") try: parsed = json.loads(message) except JSONDecodeError: raise InvalidRequest("invalid JSON") if not isinstance(parsed, dict): raise InvalidRequest("only JSON objects allowed") if not self.handshaked: try: if parsed["version"] == UI_VERSION: await self.send_json_to_client( {"config": self.server.config_json()}) self.handshaked = True else: await self.send_json_to_client( {"error": "incorrect_version"}, close=True) return except KeyError: raise InvalidRequest("invalid handshake") try: action = ApiAction[parsed["action"]] call_id = parsed["call_id"] except KeyError: raise InvalidRequest("action or call_id missing or invalid") if not isinstance(action, ApiAction) or not isinstance( call_id, (str, int, float)): raise InvalidRequest("action or call_id missing or invalid") result = self._handle_request(action, call_id, parsed) await self.send_json_to_client(result)