syntaxwebsite/app/routes/gamesapi.py

504 lines
19 KiB
Python

# 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/<int:placeId>/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/<int:placeId>/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/<int:universeId>/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/<int:universeId>/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/<int:universeId>/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/<int:universeId>/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/<int:universeId>", 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