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/", 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//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//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//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//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//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//gamepass/", 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//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//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//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//developer-products/", 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//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//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//badges/", 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//place//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//place//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//place//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//place//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//place//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//place//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//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//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