from flask import Blueprint, render_template, request, redirect, url_for, flash, make_response, jsonify from app.models.gameservers import GameServer from app.models.place_ordered_datastore import PlaceOrderedDatastore from app.models.place_datastore import PlaceDatastore from app.models.asset import Asset from app.models.universe import Universe from app.models.place import Place from app.enums.AssetType import AssetType from app.extensions import get_remote_address, db, redis_controller, csrf from functools import wraps DataStoreRoute = Blueprint('datastore', __name__) def GameServerRequired(f): @wraps(f) def decorated_function(*args, **kwargs): RequestIP = get_remote_address() if RequestIP is None: return jsonify({"success": False, "message": "Unauthorized"}),401 server : GameServer = GameServer.query.filter_by(serverIP=RequestIP).first() if server is None: return jsonify({"success": False, "message": "Unauthorized"}),401 ServerAccessKey = request.headers.get("AccessKey", default = None, type = str) if ServerAccessKey is None or ServerAccessKey != server.accessKey: return jsonify({"success": False, "message": "Unauthorized"}),401 return f(*args, **kwargs) return decorated_function @DataStoreRoute.route("/persistence/getSortedValues", methods=["POST"]) @csrf.exempt @GameServerRequired def getSortedValues(): placeId : int = request.args.get("placeId", default=None, type=int) dataType : str = request.args.get("type", default=None, type=str) scope : str = request.args.get("scope", default="global", type=str) pageSize : int = request.args.get("pageSize", default=50, type=int) exclusiveStartKey : str = request.args.get("exclusiveStartKey", default=None, type=int) # I have no idea how Roblox uses this, but I am going to use it as a page number with pagination key : str = request.args.get("key", default=None, type=str) ascending : bool = request.args.get("ascending", default="False", type=str) == "True" inclusiveMinValue : int = request.args.get("inclusiveMinValue", default=None, type=int) exclusiveMaxValue : int = request.args.get("inclusiveMaxValue", default=None, type=int) PlaceObj : Place = Place.query.filter_by(placeid = placeId).first() if PlaceObj is None: return jsonify({"data":[], "message": "Place does not exist"}), 200 UniverseObj : Universe = Universe.query.filter_by(id = PlaceObj.parent_universe_id).first() if UniverseObj is None: return jsonify({"data":[], "message": "Place does not exist"}), 200 if pageSize == 0 or pageSize > 100: return jsonify({"data":[], "message": "Page size is too large"}), 200 if dataType != "sorted": return jsonify({"data":[], "message": "Invalid data type"}), 200 if exclusiveStartKey is None: exclusiveStartKey = 1 else: if exclusiveStartKey < 1: return jsonify({"data":[], "message": "Invalid exclusive start key"}), 200 DataStoreObj = PlaceOrderedDatastore.query.filter_by( universe_id = UniverseObj.id, scope = scope, key = key ) if inclusiveMinValue is not None: DataStoreObj = DataStoreObj.filter(PlaceOrderedDatastore.value >= inclusiveMinValue) if exclusiveMaxValue is not None: DataStoreObj = DataStoreObj.filter(PlaceOrderedDatastore.value < exclusiveMaxValue) DataStoreObj : list[PlaceOrderedDatastore] = DataStoreObj.order_by(PlaceOrderedDatastore.value.asc() if ascending else PlaceOrderedDatastore.value.desc()).paginate( page=exclusiveStartKey, per_page=pageSize, error_out=False ) if DataStoreObj is None: return jsonify({"Entries": [], "ExclusiveStartKey": None}), 200 AllEntries = [] for entry in DataStoreObj.items: AllEntries.append({ "Target": entry.name, "Value": entry.value }) return jsonify({"data":{ "Entries": AllEntries, "ExclusiveStartKey": str(DataStoreObj.next_num) if DataStoreObj.has_next else None }}) # TODO: Implement @DataStoreRoute.route("/persistence/set", methods=["POST"]) @csrf.exempt @GameServerRequired def setKey(): placeId : int = request.args.get("placeId", default=None, type=int) dataType : str = request.args.get("type", default=None, type=str) scope : str = request.args.get("scope", default="global", type=str) key : str = request.args.get("key", default=None, type=str) target : str = request.args.get("target", default=None, type=str) valueLength : int = request.args.get("valueLength", default=None, type=int) value : str = request.form.get("value", default=None, type=str) if valueLength is not None and not valueLength < 1024 * 1024 * 1: # 1MB return jsonify({"success": False, "message": "Value too large"}),400 PlaceObj : Place = Place.query.filter_by(placeid = placeId).first() if PlaceObj is None: return jsonify({"data":[], "message": "Place does not exist"}), 200 UniverseObj : Universe = Universe.query.filter_by(id = PlaceObj.parent_universe_id).first() if UniverseObj is None: return jsonify({"data":[], "message": "Place does not exist"}), 200 if dataType == "standard": DataStoreObj : PlaceDatastore = PlaceDatastore.query.filter_by( universe_id = UniverseObj.id, scope = scope, key = key, name = target ).order_by(PlaceDatastore.updated_at.desc()).first() if DataStoreObj is None: DataStoreObj : PlaceDatastore = PlaceDatastore( placeid = placeId, universe_id = UniverseObj.id, scope=scope, key=key, name=target, value=value ) db.session.add(DataStoreObj) else: DataStoreObj.value = value db.session.commit() elif dataType == "sorted": try: value : int = int(value) except: return jsonify({"success": False, "message": "Value is not an integer"}), 400 DataStoreObj : PlaceOrderedDatastore = PlaceOrderedDatastore.query.filter_by( universe_id = UniverseObj.id, scope=scope, key=key, name=target ).order_by(PlaceOrderedDatastore.updated_at.desc()).first() if DataStoreObj is None: DataStoreObj : PlaceOrderedDatastore = PlaceOrderedDatastore( placeid = placeId, universe_id = UniverseObj.id, scope=scope, key=key, name=target, value=value ) db.session.add(DataStoreObj) else: DataStoreObj.value = value db.session.commit() return jsonify({"data":value}) # TODO: Implement @DataStoreRoute.route("/persistence/getV2", methods=["POST"]) @DataStoreRoute.route("/persistence/getv2", methods=["POST"]) @csrf.exempt @GameServerRequired def getv2(): placeId : int = request.args.get("placeId", default=None, type=int) dataType : str = request.args.get("type", default=None, type=str) scope : str = request.args.get("scope", default="global", type=str) PlaceObj : Place = Place.query.filter_by(placeid = placeId).first() if PlaceObj is None: return jsonify({"data":[], "message": "Place does not exist"}), 200 UniverseObj : Universe = Universe.query.filter_by(id = PlaceObj.parent_universe_id).first() if UniverseObj is None: return jsonify({"data":[], "message": "Place does not exist"}), 200 dataBeingRequested = [] StartingCount = 0 while True: ReqScope : str = request.form.get(f"qkeys[{str(StartingCount)}].scope", default=None, type=str) if ReqScope is None: break Target : str = request.form.get(f"qkeys[{str(StartingCount)}].target", default=None, type=str) DataStoreName : str = request.form.get(f"qkeys[{str(StartingCount)}].key", default=None, type=str) if DataStoreName is None or Target is None or ReqScope is None: break dataBeingRequested.append({"Scope": ReqScope, "Target": Target, "Key": DataStoreName}) StartingCount += 1 if len(dataBeingRequested) == 0: return jsonify({"data":[], "message": "No data being requested"}), 200 ReturnData = [] if dataType == 'standard': for targetReq in dataBeingRequested: DataStoreObj : PlaceDatastore = PlaceDatastore.query.filter_by(universe_id = UniverseObj.id, scope=targetReq["Scope"], key=targetReq["Key"], name=targetReq["Target"]).order_by(PlaceDatastore.updated_at.desc()).first() if DataStoreObj is not None: ReturnData.append({ "Value": DataStoreObj.value, "Scope": DataStoreObj.scope, "Key": DataStoreObj.key, "Target": DataStoreObj.name }) elif dataType == 'sorted': for targetReq in dataBeingRequested: DataStoreObj : PlaceOrderedDatastore = PlaceOrderedDatastore.query.filter_by(universe_id = UniverseObj.id, scope=targetReq["Scope"], key=targetReq["Key"], name=targetReq["Target"]).order_by(PlaceOrderedDatastore.updated_at.desc()).first() if DataStoreObj is not None: ReturnData.append({ "Value": str(DataStoreObj.value), "Scope": DataStoreObj.scope, "Key": DataStoreObj.key, "Target": DataStoreObj.name }) return jsonify({"data":ReturnData}) @DataStoreRoute.route("/persistence/increment", methods=["POST"]) @csrf.exempt @GameServerRequired def increment(): placeId : int = request.args.get("placeId", default=None, type=int) key : str = request.args.get("key", default=None, type=str) dataType : str = request.args.get("type", default=None, type=str) scope : str = request.args.get("scope", default="global", type=str) target : str = request.args.get("target", default=None, type=str) value : int = request.args.get("value", default=None, type=int) PlaceObj : Place = Place.query.filter_by(placeid = placeId).first() if PlaceObj is None: return jsonify({"data":[], "message": "Place does not exist"}), 200 UniverseObj : Universe = Universe.query.filter_by(id = PlaceObj.parent_universe_id).first() if UniverseObj is None: return jsonify({"data":[], "message": "Place does not exist"}), 200 if dataType == "standard": DataStoreObj : PlaceDatastore = PlaceDatastore.query.filter_by( universe_id=UniverseObj.id, scope=scope, key=key, name=target ).order_by(PlaceDatastore.updated_at.desc()).first() if DataStoreObj is None: DataStoreObj : PlaceDatastore = PlaceDatastore( placeid = placeId, universe_id=UniverseObj.id, scope=scope, key=key, name=target, value=str(value) ) db.session.add(DataStoreObj) else: try: DataStoreObj.value = str(int(DataStoreObj.value) + value) except: return jsonify({"data":[], "message": "Value is not an integer"}), 200 db.session.commit() elif dataType == "sorted": DataStoreObj : PlaceOrderedDatastore = PlaceOrderedDatastore.query.filter_by( universe_id=UniverseObj.id, scope=scope, key=key, name=target ).order_by(PlaceOrderedDatastore.updated_at.desc()).first() if DataStoreObj is None: DataStoreObj : PlaceOrderedDatastore = PlaceOrderedDatastore( placeid = placeId, universe_id=UniverseObj.id, scope=scope, key=key, name=target, value=value ) db.session.add(DataStoreObj) else: DataStoreObj.value += value # PlaceOrderedDatastore values should always be integers return jsonify({"data":value})