add web
This commit is contained in:
@@ -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 %}
|
||||
@@ -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%}
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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 %}
|
||||
Reference in New Issue
Block a user