Files
copyrighttxt 05211f73ef add web
2026-07-06 13:28:00 -04:00

670 lines
27 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
import base64
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, Is3D=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
Is3D: Whether to request a 3D thumbnail
"""
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
Image3DThumbnailCache = 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, full_3dcontenthash=None, updated_at=datetime.utcnow())
db.session.add(UserThumbnailObject)
ImageThumbnailCache = redis_controller.get(f"Thumbnailer:UserImage:{AvatarHash}:Thumbnail")
if ImageThumbnailCache is not None:
UserThumbnailObject.full_contenthash = ImageThumbnailCache
ImageHeadshotCache = redis_controller.get(f"Thumbnailer:UserImage:{AvatarHash}:Headshot")
if ImageHeadshotCache is not None:
UserThumbnailObject.headshot_contenthash = ImageHeadshotCache
Image3DThumbnailCache = redis_controller.get(f"Thumbnailer:UserImage:{AvatarHash}:3DThumbnail")
if Image3DThumbnailCache is not None and Is3D:
UserThumbnailObject.full_3dcontenthash = Image3DThumbnailCache
db.session.commit()
if ImageThumbnailCache is not None and ImageHeadshotCache is not None:
if not Is3D or (Is3D and Image3DThumbnailCache 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"
requests_to_send = 0
UserAvatarObj: UserAvatar = UserAvatar.query.filter_by(user_id=UserId).first()
if (not bypassCache and ImageThumbnailCache is None) or bypassCache:
ThumbnailReqId = str(uuid.uuid4())
redis_controller.set(f"Thumbnailer:Request:{ThumbnailReqId}", json.dumps({
"UserId": UserId,
"Type": 0,
"Is3D": False
}), ex=600)
try:
perform_post(
TargetGameserver=GameServerObject,
Endpoint="Thumbnail",
JSONData={
"type": 16 if UserAvatarObj.r15 else 0,
"userid": UserId,
"reqid": ThumbnailReqId,
"image_x": 768,
"image_y": 768,
"Is3D": False
},
RequestTimeout=0.5
)
requests_to_send += 1
except Exception as e:
logging.error(f"Error requesting 2D thumbnail for UserId {UserId}: {str(e)}")
if (not bypassCache and ImageHeadshotCache is None) or bypassCache:
HeadshotReqId = str(uuid.uuid4())
redis_controller.set(f"Thumbnailer:Request:{HeadshotReqId}", json.dumps({
"UserId": UserId,
"Type": 1,
"Is3D": False
}), ex=600)
try:
perform_post(
TargetGameserver=GameServerObject,
Endpoint="Thumbnail",
JSONData={
"type": 1,
"userid": UserId,
"reqid": HeadshotReqId,
"image_x": 768,
"image_y": 768,
"Is3D": False
},
RequestTimeout=0.5
)
requests_to_send += 1
except Exception as e:
logging.error(f"Error requesting headshot for UserId {UserId}: {str(e)}")
if Is3D and ((not bypassCache and Image3DThumbnailCache is None) or bypassCache):
ThreeDReqId = str(uuid.uuid4())
redis_controller.set(f"Thumbnailer:Request:{ThreeDReqId}", json.dumps({
"UserId": UserId,
"Type": 0,
"Is3D": True
}), ex=600)
try:
perform_post(
TargetGameserver=GameServerObject,
Endpoint="Thumbnail",
JSONData={
"type": 16 if UserAvatarObj.r15 else 0,
"userid": UserId,
"reqid": ThreeDReqId,
"image_x": 768,
"image_y": 768,
"Is3D": True
},
RequestTimeout=0.5
)
requests_to_send += 1
except Exception as e:
logging.error(f"Error requesting 3D thumbnail for UserId {UserId}: {str(e)}")
if requests_to_send > 0:
GameServerObject.thumbnailQueueSize += requests_to_send
db.session.commit()
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.vortexi.cc/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
import base64
def Handle3DObjectData(EncodedData: bytes) -> str:
"""
Process the 3D object data from the thumbnailer and return a hash
"""
try:
Data = json.loads(EncodedData)
TextureHashes = []
OBJBytes = None
MTLBytes = None
OBJHash = None
MTLHash = None
for FileName, FileDict in Data["files"].items():
DecodedData: bytes = base64.b64decode(FileDict["content"])
if FileName.endswith(".obj"):
OBJBytes = DecodedData
OBJHash = f"{hashlib.sha512(DecodedData).hexdigest()}"
s3helper.UploadBytesToS3(DecodedData, OBJHash, contentType="text/plain")
elif FileName.endswith(".mtl"):
MTLBytes = DecodedData
MTLHash = f"{hashlib.sha512(DecodedData).hexdigest()}"
else:
TextureHash = f"{hashlib.sha512(DecodedData).hexdigest()}"
TextureHashes.append(TextureHash)
s3helper.UploadBytesToS3(DecodedData, TextureHash, contentType="image/png")
if MTLBytes:
MTLBytes = MTLBytes.replace(FileName.encode(), f"{TextureHash}".encode())
if MTLBytes:
s3helper.UploadBytesToS3(MTLBytes, MTLHash, contentType="text/plain")
OutputData = {
"camera": Data["camera"],
"aabb": Data["AABB"],
"obj": OBJHash,
"mtl": MTLHash,
"textures": TextureHashes
}
JsonContent = json.dumps(OutputData).encode()
# bruh
ContentHash = f"{hashlib.sha512(JsonContent).hexdigest()}"
s3helper.UploadBytesToS3(JsonContent, ContentHash, contentType="application/json")
return ContentHash
except Exception as e:
logging.error(f"Error in Handle3DObjectData: {str(e)}")
return None
@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"]
ThumbnailIs3D = RequestData.get("Is3D", False)
ImageData = request.data
ImageHash = None
AvatarHash = GetAvatarHash(UserId)
if ThumbnailIs3D:
try:
ContentHash = Handle3DObjectData(ImageData)
if ContentHash is None:
return jsonify({"status": "error", "message": "Failed to process 3D data"}), 500
redis_controller.setex(f"Thumbnailer:UserImage:{AvatarHash}:3DThumbnail", 60 * 60 * 24 * 3, ContentHash)
ImageHash = ContentHash
except Exception as e:
logging.error(f"Error processing 3D avatar data: {str(e)}")
return jsonify({"status": "error", "message": f"Error processing 3D data: {str(e)}"}), 500
else:
ImageHash = hashlib.sha512(ImageData).hexdigest()
s3helper.UploadBytesToS3(ImageData, ImageHash, contentType="image/png")
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:
if ThumbnailIs3D:
UserThumbnailObject = UserThumbnail(
userid=UserId,
full_contenthash=None,
headshot_contenthash=None,
full_3dcontenthash=ImageHash,
updated_at=datetime.utcnow()
)
else:
UserThumbnailObject = UserThumbnail(
userid=UserId,
full_contenthash=ImageHash,
headshot_contenthash=None,
full_3dcontenthash=None,
updated_at=datetime.utcnow()
)
else:
UserThumbnailObject = UserThumbnail(
userid=UserId,
headshot_contenthash=ImageHash,
full_contenthash=None,
full_3dcontenthash=None,
updated_at=datetime.utcnow()
)
db.session.add(UserThumbnailObject)
else:
if ThumbnailType == 0:
if ThumbnailIs3D:
UserThumbnailObject.full_3dcontenthash = ImageHash
else:
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