551 lines
28 KiB
Python
551 lines
28 KiB
Python
from flask import Blueprint, render_template, request, redirect, url_for, flash, make_response, jsonify, abort
|
|
from app.util import auth, redislock, websiteFeatures
|
|
from app.extensions import limiter, db, csrf, user_limiter
|
|
from app.models.user_trade_items import UserTradeItem
|
|
from app.models.user_trades import UserTrade
|
|
from app.models.user import User
|
|
from app.models.asset import Asset
|
|
from app.models.userassets import UserAsset
|
|
from app.models.usereconomy import UserEconomy
|
|
from app.models.asset_rap import AssetRap
|
|
from app.models.limited_item_transfers import LimitedItemTransfer
|
|
from app.util.membership import GetUserMembership
|
|
from app.enums.MembershipType import MembershipType
|
|
from app.enums.TradeStatus import TradeStatus
|
|
from app.enums.LimitedItemTransferMethod import LimitedItemTransferMethod
|
|
from datetime import datetime, timedelta
|
|
import math
|
|
|
|
TradesPageRoute = Blueprint('trades', __name__, template_folder="pages")
|
|
|
|
@TradesPageRoute.route("/trade", methods=['GET'])
|
|
@auth.authenticated_required
|
|
def trades():
|
|
AuthenticatedUser : User = auth.GetCurrentUser()
|
|
TradeInfo = []
|
|
|
|
PageCategory = request.args.get("category", default="inbound", type=str)
|
|
PageCategory = PageCategory.lower()
|
|
PageNumber = request.args.get("page", default=1, type=int)
|
|
if PageNumber < 1:
|
|
PageNumber = 1
|
|
|
|
if PageCategory == "inbound":
|
|
TradeListObj : list[UserTrade] = UserTrade.query.filter_by(recipient_userid=AuthenticatedUser.id, status=TradeStatus.Pending).order_by(UserTrade.updated_at.desc()).paginate( per_page=15, page=PageNumber, error_out=False )
|
|
for TradeObj in TradeListObj:
|
|
OppositeUser : User = User.query.filter_by(id=TradeObj.sender_userid).first()
|
|
if OppositeUser is None:
|
|
continue
|
|
TradeInfo.append({
|
|
"TradeID": TradeObj.id,
|
|
"TradeStatus": TradeObj.status,
|
|
"Created": datetime.strftime(TradeObj.created_at, "%d/%m/%Y %H:%M:%S UTC"),
|
|
"Expiration": datetime.strftime(TradeObj.expires_at, "%d/%m/%Y %H:%M:%S UTC"),
|
|
"OppositeUser": OppositeUser
|
|
})
|
|
elif PageCategory == "outbound":
|
|
TradeListObj : list[UserTrade] = UserTrade.query.filter_by(sender_userid=AuthenticatedUser.id, status=TradeStatus.Pending).order_by(UserTrade.updated_at.desc()).paginate( per_page=15, page=PageNumber, error_out=False )
|
|
for TradeObj in TradeListObj:
|
|
OppositeUser : User = User.query.filter_by(id=TradeObj.recipient_userid).first()
|
|
if OppositeUser is None:
|
|
continue
|
|
TradeInfo.append({
|
|
"TradeID": TradeObj.id,
|
|
"TradeStatus": TradeObj.status,
|
|
"Created": datetime.strftime(TradeObj.created_at, "%d/%m/%Y %H:%M:%S UTC"),
|
|
"Expiration": datetime.strftime(TradeObj.expires_at, "%d/%m/%Y %H:%M:%S UTC"),
|
|
"OppositeUser": OppositeUser
|
|
})
|
|
elif PageCategory == "completed":
|
|
TradeListObj : list[UserTrade] = UserTrade.query.filter((UserTrade.recipient_userid == AuthenticatedUser.id) | (UserTrade.sender_userid == AuthenticatedUser.id)).filter_by(status=TradeStatus.Accepted).order_by(UserTrade.updated_at.desc()).paginate( per_page=15, page=PageNumber, error_out=False )
|
|
for TradeObj in TradeListObj:
|
|
if TradeObj.recipient_userid == AuthenticatedUser.id:
|
|
OppositeUser : User = User.query.filter_by(id=TradeObj.sender_userid).first()
|
|
else:
|
|
OppositeUser : User = User.query.filter_by(id=TradeObj.recipient_userid).first()
|
|
if OppositeUser is None:
|
|
continue
|
|
TradeInfo.append({
|
|
"TradeID": TradeObj.id,
|
|
"TradeStatus": TradeObj.status,
|
|
"Created": datetime.strftime(TradeObj.created_at, "%d/%m/%Y %H:%M:%S UTC"),
|
|
"Expiration": datetime.strftime(TradeObj.expires_at, "%d/%m/%Y %H:%M:%S UTC"),
|
|
"OppositeUser": OppositeUser
|
|
})
|
|
elif PageCategory == "inactive": # inactive trades
|
|
TradeListObj : list[UserTrade] = UserTrade.query.filter((UserTrade.recipient_userid == AuthenticatedUser.id) | (UserTrade.sender_userid == AuthenticatedUser.id)).filter( (UserTrade.status == TradeStatus.Declined) | (UserTrade.status == TradeStatus.Expired) | (UserTrade.status == TradeStatus.Cancelled)).order_by(UserTrade.updated_at.desc()).paginate( per_page=15, page=PageNumber, error_out=False )
|
|
for TradeObj in TradeListObj:
|
|
if TradeObj.recipient_userid == AuthenticatedUser.id:
|
|
OppositeUser : User = User.query.filter_by(id=TradeObj.sender_userid).first()
|
|
else:
|
|
OppositeUser : User = User.query.filter_by(id=TradeObj.recipient_userid).first()
|
|
if OppositeUser is None:
|
|
continue
|
|
TradeInfo.append({
|
|
"TradeID": TradeObj.id,
|
|
"TradeStatus": TradeObj.status,
|
|
"Created": datetime.strftime(TradeObj.created_at, "%d/%m/%Y %H:%M:%S UTC"),
|
|
"Expiration": datetime.strftime(TradeObj.expires_at, "%d/%m/%Y %H:%M:%S UTC"),
|
|
"OppositeUser": OppositeUser
|
|
})
|
|
else:
|
|
return abort(404)
|
|
|
|
return render_template("trades/index.html", TradeInfo=TradeInfo, PageCategory=PageCategory, TradeListObj=TradeListObj)
|
|
|
|
@TradesPageRoute.route("/trade/<int:userid>/create", methods=['GET'])
|
|
@auth.authenticated_required
|
|
def createTrade(userid : int):
|
|
AuthenticatedUser : User = auth.GetCurrentUser()
|
|
if AuthenticatedUser.id == userid:
|
|
return abort(400)
|
|
UserCurrentMembership : MembershipType = GetUserMembership(AuthenticatedUser)
|
|
if UserCurrentMembership == MembershipType.NonBuildersClub:
|
|
return redirect("/membership")
|
|
TargetUser : User = User.query.filter_by(id=userid).first()
|
|
if TargetUser is None:
|
|
return abort(404)
|
|
if TargetUser.accountstatus == 4 or TargetUser.accountstatus == 3:
|
|
return abort(404)
|
|
return render_template("trades/create.html", TargetUser=TargetUser, AuthenticatedUser=AuthenticatedUser)
|
|
|
|
@TradesPageRoute.route("/trade/<int:userid>/create", methods=['POST'])
|
|
@auth.authenticated_required
|
|
@limiter.limit("5/second")
|
|
@user_limiter.limit("5/second")
|
|
def createTradePost(userid : int):
|
|
AuthenticatedUser : User = auth.GetCurrentUser()
|
|
if AuthenticatedUser.id == userid:
|
|
return jsonify({"success": False, "message": "You cannot trade with yourself."}),400
|
|
|
|
if not websiteFeatures.GetWebsiteFeature("ItemTrading"):
|
|
return jsonify({"success": False, "message": "Item trading is temporarily disabled."}),403
|
|
|
|
TargetUser : User = User.query.filter_by(id=userid).first()
|
|
if TargetUser is None:
|
|
return jsonify({"success": False, "message": "User not found."}),404
|
|
if TargetUser.accountstatus == 4 or TargetUser.accountstatus == 3:
|
|
return jsonify({"success": False, "message": "User not found."}),404
|
|
TradeRequestData : dict = request.json
|
|
if TradeRequestData is None:
|
|
return jsonify({"success": False, "message": "Invalid request data."}),400
|
|
UserCurrentMembership : MembershipType = GetUserMembership(AuthenticatedUser)
|
|
if UserCurrentMembership == MembershipType.NonBuildersClub:
|
|
return jsonify({"success": False, "message": "You must be a Builders Club member to send trades."}),403
|
|
OppositeUserCurrentMembership : MembershipType = GetUserMembership(TargetUser)
|
|
if OppositeUserCurrentMembership == MembershipType.NonBuildersClub:
|
|
return jsonify({"success": False, "message": "The opposite user must be a Builders Club member to accept trades."}),403
|
|
"""
|
|
Expected JSON:
|
|
"RequesterOfferRobux": RequesterOfferRobux, # int must be >= 0 <= 100000
|
|
"TargetOfferRobux": TargetOfferRobux, # int must be >= 0 and <= 100000
|
|
"RequesterOfferUAIDs": RequesterOfferUAIDs, # list of int
|
|
"TargetOfferUAIDs": TargetOfferUAIDs, # list of int
|
|
"TOTPCode": isTOTPEnabled ? TOTPInputElement.value : null # int or null
|
|
"""
|
|
|
|
# RequesterOfferRobux : int = TradeRequestData.get("RequesterOfferRobux", 0)
|
|
# TargetOfferRobux : int = TradeRequestData.get("TargetOfferRobux", 0)
|
|
# RequesterOfferUAIDs : list = TradeRequestData.get("RequesterOfferUAIDs", [])
|
|
# TargetOfferUAIDs : list = TradeRequestData.get("TargetOfferUAIDs", [])
|
|
# TOTPCode : int | None = TradeRequestData.get("TOTPCode", None, type=int)
|
|
|
|
# if not all(key in TradeRequestData for key in ("RequesterOfferRobux", "TargetOfferRobux", "RequesterOfferUAIDs", "TargetOfferUAIDs", "TOTPCode")):
|
|
# return jsonify({"success": False, "message": "Invalid request data."}),400
|
|
# if not isinstance(TradeRequestData["RequesterOfferRobux"], int) or not isinstance(TradeRequestData["TargetOfferRobux"], int):
|
|
# return jsonify({"success": False, "message": "Invalid request data, Robux Offer must be an integer."}),400
|
|
# if not isinstance(TradeRequestData["RequesterOfferUAIDs"], list) or not isinstance(TradeRequestData["TargetOfferUAIDs"], list):
|
|
# return jsonify({"success": False, "message": "Invalid request data, Offered UAIDs must be a list."}),400
|
|
# if not isinstance(TradeRequestData["TOTPCode"], int) and TradeRequestData["TOTPCode"] is not None:
|
|
# return jsonify({"success": False, "message": "Invalid request data, TOTP code must be an integer."}),400
|
|
|
|
try:
|
|
assert "RequesterOfferRobux" in TradeRequestData, "Missing parameter RequesterOfferRobux"
|
|
assert "TargetOfferRobux" in TradeRequestData, "Missing parameter TargetOfferRobux"
|
|
assert "RequesterOfferUAIDs" in TradeRequestData, "Missing parameter RequesterOfferUAIDs"
|
|
assert "TargetOfferUAIDs" in TradeRequestData, "Missing parameter TargetOfferUAIDs"
|
|
assert "TOTPCode" in TradeRequestData, "Missing parameter TOTPCode"
|
|
assert isinstance(TradeRequestData["RequesterOfferRobux"], int), "Invalid request data, Robux Offer must be an integer."
|
|
assert isinstance(TradeRequestData["TargetOfferRobux"], int), "Invalid request data, Target Robux Offer must be an integer."
|
|
assert isinstance(TradeRequestData["RequesterOfferUAIDs"], list), "Invalid request data, Offered UAIDs must be a list."
|
|
assert isinstance(TradeRequestData["TargetOfferUAIDs"], list), "Invalid request data, Offered UAIDs must be a list."
|
|
assert isinstance(TradeRequestData["TOTPCode"], int) or TradeRequestData["TOTPCode"] is None, "Invalid request data, TOTP code must be an integer or null."
|
|
except Exception as e:
|
|
return jsonify({"success": False, "message": f"Payload Validation failed, {str(e)}"}), 400
|
|
|
|
TotalTradesActive : int = UserTrade.query.filter_by(sender_userid=AuthenticatedUser.id, status=TradeStatus.Pending).count()
|
|
if TotalTradesActive >= 25:
|
|
return jsonify({"success": False, "message": "You cannot have more than 25 active trades at once."}),400
|
|
ActiveTradesWithTarget : int = UserTrade.query.filter_by(sender_userid=AuthenticatedUser.id, recipient_userid=TargetUser.id, status=TradeStatus.Pending).count()
|
|
if ActiveTradesWithTarget >= 2:
|
|
return jsonify({"success": False, "message": "You cannot have more than 2 active trades with the same user at once."}),400
|
|
|
|
RequesterOfferRobux : int = TradeRequestData["RequesterOfferRobux"]
|
|
TargetOfferRobux : int = TradeRequestData["TargetOfferRobux"]
|
|
RequesterOfferUAIDs : list = TradeRequestData["RequesterOfferUAIDs"]
|
|
TargetOfferUAIDs : list = TradeRequestData["TargetOfferUAIDs"]
|
|
TOTPCode : int | None = TradeRequestData["TOTPCode"]
|
|
|
|
if (RequesterOfferRobux < 0 or TargetOfferRobux < 0) or (RequesterOfferRobux > 100000 or TargetOfferRobux > 100000):
|
|
return jsonify({"success": False, "message": "Invalid request data, Robux Offer must be between 0 - 100000"}),400
|
|
|
|
if len(RequesterOfferUAIDs) > 4 or len(TargetOfferUAIDs) > 4:
|
|
return jsonify({"success": False, "message": "Invalid request data, cannot trade more than 4 items at once."}),400
|
|
|
|
if len(RequesterOfferUAIDs) <= 0 or len(TargetOfferUAIDs) <= 0:
|
|
return jsonify({"success": False, "message": "Invalid request data, both request and offer must contain at least one item"}),400
|
|
|
|
if AuthenticatedUser.TOTPEnabled:
|
|
if TOTPCode is None:
|
|
return jsonify({"success": False, "message": "Invalid request data, TOTP code is required."}),400
|
|
if not auth.Validate2FACode(AuthenticatedUser.id, int(TOTPCode)):
|
|
return jsonify({"success": False, "message": "Invalid request data, TOTP code is invalid."}),400
|
|
|
|
for UAID in RequesterOfferUAIDs:
|
|
UserAssetObj : UserAsset = UserAsset.query.filter_by(id=UAID).first()
|
|
if UserAssetObj is None:
|
|
return jsonify({"success": False, "message": "Invalid request data, one of the items is invalid."}),400
|
|
if UserAssetObj.userid != AuthenticatedUser.id:
|
|
return jsonify({"success": False, "message": f"Invalid request data, you do not own UAID {str(UAID)}"}),400
|
|
AssetObj : Asset = Asset.query.filter_by(id=UserAssetObj.assetid).first()
|
|
if not AssetObj.is_limited or AssetObj.is_for_sale:
|
|
return jsonify({"success": False, "message": f"Invalid request data, you cannot trade UAID {str(UAID)}"}),400
|
|
|
|
for UAID in TargetOfferUAIDs:
|
|
UserAssetObj : UserAsset = UserAsset.query.filter_by(id=UAID).first()
|
|
if UserAssetObj is None:
|
|
return jsonify({"success": False, "message": "Invalid request data, one of the items is invalid."}),400
|
|
if UserAssetObj.userid != TargetUser.id:
|
|
return jsonify({"success": False, "message": f"Invalid request data, user does not own UAID {str(UAID)}"}),400
|
|
AssetObj : Asset = Asset.query.filter_by(id=UserAssetObj.assetid).first()
|
|
if not AssetObj.is_limited or AssetObj.is_for_sale:
|
|
return jsonify({"success": False, "message": f"Invalid request data, you cannot trade UAID {str(UAID)}"}),400
|
|
|
|
NewUserTrade : UserTrade = UserTrade(
|
|
sender_userid = AuthenticatedUser.id,
|
|
recipient_userid = TargetUser.id,
|
|
sender_userid_robux = RequesterOfferRobux,
|
|
recipient_userid_robux = TargetOfferRobux,
|
|
status = TradeStatus.Pending,
|
|
expires_at = datetime.utcnow() + timedelta(days=7)
|
|
)
|
|
try:
|
|
db.session.add(NewUserTrade)
|
|
db.session.commit()
|
|
|
|
for UAID in RequesterOfferUAIDs:
|
|
NewTradeAsset : UserTradeItem = UserTradeItem(
|
|
tradeid = NewUserTrade.id,
|
|
userid = AuthenticatedUser.id,
|
|
user_asset_id = UAID
|
|
)
|
|
db.session.add(NewTradeAsset)
|
|
|
|
for UAID in TargetOfferUAIDs:
|
|
NewTradeAsset : UserTradeItem = UserTradeItem(
|
|
tradeid = NewUserTrade.id,
|
|
userid = TargetUser.id,
|
|
user_asset_id = UAID
|
|
)
|
|
db.session.add(NewTradeAsset)
|
|
|
|
db.session.commit()
|
|
except:
|
|
db.session.delete(NewUserTrade)
|
|
return jsonify({"success": False, "message": "Internal server error"}),500
|
|
return jsonify({"success": True, "message": "Trade request sent.", "tradeId": NewUserTrade.id}),200
|
|
|
|
@TradesPageRoute.route("/trade/view/<int:tradeid>", methods=['GET'])
|
|
@auth.authenticated_required_api
|
|
def viewTrade(tradeid : int):
|
|
AuthenticatedUser : User = auth.GetCurrentUser()
|
|
TradeObj : UserTrade = UserTrade.query.filter_by(id=tradeid).first()
|
|
if TradeObj is None:
|
|
return abort(404)
|
|
if TradeObj.sender_userid != AuthenticatedUser.id and TradeObj.recipient_userid != AuthenticatedUser.id:
|
|
return abort(403)
|
|
|
|
if TradeObj.expires_at < datetime.utcnow() and TradeObj.status == TradeStatus.Pending:
|
|
TradeObj.status = TradeStatus.Expired
|
|
TradeObj.updated_at = datetime.utcnow()
|
|
db.session.commit()
|
|
|
|
TradeItems : list[UserTradeItem] = UserTradeItem.query.filter_by(tradeid=TradeObj.id).all()
|
|
SenderUser : User = User.query.filter_by(id=TradeObj.sender_userid).first()
|
|
RecipientUser : User = User.query.filter_by(id=TradeObj.recipient_userid).first()
|
|
|
|
SenderItems = []
|
|
RecipientItems = []
|
|
|
|
AssetRAPCache = {}
|
|
def GetAssetRAP( AssetID : int ):
|
|
if AssetID in AssetRAPCache:
|
|
return AssetRAPCache[AssetID]
|
|
AssetRAPObj : AssetRap | None = AssetRap.query.filter_by(assetid=AssetID).first()
|
|
if AssetRAPObj is None:
|
|
return 0
|
|
AssetRAPCache[AssetID] = AssetRAPObj.rap
|
|
return AssetRAPCache[AssetID]
|
|
SenderOfferValue = 0
|
|
RecipientOfferValue = 0
|
|
|
|
for TradeItem in TradeItems:
|
|
UserAssetObj : UserAsset = UserAsset.query.filter_by(id=TradeItem.user_asset_id).first()
|
|
AssetObj : Asset = Asset.query.filter_by(id=UserAssetObj.assetid).first()
|
|
if TradeItem.userid == SenderUser.id:
|
|
SenderItems.append({
|
|
"UAID": UserAssetObj.id,
|
|
"Name": AssetObj.name,
|
|
"AssetId": AssetObj.id,
|
|
"RAP": GetAssetRAP(AssetObj.id),
|
|
"serial": UserAssetObj.serial
|
|
})
|
|
SenderOfferValue += GetAssetRAP(AssetObj.id)
|
|
else:
|
|
RecipientItems.append({
|
|
"UAID": UserAssetObj.id,
|
|
"Name": AssetObj.name,
|
|
"AssetId": AssetObj.id,
|
|
"RAP": GetAssetRAP(AssetObj.id),
|
|
"serial": UserAssetObj.serial
|
|
})
|
|
RecipientOfferValue += GetAssetRAP(AssetObj.id)
|
|
|
|
SenderOfferValue += TradeObj.sender_userid_robux
|
|
RecipientOfferValue += TradeObj.recipient_userid_robux
|
|
OppositeUser = SenderUser if AuthenticatedUser.id == RecipientUser.id else RecipientUser
|
|
|
|
return render_template("trades/view.html",
|
|
TradeObj = TradeObj,
|
|
SenderUser = SenderUser,
|
|
RecipientUser = RecipientUser,
|
|
AuthenticatedUser = AuthenticatedUser,
|
|
SenderItems = SenderItems,
|
|
RecipientItems = RecipientItems,
|
|
SenderOfferValue = SenderOfferValue,
|
|
RecipientOfferValue = RecipientOfferValue,
|
|
OppositeUser = OppositeUser)
|
|
|
|
@TradesPageRoute.route("/trade/<int:tradeid>/accept", methods=['POST'])
|
|
@auth.authenticated_required
|
|
def acceptTrade(tradeid : int):
|
|
AuthenticatedUser : User = auth.GetCurrentUser()
|
|
TradeObj : UserTrade = UserTrade.query.filter_by(id=tradeid).first()
|
|
if TradeObj is None:
|
|
return abort(404)
|
|
if TradeObj.recipient_userid != AuthenticatedUser.id:
|
|
return abort(403)
|
|
if TradeObj.status != TradeStatus.Pending:
|
|
return abort(403)
|
|
UserCurrentMembership : MembershipType = GetUserMembership(AuthenticatedUser.id)
|
|
if UserCurrentMembership == MembershipType.NonBuildersClub:
|
|
flash("You must be a Builders Club member to accept trades.", "error")
|
|
return redirect(f"/trade/view/{str(tradeid)}")
|
|
OppositeUser : User = User.query.filter_by(id=TradeObj.sender_userid).first()
|
|
if OppositeUser is None:
|
|
flash("An error occured while trying to complete this trade. Please try again later.", "error")
|
|
return redirect(f"/trade/view/{str(tradeid)}")
|
|
OppositeUserCurrentMembership : MembershipType = GetUserMembership(OppositeUser)
|
|
if OppositeUserCurrentMembership == MembershipType.NonBuildersClub:
|
|
flash("The opposite user must be a Builders Club member to accept trades.", "error")
|
|
return redirect(f"/trade/view/{str(tradeid)}")
|
|
if AuthenticatedUser.TOTPEnabled:
|
|
TOTPCode : str = request.form.get("totpcode", default=None, type=str)
|
|
if TOTPCode is None:
|
|
flash("Invalid TOTP code", "error")
|
|
return redirect(f"/trade/view/{str(tradeid)}")
|
|
if not auth.Validate2FACode(AuthenticatedUser.id, TOTPCode):
|
|
flash("Invalid TOTP code", "error")
|
|
return redirect(f"/trade/view/{str(tradeid)}")
|
|
|
|
if TradeObj.expires_at < datetime.utcnow():
|
|
flash("This trade has expired.", "error")
|
|
TradeObj.status = TradeStatus.Expired
|
|
db.session.commit()
|
|
return redirect(f"/trade/view/{str(tradeid)}")
|
|
|
|
TradeItems : list[UserTradeItem] = UserTradeItem.query.filter_by(tradeid=TradeObj.id).all()
|
|
for TradeItem in TradeItems:
|
|
UserAssetObj : UserAsset = UserAsset.query.filter_by(id=TradeItem.user_asset_id).first()
|
|
if UserAssetObj is None:
|
|
flash("One of the items no longer exists and this trade cannot be completed.", "error")
|
|
return redirect(f"/trade/view/{str(tradeid)}")
|
|
if UserAssetObj.userid != TradeItem.userid:
|
|
flash("One of the items no longer belongs to its original owner and this trade cannot be completed.", "error")
|
|
return redirect(f"/trade/view/{str(tradeid)}")
|
|
|
|
SenderEconomyLock = redislock.acquire_lock(f"economy:{str(TradeObj.sender_userid)}", acquire_timeout=5, lock_timeout=3)
|
|
RecipientEconomyLock = redislock.acquire_lock(f"economy:{str(TradeObj.recipient_userid)}", acquire_timeout=5, lock_timeout=3)
|
|
if not SenderEconomyLock or not RecipientEconomyLock:
|
|
if SenderEconomyLock:
|
|
redislock.release_lock(f"economy:{str(TradeObj.sender_userid)}", SenderEconomyLock)
|
|
flash("An error occured while trying to complete this trade. Please try again later.", "error")
|
|
return redirect(f"/trade/view/{str(tradeid)}")
|
|
|
|
SenderEconomyObj : UserEconomy = UserEconomy.query.filter_by(userid=TradeObj.sender_userid).first()
|
|
RecipientEconomyObj : UserEconomy = UserEconomy.query.filter_by(userid=TradeObj.recipient_userid).first()
|
|
|
|
if SenderEconomyObj.robux < TradeObj.sender_userid_robux:
|
|
flash("The sender does not have enough robux to complete this trade.", "error")
|
|
return redirect(f"/trade/view/{str(tradeid)}")
|
|
if RecipientEconomyObj.robux < TradeObj.recipient_userid_robux:
|
|
flash("The recipient does not have enough robux to complete this trade.", "error")
|
|
return redirect(f"/trade/view/{str(tradeid)}")
|
|
|
|
ItemLocks = []
|
|
for TradeItem in TradeItems:
|
|
TradeItemLock = redislock.acquire_lock(f"item:{str(TradeItem.user_asset_id)}", acquire_timeout=20, lock_timeout=5)
|
|
if not TradeItemLock:
|
|
redislock.release_lock(f"economy:{str(TradeObj.sender_userid)}", SenderEconomyLock)
|
|
redislock.release_lock(f"economy:{str(TradeObj.recipient_userid)}", RecipientEconomyLock)
|
|
for ItemLock in ItemLocks:
|
|
redislock.release_lock(f"item:{str(ItemLock[1])}", ItemLock[0])
|
|
|
|
flash("An error occured while trying to complete this trade. Please try again later.", "error")
|
|
return redirect(f"/trade/view/{str(tradeid)}")
|
|
ItemLocks.append([TradeItemLock, TradeItem.user_asset_id])
|
|
|
|
def ReleaseAllLocks():
|
|
redislock.release_lock(f"economy:{str(TradeObj.sender_userid)}", SenderEconomyLock)
|
|
redislock.release_lock(f"economy:{str(TradeObj.recipient_userid)}", RecipientEconomyLock)
|
|
for ItemLock in ItemLocks:
|
|
redislock.release_lock(f"item:{str(ItemLock[1])}", ItemLock[0])
|
|
|
|
SenderEconomyObj.robux -= TradeObj.sender_userid_robux
|
|
RecipientEconomyObj.robux -= TradeObj.recipient_userid_robux
|
|
|
|
if TradeObj.sender_userid_robux > 0:
|
|
FinalAdded = math.floor(TradeObj.sender_userid_robux * 0.7)
|
|
RecipientEconomyObj.robux += FinalAdded
|
|
if TradeObj.recipient_userid_robux > 0:
|
|
FinalAdded = math.floor(TradeObj.recipient_userid_robux * 0.7)
|
|
SenderEconomyObj.robux += FinalAdded
|
|
|
|
def CreateItemTransferLog( original_owner_id : int, new_owner_id : int, asset_id : int, user_asset_id : int ):
|
|
NewTransferLog = LimitedItemTransfer(
|
|
original_owner_id = original_owner_id,
|
|
new_owner_id = new_owner_id,
|
|
asset_id = asset_id,
|
|
user_asset_id = user_asset_id,
|
|
transfer_method = LimitedItemTransferMethod.Trade,
|
|
associated_trade_id = TradeObj.id
|
|
)
|
|
db.session.add(NewTransferLog)
|
|
|
|
for TradeItem in TradeItems:
|
|
UserAssetObj : UserAsset = UserAsset.query.filter_by(id=TradeItem.user_asset_id).first()
|
|
if UserAssetObj is None:
|
|
ReleaseAllLocks()
|
|
flash("One of the items no longer exists and this trade cannot be completed.", "error")
|
|
return redirect(f"/trade/view/{str(tradeid)}")
|
|
if UserAssetObj.userid == TradeObj.sender_userid:
|
|
CreateItemTransferLog( original_owner_id = TradeObj.sender_userid, new_owner_id = TradeObj.recipient_userid, asset_id = UserAssetObj.assetid, user_asset_id = UserAssetObj.id )
|
|
UserAssetObj.userid = TradeObj.recipient_userid
|
|
else:
|
|
CreateItemTransferLog( original_owner_id = TradeObj.sender_userid, new_owner_id = TradeObj.recipient_userid, asset_id = UserAssetObj.assetid, user_asset_id = UserAssetObj.id )
|
|
UserAssetObj.userid = TradeObj.sender_userid
|
|
UserAssetObj.is_for_sale = False
|
|
UserAssetObj.price = 0
|
|
UserAssetObj.updated = datetime.utcnow()
|
|
db.session.commit()
|
|
ReleaseAllLocks()
|
|
|
|
TradeObj.status = TradeStatus.Accepted
|
|
TradeObj.updated_at = datetime.utcnow()
|
|
db.session.commit()
|
|
|
|
return redirect(f"/trade/view/{str(tradeid)}")
|
|
|
|
@TradesPageRoute.route("/trade/<int:tradeid>/cancel", methods=['POST'])
|
|
@auth.authenticated_required
|
|
def cancelTrade(tradeid : int):
|
|
AuthenticatedUser : User = auth.GetCurrentUser()
|
|
TradeObj : UserTrade = UserTrade.query.filter_by(id=tradeid).first()
|
|
if TradeObj is None:
|
|
return abort(404)
|
|
if TradeObj.sender_userid != AuthenticatedUser.id:
|
|
return abort(403)
|
|
if TradeObj.status != TradeStatus.Pending:
|
|
return abort(403)
|
|
|
|
if TradeObj.expires_at < datetime.utcnow():
|
|
TradeObj.status = TradeStatus.Expired
|
|
TradeObj.updated_at = datetime.utcnow()
|
|
db.session.commit()
|
|
flash("This trade has expired.", "error")
|
|
return redirect(f"/trade/view/{str(tradeid)}")
|
|
|
|
TradeObj.status = TradeStatus.Cancelled
|
|
TradeObj.updated_at = datetime.utcnow()
|
|
db.session.commit()
|
|
|
|
return redirect(f"/trade/view/{str(tradeid)}")
|
|
|
|
@TradesPageRoute.route("/trade/<int:tradeid>/decline", methods=['POST'])
|
|
@auth.authenticated_required
|
|
@csrf.exempt
|
|
def declineTrade(tradeid : int):
|
|
AuthenticatedUser : User = auth.GetCurrentUser()
|
|
TradeObj : UserTrade = UserTrade.query.filter_by(id=tradeid).first()
|
|
if TradeObj is None:
|
|
return abort(404)
|
|
if TradeObj.recipient_userid != AuthenticatedUser.id:
|
|
return abort(403)
|
|
if TradeObj.status != TradeStatus.Pending:
|
|
return abort(403)
|
|
|
|
if TradeObj.expires_at < datetime.utcnow():
|
|
TradeObj.status = TradeStatus.Expired
|
|
TradeObj.updated_at = datetime.utcnow()
|
|
db.session.commit()
|
|
flash("This trade has expired.", "error")
|
|
return redirect(f"/trade/view/{str(tradeid)}")
|
|
|
|
TradeObj.status = TradeStatus.Declined
|
|
TradeObj.updated_at = datetime.utcnow()
|
|
db.session.commit()
|
|
|
|
return redirect(f"/trade/view/{str(tradeid)}")
|
|
|
|
@TradesPageRoute.route("/trade/<int:userid>/inventory", methods=['GET'])
|
|
@auth.authenticated_required_api
|
|
def getInventory(userid : int):
|
|
pageNumber = request.args.get("page", default=1, type=int)
|
|
|
|
TargetUser : User = User.query.filter_by(id=userid).first()
|
|
if TargetUser is None:
|
|
return abort(404)
|
|
if TargetUser.accountstatus == 4 or TargetUser.accountstatus == 3:
|
|
return abort(404)
|
|
|
|
UserAssets : list[UserAsset] = UserAsset.query.join(Asset, UserAsset.assetid == Asset.id).filter(UserAsset.userid == TargetUser.id, Asset.is_limited == True, Asset.is_for_sale == False).paginate(page=pageNumber, error_out=False, max_per_page=12).items
|
|
ReturnInfoData = []
|
|
|
|
AssetRAPCache = {}
|
|
def GetAssetRAP( AssetID : int ):
|
|
if AssetID in AssetRAPCache:
|
|
return AssetRAPCache[AssetID]
|
|
AssetRAPObj : AssetRap | None = AssetRap.query.filter_by(assetid=AssetID).first()
|
|
if AssetRAPObj is None:
|
|
return 0
|
|
AssetRAPCache[AssetID] = AssetRAPObj.rap
|
|
return AssetRAPCache[AssetID]
|
|
|
|
for UserAssetObj in UserAssets:
|
|
AssetObj : Asset = Asset.query.filter_by(id=UserAssetObj.assetid).first()
|
|
if AssetObj is None:
|
|
continue
|
|
ReturnInfoData.append({
|
|
"id": AssetObj.id,
|
|
"name": AssetObj.name,
|
|
"serialNumber": UserAssetObj.serial,
|
|
"uaid": UserAssetObj.id,
|
|
"rap": GetAssetRAP(AssetObj.id),
|
|
})
|
|
isThereNextPage = len(UserAssets) == 12
|
|
return jsonify({
|
|
"data": ReturnInfoData,
|
|
"nextPage": isThereNextPage
|
|
})
|
|
|