408 lines
19 KiB
Python
408 lines
19 KiB
Python
from flask import Blueprint, render_template, request, redirect, url_for, jsonify, make_response, after_this_request
|
|
from app.util import auth, friends, websiteFeatures, transactions, redislock
|
|
import logging
|
|
from app.services import economy
|
|
from app.extensions import db, limiter, csrf, redis_controller, user_limiter
|
|
from app.models.asset import Asset
|
|
from app.models.user import User
|
|
from app.models.userassets import UserAsset
|
|
from app.models.package_asset import PackageAsset
|
|
from app.models.place_developer_product import DeveloperProduct
|
|
from app.models.product_receipt import ProductReceipt
|
|
from app.models.groups import Group
|
|
from app.enums.AssetType import AssetType
|
|
from app.enums.TransactionType import TransactionType
|
|
from app.pages.catalog.catalog import IncrementAssetCreator, CreateTransactionForSale
|
|
import math
|
|
|
|
MarketPlaceRoute = Blueprint("marketplace", __name__, url_prefix="/marketplace")
|
|
EconomyV1Route = Blueprint("economyv1", __name__, url_prefix="/")
|
|
|
|
@MarketPlaceRoute.route("/game-pass-product-info", methods=["GET"])
|
|
@MarketPlaceRoute.route("/productinfo", methods=["GET"])
|
|
def productinfo():
|
|
assetid = request.args.get("assetId") or request.args.get("gamePassId")
|
|
if assetid is None:
|
|
return "Invalid request",400
|
|
asset : Asset = Asset.query.filter_by(id=assetid).first()
|
|
if asset is None:
|
|
return "Asset not found",404
|
|
AssetCreator : User | Group = User.query.filter_by(id=asset.creator_id).first() if asset.creator_type == 0 else Group.query.filter_by(id=asset.creator_id).first()
|
|
if AssetCreator is None:
|
|
AssetCreatorName = "Unknown"
|
|
else:
|
|
AssetCreatorName = AssetCreator.username if isinstance(AssetCreator, User) else AssetCreator.name
|
|
return jsonify({
|
|
"Name": asset.name,
|
|
"Description": asset.description,
|
|
"Created": asset.created_at,
|
|
"Updated": asset.updated_at,
|
|
"PriceInRobux": asset.price_robux,
|
|
"PriceInTickets": asset.price_tix,
|
|
"AssetId": asset.id,
|
|
"ProductId": asset.id,
|
|
"AssetTypeId": asset.asset_type.value,
|
|
"Creator": {
|
|
"Id": asset.creator_id,
|
|
"Name": AssetCreatorName,
|
|
"CreatorType": asset.creator_type
|
|
},
|
|
"MinimumMembershipLevel": 0,
|
|
"IsForSale": asset.is_for_sale
|
|
})
|
|
|
|
@MarketPlaceRoute.route("/productDetails", methods=["GET"])
|
|
def productDetails():
|
|
productId = request.args.get( key = "productId", default = None, type = int )
|
|
if productId is None:
|
|
return jsonify({
|
|
"success": False,
|
|
"message": "Invalid request"
|
|
}), 400
|
|
TargetDeveloperProduct : DeveloperProduct = DeveloperProduct.query.filter_by( productid = productId ).first()
|
|
if TargetDeveloperProduct is None:
|
|
return jsonify({
|
|
"success": False,
|
|
"message": "Invalid request"
|
|
}), 400
|
|
|
|
return jsonify({
|
|
"TargetId" : 1,
|
|
"ProductType": "Developer Product",
|
|
"AssetId": 0,
|
|
"ProductId": TargetDeveloperProduct.productid,
|
|
"Name": TargetDeveloperProduct.name,
|
|
"Description": TargetDeveloperProduct.description,
|
|
"AssetTypeId": 0,
|
|
"Creator": {
|
|
"Id": 0,
|
|
"Name": None,
|
|
"CreatorType": None,
|
|
"CreatorTargetId": 0
|
|
},
|
|
"IconImageAssetId": TargetDeveloperProduct.iconimage_assetid,
|
|
"Created": TargetDeveloperProduct.created_at.strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
|
|
"Updated": TargetDeveloperProduct.updated_at.strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
|
|
"PriceInRobux": TargetDeveloperProduct.robux_price,
|
|
"PremiumPriceInRobux": 0,
|
|
"PriceInTickets": 0,
|
|
"IsNew": False,
|
|
"IsForSale": TargetDeveloperProduct.is_for_sale,
|
|
"IsPublicDomain": False,
|
|
"IsLimited": False,
|
|
"IsLimitedUnique": False,
|
|
"Remaining": None,
|
|
"Sales": None,
|
|
"MinimumMembershipLevel": 0
|
|
})
|
|
|
|
@MarketPlaceRoute.route("/purchase", methods=["POST"])
|
|
@csrf.exempt
|
|
@auth.authenticated_required_api
|
|
@limiter.limit("20/minute")
|
|
@limiter.limit("1/second")
|
|
@user_limiter.limit("20/minute")
|
|
@user_limiter.limit("1/second")
|
|
def inGamePurchaseHandler():
|
|
AuthenticatedUser : User = auth.GetCurrentUser()
|
|
if AuthenticatedUser is None:
|
|
return jsonify({"success": False, "message": "Unauthorized"}),401
|
|
|
|
productId = request.form.get("productId", None, int)
|
|
currencyTypeId = request.form.get("currencyTypeId", None, int)
|
|
expectedPrice = request.form.get("purchasePrice", None, int)
|
|
|
|
if not websiteFeatures.GetWebsiteFeature("EconomyPurchase"):
|
|
return jsonify({"success": False, "status": "EconomyDisabled"}),400
|
|
if productId is None or currencyTypeId is None or expectedPrice is None:
|
|
return jsonify({"success": False, "message": "Invalid request"}),400
|
|
currencyType = 0 if currencyTypeId == 1 else 1
|
|
AssetObj : Asset = Asset.query.filter_by(id=productId).first()
|
|
if AssetObj is None:
|
|
return jsonify({"success": False, "message": "Invalid request"}),400
|
|
if AssetObj.is_limited or not AssetObj.is_for_sale:
|
|
return jsonify({"success": False, "status": "NotForSale"}),400
|
|
|
|
LockAssetName = f"asset:{str(AssetObj.id)}"
|
|
AssetLock = redislock.acquire_lock(LockAssetName, acquire_timeout=15, lock_timeout=1)
|
|
if AssetLock is False:
|
|
return jsonify({"success": False, "status": "InternalServerError"}),500
|
|
|
|
@after_this_request
|
|
def release_asset_lock(response):
|
|
if AssetLock:
|
|
redislock.release_lock(LockAssetName, AssetLock)
|
|
|
|
UserRobuxBalance, UserTixBalance = economy.GetUserBalance(AuthenticatedUser)
|
|
if currencyType == 0:
|
|
if AssetObj.price_robux == 0 and AssetObj.price_tix != 0:
|
|
return jsonify({"success": False, "status": "CurrencyNotAccepted"}),400
|
|
if AssetObj.price_robux != expectedPrice:
|
|
return jsonify({"success": False, "status": "PriceChanged"}),400
|
|
if UserRobuxBalance < expectedPrice:
|
|
return jsonify({"success": False, "status": "InsufficientFunds"}),400
|
|
else:
|
|
if AssetObj.price_tix == 0 and AssetObj.price_robux != 0:
|
|
return jsonify({"success": False, "status": "CurrencyNotAccepted"}),400
|
|
if AssetObj.price_tix != expectedPrice:
|
|
return jsonify({"success": False, "status": "PriceChanged"}),400
|
|
if UserTixBalance < expectedPrice:
|
|
return jsonify({"success": False, "status": "InsufficientFunds"}),400
|
|
|
|
UserAssetObj : UserAsset = UserAsset.query.filter_by(userid=AuthenticatedUser.id, assetid=productId).first()
|
|
if UserAssetObj is not None:
|
|
return jsonify({"success": False, "status": "AlreadyOwned"}),400
|
|
try:
|
|
economy.DecrementTargetBalance(AuthenticatedUser, expectedPrice, currencyType)
|
|
except economy.InsufficientFundsException:
|
|
return jsonify({"success": False, "status": "InsufficientFunds"}),400
|
|
UserAssetObj = UserAsset(
|
|
userid = AuthenticatedUser.id,
|
|
assetid = productId
|
|
)
|
|
|
|
if AssetObj.asset_type == AssetType.Package:
|
|
PackageAssets = PackageAsset.query.filter_by(package_asset_id=AssetObj.id).all()
|
|
for PackageAssetObj in PackageAssets:
|
|
NewPackageAssetObj = UserAsset(userid=AuthenticatedUser.id, assetid=PackageAssetObj.asset_id)
|
|
db.session.add(NewPackageAssetObj)
|
|
|
|
IncrementAssetCreator(AssetObj, expectedPrice, currencyType)
|
|
|
|
AssetObj.sale_count += 1
|
|
db.session.add(UserAssetObj)
|
|
db.session.commit()
|
|
|
|
try:
|
|
if AssetObj.creator_type == 0:
|
|
SellerUserObj : User = User.query.filter_by(id=AssetObj.creator_id).first()
|
|
CreateTransactionForSale(
|
|
AssetObj = AssetObj,
|
|
PurchasePrice = expectedPrice,
|
|
PurchaseCurrencyType = currencyType,
|
|
Seller = SellerUserObj,
|
|
Buyer = AuthenticatedUser,
|
|
ApplyTaxAutomatically = True
|
|
)
|
|
elif AssetObj.creator_type == 1:
|
|
SellerGroupObj : Group = Group.query.filter_by(id=AssetObj.creator_id).first()
|
|
CreateTransactionForSale(
|
|
AssetObj = AssetObj,
|
|
PurchasePrice = expectedPrice,
|
|
PurchaseCurrencyType = currencyType,
|
|
Seller = SellerGroupObj,
|
|
Buyer = AuthenticatedUser,
|
|
ApplyTaxAutomatically = True
|
|
)
|
|
except Exception as e:
|
|
logging.warn(f"Failed to create transaction log for sale of asset {str(AssetObj.id)}, message: {str(e)}")
|
|
pass
|
|
|
|
return jsonify({"success": True}),200
|
|
|
|
@EconomyV1Route.route("/v1/purchases/products/<int:assetid>", methods=["POST"])
|
|
@csrf.exempt
|
|
@auth.authenticated_required_api
|
|
@user_limiter.limit("30/minute")
|
|
@user_limiter.limit("1/second")
|
|
def SubmitItemPurchaseEconomy( assetid : int ):
|
|
AuthenticatedUser : User = auth.GetCurrentUser()
|
|
if AuthenticatedUser is None:
|
|
return jsonify({"purchased": False, "reason": "Unauthorized"}),401
|
|
|
|
if "expectedPrice" not in request.json or "expectedCurrency" not in request.json:
|
|
return jsonify({"purchased": False, "reason": "Invalid request", "errorMsg": "Invalid Request"}),400
|
|
|
|
productId = assetid
|
|
currencyTypeId = request.json["expectedCurrency"]
|
|
expectedPrice = request.json["expectedPrice"]
|
|
|
|
if not websiteFeatures.GetWebsiteFeature("EconomyPurchase"):
|
|
return jsonify({"purchased": False, "reason": "EconomyDisabled", "errorMsg": "Purchasing is temporarily disabled, try again later."}),400
|
|
if productId is None or currencyTypeId is None or expectedPrice is None:
|
|
return jsonify({"purchased": False, "reason": "Invalid request", "errorMsg": "Invalid Request"}),400
|
|
currencyType = 0 if currencyTypeId == 1 else 1
|
|
AssetObj : Asset = Asset.query.filter_by(id=productId).first()
|
|
if AssetObj is None:
|
|
return jsonify({"purchased": False, "reason": "Invalid request", "errorMsg": "Invalid Request"}),400
|
|
if AssetObj.is_limited or not AssetObj.is_for_sale:
|
|
return jsonify({"purchased": False, "reason": "NotForSale", "errorMsg": "This item is not for sale"}),400
|
|
|
|
LockAssetName = f"asset:{str(AssetObj.id)}"
|
|
AssetLock = redislock.acquire_lock(LockAssetName, acquire_timeout=15, lock_timeout=1)
|
|
if AssetLock is False:
|
|
return jsonify({"success": False, "status": "InternalServerError"}),500
|
|
|
|
@after_this_request
|
|
def release_asset_lock(response):
|
|
if AssetLock:
|
|
redislock.release_lock(LockAssetName, AssetLock)
|
|
|
|
UserRobuxBalance, UserTixBalance = economy.GetUserBalance(AuthenticatedUser)
|
|
if currencyType == 0:
|
|
if AssetObj.price_robux == 0 and AssetObj.price_tix != 0:
|
|
return jsonify({"purchased": False, "reason": "CurrencyNotAccepted", "errorMsg": "This currency is not accepted for this item"}),400
|
|
if AssetObj.price_robux != expectedPrice:
|
|
return jsonify({"purchased": False, "reason": "PriceChanged", "errorMsg": "The price has changed"}),400
|
|
if UserRobuxBalance < expectedPrice:
|
|
return jsonify({"purchased": False, "reason": "InsufficientFunds", "errorMsg": "You do not have enough Robux to purchase this item."}),400
|
|
else:
|
|
if AssetObj.price_tix == 0 and AssetObj.price_robux != 0:
|
|
return jsonify({"purchased": False, "reason": "CurrencyNotAccepted", "errorMsg": "This currency is not accepted for this item"}),400
|
|
if AssetObj.price_tix != expectedPrice:
|
|
return jsonify({"purchased": False, "reason": "PriceChanged", "errorMsg": "The price has changed"}),400
|
|
if UserTixBalance < expectedPrice:
|
|
return jsonify({"purchased": False, "reason": "InsufficientFunds", "errorMsg": "You do not have enough Robux to purchase this item."}),400
|
|
# Check if the user already owns the asset
|
|
UserAssetObj : UserAsset = UserAsset.query.filter_by(userid=AuthenticatedUser.id, assetid=productId).first()
|
|
if UserAssetObj is not None:
|
|
return jsonify({"purchased": False, "reason": "AlreadyOwned", "errorMsg": "You already own this item."}),400
|
|
try:
|
|
economy.DecrementTargetBalance(AuthenticatedUser, expectedPrice, currencyType)
|
|
except economy.InsufficientFundsException:
|
|
return jsonify({"purchased": False, "reason": "InsufficientFunds", "errorMsg": "You do not have enough Robux to purchase this item."}),400
|
|
UserAssetObj = UserAsset(
|
|
userid = AuthenticatedUser.id,
|
|
assetid = productId
|
|
)
|
|
|
|
if AssetObj.asset_type == AssetType.Package:
|
|
PackageAssets = PackageAsset.query.filter_by(package_asset_id=AssetObj.id).all()
|
|
for PackageAssetObj in PackageAssets:
|
|
NewPackageAssetObj = UserAsset(userid=AuthenticatedUser.id, assetid=PackageAssetObj.asset_id)
|
|
db.session.add(NewPackageAssetObj)
|
|
|
|
IncrementAssetCreator(AssetObj, expectedPrice, currencyType)
|
|
|
|
AssetObj.sale_count += 1
|
|
db.session.add(UserAssetObj)
|
|
db.session.commit()
|
|
|
|
try:
|
|
if AssetObj.creator_type == 0:
|
|
SellerUserObj : User = User.query.filter_by(id=AssetObj.creator_id).first()
|
|
CreateTransactionForSale(
|
|
AssetObj = AssetObj,
|
|
PurchasePrice = expectedPrice,
|
|
PurchaseCurrencyType = currencyType,
|
|
Seller = SellerUserObj,
|
|
Buyer = AuthenticatedUser,
|
|
ApplyTaxAutomatically = True
|
|
)
|
|
elif AssetObj.creator_type == 1:
|
|
SellerGroupObj : Group = Group.query.filter_by(id=AssetObj.creator_id).first()
|
|
CreateTransactionForSale(
|
|
AssetObj = AssetObj,
|
|
PurchasePrice = expectedPrice,
|
|
PurchaseCurrencyType = currencyType,
|
|
Seller = SellerGroupObj,
|
|
Buyer = AuthenticatedUser,
|
|
ApplyTaxAutomatically = True
|
|
)
|
|
except Exception as e:
|
|
logging.warn(f"Failed to create transaction log for sale of asset {str(AssetObj.id)}, message: {str(e)}")
|
|
pass
|
|
|
|
if AssetObj.creator_type == 0:
|
|
CreatorObj : User = User.query.filter_by(id = AssetObj.creator_id).first()
|
|
sellerName = CreatorObj.username
|
|
else:
|
|
CreatorObj : Group = Group.query.filter_by(id = AssetObj.creator_id).first()
|
|
sellerName = CreatorObj.name
|
|
|
|
return jsonify({
|
|
"purchased": True,
|
|
"reason": "Success",
|
|
"productId": assetid,
|
|
"currency": currencyTypeId,
|
|
"assetId": assetid,
|
|
"assetName": AssetObj.name,
|
|
"assetType": AssetObj.asset_type.name,
|
|
"assetTypeDisplayName": AssetObj.asset_type.name,
|
|
"assetIsWearable": False,
|
|
"sellerName": sellerName,
|
|
"transactionVerb": "bought",
|
|
"isMultiPrivateSale": False
|
|
})
|
|
|
|
@MarketPlaceRoute.route("/submitpurchase", methods=["POST"])
|
|
@csrf.exempt
|
|
@auth.authenticated_required_api
|
|
@user_limiter.limit("20/minute")
|
|
@user_limiter.limit("1/second")
|
|
def SubmitProductPurchase():
|
|
productId : int = request.form.get( key = "productId", default = None, type = int )
|
|
currencyTypeId : int = request.form.get( key = "currencyTypeId", default = None, type = int )
|
|
expectedUnitPrice : int = request.form.get( key = "expectedUnitPrice", default = None, type = int )
|
|
placeId : int = request.form.get( key = "placeId", default = None, type = int )
|
|
requestId : str = request.form.get( key = "requestId", default = None, type = str )
|
|
|
|
if not websiteFeatures.GetWebsiteFeature("EconomyPurchase"):
|
|
return jsonify({"success": False, "status": "EconomyDisabled"}),400
|
|
if productId is None or currencyTypeId is None or expectedUnitPrice is None or placeId is None or requestId is None:
|
|
return jsonify({"success": False, "message": "Invalid request"}),400
|
|
currencyType = 0 if currencyTypeId == 1 else 1
|
|
TargetDeveloperProduct : DeveloperProduct = DeveloperProduct.query.filter_by( productid = productId, placeid = placeId ).first()
|
|
if TargetDeveloperProduct is None:
|
|
return jsonify({"success": False, "message": "Invalid request"}),400
|
|
|
|
PlaceAssetObj : Asset = Asset.query.filter_by( id = TargetDeveloperProduct.placeid ).first()
|
|
|
|
if TargetDeveloperProduct.is_for_sale == False:
|
|
return jsonify({"success": False, "message": "Invalid request"}),400
|
|
if TargetDeveloperProduct.robux_price != expectedUnitPrice:
|
|
return jsonify({"success": False, "message": "Invalid request"}),400
|
|
|
|
if redis_controller.exists(f"purchase_request:{requestId}"):
|
|
return jsonify({"success": False, "message": "Invalid request"}),400
|
|
|
|
if currencyType != 0: # Developer Products can only be purchased with Robux
|
|
return jsonify({"success": False, "message": "Invalid request"}),400
|
|
|
|
AuthenticatedUser : User = auth.GetCurrentUser()
|
|
if AuthenticatedUser is None:
|
|
return jsonify({"success": False, "message": "Unauthorized"}),401
|
|
UserRobuxBalance, _ = economy.GetUserBalance(AuthenticatedUser)
|
|
if UserRobuxBalance < expectedUnitPrice:
|
|
return jsonify({"success": False, "status": "InsufficientFunds"}),400
|
|
|
|
try:
|
|
economy.DecrementTargetBalance(AuthenticatedUser, expectedUnitPrice, currencyType)
|
|
except economy.InsufficientFundsException:
|
|
return jsonify({"success": False, "status": "InsufficientFunds"}),400
|
|
OwnerObj : User | Group = User.query.filter_by( id = PlaceAssetObj.creator_id ).first() if PlaceAssetObj.creator_type == 0 else Group.query.filter_by( id = PlaceAssetObj.creator_id ).first()
|
|
|
|
transactions.CreateTransaction(
|
|
Reciever = OwnerObj,
|
|
Sender = AuthenticatedUser,
|
|
CurrencyAmount = expectedUnitPrice,
|
|
CurrencyType = currencyType,
|
|
TransactionType = TransactionType.Purchase,
|
|
CustomText = f"Purchase of Product({TargetDeveloperProduct.name}) from {PlaceAssetObj.name}"
|
|
)
|
|
GrossProfitAfterTax = math.floor(expectedUnitPrice * 0.7)
|
|
|
|
economy.IncrementTargetBalance(OwnerObj, GrossProfitAfterTax, currencyType)
|
|
transactions.CreateTransaction(
|
|
Reciever = OwnerObj,
|
|
Sender = AuthenticatedUser,
|
|
CurrencyAmount = GrossProfitAfterTax,
|
|
CurrencyType = currencyType,
|
|
TransactionType = TransactionType.Sale,
|
|
CustomText = f"Sale of Product({TargetDeveloperProduct.name}) from {PlaceAssetObj.name}"
|
|
)
|
|
redis_controller.set(f"purchase_request:{requestId}", "1", ex = 60 * 10)
|
|
|
|
DeveloperProductReceiptObj : ProductReceipt = ProductReceipt(
|
|
user_id = AuthenticatedUser.id,
|
|
product_id = TargetDeveloperProduct.productid,
|
|
robux_amount = expectedUnitPrice
|
|
)
|
|
TargetDeveloperProduct.sales_count += 1
|
|
db.session.add(DeveloperProductReceiptObj)
|
|
db.session.commit()
|
|
|
|
return jsonify({
|
|
"success": True,
|
|
"receipt": DeveloperProductReceiptObj.receipt_id
|
|
}),200 |