import re import uuid from datetime import timedelta from math import ceil, floor from passlib.hash import sha256_crypt import model import version from connection import check_missing_attributes, BadRequest, Forbidden, PreconditionFailed, NotFound from game import OWNABLE_NAME_PATTERN, BANK_NAME def login(json_request): check_missing_attributes(json_request, ['username', 'password']) username = json_request['username'] password = json_request['password'] session_id = model.login(username, password) if session_id: return {'session_id': session_id} else: return Forbidden('Invalid login data') def depot(json_request): check_missing_attributes(json_request, ['session_id']) user_id = model.get_user_id_by_session_id(json_request['session_id']) return {'data': model.get_user_ownership(user_id), 'own_wealth': f'{model.user_wealth(user_id):.2f}', 'banking_license': model.user_has_banking_license(user_id)} def global_variables(_json_request): return model.global_control_values() def register(json_request): check_missing_attributes(json_request, ['username', 'password']) username = json_request['username'].strip() if username == '': return BadRequest('Username can not be empty.') if model.user_exists(username): return BadRequest('User already exists.') if model.register(username, json_request['password']): return {'message': "successfully registered user"} else: return BadRequest('Registration not successful') def order(json_request): check_missing_attributes(json_request, ['buy', 'session_id', 'amount', 'ownable', 'time_until_expiration']) if not model.ownable_name_exists(json_request['ownable']): return BadRequest('This kind of object can not be ordered.') buy = json_request['buy'] sell = not buy if not isinstance(buy, bool): return BadRequest('`buy` must be a boolean') if 'ioc' in json_request: ioc = json_request['ioc'] if not isinstance(ioc, bool): raise BadRequest('IOC must be a boolean.') else: ioc = False session_id = json_request['session_id'] user_id = model.get_user_id_by_session_id(session_id) amount = json_request['amount'] try: amount = float(amount) # so that something like 5e6 also works but only integers if amount != round(amount): raise ValueError amount = round(amount) except ValueError: return BadRequest('Invalid amount.') if amount < 0: return BadRequest('You can not order a negative amount.') if amount < 1: return BadRequest('The minimum order size is 1.') ownable_name = json_request['ownable'] time_until_expiration = float(json_request['time_until_expiration']) if time_until_expiration < 0: return BadRequest('Invalid expiration time.') ownable_id = model.ownable_id_by_name(ownable_name) model.own(user_id, ownable_name) ownership_id = model.get_ownership_id(ownable_id, user_id) try: if json_request['limit'] == '': limit = None elif json_request['limit'] is None: limit = None else: if buy: limit = floor(float(json_request['limit']) * 10000) / 10000 else: limit = ceil(float(json_request['limit']) * 10000) / 10000 except ValueError: # for example when float fails return BadRequest('Invalid limit.') except KeyError: # for example when limit was not specified limit = None if limit is not None and limit < 0: return BadRequest('Limit must not be negative.') if 'stop_loss' in json_request: if json_request['stop_loss'] == '': stop_loss = None elif json_request['stop_loss'] is None: stop_loss = None else: stop_loss = json_request['stop_loss'] else: stop_loss = None if stop_loss and limit is None: return BadRequest('You need to specify a limit for stop-loss orders') if ioc and stop_loss: raise BadRequest('Stop loss orders can not be IOC orders.') if sell: if not model.user_has_at_least_available(amount, user_id, ownable_id): return BadRequest('You can not sell more than you own.') try: expiry = model.current_db_timestamp() + timedelta(minutes=time_until_expiration).total_seconds() except OverflowError: return BadRequest('The expiration time is too far in the future.') model.place_order(buy, ownership_id, limit, stop_loss, amount, expiry, ioc) return {'message': "Order placed."} def gift(json_request): check_missing_attributes(json_request, ['session_id', 'amount', 'object_name', 'username']) if not model.ownable_name_exists(json_request['object_name']): return BadRequest('This kind of object can not be given away.') if json_request['username'] == BANK_NAME or not model.user_exists(json_request['username']): return BadRequest('There is no user with this name.') try: amount = float(json_request['amount']) except ValueError: return BadRequest('Invalid amount.') ownable_id = model.ownable_id_by_name(json_request['object_name']) sender_id = model.get_user_id_by_session_id(json_request['session_id']) if model.available_amount(sender_id, ownable_id) == 0: return BadRequest('You do not own any of these.') if not model.user_has_at_least_available(amount, sender_id, ownable_id): # for example if you have a 1.23532143213 Kollar and want to give them all away amount = model.available_amount(sender_id, ownable_id) recipient_id = model.get_user_id_by_name(json_request['username']) model.send_ownable(sender_id, recipient_id, ownable_id, amount) return {'message': f"Sent {amount} {model.ownable_name_by_id(ownable_id)} to {model.user_name_by_id(recipient_id)}."} def orders(json_request): check_missing_attributes(json_request, ['session_id']) data = model.get_user_orders(model.get_user_id_by_session_id(json_request['session_id'])) return {'data': data} def loans(json_request): check_missing_attributes(json_request, ['session_id']) data = model.get_user_loans(model.get_user_id_by_session_id(json_request['session_id'])) return {'data': data} def bonds(json_request): if 'issuer' in json_request: issuer_id = model.get_user_id_by_name(json_request['issuer']) else: issuer_id = None if 'only_next_mro_qualified' in json_request: only_next_mro_qualified = json_request['only_next_mro_qualified'] if isinstance(only_next_mro_qualified, str): raise BadRequest else: only_next_mro_qualified = False data = model.bonds(issuer_id, only_next_mro_qualified) return {'data': data} def orders_on(json_request): check_missing_attributes(json_request, ['session_id', 'ownable']) if not model.ownable_name_exists(json_request['ownable']): return BadRequest('This kind of object can not be ordered.') user_id = model.get_user_id_by_session_id(json_request['session_id']) ownable_id = model.ownable_id_by_name(json_request['ownable']) data = model.get_ownable_orders(user_id, ownable_id) return {'data': data} def old_orders(json_request): check_missing_attributes(json_request, ['session_id', 'include_canceled', 'include_executed', 'limit']) include_executed = json_request['include_executed'] include_canceled = json_request['include_canceled'] user_id = model.get_user_id_by_session_id(json_request['session_id']) limit = json_request['limit'] data = model.get_old_orders(user_id, include_executed, include_canceled, limit) return {'data': data} def cancel_order(json_request): check_missing_attributes(json_request, ['session_id', 'order_id']) if not model.user_has_order_with_id(json_request['session_id'], json_request['order_id']): return BadRequest('You do not have an order with that number.') model.delete_order(json_request['order_id'], 'Canceled') return {'message': "Successfully deleted order"} def change_password(json_request): check_missing_attributes(json_request, ['session_id', 'password']) salt = str(uuid.uuid4()) hashed_password = sha256_crypt.encrypt(json_request['password'] + salt) model.change_password(json_request['session_id'], hashed_password, salt) model.sign_out_user(json_request['session_id']) return {'message': "Successfully changed password"} def logout(json_request): check_missing_attributes(json_request, ['session_id']) model.sign_out_user(json_request['session_id']) return {'message': "Successfully logged out"} def buy_banking_license(json_request): check_missing_attributes(json_request, ['session_id']) user_id = model.get_user_id_by_session_id(json_request['session_id']) if model.user_has_banking_license(user_id): raise PreconditionFailed('You already have a banking license.') price = model.global_control_value('banking_license_price') if model.user_money(user_id) < price: raise PreconditionFailed('You do not have enough money.') model.send_ownable(user_id, model.bank_id(), model.currency_id(), price) model.assign_banking_licence(user_id) return {'message': "Successfully bought banking license"} def news(_json_request): return {'data': model.news()} def tender_calendar(_json_request): return {'data': model.tender_calendar()} def tradables(_json_request): return {'data': model.ownables()} def trades(json_request): check_missing_attributes(json_request, ['session_id', 'limit']) return {'data': model.trades(model.get_user_id_by_session_id(json_request['session_id']), json_request['limit'])} def trades_on(json_request): check_missing_attributes(json_request, ['session_id', 'ownable', 'limit']) if not model.ownable_name_exists(json_request['ownable']): return BadRequest('This kind of object can not have transactions.') return {'data': model.trades_on(model.ownable_id_by_name(json_request['ownable']), json_request['limit'])} def leaderboard(_json_request): return {'data': model.leaderboard()} def take_out_personal_loan(json_request): check_missing_attributes(json_request, ['session_id', 'amount', ]) amount = json_request['amount'] if not isinstance(amount, float) or amount <= 0: raise BadRequest('Amount must be a number larger than 0') user_id = model.get_user_id_by_session_id(json_request['session_id']) model.take_out_personal_loan(user_id, amount) return {'message': "Successfully took out personal loan"} def issue_bond(json_request): check_missing_attributes(json_request, ['session_id', 'name', 'coupon', 'run_time']) user_id = model.get_user_id_by_session_id(json_request['session_id']) coupon = json_request['coupon'] if coupon == 'next_mro': coupon = model.next_mro_interest() else: try: coupon = float(coupon) except ValueError: raise BadRequest('Coupon must be a number.') ownable_name = json_request['name'] if not re.fullmatch(OWNABLE_NAME_PATTERN, ownable_name): raise BadRequest('Invalid name.') run_time = json_request['run_time'] if run_time == 'next_mro': maturity_dt = model.next_mro_dt() else: try: run_time = int(run_time) except ValueError: raise BadRequest('Run-time must be a positive integer number.') if run_time < 0: raise BadRequest('Run-time must be a positive integer number.') maturity_dt = model.current_db_timestamp() + 60 * run_time model.issue_bond(user_id, ownable_name, coupon, maturity_dt) return {'message': "Successfully issued bond"} def repay_loan(json_request): check_missing_attributes(json_request, ['session_id', 'amount', 'loan_id']) amount = json_request['amount'] user_id = model.get_user_id_by_session_id(json_request['session_id']) loan_id = json_request['loan_id'] if amount == 'all': amount = model.loan_remaining_amount(loan_id) if amount < 0: raise BadRequest('You can not repay negative amounts.') if model.user_money(user_id) < amount: raise PreconditionFailed('You do not have enough money.') if not model.loan_id_exists(loan_id) or model.loan_recipient_id(loan_id) != user_id: raise NotFound(f'You do not have a loan with that id.') loan_volume = model.loan_remaining_amount(loan_id) if loan_volume < amount: raise PreconditionFailed(f'You can not repay more than the remaining loan volume of {loan_volume}.') model.repay_loan(loan_id, amount, known_user_id=user_id) return {'message': "Successfully repayed loan"} def server_version(_json_request): return {'version': version.__version__} def _before_request(_json_request): # update tender calendar model.update_tender_calendar() for mro_id, expiry, min_interest, mro_dt in model.triggered_mros(): # pay interest rates for loans until this mro model.pay_loan_interest(until=mro_dt) # pay interest rates for bonds until this mro model.pay_bond_interest(until=mro_dt) # handle MROs model.mro(mro_id, expiry, min_interest) # pay interest rates for loans until current time model.pay_loan_interest() # pay interest rates for bonds until current time model.pay_bond_interest()