def restore_user_session(sio: ServerApp, encrypted_session: bytes, session_id: Optional[int]): try: decrypted_session: bytes = sio.fernet_encrypt.decrypt( encrypted_session) session = json.loads(decrypted_session.decode("utf-8")) user = User.get_by_id(session["user-id"]) if "discord-access-token" in session: # TODO: test if the discord access token is still valid flask.session["DISCORD_OAUTH2_TOKEN"] = session[ "discord-access-token"] sio.get_server().save_session(flask.request.sid, session) if session_id is not None: sio.join_game_session( GameSessionMembership.get_by_ids(user.id, session_id)) return _create_client_side_session(sio, user) except (KeyError, peewee.DoesNotExist, json.JSONDecodeError): raise InvalidSession() except Exception: logger().exception("Error decoding user session") raise InvalidSession()
def restore_user_session(sio: ServerApp, encrypted_session: bytes, session_id: Optional[int]): try: decrypted_session: bytes = sio.fernet_encrypt.decrypt( encrypted_session) session = json.loads(decrypted_session.decode("utf-8")) if "discord-access-token" in session: user, result = _create_session_with_discord_token( sio, session["discord-access-token"]) else: user = User.get_by_id(session["user-id"]) sio.save_session(session) result = _create_client_side_session(sio, user) if session_id is not None: sio.join_game_session( GameSessionMembership.get_by_ids(user.id, session_id)) return result except UserNotAuthorized: raise except (KeyError, peewee.DoesNotExist, json.JSONDecodeError, InvalidTokenError) as e: # InvalidTokenError: discord token expired and couldn't renew logger().info("Client at %s was unable to restore session: (%s) %s", sio.current_client_ip(), str(type(e)), str(e)) raise InvalidSession() except Exception: logger().exception("Error decoding user session") raise InvalidSession()
def logout(sio: ServerApp): sio.leave_game_session() flask.session.pop("DISCORD_OAUTH2_TOKEN", None) with sio.session() as session: session.pop("discord-access-token", None) session.pop("user-id", None) _emit_user_session_update(sio)
def disconnect_game_session(sio: ServerApp, session_id: int): current_user = sio.get_current_user() try: current_membership = GameSessionMembership.get_by_ids( current_user.id, session_id) current_membership.connection_state = "Offline" current_membership.save() _emit_session_meta_update(current_membership.session) except peewee.DoesNotExist: pass sio.leave_game_session()
def _verify_has_admin(sio: ServerApp, session_id: int, admin_user_id: Optional[int], *, allow_when_no_admins: bool = False) -> None: """ Checks if the logged user can do admin operations to the given session, :param session_id: The GameSessions id :param admin_user_id: An user id that is exceptionally authorized for this :param allow_when_no_admins: This action is authorized for non-admins if there are no admins. :return: """ current_user = sio.get_current_user() try: current_membership = GameSessionMembership.get_by_ids( current_user.id, session_id) except peewee.DoesNotExist: raise NotAuthorizedForAction() if not (current_membership.admin or (admin_user_id is not None and current_user.id == admin_user_id)): if allow_when_no_admins and GameSessionMembership.select().where( GameSessionMembership.session == session_id, GameSessionMembership.admin == True).count() == 0: return raise NotAuthorizedForAction()
def _duplicate_session(sio: ServerApp, session: GameSession, new_title: str): _verify_has_admin(sio, session.id, None) current_user = sio.get_current_user() _add_audit_entry(sio, session, f"Duplicated session as {new_title}") with database.db.atomic(): new_session: GameSession = GameSession.create( name=new_title, password=session.password, creator=current_user, layout_description_json=session.layout_description_json, seed_hash=session.seed_hash, dev_features=session.dev_features, ) for preset in session.presets: assert isinstance(preset, GameSessionPreset) GameSessionPreset.create( session=new_session, row=preset.row, preset=preset.preset, ) GameSessionMembership.create( user=current_user, session=new_session, row=None, admin=True, connection_state="Offline", ) GameSessionAudit.create( session=new_session, user=current_user, message=f"Duplicated from {session.name}", )
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 login_with_discord(sio: ServerApp, code: str): oauth = OAuth2Session( client_id=sio.app.config["DISCORD_CLIENT_ID"], scope=["identify"], redirect_uri=sio.app.config["DISCORD_REDIRECT_URI"], ) access_token = oauth.fetch_token( "https://discord.com/api/oauth2/token", code=code, client_secret=sio.app.config["DISCORD_CLIENT_SECRET"], ) flask.session["DISCORD_OAUTH2_TOKEN"] = access_token discord_user = sio.discord.fetch_user() user: User = User.get_or_create(discord_id=discord_user.id, defaults={"name": discord_user.name})[0] if user.name != discord_user.name: user.name = discord_user.name user.save() with sio.session() as session: session["user-id"] = user.id session["discord-access-token"] = access_token return _create_client_side_session(sio, user)
def _create_client_side_session(sio: ServerApp, user: Optional[User]) -> dict: """ :param user: If the session's user was already retrieved, pass it along to avoid an extra query. :return: """ session = sio.get_session() encrypted_session = sio.fernet_encrypt.encrypt( json.dumps(session).encode("utf-8")) if user is None: user = User.get_by_id(session["user-id"]) elif user.id != session["user-id"]: raise RuntimeError(f"Provided user does not match the session's user") logger().info( f"Client at {sio.current_client_ip()} is user {user.name} ({user.id})." ) return { "user": user.as_json, "sessions": [ membership.session.create_list_entry() for membership in GameSessionMembership.select().where( GameSessionMembership.user == user) ], "encoded_session_b85": base64.b85encode(encrypted_session), }
def game_session_self_update(sio: ServerApp, session_id: int, inventory: str, game_connection_state: str): current_user = sio.get_current_user() membership = GameSessionMembership.get_by_ids(current_user.id, session_id) membership.connection_state = f"Online, {game_connection_state}" membership.inventory = inventory membership.save() _emit_session_update(membership.session)
def game_session_request_pickups(sio: ServerApp, session_id: int): current_user = sio.get_current_user() your_membership = GameSessionMembership.get_by_ids(current_user.id, session_id) session: GameSession = your_membership.session if session.state == GameSessionState.SETUP: logger().info(f"Session {session_id}, Row {your_membership.row} " f"requested pickups, but session is setup.") return None if your_membership.is_observer: logger().info( f"Session {session_id}, {current_user.name} requested pickups, but is an observer." ) return None description = session.layout_description row_to_member_name = { member.row: member.effective_name for member in GameSessionMembership.non_observer_members(session) } resource_database = _get_resource_database(description, your_membership.row) result = [] actions: List[GameSessionTeamAction] = list( _query_for_actions(your_membership)) for action in actions: pickup_target = _get_pickup_target(description, action.provider_row, action.provider_location_index) if pickup_target is None: logging.error( f"Action {action} has a location index with nothing.") result.append(None) else: name = row_to_member_name.get(action.provider_row, f"Player {action.provider_row + 1}") result.append({ "provider_name": name, "pickup": _base64_encode_pickup(pickup_target.pickup, resource_database), }) logger().info( f"Session {session_id}, Row {your_membership.row} " f"requested pickups, returning {len(result)} elements for {resource_database.game_enum.value}." ) return { "game": resource_database.game_enum.value, "pickups": result, }
def create_game_session(sio: ServerApp, session_name: str): current_user = sio.get_current_user() with database.db.atomic(): new_session = GameSession.create( name=session_name, password=None, creator=current_user, ) GameSessionPreset.create( session=new_session, row=0, preset=json.dumps(PresetManager(None).default_preset.as_json)) membership = GameSessionMembership.create(user=sio.get_current_user(), session=new_session, row=0, admin=True) sio.join_game_session(membership) return new_session.create_session_entry()
def game_session_request_update(sio: ServerApp, session_id: int): current_user = sio.get_current_user() session: GameSession = GameSession.get_by_id(session_id) membership = GameSessionMembership.get_by_ids(current_user.id, session_id) _emit_session_meta_update(session) if session.layout_description is not None: _emit_session_actions_update(session) if not membership.is_observer and session.state != GameSessionState.SETUP: _emit_game_session_pickups_update(sio, membership) _emit_session_audit_update(session)
def join_game_session(sio: ServerApp, session_id: int, password: Optional[str]): session = GameSession.get_by_id(session_id) if session.password is not None: if password is None or _hash_password(password) != session.password: raise WrongPassword() elif password is not None: raise WrongPassword() membership = GameSessionMembership.get_or_create( user=sio.get_current_user(), session=session, defaults={ "row": None, "admin": False })[0] _emit_session_update(session) sio.join_game_session(membership) return session.create_session_entry()
def server_app_fixture(flask_app, skip_qtbot): flask_app.config['SECRET_KEY'] = "key" flask_app.config["DISCORD_CLIENT_ID"] = 1234 flask_app.config["DISCORD_CLIENT_SECRET"] = 5678 flask_app.config[ "DISCORD_REDIRECT_URI"] = "http://127.0.0.1:5000/callback/" flask_app.config[ "FERNET_KEY"] = b's2D-pjBIXqEqkbeRvkapeDn82MgZXLLQGZLTgqqZ--A=' flask_app.config[ "GUEST_KEY"] = b's2D-pjBIXqEqkbeRvkapeDn82MgZXLLQGZLTgqqZ--A=' server = ServerApp(flask_app) server.metrics.summary = MagicMock() server.metrics.summary.return_value.side_effect = lambda x: x return server
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 server_app_fixture(flask_app): pytest.importorskip("engineio.async_drivers.threading") flask_app.config['SECRET_KEY'] = "key" flask_app.config["DISCORD_CLIENT_ID"] = 1234 flask_app.config["DISCORD_CLIENT_SECRET"] = 5678 flask_app.config["DISCORD_REDIRECT_URI"] = "http://127.0.0.1:5000/callback/" flask_app.config["FERNET_KEY"] = b's2D-pjBIXqEqkbeRvkapeDn82MgZXLLQGZLTgqqZ--A=' flask_app.config["GUEST_KEY"] = b's2D-pjBIXqEqkbeRvkapeDn82MgZXLLQGZLTgqqZ--A=' flask_app.config["ENFORCE_ROLE"] = None server = ServerApp(flask_app) server.metrics.summary = MagicMock() server.metrics.summary.return_value.side_effect = lambda x: x return server
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 _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 _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 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 setup_app(sio: ServerApp): sio.on("login_with_discord", login_with_discord) sio.on("login_with_guest", login_with_guest) sio.on("restore_user_session", restore_user_session) sio.on("logout", logout) @sio.app.route("/login") def browser_login_with_discord(): return sio.discord.create_session() @sio.app.route("/login_callback") def browser_discord_login_callback(): sio.discord.callback() discord_user = sio.discord.fetch_user() user: User = User.get(discord_id=discord_user.id) if user is None: return "Unknown user", 404 return flask.redirect(flask.url_for("browser_me")) @sio.admin_route("/me") def browser_me(user: User): return f"Hello {user.name}. Admin? {user.admin}"
def _create_session_with_discord_token(sio: ServerApp, access_token: str) -> Tuple[User, dict]: flask.session["DISCORD_OAUTH2_TOKEN"] = access_token discord_user = sio.discord.fetch_user() user: User = User.get_or_create(discord_id=discord_user.id, defaults={"name": discord_user.name})[0] if user.name != discord_user.name: user.name = discord_user.name user.save() if sio.enforce_role is not None: if not sio.enforce_role.verify_user(discord_user.id): logger().info( "User %s is not authorized for connecting to the server", discord_user.name) raise UserNotAuthorized() with sio.session() as session: session["user-id"] = user.id session["discord-access-token"] = access_token return user, _create_client_side_session(sio, user)
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 setup_app(sio: ServerApp): sio.on("login_with_discord", login_with_discord) sio.on("login_with_guest", login_with_guest) sio.on("restore_user_session", restore_user_session) sio.on("logout", logout) sio.on("disconnect_game_session", disconnect_game_session)
def disconnect_game_session(sio: ServerApp): sio.leave_game_session() _emit_user_session_update(sio)
def setup_app(sio: ServerApp): sio.on("list_game_sessions", list_game_sessions, with_header_check=True) sio.on("create_game_session", create_game_session, with_header_check=True) sio.on("join_game_session", join_game_session, with_header_check=True) sio.on("disconnect_game_session", disconnect_game_session) sio.on("game_session_request_update", game_session_request_update) sio.on("game_session_admin_session", game_session_admin_session) sio.on("game_session_admin_player", game_session_admin_player) sio.on("game_session_collect_locations", game_session_collect_locations) sio.on("game_session_self_update", game_session_self_update) @sio.admin_route("/sessions") def admin_sessions(user): paginated_query = flask_utils.PaginatedQuery( GameSession.select().order_by(GameSession.creation_date.desc()), paginate_by=20, check_bounds=True, ) lines = [] for session in paginated_query.get_object_list(): lines.append( "<tr><td><a href='{}'>{}</a></td><td>{}</td><td>{}</td><td>{}</td><td>{}</td></tr>" .format( flask.url_for('admin_session', session_id=session.id), session.name, session.creator.name, session.creation_date, session.state, len(session.players), )) page = paginated_query.get_page() previous = "Previous" if page > 1: previous = "<a href='{}'>Previous</a>".format( flask.url_for(".admin_sessions", page=page - 1)) next_link = "Next" if page < paginated_query.get_page_count(): next_link = "<a href='{}'>Next</a>".format( flask.url_for(".admin_sessions", page=page + 1)) return ( "<table border='1'>" "<tr><th>Name</th><th>Creator</th><th>Creation Date</th><th>State</th><th>Num Players</th></tr>" "{content}</table>Page {page} of {num_pages}. {previous} / {next}." ).format( content="".join(lines), page=page, num_pages=paginated_query.get_page_count(), previous=previous, next=next_link, ) @sio.admin_route("/session/<session_id>") def admin_session(user, session_id): session: GameSession = GameSession.get_by_id(session_id) rows = [] presets = session.all_presets for player in session.players: player = typing.cast(GameSessionMembership, player) if player.is_observer: rows.append([ player.effective_name, "Observer", "", ]) else: preset = presets[player.row] db = default_database.resource_database_for(preset.game) inventory = [] if player.inventory is not None: try: parsed_inventory: list[dict] = BinaryInventory.parse( player.inventory) except construct.ConstructError: # Handle old format in an adhoc way # TODO 4.3: remove this code and purge all old inventory from the server db items_by_id: dict[int, ItemResourceInfo] = { item.extra["item_id"]: item for item in db.item } parsed_inventory = [{ "name": items_by_id[item["index"]].short_name, **item, } for item in OldBinaryInventory.parse( player.inventory)] for item in parsed_inventory: if item["amount"] + item["capacity"] > 0: inventory.append("{} x{}/{}".format( db.get_item(item["name"]).long_name, item["amount"], item["capacity"])) rows.append([ player.effective_name, preset.name, ", ".join(inventory), ]) header = ["Name", "Preset", "Inventory"] return "<table border='1'><tr>{}</tr>{}</table>".format( "".join(f"<th>{h}</th>" for h in header), "".join("<tr>{}</tr>".format("".join(f"<td>{h}</td>" for h in r)) for r in rows), )
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)