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 _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 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 _change_password(sio: ServerApp, session: GameSession, password: str): _verify_has_admin(sio, session.id, None) session.password = _hash_password(password) logger().info(f"{_describe_session(session)}: Changing password.") session.save() _add_audit_entry(sio, session, f"Changed password")
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"Session {session.id}: Finishing session.") session.save()
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_title(sio: ServerApp, session: GameSession, title: str): _verify_has_admin(sio, session.id, None) old_name = session.name session.name = title logger().info( f"{_describe_session(session)}: Changed name from {old_name}.") session.save() _add_audit_entry(sio, session, f"Changed name from {old_name} to {title}")
def _handler(*args): try: return { "result": handler(self, *args), } except BaseNetworkError as error: return error.as_json except Exception: logger().exception(f"Unhandled exception while processing request for message {message}. Args: {args}") return ServerError().as_json
def _handler(*args): try: return { "result": handler(self, *args), } except BaseNetworkError as error: return error.as_json except Exception: logger().exception( "Unexpected exception while processing request") return ServerError().as_json
def _create_row(sio: ServerApp, session: GameSession, preset_json: dict): _verify_has_admin(sio, session.id, None) _verify_in_setup(session) _verify_no_layout_description(session) preset = _get_preset(preset_json) new_row_id = session.num_rows with database.db.atomic(): logger().info(f"Session {session.id}: Creating row {new_row_id}.") GameSessionPreset.create(session=session, row=new_row_id, preset=json.dumps(preset.as_json))
def _emit_game_session_pickups_update(sio: ServerApp, membership: GameSessionMembership): session: GameSession = membership.session if session.state == GameSessionState.SETUP: raise RuntimeError("Unable to emit pickups during SETUP") if membership.is_observer: raise RuntimeError("Unable to emit pickups for observers") 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, membership.row) result = [] actions: List[GameSessionTeamAction] = list(_query_for_actions(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"{_describe_session(session, membership)} " f"notifying {resource_database.game_enum.value} of {len(result)} pickups." ) data = { "game": resource_database.game_enum.value, "pickups": result, } flask_socketio.emit("game_session_pickups_update", data, room=f"game-session-{session.id}-{membership.user.id}")
def game_session_admin_session(sio: ServerApp, session_id: int, action: str, arg): action: SessionAdminGlobalAction = SessionAdminGlobalAction(action) session: database.GameSession = database.GameSession.get_by_id(session_id) if action == SessionAdminGlobalAction.CREATE_ROW: _create_row(sio, session, arg) elif action == SessionAdminGlobalAction.CHANGE_ROW: _change_row(sio, session, arg) elif action == SessionAdminGlobalAction.DELETE_ROW: _delete_row(sio, session, arg) elif action == SessionAdminGlobalAction.UPDATE_LAYOUT_GENERATION: _update_layout_generation(sio, session, arg) elif action == SessionAdminGlobalAction.CHANGE_LAYOUT_DESCRIPTION: _change_layout_description(sio, session, arg) elif action == SessionAdminGlobalAction.DOWNLOAD_LAYOUT_DESCRIPTION: return _download_layout_description(sio, session) elif action == SessionAdminGlobalAction.START_SESSION: _start_session(sio, session) elif action == SessionAdminGlobalAction.FINISH_SESSION: _finish_session(sio, session) elif action == SessionAdminGlobalAction.RESET_SESSION: _reset_session(sio, session) elif action == SessionAdminGlobalAction.CHANGE_PASSWORD: _change_password(sio, session, arg) elif action == SessionAdminGlobalAction.CHANGE_TITLE: _change_title(sio, session, arg) elif action == SessionAdminGlobalAction.DUPLICATE_SESSION: return _duplicate_session(sio, session, arg) elif action == SessionAdminGlobalAction.DELETE_SESSION: logger().info(f"{_describe_session(session)}: Deleting session.") session.delete_instance(recursive=True) elif action == SessionAdminGlobalAction.REQUEST_PERMALINK: return _get_permalink(sio, session) _emit_session_meta_update(session)
def verify_user(self, user_id: int) -> bool: r = self.session.get( "https://discordapp.com/api/guilds/{}/members/{}".format( self.guild_id, user_id)) try: result = r.json() if r.ok: return self.role_id in result["roles"] else: logger().warning("Unable to verify user %s: %s", user_id, r.text) return False except requests.RequestException as e: logger().warning("Unable to verify user %s: %s / %s", user_id, r.text, str(e)) return True
def _handler(*args): if with_header_check: error_msg = self.check_client_headers() if error_msg is not None: return UnsupportedClient(error_msg).as_json try: return { "result": handler(self, *args), } except BaseNetworkError as error: return error.as_json except (Exception, TypeError): logger().exception( f"Unhandled exception while processing request for message {message}. Args: {args}" ) return ServerError().as_json
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 logger().info(f"Session {session.id}: Starting session.") session.save()
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 logger().info( f"{_describe_session(session)}: Making generation in progress to {session.generation_in_progress}." ) session.save()
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(): logger().info(f"{_describe_session(session)}: Deleting {row_id}.") 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 _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) logger().info(f"Session {session.id}: Changing row {row_id}.") preset_row.save() except peewee.DoesNotExist: raise InvalidAction(f"invalid row: {row_id}")
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 _collect_location(session: GameSession, membership: GameSessionMembership, description: LayoutDescription, pickup_location: int) -> Optional[int]: """ Collects the pickup in the given location. Returns :param session: :param membership: :param description: :param pickup_location: :return: The rewarded player if some player must be updated of the fact. """ player_row: int = membership.row pickup_target = _get_pickup_target(description, player_row, pickup_location) if pickup_target is None: logger().info(f"Session {session.id}, Row {membership.row} found item " f"at {pickup_location}. It's an ETM.") return None if pickup_target.player == membership.row: logger().info( f"Session {session.id}, Row {membership.row} found item " f"at {pickup_location}. It's a {pickup_target.pickup.name} for themselves." ) return None try: GameSessionTeamAction.create( session=session, provider_row=membership.row, provider_location_index=pickup_location, receiver_row=pickup_target.player, ) except peewee.IntegrityError: # Already exists and it's for another player, no inventory update needed logger().info( f"Session {session.id}, Row {membership.row} found item " f"at {pickup_location}. It's a {pickup_target.pickup.name} for {pickup_target.player}, " f"but it was already collected.") return None logger().info( f"Session {session.id}, Row {membership.row} found item " f"at {pickup_location}. It's a {pickup_target.pickup.name} for {pickup_target.player}." ) return pickup_target.player
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 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 log(msg): logger().info( f"{_describe_session(session, membership)} found item at {pickup_location}. {msg}" )
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 log(msg): logger().info( f"Session {session.id}, Row {membership.row} found item at {pickup_location}. {msg}" )
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)
def _change_password(sio: ServerApp, session: GameSession, password: str): _verify_has_admin(sio, session.id, None) session.password = _hash_password(password) logger().info(f"Session {session.id}: Changing password.") session.save()