syntaxwebsite/app/pages/catalog/catalog.py

825 lines
40 KiB
Python

from flask import Blueprint, render_template, request, redirect, url_for, flash, make_response, jsonify, abort
from app.util import auth, turnstile, websiteFeatures
from app.routes.thumbnailer import TakeUserThumbnail
from app.util.redislock import acquire_lock, release_lock
from app.extensions import db, csrf, limiter, redis_controller, user_limiter
from app.models.asset import Asset
from app.models.usereconomy import UserEconomy
from app.models.userassets import UserAsset
from app.models.user_avatar_asset import UserAvatarAsset
from app.models.user import User
from app.models.package_asset import PackageAsset
from app.models.gamepass_link import GamepassLink
from app.models.asset_rap import AssetRap
from app.models.groups import Group
from app.models.place_badge import PlaceBadge, UserBadge
from app.models.universe import Universe
from app.models.limited_item_transfers import LimitedItemTransfer
from app.routes.badgesapi import CalculateBadgeRarity, GetBadgeAwardedPastDay
from app.pages.catalog.catalogtypes import CatalogTypes
from app.pages.messages.messages import CreateSystemMessage
from slugify import slugify
from datetime import datetime
from sqlalchemy import text, and_, or_
import math
from app.enums.AssetType import AssetType
from app.util.transactions import CreateTransactionForSale
import logging
import calendar
import time
import redis_lock
from app.util.membership import GetUserMembership
from app.enums.MembershipType import MembershipType
from app.enums.LimitedItemTransferMethod import LimitedItemTransferMethod
from app.services.economy import IncrementTargetBalance, AdjustAssetRap, GetAssetRap, TaxCurrencyAmount, GetUserBalance, GetGroupBalance, DecrementTargetBalance,EconomyLockAcquireException, InsufficientFundsException
from app.services.groups import GetUserFromId, GetGroupFromId
CatalogRoute = Blueprint('catalog', __name__, template_folder="pages", url_prefix="/catalog")
LibraryRoute = Blueprint('library', __name__, template_folder="pages", url_prefix="/library")
BadgesPageRoute = Blueprint('badges_page', __name__, template_folder="pages", url_prefix="/badges")
def IncrementAssetCreator( AssetObject : Asset, AmountGiven : int, CurrencyType : int):
""" Increments the robux of the asset creator"""
CurrencyToGive = math.floor(AmountGiven * 0.7)
if CurrencyToGive <= 0:
return True
if AssetObject.creator_type == 0:
# User
IncrementTargetBalance(GetUserFromId(AssetObject.creator_id), CurrencyToGive, CurrencyType)
return True
elif AssetObject.creator_type == 1:
# Group
IncrementTargetBalance(GetGroupFromId(AssetObject.creator_id), CurrencyToGive, CurrencyType)
return True
def ConvertQueryToAsset( queryResult ):
"""Converts a query result to a dict with the same keys as the Asset model"""
return {
"id": queryResult[0],
"roblox_asset_id": queryResult[1],
"name": queryResult[2],
"description": queryResult[3],
"created_at": queryResult[4],
"updated_at": queryResult[5],
"asset_type": queryResult[6],
"asset_genre": queryResult[7],
"creator_type": queryResult[8],
"creator_id": queryResult[9],
"moderation_status": queryResult[10],
"is_for_sale": queryResult[11],
"price_robux": queryResult[12],
"price_tix": queryResult[13],
"is_limited": queryResult[14],
"is_limited_unique": queryResult[15],
"serial_count": queryResult[16],
"sale_count": queryResult[17],
"offsale_at": queryResult[18]
}
OrderTypes = {
0: "created_at DESC", # Default
1: "price_robux ASC", # Lowest to Highest
2: "price_robux DESC", # Highest to Lowest
3: "updated_at DESC", # Recently Updated
4: "sale_count DESC" # Best Selling
}
import urllib.parse
@CatalogRoute.route("/", methods=["POST"])
@auth.authenticated_required
@csrf.exempt
def catalog_search():
CategoryType = int(request.args.get(key="category", default=0, type=int))
OrderBy = request.form.get(key="order-by", default=0, type=int)
if OrderBy not in OrderTypes:
OrderBy = 0
SearchQuery = request.form.get(key="search-input", default="", type=str)
if len(SearchQuery) < 3:
SearchQuery = ""
if SearchQuery == "":
return redirect(f"/catalog/?sort={OrderBy}&category={str(CategoryType)}")
# Make sure the query is url safe
SearchQuery = urllib.parse.quote(SearchQuery)
return redirect(f"/catalog/?q={SearchQuery}&sort={OrderBy}&category={str(CategoryType)}")
@CatalogRoute.route("/", methods=["GET"])
@auth.authenticated_required
def catalog_page():
CatalogType = int(request.args.get("category", 0))
if CatalogType not in CatalogTypes:
return redirect("/catalog/")
if request.args.get("page") is None:
Page = 1
else:
try:
Page = int(request.args.get("page"))
except:
Page = 1
SearchInput = request.args.get("q", "", type=str)
"""
Types of sorting:
0 - Relevance (Default) [ Just sort by created_at ]
1 - Price (Lowest to Highest)
2 - Price (Highest to Lowest)
3 - Recently Updated
4 - Best Selling
"""
if request.args.get("sort") is None:
SortType = 0
else:
try:
SortType = int(request.args.get("sort"))
except:
SortType = 0
if SortType > 4 or SortType < 0:
SortType = 0
SearchQuery = Asset.query
CatalogTypesDict = {
0: lambda queryObj: queryObj.filter(Asset.asset_type.in_((
AssetType.Hat,
AssetType.TShirt,
AssetType.Shirt,
AssetType.Pants,
AssetType.Gear,
AssetType.Face,
AssetType.Head,
AssetType.HairAccessory,
AssetType.FaceAccessory,
AssetType.NeckAccessory,
AssetType.ShoulderAccessory,
AssetType.FrontAccessory,
AssetType.BackAccessory,
AssetType.WaistAccessory,
AssetType.Package
))).filter(and_(Asset.creator_id == 1, Asset.creator_type == 0)).filter(or_(
Asset.is_for_sale == True, Asset.is_limited == True
)),
1: lambda queryObj: queryObj.filter(Asset.asset_type.in_((
AssetType.Hat,
AssetType.HairAccessory,
AssetType.FaceAccessory,
AssetType.NeckAccessory,
AssetType.ShoulderAccessory,
AssetType.FrontAccessory,
AssetType.BackAccessory,
AssetType.WaistAccessory,
))).filter(and_(Asset.creator_id == 1, Asset.creator_type == 0)).filter(or_(
Asset.is_for_sale == True, Asset.is_limited == True
)),
2: lambda queryObj: queryObj.filter(Asset.asset_type == AssetType.Gear).filter(and_(Asset.creator_id == 1, Asset.creator_type == 0, Asset.is_for_sale == True)),
3: lambda queryObj: queryObj.filter(Asset.asset_type == AssetType.Face).filter(and_(Asset.creator_id == 1, Asset.creator_type == 0, Asset.is_for_sale == True)),
4: lambda queryObj: queryObj.filter_by(is_limited = True),
5: lambda queryObj: queryObj.filter(Asset.asset_type.in_((
AssetType.Hat,
AssetType.HairAccessory,
AssetType.FaceAccessory,
AssetType.NeckAccessory,
AssetType.ShoulderAccessory,
AssetType.FrontAccessory,
AssetType.BackAccessory,
AssetType.WaistAccessory,
))).filter_by(is_limited = True),
6: lambda queryObj: queryObj.filter(Asset.asset_type == AssetType.Gear).filter_by(is_limited = True),
7: lambda queryObj: queryObj.filter(Asset.asset_type == AssetType.Face).filter_by(is_limited = True),
8: lambda queryObj: queryObj.filter(Asset.asset_type.in_((
AssetType.Hat,
AssetType.TShirt,
AssetType.Shirt,
AssetType.Pants,
AssetType.Package,
))).filter_by(is_for_sale = True),
9: lambda queryObj: queryObj.filter(Asset.asset_type.in_((
AssetType.Hat,
AssetType.HairAccessory,
AssetType.FaceAccessory,
AssetType.NeckAccessory,
AssetType.ShoulderAccessory,
AssetType.FrontAccessory,
AssetType.BackAccessory,
AssetType.WaistAccessory,
))).filter_by(is_for_sale = True),
10: lambda queryObj: queryObj.filter_by(asset_type = AssetType.Shirt).filter_by(is_for_sale = True),
11: lambda queryObj: queryObj.filter(Asset.asset_type == AssetType.TShirt).filter_by(is_for_sale = True),
12: lambda queryObj: queryObj.filter(Asset.asset_type == AssetType.Pants).filter_by(is_for_sale = True),
13: lambda queryObj: queryObj.filter(Asset.asset_type == AssetType.Package).filter_by(is_for_sale = True),
14: lambda queryObj: queryObj.filter(Asset.asset_type.in_((
AssetType.Head,
AssetType.Face,
AssetType.Package
))).filter_by(is_for_sale = True),
15: lambda queryObj: queryObj.filter(Asset.asset_type == AssetType.Head).filter_by(is_for_sale = True),
16: lambda queryObj: queryObj.filter(Asset.asset_type == AssetType.Face).filter_by(is_for_sale = True),
41: lambda queryObj: queryObj.filter(Asset.asset_type == AssetType.HairAccessory).filter_by(is_for_sale = True),
}
if CatalogType > 41 or CatalogType < 0:
CatalogType = 0
SearchQuery = CatalogTypesDict[CatalogType](SearchQuery)
if SearchInput != "" and len(SearchInput) > 3:
SearchQuery = SearchQuery.filter( Asset.name.ilike(f"%{SearchInput}%") )
else:
SearchInput = ""
if SortType == 0:
SearchQuery = SearchQuery.order_by(Asset.created_at.desc())
elif SortType == 1:
SearchQuery = SearchQuery.order_by(Asset.price_robux.asc())
elif SortType == 2:
SearchQuery = SearchQuery.order_by(Asset.price_robux.desc())
elif SortType == 3:
SearchQuery = SearchQuery.order_by(Asset.updated_at.desc())
elif SortType == 4:
SearchQuery = SearchQuery.order_by(Asset.sale_count.desc())
else:
SearchQuery = SearchQuery.order_by(Asset.created_at.desc())
SearchQuery = SearchQuery.paginate(
page=Page,
per_page=24,
error_out=False
)
for AssetObj in SearchQuery.items:
if AssetObj.is_limited and not AssetObj.is_for_sale:
BestPriceResult : UserAsset = UserAsset.query.filter_by(assetid=AssetObj.id, is_for_sale=True).order_by(UserAsset.price.asc()).first()
if BestPriceResult is not None:
AssetObj.best_price = str(BestPriceResult.price)
else:
AssetObj.best_price = "--"
if Page == 1:
PreviousPage = -1
else:
PreviousPage = Page -1
if SearchQuery.has_next:
NextPage = Page + 1
else:
NextPage = -1
return render_template("catalog/index.html",
categoryname=CatalogTypes[CatalogType]["name"],
queryResults = SearchQuery.items,
categoryid=CatalogType,
PreviousPage=PreviousPage,
NextPage=NextPage,
PageNumber=Page,
CatalogType=CatalogType,
SortType=SortType,
query=SearchInput,
totalPages = SearchQuery.pages,
totalResults = SearchQuery.total)
@CatalogRoute.route("/<int:assetid>/", methods=["GET"])
@CatalogRoute.route("/<int:assetid>/<assetname>", methods=["GET"])
@auth.authenticated_required
def asset_page(assetid, assetname=None):
AssetObject : Asset = Asset.query.filter_by(id=assetid).first()
if AssetObject is None:
return redirect("/catalog")
SlugName = slugify(AssetObject.name, lowercase=False)
if SlugName is None or SlugName == "":
SlugName = "unnamed"
if assetname is None:
if request.args.get("page") is None:
return redirect(f"/catalog/{assetid}/{SlugName}")
else:
return redirect(f"/catalog/{assetid}/{SlugName}?page={request.args.get('page')}")
if assetname != SlugName:
if request.args.get("page") is None:
return redirect(f"/catalog/{assetid}/{SlugName}")
else:
return redirect(f"/catalog/{assetid}/{SlugName}?page={request.args.get('page')}")
if AssetObject.asset_type.value not in [2,8,11,12,17,18,19,27,28,29,30,31,32,41,42,43,44,45,46,47,57,58]:
if AssetObject.asset_type.value == 9:
return redirect(f"/games/{assetid}/")
if AssetObject.asset_type.value in [1,3,4,5,10,13,24,34,38,40]:
return redirect(f"/library/{assetid}/")
return redirect("/catalog/")
CreatorObj : User | Group = User.query.filter_by(id=AssetObject.creator_id).first() if AssetObject.creator_type == 0 else Group.query.filter_by(id=AssetObject.creator_id).first()
Created = AssetObject.created_at.strftime("%d/%m/%Y")
Updated = AssetObject.updated_at.strftime("%d/%m/%Y")
AuthenticatedUser : User = auth.GetCurrentUser()
doesUserOwnAsset = UserAsset.query.filter_by(userid=AuthenticatedUser.id, assetid=assetid).first() is not None
BestPriceResult : UserAsset = UserAsset.query.filter_by(assetid=assetid, is_for_sale=True).order_by(UserAsset.price.asc()).first()
BestPrice = "None"
if BestPriceResult is not None:
BestPrice = str(BestPriceResult.price)
UserOwns = 0
PrivateSaleList = []
NextPage = 0
PreviousPage = 0
PageNumber = 0
AssetRap = 0
if AssetObject.is_limited and not AssetObject.is_for_sale:
AssetRap = GetAssetRap(assetid)
UserOwns = UserAsset.query.filter_by(userid=AuthenticatedUser.id, assetid=assetid).count()
Page = 1
if request.args.get("page"):
try:
Page = int(request.args.get("page"))
except:
pass
PrivateSales = UserAsset.query.filter_by(assetid=assetid, is_for_sale=True).order_by(UserAsset.price.asc()).paginate(page=Page, per_page=5)
for sale in PrivateSales.items:
SellerUser : User = User.query.filter_by(id=sale.userid).first()
PrivateSaleList.append({
"price": sale.price,
"seller": SellerUser.username,
"sellerid": SellerUser.id,
"serial": sale.serial,
"uaid": sale.id
})
if PrivateSales.has_next:
NextPage = Page + 1
else:
NextPage = -1
if Page > 1:
PreviousPage = Page - 1
else:
PreviousPage = -1
PageNumber = Page
OffsaleAt = None
if AssetObject.offsale_at is not None:
if datetime.utcnow() > AssetObject.offsale_at:
AssetObject.is_for_sale = False
AssetObject.offsale_at = None
db.session.commit()
else:
OffsaleAt = int(calendar.timegm(AssetObject.offsale_at.timetuple()))
return render_template("catalog/asset.html", asset=AssetObject, creator=CreatorObj, createddate=Created,
updateddate=Updated, doesUserOwnAsset=doesUserOwnAsset, BestPrice=BestPrice,
BestPriceResult=BestPriceResult, userOwnAmountCount=UserOwns, PrivateSales=PrivateSaleList,
NextPage=NextPage, PreviousPage=PreviousPage, PageNumber=PageNumber, AssetRap=AssetRap,
OffsaleAt=OffsaleAt)
@CatalogRoute.route("/resell/<int:assetid>", methods=["GET"])
@auth.authenticated_required
def resell_page(assetid):
AssetObject : Asset = Asset.query.filter_by(id=assetid).first()
if AssetObject is None:
return redirect("/catalog")
if not AssetObject.is_limited:
return redirect(f"/catalog/{str(AssetObject.id)}/")
if AssetObject.is_for_sale:
return redirect(f"/catalog/{str(AssetObject.id)}/") # Users cant resell items when it is still for sale
AuthenticatedUser : User = auth.GetCurrentUser()
UserOwns = UserAsset.query.filter_by(userid=AuthenticatedUser.id, assetid=assetid).count()
if UserOwns == 0:
return redirect(f"/catalog/{str(AssetObject.id)}/")
AllUserAssets = UserAsset.query.filter_by(userid=AuthenticatedUser.id, assetid=assetid).all()
UserAssetCurrentlyForSale = UserAsset.query.filter_by(userid=AuthenticatedUser.id, assetid=assetid, is_for_sale=True).count()
UserAssetNotForSale = UserAsset.query.filter_by(userid=AuthenticatedUser.id, assetid=assetid, is_for_sale=False).count()
return render_template("catalog/resell.html", asset=AssetObject, userassets=AllUserAssets, userassetforsalecount=UserAssetCurrentlyForSale, userassetnotforsalecount=UserAssetNotForSale, isOTPRequired = AuthenticatedUser.TOTPEnabled)
@CatalogRoute.route("/resell/<int:assetid>", methods=["POST"])
@auth.authenticated_required
@user_limiter.limit("1/second")
def resell_page_post(assetid):
AssetObject : Asset = Asset.query.filter_by(id=assetid).first()
if AssetObject is None:
return redirect("/catalog")
if not AssetObject.is_limited:
return redirect(f"/catalog/{str(AssetObject.id)}/")
if AssetObject.is_for_sale:
return redirect(f"/catalog/{str(AssetObject.id)}/")
AuthenticatedUser : User = auth.GetCurrentUser()
if "uaid" not in request.form or "itemprice" not in request.form:
flash("Invalid request", "error")
return redirect(f"/catalog/resell/{str(AssetObject.id)}")
try:
ItemPrice = int(request.form["itemprice"])
UAID = int(request.form["uaid"])
except:
flash("Invalid request", "error")
return redirect(f"/catalog/resell/{str(AssetObject.id)}")
UserAssetObj : UserAsset = UserAsset.query.filter_by(id=UAID).first()
if UserAssetObj is None:
flash("Invalid request", "error")
return redirect(f"/catalog/resell/{str(AssetObject.id)}")
if UserAssetObj.userid != AuthenticatedUser.id:
flash("Invalid request", "error")
return redirect(f"/catalog/resell/{str(AssetObject.id)}")
if UserAssetObj.assetid != AssetObject.id:
flash("Invalid request", "error")
return redirect(f"/catalog/resell/{str(AssetObject.id)}")
if not websiteFeatures.GetWebsiteFeature("ItemReselling"):
flash("Item reselling is currently disabled", "error")
return redirect(f"/catalog/resell/{str(AssetObject.id)}")
if AuthenticatedUser.TOTPEnabled:
if "2fa-code" not in request.form:
flash("Invalid request", "error")
return redirect(f"/catalog/resell/{str(AssetObject.id)}")
TOTPCode = request.form["2fa-code"]
if len(TOTPCode) != 6:
flash("Invalid 2FA code", "error")
return redirect(f"/catalog/resell/{str(AssetObject.id)}")
if not auth.Validate2FACode(AuthenticatedUser.id, TOTPCode):
flash("Invalid 2FA code", "error")
return redirect(f"/catalog/resell/{str(AssetObject.id)}")
if ItemPrice < 1 or ItemPrice > 999999999:
flash("Invalid price ( 1 - 999999999 )", "error")
return redirect(f"/catalog/resell/{str(AssetObject.id)}")
if UserAssetObj.is_for_sale:
flash("This asset is already for sale", "error")
return redirect(f"/catalog/resell/{str(AssetObject.id)}")
UserCurrentMembership : MembershipType = GetUserMembership(AuthenticatedUser)
if UserCurrentMembership == MembershipType.NonBuildersClub:
flash("You must be a Builders Club member to sell items", "error")
return redirect(f"/catalog/resell/{str(AssetObject.id)}")
UserAssetObj.is_for_sale = True
UserAssetObj.price = ItemPrice
db.session.commit()
return redirect(f"/catalog/resell/{str(AssetObject.id)}")
@CatalogRoute.route("/resell/<int:uaid>/takeoff", methods=["POST"])
@auth.authenticated_required
@csrf.exempt
def resell_takeoff(uaid):
AuthenticatedUser : User = auth.GetCurrentUser()
UserAssetObject : UserAsset = UserAsset.query.filter_by(id=uaid).first()
if UserAssetObject is None:
flash("Invalid asset", "error")
return redirect("/catalog")
if UserAssetObject.userid != AuthenticatedUser.id:
flash("Invalid asset", "error")
return redirect("/catalog")
if not UserAssetObject.is_for_sale:
flash("This asset is not for sale", "error")
return redirect(f"/catalog/resell/{str(UserAssetObject.assetid)}")
UserAssetObject.is_for_sale = False
UserAssetObject.price = 0
db.session.commit()
return redirect(f"/catalog/resell/{str(UserAssetObject.assetid)}")
@CatalogRoute.route("/api/purchase", methods=["POST"])
@auth.authenticated_required_api
@limiter.limit("1/second")
@user_limiter.limit("1/second")
def api_purchase():
JSONData = request.json
if "assetId" not in JSONData or "expectedPrice" not in JSONData or "currencyType" not in JSONData:
return jsonify({"success": False, "message": "Invalid request"}),400
if not websiteFeatures.GetWebsiteFeature("EconomyPurchase"):
return jsonify({"success": False, "message": "Purchasing is temporarily disabled"}),400
try:
JSONData["assetId"] = int(JSONData["assetId"])
JSONData["expectedPrice"] = int(JSONData["expectedPrice"])
JSONData["currencyType"] = int(JSONData["currencyType"])
except:
return jsonify({"success": False, "message": "Invalid request"}),400
AssetObject : Asset = Asset.query.filter_by(id=JSONData["assetId"]).first()
if AssetObject is None:
return jsonify({"success": False, "message": "Invalid asset"}),400
AuthenticatedUser : User = auth.GetCurrentUser()
try:
with redis_lock.Lock(redis_client = redis_controller, name=f"asset:{str(AssetObject.id)}", expire=15, auto_renewal=True, strict=True) as lock:
if AssetObject.offsale_at is not None:
if datetime.utcnow() > AssetObject.offsale_at:
AssetObject.is_for_sale = False
AssetObject.offsale_at = None
db.session.commit()
if AssetObject.is_for_sale == False:
return jsonify({"success": False, "message": "Asset is not for sale"}),400
if JSONData["currencyType"] == 0:
if JSONData["expectedPrice"] != AssetObject.price_robux:
return jsonify({"success": False, "message": "Expected Price is different from current price"}),400
elif JSONData["currencyType"] == 1:
if JSONData["expectedPrice"] != AssetObject.price_tix:
return jsonify({"success": False, "message": "Expected Price is different from current price"}),400
else:
return jsonify({"success": False, "message": "Invalid currency type"}),400
if AssetObject.price_robux == 0 and AssetObject.price_tix != 0 and JSONData["currencyType"] == 0:
return jsonify({"success": False, "message": "Asset is not being sold in this currency"}),400
if AssetObject.price_tix == 0 and AssetObject.price_robux != 0 and JSONData["currencyType"] == 1:
return jsonify({"success": False, "message": "Asset is not being sold in this currency"}),400
UserAssetObj : UserAsset = UserAsset.query.filter_by(userid=AuthenticatedUser.id, assetid=AssetObject.id).first()
if UserAssetObj is not None:
return jsonify({"success": False, "message": "User already owns asset"}),400
PurchaserRobuxBal, PurchaserTicketsBal = GetUserBalance(AuthenticatedUser)
if JSONData["currencyType"] == 0:
if PurchaserRobuxBal < AssetObject.price_robux:
return jsonify({"success": False, "message": "Insufficient funds"}),400
try:
DecrementTargetBalance(
Target = AuthenticatedUser,
Amount = AssetObject.price_robux,
CurrencyType = 0
)
except InsufficientFundsException:
return jsonify({"success": False, "message": "Insufficient funds"}),400
except EconomyLockAcquireException:
return jsonify({"success": False, "message": "Failed to acquire lock"}),400
except Exception as e:
logging.error(f"/api/purchase : Failed to decrement balance for user {str(AuthenticatedUser.id)}, message: {str(e)}")
return jsonify({"success": False, "message": "Failed to decrement balance"}),400
IncrementAssetCreator(
AssetObject = AssetObject,
AmountGiven = AssetObject.price_robux,
CurrencyType = 0
)
elif JSONData["currencyType"] == 1:
if PurchaserTicketsBal < AssetObject.price_tix:
return jsonify({"success": False, "message": "Insufficient funds"}),400
try:
DecrementTargetBalance(
Target = AuthenticatedUser,
Amount = AssetObject.price_tix,
CurrencyType = 1
)
except InsufficientFundsException:
return jsonify({"success": False, "message": "Insufficient funds"}),400
except EconomyLockAcquireException:
return jsonify({"success": False, "message": "Failed to acquire lock"}),400
except Exception as e:
logging.error(f"/api/purchase : Failed to decrement balance for user {str(AuthenticatedUser.id)}, message: {str(e)}")
return jsonify({"success": False, "message": "Failed to decrement balance"}),400
IncrementAssetCreator(
AssetObject = AssetObject,
AmountGiven = AssetObject.price_tix,
CurrencyType = 1
)
else:
return jsonify({"success": False, "message": "Invalid currency type"}),400
if AssetObject.is_limited_unique:
ItemSerial = AssetObject.sale_count + 1
if AssetObject.sale_count + 1 >= AssetObject.serial_count:
AssetObject.is_for_sale = False
if AssetObject.is_limited_unique:
UserAssetObj = UserAsset(userid=AuthenticatedUser.id, assetid=AssetObject.id, serial=ItemSerial)
else:
UserAssetObj = UserAsset(userid=AuthenticatedUser.id, assetid=AssetObject.id)
if AssetObject.asset_type == AssetType.Package:
PackageAssets = PackageAsset.query.filter_by(package_asset_id=AssetObject.id).all()
for PackageAssetObj in PackageAssets:
NewPackageAssetObj = UserAsset(userid=AuthenticatedUser.id, assetid=PackageAssetObj.asset_id)
db.session.add(NewPackageAssetObj)
AssetObject.sale_count += 1
db.session.add(UserAssetObj)
db.session.commit()
try:
if AssetObject.creator_type == 0:
SellerUserObj : User = User.query.filter_by(id=AssetObject.creator_id).first()
CreateTransactionForSale(
AssetObj = AssetObject,
PurchasePrice = JSONData["expectedPrice"],
PurchaseCurrencyType = JSONData["currencyType"],
Seller = SellerUserObj,
Buyer = AuthenticatedUser,
ApplyTaxAutomatically = True
)
else:
GroupObj : Group = Group.query.filter_by(id=AssetObject.creator_id).first()
CreateTransactionForSale(
AssetObj = AssetObject,
PurchasePrice = JSONData["expectedPrice"],
PurchaseCurrencyType = JSONData["currencyType"],
Seller = GroupObj,
Buyer = AuthenticatedUser,
ApplyTaxAutomatically = True
)
except Exception as e:
logging.warn(f"Failed to create transaction log for sale of asset {str(AssetObject.id)}, message: {str(e)}")
pass
return jsonify({"success": True, "message": "Asset purchased successfully"}),200
except AssertionError as e:
return jsonify({"success": False, "message": "Failed to acquire lock"}),400
@CatalogRoute.route("/api/purchase-limited", methods=["POST"])
@auth.authenticated_required_api
@limiter.limit("1/second")
@user_limiter.limit("1/second")
def api_purchase_limited():
JSONData = request.json
if "assetId" not in JSONData or "expectedPrice" not in JSONData or "expectedOwner" not in JSONData or "itemOwnershipId" not in JSONData:
return jsonify({"success": False, "message": "Invalid request"}),400
# Expected Types: assetId (int), expectedPrice (int), expectedOwner (int), itemOwnershipId (int)
if type(JSONData["assetId"]) != int or type(JSONData["expectedPrice"]) != int or type(JSONData["expectedOwner"]) != int or type(JSONData["itemOwnershipId"]) != int:
return jsonify({"success": False, "message": "Invalid request"}),400
AuthenticatedUser : User = auth.GetAuthenticatedUser(request.cookies.get(".ROBLOSECURITY"))
EconomyLock = acquire_lock(f"economy:{str(AuthenticatedUser.id)}", acquire_timeout=5, lock_timeout=1)
if EconomyLock is False:
return jsonify({"success": False, "message": "Failed to acquire lock"}),400
AssetObject : Asset = Asset.query.filter_by(id=JSONData["assetId"]).first()
if AssetObject is None:
release_lock(f"economy:{str(AuthenticatedUser.id)}", EconomyLock)
return jsonify({"success": False, "message": "Invalid asset"}),400
if AssetObject.is_limited == False:
release_lock(f"economy:{str(AuthenticatedUser.id)}", EconomyLock)
return jsonify({"success": False, "message": "Asset is not limited"}),400
LimitedAsset : UserAsset = UserAsset.query.filter_by(assetid=AssetObject.id, id=JSONData["itemOwnershipId"]).first()
if LimitedAsset is None:
release_lock(f"economy:{str(AuthenticatedUser.id)}", EconomyLock)
return jsonify({"success": False, "message": "Invalid asset info"}),400
ItemLock = acquire_lock(f"item:{str(LimitedAsset.id)}", acquire_timeout=5, lock_timeout=1)
if ItemLock is False:
return jsonify({"success": False, "message": "Failed to acquire lock ( This is usually caused by too many people purchasing this item at the same time. )"}),400
if LimitedAsset.is_for_sale == False:
release_lock(f"item:{str(LimitedAsset.id)}", ItemLock)
release_lock(f"economy:{str(AuthenticatedUser.id)}", EconomyLock)
return jsonify({"success": False, "message": "Item is not for sale"}),400
if JSONData["expectedOwner"] != LimitedAsset.userid:
release_lock(f"item:{str(LimitedAsset.id)}", ItemLock)
release_lock(f"economy:{str(AuthenticatedUser.id)}", EconomyLock)
return jsonify({"success": False, "message": f"Expected Owner ({str(JSONData['expectedOwner'])}) does not match current item owner({str(LimitedAsset.userid)})"}),400
if JSONData["expectedPrice"] != LimitedAsset.price:
release_lock(f"item:{str(LimitedAsset.id)}", ItemLock)
release_lock(f"economy:{str(AuthenticatedUser.id)}", EconomyLock)
return jsonify({"success": False, "message": f"Expected Price ({str(JSONData['expectedPrice'])}) does not match current item price ({str(LimitedAsset.price)})"}),400
if AuthenticatedUser.id == LimitedAsset.userid:
release_lock(f"item:{str(LimitedAsset.id)}", ItemLock)
release_lock(f"economy:{str(AuthenticatedUser.id)}", EconomyLock)
return jsonify({"success": False, "message": "You cannot purchase your own item"}),400
UserEconomyObj : UserEconomy = UserEconomy.query.filter_by(userid=AuthenticatedUser.id).first()
if UserEconomyObj.robux < LimitedAsset.price:
redis_controller.delete(f"economy:{str(AuthenticatedUser.id)}")
return jsonify({"success": False, "message": "Insufficient funds"}),400
UserEconomyObj.robux -= LimitedAsset.price
OriginalPrice : int = LimitedAsset.price
db.session.commit()
IncrementTargetBalance(GetUserFromId(LimitedAsset.userid), TaxCurrencyAmount(LimitedAsset.price), 0)
AdjustAssetRap(LimitedAsset.assetid, LimitedAsset.price)
LimitedAsset.userid = AuthenticatedUser.id
LimitedAsset.is_for_sale = False
LimitedAsset.price = 0
LimitedAsset.updated = datetime.utcnow()
db.session.commit()
# We release the locks first as taking a thumbnail can take a while
release_lock(f"economy:{str(AuthenticatedUser.id)}", EconomyLock)
release_lock(f"item:{str(LimitedAsset.id)}", ItemLock)
PreviousOwnerId : int = JSONData["expectedOwner"]
# Check how many items the previous owner had of the same asset
PreviousOwnerItems = UserAsset.query.filter_by(userid=PreviousOwnerId, assetid=AssetObject.id).count()
if PreviousOwnerItems == 0:
# Remove the asset from the previous owner avatar if there is any
AvatarAsset = UserAvatarAsset.query.filter_by(user_id=PreviousOwnerId, asset_id=AssetObject.id).first()
if AvatarAsset is not None:
db.session.delete(AvatarAsset)
db.session.commit()
TakeUserThumbnail(PreviousOwnerId, True, False)
CreateSystemMessage(
f"Your {AssetObject.name} has been sold!",
f"Your item {AssetObject.name} ( UAID: {str(LimitedAsset.id)} / Serial: {str(LimitedAsset.serial)} ) has been sold for R$ {str(OriginalPrice)} to {AuthenticatedUser.username} ( {str(AuthenticatedUser.id)} ).",
PreviousOwnerId
)
SellerUserObj : User = User.query.filter_by(id=PreviousOwnerId).first()
try:
CreateTransactionForSale(
AssetObj = AssetObject,
PurchasePrice = OriginalPrice,
PurchaseCurrencyType = 0,
Seller = SellerUserObj,
Buyer = AuthenticatedUser,
ApplyTaxAutomatically = True
)
except Exception as e:
logging.warn(f"Failed to create transaction log for sale of asset {str(AssetObject.id)}, message: {str(e)}")
pass
try:
newLimitedTransfer = LimitedItemTransfer(
original_owner_id = SellerUserObj.id,
new_owner_id = AuthenticatedUser.id,
asset_id = AssetObject.id,
user_asset_id = LimitedAsset.id,
transfer_method = LimitedItemTransferMethod.Purchase,
purchased_price = OriginalPrice
)
db.session.add(newLimitedTransfer)
db.session.commit()
except Exception as e:
logging.warn(f"Failed to create limited item transfer log for asset {str(AssetObject.id)}, message: {str(e)}")
pass
return jsonify({"success": True, "message": "Item purchased successfully"}),200
@LibraryRoute.route("/<int:assetid>/", methods=["GET"])
@LibraryRoute.route("/<int:assetid>/<assetname>", methods=["GET"])
@auth.authenticated_required
def asset_page(assetid, assetname=None):
AssetObject : Asset = Asset.query.filter_by(id=assetid).first()
if AssetObject is None:
return redirect("/library")
SlugName = slugify(AssetObject.name, lowercase=False)
if SlugName is None or SlugName == "":
SlugName = "unnamed"
if assetname is None:
if request.args.get("page") is None:
return redirect(f"/library/{assetid}/{SlugName}")
else:
return redirect(f"/library/{assetid}/{SlugName}?page={request.args.get('page')}")
if assetname != SlugName:
if request.args.get("page") is None:
return redirect(f"/library/{assetid}/{SlugName}")
else:
return redirect(f"/library/{assetid}/{SlugName}?page={request.args.get('page')}")
if AssetObject.asset_type.value in [2,8,11,12,17,18,19,27,28,29,30,31,32,41,42,43,44,45,46,47,57,58]:
return redirect(f"/catalog/{str(AssetObject.id)}/")
if AssetObject.asset_type.value == 9:
return redirect(f"/games/{assetid}/")
CreatorObj : User | Group = User.query.filter_by(id=AssetObject.creator_id).first() if AssetObject.creator_type == 0 else Group.query.filter_by(id=AssetObject.creator_id).first()
Created = AssetObject.created_at.strftime("%d/%m/%Y")
Updated = AssetObject.updated_at.strftime("%d/%m/%Y")
AuthenticatedUser : User = auth.GetCurrentUser()
doesUserOwnAsset = UserAsset.query.filter_by(userid=AuthenticatedUser.id, assetid=assetid).first() is not None
BestPriceResult : UserAsset = UserAsset.query.filter_by(assetid=assetid, is_for_sale=True).order_by(UserAsset.price.asc()).first()
BestPrice = "None"
if BestPriceResult is not None:
BestPrice = str(BestPriceResult.price)
UserOwns = 0
PrivateSaleList = []
NextPage = 0
PreviousPage = 0
PageNumber = 0
AssetRap = 0
if AssetObject.is_limited and not AssetObject.is_for_sale:
AssetRap = GetAssetRap(assetid)
UserOwns = UserAsset.query.filter_by(userid=AuthenticatedUser.id, assetid=assetid).count()
Page = 1
if request.args.get("page"):
try:
Page = int(request.args.get("page"))
except:
pass
PrivateSales = UserAsset.query.filter_by(assetid=assetid, is_for_sale=True).order_by(UserAsset.price.asc()).paginate(page=Page, per_page=5).items
for sale in PrivateSales:
SellerUser : User = User.query.filter_by(id=sale.userid).first()
PrivateSaleList.append({
"price": sale.price,
"seller": SellerUser.username,
"sellerid": SellerUser.id,
"serial": sale.serial,
"uaid": sale.id
})
if len(UserAsset.query.filter_by(assetid=assetid, is_for_sale=True).order_by(UserAsset.price.asc()).paginate(page=Page+1, per_page=5, error_out=False).items) > 0:
NextPage = Page + 1
else:
NextPage = -1
if Page > 1:
PreviousPage = Page - 1
else:
PreviousPage = -1
PageNumber = Page
GamepassLinkObj = None
GamepassRootPlaceAsset = None
if AssetObject.asset_type == AssetType.GamePass:
GamepassLinkObj : GamepassLink = GamepassLink.query.filter_by(gamepass_id=AssetObject.id).first()
GamepassUniverse : Universe = Universe.query.filter_by(id=GamepassLinkObj.universe_id).first()
GamepassRootPlaceAsset : Asset = Asset.query.filter_by( id = GamepassUniverse.root_place_id ).first()
return render_template("catalog/library.html", asset=AssetObject, creator=CreatorObj, createddate=Created,
updateddate=Updated, doesUserOwnAsset=doesUserOwnAsset, BestPrice=BestPrice,
BestPriceResult=BestPriceResult, userOwnAmountCount=UserOwns, PrivateSales=PrivateSaleList,
NextPage=NextPage, PreviousPage=PreviousPage, PageNumber=PageNumber, AssetRap=AssetRap,
GamepassLinkObj=GamepassLinkObj, GamepassRootPlaceAsset=GamepassRootPlaceAsset)
@BadgesPageRoute.route("/<int:badgeid>/", methods=["GET"])
@BadgesPageRoute.route("/<int:badgeid>/<badgename>", methods=["GET"])
@auth.authenticated_required
def badge_page( badgeid : int, badgename : str = None ):
BadgeObject : PlaceBadge = PlaceBadge.query.filter_by(id=badgeid).first()
if BadgeObject is None:
return abort(404)
if badgename is None:
return redirect(f"/badges/{badgeid}/{slugify(BadgeObject.name, lowercase=False)}")
elif badgename != slugify(BadgeObject.name, lowercase=False):
return redirect(f"/badges/{badgeid}/{slugify(BadgeObject.name, lowercase=False)}")
AuthenticatedUser : User = auth.GetCurrentUser()
UserBadgeObj : UserBadge = UserBadge.query.filter_by(user_id=AuthenticatedUser.id, badge_id=badgeid).first()
AssociatedPlaceAssetObj : Asset = Asset.query.filter_by(id=BadgeObject.associated_place_id).first()
CreatorObj : User | Group = User.query.filter_by(id=AssociatedPlaceAssetObj.creator_id).first() if AssociatedPlaceAssetObj.creator_type == 0 else Group.query.filter_by(id=AssociatedPlaceAssetObj.creator_id).first()
Created = BadgeObject.created_at.strftime("%d/%m/%Y")
Updated = BadgeObject.updated_at.strftime("%d/%m/%Y")
return render_template(
"catalog/badges.html",
badge = BadgeObject,
userbadge = UserBadgeObj,
AssociatedPlaceAssetObj = AssociatedPlaceAssetObj,
creator = CreatorObj,
createddate = Created,
updateddate = Updated
)