# games.roblox.com import math from flask import Blueprint, render_template, request, redirect, url_for, flash, make_response, jsonify, abort, after_this_request from app.extensions import redis_controller, get_remote_address, csrf, limiter, db from app.util import auth, websiteFeatures, placeinfo from app.models.place import Place from app.models.asset import Asset from app.models.placeservers import PlaceServer from app.models.user import User from app.models.groups import Group from app.models.asset_thumbnail import AssetThumbnail from app.models.universe import Universe from app.enums.PlaceYear import PlaceYear from app.routes.rate import GetAssetLikesAndDislikes, GetAssetFavoriteCount, GetUserFavoriteStatus from flask_wtf.csrf import CSRFError, generate_csrf from sqlalchemy import func, and_ from flask_sqlalchemy import pagination GamesAPIRoute = Blueprint('gamesapi', __name__, url_prefix='/') csrf.exempt(GamesAPIRoute) @GamesAPIRoute.errorhandler(CSRFError) def handle_csrf_error(e): ErrorResponse = make_response(jsonify({ "errors": [ { "code": 0, "message": "Token Validation Failed" } ] })) ErrorResponse.status_code = 403 ErrorResponse.headers["x-csrf-token"] = generate_csrf() return ErrorResponse @GamesAPIRoute.errorhandler(429) def handle_ratelimit_reached(e): return jsonify({ "errors": [ { "code": 9, "message": "The flood limit has been exceeded." } ] }), 429 @GamesAPIRoute.before_request def before_request(): if "Roblox/" not in request.user_agent.string: csrf.protect() @GamesAPIRoute.route('/v1/games/sorts', methods=["GET"]) @auth.authenticated_required_api def games_sorts(): return jsonify({ "sorts" : [ { "token": "MostPopular", "name": "most_popular", "displayName": "Popular", "gameSetTypeId": 1, "gameSetTargetId": 90, "timeOptionsAvailable": False, "genreOptionsAvailable": False, "numberOfRows": 2, "numberOfGames": 0, "isDefaultSort": True, "contextUniverseId": None, "contextCountryRegionId": 1, "tokenExpiryInSeconds": 3600 }, { "token": "Featured", "name": "featured", "displayName": "Featured", "gameSetTypeId": 2, "gameSetTargetId": 91, "timeOptionsAvailable": False, "genreOptionsAvailable": False, "numberOfRows": 1, "numberOfGames": 0, "isDefaultSort": True, "contextUniverseId": None, "contextCountryRegionId": 1, "tokenExpiryInSeconds": 3600 }, { "token": "RecentlyUpdated", "name": "recently_updated", "displayName": "Recently Updated", "gameSetTypeId": 3, "gameSetTargetId": 93, "timeOptionsAvailable": False, "genreOptionsAvailable": False, "numberOfRows": 1, "numberOfGames": 0, "isDefaultSort": True, "contextUniverseId": None, "contextCountryRegionId": 1, "tokenExpiryInSeconds": 3600 }, ], "timeFilters": [ { "token": "Now", "name": "Now", "tokenExpiryInSeconds": 3600 }, { "token": "PastDay", "name": "PastDay", "tokenExpiryInSeconds": 3600 }, { "token": "PastWeek", "name": "PastWeek", "tokenExpiryInSeconds": 3600 }, { "token": "PastMonth", "name": "PastMonth", "tokenExpiryInSeconds": 3600 }, { "token": "AllTime", "name": "AllTime", "tokenExpiryInSeconds": 3600 } ], "genreFilters": [ { "token": "T638364961735517991_1_89de", "name": "All", "tokenExpiryInSeconds": 3600 }, { "token": "T638364961735518009_19_3d2", "name": "Building", "tokenExpiryInSeconds": 3600 }, { "token": "T638364961735518045_11_3de6", "name": "Horror", "tokenExpiryInSeconds": 3600 }, { "token": "T638364961735518062_7_558c", "name": "Town and City", "tokenExpiryInSeconds": 3600 }, { "token": "T638364961735518076_17_c371", "name": "Military", "tokenExpiryInSeconds": 3600 }, { "token": "T638364961735518094_15_2056", "name": "Comedy", "tokenExpiryInSeconds": 3600 }, { "token": "T638364961735518107_8_6d4f", "name": "Medieval", "tokenExpiryInSeconds": 3600 }, { "token": "T638364961735518120_13_c168", "name": "Adventure", "tokenExpiryInSeconds": 3600 }, { "token": "T638364961735518134_9_e6aa", "name": "Sci-Fi", "tokenExpiryInSeconds": 3600 }, { "token": "T638364961735518156_12_13fb", "name": "Naval", "tokenExpiryInSeconds": 3600 }, { "token": "T638364961735518169_20_46a", "name": "FPS", "tokenExpiryInSeconds": 3600 }, { "token": "T638364961735518183_21_4bbf", "name": "RPG", "tokenExpiryInSeconds": 3600 }, { "token": "T638364961735518192_14_efc6", "name": "Sports", "tokenExpiryInSeconds": 3600 }, { "token": "T638364961735518205_10_fa83", "name": "Fighting", "tokenExpiryInSeconds": 3600 }, { "token": "T638364961735518223_16_5d38", "name": "Western", "tokenExpiryInSeconds": 3600 } ], "gameFilters": [ { "token": "T638364961735518263_Any_56d2", "name": "Any", "tokenExpiryInSeconds": 3600 }, { "token": "T638364961735518277_Classic_a1f4", "name": "Classic", "tokenExpiryInSeconds": 3600 } ], "pageContext": { "pageId": "f5b1510e-3810-42ab-8135-8ffa5ef221ba", "isSeeAllPage": None }, "gameSortStyle": None }) @GamesAPIRoute.route("/v1/games/list", methods=["GET"]) @limiter.limit("60/minute") @auth.authenticated_required_api def games_list(): sortToken = request.args.get("sortToken", default = "MostPopular", type = str) startRows = request.args.get("startRows", default = 0, type = int) maxRows = request.args.get("maxRows", default = 40, type = int) if sortToken not in ["MostPopular", "Featured", "RecentlyUpdated"]: return jsonify( { "errors": [ { "code": 0, "message": "Invalid sort token." } ] } ), 400 if startRows < 0: return jsonify( { "errors": [ { "code": 0, "message": "Invalid start rows." } ] } ), 400 if maxRows > 40 or maxRows < 0: return jsonify( { "errors": [ { "code": 0, "message": "Max rows must be between 0-40" } ] } ), 400 pageNumber = math.floor(startRows / maxRows) + 1 if sortToken == "MostPopular": PopularGames = Universe.query.filter( and_( Universe.is_public == True, Universe.place_year == PlaceYear.Twenty ) ).outerjoin( Place, Place.parent_universe_id == Universe.id ).outerjoin( PlaceServer, PlaceServer.serverPlaceId == Place.placeid ).group_by( Universe.id ) PopularGames = PopularGames.order_by( func.coalesce( func.sum( PlaceServer.playerCount ), 0 ).desc() ).order_by( func.coalesce( func.sum( Place.visitcount ), 0 ).desc() ) UniverseObjsList = PopularGames.paginate( page = pageNumber, per_page = maxRows, error_out = False ) elif sortToken == "Featured": FeaturedGames = Universe.query.filter( and_( Universe.is_public == True, Universe.place_year == PlaceYear.Twenty, Universe.is_featured == True ) ).outerjoin( Place, Place.parent_universe_id == Universe.id ).outerjoin( PlaceServer, PlaceServer.serverPlaceId == Place.placeid ).group_by( Universe.id ) FeaturedGames = FeaturedGames.order_by( func.coalesce( func.sum( PlaceServer.playerCount ), 0 ).desc() ).order_by( func.coalesce( func.sum( Place.visitcount ), 0 ).desc() ) UniverseObjsList = FeaturedGames.paginate( page = pageNumber, per_page = maxRows, error_out = False ) else: # Fallback to RecentlyUpdated #sortToken == "RecentlyUpdated": RecentlyUpdatedGames = Universe.query.filter( and_( Universe.is_public == True, Universe.place_year == PlaceYear.Twenty )).order_by( Universe.updated_at.desc() ) UniverseObjsList = RecentlyUpdatedGames.paginate( page = pageNumber, per_page = maxRows, error_out = False ) UniverseObjsList : pagination.Pagination = UniverseObjsList GameList = [] for UniverseObj in UniverseObjsList.items: UniverseObj : Universe PlaceObj : Place = Place.query.filter_by( placeid = UniverseObj.root_place_id ).first() Upvotes, Downvotes = GetAssetLikesAndDislikes(PlaceObj.placeid) PlaceAssetObj : Asset = PlaceObj.assetObj CreatorObj : User | Group = User.query.filter_by(id=UniverseObj.creator_id).first() if UniverseObj.creator_type == 0 else Group.query.filter_by(id=UniverseObj.creator_id).first() GameList.append({ "creatorId": UniverseObj.creator_id, "creatorName": CreatorObj.username if UniverseObj.creator_type == 0 else CreatorObj.name, "creatorType": "User" if UniverseObj.creator_type == 0 else "Group", "creatorHasVerifiedBadge": False, "totalUpVotes": Upvotes, "totalDownVotes": Downvotes, "universeId": UniverseObj.id, "name": PlaceAssetObj.name, "placeId": PlaceObj.placeid, "playerCount": placeinfo.GetUniversePlayingCount(UniverseObj), "imageToken": "", "isSponsored": False, "nativeAdData": "", "isShowSponsoredLabel": False, "price": 0, "analyticsIdentifier": "", "gameDescription": PlaceAssetObj.description, "genre": "All", "minimumAge": 0 }) return jsonify({ "games": GameList, "suggestedKeyword": "", "correctedKeyword": "", "filteredKeyword": "", "hasMoreRows": UniverseObjsList.has_next, "nextPageExclusiveStartId": 0, "featuredSearchUniverseId": 0, "emphasis": False, "cutOffIndex": 0, "algorithm": "", "algorithmQueryType": "", "suggestionAlgorithm": "", "relatedGames": [] }) @GamesAPIRoute.route("/v1/games//votes", methods=["GET"]) @limiter.limit("60/minute") @auth.authenticated_required_api def get_place_votes( placeId : int ): PlaceObj : Place = Place.query.filter_by(placeid=placeId).first() if not PlaceObj: return jsonify( { "errors": [ { "code": 0, "message": "Invalid placeId." } ] } ), 400 Upvotes, Downvotes = GetAssetLikesAndDislikes(PlaceObj.placeid) return jsonify({ "id": PlaceObj.placeid, "upVotes": Upvotes, "downVotes": Downvotes }) @GamesAPIRoute.route("/v1/games//votes/user", methods=["GET"]) @limiter.limit("60/minute") @auth.authenticated_required_api def get_place_user_votes( placeId : int ): PlaceObj : Place = Place.query.filter_by(placeid=placeId).first() if not PlaceObj: return jsonify( { "errors": [ { "code": 0, "message": "Invalid placeId." } ] } ), 400 return jsonify({ "canVote": False, "userVote": False, "reasonForNotVoteable": "Voting is disabled" }) @GamesAPIRoute.route("/v1/games/multiget-playability-status", methods=["GET"]) @limiter.limit("60/minute") @auth.authenticated_required_api def get_playability_status(): universeIdsList = request.args.get("universeIds", default = "", type = str) if universeIdsList == "": return jsonify( { "errors": [ { "code": 8, "message": "No universe IDs were specified." } ] } ), 400 try: universeIdsList = universeIdsList.split(",") universeIdsList = [int(x) for x in universeIdsList] universeIdsList = list(set(universeIdsList)) except: return jsonify( { "errors": [ { "code": 8, "message": "Invalid universe IDs were specified." } ] } ), 400 if len(universeIdsList) > 100: return jsonify( { "errors": [ { "code": 9, "message": "Too many universe IDs were specified." } ] } ), 400 RequestedData = [] for universeId in universeIdsList: RequestedData.append({ "playabilityStatus": 0, "isPlayable": True, "universeId": universeId }) return jsonify(RequestedData), 200 @GamesAPIRoute.route("/v2/games//media", methods=["GET"]) @limiter.limit("60/minute") @auth.authenticated_required_api def get_game_media( universeId : int ): UniverseObj : Universe = Universe.query.filter_by(id = universeId).first() if not UniverseObj: return jsonify( { "errors": [ { "code": 2, "message": "The requested universe does not exist." } ] } ), 404 return jsonify({"data": []}), 200 @GamesAPIRoute.route("/v1/games", methods=["GET"]) @limiter.limit("60/minute") @auth.authenticated_required_api def multi_get_game_info(): universeIdsList = request.args.get("universeIds", default = "", type = str) if universeIdsList == "": return jsonify( { "errors": [ { "code": 8, "message": "No universe IDs were specified." } ] } ), 400 try: universeIdsList = universeIdsList.split(",") universeIdsList = [int(x) for x in universeIdsList] universeIdsList = list(set(universeIdsList)) except: return jsonify( { "errors": [ { "code": 8, "message": "Invalid universe IDs were specified." } ] } ), 400 if len(universeIdsList) > 100: return jsonify( { "errors": [ { "code": 9, "message": "Too many universe IDs were specified." } ] } ), 400 RequestedData = [] for universeId in universeIdsList: UniverseObj : Universe = Universe.query.filter_by(id=universeId).first() if UniverseObj is None: continue PlaceObj : Place = Place.query.filter_by( placeid = UniverseObj.root_place_id ).first() if PlaceObj is None: continue PlaceAssetObj : Asset = PlaceObj.assetObj CreatorObj : User | Group = User.query.filter_by(id=UniverseObj.creator_id).first() if UniverseObj.creator_type == 0 else Group.query.filter_by(id=UniverseObj.creator_id).first() RequestedData.append({ "id": UniverseObj.id, "rootPlaceId": UniverseObj.root_place_id, "name": PlaceAssetObj.name, "description": PlaceAssetObj.description, "sourceName": PlaceAssetObj.name, "sourceDescription": PlaceAssetObj.description, "creator": { "id": CreatorObj.id, "type": "User" if UniverseObj.creator_type == 0 else "Group", "name": CreatorObj.username if UniverseObj.creator_type == 0 else CreatorObj.name, "hasVerifiedBadge": False, "isRNVAccount": False }, "price": 0, "allowedGearGenres": [], "allowedGearCategories": [], "isGenreEnforced": True, "copyingAllowed": False, "playing": placeinfo.GetUniversePlayingCount(UniverseObj), "visits": PlaceObj.visitcount, "maxPlayers": PlaceObj.maxplayers, "created": UniverseObj.created_at.strftime("%Y-%m-%dT%H:%M:%S.000Z"), "updated": UniverseObj.updated_at.strftime("%Y-%m-%dT%H:%M:%S.000Z"), "studioAccessToApisAllowed": False, "createVipServersAllowed": False, "universeAvatarType": 1, "genre": "All", "isAllGenre": True, "isFavoritedByUser": False, "favoritedCount": GetAssetFavoriteCount(PlaceObj.placeid) }) return jsonify({ "data": RequestedData }), 200 @GamesAPIRoute.route("/v1/games//favorites", methods=["GET"]) @limiter.limit("60/minute") @auth.authenticated_required_api def get_game_favorites( universeId : int ): UniverseObj : Universe = Universe.query.filter_by(id = universeId).first() if not UniverseObj: return jsonify( { "errors": [ { "code": 2, "message": "The requested universe does not exist." } ] } ), 404 AuthenticatedUser : User = auth.GetCurrentUser() return jsonify({ "isFavorited": GetUserFavoriteStatus( universeId, AuthenticatedUser.id) }), 200 @GamesAPIRoute.route("/v1/games//social-links/list", methods=["GET"]) @limiter.limit("60/minute") @auth.authenticated_required_api def get_universe_social_links_list( universeId : int ): UniverseObj : Universe = Universe.query.filter_by(id = universeId).first() if not UniverseObj: return jsonify( { "errors": [ { "code": 2, "message": "The requested universe does not exist." } ] } ), 404 return jsonify({ "data": [] }), 200 @GamesAPIRoute.route("/v1/games//game-passes", methods=["GET"]) @limiter.limit("60/minute") @auth.authenticated_required_api def get_universe_game_passes( universeId : int ): UniverseObj : Universe = Universe.query.filter_by(id = universeId).first() if not UniverseObj: return jsonify( { "errors": [ { "code": 2, "message": "The requested universe does not exist." } ] } ), 404 return jsonify({ "previousPageCursor": None, "nextPageCursor": None, "data": [] }), 200 @GamesAPIRoute.route("/v1/games/recommendations/game/", methods=["GET"]) @limiter.limit("60/minute") @auth.authenticated_required_api def get_recommended_games_from_universeid( universeId : int ): UniverseObj : Universe = Universe.query.filter_by(id = universeId).first() if not UniverseObj: return jsonify( { "errors": [ { "code": 2, "message": "The requested universe does not exist." } ] } ), 404 return jsonify({ "games": [], "nextPaginationKey": None }), 200