syntaxwebsite/app/routes/thumbnailer.py

518 lines
22 KiB
Python

from flask import Blueprint, render_template, request, redirect, url_for, flash, make_response, jsonify
from app.extensions import db, redis_controller, csrf
import uuid
import json
import requests
import hashlib
import logging
import os
from datetime import datetime
from app.services.gameserver_comm import perform_post
from app.util import websiteFeatures, s3helper
from app.models.gameservers import GameServer
from app.models.asset_version import AssetVersion
from app.models.asset_thumbnail import AssetThumbnail
from app.models.user_thumbnail import UserThumbnail
from app.models.place_icon import PlaceIcon
from app.models.asset import Asset
from app.models.package_asset import PackageAsset
from app.models.place import Place
from app.models.user_avatar import UserAvatar
from app.models.user_avatar_asset import UserAvatarAsset
from app.enums.PlaceYear import PlaceYear
Thumbnailer = Blueprint('thumbnailer', __name__, url_prefix='/internal')
AssetTypetoThumbnailRenderType = {
4 : 4,
8 : 11,
9 : 5,
10 : 3,
11 : 15,
12 : 14,
1 : 6,
13 : 6,
18 : 6,
19 : 12,
2 : 7,
17 : 8,
27 : 9,
28 : 9,
29 : 9,
30 : 9,
31 : 9,
32 : 17,
41 : 11,
42 : 11,
43 : 11,
44 : 11,
45 : 11,
46 : 11,
47 : 11,
40 : 13,
}
"""
omg was i fucking high or something several months ago i gotta rewrite this one day
- something.else 19/2/2024
"""
def GetAvatarHash( userid : int ) -> str:
"""
Returns the avatar hash for the userid provided
"""
UserAvatarObject : UserAvatar = UserAvatar.query.filter_by(user_id=userid).first()
if UserAvatarObject is None:
return None
UserAvatarAssetObject : UserAvatarAsset = UserAvatarAsset.query.filter_by(user_id=userid).all()
HashString = ""
for UserAvatarAssetObject in UserAvatarAssetObject:
HashString += str(UserAvatarAssetObject.asset_id)+"-"
HashString += str(UserAvatarObject.head_color_id)+"-"
HashString += str(UserAvatarObject.torso_color_id)+"-"
HashString += str(UserAvatarObject.left_arm_color_id)+"-"
HashString += str(UserAvatarObject.right_arm_color_id)+"-"
HashString += str(UserAvatarObject.left_leg_color_id)+"-"
HashString += str(UserAvatarObject.right_leg_color_id)+'-'
HashString += str(UserAvatarObject.r15)+'-'
HashString += str(UserAvatarObject.height_scale)+'-'
HashString += str(UserAvatarObject.width_scale)+'-'
HashString += str(UserAvatarObject.head_scale)+'-'
HashString += str(UserAvatarObject.proportion_scale)+'-'
HashString += str(UserAvatarObject.body_type_scale)
AvatarHash = hashlib.sha256(HashString.encode()).hexdigest()
return AvatarHash
def findBestThumbnailer() -> GameServer | None:
weight_ping_time = 3
weight_queue_size = 0.3
AllGameServers : list[GameServer] = GameServer.query.filter_by(allowThumbnailGen=True, isRCCOnline=True).filter( GameServer.thumbnailQueueSize < 40 ).all()
if len(AllGameServers) == 0:
return None
BestGameServer : GameServer = None
for GameServerObject in AllGameServers:
GameServerObject.score = (weight_ping_time * GameServerObject.heartbeatResponseTime) + (weight_queue_size * GameServerObject.thumbnailQueueSize)
if BestGameServer is None:
BestGameServer = GameServerObject
continue
if GameServerObject.score < BestGameServer.score:
BestGameServer = GameServerObject
#logging.info(f"GameserverLoadBalancer: {str(BestGameServer.serverId)} - {str(BestGameServer.score)} - Ping: {str(round(BestGameServer.heartbeatResponseTime, 3))}secs - Queue: {str(BestGameServer.thumbnailQueueSize)}")
return BestGameServer
def TakeUserThumbnail(UserId : int, bypassCooldown=False, bypassCache=False):
"""
Takes a thumbnail and headshot for the userid provided
bypassCooldown: Bypasses the 5 second cooldown for that user
bypassCache: Bypasses the cache and takes a new thumbnail and headshot
"""
if redis_controller.get(f"Thumbnailer:UserId:{UserId}:Taken") is not None and not bypassCooldown:
return "Thumbnail Attempted Recently"
redis_controller.set(f"Thumbnailer:UserId:{UserId}:Taken", "True", 5)
ImageHeadshotCache = None
ImageThumbnailCache = None
AvatarHash = GetAvatarHash(UserId)
if not bypassCache:
UserThumbnailObject = UserThumbnail.query.filter_by(userid=UserId).first()
if UserThumbnailObject is None:
UserThumbnailObject = UserThumbnail(userid=UserId, full_contenthash=None, headshot_contenthash=None, updated_at=datetime.utcnow())
db.session.add(UserThumbnailObject)
ImageThumbnailCache = redis_controller.get(f"Thumbnailer:UserImage:{AvatarHash}:Thumbnail")
if ImageThumbnailCache is not None:
#logging.info(f"Thumbnail Cache found for UserId {UserId} - {AvatarHash}")
UserThumbnailObject.full_contenthash = ImageThumbnailCache
ImageHeadshotCache = redis_controller.get(f"Thumbnailer:UserImage:{AvatarHash}:Headshot")
if ImageHeadshotCache is not None:
#logging.info(f"Headshot Cache found for UserId {UserId} - {AvatarHash}")
UserThumbnailObject.headshot_contenthash = ImageHeadshotCache
db.session.commit()
if ImageThumbnailCache is not None and ImageHeadshotCache is not None:
return
if redis_controller.get(f"Thumbnailer:AvatarHash:{AvatarHash}:Lock") is not None:
return "Thumbnail Attempted Recently"
redis_controller.set(f"Thumbnailer:AvatarHash:{AvatarHash}:Lock", "True", 5)
if not websiteFeatures.GetWebsiteFeature("ThumbnailRendering"):
return "Thumbnail Rendering is disabled"
GameServerObject : GameServer = findBestThumbnailer()
if GameServerObject is None:
return "No suitable game servers found"
ThumbnailReqId = str(uuid.uuid4())
redis_controller.set(f"Thumbnailer:Request:{ThumbnailReqId}", json.dumps({
"UserId": UserId,
"Type": 0,
}), ex=600)
HeadshotReqId = str(uuid.uuid4())
redis_controller.set(f"Thumbnailer:Request:{HeadshotReqId}", json.dumps({
"UserId": UserId,
"Type": 1,
}), ex=600)
UserAvatarObj : UserAvatar = UserAvatar.query.filter_by(user_id=UserId).first()
try:
if (not bypassCache and ImageHeadshotCache is None) or bypassCache:
perform_post(
TargetGameserver = GameServerObject,
Endpoint = "Thumbnail",
JSONData = {
"type": 1,
"userid": UserId,
"reqid": HeadshotReqId,
"image_x": 768,
"image_y": 768
},
RequestTimeout = 0.5
)
if (not bypassCache and ImageThumbnailCache is None) or bypassCache:
perform_post(
TargetGameserver = GameServerObject,
Endpoint = "Thumbnail",
JSONData = {
"type": 16 if UserAvatarObj.r15 else 0,
"userid": UserId,
"reqid": ThumbnailReqId,
"image_x": 768,
"image_y": 768
},
RequestTimeout = 0.5
)
GameServerObject.thumbnailQueueSize += 2
db.session.commit()
except Exception as e:
return str(e)
return "Thumbnail request sent"
def ValidatePlaceFileRequest(PlaceId : int, RequestId : str = None, AssetYear : PlaceYear = None ) -> None:
"""
Sends a validate file request to a thumbnailer server
"""
if not websiteFeatures.GetWebsiteFeature("AssetValidationService"):
redis_controller.set(f"ValidatePlaceFileRequest:{RequestId}", json.dumps({
"valid": False,
"error": "Asset Validation Service is temporarily disabled, try again later"
}), ex=600)
GameServerObject : GameServer = findBestThumbnailer()
if GameServerObject is None:
redis_controller.set(f"ValidatePlaceFileRequest:{RequestId}", json.dumps({
"valid": False,
"error": "Cannot verify file at this time, try again later"
}), ex=600)
return
if AssetYear is None:
PlaceObj : Place = Place.query.filter_by(placeid=PlaceId).first()
AssetYear = PlaceYear.Eighteen
if PlaceObj is not None:
AssetYear = PlaceObj.placeyear
try:
AssetYearToEndpoint = {
PlaceYear.Eighteen: "AssetValidation2018",
PlaceYear.Sixteen: "AssetValidation2016",
PlaceYear.Fourteen: "AssetValidation2016",
PlaceYear.Twenty: "AssetValidation2020",
PlaceYear.TwentyOne: "AssetValidation2021"
}
if AssetYear not in AssetYearToEndpoint:
redis_controller.set(f"ValidatePlaceFileRequest:{RequestId}", json.dumps({
"valid": False,
"error": "Invalid place year"
}), ex=600)
return
response = perform_post(
TargetGameserver = GameServerObject,
Endpoint = AssetYearToEndpoint[AssetYear],
JSONData = {
"assetid": PlaceId
},
RequestTimeout = 60
)
response.raise_for_status()
response = response.json()
if response["valid"]:
redis_controller.set(f"ValidatePlaceFileRequest:{RequestId}", json.dumps({
"valid": True,
"error": None
}), ex=600)
return
else:
redis_controller.set(f"ValidatePlaceFileRequest:{RequestId}", json.dumps({
"valid": False,
"error": response["reason"]
}), ex=600)
return
except Exception as e:
logging.error(f"Error while trying to validate place file: {str(e)}")
redis_controller.set(f"ValidatePlaceFileRequest:{RequestId}", json.dumps({
"valid": False,
"error": "An error occured while trying to validate the place file"
}), ex=600)
return
def TakeThumbnail(AssetId : int, isIcon = False, bypassCooldown = False, bypassCache = False): # isIcon only used for game icons
"""
Takes a thumbnail for the assetid provided
bypassCooldown: Bypasses the 2 minute cooldown for that asset
"""
from app.routes.asset import GenerateTempAuthToken
if redis_controller.get(f"Thumbnailer:AssetId:{AssetId}:{str(isIcon)}:Taken") is not None and not bypassCooldown:
return "Thumbnail Attempted Recently"
redis_controller.set(f"Thumbnailer:AssetId:{AssetId}:{str(isIcon)}:Taken", "True", 120)
AssetObject : Asset = Asset.query.filter_by(id=AssetId).first()
AssetVersionObject : AssetVersion = AssetVersion.query.filter_by(asset_id=AssetId).order_by(AssetVersion.version.desc()).first()
if AssetVersionObject is None and AssetObject.asset_type.value != 32:
return "Asset version not found"
if not bypassCache:
if not isIcon:
ImageThumbnailCache = redis_controller.get(f"Thumbnailer:AssetImage:{AssetVersionObject.content_hash}:Thumbnail")
if ImageThumbnailCache is not None:
ThumbnailObject : AssetThumbnail = AssetThumbnail.query.filter_by(asset_id=AssetId, asset_version_id=AssetVersionObject.version).first()
if ThumbnailObject is None:
AssetModeration = 1
if AssetObject.roblox_asset_id is not None:
AssetModeration = 0
ThumbnailObject = AssetThumbnail(
asset_id=AssetId,
asset_version_id=AssetVersionObject.version,
content_hash=ImageThumbnailCache,
moderation_status=AssetModeration,
created_at=datetime.utcnow()
)
db.session.add(ThumbnailObject)
try:
db.session.commit()
except:
return
return
if ThumbnailObject.content_hash == ImageThumbnailCache:
return
ThumbnailObject.content_hash = ImageThumbnailCache
ThumbnailObject.moderation_status = 1
try:
db.session.commit()
except:
return
return
else:
ImageIconCache = redis_controller.get(f"Thumbnailer:AssetImage:{AssetVersionObject.content_hash}:PlaceIcon")
if ImageIconCache is not None:
PlaceIconObject : PlaceIcon = PlaceIcon.query.filter_by(placeid=AssetId).first()
if PlaceIconObject is None:
PlaceIconObject = PlaceIcon(
placeid=AssetId,
contenthash=ImageIconCache,
updated_at=datetime.utcnow(),
moderation_status=1, # Pending
)
db.session.add(PlaceIconObject)
db.session.commit()
return
if PlaceIconObject.contenthash == ImageIconCache:
return
PlaceIconObject.contenthash = ImageIconCache
PlaceIconObject.moderation_status = 1
PlaceIconObject.updated_at = datetime.utcnow()
db.session.commit()
return
AssetObject : Asset = Asset.query.filter_by(id=AssetId).first()
if AssetObject is None:
return "Asset not found"
AssetType = AssetObject.asset_type.value
ThumbnailType = AssetTypetoThumbnailRenderType.get(AssetType)
if ThumbnailType is None:
if AssetType in[39, 3, 24, 5, 48,49,50,51,52,53,54,55,56]: # SolidModel, Animation, Lua and Audio
if AssetType == 39:
StaticImage = open("./app/files/NoRender.png", "rb").read()
elif AssetType == 3:
StaticImage = open("./app/files/AudioThumbnail.png", "rb").read()
elif AssetType in [24,48,49,50,51,52,53,54,55,56]:
StaticImage = open("./app/files/AnimationThumbnail.png", "rb").read()
elif AssetType == 5:
StaticImage = open("./app/files/LuaThumbnail.png", "rb").read()
ImageHash = hashlib.sha512(StaticImage).hexdigest()
if not s3helper.DoesKeyExist(ImageHash):
s3helper.UploadBytesToS3(StaticImage, ImageHash, contentType="image/png")
try:
NewThumbnailObject : AssetThumbnail = AssetThumbnail(
asset_id=AssetId,
asset_version_id=AssetVersionObject.version,
content_hash=ImageHash,
moderation_status=0,
created_at=datetime.utcnow()
)
db.session.add(NewThumbnailObject)
db.session.commit()
except:
pass
return "Used Static Image"
return "Thumbnail type not found"
if not websiteFeatures.GetWebsiteFeature("ThumbnailRendering"):
return "Thumbnail Rendering is disabled"
GameServerObject : GameServer = findBestThumbnailer()
if GameServerObject is None:
return "No suitable game servers found"
ThumbnailReqId = str(uuid.uuid4())
redis_controller.set(f"Thumbnailer:Request:{ThumbnailReqId}", json.dumps({
"AssetId": AssetId,
"AssetVersionId": AssetVersionObject.version,
"isIcon": isIcon,
"AssetType": AssetType,
}), ex=600)
TargetX = 1024
TargetY = 1024
PlaceAuthorisationToken = None
if AssetType == 9:
PlaceAuthorisationToken = GenerateTempAuthToken( AssetId, Expiration = 600, CreatorIP = None )
if AssetType == 9 and not isIcon:
TargetX = 1280
TargetY = 720
if AssetType == 1: # Image
TargetX = 256
TargetY = 256
if AssetType == 32: # Package
AllPackageAssets : list[PackageAsset] = PackageAsset.query.filter_by(package_asset_id=AssetId).all()
AssetId = ""
for i in range(len(AllPackageAssets)):
AssetId += f"https://www.syntax.eco/asset/?id={str(AllPackageAssets[i].asset_id)}"
if i != len(AllPackageAssets) - 1:
AssetId += ";"
try:
logging.info(f"thumbnailer : TakeThumbnail : Sent request to thumbnailer for asset {AssetId} with type {ThumbnailType} to {GameServerObject.serverName} [ {GameServerObject.serverId} ]")
perform_post(
TargetGameserver = GameServerObject,
Endpoint = "Thumbnail",
JSONData = {
"type": ThumbnailType,
"asset": AssetId,
"reqid": ThumbnailReqId,
"image_x": TargetX,
"image_y": TargetY,
"placetoken": PlaceAuthorisationToken
}
)
GameServerObject.thumbnailQueueSize += 1
db.session.commit()
except Exception as e:
return str(e)
return "Thumbnail request sent"
def isValidAuthorizationToken( authtoken : str) -> GameServer:
if authtoken is None:
return None
GameServerObject = GameServer.query.filter_by(accessKey=authtoken).first()
return GameServerObject
@Thumbnailer.route('/thumbnailreturn', methods=["POST"])
@csrf.exempt
def thumbnailreturn():
AuthorizationToken = request.headers.get("Authorization")
if AuthorizationToken is None:
return jsonify({"status": "error", "message": "Invalid authorization token"}),400
ThumbnailerOwner : GameServer = isValidAuthorizationToken(AuthorizationToken)
if ThumbnailerOwner is None:
return jsonify({"status": "error", "message": "Invalid authorization token"}),400
ReqId = request.headers.get("ReturnUUID")
if ReqId is None:
return jsonify({"status": "error", "message": "Invalid request id"}),400
RequestData = redis_controller.get(f"Thumbnailer:Request:{ReqId}")
if RequestData is None:
return jsonify({"status": "error", "message": "Invalid request id"}),400
redis_controller.delete(f"Thumbnailer:Request:{ReqId}")
RequestData = json.loads(RequestData)
if "UserId" not in RequestData:
AssetId = RequestData["AssetId"]
AssetVersionId = RequestData["AssetVersionId"]
isIcon = RequestData["isIcon"]
AssetType = RequestData["AssetType"]
ImageData = request.data
ImageHash = hashlib.sha512(ImageData).hexdigest()
s3helper.UploadBytesToS3(ImageData, ImageHash, contentType="image/png")
AssetVersionObject : AssetVersion = AssetVersion.query.filter_by(asset_id=AssetId, version=AssetVersionId).first()
if isIcon and AssetType == 9:
PlaceIconObject : PlaceIcon = PlaceIcon.query.filter_by(placeid=AssetId).first()
if PlaceIconObject is None:
PlaceIconObject = PlaceIcon(placeid=AssetId, contenthash=ImageHash, updated_at=datetime.utcnow())
db.session.add(PlaceIconObject)
else:
PlaceIconObject.contenthash = ImageHash
PlaceIconObject.updated_at = datetime.utcnow()
PlaceIconObject.moderation_status = 1
if AssetVersionObject:
redis_controller.setex(f"Thumbnailer:AssetImage:{AssetVersionObject.content_hash}:PlaceIcon", 60 * 60 * 24 * 3, ImageHash)
try:
ThumbnailerOwner.thumbnailQueueSize -= 1
db.session.commit()
except:
pass
return jsonify({"status": "success", "message": "Thumbnail saved"}),200
if AssetVersionObject:
redis_controller.setex(f"Thumbnailer:AssetImage:{AssetVersionObject.content_hash}:Thumbnail", 60 * 60 * 24 * 3, ImageHash)
ThumbnailObject : AssetThumbnail = AssetThumbnail.query.filter_by(asset_id=AssetId, asset_version_id=AssetVersionId).first()
if ThumbnailObject is not None:
ThumbnailObject.content_hash = ImageHash
ThumbnailObject.updated_at = datetime.utcnow()
ThumbnailerOwner.thumbnailQueueSize -= 1
db.session.commit()
return jsonify({"status": "success", "message": "Thumbnail saved"}),200
AssetObject : Asset = Asset.query.filter_by(id=AssetId).first()
AssetModeration = 1
if AssetObject.roblox_asset_id is not None:
AssetModeration = 0
AssetThumbnailObject = AssetThumbnail(asset_id=AssetId, asset_version_id=AssetVersionId, content_hash=ImageHash, created_at=datetime.utcnow(), moderation_status=AssetModeration) # 0 = Approved, 1 = Pending, 2 = Denied
db.session.add(AssetThumbnailObject)
db.session.commit()
return jsonify({"status": "success", "message": "Thumbnail saved"}),200
else:
UserId = RequestData["UserId"]
ThumbnailType = RequestData["Type"]
ImageData = request.data
ImageHash = hashlib.sha512(ImageData).hexdigest()
s3helper.UploadBytesToS3(ImageData, ImageHash, contentType="image/png")
AvatarHash = GetAvatarHash(UserId)
if ThumbnailType == 0:
redis_controller.setex(f"Thumbnailer:UserImage:{AvatarHash}:Thumbnail", 60 * 60 * 24 * 3, ImageHash)
elif ThumbnailType == 1:
redis_controller.setex(f"Thumbnailer:UserImage:{AvatarHash}:Headshot", 60 * 60 * 24 * 3, ImageHash)
UserThumbnailObject = UserThumbnail.query.filter_by(userid=UserId).first()
if UserThumbnailObject is None:
if ThumbnailType == 0:
UserThumbnailObject = UserThumbnail(userid=UserId, full_contenthash=ImageHash, headshot_contenthash=None, updated_at=datetime.utcnow())
else:
UserThumbnailObject = UserThumbnail(userid=UserId, headshot_contenthash=ImageHash, full_contenthash=None, updated_at=datetime.utcnow())
db.session.add(UserThumbnailObject)
ThumbnailerOwner.thumbnailQueueSize -= 1
db.session.commit()
return jsonify({"status": "success", "message": "Thumbnail saved"}),200
else:
if ThumbnailType == 0:
UserThumbnailObject.full_contenthash = ImageHash
else:
UserThumbnailObject.headshot_contenthash = ImageHash
UserThumbnailObject.updated_at = datetime.utcnow()
ThumbnailerOwner.thumbnailQueueSize -= 1
db.session.commit()
return jsonify({"status": "success", "message": "Thumbnail updated"}),200