This commit is contained in:
copyrighttxt
2026-07-06 13:28:00 -04:00
commit 05211f73ef
441 changed files with 94128 additions and 0 deletions
+133
View File
@@ -0,0 +1,133 @@
{% extends '__layout__.html' %}
{% block title %}New Trade{% endblock %}
{% block head %}
<style>
.text-secondary {
color: rgb(199, 199, 199) !important;
}
.text-robux {
color: rgb(26, 212, 103) !important;
font-weight: 600;
}
.bg-dark {
background-color: rgb(30,30,30) !important;
}
</style>
<script src="/static/js/newTrade.js"></script>
{% endblock %}
{% block content %}
<div class="col-md-2 mb-2" id="item-card-template" style="display: none;">
<div class="position-relative">
<img src="" width="100px" height="100px" class="border p-1 item-card-image">
<p class="position-absolute bg-dark p-1 item-card-serial text-white" style="bottom: -16px;left: 0px;font-size: 12px;">#2</p>
</div>
<a class="text-center m-0 d-block text-decoration-none item-card-name" href="/catalog/45/" target="_blank" style="font-size: 12px;width: 100px;">Test</a>
</div>
<div id="trade-info" data-totp-enabled="{{AuthenticatedUser.TOTPEnabled}}" data-requester-userid="{{currentuser.id}}" data-target-userid="{{TargetUser.id}}" data-xcsrf-token="{{csrf_token()}}"></div>
<div class="container position-relative" style="min-height: 100vh;margin-top: 100px;">
<div class="d-flex align-items-center">
<h1 class="m-0 me-3">New Trade</h1>
<a class="m-0 text-secondary text-decoration-none" href="/users/{{TargetUser.id}}/profile" target="_blank">Trading with {{TargetUser.username}}<img class="rounded-5 ms-2" width="30" height="30" src="/Thumbs/Head.ashx?x=48&y=48&userId={{TargetUser.id}}"></a>
</div>
<div class="linebreak"></div>
<div class="row">
<div class="col-md-4">
<div class="rounded bg-dark p-3">
<div class="d-flex">
<h4>Your Offer</h4>
<div class="ms-auto">
<p class="m-0 text-secondary">Value: <span class="text-robux" id="requester-offer-value">R$ 0</span></p>
</div>
</div>
<div class="mt-2 row" id="requester-item-offer-container">
<div class="col p-1">
<div class="border rounded w-100 h-100" style="aspect-ratio: 1/1;">
<img width="100%" height="100%" class="item-image d-none">
</div>
</div>
<div class="col p-1">
<div class="border rounded w-100 h-100" style="aspect-ratio: 1/1;">
<img width="100%" height="100%" class="item-image d-none">
</div>
</div>
<div class="col p-1">
<div class="border rounded w-100 h-100" style="aspect-ratio: 1/1;">
<img width="100%" height="100%" class="item-image d-none">
</div>
</div>
<div class="col p-1">
<div class="border rounded w-100 h-100" style="aspect-ratio: 1/1;">
<img width="100%" height="100%" class="item-image d-none">
</div>
</div>
</div>
<div class="d-flex align-items-center justify-content-center">
<p class="m-0 text-secondary me-2" style="font-size: 19px;">*Plus <span class="text-robux">R$</span></p>
<input type="number" class="form-control mt-2" id="requester-offer-robux-input" placeholder="Enter Amount" style="max-width: 150px;">
</div>
</div>
<div class="linebreak"></div>
<div class="rounded bg-dark p-3">
<div class="d-flex">
<h4>Your Request</h4>
<div class="ms-auto">
<p class="m-0 text-secondary">Value: <span class="text-robux" id="target-offer-value">R$ 0</span></p>
</div>
</div>
<div class="mt-2 row" id="target-item-offer-container">
<div class="col p-1">
<div class="border rounded w-100 h-100" style="aspect-ratio: 1/1;">
<img width="100%" height="100%" class="item-image d-none">
</div>
</div>
<div class="col p-1">
<div class="border rounded w-100 h-100" style="aspect-ratio: 1/1;">
<img width="100%" height="100%" class="item-image d-none">
</div>
</div>
<div class="col p-1">
<div class="border rounded w-100 h-100" style="aspect-ratio: 1/1;">
<img width="100%" height="100%" class="item-image d-none">
</div>
</div>
<div class="col p-1">
<div class="border rounded w-100 h-100" style="aspect-ratio: 1/1;">
<img width="100%" height="100%" class="item-image d-none">
</div>
</div>
</div>
<div class="d-flex align-items-center justify-content-center">
<p class="m-0 text-secondary me-2" style="font-size: 19px;">*Plus <span class="text-robux">R$</span></p>
<input type="number" class="form-control mt-2" id="target-offer-robux-input" placeholder="Enter Amount" style="max-width: 150px;">
</div>
</div>
<div class="mt-2">
<input type="text" class="form-control mb-2" id="otp-code-input" style="display: none;" placeholder="2FA Authenticator Code">
<button class="btn btn-primary fw-bold w-100" id="trade-submit-btn">Send Trade Request</button>
<p class="text-danger m-0" id="submit-btn-feedback" style="font-size: 13px; display: none;">400: Invalid Data</p>
</div>
<p class="text-secondary mt-2" style="font-size: 12px;">* Vortexi will take a 30% fee from the amount </p>
</div>
<div class="col-md-8">
<div>
<h5>My Inventory</h5>
<div class="row mb-2" id="requester-inventory-container">
</div>
<div class="align-items-center d-flex justify-content-center">
<button class="btn btn-sm text-white btn-primary me-2 pagination-back-btn" disabled>Previous</button><p class="m-0 pagination-page-number">Page 1</p><button class="btn btn-sm text-white btn-primary ms-2 pagination-next-btn" disabled>Next</button>
</div>
</div>
<div class="linebreak"></div>
<div>
<h5>{{TargetUser.username}}'s Inventory</h5>
<div class="row mb-2" id="target-inventory-container">
</div>
<div class="align-items-center d-flex justify-content-center">
<button class="btn btn-sm text-white btn-primary me-2 pagination-back-btn" disabled>Previous</button><p class="m-0 pagination-page-number">Page 1</p><button class="btn btn-sm text-white btn-primary ms-2 pagination-next-btn" disabled>Next</button>
</div>
</div>
</div>
</div>
</div>
<div class="position-absolute w-100 h-100" id="transparent-background-top" style="top: 0;left: 0;background-color: rgba(0,0,0,0.5);display: none;"></div>
{% endblock %}
+65
View File
@@ -0,0 +1,65 @@
{% extends '__layout__.html' %}
{% block title %}Trades{% endblock %}
{% block head %}
<style>
.text-secondary {
color: rgb(199, 199, 199) !important;
}
</style>
{% endblock %}
{% block content %}
<div class="container" style="min-height: 100vh; margin-top: 100px;max-width: 1000px;">
<div class="d-flex align-items-center">
<h1>Trades</h1>
<div class="ms-auto">
<select class="form-control" id="change-category-select">
<option value="inbound" {%if PageCategory == "inbound"%}selected{%endif%}>Inbound</option>
<option value="outbound" {%if PageCategory == "outbound"%}selected{%endif%}>Outbound</option>
<option value="completed" {%if PageCategory == "completed"%}selected{%endif%}>Completed</option>
<option value="inactive" {%if PageCategory == "inactive"%}selected{%endif%}>Inactive</option>
</select>
</div>
</div>
<table class="table table-dark table-striped">
<thead>
<tr class="rounded-top">
<th scope="col" style="width: 5%;">ID</th>
<th scope="col" style="width: 10%;">Date</th>
<th scope="col" style="width: 10%;">Expires</th>
<th scope="col" style="width: 40%;">Trade Partner</th>
<th scope="col" style="width: 15%;">Status</th>
<th scope="col" style="width: 10%;">Action</th>
</tr>
</thead>
<tbody class="table-group-divider" style="border-color: rgb(20,20,20);">
{% for TradeObj in TradeInfo: %}
<tr>
<th scope="row">{{TradeObj.TradeID}}</th>
<td>{{TradeObj.Created}}</td>
<td>{{TradeObj.Expiration}}</td>
<td>
<a href="/users/{{TradeObj.OppositeUser.id}}/profile" class="text-decoration-none text-truncate">
<img class="rounded" style="aspect-ratio: 1/1;width: 35px;" src="/Thumbs/Head.ashx?x=48&y=48&userId={{TradeObj.OppositeUser.id}}" alt="{{TradeObj.OppositeUser.username}}">
{{TradeObj.OppositeUser.username}}
</a>
</td>
<td>{{TradeObj.TradeStatus.name}}</td>
<td><a href="/trade/view/{{TradeObj.TradeID}}" class="text-decoration-none">View</a></td>
</tr>
{% endfor %}
</tbody>
</table>
{% if len(TradeInfo) == 0: %}
<div class="text-center">
<p class="text-secondary">No trades found</p>
</div>
{%endif%}
<div class="align-items-center d-flex justify-content-center mt-2 mb-2">
<a class="ms-auto m-0 text-decoration-none {% if not TradeListObj.has_prev %}text-secondary{%endif%}" {% if TradeListObj.has_prev %}href="/trade?page={{TradeListObj.prev_num}}&category={{PageCategory}}"{%endif%}>Previous</a>
<p class="ms-2 me-2 text-white m-0">Page {{TradeListObj.page}} of {{TradeListObj.pages}}</p>
<a class="me-auto m-0 text-decoration-none {% if not TradeListObj.has_next %}text-secondary{%endif%}" {% if TradeListObj.has_next %}href="/trade?page={{TradeListObj.next_num}}&category={{PageCategory}}"{%endif%}>Next</a>
</div>
</div>
<script>const s = document.getElementById("change-category-select");s.addEventListener("change", function() {var u = new URL(window.location.href);var sb = s.value;u.searchParams.set("category", sb);u.searchParams.set("page",1);window.location.href = u;});</script>
{%endblock%}
+550
View File
@@ -0,0 +1,550 @@
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
})
+134
View File
@@ -0,0 +1,134 @@
{% extends '__layout__.html' %}
{% block title %}View Trade{% endblock %}
{% block head %}
<style>
.text-secondary {
color: rgb(199, 199, 199) !important;
}
.text-robux {
color: rgb(26, 212, 103) !important;
font-weight: 600;
}
.bg-dark {
background-color: rgb(30,30,30) !important;
}
.messagealerts {
background: rgba(255, 0, 0, 0.2);
color: rgb(226, 226, 226);
word-wrap: break-word;
}
</style>
{% endblock %}
{% block content %}
<div class="ms-auto me-auto" style="width: 800px;min-height: 100vh;margin-top: 100px;">
<a class="m-0 mb-2 text-white text-decoration-none" href="/trade"> < Return to trades </a>
{% with messages = get_flashed_messages() %}
{% if messages %}
<div>
{% for message in messages %}
<div class="alert border border-danger p-2 text-center messagealerts">
{{ message }}
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<div class="row">
<div class="col-3 border-end ps-4 pe-4 position-relative" style="min-height: 350px;">
<img class="rounded-2 mt-3" width="100%" style="aspect-ratio: 1/1;" src="/Thumbs/Head.ashx?x=180&y=180&userId={{OppositeUser.id}}" alt="{{OppositeUser.username}}">
<p class="m-0 text-center text-white" style="font-size: 13px;">Your trade with <a class="text-decoration-none" href="/users/{{OppositeUser.id}}/profile">{{OppositeUser.username}}</a> is {{TradeObj.status.name}}</p>
<div class="mt-auto position-absolute" style="bottom: 0;width: calc(100% - 3rem);">
{% if TradeObj.status.value == 0: %}
{% if SenderUser.id == currentuser.id: %}
<form action="/trade/{{TradeObj.id}}/cancel" method="post">
<input type="hidden" name="csrf_token" value="{{csrf_token()}}">
<button class="btn btn-secondary w-100 btn-sm">Cancel Trade</button>
</form>
{%endif%}
{% if RecipientUser.id == currentuser.id: %}
<form action="/trade/{{TradeObj.id}}/accept" method="post">
{% if AuthenticatedUser.TOTPEnabled: %}
<input type="text" placeholder="2FA Code" name="totpcode" class="form-control w-100 mt-1">
{%endif%}
<input type="hidden" name="csrf_token" value="{{csrf_token()}}">
<button class="btn btn-success w-100 btn-sm mt-1" onclick="return confirm('Are you sure you want to accept this trade?')">Accept Trade</button>
</form>
<form action="/trade/{{TradeObj.id}}/decline" method="post">
<button class="btn btn-danger w-100 btn-sm mt-2">Decline Trade</button>
</form>
{%endif%}
{%endif%}
</div>
</div>
<div class=col-9>
<div class="d-flex">
{% if SenderUser.id != currentuser.id: %}
<h5>Items you will receive</h5>
{%else%}
<h5>Items you will send</h5>
{% endif %}
<p class="text-secondary m-0 ms-auto">Value: <span class="text-robux">R$ {{SenderOfferValue}}</span></p>
</div>
<div class="row">
{% for i in range(4): %}
{% if (len(SenderItems) - 1 - i) >= 0: %}
<div class="col">
<div class="border position-relative">
<img src="/Thumbs/Asset.ashx?x=100&y=100&assetId={{SenderItems[i].AssetId}}" width="100%" style="aspect-ratio: 1/1;" alt="{{SenderItems[i].Name}}">
{% if SenderItems[i].serial != None: %}
<p class="position-absolute bg-dark p-1 item-card-serial text-white" style="bottom: -16px;left: 0px;font-size: 12px;">#{{SenderItems[i].serial}}</p>
{%endif%}
</div>
<a class="m-0 text-decoration-none text-center w-100 d-block" style="font-size: 12px;" target="_blank" href="/catalog/{{SenderItems[i].AssetId}}/">{{SenderItems[i].Name}}</a>
</div>
{%else%}
<div class="col">
<div class="border w-100" style="aspect-ratio: 1/1;">
</div>
</div>
{%endif%}
{%endfor%}
<div class="col">
<div width="100%" class="border text-robux p-2 text-center d-flex align-items-center justify-content-center" style="aspect-ratio: 1/1;">
{%if TradeObj.sender_userid_robux > 0 %}<h5 class="text-robux m-0">{{TradeObj.sender_userid_robux}} ROBUX</h5>{%endif%}
</div>
</div>
</div>
<div class="linebreak"></div>
<div class="d-flex">
{% if SenderUser.id == currentuser.id: %}
<h5>Items you will receive</h5>
{%else%}
<h5>Items you will send</h5>
{% endif %}
<p class="text-secondary m-0 ms-auto">Value: <span class="text-robux">R$ {{RecipientOfferValue}}</span></p>
</div>
<div class="row">
{% for i in range(4): %}
{% if (len(RecipientItems) - 1 - i) >= 0: %}
<div class="col">
<div class="border position-relative">
<img src="/Thumbs/Asset.ashx?x=100&y=100&assetId={{RecipientItems[i].AssetId}}" width="100%" style="aspect-ratio: 1/1;" alt="{{RecipientItems[i].Name}}">
{% if RecipientItems[i].serial != None: %}
<p class="position-absolute bg-dark p-1 item-card-serial text-white" style="bottom: -16px;left: 0px;font-size: 12px;">#{{RecipientItems[i].serial}}</p>
{%endif%}
</div>
<a class="m-0 text-decoration-none text-center w-100 d-block" style="font-size: 12px;" target="_blank" href="/catalog/{{RecipientItems[i].AssetId}}/">{{RecipientItems[i].Name}}</a>
</div>
{%else%}
<div class="col">
<div class="border w-100" style="aspect-ratio: 1/1;">
</div>
</div>
{%endif%}
{%endfor%}
<div class="col">
<div width="100%" class="border text-robux p-2 text-center d-flex align-items-center justify-content-center" style="aspect-ratio: 1/1;">
{%if TradeObj.recipient_userid_robux > 0 %}<h5 class="text-robux m-0">{{TradeObj.recipient_userid_robux}} ROBUX</h5>{%endif%}
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}