825 lines
40 KiB
Python
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
|
|
)
|