from app.services.economy import GetUserBalance, DecrementTargetBalance, IncrementTargetBalance from app.enums.TransactionType import TransactionType from app.models.exchange_offer import ExchangeOffer from app.models.user import User from app.util import auth, transactions, redislock, websiteFeatures from app.extensions import db, limiter, redis_controller from datetime import datetime, timedelta from app.pages.messages.messages import CreateSystemMessage from sqlalchemy import case from flask import Blueprint, render_template, request, redirect, url_for, jsonify, make_response, flash, abort import redis_lock CurrencyExchangeRoute = Blueprint("currencyexchange", __name__, url_prefix="/currency-exchange") @CurrencyExchangeRoute.errorhandler(429) def ratelimit_handler(e): flash("You are being ratelimited.", "danger") return redirect(request.referrer) @CurrencyExchangeRoute.route("/", methods=["GET"]) @auth.authenticated_required def index(): CategoryType : str = request.args.get( key = "category", default = "all", type = str ) SortType : str = request.args.get( key = "sort", default = "best", type = str ) PageNumber : int = request.args.get( key = "page", default = 1, type = int ) if CategoryType not in ["tr", "rt", "all"]: CategoryType = "all" if SortType not in ["best", "worst", "created"]: SortType = "best" if PageNumber < 1: PageNumber = 1 QueryObj = ExchangeOffer.query QueryObj = QueryObj.filter_by( reciever_id = None ) if CategoryType == "tr": QueryObj = QueryObj.filter_by( offer_currency_type = 1 ) elif CategoryType == "rt": QueryObj = QueryObj.filter_by( offer_currency_type = 0 ) if SortType == "best": if CategoryType == "rt": QueryObj = QueryObj.order_by( ExchangeOffer.ratio.asc() ) elif CategoryType == "tr": QueryObj = QueryObj.order_by( ExchangeOffer.ratio.desc() ) else: QueryObj = QueryObj.order_by( ExchangeOffer.worth.desc() ) elif SortType == "worst": if CategoryType == "rt": QueryObj = QueryObj.order_by( ExchangeOffer.ratio.desc() ) elif CategoryType == "tr": QueryObj = QueryObj.order_by( ExchangeOffer.ratio.asc() ) else: QueryObj = QueryObj.order_by( ExchangeOffer.worth.asc() ) else: QueryObj = QueryObj.order_by( ExchangeOffer.created_at.desc() ) QueryObj = QueryObj.paginate( page = PageNumber, per_page = 15, error_out = False ) for Offer in QueryObj.items: Offer : ExchangeOffer if Offer.expires_at < datetime.utcnow(): IncrementTargetBalance(Offer.creator, Offer.offer_value, Offer.offer_currency_type) transactions.CreateTransaction( Reciever = Offer.creator, Sender = User.query.filter_by(id = 1).first(), CurrencyAmount = Offer.offer_value, CurrencyType = Offer.offer_currency_type, TransactionType = TransactionType.Sale, AssetId = None, CustomText = "Offer expired." ) db.session.delete(Offer) db.session.commit() continue return render_template( "currencyexchange/index.html", offers = QueryObj.items, hasNextPage = QueryObj.has_next, hasPrevPage = QueryObj.has_prev, PageCategory = CategoryType, SortType = SortType, PageNumber = PageNumber ) @CurrencyExchangeRoute.route("/view/", methods=["GET"]) @auth.authenticated_required def viewOffer(OfferId : int): OfferObj : ExchangeOffer = ExchangeOffer.query.filter_by( id = OfferId ).first() if OfferObj is None: return abort(404) AuthenticatedUser : User = auth.GetCurrentUser() return render_template( "currencyexchange/view.html", offer = OfferObj, AuthenticatedUser = AuthenticatedUser ) @CurrencyExchangeRoute.route("/delete/", methods=["POST"]) @auth.authenticated_required def deleteOffer(OfferId : int): OfferObj : ExchangeOffer = ExchangeOffer.query.filter_by( id = OfferId ).first() if OfferObj is None: return abort(404) AuthenticatedUser : User = auth.GetCurrentUser() if OfferObj.creator_id != AuthenticatedUser.id: return abort(403) if websiteFeatures.GetWebsiteFeature("CurrencyExchange") is False: flash("Currency exchange is currently disabled.", "error") return redirect(f"/currency-exchange/view/{OfferId}") try: with redis_lock.Lock( redis_client = redis_controller, name = f"currency_exchange_order:{OfferId}", expire = 15, auto_renewal = True ): OfferObj : ExchangeOffer = ExchangeOffer.query.filter_by( id = OfferId ).first() if OfferObj.reciever_id is not None: return abort(403) IncrementTargetBalance(OfferObj.creator, OfferObj.offer_value, OfferObj.offer_currency_type) transactions.CreateTransaction( Reciever = OfferObj.creator, Sender = User.query.filter_by(id = 1).first(), CurrencyAmount = OfferObj.offer_value, CurrencyType = OfferObj.offer_currency_type, TransactionType = TransactionType.Sale, AssetId = None, CustomText = "Offer cancelled." ) db.session.delete(OfferObj) db.session.commit() flash("Offer successfully deleted.", "success") return redirect("/currency-exchange/") except AssertionError: flash("Failed to delete offer, try again later.", "error") return redirect(f"/currency-exchange/view/{OfferId}") @CurrencyExchangeRoute.route("/fill/", methods=["POST"]) @auth.authenticated_required @limiter.limit("40/minute") def fillOffer(OfferId : int): OfferObj : ExchangeOffer = ExchangeOffer.query.filter_by( id = OfferId ).first() if OfferObj is None: return abort(404) if websiteFeatures.GetWebsiteFeature("CurrencyExchange") is False: flash("Currency exchange is currently disabled.", "error") return redirect(f"/currency-exchange/view/{OfferId}") LockName = f"currency_exchange_order:{str(OfferId)}" try: with redis_lock.Lock( redis_client = redis_controller, name = LockName, expire = 15, auto_renewal = True ): OfferObj : ExchangeOffer = ExchangeOffer.query.filter_by( id = OfferId ).first() AuthenticatedUser : User = auth.GetCurrentUser() if OfferObj.creator_id == AuthenticatedUser.id: return abort(403) if OfferObj.reciever_id is not None: return abort(403) if OfferObj.expires_at < datetime.utcnow(): return abort(403) SendingCurrencyType : int = 0 if OfferObj.offer_currency_type == 1 else 1 UserRobuxBal, UserTicketsBal = GetUserBalance(AuthenticatedUser) if SendingCurrencyType == 0: if UserRobuxBal < OfferObj.receive_value: flash("You do not have enough Robux to fill this offer.", "error") return redirect(f"/currency-exchange/view/{OfferId}") else: if UserTicketsBal < OfferObj.receive_value: flash("You do not have enough Tickets to fill this offer.", "error") return redirect(f"/currency-exchange/view/{OfferId}") transactions.CreateTransaction( Reciever = OfferObj.creator, Sender = User.query.filter_by(id = 1).first(), # Remain anonymous CurrencyAmount = OfferObj.receive_value, CurrencyType = SendingCurrencyType, TransactionType = TransactionType.Sale, AssetId = None, CustomText = f"Currency Exchange Order ({OfferId}) Filled" ) transactions.CreateTransaction( Reciever = AuthenticatedUser, Sender = User.query.filter_by(id = 1).first(), # Remain anonymous CurrencyAmount = OfferObj.offer_value, CurrencyType = OfferObj.offer_currency_type, TransactionType = TransactionType.Sale, AssetId = None, CustomText = f"Currency Exchange Order ({OfferId}) Filled" ) transactions.CreateTransaction( Reciever = User.query.filter_by(id = 1).first(), # Remain anonymous Sender = AuthenticatedUser, CurrencyAmount = OfferObj.receive_value, CurrencyType = SendingCurrencyType, TransactionType = TransactionType.Purchase, AssetId = None, CustomText = f"Currency Exchange Order ({OfferId}) Filled" ) DecrementTargetBalance(AuthenticatedUser, OfferObj.receive_value, SendingCurrencyType) IncrementTargetBalance(AuthenticatedUser, OfferObj.offer_value, OfferObj.offer_currency_type) IncrementTargetBalance(OfferObj.creator, OfferObj.receive_value, SendingCurrencyType) OfferObj.reciever_id = AuthenticatedUser.id db.session.commit() CreateSystemMessage( subject = "Offer Filled", message = f"Your currency exchange offer ({OfferId}) has been filled, you have received {OfferObj.receive_value} {'Robux' if OfferObj.offer_currency_type == 1 else 'Tickets'} in exchange for {OfferObj.offer_value} {'Robux' if OfferObj.offer_currency_type == 0 else 'Tickets'}.", userid = OfferObj.creator_id ) flash("Offer successfully filled.", "success") return redirect(f"/currency-exchange/view/{OfferId}") except AssertionError: flash("Failed to fill offer, try again later.", "error") return redirect(f"/currency-exchange/view/{OfferId}") @CurrencyExchangeRoute.route("/create", methods=["GET"]) @auth.authenticated_required def createOfferPage(): return render_template("currencyexchange/create.html") @CurrencyExchangeRoute.route("/create", methods=["POST"]) @auth.authenticated_required @limiter.limit("40/minute") def createOffer(): AuthenticatedUser : User = auth.GetCurrentUser() OfferCurrencyType : int = request.form.get( key = "offer-currency-type", default = None, type = int ) OfferValue : int = request.form.get( key = "offer-currency-amount", default = None, type = int ) ReceiveValue : int = request.form.get( key = "exchange-currency-amount", default = None, type = int ) if OfferCurrencyType is None or OfferValue is None or ReceiveValue is None: flash("Invalid request", "error") return redirect("/currency-exchange/create") if OfferValue <= 0 or ReceiveValue <= 0: flash("Invalid request", "error") return redirect("/currency-exchange/create") if websiteFeatures.GetWebsiteFeature("CurrencyExchange") is False: flash("Currency exchange is currently disabled.", "error") return redirect(f"/currency-exchange/create") try: with redis_lock.Lock( redis_client = redis_controller, name = f"currency_exchange_create_offer:{AuthenticatedUser.id}", expire = 15, auto_renewal = True ): ActiveOffers : int = ExchangeOffer.query.filter_by( creator_id = AuthenticatedUser.id, reciever_id = None ).filter( ExchangeOffer.expires_at > datetime.utcnow() ).count() if ActiveOffers >= 25: flash("You have too many active offers, please cancel them to create a new offer.", "error") return redirect("/currency-exchange/create") if OfferCurrencyType == 0: if OfferValue < 5: flash("You can only create offers of 5 Robux or more.", "error") return redirect("/currency-exchange/create") if OfferValue > 10000: flash("You can only create offers up to 10,000 Robux.", "error") return redirect("/currency-exchange/create") Ratio = ReceiveValue / OfferValue else: if OfferValue < 10: flash("You can only create offers of 10 Tickets or more.", "error") return redirect("/currency-exchange/create") if OfferValue > 100000: flash("You can only create offers up to 100,000 Tickets.", "error") return redirect("/currency-exchange/create") Ratio = OfferValue / ReceiveValue if Ratio < 2 or Ratio > 20: flash("Bad exchange ratio", "error") return redirect("/currency-exchange/create") UserRobuxBal, UserTicketsBal = GetUserBalance(AuthenticatedUser) if OfferCurrencyType == 0 and UserRobuxBal < OfferValue: flash("You do not have enough Robux to create this offer.", "error") return redirect("/currency-exchange/create") elif OfferCurrencyType == 1 and UserTicketsBal < OfferValue: flash("You do not have enough Tickets to create this offer.", "error") return redirect("/currency-exchange/create") DecrementTargetBalance(AuthenticatedUser, OfferValue, OfferCurrencyType) transactions.CreateTransaction( Reciever = User.query.filter_by(id=1).first(), Sender = AuthenticatedUser, CurrencyAmount = OfferValue, CurrencyType = OfferCurrencyType, TransactionType = TransactionType.Purchase, CustomText = "Created Exchange Order" ) NewOffer : ExchangeOffer = ExchangeOffer( creator_id = AuthenticatedUser.id, offer_value = OfferValue, receive_value = ReceiveValue, offer_currency_type = OfferCurrencyType, created_at = datetime.utcnow(), expires_at = datetime.utcnow() + timedelta(days=31), ratio = Ratio, worth = abs( 20 - Ratio ) if OfferCurrencyType == 0 else ( 20 - abs( Ratio - 20 ) ) ) db.session.add(NewOffer) db.session.commit() flash("Offer created successfully", "success") return redirect("/currency-exchange/") except AssertionError: flash("Failed to create offer, try again later.", "error") return redirect("/currency-exchange/create")