def _download_layout_description(sio: ServerApp, session: GameSession): try: # You must be a session member to do get the spoiler GameSessionMembership.get_by_ids(sio.get_current_user().id, session.id) except peewee.DoesNotExist: raise NotAuthorizedForAction() if session.layout_description_json is None: raise InvalidAction("Session does not contain a game") if not session.layout_description.permalink.spoiler: raise InvalidAction("Session does not contain a spoiler") return session.layout_description_json
def _get_preset(preset_json: dict) -> VersionedPreset: try: preset = VersionedPreset(preset_json) preset.get_preset() # test if valid return preset except Exception as e: raise InvalidAction(f"invalid preset: {e}")
def _finish_session(sio: ServerApp, session: GameSession): _verify_has_admin(sio, session.id, None) if session.state != GameSessionState.IN_PROGRESS: raise InvalidAction("Session is not in progress") session.state = GameSessionState.FINISHED session.save()
def login_with_guest(sio: ServerApp, encrypted_login_request: bytes): if sio.guest_encrypt is None: raise NotAuthorizedForAction() try: login_request_bytes = sio.guest_encrypt.decrypt(encrypted_login_request) except cryptography.fernet.InvalidToken: raise NotAuthorizedForAction() try: login_request = json.loads(login_request_bytes.decode("utf-8")) name = login_request["name"] date = datetime.datetime.fromisoformat(login_request["date"]) except (UnicodeDecodeError, json.JSONDecodeError, KeyError, ValueError) as e: raise InvalidAction(str(e)) if _get_now() - date > datetime.timedelta(days=1): raise NotAuthorizedForAction() user: User = User.create(name=f"Guest: {name}") with sio.session() as session: session["user-id"] = user.id return _create_client_side_session(sio, user)
def _start_session(sio: ServerApp, session: GameSession): _verify_has_admin(sio, session.id, None) _verify_in_setup(session) if session.layout_description_json is None: raise InvalidAction("Unable to start session, no game is available.") num_players = GameSessionMembership.select().where( GameSessionMembership.session == session, GameSessionMembership.row != None).count() expected_players = session.num_rows if num_players != expected_players: raise InvalidAction( f"Unable to start session, there are {num_players} but expected {expected_players} " f"({session.num_rows} x {session.num_teams}).") session.state = GameSessionState.IN_PROGRESS session.save()
def _find_empty_row(session: GameSession) -> int: possible_rows = set(range(session.num_rows)) for member in GameSessionMembership.non_observer_members(session): possible_rows.remove(member.row) for empty_row in sorted(possible_rows): return empty_row raise InvalidAction("Session is full")
def _change_layout_description(sio: ServerApp, session: GameSession, description_json: Optional[dict]): _verify_has_admin(sio, session.id, None) _verify_in_setup(session) rows_to_update = [] if description_json is None: description = None else: if session.generation_in_progress != sio.get_current_user(): if session.generation_in_progress is None: raise InvalidAction(f"Not waiting for a layout.") else: raise InvalidAction( f"Waiting for a layout from {session.generation_in_progress.name}." ) _verify_no_layout_description(session) description = LayoutDescription.from_json_dict(description_json) if description.player_count != session.num_rows: raise InvalidAction( f"Description is for a {description.player_count} players," f" while the session is for {session.num_rows}.") for permalink_preset, preset_row in zip(description.all_presets, session.presets): preset_row = typing.cast(GameSessionPreset, preset_row) if _get_preset(json.loads( preset_row.preset)).get_preset() != permalink_preset: preset = VersionedPreset.with_preset(permalink_preset) if preset.game not in session.allowed_games: raise InvalidAction(f"{preset.game} preset not allowed.") preset_row.preset = json.dumps(preset.as_json) rows_to_update.append(preset_row) with database.db.atomic(): for preset_row in rows_to_update: preset_row.save() session.generation_in_progress = None session.layout_description = description session.save() _add_audit_entry( sio, session, "Removed generated game" if description is None else f"Set game to {description.shareable_word_hash}")
def _finish_session(sio: ServerApp, session: GameSession): _verify_has_admin(sio, session.id, None) if session.state != GameSessionState.IN_PROGRESS: raise InvalidAction("Session is not in progress") session.state = GameSessionState.FINISHED logger().info(f"{_describe_session(session)}: Finishing session.") session.save() _add_audit_entry(sio, session, f"Finished session")
def _change_layout_description(sio: ServerApp, session: GameSession, description_json: Optional[dict]): _verify_has_admin(sio, session.id, None) _verify_in_setup(session) rows_to_update = [] if description_json is None: description = None else: if session.generation_in_progress != sio.get_current_user(): if session.generation_in_progress is None: raise InvalidAction(f"Not waiting for a layout.") else: raise InvalidAction( f"Waiting for a layout from {session.generation_in_progress.name}." ) _verify_no_layout_description(session) description = LayoutDescription.from_json_dict(description_json) permalink = description.permalink if permalink.player_count != session.num_rows: raise InvalidAction( f"Description is for a {permalink.player_count} players," f" while the session is for {session.num_rows}.") for permalink_preset, preset_row in zip(permalink.presets.values(), session.presets): preset_row = typing.cast(GameSessionPreset, preset_row) if _get_preset(json.loads( preset_row.preset)).get_preset() != permalink_preset: preset = VersionedPreset.with_preset(permalink_preset) if preset.game != RandovaniaGame.PRIME2: raise InvalidAction("Only Prime 2 presets allowed.") preset_row.preset = json.dumps(preset.as_json) rows_to_update.append(preset_row) with database.db.atomic(): for preset_row in rows_to_update: preset_row.save() session.generation_in_progress = None session.layout_description = description session.save()
def _change_row(sio: ServerApp, session: GameSession, arg: Tuple[int, dict]): if len(arg) != 2: raise InvalidAction("Missing arguments.") row_id, preset_json = arg _verify_has_admin(sio, session.id, sio.get_current_user().id) _verify_in_setup(session) _verify_no_layout_description(session) preset = _get_preset(preset_json) try: with database.db.atomic(): preset_row = GameSessionPreset.get( GameSessionPreset.session == session, GameSessionPreset.row == row_id) preset_row.preset = json.dumps(preset.as_json) preset_row.save() except peewee.DoesNotExist: raise InvalidAction(f"invalid row: {row_id}")
def _delete_row(sio: ServerApp, session: GameSession, row_id: int): _verify_has_admin(sio, session.id, None) _verify_in_setup(session) _verify_no_layout_description(session) if session.num_rows < 2: raise InvalidAction("Can't delete row when there's only one") if row_id != session.num_rows - 1: raise InvalidAction(f"Can only delete the last row") with database.db.atomic(): GameSessionPreset.delete().where( GameSessionPreset.session == session, GameSessionPreset.row == row_id).execute() GameSessionMembership.update(row=None).where( GameSessionMembership.session == session.id, GameSessionMembership.row == row_id, ).execute()
def _find_empty_row(session: GameSession) -> int: empty_row = 0 for possible_slot in GameSessionMembership.non_observer_members(session): if empty_row != possible_slot.row: break else: empty_row += 1 if empty_row >= session.num_rows: raise InvalidAction("Session is full") return empty_row
def game_session_collect_locations(sio: ServerApp, session_id: int, pickup_locations: Tuple[int, ...]): current_user = sio.get_current_user() session: GameSession = database.GameSession.get_by_id(session_id) membership = GameSessionMembership.get_by_ids(current_user.id, session_id) if session.state != GameSessionState.IN_PROGRESS: raise InvalidAction( "Unable to collect locations of sessions that aren't in progress") if membership.is_observer: raise InvalidAction("Observers can't collect locations") description = session.layout_description receiver_players = set() for location in pickup_locations: receiver_player = _collect_location(session, membership, description, location) if receiver_player is not None: receiver_players.add(receiver_player) if not receiver_players: return for receiver_player in receiver_players: try: receiver_membership = GameSessionMembership.get_by_session_position( session, row=receiver_player) flask_socketio.emit( "game_has_update", { "session": session_id, "row": receiver_player, }, room= f"game-session-{receiver_membership.session.id}-{receiver_membership.user.id}" ) except peewee.DoesNotExist: pass _emit_session_update(session)
def _change_row(sio: ServerApp, session: GameSession, arg: Tuple[int, dict]): if len(arg) != 2: raise InvalidAction("Missing arguments.") row_id, preset_json = arg _verify_has_admin(sio, session.id, sio.get_current_user().id) _verify_in_setup(session) _verify_no_layout_description(session) preset = _get_preset(preset_json) # if preset.game != RandovaniaGame.PRIME2: # raise InvalidAction("Only Prime 2 presets allowed.") try: with database.db.atomic(): preset_row = GameSessionPreset.get( GameSessionPreset.session == session, GameSessionPreset.row == row_id) preset_row.preset = json.dumps(preset.as_json) logger().info(f"Session {session.id}: Changing row {row_id}.") preset_row.save() except peewee.DoesNotExist: raise InvalidAction(f"invalid row: {row_id}")
def game_session_collect_locations(sio: ServerApp, session_id: int, pickup_locations: Tuple[int, ...]): current_user = sio.get_current_user() session: GameSession = database.GameSession.get_by_id(session_id) membership = GameSessionMembership.get_by_ids(current_user.id, session_id) if session.state != GameSessionState.IN_PROGRESS: raise InvalidAction( "Unable to collect locations of sessions that aren't in progress") if membership.is_observer: raise InvalidAction("Observers can't collect locations") logger().info( f"{_describe_session(session, membership)} found items {pickup_locations}" ) description = session.layout_description receiver_players = set() for location in pickup_locations: receiver_player = _collect_location(session, membership, description, location) if receiver_player is not None: receiver_players.add(receiver_player) if not receiver_players: return for receiver_player in receiver_players: try: receiver_membership = GameSessionMembership.get_by_session_position( session, row=receiver_player) _emit_game_session_pickups_update(sio, receiver_membership) except peewee.DoesNotExist: pass _emit_session_actions_update(session)
def _update_layout_generation(sio: ServerApp, session: GameSession, active: bool): _verify_has_admin(sio, session.id, None) _verify_in_setup(session) if active: if session.generation_in_progress is None: session.generation_in_progress = sio.get_current_user() else: raise InvalidAction( f"Generation already in progress by {session.generation_in_progress.name}." ) else: session.generation_in_progress = None session.save()
def game_session_admin_player(sio: ServerApp, session_id: int, user_id: int, action: str, arg): _verify_has_admin(sio, session_id, user_id) action: SessionAdminUserAction = SessionAdminUserAction(action) session: GameSession = database.GameSession.get_by_id(session_id) membership = GameSessionMembership.get_by_ids(user_id, session_id) if action == SessionAdminUserAction.KICK: _add_audit_entry( sio, session, f"Kicked {membership.effective_name}" if membership.user != sio.get_current_user() else "Left session") membership.delete_instance() if not list(session.players): session.delete_instance(recursive=True) logger().info( f"{_describe_session(session)}. Kicking user {user_id} and deleting session." ) else: logger().info( f"{_describe_session(session)}. Kicking user {user_id}.") elif action == SessionAdminUserAction.MOVE: offset: int = arg if membership.is_observer is None: raise InvalidAction("Player is an observer") new_row = membership.row + offset if new_row < 0: raise InvalidAction("New position is negative") if new_row >= session.num_rows: raise InvalidAction("New position is beyond num of rows") team_members = [None] * session.num_rows for member in GameSessionMembership.non_observer_members(session): team_members[member.row] = member while (0 <= new_row < session.num_rows) and team_members[new_row] is not None: new_row += offset if new_row < 0 or new_row >= session.num_rows: raise InvalidAction("No empty slots found in this direction") with database.db.atomic(): logger().info( f"{_describe_session(session)}, User {user_id}. " f"Performing {action}, new row is {new_row}, from {membership.row}." ) membership.row = new_row membership.save() elif action == SessionAdminUserAction.SWITCH_IS_OBSERVER: if membership.is_observer: membership.row = _find_empty_row(session) else: membership.row = None logger().info( f"{_describe_session(session)}, User {user_id}. Performing {action}, " f"new row is {membership.row}.") membership.save() elif action == SessionAdminUserAction.SWITCH_ADMIN: # Must be admin for this _verify_has_admin(sio, session_id, None, allow_when_no_admins=True) num_admins = GameSessionMembership.select().where( GameSessionMembership.session == session_id, GameSessionMembership.admin == True).count() if membership.admin and num_admins <= 1: raise InvalidAction("can't demote the only admin") membership.admin = not membership.admin _add_audit_entry( sio, session, f"Made {membership.effective_name} {'' if membership.admin else 'not '}an admin" ) logger().info( f"{_describe_session(session)}, User {user_id}. Performing {action}, " f"new status is {membership.admin}.") membership.save() elif action == SessionAdminUserAction.CREATE_PATCHER_FILE: player_names = {i: f"Player {i + 1}" for i in range(session.num_rows)} for member in GameSessionMembership.non_observer_members(session): player_names[member.row] = member.effective_name layout_description = session.layout_description players_config = PlayersConfiguration( player_index=membership.row, player_names=player_names, ) preset = layout_description.get_preset(players_config.player_index) cosmetic_patches = preset.game.data.layout.cosmetic_patches.from_json( arg) _add_audit_entry(sio, session, f"Made an ISO for row {membership.row + 1}") data_factory = preset.game.patch_data_factory(layout_description, players_config, cosmetic_patches) try: return data_factory.create_data() except Exception as e: logger().exception("Error when creating patch data") raise InvalidAction(f"Unable to export game: {e}") elif action == SessionAdminUserAction.ABANDON: # FIXME raise InvalidAction("Abandon is NYI") _emit_session_meta_update(session)
def _reset_session(sio: ServerApp, session: GameSession): raise InvalidAction("Restart session is not yet implemented.")
async def test_handle_network_errors_success(skip_qtbot, qapp): callee = AsyncMock() callee.return_value = MagicMock() data = MagicMock() # Run wrapped = qt_network_client.handle_network_errors(callee) result = await wrapped(qapp, "foo", data) # Assert callee.assert_awaited_once_with(qapp, "foo", data) assert result is callee.return_value @pytest.mark.parametrize(["exception", "title", "message"], [ (InvalidAction("something"), "Invalid action", "Invalid Action: something"), (ServerError(), "Server error", "An error occurred on the server while processing your request."), (NotAuthorizedForAction(), "Unauthorized", "You're not authorized to perform that action."), (NotLoggedIn(), "Unauthenticated", "You must be logged in."), (RequestTimeout("5s timeout"), "Connection Error", "<b>Timeout while communicating with the server:</b><br /><br />Request timed out: 5s timeout<br />" "Further attempts will wait for longer."), ]) async def test_handle_network_errors_exception(skip_qtbot, qapp, mocker, exception, title, message): mock_dialog = mocker.patch("randovania.gui.lib.async_dialog.warning", new_callable=AsyncMock) callee = AsyncMock()
def _verify_no_layout_description(session: GameSession): if session.layout_description_json is not None: raise InvalidAction("Session has a generated game")
def _verify_in_setup(session: GameSession): if session.state != GameSessionState.SETUP: raise InvalidAction("Session is not in setup")
def game_session_admin_player(sio: ServerApp, session_id: int, user_id: int, action: str, arg): _verify_has_admin(sio, session_id, user_id) action: SessionAdminUserAction = SessionAdminUserAction(action) session: GameSession = database.GameSession.get_by_id(session_id) membership = GameSessionMembership.get_by_ids(user_id, session_id) if action == SessionAdminUserAction.KICK: membership.delete_instance() if not list(session.players): session.delete_instance(recursive=True) logger().info( f"Session {session_id}. Kicking user {user_id} and deleting session." ) else: logger().info(f"Session {session_id}. Kicking user {user_id}.") elif action == SessionAdminUserAction.MOVE: offset: int = arg if membership.is_observer is None: raise InvalidAction("Player is an observer") new_row = membership.row + offset if new_row < 0: raise InvalidAction("New position is negative") if new_row >= session.num_rows: raise InvalidAction("New position is beyond num of rows") team_members = [None] * session.num_rows for member in GameSessionMembership.non_observer_members(session): team_members[member.row] = member while (0 <= new_row < session.num_rows) and team_members[new_row] is not None: new_row += offset if new_row < 0 or new_row >= session.num_rows: raise InvalidAction("No empty slots found in this direction") with database.db.atomic(): logger().info( f"Session {session_id}, User {user_id}. " f"Performing {action}, new row is {new_row}, from {membership.row}." ) membership.row = new_row membership.save() elif action == SessionAdminUserAction.SWITCH_IS_OBSERVER: if membership.is_observer: membership.row = _find_empty_row(session) else: membership.row = None logger().info( f"Session {session_id}, User {user_id}. Performing {action}, new row is {membership.row}." ) membership.save() elif action == SessionAdminUserAction.SWITCH_ADMIN: # Must be admin for this _verify_has_admin(sio, session_id, None, allow_when_no_admins=True) num_admins = GameSessionMembership.select().where( GameSessionMembership.session == session_id, GameSessionMembership.admin == True).count() if membership.admin and num_admins <= 1: raise InvalidAction("can't demote the only admin") membership.admin = not membership.admin logger().info( f"Session {session_id}, User {user_id}. Performing {action}, new status is {membership.admin}." ) membership.save() elif action == SessionAdminUserAction.CREATE_PATCHER_FILE: cosmetic_patches = CosmeticPatches.from_json_dict(arg) player_names = {i: f"Player {i + 1}" for i in range(session.num_rows)} for member in GameSessionMembership.non_observer_members(session): player_names[member.row] = member.effective_name players_config = PlayersConfiguration( player_index=membership.row, player_names=player_names, ) return patcher_file.create_patcher_file(session.layout_description, players_config, cosmetic_patches) elif action == SessionAdminUserAction.ABANDON: # FIXME raise InvalidAction("Abandon is NYI") _emit_session_update(session)