server_controller.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. import re
  2. import uuid
  3. from datetime import timedelta
  4. from math import ceil, floor
  5. from passlib.hash import sha256_crypt
  6. import model
  7. import version
  8. from connection import check_missing_attributes, BadRequest, Forbidden, PreconditionFailed, NotFound
  9. from game import OWNABLE_NAME_PATTERN, BANK_NAME
  10. def login(json_request):
  11. check_missing_attributes(json_request, ['username', 'password'])
  12. username = json_request['username']
  13. password = json_request['password']
  14. session_id = model.login(username, password)
  15. if session_id:
  16. return {'session_id': session_id}
  17. else:
  18. return Forbidden('Invalid login data')
  19. def depot(json_request):
  20. check_missing_attributes(json_request, ['session_id'])
  21. user_id = model.get_user_id_by_session_id(json_request['session_id'])
  22. return {'data': model.get_user_ownership(user_id),
  23. 'own_wealth': float(f'{model.user_wealth(user_id):.2f}'),
  24. 'minimum_reserve': model.required_minimum_reserve(user_id) if model.user_has_banking_license(user_id) else None,
  25. 'banking_license': model.user_has_banking_license(user_id)}
  26. def global_variables(_json_request):
  27. return model.global_control_values()
  28. def register(json_request):
  29. check_missing_attributes(json_request, ['username', 'password'])
  30. username = json_request['username'].strip()
  31. if username == '':
  32. return BadRequest('Username can not be empty.')
  33. if model.user_exists(username):
  34. return BadRequest('User already exists.')
  35. if model.register(username, json_request['password']):
  36. return {'message': "successfully registered user"}
  37. else:
  38. return BadRequest('Registration not successful')
  39. def order(json_request):
  40. check_missing_attributes(json_request, ['buy', 'session_id', 'amount', 'ownable', 'time_until_expiration'])
  41. if not model.ownable_name_exists(json_request['ownable']):
  42. return BadRequest('This kind of object can not be ordered.')
  43. buy = json_request['buy']
  44. sell = not buy
  45. if not isinstance(buy, bool):
  46. return BadRequest('`buy` must be a boolean')
  47. if 'ioc' in json_request:
  48. ioc = json_request['ioc']
  49. if not isinstance(ioc, bool):
  50. raise BadRequest('IOC must be a boolean.')
  51. else:
  52. ioc = False
  53. session_id = json_request['session_id']
  54. user_id = model.get_user_id_by_session_id(session_id)
  55. amount = json_request['amount']
  56. try:
  57. amount = float(amount) # so that something like 5e6 also works but only integers
  58. if amount != round(amount):
  59. raise ValueError
  60. amount = round(amount)
  61. except ValueError:
  62. return BadRequest('Invalid amount.')
  63. if amount < 0:
  64. return BadRequest('You can not order a negative amount.')
  65. if amount < 1:
  66. return BadRequest('The minimum order size is 1.')
  67. ownable_name = json_request['ownable']
  68. time_until_expiration = float(json_request['time_until_expiration'])
  69. if time_until_expiration < 0:
  70. return BadRequest('Invalid expiration time.')
  71. ownable_id = model.ownable_id_by_name(ownable_name)
  72. model.own(user_id, ownable_name)
  73. ownership_id = model.get_ownership_id(ownable_id, user_id)
  74. if 'limit' in json_request and 'stop_loss' not in json_request:
  75. raise BadRequest('Need to set stop_loss to either True or False for limit orders.')
  76. try:
  77. if json_request['limit'] == '':
  78. limit = None
  79. elif json_request['limit'] is None:
  80. limit = None
  81. else:
  82. if buy:
  83. limit = floor(float(json_request['limit']) * 10000) / 10000
  84. else:
  85. limit = ceil(float(json_request['limit']) * 10000) / 10000
  86. except ValueError: # for example when float fails
  87. return BadRequest('Invalid limit.')
  88. except KeyError: # for example when limit was not specified
  89. limit = None
  90. if limit is not None and limit < 0:
  91. return BadRequest('Limit must not be negative.')
  92. if 'stop_loss' in json_request:
  93. if json_request['stop_loss'] == '':
  94. stop_loss = None
  95. elif json_request['stop_loss'] is None:
  96. stop_loss = None
  97. else:
  98. stop_loss = json_request['stop_loss']
  99. else:
  100. stop_loss = None
  101. if stop_loss and limit is None:
  102. return BadRequest('You need to specify a limit for stop-loss orders')
  103. if ioc and stop_loss:
  104. raise BadRequest('Stop loss orders can not be IOC orders.')
  105. if sell:
  106. if not model.user_has_at_least_available(amount, user_id, ownable_id):
  107. return BadRequest('You can not sell more than you own (this also takes into account existing '
  108. 'sell orders and, if you are a bank, required minimum reserves at the ).')
  109. try:
  110. expiry = model.current_db_timestamp() + timedelta(minutes=time_until_expiration).total_seconds()
  111. except OverflowError:
  112. return BadRequest('The expiration time is too far in the future.')
  113. model.place_order(buy, ownership_id, limit, stop_loss, amount, expiry, ioc)
  114. return {'message': "Order placed."}
  115. def gift(json_request):
  116. check_missing_attributes(json_request, ['session_id', 'amount', 'object_name', 'username'])
  117. if not model.ownable_name_exists(json_request['object_name']):
  118. return BadRequest('This kind of object can not be given away.')
  119. if json_request['username'] == BANK_NAME or not model.user_exists(json_request['username']):
  120. return BadRequest('There is no user with this name.')
  121. try:
  122. amount = float(json_request['amount'])
  123. except ValueError:
  124. return BadRequest('Invalid amount.')
  125. ownable_id = model.ownable_id_by_name(json_request['object_name'])
  126. sender_id = model.get_user_id_by_session_id(json_request['session_id'])
  127. if model.user_available_ownable(sender_id, ownable_id) == 0:
  128. return BadRequest('You do not own any of these.')
  129. wealth = model.user_wealth(user_id=sender_id)
  130. if wealth < amount:
  131. raise PreconditionFailed(f'Your current wealth, computed from your owned equities minus debt, is {wealth}.\n'
  132. f'To protect you from giving away more than you can afford, you not gift any amounts larger than this to other players.'
  133. f'You can still try to emulate gifting by selling something at a low price and buying it back afterwards at a higher price.')
  134. recipient_id = model.get_user_id_by_name(json_request['username'])
  135. model.send_ownable(sender_id,
  136. recipient_id,
  137. ownable_id,
  138. amount)
  139. return {'message': f"Sent {amount} {model.ownable_name_by_id(ownable_id)} to {model.user_name_by_id(recipient_id)}."}
  140. def orders(json_request):
  141. check_missing_attributes(json_request, ['session_id'])
  142. data = model.get_user_orders(model.get_user_id_by_session_id(json_request['session_id']))
  143. return {'data': data}
  144. def loans(json_request):
  145. check_missing_attributes(json_request, ['session_id'])
  146. data = model.get_user_loans(model.get_user_id_by_session_id(json_request['session_id']))
  147. return {'data': data}
  148. def credits(json_request):
  149. if 'issuer' in json_request:
  150. issuer_id = model.get_user_id_by_name(json_request['issuer'])
  151. else:
  152. issuer_id = None
  153. if 'only_next_mro_qualified' in json_request:
  154. only_next_mro_qualified = json_request['only_next_mro_qualified']
  155. if isinstance(only_next_mro_qualified, str):
  156. raise BadRequest
  157. else:
  158. only_next_mro_qualified = False
  159. data = model.credits(issuer_id, only_next_mro_qualified)
  160. return {'data': data}
  161. def orders_on(json_request):
  162. check_missing_attributes(json_request, ['session_id', 'ownable'])
  163. if not model.ownable_name_exists(json_request['ownable']):
  164. return BadRequest('This kind of object can not be ordered.')
  165. user_id = model.get_user_id_by_session_id(json_request['session_id'])
  166. ownable_id = model.ownable_id_by_name(json_request['ownable'])
  167. data = model.get_ownable_orders(user_id, ownable_id)
  168. return {'data': data}
  169. def old_orders(json_request):
  170. check_missing_attributes(json_request, ['session_id', 'include_canceled', 'include_executed', 'limit'])
  171. include_executed = json_request['include_executed']
  172. include_canceled = json_request['include_canceled']
  173. user_id = model.get_user_id_by_session_id(json_request['session_id'])
  174. limit = json_request['limit']
  175. data = model.get_old_orders(user_id, include_executed, include_canceled, limit)
  176. return {'data': data}
  177. def cancel_order(json_request):
  178. check_missing_attributes(json_request, ['session_id', 'order_id'])
  179. if not model.user_has_order_with_id(json_request['session_id'], json_request['order_id']):
  180. return BadRequest('You do not have an order with that number.')
  181. model.delete_order(json_request['order_id'], 'Canceled')
  182. return {'message': "Successfully deleted order"}
  183. def change_password(json_request):
  184. check_missing_attributes(json_request, ['session_id', 'password'])
  185. salt = str(uuid.uuid4())
  186. hashed_password = sha256_crypt.encrypt(json_request['password'] + salt)
  187. model.change_password(json_request['session_id'], hashed_password, salt)
  188. model.sign_out_user(json_request['session_id'])
  189. return {'message': "Successfully changed password"}
  190. def logout(json_request):
  191. check_missing_attributes(json_request, ['session_id'])
  192. model.sign_out_user(json_request['session_id'])
  193. return {'message': "Successfully logged out"}
  194. def buy_banking_license(json_request):
  195. check_missing_attributes(json_request, ['session_id'])
  196. user_id = model.get_user_id_by_session_id(json_request['session_id'])
  197. if model.user_has_banking_license(user_id):
  198. raise PreconditionFailed('You already have a banking license.')
  199. price = model.global_control_value('banking_license_price')
  200. if model.user_available_money(user_id) < price:
  201. raise PreconditionFailed('You do not have enough money.')
  202. model.send_ownable(user_id, model.bank_id(), model.currency_id(), price)
  203. model.assign_banking_licence(user_id)
  204. return {'message': "Successfully bought banking license"}
  205. def news(_json_request):
  206. return {'data': model.news()}
  207. def tender_calendar(_json_request):
  208. return {'data': model.tender_calendar()}
  209. def tradables(_json_request):
  210. return {'data': model.ownables()}
  211. def trades(json_request):
  212. check_missing_attributes(json_request, ['session_id', 'limit'])
  213. return {'data': model.trades(model.get_user_id_by_session_id(json_request['session_id']), json_request['limit'])}
  214. def trades_on(json_request):
  215. check_missing_attributes(json_request, ['session_id', 'ownable', 'limit'])
  216. if not model.ownable_name_exists(json_request['ownable']):
  217. return BadRequest('This kind of object can not have transactions.')
  218. return {'data': model.trades_on(model.ownable_id_by_name(json_request['ownable']), json_request['limit'])}
  219. def leaderboard(_json_request):
  220. return {'data': model.leaderboard()}
  221. def take_out_personal_loan(json_request):
  222. check_missing_attributes(json_request, ['session_id', 'amount', ])
  223. amount = json_request['amount']
  224. if not isinstance(amount, float) or amount <= 0:
  225. raise BadRequest('Amount must be a number larger than 0')
  226. user_id = model.get_user_id_by_session_id(json_request['session_id'])
  227. model.take_out_personal_loan(user_id, amount)
  228. return {'message': "Successfully took out personal loan"}
  229. def issue_bond(json_request):
  230. check_missing_attributes(json_request, ['session_id', 'name', 'coupon', 'run_time'])
  231. user_id = model.get_user_id_by_session_id(json_request['session_id'])
  232. coupon = json_request['coupon']
  233. if coupon == 'next_mro':
  234. coupon = model.next_mro_interest()
  235. else:
  236. try:
  237. coupon = float(coupon)
  238. except ValueError:
  239. raise BadRequest('Coupon must be a number.')
  240. ownable_name = json_request['name']
  241. if not re.fullmatch(OWNABLE_NAME_PATTERN, ownable_name):
  242. raise BadRequest('Invalid name.')
  243. run_time = json_request['run_time']
  244. if run_time == 'next_mro':
  245. maturity_dt = model.next_mro_maturity()
  246. else:
  247. try:
  248. run_time = int(run_time)
  249. except ValueError:
  250. raise BadRequest('Run-time must be a positive integer number.')
  251. if run_time < 0:
  252. raise BadRequest('Run-time must be a positive integer number.')
  253. maturity_dt = model.current_db_timestamp() + 60 * run_time
  254. model.issue_bond(user_id, ownable_name, coupon, maturity_dt)
  255. return {'message': "Successfully issued bond"}
  256. def repay_loan(json_request):
  257. check_missing_attributes(json_request, ['session_id', 'amount', 'loan_id'])
  258. amount = json_request['amount']
  259. user_id = model.get_user_id_by_session_id(json_request['session_id'])
  260. loan_id = json_request['loan_id']
  261. if not model.user_has_loan_with_id(user_id, loan_id, ):
  262. raise NotFound('Unknown loan ID.')
  263. if amount == 'all':
  264. amount = model.loan_remaining_amount(loan_id)
  265. if amount < 0:
  266. raise BadRequest('You can not repay negative amounts.')
  267. if model.user_available_money(user_id) < amount:
  268. if model.user_has_banking_license(user_id):
  269. raise PreconditionFailed('You do not have enough money. '
  270. 'If you are a bank this also takes into account the minimum reserve you need to keep at the central bank.')
  271. else:
  272. raise PreconditionFailed('You do not have enough money.')
  273. if not model.loan_id_exists(loan_id) or model.loan_recipient_id(loan_id) != user_id:
  274. raise NotFound(f'You do not have a loan with that id.')
  275. loan_volume = model.loan_remaining_amount(loan_id)
  276. if loan_volume < amount:
  277. raise PreconditionFailed(f'You can not repay more than the remaining loan volume of {loan_volume}.')
  278. model.repay_loan(loan_id, amount, known_user_id=user_id)
  279. return {'message': "Successfully repayed loan"}
  280. def server_version(_json_request):
  281. return {'version': version.__version__}
  282. def _before_request(_json_request):
  283. # update tender calendar
  284. model.update_tender_calendar()
  285. for mro_id, maturity_dt, min_interest, mro_dt in model.triggered_mros():
  286. assert maturity_dt > mro_dt
  287. # pay interest rates for loans until this mro
  288. model.pay_loan_interest(until=mro_dt)
  289. # pay interest rates for credits until this mro
  290. model.pay_bond_interest(until=mro_dt)
  291. # pay deposit facility for minimum reserves until this mro
  292. model.pay_deposit_facility(until=mro_dt)
  293. # handle MROs
  294. model.mro(mro_id, maturity_dt, min_interest)
  295. # pay interest rates for loans until current time
  296. model.pay_loan_interest()
  297. # pay interest rates for credits until current time
  298. model.pay_bond_interest()
  299. # pay deposit facility for minimum reserves until current time
  300. model.pay_deposit_facility()