syntaxwebsite/app/pages/develop/develop.py

2003 lines
105 KiB
Python

from flask import Blueprint, render_template, request, redirect, url_for, flash, abort, after_this_request, Response
from app.util import auth, redislock, websiteFeatures, s3helper, transactions
from app.util.assetvalidation import ValidateClothingImage, ValidatePlaceFile, ValidateMP3File, ValidateMP3AndConvertToOGG
from app.routes.thumbnailer import TakeThumbnail
from app.models.userassets import UserAsset
from app.util.assetversion import CreateNewAssetVersion, GetLatestAssetVersion
from app.util.textfilter import FilterText
from app.util.membership import GetUserMembership
from app.util.placeinfo import ClearUniversePlayingCountCache, ClearPlayingCountCache
from app.enums.MembershipType import MembershipType
from app.enums.ChatStyle import ChatStyle
from app.models.place import Place
from app.models.asset_version import AssetVersion
from app.models.asset import Asset
from app.models.user import User
from app.models.placeservers import PlaceServer
from app.models.placeserver_players import PlaceServerPlayer
from app.models.gameservers import GameServer
from app.models.asset_moderation_link import AssetModerationLink
from app.extensions import db, limiter, csrf, user_limiter
from app.models.place_icon import PlaceIcon
from app.models.asset_thumbnail import AssetThumbnail
from app.models.gamepass_link import GamepassLink
from app.models.place_developer_product import DeveloperProduct
from app.models.product_receipt import ProductReceipt
from app.models.groups import Group, GroupRole, GroupRolePermission, GroupMember
from app.models.place_badge import PlaceBadge, UserBadge
from app.models.universe import Universe
from app.enums.AssetType import AssetType
from app.enums.TransactionType import TransactionType
from app.enums.PlaceYear import PlaceYear
from app.enums.PlaceRigChoice import PlaceRigChoice
from app.services import economy, groups
from datetime import datetime, timedelta
from io import BytesIO
from config import Config
import requests
import logging
import hashlib
import os
import random
import time
import math
config = Config()
DevelopPagesRoute = Blueprint('DevelopPagesRoute', __name__, template_folder='pages')
def CountAlphanumericCharacters( string : str ):
count = 0
for char in string:
if char.isalnum():
count += 1
return count
@DevelopPagesRoute.errorhandler( 429 )
def handle_ratelimit_reached(e):
flash("You are being rate limited, please try again later", "error")
return redirect(request.referrer)
@DevelopPagesRoute.route('/develop')
@auth.authenticated_required
def develop():
PageType = request.args.get('type', default = 9, type = int)
PageNumber = request.args.get('page', default = 1, type = int)
GroupIdContext = request.args.get('groupId', default = None, type = int)
AuthenticatedUser : User = auth.GetCurrentUser()
CreatorId : int = AuthenticatedUser.id
CreatorType : int = 0
if GroupIdContext is not None and GroupIdContext > 0:
GroupContext : Group = Group.query.filter_by(id=GroupIdContext).first()
if GroupContext is None:
return abort(404)
ViewerGroupRole : GroupRole | None = groups.GetUserRolesetInGroup( AuthenticatedUser, GroupContext )
if ViewerGroupRole is None:
return abort(403)
ViewerRolePermissions : GroupRolePermission = groups.GetRolesetPermission( ViewerGroupRole )
if not ViewerRolePermissions.manage_items:
return abort(403)
CreatorId = GroupContext.id
CreatorType = 1
else:
GroupIdContext = None
UserGroups : list[Group] = Group.query.join(GroupMember, GroupMember.group_id == Group.id).filter(GroupMember.user_id == AuthenticatedUser.id).join(GroupRolePermission, GroupRolePermission.group_role_id == GroupMember.group_role_id).filter(GroupRolePermission.manage_items == True).all()
PreviousPage = -1
if PageNumber > 1:
PreviousPage = PageNumber - 1
NextPage = -1
if PageType == 9:
# Get all the places
UserPlaces : list[Asset] = Asset.query.filter_by( creator_id=CreatorId, creator_type = CreatorType, asset_type=AssetType.Place ).join( Universe, Universe.root_place_id == Asset.id ).filter( Universe.id != None ).order_by( Universe.updated_at.desc() ).paginate(page = PageNumber, per_page = 10, error_out = False)
if UserPlaces.has_next:
NextPage = PageNumber + 1
def GetPlaceUniverse( AssetObj : Asset ):
return Universe.query.filter_by( root_place_id = AssetObj.id ).first()
return render_template('develop/subpages/games.html', PageType=PageType, UserPlaces=UserPlaces.items, PreviousPage=PreviousPage, NextPage=NextPage, PageNumber= PageNumber, GroupIdContext = GroupIdContext, UserGroups = UserGroups, GetPlaceUniverse = GetPlaceUniverse)
elif PageType == 11:
# Get all the clothing
UserClothing : list[Asset] = Asset.query.filter_by( creator_id=CreatorId, creator_type = CreatorType, asset_type=AssetType.Shirt ).order_by( Asset.updated_at.desc() ).paginate(page = PageNumber, per_page = 10, error_out = False)
if UserClothing.has_next:
NextPage = PageNumber + 1
return render_template('develop/subpages/shirts.html', PageType=PageType, UserClothing=UserClothing.items, PreviousPage=PreviousPage, NextPage=NextPage, PageNumber= PageNumber, GroupIdContext = GroupIdContext, UserGroups = UserGroups)
elif PageType == 12:
UserClothing : list[Asset] = Asset.query.filter_by( creator_id=CreatorId, creator_type = CreatorType, asset_type=AssetType.Pants ).order_by( Asset.updated_at.desc() ).paginate(page = PageNumber, per_page = 10, error_out = False)
if UserClothing.has_next:
NextPage = PageNumber + 1
return render_template('develop/subpages/pants.html', PageType=PageType, UserClothing=UserClothing.items, PreviousPage=PreviousPage, NextPage=NextPage, PageNumber= PageNumber, GroupIdContext = GroupIdContext, UserGroups = UserGroups)
elif PageType == 2:
UserClothing = Asset.query.filter_by( creator_id=CreatorId, creator_type = CreatorType, asset_type=AssetType.TShirt ).order_by( Asset.updated_at.desc() ).paginate(page = PageNumber, per_page = 10, error_out = False)
if UserClothing.has_next:
NextPage = PageNumber + 1
return render_template('develop/subpages/tshirt.html', PageType=PageType, UserClothing=UserClothing, PreviousPage=PreviousPage, NextPage=NextPage, PageNumber= PageNumber, GroupIdContext = GroupIdContext, UserGroups = UserGroups)
elif PageType == 3:
UserSounds = Asset.query.filter_by( creator_id=CreatorId, creator_type = CreatorType, asset_type=AssetType.Audio ).order_by( Asset.updated_at.desc() ).paginate(page = PageNumber, per_page = 10, error_out = False)
if UserSounds.has_next:
NextPage = PageNumber + 1
return render_template('develop/subpages/sound.html', PageType=PageType, UserSounds=UserSounds, PreviousPage=PreviousPage, NextPage=NextPage, PageNumber= PageNumber, GroupIdContext = GroupIdContext, UserGroups = UserGroups)
elif PageType == 1:
UserImages = Asset.query.filter_by( creator_id=CreatorId, creator_type = CreatorType, asset_type=AssetType.Image ).order_by( Asset.updated_at.desc() ).paginate(page = PageNumber, per_page = 10, error_out = False)
if UserImages.has_next:
NextPage = PageNumber + 1
return render_template('develop/subpages/image.html', PageType=PageType, UserImages=UserImages, PreviousPage=PreviousPage, NextPage=NextPage, PageNumber= PageNumber, GroupIdContext = GroupIdContext, UserGroups = UserGroups)
else:
return redirect(url_for("DevelopPagesRoute.develop"))
@DevelopPagesRoute.route("/develop/create/<int:ReqAssetType>", methods=["POST"])
@auth.authenticated_required
@limiter.limit("15/minute")
@user_limiter.limit("15/minute")
def create(ReqAssetType):
if ReqAssetType not in [9, 11, 12, 2, 3, 1]:
flash("Invalid asset type", "error")
return redirect(url_for("DevelopPagesRoute.develop"))
if not websiteFeatures.GetWebsiteFeature("AssetCreation"):
flash("Asset creation is temporarily disabled", "error")
return redirect(url_for("DevelopPagesRoute.develop"))
AuthenticatedUser : User = auth.GetCurrentUser()
TargetCreatorObj : User | Group = AuthenticatedUser
CreatorType = 0
groupIdContext = request.form.get(key = "groupIdContext", default = None, type = int)
if groupIdContext is not None and groupIdContext > 0:
GroupContext : Group = Group.query.filter_by(id=groupIdContext).first()
if GroupContext is None:
return abort(404)
ViewerGroupRole : GroupRole | None = groups.GetUserRolesetInGroup( AuthenticatedUser, GroupContext )
if ViewerGroupRole is None:
return abort(403)
ViewerRolePermissions : GroupRolePermission = groups.GetRolesetPermission( ViewerGroupRole )
if not ViewerRolePermissions.manage_items:
return abort(403)
if not ViewerRolePermissions.create_items:
flash("You do not have permission to create assets in this group", "error")
return redirect(url_for("DevelopPagesRoute.develop", groupId=groupIdContext, type=ReqAssetType))
if ReqAssetType == 9 and not ViewerRolePermissions.manage_group_games:
flash("You do not have permission to create places in this group", "error")
return redirect(url_for("DevelopPagesRoute.develop", groupId=groupIdContext, type=ReqAssetType))
TargetCreatorObj = GroupContext
CreatorType = 1
CreateLockName = f"createasset_{TargetCreatorObj.id}"
CreateLock = redislock.acquire_lock(CreateLockName, acquire_timeout=10, lock_timeout=1)
if CreateLock is None:
flash("You are creating too many assets at once", "error")
return redirect(url_for("DevelopPagesRoute.develop"))
@after_this_request
def handle_group_context( response : Response ):
if groupIdContext is not None and groupIdContext > 0:
response = redirect(url_for("DevelopPagesRoute.develop", groupId=groupIdContext, type=ReqAssetType))
return response
if ReqAssetType == 9:
# Check for the amount of places the user has
AmountOfPlaces = Universe.query.filter_by( creator_id = TargetCreatorObj.id, creator_type = CreatorType ).count()
MaxPlaces = 2
if CreatorType == 0:
UserCurrentMembership : MembershipType = GetUserMembership(AuthenticatedUser)
if UserCurrentMembership == MembershipType.BuildersClub:
MaxPlaces = 6
elif UserCurrentMembership == MembershipType.TurboBuildersClub:
MaxPlaces = 12
elif UserCurrentMembership == MembershipType.OutrageousBuildersClub:
MaxPlaces = 32
else:
MaxPlaces = 10
if AmountOfPlaces >= MaxPlaces:
redislock.release_lock(CreateLockName, CreateLock)
flash(f"You can only have {str(MaxPlaces)} places max", "error")
return redirect(url_for("DevelopPagesRoute.develop"))
HasCreatedPlaceRecently : bool = Universe.query.filter_by( creator_id = TargetCreatorObj.id, creator_type = CreatorType ).filter(Universe.created_at > datetime.utcnow() - timedelta( hours = 1 )).first() is not None
if HasCreatedPlaceRecently:
redislock.release_lock(CreateLockName, CreateLock)
flash("You can only create one place every hour", "error")
return redirect(url_for("DevelopPagesRoute.develop"))
NewAsset : Asset = Asset(
name = f"Untitled Place",
description = "Check out my new place!",
creator_id = TargetCreatorObj.id,
creator_type = CreatorType,
asset_type = AssetType.Place,
moderation_status=0,
created_at=datetime.utcnow()
)
db.session.add(NewAsset)
db.session.commit()
NewPlace : Place = Place(
placeid = NewAsset.id,
)
db.session.add(NewPlace)
db.session.commit()
DefaultPlaceFile = open("./app/files/Baseplate.rbxlx", "rb")
PlaceFileContent = DefaultPlaceFile.read()
DefaultPlaceFile.close()
DefaultPlaceFileHash = hashlib.sha512(PlaceFileContent).hexdigest()
if not s3helper.DoesKeyExist(DefaultPlaceFileHash):
s3helper.UploadBytesToS3(PlaceFileContent, DefaultPlaceFileHash)
NewAssetVersion : AssetVersion = CreateNewAssetVersion( NewAsset, DefaultPlaceFileHash, UploadedBy = AuthenticatedUser)
if NewAssetVersion is None:
redislock.release_lock(CreateLockName, CreateLock)
db.session.delete(NewAsset)
db.session.delete(NewPlace)
db.session.commit()
flash("Failed to create a new asset version", "error")
return redirect(url_for("DevelopPagesRoute.develop"))
TakeThumbnail( AssetId=NewAsset.id, isIcon=False )
TakeThumbnail( AssetId=NewAsset.id, isIcon=True )
NewUniverse : Universe = Universe(
root_place_id = NewAsset.id,
creator_id = TargetCreatorObj.id,
creator_type = CreatorType,
place_rig_choice = PlaceRigChoice.UserChoice,
place_year = PlaceYear.Sixteen
)
db.session.add(NewUniverse)
db.session.commit()
NewPlace.parent_universe_id = NewUniverse.id
db.session.commit()
return redirect(url_for("DevelopPagesRoute.develop"))
if ReqAssetType == 1: #Image
ImageName = request.form.get("name", default = "Image", type = str)
if ImageName is None:
redislock.release_lock(CreateLockName, CreateLock)
flash("No name was provided", "error")
return redirect(url_for("DevelopPagesRoute.develop", type=1))
if ImageName == "":
redislock.release_lock(CreateLockName, CreateLock)
flash("No name was provided", "error")
return redirect(url_for("DevelopPagesRoute.develop", type=1))
if len(ImageName) > 50:
redislock.release_lock(CreateLockName, CreateLock)
flash("Name is too long, max: 50 characters", "error")
return redirect(url_for("DevelopPagesRoute.develop", type=1))
ImageName = FilterText(ImageName)
ImageFile = request.files.get("file", default = None)
ImageObj = ValidateClothingImage( ImageFile, verifyResolution=False, validateFileSize=False, returnImage=True )
if ImageObj == False:
redislock.release_lock(CreateLockName, CreateLock)
flash("Invalid image file, Please make sure it is a PNG file and lesser than 3MB", "error")
return redirect(url_for("DevelopPagesRoute.develop", type=1))
if ImageFile.content_length > 1024 * 1024 * 3:
redislock.release_lock(CreateLockName, CreateLock)
flash("Image file is too large, max: 3MB", "error")
return redirect(url_for("DevelopPagesRoute.develop", type=1))
if ImageObj.size[0] > 2048 or ImageObj.size[1] > 2048:
redislock.release_lock(CreateLockName, CreateLock)
flash("Image resolution is too large, max: 2048 x 2048", "error")
return redirect(url_for("DevelopPagesRoute.develop", type=1))
NewImageAsset : Asset = Asset(
name = ImageName,
description = "",
creator_id = TargetCreatorObj.id,
creator_type = CreatorType,
asset_type = AssetType.Image,
created_at=datetime.utcnow()
)
db.session.add(NewImageAsset)
db.session.commit()
ImageFile = BytesIO()
ImageObj.save(ImageFile, format="PNG")
ImageFile.seek(0)
ImageFileContent = ImageFile.read()
ImageFileHash = hashlib.sha512(ImageFileContent).hexdigest()
if not s3helper.DoesKeyExist(ImageFileHash):
s3helper.UploadBytesToS3(ImageFileContent, ImageFileHash, contentType="image/png")
NewImageAssetVersion : AssetVersion = CreateNewAssetVersion( NewImageAsset, ImageFileHash, UploadedBy = AuthenticatedUser)
if NewImageAssetVersion is None:
redislock.release_lock(CreateLockName, CreateLock)
db.session.delete(NewImageAsset)
db.session.commit()
flash("Failed to create a new asset version", "error")
return redirect(url_for("DevelopPagesRoute.develop", type=1))
TakeThumbnail( AssetId=NewImageAsset.id, isIcon=False )
return redirect(url_for("DevelopPagesRoute.develop", type=1))
if ReqAssetType == 11: #shirt
ShirtName = request.form.get("name", default = "Shirt", type = str)
if ShirtName is None:
redislock.release_lock(CreateLockName, CreateLock)
flash("No name was provided", "error")
return redirect(url_for("DevelopPagesRoute.develop", type=11))
if ShirtName == "":
redislock.release_lock(CreateLockName, CreateLock)
flash("No name was provided", "error")
return redirect(url_for("DevelopPagesRoute.develop", type=11))
if len(ShirtName) > 50:
redislock.release_lock(CreateLockName, CreateLock)
flash("Name is too long, max: 50 characters", "error")
return redirect(url_for("DevelopPagesRoute.develop", type=11))
ShirtName = FilterText(ShirtName)
ShirtFile = request.files.get("file", default = None)
isValidClothingFile = ValidateClothingImage( ShirtFile, returnImage = True )
if not isValidClothingFile:
redislock.release_lock(CreateLockName, CreateLock)
flash("Invalid shirt file, Please make sure it is a PNG file 585 x 559 and lesser than 1mb", "error")
return redirect(url_for("DevelopPagesRoute.develop", type=11))
NewImageAsset : Asset = Asset(
name = "Image",
description = "",
creator_id = TargetCreatorObj.id,
creator_type = CreatorType,
asset_type = AssetType.Image,
created_at=datetime.utcnow()
)
db.session.add(NewImageAsset)
db.session.commit()
ShirtFile = BytesIO()
isValidClothingFile.save(ShirtFile, format="PNG")
ShirtFile.seek(0)
ImageFileContent = ShirtFile.read()
ShirtFileHash = hashlib.sha512(ImageFileContent).hexdigest()
if not s3helper.DoesKeyExist(ShirtFileHash):
s3helper.UploadBytesToS3(ImageFileContent, ShirtFileHash, contentType="image/png")
NewImageAssetVersion : AssetVersion = CreateNewAssetVersion( NewImageAsset, ShirtFileHash, UploadedBy = AuthenticatedUser)
if NewImageAssetVersion is None:
redislock.release_lock(CreateLockName, CreateLock)
db.session.delete(NewImageAsset)
db.session.commit()
flash("Failed to create a new asset version", "error")
return redirect(url_for("DevelopPagesRoute.develop", type=11))
TakeThumbnail( AssetId=NewImageAsset.id, isIcon=False )
ShirtTemplateFile = open("./app/files/Shirt.rbxmx", "r")
ShirtTemplateFileContent = ShirtTemplateFile.read()
ShirtTemplateFile.close()
ShirtTemplateFileContent = ShirtTemplateFileContent.format(ShirtImageId = str(NewImageAsset.id))
ShirtTemplateFileHash = hashlib.sha512(ShirtTemplateFileContent.encode()).hexdigest()
if not s3helper.DoesKeyExist(ShirtTemplateFileHash):
s3helper.UploadBytesToS3(ShirtTemplateFileContent, ShirtTemplateFileHash)
NewShirtAsset : Asset = Asset(
name = ShirtName,
description = "",
creator_id = TargetCreatorObj.id,
creator_type = CreatorType,
asset_type = AssetType.Shirt,
created_at=datetime.utcnow()
)
db.session.add(NewShirtAsset)
db.session.commit()
NewShirtAssetVersion : AssetVersion = CreateNewAssetVersion( NewShirtAsset, ShirtTemplateFileHash, UploadedBy = AuthenticatedUser)
if NewShirtAssetVersion is None:
redislock.release_lock(CreateLockName, CreateLock)
db.session.delete(NewShirtAsset)
db.session.commit()
flash("Failed to create a new asset version", "error")
return redirect(url_for("DevelopPagesRoute.develop", type=11))
NewAssetModerationLink : AssetModerationLink = AssetModerationLink(
ParentAssetId = NewShirtAsset.id,
ChildAssetId = NewImageAsset.id
)
db.session.add(NewAssetModerationLink)
NewUserAsset : UserAsset = UserAsset(
userid = AuthenticatedUser.id,
assetid = NewShirtAsset.id
)
db.session.add(NewUserAsset)
db.session.commit()
TakeThumbnail( AssetId=NewShirtAsset.id )
return redirect(url_for("DevelopPagesRoute.develop", type=11))
if ReqAssetType == 12: #pants
PantsName = request.form.get("name", default = "Pants", type = str)
if PantsName is None:
redislock.release_lock(CreateLockName, CreateLock)
flash("No name was provided", "error")
return redirect(url_for("DevelopPagesRoute.develop", type=12))
if PantsName == "":
redislock.release_lock(CreateLockName, CreateLock)
flash("No name was provided", "error")
return redirect(url_for("DevelopPagesRoute.develop", type=12))
if len(PantsName) > 50:
redislock.release_lock(CreateLockName, CreateLock)
flash("Name is too long, max: 50 characters", "error")
return redirect(url_for("DevelopPagesRoute.develop", type=12))
PantsName = FilterText(PantsName)
PantsFile = request.files.get("file", default = None)
isValidClothingFile = ValidateClothingImage( PantsFile, returnImage = True )
if not isValidClothingFile:
redislock.release_lock(CreateLockName, CreateLock)
flash("Invalid pants file, Please make sure it is a PNG file 585 x 559 and lesser than 1mb", "error")
return redirect(url_for("DevelopPagesRoute.develop", type=12))
NewImageAsset : Asset = Asset(
name = "Image",
description = "",
creator_id = TargetCreatorObj.id,
creator_type = CreatorType,
asset_type = AssetType.Image,
created_at=datetime.utcnow()
)
db.session.add(NewImageAsset)
db.session.commit()
PantsFile = BytesIO()
isValidClothingFile.save(PantsFile, format="PNG")
PantsFile.seek(0)
ImageFileContent = PantsFile.read()
PantsFileHash = hashlib.sha512(ImageFileContent).hexdigest()
if not s3helper.DoesKeyExist(PantsFileHash):
s3helper.UploadBytesToS3(ImageFileContent, PantsFileHash, contentType="image/png")
NewImageAssetVersion : AssetVersion = CreateNewAssetVersion( NewImageAsset, PantsFileHash, UploadedBy = AuthenticatedUser)
if NewImageAssetVersion is None:
redislock.release_lock(CreateLockName, CreateLock)
db.session.delete(NewImageAsset)
db.session.commit()
flash("Failed to create a new asset version", "error")
return redirect(url_for("DevelopPagesRoute.develop", type=12))
TakeThumbnail( AssetId=NewImageAsset.id, isIcon=False )
PantsTemplateFile = open("./app/files/Pants.rbxmx", "r")
PantsTemplateFileContent = PantsTemplateFile.read()
PantsTemplateFile.close()
PantsTemplateFileContent = PantsTemplateFileContent.format(PantsImageId = str(NewImageAsset.id))
PantsTemplateFileHash = hashlib.sha512(PantsTemplateFileContent.encode()).hexdigest()
if not s3helper.DoesKeyExist(PantsTemplateFileHash):
s3helper.UploadBytesToS3(PantsTemplateFileContent, PantsTemplateFileHash)
NewPantsAsset : Asset = Asset(
name = PantsName,
description = "",
creator_id = TargetCreatorObj.id,
creator_type = CreatorType,
asset_type = AssetType.Pants,
created_at=datetime.utcnow()
)
db.session.add(NewPantsAsset)
db.session.commit()
NewPantsAssetVersion : AssetVersion = CreateNewAssetVersion( NewPantsAsset, PantsTemplateFileHash, UploadedBy = AuthenticatedUser)
if NewPantsAssetVersion is None:
redislock.release_lock(CreateLockName, CreateLock)
db.session.delete(NewPantsAsset)
db.session.commit()
flash("Failed to create a new asset version", "error")
return redirect(url_for("DevelopPagesRoute.develop", type=12))
NewAssetModerationLink : AssetModerationLink = AssetModerationLink(
ParentAssetId = NewPantsAsset.id,
ChildAssetId = NewImageAsset.id
)
db.session.add(NewAssetModerationLink)
NewUserAsset : UserAsset = UserAsset(
userid = AuthenticatedUser.id,
assetid = NewPantsAsset.id
)
db.session.add(NewUserAsset)
db.session.commit()
TakeThumbnail( AssetId=NewPantsAsset.id )
return redirect(url_for("DevelopPagesRoute.develop", type=12))
if ReqAssetType == 2: #tshirt
TShirtName = request.form.get("name", default = "T-Shirt", type = str)
if TShirtName is None:
redislock.release_lock(CreateLockName, CreateLock)
flash("No name was provided", "error")
return redirect(url_for("DevelopPagesRoute.develop", type=2))
if TShirtName == "":
redislock.release_lock(CreateLockName, CreateLock)
flash("No name was provided", "error")
return redirect(url_for("DevelopPagesRoute.develop", type=2))
if len(TShirtName) > 50:
redislock.release_lock(CreateLockName, CreateLock)
flash("Name is too long, max: 50 characters", "error")
return redirect(url_for("DevelopPagesRoute.develop", type=2))
TShirtName = FilterText(TShirtName)
TShirtFile = request.files.get("file", default = None)
isValidClothingFile = ValidateClothingImage( TShirtFile, verifyResolution = False, returnImage = True )
if not isValidClothingFile:
redislock.release_lock(CreateLockName, CreateLock)
flash("Invalid TShirt file, Please make sure it is a PNG file and lesser than 1mb", "error")
return redirect(url_for("DevelopPagesRoute.develop", type=2))
NewImageAsset : Asset = Asset(
name = "Image",
description = "",
creator_id = TargetCreatorObj.id,
creator_type = CreatorType,
asset_type = AssetType.Image,
created_at=datetime.utcnow()
)
db.session.add(NewImageAsset)
db.session.commit()
TShirtFile = BytesIO()
isValidClothingFile.save(TShirtFile, format="PNG")
TShirtFile.seek(0)
ImageFileContent = TShirtFile.read()
TShirtFileHash = hashlib.sha512(ImageFileContent).hexdigest()
if not s3helper.DoesKeyExist(TShirtFileHash):
s3helper.UploadBytesToS3(ImageFileContent, TShirtFileHash, contentType="image/png")
NewImageAssetVersion : AssetVersion = CreateNewAssetVersion( NewImageAsset, TShirtFileHash, UploadedBy = AuthenticatedUser)
if NewImageAssetVersion is None:
redislock.release_lock(CreateLockName, CreateLock)
db.session.delete(NewImageAsset)
db.session.commit()
flash("Failed to create a new asset version", "error")
return redirect(url_for("DevelopPagesRoute.develop", type=12))
TakeThumbnail( AssetId=NewImageAsset.id, isIcon=False )
TShirtTemplateFile = open("./app/files/TShirt.rbxmx", "r")
TShirtTemplateFileContent = TShirtTemplateFile.read()
TShirtTemplateFile.close()
TShirtTemplateFileContent = TShirtTemplateFileContent.format(TShirtImageId = str(NewImageAsset.id))
TShirtTemplateFileHash = hashlib.sha512(TShirtTemplateFileContent.encode()).hexdigest()
if not s3helper.DoesKeyExist(TShirtTemplateFileHash):
s3helper.UploadBytesToS3(TShirtTemplateFileContent, TShirtTemplateFileHash)
NewTShirtAsset : Asset = Asset(
name = TShirtName,
description = "",
creator_id = TargetCreatorObj.id,
creator_type = CreatorType,
asset_type = AssetType.TShirt,
created_at=datetime.utcnow()
)
db.session.add(NewTShirtAsset)
db.session.commit()
NewTShirtAssetVersion : AssetVersion = CreateNewAssetVersion( NewTShirtAsset, TShirtTemplateFileHash, UploadedBy = AuthenticatedUser)
if NewTShirtAssetVersion is None:
redislock.release_lock(CreateLockName, CreateLock)
db.session.delete(NewTShirtAsset)
db.session.commit()
flash("Failed to create a new asset version", "error")
return redirect(url_for("DevelopPagesRoute.develop", type=2))
NewAssetModerationLink : AssetModerationLink = AssetModerationLink(
ParentAssetId = NewTShirtAsset.id,
ChildAssetId = NewImageAsset.id
)
db.session.add(NewAssetModerationLink)
NewUserAsset : UserAsset = UserAsset(
userid = AuthenticatedUser.id,
assetid = NewTShirtAsset.id
)
db.session.add(NewUserAsset)
db.session.commit()
TakeThumbnail( AssetId=NewTShirtAsset.id )
return redirect(url_for("DevelopPagesRoute.develop", type=2))
if ReqAssetType == 3: #sound
SoundName = request.form.get("name", default = "Sound", type = str)
if SoundName is None:
redislock.release_lock(CreateLockName, CreateLock)
flash("No name was provided", "error")
return redirect(url_for("DevelopPagesRoute.develop", type=3))
if SoundName == "":
redislock.release_lock(CreateLockName, CreateLock)
flash("No name was provided", "error")
return redirect(url_for("DevelopPagesRoute.develop", type=3))
if len(SoundName) > 50:
redislock.release_lock(CreateLockName, CreateLock)
flash("Name is too long, max: 50 characters", "error")
return redirect(url_for("DevelopPagesRoute.develop", type=3))
SoundName = FilterText(SoundName)
SoundFile = request.files.get("file", default = None)
if SoundFile is None:
redislock.release_lock(CreateLockName, CreateLock)
flash("No file was provided", "error")
return redirect(url_for("DevelopPagesRoute.develop", type=3))
if SoundFile.filename == "":
redislock.release_lock(CreateLockName, CreateLock)
flash("No file was provided", "error")
return redirect(url_for("DevelopPagesRoute.develop", type=3))
# soundDuration = ValidateMP3File( SoundFile )
# if soundDuration is None:
# redislock.release_lock(CreateLockName, CreateLock)
# flash("Invalid sound file, Please make sure it is a MP3 file and lesser than 4mb", "error")
# return redirect(url_for("DevelopPagesRoute.develop", type=3))
if SoundFile.content_length > 1024 * 1024 * 8:
redislock.release_lock(CreateLockName, CreateLock)
flash("Sound file is too large, max: 8MB", "error")
return redirect(url_for("DevelopPagesRoute.develop", type=3))
try:
ConvertedSoundData, soundDuration = ValidateMP3AndConvertToOGG( SoundFile )
except Exception as e:
redislock.release_lock(CreateLockName, CreateLock)
flash(f"Failed to validate file: {str(e)}", "error")
return redirect(url_for("DevelopPagesRoute.develop", type=3))
if soundDuration > 60 * 8:
redislock.release_lock(CreateLockName, CreateLock)
flash("Sound is too long, max: 8 minutes", "error")
return redirect(url_for("DevelopPagesRoute.develop", type=3))
soundDurationHalfed = soundDuration
if soundDuration > 20:
soundDurationHalfed = 20 + ((soundDuration - 20) * 0.5)
creationCost = math.floor(max(20, soundDurationHalfed))
robuxBalance, _ = economy.GetUserBalance(AuthenticatedUser)
if robuxBalance < creationCost:
redislock.release_lock(CreateLockName, CreateLock)
flash(f"You do not have enough robux to create this sound, Required: R${str(creationCost)}", "error")
return redirect(url_for("DevelopPagesRoute.develop", type=3))
try:
economy.DecrementTargetBalance(AuthenticatedUser, creationCost, 0)
except economy.EconomyLockAcquireException:
redislock.release_lock(CreateLockName, CreateLock)
flash("Failed to create sound, Please try again later", "error")
return redirect(url_for("DevelopPagesRoute.develop", type=3))
except economy.InsufficientFundsException:
redislock.release_lock(CreateLockName, CreateLock)
flash(f"You do not have enough robux to create this sound, Required: R${str(creationCost)}", "error")
return redirect(url_for("DevelopPagesRoute.develop", type=3))
except Exception as e:
redislock.release_lock(CreateLockName, CreateLock)
logging.error(f"Failed to create sound, {str(e)}")
flash("Failed to create sound, Please try again later", "error")
return redirect(url_for("DevelopPagesRoute.develop", type=3))
#transactions.CreateTransaction(User.query.filter_by(id=1).first(), AuthenticatedUser, creationCost, 0, TransactionType.Purchase, None, "Created Sound")
transactions.CreateTransaction(
Sender = AuthenticatedUser,
CurrencyAmount = creationCost,
CurrencyType = 0,
TransactionType = TransactionType.Purchase,
CustomText = "Created Sound"
)
#SoundFile.seek(0)
SoundFileContent = ConvertedSoundData
SoundFileHash = hashlib.sha512(SoundFileContent).hexdigest()
if not s3helper.DoesKeyExist(SoundFileHash):
s3helper.UploadBytesToS3(SoundFileContent, SoundFileHash, contentType="audio/ogg")
NewSoundAsset : Asset = Asset(
name = SoundName,
description = "",
creator_id = TargetCreatorObj.id,
creator_type = CreatorType,
asset_type = AssetType.Audio,
created_at=datetime.utcnow()
)
db.session.add(NewSoundAsset)
db.session.commit()
NewSoundAssetVersion : AssetVersion = CreateNewAssetVersion( NewSoundAsset, SoundFileHash, UploadedBy = AuthenticatedUser)
if NewSoundAssetVersion is None:
redislock.release_lock(CreateLockName, CreateLock)
db.session.delete(NewSoundAsset)
db.session.commit()
flash("Failed to create a new asset version, please report this error to our discord server", "error")
return redirect(url_for("DevelopPagesRoute.develop", type=3))
NewUserAsset : UserAsset = UserAsset(
userid = AuthenticatedUser.id,
assetid = NewSoundAsset.id
)
db.session.add(NewUserAsset)
db.session.commit()
TakeThumbnail( AssetId=NewSoundAsset.id )
return redirect(url_for("DevelopPagesRoute.develop", type=3))
def isUserAllowedtoViewPage( ViewerUser : User, RelatedObj : Asset | Universe, abortOnFail : bool = True, isGameContext : bool = True ):
if RelatedObj is None:
if abortOnFail:
abort(404)
else:
return False
if RelatedObj.creator_id != ViewerUser.id and RelatedObj.creator_type == 0:
if abortOnFail:
abort(404)
else:
return False
elif RelatedObj.creator_type == 1:
ViewerGroupRole : GroupRole | None = groups.GetUserRolesetInGroup( ViewerUser, RelatedObj.creator_id )
if ViewerGroupRole is None:
if abortOnFail:
abort(404)
else:
return False
ViewerRolePermissions : GroupRolePermission = groups.GetRolesetPermission( ViewerGroupRole )
if not ViewerRolePermissions.manage_items:
if abortOnFail:
abort(404)
else:
return False
if not ViewerRolePermissions.manage_group_games:
if abortOnFail:
abort(404)
else:
return False
if type(RelatedObj) != Universe:
if RelatedObj.asset_type != AssetType.Place and isGameContext:
if abortOnFail:
abort(404)
else:
return False
if RelatedObj.moderation_status != 0:
if abortOnFail:
abort(403)
else:
return False
return True
@DevelopPagesRoute.route("/develop/universes/<int:universeid>/manage", methods=["GET", "POST"])
@auth.authenticated_required
def ManageUniversePage( universeid : int ):
AuthenticatedUser : User = auth.GetCurrentUser()
UniverseObj : Universe = Universe.query.filter_by(id=universeid).first()
isUserAllowedtoViewPage(AuthenticatedUser, UniverseObj, abortOnFail=True)
RootPlaceAssetObj : Asset = Asset.query.filter_by(id = UniverseObj.root_place_id).first()
if request.method == "GET":
return render_template("develop/universes/manage.html", UniverseObj=UniverseObj, RootPlaceAssetObj=RootPlaceAssetObj)
else:
PlaceName : str = request.form.get("name", default="", type=str)
PlaceDescription : str = request.form.get("description", default="", type=str)
try:
AssetYear : PlaceYear = PlaceYear(request.form.get("place_year", default=2016, type=int))
except ValueError:
flash("Invalid year", "error")
return redirect(f"/develop/universes/{universeid}/manage")
if AssetYear not in [ PlaceYear.Sixteen, PlaceYear.Eighteen, PlaceYear.Twenty, PlaceYear.Fourteen, PlaceYear.TwentyOne ]:
flash("Invalid year", "error")
return redirect(f"/develop/universes/{universeid}/manage")
if len(PlaceName) < 3 or len(PlaceName) > 50:
flash("Place name has to be between 3 to 50 characters long", "error")
return redirect(f"/develop/universes/{universeid}/manage")
if len(PlaceDescription) < 3 or len(PlaceDescription) > 700:
flash("Place description has to be between 3 to 700 characters long", "error")
return redirect(f"/develop/universes/{universeid}/manage")
if PlaceDescription.count("\n") > 10:
flash("Place description can only have 10 or less newlines", "error")
return redirect(f"/develop/universes/{universeid}/manage")
if UniverseObj.place_year != AssetYear:
TotalServersActive : int = PlaceServer.query.join(Place, Place.placeid == PlaceServer.serverPlaceId).join( Universe, Place.parent_universe_id == Universe.id ).filter( Universe.id == universeid ).count()
if TotalServersActive > 0:
flash("You cannot change the place year of the universe while there are active servers", "error")
return redirect(f"/develop/universes/{universeid}/manage")
UniverseObj.place_year = AssetYear
flash("Successfully updated place year", "success")
PlaceName = FilterText(PlaceName)
PlaceDescription = FilterText(PlaceDescription)
RootPlaceAssetObj.name = PlaceName
RootPlaceAssetObj.description = PlaceDescription
RootPlaceAssetObj.updated_at = datetime.utcnow()
UniverseObj.updated_at = datetime.utcnow()
db.session.commit()
flash("Successfully updated place", "success")
return redirect(f"/develop/universes/{universeid}/manage")
@DevelopPagesRoute.route("/develop/universes/<int:universeid>/access", methods=["GET", "POST"])
@auth.authenticated_required
def ManageUniverseAccessPage( universeid : int ):
AuthenticatedUser : User = auth.GetCurrentUser()
UniverseObj : Universe = Universe.query.filter_by(id=universeid).first()
isUserAllowedtoViewPage(AuthenticatedUser, UniverseObj, abortOnFail=True)
RootPlaceAssetObj : Asset = Asset.query.filter_by(id = UniverseObj.root_place_id).first()
if request.method == "GET":
return render_template("develop/universes/access.html", UniverseObj=UniverseObj, RootPlaceAssetObj=RootPlaceAssetObj)
else:
MinimumAccountAge : int = request.form.get("minaccountage", default=0, type=int)
if MinimumAccountAge < 0 or MinimumAccountAge > 365:
flash("Minimum account age has to be between 0 to 365", "error")
return redirect(f"/develop/universes/{universeid}/access")
isPublic : bool = request.form.get("ispublic") == "on"
BuildersClubRequired : bool = request.form.get("bcrequired") == "on"
UniverseObj.minimum_account_age = MinimumAccountAge
UniverseObj.is_public = isPublic
UniverseObj.bc_required = BuildersClubRequired
UniverseObj.updated_at = datetime.utcnow()
db.session.commit()
flash("Successfully updated place access", "success")
return redirect(f"/develop/universes/{universeid}/access")
@DevelopPagesRoute.route("/develop/universes/<int:universeid>/places", methods=["GET"])
@auth.authenticated_required
def ManageUniversePlacesPage( universeid : int ):
AuthenticatedUser : User = auth.GetCurrentUser()
UniverseObj : Universe = Universe.query.filter_by(id=universeid).first()
isUserAllowedtoViewPage(AuthenticatedUser, UniverseObj, abortOnFail=True)
RootPlaceAssetObj : Asset = Asset.query.filter_by(id = UniverseObj.root_place_id).first()
TotalAmountPlacesCreated : int = Place.query.filter_by(parent_universe_id = universeid).count()
universePlaces : list[Place] = Place.query.filter_by(parent_universe_id = universeid).order_by(Place.placeid.asc()).all()
return render_template("develop/universes/places.html", UniverseObj=UniverseObj, RootPlaceAssetObj=RootPlaceAssetObj, universePlaces=universePlaces, TotalAmountPlacesCreated=TotalAmountPlacesCreated)
@DevelopPagesRoute.route("/develop/universes/<int:universeid>/create-place", methods=["GET", "POST"])
@auth.authenticated_required
def CreateUniversePlacePage( universeid : int ):
AuthenticatedUser : User = auth.GetCurrentUser()
UniverseObj : Universe = Universe.query.filter_by(id=universeid).first()
isUserAllowedtoViewPage(AuthenticatedUser, UniverseObj, abortOnFail=True)
RootPlaceAssetObj : Asset = Asset.query.filter_by(id = UniverseObj.root_place_id).first()
TotalAmountPlacesCreated : int = Place.query.filter_by(parent_universe_id = universeid).count()
if request.method == "GET":
return render_template("develop/universes/create-place.html", UniverseObj=UniverseObj, RootPlaceAssetObj=RootPlaceAssetObj, TotalAmountPlacesCreated=TotalAmountPlacesCreated)
else:
if TotalAmountPlacesCreated >= 10:
flash("You can only have 10 places in a universe", "error")
return redirect(f"/develop/universes/{universeid}/places")
place_creation_lock_name = f"place_creation_lock_{universeid}"
place_creation_lock = redislock.acquire_lock( lock_name = place_creation_lock_name, acquire_timeout = 5, lock_timeout = 3 )
if place_creation_lock is None:
flash("Failed to create place, please try again later", "error")
return redirect(f"/develop/universes/{universeid}/places")
NewPlaceAsset : Asset = Asset(
name = f"Untitled Place",
description = "Check out my new place!",
creator_id = UniverseObj.creator_id,
creator_type = UniverseObj.creator_type,
asset_type = AssetType.Place,
moderation_status = 0,
created_at=datetime.utcnow(),
updated_at=datetime.utcnow()
)
db.session.add(NewPlaceAsset)
db.session.commit()
NewPlace : Place = Place(
placeid = NewPlaceAsset.id,
parent_universe_id = universeid
)
db.session.add(NewPlace)
db.session.commit()
DefaultPlaceFile = open("./app/files/Baseplate.rbxlx", "rb")
PlaceFileContent = DefaultPlaceFile.read()
DefaultPlaceFile.close()
DefaultPlaceFileHash = hashlib.sha512(PlaceFileContent).hexdigest()
if not s3helper.DoesKeyExist(DefaultPlaceFileHash):
s3helper.UploadBytesToS3(PlaceFileContent, DefaultPlaceFileHash)
redislock.release_lock(place_creation_lock_name, place_creation_lock)
NewAssetVersion : AssetVersion = CreateNewAssetVersion( NewPlaceAsset, DefaultPlaceFileHash, UploadedBy = AuthenticatedUser)
if NewAssetVersion is None:
db.session.delete(NewPlaceAsset)
db.session.delete(NewPlace)
db.session.commit()
flash("Failed to create a new asset version", "error")
return redirect(f"/develop/universes/{universeid}/places")
TakeThumbnail( AssetId=NewPlaceAsset.id, isIcon=False )
TakeThumbnail( AssetId=NewPlaceAsset.id, isIcon=True )
flash("Successfully created a new place", "success")
return redirect(f"/develop/universes/{universeid}/places")
@DevelopPagesRoute.route("/develop/universes/<int:universeid>/gamepasses", methods=["GET"])
@auth.authenticated_required
def ManageUniverseGamepassesPage( universeid : int ):
AuthenticatedUser : User = auth.GetCurrentUser()
UniverseObj : Universe = Universe.query.filter_by(id=universeid).first()
isUserAllowedtoViewPage(AuthenticatedUser, UniverseObj, abortOnFail=True)
RootPlaceAssetObj : Asset = Asset.query.filter_by(id = UniverseObj.root_place_id).first()
Gamepasses : list[GamepassLink] = GamepassLink.query.filter_by(universe_id = UniverseObj.id).all()
return render_template("develop/universes/gamepasses.html", UniverseObj=UniverseObj, RootPlaceAssetObj=RootPlaceAssetObj, Gamepasses=Gamepasses)
@DevelopPagesRoute.route("/develop/universes/<int:universeid>/gamepass/<int:gamepassid>", methods=["GET", "POST"])
@auth.authenticated_required
def ManageUniverseGamepassPage( universeid : int, gamepassid : int ):
AuthenticatedUser : User = auth.GetCurrentUser()
UniverseObj : Universe = Universe.query.filter_by(id=universeid).first()
isUserAllowedtoViewPage(AuthenticatedUser, UniverseObj, abortOnFail=True)
RootPlaceAssetObj : Asset = Asset.query.filter_by(id = UniverseObj.root_place_id).first()
GamepassObj : GamepassLink = GamepassLink.query.filter_by( gamepass_id = gamepassid, universe_id = UniverseObj.id ).first()
if GamepassObj is None:
abort(404)
if request.method == "GET":
return render_template("develop/universes/edit-gamepass.html", UniverseObj=UniverseObj, RootPlaceAssetObj=RootPlaceAssetObj, GamepassObj=GamepassObj)
else:
AssetName = request.form.get("pass-name", default = "", type = str)
AssetDescription = request.form.get("pass-description", default = "", type = str)
isForSale = request.form.get("is-for-sale", default = "off") == "on"
AssetRobuxPrice = request.form.get("robux-cost", default = 0, type = int)
if AssetName == "":
flash("Name cannot be empty", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/gamepass/{GamepassObj.gamepass_id}")
if len(AssetName) > 35:
flash("Name cannot be longer than 35 characters", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/gamepass/{GamepassObj.gamepass_id}")
if CountAlphanumericCharacters(AssetName) < 3:
flash("Name must contain at least 3 alphanumeric characters", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/gamepass/{GamepassObj.gamepass_id}")
if AssetDescription == "":
flash("Description cannot be empty", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/gamepass/{GamepassObj.gamepass_id}")
if len(AssetDescription) > 200:
flash("Description cannot be longer than 200 characters", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/gamepass/{GamepassObj.gamepass_id}")
if (AssetRobuxPrice < 1 or AssetRobuxPrice > 1000000 ):
flash("Robux price has to be between 1 to 1,000,000", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/gamepass/{GamepassObj.gamepass_id}")
UserCurrentMembership : MembershipType = GetUserMembership(AuthenticatedUser)
if UserCurrentMembership == MembershipType.NonBuildersClub and isForSale:
flash("You must be Builders Club to sell items", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/gamepass/{GamepassObj.gamepass_id}")
AssetName = FilterText(AssetName)
AssetDescription = FilterText(AssetDescription)
NewIconFile = request.files.get("icon-file", default = None)
if NewIconFile is not None:
if NewIconFile.filename != "":
if NewIconFile.content_length > 1024 * 1024:
flash("File size is too big", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/gamepass/{GamepassObj.gamepass_id}")
IconImage = ValidateClothingImage( NewIconFile, verifyResolution=False, validateFileSize=False, returnImage=True )
if IconImage is False or IconImage is None:
flash("Invalid image", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/gamepass/{GamepassObj.gamepass_id}")
if IconImage.width != IconImage.height:
flash("Image is not square", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/gamepass/{GamepassObj.gamepass_id}")
if IconImage.width < 128 or IconImage.width > 1024:
flash("Image is not between 128x128 and 1024x1024", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/gamepass/{GamepassObj.gamepass_id}")
NewIconFile = BytesIO()
IconImage.save(NewIconFile, format="PNG")
NewIconFile.seek(0)
IconImageHash = hashlib.sha512(NewIconFile.read()).hexdigest()
if not s3helper.DoesKeyExist(IconImageHash):
NewIconFile.seek(0)
s3helper.UploadBytesToS3(NewIconFile.read(), IconImageHash, contentType="image/png")
LatestAssetThumbnail : AssetThumbnail = AssetThumbnail.query.filter_by(asset_id=GamepassObj.gamepass_id).order_by(AssetThumbnail.asset_version_id.desc()).first()
if LatestAssetThumbnail is None:
flash("Failed to get latest asset thumbnail", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/gamepass/{GamepassObj.gamepass_id}")
LatestAssetThumbnail.content_hash = IconImageHash
LatestAssetThumbnail.created_at = datetime.utcnow()
LatestAssetThumbnail.moderation_status = 1
db.session.commit()
flash("Successfully updated gamepass icon", "success")
GamepassObj.gamepass.name = AssetName
GamepassObj.gamepass.description = AssetDescription
GamepassObj.gamepass.is_for_sale = isForSale
GamepassObj.gamepass.price_robux = AssetRobuxPrice
GamepassObj.gamepass.updated_at = datetime.utcnow()
db.session.commit()
flash("Successfully updated gamepass settings", "success")
return redirect(f"/develop/universes/{UniverseObj.id}/gamepass/{GamepassObj.gamepass_id}")
@DevelopPagesRoute.route("/develop/universes/<int:universeid>/create-gamepass", methods=["GET", "POST"])
@auth.authenticated_required
def CreateUniverseGamepassPage( universeid : int ):
AuthenticatedUser : User = auth.GetCurrentUser()
UniverseObj : Universe = Universe.query.filter_by(id=universeid).first()
isUserAllowedtoViewPage(AuthenticatedUser, UniverseObj, abortOnFail=True)
RootPlaceAssetObj : Asset = Asset.query.filter_by(id = UniverseObj.root_place_id).first()
if request.method == "GET":
return render_template("develop/universes/create-gamepass.html", UniverseObj=UniverseObj, RootPlaceAssetObj=RootPlaceAssetObj)
else:
gamepassName = request.form.get("name", default="", type=str)
gamepassDescription = request.form.get("description", default="", type=str)
file = request.files.get("file", default=None)
if gamepassName == "":
flash("Name cannot be empty", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/create-gamepass")
if len(gamepassName) > 35:
flash("Name cannot be longer than 35 characters", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/create-gamepass")
if CountAlphanumericCharacters(gamepassName) < 3:
flash("Name must contain at least 3 alphanumeric characters", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/create-gamepass")
if gamepassDescription == "":
flash("Description cannot be empty", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/create-gamepass")
if len(gamepassDescription) > 200:
flash("Description cannot be longer than 200 characters", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/create-gamepass")
if file is None:
flash("No file was provided", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/create-gamepass")
if file.filename == "":
flash("No file was provided", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/create-gamepass")
if file.content_length > 1024 * 1024:
flash("File size is too big", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/create-gamepass")
GamepassCount : int = GamepassLink.query.filter_by(universe_id = UniverseObj.id).count()
if GamepassCount >= 15:
flash("You cannot create more than 15 gamepasses for a place", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/create-gamepass")
FileImage = ValidateClothingImage(file, verifyResolution=False, validateFileSize=False, returnImage=True)
if FileImage is False or FileImage is None:
flash("Invalid image", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/create-gamepass")
if FileImage.width != FileImage.height:
flash("Image is not square", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/create-gamepass")
if FileImage.width < 128 or FileImage.width > 1024:
flash("Image is not between 128x128 and 1024x1024", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/create-gamepass")
gamepassName = FilterText(gamepassName)
gamepassDescription = FilterText(gamepassDescription)
file = BytesIO()
FileImage.save(file, format="png")
file.seek(0)
FileImageHash = hashlib.sha512(file.read()).hexdigest()
if not s3helper.DoesKeyExist(FileImageHash):
file.seek(0)
s3helper.UploadBytesToS3(file.read(), FileImageHash, contentType="image/png")
NewGamepassObj : Asset = Asset(
name = gamepassName,
description = gamepassDescription,
creator_id = UniverseObj.creator_id,
creator_type = UniverseObj.creator_type,
asset_type = AssetType.GamePass,
created_at = datetime.utcnow(),
updated_at = datetime.utcnow(),
moderation_status = 0
)
db.session.add(NewGamepassObj)
db.session.commit()
NewGamepassLink : GamepassLink = GamepassLink(
place_id = UniverseObj.root_place_id,
universe_id = UniverseObj.id,
gamepass_id = NewGamepassObj.id,
creator_id = AuthenticatedUser.id
)
db.session.add(NewGamepassLink)
db.session.commit()
EmptyHash = hashlib.sha512(b"").hexdigest()
NewGamepassVersion : AssetVersion = CreateNewAssetVersion( NewGamepassObj, EmptyHash, ForceNewVersion = True, UploadedBy = AuthenticatedUser)
if NewGamepassVersion is None:
db.session.delete(NewGamepassObj)
db.session.delete(NewGamepassLink)
db.session.commit()
flash("Failed to create a new asset version", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/create-gamepass")
NewGamepassThumbnail : AssetThumbnail = AssetThumbnail(
asset_id = NewGamepassObj.id,
asset_version_id = NewGamepassVersion.version,
content_hash = FileImageHash,
created_at = datetime.utcnow(),
moderation_status = 1
)
db.session.add(NewGamepassThumbnail)
db.session.commit()
flash("Successfully created gamepass", "success")
return redirect(f"/develop/universes/{UniverseObj.id}/gamepasses")
@DevelopPagesRoute.route("/develop/universes/<int:universeid>/developer-products", methods=["GET"])
@auth.authenticated_required
def ManageUniverseDeveloperProductsPage( universeid : int ):
AuthenticatedUser : User = auth.GetCurrentUser()
UniverseObj : Universe = Universe.query.filter_by(id=universeid).first()
isUserAllowedtoViewPage(AuthenticatedUser, UniverseObj, abortOnFail=True)
RootPlaceAssetObj : Asset = Asset.query.filter_by(id = UniverseObj.root_place_id).first()
DeveloperProducts : list[DeveloperProduct] = DeveloperProduct.query.filter_by(universe_id = UniverseObj.id).all()
return render_template("develop/universes/developerproducts.html", UniverseObj=UniverseObj, RootPlaceAssetObj=RootPlaceAssetObj, DeveloperProducts=DeveloperProducts)
@DevelopPagesRoute.route("/develop/universes/<int:universeid>/create-product", methods=["GET", "POST"])
@auth.authenticated_required
def CreateUniveseDeveloperProductPage( universeid : int ):
AuthenticatedUser : User = auth.GetCurrentUser()
UniverseObj : Universe = Universe.query.filter_by(id=universeid).first()
isUserAllowedtoViewPage(AuthenticatedUser, UniverseObj, abortOnFail=True)
RootPlaceAssetObj : Asset = Asset.query.filter_by(id = UniverseObj.root_place_id).first()
if request.method == "GET":
return render_template("develop/universes/create-product.html", UniverseObj=UniverseObj, RootPlaceAssetObj=RootPlaceAssetObj)
else:
productName = request.form.get("name", default="", type=str)
productDescription = request.form.get("description", default="", type=str)
file = request.files.get("file", default=None)
if productName == "":
flash("Name cannot be empty", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/create-product")
if len(productName) > 35:
flash("Name cannot be longer than 35 characters", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/create-product")
if CountAlphanumericCharacters(productName) < 3:
flash("Name must contain at least 3 alphanumeric characters", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/create-product")
if productDescription == "":
flash("Description cannot be empty", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/create-product")
if len(productDescription) > 200:
flash("Description cannot be longer than 200 characters", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/create-product")
if file is None:
flash("No file was provided", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/create-product")
if file.filename == "":
flash("No file was provided", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/create-product")
if file.content_length > 1024 * 1024:
flash("File size is too big", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/create-product")
FileImage = ValidateClothingImage(file, verifyResolution=False, validateFileSize=False, returnImage=True)
if FileImage is False or FileImage is None:
flash("Invalid image", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/create-product")
if FileImage.width != FileImage.height:
flash("Image is not square", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/create-product")
if FileImage.width < 128 or FileImage.width > 1024:
flash("Image is not between 128x128 and 1024x1024", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/create-product")
productName = FilterText(productName)
productDescription = FilterText(productDescription)
file = BytesIO()
FileImage.save(file, format="png")
file.seek(0)
FileImageHash = hashlib.sha512(file.read()).hexdigest()
if not s3helper.DoesKeyExist(FileImageHash):
file.seek(0)
s3helper.UploadBytesToS3(file.read(), FileImageHash, contentType="image/png")
NewImageAssetObj : Asset = Asset(
name = "DeveloperProductImage",
description = "DeveloperProduct icon",
creator_id = AuthenticatedUser.id,
creator_type = 0,
asset_type = AssetType.Image,
created_at = datetime.utcnow(),
updated_at = datetime.utcnow(),
moderation_status = 1
)
db.session.add(NewImageAssetObj)
db.session.commit()
NewImageAssetVersion : AssetVersion = CreateNewAssetVersion( NewImageAssetObj, FileImageHash, ForceNewVersion = True, UploadedBy = AuthenticatedUser)
if NewImageAssetVersion is None:
db.session.delete(NewImageAssetObj)
db.session.commit()
flash("Failed to create a new asset version, please contact support", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/create-product")
NewDeveloperProductObj : DeveloperProduct = DeveloperProduct(
placeid = UniverseObj.root_place_id,
name = productName,
description = productDescription,
iconimage_assetid = NewImageAssetObj.id,
creator_id = AuthenticatedUser.id,
universe_id = UniverseObj.id
)
TakeThumbnail( AssetId=NewImageAssetObj.id, isIcon=False )
db.session.add(NewDeveloperProductObj)
db.session.commit()
flash("Successfully created developer product", "success")
return redirect(f"/develop/universes/{UniverseObj.id}/developer-products")
@DevelopPagesRoute.route("/develop/universes/<int:universeid>/developer-products/<int:productid>", methods=["GET", "POST"])
@auth.authenticated_required
def ManageUniverseDeveloperProductPage( universeid : int, productid : int ):
AuthenticatedUser : User = auth.GetCurrentUser()
UniverseObj : Universe = Universe.query.filter_by(id=universeid).first()
isUserAllowedtoViewPage(AuthenticatedUser, UniverseObj, abortOnFail=True)
RootPlaceAssetObj : Asset = Asset.query.filter_by(id = UniverseObj.root_place_id).first()
DeveloperProductObj : DeveloperProduct = DeveloperProduct.query.filter_by(universe_id = UniverseObj.id, productid = productid).first()
if DeveloperProductObj is None:
abort(404)
if request.method == "GET":
return render_template("develop/universes/edit-product.html", UniverseObj=UniverseObj, RootPlaceAssetObj=RootPlaceAssetObj, ProductObj=DeveloperProductObj)
else:
productName = request.form.get("product-name", default="", type=str)
productDescription = request.form.get("product-description", default="", type=str)
file = request.files.get("icon-file", default=None)
is_for_sale = request.form.get("is-for-sale", default="off") == "on"
robux_price = request.form.get("robux-cost", default=0, type=int)
if productName == "":
flash("Name cannot be empty", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/developer-products/{DeveloperProductObj.productid}")
if len(productName) > 35:
flash("Name cannot be longer than 35 characters", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/developer-products/{DeveloperProductObj.productid}")
if CountAlphanumericCharacters(productName) < 3:
flash("Name must contain at least 3 alphanumeric characters", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/developer-products/{DeveloperProductObj.productid}")
if productDescription == "":
flash("Description cannot be empty", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/developer-products/{DeveloperProductObj.productid}")
if len(productDescription) > 200:
flash("Description cannot be longer than 200 characters", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/developer-products/{DeveloperProductObj.productid}")
if (robux_price < 1 or robux_price > 1000000 ):
flash("Robux price has to be between 1 to 1,000,000", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/developer-products/{DeveloperProductObj.productid}")
UserCurrentMembership : MembershipType = GetUserMembership(AuthenticatedUser)
if UserCurrentMembership == MembershipType.NonBuildersClub and is_for_sale:
flash("You must be a Builders Club member to sell items", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/developer-products/{DeveloperProductObj.productid}")
productName = FilterText(productName)
productDescription = FilterText(productDescription)
if file is not None:
if file.filename != "":
if file.content_length > 1024 * 1024:
flash("File size is too big", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/developer-products/{DeveloperProductObj.productid}")
FileImage = ValidateClothingImage(file, verifyResolution=False, validateFileSize=False, returnImage=True)
if FileImage is False or FileImage is None:
flash("Invalid image", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/developer-products/{DeveloperProductObj.productid}")
if FileImage.width != FileImage.height:
flash("Image is not square", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/developer-products/{DeveloperProductObj.productid}")
if FileImage.width < 128 or FileImage.width > 1024:
flash("Image is not between 128x128 and 1024x1024", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/developer-products/{DeveloperProductObj.productid}")
file = BytesIO()
FileImage.save(file, format="PNG")
file.seek(0)
FileImageHash = hashlib.sha512(file.read()).hexdigest()
if not s3helper.DoesKeyExist(FileImageHash):
file.seek(0)
s3helper.UploadBytesToS3(file.read(), FileImageHash, contentType="image/png")
NewImageAssetObj : Asset = Asset(
name = "DeveloperProductImage",
description = "DeveloperProduct icon",
creator_id = AuthenticatedUser.id,
creator_type = 0,
asset_type = AssetType.Image,
created_at = datetime.utcnow(),
updated_at = datetime.utcnow(),
moderation_status = 1
)
db.session.add(NewImageAssetObj)
db.session.commit()
NewImageAssetVersion : AssetVersion = CreateNewAssetVersion( NewImageAssetObj, FileImageHash, ForceNewVersion = True, UploadedBy = AuthenticatedUser)
if NewImageAssetVersion is None:
db.session.delete(NewImageAssetObj)
db.session.commit()
flash("Failed to create a new asset version, please contact support", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/developer-products/{DeveloperProductObj.productid}")
TakeThumbnail( AssetId = NewImageAssetObj.id, isIcon=False )
DeveloperProductObj.iconimage_assetid = NewImageAssetObj.id
flash("Successfully updated developer product icon", "success")
DeveloperProductObj.name = productName
DeveloperProductObj.description = productDescription
DeveloperProductObj.is_for_sale = is_for_sale
DeveloperProductObj.robux_price = robux_price
DeveloperProductObj.updated_at = datetime.utcnow()
db.session.commit()
flash("Successfully updated developer product settings", "success")
return redirect(f"/develop/universes/{UniverseObj.id}/developer-products/{DeveloperProductObj.productid}")
@DevelopPagesRoute.route("/develop/universes/<int:universeid>/badges", methods=["GET"])
@auth.authenticated_required
def ManageUniverseBadgesPage( universeid : int ):
from app.pages.games.games import GetTotalBadgeAwardedCount, GetBadgeAwardedPastDay # Avoid circular import
AuthenticatedUser : User = auth.GetCurrentUser()
UniverseObj : Universe = Universe.query.filter_by(id=universeid).first()
isUserAllowedtoViewPage(AuthenticatedUser, UniverseObj, abortOnFail=True)
RootPlaceAssetObj : Asset = Asset.query.filter_by(id = UniverseObj.root_place_id).first()
Badges : list[PlaceBadge] = PlaceBadge.query.filter_by(universe_id = UniverseObj.id).all()
return render_template("develop/universes/badges.html", UniverseObj=UniverseObj, RootPlaceAssetObj=RootPlaceAssetObj, Badges=Badges, GetTotalBadgeAwardedCount=GetTotalBadgeAwardedCount, GetBadgeAwardedPastDay=GetBadgeAwardedPastDay)
@DevelopPagesRoute.route("/develop/universes/<int:universeid>/create-badge", methods=["GET", "POST"])
@auth.authenticated_required
def CreateUniverseBadgePage( universeid : int ):
AuthenticatedUser : User = auth.GetCurrentUser()
UniverseObj : Universe = Universe.query.filter_by(id=universeid).first()
isUserAllowedtoViewPage(AuthenticatedUser, UniverseObj, abortOnFail=True)
RootPlaceAssetObj : Asset = Asset.query.filter_by(id = UniverseObj.root_place_id).first()
if request.method == "GET":
return render_template("develop/universes/create-badge.html", UniverseObj=UniverseObj, RootPlaceAssetObj=RootPlaceAssetObj)
else:
BadgeName = request.form.get("name", default = "", type = str)
BadgeDescription = request.form.get("description", default = "", type = str)
file = request.files.get("icon-file", default=None)
if BadgeName == "":
flash("Badge name cannot be empty", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/create-badge")
if len(BadgeName) > 35:
flash("Badge name cannot be longer than 35 characters", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/create-badge")
if CountAlphanumericCharacters(BadgeName) < 3:
flash("Badge name must contain at least 3 alphanumeric characters", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/create-badge")
if BadgeDescription == "":
flash("Badge description cannot be empty", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/create-badge")
if len(BadgeDescription) > 128:
flash("Badge description cannot be longer than 128 characters", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/create-badge")
if CountAlphanumericCharacters(BadgeDescription) < 3:
flash("Badge description must contain at least 3 alphanumeric characters", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/create-badge")
BadgeName = FilterText(BadgeName)
BadgeDescription = FilterText(BadgeDescription)
TotalBadgeCount : int = PlaceBadge.query.filter_by(universe_id = UniverseObj.id).count()
if TotalBadgeCount >= 25:
flash("You cannot create more than 25 badges for a place", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/create-badge")
if file is None:
flash("No file was provided", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/create-badge")
if file.filename == "":
flash("No file was provided", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/create-badge")
if file.content_length > 1024 * 1024:
flash("File size is too big", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/create-badge")
FileImage = ValidateClothingImage(file, verifyResolution=False, validateFileSize=False, returnImage=True)
if FileImage is False or FileImage is None:
flash("Invalid image", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/create-badge")
if FileImage.width != FileImage.height:
flash("Image is not square", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/create-badge")
if FileImage.width < 128 or FileImage.width > 1024:
flash("Image is not between 128x128 and 1024x1024", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/create-badge")
file = BytesIO()
FileImage.save(file, format="PNG")
file.seek(0)
FileImageHash = hashlib.sha512(file.read()).hexdigest()
if not s3helper.DoesKeyExist(FileImageHash):
file.seek(0)
s3helper.UploadBytesToS3(file.read(), FileImageHash, contentType="image/png")
NewImageAssetObj : Asset = Asset(
name = "BadgeImage",
description = "Badge icon",
creator_id = AuthenticatedUser.id,
creator_type = 0,
asset_type = AssetType.Image,
created_at = datetime.utcnow(),
updated_at = datetime.utcnow(),
moderation_status = 1
)
db.session.add(NewImageAssetObj)
db.session.commit()
NewImageAssetVersion : AssetVersion = CreateNewAssetVersion( NewImageAssetObj, FileImageHash, ForceNewVersion = True, UploadedBy = AuthenticatedUser)
if NewImageAssetVersion is None:
db.session.delete(NewImageAssetObj)
db.session.commit()
flash("Failed to create a new asset version, please contact support", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/create-badge")
TakeThumbnail( AssetId = NewImageAssetObj.id, isIcon=False )
NewBadgeObj : PlaceBadge = PlaceBadge(
associated_place_id = UniverseObj.root_place_id,
name = BadgeName,
description = BadgeDescription,
icon_image_id = NewImageAssetObj.id,
universe_id = UniverseObj.id
)
db.session.add(NewBadgeObj)
db.session.commit()
flash("Successfully created badge", "success")
return redirect(f"/develop/universes/{UniverseObj.id}/badges")
@DevelopPagesRoute.route("/develop/universes/<int:universeid>/badges/<int:badgeid>", methods=["GET", "POST"])
@auth.authenticated_required
def ManageUniverseBadgePage( universeid : int, badgeid : int ):
AuthenticatedUser : User = auth.GetCurrentUser()
UniverseObj : Universe = Universe.query.filter_by(id=universeid).first()
isUserAllowedtoViewPage(AuthenticatedUser, UniverseObj, abortOnFail=True)
RootPlaceAssetObj : Asset = Asset.query.filter_by(id = UniverseObj.root_place_id).first()
BadgeObj : PlaceBadge = PlaceBadge.query.filter_by( universe_id = UniverseObj.id, id = badgeid ).first()
if BadgeObj is None:
abort(404)
if request.method == "GET":
return render_template("develop/universes/edit-badge.html", UniverseObj=UniverseObj, RootPlaceAssetObj=RootPlaceAssetObj, BadgeObj=BadgeObj)
else:
BadgeName = request.form.get("badge-name", default = "", type = str)
BadgeDescription = request.form.get("badge-description", default = "", type = str)
if BadgeName == "":
flash("Badge name cannot be empty", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/badges/{BadgeObj.id}")
if len(BadgeName) > 35:
flash("Badge name cannot be longer than 35 characters", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/badges/{BadgeObj.id}")
if CountAlphanumericCharacters(BadgeName) < 3:
flash("Badge name must contain at least 3 alphanumeric characters", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/badges/{BadgeObj.id}")
if BadgeDescription == "":
flash("Badge description cannot be empty", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/badges/{BadgeObj.id}")
if len(BadgeDescription) > 128:
flash("Badge description cannot be longer than 128 characters", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/badges/{BadgeObj.id}")
if CountAlphanumericCharacters(BadgeDescription) < 3:
flash("Badge description must contain at least 3 alphanumeric characters", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/badges/{BadgeObj.id}")
BadgeName = FilterText(BadgeName)
BadgeDescription = FilterText(BadgeDescription)
file = request.files.get("icon-file", default=None)
if file is not None:
if file.filename != "":
if file.content_length > 1024 * 1024:
flash("File size is too big", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/badges/{BadgeObj.id}")
FileImage = ValidateClothingImage(file, verifyResolution=False, validateFileSize=False, returnImage=True)
if FileImage is False or FileImage is None:
flash("Invalid image", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/badges/{BadgeObj.id}")
if FileImage.width != FileImage.height:
flash("Image is not square", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/badges/{BadgeObj.id}")
if FileImage.width < 128 or FileImage.width > 1024:
flash("Image is not between 128x128 and 1024x1024", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/badges/{BadgeObj.id}")
file = BytesIO()
FileImage.save(file, format="PNG")
file.seek(0)
FileImageHash = hashlib.sha512(file.read()).hexdigest()
if not s3helper.DoesKeyExist(FileImageHash):
file.seek(0)
s3helper.UploadBytesToS3(file.read(), FileImageHash, contentType="image/png")
NewImageAssetObj : Asset = Asset(
name = "BadgeImage",
description = "Badge icon",
creator_id = AuthenticatedUser.id,
creator_type = 0,
asset_type = AssetType.Image,
created_at = datetime.utcnow(),
updated_at = datetime.utcnow(),
moderation_status = 1
)
db.session.add(NewImageAssetObj)
db.session.commit()
NewImageAssetVersion : AssetVersion = CreateNewAssetVersion( NewImageAssetObj, FileImageHash, ForceNewVersion = True, UploadedBy = AuthenticatedUser)
if NewImageAssetVersion is None:
db.session.delete(NewImageAssetObj)
db.session.commit()
flash("Failed to create a new asset version, please contact support", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/badges/{BadgeObj.id}")
TakeThumbnail( AssetId = NewImageAssetObj.id, isIcon=False )
BadgeObj.icon_image_id = NewImageAssetObj.id
flash("Successfully updated developer product icon", "success")
BadgeObj.name = BadgeName
BadgeObj.description = BadgeDescription
BadgeObj.updated_at = datetime.utcnow()
db.session.commit()
flash("Successfully updated badge", "success")
return redirect(f"/develop/universes/{UniverseObj.id}/badges/{BadgeObj.id}")
@DevelopPagesRoute.route("/develop/universes/<int:universeid>/place/<int:placeid>/manage", methods=["GET", "POST"])
@auth.authenticated_required
def ManageUniversePlacePage( universeid : int, placeid : int ):
AuthenticatedUser : User = auth.GetCurrentUser()
UniverseObj : Universe = Universe.query.filter_by(id=universeid).first()
isUserAllowedtoViewPage(AuthenticatedUser, UniverseObj, abortOnFail=True)
RootPlaceAssetObj : Asset = Asset.query.filter_by(id = UniverseObj.root_place_id).first()
PlaceObj : Place = Place.query.filter_by(placeid = placeid, parent_universe_id = UniverseObj.id ).first()
if PlaceObj is None:
abort(404)
PlaceAssetObj : Asset = Asset.query.filter_by(id = PlaceObj.placeid).first() if placeid != UniverseObj.root_place_id else RootPlaceAssetObj
if request.method == "GET":
return render_template("develop/games/manage.html", UniverseObj=UniverseObj, RootPlaceAssetObj=RootPlaceAssetObj, PlaceObj=PlaceObj, PlaceAssetObj=PlaceAssetObj)
else:
PlaceName : str = request.form.get("name", default="", type=str)
PlaceDescription : str = request.form.get("description", default="", type=str)
try:
ChatStyleType : ChatStyle = ChatStyle(request.form.get("chat-style-type", default = 2, type = int))
except ValueError:
flash("Invalid chat style", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/manage")
if UniverseObj.place_year in [ PlaceYear.Eighteen, PlaceYear.Twenty, PlaceYear.TwentyOne ]:
try:
AvatarRigType : PlaceRigChoice = PlaceRigChoice(request.form.get("avatar-rig-type", default=0, type=int))
except ValueError:
flash("Invalid avatar rig type", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/manage")
if len(PlaceName) < 3 or len(PlaceName) > 50:
flash("Place name has to be between 3 to 50 characters long", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/manage")
if len(PlaceDescription) < 3 or len(PlaceDescription) > 700:
flash("Place description has to be between 3 to 700 characters long", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/manage")
if PlaceDescription.count("\n") > 10:
flash("Place description can only have 10 or less newlines", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/manage")
PlaceName : str = FilterText(PlaceName)
PlaceDescription : str = FilterText(PlaceDescription)
PlaceAssetObj.name = PlaceName
PlaceAssetObj.description = PlaceDescription
PlaceObj.chat_style = ChatStyleType
if UniverseObj.place_year in [ PlaceYear.Eighteen, PlaceYear.Twenty, PlaceYear.TwentyOne ]:
PlaceObj.rig_choice = AvatarRigType
PlaceAssetObj.updated_at = datetime.utcnow()
UniverseObj.updated_at = datetime.utcnow()
db.session.commit()
flash("Successfully updated place settings", "success")
return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/manage")
@DevelopPagesRoute.route("/develop/universes/<int:universeid>/place/<int:placeid>/access", methods=["GET", "POST"])
@auth.authenticated_required
def ManageUniversePlaceAccessPage( universeid : int, placeid : int ):
AuthenticatedUser : User = auth.GetCurrentUser()
UniverseObj : Universe = Universe.query.filter_by(id=universeid).first()
isUserAllowedtoViewPage(AuthenticatedUser, UniverseObj, abortOnFail=True)
RootPlaceAssetObj : Asset = Asset.query.filter_by(id = UniverseObj.root_place_id).first()
PlaceObj : Place = Place.query.filter_by(placeid = placeid, parent_universe_id = UniverseObj.id ).first()
if PlaceObj is None:
abort(404)
PlaceAssetObj : Asset = Asset.query.filter_by(id = PlaceObj.placeid).first() if placeid != UniverseObj.root_place_id else RootPlaceAssetObj
if request.method == "GET":
return render_template("develop/games/access.html", UniverseObj=UniverseObj, RootPlaceAssetObj=RootPlaceAssetObj, PlaceObj=PlaceObj, PlaceAssetObj=PlaceAssetObj)
else:
MaxPlayers : int = request.form.get("maxplayers", default=10, type=int)
if MaxPlayers < 2 or MaxPlayers > 50:
flash("Max players has to be between 2 to 50", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/access")
PlaceObj.maxplayers = MaxPlayers
db.session.commit()
flash("Successfully updated place access settings", "success")
return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/access")
@DevelopPagesRoute.route("/develop/universes/<int:universeid>/place/<int:placeid>/upload-version", methods=["GET", "POST"])
@auth.authenticated_required
def UploadUniversePlaceVersionPage( universeid : int, placeid : int ):
AuthenticatedUser : User = auth.GetCurrentUser()
UniverseObj : Universe = Universe.query.filter_by(id=universeid).first()
isUserAllowedtoViewPage(AuthenticatedUser, UniverseObj, abortOnFail=True)
RootPlaceAssetObj : Asset = Asset.query.filter_by(id = UniverseObj.root_place_id).first()
PlaceObj : Place = Place.query.filter_by(placeid = placeid, parent_universe_id = UniverseObj.id ).first()
if PlaceObj is None:
abort(404)
PlaceAssetObj : Asset = Asset.query.filter_by(id = PlaceObj.placeid).first() if placeid != UniverseObj.root_place_id else RootPlaceAssetObj
if request.method == "GET":
return render_template("develop/games/upload-version.html", UniverseObj=UniverseObj, RootPlaceAssetObj=RootPlaceAssetObj, PlaceObj=PlaceObj, PlaceAssetObj=PlaceAssetObj)
else:
with limiter.limit("5/minute"):
AssetFile = request.files.get("file", default = None)
if AssetFile is None:
flash("No file was provided", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/upload-version")
CreateLockName = f"UploadAssetVersion:{str(PlaceAssetObj.id)}"
CreateLock = redislock.acquire_lock(CreateLockName, acquire_timeout=20, lock_timeout=5)
AssetFile.seek(0)
AssetFileContent = AssetFile.read()
AssetFileHash = hashlib.sha512(AssetFileContent).hexdigest()
CurrentAssetVersion : AssetVersion = GetLatestAssetVersion( PlaceAssetObj )
if CurrentAssetVersion is not None:
if CurrentAssetVersion.content_hash == AssetFileHash:
redislock.release_lock(CreateLockName, CreateLock)
flash("File is the same as the current version", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/upload-version")
PlaceObj : Place = Place.query.filter_by(placeid=PlaceAssetObj.id).first()
isValidPlaceFile = ValidatePlaceFile( AssetFile, keepFileWhenInvalid=False, TestPlaceYear = PlaceObj.placeyear ) # if it returns a bool its valid, if it returns a string its invalid and the string is the error
if type(isValidPlaceFile) == str:
redislock.release_lock(CreateLockName, CreateLock)
flash(f"Validation Failed: {isValidPlaceFile}", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/upload-version")
if not s3helper.DoesKeyExist(AssetFileHash):
s3helper.UploadBytesToS3(AssetFileContent, AssetFileHash)
NewAssetVersion : AssetVersion = CreateNewAssetVersion( PlaceAssetObj, AssetFileHash, ForceNewVersion = True, UploadedBy = AuthenticatedUser)
if NewAssetVersion is None:
redislock.release_lock(CreateLockName, CreateLock)
flash("Failed to create a new asset version", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/upload-version")
PlaceAssetObj.updated_at = datetime.utcnow()
UniverseObj.updated_at = datetime.utcnow()
db.session.commit()
redislock.release_lock(CreateLockName, CreateLock)
flash("Successfully updated place file", "success")
return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/upload-version")
@DevelopPagesRoute.route("/develop/universes/<int:universeid>/place/<int:placeid>/placeicon", methods=["GET","POST"])
@auth.authenticated_required
def ManageUniversePlaceIconPage( universeid : int, placeid : int ):
AuthenticatedUser : User = auth.GetCurrentUser()
UniverseObj : Universe = Universe.query.filter_by(id=universeid).first()
isUserAllowedtoViewPage(AuthenticatedUser, UniverseObj, abortOnFail=True)
RootPlaceAssetObj : Asset = Asset.query.filter_by(id = UniverseObj.root_place_id).first()
PlaceObj : Place = Place.query.filter_by(placeid = placeid, parent_universe_id = UniverseObj.id ).first()
if PlaceObj is None:
abort(404)
PlaceAssetObj : Asset = Asset.query.filter_by(id = PlaceObj.placeid).first() if placeid != UniverseObj.root_place_id else RootPlaceAssetObj
if request.method == "GET":
return render_template("develop/games/upload-icon.html", UniverseObj=UniverseObj, RootPlaceAssetObj=RootPlaceAssetObj, PlaceObj=PlaceObj, PlaceAssetObj=PlaceAssetObj, RandomNumber=random.randint(0, 10000000))
else:
with limiter.limit("5/minute"):
useGenerated = request.form.get("usegenerated") == "on"
if useGenerated:
PlaceAssetObj.updated_at = datetime.utcnow()
CurrentPlaceIcon : PlaceIcon = PlaceIcon.query.filter_by(placeid=PlaceAssetObj.id).first()
if CurrentPlaceIcon is not None:
db.session.delete(CurrentPlaceIcon)
db.session.commit()
TakeThumbnail( AssetId=PlaceAssetObj.id, isIcon=True )
return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/placeicon")
AssetFile = request.files.get("file", default = None)
if AssetFile is None:
flash("No file was provided", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/placeicon")
if AssetFile.content_length > 2097152:
flash("File size is too big", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/placeicon")
IconImage = ValidateClothingImage( AssetFile, verifyResolution=False, validateFileSize=False, returnImage=True )
if IconImage is False or IconImage is None:
flash("Invalid image", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/placeicon")
if IconImage.width != IconImage.height:
flash("Image is not square", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/placeicon")
if IconImage.width < 256 or IconImage.width > 1024:
flash("Image is not between 256x256 and 1024x1024", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/placeicon")
AssetFile = BytesIO()
IconImage.save(AssetFile, format="PNG")
AssetFile.seek(0)
IconImageHash = hashlib.sha512(AssetFile.read()).hexdigest()
if not s3helper.DoesKeyExist(IconImageHash):
AssetFile.seek(0)
s3helper.UploadBytesToS3(AssetFile.read(), IconImageHash, contentType="image/png")
CurrentPlaceIcon : PlaceIcon = PlaceIcon.query.filter_by(placeid=PlaceAssetObj.id).first()
if CurrentPlaceIcon is None:
CurrentPlaceIcon = PlaceIcon(placeid=PlaceAssetObj.id, contenthash=IconImageHash, updated_at=datetime.utcnow(), moderation_status=1)
db.session.add(CurrentPlaceIcon)
else:
CurrentPlaceIcon.contenthash = IconImageHash
CurrentPlaceIcon.updated_at = datetime.utcnow()
CurrentPlaceIcon.moderation_status = 1
PlaceAssetObj.updated_at = datetime.utcnow()
UniverseObj.updated_at = datetime.utcnow()
db.session.commit()
flash("Successfully updated place icon", "success")
return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/placeicon")
@DevelopPagesRoute.route("/develop/universes/<int:universeid>/place/<int:placeid>/thumbnails", methods=["GET","POST"])
@auth.authenticated_required
def ManageUniversePlaceThumbnailsPage( universeid : int, placeid : int ):
AuthenticatedUser : User = auth.GetCurrentUser()
UniverseObj : Universe = Universe.query.filter_by(id=universeid).first()
isUserAllowedtoViewPage(AuthenticatedUser, UniverseObj, abortOnFail=True)
RootPlaceAssetObj : Asset = Asset.query.filter_by(id = UniverseObj.root_place_id).first()
PlaceObj : Place = Place.query.filter_by(placeid = placeid, parent_universe_id = UniverseObj.id ).first()
if PlaceObj is None:
abort(404)
PlaceAssetObj : Asset = Asset.query.filter_by(id = PlaceObj.placeid).first() if placeid != UniverseObj.root_place_id else RootPlaceAssetObj
if request.method == "GET":
return render_template("develop/games/upload-thumbnail.html", UniverseObj=UniverseObj, RootPlaceAssetObj=RootPlaceAssetObj, PlaceObj=PlaceObj, PlaceAssetObj=PlaceAssetObj, RandomNumber=random.randint(0, 10000000))
else:
with limiter.limit("5/minute"):
useGenerated = request.form.get("usegenerated") == "on"
if useGenerated:
PlaceAssetObj.updated_at = datetime.utcnow()
CurrentAssetThumbnail : AssetThumbnail = AssetThumbnail.query.filter_by(asset_id=PlaceAssetObj.id).order_by(AssetThumbnail.asset_version_id.desc()).first()
if CurrentAssetThumbnail is not None:
db.session.delete(CurrentAssetThumbnail)
db.session.commit()
TakeThumbnail( AssetId=PlaceAssetObj.id, isIcon=False )
return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/thumbnails")
AssetFile = request.files.get("file", default = None)
if AssetFile is None:
flash("No file was provided", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/thumbnails")
if AssetFile.content_length > 2097152:
flash("File size is too big", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/thumbnails")
ThumbnailImage = ValidateClothingImage( AssetFile, verifyResolution=False, validateFileSize=False, returnImage=True )
if ThumbnailImage is False or ThumbnailImage is None:
flash("Invalid image", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/thumbnails")
if ThumbnailImage.width / ThumbnailImage.height != 16 / 9:
flash("Image is not 16:9 aspect ratio", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/thumbnails")
if ThumbnailImage.width < 640 or ThumbnailImage.width > 1920:
flash("Image is not between 640x360 and 1920x1080", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/thumbnails")
if ThumbnailImage.height < 360 or ThumbnailImage.height > 1080:
flash("Image is not between 640x360 and 1920x1080", "error")
return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/thumbnails")
AssetFile = BytesIO()
ThumbnailImage.save(AssetFile, format="PNG")
AssetFile.seek(0)
ThumbnailImageHash = hashlib.sha512(AssetFile.read()).hexdigest()
if not s3helper.DoesKeyExist(ThumbnailImageHash):
AssetFile.seek(0)
s3helper.UploadBytesToS3(AssetFile.read(), ThumbnailImageHash, contentType="image/png")
CurrentAssetThumbnail : AssetThumbnail = AssetThumbnail.query.filter_by(asset_id=PlaceAssetObj.id).order_by(AssetThumbnail.asset_version_id.desc()).first()
LatestAssetVersion : AssetVersion = GetLatestAssetVersion(PlaceAssetObj)
if LatestAssetVersion is None:
flash("No asset version found", "error")
return redirect(f"/develop/{str(PlaceAssetObj.id)}/thumbnails")
if CurrentAssetThumbnail is None:
CurrentAssetThumbnail = AssetThumbnail(asset_id=PlaceAssetObj.id, asset_version_id=LatestAssetVersion.version, content_hash=ThumbnailImageHash, created_at=datetime.utcnow(), moderation_status=1)
db.session.add(CurrentAssetThumbnail)
else:
CurrentAssetThumbnail.content_hash = ThumbnailImageHash
CurrentAssetThumbnail.created_at = datetime.utcnow()
CurrentAssetThumbnail.moderation_status = 1
CurrentAssetThumbnail.asset_version_id = LatestAssetVersion.version
PlaceAssetObj.updated_at = datetime.utcnow()
UniverseObj.updated_at = datetime.utcnow()
db.session.commit()
return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/thumbnails")
@DevelopPagesRoute.route("/develop/universes/<int:universeid>/place/<int:placeid>/version-history", methods=["GET"])
@auth.authenticated_required
def ManageUniversePlaceVersionHistoryPage( universeid : int, placeid : int ):
AuthenticatedUser : User = auth.GetCurrentUser()
UniverseObj : Universe = Universe.query.filter_by(id=universeid).first()
isUserAllowedtoViewPage(AuthenticatedUser, UniverseObj, abortOnFail=True)
RootPlaceAssetObj : Asset = Asset.query.filter_by(id = UniverseObj.root_place_id).first()
PlaceObj : Place = Place.query.filter_by(placeid = placeid, parent_universe_id = UniverseObj.id ).first()
if PlaceObj is None:
abort(404)
PlaceAssetObj : Asset = Asset.query.filter_by(id = PlaceObj.placeid).first() if placeid != UniverseObj.root_place_id else RootPlaceAssetObj
PageNumber : int = max( 1, request.args.get("page", default = 1, type = int) )
AssetVersions : list[AssetVersion] = AssetVersion.query.filter_by(asset_id=PlaceAssetObj.id).order_by(AssetVersion.version.desc()).paginate( page = PageNumber, per_page = 10, error_out = False )
return render_template(
"/develop/games/version-history.html",
UniverseObj = UniverseObj,
PlaceAssetObj = PlaceAssetObj,
AssetVersions = AssetVersions,
CDN_URL = config.CDN_URL
)
@DevelopPagesRoute.route("/develop/<int:assetid>/edit", methods=["GET"])
@auth.authenticated_required
def EditItemPage( assetid : int ):
AuthenticatedUser = auth.GetCurrentUser()
AssetObj : Asset = Asset.query.filter_by(id=assetid).first()
if AssetObj is None:
abort(404)
if AssetObj.asset_type not in [AssetType.Shirt, AssetType.TShirt, AssetType.Pants, AssetType.Audio, AssetType.Image]:
abort(404)
isUserAllowedtoViewPage(AuthenticatedUser, AssetObj, abortOnFail=True, isGameContext=False)
return render_template("develop/edit.html", AssetObj=AssetObj)
@DevelopPagesRoute.route("/develop/<int:assetid>/edit", methods=["POST"])
@auth.authenticated_required
@limiter.limit("5/minute")
def EditItem( assetid : int ):
AuthenticatedUser = auth.GetCurrentUser()
AssetObj : Asset = Asset.query.filter_by(id=assetid).first()
if AssetObj is None:
abort(404)
if AssetObj.asset_type not in [AssetType.Shirt, AssetType.TShirt, AssetType.Pants, AssetType.Audio, AssetType.Image]:
abort(404)
isUserAllowedtoViewPage(AuthenticatedUser, AssetObj, abortOnFail=True, isGameContext=False)
if not websiteFeatures.GetWebsiteFeature("AssetEditing"):
flash("Asset editing is currently disabled", "error")
return redirect(f"/develop/{str(AssetObj.id)}/edit")
AssetName = request.form.get("item-name", default = "", type = str)
AssetDescription = request.form.get("item-description", default = "", type = str)
isForSale = request.form.get("is-for-sale", default = "off") == "on"
AssetRobuxPrice = request.form.get("robux-cost", default = 0, type = int)
AssetTixPrice = request.form.get("tix-cost", default = 0, type = int)
if AssetName == "":
flash("Item name cannot be empty", "error")
return redirect(f"/develop/{str(AssetObj.id)}/edit")
if len(AssetName) > 35:
flash("Item name cannot be longer than 35 characters", "error")
return redirect(f"/develop/{str(AssetObj.id)}/edit")
if len(AssetDescription) > 200:
flash("Item description cannot be longer than 200 characters", "error")
return redirect(f"/develop/{str(AssetObj.id)}/edit")
if AssetRobuxPrice < 0 or AssetRobuxPrice > 1000000:
flash("Robux price must be between 0 and 1,000,000", "error")
return redirect(f"/develop/{str(AssetObj.id)}/edit")
if AssetTixPrice < 0 or AssetTixPrice > 10000000:
flash("Tix price must be between 0 and 10,000,000", "error")
return redirect(f"/develop/{str(AssetObj.id)}/edit")
if isForSale and AssetObj.moderation_status != 0:
flash("You cannot sell an item that is not approved yet", "error")
return redirect(f"/develop/{str(AssetObj.id)}/edit")
if isForSale and (AssetObj.asset_type == AssetType.Audio or AssetObj.asset_type == AssetType.Image):
flash("You cannot sell this type of asset", "error")
return redirect(f"/develop/{str(AssetObj.id)}/edit")
UserCurrentMembership : MembershipType = GetUserMembership(AuthenticatedUser)
if UserCurrentMembership == MembershipType.NonBuildersClub and isForSale:
flash("You must be Builders Club to sell items", "error")
return redirect(f"/develop/{str(AssetObj.id)}/edit")
AssetName = FilterText(AssetName)
AssetDescription = FilterText(AssetDescription)
AssetObj.name = AssetName
AssetObj.description = AssetDescription
AssetObj.is_for_sale = isForSale
AssetObj.price_robux = AssetRobuxPrice
AssetObj.price_tix = AssetTixPrice
AssetObj.updated_at = datetime.utcnow()
db.session.commit()
return redirect(f"/develop/{str(AssetObj.id)}/edit")
def ShutdownServer(JobId):
from app.routes.jobreporthandler import HandleUserTimePlayed
from app.services.gameserver_comm import perform_post
TargetPlaceServer : PlaceServer | None = PlaceServer.query.filter_by(serveruuid=JobId).first()
if TargetPlaceServer is None:
return
MasterServer : GameServer | None = GameServer.query.filter_by(serverId=TargetPlaceServer.originServerId).first()
if MasterServer is None:
return
PlaceObj : Place = Place.query.filter_by(placeid=TargetPlaceServer.serverPlaceId).first()
UniverseObj : Universe = Universe.query.filter_by(id=PlaceObj.parent_universe_id).first()
logging.info(f"CloseJob : ShutdownServer func : Closing job {str(JobId)} for place {str(PlaceObj.placeid)}")
try:
CloseJobRequest = perform_post(
TargetGameserver = MasterServer,
Endpoint = "CloseJob",
JSONData = {
"jobid": str(JobId)
}
)
PlaceServerPlayersList : list[PlaceServerPlayer] = PlaceServerPlayer.query.filter_by(serveruuid=JobId).all()
for player in PlaceServerPlayersList:
TotalTimePlayed = (datetime.utcnow() - player.joinTime).total_seconds()
HandleUserTimePlayed(player.user, TotalTimePlayed, serverUUID=str(JobId), placeId=PlaceObj.placeid)
db.session.delete(player)
db.session.delete(TargetPlaceServer)
db.session.commit()
ClearPlayingCountCache( PlaceObj = PlaceObj )
ClearUniversePlayingCountCache( UniverseObj = UniverseObj )
except Exception as e:
logging.error(f"CloseJob : ShutdownServer func : Failed to close job {str(JobId)} for place {str(PlaceObj.placeid)} : {str(e)}")
return